mail: Kontakterreichbarkeit - Formpruefung, Domainpruefung, Einlieferer-Bericht
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>
This commit is contained in:
parent
4cefdf7f5c
commit
f581e1eb3a
34 changed files with 1985 additions and 25 deletions
114
app/Rules/PlausibleEmailDomain.php
Normal file
114
app/Rules/PlausibleEmailDomain.php
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
|
||||
/**
|
||||
* Deterministische Plausibilitätsprüfung der Domain einer E-Mail-Adresse —
|
||||
* ohne Netz (Kontakterreichbarkeit, Abschnitt 5.1).
|
||||
*
|
||||
* Laravels `email`-Regel folgt der RFC und lässt Domains ohne Punkt zu:
|
||||
* `info@hoerwelt-basel` besteht sie anstandslos. Im Bestand vom 15.09.2026
|
||||
* waren drei der sechs unzustellbaren API-Domains genau so kaputt — ohne TLD
|
||||
* oder mit vertippter TLD. Das ist kein DNS-Befund, sondern ein Formfehler,
|
||||
* und darf deshalb sofort als Validierungsfehler zurückgehen.
|
||||
*
|
||||
* Bewusst NICHT hier: Existenz der Domain oder des Postfachs. Dafür gibt es
|
||||
* die asynchrone Domainprüfung (`MailDomainChecker`) und die Rückläufer.
|
||||
*/
|
||||
class PlausibleEmailDomain implements ValidationRule
|
||||
{
|
||||
/**
|
||||
* Reine Tippfehler-TLDs aus dem Bestand. Die Liste bleibt kurz und
|
||||
* beweisbar — geraten wird hier nicht.
|
||||
*/
|
||||
private const TYPO_TLDS = ['ded', 'dee', 'cmo', 'con', 'comm', 'ne', 'ed'];
|
||||
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
$email = trim((string) $value);
|
||||
|
||||
if ($email === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
$at = strrpos($email, '@');
|
||||
|
||||
if ($at === false || $at === 0 || $at === mb_strlen($email) - 1) {
|
||||
$fail('Die E-Mail-Adresse :attribute ist unvollständig.')->translate();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$domain = self::asciiDomain(mb_substr($email, $at + 1));
|
||||
|
||||
if ($domain === null || ! str_contains($domain, '.')) {
|
||||
$fail('Die Domain der E-Mail-Adresse :attribute ist unvollständig — es fehlt die Endung (z. B. .de).')->translate();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$labels = explode('.', $domain);
|
||||
|
||||
foreach ($labels as $label) {
|
||||
if (preg_match('/^(?!-)[a-z0-9-]{1,63}(?<!-)$/', $label) !== 1) {
|
||||
$fail('Die Domain der E-Mail-Adresse :attribute ist keine gültige Internet-Adresse.')->translate();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$tld = end($labels);
|
||||
|
||||
if (preg_match('/^(xn--[a-z0-9-]+|[a-z]{2,63})$/', $tld) !== 1) {
|
||||
$fail('Die Domain der E-Mail-Adresse :attribute hat keine gültige Endung.')->translate();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_array($tld, self::TYPO_TLDS, true)) {
|
||||
$fail('Die Endung „.:tld" der E-Mail-Adresse :attribute sieht nach einem Tippfehler aus.')->translate(['tld' => $tld]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internationale Domains in die DNS-taugliche Form bringen (Punycode).
|
||||
* Ohne `intl` bleibt die Eingabe unverändert — dann greifen nur die
|
||||
* ASCII-Regeln, was für den Zweck reicht.
|
||||
*/
|
||||
public static function asciiDomain(string $domain): ?string
|
||||
{
|
||||
$domain = mb_strtolower(trim(rtrim($domain, '.')));
|
||||
|
||||
if ($domain === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (function_exists('idn_to_ascii')) {
|
||||
$converted = idn_to_ascii($domain, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
|
||||
|
||||
if (is_string($converted) && $converted !== '') {
|
||||
return $converted;
|
||||
}
|
||||
|
||||
// Nicht konvertierbar (verbotene Zeichen, zu lang) — als
|
||||
// unplausibel behandeln, nicht als gültige ASCII-Domain.
|
||||
return null;
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Domain einer Adresse in DNS-Form, oder null.
|
||||
*/
|
||||
public static function domainOf(?string $email): ?string
|
||||
{
|
||||
$email = trim((string) $email);
|
||||
$at = strrpos($email, '@');
|
||||
|
||||
return $at === false ? null : self::asciiDomain(mb_substr($email, $at + 1));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue