smtp-server
Advanced tools
Comparing version 1.7.1 to 1.8.0-beta.0
# Changelog | ||
## v1.8.0-beta.0 2016-01-26 | ||
* Fixed a bug with XCLIENT ADDR validation | ||
* Added support for XFORWARD command | ||
* Expose XCLIENT and XFORWARD data for the session object (session.xClient, session.xForward - both are Map objects where uppercase argument name is the key, eg. session.xClient.get('ADDR') to see the IP address of XCLIENT) | ||
## v1.7.1 2015-10-27 | ||
@@ -4,0 +10,0 @@ |
@@ -0,1 +1,3 @@ | ||
/* eslint no-console: 0 */ | ||
'use strict'; | ||
@@ -5,2 +7,3 @@ | ||
var SMTPServer = require('../lib/smtp-server').SMTPServer; | ||
var util = require('util'); | ||
@@ -21,2 +24,5 @@ var SERVER_PORT = 2525; | ||
// log to console | ||
logger: true, | ||
// not required but nice-to-have | ||
@@ -37,5 +43,8 @@ banner: 'Welcome to My Awesome SMTP Server', | ||
// use logging of proxied client data. Only makes sense behind proxy | ||
useXForward: true, | ||
// Setup authentication | ||
// Allow only users with username 'testuser' and password 'testpass' | ||
onAuth: function(auth, session, callback) { | ||
onAuth: function (auth, session, callback) { | ||
var username = 'testuser'; | ||
@@ -62,3 +71,6 @@ var password = 'testpass'; | ||
// If this method is not set, all addresses are allowed | ||
onMailFrom: function(address, session, callback) { | ||
onMailFrom: function (address, session, callback) { | ||
console.log(util.inspect(session.xClient, false, 22)); | ||
console.log(util.inspect(session.xForward, false, 22)); | ||
if (/^deny/i.test(address.address)) { | ||
@@ -72,3 +84,3 @@ return callback(new Error('Not accepted')); | ||
// If this method is not set, all addresses are allowed | ||
onRcptTo: function(address, session, callback) { | ||
onRcptTo: function (address, session, callback) { | ||
var err; | ||
@@ -91,5 +103,5 @@ | ||
// Handle message stream | ||
onData: function(stream, session, callback) { | ||
onData: function (stream, session, callback) { | ||
stream.pipe(process.stdout); | ||
stream.on('end', function() { | ||
stream.on('end', function () { | ||
var err; | ||
@@ -106,3 +118,3 @@ if (stream.sizeExceeded) { | ||
server.on('error', function(err) { | ||
server.on('error', function (err) { | ||
console.log('Error occurred'); | ||
@@ -109,0 +121,0 @@ console.log(err); |
'use strict'; | ||
module.exports = function(grunt) { | ||
module.exports = function (grunt) { | ||
// Project configuration. | ||
grunt.initConfig({ | ||
jshint: { | ||
all: ['lib/*.js', 'test/*.js', 'Gruntfile.js'], | ||
options: { | ||
jshintrc: '.jshintrc' | ||
} | ||
eslint: { | ||
all: ['lib/*.js', 'test/*.js', 'examples/*.js', 'Gruntfile.js', '.eslintrc.js'] | ||
}, | ||
@@ -25,7 +22,7 @@ | ||
// Load the plugin(s) | ||
grunt.loadNpmTasks('grunt-contrib-jshint'); | ||
grunt.loadNpmTasks('grunt-eslint'); | ||
grunt.loadNpmTasks('grunt-mocha-test'); | ||
// Tasks | ||
grunt.registerTask('default', ['jshint', 'mochaTest']); | ||
}; | ||
grunt.registerTask('default', ['eslint', 'mochaTest']); | ||
}; |
@@ -8,3 +8,3 @@ 'use strict'; | ||
SASL_PLAIN: function(args, callback) { | ||
SASL_PLAIN: function (args, callback) { | ||
if (args.length > 1) { | ||
@@ -24,3 +24,3 @@ this.send(501, 'Error: syntax: AUTH PLAIN token'); | ||
SASL_LOGIN: function(args, callback) { | ||
SASL_LOGIN: function (args, callback) { | ||
if (args.length > 1) { | ||
@@ -40,3 +40,3 @@ this.send(501, 'Error: syntax: AUTH LOGIN'); | ||
SASL_XOAUTH2: function(args, callback) { | ||
SASL_XOAUTH2: function (args, callback) { | ||
if (args.length > 1) { | ||
@@ -56,3 +56,3 @@ this.send(501, 'Error: syntax: AUTH XOAUTH2 token'); | ||
'SASL_CRAM-MD5': function(args, callback) { | ||
'SASL_CRAM-MD5': function (args, callback) { | ||
if (args.length) { | ||
@@ -74,3 +74,3 @@ this.send(501, 'Error: syntax: AUTH CRAM-MD5'); | ||
PLAIN_token: function(canAbort, token, callback) { | ||
PLAIN_token: function (canAbort, token, callback) { | ||
token = (token || '').toString().trim(); | ||
@@ -97,3 +97,3 @@ | ||
password: password | ||
}, this.session, function(err, response) { | ||
}, this.session, function (err, response) { | ||
@@ -121,3 +121,3 @@ if (err) { | ||
LOGIN_username: function(canAbort, username, callback) { | ||
LOGIN_username: function (canAbort, username, callback) { | ||
username = (username || '').toString().trim(); | ||
@@ -142,3 +142,3 @@ | ||
LOGIN_password: function(username, password, callback) { | ||
LOGIN_password: function (username, password, callback) { | ||
password = (password || '').toString().trim(); | ||
@@ -157,3 +157,3 @@ | ||
password: password | ||
}, this.session, function(err, response) { | ||
}, this.session, function (err, response) { | ||
@@ -181,3 +181,3 @@ if (err) { | ||
XOAUTH2_token: function(canAbort, token, callback) { | ||
XOAUTH2_token: function (canAbort, token, callback) { | ||
token = (token || '').toString().trim(); | ||
@@ -194,3 +194,3 @@ | ||
// Find username and access token from the input | ||
new Buffer(token, 'base64').toString().split('\x01').forEach(function(part) { | ||
new Buffer(token, 'base64').toString().split('\x01').forEach(function (part) { | ||
part = part.split('='); | ||
@@ -219,3 +219,3 @@ var key = part.shift().toLowerCase(); | ||
accessToken: accessToken | ||
}, this.session, function(err, response) { | ||
}, this.session, function (err, response) { | ||
@@ -244,3 +244,3 @@ if (err) { | ||
XOAUTH2_error: function(data, callback) { | ||
XOAUTH2_error: function (data, callback) { | ||
this.send(535, 'Error: Username and Password not accepted'); | ||
@@ -250,3 +250,3 @@ return callback(); | ||
'CRAM-MD5_token': function(canAbort, challenge, token, callback) { | ||
'CRAM-MD5_token': function (canAbort, challenge, token, callback) { | ||
token = (token || '').toString().trim(); | ||
@@ -266,7 +266,7 @@ | ||
username: username, | ||
validatePassword: function(password) { | ||
validatePassword: function (password) { | ||
var hmac = crypto.createHmac('md5', password); | ||
return hmac.update(challenge).digest('hex').toLowerCase() === challengeResponse; | ||
} | ||
}, this.session, function(err, response) { | ||
}, this.session, function (err, response) { | ||
@@ -293,2 +293,2 @@ if (err) { | ||
} | ||
}; | ||
}; |
@@ -7,2 +7,3 @@ 'use strict'; | ||
var net = require('net'); | ||
var ipv6normalize = require('ipv6-normalize'); | ||
var sasl = require('./sasl'); | ||
@@ -66,2 +67,7 @@ var crypto = require('crypto'); | ||
// normalize IPv6 addresses | ||
if (this.remoteAddress && net.isIPv6(this.remoteAddress)) { | ||
this.remoteAddress = ipv6normalize(this.remoteAddress); | ||
} | ||
// Error counter - if too many commands in non-authenticated state are used, then disconnect | ||
@@ -83,4 +89,7 @@ this._unauthenticatedCommands = 0; | ||
// data passed from XCLIENT command | ||
this._xclient = new Map(); | ||
this._xClient = new Map(); | ||
// data passed from XFORWARD command | ||
this._xForward = new Map(); | ||
// increment connection count | ||
@@ -96,3 +105,3 @@ this._closing = false; | ||
*/ | ||
SMTPConnection.prototype.init = function() { | ||
SMTPConnection.prototype.init = function () { | ||
// Setup event handlers for the socket | ||
@@ -113,5 +122,9 @@ this._setListeners(); | ||
SMTPConnection.prototype.connectionReady = function(next) { | ||
SMTPConnection.prototype.connectionReady = function (next) { | ||
// Resolve hostname for the remote IP | ||
var reverseCb = function(err, hostnames) { | ||
var reverseCb = function (err, hostnames) { | ||
if (err) { | ||
// ignore resolve error | ||
} | ||
if (this._closing || this._closed) { | ||
@@ -125,3 +138,3 @@ return; | ||
this._server.onConnect(this.session, function(err) { | ||
this._server.onConnect(this.session, function (err) { | ||
if (err) { | ||
@@ -156,7 +169,7 @@ this.send(err.responseCode || 554, err.message); | ||
*/ | ||
SMTPConnection.prototype.send = function(code, data) { | ||
SMTPConnection.prototype.send = function (code, data) { | ||
var payload; | ||
if (Array.isArray(data)) { | ||
payload = data.map(function(line, i, arr) { | ||
payload = data.map(function (line, i, arr) { | ||
return code + (i < arr.length - 1 ? '-' : ' ') + line; | ||
@@ -177,3 +190,3 @@ }).join('\r\n'); | ||
*/ | ||
SMTPConnection.prototype.close = function() { | ||
SMTPConnection.prototype.close = function () { | ||
if (!this._socket.destroyed && this._socket.writable) { | ||
@@ -193,3 +206,3 @@ this._socket.end(); | ||
*/ | ||
SMTPConnection.prototype._setListeners = function() { | ||
SMTPConnection.prototype._setListeners = function () { | ||
this._socket.on('close', this._onClose.bind(this)); | ||
@@ -205,3 +218,3 @@ this._socket.on('error', this._onError.bind(this)); | ||
*/ | ||
SMTPConnection.prototype._onClose = function( /* hadError */ ) { | ||
SMTPConnection.prototype._onClose = function ( /* hadError */ ) { | ||
if (this._parser) { | ||
@@ -236,3 +249,3 @@ this._parser.closed = true; | ||
*/ | ||
SMTPConnection.prototype._onError = function(err) { | ||
SMTPConnection.prototype._onError = function (err) { | ||
if (err.code === 'ECONNRESET') { | ||
@@ -252,3 +265,3 @@ this.close(); // mark connection as 'closing' | ||
*/ | ||
SMTPConnection.prototype._onTimeout = function() { | ||
SMTPConnection.prototype._onTimeout = function () { | ||
this.send(451, 'Timeout - closing connection'); | ||
@@ -264,3 +277,3 @@ this.close(); | ||
*/ | ||
SMTPConnection.prototype._onCommand = function(command, callback) { | ||
SMTPConnection.prototype._onCommand = function (command, callback) { | ||
this._server.logger.debug('[%s] C:', this._id, (command || '').toString()); | ||
@@ -300,3 +313,3 @@ | ||
callback = callback || function() {}; | ||
callback = callback || function () {}; | ||
@@ -361,3 +374,3 @@ if (this._upgrading) { | ||
*/ | ||
SMTPConnection.prototype._isSupported = function(command) { | ||
SMTPConnection.prototype._isSupported = function (command) { | ||
command = (command || '').toString().trim().toUpperCase(); | ||
@@ -375,3 +388,3 @@ return this._server.options.disabledCommands.indexOf(command) < 0 && | ||
*/ | ||
SMTPConnection.prototype._parseAddressCommand = function(name, command) { | ||
SMTPConnection.prototype._parseAddressCommand = function (name, command) { | ||
command = (command || '').toString(); | ||
@@ -398,3 +411,3 @@ name = (name || '').toString().trim().toUpperCase(); | ||
parts.forEach(function(part) { | ||
parts.forEach(function (part) { | ||
part = part.split('='); | ||
@@ -404,2 +417,9 @@ var key = part.shift().toUpperCase(); | ||
if (typeof value === 'string') { | ||
// decode 'xtext' | ||
value = value.replace(/\+([0-9A-F]{2})/g, function (match, hex) { | ||
return unescape('%' + hex); | ||
}); | ||
} | ||
if (!args) { | ||
@@ -431,3 +451,3 @@ args = {}; | ||
*/ | ||
SMTPConnection.prototype._startSession = function() { | ||
SMTPConnection.prototype._startSession = function () { | ||
var user = this.session.user || false; | ||
@@ -445,3 +465,5 @@ | ||
user: user, | ||
transaction: this._transactionCounter + 1 | ||
transaction: this._transactionCounter + 1, | ||
xClient: this._xClient, | ||
xForward: this._xForward | ||
}; | ||
@@ -455,3 +477,3 @@ }; | ||
*/ | ||
SMTPConnection.prototype.handler_EHLO = function(command, callback) { | ||
SMTPConnection.prototype.handler_EHLO = function (command, callback) { | ||
var parts = command.toString().trim().split(/\s+/); | ||
@@ -481,6 +503,12 @@ var hostname = parts[1] || ''; | ||
if (!this._xclient.has('ADDR') && this._server.options.useXClient && this._isSupported('XCLIENT')) { | ||
// XCLIENT ADDR removes any special privileges for the client | ||
if (!this._xClient.has('ADDR') && this._server.options.useXClient && this._isSupported('XCLIENT')) { | ||
features.push('XCLIENT NAME ADDR PORT PROTO HELO LOGIN'); | ||
} | ||
// If client has already issued XCLIENT ADDR then it does not have privileges for XFORWARD anymore | ||
if (!this._xClient.has('ADDR') && this._server.options.useXForward && this._isSupported('XFORWARD')) { | ||
features.push('XFORWARD NAME ADDR PORT PROTO HELO IDENT SOURCE'); | ||
} | ||
this._startSession(); // EHLO is effectively the same as RSET | ||
@@ -495,3 +523,3 @@ this.send(250, ['OK: Nice to meet you ' + this.clientHostname].concat(features || [])); | ||
*/ | ||
SMTPConnection.prototype.handler_HELO = function(command, callback) { | ||
SMTPConnection.prototype.handler_HELO = function (command, callback) { | ||
var parts = command.toString().trim().split(/\s+/); | ||
@@ -516,3 +544,3 @@ var hostname = parts[1] || ''; | ||
*/ | ||
SMTPConnection.prototype.handler_QUIT = function(command, callback) { | ||
SMTPConnection.prototype.handler_QUIT = function (command, callback) { | ||
this.send(221, 'Bye'); | ||
@@ -526,3 +554,3 @@ this.close(); | ||
*/ | ||
SMTPConnection.prototype.handler_NOOP = function(command, callback) { | ||
SMTPConnection.prototype.handler_NOOP = function (command, callback) { | ||
this.send(250, 'OK'); | ||
@@ -535,3 +563,3 @@ callback(); | ||
*/ | ||
SMTPConnection.prototype.handler_RSET = function(command, callback) { | ||
SMTPConnection.prototype.handler_RSET = function (command, callback) { | ||
this._startSession(); | ||
@@ -546,3 +574,3 @@ | ||
*/ | ||
SMTPConnection.prototype.handler_HELP = function(command, callback) { | ||
SMTPConnection.prototype.handler_HELP = function (command, callback) { | ||
this.send(214, 'See https://tools.ietf.org/html/rfc5321 for details'); | ||
@@ -555,3 +583,3 @@ callback(); | ||
*/ | ||
SMTPConnection.prototype.handler_VRFY = function(command, callback) { | ||
SMTPConnection.prototype.handler_VRFY = function (command, callback) { | ||
this.send(252, 'Try to send something. No promises though'); | ||
@@ -563,6 +591,9 @@ callback(); | ||
* Overrides connection info | ||
* http://www.postfix.org/XCLIENT_README.html | ||
* | ||
* TODO: add unit tests | ||
*/ | ||
SMTPConnection.prototype.handler_XCLIENT = function(command, callback) { | ||
SMTPConnection.prototype.handler_XCLIENT = function (command, callback) { | ||
// check if user is authorized to perform this command | ||
if (this._xclient.has('ADDR') || !this._server.options.useXClient) { | ||
if (this._xClient.has('ADDR') || !this._server.options.useXClient) { | ||
this.send(550, 'Error: Not allowed'); | ||
@@ -598,11 +629,19 @@ return callback(); | ||
key = key.toUpperCase(); | ||
value = value[0]; | ||
// value is xtext | ||
value = (value[0] || '').replace(/\+([0-9A-F]{2})/g, function (match, hex) { | ||
return unescape('%' + hex); | ||
}); | ||
if (['[UNAVAILABLE]', '[TEMPUNAVAIL]'].indexOf(value.toUpperCase()) >= 0) { | ||
value = false; | ||
} | ||
if (data.has(key)) { | ||
// ignore duplicate keys | ||
continue; | ||
} | ||
data.set(key, value); | ||
} | ||
// override connection properties | ||
data.forEach(function(value, key) { | ||
switch (key) { | ||
@@ -623,8 +662,21 @@ case 'LOGIN': | ||
if (value) { | ||
if (!net.isIP) { | ||
value = value.replace(/^IPV6:/i, ''); // IPv6 addresses are prefixed with "IPv6:" | ||
if (!net.isIP(value)) { | ||
this.send(501, 'Error: Bad command parameter syntax. Invalid address'); | ||
return callback(); | ||
} | ||
if (net.isIPv6(value)) { | ||
value = ipv6normalize(value); | ||
} | ||
this._server.logger.info('[%s] XCLIENT from %s through %s', this._id, value, this.remoteAddress); | ||
this.remoteAddress = value.toLowerCase(); | ||
// store original value for reference as ADDR:DEFAULT | ||
if (!this._xClient.has('ADDR:DEFAULT')) { | ||
this._xClient.set('ADDR:DEFAULT', this.remoteAddress || ''); | ||
} | ||
this.remoteAddress = value; | ||
this.hostNameAppearsAs = false; // reset client provided hostname, require HELO/EHLO | ||
@@ -636,2 +688,8 @@ } | ||
this._server.logger.info('[%s] XCLIENT hostname resolved as "%s"', this._id, value); | ||
// store original value for reference as NAME:DEFAULT | ||
if (!this._xClient.has('NAME:DEFAULT')) { | ||
this._xClient.set('NAME:DEFAULT', this.clientHostname || ''); | ||
} | ||
this.clientHostname = value.toLowerCase(); | ||
@@ -642,4 +700,4 @@ break; | ||
} | ||
this._xclient.set(key, value); | ||
}.bind(this)); | ||
this._xClient.set(key, value); | ||
} | ||
@@ -657,5 +715,92 @@ // Use [ADDR] if NAME was empty | ||
/** | ||
* Processes XFORWARD data | ||
* http://www.postfix.org/XFORWARD_README.html | ||
* | ||
* TODO: add unit tests | ||
*/ | ||
SMTPConnection.prototype.handler_XFORWARD = function (command, callback) { | ||
// check if user is authorized to perform this command | ||
if (!this._server.options.useXForward) { | ||
this.send(550, 'Error: Not allowed'); | ||
return callback(); | ||
} | ||
// not allowed to change properties if already processing mail | ||
if (this.session.envelope.mailFrom) { | ||
this.send(503, 'Error: Mail transaction in progress'); | ||
return callback(); | ||
} | ||
var allowedKeys = ['NAME', 'ADDR', 'PORT', 'PROTO', 'HELO', 'IDENT', 'SOURCE']; | ||
var parts = command.toString().trim().split(/\s+/); | ||
var key, value; | ||
var data = new Map(); | ||
parts.shift(); // remove XFORWARD prefix | ||
if (!parts.length) { | ||
this.send(501, 'Error: Bad command parameter syntax'); | ||
return callback(); | ||
} | ||
// parse and validate arguments | ||
for (var i = 0, len = parts.length; i < len; i++) { | ||
value = parts[i].split('='); | ||
key = value.shift(); | ||
if (value.length !== 1 || allowedKeys.indexOf(key.toUpperCase()) < 0) { | ||
this.send(501, 'Error: Bad command parameter syntax'); | ||
return callback(); | ||
} | ||
key = key.toUpperCase(); | ||
if (data.has(key)) { | ||
// ignore duplicate keys | ||
continue; | ||
} | ||
// value is xtext | ||
value = (value[0] || '').replace(/\+([0-9A-F]{2})/g, function (match, hex) { | ||
return unescape('%' + hex); | ||
}); | ||
if (value.toUpperCase() === '[UNAVAILABLE]') { | ||
value = false; | ||
} | ||
data.set(key, value); | ||
switch (key) { | ||
case 'ADDR': | ||
if (value) { | ||
value = value.replace(/^IPV6:/i, ''); // IPv6 addresses are prefixed with "IPv6:" | ||
if (!net.isIP(value)) { | ||
this.send(501, 'Error: Bad command parameter syntax. Invalid address'); | ||
return callback(); | ||
} | ||
if (net.isIPv6(value)) { | ||
value = ipv6normalize(value); | ||
} | ||
this._server.logger.info('[%s] XFORWARD from %s through %s', this._id, value, this.remoteAddress); | ||
} | ||
break; | ||
case 'NAME': | ||
value = value || ''; | ||
this._server.logger.info('[%s] XFORWARD hostname resolved as "%s"', this._id, value); | ||
break; | ||
default: | ||
// other values are not relevant | ||
} | ||
this._xForward.set(key, value); | ||
} | ||
// success | ||
this.send(250, 'OK'); | ||
callback(); | ||
}; | ||
/** | ||
* Upgrades connection to TLS if possible | ||
*/ | ||
SMTPConnection.prototype.handler_STARTTLS = function(command, callback) { | ||
SMTPConnection.prototype.handler_STARTTLS = function (command, callback) { | ||
@@ -670,3 +815,3 @@ if (this.secure) { | ||
this._upgrading = true; | ||
callback(); // resume input stream | ||
setImmediate(callback); // resume input stream | ||
@@ -679,3 +824,3 @@ var secureContext = this._server.secureContext.get('default'); | ||
SNICallback: function(servername, cb) { | ||
SNICallback: function (servername, cb) { | ||
cb(null, this._server.secureContext.get(servername.toLowerCase().trim()) || this._server.secureContext.get('default')); | ||
@@ -686,3 +831,3 @@ }.bind(this) | ||
// Apply additional socket options if these are set in the server options | ||
['requestCert', 'rejectUnauthorized', 'NPNProtocols', 'SNICallback', 'session', 'requestOCSP'].forEach(function(key) { | ||
['requestCert', 'rejectUnauthorized', 'NPNProtocols', 'SNICallback', 'session', 'requestOCSP'].forEach(function (key) { | ||
if (key in this._server.options) { | ||
@@ -705,3 +850,3 @@ socketOptions[key] = this._server.options[key]; | ||
secureSocket.on('secure', function() { | ||
secureSocket.on('secure', function () { | ||
this.secure = true; | ||
@@ -719,3 +864,3 @@ this._socket = secureSocket; | ||
*/ | ||
SMTPConnection.prototype.handler_AUTH = function(command, callback) { | ||
SMTPConnection.prototype.handler_AUTH = function (command, callback) { | ||
var args = command.toString().trim().split(/\s+/); | ||
@@ -751,3 +896,3 @@ var method; | ||
*/ | ||
SMTPConnection.prototype.handler_MAIL = function(command, callback) { | ||
SMTPConnection.prototype.handler_MAIL = function (command, callback) { | ||
var parsed = this._parseAddressCommand('mail from', command); | ||
@@ -771,3 +916,3 @@ | ||
this._server.onMailFrom(parsed, this.session, function(err) { | ||
this._server.onMailFrom(parsed, this.session, function (err) { | ||
if (err) { | ||
@@ -789,3 +934,3 @@ this.send(err.responseCode || 550, err.message); | ||
*/ | ||
SMTPConnection.prototype.handler_RCPT = function(command, callback) { | ||
SMTPConnection.prototype.handler_RCPT = function (command, callback) { | ||
var parsed = this._parseAddressCommand('rcpt to', command); | ||
@@ -804,3 +949,3 @@ | ||
this._server.onRcptTo(parsed, this.session, function(err) { | ||
this._server.onRcptTo(parsed, this.session, function (err) { | ||
if (err) { | ||
@@ -832,3 +977,3 @@ this.send(err.responseCode || 550, err.message); | ||
*/ | ||
SMTPConnection.prototype.handler_DATA = function(command, callback) { | ||
SMTPConnection.prototype.handler_DATA = function (command, callback) { | ||
if (!this.session.envelope.rcptTo.length) { | ||
@@ -841,3 +986,3 @@ this.send(503, 'Error: need RCPT command'); | ||
var close = function(err, message) { | ||
var close = function (err, message) { | ||
this._server.logger.debug('[%s] C: <%s bytes of DATA>', this._id, this._parser.dataBytes); | ||
@@ -860,6 +1005,7 @@ | ||
this._server.onData(this._dataStream, this.session, function(err, message) { | ||
this._server.onData(this._dataStream, this.session, function (err, message) { | ||
// ensure _dataStream is an object and not set to null by premature closing | ||
// do not continue until the stream has actually ended | ||
if (this._dataStream.readable) { | ||
this._dataStream.on('end', function() { | ||
if ((typeof this._dataStream === 'object') && (this._dataStream) && (this._dataStream.readable)) { | ||
this._dataStream.on('end', function () { | ||
close(err, message); | ||
@@ -881,3 +1027,3 @@ }); | ||
*/ | ||
SMTPConnection.prototype.handler_WIZ = function(command, callback) { | ||
SMTPConnection.prototype.handler_WIZ = function (command, callback) { | ||
var args = command.toString().trim().split(/\s+/); | ||
@@ -905,3 +1051,3 @@ var password; | ||
*/ | ||
SMTPConnection.prototype.handler_SHELL = function(command, callback) { | ||
SMTPConnection.prototype.handler_SHELL = function (command, callback) { | ||
if (!this.session.isWizard) { | ||
@@ -920,5 +1066,5 @@ this.send(500, 'Mere mortals musn\'t mutter that mantra'); | ||
*/ | ||
SMTPConnection.prototype.handler_KILL = function(command, callback) { | ||
SMTPConnection.prototype.handler_KILL = function (command, callback) { | ||
this.send(500, 'Can\'t kill Mom'); | ||
callback(); | ||
}; |
@@ -9,2 +9,3 @@ 'use strict'; | ||
var util = require('util'); | ||
var shared = require('nodemailer-shared'); | ||
@@ -32,7 +33,7 @@ var CLOSE_TIMEOUT = 30 * 1000; // how much to wait until pending connections are terminated | ||
if (typeof ctxMap.get === 'function') { | ||
ctxMap.forEach(function(ctx, servername) { | ||
ctxMap.forEach(function (ctx, servername) { | ||
this.secureContext.set(servername.toLowerCase().trim(), tls.createSecureContext(tlsOptions(ctx))); | ||
}.bind(this)); | ||
} else { | ||
Object.keys(ctxMap).forEach(function(servername) { | ||
Object.keys(ctxMap).forEach(function (servername) { | ||
this.secureContext.set(servername.toLowerCase().trim(), tls.createSecureContext(tlsOptions(ctxMap[servername]))); | ||
@@ -42,6 +43,6 @@ }.bind(this)); | ||
// apply TLS defaults if needed | ||
if (!!this.options.secure) { | ||
// apply TLS defaults if needed, only if there is not SNICallback. | ||
if (this.options.secure && typeof this.options.SNICallback !== 'function') { | ||
this.options = tlsOptions(this.options); | ||
this.options.SNICallback = function(servername, cb) { | ||
this.options.SNICallback = function (servername, cb) { | ||
cb(null, this.secureContext.get(servername.toLowerCase().trim()) || this.secureContext.get('default')); | ||
@@ -52,3 +53,3 @@ }.bind(this); | ||
// setup disabled commands list | ||
this.options.disabledCommands = [].concat(this.options.disabledCommands || []).map(function(command) { | ||
this.options.disabledCommands = [].concat(this.options.disabledCommands || []).map(function (command) { | ||
return (command || '').toString().toUpperCase().trim(); | ||
@@ -58,3 +59,3 @@ }); | ||
// setup allowed auth methods | ||
this.options.authMethods = [].concat(this.options.authMethods || []).map(function(method) { | ||
this.options.authMethods = [].concat(this.options.authMethods || []).map(function (method) { | ||
return (method || '').toString().toUpperCase().trim(); | ||
@@ -67,17 +68,6 @@ }); | ||
// setup logger | ||
if ('logger' in this.options) { | ||
// use provided logger or use vanity logger if option is set to false | ||
this.logger = this.options.logger || { | ||
info: function() {}, | ||
debug: function() {}, | ||
error: function() {} | ||
}; | ||
} else { | ||
// create default console logger | ||
this.logger = this._createDefaultLogger(); | ||
} | ||
this.logger = shared.getLogger(this.options); | ||
// apply shorthand handlers | ||
['onConnect','onAuth', 'onMailFrom', 'onRcptTo', 'onData'].forEach(function(handler) { | ||
['onConnect', 'onAuth', 'onMailFrom', 'onRcptTo', 'onData'].forEach(function (handler) { | ||
if (typeof this.options[handler] === 'function') { | ||
@@ -99,3 +89,3 @@ this[handler] = this.options[handler]; | ||
// setup server listener and connection handler | ||
this.server = (this.options.secure ? tls : net).createServer(this.options, function(socket) { | ||
this.server = (this.options.secure ? tls : net).createServer(this.options, function (socket) { | ||
var connection = new SMTPConnection(this, socket); | ||
@@ -117,3 +107,3 @@ this.connections.add(connection); | ||
*/ | ||
SMTPServer.prototype.listen = function( /* arguments */ ) { | ||
SMTPServer.prototype.listen = function ( /* arguments */ ) { | ||
this.server.listen.apply(this.server, Array.prototype.slice.call(arguments)); | ||
@@ -127,3 +117,3 @@ }; | ||
*/ | ||
SMTPServer.prototype.close = function(callback) { | ||
SMTPServer.prototype.close = function (callback) { | ||
var connections = this.connections.size; | ||
@@ -133,5 +123,7 @@ var timeout = this.options.closeTimeout || CLOSE_TIMEOUT; | ||
// stop accepting new connections | ||
this.server.close(function() { | ||
this.server.close(function () { | ||
clearTimeout(this._closeTimeout); | ||
callback(); | ||
if (typeof callback === 'function') { | ||
return callback(); | ||
} | ||
}.bind(this)); | ||
@@ -144,3 +136,3 @@ | ||
this._closeTimeout = setTimeout(function() { | ||
this._closeTimeout = setTimeout(function () { | ||
connections = this.connections.size; | ||
@@ -150,3 +142,3 @@ if (connections) { | ||
this.connections.forEach(function(connection) { | ||
this.connections.forEach(function (connection) { | ||
connection.send(421, 'Server shutting down'); | ||
@@ -165,5 +157,5 @@ connection.close(); | ||
*/ | ||
SMTPServer.prototype.onAuth = function(auth, session, callback) { | ||
SMTPServer.prototype.onAuth = function (auth, session, callback) { | ||
if (auth.method === 'XOAUTH2') { | ||
callback(null, { | ||
return callback(null, { | ||
data: { | ||
@@ -175,29 +167,29 @@ status: '401', | ||
}); | ||
} else { | ||
callback(null, { | ||
message: 'Authentication not implemented' | ||
}); | ||
} | ||
return callback(null, { | ||
message: 'Authentication not implemented' | ||
}); | ||
}; | ||
SMTPServer.prototype.onConnect = function(session, callback) { | ||
SMTPServer.prototype.onConnect = function (session, callback) { | ||
setImmediate(callback); | ||
}; | ||
SMTPServer.prototype.onMailFrom = function(address, session, callback) { | ||
SMTPServer.prototype.onMailFrom = function (address, session, callback) { | ||
setImmediate(callback); | ||
}; | ||
SMTPServer.prototype.onRcptTo = function(address, session, callback) { | ||
SMTPServer.prototype.onRcptTo = function (address, session, callback) { | ||
setImmediate(callback); | ||
}; | ||
SMTPServer.prototype.onData = function(stream, session, callback) { | ||
SMTPServer.prototype.onData = function (stream, session, callback) { | ||
var chunklen = 0; | ||
stream.on('data', function(chunk) { | ||
stream.on('data', function (chunk) { | ||
chunklen += chunk.length; | ||
}.bind(this)); | ||
stream.on('end', function() { | ||
stream.on('end', function () { | ||
this.logger.info('<received %s bytes>', chunklen); | ||
@@ -211,38 +203,5 @@ callback(); | ||
/** | ||
* Generates a bunyan-like logger that prints to console | ||
* | ||
* @returns {Object} Bunyan logger instance | ||
*/ | ||
SMTPServer.prototype._createDefaultLogger = function() { | ||
var logger = { | ||
_print: function( /* level, message */ ) { | ||
var args = Array.prototype.slice.call(arguments); | ||
var level = args.shift(); | ||
var message; | ||
if (args.length > 1) { | ||
message = util.format.apply(util, args); | ||
} else { | ||
message = args.shift(); | ||
} | ||
console.log('[%s] %s: %s', | ||
new Date().toISOString().substr(0, 19).replace(/T/, ' '), | ||
level.toUpperCase(), | ||
message); | ||
} | ||
}; | ||
logger.info = logger._print.bind(null, 'info'); | ||
logger.debug = logger._print.bind(null, 'debug'); | ||
logger.error = logger._print.bind(null, 'error'); | ||
return logger; | ||
}; | ||
/** | ||
* Setup server event handlers | ||
*/ | ||
SMTPServer.prototype._setListeners = function() { | ||
SMTPServer.prototype._setListeners = function () { | ||
this.server.on('listening', this._onListening.bind(this)); | ||
@@ -258,3 +217,3 @@ this.server.on('close', this._onClose.bind(this)); | ||
*/ | ||
SMTPServer.prototype._onListening = function() { | ||
SMTPServer.prototype._onListening = function () { | ||
var address = this.server.address(); | ||
@@ -273,3 +232,3 @@ this.logger.info( | ||
*/ | ||
SMTPServer.prototype._onClose = function() { | ||
SMTPServer.prototype._onClose = function () { | ||
this.logger.info('SMTP Server closed'); | ||
@@ -284,4 +243,4 @@ this.emit('close'); | ||
*/ | ||
SMTPServer.prototype._onError = function(err) { | ||
SMTPServer.prototype._onError = function (err) { | ||
this.emit('error', err); | ||
}; | ||
}; |
@@ -47,3 +47,3 @@ 'use strict'; | ||
*/ | ||
SMTPStream.prototype.oncommand = function( /* command, callback */ ) { | ||
SMTPStream.prototype.oncommand = function ( /* command, callback */ ) { | ||
throw new Error('Command handler is not set'); | ||
@@ -57,3 +57,3 @@ }; | ||
*/ | ||
SMTPStream.prototype.startDataMode = function(maxBytes) { | ||
SMTPStream.prototype.startDataMode = function (maxBytes) { | ||
this._dataMode = true; | ||
@@ -70,3 +70,3 @@ this._maxBytes = maxBytes && Number(maxBytes) || Infinity; | ||
*/ | ||
SMTPStream.prototype.continue = function() { | ||
SMTPStream.prototype.continue = function () { | ||
if (typeof this._continueCallback === 'function') { | ||
@@ -86,3 +86,3 @@ this._continueCallback(); | ||
*/ | ||
SMTPStream.prototype._write = function(chunk, encoding, next) { | ||
SMTPStream.prototype._write = function (chunk, encoding, next) { | ||
if (!chunk || !chunk.length) { | ||
@@ -97,3 +97,3 @@ return next(); | ||
var called = false; | ||
var done = function() { | ||
var done = function () { | ||
if (called) { | ||
@@ -116,3 +116,3 @@ return; | ||
var readLine = function() { | ||
var readLine = function () { | ||
var match; | ||
@@ -154,3 +154,3 @@ var line; | ||
*/ | ||
SMTPStream.prototype._feedDataStream = function(chunk, done) { | ||
SMTPStream.prototype._feedDataStream = function (chunk, done) { | ||
var i; | ||
@@ -252,3 +252,3 @@ var endseq = new Buffer('\r\n.\r\n'); | ||
*/ | ||
SMTPStream.prototype._flushData = function() { | ||
SMTPStream.prototype._flushData = function () { | ||
var line; | ||
@@ -265,3 +265,3 @@ if (this._remainder && !this.closed) { | ||
*/ | ||
SMTPStream.prototype._endDataMode = function(chunk, remainder, callback) { | ||
SMTPStream.prototype._endDataMode = function (chunk, remainder, callback) { | ||
if (this._continueCallback === true) { | ||
@@ -287,2 +287,2 @@ this._continueCallback = false; | ||
this._dataStream = null; | ||
}; | ||
}; |
@@ -89,7 +89,7 @@ 'use strict'; | ||
Object.keys(opts).forEach(function(key) { | ||
Object.keys(opts).forEach(function (key) { | ||
result[key] = opts[key]; | ||
}); | ||
Object.keys(tlsDefaults).forEach(function(key) { | ||
Object.keys(tlsDefaults).forEach(function (key) { | ||
if (!(key in result)) { | ||
@@ -96,0 +96,0 @@ result[key] = tlsDefaults[key]; |
{ | ||
"name": "smtp-server", | ||
"version": "1.7.1", | ||
"version": "1.8.0-beta.0", | ||
"description": "Create custom SMTP servers on the fly", | ||
@@ -11,10 +11,13 @@ "main": "lib/smtp-server.js", | ||
"license": "MIT", | ||
"dependencies": {}, | ||
"dependencies": { | ||
"ipv6-normalize": "^1.0.1", | ||
"nodemailer-shared": "^1.0.3" | ||
}, | ||
"devDependencies": { | ||
"chai": "^3.4.0", | ||
"chai": "^3.4.1", | ||
"grunt": "^0.4.5", | ||
"grunt-contrib-jshint": "^0.11.3", | ||
"grunt-eslint": "^17.3.1", | ||
"grunt-mocha-test": "^0.12.7", | ||
"mocha": "^2.3.3", | ||
"smtp-connection": "^1.3.1" | ||
"mocha": "^2.3.4", | ||
"smtp-connection": "^2.0.1" | ||
}, | ||
@@ -21,0 +24,0 @@ "engines": { |
@@ -33,3 +33,3 @@ # smtp-server | ||
* **options** defines the behavior of the server | ||
* **options.secure** defines if the connection should use TLS (if `true`) or not (the default). See [tls.createServer](http://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener) for additional options you can use with the options object to set up the server when using `secure`. If the server does not start in TLS mode, then it is still possible to upgrade clear text socket to TLS socket with the STARTTLS command (unless you disable support for it) | ||
* **options.secure** if `true`, the connection will use TLS. The default is `false`. If the server doesn't start in TLS mode, it is still possible to upgrade clear text socket to TLS socket with the STARTTLS command (unless you disable support for it). If secure is `true`, [additional tls options for tls.createServer](http://nodejs.org/api/tls.html#tls_tls_createserver_options_secureconnectionlistener) can be added directly onto this options object. | ||
* **options.name** optional hostname of the server, used for identifying to the client (defaults to `os.hostname()`) | ||
@@ -41,7 +41,8 @@ * **options.banner** optional greeting message. This message is appended to the default ESMTP response. | ||
* **options.hideSTARTTLS** optional boolean, if set to true then allow using STARTTLS but do not advertise or require it. It only makes sense when creating integration test servers for testing the scenario where you want to try STARTTLS even when it is not advertised | ||
* **options.sniOptions** optional [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) or an object of TLS options for SNI where servername is the key | ||
* **options.logger** optional [bunyan](https://github.com/trentm/node-bunyan) compatible logger instance. By default logs to console. If set to `false` then nothing is logged | ||
* **options.sniOptions** optional [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) or an object of TLS options for SNI where servername is the key. Overrided by SNICallback. | ||
* **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.maxClients** sets the maximum number of concurrently connected clients, defaults to `Infinity` | ||
* **options.useProxy** boolean, if set to true expects to be behind a proxy that emits a [PROXY header](http://www.haproxy.org/download/1.5/doc/proxy-protocol.txt) (version 1 only) | ||
* **options.useXClient** boolean, if set to true, enables usage of [XCLIENT](http://www.postfix.org/XCLIENT_README.html) extension to override connection properties | ||
* **options.useXClient** boolean, if set to true, enables usage of [XCLIENT](http://www.postfix.org/XCLIENT_README.html) extension to override connection properties. See `session.xClient` (Map object) for the details provided by the client | ||
* **options.useXForward** boolean, if set to true, enables usage of [XFORWARD](http://www.postfix.org/XFORWARD_README.html) extension. See `session.xForward` (Map object) for the details provided by the client | ||
* **options.socketTimeout** how many milliseconds of inactivity to allow before disconnecting the client (defaults to 1 minute) | ||
@@ -57,2 +58,9 @@ * **options.closeTimeout** how many millisceonds to wait before disconnecting pending connections once server.close() has been called (defaults to 30 seconds) | ||
#### Server Methods | ||
The `server` object returned from `new SMTPServer` has the following methods: | ||
* **listen(port)** - Begins listening on the given port | ||
* **close(callback)** - Stops the server from accepting new connections. `callback` is invoked once all client connections are closed | ||
### TLS and STARTLS notice | ||
@@ -59,0 +67,0 @@ |
@@ -0,1 +1,4 @@ | ||
/* eslint no-unused-expressions:0 */ | ||
/* globals afterEach, beforeEach, describe, it */ | ||
'use strict'; | ||
@@ -14,9 +17,9 @@ | ||
describe('SMTPServer', function() { | ||
this.timeout(10 * 1000); | ||
describe('SMTPServer', function () { | ||
this.timeout(10 * 1000); // eslint-disable-line no-invalid-this | ||
describe('Unit tests', function() { | ||
describe('Unit tests', function () { | ||
describe('#_parseAddressCommand', function() { | ||
it('should parse MAIL FROM/RCPT TO', function() { | ||
describe('#_parseAddressCommand', function () { | ||
it('should parse MAIL FROM/RCPT TO', function () { | ||
var conn = new SMTPConnection({ | ||
@@ -45,2 +48,9 @@ options: {} | ||
expect(conn._parseAddressCommand('MAIL TO', 'MAIL FROM:<test@example.com>')).to.be.false; | ||
expect(conn._parseAddressCommand('MAIL FROM', 'MAIL FROM:<sender@example.com> CUSTOM=a+ABc+20foo')).to.deep.equal({ | ||
address: 'sender@example.com', | ||
args: { | ||
CUSTOM: 'a\xabc foo' | ||
} | ||
}); | ||
}); | ||
@@ -51,3 +61,3 @@ }); | ||
describe('Plaintext server', function() { | ||
describe('Plaintext server', function () { | ||
var PORT = 1336; | ||
@@ -61,11 +71,11 @@ | ||
beforeEach(function(done) { | ||
beforeEach(function (done) { | ||
server.listen(PORT, '127.0.0.1', done); | ||
}); | ||
afterEach(function(done) { | ||
afterEach(function (done) { | ||
server.close(done); | ||
}); | ||
it('should connect without TLS', function(done) { | ||
it('should connect without TLS', function (done) { | ||
var connection = new Client({ | ||
@@ -79,3 +89,3 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.quit(); | ||
@@ -85,3 +95,3 @@ }); | ||
it('should connect with TLS', function(done) { | ||
it('should connect with TLS', function (done) { | ||
var connection = new Client({ | ||
@@ -97,3 +107,3 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.quit(); | ||
@@ -103,3 +113,3 @@ }); | ||
it('open multiple connections', function(done) { | ||
it('open multiple connections', function (done) { | ||
var limit = 5; | ||
@@ -110,3 +120,3 @@ var disconnected = 0; | ||
var createConnection = function(callback) { | ||
var createConnection = function (callback) { | ||
var connection = new Client({ | ||
@@ -120,3 +130,3 @@ port: PORT, | ||
connection.on('error', function(err) { | ||
connection.on('error', function (err) { | ||
connected++; | ||
@@ -127,10 +137,10 @@ expect(err).to.not.exist; | ||
connection.on('end', function() { | ||
connection.on('end', function () { | ||
disconnected++; | ||
if (disconnected >= limit) { | ||
done(); | ||
return done(); | ||
} | ||
}); | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connected++; | ||
@@ -141,3 +151,3 @@ callback(null, connection); | ||
var connCb = function(err, conn) { | ||
var connCb = function (err, conn) { | ||
expect(err).to.not.exist; | ||
@@ -147,3 +157,3 @@ connections.push(conn); | ||
if (connected >= limit) { | ||
connections.forEach(function(connection) { | ||
connections.forEach(function (connection) { | ||
connection.close(); | ||
@@ -160,3 +170,3 @@ }); | ||
it('should reject too many connections', function(done) { | ||
it('should reject too many connections', function (done) { | ||
var limit = 7; | ||
@@ -168,3 +178,3 @@ var expectedErrors = 2; | ||
var createConnection = function(callback) { | ||
var createConnection = function (callback) { | ||
var connection = new Client({ | ||
@@ -178,3 +188,3 @@ port: PORT, | ||
connection.on('error', function(err) { | ||
connection.on('error', function (err) { | ||
connected++; | ||
@@ -189,10 +199,10 @@ if (!expectedErrors) { | ||
connection.on('end', function() { | ||
connection.on('end', function () { | ||
disconnected++; | ||
if (disconnected >= limit) { | ||
done(); | ||
return done(); | ||
} | ||
}); | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connected++; | ||
@@ -203,3 +213,3 @@ callback(null, connection); | ||
var connCb = function(err, conn) { | ||
var connCb = function (err, conn) { | ||
expect(err).to.not.exist; | ||
@@ -209,3 +219,3 @@ connections.push(conn); | ||
if (connected >= limit) { | ||
connections.forEach(function(connection) { | ||
connections.forEach(function (connection) { | ||
connection.close(); | ||
@@ -222,3 +232,3 @@ }); | ||
it('should close on timeout', function(done) { | ||
it('should close on timeout', function (done) { | ||
var connection = new Client({ | ||
@@ -230,3 +240,3 @@ port: PORT, | ||
connection.on('error', function(err) { | ||
connection.on('error', function (err) { | ||
expect(err).to.exist; | ||
@@ -237,3 +247,3 @@ }); | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
// do nothing, wait until timeout occurs | ||
@@ -243,3 +253,3 @@ }); | ||
it('should close on timeout using secure socket', function(done) { | ||
it('should close on timeout using secure socket', function (done) { | ||
var connection = new Client({ | ||
@@ -253,3 +263,3 @@ port: PORT, | ||
connection.on('error', function(err) { | ||
connection.on('error', function (err) { | ||
expect(err).to.exist; | ||
@@ -260,3 +270,3 @@ }); | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
// do nothing, wait until timeout occurs | ||
@@ -267,4 +277,4 @@ }); | ||
describe('Plaintext server with no connection limit', function() { | ||
this.timeout(60 * 1000); | ||
describe('Plaintext server with no connection limit', function () { | ||
this.timeout(60 * 1000); // eslint-disable-line no-invalid-this | ||
@@ -279,7 +289,7 @@ var PORT = 1336; | ||
beforeEach(function(done) { | ||
beforeEach(function (done) { | ||
server.listen(PORT, '127.0.0.1', done); | ||
}); | ||
it('open multiple connections and close all at once', function(done) { | ||
it('open multiple connections and close all at once', function (done) { | ||
var limit = 100; | ||
@@ -292,3 +302,3 @@ var cleanClose = 4; | ||
var createConnection = function(callback) { | ||
var createConnection = function (callback) { | ||
var connection = new Client({ | ||
@@ -302,15 +312,15 @@ port: PORT, | ||
connection.on('error', function(err) { | ||
connection.on('error', function (err) { | ||
expect(err.responseCode).to.equal(421); // Server shutting down | ||
}); | ||
connection.on('end', function() { | ||
connection.on('end', function () { | ||
disconnected++; | ||
if (disconnected >= limit) { | ||
done(); | ||
return done(); | ||
} | ||
}); | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connected++; | ||
@@ -321,3 +331,3 @@ callback(null, connection); | ||
var connCb = function(err, conn) { | ||
var connCb = function (err, conn) { | ||
expect(err).to.not.exist; | ||
@@ -328,3 +338,3 @@ connections.push(conn); | ||
server.close(); | ||
setTimeout(function() { | ||
setTimeout(function () { | ||
for (var i = 0; i < cleanClose; i++) { | ||
@@ -344,3 +354,3 @@ connections[i].quit(); | ||
describe('Plaintext server with hidden STARTTLS', function() { | ||
describe('Plaintext server with hidden STARTTLS', function () { | ||
var PORT = 1336; | ||
@@ -355,11 +365,11 @@ | ||
beforeEach(function(done) { | ||
beforeEach(function (done) { | ||
server.listen(PORT, '127.0.0.1', done); | ||
}); | ||
afterEach(function(done) { | ||
afterEach(function (done) { | ||
server.close(done); | ||
}); | ||
it('should connect without TLS', function(done) { | ||
it('should connect without TLS', function (done) { | ||
var connection = new Client({ | ||
@@ -372,3 +382,3 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
expect(connection.secure).to.be.false; | ||
@@ -379,3 +389,3 @@ connection.quit(); | ||
it('should connect with TLS', function(done) { | ||
it('should connect with TLS', function (done) { | ||
var connection = new Client({ | ||
@@ -392,3 +402,3 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
expect(connection.secure).to.be.true; | ||
@@ -400,3 +410,3 @@ connection.quit(); | ||
describe('Plaintext server with no STARTTLS', function() { | ||
describe('Plaintext server with no STARTTLS', function () { | ||
var PORT = 1336; | ||
@@ -409,9 +419,9 @@ | ||
socketTimeout: 2 * 1000, | ||
onAuth: function(auth, session, callback) { | ||
onAuth: function (auth, session, callback) { | ||
if (auth.username === 'testuser' && auth.password === 'testpass') { | ||
callback(null, { | ||
return callback(null, { | ||
user: 'userdata' | ||
}); | ||
} else { | ||
callback(null, { | ||
return callback(null, { | ||
message: 'Authentication failed' | ||
@@ -423,11 +433,11 @@ }); | ||
beforeEach(function(done) { | ||
beforeEach(function (done) { | ||
server.listen(PORT, '127.0.0.1', done); | ||
}); | ||
afterEach(function(done) { | ||
afterEach(function (done) { | ||
server.close(done); | ||
}); | ||
it('should connect without TLS', function(done) { | ||
it('should connect without TLS', function (done) { | ||
var connection = new Client({ | ||
@@ -440,3 +450,3 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
expect(connection.secure).to.be.false; | ||
@@ -447,3 +457,3 @@ connection.quit(); | ||
it('should not connect with TLS', function(done) { | ||
it('should not connect with TLS', function (done) { | ||
var connection = new Client({ | ||
@@ -460,7 +470,7 @@ port: PORT, | ||
connection.on('error', function(err) { | ||
connection.on('error', function (err) { | ||
error = err; | ||
}); | ||
connection.on('end', function() { | ||
connection.on('end', function () { | ||
expect(error).to.exist; | ||
@@ -470,3 +480,3 @@ done(); | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
// should not be called | ||
@@ -478,3 +488,3 @@ expect(false).to.be.true; | ||
it('should close after too many unauthenticated commands', function(done) { | ||
it('should close after too many unauthenticated commands', function (done) { | ||
var connection = new Client({ | ||
@@ -486,3 +496,3 @@ port: PORT, | ||
connection.on('error', function(err) { | ||
connection.on('error', function (err) { | ||
expect(err).to.exist; | ||
@@ -493,5 +503,5 @@ }); | ||
connection.connect(function() { | ||
var looper = function() { | ||
connection._currentAction = function() { | ||
connection.connect(function () { | ||
var looper = function () { | ||
connection._currentAction = function () { | ||
looper(); | ||
@@ -505,3 +515,3 @@ }; | ||
it('should close after too many unrecognized commands', function(done) { | ||
it('should close after too many unrecognized commands', function (done) { | ||
var connection = new Client({ | ||
@@ -513,3 +523,3 @@ port: PORT, | ||
connection.on('error', function(err) { | ||
connection.on('error', function (err) { | ||
expect(err).to.exist; | ||
@@ -520,11 +530,11 @@ }); | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.login({ | ||
user: 'testuser', | ||
pass: 'testpass' | ||
}, function(err) { | ||
}, function (err) { | ||
expect(err).to.not.exist; | ||
var looper = function() { | ||
connection._currentAction = function() { | ||
var looper = function () { | ||
connection._currentAction = function () { | ||
looper(); | ||
@@ -539,9 +549,9 @@ }; | ||
it('should reject early talker', function(done) { | ||
var socket = net.connect(PORT, '127.0.0.1', function() { | ||
it('should reject early talker', function (done) { | ||
var socket = net.connect(PORT, '127.0.0.1', function () { | ||
var buffers = []; | ||
socket.on('data', function(chunk) { | ||
socket.on('data', function (chunk) { | ||
buffers.push(chunk); | ||
}); | ||
socket.on('end', function() { | ||
socket.on('end', function () { | ||
var data = Buffer.concat(buffers).toString(); | ||
@@ -555,7 +565,7 @@ expect(/^421 /.test(data)).to.be.true; | ||
it('should reject HTTP requests', function(done) { | ||
var socket = net.connect(PORT, '127.0.0.1', function() { | ||
it('should reject HTTP requests', function (done) { | ||
var socket = net.connect(PORT, '127.0.0.1', function () { | ||
var buffers = []; | ||
var started = false; | ||
socket.on('data', function(chunk) { | ||
socket.on('data', function (chunk) { | ||
buffers.push(chunk); | ||
@@ -568,3 +578,3 @@ | ||
}); | ||
socket.on('end', function() { | ||
socket.on('end', function () { | ||
var data = Buffer.concat(buffers).toString(); | ||
@@ -579,3 +589,3 @@ expect(/^554 /m.test(data)).to.be.true; | ||
describe('Secure server', function() { | ||
describe('Secure server', function () { | ||
var PORT = 1336; | ||
@@ -588,8 +598,8 @@ | ||
beforeEach(function(done) { | ||
beforeEach(function (done) { | ||
server.listen(PORT, '127.0.0.1', done); | ||
}); | ||
afterEach(function(done) { | ||
server.close(function() { | ||
afterEach(function (done) { | ||
server.close(function () { | ||
done(); | ||
@@ -599,3 +609,3 @@ }); | ||
it('should connect to secure server', function(done) { | ||
it('should connect to secure server', function (done) { | ||
var connection = new Client({ | ||
@@ -612,3 +622,3 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.quit(); | ||
@@ -619,3 +629,3 @@ }); | ||
describe('Authentication tests', function() { | ||
describe('Authentication tests', function () { | ||
var PORT = 1336; | ||
@@ -627,10 +637,10 @@ | ||
authMethods: ['PLAIN', 'LOGIN', 'XOAUTH2', 'CRAM-MD5'], | ||
onAuth: function(auth, session, callback) { | ||
onAuth: function (auth, session, callback) { | ||
if (auth.method === 'XOAUTH2') { | ||
if (auth.username === 'testuser' && auth.accessToken === 'testtoken') { | ||
callback(null, { | ||
return callback(null, { | ||
user: 'userdata' | ||
}); | ||
} else { | ||
callback(null, { | ||
return callback(null, { | ||
data: { | ||
@@ -650,7 +660,7 @@ status: '401', | ||
) { | ||
callback(null, { | ||
return callback(null, { | ||
user: 'userdata' | ||
}); | ||
} else { | ||
callback(null, { | ||
return callback(null, { | ||
message: 'Authentication failed' | ||
@@ -662,13 +672,13 @@ }); | ||
beforeEach(function(done) { | ||
beforeEach(function (done) { | ||
server.listen(PORT, '127.0.0.1', done); | ||
}); | ||
afterEach(function(done) { | ||
afterEach(function (done) { | ||
server.close(done); | ||
}); | ||
describe('PLAIN', function() { | ||
describe('PLAIN', function () { | ||
it('should authenticate', function(done) { | ||
it('should authenticate', function (done) { | ||
var connection = new Client({ | ||
@@ -685,7 +695,7 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.login({ | ||
user: 'testuser', | ||
pass: 'testpass' | ||
}, function(err) { | ||
}, function (err) { | ||
expect(err).to.not.exist; | ||
@@ -697,3 +707,3 @@ connection.quit(); | ||
it('should fail', function(done) { | ||
it('should fail', function (done) { | ||
var connection = new Client({ | ||
@@ -710,7 +720,7 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.login({ | ||
user: 'zzzz', | ||
pass: 'yyyy' | ||
}, function(err) { | ||
}, function (err) { | ||
expect(err).to.exist; | ||
@@ -723,5 +733,5 @@ connection.quit(); | ||
describe('LOGIN', function() { | ||
describe('LOGIN', function () { | ||
it('should authenticate', function(done) { | ||
it('should authenticate', function (done) { | ||
var connection = new Client({ | ||
@@ -738,7 +748,7 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.login({ | ||
user: 'testuser', | ||
pass: 'testpass' | ||
}, function(err) { | ||
}, function (err) { | ||
expect(err).to.not.exist; | ||
@@ -750,3 +760,3 @@ connection.quit(); | ||
it('should fail', function(done) { | ||
it('should fail', function (done) { | ||
var connection = new Client({ | ||
@@ -763,7 +773,7 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.login({ | ||
user: 'zzzz', | ||
pass: 'yyyy' | ||
}, function(err) { | ||
}, function (err) { | ||
expect(err).to.exist; | ||
@@ -776,5 +786,5 @@ connection.quit(); | ||
describe('XOAUTH2', function() { | ||
describe('XOAUTH2', function () { | ||
it('should authenticate', function(done) { | ||
it('should authenticate', function (done) { | ||
var connection = new Client({ | ||
@@ -791,7 +801,7 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.login({ | ||
user: 'testuser', | ||
xoauth2: 'testtoken' | ||
}, function(err) { | ||
}, function (err) { | ||
expect(err).to.not.exist; | ||
@@ -803,3 +813,3 @@ connection.quit(); | ||
it('should fail', function(done) { | ||
it('should fail', function (done) { | ||
var connection = new Client({ | ||
@@ -816,7 +826,7 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.login({ | ||
user: 'zzzz', | ||
xoauth2: 'testtoken' | ||
}, function(err) { | ||
}, function (err) { | ||
expect(err).to.exist; | ||
@@ -829,5 +839,5 @@ connection.quit(); | ||
describe('CRAM-MD5', function() { | ||
describe('CRAM-MD5', function () { | ||
it('should authenticate', function(done) { | ||
it('should authenticate', function (done) { | ||
var connection = new Client({ | ||
@@ -844,7 +854,7 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.login({ | ||
user: 'testuser', | ||
pass: 'testpass' | ||
}, function(err) { | ||
}, function (err) { | ||
expect(err).to.not.exist; | ||
@@ -856,3 +866,3 @@ connection.quit(); | ||
it('should fail', function(done) { | ||
it('should fail', function (done) { | ||
var connection = new Client({ | ||
@@ -869,7 +879,7 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.login({ | ||
user: 'zzzz', | ||
pass: 'yyyy' | ||
}, function(err) { | ||
}, function (err) { | ||
expect(err).to.exist; | ||
@@ -883,3 +893,3 @@ connection.quit(); | ||
describe('Mail tests', function() { | ||
describe('Mail tests', function () { | ||
var PORT = 1336; | ||
@@ -896,9 +906,9 @@ | ||
server.onAuth = function(auth, session, callback) { | ||
server.onAuth = function (auth, session, callback) { | ||
if (auth.username === 'testuser' && auth.password === 'testpass') { | ||
callback(null, { | ||
return callback(null, { | ||
user: 'userdata' | ||
}); | ||
} else { | ||
callback(null, { | ||
return callback(null, { | ||
message: 'Authentication failed' | ||
@@ -909,3 +919,3 @@ }); | ||
server.onMailFrom = function(address, session, callback) { | ||
server.onMailFrom = function (address, session, callback) { | ||
if (/^deny/i.test(address.address)) { | ||
@@ -917,3 +927,3 @@ return callback(new Error('Not accepted')); | ||
server.onRcptTo = function(address, session, callback) { | ||
server.onRcptTo = function (address, session, callback) { | ||
if (/^deny/i.test(address.address)) { | ||
@@ -925,7 +935,7 @@ return callback(new Error('Not accepted')); | ||
server.onData = function(stream, session, callback) { | ||
server.onData = function (stream, session, callback) { | ||
var chunks = []; | ||
var chunklen = 0; | ||
stream.on('data', function(chunk) { | ||
stream.on('data', function (chunk) { | ||
chunks.push(chunk); | ||
@@ -935,3 +945,3 @@ chunklen += chunk.length; | ||
stream.on('end', function() { | ||
stream.on('end', function () { | ||
var message = Buffer.concat(chunks, chunklen).toString(); | ||
@@ -952,4 +962,4 @@ var err; | ||
beforeEach(function(done) { | ||
server.listen(PORT, '127.0.0.1', function() { | ||
beforeEach(function (done) { | ||
server.listen(PORT, '127.0.0.1', function () { | ||
connection = new Client({ | ||
@@ -963,7 +973,7 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.login({ | ||
user: 'testuser', | ||
pass: 'testpass' | ||
}, function(err) { | ||
}, function (err) { | ||
expect(err).to.not.exist; | ||
@@ -976,4 +986,4 @@ done(); | ||
afterEach(function(done) { | ||
connection.on('end', function() { | ||
afterEach(function (done) { | ||
connection.on('end', function () { | ||
server.close(done); | ||
@@ -984,7 +994,7 @@ }); | ||
it('should send', function(done) { | ||
it('should send', function (done) { | ||
connection.send({ | ||
from: 'sender@example.com', | ||
to: ['recipient@exmaple.com'] | ||
}, 'testmessage', function(err, status) { | ||
}, 'testmessage', function (err, status) { | ||
expect(err).to.not.exist; | ||
@@ -997,7 +1007,7 @@ expect(status.accepted.length).to.equal(1); | ||
it('should reject single recipient', function(done) { | ||
it('should reject single recipient', function (done) { | ||
connection.send({ | ||
from: 'sender@example.com', | ||
to: ['recipient@exmaple.com', 'deny-recipient@example.com'] | ||
}, 'testmessage', function(err, status) { | ||
}, 'testmessage', function (err, status) { | ||
expect(err).to.not.exist; | ||
@@ -1010,7 +1020,7 @@ expect(status.accepted.length).to.equal(1); | ||
it('should reject sender', function(done) { | ||
it('should reject sender', function (done) { | ||
connection.send({ | ||
from: 'deny-sender@example.com', | ||
to: ['recipient@exmaple.com'] | ||
}, 'testmessage', function(err) { | ||
}, 'testmessage', function (err) { | ||
expect(err).to.exist; | ||
@@ -1021,7 +1031,7 @@ done(); | ||
it('should reject recipients', function(done) { | ||
it('should reject recipients', function (done) { | ||
connection.send({ | ||
from: 'sender@example.com', | ||
to: ['deny-recipient@exmaple.com'] | ||
}, 'testmessage', function(err) { | ||
}, 'testmessage', function (err) { | ||
expect(err).to.exist; | ||
@@ -1032,7 +1042,7 @@ done(); | ||
it('should reject message', function(done) { | ||
it('should reject message', function (done) { | ||
connection.send({ | ||
from: 'sender@example.com', | ||
to: ['recipient@exmaple.com'] | ||
}, 'deny-testmessage', function(err) { | ||
}, 'deny-testmessage', function (err) { | ||
expect(err).to.exist; | ||
@@ -1043,7 +1053,7 @@ done(); | ||
it('should reject too big message', function(done) { | ||
it('should reject too big message', function (done) { | ||
connection.send({ | ||
from: 'sender@example.com', | ||
to: ['recipient@exmaple.com'] | ||
}, new Array(1000).join('testmessage'), function(err) { | ||
}, new Array(1000).join('testmessage'), function (err) { | ||
expect(err).to.exist; | ||
@@ -1054,7 +1064,7 @@ done(); | ||
it('should send multiple messages', function(done) { | ||
it('should send multiple messages', function (done) { | ||
connection.send({ | ||
from: 'sender@example.com', | ||
to: ['recipient@exmaple.com'] | ||
}, 'testmessage 1', function(err, status) { | ||
}, 'testmessage 1', function (err, status) { | ||
expect(err).to.not.exist; | ||
@@ -1067,3 +1077,3 @@ expect(status.accepted.length).to.equal(1); | ||
to: ['recipient@exmaple.com'] | ||
}, 'testmessage 2', function(err, status) { | ||
}, 'testmessage 2', function (err, status) { | ||
expect(err).to.not.exist; | ||
@@ -1076,3 +1086,3 @@ expect(status.accepted.length).to.equal(1); | ||
to: ['recipient@exmaple.com'] | ||
}, 'deny-testmessage', function(err) { | ||
}, 'deny-testmessage', function (err) { | ||
expect(err).to.exist; | ||
@@ -1083,3 +1093,3 @@ | ||
to: ['recipient@exmaple.com'] | ||
}, 'testmessage 3', function(err, status) { | ||
}, 'testmessage 3', function (err, status) { | ||
expect(err).to.not.exist; | ||
@@ -1096,4 +1106,4 @@ expect(status.accepted.length).to.equal(1); | ||
describe('SMTPUTF8', function() { | ||
it('should allow addresses with UTF-8 characters', function(done) { | ||
describe('SMTPUTF8', function () { | ||
it('should allow addresses with UTF-8 characters', function (done) { | ||
var utf8Address = 'δοκιμή@παράδειγμα.δοκιμή'; | ||
@@ -1109,3 +1119,3 @@ var PORT = 1336; | ||
server.onRcptTo = function(address, session, callback) { | ||
server.onRcptTo = function (address, session, callback) { | ||
expect(utf8Address).to.equal(address.address); | ||
@@ -1115,3 +1125,3 @@ callback(); | ||
server.listen(PORT, '127.0.0.1', function() { | ||
server.listen(PORT, '127.0.0.1', function () { | ||
connection = new Client({ | ||
@@ -1122,11 +1132,11 @@ port: PORT, | ||
connection.on('end', function() { | ||
connection.on('end', function () { | ||
server.close(done); | ||
}); | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.send({ | ||
from: 'sender@example.com', | ||
to: [utf8Address] | ||
}, 'testmessage', function(err, status) { | ||
}, 'testmessage', function (err, status) { | ||
expect(err).to.not.exist; | ||
@@ -1142,4 +1152,4 @@ expect(status.accepted.length).to.equal(1); | ||
describe('#onData', function() { | ||
it('should accept a prematurely called continue callback', function(done) { | ||
describe('#onData', function () { | ||
it('should accept a prematurely called continue callback', function (done) { | ||
var PORT = 1336; | ||
@@ -1154,3 +1164,3 @@ | ||
server.onData = function(stream, session, callback) { | ||
server.onData = function (stream, session, callback) { | ||
stream.pipe(fs.createWriteStream('/dev/null')); | ||
@@ -1160,3 +1170,3 @@ callback(); | ||
server.listen(PORT, '127.0.0.1', function() { | ||
server.listen(PORT, '127.0.0.1', function () { | ||
connection = new Client({ | ||
@@ -1167,11 +1177,11 @@ port: PORT, | ||
connection.on('end', function() { | ||
connection.on('end', function () { | ||
server.close(done); | ||
}); | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
connection.send({ | ||
from: 'sender@example.com', | ||
to: ['receiver@example.com'] | ||
}, new Array(1024 * 1024).join('#'), function(err) { | ||
}, new Array(1024 * 1024).join('#'), function (err) { | ||
expect(err).to.not.exist; | ||
@@ -1185,3 +1195,3 @@ connection.quit(); | ||
describe('PROXY server', function() { | ||
describe('PROXY server', function () { | ||
var PORT = 1336; | ||
@@ -1193,4 +1203,4 @@ | ||
useProxy: true, | ||
onConnect: function(session, callback){ | ||
if(session.remoteAddress === '1.2.3.4'){ | ||
onConnect: function (session, callback) { | ||
if (session.remoteAddress === '1.2.3.4') { | ||
return callback(new Error('Blacklisted IP')); | ||
@@ -1202,11 +1212,11 @@ } | ||
beforeEach(function(done) { | ||
beforeEach(function (done) { | ||
server.listen(PORT, '127.0.0.1', done); | ||
}); | ||
afterEach(function(done) { | ||
afterEach(function (done) { | ||
server.close(done); | ||
}); | ||
it('should rewrite remote address value', function(done) { | ||
it('should rewrite remote address value', function (done) { | ||
var connection = new Client({ | ||
@@ -1220,6 +1230,6 @@ port: PORT, | ||
connection.connect(function() { | ||
connection.connect(function () { | ||
var conn; | ||
// get first connection | ||
server.connections.forEach(function(val) { | ||
server.connections.forEach(function (val) { | ||
if (!conn) { | ||
@@ -1237,9 +1247,9 @@ conn = val; | ||
it('should block blacklisted connection', function(done) { | ||
var socket = net.connect(PORT, '127.0.0.1', function() { | ||
it('should block blacklisted connection', function (done) { | ||
var socket = net.connect(PORT, '127.0.0.1', function () { | ||
var buffers = []; | ||
socket.on('data', function(chunk) { | ||
socket.on('data', function (chunk) { | ||
buffers.push(chunk); | ||
}); | ||
socket.on('end', function() { | ||
socket.on('end', function () { | ||
var data = Buffer.concat(buffers).toString(); | ||
@@ -1246,0 +1256,0 @@ expect(data.indexOf('554 ')).to.equal(0); |
@@ -0,1 +1,4 @@ | ||
/* eslint no-unused-expressions:0 */ | ||
/* globals describe, it */ | ||
'use strict'; | ||
@@ -9,4 +12,4 @@ | ||
describe('SMTPStream', function() { | ||
it('should emit commands', function(done) { | ||
describe('SMTPStream', function () { | ||
it('should emit commands', function (done) { | ||
var stream = new SMTPStream(); | ||
@@ -20,8 +23,8 @@ | ||
stream.oncommand = function(cmd, cb) { | ||
stream.oncommand = function (cmd, cb) { | ||
expect(cmd).to.deep.equal(expecting.shift()); | ||
if (cb) { | ||
cb(); | ||
return cb(); | ||
} else { | ||
done(); | ||
return done(); | ||
} | ||
@@ -33,3 +36,3 @@ }; | ||
it('should start data stream', function(done) { | ||
it('should start data stream', function (done) { | ||
var stream = new SMTPStream(); | ||
@@ -42,3 +45,3 @@ | ||
stream.oncommand = function(cmd, cb) { | ||
stream.oncommand = function (cmd, cb) { | ||
cmd = cmd.toString(); | ||
@@ -51,6 +54,6 @@ expect(cmd).to.deep.equal(expecting.shift()); | ||
datastream = stream.startDataMode(); | ||
datastream.on('data', function(chunk) { | ||
datastream.on('data', function (chunk) { | ||
output += chunk.toString(); | ||
}); | ||
datastream.on('end', function() { | ||
datastream.on('end', function () { | ||
expect(output).to.equal('test1\r\n.test2\r\n.test3'); | ||
@@ -62,5 +65,5 @@ stream.continue(); | ||
if (cb) { | ||
cb(); | ||
return cb(); | ||
} else { | ||
done(); | ||
return done(); | ||
} | ||
@@ -71,2 +74,2 @@ }; | ||
}); | ||
}); | ||
}); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
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
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
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
128961
2765
411
2
2
+ Addedipv6-normalize@^1.0.1
+ Addednodemailer-shared@^1.0.3
+ Addedipv6-normalize@1.0.1(transitive)
+ Addednodemailer-fetch@1.6.0(transitive)
+ Addednodemailer-shared@1.1.0(transitive)