diff --git a/backend/05-imap-target.go b/backend/05-imap-target.go index 30531ef..735ff90 100644 --- a/backend/05-imap-target.go +++ b/backend/05-imap-target.go @@ -11,6 +11,7 @@ type TargetMailbox interface { Delim() string // Hierarchie-Trenner des Ziels EnsureFolder(name string) error // anlegen + subscriben, falls fehlt Append(folder string, m RawMessage) error // mit m.Flags und m.InternalDate + Fetch(folder string) ([]RawMessage, error) // Ziel-Audit: geschriebene Mail wiederfinden Close() error } @@ -78,6 +79,13 @@ func (t *imapTarget) Append(folder string, m RawMessage) error { return t.c.appendMessage(folder, m) } +func (t *imapTarget) Fetch(folder string) ([]RawMessage, error) { + if err := t.c.selectMailbox(folder); err != nil { + return nil, err + } + return t.c.fetchAll() +} + func (t *imapTarget) Close() error { return t.c.Close() } diff --git a/backend/08-viewer.go b/backend/08-viewer.go index 94f4e8d..0945958 100644 --- a/backend/08-viewer.go +++ b/backend/08-viewer.go @@ -186,27 +186,37 @@ func renderReadPane(w io.Writer, title, subject, from, date, body string) { func renderTargetPane(account, folder, targetAccount string, index int, raw []byte) string { var b strings.Builder b.WriteString(renderTargetAccountSelect(account, folder, targetAccount, index)) + if match, ok, err := findTargetIMAPCopy(account, targetAccount, raw); ok { + renderTargetMatch(&b, match) + return b.String() + } else if err != nil { + fmt.Fprintf(&b, `
Zielpostfach-Pruefung fehlgeschlagen: %s
`, html.EscapeString(err.Error())) + } match, ok := findForwardedCopy(account, targetAccount, raw) if !ok { id := messageIDHeader(raw) if id == "" { id = "(ohne Message-ID)" } - fmt.Fprintf(&b, `
Keine lokale Zielkopie gefunden.

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

Message-ID: %s
`, html.EscapeString(id)) return b.String() } + renderTargetMatch(&b, match) + return b.String() +} + +func renderTargetMatch(b *strings.Builder, match forwardedMatch) { msg, err := mail.ReadMessage(bytes.NewReader(match.Raw)) if err != nil { - renderReadContent(&b, "(unlesbar)", match.Location(), "", string(match.Raw)) - return b.String() + renderReadContent(b, "(unlesbar)", match.Location(), "", string(match.Raw)) + return } - renderReadContent(&b, + renderReadContent(b, decodeHeader(msg.Header.Get("Subject")), decodeHeader(msg.Header.Get("From")), decodeHeader(msg.Header.Get("Date"))+" | "+match.Location(), messageBody(match.Raw), ) - return b.String() } func renderInitialTargetPane() string { @@ -279,6 +289,63 @@ func (m forwardedMatch) Location() string { return fmt.Sprintf("%s / %s #%d", m.Account, m.Folder, m.Index) } +func findTargetIMAPCopy(account, targetAccount string, raw []byte) (forwardedMatch, bool, error) { + id := messageIDHeader(raw) + if id == "" { + return forwardedMatch{}, false, nil + } + targetUser := strings.TrimSpace(targetAccount) + if targetUser == "" { + source, ok := accountByMboxDir(account) + if !ok { + return forwardedMatch{}, false, nil + } + targetUser = strings.TrimSpace(source.DstUser) + } + for _, a := range accountsForTargetUser(targetUser) { + dst, err := OpenIMAPTarget(a) + if err != nil { + return forwardedMatch{}, false, err + } + match, ok, scanErr := scanTargetForMessage(dst, targetUser, id) + closeErr := dst.Close() + if scanErr != nil { + return forwardedMatch{}, false, scanErr + } + if closeErr != nil { + return forwardedMatch{}, false, closeErr + } + if ok { + return match, true, nil + } + } + return forwardedMatch{}, false, nil +} + +func scanTargetForMessage(dst TargetMailbox, targetUser, messageID string) (forwardedMatch, bool, error) { + folders, err := dst.Folders() + if err != nil { + return forwardedMatch{}, false, err + } + for _, folder := range folders { + msgs, err := dst.Fetch(folder.Name) + if err != nil { + continue + } + for i, msg := range msgs { + if msg.MessageID == messageID || messageIDHeader(msg.Body) == messageID { + return forwardedMatch{ + Account: "Ziel: " + targetUser, + Folder: folder.Name, + Index: i, + Raw: msg.Body, + }, true, nil + } + } + } + return forwardedMatch{}, false, nil +} + func findForwardedCopy(account, targetAccount string, raw []byte) (forwardedMatch, bool) { id := messageIDHeader(raw) if id == "" { @@ -440,6 +507,39 @@ func accountLabels() map[string]viewerAccount { return out } +func accountByMboxDir(mboxDir string) (Account, bool) { + if DB == nil { + return Account{}, false + } + accounts, err := ListAccounts() + if err != nil { + return Account{}, false + } + for _, a := range accounts { + if accountMboxDir(a) == mboxDir { + return a, true + } + } + return Account{}, false +} + +func accountsForTargetUser(targetUser string) []Account { + if DB == nil { + return nil + } + accounts, err := ListAccounts() + if err != nil { + return nil + } + var out []Account + for _, a := range accounts { + if strings.EqualFold(strings.TrimSpace(a.DstUser), targetUser) { + out = append(out, a) + } + } + return out +} + func targetAccountOptions() []targetAccountOption { if DB == nil { return nil diff --git a/frontend-js/dist/style.css b/frontend-js/dist/style.css index 4859c81..b8ffadf 100644 --- a/frontend-js/dist/style.css +++ b/frontend-js/dist/style.css @@ -144,6 +144,7 @@ a.tree-node { display: block; color: inherit; text-decoration: none; } margin: 0; } .ef-empty { color: var(--ol-muted); padding: 24px; } +.audit-note { padding: 10px 16px; border-bottom: 1px solid var(--ol-border); font-size: 12px; } /* Buttons / Formulare (flach, 2013er) */ .btn {