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.
+
-
+
-
+
`,
- 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