Initial Commit
This commit is contained in:
208
openspec/changes/enterprise-email-processing/proposal.md
Normal file
208
openspec/changes/enterprise-email-processing/proposal.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user