Mail-Graveyard/bug-hashkey-instabil.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.2 KiB

🟠 Instabiler Ersatzschlüssel: 4.024 Doppel-Einträge in copied

Kein Datenverlust, keine Dubletten in Postfächern. Aber die Buchhaltung stimmt nicht — und beim Aufräumen lauert eine Falle (siehe „⚠️ NICHT TUN").

Befund

Datei-Records: 58.051  ═  mbox_index: 58.051     ✓ (Drift 0)
copied:        62.073                            ← 4.022 mehr

copied-Zeilen ohne passenden mbox_index-Eintrag: 4.024, davon 4.021 mit sha256:-Schlüssel. Verteilung exakt auf die Konten mit --reindex --rebuild: gb +2533, rb +719, byrne +564, crystal +112, roby +92. (gb hatte gemessen 2.537 Mails ohne Message-ID — passt auf ~4 genau.)

Ursache

Mails ohne Message-ID-Header bekommen sha256:<hash> als Ersatzschlüssel. Der Hash wird aber über unterschiedliche Bytes gebildet:

Pfad hasht
Migration (07-migrate.go) die IMAP-Rohbytes der Mail
Reindex/Rebuild (06-mbox.go) den mbox-Record aus der Datei

Die mbox-Fassung ist nicht byte-gleich mit der IMAP-Fassung: Der Writer setzt >From -Quoting und normalisiert Zeilenenden. → anderer Hash → anderer Schlüssel → dieselbe Mail steht zweimal in copied.

Betroffen sind alle 6.623 Mails ohne Message-ID (so viele hat mbox_index mit sha256:-Schlüssel).

⚠️ NICHT TUN: die „Waisen" einfach löschen

Die 4.024 Waisen sind nicht der Müll — sie sind die Zeilen, auf die der Migrationspfad tatsächlich matcht (er rechnet den IMAP-Hash aus). Die vom Rebuild ergänzten Zeilen (mbox-Hash) sind die redundanten.

Werden die Waisen gelöscht, hält die Migration diese 4.021 Mails für unkopiert und schreibt sie erneut ins Ziel-Postfach → echte Dubletten in fremden Postfächern. Genau der Schaden, den wir vermeiden wollen.

Aus demselben Grund darf die Schlüsselberechnung nicht einfach umgestellt werden: Für Konten ohne Rebuild (kolmer, palamari, colak, …) existiert nur der IMAP-Hash. Ein Wechsel auf den mbox-Hash lässt dort alle Message-ID-losen Mails als „neu" erscheinen → Massen-Neukopie.

Fix (in dieser Reihenfolge, sonst knallt es)

  1. Schlüssel vereinheitlichen: Der Ersatzschlüssel muss in beiden Pfaden über dieselben Bytes gehen. Sauberste Wahl: über die mbox-normalisierte Fassung (also das, was tatsächlich im Archiv steht) — dann liefern Migration und Reindex/Rebuild dauerhaft denselben Wert, auch nach jedem künftigen Rebuild. → In 07-migrate.go den Hash nach der mbox-Normalisierung bilden, nicht über die IMAP-Rohbytes.
  2. Einmalige copied-Migration (zwingend zusammen mit 1., nicht danach): Für jedes Konto/Ordner copied aus mbox_index neu ableiten (message_id aus dem Index, mbox_done=1), target_done aus dem bestehenden Stand übernehmen (die Mails sind im Ziel — nicht auf 0 zurücksetzen!). Anschließend die alten IMAP-Hash-Zeilen entfernen. In einer Transaktion, mit Zählung vorher/nachher.
  3. Abnahme: copied == mbox_index (58.051 == 58.051), und der erste Watch-/Migrationslauf danach meldet archived=0 target=0 pending=0 — also keine einzige Neukopie. Meldet er mehr, sofort stoppen: dann matcht der Schlüssel nicht und es entstehen gerade Dubletten.

Alternativ (wenn 1.+2. zu heikel erscheinen)

Zustand so lassen. Er ist funktional korrekt: Der Migrationspfad matcht auf die IMAP-Hash-Zeilen, es wird nichts doppelt kopiert (live bestätigt: complete=0 archived=0 target=0 pending=0 errors=0). Kosten: copied ist um ~7 % aufgebläht und die Zahl taugt nicht als Beleg für „so viel ist archiviert". Dann aber in INSTALL.md dokumentieren, warum die Zahlen auseinandergehen — sonst rätselt in zwei Jahren jemand daran.

Hängt zusammen mit

Der noch offenen body_sha256-Bereinigung (byte-verschiedene Mails mit gleicher Message-ID, ~109 Nachzügler): Beide drehen sich um „was ist die Identität einer Mail". Am besten in einem Zug lösen und eine Schlüsselregel festlegen:

  • Message-ID vorhanden → message_id + body_sha256 (fängt die byte-verschiedenen Fälle),
  • keine Message-ID → nur body_sha256,
  • body_sha256 immer über die mbox-normalisierte Fassung.