admin: Mail-Sperrliste, Markierung nicht erreichbarer Newsrooms (Roadmap 1b.6, Schritt 4)

Admin ▸ Mail-Sperrliste mit Freigabe, manueller Sperre, nicht lesbaren
Rückläufern und Breaker-Zustand. Firmenseite markiert gesperrte
Firmen-E-Mail und Pressekontakte. Die Claim-Seite zählt gesperrte
Adressen nicht mehr als Empfänger und erklärt, warum kein Link geht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-15 14:01:07 +00:00
parent f9c97ccf2f
commit e7e12b9436
12 changed files with 426 additions and 9 deletions

View file

@ -42,6 +42,7 @@ class NewsroomClaimController extends Controller
return view('web.newsroom-claim', [
'company' => $company,
'recipients' => $this->claims->maskedRecipients($company),
'unreachable' => $this->claims->isUnreachable($company),
]);
}
@ -64,7 +65,7 @@ class NewsroomClaimController extends Controller
RateLimiter::hit($this->companyKey($company->getKey()), 86400);
if ($this->claims->recipients($company)->isEmpty()) {
return back()->with('newsroom-claim-none', true);
return back()->with($this->claims->isUnreachable($company) ? 'newsroom-claim-unreachable' : 'newsroom-claim-none', true);
}
// Enumeration-sicher: dieselbe Meldung, ob ein Konto deaktiviert ist

View file

@ -6,6 +6,7 @@ use App\Enums\ConversionSource;
use App\Models\Company;
use App\Scopes\PortalScope;
use App\Services\Auth\ContactAccessService;
use App\Services\Mail\EmailSuppressionService;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
@ -26,7 +27,10 @@ use Illuminate\Support\Str;
*/
class NewsroomClaimService
{
public function __construct(private readonly ContactAccessService $contactAccess) {}
public function __construct(
private readonly ContactAccessService $contactAccess,
private readonly EmailSuppressionService $suppressions,
) {}
/**
* Firma zu einem Slug auf diesen Portalen — auch ohne veröffentlichte
@ -48,16 +52,42 @@ class NewsroomClaimService
/**
* Empfänger des Anmeldelinks: die Firmen-E-Mail, sonst die Adressen der
* Pressekontakte. Bewusst nicht beides — ein Klick eines Fremden soll
* nicht fünf Postfächer treffen.
* nicht fünf Postfächer treffen. Gesperrte Adressen (Bounce-Suppression,
* 1b.6) zählen nicht: Ein Newsroom, dessen einzige Adresse tot ist, ist
* nicht beanspruchbar — siehe isUnreachable().
*
* @return Collection<int, string>
*/
public function recipients(Company $company): Collection
{
$candidates = $this->candidateRecipients($company);
if ($candidates === []) {
return collect();
}
$suppressed = $this->suppressions->suppressedAmong($candidates);
return collect(array_values(array_filter($candidates, fn (string $email): bool => ! in_array($email, $suppressed, true))));
}
/**
* Es gäbe Adressen, aber alle sind gesperrt.
*/
public function isUnreachable(Company $company): bool
{
return $this->candidateRecipients($company) !== [] && $this->recipients($company)->isEmpty();
}
/**
* @return list<string>
*/
private function candidateRecipients(Company $company): array
{
$companyEmail = Company::emailKey($company->email);
if ($companyEmail !== null) {
return collect([$companyEmail]);
return [$companyEmail];
}
$emails = [];
@ -70,7 +100,7 @@ class NewsroomClaimService
}
}
return collect(array_values($emails));
return array_values($emails);
}
/**