Engines
Templates with layouts and typed payloads, a plain-text fallback generated from the HTML, and transport headers sanitised against injection. In development it prints to the terminal.
Setup
import { createMailer } from "yatta/mail"; export interface AppTemplates { welcome: { name: string; verifyUrl: string };} declare module "yatta/mail" { interface MailRegister { templates: AppTemplates; }} export const mailer = createMailer({ defaultFrom: "Yatta App <hello@yatta.dev>", mode: process.env.NODE_ENV === "production" ? "smtp" : "terminal",});terminal logs the rendered message instead of sending it, so local development needs no credentials.
Layouts
A layout wraps a template's body. Use {{{content}}} for the template, and {{name}} for an escaped value.
mailer.registerLayout( "default", `<!DOCTYPE html> <html> <body style="font-family:sans-serif;background:#fafafa;padding:20px;"> <div style="background:#fff;padding:24px;border-radius:8px;max-width:600px;margin:auto;"> {{{content}}} </div> </body> </html>`,);Note
{{value}} is HTML-escaped; {{{value}}} is raw. Always use the escaped form for anything a user supplied.Templates
mailer.registerTemplate<AppTemplates["welcome"]>("welcome", { subject: "Welcome to Yatta, {{name}}!", layout: "default", html: ` <h2>Welcome, {{name}}</h2> <p>Please confirm your email address:</p> <a href="{{{verifyUrl}}}">Verify Email</a> `,});Sending
// Using a registered template — data is type-checkedawait mailer.send({ to: "ada@example.com", template: "welcome", data: { name: "Ada", verifyUrl: "https://app.dev/verify?token=abc" },}); // Direct, without a templateawait mailer.send({ to: "ada@example.com", subject: "Your receipt", text: "Thanks for your order.", html: "<p>Thanks for your order.</p>",}); // Preview without sendingconst preview = await mailer.preview({ to: "ada@example.com", template: "welcome", data: { name: "Ada", verifyUrl: "https://app.dev/verify?token=abc" },});Pipe filters
Format values inline instead of pre-formatting them in JavaScript.
mailer.send({ to, subject: "Invoice {{ amount | currency:USD }}", html: `<p>Total: {{ amount | currency:USD }}</p>`,});Attachments and CC
await mailer.send({ to: "ada@example.com", cc: ["grace@example.com"], bcc: ["audit@example.com"], replyTo: "support@yatta.dev", subject: "Your report", html: "<p>Attached.</p>", attachments: [ { filename: "report.pdf", content: buffer }, { filename: "logo.png", path: "./assets/logo.png" }, ],});Transports
smtp— any SMTP relayresend,postmark,sendgrid— provider APIsses— AWS Simple Email Servicegmail— OAuth transportethereal— test messages with a preview URLterminal— print, for developmentmemory— capture, for tests
export const mailer = createMailer({ mode: "smtp", defaultFrom: "Yatta <noreply@yatta.dev>", provider: "postmark", // host, port and TLS come from the preset host: process.env.SMTP_HOST!, port: Number(process.env.SMTP_PORT ?? 587), auth: { user: process.env.SMTP_USER!, pass: process.env.SMTP_PASSWORD!, },});mode decides whether a message is actually transmitted (smtp, ethereal) or not (terminal, memory). provider is a preset for host, port and TLS. See Send real email for a full walkthrough.
Testing
In memory mode nothing is sent, and you can assert on what would have gone out.
const mailer = createMailer({ mode: "memory", defaultFrom: "T <t@test.dev>" }); await mailer.send({ to: "a@test.dev", subject: "Hi", text: "hello" }); expect(mailer.sentCount()).toBe(1);expect(mailer.lastSent()?.to).toBe("a@test.dev");expect(mailer.findSent((m) => m.subject === "Hi")).toHaveLength(1); mailer.clearSent();Sending from a job
jobs.handle("send-email", async ({ data }) => { await mailer.send({ to: data.to, subject: data.subject, text: data.body, });});