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.
Set secure: true
Enable implicit TLS mode: const server = new SMTPServer ({
secure: true ,
});
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 );
},
});
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.
View the built-in certificate details
The built-in certificate is a self-signed localhost certificate from the original smtp-server package: 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
Option Type Description 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:
Enable client certificates
const server = new SMTPServer ({
key: readFileSync ( "server.key" ),
cert: readFileSync ( "server.crt" ),
ca: readFileSync ( "ca.crt" ),
requestCert: true ,
rejectUnauthorized: true ,
});
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-MTA Traditional SMTP port for server-to-server communication. Usually supports STARTTLS but doesn’t require it.
Port 587 Message Submission Standard port for client-to-server communication. Should require STARTTLS and authentication.
Port 465 SMTPS Implicit TLS from connection start. Use secure: true for this port.
Complete Examples
Production (Port 587)
SMTPS (Port 465)
Development
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" );
import { SMTPServer } from "bun-smtp" ;
import { readFileSync } from "node:fs" ;
const server = new SMTPServer ({
// Implicit TLS
secure: true ,
key: readFileSync ( "/etc/ssl/private/mail.key" ),
cert: readFileSync ( "/etc/ssl/certs/mail.crt" ),
authOptional: false ,
minVersion: "TLSv1.2" ,
onAuth ( auth , session , callback ) {
if ( verifyUser ( auth . username , auth . password )) {
callback ( null , { user: auth . username });
} else {
callback ( new Error ( "Invalid credentials" ));
}
},
onData ( stream , session , callback ) {
processMessage ( stream , session )
. then (() => callback ( null ))
. catch ( callback );
},
});
await server . listen ( 465 );
console . log ( "SMTPS server listening on port 465" );
import { SMTPServer } from "bun-smtp" ;
const server = new SMTPServer ({
// No TLS config = uses built-in self-signed cert
authOptional: true ,
onData ( stream , session , callback ) {
// Just log the message
console . log ( "Received message from" , session . envelope . mailFrom ?. address );
stream . pipeTo ( new WritableStream ()). then (() => callback ( null ), callback );
},
});
await server . listen ( 2525 );
console . log ( "Development SMTP server listening on port 2525" );