From e4718ae71a79ed794d61807a200629bdc063e75a Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 15 Jul 2026 10:23:00 -0400 Subject: [PATCH] feat(16-01): implement selectOriginalMessage three-tier attachment selection GREEN: rfc.eml exact match -> single non-OriginatingEmail message/rfc822 candidate (covers KnowBe4 versioned filenames) -> OriginatingEmail.eml fallback -> null. Case-insensitive on both content-type and filename (checked via title/fullPath basename). All 8 selectOriginalMessage tests pass; tsc clean for eml-parser files. --- lib/services/eml-parser.ts | 62 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 lib/services/eml-parser.ts diff --git a/lib/services/eml-parser.ts b/lib/services/eml-parser.ts new file mode 100644 index 0000000..8236c48 --- /dev/null +++ b/lib/services/eml-parser.ts @@ -0,0 +1,62 @@ +/** + * EML/MIME evidence parser for the phishing-triage pipeline (Phase 16). + * + * Turns a raw RFC822/MIME `.eml` buffer (an attacker-controlled email a + * user reported as phishing/spam) into a normalized, structured + * `NormalizedMessage` — headers, structured SPF/DKIM/DMARC verdicts, + * URLs, and attachment metadata — using `mailparser` for MIME parsing + * and a small hand-rolled RFC 8601 tokenizer for Authentication-Results. + * + * Hard invariant (SC#3 / EVID-04 / T-16-03): this module must never fetch + * or execute anything found in a message. It never dereferences an + * extracted URL, never renders `mail.html`, and never performs any + * outbound network call while parsing. This is test-enforced with a + * `global.fetch` spy in eml-parser.test.ts. + * + * Also implements `selectOriginalMessage` (EVID-02) — a pure, I/O-free + * decision over ticket attachment metadata already in hand, choosing the + * originally-reported message from a ticket's attachment list. + */ + +import type { Attachment } from '@/lib/types/autotask'; + +/** Basename of an attachment's filename, lowercased, for tier matching. */ +function attachmentName(att: Attachment): string { + const raw = att.fullPath || att.title || ''; + const base = raw.split('/').pop() || raw; + return base.toLowerCase(); +} + +function isMessageRfc822(att: Attachment): boolean { + return (att.contentType || '').toLowerCase() === 'message/rfc822'; +} + +/** + * Three-tier `.eml` attachment selection, empirically validated against 15 + * real phishing tickets (see 16-RESEARCH.md Pitfall 1): + * + * 1. An attachment named exactly `rfc.eml` (case-insensitive) among + * `message/rfc822` attachments — the Microsoft "Report Message" flow. + * 2. Else, among `message/rfc822` attachments, exclude any named exactly + * `OriginatingEmail.eml` (case-insensitive) — if exactly one candidate + * remains, select it (covers KnowBe4's versioned filenames, e.g. + * `phish_alert_sp2_2.0.0.0.eml`). + * 3. Else (0 or 2+ ambiguous candidates after step 2) — fall back to + * `OriginatingEmail.eml` if present; otherwise return null. + */ +export function selectOriginalMessage(attachments: Attachment[]): Attachment | null { + const rfc822Attachments = attachments.filter(isMessageRfc822); + + const exactRfcEml = rfc822Attachments.find((att) => attachmentName(att) === 'rfc.eml'); + if (exactRfcEml) return exactRfcEml; + + const nonOriginatingCandidates = rfc822Attachments.filter( + (att) => attachmentName(att) !== 'originatingemail.eml' + ); + if (nonOriginatingCandidates.length === 1) return nonOriginatingCandidates[0]; + + const originatingFallback = rfc822Attachments.find( + (att) => attachmentName(att) === 'originatingemail.eml' + ); + return originatingFallback ?? null; +}