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 +7 -0
- package/dist/cjs/addressparser/index.js +116 -8
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/esm/addressparser/index.js +116 -8
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/package.json +1 -1
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
364
|
+
remainder = part.slice(0, at) + ' ' + part.slice(at + match[0].length);
|
|
256
365
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
.trim();
|
|
366
|
+
}
|
|
367
|
+
data.text[i] = remainder.trim();
|
|
260
368
|
if (extracted) {
|
|
261
369
|
break;
|
|
262
370
|
}
|
package/dist/cjs/package-info.js
CHANGED
|
@@ -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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
361
|
+
remainder = part.slice(0, at) + ' ' + part.slice(at + match[0].length);
|
|
253
362
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
.trim();
|
|
363
|
+
}
|
|
364
|
+
data.text[i] = remainder.trim();
|
|
257
365
|
if (extracted) {
|
|
258
366
|
break;
|
|
259
367
|
}
|
package/dist/esm/package-info.js
CHANGED