Engines

Mail

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

yatta/func/mail.tsts
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.

tsts
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

tsts
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

sendts
// 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.

Filtersts
mailer.send({  to,  subject: "Invoice {{ amount | currency:USD }}",  html: `<p>Total: {{ amount | currency:USD }}</p>`,});

Attachments and CC

tsts
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 relay
  • resend, postmark, sendgrid — provider APIs
  • ses — AWS Simple Email Service
  • gmail — OAuth transport
  • ethereal — test messages with a preview URL
  • terminal — print, for development
  • memory — capture, for tests
SMTPts
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.

memory modets
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

yatta/func/workers.tsts
jobs.handle("send-email", async ({ data }) => {  await mailer.send({    to: data.to,    subject: data.subject,    text: data.body,  });});