Mail-Graveyard/rettungslauf-fixes.md
DonVoo a7ffab1182 Correct Nachzuegler finding: they were source-side exact duplicates
Der in rettungslauf-fixes.md beschriebene Befund "~109 byte-verschiedene
Mails mit gleicher Message-ID" war eine HYPOTHESE, die faelschlich als
Befund formuliert wurde.

Messung nach Einbau von body_sha256 (petarus/INBOX.Sent): 518 Quell-Mails,
davon nur 463 eindeutige Identitaeten - exakt der Archivbestand. Die 55
"fehlenden" sind bitgenaue Duplikate an der Quelle (eine Identitaet 10x,
zwei je 6x, fuenf je 4x - Re-Import-Signatur). Es fehlte nie etwas.

Der body_sha256-Schluessel war dennoch richtig: er ist der Beweis der
Bitgleichheit. Waeren die Mails verschieden gewesen, waeren sie beim
kontrollierten Nachlauf archiviert worden.

Lehre im Brief ergaenzt: Eine Zaehlung taeuscht in beide Richtungen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 14:53:26 +02:00

6.7 KiB
Raw Blame History

Codex-Brief — Fixes aus dem echten Rettungslauf (14.07., 15 GB / 58k Mails)

Der Lauf gegen die 18 echten @dr-gold.de-Postfaecher hat vier Schwaechen freigelegt, die im Testbetrieb nie sichtbar waren. Fix 1 und 2 sind Pflicht, bevor wir den Nachschlag fahren — sonst reisst es an derselben Stelle wieder.

Kontext: Der Quell-Hoster verliert derzeit Mails. Das lokale mbox-Archiv ist das Sicherheitsnetz. Alles, was das Archiv gefaehrdet, ist ein Notfall.


1. 🔴 busy_timeout = 0 — SQLite scheitert sofort statt zu warten

Befund: PRAGMA busy_timeout steht auf 0. Bei Lock-Konflikten (Watcher + Web-App + Migration auf derselben DB) bricht die Abfrage sofort ab:

mbox index error <id>: database is locked (5) (SQLITE_BUSY)

Sechsmal im Rettungslauf passiert.

Fix: In ConnectDB (02-database.go) neben journal_mode=WAL setzen:

PRAGMA busy_timeout = 10000;   -- 10s warten statt sofort scheitern

Einzeiler, grosse Wirkung.


2. 🔴 Das Archiv haengt am Ziel — und das ist verkehrt herum

Der schwerste Befund. Heutige Reihenfolge in migrateAccount:

AlreadyCopied -> dst.Append (All-Inkl) -> mbox.Append (lokal) -> MarkCopied
                 ^^^^^^^^^^ scheitert das hier, wird per `return nil` abgebrochen
                            -> die Mail landet AUCH NICHT im lokalen Archiv

Real passiert bei petarus: Die Ziel-Verbindung starb mitten im Lauf (use of closed network connection). Danach schlug jedes EnsureFolder/Append fehl → ~620 von 1135 Mails wurden weder ins Ziel noch ins Archiv geschrieben. Das Sicherheitsnetz hat genau dann gerissen, als es gebraucht wurde.

Das Archiv muss zuerst kommen und vom Ziel unabhaengig sein. Dafuer braucht copied getrennte Zustaende — sonst erzeugt ein Retry mbox-Dubletten:

ALTER TABLE copied ADD COLUMN mbox_done   INTEGER NOT NULL DEFAULT 1;
ALTER TABLE copied ADD COLUMN target_done INTEGER NOT NULL DEFAULT 1;
-- Default 1: Bestandszeilen sind beides-fertig, das stimmt.

Neuer Ablauf:

1. Zeile vorhanden und mbox_done=1 und target_done=1  -> ueberspringen
2. mbox.Append  (ZUERST — das Archiv ist das Sicherheitsnetz)
      scheitert -> Fehler zaehlen, Mail ueberspringen, nichts markieren
3. Zeile schreiben/aktualisieren: mbox_done=1, target_done=0
4. dst.Append   (Ziel)
      OK       -> target_done=1
      scheitert -> Fehler zaehlen, WEITERMACHEN. Die Mail IST archiviert.

Was das loest:

  • Ein sterbendes Ziel kostet nie wieder eine Archiv-Kopie.
  • Ein zweiter Lauf sieht mbox_done=1, target_done=0ueberspringt den mbox-Append (keine Dublette!) und holt nur das Ziel nach.

MarkCopied bleibt sinngemaess das Letzte je Stufe. Ein Absturz zwischen mbox-Write und Zeilen-Update kopiert die Mail beim naechsten Lauf erneut ins Archiv — dieselbe bewusste Entscheidung wie bisher, nur jetzt pro Stufe.


3. 🟠 Kein Reconnect zum Ziel

Bei einem 2-GB-Postfach haelt All-Inkl die IMAP-Sitzung nicht durch. Stirbt sie, scheitert jede weitere Operation — der Rest des Kontos faellt weg.

Fix: Verbindungsfehler erkennen (use of closed network connection, EOF, broken pipe) und die Ziel-Verbindung neu aufbauen (12 Versuche mit kurzer Pause). Gelingt das nicht:

  • nicht abbrechen, sondern fuer den Rest des Kontos im Archiv-Only-Modus weiterlaufen (dank Fix 2 ist das gefahrlos — die Mails landen sicher im mbox, target_done=0), und
  • am Ende laut melden: ZIEL NICHT ERREICHBAR — N Mails nur archiviert, Nachlauf noetig.

Lieber ein vollstaendiges Archiv mit offenem Ziel-Nachlauf als beides halb.


4. 🟠 Gleiche Message-ID im selben Ordner → Mail wird still verworfen

NACHTRAG 16.07. — dieser Befund war eine HYPOTHESE, keine Messung. Nach dem Einbau von body_sha256 und einem kontrollierten Nachlauf steht fest: Die vermuteten „~109 byte-verschiedenen Mails" existieren nicht. Messung an petarus/INBOX.Sent: 518 Quell-Mails, aber nur 463 eindeutige Identitäten (message_id + kanonischer body_sha256) — exakt der Archivbestand. Die 55 „fehlenden" sind bitgenaue Duplikate an der Quelle (eine Identität liegt 10×, zwei je 6×, fünf je 4× vor — Re-Import-Signatur). Das Archiv hat korrekt zusammengefasst; es fehlte nie etwas.

Der Fix war trotzdem richtig: body_sha256 ist der Beweis der Bitgleichheit. Wären die Mails verschieden gewesen, wären sie beim Nachlauf archiviert worden. Erst der Schlüssel machte die Aussage überhaupt prüfbar.

Lehre: Eine Zählung täuscht in beide Richtungen — gleiche Anzahl kann verschiedene Mails verbergen, und eine Abweichung bedeutet nicht automatisch fehlende Mail. Belastbar ist nur der Identitäts-Vergleich.

Ursprünglicher (vermuteter) Befund: Bei mehreren Konten ist copied < total ohne Fehler (neubauer 1, simsek 5, palamari 8, gb u. a. — zusammen ~25 Mails). Vermutete Ursache: Die gleiche Message-ID kommt im selben Ordner zweimal vor (Re-Import, kaputte Mailer), und der Dedup wirft die zweite weg.

Fuer ein Beweis-Archiv ist das nicht hinnehmbar: Zwei Mails mit derselben Message-ID koennen unterschiedlichen Inhalt haben. Wir duerfen nie still eine byte-verschiedene Mail verwerfen.

Fix: copied um body_sha256 erweitern, Unique-Key auf (account_id, folder, message_id, body_sha256).

  • Gleiche ID und gleiche Bytes → echte Dublette, ueberspringen (wie bisher).
  • Gleiche ID, andere Bytes → andere Mail, archivieren.
  • Bestandszeilen: body_sha256 leer lassen und beim Vergleich als Wildcard behandeln (kein Reindex-Zwang, keine Re-Kopie des Bestands).

5. 🟢 Phantom-Ordner tolerieren

LIST meldet INBOX.Posteingang, SELECT sagt NO Mailbox doesn't exist. Kaputte Quelle (passt zum Hoster-Problem). Heute: Fehler. Kuenftig: Warnung, Ordner ueberspringen, Lauf sauber weiterfuehren — das ist kein Fehler unseres Tools.


Abnahme

  1. go test ./... gruen.
  2. PRAGMA busy_timeout → 10000.
  3. Der entscheidende Test — Ziel-Ausfall simulieren: Migration eines Testkontos starten, waehrend des Laufs das Ziel unerreichbar machen (z. B. falscher dst_host/Port). Erwartung:
    • Alle Quell-Mails landen trotzdem vollstaendig im lokalen mbox.
    • target_done=0 fuer diese Mails.
    • Lauf meldet laut „Ziel nicht erreichbar, Nachlauf noetig".
    • Zweiter Lauf mit erreichbarem Ziel: kopiert nichts ins mbox (keine Dublette!) und holt nur das Ziel nach. Das ist die Regression, die uns bei petarus ~620 Mails gekostet haette.
  4. Nachschlag petarus (--folders ordnerweise) → alle 1135 Mails im Archiv.

Ich (Claude) fahre danach den Verifikationslauf: Quell-Anzahl vs. Archiv-Anzahl je Konto, und ziehe alle Luecken nach.