nodemailer 10.0.12 → 10.0.13
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 +8 -0
- package/dist/cjs/addressparser/index.js +97 -0
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/smtp-connection/index.js +18 -14
- package/dist/esm/addressparser/index.js +97 -0
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/smtp-connection/index.js +18 -14
- package/package.json +7 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [10.0.13](https://github.com/nodemailer/nodemailer/compare/v10.0.12...v10.0.13) (2026-09-30)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* read the advertised SASL methods without backtracking regexes ([b5a896f](https://github.com/nodemailer/nodemailer/commit/b5a896fcb02df1628329d7cd665ca08490b14b56))
|
|
9
|
+
* strip comments inside an angle-addr before it becomes the address ([a502247](https://github.com/nodemailer/nodemailer/commit/a502247555fd81773eac337486781f3b6008e16a))
|
|
10
|
+
|
|
3
11
|
## [10.0.12](https://github.com/nodemailer/nodemailer/compare/v10.0.11...v10.0.12) (2026-09-28)
|
|
4
12
|
|
|
5
13
|
|
|
@@ -242,6 +242,81 @@ function _recoverAddrSpec(data) {
|
|
|
242
242
|
.filter(part => part)
|
|
243
243
|
.join(' ');
|
|
244
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Takes the RFC 5322 comments out of the contents of an angle-addr.
|
|
247
|
+
*
|
|
248
|
+
* The tokenizer tracks a single open operator, so once '<' is open a '(' is plain text and
|
|
249
|
+
* a comment inside the brackets reached the address verbatim: 'Name <user@example.com(x)evil.com>'
|
|
250
|
+
* put 'user@example.com(x)evil.com' into the envelope, a value that is no mailbox and that a
|
|
251
|
+
* receiver stripping the comment reads as 'user@example.comevil.com' (GHSA-g73g-hqqh-jr95).
|
|
252
|
+
* A comment is folding whitespace, so it is read as a space that splits two atoms and as
|
|
253
|
+
* nothing next to an '@', the same rule the token walk applies outside the brackets. Quoted
|
|
254
|
+
* strings and domain literals are copied through untouched, a '(' in there is not a comment.
|
|
255
|
+
*
|
|
256
|
+
* @param address Contents of the angle brackets
|
|
257
|
+
* @return The address with the comments removed, and the text of the comments
|
|
258
|
+
*/
|
|
259
|
+
function _stripAddressComments(address) {
|
|
260
|
+
const comments = [];
|
|
261
|
+
let result = '';
|
|
262
|
+
let comment = '';
|
|
263
|
+
let depth = 0;
|
|
264
|
+
let closer = '';
|
|
265
|
+
// carried along rather than read back off the growing result, which would flatten it on
|
|
266
|
+
// every comment (see lastChars in _handleAddress)
|
|
267
|
+
let lastChar = '';
|
|
268
|
+
for (let i = 0, len = address.length; i < len; i++) {
|
|
269
|
+
const chr = address.charAt(i);
|
|
270
|
+
if (depth) {
|
|
271
|
+
if (chr === '\\' && i < len - 1) {
|
|
272
|
+
comment += address.charAt(++i);
|
|
273
|
+
}
|
|
274
|
+
else if (chr === '(') {
|
|
275
|
+
depth++;
|
|
276
|
+
comment += chr;
|
|
277
|
+
}
|
|
278
|
+
else if (chr === ')' && !--depth) {
|
|
279
|
+
comments.push(comment.trim());
|
|
280
|
+
comment = '';
|
|
281
|
+
if (lastChar !== '@' && address.charAt(i + 1) !== '@') {
|
|
282
|
+
result += ' ';
|
|
283
|
+
lastChar = ' ';
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
comment += chr;
|
|
288
|
+
}
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
if (closer) {
|
|
292
|
+
if (chr === '\\' && closer === '"' && i < len - 1) {
|
|
293
|
+
result += chr + address.charAt(++i);
|
|
294
|
+
lastChar = address.charAt(i);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (chr === closer) {
|
|
298
|
+
closer = '';
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else if (chr === '"') {
|
|
302
|
+
closer = '"';
|
|
303
|
+
}
|
|
304
|
+
else if (chr === '[') {
|
|
305
|
+
closer = ']';
|
|
306
|
+
}
|
|
307
|
+
else if (chr === '(') {
|
|
308
|
+
depth = 1;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
result += chr;
|
|
312
|
+
lastChar = chr;
|
|
313
|
+
}
|
|
314
|
+
if (depth) {
|
|
315
|
+
// an unterminated comment runs to the end of the address
|
|
316
|
+
comments.push(comment.trim());
|
|
317
|
+
}
|
|
318
|
+
return { address: result.trim(), comments: comments.filter(text => text) };
|
|
319
|
+
}
|
|
245
320
|
/**
|
|
246
321
|
* Converts tokens for a single address into an address object
|
|
247
322
|
*
|
|
@@ -369,6 +444,24 @@ function _handleAddress(tokens, depth) {
|
|
|
369
444
|
});
|
|
370
445
|
}
|
|
371
446
|
else {
|
|
447
|
+
// Comments come out of the angle-addr before anything asks whether one was found, so
|
|
448
|
+
// that brackets holding nothing but a comment read the same as empty ones
|
|
449
|
+
const addressComments = [];
|
|
450
|
+
const addressParts = [];
|
|
451
|
+
for (const part of data.address) {
|
|
452
|
+
if (part.indexOf('(') < 0) {
|
|
453
|
+
addressParts.push(part);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
const stripped = _stripAddressComments(part);
|
|
457
|
+
for (const comment of stripped.comments) {
|
|
458
|
+
addressComments.push(comment);
|
|
459
|
+
}
|
|
460
|
+
if (stripped.address) {
|
|
461
|
+
addressParts.push(stripped.address);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
data.address = addressParts;
|
|
372
465
|
// If no address was found, try to detect one from regular text
|
|
373
466
|
if (!data.address.length && data.text.length) {
|
|
374
467
|
for (let i = data.text.length - 1; i >= 0; i--) {
|
|
@@ -435,6 +528,10 @@ function _handleAddress(tokens, depth) {
|
|
|
435
528
|
data.text = '';
|
|
436
529
|
}
|
|
437
530
|
_recoverAddrSpec(data);
|
|
531
|
+
// a comment names the mailbox only when nothing else does, as one outside the brackets
|
|
532
|
+
if (!data.text && addressComments.length) {
|
|
533
|
+
data.text = addressComments.join(' ');
|
|
534
|
+
}
|
|
438
535
|
const address = {
|
|
439
536
|
address: data.address || data.text || '',
|
|
440
537
|
name: data.text || data.address || ''
|
package/dist/cjs/package-info.js
CHANGED
|
@@ -1394,21 +1394,25 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
1394
1394
|
if (/[ -]AUTH\b/i.test(str)) {
|
|
1395
1395
|
this.allowsAuth = true;
|
|
1396
1396
|
}
|
|
1397
|
-
// Detect
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
//
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1397
|
+
// Detect the advertised SASL mechanisms. The list is split into whole tokens rather
|
|
1398
|
+
// than searched for each name with a pattern: the patterns this replaced let two
|
|
1399
|
+
// whitespace runs overlap and backtracked quadratically over an AUTH line padded with
|
|
1400
|
+
// spaces, so a hostile server could stall the event loop from its EHLO reply
|
|
1401
|
+
// (GHSA-4ffr-jq9g-5ffx).
|
|
1402
|
+
const authMechanisms = new Set();
|
|
1403
|
+
for (const line of this._ehloLines) {
|
|
1404
|
+
const authMatch = /^AUTH[\s=](.*)/i.exec(line);
|
|
1405
|
+
if (authMatch) {
|
|
1406
|
+
for (const mechanism of authMatch[1].split(/[\s=]+/)) {
|
|
1407
|
+
authMechanisms.add(mechanism.toUpperCase());
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1408
1410
|
}
|
|
1409
|
-
//
|
|
1410
|
-
|
|
1411
|
-
|
|
1411
|
+
// listed in order of preference, the first one the credentials allow is used
|
|
1412
|
+
for (const mechanism of ['PLAIN', 'LOGIN', 'CRAM-MD5', 'XOAUTH2']) {
|
|
1413
|
+
if (authMechanisms.has(mechanism)) {
|
|
1414
|
+
this._supportedAuth.push(mechanism);
|
|
1415
|
+
}
|
|
1412
1416
|
}
|
|
1413
1417
|
// Detect if the server supports SIZE extensions (and the max allowed size)
|
|
1414
1418
|
if ((match = str.match(/[ -]SIZE(?:[ \t]+(\d+))?/im))) {
|
|
@@ -240,6 +240,81 @@ function _recoverAddrSpec(data) {
|
|
|
240
240
|
.filter(part => part)
|
|
241
241
|
.join(' ');
|
|
242
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Takes the RFC 5322 comments out of the contents of an angle-addr.
|
|
245
|
+
*
|
|
246
|
+
* The tokenizer tracks a single open operator, so once '<' is open a '(' is plain text and
|
|
247
|
+
* a comment inside the brackets reached the address verbatim: 'Name <user@example.com(x)evil.com>'
|
|
248
|
+
* put 'user@example.com(x)evil.com' into the envelope, a value that is no mailbox and that a
|
|
249
|
+
* receiver stripping the comment reads as 'user@example.comevil.com' (GHSA-g73g-hqqh-jr95).
|
|
250
|
+
* A comment is folding whitespace, so it is read as a space that splits two atoms and as
|
|
251
|
+
* nothing next to an '@', the same rule the token walk applies outside the brackets. Quoted
|
|
252
|
+
* strings and domain literals are copied through untouched, a '(' in there is not a comment.
|
|
253
|
+
*
|
|
254
|
+
* @param address Contents of the angle brackets
|
|
255
|
+
* @return The address with the comments removed, and the text of the comments
|
|
256
|
+
*/
|
|
257
|
+
function _stripAddressComments(address) {
|
|
258
|
+
const comments = [];
|
|
259
|
+
let result = '';
|
|
260
|
+
let comment = '';
|
|
261
|
+
let depth = 0;
|
|
262
|
+
let closer = '';
|
|
263
|
+
// carried along rather than read back off the growing result, which would flatten it on
|
|
264
|
+
// every comment (see lastChars in _handleAddress)
|
|
265
|
+
let lastChar = '';
|
|
266
|
+
for (let i = 0, len = address.length; i < len; i++) {
|
|
267
|
+
const chr = address.charAt(i);
|
|
268
|
+
if (depth) {
|
|
269
|
+
if (chr === '\\' && i < len - 1) {
|
|
270
|
+
comment += address.charAt(++i);
|
|
271
|
+
}
|
|
272
|
+
else if (chr === '(') {
|
|
273
|
+
depth++;
|
|
274
|
+
comment += chr;
|
|
275
|
+
}
|
|
276
|
+
else if (chr === ')' && !--depth) {
|
|
277
|
+
comments.push(comment.trim());
|
|
278
|
+
comment = '';
|
|
279
|
+
if (lastChar !== '@' && address.charAt(i + 1) !== '@') {
|
|
280
|
+
result += ' ';
|
|
281
|
+
lastChar = ' ';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
comment += chr;
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (closer) {
|
|
290
|
+
if (chr === '\\' && closer === '"' && i < len - 1) {
|
|
291
|
+
result += chr + address.charAt(++i);
|
|
292
|
+
lastChar = address.charAt(i);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (chr === closer) {
|
|
296
|
+
closer = '';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (chr === '"') {
|
|
300
|
+
closer = '"';
|
|
301
|
+
}
|
|
302
|
+
else if (chr === '[') {
|
|
303
|
+
closer = ']';
|
|
304
|
+
}
|
|
305
|
+
else if (chr === '(') {
|
|
306
|
+
depth = 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
result += chr;
|
|
310
|
+
lastChar = chr;
|
|
311
|
+
}
|
|
312
|
+
if (depth) {
|
|
313
|
+
// an unterminated comment runs to the end of the address
|
|
314
|
+
comments.push(comment.trim());
|
|
315
|
+
}
|
|
316
|
+
return { address: result.trim(), comments: comments.filter(text => text) };
|
|
317
|
+
}
|
|
243
318
|
/**
|
|
244
319
|
* Converts tokens for a single address into an address object
|
|
245
320
|
*
|
|
@@ -367,6 +442,24 @@ function _handleAddress(tokens, depth) {
|
|
|
367
442
|
});
|
|
368
443
|
}
|
|
369
444
|
else {
|
|
445
|
+
// Comments come out of the angle-addr before anything asks whether one was found, so
|
|
446
|
+
// that brackets holding nothing but a comment read the same as empty ones
|
|
447
|
+
const addressComments = [];
|
|
448
|
+
const addressParts = [];
|
|
449
|
+
for (const part of data.address) {
|
|
450
|
+
if (part.indexOf('(') < 0) {
|
|
451
|
+
addressParts.push(part);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const stripped = _stripAddressComments(part);
|
|
455
|
+
for (const comment of stripped.comments) {
|
|
456
|
+
addressComments.push(comment);
|
|
457
|
+
}
|
|
458
|
+
if (stripped.address) {
|
|
459
|
+
addressParts.push(stripped.address);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
data.address = addressParts;
|
|
370
463
|
// If no address was found, try to detect one from regular text
|
|
371
464
|
if (!data.address.length && data.text.length) {
|
|
372
465
|
for (let i = data.text.length - 1; i >= 0; i--) {
|
|
@@ -433,6 +526,10 @@ function _handleAddress(tokens, depth) {
|
|
|
433
526
|
data.text = '';
|
|
434
527
|
}
|
|
435
528
|
_recoverAddrSpec(data);
|
|
529
|
+
// a comment names the mailbox only when nothing else does, as one outside the brackets
|
|
530
|
+
if (!data.text && addressComments.length) {
|
|
531
|
+
data.text = addressComments.join(' ');
|
|
532
|
+
}
|
|
436
533
|
const address = {
|
|
437
534
|
address: data.address || data.text || '',
|
|
438
535
|
name: data.text || data.address || ''
|
package/dist/esm/package-info.js
CHANGED
|
@@ -1356,21 +1356,25 @@ class SMTPConnection extends EventEmitter {
|
|
|
1356
1356
|
if (/[ -]AUTH\b/i.test(str)) {
|
|
1357
1357
|
this.allowsAuth = true;
|
|
1358
1358
|
}
|
|
1359
|
-
// Detect
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
//
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1359
|
+
// Detect the advertised SASL mechanisms. The list is split into whole tokens rather
|
|
1360
|
+
// than searched for each name with a pattern: the patterns this replaced let two
|
|
1361
|
+
// whitespace runs overlap and backtracked quadratically over an AUTH line padded with
|
|
1362
|
+
// spaces, so a hostile server could stall the event loop from its EHLO reply
|
|
1363
|
+
// (GHSA-4ffr-jq9g-5ffx).
|
|
1364
|
+
const authMechanisms = new Set();
|
|
1365
|
+
for (const line of this._ehloLines) {
|
|
1366
|
+
const authMatch = /^AUTH[\s=](.*)/i.exec(line);
|
|
1367
|
+
if (authMatch) {
|
|
1368
|
+
for (const mechanism of authMatch[1].split(/[\s=]+/)) {
|
|
1369
|
+
authMechanisms.add(mechanism.toUpperCase());
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1370
1372
|
}
|
|
1371
|
-
//
|
|
1372
|
-
|
|
1373
|
-
|
|
1373
|
+
// listed in order of preference, the first one the credentials allow is used
|
|
1374
|
+
for (const mechanism of ['PLAIN', 'LOGIN', 'CRAM-MD5', 'XOAUTH2']) {
|
|
1375
|
+
if (authMechanisms.has(mechanism)) {
|
|
1376
|
+
this._supportedAuth.push(mechanism);
|
|
1377
|
+
}
|
|
1374
1378
|
}
|
|
1375
1379
|
// Detect if the server supports SIZE extensions (and the max allowed size)
|
|
1376
1380
|
if ((match = str.match(/[ -]SIZE(?:[ \t]+(\d+))?/im))) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodemailer",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.13",
|
|
4
4
|
"description": "Easy as cake e-mail sending from your Node.js applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/nodemailer.js",
|
|
@@ -151,24 +151,24 @@
|
|
|
151
151
|
},
|
|
152
152
|
"homepage": "https://nodemailer.com/",
|
|
153
153
|
"devDependencies": {
|
|
154
|
-
"@aws-sdk/client-sesv2": "3.
|
|
154
|
+
"@aws-sdk/client-sesv2": "3.1143.0",
|
|
155
155
|
"@types/node": "20.19.43",
|
|
156
156
|
"bunyan": "1.8.15",
|
|
157
157
|
"c8": "12.0.0",
|
|
158
158
|
"eslint": "10.11.0",
|
|
159
159
|
"eslint-config-prettier": "10.1.8",
|
|
160
160
|
"globals": "17.12.0",
|
|
161
|
-
"libbase64": "1.3.
|
|
162
|
-
"libmime": "5.4.
|
|
163
|
-
"libqp": "2.1.
|
|
161
|
+
"libbase64": "1.3.1",
|
|
162
|
+
"libmime": "5.4.6",
|
|
163
|
+
"libqp": "2.1.2",
|
|
164
164
|
"mailauth": "7.1.0",
|
|
165
165
|
"prettier": "3.9.9",
|
|
166
166
|
"proxy": "1.0.2",
|
|
167
167
|
"proxy-test-server": "1.0.0",
|
|
168
|
-
"smtp-server": "3.19.
|
|
168
|
+
"smtp-server": "3.19.15",
|
|
169
169
|
"tsx": "4.23.15",
|
|
170
170
|
"typescript": "6.0.3",
|
|
171
|
-
"typescript-eslint": "8.
|
|
171
|
+
"typescript-eslint": "8.71.0"
|
|
172
172
|
},
|
|
173
173
|
"engines": {
|
|
174
174
|
"node": ">=20.0.0"
|