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-ToundEnvelope-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/mailgeparst, lowercase normalisiert; nicht parsbare Werte liefern keine Adresse (fail-closed). - Index: neues Textfeld für Zustelladressen (idempotent per
ensureColumn, auch inemails_globalundemails_tenant_N). Alle Stellen, dieMailDocumentbauen (IMAP, POP3, SMTP/Upload, Import-CLIs, Reindex, index-pending, fix-subjects), befüllen es. AnyAddress-Match (Rolleuser) 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.dedarfstefan@b.denicht 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 reindexsind 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,
mailBelongsToUsermit Delivered-To, Rollenfilter.
Edge Cases
- Mehrere
Delivered-Toim 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-Toträgt die Postfachadresse. Nur User, deren Login-Adresse exakt dieser Adresse entspricht, sehen die Mails. Envelope-Tomit 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 reindexauf 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 vonDelivered-To,X-Original-To,Envelope-To(net/mail, lowercase, dedupliziert, unparsbar → nichts; ein unparsbares oberstes Vorkommen wird NICHT durch ein tieferes ersetzt). Zugriff nur überTrustedDeliveryAddresses(pm, sourceType), liefert nur fürimap/pop3Werte (IsTrustedDeliverySource). - Index: neue Textspalte
rcpt_addr(CREATE TABLE +ensureColumn),MailDocument.Rcpt, inIndexSyncundUpdateAttachmentText(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-WorkersubmitToWorker,reindexTenant, reindex, index-pending, fix-subjects, import, archivmail-import). Quelle ist immeremails.source_typeaus der DB, nicht der Aufrufkontext. Weichtsha256(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 Phrasealice@example.comauchx.alice@example.comoderalice.example@com. Deshalb prüfthandleSearchfür Rolleuserjeden Treffer der Seite zusätzlich mitmailBelongsToUser(exakte Regel wie Detailansicht) und verwirft Abweichler;totalwird 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 nachinternal/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 übers.userOwnsMail(ctx, id, pm, email), dassource_typeaus der DB holt; Lookup-Fehler → nur Zustellpfad entfällt (nie permissiver als vorher). - Nebeneffekt DSGVO-Löschersuchen: nutzt ebenfalls
AnyAddressund sucht damit jetzt exakt als Phrase und zusätzlich inrcpt_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_addrbleibt leer wie bisher).source_typeist 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 istarchivmail reindexnötig (Freigabe des Users einholen).
Audit-Fixes (2026-10-20)
- IMAP-Scheduler (
internal/imap/scheduler.go): Store-Fehler eines Batches lassen den Cursorlast_uidnicht 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. reindexschreibt Mails in jeden Tenant-Index ausemails.tenant_idundemail_refs(vorher nuremails.tenant_id).- Detail/Anhang/Raw/Export/OCR/Restore/v1 prüfen den Tenant über
Store.MailInTenant(emails + email_refs) statt nuremails.tenant_id. - Offen:
source_typeNULL bei 50.113 Altmails (kein Zustellheader-Vertrauen, Lösung über PROJ-87);source_typeFirst-Write-Wins; Message-ID-Dedup nutzt nur Header der ersten Kopie.
QA Test Results
To be added by /qa
Deployment
- 2026-10-19: Commit
e39c1f3auf 132 deployed, Dienste und Health ok. archivmail reindexauf 132 gelaufen: total=58565, indexed=58565, errors=0.rcpt_addrin 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.