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>
This commit is contained in:
parent
6c4073a01c
commit
9f5cc59af7
15 changed files with 1650 additions and 181 deletions
88
bug-index-drift.md
Normal file
88
bug-index-drift.md
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# 🔴 KRITISCH — Vorschau/Liste um 1 verschoben (Index-Drift durch SQLITE_BUSY)
|
||||
|
||||
**Der Reader-Aliasing-Fix (6c4073a) war richtig, aber es gibt einen ZWEITEN,
|
||||
unabhaengigen Bug.** Klick auf „iTagPro" zeigt „Orivelle Pen" (die Nachbarzeile).
|
||||
|
||||
## Root Cause — bewiesen, nicht vermutet
|
||||
|
||||
**Zwei Fehler, die sich verketten:**
|
||||
|
||||
1. **6 Mails sind in der mbox-DATEI, aber nicht im `mbox_index`.**
|
||||
Ursache: `PRAGMA busy_timeout` steht immer noch auf **0** (der
|
||||
`rettungslauf-fixes.md`-Brief ist nicht umgesetzt). Beim Rettungslauf gab es
|
||||
pro betroffenem Ordner `SQLITE_BUSY`: `mbox.Append` schrieb den Record in die
|
||||
Datei, der darauffolgende **Index-INSERT scheiterte sofort**. Ergebnis:
|
||||
Datei-Record vorhanden, Index-Zeile fehlt.
|
||||
|
||||
2. **Der plain-Reader liest nach DATEI-POSITION statt nach `file_offset`.**
|
||||
[`06-mbox.go` `readMboxMessageFromIndex`](backend/06-mbox.go): holt zwar die
|
||||
Index-Zeile (mit korrektem `file_offset`), wirft sie fuer plain aber weg
|
||||
(`if !strings.HasSuffix(path, ".zst") { return nil, false, nil }`) und faellt
|
||||
auf `msgs[index]` = Datei-Position zurueck. Ab der fehlenden Index-Zeile ist
|
||||
Position != seq → alles um 1 verschoben.
|
||||
|
||||
**Der Beweis (drift-scan ueber alle 66 Ordner):**
|
||||
```
|
||||
byrne/INBOX Datei 5869 Index 5868 +1 (Rettungslauf errors=1)
|
||||
crystal/INBOX Datei 4874 Index 4873 +1 (errors=2 ->
|
||||
crystal/INBOX.Sent Datei 2146 Index 2145 +1 genau 2 Ordner)
|
||||
gb/INBOX Datei 6675 Index 6674 +1 (errors=1)
|
||||
rb/INBOX Datei 8628 Index 8627 +1 (errors=1)
|
||||
roby/INBOX Datei 2718 Index 2717 +1 (errors=1)
|
||||
```
|
||||
**Drift-Anzahl == SQLITE_BUSY-Fehleranzahl, exakt.** Die 60 fehlerfreien Ordner
|
||||
haben 0 Drift. Und: Lesen ueber `file_offset` liefert fuer gb **4129/4129**
|
||||
Mails korrekt (0 echte Fehler) — die gespeicherten Offsets sind die Wahrheit.
|
||||
|
||||
## Fix (drei Teile, in dieser Reihenfolge)
|
||||
|
||||
### A. `busy_timeout` setzen — PFLICHT, sonst passiert es wieder
|
||||
`ConnectDB` (02-database.go): `PRAGMA busy_timeout = 10000;` neben `journal_mode=WAL`.
|
||||
|
||||
### B. Plain-mbox ueber `file_offset` lesen (der eigentliche Fix)
|
||||
`readMboxMessageFromIndex` **darf fuer plain nicht mehr auf die Positions-
|
||||
Zerlegung zurueckfallen.** Analog zum zstd-Pfad:
|
||||
- Index-Zeile holen → `file_offset` seeken → Record bis zum `file_offset` der
|
||||
**naechsten** Zeile (bzw. EOF) lesen, `TrimRight("\n")`.
|
||||
- Die fragile Neu-Zerlegung `readMboxMessages`+`msgs[index]` **ganz aus dem
|
||||
Vorschau-/Export-/Copy-Pfad entfernen.** Zwei getrennte Zerleger (Writer-
|
||||
Offsets vs. Reader-Resplit), die auseinanderlaufen koennen, sind die Wurzel.
|
||||
- Damit lesen Liste UND Vorschau aus **derselben** Quelle (`mbox_index`) → sie
|
||||
koennen nicht mehr auseinanderlaufen.
|
||||
|
||||
### C. Die 6 verlorenen Index-Zeilen nachziehen — VOLLER Reindex aus der Datei
|
||||
Das bestehende `--reindex` haengt nur ab `indexed_bytes` an — es findet die
|
||||
**mittendrin** fehlenden Mails nicht. Es braucht einen **Full-Rebuild**:
|
||||
- `--reindex --rebuild <konto|all>`: `mbox_index` fuer den Ordner **loeschen**
|
||||
und aus der Datei **komplett neu** aufbauen (jeder Record → seq, file_offset,
|
||||
message_id, subject, date). Danach ist Index-Anzahl == Datei-Records.
|
||||
- **Wichtig — `copied` mitziehen:** Diese 6 Mails stehen auch **nicht** in
|
||||
`copied` (der Busy-Fehler hat auch `MarkCopied` verhindert). Ein Watch-Lauf
|
||||
wuerde sie sonst erneut ins Ziel-Postfach **und** erneut in die mbox kopieren
|
||||
(Dublette in beiden!). Beim Rebuild deshalb pro Record die Message-ID (+ ggf.
|
||||
`body_sha256`) in `copied` mit `mbox_done=1` eintragen.
|
||||
|
||||
## Sekundaerbefunde (mitnehmen, kein Blocker)
|
||||
- **Ungueltiges UTF-8 in `mbox_index.subject`** (z.B. eine Tesla-Newsletter-Mail
|
||||
mit kaputtem Byte) — bricht sogar `sqlite3`-Abfragen. Subject vor dem Speichern
|
||||
nach UTF-8 sanitisieren (ungueltige Bytes ersetzen).
|
||||
- **Envelope-Message-ID vs. Roh-Header:** In ~8 gb-Faellen steht im Index ein
|
||||
`sha256:`-Fallback, obwohl die Datei einen echten `Message-ID`-Header hat.
|
||||
Beim Indizieren zuerst den Roh-Header parsen, bevor auf Hash zurueckgefallen
|
||||
wird — sonst greift Dedup fuer diese Mails nicht.
|
||||
|
||||
## Abnahme
|
||||
1. `go test ./...` gruen; `PRAGMA busy_timeout` = 10000.
|
||||
2. Test, der die Drift faengt: mbox mit N Records schreiben, eine Index-Zeile
|
||||
**in der Mitte** loeschen, dann Vorschau von seq > Luecke abrufen → muss die
|
||||
**richtige** Mail liefern (heute die Nachbarmail).
|
||||
3. `--reindex --rebuild all`, danach **drift-scan == 0** fuer ALLE 66 Ordner
|
||||
(Datei-Records == Index == Listen-Eintraege).
|
||||
4. Watch-Lauf nach dem Rebuild: `copied=0` fuer alle Konten (keine Neukopie der
|
||||
nachgezogenen Mails).
|
||||
5. Voller Sweep (Betreff **und** Datum) ueber gb/INBOX und crystal/INBOX:
|
||||
**0 Abweichungen**.
|
||||
|
||||
Ich (Claude) fahre nach dem Fix den drift-scan ueber alle Ordner UND einen
|
||||
Voll-Sweep gegen `mbox_index` — diesmal ueber ALLE betroffenen Postfaecher,
|
||||
nicht nur eines.
|
||||
Loading…
Add table
Add a link
Reference in a new issue