Mail-Graveyard/zstd-brief.md
2026-07-14 20:20:30 +02:00

5 KiB
Raw Permalink Blame History

Codex-Brief — mbox-Kompression mit zstd-3

Ziel: Das Datengrab schrumpfen, ohne den verifizierten Append-only-Pfad und die Vorschau kaputtzumachen. Danach vergleichen wir gemessen: Größe und Vorschau-Latenz, komprimiert vs. plain.

Gemessene Ausgangslage (echte mbox auf GoldPi)

mbox roh zstd-3 zstd-19
INBOX.mbox 4.372.002 113.564 (38,5×) 111.017 (39,4×)
Test-Haupt-Ordner 51.699 11.316 (4,6×) 10.677 (4,8×)

zstd-19 bringt nur ~2 % mehr als zstd-3, kostet aber ein Vielfaches an Zeit → Level 3. (Die 38× sind ein Artefakt: die INBOX besteht großteils aus fast identischen Benachrichtigungsmails. Realistisch bei echten Postfächern: 24×.)

Format

<ordner>.mbox.zst = Folge von zstd-Frames. Konkatenierte Frames sind eine gültige .zstzstd -d datei.mbox.zst liefert exakt die mbox, die wir sonst plain geschrieben hätten. Damit bleibt der Append-only-Charakter erhalten: neue Mail = neuer Frame hinten dran.

Start mit: ein Frame pro Mail. Das macht die Vorschau trivial schnell (genau ein Frame entpacken). Nachteil: keine Redundanz zwischen Mails → schlechtere Rate. Falls die gemessene Rate enttäuscht, schalten wir auf Batch-Frames mit 8-MB-Deckel um (bessere Rate, Vorschau entpackt dann ≤ 8 MB ≈ 30 ms). Der Index unten ist so gebaut, dass dieser Wechsel nichts kostet — bitte die Felder auch dann schon so anlegen.

Bibliothek: github.com/klauspost/compress/zstd — pure Go, kein cgo, bleibt single-binary.

Offset-Index (der Kern)

Neue Tabelle, geschrieben im selben Schritt wie der mbox-Append:

CREATE TABLE IF NOT EXISTS mbox_index(
  account_id   INTEGER NOT NULL,
  folder       TEXT NOT NULL,
  seq          INTEGER NOT NULL,   -- laufende Nummer im Ordner
  message_id   TEXT NOT NULL,
  subject      TEXT NOT NULL DEFAULT '',
  from_addr    TEXT NOT NULL DEFAULT '',
  date         TEXT NOT NULL DEFAULT '',
  file_offset  INTEGER NOT NULL,   -- Byte-Offset des FRAMES in der Datei
  frame_len    INTEGER NOT NULL,   -- Laenge des Frames (komprimiert)
  inner_offset INTEGER NOT NULL DEFAULT 0,  -- Offset der Mail IM entpackten Frame
  inner_len    INTEGER NOT NULL,   -- Laenge der Mail entpackt
  UNIQUE(account_id, folder, message_id)
);

(inner_offset ist bei „ein Frame pro Mail" immer 0 — er ist die Vorbereitung für Batch-Frames.)

Das schenkt uns nebenbei einen Bug-Fix: Die Liste im Viewer parst heute die ganze mbox-Datei (ReadMboxList). Künftig kommt sie aus dem Index → kein Dateizugriff mehr, schneller als heute, auch bei einem 10-GB-Archiv.

Schreibpfad — die Reihenfolge bleibt heilig

In 07-migrate.go nicht umsortieren. Neu ist nur, dass der mbox-Append Offsets zurückgibt und danach der Index geschrieben wird:

AlreadyCopied → dst.Append → mbox.Append (liefert offset/len) → Index-Zeile → MarkCopied

MarkCopied bleibt das Letzte. Ein Absturz davor kopiert die Mail beim nächsten Lauf erneut — dieselbe bewusste Entscheidung wie bisher.

Lesepfad

  • Liste → nur SQLite (mbox_index), keine Datei anfassen.
  • Eine Mail öffnenfile_offset seeken, genau diesen Frame entpacken, inner_offset/inner_len herausschneiden.
  • Abwärtskompatibel: Bestehende plain .mbox müssen weiter funktionieren. Erkennung über Endung bzw. zstd-Magic (0xFD2FB528). Wenn für eine Datei keine Index-Zeilen existieren → alter Parse-Pfad.

Umschaltbar (wichtig für den Vergleich)

config.json: "mbox_compression": "none" | "zstd" (Default zunächst none, damit nichts überrascht). So können wir dasselbe Postfach zweimal sichern — einmal plain, einmal zstd — und sauber vergleichen.

Export (die harte Anforderung bleibt)

Eine Funktion/Route „Archiv als plain mbox exportieren": entpackt die .mbox.zst zu einer normalen .mbox. Damit bleibt die Zusage erhalten, die der Grund für mbox war: man kann sie in eM Client ziehen bzw. einem Anwalt oder Prüfer in die Hand geben. Ein Handgriff statt null — akzeptabel.

Abnahme

  1. go test ./... grün.
  2. Round-trip byte-identisch — das ist die wichtigste Prüfung für ein Beweis-Archiv: Denselben Quell-Ordner einmal mit none und einmal mit zstd sichern. Dann muss gelten: zstd -d ordner.mbox.zst == ordner.mbox Byte für Byte (cmp). Wenn nicht: Stopp. Ein Archiv, das nicht exakt zurückkommt, ist wertlos.
  3. Rate messen an einer echten mbox (nicht an meinem Testkorpus — der ist durch die vielen identischen Benachrichtigungsmails unrealistisch gut).
  4. Idempotenz unverändert: zweiter Lauf copied=0.
  5. Anhänge (PDF/PNG/ZIP) kommen aus dem komprimierten Archiv byte-identisch wieder heraus.
  6. Vorschau-Latenz: Mail öffnen aus plain vs. aus zstd — Zeit messen. (Diesen Vergleich fahre ich, Claude.)

NICHT in diesem Schritt

  • Bestehende plain-mbox nachträglich komprimieren (Migration alter Archive) — eigener, späterer Schritt.
  • Batch-Frames mit 8-MB-Deckel — nur falls die gemessene Rate enttäuscht.