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