// Reiner Test der Inbound-Mail-Logik für Vorfälle (IM-D, Fachkonzept §2/§14.3). // // Prüft parse.ts + route.ts OHNE Netz/DB/IMAP (Fixtures): // 1. Token wird aus Delivered-To/X-Envelope-To gezogen — NICHT aus To (Weiterleitung). // 2. Weiterleitung: SPF-Fail wird toleriert, DKIM-pass + Allowlist tragen → Vorfall. // 3. Auto-Reply/Bounce (Auto-Submitted/Precedence/leerer Return-Path) → ignorieren. // 4. Dedupe per Message-ID (alreadySeen) → ignorieren. // 5. Unbekannter Token → Betreiber-Review (nicht droppen). // 6. Kein Token → Betreiber-Review. // 7. Absender nicht in Allowlist → Review (fail-closed). // 8. DKIM=fail → Review (auch bei getroffener Allowlist). // // Lauf: npx tsx scripts/test-incident-inbound.ts (rein, keine DB). import { parseInbound, deliveryRecipients, tokenFromAddress, isAutoSubmitted, authResults, intakeAddress, type RawHeaders, } from "../src/server/incident-inbound/parse"; import { decideRoute, generateIntakeToken, type IntakeConfigView } from "../src/server/incident-inbound/route"; let failures = 0; const ok = (cond: boolean, msg: string) => { console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`); if (!cond) failures++; }; const DOMAIN = "in.certvia.de"; const TOKEN = "abc123def456"; // passt auf [a-z0-9]{8,64} const INTAKE = intakeAddress(TOKEN, DOMAIN); // vorfall-abc123def456@in.certvia.de const config: IntakeConfigView = { tenantId: "tenant-1", token: TOKEN, allowlistDomains: ["kunde.de"], sourceAddress: "vorfall@kunde.de", }; // Hilfsbau einer Fixture-Mail. function mail(headers: RawHeaders, extra: Partial<{ from: string; subject: string; text: string; messageId: string }> = {}) { return parseInbound({ headers, ...extra }, DOMAIN); } // --- 1) Token aus Delivered-To / X-Envelope-To, nicht aus To ------------------- { const parsed = mail( { "To": "vorfall@kunde.de", // Kundenadresse — hier steht KEIN Token "Delivered-To": INTAKE, // Zustelladresse trägt den Token "From": "Melder ", "Message-ID": "", "Authentication-Results": "mx.certvia.de; dkim=pass header.d=kunde.de; spf=fail", }, { subject: "Serverausfall", text: "Bitte prüfen." }, ); ok(parsed.token === TOKEN, "Token wird aus Delivered-To gezogen"); ok(parsed.recipientForToken === INTAKE, "recipientForToken = Intake-Adresse"); ok(tokenFromAddress("vorfall@kunde.de", DOMAIN) === null, "To-Adresse liefert keinen Token"); ok(deliveryRecipients({ To: "vorfall@kunde.de" }).length === 0, "To zählt nicht als Zustell-Empfänger"); } // --- 2) Weiterleitung: SPF-Fail toleriert, DKIM+Allowlist → Vorfall ------------ { const parsed = mail( { "X-Envelope-To": INTAKE, "From": "Alice ", "Message-ID": "", "Authentication-Results": "mx; dkim=pass; spf=fail", // Weiterleitung bricht SPF }, { subject: "Phishing gemeldet", text: "Verdächtige Mail erhalten." }, ); ok(parsed.spf === "fail" && parsed.dkim === "pass", "SPF=fail, DKIM=pass erkannt"); const d = decideRoute(parsed, { config, alreadySeen: false }); ok(d.action === "incident", "trotz SPF-Fail → Vorfall (DKIM+Allowlist tragen)"); if (d.action === "incident") { ok(d.incident.tenantId === "tenant-1", "Vorfall im richtigen Mandanten"); ok(d.incident.title === "Phishing gemeldet", "Betreff → Titel"); ok(d.incident.reporterContact === "alice@kunde.de", "Absender → Melder-Kontakt"); ok(d.spfWarning === true, "SPF-Fail als Warnung markiert (nicht blockierend)"); ok(d.incident.inboundMessageId === "m2@kunde.de", "Message-ID normalisiert übernommen"); } } // --- 3) Auto-Reply / Bounce → ignorieren -------------------------------------- { ok(isAutoSubmitted({ "Auto-Submitted": "auto-replied" }), "Auto-Submitted erkannt"); ok(isAutoSubmitted({ "Precedence": "bulk" }), "Precedence: bulk erkannt"); ok(isAutoSubmitted({ "Return-Path": "<>" }), "Leerer Return-Path (Bounce) erkannt"); ok(!isAutoSubmitted({ "Auto-Submitted": "no" }), "Auto-Submitted: no ist normal"); const parsed = mail({ "Delivered-To": INTAKE, "From": "mailer@kunde.de", "Auto-Submitted": "auto-replied", "Message-ID": "", "Authentication-Results": "mx; dkim=pass; spf=pass", }); const d = decideRoute(parsed, { config, alreadySeen: false }); ok(d.action === "ignore" && d.reason === "auto_submitted", "Auto-Reply → ignore"); } // --- 4) Dedupe per Message-ID → ignorieren ------------------------------------ { const parsed = mail({ "Delivered-To": INTAKE, "From": "alice@kunde.de", "Message-ID": "", "Authentication-Results": "mx; dkim=pass; spf=pass", }); const d = decideRoute(parsed, { config, alreadySeen: true }); ok(d.action === "ignore" && d.reason === "duplicate", "bereits gesehene Message-ID → ignore"); } // --- 5) Unbekannter Token → Review -------------------------------------------- { const parsed = mail({ "Delivered-To": intakeAddress("unknowntoken99", DOMAIN), "From": "alice@kunde.de", "Message-ID": "", "Authentication-Results": "mx; dkim=pass; spf=pass", }); ok(parsed.token === "unknowntoken99", "Token geparst, aber unbekannt"); const d = decideRoute(parsed, { config: null, alreadySeen: false }); ok(d.action === "review" && d.review.reason === "unknown_token", "unbekannter Token → Review"); ok(d.action === "review" && d.review.tenantId === null, "kein Mandant bei unbekanntem Token"); } // --- 6) Kein Token → Review --------------------------------------------------- { const parsed = mail({ "Delivered-To": "hallo@in.certvia.de", // kein vorfall- "From": "alice@kunde.de", "Message-ID": "", }); ok(parsed.token === null, "kein Token erkannt"); const d = decideRoute(parsed, { config: null, alreadySeen: false }); ok(d.action === "review" && d.review.reason === "no_token", "kein Token → Review"); } // --- 7) Absender nicht in Allowlist → Review (fail-closed) -------------------- { const parsed = mail({ "Delivered-To": INTAKE, "From": "fremd@boese.example", // nicht in Allowlist (kunde.de) "Message-ID": "", "Authentication-Results": "mx; dkim=pass; spf=pass", }); const d = decideRoute(parsed, { config, alreadySeen: false }); ok(d.action === "review" && d.review.reason === "allowlist_failed", "Absender außerhalb Allowlist → Review"); ok(d.action === "review" && d.review.tenantId === "tenant-1", "Mandant trotzdem aufgelöst (Token bekannt)"); } // --- 8) DKIM=fail → Review (auch bei getroffener Allowlist) ------------------- { const parsed = mail({ "Delivered-To": INTAKE, "From": "alice@kunde.de", "Message-ID": "", "Authentication-Results": "mx; dkim=fail; spf=pass", }); ok(authResults({ "Authentication-Results": "mx; dkim=fail" }).dkim === "fail", "DKIM=fail geparst"); const d = decideRoute(parsed, { config, alreadySeen: false }); ok(d.action === "review" && d.review.reason === "dkim_failed", "DKIM-Fail → Review"); } // --- 9) Token-Generator liefert gültiges Format ------------------------------- { const tok = generateIntakeToken(); ok(/^[a-z0-9]{8,64}$/.test(tok), "generateIntakeToken erfüllt Token-Format"); ok(tokenFromAddress(intakeAddress(tok, DOMAIN), DOMAIN) === tok, "Round-Trip Token↔Adresse"); } console.log(failures === 0 ? "\nAlle Inbound-Tests grün." : `\n${failures} Test(s) fehlgeschlagen.`); process.exit(failures === 0 ? 0 : 1);