From d69e7afa82f10f8a5d752b2e20c7424f25449007 Mon Sep 17 00:00:00 2001 From: DonVoo Date: Sun, 5 Jul 2026 23:44:53 +0200 Subject: [PATCH] Move target account selection to target pane --- backend/00-router.go | 1 + backend/08-viewer.go | 165 +++++++++++++++++++++++++++++++++---- frontend-js/dist/style.css | 14 ++++ 3 files changed, 164 insertions(+), 16 deletions(-) diff --git a/backend/00-router.go b/backend/00-router.go index 2492605..5842898 100644 --- a/backend/00-router.go +++ b/backend/00-router.go @@ -20,6 +20,7 @@ func RegisterRoutes(mux *http.ServeMux) { // Viewer (lokale mbox betrachten + weiterleiten) mux.HandleFunc("/view", viewerHandler) mux.HandleFunc("/view/message", messageHandler) + mux.HandleFunc("/view/target", targetMessageHandler) mux.HandleFunc("/view/forward", forwardHandler) } diff --git a/backend/08-viewer.go b/backend/08-viewer.go index f8cea4e..82914a5 100644 --- a/backend/08-viewer.go +++ b/backend/08-viewer.go @@ -35,6 +35,7 @@ func viewerHandler(w http.ResponseWriter, r *http.Request) { func messageHandler(w http.ResponseWriter, r *http.Request) { account := r.URL.Query().Get("account") folder := r.URL.Query().Get("folder") + targetAccount := r.URL.Query().Get("target_account") index, err := strconv.Atoi(r.URL.Query().Get("index")) if err != nil { http.Error(w, "bad index", http.StatusBadRequest) @@ -52,10 +53,10 @@ func messageHandler(w http.ResponseWriter, r *http.Request) { } msg, err := mail.ReadMessage(bytes.NewReader(raw)) if err != nil { - renderReadPanePair(w, account, folder, index, raw, "(unlesbar)", "", "", string(raw)) + renderReadPanePair(w, account, folder, targetAccount, index, raw, "(unlesbar)", "", "", string(raw)) return } - renderReadPanePair(w, account, folder, index, raw, + renderReadPanePair(w, account, folder, targetAccount, index, raw, decodeHeader(msg.Header.Get("Subject")), decodeHeader(msg.Header.Get("From")), decodeHeader(msg.Header.Get("Date")), @@ -63,6 +64,29 @@ func messageHandler(w http.ResponseWriter, r *http.Request) { ) } +func targetMessageHandler(w http.ResponseWriter, r *http.Request) { + account := r.URL.Query().Get("account") + folder := r.URL.Query().Get("folder") + targetAccount := r.URL.Query().Get("target_account") + index, err := strconv.Atoi(r.URL.Query().Get("index")) + if err != nil { + http.Error(w, "bad index", http.StatusBadRequest) + return + } + path, err := mboxPath(account, folder) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + raw, err := ReadMboxMessage(path, index) + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprint(w, renderTargetPane(account, folder, targetAccount, index, raw)) +} + func forwardHandler(w http.ResponseWriter, r *http.Request) { // TODO Codex: GET = Weiterleiten-Formular (An/Betreff/Text), POST = // ForwardMessage() aufrufen. NUR hier kommt SMTP zum Einsatz -- der Umzug @@ -71,7 +95,7 @@ func forwardHandler(w http.ResponseWriter, r *http.Request) { func renderViewerTree(w http.ResponseWriter) { w.Header().Set("Content-Type", "text/html; charset=utf-8") - fmt.Fprint(w, `
Backups
`) + fmt.Fprint(w, `
Originale
`) entries, err := os.ReadDir(Cfg.MboxRoot) if err != nil { fmt.Fprintf(w, `
%s
`, html.EscapeString(err.Error())) @@ -88,7 +112,7 @@ func renderViewerTree(w http.ResponseWriter) { continue } found = true - fmt.Fprintf(w, `
%s
`, html.EscapeString(account)) + fmt.Fprintf(w, `
%s
`, html.EscapeString(sourceAccountLabel(account))) for _, path := range mboxes { folder := strings.TrimSuffix(filepath.Base(path), ".mbox") fmt.Fprintf(w, `%s`, @@ -132,7 +156,7 @@ func renderMessageList(w http.ResponseWriter, account, folder, q string) { if from == "" { from = "(ohne Absender)" } - fmt.Fprintf(w, `%s%s%s`, + fmt.Fprintf(w, `%s%s%s`, urlEsc(account), urlEsc(folder), entry.Index, urlEsc(account), urlEsc(folder), entry.Index, html.EscapeString(from), html.EscapeString(subject), html.EscapeString(entry.Date)) } @@ -143,10 +167,10 @@ func renderMessageList(w http.ResponseWriter, account, folder, q string) { } } -func renderReadPanePair(w http.ResponseWriter, account, folder string, index int, raw []byte, subject, from, date, body string) { +func renderReadPanePair(w http.ResponseWriter, account, folder, targetAccount string, index int, raw []byte, subject, from, date, body string) { w.Header().Set("Content-Type", "text/html; charset=utf-8") renderReadPane(w, "Original", subject, from, date, body) - targetHTML := renderTargetPane(account, folder, index, raw) + targetHTML := renderTargetPane(account, folder, targetAccount, index, raw) fmt.Fprintf(w, `
%s
`, targetHTML) } @@ -158,31 +182,40 @@ func renderReadPane(w io.Writer, title, subject, from, date, body string) { html.EscapeString(title), html.EscapeString(subject), html.EscapeString(from), html.EscapeString(date), html.EscapeString(body)) } -func renderTargetPane(account, folder string, index int, raw []byte) string { - match, ok := findForwardedCopy(account, folder, index, raw) +func renderTargetPane(account, folder, targetAccount string, index int, raw []byte) string { + var b strings.Builder + b.WriteString(renderTargetAccountSelect(account, folder, targetAccount, index)) + match, ok := findForwardedCopy(account, targetAccount, raw) if !ok { id := messageIDHeader(raw) if id == "" { id = "(ohne Message-ID)" } - return fmt.Sprintf(`
Zielkopie
Keine lokale Zielkopie gefunden.

Message-ID: %s
`, html.EscapeString(id)) + fmt.Fprintf(&b, `
Keine lokale Zielkopie gefunden.

Message-ID: %s
`, html.EscapeString(id)) + return b.String() } msg, err := mail.ReadMessage(bytes.NewReader(match.Raw)) if err != nil { - var b strings.Builder - renderReadPane(&b, "Zielkopie", "(unlesbar)", match.Location(), "", string(match.Raw)) + renderReadContent(&b, "(unlesbar)", match.Location(), "", string(match.Raw)) return b.String() } - var b strings.Builder - renderReadPane(&b, "Zielkopie: "+match.Location(), + renderReadContent(&b, decodeHeader(msg.Header.Get("Subject")), decodeHeader(msg.Header.Get("From")), - decodeHeader(msg.Header.Get("Date")), + decodeHeader(msg.Header.Get("Date"))+" | "+match.Location(), messageBody(match.Raw), ) return b.String() } +func renderReadContent(w io.Writer, subject, from, date, body string) { + if subject == "" { + subject = "(ohne Betreff)" + } + fmt.Fprintf(w, `

%s

Von: %s
Datum: %s
%s
`, + html.EscapeString(subject), html.EscapeString(from), html.EscapeString(date), html.EscapeString(body)) +} + func mboxPath(account, folder string) (string, error) { if account == "" || folder == "" || strings.Contains(account, "..") || strings.Contains(folder, "..") { return "", fmt.Errorf("ungueltiges Postfach") @@ -238,7 +271,7 @@ func (m forwardedMatch) Location() string { return fmt.Sprintf("%s / %s #%d", m.Account, m.Folder, m.Index) } -func findForwardedCopy(account, folder string, index int, raw []byte) (forwardedMatch, bool) { +func findForwardedCopy(account, targetAccount string, raw []byte) (forwardedMatch, bool) { id := messageIDHeader(raw) if id == "" { return forwardedMatch{}, false @@ -251,6 +284,9 @@ func findForwardedCopy(account, folder string, index int, raw []byte) (forwarded if !entry.IsDir() || entry.Name() == account { continue } + if targetAccount != "" && entry.Name() != targetAccount { + continue + } mboxes, _ := filepath.Glob(filepath.Join(Cfg.MboxRoot, entry.Name(), "*.mbox")) for _, path := range mboxes { msgs, err := ReadMboxList(path) @@ -283,3 +319,100 @@ func messageIDHeader(raw []byte) string { } return strings.TrimSpace(msg.Header.Get("Message-ID")) } + +func renderTargetAccountSelect(account, folder, selected string, index int) string { + accounts := viewerAccounts() + if selected == account { + selected = "" + } + var b strings.Builder + fmt.Fprintf(&b, `
Zielkopie
+ + + +
`) + return b.String() +} + +type viewerAccount struct { + Name string + SourceLabel string + TargetLabel string +} + +func viewerAccounts() []viewerAccount { + labels := accountLabels() + entries, err := os.ReadDir(Cfg.MboxRoot) + if err != nil { + return nil + } + var out []viewerAccount + for _, entry := range entries { + if !entry.IsDir() { + continue + } + mboxes, _ := filepath.Glob(filepath.Join(Cfg.MboxRoot, entry.Name(), "*.mbox")) + if len(mboxes) == 0 { + continue + } + a := labels[entry.Name()] + if a.Name == "" { + a = viewerAccount{Name: entry.Name(), SourceLabel: entry.Name(), TargetLabel: entry.Name()} + } + out = append(out, a) + } + return out +} + +func sourceAccountLabel(mboxDir string) string { + if a := accountLabels()[mboxDir]; a.SourceLabel != "" { + return a.SourceLabel + } + return mboxDir +} + +func accountLabels() map[string]viewerAccount { + out := map[string]viewerAccount{} + if DB == nil { + return out + } + accounts, err := ListAccounts() + if err != nil { + return out + } + for _, a := range accounts { + dir := accountMboxDir(a) + source := a.SrcUser + if source == "" { + source = a.Name + } + target := a.DstUser + if target == "" { + target = a.Name + } + if a.Name != "" { + target = target + " (" + a.Name + ")" + } + out[dir] = viewerAccount{Name: dir, SourceLabel: source, TargetLabel: target} + } + return out +} + +func accountMboxDir(a Account) string { + if strings.TrimSpace(a.MboxDir) != "" { + return a.MboxDir + } + return a.Name +} diff --git a/frontend-js/dist/style.css b/frontend-js/dist/style.css index a8a6ced..78f90ba 100644 --- a/frontend-js/dist/style.css +++ b/frontend-js/dist/style.css @@ -73,6 +73,20 @@ body.ol2013 { font-weight: 600; color: var(--ol-muted); text-transform: none; } +.target-head { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 10px; +} +.target-select-form { margin: 0; min-width: 0; } +.target-select { + width: 100%; + height: 24px; + padding: 2px 6px; + color: var(--ol-text); + background: #fff; +} .searchbar { position: sticky; top: 30px; z-index: 2;