# Codex-Brief — Streaming (Schritt 2 aus `go-imap-migration.md`) Ziel: `Fetch` puffert nicht länger **alle Bodies eines Ordners** im RAM, sondern verarbeitet **eine Mail nach der anderen**. Das ist der **letzte Blocker vor den echten `@dr-gold.de`-Postfächern** — deren Backup-Ordner sind noch leer, und da reden wir über GB statt über die 199 kleinen Test-Mails. Aktuell: `src.Fetch(folder) ([]RawMessage, error)` lädt den kompletten Ordner in eine Slice. Ein Postfach mit 50.000 Mails × 200 KB = **10 GB im RAM** → OOM. ## 1. Interface-Änderung (`04-imap-source.go`) ```go type SourceMailbox interface { Folders() ([]Folder, error) Fetch(folder string, fn func(RawMessage) error) error // <— NEU: Callback Close() error } ``` Implementierung mit go-imap/v2 — **genau eine Mail gleichzeitig im Speicher**: ```go func (s *imapSource) Fetch(folder string, fn func(RawMessage) error) error { sel, err := s.c.Select(folder, &imap.SelectOptions{ReadOnly: true}).Wait() if err != nil { return err } if sel.NumMessages == 0 { return nil } // Empty-Guard BLEIBT (kein FETCH 1:* auf leer) fo := &imap.FetchOptions{ Flags: true, InternalDate: true, Envelope: true, BodySection: []*imap.FetchItemBodySection{{Peek: true}}, } fcmd := s.c.Fetch(imap.SeqSetRange(1, 0), fo) // 1:* defer fcmd.Close() for { msg := fcmd.Next() if msg == nil { break } buf, err := msg.Collect() // puffert GENAU DIESE eine Mail if err != nil { return err } if err := fn(buildRawMessage(buf)); err != nil { return err // nur FATAL -> Ordner abbrechen } // buf/raw gehen hier out of scope -> GC gibt den Body frei } return fcmd.Close() } ``` ## 2. `07-migrate.go` — die Pro-Mail-Schleife wandert in den Callback **Die Reihenfolge bleibt Zeichen für Zeichen dieselbe** — sie ist die Idempotenz-Garantie und ist live verifiziert. Nicht umsortieren: ```go total, done, errs := 0, 0, 0 err := src.Fetch(srcFolder, func(m RawMessage) error { total++ already, err := AlreadyCopied(a.ID, srcFolder, m.MessageID) if err != nil { errs++; log.Printf(...); return nil } if already { return nil } if err := dst.Append(dstFolder, m); err != nil { errs++; log.Printf(...); return nil } if err := mbox.Append(srcFolder, m); err != nil { errs++; log.Printf(...); return nil } if err := MarkCopied(a.ID, srcFolder, m.MessageID); err != nil { errs++; log.Printf(...); return nil } done++ return nil }) ``` `total` wird jetzt **während** des Streams gezählt (vorher `len(msgs)`). Log-Zeile pro Ordner **unverändert lassen**: `migration -> : total=N copied=M errors=E` — daran hängen meine Prüfungen und deine Abnahme. ## 3. Die fünf Regressions-Fallen (bitte ernst nehmen) 1. **Der Callback darf den Ordner NICHT abbrechen.** Ein Fehler an *einer* Mail → `errs++`, loggen, **`return nil`** (weiterstreamen). Nur ein wirklich fataler Zustand gibt einen Fehler zurück. Sonst killt eine einzige kaputte Mail die Migration des ganzen Ordners — heute tut sie das nicht. 2. **Im Callback NIEMALS Kommandos auf der Quell-Verbindung absetzen.** Der FETCH läuft noch auf `s.c`; ein `Select`/`List` mittendrin zerlegt den Protokollstrom. Der Callback fasst nur **Ziel**, **mbox** und **SQLite** an — das ist ok, andere Verbindungen. 3. **Empty-Folder-Guard behalten** (`NumMessages == 0` → sofort `return nil`). Der Fix darf nicht beim Refactor verloren gehen. 4. **Reihenfolge behalten:** `AlreadyCopied` → `dst.Append` → `mbox.Append` → `MarkCopied`. Ein Crash zwischen Append und MarkCopied darf lieber doppelt kopieren als eine Mail als „erledigt" markieren, die nie ankam. 5. **`--folders`-Scoping und `--watch` müssen weiter funktionieren** — `RunMigrationFolders` / `selectedFolders` bleiben unverändert. Nebenbei: `10-pop3.go` muss die neue Interface-Signatur mitziehen (auch wenn es noch ein Stub ist), sonst baut es nicht. ## 4. Abnahme 1. `go test ./...` grün. 2. **Keine Verhaltensänderung** — die bestehende Abnahme muss identisch laufen: - `--run codex-abnahme-vdevop02-to-vdevop03` zweimal → zweiter Lauf `copied=0` - `Gelöschte Objekte -> Papierkorb` (keine Dublette), `Entwürfe -> Entwürfe` - Ziel-`INTERNALDATE` weiter `07-Mar-2024` (Originaldatum, nicht heute) - Anhänge byte-identisch 3. **Der eigentliche Beweis — Speicher:** Leg in der Quelle einen Testordner mit z. B. **200 Mails à ~1 MB** an. Alt: RSS wächst auf ~200 MB+ (ganzer Ordner im RAM). Neu: RSS bleibt **flach** (nur eine Mail). Messen z. B. mit `podman stats` oder `/proc//status` `VmRSS` während des Laufs. → **Diesen Speicher-Test fahre und verifiziere ich** (ich bin Test/Review), du musst ihn nicht selbst aufsetzen — bau nur den Code. ## 5. NICHT in diesem Schritt - **Echtes Body-Streaming pro Mail** (Body als `io.Reader` direkt in APPEND + mbox tee-en, statt `Collect()`). Damit wäre auch eine 500-MB-Einzelmail unkritisch. Aktuell hält man **eine** Mail im RAM — das löst das gestellte Problem (GB-Postfach) vollständig; Einzelmails sind serverseitig ohnehin gedeckelt. Später bei Bedarf. - POP3 verdrahten, DB-Namen-Altlast (`emailforwarder.db` vs. `mail-graveyard.db` + zwei 0-Byte-Leichen), RFC-2047-Betreff im Forward.