Initial commit
This commit is contained in:
488
node_modules/smtp-server/lib/sasl.js
generated
vendored
Normal file
488
node_modules/smtp-server/lib/sasl.js
generated
vendored
Normal file
@@ -0,0 +1,488 @@
|
||||
'use strict';
|
||||
|
||||
const util = require('util');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const SASL = (module.exports = {
|
||||
SASL_PLAIN(args, callback) {
|
||||
if (args.length > 1) {
|
||||
this.send(501, 'Error: syntax: AUTH PLAIN token');
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!args.length) {
|
||||
this._nextHandler = SASL.PLAIN_token.bind(this, true);
|
||||
this.send(334);
|
||||
return callback();
|
||||
}
|
||||
|
||||
SASL.PLAIN_token.call(this, false, args[0], callback);
|
||||
},
|
||||
|
||||
SASL_LOGIN(args, callback) {
|
||||
if (args.length > 1) {
|
||||
this.send(501, 'Error: syntax: AUTH LOGIN');
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!args.length) {
|
||||
this._nextHandler = SASL.LOGIN_username.bind(this, true);
|
||||
this.send(334, 'VXNlcm5hbWU6');
|
||||
return callback();
|
||||
}
|
||||
|
||||
SASL.LOGIN_username.call(this, false, args[0], callback);
|
||||
},
|
||||
|
||||
SASL_XOAUTH2(args, callback) {
|
||||
if (args.length > 1) {
|
||||
this.send(501, 'Error: syntax: AUTH XOAUTH2 token');
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!args.length) {
|
||||
this._nextHandler = SASL.XOAUTH2_token.bind(this, true);
|
||||
this.send(334);
|
||||
return callback();
|
||||
}
|
||||
|
||||
SASL.XOAUTH2_token.call(this, false, args[0], callback);
|
||||
},
|
||||
|
||||
'SASL_CRAM-MD5'(args, callback) {
|
||||
if (args.length) {
|
||||
this.send(501, 'Error: syntax: AUTH CRAM-MD5');
|
||||
return callback();
|
||||
}
|
||||
|
||||
let challenge = util.format(
|
||||
'<%s%s@%s>',
|
||||
String(Math.random())
|
||||
.replace(/^[0.]+/, '')
|
||||
.substr(0, 8), // random numbers
|
||||
Math.floor(Date.now() / 1000), // timestamp
|
||||
this.name // hostname
|
||||
);
|
||||
|
||||
this._nextHandler = SASL['CRAM-MD5_token'].bind(this, true, challenge);
|
||||
this.send(334, Buffer.from(challenge).toString('base64'));
|
||||
return callback();
|
||||
},
|
||||
|
||||
PLAIN_token(canAbort, token, callback) {
|
||||
token = (token || '').toString().trim();
|
||||
|
||||
if (canAbort && token === '*') {
|
||||
this.send(501, 'Authentication aborted');
|
||||
return callback();
|
||||
}
|
||||
|
||||
let data = Buffer.from(token, 'base64').toString().split('\x00');
|
||||
|
||||
if (data.length !== 3) {
|
||||
this.send(500, 'Error: invalid userdata');
|
||||
return callback();
|
||||
}
|
||||
|
||||
let username = data[1] || data[0] || '';
|
||||
let password = data[2] || '';
|
||||
|
||||
this._server.onAuth(
|
||||
{
|
||||
method: 'PLAIN',
|
||||
username,
|
||||
password
|
||||
},
|
||||
this.session,
|
||||
(err, response) => {
|
||||
if (err) {
|
||||
this._server.logger.info(
|
||||
{
|
||||
err,
|
||||
tnx: 'autherror',
|
||||
cid: this.id,
|
||||
method: 'PLAIN',
|
||||
user: username
|
||||
},
|
||||
'Authentication error for %s using %s. %s',
|
||||
username,
|
||||
'PLAIN',
|
||||
err.message
|
||||
);
|
||||
this.send(err.responseCode || 535, err.message);
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!response.user) {
|
||||
this._server.logger.info(
|
||||
{
|
||||
tnx: 'authfail',
|
||||
cid: this.id,
|
||||
method: 'PLAIN',
|
||||
user: username
|
||||
},
|
||||
'Authentication failed for %s using %s',
|
||||
username,
|
||||
'PLAIN'
|
||||
);
|
||||
this.send(response.responseCode || 535, response.message || 'Error: Authentication credentials invalid');
|
||||
return callback();
|
||||
}
|
||||
|
||||
this._server.logger.info(
|
||||
{
|
||||
tnx: 'auth',
|
||||
cid: this.id,
|
||||
method: 'PLAIN',
|
||||
user: username
|
||||
},
|
||||
'%s authenticated using %s',
|
||||
username,
|
||||
'PLAIN'
|
||||
);
|
||||
this.session.user = response.user;
|
||||
this.session.transmissionType = this._transmissionType();
|
||||
|
||||
this.send(235, 'Authentication successful');
|
||||
callback();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
LOGIN_username(canAbort, username, callback) {
|
||||
username = (username || '').toString().trim();
|
||||
|
||||
if (canAbort && username === '*') {
|
||||
this.send(501, 'Authentication aborted');
|
||||
return callback();
|
||||
}
|
||||
|
||||
username = Buffer.from(username, 'base64').toString();
|
||||
|
||||
if (!username) {
|
||||
this.send(500, 'Error: missing username');
|
||||
return callback();
|
||||
}
|
||||
|
||||
this._nextHandler = SASL.LOGIN_password.bind(this, username);
|
||||
this.send(334, 'UGFzc3dvcmQ6');
|
||||
return callback();
|
||||
},
|
||||
|
||||
LOGIN_password(username, password, callback) {
|
||||
password = (password || '').toString().trim();
|
||||
|
||||
if (password === '*') {
|
||||
this.send(501, 'Authentication aborted');
|
||||
return callback();
|
||||
}
|
||||
|
||||
password = Buffer.from(password, 'base64').toString();
|
||||
|
||||
this._server.onAuth(
|
||||
{
|
||||
method: 'LOGIN',
|
||||
username,
|
||||
password
|
||||
},
|
||||
this.session,
|
||||
(err, response) => {
|
||||
if (err) {
|
||||
this._server.logger.info(
|
||||
{
|
||||
err,
|
||||
tnx: 'autherror',
|
||||
cid: this.id,
|
||||
method: 'LOGIN',
|
||||
user: username
|
||||
},
|
||||
'Authentication error for %s using %s. %s',
|
||||
username,
|
||||
'LOGIN',
|
||||
err.message
|
||||
);
|
||||
this.send(err.responseCode || 535, err.message);
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!response.user) {
|
||||
this._server.logger.info(
|
||||
{
|
||||
tnx: 'authfail',
|
||||
cid: this.id,
|
||||
method: 'LOGIN',
|
||||
user: username
|
||||
},
|
||||
'Authentication failed for %s using %s',
|
||||
username,
|
||||
'LOGIN'
|
||||
);
|
||||
this.send(response.responseCode || 535, response.message || 'Error: Authentication credentials invalid');
|
||||
return callback();
|
||||
}
|
||||
|
||||
this._server.logger.info(
|
||||
{
|
||||
tnx: 'auth',
|
||||
cid: this.id,
|
||||
method: 'PLAIN',
|
||||
user: username
|
||||
},
|
||||
'%s authenticated using %s',
|
||||
username,
|
||||
'LOGIN'
|
||||
);
|
||||
this.session.user = response.user;
|
||||
this.session.transmissionType = this._transmissionType();
|
||||
|
||||
this.send(235, 'Authentication successful');
|
||||
callback();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
XOAUTH2_token(canAbort, token, callback) {
|
||||
token = (token || '').toString().trim();
|
||||
|
||||
if (canAbort && token === '*') {
|
||||
this.send(501, 'Authentication aborted');
|
||||
return callback();
|
||||
}
|
||||
|
||||
let username;
|
||||
let accessToken;
|
||||
|
||||
// Find username and access token from the input
|
||||
Buffer.from(token, 'base64')
|
||||
.toString()
|
||||
.split('\x01')
|
||||
.forEach(part => {
|
||||
part = part.split('=');
|
||||
let key = part.shift().toLowerCase();
|
||||
let value = part.join('=').trim();
|
||||
|
||||
if (key === 'user') {
|
||||
username = value;
|
||||
} else if (key === 'auth') {
|
||||
value = value.split(/\s+/);
|
||||
if (value.shift().toLowerCase() === 'bearer') {
|
||||
accessToken = value.join(' ');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!username || !accessToken) {
|
||||
this.send(500, 'Error: invalid userdata');
|
||||
return callback();
|
||||
}
|
||||
|
||||
this._server.onAuth(
|
||||
{
|
||||
method: 'XOAUTH2',
|
||||
username,
|
||||
accessToken
|
||||
},
|
||||
this.session,
|
||||
(err, response) => {
|
||||
if (err) {
|
||||
this._server.logger.info(
|
||||
{
|
||||
err,
|
||||
tnx: 'autherror',
|
||||
cid: this.id,
|
||||
method: 'XOAUTH2',
|
||||
user: username
|
||||
},
|
||||
'Authentication error for %s using %s. %s',
|
||||
username,
|
||||
'XOAUTH2',
|
||||
err.message
|
||||
);
|
||||
this.send(err.responseCode || 535, err.message);
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!response.user) {
|
||||
this._server.logger.info(
|
||||
{
|
||||
tnx: 'authfail',
|
||||
cid: this.id,
|
||||
method: 'XOAUTH2',
|
||||
user: username
|
||||
},
|
||||
'Authentication failed for %s using %s',
|
||||
username,
|
||||
'XOAUTH2'
|
||||
);
|
||||
this._nextHandler = SASL.XOAUTH2_error.bind(this);
|
||||
this.send(response.responseCode || 334, Buffer.from(JSON.stringify(response.data || {})).toString('base64'));
|
||||
return callback();
|
||||
}
|
||||
|
||||
this._server.logger.info(
|
||||
{
|
||||
tnx: 'auth',
|
||||
cid: this.id,
|
||||
method: 'XOAUTH2',
|
||||
user: username
|
||||
},
|
||||
'%s authenticated using %s',
|
||||
username,
|
||||
'XOAUTH2'
|
||||
);
|
||||
this.session.user = response.user;
|
||||
this.session.transmissionType = this._transmissionType();
|
||||
|
||||
this.send(235, 'Authentication successful');
|
||||
callback();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
XOAUTH2_error(data, callback) {
|
||||
this.send(535, 'Error: Username and Password not accepted');
|
||||
return callback();
|
||||
},
|
||||
|
||||
'CRAM-MD5_token'(canAbort, challenge, token, callback) {
|
||||
token = (token || '').toString().trim();
|
||||
|
||||
if (canAbort && token === '*') {
|
||||
this.send(501, 'Authentication aborted');
|
||||
return callback();
|
||||
}
|
||||
|
||||
let tokenParts = Buffer.from(token, 'base64').toString().split(' ');
|
||||
let username = tokenParts.shift();
|
||||
let challengeResponse = (tokenParts.shift() || '').toLowerCase();
|
||||
|
||||
this._server.onAuth(
|
||||
{
|
||||
method: 'CRAM-MD5',
|
||||
username,
|
||||
challenge,
|
||||
challengeResponse,
|
||||
validatePassword(password) {
|
||||
let hmac = crypto.createHmac('md5', password);
|
||||
return hmac.update(challenge).digest('hex').toLowerCase() === challengeResponse;
|
||||
}
|
||||
},
|
||||
this.session,
|
||||
(err, response) => {
|
||||
if (err) {
|
||||
this._server.logger.info(
|
||||
{
|
||||
err,
|
||||
tnx: 'autherror',
|
||||
cid: this.id,
|
||||
method: 'CRAM-MD5',
|
||||
user: username
|
||||
},
|
||||
'Authentication error for %s using %s. %s',
|
||||
username,
|
||||
'CRAM-MD5',
|
||||
err.message
|
||||
);
|
||||
this.send(err.responseCode || 535, err.message);
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!response.user) {
|
||||
this._server.logger.info(
|
||||
{
|
||||
tnx: 'authfail',
|
||||
cid: this.id,
|
||||
method: 'CRAM-MD5',
|
||||
user: username
|
||||
},
|
||||
'Authentication failed for %s using %s',
|
||||
username,
|
||||
'CRAM-MD5'
|
||||
);
|
||||
this.send(response.responseCode || 535, response.message || 'Error: Authentication credentials invalid');
|
||||
return callback();
|
||||
}
|
||||
|
||||
this._server.logger.info(
|
||||
{
|
||||
tnx: 'auth',
|
||||
cid: this.id,
|
||||
method: 'CRAM-MD5',
|
||||
user: username
|
||||
},
|
||||
'%s authenticated using %s',
|
||||
username,
|
||||
'CRAM-MD5'
|
||||
);
|
||||
this.session.user = response.user;
|
||||
this.session.transmissionType = this._transmissionType();
|
||||
|
||||
this.send(235, 'Authentication successful');
|
||||
callback();
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
// this is not a real auth but a username validation initiated by SMTP proxy
|
||||
SASL_XCLIENT(args, callback) {
|
||||
const username = ((args && args[0]) || '').toString().trim();
|
||||
this._server.onAuth(
|
||||
{
|
||||
method: 'XCLIENT',
|
||||
username,
|
||||
password: null
|
||||
},
|
||||
this.session,
|
||||
(err, response) => {
|
||||
if (err) {
|
||||
this._server.logger.info(
|
||||
{
|
||||
err,
|
||||
tnx: 'autherror',
|
||||
cid: this.id,
|
||||
method: 'XCLIENT',
|
||||
user: username
|
||||
},
|
||||
'Authentication error for %s using %s. %s',
|
||||
username,
|
||||
'XCLIENT',
|
||||
err.message
|
||||
);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!response.user) {
|
||||
this._server.logger.info(
|
||||
{
|
||||
tnx: 'authfail',
|
||||
cid: this.id,
|
||||
method: 'XCLIENT',
|
||||
user: username
|
||||
},
|
||||
'Authentication failed for %s using %s',
|
||||
username,
|
||||
'XCLIENT'
|
||||
);
|
||||
return callback(new Error('Authentication credentials invalid'));
|
||||
}
|
||||
|
||||
this._server.logger.info(
|
||||
{
|
||||
tnx: 'auth',
|
||||
cid: this.id,
|
||||
method: 'XCLIENT',
|
||||
user: username
|
||||
},
|
||||
'%s authenticated using %s',
|
||||
username,
|
||||
'XCLIENT'
|
||||
);
|
||||
|
||||
this.session.user = response.user;
|
||||
this.session.transmissionType = this._transmissionType();
|
||||
|
||||
callback();
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
1935
node_modules/smtp-server/lib/smtp-connection.js
generated
vendored
Normal file
1935
node_modules/smtp-server/lib/smtp-connection.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
564
node_modules/smtp-server/lib/smtp-server.js
generated
vendored
Normal file
564
node_modules/smtp-server/lib/smtp-server.js
generated
vendored
Normal file
@@ -0,0 +1,564 @@
|
||||
'use strict';
|
||||
|
||||
const net = require('net');
|
||||
const tls = require('tls');
|
||||
const SMTPConnection = require('./smtp-connection').SMTPConnection;
|
||||
const tlsOptions = require('./tls-options');
|
||||
const EventEmitter = require('events');
|
||||
const shared = require('nodemailer/lib/shared');
|
||||
const punycode = require('punycode.js');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const CLOSE_TIMEOUT = 30 * 1000; // how much to wait until pending connections are terminated
|
||||
|
||||
/**
|
||||
* Creates a SMTP server instance.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} options Connection and SMTP optionsž
|
||||
*/
|
||||
class SMTPServer extends EventEmitter {
|
||||
constructor(options) {
|
||||
super();
|
||||
|
||||
this.options = options || {};
|
||||
|
||||
this.updateSecureContext();
|
||||
|
||||
// setup disabled commands list
|
||||
this.options.disabledCommands = [].concat(this.options.disabledCommands || []).map(command => (command || '').toString().toUpperCase().trim());
|
||||
|
||||
// setup allowed auth methods
|
||||
this.options.authMethods = [].concat(this.options.authMethods || []).map(method => (method || '').toString().toUpperCase().trim());
|
||||
|
||||
if (!this.options.authMethods.length) {
|
||||
this.options.authMethods = ['LOGIN', 'PLAIN'];
|
||||
}
|
||||
|
||||
// set default value for hideENHANCEDSTATUSCODES to true (disable enhanced status codes by default)
|
||||
if (this.options.hideENHANCEDSTATUSCODES === undefined) {
|
||||
this.options.hideENHANCEDSTATUSCODES = true;
|
||||
}
|
||||
|
||||
// set default value for hideDSN to true (disable delivery status notifications by default)
|
||||
if (this.options.hideDSN === undefined) {
|
||||
this.options.hideDSN = true;
|
||||
}
|
||||
|
||||
// set default value for hideREQUIRETLS to true (disable REQUIRETLS by default, opt-in)
|
||||
if (this.options.hideREQUIRETLS === undefined) {
|
||||
this.options.hideREQUIRETLS = true;
|
||||
}
|
||||
|
||||
this.logger = shared.getLogger(this.options, {
|
||||
component: this.options.component || 'smtp-server'
|
||||
});
|
||||
|
||||
// apply shorthand handlers
|
||||
['onConnect', 'onSecure', 'onAuth', 'onMailFrom', 'onRcptTo', 'onData', 'onClose'].forEach(handler => {
|
||||
if (typeof this.options[handler] === 'function') {
|
||||
this[handler] = this.options[handler];
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Timeout after close has been called until pending connections are forcibly closed
|
||||
*/
|
||||
this._closeTimeout = false;
|
||||
|
||||
/**
|
||||
* A set of all currently open connections
|
||||
*/
|
||||
this.connections = new Set();
|
||||
|
||||
// setup server listener and connection handler
|
||||
if (this.options.secure && !this.options.needsUpgrade) {
|
||||
this.server = net.createServer(this.options, socket => {
|
||||
this._handleProxy(socket, (err, socketOptions) => {
|
||||
if (err) {
|
||||
// ignore, should not happen
|
||||
}
|
||||
if (this.options.secured) {
|
||||
return this.connect(socket, socketOptions);
|
||||
}
|
||||
this._upgrade(socket, (err, tlsSocket) => {
|
||||
if (err) {
|
||||
return this._onError(err);
|
||||
}
|
||||
this.connect(tlsSocket, socketOptions);
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.server = net.createServer(this.options, socket =>
|
||||
this._handleProxy(socket, (err, socketOptions) => {
|
||||
if (err) {
|
||||
// ignore, should not happen
|
||||
}
|
||||
this.connect(socket, socketOptions);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this._setListeners();
|
||||
}
|
||||
|
||||
connect(socket, socketOptions) {
|
||||
let connection = new SMTPConnection(this, socket, socketOptions);
|
||||
this.connections.add(connection);
|
||||
connection.on('error', err => this._onError(err));
|
||||
connection.on('connect', data => this._onClientConnect(data));
|
||||
connection.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening on selected port and interface
|
||||
*/
|
||||
listen(...args) {
|
||||
return this.server.listen(...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the server
|
||||
*
|
||||
* @param {Function} callback Callback to run once the server is fully closed
|
||||
*/
|
||||
close(callback) {
|
||||
let connections = this.connections.size;
|
||||
let timeout = this.options.closeTimeout || CLOSE_TIMEOUT;
|
||||
|
||||
// stop accepting new connections
|
||||
this.server.close(() => {
|
||||
clearTimeout(this._closeTimeout);
|
||||
if (typeof callback === 'function') {
|
||||
return callback();
|
||||
}
|
||||
});
|
||||
|
||||
// close active connections
|
||||
if (connections) {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'close'
|
||||
},
|
||||
'Server closing with %s pending connection%s, waiting %s seconds before terminating',
|
||||
connections,
|
||||
connections !== 1 ? 's' : '',
|
||||
timeout / 1000
|
||||
);
|
||||
}
|
||||
|
||||
this._closeTimeout = setTimeout(() => {
|
||||
connections = this.connections.size;
|
||||
if (connections) {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'close'
|
||||
},
|
||||
'Closing %s pending connection%s to close the server',
|
||||
connections,
|
||||
connections !== 1 ? 's' : ''
|
||||
);
|
||||
|
||||
this.connections.forEach(connection => {
|
||||
connection.send(421, 'Server shutting down');
|
||||
connection.close();
|
||||
});
|
||||
}
|
||||
if (typeof callback === 'function') {
|
||||
const realCallback = callback;
|
||||
callback = null;
|
||||
return realCallback();
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
// Unref timer so it doesn't prevent process exit if all connections close naturally
|
||||
this._closeTimeout.unref();
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication handler. Override this
|
||||
*
|
||||
* @param {Object} auth Authentication options
|
||||
* @param {Function} callback Callback to run once the user is authenticated
|
||||
*/
|
||||
onAuth(auth, session, callback) {
|
||||
if (auth.method === 'XOAUTH2') {
|
||||
return callback(null, {
|
||||
data: {
|
||||
status: '401',
|
||||
schemes: 'bearer mac',
|
||||
scope: 'https://mail.google.com/'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (auth.method === 'XCLIENT') {
|
||||
return callback(); // pass through
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
message: 'Authentication not implemented'
|
||||
});
|
||||
}
|
||||
|
||||
onConnect(session, callback) {
|
||||
setImmediate(callback);
|
||||
}
|
||||
|
||||
onMailFrom(address, session, callback) {
|
||||
setImmediate(callback);
|
||||
}
|
||||
|
||||
onRcptTo(address, session, callback) {
|
||||
setImmediate(callback);
|
||||
}
|
||||
onSecure(socket, session, callback) {
|
||||
setImmediate(callback);
|
||||
}
|
||||
onData(stream, session, callback) {
|
||||
let chunklen = 0;
|
||||
|
||||
stream.on('data', chunk => {
|
||||
chunklen += chunk.length;
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'message',
|
||||
size: chunklen
|
||||
},
|
||||
'<received %s bytes>',
|
||||
chunklen
|
||||
);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
onClose(/* session */) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
updateSecureContext(options) {
|
||||
Object.keys(options || {}).forEach(key => {
|
||||
this.options[key] = options[key];
|
||||
});
|
||||
|
||||
let defaultTlsOptions = tlsOptions(this.options);
|
||||
|
||||
this.secureContext = new Map();
|
||||
this.secureContext.set('*', tls.createSecureContext(defaultTlsOptions));
|
||||
|
||||
let ctxMap = this.options.sniOptions || {};
|
||||
// sniOptions is either an object or a Map with domain names as keys and TLS option objects as values
|
||||
if (typeof ctxMap.get === 'function') {
|
||||
ctxMap.forEach((ctx, servername) => {
|
||||
this.secureContext.set(this._normalizeHostname(servername), tls.createSecureContext(tlsOptions(ctx)));
|
||||
});
|
||||
} else {
|
||||
Object.keys(ctxMap).forEach(servername => {
|
||||
this.secureContext.set(this._normalizeHostname(servername), tls.createSecureContext(tlsOptions(ctxMap[servername])));
|
||||
});
|
||||
}
|
||||
|
||||
if (this.options.secure) {
|
||||
// appy changes
|
||||
|
||||
Object.keys(defaultTlsOptions || {}).forEach(key => {
|
||||
if (!(key in this.options)) {
|
||||
this.options[key] = defaultTlsOptions[key];
|
||||
}
|
||||
});
|
||||
|
||||
// ensure SNICallback method
|
||||
if (typeof this.options.SNICallback !== 'function') {
|
||||
// create default SNI handler
|
||||
this.options.SNICallback = (servername, cb) => {
|
||||
cb(null, this.secureContext.get(servername));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE METHODS
|
||||
|
||||
/**
|
||||
* Setup server event handlers
|
||||
*/
|
||||
_setListeners() {
|
||||
let server = this.server;
|
||||
server.once('listening', (...args) => this._onListening(...args));
|
||||
server.once('close', (...args) => this._onClose(server, ...args));
|
||||
server.on('error', (...args) => this._onError(...args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when server started listening
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
_onListening() {
|
||||
let address = this.server.address();
|
||||
|
||||
// address will be null if listener is using Unix socket
|
||||
if (address === null) {
|
||||
address = { address: null, port: null, family: null };
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
//
|
||||
{
|
||||
tnx: 'listen',
|
||||
host: address.address,
|
||||
port: address.port,
|
||||
secure: !!this.options.secure,
|
||||
protocol: this.options.lmtp ? 'LMTP' : 'SMTP'
|
||||
},
|
||||
'%s%s Server listening on %s:%s',
|
||||
this.options.secure ? 'Secure ' : '',
|
||||
this.options.lmtp ? 'LMTP' : 'SMTP',
|
||||
address.family === 'IPv4' ? address.address : '[' + address.address + ']',
|
||||
address.port
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when server is closed
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
_onClose(server) {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'closed'
|
||||
},
|
||||
(this.options.lmtp ? 'LMTP' : 'SMTP') + ' Server closed'
|
||||
);
|
||||
if (server !== this.server) {
|
||||
// older instance was closed
|
||||
return;
|
||||
}
|
||||
this.emit('close');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an error occurs with the server
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
_onError(err) {
|
||||
this.emit('error', err);
|
||||
}
|
||||
|
||||
_handleProxy(socket, callback) {
|
||||
let socketOptions = {
|
||||
id: BigInt('0x' + crypto.randomBytes(10).toString('hex')).toString(32).padStart(16, '0')
|
||||
};
|
||||
|
||||
if (
|
||||
!this.options.useProxy ||
|
||||
(Array.isArray(this.options.useProxy) && !this.options.useProxy.includes(socket.remoteAddress) && !this.options.useProxy.includes('*'))
|
||||
) {
|
||||
socketOptions.ignore = this.options.ignoredHosts && this.options.ignoredHosts.includes(socket.remoteAddress);
|
||||
return setImmediate(() => callback(null, socketOptions));
|
||||
}
|
||||
|
||||
let chunks = [];
|
||||
let chunklen = 0;
|
||||
let socketReader = () => {
|
||||
let chunk;
|
||||
while ((chunk = socket.read()) !== null) {
|
||||
for (let i = 0, len = chunk.length; i < len; i++) {
|
||||
let chr = chunk[i];
|
||||
if (chr === 0x0a) {
|
||||
socket.removeListener('readable', socketReader);
|
||||
chunks.push(chunk.slice(0, i + 1));
|
||||
chunklen += i + 1;
|
||||
let remainder = chunk.slice(i + 1);
|
||||
if (remainder.length) {
|
||||
socket.unshift(remainder);
|
||||
}
|
||||
|
||||
let header = Buffer.concat(chunks, chunklen).toString().trim();
|
||||
|
||||
let params = (header || '').toString().split(' ');
|
||||
let commandName = params.shift().toUpperCase();
|
||||
if (commandName !== 'PROXY') {
|
||||
try {
|
||||
socket.end('* BAD Invalid PROXY header\r\n');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (params[1]) {
|
||||
socketOptions.remoteAddress = params[1].trim().toLowerCase();
|
||||
|
||||
socketOptions.ignore = this.options.ignoredHosts && this.options.ignoredHosts.includes(socketOptions.remoteAddress);
|
||||
|
||||
try {
|
||||
if (!socketOptions.ignore) {
|
||||
this.logger.info(
|
||||
{
|
||||
tnx: 'proxy',
|
||||
cid: socketOptions.id,
|
||||
proxy: params[1].trim().toLowerCase()
|
||||
},
|
||||
'[%s] PROXY from %s through %s (%s)',
|
||||
socketOptions.id,
|
||||
params[1].trim().toLowerCase(),
|
||||
params[2].trim().toLowerCase(),
|
||||
JSON.stringify(params)
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
socket.end('* BAD Invalid PROXY header\r\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (params[3]) {
|
||||
socketOptions.remotePort = Number(params[3].trim()) || socketOptions.remotePort;
|
||||
}
|
||||
}
|
||||
|
||||
return callback(null, socketOptions);
|
||||
}
|
||||
}
|
||||
chunks.push(chunk);
|
||||
chunklen += chunk.length;
|
||||
}
|
||||
};
|
||||
socket.on('readable', socketReader);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a new connection is established. This might not be the same time the socket is opened
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
_onClientConnect(data) {
|
||||
this.emit('connect', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize hostname
|
||||
*
|
||||
* @event
|
||||
*/
|
||||
_normalizeHostname(hostname) {
|
||||
try {
|
||||
hostname = punycode.toUnicode((hostname || '').toString().trim()).toLowerCase();
|
||||
} catch (E) {
|
||||
this.logger.error(
|
||||
{
|
||||
tnx: 'punycode'
|
||||
},
|
||||
'Failed to process punycode domain "%s". error=%s',
|
||||
hostname,
|
||||
E.message
|
||||
);
|
||||
}
|
||||
|
||||
return hostname;
|
||||
}
|
||||
|
||||
_upgrade(socket, callback) {
|
||||
let socketOptions = {
|
||||
secureContext: this.secureContext.get('*'),
|
||||
isServer: true,
|
||||
server: this.server,
|
||||
SNICallback: (servername, cb) => {
|
||||
// eslint-disable-next-line new-cap
|
||||
this.options.SNICallback(this._normalizeHostname(servername), (err, context) => {
|
||||
if (err) {
|
||||
this.logger.error(
|
||||
{
|
||||
tnx: 'sni',
|
||||
servername,
|
||||
err
|
||||
},
|
||||
'Failed to fetch SNI context for servername %s',
|
||||
servername
|
||||
);
|
||||
}
|
||||
return cb(null, context || this.secureContext.get('*'));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let returned = false;
|
||||
let tlsSocket;
|
||||
|
||||
let onError = err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
|
||||
const meta = {};
|
||||
if (tlsSocket) {
|
||||
meta.tlsProtocol = tlsSocket.getProtocol();
|
||||
}
|
||||
meta.protocol = 'smtp';
|
||||
meta.stage = 'connect';
|
||||
meta.remoteAddress = socket.remoteAddress;
|
||||
|
||||
if (err) {
|
||||
err.meta = meta;
|
||||
}
|
||||
|
||||
if (err && /SSL[23]*_GET_CLIENT_HELLO|ssl[23]*_read_bytes|ssl_bytes_to_cipher_list/i.test(err.message)) {
|
||||
let message = err.message;
|
||||
err.message = 'Failed to establish TLS session';
|
||||
err.responseCode = 500;
|
||||
err.code = err.code || 'TLSError';
|
||||
meta.message = message;
|
||||
}
|
||||
if (!err || !err.message) {
|
||||
err = new Error('Socket closed while initiating TLS');
|
||||
err.responseCode = 500;
|
||||
err.code = 'SocketError';
|
||||
err.report = false;
|
||||
err.meta = meta;
|
||||
}
|
||||
|
||||
callback(err || new Error('Socket closed unexpectedly'));
|
||||
};
|
||||
|
||||
// remove all listeners from the original socket besides the error handler
|
||||
socket.once('error', onError);
|
||||
|
||||
// upgrade connection
|
||||
tlsSocket = new tls.TLSSocket(socket, socketOptions);
|
||||
|
||||
tlsSocket.once('close', onError);
|
||||
tlsSocket.once('error', onError);
|
||||
tlsSocket.once('_tlsError', onError);
|
||||
tlsSocket.once('clientError', onError);
|
||||
tlsSocket.once('tlsClientError', onError);
|
||||
|
||||
tlsSocket.on('secure', () => {
|
||||
socket.removeListener('error', onError);
|
||||
tlsSocket.removeListener('close', onError);
|
||||
tlsSocket.removeListener('error', onError);
|
||||
tlsSocket.removeListener('_tlsError', onError);
|
||||
tlsSocket.removeListener('clientError', onError);
|
||||
tlsSocket.removeListener('tlsClientError', onError);
|
||||
if (returned) {
|
||||
try {
|
||||
tlsSocket.end();
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
return callback(null, tlsSocket);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Expose to the world
|
||||
module.exports.SMTPServer = SMTPServer;
|
||||
277
node_modules/smtp-server/lib/smtp-stream.js
generated
vendored
Executable file
277
node_modules/smtp-server/lib/smtp-stream.js
generated
vendored
Executable file
@@ -0,0 +1,277 @@
|
||||
'use strict';
|
||||
|
||||
const stream = require('stream');
|
||||
const Writable = stream.Writable;
|
||||
const PassThrough = stream.PassThrough;
|
||||
|
||||
/**
|
||||
* Incoming SMTP stream parser. Detects and emits commands. If switched to
|
||||
* data mode, emits unescaped data events until final .
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} [options] Optional Stream options object
|
||||
*/
|
||||
class SMTPStream extends Writable {
|
||||
constructor(options) {
|
||||
// init Writable
|
||||
super(options);
|
||||
|
||||
// Indicates if the stream is currently in data mode
|
||||
this._dataMode = false;
|
||||
// Output stream for the current data mode
|
||||
this._dataStream = null;
|
||||
// How many bytes are allowed for a data stream
|
||||
this._maxBytes = Infinity;
|
||||
// How many bytes have been emitted to data stream
|
||||
this.dataBytes = 0;
|
||||
// Callback to run once data mode is finished
|
||||
this._continueCallback = false;
|
||||
// unprocessed chars from the last parsing iteration (used in command mode)
|
||||
this._remainder = '';
|
||||
// unprocessed bytes from the last parsing iteration (used in data mode)
|
||||
this._lastBytes = false;
|
||||
|
||||
// Max allowed length for a single command line
|
||||
this._maxCommandLength = (options && options.maxCommandLength) || 4 * 1024;
|
||||
|
||||
this.isClosed = false;
|
||||
|
||||
// once the input stream ends, flush all output without expecting the newline
|
||||
this.on('finish', () => this._flushData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder command handler. Override this with your own.
|
||||
*/
|
||||
oncommand(/* command, callback */) {
|
||||
throw new Error('Command handler is not set');
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to data mode and return output stream. The dots in the stream are unescaped.
|
||||
*
|
||||
* @returns {Stream} Data stream
|
||||
*/
|
||||
startDataMode(maxBytes) {
|
||||
this._dataMode = true;
|
||||
this._maxBytes = (maxBytes && Number(maxBytes)) || Infinity;
|
||||
this.dataBytes = 0;
|
||||
this._dataStream = new PassThrough();
|
||||
|
||||
return this._dataStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this once data mode is over and you have finished processing the data stream
|
||||
*/
|
||||
continue() {
|
||||
if (typeof this._continueCallback === 'function') {
|
||||
this._continueCallback();
|
||||
this._continueCallback = false;
|
||||
} else {
|
||||
// indicate that the 'continue' was already called once the stream actually ends
|
||||
this._continueCallback = true;
|
||||
}
|
||||
}
|
||||
|
||||
// PRIVATE METHODS
|
||||
|
||||
/**
|
||||
* Writable._write method.
|
||||
*/
|
||||
_write(chunk, encoding, next) {
|
||||
if (!chunk || !chunk.length) {
|
||||
return next();
|
||||
}
|
||||
|
||||
let data;
|
||||
let pos = 0;
|
||||
let newlineRegex;
|
||||
|
||||
let called = false;
|
||||
let done = (...args) => {
|
||||
if (called) {
|
||||
return;
|
||||
}
|
||||
called = true;
|
||||
next(...args);
|
||||
};
|
||||
|
||||
if (this.isClosed) {
|
||||
return done();
|
||||
}
|
||||
|
||||
if (!this._dataMode) {
|
||||
newlineRegex = /\r?\n/g;
|
||||
data = this._remainder + chunk.toString('binary');
|
||||
|
||||
let readLine = () => {
|
||||
let match;
|
||||
let line;
|
||||
let buf;
|
||||
|
||||
// check if the mode is not changed
|
||||
if (this._dataMode) {
|
||||
buf = Buffer.from(data.substr(pos), 'binary');
|
||||
this._remainder = '';
|
||||
return this._write(buf, 'buffer', done);
|
||||
}
|
||||
|
||||
// search for the next newline
|
||||
// exec keeps count of the last match with lastIndex
|
||||
// so it knows from where to start with the next iteration
|
||||
if ((match = newlineRegex.exec(data))) {
|
||||
line = data.substr(pos, match.index - pos);
|
||||
pos += line.length + match[0].length;
|
||||
} else {
|
||||
this._remainder = pos < data.length ? data.substr(pos) : '';
|
||||
if (this._remainder.length > this._maxCommandLength) {
|
||||
this._remainder = '';
|
||||
return done(new Error('Command line too long'));
|
||||
}
|
||||
return done();
|
||||
}
|
||||
|
||||
this.oncommand(Buffer.from(line, 'binary'), readLine);
|
||||
};
|
||||
|
||||
// start reading lines
|
||||
readLine();
|
||||
} else {
|
||||
this._feedDataStream(chunk, done);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a chunk in data mode. Escape dots are removed and final dot ends the data mode.
|
||||
*/
|
||||
_feedDataStream(chunk, done) {
|
||||
let i;
|
||||
let endseq = Buffer.from('\r\n.\r\n');
|
||||
let len;
|
||||
let handled;
|
||||
let buf;
|
||||
|
||||
if (this._lastBytes && this._lastBytes.length) {
|
||||
chunk = Buffer.concat([this._lastBytes, chunk], this._lastBytes.length + chunk.length);
|
||||
this._lastBytes = false;
|
||||
}
|
||||
|
||||
len = chunk.length;
|
||||
|
||||
// check if the data does not start with the end terminator
|
||||
if (!this.dataBytes && len >= 3 && Buffer.compare(chunk.slice(0, 3), Buffer.from('.\r\n')) === 0) {
|
||||
this._endDataMode(false, chunk.slice(3), done);
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the first symbol is a escape dot
|
||||
if (!this.dataBytes && len >= 2 && chunk[0] === 0x2e && chunk[1] === 0x2e) {
|
||||
chunk = chunk.slice(1);
|
||||
len--;
|
||||
}
|
||||
|
||||
// seek for the stream ending
|
||||
for (i = 2; i < len - 2; i++) {
|
||||
// if the dot is the first char in a line
|
||||
if (chunk[i] === 0x2e && chunk[i - 1] === 0x0a) {
|
||||
// if the dot matches end terminator
|
||||
if (Buffer.compare(chunk.slice(i - 2, i + 3), endseq) === 0) {
|
||||
if (i > 2) {
|
||||
buf = chunk.slice(0, i);
|
||||
this.dataBytes += buf.length;
|
||||
this._endDataMode(buf, chunk.slice(i + 3), done);
|
||||
} else {
|
||||
this._endDataMode(false, chunk.slice(i + 3), done);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// check if the dot is an escape char and remove it
|
||||
if (chunk[i + 1] === 0x2e) {
|
||||
buf = chunk.slice(0, i);
|
||||
|
||||
this._lastBytes = false; // clear remainder bytes
|
||||
this.dataBytes += buf.length; // increment byte counter
|
||||
|
||||
// emit what we already have and continue without the dot
|
||||
if (this._dataStream.writable) {
|
||||
this._dataStream.write(buf);
|
||||
}
|
||||
|
||||
return setImmediate(() => this._feedDataStream(chunk.slice(i + 1), done));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// keep the last bytes
|
||||
if (chunk.length < 4) {
|
||||
this._lastBytes = chunk;
|
||||
} else {
|
||||
this._lastBytes = chunk.slice(chunk.length - 4);
|
||||
}
|
||||
|
||||
// if current chunk is longer than the remainder bytes we keep for later emit the available bytes
|
||||
if (this._lastBytes.length < chunk.length) {
|
||||
buf = chunk.slice(0, chunk.length - this._lastBytes.length);
|
||||
this.dataBytes += buf.length;
|
||||
|
||||
// write to stream but stop if need to wait for drain
|
||||
if (this._dataStream.writable) {
|
||||
handled = this._dataStream.write(buf);
|
||||
if (!handled) {
|
||||
this._dataStream.once('drain', done);
|
||||
} else {
|
||||
return done();
|
||||
}
|
||||
} else {
|
||||
return done();
|
||||
}
|
||||
} else {
|
||||
// nothing to emit, continue with the input stream
|
||||
return done();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes remaining bytes
|
||||
*/
|
||||
_flushData() {
|
||||
let line;
|
||||
if (this._remainder && !this.isClosed) {
|
||||
line = this._remainder;
|
||||
this._remainder = '';
|
||||
this.oncommand(Buffer.from(line, 'binary'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ends data mode and returns to command mode. Stream is not resumed before #continue is called
|
||||
*/
|
||||
_endDataMode(chunk, remainder, callback) {
|
||||
if (this._continueCallback === true) {
|
||||
this._continueCallback = false;
|
||||
// wait until the stream is actually over and then continue
|
||||
this._dataStream.once('end', callback);
|
||||
} else {
|
||||
this._continueCallback = () => this._write(remainder, 'buffer', callback);
|
||||
}
|
||||
|
||||
this._dataStream.byteLength = this.dataBytes;
|
||||
this._dataStream.sizeExceeded = this.dataBytes > this._maxBytes;
|
||||
|
||||
if (chunk && chunk.length && this._dataStream.writable) {
|
||||
this._dataStream.end(chunk);
|
||||
} else {
|
||||
this._dataStream.end();
|
||||
}
|
||||
|
||||
this._dataMode = false;
|
||||
this._remainder = '';
|
||||
this._dataStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Expose to the world
|
||||
module.exports.SMTPStream = SMTPStream;
|
||||
74
node_modules/smtp-server/lib/tls-options.js
generated
vendored
Normal file
74
node_modules/smtp-server/lib/tls-options.js
generated
vendored
Normal file
@@ -0,0 +1,74 @@
|
||||
'use strict';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
// Expose to the world
|
||||
module.exports = getTLSOptions;
|
||||
|
||||
const tlsDefaults = {
|
||||
// pregenerated default certificates for localhost
|
||||
// obviusly, do not use in production
|
||||
key:
|
||||
'-----BEGIN RSA PRIVATE KEY-----\n' +
|
||||
'MIIEpAIBAAKCAQEA6Z5Qqhw+oWfhtEiMHE32Ht94mwTBpAfjt3vPpX8M7DMCTwHs\n' +
|
||||
'1xcXvQ4lQ3rwreDTOWdoJeEEy7gMxXqH0jw0WfBx+8IIJU69xstOyT7FRFDvA1yT\n' +
|
||||
'RXY2yt9K5s6SKken/ebMfmZR+03ND4UFsDzkz0FfgcjrkXmrMF5Eh5UXX/+9YHeU\n' +
|
||||
'xlp0gMAt+/SumSmgCaysxZLjLpd4uXz+X+JVxsk1ACg1NoEO7lWJC/3WBP7MIcu2\n' +
|
||||
'wVsMd2XegLT0gWYfT1/jsIH64U/mS/SVXC9QhxMl9Yfko2kx1OiYhDxhHs75RJZh\n' +
|
||||
'rNRxgfiwgSb50Gw4NAQaDIxr/DJPdLhgnpY6UQIDAQABAoIBAE+tfzWFjJbgJ0ql\n' +
|
||||
's6Ozs020Sh4U8TZQuonJ4HhBbNbiTtdDgNObPK1uNadeNtgW5fOeIRdKN6iDjVeN\n' +
|
||||
'AuXhQrmqGDYVZ1HSGUfD74sTrZQvRlWPLWtzdhybK6Css41YAyPFo9k4bJ2ZW2b/\n' +
|
||||
'p4EEQ8WsNja9oBpttMU6YYUchGxo1gujN8hmfDdXUQx3k5Xwx4KA68dveJ8GasIt\n' +
|
||||
'd+0Jd/FVwCyyx8HTiF1FF8QZYQeAXxbXJgLBuCsMQJghlcpBEzWkscBR3Ap1U0Zi\n' +
|
||||
'4oat8wrPZGCblaA6rNkRUVbc/+Vw0stnuJ/BLHbPxyBs6w495yBSjBqUWZMvljNz\n' +
|
||||
'm9/aK0ECgYEA9oVIVAd0enjSVIyAZNbw11ElidzdtBkeIJdsxqhmXzeIFZbB39Gd\n' +
|
||||
'bjtAVclVbq5mLsI1j22ER2rHA4Ygkn6vlLghK3ZMPxZa57oJtmL3oP0RvOjE4zRV\n' +
|
||||
'dzKexNGo9gU/x9SQbuyOmuauvAYhXZxeLpv+lEfsZTqqrvPUGeBiEQcCgYEA8poG\n' +
|
||||
'WVnykWuTmCe0bMmvYDsWpAEiZnFLDaKcSbz3O7RMGbPy1cypmqSinIYUpURBT/WY\n' +
|
||||
'wVPAGtjkuTXtd1Cy58m7PqziB7NNWMcsMGj+lWrTPZ6hCHIBcAImKEPpd+Y9vGJX\n' +
|
||||
'oatFJguqAGOz7rigBq6iPfeQOCWpmprNAuah++cCgYB1gcybOT59TnA7mwlsh8Qf\n' +
|
||||
'bm+tSllnin2A3Y0dGJJLmsXEPKtHS7x2Gcot2h1d98V/TlWHe5WNEUmx1VJbYgXB\n' +
|
||||
'pw8wj2ACxl4ojNYqWPxegaLd4DpRbtW6Tqe9e47FTnU7hIggR6QmFAWAXI+09l8y\n' +
|
||||
'amssNShqjE9lu5YDi6BTKwKBgQCuIlKGViLfsKjrYSyHnajNWPxiUhIgGBf4PI0T\n' +
|
||||
'/Jg1ea/aDykxv0rKHnw9/5vYGIsM2st/kR7l5mMecg/2Qa145HsLfMptHo1ZOPWF\n' +
|
||||
'9gcuttPTegY6aqKPhGthIYX2MwSDMM+X0ri6m0q2JtqjclAjG7yG4CjbtGTt/UlE\n' +
|
||||
'WMlSZwKBgQDslGeLUnkW0bsV5EG3AKRUyPKz/6DVNuxaIRRhOeWVKV101claqXAT\n' +
|
||||
'wXOpdKrvkjZbT4AzcNrlGtRl3l7dEVXTu+dN7/ZieJRu7zaStlAQZkIyP9O3DdQ3\n' +
|
||||
'rIcetQpfrJ1cAqz6Ng0pD0mh77vQ13WG1BBmDFa2A9BuzLoBituf4g==\n' +
|
||||
'-----END RSA PRIVATE KEY-----',
|
||||
|
||||
cert:
|
||||
'-----BEGIN CERTIFICATE-----\n' +
|
||||
'MIICpDCCAYwCCQCuVLVKVTXnAjANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDEwls\n' +
|
||||
'b2NhbGhvc3QwHhcNMTUwMjEyMTEzMjU4WhcNMjUwMjA5MTEzMjU4WjAUMRIwEAYD\n' +
|
||||
'VQQDEwlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDp\n' +
|
||||
'nlCqHD6hZ+G0SIwcTfYe33ibBMGkB+O3e8+lfwzsMwJPAezXFxe9DiVDevCt4NM5\n' +
|
||||
'Z2gl4QTLuAzFeofSPDRZ8HH7wgglTr3Gy07JPsVEUO8DXJNFdjbK30rmzpIqR6f9\n' +
|
||||
'5sx+ZlH7Tc0PhQWwPOTPQV+ByOuReaswXkSHlRdf/71gd5TGWnSAwC379K6ZKaAJ\n' +
|
||||
'rKzFkuMul3i5fP5f4lXGyTUAKDU2gQ7uVYkL/dYE/swhy7bBWwx3Zd6AtPSBZh9P\n' +
|
||||
'X+OwgfrhT+ZL9JVcL1CHEyX1h+SjaTHU6JiEPGEezvlElmGs1HGB+LCBJvnQbDg0\n' +
|
||||
'BBoMjGv8Mk90uGCeljpRAgMBAAEwDQYJKoZIhvcNAQELBQADggEBABXm8GPdY0sc\n' +
|
||||
'mMUFlgDqFzcevjdGDce0QfboR+M7WDdm512Jz2SbRTgZD/4na42ThODOZz9z1AcM\n' +
|
||||
'zLgx2ZNZzVhBz0odCU4JVhOCEks/OzSyKeGwjIb4JAY7dh+Kju1+6MNfQJ4r1Hza\n' +
|
||||
'SVXH0+JlpJDaJ73NQ2JyfqELmJ1mTcptkA/N6rQWhlzycTBSlfogwf9xawgVPATP\n' +
|
||||
'4AuwgjHl12JI2HVVs1gu65Y3slvaHRCr0B4+Kg1GYNLLcbFcK+NEHrHmPxy9TnTh\n' +
|
||||
'Zwp1dsNQU+Xkylz8IUANWSLHYZOMtN2e5SKIdwTtl5C8YxveuY8YKb1gDExnMraT\n' +
|
||||
'VGXQDqPleug=\n' +
|
||||
'-----END CERTIFICATE-----',
|
||||
|
||||
honorCipherOrder: true,
|
||||
requestOCSP: false,
|
||||
sessionIdContext: crypto.createHash('sha1').update(process.argv.join(' ')).digest('hex').slice(0, 32),
|
||||
|
||||
minVersion: 'TLSv1' // sadly there are very old SMTP clients out there
|
||||
};
|
||||
|
||||
/**
|
||||
* Mixes existing values with the default ones.
|
||||
*
|
||||
* @param {Object} [opts] TLS options
|
||||
* @returns {Object} Object with mixed TLS values
|
||||
*/
|
||||
function getTLSOptions(opts) {
|
||||
return Object.assign({}, tlsDefaults, opts || {});
|
||||
}
|
||||
Reference in New Issue
Block a user