simplesmtp
Version:
Simple SMTP server module to create custom SMTP servers
374 lines (320 loc) • 11.8 kB
JavaScript
'use strict';
var simplesmtp = require('../index'),
EventEmitter = require('events').EventEmitter,
utillib = require('util'),
xoauth2 = require('xoauth2');
// expose to the world
module.exports = function(port, host, options) {
var pool = new SMTPConnectionPool(port, host, options);
return pool;
};
/**
* <p>Creates a SMTP connection pool</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>maxConnections</b> - how many connections to keep in the pool</li>
* </ul>
*
* @constructor
* @namespace SMTP Client Pool module
* @param {Number} [port=25] The port number to connecto to
* @param {String} [host='localhost'] THe hostname to connect to
* @param {Object} [options] optional options object
*/
function SMTPConnectionPool(port, host, options) {
EventEmitter.call(this);
/**
* Port number to connect to
* @public
*/
this.port = port || 25;
/**
* Hostname to connect to
* @public
*/
this.host = host || 'localhost';
/**
* Options object
* @public
*/
this.options = options || {};
this.options.maxConnections = this.options.maxConnections || 5;
this.options.maxMessages = this.options.maxMessages || Infinity;
/**
* An array of connections that are currently idle
* @private
*/
this._connectionsAvailable = [];
/**
* An array of connections that are currently in use
* @private
*/
this._connectionsInUse = [];
/**
* Message queue (FIFO)
* @private
*/
this._messageQueue = [];
/**
* Counter for generating ID values for debugging
* @private
*/
this._idgen = 0;
// Initialize XOAUTH2 if needed
if (this.options.auth && 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.options.auth.XOAuth2 = xoauth2.createXOAuth2Generator(this.options.auth.XOAuth2);
}
}
utillib.inherits(SMTPConnectionPool, EventEmitter);
/**
* <p>Sends a message. If there's any idling connections available
* use one to send the message immediatelly, otherwise add to queue.</p>
*
* @param {Object} message MailComposer object
* @param {Function} callback Callback function to run on finish, gets an
* <code>error</code> object as a parameter if the sending failed
* and on success an object with <code>failedRecipients</code> array as
* a list of addresses that were rejected (if any) and
* <code>message</code> which indicates the last message received from
* the server
*/
SMTPConnectionPool.prototype.sendMail = function(message, callback) {
var connection;
message.returnCallback = callback;
if (this._connectionsAvailable.length) {
// if available connections pick one
connection = this._connectionsAvailable.pop();
this._connectionsInUse.push(connection);
this._processMessage(message, connection);
} else {
this._messageQueue.push(message);
if (this._connectionsAvailable.length + this._connectionsInUse.length < this.options.maxConnections) {
this._createConnection();
}
}
};
/**
* <p>Closes all connections</p>
*/
SMTPConnectionPool.prototype.close = function(callback) {
var connection;
// for some reason destroying the connections seem to be the only way :S
while (this._connectionsAvailable.length) {
connection = this._connectionsAvailable.pop();
connection.quit();
}
while (this._connectionsInUse.length) {
connection = this._connectionsInUse.pop();
connection.quit();
}
if (callback) {
if (typeof setImmediate == 'function') {
setImmediate(callback);
} else {
process.nextTick(callback);
}
}
};
/**
* <p>Initiates a connection to the SMTP server and adds it to the pool</p>
*/
SMTPConnectionPool.prototype._createConnection = function() {
var connectionOptions = {
instanceId: ++this._idgen,
debug: !! this.options.debug,
logFile: this.options.logFile,
ignoreTLS: !! this.options.ignoreTLS,
tls: this.options.tls || false,
auth: this.options.auth || false,
authMethod: this.options.authMethod,
name: this.options.name || false,
secureConnection: !! this.options.secureConnection
},
connection;
if ('greetingTimeout' in this.options) {
connectionOptions.greetingTimeout = this.options.greetingTimeout;
}
if ('socketTimeout' in this.options) {
connectionOptions.socketTimeout = this.options.socketTimeout;
}
if ('connectionTimeout' in this.options) {
connectionOptions.connectionTimeout = this.options.connectionTimeout;
}
if ('rejectUnathorized' in this.options) {
connectionOptions.rejectUnathorized = this.options.rejectUnathorized;
}
if ('localAddress' in this.options) {
connectionOptions.localAddress = this.options.localAddress;
}
connection = simplesmtp.connect(this.port, this.host, connectionOptions);
connection._messagesProcessed = 0;
connection.on('idle', this._onConnectionIdle.bind(this, connection));
connection.on('message', this._onConnectionMessage.bind(this, connection));
connection.on('ready', this._onConnectionReady.bind(this, connection));
connection.on('error', this._onConnectionError.bind(this, connection));
connection.on('end', this._onConnectionEnd.bind(this, connection));
connection.on('rcptFailed', this._onConnectionRCPTFailed.bind(this, connection));
this.emit('connectionCreated', connection);
// as the connection is not ready yet, add to 'in use' queue
this._connectionsInUse.push(connection);
};
/**
* <p>Processes a message by assigning it to a connection object and initiating
* the sending process by setting the envelope</p>
*
* @param {Object} message MailComposer message object
* @param {Object} connection <code>simplesmtp.connect</code> connection
*/
SMTPConnectionPool.prototype._processMessage = function(message, connection) {
connection.currentMessage = message;
message.currentConnection = connection;
connection._messagesProcessed++;
// send envelope
connection.useEnvelope(message.getEnvelope());
};
/**
* <p>Will be fired on <code>'idle'</code> events by the connection, if
* there's a message currently in queue</p>
*
* @event
* @param {Object} connection Connection object that fired the event
*/
SMTPConnectionPool.prototype._onConnectionIdle = function(connection) {
var message = this._messageQueue.shift();
if (message) {
this._processMessage(message, connection);
} else {
for (var i = 0, len = this._connectionsInUse.length; i < len; i++) {
if (this._connectionsInUse[i] == connection) {
this._connectionsInUse.splice(i, 1); // remove from list
break;
}
}
this._connectionsAvailable.push(connection);
}
};
/**
* <p>Will be called when not all recipients were accepted</p>
*
* @event
* @param {Object} connection Connection object that fired the event
* @param {Array} addresses Failed addresses as an array of strings
*/
SMTPConnectionPool.prototype._onConnectionRCPTFailed = function(connection, addresses) {
if (connection.currentMessage) {
connection.currentMessage.failedRecipients = addresses;
}
};
/**
* <p>Will be called when the client is waiting for a message to deliver</p>
*
* @event
* @param {Object} connection Connection object that fired the event
*/
SMTPConnectionPool.prototype._onConnectionMessage = function(connection) {
if (connection.currentMessage) {
connection.currentMessage.streamMessage();
connection.currentMessage.pipe(connection);
}
};
/**
* <p>Will be called when a message has been delivered</p>
*
* @event
* @param {Object} connection Connection object that fired the event
* @param {Boolean} success True if the message was queued by the SMTP server
* @param {String} message Last message received from the server
*/
SMTPConnectionPool.prototype._onConnectionReady = function(connection, success, message) {
var error, responseObj = {};
if (connection._messagesProcessed >= this.options.maxMessages && connection.socket) {
connection.emit('end');
connection.removeAllListeners();
if (connection.socket) {
connection.socket.destroy();
}
this.emit('released', connection);
}
if (connection.currentMessage && connection.currentMessage.returnCallback) {
if (success) {
if (connection.currentMessage.failedRecipients) {
responseObj.failedRecipients = connection.currentMessage.failedRecipients;
}
if (message) {
responseObj.message = message;
}
if (connection.currentMessage._messageId) {
responseObj.messageId = connection.currentMessage._messageId;
}
connection.currentMessage.returnCallback(null, responseObj);
} else {
error = new Error('Message delivery failed' + (message ? ': ' + message : ''));
error.name = 'DeliveryError';
error.data = message;
connection.currentMessage.returnCallback(error);
}
}
connection.currentMessage = false;
};
/**
* <p>Will be called when an error occurs</p>
*
* @event
* @param {Object} connection Connection object that fired the event
* @param {Object} error Error object
*/
SMTPConnectionPool.prototype._onConnectionError = function(connection, error) {
var message = connection.currentMessage;
connection.currentMessage = false;
// clear a first message from the list, otherwise an infinite loop will emerge
if (!message) {
message = this._messageQueue.shift();
}
if (message && message.returnCallback) {
message.returnCallback(error);
}
};
/**
* <p>Will be called when a connection to the client is closed</p>
*
* @event
* @param {Object} connection Connection object that fired the event
*/
SMTPConnectionPool.prototype._onConnectionEnd = function(connection) {
var removed = false,
i, len;
// if in 'available' list, remove
for (i = 0, len = this._connectionsAvailable.length; i < len; i++) {
if (this._connectionsAvailable[i] == connection) {
this._connectionsAvailable.splice(i, 1); // remove from list
removed = true;
break;
}
}
if (!removed) {
// if in 'in use' list, remove
for (i = 0, len = this._connectionsInUse.length; i < len; i++) {
if (this._connectionsInUse[i] == connection) {
this._connectionsInUse.splice(i, 1); // remove from list
removed = true;
break;
}
}
}
// if there's still unprocessed mail and available connection slots, create
// a new connection
if (this._messageQueue.length &&
this._connectionsInUse.length + this._connectionsAvailable.length < this.options.maxConnections) {
this._createConnection();
}
};