nodemailer 7.0.13 → 8.0.1

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,28 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [8.0.1](https://github.com/nodemailer/nodemailer/compare/v8.0.0...v8.0.1) (2026-02-07)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * absorb TLS errors during socket teardown ([7f8dde4](https://github.com/nodemailer/nodemailer/commit/7f8dde41438c66b8311e888fa5f8c518fcaba6f1))
9
+ * absorb TLS errors during socket teardown ([381f628](https://github.com/nodemailer/nodemailer/commit/381f628d55e62bb3131bd2a452fa1ce00bc48aea))
10
+ * Add Gmail Workspace service configuration ([#1787](https://github.com/nodemailer/nodemailer/issues/1787)) ([dc97ede](https://github.com/nodemailer/nodemailer/commit/dc97ede417b3030b311771541b1f17f5ca76bcbf))
11
+
12
+ ## [8.0.0](https://github.com/nodemailer/nodemailer/compare/v7.0.13...v8.0.0) (2026-02-04)
13
+
14
+
15
+ ### ⚠ BREAKING CHANGES
16
+
17
+ * Error code 'NoAuth' renamed to 'ENOAUTH'
18
+
19
+ ### Bug Fixes
20
+
21
+ * add connection fallback to alternative DNS addresses ([e726d6f](https://github.com/nodemailer/nodemailer/commit/e726d6f44aa7ca14e943d4303243cb5494b09c75))
22
+ * centralize and standardize error codes ([45062ce](https://github.com/nodemailer/nodemailer/commit/45062ce7a4705f3e63c5d9e606547f4d99fd29b5))
23
+ * harden DNS fallback against race conditions and cleanup issues ([4fa3c63](https://github.com/nodemailer/nodemailer/commit/4fa3c63a1f36aefdbaea7f57a133adc458413a47))
24
+ * improve socket cleanup to prevent potential memory leaks ([6069fdc](https://github.com/nodemailer/nodemailer/commit/6069fdcff68a3eef9a9bb16b2bf5ddb924c02091))
25
+
3
26
  ## [7.0.13](https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13) (2026-01-27)
4
27
 
5
28
 
package/lib/errors.js ADDED
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Nodemailer Error Codes
5
+ *
6
+ * Centralized error code definitions for consistent error handling.
7
+ *
8
+ * Usage:
9
+ * const errors = require('./errors');
10
+ * let err = new Error('Connection closed');
11
+ * err.code = errors.ECONNECTION;
12
+ */
13
+
14
+ /**
15
+ * Error code descriptions for documentation and debugging
16
+ */
17
+ const ERROR_CODES = {
18
+ // Connection errors
19
+ ECONNECTION: 'Connection closed unexpectedly',
20
+ ETIMEDOUT: 'Connection or operation timed out',
21
+ ESOCKET: 'Socket-level error',
22
+ EDNS: 'DNS resolution failed',
23
+
24
+ // TLS/Security errors
25
+ ETLS: 'TLS handshake or STARTTLS failed',
26
+ EREQUIRETLS: 'REQUIRETLS not supported by server (RFC 8689)',
27
+
28
+ // Protocol errors
29
+ EPROTOCOL: 'Invalid SMTP server response',
30
+ EENVELOPE: 'Invalid mail envelope (sender or recipients)',
31
+ EMESSAGE: 'Message delivery error',
32
+ ESTREAM: 'Stream processing error',
33
+
34
+ // Authentication errors
35
+ EAUTH: 'Authentication failed',
36
+ ENOAUTH: 'Authentication credentials not provided',
37
+ EOAUTH2: 'OAuth2 token generation or refresh error',
38
+
39
+ // Resource errors
40
+ EMAXLIMIT: 'Pool resource limit reached (max messages per connection)',
41
+
42
+ // Transport-specific errors
43
+ ESENDMAIL: 'Sendmail command error',
44
+ ESES: 'AWS SES transport error',
45
+
46
+ // Configuration and access errors
47
+ ECONFIG: 'Invalid configuration',
48
+ EPROXY: 'Proxy connection error',
49
+ EFILEACCESS: 'File access rejected (disableFileAccess is set)',
50
+ EURLACCESS: 'URL access rejected (disableUrlAccess is set)',
51
+ EFETCH: 'HTTP fetch error'
52
+ };
53
+
54
+ // Export error codes as string constants and the full definitions object
55
+ module.exports = Object.keys(ERROR_CODES).reduce(
56
+ (exports, code) => {
57
+ exports[code] = code;
58
+ return exports;
59
+ },
60
+ { ERROR_CODES }
61
+ );
@@ -8,6 +8,7 @@ const PassThrough = require('stream').PassThrough;
8
8
  const Cookies = require('./cookies');
9
9
  const packageData = require('../../package.json');
10
10
  const net = require('net');
11
+ const errors = require('../errors');
11
12
 
12
13
  const MAX_REDIRECTS = 5;
13
14
 
@@ -76,7 +77,7 @@ function nmfetch(url, options) {
76
77
  return;
77
78
  }
78
79
  finished = true;
79
- err.type = 'FETCH';
80
+ err.code = errors.EFETCH;
80
81
  err.sourceUrl = url;
81
82
  fetchRes.emit('error', err);
82
83
  });
@@ -99,7 +100,7 @@ function nmfetch(url, options) {
99
100
  return;
100
101
  }
101
102
  finished = true;
102
- E.type = 'FETCH';
103
+ E.code = errors.EFETCH;
103
104
  E.sourceUrl = url;
104
105
  fetchRes.emit('error', E);
105
106
  return;
@@ -147,7 +148,7 @@ function nmfetch(url, options) {
147
148
  } catch (E) {
148
149
  finished = true;
149
150
  setImmediate(() => {
150
- E.type = 'FETCH';
151
+ E.code = errors.EFETCH;
151
152
  E.sourceUrl = url;
152
153
  fetchRes.emit('error', E);
153
154
  });
@@ -162,7 +163,7 @@ function nmfetch(url, options) {
162
163
  finished = true;
163
164
  req.abort();
164
165
  let err = new Error('Request Timeout');
165
- err.type = 'FETCH';
166
+ err.code = errors.EFETCH;
166
167
  err.sourceUrl = url;
167
168
  fetchRes.emit('error', err);
168
169
  });
@@ -173,7 +174,7 @@ function nmfetch(url, options) {
173
174
  return;
174
175
  }
175
176
  finished = true;
176
- err.type = 'FETCH';
177
+ err.code = errors.EFETCH;
177
178
  err.sourceUrl = url;
178
179
  fetchRes.emit('error', err);
179
180
  });
@@ -204,7 +205,7 @@ function nmfetch(url, options) {
204
205
  if (options.redirects > options.maxRedirects) {
205
206
  finished = true;
206
207
  let err = new Error('Maximum redirect count exceeded');
207
- err.type = 'FETCH';
208
+ err.code = errors.EFETCH;
208
209
  err.sourceUrl = url;
209
210
  fetchRes.emit('error', err);
210
211
  req.abort();
@@ -222,7 +223,7 @@ function nmfetch(url, options) {
222
223
  if (res.statusCode >= 300 && !options.allowErrorResponse) {
223
224
  finished = true;
224
225
  let err = new Error('Invalid status code ' + res.statusCode);
225
- err.type = 'FETCH';
226
+ err.code = errors.EFETCH;
226
227
  err.sourceUrl = url;
227
228
  fetchRes.emit('error', err);
228
229
  req.abort();
@@ -234,7 +235,7 @@ function nmfetch(url, options) {
234
235
  return;
235
236
  }
236
237
  finished = true;
237
- err.type = 'FETCH';
238
+ err.code = errors.EFETCH;
238
239
  err.sourceUrl = url;
239
240
  fetchRes.emit('error', err);
240
241
  req.abort();
@@ -247,7 +248,7 @@ function nmfetch(url, options) {
247
248
  return;
248
249
  }
249
250
  finished = true;
250
- err.type = 'FETCH';
251
+ err.code = errors.EFETCH;
251
252
  err.sourceUrl = url;
252
253
  fetchRes.emit('error', err);
253
254
  req.abort();
@@ -267,7 +268,7 @@ function nmfetch(url, options) {
267
268
  }
268
269
  } catch (err) {
269
270
  finished = true;
270
- err.type = 'FETCH';
271
+ err.code = errors.EFETCH;
271
272
  err.sourceUrl = url;
272
273
  fetchRes.emit('error', err);
273
274
  return;
@@ -6,6 +6,7 @@ const mimeTypes = require('../mime-funcs/mime-types');
6
6
  const MailComposer = require('../mail-composer');
7
7
  const DKIM = require('../dkim');
8
8
  const httpProxyClient = require('../smtp-connection/http-proxy-client');
9
+ const errors = require('../errors');
9
10
  const util = require('util');
10
11
  const urllib = require('url');
11
12
  const packageData = require('../../package.json');
@@ -337,7 +338,9 @@ class Mail extends EventEmitter {
337
338
  case 'socks4':
338
339
  case 'socks4a': {
339
340
  if (!this.meta.has('proxy_socks_module')) {
340
- return callback(new Error('Socks module not loaded'));
341
+ let err = new Error('Socks module not loaded');
342
+ err.code = errors.EPROXY;
343
+ return callback(err);
341
344
  }
342
345
  let connect = ipaddress => {
343
346
  let proxyV2 = !!this.meta.get('proxy_socks_module').SocksClient;
@@ -394,7 +397,9 @@ class Mail extends EventEmitter {
394
397
  });
395
398
  }
396
399
  }
397
- callback(new Error('Unknown proxy configuration'));
400
+ let err = new Error('Unknown proxy configuration');
401
+ err.code = errors.EPROXY;
402
+ callback(err);
398
403
  };
399
404
  }
400
405
 
@@ -13,6 +13,7 @@ const qp = require('../qp');
13
13
  const base64 = require('../base64');
14
14
  const addressparser = require('../addressparser');
15
15
  const nmfetch = require('../fetch');
16
+ const errors = require('../errors');
16
17
  const LastNewline = require('./last-newline');
17
18
 
18
19
  const LeWindows = require('./le-windows');
@@ -979,7 +980,11 @@ class MimeNode {
979
980
  } else if (content && typeof content.path === 'string' && !content.href) {
980
981
  if (this.disableFileAccess) {
981
982
  contentStream = new PassThrough();
982
- setImmediate(() => contentStream.emit('error', new Error('File access rejected for ' + content.path)));
983
+ setImmediate(() => {
984
+ let err = new Error('File access rejected for ' + content.path);
985
+ err.code = errors.EFILEACCESS;
986
+ contentStream.emit('error', err);
987
+ });
983
988
  return contentStream;
984
989
  }
985
990
  // read file
@@ -987,7 +992,11 @@ class MimeNode {
987
992
  } else if (content && typeof content.href === 'string') {
988
993
  if (this.disableUrlAccess) {
989
994
  contentStream = new PassThrough();
990
- setImmediate(() => contentStream.emit('error', new Error('Url access rejected for ' + content.href)));
995
+ setImmediate(() => {
996
+ let err = new Error('Url access rejected for ' + content.href);
997
+ err.code = errors.EURLACCESS;
998
+ contentStream.emit('error', err);
999
+ });
991
1000
  return contentStream;
992
1001
  }
993
1002
  // fetch URL
package/lib/nodemailer.js CHANGED
@@ -8,6 +8,7 @@ const SendmailTransport = require('./sendmail-transport');
8
8
  const StreamTransport = require('./stream-transport');
9
9
  const JSONTransport = require('./json-transport');
10
10
  const SESTransport = require('./ses-transport');
11
+ const errors = require('./errors');
11
12
  const nmfetch = require('./fetch');
12
13
  const packageData = require('../package.json');
13
14
 
@@ -49,7 +50,7 @@ module.exports.createTransport = function (transporter, defaults) {
49
50
  let error = new Error(
50
51
  'Using legacy SES configuration, expecting @aws-sdk/client-sesv2, see https://nodemailer.com/transports/ses/'
51
52
  );
52
- error.code = 'LegacyConfig';
53
+ error.code = errors.ECONFIG;
53
54
  throw error;
54
55
  }
55
56
  transporter = new SESTransport(options);
@@ -3,6 +3,7 @@
3
3
  const spawn = require('child_process').spawn;
4
4
  const packageData = require('../../package.json');
5
5
  const shared = require('../shared');
6
+ const errors = require('../errors');
6
7
 
7
8
  /**
8
9
  * Generates a Transport object for Sendmail
@@ -72,7 +73,9 @@ class SendmailTransport {
72
73
  .concat(envelope.to || [])
73
74
  .some(addr => /^-/.test(addr));
74
75
  if (hasInvalidAddresses) {
75
- return done(new Error('Can not send mail. Invalid envelope addresses.'));
76
+ let err = new Error('Can not send mail. Invalid envelope addresses.');
77
+ err.code = errors.ESENDMAIL;
78
+ return done(err);
76
79
  }
77
80
 
78
81
  if (this.args) {
@@ -141,6 +144,7 @@ class SendmailTransport {
141
144
  } else {
142
145
  err = new Error('Sendmail exited with code ' + code);
143
146
  }
147
+ err.code = errors.ESENDMAIL;
144
148
 
145
149
  this.logger.error(
146
150
  {
@@ -202,7 +206,9 @@ class SendmailTransport {
202
206
 
203
207
  sourceStream.pipe(sendmail.stdin);
204
208
  } else {
205
- return callback(new Error('sendmail was not found'));
209
+ let err = new Error('sendmail was not found');
210
+ err.code = errors.ESENDMAIL;
211
+ return callback(err);
206
212
  }
207
213
  }
208
214
  }
@@ -82,15 +82,22 @@ const formatDNSValue = (value, extra) => {
82
82
  return Object.assign({}, extra || {});
83
83
  }
84
84
 
85
+ let addresses = value.addresses || [];
86
+
87
+ // Select a random address from available addresses, or null if none
88
+ let host = null;
89
+ if (addresses.length === 1) {
90
+ host = addresses[0];
91
+ } else if (addresses.length > 1) {
92
+ host = addresses[Math.floor(Math.random() * addresses.length)];
93
+ }
94
+
85
95
  return Object.assign(
86
96
  {
87
97
  servername: value.servername,
88
- host:
89
- !value.addresses || !value.addresses.length
90
- ? null
91
- : value.addresses.length === 1
92
- ? value.addresses[0]
93
- : value.addresses[Math.floor(Math.random() * value.addresses.length)]
98
+ host,
99
+ // Include all addresses for connection fallback support
100
+ _addresses: addresses
94
101
  },
95
102
  extra || {}
96
103
  );
@@ -151,46 +158,51 @@ module.exports.resolveHostname = (options, callback) => {
151
158
  }
152
159
  }
153
160
 
161
+ // Resolve both IPv4 and IPv6 addresses for fallback support
162
+ let ipv4Addresses = [];
163
+ let ipv6Addresses = [];
164
+ let ipv4Error = null;
165
+ let ipv6Error = null;
166
+
154
167
  resolver(4, options.host, options, (err, addresses) => {
155
168
  if (err) {
156
- if (cached) {
169
+ ipv4Error = err;
170
+ } else {
171
+ ipv4Addresses = addresses || [];
172
+ }
173
+
174
+ resolver(6, options.host, options, (err, addresses) => {
175
+ if (err) {
176
+ ipv6Error = err;
177
+ } else {
178
+ ipv6Addresses = addresses || [];
179
+ }
180
+
181
+ // Combine addresses: IPv4 first, then IPv6
182
+ let allAddresses = ipv4Addresses.concat(ipv6Addresses);
183
+
184
+ if (allAddresses.length) {
185
+ let value = {
186
+ addresses: allAddresses,
187
+ servername: options.servername || options.host
188
+ };
189
+
157
190
  dnsCache.set(options.host, {
158
- value: cached.value,
191
+ value,
159
192
  expires: Date.now() + (options.dnsTtl || DNS_TTL)
160
193
  });
161
194
 
162
195
  return callback(
163
196
  null,
164
- formatDNSValue(cached.value, {
165
- cached: true,
166
- error: err
197
+ formatDNSValue(value, {
198
+ cached: false
167
199
  })
168
200
  );
169
201
  }
170
- return callback(err);
171
- }
172
-
173
- if (addresses && addresses.length) {
174
- let value = {
175
- addresses,
176
- servername: options.servername || options.host
177
- };
178
202
 
179
- dnsCache.set(options.host, {
180
- value,
181
- expires: Date.now() + (options.dnsTtl || DNS_TTL)
182
- });
183
-
184
- return callback(
185
- null,
186
- formatDNSValue(value, {
187
- cached: false
188
- })
189
- );
190
- }
191
-
192
- resolver(6, options.host, options, (err, addresses) => {
193
- if (err) {
203
+ // No addresses from resolve4/resolve6, try dns.lookup as fallback
204
+ if (ipv4Error && ipv6Error) {
205
+ // Both resolvers had errors
194
206
  if (cached) {
195
207
  dnsCache.set(options.host, {
196
208
  value: cached.value,
@@ -201,30 +213,10 @@ module.exports.resolveHostname = (options, callback) => {
201
213
  null,
202
214
  formatDNSValue(cached.value, {
203
215
  cached: true,
204
- error: err
216
+ error: ipv4Error
205
217
  })
206
218
  );
207
219
  }
208
- return callback(err);
209
- }
210
-
211
- if (addresses && addresses.length) {
212
- let value = {
213
- addresses,
214
- servername: options.servername || options.host
215
- };
216
-
217
- dnsCache.set(options.host, {
218
- value,
219
- expires: Date.now() + (options.dnsTtl || DNS_TTL)
220
- });
221
-
222
- return callback(
223
- null,
224
- formatDNSValue(value, {
225
- cached: false
226
- })
227
- );
228
220
  }
229
221
 
230
222
  try {
@@ -247,19 +239,17 @@ module.exports.resolveHostname = (options, callback) => {
247
239
  return callback(err);
248
240
  }
249
241
 
250
- let address = addresses
251
- ? addresses
252
- .filter(addr => isFamilySupported(addr.family))
253
- .map(addr => addr.address)
254
- .shift()
255
- : false;
242
+ // Get all supported addresses from dns.lookup
243
+ let supportedAddresses = addresses
244
+ ? addresses.filter(addr => isFamilySupported(addr.family)).map(addr => addr.address)
245
+ : [];
256
246
 
257
- if (addresses && addresses.length && !address) {
247
+ if (addresses && addresses.length && !supportedAddresses.length) {
258
248
  // there are addresses but none can be used
259
249
  console.warn(`Failed to resolve IPv${addresses[0].family} addresses with current network`);
260
250
  }
261
251
 
262
- if (!address && cached) {
252
+ if (!supportedAddresses.length && cached) {
263
253
  // nothing was found, fallback to cached value
264
254
  return callback(
265
255
  null,
@@ -270,7 +260,7 @@ module.exports.resolveHostname = (options, callback) => {
270
260
  }
271
261
 
272
262
  let value = {
273
- addresses: address ? [address] : [options.host],
263
+ addresses: supportedAddresses.length ? supportedAddresses : [options.host],
274
264
  servername: options.servername || options.host
275
265
  };
276
266
 
@@ -286,7 +276,7 @@ module.exports.resolveHostname = (options, callback) => {
286
276
  })
287
277
  );
288
278
  });
289
- } catch (_err) {
279
+ } catch (lookupErr) {
290
280
  if (cached) {
291
281
  dnsCache.set(options.host, {
292
282
  value: cached.value,
@@ -297,11 +287,11 @@ module.exports.resolveHostname = (options, callback) => {
297
287
  null,
298
288
  formatDNSValue(cached.value, {
299
289
  cached: true,
300
- error: err
290
+ error: lookupErr
301
291
  })
302
292
  );
303
293
  }
304
- return callback(err);
294
+ return callback(ipv4Error || ipv6Error || lookupErr);
305
295
  }
306
296
  });
307
297
  });
@@ -7,6 +7,7 @@
7
7
  const net = require('net');
8
8
  const tls = require('tls');
9
9
  const urllib = require('url');
10
+ const errors = require('../errors');
10
11
 
11
12
  /**
12
13
  * Establishes proxied connection to destinationPort
@@ -121,7 +122,9 @@ function httpProxyClient(proxyUrl, destinationPort, destinationHost, callback) {
121
122
  } catch (_E) {
122
123
  // ignore
123
124
  }
124
- return callback(new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || '')));
125
+ let err = new Error('Invalid response from proxy' + ((match && ': ' + match[1]) || ''));
126
+ err.code = errors.EPROXY;
127
+ return callback(err);
125
128
  }
126
129
 
127
130
  socket.removeListener('error', tempSocketErr);
@@ -15,6 +15,7 @@ const CONNECTION_TIMEOUT = 2 * 60 * 1000; // how much to wait for the connection
15
15
  const SOCKET_TIMEOUT = 10 * 60 * 1000; // how much to wait for socket inactivity before disconnecting the client
16
16
  const GREETING_TIMEOUT = 30 * 1000; // how much to wait after connection is established but SMTP greeting is not receieved
17
17
  const DNS_TIMEOUT = 30 * 1000; // how much to wait for resolveHostname
18
+ const TEARDOWN_NOOP = () => {}; // reusable no-op handler for absorbing errors during socket teardown
18
19
 
19
20
  /**
20
21
  * Generates a SMTP connection object
@@ -197,6 +198,17 @@ class SMTPConnection extends EventEmitter {
197
198
  this._onSocketClose = () => this._onClose();
198
199
  this._onSocketEnd = () => this._onEnd();
199
200
  this._onSocketTimeout = () => this._onTimeout();
201
+
202
+ /**
203
+ * Connection-phase error handler (supports fallback to alternative addresses)
204
+ */
205
+ this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET');
206
+
207
+ /**
208
+ * Connection attempt counter for fallback race condition protection
209
+ * @private
210
+ */
211
+ this._connectionAttemptId = 0;
200
212
  }
201
213
 
202
214
  /**
@@ -232,18 +244,10 @@ class SMTPConnection extends EventEmitter {
232
244
  opts.localAddress = this.options.localAddress;
233
245
  }
234
246
 
235
- let setupConnectionHandlers = () => {
236
- this._connectionTimeout = setTimeout(() => {
237
- this._onError('Connection timeout', 'ETIMEDOUT', false, 'CONN');
238
- }, this.options.connectionTimeout || CONNECTION_TIMEOUT);
239
-
240
- this._socket.on('error', this._onSocketError);
241
- };
242
-
243
247
  if (this.options.connection) {
244
248
  // connection is already opened
245
249
  this._socket = this.options.connection;
246
- setupConnectionHandlers();
250
+ this._setupConnectionHandlers();
247
251
 
248
252
  if (this.secureConnection && !this.alreadySecured) {
249
253
  setImmediate(() =>
@@ -288,7 +292,7 @@ class SMTPConnection extends EventEmitter {
288
292
  this._socket.setKeepAlive(true);
289
293
  this._onConnect();
290
294
  });
291
- setupConnectionHandlers();
295
+ this._setupConnectionHandlers();
292
296
  } catch (E) {
293
297
  return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
294
298
  }
@@ -327,15 +331,12 @@ class SMTPConnection extends EventEmitter {
327
331
  opts[key] = resolved[key];
328
332
  }
329
333
  });
330
- try {
331
- this._socket = tls.connect(opts, () => {
332
- this._socket.setKeepAlive(true);
333
- this._onConnect();
334
- });
335
- setupConnectionHandlers();
336
- } catch (E) {
337
- return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
338
- }
334
+
335
+ // Store fallback addresses for retry on connection failure
336
+ this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
337
+ this._connectOpts = Object.assign({}, opts);
338
+
339
+ this._connectToHost(opts, true);
339
340
  });
340
341
  } else {
341
342
  // connect using plaintext
@@ -360,19 +361,101 @@ class SMTPConnection extends EventEmitter {
360
361
  opts[key] = resolved[key];
361
362
  }
362
363
  });
363
- try {
364
- this._socket = net.connect(opts, () => {
365
- this._socket.setKeepAlive(true);
366
- this._onConnect();
367
- });
368
- setupConnectionHandlers();
369
- } catch (E) {
370
- return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
364
+
365
+ // Store fallback addresses for retry on connection failure
366
+ this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
367
+ this._connectOpts = Object.assign({}, opts);
368
+
369
+ this._connectToHost(opts, false);
370
+ });
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Attempts to connect to the specified host address
376
+ *
377
+ * @param {Object} opts Connection options
378
+ * @param {Boolean} secure Whether to use TLS
379
+ */
380
+ _connectToHost(opts, secure) {
381
+ this._connectionAttemptId++;
382
+ const currentAttemptId = this._connectionAttemptId;
383
+
384
+ let connectFn = secure ? tls.connect : net.connect;
385
+ try {
386
+ this._socket = connectFn(opts, () => {
387
+ // Ignore callback if this is a stale connection attempt
388
+ if (this._connectionAttemptId !== currentAttemptId) {
389
+ return;
371
390
  }
391
+ this._socket.setKeepAlive(true);
392
+ this._onConnect();
372
393
  });
394
+ this._setupConnectionHandlers();
395
+ } catch (E) {
396
+ return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
373
397
  }
374
398
  }
375
399
 
400
+ /**
401
+ * Sets up connection timeout and error handlers
402
+ */
403
+ _setupConnectionHandlers() {
404
+ this._connectionTimeout = setTimeout(() => {
405
+ this._onConnectionError('Connection timeout', 'ETIMEDOUT');
406
+ }, this.options.connectionTimeout || CONNECTION_TIMEOUT);
407
+
408
+ this._socket.on('error', this._onConnectionSocketError);
409
+ }
410
+
411
+ /**
412
+ * Handles connection errors with fallback to alternative addresses
413
+ *
414
+ * @param {Error|String} err Error object or message
415
+ * @param {String} code Error code
416
+ */
417
+ _onConnectionError(err, code) {
418
+ clearTimeout(this._connectionTimeout);
419
+
420
+ // Check if we have fallback addresses to try
421
+ let canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
422
+
423
+ if (!canFallback) {
424
+ // No more fallback addresses, report the error
425
+ this._onError(err, code, false, 'CONN');
426
+ return;
427
+ }
428
+
429
+ let nextHost = this._fallbackAddresses.shift();
430
+
431
+ this.logger.info(
432
+ {
433
+ tnx: 'network',
434
+ failedHost: this._connectOpts.host,
435
+ nextHost,
436
+ error: err.message || err
437
+ },
438
+ 'Connection to %s failed, trying %s',
439
+ this._connectOpts.host,
440
+ nextHost
441
+ );
442
+
443
+ // Clean up current socket
444
+ if (this._socket) {
445
+ try {
446
+ this._socket.removeListener('error', this._onConnectionSocketError);
447
+ this._socket.destroy();
448
+ } catch (_E) {
449
+ // ignore
450
+ }
451
+ this._socket = null;
452
+ }
453
+
454
+ // Update host and retry
455
+ this._connectOpts.host = nextHost;
456
+ this._connectToHost(this._connectOpts, this.secureConnection);
457
+ }
458
+
376
459
  /**
377
460
  * Sends QUIT
378
461
  */
@@ -414,6 +497,18 @@ class SMTPConnection extends EventEmitter {
414
497
 
415
498
  if (socket && !socket.destroyed) {
416
499
  try {
500
+ // Clear socket timeout to prevent timer leaks
501
+ socket.setTimeout(0);
502
+ // Remove all listeners to allow proper garbage collection
503
+ socket.removeListener('data', this._onSocketData);
504
+ socket.removeListener('timeout', this._onSocketTimeout);
505
+ socket.removeListener('close', this._onSocketClose);
506
+ socket.removeListener('end', this._onSocketEnd);
507
+ socket.removeListener('error', this._onSocketError);
508
+ socket.removeListener('error', this._onConnectionSocketError);
509
+ // Absorb errors that may fire during socket teardown (e.g. server
510
+ // sending cleartext after TLS shutdown triggers ERR_SSL_BAD_RECORD_TYPE)
511
+ socket.on('error', TEARDOWN_NOOP);
417
512
  socket[closeMethod]();
418
513
  } catch (_E) {
419
514
  // just ignore
@@ -715,7 +810,10 @@ class SMTPConnection extends EventEmitter {
715
810
  this._socket.removeListener('timeout', this._onSocketTimeout);
716
811
  this._socket.removeListener('close', this._onSocketClose);
717
812
  this._socket.removeListener('end', this._onSocketEnd);
813
+ // Switch from connection-phase error handler to normal error handler
814
+ this._socket.removeListener('error', this._onConnectionSocketError);
718
815
 
816
+ this._socket.on('error', this._onSocketError);
719
817
  this._socket.on('data', this._onSocketData);
720
818
  this._socket.once('close', this._onSocketClose);
721
819
  this._socket.once('end', this._onSocketEnd);
@@ -941,8 +1039,10 @@ class SMTPConnection extends EventEmitter {
941
1039
  this.upgrading = false;
942
1040
  this._socket.on('data', this._onSocketData);
943
1041
 
1042
+ // Remove all listeners from the plain socket to allow proper garbage collection
944
1043
  socketPlain.removeListener('close', this._onSocketClose);
945
1044
  socketPlain.removeListener('end', this._onSocketEnd);
1045
+ socketPlain.removeListener('error', this._onSocketError);
946
1046
 
947
1047
  return callback(null, true);
948
1048
  });
@@ -5,6 +5,7 @@ const PoolResource = require('./pool-resource');
5
5
  const SMTPConnection = require('../smtp-connection');
6
6
  const wellKnown = require('../well-known');
7
7
  const shared = require('../shared');
8
+ const errors = require('../errors');
8
9
  const packageData = require('../../package.json');
9
10
 
10
11
  /**
@@ -633,7 +634,7 @@ class SMTPPool extends EventEmitter {
633
634
  });
634
635
  } else if (!auth && connection.allowsAuth && options.forceAuth) {
635
636
  let err = new Error('Authentication info was not provided');
636
- err.code = 'NoAuth';
637
+ err.code = errors.ENOAUTH;
637
638
 
638
639
  returned = true;
639
640
  connection.close();
@@ -3,6 +3,7 @@
3
3
  const SMTPConnection = require('../smtp-connection');
4
4
  const assign = require('../shared').assign;
5
5
  const XOAuth2 = require('../xoauth2');
6
+ const errors = require('../errors');
6
7
  const EventEmitter = require('events');
7
8
 
8
9
  /**
@@ -121,7 +122,7 @@ class PoolResource extends EventEmitter {
121
122
  let err = new Error('Unexpected socket close');
122
123
  if (this.connection && this.connection._socket && this.connection._socket.upgrading) {
123
124
  // starttls connection errors
124
- err.code = 'ETLS';
125
+ err.code = errors.ETLS;
125
126
  }
126
127
  callback(err);
127
128
  }, 1000);
@@ -226,7 +227,7 @@ class PoolResource extends EventEmitter {
226
227
  let err;
227
228
  if (this.messages >= this.options.maxMessages) {
228
229
  err = new Error('Resource exhausted');
229
- err.code = 'EMAXLIMIT';
230
+ err.code = errors.EMAXLIMIT;
230
231
  this.connection.close();
231
232
  this.emit('error', err);
232
233
  } else {
@@ -5,6 +5,7 @@ const SMTPConnection = require('../smtp-connection');
5
5
  const wellKnown = require('../well-known');
6
6
  const shared = require('../shared');
7
7
  const XOAuth2 = require('../xoauth2');
8
+ const errors = require('../errors');
8
9
  const packageData = require('../../package.json');
9
10
 
10
11
  /**
@@ -190,7 +191,7 @@ class SMTPTransport extends EventEmitter {
190
191
  let err = new Error('Unexpected socket close');
191
192
  if (connection && connection._socket && connection._socket.upgrading) {
192
193
  // starttls connection errors
193
- err.code = 'ETLS';
194
+ err.code = errors.ETLS;
194
195
  }
195
196
  callback(err);
196
197
  }, 1000);
@@ -392,7 +393,7 @@ class SMTPTransport extends EventEmitter {
392
393
  });
393
394
  } else if (!authData && connection.allowsAuth && options.forceAuth) {
394
395
  let err = new Error('Authentication info was not provided');
395
- err.code = 'NoAuth';
396
+ err.code = errors.ENOAUTH;
396
397
 
397
398
  returned = true;
398
399
  connection.close();
@@ -147,6 +147,14 @@
147
147
  "secure": true
148
148
  },
149
149
 
150
+ "GmailWorkspace": {
151
+ "description": "Gmail Workspace",
152
+ "aliases": ["Google Workspace Mail"],
153
+ "host": "smtp-relay.gmail.com",
154
+ "port": 465,
155
+ "secure": true
156
+ },
157
+
150
158
  "GMX": {
151
159
  "description": "GMX Mail",
152
160
  "domains": ["gmx.com", "gmx.net", "gmx.de"],
@@ -4,6 +4,7 @@ const Stream = require('stream').Stream;
4
4
  const nmfetch = require('../fetch');
5
5
  const crypto = require('crypto');
6
6
  const shared = require('../shared');
7
+ const errors = require('../errors');
7
8
 
8
9
  /**
9
10
  * XOAUTH2 access_token generator for Gmail.
@@ -41,7 +42,9 @@ class XOAuth2 extends Stream {
41
42
 
42
43
  if (options && options.serviceClient) {
43
44
  if (!options.privateKey || !options.user) {
44
- setImmediate(() => this.emit('error', new Error('Options "privateKey" and "user" are required for service account!')));
45
+ let err = new Error('Options "privateKey" and "user" are required for service account!');
46
+ err.code = errors.EOAUTH2;
47
+ setImmediate(() => this.emit('error', err));
45
48
  return;
46
49
  }
47
50
 
@@ -120,7 +123,9 @@ class XOAuth2 extends Stream {
120
123
  'Cannot renew access token for %s: No refresh mechanism available',
121
124
  this.options.user
122
125
  );
123
- return callback(new Error("Can't create new access token for user"));
126
+ let err = new Error("Can't create new access token for user");
127
+ err.code = errors.EOAUTH2;
128
+ return callback(err);
124
129
  }
125
130
 
126
131
  // If renewal already in progress, queue this request instead of starting another
@@ -218,7 +223,9 @@ class XOAuth2 extends Stream {
218
223
  try {
219
224
  token = this.jwtSignRS256(tokenData);
220
225
  } catch (_err) {
221
- return callback(new Error("Can't generate token. Check your auth options"));
226
+ let err = new Error("Can't generate token. Check your auth options");
227
+ err.code = errors.EOAUTH2;
228
+ return callback(err);
222
229
  }
223
230
 
224
231
  urlOptions = {
@@ -232,7 +239,9 @@ class XOAuth2 extends Stream {
232
239
  };
233
240
  } else {
234
241
  if (!this.options.refreshToken) {
235
- return callback(new Error("Can't create new access token for user"));
242
+ let err = new Error("Can't create new access token for user");
243
+ err.code = errors.EOAUTH2;
244
+ return callback(err);
236
245
  }
237
246
 
238
247
  // web app - https://developers.google.com/identity/protocols/OAuth2WebServer
@@ -289,7 +298,9 @@ class XOAuth2 extends Stream {
289
298
  'Response: %s',
290
299
  (body || '').toString()
291
300
  );
292
- return callback(new Error('Invalid authentication response'));
301
+ let err = new Error('Invalid authentication response');
302
+ err.code = errors.EOAUTH2;
303
+ return callback(err);
293
304
  }
294
305
 
295
306
  let logData = {};
@@ -320,7 +331,9 @@ class XOAuth2 extends Stream {
320
331
  if (data.error_uri) {
321
332
  errorMessage += ' (' + data.error_uri + ')';
322
333
  }
323
- return callback(new Error(errorMessage));
334
+ let err = new Error(errorMessage);
335
+ err.code = errors.EOAUTH2;
336
+ return callback(err);
324
337
  }
325
338
 
326
339
  if (data.access_token) {
@@ -328,7 +341,9 @@ class XOAuth2 extends Stream {
328
341
  return callback(null, this.accessToken);
329
342
  }
330
343
 
331
- return callback(new Error('No access token'));
344
+ let err = new Error('No access token');
345
+ err.code = errors.EOAUTH2;
346
+ return callback(err);
332
347
  });
333
348
  }
334
349
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "7.0.13",
3
+ "version": "8.0.1",
4
4
  "description": "Easy as cake e-mail sending from your Node.js applications",
5
5
  "main": "lib/nodemailer.js",
6
6
  "scripts": {
@@ -26,12 +26,12 @@
26
26
  },
27
27
  "homepage": "https://nodemailer.com/",
28
28
  "devDependencies": {
29
- "@aws-sdk/client-sesv2": "3.975.0",
29
+ "@aws-sdk/client-sesv2": "3.985.0",
30
30
  "bunyan": "1.8.15",
31
31
  "c8": "10.1.3",
32
- "eslint": "9.39.2",
32
+ "eslint": "10.0.0",
33
33
  "eslint-config-prettier": "10.1.8",
34
- "globals": "17.1.0",
34
+ "globals": "17.3.0",
35
35
  "libbase64": "1.3.0",
36
36
  "libmime": "5.3.7",
37
37
  "libqp": "2.1.1",
@@ -39,7 +39,7 @@
39
39
  "prettier": "3.8.1",
40
40
  "proxy": "1.0.2",
41
41
  "proxy-test-server": "1.0.0",
42
- "smtp-server": "3.18.0"
42
+ "smtp-server": "3.18.1"
43
43
  },
44
44
  "engines": {
45
45
  "node": ">=6.0.0"