package backend import ( "bytes" "database/sql" "errors" "fmt" "html" "io" "net/http" "net/mail" "net/url" "os" "path/filepath" "strconv" "strings" ) func RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/", homeHandler) mux.HandleFunc("/login", loginHandler) mux.HandleFunc("/logout", logoutHandler) mux.HandleFunc("/password/forgot", forgotPasswordHandler) mux.HandleFunc("/password/reset", resetPasswordHandler) mux.HandleFunc("/users", usersHandler) mux.HandleFunc("/users/save", userSaveHandler) mux.HandleFunc("/users/toggle-active", userToggleActiveHandler) mux.HandleFunc("/users/delete", userDeleteHandler) // Umzug (Konten pflegen + starten/beobachten) mux.HandleFunc("/accounts", accountsHandler) // TODO Codex (02/07) mux.HandleFunc("/accounts/save", accountSaveHandler) // TODO Codex mux.HandleFunc("/accounts/delete", accountDeleteHandler) // TODO Codex mux.HandleFunc("/accounts/test", accountTestHandler) // TODO Codex: Quelle+Ziel-Login pruefen mux.HandleFunc("/archives", archivesHandler) mux.HandleFunc("/archives/create", archiveCreateHandler) mux.HandleFunc("/archives/delete", archiveDeleteHandler) mux.HandleFunc("/archives/dedup", archiveDedupHandler) mux.HandleFunc("/import-export", importExportHandler) mux.HandleFunc("/import-export/upload", archiveUploadHandler) mux.HandleFunc("/import-export/download", archiveDownloadHandler) mux.HandleFunc("/transfer", transferHandler) mux.HandleFunc("/transfer/", transferHandler) mux.HandleFunc("/transfer/side", transferSideHandler) mux.HandleFunc("/transfer/messages", transferMessagesHandler) mux.HandleFunc("/transfer/message", transferMessageHandler) 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) // Viewer (lokale mbox betrachten + weiterleiten) mux.HandleFunc("/view", viewerHandler) mux.HandleFunc("/view/source/select", sourceSelectHandler) mux.HandleFunc("/view/message", messageHandler) mux.HandleFunc("/view/target", targetMessageHandler) mux.HandleFunc("/view/target/select", targetSelectHandler) mux.HandleFunc("/view/manual-copy", manualCopyHandler) mux.HandleFunc("/view/forward", forwardHandler) mux.HandleFunc("/mail/send", mailSendHandler) } func mailSendHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } to := splitRecipients(r.FormValue("to")) subject := strings.TrimSpace(r.FormValue("subject")) body := r.FormValue("body") if len(to) == 0 { http.Error(w, "Bitte mindestens einen Empfaenger eintragen.", http.StatusBadRequest) return } if subject == "" { subject = "(ohne Betreff)" } if err := SendPlainMail(to, subject, body); err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprint(w, "gesendet") } func splitRecipients(value string) []string { fields := strings.FieldsFunc(value, func(r rune) bool { return r == ',' || r == ';' || r == '\n' || r == '\r' || r == '\t' || r == ' ' }) recipients := make([]string, 0, len(fields)) seen := make(map[string]bool, len(fields)) for _, field := range fields { addr := strings.TrimSpace(field) if addr == "" || seen[strings.ToLower(addr)] { continue } recipients = append(recipients, addr) seen[strings.ToLower(addr)] = true } return recipients } func homeHandler(w http.ResponseWriter, r *http.Request) { renderStartPage(w, r) } func renderStartPage(w http.ResponseWriter, r *http.Request) { user := CurrentUser(r) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ` Mail-Graveyard
%s
Willkommen

Mail-Graveyard

Angemeldet als %s. Waehle eine Email-Box zum Sichten, starte einen Mail-Transfer oder pflege Benutzer und Postfaecher.

`, renderRibbonEmailBoxMenu("home"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), html.EscapeString(userDisplayLabel(user))) } // renderShell ist das Grundgeruest im Outlook-2013-Look: // // [ Ribbon: Email-Box | Mail-Transfer | Einstellungen | Mail-Graveyard ] // [ Archiv-mbox | Nachrichtenliste | Quelle/Target ] func renderShell(w http.ResponseWriter, r *http.Request, active, readingPane string) { user := CurrentUser(r) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ` Mail-Graveyard
%s
Nachrichten
%s
`, renderRibbonEmailBoxMenu("archive"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderInitialReadPane(readingPane)) } func accountsHandler(w http.ResponseWriter, r *http.Request) { if !requireManager(w, r) { return } accounts, err := ListAccounts() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } archives, err := ListArchiveMailboxes() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } editName := strings.TrimSpace(r.URL.Query().Get("edit")) var edit Account if editName != "" { edit, err = GetAccount(editName) if err != nil && !errors.Is(err, sql.ErrNoRows) { http.Error(w, err.Error(), http.StatusInternalServerError) return } } renderAccountsPage(w, r, accounts, archives, edit, r.URL.Query().Get("msg"), r.URL.Query().Get("err")) } func accountSaveHandler(w http.ResponseWriter, r *http.Request) { if !requireManager(w, r) { return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { redirectAccounts(w, r, "", err.Error()) return } a := accountFromForm(r) if a.Name == "" || a.SrcHost == "" || a.SrcUser == "" || a.DstHost == "" || a.DstUser == "" { redirectAccounts(w, r, a.Name, "Name, Quelle und Ziel sind Pflichtfelder.") return } if existing, err := GetAccount(a.Name); err == nil { if a.SrcPass == "" { a.SrcPass = existing.SrcPass } if a.DstPass == "" { a.DstPass = existing.DstPass } } if err := SaveAccount(a); err != nil { redirectAccounts(w, r, a.Name, err.Error()) return } redirectAccounts(w, r, a.Name, "Postfach gespeichert.") } func accountDeleteHandler(w http.ResponseWriter, r *http.Request) { if !requireManager(w, r) { return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } name := strings.TrimSpace(r.FormValue("name")) if name == "" { redirectAccounts(w, r, "", "Kein Postfach gewaehlt.") return } if err := DeleteAccount(name); err != nil { redirectAccounts(w, r, name, err.Error()) return } redirectAccounts(w, r, "", "Postfach geloescht.") } func accountTestHandler(w http.ResponseWriter, r *http.Request) { if !requireManager(w, r) { return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } name := strings.TrimSpace(r.FormValue("name")) if name == "" { redirectAccounts(w, r, "", "Kein Postfach gewaehlt.") return } if err := CheckAccount(name); err != nil { redirectAccounts(w, r, name, "Test fehlgeschlagen: "+err.Error()) return } redirectAccounts(w, r, name, "Quelle und Ziel erfolgreich getestet.") } func archivesHandler(w http.ResponseWriter, r *http.Request) { if !requireManager(w, r) { return } accounts, err := ListAccounts() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } archives, err := ListArchiveMailboxes() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } renderArchivesPage(w, r, archives, accounts, archiveStatsMap(archives), r.URL.Query().Get("msg"), r.URL.Query().Get("err")) } func transferHandler(w http.ResponseWriter, r *http.Request) { if !requireManager(w, r) { return } mode := strings.TrimPrefix(r.URL.Path, "/transfer/") if mode == "" || mode == "/transfer" { http.Redirect(w, r, "/transfer/postfach-archiv", http.StatusSeeOther) return } accounts, err := ListAccounts() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } archives, err := ListArchiveMailboxes() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } renderTransferPage(w, r, transferModeBySlug(mode), accounts, archives) } func emailBoxHandler(w http.ResponseWriter, r *http.Request) { accounts, err := ListAccounts() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } kind := strings.TrimPrefix(r.URL.Path, "/boxes/") switch kind { case "source": if r.Header.Get("HX-Request") == "true" { selected := selectedSourceAccount(accounts, r.URL.Query().Get("source")) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprint(w, renderEmailBoxSourcePane(accounts, selected.Name)) fmt.Fprintf(w, `
%s
`, renderInitialSourceMailboxList(selected)) fmt.Fprintf(w, `
%s
`, renderInitialSourceMailboxRead(selected)) return } renderSourceEmailBoxPage(w, r, accounts, r.URL.Query().Get("source")) case "target": 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) } } func sourceMailboxMessagesHandler(w http.ResponseWriter, r *http.Request) { accountName := strings.TrimSpace(r.URL.Query().Get("source")) 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
Quell-Postfach und Ordner waehlen.
`) return } account, err := GetAccount(accountName) if err != nil { fmt.Fprintf(w, `
Nachrichten
%s
`, html.EscapeString(err.Error())) return } messages, err := fetchSourceFolderMessages(account, folder) if err != nil { fmt.Fprintf(w, `
Nachrichten
%s
`, html.EscapeString(err.Error())) return } renderSourceMailboxMessageList(w, account, folder, messages) fmt.Fprintf(w, `
%s
`, renderSourceMailboxRead(account, folder, 0, messages)) } func sourceMailboxMessageHandler(w http.ResponseWriter, r *http.Request) { accountName := strings.TrimSpace(r.URL.Query().Get("source")) 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 := 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, 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 } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } name, err := safeArchiveName(r.FormValue("archive_name")) if err != nil { redirectArchives(w, r, err.Error()) return } if err := os.MkdirAll(filepath.Join(Cfg.MboxRoot, name), 0o700); err != nil { redirectArchives(w, r, err.Error()) return } if err := SaveArchiveMailbox(name); err != nil { redirectArchives(w, r, err.Error()) return } redirectArchives(w, r, "Archiv-Mailbox angelegt.") } func archiveDeleteHandler(w http.ResponseWriter, r *http.Request) { if !requireManager(w, r) { return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } name, err := safeArchiveName(r.FormValue("archive_name")) if err != nil { redirectArchives(w, r, err.Error()) return } if archiveInUse(name) { redirectArchives(w, r, "Archiv-Mailbox ist noch einem Postfach zugeordnet.") return } path := filepath.Join(Cfg.MboxRoot, name) entries, err := os.ReadDir(path) if os.IsNotExist(err) { if err := DeleteArchiveMailbox(name); err != nil { redirectArchives(w, r, err.Error()) return } redirectArchives(w, r, "Archiv-Mailbox entfernt.") return } if err != nil { redirectArchives(w, r, err.Error()) return } if len(entries) > 0 { redirectArchives(w, r, "Archiv-Mailbox ist nicht leer und wurde nicht entfernt.") return } if err := os.Remove(path); err != nil { redirectArchives(w, r, err.Error()) return } if err := DeleteArchiveMailbox(name); err != nil { redirectArchives(w, r, err.Error()) return } redirectArchives(w, r, "Leere Archiv-Mailbox entfernt.") } func migrateRunHandler(w http.ResponseWriter, r *http.Request) { if !requireAdmin(w, r) { return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } name := strings.TrimSpace(r.FormValue("name")) if name == "" { name = "all" } go func() { if err := RunMigration(name, false, 0); err != nil { fmt.Printf("migration %s failed: %v\n", name, err) } }() redirectAccounts(w, r, name, "Umzug gestartet.") } func migrateStatusHandler(w http.ResponseWriter, r *http.Request) { type job struct { Account string Started string Finished sql.NullString Total int Done int Errors int State string } var j job err := DB.QueryRow(`SELECT a.name,j.started,j.finished,j.total,j.done,j.errors,j.state FROM jobs j JOIN accounts a ON a.id=j.account_id ORDER BY j.id DESC LIMIT 1`).Scan(&j.Account, &j.Started, &j.Finished, &j.Total, &j.Done, &j.Errors, &j.State) if errors.Is(err, sql.ErrNoRows) { fmt.Fprint(w, "bereit") return } if err != nil { fmt.Fprintf(w, `%s`, html.EscapeString(err.Error())) return } label := "laeuft" if j.State != "" { label = j.State } fmt.Fprintf(w, `%s: %s %d/%d Fehler %d`, html.EscapeString(j.Account), html.EscapeString(label), j.Done, j.Total, j.Errors) } func usersHandler(w http.ResponseWriter, r *http.Request) { if !requireManager(w, r) { return } users, err := ListAppUsers() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } editName := strings.TrimSpace(r.URL.Query().Get("edit")) var edit AppUser if editName != "" { edit, err = GetAppUser(editName) if err != nil && !errors.Is(err, sql.ErrNoRows) { http.Error(w, err.Error(), http.StatusInternalServerError) return } } renderUsersPage(w, r, users, edit, r.URL.Query().Get("msg"), r.URL.Query().Get("err")) } func userSaveHandler(w http.ResponseWriter, r *http.Request) { if !requireManager(w, r) { return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } oldUsername := strings.TrimSpace(r.FormValue("old_username")) username := strings.TrimSpace(r.FormValue("username")) displayName := strings.TrimSpace(r.FormValue("display_name")) password := r.FormValue("password") role := strings.TrimSpace(r.FormValue("role")) active := r.FormValue("active") == "1" if username == "" { redirectUsers(w, r, "", "Benutzername fehlt.") return } lookupUsername := username if oldUsername != "" { lookupUsername = oldUsername } existing, existingErr := GetAppUser(lookupUsername) if !IsAdmin(r) && existingErr == nil && existing.Role != roleUser { redirectUsers(w, r, oldUsername, "Nur Admins duerfen Verwalter oder Admins bearbeiten.") return } if !IsAdmin(r) { role = roleUser } if errors.Is(existingErr, sql.ErrNoRows) && password == "" { redirectUsers(w, r, username, "Passwort fuer neuen Benutzer fehlt.") return } var hash string if password != "" { var err error hash, err = HashPassword(password) if err != nil { redirectUsers(w, r, username, err.Error()) return } } if err := UpdateAppUser(oldUsername, username, displayName, hash, role, active); err != nil { redirectUsers(w, r, oldUsername, err.Error()) return } redirectUsers(w, r, username, "Benutzer gespeichert.") } func userDeleteHandler(w http.ResponseWriter, r *http.Request) { if !requireManager(w, r) { return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } username := strings.TrimSpace(r.FormValue("username")) if username == "" { redirectUsers(w, r, "", "Kein Benutzer gewaehlt.") return } current := CurrentUser(r) if strings.EqualFold(username, current.Username) { redirectUsers(w, r, username, "Eigenen Benutzer nicht loeschen.") return } target, err := GetAppUser(username) if err != nil { redirectUsers(w, r, username, err.Error()) return } if !IsAdmin(r) && target.Role != roleUser { redirectUsers(w, r, username, "Nur Admins duerfen Verwalter oder Admins loeschen.") return } if err := DeleteAppUser(username); err != nil { redirectUsers(w, r, username, err.Error()) return } redirectUsers(w, r, "", "Benutzer geloescht.") } func userToggleActiveHandler(w http.ResponseWriter, r *http.Request) { if !requireManager(w, r) { return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } username := strings.TrimSpace(r.FormValue("username")) active := r.FormValue("active") == "1" if username == "" { redirectUsers(w, r, "", "Kein Benutzer gewaehlt.") return } current := CurrentUser(r) if strings.EqualFold(username, current.Username) && !active { redirectUsers(w, r, username, "Eigenen Benutzer nicht sperren.") return } target, err := GetAppUser(username) if err != nil { redirectUsers(w, r, username, err.Error()) return } if !IsAdmin(r) && target.Role != roleUser { redirectUsers(w, r, username, "Nur Admins duerfen Verwalter oder Admins sperren.") return } if err := SaveAppUser(username, target.DisplayName, "", target.Role, active); err != nil { redirectUsers(w, r, username, err.Error()) return } if !active { _, _ = DB.Exec(`DELETE FROM app_sessions WHERE user_id=?`, target.ID) redirectUsers(w, r, username, "Benutzer gesperrt.") return } redirectUsers(w, r, username, "Benutzer aktiviert.") } func renderAccountsPage(w http.ResponseWriter, r *http.Request, accounts []Account, archives []string, edit Account, msg, errMsg string) { user := CurrentUser(r) if edit.Name == "" { edit = Account{SrcPort: 993, SrcSecurity: "tls", SrcProto: "imap", DstPort: 993, DstSecurity: "tls", Active: true} } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ` Konten-Verwaltung - Mail-Graveyard
%s
`, renderRibbonEmailBoxMenu(""), renderRibbonTransferMenu(""), renderRibbonActions(false, user)) if msg != "" { fmt.Fprintf(w, `
%s
`, html.EscapeString(msg)) } if errMsg != "" { fmt.Fprintf(w, `
%s
`, html.EscapeString(errMsg)) } fmt.Fprint(w, `
`) } func renderArchivesPage(w http.ResponseWriter, r *http.Request, archives []string, accounts []Account, stats map[string]ArchiveStats, msg, errMsg string) { user := CurrentUser(r) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ` Archiv-mbox Verwaltung - Mail-Graveyard
%s
`, renderRibbonEmailBoxMenu(""), renderRibbonTransferMenu(""), renderRibbonActions(false, user)) if msg != "" { fmt.Fprintf(w, `
%s
`, html.EscapeString(msg)) } if errMsg != "" { fmt.Fprintf(w, `
%s
`, html.EscapeString(errMsg)) } fmt.Fprint(w, `
Archiv-mboxen
`) renderArchiveList(w, archives, accounts, stats) fmt.Fprint(w, `
Archiv-mbox anlegen
`) renderArchiveForm(w) fmt.Fprint(w, `
`) } func renderUsersPage(w http.ResponseWriter, r *http.Request, users []AppUser, edit AppUser, msg, errMsg string) { if edit.Username == "" { edit = AppUser{Role: roleUser, Active: true} } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ` Benutzerverwaltung - Mail-Graveyard
%s
`, renderRibbonEmailBoxMenu(""), renderRibbonTransferMenu(""), renderRibbonActions(true, CurrentUser(r))) if msg != "" { fmt.Fprintf(w, `
%s
`, html.EscapeString(msg)) } if errMsg != "" { fmt.Fprintf(w, `
%s
`, html.EscapeString(errMsg)) } fmt.Fprint(w, `
`) } type transferMode struct { Slug string Title string LeftTitle string RightTitle string LeftKind string RightKind string } func transferModeBySlug(slug string) transferMode { modes := []transferMode{ {Slug: "postfach-archiv", Title: "Postfach <--> Archiv-MBox", LeftTitle: "Postfach", RightTitle: "Archiv-MBox", LeftKind: "source", RightKind: "archive"}, {Slug: "archiv-postfach", Title: "Archiv-MBox <--> Postfach", LeftTitle: "Archiv-MBox", RightTitle: "Postfach", LeftKind: "archive", RightKind: "target"}, {Slug: "postfach-postfach", Title: "Postfach <--> Postfach", LeftTitle: "Quell-Postfach", RightTitle: "Ziel-Postfach", LeftKind: "source", RightKind: "target"}, {Slug: "archiv-archiv", Title: "Archiv-MBox <--> Archiv-MBox", LeftTitle: "Archiv-MBox", RightTitle: "Archiv-MBox", LeftKind: "archive", RightKind: "archive"}, } for _, mode := range modes { if mode.Slug == slug { return mode } } return modes[0] } func renderTransferPage(w http.ResponseWriter, r *http.Request, mode transferMode, accounts []Account, archives []string) { user := CurrentUser(r) leftSelected := defaultTransferValue(mode.LeftKind, accounts, archives) rightSelected := defaultTransferValue(mode.RightKind, accounts, archives) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ` %s - Mail-Graveyard
%s
%s
%s
%s
`, html.EscapeString(mode.Title), renderRibbonEmailBoxMenu(""), renderRibbonTransferMenu(mode.Slug), renderRibbonActions(false, user), transferMenuItems(mode.Slug), renderTransferFolderPane("left", mode.LeftKind, accounts, archives, leftSelected), renderInitialTransferList("left", mode.LeftKind, accounts, archives, leftSelected), renderInitialTransferPreview(mode.LeftKind, leftSelected, mode.RightKind, rightSelected), renderInitialTransferList("right", mode.RightKind, accounts, archives, rightSelected), renderTransferFolderPane("right", mode.RightKind, accounts, archives, rightSelected)) } func transferSideHandler(w http.ResponseWriter, r *http.Request) { side := transferSide(r.URL.Query().Get("side")) kind := strings.TrimSpace(r.URL.Query().Get("kind")) value := strings.TrimSpace(r.URL.Query().Get("value")) accounts, err := ListAccounts() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } archives, err := ListArchiveMailboxes() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprint(w, renderTransferFolderPane(side, kind, accounts, archives, value)) fmt.Fprintf(w, `
%s
`, side, renderInitialTransferList(side, kind, accounts, archives, value)) fmt.Fprintf(w, `
%s
`, renderInitialTransferPreview(kind, value, "", "")) } func transferMessagesHandler(w http.ResponseWriter, r *http.Request) { side := transferSide(r.URL.Query().Get("side")) kind := strings.TrimSpace(r.URL.Query().Get("kind")) value := strings.TrimSpace(r.URL.Query().Get("value")) 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)) } 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, 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)) } func transferSide(value string) string { if value == "right" { return "right" } return "left" } func defaultTransferValue(kind string, accounts []Account, archives []string) string { options := transferEntityOptions(kind, accounts, archives) if len(options) == 0 { return "" } return options[0].Value } func renderTransferFolderPane(side, kind string, accounts []Account, archives []string, selected string) string { options := transferEntityOptions(kind, accounts, archives) if selected == "" && len(options) > 0 { selected = options[0].Value } var b strings.Builder fmt.Fprintf(&b, `
%s
`) if len(options) == 0 { b.WriteString(`
Keine Eintraege.
`) return b.String() } renderTransferFolders(&b, side, kind, selected) return b.String() } type transferOption struct { Value string Label string } func transferEntityOptions(kind string, accounts []Account, archives []string) []transferOption { switch kind { case "archive": out := make([]transferOption, 0, len(archives)) for _, archive := range archives { out = append(out, transferOption{Value: archive, Label: archive}) } return out case "target": targets := accountsByTarget(accounts) out := make([]transferOption, 0, len(targets)) for _, a := range targets { out = append(out, transferOption{Value: a.Name, Label: a.DstUser}) } return out default: sources := accountsBySource(accounts) out := make([]transferOption, 0, len(sources)) for _, a := range sources { out = append(out, transferOption{Value: a.Name, Label: a.SrcUser}) } return out } } func transferKindLabel(kind string) string { switch kind { case "archive": return "Archiv-MBox" case "target": return "Ziel-Postfach" default: return "Quell-Postfach" } } func renderTransferFolders(b *strings.Builder, side, kind, value string) { switch kind { case "archive": mboxes, _ := filepath.Glob(filepath.Join(Cfg.MboxRoot, value, "*.mbox")) if len(mboxes) == 0 { b.WriteString(`
Keine Ordner.
`) return } for _, folder := range archiveFolderInfos(mboxes) { fmt.Fprintf(b, `%s%d`, side, html.EscapeString(kind), urlEsc(value), urlEsc(folder.Name), side, html.EscapeString(folder.Label), folder.Count) } case "target": account, err := GetAccount(value) if err != nil { fmt.Fprintf(b, `
%s
`, html.EscapeString(err.Error())) return } 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 } for _, folder := range folders { fmt.Fprintf(b, `%s%s`, side, html.EscapeString(kind), urlEsc(value), urlEsc(folder.Name), side, html.EscapeString(folder.Name), html.EscapeString(targetFolderRole(folder))) } default: account, err := GetAccount(value) if err != nil { fmt.Fprintf(b, `
%s
`, html.EscapeString(err.Error())) return } fmt.Fprintf(b, `
%s
`, html.EscapeString(account.SrcUser)) if accountProto(account) == "pop3" { fmt.Fprintf(b, `INBOXPOP3`, side, html.EscapeString(kind), urlEsc(value), side) return } src, err := OpenIMAPSource(account) if err != nil { fmt.Fprintf(b, `
Ordner konnten nicht geladen werden: %s
`, html.EscapeString(err.Error())) return } defer src.Close() folders, err := src.Folders() if err != nil { fmt.Fprintf(b, `
Ordner konnten nicht geladen werden: %s
`, html.EscapeString(err.Error())) return } for _, folder := range folders { fmt.Fprintf(b, `%s%s`, side, html.EscapeString(kind), urlEsc(value), urlEsc(folder.Name), side, html.EscapeString(folder.Name), html.EscapeString(sourceFolderRole(folder))) } } } func renderInitialTransferList(side, kind string, accounts []Account, archives []string, selected string) string { options := transferEntityOptions(kind, accounts, archives) if selected == "" && len(options) > 0 { selected = options[0].Value } if selected == "" { return `
Nachrichten
Postfach waehlen.
` } folder, err := firstTransferFolder(kind, selected) if err != nil { return fmt.Sprintf(`
Nachrichten
%s
`, html.EscapeString(transferEmptyReason(kind, err))) } return renderTransferMessageList(side, kind, selected, folder) } func renderInitialTransferPreview(leftKind, leftValue, rightKind, rightValue string) string { if leftValue != "" { if folder, err := firstTransferFolder(leftKind, leftValue); err == nil { return renderTransferPreview(leftKind, leftValue, folder, 0) } } if rightValue != "" { if folder, err := firstTransferFolder(rightKind, rightValue); err == nil { return renderTransferPreview(rightKind, rightValue, folder, 0) } } return renderEmptyPreviewPane("Nachricht links oder rechts waehlen.") } func transferEmptyReason(kind string, err error) string { msg := err.Error() if kind == "archive" && strings.Contains(msg, "Keine Ordner") { return "Diese Archiv-MBox ist noch leer." } return msg } func firstTransferFolder(kind, value string) (string, error) { switch kind { case "archive": mboxes, _ := filepath.Glob(filepath.Join(Cfg.MboxRoot, value, "*.mbox")) infos := archiveFolderInfos(mboxes) if len(infos) == 0 { return "", fmt.Errorf("Keine Ordner.") } for _, folder := range infos { if strings.EqualFold(folder.Name, "INBOX") { return folder.Name, nil } } return infos[0].Name, nil case "target": account, err := GetAccount(value) if err != nil { return "", err } return firstTargetFolder(account) default: account, err := GetAccount(value) if err != nil { return "", err } return firstSourceFolder(account) } } func renderTransferMessageList(side, kind, value, folder string) string { var b strings.Builder b.WriteString(`
Nachrichten
`) switch kind { case "archive": path, err := mboxPath(value, folder) if err != nil { fmt.Fprintf(&b, `
%s
`, html.EscapeString(err.Error())) return b.String() } entries, err := ReadMboxList(path) if err != nil { fmt.Fprintf(&b, `
%s
`, html.EscapeString(err.Error())) return b.String() } for _, entry := range entries { subject := entry.Subject if subject == "" { subject = "(ohne Betreff)" } from := entry.From if from == "" { from = "(ohne Absender)" } active := "" if entry.Index == 0 { active = " active" } fmt.Fprintf(&b, `%s%s%s`, active, side, html.EscapeString(kind), urlEsc(value), urlEsc(folder), entry.Index, html.EscapeString(from), html.EscapeString(subject), html.EscapeString(entry.Date)) } if len(entries) == 0 { b.WriteString(`
Keine Nachrichten.
`) } case "target": account, err := GetAccount(value) if err != nil { fmt.Fprintf(&b, `
%s
`, html.EscapeString(err.Error())) return b.String() } messages, err := fetchTargetFolderMessages(account, folder) if err != nil { fmt.Fprintf(&b, `
%s
`, html.EscapeString(err.Error())) return b.String() } 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 := fetchSourceFolderMessages(account, folder) if err != nil { fmt.Fprintf(&b, `
%s
`, html.EscapeString(err.Error())) return b.String() } renderTransferRawMessages(&b, side, kind, value, folder, messages) } return b.String() } 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), 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) string { raw, label, err := transferRawMessage(kind, value, folder, index) if err != nil { return renderEmptyPreviewPane(err.Error()) } msg, err := mail.ReadMessage(bytes.NewReader(raw)) var b strings.Builder if err != nil { renderMailboxReadPane(&b, "Vorschau", label, folder, index, "(unlesbar)", "", "", string(raw)) return b.String() } renderMailboxReadPane(&b, "Vorschau", label, folder, index, decodeHeader(msg.Header.Get("Subject")), decodeHeader(msg.Header.Get("From")), decodeHeader(msg.Header.Get("Date")), messageBody(raw), ) return b.String() } func transferRawMessage(kind, value, folder string, index int) ([]byte, string, error) { switch kind { case "archive": path, err := mboxPath(value, folder) if err != nil { return nil, value, err } raw, err := ReadMboxMessage(path, index) return raw, value, err case "target": account, err := GetAccount(value) if err != nil { return nil, value, 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 } 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 } } func renderSourceEmailBoxPage(w http.ResponseWriter, r *http.Request, accounts []Account, selected string) { user := CurrentUser(r) selectedAccount := selectedSourceAccount(accounts, selected) w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ` Quell-Postfach - Mail-Graveyard
%s
%s
%s
`, renderRibbonEmailBoxMenu("source"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderEmailBoxSourcePane(accounts, selectedAccount.Name), renderInitialSourceMailboxList(selectedAccount), renderInitialSourceMailboxRead(selectedAccount)) } 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, ` Ziel-Postfach - Mail-Graveyard
%s
%s
%s
`, renderRibbonEmailBoxMenu("target"), renderRibbonTransferMenu(""), renderRibbonActions(false, user), renderInitialTargetMailboxList(selectedAccount), renderInitialTargetMailboxRead(selectedAccount), renderEmailBoxTargetPane(accounts, selectedAccount.Name)) } func renderEmailBoxSourcePane(accounts []Account, selected string) string { var b strings.Builder sourceAccounts := accountsBySource(accounts) if len(sourceAccounts) == 0 { b.WriteString(`
Quell-Postfach
Keine Quell-Postfaecher eingetragen.
`) return b.String() } selectedAccount := selectedSourceAccount(accounts, selected) b.WriteString(`
Quell-Postfach
`) renderSourceFolders(&b, selectedAccount) return b.String() } func selectedSourceAccount(accounts []Account, selected string) Account { sourceAccounts := accountsBySource(accounts) if len(sourceAccounts) == 0 { return Account{} } if selected != "" { for _, a := range sourceAccounts { if a.Name == selected { return a } } } return sourceAccounts[0] } func renderSourceFolders(b *strings.Builder, account Account) { fmt.Fprintf(b, `
%s
`, html.EscapeString(account.SrcUser)) if accountProto(account) == "pop3" { b.WriteString(`
INBOXPOP3
`) return } src, err := OpenIMAPSource(account) if err != nil { fmt.Fprintf(b, `
Ordner konnten nicht geladen werden: %s
`, html.EscapeString(err.Error())) return } defer src.Close() folders, err := src.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 Quell-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(sourceFolderRole(folder))) } } func sourceFolderRole(folder Folder) 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 fetchSourceFolderMessages(account Account, folder string) ([]RawMessage, error) { if accountProto(account) == "pop3" { src, err := OpenPOP3Source(account) if err != nil { return nil, err } defer src.Close() return src.Fetch(folder) } src, err := OpenIMAPSource(account) if err != nil { return nil, err } defer src.Close() return src.Fetch(folder) } func renderInitialSourceMailboxList(account Account) string { if account.Name == "" { return `
Nachrichten
Quell-Postfach waehlen.
` } folder, err := firstSourceFolder(account) if err != nil { return fmt.Sprintf(`
Nachrichten
%s
`, html.EscapeString(err.Error())) } 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, messages) return b.String() } func renderInitialSourceMailboxRead(account Account) string { if account.Name == "" { return `
Nachricht waehlen.
` } folder, err := firstSourceFolder(account) if err != nil { return `
Nachricht waehlen.
` } messages, err := fetchSourceFolderMessages(account, folder) if err != nil { return `
Nachricht waehlen.
` } return renderSourceMailboxRead(account, folder, 0, messages) } func firstSourceFolder(account Account) (string, error) { if accountProto(account) == "pop3" { return "INBOX", nil } src, err := OpenIMAPSource(account) if err != nil { return "", err } defer src.Close() folders, err := src.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 Quell-Postfach.") } return folders[0].Name, nil } func renderSourceMailboxMessageList(w io.Writer, account Account, folder string, messages []RawMessage) { fmt.Fprint(w, `
Nachrichten
`) 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 Quell-Ordner.
`) } } func renderSourceMailboxRead(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 { renderMailboxReadPane(&b, "Quelle", account.Name, folder, index, "(unlesbar)", "", "", string(raw)) return b.String() } renderMailboxReadPane(&b, "Quelle", 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 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.Fprint(w, `
Nachrichten
`) 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 { renderMailboxReadPane(&b, "Ziel", account.Name, folder, index, "(unlesbar)", "", "", string(raw)) return b.String() } renderMailboxReadPane(&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 renderMailboxReadPane(w io.Writer, title, account, folder string, index int, subject, from, date, body string) { if subject == "" { subject = "(ohne Betreff)" } fmt.Fprint(w, `
Vorschau
`) fmt.Fprintf(w, `

%s

Von: %s
Datum: %s
Postfach: %s / %s / #%d
%s
`, html.EscapeString(subject), html.EscapeString(from), html.EscapeString(date), html.EscapeString(account), html.EscapeString(folder), index+1, html.EscapeString(body)) } func renderEmptyPreviewPane(message string) string { return fmt.Sprintf(`
Vorschau
%s
`, html.EscapeString(message)) } 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
`) if len(targets) == 0 { b.WriteString(`
Keine Ziel-Postfaecher eingetragen.
`) return b.String() } for _, target := range targets { fmt.Fprintf(&b, `
%s
`, html.EscapeString(target)) } return b.String() } func accountsBySource(accounts []Account) []Account { out := append([]Account{}, accounts...) for i := 0; i < len(out); i++ { for j := i + 1; j < len(out); j++ { left := strings.ToLower(out[i].SrcUser) right := strings.ToLower(out[j].SrcUser) if right < left { out[i], out[j] = out[j], out[i] } } } return out } func accountProto(a Account) string { if strings.TrimSpace(a.SrcProto) != "" { return a.SrcProto } return "imap" } func transferMenuItems(active string) string { items := []transferMode{ {Slug: "postfach-archiv", Title: "Postfach <--> Archiv-MBox"}, {Slug: "archiv-postfach", Title: "Archiv-MBox <--> Postfach"}, {Slug: "postfach-postfach", Title: "Postfach <--> Postfach"}, {Slug: "archiv-archiv", Title: "Archiv-MBox <--> Archiv-MBox"}, } var b strings.Builder for _, item := range items { class := "backstage-item transfer-sub" if item.Slug == active { class += " active" } fmt.Fprintf(&b, `%s`, class, item.Slug, html.EscapeString(item.Title)) } return b.String() } func renderRibbonEmailBoxMenu(active string) string { items := []transferMode{ {Slug: "archive", Title: "Archiv-Postfach"}, {Slug: "source", Title: "Quell-Postfach"}, {Slug: "target", Title: "Ziel-Postfach"}, } paths := map[string]string{ "archive": "/view", "source": "/boxes/source", "target": "/boxes/target", } buttonClass := "tab ribbon-menu-button" if active != "" { if active == "home" { buttonClass = "tab ribbon-menu-button" } else { buttonClass += " active" } } var b strings.Builder homeClass := "tab app-tab" if active == "home" { homeClass += " active" } fmt.Fprintf(&b, `Mail-Graveyard`, homeClass) fmt.Fprintf(&b, `
`, buttonClass) for _, item := range items { class := "ribbon-dropdown-item" if item.Slug == active { class += " active" } fmt.Fprintf(&b, `%s`, class, paths[item.Slug], html.EscapeString(item.Title)) } b.WriteString(`
`) return b.String() } func renderRibbonActions(settingsActive bool, user AppUser) string { settingsClass := "ribbon-icon" if settingsActive { settingsClass += " active" } return fmt.Sprintf(`
%s`, html.EscapeString(user.Username), html.EscapeString(userDisplayName(user)), settingsClass) } func renderRibbonTransferMenu(active string) string { items := []transferMode{ {Slug: "postfach-archiv", Title: "Postfach <--> Archiv-MBox"}, {Slug: "archiv-postfach", Title: "Archiv-MBox <--> Postfach"}, {Slug: "postfach-postfach", Title: "Postfach <--> Postfach"}, {Slug: "archiv-archiv", Title: "Archiv-MBox <--> Archiv-MBox"}, } buttonClass := "tab ribbon-menu-button" if active != "" { buttonClass += " active" } var b strings.Builder fmt.Fprintf(&b, `
`, buttonClass) for _, item := range items { class := "ribbon-dropdown-item" if item.Slug == active { class += " active" } fmt.Fprintf(&b, `%s`, class, item.Slug, html.EscapeString(item.Title)) } b.WriteString(`
`) return b.String() } func renderBackstage(active string) string { items := []struct { Key string Href string Label string }{ {"home", "/", "Start"}, {"archive", "/view", "Archiv-Boxen"}, {"import-export", "/import-export", "Import/Export"}, {"accounts", "/accounts", "Konten-Verwaltung"}, {"archives", "/archives", "Archiv-mbox Verwaltung"}, {"transfer", "/transfer/postfach-archiv", "Mail-Transfer"}, {"users", "/users", "Benutzerverwaltung"}, {"logout", "/logout", "Logout"}, } var b strings.Builder b.WriteString(``) return b.String() } func renderTransferPicker(kind string, accounts []Account, archives []string) string { var b strings.Builder b.WriteString(`
`) switch kind { case "archive": fmt.Fprintf(&b, ``, transferSelect("archive", archives)) case "target": fmt.Fprintf(&b, ``, transferSelect("target", uniqueAccountValues(accounts, "target"))) default: fmt.Fprintf(&b, ``, transferSelect("source", uniqueAccountValues(accounts, "source"))) } b.WriteString(`
`) return b.String() } func transferSelect(name string, values []string) string { if len(values) == 0 { return `` } var b strings.Builder fmt.Fprintf(&b, ``) return b.String() } func uniqueAccountValues(accounts []Account, field string) []string { seen := map[string]bool{} var out []string for _, a := range accounts { value := a.SrcUser if field == "target" { value = a.DstUser } value = strings.TrimSpace(value) if value == "" || seen[value] { continue } seen[value] = true out = append(out, value) } sortStrings(out) return out } func renderAccountsTable(w http.ResponseWriter, accounts []Account, editName string) { if len(accounts) == 0 { fmt.Fprint(w, `
Noch keine Postfaecher eingetragen.
`) return } fmt.Fprint(w, ``) for _, a := range accounts { activeClass := "" if a.Name == editName { activeClass = ` class="selected"` } status := "aktiv" if !a.Active { status = "inaktiv" } fmt.Fprintf(w, ``, activeClass, urlQuery(a.Name), html.EscapeString(a.Name), html.EscapeString(a.SrcUser), html.EscapeString(accountMboxDir(a)), html.EscapeString(a.DstUser), status, html.EscapeString(a.Name), html.EscapeString(a.Name), html.EscapeString(a.Name)) } fmt.Fprint(w, `
NameOriginalArchiv-mboxZielStatus
%s%s%s%s%s
`) } func renderUsersTable(w http.ResponseWriter, users []AppUser, editName string) { if len(users) == 0 { fmt.Fprint(w, `
Noch keine Benutzer eingetragen.
`) return } fmt.Fprint(w, ``) for _, u := range users { activeClass := "" if u.Username == editName { activeClass = ` class="selected"` } status := "aktiv" if !u.Active { status = "gesperrt" } nextActive := "0" toggleLabel := "Sperren" toggleClass := "btn compact secondary" if !u.Active { nextActive = "1" toggleLabel = "Aktivieren" } editButton := fmt.Sprintf(`Bearbeiten`, urlQuery(u.Username)) toggleButton := fmt.Sprintf(``, html.EscapeString(u.Username), nextActive, toggleClass, toggleLabel) deleteButton := fmt.Sprintf(``, html.EscapeString(u.Username)) fmt.Fprintf(w, ``, activeClass, html.EscapeString(userDisplayName(u)), html.EscapeString(u.Username), html.EscapeString(u.Role), status, editButton, toggleButton, deleteButton) } fmt.Fprint(w, `
NameLogin/E-MailRolleStatus
%s%s%s%s%s%s%s
`) } func renderUserForm(w http.ResponseWriter, r *http.Request, u AppUser) { roleField := roleSelect(u.Role, IsAdmin(r)) if !IsAdmin(r) { roleField = `` } fmt.Fprintf(w, `
Zugang
%s
Neu
`, html.EscapeString(u.Username), html.EscapeString(u.Username), checked(u.Active), html.EscapeString(u.DisplayName), passwordHint(u.Username), passwordPlaceholder(u.Username), roleField) } func userDisplayName(u AppUser) string { name := strings.TrimSpace(u.DisplayName) if name != "" { return name } return u.Username } func userDisplayLabel(u AppUser) string { name := strings.TrimSpace(u.DisplayName) if name != "" { return name + " <" + u.Username + ">" } return u.Username } func roleSelect(current string, admin bool) string { if current == "" { current = roleUser } values := []string{roleUser} if admin { values = []string{roleUser, roleManager, roleAdmin} } return selectHTML("role", current, values) } func passwordPlaceholder(username string) string { if username == "" { return "Pflicht fuer neuen Benutzer" } return "leer lassen = behalten" } func passwordHint(username string) string { if username == "" { return "Neuer Benutzer: Passwort eintragen." } return "Bearbeiten: Neues Passwort eintragen oder leer lassen, um das bestehende Passwort zu behalten." } func renderArchiveList(w http.ResponseWriter, archives []string, accounts []Account, stats map[string]ArchiveStats) { if len(archives) == 0 { fmt.Fprint(w, `
Noch keine Archiv-Mailbox angelegt.
`) return } fmt.Fprint(w, ``) for _, archive := range archives { uses := archiveUses(archive, accounts) stat := stats[archive] var actions strings.Builder fmt.Fprintf(&actions, `Download`, urlQuery(archive)) if stat.Duplicates > 0 { fmt.Fprintf(&actions, ``, html.EscapeString(archive)) } if uses == "" { fmt.Fprintf(&actions, ``, html.EscapeString(archive)) uses = "frei" } fmt.Fprintf(w, ``, html.EscapeString(archive), html.EscapeString(uses), html.EscapeString(formatBytes(stat.Bytes)), stat.Messages, stat.Duplicates, actions.String()) } fmt.Fprint(w, `
Archiv-MailboxZuordnungSpeicherMailsDubletten
%s%s%s%d%d%s
`) } func renderArchiveForm(w http.ResponseWriter) { fmt.Fprint(w, `
Archivlogik

Archiv-mboxen sind eigenstaendige Mailboxen und nicht von einem Quellkonto abhaengig.

Sie koennen leer angelegt oder spaeter per Import mit mbox/PST-Inhalten gefuellt werden.

Entfernen ist nur moeglich, wenn die Mailbox keinem Konto zugeordnet und leer ist.

`) } func renderAccountForm(w http.ResponseWriter, a Account, archives []string) { fmt.Fprintf(w, `
Original
Archiv-mbox
Ziel
Neu
`, html.EscapeString(a.Name), checked(a.Active), html.EscapeString(a.SrcHost), a.SrcPort, securitySelect("src_security", a.SrcSecurity), html.EscapeString(a.SrcUser), protoSelect(a.SrcProto), checked(a.SrcInsecure), archiveSelect(accountMboxDir(a), archives), html.EscapeString(a.DstHost), a.DstPort, securitySelect("dst_security", a.DstSecurity), html.EscapeString(a.DstUser), checked(a.DstInsecure)) } func accountFromForm(r *http.Request) Account { return Account{ Name: strings.TrimSpace(r.FormValue("name")), SrcHost: strings.TrimSpace(r.FormValue("src_host")), SrcPort: parsePort(r.FormValue("src_port")), SrcSecurity: strings.TrimSpace(r.FormValue("src_security")), SrcInsecure: r.FormValue("src_insecure") == "1", SrcUser: strings.TrimSpace(r.FormValue("src_user")), SrcPass: r.FormValue("src_pass"), SrcProto: strings.TrimSpace(r.FormValue("src_proto")), DstHost: strings.TrimSpace(r.FormValue("dst_host")), DstPort: parsePort(r.FormValue("dst_port")), DstSecurity: strings.TrimSpace(r.FormValue("dst_security")), DstInsecure: r.FormValue("dst_insecure") == "1", DstUser: strings.TrimSpace(r.FormValue("dst_user")), DstPass: r.FormValue("dst_pass"), MboxDir: strings.TrimSpace(r.FormValue("mbox_dir")), Active: r.FormValue("active") == "1", } } func securitySelect(name, current string) string { if current == "" { current = "tls" } return selectHTML(name, current, []string{"tls", "starttls", "none"}) } func protoSelect(current string) string { if current == "" { current = "imap" } return selectHTML("src_proto", current, []string{"imap", "pop3"}) } func archiveSelect(current string, archives []string) string { values := append([]string{}, archives...) found := false for _, archive := range values { if archive == current { found = true break } } if current != "" && !found { values = append([]string{current}, values...) } if len(values) == 0 { return `` } var b strings.Builder b.WriteString(``) return b.String() } func selectHTML(name, current string, values []string) string { var b strings.Builder fmt.Fprintf(&b, ``) return b.String() } func checked(v bool) string { if v { return "checked" } return "" } func parsePort(value string) int { port, _ := strconv.Atoi(strings.TrimSpace(value)) return port } func archiveUses(archive string, accounts []Account) string { var uses []string for _, a := range accounts { if accountMboxDir(a) == archive { uses = append(uses, a.Name) } } return strings.Join(uses, ", ") } func archiveInUse(archive string) bool { accounts, err := ListAccounts() if err != nil { return true } return archiveUses(archive, accounts) != "" } func safeArchiveName(value string) (string, error) { name := strings.TrimSpace(value) if name == "" { return "", fmt.Errorf("Name der Archiv-Mailbox fehlt.") } if name == "." || name == ".." || strings.ContainsAny(name, `/\:`) || filepath.Clean(name) != name { return "", fmt.Errorf("Ungueltiger Archiv-Mailbox-Name.") } return name, nil } func sortStrings(values []string) { for i := 0; i < len(values); i++ { for j := i + 1; j < len(values); j++ { if strings.ToLower(values[j]) < strings.ToLower(values[i]) { values[i], values[j] = values[j], values[i] } } } } func redirectAccounts(w http.ResponseWriter, r *http.Request, edit, msg string) { key := "msg" if strings.Contains(strings.ToLower(msg), "fehl") || strings.Contains(strings.ToLower(msg), "pflicht") || strings.Contains(strings.ToLower(msg), "error") { key = "err" } target := "/accounts?" + key + "=" + urlQuery(msg) if edit != "" { target += "&edit=" + urlQuery(edit) } http.Redirect(w, r, target, http.StatusSeeOther) } func redirectArchives(w http.ResponseWriter, r *http.Request, msg string) { key := "msg" if strings.Contains(strings.ToLower(msg), "fehl") || strings.Contains(strings.ToLower(msg), "ungueltig") || strings.Contains(strings.ToLower(msg), "nicht") || strings.Contains(strings.ToLower(msg), "error") { key = "err" } http.Redirect(w, r, "/archives?"+key+"="+urlQuery(msg), http.StatusSeeOther) } func redirectUsers(w http.ResponseWriter, r *http.Request, edit, msg string) { key := "msg" if strings.Contains(strings.ToLower(msg), "fehl") || strings.Contains(strings.ToLower(msg), "pflicht") || strings.Contains(strings.ToLower(msg), "nicht") || strings.Contains(strings.ToLower(msg), "error") { key = "err" } target := "/users?" + key + "=" + urlQuery(msg) if edit != "" { target += "&edit=" + urlQuery(edit) } http.Redirect(w, r, target, http.StatusSeeOther) } func urlQuery(value string) string { return url.QueryEscape(value) }