mail: Bounce-Abruf liest RFC 6533, All-Inkl-Freitext und ganze INBOX
Befund aus 151 echten Rücklaeufern: 2 SmarterMail-DSNs (message/global-delivery-status) und 2 Freitext-Hinweise des eigenen Relays (X-KASMail-Error, formal auto-replied) fielen durch. Policy- und Quota-Codes zaehlen weich statt hart. Abruf ohne UNSEEN-Filter, weil das Verschieben die Zustandsmarke ist. Dazu Schutz in tests/TestCase.php: Suite bricht ab, wenn ein Config-Cache sie auf die Dev-Datenbank umlenkt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6e159ecf06
commit
4cefdf7f5c
9 changed files with 323 additions and 15 deletions
|
|
@ -20,6 +20,12 @@ use Illuminate\Support\Carbon;
|
|||
* Ablehnungen, alte MTAs mit Freitext), liefert null. Das wandert in den
|
||||
* Ordner `Unparsed` und vor einen Menschen — zwei Mails pro Woche von Hand
|
||||
* sind akzeptabel, eine falsch gesperrte Adresse nicht.
|
||||
*
|
||||
* Zwei bewusste Ausnahmen: die UTF-8-Variante nach RFC 6533
|
||||
* (`message/global-delivery-status`, z. B. SmarterMail) und das
|
||||
* Freitextformat des eigenen All-Inkl-Relays („anti spam system at host …
|
||||
* list of non existent recipients"), das mit `X-KASMail-Error` eindeutig
|
||||
* erkennbar ist — 2 von 151 Rückläufern im Bestand vom 15.09.2026.
|
||||
*/
|
||||
class DsnParser
|
||||
{
|
||||
|
|
@ -36,18 +42,14 @@ class DsnParser
|
|||
foreach ($parts as $part) {
|
||||
$type = strtolower((string) ($part['headers']['content-type'] ?? ''));
|
||||
|
||||
if ($status === null && str_starts_with($type, 'message/delivery-status')) {
|
||||
if ($status === null && (str_starts_with($type, 'message/delivery-status') || str_starts_with($type, 'message/global-delivery-status'))) {
|
||||
$status = $part['body'];
|
||||
} elseif ($original === null && (str_starts_with($type, 'message/rfc822') || str_starts_with($type, 'text/rfc822-headers'))) {
|
||||
} elseif ($original === null && preg_match('#^(message/rfc822|message/global|text/rfc822-headers)#', $type) === 1) {
|
||||
$original = $part['body'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($status === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$recipients = $this->recipientsFrom($status);
|
||||
$recipients = $status !== null ? $this->recipientsFrom($status) : $this->recipientsFromKasNotice($headers, $topBody);
|
||||
|
||||
if ($recipients === []) {
|
||||
return null;
|
||||
|
|
@ -76,6 +78,14 @@ class DsnParser
|
|||
|
||||
$autoSubmitted = strtolower((string) ($headers['auto-submitted'] ?? ''));
|
||||
$subject = strtolower((string) ($headers['subject'] ?? ''));
|
||||
$from = strtolower((string) ($headers['from'] ?? ''));
|
||||
|
||||
// Ein Zustellsystem antwortet formal auch „auto-replied" — sein
|
||||
// Rückläufer ist aber nie eine Abwesenheitsnotiz.
|
||||
if (preg_match('/mailer-daemon|postmaster@/', $from) === 1
|
||||
|| preg_match('/undeliver|returned to sender|delivery (status|failure)|unzustellbar/', $subject) === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ($autoSubmitted !== '' && $autoSubmitted !== 'no')
|
||||
|| isset($headers['x-autoreply'])
|
||||
|
|
@ -183,6 +193,39 @@ class DsnParser
|
|||
return $recipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Freitext-Rückläufer des All-Inkl-Relays: kein DSN, aber ein festes
|
||||
* Format mit Adressliste. Nur bei gesetztem `X-KASMail-Error` und dem
|
||||
* wörtlichen Einleitungssatz — alles andere bleibt Sache eines Menschen.
|
||||
*
|
||||
* @param array<string, string> $headers
|
||||
* @return list<array{email: string, action: string, status: string|null, diagnostic: string|null}>
|
||||
*/
|
||||
private function recipientsFromKasNotice(array $headers, string $body): array
|
||||
{
|
||||
if (! isset($headers['x-kasmail-error'])
|
||||
|| preg_match('/list of non existent recipients:\s*\n(.+?)(?:\n\s*\n|\z)/is', $body, $m) !== 1) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$recipients = [];
|
||||
|
||||
foreach (preg_split('/\s+/', trim($m[1])) ?: [] as $candidate) {
|
||||
$email = EmailSuppressionService::normalise($candidate);
|
||||
|
||||
if ($email !== null) {
|
||||
$recipients[] = [
|
||||
'email' => $email,
|
||||
'action' => 'failed',
|
||||
'status' => '5.1.1',
|
||||
'diagnostic' => 'All-Inkl anti spam: recipient reported as non existent',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $recipients;
|
||||
}
|
||||
|
||||
private function date(?string $value): ?Carbon
|
||||
{
|
||||
if ($value === null) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue