Worker Mailer is an SMTP client that runs on Cloudflare Workers. It leverages Cloudflare TCP Sockets and doesn't rely on any other dependencies.
- 🚀 Completely built on the Cloudflare Workers runtime with no other dependencies
- 📝 Full TypeScript type support
- 📧 Supports sending plain text and HTML emails with attachments
- 🔒 Supports multiple SMTP authentication methods:
plain
,login
, andCRAM-MD5
- 📅 DSN support
npm i worker-mailer
- Configure your
wrangler.toml
:
compatibility_flags = ["nodejs_compat"]
# or compatibility_flags = ["nodejs_compat_v2"]
- Use in your code:
import { WorkerMailer } from 'worker-mailer'
// Connect to SMTP server
const mailer = await WorkerMailer.connect({
credentials: {
username: 'bob@acme.com',
password: 'password',
},
authType: 'plain',
host: 'smtp.acme.com',
port: 587,
secure: true,
})
// Send email
await mailer.send({
from: { name: 'Bob', email: 'bob@acme.com' },
to: { name: 'Alice', email: 'alice@acme.com' },
subject: 'Hello from Worker Mailer',
text: 'This is a plain text message',
html: '<h1>Hello</h1><p>This is an HTML message</p>',
})
- Using with modern JavaScript frameworks (Next.js, Nuxt, SvelteKit, etc.)
When working with frameworks that use Node.js as their development runtime, you'll need to handle the fact that Cloudflare Workers-specific APIs (like cloudflare:sockets
) aren't available during local development.
The recommended approach is to use conditional dynamic imports. Here's an example for Nuxt.js:
export default defineEventHandler(async event => {
// Check if running in development environment
if (import.meta.dev) {
// Development: Use nodemailer (or any Node.js compatible email library)
const nodemailer = await import('nodemailer')
const transporter = nodemailer.default.createTransport()
return await transporter.sendMail()
} else {
// Production: Use worker-mailer in Cloudflare Workers environment
const { WorkerMailer } = await import('worker-mailer')
const mailer = await WorkerMailer.connect()
return await mailer.send()
}
})
This pattern ensures your application works seamlessly in both development and production environments.
Creates a new SMTP connection.
type WorkerMailerOptions = {
host: string // SMTP server hostname
port: number // SMTP server port (usually 587 or 465)
secure?: boolean // Use TLS (default: false)
startTls?: boolean // Upgrade to TLS if SMTP server supports (default: true)
credentials?: {
// SMTP authentication credentials
username: string
password: string
}
authType?:
| 'plain'
| 'login'
| 'cram-md5'
| Array<'plain' | 'login' | 'cram-md5'>
logLevel?: LogLevel // Logging level (default: LogLevel.INFO)
socketTimeoutMs?: number // Socket timeout in milliseconds
responseTimeoutMs?: number // Server response timeout in milliseconds
dsn?: {
RET?: {
HEADERS?: boolean
FULL?: boolean
}
NOTIFY?: {
DELAY?: boolean
FAILURE?: boolean
SUCCESS?: boolean
}
}
}
Sends an email.
type EmailOptions = {
from:
| string
| {
// Sender's email
name?: string
email: string
}
to:
| string
| string[]
| {
// Recipients (TO)
name?: string
email: string
}
| Array<{ name?: string; email: string }>
reply?:
| string
| {
// Reply-To address
name?: string
email: string
}
cc?:
| string
| string[]
| {
// Carbon Copy recipients
name?: string
email: string
}
| Array<{ name?: string; email: string }>
bcc?:
| string
| string[]
| {
// Blind Carbon Copy recipients
name?: string
email: string
}
| Array<{ name?: string; email: string }>
subject: string // Email subject
text?: string // Plain text content
html?: string // HTML content
headers?: Record<string, string> // Custom email headers
attachments?: { filename: string; content: string; mimeType?: string }[] // Attachments, content must be base64-encoded, it will try to infer mimeType if not set
dsnOverride?: // overrides dsn defined in WorkerMailer, if not set, it will take the WorkerMailer-Option.
{
envelopeId?: string | undefined
RET?: {
HEADERS?: boolean
FULL?: boolean
}
NOTIFY?: {
DELAY?: boolean
FAILURE?: boolean
SUCCESS?: boolean
}
}
}
Send a one-off email without maintaining the connection.
await WorkerMailer.send(
{
// WorkerMailerOptions
host: 'smtp.acme.com',
port: 587,
credentials: {
username: 'user',
password: 'pass',
},
},
{
// EmailOptions
from: 'sender@acme.com',
to: 'recipient@acme.com',
subject: 'Test',
text: 'Hello',
attachments: [
{
filename: 'test.txt',
content: 'SGVsbG8gV29ybGQ=', // base64-encoded string for "Hello World"
type: 'text/plain',
},
],
},
)
- Port Restrictions: Cloudflare Workers cannot make outbound connections on port 25. You won't be able to send emails via port 25, but common ports like 587 and 465 are supported.
- Connection Limits: Each Worker instance has a limit on the number of concurrent TCP connections. Make sure to properly close connections when done.
For major changes, please open an issue first to discuss what you would like to change.
- Fork and clone the repository
- Install dependencies:
pnpm install
- Create a new branch for your feature from
develop
:git checkout -b feat/your-feature-name
- Make your changes and make sure all tests pass
- Update README.md & changelog
pnpm changeset
if needed - Push your changes to your fork and create a pull request from your branch to
develop
- Unit Tests:
npm test
- Integration Tests:
Then, send a POST request to
pnpm dlx wrangler dev ./test/worker.ts
http://127.0.0.1:8787
with the following JSON body:{ "config": { "credentials": { "username": "xxx@xx.com", "password": "xxxx" }, "authType": "plain", "host": "smtp.acme.com", "port": 587, "secure": false, "startTls": true }, "email": { "from": "xxx@xx.com", "to": "yyy@yy.com", "subject": "Test Email", "text": "Hello World" } }
When reporting issues, please include:
- Version of worker-mailer you're using
- A clear description of the problem
- Steps to reproduce the issue
- Expected vs actual behavior
- Any relevant code snippets or error messages
This project is licensed under the MIT License.