smtp-connection
Advanced tools
Comparing version 2.12.2 to 3.0.0
@@ -21,3 +21,3 @@ 'use strict'; | ||
'no-unused-vars': 2, | ||
'no-undef': 2, | ||
'no-undefined': 2, | ||
'handle-callback-err': 2, | ||
@@ -43,16 +43,30 @@ 'no-new': 2, | ||
'no-throw-literal': 2, | ||
'no-useless-call': 2, | ||
'no-useless-concat': 2, | ||
'no-void': 2, | ||
yoda: 2, | ||
'no-undef': 2, | ||
'global-require': 2, | ||
'no-var': 2, | ||
'no-bitwise': 2, | ||
'no-lonely-if': 2, | ||
'no-mixed-spaces-and-tabs': 2, | ||
'no-console': 2 | ||
'arrow-body-style': [2, 'as-needed'], | ||
'arrow-parens': [2, 'as-needed'], | ||
'prefer-arrow-callback': 2, | ||
'object-shorthand': 2, | ||
'prefer-spread': 2 | ||
}, | ||
env: { | ||
es6: false, | ||
es6: true, | ||
node: true | ||
}, | ||
extends: 'eslint:recommended', | ||
globals: { | ||
it: true, | ||
describe: true, | ||
beforeEach: true, | ||
afterEach: true | ||
}, | ||
fix: true | ||
}; |
# Changelog | ||
## v3.0.0 2016-12-09 | ||
* Use ES6 syntax | ||
* Updated logging to support structured output for real Bunyan logger instances | ||
## v2.12.1 2016-10-10 | ||
@@ -4,0 +9,0 @@ |
'use strict'; | ||
var stream = require('stream'); | ||
var Transform = stream.Transform; | ||
var util = require('util'); | ||
const stream = require('stream'); | ||
const Transform = stream.Transform; | ||
module.exports = DataStream; | ||
/** | ||
@@ -15,98 +12,103 @@ * Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF> | ||
*/ | ||
function DataStream(options) { | ||
// init Transform | ||
this.options = options || {}; | ||
this._curLine = ''; | ||
class DataStream extends Transform { | ||
this.inByteCount = 0; | ||
this.outByteCount = 0; | ||
this.lastByte = false; | ||
constructor(options) { | ||
super(options); | ||
// init Transform | ||
this.options = options || {}; | ||
this._curLine = ''; | ||
Transform.call(this, this.options); | ||
} | ||
util.inherits(DataStream, Transform); | ||
this.inByteCount = 0; | ||
this.outByteCount = 0; | ||
this.lastByte = false; | ||
/** | ||
* Escapes dots | ||
*/ | ||
DataStream.prototype._transform = function (chunk, encoding, done) { | ||
var chunks = []; | ||
var chunklen = 0; | ||
var i, len, lastPos = 0; | ||
var buf; | ||
if (!chunk || !chunk.length) { | ||
return done(); | ||
} | ||
if (typeof chunk === 'string') { | ||
chunk = new Buffer(chunk); | ||
} | ||
/** | ||
* Escapes dots | ||
*/ | ||
_transform(chunk, encoding, done) { | ||
let chunks = []; | ||
let chunklen = 0; | ||
let i, len, lastPos = 0; | ||
let buf; | ||
this.inByteCount += chunk.length; | ||
if (!chunk || !chunk.length) { | ||
return done(); | ||
} | ||
for (i = 0, len = chunk.length; i < len; i++) { | ||
if (chunk[i] === 0x2E) { // . | ||
if ( | ||
(i && chunk[i - 1] === 0x0A) || | ||
(!i && (!this.lastByte || this.lastByte === 0x0A)) | ||
) { | ||
buf = chunk.slice(lastPos, i + 1); | ||
chunks.push(buf); | ||
chunks.push(new Buffer('.')); | ||
chunklen += buf.length + 1; | ||
lastPos = i + 1; | ||
} | ||
} else if (chunk[i] === 0x0A) { // . | ||
if ( | ||
(i && chunk[i - 1] !== 0x0D) || | ||
(!i && this.lastByte !== 0x0D) | ||
) { | ||
if (i > lastPos) { | ||
buf = chunk.slice(lastPos, i); | ||
if (typeof chunk === 'string') { | ||
chunk = new Buffer(chunk); | ||
} | ||
this.inByteCount += chunk.length; | ||
for (i = 0, len = chunk.length; i < len; i++) { | ||
if (chunk[i] === 0x2E) { // . | ||
if ( | ||
(i && chunk[i - 1] === 0x0A) || | ||
(!i && (!this.lastByte || this.lastByte === 0x0A)) | ||
) { | ||
buf = chunk.slice(lastPos, i + 1); | ||
chunks.push(buf); | ||
chunklen += buf.length + 2; | ||
} else { | ||
chunklen += 2; | ||
chunks.push(new Buffer('.')); | ||
chunklen += buf.length + 1; | ||
lastPos = i + 1; | ||
} | ||
chunks.push(new Buffer('\r\n')); | ||
lastPos = i + 1; | ||
} else if (chunk[i] === 0x0A) { // . | ||
if ( | ||
(i && chunk[i - 1] !== 0x0D) || | ||
(!i && this.lastByte !== 0x0D) | ||
) { | ||
if (i > lastPos) { | ||
buf = chunk.slice(lastPos, i); | ||
chunks.push(buf); | ||
chunklen += buf.length + 2; | ||
} else { | ||
chunklen += 2; | ||
} | ||
chunks.push(new Buffer('\r\n')); | ||
lastPos = i + 1; | ||
} | ||
} | ||
} | ||
} | ||
if (chunklen) { | ||
// add last piece | ||
if (lastPos < chunk.length) { | ||
buf = chunk.slice(lastPos); | ||
chunks.push(buf); | ||
chunklen += buf.length; | ||
if (chunklen) { | ||
// add last piece | ||
if (lastPos < chunk.length) { | ||
buf = chunk.slice(lastPos); | ||
chunks.push(buf); | ||
chunklen += buf.length; | ||
} | ||
this.outByteCount += chunklen; | ||
this.push(Buffer.concat(chunks, chunklen)); | ||
} else { | ||
this.outByteCount += chunk.length; | ||
this.push(chunk); | ||
} | ||
this.outByteCount += chunklen; | ||
this.push(Buffer.concat(chunks, chunklen)); | ||
} else { | ||
this.outByteCount += chunk.length; | ||
this.push(chunk); | ||
this.lastByte = chunk[chunk.length - 1]; | ||
done(); | ||
} | ||
this.lastByte = chunk[chunk.length - 1]; | ||
done(); | ||
}; | ||
/** | ||
* Finalizes the stream with a dot on a single line | ||
*/ | ||
_flush(done) { | ||
let buf; | ||
if (this.lastByte === 0x0A) { | ||
buf = new Buffer('.\r\n'); | ||
} else if (this.lastByte === 0x0D) { | ||
buf = new Buffer('\n.\r\n'); | ||
} else { | ||
buf = new Buffer('\r\n.\r\n'); | ||
} | ||
this.outByteCount += buf.length; | ||
this.push(buf); | ||
done(); | ||
} | ||
/** | ||
* Finalizes the stream with a dot on a single line | ||
*/ | ||
DataStream.prototype._flush = function (done) { | ||
var buf; | ||
if (this.lastByte === 0x0A) { | ||
buf = new Buffer('.\r\n'); | ||
} else if (this.lastByte === 0x0D) { | ||
buf = new Buffer('\n.\r\n'); | ||
} else { | ||
buf = new Buffer('\r\n.\r\n'); | ||
} | ||
this.outByteCount += buf.length; | ||
this.push(buf); | ||
done(); | ||
}; | ||
} | ||
module.exports = DataStream; |
'use strict'; | ||
var packageInfo = require('../package.json'); | ||
var EventEmitter = require('events').EventEmitter; | ||
var util = require('util'); | ||
var net = require('net'); | ||
var tls = require('tls'); | ||
var os = require('os'); | ||
var crypto = require('crypto'); | ||
var DataStream = require('./data-stream'); | ||
var PassThrough = require('stream').PassThrough; | ||
var shared = require('nodemailer-shared'); | ||
var ntlm = require('httpntlm/ntlm'); | ||
const packageInfo = require('../package.json'); | ||
const EventEmitter = require('events').EventEmitter; | ||
const net = require('net'); | ||
const tls = require('tls'); | ||
const os = require('os'); | ||
const crypto = require('crypto'); | ||
const DataStream = require('./data-stream'); | ||
const PassThrough = require('stream').PassThrough; | ||
const shared = require('nodemailer-shared'); | ||
const ntlm = require('httpntlm/ntlm'); | ||
// default timeout values in ms | ||
var CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established | ||
var SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client | ||
var GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved | ||
const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established | ||
const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client | ||
const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved | ||
module.exports = SMTPConnection; | ||
/** | ||
@@ -48,1397 +45,1499 @@ * Generates a SMTP connection object | ||
*/ | ||
function SMTPConnection(options) { | ||
EventEmitter.call(this); | ||
class SMTPConnection extends EventEmitter { | ||
constructor(options) { | ||
super(options); | ||
this.id = crypto.randomBytes(8).toString('base64').replace(/\W/g, ''); | ||
this.stage = 'init'; | ||
this.id = crypto.randomBytes(8).toString('base64').replace(/\W/g, ''); | ||
this.stage = 'init'; | ||
this.options = options || {}; | ||
this.options = options || {}; | ||
this.secureConnection = !!this.options.secure; | ||
this.alreadySecured = !!this.options.secured; | ||
this.component = this.options.component || 'smtp-connection'; | ||
this.port = this.options.port || (this.secureConnection ? 465 : 25); | ||
this.host = this.options.host || 'localhost'; | ||
this.secureConnection = !!this.options.secure; | ||
this.alreadySecured = !!this.options.secured; | ||
if (typeof this.options.secure === 'undefined' && this.port === 465) { | ||
// if secure option is not set but port is 465, then default to secure | ||
this.secureConnection = true; | ||
} | ||
this.port = this.options.port || (this.secureConnection ? 465 : 25); | ||
this.host = this.options.host || 'localhost'; | ||
this.name = this.options.name || this._getHostname(); | ||
if (typeof this.options.secure === 'undefined' && this.port === 465) { | ||
// if secure option is not set but port is 465, then default to secure | ||
this.secureConnection = true; | ||
} | ||
this.logger = shared.getLogger(this.options); | ||
this.name = this.options.name || this._getHostname(); | ||
/** | ||
* Expose version nr, just for the reference | ||
* @type {String} | ||
*/ | ||
this.version = packageInfo.version; | ||
// If true then log metainfo as first argument (needed for *real* bunyan) | ||
this.structuredLogger = this.options.structuredLogger && this.options.logger && typeof this.options.logger === 'object'; | ||
/** | ||
* If true, then the user is authenticated | ||
* @type {Boolean} | ||
*/ | ||
this.authenticated = false; | ||
// autodetect bunyan | ||
if (!('structuredLogger' in this.options) && this.options.logger && typeof this.options.logger === 'object' && this.options.logger.fields) { | ||
this.structuredLogger = true; | ||
} | ||
/** | ||
* If set to true, this instance is no longer active | ||
* @private | ||
*/ | ||
this.destroyed = false; | ||
this.logger = shared.getLogger(this.options); | ||
/** | ||
* Defines if the current connection is secure or not. If not, | ||
* STARTTLS can be used if available | ||
* @private | ||
*/ | ||
this.secure = !!this.secureConnection; | ||
/** | ||
* Expose version nr, just for the reference | ||
* @type {String} | ||
*/ | ||
this.version = packageInfo.version; | ||
/** | ||
* Store incomplete messages coming from the server | ||
* @private | ||
*/ | ||
this._remainder = ''; | ||
/** | ||
* If true, then the user is authenticated | ||
* @type {Boolean} | ||
*/ | ||
this.authenticated = false; | ||
/** | ||
* Unprocessed responses from the server | ||
* @type {Array} | ||
*/ | ||
this._responseQueue = []; | ||
/** | ||
* If set to true, this instance is no longer active | ||
* @private | ||
*/ | ||
this.destroyed = false; | ||
/** | ||
* The socket connecting to the server | ||
* @publick | ||
*/ | ||
this._socket = false; | ||
/** | ||
* Defines if the current connection is secure or not. If not, | ||
* STARTTLS can be used if available | ||
* @private | ||
*/ | ||
this.secure = !!this.secureConnection; | ||
/** | ||
* Lists supported auth mechanisms | ||
* @private | ||
*/ | ||
this._supportedAuth = []; | ||
/** | ||
* Store incomplete messages coming from the server | ||
* @private | ||
*/ | ||
this._remainder = ''; | ||
/** | ||
* Includes current envelope (from, to) | ||
* @private | ||
*/ | ||
this._envelope = false; | ||
/** | ||
* Unprocessed responses from the server | ||
* @type {Array} | ||
*/ | ||
this._responseQueue = []; | ||
/** | ||
* Lists supported extensions | ||
* @private | ||
*/ | ||
this._supportedExtensions = []; | ||
/** | ||
* The socket connecting to the server | ||
* @publick | ||
*/ | ||
this._socket = false; | ||
/** | ||
* Defines the maximum allowed size for a single message | ||
* @private | ||
*/ | ||
this._maxAllowedSize = 0; | ||
/** | ||
* Lists supported auth mechanisms | ||
* @private | ||
*/ | ||
this._supportedAuth = []; | ||
/** | ||
* Function queue to run if a data chunk comes from the server | ||
* @private | ||
*/ | ||
this._responseActions = []; | ||
this._recipientQueue = []; | ||
/** | ||
* Includes current envelope (from, to) | ||
* @private | ||
*/ | ||
this._envelope = false; | ||
/** | ||
* Timeout variable for waiting the greeting | ||
* @private | ||
*/ | ||
this._greetingTimeout = false; | ||
/** | ||
* Lists supported extensions | ||
* @private | ||
*/ | ||
this._supportedExtensions = []; | ||
/** | ||
* Timeout variable for waiting the connection to start | ||
* @private | ||
*/ | ||
this._connectionTimeout = false; | ||
/** | ||
* Defines the maximum allowed size for a single message | ||
* @private | ||
*/ | ||
this._maxAllowedSize = 0; | ||
/** | ||
* If the socket is deemed already closed | ||
* @private | ||
*/ | ||
this._destroyed = false; | ||
/** | ||
* Function queue to run if a data chunk comes from the server | ||
* @private | ||
*/ | ||
this._responseActions = []; | ||
this._recipientQueue = []; | ||
/** | ||
* If the socket is already being closed | ||
* @private | ||
*/ | ||
this._closing = false; | ||
} | ||
util.inherits(SMTPConnection, EventEmitter); | ||
/** | ||
* Timeout variable for waiting the greeting | ||
* @private | ||
*/ | ||
this._greetingTimeout = false; | ||
/** | ||
* Creates a connection to a SMTP server and sets up connection | ||
* listener | ||
*/ | ||
SMTPConnection.prototype.connect = function (connectCallback) { | ||
if (typeof connectCallback === 'function') { | ||
this.once('connect', function () { | ||
this.logger.debug('[%s] SMTP handshake finished', this.id); | ||
connectCallback(); | ||
}.bind(this)); | ||
} | ||
/** | ||
* Timeout variable for waiting the connection to start | ||
* @private | ||
*/ | ||
this._connectionTimeout = false; | ||
var opts = { | ||
port: this.port, | ||
host: this.host | ||
}; | ||
/** | ||
* If the socket is deemed already closed | ||
* @private | ||
*/ | ||
this._destroyed = false; | ||
if (this.options.localAddress) { | ||
opts.localAddress = this.options.localAddress; | ||
/** | ||
* If the socket is already being closed | ||
* @private | ||
*/ | ||
this._closing = false; | ||
} | ||
if (this.options.connection) { | ||
// connection is already opened | ||
this._socket = this.options.connection; | ||
if (this.secureConnection && !this.alreadySecured) { | ||
setImmediate(this._upgradeConnection.bind(this, function (err) { | ||
if (err) { | ||
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN'); | ||
return; | ||
} | ||
this._onConnect(); | ||
}.bind(this))); | ||
} else { | ||
setImmediate(this._onConnect.bind(this)); | ||
/** | ||
* Creates a connection to a SMTP server and sets up connection | ||
* listener | ||
*/ | ||
connect(connectCallback) { | ||
if (typeof connectCallback === 'function') { | ||
this.once('connect', () => { | ||
this._log({ | ||
level: 'debug', | ||
tnx: 'smtp' | ||
}, 'SMTP handshake finished'); | ||
connectCallback(); | ||
}); | ||
} | ||
} else if (this.options.socket) { | ||
// socket object is set up but not yet connected | ||
this._socket = this.options.socket; | ||
try { | ||
this._socket.connect(this.port, this.host, this._onConnect.bind(this)); | ||
} catch (E) { | ||
return setImmediate(this._onError.bind(this, E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
} else if (this.secureConnection) { | ||
// connect using tls | ||
if (this.options.tls) { | ||
Object.keys(this.options.tls).forEach(function (key) { | ||
opts[key] = this.options.tls[key]; | ||
}.bind(this)); | ||
} | ||
try { | ||
this._socket = tls.connect(this.port, this.host, opts, this._onConnect.bind(this)); | ||
} catch (E) { | ||
return setImmediate(this._onError.bind(this, E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
} else { | ||
// connect using plaintext | ||
try { | ||
this._socket = net.connect(opts, this._onConnect.bind(this)); | ||
} catch (E) { | ||
return setImmediate(this._onError.bind(this, E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
} | ||
this._connectionTimeout = setTimeout(function () { | ||
this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN'); | ||
}.bind(this), this.options.connectionTimeout || CONNECTION_TIMEOUT); | ||
let opts = { | ||
port: this.port, | ||
host: this.host | ||
}; | ||
this._socket.on('error', function (err) { | ||
this._onError(err, 'ECONNECTION', false, 'CONN'); | ||
}.bind(this)); | ||
}; | ||
if (this.options.localAddress) { | ||
opts.localAddress = this.options.localAddress; | ||
} | ||
/** | ||
* Sends QUIT | ||
*/ | ||
SMTPConnection.prototype.quit = function () { | ||
this._sendCommand('QUIT'); | ||
this._responseActions.push(this.close); | ||
}; | ||
if (this.options.connection) { | ||
// connection is already opened | ||
this._socket = this.options.connection; | ||
if (this.secureConnection && !this.alreadySecured) { | ||
setImmediate(() => this._upgradeConnection(err => { | ||
if (err) { | ||
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN'); | ||
return; | ||
} | ||
this._onConnect(); | ||
})); | ||
} else { | ||
setImmediate(() => this._onConnect()); | ||
} | ||
} else if (this.options.socket) { | ||
// socket object is set up but not yet connected | ||
this._socket = this.options.socket; | ||
try { | ||
this._socket.connect(this.port, this.host, () => this._onConnect()); | ||
} catch (E) { | ||
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
} else if (this.secureConnection) { | ||
// connect using tls | ||
if (this.options.tls) { | ||
Object.keys(this.options.tls).forEach(key => { | ||
opts[key] = this.options.tls[key]; | ||
}); | ||
} | ||
try { | ||
this._socket = tls.connect(this.port, this.host, opts, () => this._onConnect()); | ||
} catch (E) { | ||
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
} else { | ||
// connect using plaintext | ||
try { | ||
this._socket = net.connect(opts, () => this._onConnect()); | ||
} catch (E) { | ||
return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
} | ||
/** | ||
* Closes the connection to the server | ||
*/ | ||
SMTPConnection.prototype.close = function () { | ||
clearTimeout(this._connectionTimeout); | ||
clearTimeout(this._greetingTimeout); | ||
this._responseActions = []; | ||
this._connectionTimeout = setTimeout(() => { | ||
this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN'); | ||
}, this.options.connectionTimeout || CONNECTION_TIMEOUT); | ||
// allow to run this function only once | ||
if (this._closing) { | ||
return; | ||
this._socket.on('error', err => { | ||
this._onError(err, 'ECONNECTION', false, 'CONN'); | ||
}); | ||
} | ||
this._closing = true; | ||
var closeMethod = 'end'; | ||
if (this.stage === 'init') { | ||
// Close the socket immediately when connection timed out | ||
closeMethod = 'destroy'; | ||
/** | ||
* Sends QUIT | ||
*/ | ||
quit() { | ||
this._sendCommand('QUIT'); | ||
this._responseActions.push(this.close); | ||
} | ||
this.logger.debug('[%s] Closing connection to the server using "%s"', this.id, closeMethod); | ||
/** | ||
* Closes the connection to the server | ||
*/ | ||
close() { | ||
clearTimeout(this._connectionTimeout); | ||
clearTimeout(this._greetingTimeout); | ||
this._responseActions = []; | ||
var socket = this._socket && this._socket.socket || this._socket; | ||
// allow to run this function only once | ||
if (this._closing) { | ||
return; | ||
} | ||
this._closing = true; | ||
if (socket && !socket.destroyed) { | ||
try { | ||
this._socket[closeMethod](); | ||
} catch (E) { | ||
// just ignore | ||
let closeMethod = 'end'; | ||
if (this.stage === 'init') { | ||
// Close the socket immediately when connection timed out | ||
closeMethod = 'destroy'; | ||
} | ||
} | ||
this._destroy(); | ||
}; | ||
this._log({ | ||
level: 'debug', | ||
tnx: 'smtp' | ||
}, 'Closing connection to the server using "%s"', closeMethod); | ||
/** | ||
* Authenticate user | ||
*/ | ||
SMTPConnection.prototype.login = function (authData, callback) { | ||
this._auth = authData || {}; | ||
this._user = this._auth.xoauth2 && this._auth.xoauth2.options && this._auth.xoauth2.options.user || this._auth.user || ''; | ||
let socket = this._socket && this._socket.socket || this._socket; | ||
this._authMethod = false; | ||
if (this.options.authMethod) { | ||
this._authMethod = this.options.authMethod.toUpperCase().trim(); | ||
} else if (this._auth.xoauth2 && this._supportedAuth.indexOf('XOAUTH2') >= 0) { | ||
this._authMethod = 'XOAUTH2'; | ||
} else if (this._auth.domain && this._supportedAuth.indexOf('NTLM') >= 0) { | ||
this._authMethod = 'NTLM'; | ||
} else { | ||
// use first supported | ||
this._authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim(); | ||
} | ||
if (socket && !socket.destroyed) { | ||
try { | ||
this._socket[closeMethod](); | ||
} catch (E) { | ||
// just ignore | ||
} | ||
} | ||
switch (this._authMethod) { | ||
case 'XOAUTH2': | ||
this._handleXOauth2Token(false, callback); | ||
return; | ||
case 'LOGIN': | ||
this._responseActions.push(function (str) { | ||
this._actionAUTH_LOGIN_USER(str, callback); | ||
}.bind(this)); | ||
this._sendCommand('AUTH LOGIN'); | ||
return; | ||
case 'PLAIN': | ||
this._responseActions.push(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._responseActions.push(function (str) { | ||
this._actionAUTH_CRAM_MD5(str, callback); | ||
}.bind(this)); | ||
this._sendCommand('AUTH CRAM-MD5'); | ||
return; | ||
case 'NTLM': | ||
this._responseActions.push(function (str) { | ||
this._actionAUTH_NTLM_TYPE1(str, callback); | ||
}.bind(this)); | ||
this._sendCommand('AUTH ' + ntlm.createType1Message({ | ||
domain: this._auth.domain || '', | ||
workstation: this._auth.workstation || '' | ||
})); | ||
return; | ||
this._destroy(); | ||
} | ||
return callback(this._formatError('Unknown authentication method "' + this._authMethod + '"', 'EAUTH', false, 'API')); | ||
}; | ||
/** | ||
* Authenticate user | ||
*/ | ||
login(authData, callback) { | ||
this._auth = authData || {}; | ||
this._user = this._auth.xoauth2 && this._auth.xoauth2.options && this._auth.xoauth2.options.user || this._auth.user || ''; | ||
/** | ||
* 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, done) { | ||
if (!message) { | ||
return done(this._formatError('Empty message', 'EMESSAGE', false, 'API')); | ||
} | ||
this._authMethod = false; | ||
if (this.options.authMethod) { | ||
this._authMethod = this.options.authMethod.toUpperCase().trim(); | ||
} else if (this._auth.xoauth2 && this._supportedAuth.indexOf('XOAUTH2') >= 0) { | ||
this._authMethod = 'XOAUTH2'; | ||
} else if (this._auth.domain && this._supportedAuth.indexOf('NTLM') >= 0) { | ||
this._authMethod = 'NTLM'; | ||
} else { | ||
// use first supported | ||
this._authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim(); | ||
} | ||
// reject larger messages than allowed | ||
if (this._maxAllowedSize && envelope.size > this._maxAllowedSize) { | ||
return setImmediate(function () { | ||
done(this._formatError('Message size larger than allowed ' + this._maxAllowedSize, 'EMESSAGE', false, 'MAIL FROM')); | ||
}.bind(this)); | ||
switch (this._authMethod) { | ||
case 'XOAUTH2': | ||
this._handleXOauth2Token(false, callback); | ||
return; | ||
case 'LOGIN': | ||
this._responseActions.push(str => { | ||
this._actionAUTH_LOGIN_USER(str, callback); | ||
}); | ||
this._sendCommand('AUTH LOGIN'); | ||
return; | ||
case 'PLAIN': | ||
this._responseActions.push(str => { | ||
this._actionAUTHComplete(str, callback); | ||
}); | ||
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._responseActions.push(str => { | ||
this._actionAUTH_CRAM_MD5(str, callback); | ||
}); | ||
this._sendCommand('AUTH CRAM-MD5'); | ||
return; | ||
case 'NTLM': | ||
this._responseActions.push(str => { | ||
this._actionAUTH_NTLM_TYPE1(str, callback); | ||
}); | ||
this._sendCommand('AUTH ' + ntlm.createType1Message({ | ||
domain: this._auth.domain || '', | ||
workstation: this._auth.workstation || '' | ||
})); | ||
return; | ||
} | ||
return callback(this._formatError('Unknown authentication method "' + this._authMethod + '"', 'EAUTH', false, 'API')); | ||
} | ||
// ensure that callback is only called once | ||
var returned = false; | ||
var callback = function () { | ||
if (returned) { | ||
return; | ||
/** | ||
* 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 | ||
*/ | ||
send(envelope, message, done) { | ||
if (!message) { | ||
return done(this._formatError('Empty message', 'EMESSAGE', false, 'API')); | ||
} | ||
returned = true; | ||
done.apply(null, Array.prototype.slice.call(arguments)); | ||
}; | ||
// reject larger messages than allowed | ||
if (this._maxAllowedSize && envelope.size > this._maxAllowedSize) { | ||
return setImmediate(() => { | ||
done(this._formatError('Message size larger than allowed ' + this._maxAllowedSize, 'EMESSAGE', false, 'MAIL FROM')); | ||
}); | ||
} | ||
if (typeof message.on === 'function') { | ||
message.on('error', function (err) { | ||
return callback(this._formatError(err, 'ESTREAM', false, 'API')); | ||
}.bind(this)); | ||
} | ||
// ensure that callback is only called once | ||
let returned = false; | ||
let callback = function () { | ||
if (returned) { | ||
return; | ||
} | ||
returned = true; | ||
this._setEnvelope(envelope, function (err, info) { | ||
if (err) { | ||
return callback(err); | ||
done(...arguments); | ||
}; | ||
if (typeof message.on === 'function') { | ||
message.on('error', err => callback(this._formatError(err, 'ESTREAM', false, 'API'))); | ||
} | ||
var stream = this._createSendStream(function (err, str) { | ||
this._setEnvelope(envelope, (err, info) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
info.response = str; | ||
return callback(null, info); | ||
let stream = this._createSendStream((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(); | ||
} | ||
}); | ||
if (typeof message.pipe === 'function') { | ||
message.pipe(stream); | ||
} else { | ||
stream.write(message); | ||
stream.end(); | ||
} | ||
} | ||
}.bind(this)); | ||
}; | ||
/** | ||
* Resets connection state | ||
* | ||
* @param {Function} callback Callback to return once connection is reset | ||
*/ | ||
reset(callback) { | ||
this._sendCommand('RSET'); | ||
this._responseActions.push(str => { | ||
if (str.charAt(0) !== '2') { | ||
return callback(this._formatError('Could not reset session state:\n' + str, 'EPROTOCOL', str, 'RSET')); | ||
} | ||
this._envelope = false; | ||
return callback(null, true); | ||
}); | ||
} | ||
/** | ||
* Resets connection state | ||
* | ||
* @param {Function} callback Callback to return once connection is reset | ||
*/ | ||
SMTPConnection.prototype.reset = function (callback) { | ||
this._sendCommand('RSET'); | ||
this._responseActions.push(function (str) { | ||
if (str.charAt(0) !== '2') { | ||
return callback(this._formatError('Could not reset session state:\n' + str, 'EPROTOCOL', str, 'RSET')); | ||
/** | ||
* Connection listener that is run when the connection to | ||
* the server is opened | ||
* | ||
* @event | ||
*/ | ||
_onConnect() { | ||
clearTimeout(this._connectionTimeout); | ||
this._log({ | ||
level: 'info', | ||
tnx: 'network', | ||
localAddress: this._socket.localAddress, | ||
localPort: this._socket.localPort, | ||
remoteAddress: this._socket.remoteAddress, | ||
remotePort: this._socket.remotePort | ||
}, '%s established to %s:%s', this.secure ? 'Secure connection' : 'Connection', this._socket.remoteAddress, this._socket.remotePort); | ||
if (this._destroyed) { | ||
// Connection was established after we already had canceled it | ||
this.close(); | ||
return; | ||
} | ||
this._envelope = false; | ||
return callback(null, true); | ||
}.bind(this)); | ||
}; | ||
/** | ||
* Connection listener that is run when the connection to | ||
* the server is opened | ||
* | ||
* @event | ||
*/ | ||
SMTPConnection.prototype._onConnect = function () { | ||
clearTimeout(this._connectionTimeout); | ||
this.stage = 'connected'; | ||
this.logger.info('[%s] %s established to %s:%s', this.id, this.secure ? 'Secure connection' : 'Connection', this._socket.remoteAddress, this._socket.remotePort); | ||
// clear existing listeners for the socket | ||
this._socket.removeAllListeners('data'); | ||
this._socket.removeAllListeners('timeout'); | ||
this._socket.removeAllListeners('close'); | ||
this._socket.removeAllListeners('end'); | ||
if (this._destroyed) { | ||
// Connection was established after we already had canceled it | ||
this.close(); | ||
return; | ||
} | ||
this._socket.on('data', chunk => this._onData(chunk)); | ||
this._socket.once('close', errored => this._onClose(errored)); | ||
this._socket.once('end', () => this._onEnd()); | ||
this.stage = 'connected'; | ||
this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); | ||
this._socket.on('timeout', () => this._onTimeout()); | ||
// clear existing listeners for the socket | ||
this._socket.removeAllListeners('data'); | ||
this._socket.removeAllListeners('timeout'); | ||
this._socket.removeAllListeners('close'); | ||
this._socket.removeAllListeners('end'); | ||
this._greetingTimeout = setTimeout(() => { | ||
// if still waiting for greeting, give up | ||
if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) { | ||
this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN'); | ||
} | ||
}, this.options.greetingTimeout || GREETING_TIMEOUT); | ||
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._responseActions.push(this._actionGreeting); | ||
this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); | ||
this._socket.on('timeout', this._onTimeout.bind(this)); | ||
// we have a 'data' listener set up so resume socket if it was paused | ||
this._socket.resume(); | ||
} | ||
this._greetingTimeout = setTimeout(function () { | ||
// if still waiting for greeting, give up | ||
if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) { | ||
this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN'); | ||
/** | ||
* 'data' listener for data coming from the server | ||
* | ||
* @event | ||
* @param {Buffer} chunk Data chunk coming from the server | ||
*/ | ||
_onData(chunk) { | ||
if (this._destroyed || !chunk || !chunk.length) { | ||
return; | ||
} | ||
}.bind(this), this.options.greetingTimeout || GREETING_TIMEOUT); | ||
this._responseActions.push(this._actionGreeting); | ||
let data = (chunk || '').toString('binary'); | ||
let lines = (this._remainder + data).split(/\r?\n/); | ||
let lastline; | ||
// we have a 'data' listener set up so resume socket if it was paused | ||
this._socket.resume(); | ||
}; | ||
this._remainder = lines.pop(); | ||
/** | ||
* '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; | ||
for (let 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._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, command) { | ||
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._processResponse(); | ||
} | ||
err = this._formatError(err, type, data, command); | ||
/** | ||
* 'error' listener for the socket | ||
* | ||
* @event | ||
* @param {Error} err Error object | ||
* @param {String} type Error name | ||
*/ | ||
_onError(err, type, data, command) { | ||
clearTimeout(this._connectionTimeout); | ||
clearTimeout(this._greetingTimeout); | ||
this.logger.error('[%s] %s', this.id, err.message); | ||
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', err); | ||
this.close(); | ||
}; | ||
err = this._formatError(err, type, data, command); | ||
SMTPConnection.prototype._formatError = function (message, type, response, command) { | ||
var err; | ||
this._log({ | ||
level: 'error', | ||
err | ||
}, err.message); | ||
if (/Error\]$/i.test(Object.prototype.toString.call(message))) { | ||
err = message; | ||
} else { | ||
err = new Error(message); | ||
this.emit('error', err); | ||
this.close(); | ||
} | ||
if (type && type !== 'Error') { | ||
err.code = type; | ||
} | ||
_formatError(message, type, response, command) { | ||
let err; | ||
if (response) { | ||
err.response = response; | ||
err.message += ': ' + response; | ||
} | ||
if (/Error\]$/i.test(Object.prototype.toString.call(message))) { | ||
err = message; | ||
} else { | ||
err = new Error(message); | ||
} | ||
var responseCode = typeof response === 'string' && Number((response.match(/^\d+/) || [])[0]) || false; | ||
if (responseCode) { | ||
err.responseCode = responseCode; | ||
} | ||
if (type && type !== 'Error') { | ||
err.code = type; | ||
} | ||
if (command) { | ||
err.command = command; | ||
} | ||
if (response) { | ||
err.response = response; | ||
err.message += ': ' + response; | ||
} | ||
return err; | ||
}; | ||
let responseCode = typeof response === 'string' && Number((response.match(/^\d+/) || [])[0]) || false; | ||
if (responseCode) { | ||
err.responseCode = responseCode; | ||
} | ||
/** | ||
* 'close' listener for the socket | ||
* | ||
* @event | ||
*/ | ||
SMTPConnection.prototype._onClose = function () { | ||
this.logger.info('[%s] Connection closed', this.id); | ||
if (command) { | ||
err.command = command; | ||
} | ||
if ([this._actionGreeting, this.close].indexOf(this._responseActions[0]) < 0 && !this._destroyed) { | ||
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', false, 'CONN'); | ||
return err; | ||
} | ||
this._destroy(); | ||
}; | ||
/** | ||
* 'close' listener for the socket | ||
* | ||
* @event | ||
*/ | ||
_onClose() { | ||
this._log({ | ||
level: 'info', | ||
tnx: 'network' | ||
}, 'Connection closed'); | ||
/** | ||
* 'end' listener for the socket | ||
* | ||
* @event | ||
*/ | ||
SMTPConnection.prototype._onEnd = function () { | ||
this._destroy(); | ||
}; | ||
if ([this._actionGreeting, this.close].indexOf(this._responseActions[0]) < 0 && !this._destroyed) { | ||
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', false, 'CONN'); | ||
} | ||
/** | ||
* 'timeout' listener for the socket | ||
* | ||
* @event | ||
*/ | ||
SMTPConnection.prototype._onTimeout = function () { | ||
return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN'); | ||
}; | ||
this._destroy(); | ||
} | ||
/** | ||
* Destroys the client, emits 'end' | ||
*/ | ||
SMTPConnection.prototype._destroy = function () { | ||
if (this._destroyed) { | ||
return; | ||
/** | ||
* 'end' listener for the socket | ||
* | ||
* @event | ||
*/ | ||
_onEnd() { | ||
this._destroy(); | ||
} | ||
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 | ||
/** | ||
* 'timeout' listener for the socket | ||
* | ||
* @event | ||
*/ | ||
_onTimeout() { | ||
return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN'); | ||
} | ||
// 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 | ||
/** | ||
* Destroys the client, emits 'end' | ||
*/ | ||
_destroy() { | ||
if (this._destroyed) { | ||
return; | ||
} | ||
this._destroyed = true; | ||
this.emit('end'); | ||
} | ||
var socketPlain = this._socket; | ||
var opts = { | ||
socket: this._socket, | ||
host: this.host | ||
}; | ||
/** | ||
* Upgrades the connection to TLS | ||
* | ||
* @param {Function} callback Callback function to run when the connection | ||
* has been secured | ||
*/ | ||
_upgradeConnection(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 | ||
Object.keys(this.options.tls || {}).forEach(function (key) { | ||
opts[key] = this.options.tls[key]; | ||
}.bind(this)); | ||
// 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 | ||
this._socket = tls.connect(opts, function () { | ||
this.secure = true; | ||
this._socket.on('data', this._onData.bind(this)); | ||
let socketPlain = this._socket; | ||
let opts = { | ||
socket: this._socket, | ||
host: this.host | ||
}; | ||
socketPlain.removeAllListeners('close'); | ||
socketPlain.removeAllListeners('end'); | ||
Object.keys(this.options.tls || {}).forEach(key => { | ||
opts[key] = this.options.tls[key]; | ||
}); | ||
return callback(null, true); | ||
}.bind(this)); | ||
this.upgrading = true; | ||
this._socket = tls.connect(opts, () => { | ||
this.secure = true; | ||
this.upgrading = false; | ||
this._socket.on('data', chunk => this._onData(chunk)); | ||
this._socket.on('error', this._onError.bind(this)); | ||
this._socket.once('close', this._onClose.bind(this)); | ||
this._socket.once('end', this._onEnd.bind(this)); | ||
socketPlain.removeAllListeners('close'); | ||
socketPlain.removeAllListeners('end'); | ||
this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); // 10 min. | ||
this._socket.on('timeout', this._onTimeout.bind(this)); | ||
return callback(null, true); | ||
}); | ||
// resume in case the socket was paused | ||
socketPlain.resume(); | ||
}; | ||
this._socket.on('error', err => this._onError(err)); | ||
this._socket.once('close', errored => this._onClose(errored)); | ||
this._socket.once('end', () => this._onEnd()); | ||
/** | ||
* Processes queued responses from the server | ||
* | ||
* @param {Boolean} force If true, ignores _processing flag | ||
*/ | ||
SMTPConnection.prototype._processResponse = function () { | ||
if (!this._responseQueue.length) { | ||
return false; | ||
} | ||
this._socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT); // 10 min. | ||
this._socket.on('timeout', () => this._onTimeout()); | ||
var str = (this._responseQueue.shift() || '').toString(); | ||
if (/^\d+\-/.test(str.split('\n').pop())) { | ||
// keep waiting for the final part of multiline response | ||
return; | ||
// resume in case the socket was paused | ||
socketPlain.resume(); | ||
} | ||
if (this.options.debug || this.options.transactionLog) { | ||
this.logger.debug('[%s] S: %s', this.id, str.replace(/\r?\n$/, '')); | ||
} | ||
/** | ||
* Processes queued responses from the server | ||
* | ||
* @param {Boolean} force If true, ignores _processing flag | ||
*/ | ||
_processResponse() { | ||
if (!this._responseQueue.length) { | ||
return false; | ||
} | ||
if (!str.trim()) { // skip unexpected empty lines | ||
setImmediate(this._processResponse.bind(this, true)); | ||
} | ||
let str = (this._responseQueue.shift() || '').toString(); | ||
var action = this._responseActions.shift(); | ||
if (/^\d+\-/.test(str.split('\n').pop())) { | ||
// keep waiting for the final part of multiline response | ||
return; | ||
} | ||
if (typeof action === 'function') { | ||
action.call(this, str); | ||
setImmediate(this._processResponse.bind(this, true)); | ||
} else { | ||
return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN'); | ||
} | ||
}; | ||
if (this.options.debug || this.options.transactionLog) { | ||
this._log({ | ||
level: 'debug', | ||
tnx: 'server' | ||
}, str.replace(/\r?\n$/, '')); | ||
} | ||
/** | ||
* 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 (!str.trim()) { // skip unexpected empty lines | ||
setImmediate(() => this._processResponse(true)); | ||
} | ||
if (this._socket.destroyed) { | ||
return this.close(); | ||
} | ||
let action = this._responseActions.shift(); | ||
if (this.options.debug || this.options.transactionLog) { | ||
this.logger.debug('[%s] C: %s', this.id, (str || '').toString().replace(/\r?\n$/, '')); | ||
if (typeof action === 'function') { | ||
action.call(this, str); | ||
setImmediate(() => this._processResponse(true)); | ||
} else { | ||
return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN'); | ||
} | ||
} | ||
this._socket.write(new Buffer(str + '\r\n', 'utf-8')); | ||
}; | ||
/** | ||
* Send a command to the server, append \r\n | ||
* | ||
* @param {String} str String to be sent to the server | ||
*/ | ||
_sendCommand(str) { | ||
if (this._destroyed) { | ||
// Connection already closed, can't send any more data | ||
return; | ||
} | ||
/** | ||
* 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) { | ||
var args = []; | ||
var useSmtpUtf8 = false; | ||
if (this._socket.destroyed) { | ||
return this.close(); | ||
} | ||
this._envelope = envelope || {}; | ||
this._envelope.from = (this._envelope.from && this._envelope.from.address || this._envelope.from || '').toString().trim(); | ||
if (this.options.debug || this.options.transactionLog) { | ||
this._log({ | ||
level: 'debug', | ||
tnx: 'client' | ||
}, (str || '').toString().replace(/\r?\n$/, '')); | ||
} | ||
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', false, 'API')); | ||
this._socket.write(new Buffer(str + '\r\n', 'utf-8')); | ||
} | ||
if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) { | ||
return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE', false, 'API')); | ||
} | ||
/** | ||
* 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:'...']} | ||
*/ | ||
_setEnvelope(envelope, callback) { | ||
let args = []; | ||
let useSmtpUtf8 = false; | ||
// check if the sender address uses only ASCII characters, | ||
// otherwise require usage of SMTPUTF8 extension | ||
if (/[\x80-\uFFFF]/.test(this._envelope.from)) { | ||
useSmtpUtf8 = true; | ||
} | ||
this._envelope = envelope || {}; | ||
this._envelope.from = (this._envelope.from && this._envelope.from.address || this._envelope.from || '').toString().trim(); | ||
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', false, 'API')); | ||
this._envelope.to = [].concat(this._envelope.to || []).map(to => (to && to.address || to || '').toString().trim()); | ||
if (!this._envelope.to.length) { | ||
return callback(this._formatError('No recipients defined', 'EENVELOPE', false, 'API')); | ||
} | ||
// check if the recipients addresses use only ASCII characters, | ||
if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) { | ||
return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE', false, 'API')); | ||
} | ||
// check if the sender address uses only ASCII characters, | ||
// otherwise require usage of SMTPUTF8 extension | ||
if (/[\x80-\uFFFF]/.test(this._envelope.to[i])) { | ||
if (/[\x80-\uFFFF]/.test(this._envelope.from)) { | ||
useSmtpUtf8 = true; | ||
} | ||
} | ||
// clone the recipients array for latter manipulation | ||
this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || [])); | ||
this._envelope.rejected = []; | ||
this._envelope.rejectedErrors = []; | ||
this._envelope.accepted = []; | ||
for (let 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', false, 'API')); | ||
} | ||
if (this._envelope.dsn) { | ||
try { | ||
this._envelope.dsn = this._setDsnEnvelope(this._envelope.dsn); | ||
} catch (err) { | ||
return callback(this._formatError('Invalid dsn ' + err.message, 'EENVELOPE', false, 'API')); | ||
// check if the recipients addresses use only ASCII characters, | ||
// otherwise require usage of SMTPUTF8 extension | ||
if (/[\x80-\uFFFF]/.test(this._envelope.to[i])) { | ||
useSmtpUtf8 = true; | ||
} | ||
} | ||
} | ||
this._responseActions.push(function (str) { | ||
this._actionMAIL(str, callback); | ||
}.bind(this)); | ||
// clone the recipients array for latter manipulation | ||
this._envelope.rcptQueue = JSON.parse(JSON.stringify(this._envelope.to || [])); | ||
this._envelope.rejected = []; | ||
this._envelope.rejectedErrors = []; | ||
this._envelope.accepted = []; | ||
// If the server supports SMTPUTF8 and the envelope includes an internationalized | ||
// email address then append SMTPUTF8 keyword to the MAIL FROM command | ||
if (useSmtpUtf8 && this._supportedExtensions.indexOf('SMTPUTF8') >= 0) { | ||
args.push('SMTPUTF8'); | ||
this._usingSmtpUtf8 = true; | ||
} | ||
if (this._envelope.dsn) { | ||
try { | ||
this._envelope.dsn = this._setDsnEnvelope(this._envelope.dsn); | ||
} catch (err) { | ||
return callback(this._formatError('Invalid dsn ' + err.message, 'EENVELOPE', false, 'API')); | ||
} | ||
} | ||
// If the server supports 8BITMIME and the message might contain non-ascii bytes | ||
// then append the 8BITMIME keyword to the MAIL FROM command | ||
if (this._envelope.use8BitMime && this._supportedExtensions.indexOf('8BITMIME') >= 0) { | ||
args.push('BODY=8BITMIME'); | ||
this._using8BitMime = true; | ||
} | ||
this._responseActions.push(str => { | ||
this._actionMAIL(str, callback); | ||
}); | ||
if (this._envelope.size && this._supportedExtensions.indexOf('SIZE') >= 0) { | ||
args.push('SIZE=' + this._envelope.size); | ||
} | ||
// If the server supports SMTPUTF8 and the envelope includes an internationalized | ||
// email address then append SMTPUTF8 keyword to the MAIL FROM command | ||
if (useSmtpUtf8 && this._supportedExtensions.indexOf('SMTPUTF8') >= 0) { | ||
args.push('SMTPUTF8'); | ||
this._usingSmtpUtf8 = true; | ||
} | ||
// If the server supports DSN and the envelope includes an DSN prop | ||
// then append DSN params to the MAIL FROM command | ||
if (this._envelope.dsn && this._supportedExtensions.indexOf('DSN') >= 0) { | ||
if (this._envelope.dsn.ret) { | ||
args.push('RET=' + this._envelope.dsn.ret); | ||
// If the server supports 8BITMIME and the message might contain non-ascii bytes | ||
// then append the 8BITMIME keyword to the MAIL FROM command | ||
if (this._envelope.use8BitMime && this._supportedExtensions.indexOf('8BITMIME') >= 0) { | ||
args.push('BODY=8BITMIME'); | ||
this._using8BitMime = true; | ||
} | ||
if (this._envelope.dsn.envid) { | ||
args.push('ENVID=' + this._envelope.dsn.envid); | ||
if (this._envelope.size && this._supportedExtensions.indexOf('SIZE') >= 0) { | ||
args.push('SIZE=' + this._envelope.size); | ||
} | ||
} | ||
this._sendCommand('MAIL FROM:<' + (this._envelope.from) + '>' + (args.length ? ' ' + args.join(' ') : '')); | ||
}; | ||
// If the server supports DSN and the envelope includes an DSN prop | ||
// then append DSN params to the MAIL FROM command | ||
if (this._envelope.dsn && this._supportedExtensions.indexOf('DSN') >= 0) { | ||
if (this._envelope.dsn.ret) { | ||
args.push('RET=' + this._envelope.dsn.ret); | ||
} | ||
if (this._envelope.dsn.envid) { | ||
args.push('ENVID=' + this._envelope.dsn.envid); | ||
} | ||
} | ||
SMTPConnection.prototype._setDsnEnvelope = function (params) { | ||
var ret = params.ret ? params.ret.toString().toUpperCase() : null; | ||
if (ret && ['FULL', 'HDRS'].indexOf(ret) < 0) { | ||
throw new Error('ret: ' + JSON.stringify(ret)); | ||
this._sendCommand('MAIL FROM:<' + (this._envelope.from) + '>' + (args.length ? ' ' + args.join(' ') : '')); | ||
} | ||
var envid = params.envid ? params.envid.toString() : null; | ||
var notify = params.notify ? params.notify : null; | ||
if (notify) { | ||
if (typeof notify === 'string') { | ||
notify = notify.split(','); | ||
} | ||
notify = notify.map(function (n) { | ||
return n.trim().toUpperCase(); | ||
}); | ||
var validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY']; | ||
var invaliNotify = notify.filter(function (n) { | ||
return validNotify.indexOf(n) === -1; | ||
}); | ||
if (invaliNotify.length || (notify.length > 1 && notify.indexOf('NEVER') >= 0)) { | ||
throw new Error('notify: ' + JSON.stringify(notify.join(','))); | ||
} | ||
notify = notify.join(','); | ||
} | ||
var orcpt = params.orcpt ? params.orcpt.toString() : null; | ||
return { | ||
ret: ret, | ||
envid: envid, | ||
notify: notify, | ||
orcpt: orcpt | ||
}; | ||
}; | ||
SMTPConnection.prototype._getDsnRcptToArgs = function () { | ||
var args = []; | ||
// If the server supports DSN and the envelope includes an DSN prop | ||
// then append DSN params to the RCPT TO command | ||
if (this._envelope.dsn && this._supportedExtensions.indexOf('DSN') >= 0) { | ||
if (this._envelope.dsn.notify) { | ||
args.push('NOTIFY=' + this._envelope.dsn.notify); | ||
_setDsnEnvelope(params) { | ||
let ret = params.ret ? params.ret.toString().toUpperCase() : null; | ||
if (ret && ['FULL', 'HDRS'].indexOf(ret) < 0) { | ||
throw new Error('ret: ' + JSON.stringify(ret)); | ||
} | ||
if (this._envelope.dsn.orcpt) { | ||
args.push('ORCPT=' + this._envelope.dsn.orcpt); | ||
let envid = params.envid ? params.envid.toString() : null; | ||
let notify = params.notify ? params.notify : null; | ||
if (notify) { | ||
if (typeof notify === 'string') { | ||
notify = notify.split(','); | ||
} | ||
notify = notify.map(n => n.trim().toUpperCase()); | ||
let validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY']; | ||
let invaliNotify = notify.filter(n => !validNotify.includes(n)); | ||
if (invaliNotify.length || (notify.length > 1 && notify.indexOf('NEVER') >= 0)) { | ||
throw new Error('notify: ' + JSON.stringify(notify.join(','))); | ||
} | ||
notify = notify.join(','); | ||
} | ||
let orcpt = params.orcpt ? params.orcpt.toString() : null; | ||
return { | ||
ret, | ||
envid, | ||
notify, | ||
orcpt | ||
}; | ||
} | ||
return (args.length ? ' ' + args.join(' ') : ''); | ||
}; | ||
SMTPConnection.prototype._createSendStream = function (callback) { | ||
var dataStream = new DataStream(); | ||
var logStream; | ||
if (this.options.lmtp) { | ||
this._envelope.accepted.forEach(function (recipient, i) { | ||
var final = i === this._envelope.accepted.length - 1; | ||
this._responseActions.push(function (str) { | ||
this._actionLMTPStream(recipient, final, str, callback); | ||
}.bind(this)); | ||
}.bind(this)); | ||
} else { | ||
this._responseActions.push(function (str) { | ||
this._actionSMTPStream(str, callback); | ||
}.bind(this)); | ||
} | ||
dataStream.pipe(this._socket, { | ||
end: false | ||
}); | ||
if (this.options.debug) { | ||
logStream = new PassThrough(); | ||
logStream.on('readable', function () { | ||
var chunk; | ||
while ((chunk = logStream.read())) { | ||
this.logger.debug('[%s] C: %s', this.id, chunk.toString('binary').replace(/\r?\n$/, '')); | ||
_getDsnRcptToArgs() { | ||
let args = []; | ||
// If the server supports DSN and the envelope includes an DSN prop | ||
// then append DSN params to the RCPT TO command | ||
if (this._envelope.dsn && this._supportedExtensions.indexOf('DSN') >= 0) { | ||
if (this._envelope.dsn.notify) { | ||
args.push('NOTIFY=' + this._envelope.dsn.notify); | ||
} | ||
}.bind(this)); | ||
dataStream.pipe(logStream); | ||
if (this._envelope.dsn.orcpt) { | ||
args.push('ORCPT=' + this._envelope.dsn.orcpt); | ||
} | ||
} | ||
return (args.length ? ' ' + args.join(' ') : ''); | ||
} | ||
dataStream.once('end', function () { | ||
this.logger.info('[%s] C: <%s bytes encoded mime message (source size %s bytes)>', this.id, dataStream.outByteCount, dataStream.inByteCount); | ||
}.bind(this)); | ||
_createSendStream(callback) { | ||
let dataStream = new DataStream(); | ||
let logStream; | ||
return dataStream; | ||
}; | ||
if (this.options.lmtp) { | ||
this._envelope.accepted.forEach((recipient, i) => { | ||
let final = i === this._envelope.accepted.length - 1; | ||
this._responseActions.push(str => { | ||
this._actionLMTPStream(recipient, final, str, callback); | ||
}); | ||
}); | ||
} else { | ||
this._responseActions.push(str => { | ||
this._actionSMTPStream(str, callback); | ||
}); | ||
} | ||
/** ACTIONS **/ | ||
dataStream.pipe(this._socket, { | ||
end: false | ||
}); | ||
/** | ||
* 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 (this.options.debug) { | ||
logStream = new PassThrough(); | ||
logStream.on('readable', () => { | ||
let chunk; | ||
while ((chunk = logStream.read())) { | ||
this._log({ | ||
level: 'debug', | ||
tnx: 'message' | ||
}, chunk.toString('binary').replace(/\r?\n$/, '')); | ||
} | ||
}); | ||
dataStream.pipe(logStream); | ||
} | ||
if (str.substr(0, 3) !== '220') { | ||
this._onError(new Error('Invalid greeting from server:\n' + str), 'EPROTOCOL', str, 'CONN'); | ||
return; | ||
} | ||
dataStream.once('end', () => { | ||
this._log({ | ||
level: 'info', | ||
tnx: 'message', | ||
inByteCount: dataStream.inByteCount, | ||
outByteCount: dataStream.outByteCount | ||
}, '<%s bytes encoded mime message (source size %s bytes)>', dataStream.outByteCount, dataStream.inByteCount); | ||
}); | ||
if (this.options.lmtp) { | ||
this._responseActions.push(this._actionLHLO); | ||
this._sendCommand('LHLO ' + this.name); | ||
} else { | ||
this._responseActions.push(this._actionEHLO); | ||
this._sendCommand('EHLO ' + this.name); | ||
return dataStream; | ||
} | ||
}; | ||
/** | ||
* 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, 'LHLO'); | ||
return; | ||
} | ||
/** ACTIONS **/ | ||
this._actionEHLO(str); | ||
}; | ||
/** | ||
* 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 | ||
*/ | ||
_actionGreeting(str) { | ||
clearTimeout(this._greetingTimeout); | ||
/** | ||
* 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) { | ||
var match; | ||
if (str.substr(0, 3) !== '220') { | ||
this._onError(new Error('Invalid greeting from server:\n' + str), 'EPROTOCOL', str, 'CONN'); | ||
return; | ||
} | ||
if (str.substr(0, 3) === '421') { | ||
this._onError(new Error('Server terminates connection:\n' + str), 'ECONNECTION', str, 'EHLO'); | ||
return; | ||
if (this.options.lmtp) { | ||
this._responseActions.push(this._actionLHLO); | ||
this._sendCommand('LHLO ' + this.name); | ||
} else { | ||
this._responseActions.push(this._actionEHLO); | ||
this._sendCommand('EHLO ' + this.name); | ||
} | ||
} | ||
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, 'EHLO'); | ||
/** | ||
* 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 | ||
*/ | ||
_actionLHLO(str) { | ||
if (str.charAt(0) !== '2') { | ||
this._onError(new Error('Invalid response for LHLO:\n' + str), 'EPROTOCOL', str, 'LHLO'); | ||
return; | ||
} | ||
// Try HELO instead | ||
this._responseActions.push(this._actionHELO); | ||
this._sendCommand('HELO ' + this.name); | ||
return; | ||
this._actionEHLO(str); | ||
} | ||
// Detect if the server supports STARTTLS | ||
if (!this.secure && !this.options.ignoreTLS && (/[ \-]STARTTLS\b/mi.test(str) || this.options.requireTLS)) { | ||
this._sendCommand('STARTTLS'); | ||
this._responseActions.push(this._actionSTARTTLS); | ||
return; | ||
} | ||
/** | ||
* 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 | ||
*/ | ||
_actionEHLO(str) { | ||
let match; | ||
// Detect if the server supports SMTPUTF8 | ||
if (/[ \-]SMTPUTF8\b/mi.test(str)) { | ||
this._supportedExtensions.push('SMTPUTF8'); | ||
} | ||
if (str.substr(0, 3) === '421') { | ||
this._onError(new Error('Server terminates connection:\n' + str), 'ECONNECTION', str, 'EHLO'); | ||
return; | ||
} | ||
// Detect if the server supports DSN | ||
if (/[ \-]DSN\b/mi.test(str)) { | ||
this._supportedExtensions.push('DSN'); | ||
} | ||
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, 'EHLO'); | ||
return; | ||
} | ||
// Detect if the server supports 8BITMIME | ||
if (/[ \-]8BITMIME\b/mi.test(str)) { | ||
this._supportedExtensions.push('8BITMIME'); | ||
} | ||
// Try HELO instead | ||
this._responseActions.push(this._actionHELO); | ||
this._sendCommand('HELO ' + this.name); | ||
return; | ||
} | ||
// Detect if the server supports PIPELINING | ||
if (/[ \-]PIPELINING\b/mi.test(str)) { | ||
this._supportedExtensions.push('PIPELINING'); | ||
} | ||
// Detect if the server supports STARTTLS | ||
if (!this.secure && !this.options.ignoreTLS && (/[ \-]STARTTLS\b/mi.test(str) || this.options.requireTLS)) { | ||
this._sendCommand('STARTTLS'); | ||
this._responseActions.push(this._actionSTARTTLS); | ||
return; | ||
} | ||
// Detect if the server supports PLAIN auth | ||
if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i.test(str)) { | ||
this._supportedAuth.push('PLAIN'); | ||
} | ||
// Detect if the server supports SMTPUTF8 | ||
if (/[ \-]SMTPUTF8\b/mi.test(str)) { | ||
this._supportedExtensions.push('SMTPUTF8'); | ||
} | ||
// Detect if the server supports LOGIN auth | ||
if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i.test(str)) { | ||
this._supportedAuth.push('LOGIN'); | ||
} | ||
// Detect if the server supports DSN | ||
if (/[ \-]DSN\b/mi.test(str)) { | ||
this._supportedExtensions.push('DSN'); | ||
} | ||
// Detect if the server supports CRAM-MD5 auth | ||
if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i.test(str)) { | ||
this._supportedAuth.push('CRAM-MD5'); | ||
} | ||
// Detect if the server supports 8BITMIME | ||
if (/[ \-]8BITMIME\b/mi.test(str)) { | ||
this._supportedExtensions.push('8BITMIME'); | ||
} | ||
// Detect if the server supports XOAUTH2 auth | ||
if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i.test(str)) { | ||
this._supportedAuth.push('XOAUTH2'); | ||
} | ||
// Detect if the server supports PIPELINING | ||
if (/[ \-]PIPELINING\b/mi.test(str)) { | ||
this._supportedExtensions.push('PIPELINING'); | ||
} | ||
// Detect if the server supports SIZE extensions (and the max allowed size) | ||
if ((match = str.match(/[ \-]SIZE(?:[ \t]+(\d+))?/mi))) { | ||
this._supportedExtensions.push('SIZE'); | ||
this._maxAllowedSize = Number(match[1]) || 0; | ||
} | ||
// Detect if the server supports PLAIN auth | ||
if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i.test(str)) { | ||
this._supportedAuth.push('PLAIN'); | ||
} | ||
this.emit('connect'); | ||
}; | ||
// Detect if the server supports LOGIN auth | ||
if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i.test(str)) { | ||
this._supportedAuth.push('LOGIN'); | ||
} | ||
/** | ||
* 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, 'HELO'); | ||
return; | ||
} | ||
// Detect if the server supports CRAM-MD5 auth | ||
if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i.test(str)) { | ||
this._supportedAuth.push('CRAM-MD5'); | ||
} | ||
this.emit('connect'); | ||
}; | ||
// Detect if the server supports XOAUTH2 auth | ||
if (/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i.test(str)) { | ||
this._supportedAuth.push('XOAUTH2'); | ||
} | ||
/** | ||
* 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') { | ||
if (this.options.opportunisticTLS) { | ||
this.logger.info('[%s] Failed STARTTLS upgrade, continuing unencrypted', this.id); | ||
return this.emit('connect'); | ||
// Detect if the server supports SIZE extensions (and the max allowed size) | ||
if ((match = str.match(/[ \-]SIZE(?:[ \t]+(\d+))?/mi))) { | ||
this._supportedExtensions.push('SIZE'); | ||
this._maxAllowedSize = Number(match[1]) || 0; | ||
} | ||
this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str, 'STARTTLS'); | ||
return; | ||
this.emit('connect'); | ||
} | ||
this._upgradeConnection(function (err, secured) { | ||
if (err) { | ||
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'STARTTLS'); | ||
/** | ||
* 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 | ||
*/ | ||
_actionHELO(str) { | ||
if (str.charAt(0) !== '2') { | ||
this._onError(new Error('Invalid response for EHLO/HELO:\n' + str), 'EPROTOCOL', str, 'HELO'); | ||
return; | ||
} | ||
this.logger.info('[%s] Connection upgraded with STARTTLS', this.id); | ||
this.emit('connect'); | ||
} | ||
if (secured) { | ||
// restart session | ||
this._responseActions.push(this._actionEHLO); | ||
this._sendCommand('EHLO ' + this.name); | ||
} else { | ||
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 | ||
*/ | ||
_actionSTARTTLS(str) { | ||
if (str.charAt(0) !== '2') { | ||
if (this.options.opportunisticTLS) { | ||
this._log({ | ||
level: 'info', | ||
tnx: 'smtp' | ||
}, 'Failed STARTTLS upgrade, continuing unencrypted'); | ||
return this.emit('connect'); | ||
} | ||
this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str, 'STARTTLS'); | ||
return; | ||
} | ||
}.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, 'AUTH LOGIN')); | ||
return; | ||
} | ||
this._upgradeConnection((err, secured) => { | ||
if (err) { | ||
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'STARTTLS'); | ||
return; | ||
} | ||
this._responseActions.push(function (str) { | ||
this._actionAUTH_LOGIN_PASS(str, callback); | ||
}.bind(this)); | ||
this._log({ | ||
level: 'info', | ||
tnx: 'smtp' | ||
}, 'Connection upgraded with STARTTLS'); | ||
this._sendCommand(new Buffer(this._auth.user + '', 'utf-8').toString('base64')); | ||
}; | ||
if (secured) { | ||
// restart session | ||
this._responseActions.push(this._actionEHLO); | ||
this._sendCommand('EHLO ' + this.name); | ||
} else { | ||
this.emit('connect'); | ||
} | ||
}); | ||
} | ||
/** | ||
* Handle the response for AUTH NTLM, which should be a | ||
* '334 <challenge string>'. See http://davenport.sourceforge.net/ntlm.html | ||
* We already sent the Type1 message, the challenge is a Type2 message, we | ||
* need to respond with a Type3 message. | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
SMTPConnection.prototype._actionAUTH_NTLM_TYPE1 = function (str, callback) { | ||
var challengeMatch = str.match(/^334\s+(.+)$/); | ||
var challengeString = ''; | ||
/** | ||
* 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 | ||
*/ | ||
_actionAUTH_LOGIN_USER(str, callback) { | ||
if (str !== '334 VXNlcm5hbWU6') { | ||
callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str, 'AUTH LOGIN')); | ||
return; | ||
} | ||
if (!challengeMatch) { | ||
return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH NTLM')); | ||
} else { | ||
challengeString = challengeMatch[1]; | ||
} | ||
this._responseActions.push(str => { | ||
this._actionAUTH_LOGIN_PASS(str, callback); | ||
}); | ||
if (!/^NTLM/i.test(challengeString)) { | ||
challengeString = 'NTLM ' + challengeString; | ||
this._sendCommand(new Buffer(this._auth.user + '', 'utf-8').toString('base64')); | ||
} | ||
var type2Message = ntlm.parseType2Message(challengeString, callback); | ||
if (!type2Message) { | ||
return; | ||
} | ||
/** | ||
* Handle the response for AUTH NTLM, which should be a | ||
* '334 <challenge string>'. See http://davenport.sourceforge.net/ntlm.html | ||
* We already sent the Type1 message, the challenge is a Type2 message, we | ||
* need to respond with a Type3 message. | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
_actionAUTH_NTLM_TYPE1(str, callback) { | ||
let challengeMatch = str.match(/^334\s+(.+)$/); | ||
let challengeString = ''; | ||
var type3Message = ntlm.createType3Message(type2Message, { | ||
domain: this._auth.domain || '', | ||
workstation: this._auth.workstation || '', | ||
username: this._auth.user, | ||
password: this._auth.pass | ||
}); | ||
if (!challengeMatch) { | ||
return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH NTLM')); | ||
} else { | ||
challengeString = challengeMatch[1]; | ||
} | ||
type3Message = type3Message.substring(5); // remove the "NTLM " prefix | ||
if (!/^NTLM/i.test(challengeString)) { | ||
challengeString = 'NTLM ' + challengeString; | ||
} | ||
this._responseActions.push(function (str) { | ||
this._actionAUTH_NTLM_TYPE3(str, callback); | ||
}.bind(this)); | ||
let type2Message = ntlm.parseType2Message(challengeString, callback); | ||
if (!type2Message) { | ||
return; | ||
} | ||
this._sendCommand(type3Message); | ||
}; | ||
let type3Message = ntlm.createType3Message(type2Message, { | ||
domain: this._auth.domain || '', | ||
workstation: this._auth.workstation || '', | ||
username: this._auth.user, | ||
password: this._auth.pass | ||
}); | ||
/** | ||
* 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 = ''; | ||
type3Message = type3Message.substring(5); // remove the "NTLM " prefix | ||
if (!challengeMatch) { | ||
return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5')); | ||
} else { | ||
challengeString = challengeMatch[1]; | ||
this._responseActions.push(str => { | ||
this._actionAUTH_NTLM_TYPE3(str, callback); | ||
}); | ||
this._sendCommand(type3Message); | ||
} | ||
// Decode from base64 | ||
var base64decoded = new Buffer(challengeString, 'base64').toString('ascii'), | ||
hmac_md5 = crypto.createHmac('md5', this._auth.pass); | ||
/** | ||
* 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 | ||
*/ | ||
_actionAUTH_CRAM_MD5(str, callback) { | ||
let challengeMatch = str.match(/^334\s+(.+)$/); | ||
let challengeString = ''; | ||
hmac_md5.update(base64decoded); | ||
if (!challengeMatch) { | ||
return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5')); | ||
} else { | ||
challengeString = challengeMatch[1]; | ||
} | ||
var hex_hmac = hmac_md5.digest('hex'), | ||
prepended = this._auth.user + ' ' + hex_hmac; | ||
// Decode from base64 | ||
let base64decoded = new Buffer(challengeString, 'base64').toString('ascii'), | ||
hmac_md5 = crypto.createHmac('md5', this._auth.pass); | ||
this._responseActions.push(function (str) { | ||
this._actionAUTH_CRAM_MD5_PASS(str, callback); | ||
}.bind(this)); | ||
hmac_md5.update(base64decoded); | ||
let hex_hmac = hmac_md5.digest('hex'), | ||
prepended = this._auth.user + ' ' + hex_hmac; | ||
this._sendCommand(new Buffer(prepended).toString('base64')); | ||
}; | ||
this._responseActions.push(str => { | ||
this._actionAUTH_CRAM_MD5_PASS(str, callback); | ||
}); | ||
/** | ||
* Handles the response to CRAM-MD5 authentication, if there's no error, | ||
* the user can be considered logged in. 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, 'AUTH CRAM-MD5')); | ||
this._sendCommand(new Buffer(prepended).toString('base64')); | ||
} | ||
this.logger.info('[%s] User %s authenticated', this.id, JSON.stringify(this._user)); | ||
this.authenticated = true; | ||
callback(null, true); | ||
}; | ||
/** | ||
* Handles the response to CRAM-MD5 authentication, if there's no error, | ||
* the user can be considered logged in. Start waiting for a message to send | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
_actionAUTH_CRAM_MD5_PASS(str, callback) { | ||
if (!str.match(/^235\s+/)) { | ||
return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH CRAM-MD5')); | ||
} | ||
/** | ||
* Handles the TYPE3 response for NTLM authentication, if there's no error, | ||
* the user can be considered logged in. Start waiting for a message to send | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
SMTPConnection.prototype._actionAUTH_NTLM_TYPE3 = function (str, callback) { | ||
if (!str.match(/^235\s+/)) { | ||
return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH NTLM')); | ||
this._log({ | ||
level: 'info', | ||
tnx: 'smtp', | ||
user: this._user, | ||
method: this._authMethod | ||
}, 'User %s authenticated', JSON.stringify(this._user)); | ||
this.authenticated = true; | ||
callback(null, true); | ||
} | ||
this.logger.info('[%s] User %s authenticated', this.id, JSON.stringify(this._user)); | ||
this.authenticated = true; | ||
callback(null, true); | ||
}; | ||
/** | ||
* Handles the TYPE3 response for NTLM authentication, if there's no error, | ||
* the user can be considered logged in. Start waiting for a message to send | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
_actionAUTH_NTLM_TYPE3(str, callback) { | ||
if (!str.match(/^235\s+/)) { | ||
return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH NTLM')); | ||
} | ||
/** | ||
* 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, 'AUTH LOGIN')); | ||
this._log({ | ||
level: 'info', | ||
tnx: 'smtp', | ||
user: this._user, | ||
method: this._authMethod | ||
}, 'User %s authenticated', JSON.stringify(this._user)); | ||
this.authenticated = true; | ||
callback(null, true); | ||
} | ||
this._responseActions.push(function (str) { | ||
this._actionAUTHComplete(str, callback); | ||
}.bind(this)); | ||
/** | ||
* 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 | ||
*/ | ||
_actionAUTH_LOGIN_PASS(str, callback) { | ||
if (str !== '334 UGFzc3dvcmQ6') { | ||
return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str, 'AUTH LOGIN')); | ||
} | ||
this._sendCommand(new Buffer(this._auth.pass + '', 'utf-8').toString('base64')); | ||
}; | ||
this._responseActions.push(str => { | ||
this._actionAUTHComplete(str, callback); | ||
}); | ||
/** | ||
* Handles the response for authentication, if there's no error, | ||
* the user can be considered logged in. 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; | ||
this._sendCommand(new Buffer(this._auth.pass + '', 'utf-8').toString('base64')); | ||
} | ||
if (str.substr(0, 3) === '334') { | ||
this._responseActions.push(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; | ||
} | ||
/** | ||
* Handles the response for authentication, if there's no error, | ||
* the user can be considered logged in. Start waiting for a message to send | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
_actionAUTHComplete(str, isRetry, callback) { | ||
if (!callback && typeof isRetry === 'function') { | ||
callback = isRetry; | ||
isRetry = false; | ||
} | ||
if (str.charAt(0) !== '2') { | ||
this.logger.info('[%s] User %s failed to authenticate', this.id, JSON.stringify(this._user)); | ||
return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod)); | ||
if (str.substr(0, 3) === '334') { | ||
this._responseActions.push(str => { | ||
if (isRetry || !this._auth.xoauth2 || typeof this._auth.xoauth2 !== 'object') { | ||
this._actionAUTHComplete(str, true, callback); | ||
} else { | ||
setTimeout(() => this._handleXOauth2Token(true, callback), Math.random() * 4000 + 1000); | ||
} | ||
}); | ||
this._sendCommand(''); | ||
return; | ||
} | ||
if (str.charAt(0) !== '2') { | ||
this._log({ | ||
level: 'info', | ||
tnx: 'smtp' | ||
}, 'User %s failed to authenticate', JSON.stringify(this._user)); | ||
return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod)); | ||
} | ||
this._log({ | ||
level: 'info', | ||
tnx: 'smtp', | ||
user: this._user, | ||
method: this._authMethod | ||
}, 'User %s authenticated', JSON.stringify(this._user)); | ||
this.authenticated = true; | ||
callback(null, true); | ||
} | ||
this.logger.info('[%s] User %s authenticated', this.id, JSON.stringify(this._user)); | ||
this.authenticated = true; | ||
callback(null, true); | ||
}; | ||
/** | ||
* Handle response for a MAIL FROM: command | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
_actionMAIL(str, callback) { | ||
let message, curRecipient; | ||
if (Number(str.charAt(0)) !== 2) { | ||
if (this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.from)) { | ||
message = 'Internationalized mailbox name not allowed'; | ||
} else { | ||
message = 'Mail command failed'; | ||
} | ||
return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM')); | ||
} | ||
/** | ||
* Handle response for a MAIL FROM: command | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
SMTPConnection.prototype._actionMAIL = function (str, callback) { | ||
var message, curRecipient; | ||
if (Number(str.charAt(0)) !== 2) { | ||
if (this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.from)) { | ||
message = 'Internationalized mailbox name not allowed'; | ||
if (!this._envelope.rcptQueue.length) { | ||
return callback(this._formatError('Can\'t send mail - no recipients defined', 'EENVELOPE', false, 'API')); | ||
} else { | ||
message = 'Mail command failed'; | ||
} | ||
return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM')); | ||
} | ||
this._recipientQueue = []; | ||
if (!this._envelope.rcptQueue.length) { | ||
return callback(this._formatError('Can\'t send mail - no recipients defined', 'EENVELOPE', false, 'API')); | ||
} else { | ||
this._recipientQueue = []; | ||
if (this._supportedExtensions.indexOf('PIPELINING') >= 0) { | ||
while (this._envelope.rcptQueue.length) { | ||
if (this._supportedExtensions.indexOf('PIPELINING') >= 0) { | ||
while (this._envelope.rcptQueue.length) { | ||
curRecipient = this._envelope.rcptQueue.shift(); | ||
this._recipientQueue.push(curRecipient); | ||
this._responseActions.push(str => { | ||
this._actionRCPT(str, callback); | ||
}); | ||
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs()); | ||
} | ||
} else { | ||
curRecipient = this._envelope.rcptQueue.shift(); | ||
this._recipientQueue.push(curRecipient); | ||
this._responseActions.push(function (str) { | ||
this._responseActions.push(str => { | ||
this._actionRCPT(str, callback); | ||
}.bind(this)); | ||
}); | ||
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs()); | ||
} | ||
} | ||
} | ||
/** | ||
* Handle response for a RCPT TO: command | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
_actionRCPT(str, callback) { | ||
let message, err, curRecipient = this._recipientQueue.shift(); | ||
if (Number(str.charAt(0)) !== 2) { | ||
// this is a soft error | ||
if (this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)) { | ||
message = 'Internationalized mailbox name not allowed'; | ||
} else { | ||
message = 'Recipient command failed'; | ||
} | ||
this._envelope.rejected.push(curRecipient); | ||
// store error for the failed recipient | ||
err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO'); | ||
err.recipient = curRecipient; | ||
this._envelope.rejectedErrors.push(err); | ||
} else { | ||
this._envelope.accepted.push(curRecipient); | ||
} | ||
if (!this._envelope.rcptQueue.length && !this._recipientQueue.length) { | ||
if (this._envelope.rejected.length < this._envelope.to.length) { | ||
this._responseActions.push(str => { | ||
this._actionDATA(str, callback); | ||
}); | ||
this._sendCommand('DATA'); | ||
} else { | ||
err = this._formatError('Can\'t send mail - all recipients were rejected', 'EENVELOPE', str, 'RCPT TO'); | ||
err.rejected = this._envelope.rejected; | ||
err.rejectedErrors = this._envelope.rejectedErrors; | ||
return callback(err); | ||
} | ||
} else if (this._envelope.rcptQueue.length) { | ||
curRecipient = this._envelope.rcptQueue.shift(); | ||
this._recipientQueue.push(curRecipient); | ||
this._responseActions.push(function (str) { | ||
this._responseActions.push(str => { | ||
this._actionRCPT(str, callback); | ||
}.bind(this)); | ||
}); | ||
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs()); | ||
} | ||
} | ||
}; | ||
/** | ||
* Handle response for a RCPT TO: command | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
SMTPConnection.prototype._actionRCPT = function (str, callback) { | ||
var message, err, curRecipient = this._recipientQueue.shift(); | ||
if (Number(str.charAt(0)) !== 2) { | ||
// this is a soft error | ||
if (this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(curRecipient)) { | ||
message = 'Internationalized mailbox name not allowed'; | ||
} else { | ||
message = 'Recipient command failed'; | ||
/** | ||
* Handle response for a DATA command | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
_actionDATA(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, 'DATA')); | ||
} | ||
this._envelope.rejected.push(curRecipient); | ||
// store error for the failed recipient | ||
err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO'); | ||
err.recipient = curRecipient; | ||
this._envelope.rejectedErrors.push(err); | ||
} else { | ||
this._envelope.accepted.push(curRecipient); | ||
let response = { | ||
accepted: this._envelope.accepted, | ||
rejected: this._envelope.rejected | ||
}; | ||
if (this._envelope.rejectedErrors.length) { | ||
response.rejectedErrors = this._envelope.rejectedErrors; | ||
} | ||
callback(null, response); | ||
} | ||
if (!this._envelope.rcptQueue.length && !this._recipientQueue.length) { | ||
if (this._envelope.rejected.length < this._envelope.to.length) { | ||
this._responseActions.push(function (str) { | ||
this._actionDATA(str, callback); | ||
}.bind(this)); | ||
this._sendCommand('DATA'); | ||
/** | ||
* Handle response for a DATA stream when using SMTP | ||
* We expect a single response that defines if the sending succeeded or failed | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
_actionSMTPStream(str, callback) { | ||
if (Number(str.charAt(0)) !== 2) { | ||
// Message failed | ||
return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA')); | ||
} else { | ||
err = this._formatError('Can\'t send mail - all recipients were rejected', 'EENVELOPE', str, 'RCPT TO'); | ||
err.rejected = this._envelope.rejected; | ||
err.rejectedErrors = this._envelope.rejectedErrors; | ||
return callback(err); | ||
// Message sent succesfully | ||
return callback(null, str); | ||
} | ||
} else if (this._envelope.rcptQueue.length) { | ||
curRecipient = this._envelope.rcptQueue.shift(); | ||
this._recipientQueue.push(curRecipient); | ||
this._responseActions.push(function (str) { | ||
this._actionRCPT(str, callback); | ||
}.bind(this)); | ||
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs()); | ||
} | ||
}; | ||
/** | ||
* 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, 'DATA')); | ||
/** | ||
* Handle response for a DATA stream | ||
* We expect a separate response for every recipient. All recipients can either | ||
* succeed or fail separately | ||
* | ||
* @param {String} recipient The recipient this response applies to | ||
* @param {Boolean} final Is this the final recipient? | ||
* @param {String} str Message from the server | ||
*/ | ||
_actionLMTPStream(recipient, final, str, callback) { | ||
let err; | ||
if (Number(str.charAt(0)) !== 2) { | ||
// Message failed | ||
err = this._formatError('Message failed for recipient ' + recipient, 'EMESSAGE', str, 'DATA'); | ||
err.recipient = recipient; | ||
this._envelope.rejected.push(recipient); | ||
this._envelope.rejectedErrors.push(err); | ||
for (let i = 0, len = this._envelope.accepted.length; i < len; i++) { | ||
if (this._envelope.accepted[i] === recipient) { | ||
this._envelope.accepted.splice(i, 1); | ||
} | ||
} | ||
} | ||
if (final) { | ||
return callback(null, str); | ||
} | ||
} | ||
var response = { | ||
accepted: this._envelope.accepted, | ||
rejected: this._envelope.rejected | ||
}; | ||
_handleXOauth2Token(isRetry, callback) { | ||
this._responseActions.push(str => { | ||
this._actionAUTHComplete(str, isRetry, callback); | ||
}); | ||
if (this._envelope.rejectedErrors.length) { | ||
response.rejectedErrors = this._envelope.rejectedErrors; | ||
if (this._auth.xoauth2 && typeof this._auth.xoauth2 === 'object') { | ||
this._auth.xoauth2[isRetry ? 'generateToken' : 'getToken']((err, token) => { | ||
if (err) { | ||
this._log({ | ||
level: 'info', | ||
tnx: 'smtp' | ||
}, 'User %s failed to authenticate', JSON.stringify(this._user)); | ||
return callback(this._formatError(err, 'EAUTH', false, 'AUTH XOAUTH2')); | ||
} | ||
this._sendCommand('AUTH XOAUTH2 ' + token); | ||
}); | ||
} else { | ||
this._sendCommand('AUTH XOAUTH2 ' + this._buildXOAuth2Token(this._auth.user, this._auth.xoauth2)); | ||
} | ||
} | ||
callback(null, response); | ||
}; | ||
/** | ||
* Handle response for a DATA stream when using SMTP | ||
* We expect a single response that defines if the sending succeeded or failed | ||
* | ||
* @param {String} str Message from the server | ||
*/ | ||
SMTPConnection.prototype._actionSMTPStream = function (str, callback) { | ||
if (Number(str.charAt(0)) !== 2) { | ||
// Message failed | ||
return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA')); | ||
} else { | ||
// Message sent succesfully | ||
return callback(null, str); | ||
/** | ||
* 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 | ||
*/ | ||
_buildXOAuth2Token(user, token) { | ||
let authData = [ | ||
'user=' + (user || ''), | ||
'auth=Bearer ' + token, | ||
'', | ||
'' | ||
]; | ||
return new Buffer(authData.join('\x01')).toString('base64'); | ||
} | ||
}; | ||
/** | ||
* Handle response for a DATA stream | ||
* We expect a separate response for every recipient. All recipients can either | ||
* succeed or fail separately | ||
* | ||
* @param {String} recipient The recipient this response applies to | ||
* @param {Boolean} final Is this the final recipient? | ||
* @param {String} str Message from the server | ||
*/ | ||
SMTPConnection.prototype._actionLMTPStream = function (recipient, final, str, callback) { | ||
var err; | ||
if (Number(str.charAt(0)) !== 2) { | ||
// Message failed | ||
err = this._formatError('Message failed for recipient ' + recipient, 'EMESSAGE', str, 'DATA'); | ||
err.recipient = recipient; | ||
this._envelope.rejected.push(recipient); | ||
this._envelope.rejectedErrors.push(err); | ||
for (var i = 0, len = this._envelope.accepted.length; i < len; i++) { | ||
if (this._envelope.accepted[i] === recipient) { | ||
this._envelope.accepted.splice(i, 1); | ||
} | ||
_getHostname() { | ||
// defaul hostname is machine hostname or [IP] | ||
let defaultHostname = os.hostname() || ''; | ||
// ignore if not FQDN | ||
if (defaultHostname.indexOf('.') < 0) { | ||
defaultHostname = '[127.0.0.1]'; | ||
} | ||
} | ||
if (final) { | ||
return callback(null, str); | ||
} | ||
}; | ||
SMTPConnection.prototype._handleXOauth2Token = function (isRetry, callback) { | ||
this._responseActions.push(function (str) { | ||
this._actionAUTHComplete(str, isRetry, callback); | ||
}.bind(this)); | ||
// IP should be enclosed in [] | ||
if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) { | ||
defaultHostname = '[' + defaultHostname + ']'; | ||
} | ||
if (this._auth.xoauth2 && typeof this._auth.xoauth2 === 'object') { | ||
this._auth.xoauth2[isRetry ? 'generateToken' : 'getToken'](function (err, token) { | ||
if (err) { | ||
this.logger.info('[%s] User %s failed to authenticate', this.id, JSON.stringify(this._user)); | ||
return callback(this._formatError(err, 'EAUTH', false, 'AUTH XOAUTH2')); | ||
} | ||
this._sendCommand('AUTH XOAUTH2 ' + token); | ||
}.bind(this)); | ||
} else { | ||
this._sendCommand('AUTH XOAUTH2 ' + this._buildXOAuth2Token(this._auth.user, this._auth.xoauth2)); | ||
return defaultHostname; | ||
} | ||
}; | ||
/** | ||
* 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'); | ||
}; | ||
_log(data, message, ...args) { | ||
let level = 'debug'; | ||
let entry = { | ||
component: this.component, | ||
sid: this.id | ||
}; | ||
if (typeof data !== 'object' || !data) { | ||
level = (data || '').toString().toLowerCase().trim() || level; | ||
} else { | ||
Object.keys(data || {}).forEach(key => { | ||
if (key === 'level') { | ||
level = (data[key] || '').toString().toLowerCase().trim() || level; | ||
} else { | ||
entry[key] = data[key]; | ||
} | ||
}); | ||
} | ||
SMTPConnection.prototype._getHostname = function () { | ||
// defaul hostname is machine hostname or [IP] | ||
var defaultHostname = os.hostname() || ''; | ||
if (typeof this.logger[level] !== 'function') { | ||
level = 'debug'; | ||
} | ||
// ignore if not FQDN | ||
if (defaultHostname.indexOf('.') < 0) { | ||
defaultHostname = '[127.0.0.1]'; | ||
if (this.structuredLogger) { | ||
this.logger[level](entry, message, ...args); | ||
} else { | ||
let prefix = ''; | ||
if (entry.tnx === 'server') { | ||
prefix = 'S: '; | ||
} else if (entry.tnx === 'client') { | ||
prefix = 'C: '; | ||
} | ||
this.logger[level](message.replace(/^/mg, '[' + entry.sid + '] ' + prefix, ...args)); | ||
} | ||
} | ||
} | ||
// IP should be enclosed in [] | ||
if (defaultHostname.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) { | ||
defaultHostname = '[' + defaultHostname + ']'; | ||
} | ||
return defaultHostname; | ||
}; | ||
module.exports = SMTPConnection; |
{ | ||
"name": "smtp-connection", | ||
"version": "2.12.2", | ||
"version": "3.0.0", | ||
"description": "Connect to SMTP servers", | ||
@@ -26,2 +26,3 @@ "main": "lib/smtp-connection.js", | ||
"devDependencies": { | ||
"bunyan": "^1.8.5", | ||
"chai": "^3.5.0", | ||
@@ -32,12 +33,15 @@ "grunt": "^1.0.1", | ||
"grunt-mocha-test": "^0.13.2", | ||
"mocha": "^3.1.1", | ||
"mocha": "^3.2.0", | ||
"proxy-test-server": "^1.0.0", | ||
"sinon": "^1.17.6", | ||
"smtp-server": "^1.15.0", | ||
"smtp-server": "^1.16.1", | ||
"xoauth2": "^1.2.0" | ||
}, | ||
"dependencies": { | ||
"httpntlm": "1.6.1", | ||
"nodemailer-shared": "1.1.0" | ||
"httpntlm": "1.7.3", | ||
"nodemailer-shared": "2.0.0" | ||
}, | ||
"engines": { | ||
"node": ">=6.0.0" | ||
} | ||
} |
@@ -11,5 +11,5 @@ # smtp-connection | ||
* **[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 | ||
* **[zone-mta](https://github.com/zone-eu/zone-mta)** – full featured outbound MTA built using smtp-connection and smtp-server modules | ||
- **[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 | ||
- **[zone-mta](https://github.com/zone-eu/zone-mta)** – full featured outbound MTA built using smtp-connection and smtp-server modules | ||
@@ -26,5 +26,5 @@ ## Usage | ||
```javascript | ||
const SMTPConnection = require('smtp-connection'); | ||
``` | ||
var SMTPConnection = require('smtp-connection'); | ||
``` | ||
@@ -34,3 +34,3 @@ ### Create SMTPConnection instance | ||
```javascript | ||
var connection = new SMTPConnection(options); | ||
let connection = new SMTPConnection(options); | ||
``` | ||
@@ -54,4 +54,4 @@ | ||
- **options.logger** optional [bunyan](https://github.com/trentm/node-bunyan) compatible logger instance. If set to `true` then logs to console. If value is not set or is `false` then nothing is logged | ||
- **options.debug** if set to true, then logs SMTP traffic, otherwise logs only transaction events | ||
- **options.transactionLog** if set to true, then logs SMTP traffic without message content | ||
- **options.debug** if set to true, then logs SMTP traffic and message content, otherwise logs only transaction events | ||
- **options.authMethod** defines preferred authentication method, e.g. 'PLAIN' | ||
@@ -111,3 +111,3 @@ - **options.tls** defines additional options to be passed to the socket constructor, e.g. _{rejectUnauthorized: true}_ | ||
```javascript | ||
var generator = require('xoauth2').createXOAuth2Generator({ | ||
let generator = require('xoauth2').createXOAuth2Generator({ | ||
user: '{username}', | ||
@@ -114,0 +114,0 @@ clientId: '{Client ID}', |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
73798
1517
11
+ Addedhttpntlm@1.7.3(transitive)
+ Addednodemailer-fetch@2.0.0(transitive)
+ Addednodemailer-shared@2.0.0(transitive)
- Removedhttpntlm@1.6.1(transitive)
- Removednodemailer-fetch@1.6.0(transitive)
- Removednodemailer-shared@1.1.0(transitive)
Updatedhttpntlm@1.7.3
Updatednodemailer-shared@2.0.0