/** * 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 = /(? 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"; }