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>
This commit is contained in:
Kevin Adametz 2026-09-16 14:35:47 +00:00
parent 13716b9c06
commit 9380937562
15 changed files with 362 additions and 20 deletions

View file

@ -6,6 +6,8 @@ use App\Enums\ActivityType;
use App\Enums\EmailNotificationType;
use App\Models\User;
use App\Services\ActivityLog;
use App\Services\Company\NewsroomClaimService;
use App\Services\Mail\EmailSuppressionService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@ -89,6 +91,75 @@ class EmailUnsubscribeController extends Controller
]);
}
/**
* Adressbezogene Abmeldung (Roadmap 1d.3): Empfänger ohne Konto. Die
* Sperrliste hält den Zustand; Anmeldelinks, die die Person selbst
* anfordert, gehen weiter durch (GateOutgoingMail).
*/
public function showAddress(Request $request, EmailSuppressionService $suppressions): View
{
$notificationType = $this->resolveType((string) $request->query('type', ''));
$email = EmailSuppressionService::normalise((string) $request->query('email', ''));
if (! $request->hasValidSignature() || $email === null) {
return view('web.email-unsubscribe', ['state' => 'invalid', 'notificationType' => $notificationType]);
}
$this->unsubscribeAddress($suppressions, $email, $notificationType, 'link');
return view('web.email-unsubscribe', [
'state' => 'unsubscribed',
'notificationType' => $notificationType,
'address' => NewsroomClaimService::mask($email),
'addressMode' => true,
'resubscribeUrl' => URL::signedRoute('email-preferences.resubscribe-address', ['email' => $email, 'type' => $notificationType->value]),
]);
}
public function oneClickUnsubscribeAddress(Request $request, EmailSuppressionService $suppressions): Response
{
abort_unless($request->hasValidSignature(), 403);
$email = EmailSuppressionService::normalise((string) $request->query('email', ''));
abort_if($email === null, 404);
$this->unsubscribeAddress($suppressions, $email, $this->resolveType((string) $request->query('type', '')), 'one-click');
return response('Sie wurden erfolgreich abgemeldet.');
}
public function resubscribeAddress(Request $request, EmailSuppressionService $suppressions): View
{
abort_unless($request->hasValidSignature(), 403);
$email = EmailSuppressionService::normalise((string) $request->query('email', ''));
abort_if($email === null, 404);
$notificationType = $this->resolveType((string) $request->query('type', ''));
if ($suppressions->resubscribe($email)) {
$this->activityLog->record(
ActivityType::EmailUnsubscribe,
sprintf('%s hat die Abmeldung zurückgenommen.', $email),
meta: ['email' => $email, 'action' => 'resubscribe'],
);
}
return view('web.email-unsubscribe', ['state' => 'resubscribed', 'notificationType' => $notificationType, 'addressMode' => true]);
}
private function unsubscribeAddress(EmailSuppressionService $suppressions, string $email, EmailNotificationType $notificationType, string $method): void
{
if ($suppressions->isSuppressed($email)) {
return;
}
$suppressions->unsubscribe($email, $method);
$this->activityLog->record(
ActivityType::EmailUnsubscribe,
sprintf('%s hat sich von Benachrichtigungen abgemeldet (%s).', $email, $notificationType->unsubscribeLabel()),
meta: ['email' => $email, 'type' => $notificationType->value, 'action' => 'unsubscribe', 'method' => $method],
);
}
private function optOut(User $user, EmailNotificationType $notificationType, string $method): void
{
if (! $user->wantsEmailNotification($notificationType)) {