nodemailer 7.0.5 → 7.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/lib/base64/index.js +11 -14
- package/lib/mail-composer/index.js +26 -2
- package/lib/mime-funcs/index.js +1 -1
- package/lib/shared/index.js +55 -33
- package/lib/smtp-connection/index.js +2 -2
- package/lib/well-known/services.json +53 -0
- package/lib/xoauth2/index.js +57 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [7.0.6](https://github.com/nodemailer/nodemailer/compare/v7.0.5...v7.0.6) (2025-08-27)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **encoder:** avoid silent data loss by properly flushing trailing base64 ([#1747](https://github.com/nodemailer/nodemailer/issues/1747)) ([01ae76f](https://github.com/nodemailer/nodemailer/commit/01ae76f2cfe991c0c3fe80170f236da60531496b))
|
|
9
|
+
* handle multiple XOAUTH2 token requests correctly ([#1754](https://github.com/nodemailer/nodemailer/issues/1754)) ([dbe0028](https://github.com/nodemailer/nodemailer/commit/dbe00286351cddf012726a41a96ae613d30a34ee))
|
|
10
|
+
* ReDoS vulnerability in parseDataURI and _processDataUrl ([#1755](https://github.com/nodemailer/nodemailer/issues/1755)) ([90b3e24](https://github.com/nodemailer/nodemailer/commit/90b3e24d23929ebf9f4e16261049b40ee4055a39))
|
|
11
|
+
|
|
3
12
|
## [7.0.5](https://github.com/nodemailer/nodemailer/compare/v7.0.4...v7.0.5) (2025-07-07)
|
|
4
13
|
|
|
5
14
|
|
package/lib/base64/index.js
CHANGED
|
@@ -35,15 +35,12 @@ function wrap(str, lineLength) {
|
|
|
35
35
|
let pos = 0;
|
|
36
36
|
let chunkLength = lineLength * 1024;
|
|
37
37
|
while (pos < str.length) {
|
|
38
|
-
let wrappedLines = str
|
|
39
|
-
.substr(pos, chunkLength)
|
|
40
|
-
.replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n')
|
|
41
|
-
.trim();
|
|
38
|
+
let wrappedLines = str.substr(pos, chunkLength).replace(new RegExp('.{' + lineLength + '}', 'g'), '$&\r\n');
|
|
42
39
|
result.push(wrappedLines);
|
|
43
40
|
pos += chunkLength;
|
|
44
41
|
}
|
|
45
42
|
|
|
46
|
-
return result.join('
|
|
43
|
+
return result.join('');
|
|
47
44
|
}
|
|
48
45
|
|
|
49
46
|
/**
|
|
@@ -56,7 +53,6 @@ function wrap(str, lineLength) {
|
|
|
56
53
|
class Encoder extends Transform {
|
|
57
54
|
constructor(options) {
|
|
58
55
|
super();
|
|
59
|
-
// init Transform
|
|
60
56
|
this.options = options || {};
|
|
61
57
|
|
|
62
58
|
if (this.options.lineLength !== false) {
|
|
@@ -98,17 +94,20 @@ class Encoder extends Transform {
|
|
|
98
94
|
if (this.options.lineLength) {
|
|
99
95
|
b64 = wrap(b64, this.options.lineLength);
|
|
100
96
|
|
|
101
|
-
// remove last line as it is still most probably incomplete
|
|
102
97
|
let lastLF = b64.lastIndexOf('\n');
|
|
103
98
|
if (lastLF < 0) {
|
|
104
99
|
this._curLine = b64;
|
|
105
100
|
b64 = '';
|
|
106
|
-
} else if (lastLF === b64.length - 1) {
|
|
107
|
-
this._curLine = '';
|
|
108
101
|
} else {
|
|
109
|
-
this._curLine = b64.
|
|
110
|
-
b64 = b64.
|
|
102
|
+
this._curLine = b64.substring(lastLF + 1);
|
|
103
|
+
b64 = b64.substring(0, lastLF + 1);
|
|
104
|
+
|
|
105
|
+
if (b64 && !b64.endsWith('\r\n')) {
|
|
106
|
+
b64 += '\r\n';
|
|
107
|
+
}
|
|
111
108
|
}
|
|
109
|
+
} else {
|
|
110
|
+
this._curLine = '';
|
|
112
111
|
}
|
|
113
112
|
|
|
114
113
|
if (b64) {
|
|
@@ -125,16 +124,14 @@ class Encoder extends Transform {
|
|
|
125
124
|
}
|
|
126
125
|
|
|
127
126
|
if (this._curLine) {
|
|
128
|
-
this._curLine = wrap(this._curLine, this.options.lineLength);
|
|
129
127
|
this.outputBytes += this._curLine.length;
|
|
130
|
-
this.push(this._curLine, 'ascii');
|
|
128
|
+
this.push(Buffer.from(this._curLine, 'ascii'));
|
|
131
129
|
this._curLine = '';
|
|
132
130
|
}
|
|
133
131
|
done();
|
|
134
132
|
}
|
|
135
133
|
}
|
|
136
134
|
|
|
137
|
-
// expose to the world
|
|
138
135
|
module.exports = {
|
|
139
136
|
encode,
|
|
140
137
|
wrap,
|
|
@@ -550,9 +550,33 @@ class MailComposer {
|
|
|
550
550
|
* @return {Object} Parsed element
|
|
551
551
|
*/
|
|
552
552
|
_processDataUrl(element) {
|
|
553
|
+
const dataUrl = element.path || element.href;
|
|
554
|
+
|
|
555
|
+
// Early validation to prevent ReDoS
|
|
556
|
+
if (!dataUrl || typeof dataUrl !== 'string') {
|
|
557
|
+
return element;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!dataUrl.startsWith('data:')) {
|
|
561
|
+
return element;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (dataUrl.length > 100000) {
|
|
565
|
+
// 100KB limit for data URL string
|
|
566
|
+
// Return empty content for excessively long data URLs
|
|
567
|
+
return Object.assign({}, element, {
|
|
568
|
+
path: false,
|
|
569
|
+
href: false,
|
|
570
|
+
content: Buffer.alloc(0),
|
|
571
|
+
contentType: element.contentType || 'application/octet-stream'
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
553
575
|
let parsedDataUri;
|
|
554
|
-
|
|
555
|
-
parsedDataUri = parseDataURI(
|
|
576
|
+
try {
|
|
577
|
+
parsedDataUri = parseDataURI(dataUrl);
|
|
578
|
+
} catch (err) {
|
|
579
|
+
return element;
|
|
556
580
|
}
|
|
557
581
|
|
|
558
582
|
if (!parsedDataUri) {
|
package/lib/mime-funcs/index.js
CHANGED
|
@@ -269,7 +269,7 @@ module.exports = {
|
|
|
269
269
|
|
|
270
270
|
// first line includes the charset and language info and needs to be encoded
|
|
271
271
|
// even if it does not contain any unicode characters
|
|
272
|
-
line =
|
|
272
|
+
line = "utf-8''";
|
|
273
273
|
let encoded = true;
|
|
274
274
|
startPos = 0;
|
|
275
275
|
|
package/lib/shared/index.js
CHANGED
|
@@ -419,52 +419,74 @@ module.exports.callbackPromise = (resolve, reject) =>
|
|
|
419
419
|
};
|
|
420
420
|
|
|
421
421
|
module.exports.parseDataURI = uri => {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (!commaPos) {
|
|
425
|
-
return uri;
|
|
422
|
+
if (typeof uri !== 'string') {
|
|
423
|
+
return null;
|
|
426
424
|
}
|
|
427
425
|
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
// Early return for non-data URIs to avoid unnecessary processing
|
|
427
|
+
if (!uri.startsWith('data:')) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
430
|
|
|
431
|
-
|
|
431
|
+
// Find the first comma safely - this prevents ReDoS
|
|
432
|
+
const commaPos = uri.indexOf(',');
|
|
433
|
+
if (commaPos === -1) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
432
436
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
437
|
+
const data = uri.substring(commaPos + 1);
|
|
438
|
+
const metaStr = uri.substring('data:'.length, commaPos);
|
|
439
|
+
|
|
440
|
+
let encoding;
|
|
441
|
+
const metaEntries = metaStr.split(';');
|
|
442
|
+
|
|
443
|
+
if (metaEntries.length > 0) {
|
|
444
|
+
const lastEntry = metaEntries[metaEntries.length - 1].toLowerCase().trim();
|
|
445
|
+
// Only recognize valid encoding types to prevent manipulation
|
|
446
|
+
if (['base64', 'utf8', 'utf-8'].includes(lastEntry) && lastEntry.indexOf('=') === -1) {
|
|
447
|
+
encoding = lastEntry;
|
|
448
|
+
metaEntries.pop();
|
|
449
|
+
}
|
|
438
450
|
}
|
|
439
451
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
452
|
+
const contentType = metaEntries.length > 0 ? metaEntries.shift() : 'application/octet-stream';
|
|
453
|
+
const params = {};
|
|
454
|
+
|
|
455
|
+
for (let i = 0; i < metaEntries.length; i++) {
|
|
456
|
+
const entry = metaEntries[i];
|
|
457
|
+
const sepPos = entry.indexOf('=');
|
|
458
|
+
if (sepPos > 0) {
|
|
459
|
+
// Ensure there's a key before the '='
|
|
460
|
+
const key = entry.substring(0, sepPos).trim();
|
|
461
|
+
const value = entry.substring(sepPos + 1).trim();
|
|
462
|
+
if (key) {
|
|
463
|
+
params[key] = value;
|
|
464
|
+
}
|
|
448
465
|
}
|
|
449
466
|
}
|
|
450
467
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
break;
|
|
458
|
-
default:
|
|
468
|
+
// Decode data based on encoding with proper error handling
|
|
469
|
+
let bufferData;
|
|
470
|
+
try {
|
|
471
|
+
if (encoding === 'base64') {
|
|
472
|
+
bufferData = Buffer.from(data, 'base64');
|
|
473
|
+
} else {
|
|
459
474
|
try {
|
|
460
|
-
|
|
461
|
-
} catch (
|
|
462
|
-
|
|
475
|
+
bufferData = Buffer.from(decodeURIComponent(data));
|
|
476
|
+
} catch (decodeError) {
|
|
477
|
+
bufferData = Buffer.from(data);
|
|
463
478
|
}
|
|
464
|
-
|
|
479
|
+
}
|
|
480
|
+
} catch (bufferError) {
|
|
481
|
+
bufferData = Buffer.alloc(0);
|
|
465
482
|
}
|
|
466
483
|
|
|
467
|
-
return {
|
|
484
|
+
return {
|
|
485
|
+
data: bufferData,
|
|
486
|
+
encoding: encoding || null,
|
|
487
|
+
contentType: contentType || 'application/octet-stream',
|
|
488
|
+
params
|
|
489
|
+
};
|
|
468
490
|
};
|
|
469
491
|
|
|
470
492
|
/**
|
|
@@ -1619,7 +1619,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1619
1619
|
}
|
|
1620
1620
|
|
|
1621
1621
|
if (!this._envelope.rcptQueue.length) {
|
|
1622
|
-
return callback(this._formatError('
|
|
1622
|
+
return callback(this._formatError("Can't send mail - no recipients defined", 'EENVELOPE', false, 'API'));
|
|
1623
1623
|
} else {
|
|
1624
1624
|
this._recipientQueue = [];
|
|
1625
1625
|
|
|
@@ -1675,7 +1675,7 @@ class SMTPConnection extends EventEmitter {
|
|
|
1675
1675
|
});
|
|
1676
1676
|
this._sendCommand('DATA');
|
|
1677
1677
|
} else {
|
|
1678
|
-
err = this._formatError('
|
|
1678
|
+
err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', str, 'RCPT TO');
|
|
1679
1679
|
err.rejected = this._envelope.rejected;
|
|
1680
1680
|
err.rejectedErrors = this._envelope.rejectedErrors;
|
|
1681
1681
|
return callback(err);
|
|
@@ -43,6 +43,16 @@
|
|
|
43
43
|
"port": 587
|
|
44
44
|
},
|
|
45
45
|
|
|
46
|
+
"Aruba": {
|
|
47
|
+
"description": "Aruba PEC (Italian email provider)",
|
|
48
|
+
"domains": ["aruba.it", "pec.aruba.it"],
|
|
49
|
+
"aliases": ["Aruba PEC"],
|
|
50
|
+
"host": "smtps.aruba.it",
|
|
51
|
+
"port": 465,
|
|
52
|
+
"secure": true,
|
|
53
|
+
"authMethod": "LOGIN"
|
|
54
|
+
},
|
|
55
|
+
|
|
46
56
|
"Bluewin": {
|
|
47
57
|
"description": "Bluewin (Swiss email provider)",
|
|
48
58
|
"host": "smtpauths.bluewin.ch",
|
|
@@ -50,12 +60,29 @@
|
|
|
50
60
|
"port": 465
|
|
51
61
|
},
|
|
52
62
|
|
|
63
|
+
"BOL": {
|
|
64
|
+
"description": "BOL Mail (Brazilian provider)",
|
|
65
|
+
"domains": ["bol.com.br"],
|
|
66
|
+
"host": "smtp.bol.com.br",
|
|
67
|
+
"port": 587,
|
|
68
|
+
"requireTLS": true
|
|
69
|
+
},
|
|
70
|
+
|
|
53
71
|
"DebugMail": {
|
|
54
72
|
"description": "DebugMail (email testing service)",
|
|
55
73
|
"host": "debugmail.io",
|
|
56
74
|
"port": 25
|
|
57
75
|
},
|
|
58
76
|
|
|
77
|
+
"Disroot": {
|
|
78
|
+
"description": "Disroot (privacy-focused provider)",
|
|
79
|
+
"domains": ["disroot.org"],
|
|
80
|
+
"host": "disroot.org",
|
|
81
|
+
"port": 587,
|
|
82
|
+
"secure": false,
|
|
83
|
+
"authMethod": "LOGIN"
|
|
84
|
+
},
|
|
85
|
+
|
|
59
86
|
"DynectEmail": {
|
|
60
87
|
"description": "Dyn Email Delivery",
|
|
61
88
|
"aliases": ["Dynect"],
|
|
@@ -173,6 +200,16 @@
|
|
|
173
200
|
"port": 587
|
|
174
201
|
},
|
|
175
202
|
|
|
203
|
+
"KolabNow": {
|
|
204
|
+
"description": "KolabNow (secure email service)",
|
|
205
|
+
"domains": ["kolabnow.com"],
|
|
206
|
+
"aliases": ["Kolab"],
|
|
207
|
+
"host": "smtp.kolabnow.com",
|
|
208
|
+
"port": 465,
|
|
209
|
+
"secure": true,
|
|
210
|
+
"authMethod": "LOGIN"
|
|
211
|
+
},
|
|
212
|
+
|
|
176
213
|
"Loopia": {
|
|
177
214
|
"description": "Loopia (Swedish hosting provider)",
|
|
178
215
|
"host": "mailcluster.loopia.se",
|
|
@@ -328,6 +365,14 @@
|
|
|
328
365
|
"secure": true
|
|
329
366
|
},
|
|
330
367
|
|
|
368
|
+
"Runbox": {
|
|
369
|
+
"description": "Runbox (Norwegian email provider)",
|
|
370
|
+
"domains": ["runbox.com"],
|
|
371
|
+
"host": "smtp.runbox.com",
|
|
372
|
+
"port": 465,
|
|
373
|
+
"secure": true
|
|
374
|
+
},
|
|
375
|
+
|
|
331
376
|
"SendCloud": {
|
|
332
377
|
"description": "SendCloud (Chinese email delivery)",
|
|
333
378
|
"host": "smtp.sendcloud.net",
|
|
@@ -548,6 +593,14 @@
|
|
|
548
593
|
"secure": true
|
|
549
594
|
},
|
|
550
595
|
|
|
596
|
+
"Zimbra": {
|
|
597
|
+
"description": "Zimbra Mail Server",
|
|
598
|
+
"aliases": ["Zimbra Collaboration"],
|
|
599
|
+
"host": "smtp.zimbra.com",
|
|
600
|
+
"port": 587,
|
|
601
|
+
"requireTLS": true
|
|
602
|
+
},
|
|
603
|
+
|
|
551
604
|
"Zoho": {
|
|
552
605
|
"description": "Zoho Mail",
|
|
553
606
|
"host": "smtp.zoho.com",
|
package/lib/xoauth2/index.js
CHANGED
|
@@ -72,6 +72,9 @@ class XOAuth2 extends Stream {
|
|
|
72
72
|
let timeout = Math.max(Number(this.options.timeout) || 0, 0);
|
|
73
73
|
this.expires = (timeout && Date.now() + timeout * 1000) || 0;
|
|
74
74
|
}
|
|
75
|
+
|
|
76
|
+
this.renewing = false; // Track if renewal is in progress
|
|
77
|
+
this.renewalQueue = []; // Queue for pending requests during renewal
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
/**
|
|
@@ -82,14 +85,61 @@ class XOAuth2 extends Stream {
|
|
|
82
85
|
*/
|
|
83
86
|
getToken(renew, callback) {
|
|
84
87
|
if (!renew && this.accessToken && (!this.expires || this.expires > Date.now())) {
|
|
88
|
+
this.logger.debug(
|
|
89
|
+
{
|
|
90
|
+
tnx: 'OAUTH2',
|
|
91
|
+
user: this.options.user,
|
|
92
|
+
action: 'reuse'
|
|
93
|
+
},
|
|
94
|
+
'Reusing existing access token for %s',
|
|
95
|
+
this.options.user
|
|
96
|
+
);
|
|
85
97
|
return callback(null, this.accessToken);
|
|
86
98
|
}
|
|
87
99
|
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
// check if it is possible to renew, if not, return the current token or error
|
|
101
|
+
if (!this.provisionCallback && !this.options.refreshToken && !this.options.serviceClient) {
|
|
102
|
+
if (this.accessToken) {
|
|
103
|
+
this.logger.debug(
|
|
104
|
+
{
|
|
105
|
+
tnx: 'OAUTH2',
|
|
106
|
+
user: this.options.user,
|
|
107
|
+
action: 'reuse'
|
|
108
|
+
},
|
|
109
|
+
'Reusing existing access token (no refresh capability) for %s',
|
|
110
|
+
this.options.user
|
|
111
|
+
);
|
|
112
|
+
return callback(null, this.accessToken);
|
|
113
|
+
}
|
|
114
|
+
this.logger.error(
|
|
115
|
+
{
|
|
116
|
+
tnx: 'OAUTH2',
|
|
117
|
+
user: this.options.user,
|
|
118
|
+
action: 'renew'
|
|
119
|
+
},
|
|
120
|
+
'Cannot renew access token for %s: No refresh mechanism available',
|
|
121
|
+
this.options.user
|
|
122
|
+
);
|
|
123
|
+
return callback(new Error("Can't create new access token for user"));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// If renewal already in progress, queue this request instead of starting another
|
|
127
|
+
if (this.renewing) {
|
|
128
|
+
return this.renewalQueue.push({ renew, callback });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.renewing = true;
|
|
132
|
+
|
|
133
|
+
// Handles token renewal completion - processes queued requests and cleans up
|
|
134
|
+
const generateCallback = (err, accessToken) => {
|
|
135
|
+
this.renewalQueue.forEach(item => item.callback(err, accessToken));
|
|
136
|
+
this.renewalQueue = [];
|
|
137
|
+
this.renewing = false;
|
|
138
|
+
|
|
139
|
+
if (err) {
|
|
90
140
|
this.logger.error(
|
|
91
141
|
{
|
|
92
|
-
err
|
|
142
|
+
err,
|
|
93
143
|
tnx: 'OAUTH2',
|
|
94
144
|
user: this.options.user,
|
|
95
145
|
action: 'renew'
|
|
@@ -108,7 +158,8 @@ class XOAuth2 extends Stream {
|
|
|
108
158
|
this.options.user
|
|
109
159
|
);
|
|
110
160
|
}
|
|
111
|
-
|
|
161
|
+
// Complete original request
|
|
162
|
+
callback(err, accessToken);
|
|
112
163
|
};
|
|
113
164
|
|
|
114
165
|
if (this.provisionCallback) {
|
|
@@ -167,7 +218,7 @@ class XOAuth2 extends Stream {
|
|
|
167
218
|
try {
|
|
168
219
|
token = this.jwtSignRS256(tokenData);
|
|
169
220
|
} catch (err) {
|
|
170
|
-
return callback(new Error('
|
|
221
|
+
return callback(new Error("Can't generate token. Check your auth options"));
|
|
171
222
|
}
|
|
172
223
|
|
|
173
224
|
urlOptions = {
|
|
@@ -181,7 +232,7 @@ class XOAuth2 extends Stream {
|
|
|
181
232
|
};
|
|
182
233
|
} else {
|
|
183
234
|
if (!this.options.refreshToken) {
|
|
184
|
-
return callback(new Error('
|
|
235
|
+
return callback(new Error("Can't create new access token for user"));
|
|
185
236
|
}
|
|
186
237
|
|
|
187
238
|
// web app - https://developers.google.com/identity/protocols/OAuth2WebServer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodemailer",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.6",
|
|
4
4
|
"description": "Easy as cake e-mail sending from your Node.js applications",
|
|
5
5
|
"main": "lib/nodemailer.js",
|
|
6
6
|
"scripts": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"homepage": "https://nodemailer.com/",
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@aws-sdk/client-sesv2": "3.
|
|
26
|
+
"@aws-sdk/client-sesv2": "3.876.0",
|
|
27
27
|
"bunyan": "1.8.15",
|
|
28
28
|
"c8": "10.1.3",
|
|
29
29
|
"eslint": "8.57.0",
|