From 647ce4bc90879760ec2a4489d80d6c0bbc1aa684 Mon Sep 17 00:00:00 2001 From: DonVoo Date: Mon, 13 Jul 2026 03:47:39 +0200 Subject: [PATCH] Load selected target mailbox messages --- backend/00-router.go | 276 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 269 insertions(+), 7 deletions(-) diff --git a/backend/00-router.go b/backend/00-router.go index 4af265f..692c87e 100644 --- a/backend/00-router.go +++ b/backend/00-router.go @@ -44,6 +44,8 @@ func RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/boxes/source", emailBoxHandler) mux.HandleFunc("/boxes/source/messages", sourceMailboxMessagesHandler) mux.HandleFunc("/boxes/source/message", sourceMailboxMessageHandler) + mux.HandleFunc("/boxes/target/messages", targetMailboxMessagesHandler) + mux.HandleFunc("/boxes/target/message", targetMailboxMessageHandler) mux.HandleFunc("/boxes/target", emailBoxHandler) mux.HandleFunc("/migrate/run", migrateRunHandler) // TODO Codex: RunMigration im Hintergrund mux.HandleFunc("/migrate/status", migrateStatusHandler) // TODO Codex: jobs-Fortschritt (HTMX-Poll) @@ -316,7 +318,15 @@ func emailBoxHandler(w http.ResponseWriter, r *http.Request) { } renderSourceEmailBoxPage(w, r, accounts, r.URL.Query().Get("source")) case "target": - renderTargetEmailBoxPage(w, r, accounts) + if r.Header.Get("HX-Request") == "true" { + selected := selectedTargetAccount(accounts, r.URL.Query().Get("target")) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprint(w, renderEmailBoxTargetPane(accounts, selected.Name)) + fmt.Fprintf(w, `
%s
`, renderInitialTargetMailboxList(selected)) + fmt.Fprintf(w, `
%s
`, renderInitialTargetMailboxRead(selected)) + return + } + renderTargetEmailBoxPage(w, r, accounts, r.URL.Query().Get("target")) default: http.Redirect(w, r, "/view", http.StatusSeeOther) } @@ -366,6 +376,50 @@ func sourceMailboxMessageHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, renderSourceMailboxRead(account, folder, index, messages)) } +func targetMailboxMessagesHandler(w http.ResponseWriter, r *http.Request) { + accountName := strings.TrimSpace(r.URL.Query().Get("target")) + folder := strings.TrimSpace(r.URL.Query().Get("folder")) + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if accountName == "" || folder == "" { + fmt.Fprint(w, `
Nachrichten
Ziel-Postfach und Ordner waehlen.
`) + return + } + account, err := GetAccount(accountName) + if err != nil { + fmt.Fprintf(w, `
Nachrichten
%s
`, html.EscapeString(err.Error())) + return + } + messages, err := fetchTargetFolderMessages(account, folder) + if err != nil { + fmt.Fprintf(w, `
Nachrichten
%s
`, html.EscapeString(err.Error())) + return + } + renderTargetMailboxMessageList(w, account, folder, messages) + fmt.Fprintf(w, `
%s
`, renderTargetMailboxRead(account, folder, 0, messages)) +} + +func targetMailboxMessageHandler(w http.ResponseWriter, r *http.Request) { + accountName := strings.TrimSpace(r.URL.Query().Get("target")) + folder := strings.TrimSpace(r.URL.Query().Get("folder")) + 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 + } + 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, renderTargetMailboxRead(account, folder, index, messages)) +} + func archiveCreateHandler(w http.ResponseWriter, r *http.Request) { if !requireManager(w, r) { return @@ -879,8 +933,9 @@ func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [ renderRibbonEmailBoxMenu("source"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderEmailBoxSourcePane(accounts, selectedAccount.Name), renderInitialSourceMailboxList(selectedAccount), renderInitialSourceMailboxRead(selectedAccount)) } -func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts []Account) { +func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts []Account, selected string) { user := CurrentUser(r) + selectedAccount := selectedTargetAccount(accounts, selected) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ` @@ -911,14 +966,14 @@ func renderTargetEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts [
-
Nachrichten
Ziel-Postfach rechts waehlen.
+
%s
-
Nachricht waehlen.
+
%s
- +
`, - renderRibbonEmailBoxMenu("target"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderEmailBoxTargetPane(accounts)) + renderRibbonEmailBoxMenu("target"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderInitialTargetMailboxList(selectedAccount), renderInitialTargetMailboxRead(selectedAccount), renderEmailBoxTargetPane(accounts, selectedAccount.Name)) } func renderEmailBoxSourcePane(accounts []Account, selected string) string { @@ -1124,7 +1179,214 @@ func renderSourceMailboxRead(account Account, folder string, index int, messages return b.String() } -func renderEmailBoxTargetPane(accounts []Account) string { +func renderEmailBoxTargetPane(accounts []Account, selected string) string { + var b strings.Builder + targetAccounts := accountsByTarget(accounts) + if len(targetAccounts) == 0 { + b.WriteString(`
Ziel-Postfach
Keine Ziel-Postfaecher eingetragen.
`) + return b.String() + } + selectedAccount := selectedTargetAccount(accounts, selected) + b.WriteString(`
Ziel-Postfach
`) + renderTargetFolders(&b, selectedAccount) + return b.String() +} + +func selectedTargetAccount(accounts []Account, selected string) Account { + targetAccounts := accountsByTarget(accounts) + if len(targetAccounts) == 0 { + return Account{} + } + if selected != "" { + for _, a := range targetAccounts { + if a.Name == selected { + return a + } + } + } + return targetAccounts[0] +} + +func renderTargetFolders(b *strings.Builder, account Account) { + fmt.Fprintf(b, `
%s
`, html.EscapeString(account.DstUser)) + dst, err := OpenIMAPTarget(account) + if err != nil { + fmt.Fprintf(b, `
Ordner konnten nicht geladen werden: %s
`, html.EscapeString(err.Error())) + return + } + defer dst.Close() + folders, err := dst.Folders() + if err != nil { + fmt.Fprintf(b, `
Ordner konnten nicht geladen werden: %s
`, html.EscapeString(err.Error())) + return + } + if len(folders) == 0 { + b.WriteString(`
Keine Ordner in diesem Ziel-Postfach.
`) + return + } + for _, folder := range folders { + fmt.Fprintf(b, `%s%s`, + html.EscapeString(strings.Join(folder.Attrs, " ")), urlEsc(account.Name), urlEsc(folder.Name), html.EscapeString(folder.Name), html.EscapeString(targetFolderRole(folder))) + } +} + +func targetFolderRole(folder TargetFolder) string { + for _, attr := range folder.Attrs { + switch strings.ToLower(attr) { + case `\sent`: + return "Gesendet" + case `\drafts`: + return "Entwurf" + case `\trash`: + return "Papierkorb" + case `\junk`: + return "Spam" + } + } + return "" +} + +func fetchTargetFolderMessages(account Account, folder string) ([]RawMessage, error) { + dst, err := OpenIMAPTarget(account) + if err != nil { + return nil, err + } + defer dst.Close() + return dst.Fetch(folder) +} + +func renderInitialTargetMailboxList(account Account) string { + if account.Name == "" { + return `
Nachrichten
Ziel-Postfach waehlen.
` + } + folder, err := firstTargetFolder(account) + if err != nil { + return fmt.Sprintf(`
Nachrichten
%s
`, html.EscapeString(err.Error())) + } + 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, messages) + return b.String() +} + +func renderInitialTargetMailboxRead(account Account) string { + if account.Name == "" { + return `
Nachricht waehlen.
` + } + folder, err := firstTargetFolder(account) + if err != nil { + return `
Nachricht waehlen.
` + } + messages, err := fetchTargetFolderMessages(account, folder) + if err != nil { + return `
Nachricht waehlen.
` + } + return renderTargetMailboxRead(account, folder, 0, messages) +} + +func firstTargetFolder(account Account) (string, error) { + dst, err := OpenIMAPTarget(account) + if err != nil { + return "", err + } + defer dst.Close() + folders, err := dst.Folders() + if err != nil { + return "", err + } + for _, folder := range folders { + if strings.EqualFold(folder.Name, "INBOX") { + return folder.Name, nil + } + } + if len(folders) == 0 { + return "", fmt.Errorf("Keine Ordner in diesem Ziel-Postfach.") + } + return folders[0].Name, nil +} + +func renderTargetMailboxMessageList(w io.Writer, account Account, folder string, messages []RawMessage) { + fmt.Fprintf(w, `
%s / %s (%d)
`, html.EscapeString(account.DstUser), html.EscapeString(folder), len(messages)) + 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`, + active, + 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 { + fmt.Fprint(w, `
Keine Nachrichten in diesem Ziel-Ordner.
`) + } +} + +func renderTargetMailboxRead(account Account, folder string, index int, messages []RawMessage) string { + if index < 0 || index >= len(messages) { + return `
Nachricht waehlen.
` + } + raw := messages[index].Body + msg, err := mail.ReadMessage(bytes.NewReader(raw)) + var b strings.Builder + if err != nil { + renderReadPane(&b, "Ziel", account.Name, folder, "", index, "(unlesbar)", "", "", string(raw)) + return b.String() + } + renderReadPane(&b, "Ziel", account.Name, folder, "", index, + decodeHeader(msg.Header.Get("Subject")), + decodeHeader(msg.Header.Get("From")), + decodeHeader(msg.Header.Get("Date")), + messageBody(raw), + ) + return b.String() +} + +func accountsByTarget(accounts []Account) []Account { + seen := map[string]bool{} + out := make([]Account, 0, len(accounts)) + for _, a := range accounts { + if strings.TrimSpace(a.DstUser) == "" || seen[a.DstUser] { + continue + } + seen[a.DstUser] = true + out = append(out, a) + } + for i := 0; i < len(out); i++ { + for j := i + 1; j < len(out); j++ { + if strings.ToLower(out[j].DstUser) < strings.ToLower(out[i].DstUser) { + out[i], out[j] = out[j], out[i] + } + } + } + return out +} + +func renderEmailBoxTargetPaneLegacy(accounts []Account) string { var b strings.Builder b.WriteString(`
Ziel-Postfach