100 lines
4.1 KiB
Markdown
100 lines
4.1 KiB
Markdown
# 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"`.
|