presseportale/app/Mail/PressReleasePublished.php
Kevin Adametz 375138679d web: Erstquelle-Absatz in der Veröffentlichungsmail, vierte Transparenzseite „kostenlos veröffentlichen" (Anhang 1: 1d, 2.4, E7)
Meldungen, die über die API eingegangen sind, bekommen in der
Veröffentlichungsmail einen Absatz zum Unterschied Erstquelle/Kopie —
technische Tatsache, ohne Preis und Angebot; direkt eingereichte nicht.

/pressemitteilung-kostenlos-veroeffentlichen beantwortet die Suche nach
„kostenlos" ehrlich: kein kostenloser Weg (E7), was kostenlose Portale
leisten, warum Links aus Presseportalen kaum tragen (auch bei uns
sponsored nofollow), warum Annahme ohne Gegenwert und Prüfung sich
ausschließen, wer am Ende zahlt. Die Liste kostenloser Portale ist in
config/transparency.php vorbereitet und bleibt leer, bis geprüfte
Einträge mit Prüfdatum vorliegen.

Unter-Navigation, Hilfe, Sitemaps, llms.txt und die Modelltabelle der
Kostenseite verlinken die neue Seite. Anhang 1 und Protokoll Teil 6.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 14:22:05 +00:00

135 lines
4.8 KiB
PHP

<?php
namespace App\Mail;
use App\Enums\ConversionSource;
use App\Enums\EmailNotificationType;
use App\Mail\Concerns\Unsubscribable;
use App\Models\PressRelease;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class PressReleasePublished extends Mailable
{
use Queueable, SerializesModels, Unsubscribable;
public string $showUrl;
/**
* Öffentliche Detailseiten je Portal, auf dem die PM erschienen ist
* (bei Portal::Both beide Spokes).
*
* @var list<array{label: string, url: string}>
*/
public array $portalLinks;
/**
* Name für die Anrede — bei API-Einlieferungen der Pressekontakt bzw.
* die Firma, nie der Kontoname des API-Einlieferers.
*/
public string $recipientName;
/**
* „Diesen Newsroom verwalten" (Roadmap 1b.4, Entscheidung E4 vom
* 15.09.2026): Hinweis am Ende der Mail, kein Angebot. Null ohne Firma.
*/
public ?string $manageNewsroomUrl = null;
public function __construct(
public readonly User $user,
public readonly PressRelease $pressRelease,
?string $recipientName = null,
?string $recipientEmail = null,
/**
* Über einen Verteiler (API) eingegangen: dann erklärt die Mail den
* Unterschied Erstquelle/Kopie (Roadmap 1d, Anhang 17.09.2026) —
* eine technische Tatsache, kein Angebot.
*/
public readonly bool $viaSyndication = false,
) {
$this->recipientName = $recipientName ?? $user->name;
$this->showUrl = self::tagged(
$user->canAccessAdmin()
? route('admin.press-releases.show', $pressRelease->id)
: route('me.press-releases.show', $pressRelease->id),
$pressRelease->id,
);
$this->portalLinks = array_map(
fn (array $link): array => [
...$link,
'url' => self::tagged($link['url'], $pressRelease->id),
],
$pressRelease->publicPortalLinks(),
);
$this->manageNewsroomUrl = $this->manageNewsroomUrl($pressRelease);
// Geht die Mail an einen Empfänger ohne Konto (API-Meldung), meldet
// der Abmeldelink diese Adresse ab — nicht das Konto des Einlieferers.
if ($recipientEmail !== null && mb_strtolower(trim($recipientEmail)) !== mb_strtolower((string) $user->email)) {
$this->configureAddressUnsubscribe($recipientEmail, EmailNotificationType::PressReleasePublished);
} else {
$this->configureUnsubscribe($user, EmailNotificationType::PressReleasePublished);
}
}
/**
* Verwaltungsseite des Newsrooms auf dem Portal, auf dem die Meldung
* erschienen ist (bei beiden: presseecho). Nur für Firmen, die noch als
* Newsroom existieren — nicht 410-entfernt.
*/
private function manageNewsroomUrl(PressRelease $pressRelease): ?string
{
$company = $pressRelease->company;
if ($company === null || ! $company->slug || $company->isGone() || $this->portalLinks === []) {
return null;
}
$host = rtrim((string) parse_url($this->portalLinks[0]['url'], PHP_URL_SCHEME).'://'.parse_url($this->portalLinks[0]['url'], PHP_URL_HOST), '/');
$edition = in_array($pressRelease->language, ['de', 'en'], true) ? $pressRelease->language : 'de';
return self::tagged(sprintf('%s/%s/newsroom/%s/verwalten', $host, $edition, $company->slug), $pressRelease->id);
}
/**
* Bounce-Referenz: Ein DSN bringt die Original-Header zurück, so ordnet
* der Parser den Rückläufer der Meldung zu (Roadmap 1b.6). Gesetzt über
* den gemeinsamen headers() des Unsubscribable-Traits.
*/
public function bounceReference(): string
{
return sprintf('pm:%d:company:%s', $this->pressRelease->id, $this->pressRelease->company_id ?? '-');
}
public function envelope(): Envelope
{
return new Envelope(
subject: 'Ihre Pressemitteilung wurde veröffentlicht: '.$this->pressRelease->title,
);
}
public function content(): Content
{
return new Content(view: 'emails.press-release.published');
}
/**
* Hängt Herkunft und Meldung an einen Mail-Link (Roadmap 0.4).
*
* Ohne diesen Schritt weiß die Auswertung am Ende, dass jemand gebucht hat,
* aber nicht, ob er aus dieser Mail kam — und genau das ist die Frage.
*/
public static function tagged(string $url, int $pressReleaseId): string
{
return $url.(str_contains($url, '?') ? '&' : '?').http_build_query([
'src' => ConversionSource::MailPublish->value,
'pr' => $pressReleaseId,
]);
}
}