---
title: Mail
description: Send emails with one call. Six providers, React templates, and a local inbox you can read in your browser.
---

`@zap-ts/mail` gives the app one way to send an email. You write the body of
the email in React. You call `sendEmail`. The package sends it.

Six providers can do the sending: SMTP, console, Resend, Postmark, SendGrid
and Brevo. A provider is the service that delivers the email. You pick one
with a setting. Your code never names it.

A fresh clone sends every email to your own machine. Nothing goes to a real
person.

## Send an email

The package exports two things you need.

- `EmailLive` builds the email client from your settings.
- `sendEmail` renders the React body and sends it.

```tsx
import { EmailLive, sendEmail } from "@zap-ts/mail";
import { Effect } from "effect";

const program = sendEmail({
  to: "user@example.com",
  subject: "Welcome!",
  react: <p>Thanks for joining us.</p>,
});

await Effect.runPromise(program.pipe(Effect.provide(EmailLive)));
```

You do not pass the sender address. It comes from `MAIL_FROM`.

If a send fails, the package tries again, up to three times. It waits a little
longer before each try. If all tries fail, it writes a warning and stops. The
rest of your code keeps working. So a broken email never breaks the page the
user is on.

If you need to know that an email went out, read the log. The call itself does
not tell you.

Every provider is reached over HTTP. So the same code runs on Node and on
Cloudflare Workers.

## Templates

A template is a function that returns one ready-made email. The templates live
in `@zap-ts/mail/templates`. Each one returns `{ subject, react }`. That is
what `sendEmail` needs, apart from the address.

```ts
import { verifyEmail } from "@zap-ts/mail/templates";

const { subject, react } = verifyEmail("https://app.example.com/api/auth/verify-email?token=1");
```

Six templates are about sign-in and accounts: `verifyEmail`,
`resetPasswordEmail`, `deleteAccountEmail`, `magicLinkEmail`,
`signInCodeEmail` and `invitationEmail`.

One template is about payments: `lifecycleEmail`. You pass it an event. The
event is `trial_will_end`, `payment_failed` or `canceled`. The template picks
the right body for that event.

You do not have to call these yourself. `@zap-ts/authentication` already sends
the sign-in emails. `@zap-ts/billing` already sends the payment ones.

The templates that ship are plain. They have no design and no logo. Replace
them with your own. Keep the names of the exported functions, because the rest
of the code imports those names.

## See a template in your browser

`apps/emails` is a small app that shows the templates. Each file in
`apps/emails/emails` loads one template and draws it. You edit the real
template and see the change at once.

Start it with:

```bash
pnpm run dev:emails
```

It runs on port 3004.

## Pick a provider

The `MAIL_PROVIDER` setting picks the provider when the app starts. Two of the
six need no account:

- `smtp` sends to Mailpit. Mailpit is a fake mail server for your machine. It
  starts with `docker compose up`. It takes mail on port 1025. You read the
  mail at `http://localhost:8025`.
- `console` prints the email instead of sending it. It prints the addresses,
  the subject and the body. Tests use this.

The other four need a key. A key is a secret string from the provider. If the
key is missing, the app fails when it starts. It does not fail later, in the
middle of a send.

| `MAIL_PROVIDER` | What it reads            |
| --------------- | ------------------------ |
| `smtp`          | `SMTP_HOST`, `SMTP_PORT` |
| `console`       | nothing                  |
| `resend`        | `RESEND_API_KEY`         |
| `postmark`      | `POSTMARK_SERVER_TOKEN`  |
| `sendgrid`      | `SENDGRID_API_KEY`       |
| `brevo`         | `BREVO_API_KEY`          |

To change provider, change one setting and add one key. You do not change any
import.

## Environment

These values ship in `.env.schema`:

```bash
MAIL_PROVIDER=smtp
MAIL_FROM=zap.ts <noreply@zapstudio.dev>
SMTP_HOST=127.0.0.1
SMTP_PORT=1025
```

`RESEND_API_KEY`, `POSTMARK_SERVER_TOKEN`, `SENDGRID_API_KEY` and
`BREVO_API_KEY` are there too, but they are empty. Each one has a link to the
documentation of that provider. Fill in the one you picked.

See [Environment](/guides/environment) to learn how varlock turns these lines
into the typed `ENV` that the code reads.

:::note
Change `MAIL_PROVIDER` before you go live. Mailpit only runs on your machine.
And `console` throws the email away.
:::

## Related

- [Authentication](/packages/authentication) — the sign-in emails
- [Billing](/packages/billing) — the payment emails
- [Environment](/guides/environment) — the settings the package reads
- [Local development](/guides/local-development) — what `docker compose up` starts
