mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-10-12 02:42:27 +07:00
node_modules: update (#344)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
1 parent
b099186883
commit
6e88c72525
63 files changed
+2863
-1227
No files matched your search
+3
-1
@@ -9,6 +9,8 @@ import type { Callback } from '../errors.js';
|
||||
export interface HttpProxyClientOptions {
|
||||
/** Set to false to accept a proxy certificate that fails validation (e.g. self-signed) */
|
||||
rejectUnauthorized?: boolean | undefined;
|
||||
/** Time in milliseconds the CONNECT handshake may take, defaults to httpProxyClient.timeout or 30 seconds */
|
||||
timeout?: number | undefined;
|
||||
}
|
||||
/**
|
||||
* Receives the proxied socket once the CONNECT handshake has succeeded, or the error that prevented it
|
||||
@@ -38,7 +40,7 @@ declare function httpProxyClient(proxyUrl: string, destinationPort: number | str
|
||||
*/
|
||||
declare function httpProxyClient(proxyUrl: string, destinationPort: number | string, destinationHost: string, tlsOptions: HttpProxyClientOptions | undefined, callback: HttpProxyClientCallback): void;
|
||||
/**
|
||||
* Socket timeout in milliseconds while the CONNECT handshake is in progress, defaults to 30 seconds.
|
||||
* Time in milliseconds the CONNECT handshake may take when the call does not set one, defaults to 30 seconds.
|
||||
* Settable on the function itself, the same way the CommonJS module exposed it.
|
||||
*/
|
||||
declare namespace httpProxyClient {
|
||||
|
||||
+88
-68
@@ -8,6 +8,8 @@ import * as errors from '../errors.js';
|
||||
// Cap the CONNECT response we buffer before the header terminator, so a proxy that
|
||||
// never sends \r\n\r\n cannot grow memory unboundedly before the socket times out.
|
||||
const MAX_RESPONSE_HEADER_BYTES = 64 * 1024;
|
||||
// URL hostnames keep the brackets around an IPv6 literal, socket options and net.isIPv6 take it without
|
||||
const unbracket = (host) => typeof host === 'string' && host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
||||
function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, callback) {
|
||||
if (typeof tlsOptions === 'function') {
|
||||
callback = tlsOptions;
|
||||
@@ -27,6 +29,8 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
||||
return;
|
||||
}
|
||||
const proxy = urllib.parse(proxyUrl);
|
||||
// the CONNECT request line and the Host header take an IPv6 destination in brackets
|
||||
const authority = (net.isIPv6(unbracket(destinationHost)) ? '[' + unbracket(destinationHost) + ']' : destinationHost) + ':' + destinationPort;
|
||||
const connectOptions = {
|
||||
host: proxy.hostname,
|
||||
port: Number(proxy.port) ? Number(proxy.port) : proxy.protocol === 'https:' ? 443 : 80
|
||||
@@ -42,15 +46,27 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
||||
else {
|
||||
connect = net.connect.bind(net);
|
||||
}
|
||||
// The handshake is bounded as a whole, a proxy that keeps sending a byte now and then can
|
||||
// not hold the connection open past it
|
||||
const timeout = Number(tlsOptions.timeout) || httpProxyClient.timeout || 30 * 1000;
|
||||
let socket;
|
||||
// Error harness for initial connection. Once connection is established, the responsibility
|
||||
// to handle errors is passed to whoever uses this socket
|
||||
// Single settlement path for the handshake: every temporary listener and the timer are
|
||||
// dropped exactly once. Once the tunnel is up, the responsibility to handle errors is passed
|
||||
// to whoever uses this socket
|
||||
let finished = false;
|
||||
const tempSocketErr = (err) => {
|
||||
let timer;
|
||||
const cleanup = () => {
|
||||
clearTimeout(timer);
|
||||
socket.removeListener('data', onSocketData);
|
||||
socket.removeListener('error', fail);
|
||||
socket.removeListener('close', onEarlyClose);
|
||||
};
|
||||
function fail(err) {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
cleanup();
|
||||
try {
|
||||
socket.destroy();
|
||||
}
|
||||
@@ -58,18 +74,72 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
||||
// ignore
|
||||
}
|
||||
done(err);
|
||||
};
|
||||
const timeoutErr = () => {
|
||||
const err = new Error('Proxy socket timed out');
|
||||
err.code = 'ETIMEDOUT';
|
||||
tempSocketErr(err);
|
||||
};
|
||||
}
|
||||
function onEarlyClose() {
|
||||
const err = new Error('Proxy closed the connection before the tunnel was established');
|
||||
err.code = errors.EPROXY;
|
||||
fail(err);
|
||||
}
|
||||
// The response is collected as chunks and only the bytes that just arrived, together
|
||||
// with the three before them, are searched for the end of the headers. Appending to a
|
||||
// string and searching all of it again re-read the whole response on every chunk.
|
||||
const chunks = [];
|
||||
let received = 0;
|
||||
let tail = '';
|
||||
function onSocketData(chunk) {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
const window = tail + chunk.toString('binary');
|
||||
const windowEnd = window.indexOf('\r\n\r\n');
|
||||
chunks.push(chunk);
|
||||
received += chunk.length;
|
||||
tail = window.slice(-3);
|
||||
if (windowEnd < 0) {
|
||||
if (received > MAX_RESPONSE_HEADER_BYTES) {
|
||||
const err = new Error('Proxy response headers too large');
|
||||
err.code = errors.EPROXY;
|
||||
fail(err);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Stop reading before anything is put back. A socket that keeps flowing would emit the
|
||||
// bytes after the headers, a greeting the proxy sent together with its own response,
|
||||
// before the next owner of the socket has a listener for them
|
||||
socket.removeListener('data', onSocketData);
|
||||
socket.pause();
|
||||
const headerEnd = received - window.length + windowEnd;
|
||||
const response = Buffer.concat(chunks, received);
|
||||
if (response.length > headerEnd + 4) {
|
||||
socket.unshift(response.subarray(headerEnd + 4));
|
||||
}
|
||||
// check response code
|
||||
const match = response.toString('binary', 0, headerEnd).match(/^HTTP\/\d+\.\d+ (\d+)/i);
|
||||
if (!match || (match[1] || '').charAt(0) !== '2') {
|
||||
const err = new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''));
|
||||
err.code = errors.EPROXY;
|
||||
return fail(err);
|
||||
}
|
||||
// proxy connection is now established
|
||||
finished = true;
|
||||
cleanup();
|
||||
// A fresh socket starts flowing once something listens for 'data', a paused one would
|
||||
// not. Keep that behaviour for the next owner of the socket
|
||||
const resumeOnData = (event) => {
|
||||
if (event === 'data') {
|
||||
socket.removeListener('newListener', resumeOnData);
|
||||
socket.resume();
|
||||
}
|
||||
};
|
||||
socket.on('newListener', resumeOnData);
|
||||
return done(null, socket);
|
||||
}
|
||||
socket = connect(connectOptions, () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
const reqHeaders = {
|
||||
Host: destinationHost + ':' + destinationPort,
|
||||
Host: authority,
|
||||
Connection: 'close'
|
||||
};
|
||||
if (proxy.auth) {
|
||||
@@ -78,9 +148,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
||||
socket.write(
|
||||
// HTTP method
|
||||
'CONNECT ' +
|
||||
destinationHost +
|
||||
':' +
|
||||
destinationPort +
|
||||
authority +
|
||||
' HTTP/1.1\r\n' +
|
||||
// HTTP request headers
|
||||
Object.keys(reqHeaders)
|
||||
@@ -88,62 +156,14 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
||||
.join('\r\n') +
|
||||
// End request
|
||||
'\r\n\r\n');
|
||||
// The response is collected as chunks and only the bytes that just arrived, together
|
||||
// with the three before them, are searched for the end of the headers. Appending to a
|
||||
// string and searching all of it again re-read the whole response on every chunk.
|
||||
const chunks = [];
|
||||
let received = 0;
|
||||
let tail = '';
|
||||
const onSocketData = (chunk) => {
|
||||
let match;
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
const window = tail + chunk.toString('binary');
|
||||
const windowEnd = window.indexOf('\r\n\r\n');
|
||||
chunks.push(chunk);
|
||||
received += chunk.length;
|
||||
tail = window.slice(-3);
|
||||
if (windowEnd >= 0) {
|
||||
socket.removeListener('data', onSocketData);
|
||||
const headerEnd = received - window.length + windowEnd;
|
||||
const response = Buffer.concat(chunks, received).toString('binary');
|
||||
const headers = response.substr(0, headerEnd);
|
||||
const remainder = response.substr(headerEnd + 4);
|
||||
if (remainder) {
|
||||
socket.unshift(Buffer.from(remainder, 'binary'));
|
||||
}
|
||||
// proxy connection is now established
|
||||
finished = true;
|
||||
// check response code
|
||||
match = headers.match(/^HTTP\/\d+\.\d+ (\d+)/i);
|
||||
if (!match || (match[1] || '').charAt(0) !== '2') {
|
||||
try {
|
||||
socket.destroy();
|
||||
}
|
||||
catch (_E) {
|
||||
// ignore
|
||||
}
|
||||
const err = new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''));
|
||||
err.code = errors.EPROXY;
|
||||
return done(err);
|
||||
}
|
||||
socket.removeListener('error', tempSocketErr);
|
||||
socket.removeListener('timeout', timeoutErr);
|
||||
socket.setTimeout(0);
|
||||
return done(null, socket);
|
||||
}
|
||||
if (received > MAX_RESPONSE_HEADER_BYTES) {
|
||||
socket.removeListener('data', onSocketData);
|
||||
const err = new Error('Proxy response headers too large');
|
||||
err.code = errors.EPROXY;
|
||||
return tempSocketErr(err);
|
||||
}
|
||||
};
|
||||
socket.on('data', onSocketData);
|
||||
});
|
||||
socket.setTimeout(httpProxyClient.timeout || 30 * 1000);
|
||||
socket.on('timeout', timeoutErr);
|
||||
socket.once('error', tempSocketErr);
|
||||
timer = setTimeout(() => {
|
||||
const err = new Error('Proxy socket timed out');
|
||||
err.code = errors.ETIMEDOUT;
|
||||
fail(err);
|
||||
}, timeout);
|
||||
socket.once('error', fail);
|
||||
socket.once('close', onEarlyClose);
|
||||
}
|
||||
export default httpProxyClient;
|
||||
+7
-1
@@ -3,7 +3,7 @@ import net from 'node:net';
|
||||
import tls from 'node:tls';
|
||||
import { type Readable } from 'node:stream';
|
||||
import * as shared from '../shared/index.js';
|
||||
import type { Callback, NodemailerError } from '../errors.js';
|
||||
import { type Callback, type NodemailerError } from '../errors.js';
|
||||
import type XOAuth2 from '../xoauth2/index.js';
|
||||
import type { XOAuth2Options } from '../xoauth2/index.js';
|
||||
/**
|
||||
@@ -275,6 +275,12 @@ export interface SMTPConnectionConnectOptions extends tls.ConnectionOptions {
|
||||
allowInternalNetworkInterfaces?: boolean | undefined;
|
||||
/** DNS lookup timeout in ms */
|
||||
timeout?: number | undefined;
|
||||
/** Try the resolved addresses in turn, see net.connect */
|
||||
autoSelectFamily?: boolean | undefined;
|
||||
/** Time in ms an address gets before the next one is tried, see net.connect */
|
||||
autoSelectFamilyAttemptTimeout?: number | undefined;
|
||||
/** Hands the resolved addresses to net.connect */
|
||||
lookup?: net.LookupFunction | undefined;
|
||||
}
|
||||
/**
|
||||
* A queued handler for the next server response
|
||||
|
||||
+349
-137
@@ -7,12 +7,30 @@ import crypto from 'node:crypto';
|
||||
import DataStream from './data-stream.js';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import * as shared from '../shared/index.js';
|
||||
import { ERR_ACCESS_DENIED, isTransientError } from '../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;
|
||||
@@ -135,8 +153,8 @@ class SMTPConnection extends 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;
|
||||
@@ -147,14 +165,25 @@ class SMTPConnection extends 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', () => {
|
||||
@@ -169,11 +198,15 @@ class SMTPConnection extends 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;
|
||||
@@ -202,7 +235,6 @@ class SMTPConnection extends 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
|
||||
@@ -234,9 +266,40 @@ class SMTPConnection extends 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 = net.isIPv6(opts.localAddress) ? 6 : 4;
|
||||
const sameFamily = addresses.filter(addr => net.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 => net.isIPv6(addr));
|
||||
const ipv4 = addresses.filter(addr => !net.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: net.isIPv6(address) ? 6 : 4 }));
|
||||
return setImmediate(() => callback(null, all));
|
||||
}
|
||||
setImmediate(() => callback(null, ordered[0], net.isIPv6(ordered[0]) ? 6 : 4));
|
||||
});
|
||||
}
|
||||
else if (addresses.length) {
|
||||
opts.host = addresses[0];
|
||||
}
|
||||
this._connectToHost(opts, this.secureConnection);
|
||||
});
|
||||
}
|
||||
@@ -283,20 +346,11 @@ class SMTPConnection extends EventEmitter {
|
||||
if (this._destroyed || this._closing) {
|
||||
return;
|
||||
}
|
||||
this._connectionAttemptId++;
|
||||
const currentAttemptId = this._connectionAttemptId;
|
||||
const connectFn = secure
|
||||
? tls.connect
|
||||
: net.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) {
|
||||
@@ -309,51 +363,37 @@ class SMTPConnection extends 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
|
||||
@@ -366,8 +406,7 @@ class SMTPConnection extends 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) {
|
||||
@@ -390,13 +429,15 @@ class SMTPConnection extends 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;
|
||||
}
|
||||
@@ -415,6 +456,15 @@ class SMTPConnection extends 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
|
||||
@@ -619,14 +669,9 @@ class SMTPConnection extends EventEmitter {
|
||||
const startTime = Date.now();
|
||||
this._setEnvelope(envelope, (err, info) => {
|
||||
if (err) {
|
||||
// create passthrough stream to consume to prevent OOM
|
||||
const stream = new 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);
|
||||
}
|
||||
@@ -680,7 +725,7 @@ class SMTPConnection extends EventEmitter {
|
||||
*/
|
||||
_onConnect() {
|
||||
const socket = this._socket;
|
||||
clearTimeout(this._connectionTimeout);
|
||||
this._clearPhase();
|
||||
this.logger.info({
|
||||
tnx: 'network',
|
||||
localAddress: socket.localAddress,
|
||||
@@ -710,16 +755,42 @@ class SMTPConnection extends 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
|
||||
*
|
||||
@@ -794,8 +865,7 @@ class SMTPConnection extends 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
|
||||
@@ -803,12 +873,12 @@ class SMTPConnection extends 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 (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
|
||||
@@ -828,7 +898,12 @@ class SMTPConnection extends 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 !== 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) {
|
||||
@@ -880,6 +955,19 @@ class SMTPConnection extends 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');
|
||||
}
|
||||
@@ -905,7 +993,7 @@ class SMTPConnection extends 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'
|
||||
@@ -920,10 +1008,7 @@ class SMTPConnection extends 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');
|
||||
}
|
||||
/**
|
||||
@@ -940,6 +1025,15 @@ class SMTPConnection extends 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;
|
||||
@@ -949,6 +1043,7 @@ class SMTPConnection extends 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
|
||||
@@ -969,9 +1064,14 @@ class SMTPConnection extends 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 = tls.connect(opts, () => {
|
||||
this._clearPhase();
|
||||
this.secure = true;
|
||||
this.upgrading = false;
|
||||
this._socket.on('data', this._onSocketData);
|
||||
@@ -980,6 +1080,7 @@ class SMTPConnection extends EventEmitter {
|
||||
});
|
||||
}
|
||||
catch (err) {
|
||||
this._clearPhase();
|
||||
removePlainSocketListeners();
|
||||
return callback(err);
|
||||
}
|
||||
@@ -1020,10 +1121,24 @@ class SMTPConnection extends 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');
|
||||
}
|
||||
@@ -1118,6 +1233,9 @@ class SMTPConnection extends 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);
|
||||
});
|
||||
@@ -1155,7 +1273,38 @@ class SMTPConnection extends 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) {
|
||||
@@ -1271,7 +1420,7 @@ class SMTPConnection extends 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;
|
||||
@@ -1309,7 +1458,6 @@ class SMTPConnection extends 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;
|
||||
@@ -1324,17 +1472,30 @@ class SMTPConnection extends 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');
|
||||
@@ -1384,7 +1545,6 @@ class SMTPConnection extends EventEmitter {
|
||||
this._supportedExtensions.push('SIZE');
|
||||
this._maxAllowedSize = Number(match[1]) || 0;
|
||||
}
|
||||
this.emit('connect');
|
||||
}
|
||||
/**
|
||||
* Handles server response for HELO command. If it yielded in
|
||||
@@ -1416,6 +1576,14 @@ class SMTPConnection extends 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;
|
||||
}
|
||||
@@ -1596,21 +1764,9 @@ class SMTPConnection extends 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
|
||||
@@ -1637,32 +1793,67 @@ class SMTPConnection extends 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
|
||||
@@ -1673,6 +1864,27 @@ class SMTPConnection extends 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)) {
|
||||
|
||||
Reference in new issue
Block a user