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:
Kevin Adametz 2026-09-15 15:27:09 +00:00
parent 6e159ecf06
commit 4cefdf7f5c
9 changed files with 323 additions and 15 deletions

View file

@ -12,13 +12,16 @@ use Webklex\PHPIMAP\Message;
/**
* Holt Rückläufer aus dem Bounce-Postfach (Roadmap 1b.6, Konzept
* `bounce-mails-2026-09-15.md`). Alle zehn Minuten auf UNSEEN — IDLE braucht
* es bei zwei bis drei Mails am Tag nicht.
* `bounce-mails-2026-09-15.md`). Alle zehn Minuten — IDLE braucht es bei
* zwei bis drei Mails am Tag nicht.
*
* Jede Mail wird nach dem Lesen in einen Ordner verschoben: `Processed`,
* `Ignored` oder `Unparsed`. Verschieben statt Flag: Bricht der Lauf
* dazwischen ab, liest der nächste dieselbe Mail erneut — die Sperre bleibt
* dank Message-ID-Unique trotzdem einmalig.
* dank Message-ID-Unique trotzdem einmalig. Deshalb zählt auch alles in
* der INBOX, nicht nur Ungelesenes: Was ein Mensch dort angesehen oder aus
* `service@` hinübergeschoben hat, trägt das Gelesen-Flag schon und würde
* sonst nie verarbeitet.
*/
class FetchMailBounces extends Command
{
@ -53,7 +56,7 @@ class FetchMailBounces extends Command
throw new \RuntimeException('INBOX nicht gefunden.');
}
$messages = $inbox->query()->whereUnseen()->setFetchBody(true)->limit(max(1, (int) $this->option('limit')))->get();
$messages = $inbox->query()->all()->setFetchBody(true)->limit(max(1, (int) $this->option('limit')))->get();
/** @var Message $message */
foreach ($messages as $message) {