Files
2026-05-27 23:29:53 +01:00

209 lines
6.5 KiB
Markdown

# 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
```ts
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
```ts
// 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
```ts
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
1. **`EmailProcessingError`** extends `SuaveplanError` with six stable error codes.
2. **Zod schemas** for `ParsedEmail`, `EmailAddress`, `Attachment`, `ParseOptions`, `ValidateOptions`.
3. **`AbortSignal` support** on `parseEmail` and `createEmailStream` — throws `EMAIL_PROC_ABORTED` on cancel.
4. **OTel instrumentation** — span `email.parse`, counter `email.parse_total`, histogram `email.parse_duration_ms`, attribute `email.attachment_count`.
5. **Disposable-domain detection** — bundled list of ~3 000 known disposable domains; `validateAddress` checks against it when `options.checkDisposable` is true.
6. **Property-based tests** — RFC 5322 header fold/unfold round-trip, address list round-trip, HTML-to-text idempotency on plain text input.
7. **Benchmark suite** — parse a 50 KB multipart message (cold), header-only parse (hot), 1 000-address list parse.
---
## File layout
```text
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.