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

8
node_modules/nodemailer/.ncurc.js generated vendored
View File

@@ -1,11 +1,9 @@
'use strict';
module.exports = {
upgrade: true,
reject: [
// API changes break existing tests
'proxy',
// API changes
'eslint',
'eslint-config-prettier'
'proxy'
]
};

8
node_modules/nodemailer/.prettierignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
node_modules
coverage
*.min.js
dist
build
.nyc_output
package-lock.json
CHANGELOG.md

12
node_modules/nodemailer/.prettierrc generated vendored Normal file
View File

@@ -0,0 +1,12 @@
{
"printWidth": 140,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"endOfLine": "lf"
}

View File

@@ -1,3 +1,5 @@
'use strict';
module.exports = {
printWidth: 160,
tabWidth: 4,

9
node_modules/nodemailer/.release-please-config.json generated vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"packages": {
".": {
"release-type": "node",
"package-name": "nodemailer",
"pull-request-title-pattern": "chore${scope}: release ${version} [skip-ci]"
}
}
}

651
node_modules/nodemailer/CHANGELOG.md generated vendored

File diff suppressed because it is too large Load Diff

View File

@@ -14,22 +14,22 @@ appearance, race, religion, or sexual identity and orientation.
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities

20
node_modules/nodemailer/README.md generated vendored
View File

@@ -37,20 +37,20 @@ It's either a firewall issue, or your SMTP server blocks authentication attempts
#### I get TLS errors
- If you are running the code on your machine, check your antivirus settings. Antiviruses often mess around with email ports usage. Node.js might not recognize the MITM cert your antivirus is using.
- Latest Node versions allow only TLS versions 1.2 and higher. Some servers might still use TLS 1.1 or lower. Check Node.js docs on how to get correct TLS support for your app. You can change this with [tls.minVersion](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tls_tls_createsecurecontext_options) option
- You might have the wrong value for the `secure` option. This should be set to `true` only for port 465. For every other port, it should be `false`. Setting it to `false` does not mean that Nodemailer would not use TLS. Nodemailer would still try to upgrade the connection to use TLS if the server supports it.
- Older Node versions do not fully support the certificate chain of the newest Let's Encrypt certificates. Either set [tls.rejectUnauthorized](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tlsconnectoptions-callback) to `false` to skip chain verification or upgrade your Node version
- If you are running the code on your machine, check your antivirus settings. Antiviruses often mess around with email ports usage. Node.js might not recognize the MITM cert your antivirus is using.
- Latest Node versions allow only TLS versions 1.2 and higher. Some servers might still use TLS 1.1 or lower. Check Node.js docs on how to get correct TLS support for your app. You can change this with [tls.minVersion](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tls_tls_createsecurecontext_options) option
- You might have the wrong value for the `secure` option. This should be set to `true` only for port 465. For every other port, it should be `false`. Setting it to `false` does not mean that Nodemailer would not use TLS. Nodemailer would still try to upgrade the connection to use TLS if the server supports it.
- Older Node versions do not fully support the certificate chain of the newest Let's Encrypt certificates. Either set [tls.rejectUnauthorized](https://nodejs.org/dist/latest-v16.x/docs/api/tls.html#tlsconnectoptions-callback) to `false` to skip chain verification or upgrade your Node version
```js
let configOptions = {
host: "smtp.example.com",
host: 'smtp.example.com',
port: 587,
tls: {
rejectUnauthorized: true,
minVersion: "TLSv1.2"
minVersion: 'TLSv1.2'
}
}
};
```
#### I have issues with DNS / hosts file
@@ -59,14 +59,14 @@ Node.js uses [c-ares](https://nodejs.org/en/docs/meta/topics/dependencies/#c-are
```js
let configOptions = {
host: "1.2.3.4",
host: '1.2.3.4',
port: 465,
secure: true,
tls: {
// must provide server name, otherwise TLS certificate check will fail
servername: "example.com"
servername: 'example.com'
}
}
};
```
#### I have an issue with TypeScript types

88
node_modules/nodemailer/eslint.config.js generated vendored Normal file
View File

@@ -0,0 +1,88 @@
'use strict';
const globals = require('globals');
module.exports = [
{
ignores: ['node_modules/**', 'coverage/**', 'dist/**', 'build/**', '.nyc_output/**']
},
{
files: ['**/*.js'],
languageOptions: {
ecmaVersion: 2017,
sourceType: 'script',
globals: Object.assign({}, globals.node, globals.es2017, {
it: true,
describe: true,
beforeEach: true,
afterEach: true
})
},
rules: {
// Error detection
'for-direction': 'error',
'no-await-in-loop': 'error',
'no-div-regex': 'error',
eqeqeq: 'error',
'dot-notation': 'error',
curly: 'error',
'no-fallthrough': 'error',
'no-unused-expressions': [
'error',
{
allowShortCircuit: true
}
],
'no-unused-vars': [
'error',
{
varsIgnorePattern: '^_',
argsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_'
}
],
'handle-callback-err': 'error',
'no-new': 'error',
'new-cap': 'error',
'no-eval': 'error',
'no-invalid-this': 'error',
radix: ['error', 'always'],
'no-use-before-define': ['error', 'nofunc'],
'callback-return': ['error', ['callback', 'cb', 'done']],
'no-regex-spaces': 'error',
'no-empty': 'error',
'no-duplicate-case': 'error',
'no-empty-character-class': 'error',
'no-redeclare': 'off', // Disabled per project preference
'block-scoped-var': 'error',
'no-sequences': 'error',
'no-throw-literal': 'error',
'no-useless-call': 'error',
'no-useless-concat': 'error',
'no-void': 'error',
yoda: 'error',
'no-undef': 'error',
'global-require': 'error',
'no-var': 'error',
'no-bitwise': 'error',
'no-lonely-if': 'error',
'no-mixed-spaces-and-tabs': 'error',
'arrow-body-style': ['error', 'as-needed'],
'arrow-parens': ['error', 'as-needed'],
'prefer-arrow-callback': 'error',
'object-shorthand': 'error',
'prefer-spread': 'error',
'no-prototype-builtins': 'off', // Disabled per project preference
strict: ['error', 'global'],
// Disable all formatting rules (handled by Prettier)
indent: 'off',
quotes: 'off',
'linebreak-style': 'off',
semi: 'off',
'quote-props': 'off',
'comma-dangle': 'off',
'comma-style': 'off'
}
}
];

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

18
node_modules/nodemailer/package.json generated vendored
View File

@@ -1,12 +1,15 @@
{
"name": "nodemailer",
"version": "7.0.3",
"version": "7.0.12",
"description": "Easy as cake e-mail sending from your Node.js applications",
"main": "lib/nodemailer.js",
"scripts": {
"test": "node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js",
"test:coverage": "c8 node --test --test-concurrency=1 test/**/*.test.js test/**/*-test.js",
"format": "prettier --write \"**/*.{js,json,md}\"",
"format:check": "prettier --check \"**/*.{js,json,md}\"",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"update": "rm -rf node_modules/ package-lock.json && ncu -u && npm install"
},
"repository": {
@@ -23,19 +26,20 @@
},
"homepage": "https://nodemailer.com/",
"devDependencies": {
"@aws-sdk/client-sesv2": "3.804.0",
"@aws-sdk/client-sesv2": "3.940.0",
"bunyan": "1.8.15",
"c8": "10.1.3",
"eslint": "8.57.0",
"eslint-config-nodemailer": "1.2.0",
"eslint-config-prettier": "9.1.0",
"eslint": "9.39.1",
"eslint-config-prettier": "10.1.8",
"globals": "16.5.0",
"libbase64": "1.3.0",
"libmime": "5.3.6",
"libmime": "5.3.7",
"libqp": "2.1.1",
"nodemailer-ntlm-auth": "1.0.4",
"prettier": "3.6.2",
"proxy": "1.0.2",
"proxy-test-server": "1.0.0",
"smtp-server": "3.13.6"
"smtp-server": "3.16.1"
},
"engines": {
"node": ">=6.0.0"