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

@nds-stack/bun-mail

v0.1.0-alpha.1

Published

Email sender for Bun — SMTP client, MIME builder, HTML templates, zero dependencies

Downloads

31

Readme

@nds-stack/bun-mail

Bun-native email sender — SMTP client, MIME builder, HTML templates, zero dependencies.

npm version Bun TypeScript License


Why bun-mail

Nodemailer has 15.8M weekly downloads but depends on Node.js stream, tls, and dns modules. Bun-native applications shouldn't need Node.js polyfills just to send email.

bun-mail is a lightweight, Bun-native email sender:

import { BunMail } from "@nds-stack/bun-mail";

const mailer = new BunMail({
  host: "smtp.gmail.com",
  port: 587,
  auth: { username: "[email protected]", password: "app-password" },
});

const result = await mailer.send({
  from: { name: "You", address: "[email protected]" },
  to: "[email protected]",
  subject: "Hello from Bun!",
  text: "Plain text version",
  html: "<h1>Hello!</h1><p>HTML version</p>",
});

How It Works

bun-mail consists of three layers:

BunMail (facade)         — validates input, orchestrates send flow
  ├── MimeBuilder        — builds RFC 5322/2045 MIME message
  └── SmtpClient         — handles SMTP wire protocol via Bun.connect()

MIME Construction: MimeBuilder builds structured email messages — plain text, HTML, multipart/alternative (text+HTML), or multipart/mixed (with attachments). Content is base64-encoded and formatted per RFC standards.

SMTP Protocol: SmtpClient connects to the SMTP server using Bun.connect() (native TCP/TLS). The handshake flow is: connect → 220 greeting → EHLO → AUTH LOGIN (optional) → MAIL FROM → RCPT TO → DATA → QUIT. Each command awaits a response code; multi-line responses are consumed properly per RFC 5321.

Connection: Each send() call opens a fresh TCP/TLS connection, performs the SMTP handshake, transmits the message, and closes with QUIT. Connection pooling is planned for beta.


Installation

bun add @nds-stack/bun-mail

API

Constructor

new BunMail(options: BunMailOptions)

| Option | Type | Default | Description | |--------|------|---------|-------------| | host | string | — | SMTP server hostname (required) | | port | number | 587 or 465 | SMTP port. Defaults to 465 if tls: true | | tls | boolean | false | Use TLS. Auto-set for port 465 | | auth | { username, password } | — | SMTP AUTH PLAIN (preferred) / AUTH LOGIN credentials | | timeout | number | 30000 | Connection/SMTP timeout in ms | | defaultFrom | string | — | Default sender address fallback | | maxConcurrency | number | 10 | Max parallel connections in sendBulk() | | dkim | DkimOptions | — | DKIM signing configuration |

Methods

send(msg: EmailMessage): Promise<SendResult>

Send an email via SMTP.

| Field | Type | Description | |-------|------|-------------| | from | string \| EmailAddress | Sender address | | to | string \| EmailAddress \| (string \| EmailAddress)[] | Recipients | | cc | Same as to | CC recipients | | bcc | Same as to | BCC recipients | | subject | string | Email subject (UTF-8) | | text | string | Plain text body | | html | string | HTML body | | attachments | Attachment[] | File attachments | | headers | Record<string, string> | Custom headers |

Returns SendResult:

{
  messageId: string;   // Generated Message-ID
  accepted: string[];  // Accepted recipients
  rejected: string[];  // Rejected recipients
}

sendBulk(messages: EmailMessage[]): Promise<SendResult[]>

Send multiple emails concurrently.

verifyConnectivity(): Promise<boolean>

Test SMTP connection without sending email.

EmailAddress

{ name?: string; address: string }

Attachment

{
  filename: string;
  content: string | Uint8Array;
  contentType?: string;    // Default: application/octet-stream
  encoding?: "base64";
}

Error Handling

All SMTP errors throw SmtpError:

import { SmtpError } from "@nds-stack/bun-mail";

try {
  await mailer.send({ ... });
} catch (err) {
  if (err instanceof SmtpError) {
    console.error(`SMTP ${err.code}: ${err.message}`);
    console.error(`Server response: ${err.response}`);
  }
}

Error codes:

  • 535 — Authentication failed
  • 550 — Mailbox unavailable
  • 554 — Transaction failed
  • Validation errors throw plain Error (missing host, no recipients, etc.)

Limitations

  • SMTP only (no SendGrid/Mailgun/SES API providers — future)
  • AUTH LOGIN and AUTH PLAIN (auto-negotiation, PLAIN tried first)
  • WARNING: AUTH LOGIN credentials are base64-encoded, not encrypted. Always use TLS (port 465) when authentication is enabled.
  • No STARTTLS upgrade (uses direct TLS on port 465)
  • DKIM signing via RSA-SHA256 (relaxed canonicalization) — PEM private key required
  • Connection per send (pooling coming in beta)
  • Base64 content transfer only (QP planned)

Multi-Instance / Cross-Boundary

Each BunMail instance is independent with its own SMTP connection pool (once implemented). For multi-process/worker scenarios:

// Worker 1
const mailer1 = new BunMail({ host: "smtp.example.com", auth: { ... } });

// Worker 2
const mailer2 = new BunMail({ host: "smtp.example.com", auth: { ... } });

No shared state between instances. For centralized email sending in a distributed system, route through a queue or Bunova message bus.


DKIM Signing

DKIM (DomainKeys Identified Mail) is required for deliverability to Gmail, Yahoo, Outlook, ProtonMail, and other major providers. Without DKIM, emails are likely to be marked as spam.

Setup

Generate a DKIM private key and DNS record:

# Generate 1024-bit RSA private key
openssl genrsa -out dkim.pem 1024
# Extract public key for DNS
openssl rsa -in dkim.pem -pubout

Add a TXT record to your domain's DNS:

{selector}._domainkey.{domain}  TXT  "v=DKIM1; k=rsa; p={base64-public-key}"

Usage

import { BunMail } from "@nds-stack/bun-mail";
import { readFileSync } from "fs";

const privateKey = readFileSync("./dkim.pem", "utf-8");

const mailer = new BunMail({
  host: "smtp.example.com",
  auth: { username: "...", password: "..." },
  dkim: {
    privateKey,          // PEM-encoded RSA private key
    selector: "default", // DNS selector name
    domain: "example.com",
    headers: ["from", "to", "subject", "date", "message-id"], // optional
  },
});

How it Works

  1. The MIME message is built (headers + body)
  2. DKIM signs the selected headers + body hash using RSA-SHA256
  3. The DKIM-Signature header is prepended to the email
  4. Relaxed canonicalization is used (whitespace normalization)
  5. Authentication is automatic via Web Crypto API (Bun native)

DkimOptions

| Option | Type | Default | Description | |--------|------|---------|-------------| | privateKey | string | — | PEM-encoded RSA private key (required) | | selector | string | — | DKIM DNS selector (required) | | domain | string | — | Signing domain (required) | | headers | string[] | All standard headers | Headers to include in signature |


Customization Guide

Subclassing

class CustomMailer extends BunMail {
  async sendWithTracking(msg: EmailMessage) {
    const result = await this.send(msg);
    await this.logToDb(result);
    return result;
  }
}

Custom Headers

await mailer.send({
  ...msg,
  headers: {
    "X-Application": "my-app",
    "List-Unsubscribe": "<mailto:[email protected]>",
    "Priority": "urgent",
  },
});

Template Integration

function renderTemplate(template: string, data: Record<string, unknown>): string {
  return template.replace(/\{\{(\w+)\}\}/g, (_, key) => String(data[key] ?? ""));
}

await mailer.send({
  ...msg,
  html: renderTemplate("<h1>Hello {{name}}</h1>", { name: "Alice" }),
});

Comparison Table

| Feature | @nds-stack/bun-mail | nodemailer | |---------|:---------------------:|:------------:| | Runtime | Bun native | Node.js + polyfills | | Dependencies | 0 | 8+ (nodemailer + deps) | | SMTP TLS | ✅ via Bun.connect() | ✅ via tls module | | AUTH LOGIN | ✅ | ✅ | | AUTH PLAIN | ⏳ Beta | ✅ | | HTML + Text | ✅ | ✅ | | Attachments | ✅ | ✅ | | DKIM | ✅ via RSA-SHA256 | ✅ | | Connection Pool | ⏳ Beta | ✅ | | SendGrid / SES | ⏳ Future | ✅ (via plugins) | | Bundle Size | ~15KB | ~200KB+ |


Benchmarks

| Operation | @nds-stack/bun-mail | nodemailer | Overhead | |-----------|:---------------------:|:------------:|:--------:| | MIME build (plain) | ~175K ops/s | — | Baseline | | MIME build (HTML+text) | ~181K ops/s | — | — | | MIME build (10KB attach) | ~34K ops/s | — | — |

Run your own: bun run bench


Real-World Example

import { BunMail } from "@nds-stack/bun-mail";

interface WelcomeEmail {
  to: string;
  name: string;
  verifyLink: string;
}

async function sendWelcomeEmail(mailer: BunMail, data: WelcomeEmail) {
  return mailer.send({
    from: { name: "My App", address: "[email protected]" },
    to: data.to,
    subject: `Welcome, ${data.name}!`,
    text: `Hi ${data.name},\n\nPlease verify your email: ${data.verifyLink}`,
    html: `
      <h1>Welcome ${data.name}!</h1>
      <p>Please <a href="${data.verifyLink}">verify your email</a>.</p>
    `,
  });
}

const mailer = new BunMail({
  host: process.env.SMTP_HOST!,
  auth: {
    username: process.env.SMTP_USER!,
    password: process.env.SMTP_PASS!,
  },
});

await sendWelcomeEmail(mailer, {
  to: "[email protected]",
  name: "Alice",
  verifyLink: "https://myapp.com/verify?token=abc123",
});