Stratal provides an EmailModule for sending transactional emails asynchronously through a queue. It supports multiple providers (Resend and SMTP), 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 dependencies
Section titled “Install dependencies”Install the provider package for your email service:
yarn add nodemaileryarn add resendIf you want to use React Email templates, also install:
yarn add @react-email/render reactConfiguration
Section titled “Configuration”The EmailModule requires the QueueModule to be configured first, since emails are dispatched through a queue. Register the queue, then configure the email module:
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) => ({ provider: 'resend', apiKey: env.RESEND_API_KEY, from: { name: 'My App', email: 'noreply@example.com' }, queue: 'email-queue', }), }), ],})export class AppModule {}EmailModuleOptions
Section titled “EmailModuleOptions”| Option | Type | Required | Description |
|---|---|---|---|
provider | 'resend' | 'smtp' | Yes | Email provider to use |
from | { name: string; email: string } | Yes | Default sender address |
queue | QueueName | Yes | Queue name for email dispatch (must be registered with QueueModule.registerQueue) |
apiKey | string | If Resend | Resend API key |
smtp | SmtpConfig | If SMTP | SMTP connection settings |
replyTo | string | No | Default reply-to address |
Resend provider
Section titled “Resend provider”Set provider to 'resend' and provide your API key:
EmailModule.forRoot({ provider: 'resend', apiKey: env.RESEND_API_KEY, from: { name: 'My App', email: 'noreply@example.com' }, queue: 'email-queue',})SMTP provider
Section titled “SMTP provider”Set provider to 'smtp' and provide connection settings:
EmailModule.forRoot({ provider: 'smtp', smtp: { host: 'smtp.example.com', port: 587, secure: false, username: env.SMTP_USERNAME, password: env.SMTP_PASSWORD, }, from: { name: 'My App', email: 'noreply@example.com' }, queue: 'email-queue',})SmtpConfig
Section titled “SmtpConfig”| Option | Type | Required | Description |
|---|---|---|---|
host | string | Yes | SMTP server hostname |
port | number | Yes | SMTP server port |
secure | boolean | No | Use TLS |
username | string | No | Auth username |
password | string | No | Auth password |
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:
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 creates the configured email provider and sends the email.
- 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.
Error handling
Section titled “Error handling”The email module uses focused error classes that extend ApplicationError. Each error has a localized i18n message key.
Configuration errors
Section titled “Configuration errors”| Error class | When thrown |
|---|---|
ResendApiKeyMissingError | Resend API key is not configured |
SmtpConfigurationMissingError | SMTP configuration not provided |
SmtpHostMissingError | SMTP host is missing from SmtpConfig |
EmailProviderNotSupportedError | Unsupported provider value configured |
Runtime errors
Section titled “Runtime errors”| Error class | When thrown |
|---|---|
EmailSmtpConnectionFailedError | SMTP server connection failed |
EmailResendApiFailedError | Resend API returned an error |
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.
Testing
Section titled “Testing”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 configure your email module to use 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 -dThen configure your email module to use Mailpit as the SMTP provider:
EmailModule.forRoot({ provider: 'smtp', smtp: { host: 'localhost', port: 1025 }, from: { name: 'App', email: 'noreply@example.com' }, 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.