nodemailer 10.0.5 → 10.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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [10.0.6](https://github.com/nodemailer/nodemailer/compare/v10.0.5...v10.0.6) (2026-09-11)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **addressparser:** scan free text for an address in linear time ([437d7fc](https://github.com/nodemailer/nodemailer/commit/437d7fc47403df176bc39271641541b7a9bce102))
9
+
3
10
  ## [10.0.5](https://github.com/nodemailer/nodemailer/compare/v10.0.4...v10.0.5) (2026-09-11)
4
11
 
5
12
 
@@ -50,6 +50,111 @@ const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
50
50
  * further '@' that a domain should not have but malformed headers carry anyway.
51
51
  */
52
52
  const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
53
+ /**
54
+ * An addr-spec sitting inside free text, together with the whitespace around it. Sticky
55
+ * on purpose: it is run at the one offset _looseAddressStart picks rather than being let
56
+ * loose to search, see there.
57
+ */
58
+ const LOOSE_TEXT_ADDR = /\s*\b[^@\s]+@[^\s]+\b\s*/y;
59
+ /**
60
+ * The characters JS `\s` matches, which the scan below has to agree with to land on the
61
+ * same match the pattern would.
62
+ */
63
+ function _isSpaceCode(code) {
64
+ return (code === 0x20 ||
65
+ (code >= 0x09 && code <= 0x0d) ||
66
+ code === 0xa0 ||
67
+ code === 0x1680 ||
68
+ (code >= 0x2000 && code <= 0x200a) ||
69
+ code === 0x2028 ||
70
+ code === 0x2029 ||
71
+ code === 0x202f ||
72
+ code === 0x205f ||
73
+ code === 0x3000 ||
74
+ code === 0xfeff);
75
+ }
76
+ /**
77
+ * The characters JS `\w` matches without the unicode flag, the set the `\b` in
78
+ * LOOSE_TEXT_ADDR is read against. charCodeAt off either end of the string gives NaN,
79
+ * which compares false throughout, so out of range reads as the non-word the pattern
80
+ * treats them as.
81
+ */
82
+ function _isWordCode(code) {
83
+ return (code >= 0x30 && code <= 0x39) || (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a) || code === 0x5f;
84
+ }
85
+ /**
86
+ * Whether `\b` holds at an offset
87
+ */
88
+ function _isBoundary(text, at) {
89
+ return _isWordCode(text.charCodeAt(at - 1)) !== _isWordCode(text.charCodeAt(at));
90
+ }
91
+ /**
92
+ * Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
93
+ *
94
+ * Letting the pattern search for itself is quadratic: '[^@\s]+' is retried from every
95
+ * offset and rescans the run to the next '@' each time, so 140KB of header holding no
96
+ * usable '@' blocks the event loop for about ten seconds (GHSA-v53p-9fqp-m79j). The search is also unnecessary.
97
+ * '[^@\s]+' crosses neither whitespace nor a '@', so a match can only begin at the head of
98
+ * a whitespace delimited run or just past a '@' inside one, and '[^\s]+\b' gives characters
99
+ * back until it lands on a boundary, so the only end it can take in that run is the last
100
+ * boundary in it. Both are found in one pass, and the pattern is then run at that single
101
+ * offset.
102
+ *
103
+ * @param text Free text to look in
104
+ * @return Offset to match at, or -1
105
+ */
106
+ function _looseAddressStart(text) {
107
+ const len = text.length;
108
+ let pos = 0;
109
+ while (pos < len) {
110
+ while (pos < len && _isSpaceCode(text.charCodeAt(pos))) {
111
+ pos++;
112
+ }
113
+ if (pos >= len) {
114
+ break;
115
+ }
116
+ const runStart = pos;
117
+ let runEnd = pos;
118
+ while (runEnd < len && !_isSpaceCode(text.charCodeAt(runEnd))) {
119
+ runEnd++;
120
+ }
121
+ let at = text.indexOf('@', runStart);
122
+ if (at >= 0 && at < runEnd) {
123
+ let lastBoundary = -1;
124
+ for (let k = runEnd; k > runStart; k--) {
125
+ if (_isBoundary(text, k)) {
126
+ lastBoundary = k;
127
+ break;
128
+ }
129
+ }
130
+ let atomStart = runStart;
131
+ while (lastBoundary >= 0 && at >= 0 && at < runEnd) {
132
+ // '[^@\s]+' has to cover a character before the '@' and '[^\s]+' one after it,
133
+ // and the boundary that ends the match has to sit past both
134
+ if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
135
+ for (let start = atomStart; start < at; start++) {
136
+ if (_isBoundary(text, start)) {
137
+ if (start > runStart) {
138
+ return start;
139
+ }
140
+ // the leading '\s*' is greedy, so a match that begins at the run
141
+ // takes the whitespace in front of it along
142
+ let padded = runStart;
143
+ while (padded > 0 && _isSpaceCode(text.charCodeAt(padded - 1))) {
144
+ padded--;
145
+ }
146
+ return padded;
147
+ }
148
+ }
149
+ }
150
+ atomStart = at + 1;
151
+ at = text.indexOf('@', atomStart);
152
+ }
153
+ }
154
+ pos = runEnd;
155
+ }
156
+ return -1;
157
+ }
53
158
  /**
54
159
  * Recovers the addr-spec from an angle-addr that came back holding unquoted whitespace.
55
160
  *
@@ -247,16 +352,19 @@ function _handleAddress(tokens, depth) {
247
352
  for (let i = data.text.length - 1; i >= 0; i--) {
248
353
  // Security: Do not extract email addresses from quoted strings
249
354
  if (!data.textWasQuoted[i]) {
250
- data.text[i] = data.text[i]
251
- .replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, (match) => {
252
- if (!extracted) {
253
- data.address = [match.trim()];
355
+ const part = data.text[i];
356
+ let remainder = part;
357
+ const at = _looseAddressStart(part);
358
+ if (at >= 0) {
359
+ LOOSE_TEXT_ADDR.lastIndex = at;
360
+ const match = LOOSE_TEXT_ADDR.exec(part);
361
+ if (match) {
362
+ data.address = [match[0].trim()];
254
363
  extracted = true;
255
- return ' ';
364
+ remainder = part.slice(0, at) + ' ' + part.slice(at + match[0].length);
256
365
  }
257
- return match;
258
- })
259
- .trim();
366
+ }
367
+ data.text[i] = remainder.trim();
260
368
  if (extracted) {
261
369
  break;
262
370
  }
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.0.5";
2
+ export declare const version = "10.0.6";
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.0.5';
6
+ exports.version = '10.0.6';
7
7
  exports.homepage = 'https://nodemailer.com/';
@@ -47,6 +47,111 @@ const ADDR_SPEC = /^[^@\s]+@[^@\s]+$/;
47
47
  * further '@' that a domain should not have but malformed headers carry anyway.
48
48
  */
49
49
  const LOOSE_ADDR_SPEC = /^[^@\s]+@\S+$/;
50
+ /**
51
+ * An addr-spec sitting inside free text, together with the whitespace around it. Sticky
52
+ * on purpose: it is run at the one offset _looseAddressStart picks rather than being let
53
+ * loose to search, see there.
54
+ */
55
+ const LOOSE_TEXT_ADDR = /\s*\b[^@\s]+@[^\s]+\b\s*/y;
56
+ /**
57
+ * The characters JS `\s` matches, which the scan below has to agree with to land on the
58
+ * same match the pattern would.
59
+ */
60
+ function _isSpaceCode(code) {
61
+ return (code === 0x20 ||
62
+ (code >= 0x09 && code <= 0x0d) ||
63
+ code === 0xa0 ||
64
+ code === 0x1680 ||
65
+ (code >= 0x2000 && code <= 0x200a) ||
66
+ code === 0x2028 ||
67
+ code === 0x2029 ||
68
+ code === 0x202f ||
69
+ code === 0x205f ||
70
+ code === 0x3000 ||
71
+ code === 0xfeff);
72
+ }
73
+ /**
74
+ * The characters JS `\w` matches without the unicode flag, the set the `\b` in
75
+ * LOOSE_TEXT_ADDR is read against. charCodeAt off either end of the string gives NaN,
76
+ * which compares false throughout, so out of range reads as the non-word the pattern
77
+ * treats them as.
78
+ */
79
+ function _isWordCode(code) {
80
+ return (code >= 0x30 && code <= 0x39) || (code >= 0x41 && code <= 0x5a) || (code >= 0x61 && code <= 0x7a) || code === 0x5f;
81
+ }
82
+ /**
83
+ * Whether `\b` holds at an offset
84
+ */
85
+ function _isBoundary(text, at) {
86
+ return _isWordCode(text.charCodeAt(at - 1)) !== _isWordCode(text.charCodeAt(at));
87
+ }
88
+ /**
89
+ * Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
90
+ *
91
+ * Letting the pattern search for itself is quadratic: '[^@\s]+' is retried from every
92
+ * offset and rescans the run to the next '@' each time, so 140KB of header holding no
93
+ * usable '@' blocks the event loop for about ten seconds (GHSA-v53p-9fqp-m79j). The search is also unnecessary.
94
+ * '[^@\s]+' crosses neither whitespace nor a '@', so a match can only begin at the head of
95
+ * a whitespace delimited run or just past a '@' inside one, and '[^\s]+\b' gives characters
96
+ * back until it lands on a boundary, so the only end it can take in that run is the last
97
+ * boundary in it. Both are found in one pass, and the pattern is then run at that single
98
+ * offset.
99
+ *
100
+ * @param text Free text to look in
101
+ * @return Offset to match at, or -1
102
+ */
103
+ function _looseAddressStart(text) {
104
+ const len = text.length;
105
+ let pos = 0;
106
+ while (pos < len) {
107
+ while (pos < len && _isSpaceCode(text.charCodeAt(pos))) {
108
+ pos++;
109
+ }
110
+ if (pos >= len) {
111
+ break;
112
+ }
113
+ const runStart = pos;
114
+ let runEnd = pos;
115
+ while (runEnd < len && !_isSpaceCode(text.charCodeAt(runEnd))) {
116
+ runEnd++;
117
+ }
118
+ let at = text.indexOf('@', runStart);
119
+ if (at >= 0 && at < runEnd) {
120
+ let lastBoundary = -1;
121
+ for (let k = runEnd; k > runStart; k--) {
122
+ if (_isBoundary(text, k)) {
123
+ lastBoundary = k;
124
+ break;
125
+ }
126
+ }
127
+ let atomStart = runStart;
128
+ while (lastBoundary >= 0 && at >= 0 && at < runEnd) {
129
+ // '[^@\s]+' has to cover a character before the '@' and '[^\s]+' one after it,
130
+ // and the boundary that ends the match has to sit past both
131
+ if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
132
+ for (let start = atomStart; start < at; start++) {
133
+ if (_isBoundary(text, start)) {
134
+ if (start > runStart) {
135
+ return start;
136
+ }
137
+ // the leading '\s*' is greedy, so a match that begins at the run
138
+ // takes the whitespace in front of it along
139
+ let padded = runStart;
140
+ while (padded > 0 && _isSpaceCode(text.charCodeAt(padded - 1))) {
141
+ padded--;
142
+ }
143
+ return padded;
144
+ }
145
+ }
146
+ }
147
+ atomStart = at + 1;
148
+ at = text.indexOf('@', atomStart);
149
+ }
150
+ }
151
+ pos = runEnd;
152
+ }
153
+ return -1;
154
+ }
50
155
  /**
51
156
  * Recovers the addr-spec from an angle-addr that came back holding unquoted whitespace.
52
157
  *
@@ -244,16 +349,19 @@ function _handleAddress(tokens, depth) {
244
349
  for (let i = data.text.length - 1; i >= 0; i--) {
245
350
  // Security: Do not extract email addresses from quoted strings
246
351
  if (!data.textWasQuoted[i]) {
247
- data.text[i] = data.text[i]
248
- .replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, (match) => {
249
- if (!extracted) {
250
- data.address = [match.trim()];
352
+ const part = data.text[i];
353
+ let remainder = part;
354
+ const at = _looseAddressStart(part);
355
+ if (at >= 0) {
356
+ LOOSE_TEXT_ADDR.lastIndex = at;
357
+ const match = LOOSE_TEXT_ADDR.exec(part);
358
+ if (match) {
359
+ data.address = [match[0].trim()];
251
360
  extracted = true;
252
- return ' ';
361
+ remainder = part.slice(0, at) + ' ' + part.slice(at + match[0].length);
253
362
  }
254
- return match;
255
- })
256
- .trim();
363
+ }
364
+ data.text[i] = remainder.trim();
257
365
  if (extracted) {
258
366
  break;
259
367
  }
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.0.5";
2
+ export declare const version = "10.0.6";
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.0.5';
3
+ export const version = '10.0.6';
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.0.5",
3
+ "version": "10.0.6",
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",