npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ribassu/worker-mailer

v1.3.1

Published

[English](./README.md) | [Português](./README_pt-BR.md)

Downloads

173

Readme

Worker Mailer

English | Português

npm version License: MIT

Worker Mailer is an SMTP client that runs on Cloudflare Workers. It leverages Cloudflare TCP Sockets and doesn't rely on any other dependencies.

Features

  • 🚀 Completely built on the Cloudflare Workers runtime with no other dependencies
  • 📝 Full TypeScript type support
  • 📧 Supports sending plain text and HTML emails with attachments
  • �️ Inline image attachments with Content-ID (CID) support
  • 🔒 Supports multiple SMTP authentication methods: plain, login, and CRAM-MD5
  • ✅ Email address validation (RFC 5322 compliant)
  • 🎯 Custom error classes for better error handling
  • 🪝 Lifecycle hooks for monitoring email operations
  • 📅 DSN support
  • 📬 Optional Cloudflare Queues integration for async email processing

Table of Contents

Installation

npm i @ribassu/worker-mailer

Quick Start

  1. Configure your wrangler.toml:
compatibility_flags = ["nodejs_compat"]
# or compatibility_flags = ["nodejs_compat_v2"]
  1. Use in your code:
import { WorkerMailer } from '@ribassu/worker-mailer'

// Connect to SMTP server
const mailer = await WorkerMailer.connect({
  credentials: {
    username: '[email protected]',
    password: 'password',
  },
  authType: 'plain',
  host: 'smtp.acme.com',
  port: 587,
  secure: true,
})

// Send email
await mailer.send({
  from: { name: 'Bob', email: '[email protected]' },
  to: { name: 'Alice', email: '[email protected]' },
  subject: 'Hello from Worker Mailer',
  text: 'This is a plain text message',
  html: '<h1>Hello</h1><p>This is an HTML message</p>',
})
  1. 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('@ribassu/worker-mailer')
    const mailer = await WorkerMailer.connect()
    return await mailer.send()
  }
})

This pattern ensures your application works seamlessly in both development and production environments.

API Reference

WorkerMailer.connect(options)

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
  hooks?: WorkerMailerHooks // Lifecycle hooks for monitoring
  dsn?: {
    RET?: {
      HEADERS?: boolean
      FULL?: boolean
    }
    NOTIFY?: {
      DELAY?: boolean
      FAILURE?: boolean
      SUCCESS?: boolean
    }
  }
}

mailer.send(options)

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?: Attachment[] // Attachments
  dsnOverride?: {
    // Overrides dsn defined in WorkerMailer
    envelopeId?: string | undefined
    RET?: {
      HEADERS?: boolean
      FULL?: boolean
    }
    NOTIFY?: {
      DELAY?: boolean
      FAILURE?: boolean
      SUCCESS?: boolean
    }
  }
}

type Attachment = {
  filename: string
  content: string // Base64-encoded content
  mimeType?: string // MIME type (auto-detected if not set)
  cid?: string // Content-ID for inline images
  inline?: boolean // If true, attachment will be inline
}

Static Method: WorkerMailer.send()

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: '[email protected]',
    to: '[email protected]',
    subject: 'Test',
    text: 'Hello',
    attachments: [
      {
        filename: 'test.txt',
        content: 'SGVsbG8gV29ybGQ=', // base64-encoded string for "Hello World"
        mimeType: 'text/plain',
      },
    ],
  },
)

Inline Images (CID)

You can embed images directly in HTML emails using Content-ID (CID):

import { WorkerMailer } from '@ribassu/worker-mailer'

const mailer = await WorkerMailer.connect({
  host: 'smtp.acme.com',
  port: 587,
  credentials: { username: 'user', password: 'pass' },
})

await mailer.send({
  from: '[email protected]',
  to: '[email protected]',
  subject: 'Email with embedded image',
  html: `
    <h1>Hello!</h1>
    <p>Here's our logo:</p>
    <img src="cid:company-logo" alt="Company Logo">
  `,
  attachments: [
    {
      filename: 'logo.png',
      content: logoBase64, // Base64-encoded image
      mimeType: 'image/png',
      cid: 'company-logo', // Referenced in HTML as cid:company-logo
      inline: true,
    },
  ],
})

Lifecycle Hooks

Monitor email operations with lifecycle hooks:

import { WorkerMailer } from '@ribassu/worker-mailer'

const mailer = await WorkerMailer.connect({
  host: 'smtp.acme.com',
  port: 587,
  credentials: { username: 'user', password: 'pass' },
  hooks: {
    onConnect: () => {
      console.log('Connected to SMTP server')
    },
    onSent: (email, response) => {
      console.log(`Email sent to ${email.to}:`, response)
    },
    onError: (email, error) => {
      console.error(`Failed to send email:`, error)
      // Send to error tracking service, etc.
    },
    onClose: error => {
      if (error) {
        console.error('Connection closed with error:', error)
      } else {
        console.log('Connection closed')
      }
    },
  },
})

Error Handling

Worker Mailer provides custom error classes for better error handling:

import {
  WorkerMailer,
  InvalidEmailError,
  SmtpAuthError,
  SmtpConnectionError,
  SmtpRecipientError,
  SmtpTimeoutError,
  InvalidContentError,
} from '@ribassu/worker-mailer'

try {
  const mailer = await WorkerMailer.connect({
    host: 'smtp.acme.com',
    port: 587,
    credentials: { username: 'user', password: 'wrong-password' },
  })

  await mailer.send({
    from: 'invalid-email', // This will throw InvalidEmailError
    to: '[email protected]',
    subject: 'Test',
    text: 'Hello',
  })
} catch (error) {
  if (error instanceof InvalidEmailError) {
    console.error('Invalid emails:', error.invalidEmails)
  } else if (error instanceof SmtpAuthError) {
    console.error('Authentication failed')
  } else if (error instanceof SmtpConnectionError) {
    console.error('Could not connect to SMTP server')
  } else if (error instanceof SmtpRecipientError) {
    console.error('Recipient rejected:', error.recipient)
  } else if (error instanceof SmtpTimeoutError) {
    console.error('Operation timed out')
  } else if (error instanceof InvalidContentError) {
    console.error('Invalid email content (missing text or html)')
  }
}

Cloudflare Queues Integration

For high-volume email sending, you can use Cloudflare Queues for async processing:

Setup

  1. Add a Queue binding in wrangler.toml:
[[queues.producers]]
queue = "email-queue"
binding = "EMAIL_QUEUE"

[[queues.consumers]]
queue = "email-queue"
max_batch_size = 10
max_retries = 3
  1. Create your worker with queue handler:
import { WorkerMailer } from '@ribassu/worker-mailer'
import {
  createQueueHandler,
  enqueueEmail,
  type QueueEmailMessage,
} from '@ribassu/worker-mailer/queue'

interface Env {
  EMAIL_QUEUE: Queue<QueueEmailMessage>
}

export default {
  // Handle HTTP requests - enqueue emails
  async fetch(request: Request, env: Env): Promise<Response> {
    await enqueueEmail(env.EMAIL_QUEUE, {
      mailerOptions: {
        host: 'smtp.acme.com',
        port: 587,
        credentials: { username: 'user', password: 'pass' },
        authType: 'plain',
      },
      emailOptions: {
        from: '[email protected]',
        to: '[email protected]',
        subject: 'Hello from Queue',
        text: 'This email was sent via Cloudflare Queues!',
      },
    })

    return new Response('Email queued successfully')
  },

  // Process queued emails
  async queue(batch: MessageBatch<QueueEmailMessage>, env: Env): Promise<void> {
    const handler = createQueueHandler({
      onSuccess: result => console.log('Email sent:', result.emailOptions.to),
      onError: result => console.error('Failed:', result.error),
    })

    await handler(batch)
  },
}

Queue Helper Functions

import {
  enqueueEmail,
  enqueueEmails,
  type QueueEmailMessage,
} from '@ribassu/worker-mailer/queue'

// Enqueue a single email
await enqueueEmail(env.EMAIL_QUEUE, {
  mailerOptions: { host: 'smtp.acme.com', port: 587 /* ... */ },
  emailOptions: {
    from: '[email protected]',
    to: '[email protected]',
    subject: 'Hi',
    text: 'Hello',
  },
})

// Enqueue multiple emails at once
await enqueueEmails(env.EMAIL_QUEUE, [
  {
    mailerOptions: {
      /* ... */
    },
    emailOptions: {
      /* ... */
    },
  },
  {
    mailerOptions: {
      /* ... */
    },
    emailOptions: {
      /* ... */
    },
  },
])

Limitations

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

Contributing

Development Workflow

For major changes, please open an issue first to discuss what you would like to change.

  1. Fork and clone the repository
  2. Install dependencies:
    bun install
  3. Create a new branch for your feature from develop:
    git checkout -b feat/your-feature-name
  4. Make your changes and make sure all tests pass
  5. Update README.md & changelog bun changeset if needed
  6. Push your changes to your fork and create a pull request from your branch to develop

Testing

  1. Unit Tests:
    bun test
  2. Integration Tests:
    bunx wrangler dev ./test/worker.ts
    Then, send a POST request to http://127.0.0.1:8787 with the following JSON body:
    {
      "config": {
        "credentials": {
          "username": "[email protected]",
          "password": "xxxx"
        },
        "authType": "plain",
        "host": "smtp.acme.com",
        "port": 587,
        "secure": false,
        "startTls": true
      },
      "email": {
        "from": "[email protected]",
        "to": "[email protected]",
        "subject": "Test Email",
        "text": "Hello World"
      }
    }

Reporting Issues

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

License

This project is licensed under the MIT License.