Initial Commit
This commit is contained in:
38
openspec/archive/2026-05-24-email-processing/proposal.md
Normal file
38
openspec/archive/2026-05-24-email-processing/proposal.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Email Processing — universal-tier migration
|
||||
|
||||
## Why
|
||||
|
||||
`@suaveplan/email-processing` is the canonical MIME + email-content
|
||||
processor for every downstream consumer that ingests email (campaigns,
|
||||
emailer, delivery-tracking, suppression, dmarc). The package exists on
|
||||
main at `/mnt/s/web/suaveplan/packages/universal/email-processing` at
|
||||
`v1.2.3` with 96.23% coverage; the enterprise plan at
|
||||
`/mnt/s/web/suaveplan/enterprise/email-processing.md` demands a
|
||||
genesis-quality rewrite with Zod boundary validation, OTel
|
||||
instrumentation, structured error codes, injectable resolvers, and 100%
|
||||
coverage. This change lands the package fresh under the canonical
|
||||
3-level layout (`packages/communication/emails/email-processing/`).
|
||||
|
||||
## What changes
|
||||
|
||||
- New universal-tier package `@suaveplan/email-processing@0.1.0`.
|
||||
- Wraps `postal-mime` and `libmime` via a strict TypeScript surface;
|
||||
no `@ts-expect-error` or `any` leaks.
|
||||
- Adds `EMAIL_PROCESSING_CODES` (eight codes) and a
|
||||
`EmailProcessingError` class with a `public` constructor.
|
||||
- Adds OTel instruments: parse counter, parse-failure counter,
|
||||
attachment processed / rejected counters, attachment-extraction
|
||||
duration histogram, hook-invocation counter, DKIM/SPF/DMARC result
|
||||
counters, spam-classification counter, threads-grouped counter.
|
||||
- Replaces the upstream `@suaveplan/events/hooks` substrate (which
|
||||
does not exist in genesis yet) with an in-package `Hooks<TMap>`
|
||||
pipeline so the public `email:processing` surface is preserved.
|
||||
- 100% statement / branch / function / line coverage (Istanbul).
|
||||
- No mocks: DKIM round-trip is exercised with a real RSA keypair
|
||||
generated via Web Crypto.
|
||||
|
||||
## Impact
|
||||
|
||||
- Affected specs: `email-processing` (new capability).
|
||||
- Affected code: new package only — no consumers migrated yet.
|
||||
- Catalog: adds `postal-mime` and `libmime` to the `parsing` catalog.
|
||||
@@ -0,0 +1,99 @@
|
||||
# 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"`.
|
||||
79
openspec/archive/2026-05-24-email-processing/tasks.md
Normal file
79
openspec/archive/2026-05-24-email-processing/tasks.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Tasks — email-processing
|
||||
|
||||
## §0 Pre-flight (worktree isolation)
|
||||
|
||||
- [x] `git worktree list` confirms the agent is inside
|
||||
`.claude/worktrees/agent-<id>/` and not the main checkout at
|
||||
`/home/paul/web/genesis/`.
|
||||
- [x] §0a Sync confirms HEAD is at or ahead of
|
||||
`9225d658502add3fb62ef0a8f9ac35563de985ff` (post health-check
|
||||
archive).
|
||||
- [x] `git status` is clean at start; every write lands under the
|
||||
worktree path.
|
||||
|
||||
## §1 Package shell
|
||||
|
||||
- [x] `packages/communication/emails/email-processing/package.json`
|
||||
at `0.1.0`, sideEffects: false, every subpath export listed.
|
||||
- [x] `tsconfig.lib.json` extending the root base.
|
||||
- [x] `vite.config.ts` with explicit entry map covering every feature
|
||||
folder.
|
||||
- [x] `vitest.config.ts` with Istanbul 100/100/100/100 thresholds.
|
||||
- [x] `tests/setup/polyfills.ts` installing a real
|
||||
`InMemoryMetricExporter`-backed `MeterProvider`.
|
||||
|
||||
## §2 Feature folders (15)
|
||||
|
||||
- [x] `error-codes/` — `EMAIL_PROCESSING_CODES` + `EmailProcessingError`
|
||||
with public ctor + test + doc.
|
||||
- [x] `telemetry/` — shared meter + 11 instruments + test + doc.
|
||||
- [x] `types/` — flat shared MIME / address / header types.
|
||||
- [x] `hooks/` — local `Hooks<TMap>` pipeline + singleton + test + doc.
|
||||
- [x] `parser/` — `parseEmail`, `parseEmailAddress`, `decodeEmailWords`
|
||||
+ Zod option boundary + test + doc.
|
||||
- [x] `mime-utils/` — eleven libmime wrappers + local `libmime.d.ts`
|
||||
ambient declaration + test + doc.
|
||||
- [x] `attachment-processor/` — `processAttachment(s)` + SHA-256 hash +
|
||||
size / MIME allowlist + duration histogram + test + doc.
|
||||
- [x] `cid-resolver/` — resolve/extract/isReferencedInline + test +
|
||||
doc.
|
||||
- [x] `calendar-parser/` — VEVENT extraction + test + doc.
|
||||
- [x] `data-extractor/` — tracking + receipt + extractData + test +
|
||||
doc.
|
||||
- [x] `dkim-canonicalize/` — relaxed + simple canon + signed-block
|
||||
builder + test + doc.
|
||||
- [x] `dkim-verifier/` — parseDkim + verifyDkim with real Web-Crypto
|
||||
round-trip in tests + test + doc.
|
||||
- [x] `dmarc-checker/` — record parser + alignment evaluator + test +
|
||||
doc.
|
||||
- [x] `spf-checker/` — record parser + ipv4/ipv6 matchers + recursive
|
||||
checkSpf with bounded depth + test + doc.
|
||||
- [x] `spam-scorer/` — 12 default rules + custom-rule merge + test +
|
||||
doc.
|
||||
- [x] `thread-grouper/` — In-Reply-To + References + subject
|
||||
grouping, tree builder + test + doc.
|
||||
|
||||
## §3 OpenSpec spec delta
|
||||
|
||||
- [x] `openspec/changes/email-processing/specs/email-processing/spec.md`
|
||||
captures the ADDED Requirements for parse, attachment processing,
|
||||
hook pipeline, DKIM/SPF/DMARC, spam scoring, threading, and OTel
|
||||
instruments.
|
||||
|
||||
## §4 Quality gates (run before commit)
|
||||
|
||||
- [ ] `bunx turbo lint typecheck test --filter=@suaveplan/email-processing` green.
|
||||
- [ ] `bunx turbo test:coverage --filter=@suaveplan/email-processing`
|
||||
at 100/100/100/100.
|
||||
- [ ] `bunx turbo build --filter=@suaveplan/email-processing` green.
|
||||
- [ ] `grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/communication/emails/email-processing/src/` → empty.
|
||||
- [ ] `grep -rn "biome-ignore\|TODO\|FIXME\|XXX" packages/communication/emails/email-processing/src/` → empty.
|
||||
- [ ] `bun scripts/lint/check-feature-layout.ts | grep email-processing` → empty.
|
||||
- [ ] `bun scripts/folder-reorg/generate-manifest.ts` regenerates
|
||||
`MANIFEST.md` cleanly.
|
||||
|
||||
## §5 Ship
|
||||
|
||||
- [ ] Commit on a `feat/email-processing` branch.
|
||||
- [ ] Surface SHA + coverage + file count in the agent's final
|
||||
report.
|
||||
Reference in New Issue
Block a user