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
| Option | Type | Default | Description | 
|---|---|---|---|
| secure | Boolean | false | Start in TLS mode. Can still be upgraded with STARTTLSif you leave thisfalse. | 
| name | String | os.hostname() | Hostname announced in banner. | 
| banner | String | – | Greeting message appended to the standard ESMTP banner (initial connection greeting). | 
| heloResponse | String | '%s Nice to meet you, %s' | HELO/EHLO greeting format. Use %splaceholders: 1st = server name, 2nd = client hostname. | 
| size | Number | 0 | Maximum accepted message size in bytes. 0means unlimited. | 
| hideSize | Boolean | false | Hide the SIZE limit from clients but still track stream.sizeExceeded. | 
| authMethods | String[] | ['PLAIN', 'LOGIN'] | Allowed auth mechanisms. Add 'XOAUTH2'and/or'CRAM-MD5'as needed. | 
| authOptional | Boolean | false | Allow but do not require auth. | 
| disabledCommands | String[] | – | Commands to disable, e.g. ['AUTH']. | 
| hideSTARTTLS / hidePIPELINING / hide8BITMIME / hideSMTPUTF8 | Boolean | false | Remove the respective feature from the EHLO response. | 
| hideENHANCEDSTATUSCODES | Boolean | true | Enable or disable the ENHANCEDSTATUSCODEScapability inEHLOresponse. Enhanced status codes are disabled by default. | 
| hideDSN | Boolean | true | Enable or disable the DSNcapability inEHLOresponse. Delivery status notifications are disabled by default. | 
| hideREQUIRETLS | Boolean | true | Enable or disable the REQUIRETLScapability inEHLOresponse. REQUIRETLS is disabled by default (opt-in). | 
| allowInsecureAuth | Boolean | false | Allow authentication before TLS. | 
| disableReverseLookup | Boolean | false | Skip reverse DNS lookup of the client. | 
| sniOptions | Map | Object | – | TLS options per SNI hostname. | 
| logger | Boolean | Object | false | true→ log toconsole, or supply a Bunyan instance. | 
| maxClients | Number | Infinity | Max concurrent clients. | 
| useProxy | Boolean | false | Expect an HAProxy PROXY header. | 
| useXClient / useXForward | Boolean | false | Enable Postfix XCLIENT or XFORWARD. | 
| lmtp | Boolean | false | Speak LMTP instead of SMTP. | 
| socketTimeout | Number | 60_000 | Idle timeout (ms) before disconnect. | 
| closeTimeout | Number | 30_000 | Wait (ms) for pending connections on close(). | 
| onAuth / onConnect / onSecure / onMailFrom / onRcptTo / onData / onClose | Function | – | Lifecycle callbacks detailed below. | 
| resolver | Object | – | Custom DNS resolver with .reversefunction, defaults to Node.js nativednsmodule and itsdns.reversefunction. | 
You may also pass any net.createServer options and, when secure is true, any tls.createServer options.
Customizing greetings
Initial connection banner
The banner option adds a custom greeting to the initial connection response (220 code):
const server = new SMTPServer({
  banner: "Welcome to our mail service",
});
// Client sees: "220 hostname ESMTP Welcome to our mail service"
HELO/EHLO response
The heloResponse option customizes the HELO/EHLO greeting message using %s placeholders:
const server = new SMTPServer({
  heloResponse: "%s says hello to %s",
});
// Client sees: "250 hostname says hello to client.example.com"
Placeholders:
- First %s→ Server name (fromnameoption oros.hostname())
- Second %s→ Client hostname (resolved or IP address)
Examples:
// Default (no configuration)
// "250 hostname Nice to meet you, client.example.com"
// Custom formal greeting
heloResponse: "Welcome to %s mail server"
// "250 Welcome to hostname mail server"
// Minimal greeting
heloResponse: "Hello"
// "250 Hello"
// Custom with both placeholders
heloResponse: "%s greets %s"
// "250 hostname greets client.example.com"
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
| Property | Type | Description | 
|---|---|---|
| id | String | Random connection ID. | 
| remoteAddress | String | Client IP address. | 
| clientHostname | String | Reverse‑DNS of remoteAddress(unlessdisableReverseLookup). | 
| openingCommand | "HELO" | "EHLO" | "LHLO" | First command sent by the client. | 
| hostNameAppearsAs | String | Hostname the client gave in HELO/EHLO. | 
| envelope | Object | Contains mailFrom,rcptToarrays, anddsndata (see below). | 
| user | any | Value you returned from onAuth. | 
| transaction | Number | 1 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", "BODY": "8BITMIME", "SMTPUTF8": true, "REQUIRETLS": true },
    "dsn": { "ret": "HDRS", "envid": "abc123" }
  },
  "rcptTo": [
    {
      "address": "user1@example.com",
      "args": { "NOTIFY": "SUCCESS,FAILURE" },
      "dsn": { "notify": ["SUCCESS", "FAILURE"], "orcpt": "rfc822;user1@example.com" }
    }
  ],
  "bodyType": "8bitmime",
  "smtpUtf8": true,
  "requireTLS": true,
  "dsn": {
    "ret": "HDRS",
    "envid": "abc123"
  }
}
| Property | Type | Description | 
|---|---|---|
| mailFrom | Object | Sender address object (see Address object) | 
| rcptTo | Object[] | Array of recipient address objects | 
| bodyType | String | RFC 6152: Message body type - '7bit'or'8bitmime' | 
| smtpUtf8 | Boolean | RFC 6531: Whether UTF-8 support was requested | 
| requireTLS | Boolean | RFC 8689: Whether TLS is required for entire delivery chain | 
| dsn | Object | DSN 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"
  }
}
| Field | Description | 
|---|---|
| address | The literal address given in MAIL FROM:/RCPT TO:. | 
| args | Additional arguments (uppercase keys). | 
| dsn | DSN parameters (when ENHANCEDSTATUSCODES is enabled). | 
DSN Object Properties
| Property | Type | Description | 
|---|---|---|
| ret | String | Return type: 'FULL'or'HDRS'(MAIL FROM) | 
| envid | String | Envelope identifier (MAIL FROM) | 
| notify | String[] | Notification types (RCPT TO) | 
| orcpt | String | Original 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 Code | Enhanced Code | Description | 
|---|---|---|
| 250 | 2.0.0 | General success | 
| 250 | 2.1.0 | MAIL FROM accepted | 
| 250 | 2.1.5 | RCPT TO accepted | 
| 250 | 2.6.0 | Message accepted | 
| 501 | 5.5.4 | Syntax error in parameters | 
| 550 | 5.1.1 | Mailbox unavailable | 
| 552 | 5.2.2 | Storage 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 delivery status notifications to be enabled. Since delivery status notifications are disabled by default, you must set hideDSN: false to use DSN features.
DSN Parameters
MAIL FROM Parameters
- RET=FULLor- 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({
  hideDSN: 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:
- RETmust be- FULLor- HDRS
- NOTIFYmust be- SUCCESS,- FAILURE,- DELAY, or- NEVER
- NOTIFY=NEVERcannot be combined with other values
- Invalid parameters return appropriate error responses with enhanced status codes
Complete DSN Example
const server = new SMTPServer({
  hideDSN: 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({
  hideDSN: 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
MAIL FROM Parameters (BODY, SMTPUTF8, REQUIRETLS)
smtp-server supports several RFC-compliant MAIL FROM parameters that allow clients to specify message characteristics and delivery requirements.
BODY Parameter (RFC 6152)
The BODY parameter specifies the message body encoding type:
- BODY=7BIT- 7-bit ASCII encoding (default)
- BODY=8BITMIME- 8-bit MIME encoding
// Client sends: MAIL FROM:<sender@example.com> BODY=8BITMIME
The selected body type is available in session.envelope.bodyType:
const server = new SMTPServer({
  onMailFrom(address, session, callback) {
    console.log(`Body type: ${session.envelope.bodyType}`); // '7bit' or '8bitmime'
    callback();
  },
});
Note: BINARYMIME is not supported as it requires the BDAT command (RFC 3030) which is not implemented.
SMTPUTF8 Parameter (RFC 6531)
The SMTPUTF8 parameter indicates that the client wants to use UTF-8 encoding in email addresses and headers:
// Client sends: MAIL FROM:<sender@example.com> SMTPUTF8
The UTF-8 flag is available in session.envelope.smtpUtf8:
const server = new SMTPServer({
  onMailFrom(address, session, callback) {
    if (session.envelope.smtpUtf8) {
      console.log("UTF-8 support requested");
    }
    callback();
  },
});
REQUIRETLS Parameter (RFC 8689)
The REQUIRETLS parameter indicates that the client requires TLS encryption for the entire delivery chain, not just the client-to-server connection.
Important: REQUIRETLS is disabled by default and must be explicitly enabled:
const server = new SMTPServer({
  hideREQUIRETLS: false, // Enable REQUIRETLS support
  onMailFrom(address, session, callback) {
    if (session.envelope.requireTLS) {
      console.log("TLS required for entire delivery chain");
      // Ensure downstream delivery also uses TLS
    }
    callback();
  },
});
Requirements:
- REQUIRETLS is only advertised over TLS connections (after STARTTLS or on initially secure connections)
- Clients can only use REQUIRETLS when connected via TLS
- If a client attempts to use REQUIRETLS without TLS, the server returns error code 530
// Client sends: MAIL FROM:<sender@example.com> REQUIRETLS
// Server checks: session.envelope.requireTLS === true
Combined Parameters Example
All MAIL FROM parameters can be used together:
const server = new SMTPServer({
  hideREQUIRETLS: false, // Enable REQUIRETLS
  onMailFrom(address, session, callback) {
    const { bodyType, smtpUtf8, requireTLS } = session.envelope;
    console.log(`
      Body Type: ${bodyType}
      UTF-8: ${smtpUtf8}
      Require TLS: ${requireTLS}
    `);
    // Validate requirements
    if (requireTLS && !session.secure) {
      return callback(new Error("TLS required but not established"));
    }
    callback();
  },
});
// Client sends: MAIL FROM:<sender@example.com> BODY=8BITMIME SMTPUTF8 REQUIRETLS
Parameter Validation
smtp-server automatically validates all MAIL FROM parameters:
- BODY must be 7BITor8BITMIME(case-insensitive)
- SMTPUTF8 is a flag and must not have a value
- REQUIRETLS is a flag and must not have a value
- REQUIRETLS can only be used over TLS connections
Invalid parameters return appropriate error codes (501 for syntax errors, 530 for TLS requirement violations).
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(RFC 6152)
- SMTPUTF8(RFC 6531)
- SIZE
- DSN(RFC 3461)
- ENHANCEDSTATUSCODES(RFC 2034/3463)
- REQUIRETLS(RFC 8689) - opt-in via- hideREQUIRETLS: false
The
CHUNKINGextension (BDAT command) is not implemented.