Files
archivmail/features/PROJ-43-archivierungsregeln.md
T
sysopsandClaude Sonnet 5 4c92587b60 fix(PROJ-43): Dry-Run für from_addr/to_addr matcht bare Adressen statt <addr>-Form
dryRunCondition() erwartete faelschlich die Winkelklammer-Form "Name <addr>",
waehrend mail_from/mail_to die Adresse bare speichern - Dry-Run zeigte dadurch
immer 0 Treffer fuer Adress-Regeln, obwohl der Live-Matcher (routeBareAddr)
korrekt matcht. QA-Ergebnisse (Bug-1) in die Feature-Spec uebernommen.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 11:57:21 +02:00

11 KiB

id, title, status, created
id title status created
PROJ-43 Automatische Archivierungsregeln (by Domain/Sender) In Review 2026-04-05

Kontext

Das SMTP Domain-Routing (PROJ-21 Phase 5) ist bereits implementiert:

  • tenant_domains-Tabelle ordnet Domains Mandanten zu
  • resolveTenantFromRcpts() im SMTP-Daemon weist Mails automatisch zu
  • IMAP/POP3-Importer unterstützen ebenfalls TenantID

PROJ-43 erweitert diese Basis um flexiblere Muster-Regeln und eine GUI.

Ziel

Admins können über die Web-Oberfläche Regeln verwalten die über einfache Domain-Zuordnung hinausgehen — z.B. Wildcard-Domains, Absender-Adressen, Betreff-Muster.

User Stories

  • Als Admin möchte ich alle Mails von @kunde.de automatisch dem Mandanten "Kunde GmbH" zuweisen
  • Als Admin möchte ich Mails an archiv@firma.de einem bestimmten Tenant zuordnen

Namenskonflikt (Nachtrag 2026-07-03)

PROJ-51 (Retention-Kategorien) hat bereits eine Tabelle archiving_rules angelegt (internal/storage/retention_rules.go, Spalten condition_type, pattern, priority, retention_days) — andere Bedeutung (Aufbewahrungsfrist statt Tenant-Zuordnung). Um Kollision zu vermeiden, heißt die neue Tabelle für PROJ-43 tenant_routing_rules, nicht archiving_rules wie ursprünglich benannt.

Acceptance Criteria

  • Tabelle tenant_routing_rules (id, tenant_id, match_type [from_domain|to_domain|from_addr|to_addr], pattern, priority, created_at)
  • SMTP-Daemon und IMAP-Import prüfen Regeln nach jedem eingehenden Mail
  • API: CRUD für tenant_routing_rules (Admin only)
  • Frontend: Regel-Verwaltung im Admin-Bereich
  • Priorität: höhere Priorität gewinnt bei mehreren Treffern
  • Dry-Run: zeigt welche bestehenden Mails von einer Regel betroffen wären

Betroffene Dateien

  • internal/storage/storage.go bzw. neues internal/storage/tenant_routing_rules.go (DB-Schema + ApplyRules-Methode, analog retention_rules.go)
  • internal/smtpd/smtpd.go (Regel-Check nach Save, ergänzt bestehendes resolveTenantFromRcpts() aus PROJ-21 Phase 5)
  • internal/imap/importer.go (Regel-Check nach Import)
  • internal/api/rules_handlers.go (neu)
  • src/app/admin/ (Regel-UI)

Implementierungsnotizen (2026-07-03, Status: In Review)

Kein lokaler go build möglich (kein Go-Toolchain im Arbeitsverzeichnis) — Build/Tests laufen separat auf dem Testserver.

Neue/geänderte Dateien

  • internal/storage/tenant_routing_rules.go (NEU) — Tabelle tenant_routing_rules, CRUD, Matching-Engine ResolveTenantByRoutingRules(), Dry-Run DryRunRoutingRule(). Stil analog retention_rules.go. Match-Typen: from_domain, to_domain, from_addr, to_addr. Wildcard-Domains via *.kunde.de (matcht Sub-Domains).
  • internal/storage/storage.goinitTenantRoutingRulesSchema(ctx) in Init-Kette.
  • internal/smtpd/smtpd.goresolveTenantByRules() neu; in resolveTenant() als Stufe 0 VOR der tenant_domains-Logik (Regeln sind expliziter → Vorrang).
  • internal/imap/importer.gostoreAndIndex(): Mail wird jetzt früh geparst, Regel-Match kann die Account-Default-TenantID vor dem Save überschreiben.
  • internal/api/rules_handlers.go (NEU) — CRUD + Dry-Run, authAdmin (domain_admin+), Tenant-Scoping + IDOR-Check (tenantAccessAllowed) bei jedem {id}.
  • internal/api/server.go — Routen registriert.

Tabelle

tenant_routing_rules (id, tenant_id [NOT NULL, FK tenants ON DELETE CASCADE], match_type, pattern, priority, created_at). Höhere priority gewinnt, bei Gleichstand niedrigste id.

API-Endpunkte (alle domain_admin+; superadmin = alle Tenants, domain_admin = eigener)

  • GET /api/admin/routing-rules{ "rules": RoutingRule[] }
  • POST /api/admin/routing-rules Body {tenant_id?, match_type, pattern, priority}201 {"id": <int>} (superadmin muss tenant_id setzen; domain_admin bekommt eigenen Tenant erzwungen)
  • PUT /api/admin/routing-rules/{id} gleicher Body → 200 {"ok": true}
  • DELETE /api/admin/routing-rules/{id}200 {"ok": true}
  • POST /api/admin/routing-rules/dry-run Body entweder {rule_id} ODER {match_type, pattern} (+ optional limit, default 20, max 100) → 200 {match_type, pattern, match_count, sample_limit, sample: [{id, mail_from, mail_to, subject, received_at}]}. Dry-Run ist ILIKE-Näherung gegen emails, LIMIT-begrenzt (kein Vollscan-Timeout); domain_admin sieht nur eigene Tenant-Mails.

RoutingRule-Shape: {id, tenant_id, match_type, pattern, priority, created_at}.

Frontend (2026-07-03, Status: In Review — QA offen)

  • src/lib/api/routing_rules.ts (NEU) — TS-Typen (RoutingRule, RoutingRuleInput, RoutingMatchType, RoutingDryRunResult/Input/Sample) + API-Funktionen getRoutingRules, createRoutingRule, updateRoutingRule, deleteRoutingRule, dryRunRoutingRule. Nutzt zentralen request()-Wrapper aus core.ts.
  • src/lib/api/index.ts — Re-exports der neuen Typen + Funktionen ergänzt.
  • src/components/admin/tabs/RoutingRulesTab.tsx (NEU) — Tab „Routing-Regeln": Tabelle (Prio, Typ, Muster, Angelegt; bei superadmin zusätzlich Tenant-Spalte), CRUD via Dialog (match_type-Select, pattern-Input mit typabhängigem Placeholder, priority-Input, bei superadmin Tenant-Auswahl-Dropdown), Löschen mit Bestätigung. Dry-Run-Button im Dialog ruft dryRunRoutingRule({match_type, pattern}) und zeigt match_count + Stichproben-Tabelle. Prioritäts-Erklärung + Hinweis „keine rückwirkende Umroutung" als Alert. Loading/Error/Empty-States implementiert.
  • src/app/admin/page.tsx — Tab-Trigger + <TabsContent value="routing-rules"> eingebunden; Tab für alle Admin-Seiten-Besucher sichtbar (Seite ist bereits per useAuth("domain_admin") auf domain_admin+ gegated). isSuperAdmin-Prop steuert Tenant-Spalte/-Dropdown; serverseitiges Scoping bleibt maßgeblich.
  • Typecheck: npx tsc --noEmit → sauber (Exit 0).

Offen / Handoff

  • QA gegen Acceptance Criteria (CRUD, Dry-Run, Rollen-Gate) auf Testserver.
  • Bereits archivierte Mails werden NICHT rückwirkend umgeroutet (nur neue Ingests).

QA Test Results

Getestet: 2026-07-04 auf Testserver 192.168.1.132 (Binary v0.9.1, deployt 2026-07-03). Test-Accounts: qa-superadmin (superadmin), qa-da-t1 (domain_admin Tenant 1), qa-da-t3 (domain_admin Tenant 3). Gesamtergebnis: BESTANDEN MIT 1 BUG (Dry-Run für from_addr/to_addr liefert falsche Nullwerte — siehe Bug-1). CRUD, Tenant-Scoping/IDOR und Wildcard-Matching sind grün.

Ergebnis je Acceptance Criterion

  • Tabelle tenant_routing_rules — BESTANDEN. Schema mit Spalten (id, tenant_id NOT NULL FK, match_type, pattern, priority, created_at) vorhanden; CRUD liefert erwartete Shape.
  • SMTP-Daemon + IMAP-Import prüfen Regeln — Code-verifiziert (nicht per Live-Mail). resolveTenantByRules() in internal/smtpd/smtpd.go:107 VOR tenant_domains-Logik; ResolveTenantByRoutingRules() in internal/imap/importer.go:255. Verdrahtung vorhanden. Hinweis: End-to-End-Zuordnung per echtem Mailversand nicht durchgeführt (kein Live-SMTP-Test).
  • API CRUD (Admin only) — BESTANDEN. GET/POST/PUT/DELETE unter /api/admin/routing-rules funktionieren; ohne Cookie → 401. POST als domain_admin ohne tenant_id erzwingt eigenen Tenant (201 {"id":5}).
  • Frontend Regel-Verwaltung — Komponente RoutingRulesTab.tsx vorhanden, Datenkontrakt passt zur API. UI visuell nicht separat durchgeklickt.
  • Priorität: höhere gewinnt — Code-verifiziert. ListTenantRoutingRules sortiert ORDER BY priority DESC, id ASC; ResolveTenantByRoutingRules nimmt ersten Match → höchste Priorität, bei Gleichstand niedrigste id. Logik korrekt.
  • [~] Dry-Run — TEILWEISE. from_domain/to_domain korrekt (perlbach24.de → 955 bzw. 2962 Treffer, inkl. Wildcard). from_addr/to_addr liefern falsche 0-Treffer → Bug-1.

Sicherheit / Tenant-Isolation / IDOR

  • Auth-Bypass: Ohne Cookie → 401 auf allen Endpunkten. BESTANDEN.
  • IDOR Create: domain_admin T1 versucht Regel mit tenant_id:3403 {"error":"tenant_id required and must match your scope"}. BESTANDEN.
  • IDOR Update: T1 versucht PUT auf Regel id 2 (gehört Tenant 3) → 403 {"error":"forbidden"}, Regel unverändert. BESTANDEN.
  • IDOR Delete: T1 versucht DELETE Regel id 2 (Tenant 3) → 403 {"error":"forbidden"}, Regel bleibt bestehen. BESTANDEN.
  • Tenant-Move via Body: T1 versucht eigene Regel id 5 per PUT tenant_id:3 zu verschieben → 403 {"error":"tenant_id must match your scope"}, Regel bleibt Tenant 1. BESTANDEN.
  • Listing-Scope: domain_admin T1 sieht nur Tenant-1-Regeln, T3 nur Tenant-3; superadmin alle. Kein Cross-Tenant-Leck. BESTANDEN. (Deckt die PROJ-61-Bugklasse ab: Tenant-Scope wird zusätzlich zum Rollen-Check via tenantAccessAllowed geprüft.)
  • SQL-Injection Dry-Run: Pattern ' OR 1=1 --match_count:0, keine Fehler, keine Anomalie im Log. Parametrisierte Query ($1), sauber. BESTANDEN.

Wildcard-Matching

  • domainMatches() (tenant_routing_rules.go:217): *.base matcht Sub-Domains UND die Apex-Domain (dokumentiert im Code-Kommentar Zeile 216). Dry-Run *.perlbach24.de = 955 = perlbach24.de bestätigt dieses beabsichtigte Verhalten. BESTANDEN (Spec "matcht Sub-Domains" wird als "Sub-Domains + Apex" umgesetzt — konsistent Engine↔Dry-Run).

Offene Bugs

Bug-1 — Dry-Run für from_addr/to_addr liefert falsche Nullwerte. Severity: MEDIUM. Priorität: Hoch (Kernfunktion des AC "Dry-Run" für 2 von 4 Match-Typen unbrauchbar).

  • Repro: POST /api/admin/routing-rules/dry-run {"match_type":"from_addr","pattern": "support@perlbach24.de"}match_count:0, obwohl 955 Mails exakt von dieser Adresse existieren (bestätigt via SELECT ... WHERE mail_from ILIKE '%support@perlbach24.de%').
  • Ursache: dryRunCondition() in internal/storage/tenant_routing_rules.go:317-319 baut für from_addr/to_addr die Bedingung LOWER(mail_from) LIKE '%<' + pattern + '>%', erwartet also die Winkelklammer-Form Name <addr>. Die Spalte mail_from speichert die Adresse aber bare (support@perlbach24.de, ohne <>), daher matcht das Pattern nie. Der Live-Matcher (routeBareAddr, Zeile 205) normalisiert korrekt und würde matchen → Dry-Run und echte Regel-Anwendung sind inkonsistent. Admin sieht "0 betroffen" und konfiguriert Adress-Regeln im Blindflug.
  • Fix-Richtung (für Backend Developer, nicht von QA umgesetzt): Bedingung so bauen, dass beide Speicherformen abgedeckt sind, z.B. Match auf bare Adresse (LOWER(mail_from) LIKE '%'||p||'%' bzw. genauer gegen @-Grenze) statt harte <...>-Umklammerung.

Testdaten-Hygiene

  • Die für den Test angelegte Regel id 5 (Tenant 1) wurde nach dem Test wieder gelöscht (DELETE /api/admin/routing-rules/5200 {"ok":true}). Vorbestehende Regeln id 1-4 (aus früherem QA-Lauf) unangetastet gelassen.
  • Passwörter der qa-*-Accounts für den Test gesetzt (siehe PROJ-52 QA-Hinweis). Kein Produktivserver (131) berührt.