nodemailer 10.0.11 → 10.0.13

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/addressparser/index.js +97 -0
  3. package/dist/cjs/dkim/index.js +25 -8
  4. package/dist/cjs/dkim/sign.d.ts +2 -0
  5. package/dist/cjs/dkim/sign.js +17 -1
  6. package/dist/cjs/fetch/index.d.ts +4 -1
  7. package/dist/cjs/fetch/index.js +34 -59
  8. package/dist/cjs/package-info.d.ts +1 -1
  9. package/dist/cjs/package-info.js +1 -1
  10. package/dist/cjs/smtp-connection/data-stream.d.ts +2 -1
  11. package/dist/cjs/smtp-connection/data-stream.js +32 -24
  12. package/dist/cjs/smtp-connection/index.d.ts +5 -5
  13. package/dist/cjs/smtp-connection/index.js +78 -24
  14. package/dist/cjs/smtp-pool/index.d.ts +2 -1
  15. package/dist/cjs/smtp-pool/index.js +31 -5
  16. package/dist/cjs/smtp-pool/pool-resource.js +27 -29
  17. package/dist/cjs/smtp-transport/index.js +17 -27
  18. package/dist/cjs/xoauth2/index.d.ts +2 -0
  19. package/dist/cjs/xoauth2/index.js +11 -3
  20. package/dist/esm/addressparser/index.js +97 -0
  21. package/dist/esm/dkim/index.js +25 -8
  22. package/dist/esm/dkim/sign.d.ts +2 -0
  23. package/dist/esm/dkim/sign.js +17 -1
  24. package/dist/esm/fetch/index.d.ts +4 -1
  25. package/dist/esm/fetch/index.js +34 -59
  26. package/dist/esm/package-info.d.ts +1 -1
  27. package/dist/esm/package-info.js +1 -1
  28. package/dist/esm/smtp-connection/data-stream.d.ts +2 -1
  29. package/dist/esm/smtp-connection/data-stream.js +32 -24
  30. package/dist/esm/smtp-connection/index.d.ts +5 -5
  31. package/dist/esm/smtp-connection/index.js +78 -24
  32. package/dist/esm/smtp-pool/index.d.ts +2 -1
  33. package/dist/esm/smtp-pool/index.js +31 -5
  34. package/dist/esm/smtp-pool/pool-resource.js +27 -29
  35. package/dist/esm/smtp-transport/index.js +17 -27
  36. package/dist/esm/xoauth2/index.d.ts +2 -0
  37. package/dist/esm/xoauth2/index.js +11 -3
  38. package/package.json +7 -7
@@ -5,6 +5,10 @@ import wellKnown from '../well-known/index.js';
5
5
  import * as shared from '../shared/index.js';
6
6
  import * as errors from '../errors.js';
7
7
  import * as packageData from '../package-info.js';
8
+ /** First delay before a requeued message is retried, doubled on every further requeue */
9
+ const REQUEUE_BASE_DELAY = 50;
10
+ /** Upper bound for the requeue delay */
11
+ const REQUEUE_MAX_DELAY = 2000;
8
12
  /**
9
13
  * Creates a SMTP pool transport object for Nodemailer
10
14
  *
@@ -36,6 +40,8 @@ class SMTPPool extends EventEmitter {
36
40
  );
37
41
  this.options.maxConnections = this.options.maxConnections || 5;
38
42
  this.options.maxMessages = this.options.maxMessages || 100;
43
+ // a default bound, a server that closes every connection before the greeting would otherwise be retried forever
44
+ this.options.maxRequeues = typeof this.options.maxRequeues === 'number' ? this.options.maxRequeues : 5;
39
45
  this.logger = shared.getLogger(this.options, {
40
46
  component: this.options.component || 'smtp-pool'
41
47
  });
@@ -79,6 +85,10 @@ class SMTPPool extends EventEmitter {
79
85
  */
80
86
  send(mail, callback) {
81
87
  if (this._closed) {
88
+ // Mail.sendMail ignores the return value, so without a callback its promise would never settle
89
+ const err = new Error('Connection pool was closed');
90
+ err.code = errors.ECONNECTION;
91
+ setImmediate(() => callback(err));
82
92
  return false;
83
93
  }
84
94
  this._queue.push({
@@ -292,15 +302,22 @@ class SMTPPool extends EventEmitter {
292
302
  // Note that we must wait a bit.. because the callback of the 'error' handler might be called
293
303
  // in the next event loop
294
304
  setTimeout(() => {
305
+ let delay = 0;
295
306
  if (connection.queueEntry) {
296
307
  if (this._shouldRequeuOnConnectionClose(connection.queueEntry)) {
297
- this._requeueEntryOnConnectionClose(connection);
308
+ delay = this._requeueEntryOnConnectionClose(connection);
298
309
  }
299
310
  else {
300
311
  this._failDeliveryOnConnectionClose(connection);
301
312
  }
302
313
  }
303
- this._continueProcessing();
314
+ if (delay) {
315
+ // back off, a server that keeps dropping connections is not hammered
316
+ setTimeout(() => this._continueProcessing(), delay);
317
+ }
318
+ else {
319
+ this._continueProcessing();
320
+ }
304
321
  }, 50);
305
322
  }
306
323
  else {
@@ -315,7 +332,7 @@ class SMTPPool extends EventEmitter {
315
332
  }
316
333
  /** @internal */
317
334
  _shouldRequeuOnConnectionClose(queueEntry) {
318
- if (this.options.maxRequeues === undefined || this.options.maxRequeues < 0) {
335
+ if (this.options.maxRequeues < 0) {
319
336
  return true;
320
337
  }
321
338
  return queueEntry.requeueAttempts < this.options.maxRequeues;
@@ -324,7 +341,9 @@ class SMTPPool extends EventEmitter {
324
341
  _failDeliveryOnConnectionClose(connection) {
325
342
  if (connection.queueEntry && connection.queueEntry.callback) {
326
343
  try {
327
- connection.queueEntry.callback(new Error('Reached maximum number of retries after connection was closed'));
344
+ const err = new Error('Reached maximum number of retries after connection was closed');
345
+ err.code = errors.ECONNECTION;
346
+ connection.queueEntry.callback(err);
328
347
  }
329
348
  catch (E) {
330
349
  this.logger.error({
@@ -339,6 +358,7 @@ class SMTPPool extends EventEmitter {
339
358
  }
340
359
  /** @internal */
341
360
  _requeueEntryOnConnectionClose(connection) {
361
+ const delay = Math.min(REQUEUE_BASE_DELAY * 2 ** connection.queueEntry.requeueAttempts, REQUEUE_MAX_DELAY);
342
362
  connection.queueEntry.requeueAttempts += 1;
343
363
  this.logger.debug({
344
364
  tnx: 'pool',
@@ -348,6 +368,7 @@ class SMTPPool extends EventEmitter {
348
368
  }, 'Re-queued message <%s> for #%s. Attempt: #%s', connection.queueEntry.messageId, connection.id, connection.queueEntry.requeueAttempts);
349
369
  this._queue.unshift(connection.queueEntry);
350
370
  connection.queueEntry = false;
371
+ return delay;
351
372
  }
352
373
  /**
353
374
  * Continue to process message if the pool hasn't closed
@@ -468,10 +489,15 @@ class SMTPPool extends EventEmitter {
468
489
  connection.quit();
469
490
  return done(null, true);
470
491
  };
471
- connection.connect(() => {
492
+ connection.connect(err => {
472
493
  if (returned) {
473
494
  return;
474
495
  }
496
+ if (err) {
497
+ returned = true;
498
+ connection.close();
499
+ return done(err);
500
+ }
475
501
  if (auth && (connection.allowsAuth || options.forceAuth)) {
476
502
  connection.login(auth, err => {
477
503
  if (returned) {
@@ -28,7 +28,7 @@ export default class PoolResource extends EventEmitter {
28
28
  method: 'XOAUTH2'
29
29
  };
30
30
  oauth2.on('token', (token) => this.pool.mailer.emit('token', token));
31
- oauth2.on('error', err => this.emit('error', err));
31
+ oauth2.on('error', err => this._fail(err));
32
32
  break;
33
33
  }
34
34
  default:
@@ -51,6 +51,19 @@ export default class PoolResource extends EventEmitter {
51
51
  this._connected = false;
52
52
  this.messages = 0;
53
53
  this.available = true;
54
+ this._failed = false;
55
+ }
56
+ /**
57
+ * Emits 'error' for the first failure only. A dead resource can report the same failure more
58
+ * than once (the connection error, then the send callback), the pool handles it once
59
+ * @internal
60
+ */
61
+ _fail(err) {
62
+ if (this._failed) {
63
+ return;
64
+ }
65
+ this._failed = true;
66
+ this.emit('error', err);
54
67
  }
55
68
  /**
56
69
  * Initiates a connection to the SMTP server
@@ -63,7 +76,7 @@ export default class PoolResource extends EventEmitter {
63
76
  // nothing was connected, so no 'close' event is coming that would free the
64
77
  // slot this resource holds in the pool, report the failure the way a failed
65
78
  // login does
66
- this.emit('error', err);
79
+ this._fail(err);
67
80
  return callback(err);
68
81
  }
69
82
  let returned = false;
@@ -80,8 +93,8 @@ export default class PoolResource extends EventEmitter {
80
93
  options = Object.assign(assign(false, options), socketOptions);
81
94
  }
82
95
  this.connection = new SMTPConnection(options);
83
- this.connection.once('error', err => {
84
- this.emit('error', err);
96
+ this.connection.on('error', err => {
97
+ this._fail(err);
85
98
  if (returned) {
86
99
  return;
87
100
  }
@@ -90,33 +103,18 @@ export default class PoolResource extends EventEmitter {
90
103
  });
91
104
  this.connection.once('end', () => {
92
105
  this.close();
93
- if (returned) {
94
- return;
95
- }
96
106
  returned = true;
97
- const timer = setTimeout(() => {
98
- if (returned) {
99
- return;
100
- }
101
- // still have not returned, this means we have an unexpected connection close
102
- const err = new Error('Unexpected socket close');
103
- if (this.connection && this.connection.upgrading) {
104
- // starttls connection errors
105
- err.code = errors.ETLS;
106
- }
107
- callback(err);
108
- }, 1000);
109
- try {
110
- timer.unref();
111
- }
112
- catch (_E) {
113
- // Ignore. Happens on envs with non-node timer implementation
114
- }
115
107
  });
116
- this.connection.connect(() => {
108
+ this.connection.connect(err => {
117
109
  if (returned) {
118
110
  return;
119
111
  }
112
+ if (err) {
113
+ // a close before the greeting, the 'end' that follows closes this resource
114
+ // and the pool requeues or fails the entry, bounded by maxRequeues
115
+ returned = true;
116
+ return;
117
+ }
120
118
  if (this.auth && (this.connection.allowsAuth || options.forceAuth)) {
121
119
  this.connection.login(this.auth, err => {
122
120
  if (returned) {
@@ -125,7 +123,7 @@ export default class PoolResource extends EventEmitter {
125
123
  returned = true;
126
124
  if (err) {
127
125
  this.connection.close();
128
- this.emit('error', err);
126
+ this._fail(err);
129
127
  return callback(err);
130
128
  }
131
129
  this._connected = true;
@@ -177,7 +175,7 @@ export default class PoolResource extends EventEmitter {
177
175
  this.messages++;
178
176
  if (err) {
179
177
  this.connection.close();
180
- this.emit('error', err);
178
+ this._fail(err);
181
179
  return callback(err);
182
180
  }
183
181
  info.envelope = {
@@ -190,7 +188,7 @@ export default class PoolResource extends EventEmitter {
190
188
  const err = new Error('Resource exhausted');
191
189
  err.code = errors.EMAXLIMIT;
192
190
  this.connection.close();
193
- this.emit('error', err);
191
+ this._fail(err);
194
192
  }
195
193
  else {
196
194
  this.pool._checkRateLimit(() => {
@@ -139,31 +139,6 @@ class SMTPTransport extends EventEmitter {
139
139
  connection.close();
140
140
  return callback(err);
141
141
  });
142
- connection.once('end', () => {
143
- if (returned) {
144
- return;
145
- }
146
- const timer = setTimeout(() => {
147
- if (returned) {
148
- return;
149
- }
150
- returned = true;
151
- cleanupPerCallAuth();
152
- // still have not returned, this means we have an unexpected connection close
153
- const err = new Error('Unexpected socket close');
154
- if (connection && connection.upgrading) {
155
- // starttls connection errors
156
- err.code = errors.ETLS;
157
- }
158
- callback(err);
159
- }, 1000);
160
- try {
161
- timer.unref();
162
- }
163
- catch (_E) {
164
- // Ignore. Happens on envs with non-node timer implementation
165
- }
166
- });
167
142
  const sendMessage = () => {
168
143
  const envelope = mail.message.getEnvelope();
169
144
  const messageId = mail.message.messageId();
@@ -183,6 +158,10 @@ class SMTPTransport extends EventEmitter {
183
158
  messageId
184
159
  }, 'Sending message %s to <%s>', messageId, recipients.join(', '));
185
160
  connection.send(envelope, mail.message.createReadStream(), (err, info) => {
161
+ if (returned) {
162
+ // the connection error handler has already reported this send
163
+ return;
164
+ }
186
165
  returned = true;
187
166
  cleanupPerCallAuth();
188
167
  connection.close();
@@ -209,10 +188,16 @@ class SMTPTransport extends EventEmitter {
209
188
  }
210
189
  });
211
190
  };
212
- connection.connect(() => {
191
+ connection.connect(err => {
213
192
  if (returned) {
214
193
  return;
215
194
  }
195
+ if (err) {
196
+ // the server closed the connection before the greeting
197
+ returned = true;
198
+ connection.close();
199
+ return callback(err);
200
+ }
216
201
  perCallAuth = this.getAuth(mail.data.auth);
217
202
  if (perCallAuth && (connection.allowsAuth || options.forceAuth)) {
218
203
  connection.login(perCallAuth, err => {
@@ -294,10 +279,15 @@ class SMTPTransport extends EventEmitter {
294
279
  connection.quit();
295
280
  return done(null, true);
296
281
  };
297
- connection.connect(() => {
282
+ connection.connect(err => {
298
283
  if (returned) {
299
284
  return;
300
285
  }
286
+ if (err) {
287
+ returned = true;
288
+ connection.close();
289
+ return done(err);
290
+ }
301
291
  perCallAuth = this.getAuth({});
302
292
  if (perCallAuth && (connection.allowsAuth || options.forceAuth)) {
303
293
  connection.login(perCallAuth, err => {
@@ -61,6 +61,8 @@ export interface XOAuth2Options {
61
61
  component?: string | undefined;
62
62
  /** Extra headers for the token request */
63
63
  customHeaders?: OutgoingHttpHeaders | undefined;
64
+ /** Timeout for the token request in milliseconds, defaults to 60000, 0 disables it */
65
+ requestTimeout?: number | undefined;
64
66
  /** Extra form fields for the token request */
65
67
  customParams?: {
66
68
  [key: string]: any;
@@ -36,12 +36,14 @@ class XOAuth2 extends Stream {
36
36
  constructor(options, logger) {
37
37
  super();
38
38
  this.options = options || {};
39
+ this.configError = false;
39
40
  if (options && options.serviceClient) {
40
41
  if (!options.privateKey || !options.user) {
42
+ // reported through getToken() rather than an 'error' event: the transports forward
43
+ // that event up to the Mail object, where nothing may be listening
41
44
  const err = new Error('Options "privateKey" and "user" are required for service account!');
42
45
  err.code = errors.EOAUTH2;
43
- setImmediate(() => this.emit('error', err));
44
- return;
46
+ this.configError = err;
45
47
  }
46
48
  const serviceRequestTimeout = Math.min(Math.max(Number(this.options.serviceRequestTimeout) || 0, 0), 3600);
47
49
  this.options.serviceRequestTimeout = serviceRequestTimeout || 5 * 60;
@@ -75,6 +77,9 @@ class XOAuth2 extends Stream {
75
77
  getToken(renew, callback) {
76
78
  // the error paths hand over the error alone
77
79
  const done = callback;
80
+ if (this.configError) {
81
+ return done(this.configError);
82
+ }
78
83
  if (!renew && this.accessToken && (!this.expires || this.expires > Date.now())) {
79
84
  this.logger.debug({
80
85
  tnx: 'OAUTH2',
@@ -311,7 +316,10 @@ class XOAuth2 extends Stream {
311
316
  method: 'post',
312
317
  headers: params.customHeaders,
313
318
  body: payload,
314
- allowErrorResponse: true
319
+ allowErrorResponse: true,
320
+ // unset falls back to the fetch default, a stalled token endpoint would otherwise keep
321
+ // `renewing` set and queue every later request
322
+ timeout: params.requestTimeout
315
323
  };
316
324
  // OAuth2 token endpoints are credential-bearing. src/fetch already
317
325
  // validates certs by default; pin rejectUnauthorized:true here so the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nodemailer",
3
- "version": "10.0.11",
3
+ "version": "10.0.13",
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",
@@ -151,24 +151,24 @@
151
151
  },
152
152
  "homepage": "https://nodemailer.com/",
153
153
  "devDependencies": {
154
- "@aws-sdk/client-sesv2": "3.1141.0",
154
+ "@aws-sdk/client-sesv2": "3.1143.0",
155
155
  "@types/node": "20.19.43",
156
156
  "bunyan": "1.8.15",
157
157
  "c8": "12.0.0",
158
158
  "eslint": "10.11.0",
159
159
  "eslint-config-prettier": "10.1.8",
160
160
  "globals": "17.12.0",
161
- "libbase64": "1.3.0",
162
- "libmime": "5.4.4",
163
- "libqp": "2.1.1",
161
+ "libbase64": "1.3.1",
162
+ "libmime": "5.4.6",
163
+ "libqp": "2.1.2",
164
164
  "mailauth": "7.1.0",
165
165
  "prettier": "3.9.9",
166
166
  "proxy": "1.0.2",
167
167
  "proxy-test-server": "1.0.0",
168
- "smtp-server": "3.19.13",
168
+ "smtp-server": "3.19.15",
169
169
  "tsx": "4.23.15",
170
170
  "typescript": "6.0.3",
171
- "typescript-eslint": "8.70.1"
171
+ "typescript-eslint": "8.71.0"
172
172
  },
173
173
  "engines": {
174
174
  "node": ">=20.0.0"