diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index d1d9f61c..02762b99 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -94,9 +94,9 @@ } }, "node_modules/nodemailer": { - "version": "10.0.13", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.0.13.tgz", - "integrity": "sha512-SzG86OlvcW/NNhUFC6uROMwRTL4n7MswfQqC/T8mhkmnY1YVa23zUEMYi4ijSeXSl9GLz9ZeTJDUatEDuY5FeQ==", + "version": "10.0.14", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-10.0.14.tgz", + "integrity": "sha512-eJoLFKg55fesSlzUekmfOv/SBXh4KUYT8oiQc2RLP0tfG0rbNMJBK7wgHJwXTJwqyKlSJMz9UzDZ4v+aHEvo8g==", "license": "MIT-0", "engines": { "node": ">=20.0.0" diff --git a/node_modules/nodemailer/CHANGELOG.md b/node_modules/nodemailer/CHANGELOG.md index f4f622f9..43a3c76f 100644 --- a/node_modules/nodemailer/CHANGELOG.md +++ b/node_modules/nodemailer/CHANGELOG.md @@ -1,5 +1,17 @@ # CHANGELOG +## [10.0.14](https://github.com/nodemailer/nodemailer/compare/v10.0.13...v10.0.14) (2026-10-03) + + +### Bug Fixes + +* **addressparser:** keep a quoted display name that holds no "@" out of the address ([3570d26](https://github.com/nodemailer/nodemailer/commit/3570d26c7b55f6e5df50d4076046600ad3fd040b)) +* **addressparser:** keep the group recursion depth out of the options object ([a680254](https://github.com/nodemailer/nodemailer/commit/a68025405a1378c82e78f64ffad27b92c1035cbe)) +* **addressparser:** stop a "[" from hiding the operators after it ([5619784](https://github.com/nodemailer/nodemailer/commit/561978491d5cc33d2052a6c89a499b838b960262)) +* **dkim:** trim a header field name in linear time ([c6f7a55](https://github.com/nodemailer/nodemailer/commit/c6f7a55a2978bf031b8519ebf59baddf94c032c8)) +* **mime-funcs:** read and write header parameters per rfc2045 and rfc2231 ([b8ccad7](https://github.com/nodemailer/nodemailer/commit/b8ccad723a7e49a674a4d70f32eb9fe77312008d)) +* search only the new bytes for the end of the proxy CONNECT response ([81efd7b](https://github.com/nodemailer/nodemailer/commit/81efd7bfc559305f900d6a22b5c56aa94556002b)) + ## [10.0.13](https://github.com/nodemailer/nodemailer/compare/v10.0.12...v10.0.13) (2026-09-30) diff --git a/node_modules/nodemailer/dist/cjs/addressparser/index.d.ts b/node_modules/nodemailer/dist/cjs/addressparser/index.d.ts index 583c45b7..edbeffc5 100644 --- a/node_modules/nodemailer/dist/cjs/addressparser/index.d.ts +++ b/node_modules/nodemailer/dist/cjs/addressparser/index.d.ts @@ -39,7 +39,6 @@ export type Address = MailboxAddress | GroupAddress; * * @param str Address field * @param options Optional options object - * @param options._depth Internal recursion depth counter (do not set manually) * @return An array of address objects */ declare function addressparser(str: string | null | undefined, options: AddressParserOptions & { diff --git a/node_modules/nodemailer/dist/cjs/addressparser/index.js b/node_modules/nodemailer/dist/cjs/addressparser/index.js index 6d52da9d..4c36464b 100644 --- a/node_modules/nodemailer/dist/cjs/addressparser/index.js +++ b/node_modules/nodemailer/dist/cjs/addressparser/index.js @@ -428,7 +428,7 @@ function _handleAddress(tokens, depth) { // Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting) let groupMembers = []; if (data.group.length) { - const parsedGroup = addressparser(data.group.join(','), { _depth: depth + 1 }); + const parsedGroup = _parseAddressList(data.group.join(','), depth + 1); parsedGroup.forEach(member => { if (member.group) { groupMembers = groupMembers.concat(member.group); @@ -515,7 +515,13 @@ function _handleAddress(tokens, depth) { // Join values with spaces data.text = data.text.join(' '); data.address = data.address.join(' '); - if (addressFromQuotedText && data.text) { + if (addressFromQuotedText && data.text.indexOf('@') >= 0) { + // A quoted run with no '@' in it is a display name and nothing else, so it stays + // where it is. Moving it over anyway made a comment the mailbox was read from: + // '"Display Name" <(a comment)>' handed on 'Display Name' as the address with the + // comment as the name, and a quoted name holding a ',' or a ';' became an address + // that reads as two recipients once a consumer writes it back into a header. + // // The mailbox is still sitting in the text, so it moves over here and is quoted // before the recovery below rather than after it. Anything else the text holds // came along with it: a comment ends the domain but leaves the atoms behind it in @@ -562,6 +568,7 @@ class Tokenizer { this.node = null; this.escaped = false; this.inDomainLiteral = false; + this.afterAt = false; this.list = []; /** * Operator tokens and which tokens are expected to end the sequence @@ -605,17 +612,23 @@ class Tokenizer { * Checks if a character is an operator or text and acts accordingly * * @param chr Character from the address field + * @param nextChr Character following chr, null at the end of the field */ checkChar(chr, nextChr) { - // Track RFC 5322 domain-literals ("[" *dtext "]"). Operator characters such - // as the ":" of an IPv6 address-literal (user@[IPv6:2001:db8::1]) are dtext - // and must not be treated as the group delimiter while inside the brackets. - // Quoted strings and comments are handled separately via operatorExpecting, - // so only enter this state when no operator is open. The list separators "," - // and ";" are the exception: they always end the literal (and split the - // address list) so that an unclosed "[" cannot swallow later recipients. + // Track RFC 5322 domain-literals ("[" *dtext "]"). The ":" of an IPv6 + // address-literal (user@[IPv6:2001:db8::1]) is dtext and must not be treated as + // the group delimiter while inside the brackets. That is the only operator the + // literal hides: letting it hide the others as well meant a "[" could turn the + // comment, quoted string or angle-addr after it into text and pick a different + // mailbox, '[ ( ] ) ' yielded a@victim.com where + // RFC 5322 reads b@evil.com. A "[" also only opens a literal right after the "@" + // of an addr-spec, so the group delimiter of a display name holding one stays + // intact. Quoted strings and comments are handled separately via + // operatorExpecting, so only enter this state when no operator is open. The list + // separators "," and ";" always end the literal (and split the address list) so + // that an unclosed "[" cannot swallow later recipients. if (!this.escaped && !this.operatorExpecting) { - if (!this.inDomainLiteral && chr === '[') { + if (!this.inDomainLiteral && chr === '[' && this.afterAt) { this.inDomainLiteral = true; } else if (this.inDomainLiteral && (chr === ']' || chr === ',' || chr === ';')) { @@ -635,17 +648,24 @@ class Tokenizer { } this.list.push(this.node); this.node = null; + if (chr !== ')') { + // a comment is folding whitespace, it does not part the "@" from the "[" + this.afterAt = false; + } this.operatorExpecting = ''; this.escaped = false; return; } - else if (!this.operatorExpecting && !this.inDomainLiteral && chr in this.operators) { + else if (!this.operatorExpecting && !(this.inDomainLiteral && chr === ':') && chr in this.operators) { this.node = { type: 'operator', value: chr }; this.list.push(this.node); this.node = null; + if (chr !== '(') { + this.afterAt = false; + } this.operatorExpecting = this.operators[chr]; this.escaped = false; return; @@ -669,6 +689,11 @@ class Tokenizer { if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) { // skip command bytes this.node.value += chr; + // text inside a quoted string or a comment is not part of an addr-spec. Tracked + // as it goes rather than read back off the value (see lastChars in _handleAddress) + if (!this.operatorExpecting && chr !== ' ' && chr !== '\t') { + this.afterAt = chr === '@'; + } } this.escaped = false; } @@ -679,9 +704,19 @@ class Tokenizer { * malicious input that could cause stack overflow. */ const MAX_NESTED_GROUP_DEPTH = 50; -function addressparser(str, options) { - options = options || {}; - const depth = options._depth || 0; +/** + * Parses an address list, recursing into the groups it holds. + * + * The depth is threaded through the calls rather than carried in an options object, so a + * caller supplied one can not seed it and lift the recursion limit: `{ _depth: -1e9 }` + * bought a billion levels of nesting and turned the guard below into the stack overflow it + * is there to prevent. + * + * @param str Address field + * @param depth Current recursion depth for nested group protection + * @return An array of address objects + */ +function _parseAddressList(str, depth) { // Prevent stack overflow from deeply nested groups (DoS protection) if (depth > MAX_NESTED_GROUP_DEPTH) { return []; @@ -690,7 +725,7 @@ function addressparser(str, options) { const tokens = tokenizer.tokenize(); const addresses = []; let address = []; - let parsedAddresses = []; + const parsedAddresses = []; tokens.forEach(token => { if (token.type === 'operator' && (token.value === ',' || token.value === ';')) { if (address.length) { @@ -731,8 +766,11 @@ function addressparser(str, options) { } } mergedAddresses.reverse(); - parsedAddresses = mergedAddresses; - if (options.flatten) { + return mergedAddresses; +} +function addressparser(str, options) { + const parsedAddresses = _parseAddressList(str, 0); + if (options?.flatten) { const flatAddresses = []; const walkAddressList = (list) => { list.forEach(entry => { diff --git a/node_modules/nodemailer/dist/cjs/dkim/message-parser.js b/node_modules/nodemailer/dist/cjs/dkim/message-parser.js index 603d1299..e34a5076 100644 --- a/node_modules/nodemailer/dist/cjs/dkim/message-parser.js +++ b/node_modules/nodemailer/dist/cjs/dkim/message-parser.js @@ -1,6 +1,33 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const node_stream_1 = require("node:stream"); +/** + * Drops the SP and HTAB from both ends of a field name. + * + * An index scan rather than `/^[ \t]+|[ \t]+$/g`, which retries the trailing branch at + * every offset of a blank run that is followed by other text: the run is consumed greedily, + * `$` fails, and the engine gives the characters back one at a time before moving on to + * start the same walk one offset further in. A field name carrying a 128 KiB run of spaces + * between two atoms took about 6.7 seconds to trim, and the cost grows with its square. + * + * Not `.trim()`, which takes the other Unicode spaces along: an NBSP in front of a field + * name would come off and select the name into the signed set, where the relaxed + * canonicalization in sign.ts deliberately leaves everything but SP and HTAB alone. + * + * @param str Field name to trim + * @return The field name without the surrounding SP and HTAB + */ +function _trimFieldName(str) { + let start = 0; + let end = str.length; + while (start < end && (str.charCodeAt(start) === 0x20 || str.charCodeAt(start) === 0x09)) { + start++; + } + while (end > start && (str.charCodeAt(end - 1) === 0x20 || str.charCodeAt(end - 1) === 0x09)) { + end--; + } + return str.slice(start, end); +} /** * MessageParser instance is a transform stream that separates message headers * from the rest of the body. Headers are emitted with the 'headers' event. Message @@ -143,10 +170,7 @@ class MessageParser extends node_stream_1.Transform { return lines .filter(line => /[^ \t\r]/.test(line)) .map(line => ({ - key: line - .substr(0, line.indexOf(':')) - .replace(/^[ \t]+|[ \t]+$/g, '') - .toLowerCase(), + key: _trimFieldName(line.substr(0, line.indexOf(':'))).toLowerCase(), line })); } diff --git a/node_modules/nodemailer/dist/cjs/dkim/sign.d.ts b/node_modules/nodemailer/dist/cjs/dkim/sign.d.ts index a1e4beda..18b6b02b 100644 --- a/node_modules/nodemailer/dist/cjs/dkim/sign.d.ts +++ b/node_modules/nodemailer/dist/cjs/dkim/sign.d.ts @@ -36,6 +36,7 @@ export interface DKIMRelaxedHeaders { * Returns DKIM signature header line * * @param headers Parsed headers object from MessageParser + * @param hashAlgo Hash algorithm the body hash was calculated with, for example "sha256" * @param bodyHash Base64 encoded hash of the message * @param options DKIM options * @param options.domainName Domain name to be signed for diff --git a/node_modules/nodemailer/dist/cjs/dkim/sign.js b/node_modules/nodemailer/dist/cjs/dkim/sign.js index 6fe8c4e9..1fc08c2b 100644 --- a/node_modules/nodemailer/dist/cjs/dkim/sign.js +++ b/node_modules/nodemailer/dist/cjs/dkim/sign.js @@ -52,6 +52,7 @@ function unsupportedHashAlgoError(hashAlgo) { * Returns DKIM signature header line * * @param headers Parsed headers object from MessageParser + * @param hashAlgo Hash algorithm the body hash was calculated with, for example "sha256" * @param bodyHash Base64 encoded hash of the message * @param options DKIM options * @param options.domainName Domain name to be signed for diff --git a/node_modules/nodemailer/dist/cjs/mailer/index.d.ts b/node_modules/nodemailer/dist/cjs/mailer/index.d.ts index 58684f49..cdecd501 100644 --- a/node_modules/nodemailer/dist/cjs/mailer/index.d.ts +++ b/node_modules/nodemailer/dist/cjs/mailer/index.d.ts @@ -193,9 +193,15 @@ declare class Mail; + /** + * Sends an email using the preselected transport object + * + * @param data E-data description + * @param callback Callback to run once the sending succeeded or failed + */ sendMail(data: SendMailOptions, callback: SendMailCallback): void; getVersionString(): string; /** diff --git a/node_modules/nodemailer/dist/cjs/mime-funcs/index.d.ts b/node_modules/nodemailer/dist/cjs/mime-funcs/index.d.ts index a9a79e72..c4278fa6 100644 --- a/node_modules/nodemailer/dist/cjs/mime-funcs/index.d.ts +++ b/node_modules/nodemailer/dist/cjs/mime-funcs/index.d.ts @@ -59,6 +59,7 @@ export declare function quoteString(value?: string): string; * no need to encode the values in any way. If the value is plaintext but has * longer lines then allowed, then use format=flowed * + * @param str Multi line string to check * @param lineLength Max line length to check for * @returns Returns true if there is at least one line longer than lineLength chars */ @@ -100,9 +101,9 @@ export declare function buildHeaderValue(structured: StructuredHeaderValue): str * title*0*=utf-8''unicode * title*1*=%20string * + * @param key Parameter name the generated keys are built from, for example title * @param data String to be encoded * @param [maxLength=50] Max length for generated chunks - * @param [fromCharset='UTF-8'] Source sharacter set * @return A list of encoded keys and headers */ export declare function buildHeaderParam(key: string, data: string | Buffer, maxLength?: number): EncodedHeaderParam[]; diff --git a/node_modules/nodemailer/dist/cjs/mime-funcs/index.js b/node_modules/nodemailer/dist/cjs/mime-funcs/index.js index 6a66aa67..a2603b95 100644 --- a/node_modules/nodemailer/dist/cjs/mime-funcs/index.js +++ b/node_modules/nodemailer/dist/cjs/mime-funcs/index.js @@ -88,6 +88,7 @@ function quoteString(value) { * no need to encode the values in any way. If the value is plaintext but has * longer lines then allowed, then use format=flowed * + * @param str Multi line string to check * @param lineLength Max line length to check for * @returns Returns true if there is at least one line longer than lineLength chars */ @@ -234,7 +235,10 @@ function buildHeaderValue(structured) { } }); } - else if (/[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) { + else if (!value.length || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) { + // a parameter value is a token or a quoted-string and a token is never empty, so + // a valueless parameter such as the 'flag' of 'multipart/mixed; flag; boundary=b' + // goes out as 'flag=""' rather than as the 'flag=' that parses as neither paramsArray.push(param + '=' + JSON.stringify(value)); } else { @@ -256,9 +260,9 @@ function buildHeaderValue(structured) { * title*0*=utf-8''unicode * title*1*=%20string * + * @param key Parameter name the generated keys are built from, for example title * @param data String to be encoded * @param [maxLength=50] Max length for generated chunks - * @param [fromCharset='UTF-8'] Source sharacter set * @return A list of encoded keys and headers */ function buildHeaderParam(key, data, maxLength) { @@ -385,6 +389,26 @@ function buildHeaderParam(key, data, maxLength) { value: item.line })); } +/** + * An RFC 2045 token: printable ASCII without SPACE, the control characters, DEL and the + * tspecials. A charset name is one, and the name a continuation carries is written into + * the encoded word the parameter value becomes, so it is checked against this before it + * goes in. Whitespace used to come off it only because the value was trimmed first. + */ +const TOKEN = /^[^\x00-\x20\x7f()<>@,;:\\"/[\]?=]+$/; +/** + * Whether a string can be a header parameter name. + * + * A parameter name is a token, so it is never empty. A "__proto__" name would target the + * prototype chain of the params object instead of an own property of it and read back as + * Object.prototype, so it is no name either. + * + * @param name Candidate parameter name, already lowercased + * @return true when the name can be used + */ +function _isParamName(name) { + return !!name && !(0, objects_js_1.isProtoKey)(name); +} /** * Parses a header value with key=value arguments into a structured * object. @@ -405,134 +429,224 @@ function parseHeaderValue(str) { value: '', params: {} }; - // Parameter names come from a caller supplied contentType/contentDisposition. A - // "__proto__" name would target the prototype chain of the params object instead of - // an own property of it, and read back as Object.prototype, so it is dropped. + // A duplicated parameter resolves to its first occurrence, the way a duplicated header + // does. Letting the last one win disagrees with the receivers that take the first, and + // the two readings of 'boundary="b"; boundary="c"' name different delimiters. The + // continuation join below is the only writer of the name it builds, so it tests the + // name with _isParamName directly rather than taking that rule along from here. const setParam = (name, value) => { - if (!(0, objects_js_1.isProtoKey)(name)) { + name = name.toLowerCase(); + if (_isParamName(name) && !Object.prototype.hasOwnProperty.call(response.params, name)) { response.params[name] = value; } }; let key = false; let value = ''; - let type = 'value'; + let stage = 'value'; let quote = false; let escaped = false; let chr; + // Whitespace seen outside a quoted string is held back until a significant character + // follows it, so the whitespace around a value is dropped without trimming spaces the + // sender quoted on purpose. Trimming the stored value instead loses the trailing space + // of 'filename*0="Annual Report "', which the next continuation section is appended to. + let pendingSpace = ''; + let quoteClosed = false; + // Whitespace ahead of the first character of a value is padding and is dropped, the + // whitespace between two characters of it is content + const flushSpace = () => { + if (value.length) { + value += pendingSpace; + } + pendingSpace = ''; + }; + const addChr = (c) => { + flushSpace(); + value += c; + }; + const takeValue = () => { + const taken = value; + value = ''; + pendingSpace = ''; + quoteClosed = false; + return taken; + }; + const storeValue = () => { + const taken = takeValue(); + if (key === false) { + response.value = taken; + } + else { + setParam(key, taken); + } + }; + // A parameter name with no '=' is a valueless parameter, not the start of the next one. + // Without this the name keeps growing across the ';' and swallows whatever follows, which + // is how 'multipart/mixed; flag; boundary="AAA"' lost its boundary to a parameter named + // 'flag; boundary' and left the node declaring the generated boundary beside the asked + // for one, so a receiver reading the first of the two found no delimiter it matched. + const storeEmptyKey = () => { + setParam(takeValue().trim(), ''); + }; for (let i = 0, len = str.length; i < len; i++) { chr = str.charAt(i); - if (type === 'key') { + if (stage === 'key') { if (chr === '=') { - key = value.trim().toLowerCase(); - type = 'value'; - value = ''; + key = takeValue().trim(); + stage = 'value'; + continue; + } + if (chr === ';') { + storeEmptyKey(); continue; } value += chr; } else { - if (escaped) { - value += chr; + if (quoteClosed && chr !== ';') { + // Nothing behind a closed quoted string reaches the value. RFC 2045 says a + // parameter value is a token or a quoted string and not both, so what follows + // one is junk and only the ';' that ends the parameter still counts. Tested + // ahead of the branches rather than beside the append at the foot of them, + // where each branch above was a way around it: a second '"' reopened quoting + // and swallowed the rest of the header, so 'boundary="AAA" "; boundary=BBB"' + // read as a single boundary of 'AAA ', and the junk of + // 'boundary="AAA" (unterminated comment' still joined the declared boundary + // through the escape branch. quoteClosed is only ever set while no quote is + // open, and this is what keeps one from being opened afterwards. + escaped = false; + continue; } - else if (chr === '\\') { + if (escaped) { + addChr(chr); + } + else if (quote && chr === '\\') { + // a backslash only escapes inside a quoted string, everywhere else it is an + // ordinary character. Treating it as an escape turns the parameter value + // 'C:\Users\me\report.txt' into 'C:Usersmereport.txt' escaped = true; continue; } else if (quote && chr === quote) { quote = false; + quoteClosed = true; } else if (!quote && chr === '"') { quote = chr; + flushSpace(); } else if (!quote && chr === ';') { - if (key === false) { - response.value = value.trim(); - } - else { - setParam(key, value.trim()); - } - type = 'key'; - value = ''; + storeValue(); + stage = 'key'; + } + else if (!quote && (chr === ' ' || chr === '\t')) { + pendingSpace += chr; } else { - value += chr; + addChr(chr); } escaped = false; } } - if (type === 'value') { - if (key === false) { - response.value = value.trim(); - } - else { - setParam(key, value.trim()); - } + if (stage === 'value') { + storeValue(); } - else if (value.trim()) { - setParam(value.trim().toLowerCase(), ''); + else { + // a key with no value, as in 'Header-Key: somevalue; key=value; emptykey' + storeEmptyKey(); } // handle parameter value continuations // https://tools.ietf.org/html/rfc2231#section-3 - // preprocess values + // Sections are collected in a list and ordered below rather than written into an array + // at their own section number. An index write makes the array as long as the number the + // header asked for, and the join that follows walks all of it, so the 55 byte value + // "attachment; filename*0*=utf-8''a; filename*4000000000=b" held a core for over two + // minutes. + const continuations = new Map(); Object.keys(response.params).forEach(key => { - let actualKey, nr, match, value; - if ((match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/))) { - actualKey = key.substr(0, match.index); - nr = Number(match[2] || match[3]) || 0; - if ((0, objects_js_1.isProtoKey)(actualKey)) { - // see setParam. Reading it back would yield Object.prototype, which is - // an object, so the initializer below would be skipped and the write - // that follows would throw out of a header build the caller can not catch - delete response.params[key]; - return; - } - if (!response.params[actualKey] || typeof response.params[actualKey] !== 'object') { - response.params[actualKey] = { - charset: false, - values: [] - }; - } - value = response.params[key]; - if (nr === 0 && match[0].substr(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) { - response.params[actualKey].charset = match[1] || 'iso-8859-1'; - value = match[2]; - } - response.params[actualKey].values[nr] = value; - // remove the old reference - delete response.params[key]; + const match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/); + if (!match) { + // not a continuation parameter, there is nothing to join + return; } + const actualKey = key.substr(0, match.index); + const nr = Number(match[2] || match[3]) || 0; + // RFC 2231 section 4.1: only a section whose name ends in '*' is percent encoded + const encoded = match[0].substr(-1) === '*'; + // remove the old reference + let value = response.params[key]; + delete response.params[key]; + if (!_isParamName(actualKey)) { + // the joined value can not be written back under this name. It is empty when the + // continuation suffix was all there was of it, as in the bare '*' of 'text/plain; a;*' + return; + } + let continuation = continuations.get(actualKey); + if (!continuation) { + continuation = { charset: false, sections: [] }; + continuations.set(actualKey, continuation); + } + const charsetMatch = nr === 0 && encoded ? value.match(/^([^']*)'[^']*'(.*)$/) : null; + if (charsetMatch) { + // the charset is a token, and anything else named as one is no charset a consumer + // could resolve, so it reads as the unnamed case rather than being carried into + // the encoded word below. A "\r\n" of a prefix would otherwise reach a consumer + // of the parsed value as the charset of a word that no decoder can act on + continuation.charset = TOKEN.test(charsetMatch[1]) ? charsetMatch[1] : 'iso-8859-1'; + value = charsetMatch[2]; + } + continuation.sections.push({ nr, value, encoded }); }); // concatenate split rfc2231 strings and convert encoded strings to mime encoded words - Object.keys(response.params).forEach(key => { - let value; - if (response.params[key] && Array.isArray(response.params[key].values)) { - value = response.params[key].values.map((val) => val || '').join(''); - if (response.params[key].charset) { - // convert "%AB" to "=?charset?Q?=AB?=" - response.params[key] = - '=?' + - response.params[key].charset + - '?Q?' + - value - // fix invalidly encoded chars - .replace(/[=?_\s]/g, s => { - const c = s.charCodeAt(0).toString(16); - if (s === ' ') { - return '_'; - } - return '%' + (c.length < 2 ? '0' : '') + c; - }) - // change from urlencoding to percent encoding - .replace(/%/g, '=') + - '?='; - } - else { - response.params[key] = value; - } + continuations.forEach((continuation, key) => { + if (Object.prototype.hasOwnProperty.call(response.params, key)) { + // The same name was also given as a plain parameter, which the starred keys were + // just deleted from around, so this write would be the only one in the function + // to override a name already taken. 'filename=plain.txt; filename*0=evil.txt' + // resolves to the plain parameter either way round, so the reading does not come + // down to which of the two spellings the sender put first + return; } + continuation.sections.sort((a, b) => a.nr - b.nr); + if (!continuation.charset) { + // nothing said which charset the percent escapes of an encoded section are in, + // so every section is passed on as the text it already is + response.params[key] = continuation.sections.map(section => section.value).join(''); + return; + } + // convert "%AB" to "=?charset?Q?=AB?=" + response.params[key] = '=?' + continuation.charset + '?Q?' + continuation.sections.map(_encodeContinuationSection).join('') + '?='; }); return response; } +/** + * Renders one parameter value continuation section as the payload of a Q encoded word. + * + * A section whose name ends in '*' is percent encoded and its escapes carry the bytes of + * the value, so they only have to be rewritten into the "=AB" spelling a Q encoded word + * uses. A section without the '*' is literal text (RFC 2231 section 4.1), so its '%' is a + * '%' and is escaped along with the characters a Q encoded word can not carry bare. + * Decoding a literal section invents bytes that never appeared on the wire: it is how the + * value 'filename*0*=utf-8''safe; filename*1=%2F..%2F..%2Fetc%2Fpasswd' was emitted as a + * filename every receiving client reads back as 'safe/../../etc/passwd'. + * + * @param section One collected continuation section + * @return The section as Q encoded word payload + */ +function _encodeContinuationSection(section) { + const specials = section.encoded ? /[=?_\s]/g : /[=?_\s%]/g; + return (section.value + // fix invalidly encoded chars + .replace(specials, s => { + const c = s.charCodeAt(0).toString(16); + if (s === ' ') { + return '_'; + } + return '%' + (c.length < 2 ? '0' : '') + c; + }) + // change from urlencoding to percent encoding + .replace(/%/g, '=')); +} /** * Returns file extension for a content type string. If no suitable extensions * are found, 'bin' is used as the default extension diff --git a/node_modules/nodemailer/dist/cjs/mime-node/index.js b/node_modules/nodemailer/dist/cjs/mime-node/index.js index 03c5381d..97e5ab14 100644 --- a/node_modules/nodemailer/dist/cjs/mime-node/index.js +++ b/node_modules/nodemailer/dist/cjs/mime-node/index.js @@ -1294,6 +1294,7 @@ class MimeNode { * * @param addresses An array of address objects * @param [uniqueList] An array to be populated with addresses + * @param [seenAddresses] Addresses already added to uniqueList, shared with recursive calls to keep deduplication linear * @return address string * @internal */ @@ -1431,7 +1432,7 @@ class MimeNode { /** * If needed, mime encodes the name part * - * @param name Name part of an address + * @param value Name part of an address * @returns Mime word encoded string if needed * @internal */ diff --git a/node_modules/nodemailer/dist/cjs/nodemailer.d.ts b/node_modules/nodemailer/dist/cjs/nodemailer.d.ts index 8552d000..baad0139 100644 --- a/node_modules/nodemailer/dist/cjs/nodemailer.d.ts +++ b/node_modules/nodemailer/dist/cjs/nodemailer.d.ts @@ -75,11 +75,22 @@ export declare function createTransport(transporter?: TransportConfig | Transpor /** * Creates a test account from the Ethereal service (https://ethereal.email) * - * @param apiUrl Optional API endpoint, defaults to https://api.nodemailer.com - * @param callback Callback function to run with the account object. If not set, a Promise is returned + * @param callback Callback function to run with the account object */ export declare function createTestAccount(callback: TestAccountCallback): void; +/** + * Creates a test account from the Ethereal service (https://ethereal.email) + * + * @param apiUrl API endpoint, defaults to https://api.nodemailer.com + * @param callback Callback function to run with the account object + */ export declare function createTestAccount(apiUrl: string | false | null | undefined, callback: TestAccountCallback): void; +/** + * Creates a test account from the Ethereal service (https://ethereal.email) + * + * @param [apiUrl] API endpoint, defaults to https://api.nodemailer.com + * @returns Promise that resolves with the account object + */ export declare function createTestAccount(apiUrl?: string | false | null): Promise; /** * Resolves the Ethereal web URL for a message sent through an Ethereal test account diff --git a/node_modules/nodemailer/dist/cjs/package-info.d.ts b/node_modules/nodemailer/dist/cjs/package-info.d.ts index 5a97db40..89eee5e0 100644 --- a/node_modules/nodemailer/dist/cjs/package-info.d.ts +++ b/node_modules/nodemailer/dist/cjs/package-info.d.ts @@ -1,3 +1,3 @@ export declare const name = "nodemailer"; -export declare const version = "10.0.13"; +export declare const version = "10.0.14"; export declare const homepage = "https://nodemailer.com/"; diff --git a/node_modules/nodemailer/dist/cjs/package-info.js b/node_modules/nodemailer/dist/cjs/package-info.js index 6822cb55..721b8bff 100644 --- a/node_modules/nodemailer/dist/cjs/package-info.js +++ b/node_modules/nodemailer/dist/cjs/package-info.js @@ -3,5 +3,5 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.homepage = exports.version = exports.name = void 0; exports.name = 'nodemailer'; -exports.version = '10.0.13'; +exports.version = '10.0.14'; exports.homepage = 'https://nodemailer.com/'; diff --git a/node_modules/nodemailer/dist/cjs/punycode/index.js b/node_modules/nodemailer/dist/cjs/punycode/index.js index 43b1524e..cbbbbdd6 100644 --- a/node_modules/nodemailer/dist/cjs/punycode/index.js +++ b/node_modules/nodemailer/dist/cjs/punycode/index.js @@ -178,6 +178,7 @@ const basicToDigit = function (codePoint) { * @see `basicToDigit()` * @private * @param digit The numeric value of a basic code point. + * @param flag Non-zero to use the uppercase form of the code point. * @returns The basic code point whose value (when used for * representing integers) is `digit`, which needs to be in the range * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is diff --git a/node_modules/nodemailer/dist/cjs/ses-transport/index.d.ts b/node_modules/nodemailer/dist/cjs/ses-transport/index.d.ts index 26432d54..8398dec5 100644 --- a/node_modules/nodemailer/dist/cjs/ses-transport/index.d.ts +++ b/node_modules/nodemailer/dist/cjs/ses-transport/index.d.ts @@ -85,9 +85,14 @@ declare class SESTransport extends EventEmitter { /** * Verifies SES configuration * - * @param callback Callback function + * @returns Promise that resolves to true if the configuration is usable */ verify(): Promise; + /** + * Verifies SES configuration + * + * @param callback Callback function + */ verify(callback: VerifyCallback): void; } /** diff --git a/node_modules/nodemailer/dist/cjs/shared/index.d.ts b/node_modules/nodemailer/dist/cjs/shared/index.d.ts index 3aeb3e79..805783ef 100644 --- a/node_modules/nodemailer/dist/cjs/shared/index.d.ts +++ b/node_modules/nodemailer/dist/cjs/shared/index.d.ts @@ -187,6 +187,7 @@ export declare const parseConnectionUrl: (str?: string | null) => ConnectionUrlO * creates a default console logger * * @param [options] Options object that might include 'logger' value + * @param [defaults] Fields merged into every log entry, overridden by the fields of the entry itself * @return bunyan compatible logger */ export declare const getLogger: (options?: GetLoggerOptions, defaults?: LogEntry) => Logger; @@ -210,18 +211,41 @@ export declare const parseDataURI: (uri: unknown) => ParsedDataURI | null; * * @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands * @param key Property name or an Array index - * @param [options] Optional access policy: { disableFileAccess, disableUrlAccess } * @param callback Callback function with (err, value) */ export declare function resolveContent(data: { [key: string]: any; }, key: string | number, callback: ResolveContentCallback): void; +/** + * Resolves a String or a Buffer value for content value + * + * @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands + * @param key Property name or an Array index + * @param options Access policy: { disableFileAccess, disableUrlAccess } + * @param callback Callback function with (err, value) + */ export declare function resolveContent(data: { [key: string]: any; }, key: string | number, options: ResolveContentOptions | false | undefined, callback: ResolveContentCallback): void; +/** + * Resolves a String or a Buffer value for content value + * + * @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands + * @param key Property name or an Array index + * @param [options] Optional access policy: { disableFileAccess, disableUrlAccess } + * @returns Promise that resolves with the value + */ export declare function resolveContent(data: { [key: string]: any; }, key: string | number, options?: ResolveContentOptions | false): Promise; +/** + * Resolves a String or a Buffer value for content value + * + * @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands + * @param key Property name or an Array index + * @param options Access policy: { disableFileAccess, disableUrlAccess } + * @param callback Callback function with (err, value), a Promise is returned if not set + */ export declare function resolveContent(data: { [key: string]: any; }, key: string | number, options: ResolveContentOptions | false | undefined, callback: ResolveContentCallback | undefined): Promise | void; diff --git a/node_modules/nodemailer/dist/cjs/shared/index.js b/node_modules/nodemailer/dist/cjs/shared/index.js index 2f987f0e..a78ae8a5 100644 --- a/node_modules/nodemailer/dist/cjs/shared/index.js +++ b/node_modules/nodemailer/dist/cjs/shared/index.js @@ -362,6 +362,7 @@ exports._logFunc = _logFunc; * creates a default console logger * * @param [options] Options object that might include 'logger' value + * @param [defaults] Fields merged into every log entry, overridden by the fields of the entry itself * @return bunyan compatible logger */ const getLogger = (options, defaults) => { diff --git a/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts b/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts index f0f9a481..934d5cbd 100644 --- a/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts +++ b/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.d.ts @@ -21,13 +21,21 @@ export type HttpProxyClientCallback = Callback; * socket.write("GET / HTTP/1.0\r\n\r\n"); * }); * - * @param proxyUrl proxy configuration, etg "http://proxy.host:3128/" + * @param proxyUrl proxy configuration, e.g. "http://proxy.host:3128/" * @param destinationPort Port to open in destination host * @param destinationHost Destination hostname - * @param [tlsOptions] Optional TLS options for an HTTPS proxy (e.g. { rejectUnauthorized: false }) - * @param callback Callback to run with the rocket object once connection is established + * @param callback Callback to run with the socket object once connection is established */ declare function httpProxyClient(proxyUrl: string, destinationPort: number | string, destinationHost: string, callback: HttpProxyClientCallback): void; +/** + * Establishes proxied connection to destinationPort through an HTTPS proxy + * + * @param proxyUrl proxy configuration, e.g. "https://proxy.host:3128/" + * @param destinationPort Port to open in destination host + * @param destinationHost Destination hostname + * @param tlsOptions TLS options for the proxy connection (e.g. { rejectUnauthorized: false }) + * @param callback Callback to run with the socket object once connection is established + */ declare function httpProxyClient(proxyUrl: string, destinationPort: number | string, destinationHost: string, tlsOptions: HttpProxyClientOptions | undefined, callback: HttpProxyClientCallback): void; /** * Socket timeout in milliseconds while the CONNECT handshake is in progress, defaults to 30 seconds. diff --git a/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js b/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js index 05da36f9..0ca39702 100644 --- a/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js +++ b/node_modules/nodemailer/dist/cjs/smtp-connection/http-proxy-client.js @@ -126,18 +126,28 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, .join('\r\n') + // End request '\r\n\r\n'); - let headers = ''; + // The response is collected as chunks and only the bytes that just arrived, together + // with the three before them, are searched for the end of the headers. Appending to a + // string and searching all of it again re-read the whole response on every chunk. + const chunks = []; + let received = 0; + let tail = ''; const onSocketData = (chunk) => { let match; - let remainder; if (finished) { return; } - headers += chunk.toString('binary'); - if ((match = headers.match(/\r\n\r\n/))) { + const window = tail + chunk.toString('binary'); + const windowEnd = window.indexOf('\r\n\r\n'); + chunks.push(chunk); + received += chunk.length; + tail = window.slice(-3); + if (windowEnd >= 0) { socket.removeListener('data', onSocketData); - remainder = headers.substr(match.index + match[0].length); - headers = headers.substr(0, match.index); + const headerEnd = received - window.length + windowEnd; + const response = Buffer.concat(chunks, received).toString('binary'); + const headers = response.substr(0, headerEnd); + const remainder = response.substr(headerEnd + 4); if (remainder) { socket.unshift(Buffer.from(remainder, 'binary')); } @@ -161,7 +171,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, socket.setTimeout(0); return done(null, socket); } - if (headers.length > MAX_RESPONSE_HEADER_BYTES) { + if (received > MAX_RESPONSE_HEADER_BYTES) { socket.removeListener('data', onSocketData); const err = new Error('Proxy response headers too large'); err.code = errors.EPROXY; diff --git a/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts b/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts index 87a5e643..a14a5903 100644 --- a/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts +++ b/node_modules/nodemailer/dist/cjs/smtp-connection/index.d.ts @@ -379,7 +379,7 @@ declare class SMTPConnection extends EventEmitter { * * @param envelope Envelope object, {from: addr, to: [addr]} * @param message String, Buffer or a Stream - * @param callback Callback to return once sending is completed + * @param done Callback to return once sending is completed */ send(envelope: SMTPEnvelope, message: string | Buffer | Readable, done: SMTPConnectionSendCallback): void; /** diff --git a/node_modules/nodemailer/dist/cjs/smtp-connection/index.js b/node_modules/nodemailer/dist/cjs/smtp-connection/index.js index 2b06a180..c02906ae 100644 --- a/node_modules/nodemailer/dist/cjs/smtp-connection/index.js +++ b/node_modules/nodemailer/dist/cjs/smtp-connection/index.js @@ -614,7 +614,7 @@ class SMTPConnection extends node_events_1.EventEmitter { * * @param envelope Envelope object, {from: addr, to: [addr]} * @param message String, Buffer or a Stream - * @param callback Callback to return once sending is completed + * @param done Callback to return once sending is completed */ send(envelope, message, done) { // ensure that the callback is only called once. The public callback type has a @@ -827,6 +827,8 @@ class SMTPConnection extends node_events_1.EventEmitter { * @event * @param err Error object * @param type Error name + * @param data Server response that triggered the error, false if there is none + * @param command SMTP command that was in flight * @internal */ _onError(err, type, data, command) { @@ -1095,6 +1097,7 @@ class SMTPConnection extends node_events_1.EventEmitter { * {from:'...', to:['...']} * or * {from:{address:'...',name:'...'}, to:[address:'...',name:'...']} + * @param callback Callback to run once the envelope is processed * @internal */ _setEnvelope(envelope, callback) { @@ -1489,6 +1492,7 @@ class SMTPConnection extends node_events_1.EventEmitter { * hosts invalidly use a longer message than VXNlcm5hbWU6 * * @param str Message from the server + * @param callback Callback to run once the authentication sequence completes * @internal */ _actionAUTH_LOGIN_USER(str, callback) { @@ -1510,6 +1514,7 @@ class SMTPConnection extends node_events_1.EventEmitter { * base64 encoded again. * * @param str Message from the server + * @param callback Callback to run once the authentication sequence completes * @internal */ _actionAUTH_CRAM_MD5(str, callback) { @@ -1534,6 +1539,7 @@ class SMTPConnection extends node_events_1.EventEmitter { * the user can be considered logged in. Start waiting for a message to send * * @param str Message from the server + * @param callback Callback to run once the authentication sequence completes * @internal */ _actionAUTH_CRAM_MD5_PASS(str, callback) { @@ -1555,6 +1561,7 @@ class SMTPConnection extends node_events_1.EventEmitter { * response needs to be base64 encoded password. * * @param str Message from the server + * @param callback Callback to run once the authentication sequence completes * @internal */ _actionAUTH_LOGIN_PASS(str, callback) { @@ -1574,6 +1581,8 @@ class SMTPConnection extends node_events_1.EventEmitter { * the user can be considered logged in. Start waiting for a message to send * * @param str Message from the server + * @param isRetry True if this is a retry after a failed login, or the callback itself + * @param [callback] Callback to run once the authentication sequence completes * @internal */ _actionAUTHComplete(str, isRetry, callback) { @@ -1616,6 +1625,7 @@ class SMTPConnection extends node_events_1.EventEmitter { * Handle response for a MAIL FROM: command * * @param str Message from the server + * @param callback Callback to run once the envelope is processed * @internal */ _actionMAIL(str, callback) { @@ -1644,6 +1654,7 @@ class SMTPConnection extends node_events_1.EventEmitter { * Handle response for a RCPT TO: command * * @param str Message from the server + * @param callback Callback to run once the envelope is processed * @internal */ _actionRCPT(str, callback) { @@ -1695,6 +1706,7 @@ class SMTPConnection extends node_events_1.EventEmitter { * Handle response for a DATA command * * @param str Message from the server + * @param callback Callback to run once the envelope is processed * @internal */ _actionDATA(str, callback) { @@ -1721,6 +1733,7 @@ class SMTPConnection extends node_events_1.EventEmitter { * We expect a single response that defines if the sending succeeded or failed * * @param str Message from the server + * @param callback Callback to run with the final send result * @internal */ _actionSMTPStream(str, callback) { @@ -1737,6 +1750,7 @@ class SMTPConnection extends node_events_1.EventEmitter { * @param recipient The recipient this response applies to * @param final Is this the final recipient? * @param str Message from the server + * @param callback Callback to run with the final send result * @internal */ _actionLMTPStream(recipient, final, str, callback) { diff --git a/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts b/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts index 9502b6e3..1f1aeff0 100644 --- a/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts +++ b/node_modules/nodemailer/dist/cjs/smtp-pool/index.d.ts @@ -110,9 +110,14 @@ declare class SMTPPool extends EventEmitter { /** * Verifies SMTP configuration * - * @param callback Callback function + * @returns Promise that resolves to true if the configuration is usable */ verify(): Promise; + /** + * Verifies SMTP configuration + * + * @param callback Callback function + */ verify(callback: VerifyCallback): void; } /** diff --git a/node_modules/nodemailer/dist/cjs/smtp-transport/index.d.ts b/node_modules/nodemailer/dist/cjs/smtp-transport/index.d.ts index 11bbb3c9..7026ef35 100644 --- a/node_modules/nodemailer/dist/cjs/smtp-transport/index.d.ts +++ b/node_modules/nodemailer/dist/cjs/smtp-transport/index.d.ts @@ -125,9 +125,14 @@ declare class SMTPTransport extends EventEmitter { /** * Verifies SMTP configuration * - * @param callback Callback function + * @returns Promise that resolves to true if the configuration is usable */ verify(): Promise; + /** + * Verifies SMTP configuration + * + * @param callback Callback function + */ verify(callback: VerifyCallback): void; /** * Releases resources diff --git a/node_modules/nodemailer/dist/cjs/well-known/services.js b/node_modules/nodemailer/dist/cjs/well-known/services.js index a4188272..847959ff 100644 --- a/node_modules/nodemailer/dist/cjs/well-known/services.js +++ b/node_modules/nodemailer/dist/cjs/well-known/services.js @@ -314,6 +314,12 @@ exports.services = { "host": "mailosaur.io", "port": 25 }, + "MailSenpai": { + "description": "MailSenpai (SMTP Senpai, EU)", + "host": "relay.mailsenpai.com", + "port": 2525, + "secure": false + }, "Mailtrap": { "description": "Mailtrap", "host": "live.smtp.mailtrap.io", diff --git a/node_modules/nodemailer/dist/esm/addressparser/index.d.ts b/node_modules/nodemailer/dist/esm/addressparser/index.d.ts index 583c45b7..edbeffc5 100644 --- a/node_modules/nodemailer/dist/esm/addressparser/index.d.ts +++ b/node_modules/nodemailer/dist/esm/addressparser/index.d.ts @@ -39,7 +39,6 @@ export type Address = MailboxAddress | GroupAddress; * * @param str Address field * @param options Optional options object - * @param options._depth Internal recursion depth counter (do not set manually) * @return An array of address objects */ declare function addressparser(str: string | null | undefined, options: AddressParserOptions & { diff --git a/node_modules/nodemailer/dist/esm/addressparser/index.js b/node_modules/nodemailer/dist/esm/addressparser/index.js index b3ea5334..64c03e2d 100644 --- a/node_modules/nodemailer/dist/esm/addressparser/index.js +++ b/node_modules/nodemailer/dist/esm/addressparser/index.js @@ -426,7 +426,7 @@ function _handleAddress(tokens, depth) { // Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting) let groupMembers = []; if (data.group.length) { - const parsedGroup = addressparser(data.group.join(','), { _depth: depth + 1 }); + const parsedGroup = _parseAddressList(data.group.join(','), depth + 1); parsedGroup.forEach(member => { if (member.group) { groupMembers = groupMembers.concat(member.group); @@ -513,7 +513,13 @@ function _handleAddress(tokens, depth) { // Join values with spaces data.text = data.text.join(' '); data.address = data.address.join(' '); - if (addressFromQuotedText && data.text) { + if (addressFromQuotedText && data.text.indexOf('@') >= 0) { + // A quoted run with no '@' in it is a display name and nothing else, so it stays + // where it is. Moving it over anyway made a comment the mailbox was read from: + // '"Display Name" <(a comment)>' handed on 'Display Name' as the address with the + // comment as the name, and a quoted name holding a ',' or a ';' became an address + // that reads as two recipients once a consumer writes it back into a header. + // // The mailbox is still sitting in the text, so it moves over here and is quoted // before the recovery below rather than after it. Anything else the text holds // came along with it: a comment ends the domain but leaves the atoms behind it in @@ -560,6 +566,7 @@ class Tokenizer { this.node = null; this.escaped = false; this.inDomainLiteral = false; + this.afterAt = false; this.list = []; /** * Operator tokens and which tokens are expected to end the sequence @@ -603,17 +610,23 @@ class Tokenizer { * Checks if a character is an operator or text and acts accordingly * * @param chr Character from the address field + * @param nextChr Character following chr, null at the end of the field */ checkChar(chr, nextChr) { - // Track RFC 5322 domain-literals ("[" *dtext "]"). Operator characters such - // as the ":" of an IPv6 address-literal (user@[IPv6:2001:db8::1]) are dtext - // and must not be treated as the group delimiter while inside the brackets. - // Quoted strings and comments are handled separately via operatorExpecting, - // so only enter this state when no operator is open. The list separators "," - // and ";" are the exception: they always end the literal (and split the - // address list) so that an unclosed "[" cannot swallow later recipients. + // Track RFC 5322 domain-literals ("[" *dtext "]"). The ":" of an IPv6 + // address-literal (user@[IPv6:2001:db8::1]) is dtext and must not be treated as + // the group delimiter while inside the brackets. That is the only operator the + // literal hides: letting it hide the others as well meant a "[" could turn the + // comment, quoted string or angle-addr after it into text and pick a different + // mailbox, '[ ( ] ) ' yielded a@victim.com where + // RFC 5322 reads b@evil.com. A "[" also only opens a literal right after the "@" + // of an addr-spec, so the group delimiter of a display name holding one stays + // intact. Quoted strings and comments are handled separately via + // operatorExpecting, so only enter this state when no operator is open. The list + // separators "," and ";" always end the literal (and split the address list) so + // that an unclosed "[" cannot swallow later recipients. if (!this.escaped && !this.operatorExpecting) { - if (!this.inDomainLiteral && chr === '[') { + if (!this.inDomainLiteral && chr === '[' && this.afterAt) { this.inDomainLiteral = true; } else if (this.inDomainLiteral && (chr === ']' || chr === ',' || chr === ';')) { @@ -633,17 +646,24 @@ class Tokenizer { } this.list.push(this.node); this.node = null; + if (chr !== ')') { + // a comment is folding whitespace, it does not part the "@" from the "[" + this.afterAt = false; + } this.operatorExpecting = ''; this.escaped = false; return; } - else if (!this.operatorExpecting && !this.inDomainLiteral && chr in this.operators) { + else if (!this.operatorExpecting && !(this.inDomainLiteral && chr === ':') && chr in this.operators) { this.node = { type: 'operator', value: chr }; this.list.push(this.node); this.node = null; + if (chr !== '(') { + this.afterAt = false; + } this.operatorExpecting = this.operators[chr]; this.escaped = false; return; @@ -667,6 +687,11 @@ class Tokenizer { if (chr.charCodeAt(0) >= 0x21 || [' ', '\t'].includes(chr)) { // skip command bytes this.node.value += chr; + // text inside a quoted string or a comment is not part of an addr-spec. Tracked + // as it goes rather than read back off the value (see lastChars in _handleAddress) + if (!this.operatorExpecting && chr !== ' ' && chr !== '\t') { + this.afterAt = chr === '@'; + } } this.escaped = false; } @@ -677,9 +702,19 @@ class Tokenizer { * malicious input that could cause stack overflow. */ const MAX_NESTED_GROUP_DEPTH = 50; -function addressparser(str, options) { - options = options || {}; - const depth = options._depth || 0; +/** + * Parses an address list, recursing into the groups it holds. + * + * The depth is threaded through the calls rather than carried in an options object, so a + * caller supplied one can not seed it and lift the recursion limit: `{ _depth: -1e9 }` + * bought a billion levels of nesting and turned the guard below into the stack overflow it + * is there to prevent. + * + * @param str Address field + * @param depth Current recursion depth for nested group protection + * @return An array of address objects + */ +function _parseAddressList(str, depth) { // Prevent stack overflow from deeply nested groups (DoS protection) if (depth > MAX_NESTED_GROUP_DEPTH) { return []; @@ -688,7 +723,7 @@ function addressparser(str, options) { const tokens = tokenizer.tokenize(); const addresses = []; let address = []; - let parsedAddresses = []; + const parsedAddresses = []; tokens.forEach(token => { if (token.type === 'operator' && (token.value === ',' || token.value === ';')) { if (address.length) { @@ -729,8 +764,11 @@ function addressparser(str, options) { } } mergedAddresses.reverse(); - parsedAddresses = mergedAddresses; - if (options.flatten) { + return mergedAddresses; +} +function addressparser(str, options) { + const parsedAddresses = _parseAddressList(str, 0); + if (options?.flatten) { const flatAddresses = []; const walkAddressList = (list) => { list.forEach(entry => { diff --git a/node_modules/nodemailer/dist/esm/dkim/message-parser.js b/node_modules/nodemailer/dist/esm/dkim/message-parser.js index eccc1b16..03e89397 100644 --- a/node_modules/nodemailer/dist/esm/dkim/message-parser.js +++ b/node_modules/nodemailer/dist/esm/dkim/message-parser.js @@ -1,4 +1,31 @@ import { Transform } from 'node:stream'; +/** + * Drops the SP and HTAB from both ends of a field name. + * + * An index scan rather than `/^[ \t]+|[ \t]+$/g`, which retries the trailing branch at + * every offset of a blank run that is followed by other text: the run is consumed greedily, + * `$` fails, and the engine gives the characters back one at a time before moving on to + * start the same walk one offset further in. A field name carrying a 128 KiB run of spaces + * between two atoms took about 6.7 seconds to trim, and the cost grows with its square. + * + * Not `.trim()`, which takes the other Unicode spaces along: an NBSP in front of a field + * name would come off and select the name into the signed set, where the relaxed + * canonicalization in sign.ts deliberately leaves everything but SP and HTAB alone. + * + * @param str Field name to trim + * @return The field name without the surrounding SP and HTAB + */ +function _trimFieldName(str) { + let start = 0; + let end = str.length; + while (start < end && (str.charCodeAt(start) === 0x20 || str.charCodeAt(start) === 0x09)) { + start++; + } + while (end > start && (str.charCodeAt(end - 1) === 0x20 || str.charCodeAt(end - 1) === 0x09)) { + end--; + } + return str.slice(start, end); +} /** * MessageParser instance is a transform stream that separates message headers * from the rest of the body. Headers are emitted with the 'headers' event. Message @@ -141,10 +168,7 @@ class MessageParser extends Transform { return lines .filter(line => /[^ \t\r]/.test(line)) .map(line => ({ - key: line - .substr(0, line.indexOf(':')) - .replace(/^[ \t]+|[ \t]+$/g, '') - .toLowerCase(), + key: _trimFieldName(line.substr(0, line.indexOf(':'))).toLowerCase(), line })); } diff --git a/node_modules/nodemailer/dist/esm/dkim/sign.d.ts b/node_modules/nodemailer/dist/esm/dkim/sign.d.ts index a1e4beda..18b6b02b 100644 --- a/node_modules/nodemailer/dist/esm/dkim/sign.d.ts +++ b/node_modules/nodemailer/dist/esm/dkim/sign.d.ts @@ -36,6 +36,7 @@ export interface DKIMRelaxedHeaders { * Returns DKIM signature header line * * @param headers Parsed headers object from MessageParser + * @param hashAlgo Hash algorithm the body hash was calculated with, for example "sha256" * @param bodyHash Base64 encoded hash of the message * @param options DKIM options * @param options.domainName Domain name to be signed for diff --git a/node_modules/nodemailer/dist/esm/dkim/sign.js b/node_modules/nodemailer/dist/esm/dkim/sign.js index 85f843c6..bf42edc1 100644 --- a/node_modules/nodemailer/dist/esm/dkim/sign.js +++ b/node_modules/nodemailer/dist/esm/dkim/sign.js @@ -14,6 +14,7 @@ function unsupportedHashAlgoError(hashAlgo) { * Returns DKIM signature header line * * @param headers Parsed headers object from MessageParser + * @param hashAlgo Hash algorithm the body hash was calculated with, for example "sha256" * @param bodyHash Base64 encoded hash of the message * @param options DKIM options * @param options.domainName Domain name to be signed for diff --git a/node_modules/nodemailer/dist/esm/mailer/index.d.ts b/node_modules/nodemailer/dist/esm/mailer/index.d.ts index 58684f49..cdecd501 100644 --- a/node_modules/nodemailer/dist/esm/mailer/index.d.ts +++ b/node_modules/nodemailer/dist/esm/mailer/index.d.ts @@ -193,9 +193,15 @@ declare class Mail; + /** + * Sends an email using the preselected transport object + * + * @param data E-data description + * @param callback Callback to run once the sending succeeded or failed + */ sendMail(data: SendMailOptions, callback: SendMailCallback): void; getVersionString(): string; /** diff --git a/node_modules/nodemailer/dist/esm/mime-funcs/index.d.ts b/node_modules/nodemailer/dist/esm/mime-funcs/index.d.ts index a9a79e72..c4278fa6 100644 --- a/node_modules/nodemailer/dist/esm/mime-funcs/index.d.ts +++ b/node_modules/nodemailer/dist/esm/mime-funcs/index.d.ts @@ -59,6 +59,7 @@ export declare function quoteString(value?: string): string; * no need to encode the values in any way. If the value is plaintext but has * longer lines then allowed, then use format=flowed * + * @param str Multi line string to check * @param lineLength Max line length to check for * @returns Returns true if there is at least one line longer than lineLength chars */ @@ -100,9 +101,9 @@ export declare function buildHeaderValue(structured: StructuredHeaderValue): str * title*0*=utf-8''unicode * title*1*=%20string * + * @param key Parameter name the generated keys are built from, for example title * @param data String to be encoded * @param [maxLength=50] Max length for generated chunks - * @param [fromCharset='UTF-8'] Source sharacter set * @return A list of encoded keys and headers */ export declare function buildHeaderParam(key: string, data: string | Buffer, maxLength?: number): EncodedHeaderParam[]; diff --git a/node_modules/nodemailer/dist/esm/mime-funcs/index.js b/node_modules/nodemailer/dist/esm/mime-funcs/index.js index ef1a5556..24e5fb0e 100644 --- a/node_modules/nodemailer/dist/esm/mime-funcs/index.js +++ b/node_modules/nodemailer/dist/esm/mime-funcs/index.js @@ -39,6 +39,7 @@ export function quoteString(value) { * no need to encode the values in any way. If the value is plaintext but has * longer lines then allowed, then use format=flowed * + * @param str Multi line string to check * @param lineLength Max line length to check for * @returns Returns true if there is at least one line longer than lineLength chars */ @@ -185,7 +186,10 @@ export function buildHeaderValue(structured) { } }); } - else if (/[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) { + else if (!value.length || /[\s'"\\;:/=(),<>@[\]?]|^-/.test(value)) { + // a parameter value is a token or a quoted-string and a token is never empty, so + // a valueless parameter such as the 'flag' of 'multipart/mixed; flag; boundary=b' + // goes out as 'flag=""' rather than as the 'flag=' that parses as neither paramsArray.push(param + '=' + JSON.stringify(value)); } else { @@ -207,9 +211,9 @@ export function buildHeaderValue(structured) { * title*0*=utf-8''unicode * title*1*=%20string * + * @param key Parameter name the generated keys are built from, for example title * @param data String to be encoded * @param [maxLength=50] Max length for generated chunks - * @param [fromCharset='UTF-8'] Source sharacter set * @return A list of encoded keys and headers */ export function buildHeaderParam(key, data, maxLength) { @@ -336,6 +340,26 @@ export function buildHeaderParam(key, data, maxLength) { value: item.line })); } +/** + * An RFC 2045 token: printable ASCII without SPACE, the control characters, DEL and the + * tspecials. A charset name is one, and the name a continuation carries is written into + * the encoded word the parameter value becomes, so it is checked against this before it + * goes in. Whitespace used to come off it only because the value was trimmed first. + */ +const TOKEN = /^[^\x00-\x20\x7f()<>@,;:\\"/[\]?=]+$/; +/** + * Whether a string can be a header parameter name. + * + * A parameter name is a token, so it is never empty. A "__proto__" name would target the + * prototype chain of the params object instead of an own property of it and read back as + * Object.prototype, so it is no name either. + * + * @param name Candidate parameter name, already lowercased + * @return true when the name can be used + */ +function _isParamName(name) { + return !!name && !isProtoKey(name); +} /** * Parses a header value with key=value arguments into a structured * object. @@ -356,134 +380,224 @@ export function parseHeaderValue(str) { value: '', params: {} }; - // Parameter names come from a caller supplied contentType/contentDisposition. A - // "__proto__" name would target the prototype chain of the params object instead of - // an own property of it, and read back as Object.prototype, so it is dropped. + // A duplicated parameter resolves to its first occurrence, the way a duplicated header + // does. Letting the last one win disagrees with the receivers that take the first, and + // the two readings of 'boundary="b"; boundary="c"' name different delimiters. The + // continuation join below is the only writer of the name it builds, so it tests the + // name with _isParamName directly rather than taking that rule along from here. const setParam = (name, value) => { - if (!isProtoKey(name)) { + name = name.toLowerCase(); + if (_isParamName(name) && !Object.prototype.hasOwnProperty.call(response.params, name)) { response.params[name] = value; } }; let key = false; let value = ''; - let type = 'value'; + let stage = 'value'; let quote = false; let escaped = false; let chr; + // Whitespace seen outside a quoted string is held back until a significant character + // follows it, so the whitespace around a value is dropped without trimming spaces the + // sender quoted on purpose. Trimming the stored value instead loses the trailing space + // of 'filename*0="Annual Report "', which the next continuation section is appended to. + let pendingSpace = ''; + let quoteClosed = false; + // Whitespace ahead of the first character of a value is padding and is dropped, the + // whitespace between two characters of it is content + const flushSpace = () => { + if (value.length) { + value += pendingSpace; + } + pendingSpace = ''; + }; + const addChr = (c) => { + flushSpace(); + value += c; + }; + const takeValue = () => { + const taken = value; + value = ''; + pendingSpace = ''; + quoteClosed = false; + return taken; + }; + const storeValue = () => { + const taken = takeValue(); + if (key === false) { + response.value = taken; + } + else { + setParam(key, taken); + } + }; + // A parameter name with no '=' is a valueless parameter, not the start of the next one. + // Without this the name keeps growing across the ';' and swallows whatever follows, which + // is how 'multipart/mixed; flag; boundary="AAA"' lost its boundary to a parameter named + // 'flag; boundary' and left the node declaring the generated boundary beside the asked + // for one, so a receiver reading the first of the two found no delimiter it matched. + const storeEmptyKey = () => { + setParam(takeValue().trim(), ''); + }; for (let i = 0, len = str.length; i < len; i++) { chr = str.charAt(i); - if (type === 'key') { + if (stage === 'key') { if (chr === '=') { - key = value.trim().toLowerCase(); - type = 'value'; - value = ''; + key = takeValue().trim(); + stage = 'value'; + continue; + } + if (chr === ';') { + storeEmptyKey(); continue; } value += chr; } else { - if (escaped) { - value += chr; + if (quoteClosed && chr !== ';') { + // Nothing behind a closed quoted string reaches the value. RFC 2045 says a + // parameter value is a token or a quoted string and not both, so what follows + // one is junk and only the ';' that ends the parameter still counts. Tested + // ahead of the branches rather than beside the append at the foot of them, + // where each branch above was a way around it: a second '"' reopened quoting + // and swallowed the rest of the header, so 'boundary="AAA" "; boundary=BBB"' + // read as a single boundary of 'AAA ', and the junk of + // 'boundary="AAA" (unterminated comment' still joined the declared boundary + // through the escape branch. quoteClosed is only ever set while no quote is + // open, and this is what keeps one from being opened afterwards. + escaped = false; + continue; } - else if (chr === '\\') { + if (escaped) { + addChr(chr); + } + else if (quote && chr === '\\') { + // a backslash only escapes inside a quoted string, everywhere else it is an + // ordinary character. Treating it as an escape turns the parameter value + // 'C:\Users\me\report.txt' into 'C:Usersmereport.txt' escaped = true; continue; } else if (quote && chr === quote) { quote = false; + quoteClosed = true; } else if (!quote && chr === '"') { quote = chr; + flushSpace(); } else if (!quote && chr === ';') { - if (key === false) { - response.value = value.trim(); - } - else { - setParam(key, value.trim()); - } - type = 'key'; - value = ''; + storeValue(); + stage = 'key'; + } + else if (!quote && (chr === ' ' || chr === '\t')) { + pendingSpace += chr; } else { - value += chr; + addChr(chr); } escaped = false; } } - if (type === 'value') { - if (key === false) { - response.value = value.trim(); - } - else { - setParam(key, value.trim()); - } + if (stage === 'value') { + storeValue(); } - else if (value.trim()) { - setParam(value.trim().toLowerCase(), ''); + else { + // a key with no value, as in 'Header-Key: somevalue; key=value; emptykey' + storeEmptyKey(); } // handle parameter value continuations // https://tools.ietf.org/html/rfc2231#section-3 - // preprocess values + // Sections are collected in a list and ordered below rather than written into an array + // at their own section number. An index write makes the array as long as the number the + // header asked for, and the join that follows walks all of it, so the 55 byte value + // "attachment; filename*0*=utf-8''a; filename*4000000000=b" held a core for over two + // minutes. + const continuations = new Map(); Object.keys(response.params).forEach(key => { - let actualKey, nr, match, value; - if ((match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/))) { - actualKey = key.substr(0, match.index); - nr = Number(match[2] || match[3]) || 0; - if (isProtoKey(actualKey)) { - // see setParam. Reading it back would yield Object.prototype, which is - // an object, so the initializer below would be skipped and the write - // that follows would throw out of a header build the caller can not catch - delete response.params[key]; - return; - } - if (!response.params[actualKey] || typeof response.params[actualKey] !== 'object') { - response.params[actualKey] = { - charset: false, - values: [] - }; - } - value = response.params[key]; - if (nr === 0 && match[0].substr(-1) === '*' && (match = value.match(/^([^']*)'[^']*'(.*)$/))) { - response.params[actualKey].charset = match[1] || 'iso-8859-1'; - value = match[2]; - } - response.params[actualKey].values[nr] = value; - // remove the old reference - delete response.params[key]; + const match = key.match(/(\*(\d+)|\*(\d+)\*|\*)$/); + if (!match) { + // not a continuation parameter, there is nothing to join + return; } + const actualKey = key.substr(0, match.index); + const nr = Number(match[2] || match[3]) || 0; + // RFC 2231 section 4.1: only a section whose name ends in '*' is percent encoded + const encoded = match[0].substr(-1) === '*'; + // remove the old reference + let value = response.params[key]; + delete response.params[key]; + if (!_isParamName(actualKey)) { + // the joined value can not be written back under this name. It is empty when the + // continuation suffix was all there was of it, as in the bare '*' of 'text/plain; a;*' + return; + } + let continuation = continuations.get(actualKey); + if (!continuation) { + continuation = { charset: false, sections: [] }; + continuations.set(actualKey, continuation); + } + const charsetMatch = nr === 0 && encoded ? value.match(/^([^']*)'[^']*'(.*)$/) : null; + if (charsetMatch) { + // the charset is a token, and anything else named as one is no charset a consumer + // could resolve, so it reads as the unnamed case rather than being carried into + // the encoded word below. A "\r\n" of a prefix would otherwise reach a consumer + // of the parsed value as the charset of a word that no decoder can act on + continuation.charset = TOKEN.test(charsetMatch[1]) ? charsetMatch[1] : 'iso-8859-1'; + value = charsetMatch[2]; + } + continuation.sections.push({ nr, value, encoded }); }); // concatenate split rfc2231 strings and convert encoded strings to mime encoded words - Object.keys(response.params).forEach(key => { - let value; - if (response.params[key] && Array.isArray(response.params[key].values)) { - value = response.params[key].values.map((val) => val || '').join(''); - if (response.params[key].charset) { - // convert "%AB" to "=?charset?Q?=AB?=" - response.params[key] = - '=?' + - response.params[key].charset + - '?Q?' + - value - // fix invalidly encoded chars - .replace(/[=?_\s]/g, s => { - const c = s.charCodeAt(0).toString(16); - if (s === ' ') { - return '_'; - } - return '%' + (c.length < 2 ? '0' : '') + c; - }) - // change from urlencoding to percent encoding - .replace(/%/g, '=') + - '?='; - } - else { - response.params[key] = value; - } + continuations.forEach((continuation, key) => { + if (Object.prototype.hasOwnProperty.call(response.params, key)) { + // The same name was also given as a plain parameter, which the starred keys were + // just deleted from around, so this write would be the only one in the function + // to override a name already taken. 'filename=plain.txt; filename*0=evil.txt' + // resolves to the plain parameter either way round, so the reading does not come + // down to which of the two spellings the sender put first + return; } + continuation.sections.sort((a, b) => a.nr - b.nr); + if (!continuation.charset) { + // nothing said which charset the percent escapes of an encoded section are in, + // so every section is passed on as the text it already is + response.params[key] = continuation.sections.map(section => section.value).join(''); + return; + } + // convert "%AB" to "=?charset?Q?=AB?=" + response.params[key] = '=?' + continuation.charset + '?Q?' + continuation.sections.map(_encodeContinuationSection).join('') + '?='; }); return response; } +/** + * Renders one parameter value continuation section as the payload of a Q encoded word. + * + * A section whose name ends in '*' is percent encoded and its escapes carry the bytes of + * the value, so they only have to be rewritten into the "=AB" spelling a Q encoded word + * uses. A section without the '*' is literal text (RFC 2231 section 4.1), so its '%' is a + * '%' and is escaped along with the characters a Q encoded word can not carry bare. + * Decoding a literal section invents bytes that never appeared on the wire: it is how the + * value 'filename*0*=utf-8''safe; filename*1=%2F..%2F..%2Fetc%2Fpasswd' was emitted as a + * filename every receiving client reads back as 'safe/../../etc/passwd'. + * + * @param section One collected continuation section + * @return The section as Q encoded word payload + */ +function _encodeContinuationSection(section) { + const specials = section.encoded ? /[=?_\s]/g : /[=?_\s%]/g; + return (section.value + // fix invalidly encoded chars + .replace(specials, s => { + const c = s.charCodeAt(0).toString(16); + if (s === ' ') { + return '_'; + } + return '%' + (c.length < 2 ? '0' : '') + c; + }) + // change from urlencoding to percent encoding + .replace(/%/g, '=')); +} /** * Returns file extension for a content type string. If no suitable extensions * are found, 'bin' is used as the default extension diff --git a/node_modules/nodemailer/dist/esm/mime-node/index.js b/node_modules/nodemailer/dist/esm/mime-node/index.js index 314a6cc3..40f8eed8 100644 --- a/node_modules/nodemailer/dist/esm/mime-node/index.js +++ b/node_modules/nodemailer/dist/esm/mime-node/index.js @@ -1256,6 +1256,7 @@ class MimeNode { * * @param addresses An array of address objects * @param [uniqueList] An array to be populated with addresses + * @param [seenAddresses] Addresses already added to uniqueList, shared with recursive calls to keep deduplication linear * @return address string * @internal */ @@ -1393,7 +1394,7 @@ class MimeNode { /** * If needed, mime encodes the name part * - * @param name Name part of an address + * @param value Name part of an address * @returns Mime word encoded string if needed * @internal */ diff --git a/node_modules/nodemailer/dist/esm/nodemailer.d.ts b/node_modules/nodemailer/dist/esm/nodemailer.d.ts index 8552d000..baad0139 100644 --- a/node_modules/nodemailer/dist/esm/nodemailer.d.ts +++ b/node_modules/nodemailer/dist/esm/nodemailer.d.ts @@ -75,11 +75,22 @@ export declare function createTransport(transporter?: TransportConfig | Transpor /** * Creates a test account from the Ethereal service (https://ethereal.email) * - * @param apiUrl Optional API endpoint, defaults to https://api.nodemailer.com - * @param callback Callback function to run with the account object. If not set, a Promise is returned + * @param callback Callback function to run with the account object */ export declare function createTestAccount(callback: TestAccountCallback): void; +/** + * Creates a test account from the Ethereal service (https://ethereal.email) + * + * @param apiUrl API endpoint, defaults to https://api.nodemailer.com + * @param callback Callback function to run with the account object + */ export declare function createTestAccount(apiUrl: string | false | null | undefined, callback: TestAccountCallback): void; +/** + * Creates a test account from the Ethereal service (https://ethereal.email) + * + * @param [apiUrl] API endpoint, defaults to https://api.nodemailer.com + * @returns Promise that resolves with the account object + */ export declare function createTestAccount(apiUrl?: string | false | null): Promise; /** * Resolves the Ethereal web URL for a message sent through an Ethereal test account diff --git a/node_modules/nodemailer/dist/esm/package-info.d.ts b/node_modules/nodemailer/dist/esm/package-info.d.ts index 5a97db40..89eee5e0 100644 --- a/node_modules/nodemailer/dist/esm/package-info.d.ts +++ b/node_modules/nodemailer/dist/esm/package-info.d.ts @@ -1,3 +1,3 @@ export declare const name = "nodemailer"; -export declare const version = "10.0.13"; +export declare const version = "10.0.14"; export declare const homepage = "https://nodemailer.com/"; diff --git a/node_modules/nodemailer/dist/esm/package-info.js b/node_modules/nodemailer/dist/esm/package-info.js index 806eb69b..88056f0d 100644 --- a/node_modules/nodemailer/dist/esm/package-info.js +++ b/node_modules/nodemailer/dist/esm/package-info.js @@ -1,4 +1,4 @@ // Generated by scripts/build.js from package.json. Do not edit by hand. export const name = 'nodemailer'; -export const version = '10.0.13'; +export const version = '10.0.14'; export const homepage = 'https://nodemailer.com/'; diff --git a/node_modules/nodemailer/dist/esm/punycode/index.js b/node_modules/nodemailer/dist/esm/punycode/index.js index f5ef7554..b0217474 100644 --- a/node_modules/nodemailer/dist/esm/punycode/index.js +++ b/node_modules/nodemailer/dist/esm/punycode/index.js @@ -175,6 +175,7 @@ const basicToDigit = function (codePoint) { * @see `basicToDigit()` * @private * @param digit The numeric value of a basic code point. + * @param flag Non-zero to use the uppercase form of the code point. * @returns The basic code point whose value (when used for * representing integers) is `digit`, which needs to be in the range * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is diff --git a/node_modules/nodemailer/dist/esm/ses-transport/index.d.ts b/node_modules/nodemailer/dist/esm/ses-transport/index.d.ts index 26432d54..8398dec5 100644 --- a/node_modules/nodemailer/dist/esm/ses-transport/index.d.ts +++ b/node_modules/nodemailer/dist/esm/ses-transport/index.d.ts @@ -85,9 +85,14 @@ declare class SESTransport extends EventEmitter { /** * Verifies SES configuration * - * @param callback Callback function + * @returns Promise that resolves to true if the configuration is usable */ verify(): Promise; + /** + * Verifies SES configuration + * + * @param callback Callback function + */ verify(callback: VerifyCallback): void; } /** diff --git a/node_modules/nodemailer/dist/esm/shared/index.d.ts b/node_modules/nodemailer/dist/esm/shared/index.d.ts index 3aeb3e79..805783ef 100644 --- a/node_modules/nodemailer/dist/esm/shared/index.d.ts +++ b/node_modules/nodemailer/dist/esm/shared/index.d.ts @@ -187,6 +187,7 @@ export declare const parseConnectionUrl: (str?: string | null) => ConnectionUrlO * creates a default console logger * * @param [options] Options object that might include 'logger' value + * @param [defaults] Fields merged into every log entry, overridden by the fields of the entry itself * @return bunyan compatible logger */ export declare const getLogger: (options?: GetLoggerOptions, defaults?: LogEntry) => Logger; @@ -210,18 +211,41 @@ export declare const parseDataURI: (uri: unknown) => ParsedDataURI | null; * * @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands * @param key Property name or an Array index - * @param [options] Optional access policy: { disableFileAccess, disableUrlAccess } * @param callback Callback function with (err, value) */ export declare function resolveContent(data: { [key: string]: any; }, key: string | number, callback: ResolveContentCallback): void; +/** + * Resolves a String or a Buffer value for content value + * + * @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands + * @param key Property name or an Array index + * @param options Access policy: { disableFileAccess, disableUrlAccess } + * @param callback Callback function with (err, value) + */ export declare function resolveContent(data: { [key: string]: any; }, key: string | number, options: ResolveContentOptions | false | undefined, callback: ResolveContentCallback): void; +/** + * Resolves a String or a Buffer value for content value + * + * @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands + * @param key Property name or an Array index + * @param [options] Optional access policy: { disableFileAccess, disableUrlAccess } + * @returns Promise that resolves with the value + */ export declare function resolveContent(data: { [key: string]: any; }, key: string | number, options?: ResolveContentOptions | false): Promise; +/** + * Resolves a String or a Buffer value for content value + * + * @param data An object or an Array you want to resolve an element for, see ContentDescriptor for the values it understands + * @param key Property name or an Array index + * @param options Access policy: { disableFileAccess, disableUrlAccess } + * @param callback Callback function with (err, value), a Promise is returned if not set + */ export declare function resolveContent(data: { [key: string]: any; }, key: string | number, options: ResolveContentOptions | false | undefined, callback: ResolveContentCallback | undefined): Promise | void; diff --git a/node_modules/nodemailer/dist/esm/shared/index.js b/node_modules/nodemailer/dist/esm/shared/index.js index 6e01921f..95ef4030 100644 --- a/node_modules/nodemailer/dist/esm/shared/index.js +++ b/node_modules/nodemailer/dist/esm/shared/index.js @@ -318,6 +318,7 @@ export const _logFunc = (logger, level, defaults, data, message, ...args) => { * creates a default console logger * * @param [options] Options object that might include 'logger' value + * @param [defaults] Fields merged into every log entry, overridden by the fields of the entry itself * @return bunyan compatible logger */ export const getLogger = (options, defaults) => { diff --git a/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts b/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts index f0f9a481..934d5cbd 100644 --- a/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts +++ b/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.d.ts @@ -21,13 +21,21 @@ export type HttpProxyClientCallback = Callback; * socket.write("GET / HTTP/1.0\r\n\r\n"); * }); * - * @param proxyUrl proxy configuration, etg "http://proxy.host:3128/" + * @param proxyUrl proxy configuration, e.g. "http://proxy.host:3128/" * @param destinationPort Port to open in destination host * @param destinationHost Destination hostname - * @param [tlsOptions] Optional TLS options for an HTTPS proxy (e.g. { rejectUnauthorized: false }) - * @param callback Callback to run with the rocket object once connection is established + * @param callback Callback to run with the socket object once connection is established */ declare function httpProxyClient(proxyUrl: string, destinationPort: number | string, destinationHost: string, callback: HttpProxyClientCallback): void; +/** + * Establishes proxied connection to destinationPort through an HTTPS proxy + * + * @param proxyUrl proxy configuration, e.g. "https://proxy.host:3128/" + * @param destinationPort Port to open in destination host + * @param destinationHost Destination hostname + * @param tlsOptions TLS options for the proxy connection (e.g. { rejectUnauthorized: false }) + * @param callback Callback to run with the socket object once connection is established + */ declare function httpProxyClient(proxyUrl: string, destinationPort: number | string, destinationHost: string, tlsOptions: HttpProxyClientOptions | undefined, callback: HttpProxyClientCallback): void; /** * Socket timeout in milliseconds while the CONNECT handshake is in progress, defaults to 30 seconds. diff --git a/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.js b/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.js index 3b23374b..023b3fcc 100644 --- a/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.js +++ b/node_modules/nodemailer/dist/esm/smtp-connection/http-proxy-client.js @@ -88,18 +88,28 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, .join('\r\n') + // End request '\r\n\r\n'); - let headers = ''; + // The response is collected as chunks and only the bytes that just arrived, together + // with the three before them, are searched for the end of the headers. Appending to a + // string and searching all of it again re-read the whole response on every chunk. + const chunks = []; + let received = 0; + let tail = ''; const onSocketData = (chunk) => { let match; - let remainder; if (finished) { return; } - headers += chunk.toString('binary'); - if ((match = headers.match(/\r\n\r\n/))) { + const window = tail + chunk.toString('binary'); + const windowEnd = window.indexOf('\r\n\r\n'); + chunks.push(chunk); + received += chunk.length; + tail = window.slice(-3); + if (windowEnd >= 0) { socket.removeListener('data', onSocketData); - remainder = headers.substr(match.index + match[0].length); - headers = headers.substr(0, match.index); + const headerEnd = received - window.length + windowEnd; + const response = Buffer.concat(chunks, received).toString('binary'); + const headers = response.substr(0, headerEnd); + const remainder = response.substr(headerEnd + 4); if (remainder) { socket.unshift(Buffer.from(remainder, 'binary')); } @@ -123,7 +133,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, tlsOptions, socket.setTimeout(0); return done(null, socket); } - if (headers.length > MAX_RESPONSE_HEADER_BYTES) { + if (received > MAX_RESPONSE_HEADER_BYTES) { socket.removeListener('data', onSocketData); const err = new Error('Proxy response headers too large'); err.code = errors.EPROXY; diff --git a/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts b/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts index 87a5e643..a14a5903 100644 --- a/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts +++ b/node_modules/nodemailer/dist/esm/smtp-connection/index.d.ts @@ -379,7 +379,7 @@ declare class SMTPConnection extends EventEmitter { * * @param envelope Envelope object, {from: addr, to: [addr]} * @param message String, Buffer or a Stream - * @param callback Callback to return once sending is completed + * @param done Callback to return once sending is completed */ send(envelope: SMTPEnvelope, message: string | Buffer | Readable, done: SMTPConnectionSendCallback): void; /** diff --git a/node_modules/nodemailer/dist/esm/smtp-connection/index.js b/node_modules/nodemailer/dist/esm/smtp-connection/index.js index d9e2c3c0..259eedf7 100644 --- a/node_modules/nodemailer/dist/esm/smtp-connection/index.js +++ b/node_modules/nodemailer/dist/esm/smtp-connection/index.js @@ -576,7 +576,7 @@ class SMTPConnection extends EventEmitter { * * @param envelope Envelope object, {from: addr, to: [addr]} * @param message String, Buffer or a Stream - * @param callback Callback to return once sending is completed + * @param done Callback to return once sending is completed */ send(envelope, message, done) { // ensure that the callback is only called once. The public callback type has a @@ -789,6 +789,8 @@ class SMTPConnection extends EventEmitter { * @event * @param err Error object * @param type Error name + * @param data Server response that triggered the error, false if there is none + * @param command SMTP command that was in flight * @internal */ _onError(err, type, data, command) { @@ -1057,6 +1059,7 @@ class SMTPConnection extends EventEmitter { * {from:'...', to:['...']} * or * {from:{address:'...',name:'...'}, to:[address:'...',name:'...']} + * @param callback Callback to run once the envelope is processed * @internal */ _setEnvelope(envelope, callback) { @@ -1451,6 +1454,7 @@ class SMTPConnection extends EventEmitter { * hosts invalidly use a longer message than VXNlcm5hbWU6 * * @param str Message from the server + * @param callback Callback to run once the authentication sequence completes * @internal */ _actionAUTH_LOGIN_USER(str, callback) { @@ -1472,6 +1476,7 @@ class SMTPConnection extends EventEmitter { * base64 encoded again. * * @param str Message from the server + * @param callback Callback to run once the authentication sequence completes * @internal */ _actionAUTH_CRAM_MD5(str, callback) { @@ -1496,6 +1501,7 @@ class SMTPConnection extends EventEmitter { * the user can be considered logged in. Start waiting for a message to send * * @param str Message from the server + * @param callback Callback to run once the authentication sequence completes * @internal */ _actionAUTH_CRAM_MD5_PASS(str, callback) { @@ -1517,6 +1523,7 @@ class SMTPConnection extends EventEmitter { * response needs to be base64 encoded password. * * @param str Message from the server + * @param callback Callback to run once the authentication sequence completes * @internal */ _actionAUTH_LOGIN_PASS(str, callback) { @@ -1536,6 +1543,8 @@ class SMTPConnection extends EventEmitter { * the user can be considered logged in. Start waiting for a message to send * * @param str Message from the server + * @param isRetry True if this is a retry after a failed login, or the callback itself + * @param [callback] Callback to run once the authentication sequence completes * @internal */ _actionAUTHComplete(str, isRetry, callback) { @@ -1578,6 +1587,7 @@ class SMTPConnection extends EventEmitter { * Handle response for a MAIL FROM: command * * @param str Message from the server + * @param callback Callback to run once the envelope is processed * @internal */ _actionMAIL(str, callback) { @@ -1606,6 +1616,7 @@ class SMTPConnection extends EventEmitter { * Handle response for a RCPT TO: command * * @param str Message from the server + * @param callback Callback to run once the envelope is processed * @internal */ _actionRCPT(str, callback) { @@ -1657,6 +1668,7 @@ class SMTPConnection extends EventEmitter { * Handle response for a DATA command * * @param str Message from the server + * @param callback Callback to run once the envelope is processed * @internal */ _actionDATA(str, callback) { @@ -1683,6 +1695,7 @@ class SMTPConnection extends EventEmitter { * We expect a single response that defines if the sending succeeded or failed * * @param str Message from the server + * @param callback Callback to run with the final send result * @internal */ _actionSMTPStream(str, callback) { @@ -1699,6 +1712,7 @@ class SMTPConnection extends EventEmitter { * @param recipient The recipient this response applies to * @param final Is this the final recipient? * @param str Message from the server + * @param callback Callback to run with the final send result * @internal */ _actionLMTPStream(recipient, final, str, callback) { diff --git a/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts b/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts index 9502b6e3..1f1aeff0 100644 --- a/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts +++ b/node_modules/nodemailer/dist/esm/smtp-pool/index.d.ts @@ -110,9 +110,14 @@ declare class SMTPPool extends EventEmitter { /** * Verifies SMTP configuration * - * @param callback Callback function + * @returns Promise that resolves to true if the configuration is usable */ verify(): Promise; + /** + * Verifies SMTP configuration + * + * @param callback Callback function + */ verify(callback: VerifyCallback): void; } /** diff --git a/node_modules/nodemailer/dist/esm/smtp-transport/index.d.ts b/node_modules/nodemailer/dist/esm/smtp-transport/index.d.ts index 11bbb3c9..7026ef35 100644 --- a/node_modules/nodemailer/dist/esm/smtp-transport/index.d.ts +++ b/node_modules/nodemailer/dist/esm/smtp-transport/index.d.ts @@ -125,9 +125,14 @@ declare class SMTPTransport extends EventEmitter { /** * Verifies SMTP configuration * - * @param callback Callback function + * @returns Promise that resolves to true if the configuration is usable */ verify(): Promise; + /** + * Verifies SMTP configuration + * + * @param callback Callback function + */ verify(callback: VerifyCallback): void; /** * Releases resources diff --git a/node_modules/nodemailer/dist/esm/well-known/services.js b/node_modules/nodemailer/dist/esm/well-known/services.js index 85051c51..938e73d1 100644 --- a/node_modules/nodemailer/dist/esm/well-known/services.js +++ b/node_modules/nodemailer/dist/esm/well-known/services.js @@ -311,6 +311,12 @@ export const services = { "host": "mailosaur.io", "port": 25 }, + "MailSenpai": { + "description": "MailSenpai (SMTP Senpai, EU)", + "host": "relay.mailsenpai.com", + "port": 2525, + "secure": false + }, "Mailtrap": { "description": "Mailtrap", "host": "live.smtp.mailtrap.io", diff --git a/node_modules/nodemailer/dist/well-known/services.json b/node_modules/nodemailer/dist/well-known/services.json index 3bf69b3e..2be39d3b 100644 --- a/node_modules/nodemailer/dist/well-known/services.json +++ b/node_modules/nodemailer/dist/well-known/services.json @@ -279,6 +279,13 @@ "port": 25 }, + "MailSenpai": { + "description": "MailSenpai (SMTP Senpai, EU)", + "host": "relay.mailsenpai.com", + "port": 2525, + "secure": false + }, + "Mailtrap": { "description": "Mailtrap", "host": "live.smtp.mailtrap.io", diff --git a/node_modules/nodemailer/package.json b/node_modules/nodemailer/package.json index 4faacaad..90660033 100644 --- a/node_modules/nodemailer/package.json +++ b/node_modules/nodemailer/package.json @@ -1,6 +1,6 @@ { "name": "nodemailer", - "version": "10.0.13", + "version": "10.0.14", "description": "Easy as cake e-mail sending from your Node.js applications", "type": "module", "main": "./dist/cjs/nodemailer.js", @@ -151,13 +151,13 @@ }, "homepage": "https://nodemailer.com/", "devDependencies": { - "@aws-sdk/client-sesv2": "3.1143.0", + "@aws-sdk/client-sesv2": "3.1146.0", "@types/node": "20.19.43", "bunyan": "1.8.15", "c8": "12.0.0", - "eslint": "10.11.0", + "eslint": "10.12.0", "eslint-config-prettier": "10.1.8", - "globals": "17.12.0", + "globals": "17.13.0", "libbase64": "1.3.1", "libmime": "5.4.6", "libqp": "2.1.2", @@ -165,7 +165,7 @@ "prettier": "3.9.9", "proxy": "1.0.2", "proxy-test-server": "1.0.0", - "smtp-server": "3.19.15", + "smtp-server": "3.19.16", "tsx": "4.23.15", "typescript": "6.0.3", "typescript-eslint": "8.71.0"