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
|
|
@ -24,3 +24,34 @@ func TestMessageIDFallsBackToBodyHash(t *testing.T) {
|
|||
t.Fatal("messageID hash fallback is not stable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBodySHA256IsStableAcrossMboxNormalization(t *testing.T) {
|
||||
raw := []byte("From: a@example.com\r\nSubject: Test\r\n\r\nFirst\r\nFrom escaped\r\n\r\n")
|
||||
record := mboxRecord(RawMessage{Body: raw})
|
||||
parts := splitMboxRecordsWithOffsets(record)
|
||||
if len(parts) != 1 {
|
||||
t.Fatalf("mbox parts=%d, want 1", len(parts))
|
||||
}
|
||||
if got, want := bodySHA256(parts[0].Message), bodySHA256(raw); got != want {
|
||||
t.Fatalf("mbox hash=%s, raw hash=%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityUsesMessageIDAndBodyHash(t *testing.T) {
|
||||
first := identityForRawMessage(RawMessage{MessageID: "<same@example.com>", Body: []byte("Message-ID: <same@example.com>\r\n\r\nfirst")})
|
||||
second := identityForRawMessage(RawMessage{MessageID: "<same@example.com>", Body: []byte("Message-ID: <same@example.com>\r\n\r\nsecond")})
|
||||
if first.MessageID != second.MessageID {
|
||||
t.Fatalf("message IDs differ: %#v %#v", first, second)
|
||||
}
|
||||
if first.BodySHA256 == second.BodySHA256 {
|
||||
t.Fatalf("different bodies have same identity hash: %#v %#v", first, second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityWithoutMessageIDKeepsLegacyRawHash(t *testing.T) {
|
||||
body := []byte("From: a@example.com\r\n\r\nbody")
|
||||
identity := identityForRawMessage(RawMessage{MessageID: messageID(body), Body: body})
|
||||
if identity.MessageID != "" || identity.BodySHA256 == "" || !strings.HasPrefix(identity.LegacyMessageID, "sha256:") {
|
||||
t.Fatalf("unexpected identity: %#v", identity)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue