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
|
@@ -104,8 +104,8 @@ function isPartialLine(line) {
|
|
|
104
104
|
* * **port** - is the port to connect to (defaults to 587 or 465)
|
|
105
105
|
* * **host** - is the hostname or IP address to connect to (defaults to 'localhost')
|
|
106
106
|
* * **secure** - use SSL
|
|
107
|
-
* * **ignoreTLS** - ignore server support for STARTTLS
|
|
108
|
-
* * **requireTLS** - forces the client to use STARTTLS
|
|
107
|
+
* * **ignoreTLS** - ignore server support for STARTTLS (has no effect when requireTLS is set)
|
|
108
|
+
* * **requireTLS** - forces the client to use STARTTLS, takes precedence over ignoreTLS and opportunisticTLS
|
|
109
109
|
* * **name** - the name of the client server
|
|
110
110
|
* * **localAddress** - outbound address to bind to (see: http://nodejs.org/api/net.html#net_net_connect_options_connectionlistener)
|
|
111
111
|
* * **greetingTimeout** - Time to wait in ms until greeting message is received from the server (defaults to 30 seconds)
|
|
@@ -130,6 +130,11 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
130
130
|
this.id = node_crypto_1.default.randomBytes(8).toString('base64').replace(/\W/g, '');
|
|
131
131
|
this.stage = 'init';
|
|
132
132
|
this.options = options || {};
|
|
133
|
+
if (this.options.requireTLS && (this.options.ignoreTLS || this.options.opportunisticTLS)) {
|
|
134
|
+
// requireTLS wins, a contradictory configuration must not quietly fall back to plaintext.
|
|
135
|
+
// Copied so the caller's (possibly shared) options object is left as it was
|
|
136
|
+
this.options = Object.assign({}, this.options, { ignoreTLS: false, opportunisticTLS: false });
|
|
137
|
+
}
|
|
133
138
|
this.secureConnection = !!this.options.secure;
|
|
134
139
|
this.alreadySecured = !!this.options.secured;
|
|
135
140
|
this.port = Number(this.options.port) || (this.secureConnection ? 465 : 587);
|
|
@@ -173,6 +178,8 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
173
178
|
this._destroyed = false;
|
|
174
179
|
this._closing = false;
|
|
175
180
|
this._currentDataStream = false;
|
|
181
|
+
this._pendingSend = false;
|
|
182
|
+
this._connectCallback = false;
|
|
176
183
|
this._onSocketData = chunk => this._onData(chunk);
|
|
177
184
|
this._onSocketError = error => this._onError(error, 'ESOCKET', false, 'CONN');
|
|
178
185
|
this._onSocketClose = () => this._onClose();
|
|
@@ -187,7 +194,9 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
187
194
|
*/
|
|
188
195
|
connect(connectCallback) {
|
|
189
196
|
if (typeof connectCallback === 'function') {
|
|
197
|
+
this._connectCallback = connectCallback;
|
|
190
198
|
this.once('connect', () => {
|
|
199
|
+
this._connectCallback = false;
|
|
191
200
|
this.logger.debug({
|
|
192
201
|
tnx: 'smtp'
|
|
193
202
|
}, 'SMTP handshake finished');
|
|
@@ -419,6 +428,16 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
419
428
|
}
|
|
420
429
|
this._currentDataStream = false;
|
|
421
430
|
}
|
|
431
|
+
// Detach from the message stream as well. The listener is swapped for a no-op rather than
|
|
432
|
+
// removed, a stream destroyed with an error later on would otherwise throw it as unhandled
|
|
433
|
+
if (this._pendingSend) {
|
|
434
|
+
const { stream, onStreamError } = this._pendingSend;
|
|
435
|
+
if (stream) {
|
|
436
|
+
stream.removeListener('error', onStreamError);
|
|
437
|
+
stream.on('error', TEARDOWN_NOOP);
|
|
438
|
+
}
|
|
439
|
+
this._pendingSend = false;
|
|
440
|
+
}
|
|
422
441
|
if (socket && !socket.destroyed) {
|
|
423
442
|
try {
|
|
424
443
|
// Clear socket timeout to prevent timer leaks
|
|
@@ -606,6 +625,9 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
606
625
|
return;
|
|
607
626
|
}
|
|
608
627
|
returned = true;
|
|
628
|
+
if (this._pendingSend && this._pendingSend.callback === callback) {
|
|
629
|
+
this._pendingSend = false;
|
|
630
|
+
}
|
|
609
631
|
done(err, info);
|
|
610
632
|
};
|
|
611
633
|
if (!message) {
|
|
@@ -622,9 +644,16 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
622
644
|
});
|
|
623
645
|
return;
|
|
624
646
|
}
|
|
647
|
+
const pendingSend = {
|
|
648
|
+
callback,
|
|
649
|
+
stream: false,
|
|
650
|
+
onStreamError: err => callback(this._formatError(err, 'ESTREAM', false, 'API'))
|
|
651
|
+
};
|
|
625
652
|
if (typeof message.on === 'function') {
|
|
626
|
-
|
|
653
|
+
pendingSend.stream = message;
|
|
654
|
+
pendingSend.stream.on('error', pendingSend.onStreamError);
|
|
627
655
|
}
|
|
656
|
+
this._pendingSend = pendingSend;
|
|
628
657
|
const startTime = Date.now();
|
|
629
658
|
this._setEnvelope(envelope, (err, info) => {
|
|
630
659
|
if (err) {
|
|
@@ -817,8 +846,14 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
817
846
|
else {
|
|
818
847
|
this.logger.error(data, err.message);
|
|
819
848
|
}
|
|
849
|
+
// close() forgets the send in flight, it is completed with this same error afterwards so
|
|
850
|
+
// a late message stream error has nothing left to report
|
|
851
|
+
const pendingSend = this._pendingSend;
|
|
820
852
|
this.emit('error', err);
|
|
821
853
|
this.close();
|
|
854
|
+
if (pendingSend) {
|
|
855
|
+
pendingSend.callback(err);
|
|
856
|
+
}
|
|
822
857
|
}
|
|
823
858
|
/** @internal */
|
|
824
859
|
_formatError(message, type, response, command) {
|
|
@@ -864,14 +899,25 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
864
899
|
this.logger.info({
|
|
865
900
|
tnx: 'network'
|
|
866
901
|
}, 'Connection closed');
|
|
902
|
+
// the unterminated remainder is only reported as a reply (and so gives the error a responseCode)
|
|
903
|
+
// when it starts like a complete failure reply, not for a fragment such as "55" or a 250
|
|
904
|
+
const failureResponse = typeof serverResponse === 'string' && /^[45]\d{2}[ -]/.test(serverResponse) ? serverResponse : false;
|
|
867
905
|
if (this.upgrading && !this._destroyed) {
|
|
868
|
-
return this._onError(new Error('Connection closed unexpectedly'), 'ETLS',
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
|
|
906
|
+
return this._onError(new Error('Connection closed unexpectedly'), 'ETLS', failureResponse, 'CONN');
|
|
907
|
+
}
|
|
908
|
+
if (!failureResponse && this._responseActions[0] === this._actionGreeting && this._connectCallback && !this._destroyed) {
|
|
909
|
+
// A silent close before the greeting is handed to the connect() callback rather than
|
|
910
|
+
// emitted as 'error', callers that never saw an error for it must not start throwing one
|
|
911
|
+
const connectCallback = this._connectCallback;
|
|
912
|
+
this._connectCallback = false;
|
|
913
|
+
const err = this._formatError(new Error('Connection closed unexpectedly'), 'ECONNECTION', false, 'CONN');
|
|
914
|
+
this.logger.warn({ tnx: 'network' }, err.message);
|
|
915
|
+
connectCallback(err);
|
|
916
|
+
this.close();
|
|
917
|
+
return;
|
|
872
918
|
}
|
|
873
|
-
|
|
874
|
-
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION',
|
|
919
|
+
if (failureResponse || (this._responseActions[0] !== this.close && !this._destroyed)) {
|
|
920
|
+
return this._onError(new Error('Connection closed unexpectedly'), 'ECONNECTION', failureResponse, 'CONN');
|
|
875
921
|
}
|
|
876
922
|
this._destroy();
|
|
877
923
|
}
|
|
@@ -1348,21 +1394,25 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
1348
1394
|
if (/[ -]AUTH\b/i.test(str)) {
|
|
1349
1395
|
this.allowsAuth = true;
|
|
1350
1396
|
}
|
|
1351
|
-
// Detect
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
//
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1397
|
+
// Detect the advertised SASL mechanisms. The list is split into whole tokens rather
|
|
1398
|
+
// than searched for each name with a pattern: the patterns this replaced let two
|
|
1399
|
+
// whitespace runs overlap and backtracked quadratically over an AUTH line padded with
|
|
1400
|
+
// spaces, so a hostile server could stall the event loop from its EHLO reply
|
|
1401
|
+
// (GHSA-4ffr-jq9g-5ffx).
|
|
1402
|
+
const authMechanisms = new Set();
|
|
1403
|
+
for (const line of this._ehloLines) {
|
|
1404
|
+
const authMatch = /^AUTH[\s=](.*)/i.exec(line);
|
|
1405
|
+
if (authMatch) {
|
|
1406
|
+
for (const mechanism of authMatch[1].split(/[\s=]+/)) {
|
|
1407
|
+
authMechanisms.add(mechanism.toUpperCase());
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1362
1410
|
}
|
|
1363
|
-
//
|
|
1364
|
-
|
|
1365
|
-
|
|
1411
|
+
// listed in order of preference, the first one the credentials allow is used
|
|
1412
|
+
for (const mechanism of ['PLAIN', 'LOGIN', 'CRAM-MD5', 'XOAUTH2']) {
|
|
1413
|
+
if (authMechanisms.has(mechanism)) {
|
|
1414
|
+
this._supportedAuth.push(mechanism);
|
|
1415
|
+
}
|
|
1366
1416
|
}
|
|
1367
1417
|
// Detect if the server supports SIZE extensions (and the max allowed size)
|
|
1368
1418
|
if ((match = str.match(/[ -]SIZE(?:[ \t]+(\d+))?/im))) {
|
|
@@ -1622,7 +1672,11 @@ class SMTPConnection extends node_events_1.EventEmitter {
|
|
|
1622
1672
|
this._sendCommand('DATA');
|
|
1623
1673
|
}
|
|
1624
1674
|
else {
|
|
1625
|
-
|
|
1675
|
+
// report a temporary rejection when there is one, taking the last reply would mark the
|
|
1676
|
+
// whole message as permanently failed although some recipients were only deferred
|
|
1677
|
+
const deferred = envelope.rejectedErrors.find(rejectedErr => rejectedErr.responseCode && rejectedErr.responseCode < 500);
|
|
1678
|
+
const reply = deferred?.response ?? str;
|
|
1679
|
+
err = this._formatError("Can't send mail - all recipients were rejected", 'EENVELOPE', reply, 'RCPT TO');
|
|
1626
1680
|
err.rejected = envelope.rejected;
|
|
1627
1681
|
err.rejectedErrors = envelope.rejectedErrors;
|
|
1628
1682
|
return callback(err);
|
|
@@ -17,7 +17,7 @@ export interface SMTPPoolOptions extends SMTPTransportOptions {
|
|
|
17
17
|
rateLimit?: number | undefined;
|
|
18
18
|
/** Time window for rateLimit in milliseconds, defaults to 1000 */
|
|
19
19
|
rateDelta?: number | undefined;
|
|
20
|
-
/** How many times a message is requeued when its connection closes while sending,
|
|
20
|
+
/** How many times a message is requeued when its connection closes while sending, defaults to 5, a negative value means unlimited */
|
|
21
21
|
maxRequeues?: number | undefined;
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
@@ -26,6 +26,7 @@ export interface SMTPPoolOptions extends SMTPTransportOptions {
|
|
|
26
26
|
export type SMTPPoolResolvedOptions = SMTPPoolOptions & {
|
|
27
27
|
maxConnections: number;
|
|
28
28
|
maxMessages: number;
|
|
29
|
+
maxRequeues: number;
|
|
29
30
|
};
|
|
30
31
|
/**
|
|
31
32
|
* Result of a message sent through the pool, same as for the SMTP transport
|
|
@@ -43,6 +43,10 @@ const index_js_2 = __importDefault(require("../well-known/index.js"));
|
|
|
43
43
|
const shared = __importStar(require("../shared/index.js"));
|
|
44
44
|
const errors = __importStar(require("../errors.js"));
|
|
45
45
|
const packageData = __importStar(require("../package-info.js"));
|
|
46
|
+
/** First delay before a requeued message is retried, doubled on every further requeue */
|
|
47
|
+
const REQUEUE_BASE_DELAY = 50;
|
|
48
|
+
/** Upper bound for the requeue delay */
|
|
49
|
+
const REQUEUE_MAX_DELAY = 2000;
|
|
46
50
|
/**
|
|
47
51
|
* Creates a SMTP pool transport object for Nodemailer
|
|
48
52
|
*
|
|
@@ -74,6 +78,8 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|
|
74
78
|
);
|
|
75
79
|
this.options.maxConnections = this.options.maxConnections || 5;
|
|
76
80
|
this.options.maxMessages = this.options.maxMessages || 100;
|
|
81
|
+
// a default bound, a server that closes every connection before the greeting would otherwise be retried forever
|
|
82
|
+
this.options.maxRequeues = typeof this.options.maxRequeues === 'number' ? this.options.maxRequeues : 5;
|
|
77
83
|
this.logger = shared.getLogger(this.options, {
|
|
78
84
|
component: this.options.component || 'smtp-pool'
|
|
79
85
|
});
|
|
@@ -117,6 +123,10 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|
|
117
123
|
*/
|
|
118
124
|
send(mail, callback) {
|
|
119
125
|
if (this._closed) {
|
|
126
|
+
// Mail.sendMail ignores the return value, so without a callback its promise would never settle
|
|
127
|
+
const err = new Error('Connection pool was closed');
|
|
128
|
+
err.code = errors.ECONNECTION;
|
|
129
|
+
setImmediate(() => callback(err));
|
|
120
130
|
return false;
|
|
121
131
|
}
|
|
122
132
|
this._queue.push({
|
|
@@ -330,15 +340,22 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|
|
330
340
|
// Note that we must wait a bit.. because the callback of the 'error' handler might be called
|
|
331
341
|
// in the next event loop
|
|
332
342
|
setTimeout(() => {
|
|
343
|
+
let delay = 0;
|
|
333
344
|
if (connection.queueEntry) {
|
|
334
345
|
if (this._shouldRequeuOnConnectionClose(connection.queueEntry)) {
|
|
335
|
-
this._requeueEntryOnConnectionClose(connection);
|
|
346
|
+
delay = this._requeueEntryOnConnectionClose(connection);
|
|
336
347
|
}
|
|
337
348
|
else {
|
|
338
349
|
this._failDeliveryOnConnectionClose(connection);
|
|
339
350
|
}
|
|
340
351
|
}
|
|
341
|
-
|
|
352
|
+
if (delay) {
|
|
353
|
+
// back off, a server that keeps dropping connections is not hammered
|
|
354
|
+
setTimeout(() => this._continueProcessing(), delay);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
this._continueProcessing();
|
|
358
|
+
}
|
|
342
359
|
}, 50);
|
|
343
360
|
}
|
|
344
361
|
else {
|
|
@@ -353,7 +370,7 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|
|
353
370
|
}
|
|
354
371
|
/** @internal */
|
|
355
372
|
_shouldRequeuOnConnectionClose(queueEntry) {
|
|
356
|
-
if (this.options.maxRequeues
|
|
373
|
+
if (this.options.maxRequeues < 0) {
|
|
357
374
|
return true;
|
|
358
375
|
}
|
|
359
376
|
return queueEntry.requeueAttempts < this.options.maxRequeues;
|
|
@@ -362,7 +379,9 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|
|
362
379
|
_failDeliveryOnConnectionClose(connection) {
|
|
363
380
|
if (connection.queueEntry && connection.queueEntry.callback) {
|
|
364
381
|
try {
|
|
365
|
-
|
|
382
|
+
const err = new Error('Reached maximum number of retries after connection was closed');
|
|
383
|
+
err.code = errors.ECONNECTION;
|
|
384
|
+
connection.queueEntry.callback(err);
|
|
366
385
|
}
|
|
367
386
|
catch (E) {
|
|
368
387
|
this.logger.error({
|
|
@@ -377,6 +396,7 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|
|
377
396
|
}
|
|
378
397
|
/** @internal */
|
|
379
398
|
_requeueEntryOnConnectionClose(connection) {
|
|
399
|
+
const delay = Math.min(REQUEUE_BASE_DELAY * 2 ** connection.queueEntry.requeueAttempts, REQUEUE_MAX_DELAY);
|
|
380
400
|
connection.queueEntry.requeueAttempts += 1;
|
|
381
401
|
this.logger.debug({
|
|
382
402
|
tnx: 'pool',
|
|
@@ -386,6 +406,7 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|
|
386
406
|
}, 'Re-queued message <%s> for #%s. Attempt: #%s', connection.queueEntry.messageId, connection.id, connection.queueEntry.requeueAttempts);
|
|
387
407
|
this._queue.unshift(connection.queueEntry);
|
|
388
408
|
connection.queueEntry = false;
|
|
409
|
+
return delay;
|
|
389
410
|
}
|
|
390
411
|
/**
|
|
391
412
|
* Continue to process message if the pool hasn't closed
|
|
@@ -506,10 +527,15 @@ class SMTPPool extends node_events_1.EventEmitter {
|
|
|
506
527
|
connection.quit();
|
|
507
528
|
return done(null, true);
|
|
508
529
|
};
|
|
509
|
-
connection.connect(
|
|
530
|
+
connection.connect(err => {
|
|
510
531
|
if (returned) {
|
|
511
532
|
return;
|
|
512
533
|
}
|
|
534
|
+
if (err) {
|
|
535
|
+
returned = true;
|
|
536
|
+
connection.close();
|
|
537
|
+
return done(err);
|
|
538
|
+
}
|
|
513
539
|
if (auth && (connection.allowsAuth || options.forceAuth)) {
|
|
514
540
|
connection.login(auth, err => {
|
|
515
541
|
if (returned) {
|
|
@@ -66,7 +66,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
|
|
66
66
|
method: 'XOAUTH2'
|
|
67
67
|
};
|
|
68
68
|
oauth2.on('token', (token) => this.pool.mailer.emit('token', token));
|
|
69
|
-
oauth2.on('error', err => this.
|
|
69
|
+
oauth2.on('error', err => this._fail(err));
|
|
70
70
|
break;
|
|
71
71
|
}
|
|
72
72
|
default:
|
|
@@ -89,6 +89,19 @@ class PoolResource extends node_events_1.EventEmitter {
|
|
|
89
89
|
this._connected = false;
|
|
90
90
|
this.messages = 0;
|
|
91
91
|
this.available = true;
|
|
92
|
+
this._failed = false;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Emits 'error' for the first failure only. A dead resource can report the same failure more
|
|
96
|
+
* than once (the connection error, then the send callback), the pool handles it once
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
_fail(err) {
|
|
100
|
+
if (this._failed) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this._failed = true;
|
|
104
|
+
this.emit('error', err);
|
|
92
105
|
}
|
|
93
106
|
/**
|
|
94
107
|
* Initiates a connection to the SMTP server
|
|
@@ -101,7 +114,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
|
|
101
114
|
// nothing was connected, so no 'close' event is coming that would free the
|
|
102
115
|
// slot this resource holds in the pool, report the failure the way a failed
|
|
103
116
|
// login does
|
|
104
|
-
this.
|
|
117
|
+
this._fail(err);
|
|
105
118
|
return callback(err);
|
|
106
119
|
}
|
|
107
120
|
let returned = false;
|
|
@@ -118,8 +131,8 @@ class PoolResource extends node_events_1.EventEmitter {
|
|
|
118
131
|
options = Object.assign((0, index_js_2.assign)(false, options), socketOptions);
|
|
119
132
|
}
|
|
120
133
|
this.connection = new index_js_1.default(options);
|
|
121
|
-
this.connection.
|
|
122
|
-
this.
|
|
134
|
+
this.connection.on('error', err => {
|
|
135
|
+
this._fail(err);
|
|
123
136
|
if (returned) {
|
|
124
137
|
return;
|
|
125
138
|
}
|
|
@@ -128,33 +141,18 @@ class PoolResource extends node_events_1.EventEmitter {
|
|
|
128
141
|
});
|
|
129
142
|
this.connection.once('end', () => {
|
|
130
143
|
this.close();
|
|
131
|
-
if (returned) {
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
144
|
returned = true;
|
|
135
|
-
const timer = setTimeout(() => {
|
|
136
|
-
if (returned) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
// still have not returned, this means we have an unexpected connection close
|
|
140
|
-
const err = new Error('Unexpected socket close');
|
|
141
|
-
if (this.connection && this.connection.upgrading) {
|
|
142
|
-
// starttls connection errors
|
|
143
|
-
err.code = errors.ETLS;
|
|
144
|
-
}
|
|
145
|
-
callback(err);
|
|
146
|
-
}, 1000);
|
|
147
|
-
try {
|
|
148
|
-
timer.unref();
|
|
149
|
-
}
|
|
150
|
-
catch (_E) {
|
|
151
|
-
// Ignore. Happens on envs with non-node timer implementation
|
|
152
|
-
}
|
|
153
145
|
});
|
|
154
|
-
this.connection.connect(
|
|
146
|
+
this.connection.connect(err => {
|
|
155
147
|
if (returned) {
|
|
156
148
|
return;
|
|
157
149
|
}
|
|
150
|
+
if (err) {
|
|
151
|
+
// a close before the greeting, the 'end' that follows closes this resource
|
|
152
|
+
// and the pool requeues or fails the entry, bounded by maxRequeues
|
|
153
|
+
returned = true;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
158
156
|
if (this.auth && (this.connection.allowsAuth || options.forceAuth)) {
|
|
159
157
|
this.connection.login(this.auth, err => {
|
|
160
158
|
if (returned) {
|
|
@@ -163,7 +161,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
|
|
163
161
|
returned = true;
|
|
164
162
|
if (err) {
|
|
165
163
|
this.connection.close();
|
|
166
|
-
this.
|
|
164
|
+
this._fail(err);
|
|
167
165
|
return callback(err);
|
|
168
166
|
}
|
|
169
167
|
this._connected = true;
|
|
@@ -215,7 +213,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
|
|
215
213
|
this.messages++;
|
|
216
214
|
if (err) {
|
|
217
215
|
this.connection.close();
|
|
218
|
-
this.
|
|
216
|
+
this._fail(err);
|
|
219
217
|
return callback(err);
|
|
220
218
|
}
|
|
221
219
|
info.envelope = {
|
|
@@ -228,7 +226,7 @@ class PoolResource extends node_events_1.EventEmitter {
|
|
|
228
226
|
const err = new Error('Resource exhausted');
|
|
229
227
|
err.code = errors.EMAXLIMIT;
|
|
230
228
|
this.connection.close();
|
|
231
|
-
this.
|
|
229
|
+
this._fail(err);
|
|
232
230
|
}
|
|
233
231
|
else {
|
|
234
232
|
this.pool._checkRateLimit(() => {
|
|
@@ -177,31 +177,6 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
|
|
177
177
|
connection.close();
|
|
178
178
|
return callback(err);
|
|
179
179
|
});
|
|
180
|
-
connection.once('end', () => {
|
|
181
|
-
if (returned) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
const timer = setTimeout(() => {
|
|
185
|
-
if (returned) {
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
returned = true;
|
|
189
|
-
cleanupPerCallAuth();
|
|
190
|
-
// still have not returned, this means we have an unexpected connection close
|
|
191
|
-
const err = new Error('Unexpected socket close');
|
|
192
|
-
if (connection && connection.upgrading) {
|
|
193
|
-
// starttls connection errors
|
|
194
|
-
err.code = errors.ETLS;
|
|
195
|
-
}
|
|
196
|
-
callback(err);
|
|
197
|
-
}, 1000);
|
|
198
|
-
try {
|
|
199
|
-
timer.unref();
|
|
200
|
-
}
|
|
201
|
-
catch (_E) {
|
|
202
|
-
// Ignore. Happens on envs with non-node timer implementation
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
180
|
const sendMessage = () => {
|
|
206
181
|
const envelope = mail.message.getEnvelope();
|
|
207
182
|
const messageId = mail.message.messageId();
|
|
@@ -221,6 +196,10 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
|
|
221
196
|
messageId
|
|
222
197
|
}, 'Sending message %s to <%s>', messageId, recipients.join(', '));
|
|
223
198
|
connection.send(envelope, mail.message.createReadStream(), (err, info) => {
|
|
199
|
+
if (returned) {
|
|
200
|
+
// the connection error handler has already reported this send
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
224
203
|
returned = true;
|
|
225
204
|
cleanupPerCallAuth();
|
|
226
205
|
connection.close();
|
|
@@ -247,10 +226,16 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
|
|
247
226
|
}
|
|
248
227
|
});
|
|
249
228
|
};
|
|
250
|
-
connection.connect(
|
|
229
|
+
connection.connect(err => {
|
|
251
230
|
if (returned) {
|
|
252
231
|
return;
|
|
253
232
|
}
|
|
233
|
+
if (err) {
|
|
234
|
+
// the server closed the connection before the greeting
|
|
235
|
+
returned = true;
|
|
236
|
+
connection.close();
|
|
237
|
+
return callback(err);
|
|
238
|
+
}
|
|
254
239
|
perCallAuth = this.getAuth(mail.data.auth);
|
|
255
240
|
if (perCallAuth && (connection.allowsAuth || options.forceAuth)) {
|
|
256
241
|
connection.login(perCallAuth, err => {
|
|
@@ -332,10 +317,15 @@ class SMTPTransport extends node_events_1.EventEmitter {
|
|
|
332
317
|
connection.quit();
|
|
333
318
|
return done(null, true);
|
|
334
319
|
};
|
|
335
|
-
connection.connect(
|
|
320
|
+
connection.connect(err => {
|
|
336
321
|
if (returned) {
|
|
337
322
|
return;
|
|
338
323
|
}
|
|
324
|
+
if (err) {
|
|
325
|
+
returned = true;
|
|
326
|
+
connection.close();
|
|
327
|
+
return done(err);
|
|
328
|
+
}
|
|
339
329
|
perCallAuth = this.getAuth({});
|
|
340
330
|
if (perCallAuth && (connection.allowsAuth || options.forceAuth)) {
|
|
341
331
|
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;
|
|
@@ -74,12 +74,14 @@ class XOAuth2 extends node_stream_1.Stream {
|
|
|
74
74
|
constructor(options, logger) {
|
|
75
75
|
super();
|
|
76
76
|
this.options = options || {};
|
|
77
|
+
this.configError = false;
|
|
77
78
|
if (options && options.serviceClient) {
|
|
78
79
|
if (!options.privateKey || !options.user) {
|
|
80
|
+
// reported through getToken() rather than an 'error' event: the transports forward
|
|
81
|
+
// that event up to the Mail object, where nothing may be listening
|
|
79
82
|
const err = new Error('Options "privateKey" and "user" are required for service account!');
|
|
80
83
|
err.code = errors.EOAUTH2;
|
|
81
|
-
|
|
82
|
-
return;
|
|
84
|
+
this.configError = err;
|
|
83
85
|
}
|
|
84
86
|
const serviceRequestTimeout = Math.min(Math.max(Number(this.options.serviceRequestTimeout) || 0, 0), 3600);
|
|
85
87
|
this.options.serviceRequestTimeout = serviceRequestTimeout || 5 * 60;
|
|
@@ -113,6 +115,9 @@ class XOAuth2 extends node_stream_1.Stream {
|
|
|
113
115
|
getToken(renew, callback) {
|
|
114
116
|
// the error paths hand over the error alone
|
|
115
117
|
const done = callback;
|
|
118
|
+
if (this.configError) {
|
|
119
|
+
return done(this.configError);
|
|
120
|
+
}
|
|
116
121
|
if (!renew && this.accessToken && (!this.expires || this.expires > Date.now())) {
|
|
117
122
|
this.logger.debug({
|
|
118
123
|
tnx: 'OAUTH2',
|
|
@@ -349,7 +354,10 @@ class XOAuth2 extends node_stream_1.Stream {
|
|
|
349
354
|
method: 'post',
|
|
350
355
|
headers: params.customHeaders,
|
|
351
356
|
body: payload,
|
|
352
|
-
allowErrorResponse: true
|
|
357
|
+
allowErrorResponse: true,
|
|
358
|
+
// unset falls back to the fetch default, a stalled token endpoint would otherwise keep
|
|
359
|
+
// `renewing` set and queue every later request
|
|
360
|
+
timeout: params.requestTimeout
|
|
353
361
|
};
|
|
354
362
|
// OAuth2 token endpoints are credential-bearing. src/fetch already
|
|
355
363
|
// validates certs by default; pin rejectUnauthorized:true here so the
|
|
@@ -240,6 +240,81 @@ function _recoverAddrSpec(data) {
|
|
|
240
240
|
.filter(part => part)
|
|
241
241
|
.join(' ');
|
|
242
242
|
}
|
|
243
|
+
/**
|
|
244
|
+
* Takes the RFC 5322 comments out of the contents of an angle-addr.
|
|
245
|
+
*
|
|
246
|
+
* The tokenizer tracks a single open operator, so once '<' is open a '(' is plain text and
|
|
247
|
+
* a comment inside the brackets reached the address verbatim: 'Name <user@example.com(x)evil.com>'
|
|
248
|
+
* put 'user@example.com(x)evil.com' into the envelope, a value that is no mailbox and that a
|
|
249
|
+
* receiver stripping the comment reads as 'user@example.comevil.com' (GHSA-g73g-hqqh-jr95).
|
|
250
|
+
* A comment is folding whitespace, so it is read as a space that splits two atoms and as
|
|
251
|
+
* nothing next to an '@', the same rule the token walk applies outside the brackets. Quoted
|
|
252
|
+
* strings and domain literals are copied through untouched, a '(' in there is not a comment.
|
|
253
|
+
*
|
|
254
|
+
* @param address Contents of the angle brackets
|
|
255
|
+
* @return The address with the comments removed, and the text of the comments
|
|
256
|
+
*/
|
|
257
|
+
function _stripAddressComments(address) {
|
|
258
|
+
const comments = [];
|
|
259
|
+
let result = '';
|
|
260
|
+
let comment = '';
|
|
261
|
+
let depth = 0;
|
|
262
|
+
let closer = '';
|
|
263
|
+
// carried along rather than read back off the growing result, which would flatten it on
|
|
264
|
+
// every comment (see lastChars in _handleAddress)
|
|
265
|
+
let lastChar = '';
|
|
266
|
+
for (let i = 0, len = address.length; i < len; i++) {
|
|
267
|
+
const chr = address.charAt(i);
|
|
268
|
+
if (depth) {
|
|
269
|
+
if (chr === '\\' && i < len - 1) {
|
|
270
|
+
comment += address.charAt(++i);
|
|
271
|
+
}
|
|
272
|
+
else if (chr === '(') {
|
|
273
|
+
depth++;
|
|
274
|
+
comment += chr;
|
|
275
|
+
}
|
|
276
|
+
else if (chr === ')' && !--depth) {
|
|
277
|
+
comments.push(comment.trim());
|
|
278
|
+
comment = '';
|
|
279
|
+
if (lastChar !== '@' && address.charAt(i + 1) !== '@') {
|
|
280
|
+
result += ' ';
|
|
281
|
+
lastChar = ' ';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
comment += chr;
|
|
286
|
+
}
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (closer) {
|
|
290
|
+
if (chr === '\\' && closer === '"' && i < len - 1) {
|
|
291
|
+
result += chr + address.charAt(++i);
|
|
292
|
+
lastChar = address.charAt(i);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (chr === closer) {
|
|
296
|
+
closer = '';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (chr === '"') {
|
|
300
|
+
closer = '"';
|
|
301
|
+
}
|
|
302
|
+
else if (chr === '[') {
|
|
303
|
+
closer = ']';
|
|
304
|
+
}
|
|
305
|
+
else if (chr === '(') {
|
|
306
|
+
depth = 1;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
result += chr;
|
|
310
|
+
lastChar = chr;
|
|
311
|
+
}
|
|
312
|
+
if (depth) {
|
|
313
|
+
// an unterminated comment runs to the end of the address
|
|
314
|
+
comments.push(comment.trim());
|
|
315
|
+
}
|
|
316
|
+
return { address: result.trim(), comments: comments.filter(text => text) };
|
|
317
|
+
}
|
|
243
318
|
/**
|
|
244
319
|
* Converts tokens for a single address into an address object
|
|
245
320
|
*
|
|
@@ -367,6 +442,24 @@ function _handleAddress(tokens, depth) {
|
|
|
367
442
|
});
|
|
368
443
|
}
|
|
369
444
|
else {
|
|
445
|
+
// Comments come out of the angle-addr before anything asks whether one was found, so
|
|
446
|
+
// that brackets holding nothing but a comment read the same as empty ones
|
|
447
|
+
const addressComments = [];
|
|
448
|
+
const addressParts = [];
|
|
449
|
+
for (const part of data.address) {
|
|
450
|
+
if (part.indexOf('(') < 0) {
|
|
451
|
+
addressParts.push(part);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const stripped = _stripAddressComments(part);
|
|
455
|
+
for (const comment of stripped.comments) {
|
|
456
|
+
addressComments.push(comment);
|
|
457
|
+
}
|
|
458
|
+
if (stripped.address) {
|
|
459
|
+
addressParts.push(stripped.address);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
data.address = addressParts;
|
|
370
463
|
// If no address was found, try to detect one from regular text
|
|
371
464
|
if (!data.address.length && data.text.length) {
|
|
372
465
|
for (let i = data.text.length - 1; i >= 0; i--) {
|
|
@@ -433,6 +526,10 @@ function _handleAddress(tokens, depth) {
|
|
|
433
526
|
data.text = '';
|
|
434
527
|
}
|
|
435
528
|
_recoverAddrSpec(data);
|
|
529
|
+
// a comment names the mailbox only when nothing else does, as one outside the brackets
|
|
530
|
+
if (!data.text && addressComments.length) {
|
|
531
|
+
data.text = addressComments.join(' ');
|
|
532
|
+
}
|
|
436
533
|
const address = {
|
|
437
534
|
address: data.address || data.text || '',
|
|
438
535
|
name: data.text || data.address || ''
|