nodemailer 7.0.13 → 8.0.0

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,19 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## [8.0.0](https://github.com/nodemailer/nodemailer/compare/v7.0.13...v8.0.0) (2026-02-04)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * Error code 'NoAuth' renamed to 'ENOAUTH'
9
+
10
+ ### Bug Fixes
11
+
12
+ * add connection fallback to alternative DNS addresses ([e726d6f](https://github.com/nodemailer/nodemailer/commit/e726d6f44aa7ca14e943d4303243cb5494b09c75))
13
+ * centralize and standardize error codes ([45062ce](https://github.com/nodemailer/nodemailer/commit/45062ce7a4705f3e63c5d9e606547f4d99fd29b5))
14
+ * harden DNS fallback against race conditions and cleanup issues ([4fa3c63](https://github.com/nodemailer/nodemailer/commit/4fa3c63a1f36aefdbaea7f57a133adc458413a47))
15
+ * improve socket cleanup to prevent potential memory leaks ([6069fdc](https://github.com/nodemailer/nodemailer/commit/6069fdcff68a3eef9a9bb16b2bf5ddb924c02091))
16
+
3
17
  ## [7.0.13](https://github.com/nodemailer/nodemailer/compare/v7.0.12...v7.0.13) (2026-01-27)
4
18
 
5
19
 
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);
@@ -197,6 +197,17 @@ class SMTPConnection extends EventEmitter {
197
197
  this._onSocketClose = () => this._onClose();
198
198
  this._onSocketEnd = () => this._onEnd();
199
199
  this._onSocketTimeout = () => this._onTimeout();
200
+
201
+ /**
202
+ * Connection-phase error handler (supports fallback to alternative addresses)
203
+ */
204
+ this._onConnectionSocketError = err => this._onConnectionError(err, 'ESOCKET');
205
+
206
+ /**
207
+ * Connection attempt counter for fallback race condition protection
208
+ * @private
209
+ */
210
+ this._connectionAttemptId = 0;
200
211
  }
201
212
 
202
213
  /**
@@ -232,18 +243,10 @@ class SMTPConnection extends EventEmitter {
232
243
  opts.localAddress = this.options.localAddress;
233
244
  }
234
245
 
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
246
  if (this.options.connection) {
244
247
  // connection is already opened
245
248
  this._socket = this.options.connection;
246
- setupConnectionHandlers();
249
+ this._setupConnectionHandlers();
247
250
 
248
251
  if (this.secureConnection && !this.alreadySecured) {
249
252
  setImmediate(() =>
@@ -288,7 +291,7 @@ class SMTPConnection extends EventEmitter {
288
291
  this._socket.setKeepAlive(true);
289
292
  this._onConnect();
290
293
  });
291
- setupConnectionHandlers();
294
+ this._setupConnectionHandlers();
292
295
  } catch (E) {
293
296
  return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
294
297
  }
@@ -327,15 +330,12 @@ class SMTPConnection extends EventEmitter {
327
330
  opts[key] = resolved[key];
328
331
  }
329
332
  });
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
- }
333
+
334
+ // Store fallback addresses for retry on connection failure
335
+ this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
336
+ this._connectOpts = Object.assign({}, opts);
337
+
338
+ this._connectToHost(opts, true);
339
339
  });
340
340
  } else {
341
341
  // connect using plaintext
@@ -360,19 +360,101 @@ class SMTPConnection extends EventEmitter {
360
360
  opts[key] = resolved[key];
361
361
  }
362
362
  });
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'));
363
+
364
+ // Store fallback addresses for retry on connection failure
365
+ this._fallbackAddresses = (resolved._addresses || []).filter(addr => addr !== opts.host);
366
+ this._connectOpts = Object.assign({}, opts);
367
+
368
+ this._connectToHost(opts, false);
369
+ });
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Attempts to connect to the specified host address
375
+ *
376
+ * @param {Object} opts Connection options
377
+ * @param {Boolean} secure Whether to use TLS
378
+ */
379
+ _connectToHost(opts, secure) {
380
+ this._connectionAttemptId++;
381
+ const currentAttemptId = this._connectionAttemptId;
382
+
383
+ let connectFn = secure ? tls.connect : net.connect;
384
+ try {
385
+ this._socket = connectFn(opts, () => {
386
+ // Ignore callback if this is a stale connection attempt
387
+ if (this._connectionAttemptId !== currentAttemptId) {
388
+ return;
371
389
  }
390
+ this._socket.setKeepAlive(true);
391
+ this._onConnect();
372
392
  });
393
+ this._setupConnectionHandlers();
394
+ } catch (E) {
395
+ return setImmediate(() => this._onError(E, 'ECONNECTION', false, 'CONN'));
373
396
  }
374
397
  }
375
398
 
399
+ /**
400
+ * Sets up connection timeout and error handlers
401
+ */
402
+ _setupConnectionHandlers() {
403
+ this._connectionTimeout = setTimeout(() => {
404
+ this._onConnectionError('Connection timeout', 'ETIMEDOUT');
405
+ }, this.options.connectionTimeout || CONNECTION_TIMEOUT);
406
+
407
+ this._socket.on('error', this._onConnectionSocketError);
408
+ }
409
+
410
+ /**
411
+ * Handles connection errors with fallback to alternative addresses
412
+ *
413
+ * @param {Error|String} err Error object or message
414
+ * @param {String} code Error code
415
+ */
416
+ _onConnectionError(err, code) {
417
+ clearTimeout(this._connectionTimeout);
418
+
419
+ // Check if we have fallback addresses to try
420
+ let canFallback = this._fallbackAddresses && this._fallbackAddresses.length && this.stage === 'init' && !this._destroyed;
421
+
422
+ if (!canFallback) {
423
+ // No more fallback addresses, report the error
424
+ this._onError(err, code, false, 'CONN');
425
+ return;
426
+ }
427
+
428
+ let nextHost = this._fallbackAddresses.shift();
429
+
430
+ this.logger.info(
431
+ {
432
+ tnx: 'network',
433
+ failedHost: this._connectOpts.host,
434
+ nextHost,
435
+ error: err.message || err
436
+ },
437
+ 'Connection to %s failed, trying %s',
438
+ this._connectOpts.host,
439
+ nextHost
440
+ );
441
+
442
+ // Clean up current socket
443
+ if (this._socket) {
444
+ try {
445
+ this._socket.removeListener('error', this._onConnectionSocketError);
446
+ this._socket.destroy();
447
+ } catch (_E) {
448
+ // ignore
449
+ }
450
+ this._socket = null;
451
+ }
452
+
453
+ // Update host and retry
454
+ this._connectOpts.host = nextHost;
455
+ this._connectToHost(this._connectOpts, this.secureConnection);
456
+ }
457
+
376
458
  /**
377
459
  * Sends QUIT
378
460
  */
@@ -414,6 +496,15 @@ class SMTPConnection extends EventEmitter {
414
496
 
415
497
  if (socket && !socket.destroyed) {
416
498
  try {
499
+ // Clear socket timeout to prevent timer leaks
500
+ socket.setTimeout(0);
501
+ // Remove all listeners to allow proper garbage collection
502
+ socket.removeListener('data', this._onSocketData);
503
+ socket.removeListener('timeout', this._onSocketTimeout);
504
+ socket.removeListener('close', this._onSocketClose);
505
+ socket.removeListener('end', this._onSocketEnd);
506
+ socket.removeListener('error', this._onSocketError);
507
+ socket.removeListener('error', this._onConnectionSocketError);
417
508
  socket[closeMethod]();
418
509
  } catch (_E) {
419
510
  // just ignore
@@ -715,7 +806,10 @@ class SMTPConnection extends EventEmitter {
715
806
  this._socket.removeListener('timeout', this._onSocketTimeout);
716
807
  this._socket.removeListener('close', this._onSocketClose);
717
808
  this._socket.removeListener('end', this._onSocketEnd);
809
+ // Switch from connection-phase error handler to normal error handler
810
+ this._socket.removeListener('error', this._onConnectionSocketError);
718
811
 
812
+ this._socket.on('error', this._onSocketError);
719
813
  this._socket.on('data', this._onSocketData);
720
814
  this._socket.once('close', this._onSocketClose);
721
815
  this._socket.once('end', this._onSocketEnd);
@@ -941,8 +1035,10 @@ class SMTPConnection extends EventEmitter {
941
1035
  this.upgrading = false;
942
1036
  this._socket.on('data', this._onSocketData);
943
1037
 
1038
+ // Remove all listeners from the plain socket to allow proper garbage collection
944
1039
  socketPlain.removeListener('close', this._onSocketClose);
945
1040
  socketPlain.removeListener('end', this._onSocketEnd);
1041
+ socketPlain.removeListener('error', this._onSocketError);
946
1042
 
947
1043
  return callback(null, true);
948
1044
  });
@@ -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();
@@ -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.0",
4
4
  "description": "Easy as cake e-mail sending from your Node.js applications",
5
5
  "main": "lib/nodemailer.js",
6
6
  "scripts": {