diff --git a/uid-delta-brief.md b/uid-delta-brief.md new file mode 100644 index 0000000..7ebd640 --- /dev/null +++ b/uid-delta-brief.md @@ -0,0 +1,108 @@ +# 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 + +**Die UID ist die einzige Kennung, die ohne Body zu haben ist.** Sie bricht die +Ringabhaengigkeit. `UID FETCH :* (UID)` kostet einen Roundtrip statt +20 GB. + +Der Preis: UIDs sind Server-Zusagen, keine Wahrheiten. Sie gelten nur, solange +`UIDVALIDITY` konstant bleibt. **Deshalb ist die UIDVALIDITY-Pruefung kein +Beiwerk, sondern das, was die Abkuerzung ueberhaupt erst zulaessig macht.** +Ohne sie 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 diese UIDs mit Body holen und + durch die **unveraenderte** Pipeline schicken: + `AlreadyCopied -> mbox.Append -> dst.Append -> MarkCopied`. +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 : 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.