smtp-server
Advanced tools
Comparing version 1.17.0 to 2.0.0
# 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 @@ |
@@ -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); |
172
lib/sasl.js
'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
SPDX disjunction
LicenseSPDX disjunction for an artifact's license information
Found 1 instance in 1 package
Non-permissive License
License(Experimental) A license not known to be considered permissive was found
Found 1 instance in 1 package
SPDX disjunction
LicenseSPDX disjunction for an artifact's license information
Found 1 instance in 1 package
Mixed license
License(Experimental) Package contains multiple licenses.
Found 1 instance in 1 package
168604
3160
90
475
+ Addednodemailer@^3.0.2
+ Addednodemailer@3.1.8(transitive)
- Removednodemailer-shared@^1.1.0
- Removednodemailer-fetch@1.6.0(transitive)
- Removednodemailer-shared@1.1.0(transitive)