nodemailer 10.0.5 → 10.0.7
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 +17 -0
- package/dist/cjs/addressparser/index.js +116 -8
- package/dist/cjs/mime-funcs/index.js +4 -1
- package/dist/cjs/mime-node/index.js +27 -5
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/smtp-pool/index.js +4 -2
- package/dist/esm/addressparser/index.js +116 -8
- package/dist/esm/mime-funcs/index.js +4 -1
- package/dist/esm/mime-node/index.js +27 -5
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/smtp-pool/index.js +4 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [10.0.7](https://github.com/nodemailer/nodemailer/compare/v10.0.6...v10.0.7) (2026-09-11)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **mime-funcs:** do not double encode Buffer input when chunking base64 mime words ([#1865](https://github.com/nodemailer/nodemailer/issues/1865)) ([4327a59](https://github.com/nodemailer/nodemailer/commit/4327a59939748a7f9394b0a029495fd476f68f30))
|
|
9
|
+
* **mime-node:** keep a boundary that is only line breaks from stripping to empty ([ec46800](https://github.com/nodemailer/nodemailer/commit/ec46800cdcab734d9aa79e1278e819962b3b8f09))
|
|
10
|
+
* **mime-node:** strip line breaks from multipart boundary material ([#1867](https://github.com/nodemailer/nodemailer/issues/1867)) ([03c1a5c](https://github.com/nodemailer/nodemailer/commit/03c1a5c48b6828d9392983d8718fdb1fd583a06c))
|
|
11
|
+
* **smtp-pool:** release rate-limited connections on close ([#1866](https://github.com/nodemailer/nodemailer/issues/1866)) ([7f5c7a4](https://github.com/nodemailer/nodemailer/commit/7f5c7a46b61da9f5c5feaffd921c55b9d5892ee5))
|
|
12
|
+
|
|
13
|
+
## [10.0.6](https://github.com/nodemailer/nodemailer/compare/v10.0.5...v10.0.6) (2026-09-11)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* **addressparser:** scan free text for an address in linear time ([437d7fc](https://github.com/nodemailer/nodemailer/commit/437d7fc47403df176bc39271641541b7a9bce102))
|
|
19
|
+
|
|
3
20
|
## [10.0.5](https://github.com/nodemailer/nodemailer/compare/v10.0.4...v10.0.5) (2026-09-11)
|
|
4
21
|
|
|
5
22
|
|
|
@@ -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
|
}
|
|
@@ -125,7 +125,10 @@ function encodeWord(data, mimeWordEncoding, maxLength) {
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
else if (mimeWordEncoding === 'B') {
|
|
128
|
-
|
|
128
|
+
// the chunking loop below splits raw text and base64 encodes each part, so a
|
|
129
|
+
// Buffer goes in as its UTF-8 string: handing it the base64 of the whole input
|
|
130
|
+
// would encode the encoding itself and decode back to base64 text
|
|
131
|
+
encodedStr = typeof data === 'string' ? data : data.toString('utf-8');
|
|
129
132
|
maxLength = maxLength ? Math.max(3, ((maxLength - (maxLength % 4)) / 4) * 3) : 0;
|
|
130
133
|
}
|
|
131
134
|
if (maxLength && (mimeWordEncoding !== 'B' ? encodedStr : base64.encode(data)).length > maxLength) {
|
|
@@ -99,6 +99,15 @@ function normalizeDomain(domain, toUnicode) {
|
|
|
99
99
|
}
|
|
100
100
|
return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Removes the line breaks that would split a value across lines once it is written out.
|
|
104
|
+
*
|
|
105
|
+
* @param value Value to clean
|
|
106
|
+
* @return Value with every CR and LF removed
|
|
107
|
+
*/
|
|
108
|
+
function _stripLineBreaks(value) {
|
|
109
|
+
return value.replace(/[\r\n]+/g, '');
|
|
110
|
+
}
|
|
102
111
|
/**
|
|
103
112
|
* Creates a new mime tree node. Assumes 'multipart/*' as the content type
|
|
104
113
|
* if it is a branch, anything else counts as leaf. If rootNode is missing from
|
|
@@ -119,10 +128,12 @@ class MimeNode {
|
|
|
119
128
|
this.nodeCounter = 0;
|
|
120
129
|
options = options || {};
|
|
121
130
|
/**
|
|
122
|
-
* shared part of the unique multipart boundary
|
|
131
|
+
* shared part of the unique multipart boundary. A line break here would split
|
|
132
|
+
* the delimiter lines the tree is streamed with, so the declared boundary could
|
|
133
|
+
* never match them again and the remainder would go out as body lines
|
|
123
134
|
*/
|
|
124
|
-
this.baseBoundary = options.baseBoundary || node_crypto_1.default.randomBytes(8).toString('hex');
|
|
125
|
-
this.boundaryPrefix = options.boundaryPrefix || '--_NmP';
|
|
135
|
+
this.baseBoundary = _stripLineBreaks(options.baseBoundary || node_crypto_1.default.randomBytes(8).toString('hex'));
|
|
136
|
+
this.boundaryPrefix = _stripLineBreaks(options.boundaryPrefix || '--_NmP');
|
|
126
137
|
this.disableFileAccess = !!options.disableFileAccess;
|
|
127
138
|
this.disableUrlAccess = !!options.disableUrlAccess;
|
|
128
139
|
this.normalizeHeaderKey = options.normalizeHeaderKey;
|
|
@@ -1169,8 +1180,19 @@ class MimeNode {
|
|
|
1169
1180
|
this.contentType = structured.value.trim().toLowerCase();
|
|
1170
1181
|
this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
|
|
1171
1182
|
if (this.multipart) {
|
|
1172
|
-
|
|
1173
|
-
|
|
1183
|
+
// A line break in the boundary would split the delimiter lines the tree is
|
|
1184
|
+
// streamed with, so the declared boundary could never match them again and
|
|
1185
|
+
// the remainder would go out as body lines. The declared value and the
|
|
1186
|
+
// delimiters are assigned from the same expression here, so whatever the
|
|
1187
|
+
// header emitter then does with it, the two sides cannot disagree.
|
|
1188
|
+
//
|
|
1189
|
+
// Stripping runs before the fallback rather than over the whole chain: a
|
|
1190
|
+
// boundary that was nothing but line breaks would otherwise strip to '' and
|
|
1191
|
+
// leave the node declaring no boundary and streaming bare '--' delimiters.
|
|
1192
|
+
// The generated boundary is stripped too, since baseBoundary and
|
|
1193
|
+
// boundaryPrefix are public and may have been written after construction.
|
|
1194
|
+
const declared = _stripLineBreaks(structured.params.boundary || this.boundary || '');
|
|
1195
|
+
this.boundary = structured.params.boundary = declared || _stripLineBreaks(this._generateBoundary());
|
|
1174
1196
|
}
|
|
1175
1197
|
else {
|
|
1176
1198
|
this.boundary = false;
|
package/dist/cjs/package-info.js
CHANGED
|
@@ -138,8 +138,10 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|
|
138
138
|
let connection;
|
|
139
139
|
const len = this._connections.length;
|
|
140
140
|
this._closed = true;
|
|
141
|
-
//
|
|
142
|
-
|
|
141
|
+
// release connections gated by the rate limiter so they become
|
|
142
|
+
// available and are torn down below instead of leaking with a
|
|
143
|
+
// cleared timer that never fires
|
|
144
|
+
this._clearRateLimit();
|
|
143
145
|
if (!len && !this._queue.length) {
|
|
144
146
|
return;
|
|
145
147
|
}
|
|
@@ -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
|
}
|
|
@@ -76,7 +76,10 @@ export function encodeWord(data, mimeWordEncoding, maxLength) {
|
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
78
|
else if (mimeWordEncoding === 'B') {
|
|
79
|
-
|
|
79
|
+
// the chunking loop below splits raw text and base64 encodes each part, so a
|
|
80
|
+
// Buffer goes in as its UTF-8 string: handing it the base64 of the whole input
|
|
81
|
+
// would encode the encoding itself and decode back to base64 text
|
|
82
|
+
encodedStr = typeof data === 'string' ? data : data.toString('utf-8');
|
|
80
83
|
maxLength = maxLength ? Math.max(3, ((maxLength - (maxLength % 4)) / 4) * 3) : 0;
|
|
81
84
|
}
|
|
82
85
|
if (maxLength && (mimeWordEncoding !== 'B' ? encodedStr : base64.encode(data)).length > maxLength) {
|
|
@@ -61,6 +61,15 @@ function normalizeDomain(domain, toUnicode) {
|
|
|
61
61
|
}
|
|
62
62
|
return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
|
|
63
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Removes the line breaks that would split a value across lines once it is written out.
|
|
66
|
+
*
|
|
67
|
+
* @param value Value to clean
|
|
68
|
+
* @return Value with every CR and LF removed
|
|
69
|
+
*/
|
|
70
|
+
function _stripLineBreaks(value) {
|
|
71
|
+
return value.replace(/[\r\n]+/g, '');
|
|
72
|
+
}
|
|
64
73
|
/**
|
|
65
74
|
* Creates a new mime tree node. Assumes 'multipart/*' as the content type
|
|
66
75
|
* if it is a branch, anything else counts as leaf. If rootNode is missing from
|
|
@@ -81,10 +90,12 @@ class MimeNode {
|
|
|
81
90
|
this.nodeCounter = 0;
|
|
82
91
|
options = options || {};
|
|
83
92
|
/**
|
|
84
|
-
* shared part of the unique multipart boundary
|
|
93
|
+
* shared part of the unique multipart boundary. A line break here would split
|
|
94
|
+
* the delimiter lines the tree is streamed with, so the declared boundary could
|
|
95
|
+
* never match them again and the remainder would go out as body lines
|
|
85
96
|
*/
|
|
86
|
-
this.baseBoundary = options.baseBoundary || crypto.randomBytes(8).toString('hex');
|
|
87
|
-
this.boundaryPrefix = options.boundaryPrefix || '--_NmP';
|
|
97
|
+
this.baseBoundary = _stripLineBreaks(options.baseBoundary || crypto.randomBytes(8).toString('hex'));
|
|
98
|
+
this.boundaryPrefix = _stripLineBreaks(options.boundaryPrefix || '--_NmP');
|
|
88
99
|
this.disableFileAccess = !!options.disableFileAccess;
|
|
89
100
|
this.disableUrlAccess = !!options.disableUrlAccess;
|
|
90
101
|
this.normalizeHeaderKey = options.normalizeHeaderKey;
|
|
@@ -1131,8 +1142,19 @@ class MimeNode {
|
|
|
1131
1142
|
this.contentType = structured.value.trim().toLowerCase();
|
|
1132
1143
|
this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
|
|
1133
1144
|
if (this.multipart) {
|
|
1134
|
-
|
|
1135
|
-
|
|
1145
|
+
// A line break in the boundary would split the delimiter lines the tree is
|
|
1146
|
+
// streamed with, so the declared boundary could never match them again and
|
|
1147
|
+
// the remainder would go out as body lines. The declared value and the
|
|
1148
|
+
// delimiters are assigned from the same expression here, so whatever the
|
|
1149
|
+
// header emitter then does with it, the two sides cannot disagree.
|
|
1150
|
+
//
|
|
1151
|
+
// Stripping runs before the fallback rather than over the whole chain: a
|
|
1152
|
+
// boundary that was nothing but line breaks would otherwise strip to '' and
|
|
1153
|
+
// leave the node declaring no boundary and streaming bare '--' delimiters.
|
|
1154
|
+
// The generated boundary is stripped too, since baseBoundary and
|
|
1155
|
+
// boundaryPrefix are public and may have been written after construction.
|
|
1156
|
+
const declared = _stripLineBreaks(structured.params.boundary || this.boundary || '');
|
|
1157
|
+
this.boundary = structured.params.boundary = declared || _stripLineBreaks(this._generateBoundary());
|
|
1136
1158
|
}
|
|
1137
1159
|
else {
|
|
1138
1160
|
this.boundary = false;
|
package/dist/esm/package-info.js
CHANGED
|
@@ -100,8 +100,10 @@ class SMTPPool extends EventEmitter {
|
|
|
100
100
|
let connection;
|
|
101
101
|
const len = this._connections.length;
|
|
102
102
|
this._closed = true;
|
|
103
|
-
//
|
|
104
|
-
|
|
103
|
+
// release connections gated by the rate limiter so they become
|
|
104
|
+
// available and are torn down below instead of leaking with a
|
|
105
|
+
// cleared timer that never fires
|
|
106
|
+
this._clearRateLimit();
|
|
105
107
|
if (!len && !this._queue.length) {
|
|
106
108
|
return;
|
|
107
109
|
}
|