Use header-only IMAP lists

This commit is contained in:
DonVoo 2026-07-14 18:23:31 +02:00
parent a52963c380
commit a17d2c8a2c
5 changed files with 456 additions and 184 deletions

99
ui-fetch-brief.md Normal file
View file

@ -0,0 +1,99 @@
# 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.