# Review — Snapshot (14-snapshot.go) + Streaming-Reindex Geprueft von Claude am 2026-07-16 gegen den nicht-committeten Arbeitsstand. ## Urteil **Der Snapshot ist richtig gebaut und darf so committet werden.** **Die Aenderung an `normalizeMessageID` darf NICHT ohne Fix ausgerollt werden.** ## Was gepasst hat Die zwei Stellen, an denen ich Fehler erwartet habe, sind beide korrekt: ```go 104: DB.Exec(`VACUUM INTO ?`, databasePath) // erst der Schnappschuss 120: collectSnapshotWatermarks(snapshotDB, Cfg.MboxRoot) // dann N DARAUS MAX(i.file_offset+i.frame_len) // richtige Wasserlinie 327: os.Stat(...) -> info.Size() < watermark.Bytes // stat nur als Notbremse ``` - `indexed_bytes` kommt nirgends vor (die Falle aus backup-brief.md). - Reihenfolge stimmt; `stat` leitet N nicht ab, sondern verweigert nur. - Zeile 145 bricht ab, wenn indizierte und abgebildete Datensaetze auseinander- laufen. **Das stand nicht im Brief und ist besser als verlangt.** - Pfad-Ausbruchspruefung (`relative == ".."`) vorhanden. - `snapshotPathKey` senkt nur unter Windows auf Kleinschreibung — richtig herum: auf Linux sind `Drafts` und `drafts` zwei echte Dateien und duerfen nicht kollidieren, unter Windows wuerden sie es. - Streaming-Reindex (`06-mbox.go:293`) liest nachrichtenweise ueber `bufio` statt 9,4 GB in den RAM. Die Message-ID-Erhaltung im Reindex ist entgegen meinem ersten Verdacht sauber: ```go if storedIdentity.bodySHA256 != "" && storedIdentity.bodySHA256 == entry.BodySHA256 { entry.MessageID = storedIdentity.messageID } ``` Die Position ist nur der Nachschlage-Schluessel, der **Body-Hash ist die Bedingung**. Eine falsche Zuordnung kann keine falsche Uebernahme ausloesen. Nachgeprueft am realen Snapshot `archive-20260716-2250`: 228 Archivdateien, `mbox_index` 121.861 im Snapshot **und** in Produktion, 121.861 praezise `copied`-Identitaeten, `integrity_check=ok`. `go vet` sauber, `go test -count=1 ./backend/` gruen (0,366s). --- ## BEFUND 1 — Identitaetsaenderung, Dublettenrisiko (Blocker fuer Rollout) ```go func normalizeMessageID(id string) string { - return strings.Trim(strings.TrimSpace(id), "<>") + if start := strings.IndexByte(id, '<'); start >= 0 { ... } ``` `normalizeMessageID` speist `message_id` = die halbe Identitaet. Alt und neu gegen die **echten gespeicherten Werte** verglichen (lauffaehig nachgestellt): ``` <476BDFDB019289BE@mail21.bluewin.ch> (added by postmaster@bluewin.ch) alt: [476BDFDB019289BE@mail21.bluewin.ch> (added by postmaster@bluewin.ch)] neu: [476BDFDB019289BE@mail21.bluewin.ch] <<< WEICHT AB <22578196653698419217.BB8A0E8F48F3F290@dr-gold.de>+D271B1409C3C6028 alt: [22578196653698419217.BB8A0E8F48F3F290@dr-gold.de>+D271B1409C3C6028] neu: [22578196653698419217.BB8A0E8F48F3F290@dr-gold.de] <<< WEICHT AB <1280874184.3726.1662111901550.JavaMail."LOCAL SERVICE"@DESV226350> gleich <476FE4C5.2060001@yahoo.com;Evy O'Brien> gleich <13140-22021132765855760@strepack.de> gleich 13140-22021132765855760@strepack.de gleich ``` Nur Werte mit einem **verirrten `>` im Inneren** aendern sich. Umfang gemessen: **3 Zeilen in `copied`, 2 in `mbox_index`** von 121.861. **Warum die Reindex-Erhaltung nicht reicht:** Sie schuetzt den Index, nicht den Live-Pfad. Dort gilt: 1. Praezise Abfrage `message_id=NEU AND body_sha256=X` → gespeichert ist ALT → **kein Treffer**. 2. Die Alias-Schleife hilft nicht: `LegacyMessageID` wird in `identityForRawMessage` ebenfalls mit der **neuen** Funktion gebildet (`id := normalizeMessageID(...); legacyID := id`) — sie traegt die alte Normalisierung nicht. 3. Ergebnis: `CopyState{}` → nicht kopiert → **erneutes mbox.Append UND erneutes dst.Append**. **Beim ersten Nachtlauf nach dem Rollout entstehen so 3 Dubletten in beiden Haelften — lautlos.** Klein, aber es ist dieselbe Klasse wie die 4.024 Waisen, und Dubletten sind unsere Stopp-Bedingung. ### Zwei Wege, Empfehlung (a) **(a) Alte Normalisierung als zusaetzlichen Alias fuehren.** `identityForRawMessage` bekommt zusaetzlich den alt-normalisierten Wert; die Alias-Schleife in `GetCopyIdentityStateWithFolderAlias` muss ihn dann auch gegen **praezise** Zeilen pruefen, nicht nur gegen `body_sha256=''`. *Additiv, mutiert keine Geschichte, nutzt vorhandene Maschinerie.* Passt zur Linie „Aliase sind Rueckrollschutz". **(b) Einmal-Migration der 5 Zeilen** auf den neuen Wert. Simpler, aber schreibt Geschichte um — und die Regel lautet: Aliase nicht ohne gesonderte, geprueefte Bereinigung anfassen. ### Abnahme fuer den Fix 1. Lauf gegen ein Konto mit einer der beiden IDs: `archived=0`. Ein zweiter Lauf danach ebenfalls `archived=0`. 2. `SELECT COUNT(*) FROM mbox_index WHERE message_id LIKE '%>%'` bleibt bei 2 — nicht 4. 3. Test mit genau diesen zwei Roh-Headern als Fixture. --- ## BEFUND 2 — Snapshot-DB world-readable (klein, aber schnell zu beheben) ``` 600 root:root .../backup/colak@dr-gold.de/INBOX.mbox <- richtig 644 root:root .../data/mail-graveyard.db <- zu offen ``` Die Snapshot-DB enthaelt **23 Quell- und 23 Ziel-Passwoerter im Klartext**. Heute schuetzt nur die `0700`-Wurzel darueber — ein `chmod` von der Preisgabe entfernt, und inkonsistent zu den mbox-Dateien daneben. Bitte `0600`. --- ## Nicht deins `gofmt -l` meldet `backend/11-folders.go`. Die Datei ist im Diff unveraendert (Altbestand), reine Ausrichtung einer Map. Kein Rollout-Hindernis. ## Weiterhin offen (unveraendert aus backup-brief.md) - Der Snapshot liegt auf **derselben Platte** — er ist die Vorstufe, nicht das Backup. Raeumlich getrennte, verschluesselte Kopie fehlt. - Restore-Test gehoert **wiederholt** gefahren, nicht einmal.