email.transactional v1.0.1
Send transactional email through a contract-stable interface with pluggable, attested vendor adapters.
provides email.transactional@1
Attestations
adapterpostmark
verified2026-06-11
expires2026-06-25
conformance10 tests
signaturedev (pre-v0)
adapterresend
verified2026-06-11
expires2026-06-25
conformance10 tests
signaturedev (pre-v0)
Invariants
Testable claims, not adjectives — each maps to at least one named conformance test.
- Importing the part performs no I/O and never throws; configuration is validated at call time with typed errors
- An invalid message fails fast with a typed error and zero network calls
- CR/LF sequences in subject, display names, or custom headers are rejected (header-injection defense)
- Transient vendor failures (429, 5xx, network) are retried up to 3 attempts with exponential backoff and jitter; permanent failures are never retried
- All failures surface as typed EmailError values; raw vendor responses never escape the part
- Secret values never appear in error messages
Interface
send(message: EmailMessage): Promise<SendResult>
class EmailError extends Error { code: EmailErrorCode; retryable: boolean; status: number | null }
types: EmailMessage, EmailAddress, SendResult, EmailErrorCode
Environment
EMAIL_ADAPTER | required · one of: resend | postmark |
|---|---|
EMAIL_FROM | required |
RESEND_API_KEY | optional · secret |
POSTMARK_SERVER_TOKEN | optional · secret |
Seams — what your app writes
Sufficient without reading src/; that is part of the quality bar.
# Seams — email.transactional
What YOUR app provides. Reading `contract.json` + this file is enough to wire
the part — you never need to read `src/`. Never edit `src/` or `adapters/`
(attested interiors; edits void the attestation and fail CI).
## 1. Environment
| Var | Required | Notes |
|---|---|---|
| `EMAIL_ADAPTER` | yes | Must equal the vendored adapter — `partkit add` already set it in `.env.example`. |
| `EMAIL_FROM` | yes | `"Acme <hello@yourdomain.com>"` or bare address. The domain must be verified with your vendor. |
| `RESEND_API_KEY` | when adapter = resend | Secret. |
| `POSTMARK_SERVER_TOKEN` | when adapter = postmark | Secret. `POSTMARK_MESSAGE_STREAM` optional (default `outbound`). |
## 2. Import path
Add one tsconfig alias (recommended):
```jsonc
// tsconfig.json → compilerOptions
"paths": { "@parts/*": ["./parts/*/src"] }
```
Then:
```ts
import { send, EmailError } from "@parts/email.transactional";
```
Plain relative imports of `parts/email.transactional/src/index.js` work too.
Never deep-import `src/internal/**` or `adapters/**` (lint-enforced).
## 3. Templates are YOUR domain (the template seam)
The part sends; it does not own your copy. Write plain functions that return
`{ subject, html, text }` and keep them in app code (e.g. `src/email/`) —
start from `examples/welcome-email.ts`, which is outside the boundary and
freely copyable.
## 4. Error handling
Every failure is an `EmailError` with `.code` (`"config" | "invalid_message" |
"auth" | "rate_limited" | "rejected" | "vendor_unavailable" | "unknown"`) and
`.retryable`. Retries already happened inside the part — if you catch a
retryable error, queue or defer; do not instant-retry in a loop.
## 5. Switching vendors
`partkit upgrade email.transactional --adapter=postmark` re-vendors and
updates env — never edit `adapters/` by hand. (Until `upgrade` ships:
`partkit eject` then re-`add` with the other adapter.)
## 6. What you must NOT do
- Edit or import anything under `src/internal/**` or `adapters/**`.
- Log `RESEND_API_KEY` / `POSTMARK_SERVER_TOKEN` or full vendor responses.
- Set `RESEND_BASE_URL` / `POSTMARK_BASE_URL` in production — they exist for
the conformance fakes only.
Install
$ partkit add email.transactional --adapter=resend|postmark