---
title: "@playstack/mail"
description: "Portable typed mail rendering, envelope validation, provider templates, and delivery contracts."
tags: ["package","infrastructure","mail","email","free"]
---

{/* package-access:start */}

> **Free.** MIT licensed. Check preview availability before installing. See [package access](/docs/packages#access-policy).

{/* package-access:end */}

`@playstack/mail` separates typed application rendering and validated portable envelopes from Resend, Postmark, Amazon SES, Mailgun, SendGrid, SMTP, or test delivery providers.

{/* package-install:start */}

## Install

After confirming [preview access](/docs/packages#access-policy), install the package at your application's shared Playstack version:

```sh
npm install --save-exact @playstack/mail@0.1.0-beta.1
```

Check the peer requirements below before choosing a runtime or provider.

{/* package-install:end */}

## Compose rendered mail

```ts
import { createMail } from '@playstack/mail'
import { createPostmarkMailProvider } from '@playstack/mail/postmark'

const mail = createMail({
  driver: createPostmarkMailProvider(process.env.POSTMARK_SERVER_TOKEN!, {
    messageStream: 'outbound',
  }),
  from: { address: 'hello@example.com', name: 'Example' },
  replyTo: 'support@example.com',
})

await mail.send({
  to: 'person@example.com',
  template: renderInvitation,
  props: { name: 'Ada', invitationUrl },
})
```

The application owns rendering and localization. A typed template returns subject plus HTML/text before provider I/O, so renderer failures are distinct from delivery failures. Chakra Email or any other renderer can implement it; this package has no React dependency.

## Driver configuration

| `createMail` property | Required | Purpose |
| --- | --- | --- |
| `driver` | Yes | Portable provider bridge with native client and capabilities. |
| `from` | Yes | Default address or `{ address, name }`. |
| `replyTo` | No | Default reply address, overridable per message. |

Messages accept `to`, `cc`, and `bcc` as one or many address inputs, optional sender/reply overrides, custom safe headers, and attachments with filename, MIME type, and byte/blob content. The total recipient ceiling is 50. Control/header injection and protected-header overrides fail before delivery.

## Provider bridge and capabilities

A provider exposes its native `client`, declares whether provider templates are supported, and sends a normalized rendered or provider-template message. `mail.client` and `mail.capabilities` preserve those escape hatches.

- Postmark supports rendered mail and provider `templateId`/exact-JSON `model`. Configure provider instances for the required message stream; an injected SDK client can be shared.
- Resend supports rendered mail and provider templates. Template variables are limited to strings and numbers and are validated before provider I/O.
- SMTP supports rendered mail and exposes the Nodemailer transport/`verify`; filesystem and URL attachment loading are disabled on the portable path.
- `CaptureMailProvider` from `@playstack/mail/testing` records normalized messages for tests. `messages()` returns the rendered/provider-template union; `rawMessages()` and `templateMessages()` return each side already narrowed, so a test can read `subject` and `text` without checking `kind` first.

```ts
import { CaptureMailProvider } from '@playstack/mail/testing'

const capture = new CaptureMailProvider()
const mail = createMail({ driver: capture, from: 'no-reply@example.com' })
await mail.send({ to: 'person@example.com', template: renderInvitation, props })
const [message] = capture.rawMessages()
expect(message?.text).toContain('/invitations/accept?token=')
```

## Results and failure policy

`send` returns provider message ID plus accepted/rejected recipients. Acceptance means submission, not inbox delivery. Input, unsupported-capability, and provider-rejection errors are non-retryable; rate-limit and definitive availability errors may be retried under application policy. `MailOutcomeUnknownError` means a send may already have succeeded, so reconcile before retrying.

SMTP success requires a nonempty message ID and explicit accepted/rejected arrays accounting for the requested recipients, with no foreign or conflicting evidence. An optional returned envelope must also match. Missing, malformed, mismatched, or partial evidence produces non-retryable uncertainty. `SmtpReceiptError` extends `MailOutcomeUnknownError` and exposes immutable indexed outcomes without recipient addresses. Complete explicit rejection is `MailRejectedError`; partial acceptance must never trigger a whole-message resend. A timeout or connection error can occur after submission and is not proof of rejection.

## Postmark attribution

The Postmark provider options support `tag` and `metadata`, forwarded as native `Tag` and `Metadata` for rendered and provider-template sends. Create a per-attempt provider around the same injected SDK client when attribution varies; no client-method wrapper is required. Tags allow up to 1,000 characters. Metadata allows up to ten fields, with keys up to 20 characters and values up to 80; case-insensitive duplicate keys are rejected before I/O. Attribution does not provide delivery idempotency.

## Boundary

The package submits immediately. Queueing, retry idempotency, provider webhooks, bounces, complaints, opens, and analytics belong to application workflows—typically `@playstack/queues`. Native provider-only attachments and administration remain available through `mail.client`.

{/* package-reference:start */}

## API entry points and requirements

Reference snapshot: `@playstack/mail@0.1.0-beta.1`. Import only the entry point your runtime needs. Paths below are relative to the installed package; use **Go to Definition** in your editor to inspect exact parameters, return types and overloads. Do not import the declaration-file paths directly.

| Public entry point | Declaration file |
| --- | --- |
| `@playstack/mail` | `./dist/index.d.ts` |
| `@playstack/mail/errors` | `./dist/errors.d.ts` |
| `@playstack/mail/mailgun` | `./dist/mailgun.d.ts` |
| `@playstack/mail/types` | `./dist/types.d.ts` |
| `@playstack/mail/testing` | `./dist/testing.d.ts` |
| `@playstack/mail/postmark` | `./dist/postmark.d.ts` |
| `@playstack/mail/resend` | `./dist/resend.d.ts` |
| `@playstack/mail/ses` | `./dist/ses.d.ts` |
| `@playstack/mail/sendgrid` | `./dist/sendgrid.d.ts` |
| `@playstack/mail/smtp` | `./dist/smtp.d.ts` |
| `@playstack/mail/package.json` | No TypeScript declaration (asset or metadata export). |

Node.js engine requirement: `>=20`. This is not a claim that every entry point works in browsers or Workers.

### Peer dependencies

Keep existing framework versions that satisfy these ranges. Install optional peers only when using the corresponding adapter. The package manager resolves ordinary dependencies separately.

| Peer | Compatible range | When needed |
| --- | --- | --- |
| `@aws-sdk/client-sesv2` | `^3.1119.0` | Optional; only for the entry points that use it. |
| `@sendgrid/mail` | `^8.1.6` | Optional; only for the entry points that use it. |
| `mailgun.js` | `^14.0.0` | Optional; only for the entry points that use it. |
| `nodemailer` | `^9.0.5` | Optional; only for the entry points that use it. |
| `postmark` | `^5.1.0` | Optional; only for the entry points that use it. |
| `resend` | `^6.24.0` | Optional; only for the entry points that use it. |

For a complete first program, start with [Getting started](/docs/getting-started). For API lookup and partial-example conventions, see [Reading the reference](/docs/packages#reading-the-reference). Provider failures, lifecycle requirements and application responsibilities remain described in the guide above; types alone do not establish production safety.

{/* package-reference:end */}
