Mail-Graveyard/bug-index-drift.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

4.8 KiB

🔴 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: 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.