nodemailer 10.2.0 → 10.2.1

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [10.2.1](https://github.com/nodemailer/nodemailer/compare/v10.2.0...v10.2.1) (2026-10-22)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * drop the comments of a Content-Type or Content-Disposition value with the stripComments option ([b3052b7](https://github.com/nodemailer/nodemailer/commit/b3052b7fe2e49b51dd66d6c7814a072e885f1ac8))
9
+
3
10
  ## [10.2.0](https://github.com/nodemailer/nodemailer/compare/v10.1.7...v10.2.0) (2026-10-21)
4
11
 
5
12
 
@@ -107,6 +107,22 @@ export declare function buildHeaderValue(structured: StructuredHeaderValue): str
107
107
  * @return A list of encoded keys and headers
108
108
  */
109
109
  export declare function buildHeaderParam(key: string, data: string | Buffer, maxLength?: number): EncodedHeaderParam[];
110
+ /**
111
+ * Options of parseHeaderValue()
112
+ */
113
+ export interface ParseHeaderValueOptions {
114
+ /**
115
+ * Drop the RFC 822 comments outside quoted strings, which the MIME headers allow and
116
+ * which are not part of the value: 'boundary=b (comment)' names the boundary 'b'. A
117
+ * parenthesis right behind the '=' of a parameter or behind a token character of its
118
+ * value continues the token, as it does for Python and postal-mime, so 'Invoice(1).pdf'
119
+ * keeps its name and 'boundary=(c)b' names the boundary '(c)b'. A comment that never
120
+ * closes runs to the end of the value, unless a parameter follows it, in which case it is
121
+ * content and the parameter survives. Comments are kept by default for the sake of the
122
+ * callers of this exported function, not because keeping them is ever the better reading
123
+ */
124
+ stripComments?: boolean | undefined;
125
+ }
110
126
  /**
111
127
  * Parses a header value with key=value arguments into a structured
112
128
  * object.
@@ -120,9 +136,10 @@ export declare function buildHeaderParam(key: string, data: string | Buffer, max
120
136
  * }
121
137
  *
122
138
  * @param str Header value
139
+ * @param options Options, see ParseHeaderValueOptions
123
140
  * @return Header value as a parsed structure
124
141
  */
125
- export declare function parseHeaderValue(str: string): ParsedHeaderValue;
142
+ export declare function parseHeaderValue(str: string, options?: ParseHeaderValueOptions): ParsedHeaderValue;
126
143
  /**
127
144
  * Returns file extension for a content type string. If no suitable extensions
128
145
  * are found, 'bin' is used as the default extension
@@ -414,6 +414,26 @@ function buildHeaderParam(key, data, maxLength) {
414
414
  * goes in. Whitespace used to come off it only because the value was trimmed first.
415
415
  */
416
416
  const TOKEN = /^[^\x00-\x20\x7f()<>@,;:\\"/[\]?=]+$/;
417
+ /**
418
+ * Offset of the ')' that closes the RFC 822 comment opening at `start`, or -1 when the
419
+ * comment never closes. Comments nest, and a backslash escapes the character after it
420
+ */
421
+ function _findCommentEnd(str, start) {
422
+ let depth = 0;
423
+ for (let i = start, len = str.length; i < len; i++) {
424
+ const c = str.charAt(i);
425
+ if (c === '\\') {
426
+ i++;
427
+ }
428
+ else if (c === '(') {
429
+ depth++;
430
+ }
431
+ else if (c === ')' && --depth === 0) {
432
+ return i;
433
+ }
434
+ }
435
+ return -1;
436
+ }
417
437
  /**
418
438
  * Whether a string can be a header parameter name.
419
439
  *
@@ -440,13 +460,16 @@ function _isParamName(name) {
440
460
  * }
441
461
  *
442
462
  * @param str Header value
463
+ * @param options Options, see ParseHeaderValueOptions
443
464
  * @return Header value as a parsed structure
444
465
  */
445
- function parseHeaderValue(str) {
466
+ function parseHeaderValue(str, options) {
446
467
  const response = {
447
468
  value: '',
448
469
  params: {}
449
470
  };
471
+ // Cleared again by an unclosed '(', see the loop
472
+ let stripComments = !!options?.stripComments;
450
473
  // A duplicated parameter resolves to its first occurrence, the way a duplicated header
451
474
  // does. Letting the last one win disagrees with the receivers that take the first, and
452
475
  // the two readings of 'boundary="b"; boundary="c"' name different delimiters. The
@@ -508,6 +531,25 @@ function parseHeaderValue(str) {
508
531
  };
509
532
  for (let i = 0, len = str.length; i < len; i++) {
510
533
  chr = str.charAt(i);
534
+ // With stripComments, an RFC 822 comment stands anywhere outside a quoted string but
535
+ // right behind a token it would continue, see ParseHeaderValueOptions. In the value
536
+ // of a parameter, no pending whitespace means the last significant character was the
537
+ // '=' or a character of the token
538
+ if (stripComments && chr === '(' && !quote && !quoteClosed && !(stage === 'value' && key !== false && !pendingSpace)) {
539
+ let end = _findCommentEnd(str, i);
540
+ if (end < 0 && str.indexOf(';', i) < 0) {
541
+ // a comment that never closes runs to the end of the value, unless a
542
+ // parameter follows it, which is content then and survives
543
+ end = len - 1;
544
+ }
545
+ if (end >= 0) {
546
+ i = end;
547
+ continue;
548
+ }
549
+ // looking for a closing ')' again would scan a run of unclosed '(' to its end
550
+ // once for each of them
551
+ stripComments = false;
552
+ }
511
553
  if (stage === 'key') {
512
554
  if (chr === '=') {
513
555
  key = takeValue().trim();
@@ -654,15 +654,18 @@ class MimeNode {
654
654
  return;
655
655
  }
656
656
  switch (header.key) {
657
+ // The comments of a MIME header are not part of its value and are dropped, so a
658
+ // caller supplied 'boundary=b (comment)' is the boundary b it names, and the
659
+ // header goes out without the comment
657
660
  case 'Content-Disposition':
658
- structured = mimeFuncs.parseHeaderValue(value);
661
+ structured = mimeFuncs.parseHeaderValue(value, { stripComments: true });
659
662
  if (this.filename) {
660
663
  structured.params.filename = this.filename;
661
664
  }
662
665
  value = mimeFuncs.buildHeaderValue(structured);
663
666
  break;
664
667
  case 'Content-Type':
665
- structured = mimeFuncs.parseHeaderValue(value);
668
+ structured = mimeFuncs.parseHeaderValue(value, { stripComments: true });
666
669
  // the type token decides multipart and charset below, so clean it before
667
670
  // those run and not just on the way out, otherwise a control char makes
668
671
  // the checks miss and the header ends up claiming a type it is not set up for
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.2.0";
2
+ export declare const version = "10.2.1";
3
3
  export declare const homepage = "https://nodemailer.com/";
@@ -3,5 +3,5 @@
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.homepage = exports.version = exports.name = void 0;
5
5
  exports.name = 'nodemailer';
6
- exports.version = '10.2.0';
6
+ exports.version = '10.2.1';
7
7
  exports.homepage = 'https://nodemailer.com/';
@@ -107,6 +107,22 @@ export declare function buildHeaderValue(structured: StructuredHeaderValue): str
107
107
  * @return A list of encoded keys and headers
108
108
  */
109
109
  export declare function buildHeaderParam(key: string, data: string | Buffer, maxLength?: number): EncodedHeaderParam[];
110
+ /**
111
+ * Options of parseHeaderValue()
112
+ */
113
+ export interface ParseHeaderValueOptions {
114
+ /**
115
+ * Drop the RFC 822 comments outside quoted strings, which the MIME headers allow and
116
+ * which are not part of the value: 'boundary=b (comment)' names the boundary 'b'. A
117
+ * parenthesis right behind the '=' of a parameter or behind a token character of its
118
+ * value continues the token, as it does for Python and postal-mime, so 'Invoice(1).pdf'
119
+ * keeps its name and 'boundary=(c)b' names the boundary '(c)b'. A comment that never
120
+ * closes runs to the end of the value, unless a parameter follows it, in which case it is
121
+ * content and the parameter survives. Comments are kept by default for the sake of the
122
+ * callers of this exported function, not because keeping them is ever the better reading
123
+ */
124
+ stripComments?: boolean | undefined;
125
+ }
110
126
  /**
111
127
  * Parses a header value with key=value arguments into a structured
112
128
  * object.
@@ -120,9 +136,10 @@ export declare function buildHeaderParam(key: string, data: string | Buffer, max
120
136
  * }
121
137
  *
122
138
  * @param str Header value
139
+ * @param options Options, see ParseHeaderValueOptions
123
140
  * @return Header value as a parsed structure
124
141
  */
125
- export declare function parseHeaderValue(str: string): ParsedHeaderValue;
142
+ export declare function parseHeaderValue(str: string, options?: ParseHeaderValueOptions): ParsedHeaderValue;
126
143
  /**
127
144
  * Returns file extension for a content type string. If no suitable extensions
128
145
  * are found, 'bin' is used as the default extension
@@ -365,6 +365,26 @@ export function buildHeaderParam(key, data, maxLength) {
365
365
  * goes in. Whitespace used to come off it only because the value was trimmed first.
366
366
  */
367
367
  const TOKEN = /^[^\x00-\x20\x7f()<>@,;:\\"/[\]?=]+$/;
368
+ /**
369
+ * Offset of the ')' that closes the RFC 822 comment opening at `start`, or -1 when the
370
+ * comment never closes. Comments nest, and a backslash escapes the character after it
371
+ */
372
+ function _findCommentEnd(str, start) {
373
+ let depth = 0;
374
+ for (let i = start, len = str.length; i < len; i++) {
375
+ const c = str.charAt(i);
376
+ if (c === '\\') {
377
+ i++;
378
+ }
379
+ else if (c === '(') {
380
+ depth++;
381
+ }
382
+ else if (c === ')' && --depth === 0) {
383
+ return i;
384
+ }
385
+ }
386
+ return -1;
387
+ }
368
388
  /**
369
389
  * Whether a string can be a header parameter name.
370
390
  *
@@ -391,13 +411,16 @@ function _isParamName(name) {
391
411
  * }
392
412
  *
393
413
  * @param str Header value
414
+ * @param options Options, see ParseHeaderValueOptions
394
415
  * @return Header value as a parsed structure
395
416
  */
396
- export function parseHeaderValue(str) {
417
+ export function parseHeaderValue(str, options) {
397
418
  const response = {
398
419
  value: '',
399
420
  params: {}
400
421
  };
422
+ // Cleared again by an unclosed '(', see the loop
423
+ let stripComments = !!options?.stripComments;
401
424
  // A duplicated parameter resolves to its first occurrence, the way a duplicated header
402
425
  // does. Letting the last one win disagrees with the receivers that take the first, and
403
426
  // the two readings of 'boundary="b"; boundary="c"' name different delimiters. The
@@ -459,6 +482,25 @@ export function parseHeaderValue(str) {
459
482
  };
460
483
  for (let i = 0, len = str.length; i < len; i++) {
461
484
  chr = str.charAt(i);
485
+ // With stripComments, an RFC 822 comment stands anywhere outside a quoted string but
486
+ // right behind a token it would continue, see ParseHeaderValueOptions. In the value
487
+ // of a parameter, no pending whitespace means the last significant character was the
488
+ // '=' or a character of the token
489
+ if (stripComments && chr === '(' && !quote && !quoteClosed && !(stage === 'value' && key !== false && !pendingSpace)) {
490
+ let end = _findCommentEnd(str, i);
491
+ if (end < 0 && str.indexOf(';', i) < 0) {
492
+ // a comment that never closes runs to the end of the value, unless a
493
+ // parameter follows it, which is content then and survives
494
+ end = len - 1;
495
+ }
496
+ if (end >= 0) {
497
+ i = end;
498
+ continue;
499
+ }
500
+ // looking for a closing ')' again would scan a run of unclosed '(' to its end
501
+ // once for each of them
502
+ stripComments = false;
503
+ }
462
504
  if (stage === 'key') {
463
505
  if (chr === '=') {
464
506
  key = takeValue().trim();
@@ -616,15 +616,18 @@ class MimeNode {
616
616
  return;
617
617
  }
618
618
  switch (header.key) {
619
+ // The comments of a MIME header are not part of its value and are dropped, so a
620
+ // caller supplied 'boundary=b (comment)' is the boundary b it names, and the
621
+ // header goes out without the comment
619
622
  case 'Content-Disposition':
620
- structured = mimeFuncs.parseHeaderValue(value);
623
+ structured = mimeFuncs.parseHeaderValue(value, { stripComments: true });
621
624
  if (this.filename) {
622
625
  structured.params.filename = this.filename;
623
626
  }
624
627
  value = mimeFuncs.buildHeaderValue(structured);
625
628
  break;
626
629
  case 'Content-Type':
627
- structured = mimeFuncs.parseHeaderValue(value);
630
+ structured = mimeFuncs.parseHeaderValue(value, { stripComments: true });
628
631
  // the type token decides multipart and charset below, so clean it before
629
632
  // those run and not just on the way out, otherwise a control char makes
630
633
  // the checks miss and the header ends up claiming a type it is not set up for
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.2.0";
2
+ export declare const version = "10.2.1";
3
3
  export declare const homepage = "https://nodemailer.com/";
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/build.js from package.json. Do not edit by hand.
2
2
  export const name = 'nodemailer';
3
- export const version = '10.2.0';
3
+ export const version = '10.2.1';
4
4
  export const homepage = 'https://nodemailer.com/';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "10.2.0",
3
+ "version": "10.2.1",
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",
@@ -153,24 +153,24 @@
153
153
  },
154
154
  "homepage": "https://nodemailer.com/",
155
155
  "devDependencies": {
156
- "@aws-sdk/client-sesv2": "3.1151.0",
156
+ "@aws-sdk/client-sesv2": "3.1155.0",
157
157
  "@types/node": "20.19.43",
158
158
  "bunyan": "1.8.15",
159
159
  "c8": "12.0.0",
160
160
  "eslint": "10.13.0",
161
161
  "eslint-config-prettier": "10.1.8",
162
162
  "globals": "17.13.0",
163
- "libbase64": "1.3.2",
164
- "libmime": "5.4.7",
165
- "libqp": "2.1.2",
166
- "mailauth": "7.3.0",
163
+ "libbase64": "1.4.1",
164
+ "libmime": "5.6.3",
165
+ "libqp": "3.0.1",
166
+ "mailauth": "8.0.2",
167
167
  "prettier": "3.9.10",
168
168
  "proxy": "1.0.2",
169
169
  "proxy-test-server": "1.0.0",
170
- "smtp-server": "3.19.21",
170
+ "smtp-server": "3.21.0",
171
171
  "tsx": "4.23.15",
172
172
  "typescript": "6.0.3",
173
- "typescript-eslint": "8.71.2"
173
+ "typescript-eslint": "8.72.0"
174
174
  },
175
175
  "engines": {
176
176
  "node": ">=20.0.0"