mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-09-29 04:20:39 +07:00
node_modules: update (#325)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
+3
-3
@@ -94,9 +94,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nodemailer": {
|
"node_modules/nodemailer": {
|
||||||
"version": "10.0.8",
|
"version": "10.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.0.10.tgz",
|
||||||
"integrity": "sha512-uCZ0AtFDI46sHwJ6mhwNuIoA/KiTC0ZCzX7g01941+H9VMIXEiZjtgjQkzoBRTeQba0cIrKfwk31q99PfcIMRw==",
|
"integrity": "sha512-He9XskOFms62SyAKkLu8CcGcYHAo+BSHSsORI8s4PJH8qZgZY4L4lDfvyN0twvmbpyG+eGrxpyBlskj9d9rJ8A==",
|
||||||
"license": "MIT-0",
|
"license": "MIT-0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
|||||||
+17
@@ -1,5 +1,22 @@
|
|||||||
# CHANGELOG
|
# 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)
|
## [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) {
|
function _isBoundary(text, at) {
|
||||||
return _isWordCode(text.charCodeAt(at - 1)) !== _isWordCode(text.charCodeAt(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.
|
* 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))) {
|
while (runEnd < len && !_isSpaceCode(text.charCodeAt(runEnd))) {
|
||||||
runEnd++;
|
runEnd++;
|
||||||
}
|
}
|
||||||
let at = text.indexOf('@', runStart);
|
let at = _indexOfAt(text, runStart, runEnd);
|
||||||
if (at >= 0 && at < runEnd) {
|
if (at >= 0) {
|
||||||
let lastBoundary = -1;
|
let lastBoundary = -1;
|
||||||
for (let k = runEnd; k > runStart; k--) {
|
for (let k = runEnd; k > runStart; k--) {
|
||||||
if (_isBoundary(text, k)) {
|
if (_isBoundary(text, k)) {
|
||||||
@@ -128,7 +151,7 @@ function _looseAddressStart(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let atomStart = runStart;
|
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,
|
// '[^@\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
|
// and the boundary that ends the match has to sit past both
|
||||||
if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
|
if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
|
||||||
@@ -148,7 +171,7 @@ function _looseAddressStart(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
atomStart = at + 1;
|
atomStart = at + 1;
|
||||||
at = text.indexOf('@', atomStart);
|
at = _indexOfAt(text, atomStart, runEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pos = runEnd;
|
pos = runEnd;
|
||||||
@@ -275,6 +298,19 @@ function _handleAddress(tokens, depth) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (token.value) {
|
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') {
|
if (state === 'address') {
|
||||||
// Handle unquoted name that includes a "<".
|
// Handle unquoted name that includes a "<".
|
||||||
// Apple Mail truncates everything between an unexpected < and an address.
|
// Apple Mail truncates everything between an unexpected < and an address.
|
||||||
@@ -302,7 +338,7 @@ function _handleAddress(tokens, depth) {
|
|||||||
data[state].push(token.value);
|
data[state].push(token.value);
|
||||||
lastChars[state] = token.value.charAt(token.value.length - 1);
|
lastChars[state] = token.value.charAt(token.value.length - 1);
|
||||||
if (state === 'text') {
|
if (state === 'text') {
|
||||||
data.textWasQuoted.push(insideQuotes);
|
data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,6 +423,18 @@ function _handleAddress(tokens, depth) {
|
|||||||
// Join values with spaces
|
// Join values with spaces
|
||||||
data.text = data.text.join(' ');
|
data.text = data.text.join(' ');
|
||||||
data.address = data.address.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);
|
_recoverAddrSpec(data);
|
||||||
const address = {
|
const address = {
|
||||||
address: data.address || data.text || '',
|
address: data.address || data.text || '',
|
||||||
@@ -400,9 +448,6 @@ function _handleAddress(tokens, depth) {
|
|||||||
address.address = '';
|
address.address = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (addressFromQuotedText && address.address) {
|
|
||||||
address.address = _quoteLocalPart(address.address);
|
|
||||||
}
|
|
||||||
addresses.push(address);
|
addresses.push(address);
|
||||||
}
|
}
|
||||||
return addresses;
|
return addresses;
|
||||||
|
|||||||
+12
-5
@@ -126,11 +126,18 @@ class MessageParser extends node_stream_1.Transform {
|
|||||||
// signature covers exactly the bytes the receiving side canonicalizes
|
// 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
|
// 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
|
// same whitespace the relaxed canonicalization in sign.ts works with
|
||||||
const lines = (this.rawHeaders || Buffer.alloc(0)).toString('binary').split(/\r?\n/);
|
const rawLines = (this.rawHeaders || Buffer.alloc(0)).toString('binary').split(/\r?\n/);
|
||||||
for (let i = lines.length - 1; i > 0; i--) {
|
// Unfold in a single forward pass and only ever test a freshly split line for the
|
||||||
if (/^[ \t]/.test(lines[i])) {
|
// continuation prefix. Testing an already merged line instead would rescan a string
|
||||||
lines[i - 1] += '\n' + lines[i];
|
// that grows with every continuation line, so a header folded into many continuation
|
||||||
lines.splice(i, 1);
|
// 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
|
return lines
|
||||||
|
|||||||
+3
-1
@@ -151,8 +151,10 @@ class MailComposer {
|
|||||||
data.filename = attachment.filename;
|
data.filename = attachment.filename;
|
||||||
}
|
}
|
||||||
else if (!isMessageNode && attachment.filename !== false) {
|
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 =
|
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) {
|
if (data.filename.indexOf('.') < 0) {
|
||||||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -107,8 +107,9 @@ class MailMessage {
|
|||||||
if (this.data.attachments && this.data.attachments.length) {
|
if (this.data.attachments && this.data.attachments.length) {
|
||||||
this.data.attachments.forEach((attachment, i) => {
|
this.data.attachments.forEach((attachment, i) => {
|
||||||
if (!attachment.filename) {
|
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.filename =
|
||||||
(attachment.path || attachment.href || '').split('/').pop().split('?').shift() ||
|
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||||
'attachment-' + (i + 1);
|
'attachment-' + (i + 1);
|
||||||
if (attachment.filename.indexOf('.') < 0) {
|
if (attachment.filename.indexOf('.') < 0) {
|
||||||
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
export declare const name = "nodemailer";
|
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/";
|
export declare const homepage = "https://nodemailer.com/";
|
||||||
|
|||||||
+1
-1
@@ -3,5 +3,5 @@
|
|||||||
Object.defineProperty(exports, "__esModule", { value: true });
|
Object.defineProperty(exports, "__esModule", { value: true });
|
||||||
exports.homepage = exports.version = exports.name = void 0;
|
exports.homepage = exports.version = exports.name = void 0;
|
||||||
exports.name = 'nodemailer';
|
exports.name = 'nodemailer';
|
||||||
exports.version = '10.0.8';
|
exports.version = '10.0.10';
|
||||||
exports.homepage = 'https://nodemailer.com/';
|
exports.homepage = 'https://nodemailer.com/';
|
||||||
|
|||||||
+3
@@ -41,6 +41,8 @@ export interface SMTPConnectionOptions {
|
|||||||
greetingTimeout?: number | undefined;
|
greetingTimeout?: number | undefined;
|
||||||
/** Time of inactivity in ms until the connection is closed, defaults to 10 minutes */
|
/** Time of inactivity in ms until the connection is closed, defaults to 10 minutes */
|
||||||
socketTimeout?: number | undefined;
|
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 */
|
/** Time to wait in ms for the DNS requests to be resolved, defaults to 30 seconds */
|
||||||
dnsTimeout?: number | undefined;
|
dnsTimeout?: number | undefined;
|
||||||
/** Use LMTP instead of SMTP */
|
/** 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)
|
* * **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)
|
* * **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)
|
* * **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)
|
* * **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
|
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
||||||
* * **logger** - bunyan compatible logger interface
|
* * **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 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 DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
|
||||||
const TEARDOWN_NOOP = () => { }; // reusable no-op handler for absorbing errors during socket teardown
|
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
|
* Re-interpret a server response stored in fake 8-bit byte-container form
|
||||||
* (the result of chunk.toString('binary') in _onData) as UTF-8.
|
* (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
|
* 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
|
* 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.
|
* 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
|
* The last line is read with lastIndexOf rather than split() because a queue entry may hold
|
||||||
* with every chunk appended to it and only its final line matters.
|
* a whole multiline reply and only its final line matters.
|
||||||
*/
|
*/
|
||||||
function isPartialResponse(str) {
|
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
|
* 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)
|
* * **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)
|
* * **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)
|
* * **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)
|
* * **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
|
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
||||||
* * **logger** - bunyan compatible logger interface
|
* * **logger** - bunyan compatible logger interface
|
||||||
@@ -146,6 +158,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|||||||
this.secure = !!this.secureConnection;
|
this.secure = !!this.secureConnection;
|
||||||
this._remainder = '';
|
this._remainder = '';
|
||||||
this._responseQueue = [];
|
this._responseQueue = [];
|
||||||
|
this._responsePartial = false;
|
||||||
this.lastServerResponse = false;
|
this.lastServerResponse = false;
|
||||||
this._socket = false;
|
this._socket = false;
|
||||||
this._supportedAuth = [];
|
this._supportedAuth = [];
|
||||||
@@ -714,28 +727,58 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|||||||
if (this._destroyed || !chunk || !chunk.length) {
|
if (this._destroyed || !chunk || !chunk.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let data = chunk.toString('binary');
|
const maxResponseSize = this.options.maxResponseSize || MAX_RESPONSE_SIZE;
|
||||||
let lines = (this._remainder + data).split(/\r?\n/);
|
const data = chunk.toString('binary');
|
||||||
let lastline;
|
// 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();
|
this._remainder = lines.pop();
|
||||||
for (let i = 0, len = lines.length; i < len; i++) {
|
for (let i = 0, len = lines.length; i < len; i++) {
|
||||||
if (this._responseQueue.length) {
|
if (this._responsePartial) {
|
||||||
lastline = this._responseQueue[this._responseQueue.length - 1];
|
this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
|
||||||
if (isPartialResponse(lastline)) {
|
}
|
||||||
this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
|
else {
|
||||||
continue;
|
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) {
|
if (this._remainder.length > maxResponseSize) {
|
||||||
lastline = this._responseQueue[this._responseQueue.length - 1];
|
return this._onResponseTooLarge();
|
||||||
if (isPartialResponse(lastline)) {
|
}
|
||||||
return;
|
if (this._responsePartial) {
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
this._processResponse();
|
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
|
* '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.
|
// part of the secured EHLO capabilities). STARTTLS response injection.
|
||||||
this._remainder = '';
|
this._remainder = '';
|
||||||
this._responseQueue = [];
|
this._responseQueue = [];
|
||||||
|
this._responsePartial = false;
|
||||||
// do not remove all listeners or it breaks node v0.10 as there's
|
// 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
|
// apparently a 'finish' event set that would be cleared as well
|
||||||
// we can safely keep 'error', 'end', 'close' etc. events
|
// we can safely keep 'error', 'end', 'close' etc. events
|
||||||
|
|||||||
+53
-8
@@ -85,6 +85,29 @@ function _isWordCode(code) {
|
|||||||
function _isBoundary(text, at) {
|
function _isBoundary(text, at) {
|
||||||
return _isWordCode(text.charCodeAt(at - 1)) !== _isWordCode(text.charCodeAt(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.
|
* 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))) {
|
while (runEnd < len && !_isSpaceCode(text.charCodeAt(runEnd))) {
|
||||||
runEnd++;
|
runEnd++;
|
||||||
}
|
}
|
||||||
let at = text.indexOf('@', runStart);
|
let at = _indexOfAt(text, runStart, runEnd);
|
||||||
if (at >= 0 && at < runEnd) {
|
if (at >= 0) {
|
||||||
let lastBoundary = -1;
|
let lastBoundary = -1;
|
||||||
for (let k = runEnd; k > runStart; k--) {
|
for (let k = runEnd; k > runStart; k--) {
|
||||||
if (_isBoundary(text, k)) {
|
if (_isBoundary(text, k)) {
|
||||||
@@ -125,7 +148,7 @@ function _looseAddressStart(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let atomStart = runStart;
|
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,
|
// '[^@\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
|
// and the boundary that ends the match has to sit past both
|
||||||
if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
|
if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
|
||||||
@@ -145,7 +168,7 @@ function _looseAddressStart(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
atomStart = at + 1;
|
atomStart = at + 1;
|
||||||
at = text.indexOf('@', atomStart);
|
at = _indexOfAt(text, atomStart, runEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pos = runEnd;
|
pos = runEnd;
|
||||||
@@ -272,6 +295,19 @@ function _handleAddress(tokens, depth) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (token.value) {
|
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') {
|
if (state === 'address') {
|
||||||
// Handle unquoted name that includes a "<".
|
// Handle unquoted name that includes a "<".
|
||||||
// Apple Mail truncates everything between an unexpected < and an address.
|
// Apple Mail truncates everything between an unexpected < and an address.
|
||||||
@@ -299,7 +335,7 @@ function _handleAddress(tokens, depth) {
|
|||||||
data[state].push(token.value);
|
data[state].push(token.value);
|
||||||
lastChars[state] = token.value.charAt(token.value.length - 1);
|
lastChars[state] = token.value.charAt(token.value.length - 1);
|
||||||
if (state === 'text') {
|
if (state === 'text') {
|
||||||
data.textWasQuoted.push(insideQuotes);
|
data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,6 +420,18 @@ function _handleAddress(tokens, depth) {
|
|||||||
// Join values with spaces
|
// Join values with spaces
|
||||||
data.text = data.text.join(' ');
|
data.text = data.text.join(' ');
|
||||||
data.address = data.address.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);
|
_recoverAddrSpec(data);
|
||||||
const address = {
|
const address = {
|
||||||
address: data.address || data.text || '',
|
address: data.address || data.text || '',
|
||||||
@@ -397,9 +445,6 @@ function _handleAddress(tokens, depth) {
|
|||||||
address.address = '';
|
address.address = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (addressFromQuotedText && address.address) {
|
|
||||||
address.address = _quoteLocalPart(address.address);
|
|
||||||
}
|
|
||||||
addresses.push(address);
|
addresses.push(address);
|
||||||
}
|
}
|
||||||
return addresses;
|
return addresses;
|
||||||
|
|||||||
+12
-5
@@ -124,11 +124,18 @@ export default class MessageParser extends Transform {
|
|||||||
// signature covers exactly the bytes the receiving side canonicalizes
|
// 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
|
// 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
|
// same whitespace the relaxed canonicalization in sign.ts works with
|
||||||
const lines = (this.rawHeaders || Buffer.alloc(0)).toString('binary').split(/\r?\n/);
|
const rawLines = (this.rawHeaders || Buffer.alloc(0)).toString('binary').split(/\r?\n/);
|
||||||
for (let i = lines.length - 1; i > 0; i--) {
|
// Unfold in a single forward pass and only ever test a freshly split line for the
|
||||||
if (/^[ \t]/.test(lines[i])) {
|
// continuation prefix. Testing an already merged line instead would rescan a string
|
||||||
lines[i - 1] += '\n' + lines[i];
|
// that grows with every continuation line, so a header folded into many continuation
|
||||||
lines.splice(i, 1);
|
// 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
|
return lines
|
||||||
|
|||||||
+3
-1
@@ -113,8 +113,10 @@ class MailComposer {
|
|||||||
data.filename = attachment.filename;
|
data.filename = attachment.filename;
|
||||||
}
|
}
|
||||||
else if (!isMessageNode && attachment.filename !== false) {
|
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 =
|
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) {
|
if (data.filename.indexOf('.') < 0) {
|
||||||
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
data.filename += '.' + mimeFuncs.detectExtension(data.contentType);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -69,8 +69,9 @@ export default class MailMessage {
|
|||||||
if (this.data.attachments && this.data.attachments.length) {
|
if (this.data.attachments && this.data.attachments.length) {
|
||||||
this.data.attachments.forEach((attachment, i) => {
|
this.data.attachments.forEach((attachment, i) => {
|
||||||
if (!attachment.filename) {
|
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.filename =
|
||||||
(attachment.path || attachment.href || '').split('/').pop().split('?').shift() ||
|
(attachment.path || attachment.href || '').split(/[/\\]/).pop().split('?').shift() ||
|
||||||
'attachment-' + (i + 1);
|
'attachment-' + (i + 1);
|
||||||
if (attachment.filename.indexOf('.') < 0) {
|
if (attachment.filename.indexOf('.') < 0) {
|
||||||
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
export declare const name = "nodemailer";
|
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/";
|
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.
|
// Generated by scripts/build.js from package.json. Do not edit by hand.
|
||||||
export const name = 'nodemailer';
|
export const name = 'nodemailer';
|
||||||
export const version = '10.0.8';
|
export const version = '10.0.10';
|
||||||
export const homepage = 'https://nodemailer.com/';
|
export const homepage = 'https://nodemailer.com/';
|
||||||
|
|||||||
+3
@@ -41,6 +41,8 @@ export interface SMTPConnectionOptions {
|
|||||||
greetingTimeout?: number | undefined;
|
greetingTimeout?: number | undefined;
|
||||||
/** Time of inactivity in ms until the connection is closed, defaults to 10 minutes */
|
/** Time of inactivity in ms until the connection is closed, defaults to 10 minutes */
|
||||||
socketTimeout?: number | undefined;
|
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 */
|
/** Time to wait in ms for the DNS requests to be resolved, defaults to 30 seconds */
|
||||||
dnsTimeout?: number | undefined;
|
dnsTimeout?: number | undefined;
|
||||||
/** Use LMTP instead of SMTP */
|
/** 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)
|
* * **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)
|
* * **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)
|
* * **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)
|
* * **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
|
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
||||||
* * **logger** - bunyan compatible logger interface
|
* * **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 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 DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
|
||||||
const TEARDOWN_NOOP = () => { }; // reusable no-op handler for absorbing errors during socket teardown
|
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
|
* Re-interpret a server response stored in fake 8-bit byte-container form
|
||||||
* (the result of chunk.toString('binary') in _onData) as UTF-8.
|
* (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
|
* 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
|
* 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.
|
* 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
|
* The last line is read with lastIndexOf rather than split() because a queue entry may hold
|
||||||
* with every chunk appended to it and only its final line matters.
|
* a whole multiline reply and only its final line matters.
|
||||||
*/
|
*/
|
||||||
function isPartialResponse(str) {
|
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
|
* 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)
|
* * **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)
|
* * **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)
|
* * **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)
|
* * **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
|
* * **lmtp** - if true, uses LMTP instead of SMTP protocol
|
||||||
* * **logger** - bunyan compatible logger interface
|
* * **logger** - bunyan compatible logger interface
|
||||||
@@ -108,6 +120,7 @@ class SMTPConnection extends EventEmitter {
|
|||||||
this.secure = !!this.secureConnection;
|
this.secure = !!this.secureConnection;
|
||||||
this._remainder = '';
|
this._remainder = '';
|
||||||
this._responseQueue = [];
|
this._responseQueue = [];
|
||||||
|
this._responsePartial = false;
|
||||||
this.lastServerResponse = false;
|
this.lastServerResponse = false;
|
||||||
this._socket = false;
|
this._socket = false;
|
||||||
this._supportedAuth = [];
|
this._supportedAuth = [];
|
||||||
@@ -676,28 +689,58 @@ class SMTPConnection extends EventEmitter {
|
|||||||
if (this._destroyed || !chunk || !chunk.length) {
|
if (this._destroyed || !chunk || !chunk.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let data = chunk.toString('binary');
|
const maxResponseSize = this.options.maxResponseSize || MAX_RESPONSE_SIZE;
|
||||||
let lines = (this._remainder + data).split(/\r?\n/);
|
const data = chunk.toString('binary');
|
||||||
let lastline;
|
// 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();
|
this._remainder = lines.pop();
|
||||||
for (let i = 0, len = lines.length; i < len; i++) {
|
for (let i = 0, len = lines.length; i < len; i++) {
|
||||||
if (this._responseQueue.length) {
|
if (this._responsePartial) {
|
||||||
lastline = this._responseQueue[this._responseQueue.length - 1];
|
this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
|
||||||
if (isPartialResponse(lastline)) {
|
}
|
||||||
this._responseQueue[this._responseQueue.length - 1] += '\n' + lines[i];
|
else {
|
||||||
continue;
|
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) {
|
if (this._remainder.length > maxResponseSize) {
|
||||||
lastline = this._responseQueue[this._responseQueue.length - 1];
|
return this._onResponseTooLarge();
|
||||||
if (isPartialResponse(lastline)) {
|
}
|
||||||
return;
|
if (this._responsePartial) {
|
||||||
}
|
return;
|
||||||
}
|
}
|
||||||
this._processResponse();
|
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
|
* 'error' listener for the socket
|
||||||
*
|
*
|
||||||
@@ -838,6 +881,7 @@ class SMTPConnection extends EventEmitter {
|
|||||||
// part of the secured EHLO capabilities). STARTTLS response injection.
|
// part of the secured EHLO capabilities). STARTTLS response injection.
|
||||||
this._remainder = '';
|
this._remainder = '';
|
||||||
this._responseQueue = [];
|
this._responseQueue = [];
|
||||||
|
this._responsePartial = false;
|
||||||
// do not remove all listeners or it breaks node v0.10 as there's
|
// 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
|
// apparently a 'finish' event set that would be cleared as well
|
||||||
// we can safely keep 'error', 'end', 'close' etc. events
|
// we can safely keep 'error', 'end', 'close' etc. events
|
||||||
|
|||||||
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "nodemailer",
|
"name": "nodemailer",
|
||||||
"version": "10.0.8",
|
"version": "10.0.10",
|
||||||
"description": "Easy as cake e-mail sending from your Node.js applications",
|
"description": "Easy as cake e-mail sending from your Node.js applications",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/cjs/nodemailer.js",
|
"main": "./dist/cjs/nodemailer.js",
|
||||||
@@ -147,24 +147,24 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://nodemailer.com/",
|
"homepage": "https://nodemailer.com/",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@aws-sdk/client-sesv2": "3.1124.0",
|
"@aws-sdk/client-sesv2": "3.1131.0",
|
||||||
"@types/node": "20.19.43",
|
"@types/node": "20.19.43",
|
||||||
"bunyan": "1.8.15",
|
"bunyan": "1.8.15",
|
||||||
"c8": "12.0.0",
|
"c8": "12.0.0",
|
||||||
"eslint": "10.9.1",
|
"eslint": "10.10.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"globals": "17.12.0",
|
"globals": "17.12.0",
|
||||||
"libbase64": "1.3.0",
|
"libbase64": "1.3.0",
|
||||||
"libmime": "5.4.3",
|
"libmime": "5.4.3",
|
||||||
"libqp": "2.1.1",
|
"libqp": "2.1.1",
|
||||||
"mailauth": "5.0.2",
|
"mailauth": "5.0.3",
|
||||||
"prettier": "3.9.6",
|
"prettier": "3.9.6",
|
||||||
"proxy": "1.0.2",
|
"proxy": "1.0.2",
|
||||||
"proxy-test-server": "1.0.0",
|
"proxy-test-server": "1.0.0",
|
||||||
"smtp-server": "3.19.7",
|
"smtp-server": "3.19.11",
|
||||||
"tsx": "4.23.13",
|
"tsx": "4.23.13",
|
||||||
"typescript": "6.0.3",
|
"typescript": "6.0.3",
|
||||||
"typescript-eslint": "8.69.0"
|
"typescript-eslint": "8.70.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user