Files
archivmail/features/PROJ-87-zusatzadressen-pro-user.md

18 KiB

PROJ-87: Zusatzadressen pro User für die Mail-Sichtbarkeit (Rolle "user")

Status: In Review

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

Dependencies

  • Requires: PROJ-54 (Index-seitige User-Filterung via AnyAddress, Fail-safe bei leerer E-Mail)
  • Requires: PROJ-50 (cc_addr/bcc_addr im Index)
  • Requires: PROJ-21 (Multi-Tenancy, Tenant-Scope)
  • Berührt: PROJ-55/PROJ-63 (Tenant-Isolation, tenantAccessAllowed()-Muster), PROJ-70 (IMAP-Rückholung nutzt mailBelongsToUser)

Hintergrund

Für Rolle user sieht die Liste/Suche nur Mails, in denen die Login-E-Mail (JWT-Session) in From/To/CC/BCC steht (internal/api/search_handlers.go → index.SearchRequest.AnyAddress, internal/index/manticore.go). Der Detailzugriff prüft dasselbe über mailBelongsToUser() (From/To/CC, exakter Adressvergleich).

Mails, die über Alias, Verteiler oder eine abweichende Adresse im Postfach landen (z.B. IMAP-Username eines eigenen Kontos auf anderer Domain als die Login-Adresse), sind für den User unsichtbar. Beobachtet am 2026-10-15: Tenant 1, User 19, IMAP-Konto 1 — 2 von 22 Mails des Tages nicht sichtbar, obwohl archiviert und indexiert.

Entscheidungen (mit Nutzer geklärt)

  • Pflege nur durch Tenant-Admin (domain_admin des eigenen Tenants) und Superadmin. Nie durch den User selbst.
  • Zusatzadressen werden nur manuell gepflegt. Keine automatische Übernahme von IMAP-Konto-Usernames.
  • Dieselbe Adresse darf innerhalb eines Tenants mehreren Usern zugeordnet sein (z.B. Team-Postfach). Tenant-übergreifend verboten.
  • Die erweiterte Adressmenge gilt überall konsistent, nicht nur in Liste/Suche.

User Stories

  • Als Tenant-Admin möchte ich einem User zusätzliche E-Mail-Adressen zuordnen, damit er Mails sieht, die über Alias, Verteiler oder abweichende Adresse bei ihm eingehen.
  • Als Tenant-Admin möchte ich Zusatzadressen eines Users einsehen und wieder entfernen, damit ich die Sichtbarkeit nachvollziehbar steuern und entziehen kann.
  • Als Superadmin möchte ich Zusatzadressen für User beliebiger Tenants verwalten, damit ich Support-Fälle lösen kann.
  • Als User möchte ich Mails über meine Zusatzadressen in Liste, Suche und Detailansicht sehen und öffnen können, damit nichts "sichtbar, aber nicht öffenbar" oder unsichtbar ist.
  • Als Compliance-Verantwortlicher möchte ich jede Änderung an Zusatzadressen im Audit-Log finden, damit Sichtbarkeitsänderungen nachvollziehbar sind (DSGVO/GoBD).

Acceptance Criteria

  • Neue Tabelle für Zusatzadressen pro User (User-ID, Adresse normalisiert lowercase, Tenant-ID, angelegt von, angelegt am); idempotent über initSchema angelegt; Löschen eines Users entfernt seine Zusatzadressen.
  • API zum Auflisten, Hinzufügen und Entfernen von Zusatzadressen eines Users. Zugriff nur für superadmin und domain_admin des Tenants des Ziel-Users; alle anderen Rollen (inkl. user, auditor, domain_auditor) erhalten 403.
  • Tenant-Scope serverseitig erzwungen: Ein domain_admin kann nur User des eigenen Tenants bearbeiten (Muster tenantAccessAllowed()); fremder Tenant liefert 403/404 ohne Information über die Existenz.
  • Validierung: syntaktisch gültige E-Mail-Adresse, trim + lowercase, max. 255 Zeichen, max. 10 Zusatzadressen pro User; ungültig → 400; Duplikat beim selben User → 409 bzw. idempotent ohne Fehler (eine Variante festlegen).
  • Kollision: Dieselbe Adresse (Login-Adresse oder Zusatzadresse) darf im selben Tenant mehreren Usern zugeordnet sein. Eine Adresse, die in einem anderen Tenant als Login- oder Zusatzadresse eines Users vergeben ist, wird abgelehnt (409).
  • Liste/Suche für Rolle user: AnyAddress-Filter berücksichtigt Login-Adresse plus alle Zusatzadressen (ODER-Verknüpfung) und wird weiterhin im Index vor LIMIT/OFFSET angewendet. total und Pagination bleiben korrekt (PROJ-54 nicht regressieren).
  • Alle Stellen mit mailBelongsToUser() nutzen dieselbe Adressmenge: Mail-Detail/Anhänge (search_handlers.go), Threads, OCR, Export, eDiscovery, IMAP-Rückholung (restore_handlers.go).
  • Konsistenz Index ↔ Detailzugriff: Eine in der Liste sichtbare Mail ist für denselben User auch öffenbar. Insbesondere BCC: Der Index matcht bcc_addr, mailBelongsToUser() prüft bisher nur From/To/CC — Abweichung wird beseitigt (Variante in Tech Design festlegen) und per Test abgedeckt.
  • Änderungen wirken sofort ohne erneuten Login des Users (Zusatzadressen werden nicht ins JWT gebrannt) und ohne Reindex für Bestandsmails (Abgleich zur Abfragezeit gegen die bereits indexierten Adressfelder).
  • Entfernen einer Zusatzadresse entzieht die Sichtbarkeit sofort (nächster Request); Deaktivieren/Löschen des Users ebenso.
  • Fail-safe bleibt: Leere/fehlende Login-Adresse und keine Zusatzadressen → total: 0, hits: []. Eine fehlerhafte oder nicht ladbare Zusatzadress-Abfrage darf nie zu "alle Mails sichtbar" führen (Fehler → Zugriff verweigern bzw. nur Login-Adresse, nie ungefiltert).
  • Admin-UI: Im Benutzer-Bereich des Admin-Dashboards kann ein berechtigter Admin Zusatzadressen eines Users anzeigen, hinzufügen und entfernen. Für Rolle user ist die Funktion weder sichtbar noch aufrufbar. shadcn/ui-Komponenten, keine Custom-Versionen.
  • Audit-Log: Hinzufügen und Entfernen werden mit Akteur, Ziel-User, Adresse und Zeitpunkt protokolliert (PostgreSQL + Flat-File wie bestehende Audit-Events).
  • Tests: Unit-/Handler-Tests für Berechtigungen (jede Rolle), Tenant-Isolation (Cross-Tenant-Versuch), Kollisionsregeln, Validierung, Fail-safe-Pfad und Konsistenz Liste ↔ Detail inkl. BCC. Testdaten ausschließlich synthetisch.

Edge Cases

  • Zusatzadresse entspricht der Login-Adresse eines anderen Users im selben Tenant → erlaubt (Team-Postfach); beide sehen die Mails.
  • Zusatzadresse entspricht der Login- oder Zusatzadresse eines Users in einem anderen Tenant → abgelehnt (409), ohne Details über den fremden Tenant preiszugeben.
  • Zusatzadresse mit anderer Schreibweise (Groß-/Kleinschreibung, Leerzeichen) → wird normalisiert; Duplikat-Erkennung auf normalisierter Form.
  • Mail enthält nur die Zusatzadresse in BCC → in Liste sichtbar und öffenbar (siehe Konsistenz-Kriterium).
  • User ohne Tenant (tenant_id NULL): Zusatzadressen nur durch Superadmin; Kollisionsprüfung gegen alle Tenants ohne Tenant-Zuordnung bleibt fail-closed.
  • Rollenwechsel eines Users von user auf eine andere Rolle → Zusatzadressen bleiben gespeichert, wirken aber nicht (nur Rolle user wird adressgefiltert); bei Rückwechsel gelten sie wieder.
  • Bestandsmails vor dem PROJ-50-Reindex mit leerem cc_addr/bcc_addr → erscheinen für CC/BCC-Treffer ggf. erst nach archivmail reindex (bekannte Einschränkung aus PROJ-54, unverändert).
  • Sehr breite Adressen (z.B. Catch-all-artige Einträge, Wildcard-Zeichen *, ?) → nur exakte, vollständige Adressen zulässig; Wildcards und reine Domains werden abgelehnt, damit Manticore-Match-Syntax nicht ausgenutzt werden kann.
  • Gleichzeitiges Anlegen derselben Adresse durch zwei Admins → Datenbank-Constraint verhindert Duplikate pro User; kein Fehler 500.
  • Admin entfernt die letzte Zusatzadresse während der User sucht → nächster Request liefert wieder nur Login-Adresse-Treffer, keine Cache-Reste.

Technical Requirements

  • Security: Tenant-Isolation und DSGVO-Datenminimierung haben Vorrang; Admin-Aktionen authentifiziert und auditiert; Match-Eingaben für Manticore über escapeManticoreMatch (keine Injection über Adressen).
  • Performance: Laden der Zusatzadressen pro Request ohne spürbare Latenz (< 10 ms zusätzlich, ein indizierter Query oder kurzer Cache mit sofortiger Invalidierung bei Änderung); Match-Ausdruck bei bis zu 11 Adressen bleibt performant.
  • Strenge: Index-Match darf nicht großzügiger sein als der exakte Adressvergleich in mailBelongsToUser(), damit keine Mail in der Liste erscheint, die der Detailcheck verweigert (oder umgekehrt).
  • Keine Änderung am JWT-Format.

Tech Design (Solution Architect)

Umfang

Backend und Frontend. Kein neues Paket nötig. Keine Änderung am JWT. Kein Reindex nötig.

A) Bausteine (Übersicht)

Admin-Bereich > Benutzer (bestehende Tabelle)
+-- Zeile pro User (nur Rolle "user" bekommt die Aktion)
|   +-- Aktion "Zusatzadressen" (neu, neben den bestehenden User-Aktionen)
+-- Dialog "Zusatzadressen von <User>" (neu, in UserDialogs)
    +-- Liste der Adressen (Adresse, angelegt von, angelegt am, Entfernen-Knopf)
    +-- Eingabefeld + "Hinzufügen"
    +-- Hinweis: Adresse zählt für Liste, Suche und Detailansicht, wirkt sofort
    +-- Fehleranzeige (ungültig, Limit erreicht, Adresse in anderem Tenant vergeben)

Server
+-- Zusatzadress-Verwaltung (neu): auflisten, hinzufügen, entfernen
+-- Adressmenge eines Users (neu): Login-Adresse + Zusatzadressen
+-- Suche für Rolle "user": nutzt die Adressmenge statt nur der Login-Adresse
+-- Zugriffsprüfung auf einzelne Mails: nutzt dieselbe Adressmenge

B) Daten (Klartext)

Neue Tabelle "Zusatzadressen". Pro Eintrag:

  • Zu welchem User sie gehört
  • Tenant des Users (zur Kollisionsprüfung)
  • Adresse, kleingeschrieben, ohne Leerzeichen, höchstens 255 Zeichen
  • Wer sie angelegt hat und wann

Regeln:

  • Eine Adresse kommt pro User nur einmal vor (Datenbank erzwingt das).
  • Wird ein User gelöscht, verschwinden seine Zusatzadressen mit.
  • Gespeichert in PostgreSQL, wie die übrigen Benutzerdaten. Nichts im Browser, nichts im Token.

C) Entscheidungen und Begründung

  1. Adressmenge wird bei jeder Anfrage frisch aus der Datenbank gelesen, kein Cache. Eine Abfrage über einen Index kostet Bruchteile einer Millisekunde und ist damit ein Bruchteil des Zeitbudgets (10 ms). Dafür greift ein Entzug der Sichtbarkeit sofort, und es gibt keine Cache-Reste, die vergessen werden könnten.
  2. Fail-safe bei Datenbankfehler: Schlägt das Laden der Zusatzadressen fehl, gilt nur die Login-Adresse. Nie mehr Sichtbarkeit als vorher, nie ungefiltert.
  3. Suche: Der bestehende Adressfilter wird von "eine Adresse" auf "bis zu 11 Adressen, ODER-verknüpft" erweitert. Jede Adresse wird weiter als exakte Phrase gesucht (PROJ-88). Er greift im Index vor Seitenaufteilung, total bleibt korrekt.
  4. Zusatzadressen zählen in denselben Feldern wie die Login-Adresse: Absender, Empfänger, CC und, bei vertrauenswürdigen Quellen (IMAP/POP3), Zustelladressen. Eine Zusatzadresse macht die Vertrauensgrenze von PROJ-88 also nicht weicher.
  5. Zugriff auf einzelne Mails: Detail, Anhänge, Threads, OCR, Export, eDiscovery und IMAP-Rückholung prüfen gegen dieselbe Adressmenge wie die Suche. So ist jede sichtbare Mail auch öffenbar.
  6. BCC-Abweichung (Spec-Auftrag): Der Parser liest den Bcc-Header gar nicht, das BCC-Feld im Index ist deshalb in der Praxis leer. Ich lasse BCC in beiden Prüfungen unverändert. Beide Seiten bleiben damit gleich streng. Der Test deckt ab, dass Liste und Detail bei jeder Adresse dasselbe Ergebnis liefern. Falls später BCC indexiert wird, muss die Detailprüfung mitziehen (als Hinweis in die Spec).
  7. Duplikat beim selben User: Antwort 409 "bereits vorhanden". Das ist eindeutig und fürs Audit-Log sauberer als stilles Ignorieren.
  8. Kollision: Geprüft gegen Login-Adressen und Zusatzadressen aller User. Im selben Tenant erlaubt (Team-Postfach). In einem anderen Tenant: 409 mit neutraler Meldung, ohne Hinweis, wem die Adresse gehört. Race (zwei Admins gleichzeitig): die Datenbank-Eindeutigkeit fängt es ab, Antwort 409 statt 500.
  9. Wer darf: Superadmin und Domain-Admin des Tenants des Ziel-Users. Alle anderen Rollen bekommen 403. Ein Domain-Admin sieht für fremde Tenants nur "nicht gefunden". Es gilt das bestehende Muster tenantAccessAllowed().
  10. Nur Rolle "user": Die Aktion erscheint nur für User mit Rolle user. Bei Rollenwechsel bleiben die Adressen gespeichert, wirken aber nur für Rolle user.
  11. Eingabe: Nur vollständige, syntaktisch gültige Adressen. Keine Wildcards, keine reinen Domains. Das schützt vor Missbrauch der Suchsyntax und vor "alles sichtbar"-Einträgen.
  12. Audit-Log: Zwei neue Ereignisse (Zusatzadresse hinzugefügt, entfernt) mit Akteur, Ziel-User, Adresse und Tenant, in PostgreSQL und Flat-File wie die bestehenden Ereignisse.
  13. Admin-Oberfläche: Dialog in den bestehenden User-Dialogen, ausschließlich mit vorhandenen shadcn/ui-Komponenten.

D) Wirkung auf Bestandsmails

Kein Reindex nötig: Die Zusatzadresse wird gegen die bereits indexierten Felder gesucht. Grenze: Bei den rund 50.000 Altmails ohne Quellenangabe (source_type leer) zählen Zustellheader nicht (siehe PROJ-88). Eine Zusatzadresse hilft dort nur, wenn sie in Absender/Empfänger/CC der Mail steht, was bei Weiterleitungen meist der Fall ist (Beispiel Stefan: die ursprüngliche asb-Adresse steht im To).

E) Seiteneffekte auf bestehende Funktionen

  • DSGVO-Löschsuche nutzt denselben Adressfilter mit einer Adresse und bleibt unverändert.
  • Die Nachprüfung pro Suchseite (PROJ-88) und der Detailzugriff nutzen dieselbe Adressmenge.
  • eDiscovery-Export für Rolle user filtert auf die gesamte Adressmenge.

F) Abhängigkeiten

Keine neuen Pakete.

G) Umsetzungsreihenfolge

  1. Backend: Tabelle, Speicherfunktionen, Adressmenge, Verwaltungs-API, Audit-Ereignisse, Rechte- und Kollisionsregeln.
  2. Backend: Suche und alle Zugriffsprüfungen auf die Adressmenge umstellen, Tests für Konsistenz Liste und Detail.
  3. Frontend: Dialog und Aktion im Benutzer-Bereich.
  4. QA: Rollen, Cross-Tenant, Kollisionen, Fail-safe.

Implementation Notes

Backend (2026-10-22)

  • Speicher (internal/userstore/extra_addresses.go): Tabelle user_extra_addresses (user_id FK ON DELETE CASCADE, tenant_id, address, created_by, created_at, UNIQUE(user_id,address), Index auf address, zusätzlich Index lower(users.email)), idempotent aus initSchema. AddExtraAddress läuft in einer Transaktion mit Advisory-Lock pro Adresse (Race über Tenants hinweg) und FOR UPDATE auf den User (10er-Limit). Prüfreihenfolge: eigene Login-Adresse/Duplikat → Limit → Kollision. Kollision prüft den aktuellen Tenant aus users (nicht die gespeicherte tenant_id). Ein User ohne Tenant teilt nie eine Adresse (fail-closed). ON CONFLICT DO NOTHING → 409 statt 500.
  • Adressmenge AddressesForUser: Login-Adresse + Zusatzadressen, lowercase, dedupliziert, ein Query. Zusatzadressen zählen nur bei Rolle user. Inaktiver oder gelöschter User → leere Menge (Zugriff verweigert). API-Helper sessionAddresses/userAddresses (internal/api/mail_ownership.go): bei jedem Request frisch, kein Cache. DB-Fehler → nur Login-Adresse.
  • Suche: index.SearchRequest.AnyAddresses []string, Match @(from_addr,to_addr,cc_addr,bcc_addr,rcpt_addr) ("a" | "b" ...). Bei einer Adresse ist der Ausdruck identisch zu vorher (DSGVO-Pfad mit AnyAddress unverändert). Ein leeres, aber gesetztes (nicht-nil) AnyAddresses → keine Treffer.
  • Zugriff: mailBelongsToAnyAddress(pm, addrs, sourceType); userOwnsMail(ctx, id, pm, addrs []string). Detail, Anhang, Raw, PDF/ZIP-Export, eDiscovery (jetzt zusätzlich Index-Filter vor dem 10.000-Limit), OCR, Threads, IMAP-Rückholung und der Nachcheck in handleSearch nutzen dieselbe Menge. BCC wird seit PROJ-89 auf beiden Seiten geprüft (Entscheidung 6 damit überholt).
  • API (internal/api/user_address_handlers.go, Regeln in user_address_rules.go): GET/POST/DELETE /api/admin/users/{id}/addresses. Hinzufügen nur bei Ziel-Rolle user (400). Auflisten und Entfernen sind auch nach einem Rollenwechsel möglich, damit gespeicherte Adressen weiter sichtbar und löschbar bleiben. Ein domain_admin ohne Tenant erhält 403.
  • Audit: user_address_added / user_address_removed (Detail: user_id, user, address, tenant; TenantID = Tenant des Ziel-Users). Abgelehnte Cross-Tenant-Kollisionen werden mit success=false protokolliert, Cross-Tenant-Zugriffe als user_mgmt denied.
  • Tests: internal/index/address_match_test.go, internal/api/mail_ownership_test.go, internal/api/user_address_rules_test.go (reine Funktionen), internal/userstore/extra_addresses_test.go und internal/api/user_address_api_test.go (brauchen PostgreSQL/Manticore, sonst Skip). Lokal keine Go-Toolchain vorhanden, daher weder Build noch Tests ausgeführt.
  • Offen: Eingebetteter IMAP-Server (Personal-Modus, GetMailsByRecipient) nutzt weiterhin nur die Login-Adresse. Eine spätere Änderung der Login-Adresse (Profil/Admin) wird nicht gegen Zusatzadressen anderer Tenants geprüft. openapi.yaml ist nicht ergänzt.

Frontend (2026-10-22)

  • API (src/lib/api/user_addresses.ts, re-export in index.ts): getUserAddresses, addUserAddress, removeUserAddress über den bestehenden request-Helper. UserAddressApiError löst {error, code} aus dem Fehler-Body und stellt code bereit.
  • Dialog (src/components/admin/UserAddressesDialog.tsx, eigene Datei statt in UserDialogs.tsx): Titel "Zusatzadressen von <login_email>", Hinweis zu Geltung/Sofortwirkung/Maximum, Tabelle (Adresse, angelegt von, angelegt am in de-DE; auf Mobile in der Adresszelle zusammengefasst), Entfernen mit AlertDialog-Bestätigung, Eingabe + "Hinzufügen" (Form-Submit, Enter sendet, nur trim). Fehlercodes werden auf deutsche Meldungen abgebildet, 401/403/404 über den Fehlertext. Lade-, Leer- und Fehlerzustand (mit "Erneut versuchen"), Buttons während Anfragen deaktiviert, Eingabe bei erreichtem Limit gesperrt. Hat der Ziel-User nicht mehr Rolle user, wird das Eingabefeld ausgeblendet, Entfernen bleibt möglich (passend zum Backend).
  • Einbindung: Button "Zusatzadressen" in UsersTab.tsx nur bei u.role === "user" und canManageAddresses (superadmin, domain_admin, admin; src/app/admin/page.tsx). Der Dialog wird pro User mit key neu gemountet, damit kein Zustand eines anderen Users stehen bleibt.
  • Geprüft: npx tsc --noEmit, ESLint auf den geänderten Dateien, npm run build ohne Fehler. Nicht im Browser getestet.

QA Test Results

To be added by /qa

Deployment

To be added by /deploy