nodemailer 10.0.6 → 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,15 @@
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
+
3
13
  ## [10.0.6](https://github.com/nodemailer/nodemailer/compare/v10.0.5...v10.0.6) (2026-09-11)
4
14
 
5
15
 
@@ -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.6";
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.6';
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
  }
@@ -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.6";
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.6';
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.6",
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",