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 3.0.1 to 3.1.0

4

CHANGELOG.md
# Changelog
## v3.1.0 2017-08-16
* Added new server option `needsUpgrade` to upgrade sockets to TLS immediately after connection is established. Works with secure: true
## v3.0.0 2017-04-06

@@ -4,0 +8,0 @@

1

examples/lmtp.js

@@ -18,3 +18,2 @@ /* eslint no-console: 0 */

const server = new SMTPServer({
// log to console

@@ -21,0 +20,0 @@ logger: true,

@@ -21,3 +21,2 @@ /* eslint no-console: 0 */

const server = new SMTPServer({
// log to console

@@ -53,8 +52,7 @@ logger: true,

// check username and password
if (auth.username === username &&
(
auth.method === 'CRAM-MD5' ?
auth.validatePassword(password) : // if cram-md5, validate challenge response
auth.password === password // for other methods match plaintext passwords
)
if (
auth.username === username &&
(auth.method === 'CRAM-MD5'
? auth.validatePassword(password) // if cram-md5, validate challenge response
: auth.password === password) // for other methods match plaintext passwords
) {

@@ -61,0 +59,0 @@ return callback(null, {

'use strict';
module.exports = function (grunt) {
module.exports = function(grunt) {
// Project configuration.

@@ -6,0 +5,0 @@ grunt.initConfig({

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

const SASL = module.exports = {
const SASL = (module.exports = {
SASL_PLAIN(args, callback) {

@@ -54,3 +53,3 @@ if (args.length > 1) {

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

@@ -61,4 +60,5 @@ this.send(501, 'Error: syntax: AUTH CRAM-MD5');

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

@@ -91,44 +91,62 @@ this.name // hostname

this._server.onAuth({
method: 'PLAIN',
username,
password
}, this.session, (err, response) => {
this._server.onAuth(
{
method: 'PLAIN',
username,
password
},
this.session,
(err, response) => {
if (err) {
this._server.logger.info(
{
err,
tnx: 'autherror',
cid: this._id,
method: 'PLAIN',
user: username
},
'Authentication error for %s using %s. %s',
username,
'PLAIN',
err.message
);
this.send(err.responseCode || 535, err.message);
return callback();
}
if (err) {
this._server.logger.info({
err,
tnx: 'autherror',
cid: this._id,
method: 'PLAIN',
user: username
}, 'Authentication error for %s using %s. %s', username, 'PLAIN', err.message);
this.send(err.responseCode || 535, err.message);
return callback();
}
if (!response.user) {
this._server.logger.info(
{
tnx: 'authfail',
cid: this._id,
method: 'PLAIN',
user: username
},
'Authentication failed for %s using %s',
username,
'PLAIN'
);
this.send(response.responseCode || 535, response.message || 'Error: Authentication credentials invalid');
return callback();
}
if (!response.user) {
this._server.logger.info({
tnx: 'authfail',
cid: this._id,
method: 'PLAIN',
user: username
}, 'Authentication failed for %s using %s', username, 'PLAIN');
this.send(response.responseCode || 535, response.message || 'Error: Authentication credentials invalid');
return callback();
this._server.logger.info(
{
tnx: 'auth',
cid: this._id,
method: 'PLAIN',
user: username
},
'%s authenticated using %s',
username,
'PLAIN'
);
this.session.user = response.user;
this.session.transmissionType = this._transmissionType();
this.send(235, 'Authentication successful');
callback();
}
this._server.logger.info({
tnx: 'auth',
cid: this._id,
method: 'PLAIN',
user: username
}, '%s authenticated using %s', username, 'PLAIN');
this.session.user = response.user;
this.session.transmissionType = this._transmissionType();
this.send(235, 'Authentication successful');
callback();
});
);
},

@@ -166,44 +184,62 @@

this._server.onAuth({
method: 'LOGIN',
username,
password
}, this.session, (err, response) => {
this._server.onAuth(
{
method: 'LOGIN',
username,
password
},
this.session,
(err, response) => {
if (err) {
this._server.logger.info(
{
err,
tnx: 'autherror',
cid: this._id,
method: 'LOGIN',
user: username
},
'Authentication error for %s using %s. %s',
username,
'LOGIN',
err.message
);
this.send(err.responseCode || 535, err.message);
return callback();
}
if (err) {
this._server.logger.info({
err,
tnx: 'autherror',
cid: this._id,
method: 'LOGIN',
user: username
}, 'Authentication error for %s using %s. %s', username, 'LOGIN', err.message);
this.send(err.responseCode || 535, err.message);
return callback();
}
if (!response.user) {
this._server.logger.info(
{
tnx: 'authfail',
cid: this._id,
method: 'LOGIN',
user: username
},
'Authentication failed for %s using %s',
username,
'LOGIN'
);
this.send(response.responseCode || 535, response.message || 'Error: Authentication credentials invalid');
return callback();
}
if (!response.user) {
this._server.logger.info({
tnx: 'authfail',
cid: this._id,
method: 'LOGIN',
user: username
}, 'Authentication failed for %s using %s', username, 'LOGIN');
this.send(response.responseCode || 535, response.message || 'Error: Authentication credentials invalid');
return callback();
this._server.logger.info(
{
tnx: 'auth',
cid: this._id,
method: 'PLAIN',
user: username
},
'%s authenticated using %s',
username,
'LOGIN'
);
this.session.user = response.user;
this.session.transmissionType = this._transmissionType();
this.send(235, 'Authentication successful');
callback();
}
this._server.logger.info({
tnx: 'auth',
cid: this._id,
method: 'PLAIN',
user: username
}, '%s authenticated using %s', username, 'LOGIN');
this.session.user = response.user;
this.session.transmissionType = this._transmissionType();
this.send(235, 'Authentication successful');
callback();
});
);
},

@@ -243,45 +279,63 @@

this._server.onAuth({
method: 'XOAUTH2',
username,
accessToken
}, this.session, (err, response) => {
this._server.onAuth(
{
method: 'XOAUTH2',
username,
accessToken
},
this.session,
(err, response) => {
if (err) {
this._server.logger.info(
{
err,
tnx: 'autherror',
cid: this._id,
method: 'XOAUTH2',
user: username
},
'Authentication error for %s using %s. %s',
username,
'XOAUTH2',
err.message
);
this.send(err.responseCode || 535, err.message);
return callback();
}
if (err) {
this._server.logger.info({
err,
tnx: 'autherror',
cid: this._id,
method: 'XOAUTH2',
user: username
}, 'Authentication error for %s using %s. %s', username, 'XOAUTH2', err.message);
this.send(err.responseCode || 535, err.message);
return callback();
}
if (!response.user) {
this._server.logger.info(
{
tnx: 'authfail',
cid: this._id,
method: 'XOAUTH2',
user: username
},
'Authentication failed for %s using %s',
username,
'XOAUTH2'
);
this._nextHandler = SASL.XOAUTH2_error.bind(this);
this.send(response.responseCode || 334, new Buffer(JSON.stringify(response.data || {})).toString('base64'));
return callback();
}
if (!response.user) {
this._server.logger.info({
tnx: 'authfail',
cid: this._id,
method: 'XOAUTH2',
user: username
}, 'Authentication failed for %s using %s', username, 'XOAUTH2');
this._nextHandler = SASL.XOAUTH2_error.bind(this);
this.send(response.responseCode || 334, new Buffer(JSON.stringify(response.data || {})).toString('base64'));
return callback();
this._server.logger.info(
{
tnx: 'auth',
cid: this._id,
method: 'XOAUTH2',
user: username
},
'%s authenticated using %s',
username,
'XOAUTH2'
);
this.session.user = response.user;
this.session.transmissionType = this._transmissionType();
this.send(235, 'Authentication successful');
callback();
}
this._server.logger.info({
tnx: 'auth',
cid: this._id,
method: 'XOAUTH2',
user: username
}, '%s authenticated using %s', username, 'XOAUTH2');
this.session.user = response.user;
this.session.transmissionType = this._transmissionType();
this.send(235, 'Authentication successful');
callback();
});
);
},

@@ -294,3 +348,3 @@

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

@@ -307,48 +361,66 @@

this._server.onAuth({
method: 'CRAM-MD5',
username,
validatePassword(password) {
let hmac = crypto.createHmac('md5', password);
return hmac.update(challenge).digest('hex').toLowerCase() === challengeResponse;
}
}, this.session, (err, response) => {
this._server.onAuth(
{
method: 'CRAM-MD5',
username,
validatePassword(password) {
let hmac = crypto.createHmac('md5', password);
return hmac.update(challenge).digest('hex').toLowerCase() === challengeResponse;
}
},
this.session,
(err, response) => {
if (err) {
this._server.logger.info(
{
err,
tnx: 'autherror',
cid: this._id,
method: 'CRAM-MD5',
user: username
},
'Authentication error for %s using %s. %s',
username,
'CRAM-MD5',
err.message
);
this.send(err.responseCode || 535, err.message);
return callback();
}
if (err) {
this._server.logger.info({
err,
tnx: 'autherror',
cid: this._id,
method: 'CRAM-MD5',
user: username
}, 'Authentication error for %s using %s. %s', username, 'CRAM-MD5', err.message);
this.send(err.responseCode || 535, err.message);
return callback();
}
if (!response.user) {
this._server.logger.info(
{
tnx: 'authfail',
cid: this._id,
method: 'CRAM-MD5',
user: username
},
'Authentication failed for %s using %s',
username,
'CRAM-MD5'
);
this.send(response.responseCode || 535, response.message || 'Error: Authentication credentials invalid');
return callback();
}
if (!response.user) {
this._server.logger.info({
tnx: 'authfail',
cid: this._id,
method: 'CRAM-MD5',
user: username
}, 'Authentication failed for %s using %s', username, 'CRAM-MD5');
this.send(response.responseCode || 535, response.message || 'Error: Authentication credentials invalid');
return callback();
this._server.logger.info(
{
tnx: 'auth',
cid: this._id,
method: 'CRAM-MD5',
user: username
},
'%s authenticated using %s',
username,
'CRAM-MD5'
);
this.session.user = response.user;
this.session.transmissionType = this._transmissionType();
this.send(235, 'Authentication successful');
callback();
}
this._server.logger.info({
tnx: 'auth',
cid: this._id,
method: 'CRAM-MD5',
user: username
}, '%s authenticated using %s', username, 'CRAM-MD5');
this.session.user = response.user;
this.session.transmissionType = this._transmissionType();
this.send(235, 'Authentication successful');
callback();
});
);
}
};
});

@@ -61,4 +61,6 @@ 'use strict';

this.tlsOptions = this.secure ? this._socket.getCipher() : false;
this.needsUpgrade = !!this._server.options.needsUpgrade;
this.tlsOptions = this.secure && !this.needsUpgrade ? this._socket.getCipher() : false;
// Store remote address for later usage

@@ -111,17 +113,16 @@ this.remoteAddress = (this._socket.remoteAddress || '').replace(/^::ffff:/, '');

// Setup event handlers for the socket
this._setListeners();
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(), 100);
}
});
}
connectionReady(next) {
// Resolve hostname for the remote IP

@@ -137,3 +138,3 @@ let reverseCb = (err, hostnames) => {

this.clientHostname = hostnames && hostnames.shift() || '[' + this.remoteAddress + ']';
this.clientHostname = (hostnames && hostnames.shift()) || '[' + this.remoteAddress + ']';

@@ -143,8 +144,12 @@ this._resetSession();

this._server.onConnect(this.session, err => {
this._server.logger.info({
tnx: 'connection',
cid: this._id,
host: this.remoteAddress,
hostname: this.clientHostname
}, 'Connection from %s', this.clientHostname);
this._server.logger.info(
{
tnx: 'connection',
cid: this._id,
host: this.remoteAddress,
hostname: this.clientHostname
},
'Connection from %s',
this.clientHostname
);

@@ -162,3 +167,6 @@ if (err) {

this.send(220, this.name + ' ' + (this._server.options.lmtp ? 'LMTP' : 'ESMTP') + (this._server.options.banner ? ' ' + this._server.options.banner : ''));
this.send(
220,
this.name + ' ' + (this._server.options.lmtp ? 'LMTP' : 'ESMTP') + (this._server.options.banner ? ' ' + this._server.options.banner : '')
);

@@ -182,3 +190,2 @@ if (typeof next === 'function') {

}
}

@@ -203,7 +210,11 @@

this._socket.write(payload + '\r\n');
this._server.logger.debug({
tnx: 'send',
cid: this._id,
user: this.session.user && this.session.user.username
}, 'S:', payload);
this._server.logger.debug(
{
tnx: 'send',
cid: this._id,
user: this.session.user && this.session.user.username
},
'S:',
payload
);
}

@@ -234,3 +245,3 @@

*/
_setListeners() {
_setListeners(callback) {
this._socket.on('close', () => this._onClose());

@@ -240,2 +251,6 @@ this._socket.on('error', err => this._onError(err));

this._socket.pipe(this._parser);
if (!this.needsUpgrade) {
return callback();
}
this.upgrade(() => false, callback);
}

@@ -247,3 +262,3 @@

*/
_onClose( /* hadError */ ) {
_onClose(/* hadError */) {
if (this._parser) {

@@ -269,8 +284,12 @@ this._parser.closed = true;

this._server.logger.info({
tnx: 'close',
cid: this._id,
host: this.remoteAddress,
user: this.session.user && this.session.user.username
}, 'Connection closed to %s', this.clientHostname || this.remoteAddress);
this._server.logger.info(
{
tnx: 'close',
cid: this._id,
host: this.remoteAddress,
user: this.session.user && this.session.user.username
},
'Connection closed to %s',
this.clientHostname || this.remoteAddress
);
setImmediate(() => this._server.onClose(this.session));

@@ -286,4 +305,3 @@ }

_onError(err) {
if ((err.code === 'ECONNRESET' || err.code === 'EPIPE') &&
(!this.session.envelope || !this.session.envelope.mailFrom)) {
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

@@ -295,7 +313,11 @@ // connection ending by the other party, so we can just ignore it

this._server.logger.error({
err,
tnx: 'error',
user: this.session.user && this.session.user.username
}, '%s', err.message);
this._server.logger.error(
{
err,
tnx: 'error',
user: this.session.user && this.session.user.username
},
'%s',
err.message
);
this.emit('error', err);

@@ -321,8 +343,12 @@ }

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
}, 'C:', (command || '').toString());
this._server.logger.debug(
{
tnx: 'command',
cid: this._id,
command: commandName,
user: this.session.user && this.session.user.username
},
'C:',
(command || '').toString()
);

@@ -342,9 +368,14 @@ let handler;

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
}, 'PROXY from %s through %s', params[1].trim().toLowerCase(), this.remoteAddress);
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
},
'PROXY from %s through %s',
params[1].trim().toLowerCase(),
this.remoteAddress
);
this.remoteAddress = params[1].trim().toLowerCase();

@@ -446,4 +477,3 @@ if (params[3]) {

command = (command || '').toString().trim().toUpperCase();
return !this._server.options.disabledCommands.includes(command) &&
typeof this['handler_' + command] === 'function';
return !this._server.options.disabledCommands.includes(command) && typeof this['handler_' + command] === 'function';
}

@@ -500,3 +530,4 @@

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
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;

@@ -508,6 +539,8 @@ } else {

return invalid ? false : {
address,
args
};
return invalid
? false
: {
address,
args
};
}

@@ -520,3 +553,2 @@

_resetSession() {
let session = this.session;

@@ -748,15 +780,24 @@

if (this.session.user) {
this._server.logger.info({
tnx: 'deauth',
cid: this._id,
user: this.session.user && this.session.user.username
}, 'User deauthenticated using %s', 'XCLIENT');
this._server.logger.info(
{
tnx: 'deauth',
cid: this._id,
user: this.session.user && this.session.user.username
},
'User deauthenticated using %s',
'XCLIENT'
);
this.session.user = false;
}
} else {
this._server.logger.info({
tnx: 'auth',
cid: this._id,
user: value
}, '%s authenticated using %s', value, 'XCLIENT');
this._server.logger.info(
{
tnx: 'auth',
cid: this._id,
user: value
},
'%s authenticated using %s',
value,
'XCLIENT'
);

@@ -781,9 +822,14 @@ this.session.user = {

this._server.logger.info({
tnx: 'xclient',
cid: this._id,
xclientKey: 'ADDR',
xclient: value,
user: this.session.user && this.session.user.username
}, 'XCLIENT from %s through %s', value, this.remoteAddress);
this._server.logger.info(
{
tnx: 'xclient',
cid: this._id,
xclientKey: 'ADDR',
xclient: value,
user: this.session.user && this.session.user.username
},
'XCLIENT from %s through %s',
value,
this.remoteAddress
);

@@ -801,9 +847,13 @@ // store original value for reference as ADDR:DEFAULT

value = value || '';
this._server.logger.info({
tnx: 'xclient',
cid: this._id,
xclientKey: 'NAME',
xclient: value,
user: this.session.user && this.session.user.username
}, 'XCLIENT hostname resolved as "%s"', value);
this._server.logger.info(
{
tnx: 'xclient',
cid: this._id,
xclientKey: 'NAME',
xclient: value,
user: this.session.user && this.session.user.username
},
'XCLIENT hostname resolved as "%s"',
value
);

@@ -819,9 +869,13 @@ // store original value for reference as NAME:DEFAULT

value = Number(value) || '';
this._server.logger.info({
tnx: 'xclient',
cid: this._id,
xclientKey: 'PORT',
xclient: value,
user: this.session.user && this.session.user.username
}, 'XCLIENT remote port resolved as "%s"', value);
this._server.logger.info(
{
tnx: 'xclient',
cid: this._id,
xclientKey: 'PORT',
xclient: value,
user: this.session.user && this.session.user.username
},
'XCLIENT remote port resolved as "%s"',
value
);

@@ -836,3 +890,3 @@ // store original value for reference as NAME:DEFAULT

default:
// other values are not relevant
// other values are not relevant
}

@@ -852,3 +906,6 @@ this._xClient.set(key, value);

// success
this.send(220, this.name + ' ' + (this._server.options.lmtp ? 'LMTP' : 'ESMTP') + (this._server.options.banner ? ' ' + this._server.options.banner : ''));
this.send(
220,
this.name + ' ' + (this._server.options.lmtp ? 'LMTP' : 'ESMTP') + (this._server.options.banner ? ' ' + this._server.options.banner : '')
);
callback();

@@ -925,9 +982,14 @@ }

this._server.logger.info({
tnx: 'xforward',
cid: this._id,
xforwardKey: 'ADDR',
xforward: value,
user: this.session.user && this.session.user.username
}, 'XFORWARD from %s through %s', value, this.remoteAddress);
this._server.logger.info(
{
tnx: 'xforward',
cid: this._id,
xforwardKey: 'ADDR',
xforward: value,
user: this.session.user && this.session.user.username
},
'XFORWARD from %s through %s',
value,
this.remoteAddress
);

@@ -945,9 +1007,13 @@ // store original value for reference as ADDR:DEFAULT

value = value || '';
this._server.logger.info({
tnx: 'xforward',
cid: this._id,
xforwardKey: 'NAME',
xforward: value,
user: this.session.user && this.session.user.username
}, 'XFORWARD hostname resolved as "%s"', value);
this._server.logger.info(
{
tnx: 'xforward',
cid: this._id,
xforwardKey: 'NAME',
xforward: value,
user: this.session.user && this.session.user.username
},
'XFORWARD hostname resolved as "%s"',
value
);
this.clientHostname = value.toLowerCase();

@@ -957,9 +1023,13 @@ break;

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
}, 'XFORWARD port resolved as "%s"', value);
this._server.logger.info(
{
tnx: 'xforward',
cid: this._id,
xforwardKey: 'PORT',
xforward: value,
user: this.session.user && this.session.user.username
},
'XFORWARD port resolved as "%s"',
value
);
this.remotePort = value;

@@ -969,13 +1039,17 @@ break;

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
}, 'XFORWARD HELO name resolved as "%s"', value);
this._server.logger.info(
{
tnx: 'xforward',
cid: this._id,
xforwardKey: 'HELO',
xforward: value,
user: this.session.user && this.session.user.username
},
'XFORWARD HELO name resolved as "%s"',
value
);
this.hostNameAppearsAs = value;
break;
default:
// other values are not relevant
// other values are not relevant
}

@@ -999,3 +1073,2 @@ this._xForward.set(key, value);

handler_STARTTLS(command, callback) {
if (this.secure) {

@@ -1007,51 +1080,4 @@ this.send(503, 'Error: TLS already active');

this.send(220, 'Ready to start TLS');
this._socket.unpipe(this._parser);
this._upgrading = true;
setImmediate(callback); // resume input stream
let secureContext = this._server.secureContext.get('default');
let socketOptions = {
secureContext,
isServer: true,
server: this._server.server,
SNICallback: (servername, cb) => {
cb(null, this._server.secureContext.get(servername.toLowerCase().trim()) || this._server.secureContext.get('default'));
}
};
// 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];
}
});
// remove all listeners from the original socket besides the error handler
this._socket.removeAllListeners();
this._socket.on('error', err => this._onError(err));
// upgrade connection
let secureSocket = new tls.TLSSocket(this._socket, socketOptions);
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());
secureSocket.on('secure', () => {
this.session.secure = this.secure = true;
this._socket = secureSocket;
this._upgrading = false;
this.session.tlsOptions = this.tlsOptions = this._socket.getCipher();
let cipher = this.session.tlsOptions && this.session.tlsOptions.name;
this._server.logger.info({
tnx: 'starttls',
cid: this._id,
user: this.session.user && this.session.user.username,
cipher
}, 'Connection upgraded to TLS using ', cipher || 'N/A');
this._socket.pipe(this._parser);
});
this.upgrade(callback);
}

@@ -1187,10 +1213,14 @@

this._server.logger.debug({
tnx: 'data',
cid: this._id,
bytes: this._parser.dataBytes,
user: this.session.user && this.session.user.username
}, 'C: <%s bytes of DATA>', this._parser.dataBytes);
this._server.logger.debug(
{
tnx: 'data',
cid: this._id,
bytes: this._parser.dataBytes,
user: this.session.user && this.session.user.username
},
'C: <%s bytes of DATA>',
this._parser.dataBytes
);
if ((typeof this._dataStream === 'object') && (this._dataStream) && (this._dataStream.readable)) {
if (typeof this._dataStream === 'object' && this._dataStream && this._dataStream.readable) {
this._dataStream.removeAllListeners();

@@ -1233,3 +1263,3 @@ }

if ((typeof this._parser === 'object') && (this._parser)) {
if (typeof this._parser === 'object' && this._parser) {
this._parser.continue();

@@ -1242,3 +1272,3 @@ }

// do not continue until the stream has actually ended
if ((typeof this._dataStream === 'object') && (this._dataStream) && (this._dataStream.readable)) {
if (typeof this._dataStream === 'object' && this._dataStream && this._dataStream.readable) {
this._dataStream.on('end', () => close(err, message));

@@ -1283,7 +1313,10 @@ return;

handler_SHELL(command, callback) {
this._server.logger.info({
tnx: 'shell',
cid: this._id,
user: this.session.user && this.session.user.username
}, 'Client tried to invoke SHELL');
this._server.logger.info(
{
tnx: 'shell',
cid: this._id,
user: this.session.user && this.session.user.username
},
'Client tried to invoke SHELL'
);

@@ -1303,7 +1336,10 @@ if (!this.session.isWizard) {

handler_KILL(command, callback) {
this._server.logger.info({
tnx: 'kill',
cid: this._id,
user: this.session.user && this.session.user.username
}, 'Client tried to invoke KILL');
this._server.logger.info(
{
tnx: 'kill',
cid: this._id,
user: this.session.user && this.session.user.username
},
'Client tried to invoke KILL'
);

@@ -1314,4 +1350,62 @@ this.send(500, 'Can\'t kill Mom');

upgrade(callback, secureCallback) {
this._socket.unpipe(this._parser);
this._upgrading = true;
setImmediate(callback); // resume input stream
let secureContext = this._server.secureContext.get('default');
let socketOptions = {
secureContext,
isServer: true,
server: this._server.server,
SNICallback: (servername, cb) => {
cb(null, this._server.secureContext.get(servername.toLowerCase().trim()) || this._server.secureContext.get('default'));
}
};
// 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];
}
});
// remove all listeners from the original socket besides the error handler
this._socket.removeAllListeners();
this._socket.on('error', err => this._onError(err));
// upgrade connection
let secureSocket = new tls.TLSSocket(this._socket, socketOptions);
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());
secureSocket.on('secure', () => {
this.session.secure = this.secure = true;
this._socket = secureSocket;
this._upgrading = false;
this.session.tlsOptions = this.tlsOptions = this._socket.getCipher();
let cipher = this.session.tlsOptions && this.session.tlsOptions.name;
this._server.logger.info(
{
tnx: 'starttls',
cid: this._id,
user: this.session.user && this.session.user.username,
cipher
},
'Connection upgraded to TLS using ',
cipher || 'N/A'
);
this._socket.pipe(this._parser);
if (typeof secureCallback === 'function') {
secureCallback();
}
});
}
}
// Expose to the world
module.exports.SMTPConnection = SMTPConnection;

@@ -47,8 +47,6 @@ 'use strict';

// setup disabled commands list
this.options.disabledCommands = [].concat(this.options.disabledCommands || [])
.map(command => (command || '').toString().toUpperCase().trim());
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(method => (method || '').toString().toUpperCase().trim());
this.options.authMethods = [].concat(this.options.authMethods || []).map(method => (method || '').toString().toUpperCase().trim());

@@ -81,4 +79,3 @@ if (!this.options.authMethods.length) {

// setup server listener and connection handler
this.server = (this.options.secure ? tls : net)
.createServer(this.options, socket => this.connect(socket));
this.server = (this.options.secure && !this.options.needsUpgrade ? tls : net).createServer(this.options, socket => this.connect(socket));

@@ -125,5 +122,11 @@ // ensure _sharedCreds, fixes an issue in node v4+ where STARTTLS fails because _sharedCreds does not exist

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);
this.logger.info(
{
tnx: 'close'
},
'Server closing with %s pending connection%s, waiting %s seconds before terminating',
connections,
connections !== 1 ? 's' : '',
timeout / 1000
);
}

@@ -134,5 +137,10 @@

if (connections) {
this.logger.info({
tnx: 'close'
}, 'Closing %s pending connection%s to close the server', connections, connections !== 1 ? 's' : '');
this.logger.info(
{
tnx: 'close'
},
'Closing %s pending connection%s to close the server',
connections,
connections !== 1 ? 's' : ''
);

@@ -189,6 +197,10 @@ this.connections.forEach(connection => {

stream.on('end', () => {
this.logger.info({
tnx: 'message',
size: chunklen
}, '<received %s bytes>', chunklen);
this.logger.info(
{
tnx: 'message',
size: chunklen
},
'<received %s bytes>',
chunklen
);
callback();

@@ -198,3 +210,3 @@ });

onClose( /* session */ ) {
onClose(/* session */) {
// do nothing

@@ -234,3 +246,4 @@ }

address.family === 'IPv4' ? address.address : '[' + address.address + ']',
address.port);
address.port
);
}

@@ -244,5 +257,8 @@

_onClose() {
this.logger.info({
tnx: 'closed'
}, (this.options.lmtp ? 'LMTP' : 'SMTP') + ' Server closed');
this.logger.info(
{
tnx: 'closed'
},
(this.options.lmtp ? 'LMTP' : 'SMTP') + ' Server closed'
);
this.emit('close');

@@ -249,0 +265,0 @@ }

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

*/
oncommand( /* command, callback */ ) {
oncommand(/* command, callback */) {
throw new Error('Command handler is not set');

@@ -54,3 +54,3 @@ }

this._dataMode = true;
this._maxBytes = maxBytes && Number(maxBytes) || Infinity;
this._maxBytes = (maxBytes && Number(maxBytes)) || Infinity;
this.dataBytes = 0;

@@ -65,3 +65,3 @@ this._dataStream = new PassThrough();

*/
continue () {
continue() {
if (typeof this._continueCallback === 'function') {

@@ -104,3 +104,2 @@ this._continueCallback();

if (!this._dataMode) {
newlineRegex = /\r?\n/g;

@@ -137,3 +136,2 @@ data = this._remainder + chunk.toString('binary');

readLine();
} else {

@@ -168,3 +166,3 @@ this._feedDataStream(chunk, done);

// check if the first symbol is a escape dot
if (!this.dataBytes && len >= 2 && chunk[0] === 0x2E && chunk[1] === 0x2E) {
if (!this.dataBytes && len >= 2 && chunk[0] === 0x2e && chunk[1] === 0x2e) {
chunk = chunk.slice(1);

@@ -176,9 +174,6 @@ len--;

for (i = 2; i < len - 2; i++) {
// if the dot is the first char in a line
if (chunk[i] === 0x2E && chunk[i - 1] === 0x0A) {
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) {

@@ -196,3 +191,3 @@ buf = chunk.slice(0, i);

// check if the dot is an escape char and remove it
if (chunk[i + 1] === 0x2E) {
if (chunk[i + 1] === 0x2e) {
buf = chunk.slice(0, i);

@@ -222,3 +217,2 @@

if (this._lastBytes.length < chunk.length) {
buf = chunk.slice(0, chunk.length - this._lastBytes.length);

@@ -238,3 +232,2 @@ this.dataBytes += buf.length;

}
} else {

@@ -283,3 +276,2 @@ // nothing to emit, continue with the input stream

}
}

@@ -286,0 +278,0 @@

@@ -9,3 +9,4 @@ 'use strict';

// obviusly, do not use in production
key: '-----BEGIN RSA PRIVATE KEY-----\n' +
key:
'-----BEGIN RSA PRIVATE KEY-----\n' +
'MIIEpAIBAAKCAQEA6Z5Qqhw+oWfhtEiMHE32Ht94mwTBpAfjt3vPpX8M7DMCTwHs\n' +

@@ -37,3 +38,4 @@ '1xcXvQ4lQ3rwreDTOWdoJeEEy7gMxXqH0jw0WfBx+8IIJU69xstOyT7FRFDvA1yT\n' +

'-----END RSA PRIVATE KEY-----',
cert: '-----BEGIN CERTIFICATE-----\n' +
cert:
'-----BEGIN CERTIFICATE-----\n' +
'MIICpDCCAYwCCQCuVLVKVTXnAjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwls\n' +

@@ -91,3 +93,3 @@ 'b2NhbGhvc3QwHhcNMTUwMjEyMTEzMjU4WhcNMjUwMjA5MTEzMjU4WjAUMRIwEAYD\n' +

Object.keys(opts).forEach(key => result[key] = opts[key]);
Object.keys(opts).forEach(key => (result[key] = opts[key]));

@@ -94,0 +96,0 @@ Object.keys(tlsDefaults).forEach(key => {

{
"name": "smtp-server",
"version": "3.0.1",
"description": "Create custom SMTP servers on the fly",
"main": "lib/smtp-server.js",
"scripts": {
"test": "grunt"
},
"author": "Andris Reinman",
"license": "MIT",
"dependencies": {
"ipv6-normalize": "^1.0.1",
"nodemailer": "^4.0.0"
},
"devDependencies": {
"chai": "^3.5.0",
"eslint-config-nodemailer": "^1.0.0",
"grunt": "^1.0.1",
"grunt-cli": "^1.2.0",
"grunt-eslint": "^19.0.0",
"grunt-mocha-test": "^0.13.2",
"mocha": "^3.2.0"
},
"repository": {
"type": "git",
"url": "git://github.com/andris9/smtp-server.git"
},
"bugs": {
"url": "https://github.com/andris9/smtp-server/issues"
},
"keywords": [
"SMTP"
],
"engines": {
"node": ">=6.0.0"
}
"name": "smtp-server",
"version": "3.1.0",
"description": "Create custom SMTP servers on the fly",
"main": "lib/smtp-server.js",
"scripts": {
"test": "grunt"
},
"author": "Andris Reinman",
"license": "MIT",
"dependencies": {
"ipv6-normalize": "^1.0.1",
"nodemailer": "^4.0.1"
},
"devDependencies": {
"chai": "^4.1.1",
"eslint-config-nodemailer": "^1.2.0",
"grunt": "^1.0.1",
"grunt-cli": "^1.2.0",
"grunt-eslint": "^20.0.0",
"grunt-mocha-test": "^0.13.2",
"mocha": "^3.5.0"
},
"repository": {
"type": "git",
"url": "git://github.com/andris9/smtp-server.git"
},
"bugs": {
"url": "https://github.com/andris9/smtp-server/issues"
},
"keywords": ["SMTP"],
"engines": {
"node": ">=6.0.0"
}
}

@@ -18,13 +18,15 @@ /* eslint no-unused-expressions:0, prefer-arrow-callback: 0 */

describe('SMTPServer', function () {
describe('SMTPServer', function() {
this.timeout(10 * 1000); // eslint-disable-line no-invalid-this
describe('Unit tests', function () {
describe('Unit tests', function() {
describe('#_parseAddressCommand', function() {
it('should parse MAIL FROM/RCPT TO', function() {
let conn = new SMTPConnection(
{
options: {}
},
{}
);
describe('#_parseAddressCommand', function () {
it('should parse MAIL FROM/RCPT TO', function () {
let conn = new SMTPConnection({
options: {}
}, {});
expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<test@example.com>')).to.deep.equal({

@@ -58,6 +60,5 @@ address: 'test@example.com',

});
});
describe('Plaintext server', function () {
describe('Plaintext server', function() {
let PORT = 1336;

@@ -71,11 +72,11 @@

beforeEach(function (done) {
beforeEach(function(done) {
server.listen(PORT, '127.0.0.1', done);
});
afterEach(function (done) {
afterEach(function(done) {
server.close(done);
});
it('should connect without TLS', function (done) {
it('should connect without TLS', function(done) {
let connection = new Client({

@@ -89,3 +90,3 @@ port: PORT,

connection.connect(function () {
connection.connect(function() {
connection.quit();

@@ -95,3 +96,3 @@ });

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

@@ -107,3 +108,3 @@ port: PORT,

connection.connect(function () {
connection.connect(function() {
connection.quit();

@@ -113,3 +114,3 @@ });

it('open multiple connections', function (done) {
it('open multiple connections', function(done) {
let limit = 5;

@@ -120,3 +121,3 @@ let disconnected = 0;

let createConnection = function (callback) {
let createConnection = function(callback) {
let connection = new Client({

@@ -130,3 +131,3 @@ port: PORT,

connection.on('error', function (err) {
connection.on('error', function(err) {
connected++;

@@ -137,3 +138,3 @@ expect(err).to.not.exist;

connection.on('end', function () {
connection.on('end', function() {
disconnected++;

@@ -145,3 +146,3 @@ if (disconnected >= limit) {

connection.connect(function () {
connection.connect(function() {
connected++;

@@ -152,3 +153,3 @@ callback(null, connection);

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

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

if (connected >= limit) {
connections.forEach(function (connection) {
connections.forEach(function(connection) {
connection.close();

@@ -168,6 +169,5 @@ });

}
});
it('should reject too many connections', function (done) {
it('should reject too many connections', function(done) {
let limit = 7;

@@ -179,3 +179,3 @@ let expectedErrors = 2;

let createConnection = function (callback) {
let createConnection = function(callback) {
let connection = new Client({

@@ -189,3 +189,3 @@ port: PORT,

connection.on('error', function (err) {
connection.on('error', function(err) {
connected++;

@@ -200,3 +200,3 @@ if (!expectedErrors) {

connection.on('end', function () {
connection.on('end', function() {
disconnected++;

@@ -208,3 +208,3 @@ if (disconnected >= limit) {

connection.connect(function () {
connection.connect(function() {
connected++;

@@ -215,3 +215,3 @@ callback(null, connection);

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

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

if (connected >= limit) {
connections.forEach(function (connection) {
connections.forEach(function(connection) {
connection.close();

@@ -231,6 +231,5 @@ });

}
});
it('should close on timeout', function (done) {
it('should close on timeout', function(done) {
let connection = new Client({

@@ -242,3 +241,3 @@ port: PORT,

connection.on('error', function (err) {
connection.on('error', function(err) {
expect(err).to.exist;

@@ -249,3 +248,3 @@ });

connection.connect(function () {
connection.connect(function() {
// do nothing, wait until timeout occurs

@@ -255,3 +254,3 @@ });

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

@@ -265,3 +264,3 @@ port: PORT,

connection.on('error', function (err) {
connection.on('error', function(err) {
expect(err).to.exist;

@@ -272,3 +271,3 @@ });

connection.connect(function () {
connection.connect(function() {
// do nothing, wait until timeout occurs

@@ -279,3 +278,3 @@ });

describe('Plaintext server with no connection limit', function () {
describe('Plaintext server with no connection limit', function() {
this.timeout(60 * 1000); // eslint-disable-line no-invalid-this

@@ -291,7 +290,7 @@

beforeEach(function (done) {
beforeEach(function(done) {
server.listen(PORT, '127.0.0.1', done);
});
it('open multiple connections and close all at once', function (done) {
it('open multiple connections and close all at once', function(done) {
let limit = 100;

@@ -304,3 +303,3 @@ let cleanClose = 4;

let createConnection = function (callback) {
let createConnection = function(callback) {
let connection = new Client({

@@ -314,7 +313,7 @@ port: PORT,

connection.on('error', function (err) {
connection.on('error', function(err) {
expect(err.responseCode).to.equal(421); // Server shutting down
});
connection.on('end', function () {
connection.on('end', function() {
disconnected++;

@@ -327,3 +326,3 @@

connection.connect(function () {
connection.connect(function() {
connected++;

@@ -334,3 +333,3 @@ callback(null, connection);

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

@@ -341,3 +340,3 @@ connections.push(conn);

server.close();
setTimeout(function () {
setTimeout(function() {
for (let i = 0; i < cleanClose; i++) {

@@ -353,7 +352,6 @@ connections[i].quit();

createConnection(connCb);
});
});
describe('Plaintext server with hidden STARTTLS', function () {
describe('Plaintext server with hidden STARTTLS', function() {
let PORT = 1336;

@@ -368,11 +366,11 @@

beforeEach(function (done) {
beforeEach(function(done) {
server.listen(PORT, '127.0.0.1', done);
});
afterEach(function (done) {
afterEach(function(done) {
server.close(done);
});
it('should connect without TLS', function (done) {
it('should connect without TLS', function(done) {
let connection = new Client({

@@ -385,3 +383,3 @@ port: PORT,

connection.connect(function () {
connection.connect(function() {
expect(connection.secure).to.be.false;

@@ -392,3 +390,3 @@ connection.quit();

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

@@ -405,3 +403,3 @@ port: PORT,

connection.connect(function () {
connection.connect(function() {
expect(connection.secure).to.be.true;

@@ -413,3 +411,3 @@ connection.quit();

describe('Plaintext server with no STARTTLS', function () {
describe('Plaintext server with no STARTTLS', function() {
let PORT = 1336;

@@ -436,11 +434,11 @@

beforeEach(function (done) {
beforeEach(function(done) {
server.listen(PORT, '127.0.0.1', done);
});
afterEach(function (done) {
afterEach(function(done) {
server.close(done);
});
it('should connect without TLS', function (done) {
it('should connect without TLS', function(done) {
let connection = new Client({

@@ -453,3 +451,3 @@ port: PORT,

connection.connect(function () {
connection.connect(function() {
expect(connection.secure).to.be.false;

@@ -460,3 +458,3 @@ connection.quit();

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

@@ -473,7 +471,7 @@ port: PORT,

connection.on('error', function (err) {
connection.on('error', function(err) {
error = err;
});
connection.on('end', function () {
connection.on('end', function() {
expect(error).to.exist;

@@ -483,3 +481,3 @@ done();

connection.connect(function () {
connection.connect(function() {
// should not be called

@@ -491,3 +489,3 @@ expect(false).to.be.true;

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

@@ -499,3 +497,3 @@ port: PORT,

connection.on('error', function (err) {
connection.on('error', function(err) {
expect(err).to.exist;

@@ -506,5 +504,5 @@ });

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

@@ -518,3 +516,3 @@ };

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

@@ -526,3 +524,3 @@ port: PORT,

connection.on('error', function (err) {
connection.on('error', function(err) {
expect(err).to.exist;

@@ -533,27 +531,30 @@ });

connection.connect(function () {
connection.login({
user: 'testuser',
pass: 'testpass'
}, function (err) {
expect(err).to.not.exist;
connection.connect(function() {
connection.login(
{
user: 'testuser',
pass: 'testpass'
},
function(err) {
expect(err).to.not.exist;
let looper = function () {
connection._currentAction = function () {
looper();
let looper = function() {
connection._currentAction = function() {
looper();
};
connection._sendCommand('ZOOP');
};
connection._sendCommand('ZOOP');
};
looper();
});
looper();
}
);
});
});
it('should reject early talker', function (done) {
let socket = net.connect(PORT, '127.0.0.1', function () {
it('should reject early talker', function(done) {
let socket = net.connect(PORT, '127.0.0.1', function() {
let buffers = [];
socket.on('data', function (chunk) {
socket.on('data', function(chunk) {
buffers.push(chunk);
});
socket.on('end', function () {
socket.on('end', function() {
let data = Buffer.concat(buffers).toString();

@@ -567,7 +568,7 @@ expect(/^421 /.test(data)).to.be.true;

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

@@ -580,3 +581,3 @@

});
socket.on('end', function () {
socket.on('end', function() {
let data = Buffer.concat(buffers).toString();

@@ -588,6 +589,41 @@ expect(/^421 /m.test(data)).to.be.true;

});
});
describe('Secure server', function() {
let PORT = 1336;
let server = new SMTPServer({
secure: true,
logger: false
});
beforeEach(function(done) {
server.listen(PORT, '127.0.0.1', done);
});
afterEach(function(done) {
server.close(function() {
done();
});
});
it('should connect to secure server', function(done) {
let connection = new Client({
port: PORT,
host: '127.0.0.1',
secure: true,
tls: {
rejectUnauthorized: false
}
});
connection.on('end', done);
connection.connect(function() {
connection.quit();
});
});
});
describe('Secure server', function () {
describe('Secure server with upgrade', function() {
let PORT = 1336;

@@ -597,11 +633,12 @@

secure: true,
needsUpgrade: true,
logger: false
});
beforeEach(function (done) {
beforeEach(function(done) {
server.listen(PORT, '127.0.0.1', done);
});
afterEach(function (done) {
server.close(function () {
afterEach(function(done) {
server.close(function() {
done();

@@ -611,3 +648,3 @@ });

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

@@ -624,3 +661,3 @@ port: PORT,

connection.connect(function () {
connection.connect(function() {
connection.quit();

@@ -631,3 +668,3 @@ });

describe('Authentication tests', function () {
describe('Authentication tests', function() {
let PORT = 1336;

@@ -656,9 +693,3 @@

}
} else if (auth.username === 'testuser' &&
(
auth.method === 'CRAM-MD5' ?
auth.validatePassword('testpass') :
auth.password === 'testpass'
)
) {
} else if (auth.username === 'testuser' && (auth.method === 'CRAM-MD5' ? auth.validatePassword('testpass') : auth.password === 'testpass')) {
return callback(null, {

@@ -675,13 +706,12 @@ user: 'userdata'

beforeEach(function (done) {
beforeEach(function(done) {
server.listen(PORT, '127.0.0.1', done);
});
afterEach(function (done) {
afterEach(function(done) {
server.close(done);
});
describe('PLAIN', function () {
it('should authenticate', function (done) {
describe('PLAIN', function() {
it('should authenticate', function(done) {
let connection = new Client({

@@ -697,15 +727,18 @@ port: PORT,

connection.connect(function () {
connection.login({
user: 'testuser',
pass: 'testpass',
method: 'PLAIN'
}, function (err) {
expect(err).to.not.exist;
connection.quit();
});
connection.connect(function() {
connection.login(
{
user: 'testuser',
pass: 'testpass',
method: 'PLAIN'
},
function(err) {
expect(err).to.not.exist;
connection.quit();
}
);
});
});
it('should fail', function (done) {
it('should fail', function(done) {
let connection = new Client({

@@ -721,11 +754,14 @@ port: PORT,

connection.connect(function () {
connection.login({
user: 'zzzz',
pass: 'yyyy',
method: 'PLAIN'
}, function (err) {
expect(err).to.exist;
connection.quit();
});
connection.connect(function() {
connection.login(
{
user: 'zzzz',
pass: 'yyyy',
method: 'PLAIN'
},
function(err) {
expect(err).to.exist;
connection.quit();
}
);
});

@@ -735,5 +771,4 @@ });

describe('LOGIN', function () {
it('should authenticate', function (done) {
describe('LOGIN', function() {
it('should authenticate', function(done) {
let connection = new Client({

@@ -750,15 +785,18 @@ port: PORT,

connection.connect(function () {
connection.login({
user: 'testuser',
pass: 'testpass',
method: 'LOGIN'
}, function (err) {
expect(err).to.not.exist;
connection.quit();
});
connection.connect(function() {
connection.login(
{
user: 'testuser',
pass: 'testpass',
method: 'LOGIN'
},
function(err) {
expect(err).to.not.exist;
connection.quit();
}
);
});
});
it('should authenticate without STARTTLS', function (done) {
it('should authenticate without STARTTLS', function(done) {
let connection = new Client({

@@ -773,15 +811,18 @@ port: PORT,

connection.connect(function () {
connection.login({
user: 'testuser',
pass: 'testpass',
method: 'LOGIN'
}, function (err) {
expect(err).to.not.exist;
connection.quit();
});
connection.connect(function() {
connection.login(
{
user: 'testuser',
pass: 'testpass',
method: 'LOGIN'
},
function(err) {
expect(err).to.not.exist;
connection.quit();
}
);
});
});
it('should fail', function (done) {
it('should fail', function(done) {
let connection = new Client({

@@ -797,11 +838,14 @@ port: PORT,

connection.connect(function () {
connection.login({
user: 'zzzz',
pass: 'yyyy',
method: 'LOGIN'
}, function (err) {
expect(err).to.exist;
connection.quit();
});
connection.connect(function() {
connection.login(
{
user: 'zzzz',
pass: 'yyyy',
method: 'LOGIN'
},
function(err) {
expect(err).to.exist;
connection.quit();
}
);
});

@@ -811,5 +855,4 @@ });

describe('XOAUTH2', function () {
it('should authenticate', function (done) {
describe('XOAUTH2', function() {
it('should authenticate', function(done) {
let connection = new Client({

@@ -825,19 +868,25 @@ port: PORT,

connection.connect(function () {
connection.login({
type: 'oauth2',
user: 'testuser',
method: 'XOAUTH2',
oauth2: new XOAuth2({
connection.connect(function() {
connection.login(
{
type: 'oauth2',
user: 'testuser',
accessToken: 'testtoken'
}, false)
}, function (err) {
expect(err).to.not.exist;
connection.quit();
});
method: 'XOAUTH2',
oauth2: new XOAuth2(
{
user: 'testuser',
accessToken: 'testtoken'
},
false
)
},
function(err) {
expect(err).to.not.exist;
connection.quit();
}
);
});
});
it('should fail', function (done) {
it('should fail', function(done) {
let connection = new Client({

@@ -853,15 +902,21 @@ port: PORT,

connection.connect(function () {
connection.login({
type: 'oauth2',
user: 'zzzz',
method: 'XOAUTH2',
oauth2: new XOAuth2({
connection.connect(function() {
connection.login(
{
type: 'oauth2',
user: 'zzzz',
accessToken: 'testtoken'
}, false)
}, function (err) {
expect(err).to.exist;
connection.quit();
});
method: 'XOAUTH2',
oauth2: new XOAuth2(
{
user: 'zzzz',
accessToken: 'testtoken'
},
false
)
},
function(err) {
expect(err).to.exist;
connection.quit();
}
);
});

@@ -871,5 +926,4 @@ });

describe('CRAM-MD5', function () {
it('should authenticate', function (done) {
describe('CRAM-MD5', function() {
it('should authenticate', function(done) {
let connection = new Client({

@@ -885,15 +939,18 @@ port: PORT,

connection.connect(function () {
connection.login({
user: 'testuser',
pass: 'testpass',
method: 'CRAM-MD5'
}, function (err) {
expect(err).to.not.exist;
connection.quit();
});
connection.connect(function() {
connection.login(
{
user: 'testuser',
pass: 'testpass',
method: 'CRAM-MD5'
},
function(err) {
expect(err).to.not.exist;
connection.quit();
}
);
});
});
it('should fail', function (done) {
it('should fail', function(done) {
let connection = new Client({

@@ -909,11 +966,14 @@ port: PORT,

connection.connect(function () {
connection.login({
user: 'zzzz',
pass: 'yyyy',
method: 'CRAM-MD5'
}, function (err) {
expect(err).to.exist;
connection.quit();
});
connection.connect(function() {
connection.login(
{
user: 'zzzz',
pass: 'yyyy',
method: 'CRAM-MD5'
},
function(err) {
expect(err).to.exist;
connection.quit();
}
);
});

@@ -924,3 +984,3 @@ });

describe('Mail tests', function () {
describe('Mail tests', function() {
let PORT = 1336;

@@ -937,3 +997,3 @@

server.onAuth = function (auth, session, callback) {
server.onAuth = function(auth, session, callback) {
if (auth.username === 'testuser' && auth.password === 'testpass') {

@@ -950,3 +1010,3 @@ return callback(null, {

server.onMailFrom = function (address, session, callback) {
server.onMailFrom = function(address, session, callback) {
if (/^deny/i.test(address.address)) {

@@ -958,3 +1018,3 @@ return callback(new Error('Not accepted'));

server.onRcptTo = function (address, session, callback) {
server.onRcptTo = function(address, session, callback) {
if (/^deny/i.test(address.address)) {

@@ -966,3 +1026,3 @@ return callback(new Error('Not accepted'));

server.onData = function (stream, session, callback) {
server.onData = function(stream, session, callback) {
let chunks = [];

@@ -992,4 +1052,4 @@ let chunklen = 0;

beforeEach(function (done) {
server.listen(PORT, '127.0.0.1', function () {
beforeEach(function(done) {
server.listen(PORT, '127.0.0.1', function() {
connection = new Client({

@@ -1003,10 +1063,13 @@ port: PORT,

connection.connect(function () {
connection.login({
user: 'testuser',
pass: 'testpass'
}, function (err) {
expect(err).to.not.exist;
done();
});
connection.connect(function() {
connection.login(
{
user: 'testuser',
pass: 'testpass'
},
function(err) {
expect(err).to.not.exist;
done();
}
);
});

@@ -1016,4 +1079,4 @@ });

afterEach(function (done) {
connection.on('end', function () {
afterEach(function(done) {
connection.on('end', function() {
server.close(done);

@@ -1024,79 +1087,98 @@ });

it('should send', function (done) {
connection.send({
from: 'sender@example.com',
to: ['recipient@exmaple.com']
}, 'testmessage', function (err, status) {
expect(err).to.not.exist;
expect(status.accepted.length).to.equal(1);
expect(status.rejected.length).to.equal(0);
done();
});
it('should send', function(done) {
connection.send(
{
from: 'sender@example.com',
to: ['recipient@exmaple.com']
},
'testmessage',
function(err, status) {
expect(err).to.not.exist;
expect(status.accepted.length).to.equal(1);
expect(status.rejected.length).to.equal(0);
done();
}
);
});
it('should reject single recipient', function (done) {
connection.send({
from: 'sender@example.com',
to: ['recipient@exmaple.com', 'deny-recipient@example.com']
}, 'testmessage', function (err, status) {
expect(err).to.not.exist;
expect(status.accepted.length).to.equal(1);
expect(status.rejected.length).to.equal(1);
done();
});
it('should reject single recipient', function(done) {
connection.send(
{
from: 'sender@example.com',
to: ['recipient@exmaple.com', 'deny-recipient@example.com']
},
'testmessage',
function(err, status) {
expect(err).to.not.exist;
expect(status.accepted.length).to.equal(1);
expect(status.rejected.length).to.equal(1);
done();
}
);
});
it('should reject sender', function (done) {
connection.send({
from: 'deny-sender@example.com',
to: ['recipient@exmaple.com']
}, 'testmessage', function (err) {
expect(err).to.exist;
done();
});
it('should reject sender', function(done) {
connection.send(
{
from: 'deny-sender@example.com',
to: ['recipient@exmaple.com']
},
'testmessage',
function(err) {
expect(err).to.exist;
done();
}
);
});
it('should reject recipients', function (done) {
connection.send({
from: 'sender@example.com',
to: ['deny-recipient@exmaple.com']
}, 'testmessage', function (err) {
expect(err).to.exist;
done();
});
it('should reject recipients', function(done) {
connection.send(
{
from: 'sender@example.com',
to: ['deny-recipient@exmaple.com']
},
'testmessage',
function(err) {
expect(err).to.exist;
done();
}
);
});
it('should reject message', function (done) {
connection.send({
from: 'sender@example.com',
to: ['recipient@exmaple.com']
}, 'deny-testmessage', function (err) {
expect(err).to.exist;
done();
});
it('should reject message', function(done) {
connection.send(
{
from: 'sender@example.com',
to: ['recipient@exmaple.com']
},
'deny-testmessage',
function(err) {
expect(err).to.exist;
done();
}
);
});
it('should reject too big message', function (done) {
connection.send({
from: 'sender@example.com',
to: ['recipient@exmaple.com']
}, new Array(1000).join('testmessage'), function (err) {
expect(err).to.exist;
done();
});
it('should reject too big message', function(done) {
connection.send(
{
from: 'sender@example.com',
to: ['recipient@exmaple.com']
},
new Array(1000).join('testmessage'),
function(err) {
expect(err).to.exist;
done();
}
);
});
it('should send multiple messages', function (done) {
connection.send({
from: 'sender@example.com',
to: ['recipient@exmaple.com']
}, 'testmessage 1', function (err, status) {
expect(err).to.not.exist;
expect(status.accepted.length).to.equal(1);
expect(status.rejected.length).to.equal(0);
connection.send({
it('should send multiple messages', function(done) {
connection.send(
{
from: 'sender@example.com',
to: ['recipient@exmaple.com']
}, 'testmessage 2', function (err, status) {
},
'testmessage 1',
function(err, status) {
expect(err).to.not.exist;

@@ -1106,25 +1188,46 @@ expect(status.accepted.length).to.equal(1);

connection.send({
from: 'sender@example.com',
to: ['recipient@exmaple.com']
}, 'deny-testmessage', function (err) {
expect(err).to.exist;
connection.send({
connection.send(
{
from: 'sender@example.com',
to: ['recipient@exmaple.com']
}, 'testmessage 3', function (err, status) {
},
'testmessage 2',
function(err, status) {
expect(err).to.not.exist;
expect(status.accepted.length).to.equal(1);
expect(status.rejected.length).to.equal(0);
done();
});
});
});
});
connection.send(
{
from: 'sender@example.com',
to: ['recipient@exmaple.com']
},
'deny-testmessage',
function(err) {
expect(err).to.exist;
connection.send(
{
from: 'sender@example.com',
to: ['recipient@exmaple.com']
},
'testmessage 3',
function(err, status) {
expect(err).to.not.exist;
expect(status.accepted.length).to.equal(1);
expect(status.rejected.length).to.equal(0);
done();
}
);
}
);
}
);
}
);
});
});
describe('SMTPUTF8', function () {
it('should allow addresses with UTF-8 characters', function (done) {
describe('SMTPUTF8', function() {
it('should allow addresses with UTF-8 characters', function(done) {
let utf8Address = 'δοκιμή@παράδειγμα.δοκιμή';

@@ -1140,3 +1243,3 @@ let PORT = 1336;

server.onRcptTo = function (address, session, callback) {
server.onRcptTo = function(address, session, callback) {
expect(utf8Address).to.equal(address.address);

@@ -1146,3 +1249,3 @@ callback();

server.listen(PORT, '127.0.0.1', function () {
server.listen(PORT, '127.0.0.1', function() {
connection = new Client({

@@ -1153,16 +1256,20 @@ port: PORT,

connection.on('end', function () {
connection.on('end', function() {
server.close(done);
});
connection.connect(function () {
connection.send({
from: 'sender@example.com',
to: [utf8Address]
}, 'testmessage', function (err, status) {
expect(err).to.not.exist;
expect(status.accepted.length).to.equal(1);
expect(status.rejected.length).to.equal(0);
connection.quit();
});
connection.connect(function() {
connection.send(
{
from: 'sender@example.com',
to: [utf8Address]
},
'testmessage',
function(err, status) {
expect(err).to.not.exist;
expect(status.accepted.length).to.equal(1);
expect(status.rejected.length).to.equal(0);
connection.quit();
}
);
});

@@ -1173,4 +1280,4 @@ });

describe('#onData', function () {
it('should accept a prematurely called continue callback', function (done) {
describe('#onData', function() {
it('should accept a prematurely called continue callback', function(done) {
let PORT = 1336;

@@ -1185,3 +1292,3 @@

server.onData = function (stream, session, callback) {
server.onData = function(stream, session, callback) {
stream.pipe(fs.createWriteStream('/dev/null'));

@@ -1191,3 +1298,3 @@ callback();

server.listen(PORT, '127.0.0.1', function () {
server.listen(PORT, '127.0.0.1', function() {
connection = new Client({

@@ -1198,14 +1305,18 @@ port: PORT,

connection.on('end', function () {
connection.on('end', function() {
server.close(done);
});
connection.connect(function () {
connection.send({
from: 'sender@example.com',
to: ['receiver@example.com']
}, new Array(1024 * 1024).join('#'), function (err) {
expect(err).to.not.exist;
connection.quit();
});
connection.connect(function() {
connection.send(
{
from: 'sender@example.com',
to: ['receiver@example.com']
},
new Array(1024 * 1024).join('#'),
function(err) {
expect(err).to.not.exist;
connection.quit();
}
);
});

@@ -1216,3 +1327,3 @@ });

describe('PROXY server', function () {
describe('PROXY server', function() {
let PORT = 1336;

@@ -1234,11 +1345,11 @@

beforeEach(function (done) {
beforeEach(function(done) {
server.listen(PORT, '127.0.0.1', done);
});
afterEach(function (done) {
afterEach(function(done) {
server.close(done);
});
it('should rewrite remote address value', function (done) {
it('should rewrite remote address value', function(done) {
let connection = new Client({

@@ -1252,6 +1363,6 @@ port: PORT,

connection.connect(function () {
connection.connect(function() {
let conn;
// get first connection
server.connections.forEach(function (val) {
server.connections.forEach(function(val) {
if (!conn) {

@@ -1270,9 +1381,9 @@ conn = val;

it('should block blacklisted connection', function (done) {
let socket = net.connect(PORT, '127.0.0.1', function () {
it('should block blacklisted connection', function(done) {
let socket = net.connect(PORT, '127.0.0.1', function() {
let buffers = [];
socket.on('data', function (chunk) {
socket.on('data', function(chunk) {
buffers.push(chunk);
});
socket.on('end', function () {
socket.on('end', function() {
let data = Buffer.concat(buffers).toString();

@@ -1288,6 +1399,6 @@ expect(data.indexOf('421 ')).to.equal(0);

describe('onClose handler', function () {
describe('onClose handler', function() {
let PORT = 1336;
it('should detect once a connection is closed', function (done) {
it('should detect once a connection is closed', function(done) {
let closed = 0;

@@ -1306,4 +1417,4 @@ let total = 50;

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

@@ -1315,3 +1426,3 @@ port: PORT,

connection.connect(function () {
connection.connect(function() {
setTimeout(() => connection.quit(), 100);

@@ -1318,0 +1429,0 @@ });

@@ -12,13 +12,9 @@ /* eslint no-unused-expressions:0, prefer-arrow-callback: 0 */

describe('SMTPStream', function () {
it('should emit commands', function (done) {
describe('SMTPStream', function() {
it('should emit commands', function(done) {
let stream = new SMTPStream();
let expecting = [
new Buffer([0x43, 0x4d, 0x44, 0x31]),
new Buffer([0x43, 0x4d, 0x44, 0x32]),
new Buffer([0x43, 0x4d, 0x44, 0x33])
];
let expecting = [new Buffer([0x43, 0x4d, 0x44, 0x31]), new Buffer([0x43, 0x4d, 0x44, 0x32]), new Buffer([0x43, 0x4d, 0x44, 0x33])];
stream.oncommand = function (cmd, cb) {
stream.oncommand = function(cmd, cb) {
expect(cmd).to.deep.equal(expecting.shift());

@@ -35,11 +31,8 @@ if (cb) {

it('should start data stream', function (done) {
it('should start data stream', function(done) {
let stream = new SMTPStream();
let expecting = [
'DATA',
'QUIT'
];
let expecting = ['DATA', 'QUIT'];
stream.oncommand = function (cmd, cb) {
stream.oncommand = function(cmd, cb) {
cmd = cmd.toString();

@@ -52,6 +45,6 @@ expect(cmd).to.deep.equal(expecting.shift());

datastream = stream.startDataMode();
datastream.on('data', function (chunk) {
datastream.on('data', function(chunk) {
output += chunk.toString();
});
datastream.on('end', function () {
datastream.on('end', function() {
expect(output).to.equal('test1\r\n.test2\r\n.test3\r\n');

@@ -58,0 +51,0 @@ stream.continue();

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