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.
- package/CHANGELOG.md +15 -0
- package/dist/cjs/addressparser/index.js +97 -0
- package/dist/cjs/dkim/index.js +25 -8
- package/dist/cjs/dkim/sign.d.ts +2 -0
- package/dist/cjs/dkim/sign.js +17 -1
- package/dist/cjs/fetch/index.d.ts +4 -1
- package/dist/cjs/fetch/index.js +34 -59
- package/dist/cjs/package-info.d.ts +1 -1
- package/dist/cjs/package-info.js +1 -1
- package/dist/cjs/smtp-connection/data-stream.d.ts +2 -1
- package/dist/cjs/smtp-connection/data-stream.js +32 -24
- package/dist/cjs/smtp-connection/index.d.ts +5 -5
- package/dist/cjs/smtp-connection/index.js +78 -24
- package/dist/cjs/smtp-pool/index.d.ts +2 -1
- package/dist/cjs/smtp-pool/index.js +31 -5
- package/dist/cjs/smtp-pool/pool-resource.js +27 -29
- package/dist/cjs/smtp-transport/index.js +17 -27
- package/dist/cjs/xoauth2/index.d.ts +2 -0
- package/dist/cjs/xoauth2/index.js +11 -3
- package/dist/esm/addressparser/index.js +97 -0
- package/dist/esm/dkim/index.js +25 -8
- package/dist/esm/dkim/sign.d.ts +2 -0
- package/dist/esm/dkim/sign.js +17 -1
- package/dist/esm/fetch/index.d.ts +4 -1
- package/dist/esm/fetch/index.js +34 -59
- package/dist/esm/package-info.d.ts +1 -1
- package/dist/esm/package-info.js +1 -1
- package/dist/esm/smtp-connection/data-stream.d.ts +2 -1
- package/dist/esm/smtp-connection/data-stream.js +32 -24
- package/dist/esm/smtp-connection/index.d.ts +5 -5
- package/dist/esm/smtp-connection/index.js +78 -24
- package/dist/esm/smtp-pool/index.d.ts +2 -1
- package/dist/esm/smtp-pool/index.js +31 -5
- package/dist/esm/smtp-pool/pool-resource.js +27 -29
- package/dist/esm/smtp-transport/index.js +17 -27
- package/dist/esm/xoauth2/index.d.ts +2 -0
- package/dist/esm/xoauth2/index.js +11 -3
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
84
|
-
this.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
162
|
-
"libmime": "5.4.
|
|
163
|
-
"libqp": "2.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.
|
|
168
|
+
"smtp-server": "3.19.15",
|
|
169
169
|
"tsx": "4.23.15",
|
|
170
170
|
"typescript": "6.0.3",
|
|
171
|
-
"typescript-eslint": "8.
|
|
171
|
+
"typescript-eslint": "8.71.0"
|
|
172
172
|
},
|
|
173
173
|
"engines": {
|
|
174
174
|
"node": ">=20.0.0"
|