Skip to main content

SMTP Server

Create SMTP and LMTP server instances on the fly. smtp‑server is not a full‑blown server application like Haraka but a convenient way to add custom SMTP or LMTP listeners to your app. It is the successor of the server part of the now‑deprecated simplesmtp module. For a matching SMTP client, see smtp‑connection.

Usage

1 — Install

npm install smtp-server --save

2 — Require in your script

const { SMTPServer } = require("smtp-server");

3 — Create a server instance

const server = new SMTPServer(options);

4 — Start listening

server.listen(port[, host][, callback]);

5 — Shut down

server.close(callback);

Options reference

OptionTypeDefaultDescription
secureBooleanfalseStart in TLS mode. Can still be upgraded with STARTTLS if you leave this false.
nameStringos.hostname()Hostname announced in banner.
bannerStringGreeting appended to the standard ESMTP banner.
sizeNumber0Maximum accepted message size in bytes. 0 means unlimited.
hideSizeBooleanfalseHide the SIZE limit from clients but still track stream.sizeExceeded.
authMethodsString[]['PLAIN', 'LOGIN']Allowed auth mechanisms. Add 'XOAUTH2' and/or 'CRAM-MD5' as needed.
authOptionalBooleanfalseAllow but do not require auth.
disabledCommandsString[]Commands to disable, e.g. ['AUTH'].
hideSTARTTLS / hidePIPELINING / hide8BITMIME / hideSMTPUTF8BooleanfalseRemove the respective feature from the EHLO response.
hideENHANCEDSTATUSCODESBooleantrueEnable or disable the ENHANCEDSTATUSCODES capability in EHLO response. Enhanced status codes are disabled by default.
allowInsecureAuthBooleanfalseAllow authentication before TLS.
disableReverseLookupBooleanfalseSkip reverse DNS lookup of the client.
sniOptionsMap | ObjectTLS options per SNI hostname.
loggerBoolean | Objectfalsetrue → log to console, or supply a Bunyan instance.
maxClientsNumberInfinityMax concurrent clients.
useProxyBooleanfalseExpect an HAProxy PROXY header.
useXClient / useXForwardBooleanfalseEnable Postfix XCLIENT or XFORWARD.
lmtpBooleanfalseSpeak LMTP instead of SMTP.
socketTimeoutNumber60_000Idle timeout (ms) before disconnect.
closeTimeoutNumber30_000Wait (ms) for pending connections on close().
onAuth / onConnect / onSecure / onMailFrom / onRcptTo / onData / onCloseFunctionLifecycle callbacks detailed below.
resolverObjectCustom DNS resolver with .reverse function, defaults to Node.js native dns module and its dns.reverse function.

You may also pass any net.createServer options and, when secure is true, any tls.createServer options.


TLS and STARTTLS

If you enable TLS (secure: true) or leave STARTTLS enabled, ship a proper certificate via key, cert, and optionally ca. Otherwise smtp‑server falls back to a self‑signed cert for localhost, which almost every client rejects.

const fs = require("fs");
const server = new SMTPServer({
secure: true,
key: fs.readFileSync("private.key"),
cert: fs.readFileSync("server.crt"),
});
server.listen(465);

Handling errors

Attach an error listener to surface server errors:

server.on("error", (err) => {
console.error("SMTP Server error:", err.message);
});

Handling authentication (onAuth)

const server = new SMTPServer({
onAuth(auth, session, callback) {
// auth.method → 'PLAIN', 'LOGIN', 'XOAUTH2', or 'CRAM-MD5'
// Return `callback(err)` to reject, `callback(null, response)` to accept
},
});

Password‑based (PLAIN / LOGIN)

onAuth(auth, session, cb) {
if (auth.username !== "alice" || auth.password !== "s3cr3t") {
return cb(new Error("Invalid username or password"));
}
cb(null, { user: auth.username });
}

OAuth 2 (XOAUTH2)

const server = new SMTPServer({
authMethods: ["XOAUTH2"],
onAuth(auth, session, cb) {
if (auth.accessToken !== "ya29.a0Af…") {
return cb(null, {
data: { status: "401", schemes: "bearer" },
}); // see RFC 6750 Sec. 3
}
cb(null, { user: auth.username });
},
});

Validating client connection (onConnect / onClose)

const server = new SMTPServer({
onConnect(session, cb) {
if (session.remoteAddress === "127.0.0.1") {
return cb(new Error("Connections from localhost are not allowed"));
}
cb(); // accept
},
onClose(session) {
console.log(`Connection from ${session.remoteAddress} closed`);
},
});

Validating TLS information (onSecure)

onSecure(socket, session, cb) {
if (session.servername !== "mail.example.com") {
return cb(new Error("SNI mismatch"));
}
cb();
}

Validating sender (onMailFrom)

onMailFrom(address, session, cb) {
if (!address.address.endsWith("@example.com")) {
return cb(Object.assign(new Error("Relay denied"), { responseCode: 553 }));
}
cb();
}

Validating recipients (onRcptTo)

onRcptTo(address, session, cb) {
if (address.address === "blackhole@example.com") {
return cb(new Error("User unknown"));
}
cb();
}

Processing incoming messages (onData)

onData(stream, session, cb) {
const write = require("fs").createWriteStream("/tmp/message.eml");
stream.pipe(write);
stream.on("end", () => cb(null, "Queued"));
}

smtp‑server streams your message verbatim — no Received: header is added. Add one yourself if you need full RFC 5321 compliance.


Using the SIZE extension

Set the size option to advertise a limit, then check stream.sizeExceeded in onData:

const server = new SMTPServer({
size: 1024 * 1024, // 1 MiB
onData(s, sess, cb) {
s.on("end", () => {
if (s.sizeExceeded) {
const err = Object.assign(new Error("Message too large"), { responseCode: 552 });
return cb(err);
}
cb(null, "OK");
});
},
});

Using LMTP

const server = new SMTPServer({
lmtp: true,
onData(stream, session, cb) {
stream.on("end", () => {
// Return one reply **per** recipient
const replies = session.envelope.rcptTo.map((rcpt, i) => (i % 2 ? new Error(`<${rcpt.address}> rejected`) : `<${rcpt.address}> accepted`));
cb(null, replies);
});
},
});

Session object

PropertyTypeDescription
idStringRandom connection ID.
remoteAddressStringClient IP address.
clientHostnameStringReverse‑DNS of remoteAddress (unless disableReverseLookup).
openingCommand"HELO" | "EHLO" | "LHLO"First command sent by the client.
hostNameAppearsAsStringHostname the client gave in HELO/EHLO.
envelopeObjectContains mailFrom, rcptTo arrays, and dsn data (see below).
useranyValue you returned from onAuth.
transactionNumber1 for the first message, 2 for the second, …
transmissionType"SMTP" | "ESMTP" | "ESMTPA" …Calculated for Received: headers.

Envelope object

The session.envelope object contains transaction-specific data:

{
"mailFrom": {
"address": "sender@example.com",
"args": { "SIZE": "12345", "RET": "HDRS" },
"dsn": { "ret": "HDRS", "envid": "abc123" }
},
"rcptTo": [
{
"address": "user1@example.com",
"args": { "NOTIFY": "SUCCESS,FAILURE" },
"dsn": { "notify": ["SUCCESS", "FAILURE"], "orcpt": "rfc822;user1@example.com" }
}
],
"dsn": {
"ret": "HDRS",
"envid": "abc123"
}
}
PropertyTypeDescription
mailFromObjectSender address object (see Address object)
rcptToObject[]Array of recipient address objects
dsnObjectDSN parameters from MAIL FROM command

Address object

{
"address": "sender@example.com",
"args": {
"SIZE": "12345",
"RET": "HDRS"
},
"dsn": {
"ret": "HDRS",
"envid": "abc123",
"notify": ["SUCCESS", "FAILURE"],
"orcpt": "rfc822;original@example.com"
}
}
FieldDescription
addressThe literal address given in MAIL FROM:/RCPT TO:.
argsAdditional arguments (uppercase keys).
dsnDSN parameters (when ENHANCEDSTATUSCODES is enabled).

DSN Object Properties

PropertyTypeDescription
retStringReturn type: 'FULL' or 'HDRS' (MAIL FROM)
envidStringEnvelope identifier (MAIL FROM)
notifyString[]Notification types (RCPT TO)
orcptStringOriginal recipient (RCPT TO)

Enhanced Status Codes (RFC 2034/3463)

smtp‑server supports Enhanced Status Codes as defined in RFC 2034 and RFC 3463. When enabled, all SMTP responses include enhanced status codes in the format X.Y.Z:

250 2.1.0 Accepted        ← Enhanced status code: 2.1.0
550 5.1.1 Mailbox unavailable ← Enhanced status code: 5.1.1

Enabling Enhanced Status Codes

To enable enhanced status codes (they are disabled by default):

const server = new SMTPServer({
hideENHANCEDSTATUSCODES: false, // Enable enhanced status codes
onMailFrom(address, session, callback) {
callback(); // Response: "250 2.1.0 Accepted" (with enhanced code)
},
});

Disabling Enhanced Status Codes

Enhanced status codes are disabled by default, but you can explicitly disable them:

const server = new SMTPServer({
hideENHANCEDSTATUSCODES: true, // Explicitly disable enhanced status codes (default behavior)
onMailFrom(address, session, callback) {
callback(); // Response: "250 Accepted" (no enhanced code)
},
});

Enhanced Status Code Examples

Response CodeEnhanced CodeDescription
2502.0.0General success
2502.1.0MAIL FROM accepted
2502.1.5RCPT TO accepted
2502.6.0Message accepted
5015.5.4Syntax error in parameters
5505.1.1Mailbox unavailable
5525.2.2Storage exceeded

DSN (Delivery Status Notification) Support

smtp‑server fully supports DSN parameters as defined in RFC 3461, allowing clients to request delivery status notifications.

DSN functionality requires Enhanced Status Codes to be enabled. Since enhanced status codes are disabled by default, you must set hideENHANCEDSTATUSCODES: false to use DSN features.

DSN Parameters

MAIL FROM Parameters

  • RET=FULL or RET=HDRS — Return full message or headers only in DSN
  • ENVID=<envelope-id> — Envelope identifier for tracking
// Client sends: MAIL FROM:<sender@example.com> RET=FULL ENVID=abc123

RCPT TO Parameters

  • NOTIFY=SUCCESS,FAILURE,DELAY,NEVER — When to send DSN
  • ORCPT=<original-recipient> — Original recipient for tracking
// Client sends: RCPT TO:<user@example.com> NOTIFY=SUCCESS,FAILURE ORCPT=rfc822;user@example.com

Accessing DSN Parameters

DSN parameters are available in your callback handlers:

const server = new SMTPServer({
hideENHANCEDSTATUSCODES: false, // Required for DSN functionality
onMailFrom(address, session, callback) {
// Access DSN parameters from MAIL FROM
const ret = session.envelope.dsn.ret; // 'FULL' or 'HDRS'
const envid = session.envelope.dsn.envid; // Envelope ID

console.log(`RET: ${ret}, ENVID: ${envid}`);
callback();
},

onRcptTo(address, session, callback) {
// Access DSN parameters from RCPT TO
const notify = address.dsn.notify; // ['SUCCESS', 'FAILURE', 'DELAY']
const orcpt = address.dsn.orcpt; // Original recipient

console.log(`NOTIFY: ${notify.join(',')}, ORCPT: ${orcpt}`);
callback();
},
});

DSN Parameter Validation

smtp‑server automatically validates DSN parameters:

  • RET must be FULL or HDRS
  • NOTIFY must be SUCCESS, FAILURE, DELAY, or NEVER
  • NOTIFY=NEVER cannot be combined with other values
  • Invalid parameters return appropriate error responses with enhanced status codes

Complete DSN Example

const server = new SMTPServer({
hideENHANCEDSTATUSCODES: false, // Required for DSN functionality
onMailFrom(address, session, callback) {
const { ret, envid } = session.envelope.dsn;
console.log(`Mail from ${address.address}, RET=${ret}, ENVID=${envid}`);
callback();
},

onRcptTo(address, session, callback) {
const { notify, orcpt } = address.dsn;
console.log(`Rcpt to ${address.address}, NOTIFY=${notify.join(',')}, ORCPT=${orcpt}`);
callback();
},

onData(stream, session, callback) {
// Process message with DSN context
const { dsn } = session.envelope;
console.log(`Processing message with DSN: ${JSON.stringify(dsn)}`);

stream.on('end', () => {
callback(null, 'Message accepted for delivery');
});
stream.resume();
},
});

Production DSN Implementation Example

Here's a complete example showing how to implement DSN notifications using nodemailer:

const { SMTPServer } = require('smtp-server');
const nodemailer = require('nodemailer');

// Create a nodemailer transporter for sending DSN notifications
const dsnTransporter = nodemailer.createTransporter({
host: 'smtp.example.com',
port: 587,
secure: false,
auth: {
user: 'dsn-sender@example.com',
pass: 'your-password'
}
});

// DSN notification generator
class DSNNotifier {
constructor(transporter) {
this.transporter = transporter;
}

async sendSuccessNotification(envelope, messageId, deliveryTime) {
// Only send if SUCCESS notification was requested
const needsSuccessNotification = envelope.rcptTo.some(rcpt =>
rcpt.dsn.notify && rcpt.dsn.notify.includes('SUCCESS')
);

if (!needsSuccessNotification || !envelope.mailFrom.address) {
return;
}

const dsnMessage = this.generateDSNMessage({
action: 'delivered',
status: '2.0.0',
envelope,
messageId,
deliveryTime,
diagnosticCode: 'smtp; 250 2.0.0 Message accepted for delivery'
});

await this.transporter.sendMail({
from: 'postmaster@example.com',
to: envelope.mailFrom.address,
subject: 'Delivery Status Notification (Success)',
text: dsnMessage.text,
headers: {
'Auto-Submitted': 'auto-replied',
'Content-Type': 'multipart/report; report-type=delivery-status'
}
});
}

generateDSNMessage({ action, status, envelope, messageId, deliveryTime, diagnosticCode }) {
const { dsn } = envelope;
const timestamp = deliveryTime || new Date().toISOString();

// Generate RFC 3464 compliant delivery status notification
const text = `This is an automatically generated Delivery Status Notification.

Original Message Details:
- Message ID: ${messageId}
- Envelope ID: ${dsn.envid || 'Not provided'}
- Sender: ${envelope.mailFrom.address}
- Recipients: ${envelope.rcptTo.map(r => r.address).join(', ')}
- Action: ${action}
- Status: ${status}
- Time: ${timestamp}

${action === 'delivered' ?
'Your message has been successfully delivered to all recipients.' :
'Delivery failed for one or more recipients.'
}`;

return { text };
}
}

// Create DSN notifier instance
const dsnNotifier = new DSNNotifier(dsnTransporter);

// SMTP Server with DSN support
const server = new SMTPServer({
hideENHANCEDSTATUSCODES: false, // Required for DSN functionality
name: 'mail.example.com',

onMailFrom(address, session, callback) {
const { dsn } = session.envelope;
console.log(`MAIL FROM: ${address.address}, RET=${dsn.ret}, ENVID=${dsn.envid}`);
callback();
},

onRcptTo(address, session, callback) {
const { notify, orcpt } = address.dsn;
console.log(`RCPT TO: ${address.address}, NOTIFY=${notify?.join(',')}, ORCPT=${orcpt}`);
callback();
},

async onData(stream, session, callback) {
const messageId = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;

stream.on('end', async () => {
try {
// Simulate message delivery
const deliveryTime = new Date();

// Send DSN success notification if requested
await dsnNotifier.sendSuccessNotification(
session.envelope,
messageId,
deliveryTime
);

callback(null, `Message ${messageId} accepted for delivery`);
} catch (error) {
callback(error);
}
});

stream.resume();
}
});

server.listen(2525, () => {
console.log('DSN-enabled SMTP server listening on port 2525');
});

This example demonstrates:

  • Complete DSN workflow from parameter parsing to notification sending
  • RFC-compliant DSN messages with proper headers and content
  • Conditional notifications based on NOTIFY parameters
  • Integration with nodemailer for sending DSN notifications
  • Production-ready structure with error handling

Supported commands and extensions

Commands

  • EHLO / HELO
  • AUTH LOGIN · PLAIN · XOAUTH2† · CRAM‑MD5
  • MAIL / RCPT / DATA
  • RSET / NOOP / QUIT / VRFY
  • HELP (returns RFC 5321 URL)
  • STARTTLS

† XOAUTH2 and CRAM‑MD5 must be enabled via authMethods.

Extensions

  • PIPELINING
  • 8BITMIME
  • SMTPUTF8
  • SIZE
  • ENHANCEDSTATUSCODES (RFC 2034/3463)

The CHUNKING extension is not implemented.


License

MIT