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

165 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:**
```sql
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`](backend/07-migrate.go) uebergibt den Override hart als `""`:
```go
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.