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
106
bug-htmltotext-panic.md
Normal file
106
bug-htmltotext-panic.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# 🔴 Vorschau stürzt bei ~0,1 % der Mails ab (htmlToText, Unicode)
|
||||
|
||||
Gefunden beim Verifizieren des Index-Drift-Fixes. **Der Drift-Fix selbst ist
|
||||
sauber** (2000 Stichproben über 8 Ordner, 0 falsche Zuordnungen, Drift 0 über
|
||||
alle 69 Ordner) — das hier ist ein **separater, neuer Befund**.
|
||||
|
||||
## Symptom
|
||||
|
||||
```
|
||||
2026/07/16 09:45:44 http: panic serving: runtime error:
|
||||
slice bounds out of range [:207] with length 175
|
||||
backend.replaceCaseInsensitive 06-mbox.go:770
|
||||
backend.markHTMLBreaks 06-mbox.go:755
|
||||
backend.htmlToText 06-mbox.go:690
|
||||
backend.extractTextBody / messageBody / messageHandler
|
||||
```
|
||||
Die betroffene Mail ist **nicht anzeigbar** — die Verbindung stirbt. Gemessen:
|
||||
**1 von 2000 Stichproben (~0,1 %)**, hochgerechnet ~58 der 57.771 Mails.
|
||||
Beispiel: `kolmer/INBOX seq=2378 "[www.mtp24.pl] Weryfikacja adresu"` — eine
|
||||
polnische Mail. Auslöser ist Nicht-ASCII.
|
||||
|
||||
## Ursache — dieselbe Klasse wie der Buffer-Bug: zwei Koordinatensysteme
|
||||
|
||||
[`06-mbox.go:760`](backend/06-mbox.go):
|
||||
```go
|
||||
func replaceCaseInsensitive(s, old, new string) string {
|
||||
lower := strings.ToLower(s) // <-- kann ANDERE Byte-Laenge haben als s!
|
||||
needle := strings.ToLower(old)
|
||||
for {
|
||||
i := strings.Index(lower, needle) // Index in LOWER
|
||||
...
|
||||
b.WriteString(s[:i]) // auf S angewendet <-- FALSCH
|
||||
cut := i + len(old)
|
||||
s = s[cut:]
|
||||
lower = lower[cut:] // beide mit demselben cut -> driften auseinander
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`strings.ToLower` ist **nicht byte-laengen-erhaltend**:
|
||||
- `İ` (U+0130, 2 Bytes) → `i̇` (3 Bytes) = **+1**
|
||||
- `K` (U+212A Kelvin, 3 Bytes) → `k` (1 Byte) = **−2**
|
||||
|
||||
Sobald ein solches Zeichen vorkommt, passen die Indizes aus `lower` nicht mehr
|
||||
auf `s`. Mit jedem `cut` driftet es weiter, bis `i > len(s)` → Panic. Das erklaert
|
||||
`[:207] with length 175` exakt.
|
||||
|
||||
**`stripHTMLBlock` direkt darunter hat denselben Fehler** (`lower`-Index auf `s`
|
||||
angewendet) — bitte mitfixen, auch wenn er noch nicht gecrasht ist.
|
||||
|
||||
## Fix
|
||||
|
||||
Die Needles sind **alle ASCII** (`<br>`, `</p>`, `</div>`, …). Also gar keine
|
||||
lowercase-Kopie bauen, sondern direkt auf dem Original suchen — mit
|
||||
ASCII-Case-Folding:
|
||||
|
||||
```go
|
||||
// asciiFoldIndex sucht needle (ASCII, bereits lowercase) case-insensitiv in s
|
||||
// und liefert einen Index, der IMMER fuer s gueltig ist.
|
||||
func asciiFoldIndex(s, needle string) int {
|
||||
n := len(needle)
|
||||
if n == 0 || len(s) < n { return -1 }
|
||||
for i := 0; i+n <= len(s); i++ {
|
||||
ok := true
|
||||
for j := 0; j < n; j++ {
|
||||
c := s[i+j]
|
||||
if 'A' <= c && c <= 'Z' { c += 'a' - 'A' }
|
||||
if c != needle[j] { ok = false; break }
|
||||
}
|
||||
if ok { return i }
|
||||
}
|
||||
return -1
|
||||
}
|
||||
```
|
||||
Damit entfaellt das zweite Koordinatensystem komplett — es gibt nur noch `s`.
|
||||
(Alternative: vorkompilierte `regexp.MustCompile("(?i)"+regexp.QuoteMeta(old))`
|
||||
— ebenfalls korrekt, aber langsamer und schwerer als noetig.)
|
||||
|
||||
## Zusaetzlich: `recover()` im Vorschau-Pfad — Pflicht für ein Beweis-Archiv
|
||||
|
||||
`htmlToText` verarbeitet **beliebiges, fremdes HTML** aus 20 Jahren Mail
|
||||
(Spam, kaputte Mailer, alle Sprachen). Ein Parser-Bug darf **nie** dazu fuehren,
|
||||
dass eine archivierte Mail unsichtbar wird.
|
||||
|
||||
In `messageHandler` (bzw. um `messageBody`) ein `defer recover()`:
|
||||
- bei Panic → **nicht** die Verbindung sterben lassen,
|
||||
- stattdessen den **Rohtext** der Mail anzeigen (bzw. Header + Hinweis
|
||||
„Darstellung fehlgeschlagen, Rohfassung unten"),
|
||||
- und den Vorfall loggen (Konto/Ordner/seq), damit wir solche Mails finden.
|
||||
|
||||
**Grundsatz:** Man muss jede archivierte Mail *irgendwie* ansehen koennen —
|
||||
notfalls roh. Lieber haesslich als unsichtbar.
|
||||
|
||||
## Abnahme
|
||||
|
||||
1. Unit-Test, der den Bug faengt (mit ASCII-only Test wuerde er durchrutschen!):
|
||||
```go
|
||||
in := "<p>Turkish İ test</p><BR>x" // U+0130 aendert die Byte-Laenge
|
||||
out := htmlToText(in) // darf NICHT panicken
|
||||
```
|
||||
Zusaetzlich ein Fall mit `K` (U+212A) und ein gemischter (mehrere Vorkommen,
|
||||
damit die Drift ueber mehrere Schleifendurchlaeufe sichtbar wuerde).
|
||||
2. `go test ./...` gruen.
|
||||
3. Live: `kolmer/INBOX seq=2378` (`[www.mtp24.pl] Weryfikacja adresu`) muss sich
|
||||
oeffnen lassen — heute stirbt die Verbindung.
|
||||
4. Ich (Claude) fahre danach den Sweep erneut: **0 Abstuerze** in 2000+ Proben.
|
||||
Loading…
Add table
Add a link
Reference in a new issue