Files
suaveplan/openspec/specs/email-processing/spec.md
2026-05-27 23:29:53 +01:00

4.1 KiB

Spec delta — email-processing

ADDED Requirements

Requirement: Parse RFC 822 messages with hook pipeline

The package SHALL expose parseEmail(raw, options?) that:

  • accepts string | ArrayBuffer | Uint8Array,
  • validates options via a strict Zod schema,
  • raises EmailProcessingError(INVALID_INPUT) for null / undefined / wrong-type raw,
  • raises EmailProcessingError(INVALID_OPTIONS) for any Zod failure,
  • wraps postal-mime parse failures in EmailProcessingError(PARSE_FAILURE) and increments the OTel counter email_processing.parse.failure_total,
  • on success, increments email_processing.parse.total (attributed by attachment count) and pipes the result through the email:processing hook pipeline.

Scenario: Successful parse threads through the hook pipeline

  • WHEN parseEmail is called with a valid multipart MIME string and an onEmailProcessing handler is registered that mutates parsed.subject
  • THEN the returned ParsedEmail.subject reflects the handler's transformation.

Requirement: Attachment processing with size + MIME allowlist

processAttachment SHALL hash content with SHA-256, validate against maxSize (default 25 MiB) and allowedMimeTypes (wildcard image/* supported), increment email_processing.attachment.processed_total or ...rejected_total (attributed by reason), and record an ...extraction_duration_ms histogram entry per call.

Scenario: Attachment over maxSize is rejected with reason=size

  • WHEN processAttachment is called with content larger than the configured maxSize
  • THEN the result has valid: false, the error mentions the size limit, and the rejected counter is incremented with reason=size.

Requirement: Hook pipeline with public registry

The package SHALL expose Hooks<TMap>, emailHooks, and onEmailProcessing. Handler errors SHALL short-circuit the pipeline and surface as EmailProcessingError(HOOK_FAILURE). The email_processing.hook.invocation_total counter SHALL be incremented once per successful pipeline invocation, attributed by event name and handler count.

Requirement: DKIM / SPF / DMARC verification

  • verifyDkim SHALL parse the DKIM-Signature header, look up the public key through an injectable DNS resolver, recompute the body hash, rebuild the canonicalised signed-header block, call crypto.subtle.verify, and emit email_processing.dkim.result_total attributed by outcome.
  • checkSpf SHALL enforce MAX_INCLUDE_DEPTH = 10 and MAX_DNS_LOOKUPS = 10, emit email_processing.spf.result_total attributed by outcome.
  • checkDmarc SHALL combine SPF + DKIM into a relaxed / strict alignment evaluation, emit email_processing.dmarc.result_total attributed by outcome.

Requirement: Heuristic spam scoring

scoreSpam SHALL apply the twelve default rules, accept customRules (which replace defaults with the same id), clamp the score at [0, 10], classify against suspiciousThreshold (default 3) and spamThreshold (default 6), and emit email_processing.spam.scored_total attributed by classification.

Requirement: Threading

groupThreads SHALL produce one EmailThread per conversation, using In-Reply-To, References, and a subject-change heuristic, sorted chronologically by first-message date. The email_processing.thread.grouped_total counter SHALL be incremented with the produced thread count per call.

Requirement: Stable error registry

The package SHALL expose EMAIL_PROCESSING_CODES containing exactly eight codes — INVALID_INPUT, PARSE_FAILURE, INVALID_OPTIONS, ATTACHMENT_TOO_LARGE, ATTACHMENT_TYPE_REJECTED, HASH_FAILURE, DKIM_VERIFY_FAILURE, HOOK_FAILURE. Every code SHALL be thrown by at least one production path. EmailProcessingError's constructor SHALL be public.

Scenario: Public constructor allows direct construction

  • WHEN a downstream package imports EmailProcessingError and EMAIL_PROCESSING_CODES.INVALID_INPUT and calls new EmailProcessingError(code, key)
  • THEN TypeScript accepts the call and the resulting instance has code === "EMAIL_PROCESSING.INVALID_INPUT".