# Codex-Brief — Sicherung des Archivs (mbox + DB als EIN Stand) Betrifft Pos-81 des Ledger-Vorgangs `netcup-svr-mail-graveyard-eklarner-sicherung-2026-07-16`. ## Die Regel **mbox ist der Inhaltsbeleg, die DB ist Index und Zustandsbuchhaltung. Beide gehoeren in denselben Sicherungszeitpunkt — aber nur die DB ist aus der mbox rekonstruierbar.** Die Abhaengigkeit ist **nicht symmetrisch**. Das ist der Kern: | Kombination | Bewertung | |---|---| | mbox ohne DB | **heilbar.** `--reindex` baut den Index vollstaendig neu (`main.go:26`, `ReindexArchives`, plain und zstd). Wir haben das bei der Index-Drift real benutzt. | | DB ohne mbox | **wertlos.** Die DB enthaelt nur Offsets, Hashes und Zustaende — keinen einzigen Mailinhalt. | | **neuere mbox + aeltere DB** | **grundsaetzlich heilbar** per Reindex. Die ungefaehrliche Richtung. | | **neuere DB + aeltere mbox** | **GEFAEHRLICH.** `copied` markiert eine Mail als gesichert, obwohl ihr Archivdatensatz im Backup fehlt. Ein stiller Verlust, der sich als gruener Haken tarnt. | Daraus folgt eine Reihenfolge-Regel, falls kein gemeinsamer Stand erzeugt werden kann: **erst die DB sichern, dann die mbox.** Dann ist die mbox im Backup immer die neuere — die heilbare Richtung. Die umgekehrte Reihenfolge produziert exakt den gefaehrlichen Fall. ## Was der Host hergibt Gemessen auf netcup-svr am 2026-07-16: ``` /mnt/DATA ext4 auf /dev/vda4, noatime lazytime LVM nicht vorhanden btrfs/ZFS nicht vorhanden SQLite journal_mode=wal, WAL 34 MB unmerged, integrity_check=ok data/ + backup/ liegen unter EINEM Elternbestand (/mnt/DATA/Containers/Test-Container/Mail-Graveyard-Pod/) ``` **Der atomare Dateisystem-Snapshot — die eigentlich beste Loesung — steht auf diesem Host nicht zur Verfuegung.** Ein Wechsel auf LVM/btrfs/ZFS waere die saubere Antwort, ist aber ein Eingriff am laufenden Rettungsbetrieb. Nicht jetzt. Nebenbefund: Ein blosses `cp` der `.db` ohne `-wal` wuerde die neuesten 34 MB Zustand verlieren. Das ist zufaellig die *heilbare* Richtung (aeltere DB), aber verlassen darf man sich darauf nicht. ## Der Weg, den das Append-Only-Design erlaubt Zwei gemessene Eigenschaften machen einen dritten Weg moeglich, der **keinen Stillstand braucht**: **1. Die mbox-Dateien sind strikt append-only.** ```go // 06-mbox.go:61 — der einzige Schreibpfad f, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) ``` Kein `O_TRUNC`, kein `Truncate`, kein `Rename`, keine Kompaktierung in der gesamten Datei. **Einmal geschriebene Bytes aendern sich nie mehr.** Ein Praefix `[0, N)` ist damit zu jedem spaeteren Zeitpunkt byte-identisch mit dem Stand von damals. **2. Die DB kennt die exakte Schreib-Wasserlinie.** `MAX(file_offset + frame_len)` aus `mbox_index` ist per Konstruktion eine Grenze auf vollstaendigen Datensaetzen. Geprueft ueber **alle 228 mbox-Dateien**: 0 Abweichungen zur echten Dateigroesse. > Nicht verwenden: `mbox_index_state.indexed_bytes`. Das ist eine Lese-/ > Viewer-Marke (`02-database.go:281` setzt sie beim Reindex auf `-1`) und laeuft > dem Schreibstand um Groessenordnungen hinterher — bei e.klarner/INBOX 4,5 MB > gegenueber 9,4 GB Datei. Diese Falle hat mich beim Entwurf einmal erwischt. ### Das Verfahren ``` 1. VACUUM INTO '/mail-graveyard.db' -> konsistenter DB-Stand einer LAUFENDEN WAL-Datenbank, ohne Stopp. SQLite garantiert das; WAL und -shm werden mit aufgeloest. 2. Aus DIESEM Schnappschuss je Konto/Ordner: N = SELECT MAX(file_offset+frame_len) FROM mbox_index WHERE ... 3. Je mbox die ersten N Bytes kopieren (in Ruhe, waehrend das Tool weiterlaeuft). 4. Fertig. Kein Stillstand, kein LVM, kein Stoppen der Container. ``` **Warum das ein Stand ist und kein Widerspruch:** N stammt aus dem DB-Schnappschuss selbst. Jeder `copied`- und jeder `mbox_index`-Eintrag darin liegt per Konstruktion vollstaendig innerhalb des Praefix. Mails, die waehrend des Kopierens ankommen, liegen jenseits von N und sind in **beiden** Haelften abwesend. Die gefaehrliche Kombination „neuere DB + aeltere mbox" kann gar nicht entstehen — nicht weil wir schnell sind, sondern weil das Paar konstruktiv einen Zeitpunkt beschreibt. Sollte doch etwas driften, zeigt es in die heilbare Richtung (Praefix enthaelt mindestens, was die DB kennt → Reindex). ### Der Fallback, falls Schritt 1-3 nicht gebaut wird Deine urspruengliche Variante bleibt gueltig und ist als Handgriff sofort verfuegbar: schreibende Container kurz stoppen, WAL sauber checkpointen (`PRAGMA wal_checkpoint(TRUNCATE)`), gemeinsamen Kopierstand erzeugen, starten. Nur: ohne Snapshot-Dateisystem muesste hier fuer die vollen 42 GB gestoppt werden — genau das vermeidet der Praefix-Weg. ## Was zu bauen ist ``` --snapshot erzeugt DB-Schnappschuss + mbox-Praefixe --verify-snapshot prueft einen Stand isoliert durch ``` ## Fallstricke 1. **N NIEMALS per `stat` von der Datei nehmen.** Dann kann die Grenze mitten in eine gerade geschriebene Mail fallen. N kommt aus dem DB-Schnappschuss, sonst ist die ganze Garantie hin. 2. **Reihenfolge ist Teil der Korrektheit:** erst `VACUUM INTO`, dann N daraus lesen. Nicht umgekehrt, nicht aus der Live-DB. 3. **`indexed_bytes` ist nicht die Wasserlinie** (s. o.). 4. **Der Snapshot ist kein Backup.** Er liegt auf derselben Platte. Er ist die Vorstufe: von ihm wird raeumlich getrennt und moeglichst verschluesselt weggeschrieben. 5. **Kein stiller Default.** Ohne Flag aendert sich nichts. ## Abnahme 1. `go test ./...` gruen. 2. `--snapshot` waehrend eines **laufenden** Migrationslaufs: Der Stand muss danach in sich stimmen. Das ist der eigentliche Test — ein Snapshot im Leerlauf beweist nichts. 3. **Restore isoliert testen** (nicht auf der Produktions-DB): - `PRAGMA integrity_check` = ok - `--reindex all --rebuild` auf dem Restore laeuft durch - **Indexanzahl** stimmt mit `SELECT COUNT(*) FROM mbox_index` des Schnappschusses ueberein - **Hashabdeckung**: jede `copied`-Zeile mit `body_sha256 <> ''` hat einen `mbox_index`-Eintrag - **Pending-Stufen**: `mbox_done`/`target_done` plausibel, keine Zeile mit `mbox_done=1` ohne Archivdatensatz 4. **Der Restore-Test gehoert wiederholt gefahren, nicht einmal.** Ein Backup, das nie zurueckgespielt wurde, ist eine Behauptung.