smtp-connection
Advanced tools
Comparing version 2.5.0 to 2.6.0
# Changelog | ||
## v2.6.0 2016-07-06 | ||
* Added support for DSN | ||
* Added new option use8BitMime to indicate that the message might include non-ascii bytes | ||
* Added new info property rejectedErrors that includes errors for failed recipients | ||
* Updated errors to indicate where the error happened (SMTP command, API, CONN) | ||
## v2.5.0 2016-05-11 | ||
@@ -4,0 +11,0 @@ |
@@ -8,3 +8,3 @@ 'use strict'; | ||
eslint: { | ||
all: ['lib/*.js', 'test/*.js', 'Gruntfile.js', '.eslintrc.js'] | ||
all: ['lib/*.js', 'test/*.js', 'Gruntfile.js'] | ||
}, | ||
@@ -11,0 +11,0 @@ |
@@ -180,3 +180,3 @@ 'use strict'; | ||
if (err) { | ||
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS'); | ||
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN'); | ||
return; | ||
@@ -195,3 +195,3 @@ } | ||
} catch (E) { | ||
return setImmediate(this._onError.bind(this, E, 'ECONNECTION')); | ||
return setImmediate(this._onError.bind(this, E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
@@ -208,3 +208,3 @@ } else if (this.secureConnection) { | ||
} catch (E) { | ||
return setImmediate(this._onError.bind(this, E, 'ECONNECTION')); | ||
return setImmediate(this._onError.bind(this, E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
@@ -216,3 +216,3 @@ } else { | ||
} catch (E) { | ||
return setImmediate(this._onError.bind(this, E, 'ECONNECTION')); | ||
return setImmediate(this._onError.bind(this, E, 'ECONNECTION', false, 'CONN')); | ||
} | ||
@@ -222,6 +222,8 @@ } | ||
this._connectionTimeout = setTimeout(function () { | ||
this._onError('Connection timeout', 'ETIMEDOUT'); | ||
this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN'); | ||
}.bind(this), this.options.connectionTimeout || 60 * 1000); | ||
this._socket.on('error', this._onError.bind(this)); | ||
this._socket.on('error', function (err) { | ||
this._onError(err, 'ECONNECTION', false, 'CONN'); | ||
}.bind(this)); | ||
}; | ||
@@ -279,15 +281,15 @@ | ||
var authMethod; | ||
this._authMethod = false; | ||
if (this.options.authMethod) { | ||
authMethod = this.options.authMethod.toUpperCase().trim(); | ||
this._authMethod = this.options.authMethod.toUpperCase().trim(); | ||
} else if (this._auth.xoauth2 && this._supportedAuth.indexOf('XOAUTH2') >= 0) { | ||
authMethod = 'XOAUTH2'; | ||
this._authMethod = 'XOAUTH2'; | ||
} else if (this._auth.domain && this._supportedAuth.indexOf('NTLM') >= 0) { | ||
authMethod = 'NTLM'; | ||
this._authMethod = 'NTLM'; | ||
} else { | ||
// use first supported | ||
authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim(); | ||
this._authMethod = (this._supportedAuth[0] || 'PLAIN').toUpperCase().trim(); | ||
} | ||
switch (authMethod) { | ||
switch (this._authMethod) { | ||
case 'XOAUTH2': | ||
@@ -329,3 +331,3 @@ this._handleXOauth2Token(false, callback); | ||
return callback(this._formatError('Unknown authentication method "' + authMethod + '"', 'EAUTH')); | ||
return callback(this._formatError('Unknown authentication method "' + this._authMethod + '"', 'EAUTH', false, 'API')); | ||
}; | ||
@@ -342,3 +344,3 @@ | ||
if (!message) { | ||
return done(this._formatError('Empty message', 'EMESSAGE')); | ||
return done(this._formatError('Empty message', 'EMESSAGE', false, 'API')); | ||
} | ||
@@ -359,3 +361,3 @@ | ||
message.on('error', function (err) { | ||
return callback(this._formatError(err, 'ESTREAM')); | ||
return callback(this._formatError(err, 'ESTREAM', false, 'API')); | ||
}.bind(this)); | ||
@@ -420,3 +422,3 @@ } | ||
if (this._socket && !this._destroyed && this._currentAction === this._actionGreeting) { | ||
this._onError('Greeting never received', 'ETIMEDOUT'); | ||
this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN'); | ||
} | ||
@@ -469,3 +471,3 @@ }.bind(this), this.options.greetingTimeout || 10000); | ||
*/ | ||
SMTPConnection.prototype._onError = function (err, type, data) { | ||
SMTPConnection.prototype._onError = function (err, type, data, command) { | ||
clearTimeout(this._connectionTimeout); | ||
@@ -481,3 +483,3 @@ clearTimeout(this._greetingTimeout); | ||
err = this._formatError(err, type, data); | ||
err = this._formatError(err, type, data, command); | ||
@@ -490,3 +492,3 @@ this.logger.error('[%s] %s', this.id, err.message); | ||
SMTPConnection.prototype._formatError = function (message, type, response) { | ||
SMTPConnection.prototype._formatError = function (message, type, response, command) { | ||
var err; | ||
@@ -514,2 +516,6 @@ | ||
if (command) { | ||
err.command = command; | ||
} | ||
return err; | ||
@@ -527,3 +533,3 @@ }; | ||
if ([this._actionGreeting, this.close].indexOf(this._currentAction) < 0 && !this._destroyed) { | ||
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION'); | ||
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', false, 'CONN'); | ||
} | ||
@@ -549,3 +555,3 @@ | ||
SMTPConnection.prototype._onTimeout = function () { | ||
return this._onError(new Error('Timeout'), 'ETIMEDOUT'); | ||
return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN'); | ||
}; | ||
@@ -641,3 +647,3 @@ | ||
} else { | ||
return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str); | ||
return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN'); | ||
} | ||
@@ -689,7 +695,7 @@ }; | ||
if (!this._envelope.to.length) { | ||
return callback(this._formatError('No recipients defined', 'EENVELOPE')); | ||
return callback(this._formatError('No recipients defined', 'EENVELOPE', false, 'API')); | ||
} | ||
if (this._envelope.from && /[\r\n<>]/.test(this._envelope.from)) { | ||
return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE')); | ||
return callback(this._formatError('Invalid sender ' + JSON.stringify(this._envelope.from), 'EENVELOPE', false, 'API')); | ||
} | ||
@@ -705,3 +711,3 @@ | ||
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')); | ||
return callback(this._formatError('Invalid recipient ' + JSON.stringify(this._envelope.to[i]), 'EENVELOPE', false, 'API')); | ||
} | ||
@@ -719,4 +725,13 @@ | ||
this._envelope.rejected = []; | ||
this._envelope.rejectedErrors = []; | ||
this._envelope.accepted = []; | ||
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')); | ||
} | ||
} | ||
this._currentAction = function (str) { | ||
@@ -730,7 +745,73 @@ this._actionMAIL(str, callback); | ||
args.push('SMTPUTF8'); | ||
this._usingSmtpUtf8 = true; | ||
} | ||
// 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 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); | ||
} | ||
} | ||
this._sendCommand('MAIL FROM:<' + (this._envelope.from) + '>' + (args.length ? ' ' + args.join(' ') : '')); | ||
}; | ||
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)); | ||
} | ||
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); | ||
} | ||
if (this._envelope.dsn.orcpt) { | ||
args.push('ORCPT=' + this._envelope.dsn.orcpt); | ||
} | ||
} | ||
return (args.length ? ' ' + args.join(' ') : ''); | ||
}; | ||
SMTPConnection.prototype._createSendStream = function (callback) { | ||
@@ -779,3 +860,3 @@ var dataStream = new DataStream(); | ||
if (str.substr(0, 3) !== '220') { | ||
this._onError(new Error('Invalid greeting from server:\n' + str), 'EPROTOCOL', str); | ||
this._onError(new Error('Invalid greeting from server:\n' + str), 'EPROTOCOL', str, 'CONN'); | ||
return; | ||
@@ -801,3 +882,3 @@ } | ||
if (str.charAt(0) !== '2') { | ||
this._onError(new Error('Invalid response for LHLO:\n' + str), 'EPROTOCOL', str); | ||
this._onError(new Error('Invalid response for LHLO:\n' + str), 'EPROTOCOL', str, 'LHLO'); | ||
return; | ||
@@ -819,3 +900,3 @@ } | ||
if (str.substr(0, 3) === '421') { | ||
this._onError(new Error('Server terminates connection:\n' + str), 'ECONNECTION', str); | ||
this._onError(new Error('Server terminates connection:\n' + str), 'ECONNECTION', str, 'EHLO'); | ||
return; | ||
@@ -826,3 +907,3 @@ } | ||
if (this.options.requireTLS) { | ||
this._onError(new Error('EHLO failed but HELO does not support required STARTTLS:\n' + str), 'ECONNECTION', str); | ||
this._onError(new Error('EHLO failed but HELO does not support required STARTTLS:\n' + str), 'ECONNECTION', str, 'EHLO'); | ||
return; | ||
@@ -849,2 +930,12 @@ } | ||
// Detect if the server supports DSN | ||
if (/[ \-]DSN\b/mi.test(str)) { | ||
this._supportedExtensions.push('DSN'); | ||
} | ||
// Detect if the server supports 8BITMIME | ||
if (/[ \-]8BITMIME\b/mi.test(str)) { | ||
this._supportedExtensions.push('8BITMIME'); | ||
} | ||
// Detect if the server supports PLAIN auth | ||
@@ -881,3 +972,3 @@ if (str.match(/AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i)) { | ||
if (str.charAt(0) !== '2') { | ||
this._onError(new Error('Invalid response for EHLO/HELO:\n' + str), 'EPROTOCOL', str); | ||
this._onError(new Error('Invalid response for EHLO/HELO:\n' + str), 'EPROTOCOL', str, 'HELO'); | ||
return; | ||
@@ -898,3 +989,3 @@ } | ||
if (str.charAt(0) !== '2') { | ||
this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str); | ||
this._onError(new Error('Error upgrading connection with STARTTLS'), 'ETLS', str, 'STARTTLS'); | ||
return; | ||
@@ -905,3 +996,3 @@ } | ||
if (err) { | ||
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS'); | ||
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'STARTTLS'); | ||
return; | ||
@@ -931,3 +1022,3 @@ } | ||
if (str !== '334 VXNlcm5hbWU6') { | ||
callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str)); | ||
callback(this._formatError('Invalid login sequence while waiting for "334 VXNlcm5hbWU6"', 'EAUTH', str, 'AUTH LOGIN')); | ||
return; | ||
@@ -956,3 +1047,3 @@ } | ||
if (!challengeMatch) { | ||
return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str)); | ||
return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH NTLM')); | ||
} else { | ||
@@ -1001,3 +1092,3 @@ challengeString = challengeMatch[1]; | ||
if (!challengeMatch) { | ||
return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str)); | ||
return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5')); | ||
} else { | ||
@@ -1032,3 +1123,3 @@ challengeString = challengeMatch[1]; | ||
if (!str.match(/^235\s+/)) { | ||
return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str)); | ||
return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH CRAM-MD5')); | ||
} | ||
@@ -1049,3 +1140,3 @@ | ||
if (!str.match(/^235\s+/)) { | ||
return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str)); | ||
return callback(this._formatError('Invalid login sequence while waiting for "235"', 'EAUTH', str, 'AUTH NTLM')); | ||
} | ||
@@ -1067,3 +1158,3 @@ | ||
if (str !== '334 UGFzc3dvcmQ6') { | ||
return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str)); | ||
return callback(this._formatError('Invalid login sequence while waiting for "334 UGFzc3dvcmQ6"', 'EAUTH', str, 'AUTH LOGIN')); | ||
} | ||
@@ -1104,3 +1195,3 @@ | ||
this.logger.info('[%s] User %s failed to authenticate', this.id, JSON.stringify(this._user)); | ||
return callback(this._formatError('Invalid login', 'EAUTH', str)); | ||
return callback(this._formatError('Invalid login', 'EAUTH', str, 'AUTH ' + this._authMethod)); | ||
} | ||
@@ -1119,8 +1210,14 @@ | ||
SMTPConnection.prototype._actionMAIL = function (str, callback) { | ||
var message; | ||
if (Number(str.charAt(0)) !== 2) { | ||
return callback(this._formatError('Mail command failed', 'EENVELOPE', str)); | ||
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')); | ||
} | ||
if (!this._envelope.rcptQueue.length) { | ||
return callback(this._formatError('Can\'t send mail - no recipients defined', 'EENVELOPE')); | ||
return callback(this._formatError('Can\'t send mail - no recipients defined', 'EENVELOPE', false, 'API')); | ||
} else { | ||
@@ -1131,3 +1228,3 @@ this._envelope.curRecipient = this._envelope.rcptQueue.shift(); | ||
}.bind(this); | ||
this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>'); | ||
this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>' + this._getDsnRcptToArgs()); | ||
} | ||
@@ -1142,5 +1239,15 @@ }; | ||
SMTPConnection.prototype._actionRCPT = function (str, callback) { | ||
var message, err; | ||
if (Number(str.charAt(0)) !== 2) { | ||
// this is a soft error | ||
if (this._usingSmtpUtf8 && /^553 /.test(str) && /[\x80-\uFFFF]/.test(this._envelope.curRecipient)) { | ||
message = 'Internationalized mailbox name not allowed'; | ||
} else { | ||
message = 'Recipient command failed'; | ||
} | ||
this._envelope.rejected.push(this._envelope.curRecipient); | ||
// store error for the failed recipient | ||
err = this._formatError(message, 'EENVELOPE', str, 'RCPT TO'); | ||
err.recipient = this._envelope.curRecipient; | ||
this._envelope.rejectedErrors.push(err); | ||
} else { | ||
@@ -1157,3 +1264,3 @@ this._envelope.accepted.push(this._envelope.curRecipient); | ||
} else { | ||
return callback(this._formatError('Can\'t send mail - all recipients were rejected', 'EENVELOPE', str)); | ||
return callback(this._formatError('Can\'t send mail - all recipients were rejected', 'EENVELOPE', str, 'RCPT TO')); | ||
} | ||
@@ -1165,3 +1272,3 @@ } else { | ||
}.bind(this); | ||
this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>'); | ||
this._sendCommand('RCPT TO:<' + this._envelope.curRecipient + '>' + this._getDsnRcptToArgs()); | ||
} | ||
@@ -1179,9 +1286,15 @@ }; | ||
if ([2, 3].indexOf(Number(str.charAt(0))) < 0) { | ||
return callback(this._formatError('Data command failed', 'EENVELOPE', str)); | ||
return callback(this._formatError('Data command failed', 'EENVELOPE', str, 'DATA')); | ||
} | ||
callback(null, { | ||
var response = { | ||
accepted: this._envelope.accepted, | ||
rejected: this._envelope.rejected | ||
}); | ||
}; | ||
if (this._envelope.rejectedErrors.length) { | ||
response.rejectedErrors = this._envelope.rejectedErrors; | ||
} | ||
callback(null, response); | ||
}; | ||
@@ -1197,3 +1310,3 @@ | ||
// Message failed | ||
return callback(this._formatError('Message failed', 'EMESSAGE', str)); | ||
return callback(this._formatError('Message failed', 'EMESSAGE', str, 'DATA')); | ||
} else { | ||
@@ -1214,3 +1327,3 @@ // Message sent succesfully | ||
this.logger.info('[%s] User %s failed to authenticate', this.id, JSON.stringify(this._user)); | ||
return callback(this._formatError(err, 'EAUTH')); | ||
return callback(this._formatError(err, 'EAUTH', false, 'AUTH XOAUTH2')); | ||
} | ||
@@ -1217,0 +1330,0 @@ this._sendCommand('AUTH XOAUTH2 ' + token); |
{ | ||
"name": "smtp-connection", | ||
"version": "2.5.0", | ||
"version": "2.6.0", | ||
"description": "Connect to SMTP servers", | ||
@@ -29,5 +29,5 @@ "main": "lib/smtp-connection.js", | ||
"grunt-cli": "^1.2.0", | ||
"grunt-eslint": "^18.1.0", | ||
"grunt-eslint": "^19.0.0", | ||
"grunt-mocha-test": "^0.12.7", | ||
"mocha": "^2.4.5", | ||
"mocha": "^2.5.3", | ||
"proxy-test-server": "^1.0.0", | ||
@@ -34,0 +34,0 @@ "sinon": "^1.17.4", |
@@ -141,2 +141,8 @@ # smtp-connection | ||
* **envelope.to** is the recipient address or an array of addresses | ||
* **envelope.use8BitMime** if `true` then inform the server that this message might contain bytes outside 7bit ascii range | ||
* **envelope.dsn** is the dsn options | ||
* **envelope.dsn.ret** return either the full message 'FULL' or only headers 'HDRS' | ||
* **envelope.dsn.envid** sender's 'envelope identifier' for tracking | ||
* **envelope.dsn.notify** when to send a DSN. Multiple options are OK - array or comma delimited. NEVER must appear by itself. Available options: 'NEVER', 'SUCCESS', 'FAILURE', 'DELAY' | ||
* **envelope.dsn.orcpt** original recipient | ||
* **message** is either a String, Buffer or a Stream. All newlines are converted to \r\n and all dots are escaped automatically, no need to convert anything before. | ||
@@ -149,4 +155,5 @@ * **callback** is the callback to run once the sending is finished or failed. Callback has the following arguments | ||
* **info** information object about accepted and rejected recipients | ||
* **accepted** and array of accepted recipient addresses | ||
* **rejected** and array of rejected recipient addresses | ||
* **accepted** an array of accepted recipient addresses | ||
* **rejected** an array of rejected recipient addresses | ||
* **rejectedErrors** if some recipients were rejected then this property holds an array of error objects for the rejected recipients | ||
* **response** is the last response received from the server | ||
@@ -153,0 +160,0 @@ |
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
61250
1303
178