# Codex-Brief — UI-Fetch: Header-only + Pagination Der Migrationspfad streamt jetzt (bewiesen: 80 MB Ordner durch eine harte 16-MiB-Grenze). **Die UI tut es nicht.** Sie ist damit das letzte OOM-Loch — und das gefährlichere, weil ein Mensch es mit **einem Klick** auslöst. ## Das Problem `fetchSourceFolderMessages` → `collectSourceMessages` ([00-router.go:1548](backend/00-router.go)) sammelt **alle Mails eines Ordners samt vollem Body** in eine Slice — nur um eine **Liste** zu rendern. Aufgerufen an **sechs** Stellen: `00-router.go:397, 422, 1286, 1369, 1585, 1602`. Dasselbe auf der Zielseite: [05-imap-target.go:124](backend/05-imap-target.go) → `fetchAll`. Rechnung für ein echtes `@dr-gold.de`-Postfach: 20.000 Mails × 200 KB = **~4 GB RAM für einen Klick auf einen Ordner** → Container wird OOM-gekillt. **Der Kern der Sache:** Eine Nachrichtenliste braucht **Betreff, Absender, Datum, Flags** — sie braucht **keine Bodies**. Wir holen also Daten, die wir wegwerfen. ## Der Fix ### 1. Header-only-Fetch (neu) `ENVELOPE` liefert Betreff/Absender/Datum/Message-ID — **ohne** `BodySection`: ```go type MessageHeader struct { UID uint32 MessageID string Subject string From string Date time.Time InternalDate time.Time Flags []string Size uint32 // RFC822.SIZE, fuer die Anzeige } // Fenster: neueste `limit` Mails ab `offset`. NIE unbegrenzt 1:*. Headers(folder string, limit, offset int) ([]MessageHeader, error) ``` Implementierung (go-imap/v2) — beachte: **kein `BodySection`**: ```go fo := &imap.FetchOptions{ Envelope: true, Flags: true, InternalDate: true, UID: true, RFC822Size: true, // BodySection: ABSICHTLICH NICHT } ``` Damit ist der Speicher proportional zur **Fenstergröße**, nicht zur Ordnergröße. ### 2. Fenster statt „alles" Immer ein Bereich (z. B. die **neuesten 200**), berechnet aus `selected.NumMessages`. Blättern über `offset`. Kein `1:*` für Listen — nirgends. ### 3. Voller Body erst beim Öffnen **einer** Mail ```go FetchOne(folder string, uid uint32) (RawMessage, error) // mit BodySection ``` Der Lesebereich holt genau **eine** Mail. Das ist der einzige Ort, an dem ein Body in den Speicher gehört. ### 4. Migration bleibt unangetastet `Fetch(folder string, fn func(RawMessage) error) error` ist der **Migrationspfad** und bleibt exakt wie er ist (inkl. der verifizierten Reihenfolge `AlreadyCopied → dst.Append → mbox.Append → MarkCopied`). Nicht anfassen. ## Umzubauen - Die **sechs** `fetchSourceFolderMessages`-Aufrufer auf `Headers(...)` umstellen (Listen) bzw. auf `FetchOne(...)` (Lesebereich). - Zielseite: `05-imap-target.go:124` (`fetchAll`) genauso. - `collectSourceMessages` und `fetchAll` danach **löschen** — sie sind die Falle; solange sie existieren, greift sie jemand wieder auf. - `10-pop3.go` mitziehen (POP3 kennt keine Envelope-Fetches → dort Header aus dem `TOP`-Kommando bauen oder die Methode als „nicht unterstuetzt" zurueckgeben). ## Kleinigkeit aus dem letzten Review (gern mitnehmen) In `fetchEach` ([04-imap-source.go](backend/04-imap-source.go)) wurde `defer cmd.Close()` durch explizite Closes ersetzt. Alle Pfade sind abgedeckt, aber bei einem Panic im Callback bleibt das FETCH-Kommando offen und die Verbindung kaputt. `defer` wieder rein (Doppel-Close ist harmlos). ## Abnahme 1. `go test ./...` grün. 2. Funktional unverändert: Listen zeigen weiterhin Betreff/Absender/Datum, Öffnen einer Mail zeigt weiterhin den vollen Inhalt inkl. Anhängen. 3. **Speicher (fahre ich):** Ordner mit ~40 Mails à 2 MB (80 MB), Web-Container mit harter 64-MiB-Grenze, dann per HTTP die Ordner-Liste abrufen. Vorher: OOM-Kill. Nachher: Liste kommt, Speicher bleibt flach. Ich setze das auf und verifiziere es — du baust nur den Code. ## NICHT in diesem Schritt - Der lokale **mbox-Viewer** (`ReadMboxList`) liest die ganze Datei, hält aber nur Header — Speicher ist unkritisch. Pagination dort später, reine Kosmetik. - POP3 vollständig verdrahten, DB-Namen-Altlast (`emailforwarder.db` vs. `mail-graveyard.db` + zwei 0-Byte-Leichen), RFC-2047-Betreff im Forward.