Search docs

Jump between documentation pages.

Browse docs

Email integrations

DaloyJS doesn't ship its own email transport. Send mail by registering a small plugin that decorates app.state with a provider client, then call it from your route handlers. The pages in this section show how to wire up the six most common transactional email providers using their official Node SDKs.

One EmailSender, six providers
your appEmailSender interfacesend(msg): Promise<{ id }>
AWSAWS SES (SESv2)@aws-sdk/client-sesv2
TwilioSendGrid@sendgrid/mail
edge-readyResendresend
inbox-firstPostmarkpostmark
SinchMailgunmailgun.js
sandboxMailtrapmailtrap
One tiny plugin decorates app.state with a provider client behind a shared EmailSender contract, so route handlers call state.email.send() and never know which provider is wired in. Swapping providers is a one-file change.

Supported providers

  • AWS SES (SESv2): pay-as-you-go SMTP/HTTP at AWS scale via @aws-sdk/client-sesv2. Best fit when you already run on AWS or need the cheapest per-message price.
  • SendGrid: Twilio's established sender via @sendgrid/mail. Good for high-volume marketing plus transactional.
  • Resend: modern, developer-first API via the resend SDK. Great DX, React Email templating, edge-friendly.
  • Postmark: transactional-first delivery via the postmark SDK. Known for very high inbox placement.
  • Mailgun: Sinch-backed sender via mailgun.js. Strong validation, routing, and EU/US regions.
  • Mailtrap: sandbox + production sending via the mailtrap SDK. Switch between a test inbox and live sending with a single flag.

Runtime compatibility at a glance

Most provider SDKs are HTTPS-based and work on every runtime DaloyJS targets, but a few depend on Node-only APIs (filesystem, TCP, AWS Signature V4 with NodeHttpHandler) and won't run on Cloudflare Workers or Vercel without adjustments.

ProviderNode / Bun / DenoCloudflare WorkersVercelAWS Lambda
AWS SES (SESv2)YesWith fetch handler & static credsWith fetch handler & static credsYes (IAM role)
SendGridYesUse Web API via fetch (SDK is Node-oriented)Use Web API via fetchYes
ResendYesYesYesYes
PostmarkYesCall REST via fetch (SDK uses axios)Call REST via fetchYes
MailgunYesYes (enable useFetch: true in v12.1+)Yes (useFetch: true)Yes
MailtrapYesCall REST via fetch (SDK uses Node features)Call REST via fetchYes

Common pattern

Every guide in this section follows the same three steps: install the SDK, register a DaloyJS plugin that puts the client on app.state, then call it inside a validated route handler. The plugin shape is intentionally tiny so you can swap providers without touching business logic:

Wire it up once
  1. 01step 1Install the SDKpnpm add <provider-sdk>
  2. 02step 2Register the pluginapp.decorate("email", sender)
  3. 03step 3Call from a routestate.email.send(msg)
Every guide in this section follows the same three steps. The plugin puts the client on app.state, then validated route handlers send through it.
// src/plugins/email.ts
import type { App } from "@daloyjs/core";

export interface EmailMessage {
  to: string;
  subject: string;
  text?: string;
  html?: string;
}

export interface EmailSender {
  send(msg: EmailMessage): Promise<{ id: string }>;
}

export function emailPlugin(sender: EmailSender) {
  return {
    name: "email",
    register(app: App) {
      app.decorate("email", sender);
    },
  };
}

declare module "@daloyjs/core" {
  interface AppState {
    email: EmailSender;
  }
}

Each provider page implements EmailSender with the official SDK so the rest of your app stays provider-agnostic.

Security checklist

  • Keep API keys in environment variables. Never commit them. Use AWS IAM roles on Lambda and platform-managed secrets on Vercel, Cloudflare, Fly, and Render.
  • Verify your sending domain. Add SPF, DKIM, and DMARC records before going live; every provider here rejects unverified senders in production.
  • Validate inputs and reject CRLF in header fields. Treat to, subject, display names, reply_to, and any custom header value as untrusted. Use DaloyJS validation with z.string().email() for addresses and reject \r / \n in every other header-bound string. This is the classic SMTP / email header injection vector (Snyk's 2022 write-up Avoiding SMTP Injection documents library-level CVEs that were fixed upstream, but the application-level rule still applies whenever you forward user input into mail headers). A reusable schema looks like:
// src/schemas/email.ts
import { z } from "zod";

// Reject CR, LF, NUL - the SMTP / email header injection trio.
const headerSafe = z
  .string()
  .max(998) // RFC 5322 line length
  .regex(/^[^\r\n\u0000]+$/, "CRLF / NUL not allowed in header fields");

export const SendEmailBody = z
  .object({
    to: z.string().email(),
    subject: headerSafe,
    fromName: headerSafe.optional(),
    replyTo: z.string().email().optional(),
    text: z.string().max(50_000),
  })
  .strict();
  • Prefer HTTPS provider SDKs over raw SMTP libraries. The six providers documented here all speak HTTPS+JSON, so the CRLF-in-SMTP-command class of bug (smtp-client, smtp-channel, aiosmtplib source_address) doesn't reach the wire. If you must use a raw SMTP client, validate every field, including hostname and source address, with the schema above.
  • Rate-limit the send route. Use the built-in rateLimit middleware (or the Redis store) on any endpoint that triggers email so abuse can't drive your bill or reputation down.
  • Verify provider webhooks. If you process bounces, complaints, or opens, verify the signature on every incoming webhook before trusting its payload.