Initial commit

This commit is contained in:
Alex Rennie-Lis
2026-05-01 10:09:51 +01:00
parent e9314ae1e8
commit 299aacd2a4
144 changed files with 39902 additions and 0 deletions

488
node_modules/smtp-server/lib/sasl.js generated vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

564
node_modules/smtp-server/lib/smtp-server.js generated vendored Normal file
View 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
View 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
View 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 || {});
}