nodemailer 10.0.1 → 10.0.2

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,13 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [10.0.2](https://github.com/nodemailer/nodemailer/compare/v10.0.1...v10.0.2) (2026-09-09)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **mime-node:** flatten nested recipient arrays without recursion ([ebe0849](https://github.com/nodemailer/nodemailer/commit/ebe084940aef88278afc6016b78c6d1c3821bb66))
9
+ * **shared:** keep the TLS server name out of the DNS cache ([a6512db](https://github.com/nodemailer/nodemailer/commit/a6512dbcb3c6e7f2f70d3acccc5752defe3c61fe))
10
+
3
11
  ## [10.0.1](https://github.com/nodemailer/nodemailer/compare/v10.0.0...v10.0.1) (2026-09-07)
4
12
 
5
13
 
@@ -172,12 +172,24 @@ class Mail extends node_events_1.EventEmitter {
172
172
  }, 'PluginCompile Error: %s', err.message);
173
173
  return done(err);
174
174
  }
175
- mail.message = new index_js_1.default(mail.data).compile();
176
- mail.setMailerHeader();
177
- mail.setPriorityHeaders();
178
- mail.setListHeaders();
175
+ let recipientCount;
176
+ try {
177
+ mail.message = new index_js_1.default(mail.data).compile();
178
+ mail.setMailerHeader();
179
+ mail.setPriorityHeaders();
180
+ mail.setListHeaders();
181
+ recipientCount = mail.message.getEnvelope().to.length;
182
+ }
183
+ catch (err) {
184
+ // message data can throw while it is compiled, the error belongs to the callback
185
+ this.logger.error({
186
+ err,
187
+ tnx: 'transport',
188
+ action: 'send'
189
+ }, 'Compile Error: %s', err.message);
190
+ return done(err);
191
+ }
179
192
  const maxRecipients = mail.data.maxRecipients === undefined ? DEFAULT_MAX_RECIPIENTS : mail.data.maxRecipients;
180
- const recipientCount = mail.message.getEnvelope().to.length;
181
193
  if (maxRecipients && recipientCount > maxRecipients) {
182
194
  const err = new Error(`Message has ${recipientCount} recipients, which is over the ${maxRecipients} allowed by maxRecipients`);
183
195
  err.code = errors.EMAXRECIPIENTS;
@@ -1042,17 +1042,38 @@ class MimeNode {
1042
1042
  * @internal
1043
1043
  */
1044
1044
  _parseAddresses(addresses) {
1045
- // Collected into one list as we go. concat.apply spreads the entries into arguments
1046
- // and throws a RangeError once a recipient array is long enough to pass the
1047
- // argument limit, which a large Bcc list reaches on its own.
1048
1045
  const flattened = [];
1049
- [].concat(addresses).forEach(address => {
1046
+ // Nested arrays are flattened iteratively. concat.apply throws once a recipient list
1047
+ // is long enough to pass the argument limit, and an array handed to addressparser is
1048
+ // stringified, which recurses once per nesting level and exhausts the call stack on
1049
+ // a deep enough value. Every array is walked once, which also ends a self-referential
1050
+ // input
1051
+ const seen = new WeakSet();
1052
+ const stack = [];
1053
+ const enter = (list) => {
1054
+ if (!seen.has(list)) {
1055
+ seen.add(list);
1056
+ stack.push({ list, pos: 0 });
1057
+ }
1058
+ };
1059
+ enter(Array.isArray(addresses) ? addresses : [addresses]);
1060
+ while (stack.length) {
1061
+ const frame = stack[stack.length - 1];
1062
+ if (frame.pos >= frame.list.length) {
1063
+ stack.pop();
1064
+ continue;
1065
+ }
1066
+ const address = frame.list[frame.pos++];
1067
+ if (Array.isArray(address)) {
1068
+ enter(address);
1069
+ continue;
1070
+ }
1050
1071
  if (address && address.address) {
1051
1072
  const normalized = this._normalizeAddress(address.address);
1052
1073
  if (normalized === address.address && typeof address.name === 'string') {
1053
1074
  // there is nothing to rewrite, so there is nothing to keep off the original
1054
1075
  flattened.push(address);
1055
- return;
1076
+ continue;
1056
1077
  }
1057
1078
  // rewriting would land on the object the caller passed in and might
1058
1079
  // still hold a reference to, so rewrite a copy of it instead. An own
@@ -1062,13 +1083,13 @@ class MimeNode {
1062
1083
  copy.address = normalized;
1063
1084
  copy.name = address.name || '';
1064
1085
  flattened.push(copy);
1065
- return;
1086
+ continue;
1066
1087
  }
1067
1088
  const parsed = this._normalizeParsedAddresses((0, index_js_1.default)(address));
1068
1089
  for (let i = 0; i < parsed.length; i++) {
1069
1090
  flattened.push(parsed[i]);
1070
1091
  }
1071
- });
1092
+ }
1072
1093
  return flattened;
1073
1094
  }
1074
1095
  /**
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.0.1";
2
+ export declare const version = "10.0.2";
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.1';
6
+ exports.version = '10.0.2';
7
7
  exports.homepage = 'https://nodemailer.com/';
@@ -39,7 +39,6 @@ export interface ResolvedHostname {
39
39
  */
40
40
  export interface DnsCacheValue {
41
41
  addresses: string[];
42
- servername: string | false;
43
42
  }
44
43
  /**
45
44
  * A DNS cache entry
@@ -109,7 +109,6 @@ const formatDNSValue = (value, extra) => {
109
109
  // Select a random address from available addresses, or null if none
110
110
  const host = addresses.length > 0 ? addresses[Math.floor(Math.random() * addresses.length)] : null;
111
111
  return Object.assign({
112
- servername: value.servername,
113
112
  host,
114
113
  // Include all addresses for connection fallback support
115
114
  _addresses: addresses
@@ -123,14 +122,19 @@ const resolveHostname = (options, callback) => {
123
122
  if (!options.host || node_net_1.default.isIP(options.host)) {
124
123
  // nothing to do here
125
124
  const value = {
126
- addresses: [options.host],
127
- servername: options.servername || false
125
+ addresses: [options.host]
128
126
  };
129
127
  return callback(null, formatDNSValue(value, {
128
+ servername: options.servername || false,
130
129
  cached: false
131
130
  }));
132
131
  }
133
132
  const host = options.host;
133
+ // The TLS server name belongs to the connection asking, not to the host it resolves. The
134
+ // cache is shared by every transport of the process and keyed by host alone, so a server
135
+ // name stored in it would be the one of whichever transport resolved the host first, and a
136
+ // later transport with its own tls.servername would present and verify that name instead
137
+ const servername = options.servername || host;
134
138
  let cached;
135
139
  if (exports.dnsCache.has(options.host)) {
136
140
  cached = exports.dnsCache.get(options.host);
@@ -153,6 +157,7 @@ const resolveHostname = (options, callback) => {
153
157
  }
154
158
  if (!cached.expires || cached.expires >= now) {
155
159
  return callback(null, formatDNSValue(cached.value, {
160
+ servername,
156
161
  cached: true
157
162
  }));
158
163
  }
@@ -180,14 +185,14 @@ const resolveHostname = (options, callback) => {
180
185
  const allAddresses = ipv4Addresses.concat(ipv6Addresses);
181
186
  if (allAddresses.length) {
182
187
  const value = {
183
- addresses: allAddresses,
184
- servername: options.servername || host
188
+ addresses: allAddresses
185
189
  };
186
190
  exports.dnsCache.set(host, {
187
191
  value,
188
192
  expires: Date.now() + (options.dnsTtl || DNS_TTL)
189
193
  });
190
194
  return callback(null, formatDNSValue(value, {
195
+ servername,
191
196
  cached: false
192
197
  }));
193
198
  }
@@ -200,6 +205,7 @@ const resolveHostname = (options, callback) => {
200
205
  expires: Date.now() + (options.dnsTtl || DNS_TTL)
201
206
  });
202
207
  return callback(null, formatDNSValue(cached.value, {
208
+ servername,
203
209
  cached: true,
204
210
  error: ipv4Error
205
211
  }));
@@ -214,6 +220,7 @@ const resolveHostname = (options, callback) => {
214
220
  expires: Date.now() + (options.dnsTtl || DNS_TTL)
215
221
  });
216
222
  return callback(null, formatDNSValue(cached.value, {
223
+ servername,
217
224
  cached: true,
218
225
  error: err
219
226
  }));
@@ -231,18 +238,19 @@ const resolveHostname = (options, callback) => {
231
238
  if (!supportedAddresses.length && cached) {
232
239
  // nothing was found, fallback to cached value
233
240
  return callback(null, formatDNSValue(cached.value, {
241
+ servername,
234
242
  cached: true
235
243
  }));
236
244
  }
237
245
  const value = {
238
- addresses: supportedAddresses.length ? supportedAddresses : [host],
239
- servername: options.servername || host
246
+ addresses: supportedAddresses.length ? supportedAddresses : [host]
240
247
  };
241
248
  exports.dnsCache.set(host, {
242
249
  value,
243
250
  expires: Date.now() + (options.dnsTtl || DNS_TTL)
244
251
  });
245
252
  return callback(null, formatDNSValue(value, {
253
+ servername,
246
254
  cached: false
247
255
  }));
248
256
  });
@@ -254,6 +262,7 @@ const resolveHostname = (options, callback) => {
254
262
  expires: Date.now() + (options.dnsTtl || DNS_TTL)
255
263
  });
256
264
  return callback(null, formatDNSValue(cached.value, {
265
+ servername,
257
266
  cached: true,
258
267
  error: lookupErr
259
268
  }));
@@ -134,12 +134,24 @@ class Mail extends EventEmitter {
134
134
  }, 'PluginCompile Error: %s', err.message);
135
135
  return done(err);
136
136
  }
137
- mail.message = new MailComposer(mail.data).compile();
138
- mail.setMailerHeader();
139
- mail.setPriorityHeaders();
140
- mail.setListHeaders();
137
+ let recipientCount;
138
+ try {
139
+ mail.message = new MailComposer(mail.data).compile();
140
+ mail.setMailerHeader();
141
+ mail.setPriorityHeaders();
142
+ mail.setListHeaders();
143
+ recipientCount = mail.message.getEnvelope().to.length;
144
+ }
145
+ catch (err) {
146
+ // message data can throw while it is compiled, the error belongs to the callback
147
+ this.logger.error({
148
+ err,
149
+ tnx: 'transport',
150
+ action: 'send'
151
+ }, 'Compile Error: %s', err.message);
152
+ return done(err);
153
+ }
141
154
  const maxRecipients = mail.data.maxRecipients === undefined ? DEFAULT_MAX_RECIPIENTS : mail.data.maxRecipients;
142
- const recipientCount = mail.message.getEnvelope().to.length;
143
155
  if (maxRecipients && recipientCount > maxRecipients) {
144
156
  const err = new Error(`Message has ${recipientCount} recipients, which is over the ${maxRecipients} allowed by maxRecipients`);
145
157
  err.code = errors.EMAXRECIPIENTS;
@@ -1004,17 +1004,38 @@ class MimeNode {
1004
1004
  * @internal
1005
1005
  */
1006
1006
  _parseAddresses(addresses) {
1007
- // Collected into one list as we go. concat.apply spreads the entries into arguments
1008
- // and throws a RangeError once a recipient array is long enough to pass the
1009
- // argument limit, which a large Bcc list reaches on its own.
1010
1007
  const flattened = [];
1011
- [].concat(addresses).forEach(address => {
1008
+ // Nested arrays are flattened iteratively. concat.apply throws once a recipient list
1009
+ // is long enough to pass the argument limit, and an array handed to addressparser is
1010
+ // stringified, which recurses once per nesting level and exhausts the call stack on
1011
+ // a deep enough value. Every array is walked once, which also ends a self-referential
1012
+ // input
1013
+ const seen = new WeakSet();
1014
+ const stack = [];
1015
+ const enter = (list) => {
1016
+ if (!seen.has(list)) {
1017
+ seen.add(list);
1018
+ stack.push({ list, pos: 0 });
1019
+ }
1020
+ };
1021
+ enter(Array.isArray(addresses) ? addresses : [addresses]);
1022
+ while (stack.length) {
1023
+ const frame = stack[stack.length - 1];
1024
+ if (frame.pos >= frame.list.length) {
1025
+ stack.pop();
1026
+ continue;
1027
+ }
1028
+ const address = frame.list[frame.pos++];
1029
+ if (Array.isArray(address)) {
1030
+ enter(address);
1031
+ continue;
1032
+ }
1012
1033
  if (address && address.address) {
1013
1034
  const normalized = this._normalizeAddress(address.address);
1014
1035
  if (normalized === address.address && typeof address.name === 'string') {
1015
1036
  // there is nothing to rewrite, so there is nothing to keep off the original
1016
1037
  flattened.push(address);
1017
- return;
1038
+ continue;
1018
1039
  }
1019
1040
  // rewriting would land on the object the caller passed in and might
1020
1041
  // still hold a reference to, so rewrite a copy of it instead. An own
@@ -1024,13 +1045,13 @@ class MimeNode {
1024
1045
  copy.address = normalized;
1025
1046
  copy.name = address.name || '';
1026
1047
  flattened.push(copy);
1027
- return;
1048
+ continue;
1028
1049
  }
1029
1050
  const parsed = this._normalizeParsedAddresses(addressparser(address));
1030
1051
  for (let i = 0; i < parsed.length; i++) {
1031
1052
  flattened.push(parsed[i]);
1032
1053
  }
1033
- });
1054
+ }
1034
1055
  return flattened;
1035
1056
  }
1036
1057
  /**
@@ -1,3 +1,3 @@
1
1
  export declare const name = "nodemailer";
2
- export declare const version = "10.0.1";
2
+ export declare const version = "10.0.2";
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.1';
3
+ export const version = '10.0.2';
4
4
  export const homepage = 'https://nodemailer.com/';
@@ -39,7 +39,6 @@ export interface ResolvedHostname {
39
39
  */
40
40
  export interface DnsCacheValue {
41
41
  addresses: string[];
42
- servername: string | false;
43
42
  }
44
43
  /**
45
44
  * A DNS cache entry
@@ -68,7 +68,6 @@ const formatDNSValue = (value, extra) => {
68
68
  // Select a random address from available addresses, or null if none
69
69
  const host = addresses.length > 0 ? addresses[Math.floor(Math.random() * addresses.length)] : null;
70
70
  return Object.assign({
71
- servername: value.servername,
72
71
  host,
73
72
  // Include all addresses for connection fallback support
74
73
  _addresses: addresses
@@ -82,14 +81,19 @@ export const resolveHostname = (options, callback) => {
82
81
  if (!options.host || net.isIP(options.host)) {
83
82
  // nothing to do here
84
83
  const value = {
85
- addresses: [options.host],
86
- servername: options.servername || false
84
+ addresses: [options.host]
87
85
  };
88
86
  return callback(null, formatDNSValue(value, {
87
+ servername: options.servername || false,
89
88
  cached: false
90
89
  }));
91
90
  }
92
91
  const host = options.host;
92
+ // The TLS server name belongs to the connection asking, not to the host it resolves. The
93
+ // cache is shared by every transport of the process and keyed by host alone, so a server
94
+ // name stored in it would be the one of whichever transport resolved the host first, and a
95
+ // later transport with its own tls.servername would present and verify that name instead
96
+ const servername = options.servername || host;
93
97
  let cached;
94
98
  if (dnsCache.has(options.host)) {
95
99
  cached = dnsCache.get(options.host);
@@ -112,6 +116,7 @@ export const resolveHostname = (options, callback) => {
112
116
  }
113
117
  if (!cached.expires || cached.expires >= now) {
114
118
  return callback(null, formatDNSValue(cached.value, {
119
+ servername,
115
120
  cached: true
116
121
  }));
117
122
  }
@@ -139,14 +144,14 @@ export const resolveHostname = (options, callback) => {
139
144
  const allAddresses = ipv4Addresses.concat(ipv6Addresses);
140
145
  if (allAddresses.length) {
141
146
  const value = {
142
- addresses: allAddresses,
143
- servername: options.servername || host
147
+ addresses: allAddresses
144
148
  };
145
149
  dnsCache.set(host, {
146
150
  value,
147
151
  expires: Date.now() + (options.dnsTtl || DNS_TTL)
148
152
  });
149
153
  return callback(null, formatDNSValue(value, {
154
+ servername,
150
155
  cached: false
151
156
  }));
152
157
  }
@@ -159,6 +164,7 @@ export const resolveHostname = (options, callback) => {
159
164
  expires: Date.now() + (options.dnsTtl || DNS_TTL)
160
165
  });
161
166
  return callback(null, formatDNSValue(cached.value, {
167
+ servername,
162
168
  cached: true,
163
169
  error: ipv4Error
164
170
  }));
@@ -173,6 +179,7 @@ export const resolveHostname = (options, callback) => {
173
179
  expires: Date.now() + (options.dnsTtl || DNS_TTL)
174
180
  });
175
181
  return callback(null, formatDNSValue(cached.value, {
182
+ servername,
176
183
  cached: true,
177
184
  error: err
178
185
  }));
@@ -190,18 +197,19 @@ export const resolveHostname = (options, callback) => {
190
197
  if (!supportedAddresses.length && cached) {
191
198
  // nothing was found, fallback to cached value
192
199
  return callback(null, formatDNSValue(cached.value, {
200
+ servername,
193
201
  cached: true
194
202
  }));
195
203
  }
196
204
  const value = {
197
- addresses: supportedAddresses.length ? supportedAddresses : [host],
198
- servername: options.servername || host
205
+ addresses: supportedAddresses.length ? supportedAddresses : [host]
199
206
  };
200
207
  dnsCache.set(host, {
201
208
  value,
202
209
  expires: Date.now() + (options.dnsTtl || DNS_TTL)
203
210
  });
204
211
  return callback(null, formatDNSValue(value, {
212
+ servername,
205
213
  cached: false
206
214
  }));
207
215
  });
@@ -213,6 +221,7 @@ export const resolveHostname = (options, callback) => {
213
221
  expires: Date.now() + (options.dnsTtl || DNS_TTL)
214
222
  });
215
223
  return callback(null, formatDNSValue(cached.value, {
224
+ servername,
216
225
  cached: true,
217
226
  error: lookupErr
218
227
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "10.0.1",
3
+ "version": "10.0.2",
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",