mirror of
https://github.com/mgerb/mywebsite
synced 2026-01-11 18:32:50 +00:00
1020 lines
30 KiB
JavaScript
1020 lines
30 KiB
JavaScript
'use strict';
|
|
|
|
var packageInfo = require('../package.json');
|
|
var EventEmitter = require('events').EventEmitter;
|
|
var utillib = require('util');
|
|
var net = require('net');
|
|
var tls = require('tls');
|
|
var os = require('os');
|
|
var crypto = require('crypto');
|
|
var DataStream = require('./data-stream');
|
|
|
|
module.exports = SMTPConnection;
|
|
|
|
/**
|
|
* Generates a SMTP connection object
|
|
*
|
|
* Optional options object takes the following possible properties:
|
|
*
|
|
* * **port** - is the port to connect to (defaults to 25 or 465)
|
|
* * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
|
|
* * **secure** - use SSL
|
|
* * **ignoreTLS** - ignore server support for STARTTLS
|
|
* * **requireTLS** - forces the client to use STARTTLS
|
|
* * **name** - the name of the client server
|
|
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
|
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 10000)
|
|
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish
|
|
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 1 hour)
|
|
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
|
* * **debug** - if true, emits 'log' events with all traffic between client and server
|
|
* * **tls** - options for createCredentials
|
|
* * **socket** - existing socket to use instead of creating a new one (see: http://nodejs.org/api/net.html#net_class_net_socket)
|
|
*
|
|
* @constructor
|
|
* @namespace SMTP Client module
|
|
* @param {Object} [options] Option properties
|
|
*/
|
|
function SMTPConnection(options) {
|
|
EventEmitter.call(this);
|
|
|
|
this.stage = 'init';
|
|
|
|
this.options = options || {};
|
|
|
|
this.options.port = this.options.port || (this.options.secure ? 465 : 25);
|
|
this.options.host = this.options.host || 'localhost';
|
|
this.options.secure = !!this.options.secure;
|
|
|
|
this.options.name = this.options.name || this._getHostname();
|
|
|
|
/**
|
|
* Expose version nr, just for the reference
|
|
* @type {String}
|
|
*/
|
|
this.version = packageInfo.version;
|
|
|
|
/**
|
|
* If true, then the user is authenticated
|
|
* @type {Boolean}
|
|
*/
|
|
this.authenticated = false;
|
|
|
|
/**
|
|
* If set to true, this instance is no longer active
|
|
* @private
|
|
*/
|
|
this.destroyed = false;
|
|
|
|
/**
|
|
* Defines if the current connection is secure or not. If not,
|
|
* STARTTLS can be used if available
|
|
* @private
|
|
*/
|
|
this.secure = false;
|
|
|
|
/**
|
|
* Store incomplete messages coming from the server
|
|
* @private
|
|
*/
|
|
this._remainder = '';
|
|
|
|
/**
|
|
* Unprocessed responses from the server
|
|
* @type {Array}
|
|
*/
|
|
this._responseQueue = [];
|
|
|
|
/**
|
|
* The socket connecting to the server
|
|
* @publick
|
|
*/
|
|
this._socket = false;
|
|
|
|
/**
|
|
* Lists supported auth mechanisms
|
|
* @private
|
|
*/
|
|
this._supportedAuth = [];
|
|
|
|
/**
|
|
* 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 the socket is deemed already closed
|
|
* @private
|
|
*/
|
|
this._destroyed = false;
|
|
|
|
if (this.options.secure) {
|
|
this.secure = true;
|
|
}
|
|
}
|
|
utillib.inherits(SMTPConnection, EventEmitter);
|
|
|
|
/**
|
|
* Creates a connection to a SMTP server and sets up connection
|
|
* listener
|
|
*/
|
|
SMTPConnection.prototype.connect = function (connectCallback) {
|
|
if (typeof connectCallback === 'function') {
|
|
this.once('connect', connectCallback);
|
|
}
|
|
|
|
var opts = {
|
|
port: this.options.port,
|
|
host: this.options.host
|
|
};
|
|
|
|
if (this.options.localAddress) {
|
|
opts.localAddress = this.options.localAddress;
|
|
}
|
|
|
|
if (this.options.socket) {
|
|
this._socket = this.options.socket;
|
|
this._socket.connect(this.options.port, this.options.host, this._onConnect.bind(this));
|
|
} else if (this.options.secure) {
|
|
if (this.options.tls) {
|
|
Object.keys(this.options.tls).forEach((function (key) {
|
|
opts[key] = this.options.tls[key];
|
|
}).bind(this));
|
|
}
|
|
this._socket = tls.connect(this.options.port, this.options.host, opts, this._onConnect.bind(this));
|
|
} else {
|
|
this._socket = net.connect(opts, this._onConnect.bind(this));
|
|
}
|
|
|
|
this._connectionTimeout = setTimeout((function () {
|
|
this._onError('Connection timeout', 'ETIMEDOUT');
|
|
}).bind(this), this.options.connectionTimeout || 60 * 1000);
|
|
|
|
this._socket.on('error', this._onError.bind(this));
|
|
};
|
|
|
|
/**
|
|
* Sends QUIT
|
|
*/
|
|
SMTPConnection.prototype.quit = function () {
|
|
this._sendCommand('QUIT');
|
|
this._currentAction = this.close;
|
|
};
|
|
|
|
/**
|
|
* Closes the connection to the server
|
|
*/
|
|
SMTPConnection.prototype.close = function () {
|
|
clearTimeout(this._connectionTimeout);
|
|
clearTimeout(this._greetingTimeout);
|
|
|
|
if (this.options.debug) {
|
|
this.emit('log', {
|
|
type: 'socket',
|
|
message: 'Closing connection to the server'
|
|
});
|
|
}
|
|
|
|
var closeMethod = 'end';
|
|
|
|
if (this.stage === 'init') {
|
|
// Close the socket immediately when connection timed out
|
|
closeMethod = 'destroy';
|
|
}
|
|
|
|
var socket = this._socket && this._socket.socket || this._socket;
|
|
|
|
if (socket && !socket.destroyed) {
|
|
try {
|
|
this._socket[closeMethod]();
|
|
} catch (E) {
|
|
// just ignore
|
|
}
|
|
}
|
|
|
|
this._destroy();
|
|
};
|
|
|
|
/**
|
|
* Authenticate user
|
|
*/
|
|
SMTPConnection.prototype.login = function (authData, callback) {
|
|
this._auth = authData || {};
|
|
|
|
var authMethod;
|
|
if (this._auth.xoauth2 && this._supportedAuth.indexOf('XOAUTH2') >= 0) {
|
|
authMethod = 'XOAUTH2';
|
|
} else if (this.options.authMethod) {
|
|
authMethod = this.options.authMethod.toUpperCase().trim();
|
|
} else {
|
|
// use first supported
|
|
authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim();
|
|
}
|
|
|
|
switch (authMethod) {
|
|
case 'XOAUTH2':
|
|
this._handleXOauth2Token(false, callback);
|
|
return;
|
|
case 'LOGIN':
|
|
this._currentAction = function (str) {
|
|
this._actionAUTH_LOGIN_USER(str, callback);
|
|
}.bind(this);
|
|
this._sendCommand('AUTH LOGIN');
|
|
return;
|
|
case 'PLAIN':
|
|
this._currentAction = function (str) {
|
|
this._actionAUTHComplete(str, callback);
|
|
}.bind(this);
|
|
this._sendCommand('AUTH PLAIN ' + new Buffer(
|
|
//this._auth.user+'\u0000'+
|
|
'\u0000' + // skip authorization identity as it causes problems with some servers
|
|
this._auth.user + '\u0000' +
|
|
this._auth.pass, 'utf-8').toString('base64'));
|
|
return;
|
|
case 'CRAM-MD5':
|
|
this._currentAction = function (str) {
|
|
this._actionAUTH_CRAM_MD5(str, callback);
|
|
}.bind(this);
|
|
this._sendCommand('AUTH CRAM-MD5');
|
|
return;
|
|
}
|
|
|
|
return callback(this._formatError('Unknown authentication method "' + authMethod + '"', 'EAUTH'));
|
|
};
|
|
|
|
/**
|
|
* Sends a message
|
|
*
|
|
* @param {Object} envelope Envelope object, {from: addr, to: [addr]}
|
|
* @param {Object} message String, Buffer or a Stream
|
|
* @param {Function} callback Callback to return once sending is completed
|
|
*/
|
|
SMTPConnection.prototype.send = function (envelope, message, callback) {
|
|
if (!message) {
|
|
return callback(this._formatError('Empty message', 'EMESSAGE'));
|
|
}
|
|
|
|
this._setEnvelope(envelope, function (err, info) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
var stream = this._createSendStream(function (err, str) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
info.response = str;
|
|
return callback(null, info);
|
|
});
|
|
if (typeof message.pipe === 'function') {
|
|
message.pipe(stream);
|
|
} else {
|
|
stream.write(message);
|
|
stream.end();
|
|
}
|
|
|
|
}.bind(this));
|
|
};
|
|
|
|
/**
|
|
* Connection listener that is run when the connection to
|
|
* the server is opened
|
|
*
|
|
* @event
|
|
*/
|
|
SMTPConnection.prototype._onConnect = function () {
|
|
clearTimeout(this._connectionTimeout);
|
|
|
|
if (this._destroyed) {
|
|
// Connection was established after we already had canceled it
|
|
this.close();
|
|
return;
|
|
}
|
|
|
|
this.stage = 'connected';
|
|
|
|
this._socket.on('data', this._onData.bind(this));
|
|
this._socket.once('close', this._onClose.bind(this));
|
|
this._socket.once('end', this._onEnd.bind(this));
|
|
|
|
this._socket.setTimeout(this.options.socketTimeout || (10 * 60 * 1000)); // 10 min.
|
|
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) {
|
|
this._onError('Greeting never received', 'ETIMEDOUT');
|
|
}
|
|
}).bind(this), this.options.greetingTimeout || 10000);
|
|
|
|
this._currentAction = this._actionGreeting;
|
|
};
|
|
|
|
/**
|
|
* 'data' listener for data coming from the server
|
|
*
|
|
* @event
|
|
* @param {Buffer} chunk Data chunk coming from the server
|
|
*/
|
|
SMTPConnection.prototype._onData = function (chunk) {
|
|
if (this._destroyed || !chunk || !chunk.length) {
|
|
return;
|
|
}
|
|
|
|
var data = (chunk || '').toString('binary');
|
|
var lines = (this._remainder + data).split(/\r?\n/);
|
|
var lastline;
|
|
|
|
this._remainder = lines.pop();
|
|
|
|
for (var i = 0, len = lines.length; i < len; i++) {
|
|
if (this._responseQueue.length) {
|
|
lastline = this._responseQueue[this._responseQueue.length - 1];
|
|
if (/^\d+\-/.test(lastline.split('\n').pop())) {
|
|
this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
|
|
continue;
|
|
}
|
|
}
|
|
this._responseQueue.push(lines[i]);
|
|
}
|
|
|
|
this._processResponse();
|
|
};
|
|
|
|
/**
|
|
* 'error' listener for the socket
|
|
*
|
|
* @event
|
|
* @param {Error} err Error object
|
|
* @param {String} type Error name
|
|
*/
|
|
SMTPConnection.prototype._onError = function (err, type, data) {
|
|
clearTimeout(this._connectionTimeout);
|
|
clearTimeout(this._greetingTimeout);
|
|
|
|
if (this._destroyed) {
|
|
// just ignore, already closed
|
|
// this might happen when a socket is canceled because of reached timeout
|
|
// but the socket timeout error itself receives only after
|
|
return;
|
|
}
|
|
|
|
this.emit('error', this._formatError(err, type, data));
|
|
this.close();
|
|
};
|
|
|
|
SMTPConnection.prototype._formatError = function (message, type, response) {
|
|
var err;
|
|
|
|
if (/Error\]$/i.test(Object.prototype.toString.call(message))) {
|
|
err = message;
|
|
} else {
|
|
err = new Error(message);
|
|
}
|
|
|
|
if (type && type !== 'Error') {
|
|
err.code = type;
|
|
}
|
|
|
|
if (response) {
|
|
err.response = response;
|
|
err.message += ': ' + response;
|
|
}
|
|
|
|
var responseCode = typeof response === 'string' && Number((response.match(/^\d+/) || [])[0]) || false;
|
|
if (responseCode) {
|
|
err.responseCode = responseCode;
|
|
}
|
|
|
|
return err;
|
|
};
|
|
|
|
/**
|
|
* 'close' listener for the socket
|
|
*
|
|
* @event
|
|
*/
|
|
SMTPConnection.prototype._onClose = function () {
|
|
if ([this._actionGreeting, this.close].indexOf(this._currentAction) < 0 && !this._destroyed) {
|
|
return this._onError(new Error('Connection closed unexpectedly'));
|
|
}
|
|
|
|
this._destroy();
|
|
};
|
|
|
|
/**
|
|
* 'end' listener for the socket
|
|
*
|
|
* @event
|
|
*/
|
|
SMTPConnection.prototype._onEnd = function () {
|
|
this._destroy();
|
|
};
|
|
|
|
/**
|
|
* 'timeout' listener for the socket
|
|
*
|
|
* @event
|
|
*/
|
|
SMTPConnection.prototype._onTimeout = function () {
|
|
return this._onError(new Error('Timeout'), 'ETIMEOUT');
|
|
};
|
|
|
|
/**
|
|
* Destroys the client, emits 'end'
|
|
*/
|
|
SMTPConnection.prototype._destroy = function () {
|
|
if (this._destroyed) {
|
|
return;
|
|
}
|
|
this._destroyed = true;
|
|
this.emit('end');
|
|
};
|
|
|
|
/**
|
|
* Upgrades the connection to TLS
|
|
*
|
|
* @param {Function} callback Callback function to run when the connection
|
|
* has been secured
|
|
*/
|
|
SMTPConnection.prototype._upgradeConnection = function (callback) {
|
|
// do not remove all listeners or it breaks node v0.10 as there's
|
|
// apparently a 'finish' event set that would be cleared as well
|
|
|
|
// we can safely keep 'error', 'end', 'close' etc. events
|
|
this._socket.removeAllListeners('data'); // incoming data is going to be gibberish from this point onwards
|
|
this._socket.removeAllListeners('timeout'); // timeout will be re-set for the new socket object
|
|
|
|
var opts = {
|
|
socket: this._socket,
|
|
host: this.options.host
|
|
};
|
|
|
|
Object.keys(this.options.tls || {}).forEach((function (key) {
|
|
opts[key] = this.options.tls[key];
|
|
}).bind(this));
|
|
|
|
this._socket = tls.connect(opts, function () {
|
|
this.secure = true;
|
|
this._socket.on('data', this._onData.bind(this));
|
|
|
|
return callback(null, true);
|
|
}.bind(this));
|
|
|
|
this._socket.on('error', this._onError.bind(this));
|
|
this._socket.once('close', this._onClose.bind(this));
|
|
this._socket.once('end', this._onEnd.bind(this));
|
|
|
|
this._socket.setTimeout(this.options.socketTimeout || (10 * 60 * 1000)); // 10 min.
|
|
this._socket.on('timeout', this._onTimeout.bind(this));
|
|
};
|
|
|
|
/**
|
|
* Processes queued responses from the server
|
|
*
|
|
* @param {Boolean} force If true, ignores _processing flag
|
|
*/
|
|
SMTPConnection.prototype._processResponse = function () {
|
|
if (!this._responseQueue.length) {
|
|
return false;
|
|
}
|
|
|
|
var str = (this._responseQueue.shift() || '').toString();
|
|
|
|
if (/^\d+\-/.test(str.split('\n').pop())) {
|
|
// keep waiting for the final part of multiline response
|
|
return;
|
|
}
|
|
|
|
if (this.options.debug) {
|
|
this.emit('log', {
|
|
type: 'server',
|
|
message: str.trim()
|
|
});
|
|
}
|
|
|
|
if (!str.trim()) { // skip unexpected empty lines
|
|
setImmediate(this._processResponse.bind(this, true));
|
|
}
|
|
|
|
var action = this._currentAction;
|
|
this._currentAction = null;
|
|
|
|
if (typeof action === 'function') {
|
|
action.call(this, str);
|
|
setImmediate(this._processResponse.bind(this, true));
|
|
} else {
|
|
return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Send a command to the server, append \r\n
|
|
*
|
|
* @param {String} str String to be sent to the server
|
|
*/
|
|
SMTPConnection.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) {
|
|
this.emit('log', {
|
|
type: 'client',
|
|
message: (str || '').toString().trim()
|
|
});
|
|
}
|
|
|
|
this._socket.write(new Buffer(str + '\r\n', 'utf-8'));
|
|
};
|
|
|
|
/**
|
|
* Initiates a new message by submitting envelope data, starting with
|
|
* MAIL FROM: command
|
|
*
|
|
* @param {Object} envelope Envelope object in the form of
|
|
* {from:'...', to:['...']}
|
|
* or
|
|
* {from:{address:'...',name:'...'}, to:[address:'...',name:'...']}
|
|
*/
|
|
SMTPConnection.prototype._setEnvelope = function (envelope, callback) {
|
|
this._envelope = envelope || {};
|
|
this._envelope.from = (this._envelope.from && this._envelope.from.address || this._envelope.from || '').toString().trim();
|
|
|
|
this._envelope.to = [].concat(this._envelope.to || []).map(function (to) {
|
|
return (to && to.address || to || '').toString().trim();
|
|
});
|
|
|
|
if (!this._envelope.to.length) {
|
|
return callback(this._formatError('No recipients defined', 'EENVELOPE'));
|
|
}
|
|
|
|
if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) {
|
|
return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE'));
|
|
}
|
|
|
|
for (var i = 0, len = this._envelope.to.length; i < len; i++) {
|
|
if (!this._envelope.to[i] || /[\r\n<>]/.test(this._envelope.to[i])) {
|
|
return callback(this._formatError('Invalid recipient ' + JSON.stringify(this._envelope.to[i]), 'EENVELOPE'));
|
|
}
|
|
}
|
|
|
|
// clone the recipients array for latter manipulation
|
|
this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || []));
|
|
this._envelope.rejected = [];
|
|
this._envelope.accepted = [];
|
|
|
|
this._currentAction = function (str) {
|
|
this._actionMAIL(str, callback);
|
|
}.bind(this);
|
|
this._sendCommand('MAIL FROM:<' + (this._envelope.from) + '>');
|
|
};
|
|
|
|
SMTPConnection.prototype._createSendStream = function (callback) {
|
|
var dataStream = new DataStream();
|
|
|
|
this._currentAction = function (str) {
|
|
this._actionStream(str, callback);
|
|
}.bind(this);
|
|
|
|
dataStream.pipe(this._socket, {
|
|
end: false
|
|
});
|
|
|
|
if (this.options.debug) {
|
|
dataStream.on('data', function (chunk) {
|
|
this.emit('log', {
|
|
type: 'stream',
|
|
message: chunk.toString('binary').trim()
|
|
});
|
|
}.bind(this));
|
|
}
|
|
|
|
return dataStream;
|
|
};
|
|
|
|
/** ACTIONS **/
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionGreeting = function (str) {
|
|
clearTimeout(this._greetingTimeout);
|
|
|
|
if (str.substr(0, 3) !== '220') {
|
|
this._onError(new Error('Invalid greeting from server:\n' + str), 'EPROTOCOL', str);
|
|
return;
|
|
}
|
|
|
|
if (this.options.lmtp) {
|
|
this._currentAction = this._actionLHLO;
|
|
this._sendCommand('LHLO ' + this.options.name);
|
|
} else {
|
|
this._currentAction = this._actionEHLO;
|
|
this._sendCommand('EHLO ' + this.options.name);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handles server response for LHLO command. If it yielded in
|
|
* error, emit 'error', otherwise treat this as an EHLO response
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionLHLO = function (str) {
|
|
if (str.charAt(0) !== '2') {
|
|
this._onError(new Error('Invalid response for LHLO:\n' + str), 'EPROTOCOL', str);
|
|
return;
|
|
}
|
|
|
|
this._actionEHLO(str);
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionEHLO = function (str) {
|
|
if (str.substr(0, 3) === '421') {
|
|
this._onError(new Error('Server terminates connection:\n' + str), 'ECONNECTION', str);
|
|
return;
|
|
}
|
|
|
|
if (str.charAt(0) !== '2') {
|
|
if (this.options.requireTLS) {
|
|
this._onError(new Error('EHLO failed but HELO does not support required STARTTLS:\n' + str), 'ECONNECTION', str);
|
|
return;
|
|
}
|
|
|
|
// Try HELO instead
|
|
this._currentAction = this._actionHELO;
|
|
this._sendCommand('HELO ' + this.options.name);
|
|
return;
|
|
}
|
|
|
|
// Detect if the server supports STARTTLS
|
|
if (!this.secure && !this.options.ignoreTLS && (/[ \-]STARTTLS\r?$/mi.test(str) || this.options.requireTLS)) {
|
|
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 XOAUTH2 auth
|
|
if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i)) {
|
|
this._supportedAuth.push('XOAUTH2');
|
|
}
|
|
|
|
this.emit('connect');
|
|
};
|
|
|
|
/**
|
|
* Handles server response for HELO command. If it yielded in
|
|
* error, emit 'error', otherwise move into the authentication phase.
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionHELO = function (str) {
|
|
if (str.charAt(0) !== '2') {
|
|
this._onError(new Error('Invalid response for EHLO/HELO:\n' + str), 'EPROTOCOL', str);
|
|
return;
|
|
}
|
|
|
|
this.emit('connect');
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionSTARTTLS = function (str) {
|
|
if (str.charAt(0) !== '2') {
|
|
this._onError(new Error('Error upgrading connection with STARTTLS', 'ETLS', str));
|
|
return;
|
|
}
|
|
|
|
this._upgradeConnection((function (err, secured) {
|
|
if (err) {
|
|
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS');
|
|
return;
|
|
}
|
|
|
|
if (this.options.debug) {
|
|
this.emit('log', {
|
|
type: 'socket',
|
|
message: 'Connection upgraded'
|
|
});
|
|
}
|
|
|
|
if (secured) {
|
|
// restart session
|
|
this._currentAction = this._actionEHLO;
|
|
this._sendCommand('EHLO ' + this.options.name);
|
|
} else {
|
|
this.emit('connect');
|
|
}
|
|
}).bind(this));
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionAUTH_LOGIN_USER = function (str, callback) {
|
|
if (str !== '334 VXNlcm5hbWU6') {
|
|
callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str));
|
|
return;
|
|
}
|
|
|
|
this._currentAction = function (str) {
|
|
this._actionAUTH_LOGIN_PASS(str, callback);
|
|
}.bind(this);
|
|
|
|
this._sendCommand(new Buffer(this._auth.user + '', 'utf-8').toString('base64'));
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionAUTH_CRAM_MD5 = function (str, callback) {
|
|
var challengeMatch = str.match(/^334\s+(.+)$/);
|
|
var challengeString = '';
|
|
|
|
if (!challengeMatch) {
|
|
return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str));
|
|
} else {
|
|
challengeString = challengeMatch[1];
|
|
}
|
|
|
|
// Decode from base64
|
|
var base64decoded = new Buffer(challengeString, 'base64').toString('ascii'),
|
|
hmac_md5 = crypto.createHmac('md5', this._auth.pass);
|
|
|
|
hmac_md5.update(base64decoded);
|
|
|
|
var hex_hmac = hmac_md5.digest('hex'),
|
|
prepended = this._auth.user + ' ' + hex_hmac;
|
|
|
|
this._currentAction = function (str) {
|
|
this._actionAUTH_CRAM_MD5_PASS(str, callback);
|
|
}.bind(this);
|
|
|
|
|
|
this._sendCommand(new Buffer(prepended).toString('base64'));
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionAUTH_CRAM_MD5_PASS = function (str, callback) {
|
|
if (!str.match(/^235\s+/)) {
|
|
return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str));
|
|
}
|
|
|
|
this.authenticated = true;
|
|
callback(null, true);
|
|
};
|
|
|
|
/**
|
|
* 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.
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionAUTH_LOGIN_PASS = function (str, callback) {
|
|
if (str !== '334 UGFzc3dvcmQ6') {
|
|
return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str));
|
|
}
|
|
|
|
this._currentAction = function (str) {
|
|
this._actionAUTHComplete(str, callback);
|
|
}.bind(this);
|
|
|
|
this._sendCommand(new Buffer(this._auth.pass + '', 'utf-8').toString('base64'));
|
|
};
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionAUTHComplete = function (str, isRetry, callback) {
|
|
if (!callback && typeof isRetry === 'function') {
|
|
callback = isRetry;
|
|
isRetry = undefined;
|
|
}
|
|
|
|
if (str.substr(0, 3) === '334') {
|
|
this._currentAction = function (str) {
|
|
if (isRetry || !this._auth.xoauth2 || typeof this._auth.xoauth2 !== 'object') {
|
|
this._actionAUTHComplete(str, true, callback);
|
|
} else {
|
|
setTimeout(this._handleXOauth2Token.bind(this, true, callback), Math.random() * 4000 + 1000);
|
|
}
|
|
}.bind(this);
|
|
this._sendCommand('');
|
|
return;
|
|
}
|
|
|
|
if (str.charAt(0) !== '2') {
|
|
return callback(this._formatError('Invalid login', 'EAUTH', str));
|
|
}
|
|
|
|
this.authenticated = true;
|
|
callback(null, true);
|
|
};
|
|
|
|
/**
|
|
* Handle response for a MAIL FROM: command
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionMAIL = function (str, callback) {
|
|
if (Number(str.charAt(0)) !== 2) {
|
|
return callback(this._formatError('Mail command failed', 'EENVELOPE', str));
|
|
}
|
|
|
|
if (!this._envelope.rcptQueue.length) {
|
|
return callback(this._formatError('Can\'t send mail - no recipients defined', 'EENVELOPE'));
|
|
} else {
|
|
this._envelope.curRecipient = this._envelope.rcptQueue.shift();
|
|
this._currentAction = function (str) {
|
|
this._actionRCPT(str, callback);
|
|
}.bind(this);
|
|
this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle response for a RCPT TO: command
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionRCPT = function (str, callback) {
|
|
if (Number(str.charAt(0)) !== 2) {
|
|
// this is a soft error
|
|
this._envelope.rejected.push(this._envelope.curRecipient);
|
|
} else {
|
|
this._envelope.accepted.push(this._envelope.curRecipient);
|
|
}
|
|
|
|
if (!this._envelope.rcptQueue.length) {
|
|
if (this._envelope.rejected.length < this._envelope.to.length) {
|
|
this._currentAction = function (str) {
|
|
this._actionDATA(str, callback);
|
|
}.bind(this);
|
|
this._sendCommand('DATA');
|
|
} else {
|
|
return callback(this._formatError('Can\'t send mail - all recipients were rejected', 'EENVELOPE', str));
|
|
}
|
|
} else {
|
|
this._envelope.curRecipient = this._envelope.rcptQueue.shift();
|
|
this._currentAction = function (str) {
|
|
this._actionRCPT(str, callback);
|
|
}.bind(this);
|
|
this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handle response for a DATA command
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionDATA = function (str, callback) {
|
|
// 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) {
|
|
return callback(this._formatError('Data command failed', 'EENVELOPE', str));
|
|
}
|
|
|
|
callback(null, {
|
|
accepted: this._envelope.accepted,
|
|
rejected: this._envelope.rejected
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Handle response for a DATA stream
|
|
*
|
|
* @param {String} str Message from the server
|
|
*/
|
|
SMTPConnection.prototype._actionStream = function (str, callback) {
|
|
if (Number(str.charAt(0)) !== 2) {
|
|
// Message failed
|
|
return callback(this._formatError('Message failed', 'EMESSAGE', str));
|
|
} else {
|
|
// Message sent succesfully
|
|
return callback(null, str);
|
|
}
|
|
};
|
|
|
|
SMTPConnection.prototype._handleXOauth2Token = function (isRetry, callback) {
|
|
this._currentAction = function (str) {
|
|
this._actionAUTHComplete(str, isRetry, callback);
|
|
}.bind(this);
|
|
|
|
if (this._auth.xoauth2 && typeof this._auth.xoauth2 === 'object') {
|
|
this._auth.xoauth2[isRetry ? 'generateToken' : 'getToken'](function (err, token) {
|
|
if (err) {
|
|
return callback(this._formatError(err, 'EAUTH'));
|
|
}
|
|
this._sendCommand('AUTH XOAUTH2 ' + token);
|
|
}.bind(this));
|
|
} else {
|
|
this._sendCommand('AUTH XOAUTH2 ' + this._buildXOAuth2Token(this._auth.user, this._auth.xoauth2));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Builds a login token for XOAUTH2 authentication command
|
|
*
|
|
* @param {String} user E-mail address of the user
|
|
* @param {String} token Valid access token for the user
|
|
* @return {String} Base64 formatted login token
|
|
*/
|
|
SMTPConnection.prototype._buildXOAuth2Token = function (user, token) {
|
|
var authData = [
|
|
'user=' + (user || ''),
|
|
'auth=Bearer ' + token,
|
|
'',
|
|
''
|
|
];
|
|
return new Buffer(authData.join('\x01')).toString('base64');
|
|
};
|
|
|
|
SMTPConnection.prototype._getHostname = function () {
|
|
// defaul hostname is machine hostname or [IP]
|
|
var defaultHostname = os.hostname() || '';
|
|
|
|
// ignore if not FQDN
|
|
if (defaultHostname.indexOf('.') < 0) {
|
|
defaultHostname = '[127.0.0.1]';
|
|
}
|
|
|
|
// IP should be enclosed in []
|
|
if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) {
|
|
defaultHostname = '[' + defaultHostname + ']';
|
|
}
|
|
|
|
return defaultHostname;
|
|
};
|