Der Volllauf laedt strukturell jede Quellmail, bevor er weiss, ob er sie hat - die Identitaet ist der Body-Hash. Belegt am Lauf e.klarner: 46.415 gelesen, 10.023 geschrieben. Der Nachtlauf zieht so ~42 GB/Nacht. Die UID ist die einzige Kennung ohne Body und bricht die Ringabhaengigkeit. UIDVALIDITY-Pruefung ist dabei nicht Beiwerk, sondern Voraussetzung. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
4.6 KiB
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:
// 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 Schreibvorgaenge 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 <cursor+1>:* (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:
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:
SELECT(readonly) → liefertUIDVALIDITYundUIDNEXT.- 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. - Kein Eintrag vorhanden → Volllauf, danach Cursor setzen.
- 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. - Cursor erst nach erfolgreichem Durchlauf aller neuen Mails setzen. Bricht etwas ab, bleibt der alte Stand stehen. Lieber zweimal lesen als einmal ueberspringen.
Fallstricke
- Cursor niemals bei Fehlern fortschreiben. Ein Abbruch nach Mail 5 von 20 darf den Cursor nicht auf 20 setzen. Im Zweifel: alter Wert bleibt.
- UID-Rueckgang (
UIDNEXTkleiner alslast_uid) ist unmoeglich, solange UIDVALIDITY gilt. Tritt er auf, ist der Server kaputt → ALARM, kein Rechnen mit dem Wert. - 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.
- Neue Ordner tauchen ohne Cursor auf → Fall 3, Volllauf fuer diesen Ordner. Kein Sonderweg.
- Kein stiller Default. Ohne Flag laeuft alles wie bisher. Der Watcher ist
ein eigener Modus (
--watch, Intervall konfigurierbar, Vorschlag 5 min). - 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
go test ./...gruen.- Testkonto, zweiter Lauf ohne neue Mails:
neu=0, und im Netzwerk-Log darf keinBODY-Fetch auftauchen. Wird weiterhin alles geladen, ist der ganze Sinn verfehlt. - Eine Mail zustellen → naechster Lauf holt genau eine.
- UIDVALIDITY kuenstlich in der DB veraendern → ALARM + Volllauf, Cursor wird nicht leise korrigiert. Dieser Test ist der wichtigste.
- 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.