Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/puiusabin/bun-smtp/llms.txt

Use this file to discover all available pages before exploring further.

Overview

bun-smtp supports two TLS modes for securing SMTP connections:
  • Implicit TLS — TLS encryption from the first byte (port 465)
  • STARTTLS — Plain connection upgraded to TLS on demand (ports 25, 587)

Implicit TLS

Implicit TLS (also called SMTPS) establishes an encrypted connection immediately. This is typically used on port 465.
1

Set secure: true

Enable implicit TLS mode:
const server = new SMTPServer({
  secure: true,
});
2

Provide certificates

Add your TLS certificate and private key:
import { readFileSync } from "node:fs";

const server = new SMTPServer({
  secure: true,
  key: readFileSync("server.key"),
  cert: readFileSync("server.crt"),
  onData(stream, session, callback) {
    stream.pipeTo(new WritableStream()).then(() => callback(null), callback);
  },
});
3

Listen on port 465

Start the server on the standard SMTPS port:
await server.listen(465);
console.log("SMTPS server listening on port 465");
With Bun, you can use Bun.file() instead of readFileSync for better performance:
const server = new SMTPServer({
  secure: true,
  key: await Bun.file("server.key").text(),
  cert: await Bun.file("server.crt").text(),
});

STARTTLS

STARTTLS allows clients to upgrade a plain connection to TLS. This is advertised in the EHLO response and is the standard for ports 25 and 587.
import { readFileSync } from "node:fs";

const server = new SMTPServer({
  key: readFileSync("server.key"),
  cert: readFileSync("server.crt"),
  onData(stream, session, callback) {
    stream.pipeTo(new WritableStream()).then(() => callback(null), callback);
  },
});

await server.listen(587);
When key and cert are provided but secure is not set (or is false), STARTTLS is automatically advertised in the EHLO capabilities.

Hiding STARTTLS

To support STARTTLS without advertising it in EHLO (clients can still use it if they know about it):
const server = new SMTPServer({
  key: readFileSync("server.key"),
  cert: readFileSync("server.crt"),
  hideSTARTTLS: true,
});

Requiring STARTTLS

Force clients to complete STARTTLS before sending AUTH or MAIL commands:
const server = new SMTPServer({
  needsUpgrade: true,
  key: readFileSync("server.key"),
  cert: readFileSync("server.crt"),
});
Clients that attempt AUTH or MAIL before upgrading receive:
530 5.7.0 Must issue a STARTTLS command first

Development Mode (No Certificate)

When no key or cert is provided, bun-smtp uses a built-in self-signed certificate. This lets you test TLS functionality without certificate setup:
const server = new SMTPServer({ 
  authOptional: true,
});

await server.listen(2525);
// STARTTLS works immediately — no cert setup required
Never use the built-in certificate in production. It’s self-signed and provides no security guarantees. Always use proper certificates from a trusted CA or Let’s Encrypt.
The built-in certificate is a self-signed localhost certificate from the original smtp-server package:
src/smtp-server.ts
const DEFAULT_TLS_CERT =
  "-----BEGIN CERTIFICATE-----\n" +
  "MIICpDCCAYwCCQCuVLVKVTXnAjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwls\n" +
  "b2NhbGhvc3QwHhcNMTUwMjEyMTEzMjU4WhcNMjUwMjA5MTEzMjU4WjAUMRIwEAYD\n" +
  // ... truncated for brevity
Subject: CN=localhost Valid: 2015-02-12 to 2025-02-09

SNI (Server Name Indication)

Serve different certificates for different hostnames using SNI:
const server = new SMTPServer({
  sniOptions: {
    "mail.example.com": {
      key: readFileSync("example-com.key"),
      cert: readFileSync("example-com.crt"),
    },
    "mail.other.org": {
      key: readFileSync("other-org.key"),
      cert: readFileSync("other-org.crt"),
    },
  },
});
sniOptions accepts either a plain object or a Map<string, TLSOptions> for dynamic certificate management.

Dynamic SNI with Map

Use a Map for runtime certificate updates:
const sniMap = new Map<string, TLSOptions>();

sniMap.set("mail.example.com", {
  key: readFileSync("example-com.key"),
  cert: readFileSync("example-com.crt"),
});

const server = new SMTPServer({
  sniOptions: sniMap,
});

// Add a new domain at runtime
sniMap.set("mail.newdomain.com", {
  key: readFileSync("newdomain.key"),
  cert: readFileSync("newdomain.crt"),
});

Validating the TLS Handshake

Use onSecure to inspect or reject connections after TLS is established:
const server = new SMTPServer({
  requestCert: true,
  onSecure(socket, session, callback) {
    // Access TLS details via session.tlsOptions
    console.log("Cipher:", session.tlsOptions?.name);
    console.log("Protocol:", session.tlsOptions?.version);
    
    // Reject connections based on TLS properties
    if (session.tlsOptions?.version === "TLSv1.0") {
      return callback(new Error("TLS 1.0 is not supported"));
    }
    
    callback(null);
  },
});
onSecure is called after both implicit TLS and STARTTLS upgrades. The socket parameter is a Bun Socket, not a Node.js tls.TLSSocket.

TLS Options Reference

OptionTypeDescription
keystring | BufferPrivate key in PEM format
certstring | BufferCertificate in PEM format
castring | Buffer | ArrayCA certificates for client verification
requestCertbooleanRequest a client certificate during handshake
rejectUnauthorizedbooleanReject clients with invalid certificates
minVersionstringMinimum TLS version (e.g., "TLSv1.2")
maxVersionstringMaximum TLS version (e.g., "TLSv1.3")
sniOptionsRecord<string, TLSOptions> | Map<string, TLSOptions>Per-hostname TLS configuration

Client Certificate Authentication

Require and validate client certificates:
1

Enable client certificates

const server = new SMTPServer({
  key: readFileSync("server.key"),
  cert: readFileSync("server.crt"),
  ca: readFileSync("ca.crt"),
  requestCert: true,
  rejectUnauthorized: true,
});
2

Validate in onSecure

onSecure(socket, session, callback) {
  // In Bun, client cert details aren't directly exposed on the socket
  // You can implement custom validation based on your requirements
  callback(null);
}

Updating Certificates at Runtime

Rotate certificates without restarting the server:
// Initial setup
const server = new SMTPServer({
  key: readFileSync("server.key"),
  cert: readFileSync("server.crt"),
});

await server.listen(587);

// Later, update certificates (e.g., after Let's Encrypt renewal)
server.updateSecureContext({
  key: readFileSync("new-server.key"),
  cert: readFileSync("new-server.crt"),
});
New connections will use the updated certificates immediately. Existing connections continue using the old certificates until they close.

Port Recommendations

Port 25

MTA-to-MTATraditional SMTP port for server-to-server communication. Usually supports STARTTLS but doesn’t require it.

Port 587

Message SubmissionStandard port for client-to-server communication. Should require STARTTLS and authentication.

Port 465

SMTPSImplicit TLS from connection start. Use secure: true for this port.

Complete Examples

import { SMTPServer } from "bun-smtp";
import { readFileSync } from "node:fs";

const server = new SMTPServer({
  // Require STARTTLS before auth/mail
  needsUpgrade: true,
  key: readFileSync("/etc/ssl/private/mail.key"),
  cert: readFileSync("/etc/ssl/certs/mail.crt"),
  
  // Require authentication
  authOptional: false,
  allowInsecureAuth: false,
  
  // Enforce modern TLS
  minVersion: "TLSv1.2",
  
  onAuth(auth, session, callback) {
    // Verify credentials
    if (verifyUser(auth.username, auth.password)) {
      callback(null, { user: auth.username });
    } else {
      callback(new Error("Invalid credentials"));
    }
  },
  
  onData(stream, session, callback) {
    // Process message
    processMessage(stream, session)
      .then(() => callback(null))
      .catch(callback);
  },
});

await server.listen(587);
console.log("SMTP server listening on port 587");