Mail-Graveyard/ui-fetch-brief.md
2026-07-14 18:23:31 +02:00

4.1 KiB
Raw Blame History

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

fetchSourceFolderMessagescollectSourceMessages (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:124fetchAll.

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 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) 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.