presseportale/app/Models/EmailSuppression.php
Kevin Adametz 8bf40ee37e 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>
2026-09-15 13:48:56 +00:00

71 lines
1.8 KiB
PHP

<?php
namespace App\Models;
use App\Enums\EmailSuppressionReason;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Zustand „nicht erreichbar" je E-Mail-Adresse (Roadmap 1b.6). Geschrieben
* nur über `EmailSuppressionService`, gelesen vom Versand-Gate, der
* Claim-Seite und dem Admin.
*/
class EmailSuppression extends Model
{
protected $fillable = [
'email',
'reason',
'source',
'hard_bounce_count',
'soft_bounce_count',
'first_bounced_at',
'last_bounced_at',
'suppressed_at',
'released_at',
'released_by_user_id',
'note',
];
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'reason' => EmailSuppressionReason::class,
'first_bounced_at' => 'datetime',
'last_bounced_at' => 'datetime',
'suppressed_at' => 'datetime',
'released_at' => 'datetime',
];
}
/**
* @return BelongsTo<User, $this>
*/
public function releasedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'released_by_user_id');
}
/**
* Gesperrt heißt: gesetzt und nicht freigegeben. Eine Freigabe hebt die
* Sperre auf; ein späterer Bounce setzt `suppressed_at` neu und löscht
* die Freigabe wieder — der Verlauf steht im Aktivitätslog.
*/
public function isActive(): bool
{
return $this->suppressed_at !== null && $this->released_at === null;
}
/**
* @param Builder<EmailSuppression> $query
* @return Builder<EmailSuppression>
*/
public function scopeActive(Builder $query): Builder
{
return $query->whereNotNull('suppressed_at')->whereNull('released_at');
}
}