Task B — linguistische FP-Fix: - mail-classifier.ts: Subject-Keyword-Loop überspringt Keyword-Score wenn Subject das Keyword als Sucht-Compound enthält (z.B. "glücksspiel" in "Glücksspielsucht" → kein +50 Score). Globale linguistische Invariante Deutsch — Gambling-Marketer schreiben nie "Glücksspielsucht-Bonus". - gambling-keywords.mjs: GAMBLING_WHITELIST erweitert um Stamm-Varianten (wettsucht, spielsucht, suchtberatung, suchthilfe) als Fallback für Compounds wo keyword ≠ exakter Stamm. - 4 neue Tests: Forum Glücksspielsucht → PASS, Hilfe bei Spielsucht → PASS, Wettsucht-Selbsthilfe → PASS, Glücksspiel-Bonus 100€ → BLOCK. Task C — Phase-1-Data-Foundation: - mail-training-utils.ts: sanitizeSubjectForTraining() (PII-Stripping via Regex: EMAIL/URL/NUM/Greeting/ALL-CAPS) + detectSubjectLanguage() via franc (iso639-3). 26 Unit-Tests. - franc@6.2.0 installiert (~50KB ESM). - mail.ts insertMailClassificationSample(): ruft sanitizeSubjectForTraining() auf, schreibt detectedLang + subjectSanitized in features-JSON (Interim bis Schema-Migration). - mail-retention-cron.ts: Subject-Nullification nach 30 Tagen (täglich) + Sample-Purge nach 12 Monaten (monatlich). DSGVO Art. 5 Abs. 1e. 105 Tests grün (58 classifier + 26 training-utils + 11 display-name + 10 gmail). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
149 lines
5.9 KiB
TypeScript
149 lines
5.9 KiB
TypeScript
/**
|
|
* Mail-Training-Utils — Phase 1 Data-Foundation.
|
|
*
|
|
* Zweck: Anonymisierung + Spracherkennung für zukünftiges ML-Training (Phase 3).
|
|
* Diese Utilities bereiten Klassifikations-Samples so auf, dass sie DSGVO-konform
|
|
* für Modell-Training verwendbar sind (Art. 5 Abs. 1e — Speicherbegrenzung).
|
|
*
|
|
* Scope dieser Datei:
|
|
* - sanitizeSubjectForTraining() — PII-Stripping aus Betreff-Zeilen
|
|
* - detectSubjectLanguage() — Spracherkennung via franc (iso639-3)
|
|
*
|
|
* Was hier NICHT ist:
|
|
* - ML-Inference / Modell-Calls (Phase 3)
|
|
* - NER (zu aufwendig für MVP)
|
|
* - Consent-Prüfung (liegt beim Aufrufer, nicht in dieser Util)
|
|
*
|
|
* DSGVO-Anmerkungen:
|
|
* - sanitizeSubjectForTraining() entfernt PII bevor Samples persistiert werden.
|
|
* - Raw-Subject bleibt 30 Tage in DB, danach via Retention-Cron auf null gesetzt.
|
|
* - detectedLang (iso639-3-Code) ist kein personenbezogenes Datum.
|
|
*/
|
|
|
|
// franc ist ein ESM-only package (v6+). Import funktioniert in Node ESM context.
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore — franc hat keine @types, Exports sind sauber via JSDoc
|
|
import { franc } from "franc";
|
|
|
|
// ─── Typen ────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Ergebnis der Sanitization.
|
|
* subjectSanitized: PII-freie Version des Betreffs für ML-Training.
|
|
* detectedLang: iso639-3-Code (z.B. "deu", "eng", "fra") oder "und" wenn unbekannt.
|
|
*/
|
|
export interface SubjectSanitizationResult {
|
|
subjectSanitized: string;
|
|
detectedLang: string;
|
|
}
|
|
|
|
// ─── Regex-Patterns für PII-Detection ────────────────────────────────────────
|
|
|
|
/**
|
|
* Erkennt E-Mail-Adressen im Betreff.
|
|
* Beispiel: "Info für test@example.com" → "Info für [EMAIL]"
|
|
*/
|
|
const RE_EMAIL = /[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}/g;
|
|
|
|
/**
|
|
* Erkennt URLs (http/https/www-prefixed).
|
|
* Beispiel: "Besuche https://casino.bet/promo" → "Besuche [URL]"
|
|
*/
|
|
const RE_URL = /(?:https?:\/\/|www\.)[^\s<>"'{}|\\^`\[\]]+/gi;
|
|
|
|
/**
|
|
* Erkennt Zahlenfolgen mit mehr als 4 Ziffern (z.B. Kundennummern, Tracking-IDs).
|
|
* 4-stellige Zahlen bleiben (Jahres-/Score-Angaben wie "2024", "1000€" bleiben lesbar).
|
|
* Beispiel: "Bestellung 123456789" → "Bestellung [NUM]"
|
|
*/
|
|
const RE_LONG_NUMBER = /\b\d{5,}\b/g;
|
|
|
|
/**
|
|
* Erkennt typische Grußformeln am Anfang des Betreffs.
|
|
* Nach dem Grußwort + Komma folgt oft ein Name — alles bis zum nächsten
|
|
* Satzzeichen/Trennzeichen wird als Personenreferenz behandelt und entfernt.
|
|
*
|
|
* Beispiele:
|
|
* "Hallo Max, dein Bonus wartet" → "dein Bonus wartet"
|
|
* "Hi Sarah! Neues Angebot" → "Neues Angebot"
|
|
* "Dear John, your account" → "your account"
|
|
* "Cher Michel, votre offre" → "votre offre"
|
|
* "Sehr geehrte Frau Müller, ..." → "..."
|
|
*/
|
|
const RE_GREETING_PREFIX =
|
|
/^(?:hallo|hi|hey|dear|cher|chère|sehr geehrte[rn]?|bonjour|buenos días|caro|cara)\s+[^,!.;:]+[,!.;:]\s*/i;
|
|
|
|
/**
|
|
* Erkennt ALL-CAPS-Wörter die typischerweise Personennamen oder
|
|
* tracking-spezifische Tokens sind (z.B. "JOHN", "ABC123XY" nach Grußposition).
|
|
* Nur Wörter >= 4 Zeichen, um Abkürzungen wie "SMS", "VIP" zu schonen.
|
|
*
|
|
* Negative Lookahead: Tokens in eckigen Klammern ([EMAIL], [URL], [NUM])
|
|
* werden NICHT angefasst — verhindert [[NAME]] double-replacement.
|
|
*
|
|
* Hinweis: Wird nach Greeting-Strip angewendet, um verbleibende Name-Tokens
|
|
* zu ersetzen. Bewusst konservativ — keine Volltext-NER.
|
|
*/
|
|
const RE_ALL_CAPS_LONG = /(?<!\[)\b[A-ZÄÖÜ]{4,}\b(?!\])/g;
|
|
|
|
// ─── Haupt-Funktion ───────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Sanitiert einen Mail-Betreff für ML-Training:
|
|
* 1. Spracherkennung (franc) auf Raw-Subject — vor Sanitization für bessere Accuracy.
|
|
* 2. E-Mail-Adressen → [EMAIL]
|
|
* 3. URLs → [URL]
|
|
* 4. Zahlen > 4 Stellen → [NUM]
|
|
* 5. Grußwort-Prefix mit Name → strip
|
|
* 6. ALL-CAPS-Wörter >= 4 Zeichen → [NAME] (konservative Heuristik)
|
|
* 7. Whitespace normalisieren
|
|
*
|
|
* Die Reihenfolge ist bedeutsam: URLs vor Emails prüfen (URLs können @-Zeichen
|
|
* enthalten), Greeting vor ALL-CAPS (Greeting-Strip reduziert false positives).
|
|
*
|
|
* Gibt einen leeren String zurück wenn subject null/leer ist.
|
|
*/
|
|
export function sanitizeSubjectForTraining(
|
|
subject: string | null | undefined,
|
|
): SubjectSanitizationResult {
|
|
if (!subject || subject.trim().length === 0) {
|
|
return { subjectSanitized: "", detectedLang: "und" };
|
|
}
|
|
|
|
// Schritt 1: Spracherkennung auf dem Raw-Subject (franc braucht natürlichen Text)
|
|
// franc gibt "und" zurück wenn zu kurz oder unbekannt (< ~20 Zeichen oft unsicher).
|
|
const detectedLang = (franc(subject) as string) ?? "und";
|
|
|
|
// Schritt 2: Sanitization
|
|
let sanitized = subject;
|
|
|
|
// URLs vor E-Mails (URLs können @ enthalten)
|
|
sanitized = sanitized.replace(RE_URL, "[URL]");
|
|
|
|
// E-Mail-Adressen
|
|
sanitized = sanitized.replace(RE_EMAIL, "[EMAIL]");
|
|
|
|
// Lange Zahlenfolgen (> 4 Stellen)
|
|
sanitized = sanitized.replace(RE_LONG_NUMBER, "[NUM]");
|
|
|
|
// Grußwort-Prefix mit Name entfernen
|
|
sanitized = sanitized.replace(RE_GREETING_PREFIX, "");
|
|
|
|
// ALL-CAPS-Wörter >= 4 Zeichen → [NAME]
|
|
sanitized = sanitized.replace(RE_ALL_CAPS_LONG, "[NAME]");
|
|
|
|
// Whitespace normalisieren (mehrfache Leerzeichen, leading/trailing)
|
|
sanitized = sanitized.replace(/\s+/g, " ").trim();
|
|
|
|
return { subjectSanitized: sanitized, detectedLang };
|
|
}
|
|
|
|
/**
|
|
* Wrapper der nur die Spracherkennung ausführt (ohne Sanitization).
|
|
* Nützlich wenn Sanitization bereits anderweitig passiert ist.
|
|
*/
|
|
export function detectSubjectLanguage(subject: string | null | undefined): string {
|
|
if (!subject || subject.trim().length === 0) return "und";
|
|
return (franc(subject) as string) ?? "und";
|
|
}
|