Socket
Socket
Sign inDemoInstall

smtp-server

Package Overview
Dependencies
2
Maintainers
1
Versions
65
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.17.0 to 2.0.0

.eslintrc

5

CHANGELOG.md
# Changelog
## v2.0.0 2017-02-04
* Changed license from MIT to EUPL-v1.1
* Rewrite to use ES6, this means at least Node.js v6.0.0 is required to use smtp-server
## v1.16.1 2016-10-17

@@ -4,0 +9,0 @@

22

examples/lmtp.js

@@ -6,6 +6,6 @@ /* eslint no-console: 0 */

// Replace '../lib/smtp-server' with 'smtp-server' when running this script outside this directory
var SMTPServer = require('../lib/smtp-server').SMTPServer;
const SMTPServer = require('../lib/smtp-server').SMTPServer;
var SERVER_PORT = 2524;
var SERVER_HOST = '0.0.0.0';
const SERVER_PORT = 2524;
const SERVER_HOST = '0.0.0.0';

@@ -18,3 +18,3 @@ // Connect to this example server by running

// Setup server
var server = new SMTPServer({
const server = new SMTPServer({

@@ -37,3 +37,3 @@ // log to console

// If this method is not set, all addresses are allowed
onMailFrom: function (address, session, callback) {
onMailFrom(address, session, callback) {
if (/^deny/i.test(address.address)) {

@@ -47,4 +47,4 @@ return callback(new Error('Not accepted'));

// If this method is not set, all addresses are allowed
onRcptTo: function (address, session, callback) {
var err;
onRcptTo(address, session, callback) {
let err;

@@ -66,6 +66,6 @@ if (/^deny/i.test(address.address)) {

// Handle message stream
onData: function (stream, session, callback) {
onData(stream, session, callback) {
stream.pipe(process.stdout);
stream.on('end', function () {
var err;
stream.on('end', () => {
let err;
if (stream.sizeExceeded) {

@@ -81,3 +81,3 @@ err = new Error('Error: message exceeds fixed maximum message size 10 MB');

server.on('error', function (err) {
server.on('error', err => {
console.log('Error occurred');

@@ -84,0 +84,0 @@ console.log(err);

@@ -6,6 +6,6 @@ /* eslint no-console: 0 */

// Replace '../lib/smtp-server' with 'smtp-server' when running this script outside this directory
var SMTPServer = require('../lib/smtp-server').SMTPServer;
const SMTPServer = require('../lib/smtp-server').SMTPServer;
var SERVER_PORT = 2525;
var SERVER_HOST = false;
const SERVER_PORT = 2525;
const SERVER_HOST = false;

@@ -21,3 +21,3 @@ // Connect to this example server by running

// Setup server
var server = new SMTPServer({
const server = new SMTPServer({

@@ -49,5 +49,5 @@ // log to console

// Allow only users with username 'testuser' and password 'testpass'
onAuth: function (auth, session, callback) {
var username = 'testuser';
var password = 'testpass';
onAuth(auth, session, callback) {
let username = 'testuser';
let password = 'testpass';

@@ -72,3 +72,3 @@ // check username and password

// If this method is not set, all addresses are allowed
onMailFrom: function (address, session, callback) {
onMailFrom(address, session, callback) {
if (/^deny/i.test(address.address)) {

@@ -82,4 +82,4 @@ return callback(new Error('Not accepted'));

// If this method is not set, all addresses are allowed
onRcptTo: function (address, session, callback) {
var err;
onRcptTo(address, session, callback) {
let err;

@@ -101,6 +101,6 @@ if (/^deny/i.test(address.address)) {

// Handle message stream
onData: function (stream, session, callback) {
onData(stream, session, callback) {
stream.pipe(process.stdout);
stream.on('end', function () {
var err;
stream.on('end', () => {
let err;
if (stream.sizeExceeded) {

@@ -116,3 +116,3 @@ err = new Error('Error: message exceeds fixed maximum message size 10 MB');

server.on('error', function (err) {
server.on('error', err => {
console.log('Error occurred');

@@ -119,0 +119,0 @@ console.log(err);

'use strict';
var util = require('util');
var crypto = require('crypto');
const util = require('util');
const crypto = require('crypto');
var SASL = module.exports = {
const SASL = module.exports = {
SASL_PLAIN: function (args, callback) {
SASL_PLAIN(args, callback) {
if (args.length > 1) {

@@ -23,3 +23,3 @@ this.send(501, 'Error: syntax: AUTH PLAIN token');

SASL_LOGIN: function (args, callback) {
SASL_LOGIN(args, callback) {
if (args.length > 1) {

@@ -39,3 +39,3 @@ this.send(501, 'Error: syntax: AUTH LOGIN');

SASL_XOAUTH2: function (args, callback) {
SASL_XOAUTH2(args, callback) {
if (args.length > 1) {

@@ -55,3 +55,3 @@ this.send(501, 'Error: syntax: AUTH XOAUTH2 token');

'SASL_CRAM-MD5': function (args, callback) {
'SASL_CRAM-MD5' (args, callback) {
if (args.length) {

@@ -62,3 +62,3 @@ this.send(501, 'Error: syntax: AUTH CRAM-MD5');

var challenge = util.format('<%s%s@%s>',
let challenge = util.format('<%s%s@%s>',
String(Math.random()).replace(/^[0\.]+/, '').substr(0, 8), // random numbers

@@ -74,3 +74,3 @@ Math.floor(Date.now() / 1000), // timestamp

PLAIN_token: function (canAbort, token, callback) {
PLAIN_token(canAbort, token, callback) {
token = (token || '').toString().trim();

@@ -83,3 +83,3 @@

var data = new Buffer(token, 'base64').toString().split('\x00');
let data = new Buffer(token, 'base64').toString().split('\x00');

@@ -91,13 +91,19 @@ if (data.length !== 3) {

var username = data[1] || data[0] || '';
var password = data[2] || '';
let username = data[1] || data[0] || '';
let password = data[2] || '';
this._server.onAuth({
method: 'PLAIN',
username: username,
password: password
}, this.session, function (err, response) {
username,
password
}, this.session, (err, response) => {
if (err) {
this._server.logger.info('[%s] Authentication error for %s using %s\n%s', this._id, username, 'PLAIN', err.message);
this._server.logger.info({
err,
tnx: 'autherror',
cid: this._id,
method: 'PLAIN',
user: username
}, '[%s] Authentication error for %s using %s. %s', this._id, username, 'PLAIN', err.message);
this.send(err.responseCode || 535, err.message);

@@ -108,3 +114,8 @@ return callback();

if (!response.user) {
this._server.logger.info('[%s] Authentication failed for %s using %s', this._id, username, 'PLAIN');
this._server.logger.info({
tnx: 'authfail',
cid: this._id,
method: 'PLAIN',
user: username
}, '[%s] Authentication failed for %s using %s', this._id, username, 'PLAIN');
this.send(response.responseCode || 535, response.message || 'Error: Authentication credentials invalid');

@@ -114,3 +125,8 @@ return callback();

this._server.logger.info('[%s] %s authenticated using %s', this._id, username, 'PLAIN');
this._server.logger.info({
tnx: 'auth',
cid: this._id,
method: 'PLAIN',
user: username
}, '[%s] %s authenticated using %s', this._id, username, 'PLAIN');
this.session.user = response.user;

@@ -122,6 +138,6 @@ this.session.transmissionType = this._transmissionType();

}.bind(this));
});
},
LOGIN_username: function (canAbort, username, callback) {
LOGIN_username(canAbort, username, callback) {
username = (username || '').toString().trim();

@@ -146,3 +162,3 @@

LOGIN_password: function (username, password, callback) {
LOGIN_password(username, password, callback) {
password = (password || '').toString().trim();

@@ -159,8 +175,14 @@

method: 'LOGIN',
username: username,
password: password
}, this.session, function (err, response) {
username,
password
}, this.session, (err, response) => {
if (err) {
this._server.logger.info('[%s] Authentication error for %s using %s\n%s', this._id, username, 'LOGIN', err.message);
this._server.logger.info({
err,
tnx: 'autherror',
cid: this._id,
method: 'LOGIN',
user: username
}, '[%s] Authentication error for %s using %s. %s', this._id, username, 'LOGIN', err.message);
this.send(err.responseCode || 535, err.message);

@@ -171,3 +193,8 @@ return callback();

if (!response.user) {
this._server.logger.info('[%s] Authentication failed for %s using %s', this._id, username, 'LOGIN');
this._server.logger.info({
tnx: 'authfail',
cid: this._id,
method: 'LOGIN',
user: username
}, '[%s] Authentication failed for %s using %s', this._id, username, 'LOGIN');
this.send(response.responseCode || 535, response.message || 'Error: Authentication credentials invalid');

@@ -177,3 +204,8 @@ return callback();

this._server.logger.info('[%s] %s authenticated using %s', this._id, username, 'LOGIN');
this._server.logger.info({
tnx: 'auth',
cid: this._id,
method: 'PLAIN',
user: username
}, '[%s] %s authenticated using %s', this._id, username, 'LOGIN');
this.session.user = response.user;

@@ -185,6 +217,6 @@ this.session.transmissionType = this._transmissionType();

}.bind(this));
});
},
XOAUTH2_token: function (canAbort, token, callback) {
XOAUTH2_token(canAbort, token, callback) {
token = (token || '').toString().trim();

@@ -197,10 +229,10 @@

var username;
var accessToken;
let username;
let accessToken;
// Find username and access token from the input
new Buffer(token, 'base64').toString().split('\x01').forEach(function (part) {
new Buffer(token, 'base64').toString().split('\x01').forEach(part => {
part = part.split('=');
var key = part.shift().toLowerCase();
var value = part.join('=').trim();
let key = part.shift().toLowerCase();
let value = part.join('=').trim();

@@ -224,8 +256,14 @@ if (key === 'user') {

method: 'XOAUTH2',
username: username,
accessToken: accessToken
}, this.session, function (err, response) {
username,
accessToken
}, this.session, (err, response) => {
if (err) {
this._server.logger.info('[%s] Authentication error for %s using %s\n%s', this._id, username, 'XOAUTH2', err.message);
this._server.logger.info({
err,
tnx: 'autherror',
cid: this._id,
method: 'XOAUTH2',
user: username
}, '[%s] Authentication error for %s using %s. %s', this._id, username, 'XOAUTH2', err.message);
this.send(err.responseCode || 535, err.message);

@@ -236,3 +274,8 @@ return callback();

if (!response.user) {
this._server.logger.info('[%s] Authentication failed for %s using %s', this._id, username, 'XOAUTH2');
this._server.logger.info({
tnx: 'authfail',
cid: this._id,
method: 'XOAUTH2',
user: username
}, '[%s] Authentication failed for %s using %s', this._id, username, 'XOAUTH2');
this._nextHandler = SASL.XOAUTH2_error.bind(this);

@@ -243,3 +286,8 @@ this.send(response.responseCode || 334, new Buffer(JSON.stringify(response.data || {})).toString('base64'));

this._server.logger.info('[%s] %s authenticated using %s', this._id, username, 'XOAUTH2');
this._server.logger.info({
tnx: 'auth',
cid: this._id,
method: 'XOAUTH2',
user: username
}, '[%s] %s authenticated using %s', this._id, username, 'XOAUTH2');
this.session.user = response.user;

@@ -251,6 +299,6 @@ this.session.transmissionType = this._transmissionType();

}.bind(this));
});
},
XOAUTH2_error: function (data, callback) {
XOAUTH2_error(data, callback) {
this.send(535, 'Error: Username and Password not accepted');

@@ -260,3 +308,3 @@ return callback();

'CRAM-MD5_token': function (canAbort, challenge, token, callback) {
'CRAM-MD5_token' (canAbort, challenge, token, callback) {
token = (token || '').toString().trim();

@@ -269,17 +317,23 @@

var tokenParts = new Buffer(token, 'base64').toString().split(' ');
var username = tokenParts.shift();
var challengeResponse = (tokenParts.shift() || '').toLowerCase();
let tokenParts = new Buffer(token, 'base64').toString().split(' ');
let username = tokenParts.shift();
let challengeResponse = (tokenParts.shift() || '').toLowerCase();
this._server.onAuth({
method: 'CRAM-MD5',
username: username,
validatePassword: function (password) {
var hmac = crypto.createHmac('md5', password);
username,
validatePassword(password) {
let hmac = crypto.createHmac('md5', password);
return hmac.update(challenge).digest('hex').toLowerCase() === challengeResponse;
}
}, this.session, function (err, response) {
}, this.session, (err, response) => {
if (err) {
this._server.logger.info('[%s] Authentication error for %s using %s\n%s', this._id, username, 'CRAM-MD5', err.message);
this._server.logger.info({
err,
tnx: 'autherror',
cid: this._id,
method: 'CRAM-MD5',
user: username
}, '[%s] Authentication error for %s using %s. %s', this._id, username, 'CRAM-MD5', err.message);
this.send(err.responseCode || 535, err.message);

@@ -290,3 +344,8 @@ return callback();

if (!response.user) {
this._server.logger.info('[%s] Authentication failed for %s using %s', this._id, username, 'CRAM-MD5');
this._server.logger.info({
tnx: 'authfail',
cid: this._id,
method: 'CRAM-MD5',
user: username
}, '[%s] Authentication failed for %s using %s', this._id, username, 'CRAM-MD5');
this.send(response.responseCode || 535, response.message || 'Error: Authentication credentials invalid');

@@ -296,3 +355,8 @@ return callback();

this._server.logger.info('[%s] %s authenticated using %s', this._id, username, 'CRAM-MD5');
this._server.logger.info({
tnx: 'auth',
cid: this._id,
method: 'CRAM-MD5',
user: username
}, '[%s] %s authenticated using %s', this._id, username, 'CRAM-MD5');
this.session.user = response.user;

@@ -304,4 +368,4 @@ this.session.transmissionType = this._transmissionType();

}.bind(this));
});
}
};
'use strict';
var SMTPStream = require('./smtp-stream').SMTPStream;
var dns = require('dns');
var tls = require('tls');
var net = require('net');
var ipv6normalize = require('ipv6-normalize');
var sasl = require('./sasl');
var crypto = require('crypto');
var os = require('os');
var punycode = require('punycode');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
const SMTPStream = require('./smtp-stream').SMTPStream;
const dns = require('dns');
const tls = require('tls');
const net = require('net');
const ipv6normalize = require('ipv6-normalize');
const sasl = require('./sasl');
const crypto = require('crypto');
const os = require('os');
const punycode = require('punycode');
const EventEmitter = require('events');
var SOCKET_TIMEOUT = 60 * 1000;
const SOCKET_TIMEOUT = 60 * 1000;
// Expose to the world
module.exports.SMTPConnection = SMTPConnection;
/**

@@ -27,1164 +23,1256 @@ * Creates a handler for new socket

*/
function SMTPConnection(server, socket) {
EventEmitter.call(this);
class SMTPConnection extends EventEmitter {
constructor(server, socket) {
super();
// Random session ID, used for logging
this._id = crypto.randomBytes(9).toString('base64');
// Random session ID, used for logging
this._id = crypto.randomBytes(9).toString('base64');
this._server = server;
this._socket = socket;
this._server = server;
this._socket = socket;
// session data (envelope, user etc.)
this.session = this.session = {
id: this._id
};
// session data (envelope, user etc.)
this.session = this.session = {
id: this._id
};
// how many messages have been processed
this._transactionCounter = 0;
// how many messages have been processed
this._transactionCounter = 0;
// Do not allow input from client until initial greeting has been sent
this._ready = false;
// Do not allow input from client until initial greeting has been sent
this._ready = false;
// If true then the connection is currently being upgraded to TLS
this._upgrading = false;
// If true then the connection is currently being upgraded to TLS
this._upgrading = false;
// Set handler for incoming command and handler bypass detection by command name
this._nextHandler = false;
// Set handler for incoming command and handler bypass detection by command name
this._nextHandler = false;
// Parser instance for the incoming stream
this._parser = new SMTPStream();
// Parser instance for the incoming stream
this._parser = new SMTPStream();
// Set handler for incoming commands
this._parser.oncommand = (...args) => this._onCommand(...args);
// Set handler for incoming commands
this._parser.oncommand = this._onCommand.bind(this);
// if currently in data mode, this stream gets the content of incoming message
this._dataStream = false;
// if currently in data mode, this stream gets the content of incoming message
this._dataStream = false;
// If true, then the connection is using TLS
this.secure = !!this._server.options.secure;
// If true, then the connection is using TLS
this.secure = !!this._server.options.secure;
this.tlsOptions = this.secure ? this._socket.getCipher() : false;
this.tlsOptions = this.secure ? this._socket.getCipher() : false;
// Store remote address for later usage
this.remoteAddress = (this._socket.remoteAddress || '').replace(/^::ffff:/, '');
this.remotePort = Number(this._socket.remotePort) || 0;
// Store remote address for later usage
this.remoteAddress = (this._socket.remoteAddress || '').replace(/^::ffff:/, '');
this.remotePort = Number(this._socket.remotePort) || 0;
// normalize IPv6 addresses
if (this.remoteAddress && net.isIPv6(this.remoteAddress)) {
this.remoteAddress = ipv6normalize(this.remoteAddress);
}
// normalize IPv6 addresses
if (this.remoteAddress && net.isIPv6(this.remoteAddress)) {
this.remoteAddress = ipv6normalize(this.remoteAddress);
}
// Error counter - if too many commands in non-authenticated state are used, then disconnect
this._unauthenticatedCommands = 0;
// Error counter - if too many commands in non-authenticated state are used, then disconnect
this._unauthenticatedCommands = 0;
// Error counter - if too many invalid commands are used, then disconnect
this._unrecognizedCommands = 0;
// Error counter - if too many invalid commands are used, then disconnect
this._unrecognizedCommands = 0;
// Server hostname for the greegins
this.name = this._server.options.name || os.hostname();
// Server hostname for the greegins
this.name = this._server.options.name || os.hostname();
// Resolved hostname for remote IP address
this.clientHostname = false;
// Resolved hostname for remote IP address
this.clientHostname = false;
// The opening SMTP command (HELO, EHLO or LHLO)
this.openingCommand = false;
// The opening SMTP command (HELO, EHLO or LHLO)
this.openingCommand = false;
// The hostname client identifies itself with
this.hostNameAppearsAs = false;
// The hostname client identifies itself with
this.hostNameAppearsAs = false;
// data passed from XCLIENT command
this._xClient = new Map();
// data passed from XCLIENT command
this._xClient = new Map();
// data passed from XFORWARD command
this._xForward = new Map();
// data passed from XFORWARD command
this._xForward = new Map();
// if true then can emit connection info
this._canEmitConnection = true;
// if true then can emit connection info
this._canEmitConnection = true;
// increment connection count
this._closing = false;
this._closed = false;
}
// increment connection count
this._closing = false;
this._closed = false;
}
util.inherits(SMTPConnection, EventEmitter);
/**
* Initiates the connection. Checks connection limits and reverse resolves client hostname. The client
* is not allowed to send anything before init has finished otherwise 'You talk too soon' error is returned
*/
init() {
// Setup event handlers for the socket
this._setListeners();
/**
* Initiates the connection. Checks connection limits and reverse resolves client hostname. The client
* is not allowed to send anything before init has finished otherwise 'You talk too soon' error is returned
*/
SMTPConnection.prototype.init = function () {
// Setup event handlers for the socket
this._setListeners();
// Check that connection limit is not exceeded
if (this._server.options.maxClients && this._server.connections.size > this._server.options.maxClients) {
return this.send(421, this.name + ' Too many connected clients, try again in a moment');
}
// Check that connection limit is not exceeded
if (this._server.options.maxClients && this._server.connections.size > this._server.options.maxClients) {
return this.send(421, this.name + ' Too many connected clients, try again in a moment');
if (!this._server.options.useProxy) {
// Keep a small delay for detecting early talkers
setTimeout(() => this.connectionReady(), 100);
}
}
if (!this._server.options.useProxy) {
// Keep a small delay for detecting early talkers
setTimeout(this.connectionReady.bind(this), 100);
}
};
connectionReady(next) {
SMTPConnection.prototype.connectionReady = function (next) {
// Resolve hostname for the remote IP
let reverseCb = (err, hostnames) => {
if (err) {
// ignore resolve error
}
// Resolve hostname for the remote IP
var reverseCb = function (err, hostnames) {
if (err) {
// ignore resolve error
}
if (this._closing || this._closed) {
return;
}
if (this._closing || this._closed) {
return;
}
this.clientHostname = hostnames && hostnames.shift() || '[' + this.remoteAddress + ']';
this.clientHostname = hostnames && hostnames.shift() || '[' + this.remoteAddress + ']';
this._resetSession();
this._resetSession();
this._server.onConnect(this.session, err => {
if (err) {
this.send(err.responseCode || 554, err.message);
}
this._server.onConnect(this.session, function (err) {
if (err) {
this.send(err.responseCode || 554, err.message);
}
this._ready = true; // Start accepting data from input
this._ready = true; // Start accepting data from input
if (!this._server.options.useXClient && !this._server.options.useXForward) {
this.emitConnection();
}
if (!this._server.options.useXClient && !this._server.options.useXForward) {
this.emitConnection();
}
this._server.logger.info({
tnx: 'connection',
cid: this._id,
host: this.remoteAddress,
hostname: this.clientHostname
}, '[%s] Connection from %s', this._id, this.clientHostname);
this._server.logger.info('[%s] Connection from %s', this._id, this.clientHostname);
this.send(220, this.name + ' ' + (this._server.options.lmtp ? 'LMTP' : 'ESMTP') + (this._server.options.banner ? ' ' + this._server.options.banner : ''));
if (typeof next === 'function') {
next();
}
}.bind(this));
}.bind(this);
this.send(220, this.name + ' ' + (this._server.options.lmtp ? 'LMTP' : 'ESMTP') + (this._server.options.banner ? ' ' + this._server.options.banner : ''));
if (typeof next === 'function') {
next();
}
});
};
// Skip reverse name resolution if disabled.
if (this._server.options.disableReverseLookup) {
return reverseCb(null, false);
}
// Skip reverse name resolution if disabled.
if (this._server.options.disableReverseLookup) {
return reverseCb(null, false);
}
try {
// dns.reverse throws on invalid input, see https://github.com/nodejs/node/issues/3112
dns.reverse(this.remoteAddress.toString(), reverseCb);
} catch (E) {
reverseCb(E);
try {
// dns.reverse throws on invalid input, see https://github.com/nodejs/node/issues/3112
dns.reverse(this.remoteAddress.toString(), reverseCb);
} catch (E) {
reverseCb(E);
}
}
};
/**
* Send data to socket
*
* @param {Number} code Response code
* @param {String|Array} data If data is Array, send a multi-line response
*/
send(code, data) {
let payload;
/**
* Send data to socket
*
* @param {Number} code Response code
* @param {String|Array} data If data is Array, send a multi-line response
*/
SMTPConnection.prototype.send = function (code, data) {
var payload;
if (Array.isArray(data)) {
payload = data.map((line, i, arr) => code + (i < arr.length - 1 ? '-' : ' ') + line).join('\r\n');
} else {
payload = [].concat(code || []).concat(data || []).join(' ');
}
if (Array.isArray(data)) {
payload = data.map(function (line, i, arr) {
return code + (i < arr.length - 1 ? '-' : ' ') + line;
}).join('\r\n');
} else {
payload = [].concat(code || []).concat(data || []).join(' ');
}
if (this._socket && this._socket.writable) {
this._socket.write(payload + '\r\n');
this._server.logger.debug({
tnx: 'send',
cid: this._id,
user: this.session.user && this.session.user.username
}, '[%s] S:', this._id, payload);
}
if (this._socket && this._socket.writable) {
this._socket.write(payload + '\r\n');
this._server.logger.debug('[%s] S:', this._id, payload);
if (code === 421) {
this.close();
}
}
if (code === 421) {
this.close();
/**
* Close socket
*/
close() {
if (!this._socket.destroyed && this._socket.writable) {
this._socket.end();
}
this._server.connections.delete(this);
this._closing = true;
}
};
/**
* Close socket
*/
SMTPConnection.prototype.close = function () {
if (!this._socket.destroyed && this._socket.writable) {
this._socket.end();
// PRIVATE METHODS
/**
* Setup socket event handlers
*/
_setListeners() {
this._socket.on('close', () => this._onClose());
this._socket.on('error', err => this._onError(err));
this._socket.setTimeout(this._server.options.socketTimeout || SOCKET_TIMEOUT, () => this._onTimeout());
this._socket.pipe(this._parser);
}
this._server.connections.delete(this);
/**
* Fired when the socket is closed
* @event
*/
_onClose( /* hadError */ ) {
if (this._parser) {
this._parser.closed = true;
this._socket.unpipe(this._parser);
this._parser = false;
}
this._closing = true;
};
if (this._dataStream) {
this._dataStream.unpipe();
this._dataStream = null;
}
// PRIVATE METHODS
this._server.connections.delete(this);
/**
* Setup socket event handlers
*/
SMTPConnection.prototype._setListeners = function () {
this._socket.on('close', this._onClose.bind(this));
this._socket.on('error', this._onError.bind(this));
this._socket.setTimeout(this._server.options.socketTimeout || SOCKET_TIMEOUT, this._onTimeout.bind(this));
this._socket.pipe(this._parser);
};
if (this._closed) {
return;
}
/**
* Fired when the socket is closed
* @event
*/
SMTPConnection.prototype._onClose = function ( /* hadError */ ) {
if (this._parser) {
this._parser.closed = true;
this._socket.unpipe(this._parser);
this._parser = false;
}
this._closed = true;
this._closing = false;
if (this._dataStream) {
this._dataStream.unpipe();
this._dataStream = null;
this._server.logger.info({
tnx: 'close',
cid: this._id,
host: this.remoteAddress,
user: this.session.user && this.session.user.username
}, '[%s] Connection closed to %s', this._id, this.clientHostname || this.remoteAddress);
setImmediate(() => this._server.onClose(this.session));
}
this._server.connections.delete(this);
/**
* Fired when an error occurs with the socket
*
* @event
* @param {Error} err Error object
*/
_onError(err) {
if ((err.code === 'ECONNRESET' || err.code === 'EPIPE') &&
(!this.session.envelope || !this.session.envelope.mailFrom)) {
// We got a connection error outside transaction. In most cases it means dirty
// connection ending by the other party, so we can just ignore it
this.close(); // mark connection as 'closing'
return;
}
if (this._closed) {
return;
this._server.logger.error({
err,
tnx: 'error',
user: this.session.user && this.session.user.username
}, '[%s]', this._id, err);
this.emit('error', err);
}
this._closed = true;
this._closing = false;
this._server.logger.info('[%s] Connection closed to %s', this._id, this.clientHostname || this.remoteAddress);
setImmediate(this._server.onClose.bind(this._server, this.session));
};
/**
* Fired when an error occurs with the socket
*
* @event
* @param {Error} err Error object
*/
SMTPConnection.prototype._onError = function (err) {
if ((err.code === 'ECONNRESET' || err.code === 'EPIPE') &&
(!this.session.envelope || !this.session.envelope.mailFrom)) {
// We got a connection error outside transaction. In most cases it means dirty
// connection ending by the other party, so we can just ignore it
this.close(); // mark connection as 'closing'
return;
/**
* Fired when socket timeouts. Closes connection
*
* @event
*/
_onTimeout() {
this.send(421, 'Timeout - closing connection');
}
this._server.logger.error('[%s]', this._id, err);
this.emit('error', err);
};
/**
* Checks if a selected command is available and ivokes it
*
* @param {Buffer} command Single line of data from the client
* @param {Function} callback Callback to run once the command is processed
*/
_onCommand(command, callback) {
let commandName = (command || '').toString().split(' ').shift().toUpperCase();
this._server.logger.debug({
tnx: 'command',
cid: this._id,
command: commandName,
user: this.session.user && this.session.user.username
}, '[%s] C:', this._id, (command || '').toString());
/**
* Fired when socket timeouts. Closes connection
*
* @event
*/
SMTPConnection.prototype._onTimeout = function () {
this.send(421, 'Timeout - closing connection');
};
let handler;
let params;
/**
* Checks if a selected command is available and ivokes it
*
* @param {Buffer} command Single line of data from the client
* @param {Function} callback Callback to run once the command is processed
*/
SMTPConnection.prototype._onCommand = function (command, callback) {
this._server.logger.debug('[%s] C:', this._id, (command || '').toString());
if (!this._ready) {
if (this._server.options.useProxy) {
params = (command || '').toString().split(' ');
commandName = params.shift().toUpperCase();
if (commandName !== 'PROXY') {
this.send(500, 'Invalid PROXY header');
return this.close();
}
var commandName;
var handler;
var params;
if (params[1]) {
this._server.logger.info({
tnx: 'proxy',
cid: this._id,
proxy: params[1].trim().toLowerCase(),
destination: this.remoteAddress,
user: this.session.user && this.session.user.username
}, '[%s] PROXY from %s through %s', this._id, params[1].trim().toLowerCase(), this.remoteAddress);
this.remoteAddress = params[1].trim().toLowerCase();
if (params[3]) {
this.remotePort = Number(params[3].trim()) || this.remotePort;
}
this.emitConnection();
}
if (!this._ready) {
if (this._server.options.useProxy) {
params = (command || '').toString().split(' ');
commandName = params.shift().toUpperCase();
if (commandName !== 'PROXY') {
this.send(500, 'Invalid PROXY header');
return this.close();
return this.connectionReady(callback);
} else {
// block spammers that send payloads before server greeting
return this.send(421, this.name + ' You talk too soon');
}
}
if (params[1]) {
this._server.logger.info('[%s] PROXY from %s through %s', this._id, params[1].trim().toLowerCase(), this.remoteAddress);
this.remoteAddress = params[1].trim().toLowerCase();
if (params[3]) {
this.remotePort = Number(params[3].trim()) || this.remotePort;
}
this.emitConnection();
}
return this.connectionReady(callback);
} else {
// block spammers that send payloads before server greeting
return this.send(421, this.name + ' You talk too soon');
// block malicious web pages that try to make SMTP calls from an AJAX request
if (/^(OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT) \/.* HTTP\/\d\.\d$/i.test(command)) {
return this.send(421, 'HTTP requests not allowed');
}
}
// block malicious web pages that try to make SMTP calls from an AJAX request
if (/^(OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE|CONNECT) \/.* HTTP\/\d\.\d$/i.test(command)) {
return this.send(421, 'HTTP requests not allowed');
}
callback = callback || (() => false);
callback = callback || function () {};
if (this._upgrading) {
// ignore any commands before TLS upgrade is finished
return callback();
}
if (this._upgrading) {
// ignore any commands before TLS upgrade is finished
return callback();
}
if (this._nextHandler) {
// If we already have a handler method queued up then use this
handler = this._nextHandler;
this._nextHandler = false;
} else {
// detect handler from the command name
commandName = (command || '').toString().split(' ').shift().toUpperCase();
switch (commandName) {
case 'HELO':
case 'EHLO':
case 'LHLO':
this.openingCommand = commandName;
break;
}
if (this._server.options.lmtp) {
if (this._nextHandler) {
// If we already have a handler method queued up then use this
handler = this._nextHandler;
this._nextHandler = false;
} else {
// detect handler from the command name
switch (commandName) {
case 'HELO':
case 'EHLO':
this.send(500, 'Error: ' + commandName + ' not allowed in LMTP server');
return setImmediate(callback);
case 'LHLO':
commandName = 'EHLO';
this.openingCommand = commandName;
break;
}
if (this._server.options.lmtp) {
switch (commandName) {
case 'HELO':
case 'EHLO':
this.send(500, 'Error: ' + commandName + ' not allowed in LMTP server');
return setImmediate(callback);
case 'LHLO':
commandName = 'EHLO';
break;
}
}
if (this._isSupported(commandName)) {
handler = this['handler_' + commandName];
}
}
if (this._isSupported(commandName)) {
handler = this['handler_' + commandName];
if (!handler) {
// if the user makes more
this._unrecognizedCommands++;
if (this._unrecognizedCommands >= 10) {
return this.send(421, 'Error: too many unrecognized commands');
}
this.send(500, 'Error: command not recognized');
return setImmediate(callback);
}
}
if (!handler) {
// if the user makes more
this._unrecognizedCommands++;
if (this._unrecognizedCommands >= 10) {
return this.send(421, 'Error: too many unrecognized commands');
// block users that try to fiddle around without logging in
if (!this.session.user && this._isSupported('AUTH') && commandName !== 'AUTH') {
this._unauthenticatedCommands++;
if (this._unauthenticatedCommands >= 10) {
return this.send(421, 'Error: too many unauthenticated commands');
}
}
this.send(500, 'Error: command not recognized');
return setImmediate(callback);
}
if (!this.hostNameAppearsAs && commandName && ['MAIL', 'RCPT', 'DATA', 'AUTH'].includes(commandName)) {
this.send(503, 'Error: send ' + (this._server.options.lmtp ? 'LHLO' : 'HELO/EHLO') + ' first');
return setImmediate(callback);
}
// block users that try to fiddle around without logging in
if (!this.session.user && this._isSupported('AUTH') && commandName !== 'AUTH') {
this._unauthenticatedCommands++;
if (this._unauthenticatedCommands >= 10) {
return this.send(421, 'Error: too many unauthenticated commands');
// Check if authentication is required
if (!this.session.user && this._isSupported('AUTH') && ['MAIL', 'RCPT', 'DATA'].includes(commandName) && !this._server.options.authOptional) {
this.send(530, 'Error: authentication Required');
return setImmediate(callback);
}
}
if (!this.hostNameAppearsAs && commandName && ['MAIL', 'RCPT', 'DATA', 'AUTH'].indexOf(commandName) >= 0) {
this.send(503, 'Error: send ' + (this._server.options.lmtp ? 'LHLO' : 'HELO/EHLO') + ' first');
return setImmediate(callback);
handler.call(this, command, callback);
}
// Check if authentication is required
if (!this.session.user && this._isSupported('AUTH') && ['MAIL', 'RCPT', 'DATA'].indexOf(commandName) >= 0 && !this._server.options.authOptional) {
this.send(530, 'Error: authentication Required');
return setImmediate(callback);
/**
* Checks that a command is available and is not listed in the disabled commands array
*
* @param {String} command Command name
* @returns {Boolean} Returns true if the command can be used
*/
_isSupported(command) {
command = (command || '').toString().trim().toUpperCase();
return !this._server.options.disabledCommands.includes(command) &&
typeof this['handler_' + command] === 'function';
}
handler.call(this, command, callback);
};
/**
* Parses commands like MAIL FROM and RCPT TO. Returns an object with the address and optional arguments.
*
* @param {[type]} name Address type, eg 'mail from' or 'rcpt to'
* @param {[type]} command Data payload to parse
* @returns {Object|Boolean} Parsed address in the form of {address:, args: {}} or false if parsing failed
*/
_parseAddressCommand(name, command) {
command = (command || '').toString();
name = (name || '').toString().trim().toUpperCase();
/**
* Checks that a command is available and is not listed in the disabled commands array
*
* @param {String} command Command name
* @returns {Boolean} Returns true if the command can be used
*/
SMTPConnection.prototype._isSupported = function (command) {
command = (command || '').toString().trim().toUpperCase();
return this._server.options.disabledCommands.indexOf(command) < 0 &&
typeof this['handler_' + command] === 'function';
};
let parts = command.split(':');
command = parts.shift().trim().toUpperCase();
parts = parts.join(':').trim().split(/\s+/);
/**
* Parses commands like MAIL FROM and RCPT TO. Returns an object with the address and optional arguments.
*
* @param {[type]} name Address type, eg 'mail from' or 'rcpt to'
* @param {[type]} command Data payload to parse
* @returns {Object|Boolean} Parsed address in the form of {address:, args: {}} or false if parsing failed
*/
SMTPConnection.prototype._parseAddressCommand = function (name, command) {
command = (command || '').toString();
name = (name || '').toString().trim().toUpperCase();
let address = parts.shift();
let args = false;
let invalid = false;
var parts = command.split(':');
command = parts.shift().trim().toUpperCase();
parts = parts.join(':').trim().split(/\s+/);
if (name !== command) {
return false;
}
var address = parts.shift();
var args = false;
var invalid = false;
if (!/^<[^<>]*>$/.test(address)) {
invalid = true;
} else {
address = address.substr(1, address.length - 2);
}
if (name !== command) {
return false;
}
parts.forEach(part => {
part = part.split('=');
let key = part.shift().toUpperCase();
let value = part.join('=') || true;
if (!/^<[^<>]*>$/.test(address)) {
invalid = true;
} else {
address = address.substr(1, address.length - 2);
}
if (typeof value === 'string') {
// decode 'xtext'
value = value.replace(/\+([0-9A-F]{2})/g, (match, hex) => unescape('%' + hex));
}
parts.forEach(function (part) {
part = part.split('=');
var key = part.shift().toUpperCase();
var value = part.join('=') || true;
if (!args) {
args = {};
}
if (typeof value === 'string') {
// decode 'xtext'
value = value.replace(/\+([0-9A-F]{2})/g, function (match, hex) {
return unescape('%' + hex);
});
}
args[key] = value;
});
if (!args) {
args = {};
if (address) {
// enforce unycode
address = address.split('@');
if (address.length !== 2 || !address[0] || !address[1]) { // really bad e-mail address validation. was not able to use joi because of the missing unicode support
invalid = true;
} else {
address = [address[0] || '', '@', punycode.toUnicode(address[1] || '')].join('');
}
}
args[key] = value;
});
if (address) {
// enforce unycode
address = address.split('@');
if (address.length !== 2 || !address[0] || !address[1]) { // really bad e-mail address validation. was not able to use joi because of the missing unicode support
invalid = true;
} else {
address = [address[0] || '', '@', punycode.toUnicode(address[1] || '')].join('');
}
return invalid ? false : {
address,
args
};
}
return invalid ? false : {
address: address,
args: args
};
};
/**
* Resets or sets up a new session. We reuse existing session object to keep
* application specific data.
*/
_resetSession() {
/**
* Resets or sets up a new session. We reuse existing session object to keep
* application specific data.
*/
SMTPConnection.prototype._resetSession = function () {
let session = this.session;
var session = this.session;
// reset data that might be overwritten
session.remoteAddress = this.remoteAddress;
session.remotePort = this.remotePort;
session.clientHostname = this.clientHostname;
session.openingCommand = this.openingCommand;
session.hostNameAppearsAs = this.hostNameAppearsAs;
session.xClient = this._xClient;
session.xForward = this._xForward;
session.transmissionType = this._transmissionType();
// reset data that might be overwritten
session.remoteAddress = this.remoteAddress;
session.remotePort = this.remotePort;
session.clientHostname = this.clientHostname;
session.openingCommand = this.openingCommand;
session.hostNameAppearsAs = this.hostNameAppearsAs;
session.xClient = this._xClient;
session.xForward = this._xForward;
session.transmissionType = this._transmissionType();
session.tlsOptions = this.tlsOptions;
session.tlsOptions = this.tlsOptions;
// reset transaction properties
session.envelope = {
mailFrom: false,
rcptTo: []
};
// reset transaction properties
session.envelope = {
mailFrom: false,
rcptTo: []
};
session.transaction = this._transactionCounter + 1;
}
session.transaction = this._transactionCounter + 1;
};
/**
* Returns current transmission type
*
* @return {String} Transmission type
*/
_transmissionType() {
let type = this._server.options.lmtp ? 'LMTP' : 'SMTP';
/**
* Returns current transmission type
*
* @return {String} Transmission type
*/
SMTPConnection.prototype._transmissionType = function () {
var type = this._server.options.lmtp ? 'LMTP' : 'SMTP';
if (this.openingCommand === 'EHLO') {
type = 'E' + type;
}
if (this.openingCommand === 'EHLO') {
type = 'E' + type;
}
if (this.secure) {
type += 'S';
}
if (this.secure) {
type += 'S';
}
if (this.session.user) {
type += 'A';
}
if (this.session.user) {
type += 'A';
return type;
}
return type;
};
SMTPConnection.prototype.emitConnection = function () {
if (!this._canEmitConnection) {
return;
emitConnection() {
if (!this._canEmitConnection) {
return;
}
this._canEmitConnection = false;
this.emit('connect', {
remoteAddress: this.remoteAddress,
remotePort: this.remotePort,
hostNameAppearsAs: this.hostNameAppearsAs,
clientHostname: this.clientHostname
});
}
this._canEmitConnection = false;
this.emit('connect', {
remoteAddress: this.remoteAddress,
remotePort: this.remotePort,
hostNameAppearsAs: this.hostNameAppearsAs,
clientHostname: this.clientHostname
});
};
// COMMAND HANDLERS
// COMMAND HANDLERS
/**
* Processes EHLO. Requires valid hostname as the single argument.
*/
SMTPConnection.prototype.handler_EHLO = function (command, callback) {
var parts = command.toString().trim().split(/\s+/);
var hostname = parts[1] || '';
/**
* Processes EHLO. Requires valid hostname as the single argument.
*/
handler_EHLO(command, callback) {
let parts = command.toString().trim().split(/\s+/);
let hostname = parts[1] || '';
if (parts.length !== 2) {
this.send(501, 'Error: syntax: ' + (this._server.options.lmtp ? 'LHLO' : 'EHLO') + ' hostname');
return callback();
}
if (parts.length !== 2) {
this.send(501, 'Error: syntax: ' + (this._server.options.lmtp ? 'LHLO' : 'EHLO') + ' hostname');
return callback();
}
this.hostNameAppearsAs = hostname.toLowerCase();
this.hostNameAppearsAs = hostname.toLowerCase();
var features = ['PIPELINING', '8BITMIME', 'SMTPUTF8'].filter(function (feature) {
return !this._server.options['hide' + feature];
}.bind(this));
let features = ['PIPELINING', '8BITMIME', 'SMTPUTF8'].filter(feature => !this._server.options['hide' + feature]);
if (this._server.options.authMethods.length && this._isSupported('AUTH')) {
features.push(['AUTH'].concat(this._server.options.authMethods).join(' '));
}
if (this._server.options.authMethods.length && this._isSupported('AUTH')) {
features.push(['AUTH'].concat(this._server.options.authMethods).join(' '));
}
if (!this.secure && this._isSupported('STARTTLS') && !this._server.options.hideSTARTTLS) {
features.push('STARTTLS');
}
if (!this.secure && this._isSupported('STARTTLS') && !this._server.options.hideSTARTTLS) {
features.push('STARTTLS');
}
if (this._server.options.size) {
features.push('SIZE' + (this._server.options.hideSize ? '' : ' ' + this._server.options.size));
}
if (this._server.options.size) {
features.push('SIZE' + (this._server.options.hideSize ? '' : ' ' + this._server.options.size));
}
// XCLIENT ADDR removes any special privileges for the client
if (!this._xClient.has('ADDR') && this._server.options.useXClient && this._isSupported('XCLIENT')) {
features.push('XCLIENT NAME ADDR PORT PROTO HELO LOGIN');
}
// XCLIENT ADDR removes any special privileges for the client
if (!this._xClient.has('ADDR') && this._server.options.useXClient && this._isSupported('XCLIENT')) {
features.push('XCLIENT NAME ADDR PORT PROTO HELO LOGIN');
}
// If client has already issued XCLIENT ADDR then it does not have privileges for XFORWARD anymore
if (!this._xClient.has('ADDR') && this._server.options.useXForward && this._isSupported('XFORWARD')) {
features.push('XFORWARD NAME ADDR PORT PROTO HELO IDENT SOURCE');
}
// If client has already issued XCLIENT ADDR then it does not have privileges for XFORWARD anymore
if (!this._xClient.has('ADDR') && this._server.options.useXForward && this._isSupported('XFORWARD')) {
features.push('XFORWARD NAME ADDR PORT PROTO HELO IDENT SOURCE');
}
this._resetSession(); // EHLO is effectively the same as RSET
this.send(250, [this.name + ' Nice to meet you, ' + this.clientHostname].concat(features || []));
this._resetSession(); // EHLO is effectively the same as RSET
this.send(250, [this.name + ' Nice to meet you, ' + this.clientHostname].concat(features || []));
callback();
};
/**
* Processes HELO. Requires valid hostname as the single argument.
*/
SMTPConnection.prototype.handler_HELO = function (command, callback) {
var parts = command.toString().trim().split(/\s+/);
var hostname = parts[1] || '';
if (parts.length !== 2) {
this.send(501, 'Error: Syntax: HELO hostname');
return callback();
callback();
}
this.hostNameAppearsAs = hostname.toLowerCase();
/**
* Processes HELO. Requires valid hostname as the single argument.
*/
handler_HELO(command, callback) {
let parts = command.toString().trim().split(/\s+/);
let hostname = parts[1] || '';
this._resetSession(); // HELO is effectively the same as RSET
this.send(250, this.name + ' Nice to meet you, ' + this.clientHostname);
if (parts.length !== 2) {
this.send(501, 'Error: Syntax: HELO hostname');
return callback();
}
callback();
};
this.hostNameAppearsAs = hostname.toLowerCase();
/**
* Processes QUIT. Closes the connection
*/
SMTPConnection.prototype.handler_QUIT = function (command, callback) {
this.send(221, 'Bye');
this.close();
callback();
};
this._resetSession(); // HELO is effectively the same as RSET
this.send(250, this.name + ' Nice to meet you, ' + this.clientHostname);
/**
* Processes NOOP. Does nothing but keeps the connection alive
*/
SMTPConnection.prototype.handler_NOOP = function (command, callback) {
this.send(250, 'OK');
callback();
};
callback();
}
/**
* Processes RSET. Resets user and session info
*/
SMTPConnection.prototype.handler_RSET = function (command, callback) {
this._resetSession();
/**
* Processes QUIT. Closes the connection
*/
handler_QUIT(command, callback) {
this.send(221, 'Bye');
this.close();
callback();
}
this.send(250, 'Flushed');
callback();
};
/**
* Processes NOOP. Does nothing but keeps the connection alive
*/
handler_NOOP(command, callback) {
this.send(250, 'OK');
callback();
}
/**
* Processes HELP. Responds with url to RFC
*/
SMTPConnection.prototype.handler_HELP = function (command, callback) {
this.send(214, 'See https://tools.ietf.org/html/rfc5321 for details');
callback();
};
/**
* Processes RSET. Resets user and session info
*/
handler_RSET(command, callback) {
this._resetSession();
/**
* Processes VRFY. Does not verify anything
*/
SMTPConnection.prototype.handler_VRFY = function (command, callback) {
this.send(252, 'Try to send something. No promises though');
callback();
};
/**
* Overrides connection info
* http://www.postfix.org/XCLIENT_README.html
*
* TODO: add unit tests
*/
SMTPConnection.prototype.handler_XCLIENT = function (command, callback) {
// check if user is authorized to perform this command
if (this._xClient.has('ADDR') || !this._server.options.useXClient) {
this.send(550, 'Error: Not allowed');
return callback();
this.send(250, 'Flushed');
callback();
}
// not allowed to change properties if already processing mail
if (this.session.envelope.mailFrom) {
this.send(503, 'Error: Mail transaction in progress');
return callback();
/**
* Processes HELP. Responds with url to RFC
*/
handler_HELP(command, callback) {
this.send(214, 'See https://tools.ietf.org/html/rfc5321 for details');
callback();
}
var allowedKeys = ['NAME', 'ADDR', 'PORT', 'PROTO', 'HELO', 'LOGIN'];
var parts = command.toString().trim().split(/\s+/);
var key, value;
var data = new Map();
parts.shift(); // remove XCLIENT prefix
if (!parts.length) {
this.send(501, 'Error: Bad command parameter syntax');
return callback();
/**
* Processes VRFY. Does not verify anything
*/
handler_VRFY(command, callback) {
this.send(252, 'Try to send something. No promises though');
callback();
}
// parse and validate arguments
for (var i = 0, len = parts.length; i < len; i++) {
value = parts[i].split('=');
key = value.shift();
if (value.length !== 1 || allowedKeys.indexOf(key.toUpperCase()) < 0) {
this.send(501, 'Error: Bad command parameter syntax');
/**
* Overrides connection info
* http://www.postfix.org/XCLIENT_README.html
*
* TODO: add unit tests
*/
handler_XCLIENT(command, callback) {
// check if user is authorized to perform this command
if (this._xClient.has('ADDR') || !this._server.options.useXClient) {
this.send(550, 'Error: Not allowed');
return callback();
}
key = key.toUpperCase();
// value is xtext
value = (value[0] || '').replace(/\+([0-9A-F]{2})/g, function (match, hex) {
return unescape('%' + hex);
});
if (['[UNAVAILABLE]', '[TEMPUNAVAIL]'].indexOf(value.toUpperCase()) >= 0) {
value = false;
// not allowed to change properties if already processing mail
if (this.session.envelope.mailFrom) {
this.send(503, 'Error: Mail transaction in progress');
return callback();
}
if (data.has(key)) {
// ignore duplicate keys
continue;
let allowedKeys = ['NAME', 'ADDR', 'PORT', 'PROTO', 'HELO', 'LOGIN'];
let parts = command.toString().trim().split(/\s+/);
let key, value;
let data = new Map();
parts.shift(); // remove XCLIENT prefix
if (!parts.length) {
this.send(501, 'Error: Bad command parameter syntax');
return callback();
}
data.set(key, value);
// parse and validate arguments
for (let i = 0, len = parts.length; i < len; i++) {
value = parts[i].split('=');
key = value.shift();
if (value.length !== 1 || !allowedKeys.includes(key.toUpperCase())) {
this.send(501, 'Error: Bad command parameter syntax');
return callback();
}
key = key.toUpperCase();
switch (key) {
case 'LOGIN':
if (!value) {
if (this.session.user) {
this._server.logger.info('[%s] User deauthenticated using %s', this._id, 'XCLIENT');
}
} else {
this._server.logger.info('[%s] %s authenticated using %s', this._id, value, 'XCLIENT');
this.session.user = {
username: value
};
}
break;
case 'ADDR':
if (value) {
value = value.replace(/^IPV6:/i, ''); // IPv6 addresses are prefixed with "IPv6:"
// value is xtext
value = (value[0] || '').replace(/\+([0-9A-F]{2})/g, (match, hex) => unescape('%' + hex));
if (!net.isIP(value)) {
this.send(501, 'Error: Bad command parameter syntax. Invalid address');
return callback();
}
if (['[UNAVAILABLE]', '[TEMPUNAVAIL]'].includes(value.toUpperCase())) {
value = false;
}
if (net.isIPv6(value)) {
value = ipv6normalize(value);
}
if (data.has(key)) {
// ignore duplicate keys
continue;
}
this._server.logger.info('[%s] XCLIENT from %s through %s', this._id, value, this.remoteAddress);
data.set(key, value);
// store original value for reference as ADDR:DEFAULT
if (!this._xClient.has('ADDR:DEFAULT')) {
this._xClient.set('ADDR:DEFAULT', this.remoteAddress);
switch (key) {
case 'LOGIN':
if (!value) {
if (this.session.user) {
this._server.logger.info({
tnx: 'deauth',
cid: this._id,
user: this.session.user && this.session.user.username
}, '[%s] User deauthenticated using %s', this._id, 'XCLIENT');
this.session.user = false;
}
} else {
this._server.logger.info({
tnx: 'auth',
cid: this._id,
user: value
}, '[%s] %s authenticated using %s', this._id, value, 'XCLIENT');
this.session.user = {
username: value
};
}
break;
case 'ADDR':
if (value) {
value = value.replace(/^IPV6:/i, ''); // IPv6 addresses are prefixed with "IPv6:"
this.remoteAddress = value;
this.hostNameAppearsAs = false; // reset client provided hostname, require HELO/EHLO
}
break;
case 'NAME':
value = value || '';
this._server.logger.info('[%s] XCLIENT hostname resolved as "%s"', this._id, value);
if (!net.isIP(value)) {
this.send(501, 'Error: Bad command parameter syntax. Invalid address');
return callback();
}
// store original value for reference as NAME:DEFAULT
if (!this._xClient.has('NAME:DEFAULT')) {
this._xClient.set('NAME:DEFAULT', this.clientHostname || '');
}
if (net.isIPv6(value)) {
value = ipv6normalize(value);
}
this.clientHostname = value.toLowerCase();
break;
case 'PORT':
value = Number(value) || '';
this._server.logger.info('[%s] XCLIENT remote port resolved as "%s"', this._id, value);
this._server.logger.info({
tnx: 'xclient',
cid: this._id,
xclientKey: 'ADDR',
xclient: value,
user: this.session.user && this.session.user.username
}, '[%s] XCLIENT from %s through %s', this._id, value, this.remoteAddress);
// store original value for reference as NAME:DEFAULT
if (!this._xClient.has('PORT:DEFAULT')) {
this._xClient.set('PORT:DEFAULT', this.remotePort || '');
}
// store original value for reference as ADDR:DEFAULT
if (!this._xClient.has('ADDR:DEFAULT')) {
this._xClient.set('ADDR:DEFAULT', this.remoteAddress);
}
this.remotePort = value;
break;
default:
// other values are not relevant
}
this._xClient.set(key, value);
}
this.remoteAddress = value;
this.hostNameAppearsAs = false; // reset client provided hostname, require HELO/EHLO
}
break;
case 'NAME':
value = value || '';
this._server.logger.info({
tnx: 'xclient',
cid: this._id,
xclientKey: 'NAME',
xclient: value,
user: this.session.user && this.session.user.username
}, '[%s] XCLIENT hostname resolved as "%s"', this._id, value);
// Use [ADDR] if NAME was empty
if (this.remoteAddress && !this.clientHostname) {
this.clientHostname = '[' + this.remoteAddress + ']';
}
// store original value for reference as NAME:DEFAULT
if (!this._xClient.has('NAME:DEFAULT')) {
this._xClient.set('NAME:DEFAULT', this.clientHostname || '');
}
if (data.has('ADDR')) {
this.emitConnection();
}
this.clientHostname = value.toLowerCase();
break;
case 'PORT':
value = Number(value) || '';
this._server.logger.info({
tnx: 'xclient',
cid: this._id,
xclientKey: 'PORT',
xclient: value,
user: this.session.user && this.session.user.username
}, '[%s] XCLIENT remote port resolved as "%s"', this._id, value);
// success
this.send(220, this.name + ' ' + (this._server.options.lmtp ? 'LMTP' : 'ESMTP') + (this._server.options.banner ? ' ' + this._server.options.banner : ''));
callback();
};
// store original value for reference as NAME:DEFAULT
if (!this._xClient.has('PORT:DEFAULT')) {
this._xClient.set('PORT:DEFAULT', this.remotePort || '');
}
/**
* Processes XFORWARD data
* http://www.postfix.org/XFORWARD_README.html
*
* TODO: add unit tests
*/
SMTPConnection.prototype.handler_XFORWARD = function (command, callback) {
// check if user is authorized to perform this command
if (!this._server.options.useXForward) {
this.send(550, 'Error: Not allowed');
return callback();
}
this.remotePort = value;
break;
default:
// other values are not relevant
}
this._xClient.set(key, value);
}
// not allowed to change properties if already processing mail
if (this.session.envelope.mailFrom) {
this.send(503, 'Error: Mail transaction in progress');
return callback();
}
// Use [ADDR] if NAME was empty
if (this.remoteAddress && !this.clientHostname) {
this.clientHostname = '[' + this.remoteAddress + ']';
}
var allowedKeys = ['NAME', 'ADDR', 'PORT', 'PROTO', 'HELO', 'IDENT', 'SOURCE'];
var parts = command.toString().trim().split(/\s+/);
var key, value;
var data = new Map();
var hasAddr = false;
parts.shift(); // remove XFORWARD prefix
if (data.has('ADDR')) {
this.emitConnection();
}
if (!parts.length) {
this.send(501, 'Error: Bad command parameter syntax');
return callback();
// success
this.send(220, this.name + ' ' + (this._server.options.lmtp ? 'LMTP' : 'ESMTP') + (this._server.options.banner ? ' ' + this._server.options.banner : ''));
callback();
}
// parse and validate arguments
for (var i = 0, len = parts.length; i < len; i++) {
value = parts[i].split('=');
key = value.shift();
if (value.length !== 1 || allowedKeys.indexOf(key.toUpperCase()) < 0) {
this.send(501, 'Error: Bad command parameter syntax');
/**
* Processes XFORWARD data
* http://www.postfix.org/XFORWARD_README.html
*
* TODO: add unit tests
*/
handler_XFORWARD(command, callback) {
// check if user is authorized to perform this command
if (!this._server.options.useXForward) {
this.send(550, 'Error: Not allowed');
return callback();
}
key = key.toUpperCase();
if (data.has(key)) {
// ignore duplicate keys
continue;
// not allowed to change properties if already processing mail
if (this.session.envelope.mailFrom) {
this.send(503, 'Error: Mail transaction in progress');
return callback();
}
// value is xtext
value = (value[0] || '').replace(/\+([0-9A-F]{2})/g, function (match, hex) {
return unescape('%' + hex);
});
let allowedKeys = ['NAME', 'ADDR', 'PORT', 'PROTO', 'HELO', 'IDENT', 'SOURCE'];
let parts = command.toString().trim().split(/\s+/);
let key, value;
let data = new Map();
let hasAddr = false;
parts.shift(); // remove XFORWARD prefix
if (value.toUpperCase() === '[UNAVAILABLE]') {
value = false;
if (!parts.length) {
this.send(501, 'Error: Bad command parameter syntax');
return callback();
}
data.set(key, value);
// parse and validate arguments
for (let i = 0, len = parts.length; i < len; i++) {
value = parts[i].split('=');
key = value.shift();
if (value.length !== 1 || !allowedKeys.includes(key.toUpperCase())) {
this.send(501, 'Error: Bad command parameter syntax');
return callback();
}
key = key.toUpperCase();
if (data.has(key)) {
// ignore duplicate keys
continue;
}
switch (key) {
case 'ADDR':
if (value) {
value = value.replace(/^IPV6:/i, ''); // IPv6 addresses are prefixed with "IPv6:"
// value is xtext
value = (value[0] || '').replace(/\+([0-9A-F]{2})/g, (match, hex) => unescape('%' + hex));
if (!net.isIP(value)) {
this.send(501, 'Error: Bad command parameter syntax. Invalid address');
return callback();
}
if (value.toUpperCase() === '[UNAVAILABLE]') {
value = false;
}
if (net.isIPv6(value)) {
value = ipv6normalize(value);
}
data.set(key, value);
this._server.logger.info('[%s] XFORWARD from %s through %s', this._id, value, this.remoteAddress);
switch (key) {
case 'ADDR':
if (value) {
value = value.replace(/^IPV6:/i, ''); // IPv6 addresses are prefixed with "IPv6:"
// store original value for reference as ADDR:DEFAULT
if (!this._xClient.has('ADDR:DEFAULT')) {
this._xClient.set('ADDR:DEFAULT', this.remoteAddress);
}
if (!net.isIP(value)) {
this.send(501, 'Error: Bad command parameter syntax. Invalid address');
return callback();
}
hasAddr = true;
this.remoteAddress = value;
}
break;
case 'NAME':
value = value || '';
this._server.logger.info('[%s] XFORWARD hostname resolved as "%s"', this._id, value);
this.clientHostname = value.toLowerCase();
break;
case 'PORT':
value = Number(value) || 0;
this._server.logger.info('[%s] XFORWARD port resolved as "%s"', this._id, value);
this.remotePort = value;
break;
case 'HELO':
value = Number(value) || 0;
this._server.logger.info('[%s] XFORWARD HELO name resolved as "%s"', this._id, value);
this.hostNameAppearsAs = value;
break;
default:
// other values are not relevant
}
this._xForward.set(key, value);
}
if (net.isIPv6(value)) {
value = ipv6normalize(value);
}
if (hasAddr) {
this._canEmitConnection = true;
this.emitConnection();
}
this._server.logger.info({
tnx: 'xforward',
cid: this._id,
xforwardKey: 'ADDR',
xforward: value,
user: this.session.user && this.session.user.username
}, '[%s] XFORWARD from %s through %s', this._id, value, this.remoteAddress);
// success
this.send(250, 'OK');
callback();
};
// store original value for reference as ADDR:DEFAULT
if (!this._xClient.has('ADDR:DEFAULT')) {
this._xClient.set('ADDR:DEFAULT', this.remoteAddress);
}
/**
* Upgrades connection to TLS if possible
*/
SMTPConnection.prototype.handler_STARTTLS = function (command, callback) {
hasAddr = true;
this.remoteAddress = value;
}
break;
case 'NAME':
value = value || '';
this._server.logger.info({
tnx: 'xforward',
cid: this._id,
xforwardKey: 'NAME',
xforward: value,
user: this.session.user && this.session.user.username
}, '[%s] XFORWARD hostname resolved as "%s"', this._id, value);
this.clientHostname = value.toLowerCase();
break;
case 'PORT':
value = Number(value) || 0;
this._server.logger.info({
tnx: 'xforward',
cid: this._id,
xforwardKey: 'PORT',
xforward: value,
user: this.session.user && this.session.user.username
}, '[%s] XFORWARD port resolved as "%s"', this._id, value);
this.remotePort = value;
break;
case 'HELO':
value = Number(value) || 0;
this._server.logger.info({
tnx: 'xforward',
cid: this._id,
xforwardKey: 'HELO',
xforward: value,
user: this.session.user && this.session.user.username
}, '[%s] XFORWARD HELO name resolved as "%s"', this._id, value);
this.hostNameAppearsAs = value;
break;
default:
// other values are not relevant
}
this._xForward.set(key, value);
}
if (this.secure) {
this.send(503, 'Error: TLS already active');
return callback();
if (hasAddr) {
this._canEmitConnection = true;
this.emitConnection();
}
// success
this.send(250, 'OK');
callback();
}
this.send(220, 'Ready to start TLS');
this._socket.unpipe(this._parser);
this._upgrading = true;
setImmediate(callback); // resume input stream
/**
* Upgrades connection to TLS if possible
*/
handler_STARTTLS(command, callback) {
var secureContext = this._server.secureContext.get('default');
var socketOptions = {
secureContext: secureContext,
isServer: true,
server: this._server.server,
SNICallback: function (servername, cb) {
cb(null, this._server.secureContext.get(servername.toLowerCase().trim()) || this._server.secureContext.get('default'));
}.bind(this)
};
// Apply additional socket options if these are set in the server options
['requestCert', 'rejectUnauthorized', 'NPNProtocols', 'SNICallback', 'session', 'requestOCSP'].forEach(function (key) {
if (key in this._server.options) {
socketOptions[key] = this._server.options[key];
if (this.secure) {
this.send(503, 'Error: TLS already active');
return callback();
}
}.bind(this));
// remove all listeners from the original socket besides the error handler
this._socket.removeAllListeners();
this._socket.on('error', this._onError.bind(this));
this.send(220, 'Ready to start TLS');
this._socket.unpipe(this._parser);
this._upgrading = true;
setImmediate(callback); // resume input stream
// upgrade connection
var secureSocket = new tls.TLSSocket(this._socket, socketOptions);
let secureContext = this._server.secureContext.get('default');
let socketOptions = {
secureContext,
isServer: true,
server: this._server.server,
secureSocket.on('close', this._onClose.bind(this));
secureSocket.on('error', this._onError.bind(this));
secureSocket.on('clientError', this._onError.bind(this));
secureSocket.setTimeout(this._server.options.socketTimeout || SOCKET_TIMEOUT, this._onTimeout.bind(this));
SNICallback: (servername, cb) => {
cb(null, this._server.secureContext.get(servername.toLowerCase().trim()) || this._server.secureContext.get('default'));
}
};
secureSocket.on('secure', function () {
this.secure = true;
this._socket = secureSocket;
this._upgrading = false;
// Apply additional socket options if these are set in the server options
['requestCert', 'rejectUnauthorized', 'NPNProtocols', 'SNICallback', 'session', 'requestOCSP'].forEach(key => {
if (key in this._server.options) {
socketOptions[key] = this._server.options[key];
}
});
this.session.tlsOptions = this.tlsOptions = this._socket.getCipher();
// remove all listeners from the original socket besides the error handler
this._socket.removeAllListeners();
this._socket.on('error', err => this._onError(err));
this._server.logger.info('[%s] Connection upgraded to TLS', this._id);
this._socket.pipe(this._parser);
}.bind(this));
};
// upgrade connection
let secureSocket = new tls.TLSSocket(this._socket, socketOptions);
/**
* Check if selected authentication is available and delegate auth data to SASL
*/
SMTPConnection.prototype.handler_AUTH = function (command, callback) {
var args = command.toString().trim().split(/\s+/);
var method;
var handler;
secureSocket.on('close', () => this._onClose());
secureSocket.on('error', err => this._onError(err));
secureSocket.on('clientError', err => this._onError(err));
secureSocket.setTimeout(this._server.options.socketTimeout || SOCKET_TIMEOUT, () => this._onTimeout());
args.shift(); // remove AUTH
method = (args.shift() || '').toString().toUpperCase(); // get METHOD and keep additional arguments in the array
handler = sasl['SASL_' + method];
handler = handler ? handler.bind(this) : handler;
secureSocket.on('secure', () => {
this.secure = true;
this._socket = secureSocket;
this._upgrading = false;
if (!this.secure && this._isSupported('STARTTLS') && !this._server.options.hideSTARTTLS && !this._server.options.allowInsecureAuth) {
this.send(538, 'Error: Must issue a STARTTLS command first');
return callback();
}
this.session.tlsOptions = this.tlsOptions = this._socket.getCipher();
if (this.session.user) {
this.send(503, 'Error: No identity changes permitted');
return callback();
this._server.logger.info({
tnx: 'starttls',
cid: this._id,
user: this.session.user && this.session.user.username
}, '[%s] Connection upgraded to TLS', this._id);
this._socket.pipe(this._parser);
});
}
if (this._server.options.authMethods.indexOf(method) < 0 || typeof handler !== 'function') {
this.send(504, 'Error: Unrecognized authentication type');
return callback();
}
/**
* Check if selected authentication is available and delegate auth data to SASL
*/
handler_AUTH(command, callback) {
let args = command.toString().trim().split(/\s+/);
let method;
let handler;
handler(args, callback);
};
args.shift(); // remove AUTH
method = (args.shift() || '').toString().toUpperCase(); // get METHOD and keep additional arguments in the array
handler = sasl['SASL_' + method];
handler = handler ? handler.bind(this) : handler;
/**
* Processes MAIL FROM command, parses address and extra arguments
*/
SMTPConnection.prototype.handler_MAIL = function (command, callback) {
var parsed = this._parseAddressCommand('mail from', command);
if (!this.secure && this._isSupported('STARTTLS') && !this._server.options.hideSTARTTLS && !this._server.options.allowInsecureAuth) {
this.send(538, 'Error: Must issue a STARTTLS command first');
return callback();
}
// in case we still haven't informed about the new connection emit it
this.emitConnection();
if (this.session.user) {
this.send(503, 'Error: No identity changes permitted');
return callback();
}
// sender address can be empty, so we only check if parsing failed or not
if (!parsed) {
this.send(501, 'Error: Bad sender address syntax');
return callback();
}
if (!this._server.options.authMethods.includes(method) || typeof handler !== 'function') {
this.send(504, 'Error: Unrecognized authentication type');
return callback();
}
if (this.session.envelope.mailFrom) {
this.send(503, 'Error: nested MAIL command');
return callback();
handler(args, callback);
}
if (!this._server.options.hideSize && this._server.options.size && parsed.args.SIZE && Number(parsed.args.SIZE) > this._server.options.size) {
this.send(552, 'Error: message exceeds fixed maximum message size ' + this._server.options.size);
return callback();
}
/**
* Processes MAIL FROM command, parses address and extra arguments
*/
handler_MAIL(command, callback) {
let parsed = this._parseAddressCommand('mail from', command);
this._server.onMailFrom(parsed, this.session, function (err) {
if (err) {
this.send(err.responseCode || 550, err.message);
// in case we still haven't informed about the new connection emit it
this.emitConnection();
// sender address can be empty, so we only check if parsing failed or not
if (!parsed) {
this.send(501, 'Error: Bad sender address syntax');
return callback();
}
this.session.envelope.mailFrom = parsed;
if (this.session.envelope.mailFrom) {
this.send(503, 'Error: nested MAIL command');
return callback();
}
this.send(250, 'Accepted');
callback();
}.bind(this));
};
if (!this._server.options.hideSize && this._server.options.size && parsed.args.SIZE && Number(parsed.args.SIZE) > this._server.options.size) {
this.send(552, 'Error: message exceeds fixed maximum message size ' + this._server.options.size);
return callback();
}
/**
* Processes RCPT TO command, parses address and extra arguments
*/
SMTPConnection.prototype.handler_RCPT = function (command, callback) {
var parsed = this._parseAddressCommand('rcpt to', command);
this._server.onMailFrom(parsed, this.session, err => {
if (err) {
this.send(err.responseCode || 550, err.message);
return callback();
}
// recipient address can not be empty
if (!parsed || !parsed.address) {
this.send(501, 'Error: Bad recipient address syntax');
return callback();
}
this.session.envelope.mailFrom = parsed;
if (!this.session.envelope.mailFrom) {
this.send(503, 'Error: need MAIL command');
return callback();
this.send(250, 'Accepted');
callback();
});
}
this._server.onRcptTo(parsed, this.session, function (err) {
if (err) {
this.send(err.responseCode || 550, err.message);
/**
* Processes RCPT TO command, parses address and extra arguments
*/
handler_RCPT(command, callback) {
let parsed = this._parseAddressCommand('rcpt to', command);
// recipient address can not be empty
if (!parsed || !parsed.address) {
this.send(501, 'Error: Bad recipient address syntax');
return callback();
}
// check if the address is already used, if so then overwrite
for (var i = 0, len = this.session.envelope.rcptTo.length; i < len; i++) {
if (this.session.envelope.rcptTo[i].address.toLowerCase() === parsed.address.toLowerCase()) {
this.session.envelope.rcptTo[i] = parsed;
parsed = false;
break;
}
if (!this.session.envelope.mailFrom) {
this.send(503, 'Error: need MAIL command');
return callback();
}
if (parsed) {
this.session.envelope.rcptTo.push(parsed);
}
this._server.onRcptTo(parsed, this.session, err => {
if (err) {
this.send(err.responseCode || 550, err.message);
return callback();
}
this.send(250, 'Accepted');
callback();
}.bind(this));
};
// check if the address is already used, if so then overwrite
for (let i = 0, len = this.session.envelope.rcptTo.length; i < len; i++) {
if (this.session.envelope.rcptTo[i].address.toLowerCase() === parsed.address.toLowerCase()) {
this.session.envelope.rcptTo[i] = parsed;
parsed = false;
break;
}
}
/**
* Processes DATA by forwarding incoming stream to the onData handler
*/
SMTPConnection.prototype.handler_DATA = function (command, callback) {
if (!this.session.envelope.rcptTo.length) {
this.send(503, 'Error: need RCPT command');
return callback();
}
if (parsed) {
this.session.envelope.rcptTo.push(parsed);
}
if (!this._parser) {
return callback();
this.send(250, 'Accepted');
callback();
});
}
this._dataStream = this._parser.startDataMode(this._server.options.size);
/**
* Processes DATA by forwarding incoming stream to the onData handler
*/
handler_DATA(command, callback) {
if (!this.session.envelope.rcptTo.length) {
this.send(503, 'Error: need RCPT command');
return callback();
}
var close = function (err, message) {
var i, len;
if (!this._parser) {
return callback();
}
this._server.logger.debug('[%s] C: <%s bytes of DATA>', this._id, this._parser.dataBytes);
this._dataStream = this._parser.startDataMode(this._server.options.size);
if ((typeof this._dataStream === 'object') && (this._dataStream) && (this._dataStream.readable)) {
this._dataStream.removeAllListeners();
}
let close = (err, message) => {
let i, len;
if (err) {
if (this._server.options.lmtp) {
// separate error response for every recipient when using LMTP
for (i = 0, len = this.session.envelope.rcptTo.length; i < len; i++) {
this._server.logger.debug({
tnx: 'data',
cid: this._id,
bytes: this._parser.dataBytes,
user: this.session.user && this.session.user.username
}, '[%s] C: <%s bytes of DATA>', this._id, this._parser.dataBytes);
if ((typeof this._dataStream === 'object') && (this._dataStream) && (this._dataStream.readable)) {
this._dataStream.removeAllListeners();
}
if (err) {
if (this._server.options.lmtp) {
// separate error response for every recipient when using LMTP
for (i = 0, len = this.session.envelope.rcptTo.length; i < len; i++) {
this.send(err.responseCode || 450, err.message);
}
} else {
// single error response when using SMTP
this.send(err.responseCode || 450, err.message);
}
} else if (Array.isArray(message)) {
// separate responses for every recipient when using LMTP
message.forEach(response => {
if (/Error\]$/i.test(Object.prototype.toString.call(response))) {
this.send(response.responseCode || 450, response.message);
} else {
this.send(250, typeof response === 'string' ? response : 'OK: message accepted');
}
});
} else if (this._server.options.lmtp) {
// separate success response for every recipient when using LMTP
for (i = 0, len = this.session.envelope.rcptTo.length; i < len; i++) {
this.send(250, typeof message === 'string' ? message : 'OK: message accepted');
}
} else {
// single error response when using SMTP
this.send(err.responseCode || 450, err.message);
// single success response when using SMTP
this.send(250, typeof message === 'string' ? message : 'OK: message queued');
}
} else if (Array.isArray(message)) {
// separate responses for every recipient when using LMTP
message.forEach(function (response) {
if (/Error\]$/i.test(Object.prototype.toString.call(response))) {
this.send(response.responseCode || 450, response.message);
} else {
this.send(250, typeof response === 'string' ? response : 'OK: message accepted');
}
}.bind(this));
} else if (this._server.options.lmtp) {
// separate success response for every recipient when using LMTP
for (i = 0, len = this.session.envelope.rcptTo.length; i < len; i++) {
this.send(250, typeof message === 'string' ? message : 'OK: message accepted');
this._transactionCounter++;
this._unrecognizedCommands = 0; // reset unrecognized commands counter
this._resetSession(); // reset session state
if ((typeof this._parser === 'object') && (this._parser)) {
this._parser.continue();
}
} else {
// single success response when using SMTP
this.send(250, typeof message === 'string' ? message : 'OK: message queued');
}
};
this._transactionCounter++;
this._server.onData(this._dataStream, this.session, (err, message) => {
// ensure _dataStream is an object and not set to null by premature closing
// do not continue until the stream has actually ended
if ((typeof this._dataStream === 'object') && (this._dataStream) && (this._dataStream.readable)) {
this._dataStream.on('end', () => close(err, message));
return;
}
close(err, message);
});
this._unrecognizedCommands = 0; // reset unrecognized commands counter
this._resetSession(); // reset session state
this.send(354, 'End data with <CR><LF>.<CR><LF>');
callback();
}
if ((typeof this._parser === 'object') && (this._parser)) {
this._parser.continue();
}
}.bind(this);
// Dummy handlers for some old sendmail specific commands
this._server.onData(this._dataStream, this.session, function (err, message) {
// ensure _dataStream is an object and not set to null by premature closing
// do not continue until the stream has actually ended
if ((typeof this._dataStream === 'object') && (this._dataStream) && (this._dataStream.readable)) {
this._dataStream.on('end', function () {
close(err, message);
});
return;
/**
* Processes sendmail WIZ command, upgrades to "wizard mode"
*/
handler_WIZ(command, callback) {
let args = command.toString().trim().split(/\s+/);
let password;
args.shift(); // remove WIZ
password = (args.shift() || '').toString();
// require password argument
if (!password) {
this.send(500, 'You are no wizard!');
return callback();
}
close(err, message);
}.bind(this));
this.send(354, 'End data with <CR><LF>.<CR><LF>');
callback();
};
// all passwords pass validation, so everyone is a wizard!
this.session.isWizard = true;
this.send(200, 'Please pass, oh mighty wizard');
callback();
}
// Dummy handlers for some old sendmail specific commands
/**
* Processes sendmail SHELL command, should return interactive shell but this is a dummy function
* so no actual shell is provided to the client
*/
handler_SHELL(command, callback) {
this._server.logger.info({
tnx: 'shell',
cid: this._id,
user: this.session.user && this.session.user.username
}, '[%s] Client tried to invoke SHELL', this._id);
/**
* Processes sendmail WIZ command, upgrades to "wizard mode"
*/
SMTPConnection.prototype.handler_WIZ = function (command, callback) {
var args = command.toString().trim().split(/\s+/);
var password;
if (!this.session.isWizard) {
this.send(500, 'Mere mortals musn\'t mutter that mantra');
return callback();
}
args.shift(); // remove WIZ
password = (args.shift() || '').toString();
// require password argument
if (!password) {
this.send(500, 'You are no wizard!');
return callback();
this.send(500, 'Error: Invoking shell is not allowed. This incident will be reported.');
callback();
}
// all passwords pass validation, so everyone is a wizard!
this.session.isWizard = true;
this.send(200, 'Please pass, oh mighty wizard');
callback();
};
/**
* Processes sendmail KILL command
*/
handler_KILL(command, callback) {
this._server.logger.info({
tnx: 'kill',
cid: this._id,
user: this.session.user && this.session.user.username
}, '[%s] Client tried to invoke KILL', this._id);
/**
* Processes sendmail SHELL command, should return interactive shell but this is a dummy function
* so no actual shell is provided to the client
*/
SMTPConnection.prototype.handler_SHELL = function (command, callback) {
if (!this.session.isWizard) {
this.send(500, 'Mere mortals musn\'t mutter that mantra');
return callback();
this.send(500, 'Can\'t kill Mom');
callback();
}
this._server.logger.info('[%s] Client tried to invoke SHELL', this._id);
this.send(500, 'Error: Invoking shell is not allowed. This incident will be reported.');
callback();
};
/**
* Processes sendmail KILL command
*/
SMTPConnection.prototype.handler_KILL = function (command, callback) {
this.send(500, 'Can\'t kill Mom');
callback();
};
}
// Expose to the world
module.exports.SMTPConnection = SMTPConnection;
'use strict';
var net = require('net');
var tls = require('tls');
var SMTPConnection = require('./smtp-connection').SMTPConnection;
var tlsOptions = require('./tls-options');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var shared = require('nodemailer-shared');
const net = require('net');
const tls = require('tls');
const SMTPConnection = require('./smtp-connection').SMTPConnection;
const tlsOptions = require('./tls-options');
const EventEmitter = require('events');
const shared = require('nodemailer/lib/shared');
var CLOSE_TIMEOUT = 30 * 1000; // how much to wait until pending connections are terminated
const CLOSE_TIMEOUT = 30 * 1000; // how much to wait until pending connections are terminated
// Expose to the world
module.exports.SMTPServer = SMTPServer;
/**

@@ -22,227 +18,247 @@ * Creates a SMTP server instance.

*/
function SMTPServer(options) {
EventEmitter.call(this);
class SMTPServer extends EventEmitter {
constructor(options) {
super();
this.options = options || {};
this.options = options || {};
this.secureContext = new Map();
this.secureContext.set('default', tls.createSecureContext(tlsOptions(this.options)));
this.secureContext = new Map();
this.secureContext.set('default', tls.createSecureContext(tlsOptions(this.options)));
var ctxMap = this.options.sniOptions || {};
if (typeof ctxMap.get === 'function') {
ctxMap.forEach(function (ctx, servername) {
this.secureContext.set(servername.toLowerCase().trim(), tls.createSecureContext(tlsOptions(ctx)));
}.bind(this));
} else {
Object.keys(ctxMap).forEach(function (servername) {
this.secureContext.set(servername.toLowerCase().trim(), tls.createSecureContext(tlsOptions(ctxMap[servername])));
}.bind(this));
}
let ctxMap = this.options.sniOptions || {};
if (typeof ctxMap.get === 'function') {
ctxMap.forEach((ctx, servername) => {
this.secureContext.set(servername.toLowerCase().trim(), tls.createSecureContext(tlsOptions(ctx)));
});
} else {
Object.keys(ctxMap).forEach(servername => {
this.secureContext.set(servername.toLowerCase().trim(), tls.createSecureContext(tlsOptions(ctxMap[servername])));
});
}
// apply TLS defaults if needed, only if there is not SNICallback.
if (this.options.secure && typeof this.options.SNICallback !== 'function') {
this.options = tlsOptions(this.options);
this.options.SNICallback = function (servername, cb) {
cb(null, this.secureContext.get(servername.toLowerCase().trim()) || this.secureContext.get('default'));
}.bind(this);
}
// apply TLS defaults if needed, only if there is not SNICallback.
if (this.options.secure && typeof this.options.SNICallback !== 'function') {
this.options = tlsOptions(this.options);
this.options.SNICallback = (servername, cb) => {
cb(null, this.secureContext.get(servername.toLowerCase().trim()) || this.secureContext.get('default'));
};
}
// setup disabled commands list
this.options.disabledCommands = [].concat(this.options.disabledCommands || []).map(function (command) {
return (command || '').toString().toUpperCase().trim();
});
// setup disabled commands list
this.options.disabledCommands = [].concat(this.options.disabledCommands || [])
.map(command => (command || '').toString().toUpperCase().trim());
// setup allowed auth methods
this.options.authMethods = [].concat(this.options.authMethods || []).map(function (method) {
return (method || '').toString().toUpperCase().trim();
});
// setup allowed auth methods
this.options.authMethods = [].concat(this.options.authMethods || [])
.map(method => (method || '').toString().toUpperCase().trim());
if (!this.options.authMethods.length) {
this.options.authMethods = ['LOGIN', 'PLAIN'];
}
if (!this.options.authMethods.length) {
this.options.authMethods = ['LOGIN', 'PLAIN'];
}
this.logger = shared.getLogger(this.options);
this.logger = shared.getLogger(this.options, {
component: this.options.component || 'smtp-server'
});
// apply shorthand handlers
['onConnect', 'onAuth', 'onMailFrom', 'onRcptTo', 'onData', 'onClose'].forEach(function (handler) {
if (typeof this.options[handler] === 'function') {
this[handler] = this.options[handler];
}
}.bind(this));
// apply shorthand handlers
['onConnect', 'onAuth', 'onMailFrom', 'onRcptTo', 'onData', 'onClose'].forEach(handler => {
if (typeof this.options[handler] === 'function') {
this[handler] = this.options[handler];
}
});
/**
* Timeout after close has been called until pending connections are forcibly closed
*/
this._closeTimeout = false;
/**
* Timeout after close has been called until pending connections are forcibly closed
*/
this._closeTimeout = false;
/**
* A set of all currently open connections
*/
this.connections = new Set();
/**
* A set of all currently open connections
*/
this.connections = new Set();
// setup server listener and connection handler
this.server = (this.options.secure ? tls : net).createServer(this.options, function (socket) {
this.connect(socket);
}.bind(this));
// setup server listener and connection handler
this.server = (this.options.secure ? tls : net)
.createServer(this.options, socket => this.connect(socket));
// ensure _sharedCreds, fixes an issue in node v4+ where STARTTLS fails because _sharedCreds does not exist
this.server._sharedCreds = this.server._sharedCreds || this.secureContext.get('default');
// ensure _sharedCreds, fixes an issue in node v4+ where STARTTLS fails because _sharedCreds does not exist
this.server._sharedCreds = this.server._sharedCreds || this.secureContext.get('default');
this._setListeners();
}
util.inherits(SMTPServer, EventEmitter);
this._setListeners();
}
SMTPServer.prototype.connect = function (socket) {
var connection = new SMTPConnection(this, socket);
this.connections.add(connection);
connection.on('error', this._onError.bind(this));
connection.on('connect', this._onClientConnect.bind(this));
connection.init();
};
connect(socket) {
let connection = new SMTPConnection(this, socket);
this.connections.add(connection);
connection.on('error', err => this._onError(err));
connection.on('connect', () => this._onClientConnect());
connection.init();
}
/**
* Start listening on selected port and interface
*/
SMTPServer.prototype.listen = function ( /* arguments */ ) {
this.server.listen.apply(this.server, Array.prototype.slice.call(arguments));
};
/**
* Start listening on selected port and interface
*/
listen(...args) {
this.server.listen(...args);
}
/**
* Closes the server
*
* @param {Function} callback Callback to run once the server is fully closed
*/
SMTPServer.prototype.close = function (callback) {
var connections = this.connections.size;
var timeout = this.options.closeTimeout || CLOSE_TIMEOUT;
/**
* Closes the server
*
* @param {Function} callback Callback to run once the server is fully closed
*/
close(callback) {
let connections = this.connections.size;
let timeout = this.options.closeTimeout || CLOSE_TIMEOUT;
// stop accepting new connections
this.server.close(function () {
clearTimeout(this._closeTimeout);
if (typeof callback === 'function') {
return callback();
// stop accepting new connections
this.server.close(() => {
clearTimeout(this._closeTimeout);
if (typeof callback === 'function') {
return callback();
}
});
// close active connections
if (connections) {
this.logger.info({
tnx: 'close'
}, 'Server closing with %s pending connection%s, waiting %s seconds before terminating', connections, connections !== 1 ? 's' : '', timeout / 1000);
}
}.bind(this));
// close active connections
if (connections) {
this.logger.info('Server closing with %s pending connection%s, waiting %s seconds before terminating', connections, connections !== 1 ? 's' : '', timeout / 1000);
this._closeTimeout = setTimeout(() => {
connections = this.connections.size;
if (connections) {
this.logger.info({
tnx: 'close'
}, 'Closing %s pending connection%s to close the server', connections, connections !== 1 ? 's' : '');
this.connections.forEach(connection => {
connection.send(421, 'Server shutting down');
connection.close();
});
}
}, timeout);
}
this._closeTimeout = setTimeout(function () {
connections = this.connections.size;
if (connections) {
this.logger.info('Closing %s pending connection%s to close the server', connections, connections !== 1 ? 's' : '');
this.connections.forEach(function (connection) {
connection.send(421, 'Server shutting down');
connection.close();
}.bind(this));
/**
* Authentication handler. Override this
*
* @param {Object} auth Authentication options
* @param {Function} callback Callback to run once the user is authenticated
*/
onAuth(auth, session, callback) {
if (auth.method === 'XOAUTH2') {
return callback(null, {
data: {
status: '401',
schemes: 'bearer mac',
scope: 'https://mail.google.com/'
}
});
}
}.bind(this), timeout);
};
/**
* Authentication handler. Override this
*
* @param {Object} auth Authentication options
* @param {Function} callback Callback to run once the user is authenticated
*/
SMTPServer.prototype.onAuth = function (auth, session, callback) {
if (auth.method === 'XOAUTH2') {
return callback(null, {
data: {
status: '401',
schemes: 'bearer mac',
scope: 'https://mail.google.com/'
}
message: 'Authentication not implemented'
});
}
return callback(null, {
message: 'Authentication not implemented'
});
};
onConnect(session, callback) {
setImmediate(callback);
}
SMTPServer.prototype.onConnect = function (session, callback) {
setImmediate(callback);
};
onMailFrom(address, session, callback) {
setImmediate(callback);
}
SMTPServer.prototype.onMailFrom = function (address, session, callback) {
setImmediate(callback);
};
onRcptTo(address, session, callback) {
setImmediate(callback);
}
SMTPServer.prototype.onRcptTo = function (address, session, callback) {
setImmediate(callback);
};
onData(stream, session, callback) {
let chunklen = 0;
SMTPServer.prototype.onData = function (stream, session, callback) {
var chunklen = 0;
stream.on('data', chunk => {
chunklen += chunk.length;
});
stream.on('data', function (chunk) {
chunklen += chunk.length;
}.bind(this));
stream.on('end', () => {
this.logger.info({
tnx: 'message',
size: chunklen
}, '<received %s bytes>', chunklen);
callback();
});
}
stream.on('end', function () {
this.logger.info('<received %s bytes>', chunklen);
callback();
}.bind(this));
};
onClose( /* session */ ) {
// do nothing
}
SMTPServer.prototype.onClose = function ( /* session */ ) {
// do nothing
};
// PRIVATE METHODS
// PRIVATE METHODS
/**
* Setup server event handlers
*/
_setListeners() {
this.server.on('listening', () => this._onListening());
this.server.on('close', () => this._onClose());
this.server.on('error', err => this._onError(err));
}
/**
* Setup server event handlers
*/
SMTPServer.prototype._setListeners = function () {
this.server.on('listening', this._onListening.bind(this));
this.server.on('close', this._onClose.bind(this));
this.server.on('error', this._onError.bind(this));
};
/**
* Called when server started listening
*
* @event
*/
_onListening() {
let address = this.server.address();
this.logger.info(
//
{
tnx: 'listen',
host: address.address,
port: address.port,
secure: !!this.options.secure,
protocol: this.options.lmtp ? 'LMTP' : 'SMTP'
},
'%s%s Server listening on %s:%s',
this.options.secure ? 'Secure ' : '',
this.options.lmtp ? 'LMTP' : 'SMTP',
address.family === 'IPv4' ? address.address : '[' + address.address + ']',
address.port);
}
/**
* Called when server started listening
*
* @event
*/
SMTPServer.prototype._onListening = function () {
var address = this.server.address();
this.logger.info(
'%s%s Server listening on %s:%s',
this.options.secure ? 'Secure ' : '',
this.options.lmtp ? 'LMTP' : 'SMTP',
address.family === 'IPv4' ? address.address : '[' + address.address + ']',
address.port);
};
/**
* Called when server is closed
*
* @event
*/
_onClose() {
this.logger.info({
tnx: 'closed'
}, (this.options.lmtp ? 'LMTP' : 'SMTP') + ' Server closed');
this.emit('close');
}
/**
* Called when server is closed
*
* @event
*/
SMTPServer.prototype._onClose = function () {
this.logger.info((this.options.lmtp ? 'LMTP' : 'SMTP') + ' Server closed');
this.emit('close');
};
/**
* Called when an error occurs with the server
*
* @event
*/
_onError(err) {
this.emit('error', err);
}
/**
* Called when an error occurs with the server
*
* @event
*/
SMTPServer.prototype._onError = function (err) {
this.emit('error', err);
};
/**
* Called when a new connection is established. This might not be the same time the socket is opened
*
* @event
*/
_onClientConnect(data) {
this.emit('connect', data);
}
}
/**
* Called when a new connection is established. This might not be the same time the socket is opened
*
* @event
*/
SMTPServer.prototype._onClientConnect = function (data) {
this.emit('connect', data);
};
// Expose to the world
module.exports.SMTPServer = SMTPServer;
'use strict';
var stream = require('stream');
var util = require('util');
var Writable = stream.Writable;
var PassThrough = stream.PassThrough;
const stream = require('stream');
const Writable = stream.Writable;
const PassThrough = stream.PassThrough;
// Expose to the world
module.exports.SMTPStream = SMTPStream;
/**

@@ -18,262 +14,265 @@ * Incoming SMTP stream parser. Detects and emits commands. If switched to

*/
function SMTPStream(options) {
// init Writable
this.options = options || {};
Writable.call(this, this.options);
class SMTPStream extends Writable {
constructor(options) {
// init Writable
super(options);
// Indicates if the stream is currently in data mode
this._dataMode = false;
// Output stream for the current data mode
this._dataStream = null;
// How many bytes are allowed for a data stream
this._maxBytes = Infinity;
// How many bytes have been emitted to data stream
this.dataBytes = 0;
// Callback to run once data mode is finished
this._continueCallback = false;
// unprocessed chars from the last parsing iteration (used in command mode)
this._remainder = '';
// unprocessed bytes from the last parsing iteration (used in data mode)
this._lastBytes = false;
// Indicates if the stream is currently in data mode
this._dataMode = false;
// Output stream for the current data mode
this._dataStream = null;
// How many bytes are allowed for a data stream
this._maxBytes = Infinity;
// How many bytes have been emitted to data stream
this.dataBytes = 0;
// Callback to run once data mode is finished
this._continueCallback = false;
// unprocessed chars from the last parsing iteration (used in command mode)
this._remainder = '';
// unprocessed bytes from the last parsing iteration (used in data mode)
this._lastBytes = false;
this.closed = false;
// once the input stream ends, flush all output without expecting the newline
this.on('finish', this._flushData.bind(this));
}
util.inherits(SMTPStream, Writable);
this.closed = false;
// once the input stream ends, flush all output without expecting the newline
this.on('finish', () => this._flushData());
}
/**
* Placeholder command handler. Override this with your own.
*/
SMTPStream.prototype.oncommand = function ( /* command, callback */ ) {
throw new Error('Command handler is not set');
};
/**
* Placeholder command handler. Override this with your own.
*/
oncommand( /* command, callback */ ) {
throw new Error('Command handler is not set');
}
/**
* Switch to data mode and return output stream. The dots in the stream are unescaped.
*
* @returns {Stream} Data stream
*/
SMTPStream.prototype.startDataMode = function (maxBytes) {
this._dataMode = true;
this._maxBytes = maxBytes && Number(maxBytes) || Infinity;
this.dataBytes = 0;
this._dataStream = new PassThrough();
/**
* Switch to data mode and return output stream. The dots in the stream are unescaped.
*
* @returns {Stream} Data stream
*/
startDataMode(maxBytes) {
this._dataMode = true;
this._maxBytes = maxBytes && Number(maxBytes) || Infinity;
this.dataBytes = 0;
this._dataStream = new PassThrough();
return this._dataStream;
};
/**
* Call this once data mode is over and you have finished processing the data stream
*/
SMTPStream.prototype.continue = function () {
if (typeof this._continueCallback === 'function') {
this._continueCallback();
this._continueCallback = false;
} else {
// indicate that the 'continue' was already called once the stream actually ends
this._continueCallback = true;
return this._dataStream;
}
};
// PRIVATE METHODS
/**
* Writable._write method.
*/
SMTPStream.prototype._write = function (chunk, encoding, next) {
if (!chunk || !chunk.length) {
return next();
/**
* Call this once data mode is over and you have finished processing the data stream
*/
continue () {
if (typeof this._continueCallback === 'function') {
this._continueCallback();
this._continueCallback = false;
} else {
// indicate that the 'continue' was already called once the stream actually ends
this._continueCallback = true;
}
}
var data;
var pos = 0;
var newlineRegex;
// PRIVATE METHODS
var called = false;
var done = function () {
if (called) {
return;
/**
* Writable._write method.
*/
_write(chunk, encoding, next) {
if (!chunk || !chunk.length) {
return next();
}
called = true;
var args = [].slice.call(arguments);
next.apply(null, args);
};
if (this.closed) {
return done();
}
let data;
let pos = 0;
let newlineRegex;
if (!this._dataMode) {
let called = false;
let done = (...args) => {
if (called) {
return;
}
called = true;
next(...args);
};
newlineRegex = /\r?\n/g;
data = this._remainder + chunk.toString('binary');
if (this.closed) {
return done();
}
var readLine = function () {
var match;
var line;
var buf;
if (!this._dataMode) {
// check if the mode is not changed
if (this._dataMode) {
buf = new Buffer(data.substr(pos), 'binary');
this._remainder = '';
return this._write(buf, 'buffer', done);
}
newlineRegex = /\r?\n/g;
data = this._remainder + chunk.toString('binary');
// search for the next newline
// exec keeps count of the last match with lastIndex
// so it knows from where to start with the next iteration
if ((match = newlineRegex.exec(data))) {
line = data.substr(pos, match.index - pos);
pos += line.length + match[0].length;
} else {
this._remainder = pos < data.length ? data.substr(pos) : '';
return done();
}
let readLine = () => {
let match;
let line;
let buf;
this.oncommand(new Buffer(line, 'binary'), readLine);
}.bind(this);
// check if the mode is not changed
if (this._dataMode) {
buf = new Buffer(data.substr(pos), 'binary');
this._remainder = '';
return this._write(buf, 'buffer', done);
}
// start reading lines
readLine();
// search for the next newline
// exec keeps count of the last match with lastIndex
// so it knows from where to start with the next iteration
if ((match = newlineRegex.exec(data))) {
line = data.substr(pos, match.index - pos);
pos += line.length + match[0].length;
} else {
this._remainder = pos < data.length ? data.substr(pos) : '';
return done();
}
} else {
this._feedDataStream(chunk, done);
}
};
this.oncommand(new Buffer(line, 'binary'), readLine);
};
/**
* Processes a chunk in data mode. Escape dots are removed and final dot ends the data mode.
*/
SMTPStream.prototype._feedDataStream = function (chunk, done) {
var i;
var endseq = new Buffer('\r\n.\r\n');
var len;
var handled;
var buf;
// start reading lines
readLine();
if (this._lastBytes && this._lastBytes.length) {
chunk = Buffer.concat([this._lastBytes, chunk], this._lastBytes.length + chunk.length);
this._lastBytes = false;
} else {
this._feedDataStream(chunk, done);
}
}
len = chunk.length;
/**
* Processes a chunk in data mode. Escape dots are removed and final dot ends the data mode.
*/
_feedDataStream(chunk, done) {
let i;
let endseq = new Buffer('\r\n.\r\n');
let len;
let handled;
let buf;
// check if the data does not start with the end terminator
if (!this.dataBytes && len >= 3 && Buffer.compare(chunk.slice(0, 3), new Buffer('.\r\n')) === 0) {
this._endDataMode(false, chunk.slice(3), done);
return;
}
if (this._lastBytes && this._lastBytes.length) {
chunk = Buffer.concat([this._lastBytes, chunk], this._lastBytes.length + chunk.length);
this._lastBytes = false;
}
// check if the first symbol is a escape dot
if (!this.dataBytes && len >= 2 && chunk[0] === 0x2E && chunk[1] === 0x2E) {
chunk = chunk.slice(1);
len--;
}
len = chunk.length;
// seek for the stream ending
for (i = 2; i < len - 2; i++) {
// check if the data does not start with the end terminator
if (!this.dataBytes && len >= 3 && Buffer.compare(chunk.slice(0, 3), new Buffer('.\r\n')) === 0) {
this._endDataMode(false, chunk.slice(3), done);
return;
}
// if the dot is the first char in a line
if (chunk[i] === 0x2E && chunk[i - 1] === 0x0A) {
// check if the first symbol is a escape dot
if (!this.dataBytes && len >= 2 && chunk[0] === 0x2E && chunk[1] === 0x2E) {
chunk = chunk.slice(1);
len--;
}
// if the dot matches end terminator
if (Buffer.compare(chunk.slice(i - 2, i + 3), endseq) === 0) {
// seek for the stream ending
for (i = 2; i < len - 2; i++) {
if (i > 2) {
buf = chunk.slice(0, i);
this.dataBytes += buf.length;
this._endDataMode(buf, chunk.slice(i + 3), done);
} else {
this._endDataMode(false, chunk.slice(i + 3), done);
// if the dot is the first char in a line
if (chunk[i] === 0x2E && chunk[i - 1] === 0x0A) {
// if the dot matches end terminator
if (Buffer.compare(chunk.slice(i - 2, i + 3), endseq) === 0) {
if (i > 2) {
buf = chunk.slice(0, i);
this.dataBytes += buf.length;
this._endDataMode(buf, chunk.slice(i + 3), done);
} else {
this._endDataMode(false, chunk.slice(i + 3), done);
}
return;
}
return;
}
// check if the dot is an escape char and remove it
if (chunk[i + 1] === 0x2E) {
buf = chunk.slice(0, i);
// check if the dot is an escape char and remove it
if (chunk[i + 1] === 0x2E) {
buf = chunk.slice(0, i);
this._lastBytes = false; // clear remainder bytes
this.dataBytes += buf.length; // increment byte counter
this._lastBytes = false; // clear remainder bytes
this.dataBytes += buf.length; // increment byte counter
// emit what we already have and continue without the dot
if (this._dataStream.writable) {
this._dataStream.write(buf);
}
// emit what we already have and continue without the dot
if (this._dataStream.writable) {
this._dataStream.write(buf);
return setImmediate(() => this._feedDataStream(chunk.slice(i + 1), done));
}
return setImmediate(this._feedDataStream.bind(this, chunk.slice(i + 1), done));
}
}
}
// keep the last bytes
if (chunk.length < 4) {
this._lastBytes = chunk;
} else {
this._lastBytes = chunk.slice(chunk.length - 4);
}
// keep the last bytes
if (chunk.length < 4) {
this._lastBytes = chunk;
} else {
this._lastBytes = chunk.slice(chunk.length - 4);
}
// if current chunk is longer than the remainder bytes we keep for later emit the available bytes
if (this._lastBytes.length < chunk.length) {
// if current chunk is longer than the remainder bytes we keep for later emit the available bytes
if (this._lastBytes.length < chunk.length) {
buf = chunk.slice(0, chunk.length - this._lastBytes.length);
this.dataBytes += buf.length;
buf = chunk.slice(0, chunk.length - this._lastBytes.length);
this.dataBytes += buf.length;
// write to stream but stop if need to wait for drain
if (this._dataStream.writable) {
handled = this._dataStream.write(buf);
if (!handled) {
this._dataStream.once('drain', done);
// write to stream but stop if need to wait for drain
if (this._dataStream.writable) {
handled = this._dataStream.write(buf);
if (!handled) {
this._dataStream.once('drain', done);
} else {
return done();
}
} else {
return done();
}
} else {
// nothing to emit, continue with the input stream
return done();
}
} else {
// nothing to emit, continue with the input stream
return done();
}
};
/**
* Flushes remaining bytes
*/
SMTPStream.prototype._flushData = function () {
var line;
if (this._remainder && !this.closed) {
line = this._remainder;
this._remainder = '';
this.oncommand(new Buffer(line, 'binary'));
/**
* Flushes remaining bytes
*/
_flushData() {
let line;
if (this._remainder && !this.closed) {
line = this._remainder;
this._remainder = '';
this.oncommand(new Buffer(line, 'binary'));
}
}
};
/**
* Ends data mode and returns to command mode. Stream is not resumed before #continue is called
*/
SMTPStream.prototype._endDataMode = function (chunk, remainder, callback) {
if (this._continueCallback === true) {
this._continueCallback = false;
// wait until the stream is actually over and then continue
this._dataStream.once('end', callback);
} else {
this._continueCallback = this._write.bind(this, remainder, 'buffer', callback);
}
/**
* Ends data mode and returns to command mode. Stream is not resumed before #continue is called
*/
_endDataMode(chunk, remainder, callback) {
if (this._continueCallback === true) {
this._continueCallback = false;
// wait until the stream is actually over and then continue
this._dataStream.once('end', callback);
} else {
this._continueCallback = () => this._write(remainder, 'buffer', callback);
}
this._dataStream.byteLength = this.dataBytes;
this._dataStream.sizeExceeded = this.dataBytes > this._maxBytes;
this._dataStream.byteLength = this.dataBytes;
this._dataStream.sizeExceeded = this.dataBytes > this._maxBytes;
if (chunk && chunk.length && this._dataStream.writable) {
this._dataStream.end(chunk);
} else {
this._dataStream.end();
if (chunk && chunk.length && this._dataStream.writable) {
this._dataStream.end(chunk);
} else {
this._dataStream.end();
}
this._dataMode = false;
this._remainder = '';
this._dataStream = null;
}
this._dataMode = false;
this._remainder = '';
this._dataStream = null;
};
}
// Expose to the world
module.exports.SMTPStream = SMTPStream;

@@ -6,3 +6,3 @@ 'use strict';

var tlsDefaults = {
const tlsDefaults = {
// pregenerated default certificates for localhost

@@ -86,11 +86,9 @@ // obviusly, do not use in production

function getTLSOptions(opts) {
var result = {};
let result = {};
opts = opts || {};
Object.keys(opts).forEach(function (key) {
result[key] = opts[key];
});
Object.keys(opts).forEach(key => result[key] = opts[key]);
Object.keys(tlsDefaults).forEach(function (key) {
Object.keys(tlsDefaults).forEach(key => {
if (!(key in result)) {

@@ -97,0 +95,0 @@ result[key] = tlsDefaults[key];

{
"name": "smtp-server",
"version": "1.17.0",
"version": "2.0.0",
"description": "Create custom SMTP servers on the fly",
"main": "lib/smtp-server.js",
"scripts": {
"test": "grunt mochaTest"
"test": "grunt"
},
"author": "Andris Reinman",
"license": "MIT",
"license": "EUPL-1.1",
"dependencies": {
"ipv6-normalize": "^1.0.1",
"nodemailer-shared": "^1.1.0"
"nodemailer": "^3.0.2"
},
"devDependencies": {
"chai": "^3.5.0",
"eslint-config-nodemailer": "^1.0.0",
"grunt": "^1.0.1",

@@ -21,8 +22,4 @@ "grunt-cli": "^1.2.0",

"grunt-mocha-test": "^0.13.2",
"mocha": "^3.2.0",
"smtp-connection": "^2.12.2"
"mocha": "^3.2.0"
},
"engines": {
"node": ">=0.12"
},
"repository": {

@@ -37,3 +34,6 @@ "type": "git",

"SMTP"
]
],
"engines": {
"node": ">=6.0.0"
}
}

@@ -10,15 +10,4 @@ # smtp-server

Requires Node v0.12 or iojs. The module does not run on Node v0.10 as it uses [Buffer.compare](http://nodejs.org/api/buffer.html#buffer_class_method_buffer_compare_buf1_buf2) and [TLSSocket](http://nodejs.org/api/tls.html#tls_new_tls_tlssocket_socket_options).
> This module is part of the [Nodemailer bundle](https://nodemailer.com/about/pricing/). Starting from v2.0.0 *smtp-server* is licensed under the **[European Union Public License 1.1](http://ec.europa.eu/idabc/eupl.html)**. In general, EUPLv1.1 is a _copyleft_ license compatible with GPLv2, so if you're OK using GPL then you should be OK using *smtp-server*. Previous versions of *smtp-server* are licensed under the MIT license.
## Other similar packages you might be interested in
* **[nodemailer](https://github.com/nodemailer/nodemailer)** – all in one package to send email from Node.js
* **[smtp-server](https://github.com/andris9/smtp-server)** – add SMTP server interface to your application
* **[smtp-connection](https://github.com/nodemailer/smtp-connection)** – connect to SMTP servers from your application
* **[zone-mta](https://github.com/zone-eu/zone-mta)** – full featured outbound MTA built using smtp-connection and smtp-server modules
## Support smtp-server development
[![Donate to author](https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=DB26KWR2BQX5W)
## Usage

@@ -32,3 +21,3 @@

var SMTPServer = require('smtp-server').SMTPServer;
let SMTPServer = require('smtp-server').SMTPServer;

@@ -38,3 +27,3 @@ ### Create SMTPServer instance

```javascript
var server = new SMTPServer(options);
let server = new SMTPServer(options);
```

@@ -92,3 +81,3 @@

// This example starts a SMTP server using TLS with your own certificate and key
var server = new SMTPServer({
let server = new SMTPServer({
secure: true,

@@ -118,3 +107,3 @@ key: fs.readFileSync('private.key'),

```javascript
server.on('error', function(err){
server.on('error', err => {
console.log('Error %s', err.message);

@@ -129,4 +118,4 @@ });

```javascript
var server = new SMTPServer({
onAuth: function(auth, session, callback){}
let server = new SMTPServer({
onAuth(auth, session, callback){}
});

@@ -157,4 +146,4 @@ ```

```javascript
var server = new SMTPServer({
onAuth: function(auth, session, callback){
let server = new SMTPServer({
onAuth(auth, session, callback){
if(auth.username !== 'abc' || auth.password !== 'def'){

@@ -174,5 +163,5 @@ return callback(new Error('Invalid username or password'));

```javascript
var server = new SMTPServer({
let server = new SMTPServer({
authMethods: ['XOAUTH2'], // XOAUTH2 is not enabled by default
onAuth: function(auth, session, callback){
onAuth(auth, session, callback){
if(auth.method !== 'XOAUTH2'){

@@ -204,5 +193,5 @@ // should never occur in this case as only XOAUTH2 is allowed

```javascript
var server = new SMTPServer({
let server = new SMTPServer({
authMethods: ['CRAM-MD5'], // CRAM-MD5 is not enabled by default
onAuth: function(auth, session, callback){
onAuth(auth, session, callback){
if(auth.method !== 'CRAM-MD5'){

@@ -231,4 +220,4 @@ // should never occur in this case as only CRAM-MD5 is allowed

```javascript
var server = new SMTPServer({
onConnect: function(session, callback){}
let server = new SMTPServer({
onConnect(session, callback){}
});

@@ -243,4 +232,4 @@ ```

```javascript
var server = new SMTPServer({
onConnect: function(session, callback){
let server = new SMTPServer({
onConnect(session, callback){
if(session.remoteAddress === '127.0.0.1'){

@@ -257,4 +246,4 @@ return callback(new Error('No connections from localhost allowed'));

```javascript
var server = new SMTPServer({
onClose: function(session){}
let server = new SMTPServer({
onClose(session){}
});

@@ -269,4 +258,4 @@ ```

```javascript
var server = new SMTPServer({
onMailFrom: function(address, session, callback){}
let server = new SMTPServer({
onMailFrom(address, session, callback){}
});

@@ -282,4 +271,4 @@ ```

```javascript
var server = new SMTPServer({
onMailFrom: function(address, session, callback){
let server = new SMTPServer({
onMailFrom(address, session, callback){
if(address.address !== 'allowed@example.com'){

@@ -299,4 +288,4 @@ return callback(new Error('Only allowed@example.com is allowed to send mail'));

```javascript
var server = new SMTPServer({
onRcptTo: function(address, session, callback){}
let server = new SMTPServer({
onRcptTo(address, session, callback){}
});

@@ -312,4 +301,4 @@ ```

```javascript
var server = new SMTPServer({
onRcptTo: function(address, session, callback){
let server = new SMTPServer({
onRcptTo(address, session, callback){
if(address.address !== 'allowed@example.com'){

@@ -328,4 +317,4 @@ return callback(new Error('Only allowed@example.com is allowed to receive mail'));

```javascript
var server = new SMTPServer({
onData: function(stream, session, callback){}
let server = new SMTPServer({
onData(stream, session, callback){}
});

@@ -338,7 +327,7 @@ ```

* **session** includes the `envelope` object and `user` data if logged in, see details [here](#session-object)
* **callback** is the function to run once the stream is ended and you have processed the outcome. If you return an error object, the message is rejected, otherwise it is accepted
* **callback** is the on to run once the stream is ended and you have processed the outcome. If you return an error object, the message is rejected, otherwise it is accepted
```javascript
var server = new SMTPServer({
onData: function(stream, session, callback){
let server = new SMTPServer({
onData(stream, session, callback){
stream.pipe(process.stdout); // print message to console

@@ -357,7 +346,7 @@ stream.on('end', callback);

```javascript
var server = new SMTPServer({
let server = new SMTPServer({
size: 1024, // allow messages up to 1 kb
onRcptTo: function (address, session, callback) {
onRcptTo(address, session, callback) {
// do not accept messages larger than 100 bytes to specific recipients
var expectedSize = Number(session.envelope.mailFrom.args.SIZE) || 0;
let expectedSize = Number(session.envelope.mailFrom.args.SIZE) || 0;
if (address.address === 'almost-full@example.com' && expectedSize > 100) {

@@ -370,6 +359,6 @@ err = new Error('Insufficient channel storage: ' + address.address);

},
onData: function(stream, session, callback){
onData(stream, session, callback){
stream.pipe(process.stdout); // print message to console
stream.on('end', function(){
var err;
stream.on('end', () => {
let err;
if(stream.sizeExceeded){

@@ -398,9 +387,9 @@ err = new Error('Message exceeds fixed maximum message size');

```javascript
var server = new SMTPServer({
let server = new SMTPServer({
lmtp: true,
onData: function(stream, session, callback){
onData(stream, session, callback){
stream.pipe(process.stdout); // print message to console
stream.on('end', function(){
stream.on('end', () => {
// reject every other recipient
var response = session.envelope.rcptTo.map(function (rcpt, i) {
let response = session.envelope.rcptTo.map((rcpt, i) => {
if (i % 2) {

@@ -429,3 +418,3 @@ return new Error('<' + rcpt.address + '> Not accepted');

* **hostNameAppearsAs** hostname the client provided with HELO/EHLO call
* **envelope** includes denvelope data
* **envelope** includes envelope data
* **mailFrom** includes an address object or is set to false

@@ -507,2 +496,2 @@ * **rcptTo** includes an array of address objects

**MIT**
**EUPLv1.1**

@@ -1,14 +0,15 @@

/* eslint no-unused-expressions:0 */
/* globals afterEach, beforeEach, describe, it */
/* eslint no-unused-expressions:0, prefer-arrow-callback: 0 */
/* globals beforeEach, describe, it */
'use strict';
var chai = require('chai');
var Client = require('smtp-connection');
var SMTPServer = require('../lib/smtp-server').SMTPServer;
var SMTPConnection = require('../lib/smtp-connection').SMTPConnection;
var net = require('net');
const chai = require('chai');
const Client = require('nodemailer/lib/smtp-connection');
const XOAuth2 = require('nodemailer/lib/xoauth2');
const SMTPServer = require('../lib/smtp-server').SMTPServer;
const SMTPConnection = require('../lib/smtp-connection').SMTPConnection;
const net = require('net');
var expect = chai.expect;
var fs = require('fs');
const expect = chai.expect;
const fs = require('fs');

@@ -24,3 +25,3 @@ chai.config.includeStack = true;

it('should parse MAIL FROM/RCPT TO', function () {
var conn = new SMTPConnection({
let conn = new SMTPConnection({
options: {}

@@ -61,5 +62,5 @@ }, {});

describe('Plaintext server', function () {
var PORT = 1336;
let PORT = 1336;
var server = new SMTPServer({
let server = new SMTPServer({
maxClients: 5,

@@ -79,3 +80,3 @@ logger: false,

it('should connect without TLS', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -94,3 +95,3 @@ host: '127.0.0.1',

it('should connect with TLS', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -111,9 +112,9 @@ host: '127.0.0.1',

it('open multiple connections', function (done) {
var limit = 5;
var disconnected = 0;
var connected = 0;
var connections = [];
let limit = 5;
let disconnected = 0;
let connected = 0;
let connections = [];
var createConnection = function (callback) {
var connection = new Client({
let createConnection = function (callback) {
let connection = new Client({
port: PORT,

@@ -145,3 +146,3 @@ host: '127.0.0.1',

var connCb = function (err, conn) {
let connCb = function (err, conn) {
expect(err).to.not.exist;

@@ -157,3 +158,3 @@ connections.push(conn);

for (var i = 0; i < limit; i++) {
for (let i = 0; i < limit; i++) {
createConnection(connCb);

@@ -165,10 +166,10 @@ }

it('should reject too many connections', function (done) {
var limit = 7;
var expectedErrors = 2;
var disconnected = 0;
var connected = 0;
var connections = [];
let limit = 7;
let expectedErrors = 2;
let disconnected = 0;
let connected = 0;
let connections = [];
var createConnection = function (callback) {
var connection = new Client({
let createConnection = function (callback) {
let connection = new Client({
port: PORT,

@@ -204,3 +205,3 @@ host: '127.0.0.1',

var connCb = function (err, conn) {
let connCb = function (err, conn) {
expect(err).to.not.exist;

@@ -216,3 +217,3 @@ connections.push(conn);

for (var i = 0; i < limit; i++) {
for (let i = 0; i < limit; i++) {
createConnection(connCb);

@@ -224,3 +225,3 @@ }

it('should close on timeout', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -243,3 +244,3 @@ host: '127.0.0.1',

it('should close on timeout using secure socket', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -267,5 +268,5 @@ host: '127.0.0.1',

var PORT = 1336;
let PORT = 1336;
var server = new SMTPServer({
let server = new SMTPServer({
logger: false,

@@ -281,11 +282,11 @@ socketTimeout: 100 * 1000,

it('open multiple connections and close all at once', function (done) {
var limit = 100;
var cleanClose = 4;
let limit = 100;
let cleanClose = 4;
var disconnected = 0;
var connected = 0;
var connections = [];
let disconnected = 0;
let connected = 0;
let connections = [];
var createConnection = function (callback) {
var connection = new Client({
let createConnection = function (callback) {
let connection = new Client({
port: PORT,

@@ -316,3 +317,3 @@ host: '127.0.0.1',

var connCb = function (err, conn) {
let connCb = function (err, conn) {
expect(err).to.not.exist;

@@ -324,3 +325,3 @@ connections.push(conn);

setTimeout(function () {
for (var i = 0; i < cleanClose; i++) {
for (let i = 0; i < cleanClose; i++) {
connections[i].quit();

@@ -340,5 +341,5 @@ }

describe('Plaintext server with hidden STARTTLS', function () {
var PORT = 1336;
let PORT = 1336;
var server = new SMTPServer({
let server = new SMTPServer({
maxClients: 5,

@@ -359,3 +360,3 @@ hideSTARTTLS: true,

it('should connect without TLS', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -374,3 +375,3 @@ host: '127.0.0.1'

it('should connect with TLS', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -394,5 +395,5 @@ host: '127.0.0.1',

describe('Plaintext server with no STARTTLS', function () {
var PORT = 1336;
let PORT = 1336;
var server = new SMTPServer({
let server = new SMTPServer({
maxClients: 5,

@@ -402,3 +403,3 @@ disabledCommands: ['STARTTLS'],

socketTimeout: 2 * 1000,
onAuth: function (auth, session, callback) {
onAuth(auth, session, callback) {
expect(session.tlsOptions).to.be.false;

@@ -426,3 +427,3 @@ if (auth.username === 'testuser' && auth.password === 'testpass') {

it('should connect without TLS', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -441,3 +442,3 @@ host: '127.0.0.1'

it('should not connect with TLS', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -451,3 +452,3 @@ host: '127.0.0.1',

var error;
let error;

@@ -471,3 +472,3 @@ connection.on('error', function (err) {

it('should close after too many unauthenticated commands', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -485,3 +486,3 @@ host: '127.0.0.1',

connection.connect(function () {
var looper = function () {
let looper = function () {
connection._currentAction = function () {

@@ -497,3 +498,3 @@ looper();

it('should close after too many unrecognized commands', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -517,3 +518,3 @@ host: '127.0.0.1',

var looper = function () {
let looper = function () {
connection._currentAction = function () {

@@ -530,4 +531,4 @@ looper();

it('should reject early talker', function (done) {
var socket = net.connect(PORT, '127.0.0.1', function () {
var buffers = [];
let socket = net.connect(PORT, '127.0.0.1', function () {
let buffers = [];
socket.on('data', function (chunk) {

@@ -537,3 +538,3 @@ buffers.push(chunk);

socket.on('end', function () {
var data = Buffer.concat(buffers).toString();
let data = Buffer.concat(buffers).toString();
expect(/^421 /.test(data)).to.be.true;

@@ -547,5 +548,5 @@ done();

it('should reject HTTP requests', function (done) {
var socket = net.connect(PORT, '127.0.0.1', function () {
var buffers = [];
var started = false;
let socket = net.connect(PORT, '127.0.0.1', function () {
let buffers = [];
let started = false;
socket.on('data', function (chunk) {

@@ -560,3 +561,3 @@ buffers.push(chunk);

socket.on('end', function () {
var data = Buffer.concat(buffers).toString();
let data = Buffer.concat(buffers).toString();
expect(/^421 /m.test(data)).to.be.true;

@@ -571,5 +572,5 @@ done();

describe('Secure server', function () {
var PORT = 1336;
let PORT = 1336;
var server = new SMTPServer({
let server = new SMTPServer({
secure: true,

@@ -590,3 +591,3 @@ logger: false

it('should connect to secure server', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -609,5 +610,5 @@ host: '127.0.0.1',

describe('Authentication tests', function () {
var PORT = 1336;
let PORT = 1336;
var server = new SMTPServer({
let server = new SMTPServer({
maxClients: 5,

@@ -617,3 +618,3 @@ logger: false,

allowInsecureAuth: true,
onAuth: function (auth, session, callback) {
onAuth(auth, session, callback) {
expect(session.tlsOptions).to.exist;

@@ -663,3 +664,3 @@ if (auth.method === 'XOAUTH2') {

it('should authenticate', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -669,4 +670,3 @@ host: '127.0.0.1',

rejectUnauthorized: false
},
authMethod: 'PLAIN'
}
});

@@ -679,3 +679,4 @@

user: 'testuser',
pass: 'testpass'
pass: 'testpass',
method: 'PLAIN'
}, function (err) {

@@ -689,3 +690,3 @@ expect(err).to.not.exist;

it('should fail', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -695,4 +696,3 @@ host: '127.0.0.1',

rejectUnauthorized: false
},
authMethod: 'PLAIN'
}
});

@@ -705,3 +705,4 @@

user: 'zzzz',
pass: 'yyyy'
pass: 'yyyy',
method: 'PLAIN'
}, function (err) {

@@ -718,3 +719,3 @@ expect(err).to.exist;

it('should authenticate', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -725,3 +726,2 @@ host: '127.0.0.1',

},
authMethod: 'LOGIN',
logger: false

@@ -735,3 +735,4 @@ });

user: 'testuser',
pass: 'testpass'
pass: 'testpass',
method: 'LOGIN'
}, function (err) {

@@ -745,7 +746,6 @@ expect(err).to.not.exist;

it('should authenticate without STARTTLS', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,
host: '127.0.0.1',
ignoreTLS: true,
authMethod: 'LOGIN',
logger: false

@@ -759,3 +759,4 @@ });

user: 'testuser',
pass: 'testpass'
pass: 'testpass',
method: 'LOGIN'
}, function (err) {

@@ -769,3 +770,3 @@ expect(err).to.not.exist;

it('should fail', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -775,4 +776,3 @@ host: '127.0.0.1',

rejectUnauthorized: false
},
authMethod: 'LOGIN'
}
});

@@ -785,3 +785,4 @@

user: 'zzzz',
pass: 'yyyy'
pass: 'yyyy',
method: 'LOGIN'
}, function (err) {

@@ -798,3 +799,3 @@ expect(err).to.exist;

it('should authenticate', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -804,4 +805,3 @@ host: '127.0.0.1',

rejectUnauthorized: false
},
authMethod: 'XOAUTH2'
}
});

@@ -813,4 +813,9 @@

connection.login({
type: 'oauth2',
user: 'testuser',
xoauth2: 'testtoken'
method: 'XOAUTH2',
oauth2: new XOAuth2({
user: 'testuser',
accessToken: 'testtoken'
}, false)
}, function (err) {

@@ -824,3 +829,3 @@ expect(err).to.not.exist;

it('should fail', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -830,4 +835,3 @@ host: '127.0.0.1',

rejectUnauthorized: false
},
authMethod: 'XOAUTH2'
}
});

@@ -839,4 +843,9 @@

connection.login({
type: 'oauth2',
user: 'zzzz',
xoauth2: 'testtoken'
method: 'XOAUTH2',
oauth2: new XOAuth2({
user: 'zzzz',
accessToken: 'testtoken'
}, false)
}, function (err) {

@@ -853,3 +862,3 @@ expect(err).to.exist;

it('should authenticate', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -859,4 +868,3 @@ host: '127.0.0.1',

rejectUnauthorized: false
},
authMethod: 'CRAM-MD5'
}
});

@@ -869,3 +877,4 @@

user: 'testuser',
pass: 'testpass'
pass: 'testpass',
method: 'CRAM-MD5'
}, function (err) {

@@ -879,3 +888,3 @@ expect(err).to.not.exist;

it('should fail', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -885,4 +894,3 @@ host: '127.0.0.1',

rejectUnauthorized: false
},
authMethod: 'CRAM-MD5'
}
});

@@ -895,3 +903,4 @@

user: 'zzzz',
pass: 'yyyy'
pass: 'yyyy',
method: 'CRAM-MD5'
}, function (err) {

@@ -907,7 +916,7 @@ expect(err).to.exist;

describe('Mail tests', function () {
var PORT = 1336;
let PORT = 1336;
var connection;
let connection;
var server = new SMTPServer({
let server = new SMTPServer({
maxClients: 5,

@@ -946,13 +955,13 @@ logger: false,

server.onData = function (stream, session, callback) {
var chunks = [];
var chunklen = 0;
let chunks = [];
let chunklen = 0;
stream.on('data', function (chunk) {
stream.on('data', chunk => {
chunks.push(chunk);
chunklen += chunk.length;
}.bind(this));
});
stream.on('end', function () {
var message = Buffer.concat(chunks, chunklen).toString();
var err;
stream.on('end', () => {
let message = Buffer.concat(chunks, chunklen).toString();
let err;

@@ -968,3 +977,3 @@ if (/^deny/i.test(message)) {

callback(null, 'Message queued as abcdef'); // accept the message once the stream is ended
}.bind(this));
});
};

@@ -1105,8 +1114,8 @@

it('should allow addresses with UTF-8 characters', function (done) {
var utf8Address = 'δοκιμή@παράδειγμα.δοκιμή';
var PORT = 1336;
let utf8Address = 'δοκιμή@παράδειγμα.δοκιμή';
let PORT = 1336;
var connection;
let connection;
var server = new SMTPServer({
let server = new SMTPServer({
logger: false,

@@ -1148,7 +1157,7 @@ disabledCommands: ['AUTH', 'STARTTLS']

it('should accept a prematurely called continue callback', function (done) {
var PORT = 1336;
let PORT = 1336;
var connection;
let connection;
var server = new SMTPServer({
let server = new SMTPServer({
logger: false,

@@ -1187,11 +1196,11 @@ disabledCommands: ['AUTH', 'STARTTLS']

describe('PROXY server', function () {
var PORT = 1336;
let PORT = 1336;
var server = new SMTPServer({
let server = new SMTPServer({
maxClients: 5,
logger: false,
useProxy: true,
onConnect: function (session, callback) {
onConnect(session, callback) {
if (session.remoteAddress === '1.2.3.4') {
var err = new Error('Blacklisted IP');
let err = new Error('Blacklisted IP');
err.responseCode = 421;

@@ -1213,3 +1222,3 @@ return callback(err);

it('should rewrite remote address value', function (done) {
var connection = new Client({
let connection = new Client({
port: PORT,

@@ -1223,3 +1232,3 @@ host: '127.0.0.1',

connection.connect(function () {
var conn;
let conn;
// get first connection

@@ -1241,4 +1250,4 @@ server.connections.forEach(function (val) {

it('should block blacklisted connection', function (done) {
var socket = net.connect(PORT, '127.0.0.1', function () {
var buffers = [];
let socket = net.connect(PORT, '127.0.0.1', function () {
let buffers = [];
socket.on('data', function (chunk) {

@@ -1248,3 +1257,3 @@ buffers.push(chunk);

socket.on('end', function () {
var data = Buffer.concat(buffers).toString();
let data = Buffer.concat(buffers).toString();
expect(data.indexOf('421 ')).to.equal(0);

@@ -1260,10 +1269,10 @@ expect(data.indexOf('Blacklisted')).to.gte(4);

describe('onClose handler', function () {
var PORT = 1336;
let PORT = 1336;
it('should detect once a connection is closed', function (done) {
var closed = 0;
var total = 50;
var server = new SMTPServer({
let closed = 0;
let total = 50;
let server = new SMTPServer({
logger: false,
onClose: function (session) {
onClose(session) {
expect(session).to.exist;

@@ -1278,4 +1287,4 @@ expect(closed).to.be.lt(total);

server.listen(PORT, '127.0.0.1', function () {
var createConnection = function () {
var connection = new Client({
let createConnection = function () {
let connection = new Client({
port: PORT,

@@ -1287,6 +1296,6 @@ host: '127.0.0.1',

connection.connect(function () {
setTimeout(connection.quit.bind(connection), 100);
setTimeout(() => connection.quit(), 100);
});
};
for (var i = 0; i < total; i++) {
for (let i = 0; i < total; i++) {
createConnection();

@@ -1293,0 +1302,0 @@ }

@@ -1,9 +0,9 @@

/* eslint no-unused-expressions:0 */
/* globals describe, it */
/* eslint no-unused-expressions:0, prefer-arrow-callback: 0 */
/* globals beforeEach, describe, it */
'use strict';
var chai = require('chai');
var SMTPStream = require('../lib/smtp-stream').SMTPStream;
var expect = chai.expect;
const chai = require('chai');
const SMTPStream = require('../lib/smtp-stream').SMTPStream;
const expect = chai.expect;

@@ -14,5 +14,5 @@ chai.config.includeStack = true;

it('should emit commands', function (done) {
var stream = new SMTPStream();
let stream = new SMTPStream();
var expecting = [
let expecting = [
new Buffer([0x43, 0x4d, 0x44, 0x31]),

@@ -36,5 +36,5 @@ new Buffer([0x43, 0x4d, 0x44, 0x32]),

it('should start data stream', function (done) {
var stream = new SMTPStream();
let stream = new SMTPStream();
var expecting = [
let expecting = [
'DATA',

@@ -48,4 +48,4 @@ 'QUIT'

var datastream;
var output = '';
let datastream;
let output = '';
if (cmd === 'DATA') {

@@ -52,0 +52,0 @@ datastream = stream.startDataMode();

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc