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.
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
resendSDK. Great DX, React Email templating, edge-friendly. - Postmark: transactional-first delivery via the
postmarkSDK. 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
mailtrapSDK. 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.
| Provider | Node / Bun / Deno | Cloudflare Workers | Vercel | AWS Lambda |
|---|---|---|---|---|
| AWS SES (SESv2) | Yes | With fetch handler & static creds | With fetch handler & static creds | Yes (IAM role) |
| SendGrid | Yes | Use Web API via fetch (SDK is Node-oriented) | Use Web API via fetch | Yes |
| Resend | Yes | Yes | Yes | Yes |
| Postmark | Yes | Call REST via fetch (SDK uses axios) | Call REST via fetch | Yes |
| Mailgun | Yes | Yes (enable useFetch: true in v12.1+) | Yes (useFetch: true) | Yes |
| Mailtrap | Yes | Call REST via fetch (SDK uses Node features) | Call REST via fetch | Yes |
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:
- 01step 1Install the SDKpnpm add <provider-sdk>
- 02step 2Register the pluginapp.decorate("email", sender)
- 03step 3Call from a routestate.email.send(msg)
// 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 withz.string().email()for addresses and reject\r/\nin 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,aiosmtplibsource_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.