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 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
- 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
  }
@@ -125,7 +125,10 @@ function encodeWord(data, mimeWordEncoding, maxLength) {
125
125
  });
126
126
  }
127
127
  else if (mimeWordEncoding === 'B') {
128
- encodedStr = typeof data === 'string' ? data : base64.encode(data);
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
- this.boundary = structured.params.boundary =
1173
- structured.params.boundary || this.boundary || this._generateBoundary();
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;
@@ -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.7";
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.7';
7
7
  exports.homepage = 'https://nodemailer.com/';
@@ -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
- // clear rate limit timer if it exists
142
- clearTimeout(this._rateLimit.timeout);
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
- 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
  }
@@ -76,7 +76,10 @@ export function encodeWord(data, mimeWordEncoding, maxLength) {
76
76
  });
77
77
  }
78
78
  else if (mimeWordEncoding === 'B') {
79
- encodedStr = typeof data === 'string' ? data : base64.encode(data);
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
- this.boundary = structured.params.boundary =
1135
- structured.params.boundary || this.boundary || this._generateBoundary();
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;
@@ -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.7";
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.7';
4
4
  export const homepage = 'https://nodemailer.com/';
@@ -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
- // clear rate limit timer if it exists
104
- clearTimeout(this._rateLimit.timeout);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "10.0.5",
3
+ "version": "10.0.7",
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",