6.5 KiB
OpenSpec Change: enterprise-email-processing
- Change ID: enterprise-email-processing
- Package:
@suaveplan/email-processing - Category: universal
- Target path:
universal/email/email-processing - Status: IN PROGRESS
- Date: 2026-04-24
- Reference spec:
/mnt/s/web/suaveplan/enterprise/email-processing.md
Summary
Deliver @suaveplan/email-processing on the genesis branch from scratch at enterprise quality. The package provides MIME email parsing and processing: raw RFC 5322/MIME message parsing, header field extraction and folding, multipart boundary handling, HTML-to-text extraction, inline attachment enumeration, address list parsing (RFC 5321/5322), email address validation with disposable-domain detection, and a streaming parse API over ReadableStream. Genesis adds structured errors via @suaveplan/error, Zod schemas for every public type, OTel instrumentation, AbortSignal cancellation on async paths, property-based tests with fast-check, and a benchmark suite.
Current-state audit (genesis)
The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data.
Available workspace deps on genesis
| Package | Role |
|---|---|
@suaveplan/error (workspace:^) |
Base class for EmailProcessingError; defineCodes for EMAIL_PROC.* codes |
@suaveplan/types (workspace:^) |
LoggerLike, Clock (injectable clock for header date parsing) |
@suaveplan/validation (workspace:^) |
Schema composition helpers for address validation |
zod (catalog:forms) |
Zod schemas for all public option types and parsed output shapes |
@opentelemetry/api (catalog:observability) |
Spans + counters on parse, validate, extract operations |
tslib (catalog:build) |
Runtime helpers |
Dev deps: vitest, @vitest/coverage-istanbul, fast-check (all catalog:testing).
Public API surface
Core types
interface ParsedEmail {
messageId?: string
from: EmailAddress[]
to: EmailAddress[]
cc: EmailAddress[]
bcc: EmailAddress[]
replyTo: EmailAddress[]
subject?: string
date?: Date
headers: Map<string, string[]>
textBody?: string
htmlBody?: string
attachments: Attachment[]
inlines: Attachment[]
}
interface EmailAddress {
name?: string
address: string
}
interface Attachment {
filename?: string
contentType: string
contentId?: string
content: Uint8Array
size: number
}
interface ParseOptions {
maxSize?: number // default 25 MB
allowPartial?: boolean // default false
signal?: AbortSignal
}
Functions
// Parse raw RFC 5322 message bytes or string
function parseEmail(raw: string | Uint8Array, options?: ParseOptions): Promise<ParsedEmail>
// Parse headers only (no body decode — fast path)
function parseHeaders(raw: string | Uint8Array): Map<string, string[]>
// RFC 5321/5322 address list parser
function parseAddressList(value: string): EmailAddress[]
// HTML body to plain-text extraction
function htmlToText(html: string, options?: HtmlToTextOptions): string
// Email address validation (syntax + MX-optional + disposable-domain check)
function validateAddress(address: string, options?: ValidateOptions): ValidationResult
// Streaming parse — emits header, textBody, htmlBody, attachment events
function createEmailStream(options?: ParseOptions): EmailParseStream
interface EmailParseStream {
writable: WritableStream<Uint8Array>
readable: ReadableStream<EmailParseEvent>
}
type EmailParseEvent =
| { type: 'headers'; headers: Map<string, string[]> }
| { type: 'textBody'; text: string }
| { type: 'htmlBody'; html: string }
| { type: 'attachment'; attachment: Attachment }
Error codes
type EmailProcessingErrorCode =
| 'EMAIL_PROC_PARSE_FAILED'
| 'EMAIL_PROC_TOO_LARGE'
| 'EMAIL_PROC_INVALID_ADDRESS'
| 'EMAIL_PROC_INVALID_HEADER'
| 'EMAIL_PROC_ENCODING_FAILED'
| 'EMAIL_PROC_ABORTED'
Genesis additions
EmailProcessingErrorextendsSuaveplanErrorwith six stable error codes.- Zod schemas for
ParsedEmail,EmailAddress,Attachment,ParseOptions,ValidateOptions. AbortSignalsupport onparseEmailandcreateEmailStream— throwsEMAIL_PROC_ABORTEDon cancel.- OTel instrumentation — span
email.parse, counteremail.parse_total, histogramemail.parse_duration_ms, attributeemail.attachment_count. - Disposable-domain detection — bundled list of ~3 000 known disposable domains;
validateAddresschecks against it whenoptions.checkDisposableis true. - Property-based tests — RFC 5322 header fold/unfold round-trip, address list round-trip, HTML-to-text idempotency on plain text input.
- Benchmark suite — parse a 50 KB multipart message (cold), header-only parse (hot), 1 000-address list parse.
File layout
universal/email/email-processing/
package.json
tsconfig.json
vitest.config.ts
src/
index.ts
error-codes.ts
constants/
index.ts # MAX_EMAIL_SIZE, CRLF, MIME_BOUNDARY_RE
types/
common.ts
common.md
core/
parser/
parser.ts # parseEmail orchestrator
parser.md
parser.types.ts
parser.test.ts
index.ts
header-parser/
header-parser.ts # parseHeaders, header fold/unfold
header-parser.md
header-parser.test.ts
index.ts
mime-decoder/
mime-decoder.ts # multipart, base64, QP decoding
mime-decoder.md
mime-decoder.test.ts
index.ts
address-parser/
address-parser.ts # parseAddressList
address-parser.md
address-parser.test.ts
index.ts
validation/
address-validator/
address-validator.ts # validateAddress + disposable-domain list
address-validator.md
address-validator.test.ts
index.ts
extraction/
html-to-text/
html-to-text.ts
html-to-text.md
html-to-text.test.ts
index.ts
streaming/
email-stream/
email-stream.ts # createEmailStream
email-stream.md
email-stream.test.ts
index.ts
__benchmarks__/
email-processing.bench.ts
tests/
integration.test.ts
property.test.ts
Impact
Downstream consumers: @suaveplan/campaigns, @suaveplan/delivery-tracking, @suaveplan/notification-channel.
Workspace dep prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/validation must be published to Verdaccio before building.