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>
137 lines
5.6 KiB
Markdown
137 lines
5.6 KiB
Markdown
# 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** (1–2 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.
|