*/ 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, ]); } }