Summary of Plan 16-01: mailparser + linkify-it dependency install,
selectOriginalMessage (EVID-02 three-tier selection), and parseEml +
parseAuthResults + extractUrls + buildBodyPreview (EVID-03/EVID-04),
with a DoS size guard ahead of simpleParser. 26/26 tests pass, tsc
clean. Logs 2 pre-existing unrelated itglue-search.test.ts failures to
deferred-items.md (out of scope, not fixed).
GREEN: parseEml normalizes headers (From/Reply-To/Return-Path/To/Cc/
Subject/Date/Message-ID), builds the ordered Received chain from
mail.headerLines, and maps mailparser attachments to AttachmentMeta
(name/content-type/size/sha256 checksum, related flag preserved for
inline/CID parts per Pitfall 5).
parseAuthResults hand-rolls RFC 8601 Authentication-Results parsing
(spf/dkim/dmarc verdicts) rather than using mailauth, which performs
live DNS/HTTP verification (SC#3 violation). Both Authentication-Results
and Authentication-Results-Original are read via mail.headerLines
(Pitfall 4 — headers Map only exposes one occurrence of a repeated
header) and parsed into distinct authResults/authResultsOriginal fields.
extractUrls uses linkify-it with fuzzyLink enabled (scheme-less www.
URLs) scanning both text and html parts, deduped, never dereferenced.
buildBodyPreview prefers mail.text, falling back to a small hand-rolled
HTML-to-text stripper (not the undeclared transitive html-to-text
dependency — see SUMMARY deviations) when only HTML exists; truncated
to 500 chars.
MAX_EML_BYTES (10 MB, below B2's 25 MB cap) is enforced before
simpleParser is ever called (T-16-01 DoS guard).
26/26 tests pass; tsc clean for eml-parser files; full npm test run
confirms 2 pre-existing itglue-search.test.ts failures are unrelated
(logged to deferred-items.md).
RED: covers EVID-03 (normalized headers, structured auth verdicts,
Received chain, URLs, attachment metadata incl. related flag),
EVID-04 (no-network spy, truncated body preview), and the DoS size
guard (oversized buffer rejected before simpleParser). New synthetic
fixtures: rich multipart, auth-results-original, inline/CID attachment,
long-body, fuzzy-URL, and an oversized-buffer generator. None of these
exports exist on eml-parser.ts yet.
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.
RED: covers all three selection tiers plus ambiguous/no-eml/empty edge
cases. lib/services/eml-parser.ts does not exist yet — tests fail to
resolve the module import.
ticket_notes and time_entries both carry an is_deleted soft-delete flag
(per CLAUDE.md audit-column convention); gatherTicketEvidence was reading
both without filtering it, so retracted notes and reversed time entries
showed up as evidence for every phishing report. Found during code-review
re-verification of the Phase 15 CR-01/WR-01/WR-02 fixes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wWroM6FXkQJiH3JgYcony
- triggerPhishingDetection() mirrors triggerWorkflowEngine's payload.entity-first shape
- reads createdByContactID (Autotask field) into created_by_contact_id, per entity-mapper.ts:211
- called alongside the existing workflow-engine trigger, not awaited in the request path
- gatherTicketEvidence: company_name, ticket_notes, time_entries (all
parameterized $1 queries), and Autotask attachment metadata only
(fullPath/title/contentType, never base64 data); Autotask call wrapped in
try/catch so a failure degrades to an empty attachments array
- detectPhishingTicket: matches, hashes, checks D-04 idempotency guard
(skips re-gathering/writing when content_hash is unchanged), then upserts
one reports row via ON CONFLICT (ticket_id) DO UPDATE ... RETURNING id
- requester_contact_id binds from ticket.contact_id, created_by_contact_id
from ticket.created_by_contact_id per interfaces contract
- KNOWN_PHISHING_PATTERNS: the 8 locked DETECT-01 strings
- matchesPhishingPatterns: case-insensitive substring match (toLowerCase +
includes only, no RegExp/eval), mirrors robotic-classifier.evaluateContains
- computePhishingContentHash: sha256 over title+description only (D-04),
excludes bump-prone fields like status/last_activity_date
- Migration 097 applied and verified in dev DB (7 tables, idempotent re-run confirmed)
- SUMMARY.md documents schema design decisions and worktree-path caveat with scripts/apply-migrations.sh
- 7-table schema: campaigns, reports, messages, indicators, classifications, remediation_actions, audit_events
- reports table fully designed with ticket_id FK, content_hash (D-04 idempotency), matched_patterns, evidence (EVID-01) columns
- All tables/indexes use IF NOT EXISTS for idempotent re-application
- Remaining 6 tables are stubs for Phases 16-21