← all parts

Part · email.transactional

What's actually behind email.transactional

The part exactly as partkit add email.transactional vendors it into your repo — verified, locked, every byte readable. Nothing here is mocked.

email.transactionalv1.2.0

✓ attested🔒 read-only↑ ctrlai upgradeemail.transactional@1

Lives at parts/email.transactional/ in your repo — open, owned, readable. Not buried in node_modules. 418 lines of source you can audit.

11 conformance tests passedverified 2026-06-15adapter postmark↗ CI run
content hash ab9d4c4ef6…f85e2dpinned in parts.lockctrlai guard fails CI if a single byte changes
tested against node 25.3.0

Public API — what your seam calls

  • send(message: EmailMessage): Promise<SendResult>
  • class EmailError extends Error { code: EmailErrorCode; retryable: boolean; status: number | null }
  • types: EmailMessage, EmailAddress, SendResult, EmailErrorCode

Invariants — guarantees the contract pins

  1. Importing the part performs no I/O and never throws; configuration is validated at call time with typed errors
  2. An invalid message fails fast with a typed error and zero network calls
  3. CR/LF sequences in subject, display names, or custom headers are rejected (header-injection defense)
  4. Transient vendor failures (429, 5xx, network) are retried up to 3 attempts with exponential backoff and jitter; permanent failures are never retried
  5. All failures surface as typed EmailError values; raw vendor responses never escape the part
  6. Secret values never appear in error messages

Dependencies

zero-dep — runs on your Postgres

Swappable adapters

resendpostmarkses
SOURCEparts/email.transactional/18 files · click to read
parts/email.transactional/src/index.tstypescript · 2,292 bytes
/**
 * email.transactional — public interface. The ONLY legal import surface.
 * Contract: ../contract.json · What your app must provide: ../seams.md
 */
import { adapter } from "../adapters/selected/adapter";
import { parseFromAddress, requireEnv } from "./internal/config";
import { EmailError } from "./internal/errors";
import { redactSecrets } from "./internal/redact";
import { withRetry } from "./internal/retry";
import type { EmailMessage } from "./internal/types";
import { normalizeMessage } from "./internal/validate";

export { EmailError } from "./internal/errors";
export type { EmailErrorCode } from "./internal/errors";
export type { EmailAddress, EmailMessage } from "./internal/types";

export interface SendResult {
  /** Vendor-assigned message id. */
  id: string;
  /** Adapter that performed the send. */
  adapter: string;
}

/**
 * Send one transactional email through the vendored adapter.
 *
 * Importing this module performs no I/O; configuration is validated here, at
 * call time, with typed errors (contract invariant 1 — serverless-safe).
 * Transient vendor failures are retried inside this call (invariant 4); every
 * failure surfaces as an EmailError with secrets redacted (invariants 5, 6).
 */
export async function send(message: EmailMessage): Promise<SendResult> {
  try {
    const normalized = normalizeMessage(message);

    const configured = requireEnv("EMAIL_ADAPTER");
    if (configured !== adapter.name) {
      throw new EmailError(
        "config",
        `EMAIL_ADAPTER is "${configured}" but the vendored adapter is "${adapter.name}" — ` +
          `re-vendor with: partkit upgrade email.transactional --adapter=${configured}`,
        { retryable: false },
      );
    }
    const from = parseFromAddress(requireEnv("EMAIL_FROM"));

    const result = await withRetry(() => adapter.send({ from, message: normalized }));
    return { id: result.id, adapter: adapter.name };
  } catch (e) {
    if (e instanceof EmailError) {
      throw new EmailError(e.code, redactSecrets(e.message), {
        retryable: e.retryable,
        ...(e.status !== null && { status: e.status }),
      });
    }
    throw new EmailError("unknown", redactSecrets(e instanceof Error ? e.message : String(e)), {
      retryable: false,
    });
  }
}