Mail-Graveyard/rettungslauf-fixes.md
DonVoo 9f5cc59af7 Stabilize mail identity: canonical hash, index drift, HTML panic
Behebt drei Fehler derselben Klasse: an jeder Stelle wurde derselbe Wert
zweimal berechnet, statt einmal berechnet und weitergereicht - und die
beiden Berechnungen liefen auseinander.

1. Instabiler Ersatzschluessel (bug-hashkey-instabil.md)
   Mails ohne Message-ID bekamen sha256 ueber die IMAP-Rohbytes, der
   Reindex hashte dieselbe Mail ueber die mbox-gespeicherten Bytes
   (>From-Quoting, andere Zeilenenden) -> zwei Schluessel, 4.024
   Doppel-Eintraege in copied.
   Fix: canonicalMessageBytes() bringt beide Seiten auf eine Form
   (>From zurueckdrehen, CRLF->LF, Trailing-Newlines weg). Alle Pfade
   (Index, Migration, Viewer, Dedup) nutzen dieselbe Funktion.
   body_sha256 in copied + mbox_index, UNIQUE erweitert.

2. Index-Drift (bug-index-drift.md)
   Der plain-mbox-Reader las nach Datei-Position statt nach dem
   gespeicherten file_offset. Weil SQLITE_BUSY (busy_timeout=0) je
   Ordner einen Index-Eintrag verschluckt hatte, war ab dieser Luecke
   alles um 1 verschoben: Klick auf Mail X zeigte Mail X+1.
   Betroffen genau die 6 Ordner mit Busy-Fehler beim Rettungslauf.
   Fix: busy_timeout=10000, Lesen ueber file_offset, --reindex --rebuild.

3. HTML-Vorschau-Panic (bug-htmltotext-panic.md)
   replaceCaseInsensitive/stripHTMLBlock indizierten mit Offsets aus
   strings.ToLower(s) in s - ToLower ist nicht byte-laengen-erhaltend
   (z.B. U+0130). ~0,1% der Mails brachten die Vorschau zum Absturz.
   Fix: asciiFoldIndex() sucht direkt auf den Original-Bytes; zusaetzlich
   Rohtext-Fallback, damit keine archivierte Mail unsichtbar wird.

Weiter: Archiv-zuerst-Reihenfolge (mbox_done/target_done) - ein
sterbendes Ziel kostet keine Archiv-Kopie mehr; Dedup vergleicht
zusaetzlich den Inhalt und schuetzt byte-verschiedene Varianten.

WICHTIG: Die 62.073 alten Alias-Zeilen in copied bleiben bewusst
erhalten. Sie sehen wie Muell aus, sind aber die Zeilen, auf die der
alte Schluessel matcht - ein Loeschen wuerde Mails erneut in fremde
Postfaecher kopieren.

Verifiziert: go test ./... gruen; 58.051 Archivmails, 0 ohne
copied-Zeile; Drift 0 ueber alle 69 Ordner; 2.000 HTTP-Stichproben
ueber 8 Ordner: 0 falsche Zuordnung, 0 Panics; Watcher 220 + 60 Mails
archived=0 target=0 errors=0. Live als Image fix-identity2-20260716.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 13:22:27 +02:00

137 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```sql
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:
```sql
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=0` → **ueberspringt 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
**Befund:** Bei mehreren Konten ist `copied < total` **ohne** Fehler
(neubauer 1, simsek 5, palamari 8, gb u. a. — zusammen ~25 Mails). 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.