diff --git a/backend/00-router.go b/backend/00-router.go
index 31f805f..14f7ab4 100644
--- a/backend/00-router.go
+++ b/backend/00-router.go
@@ -14,11 +14,8 @@ import (
"path/filepath"
"strconv"
"strings"
- "time"
)
-const mailboxListLimit = 200
-
func RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/", homeHandler)
mux.HandleFunc("/login", loginHandler)
@@ -397,13 +394,13 @@ func sourceMailboxMessagesHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `
Nachrichten
%s
`, html.EscapeString(err.Error()))
return
}
- headers, err := fetchSourceFolderHeaders(account, folder)
+ messages, err := fetchSourceFolderMessages(account, folder)
if err != nil {
fmt.Fprintf(w, `Nachrichten
%s
`, html.EscapeString(err.Error()))
return
}
- renderSourceMailboxMessageList(w, account, folder, headers)
- fmt.Fprintf(w, `%s
`, renderSourceMailboxRead(account, folder, firstHeaderUID(headers)))
+ renderSourceMailboxMessageList(w, account, folder, messages)
+ fmt.Fprintf(w, `%s
`, renderSourceMailboxRead(account, folder, 0, messages))
}
func sourceMailboxMessageHandler(w http.ResponseWriter, r *http.Request) {
@@ -412,19 +409,23 @@ func sourceMailboxMessageHandler(w http.ResponseWriter, r *http.Request) {
}
accountName := strings.TrimSpace(r.URL.Query().Get("source"))
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
- uid := parseUIDParam(r)
+ index, err := strconv.Atoi(r.URL.Query().Get("index"))
+ if err != nil {
+ http.Error(w, "bad index", http.StatusBadRequest)
+ return
+ }
account, err := GetAccount(accountName)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- raw, err := fetchSourceMailboxMessage(account, folder, uid)
+ messages, err := fetchSourceFolderMessages(account, folder)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
- fmt.Fprint(w, renderSourceMailboxReadMessage(account, folder, raw))
+ fmt.Fprint(w, renderSourceMailboxRead(account, folder, index, messages))
}
func targetMailboxMessagesHandler(w http.ResponseWriter, r *http.Request) {
@@ -443,13 +444,13 @@ func targetMailboxMessagesHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `Nachrichten
%s
`, html.EscapeString(err.Error()))
return
}
- headers, err := fetchTargetFolderHeaders(account, folder)
+ messages, err := fetchTargetFolderMessages(account, folder)
if err != nil {
fmt.Fprintf(w, `Nachrichten
%s
`, html.EscapeString(err.Error()))
return
}
- renderTargetMailboxMessageList(w, account, folder, headers)
- fmt.Fprintf(w, `%s
`, renderTargetMailboxRead(account, folder, firstHeaderUID(headers)))
+ renderTargetMailboxMessageList(w, account, folder, messages)
+ fmt.Fprintf(w, `%s
`, renderTargetMailboxRead(account, folder, 0, messages))
}
func targetMailboxMessageHandler(w http.ResponseWriter, r *http.Request) {
@@ -458,19 +459,23 @@ func targetMailboxMessageHandler(w http.ResponseWriter, r *http.Request) {
}
accountName := strings.TrimSpace(r.URL.Query().Get("target"))
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
- uid := parseUIDParam(r)
+ index, err := strconv.Atoi(r.URL.Query().Get("index"))
+ if err != nil {
+ http.Error(w, "bad index", http.StatusBadRequest)
+ return
+ }
account, err := GetAccount(accountName)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
- raw, err := fetchTargetMailboxMessage(account, folder, uid)
+ messages, err := fetchTargetFolderMessages(account, folder)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
- fmt.Fprint(w, renderTargetMailboxReadMessage(account, folder, raw))
+ fmt.Fprint(w, renderTargetMailboxRead(account, folder, index, messages))
}
func archiveCreateHandler(w http.ResponseWriter, r *http.Request) {
@@ -994,7 +999,7 @@ func transferMessagesHandler(w http.ResponseWriter, r *http.Request) {
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, renderTransferMessageList(side, kind, value, folder))
- fmt.Fprintf(w, `%s
`, renderTransferPreview(kind, value, folder, 0, 0))
+ fmt.Fprintf(w, `%s
`, renderTransferPreview(kind, value, folder, 0))
}
func transferMessageHandler(w http.ResponseWriter, r *http.Request) {
@@ -1004,10 +1009,13 @@ func transferMessageHandler(w http.ResponseWriter, r *http.Request) {
kind := strings.TrimSpace(r.URL.Query().Get("kind"))
value := strings.TrimSpace(r.URL.Query().Get("value"))
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
- index, _ := strconv.Atoi(r.URL.Query().Get("index"))
- uid := parseUIDParam(r)
+ index, err := strconv.Atoi(r.URL.Query().Get("index"))
+ if err != nil {
+ http.Error(w, "bad index", http.StatusBadRequest)
+ return
+ }
w.Header().Set("Content-Type", "text/html; charset=utf-8")
- fmt.Fprint(w, renderTransferPreview(kind, value, folder, index, uid))
+ fmt.Fprint(w, renderTransferPreview(kind, value, folder, index))
}
func transferSide(value string) string {
@@ -1175,12 +1183,12 @@ func renderInitialTransferList(side, kind string, accounts []Account, archives [
func renderInitialTransferPreview(leftKind, leftValue, rightKind, rightValue string) string {
if leftValue != "" {
if folder, err := firstTransferFolder(leftKind, leftValue); err == nil {
- return renderTransferPreview(leftKind, leftValue, folder, 0, 0)
+ return renderTransferPreview(leftKind, leftValue, folder, 0)
}
}
if rightValue != "" {
if folder, err := firstTransferFolder(rightKind, rightValue); err == nil {
- return renderTransferPreview(rightKind, rightValue, folder, 0, 0)
+ return renderTransferPreview(rightKind, rightValue, folder, 0)
}
}
return renderEmptyPreviewPane("Nachricht links oder rechts waehlen.")
@@ -1263,45 +1271,56 @@ func renderTransferMessageList(side, kind, value, folder string) string {
fmt.Fprintf(&b, `%s
`, html.EscapeString(err.Error()))
return b.String()
}
- messages, err := fetchTargetFolderHeaders(account, folder)
+ messages, err := fetchTargetFolderMessages(account, folder)
if err != nil {
fmt.Fprintf(&b, `%s
`, html.EscapeString(err.Error()))
return b.String()
}
- renderTransferHeaders(&b, side, kind, value, folder, messages)
+ renderTransferRawMessages(&b, side, kind, value, folder, messages)
default:
account, err := GetAccount(value)
if err != nil {
fmt.Fprintf(&b, `%s
`, html.EscapeString(err.Error()))
return b.String()
}
- messages, err := fetchSourceFolderHeaders(account, folder)
+ messages, err := fetchSourceFolderMessages(account, folder)
if err != nil {
fmt.Fprintf(&b, `%s
`, html.EscapeString(err.Error()))
return b.String()
}
- renderTransferHeaders(&b, side, kind, value, folder, messages)
+ renderTransferRawMessages(&b, side, kind, value, folder, messages)
}
return b.String()
}
-func renderTransferHeaders(b *strings.Builder, side, kind, value, folder string, messages []MessageHeader) {
- for i, header := range messages {
- subject, from, date := headerListFields(header)
+func renderTransferRawMessages(b *strings.Builder, side, kind, value, folder string, messages []RawMessage) {
+ for i, raw := range messages {
+ subject := "(ohne Betreff)"
+ from := "(ohne Absender)"
+ date := ""
+ if msg, err := mail.ReadMessage(bytes.NewReader(raw.Body)); err == nil {
+ if v := decodeHeader(msg.Header.Get("Subject")); v != "" {
+ subject = v
+ }
+ if v := decodeHeader(msg.Header.Get("From")); v != "" {
+ from = v
+ }
+ date = decodeHeader(msg.Header.Get("Date"))
+ }
active := ""
if i == 0 {
active = " active"
}
- fmt.Fprintf(b, `%s%s%s`,
- active, side, html.EscapeString(kind), urlEsc(value), urlEsc(folder), header.UID, html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
+ fmt.Fprintf(b, `%s%s%s`,
+ active, side, html.EscapeString(kind), urlEsc(value), urlEsc(folder), i, html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
}
if len(messages) == 0 {
b.WriteString(`Keine Nachrichten.
`)
}
}
-func renderTransferPreview(kind, value, folder string, index int, uid uint32) string {
- raw, label, err := transferRawMessage(kind, value, folder, index, uid)
+func renderTransferPreview(kind, value, folder string, index int) string {
+ raw, label, err := transferRawMessage(kind, value, folder, index)
if err != nil {
return renderEmptyPreviewPane(err.Error())
}
@@ -1320,7 +1339,7 @@ func renderTransferPreview(kind, value, folder string, index int, uid uint32) st
return b.String()
}
-func transferRawMessage(kind, value, folder string, index int, uid uint32) ([]byte, string, error) {
+func transferRawMessage(kind, value, folder string, index int) ([]byte, string, error) {
switch kind {
case "archive":
path, err := mboxPath(value, folder)
@@ -1334,15 +1353,27 @@ func transferRawMessage(kind, value, folder string, index int, uid uint32) ([]by
if err != nil {
return nil, value, err
}
- msg, err := fetchTargetMailboxMessage(account, folder, uid)
- return msg.Body, account.DstUser, err
+ messages, err := fetchTargetFolderMessages(account, folder)
+ if err != nil {
+ return nil, account.DstUser, err
+ }
+ if index < 0 || index >= len(messages) {
+ return nil, account.DstUser, fmt.Errorf("Nachricht waehlen.")
+ }
+ return messages[index].Body, account.DstUser, nil
default:
account, err := GetAccount(value)
if err != nil {
return nil, value, err
}
- msg, err := fetchSourceMailboxMessage(account, folder, uid)
- return msg.Body, account.SrcUser, err
+ messages, err := fetchSourceFolderMessages(account, folder)
+ if err != nil {
+ return nil, account.SrcUser, err
+ }
+ if index < 0 || index >= len(messages) {
+ return nil, account.SrcUser, fmt.Errorf("Nachricht waehlen.")
+ }
+ return messages[index].Body, account.SrcUser, nil
}
}
@@ -1514,35 +1545,33 @@ func sourceFolderRole(folder Folder) string {
return ""
}
-func fetchSourceFolderHeaders(account Account, folder string) ([]MessageHeader, error) {
+func fetchSourceFolderMessages(account Account, folder string) ([]RawMessage, error) {
if accountProto(account) == "pop3" {
- return nil, fmt.Errorf("POP3-Headerlisten sind noch nicht unterstuetzt.")
+ src, err := OpenPOP3Source(account)
+ if err != nil {
+ return nil, err
+ }
+ defer src.Close()
+ return collectSourceMessages(src, folder)
}
src, err := OpenIMAPSource(account)
if err != nil {
return nil, err
}
defer src.Close()
- return src.Headers(folder, mailboxListLimit, 0)
+ return collectSourceMessages(src, folder)
}
-func fetchSourceMailboxMessage(account Account, folder string, uid uint32) (RawMessage, error) {
- if accountProto(account) == "pop3" {
- return RawMessage{}, fmt.Errorf("POP3-Nachrichtenansicht ist noch nicht unterstuetzt.")
- }
- src, err := OpenIMAPSource(account)
+func collectSourceMessages(src SourceMailbox, folder string) ([]RawMessage, error) {
+ var messages []RawMessage
+ err := src.Fetch(folder, func(m RawMessage) error {
+ messages = append(messages, m)
+ return nil
+ })
if err != nil {
- return RawMessage{}, err
+ return messages, err
}
- defer src.Close()
- if uid == 0 {
- headers, err := src.Headers(folder, 1, 0)
- if err != nil {
- return RawMessage{}, err
- }
- uid = firstHeaderUID(headers)
- }
- return src.FetchOne(folder, uid)
+ return messages, nil
}
func renderInitialSourceMailboxList(account Account) string {
@@ -1553,12 +1582,12 @@ func renderInitialSourceMailboxList(account Account) string {
if err != nil {
return fmt.Sprintf(`Nachrichten
%s
`, html.EscapeString(err.Error()))
}
- headers, err := fetchSourceFolderHeaders(account, folder)
+ messages, err := fetchSourceFolderMessages(account, folder)
if err != nil {
return fmt.Sprintf(`Nachrichten
%s
`, html.EscapeString(err.Error()))
}
var b strings.Builder
- renderSourceMailboxMessageList(&b, account, folder, headers)
+ renderSourceMailboxMessageList(&b, account, folder, messages)
return b.String()
}
@@ -1570,11 +1599,11 @@ func renderInitialSourceMailboxRead(account Account) string {
if err != nil {
return `Nachricht waehlen.
`
}
- headers, err := fetchSourceFolderHeaders(account, folder)
+ messages, err := fetchSourceFolderMessages(account, folder)
if err != nil {
return `Nachricht waehlen.
`
}
- return renderSourceMailboxRead(account, folder, firstHeaderUID(headers))
+ return renderSourceMailboxRead(account, folder, 0, messages)
}
func firstSourceFolder(account Account) (string, error) {
@@ -1601,18 +1630,29 @@ func firstSourceFolder(account Account) (string, error) {
return folders[0].Name, nil
}
-func renderSourceMailboxMessageList(w io.Writer, account Account, folder string, messages []MessageHeader) {
+func renderSourceMailboxMessageList(w io.Writer, account Account, folder string, messages []RawMessage) {
fmt.Fprint(w, `Nachrichten
`)
- for i, header := range messages {
- subject, from, date := headerListFields(header)
+ for i, raw := range messages {
+ subject := "(ohne Betreff)"
+ from := "(ohne Absender)"
+ date := ""
+ if msg, err := mail.ReadMessage(bytes.NewReader(raw.Body)); err == nil {
+ if v := decodeHeader(msg.Header.Get("Subject")); v != "" {
+ subject = v
+ }
+ if v := decodeHeader(msg.Header.Get("From")); v != "" {
+ from = v
+ }
+ date = decodeHeader(msg.Header.Get("Date"))
+ }
active := ""
if i == 0 {
active = " active"
}
- fmt.Fprintf(w, `%s%s%s`,
+ fmt.Fprintf(w, `%s%s%s`,
active,
- html.EscapeString(account.Name), html.EscapeString(folder), header.UID,
- urlEsc(account.Name), urlEsc(folder), header.UID,
+ html.EscapeString(account.Name), html.EscapeString(folder), i,
+ urlEsc(account.Name), urlEsc(folder), i,
html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
}
if len(messages) == 0 {
@@ -1620,29 +1660,22 @@ func renderSourceMailboxMessageList(w io.Writer, account Account, folder string,
}
}
-func renderSourceMailboxRead(account Account, folder string, uid uint32) string {
- if uid == 0 {
+func renderSourceMailboxRead(account Account, folder string, index int, messages []RawMessage) string {
+ if index < 0 || index >= len(messages) {
return `Nachricht waehlen.
`
}
- raw, err := fetchSourceMailboxMessage(account, folder, uid)
- if err != nil {
- return `Nachricht waehlen.
`
- }
- return renderSourceMailboxReadMessage(account, folder, raw)
-}
-
-func renderSourceMailboxReadMessage(account Account, folder string, raw RawMessage) string {
- msg, err := mail.ReadMessage(bytes.NewReader(raw.Body))
+ raw := messages[index].Body
+ msg, err := mail.ReadMessage(bytes.NewReader(raw))
var b strings.Builder
if err != nil {
- renderMailboxReadPane(&b, "Quelle", account.Name, folder, 0, "(unlesbar)", "", "", string(raw.Body))
+ renderMailboxReadPane(&b, "Quelle", account.Name, folder, index, "(unlesbar)", "", "", string(raw))
return b.String()
}
- renderMailboxReadPane(&b, "Quelle", account.Name, folder, 0,
+ renderMailboxReadPane(&b, "Quelle", account.Name, folder, index,
decodeHeader(msg.Header.Get("Subject")),
decodeHeader(msg.Header.Get("From")),
decodeHeader(msg.Header.Get("Date")),
- messageBody(raw.Body),
+ messageBody(raw),
)
return b.String()
}
@@ -1722,29 +1755,13 @@ func targetFolderRole(folder TargetFolder) string {
return ""
}
-func fetchTargetFolderHeaders(account Account, folder string) ([]MessageHeader, error) {
+func fetchTargetFolderMessages(account Account, folder string) ([]RawMessage, error) {
dst, err := OpenIMAPTarget(account)
if err != nil {
return nil, err
}
defer dst.Close()
- return dst.Headers(folder, mailboxListLimit, 0)
-}
-
-func fetchTargetMailboxMessage(account Account, folder string, uid uint32) (RawMessage, error) {
- dst, err := OpenIMAPTarget(account)
- if err != nil {
- return RawMessage{}, err
- }
- defer dst.Close()
- if uid == 0 {
- headers, err := dst.Headers(folder, 1, 0)
- if err != nil {
- return RawMessage{}, err
- }
- uid = firstHeaderUID(headers)
- }
- return dst.FetchOne(folder, uid)
+ return dst.Fetch(folder)
}
func renderInitialTargetMailboxList(account Account) string {
@@ -1755,12 +1772,12 @@ func renderInitialTargetMailboxList(account Account) string {
if err != nil {
return fmt.Sprintf(`Nachrichten
%s
`, html.EscapeString(err.Error()))
}
- headers, err := fetchTargetFolderHeaders(account, folder)
+ messages, err := fetchTargetFolderMessages(account, folder)
if err != nil {
return fmt.Sprintf(`Nachrichten
%s
`, html.EscapeString(err.Error()))
}
var b strings.Builder
- renderTargetMailboxMessageList(&b, account, folder, headers)
+ renderTargetMailboxMessageList(&b, account, folder, messages)
return b.String()
}
@@ -1772,11 +1789,11 @@ func renderInitialTargetMailboxRead(account Account) string {
if err != nil {
return `Nachricht waehlen.
`
}
- headers, err := fetchTargetFolderHeaders(account, folder)
+ messages, err := fetchTargetFolderMessages(account, folder)
if err != nil {
return `Nachricht waehlen.
`
}
- return renderTargetMailboxRead(account, folder, firstHeaderUID(headers))
+ return renderTargetMailboxRead(account, folder, 0, messages)
}
func firstTargetFolder(account Account) (string, error) {
@@ -1800,18 +1817,29 @@ func firstTargetFolder(account Account) (string, error) {
return folders[0].Name, nil
}
-func renderTargetMailboxMessageList(w io.Writer, account Account, folder string, messages []MessageHeader) {
+func renderTargetMailboxMessageList(w io.Writer, account Account, folder string, messages []RawMessage) {
fmt.Fprint(w, `Nachrichten
`)
- for i, header := range messages {
- subject, from, date := headerListFields(header)
+ for i, raw := range messages {
+ subject := "(ohne Betreff)"
+ from := "(ohne Absender)"
+ date := ""
+ if msg, err := mail.ReadMessage(bytes.NewReader(raw.Body)); err == nil {
+ if v := decodeHeader(msg.Header.Get("Subject")); v != "" {
+ subject = v
+ }
+ if v := decodeHeader(msg.Header.Get("From")); v != "" {
+ from = v
+ }
+ date = decodeHeader(msg.Header.Get("Date"))
+ }
active := ""
if i == 0 {
active = " active"
}
- fmt.Fprintf(w, `%s%s%s`,
+ fmt.Fprintf(w, `%s%s%s`,
active,
- html.EscapeString(account.Name), html.EscapeString(folder), header.UID,
- urlEsc(account.Name), urlEsc(folder), header.UID,
+ html.EscapeString(account.Name), html.EscapeString(folder), i,
+ urlEsc(account.Name), urlEsc(folder), i,
html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
}
if len(messages) == 0 {
@@ -1819,62 +1847,26 @@ func renderTargetMailboxMessageList(w io.Writer, account Account, folder string,
}
}
-func renderTargetMailboxRead(account Account, folder string, uid uint32) string {
- if uid == 0 {
+func renderTargetMailboxRead(account Account, folder string, index int, messages []RawMessage) string {
+ if index < 0 || index >= len(messages) {
return `Nachricht waehlen.
`
}
- raw, err := fetchTargetMailboxMessage(account, folder, uid)
- if err != nil {
- return `Nachricht waehlen.
`
- }
- return renderTargetMailboxReadMessage(account, folder, raw)
-}
-
-func renderTargetMailboxReadMessage(account Account, folder string, raw RawMessage) string {
- msg, err := mail.ReadMessage(bytes.NewReader(raw.Body))
+ raw := messages[index].Body
+ msg, err := mail.ReadMessage(bytes.NewReader(raw))
var b strings.Builder
if err != nil {
- renderMailboxReadPane(&b, "Ziel", account.Name, folder, 0, "(unlesbar)", "", "", string(raw.Body))
+ renderMailboxReadPane(&b, "Ziel", account.Name, folder, index, "(unlesbar)", "", "", string(raw))
return b.String()
}
- renderMailboxReadPane(&b, "Ziel", account.Name, folder, 0,
+ renderMailboxReadPane(&b, "Ziel", account.Name, folder, index,
decodeHeader(msg.Header.Get("Subject")),
decodeHeader(msg.Header.Get("From")),
decodeHeader(msg.Header.Get("Date")),
- messageBody(raw.Body),
+ messageBody(raw),
)
return b.String()
}
-func firstHeaderUID(headers []MessageHeader) uint32 {
- if len(headers) == 0 {
- return 0
- }
- return headers[0].UID
-}
-
-func parseUIDParam(r *http.Request) uint32 {
- uid, _ := strconv.ParseUint(strings.TrimSpace(r.URL.Query().Get("uid")), 10, 32)
- return uint32(uid)
-}
-
-func headerListFields(header MessageHeader) (subject, from, date string) {
- subject = strings.TrimSpace(header.Subject)
- if subject == "" {
- subject = "(ohne Betreff)"
- }
- from = strings.TrimSpace(header.From)
- if from == "" {
- from = "(ohne Absender)"
- }
- if !header.Date.IsZero() {
- date = header.Date.Format(time.RFC1123Z)
- } else if !header.InternalDate.IsZero() {
- date = header.InternalDate.Format(time.RFC1123Z)
- }
- return subject, from, date
-}
-
func renderMailboxReadPane(w io.Writer, title, account, folder string, index int, subject, from, date, body string) {
if subject == "" {
subject = "(ohne Betreff)"
diff --git a/backend/04-imap-source.go b/backend/04-imap-source.go
index 0a48b99..100a5d3 100644
--- a/backend/04-imap-source.go
+++ b/backend/04-imap-source.go
@@ -24,17 +24,6 @@ type RawMessage struct {
InternalDate time.Time // Original-Zeit, per APPEND erhalten
}
-type MessageHeader struct {
- UID uint32
- MessageID string
- Subject string
- From string
- Date time.Time
- InternalDate time.Time
- Flags []string
- Size int64
-}
-
// Folder ist ein Ordner der Quelle MIT den Infos, die 11-folders.go fuer die
// Rollen-Zuordnung braucht: dekodierter Name, SPECIAL-USE-Attribute und der
// Hierarchie-Trenner.
@@ -47,10 +36,7 @@ type Folder struct {
// SourceMailbox abstrahiert Quelle (IMAP primaer, POP3 Fallback), damit
// 07-migrate.go gegen ein Interface arbeitet.
type SourceMailbox interface {
- Folders() ([]Folder, error) // rekursiver Ordnerbaum
- Count(folder string) (int, error)
- Headers(folder string, limit, offset int) ([]MessageHeader, error)
- FetchOne(folder string, uid uint32) (RawMessage, error)
+ Folders() ([]Folder, error) // rekursiver Ordnerbaum
Fetch(folder string, fn func(RawMessage) error) error // BODY[] FLAGS INTERNALDATE
Close() error
}
@@ -93,18 +79,6 @@ func (s *imapSource) Fetch(folder string, fn func(RawMessage) error) error {
return s.mailbox.fetchEach(folder, fn)
}
-func (s *imapSource) Count(folder string) (int, error) {
- return s.mailbox.count(folder)
-}
-
-func (s *imapSource) Headers(folder string, limit, offset int) ([]MessageHeader, error) {
- return s.mailbox.fetchHeaders(folder, limit, offset)
-}
-
-func (s *imapSource) FetchOne(folder string, uid uint32) (RawMessage, error) {
- return s.mailbox.fetchOne(folder, uid)
-}
-
func (s *imapSource) Close() error {
return s.mailbox.Close()
}
@@ -180,7 +154,6 @@ func (m *imapClientMailbox) fetchEach(folder string, fn func(RawMessage) error)
InternalDate: true,
BodySection: []*imap.FetchItemBodySection{section},
})
- defer cmd.Close()
for {
data := cmd.Next()
@@ -189,111 +162,38 @@ func (m *imapClientMailbox) fetchEach(folder string, fn func(RawMessage) error)
}
buf, err := data.Collect()
if err != nil {
+ _ = cmd.Close()
return err
}
- if err := fn(rawMessageFromFetch(buf, section)); err != nil {
+ body := buf.FindBodySection(section)
+ if body == nil {
+ body = []byte{}
+ }
+ if err := fn(RawMessage{
+ MessageID: messageIDFromFetch(buf, body),
+ Body: body,
+ Flags: flagsToStrings(sanitizeIMAPFlags(buf.Flags)),
+ InternalDate: buf.InternalDate,
+ }); err != nil {
+ _ = cmd.Close()
return err
}
}
return cmd.Close()
}
-func (m *imapClientMailbox) count(folder string) (int, error) {
- selected, err := m.c.Select(folder, &imap.SelectOptions{ReadOnly: true}).Wait()
- if err != nil {
- return 0, err
- }
- return int(selected.NumMessages), nil
-}
-
-func (m *imapClientMailbox) fetchHeaders(folder string, limit, offset int) ([]MessageHeader, error) {
- selected, err := m.c.Select(folder, &imap.SelectOptions{ReadOnly: true}).Wait()
- if err != nil {
- return nil, err
- }
- if selected.NumMessages == 0 {
- return nil, nil
- }
- if limit <= 0 || limit > 200 {
- limit = 200
- }
- if offset < 0 {
- offset = 0
- }
- stop := int(selected.NumMessages) - offset
- if stop <= 0 {
- return nil, nil
- }
- start := stop - limit + 1
- if start < 1 {
- start = 1
- }
-
- seqSet := imap.SeqSet{}
- seqSet.AddRange(uint32(start), uint32(stop))
- cmd := m.c.Fetch(seqSet, &imap.FetchOptions{
- Envelope: true,
- Flags: true,
- InternalDate: true,
- RFC822Size: true,
- UID: true,
+func (m *imapClientMailbox) fetchAll(folder string) ([]RawMessage, error) {
+ var out []RawMessage
+ err := m.fetchEach(folder, func(m RawMessage) error {
+ out = append(out, m)
+ return nil
})
- defer cmd.Close()
-
- var out []MessageHeader
- for {
- data := cmd.Next()
- if data == nil {
- break
- }
- buf, err := data.Collect()
- if err != nil {
- return out, err
- }
- out = append(out, messageHeaderFromFetch(buf))
- }
- if err := cmd.Close(); err != nil {
+ if err != nil {
return out, err
}
- reverseHeaders(out)
return out, nil
}
-func (m *imapClientMailbox) fetchOne(folder string, uid uint32) (RawMessage, error) {
- if uid == 0 {
- return RawMessage{}, fmt.Errorf("Nachricht ohne UID")
- }
- if _, err := m.c.Select(folder, &imap.SelectOptions{ReadOnly: true}).Wait(); err != nil {
- return RawMessage{}, err
- }
-
- section := &imap.FetchItemBodySection{Peek: true}
- cmd := m.c.Fetch(imap.UIDSetNum(imap.UID(uid)), &imap.FetchOptions{
- Envelope: true,
- Flags: true,
- InternalDate: true,
- UID: true,
- BodySection: []*imap.FetchItemBodySection{section},
- })
- defer cmd.Close()
- data := cmd.Next()
- if data == nil {
- if err := cmd.Close(); err != nil {
- return RawMessage{}, err
- }
- return RawMessage{}, fmt.Errorf("Nachricht nicht gefunden")
- }
- buf, err := data.Collect()
- if err != nil {
- return RawMessage{}, err
- }
- msg := rawMessageFromFetch(buf, section)
- if err := cmd.Close(); err != nil {
- return RawMessage{}, err
- }
- return msg, nil
-}
-
func (m *imapClientMailbox) Close() error {
if m == nil || m.c == nil {
return nil
@@ -362,68 +262,9 @@ func stringsToFlags(flags []string) []imap.Flag {
}())
}
-func rawMessageFromFetch(buf *imapclient.FetchMessageBuffer, section *imap.FetchItemBodySection) RawMessage {
- body := buf.FindBodySection(section)
- if body == nil {
- body = []byte{}
- }
- return RawMessage{
- MessageID: messageIDFromFetch(buf, body),
- Body: body,
- Flags: flagsToStrings(sanitizeIMAPFlags(buf.Flags)),
- InternalDate: buf.InternalDate,
- }
-}
-
-func messageHeaderFromFetch(buf *imapclient.FetchMessageBuffer) MessageHeader {
- h := MessageHeader{
- UID: uint32(buf.UID),
- MessageID: messageIDFromFetch(buf, nil),
- InternalDate: buf.InternalDate,
- Flags: flagsToStrings(sanitizeIMAPFlags(buf.Flags)),
- Size: buf.RFC822Size,
- }
- if buf.Envelope != nil {
- h.Subject = strings.TrimSpace(buf.Envelope.Subject)
- h.From = envelopeFrom(buf.Envelope)
- h.Date = buf.Envelope.Date
- }
- if h.Date.IsZero() {
- h.Date = h.InternalDate
- }
- return h
-}
-
-func envelopeFrom(env *imap.Envelope) string {
- if env == nil || len(env.From) == 0 {
- return ""
- }
- for _, addr := range env.From {
- email := strings.TrimSpace(addr.Addr())
- if email == "" {
- continue
- }
- name := strings.TrimSpace(addr.Name)
- if name != "" {
- return (&mail.Address{Name: name, Address: email}).String()
- }
- return email
- }
- return ""
-}
-
-func reverseHeaders(headers []MessageHeader) {
- for i, j := 0, len(headers)-1; i < j; i, j = i+1, j-1 {
- headers[i], headers[j] = headers[j], headers[i]
- }
-}
-
func messageIDFromFetch(buf *imapclient.FetchMessageBuffer, body []byte) string {
if buf != nil && buf.Envelope != nil && strings.TrimSpace(buf.Envelope.MessageID) != "" {
- return normalizeMessageID(buf.Envelope.MessageID)
- }
- if len(body) == 0 {
- return ""
+ return strings.TrimSpace(buf.Envelope.MessageID)
}
return messageID(body)
}
@@ -432,16 +273,12 @@ func messageID(body []byte) string {
msg, err := mail.ReadMessage(strings.NewReader(string(body)))
if err == nil {
if id := strings.TrimSpace(msg.Header.Get("Message-ID")); id != "" {
- return normalizeMessageID(id)
+ return id
}
}
return fmt.Sprintf("sha256:%x", sha256.Sum256(body))
}
-func normalizeMessageID(id string) string {
- return strings.Trim(strings.TrimSpace(id), "<>")
-}
-
func isAlreadyExistsError(err error) bool {
var imapErr *imap.Error
if errors.As(err, &imapErr) {
diff --git a/backend/05-imap-target.go b/backend/05-imap-target.go
index f62aa95..1c04173 100644
--- a/backend/05-imap-target.go
+++ b/backend/05-imap-target.go
@@ -12,12 +12,11 @@ import (
// Original-Datum; SMTP wuerde alles als "neu/ungelesen" mit falschem Datum
// zustellen und die Ordnerstruktur zerstoeren.
type TargetMailbox interface {
- Folders() ([]TargetFolder, error) // vorhandene Ziel-Ordner (fuer Rollen-Zuordnung)
- Delim() string // Hierarchie-Trenner des Ziels
- EnsureFolder(name string) error // anlegen + subscriben, falls fehlt
- Append(folder string, m RawMessage) error // mit m.Flags und m.InternalDate
- Headers(folder string, limit, offset int) ([]MessageHeader, error)
- FetchOne(folder string, uid uint32) (RawMessage, error)
+ Folders() ([]TargetFolder, error) // vorhandene Ziel-Ordner (fuer Rollen-Zuordnung)
+ Delim() string // Hierarchie-Trenner des Ziels
+ EnsureFolder(name string) error // anlegen + subscriben, falls fehlt
+ Append(folder string, m RawMessage) error // mit m.Flags und m.InternalDate
+ Fetch(folder string) ([]RawMessage, error) // Ziel-Audit: geschriebene Mail wiederfinden
Close() error
}
@@ -121,12 +120,8 @@ func (t *imapTarget) Append(folder string, m RawMessage) error {
return err
}
-func (t *imapTarget) Headers(folder string, limit, offset int) ([]MessageHeader, error) {
- return t.mailbox.fetchHeaders(folder, limit, offset)
-}
-
-func (t *imapTarget) FetchOne(folder string, uid uint32) (RawMessage, error) {
- return t.mailbox.fetchOne(folder, uid)
+func (t *imapTarget) Fetch(folder string) ([]RawMessage, error) {
+ return t.mailbox.fetchAll(folder)
}
func (t *imapTarget) Close() error {
diff --git a/backend/07-migrate.go b/backend/07-migrate.go
index 4bc7e01..8190ac2 100644
--- a/backend/07-migrate.go
+++ b/backend/07-migrate.go
@@ -210,9 +210,12 @@ func CheckAccount(name string) error {
}
total := 0
for _, folder := range folders {
- folderTotal, err := src.Count(folder.Name)
- if err != nil {
- return fmt.Errorf("count source %s %s: %w", a.Name, folder.Name, err)
+ folderTotal := 0
+ if err := src.Fetch(folder.Name, func(m RawMessage) error {
+ folderTotal++
+ return nil
+ }); err != nil {
+ return fmt.Errorf("fetch source %s %s: %w", a.Name, folder.Name, err)
}
log.Printf("account %s source folder %s messages=%d", a.Name, folder.Name, folderTotal)
total += folderTotal
diff --git a/backend/08-viewer.go b/backend/08-viewer.go
index aa3dfad..bbda7d1 100644
--- a/backend/08-viewer.go
+++ b/backend/08-viewer.go
@@ -490,33 +490,23 @@ func findTargetIMAPCopy(account, targetAccount string, raw []byte) (forwardedMat
}
func scanTargetForMessage(dst TargetMailbox, targetUser, messageID string) (forwardedMatch, bool, error) {
- messageID = normalizeMessageID(messageID)
folders, err := dst.Folders()
if err != nil {
return forwardedMatch{}, false, err
}
for _, folder := range folders {
- for offset := 0; ; offset += mailboxListLimit {
- headers, err := dst.Headers(folder.Name, mailboxListLimit, offset)
- if err != nil {
- break
- }
- if len(headers) == 0 {
- break
- }
- for i, header := range headers {
- if normalizeMessageID(header.MessageID) == messageID {
- msg, err := dst.FetchOne(folder.Name, header.UID)
- if err != nil {
- continue
- }
- return forwardedMatch{
- Account: "Ziel: " + targetUser,
- Folder: folder.Name,
- Index: offset + i,
- Raw: msg.Body,
- }, true, nil
- }
+ msgs, err := dst.Fetch(folder.Name)
+ if err != nil {
+ continue
+ }
+ for i, msg := range msgs {
+ if msg.MessageID == messageID || messageIDHeader(msg.Body) == messageID {
+ return forwardedMatch{
+ Account: "Ziel: " + targetUser,
+ Folder: folder.Name,
+ Index: i,
+ Raw: msg.Body,
+ }, true, nil
}
}
}
@@ -570,7 +560,7 @@ func messageIDHeader(raw []byte) string {
if err != nil {
return ""
}
- return normalizeMessageID(msg.Header.Get("Message-ID"))
+ return strings.TrimSpace(msg.Header.Get("Message-ID"))
}
func renderTargetAccountSelect(account, folder, selected string, index int) string {
diff --git a/ui-fetch-brief.md b/ui-fetch-brief.md
deleted file mode 100644
index f1c8786..0000000
--- a/ui-fetch-brief.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# 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.