Skip to content

Email

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 the provider package for your email service:

Terminal window
yarn add nodemailer

If you want to use React Email templates, also install:

Terminal window
yarn add @react-email/render react

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 {}
OptionTypeRequiredDescription
provider'resend' | 'smtp'YesEmail provider to use
from{ name: string; email: string }YesDefault sender address
queueQueueNameYesQueue name for email dispatch (must be registered with QueueModule.registerQueue)
apiKeystringIf ResendResend API key
smtpSmtpConfigIf SMTPSMTP connection settings
replyTostringNoDefault reply-to address

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',
})

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',
})
OptionTypeRequiredDescription
hoststringYesSMTP server hostname
portnumberYesSMTP server port
securebooleanNoUse TLS
usernamestringNoAuth username
passwordstringNoAuth password

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:

FieldTypeRequiredDescription
tostring | string[]YesRecipient email(s)
from{ name?: string; email: string }NoSender (defaults to module config)
subjectstringYesEmail subject (1-500 characters)
htmlstringOne of html, text, or templateHTML content
textstringOne of html, text, or templatePlain text content
templateReactElementOne of html, text, or templateReact Email template
ccstring[]NoCarbon copy recipients
bccstring[]NoBlind carbon copy recipients
replyTostringNoReply-to address
attachmentsEmailAttachment[]NoEmail attachments
metadataRecord<string, unknown>NoCustom metadata for tracking

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:

templates/welcome.tsx
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>
)
}

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.

The email module supports two types of 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',
},
],
})

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.

The email module uses the queue system internally. Here is how the flow works:

  1. You call emailService.send() in your request handler.
  2. The email service renders any React template and dispatches a queue message with type email.send.
  3. The EmailConsumer (auto-registered by the module) picks up the message.
  4. The consumer resolves attachments (decodes inline base64 or downloads from storage).
  5. The consumer creates the configured email provider and sends the email.
  6. 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.

The email module uses focused error classes that extend ApplicationError. Each error has a localized i18n message key.

Error classWhen thrown
ResendApiKeyMissingErrorResend API key is not configured
SmtpConfigurationMissingErrorSMTP configuration not provided
SmtpHostMissingErrorSMTP host is missing from SmtpConfig
EmailProviderNotSupportedErrorUnsupported provider value configured
Error classWhen thrown
EmailSmtpConnectionFailedErrorSMTP server connection failed
EmailResendApiFailedErrorResend API returned an error

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.

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:

Terminal window
# Start Mailpit
docker run -d --name mailpit -p 8025:8025 -p 1025:1025 axllent/mailpit
# Or via Docker Compose if you have it in your docker-compose.yml
docker compose up -d

Then 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.

  • 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.