package backend import ( "database/sql" "errors" "fmt" "html" "net/http" "net/url" "strconv" "strings" ) func RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/", homeHandler) mux.HandleFunc("/login", loginHandler) // 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("/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) } // homeHandler rendert die Outlook-2013-Oberflaeche (Dreispalter). Der linke // Baum, die Nachrichtenliste und der Lesebereich werden per HTMX nachgeladen. func homeHandler(w http.ResponseWriter, r *http.Request) { // "active" ist die CSS-Klasse fuer den hervorgehobenen Reiter. Wenn Codex // weitere Seiten baut, jeweils den passenden Reiter aktiv setzen. renderShell(w, "active", `
Konto links waehlen oder unter „Postfaecher" anlegen.
`) } // renderShell ist das Grundgeruest im Outlook-2013-Look: // // [ Ribbon: Archiv-Boxen | Import/Export | Postfach-Verwaltung | Mail-Transfer | Einstellungen | Mail-Graveyard ] // [ Archiv-mbox | Nachrichtenliste | Quelle/Target ] func renderShell(w http.ResponseWriter, active, readingPane string) { w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ` Mail-Graveyard
Mail-Graveyard
Nachrichten
%s
`, active, renderInitialReadPane(readingPane)) } func accountsHandler(w http.ResponseWriter, r *http.Request) { accounts, err := ListAccounts() 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, accounts, edit, r.URL.Query().Get("msg"), r.URL.Query().Get("err")) } func accountSaveHandler(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 { 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 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 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 migrateRunHandler(w http.ResponseWriter, r *http.Request) { 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 renderAccountsPage(w http.ResponseWriter, accounts []Account, edit Account, msg, errMsg string) { 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, ` Postfach-Verwaltung - Mail-Graveyard
Mail-Graveyard
`) if msg != "" { fmt.Fprintf(w, `
%s
`, html.EscapeString(msg)) } if errMsg != "" { fmt.Fprintf(w, `
%s
`, html.EscapeString(errMsg)) } fmt.Fprint(w, `
`) } 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(a.DstUser), html.EscapeString(a.MboxDir), status, html.EscapeString(a.Name), html.EscapeString(a.Name), html.EscapeString(a.Name)) } fmt.Fprint(w, `
NameQuelleZielArchivStatus
%s%s%s%s%s
`) } func renderAccountForm(w http.ResponseWriter, a Account) { fmt.Fprintf(w, `
Quelle
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), html.EscapeString(a.DstHost), a.DstPort, securitySelect("dst_security", a.DstSecurity), html.EscapeString(a.DstUser), html.EscapeString(a.MboxDir), 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 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 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 urlQuery(value string) string { return url.QueryEscape(value) }