mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-09-29 12:28:07 +07:00
node_modules: update (#325)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
+17
@@ -1,5 +1,22 @@
|
||||
# CHANGELOG
|
||||
|
||||
## [10.0.10](https://github.com/nodemailer/nodemailer/compare/v10.0.9...v10.0.10) (2026-09-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* derive the attachment filename from the basename of a Windows path ([c7cc7ce](https://github.com/nodemailer/nodemailer/commit/c7cc7ce41a3602441747476a2a2c4a8ff466a83e))
|
||||
* **dkim:** unfold folded header lines in linear time ([28a5909](https://github.com/nodemailer/nodemailer/commit/28a5909cec27646cb001dc7e97a8f5d26c973078))
|
||||
* **smtp-connection:** reassemble multiline replies in linear time ([f2d82fa](https://github.com/nodemailer/nodemailer/commit/f2d82fa84d47015a7bbb4253da61eeb778c658b1))
|
||||
|
||||
## [10.0.9](https://github.com/nodemailer/nodemailer/compare/v10.0.8...v10.0.9) (2026-09-12)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **addressparser:** bound the '@' probe to the run being scanned ([1465c3f](https://github.com/nodemailer/nodemailer/commit/1465c3f5ff74a7c4fbbe9853bd01448bb92bf5b7))
|
||||
* **addressparser:** keep the text after a comment out of a quoted local part address ([2f36eb1](https://github.com/nodemailer/nodemailer/commit/2f36eb1aa1dd33e312411dc9b888548e14db54ee))
|
||||
|
||||
## [10.0.8](https://github.com/nodemailer/nodemailer/compare/v10.0.7...v10.0.8) (2026-09-11)
|
||||
|
||||
|
||||
|
||||
+53
-8
@@ -88,6 +88,29 @@ function _isWordCode(code) {
|
||||
function _isBoundary(text, at) {
|
||||
return _isWordCode(text.charCodeAt(at - 1)) !== _isWordCode(text.charCodeAt(at));
|
||||
}
|
||||
/**
|
||||
* Offset of the first '@' in `text` between `from` and `to`, or -1 when the range holds none.
|
||||
*
|
||||
* indexOf would scan on to the end of the value, and the value here is a whole header. The
|
||||
* walk below steps one whitespace delimited run at a time and only ever uses a '@' that sits
|
||||
* inside the run it is on, so an unbounded probe rescans everything behind that run once per
|
||||
* run and grows with the square of the header: 400KB of free text carrying no '@' took a
|
||||
* quarter of a second. GHSA-v53p-9fqp-m79j took the pattern search out of this walk and left
|
||||
* the probe unbounded behind it.
|
||||
*
|
||||
* @param text Text to look in
|
||||
* @param from Offset to start at
|
||||
* @param to Offset to stop before
|
||||
* @return Offset of the '@', or -1
|
||||
*/
|
||||
function _indexOfAt(text, from, to) {
|
||||
for (let i = from; i < to; i++) {
|
||||
if (text.charCodeAt(i) === 0x40) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
/**
|
||||
* Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
|
||||
*
|
||||
@@ -118,8 +141,8 @@ function _looseAddressStart(text) {
|
||||
while (runEnd < len && !_isSpaceCode(text.charCodeAt(runEnd))) {
|
||||
runEnd++;
|
||||
}
|
||||
let at = text.indexOf('@', runStart);
|
||||
if (at >= 0 && at < runEnd) {
|
||||
let at = _indexOfAt(text, runStart, runEnd);
|
||||
if (at >= 0) {
|
||||
let lastBoundary = -1;
|
||||
for (let k = runEnd; k > runStart; k--) {
|
||||
if (_isBoundary(text, k)) {
|
||||
@@ -128,7 +151,7 @@ function _looseAddressStart(text) {
|
||||
}
|
||||
}
|
||||
let atomStart = runStart;
|
||||
while (lastBoundary >= 0 && at >= 0 && at < runEnd) {
|
||||
while (lastBoundary >= 0 && at >= 0) {
|
||||
// '[^@\s]+' has to cover a character before the '@' and '[^\s]+' one after it,
|
||||
// and the boundary that ends the match has to sit past both
|
||||
if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
|
||||
@@ -148,7 +171,7 @@ function _looseAddressStart(text) {
|
||||
}
|
||||
}
|
||||
atomStart = at + 1;
|
||||
at = text.indexOf('@', atomStart);
|
||||
at = _indexOfAt(text, atomStart, runEnd);
|
||||
}
|
||||
}
|
||||
pos = runEnd;
|
||||
@@ -275,6 +298,19 @@ function _handleAddress(tokens, depth) {
|
||||
}
|
||||
}
|
||||
else if (token.value) {
|
||||
// An empty quoted string is dropped by the tokenizer, leaving no text token of its
|
||||
// own for textWasQuoted to be recorded on, so the pair of quote operators right in
|
||||
// front of this token is all that is left of it and the run it opens carries the
|
||||
// quoting instead. Without this '""@example.com' reads as the bare '@example.com',
|
||||
// the quotes never go back on, and the value is no longer an addr-spec a trailing
|
||||
// comment can be peeled off of. It only ever opens a run: a run that already holds
|
||||
// material collected outside the quotes is not a quoted string, whatever follows it
|
||||
const prevPrevToken = i > 1 ? tokens[i - 2] : null;
|
||||
const opensAfterEmptyQuotedString = prevToken?.type === 'operator' &&
|
||||
prevToken.value === '"' &&
|
||||
!!prevToken.noBreak &&
|
||||
prevPrevToken?.type === 'operator' &&
|
||||
prevPrevToken.value === '"';
|
||||
if (state === 'address') {
|
||||
// Handle unquoted name that includes a "<".
|
||||
// Apple Mail truncates everything between an unexpected < and an address.
|
||||
@@ -302,7 +338,7 @@ function _handleAddress(tokens, depth) {
|
||||
data[state].push(token.value);
|
||||
lastChars[state] = token.value.charAt(token.value.length - 1);
|
||||
if (state === 'text') {
|
||||
data.textWasQuoted.push(insideQuotes);
|
||||
data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -387,6 +423,18 @@ function _handleAddress(tokens, depth) {
|
||||
// Join values with spaces
|
||||
data.text = data.text.join(' ');
|
||||
data.address = data.address.join(' ');
|
||||
if (addressFromQuotedText && data.text) {
|
||||
// The mailbox is still sitting in the text, so it moves over here and is quoted
|
||||
// before the recovery below rather than after it. Anything else the text holds
|
||||
// came along with it: a comment ends the domain but leaves the atoms behind it in
|
||||
// the same text, and '"user"@example.com(x)evil.com' was handed on as the address
|
||||
// 'user@example.com evil.com', a second domain riding into the envelope recipient
|
||||
// on a value that is no addr-spec at all (GHSA-g57g-f23g-4646). Putting the quotes
|
||||
// back first is what lets the recovery tell the whitespace an addr-spec may carry
|
||||
// from the wreckage trailing one, as only a quoted local part may hold whitespace
|
||||
data.address = _quoteLocalPart(data.text);
|
||||
data.text = '';
|
||||
}
|
||||
_recoverAddrSpec(data);
|
||||
const address = {
|
||||
address: data.address || data.text || '',
|
||||
@@ -400,9 +448,6 @@ function _handleAddress(tokens, depth) {
|
||||
address.address = '';
|
||||
}
|
||||
}
|
||||
if (addressFromQuotedText && address.address) {
|
||||
address.address = _quoteLocalPart(address.address);
|
||||
}
|
||||
addresses.push(address);
|
||||
}
|
||||
return addresses;
|
||||
|
||||
+12
-5
@@ -126,11 +126,18 @@ class MessageParser extends node_stream_1.Transform {
|
||||
// signature covers exactly the bytes the receiving side canonicalizes
|
||||
// Only SP and HTAB fold a line, and only they are trimmed from the field name, the
|
||||
// same whitespace the relaxed canonicalization in sign.ts works with
|
||||
const lines = (this.rawHeaders || Buffer.alloc(0)).toString('binary').split(/\r?\n/);
|
||||
for (let i = lines.length - 1; i > 0; i--) {
|
||||
if (/^[ \t]/.test(lines[i])) {
|
||||
lines[i - 1] += '\n' + lines[i];
|
||||
lines.splice(i, 1);
|
||||
const rawLines = (this.rawHeaders || Buffer.alloc(0)).toString('binary').split(/\r?\n/);
|
||||
// Unfold in a single forward pass and only ever test a freshly split line for the
|
||||
// continuation prefix. Testing an already merged line instead would rescan a string
|
||||
// that grows with every continuation line, so a header folded into many continuation
|
||||
// lines (a large recipient list, for example) would take quadratic time to unfold
|
||||
const lines = [];
|
||||
for (const rawLine of rawLines) {
|
||||
if (lines.length && /^[ \t]/.test(rawLine)) {
|
||||
lines[lines.length - 1] += '\n' + rawLine;
|
||||
}
|
||||
else {
|
||||
lines.push(rawLine);
|
||||
}
|
||||
}
|
||||
return lines
|
||||
|
||||
+3
-1
@@ -151,8 +151,10 @@ class MailComposer {
|
||||
data.filename = attachment.filename;
|
||||
}
|
||||
else if (!isMessageNode && attachment.filename !== false) {
|
||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||
data.filename =
|
||||
(attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||
'attachment-' + (i + 1);
|
||||
if (data.filename.indexOf('.') < 0) {
|
||||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
||||
}
|
||||
|
||||
+2
-1
@@ -107,8 +107,9 @@ class MailMessage {
|
||||
if (this.data.attachments && this.data.attachments.length) {
|
||||
this.data.attachments.forEach((attachment, i) => {
|
||||
if (!attachment.filename) {
|
||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||
attachment.filename =
|
||||
(attachment.path || attachment.href || '').split('/').pop().split('?').shift() ||
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||
'attachment-' + (i + 1);
|
||||
if (attachment.filename.indexOf('.') < 0) {
|
||||
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
export declare const name = "nodemailer";
|
||||
export declare const version = "10.0.8";
|
||||
export declare const version = "10.0.10";
|
||||
export declare const homepage = "https://nodemailer.com/";
|
||||
|
||||
+1
-1
@@ -3,5 +3,5 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.homepage = exports.version = exports.name = void 0;
|
||||
exports.name = 'nodemailer';
|
||||
exports.version = '10.0.8';
|
||||
exports.version = '10.0.10';
|
||||
exports.homepage = 'https://nodemailer.com/';
|
||||
|
||||
+3
@@ -41,6 +41,8 @@ export interface SMTPConnectionOptions {
|
||||
greetingTimeout?: number | undefined;
|
||||
/** Time of inactivity in ms until the connection is closed, defaults to 10 minutes */
|
||||
socketTimeout?: number | undefined;
|
||||
/** Largest single server response to accept in bytes, defaults to 1 MB */
|
||||
maxResponseSize?: number | undefined;
|
||||
/** Time to wait in ms for the DNS requests to be resolved, defaults to 30 seconds */
|
||||
dnsTimeout?: number | undefined;
|
||||
/** Use LMTP instead of SMTP */
|
||||
@@ -268,6 +270,7 @@ export type SMTPConnectionResponseAction = (str: string) => void;
|
||||
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
||||
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish (defaults to 2 minutes)
|
||||
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 10 minutes)
|
||||
* * **maxResponseSize** - Largest single server response to accept in bytes (defaults to 1 MB)
|
||||
* * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
|
||||
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
||||
* * **logger** - bunyan compatible logger interface
|
||||
|
||||
+62
-18
@@ -51,6 +51,9 @@ const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity
|
||||
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 TEARDOWN_NOOP = () => { }; // reusable no-op handler for absorbing errors during socket teardown
|
||||
// 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;
|
||||
/**
|
||||
* Re-interpret a server response stored in fake 8-bit byte-container form
|
||||
* (the result of chunk.toString('binary') in _onData) as UTF-8.
|
||||
@@ -79,11 +82,19 @@ function decodeServerResponse(str) {
|
||||
* Called with the byte-container form the queue holds (see _onData): the check only looks
|
||||
* at leading ASCII digits and '-' of the last line, and a UTF-8 continuation byte is never
|
||||
* 0x0A, so line boundaries and the tested prefix are the same before and after decoding.
|
||||
* The last line is read with lastIndexOf rather than split() because a queue entry grows
|
||||
* with every chunk appended to it and only its final line matters.
|
||||
* The last line is read with lastIndexOf rather than split() because a queue entry may hold
|
||||
* a whole multiline reply and only its final line matters.
|
||||
*/
|
||||
function isPartialResponse(str) {
|
||||
return /^\d+-/.test(str.slice(str.lastIndexOf('\n') + 1));
|
||||
return isPartialLine(str.slice(str.lastIndexOf('\n') + 1));
|
||||
}
|
||||
/**
|
||||
* True when a single reply line is a continuation ("250-..."). Used where the line is
|
||||
* already known to be one line, which skips the scan isPartialResponse needs to find the
|
||||
* last line of a whole reply.
|
||||
*/
|
||||
function isPartialLine(line) {
|
||||
return /^\d+-/.test(line);
|
||||
}
|
||||
/**
|
||||
* Generates a SMTP connection object
|
||||
@@ -100,6 +111,7 @@ function isPartialResponse(str) {
|
||||
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
||||
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish (defaults to 2 minutes)
|
||||
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 10 minutes)
|
||||
* * **maxResponseSize** - Largest single server response to accept in bytes (defaults to 1 MB)
|
||||
* * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
|
||||
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
||||
* * **logger** - bunyan compatible logger interface
|
||||
@@ -146,6 +158,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
this.secure = !!this.secureConnection;
|
||||
this._remainder = '';
|
||||
this._responseQueue = [];
|
||||
this._responsePartial = false;
|
||||
this.lastServerResponse = false;
|
||||
this._socket = false;
|
||||
this._supportedAuth = [];
|
||||
@@ -714,28 +727,58 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
if (this._destroyed || !chunk || !chunk.length) {
|
||||
return;
|
||||
}
|
||||
let data = chunk.toString('binary');
|
||||
let lines = (this._remainder + data).split(/\r?\n/);
|
||||
let lastline;
|
||||
const maxResponseSize = this.options.maxResponseSize || MAX_RESPONSE_SIZE;
|
||||
const data = chunk.toString('binary');
|
||||
// A chunk without a line break only extends the line currently being received, so
|
||||
// keep it in the remainder and leave that string unflattened. Splitting the whole
|
||||
// remainder again on every chunk would rescan everything buffered for that line so
|
||||
// far, which is quadratic in the length of a line the peer never terminates
|
||||
if (!data.includes('\n')) {
|
||||
this._remainder += data;
|
||||
if (this._remainder.length > maxResponseSize) {
|
||||
return this._onResponseTooLarge();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const lines = (this._remainder + data).split(/\r?\n/);
|
||||
this._remainder = lines.pop();
|
||||
for (let i = 0, len = lines.length; i < len; i++) {
|
||||
if (this._responseQueue.length) {
|
||||
lastline = this._responseQueue[this._responseQueue.length - 1];
|
||||
if (isPartialResponse(lastline)) {
|
||||
this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
|
||||
continue;
|
||||
}
|
||||
if (this._responsePartial) {
|
||||
this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
|
||||
}
|
||||
else {
|
||||
this._responseQueue.push(lines[i]);
|
||||
}
|
||||
// The line just added is the last line of that queue entry, so it alone decides
|
||||
// whether the reply is still partial. Looking for the last line of the accumulated
|
||||
// entry instead would rescan a string that grows with every continuation line,
|
||||
// which is quadratic in the size of the reply
|
||||
this._responsePartial = isPartialLine(lines[i]);
|
||||
// Checked as each line lands, so a peer that never completes a reply cannot keep
|
||||
// buffering, and the limit does not depend on how it split its bytes into chunks
|
||||
if (this._responsePartial && this._responseQueue[this._responseQueue.length - 1].length > maxResponseSize) {
|
||||
return this._onResponseTooLarge();
|
||||
}
|
||||
this._responseQueue.push(lines[i]);
|
||||
}
|
||||
if (this._responseQueue.length) {
|
||||
lastline = this._responseQueue[this._responseQueue.length - 1];
|
||||
if (isPartialResponse(lastline)) {
|
||||
return;
|
||||
}
|
||||
if (this._remainder.length > maxResponseSize) {
|
||||
return this._onResponseTooLarge();
|
||||
}
|
||||
if (this._responsePartial) {
|
||||
return;
|
||||
}
|
||||
this._processResponse();
|
||||
}
|
||||
/**
|
||||
* Drops a connection whose peer keeps extending a reply it never completes, releasing
|
||||
* whatever was buffered for that reply
|
||||
* @internal
|
||||
*/
|
||||
_onResponseTooLarge() {
|
||||
this._remainder = '';
|
||||
this._responseQueue = [];
|
||||
this._responsePartial = false;
|
||||
this._onError(new Error('Server response exceeds maximum allowed size'), 'EPROTOCOL', false, 'CONN');
|
||||
}
|
||||
/**
|
||||
* 'error' listener for the socket
|
||||
*
|
||||
@@ -876,6 +919,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
||||
// part of the secured EHLO capabilities). STARTTLS response injection.
|
||||
this._remainder = '';
|
||||
this._responseQueue = [];
|
||||
this._responsePartial = false;
|
||||
// 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
|
||||
// we can safely keep 'error', 'end', 'close' etc. events
|
||||
|
||||
+53
-8
@@ -85,6 +85,29 @@ function _isWordCode(code) {
|
||||
function _isBoundary(text, at) {
|
||||
return _isWordCode(text.charCodeAt(at - 1)) !== _isWordCode(text.charCodeAt(at));
|
||||
}
|
||||
/**
|
||||
* Offset of the first '@' in `text` between `from` and `to`, or -1 when the range holds none.
|
||||
*
|
||||
* indexOf would scan on to the end of the value, and the value here is a whole header. The
|
||||
* walk below steps one whitespace delimited run at a time and only ever uses a '@' that sits
|
||||
* inside the run it is on, so an unbounded probe rescans everything behind that run once per
|
||||
* run and grows with the square of the header: 400KB of free text carrying no '@' took a
|
||||
* quarter of a second. GHSA-v53p-9fqp-m79j took the pattern search out of this walk and left
|
||||
* the probe unbounded behind it.
|
||||
*
|
||||
* @param text Text to look in
|
||||
* @param from Offset to start at
|
||||
* @param to Offset to stop before
|
||||
* @return Offset of the '@', or -1
|
||||
*/
|
||||
function _indexOfAt(text, from, to) {
|
||||
for (let i = from; i < to; i++) {
|
||||
if (text.charCodeAt(i) === 0x40) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
/**
|
||||
* Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
|
||||
*
|
||||
@@ -115,8 +138,8 @@ function _looseAddressStart(text) {
|
||||
while (runEnd < len && !_isSpaceCode(text.charCodeAt(runEnd))) {
|
||||
runEnd++;
|
||||
}
|
||||
let at = text.indexOf('@', runStart);
|
||||
if (at >= 0 && at < runEnd) {
|
||||
let at = _indexOfAt(text, runStart, runEnd);
|
||||
if (at >= 0) {
|
||||
let lastBoundary = -1;
|
||||
for (let k = runEnd; k > runStart; k--) {
|
||||
if (_isBoundary(text, k)) {
|
||||
@@ -125,7 +148,7 @@ function _looseAddressStart(text) {
|
||||
}
|
||||
}
|
||||
let atomStart = runStart;
|
||||
while (lastBoundary >= 0 && at >= 0 && at < runEnd) {
|
||||
while (lastBoundary >= 0 && at >= 0) {
|
||||
// '[^@\s]+' has to cover a character before the '@' and '[^\s]+' one after it,
|
||||
// and the boundary that ends the match has to sit past both
|
||||
if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
|
||||
@@ -145,7 +168,7 @@ function _looseAddressStart(text) {
|
||||
}
|
||||
}
|
||||
atomStart = at + 1;
|
||||
at = text.indexOf('@', atomStart);
|
||||
at = _indexOfAt(text, atomStart, runEnd);
|
||||
}
|
||||
}
|
||||
pos = runEnd;
|
||||
@@ -272,6 +295,19 @@ function _handleAddress(tokens, depth) {
|
||||
}
|
||||
}
|
||||
else if (token.value) {
|
||||
// An empty quoted string is dropped by the tokenizer, leaving no text token of its
|
||||
// own for textWasQuoted to be recorded on, so the pair of quote operators right in
|
||||
// front of this token is all that is left of it and the run it opens carries the
|
||||
// quoting instead. Without this '""@example.com' reads as the bare '@example.com',
|
||||
// the quotes never go back on, and the value is no longer an addr-spec a trailing
|
||||
// comment can be peeled off of. It only ever opens a run: a run that already holds
|
||||
// material collected outside the quotes is not a quoted string, whatever follows it
|
||||
const prevPrevToken = i > 1 ? tokens[i - 2] : null;
|
||||
const opensAfterEmptyQuotedString = prevToken?.type === 'operator' &&
|
||||
prevToken.value === '"' &&
|
||||
!!prevToken.noBreak &&
|
||||
prevPrevToken?.type === 'operator' &&
|
||||
prevPrevToken.value === '"';
|
||||
if (state === 'address') {
|
||||
// Handle unquoted name that includes a "<".
|
||||
// Apple Mail truncates everything between an unexpected < and an address.
|
||||
@@ -299,7 +335,7 @@ function _handleAddress(tokens, depth) {
|
||||
data[state].push(token.value);
|
||||
lastChars[state] = token.value.charAt(token.value.length - 1);
|
||||
if (state === 'text') {
|
||||
data.textWasQuoted.push(insideQuotes);
|
||||
data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,6 +420,18 @@ function _handleAddress(tokens, depth) {
|
||||
// Join values with spaces
|
||||
data.text = data.text.join(' ');
|
||||
data.address = data.address.join(' ');
|
||||
if (addressFromQuotedText && data.text) {
|
||||
// The mailbox is still sitting in the text, so it moves over here and is quoted
|
||||
// before the recovery below rather than after it. Anything else the text holds
|
||||
// came along with it: a comment ends the domain but leaves the atoms behind it in
|
||||
// the same text, and '"user"@example.com(x)evil.com' was handed on as the address
|
||||
// 'user@example.com evil.com', a second domain riding into the envelope recipient
|
||||
// on a value that is no addr-spec at all (GHSA-g57g-f23g-4646). Putting the quotes
|
||||
// back first is what lets the recovery tell the whitespace an addr-spec may carry
|
||||
// from the wreckage trailing one, as only a quoted local part may hold whitespace
|
||||
data.address = _quoteLocalPart(data.text);
|
||||
data.text = '';
|
||||
}
|
||||
_recoverAddrSpec(data);
|
||||
const address = {
|
||||
address: data.address || data.text || '',
|
||||
@@ -397,9 +445,6 @@ function _handleAddress(tokens, depth) {
|
||||
address.address = '';
|
||||
}
|
||||
}
|
||||
if (addressFromQuotedText && address.address) {
|
||||
address.address = _quoteLocalPart(address.address);
|
||||
}
|
||||
addresses.push(address);
|
||||
}
|
||||
return addresses;
|
||||
|
||||
+12
-5
@@ -124,11 +124,18 @@ export default class MessageParser extends Transform {
|
||||
// signature covers exactly the bytes the receiving side canonicalizes
|
||||
// Only SP and HTAB fold a line, and only they are trimmed from the field name, the
|
||||
// same whitespace the relaxed canonicalization in sign.ts works with
|
||||
const lines = (this.rawHeaders || Buffer.alloc(0)).toString('binary').split(/\r?\n/);
|
||||
for (let i = lines.length - 1; i > 0; i--) {
|
||||
if (/^[ \t]/.test(lines[i])) {
|
||||
lines[i - 1] += '\n' + lines[i];
|
||||
lines.splice(i, 1);
|
||||
const rawLines = (this.rawHeaders || Buffer.alloc(0)).toString('binary').split(/\r?\n/);
|
||||
// Unfold in a single forward pass and only ever test a freshly split line for the
|
||||
// continuation prefix. Testing an already merged line instead would rescan a string
|
||||
// that grows with every continuation line, so a header folded into many continuation
|
||||
// lines (a large recipient list, for example) would take quadratic time to unfold
|
||||
const lines = [];
|
||||
for (const rawLine of rawLines) {
|
||||
if (lines.length && /^[ \t]/.test(rawLine)) {
|
||||
lines[lines.length - 1] += '\n' + rawLine;
|
||||
}
|
||||
else {
|
||||
lines.push(rawLine);
|
||||
}
|
||||
}
|
||||
return lines
|
||||
|
||||
+3
-1
@@ -113,8 +113,10 @@ class MailComposer {
|
||||
data.filename = attachment.filename;
|
||||
}
|
||||
else if (!isMessageNode && attachment.filename !== false) {
|
||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||
data.filename =
|
||||
(attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||
'attachment-' + (i + 1);
|
||||
if (data.filename.indexOf('.') < 0) {
|
||||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
||||
}
|
||||
|
||||
+2
-1
@@ -69,8 +69,9 @@ export default class MailMessage {
|
||||
if (this.data.attachments && this.data.attachments.length) {
|
||||
this.data.attachments.forEach((attachment, i) => {
|
||||
if (!attachment.filename) {
|
||||
// a backslash separates as well, so a Windows path does not put the sender's directories in the headers
|
||||
attachment.filename =
|
||||
(attachment.path || attachment.href || '').split('/').pop().split('?').shift() ||
|
||||
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||
'attachment-' + (i + 1);
|
||||
if (attachment.filename.indexOf('.') < 0) {
|
||||
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
export declare const name = "nodemailer";
|
||||
export declare const version = "10.0.8";
|
||||
export declare const version = "10.0.10";
|
||||
export declare const homepage = "https://nodemailer.com/";
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Generated by scripts/build.js from package.json. Do not edit by hand.
|
||||
export const name = 'nodemailer';
|
||||
export const version = '10.0.8';
|
||||
export const version = '10.0.10';
|
||||
export const homepage = 'https://nodemailer.com/';
|
||||
|
||||
+3
@@ -41,6 +41,8 @@ export interface SMTPConnectionOptions {
|
||||
greetingTimeout?: number | undefined;
|
||||
/** Time of inactivity in ms until the connection is closed, defaults to 10 minutes */
|
||||
socketTimeout?: number | undefined;
|
||||
/** Largest single server response to accept in bytes, defaults to 1 MB */
|
||||
maxResponseSize?: number | undefined;
|
||||
/** Time to wait in ms for the DNS requests to be resolved, defaults to 30 seconds */
|
||||
dnsTimeout?: number | undefined;
|
||||
/** Use LMTP instead of SMTP */
|
||||
@@ -268,6 +270,7 @@ export type SMTPConnectionResponseAction = (str: string) => void;
|
||||
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
||||
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish (defaults to 2 minutes)
|
||||
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 10 minutes)
|
||||
* * **maxResponseSize** - Largest single server response to accept in bytes (defaults to 1 MB)
|
||||
* * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
|
||||
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
||||
* * **logger** - bunyan compatible logger interface
|
||||
|
||||
+62
-18
@@ -13,6 +13,9 @@ const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity
|
||||
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 TEARDOWN_NOOP = () => { }; // reusable no-op handler for absorbing errors during socket teardown
|
||||
// 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;
|
||||
/**
|
||||
* Re-interpret a server response stored in fake 8-bit byte-container form
|
||||
* (the result of chunk.toString('binary') in _onData) as UTF-8.
|
||||
@@ -41,11 +44,19 @@ function decodeServerResponse(str) {
|
||||
* Called with the byte-container form the queue holds (see _onData): the check only looks
|
||||
* at leading ASCII digits and '-' of the last line, and a UTF-8 continuation byte is never
|
||||
* 0x0A, so line boundaries and the tested prefix are the same before and after decoding.
|
||||
* The last line is read with lastIndexOf rather than split() because a queue entry grows
|
||||
* with every chunk appended to it and only its final line matters.
|
||||
* The last line is read with lastIndexOf rather than split() because a queue entry may hold
|
||||
* a whole multiline reply and only its final line matters.
|
||||
*/
|
||||
function isPartialResponse(str) {
|
||||
return /^\d+-/.test(str.slice(str.lastIndexOf('\n') + 1));
|
||||
return isPartialLine(str.slice(str.lastIndexOf('\n') + 1));
|
||||
}
|
||||
/**
|
||||
* True when a single reply line is a continuation ("250-..."). Used where the line is
|
||||
* already known to be one line, which skips the scan isPartialResponse needs to find the
|
||||
* last line of a whole reply.
|
||||
*/
|
||||
function isPartialLine(line) {
|
||||
return /^\d+-/.test(line);
|
||||
}
|
||||
/**
|
||||
* Generates a SMTP connection object
|
||||
@@ -62,6 +73,7 @@ function isPartialResponse(str) {
|
||||
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
||||
* * **connectionTimeout** - how many milliseconds to wait for the connection to establish (defaults to 2 minutes)
|
||||
* * **socketTimeout** - Time of inactivity until the connection is closed (defaults to 10 minutes)
|
||||
* * **maxResponseSize** - Largest single server response to accept in bytes (defaults to 1 MB)
|
||||
* * **dnsTimeout** - Time to wait in ms for the DNS requests to be resolved (defaults to 30 seconds)
|
||||
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
||||
* * **logger** - bunyan compatible logger interface
|
||||
@@ -108,6 +120,7 @@ class SMTPConnection extends EventEmitter {
|
||||
this.secure = !!this.secureConnection;
|
||||
this._remainder = '';
|
||||
this._responseQueue = [];
|
||||
this._responsePartial = false;
|
||||
this.lastServerResponse = false;
|
||||
this._socket = false;
|
||||
this._supportedAuth = [];
|
||||
@@ -676,28 +689,58 @@ class SMTPConnection extends EventEmitter {
|
||||
if (this._destroyed || !chunk || !chunk.length) {
|
||||
return;
|
||||
}
|
||||
let data = chunk.toString('binary');
|
||||
let lines = (this._remainder + data).split(/\r?\n/);
|
||||
let lastline;
|
||||
const maxResponseSize = this.options.maxResponseSize || MAX_RESPONSE_SIZE;
|
||||
const data = chunk.toString('binary');
|
||||
// A chunk without a line break only extends the line currently being received, so
|
||||
// keep it in the remainder and leave that string unflattened. Splitting the whole
|
||||
// remainder again on every chunk would rescan everything buffered for that line so
|
||||
// far, which is quadratic in the length of a line the peer never terminates
|
||||
if (!data.includes('\n')) {
|
||||
this._remainder += data;
|
||||
if (this._remainder.length > maxResponseSize) {
|
||||
return this._onResponseTooLarge();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const lines = (this._remainder + data).split(/\r?\n/);
|
||||
this._remainder = lines.pop();
|
||||
for (let i = 0, len = lines.length; i < len; i++) {
|
||||
if (this._responseQueue.length) {
|
||||
lastline = this._responseQueue[this._responseQueue.length - 1];
|
||||
if (isPartialResponse(lastline)) {
|
||||
this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
|
||||
continue;
|
||||
}
|
||||
if (this._responsePartial) {
|
||||
this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
|
||||
}
|
||||
else {
|
||||
this._responseQueue.push(lines[i]);
|
||||
}
|
||||
// The line just added is the last line of that queue entry, so it alone decides
|
||||
// whether the reply is still partial. Looking for the last line of the accumulated
|
||||
// entry instead would rescan a string that grows with every continuation line,
|
||||
// which is quadratic in the size of the reply
|
||||
this._responsePartial = isPartialLine(lines[i]);
|
||||
// Checked as each line lands, so a peer that never completes a reply cannot keep
|
||||
// buffering, and the limit does not depend on how it split its bytes into chunks
|
||||
if (this._responsePartial && this._responseQueue[this._responseQueue.length - 1].length > maxResponseSize) {
|
||||
return this._onResponseTooLarge();
|
||||
}
|
||||
this._responseQueue.push(lines[i]);
|
||||
}
|
||||
if (this._responseQueue.length) {
|
||||
lastline = this._responseQueue[this._responseQueue.length - 1];
|
||||
if (isPartialResponse(lastline)) {
|
||||
return;
|
||||
}
|
||||
if (this._remainder.length > maxResponseSize) {
|
||||
return this._onResponseTooLarge();
|
||||
}
|
||||
if (this._responsePartial) {
|
||||
return;
|
||||
}
|
||||
this._processResponse();
|
||||
}
|
||||
/**
|
||||
* Drops a connection whose peer keeps extending a reply it never completes, releasing
|
||||
* whatever was buffered for that reply
|
||||
* @internal
|
||||
*/
|
||||
_onResponseTooLarge() {
|
||||
this._remainder = '';
|
||||
this._responseQueue = [];
|
||||
this._responsePartial = false;
|
||||
this._onError(new Error('Server response exceeds maximum allowed size'), 'EPROTOCOL', false, 'CONN');
|
||||
}
|
||||
/**
|
||||
* 'error' listener for the socket
|
||||
*
|
||||
@@ -838,6 +881,7 @@ class SMTPConnection extends EventEmitter {
|
||||
// part of the secured EHLO capabilities). STARTTLS response injection.
|
||||
this._remainder = '';
|
||||
this._responseQueue = [];
|
||||
this._responsePartial = false;
|
||||
// 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
|
||||
// we can safely keep 'error', 'end', 'close' etc. events
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nodemailer",
|
||||
"version": "10.0.8",
|
||||
"version": "10.0.10",
|
||||
"description": "Easy as cake e-mail sending from your Node.js applications",
|
||||
"type": "module",
|
||||
"main": "./dist/cjs/nodemailer.js",
|
||||
@@ -147,24 +147,24 @@
|
||||
},
|
||||
"homepage": "https://nodemailer.com/",
|
||||
"devDependencies": {
|
||||
"@aws-sdk/client-sesv2": "3.1124.0",
|
||||
"@aws-sdk/client-sesv2": "3.1131.0",
|
||||
"@types/node": "20.19.43",
|
||||
"bunyan": "1.8.15",
|
||||
"c8": "12.0.0",
|
||||
"eslint": "10.9.1",
|
||||
"eslint": "10.10.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "17.12.0",
|
||||
"libbase64": "1.3.0",
|
||||
"libmime": "5.4.3",
|
||||
"libqp": "2.1.1",
|
||||
"mailauth": "5.0.2",
|
||||
"mailauth": "5.0.3",
|
||||
"prettier": "3.9.6",
|
||||
"proxy": "1.0.2",
|
||||
"proxy-test-server": "1.0.0",
|
||||
"smtp-server": "3.19.7",
|
||||
"smtp-server": "3.19.11",
|
||||
"tsx": "4.23.13",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.69.0"
|
||||
"typescript-eslint": "8.70.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
|
||||
Reference in New Issue
Block a user