Stratal provides an EmailModule for sending transactional emails asynchronously through a queue. Email is delivered by a built-in SMTP client that runs over cloudflare:sockets, so there are zero runtime dependencies for sending. Point the module at any SMTP endpoint (Resend, Postmark, SendGrid, Mailgun, or a self-hosted server) and it just works. The module supports React Email templates, batch sending, and both inline and storage-based attachments. Emails are dispatched to a queue and processed by a built-in consumer, keeping your request handlers fast.
Install
Section titled “Install”Sending plain html or text email needs no extra packages. The SMTP client is built in.
If you want to render React Email templates, install the renderer and React:
yarn add @react-email/render reactConfiguration
Section titled “Configuration”The EmailModule requires the QueueModule, since emails are dispatched through a queue. Register the queue, then configure the email module with your SMTP URL:
import { Module } from 'stratal/module'import { DI_TOKENS } from 'stratal/di'import type { StratalEnv } from 'stratal'import { QueueModule } from 'stratal/queue'import { EmailModule } from 'stratal/email'
@Module({ imports: [ QueueModule.forRootAsync({ inject: [DI_TOKENS.CloudflareEnv], useFactory: (env: StratalEnv) => ({ provider: env.ENVIRONMENT === 'test' ? 'sync' : 'cloudflare', }), }), QueueModule.registerQueue('email-queue'), EmailModule.forRootAsync({ inject: [DI_TOKENS.CloudflareEnv], useFactory: (env: StratalEnv) => ({ from: { name: 'My App', email: 'noreply@example.com' }, smtp: { url: env.SMTP_URL }, queue: 'email-queue', }), }), ],})export class AppModule {}Store the SMTP URL as a secret (for example SMTP_URL) rather than hardcoding credentials.
SMTP URL format
Section titled “SMTP URL format”The smtp.url carries the host, port, and credentials. The scheme selects the transport security:
smtp://user:pass@host:portUpgrades a plaintext connection to TLS with STARTTLS. Default port 587.
smtps://user:pass@host:portOpens a TLS connection immediately. Default port 465.
Pointing at a hosted provider is a matter of using its SMTP endpoint and credentials in the URL:
# ResendSMTP_URL="smtp://resend:re_xxxxxxxx@smtp.resend.com:587"
# PostmarkSMTP_URL="smtp://<token>:<token>@smtp.postmarkapp.com:587"
# SendGridSMTP_URL="smtp://apikey:SG.xxxxxxxx@smtp.sendgrid.net:587"EmailModuleOptions
Section titled “EmailModuleOptions”| Option | Type | Required | Description |
|---|---|---|---|
from | { name: string; email: string } | Yes | Default sender address |
smtp | { url: string } | Yes | SMTP connection URL (smtp:// or smtps://) |
queue | QueueBinding | Yes | Queue binding for email dispatch (must be registered with QueueModule.registerQueue) |
replyTo | string | No | Default reply-to address |
Sending emails
Section titled “Sending emails”Inject EmailService and call send:
import { Transient, inject } from 'stratal/di'import { EMAIL_TOKENS, type EmailService } from 'stratal/email'
@Transient()export class NotificationService { constructor( @inject(EMAIL_TOKENS.EmailService) private readonly email: EmailService, ) {}
async sendWelcome(userEmail: string, userName: string) { await this.email.send({ to: userEmail, subject: `Welcome, ${userName}!`, html: `<h1>Welcome to our platform, ${userName}!</h1>`, }) }}The send method accepts the following fields:
| Field | Type | Required | Description |
|---|---|---|---|
to | string | string[] | Yes | Recipient email(s) |
from | { name?: string; email: string } | No | Sender (defaults to module config) |
subject | string | Yes | Email subject (1-500 characters) |
html | string | One of html, text, or template | HTML content |
text | string | One of html, text, or template | Plain text content |
template | ReactElement | One of html, text, or template | React Email template |
cc | string[] | No | Carbon copy recipients |
bcc | string[] | No | Blind carbon copy recipients |
replyTo | string | No | Reply-to address |
attachments | EmailAttachment[] | No | Email attachments |
metadata | Record<string, unknown> | No | Custom metadata for tracking |
React Email templates
Section titled “React Email templates”Instead of writing raw HTML, you can use React Email components. Pass a React element as the template prop:
import React from 'react'import { WelcomeEmail } from './templates/welcome'
await this.email.send({ to: 'user@example.com', subject: 'Welcome!', template: React.createElement(WelcomeEmail, { name: 'Alice' }),})The template is rendered to HTML before being dispatched to the queue. Define your templates as regular React components:
import { Html, Body, Heading, Text } from '@react-email/components'
interface WelcomeEmailProps { name: string}
export function WelcomeEmail({ name }: WelcomeEmailProps) { return ( <Html> <Body> <Heading>Welcome, {name}!</Heading> <Text>We are glad to have you on board.</Text> </Body> </Html> )}Batch sending
Section titled “Batch sending”Send multiple emails in one call using sendBatch:
await this.email.sendBatch({ messages: [ { to: 'user1@example.com', subject: 'Notification', text: 'Hello user 1', }, { to: 'user2@example.com', subject: 'Notification', text: 'Hello user 2', }, ],})Each message is dispatched as a separate queue entry with message type email.send. The batch can contain up to 100 messages.
Attachments
Section titled “Attachments”The email module supports two types of attachments. Each attachment is hard-capped at 20 MB: it is fully buffered before base64 encoding, and exceeding the cap throws.
Inline attachments
Section titled “Inline attachments”Provide the file content as a base64-encoded string:
await this.email.send({ to: 'user@example.com', subject: 'Your invoice', text: 'Please find your invoice attached.', attachments: [ { filename: 'invoice.pdf', content: base64EncodedPdf, contentType: 'application/pdf', }, ],})Storage attachments
Section titled “Storage attachments”Reference a file stored in the Storage module:
await this.email.send({ to: 'user@example.com', subject: 'Your report', text: 'Please find your report attached.', attachments: [ { filename: 'report.pdf', storageKey: 'reports/2026/02/monthly-report.pdf', disk: 'documents', }, ],})The EmailConsumer downloads the file from storage at processing time and attaches it to the email.
Queue integration
Section titled “Queue integration”The email module uses the queue system internally. Here is how the flow works:
- You call
emailService.send()in your request handler. - The email service renders any React template and dispatches a queue message with type
email.send. - The
EmailConsumer(auto-registered by the module) picks up the message. - The consumer resolves attachments (decodes inline base64 or downloads from storage).
- The consumer sends the message over the built-in SMTP client.
- On success, the message is acknowledged. On failure, it is retried.
You do not need to register the EmailConsumer yourself. It is included automatically when you configure the EmailModule.
Retry logic
Section titled “Retry logic”Email dispatch uses Cloudflare Queues’ built-in retry mechanism:
- Max retries: 3 attempts with automatic exponential backoff.
- Dead letter queue: Messages that fail all retries are routed to the configured DLQ for manual inspection.
Configure these options in your wrangler.jsonc consumer entry. See the Queues wrangler configuration for the full example.
Local testing with Mailpit
Section titled “Local testing with Mailpit”Mailpit provides a local SMTP server with a web UI for inspecting captured emails. Run Mailpit with Docker and point the email module at it:
# Start Mailpitdocker run -d --name mailpit -p 8025:8025 -p 1025:1025 axllent/mailpit
# Or via Docker Compose if you have it in your docker-compose.ymldocker compose up -dMailpit listens for plaintext SMTP on port 1025, so use an smtp:// URL:
EmailModule.forRoot({ from: { name: 'App', email: 'noreply@example.com' }, smtp: { url: 'smtp://localhost:1025' }, queue: 'email-queue',})Open http://localhost:8025 to view captured emails in the Mailpit web UI. All emails sent through your application will appear there instead of being delivered to real recipients.
Next steps
Section titled “Next steps”- Queues for details on queue configuration and consumers.
- Storage for setting up file storage used by storage-based attachments.
- Internationalization for translating email subjects and content.