# 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.