Replace hand-rolled IMAP with go-imap
This commit is contained in:
parent
21aa6e39e1
commit
253435d87f
7 changed files with 449 additions and 346 deletions
160
go-imap-migration.md
Normal file
160
go-imap-migration.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# Codex-Brief — Migrationskern auf emersion/go-imap/v2
|
||||
|
||||
Ziel: den handgerollten IMAP-Parser (`simpleIMAP` in `04-imap-source.go`, plus
|
||||
`05-imap-target.go`) durch **`github.com/emersion/go-imap/v2`** ersetzen. Das
|
||||
behebt in einem Zug die vier Integritäts-Bugs aus dem Review (siehe
|
||||
`REVIEW-FIXES.md`), die alle Symptome desselben Problems sind: eigener Parser
|
||||
statt Bibliothek.
|
||||
|
||||
**Warum:** Bei einem Beweis-/Compliance-Archiv ist Vollständigkeit der ganze
|
||||
Zweck. go-imap ist eine gepflegte pure-Go-Lib (keine Dependency-Lawine) und
|
||||
erledigt genau das, was der handgerollte Parser falsch macht.
|
||||
|
||||
## Bleibt UNVERÄNDERT (nicht anfassen)
|
||||
|
||||
- Die Interfaces und Typen: `SourceMailbox`, `TargetMailbox`, `RawMessage`,
|
||||
`Folder{Name,Attrs,Delim}`, `TargetFolder{Name,Attrs}`. Nur die *Implementierung*
|
||||
dahinter wird getauscht.
|
||||
- `06-mbox.go` (mbox-Writer), `11-folders.go` (Rollen-Mapping),
|
||||
`02-database.go`, die ganze Web-/UI-Schicht.
|
||||
- Die **Idempotenz-Reihenfolge** in `07-migrate.go`: prüfen → `dst.Append` →
|
||||
`mbox.Append` → **erst dann** `MarkCopied`. Nicht verändern.
|
||||
- Die drei Security-Modi `tls`/`starttls`/`none` + `insecure` (Semantik bleibt,
|
||||
nur die API dahinter wird go-imap).
|
||||
|
||||
## Neu bauen: `04-imap-source.go` + `05-imap-target.go`
|
||||
|
||||
Alles Handgerollte raus: `simpleIMAP`, `dialIMAP`, `parseListMailbox`,
|
||||
`fetchAll`, `parseFlags`, `parseInternalDate`, `appendMessage`. Und die
|
||||
irreführenden Kommentare („go-imap dekodiert…") stimmen danach endlich.
|
||||
|
||||
> `go-imap/v2` ist **Beta** — die exakten Signaturen gegen die gepinnte Version
|
||||
> prüfen (`go get github.com/emersion/go-imap/v2@latest`, dann die aufgelöste
|
||||
> Version in `go.mod` festnageln). Unten steht die *Form*, nicht das Evangelium.
|
||||
|
||||
### 1. Verbinden nach Security-Modus (`imapclient`)
|
||||
```go
|
||||
opts := &imapclient.Options{ TLSConfig: &tls.Config{
|
||||
ServerName: host, MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: a.SrcInsecure } }
|
||||
switch a.SrcSecurity {
|
||||
case "tls": c, err = imapclient.DialTLS(addr, opts) // implizit, 993
|
||||
case "starttls": c, err = imapclient.DialStartTLS(addr, opts) // 143 + Upgrade
|
||||
case "none": c, err = imapclient.DialInsecure(addr, opts) // reiner Klartext
|
||||
}
|
||||
c.Login(a.SrcUser, a.SrcPass).Wait()
|
||||
```
|
||||
`none` (`DialInsecure`) ist der Alt-Provider-Fall — muss ohne TLS funktionieren.
|
||||
|
||||
### 2. `Folders()` — LIST mit SPECIAL-USE + Delimiter
|
||||
```go
|
||||
cmd := c.List("", "*", &imap.ListOptions{ReturnSpecialUse: true})
|
||||
for { d := cmd.Next(); if d == nil { break }
|
||||
attrs := make([]string, len(d.Attrs))
|
||||
for i, at := range d.Attrs { attrs[i] = string(at) } // z.B. "\\Sent"
|
||||
folders = append(folders, Folder{
|
||||
Name: d.Mailbox, // **bereits UTF-8-dekodiert** -> Bug 1 weg
|
||||
Attrs: attrs, // -> 11-folders RoleFromAttrs funktioniert
|
||||
Delim: string(d.Delim) }) // -> Trenner-Übersetzung stimmt
|
||||
}
|
||||
```
|
||||
go-imap gibt `d.Mailbox` als dekodierten Klartext zurück → `Gelöschte Objekte`
|
||||
statt `Gel&APY-schte…`. Damit greift `RoleFromName`/`RoleFromAttrs` und die
|
||||
Standardordner werden **nicht mehr gedoppelt**.
|
||||
|
||||
### 3. `Fetch()` — FLAGS + INTERNALDATE + Body, ein Message pro Iteration
|
||||
```go
|
||||
c.Select(folder, nil).Wait()
|
||||
seq := imap.SeqSetRange(1, 0) // 1:* (alle)
|
||||
fo := &imap.FetchOptions{
|
||||
Flags: true, InternalDate: true, Envelope: true,
|
||||
BodySection: []*imap.FetchItemBodySection{{Peek: true}} } // BODY.PEEK[] -> setzt kein \Seen
|
||||
fcmd := c.Fetch(seq, fo)
|
||||
for {
|
||||
msg := fcmd.Next(); if msg == nil { break }
|
||||
buf, err := msg.Collect() // puffert GENAU EINE Mail -> kein OOM mehr
|
||||
raw := RawMessage{
|
||||
MessageID: messageID(buf), // s.u.
|
||||
Body: buf.FindBodySection(&imap.FetchItemBodySection{Peek:true}),
|
||||
Flags: sanitizeFlags(buf.Flags), // s.u. — \Recent raus
|
||||
InternalDate: buf.InternalDate } // go-imap liefert echte time.Time -> Bug 3 weg
|
||||
// ... an Callback/Slice geben
|
||||
}
|
||||
```
|
||||
`buf.Flags`/`buf.InternalDate`/`buf.Envelope` sind strukturiert und
|
||||
**reihenfolge-unabhängig** geparst → Bug 4 weg. `InternalDate` als `time.Time`
|
||||
direkt aus der Lib → Bug 3 (Leerzeichen-Tag) weg.
|
||||
|
||||
### 4. `sanitizeFlags` — `\Recent`/`\*` beim APPEND filtern (Bug 2)
|
||||
```go
|
||||
func sanitizeFlags(in []imap.Flag) []imap.Flag {
|
||||
var out []imap.Flag
|
||||
for _, f := range in {
|
||||
if f == imap.FlagRecent || f == imap.FlagWildcard { continue }
|
||||
out = append(out, f)
|
||||
}
|
||||
return out
|
||||
}
|
||||
```
|
||||
`\Recent` per APPEND ist RFC-verboten → Server antwortet `NO` → Mail ginge sonst
|
||||
verloren. Muss raus.
|
||||
|
||||
### 5. Message-ID (Dedup-Schlüssel) — aus Envelope, mit Hash-Fallback
|
||||
```go
|
||||
func messageID(buf *imapclient.FetchMessageBuffer) string {
|
||||
if buf.Envelope != nil && buf.Envelope.MessageID != "" { return buf.Envelope.MessageID }
|
||||
return "sha256:" + hex(sha256(body)) // Mails ohne Message-ID trotzdem deduplizieren
|
||||
}
|
||||
```
|
||||
Verbessert das aktuelle Verhalten (Mails ohne Message-ID wurden bisher bei jedem
|
||||
Lauf erneut kopiert → Dubletten).
|
||||
|
||||
### 6. Ziel (`05-imap-target.go`)
|
||||
- `Folders()` / `Delim()`: gleiche LIST-Logik wie oben, in `[]TargetFolder`.
|
||||
- `EnsureFolder(name)`: `c.Create(name, nil).Wait()`; „already exists" schlucken;
|
||||
danach `c.Subscribe(name).Wait()`. **Verschachtelte Ordner:** wenn `name`
|
||||
Trenner enthält, Eltern-Pfade zuerst anlegen (manche Server legen sie nicht
|
||||
automatisch an — Review-Bug 6).
|
||||
- `Append(folder, m)`:
|
||||
```go
|
||||
opts := &imap.AppendOptions{ Flags: sanitizeFlags(m.Flags), Time: m.InternalDate }
|
||||
ac := c.Append(folder, int64(len(m.Body)), opts)
|
||||
ac.Write(m.Body); ac.Close()
|
||||
_, err := ac.Wait()
|
||||
```
|
||||
Flags + Originaldatum bleiben erhalten (Regel 1). Nie SMTP.
|
||||
|
||||
## Streaming (Schritt 2 — nach dem Kern-Swap, separat verifizieren)
|
||||
|
||||
Damit ein GB-Postfach nicht in den RAM läuft, `Fetch` von „Slice zurückgeben"
|
||||
auf **Callback pro Mail** umstellen:
|
||||
```go
|
||||
// Interface:
|
||||
Fetch(folder string, fn func(RawMessage) error) error
|
||||
```
|
||||
In `07-migrate.go` wandert die Pro-Mail-Schleife (dedup → Append → mbox →
|
||||
MarkCopied) **in den Callback** — die Reihenfolge bleibt exakt gleich, nur der
|
||||
Rahmen ändert sich. So ist immer nur eine Mail im Speicher.
|
||||
|
||||
## go.mod
|
||||
`github.com/emersion/go-imap/v2` hinzufügen, `go mod tidy`. (Zieht go-sasl/
|
||||
go-message transitiv; kein cgo — bleibt single-binary.)
|
||||
|
||||
## Abnahme (mit unserem echten Testkorpus)
|
||||
Nutze die **9 Test-Mails in `vdevop-02@golddata.eu`** (Text, Anhänge einzeln/
|
||||
mehrfach, ZIP, HTML-Body, Umlaut-Betreff) als Quelle:
|
||||
1. `go test ./...` grün (inkl. `sanitizeFlags`- und `messageID`-Unit-Tests).
|
||||
2. Migration **zweimal** laufen → zweiter Lauf kopiert **0** (Idempotenz).
|
||||
3. Ziel-`INTERNALDATE` = Originaldatum der Quelle (nicht „heute") — inkl. einer
|
||||
Mail mit einstelligem Tag.
|
||||
4. Quell-Ordner `Gesendete Objekte`/`Papierkorb` landen im **vorhandenen**
|
||||
Rollen-Ordner des Ziels, **keine Dublette**.
|
||||
5. Eine Mail mit `\Recent` in der Quelle wird migriert (nicht mit `NO` abgelehnt).
|
||||
6. Anhänge (PDF/PNG/ZIP) und Umlaut-Betreff kommen **byte-identisch** an
|
||||
(Quelle-Body == Ziel-Body == mbox-Body).
|
||||
|
||||
## NICHT in diesem Schritt
|
||||
- POP3-Fallback verdrahten (`src_proto=pop3` → `OpenPOP3Source` in
|
||||
`migrateAccount`) — eigener kleiner Folgeschritt.
|
||||
- Nicht-ASCII-Betreff-Encoding im Forward (RFC 2047) — gehört zum späteren
|
||||
kontrollierten Forward-Endpunkt.
|
||||
Loading…
Add table
Add a link
Reference in a new issue