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 {