4.1 KiB
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)
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 → 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:
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:
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
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 aufHeaders(...)umstellen (Listen) bzw. aufFetchOne(...)(Lesebereich). - Zielseite:
05-imap-target.go:124(fetchAll) genauso. collectSourceMessagesundfetchAlldanach löschen — sie sind die Falle; solange sie existieren, greift sie jemand wieder auf.10-pop3.gomitziehen (POP3 kennt keine Envelope-Fetches → dort Header aus demTOP-Kommando bauen oder die Methode als „nicht unterstuetzt" zurueckgeben).
Kleinigkeit aus dem letzten Review (gern mitnehmen)
In fetchEach (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
go test ./...grün.- Funktional unverändert: Listen zeigen weiterhin Betreff/Absender/Datum, Öffnen einer Mail zeigt weiterhin den vollen Inhalt inkl. Anhängen.
- 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.dbvs.mail-graveyard.db+ zwei 0-Byte-Leichen), RFC-2047-Betreff im Forward.