presseportale/app/Mail/Concerns/Unsubscribable.php
Kevin Adametz 35db30e019 mail: adressbezogene Abmeldung fuer Empfaenger ohne Konto (Roadmap 1d.3)
Der Abmeldelink in API-Benachrichtigungen zeigte auf das Konto des
Einlieferers - ein Klick des Pressekontakts haette connektar abgemeldet.
Jetzt traegt der Link die Empfaengeradresse (signiert), der Zustand liegt
als Unsubscribed in der Sperrliste. Anmeldelinks, die die Person selbst
anfordert, gehen weiter durch; harte Bounces sperren alles.

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

67 lines
2.3 KiB
PHP

<?php
namespace App\Mail\Concerns;
use App\Enums\EmailNotificationType;
use App\Models\User;
use Illuminate\Mail\Mailables\Headers;
use Illuminate\Support\Facades\URL;
/**
* Macht eine Benachrichtigungs-Mail abbestellbar: signierter Abmelde-Link
* für den Mail-Footer (ohne Login nutzbar, ohne Ablauf — der Link aus alten
* Mails bleibt gültig) plus die List-Unsubscribe-Header nach RFC 8058, mit
* denen Mail-Clients (Gmail, Apple Mail …) einen nativen
* „Abbestellen"-Button anbieten.
*
* Der Link wird im Konstruktor der Mailable erzeugt (configureUnsubscribe),
* damit er beim Queuen mit serialisiert wird.
*/
trait Unsubscribable
{
public string $unsubscribeUrl;
public EmailNotificationType $notificationType;
protected function configureUnsubscribe(User $user, EmailNotificationType $type): void
{
$this->notificationType = $type;
$this->unsubscribeUrl = URL::signedRoute('email-preferences.unsubscribe', [
'user' => $user->id,
'type' => $type->value,
]);
}
/**
* Abmeldung für Empfänger ohne Konto (Roadmap 1d.3): Pressekontakt oder
* Firmen-E-Mail einer API-Meldung. Der Link trägt die Adresse selbst —
* ein Konto-Link würde den Einlieferer abmelden, nicht den Empfänger.
*/
protected function configureAddressUnsubscribe(string $email, EmailNotificationType $type): void
{
$this->notificationType = $type;
$this->unsubscribeUrl = URL::signedRoute('email-preferences.unsubscribe-address', [
'email' => mb_strtolower(trim($email)),
'type' => $type->value,
]);
}
public function headers(): Headers
{
return new Headers(text: array_filter([
'List-Unsubscribe' => '<'.$this->unsubscribeUrl.'>',
'List-Unsubscribe-Post' => 'List-Unsubscribe=One-Click',
// Bounce-Referenz (Roadmap 1b.6), sofern die Mailable eine hat.
(string) config('bounces.reference_header', 'X-Pk-Ref') => $this->bounceReference(),
]));
}
/**
* Ein DSN bringt die Original-Header zurück; hiermit ordnet der
* Bounce-Parser den Rückläufer dem Datensatz zu. Mailables überschreiben.
*/
public function bounceReference(): ?string
{
return null;
}
}