Beide Features wurden auf 192.168.1.131 deployt (Commit 4c92587), inkl. der
zugehörigen QA-Bugfixes (Dry-Run-Adressmatching PROJ-43, days-Clamp PROJ-52).
Deployment-Abschnitte in den Feature-Specs und INDEX.md aktualisiert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
12 KiB
id, title, status, created
| id | title | status | created |
|---|---|---|---|
| PROJ-43 | Automatische Archivierungsregeln (by Domain/Sender) | Deployed | 2026-04-05 |
Kontext
Das SMTP Domain-Routing (PROJ-21 Phase 5) ist bereits implementiert:
tenant_domains-Tabelle ordnet Domains Mandanten zuresolveTenantFromRcpts()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.gobzw. neuesinternal/storage/tenant_routing_rules.go(DB-Schema + ApplyRules-Methode, analogretention_rules.go)internal/smtpd/smtpd.go(Regel-Check nach Save, ergänzt bestehendesresolveTenantFromRcpts()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) — Tabelletenant_routing_rules, CRUD, Matching-EngineResolveTenantByRoutingRules(), Dry-RunDryRunRoutingRule(). Stil analogretention_rules.go. Match-Typen:from_domain,to_domain,from_addr,to_addr. Wildcard-Domains via*.kunde.de(matcht Sub-Domains).internal/storage/storage.go—initTenantRoutingRulesSchema(ctx)in Init-Kette.internal/smtpd/smtpd.go—resolveTenantByRules()neu; inresolveTenant()als Stufe 0 VOR dertenant_domains-Logik (Regeln sind expliziter → Vorrang).internal/imap/importer.go—storeAndIndex(): 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-rulesBody{tenant_id?, match_type, pattern, priority}→201 {"id": <int>}(superadmin musstenant_idsetzen; 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-runBody entweder{rule_id}ODER{match_type, pattern}(+ optionallimit, 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 gegenemails, 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-FunktionengetRoutingRules,createRoutingRule,updateRoutingRule,deleteRoutingRule,dryRunRoutingRule. Nutzt zentralenrequest()-Wrapper auscore.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 ruftdryRunRoutingRule({match_type, pattern})und zeigtmatch_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 peruseAuth("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()ininternal/smtpd/smtpd.go:107VORtenant_domains-Logik;ResolveTenantByRoutingRules()ininternal/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-rulesfunktionieren; ohne Cookie →401. POST als domain_admin ohnetenant_iderzwingt eigenen Tenant (201 {"id":5}). - Frontend Regel-Verwaltung — Komponente
RoutingRulesTab.tsxvorhanden, Datenkontrakt passt zur API. UI visuell nicht separat durchgeklickt. - Priorität: höhere gewinnt — Code-verifiziert.
ListTenantRoutingRulessortiertORDER BY priority DESC, id ASC;ResolveTenantByRoutingRulesnimmt ersten Match → höchste Priorität, bei Gleichstand niedrigste id. Logik korrekt. - [~] Dry-Run — TEILWEISE.
from_domain/to_domainkorrekt (perlbach24.de → 955 bzw. 2962 Treffer, inkl. Wildcard).from_addr/to_addrliefern falsche 0-Treffer → Bug-1.
Sicherheit / Tenant-Isolation / IDOR
- Auth-Bypass: Ohne Cookie →
401auf allen Endpunkten. BESTANDEN. - IDOR Create: domain_admin T1 versucht Regel mit
tenant_id:3→403 {"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:3zu 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
tenantAccessAllowedgeprü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):*.basematcht Sub-Domains UND die Apex-Domain (dokumentiert im Code-Kommentar Zeile 216). Dry-Run*.perlbach24.de= 955 =perlbach24.debestä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 viaSELECT ... WHERE mail_from ILIKE '%support@perlbach24.de%'). - Ursache:
dryRunCondition()ininternal/storage/tenant_routing_rules.go:317-319baut fürfrom_addr/to_addrdie BedingungLOWER(mail_from) LIKE '%<' + pattern + '>%', erwartet also die Winkelklammer-FormName <addr>. Die Spaltemail_fromspeichert 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/5→200 {"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.
Deployment
Deployt: 2026-07-04 auf Produktivserver 192.168.1.131 via update.sh (Commit 4c92587,
zusammen mit PROJ-52 im selben Deploy). Enthält den QA-Bugfix "Bug-1" (Dry-Run für
from_addr/to_addr matcht jetzt bare Adressen statt der falschen <addr>-Form in
internal/storage/tenant_routing_rules.go).
- Backend ✓ läuft (
archivmail, Health/api/health→{"status":"ok","version":"0.9.1"}) - Frontend ✓ läuft (
archivmail-web) - Smoke-Test:
GET /api/admin/routing-rulesohne Auth →401(Endpoint erreichbar, Auth-Gate aktiv). Rollen-/Funktionstest ist QA-Scope, nicht Teil dieses Deploy-Checks. - Kein offener Handoff-Punkt für devops-deploy (Feature hat keinen eigenen Cron-Job).