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>
126 lines
5.5 KiB
Markdown
126 lines
5.5 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
|
|
|
|
`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:
|
|
|
|
```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 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.
|