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
|
|
@ -62,7 +62,7 @@ func messageHandler(w http.ResponseWriter, r *http.Request) {
|
|||
decodeHeader(msg.Header.Get("Subject")),
|
||||
decodeHeader(msg.Header.Get("From")),
|
||||
decodeHeader(msg.Header.Get("Date")),
|
||||
messageBody(raw),
|
||||
messageBodyWithContext(raw, fmt.Sprintf("archive account=%q folder=%q seq=%d", account, folder, index)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -393,7 +393,7 @@ func renderTargetMatch(b *strings.Builder, match forwardedMatch) {
|
|||
decodeHeader(msg.Header.Get("Subject")),
|
||||
decodeHeader(msg.Header.Get("From")),
|
||||
decodeHeader(msg.Header.Get("Date"))+" | "+match.Location(),
|
||||
messageBody(match.Raw),
|
||||
messageBodyWithContext(match.Raw, "target "+match.Location()),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -487,7 +487,8 @@ func filterEntries(path string, entries []MboxEntry, q string) []MboxEntry {
|
|||
continue
|
||||
}
|
||||
raw, err := ReadMboxMessage(path, entry.Index)
|
||||
if err == nil && strings.Contains(strings.ToLower(messageBody(raw)), needle) {
|
||||
if err == nil && strings.Contains(strings.ToLower(messageBodyWithContext(raw,
|
||||
fmt.Sprintf("archive search path=%q seq=%d", path, entry.Index))), needle) {
|
||||
out = append(out, entry)
|
||||
}
|
||||
}
|
||||
|
|
@ -523,7 +524,7 @@ func findTargetIMAPCopy(account, targetAccount string, raw []byte) (forwardedMat
|
|||
if err != nil {
|
||||
return forwardedMatch{}, false, err
|
||||
}
|
||||
match, ok, scanErr := scanTargetForMessage(dst, targetUser, id)
|
||||
match, ok, scanErr := scanTargetForMessage(dst, targetUser, id, bodySHA256(raw))
|
||||
closeErr := dst.Close()
|
||||
if scanErr != nil {
|
||||
return forwardedMatch{}, false, scanErr
|
||||
|
|
@ -538,7 +539,7 @@ func findTargetIMAPCopy(account, targetAccount string, raw []byte) (forwardedMat
|
|||
return forwardedMatch{}, false, nil
|
||||
}
|
||||
|
||||
func scanTargetForMessage(dst TargetMailbox, targetUser, messageID string) (forwardedMatch, bool, error) {
|
||||
func scanTargetForMessage(dst TargetMailbox, targetUser, messageID, bodyHash string) (forwardedMatch, bool, error) {
|
||||
messageID = normalizeMessageID(messageID)
|
||||
folders, err := dst.Folders()
|
||||
if err != nil {
|
||||
|
|
@ -559,6 +560,9 @@ func scanTargetForMessage(dst TargetMailbox, targetUser, messageID string) (forw
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if bodySHA256(msg.Body) != bodyHash {
|
||||
continue
|
||||
}
|
||||
return forwardedMatch{
|
||||
Account: "Ziel: " + targetUser,
|
||||
Folder: folder.Name,
|
||||
|
|
@ -582,6 +586,7 @@ func findForwardedCopy(account, targetAccount string, raw []byte) (forwardedMatc
|
|||
if err != nil {
|
||||
return forwardedMatch{}, false
|
||||
}
|
||||
bodyHash := bodySHA256(raw)
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() || entry.Name() == account {
|
||||
continue
|
||||
|
|
@ -600,7 +605,7 @@ func findForwardedCopy(account, targetAccount string, raw []byte) (forwardedMatc
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if messageIDHeader(candidate) == id {
|
||||
if messageIDHeader(candidate) == id && bodySHA256(candidate) == bodyHash {
|
||||
return forwardedMatch{
|
||||
Account: entry.Name(),
|
||||
Folder: folderNameFromMboxPath(path),
|
||||
|
|
@ -713,7 +718,7 @@ func appendManualTargetCopy(account, folder, targetAccount string, raw []byte) (
|
|||
return "", err
|
||||
}
|
||||
if a.ID != 0 {
|
||||
_ = MarkCopied(a.ID, folder, id)
|
||||
_ = MarkIdentityCopied(a.ID, folder, identityForRawMessage(msg))
|
||||
}
|
||||
return "Manuell kopiert nach " + strings.TrimSpace(a.DstUser) + " / " + dstFolder + ".", nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue