# 🔴 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** (`
`, `

`, ``, …). 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 := "

Turkish İ test


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.