mail: zentrale Bounce-Suppression mit Versand-Gate und Circuit Breaker (Roadmap 1b.6, Schritte 1–2)
email_suppressions/email_bounces als Zustand und Ereignis-Log; ein MessageSending-Listener streicht gesperrte Empfänger aus JEDER ausgehenden Mail und setzt den Return-Path. Zweiter Aufnahmepfad: SMTP-Ablehnungen im Worker — 5.1.x sperrt den Empfänger, Relay-Ablehnungen zählen für den Circuit Breaker, der den Versand anhält statt gesunde Adressen zu sperren. X-Pk-Ref-Header auf den Mails, API-Meldungen fallen auf die Firmen-E-Mail zurück, der Bounce-Import schreibt in die zentrale Liste. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
85b039e739
commit
8bf40ee37e
25 changed files with 1312 additions and 4 deletions
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Providers;
|
||||
|
||||
use App\Contracts\NewsletterSyncClient;
|
||||
use App\Exceptions\MailDeliveryPausedException;
|
||||
use App\Helpers\ThemeHelper;
|
||||
use App\Http\Middleware\BlockAdminDuringImpersonation;
|
||||
use App\Http\Middleware\EnsureUserIsAdmin;
|
||||
|
|
@ -10,8 +11,10 @@ use App\Http\Middleware\EnsureUserIsCustomer;
|
|||
use App\Http\Middleware\LogSlowAdminRequests;
|
||||
use App\Http\Middleware\SetEdition;
|
||||
use App\Listeners\ActivateUserAfterVerification;
|
||||
use App\Listeners\GateOutgoingMail;
|
||||
use App\Listeners\RecordAuthActivity;
|
||||
use App\Listeners\RecordMailActivity;
|
||||
use App\Listeners\RecordSmtpFailure;
|
||||
use App\Listeners\SyncCompanyMembershipsOnLogin;
|
||||
use App\Models\AdminPreset;
|
||||
use App\Models\Category;
|
||||
|
|
@ -41,6 +44,7 @@ use Illuminate\Auth\Notifications\ResetPassword;
|
|||
use Illuminate\Auth\Notifications\VerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Events\QueryExecuted;
|
||||
use Illuminate\Mail\Events\MessageSending;
|
||||
use Illuminate\Mail\Events\MessageSent;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Queue\Events\JobFailed;
|
||||
|
|
@ -123,6 +127,17 @@ class AppServiceProvider extends ServiceProvider
|
|||
// laufen auf der `classification`-Queue — ein stiller Stau in
|
||||
// `failed_jobs` würde jede Einreichung in der Prüfung hängen lassen.
|
||||
Queue::failing(function (JobFailed $event): void {
|
||||
// Bounce-Pfad 2: SMTP-Ablehnung im Worker → Sperre bzw. Circuit
|
||||
// Breaker (Roadmap 1b.6). Läuft VOR dem Alert, damit der Breaker
|
||||
// seinen eigenen, einmaligen Alert setzen kann.
|
||||
app(RecordSmtpFailure::class)->record($event);
|
||||
|
||||
// Der Breaker hat den Versand angehalten: Das ist EIN Alert (beim
|
||||
// Öffnen), nicht einer je liegengebliebener Mail.
|
||||
if ($event->exception instanceof MailDeliveryPausedException) {
|
||||
return;
|
||||
}
|
||||
|
||||
OperationalAlert::send("Queue-Job fehlgeschlagen: {$event->job->resolveName()}", [
|
||||
'connection' => $event->connectionName,
|
||||
'queue' => $event->job->getQueue(),
|
||||
|
|
@ -139,6 +154,10 @@ class AppServiceProvider extends ServiceProvider
|
|||
Event::listen(Registered::class, [RecordAuthActivity::class, 'recordRegistered']);
|
||||
Event::listen(Login::class, [RecordAuthActivity::class, 'recordLogin']);
|
||||
|
||||
// Das eine Gate vor jeder ausgehenden Mail: Suppression, Return-Path,
|
||||
// Circuit Breaker (Roadmap 1b.6). Gibt false zurück → Mail geht nicht raus.
|
||||
Event::listen(MessageSending::class, [GateOutgoingMail::class, 'gate']);
|
||||
|
||||
// Jede tatsächlich versendete E-Mail landet ebenfalls im Verlauf.
|
||||
Event::listen(MessageSent::class, [RecordMailActivity::class, 'recordSent']);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue