nodemailer 10.0.6 → 10.0.8

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,23 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [10.0.8](https://github.com/nodemailer/nodemailer/compare/v10.0.7...v10.0.8) (2026-09-11)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **mime-node:** clean the boundary where it is written, not only where it is built ([e14278d](https://github.com/nodemailer/nodemailer/commit/e14278d2dae9427280c79450605a27b1c5d2c355))
9
+ * **mime-node:** drop every control character from multipart boundary material ([a82a355](https://github.com/nodemailer/nodemailer/commit/a82a35554848a2e07485ff81f80aefe2736e5a04))
10
+
11
+ ## [10.0.7](https://github.com/nodemailer/nodemailer/compare/v10.0.6...v10.0.7) (2026-09-11)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * **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))
17
+ * **mime-node:** keep a boundary that is only line breaks from stripping to empty ([ec46800](https://github.com/nodemailer/nodemailer/commit/ec46800cdcab734d9aa79e1278e819962b3b8f09))
18
+ * **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))
19
+ * **smtp-pool:** release rate-limited connections on close ([#1866](https://github.com/nodemailer/nodemailer/issues/1866)) ([7f5c7a4](https://github.com/nodemailer/nodemailer/commit/7f5c7a46b61da9f5c5feaffd921c55b9d5892ee5))
20
+
3
21
  ## [10.0.6](https://github.com/nodemailer/nodemailer/compare/v10.0.5...v10.0.6) (2026-09-11)
4
22
 
5
23
 
@@ -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,23 @@ function normalizeDomain(domain, toUnicode) {
99
99
  }
100
100
  return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
101
101
  }
102
+ /**
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.
112
+ *
113
+ * @param value Value to clean
114
+ * @return Value with every control character removed
115
+ */
116
+ function _stripBoundaryControls(value) {
117
+ return value.replace(/[\x00-\x1f\x7f]+/g, '');
118
+ }
102
119
  /**
103
120
  * Creates a new mime tree node. Assumes 'multipart/*' as the content type
104
121
  * if it is a branch, anything else counts as leaf. If rootNode is missing from
@@ -119,10 +136,11 @@ class MimeNode {
119
136
  this.nodeCounter = 0;
120
137
  options = options || {};
121
138
  /**
122
- * shared part of the unique multipart boundary
139
+ * shared part of the unique multipart boundary. Control characters are dropped
140
+ * here rather than at the delimiter, see _stripBoundaryControls
123
141
  */
124
- this.baseBoundary = options.baseBoundary || node_crypto_1.default.randomBytes(8).toString('hex');
125
- this.boundaryPrefix = options.boundaryPrefix || '--_NmP';
142
+ this.baseBoundary = _stripBoundaryControls(options.baseBoundary || node_crypto_1.default.randomBytes(8).toString('hex'));
143
+ this.boundaryPrefix = _stripBoundaryControls(options.boundaryPrefix || '--_NmP');
126
144
  this.disableFileAccess = !!options.disableFileAccess;
127
145
  this.disableUrlAccess = !!options.disableUrlAccess;
128
146
  this.normalizeHeaderKey = options.normalizeHeaderKey;
@@ -1169,8 +1187,20 @@ class MimeNode {
1169
1187
  this.contentType = structured.value.trim().toLowerCase();
1170
1188
  this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
1171
1189
  if (this.multipart) {
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.
1193
+ //
1194
+ // Stripping runs before the fallback rather than over the whole chain: a
1195
+ // boundary made only of control characters would otherwise strip to '' and
1196
+ // leave the node declaring no boundary and streaming bare '--' delimiters.
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 || '');
1172
1202
  this.boundary = structured.params.boundary =
1173
- structured.params.boundary || this.boundary || this._generateBoundary();
1203
+ declared || _stripBoundaryControls(this._generateBoundary());
1174
1204
  }
1175
1205
  else {
1176
1206
  this.boundary = false;
@@ -1183,7 +1213,9 @@ class MimeNode {
1183
1213
  * @internal
1184
1214
  */
1185
1215
  _generateBoundary() {
1186
- return this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary + '-Part_' + this._nodeId;
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;
1187
1219
  }
1188
1220
  /**
1189
1221
  * Encodes a header value for use in the generated rfc2822 email.
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.0.6";
2
+ export declare const version = "10.0.8";
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.6';
6
+ exports.version = '10.0.8';
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
  }
@@ -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,23 @@ function normalizeDomain(domain, toUnicode) {
61
61
  }
62
62
  return toUnicode ? punycode.toUnicode(domain) : punycode.toASCII(domain);
63
63
  }
64
+ /**
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.
74
+ *
75
+ * @param value Value to clean
76
+ * @return Value with every control character removed
77
+ */
78
+ function _stripBoundaryControls(value) {
79
+ return value.replace(/[\x00-\x1f\x7f]+/g, '');
80
+ }
64
81
  /**
65
82
  * Creates a new mime tree node. Assumes 'multipart/*' as the content type
66
83
  * if it is a branch, anything else counts as leaf. If rootNode is missing from
@@ -81,10 +98,11 @@ class MimeNode {
81
98
  this.nodeCounter = 0;
82
99
  options = options || {};
83
100
  /**
84
- * shared part of the unique multipart boundary
101
+ * shared part of the unique multipart boundary. Control characters are dropped
102
+ * here rather than at the delimiter, see _stripBoundaryControls
85
103
  */
86
- this.baseBoundary = options.baseBoundary || crypto.randomBytes(8).toString('hex');
87
- this.boundaryPrefix = options.boundaryPrefix || '--_NmP';
104
+ this.baseBoundary = _stripBoundaryControls(options.baseBoundary || crypto.randomBytes(8).toString('hex'));
105
+ this.boundaryPrefix = _stripBoundaryControls(options.boundaryPrefix || '--_NmP');
88
106
  this.disableFileAccess = !!options.disableFileAccess;
89
107
  this.disableUrlAccess = !!options.disableUrlAccess;
90
108
  this.normalizeHeaderKey = options.normalizeHeaderKey;
@@ -1131,8 +1149,20 @@ class MimeNode {
1131
1149
  this.contentType = structured.value.trim().toLowerCase();
1132
1150
  this.multipart = /^multipart\//i.test(this.contentType) ? this.contentType.substr(this.contentType.indexOf('/') + 1) : false;
1133
1151
  if (this.multipart) {
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.
1155
+ //
1156
+ // Stripping runs before the fallback rather than over the whole chain: a
1157
+ // boundary made only of control characters would otherwise strip to '' and
1158
+ // leave the node declaring no boundary and streaming bare '--' delimiters.
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 || '');
1134
1164
  this.boundary = structured.params.boundary =
1135
- structured.params.boundary || this.boundary || this._generateBoundary();
1165
+ declared || _stripBoundaryControls(this._generateBoundary());
1136
1166
  }
1137
1167
  else {
1138
1168
  this.boundary = false;
@@ -1145,7 +1175,9 @@ class MimeNode {
1145
1175
  * @internal
1146
1176
  */
1147
1177
  _generateBoundary() {
1148
- return this.rootNode.boundaryPrefix + '-' + this.rootNode.baseBoundary + '-Part_' + this._nodeId;
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;
1149
1181
  }
1150
1182
  /**
1151
1183
  * Encodes a header value for use in the generated rfc2822 email.
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.0.6";
2
+ export declare const version = "10.0.8";
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.6';
3
+ export const version = '10.0.8';
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.6",
3
+ "version": "10.0.8",
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",