Each provider is its own entry point, so you only bundle the one you import. Every provider
exposes the same send() and is_error() methods: swap providers without changing your
calling code.
This page is the email roster. The other channels’ providers live with their channel: SMS (The SMS Works, Twilio, Amazon SNS), WhatsApp (Twilio, Meta Cloud API), Push (Web Push, FCM), and the chat platforms — Slack, Discord, Teams, Telegram — each on its own page.
The environment variables are what postboi init writes and the
zero-config mail() reads; the constructor options are the same values passed
explicitly when you construct a provider directly. Non-secret
bits (a Mailgun domain, an SES region) can instead live in postboi.config.ts;
keep the secrets in the environment.
Want another provider? Quit being a baby and open a PR.
Using a provider directly
Useful when you want an explicit instance, or credentials that don’t come from the environment:
Every provider also accepts default, timeout, retries, retry_delay, auto_text,
and hooks on top of its own credentials. See Common constructor options.
Mock provider
postboi/mock records messages in-memory instead of sending them, with the same normalisation as a
real provider. Perfect for tests.
Pass log: true to print each message as it is captured — subject, addresses, attachment
names, and the body in full, so a magic link or confirmation code is readable in the
terminal. It is off by default so test suites stay quiet.
mail() turns it on whenever it resolves the mock itself, which happens two ways:
- No credential in development — see below.
provider: 'mock'inpostboi.config.tsorPOSTBOI_PROVIDER. Choosing the mock is choosing not to send, so printing is the only way to observe it.
Constructing new Mock() yourself stays silent — that is the test path, where the point is
asserting on sent rather than printing to the run.
No credential yet
A fresh clone has no token, and that is the normal state of a checkout. Rather than
failing, mail() logs the message to the console and carries on, so sign-in links and
receipts are readable and app code needs no if (token) branch around every send:
This only happens when the environment identifies itself as development
(NODE_ENV=development). Anywhere else a missing credential throws no_token or no_provider — including when the environment is unset or
unrecognised. The asymmetry is deliberate: a real deploy that lost its secret must fail
loudly, because mail that silently becomes a console line locks people out with no error
anywhere. NODE_ENV=test throws too, so suites keep asserting the real failure.
Custom headers & tags
headers and tags on send() are forwarded to each provider’s
native concept, and quietly ignored where unsupported:
- headers → Resend, Postmark, SendGrid, Mailgun (
h:), Brevo, SparkPost, Mandrill, Plunk, Mailtrap, Scaleway, Cloudflare, SES, Mailjet, Elastic Email. - tags → SendGrid (categories), Mailgun (
o:tag), Brevo, MailerSend, Mandrill, MailPace, SES (EmailTags), Resend ({name,value}pairs). Postmark and Mailtrap use the first tag only.
The same forwarded-where-supported convention applies to scheduled_at, tracking and unsubscribe_url, and most providers’ delivery
events can be received and normalized too, see Webhooks.