DSGVO Art. 9 — Compliance-Gap im Mail-Connect-Flow geschlossen (Hans-Müller-DSB
hat den Gap für Gmail/iCloud/GMX identifiziert, schon vor Outlook-OAuth-Pflicht):
- Schema: mail_connections.consent_at + consent_version + consent_ip_address;
neue consent_logs-Tabelle für Audit (grant + revoke append-only)
- Endpoints:
- POST /api/mail-connections/consent (Bulk-Array für Re-Consent, partial-fail
wirft sofort = DSGVO-sicher gegen silent-skip fremder IDs)
- POST /api/mail-connections/:id mit consent-gate (412 wenn consentVersion fehlt)
- DELETE /api/mail-connections/:id mit Widerruf-Log (OAuth-Token-Revoke als
TODO für mo Phase 2)
- GET /api/mail-connections/pending-consent — listet Bestands-Connections
mit consent_at=NULL für Re-Consent-Modal
- Account-Lösch-Bug fix: deleteAllMailConnections() war in user/delete nicht
eingebunden — Verbindungen blieben als Waisen
- Frontend:
- ConnectMailSheet: neuer Consent-Step VOR Provider-Grid (view-Machine
consent → grid → form), exakter Hans-Müller-Wortlaut für Art. 9 Abs. 2
lit. a Einwilligung
- MailConsentReminderSheet: Re-Consent-Modal beim App-Open für Bestands-User
- Stores mailConsent + mailConnectDraft (letzterer fixt Bug: Email/Provider
ging verloren wenn User Browser für App-Pw-Generierung öffnete)
- 12 neue i18n-Keys mail.consent.* in DE + EN
- Versionierter Consent-Text: art9-mail-v1-2026-05-13 (Bump bei Text-Änderung
triggert Re-Consent für alle)
Outlook-OAuth Schema (Phase 0 — additiv, Endpoints kommen später):
- mail_connections: auth_method (default 'app_password' → keine Bestands-
Connection bricht), oauth_access_token, oauth_refresh_token,
oauth_token_expiry, oauth_scope
- Encryption via bestehendes server/utils/crypto.ts (AES-256-GCM, Key aus
Infisical)
- Plan-Doc backend/docs/mail-outlook-oauth-plan.md (mo)
- DSB-Review backend/docs/mail-outlook-oauth-dsgvo-review.md (Hans-Müller):
MS als Sub-AV via DPA Sep 2025, EU Data Boundary seit Feb 2025; 5 Pflicht-
Aufgaben + Anwalts-Klärung zu DPA-Anspruch ohne MS-Lizenz
Profile — Cooldown-Pattern-Analysis als Collapsible:
- CooldownPatternAnalysis: 24h-Uhrzeit-Heatmap, Mo–So-Wochentag-Histogramm,
Top-5-Reason-Wortcloud mit Stop-Words-Filter, Cancel-Rate-Anzeige
- DiGA-relevant: NLP läuft client-side, reason-Texte verlassen das Device
nicht (gut für DSB-Akte)
- useProfileData: useCooldownHistoryFull (limit=100) für Pattern-Analyse
- Neutral formuliert, kein Stigma, alle Headings als Frage
Plan-Docs (kein Code):
- backend/docs/mail-custom-keywords-plan.md — Pro/Legend Custom-Keyword-Filter
(3.25 PT MVP, user-scoped, Body-Match in Phase 2)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
351 lines
16 KiB
Markdown
351 lines
16 KiB
Markdown
# Mail Custom Keywords — Architektur-Plan
|
||
|
||
**Status:** Plan (kein Code, kein Schema-Commit)
|
||
**Datum:** 2026-05-13
|
||
**Autor:** Mo (Mail-Architektur-Agent)
|
||
**Scope:** Pro + Legend User
|
||
|
||
---
|
||
|
||
## 1. Use-Case + Motivation
|
||
|
||
Rebreak filtert Gambling-Mails heute mit zwei Mechanismen:
|
||
|
||
1. `GAMBLING_KEYWORDS` — statische Liste in `server/utils/gambling-keywords.mjs` (single-source-of-truth)
|
||
2. Domain-Blocklist — global (Pro/Legend) oder kuratierter Stub (Free)
|
||
|
||
Der blinde Fleck: personalisierte Gambling-Kommunikation. Ein Anbieter der User persönlich mit Promotions wie "Dein VIP-Bonus wartet, Chahine" oder "Tipico Oktoberfest-Wette exklusiv" anschreibt, umgeht eine generische Keyword-Liste. User kennt seinen eigenen Spam-Pattern besser als wir.
|
||
|
||
Custom Keywords sind kein Convenience-Feature — sie sind eine direkte Antwort auf Sucht-Psychologie: Anbieter personalisieren aggressiv. Der User bekommt damit ein Werkzeug zurück, das auf seine konkrete Situation zugeschnitten ist.
|
||
|
||
**Feature-Gate:** Pro + Legend. Free bleibt bei statischer Liste (Motivation zum Upgrade).
|
||
|
||
---
|
||
|
||
## 2. Architektur-Vorschlag
|
||
|
||
### 2.1 Aktueller Filter-Pfad (Ist-Zustand)
|
||
|
||
```
|
||
IMAP EXISTS-Event
|
||
|
|
||
v
|
||
imap-idle/index.mjs
|
||
triggerScan(conn) → POST /api/mail/scan-internal
|
||
|
|
||
v
|
||
scan-internal.post.ts
|
||
fetchAll(envelope) ← nur Header, kein Body
|
||
haystack = senderEmail + subject
|
||
GAMBLING_KEYWORDS.some(kw => haystack.includes(kw)) ← statisch
|
||
blockedDomainSet.has(senderDomain) ← DB-Lookup
|
||
|
|
||
v
|
||
messageDelete + insertMailBlocked
|
||
```
|
||
|
||
Wichtig: Der Daemon (`imap-idle`) triggert nur `scan-internal`. Die eigentliche Matching-Logik liegt komplett in `scan-internal.post.ts` (und identisch in `scan.post.ts`). Der Daemon selbst macht kein Matching.
|
||
|
||
### 2.2 Ziel-Architektur (Soll-Zustand)
|
||
|
||
```
|
||
IMAP EXISTS-Event
|
||
|
|
||
v
|
||
imap-idle/index.mjs
|
||
triggerScan(conn) → POST /api/mail/scan-internal
|
||
|
|
||
v
|
||
scan-internal.post.ts
|
||
fetchAll(envelope) ← weiterhin nur Header (kein Body-Fetch)
|
||
haystack = senderEmail + subject
|
||
[1] GAMBLING_KEYWORDS.some(kw => haystack.includes(kw)) ← statisch, wie bisher
|
||
[2] customKeywords.some(kw => haystack.includes(kw)) ← NEU: user-spezifisch
|
||
[3] blockedDomainSet.has(senderDomain) ← wie bisher
|
||
|
|
||
v
|
||
messageDelete + insertMailBlocked (action="deleted_custom_keyword" wenn [2] matched)
|
||
```
|
||
|
||
Der Custom-Keyword-Match erfolgt **im selben Scan-Loop** wie der statische Match. Kein separater IMAP-Fetch, kein extra Netzwerk-Hop. Die Keywords werden **einmalig pro Scan-Call** aus der DB geladen (nicht pro Mail).
|
||
|
||
### 2.3 Keyword-Laden: Wann und Wie
|
||
|
||
```
|
||
pro Scan-Call (nicht pro Mail):
|
||
getUserCustomKeywords(userId) → string[] aus DB
|
||
compiledRegex = buildKeywordRegex(keywords) ← einmal pro Scan
|
||
|
||
pro Mail im Loop:
|
||
customMatch = compiledRegex.test(haystack) ← regex.test() ist O(n) auf haystack-length
|
||
```
|
||
|
||
Die Keywords werden als **kompiliertes regex-OR** ausgeführt, nicht als `.some()` + `.includes()` Chain. Das ist relevant sobald ein User 10+ Keywords hat.
|
||
|
||
---
|
||
|
||
## 3. Schema-Spec
|
||
|
||
### 3.1 Neue Tabelle: `user_mail_keywords`
|
||
|
||
Separate Tabelle, keine Spalte in `mail_connections`. Begründung:
|
||
|
||
- Keywords sind user-scoped, nicht connection-scoped. Ein User mit drei Mail-Accounts (Legend) will dasselbe Keyword gegen alle drei prüfen.
|
||
- Erleichtert Downgrade-Handling: Tabelle bleibt befüllt, wird nur ignoriert.
|
||
- Saubere DB-Normalisierung.
|
||
|
||
```
|
||
Tabelle: rebreak.user_mail_keywords
|
||
|
||
id UUID PK default(uuid())
|
||
userId UUID FK → rebreak.profiles(id) ON DELETE CASCADE
|
||
keyword TEXT NOT NULL
|
||
matchScope TEXT NOT NULL -- 'subject_sender' | 'body' (siehe 3.2)
|
||
createdAt TIMESTAMP NOT NULL DEFAULT NOW()
|
||
|
||
INDEX: (userId)
|
||
UNIQUE: (userId, keyword) -- kein Duplikat pro User
|
||
```
|
||
|
||
**Kein `caseSensitive`-Flag.** Matching ist immer case-insensitive (`.toLowerCase()` auf beiden Seiten). Deutsche Umlaute: JavaScript `.toLowerCase()` behandelt ä/ö/ü/ß korrekt in V8, kein Extra-Handling nötig.
|
||
|
||
**Kein `matchType` mit subject/sender/body einzeln wählbar.** Stattdessen zwei Scopes (Details in 3.2).
|
||
|
||
### 3.2 matchScope statt matchType
|
||
|
||
Ursprüngliche Idee: User wählt ob subject, sender oder body gematcht wird. Problem: drei getrennte Felder erhöhen UI-Komplexität deutlich und body-Match braucht separaten IMAP-FETCH (teuer). Empfehlung: zwei Scopes.
|
||
|
||
| Scope | Was wird gematcht | IMAP-Fetch nötig |
|
||
|---|---|---|
|
||
| `subject_sender` | Subject + Sender-Email + Sender-Name | Nein (envelope reicht) |
|
||
| `body` | Gesamter Mail-Body (text/plain) | Ja (separater FETCH TEXT) |
|
||
|
||
`body` ist **Legend-only**. Pro bekommt nur `subject_sender`. Begründung: Body-Fetch pro Mail erhöht IMAP-Traffic und Latenz signifikant (Details in Abschnitt 6).
|
||
|
||
Default-Scope wenn User nichts angibt: `subject_sender`.
|
||
|
||
### 3.3 Limits pro Plan
|
||
|
||
| Plan | Max Keywords | Scopes verfügbar |
|
||
|---|---|---|
|
||
| free | 0 (Feature gesperrt) | — |
|
||
| pro | 10 | `subject_sender` only |
|
||
| legend | 50 | `subject_sender` + `body` |
|
||
|
||
Die Limits kommen als neue Felder in `PLAN_LIMITS` in `plan-features.ts`:
|
||
|
||
```
|
||
customKeywords: number // 0 | 10 | 50
|
||
customKeywordBodyMatch: boolean // false | false | true
|
||
```
|
||
|
||
**Eskalation an rebreak-backend:** Schema-Migration für `user_mail_keywords` + neuer Index. PLAN_LIMITS-Erweiterung in `plan-features.ts` ist eigenständig, kein Schema-Change.
|
||
|
||
---
|
||
|
||
## 4. Tier-Gating
|
||
|
||
### 4.1 Wo wird gegated
|
||
|
||
**Doppelt gegated:**
|
||
|
||
1. **Endpoint `POST /api/mail/keywords`** (neu, anlegen/ändern/löschen von Keywords): Prüft Plan beim Schreiben. Free bekommt 403 mit `error: "plan_limit"`. Pro darf maximal 10 Keywords anlegen, Legend 50.
|
||
|
||
2. **`scan-internal.post.ts` + `scan.post.ts`**: Lädt Keywords nur wenn `limits.customKeywords > 0`. Bei Free: `getUserCustomKeywords()` wird gar nicht aufgerufen — kein unnötiger DB-Round-Trip.
|
||
|
||
Der IDLE-Daemon selbst (`imap-idle/index.mjs`) macht keinen Tier-Check — er triggert nur den Scan. Der Tier-Check bleibt in `scan-internal.post.ts`, analog zum bestehenden `includeGlobal`-Pattern.
|
||
|
||
### 4.2 Downgrade-Handling
|
||
|
||
**Keywords werden nicht gelöscht bei Downgrade.** Sie werden pausiert durch den Plan-Check beim Scan. Konkret: `getUserCustomKeywords()` gibt bei Free immer `[]` zurück (early-return wenn `limits.customKeywords === 0`). Die Rows in `user_mail_keywords` bleiben erhalten.
|
||
|
||
Bei Re-Upgrade auf Pro/Legend sind Keywords sofort wieder aktiv — ohne dass User sie neu eingeben muss. Das ist UX-kritisch: User der downgraded wegen Kosten und dann upgraded will nicht neu konfigurieren.
|
||
|
||
Wenn Pro-User mehr als 10 Keywords hatte und auf Legend upgraded: alle Keywords aktiv (50er-Limit greift). Umgekehrt (Legend → Pro): nur die ersten 10 nach `createdAt ASC` werden genutzt, der Rest ruht. Im UI deutlich kommunizieren welche Keywords aktiv sind.
|
||
|
||
### 4.3 Plan-Check beim Schreiben
|
||
|
||
```
|
||
POST /api/mail/keywords (Keyword anlegen)
|
||
→ getPlanLimits(profile.plan)
|
||
→ if limits.customKeywords === 0: 403 plan_limit
|
||
→ count existing keywords für userId
|
||
→ if count >= limits.customKeywords: 403 keyword_limit
|
||
→ INSERT
|
||
```
|
||
|
||
---
|
||
|
||
## 5. UX-Anforderungen (für rebreak-native-ui)
|
||
|
||
Diese Section ist für den UI-Agent (`rebreak-ui`) — nicht für Mo's Scope. Hier nur die Spezifikation.
|
||
|
||
### 5.1 Platzierung
|
||
|
||
Keywords-Management gehört in die Mail-Settings, als eigener Bereich unterhalb der Account-Cards. Kein separater Menü-Punkt. Begründung: User kommt in Mail-Tab wenn er Mail-Schutz konfigurieren will — Keywords sind Teil desselben mentalen Modells.
|
||
|
||
Vorschlag Hierarchie:
|
||
```
|
||
Mail-Tab
|
||
└── [Mail-Accounts] (bestehend)
|
||
└── [Keyword-Filter] (neu, Pro/Legend-Badge)
|
||
└── Liste der aktiven Keywords (Tags)
|
||
└── "+ Keyword hinzufügen" Button
|
||
```
|
||
|
||
### 5.2 Eingabe-Pattern
|
||
|
||
Tag-Input-Muster: User gibt Text ein, tippt "Hinzufügen" oder Return, Keyword erscheint als Tag in der Liste. Kein Freitext-Textarea. Jedes Keyword einzeln editierbar/löschbar.
|
||
|
||
**Kein matchScope-Picker für Pro-User** (haben nur `subject_sender` sowieso). Bei Legend: optionaler Toggle "Auch Mail-Body durchsuchen" pro Keyword.
|
||
|
||
### 5.3 Validation im UI (vor API-Call)
|
||
|
||
- Min-Länge: 4 Zeichen (False-Positive-Schutz, Details in Abschnitt 7)
|
||
- Max-Länge: 100 Zeichen
|
||
- Keine Sonderzeichen die regex-Syntax brechen (escapen auf Server-Seite, UI gibt Warnung)
|
||
- Duplikate: Client-seitiger Check gegen existierende Tags
|
||
|
||
### 5.4 Feedback wenn Keyword Treffer erzielt
|
||
|
||
Im Activity-Log (bestehende `MailActivityLog`-Komponente) taucht eine gelöschte Mail mit Reason "Dein Keyword: Tipico Bonus" auf, nicht nur "Gambling-Erkennung". User sieht welche seiner Keywords greifen. Das ist motivational — User merkt dass das Feature funktioniert.
|
||
|
||
Dafür: `action`-Feld in `MailBlocked` bekommt einen neuen Wert `"deleted_custom_keyword"` plus ein optionales `matchedKeyword`-Feld (max 100 Zeichen). Eskalation an rebreak-backend für Schema-Erweiterung von `MailBlocked`.
|
||
|
||
---
|
||
|
||
## 6. DSGVO-Aspekte
|
||
|
||
### 6.1 Klassifikation der Keywords
|
||
|
||
User-eingegebene Keywords ("Tipico Konto", "Wiesn-Wette") sind selbsteingegebene Daten des Users zu eigenem Schutz. Das ist Art. 6 Abs. 1 lit. b DSGVO (Vertrag) — kein separater DSB-Review nötig. Keine Art. 9-Klassifikation (keine Gesundheitsdaten, nur selbstgewählte Filterbegriffe).
|
||
|
||
**Einschränkung:** Keywords können indirekt sensitive Informationen enthalten ("Mein Konto bei Lottoland" als Keyword). Das ist vertretbar: User gibt diese Daten freiwillig und zu eigenem Zweck ein. Datenminimierung ist gegeben — wir speichern nur was User eingibt.
|
||
|
||
### 6.2 Verschlüsselung at-rest
|
||
|
||
Keywords werden **nicht** AES-verschlüsselt gespeichert. Begründung: Keywords sind keine Credentials. Sie sind konfigurierter Filter-Input. AES-Verschlüsselung würde DB-Level-Queries (LIKE, Index-Nutzung) unmöglich machen und Mehrwert ist minimal bei selbsteingegebenen Filterwörtern.
|
||
|
||
Falls Hans-Müller (DSB) anderes bewertet: Encryption möglich auf Spaltenebene, aber dann kein DB-Index mehr auf `keyword` — Performance-Implikation.
|
||
|
||
### 6.3 Audit-Log
|
||
|
||
Kein separates Audit-Log für Keyword-Änderungen. `createdAt` genügt für Datenminimierung. Änderungen (Delete + Re-Insert) hinterlassen keinen History-Trail — das ist DSGVO-konform (weniger ist mehr).
|
||
|
||
### 6.4 Datenlöschung
|
||
|
||
Bei Account-Löschung: `ON DELETE CASCADE` über `userId` FK. Keine manuelle Cleanup-Logik nötig.
|
||
|
||
**Eskalation an hans-mueller:** DSB-Review für `matchedKeyword`-Feld in `MailBlocked` (speichern wir das Keyword das zur Löschung geführt hat). Das könnte als Log personenbezogener Filter-Entscheidungen gewertet werden. Alternativ: `matchedKeyword` weglassen, nur `action="deleted_custom_keyword"` ohne welches Keyword matched hat. Letzteres ist sicherer.
|
||
|
||
---
|
||
|
||
## 7. Performance + False-Positive-Risiken
|
||
|
||
### 7.1 Performance-Analyse
|
||
|
||
**Normaler Fall (Pro, 10 Keywords, subject_sender):**
|
||
|
||
```
|
||
Pro Scan-Call:
|
||
getUserCustomKeywords() → 1 DB-Query, ~1ms, gibt 10 Strings zurück
|
||
buildKeywordRegex(10 keywords) → kompiliert zu /keyword1|keyword2|.../i
|
||
einmal pro Scan-Call, nicht pro Mail
|
||
|
||
Pro Mail im Loop:
|
||
regex.test(haystack) → O(len(haystack)) ≈ O(200 Zeichen) → < 0.01ms
|
||
```
|
||
|
||
Selbst bei 200 Mails pro Scan (SCAN_LIMIT): 200 × 0.01ms = 2ms. Nicht messbar. Kein Performance-Problem für `subject_sender`.
|
||
|
||
**Legend, 50 Keywords, body-Match:**
|
||
|
||
Body-Match ist fundamental anders. Der aktuelle IMAP-Fetch lädt nur `envelope` (Header). Für Body braucht es:
|
||
|
||
```
|
||
await imap.fetchAll(range, { envelope: true, bodyParts: ['TEXT'] })
|
||
```
|
||
|
||
Das erhöht:
|
||
- IMAP-Traffic: Body kann 10–500 KB pro Mail sein
|
||
- Latenz: Provider drosseln große FETCH-Requests (Gmail: ~10 concurrent)
|
||
- Memory: 200 Mails × ∅ 50 KB = 10 MB pro Scan-Session
|
||
|
||
**Empfehlung für body-Match:** Nicht im Batch-Fetch. Stattdessen: erst `envelope`-Scan wie bisher. Wenn envelope-Match (statisch oder subject_sender-keyword) NICHT anschlägt UND User hat body-keywords: dann lazy FETCH TEXT für diese Mail einzeln, `imap.fetchOne()`. Das hält den Common-Path (header-only) schnell.
|
||
|
||
Alternativ: body-scope aus MVP raushalten, erst in Phase 2. Der subject_sender-scope deckt 90% der Fälle ab — Gambling-Anbieter setzen das Wichtigste ins Subject.
|
||
|
||
### 7.2 False-Positive-Risiken
|
||
|
||
**Kernproblem:** Substring-Match ist breit. "spiel" matcht "Spielzeug-Newsletter", "wettkampf" matcht nicht (Whitelist), aber "sport" würde "Sport Bild" treffen.
|
||
|
||
**Maßnahmen:**
|
||
|
||
| Maßnahme | Umsetzung | Schutz-Level |
|
||
|---|---|---|
|
||
| Min-Länge 4 Zeichen | Server-Validation beim Anlegen | Blockt "ca", "bet" als Standalone |
|
||
| Existierende GAMBLING_WHITELIST | Wird vor custom-keyword-check angewandt | Schützt "wetter", "wettkampf" etc. |
|
||
| User trägt Verantwortung | Terms of Service + UI-Hinweis | Verhaltens-Steuerung |
|
||
| Action im Log sichtbar | User sieht welches Keyword griff | User kann Keyword nachbessern |
|
||
|
||
**Kein Confidence-Score** für MVP. Zu komplex, zu wenig Mehrwert wenn User Kontrolle hat.
|
||
|
||
**Kein Trash-statt-Delete** für MVP. Begründung: Der bestehende Mechanismus löscht hart. Ein gemischtes Verhalten (statische Keywords → hart löschen, custom keywords → Trash) ist UX-inkonsistent und macht dem User das Modell schwerer verständlich. Wenn Trash generell gewünscht ist, ist das ein separates Feature für alle Löschungen.
|
||
|
||
### 7.3 Regex-Injection
|
||
|
||
User-Input wird vor Regex-Kompilierung escaped (`keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')`). Kein Injection-Risiko. Das ist Server-side Pflicht, nicht optional.
|
||
|
||
---
|
||
|
||
## 8. Aufwand-Schätzung
|
||
|
||
| Komponente | Aufwand | Abhängigkeit |
|
||
|---|---|---|
|
||
| Schema: neue Tabelle `user_mail_keywords` | 0.5 PT | rebreak-backend |
|
||
| Schema: `matchedKeyword`-Feld in `MailBlocked` (optional) | 0.25 PT | rebreak-backend |
|
||
| `PLAN_LIMITS` erweitern (`customKeywords`, `customKeywordBodyMatch`) | 0.25 PT | Mo |
|
||
| `getUserCustomKeywords()` in `server/db/mail.ts` | 0.25 PT | Mo |
|
||
| `scan-internal.post.ts` + `scan.post.ts` erweitern | 0.5 PT | Mo |
|
||
| Neue API-Endpoints (CRUD für Keywords) | 0.5 PT | Mo |
|
||
| UI: Keyword-Manager in Mail-Tab | 1.0 PT | rebreak-ui |
|
||
| **Gesamt MVP (subject_sender, kein body)** | **3.25 PT** | — |
|
||
| Body-Match (Legend, Phase 2) | +1.0 PT | Mo + rebreak-backend |
|
||
|
||
MVP-Definition: `subject_sender`-Scope, Pro 10 / Legend 50 Limit, CRUD-Endpoints, UI-Integration. Kein body-Match.
|
||
|
||
**Hauptaufwand liegt in der UI** (Tag-Input-Komponente, Plan-Gate-Anzeige, Activity-Log-Erweiterung). Der Backend-Teil ist überschaubar, da das Matching-Pattern aus `scan-internal.post.ts` minimal erweitert wird.
|
||
|
||
---
|
||
|
||
## 9. Open Questions
|
||
|
||
**Q1: Body-Match in Phase 1 oder Phase 2?**
|
||
Empfehlung: Phase 2. Subject_sender deckt den Hauptanwendungsfall. Body-Match bringt IMAP-Traffic-Overhead und neue Edge-Cases (MIME-decoding, encoding-Vielfalt). Nicht für MVP.
|
||
|
||
**Q2: matchedKeyword in MailBlocked speichern?**
|
||
Für User-Feedback wichtig (er sieht welches Keyword griff). Aber DSB-Review empfohlen (hans-mueller). Alternativvorschlag: im ersten MVP nur `action="deleted_custom_keyword"` ohne Keyword-Text. Das reicht für statistische Auswertung, lässt das Detail-Thema offen.
|
||
|
||
**Q3: Regex-OR vs. `.some()` + `.includes()`?**
|
||
Bei <= 10 Keywords: vernachlässigbarer Unterschied. Regex-OR ist trotzdem besser als Pattern, weil es skaliert und weil ein einzelner `regex.test()` call klarer ist als eine Schleife. Einigung vorab verhindert spätere Refactors.
|
||
|
||
**Q4: Keyword-Deaktivierung ohne Löschen?**
|
||
User möchte vielleicht ein Keyword temporär pausieren. Nicht für MVP — erhöht Schema-Komplexität (aktives `enabled`-Flag). User kann löschen und neu anlegen.
|
||
|
||
**Q5: Case-Sensitivity für Deutsche Umlaute**
|
||
JavaScript V8 `.toLowerCase()` auf String mit ä/ö/ü: `"Ö".toLowerCase() === "ö"` — korrekt. Kein ICU-Library-Problem in Node.js >= 13 (full-icu built-in). Kein Handlungsbedarf, aber in Integrations-Test verifizieren.
|
||
|
||
**Q6: Keyword-Import / Bulk-Add?**
|
||
Kein MVP-Feature. Pro-User kann 10 Keywords manuell eingeben. Bulk-CSV-Import ist Overkill für diesen Use-Case.
|
||
|
||
---
|
||
|
||
## Eskalationen
|
||
|
||
| Thema | Agent |
|
||
|---|---|
|
||
| Schema-Migration (`user_mail_keywords`, `MailBlocked.matchedKeyword`) | rebreak-backend |
|
||
| DSB-Review `matchedKeyword`-Feld | hans-mueller |
|
||
| UI-Implementierung (Tag-Input, Mail-Settings, Activity-Log) | rebreak-ui |
|
||
| Plan-Tier-Änderungen jenseits `customKeywords`-Feld | Orchestrator |
|