Files
archivmail/features/PROJ-88-fix-sichtbarkeit-delivered-to.md

10 KiB

PROJ-88: Fix Mail-Sichtbarkeit für Rolle "user" bei Zustellung ohne Adresse in From/To/CC (Delivered-To) + strikter Adress-Match

Status: In Review

Created: 2026-10-19 Last Updated: 2026-10-19

Dependencies

  • Requires: PROJ-54 (Index-seitige User-Filterung via AnyAddress), PROJ-50 (cc_addr/bcc_addr), PROJ-21 (Tenant-Scope)
  • Berührt: PROJ-55/PROJ-63 (Tenant-Isolation), PROJ-70 (IMAP-Rückholung)
  • Ergänzt: PROJ-87 (manuelle Zusatzadressen, bleibt separat)

Hintergrund / Bug

Ein User der Rolle user sieht nur Mails, in denen seine Login-Adresse in From/To/CC/BCC steht. Mails, die per Alias, Verteiler, BCC oder Weiterleitung im Postfach landen, tragen seine Adresse nur im Zustellweg (Delivered-To, X-Original-To, Envelope-To) und sind unsichtbar.

Beobachtet 2026-10-15, Tenant 1, User 19, IMAP-Konto 1: 2 von 22 Mails des Tages archiviert und indexiert, aber unsichtbar. Beide tragen die Login-Adresse exakt in Delivered-To (und den Received-For-Klauseln), in From/To/CC/Reply-To/Sender nicht. Ein Bcc-Header existiert nicht.

Zusätzlicher Befund: escapeManticoreMatch maskiert . und - nicht und setzt den Match nicht in Anführungszeichen. Eine Adresse wird dadurch vermutlich als UND-Verknüpfung einzelner Wörter (local, domain, tld) gematcht statt als exakte Adresse. Die Liste kann dadurch Metadaten von Mails anderer Adressen zeigen, die der exakte Detailcheck verweigert.

User Stories

  • Als User möchte ich Mails sehen und öffnen, die mir zugestellt wurden, auch wenn meine Adresse nur im Zustellweg steht (Alias, Verteiler, BCC, Weiterleitung).
  • Als User möchte ich nur Mails sehen, die wirklich mir gehören, damit keine fremden Mails durch unscharfes Matching in meiner Liste auftauchen.
  • Als Compliance-Verantwortlicher möchte ich, dass Liste und Detailzugriff dieselbe Regel nutzen, damit nichts sichtbar, aber nicht öffenbar ist.

Acceptance Criteria

  • Der Parser liefert pro Mail die Zustelladressen aus Delivered-To, X-Original-To und Envelope-To. Pro Headername zählt nur das erste (oberste) Vorkommen, also das vom eigenen Mailserver zuletzt gesetzte. Weitere Vorkommen weiter unten (Weiterleitungsketten, vom Absender mitgeschickte Header) zählen nicht.
  • Vertrauensgrenze: Zustelladressen werden nur für Mails aus Pull-Quellen (imap, pop3) ausgewertet, bei denen der eigene Mailserver den Header gesetzt hat. Bei SMTP-Einlieferung und Upload/Import (smtp, Upload, MBOX/EML) sind die Header vom Absender kontrolliert und werden ignoriert (Feld bleibt leer).
  • Adressen werden mit net/mail geparst, lowercase normalisiert; nicht parsbare Werte liefern keine Adresse (fail-closed).
  • Index: neues Textfeld für Zustelladressen (idempotent per ensureColumn, auch in emails_global und emails_tenant_N). Alle Stellen, die MailDocument bauen (IMAP, POP3, SMTP/Upload, Import-CLIs, Reindex, index-pending, fix-subjects), befüllen es.
  • AnyAddress-Match (Rolle user) umfasst From, To, CC, BCC und das neue Zustelladressfeld.
  • AnyAddress-Match ist exakt: Adresse wird als Phrase gematcht (in Anführungszeichen, Sonderzeichen maskiert), nicht als UND einzelner Wörter. Test gegen die Match-Konstruktion mit Adressen mit ., -, + und Teilübereinstimmungen (stefan@a.de darf stefan@b.de nicht treffen).
  • mailBelongsToUser() prüft zusätzlich die Zustelladressen mit exaktem Vergleich. Alle Aufrufer (Detail/Anhänge, Threads, OCR, Export, eDiscovery, IMAP-Rückholung) profitieren ohne weitere Änderung.
  • Listen-/Detailkonsistenz: Jede in der Liste für User X sichtbare Mail ist für X öffenbar und umgekehrt. Test mit Mail, die nur im Zustellheader die Adresse trägt.
  • Tenant-Isolation unverändert: Es werden ausschließlich Mails des Tenants des Users durchsucht; kein Cross-Tenant-Treffer durch das neue Feld.
  • Fail-safe bleibt: leere Login-Adresse → total: 0, hits: [].
  • Bestandsmails: nach archivmail reindex sind die Zustelladressen befüllt. Ohne Reindex verhalten sich Bestandsmails wie bisher (kein Regress).
  • Tests (synthetische Daten): Parser (oberstes vs. weitere Header, ungültige Werte, Groß-/Kleinschreibung), Match-Konstruktion, mailBelongsToUser mit Delivered-To, Rollenfilter.

Edge Cases

  • Mehrere Delivered-To im Header (Weiterleitungskette): nur das oberste zählt; andere Adressen erhalten dadurch keine Sichtbarkeit.
  • Vom Absender gefälschter Delivered-To: steht weiter unten als der eigene und zählt nicht; selbst bei Treffer sieht der Nutzer nur eine Mail mit seiner Adresse im Zustellweg.
  • Geteiltes Postfach/Sammelkonto: Delivered-To trägt die Postfachadresse. Nur User, deren Login-Adresse exakt dieser Adresse entspricht, sehen die Mails.
  • Envelope-To mit mehreren Adressen (Komma-Liste): alle geparsten Adressen zählen.
  • Adresse mit Plus-Tag (user+tag@domain): exakter Vergleich, kein Tag-Stripping.
  • Mails ohne diese Header: Verhalten wie bisher.
  • Parse-Fehler beim Reindex einer Mail: Mail bleibt wie bisher indexiert, Zustellfeld leer.

Technical Requirements

  • Security: exakter Match, Tenant-Isolation, keine Lockerung des Fail-safe; keine Änderung am JWT.
  • Performance: ein zusätzliches Textfeld im Match; Reindex von ~58.000 Mails auf 132 in Wartungsfenster, nicht während Last.
  • Deployment: neues Indexfeld erfordert nach Deploy archivmail reindex auf 132 (Freigabe des Users vor Ausführung einholen).

Tech Design (Solution Architect)

Übersprungen: Bugfix mit klar umrissenem Umfang (Parser-Feld, Indexfeld, Match, mailBelongsToUser).

Implementation Notes (Backend, 2026-10-19)

  • Parser (pkg/mailparser/delivery.go): ParsedMail.DeliveryAddresses = oberstes Vorkommen von Delivered-To, X-Original-To, Envelope-To (net/mail, lowercase, dedupliziert, unparsbar → nichts; ein unparsbares oberstes Vorkommen wird NICHT durch ein tieferes ersetzt). Zugriff nur über TrustedDeliveryAddresses(pm, sourceType), liefert nur für imap/pop3 Werte (IsTrustedDeliverySource).
  • Index: neue Textspalte rcpt_addr (CREATE TABLE + ensureColumn), MailDocument.Rcpt, in IndexSync und UpdateAttachmentText (OCR-REPLACE) mitgeführt.
  • Gemeinsamer Helper storage.Store.DeliveryRcpt(ctx, id, raw, pm) (internal/storage/delivery_rcpt.go), genutzt von allen MailDocument-Buildern (IMAP, POP3, Upload, SMTP-Worker submitToWorker, reindexTenant, reindex, index-pending, fix-subjects, import, archivmail-import). Quelle ist immer emails.source_type aus der DB, nicht der Aufrufkontext. Weicht sha256(raw) von der ID ab (Message-ID-Dedup auf eine andere gespeicherte Kopie), wird die gespeicherte Kopie geparst — Index und Detailcheck werten damit dieselbe Kopie und dieselbe Quelle aus.
  • Match (internal/index/address_match.go): AnyAddress → @(from_addr,to_addr,cc_addr,bcc_addr,rcpt_addr) "<adresse>" als Phrase, Sonderzeichen maskiert. Adresse ohne Buchstabe/Ziffer → leeres Ergebnis (fail-closed). From/To/OwnEmail-Pfade unverändert.
  • Restunschärfe der Phrase: Manticore tokenisiert an @ . - +, daher matcht die Phrase alice@example.com auch x.alice@example.com oder alice.example@com. Deshalb prüft handleSearch für Rolle user jeden Treffer der Seite zusätzlich mit mailBelongsToUser (exakte Regel wie Detailansicht) und verwirft Abweichler; total wird um die verworfenen Treffer der Seite korrigiert (kann bei solchen Randfällen auf späteren Seiten minimal überzählen, es werden aber keine Metadaten fremder Mails ausgeliefert).
  • Detailzugriff: mailBelongsToUser(pm, email, sourceType) (verschoben nach internal/api/mail_ownership.go) prüft zusätzlich die vertrauenswürdigen Zustelladressen exakt. Alle Aufrufer (Detail, Anhang, Raw, PDF/ZIP-Export, eDiscovery, OCR, Threads, IMAP-Rückholung) laufen über s.userOwnsMail(ctx, id, pm, email), das source_type aus der DB holt; Lookup-Fehler → nur Zustellpfad entfällt (nie permissiver als vorher).
  • Nebeneffekt DSGVO-Löschersuchen: nutzt ebenfalls AnyAddress und sucht damit jetzt exakt als Phrase und zusätzlich in rcpt_addr.
  • Tests: pkg/mailparser/delivery_test.go, internal/index/address_match_test.go (Phrase-vs-Wort-UND gegen simulierte Manticore-Tokenisierung, stefan@a.de ≠ stefan@b.de, ./-/+, Injection), internal/api/mail_ownership_test.go (Header, Vertrauensgrenze, Listen-/Detailkonsistenz). Lokal kein Go-Toolchain vorhanden — Build/Tests nicht lokal ausgeführt.
  • Offen: BCC wird vom Parser nicht ausgewertet (bcc_addr bleibt leer wie bisher). source_type ist First-Write-Wins pro Mail: kam eine Mail zuerst per SMTP/Upload, zählen ihre Zustellheader auch bei späterem IMAP-Abruf nicht. Bei Message-ID-Dedup gilt nur der Zustellheader der zuerst gespeicherten Kopie (eine Kopie pro Mail) — weitere Empfänger desselben Verteilers sehen die Mail nur über From/To/CC. Nach Deploy ist archivmail reindex nötig (Freigabe des Users einholen).

Audit-Fixes (2026-10-20)

  • IMAP-Scheduler (internal/imap/scheduler.go): Store-Fehler eines Batches lassen den Cursor last_uid nicht mehr überspringen; Folder-Fehler setzen den Konto-Status auf error und lösen Retry aus. Ein Batch, der 5-mal in Folge nicht speicherbar ist, wird mit ERROR-Log übersprungen, damit der Ordner nicht dauerhaft blockiert.
  • reindex schreibt Mails in jeden Tenant-Index aus emails.tenant_id und email_refs (vorher nur emails.tenant_id).
  • Detail/Anhang/Raw/Export/OCR/Restore/v1 prüfen den Tenant über Store.MailInTenant (emails + email_refs) statt nur emails.tenant_id.
  • Offen: source_type NULL bei 50.113 Altmails (kein Zustellheader-Vertrauen, Lösung über PROJ-87); source_type First-Write-Wins; Message-ID-Dedup nutzt nur Header der ersten Kopie.

QA Test Results

To be added by /qa

Deployment

  • 2026-10-19: Commit e39c1f3 auf 132 deployed, Dienste und Health ok.
  • archivmail reindex auf 132 gelaufen: total=58565, indexed=58565, errors=0. rcpt_addr in emails_global und emails_tenant_1/2/3 vorhanden und befüllt.
  • Manuelle Abnahme durch User im UI: Mail vom 15.10. bei User 19 sichtbar und öffenbar.
  • Offen: formale QA (/qa), Entscheidung zur Nachprüfung pro Suchseite in handleSearch.