Mail-Graveyard/korrektur-brief.md
2026-07-14 20:57:09 +02:00

7.2 KiB
Raw Blame History

Codex-Brief — Korrekturen aus dem zstd-Messlauf

Vier Befunde aus meinem Messlauf (14.07.). Reihenfolge = Priorität. 1 und 2 sind Pflicht vor der ersten echten @dr-gold.de-Migration.


1. 🔴 REGRESSION: Alte plain-Archive sind im Viewer unsichtbar

Befund: Die Nachrichtenliste kommt jetzt aus mbox_index. Archive, die vor dem zstd-Deploy geschrieben wurden, haben keine Index-Zeilen:

mbox_index:   codex-abnahme.../INBOX  ->    1 Zeile   (Datei enthaelt 207 Mails)
Viewer zeigt: "INBOX (1)"

Das Perfide: Es zeigt „(1)" statt eines Fehlers. Ein halb-wahres Ergebnis ist schlimmer als gar keins — es sieht aus, als funktioniere es. Der im zstd-Brief geforderte Fallback auf den alten Parse-Pfad fehlt.

Fix — die mbox ist append-only, also reicht ein Fortschritts-Marker pro Datei:

CREATE TABLE IF NOT EXISTS mbox_index_state(
  account_id    INTEGER NOT NULL,
  folder        TEXT NOT NULL,
  indexed_bytes INTEGER NOT NULL DEFAULT 0,  -- bis hierhin ist die Datei indiziert
  UNIQUE(account_id, folder)
);
  • Nachindizieren: Wenn dateigroesse > indexed_bytes → ab indexed_bytes bis EOF scannen, Index-Zeilen anhaengen, indexed_bytes fortschreiben. Deckt leeren und teilweisen Index gleichermassen ab.
  • Der Migrationspfad schreibt indexed_bytes beim Append mit fort — dann muss im Normalbetrieb nie nachgescannt werden.
  • CLI --reindex <konto|all> fuer den Bestand.
  • Der Scanner muss beide Formate koennen: plain (From -Trennzeilen) und .zst (Frame fuer Frame entpacken, dann innerhalb des Frames an den From -Trennzeilen splitten). Das ist genau die Logik, die Punkt 4 spaeter auch fuer Batch-Frames braucht — einmal richtig bauen.
  • Nie wieder still halbe Wahrheiten: Wenn dateigroesse != indexed_bytes und nicht nachindiziert werden kann → Fehler zeigen, keine gekuerzte Liste.

2. 🔴 Ziel-Dedup-Werkzeug (die Feuerwehr)

Befund: In vdevop-03/INBOX existieren 199 Message-IDs doppelt — die INBOX wurde irgendwann komplett ein zweites Mal migriert. Niemand hat es gemerkt, weil im Log nur copied=199 stand und wie normaler Fortschritt aussah.

Die Ursachen sind inzwischen dicht (Message-ID-Normalisierung, DB-Guard, Watch-Fail-Fast). Aber es gibt kein Werkzeug, das Dubletten im Ziel findet oder repariert. Wenn das bei einem echten dr-gold-Postfach passiert, gibt es keinen Ausweg — und das merkt man erst, wenn es zu spaet ist.

Fix: --dedup-target <konto> [--folders ...] [--apply] (+ Admin-Route).

  • Ordner scannen, header-only (Headers()-Pfad, keine Bodies!), nach normalisierter Message-ID gruppieren.
  • Pro Gruppe mit >1 Kopie: die niedrigste UID behalten (die aelteste, zuerst eingespielte), den Rest \Deleted + EXPUNGE.

Sicherheitsregeln — das Ding loescht fremde Mail, ein Bug hier ist schlimmer als die Dubletten:

  1. Dry-Run ist Default. Ohne --apply wird nur berichtet: pro Ordner, wie viele Gruppen, wie viele ueberzaehlige Kopien, welche UIDs.
  2. Nie die letzte Kopie loeschen. Pro Message-ID bleibt immer genau eine.
  3. Mails ohne Message-ID nie anfassen — nicht sicher dedupliizierbar. Separat auszaehlen und berichten.
  4. Jede Loeschung protokollieren (Konto, Ordner, UID, Message-ID) — es braucht eine Beweiskette.
  5. Nur requireAdmin.

3. 🟡 folder_map ist toter Code

Die Tabelle wird angelegt, aber nirgends gelesen: 07-migrate.go:122 uebergibt den Override hart als "":

dstFolder := MapSourceToTarget(folder.Name, folder.Attrs, folder.Delim, dst.Delim(), targets, "")
//                                                                                            ^^ immer leer

MapSourceToTarget hat den Parameter, 11-folders.go dokumentiert „manuelles folder_map hat Vorrang" — es tut es nur nicht. Ein dokumentiertes Feature, das stillschweigend nichts tut. (Genau daran ist mein Messlauf haengengeblieben: die Umleitung wurde ignoriert und 207 Mails landeten im echten Ziel-Ordner.)

Fix: folder_map pro Konto laden und als override durchreichen. Plus Pflege in der UI (Konten-Formular: Quell-Ordner → Ziel-Ordner).


4. 🟢 Kompressionsrate — gemessen, und die Entscheidung dazu

Groesse Faktor
plain mbox (207 Mails) 4.372.002
zstd, Frame pro Mail (heute) 1.470.412 2,97×
zstd, ganze Datei am Stueck 113.564 38,50×

Der Abstand ist die Cross-Message-Redundanz, die Frame-pro-Mail wegwirft. (Mein Korpus uebertreibt ihn — 199 fast identische Benachrichtigungsmails. Auf echter Mail waere die Luecke kleiner, aber real.)

Was wir NICHT machen: zstd-Dictionary. Das waere technisch der eleganteste Weg (genau dafuer sind Dictionaries da), aber es macht das Archiv abhaengig von einer externen Datei: Dictionary weg → Archiv unlesbar. Fuer ein Beweis-Archiv, das 610 Jahre halten und im Streitfall herausgegeben werden muss, ist „zstd -d funktioniert einfach, ohne Beiwerk" mehr wert als jeder Kompressionsfaktor. Selbstgenuegsamkeit schlaegt Rate.

Was wir machen koennen: Batch-Frames mit kleinem Deckel (2 MB). Der Index (inner_offset/inner_len) ist bereits dafuer gebaut.

Aber die Durability-Regel ist dabei nicht verhandelbar: Bei Batching ist eine Mail nach mbox.Append noch nicht auf der Platte — sie liegt im Frame-Puffer. Deshalb:

Frame voll (oder Ordner zu Ende)
  -> Frame komprimieren
  -> KOMPLETTEN Frame an die Datei anhaengen + fsync
  -> DANN erst: Index-Zeilen schreiben
  -> DANN erst: MarkCopied fuer alle Mails des Batches (eine Transaktion)

MarkCopied darf niemals vor dem fsync passieren — sonst gilt eine Mail als gesichert, die nie im Archiv ankam. Das waere der schlimmstmoegliche Bug fuer ein Beweis-Archiv.

Nebenwirkung, die man kennen muss: Ein Absturz mitten im Batch laesst bis zu 2 MB Mails unmarkiert → sie werden beim naechsten Lauf erneut ins IMAP-Ziel kopiert → Dubletten dort. Dasselbe Fenster wie heute, nur groesser (Batch statt Einzelmail). Deshalb der kleine Deckel (2 MB, nicht 8) — und deshalb ist Punkt 2 (Dedup-Werkzeug) die Voraussetzung dafuer.

Priorität: niedriger als 13. 2,97× ist bereits ein Gewinn, und die Vorschau wurde durch den Index sogar 6× schneller (1,8 ms statt 10,8 ms). Korrektheit zuerst.


Abnahme

  1. go test ./... gruen.
  2. Reindex: --reindex all → der Viewer zeigt fuer das bestehende plain-Archiv wieder alle 207 Mails (statt „(1)"). Liste und Oeffnen funktionieren fuer plain und .zst.
  3. Dedup, Dry-Run: --dedup-target codex-abnahme-vdevop02-to-vdevop03 meldet fuer INBOX 199 Gruppen mit je einer ueberzaehligen Kopie (das ist der bekannte, echte Bestand — ein guter Selbsttest). Mit --apply bleiben danach genau 365 eindeutige Mails uebrig. Ohne --apply darf sich nichts aendern.
  4. folder_map: Ein Eintrag INBOX → TEST-ZIEL fuehrt im Log zu INBOX -> TEST-ZIEL (heute: INBOX -> INBOX).
  5. Round-trip bleibt byte-identisch — auch nach jeder Aenderung an der Frame-Struktur: zstd -d x.mbox.zst == plain-mbox, cmp-sauber. Das ist die Linie, die nie fallen darf.

Die Messungen (Rate, Vorschau-Latenz, Round-trip) fahre ich danach erneut.