simplesmtp
Version:
Simple SMTP server module to create custom SMTP servers
1,145 lines (984 loc) • 34.5 kB
JavaScript
'use strict';
var Stream = require('stream').Stream,
utillib = require('util'),
net = require('net'),
tls = require('tls'),
oslib = require('os'),
xoauth2 = require('xoauth2'),
crypto = require('crypto'),
fs = require('fs');
// expose to the world
module.exports = function(port, host, options) {
var connection = new SMTPClient(port, host, options);
if (typeof setImmediate == 'function') {
setImmediate(connection.connect.bind(connection));
} else {
process.nextTick(connection.connect.bind(connection));
}
return connection;
};
/**
* <p>Generates a SMTP connection object</p>
*
* <p>Optional options object takes the following possible properties:</p>
* <ul>
* <li><b>secureConnection</b> - use SSL</li>
* <li><b>name</b> - the name of the client server</li>
* <li><b>auth</b> - authentication object <code>{user:'...', pass:'...'}</code>
* <li><b>ignoreTLS</b> - ignore server support for STARTTLS</li>
* <li><b>tls</b> - options for createCredentials</li>
* <li><b>debug</b> - output client and server messages to console</li>
* <li><b>logFile</b> - output client and server messages to file</li>
* <li><b>instanceId</b> - unique instance id for debugging</li>
* <li><b>localAddress</b> - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)</li>
* <li><b>greetingTimeout</b> - Time to wait in ms until greeting message is received from the server (defaults to 10000)</li>
* <li><b>socketTimeout</b> - Time of inactivity until the connection is closed (defaults to 1 hour)</li>
* </ul>
*
* @constructor
* @namespace SMTP Client module
* @param {Number} [port=25] Port number to connect to
* @param {String} [host='localhost'] Hostname to connect to
* @param {Object} [options] Option properties
*/
function SMTPClient(port, host, options) {
Stream.call(this);
this.writable = true;
this.readable = true;
this.stage = 'init';
this.options = options || {};
this.port = port || (this.options.secureConnection ? 465 : 25);
this.host = host || 'localhost';
this.options.secureConnection = !! this.options.secureConnection;
this.options.auth = this.options.auth || false;
this.options.maxConnections = this.options.maxConnections || 5;
this.options.enableDotEscaping = this.options.enableDotEscaping || false;
this._closing = false;
if (!this.options.name) {
// defaul hostname is machine hostname or [IP]
var defaultHostname = (oslib.hostname && oslib.hostname()) || '';
if (defaultHostname.indexOf('.') < 0) {
defaultHostname = '[127.0.0.1]';
}
if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
defaultHostname = '[' + defaultHostname + ']';
}
this.options.name = defaultHostname;
}
this._init();
}
utillib.inherits(SMTPClient, Stream);
/**
* <p>Initializes instance variables</p>
*/
SMTPClient.prototype._init = function() {
/**
* Defines if the current connection is secure or not. If not,
* STARTTLS can be used if available
* @private
*/
this._secureMode = false;
/**
* Ignore incoming data on TLS negotiation
* @private
*/
this._ignoreData = false;
/**
* Store incomplete messages coming from the server
* @private
*/
this._remainder = '';
/**
* If set to true, then this object is no longer active
* @private
*/
this.destroyed = false;
/**
* The socket connecting to the server
* @publick
*/
this.socket = false;
/**
* Lists supported auth mechanisms
* @private
*/
this._supportedAuth = [];
/**
* Currently in data transfer state
* @private
*/
this._dataMode = false;
/**
* Keep track if the client sends a leading \r\n in data mode
* @private
*/
this._lastDataBytes = new Buffer(2);
this._lastDataBytes[0] = 0x0D;
this._lastDataBytes[1] = 0x0A;
/**
* Function to run if a data chunk comes from the server
* @private
*/
this._currentAction = false;
/**
* Timeout variable for waiting the greeting
* @private
*/
this._greetingTimeout = false;
/**
* Timeout variable for waiting the connection to start
* @private
*/
this._connectionTimeout = false;
if (this.options.ignoreTLS || this.options.secureConnection) {
this._secureMode = true;
}
/**
* XOAuth2 token generator if XOAUTH2 auth is used
* @private
*/
this._xoauth2 = false;
if (typeof this.options.auth.XOAuth2 == 'object' && typeof this.options.auth.XOAuth2.getToken == 'function') {
this._xoauth2 = this.options.auth.XOAuth2;
} else if (typeof this.options.auth.XOAuth2 == 'object') {
if (!this.options.auth.XOAuth2.user && this.options.auth.user) {
this.options.auth.XOAuth2.user = this.options.auth.user;
}
this._xoauth2 = xoauth2.createXOAuth2Generator(this.options.auth.XOAuth2);
}
};
/**
* <p>Creates a connection to a SMTP server and sets up connection
* listener</p>
*/
SMTPClient.prototype.connect = function() {
var opts = {};
if (this.options.secureConnection) {
if (this.options.tls) {
Object.keys(this.options.tls).forEach((function(key) {
opts[key] = this.options.tls[key];
}).bind(this));
}
if (!('rejectUnauthorized' in opts)) {
opts.rejectUnauthorized = !! this.options.rejectUnauthorized;
}
if (this.options.localAddress) {
opts.localAddress = this.options.localAddress;
}
this.socket = tls.connect(this.port, this.host, opts, this._onConnect.bind(this));
} else {
opts = {
port: this.port,
host: this.host
};
if (this.options.localAddress) {
opts.localAddress = this.options.localAddress;
}
this.socket = net.connect(opts, this._onConnect.bind(this));
}
if (this.options.connectionTimeout) {
this._connectionTimeout = setTimeout((function() {
var error = new Error('Connection timeout');
error.code = 'ETIMEDOUT';
error.errno = 'ETIMEDOUT';
error.stage = this.stage;
this.emit('error', error);
this.close();
}).bind(this), this.options.connectionTimeout);
}
this.socket.on('drain', this._onDrain.bind(this));
this.socket.on('error', this._onError.bind(this));
};
/**
* <p>Upgrades the connection to TLS</p>
*
* @param {Function} callback Callback function to run when the connection
* has been secured
*/
SMTPClient.prototype._upgradeConnection = function(callback) {
this._ignoreData = true;
this.socket.removeAllListeners('data');
this.socket.removeAllListeners('error');
var opts = {
socket: this.socket,
host: this.host,
rejectUnauthorized: !! this.options.rejectUnauthorized
};
Object.keys(this.options.tls || {}).forEach((function(key) {
opts[key] = this.options.tls[key];
}).bind(this));
this.socket = tls.connect(opts, (function() {
this._ignoreData = false;
this._secureMode = true;
this.socket.on('data', this._onData.bind(this));
return callback(null, true);
}).bind(this));
this.socket.on('error', this._onError.bind(this));
};
/**
* <p>Connection listener that is run when the connection to
* the server is opened</p>
*
* @event
*/
SMTPClient.prototype._onConnect = function() {
this.stage = 'connect';
clearTimeout(this._connectionTimeout);
if ('setKeepAlive' in this.socket) {
this.socket.setKeepAlive(true);
}
if ('setNoDelay' in this.socket) {
this.socket.setNoDelay(true);
}
this.socket.on('data', this._onData.bind(this));
this.socket.on('close', this._onClose.bind(this));
this.socket.on('end', this._onEnd.bind(this));
this.socket.setTimeout(this.options.socketTimeout || (3 * 3600 * 1000)); // 1 hours
this.socket.on('timeout', this._onTimeout.bind(this));
this._greetingTimeout = setTimeout((function() {
// if still waiting for greeting, give up
if (this.socket && !this._destroyed && this._currentAction == this._actionGreeting) {
var error = new Error('Greeting never received');
error.code = 'ETIMEDOUT';
error.errno = 'ETIMEDOUT';
error.stage = this.stage;
this.emit('error', error);
this.close();
}
}).bind(this), this.options.greetingTimeout || 10000);
this._currentAction = this._actionGreeting;
};
/**
* <p>Destroys the client - removes listeners etc.</p>
*/
SMTPClient.prototype._destroy = function() {
if (this._destroyed) {
return;
}
this._destroyed = true;
this._ignoreData = true;
this.emit('end');
this.removeAllListeners();
// keep the error handler around, just in case
this.socket.on('error', this._onError.bind(this));
};
/**
* <p>'data' listener for data coming from the server</p>
*
* @event
* @param {Buffer} chunk Data chunk coming from the server
*/
SMTPClient.prototype._onData = function(chunk) {
var str;
if (this._ignoreData || !chunk || !chunk.length) {
return;
}
// Wait until end of line
if (chunk.readUInt8(chunk.length - 1) != 0x0A) {
this._remainder += chunk.toString();
return;
} else {
str = (this._remainder + chunk.toString()).trim();
this._remainder = '';
}
// if this is a multi line reply, wait until the ending
if (str.match(/(?:^|\n)\d{3}-.+$/)) {
this._remainder = str + '\r\n';
return;
}
if (this.options.debug) {
console.log('SERVER' + (this.options.instanceId ? ' ' +
this.options.instanceId : '') + ':\n└──' + str.replace(/\r?\n/g, '\n '));
}
if (this.options.logFile) {
this.log('SERVER' + (this.options.instanceId ? ' ' +
this.options.instanceId : '') + ':\n└──' + str.replace(/\r?\n/g, '\n '));
}
if (typeof this._currentAction == 'function') {
this._currentAction.call(this, str);
}
};
/**
* <p>'error' listener for the socket</p>
*
* @event
* @param {Error} err Error object
* @param {String} type Error name
*/
SMTPClient.prototype._onError = function(err, type, data) {
if (type && type != 'Error') {
err.name = type;
}
if (data) {
err.data = data;
}
err.stage = this.stage;
this.emit('error', err);
this.close();
};
/**
* <p>'drain' listener for the socket</p>
*
* @event
*/
SMTPClient.prototype._onDrain = function() {
this.emit('drain');
};
/**
* <p>'close' listener for the socket</p>
*
* @event
*/
SMTPClient.prototype._onClose = function() {
if ([this._actionGreeting, this._actionIdle, this.close].indexOf(this._currentAction) < 0 && !this._destroyed) {
return this._onError(new Error('Connection closed unexpectedly'));
}
this.stage = 'close';
this._destroy();
};
/**
* <p>'end' listener for the socket</p>
*
* @event
*/
SMTPClient.prototype._onEnd = function() {
this.stage = 'end';
this._destroy();
};
/**
* <p>'timeout' listener for the socket</p>
*
* @event
*/
SMTPClient.prototype._onTimeout = function() {
this.close();
};
/**
* <p>Passes data stream to socket if in data mode</p>
*
* @param {Buffer} chunk Chunk of data to be sent to the server
*/
SMTPClient.prototype.write = function(chunk) {
// works only in data mode
if (!this._dataMode || this._destroyed) {
// this line should never be reached but if it does, then
// say act like everything's normal.
return true;
}
if (typeof chunk == 'string') {
chunk = new Buffer(chunk, 'utf-8');
}
if (!this.options.enableDotEscaping) {
if (chunk.length >= 2) {
this._lastDataBytes[0] = chunk[chunk.length - 2];
this._lastDataBytes[1] = chunk[chunk.length - 1];
} else if (chunk.length == 1) {
this._lastDataBytes[0] = this._lastDataBytes[1];
this._lastDataBytes[1] = chunk[0];
}
} else {
chunk = this._escapeDot(chunk);
}
if (this.options.debug) {
console.log('CLIENT (DATA)' + (this.options.instanceId ? ' ' +
this.options.instanceId : '') + ':\n└──' + chunk.toString().trim().replace(/\n/g, '\n '));
}
if (this.options.logFile) {
this.log('CLIENT (DATA)' + (this.options.instanceId ? ' ' +
this.options.instanceId : '') + ':\n└──' + chunk.toString().trim().replace(/\n/g, '\n '));
}
// pass the chunk to the socket
return this.socket.write(chunk);
};
/**
* <p>Indicates that a data stream for the socket is ended. Works only
* in data mode.</p>
*
* @param {Buffer} [chunk] Chunk of data to be sent to the server
*/
SMTPClient.prototype.end = function(chunk) {
// works only in data mode
if (!this._dataMode || this._destroyed) {
// this line should never be reached but if it does, then
// say act like everything's normal.
return true;
}
if (chunk && chunk.length) {
this.write(chunk);
}
// redirect output from the server to _actionStream
this._currentAction = this._actionStream;
// indicate that the stream has ended by sending a single dot on its own line
// if the client already closed the data with \r\n no need to do it again
if (this._lastDataBytes[0] == 0x0D && this._lastDataBytes[1] == 0x0A) {
this.socket.write(new Buffer('.\r\n', 'utf-8'));
} else if (this._lastDataBytes[1] == 0x0D) {
this.socket.write(new Buffer('\n.\r\n'));
} else {
this.socket.write(new Buffer('\r\n.\r\n'));
}
this._lastDataBytes[0] = 0x0D;
this._lastDataBytes[1] = 0x0A;
// end data mode
this._dataMode = false;
};
/**
* <p>Send a command to the server, append \r\n</p>
*
* @param {String} str String to be sent to the server
*/
SMTPClient.prototype.sendCommand = function(str) {
if (this._destroyed) {
// Connection already closed, can't send any more data
return;
}
if (this.socket.destroyed) {
return this.close();
}
if (this.options.debug) {
console.log('CLIENT' + (this.options.instanceId ? ' ' +
this.options.instanceId : '') + ':\n└──' + (str || '').toString().trim().replace(/\n/g, '\n '));
}
if (this.options.logFile) {
this.log('CLIENT' + (this.options.instanceId ? ' ' +
this.options.instanceId : '') + ':\n└──' + (str || '').toString().trim().replace(/\n/g, '\n '));
}
this.socket.write(new Buffer(str + '\r\n', 'utf-8'));
};
/**
* <p>Sends QUIT</p>
*/
SMTPClient.prototype.quit = function() {
this._closing = true;
this.sendCommand('QUIT');
this._currentAction = this.close;
};
/**
* <p>Closes the connection to the server</p>
*/
SMTPClient.prototype.close = function() {
this._closing = true;
if (this.options.debug) {
console.log('Closing connection to the server');
}
if (this.options.logFile) {
this.log('Closing connection to the server');
}
var closeMethod = 'end';
// Clear current job
this._currentAction = this._actionIdle;
if (this.stage === 'init') {
// Clear connection timeout timer if other than timeout error occurred
clearTimeout(this._connectionTimeout);
// Close the socket immediately when connection timed out
closeMethod = 'destroy';
}
if (this.socket && this.socket.socket && this.socket.socket[closeMethod] && !this.socket.socket.destroyed) {
this.socket.socket[closeMethod]();
}
if (this.socket && this.socket[closeMethod] && !this.socket.destroyed) {
this.socket[closeMethod]();
}
this._destroy();
};
/**
* <p>Initiates a new message by submitting envelope data, starting with
* <code>MAIL FROM:</code> command</p>
*
* @param {Object} envelope Envelope object in the form of
* <code>{from:'...', to:['...']}</code>
* or
* <code>{from:{address:'...',name:'...'}, to:[address:'...',name:'...']}</code>
*/
SMTPClient.prototype.useEnvelope = function(envelope) {
this._envelope = envelope || {};
this._envelope.from = this._envelope.from && this._envelope.from.address || this._envelope.from || ('anonymous@' + this.options.name);
this._envelope.to = [].concat(this._envelope.to || []).map(function(to) {
return to && to.address || to;
});
// clone the recipients array for latter manipulation
this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || []));
this._envelope.rcptFailed = [];
this._currentAction = this._actionMAIL;
this.sendCommand('MAIL FROM:<' + (this._envelope.from) + '>');
};
/**
* <p>If needed starts the authentication, if not emits 'idle' to
* indicate that this client is ready to take in an outgoing mail</p>
*/
SMTPClient.prototype._authenticateUser = function() {
this.stage = 'auth';
if (!this.options.auth) {
// no need to authenticate, at least no data given
this._enterIdle();
return;
}
var auth;
if (this.options.auth.XOAuthToken && this._supportedAuth.indexOf('XOAUTH') >= 0) {
auth = 'XOAUTH';
} else if (this._xoauth2 && this._supportedAuth.indexOf('XOAUTH2') >= 0) {
auth = 'XOAUTH2';
} else if (this.options.authMethod) {
auth = this.options.authMethod.toUpperCase().trim();
} else {
// use first supported
auth = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim();
}
switch (auth) {
case 'XOAUTH':
this._currentAction = this._actionAUTHComplete;
if (typeof this.options.auth.XOAuthToken == 'object' &&
typeof this.options.auth.XOAuthToken.generate == 'function') {
this.options.auth.XOAuthToken.generate((function(err, XOAuthToken) {
if (this._destroyed) {
// Nothing to do here anymore, connection already closed
return;
}
if (err) {
return this._onError(err, 'XOAuthTokenError');
}
this.sendCommand('AUTH XOAUTH ' + XOAuthToken);
}).bind(this));
} else {
this.sendCommand('AUTH XOAUTH ' + this.options.auth.XOAuthToken.toString());
}
return;
case 'XOAUTH2':
this._currentAction = this._actionAUTHComplete;
this._xoauth2.getToken((function(err, token) {
if (this._destroyed) {
// Nothing to do here anymore, connection already closed
return;
}
if (err) {
this._onError(err, 'XOAUTH2Error');
return;
}
this.sendCommand('AUTH XOAUTH2 ' + token);
}).bind(this));
return;
case 'LOGIN':
this._currentAction = this._actionAUTH_LOGIN_USER;
this.sendCommand('AUTH LOGIN');
return;
case 'PLAIN':
this._currentAction = this._actionAUTHComplete;
this.sendCommand('AUTH PLAIN ' + new Buffer(
//this.options.auth.user+'\u0000'+
'\u0000' + // skip authorization identity as it causes problems with some servers
this.options.auth.user + '\u0000' +
this.options.auth.pass, 'utf-8').toString('base64'));
return;
case 'CRAM-MD5':
this._currentAction = this._actionAUTH_CRAM_MD5;
this.sendCommand('AUTH CRAM-MD5');
return;
}
this._onError(new Error('Unknown authentication method - ' + auth), 'UnknowAuthError');
};
/** ACTIONS **/
/**
* <p>Will be run after the connection is created and the server sends
* a greeting. If the incoming message starts with 220 initiate
* SMTP session by sending EHLO command</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionGreeting = function(str) {
this.stage = 'greeting';
clearTimeout(this._greetingTimeout);
if (str.substr(0, 3) != '220') {
this._onError(new Error('Invalid greeting from server - ' + str), false, str);
return;
}
this._currentAction = this._actionEHLO;
this.sendCommand('EHLO ' + this.options.name);
};
/**
* <p>Handles server response for EHLO command. If it yielded in
* error, try HELO instead, otherwise initiate TLS negotiation
* if STARTTLS is supported by the server or move into the
* authentication phase.</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionEHLO = function(str) {
this.stage = 'ehlo';
if (str.substr(0, 3) == '421') {
this._onError(new Error('Server terminates connection - ' + str), false, str);
return;
}
if (str.charAt(0) != '2') {
// Try HELO instead
this._currentAction = this._actionHELO;
this.sendCommand('HELO ' + this.options.name);
return;
}
// Detect if the server supports STARTTLS
if (!this._secureMode && str.match(/[ \-]STARTTLS\r?$/mi)) {
this.sendCommand('STARTTLS');
this._currentAction = this._actionSTARTTLS;
return;
}
// Detect if the server supports PLAIN auth
if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i)) {
this._supportedAuth.push('PLAIN');
}
// Detect if the server supports LOGIN auth
if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i)) {
this._supportedAuth.push('LOGIN');
}
// Detect if the server supports CRAM-MD5 auth
if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i)) {
this._supportedAuth.push('CRAM-MD5');
}
// Detect if the server supports XOAUTH auth
if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH/i)) {
this._supportedAuth.push('XOAUTH');
}
// Detect if the server supports XOAUTH2 auth
if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i)) {
this._supportedAuth.push('XOAUTH2');
}
this._authenticateUser.call(this);
};
/**
* <p>Handles server response for HELO command. If it yielded in
* error, emit 'error', otherwise move into the authentication phase.</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionHELO = function(str) {
this.stage = 'helo';
if (str.charAt(0) != '2') {
this._onError(new Error('Invalid response for EHLO/HELO - ' + str), false, str);
return;
}
this._authenticateUser.call(this);
};
/**
* <p>Handles server response for STARTTLS command. If there's an error
* try HELO instead, otherwise initiate TLS upgrade. If the upgrade
* succeedes restart the EHLO</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionSTARTTLS = function(str) {
this.stage = 'starttls';
if (str.charAt(0) != '2') {
// Try HELO instead
this._currentAction = this._actionHELO;
this.sendCommand('HELO ' + this.options.name);
return;
}
this._upgradeConnection((function(err, secured) {
if (err) {
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'TLSError');
return;
}
if (this.options.debug) {
console.log('Connection secured');
}
if (this.options.logFile) {
this.log('Connection secured');
}
if (secured) {
// restart session
this._currentAction = this._actionEHLO;
this.sendCommand('EHLO ' + this.options.name);
} else {
this._authenticateUser.call(this);
}
}).bind(this));
};
/**
* <p>Handle the response for AUTH LOGIN command. We are expecting
* '334 VXNlcm5hbWU6' (base64 for 'Username:'). Data to be sent as
* response needs to be base64 encoded username.</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionAUTH_LOGIN_USER = function(str) {
if (str != '334 VXNlcm5hbWU6') {
this._onError(new Error('Invalid login sequence while waiting for "334 VXNlcm5hbWU6" - ' + str), false, str);
return;
}
this._currentAction = this._actionAUTH_LOGIN_PASS;
this.sendCommand(new Buffer(
this.options.auth.user + '', 'utf-8').toString('base64'));
};
/**
* <p>Handle the response for AUTH CRAM-MD5 command. We are expecting
* '334 <challenge string>'. Data to be sent as response needs to be
* base64 decoded challenge string, MD5 hashed using the password as
* a HMAC key, prefixed by the username and a space, and finally all
* base64 encoded again.</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionAUTH_CRAM_MD5 = function(str) {
var challengeMatch = str.match(/^334\s+(.+)$/),
challengeString = '';
if (!challengeMatch) {
this._onError(new Error('Invalid login sequence while waiting for server challenge string - ' + str), false, str);
return;
} else {
challengeString = challengeMatch[1];
}
// Decode from base64
var base64decoded = new Buffer(challengeString, 'base64').toString('ascii'),
hmac_md5 = crypto.createHmac('md5', this.options.auth.pass);
hmac_md5.update(base64decoded);
var hex_hmac = hmac_md5.digest('hex'),
prepended = this.options.auth.user + ' ' + hex_hmac;
this._currentAction = this._actionAUTH_CRAM_MD5_PASS;
this.sendCommand(new Buffer(prepended).toString('base64'));
};
/**
* <p>Handles the response to CRAM-MD5 authentication, if there's no error,
* the user can be considered logged in. Emit 'idle' and start
* waiting for a message to send</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionAUTH_CRAM_MD5_PASS = function(str) {
if (!str.match(/^235\s+/)) {
this._onError(new Error('Invalid login sequence while waiting for "235 go ahead" - ' + str), false, str);
return;
}
this._enterIdle();
};
/**
* <p>Handle the response for AUTH LOGIN command. We are expecting
* '334 UGFzc3dvcmQ6' (base64 for 'Password:'). Data to be sent as
* response needs to be base64 encoded password.</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionAUTH_LOGIN_PASS = function(str) {
if (str != '334 UGFzc3dvcmQ6') {
this._onError(new Error('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6" - ' + str), false, str);
return;
}
this._currentAction = this._actionAUTHComplete;
this.sendCommand(new Buffer(this.options.auth.pass + '', 'utf-8').toString('base64'));
};
/**
* <p>Handles the response for authentication, if there's no error,
* the user can be considered logged in. Emit 'idle' and start
* waiting for a message to send</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionAUTHComplete = function(str) {
var response;
if (this._xoauth2 && str.substr(0, 3) == '334') {
try {
response = str.split(' ');
response.shift();
response = JSON.parse(new Buffer(response.join(' '), 'base64').toString('utf-8'));
if ((!this._xoauth2.reconnectCount || this._xoauth2.reconnectCount < 200) && ['400', '401'].indexOf(response.status) >= 0) {
this._xoauth2.reconnectCount = (this._xoauth2.reconnectCount || 0) + 1;
this._currentAction = this._actionXOAUTHRetry;
} else {
this._xoauth2.reconnectCount = 0;
this._currentAction = this._actionAUTHComplete;
}
this.sendCommand(new Buffer(0));
return;
} catch (E) {}
}
if(this._xoauth2){
this._xoauth2.reconnectCount = 0;
}
if (str.charAt(0) != '2') {
this._onError(new Error('Invalid login - ' + str), 'AuthError', str);
return;
}
this._enterIdle();
};
/**
* If XOAUTH2 authentication failed, try again by generating
* new access token
*/
SMTPClient.prototype._actionXOAUTHRetry = function() {
// ensure that something is listening unexpected responses
this._currentAction = this._actionIdle;
this._xoauth2.generateToken((function(err, token) {
if (this._destroyed) {
// Nothing to do here anymore, connection already closed
return;
}
if (err) {
this._onError(err, 'XOAUTH2Error');
return;
}
this._currentAction = this._actionAUTHComplete;
this.sendCommand('AUTH XOAUTH2 ' + token);
}).bind(this));
};
/**
* <p>This function is not expected to run. If it does then there's probably
* an error (timeout etc.)</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionIdle = function(str) {
this.stage = 'idle';
if (Number(str.charAt(0)) > 3) {
this._onError(new Error(str), false, str);
return;
}
// this line should never get called
};
/**
* <p>Handle response for a <code>MAIL FROM:</code> command</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionMAIL = function(str) {
this.stage = 'mail';
if (Number(str.charAt(0)) != '2') {
this._onError(new Error('Mail from command failed - ' + str), 'SenderError', str);
return;
}
if (!this._envelope.rcptQueue.length) {
this._onError(new Error('Can\'t send mail - no recipients defined'), 'RecipientError', str);
} else {
this._envelope.curRecipient = this._envelope.rcptQueue.shift();
this._currentAction = this._actionRCPT;
this.sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>' + this._getDSN());
}
};
/**
* Emits 'idle'
*/
SMTPClient.prototype._enterIdle = function() {
this._currentAction = this._actionIdle;
this.emit('idle'); // ready to take orders
};
/**
* <p>SetsUp DSN</p>
*/
SMTPClient.prototype._getDSN = function() {
var ret = '',
n = [],
dsn;
if (this.currentMessage && this.currentMessage.options && 'dsn' in this.currentMessage.options) {
dsn = this.currentMessage.options.dsn;
if (dsn.success) {
n.push('SUCCESS');
}
if (dsn.failure) {
n.push('FAILURE');
}
if (dsn.delay) {
n.push('DELAY');
}
if (n.length > 0) {
ret = ' NOTIFY=' + n.join(',') + ' ORCPT=rfc822;' + this.currentMessage._message.from;
}
}
return ret;
};
/**
* <p>Handle response for a <code>RCPT TO:</code> command</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionRCPT = function(str) {
this.stage = 'rcpt';
if (Number(str.charAt(0)) != '2') {
// this is a soft error
this._envelope.rcptFailed.push(this._envelope.curRecipient);
}
if (!this._envelope.rcptQueue.length) {
if (this._envelope.rcptFailed.length < this._envelope.to.length) {
if (this._envelope.rcptFailed.length) {
this.emit('rcptFailed', this._envelope.rcptFailed);
}
this._currentAction = this._actionDATA;
this.sendCommand('DATA');
} else {
this._onError(new Error('Can\'t send mail - all recipients were rejected'), 'RecipientError', str);
return;
}
} else {
this._envelope.curRecipient = this._envelope.rcptQueue.shift();
this._currentAction = this._actionRCPT;
this.sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>');
}
};
/**
* <p>Handle response for a <code>DATA</code> command</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionDATA = function(str) {
this.stage = 'data';
// response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
// some servers might use 250 instead, so lets check for 2 or 3 as the first digit
if ([2, 3].indexOf(Number(str.charAt(0))) < 0) {
this._onError(new Error('Data command failed - ' + str), false, str);
return;
}
// Emit that connection is set up for streaming
this._dataMode = true;
this._currentAction = this._actionIdle;
this.emit('message');
};
/**
* <p>Handle response for a <code>DATA</code> stream</p>
*
* @param {String} str Message from the server
*/
SMTPClient.prototype._actionStream = function(str) {
if (Number(str.charAt(0)) != '2') {
// Message failed
this.emit('ready', false, str);
} else {
// Message sent succesfully
this.emit('ready', true, str);
}
// Waiting for new connections
this._currentAction = this._actionIdle;
if (typeof setImmediate == 'function') {
setImmediate(this._enterIdle.bind(this));
} else {
process.nextTick(this._enterIdle.bind(this));
}
};
/**
* <p>Log debugs to given file</p>
*
* @param {String} str Log message
*/
SMTPClient.prototype.log = function(str) {
fs.appendFile(this.options.logFile, str + '\n', function(err) {
if (err) {
console.log('Log write failed. Data to log: ' + str);
}
});
};
/**
* <p>Inserts an extra dot at the begining of a line if it starts with a dot
* See RFC 2821 Section 4.5.2</p>
*
* @param {Buffer} chunk The chunk that will be send.
*/
SMTPClient.prototype._escapeDot = function(chunk) {
var pos, OutBuff, i;
OutBuff = new Buffer(chunk.length * 2);
pos = 0;
for (i = 0; i < chunk.length; i++) {
if (this._lastDataBytes[0] == 0x0D && this._lastDataBytes[1] == 0x0A && chunk[i] == 0x2E) {
OutBuff[pos] = 0x2E;
pos += 1;
}
OutBuff[pos] = chunk[i];
pos += 1;
this._lastDataBytes[0] = this._lastDataBytes[1];
this._lastDataBytes[1] = chunk[i];
}
return OutBuff.slice(0, pos);
};