v1.3.3 • Bun recommended, npm supported

Documentation

Shipping SMTP from Cloudflare Workers, cleanly.

The docs below stay close to the published API: install with Bun or npm, configure Workers compatibility, send email, and scale into queues, hooks, and inline assets.

Introduction

What worker-mailer is built to do

worker-mailer is an SMTP client for Cloudflare Workers. It uses TCP sockets, supports typed message envelopes, handles HTML and attachments, and exposes queue helpers for when mail should leave the request path.

Transactional email

Forms, account notifications, and operational emails directly from a Worker.

Queue-backed delivery

Move send work into Cloudflare Queues when throughput or latency matters.

Operational visibility

Hooks, DSN, and explicit errors help with retries and monitoring.

Installation

Install with Bun or npm and enable Worker compatibility

Our recommendation is Bun, especially if the rest of your Worker setup already runs on it. If your team prefers npm, the package install is straightforward there too. Either way, turn on the Worker compatibility flag that the Cloudflare socket runtime needs.

terminal Recommended: Bun
bun add @workermailer/smtp
terminal Alternative: npm
npm install @workermailer/smtp
wrangler.json Worker config
{
  "compatibility_flags": ["nodejs_compat"]
}

Quick start

Connect once, or send once

Start with a persistent mailer when you want multiple sends on the same transport, or use the static helper when you just need to fire a single message and return.

send-email.ts Persistent transport
import { WorkerMailer } from '@workermailer/smtp'

const mailer = await WorkerMailer.connect({
  host: 'smtp.example.com',
  port: 465,
  secure: true,
  authType: 'plain',
  credentials: {
    username: env.SMTP_USERNAME,
    password: env.SMTP_PASSWORD
  }
})

await mailer.send({
  from: { name: 'Worker Mailer', email: 'noreply@example.com' },
  to: { name: 'Alice', email: 'alice@example.com' },
  subject: 'Hello from the edge',
  text: 'This message was sent from a Cloudflare Worker.',
  html: '<h1>Hello from the edge</h1><p>SMTP over TCP sockets.</p>'
})
send-once.ts One-off send
import { WorkerMailer } from '@workermailer/smtp'

await WorkerMailer.send(
  {
    host: 'smtp.example.com',
    port: 465,
    secure: true,
    credentials: {
      username: env.SMTP_USERNAME,
      password: env.SMTP_PASSWORD
    }
  },
  {
    from: 'noreply@example.com',
    to: 'alice@example.com',
    subject: 'One-off send',
    text: 'No persistent connection required.'
  }
)
queue.ts Queue handler
import {
  createQueueHandler,
  enqueueEmail,
  type QueueEmailMessage
} from '@workermailer/smtp/queue'

interface Env {
  EMAIL_QUEUE: Queue<QueueEmailMessage>
}

export default {
  async fetch(_request: Request, env: Env) {
    await enqueueEmail(env.EMAIL_QUEUE, {
      mailerOptions: {
        host: 'smtp.example.com',
        port: 465,
        secure: true,
        credentials: {
          username: env.SMTP_USERNAME,
          password: env.SMTP_PASSWORD
        }
      },
      emailOptions: {
        from: 'noreply@example.com',
        to: 'alice@example.com',
        subject: 'Queued email',
        text: 'Sent from Cloudflare Queues.'
      }
    })

    return new Response('queued')
  },

  async queue(batch: MessageBatch<QueueEmailMessage>) {
    const handleQueue = createQueueHandler()
    await handleQueue(batch)
  }
}

Providers

SMTP is the documented transport today

worker-mailer ships SMTP. Other provider names appear only in broader ecosystem discussions, so this site does not publish setup code for them.

Configuration

Core connection options

These are the fields you will tune most often in production for connection behavior, auth, delivery status notifications, and timeouts.

Option Type Default Description
host string required SMTP hostname, such as `smtp.example.com`.
port number required SMTP port. Cloudflare Workers commonly pair well with `465` or `587`, depending on your provider.
secure boolean false Start the connection over TLS immediately.
startTls boolean true Upgrade to TLS when the SMTP server supports it.
credentials { username, password } undefined SMTP authentication credentials.
authType 'plain' | 'login' | 'cram-md5' | array 'plain' Choose a specific auth mode or provide a preferred order.
hooks WorkerMailerHooks undefined Lifecycle hooks for connect, send, error, and close events.
dsn DSN options undefined Adds delivery status notifications at the envelope level.
socketTimeoutMs number runtime default Socket timeout guard for slow SMTP connections.
responseTimeoutMs number runtime default How long to wait for a reply from the server before aborting.

EmDash discussion

What this site is willing to say today

worker-mailer is relevant to the EmDash email-provider conversation because it already solves SMTP on Cloudflare Workers. Beyond that, the integration surface is still being discussed publicly, so this site avoids publishing guessed implementation details.

  • This site documents the current worker-mailer API, not a speculative EmDash plugin API.
  • The dedicated EmDash page summarizes the public discussion themes and open questions.
  • When the integration direction is final, the docs can grow from there with real examples.

API reference

Transport options and message options

`WorkerMailer.connect()` controls transport behavior. `mailer.send()` controls the message envelope, content, headers, and optional DSN overrides.

types.ts Transport options
type WorkerMailerOptions = {
  host: string
  port: number
  secure?: boolean
  startTls?: boolean
  credentials?: {
    username: string
    password: string
  }
  authType?:
    | 'plain'
    | 'login'
    | 'cram-md5'
    | Array<'plain' | 'login' | 'cram-md5'>
  hooks?: WorkerMailerHooks
  dsn?: {
    RET?: { HEADERS?: boolean; FULL?: boolean }
    NOTIFY?: { DELAY?: boolean; FAILURE?: boolean; SUCCESS?: boolean }
  }
}
email.ts Message options
type EmailOptions = {
  from: string | { name?: string; email: string }
  to:
    | string
    | string[]
    | { name?: string; email: string }
    | Array<{ name?: string; email: string }>
  cc?: string | string[]
  bcc?: string | string[]
  reply?: string | { name?: string; email: string }
  subject: string
  text?: string
  html?: string
  headers?: Record<string, string>
  attachments?: Attachment[]
  dsnOverride?: {
    envelopeId?: string
    RET?: { HEADERS?: boolean; FULL?: boolean }
    NOTIFY?: { DELAY?: boolean; FAILURE?: boolean; SUCCESS?: boolean }
  }
}
inline-image.ts Inline image
await mailer.send({
  from: 'noreply@example.com',
  to: 'alice@example.com',
  subject: 'Email with inline image',
  html: `
    <h1>Hello</h1>
    <p>Here is the logo:</p>
    <img src="cid:company-logo" alt="Company logo" />
  `,
  attachments: [
    {
      filename: 'logo.png',
      content: env.LOGO_BASE64,
      mimeType: 'image/png',
      cid: 'company-logo',
      inline: true
    }
  ]
})
hooks.ts Lifecycle hooks
const mailer = await WorkerMailer.connect({
  host: 'smtp.example.com',
  port: 465,
  secure: true,
  credentials: {
    username: env.SMTP_USERNAME,
    password: env.SMTP_PASSWORD
  },
  hooks: {
    onConnect: () => console.log('Connected to SMTP server'),
    onSent: (email, response) => console.log('Email sent', email.to, response),
    onError: (_email, error) => console.error('Delivery failed', error),
    onClose: (error) => console.log('Connection closed', error ?? 'cleanly')
  }
})