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>
108 lines
4.6 KiB
Markdown
108 lines
4.6 KiB
Markdown
# 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 <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:
|
|
|
|
```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 <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.
|