← all parts

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_ADAPTERrequired · one of: resend | postmark
EMAIL_FROMrequired
RESEND_API_KEYoptional · secret
POSTMARK_SERVER_TOKENoptional · 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