mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-10-09 09:59:45 +07:00
node_modules: update (#338)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
1 parent
e1936242ed
commit
a9c5eb6c35
39 files changed
+799
-388
No files matched your search
+2
-1
@@ -1,7 +1,8 @@
|
||||
import { Transform, type TransformOptions } from 'node:stream';
|
||||
/**
|
||||
* Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
|
||||
* Also makes sure that only <CR><LF> sequences are used for linebreaks
|
||||
* Also makes sure that only <CR><LF> sequences are used for linebreaks, bare CR and bare LF
|
||||
* are both turned into <CR><LF>
|
||||
*
|
||||
* @param options Stream options
|
||||
*/
|
||||
|
||||
+32
-24
@@ -1,9 +1,15 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const node_stream_1 = require("node:stream");
|
||||
// bytes inserted into the output, shared as they are only ever copied by Buffer.concat
|
||||
const INSERT_LF = Buffer.from('\n');
|
||||
const INSERT_LF_DOT = Buffer.from('\n.');
|
||||
const INSERT_CR = Buffer.from('\r');
|
||||
const INSERT_DOT = Buffer.from('.');
|
||||
/**
|
||||
* Escapes dots in the beginning of lines. Ends the stream with <CR><LF>.<CR><LF>
|
||||
* Also makes sure that only <CR><LF> sequences are used for linebreaks
|
||||
* Also makes sure that only <CR><LF> sequences are used for linebreaks, bare CR and bare LF
|
||||
* are both turned into <CR><LF>
|
||||
*
|
||||
* @param options Stream options
|
||||
*/
|
||||
@@ -32,33 +38,35 @@ class DataStream extends node_stream_1.Transform {
|
||||
}
|
||||
this.inByteCount += chunk.length;
|
||||
for (i = 0, len = chunk.length; i < len; i++) {
|
||||
if (chunk[i] === 0x2e) {
|
||||
// .
|
||||
if ((i && chunk[i - 1] === 0x0a) || (!i && (!this.lastByte || this.lastByte === 0x0a))) {
|
||||
buf = chunk.slice(lastPos, i + 1);
|
||||
chunks.push(buf);
|
||||
chunks.push(Buffer.from('.'));
|
||||
chunklen += buf.length + 1;
|
||||
lastPos = i + 1;
|
||||
}
|
||||
const byte = chunk[i];
|
||||
const prev = i ? chunk[i - 1] : this.lastByte;
|
||||
let insert = false;
|
||||
if (prev === 0x0d && byte !== 0x0a) {
|
||||
// a bare CR becomes CRLF. A receiver that treats a lone CR as a line end would
|
||||
// otherwise see "\r.\r" as the end of the data (SMTP smuggling), so a dot
|
||||
// following it is stuffed like at the start of any other line
|
||||
insert = byte === 0x2e ? INSERT_LF_DOT : INSERT_LF;
|
||||
}
|
||||
else if (chunk[i] === 0x0a) {
|
||||
// \n
|
||||
if ((i && chunk[i - 1] !== 0x0d) || (!i && this.lastByte !== 0x0d)) {
|
||||
if (i > lastPos) {
|
||||
buf = chunk.slice(lastPos, i);
|
||||
chunks.push(buf);
|
||||
chunklen += buf.length + 2;
|
||||
}
|
||||
else {
|
||||
chunklen += 2;
|
||||
}
|
||||
chunks.push(Buffer.from('\r\n'));
|
||||
lastPos = i + 1;
|
||||
else if (byte === 0x0a && prev !== 0x0d) {
|
||||
// a bare LF becomes CRLF
|
||||
insert = INSERT_CR;
|
||||
}
|
||||
else if (byte === 0x2e && (prev === 0x0a || prev === false)) {
|
||||
// a dot at the start of a line
|
||||
insert = INSERT_DOT;
|
||||
}
|
||||
if (insert) {
|
||||
if (i > lastPos) {
|
||||
buf = chunk.slice(lastPos, i);
|
||||
chunks.push(buf);
|
||||
chunklen += buf.length;
|
||||
}
|
||||
chunks.push(insert);
|
||||
chunklen += insert.length;
|
||||
lastPos = i;
|
||||
}
|
||||
}
|
||||
if (chunklen) {
|
||||
if (chunks.length) {
|
||||
// add last piece
|
||||
if (lastPos < chunk.length) {
|
||||
buf = chunk.slice(lastPos);
|
||||
|
||||
+5
-5
@@ -26,11 +26,11 @@ export interface SMTPConnectionOptions {
|
||||
secured?: boolean | undefined;
|
||||
/** Server name for SNI, defaults to host when that is not an IP address */
|
||||
servername?: string | undefined;
|
||||
/** Ignore STARTTLS even when the server advertises it */
|
||||
/** Ignore STARTTLS even when the server advertises it, has no effect when requireTLS is set */
|
||||
ignoreTLS?: boolean | undefined;
|
||||
/** Force STARTTLS, fail when the server does not support it */
|
||||
/** Force STARTTLS, fail when the server does not support it. Takes precedence over ignoreTLS and opportunisticTLS */
|
||||
requireTLS?: boolean | undefined;
|
||||
/** Continue unencrypted when the STARTTLS upgrade fails */
|
||||
/** Continue unencrypted when the STARTTLS upgrade fails, has no effect when requireTLS is set */
|
||||
opportunisticTLS?: boolean | undefined;
|
||||
/** Name of the client server, sent with EHLO/HELO, CRLF is stripped */
|
||||
name?: string | undefined;
|
||||
@@ -288,8 +288,8 @@ export type SMTPConnectionResponseAction = (str: string) => void;
|
||||
* * **port** - is the port to connect to (defaults to 587 or 465)
|
||||
* * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
|
||||
* * **secure** - use SSL
|
||||
* * **ignoreTLS** - ignore server support for STARTTLS
|
||||
* * **requireTLS** - forces the client to use STARTTLS
|
||||
* * **ignoreTLS** - ignore server support for STARTTLS (has no effect when requireTLS is set)
|
||||
* * **requireTLS** - forces the client to use STARTTLS, takes precedence over ignoreTLS and opportunisticTLS
|
||||
* * **name** - the name of the client server
|
||||
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
||||
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
||||
|
||||
+77
-23
@@ -104,8 +104,8 @@ function isPartialLine(line) {
|
||||
* * **port** - is the port to connect to (defaults to 587 or 465)
|
||||
* * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
|
||||
* * **secure** - use SSL
|
||||
* * **ignoreTLS** - ignore server support for STARTTLS
|
||||
* * **requireTLS** - forces the client to use STARTTLS
|
||||
* * **ignoreTLS** - ignore server support for STARTTLS (has no effect when requireTLS is set)
|
||||
* * **requireTLS** - forces the client to use STARTTLS, takes precedence over ignoreTLS and opportunisticTLS
|
||||
* * **name** - the name of the client server
|
||||
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
||||
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
||||
@@ -130,6 +130,11 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
this.id = node_crypto_1.default.randomBytes(8).toString('base64').replace(/\W/g, '');
|
||||
this.stage = 'init';
|
||||
this.options = options || {};
|
||||
if (this.options.requireTLS && (this.options.ignoreTLS || this.options.opportunisticTLS)) {
|
||||
// requireTLS wins, a contradictory configuration must not quietly fall back to plaintext.
|
||||
// Copied so the caller's (possibly shared) options object is left as it was
|
||||
this.options = Object.assign({}, this.options, { ignoreTLS: false, opportunisticTLS: false });
|
||||
}
|
||||
this.secureConnection = !!this.options.secure;
|
||||
this.alreadySecured = !!this.options.secured;
|
||||
this.port = Number(this.options.port) || (this.secureConnection ? 465 : 587);
|
||||
@@ -173,6 +178,8 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
this._destroyed = false;
|
||||
this._closing = false;
|
||||
this._currentDataStream = false;
|
||||
this._pendingSend = false;
|
||||
this._connectCallback = false;
|
||||
this._onSocketData = chunk => this._onData(chunk);
|
||||
this._onSocketError = error => this._onError(error, 'ESOCKET', false, 'CONN');
|
||||
this._onSocketClose = () => this._onClose();
|
||||
@@ -187,7 +194,9 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
*/
|
||||
connect(connectCallback) {
|
||||
if (typeof connectCallback === 'function') {
|
||||
this._connectCallback = connectCallback;
|
||||
this.once('connect', () => {
|
||||
this._connectCallback = false;
|
||||
this.logger.debug({
|
||||
tnx: 'smtp'
|
||||
}, 'SMTP handshake finished');
|
||||
@@ -419,6 +428,16 @@ 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
|
||||
if (this._pendingSend) {
|
||||
const { stream, onStreamError } = this._pendingSend;
|
||||
if (stream) {
|
||||
stream.removeListener('error', onStreamError);
|
||||
stream.on('error', TEARDOWN_NOOP);
|
||||
}
|
||||
this._pendingSend = false;
|
||||
}
|
||||
if (socket && !socket.destroyed) {
|
||||
try {
|
||||
// Clear socket timeout to prevent timer leaks
|
||||
@@ -606,6 +625,9 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
if (this._pendingSend && this._pendingSend.callback === callback) {
|
||||
this._pendingSend = false;
|
||||
}
|
||||
done(err, info);
|
||||
};
|
||||
if (!message) {
|
||||
@@ -622,9 +644,16 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const pendingSend = {
|
||||
callback,
|
||||
stream: false,
|
||||
onStreamError: err => callback(this._formatError(err, 'ESTREAM', false, 'API'))
|
||||
};
|
||||
if (typeof message.on === 'function') {
|
||||
message.on('error', err => callback(this._formatError(err, 'ESTREAM', false, 'API')));
|
||||
pendingSend.stream = message;
|
||||
pendingSend.stream.on('error', pendingSend.onStreamError);
|
||||
}
|
||||
this._pendingSend = pendingSend;
|
||||
const startTime = Date.now();
|
||||
this._setEnvelope(envelope, (err, info) => {
|
||||
if (err) {
|
||||
@@ -817,8 +846,14 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
else {
|
||||
this.logger.error(data, 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
|
||||
const pendingSend = this._pendingSend;
|
||||
this.emit('error', err);
|
||||
this.close();
|
||||
if (pendingSend) {
|
||||
pendingSend.callback(err);
|
||||
}
|
||||
}
|
||||
/** @internal */
|
||||
_formatError(message, type, response, command) {
|
||||
@@ -864,14 +899,25 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
this.logger.info({
|
||||
tnx: 'network'
|
||||
}, 'Connection closed');
|
||||
// the unterminated remainder is only reported as a reply (and so gives the error a responseCode)
|
||||
// when it starts like a complete failure reply, not for a fragment such as "55" or a 250
|
||||
const failureResponse = typeof serverResponse === 'string' && /^[45]\d{2}[ -]/.test(serverResponse) ? serverResponse : false;
|
||||
if (this.upgrading && !this._destroyed) {
|
||||
return this._onError(new Error('Connection closed unexpectedly'), 'ETLS', serverResponse, 'CONN');
|
||||
return this._onError(new Error('Connection closed unexpectedly'), 'ETLS', failureResponse, 'CONN');
|
||||
}
|
||||
else if (![this._actionGreeting, this.close].includes(this._responseActions[0]) && !this._destroyed) {
|
||||
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', serverResponse, 'CONN');
|
||||
if (!failureResponse && this._responseActions[0] === this._actionGreeting && this._connectCallback && !this._destroyed) {
|
||||
// A silent close before the greeting is handed to the connect() callback rather than
|
||||
// emitted as 'error', callers that never saw an error for it must not start throwing one
|
||||
const connectCallback = this._connectCallback;
|
||||
this._connectCallback = false;
|
||||
const err = this._formatError(new Error('Connection closed unexpectedly'), 'ECONNECTION', false, 'CONN');
|
||||
this.logger.warn({ tnx: 'network' }, err.message);
|
||||
connectCallback(err);
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
else if (/^[45]\d{2}\b/.test(serverResponse)) {
|
||||
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', serverResponse, 'CONN');
|
||||
if (failureResponse || (this._responseActions[0] !== this.close && !this._destroyed)) {
|
||||
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', failureResponse, 'CONN');
|
||||
}
|
||||
this._destroy();
|
||||
}
|
||||
@@ -1348,21 +1394,25 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
if (/[ -]AUTH\b/i.test(str)) {
|
||||
this.allowsAuth = true;
|
||||
}
|
||||
// Detect if the server supports PLAIN auth
|
||||
if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)PLAIN/i.test(str)) {
|
||||
this._supportedAuth.push('PLAIN');
|
||||
// Detect the advertised SASL mechanisms. The list is split into whole tokens rather
|
||||
// than searched for each name with a pattern: the patterns this replaced let two
|
||||
// whitespace runs overlap and backtracked quadratically over an AUTH line padded with
|
||||
// spaces, so a hostile server could stall the event loop from its EHLO reply
|
||||
// (GHSA-4ffr-jq9g-5ffx).
|
||||
const authMechanisms = new Set();
|
||||
for (const line of this._ehloLines) {
|
||||
const authMatch = /^AUTH[\s=](.*)/i.exec(line);
|
||||
if (authMatch) {
|
||||
for (const mechanism of authMatch[1].split(/[\s=]+/)) {
|
||||
authMechanisms.add(mechanism.toUpperCase());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Detect if the server supports LOGIN auth
|
||||
if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)LOGIN/i.test(str)) {
|
||||
this._supportedAuth.push('LOGIN');
|
||||
}
|
||||
// Detect if the server supports CRAM-MD5 auth
|
||||
if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)CRAM-MD5/i.test(str)) {
|
||||
this._supportedAuth.push('CRAM-MD5');
|
||||
}
|
||||
// Detect if the server supports XOAUTH2 auth
|
||||
if (/[ -]AUTH(?:(\s+|=)[^\n]*\s+|\s+|=)XOAUTH2/i.test(str)) {
|
||||
this._supportedAuth.push('XOAUTH2');
|
||||
// listed in order of preference, the first one the credentials allow is used
|
||||
for (const mechanism of ['PLAIN', 'LOGIN', 'CRAM-MD5', 'XOAUTH2']) {
|
||||
if (authMechanisms.has(mechanism)) {
|
||||
this._supportedAuth.push(mechanism);
|
||||
}
|
||||
}
|
||||
// Detect if the server supports SIZE extensions (and the max allowed size)
|
||||
if ((match = str.match(/[ -]SIZE(?:[ \t]+(\d+))?/im))) {
|
||||
@@ -1622,7 +1672,11 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
this._sendCommand('DATA');
|
||||
}
|
||||
else {
|
||||
err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', str, 'RCPT TO');
|
||||
// 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);
|
||||
|
||||
Reference in new issue
Block a user