Mail-Graveyard/uid-delta-brief.md
DonVoo e5aacf9b3f Briefe: UID ist Cursor (keine Identitaet), Backup-Weg ohne Stillstand
uid-delta-brief.md:
  UIDVALIDITY+UID waehlt nur KANDIDATEN aus. Body wird weiterhin geladen,
  Identitaet weiterhin aus canonicalMessageBytes berechnet. body_sha256
  bleibt die Wahrheit; die UID spart nur den Download alter Kandidaten.

backup-brief.md (Pos-81):
  DB und mbox sind NICHT symmetrisch abhaengig - mbox ohne DB ist per
  Reindex heilbar, DB ohne mbox ist wertlos. Gefaehrlich ist allein
  neuere DB + aeltere mbox. Daraus die Reihenfolge-Regel: DB zuerst.

  Gemessen: /mnt/DATA ist ext4 ohne LVM -> atomarer Snapshot unmoeglich.
  Aber mbox ist strikt append-only (06-mbox.go:61, kein O_TRUNC/Truncate/
  Rename), und MAX(file_offset+frame_len) aus mbox_index ist die exakte
  Schreib-Wasserlinie - ueber alle 228 mbox-Dateien 0 Abweichungen.
  Daraus: VACUUM INTO + Praefix-Kopie = konsistenter Stand OHNE Stillstand.
  Falle notiert: mbox_index_state.indexed_bytes ist NICHT die Wasserlinie.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 21:44:20 +02:00

126 lines
5.5 KiB
Markdown

# Codex-Brief — UID-Delta-Watcher
**Hoechste Prioritaet.** Alles andere (`--archive-only`, `--since`) ist Kuer.
## Das Problem, gemessen
Der Volllauf laedt **jede** Quellmail vollstaendig, bevor er weiss, ob er sie
schon hat. Das ist kein Bug, sondern strukturell — die Identitaet *ist* der
Body-Hash:
```go
// 04-imap-source.go
seqSet.AddRange(1, 0) // alle Nachrichten
cmd := m.c.Fetch(seqSet, &imap.FetchOptions{
BodySection: []*imap.FetchItemBodySection{section}, // samt Body
})
fn(rawMessageFromFetch(buf, section)) // AlreadyCopied erst hier
BodySHA256: bodySHA256(m.Body) // braucht den Body
```
Belegt am Lauf e.klarner 2026-07-16, Stueck 3/3:
`total=46415 complete=10023 archived=10023` — **36.392 Mails wurden gelesen und
verworfen.** Bei 22 Konten bedeutet der Nachtlauf rund **42 GB pro Nacht**
(~1,3 TB/Monat) von einem Anbieter, der gerade Mail verliert.
Die Idempotenz verhindert doppelte **Schreib**vorgaenge in Archiv und Ziel. Sie
verhindert **keinen** Doppel-Download. Diese Unterscheidung bitte auch in der
Doku sauber halten — wir haben sie selbst kurz verwechselt.
## Die Idee
`UID FETCH <cursor+1>:* (UID)` kostet einen Roundtrip statt 20 GB.
### Die UID ist ein CURSOR, keine Mail-Identitaet
Das ist die wichtigste Abgrenzung des ganzen Briefes, bitte nicht verwaschen:
- **`UIDVALIDITY + UID` entscheidet nur, WELCHE Mail neu untersucht werden
muss.** Das ist eine billige Vorauswahl von Kandidaten.
- **Erst fuer diese neuen UIDs wird der Body geladen, und daraus weiterhin die
kanonische Identitaet `(normalisierte Message-ID, body_sha256 ueber
canonicalMessageBytes)` berechnet.**
- **`body_sha256` bleibt die Wahrheit.** Die UID ersetzt sie an keiner Stelle.
Sie vermeidet ausschliesslich den erneuten Download alter Kandidaten.
Der Watcher schwaecht die Identitaetsgarantie also **nicht** ab — er spart nur
den Weg zu Kandidaten, die wir ohnehin verwerfen wuerden. Wer die UID zur
Identitaet befoerdert, hat den Brief falsch gelesen: UIDs sind
Server-Zusagen, keine Wahrheiten.
### Warum die UIDVALIDITY-Pruefung nicht optional ist
Die Zusage gilt nur, solange `UIDVALIDITY` konstant bleibt. **Deshalb ist die
UIDVALIDITY-Pruefung kein Beiwerk, sondern das, was die Abkuerzung ueberhaupt
erst zulaessig macht.** Faellt sie weg, tauschen wir einen teuren Beweis gegen
eine billige Annahme — bei diesem Anbieter die schlechteste aller Ideen.
## Was zu bauen ist
Neue Tabelle, pro Konto und Ordner:
```sql
CREATE TABLE uid_cursor (
account_id INTEGER NOT NULL,
folder TEXT NOT NULL,
uidvalidity INTEGER NOT NULL, -- vom Server bei SELECT
last_uid INTEGER NOT NULL, -- hoechste verarbeitete UID
checked_at INTEGER NOT NULL,
PRIMARY KEY (account_id, folder)
);
```
Ablauf je Ordner:
1. `SELECT` (readonly) → liefert `UIDVALIDITY` und `UIDNEXT`.
2. **Ist `uidvalidity` != gespeicherter Wert → ALARM + Volllauf.**
Cursor **nicht** stillschweigend fortschreiben. Nicht „reparieren".
Loggen, melden, komplett neu einlesen. Der Fall ist selten und teuer —
genau darum darf er nicht leise passieren.
3. Kein Eintrag vorhanden → Volllauf, danach Cursor setzen.
4. Sonst: `UID FETCH (last_uid+1):* (UID)`**nur UIDs, kein Body.** Fuer genau
diese UIDs dann den Body holen und durch die **unveraenderte** Pipeline
schicken: `AlreadyCopied -> mbox.Append -> dst.Append -> MarkCopied`.
Die Identitaet wird dort wie bisher aus dem Body berechnet — der Watcher
liefert Kandidaten, keine Identitaeten.
5. Cursor **erst nach erfolgreichem Durchlauf** aller neuen Mails setzen.
Bricht etwas ab, bleibt der alte Stand stehen. Lieber zweimal lesen als
einmal ueberspringen.
## Fallstricke
1. **Cursor niemals bei Fehlern fortschreiben.** Ein Abbruch nach Mail 5 von 20
darf den Cursor nicht auf 20 setzen. Im Zweifel: alter Wert bleibt.
2. **UID-Rueckgang** (`UIDNEXT` kleiner als `last_uid`) ist unmoeglich, solange
UIDVALIDITY gilt. Tritt er auf, ist der Server kaputt → ALARM, kein
Rechnen mit dem Wert.
3. **Der Watcher findet nur Neues, nichts Geloeschtes.** Das ist in Ordnung —
Geloeschtes soll das Archiv ja behalten. Aber es heisst: **der Watcher
ersetzt den Volllauf nicht als Beweis, nur als Alltag.** Ein
Voll-Abgleich (z. B. woechentlich) bleibt noetig.
4. **Neue Ordner** tauchen ohne Cursor auf → Fall 3, Volllauf fuer diesen
Ordner. Kein Sonderweg.
5. **Kein stiller Default.** Ohne Flag laeuft alles wie bisher. Der Watcher ist
ein eigener Modus (`--watch`, Intervall konfigurierbar, Vorschlag 5 min).
6. **Log muss den Modus zeigen**, z. B.
`watch <konto> <ordner>: uidvalidity=N cursor=M neu=K` — sonst sieht ein
Delta-Lauf spaeter wie ein vollstaendiger Lauf aus. Wir haben genug Zeit
damit verbracht, gruenen Meldungen zu misstrauen.
## Abnahme
1. `go test ./...` gruen.
2. Testkonto, zweiter Lauf ohne neue Mails: **`neu=0`, und im Netzwerk-Log darf
kein `BODY`-Fetch auftauchen.** Wird weiterhin alles geladen, ist der
ganze Sinn verfehlt.
3. Eine Mail zustellen → naechster Lauf holt genau **eine**.
4. UIDVALIDITY kuenstlich in der DB veraendern → **ALARM + Volllauf**, Cursor
wird nicht leise korrigiert. Dieser Test ist der wichtigste.
5. Abbruch mitten im Delta (Prozess killen) → Cursor unveraendert, naechster
Lauf holt dieselben Mails erneut, Idempotenz verhindert Doppelschreiben.
## Danach
Nachtlauf-Cron (`/etc/cron.d/mail-graveyard-nightly`) entfernen — er ist der
Uebergang, nicht das Ziel. Erst wenn Abnahme 2 und 4 belegt sind.