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.
bannerString –Greeting appended to the standard ESMTP banner.
sizeNumber 0Maximum 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.
allowInsecureAuthBooleanfalseAllow authentication before TLS.
disableReverseLookupBooleanfalseSkip reverse DNS lookup of the client.
sniOptionsMap | Object –TLS 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 / onCloseFunction –Lifecycle callbacks detailed below.

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 and rcptTo arrays (see below).
useranyValue you returned from onAuth.
transactionNumber1 for the first message, 2 for the second, …
transmissionType"SMTP" | "ESMTP" | "ESMTPA" …Calculated for Received: headers.

Address object

{
"address": "sender@example.com",
"args": {
"SIZE": "12345",
"RET": "HDRS"
}
}
FieldDescription
addressThe literal address given in MAIL FROM:/RCPT TO:.
argsAdditional arguments (uppercase keys).

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

The ENHANCEDSTATUSCODES and CHUNKING extensions are not implemented.


License

MIT