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

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=1002336.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 + 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:

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.