nodemailer 10.0.7 → 10.0.9
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 +16 -0
- package/dist/cjs/addressparser/index.js +53 -8
- package/dist/cjs/mime-node/index.js +30 -20
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/esm/addressparser/index.js +53 -8
- package/dist/esm/mime-node/index.js +30 -20
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [10.0.9](https://github.com/nodemailer/nodemailer/compare/v10.0.8...v10.0.9) (2026-09-12)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **addressparser:** bound the '@' probe to the run being scanned ([1465c3f](https://github.com/nodemailer/nodemailer/commit/1465c3f5ff74a7c4fbbe9853bd01448bb92bf5b7))
|
|
9
|
+
* **addressparser:** keep the text after a comment out of a quoted local part address ([2f36eb1](https://github.com/nodemailer/nodemailer/commit/2f36eb1aa1dd33e312411dc9b888548e14db54ee))
|
|
10
|
+
|
|
11
|
+
## [10.0.8](https://github.com/nodemailer/nodemailer/compare/v10.0.7...v10.0.8) (2026-09-11)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
|
|
16
|
+
* **mime-node:** clean the boundary where it is written, not only where it is built ([e14278d](https://github.com/nodemailer/nodemailer/commit/e14278d2dae9427280c79450605a27b1c5d2c355))
|
|
17
|
+
* **mime-node:** drop every control character from multipart boundary material ([a82a355](https://github.com/nodemailer/nodemailer/commit/a82a35554848a2e07485ff81f80aefe2736e5a04))
|
|
18
|
+
|
|
3
19
|
## [10.0.7](https://github.com/nodemailer/nodemailer/compare/v10.0.6...v10.0.7) (2026-09-11)
|
|
4
20
|
|
|
5
21
|
|
|
@@ -88,6 +88,29 @@ function _isWordCode(code) {
|
|
|
88
88
|
function _isBoundary(text, at) {
|
|
89
89
|
return _isWordCode(text.charCodeAt(at - 1)) !== _isWordCode(text.charCodeAt(at));
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Offset of the first '@' in `text` between `from` and `to`, or -1 when the range holds none.
|
|
93
|
+
*
|
|
94
|
+
* indexOf would scan on to the end of the value, and the value here is a whole header. The
|
|
95
|
+
* walk below steps one whitespace delimited run at a time and only ever uses a '@' that sits
|
|
96
|
+
* inside the run it is on, so an unbounded probe rescans everything behind that run once per
|
|
97
|
+
* run and grows with the square of the header: 400KB of free text carrying no '@' took a
|
|
98
|
+
* quarter of a second. GHSA-v53p-9fqp-m79j took the pattern search out of this walk and left
|
|
99
|
+
* the probe unbounded behind it.
|
|
100
|
+
*
|
|
101
|
+
* @param text Text to look in
|
|
102
|
+
* @param from Offset to start at
|
|
103
|
+
* @param to Offset to stop before
|
|
104
|
+
* @return Offset of the '@', or -1
|
|
105
|
+
*/
|
|
106
|
+
function _indexOfAt(text, from, to) {
|
|
107
|
+
for (let i = from; i < to; i++) {
|
|
108
|
+
if (text.charCodeAt(i) === 0x40) {
|
|
109
|
+
return i;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return -1;
|
|
113
|
+
}
|
|
91
114
|
/**
|
|
92
115
|
* Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
|
|
93
116
|
*
|
|
@@ -118,8 +141,8 @@ function _looseAddressStart(text) {
|
|
|
118
141
|
while (runEnd < len && !_isSpaceCode(text.charCodeAt(runEnd))) {
|
|
119
142
|
runEnd++;
|
|
120
143
|
}
|
|
121
|
-
let at = text
|
|
122
|
-
if (at >= 0
|
|
144
|
+
let at = _indexOfAt(text, runStart, runEnd);
|
|
145
|
+
if (at >= 0) {
|
|
123
146
|
let lastBoundary = -1;
|
|
124
147
|
for (let k = runEnd; k > runStart; k--) {
|
|
125
148
|
if (_isBoundary(text, k)) {
|
|
@@ -128,7 +151,7 @@ function _looseAddressStart(text) {
|
|
|
128
151
|
}
|
|
129
152
|
}
|
|
130
153
|
let atomStart = runStart;
|
|
131
|
-
while (lastBoundary >= 0 && at >= 0
|
|
154
|
+
while (lastBoundary >= 0 && at >= 0) {
|
|
132
155
|
// '[^@\s]+' has to cover a character before the '@' and '[^\s]+' one after it,
|
|
133
156
|
// and the boundary that ends the match has to sit past both
|
|
134
157
|
if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
|
|
@@ -148,7 +171,7 @@ function _looseAddressStart(text) {
|
|
|
148
171
|
}
|
|
149
172
|
}
|
|
150
173
|
atomStart = at + 1;
|
|
151
|
-
at = text
|
|
174
|
+
at = _indexOfAt(text, atomStart, runEnd);
|
|
152
175
|
}
|
|
153
176
|
}
|
|
154
177
|
pos = runEnd;
|
|
@@ -275,6 +298,19 @@ function _handleAddress(tokens, depth) {
|
|
|
275
298
|
}
|
|
276
299
|
}
|
|
277
300
|
else if (token.value) {
|
|
301
|
+
// An empty quoted string is dropped by the tokenizer, leaving no text token of its
|
|
302
|
+
// own for textWasQuoted to be recorded on, so the pair of quote operators right in
|
|
303
|
+
// front of this token is all that is left of it and the run it opens carries the
|
|
304
|
+
// quoting instead. Without this '""@example.com' reads as the bare '@example.com',
|
|
305
|
+
// the quotes never go back on, and the value is no longer an addr-spec a trailing
|
|
306
|
+
// comment can be peeled off of. It only ever opens a run: a run that already holds
|
|
307
|
+
// material collected outside the quotes is not a quoted string, whatever follows it
|
|
308
|
+
const prevPrevToken = i > 1 ? tokens[i - 2] : null;
|
|
309
|
+
const opensAfterEmptyQuotedString = prevToken?.type === 'operator' &&
|
|
310
|
+
prevToken.value === '"' &&
|
|
311
|
+
!!prevToken.noBreak &&
|
|
312
|
+
prevPrevToken?.type === 'operator' &&
|
|
313
|
+
prevPrevToken.value === '"';
|
|
278
314
|
if (state === 'address') {
|
|
279
315
|
// Handle unquoted name that includes a "<".
|
|
280
316
|
// Apple Mail truncates everything between an unexpected < and an address.
|
|
@@ -302,7 +338,7 @@ function _handleAddress(tokens, depth) {
|
|
|
302
338
|
data[state].push(token.value);
|
|
303
339
|
lastChars[state] = token.value.charAt(token.value.length - 1);
|
|
304
340
|
if (state === 'text') {
|
|
305
|
-
data.textWasQuoted.push(insideQuotes);
|
|
341
|
+
data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
|
306
342
|
}
|
|
307
343
|
}
|
|
308
344
|
}
|
|
@@ -387,6 +423,18 @@ function _handleAddress(tokens, depth) {
|
|
|
387
423
|
// Join values with spaces
|
|
388
424
|
data.text = data.text.join(' ');
|
|
389
425
|
data.address = data.address.join(' ');
|
|
426
|
+
if (addressFromQuotedText && data.text) {
|
|
427
|
+
// The mailbox is still sitting in the text, so it moves over here and is quoted
|
|
428
|
+
// before the recovery below rather than after it. Anything else the text holds
|
|
429
|
+
// came along with it: a comment ends the domain but leaves the atoms behind it in
|
|
430
|
+
// the same text, and '"user"@example.com(x)evil.com' was handed on as the address
|
|
431
|
+
// 'user@example.com evil.com', a second domain riding into the envelope recipient
|
|
432
|
+
// on a value that is no addr-spec at all (GHSA-g57g-f23g-4646). Putting the quotes
|
|
433
|
+
// back first is what lets the recovery tell the whitespace an addr-spec may carry
|
|
434
|
+
// from the wreckage trailing one, as only a quoted local part may hold whitespace
|
|
435
|
+
data.address = _quoteLocalPart(data.text);
|
|
436
|
+
data.text = '';
|
|
437
|
+
}
|
|
390
438
|
_recoverAddrSpec(data);
|
|
391
439
|
const address = {
|
|
392
440
|
address: data.address || data.text || '',
|
|
@@ -400,9 +448,6 @@ function _handleAddress(tokens, depth) {
|
|
|
400
448
|
address.address = '';
|
|
401
449
|
}
|
|
402
450
|
}
|
|
403
|
-
if (addressFromQuotedText && address.address) {
|
|
404
|
-
address.address = _quoteLocalPart(address.address);
|
|
405
|
-
}
|
|
406
451
|
addresses.push(address);
|
|
407
452
|
}
|
|
408
453
|
return addresses;
|
|
@@ -100,13 +100,21 @@ function normalizeDomain(domain, toUnicode) {
|
|
|
100
100
|
return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
|
|
101
101
|
}
|
|
102
102
|
/**
|
|
103
|
-
* Removes the
|
|
103
|
+
* Removes the characters that must never reach a multipart delimiter line.
|
|
104
|
+
*
|
|
105
|
+
* A line break splits the delimiter, so the boundary declared in the header can never
|
|
106
|
+
* match it again and the parts go out as body lines instead. The other C0 controls and
|
|
107
|
+
* DEL do not split anything but must not be written either: RFC 5321 does not allow a
|
|
108
|
+
* NUL in DATA at all, and an MTA or a scanner that stops at one reads a different
|
|
109
|
+
* message than a client that does not, which is the same parser disagreement a split
|
|
110
|
+
* delimiter creates. Both sides are cleaned together, since the declared value and the
|
|
111
|
+
* delimiters come from this one result.
|
|
104
112
|
*
|
|
105
113
|
* @param value Value to clean
|
|
106
|
-
* @return Value with every
|
|
114
|
+
* @return Value with every control character removed
|
|
107
115
|
*/
|
|
108
|
-
function
|
|
109
|
-
return value.replace(/[\
|
|
116
|
+
function _stripBoundaryControls(value) {
|
|
117
|
+
return value.replace(/[\x00-\x1f\x7f]+/g, '');
|
|
110
118
|
}
|
|
111
119
|
/**
|
|
112
120
|
* Creates a new mime tree node. Assumes 'multipart/*' as the content type
|
|
@@ -128,12 +136,11 @@ class MimeNode {
|
|
|
128
136
|
this.nodeCounter = 0;
|
|
129
137
|
options = options || {};
|
|
130
138
|
/**
|
|
131
|
-
* shared part of the unique multipart boundary.
|
|
132
|
-
*
|
|
133
|
-
* never match them again and the remainder would go out as body lines
|
|
139
|
+
* shared part of the unique multipart boundary. Control characters are dropped
|
|
140
|
+
* here rather than at the delimiter, see _stripBoundaryControls
|
|
134
141
|
*/
|
|
135
|
-
this.baseBoundary =
|
|
136
|
-
this.boundaryPrefix =
|
|
142
|
+
this.baseBoundary = _stripBoundaryControls(options.baseBoundary || node_crypto_1.default.randomBytes(8).toString('hex'));
|
|
143
|
+
this.boundaryPrefix = _stripBoundaryControls(options.boundaryPrefix || '--_NmP');
|
|
137
144
|
this.disableFileAccess = !!options.disableFileAccess;
|
|
138
145
|
this.disableUrlAccess = !!options.disableUrlAccess;
|
|
139
146
|
this.normalizeHeaderKey = options.normalizeHeaderKey;
|
|
@@ -1180,19 +1187,20 @@ class MimeNode {
|
|
|
1180
1187
|
this.contentType = structured.value.trim().toLowerCase();
|
|
1181
1188
|
this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
|
|
1182
1189
|
if (this.multipart) {
|
|
1183
|
-
//
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1186
|
-
// delimiters are assigned from the same expression here, so whatever the
|
|
1187
|
-
// header emitter then does with it, the two sides cannot disagree.
|
|
1190
|
+
// The declared value and the delimiters are assigned from the same expression
|
|
1191
|
+
// here, so whatever the header emitter then does with it, the two sides cannot
|
|
1192
|
+
// disagree about which characters the boundary is made of.
|
|
1188
1193
|
//
|
|
1189
1194
|
// Stripping runs before the fallback rather than over the whole chain: a
|
|
1190
|
-
// boundary
|
|
1195
|
+
// boundary made only of control characters would otherwise strip to '' and
|
|
1191
1196
|
// leave the node declaring no boundary and streaming bare '--' delimiters.
|
|
1192
|
-
//
|
|
1193
|
-
//
|
|
1194
|
-
|
|
1195
|
-
|
|
1197
|
+
// _generateBoundary cleans what it builds, but this is the one place the boundary
|
|
1198
|
+
// is written, so it cleans the value it is about to write rather than trusting
|
|
1199
|
+
// where it came from. MimeNode is exported and subclassable, so the generator is
|
|
1200
|
+
// not necessarily the one below. Stripping twice costs nothing, it is idempotent.
|
|
1201
|
+
const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
|
|
1202
|
+
this.boundary = structured.params.boundary =
|
|
1203
|
+
declared || _stripBoundaryControls(this._generateBoundary());
|
|
1196
1204
|
}
|
|
1197
1205
|
else {
|
|
1198
1206
|
this.boundary = false;
|
|
@@ -1205,7 +1213,9 @@ class MimeNode {
|
|
|
1205
1213
|
* @internal
|
|
1206
1214
|
*/
|
|
1207
1215
|
_generateBoundary() {
|
|
1208
|
-
|
|
1216
|
+
// baseBoundary and boundaryPrefix are public, so they may hold something the
|
|
1217
|
+
// constructor never cleaned, and the -Part_ suffix keeps the result non empty
|
|
1218
|
+
return _stripBoundaryControls(this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary) + '-Part_' + this._nodeId;
|
|
1209
1219
|
}
|
|
1210
1220
|
/**
|
|
1211
1221
|
* Encodes a header value for use in the generated rfc2822 email.
|
package/dist/cjs/package-info.js
CHANGED
|
@@ -85,6 +85,29 @@ function _isWordCode(code) {
|
|
|
85
85
|
function _isBoundary(text, at) {
|
|
86
86
|
return _isWordCode(text.charCodeAt(at - 1)) !== _isWordCode(text.charCodeAt(at));
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Offset of the first '@' in `text` between `from` and `to`, or -1 when the range holds none.
|
|
90
|
+
*
|
|
91
|
+
* indexOf would scan on to the end of the value, and the value here is a whole header. The
|
|
92
|
+
* walk below steps one whitespace delimited run at a time and only ever uses a '@' that sits
|
|
93
|
+
* inside the run it is on, so an unbounded probe rescans everything behind that run once per
|
|
94
|
+
* run and grows with the square of the header: 400KB of free text carrying no '@' took a
|
|
95
|
+
* quarter of a second. GHSA-v53p-9fqp-m79j took the pattern search out of this walk and left
|
|
96
|
+
* the probe unbounded behind it.
|
|
97
|
+
*
|
|
98
|
+
* @param text Text to look in
|
|
99
|
+
* @param from Offset to start at
|
|
100
|
+
* @param to Offset to stop before
|
|
101
|
+
* @return Offset of the '@', or -1
|
|
102
|
+
*/
|
|
103
|
+
function _indexOfAt(text, from, to) {
|
|
104
|
+
for (let i = from; i < to; i++) {
|
|
105
|
+
if (text.charCodeAt(i) === 0x40) {
|
|
106
|
+
return i;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return -1;
|
|
110
|
+
}
|
|
88
111
|
/**
|
|
89
112
|
* Finds the offset LOOSE_TEXT_ADDR matches at, or -1 when it does not match at all.
|
|
90
113
|
*
|
|
@@ -115,8 +138,8 @@ function _looseAddressStart(text) {
|
|
|
115
138
|
while (runEnd < len && !_isSpaceCode(text.charCodeAt(runEnd))) {
|
|
116
139
|
runEnd++;
|
|
117
140
|
}
|
|
118
|
-
let at = text
|
|
119
|
-
if (at >= 0
|
|
141
|
+
let at = _indexOfAt(text, runStart, runEnd);
|
|
142
|
+
if (at >= 0) {
|
|
120
143
|
let lastBoundary = -1;
|
|
121
144
|
for (let k = runEnd; k > runStart; k--) {
|
|
122
145
|
if (_isBoundary(text, k)) {
|
|
@@ -125,7 +148,7 @@ function _looseAddressStart(text) {
|
|
|
125
148
|
}
|
|
126
149
|
}
|
|
127
150
|
let atomStart = runStart;
|
|
128
|
-
while (lastBoundary >= 0 && at >= 0
|
|
151
|
+
while (lastBoundary >= 0 && at >= 0) {
|
|
129
152
|
// '[^@\s]+' has to cover a character before the '@' and '[^\s]+' one after it,
|
|
130
153
|
// and the boundary that ends the match has to sit past both
|
|
131
154
|
if (at > atomStart && runEnd > at + 1 && lastBoundary > at + 1) {
|
|
@@ -145,7 +168,7 @@ function _looseAddressStart(text) {
|
|
|
145
168
|
}
|
|
146
169
|
}
|
|
147
170
|
atomStart = at + 1;
|
|
148
|
-
at = text
|
|
171
|
+
at = _indexOfAt(text, atomStart, runEnd);
|
|
149
172
|
}
|
|
150
173
|
}
|
|
151
174
|
pos = runEnd;
|
|
@@ -272,6 +295,19 @@ function _handleAddress(tokens, depth) {
|
|
|
272
295
|
}
|
|
273
296
|
}
|
|
274
297
|
else if (token.value) {
|
|
298
|
+
// An empty quoted string is dropped by the tokenizer, leaving no text token of its
|
|
299
|
+
// own for textWasQuoted to be recorded on, so the pair of quote operators right in
|
|
300
|
+
// front of this token is all that is left of it and the run it opens carries the
|
|
301
|
+
// quoting instead. Without this '""@example.com' reads as the bare '@example.com',
|
|
302
|
+
// the quotes never go back on, and the value is no longer an addr-spec a trailing
|
|
303
|
+
// comment can be peeled off of. It only ever opens a run: a run that already holds
|
|
304
|
+
// material collected outside the quotes is not a quoted string, whatever follows it
|
|
305
|
+
const prevPrevToken = i > 1 ? tokens[i - 2] : null;
|
|
306
|
+
const opensAfterEmptyQuotedString = prevToken?.type === 'operator' &&
|
|
307
|
+
prevToken.value === '"' &&
|
|
308
|
+
!!prevToken.noBreak &&
|
|
309
|
+
prevPrevToken?.type === 'operator' &&
|
|
310
|
+
prevPrevToken.value === '"';
|
|
275
311
|
if (state === 'address') {
|
|
276
312
|
// Handle unquoted name that includes a "<".
|
|
277
313
|
// Apple Mail truncates everything between an unexpected < and an address.
|
|
@@ -299,7 +335,7 @@ function _handleAddress(tokens, depth) {
|
|
|
299
335
|
data[state].push(token.value);
|
|
300
336
|
lastChars[state] = token.value.charAt(token.value.length - 1);
|
|
301
337
|
if (state === 'text') {
|
|
302
|
-
data.textWasQuoted.push(insideQuotes);
|
|
338
|
+
data.textWasQuoted.push(insideQuotes || opensAfterEmptyQuotedString);
|
|
303
339
|
}
|
|
304
340
|
}
|
|
305
341
|
}
|
|
@@ -384,6 +420,18 @@ function _handleAddress(tokens, depth) {
|
|
|
384
420
|
// Join values with spaces
|
|
385
421
|
data.text = data.text.join(' ');
|
|
386
422
|
data.address = data.address.join(' ');
|
|
423
|
+
if (addressFromQuotedText && data.text) {
|
|
424
|
+
// The mailbox is still sitting in the text, so it moves over here and is quoted
|
|
425
|
+
// before the recovery below rather than after it. Anything else the text holds
|
|
426
|
+
// came along with it: a comment ends the domain but leaves the atoms behind it in
|
|
427
|
+
// the same text, and '"user"@example.com(x)evil.com' was handed on as the address
|
|
428
|
+
// 'user@example.com evil.com', a second domain riding into the envelope recipient
|
|
429
|
+
// on a value that is no addr-spec at all (GHSA-g57g-f23g-4646). Putting the quotes
|
|
430
|
+
// back first is what lets the recovery tell the whitespace an addr-spec may carry
|
|
431
|
+
// from the wreckage trailing one, as only a quoted local part may hold whitespace
|
|
432
|
+
data.address = _quoteLocalPart(data.text);
|
|
433
|
+
data.text = '';
|
|
434
|
+
}
|
|
387
435
|
_recoverAddrSpec(data);
|
|
388
436
|
const address = {
|
|
389
437
|
address: data.address || data.text || '',
|
|
@@ -397,9 +445,6 @@ function _handleAddress(tokens, depth) {
|
|
|
397
445
|
address.address = '';
|
|
398
446
|
}
|
|
399
447
|
}
|
|
400
|
-
if (addressFromQuotedText && address.address) {
|
|
401
|
-
address.address = _quoteLocalPart(address.address);
|
|
402
|
-
}
|
|
403
448
|
addresses.push(address);
|
|
404
449
|
}
|
|
405
450
|
return addresses;
|
|
@@ -62,13 +62,21 @@ function normalizeDomain(domain, toUnicode) {
|
|
|
62
62
|
return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
|
-
* Removes the
|
|
65
|
+
* Removes the characters that must never reach a multipart delimiter line.
|
|
66
|
+
*
|
|
67
|
+
* A line break splits the delimiter, so the boundary declared in the header can never
|
|
68
|
+
* match it again and the parts go out as body lines instead. The other C0 controls and
|
|
69
|
+
* DEL do not split anything but must not be written either: RFC 5321 does not allow a
|
|
70
|
+
* NUL in DATA at all, and an MTA or a scanner that stops at one reads a different
|
|
71
|
+
* message than a client that does not, which is the same parser disagreement a split
|
|
72
|
+
* delimiter creates. Both sides are cleaned together, since the declared value and the
|
|
73
|
+
* delimiters come from this one result.
|
|
66
74
|
*
|
|
67
75
|
* @param value Value to clean
|
|
68
|
-
* @return Value with every
|
|
76
|
+
* @return Value with every control character removed
|
|
69
77
|
*/
|
|
70
|
-
function
|
|
71
|
-
return value.replace(/[\
|
|
78
|
+
function _stripBoundaryControls(value) {
|
|
79
|
+
return value.replace(/[\x00-\x1f\x7f]+/g, '');
|
|
72
80
|
}
|
|
73
81
|
/**
|
|
74
82
|
* Creates a new mime tree node. Assumes 'multipart/*' as the content type
|
|
@@ -90,12 +98,11 @@ class MimeNode {
|
|
|
90
98
|
this.nodeCounter = 0;
|
|
91
99
|
options = options || {};
|
|
92
100
|
/**
|
|
93
|
-
* shared part of the unique multipart boundary.
|
|
94
|
-
*
|
|
95
|
-
* never match them again and the remainder would go out as body lines
|
|
101
|
+
* shared part of the unique multipart boundary. Control characters are dropped
|
|
102
|
+
* here rather than at the delimiter, see _stripBoundaryControls
|
|
96
103
|
*/
|
|
97
|
-
this.baseBoundary =
|
|
98
|
-
this.boundaryPrefix =
|
|
104
|
+
this.baseBoundary = _stripBoundaryControls(options.baseBoundary || crypto.randomBytes(8).toString('hex'));
|
|
105
|
+
this.boundaryPrefix = _stripBoundaryControls(options.boundaryPrefix || '--_NmP');
|
|
99
106
|
this.disableFileAccess = !!options.disableFileAccess;
|
|
100
107
|
this.disableUrlAccess = !!options.disableUrlAccess;
|
|
101
108
|
this.normalizeHeaderKey = options.normalizeHeaderKey;
|
|
@@ -1142,19 +1149,20 @@ class MimeNode {
|
|
|
1142
1149
|
this.contentType = structured.value.trim().toLowerCase();
|
|
1143
1150
|
this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
|
|
1144
1151
|
if (this.multipart) {
|
|
1145
|
-
//
|
|
1146
|
-
//
|
|
1147
|
-
//
|
|
1148
|
-
// delimiters are assigned from the same expression here, so whatever the
|
|
1149
|
-
// header emitter then does with it, the two sides cannot disagree.
|
|
1152
|
+
// The declared value and the delimiters are assigned from the same expression
|
|
1153
|
+
// here, so whatever the header emitter then does with it, the two sides cannot
|
|
1154
|
+
// disagree about which characters the boundary is made of.
|
|
1150
1155
|
//
|
|
1151
1156
|
// Stripping runs before the fallback rather than over the whole chain: a
|
|
1152
|
-
// boundary
|
|
1157
|
+
// boundary made only of control characters would otherwise strip to '' and
|
|
1153
1158
|
// leave the node declaring no boundary and streaming bare '--' delimiters.
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
|
|
1157
|
-
|
|
1159
|
+
// _generateBoundary cleans what it builds, but this is the one place the boundary
|
|
1160
|
+
// is written, so it cleans the value it is about to write rather than trusting
|
|
1161
|
+
// where it came from. MimeNode is exported and subclassable, so the generator is
|
|
1162
|
+
// not necessarily the one below. Stripping twice costs nothing, it is idempotent.
|
|
1163
|
+
const declared = _stripBoundaryControls(structured.params.boundary || this.boundary || '');
|
|
1164
|
+
this.boundary = structured.params.boundary =
|
|
1165
|
+
declared || _stripBoundaryControls(this._generateBoundary());
|
|
1158
1166
|
}
|
|
1159
1167
|
else {
|
|
1160
1168
|
this.boundary = false;
|
|
@@ -1167,7 +1175,9 @@ class MimeNode {
|
|
|
1167
1175
|
* @internal
|
|
1168
1176
|
*/
|
|
1169
1177
|
_generateBoundary() {
|
|
1170
|
-
|
|
1178
|
+
// baseBoundary and boundaryPrefix are public, so they may hold something the
|
|
1179
|
+
// constructor never cleaned, and the -Part_ suffix keeps the result non empty
|
|
1180
|
+
return _stripBoundaryControls(this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary) + '-Part_' + this._nodeId;
|
|
1171
1181
|
}
|
|
1172
1182
|
/**
|
|
1173
1183
|
* Encodes a header value for use in the generated rfc2822 email.
|
package/dist/esm/package-info.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nodemailer",
|
|
3
|
-
"version": "10.0.
|
|
3
|
+
"version": "10.0.9",
|
|
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",
|
|
@@ -147,24 +147,24 @@
|
|
|
147
147
|
},
|
|
148
148
|
"homepage": "https://nodemailer.com/",
|
|
149
149
|
"devDependencies": {
|
|
150
|
-
"@aws-sdk/client-sesv2": "3.
|
|
150
|
+
"@aws-sdk/client-sesv2": "3.1131.0",
|
|
151
151
|
"@types/node": "20.19.43",
|
|
152
152
|
"bunyan": "1.8.15",
|
|
153
153
|
"c8": "12.0.0",
|
|
154
|
-
"eslint": "10.
|
|
154
|
+
"eslint": "10.10.0",
|
|
155
155
|
"eslint-config-prettier": "10.1.8",
|
|
156
156
|
"globals": "17.12.0",
|
|
157
157
|
"libbase64": "1.3.0",
|
|
158
158
|
"libmime": "5.4.3",
|
|
159
159
|
"libqp": "2.1.1",
|
|
160
|
-
"mailauth": "5.0.
|
|
160
|
+
"mailauth": "5.0.3",
|
|
161
161
|
"prettier": "3.9.6",
|
|
162
162
|
"proxy": "1.0.2",
|
|
163
163
|
"proxy-test-server": "1.0.0",
|
|
164
|
-
"smtp-server": "3.19.
|
|
164
|
+
"smtp-server": "3.19.11",
|
|
165
165
|
"tsx": "4.23.13",
|
|
166
166
|
"typescript": "6.0.3",
|
|
167
|
-
"typescript-eslint": "8.
|
|
167
|
+
"typescript-eslint": "8.70.0"
|
|
168
168
|
},
|
|
169
169
|
"engines": {
|
|
170
170
|
"node": ">=20.0.0"
|