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>
5.5 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
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 + UIDentscheidet 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_sha256bleibt 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:
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 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. - 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.