Create SMTP and LMTP server instances on the fly. This is not a full-blown server application like Haraka but an easy way to add custom SMTP listeners to your app. This module is the successor for the server part of the (now deprecated) SMTP module simplesmtp. For matching SMTP client see smtp-connection.
npm install smtp-server --save
const SMTPServer = require("smtp-server").SMTPServer;
const server = new SMTPServer(options);
Where
Additionally you can use the options from net.createServer and tls.createServer (applies if secure is set to true)
The server object returned from new SMTPServer has the following methods:
If you use secure:true option or you do not disable STARTTLS command then you SHOULD also define the key, cert and possibly ca properties to use a proper certificate. If you do no specify your own certificate then a pregenerated self-signed certificate for ‘localhost’ is used. Any respectful client refuses to accept such certificate.
Example
// This example starts a SMTP server using TLS with your own certificate and key
const server = new SMTPServer({
secure: true,
key: fs.readFileSync("private.key"),
cert: fs.readFileSync("server.crt"),
});
server.listen(465);
server.listen(port[,host][,callback]);
Where
Errors can be handled by setting an ‘error’ event listener to the server instance
server.on("error", (err) => {
console.log("Error %s", err.message);
});
Authentication calls can be handled with onAuth handler
const server = new SMTPServer({
onAuth(auth, session, callback) {},
});
Where
This module supports CRAM-MD5 but the use of it is discouraged as it requires access to unencrypted user passwords during the authentication process. You shouldn’t store passwords unencrypted.
const server = new SMTPServer({
onAuth(auth, session, callback) {
if (auth.username !== "abc" || auth.password !== "def") {
return callback(new Error("Invalid username or password"));
}
callback(null, { user: 123 }); // where 123 is the user id or similar property
},
});
XOAUTH2 support needs to enabled with the authMethods array option as it is disabled by default. If you support multiple authentication mechanisms, then you can check the used mechanism from the method property.
const server = new SMTPServer({
authMethods: ["XOAUTH2"], // XOAUTH2 is not enabled by default
onAuth(auth, session, callback) {
if (auth.method !== "XOAUTH2") {
// should never occur in this case as only XOAUTH2 is allowed
return callback(new Error("Expecting XOAUTH2"));
}
if (auth.username !== "abc" || auth.accessToken !== "def") {
return callback(null, {
data: {
status: "401",
schemes: "bearer mac",
scope: "my_smtp_access_scope_name",
},
});
}
callback(null, { user: 123 }); // where 123 is the user id or similar property
},
});
CRAM-MD5 support needs to enabled with the authMethods array option as it is disabled by default. If you support multiple authentication mechanisms, then you can check the used mechanism from the method property.
This authentication method does not return a password with the username but a response to a challenge. To validate the returned challenge response, the authentication object includes a method validatePassword that takes the actual plaintext password as an argument and returns either true if the password matches with the challenge response or false if it does not.
const server = new SMTPServer({
authMethods: ["CRAM-MD5"], // CRAM-MD5 is not enabled by default
onAuth(auth, session, callback) {
if (auth.method !== "CRAM-MD5") {
// should never occur in this case as only CRAM-MD5 is allowed
return callback(new Error("Expecting CRAM-MD5"));
}
// CRAM-MD5 does not provide a password but a challenge response
// that can be validated against the actual password of the user
if (auth.username !== "abc" || !auth.validatePassword("def")) {
return callback(new Error("Invalid username or password"));
}
callback(null, { user: 123 }); // where 123 is the user id or similar property
},
});
By default any client connection is allowed. If you want to check the remoteAddress or clientHostname before any other command, you can set a handler for it with onConnect
const server = new SMTPServer({
onConnect(session, callback) {},
});
Where
const server = new SMTPServer({
onConnect(session, callback) {
if (session.remoteAddress === "127.0.0.1") {
return callback(new Error("No connections from localhost allowed"));
}
return callback(); // Accept the connection
},
});
If you also need to detect when a connection is closed use onClose. This method does not expect you to run a callback function as it is purely informational.
const server = new SMTPServer({
onClose(session) {},
});
onSecure allows to validate TLS information for TLS and STARTTLS connections.
const server = new SMTPServer({
onSecure(socket, session, callback) {},
});
Where
const server = new SMTPServer({
onSecure(socket, session, callback) {
if (session.servername !== "sni.example.com") {
return callback(new Error("Only connections for sni.example.com are allowed"));
}
return callback(); // Accept the connection
},
});
By default all sender addresses (as long as these are in valid email format) are allowed. If you want to check the address before it is accepted you can set a handler for it with onMailFrom
const server = new SMTPServer({
onMailFrom(address, session, callback) {},
});
Where
const server = new SMTPServer({
onMailFrom(address, session, callback) {
if (address.address !== "allowed@example.com") {
return callback(new Error("Only allowed@example.com is allowed to send mail"));
}
return callback(); // Accept the address
},
});
By default all recipient addresses (as long as these are in valid email format) are allowed. If you want to check the address before it is accepted you can set a handler for it with onRcptTo
const server = new SMTPServer({
onRcptTo(address, session, callback) {},
});
Where
const server = new SMTPServer({
onRcptTo(address, session, callback) {
if (address.address !== "allowed@example.com") {
return callback(new Error("Only allowed@example.com is allowed to receive mail"));
}
return callback(); // Accept the address
},
});
You can get the stream for the incoming message with onData handler
const server = new SMTPServer({
onData(stream, session, callback) {},
});
Where
const server = new SMTPServer({
onData(stream, session, callback) {
stream.pipe(process.stdout); // print message to console
stream.on("end", callback);
},
});
This module does not prepend Received or any other header field to the streamed message. The entire message is streamed as-is with no modifications whatsoever. For compliancy you should add the Received data to the message yourself, see rfc5321 4.4. Trace Information for details.
When creating the server you can define maximum allowed message size with the size option, see RFC1870 for details. This is not a strict limitation, the client is informed about the size limit but the client can still send a larger message than allowed, it is up to your application to reject or accept the oversized message. To check if the message was oversized, see stream.sizeExceeded property.
const server = new SMTPServer({
size: 1024, // allow messages up to 1 kb
onRcptTo(address, session, callback) {
// do not accept messages larger than 100 bytes to specific recipients
let expectedSize = Number(session.envelope.mailFrom.args.SIZE) || 0;
if (address.address === "almost-full@example.com" && expectedSize > 100) {
err = new Error("Insufficient channel storage: " + address.address);
err.responseCode = 452;
return callback(err);
}
callback();
},
onData(stream, session, callback) {
stream.pipe(process.stdout); // print message to console
stream.on("end", () => {
let err;
if (stream.sizeExceeded) {
err = new Error("Message exceeds fixed maximum message size");
err.responseCode = 552;
return callback(err);
}
callback(null, "Message queued as abcdef");
});
},
});
If lmtp option is set to true when starting the server, then LMTP protocol is used instead of SMTP. The main difference between these two is how multiple recipients are handled. In case of SMTP the message either fails or succeeds but in LMTP the message might fail and succeed individually for every recipient.
If your LMTP server application does not distinguish between different recipients then you do not need to care about it. On the other hand if you want to report results separately for every recipient you can do this by providing an array of responses instead of a single error or success message. The array must contain responses in the same order as in the envelope rcptTo array.
const server = new SMTPServer({
lmtp: true,
onData(stream, session, callback) {
stream.pipe(process.stdout); // print message to console
stream.on("end", () => {
// reject every other recipient
let response = session.envelope.rcptTo.map((rcpt, i) => {
if (i % 2) {
return new Error("<" + rcpt.address + "> Not accepted");
} else {
return "<" + rcpt.address + "> Accepted";
}
});
callback(null, response);
});
},
});
If you provide a single error by invoking callback(err) or single success message callback(null, ‘OK’) like when dealing with SMTP then every recipient gets the same response.
Session object that is passed to the handler functions includes the following properties
Address object in the mailFrom and rcptTo values include the following properties
For example if the client runs the following commands:
C: MAIL FROM:<sender@example.com> SIZE=12345 RET=HDRS
C: RCPT TO:<recipient@example.com> NOTIFY=NEVER
then the envelope object is going go look like this:
{
"mailFrom": {
"address": "sender@example.com",
"args": {
"SIZE": "12345",
"RET": "HDRS"
}
},
"rcptTo": [
{
"address": "receiver@example.com",
"args": {
"NOTIFY": "NEVER"
}
}
]
}
Most notably, the ENHANCEDSTATUSCODES extension is not supported, all response codes use the standard three digit format and nothing else. I might change this in the future if I have time to revisit all responses and find the appropriate response codes.
CHUNKING is also missing. I might add support for it in the future but not at this moment since DATA already accepts a stream and CHUNKING is not supported everywhere.
MIT