node_modules: update (#246)

Co-authored-by: dawidd6 <9713907+dawidd6@users.noreply.github.com>
This commit is contained in:
Dawid Dziurla
2025-12-25 10:58:28 +01:00
committed by GitHub
parent de27f3a58b
commit 6e71c855c9
125 changed files with 6609 additions and 655 deletions

View File

@@ -4,9 +4,10 @@
* Converts tokens for a single address into an address object
*
* @param {Array} tokens Tokens object
* @param {Number} depth Current recursion depth for nested group protection
* @return {Object} Address object
*/
function _handleAddress(tokens) {
function _handleAddress(tokens, depth) {
let isGroup = false;
let state = 'text';
let address;
@@ -15,10 +16,12 @@ function _handleAddress(tokens) {
address: [],
comment: [],
group: [],
text: []
text: [],
textWasQuoted: [] // Track which text tokens came from inside quotes
};
let i;
let len;
let insideQuotes = false; // Track if we're currently inside a quoted string
// Filter out <addresses>, (comments) and regular text
for (i = 0, len = tokens.length; i < len; i++) {
@@ -28,16 +31,25 @@ function _handleAddress(tokens) {
switch (token.value) {
case '<':
state = 'address';
insideQuotes = false;
break;
case '(':
state = 'comment';
insideQuotes = false;
break;
case ':':
state = 'group';
isGroup = true;
insideQuotes = false;
break;
case '"':
// Track quote state for text tokens
insideQuotes = !insideQuotes;
state = 'text';
break;
default:
state = 'text';
insideQuotes = false;
break;
}
} else if (token.value) {
@@ -51,8 +63,14 @@ function _handleAddress(tokens) {
if (prevToken && prevToken.noBreak && data[state].length) {
// join values
data[state][data[state].length - 1] += token.value;
if (state === 'text' && insideQuotes) {
data.textWasQuoted[data.textWasQuoted.length - 1] = true;
}
} else {
data[state].push(token.value);
if (state === 'text') {
data.textWasQuoted.push(insideQuotes);
}
}
}
}
@@ -66,16 +84,36 @@ function _handleAddress(tokens) {
if (isGroup) {
// http://tools.ietf.org/html/rfc2822#appendix-A.1.3
data.text = data.text.join(' ');
// Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
let groupMembers = [];
if (data.group.length) {
let parsedGroup = addressparser(data.group.join(','), { _depth: depth + 1 });
// Flatten: if any member is itself a group, extract its members into the sequence
parsedGroup.forEach(member => {
if (member.group) {
// Nested group detected - flatten it by adding its members directly
groupMembers = groupMembers.concat(member.group);
} else {
groupMembers.push(member);
}
});
}
addresses.push({
name: data.text || (address && address.name),
group: data.group.length ? addressparser(data.group.join(',')) : []
group: groupMembers
});
} else {
// If no address was found, try to detect one from regular text
if (!data.address.length && data.text.length) {
for (i = data.text.length - 1; i >= 0; i--) {
if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
// Security fix: Do not extract email addresses from quoted strings
// RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com
// Extracting emails from quoted text leads to misrouting vulnerabilities
if (!data.textWasQuoted[i] && data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
data.address = data.text.splice(i, 1);
data.textWasQuoted.splice(i, 1);
break;
}
}
@@ -92,10 +130,13 @@ function _handleAddress(tokens) {
// still no address
if (!data.address.length) {
for (i = data.text.length - 1; i >= 0; i--) {
// fixed the regex to parse email address correctly when email address has more than one @
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
if (data.address.length) {
break;
// Security fix: Do not extract email addresses from quoted strings
if (!data.textWasQuoted[i]) {
// fixed the regex to parse email address correctly when email address has more than one @
data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
if (data.address.length) {
break;
}
}
}
}
@@ -259,6 +300,13 @@ class Tokenizer {
}
}
/**
* Maximum recursion depth for parsing nested groups.
* RFC 5322 doesn't allow nested groups, so this is a safeguard against
* malicious input that could cause stack overflow.
*/
const MAX_NESTED_GROUP_DEPTH = 50;
/**
* Parses structured e-mail addresses from an address field
*
@@ -271,10 +319,18 @@ class Tokenizer {
* [{name: 'Name', address: 'address@domain'}]
*
* @param {String} str Address field
* @param {Object} options Optional options object
* @param {Number} options._depth Internal recursion depth counter (do not set manually)
* @return {Array} An array of address objects
*/
function addressparser(str, options) {
options = options || {};
let depth = options._depth || 0;
// Prevent stack overflow from deeply nested groups (DoS protection)
if (depth > MAX_NESTED_GROUP_DEPTH) {
return [];
}
let tokenizer = new Tokenizer(str);
let tokens = tokenizer.tokenize();
@@ -299,7 +355,7 @@ function addressparser(str, options) {
}
addresses.forEach(address => {
address = _handleAddress(address);
address = _handleAddress(address, depth);
if (address.length) {
parsedAddresses = parsedAddresses.concat(address);
}

View File

@@ -35,15 +35,12 @@ function wrap(str, lineLength) {
let pos = 0;
let chunkLength = lineLength * 1024;
while (pos < str.length) {
let wrappedLines = str
.substr(pos, chunkLength)
.replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n')
.trim();
let wrappedLines = str.substr(pos, chunkLength).replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n');
result.push(wrappedLines);
pos += chunkLength;
}
return result.join('\r\n').trim();
return result.join('');
}
/**
@@ -56,7 +53,6 @@ function wrap(str, lineLength) {
class Encoder extends Transform {
constructor(options) {
super();
// init Transform
this.options = options || {};
if (this.options.lineLength !== false) {
@@ -98,17 +94,20 @@ class Encoder extends Transform {
if (this.options.lineLength) {
b64 = wrap(b64, this.options.lineLength);
// remove last line as it is still most probably incomplete
let lastLF = b64.lastIndexOf('\n');
if (lastLF < 0) {
this._curLine = b64;
b64 = '';
} else if (lastLF === b64.length - 1) {
this._curLine = '';
} else {
this._curLine = b64.substr(lastLF + 1);
b64 = b64.substr(0, lastLF + 1);
this._curLine = b64.substring(lastLF + 1);
b64 = b64.substring(0, lastLF + 1);
if (b64 && !b64.endsWith('\r\n')) {
b64 += '\r\n';
}
}
} else {
this._curLine = '';
}
if (b64) {
@@ -125,16 +124,14 @@ class Encoder extends Transform {
}
if (this._curLine) {
this._curLine = wrap(this._curLine, this.options.lineLength);
this.outputBytes += this._curLine.length;
this.push(this._curLine, 'ascii');
this.push(Buffer.from(this._curLine, 'ascii'));
this._curLine = '';
}
done();
}
}
// expose to the world
module.exports = {
encode,
wrap,

View File

@@ -42,7 +42,9 @@ class DKIMSigner {
this.chunks = [];
this.chunklen = 0;
this.readPos = 0;
this.cachePath = this.cacheDir ? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex')) : false;
this.cachePath = this.cacheDir
? path.join(this.cacheDir, 'message.' + Date.now() + '-' + crypto.randomBytes(14).toString('hex'))
: false;
this.cache = false;
this.headers = false;

View File

@@ -41,7 +41,7 @@ module.exports = (headers, hashAlgo, bodyHash, options) => {
signer.update(canonicalizedHeaderData.headers);
try {
signature = signer.sign(options.privateKey, 'base64');
} catch (E) {
} catch (_E) {
return false;
}

View File

@@ -132,7 +132,13 @@ function nmfetch(url, options) {
});
}
if (parsed.protocol === 'https:' && parsed.hostname && parsed.hostname !== reqOptions.host && !net.isIP(parsed.hostname) && !reqOptions.servername) {
if (
parsed.protocol === 'https:' &&
parsed.hostname &&
parsed.hostname !== reqOptions.host &&
!net.isIP(parsed.hostname) &&
!reqOptions.servername
) {
reqOptions.servername = parsed.hostname;
}

View File

@@ -91,19 +91,22 @@ class MailComposer {
attachment = this._processDataUrl(attachment);
}
let contentType = attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
let contentType =
attachment.contentType || mimeFuncs.detectMimeType(attachment.filename || attachment.path || attachment.href || 'bin');
let isImage = /^image\//i.test(contentType);
let isMessageNode = /^message\//i.test(contentType);
let contentDisposition = attachment.contentDisposition || (isMessageNode || (isImage && attachment.cid) ? 'inline' : 'attachment');
let contentDisposition =
attachment.contentDisposition || (isMessageNode || (isImage && attachment.cid) ? 'inline' : 'attachment');
let contentTransferEncoding;
if ('contentTransferEncoding' in attachment) {
// also contains `false`, to set
contentTransferEncoding = attachment.contentTransferEncoding;
} else if (isMessageNode) {
contentTransferEncoding = '7bit';
// the content might include non-ASCII bytes but at this point we do not know it yet
contentTransferEncoding = '8bit';
} else {
contentTransferEncoding = 'base64'; // the default
}
@@ -212,7 +215,10 @@ class MailComposer {
eventObject;
if (this.mail.text) {
if (typeof this.mail.text === 'object' && (this.mail.text.content || this.mail.text.path || this.mail.text.href || this.mail.text.raw)) {
if (
typeof this.mail.text === 'object' &&
(this.mail.text.content || this.mail.text.path || this.mail.text.href || this.mail.text.raw)
) {
text = this.mail.text;
} else {
text = {
@@ -237,7 +243,10 @@ class MailComposer {
}
if (this.mail.amp) {
if (typeof this.mail.amp === 'object' && (this.mail.amp.content || this.mail.amp.path || this.mail.amp.href || this.mail.amp.raw)) {
if (
typeof this.mail.amp === 'object' &&
(this.mail.amp.content || this.mail.amp.path || this.mail.amp.href || this.mail.amp.raw)
) {
amp = this.mail.amp;
} else {
amp = {
@@ -272,14 +281,18 @@ class MailComposer {
}
eventObject.filename = false;
eventObject.contentType = 'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
eventObject.contentType =
'text/calendar; charset=utf-8; method=' + (eventObject.method || 'PUBLISH').toString().trim().toUpperCase();
if (!eventObject.headers) {
eventObject.headers = {};
}
}
if (this.mail.html) {
if (typeof this.mail.html === 'object' && (this.mail.html.content || this.mail.html.path || this.mail.html.href || this.mail.html.raw)) {
if (
typeof this.mail.html === 'object' &&
(this.mail.html.content || this.mail.html.path || this.mail.html.href || this.mail.html.raw)
) {
html = this.mail.html;
} else {
html = {
@@ -304,7 +317,9 @@ class MailComposer {
}
data = {
contentType: alternative.contentType || mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
contentType:
alternative.contentType ||
mimeFuncs.detectMimeType(alternative.filename || alternative.path || alternative.href || 'txt'),
contentTransferEncoding: alternative.contentTransferEncoding
};
@@ -550,9 +565,46 @@ class MailComposer {
* @return {Object} Parsed element
*/
_processDataUrl(element) {
const dataUrl = element.path || element.href;
// Early validation to prevent ReDoS
if (!dataUrl || typeof dataUrl !== 'string') {
return element;
}
if (!dataUrl.startsWith('data:')) {
return element;
}
if (dataUrl.length > 52428800) {
// 52428800 chars = 50MB limit for data URL string (~37.5MB decoded image)
// Extract content type before rejecting to preserve MIME type
let detectedType = 'application/octet-stream';
const commaPos = dataUrl.indexOf(',');
if (commaPos > 0 && commaPos < 200) {
// Parse header safely with size limit
const header = dataUrl.substring(5, commaPos); // skip 'data:'
const parts = header.split(';');
if (parts[0] && parts[0].includes('/')) {
detectedType = parts[0].trim();
}
}
// Return empty content for excessively long data URLs
return Object.assign({}, element, {
path: false,
href: false,
content: Buffer.alloc(0),
contentType: element.contentType || detectedType
});
}
let parsedDataUri;
if ((element.path || element.href).match(/^data:/)) {
parsedDataUri = parseDataURI(element.path || element.href);
try {
parsedDataUri = parseDataURI(dataUrl);
} catch (_err) {
return element;
}
if (!parsedDataUri) {

View File

@@ -87,6 +87,11 @@ class Mail extends EventEmitter {
this.transporter.on('idle', (...args) => {
this.emit('idle', ...args);
});
// indicates if the sender has became idle and all connections are terminated
this.transporter.on('clear', (...args) => {
this.emit('clear', ...args);
});
}
/**
@@ -236,7 +241,14 @@ class Mail extends EventEmitter {
}
getVersionString() {
return util.format('%s (%s; +%s; %s/%s)', packageData.name, packageData.version, packageData.homepage, this.transporter.name, this.transporter.version);
return util.format(
'%s (%s; +%s; %s/%s)',
packageData.name,
packageData.version,
packageData.homepage,
this.transporter.name,
this.transporter.version
);
}
_processPlugins(step, mail, callback) {

View File

@@ -64,7 +64,8 @@ class MailMessage {
if (this.data.attachments && this.data.attachments.length) {
this.data.attachments.forEach((attachment, i) => {
if (!attachment.filename) {
attachment.filename = (attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
attachment.filename =
(attachment.path || attachment.href || '').split('/').pop().split('?').shift() || 'attachment-' + (i + 1);
if (attachment.filename.indexOf('.') < 0) {
attachment.filename += '.' + mimeFuncs.detectExtension(attachment.contentType);
}

View File

@@ -269,7 +269,7 @@ module.exports = {
// first line includes the charset and language info and needs to be encoded
// even if it does not contain any unicode characters
line = 'utf-8\x27\x27';
line = "utf-8''";
let encoded = true;
startPos = 0;
@@ -614,7 +614,7 @@ module.exports = {
try {
// might throw if we try to encode invalid sequences, eg. partial emoji
str = encodeURIComponent(str);
} catch (E) {
} catch (_E) {
// should never run
return str.replace(/[^\x00-\x1F *'()<>@,;:\\"[\]?=\u007F-\uFFFF]+/g, '');
}

View File

@@ -1102,7 +1102,10 @@ const extensions = new Map([
['bdm', 'application/vnd.syncml.dm+wbxml'],
['bed', 'application/vnd.realvnc.bed'],
['bh2', 'application/vnd.fujitsu.oasysprs'],
['bin', ['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']],
[
'bin',
['application/octet-stream', 'application/mac-binary', 'application/macbinary', 'application/x-macbinary', 'application/x-binary']
],
['bm', 'image/bmp'],
['bmi', 'application/vnd.bmi'],
['bmp', ['image/bmp', 'image/x-windows-bmp']],
@@ -1147,7 +1150,10 @@ const extensions = new Map([
['cii', 'application/vnd.anser-web-certificate-issue-initiation'],
['cil', 'application/vnd.ms-artgalry'],
['cla', 'application/vnd.claymore'],
['class', ['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']],
[
'class',
['application/octet-stream', 'application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java-class']
],
['clkk', 'application/vnd.crick.clicker.keyboard'],
['clkp', 'application/vnd.crick.clicker.palette'],
['clkt', 'application/vnd.crick.clicker.template'],
@@ -1752,7 +1758,10 @@ const extensions = new Map([
['sbml', 'application/sbml+xml'],
['sc', 'application/vnd.ibm.secure-container'],
['scd', 'application/x-msschedule'],
['scm', ['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']],
[
'scm',
['application/vnd.lotus-screencam', 'video/x-scm', 'text/x-script.guile', 'application/x-lotusscreencam', 'text/x-script.scheme']
],
['scq', 'application/scvp-cv-request'],
['scs', 'application/scvp-cv-response'],
['sct', 'text/scriptlet'],

View File

@@ -552,7 +552,11 @@ class MimeNode {
this._handleContentType(structured);
if (structured.value.match(/^text\/plain\b/) && typeof this.content === 'string' && /[\u0080-\uFFFF]/.test(this.content)) {
if (
structured.value.match(/^text\/plain\b/) &&
typeof this.content === 'string' &&
/[\u0080-\uFFFF]/.test(this.content)
) {
structured.params.charset = 'utf-8';
}
@@ -963,8 +967,8 @@ class MimeNode {
setImmediate(() => {
try {
contentStream.end(content._resolvedValue);
} catch (err) {
contentStream.emit('error', err);
} catch (_err) {
contentStream.emit('error', _err);
}
});
@@ -995,8 +999,8 @@ class MimeNode {
setImmediate(() => {
try {
contentStream.end(content || '');
} catch (err) {
contentStream.emit('error', err);
} catch (_err) {
contentStream.emit('error', _err);
}
});
return contentStream;
@@ -1014,7 +1018,6 @@ class MimeNode {
return [].concat.apply(
[],
[].concat(addresses).map(address => {
// eslint-disable-line prefer-spread
if (address && address.address) {
address.address = this._normalizeAddress(address.address);
address.name = address.name || '';
@@ -1113,7 +1116,6 @@ class MimeNode {
.apply(
[],
[].concat(value || '').map(elm => {
// eslint-disable-line prefer-spread
elm = (elm || '')
.toString()
.replace(/\r?\n|\r/g, ' ')
@@ -1219,7 +1221,7 @@ class MimeNode {
try {
encodedDomain = punycode.toASCII(domain.toLowerCase());
} catch (err) {
} catch (_err) {
// keep as is?
}
@@ -1282,7 +1284,7 @@ class MimeNode {
// count latin alphabet symbols and 8-bit range symbols + control symbols
// if there are more latin characters, then use quoted-printable
// encoding, otherwise use base64
nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length; // eslint-disable-line no-control-regex
nonLatinLen = (value.match(/[\x00-\x08\x0B\x0C\x0E-\x1F\u0080-\uFFFF]/g) || []).length;
latinLen = (value.match(/[a-z]/gi) || []).length;
// if there are more latin symbols than binary/unicode, then prefer Q, otherwise B
encoding = nonLatinLen < latinLen ? 'Q' : 'B';

View File

@@ -46,7 +46,9 @@ module.exports.createTransport = function (transporter, defaults) {
transporter = new JSONTransport(options);
} else if (options.SES) {
if (options.SES.ses && options.SES.aws) {
let error = new Error('Using legacy SES configuration, expecting @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/');
let error = new Error(
'Using legacy SES configuration, expecting @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/'
);
error.code = 'LegacyConfig';
throw error;
}

View File

@@ -28,7 +28,10 @@ function encode(buffer) {
for (let i = 0, len = buffer.length; i < len; i++) {
ord = buffer[i];
// if the char is in allowed range, then keep as is, unless it is a WS in the end of a line
if (checkRanges(ord, ranges) && !((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d))) {
if (
checkRanges(ord, ranges) &&
!((ord === 0x20 || ord === 0x09) && (i === len - 1 || buffer[i + 1] === 0x0a || buffer[i + 1] === 0x0d))
) {
result += String.fromCharCode(ord);
continue;
}
@@ -90,7 +93,12 @@ function wrap(str, lineLength) {
}
// ensure that utf-8 sequences are not split
while (line.length > 3 && line.length < len - pos && !line.match(/^(?:=[\da-f]{2}){1,4}$/i) && (match = line.match(/[=][\da-f]{2}$/gi))) {
while (
line.length > 3 &&
line.length < len - pos &&
!line.match(/^(?:=[\da-f]{2}){1,4}$/i) &&
(match = line.match(/[=][\da-f]{2}$/gi))
) {
code = parseInt(match[0].substr(1, 2), 16);
if (code < 128) {
break;

View File

@@ -11,11 +11,19 @@ const net = require('net');
const os = require('os');
const DNS_TTL = 5 * 60 * 1000;
const CACHE_CLEANUP_INTERVAL = 30 * 1000; // Minimum 30 seconds between cleanups
const MAX_CACHE_SIZE = 1000; // Maximum number of entries in cache
let lastCacheCleanup = 0;
module.exports._lastCacheCleanup = () => lastCacheCleanup;
module.exports._resetCacheCleanup = () => {
lastCacheCleanup = 0;
};
let networkInterfaces;
try {
networkInterfaces = os.networkInterfaces();
} catch (err) {
} catch (_err) {
// fails on some systems
}
@@ -81,8 +89,8 @@ const formatDNSValue = (value, extra) => {
!value.addresses || !value.addresses.length
? null
: value.addresses.length === 1
? value.addresses[0]
: value.addresses[Math.floor(Math.random() * value.addresses.length)]
? value.addresses[0]
: value.addresses[Math.floor(Math.random() * value.addresses.length)]
},
extra || {}
);
@@ -113,7 +121,27 @@ module.exports.resolveHostname = (options, callback) => {
if (dnsCache.has(options.host)) {
cached = dnsCache.get(options.host);
if (!cached.expires || cached.expires >= Date.now()) {
// Lazy cleanup with time throttling
const now = Date.now();
if (now - lastCacheCleanup > CACHE_CLEANUP_INTERVAL) {
lastCacheCleanup = now;
// Clean up expired entries
for (const [host, entry] of dnsCache.entries()) {
if (entry.expires && entry.expires < now) {
dnsCache.delete(host);
}
}
// If cache is still too large, remove oldest entries
if (dnsCache.size > MAX_CACHE_SIZE) {
const toDelete = Math.floor(MAX_CACHE_SIZE * 0.1); // Remove 10% of entries
const keys = Array.from(dnsCache.keys()).slice(0, toDelete);
keys.forEach(key => dnsCache.delete(key));
}
}
if (!cached.expires || cached.expires >= now) {
return callback(
null,
formatDNSValue(cached.value, {
@@ -126,7 +154,11 @@ module.exports.resolveHostname = (options, callback) => {
resolver(4, options.host, options, (err, addresses) => {
if (err) {
if (cached) {
// ignore error, use expired value
dnsCache.set(options.host, {
value: cached.value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
return callback(
null,
formatDNSValue(cached.value, {
@@ -160,7 +192,11 @@ module.exports.resolveHostname = (options, callback) => {
resolver(6, options.host, options, (err, addresses) => {
if (err) {
if (cached) {
// ignore error, use expired value
dnsCache.set(options.host, {
value: cached.value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
return callback(
null,
formatDNSValue(cached.value, {
@@ -195,7 +231,11 @@ module.exports.resolveHostname = (options, callback) => {
dns.lookup(options.host, { all: true }, (err, addresses) => {
if (err) {
if (cached) {
// ignore error, use expired value
dnsCache.set(options.host, {
value: cached.value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
return callback(
null,
formatDNSValue(cached.value, {
@@ -246,9 +286,13 @@ module.exports.resolveHostname = (options, callback) => {
})
);
});
} catch (err) {
} catch (_err) {
if (cached) {
// ignore error, use expired value
dnsCache.set(options.host, {
value: cached.value,
expires: Date.now() + (options.dnsTtl || DNS_TTL)
});
return callback(
null,
formatDNSValue(cached.value, {
@@ -419,52 +463,74 @@ module.exports.callbackPromise = (resolve, reject) =>
};
module.exports.parseDataURI = uri => {
let input = uri;
let commaPos = input.indexOf(',');
if (!commaPos) {
return uri;
if (typeof uri !== 'string') {
return null;
}
let data = input.substring(commaPos + 1);
let metaStr = input.substring('data:'.length, commaPos);
// Early return for non-data URIs to avoid unnecessary processing
if (!uri.startsWith('data:')) {
return null;
}
// Find the first comma safely - this prevents ReDoS
const commaPos = uri.indexOf(',');
if (commaPos === -1) {
return null;
}
const data = uri.substring(commaPos + 1);
const metaStr = uri.substring('data:'.length, commaPos);
let encoding;
const metaEntries = metaStr.split(';');
let metaEntries = metaStr.split(';');
let lastMetaEntry = metaEntries.length > 1 ? metaEntries[metaEntries.length - 1] : false;
if (lastMetaEntry && lastMetaEntry.indexOf('=') < 0) {
encoding = lastMetaEntry.toLowerCase();
metaEntries.pop();
}
let contentType = metaEntries.shift() || 'application/octet-stream';
let params = {};
for (let entry of metaEntries) {
let sep = entry.indexOf('=');
if (sep >= 0) {
let key = entry.substring(0, sep);
let value = entry.substring(sep + 1);
params[key] = value;
if (metaEntries.length > 0) {
const lastEntry = metaEntries[metaEntries.length - 1].toLowerCase().trim();
// Only recognize valid encoding types to prevent manipulation
if (['base64', 'utf8', 'utf-8'].includes(lastEntry) && lastEntry.indexOf('=') === -1) {
encoding = lastEntry;
metaEntries.pop();
}
}
switch (encoding) {
case 'base64':
data = Buffer.from(data, 'base64');
break;
case 'utf8':
data = Buffer.from(data);
break;
default:
try {
data = Buffer.from(decodeURIComponent(data));
} catch (err) {
data = Buffer.from(data);
const contentType = metaEntries.length > 0 ? metaEntries.shift() : 'application/octet-stream';
const params = {};
for (let i = 0; i < metaEntries.length; i++) {
const entry = metaEntries[i];
const sepPos = entry.indexOf('=');
if (sepPos > 0) {
// Ensure there's a key before the '='
const key = entry.substring(0, sepPos).trim();
const value = entry.substring(sepPos + 1).trim();
if (key) {
params[key] = value;
}
data = Buffer.from(data);
}
}
return { data, encoding, contentType, params };
// Decode data based on encoding with proper error handling
let bufferData;
try {
if (encoding === 'base64') {
bufferData = Buffer.from(data, 'base64');
} else {
try {
bufferData = Buffer.from(decodeURIComponent(data));
} catch (_decodeError) {
bufferData = Buffer.from(data);
}
}
} catch (_bufferError) {
bufferData = Buffer.alloc(0);
}
return {
data: bufferData,
encoding: encoding || null,
contentType: contentType || 'application/octet-stream',
params
};
};
/**

View File

@@ -51,7 +51,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
finished = true;
try {
socket.destroy();
} catch (E) {
} catch (_E) {
// ignore
}
callback(err);
@@ -118,7 +118,7 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
if (!match || (match[1] || '').charAt(0) !== '2') {
try {
socket.destroy();
} catch (E) {
} catch (_E) {
// ignore
}
return callback(new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || '')));

View File

@@ -124,7 +124,7 @@ class SMTPConnection extends EventEmitter {
/**
* The socket connecting to the server
* @publick
* @public
*/
this._socket = false;
@@ -415,7 +415,7 @@ class SMTPConnection extends EventEmitter {
if (socket && !socket.destroyed) {
try {
socket[closeMethod]();
} catch (E) {
} catch (_E) {
// just ignore
}
}
@@ -1116,6 +1116,23 @@ class SMTPConnection extends EventEmitter {
}
}
// RFC 8689: If the envelope requests REQUIRETLS extension
// then append REQUIRETLS keyword to the MAIL FROM command
// Note: REQUIRETLS can only be used over TLS connections and requires server support
if (this._envelope.requireTLSExtensionEnabled) {
if (!this.secure) {
return callback(
this._formatError('REQUIRETLS can only be used over TLS connections (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
);
}
if (!this._supportedExtensions.includes('REQUIRETLS')) {
return callback(
this._formatError('Server does not support REQUIRETLS extension (RFC 8689)', 'EREQUIRETLS', false, 'MAIL FROM')
);
}
args.push('REQUIRETLS');
}
this._sendCommand('MAIL FROM:<' + this._envelope.from + '>' + (args.length ? ' ' + args.join(' ') : ''));
}
@@ -1147,8 +1164,8 @@ class SMTPConnection extends EventEmitter {
}
notify = notify.map(n => n.trim().toUpperCase());
let validNotify = ['NEVER', 'SUCCESS', 'FAILURE', 'DELAY'];
let invaliNotify = notify.filter(n => !validNotify.includes(n));
if (invaliNotify.length || (notify.length > 1 && notify.includes('NEVER'))) {
let invalidNotify = notify.filter(n => !validNotify.includes(n));
if (invalidNotify.length || (notify.length > 1 && notify.includes('NEVER'))) {
throw new Error('notify: ' + JSON.stringify(notify.join(',')));
}
notify = notify.join(',');
@@ -1294,7 +1311,12 @@ class SMTPConnection extends EventEmitter {
if (str.charAt(0) !== '2') {
if (this.options.requireTLS) {
this._onError(new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str), 'ECONNECTION', str, 'EHLO');
this._onError(
new Error('EHLO failed but HELO does not support required STARTTLS. response=' + str),
'ECONNECTION',
str,
'EHLO'
);
return;
}
@@ -1332,6 +1354,11 @@ class SMTPConnection extends EventEmitter {
this._supportedExtensions.push('8BITMIME');
}
// Detect if the server supports REQUIRETLS (RFC 8689)
if (/[ -]REQUIRETLS\b/im.test(str)) {
this._supportedExtensions.push('REQUIRETLS');
}
// Detect if the server supports PIPELINING
if (/[ -]PIPELINING\b/im.test(str)) {
this._supportedExtensions.push('PIPELINING');
@@ -1476,7 +1503,9 @@ class SMTPConnection extends EventEmitter {
let challengeString = '';
if (!challengeMatch) {
return callback(this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5'));
return callback(
this._formatError('Invalid login sequence while waiting for server challenge string', 'EAUTH', str, 'AUTH CRAM-MD5')
);
} else {
challengeString = challengeMatch[1];
}
@@ -1619,7 +1648,7 @@ class SMTPConnection extends EventEmitter {
}
if (!this._envelope.rcptQueue.length) {
return callback(this._formatError('Can\x27t send mail - no recipients defined', 'EENVELOPE', false, 'API'));
return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
} else {
this._recipientQueue = [];
@@ -1675,7 +1704,7 @@ class SMTPConnection extends EventEmitter {
});
this._sendCommand('DATA');
} else {
err = this._formatError('Can\x27t send mail - all recipients were rejected', 'EENVELOPE', str, 'RCPT TO');
err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', str, 'RCPT TO');
err.rejected = this._envelope.rejected;
err.rejectedErrors = this._envelope.rejectedErrors;
return callback(err);
@@ -1814,7 +1843,7 @@ class SMTPConnection extends EventEmitter {
let defaultHostname;
try {
defaultHostname = os.hostname() || '';
} catch (err) {
} catch (_err) {
// fails on windows 7
defaultHostname = 'localhost';
}

View File

@@ -406,6 +406,10 @@ class SMTPPool extends EventEmitter {
this._continueProcessing();
}, 50);
} else {
if (!this._closed && this.idling && !this._connections.length) {
this.emit('clear');
}
this._continueProcessing();
}
});

View File

@@ -23,7 +23,8 @@ class PoolResource extends EventEmitter {
switch ((this.options.auth.type || '').toString().toUpperCase()) {
case 'OAUTH2': {
let oauth2 = new XOAuth2(this.options.auth, this.logger);
oauth2.provisionCallback = (this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
oauth2.provisionCallback =
(this.pool.mailer && this.pool.mailer.get('oauth2_provision_cb')) || oauth2.provisionCallback;
this.auth = {
type: 'OAUTH2',
user: this.options.auth.user,
@@ -127,7 +128,7 @@ class PoolResource extends EventEmitter {
try {
timer.unref();
} catch (E) {
} catch (_E) {
// Ignore. Happens on envs with non-node timer implementation
}
});
@@ -201,6 +202,11 @@ class PoolResource extends EventEmitter {
envelope.dsn = mail.data.dsn;
}
// RFC 8689: Pass requireTLSExtensionEnabled to envelope for MAIL FROM parameter
if (mail.data.requireTLSExtensionEnabled) {
envelope.requireTLSExtensionEnabled = mail.data.requireTLSExtensionEnabled;
}
this.connection.send(envelope, mail.message.createReadStream(), (err, info) => {
this.messages++;

View File

@@ -197,7 +197,7 @@ class SMTPTransport extends EventEmitter {
try {
timer.unref();
} catch (E) {
} catch (_E) {
// Ignore. Happens on envs with non-node timer implementation
}
});
@@ -215,6 +215,11 @@ class SMTPTransport extends EventEmitter {
envelope.dsn = mail.data.dsn;
}
// RFC 8689: Pass requireTLSExtensionEnabled to envelope for MAIL FROM parameter
if (mail.data.requireTLSExtensionEnabled) {
envelope.requireTLSExtensionEnabled = mail.data.requireTLSExtensionEnabled;
}
this.logger.info(
{
tnx: 'send',

View File

@@ -1,63 +1,120 @@
{
"1und1": {
"description": "1&1 Mail (German hosting provider)",
"host": "smtp.1und1.de",
"port": 465,
"secure": true,
"authMethod": "LOGIN"
},
"126": {
"description": "126 Mail (NetEase)",
"host": "smtp.126.com",
"port": 465,
"secure": true
},
"163": {
"description": "163 Mail (NetEase)",
"host": "smtp.163.com",
"port": 465,
"secure": true
},
"Aliyun": {
"description": "Alibaba Cloud Mail",
"domains": ["aliyun.com"],
"host": "smtp.aliyun.com",
"port": 465,
"secure": true
},
"AliyunQiye": {
"description": "Alibaba Cloud Enterprise Mail",
"host": "smtp.qiye.aliyun.com",
"port": 465,
"secure": true
},
"AOL": {
"description": "AOL Mail",
"domains": ["aol.com"],
"host": "smtp.aol.com",
"port": 587
},
"Aruba": {
"description": "Aruba PEC (Italian email provider)",
"domains": ["aruba.it", "pec.aruba.it"],
"aliases": ["Aruba PEC"],
"host": "smtps.aruba.it",
"port": 465,
"secure": true,
"authMethod": "LOGIN"
},
"Bluewin": {
"description": "Bluewin (Swiss email provider)",
"host": "smtpauths.bluewin.ch",
"domains": ["bluewin.ch"],
"port": 465
},
"BOL": {
"description": "BOL Mail (Brazilian provider)",
"domains": ["bol.com.br"],
"host": "smtp.bol.com.br",
"port": 587,
"requireTLS": true
},
"DebugMail": {
"description": "DebugMail (email testing service)",
"host": "debugmail.io",
"port": 25
},
"Disroot": {
"description": "Disroot (privacy-focused provider)",
"domains": ["disroot.org"],
"host": "disroot.org",
"port": 587,
"secure": false,
"authMethod": "LOGIN"
},
"DynectEmail": {
"description": "Dyn Email Delivery",
"aliases": ["Dynect"],
"host": "smtp.dynect.net",
"port": 25
},
"ElasticEmail": {
"description": "Elastic Email",
"aliases": ["Elastic Email"],
"host": "smtp.elasticemail.com",
"port": 465,
"secure": true
},
"Ethereal": {
"description": "Ethereal Email (email testing service)",
"aliases": ["ethereal.email"],
"host": "smtp.ethereal.email",
"port": 587
},
"FastMail": {
"description": "FastMail",
"domains": ["fastmail.fm"],
"host": "smtp.fastmail.com",
"port": 465,
"secure": true
},
"Forward Email": {
"aliases": ["FE", "ForwardEmail"],
"domains": ["forwardemail.net"],
"host": "smtp.forwardemail.net",
"port": 465,
"secure": true
},
"Feishu Mail": {
"description": "Feishu Mail (Lark)",
"aliases": ["Feishu", "FeishuMail"],
"domains": ["www.feishu.cn"],
"host": "smtp.feishu.cn",
@@ -65,13 +122,24 @@
"secure": true
},
"Forward Email": {
"description": "Forward Email (email forwarding service)",
"aliases": ["FE", "ForwardEmail"],
"domains": ["forwardemail.net"],
"host": "smtp.forwardemail.net",
"port": 465,
"secure": true
},
"GandiMail": {
"description": "Gandi Mail",
"aliases": ["Gandi", "Gandi Mail"],
"host": "mail.gandi.net",
"port": 587
},
"Gmail": {
"description": "Gmail",
"aliases": ["Google Mail"],
"domains": ["gmail.com", "googlemail.com"],
"host": "smtp.gmail.com",
@@ -79,26 +147,38 @@
"secure": true
},
"GMX": {
"description": "GMX Mail",
"domains": ["gmx.com", "gmx.net", "gmx.de"],
"host": "mail.gmx.com",
"port": 587
},
"Godaddy": {
"description": "GoDaddy Email (US)",
"host": "smtpout.secureserver.net",
"port": 25
},
"GodaddyAsia": {
"description": "GoDaddy Email (Asia)",
"host": "smtp.asia.secureserver.net",
"port": 25
},
"GodaddyEurope": {
"description": "GoDaddy Email (Europe)",
"host": "smtp.europe.secureserver.net",
"port": 25
},
"hot.ee": {
"description": "Hot.ee (Estonian email provider)",
"host": "mail.hot.ee"
},
"Hotmail": {
"description": "Outlook.com / Hotmail",
"aliases": ["Outlook", "Outlook.com", "Hotmail.com"],
"domains": ["hotmail.com", "outlook.com"],
"host": "smtp-mail.outlook.com",
@@ -106,6 +186,7 @@
},
"iCloud": {
"description": "iCloud Mail",
"aliases": ["Me", "Mac"],
"domains": ["me.com", "mac.com"],
"host": "smtp.mail.me.com",
@@ -113,72 +194,117 @@
},
"Infomaniak": {
"description": "Infomaniak Mail (Swiss hosting provider)",
"host": "mail.infomaniak.com",
"domains": ["ik.me", "ikmail.com", "etik.com"],
"port": 587
},
"KolabNow": {
"description": "KolabNow (secure email service)",
"domains": ["kolabnow.com"],
"aliases": ["Kolab"],
"host": "smtp.kolabnow.com",
"port": 465,
"secure": true,
"authMethod": "LOGIN"
},
"Loopia": {
"description": "Loopia (Swedish hosting provider)",
"host": "mailcluster.loopia.se",
"port": 465
},
"Loops": {
"description": "Loops",
"host": "smtp.loops.so",
"port": 587
},
"mail.ee": {
"description": "Mail.ee (Estonian email provider)",
"host": "smtp.mail.ee"
},
"Mail.ru": {
"description": "Mail.ru",
"host": "smtp.mail.ru",
"port": 465,
"secure": true
},
"Mailcatch.app": {
"description": "Mailcatch (email testing service)",
"host": "sandbox-smtp.mailcatch.app",
"port": 2525
},
"Maildev": {
"description": "MailDev (local email testing)",
"port": 1025,
"ignoreTLS": true
},
"MailerSend": {
"description": "MailerSend",
"host": "smtp.mailersend.net",
"port": 587
},
"Mailgun": {
"description": "Mailgun",
"host": "smtp.mailgun.org",
"port": 465,
"secure": true
},
"Mailjet": {
"description": "Mailjet",
"host": "in.mailjet.com",
"port": 587
},
"Mailosaur": {
"description": "Mailosaur (email testing service)",
"host": "mailosaur.io",
"port": 25
},
"Mailtrap": {
"description": "Mailtrap",
"host": "live.smtp.mailtrap.io",
"port": 587
},
"Mandrill": {
"description": "Mandrill (by Mailchimp)",
"host": "smtp.mandrillapp.com",
"port": 587
},
"Naver": {
"description": "Naver Mail (Korean email provider)",
"host": "smtp.naver.com",
"port": 587
},
"OhMySMTP": {
"description": "OhMySMTP (email delivery service)",
"host": "smtp.ohmysmtp.com",
"port": 587,
"secure": false
},
"One": {
"description": "One.com Email",
"host": "send.one.com",
"port": 465,
"secure": true
},
"OpenMailBox": {
"description": "OpenMailBox",
"aliases": ["OMB", "openmailbox.org"],
"host": "smtp.openmailbox.org",
"port": 465,
@@ -186,24 +312,21 @@
},
"Outlook365": {
"description": "Microsoft 365 / Office 365",
"host": "smtp.office365.com",
"port": 587,
"secure": false
},
"OhMySMTP": {
"host": "smtp.ohmysmtp.com",
"port": 587,
"secure": false
},
"Postmark": {
"description": "Postmark",
"aliases": ["PostmarkApp"],
"host": "smtp.postmarkapp.com",
"port": 2525
},
"Proton": {
"description": "Proton Mail",
"aliases": ["ProtonMail", "Proton.me", "Protonmail.com", "Protonmail.ch"],
"domains": ["proton.me", "protonmail.com", "pm.me", "protonmail.ch"],
"host": "smtp.protonmail.ch",
@@ -212,12 +335,14 @@
},
"qiye.aliyun": {
"description": "Alibaba Mail Enterprise Edition",
"host": "smtp.mxhichina.com",
"port": "465",
"secure": true
},
"QQ": {
"description": "QQ Mail",
"domains": ["qq.com"],
"host": "smtp.qq.com",
"port": 465,
@@ -225,6 +350,7 @@
},
"QQex": {
"description": "QQ Enterprise Mail",
"aliases": ["QQ Enterprise"],
"domains": ["exmail.qq.com"],
"host": "smtp.exmail.qq.com",
@@ -232,89 +358,189 @@
"secure": true
},
"Resend": {
"description": "Resend",
"host": "smtp.resend.com",
"port": 465,
"secure": true
},
"Runbox": {
"description": "Runbox (Norwegian email provider)",
"domains": ["runbox.com"],
"host": "smtp.runbox.com",
"port": 465,
"secure": true
},
"SendCloud": {
"description": "SendCloud (Chinese email delivery)",
"host": "smtp.sendcloud.net",
"port": 2525
},
"SendGrid": {
"description": "SendGrid",
"host": "smtp.sendgrid.net",
"port": 587
},
"SendinBlue": {
"description": "Brevo (formerly Sendinblue)",
"aliases": ["Brevo"],
"host": "smtp-relay.brevo.com",
"port": 587
},
"SendPulse": {
"description": "SendPulse",
"host": "smtp-pulse.com",
"port": 465,
"secure": true
},
"SES": {
"description": "AWS SES US East (N. Virginia)",
"host": "email-smtp.us-east-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-EAST-1": {
"host": "email-smtp.us-east-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-WEST-2": {
"host": "email-smtp.us-west-2.amazonaws.com",
"port": 465,
"secure": true
},
"SES-EU-WEST-1": {
"host": "email-smtp.eu-west-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-SOUTH-1": {
"host": "email-smtp.ap-south-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-NORTHEAST-1": {
"description": "AWS SES Asia Pacific (Tokyo)",
"host": "email-smtp.ap-northeast-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-NORTHEAST-2": {
"description": "AWS SES Asia Pacific (Seoul)",
"host": "email-smtp.ap-northeast-2.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-NORTHEAST-3": {
"description": "AWS SES Asia Pacific (Osaka)",
"host": "email-smtp.ap-northeast-3.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-SOUTH-1": {
"description": "AWS SES Asia Pacific (Mumbai)",
"host": "email-smtp.ap-south-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-SOUTHEAST-1": {
"description": "AWS SES Asia Pacific (Singapore)",
"host": "email-smtp.ap-southeast-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-AP-SOUTHEAST-2": {
"description": "AWS SES Asia Pacific (Sydney)",
"host": "email-smtp.ap-southeast-2.amazonaws.com",
"port": 465,
"secure": true
},
"SES-CA-CENTRAL-1": {
"description": "AWS SES Canada (Central)",
"host": "email-smtp.ca-central-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-EU-CENTRAL-1": {
"description": "AWS SES Europe (Frankfurt)",
"host": "email-smtp.eu-central-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-EU-NORTH-1": {
"description": "AWS SES Europe (Stockholm)",
"host": "email-smtp.eu-north-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-EU-WEST-1": {
"description": "AWS SES Europe (Ireland)",
"host": "email-smtp.eu-west-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-EU-WEST-2": {
"description": "AWS SES Europe (London)",
"host": "email-smtp.eu-west-2.amazonaws.com",
"port": 465,
"secure": true
},
"SES-EU-WEST-3": {
"description": "AWS SES Europe (Paris)",
"host": "email-smtp.eu-west-3.amazonaws.com",
"port": 465,
"secure": true
},
"SES-SA-EAST-1": {
"description": "AWS SES South America (São Paulo)",
"host": "email-smtp.sa-east-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-EAST-1": {
"description": "AWS SES US East (N. Virginia)",
"host": "email-smtp.us-east-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-EAST-2": {
"description": "AWS SES US East (Ohio)",
"host": "email-smtp.us-east-2.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-GOV-EAST-1": {
"description": "AWS SES GovCloud (US-East)",
"host": "email-smtp.us-gov-east-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-GOV-WEST-1": {
"description": "AWS SES GovCloud (US-West)",
"host": "email-smtp.us-gov-west-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-WEST-1": {
"description": "AWS SES US West (N. California)",
"host": "email-smtp.us-west-1.amazonaws.com",
"port": 465,
"secure": true
},
"SES-US-WEST-2": {
"description": "AWS SES US West (Oregon)",
"host": "email-smtp.us-west-2.amazonaws.com",
"port": 465,
"secure": true
},
"Seznam": {
"description": "Seznam Email (Czech email provider)",
"aliases": ["Seznam Email"],
"domains": ["seznam.cz", "email.cz", "post.cz", "spoluzaci.cz"],
"host": "smtp.seznam.cz",
@@ -322,7 +548,14 @@
"secure": true
},
"SMTP2GO": {
"description": "SMTP2GO",
"host": "mail.smtp2go.com",
"port": 2525
},
"Sparkpost": {
"description": "SparkPost",
"aliases": ["SparkPost", "SparkPost Mail"],
"domains": ["sparkpost.com"],
"host": "smtp.sparkpostmail.com",
@@ -331,11 +564,21 @@
},
"Tipimail": {
"description": "Tipimail (email delivery service)",
"host": "smtp.tipimail.com",
"port": 587
},
"Tutanota": {
"description": "Tutanota (Tuta Mail)",
"domains": ["tutanota.com", "tuta.com", "tutanota.de", "tuta.io"],
"host": "smtp.tutanota.com",
"port": 465,
"secure": true
},
"Yahoo": {
"description": "Yahoo Mail",
"domains": ["yahoo.com"],
"host": "smtp.mail.yahoo.com",
"port": 465,
@@ -343,28 +586,26 @@
},
"Yandex": {
"description": "Yandex Mail",
"domains": ["yandex.ru"],
"host": "smtp.yandex.ru",
"port": 465,
"secure": true
},
"Zimbra": {
"description": "Zimbra Mail Server",
"aliases": ["Zimbra Collaboration"],
"host": "smtp.zimbra.com",
"port": 587,
"requireTLS": true
},
"Zoho": {
"description": "Zoho Mail",
"host": "smtp.zoho.com",
"port": 465,
"secure": true,
"authMethod": "LOGIN"
},
"126": {
"host": "smtp.126.com",
"port": 465,
"secure": true
},
"163": {
"host": "smtp.163.com",
"port": 465,
"secure": true
}
}

View File

@@ -72,6 +72,9 @@ class XOAuth2 extends Stream {
let timeout = Math.max(Number(this.options.timeout) || 0, 0);
this.expires = (timeout && Date.now() + timeout * 1000) || 0;
}
this.renewing = false; // Track if renewal is in progress
this.renewalQueue = []; // Queue for pending requests during renewal
}
/**
@@ -82,14 +85,61 @@ class XOAuth2 extends Stream {
*/
getToken(renew, callback) {
if (!renew && this.accessToken && (!this.expires || this.expires > Date.now())) {
this.logger.debug(
{
tnx: 'OAUTH2',
user: this.options.user,
action: 'reuse'
},
'Reusing existing access token for %s',
this.options.user
);
return callback(null, this.accessToken);
}
let generateCallback = (...args) => {
if (args[0]) {
// check if it is possible to renew, if not, return the current token or error
if (!this.provisionCallback && !this.options.refreshToken && !this.options.serviceClient) {
if (this.accessToken) {
this.logger.debug(
{
tnx: 'OAUTH2',
user: this.options.user,
action: 'reuse'
},
'Reusing existing access token (no refresh capability) for %s',
this.options.user
);
return callback(null, this.accessToken);
}
this.logger.error(
{
tnx: 'OAUTH2',
user: this.options.user,
action: 'renew'
},
'Cannot renew access token for %s: No refresh mechanism available',
this.options.user
);
return callback(new Error("Can't create new access token for user"));
}
// If renewal already in progress, queue this request instead of starting another
if (this.renewing) {
return this.renewalQueue.push({ renew, callback });
}
this.renewing = true;
// Handles token renewal completion - processes queued requests and cleans up
const generateCallback = (err, accessToken) => {
this.renewalQueue.forEach(item => item.callback(err, accessToken));
this.renewalQueue = [];
this.renewing = false;
if (err) {
this.logger.error(
{
err: args[0],
err,
tnx: 'OAUTH2',
user: this.options.user,
action: 'renew'
@@ -108,7 +158,8 @@ class XOAuth2 extends Stream {
this.options.user
);
}
callback(...args);
// Complete original request
callback(err, accessToken);
};
if (this.provisionCallback) {
@@ -166,8 +217,8 @@ class XOAuth2 extends Stream {
let token;
try {
token = this.jwtSignRS256(tokenData);
} catch (err) {
return callback(new Error('Can\x27t generate token. Check your auth options'));
} catch (_err) {
return callback(new Error("Can't generate token. Check your auth options"));
}
urlOptions = {
@@ -181,7 +232,7 @@ class XOAuth2 extends Stream {
};
} else {
if (!this.options.refreshToken) {
return callback(new Error('Can\x27t create new access token for user'));
return callback(new Error("Can't create new access token for user"));
}
// web app - https://developers.google.com/identity/protocols/OAuth2WebServer