node_modules: update (#349)

Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
Dawid Dziurlaanddawidd6 authored and GitHub committed 2026-10-18 15:39:30 +02:00
1 parent 9d0c26ec8b
commit 0a1f5db16c
36 files changed
+1934 -648

No files matched your search

+699 -271
View File
File diff suppressed because it is too large. Load diff
+3 -2
View File
@@ -133,8 +133,9 @@ class DKIMSigner {
}
createWriteCache() {
this.output.usingCache = true;
// pipe remainings to cache file
this.cache = node_fs_1.default.createWriteStream(this.cachePath);
// pipe remainings to cache file. It holds the message body and cacheDir is often a
// shared directory such as /tmp, so nobody but the owner gets to read it
this.cache = node_fs_1.default.createWriteStream(this.cachePath, { mode: 0o600 });
this.cache.once('error', err => {
// drain input
this.relaxedBody.unpipe(this.cache);
+5
View File
@@ -27,6 +27,8 @@ export interface FetchOptions {
} | undefined;
/** Socket inactivity timeout in milliseconds, defaults to 60000, 0 disables it */
timeout?: number | undefined;
/** Time limit for the whole request in milliseconds, redirects included, defaults to 120000, 0 disables it */
maxTime?: number | undefined;
/** Maximum size of the (decoded) response body in bytes, defaults to 64 MB, Infinity disables the limit */
maxBytes?: number | undefined;
/** Maximum number of redirects to follow (default 5) */
@@ -37,6 +39,8 @@ export interface FetchOptions {
redirects?: number | undefined;
/** Response stream shared across redirects, set by nmfetch itself */
fetchRes?: FetchResponse | undefined;
/** Timestamp the whole request has to be done by, shared across redirects, set by nmfetch itself */
deadline?: number | undefined;
}
/**
* The stream nmfetch returns. The response body is piped into it, the status code and
@@ -50,6 +54,7 @@ declare function nmfetch(url: string, options?: FetchOptions): FetchResponse;
declare namespace nmfetch {
var Cookies: typeof import("./cookies.js").default;
var DEFAULT_TIMEOUT: number;
var DEFAULT_MAX_TIME: number;
}
type CookiesJar = Cookies;
/**
+30
View File
@@ -49,6 +49,10 @@ const objects_js_1 = require("../shared/objects.js");
const MAX_REDIRECTS = 5;
// a stalled server would otherwise hold the request, and whatever waits on it, open forever
const DEFAULT_TIMEOUT = 60 * 1000;
// the inactivity timeout above is reset by every byte, so a server dripping one within each
// window could still hold the request, and the message being built from it, open for as
// long as it liked. The whole request, redirects included, has to be done within this
const DEFAULT_MAX_TIME = 2 * 60 * 1000;
// the body is usually buffered in memory by the caller, so an unbounded download is refused
const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
// Only genuine TLS settings are taken from options.tls. That object reaches us straight
@@ -126,6 +130,12 @@ function nmfetch(url, options) {
options.cookies = options.cookies || new cookies_js_1.default();
options.redirects = options.redirects || 0;
options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
if (!options.deadline) {
options.maxTime = typeof options.maxTime === 'number' && options.maxTime >= 0 ? options.maxTime : DEFAULT_MAX_TIME;
// the one deadline is shared with the requests the redirects lead to, every hop
// gets the time that is left rather than starting over
options.deadline = options.maxTime ? Date.now() + options.maxTime : 0;
}
const fetchRes = options.fetchRes;
const parsed = parseFetchUrl(url);
if (!parsed) {
@@ -271,12 +281,22 @@ function nmfetch(url, options) {
});
return fetchRes;
}
// the whole request has to be done by options.deadline, see maxTime. The timer is
// released on every way out of the request, so it never outlives it
let deadlineTimer;
const clearDeadline = () => {
if (deadlineTimer) {
clearTimeout(deadlineTimer);
deadlineTimer = undefined;
}
};
// reports the first failure of this request on fetchRes and releases the request
const fail = (err, sourceUrl = url) => {
if (finished) {
return;
}
finished = true;
clearDeadline();
setFetchCode(err);
err.sourceUrl = sourceUrl;
fetchRes.emit('error', err);
@@ -286,10 +306,16 @@ function nmfetch(url, options) {
if (timeout) {
req.setTimeout(timeout, () => fail(new Error('Request Timeout')));
}
if (options.deadline) {
deadlineTimer = setTimeout(() => fail(new Error('Request exceeded the allowed ' + options.maxTime + ' ms')), Math.max(options.deadline - Date.now(), 1));
// a pending deadline is no reason to keep the process alive
deadlineTimer.unref();
}
req.on('error', (err) => fail(err));
// a consumer that destroys the response stream before it ended does not want the rest of
// the body, release the request now instead of when the timeout fires
fetchRes.once('close', () => {
clearDeadline();
if (finished || fetchRes.readableEnded) {
return;
}
@@ -358,6 +384,7 @@ function nmfetch(url, options) {
// this request is done with, release it so its socket and timeout do not
// outlive it and report a late error on the shared fetchRes
finished = true;
clearDeadline();
res.resume();
req.abort();
return nmfetch(redirectUrl, options);
@@ -368,6 +395,8 @@ function nmfetch(url, options) {
return fail(new Error('Invalid status code ' + res.statusCode));
}
res.on('error', (err) => fail(err));
// the response is in, whatever is left of it is in memory and no longer on the network
res.once('end', () => clearDeadline());
const maxBytes = typeof options.maxBytes === 'number' && options.maxBytes > 0 ? options.maxBytes : DEFAULT_MAX_BYTES;
const source = inflate || res;
let received = 0;
@@ -405,6 +434,7 @@ function nmfetch(url, options) {
}
nmfetch.Cookies = cookies_js_1.default;
nmfetch.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
nmfetch.DEFAULT_MAX_TIME = DEFAULT_MAX_TIME;
exports.default = nmfetch;
module.exports = exports.default;
Object.defineProperty(module.exports, 'default', { value: exports.default, enumerable: false, writable: true, configurable: true });
+4 -1
View File
@@ -398,9 +398,12 @@ class Mail extends node_events_1.EventEmitter {
return callback(err);
}
let cidCounter = 0;
// the media type is a bounded token: a quote, whitespace, '>' or '<' ends the
// attribute value and with it the type, so one img tag never scans ahead to a
// ';' that belongs to a later tag, which was quadratic
html = (html || '')
.toString()
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;"'<>\s]{1,255});[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
const cid = node_crypto_1.default.randomBytes(10).toString('hex') + '@localhost';
if (!mail.data.attachments) {
mail.data.attachments = [];
+5 -1
View File
@@ -23,7 +23,11 @@ export interface SendMailOptions extends MailComposerOptions {
dsn?: SMTPEnvelopeDsn | undefined;
/** SMTP transports: RFC 8689, send the REQUIRETLS parameter with MAIL FROM */
requireTLSExtensionEnabled?: boolean | undefined;
/** SMTP transports: per-message authentication settings, used instead of the transport level auth */
/**
* SMTP transports: per-message authentication settings, used instead of the transport level auth.
* Only the credential fields are taken from it (type, user, pass, accessToken, refreshToken, expires),
* the token endpoint and client settings stay those of the transport
*/
auth?: SMTPTransportAuthOptions | undefined;
/** SES transport: extra SendEmailCommand parameters merged into the API call */
ses?: SESSendEmailRequest | undefined;
+4 -1
View File
@@ -183,7 +183,10 @@ class MailMessage {
const values = [data.html, data.text, data.watchHtml, data.amp, data.raw, data.icalEvent];
for (const key of ['attachments', 'alternatives']) {
if (Array.isArray(data[key])) {
values.push(...data[key]);
// one at a time, spreading a long enough list into the arguments throws
for (const entry of data[key]) {
values.push(entry);
}
}
}
for (const value of values) {
+25 -13
View File
@@ -186,26 +186,38 @@ function encodeWord(data, mimeWordEncoding, maxLength) {
*/
function encodeWords(value, mimeWordEncoding, maxLength, encodeAll) {
maxLength = maxLength || 0;
// find first word with a non-printable ascii or special symbol in it
const firstMatch = value.match(/(?:^|\s)([^\s]*["\u0080-\uFFFF])/);
if (!firstMatch) {
// find the first and the last character that needs encoding, a quote or anything outside
// printable ascii. A scan rather than a regex: matching the last such word with a
// backtracking pattern took quadratic time on a long word of non-ascii characters
let firstSpecial = -1;
let lastSpecial = -1;
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
if (code === 0x22 || code >= 0x80) {
if (firstSpecial < 0) {
firstSpecial = i;
}
lastSpecial = i;
}
}
if (firstSpecial < 0) {
return value;
}
if (encodeAll) {
// if it is requested to encode everything or the string contains something that resebles encoded word, then encode everything
return encodeWord(value, mimeWordEncoding, maxLength);
}
// find the last word with a non-printable ascii in it
const lastMatch = value.match(/(["\u0080-\uFFFF][^\s]*)[^"\u0080-\uFFFF]*$/);
if (!lastMatch) {
// should not happen
return value;
// encode from the start of the word holding the first such character to the end of the
// word holding the last one, so the encoded words stay separated from the plain text by
// the whitespace that was already there
let startIndex = firstSpecial;
while (startIndex > 0 && !/\s/.test(value.charAt(startIndex - 1))) {
startIndex--;
}
let endIndex = lastSpecial + 1;
while (endIndex < value.length && !/\s/.test(value.charAt(endIndex))) {
endIndex++;
}
const startIndex = firstMatch.index +
(firstMatch[0].match(/[^\s]/) || {
index: 0
}).index;
const endIndex = lastMatch.index + (lastMatch[1] || '').length;
return ((startIndex ? value.substr(0, startIndex) : '') +
encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
(endIndex < value.length ? value.substr(endIndex) : ''));
+97 -22
View File
@@ -64,6 +64,35 @@ const QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"$/;
// else goes into angle brackets so that the header can not be read as more addresses than
// the envelope carries
const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
/**
* A domain literal that ends an address, see _normalizeAddress
*/
const DOMAIN_LITERAL_TAIL = /@(\[[^[\]\\]*\])$/;
/**
* Removes the whitespace inside every <...> of a References value. A "<" with no ">" after
* it is left as it is. An index scan, the regex /<[^>]*>/g rescans the rest of the value
* from every such "<"
*
* @param value References header value
* @returns The value with the msg-ids closed up
*/
function stripWhitespaceInAngleBrackets(value) {
let result = '';
let pos = 0;
for (;;) {
const open = value.indexOf('<', pos);
if (open < 0) {
break;
}
const close = value.indexOf('>', open + 1);
if (close < 0) {
break;
}
result += value.slice(pos, open) + value.slice(open, close + 1).replace(/\s/g, '');
pos = close + 1;
}
return result + value.slice(pos);
}
// domainToASCII and domainToUnicode are WHATWG host parsers rather than plain IDNA
// mappers, so they do more than map: they cut the host at '/', '\\', '?' and '#', drop C0
// controls, and percent-decode. Handing them 'evil.example/mail.corp.example' returns the
@@ -71,6 +100,13 @@ const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
// unroutable garbage into mail for a domain the sender never named. None of these
// characters are legal in a domain, so keep them away from the mapper.
const URL_PARSER_UNSAFE = /[/\\?#%\x00-\x20\x7F]/;
// RFC 1035 bounds a label at 63 octets and a domain at 253. An A-label is never shorter than
// the code points it encodes, so anything past these bounds could not have become a valid
// domain whichever way it is encoded. The bundled codec is super-linear in the length of a
// label, a 60 KB label of distinct code points took seconds to encode, several times per
// address, so it is only handed what could be a domain
const MAX_DOMAIN_LENGTH = 253;
const MAX_LABEL_LENGTH = 63;
// pipeline() needs a callback to not throw, the errors it sees reach the last stream anyway
const PIPELINE_NOOP = () => false;
// The error a node streaming into an output that was destroyed stops with
@@ -79,6 +115,27 @@ function abortedError() {
err.code = errors.ESTREAM;
return err;
}
/**
* Checks a domain against the DNS length limits, counted in code points per label and in
* total. The label separators are the ones the bundled codec splits on.
*
* @param domain Domain to check
* @return true if a label or the domain as a whole is too long to be a valid domain
*/
function exceedsDomainLimits(domain) {
let total = 0;
let label = 0;
for (const char of domain) {
total++;
if (char === '.' || char === '。' || char === '.' || char === '。') {
label = 0;
}
else if (++label > MAX_LABEL_LENGTH) {
return true;
}
}
return total > MAX_DOMAIN_LENGTH;
}
/**
* Encodes a domain the way browsers, the WHATWG URL Standard and DNS facing resolvers do,
* which is with UTS-46 mapping applied before the Punycode step.
@@ -110,6 +167,10 @@ function normalizeDomain(domain, toUnicode) {
return mapped;
}
}
if (exceedsDomainLimits(domain)) {
// too long to be a domain whichever way it is encoded, keep it as supplied
return domain;
}
return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
}
/**
@@ -614,6 +675,17 @@ class MimeNode {
/[\u0080-\uFFFF]/.test(this.content)) {
structured.params.charset = 'utf-8';
}
if (this.filename) {
// the name parameter is derived from the filename below, so a name that
// arrived inside the content type is dropped first (a plain parameter or
// rfc2231 continuation sections), otherwise the header carries two and a
// receiver that reads the first sees a name the filename does not match
for (const paramKey of Object.keys(structured.params)) {
if (/^name(\*|$)/i.test(paramKey)) {
delete structured.params[paramKey];
}
}
}
value = mimeFuncs.buildHeaderValue(structured);
if (this.filename) {
// add support for non-compliant clients like QQ webmail
@@ -1223,12 +1295,12 @@ class MimeNode {
_normalizeHeaderKey(key) {
key = (key || '')
.toString()
// no newlines in keys
.replace(/\r?\n|\r/g, ' ')
// a field name is printable ascii without the colon, so a control char or DEL
// can only be dropped, there is no quoting construct around a field name
.replace(/[\x00-\x1f\x7f]/g, '')
.trim()
// a field name is printable ascii without the colon, so a control char, DEL,
// whitespace or a colon can only be dropped, there is no quoting construct
// around a field name. A colon or a space left in place ends the name early on
// the wire: the key 'From: ceo@example.com (' with the value ')' was emitted as a
// second From header that neither setHeader() nor the Bcc filter could match
.replace(/[\s\x00-\x20\x7f:]/g, '')
.toLowerCase()
// use uppercase words, except MIME
.replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
@@ -1314,26 +1386,26 @@ class MimeNode {
}
return value;
// space separated list of values enclosed in <>
case 'References':
value = [].concat
.apply([], [].concat(value || '').map(elm => {
elm = (elm || '')
case 'References': {
const ids = [];
for (const entry of [].concat(value || '')) {
const elm = (entry || '')
.toString()
.replace(/\r?\n|\r/g, ' ')
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
.trim();
return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
}))
.map(elm => {
if (elm.charAt(0) !== '<') {
elm = '<' + elm;
for (let id of stripWhitespaceInAngleBrackets(elm).split(/\s+/)) {
if (id.charAt(0) !== '<') {
id = '<' + id;
}
if (id.charAt(id.length - 1) !== '>') {
id = id + '>';
}
ids.push(id);
}
if (elm.charAt(elm.length - 1) !== '>') {
elm = elm + '>';
}
return elm;
});
return value.join(' ').trim();
}
return ids.join(' ').trim();
}
case 'Date':
if (Object.prototype.toString.call(value) === '[object Date]') {
return value.toUTCString().replace(/GMT/, '+0000');
@@ -1412,7 +1484,10 @@ class MimeNode {
// callers use an empty value to detect a missing address
return address;
}
const lastAt = address.lastIndexOf('@');
// dtext includes '@', so a domain literal is split off at the '@' in front of it and
// not at the last one, which delivered 'a@[x@evil.com]' to evil.com
const literal = address.charAt(address.length - 1) === ']' ? DOMAIN_LITERAL_TAIL.exec(address) : null;
const lastAt = literal ? address.length - literal[1].length - 1 : address.lastIndexOf('@');
if (lastAt < 0) {
// Bare username, there is no domain to split off
return this._normalizeLocalPart(address);
+1 -1
View File
@@ -1,3 +1,3 @@
export declare const name = "nodemailer";
export declare const version = "10.1.1";
export declare const version = "10.1.6";
export declare const homepage = "https://nodemailer.com/";
+1 -1
View File
@@ -3,5 +3,5 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.homepage = exports.version = exports.name = void 0;
exports.name = 'nodemailer';
exports.version = '10.1.1';
exports.version = '10.1.6';
exports.homepage = 'https://nodemailer.com/';
+22 -5
View File
@@ -58,10 +58,27 @@ const punycode = __importStar(require("../punycode/index.js"));
// Matches a "scheme:" not followed by "//" (and with something after it), used
// to re-insert the authority separator the legacy parser did not require.
const SLASHLESS_AUTHORITY = /^([a-zA-Z][a-zA-Z0-9+.-]*:)(?!\/\/)([\s\S]+)$/;
// Leading and trailing C0 controls and spaces, which the WHATWG parser strips before it
// looks at the input. Stripped up front so that the slash-less form is recognized in a
// value read from a file with a trailing newline as well.
const SURROUNDING_WHITESPACE = /^[\x00-\x20]+|[\x00-\x20]+$/g;
/**
* Drops the leading and trailing C0 controls and spaces, which the WHATWG parser strips
* before it looks at the input. Stripped up front so that the slash-less form is recognized
* in a value read from a file with a trailing newline as well. An index scan, the regex
* /^[\x00-\x20]+|[\x00-\x20]+$/g retried a run of spaces inside the value from every
* position in it
*
* @param input URL string
* @returns The string without the surrounding controls and spaces
*/
function stripSurroundingWhitespace(input) {
let start = 0;
let end = input.length;
while (start < end && input.charCodeAt(start) <= 0x20) {
start++;
}
while (end > start && input.charCodeAt(end - 1) <= 0x20) {
end--;
}
return input.slice(start, end);
}
// Leading characters legacy url.parse() skips before it reads the scheme
const LEGACY_TRIM = /^[\x00-\x20\u00a0\ufeff]+/;
// The authority of a "scheme://authority/..." or a scheme-relative "//authority/..."
@@ -156,7 +173,7 @@ function normalizeHostname(raw, href) {
return mapped;
}
const parse = (input, parseQueryString) => {
input = (input || '').replace(SURROUNDING_WHITESPACE, '');
input = stripSurroundingWhitespace(input || '');
// Legacy url.parse() parses a "user:pass@host:port" authority that follows
// the scheme even without the "//" separator, for schemes outside its
// built-in slashed-protocol list (smtp:/smtps:/socks:/...). The WHATWG
+16 -1
View File
@@ -114,6 +114,21 @@ function isPartialResponse(str) {
function isPartialLine(line) {
return /^\d+-/.test(line);
}
/**
* The log form of a command a custom authentication handler sends. Such a command carries
* the credentials in whatever shape its mechanism uses, so only an AUTH verb with its
* mechanism name is kept, the way the built-in mechanisms hide their own secrets.
*
* @param cmd Command the handler sends
* @return What the transaction log gets instead
*/
function maskAuthCommand(cmd) {
const match = (cmd || '').toString().match(/^AUTH\s+[^\s]+/i);
if (!match) {
return '/* secret */';
}
return match[0].length === cmd.length ? cmd : match[0] + ' /* secret */';
}
/**
* Generates a SMTP connection object
*
@@ -601,7 +616,7 @@ class SMTPConnection extends node_events_1.EventEmitter {
}
done(null, data);
});
setImmediate(() => this._sendCommand(cmd));
setImmediate(() => this._sendCommand(cmd, maskAuthCommand(cmd)));
return promise;
};
const handlerResponse = handler({
+12 -1
View File
@@ -43,6 +43,13 @@ const shared = __importStar(require("../shared/index.js"));
const index_js_3 = __importDefault(require("../xoauth2/index.js"));
const errors = __importStar(require("../errors.js"));
const packageData = __importStar(require("../package-info.js"));
// The keys a message's `auth` may set on top of the transport's auth settings: the ones that
// say who logs in and with what. Everything else in the auth block (accessUrl, clientId,
// clientSecret, customHeaders, customParams, tls, privateKey, serviceClient and the rest)
// names the endpoint the credentials go to or is a secret of the application itself, and
// a message that could set `auth.accessUrl` and force a refresh would have the transport's
// client secret and refresh token posted to a URL of its choosing
const MESSAGE_AUTH_KEYS = ['type', 'user', 'pass', 'accessToken', 'refreshToken', 'expires'];
/**
* Creates a SMTP transport object for Nodemailer
*
@@ -102,7 +109,11 @@ class SMTPTransport extends node_events_1.EventEmitter {
}
return this.auth;
}
const authData = Object.assign({}, this.options.auth && typeof this.options.auth === 'object' ? this.options.auth : {}, typeof authOpts === 'object' ? authOpts : {});
const authData = Object.assign({}, this.options.auth && typeof this.options.auth === 'object' ? this.options.auth : {});
if (typeof authOpts === 'object') {
// see MESSAGE_AUTH_KEYS
shared.copyOwnKeys(authData, authOpts, key => !MESSAGE_AUTH_KEYS.includes(key));
}
if (Object.keys(authData).length === 0) {
return false;
}
+7
View File
@@ -363,6 +363,13 @@ exports.services = {
"port": 587,
"secure": false
},
"Oveyon": {
"description": "Oveyon",
"host": "smtp.oveyon.com",
"port": 587,
"secure": false,
"requireTLS": true
},
"Postmark": {
"description": "Postmark",
"aliases": [
+8 -2
View File
@@ -264,8 +264,14 @@ class XOAuth2 extends node_stream_1.Stream {
grant_type: 'refresh_token'
};
}
Object.assign(urlOptions, this.options.customParams);
Object.assign(loggedUrlOptions, this.options.customParams);
const customParams = this.options.customParams || {};
Object.assign(urlOptions, customParams);
// a custom param is as likely to be a client assertion or an API key as a tenant
// name, so the log gets the same glimpse of it that it gets of the client secret
for (const key of Object.keys(customParams)) {
const param = customParams[key];
loggedUrlOptions[key] = (param === undefined || param === null ? '' : String(param)).substr(0, 6) + '...';
}
this.logger.debug({
tnx: 'OAUTH2',
user: this.options.user,
+699 -271
View File
File diff suppressed because it is too large. Load diff
+3 -2
View File
@@ -128,8 +128,9 @@ class DKIMSigner {
}
createWriteCache() {
this.output.usingCache = true;
// pipe remainings to cache file
this.cache = fs.createWriteStream(this.cachePath);
// pipe remainings to cache file. It holds the message body and cacheDir is often a
// shared directory such as /tmp, so nobody but the owner gets to read it
this.cache = fs.createWriteStream(this.cachePath, { mode: 0o600 });
this.cache.once('error', err => {
// drain input
this.relaxedBody.unpipe(this.cache);
+5
View File
@@ -27,6 +27,8 @@ export interface FetchOptions {
} | undefined;
/** Socket inactivity timeout in milliseconds, defaults to 60000, 0 disables it */
timeout?: number | undefined;
/** Time limit for the whole request in milliseconds, redirects included, defaults to 120000, 0 disables it */
maxTime?: number | undefined;
/** Maximum size of the (decoded) response body in bytes, defaults to 64 MB, Infinity disables the limit */
maxBytes?: number | undefined;
/** Maximum number of redirects to follow (default 5) */
@@ -37,6 +39,8 @@ export interface FetchOptions {
redirects?: number | undefined;
/** Response stream shared across redirects, set by nmfetch itself */
fetchRes?: FetchResponse | undefined;
/** Timestamp the whole request has to be done by, shared across redirects, set by nmfetch itself */
deadline?: number | undefined;
}
/**
* The stream nmfetch returns. The response body is piped into it, the status code and
@@ -50,6 +54,7 @@ declare function nmfetch(url: string, options?: FetchOptions): FetchResponse;
declare namespace nmfetch {
var Cookies: typeof import("./cookies.js").default;
var DEFAULT_TIMEOUT: number;
var DEFAULT_MAX_TIME: number;
}
type CookiesJar = Cookies;
/**
+30
View File
@@ -11,6 +11,10 @@ import { isProtoKey } from '../shared/objects.js';
const MAX_REDIRECTS = 5;
// a stalled server would otherwise hold the request, and whatever waits on it, open forever
const DEFAULT_TIMEOUT = 60 * 1000;
// the inactivity timeout above is reset by every byte, so a server dripping one within each
// window could still hold the request, and the message being built from it, open for as
// long as it liked. The whole request, redirects included, has to be done within this
const DEFAULT_MAX_TIME = 2 * 60 * 1000;
// the body is usually buffered in memory by the caller, so an unbounded download is refused
const DEFAULT_MAX_BYTES = 64 * 1024 * 1024;
// Only genuine TLS settings are taken from options.tls. That object reaches us straight
@@ -88,6 +92,12 @@ function nmfetch(url, options) {
options.cookies = options.cookies || new Cookies();
options.redirects = options.redirects || 0;
options.maxRedirects = isNaN(options.maxRedirects) ? MAX_REDIRECTS : options.maxRedirects;
if (!options.deadline) {
options.maxTime = typeof options.maxTime === 'number' && options.maxTime >= 0 ? options.maxTime : DEFAULT_MAX_TIME;
// the one deadline is shared with the requests the redirects lead to, every hop
// gets the time that is left rather than starting over
options.deadline = options.maxTime ? Date.now() + options.maxTime : 0;
}
const fetchRes = options.fetchRes;
const parsed = parseFetchUrl(url);
if (!parsed) {
@@ -233,12 +243,22 @@ function nmfetch(url, options) {
});
return fetchRes;
}
// the whole request has to be done by options.deadline, see maxTime. The timer is
// released on every way out of the request, so it never outlives it
let deadlineTimer;
const clearDeadline = () => {
if (deadlineTimer) {
clearTimeout(deadlineTimer);
deadlineTimer = undefined;
}
};
// reports the first failure of this request on fetchRes and releases the request
const fail = (err, sourceUrl = url) => {
if (finished) {
return;
}
finished = true;
clearDeadline();
setFetchCode(err);
err.sourceUrl = sourceUrl;
fetchRes.emit('error', err);
@@ -248,10 +268,16 @@ function nmfetch(url, options) {
if (timeout) {
req.setTimeout(timeout, () => fail(new Error('Request Timeout')));
}
if (options.deadline) {
deadlineTimer = setTimeout(() => fail(new Error('Request exceeded the allowed ' + options.maxTime + ' ms')), Math.max(options.deadline - Date.now(), 1));
// a pending deadline is no reason to keep the process alive
deadlineTimer.unref();
}
req.on('error', (err) => fail(err));
// a consumer that destroys the response stream before it ended does not want the rest of
// the body, release the request now instead of when the timeout fires
fetchRes.once('close', () => {
clearDeadline();
if (finished || fetchRes.readableEnded) {
return;
}
@@ -320,6 +346,7 @@ function nmfetch(url, options) {
// this request is done with, release it so its socket and timeout do not
// outlive it and report a late error on the shared fetchRes
finished = true;
clearDeadline();
res.resume();
req.abort();
return nmfetch(redirectUrl, options);
@@ -330,6 +357,8 @@ function nmfetch(url, options) {
return fail(new Error('Invalid status code ' + res.statusCode));
}
res.on('error', (err) => fail(err));
// the response is in, whatever is left of it is in memory and no longer on the network
res.once('end', () => clearDeadline());
const maxBytes = typeof options.maxBytes === 'number' && options.maxBytes > 0 ? options.maxBytes : DEFAULT_MAX_BYTES;
const source = inflate || res;
let received = 0;
@@ -367,4 +396,5 @@ function nmfetch(url, options) {
}
nmfetch.Cookies = Cookies;
nmfetch.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
nmfetch.DEFAULT_MAX_TIME = DEFAULT_MAX_TIME;
export default nmfetch;
+4 -1
View File
@@ -360,9 +360,12 @@ class Mail extends EventEmitter {
return callback(err);
}
let cidCounter = 0;
// the media type is a bounded token: a quote, whitespace, '>' or '<' ends the
// attribute value and with it the type, so one img tag never scans ahead to a
// ';' that belongs to a later tag, which was quadratic
html = (html || '')
.toString()
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;"'<>\s]{1,255});[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
const cid = crypto.randomBytes(10).toString('hex') + '@localhost';
if (!mail.data.attachments) {
mail.data.attachments = [];
+5 -1
View File
@@ -23,7 +23,11 @@ export interface SendMailOptions extends MailComposerOptions {
dsn?: SMTPEnvelopeDsn | undefined;
/** SMTP transports: RFC 8689, send the REQUIRETLS parameter with MAIL FROM */
requireTLSExtensionEnabled?: boolean | undefined;
/** SMTP transports: per-message authentication settings, used instead of the transport level auth */
/**
* SMTP transports: per-message authentication settings, used instead of the transport level auth.
* Only the credential fields are taken from it (type, user, pass, accessToken, refreshToken, expires),
* the token endpoint and client settings stay those of the transport
*/
auth?: SMTPTransportAuthOptions | undefined;
/** SES transport: extra SendEmailCommand parameters merged into the API call */
ses?: SESSendEmailRequest | undefined;
+4 -1
View File
@@ -145,7 +145,10 @@ export default class MailMessage {
const values = [data.html, data.text, data.watchHtml, data.amp, data.raw, data.icalEvent];
for (const key of ['attachments', 'alternatives']) {
if (Array.isArray(data[key])) {
values.push(...data[key]);
// one at a time, spreading a long enough list into the arguments throws
for (const entry of data[key]) {
values.push(entry);
}
}
}
for (const value of values) {
+25 -13
View File
@@ -137,26 +137,38 @@ export function encodeWord(data, mimeWordEncoding, maxLength) {
*/
export function encodeWords(value, mimeWordEncoding, maxLength, encodeAll) {
maxLength = maxLength || 0;
// find first word with a non-printable ascii or special symbol in it
const firstMatch = value.match(/(?:^|\s)([^\s]*["\u0080-\uFFFF])/);
if (!firstMatch) {
// find the first and the last character that needs encoding, a quote or anything outside
// printable ascii. A scan rather than a regex: matching the last such word with a
// backtracking pattern took quadratic time on a long word of non-ascii characters
let firstSpecial = -1;
let lastSpecial = -1;
for (let i = 0; i < value.length; i++) {
const code = value.charCodeAt(i);
if (code === 0x22 || code >= 0x80) {
if (firstSpecial < 0) {
firstSpecial = i;
}
lastSpecial = i;
}
}
if (firstSpecial < 0) {
return value;
}
if (encodeAll) {
// if it is requested to encode everything or the string contains something that resebles encoded word, then encode everything
return encodeWord(value, mimeWordEncoding, maxLength);
}
// find the last word with a non-printable ascii in it
const lastMatch = value.match(/(["\u0080-\uFFFF][^\s]*)[^"\u0080-\uFFFF]*$/);
if (!lastMatch) {
// should not happen
return value;
// encode from the start of the word holding the first such character to the end of the
// word holding the last one, so the encoded words stay separated from the plain text by
// the whitespace that was already there
let startIndex = firstSpecial;
while (startIndex > 0 && !/\s/.test(value.charAt(startIndex - 1))) {
startIndex--;
}
let endIndex = lastSpecial + 1;
while (endIndex < value.length && !/\s/.test(value.charAt(endIndex))) {
endIndex++;
}
const startIndex = firstMatch.index +
(firstMatch[0].match(/[^\s]/) || {
index: 0
}).index;
const endIndex = lastMatch.index + (lastMatch[1] || '').length;
return ((startIndex ? value.substr(0, startIndex) : '') +
encodeWord(value.substring(startIndex, endIndex), mimeWordEncoding || 'Q', maxLength) +
(endIndex < value.length ? value.substr(endIndex) : ''));
+97 -22
View File
@@ -26,6 +26,35 @@ const QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"$/;
// else goes into angle brackets so that the header can not be read as more addresses than
// the envelope carries
const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
/**
* A domain literal that ends an address, see _normalizeAddress
*/
const DOMAIN_LITERAL_TAIL = /@(\[[^[\]\\]*\])$/;
/**
* Removes the whitespace inside every <...> of a References value. A "<" with no ">" after
* it is left as it is. An index scan, the regex /<[^>]*>/g rescans the rest of the value
* from every such "<"
*
* @param value References header value
* @returns The value with the msg-ids closed up
*/
function stripWhitespaceInAngleBrackets(value) {
let result = '';
let pos = 0;
for (;;) {
const open = value.indexOf('<', pos);
if (open < 0) {
break;
}
const close = value.indexOf('>', open + 1);
if (close < 0) {
break;
}
result += value.slice(pos, open) + value.slice(open, close + 1).replace(/\s/g, '');
pos = close + 1;
}
return result + value.slice(pos);
}
// domainToASCII and domainToUnicode are WHATWG host parsers rather than plain IDNA
// mappers, so they do more than map: they cut the host at '/', '\\', '?' and '#', drop C0
// controls, and percent-decode. Handing them 'evil.example/mail.corp.example' returns the
@@ -33,6 +62,13 @@ const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
// unroutable garbage into mail for a domain the sender never named. None of these
// characters are legal in a domain, so keep them away from the mapper.
const URL_PARSER_UNSAFE = /[/\\?#%\x00-\x20\x7F]/;
// RFC 1035 bounds a label at 63 octets and a domain at 253. An A-label is never shorter than
// the code points it encodes, so anything past these bounds could not have become a valid
// domain whichever way it is encoded. The bundled codec is super-linear in the length of a
// label, a 60 KB label of distinct code points took seconds to encode, several times per
// address, so it is only handed what could be a domain
const MAX_DOMAIN_LENGTH = 253;
const MAX_LABEL_LENGTH = 63;
// pipeline() needs a callback to not throw, the errors it sees reach the last stream anyway
const PIPELINE_NOOP = () => false;
// The error a node streaming into an output that was destroyed stops with
@@ -41,6 +77,27 @@ function abortedError() {
err.code = errors.ESTREAM;
return err;
}
/**
* Checks a domain against the DNS length limits, counted in code points per label and in
* total. The label separators are the ones the bundled codec splits on.
*
* @param domain Domain to check
* @return true if a label or the domain as a whole is too long to be a valid domain
*/
function exceedsDomainLimits(domain) {
let total = 0;
let label = 0;
for (const char of domain) {
total++;
if (char === '.' || char === '。' || char === '.' || char === '。') {
label = 0;
}
else if (++label > MAX_LABEL_LENGTH) {
return true;
}
}
return total > MAX_DOMAIN_LENGTH;
}
/**
* Encodes a domain the way browsers, the WHATWG URL Standard and DNS facing resolvers do,
* which is with UTS-46 mapping applied before the Punycode step.
@@ -72,6 +129,10 @@ function normalizeDomain(domain, toUnicode) {
return mapped;
}
}
if (exceedsDomainLimits(domain)) {
// too long to be a domain whichever way it is encoded, keep it as supplied
return domain;
}
return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
}
/**
@@ -576,6 +637,17 @@ class MimeNode {
/[\u0080-\uFFFF]/.test(this.content)) {
structured.params.charset = 'utf-8';
}
if (this.filename) {
// the name parameter is derived from the filename below, so a name that
// arrived inside the content type is dropped first (a plain parameter or
// rfc2231 continuation sections), otherwise the header carries two and a
// receiver that reads the first sees a name the filename does not match
for (const paramKey of Object.keys(structured.params)) {
if (/^name(\*|$)/i.test(paramKey)) {
delete structured.params[paramKey];
}
}
}
value = mimeFuncs.buildHeaderValue(structured);
if (this.filename) {
// add support for non-compliant clients like QQ webmail
@@ -1185,12 +1257,12 @@ class MimeNode {
_normalizeHeaderKey(key) {
key = (key || '')
.toString()
// no newlines in keys
.replace(/\r?\n|\r/g, ' ')
// a field name is printable ascii without the colon, so a control char or DEL
// can only be dropped, there is no quoting construct around a field name
.replace(/[\x00-\x1f\x7f]/g, '')
.trim()
// a field name is printable ascii without the colon, so a control char, DEL,
// whitespace or a colon can only be dropped, there is no quoting construct
// around a field name. A colon or a space left in place ends the name early on
// the wire: the key 'From: ceo@example.com (' with the value ')' was emitted as a
// second From header that neither setHeader() nor the Bcc filter could match
.replace(/[\s\x00-\x20\x7f:]/g, '')
.toLowerCase()
// use uppercase words, except MIME
.replace(/^X-SMTPAPI$|^(MIME|DKIM|ARC|BIMI)\b|^[a-z]|-(SPF|FBL|ID|MD5)$|-[a-z]/gi, c => c.toUpperCase())
@@ -1276,26 +1348,26 @@ class MimeNode {
}
return value;
// space separated list of values enclosed in <>
case 'References':
value = [].concat
.apply([], [].concat(value || '').map(elm => {
elm = (elm || '')
case 'References': {
const ids = [];
for (const entry of [].concat(value || '')) {
const elm = (entry || '')
.toString()
.replace(/\r?\n|\r/g, ' ')
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
.trim();
return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
}))
.map(elm => {
if (elm.charAt(0) !== '<') {
elm = '<' + elm;
for (let id of stripWhitespaceInAngleBrackets(elm).split(/\s+/)) {
if (id.charAt(0) !== '<') {
id = '<' + id;
}
if (id.charAt(id.length - 1) !== '>') {
id = id + '>';
}
ids.push(id);
}
if (elm.charAt(elm.length - 1) !== '>') {
elm = elm + '>';
}
return elm;
});
return value.join(' ').trim();
}
return ids.join(' ').trim();
}
case 'Date':
if (Object.prototype.toString.call(value) === '[object Date]') {
return value.toUTCString().replace(/GMT/, '+0000');
@@ -1374,7 +1446,10 @@ class MimeNode {
// callers use an empty value to detect a missing address
return address;
}
const lastAt = address.lastIndexOf('@');
// dtext includes '@', so a domain literal is split off at the '@' in front of it and
// not at the last one, which delivered 'a@[x@evil.com]' to evil.com
const literal = address.charAt(address.length - 1) === ']' ? DOMAIN_LITERAL_TAIL.exec(address) : null;
const lastAt = literal ? address.length - literal[1].length - 1 : address.lastIndexOf('@');
if (lastAt < 0) {
// Bare username, there is no domain to split off
return this._normalizeLocalPart(address);
+1 -1
View File
@@ -1,3 +1,3 @@
export declare const name = "nodemailer";
export declare const version = "10.1.1";
export declare const version = "10.1.6";
export declare const homepage = "https://nodemailer.com/";
+1 -1
View File
@@ -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.1.1';
export const version = '10.1.6';
export const homepage = 'https://nodemailer.com/';
+22 -5
View File
@@ -19,10 +19,27 @@ import * as punycode from '../punycode/index.js';
// Matches a "scheme:" not followed by "//" (and with something after it), used
// to re-insert the authority separator the legacy parser did not require.
const SLASHLESS_AUTHORITY = /^([a-zA-Z][a-zA-Z0-9+.-]*:)(?!\/\/)([\s\S]+)$/;
// Leading and trailing C0 controls and spaces, which the WHATWG parser strips before it
// looks at the input. Stripped up front so that the slash-less form is recognized in a
// value read from a file with a trailing newline as well.
const SURROUNDING_WHITESPACE = /^[\x00-\x20]+|[\x00-\x20]+$/g;
/**
* Drops the leading and trailing C0 controls and spaces, which the WHATWG parser strips
* before it looks at the input. Stripped up front so that the slash-less form is recognized
* in a value read from a file with a trailing newline as well. An index scan, the regex
* /^[\x00-\x20]+|[\x00-\x20]+$/g retried a run of spaces inside the value from every
* position in it
*
* @param input URL string
* @returns The string without the surrounding controls and spaces
*/
function stripSurroundingWhitespace(input) {
let start = 0;
let end = input.length;
while (start < end && input.charCodeAt(start) <= 0x20) {
start++;
}
while (end > start && input.charCodeAt(end - 1) <= 0x20) {
end--;
}
return input.slice(start, end);
}
// Leading characters legacy url.parse() skips before it reads the scheme
const LEGACY_TRIM = /^[\x00-\x20\u00a0\ufeff]+/;
// The authority of a "scheme://authority/..." or a scheme-relative "//authority/..."
@@ -117,7 +134,7 @@ function normalizeHostname(raw, href) {
return mapped;
}
export const parse = (input, parseQueryString) => {
input = (input || '').replace(SURROUNDING_WHITESPACE, '');
input = stripSurroundingWhitespace(input || '');
// Legacy url.parse() parses a "user:pass@host:port" authority that follows
// the scheme even without the "//" separator, for schemes outside its
// built-in slashed-protocol list (smtp:/smtps:/socks:/...). The WHATWG
+16 -1
View File
@@ -76,6 +76,21 @@ function isPartialResponse(str) {
function isPartialLine(line) {
return /^\d+-/.test(line);
}
/**
* The log form of a command a custom authentication handler sends. Such a command carries
* the credentials in whatever shape its mechanism uses, so only an AUTH verb with its
* mechanism name is kept, the way the built-in mechanisms hide their own secrets.
*
* @param cmd Command the handler sends
* @return What the transaction log gets instead
*/
function maskAuthCommand(cmd) {
const match = (cmd || '').toString().match(/^AUTH\s+[^\s]+/i);
if (!match) {
return '/* secret */';
}
return match[0].length === cmd.length ? cmd : match[0] + ' /* secret */';
}
/**
* Generates a SMTP connection object
*
@@ -563,7 +578,7 @@ class SMTPConnection extends EventEmitter {
}
done(null, data);
});
setImmediate(() => this._sendCommand(cmd));
setImmediate(() => this._sendCommand(cmd, maskAuthCommand(cmd)));
return promise;
};
const handlerResponse = handler({
+12 -1
View File
@@ -5,6 +5,13 @@ import * as shared from '../shared/index.js';
import XOAuth2 from '../xoauth2/index.js';
import * as errors from '../errors.js';
import * as packageData from '../package-info.js';
// The keys a message's `auth` may set on top of the transport's auth settings: the ones that
// say who logs in and with what. Everything else in the auth block (accessUrl, clientId,
// clientSecret, customHeaders, customParams, tls, privateKey, serviceClient and the rest)
// names the endpoint the credentials go to or is a secret of the application itself, and
// a message that could set `auth.accessUrl` and force a refresh would have the transport's
// client secret and refresh token posted to a URL of its choosing
const MESSAGE_AUTH_KEYS = ['type', 'user', 'pass', 'accessToken', 'refreshToken', 'expires'];
/**
* Creates a SMTP transport object for Nodemailer
*
@@ -64,7 +71,11 @@ class SMTPTransport extends EventEmitter {
}
return this.auth;
}
const authData = Object.assign({}, this.options.auth && typeof this.options.auth === 'object' ? this.options.auth : {}, typeof authOpts === 'object' ? authOpts : {});
const authData = Object.assign({}, this.options.auth && typeof this.options.auth === 'object' ? this.options.auth : {});
if (typeof authOpts === 'object') {
// see MESSAGE_AUTH_KEYS
shared.copyOwnKeys(authData, authOpts, key => !MESSAGE_AUTH_KEYS.includes(key));
}
if (Object.keys(authData).length === 0) {
return false;
}
+7
View File
@@ -360,6 +360,13 @@ export const services = {
"port": 587,
"secure": false
},
"Oveyon": {
"description": "Oveyon",
"host": "smtp.oveyon.com",
"port": 587,
"secure": false,
"requireTLS": true
},
"Postmark": {
"description": "Postmark",
"aliases": [
+8 -2
View File
@@ -226,8 +226,14 @@ class XOAuth2 extends Stream {
grant_type: 'refresh_token'
};
}
Object.assign(urlOptions, this.options.customParams);
Object.assign(loggedUrlOptions, this.options.customParams);
const customParams = this.options.customParams || {};
Object.assign(urlOptions, customParams);
// a custom param is as likely to be a client assertion or an API key as a tenant
// name, so the log gets the same glimpse of it that it gets of the client secret
for (const key of Object.keys(customParams)) {
const param = customParams[key];
loggedUrlOptions[key] = (param === undefined || param === null ? '' : String(param)).substr(0, 6) + '...';
}
this.logger.debug({
tnx: 'OAUTH2',
user: this.options.user,
+8
View File
@@ -333,6 +333,14 @@
"secure": false
},
"Oveyon": {
"description": "Oveyon",
"host": "smtp.oveyon.com",
"port": 587,
"secure": false,
"requireTLS": true
},
"Postmark": {
"description": "Postmark",
"aliases": ["PostmarkApp"],