node_modules: update (#344)

Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
Dawid Dziurlaanddawidd6 authored and GitHub committed 2026-10-10 17:27:57 +02:00
1 parent b099186883
commit 6e88c72525
63 files changed
+2863 -1227

No files matched your search

+349 -137
View File
@@ -45,12 +45,30 @@ const node_crypto_1 = __importDefault(require("node:crypto"));
const data_stream_js_1 = __importDefault(require("./data-stream.js"));
const node_stream_1 = require("node:stream");
const shared = __importStar(require("../shared/index.js"));
const errors_js_1 = require("../errors.js");
// default timeout values in ms
const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection to be established
const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
const CLOSE_TIMEOUT = 5 * 1000; // how much to wait for the server to close its side after we closed ours
const KEEPALIVE_DELAY = 30 * 1000; // idle time before TCP keepalive probes start, keeps NAT mappings of idle connections alive
const TEARDOWN_NOOP = () => { }; // reusable no-op handler for absorbing errors during socket teardown
// Random order, so that connections spread over the addresses of a host
function shuffle(list) {
const result = list.slice();
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[result[i], result[j]] = [result[j], result[i]];
}
return result;
}
// Every timeout is reported with the ETIMEDOUT code, timeoutType tells which one it was
function timeoutError(message, timeoutType) {
const err = new Error(message);
err.timeoutType = timeoutType;
return err;
}
// how many bytes a single server response may occupy while it is still being received.
// Generous compared to any real reply, it only stops a peer that never completes one
const MAX_RESPONSE_SIZE = 1024 * 1024;
@@ -173,8 +191,8 @@ class SMTPConnection extends node_events_1.EventEmitter {
this._maxAllowedSize = 0;
this._responseActions = [];
this._recipientQueue = [];
this._greetingTimeout = false;
this._connectionTimeout = false;
this._phaseTimer = false;
this._plaintextEhlo = false;
this._destroyed = false;
this._closing = false;
this._currentDataStream = false;
@@ -185,14 +203,25 @@ class SMTPConnection extends node_events_1.EventEmitter {
this._onSocketClose = () => this._onClose();
this._onSocketEnd = () => this._onEnd();
this._onSocketTimeout = () => this._onTimeout();
this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET');
this._connectionAttemptId = 0;
this._onConnectionSocketError = err => this._onError(err, 'ESOCKET', false, 'CONN');
}
/**
* Creates a connection to a SMTP server and sets up connection
* listener
*/
connect(connectCallback) {
if (this._connectCalled && !this._destroyed) {
// A connection is opened once. A second call would open a second socket over the
// first one and run the session handlers of both against the same state
const err = this._formatError('Cannot connect - connect() was already called for this connection', 'ECONNECTION', false, 'API');
if (typeof connectCallback === 'function') {
setImmediate(() => connectCallback(err));
return;
}
this.logger.warn({ tnx: 'smtp' }, '%s', err.message);
return;
}
this._connectCalled = true;
if (typeof connectCallback === 'function') {
this._connectCallback = connectCallback;
this.once('connect', () => {
@@ -207,11 +236,15 @@ class SMTPConnection extends node_events_1.EventEmitter {
return connectCallback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'CONN'));
}
}
// connectionTimeout covers the whole of connecting: the DNS lookup and every address tried
const connectionTimeout = this.options.connectionTimeout || CONNECTION_TIMEOUT;
// a transport that first opened a proxy connection for this one sets when that started
this._connectionDeadline = (this._connectStartedAt || Date.now()) + connectionTimeout;
let opts = {
port: this.port,
host: this.host,
allowInternalNetworkInterfaces: this.allowInternalNetworkInterfaces,
timeout: this.options.dnsTimeout || DNS_TIMEOUT
timeout: Math.min(this.options.dnsTimeout || DNS_TIMEOUT, connectionTimeout)
};
if (this.options.localAddress) {
opts.localAddress = this.options.localAddress;
@@ -240,7 +273,6 @@ class SMTPConnection extends node_events_1.EventEmitter {
return this._resolveAndConnect(opts, _resolved => {
try {
this._socket.connect(this.port, this.host, () => {
this._socket.setKeepAlive(true);
// a `secure` connection over a caller-provided socket must still
// perform the TLS handshake, otherwise AUTH and the message body
// would be sent in cleartext despite the caller requesting TLS
@@ -272,9 +304,40 @@ class SMTPConnection extends node_events_1.EventEmitter {
}
}
return this._resolveAndConnect(opts, resolved => {
// Store fallback addresses for retry on connection failure
this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
this._connectOpts = Object.assign({}, opts);
let addresses = resolved._addresses || [];
if (opts.localAddress) {
// a socket bound to an address of one family can not reach the other one
const localFamily = node_net_1.default.isIPv6(opts.localAddress) ? 6 : 4;
const sameFamily = addresses.filter(addr => node_net_1.default.isIP(addr) === localFamily);
addresses = sameFamily.length ? sameFamily : addresses;
}
if (addresses.length > 1) {
// net.connect tries the addresses in turn and moves on to the next one when an
// attempt fails or takes too long. With both families it starts on IPv6 and
// alternates (RFC 8305), so a host with a broken IPv6 path costs a fraction of
// a second instead of a whole connection timeout
const ipv6 = addresses.filter(addr => node_net_1.default.isIPv6(addr));
const ipv4 = addresses.filter(addr => !node_net_1.default.isIPv6(addr));
const ordered = shuffle(ipv6).concat(shuffle(ipv4));
opts.host = this.host;
opts.autoSelectFamily = true;
if (!ipv6.length || !ipv4.length) {
// a slow address of the only family gets its share of the time, not the
// quarter second meant for an address family that does not work
const remaining = this._connectionDeadline - Date.now();
opts.autoSelectFamilyAttemptTimeout = Math.max(Math.floor(remaining / ordered.length), 10);
}
opts.lookup = ((hostname, lookupOptions, callback) => {
if (lookupOptions && lookupOptions.all) {
const all = ordered.map(address => ({ address, family: node_net_1.default.isIPv6(address) ? 6 : 4 }));
return setImmediate(() => callback(null, all));
}
setImmediate(() => callback(null, ordered[0], node_net_1.default.isIPv6(ordered[0]) ? 6 : 4));
});
}
else if (addresses.length) {
opts.host = addresses[0];
}
this._connectToHost(opts, this.secureConnection);
});
}
@@ -321,20 +384,11 @@ class SMTPConnection extends node_events_1.EventEmitter {
if (this._destroyed || this._closing) {
return;
}
this._connectionAttemptId++;
const currentAttemptId = this._connectionAttemptId;
const connectFn = secure
? node_tls_1.default.connect
: node_net_1.default.connect;
try {
this._socket = connectFn(opts, () => {
// Ignore callback if this is a stale connection attempt
if (this._connectionAttemptId !== currentAttemptId) {
return;
}
this._socket.setKeepAlive(true);
this._onConnect();
});
this._socket = connectFn(opts, () => this._onConnect());
this._setupConnectionHandlers();
}
catch (E) {
@@ -347,51 +401,37 @@ class SMTPConnection extends node_events_1.EventEmitter {
* @internal
*/
_setupConnectionHandlers() {
this._connectionTimeout = setTimeout(() => {
this._onConnectionError('Connection timeout', 'ETIMEDOUT');
}, this.options.connectionTimeout || CONNECTION_TIMEOUT);
this._startPhase(Math.max((this._connectionDeadline || Date.now()) - Date.now(), 0), timeoutError('Connection timeout', 'CONNECT_TIMEOUT'));
this._socket.on('error', this._onConnectionSocketError);
}
/**
* Handles connection errors with fallback to alternative addresses
* Starts the timer of a connection phase: connecting, waiting for the greeting or a TLS
* upgrade. The phases follow one another, so starting one ends the one before
*
* @param err Error object or message
* @param code Error code
* @param timeout Time the phase may take
* @param err Error to fail with when it takes longer
* @internal
*/
_onConnectionError(err, code) {
clearTimeout(this._connectionTimeout);
// Check if we have fallback addresses to try
const canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
if (!canFallback) {
// No more fallback addresses, report the error
this._onError(err, code, false, 'CONN');
return;
}
const nextHost = this._fallbackAddresses.shift();
this.logger.info({
tnx: 'network',
failedHost: this._connectOpts.host,
nextHost,
error: err.message || err
}, 'Connection to %s failed, trying %s', this._connectOpts.host, nextHost);
// Clean up current socket
if (this._socket) {
try {
this._socket.removeListener('error', this._onConnectionSocketError);
// Absorb any late teardown error (e.g. a TLS fallback socket emitting
// after destroy), mirroring the guard used in close()
this._socket.on('error', TEARDOWN_NOOP);
this._socket.destroy();
}
catch (_E) {
// ignore
}
this._socket = null;
}
// Update host and retry
this._connectOpts.host = nextHost;
this._connectToHost(this._connectOpts, this.secureConnection);
_startPhase(timeout, err) {
this._clearPhase();
this._phaseTimer = setTimeout(() => {
this._phaseTimer = false;
this._onError(err, 'ETIMEDOUT', false, 'CONN');
}, timeout);
}
/**
* Time the greeting or a STARTTLS upgrade may take: greetingTimeout, cut short by what is left
* of connectionTimeout
*
* @internal
*/
_remainingSetupTime() {
return Math.min(this.options.greetingTimeout || GREETING_TIMEOUT, Math.max((this._connectionDeadline || Infinity) - Date.now(), 1));
}
/** @internal */
_clearPhase() {
clearTimeout(this._phaseTimer);
this._phaseTimer = false;
}
/**
* Sends QUIT
@@ -404,8 +444,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
* Closes the connection to the server
*/
close() {
clearTimeout(this._connectionTimeout);
clearTimeout(this._greetingTimeout);
this._clearPhase();
this._responseActions = [];
// allow to run this function only once
if (this._closing) {
@@ -428,13 +467,15 @@ class SMTPConnection extends node_events_1.EventEmitter {
}
this._currentDataStream = false;
}
// Detach from the message stream as well. The listener is swapped for a no-op rather than
// removed, a stream destroyed with an error later on would otherwise throw it as unhandled
// Detach from the message stream as well and release whatever it reads from, the message
// can not be sent over this connection anymore. The listener is swapped for a no-op rather
// than removed, a stream destroyed with an error would otherwise throw it as unhandled
if (this._pendingSend) {
const { stream, onStreamError } = this._pendingSend;
if (stream) {
stream.removeListener('error', onStreamError);
stream.on('error', TEARDOWN_NOOP);
stream.destroy();
}
this._pendingSend = false;
}
@@ -453,6 +494,15 @@ class SMTPConnection extends node_events_1.EventEmitter {
// sending cleartext after TLS shutdown triggers ERR_SSL_BAD_RECORD_TYPE)
socket.on('error', TEARDOWN_NOOP);
socket[closeMethod]();
if (closeMethod === 'end') {
// end() only closes our side, a server that never closes its own would keep
// the socket, and the process with it, around for good
const closeTimer = setTimeout(() => socket.destroy(), CLOSE_TIMEOUT);
if (typeof closeTimer.unref === 'function') {
closeTimer.unref();
}
socket.once('close', () => clearTimeout(closeTimer));
}
}
catch (_E) {
// just ignore
@@ -657,14 +707,9 @@ class SMTPConnection extends node_events_1.EventEmitter {
const startTime = Date.now();
this._setEnvelope(envelope, (err, info) => {
if (err) {
// create passthrough stream to consume to prevent OOM
const stream = new node_stream_1.PassThrough();
if (typeof message.pipe === 'function') {
message.pipe(stream);
}
else {
stream.write(message);
stream.end();
// the message is not going to be sent, release whatever the stream reads from
if (typeof message.destroy === 'function') {
message.destroy();
}
return callback(err);
}
@@ -718,7 +763,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
*/
_onConnect() {
const socket = this._socket;
clearTimeout(this._connectionTimeout);
this._clearPhase();
this.logger.info({
tnx: 'network',
localAddress: socket.localAddress,
@@ -748,16 +793,42 @@ class SMTPConnection extends node_events_1.EventEmitter {
socket.once('end', this._onSocketEnd);
socket.setTimeout(this.options.socketTimeout || SOCKET_TIMEOUT);
socket.on('timeout', this._onSocketTimeout);
this._greetingTimeout = setTimeout(() => {
// if still waiting for greeting, give up
if (this._socket && !this._destroyed && this._responseActions[0] === this._actionGreeting) {
this._onError('Greeting never received', 'ETIMEDOUT', false, 'CONN');
}
}, this.options.greetingTimeout || GREETING_TIMEOUT);
// keepalive also covers sockets handed over by a proxy or by the caller
if (typeof socket.setKeepAlive === 'function') {
socket.setKeepAlive(true, KEEPALIVE_DELAY);
}
// Commands are written in the batches they belong to (see cork() for PIPELINING), Nagle
// would only hold a write back until the server acknowledged the previous one. Against a
// server that delays its ACKs that costs 40ms on every message
if (typeof socket.setNoDelay === 'function') {
socket.setNoDelay(true);
}
// bounded by greetingTimeout and by what is left of connectionTimeout, which covers setting
// the session up from the DNS lookup to the end of a STARTTLS upgrade
this._startPhase(this._remainingSetupTime(), timeoutError('Greeting never received', 'GREETING_TIMEOUT'));
this._responseActions.push(this._actionGreeting);
// we have a 'data' listener set up so resume socket if it was paused
socket.resume();
}
/**
* Ends the session after a 421 reply. The replies queued for commands sent along with the
* answered one are not going to come, so the message in flight is failed here
*
* @param str The 421 reply
* @internal
*/
_onServerClosing(str) {
if (this._destroyed) {
return;
}
const pendingSend = this._pendingSend;
const envelope = this._envelope;
this._responseActions = [];
this.close();
if (pendingSend) {
pendingSend.callback((envelope && envelope.mailError) || this._formatError('Server closed the connection', 'ECONNECTION', str, 'CONN'));
}
}
/**
* 'data' listener for data coming from the server
*
@@ -832,8 +903,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
* @internal
*/
_onError(err, type, data, command) {
clearTimeout(this._connectionTimeout);
clearTimeout(this._greetingTimeout);
this._clearPhase();
if (this._destroyed) {
// just ignore, already closed
// this might happen when a socket is canceled because of reached timeout
@@ -841,12 +911,12 @@ class SMTPConnection extends node_events_1.EventEmitter {
return;
}
err = this._formatError(err, type, data, command);
const transientCodes = ['ETIMEDOUT', 'ESOCKET', 'ECONNECTION'];
if (transientCodes.includes(err.code)) {
this.logger.warn(data, err.message);
// the message carries the server response, it is an argument and not the format string
if ((0, errors_js_1.isTransientError)(err)) {
this.logger.warn({ tnx: 'smtp', err }, '%s', err.message);
}
else {
this.logger.error(data, err.message);
this.logger.error({ tnx: 'smtp', err }, '%s', err.message);
}
// close() forgets the send in flight, it is completed with this same error afterwards so
// a late message stream error has nothing left to report
@@ -866,7 +936,12 @@ class SMTPConnection extends node_events_1.EventEmitter {
else {
err = new Error(message);
}
if (type && type !== 'Error') {
// a permission model denial keeps its own code, see ERR_ACCESS_DENIED
if (type && type !== 'Error' && err.code !== errors_js_1.ERR_ACCESS_DENIED) {
// the code of a system error, such as ECONNREFUSED, still tells what happened
if (err.code && err.code !== type && !err.originalCode) {
err.originalCode = err.code;
}
err.code = type;
}
if (response) {
@@ -918,6 +993,19 @@ class SMTPConnection extends node_events_1.EventEmitter {
this.close();
return;
}
if (!failureResponse &&
this.stage === 'connected' &&
!this._responseActions.length &&
!this._pendingSend &&
!this._destroyed &&
!this._closing) {
// nothing was waiting for the server, so this is the server ending an idle session
// (usually after its idle timeout), not a failure
this.logger.info({
tnx: 'network'
}, 'Server closed the idle connection');
return this._destroy();
}
if (failureResponse || (this._responseActions[0] !== this.close && !this._destroyed)) {
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', failureResponse, 'CONN');
}
@@ -943,7 +1031,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
* @internal
*/
_onTimeout() {
return this._onError(new Error('Timeout'), 'ETIMEDOUT', false, 'CONN');
return this._onError(timeoutError('Timeout', 'SOCKET_TIMEOUT'), 'ETIMEDOUT', false, 'CONN');
}
/**
* Destroys the client, emits 'end'
@@ -958,10 +1046,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
this.destroyed = true;
// a connection the server dropped before the greeting would otherwise keep
// the greeting timer, and with it the process, alive until it fires
clearTimeout(this._connectionTimeout);
clearTimeout(this._greetingTimeout);
this._connectionTimeout = false;
this._greetingTimeout = false;
this._clearPhase();
this.emit('end');
}
/**
@@ -978,6 +1063,15 @@ class SMTPConnection extends node_events_1.EventEmitter {
// inject plaintext bytes after the "220" reply (e.g. a CRLF-free fragment that
// would otherwise be prepended to the first post-TLS response and parsed as
// part of the secured EHLO capabilities). STARTTLS response injection.
const discarded = this._remainder.length + this._responseQueue.reduce((total, response) => total + response.length, 0);
if (discarded) {
// a server does not send anything here on its own, this is worth knowing about
this.logger.warn({
tnx: 'smtp',
discarded
}, 'Discarded %s bytes received in plaintext after the STARTTLS response', discarded);
}
this._plaintextEhlo = false;
this._remainder = '';
this._responseQueue = [];
this._responsePartial = false;
@@ -987,6 +1081,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
const socketPlain = this._socket;
socketPlain.removeListener('data', this._onSocketData); // incoming data is going to be gibberish from this point onwards
socketPlain.removeListener('timeout', this._onSocketTimeout); // timeout will be re-set for the new socket object
socketPlain.setTimeout(0);
const opts = Object.assign({
socket: socketPlain,
host: this.host
@@ -1007,9 +1102,14 @@ class SMTPConnection extends node_events_1.EventEmitter {
socketPlain.removeListener('error', this._onConnectionSocketError);
};
this.upgrading = true;
// the socket timeout only notices a server that sends nothing at all, a handshake that
// trickles along would otherwise hold the connection for as long as the server likes
// STARTTLS is the last step of setting the session up
this._startPhase(this._remainingSetupTime(), timeoutError('TLS handshake timed out', 'UPGRADE_TIMEOUT'));
// tls.connect is not an asynchronous function however it may still throw errors and requires to be wrapped with try/catch
try {
this._socket = node_tls_1.default.connect(opts, () => {
this._clearPhase();
this.secure = true;
this.upgrading = false;
this._socket.on('data', this._onSocketData);
@@ -1018,6 +1118,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
});
}
catch (err) {
this._clearPhase();
removePlainSocketListeners();
return callback(err);
}
@@ -1058,10 +1159,24 @@ class SMTPConnection extends node_events_1.EventEmitter {
}, str.replace(/\r?\n$/, ''));
}
const action = this._responseActions.shift();
// RFC 5321 4.2: 421 means the server is about to close the connection, whatever it answers
const closing = /^421[ -]/.test(str);
if (typeof action === 'function') {
// the command gets its own error first, the code tells which step failed
action.call(this, str);
if (closing) {
return this._onServerClosing(str);
}
setImmediate(() => this._processResponse());
}
else if (closing && !this._pendingSend && this.stage === 'connected') {
// RFC 5321 4.2: a server may send 421 at any time when it is about to close the
// connection. Nothing was waiting for a reply, so this ends an idle session
this.logger.info({
tnx: 'smtp'
}, 'Server closed the idle connection: %s', str);
this.close();
}
else {
return this._onError(new Error('Unexpected Response'), 'EPROTOCOL', str, 'CONN');
}
@@ -1156,6 +1271,9 @@ class SMTPConnection extends node_events_1.EventEmitter {
return callback(this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM'));
}
}
// RFC 2920: with PIPELINING the whole envelope and DATA go out without waiting for the
// replies in between, which saves two round trips for every message
this._envelope.pipelined = this._supportedExtensions.includes('PIPELINING');
this._responseActions.push(str => {
this._actionMAIL(str, callback);
});
@@ -1193,7 +1311,38 @@ class SMTPConnection extends node_events_1.EventEmitter {
if (this._envelope.requireTLSExtensionEnabled) {
args.push('REQUIRETLS');
}
this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
const mailFrom = 'MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : '');
this._recipientQueue = [];
if (!this._envelope.pipelined) {
this._sendCommand(mailFrom);
return;
}
// corked, so the batch leaves in one segment instead of the first command alone
const socket = this._socket;
socket.cork();
this._sendCommand(mailFrom);
while (this._envelope.rcptQueue.length) {
this._sendRcpt(this._envelope.rcptQueue.shift(), callback);
}
this._responseActions.push(str => {
this._actionDATA(str, callback);
});
this._sendCommand('DATA');
socket.uncork();
}
/**
* Sends RCPT TO for a recipient and queues the handler for the reply
*
* @param recipient Recipient address
* @param callback Callback to run once the envelope is processed
* @internal
*/
_sendRcpt(recipient, callback) {
this._recipientQueue.push(recipient);
this._responseActions.push(str => {
this._actionRCPT(str, callback);
});
this._sendCommand('RCPT TO:<' + recipient + '>' + this._getDsnRcptToArgs());
}
/** @internal */
_setDsnEnvelope(params) {
@@ -1309,7 +1458,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
* @internal
*/
_actionGreeting(str) {
clearTimeout(this._greetingTimeout);
this._clearPhase();
if (str.substr(0, 3) !== '220') {
this._onError(new Error('Invalid greeting. response=' + str), 'EPROTOCOL', str, 'CONN');
return;
@@ -1347,7 +1496,6 @@ class SMTPConnection extends node_events_1.EventEmitter {
* @internal
*/
_actionEHLO(str) {
let match;
if (str.substr(0, 3) === '421') {
this._onError(new Error('Server terminates connection. response=' + str), 'ECONNECTION', str, 'EHLO');
return;
@@ -1362,17 +1510,30 @@ class SMTPConnection extends node_events_1.EventEmitter {
this._sendCommand('HELO ' + this.name);
return;
}
// Detect if the server supports STARTTLS
if (!this.secure && !this.options.ignoreTLS && (/[ -]STARTTLS\b/im.test(str) || this.options.requireTLS)) {
// kept for opportunisticTLS, a session that stays in plaintext still has these extensions
this._plaintextEhlo = str;
this._sendCommand('STARTTLS');
this._responseActions.push(this._actionSTARTTLS);
return;
}
this._parseEhloExtensions(str);
this.emit('connect');
}
/**
* Reads the extensions and the authentication mechanisms out of an EHLO response
*
* @param str EHLO response from the server
* @internal
*/
_parseEhloExtensions(str) {
let match;
this._ehloLines = str
.split(/\r?\n/)
.map(line => line.replace(/^\d+[ -]/, '').trim())
.filter(line => line)
.slice(1);
// Detect if the server supports STARTTLS
if (!this.secure && !this.options.ignoreTLS && (/[ -]STARTTLS\b/im.test(str) || this.options.requireTLS)) {
this._sendCommand('STARTTLS');
this._responseActions.push(this._actionSTARTTLS);
return;
}
// Detect if the server supports SMTPUTF8
if (/[ -]SMTPUTF8\b/im.test(str)) {
this._supportedExtensions.push('SMTPUTF8');
@@ -1422,7 +1583,6 @@ class SMTPConnection extends node_events_1.EventEmitter {
this._supportedExtensions.push('SIZE');
this._maxAllowedSize = Number(match[1]) || 0;
}
this.emit('connect');
}
/**
* Handles server response for HELO command. If it yielded in
@@ -1454,6 +1614,14 @@ class SMTPConnection extends node_events_1.EventEmitter {
this.logger.info({
tnx: 'smtp'
}, 'Failed STARTTLS upgrade, continuing unencrypted');
// the plaintext session goes on with what the server announced for it, except for
// AUTH: credentials are not sent over a connection that failed to encrypt
if (this._plaintextEhlo) {
this._parseEhloExtensions(this._plaintextEhlo);
this._plaintextEhlo = false;
this.allowsAuth = false;
this._supportedAuth = [];
}
this.emit('connect');
return;
}
@@ -1634,21 +1802,9 @@ class SMTPConnection extends node_events_1.EventEmitter {
const message = this._usingSmtpUtf8 && /^550 /.test(str) && /[\x80-\uFFFF]/.test(envelope.from)
? 'Internationalized mailbox name not allowed'
: 'Mail command failed';
return callback(this._formatError(message, 'EENVELOPE', str, 'MAIL FROM'));
envelope.mailError = this._formatError(message, 'EENVELOPE', str, 'MAIL FROM');
}
if (!envelope.rcptQueue.length) {
return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
}
this._recipientQueue = [];
const usePipelining = this._supportedExtensions.includes('PIPELINING');
do {
const curRecipient = envelope.rcptQueue.shift();
this._recipientQueue.push(curRecipient);
this._responseActions.push(str => {
this._actionRCPT(str, callback);
});
this._sendCommand('RCPT TO:<' + curRecipient + '>' + this._getDsnRcptToArgs());
} while (usePipelining && envelope.rcptQueue.length);
this._advanceEnvelope(str, callback);
}
/**
* Handle response for a RCPT TO: command
@@ -1675,32 +1831,67 @@ class SMTPConnection extends node_events_1.EventEmitter {
else {
envelope.accepted.push(curRecipient);
}
if (!envelope.rcptQueue.length && !this._recipientQueue.length) {
if (envelope.rejected.length < envelope.to.length) {
this._responseActions.push(str => {
this._actionDATA(str, callback);
});
this._sendCommand('DATA');
}
else {
// report a temporary rejection when there is one, taking the last reply would mark the
// whole message as permanently failed although some recipients were only deferred
const deferred = envelope.rejectedErrors.find(rejectedErr => rejectedErr.responseCode && rejectedErr.responseCode < 500);
const reply = deferred?.response ?? str;
err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', reply, 'RCPT TO');
err.rejected = envelope.rejected;
err.rejectedErrors = envelope.rejectedErrors;
return callback(err);
}
this._advanceEnvelope(str, callback);
}
/**
* Moves the envelope on after a reply to MAIL FROM or RCPT TO. A pipelined envelope sent every
* command at once and is decided by the reply to DATA. Otherwise the commands go one at a
* time: the next recipient, or once every reply is in, DATA or the error that ends the message
*
* @param str The reply that was handled
* @param callback Callback to run once the envelope is processed
* @internal
*/
_advanceEnvelope(str, callback) {
const envelope = this._envelope;
if (envelope.pipelined) {
return;
}
else if (envelope.rcptQueue.length) {
const nextRecipient = envelope.rcptQueue.shift();
this._recipientQueue.push(nextRecipient);
this._responseActions.push(str => {
this._actionRCPT(str, callback);
});
this._sendCommand('RCPT TO:<' + nextRecipient + '>' + this._getDsnRcptToArgs());
if (envelope.mailError) {
return callback(envelope.mailError);
}
if (envelope.rcptQueue.length) {
return this._sendRcpt(envelope.rcptQueue.shift(), callback);
}
if (this._recipientQueue.length) {
// replies still to come
return;
}
const err = this._envelopeError(str);
if (err) {
return callback(err);
}
this._responseActions.push(str => {
this._actionDATA(str, callback);
});
this._sendCommand('DATA');
}
/**
* Decides how the envelope went once every reply to MAIL FROM and RCPT TO is in. Called after
* the last RCPT TO reply, or with PIPELINING on the reply to the DATA command sent along
*
* @param str The reply being handled
* @returns The error to fail the message with, or null when DATA can go ahead
* @internal
*/
_envelopeError(str) {
const envelope = this._envelope;
if (envelope.mailError) {
return envelope.mailError;
}
if (envelope.accepted.length) {
return null;
}
// report a temporary rejection when there is one, taking the last reply would mark the
// whole message as permanently failed although some recipients were only deferred
const deferred = envelope.rejectedErrors.find(rejectedErr => rejectedErr.responseCode && rejectedErr.responseCode < 500);
const lastRejected = envelope.rejectedErrors[envelope.rejectedErrors.length - 1];
const reply = deferred?.response ?? (envelope.pipelined && lastRejected ? lastRejected.response : str);
// every recipient was rejected
const err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', reply, 'RCPT TO');
err.rejected = envelope.rejected;
err.rejectedErrors = envelope.rejectedErrors;
return err;
}
/**
* Handle response for a DATA command
@@ -1711,6 +1902,27 @@ class SMTPConnection extends node_events_1.EventEmitter {
*/
_actionDATA(str, callback) {
const envelope = this._envelope;
if (envelope.pipelined) {
const err = this._envelopeError(str);
if (err) {
if (/^3/.test(str)) {
if (envelope.mailError) {
// A server that refused the sender has no transaction to end, one that
// took DATA anyway can not be trusted with an empty message either.
// Drop the connection instead of answering it
this.close();
return callback(err);
}
// A server must refuse DATA without an accepted recipient, this one took it
// anyway. End the empty message, it has nobody to go to, so the session
// stays usable
this._responseActions.push(() => callback(err));
this._sendCommand('.');
return;
}
return callback(err);
}
}
// response should be 354 but according to this issue https://github.com/eleith/emailjs/issues/24
// some servers might use 250 instead, so lets check for 2 or 3 as the first digit
if (!/^[23]/.test(str)) {