Compare commits
No commits in common. "03ce0ad8eaba75280a5be55ec21ee4814c939a7c" and "a52963c3809080dead95f4eff70e3a12eaec9dad" have entirely different histories.
03ce0ad8ea
...
a52963c380
6 changed files with 189 additions and 471 deletions
|
|
@ -14,11 +14,8 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const mailboxListLimit = 200
|
|
||||||
|
|
||||||
func RegisterRoutes(mux *http.ServeMux) {
|
func RegisterRoutes(mux *http.ServeMux) {
|
||||||
mux.HandleFunc("/", homeHandler)
|
mux.HandleFunc("/", homeHandler)
|
||||||
mux.HandleFunc("/login", loginHandler)
|
mux.HandleFunc("/login", loginHandler)
|
||||||
|
|
@ -397,13 +394,13 @@ func sourceMailboxMessagesHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
headers, err := fetchSourceFolderHeaders(account, folder)
|
messages, err := fetchSourceFolderMessages(account, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderSourceMailboxMessageList(w, account, folder, headers)
|
renderSourceMailboxMessageList(w, account, folder, messages)
|
||||||
fmt.Fprintf(w, `<div id="read" hx-swap-oob="innerHTML">%s</div>`, renderSourceMailboxRead(account, folder, firstHeaderUID(headers)))
|
fmt.Fprintf(w, `<div id="read" hx-swap-oob="innerHTML">%s</div>`, renderSourceMailboxRead(account, folder, 0, messages))
|
||||||
}
|
}
|
||||||
|
|
||||||
func sourceMailboxMessageHandler(w http.ResponseWriter, r *http.Request) {
|
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"))
|
accountName := strings.TrimSpace(r.URL.Query().Get("source"))
|
||||||
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
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)
|
account, err := GetAccount(accountName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
raw, err := fetchSourceMailboxMessage(account, folder, uid)
|
messages, err := fetchSourceFolderMessages(account, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
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) {
|
func targetMailboxMessagesHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -443,13 +444,13 @@ func targetMailboxMessagesHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
headers, err := fetchTargetFolderHeaders(account, folder)
|
messages, err := fetchTargetFolderMessages(account, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
fmt.Fprintf(w, `<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
renderTargetMailboxMessageList(w, account, folder, headers)
|
renderTargetMailboxMessageList(w, account, folder, messages)
|
||||||
fmt.Fprintf(w, `<div id="read" hx-swap-oob="innerHTML">%s</div>`, renderTargetMailboxRead(account, folder, firstHeaderUID(headers)))
|
fmt.Fprintf(w, `<div id="read" hx-swap-oob="innerHTML">%s</div>`, renderTargetMailboxRead(account, folder, 0, messages))
|
||||||
}
|
}
|
||||||
|
|
||||||
func targetMailboxMessageHandler(w http.ResponseWriter, r *http.Request) {
|
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"))
|
accountName := strings.TrimSpace(r.URL.Query().Get("target"))
|
||||||
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
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)
|
account, err := GetAccount(accountName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
raw, err := fetchTargetMailboxMessage(account, folder, uid)
|
messages, err := fetchTargetFolderMessages(account, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
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) {
|
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"))
|
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
fmt.Fprint(w, renderTransferMessageList(side, kind, value, folder))
|
fmt.Fprint(w, renderTransferMessageList(side, kind, value, folder))
|
||||||
fmt.Fprintf(w, `<div id="transfer-preview" hx-swap-oob="innerHTML">%s</div>`, renderTransferPreview(kind, value, folder, 0, 0))
|
fmt.Fprintf(w, `<div id="transfer-preview" hx-swap-oob="innerHTML">%s</div>`, renderTransferPreview(kind, value, folder, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
func transferMessageHandler(w http.ResponseWriter, r *http.Request) {
|
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"))
|
kind := strings.TrimSpace(r.URL.Query().Get("kind"))
|
||||||
value := strings.TrimSpace(r.URL.Query().Get("value"))
|
value := strings.TrimSpace(r.URL.Query().Get("value"))
|
||||||
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
folder := strings.TrimSpace(r.URL.Query().Get("folder"))
|
||||||
index, _ := strconv.Atoi(r.URL.Query().Get("index"))
|
index, err := strconv.Atoi(r.URL.Query().Get("index"))
|
||||||
uid := parseUIDParam(r)
|
if err != nil {
|
||||||
|
http.Error(w, "bad index", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
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 {
|
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 {
|
func renderInitialTransferPreview(leftKind, leftValue, rightKind, rightValue string) string {
|
||||||
if leftValue != "" {
|
if leftValue != "" {
|
||||||
if folder, err := firstTransferFolder(leftKind, leftValue); err == nil {
|
if folder, err := firstTransferFolder(leftKind, leftValue); err == nil {
|
||||||
return renderTransferPreview(leftKind, leftValue, folder, 0, 0)
|
return renderTransferPreview(leftKind, leftValue, folder, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if rightValue != "" {
|
if rightValue != "" {
|
||||||
if folder, err := firstTransferFolder(rightKind, rightValue); err == nil {
|
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.")
|
return renderEmptyPreviewPane("Nachricht links oder rechts waehlen.")
|
||||||
|
|
@ -1263,45 +1271,56 @@ func renderTransferMessageList(side, kind, value, folder string) string {
|
||||||
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
messages, err := fetchTargetFolderHeaders(account, folder)
|
messages, err := fetchTargetFolderMessages(account, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
renderTransferHeaders(&b, side, kind, value, folder, messages)
|
renderTransferRawMessages(&b, side, kind, value, folder, messages)
|
||||||
default:
|
default:
|
||||||
account, err := GetAccount(value)
|
account, err := GetAccount(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
messages, err := fetchSourceFolderHeaders(account, folder)
|
messages, err := fetchSourceFolderMessages(account, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
fmt.Fprintf(&b, `<div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
renderTransferHeaders(&b, side, kind, value, folder, messages)
|
renderTransferRawMessages(&b, side, kind, value, folder, messages)
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderTransferHeaders(b *strings.Builder, side, kind, value, folder string, messages []MessageHeader) {
|
func renderTransferRawMessages(b *strings.Builder, side, kind, value, folder string, messages []RawMessage) {
|
||||||
for i, header := range messages {
|
for i, raw := range messages {
|
||||||
subject, from, date := headerListFields(header)
|
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 := ""
|
active := ""
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
active = " active"
|
active = " active"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(b, `<a class="msg-row%s" href="#" hx-get="/transfer/message?side=%s&kind=%s&value=%s&folder=%s&uid=%d" hx-target="#transfer-preview" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
fmt.Fprintf(b, `<a class="msg-row%s" href="#" hx-get="/transfer/message?side=%s&kind=%s&value=%s&folder=%s&index=%d" hx-target="#transfer-preview" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
||||||
active, side, html.EscapeString(kind), urlEsc(value), urlEsc(folder), header.UID, html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
|
active, side, html.EscapeString(kind), urlEsc(value), urlEsc(folder), i, html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
|
||||||
}
|
}
|
||||||
if len(messages) == 0 {
|
if len(messages) == 0 {
|
||||||
b.WriteString(`<div class="ef-empty">Keine Nachrichten.</div>`)
|
b.WriteString(`<div class="ef-empty">Keine Nachrichten.</div>`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderTransferPreview(kind, value, folder string, index int, uid uint32) string {
|
func renderTransferPreview(kind, value, folder string, index int) string {
|
||||||
raw, label, err := transferRawMessage(kind, value, folder, index, uid)
|
raw, label, err := transferRawMessage(kind, value, folder, index)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return renderEmptyPreviewPane(err.Error())
|
return renderEmptyPreviewPane(err.Error())
|
||||||
}
|
}
|
||||||
|
|
@ -1320,7 +1339,7 @@ func renderTransferPreview(kind, value, folder string, index int, uid uint32) st
|
||||||
return b.String()
|
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 {
|
switch kind {
|
||||||
case "archive":
|
case "archive":
|
||||||
path, err := mboxPath(value, folder)
|
path, err := mboxPath(value, folder)
|
||||||
|
|
@ -1334,15 +1353,27 @@ func transferRawMessage(kind, value, folder string, index int, uid uint32) ([]by
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, value, err
|
return nil, value, err
|
||||||
}
|
}
|
||||||
msg, err := fetchTargetMailboxMessage(account, folder, uid)
|
messages, err := fetchTargetFolderMessages(account, folder)
|
||||||
return msg.Body, account.DstUser, err
|
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:
|
default:
|
||||||
account, err := GetAccount(value)
|
account, err := GetAccount(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, value, err
|
return nil, value, err
|
||||||
}
|
}
|
||||||
msg, err := fetchSourceMailboxMessage(account, folder, uid)
|
messages, err := fetchSourceFolderMessages(account, folder)
|
||||||
return msg.Body, account.SrcUser, err
|
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchSourceFolderHeaders(account Account, folder string) ([]MessageHeader, error) {
|
func fetchSourceFolderMessages(account Account, folder string) ([]RawMessage, error) {
|
||||||
if accountProto(account) == "pop3" {
|
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)
|
src, err := OpenIMAPSource(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer src.Close()
|
defer src.Close()
|
||||||
return src.Headers(folder, mailboxListLimit, 0)
|
return collectSourceMessages(src, folder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchSourceMailboxMessage(account Account, folder string, uid uint32) (RawMessage, error) {
|
func collectSourceMessages(src SourceMailbox, folder string) ([]RawMessage, error) {
|
||||||
if accountProto(account) == "pop3" {
|
var messages []RawMessage
|
||||||
return RawMessage{}, fmt.Errorf("POP3-Nachrichtenansicht ist noch nicht unterstuetzt.")
|
err := src.Fetch(folder, func(m RawMessage) error {
|
||||||
}
|
messages = append(messages, m)
|
||||||
src, err := OpenIMAPSource(account)
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return RawMessage{}, err
|
return messages, err
|
||||||
}
|
}
|
||||||
defer src.Close()
|
return messages, nil
|
||||||
if uid == 0 {
|
|
||||||
headers, err := src.Headers(folder, 1, 0)
|
|
||||||
if err != nil {
|
|
||||||
return RawMessage{}, err
|
|
||||||
}
|
|
||||||
uid = firstHeaderUID(headers)
|
|
||||||
}
|
|
||||||
return src.FetchOne(folder, uid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderInitialSourceMailboxList(account Account) string {
|
func renderInitialSourceMailboxList(account Account) string {
|
||||||
|
|
@ -1553,12 +1582,12 @@ func renderInitialSourceMailboxList(account Account) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
}
|
}
|
||||||
headers, err := fetchSourceFolderHeaders(account, folder)
|
messages, err := fetchSourceFolderMessages(account, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
renderSourceMailboxMessageList(&b, account, folder, headers)
|
renderSourceMailboxMessageList(&b, account, folder, messages)
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1570,11 +1599,11 @@ func renderInitialSourceMailboxRead(account Account) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
||||||
}
|
}
|
||||||
headers, err := fetchSourceFolderHeaders(account, folder)
|
messages, err := fetchSourceFolderMessages(account, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
||||||
}
|
}
|
||||||
return renderSourceMailboxRead(account, folder, firstHeaderUID(headers))
|
return renderSourceMailboxRead(account, folder, 0, messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstSourceFolder(account Account) (string, error) {
|
func firstSourceFolder(account Account) (string, error) {
|
||||||
|
|
@ -1601,18 +1630,29 @@ func firstSourceFolder(account Account) (string, error) {
|
||||||
return folders[0].Name, nil
|
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, `<div class="pane-head">Nachrichten</div>`)
|
fmt.Fprint(w, `<div class="pane-head">Nachrichten</div>`)
|
||||||
for i, header := range messages {
|
for i, raw := range messages {
|
||||||
subject, from, date := headerListFields(header)
|
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 := ""
|
active := ""
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
active = " active"
|
active = " active"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, `<a class="msg-row%s" href="#" draggable="true" data-source="%s" data-folder="%s" data-uid="%d" hx-get="/boxes/source/message?source=%s&folder=%s&uid=%d" hx-target="#read" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
fmt.Fprintf(w, `<a class="msg-row%s" href="#" draggable="true" data-source="%s" data-folder="%s" data-index="%d" hx-get="/boxes/source/message?source=%s&folder=%s&index=%d" hx-target="#read" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
||||||
active,
|
active,
|
||||||
html.EscapeString(account.Name), html.EscapeString(folder), header.UID,
|
html.EscapeString(account.Name), html.EscapeString(folder), i,
|
||||||
urlEsc(account.Name), urlEsc(folder), header.UID,
|
urlEsc(account.Name), urlEsc(folder), i,
|
||||||
html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
|
html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
|
||||||
}
|
}
|
||||||
if len(messages) == 0 {
|
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 {
|
func renderSourceMailboxRead(account Account, folder string, index int, messages []RawMessage) string {
|
||||||
if uid == 0 {
|
if index < 0 || index >= len(messages) {
|
||||||
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
||||||
}
|
}
|
||||||
raw, err := fetchSourceMailboxMessage(account, folder, uid)
|
raw := messages[index].Body
|
||||||
if err != nil {
|
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
||||||
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
|
||||||
}
|
|
||||||
return renderSourceMailboxReadMessage(account, folder, raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderSourceMailboxReadMessage(account Account, folder string, raw RawMessage) string {
|
|
||||||
msg, err := mail.ReadMessage(bytes.NewReader(raw.Body))
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
if err != nil {
|
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()
|
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("Subject")),
|
||||||
decodeHeader(msg.Header.Get("From")),
|
decodeHeader(msg.Header.Get("From")),
|
||||||
decodeHeader(msg.Header.Get("Date")),
|
decodeHeader(msg.Header.Get("Date")),
|
||||||
messageBody(raw.Body),
|
messageBody(raw),
|
||||||
)
|
)
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
@ -1722,29 +1755,13 @@ func targetFolderRole(folder TargetFolder) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchTargetFolderHeaders(account Account, folder string) ([]MessageHeader, error) {
|
func fetchTargetFolderMessages(account Account, folder string) ([]RawMessage, error) {
|
||||||
dst, err := OpenIMAPTarget(account)
|
dst, err := OpenIMAPTarget(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer dst.Close()
|
defer dst.Close()
|
||||||
return dst.Headers(folder, mailboxListLimit, 0)
|
return dst.Fetch(folder)
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderInitialTargetMailboxList(account Account) string {
|
func renderInitialTargetMailboxList(account Account) string {
|
||||||
|
|
@ -1755,12 +1772,12 @@ func renderInitialTargetMailboxList(account Account) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
}
|
}
|
||||||
headers, err := fetchTargetFolderHeaders(account, folder)
|
messages, err := fetchTargetFolderMessages(account, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
return fmt.Sprintf(`<div class="pane-head">Nachrichten</div><div class="ef-empty">%s</div>`, html.EscapeString(err.Error()))
|
||||||
}
|
}
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
renderTargetMailboxMessageList(&b, account, folder, headers)
|
renderTargetMailboxMessageList(&b, account, folder, messages)
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1772,11 +1789,11 @@ func renderInitialTargetMailboxRead(account Account) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
||||||
}
|
}
|
||||||
headers, err := fetchTargetFolderHeaders(account, folder)
|
messages, err := fetchTargetFolderMessages(account, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
||||||
}
|
}
|
||||||
return renderTargetMailboxRead(account, folder, firstHeaderUID(headers))
|
return renderTargetMailboxRead(account, folder, 0, messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstTargetFolder(account Account) (string, error) {
|
func firstTargetFolder(account Account) (string, error) {
|
||||||
|
|
@ -1800,18 +1817,29 @@ func firstTargetFolder(account Account) (string, error) {
|
||||||
return folders[0].Name, nil
|
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, `<div class="pane-head">Nachrichten</div>`)
|
fmt.Fprint(w, `<div class="pane-head">Nachrichten</div>`)
|
||||||
for i, header := range messages {
|
for i, raw := range messages {
|
||||||
subject, from, date := headerListFields(header)
|
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 := ""
|
active := ""
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
active = " active"
|
active = " active"
|
||||||
}
|
}
|
||||||
fmt.Fprintf(w, `<a class="msg-row%s" href="#" draggable="true" data-target="%s" data-folder="%s" data-uid="%d" hx-get="/boxes/target/message?target=%s&folder=%s&uid=%d" hx-target="#read" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
fmt.Fprintf(w, `<a class="msg-row%s" href="#" draggable="true" data-target="%s" data-folder="%s" data-index="%d" hx-get="/boxes/target/message?target=%s&folder=%s&index=%d" hx-target="#read" hx-swap="innerHTML"><span class="msg-from">%s</span><span class="msg-subject">%s</span><span class="msg-date">%s</span></a>`,
|
||||||
active,
|
active,
|
||||||
html.EscapeString(account.Name), html.EscapeString(folder), header.UID,
|
html.EscapeString(account.Name), html.EscapeString(folder), i,
|
||||||
urlEsc(account.Name), urlEsc(folder), header.UID,
|
urlEsc(account.Name), urlEsc(folder), i,
|
||||||
html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
|
html.EscapeString(from), html.EscapeString(subject), html.EscapeString(date))
|
||||||
}
|
}
|
||||||
if len(messages) == 0 {
|
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 {
|
func renderTargetMailboxRead(account Account, folder string, index int, messages []RawMessage) string {
|
||||||
if uid == 0 {
|
if index < 0 || index >= len(messages) {
|
||||||
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
||||||
}
|
}
|
||||||
raw, err := fetchTargetMailboxMessage(account, folder, uid)
|
raw := messages[index].Body
|
||||||
if err != nil {
|
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
||||||
return `<div class="ef-empty">Nachricht waehlen.</div>`
|
|
||||||
}
|
|
||||||
return renderTargetMailboxReadMessage(account, folder, raw)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderTargetMailboxReadMessage(account Account, folder string, raw RawMessage) string {
|
|
||||||
msg, err := mail.ReadMessage(bytes.NewReader(raw.Body))
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
if err != nil {
|
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()
|
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("Subject")),
|
||||||
decodeHeader(msg.Header.Get("From")),
|
decodeHeader(msg.Header.Get("From")),
|
||||||
decodeHeader(msg.Header.Get("Date")),
|
decodeHeader(msg.Header.Get("Date")),
|
||||||
messageBody(raw.Body),
|
messageBody(raw),
|
||||||
)
|
)
|
||||||
return b.String()
|
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) {
|
func renderMailboxReadPane(w io.Writer, title, account, folder string, index int, subject, from, date, body string) {
|
||||||
if subject == "" {
|
if subject == "" {
|
||||||
subject = "(ohne Betreff)"
|
subject = "(ohne Betreff)"
|
||||||
|
|
|
||||||
|
|
@ -24,17 +24,6 @@ type RawMessage struct {
|
||||||
InternalDate time.Time // Original-Zeit, per APPEND erhalten
|
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
|
// 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
|
// Rollen-Zuordnung braucht: dekodierter Name, SPECIAL-USE-Attribute und der
|
||||||
// Hierarchie-Trenner.
|
// Hierarchie-Trenner.
|
||||||
|
|
@ -48,9 +37,6 @@ type Folder struct {
|
||||||
// 07-migrate.go gegen ein Interface arbeitet.
|
// 07-migrate.go gegen ein Interface arbeitet.
|
||||||
type SourceMailbox interface {
|
type SourceMailbox interface {
|
||||||
Folders() ([]Folder, error) // rekursiver Ordnerbaum
|
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)
|
|
||||||
Fetch(folder string, fn func(RawMessage) error) error // BODY[] FLAGS INTERNALDATE
|
Fetch(folder string, fn func(RawMessage) error) error // BODY[] FLAGS INTERNALDATE
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
@ -93,18 +79,6 @@ func (s *imapSource) Fetch(folder string, fn func(RawMessage) error) error {
|
||||||
return s.mailbox.fetchEach(folder, fn)
|
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 {
|
func (s *imapSource) Close() error {
|
||||||
return s.mailbox.Close()
|
return s.mailbox.Close()
|
||||||
}
|
}
|
||||||
|
|
@ -180,7 +154,6 @@ func (m *imapClientMailbox) fetchEach(folder string, fn func(RawMessage) error)
|
||||||
InternalDate: true,
|
InternalDate: true,
|
||||||
BodySection: []*imap.FetchItemBodySection{section},
|
BodySection: []*imap.FetchItemBodySection{section},
|
||||||
})
|
})
|
||||||
defer cmd.Close()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
data := cmd.Next()
|
data := cmd.Next()
|
||||||
|
|
@ -189,111 +162,38 @@ func (m *imapClientMailbox) fetchEach(folder string, fn func(RawMessage) error)
|
||||||
}
|
}
|
||||||
buf, err := data.Collect()
|
buf, err := data.Collect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = cmd.Close()
|
||||||
return err
|
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 err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return cmd.Close()
|
return cmd.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *imapClientMailbox) count(folder string) (int, error) {
|
func (m *imapClientMailbox) fetchAll(folder string) ([]RawMessage, error) {
|
||||||
selected, err := m.c.Select(folder, &imap.SelectOptions{ReadOnly: true}).Wait()
|
var out []RawMessage
|
||||||
if err != nil {
|
err := m.fetchEach(folder, func(m RawMessage) error {
|
||||||
return 0, err
|
out = append(out, m)
|
||||||
}
|
return nil
|
||||||
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,
|
|
||||||
})
|
})
|
||||||
defer cmd.Close()
|
|
||||||
|
|
||||||
var out []MessageHeader
|
|
||||||
for {
|
|
||||||
data := cmd.Next()
|
|
||||||
if data == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buf, err := data.Collect()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return out, err
|
return out, err
|
||||||
}
|
}
|
||||||
out = append(out, messageHeaderFromFetch(buf))
|
|
||||||
}
|
|
||||||
if err := cmd.Close(); err != nil {
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
reverseHeaders(out)
|
|
||||||
return out, nil
|
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 {
|
func (m *imapClientMailbox) Close() error {
|
||||||
if m == nil || m.c == nil {
|
if m == nil || m.c == nil {
|
||||||
return 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 {
|
func messageIDFromFetch(buf *imapclient.FetchMessageBuffer, body []byte) string {
|
||||||
if buf != nil && buf.Envelope != nil && strings.TrimSpace(buf.Envelope.MessageID) != "" {
|
if buf != nil && buf.Envelope != nil && strings.TrimSpace(buf.Envelope.MessageID) != "" {
|
||||||
return normalizeMessageID(buf.Envelope.MessageID)
|
return strings.TrimSpace(buf.Envelope.MessageID)
|
||||||
}
|
|
||||||
if len(body) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
return messageID(body)
|
return messageID(body)
|
||||||
}
|
}
|
||||||
|
|
@ -432,16 +273,12 @@ func messageID(body []byte) string {
|
||||||
msg, err := mail.ReadMessage(strings.NewReader(string(body)))
|
msg, err := mail.ReadMessage(strings.NewReader(string(body)))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if id := strings.TrimSpace(msg.Header.Get("Message-ID")); id != "" {
|
if id := strings.TrimSpace(msg.Header.Get("Message-ID")); id != "" {
|
||||||
return normalizeMessageID(id)
|
return id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("sha256:%x", sha256.Sum256(body))
|
return fmt.Sprintf("sha256:%x", sha256.Sum256(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeMessageID(id string) string {
|
|
||||||
return strings.Trim(strings.TrimSpace(id), "<>")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAlreadyExistsError(err error) bool {
|
func isAlreadyExistsError(err error) bool {
|
||||||
var imapErr *imap.Error
|
var imapErr *imap.Error
|
||||||
if errors.As(err, &imapErr) {
|
if errors.As(err, &imapErr) {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,7 @@ type TargetMailbox interface {
|
||||||
Delim() string // Hierarchie-Trenner des Ziels
|
Delim() string // Hierarchie-Trenner des Ziels
|
||||||
EnsureFolder(name string) error // anlegen + subscriben, falls fehlt
|
EnsureFolder(name string) error // anlegen + subscriben, falls fehlt
|
||||||
Append(folder string, m RawMessage) error // mit m.Flags und m.InternalDate
|
Append(folder string, m RawMessage) error // mit m.Flags und m.InternalDate
|
||||||
Headers(folder string, limit, offset int) ([]MessageHeader, error)
|
Fetch(folder string) ([]RawMessage, error) // Ziel-Audit: geschriebene Mail wiederfinden
|
||||||
FetchOne(folder string, uid uint32) (RawMessage, error)
|
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,12 +120,8 @@ func (t *imapTarget) Append(folder string, m RawMessage) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *imapTarget) Headers(folder string, limit, offset int) ([]MessageHeader, error) {
|
func (t *imapTarget) Fetch(folder string) ([]RawMessage, error) {
|
||||||
return t.mailbox.fetchHeaders(folder, limit, offset)
|
return t.mailbox.fetchAll(folder)
|
||||||
}
|
|
||||||
|
|
||||||
func (t *imapTarget) FetchOne(folder string, uid uint32) (RawMessage, error) {
|
|
||||||
return t.mailbox.fetchOne(folder, uid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *imapTarget) Close() error {
|
func (t *imapTarget) Close() error {
|
||||||
|
|
|
||||||
|
|
@ -210,9 +210,12 @@ func CheckAccount(name string) error {
|
||||||
}
|
}
|
||||||
total := 0
|
total := 0
|
||||||
for _, folder := range folders {
|
for _, folder := range folders {
|
||||||
folderTotal, err := src.Count(folder.Name)
|
folderTotal := 0
|
||||||
if err != nil {
|
if err := src.Fetch(folder.Name, func(m RawMessage) error {
|
||||||
return fmt.Errorf("count source %s %s: %w", a.Name, folder.Name, err)
|
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)
|
log.Printf("account %s source folder %s messages=%d", a.Name, folder.Name, folderTotal)
|
||||||
total += folderTotal
|
total += folderTotal
|
||||||
|
|
|
||||||
|
|
@ -490,36 +490,26 @@ func findTargetIMAPCopy(account, targetAccount string, raw []byte) (forwardedMat
|
||||||
}
|
}
|
||||||
|
|
||||||
func scanTargetForMessage(dst TargetMailbox, targetUser, messageID string) (forwardedMatch, bool, error) {
|
func scanTargetForMessage(dst TargetMailbox, targetUser, messageID string) (forwardedMatch, bool, error) {
|
||||||
messageID = normalizeMessageID(messageID)
|
|
||||||
folders, err := dst.Folders()
|
folders, err := dst.Folders()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return forwardedMatch{}, false, err
|
return forwardedMatch{}, false, err
|
||||||
}
|
}
|
||||||
for _, folder := range folders {
|
for _, folder := range folders {
|
||||||
for offset := 0; ; offset += mailboxListLimit {
|
msgs, err := dst.Fetch(folder.Name)
|
||||||
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 {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
for i, msg := range msgs {
|
||||||
|
if msg.MessageID == messageID || messageIDHeader(msg.Body) == messageID {
|
||||||
return forwardedMatch{
|
return forwardedMatch{
|
||||||
Account: "Ziel: " + targetUser,
|
Account: "Ziel: " + targetUser,
|
||||||
Folder: folder.Name,
|
Folder: folder.Name,
|
||||||
Index: offset + i,
|
Index: i,
|
||||||
Raw: msg.Body,
|
Raw: msg.Body,
|
||||||
}, true, nil
|
}, true, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return forwardedMatch{}, false, nil
|
return forwardedMatch{}, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -570,7 +560,7 @@ func messageIDHeader(raw []byte) string {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
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 {
|
func renderTargetAccountSelect(account, folder, selected string, index int) string {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue