nodemailer 10.1.2 → 10.1.3

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.1.3](https://github.com/nodemailer/nodemailer/compare/v10.1.2...v10.1.3) (2026-10-18)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * format References and strip URLs in linear time, release any number of streams ([2f18641](https://github.com/nodemailer/nodemailer/commit/2f186418cf6c8fff18f235d4318892fd42f810fa))
9
+
3
10
  ## [10.1.2](https://github.com/nodemailer/nodemailer/compare/v10.1.1...v10.1.2) (2026-10-16)
4
11
 
5
12
 
@@ -183,7 +183,10 @@ class MailMessage {
183
183
  const values = [data.html, data.text, data.watchHtml, data.amp, data.raw, data.icalEvent];
184
184
  for (const key of ['attachments', 'alternatives']) {
185
185
  if (Array.isArray(data[key])) {
186
- values.push(...data[key]);
186
+ // one at a time, spreading a long enough list into the arguments throws
187
+ for (const entry of data[key]) {
188
+ values.push(entry);
189
+ }
187
190
  }
188
191
  }
189
192
  for (const value of values) {
@@ -64,6 +64,31 @@ const QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"$/;
64
64
  // else goes into angle brackets so that the header can not be read as more addresses than
65
65
  // the envelope carries
66
66
  const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
67
+ /**
68
+ * Removes the whitespace inside every <...> of a References value. A "<" with no ">" after
69
+ * it is left as it is. An index scan, the regex /<[^>]*>/g rescans the rest of the value
70
+ * from every such "<"
71
+ *
72
+ * @param value References header value
73
+ * @returns The value with the msg-ids closed up
74
+ */
75
+ function stripWhitespaceInAngleBrackets(value) {
76
+ let result = '';
77
+ let pos = 0;
78
+ for (;;) {
79
+ const open = value.indexOf('<', pos);
80
+ if (open < 0) {
81
+ break;
82
+ }
83
+ const close = value.indexOf('>', open + 1);
84
+ if (close < 0) {
85
+ break;
86
+ }
87
+ result += value.slice(pos, open) + value.slice(open, close + 1).replace(/\s/g, '');
88
+ pos = close + 1;
89
+ }
90
+ return result + value.slice(pos);
91
+ }
67
92
  // domainToASCII and domainToUnicode are WHATWG host parsers rather than plain IDNA
68
93
  // mappers, so they do more than map: they cut the host at '/', '\\', '?' and '#', drop C0
69
94
  // controls, and percent-decode. Handing them 'evil.example/mail.corp.example' returns the
@@ -1357,26 +1382,26 @@ class MimeNode {
1357
1382
  }
1358
1383
  return value;
1359
1384
  // space separated list of values enclosed in <>
1360
- case 'References':
1361
- value = [].concat
1362
- .apply([], [].concat(value || '').map(elm => {
1363
- elm = (elm || '')
1385
+ case 'References': {
1386
+ const ids = [];
1387
+ for (const entry of [].concat(value || '')) {
1388
+ const elm = (entry || '')
1364
1389
  .toString()
1365
1390
  .replace(/\r?\n|\r/g, ' ')
1366
1391
  .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
1367
1392
  .trim();
1368
- return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
1369
- }))
1370
- .map(elm => {
1371
- if (elm.charAt(0) !== '<') {
1372
- elm = '<' + elm;
1373
- }
1374
- if (elm.charAt(elm.length - 1) !== '>') {
1375
- elm = elm + '>';
1393
+ for (let id of stripWhitespaceInAngleBrackets(elm).split(/\s+/)) {
1394
+ if (id.charAt(0) !== '<') {
1395
+ id = '<' + id;
1396
+ }
1397
+ if (id.charAt(id.length - 1) !== '>') {
1398
+ id = id + '>';
1399
+ }
1400
+ ids.push(id);
1376
1401
  }
1377
- return elm;
1378
- });
1379
- return value.join(' ').trim();
1402
+ }
1403
+ return ids.join(' ').trim();
1404
+ }
1380
1405
  case 'Date':
1381
1406
  if (Object.prototype.toString.call(value) === '[object Date]') {
1382
1407
  return value.toUTCString().replace(/GMT/, '+0000');
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.1.2";
2
+ export declare const version = "10.1.3";
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.1.2';
6
+ exports.version = '10.1.3';
7
7
  exports.homepage = 'https://nodemailer.com/';
@@ -58,10 +58,27 @@ const punycode = __importStar(require("../punycode/index.js"));
58
58
  // Matches a "scheme:" not followed by "//" (and with something after it), used
59
59
  // to re-insert the authority separator the legacy parser did not require.
60
60
  const SLASHLESS_AUTHORITY = /^([a-zA-Z][a-zA-Z0-9+.-]*:)(?!\/\/)([\s\S]+)$/;
61
- // Leading and trailing C0 controls and spaces, which the WHATWG parser strips before it
62
- // looks at the input. Stripped up front so that the slash-less form is recognized in a
63
- // value read from a file with a trailing newline as well.
64
- const SURROUNDING_WHITESPACE = /^[\x00-\x20]+|[\x00-\x20]+$/g;
61
+ /**
62
+ * Drops the leading and trailing C0 controls and spaces, which the WHATWG parser strips
63
+ * before it looks at the input. Stripped up front so that the slash-less form is recognized
64
+ * in a value read from a file with a trailing newline as well. An index scan, the regex
65
+ * /^[\x00-\x20]+|[\x00-\x20]+$/g retried a run of spaces inside the value from every
66
+ * position in it
67
+ *
68
+ * @param input URL string
69
+ * @returns The string without the surrounding controls and spaces
70
+ */
71
+ function stripSurroundingWhitespace(input) {
72
+ let start = 0;
73
+ let end = input.length;
74
+ while (start < end && input.charCodeAt(start) <= 0x20) {
75
+ start++;
76
+ }
77
+ while (end > start && input.charCodeAt(end - 1) <= 0x20) {
78
+ end--;
79
+ }
80
+ return input.slice(start, end);
81
+ }
65
82
  // Leading characters legacy url.parse() skips before it reads the scheme
66
83
  const LEGACY_TRIM = /^[\x00-\x20\u00a0\ufeff]+/;
67
84
  // The authority of a "scheme://authority/..." or a scheme-relative "//authority/..."
@@ -156,7 +173,7 @@ function normalizeHostname(raw, href) {
156
173
  return mapped;
157
174
  }
158
175
  const parse = (input, parseQueryString) => {
159
- input = (input || '').replace(SURROUNDING_WHITESPACE, '');
176
+ input = stripSurroundingWhitespace(input || '');
160
177
  // Legacy url.parse() parses a "user:pass@host:port" authority that follows
161
178
  // the scheme even without the "//" separator, for schemes outside its
162
179
  // built-in slashed-protocol list (smtp:/smtps:/socks:/...). The WHATWG
@@ -145,7 +145,10 @@ export default class MailMessage {
145
145
  const values = [data.html, data.text, data.watchHtml, data.amp, data.raw, data.icalEvent];
146
146
  for (const key of ['attachments', 'alternatives']) {
147
147
  if (Array.isArray(data[key])) {
148
- values.push(...data[key]);
148
+ // one at a time, spreading a long enough list into the arguments throws
149
+ for (const entry of data[key]) {
150
+ values.push(entry);
151
+ }
149
152
  }
150
153
  }
151
154
  for (const value of values) {
@@ -26,6 +26,31 @@ const QUOTED_STRING = /^"(?:[^"\\]|\\[\s\S])*"$/;
26
26
  // else goes into angle brackets so that the header can not be read as more addresses than
27
27
  // the envelope carries
28
28
  const PLAIN_ADDRESS = /^[^\s"(),:;<>@[\\\]]+@[^\s"(),:;<>@[\\\]]+$/;
29
+ /**
30
+ * Removes the whitespace inside every <...> of a References value. A "<" with no ">" after
31
+ * it is left as it is. An index scan, the regex /<[^>]*>/g rescans the rest of the value
32
+ * from every such "<"
33
+ *
34
+ * @param value References header value
35
+ * @returns The value with the msg-ids closed up
36
+ */
37
+ function stripWhitespaceInAngleBrackets(value) {
38
+ let result = '';
39
+ let pos = 0;
40
+ for (;;) {
41
+ const open = value.indexOf('<', pos);
42
+ if (open < 0) {
43
+ break;
44
+ }
45
+ const close = value.indexOf('>', open + 1);
46
+ if (close < 0) {
47
+ break;
48
+ }
49
+ result += value.slice(pos, open) + value.slice(open, close + 1).replace(/\s/g, '');
50
+ pos = close + 1;
51
+ }
52
+ return result + value.slice(pos);
53
+ }
29
54
  // domainToASCII and domainToUnicode are WHATWG host parsers rather than plain IDNA
30
55
  // mappers, so they do more than map: they cut the host at '/', '\\', '?' and '#', drop C0
31
56
  // controls, and percent-decode. Handing them 'evil.example/mail.corp.example' returns the
@@ -1319,26 +1344,26 @@ class MimeNode {
1319
1344
  }
1320
1345
  return value;
1321
1346
  // space separated list of values enclosed in <>
1322
- case 'References':
1323
- value = [].concat
1324
- .apply([], [].concat(value || '').map(elm => {
1325
- elm = (elm || '')
1347
+ case 'References': {
1348
+ const ids = [];
1349
+ for (const entry of [].concat(value || '')) {
1350
+ const elm = (entry || '')
1326
1351
  .toString()
1327
1352
  .replace(/\r?\n|\r/g, ' ')
1328
1353
  .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
1329
1354
  .trim();
1330
- return elm.replace(/<[^>]*>/g, str => str.replace(/\s/g, '')).split(/\s+/);
1331
- }))
1332
- .map(elm => {
1333
- if (elm.charAt(0) !== '<') {
1334
- elm = '<' + elm;
1335
- }
1336
- if (elm.charAt(elm.length - 1) !== '>') {
1337
- elm = elm + '>';
1355
+ for (let id of stripWhitespaceInAngleBrackets(elm).split(/\s+/)) {
1356
+ if (id.charAt(0) !== '<') {
1357
+ id = '<' + id;
1358
+ }
1359
+ if (id.charAt(id.length - 1) !== '>') {
1360
+ id = id + '>';
1361
+ }
1362
+ ids.push(id);
1338
1363
  }
1339
- return elm;
1340
- });
1341
- return value.join(' ').trim();
1364
+ }
1365
+ return ids.join(' ').trim();
1366
+ }
1342
1367
  case 'Date':
1343
1368
  if (Object.prototype.toString.call(value) === '[object Date]') {
1344
1369
  return value.toUTCString().replace(/GMT/, '+0000');
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.1.2";
2
+ export declare const version = "10.1.3";
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.1.2';
3
+ export const version = '10.1.3';
4
4
  export const homepage = 'https://nodemailer.com/';
@@ -19,10 +19,27 @@ import * as punycode from '../punycode/index.js';
19
19
  // Matches a "scheme:" not followed by "//" (and with something after it), used
20
20
  // to re-insert the authority separator the legacy parser did not require.
21
21
  const SLASHLESS_AUTHORITY = /^([a-zA-Z][a-zA-Z0-9+.-]*:)(?!\/\/)([\s\S]+)$/;
22
- // Leading and trailing C0 controls and spaces, which the WHATWG parser strips before it
23
- // looks at the input. Stripped up front so that the slash-less form is recognized in a
24
- // value read from a file with a trailing newline as well.
25
- const SURROUNDING_WHITESPACE = /^[\x00-\x20]+|[\x00-\x20]+$/g;
22
+ /**
23
+ * Drops the leading and trailing C0 controls and spaces, which the WHATWG parser strips
24
+ * before it looks at the input. Stripped up front so that the slash-less form is recognized
25
+ * in a value read from a file with a trailing newline as well. An index scan, the regex
26
+ * /^[\x00-\x20]+|[\x00-\x20]+$/g retried a run of spaces inside the value from every
27
+ * position in it
28
+ *
29
+ * @param input URL string
30
+ * @returns The string without the surrounding controls and spaces
31
+ */
32
+ function stripSurroundingWhitespace(input) {
33
+ let start = 0;
34
+ let end = input.length;
35
+ while (start < end && input.charCodeAt(start) <= 0x20) {
36
+ start++;
37
+ }
38
+ while (end > start && input.charCodeAt(end - 1) <= 0x20) {
39
+ end--;
40
+ }
41
+ return input.slice(start, end);
42
+ }
26
43
  // Leading characters legacy url.parse() skips before it reads the scheme
27
44
  const LEGACY_TRIM = /^[\x00-\x20\u00a0\ufeff]+/;
28
45
  // The authority of a "scheme://authority/..." or a scheme-relative "//authority/..."
@@ -117,7 +134,7 @@ function normalizeHostname(raw, href) {
117
134
  return mapped;
118
135
  }
119
136
  export const parse = (input, parseQueryString) => {
120
- input = (input || '').replace(SURROUNDING_WHITESPACE, '');
137
+ input = stripSurroundingWhitespace(input || '');
121
138
  // Legacy url.parse() parses a "user:pass@host:port" authority that follows
122
139
  // the scheme even without the "//" separator, for schemes outside its
123
140
  // built-in slashed-protocol list (smtp:/smtps:/socks:/...). The WHATWG
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "10.1.2",
3
+ "version": "10.1.3",
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",