Vier Punkte aus kontakterreichbarkeit-mx-bounces-2026-09-15.md, ohne Veroeffentlichungssperre: - PlausibleEmailDomain: Domain ohne Endung oder mit Tippfehler-TLD ist ein 422 an der Legacy-API, deterministisch ohne DNS. - email_domain_checks + MailDomainChecker ueber DnsResolver-Schnittstelle (RFC 5321 impliziter MX, RFC 7505 Null MX, Unresolved ist kein Befund), Observer an Company/Contact, Job nach Commit, taeglicher Refresh, mail:check-domains fuer den Bestand. Nie eine Adresssperre aus DNS. - Admin > Reports > Einlieferer-Bericht: tote Adressen aus Sperrliste und Domainbefund, automatisch abgelehnte Meldungen mit Grund, Text zum Kopieren und CSV. Versendet nichts. - EmailBounceKind::Policy: 5.7.x wird protokolliert, zaehlt aber nicht auf den Schwellwert. Statuscode entscheidet vor Diagnosetext. Messung vorab: 6 von 877 API-Domains ohne Zustellweg, eine davon fuer 66 der 151 Ruecklaeufer verantwortlich. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
114 lines
4.1 KiB
PHP
114 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\Company;
|
|
use App\Models\Contact;
|
|
use App\Models\EmailDomainCheck;
|
|
use App\Rules\PlausibleEmailDomain;
|
|
use App\Services\Mail\MailDomainChecker;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Collection;
|
|
|
|
/**
|
|
* Prüft die Mail-Domains des Bestands (Kontakterreichbarkeit, Abschnitt 7).
|
|
*
|
|
* Zwei Aufgaben: einmalig den Bestand aufnehmen (`--months=` grenzt auf
|
|
* Firmen mit jüngeren Meldungen ein) und danach täglich die abgelaufenen
|
|
* Befunde erneuern. Der Lauf bewertet nichts und sperrt nichts — er hält
|
|
* fest, was das DNS sagt.
|
|
*/
|
|
class CheckMailDomains extends Command
|
|
{
|
|
protected $signature = 'mail:check-domains
|
|
{--months= : Nur Firmen mit Meldungen aus den letzten N Monaten}
|
|
{--limit=500 : Höchstens so viele Domains je Lauf}
|
|
{--refresh : Auch gültige Befunde neu prüfen}
|
|
{--stale : Nur abgelaufene Befunde erneuern (Scheduler)}';
|
|
|
|
protected $description = 'Prüft die DNS-Zustellwege der hinterlegten E-Mail-Domains.';
|
|
|
|
public function handle(MailDomainChecker $checker): int
|
|
{
|
|
$limit = max(1, (int) $this->option('limit'));
|
|
$domains = $this->option('stale') ? $this->staleDomains($limit) : $this->domainsFromContacts($limit);
|
|
|
|
if ($domains->isEmpty()) {
|
|
$this->info('Nichts zu prüfen.');
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
$counts = [];
|
|
$bar = $this->output->createProgressBar($domains->count());
|
|
|
|
foreach ($domains as $domain) {
|
|
$check = $checker->check($domain, force: (bool) $this->option('refresh') || (bool) $this->option('stale'));
|
|
$counts[$check->state->value] = ($counts[$check->state->value] ?? 0) + 1;
|
|
$bar->advance();
|
|
}
|
|
|
|
$bar->finish();
|
|
$this->newLine(2);
|
|
|
|
foreach ($counts as $state => $count) {
|
|
$this->line(sprintf(' %-16s %d', $state, $count));
|
|
}
|
|
|
|
$broken = EmailDomainCheck::query()->broken()->count();
|
|
$this->info(sprintf('%d Domains geprüft, %d ohne Zustellweg im Bestand.', $domains->count(), $broken));
|
|
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, string>
|
|
*/
|
|
private function staleDomains(int $limit): Collection
|
|
{
|
|
return EmailDomainCheck::query()
|
|
->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '<=', now()))
|
|
->orderBy('expires_at')
|
|
->limit($limit)
|
|
->pluck('domain');
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, string>
|
|
*/
|
|
private function domainsFromContacts(int $limit): Collection
|
|
{
|
|
$months = $this->option('months') !== null ? max(1, (int) $this->option('months')) : null;
|
|
|
|
$companies = Company::query()
|
|
->withoutGlobalScopes()
|
|
->whereNotNull('email')
|
|
->where('email', '!=', '')
|
|
->when($months !== null, fn ($q) => $q->whereHas('pressReleases', fn ($r) => $r->withoutGlobalScopes()->where('press_releases.created_at', '>=', now()->subMonths($months))))
|
|
->pluck('email');
|
|
|
|
$contacts = Contact::query()
|
|
->withoutGlobalScopes()
|
|
->whereNotNull('email')
|
|
->where('email', '!=', '')
|
|
->when($months !== null, fn ($q) => $q->whereHas('company', fn ($c) => $c->withoutGlobalScopes()->whereHas('pressReleases', fn ($r) => $r->withoutGlobalScopes()->where('press_releases.created_at', '>=', now()->subMonths($months)))))
|
|
->pluck('email');
|
|
|
|
$known = EmailDomainCheck::query()
|
|
->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
|
|
->pluck('domain')
|
|
->flip();
|
|
|
|
$domains = [];
|
|
|
|
foreach ($companies->merge($contacts) as $email) {
|
|
$domain = PlausibleEmailDomain::domainOf($email);
|
|
|
|
if ($domain !== null && ! isset($known[$domain])) {
|
|
$domains[$domain] = true;
|
|
}
|
|
}
|
|
|
|
return collect(array_keys($domains))->take($limit)->values();
|
|
}
|
|
}
|