E-Mail-Praeferenzen: Opt-out je Mail-Typ + One-Click-Unsubscribe ohne Login

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin Adametz 2026-07-10 17:15:05 +00:00
parent 06d80b255b
commit 51da374173
21 changed files with 746 additions and 9 deletions

View file

@ -0,0 +1,42 @@
<?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,
]);
}
public function headers(): Headers
{
return new Headers(text: [
'List-Unsubscribe' => '<'.$this->unsubscribeUrl.'>',
'List-Unsubscribe-Post' => 'List-Unsubscribe=One-Click',
]);
}
}

View file

@ -2,6 +2,8 @@
namespace App\Mail;
use App\Enums\EmailNotificationType;
use App\Mail\Concerns\Unsubscribable;
use App\Models\PressRelease;
use App\Models\User;
use Illuminate\Bus\Queueable;
@ -17,7 +19,7 @@ use Illuminate\Queue\SerializesModels;
*/
class PressReleaseDepublished extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, Unsubscribable;
public string $showUrl;
@ -28,6 +30,8 @@ class PressReleaseDepublished extends Mailable
$this->showUrl = $user->canAccessAdmin()
? route('admin.press-releases.show', $pressRelease->id)
: route('me.press-releases.show', $pressRelease->id);
$this->configureUnsubscribe($user, EmailNotificationType::PressReleaseDepublished);
}
public function envelope(): Envelope

View file

@ -2,6 +2,8 @@
namespace App\Mail;
use App\Enums\EmailNotificationType;
use App\Mail\Concerns\Unsubscribable;
use App\Models\PressRelease;
use App\Models\User;
use Illuminate\Bus\Queueable;
@ -12,7 +14,7 @@ use Illuminate\Queue\SerializesModels;
class PressReleasePublished extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, Unsubscribable;
public string $showUrl;
@ -33,6 +35,8 @@ class PressReleasePublished extends Mailable
: route('me.press-releases.show', $pressRelease->id);
$this->portalLinks = $pressRelease->publicPortalLinks();
$this->configureUnsubscribe($user, EmailNotificationType::PressReleasePublished);
}
public function envelope(): Envelope

View file

@ -2,6 +2,8 @@
namespace App\Mail;
use App\Enums\EmailNotificationType;
use App\Mail\Concerns\Unsubscribable;
use App\Models\PressRelease;
use App\Models\User;
use Illuminate\Bus\Queueable;
@ -12,7 +14,7 @@ use Illuminate\Queue\SerializesModels;
class PressReleaseRejected extends Mailable
{
use Queueable, SerializesModels;
use Queueable, SerializesModels, Unsubscribable;
public string $editUrl;
@ -24,6 +26,8 @@ class PressReleaseRejected extends Mailable
$this->editUrl = $user->canAccessAdmin()
? route('admin.press-releases.edit', $pressRelease->id)
: route('me.press-releases.edit', $pressRelease->id);
$this->configureUnsubscribe($user, EmailNotificationType::PressReleaseRejected);
}
public function envelope(): Envelope