@setu-ts/mail-plugin
v0.11.0
Published
Email over log, SMTP, SES, or SendGrid, with a zero-dependency template engine
Readme
@setu-ts/mail-plugin
Transactional email. Registers an IMailer under CAPABILITIES.MAIL ('mail').
Four providers ship: LogProvider (zero-dependency default), SmtpProvider (over nodemailer),
SesProvider (AWS SESv2), and SendGridProvider (SendGrid v3 HTTP API over fetch, so it is
Workers-portable).
Installation
import { MailPlugin } from '@setu-ts/mail-plugin';Usage
import { MailPlugin } from '@setu-ts/mail-plugin';
import { CAPABILITIES, type IMailer } from '@setu-ts/common';
app.register(MailPlugin({
provider: 'sendgrid',
options: { apiKey: process.env.SENDGRID_API_KEY! },
defaults: { from: '[email protected]' },
templates: {
welcome: { html: '<p>Hello {{ name }}</p>' },
},
}));
const mailer = app.services.get<IMailer>(CAPABILITIES.MAIL);
await mailer.send({ to: '[email protected]', subject: 'Hi', text: 'Hello' });
// `subject` is required on sendTemplate; the template supplies the body.
await mailer.sendTemplate('welcome', { to: '[email protected]', subject: 'Welcome' }, {
name: 'Ada',
});Options
| Option | Type | Default | Description |
| ----------- | ---------------------------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| provider | 'log' \| 'smtp' \| 'ses' \| 'sendgrid' | 'log' | Backend. |
| options | MailProviderOptions | — | Provider-specific configuration. |
| defaults | { from?: string } | — | Applied when a message omits the field. |
| templates | Record<string, MailTemplate> | — | Named bodies available to sendTemplate: {{ variable }} strings, or view components rendered through CAPABILITIES.VIEW (see Templates). |
LogProvider.messages retains the most recent 1,000 outgoing messages in send order. The optional
sink still receives every message, including those later dropped from that retained tail.
Runtime support
SmtpProvider needs raw sockets, so it runs on Node/Deno/Bun only. LogProvider, SesProvider,
and SendGridProvider work on every runtime including Cloudflare Workers.
Templates
A template is one of two arms, and the two never mix in one template (a template carrying both
view and html is a compile error).
String templates render named {{ variable }} placeholders. The html body is
HTML-escaped; a missing variable or an unknown template throws rather than rendering an
empty string.
Component templates ({ view, text? }) render through the view engine registered under
CAPABILITIES.VIEW — a JSX component, an html tagged template, or a plain (props) => string
function — with sendTemplate's data passed to each component verbatim as its props. view
renders the HTML body; the optional text renders the plain-text body and is used verbatim, so
write it as a plain (props) => string function — a text component written with the html tag or
JSX is HTML-escaped like any other, which puts entities (&) into a plain-text mail.
Configuring one requires a CAPABILITIES.VIEW provider: MailPlugin refuses at register()
otherwise, naming both remedies, so the failure is a startup failure rather than a throw on the
first send.
import { createApplication } from '@setu-ts/kernel';
import { RuntimePlugin } from '@setu-ts/runtime';
import { ViewPlugin } from '@setu-ts/view-plugin';
import { MailPlugin } from '@setu-ts/mail-plugin';
import { CAPABILITIES, type IMailer } from '@setu-ts/common';
import { html } from 'hono/html';
interface WelcomeProps {
readonly name: string;
readonly plan: string;
}
// The `html` tag escapes `name` and `plan`; a JSX component would too.
const WelcomeHtml = (p: WelcomeProps) =>
html`
<h1>Welcome ${p.name}</h1>
<p>You are on the ${p.plan} plan.</p>
`;
const WelcomeText = (p: WelcomeProps) => `Welcome ${p.name}. You are on the ${p.plan} plan.`;
const app = createApplication({
plugins: [
RuntimePlugin(),
ViewPlugin({ engine: 'hono-html' }),
MailPlugin({
defaults: { from: '[email protected]' },
templates: {
welcome: { view: WelcomeHtml, text: WelcomeText },
},
}),
],
});
await app.start();
const mailer = app.services.get<IMailer>(CAPABILITIES.MAIL);
await mailer.sendTemplate('welcome', { to: '[email protected]', subject: 'Welcome' }, {
name: 'Ada',
plan: 'team',
});Two things differ from the string arm. Escaping is the rendering runtime's: an html template and a
JSX component escape their interpolations, while a hand-written template literal does not (the same
caveat IViewEngine.render carries for a page). And there is no missing-key check: a component
reads whatever it reads, so a key absent from data renders as undefined rather than throwing.
The committed sendTemplate signature types data as Record<string, unknown>; for compile-time
props, render by hand — engine.render(WelcomeHtml, props) on the engine resolved from
CAPABILITIES.VIEW, then mailer.send({ ..., html }).
TemplateEngine.render is asynchronous for both arms, because a view component's render may be.
Health indicator
Registered under the mail capability. Since M70c it reports two signals: the provider's lifecycle
(isReady()) and its reachability (isHealthy()).
| Status | Meaning |
| ------ | ------------------------------------------------------------------------------------------ |
| up | The provider is connected and reachable, or cannot be probed (reachable is 'unknown'). |
| down | The provider is not connected, or is connected but unreachable. |
data reports { provider, reachable }, where reachable is true, false, or 'unknown' when
the provider has no liveness check (e.g. the log provider always reports true).
Readiness (M119b, V10-49). The indicator is registered outside readiness by default. Its
status mapping is unchanged: a mail backend that is unreachable still reads down, so /health
shows the outage (503) and alerts still fire — but /ready ignores the indicator and stays 200,
because an SMTP outage is shared by every replica and does not stop request serving. An application
that wants a mail outage to drain its replicas restores the previous behaviour with
HealthPlugin({ readiness: { include: ['mail'] } }).
Exports
| Export | Kind |
| ------------------------- | --------- |
| adaptNodemailerModule | function |
| adaptSesModule | function |
| createProvider | function |
| escapeHtml | function |
| loadNodemailerModule | function |
| loadSesModule | function |
| MailPlugin | function |
| toNodemailerMessage | function |
| toSendGridBody | function |
| toSesInput | function |
| validateSesClient | function |
| validateSmtpTransport | function |
| LogProvider | class |
| MailService | class |
| SendGridProvider | class |
| SesProvider | class |
| SmtpProvider | class |
| TemplateEngine | class |
| IMailer | interface |
| ISesClient | interface |
| ISmtpTransport | interface |
| LogProviderOptions | interface |
| MailComponentTemplate | interface |
| MailMessage | interface |
| MailPluginOptions | interface |
| MailProviderOptions | interface |
| MailServiceOptions | interface |
| MailStringTemplate | interface |
| NodemailerModule | interface |
| RenderedTemplate | interface |
| SendGridProviderOptions | interface |
| SesProviderOptions | interface |
| SesSdkModule | interface |
| SmtpProviderOptions | interface |
| IMailHttp | type |
| MailProviderType | type |
| MailTemplate | type |
| OutgoingMail | type |
Generated from the package barrel by pnpm run docs:exports; pnpm run check:docs fails when it
drifts.
Full API
Every export and option is documented in PUBLIC_API.md.
