mirror of
https://github.com/dawidd6/action-send-mail.git
synced 2026-10-19 14:04:32 +07:00
node_modules: update (#349)
Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
1 parent
9d0c26ec8b
commit
0a1f5db16c
36 files changed
+1934
-648
No files matched your search
+44
@@ -1,5 +1,49 @@
|
||||
# CHANGELOG
|
||||
|
||||
## [10.1.6](https://github.com/nodemailer/nodemailer/compare/v10.1.5...v10.1.6) (2026-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **addressparser:** read groups in the same pass as the rest of the list ([ddf0de1](https://github.com/nodemailer/nodemailer/commit/ddf0de1e7615908101bf593c62edcadccc05c8d3))
|
||||
|
||||
## [10.1.5](https://github.com/nodemailer/nodemailer/compare/v10.1.4...v10.1.5) (2026-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **addressparser:** handle an address list without copying it per address ([6bd09b3](https://github.com/nodemailer/nodemailer/commit/6bd09b395cb7ffd1b0ae95f960f3985ba9c25afb))
|
||||
* **addressparser:** tokenize into offsets instead of an object per token ([3fd3f50](https://github.com/nodemailer/nodemailer/commit/3fd3f50ce86671884896ef1eaddafff5e137f820))
|
||||
|
||||
## [10.1.4](https://github.com/nodemailer/nodemailer/compare/v10.1.3...v10.1.4) (2026-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **addressparser:** apply the unexpected "<" rule outside quotes and comments ([ee35f57](https://github.com/nodemailer/nodemailer/commit/ee35f5745658bf46952651fa53207255e9dc3406))
|
||||
* **addressparser:** bound group nesting at ten levels ([ce63303](https://github.com/nodemailer/nodemailer/commit/ce633031d58f8d2e9c9b0f5b671944a1afe71f95))
|
||||
* **addressparser:** drop the obsolete route from an angle-addr ([3dbc730](https://github.com/nodemailer/nodemailer/commit/3dbc7304b0cfa06551543fa2dc01222447c2b554))
|
||||
* **addressparser:** end a comment only at the ")" that closes the outermost one ([0a87617](https://github.com/nodemailer/nodemailer/commit/0a8761762ac78f2db2ba37f712f2a52a404ce011))
|
||||
* **addressparser:** honour quoted strings and comments inside an angle-addr and a group ([1cd57d2](https://github.com/nodemailer/nodemailer/commit/1cd57d2b8c49dff048c21cdbafdb54439a7f07e2))
|
||||
* **addressparser:** never read a mailbox out of a comment ([a1538ea](https://github.com/nodemailer/nodemailer/commit/a1538ea39c76c48a4134806c41e31e59a258b1e5))
|
||||
* **addressparser:** read CFWS beside the "@" of an addr-spec as nothing ([ba3c095](https://github.com/nodemailer/nodemailer/commit/ba3c0952542581f95cb20d9322cc9e27baa470a2))
|
||||
* **addressparser:** read the first addr-spec of the display text ([038940a](https://github.com/nodemailer/nodemailer/commit/038940a069a2cecb64a70d0e81b17eb8ce200d3a))
|
||||
* split a domain literal off at the "@" in front of it ([d5983b0](https://github.com/nodemailer/nodemailer/commit/d5983b0ae234cd22ab45be817fefd434e9a53d58))
|
||||
|
||||
## [10.1.3](https://github.com/nodemailer/nodemailer/compare/v10.1.2...v10.1.3) (2026-10-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* format References and strip URLs in linear time, release any number of streams ([2f18641](https://github.com/nodemailer/nodemailer/commit/2f186418cf6c8fff18f235d4318892fd42f810fa))
|
||||
|
||||
## [10.1.2](https://github.com/nodemailer/nodemailer/compare/v10.1.1...v10.1.2) (2026-10-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* bound the work a message can make the mailer do and keep per-message auth off the transport's token endpoint ([e347fa2](https://github.com/nodemailer/nodemailer/commit/e347fa2260a7e342e772a7e187937aa87ac96364))
|
||||
|
||||
## [10.1.1](https://github.com/nodemailer/nodemailer/compare/v10.1.0...v10.1.1) (2026-10-14)
|
||||
|
||||
|
||||
|
||||
+699
-271
File diff suppressed because it is too large.
Load diff
+3
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large.
Load diff
+3
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -333,6 +333,14 @@
|
||||
"secure": false
|
||||
},
|
||||
|
||||
"Oveyon": {
|
||||
"description": "Oveyon",
|
||||
"host": "smtp.oveyon.com",
|
||||
"port": 587,
|
||||
"secure": false,
|
||||
"requireTLS": true
|
||||
},
|
||||
|
||||
"Postmark": {
|
||||
"description": "Postmark",
|
||||
"aliases": ["PostmarkApp"],
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "nodemailer",
|
||||
"version": "10.1.1",
|
||||
"version": "10.1.6",
|
||||
"description": "Easy as cake e-mail sending from your Node.js applications",
|
||||
"type": "module",
|
||||
"main": "./dist/cjs/nodemailer.js",
|
||||
|
||||
Reference in new issue
Block a user