mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-08-12 20:01:24 +07:00
node_modules: update (#307)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
+25
@@ -1,5 +1,30 @@
|
||||
# CHANGELOG
|
||||
|
||||
## [9.0.3](https://github.com/nodemailer/nodemailer/compare/v9.0.2...v9.0.3) (2026-06-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **smtp-connection:** harden STARTTLS upgrade and secure socket handling ([#1835](https://github.com/nodemailer/nodemailer/issues/1835)) ([07d8253](https://github.com/nodemailer/nodemailer/commit/07d8253326ecefff9f7d92c157429ce8bc7335f8))
|
||||
|
||||
## [9.0.2](https://github.com/nodemailer/nodemailer/compare/v9.0.1...v9.0.2) (2026-06-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **addressparser:** keep operator chars inside an address-literal as text ([#1829](https://github.com/nodemailer/nodemailer/issues/1829)) ([9ba1064](https://github.com/nodemailer/nodemailer/commit/9ba1064f3b115cd60cb63aa954a3c0961e86fbb7))
|
||||
* harden smtp-connection low-severity issues ([22ddcea](https://github.com/nodemailer/nodemailer/commit/22ddcea8ed043e4ea23b8971b75a2df196b5a581))
|
||||
* harden smtp-connection response parsing and socket lifecycle ([68860b9](https://github.com/nodemailer/nodemailer/commit/68860b94311b5b6837754e5e790f186ca8a00b70))
|
||||
* prevent SES transport callback double-invocation and hang on sync errors ([#1831](https://github.com/nodemailer/nodemailer/issues/1831)) ([9517bc5](https://github.com/nodemailer/nodemailer/commit/9517bc5dc94e77907bb157e3466bf73a2b327f5c))
|
||||
* reject CRLF in HTTP proxy CONNECT destination to prevent request injection ([6347b47](https://github.com/nodemailer/nodemailer/commit/6347b47c7d12f9d3acf53d391b921e836f400640))
|
||||
|
||||
## [9.0.1](https://github.com/nodemailer/nodemailer/compare/v9.0.0...v9.0.1) (2026-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enforce disableFileAccess/disableUrlAccess for raw message option ([a82e060](https://github.com/nodemailer/nodemailer/commit/a82e060d978f27e5f41369a9a9807b1e3dedc2e2))
|
||||
|
||||
## [9.0.0](https://github.com/nodemailer/nodemailer/compare/v8.0.11...v9.0.0) (2026-06-14)
|
||||
|
||||
|
||||
|
||||
+17
-1
@@ -181,6 +181,7 @@ class Tokenizer {
|
||||
this.operatorExpecting = '';
|
||||
this.node = null;
|
||||
this.escaped = false;
|
||||
this.inDomainLiteral = false;
|
||||
|
||||
this.list = [];
|
||||
/**
|
||||
@@ -232,6 +233,21 @@ class Tokenizer {
|
||||
* @param {String} chr Character from the address field
|
||||
*/
|
||||
checkChar(chr, nextChr) {
|
||||
// Track RFC 5322 domain-literals ("[" *dtext "]"). Operator characters such
|
||||
// as the ":" of an IPv6 address-literal (user@[IPv6:2001:db8::1]) are dtext
|
||||
// and must not be treated as the group delimiter while inside the brackets.
|
||||
// Quoted strings and comments are handled separately via operatorExpecting,
|
||||
// so only enter this state when no operator is open. The list separators ","
|
||||
// and ";" are the exception: they always end the literal (and split the
|
||||
// address list) so that an unclosed "[" cannot swallow later recipients.
|
||||
if (!this.escaped && !this.operatorExpecting) {
|
||||
if (!this.inDomainLiteral && chr === '[') {
|
||||
this.inDomainLiteral = true;
|
||||
} else if (this.inDomainLiteral && (chr === ']' || chr === ',' || chr === ';')) {
|
||||
this.inDomainLiteral = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.escaped) {
|
||||
// ignore next condition blocks
|
||||
} else if (chr === this.operatorExpecting) {
|
||||
@@ -250,7 +266,7 @@ class Tokenizer {
|
||||
this.escaped = false;
|
||||
|
||||
return;
|
||||
} else if (!this.operatorExpecting && chr in this.operators) {
|
||||
} else if (!this.operatorExpecting && !this.inDomainLiteral && chr in this.operators) {
|
||||
this.node = {
|
||||
type: 'operator',
|
||||
value: chr
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ class RelaxedBody extends Transform {
|
||||
options = options || {};
|
||||
this.chunkBuffer = [];
|
||||
this.chunkBufferLen = 0;
|
||||
this.bodyHash = crypto.createHash(options.hashAlgo || 'sha1');
|
||||
this.bodyHash = crypto.createHash(options.hashAlgo || 'sha256');
|
||||
this.remainder = '';
|
||||
this.byteLength = 0;
|
||||
|
||||
|
||||
+5
-1
@@ -32,7 +32,11 @@ class MailComposer {
|
||||
|
||||
// Compose MIME tree
|
||||
if (this.mail.raw) {
|
||||
this.message = new MimeNode('message/rfc822', { newline: this.mail.newline }).setRaw(this.mail.raw);
|
||||
this.message = new MimeNode('message/rfc822', {
|
||||
newline: this.mail.newline,
|
||||
disableUrlAccess: this.mail.disableUrlAccess,
|
||||
disableFileAccess: this.mail.disableFileAccess
|
||||
}).setRaw(this.mail.raw);
|
||||
} else if (this._useMixed) {
|
||||
this.message = this._createMixed();
|
||||
} else if (this._useAlternative) {
|
||||
|
||||
+43
-13
@@ -43,11 +43,12 @@ class SESTransport extends EventEmitter {
|
||||
|
||||
getRegion(cb) {
|
||||
if (this.ses.sesClient.config && typeof this.ses.sesClient.config.region === 'function') {
|
||||
// promise
|
||||
return this.ses.sesClient.config
|
||||
.region()
|
||||
.then(region => cb(null, region))
|
||||
.catch(err => cb(err));
|
||||
// Resolve the region provider. Use the two-argument form of then() so that a
|
||||
// synchronous throw from cb is not recaught here and used to invoke cb a second time.
|
||||
return this.ses.sesClient.config.region().then(
|
||||
region => cb(null, region),
|
||||
err => cb(err)
|
||||
);
|
||||
}
|
||||
return cb(null, false);
|
||||
}
|
||||
@@ -150,8 +151,27 @@ class SESTransport extends EventEmitter {
|
||||
region = 'us-east-1';
|
||||
}
|
||||
|
||||
const command = new this.ses.SendEmailCommand(sesMessage);
|
||||
const sendPromise = this.ses.sesClient.send(command);
|
||||
let sendPromise;
|
||||
try {
|
||||
// command construction or dispatch can throw synchronously on a
|
||||
// misconfigured SDK; surface it as a single error callback instead
|
||||
// of letting it escape into getRegion's promise chain
|
||||
const command = new this.ses.SendEmailCommand(sesMessage);
|
||||
sendPromise = this.ses.sesClient.send(command);
|
||||
} catch (err) {
|
||||
tagSesError(err);
|
||||
this.logger.error(
|
||||
{
|
||||
err,
|
||||
tnx: 'send'
|
||||
},
|
||||
'Send error for %s: %s',
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
setImmediate(() => callback(err));
|
||||
return;
|
||||
}
|
||||
|
||||
sendPromise
|
||||
.then(data => {
|
||||
@@ -159,7 +179,7 @@ class SESTransport extends EventEmitter {
|
||||
region = 'email';
|
||||
}
|
||||
|
||||
callback(null, {
|
||||
const info = {
|
||||
envelope: {
|
||||
from: envelope.from,
|
||||
to: envelope.to
|
||||
@@ -167,7 +187,11 @@ class SESTransport extends EventEmitter {
|
||||
messageId: '<' + data.MessageId + (!/@/.test(data.MessageId) ? '@' + region + '.amazonses.com' : '') + '>',
|
||||
response: data.MessageId,
|
||||
raw
|
||||
});
|
||||
};
|
||||
|
||||
// invoke the callback outside the promise chain so a throw from it
|
||||
// is not recaught by .catch() and used to call it a second time
|
||||
setImmediate(() => callback(null, info));
|
||||
})
|
||||
.catch(err => {
|
||||
tagSesError(err);
|
||||
@@ -180,7 +204,7 @@ class SESTransport extends EventEmitter {
|
||||
messageId,
|
||||
err.message
|
||||
);
|
||||
callback(err);
|
||||
setImmediate(() => callback(err));
|
||||
});
|
||||
});
|
||||
})
|
||||
@@ -222,10 +246,16 @@ class SESTransport extends EventEmitter {
|
||||
// the region value is not used for anything when verifying, but the lookup
|
||||
// exercises the client configuration the same way as send() does
|
||||
this.getRegion(() => {
|
||||
const command = new this.ses.SendEmailCommand(sesMessage);
|
||||
const sendPromise = this.ses.sesClient.send(command);
|
||||
let sendPromise;
|
||||
try {
|
||||
const command = new this.ses.SendEmailCommand(sesMessage);
|
||||
sendPromise = this.ses.sesClient.send(command);
|
||||
} catch (err) {
|
||||
setImmediate(() => cb(err));
|
||||
return;
|
||||
}
|
||||
|
||||
sendPromise.then(() => cb(null)).catch(err => cb(err));
|
||||
sendPromise.then(() => setImmediate(() => cb(null))).catch(err => setImmediate(() => cb(err)));
|
||||
});
|
||||
|
||||
return promise;
|
||||
|
||||
+21
@@ -9,6 +9,10 @@ const tls = require('tls');
|
||||
const urllib = require('../shared/url');
|
||||
const errors = require('../errors');
|
||||
|
||||
// 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;
|
||||
|
||||
/**
|
||||
* Establishes proxied connection to destinationPort
|
||||
*
|
||||
@@ -29,6 +33,16 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
||||
}
|
||||
tlsOptions = tlsOptions || {};
|
||||
|
||||
// Reject CRLF in the destination before it reaches the CONNECT request line
|
||||
// and Host header. A tainted host/port could otherwise inject additional
|
||||
// request headers into the proxy connection (HTTP request splitting).
|
||||
destinationPort = Number(destinationPort) || 0;
|
||||
if (!destinationPort || /[\r\n]/.test(destinationHost)) {
|
||||
const err = new Error('Invalid proxy destination');
|
||||
err.code = errors.EPROXY;
|
||||
return setImmediate(() => callback(err));
|
||||
}
|
||||
|
||||
const proxy = urllib.parse(proxyUrl);
|
||||
|
||||
const connectOptions = {
|
||||
@@ -140,6 +154,13 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions,
|
||||
|
||||
return callback(null, socket);
|
||||
}
|
||||
|
||||
if (headers.length > 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);
|
||||
});
|
||||
|
||||
+80
-22
@@ -298,6 +298,20 @@ class SMTPConnection extends EventEmitter {
|
||||
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
|
||||
if (this.secureConnection && !this.alreadySecured) {
|
||||
return this._upgradeConnection(err => {
|
||||
if (err) {
|
||||
this._onError(new Error('Error initiating TLS - ' + (err.message || err)), 'ETLS', false, 'CONN');
|
||||
return;
|
||||
}
|
||||
this._onConnect();
|
||||
});
|
||||
}
|
||||
|
||||
this._onConnect();
|
||||
});
|
||||
this._setupConnectionHandlers();
|
||||
@@ -365,6 +379,14 @@ class SMTPConnection extends EventEmitter {
|
||||
* @param {Boolean} secure Whether to use TLS
|
||||
*/
|
||||
_connectToHost(opts, secure) {
|
||||
// If the client was closed while DNS resolution was in flight, do not open
|
||||
// a socket here: close() ran with this._socket still unset and so had
|
||||
// nothing to tear down, and _onConnect's remedial close() is a no-op once
|
||||
// _closing is set — the freshly connected socket would leak.
|
||||
if (this._destroyed || this._closing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._connectionAttemptId++;
|
||||
const currentAttemptId = this._connectionAttemptId;
|
||||
|
||||
@@ -431,6 +453,9 @@ class SMTPConnection extends EventEmitter {
|
||||
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
|
||||
@@ -757,6 +782,11 @@ class SMTPConnection extends EventEmitter {
|
||||
* @param {Function} callback Callback to return once connection is reset
|
||||
*/
|
||||
reset(callback) {
|
||||
const isDestroyedMessage = this._isDestroyedMessage('reset');
|
||||
if (isDestroyedMessage) {
|
||||
return callback(this._formatError(isDestroyedMessage, 'ECONNECTION', false, 'API'));
|
||||
}
|
||||
|
||||
this._sendCommand('RSET');
|
||||
this._responseActions.push(str => {
|
||||
if (str.charAt(0) !== '2') {
|
||||
@@ -805,6 +835,9 @@ class SMTPConnection extends EventEmitter {
|
||||
this._socket.removeListener('end', this._onSocketEnd);
|
||||
// Switch from connection-phase error handler to normal error handler
|
||||
this._socket.removeListener('error', this._onConnectionSocketError);
|
||||
// _upgradeConnection (options.connection + secure) may already have attached
|
||||
// the normal handler; remove it first so we never end up with a duplicate
|
||||
this._socket.removeListener('error', this._onSocketError);
|
||||
|
||||
this._socket.on('error', this._onSocketError);
|
||||
this._socket.on('data', this._onSocketData);
|
||||
@@ -994,6 +1027,8 @@ class SMTPConnection extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
this._destroyed = true;
|
||||
// keep the documented public flag in sync with the private state
|
||||
this.destroyed = true;
|
||||
this.emit('end');
|
||||
}
|
||||
|
||||
@@ -1004,6 +1039,15 @@ class SMTPConnection extends EventEmitter {
|
||||
* has been secured
|
||||
*/
|
||||
_upgradeConnection(callback) {
|
||||
// RFC 3207 section 6: the client MUST discard any knowledge obtained from
|
||||
// the server that was not received over the TLS-protected session. Drop any
|
||||
// buffered input received before the handshake so a man-in-the-middle cannot
|
||||
// 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.
|
||||
this._remainder = '';
|
||||
this._responseQueue = [];
|
||||
|
||||
// do not remove all listeners or it breaks node v0.10 as there's
|
||||
// apparently a 'finish' event set that would be cleared as well
|
||||
|
||||
@@ -1032,6 +1076,9 @@ class SMTPConnection extends EventEmitter {
|
||||
socketPlain.removeListener('close', this._onSocketClose);
|
||||
socketPlain.removeListener('end', this._onSocketEnd);
|
||||
socketPlain.removeListener('error', this._onSocketError);
|
||||
// the connection-phase handler is attached when upgrading a pre-opened
|
||||
// options.connection socket; strip it so nothing lingers on the plain socket
|
||||
socketPlain.removeListener('error', this._onConnectionSocketError);
|
||||
};
|
||||
|
||||
this.upgrading = true;
|
||||
@@ -1064,18 +1111,27 @@ class SMTPConnection extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Processes queued responses from the server
|
||||
*
|
||||
* @param {Boolean} force If true, ignores _processing flag
|
||||
*/
|
||||
_processResponse() {
|
||||
if (!this._responseQueue.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let str = (this.lastServerResponse = decodeServerResponse((this._responseQueue.shift() || '').toString()));
|
||||
const raw = (this._responseQueue.shift() || '').toString();
|
||||
|
||||
// Skip unexpected empty lines without consuming a response action or
|
||||
// overwriting lastServerResponse; reprocess whatever else is queued.
|
||||
if (!raw.trim()) {
|
||||
setImmediate(() => this._processResponse());
|
||||
return;
|
||||
}
|
||||
|
||||
let str = (this.lastServerResponse = decodeServerResponse(raw));
|
||||
|
||||
if (/^\d+-/.test(str.split('\n').pop())) {
|
||||
// keep waiting for the final part of multiline response
|
||||
// last line is still a continuation: put the partial response back on the
|
||||
// queue and wait for the rest rather than dropping it
|
||||
this._responseQueue.unshift(raw);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1088,11 +1144,6 @@ class SMTPConnection extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
if (!str.trim()) {
|
||||
// skip unexpected empty lines
|
||||
setImmediate(() => this._processResponse());
|
||||
}
|
||||
|
||||
const action = this._responseActions.shift();
|
||||
|
||||
if (typeof action === 'function') {
|
||||
@@ -1189,6 +1240,23 @@ class SMTPConnection extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// RFC 8689: validate REQUIRETLS eligibility before queuing the MAIL FROM
|
||||
// response action, so a rejection here cannot leave an orphaned action in
|
||||
// _responseActions (which would consume the next reply and desync a reused
|
||||
// connection).
|
||||
if (this._envelope.requireTLSExtensionEnabled) {
|
||||
if (!this.secure) {
|
||||
return callback(
|
||||
this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
||||
);
|
||||
}
|
||||
if (!this._supportedExtensions.includes('REQUIRETLS')) {
|
||||
return callback(
|
||||
this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this._responseActions.push(str => {
|
||||
this._actionMAIL(str, callback);
|
||||
});
|
||||
@@ -1225,20 +1293,10 @@ class SMTPConnection extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// RFC 8689: If the envelope requests REQUIRETLS extension
|
||||
// then append REQUIRETLS keyword to the MAIL FROM command
|
||||
// Note: REQUIRETLS can only be used over TLS connections and requires server support
|
||||
// RFC 8689: append the REQUIRETLS keyword to MAIL FROM. Eligibility
|
||||
// (TLS connection + server support) was already validated above, before
|
||||
// the response action was queued.
|
||||
if (this._envelope.requireTLSExtensionEnabled) {
|
||||
if (!this.secure) {
|
||||
return callback(
|
||||
this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
||||
);
|
||||
}
|
||||
if (!this._supportedExtensions.includes('REQUIRETLS')) {
|
||||
return callback(
|
||||
this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
|
||||
);
|
||||
}
|
||||
args.push('REQUIRETLS');
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nodemailer",
|
||||
"version": "9.0.0",
|
||||
"version": "9.0.3",
|
||||
"description": "Easy as cake e-mail sending from your Node.js applications",
|
||||
"main": "lib/nodemailer.js",
|
||||
"scripts": {
|
||||
@@ -27,10 +27,10 @@
|
||||
},
|
||||
"homepage": "https://nodemailer.com/",
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-sesv2": "3.1065.0",
|
||||
"@aws-sdk/client-sesv2": "3.1068.0",
|
||||
"bunyan": "1.8.15",
|
||||
"c8": "11.0.0",
|
||||
"eslint": "10.4.1",
|
||||
"eslint": "10.5.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.6.0",
|
||||
"libbase64": "1.3.0",
|
||||
@@ -39,7 +39,7 @@
|
||||
"prettier": "3.8.4",
|
||||
"proxy": "1.0.2",
|
||||
"proxy-test-server": "1.0.0",
|
||||
"smtp-server": "3.18.5"
|
||||
"smtp-server": "3.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
|
||||
Reference in New Issue
Block a user