mail-time 4.1.0 → 5.1.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/README.md +76 -19
- package/adapters/redis.d.ts +19 -1
- package/adapters/redis.js +337 -8
- package/helpers.js +19 -0
- package/index.cjs +778 -127
- package/index.d.cts +50 -7
- package/index.d.ts +50 -7
- package/index.js +423 -118
- package/package.json +21 -4
- package/presets.js +2 -2
- package/scripts/migrate-redis-queue-to-cluster.mjs +108 -0
package/index.js
CHANGED
|
@@ -5,11 +5,56 @@ import { MongoQueue } from './adapters/mongo.js';
|
|
|
5
5
|
import { RedisQueue } from './adapters/redis.js';
|
|
6
6
|
import { PostgresQueue } from './adapters/postgres.js';
|
|
7
7
|
import { mailTimePreset, presets, presetNames } from './presets.js';
|
|
8
|
-
import { debug, logError, hasOwnProp, deepMerge, equals, isPlainObject, extractEmail, toAddressList, filterAddressField } from './helpers.js';
|
|
8
|
+
import { debug, escapeHtml, logError, hasOwnProp, deepMerge, equals, isPlainObject, extractEmail, toAddressList, filterAddressField } from './helpers.js';
|
|
9
9
|
|
|
10
10
|
const noop = () => {};
|
|
11
11
|
const queueMethods = ['ping', 'iterate', 'getPendingTo', 'push', 'remove', 'update', 'cancel'];
|
|
12
12
|
|
|
13
|
+
const callHook = (name, callback, ...args) => {
|
|
14
|
+
try {
|
|
15
|
+
const result = callback(...args);
|
|
16
|
+
if (result && typeof result.then === 'function') {
|
|
17
|
+
Promise.resolve(result).catch((hookError) => {
|
|
18
|
+
logError(`[${name}] callback failed`, hookError);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
} catch (hookError) {
|
|
22
|
+
logError(`[${name}] callback failed`, hookError);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Floor below which `sendingTimeout` is likely shorter than a real SMTP roundtrip
|
|
28
|
+
* (connect + greeting + STARTTLS + envelope + DATA, times MX rollover). A claim that
|
|
29
|
+
* expires mid-send is re-claimed by a peer, which delivers the same letter twice.
|
|
30
|
+
*/
|
|
31
|
+
const MIN_SAFE_SENDING_TIMEOUT = 120000;
|
|
32
|
+
const DEFAULT_MAX_RENEWALS = 10;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fields a queued task may set under `{strictPayload: true}`. Everything else —
|
|
36
|
+
* `attachments`, `envelope`, `dkim`, `raw`, `icalEvent`, … — is dropped, because a
|
|
37
|
+
* process with queue-write access would otherwise inherit nodemailer's whole
|
|
38
|
+
* capability surface (local file read, URL fetch, sender/DKIM override).
|
|
39
|
+
*/
|
|
40
|
+
const STRICT_MAIL_FIELDS = [
|
|
41
|
+
'to',
|
|
42
|
+
'cc',
|
|
43
|
+
'bcc',
|
|
44
|
+
'replyTo',
|
|
45
|
+
'subject',
|
|
46
|
+
'text',
|
|
47
|
+
'html',
|
|
48
|
+
'headers',
|
|
49
|
+
'list',
|
|
50
|
+
'priority',
|
|
51
|
+
'encoding',
|
|
52
|
+
'textEncoding',
|
|
53
|
+
'inReplyTo',
|
|
54
|
+
'references',
|
|
55
|
+
'date',
|
|
56
|
+
];
|
|
57
|
+
|
|
13
58
|
const __withLease = (task, updateObj) => {
|
|
14
59
|
if (task?.isSending === true && typeof task.tries === 'number' && typeof task.sendingAt === 'number') {
|
|
15
60
|
return { ...updateObj, leaseTries: task.tries, leaseSendingAt: task.sendingAt };
|
|
@@ -128,7 +173,7 @@ const buildRejectionErrorMap = (info) => {
|
|
|
128
173
|
return map;
|
|
129
174
|
};
|
|
130
175
|
|
|
131
|
-
let DEFAULT_TEMPLATE = '<!DOCTYPE html><html xmlns=http://www.w3.org/1999/xhtml><meta content="text/html; charset=utf-8"http-equiv=Content-Type><meta content="width=device-width,initial-scale=1"name=viewport><title>{{subject}}</title><style>body{-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;font-family:Tiempos,Georgia,Times,serif;font-weight:400;width:100%;height:100%;background:#fff;font-size:15px;color:#000;line-height:1.5}a{text-decoration:underline;border:0;color:#000;outline:0;color:inherit}a:hover{text-decoration:none}a[href^=sms],a[href^=tel]{text-decoration:none;color:#000;cursor:default}a img{border:none;text-decoration:none}td{font-family:Tiempos,Georgia,Times,serif;font-weight:400}hr{height:1px;border:none;width:100%;margin:0;margin-top:25px;margin-bottom:25px;background-color:#ECECEC}h1,h2,h3,h4,h5,h6{font-family:HelveticaNeue,"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;line-height:normal;margin-top:35px;margin-bottom:4px;margin-left:0;margin-right:0}h1{margin:23px 15px;font-size:25px}h2{margin-top:15px;font-size:21px}h3{font-weight:400;font-size:19px;border-bottom:1px solid #ECECEC}h4{font-weight:400;font-size:18px}h5{font-weight:400;font-size:17px}h6{font-weight:600;font-size:16px}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{text-decoration:none}pre{font-family:Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif;display:block;font-size:13px;padding:9.5px;margin:0 0 10px;line-height:1.42;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px;text-align:left!important;max-width:100%;white-space:pre-wrap;width:auto;overflow:auto}code{font-size:13px;font-family:font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif;border:1px solid rgba(0,0,0,.223);border-radius:2px;padding:1px 2px;word-break:break-all;word-wrap:break-word}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border:none;border-radius:0;word-break:break-all;word-wrap:break-word}td{text-align:center}table{border-collapse:collapse!important}.force-full-width{width:100%!important}</style><style media=screen>@media screen{h1,h2,h3,h4,h5,h6{font-family:\'Helvetica Neue\',Arial,sans-serif!important}td{font-family:Tiempos,Georgia,Times,serif!important}code,pre{font-family:Consolas,Menlo,Monaco,\'Lucida Console\',\'Liberation Mono\',\'DejaVu Sans Mono\',\'Bitstream Vera Sans Mono\',\'Courier New\',monospace,sans-serif!important}}</style><style media="only screen and (max-width:480px)">@media only screen and (max-width:480px){table[class=w320]{width:100%!important}}</style><body bgcolor=#FFFFFF class=body style=padding:0;margin:0;display:block;background:#fff;-webkit-text-size-adjust:none><table cellpadding=0 cellspacing=0 width=100% align=center><tr><td align=center valign=top bgcolor=#FFFFFF width=100%><center><table cellpadding=0 cellspacing=0 width=600 style="margin:0 auto"class=w320><tr><td align=center valign=top><table cellpadding=0 cellspacing=0 width=100% style="margin:0 auto;border-bottom:1px solid #ddd"bgcolor=#ECECEC><tr><td><h1>{{
|
|
176
|
+
let DEFAULT_TEMPLATE = '<!DOCTYPE html><html xmlns=http://www.w3.org/1999/xhtml><meta content="text/html; charset=utf-8"http-equiv=Content-Type><meta content="width=device-width,initial-scale=1"name=viewport><title>{{subject}}</title><style>body{-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:none;font-family:Tiempos,Georgia,Times,serif;font-weight:400;width:100%;height:100%;background:#fff;font-size:15px;color:#000;line-height:1.5}a{text-decoration:underline;border:0;color:#000;outline:0;color:inherit}a:hover{text-decoration:none}a[href^=sms],a[href^=tel]{text-decoration:none;color:#000;cursor:default}a img{border:none;text-decoration:none}td{font-family:Tiempos,Georgia,Times,serif;font-weight:400}hr{height:1px;border:none;width:100%;margin:0;margin-top:25px;margin-bottom:25px;background-color:#ECECEC}h1,h2,h3,h4,h5,h6{font-family:HelveticaNeue,"Helvetica Neue",Helvetica,Arial,sans-serif;font-weight:300;line-height:normal;margin-top:35px;margin-bottom:4px;margin-left:0;margin-right:0}h1{margin:23px 15px;font-size:25px}h2{margin-top:15px;font-size:21px}h3{font-weight:400;font-size:19px;border-bottom:1px solid #ECECEC}h4{font-weight:400;font-size:18px}h5{font-weight:400;font-size:17px}h6{font-weight:600;font-size:16px}h1 a,h2 a,h3 a,h4 a,h5 a,h6 a{text-decoration:none}pre{font-family:Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif;display:block;font-size:13px;padding:9.5px;margin:0 0 10px;line-height:1.42;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px;text-align:left!important;max-width:100%;white-space:pre-wrap;width:auto;overflow:auto}code{font-size:13px;font-family:font-family: Consolas,Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,sans-serif;border:1px solid rgba(0,0,0,.223);border-radius:2px;padding:1px 2px;word-break:break-all;word-wrap:break-word}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border:none;border-radius:0;word-break:break-all;word-wrap:break-word}td{text-align:center}table{border-collapse:collapse!important}.force-full-width{width:100%!important}</style><style media=screen>@media screen{h1,h2,h3,h4,h5,h6{font-family:\'Helvetica Neue\',Arial,sans-serif!important}td{font-family:Tiempos,Georgia,Times,serif!important}code,pre{font-family:Consolas,Menlo,Monaco,\'Lucida Console\',\'Liberation Mono\',\'DejaVu Sans Mono\',\'Bitstream Vera Sans Mono\',\'Courier New\',monospace,sans-serif!important}}</style><style media="only screen and (max-width:480px)">@media only screen and (max-width:480px){table[class=w320]{width:100%!important}}</style><body bgcolor=#FFFFFF class=body style=padding:0;margin:0;display:block;background:#fff;-webkit-text-size-adjust:none><table cellpadding=0 cellspacing=0 width=100% align=center><tr><td align=center valign=top bgcolor=#FFFFFF width=100%><center><table cellpadding=0 cellspacing=0 width=600 style="margin:0 auto"class=w320><tr><td align=center valign=top><table cellpadding=0 cellspacing=0 width=100% style="margin:0 auto;border-bottom:1px solid #ddd"bgcolor=#ECECEC><tr><td><h1>{{subject}}</h1></table><table cellpadding=0 cellspacing=0 width=100% style="margin:0 auto"bgcolor=#F2F2F2><tr><td><center><table cellpadding=0 cellspacing=0 width=100% style="margin:0 auto"><tr><td align=left style="text-align:left;padding:30px 25px">{{{html}}}</table></center></table></table></center></table>';
|
|
132
177
|
|
|
133
178
|
/**
|
|
134
179
|
* @typedef {import('./presets.js').MailTimePresetName} MailTimePresetName
|
|
@@ -159,7 +204,11 @@ let DEFAULT_TEMPLATE = '<!DOCTYPE html><html xmlns=http://www.w3.org/1999/xhtml>
|
|
|
159
204
|
*/
|
|
160
205
|
|
|
161
206
|
/**
|
|
162
|
-
* @typedef {{ [key: string]: any, adapter: MailTimeJoSkAdapterOptions | object, debug?: boolean, autoClear?: boolean, zombieTime?: number, minRevolvingDelay?: number, maxRevolvingDelay?: number, execute?: 'batch' | 'one', concurrency?: number, lockOwnerId?: string, resetOnInit?: boolean, onError?: (title: string, details: object) => void, onExecuted?: (uid: string, details: object) => void }} MailTimeJoSkOptions
|
|
207
|
+
* @typedef {{ [key: string]: any, adapter: MailTimeJoSkAdapterOptions | object, debug?: boolean, autoClear?: boolean, zombieTime?: number, lockLeaseTime?: number, minRevolvingDelay?: number, maxRevolvingDelay?: number, execute?: 'batch' | 'one', concurrency?: number, lockOwnerId?: string, resetOnInit?: boolean, onError?: (title: string, details: object) => void, onExecuted?: (uid: string, details: object) => void }} MailTimeJoSkOptions
|
|
208
|
+
*/
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* @typedef {{ [key: string]: any, ping: () => Promise<MailTimePingResult>, setInterval: (func: (...args: any[]) => unknown, delay: number, uid: string) => Promise<string>, destroy: () => boolean, pause: (timerId?: string) => boolean, resume: (timerId?: string) => boolean }} MailTimeScheduler
|
|
163
212
|
*/
|
|
164
213
|
|
|
165
214
|
/**
|
|
@@ -187,7 +236,11 @@ let DEFAULT_TEMPLATE = '<!DOCTYPE html><html xmlns=http://www.w3.org/1999/xhtml>
|
|
|
187
236
|
*/
|
|
188
237
|
|
|
189
238
|
/**
|
|
190
|
-
* @typedef {{
|
|
239
|
+
* @typedef {{ index: number, from: string | undefined }} MailTimeFromDetails
|
|
240
|
+
*/
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @typedef {{ queue: RedisQueue | MongoQueue | PostgresQueue | CustomQueue, type?: 'server' | 'client', from?: string | ((transport: MailTimeTransport, details: MailTimeFromDetails) => string), transports?: MailTimeTransport[], strategy?: 'backup' | 'balancer', failsToNext?: number, shouldFailOver?: (error: unknown, info: object | undefined, email: MailTimeTask) => boolean, retries?: number, maxTries?: number, retryDelay?: number, interval?: number, keepHistory?: boolean, concatEmails?: boolean | MailTimeConcatEmailsOptions, concatSubject?: string, concatDelimiter?: string, concatDelay?: number, concatThrottling?: number, revolvingInterval?: number, mode?: 'one' | 'batch', concurrency?: number, sendingTimeout?: number, renewClaim?: boolean | number, maxRenewals?: number, strictPayload?: boolean, allowedMailFields?: string[], verifyTransports?: boolean, template?: string, prefix?: string, debug?: boolean, josk?: MailTimeJoSkOptions, onError?: (error: unknown, email: MailTimeTask | null, details?: object) => void, onSent?: (email: MailTimeTask, details?: object) => void }} MailTimeOptions
|
|
191
244
|
*/
|
|
192
245
|
|
|
193
246
|
/** Class of MailTime */
|
|
@@ -248,7 +301,34 @@ class MailTime {
|
|
|
248
301
|
this.revolvingInterval = (typeof opts.revolvingInterval === 'number' && opts.revolvingInterval > 0) ? opts.revolvingInterval : 1536;
|
|
249
302
|
this.mode = (opts.mode === 'one' || opts.mode === 'batch') ? opts.mode : 'batch';
|
|
250
303
|
this.concurrency = (typeof opts.concurrency === 'number' && opts.concurrency > 0 && Number.isFinite(opts.concurrency)) ? Math.floor(opts.concurrency) : 1;
|
|
251
|
-
this.sendingTimeout = (typeof opts.sendingTimeout === 'number' && opts.sendingTimeout > 0) ? opts.sendingTimeout : 300000;
|
|
304
|
+
this.sendingTimeout = (typeof opts.sendingTimeout === 'number' && Number.isFinite(opts.sendingTimeout) && opts.sendingTimeout > 0) ? opts.sendingTimeout : 300000;
|
|
305
|
+
if (this.sendingTimeout < MIN_SAFE_SENDING_TIMEOUT) {
|
|
306
|
+
logError(`{sendingTimeout: ${this.sendingTimeout}} is below the ${MIN_SAFE_SENDING_TIMEOUT}ms safe floor — a claim that expires while SMTP is still in flight is re-claimed by a peer and the letter is delivered twice. Raise {sendingTimeout} above the worst-case SMTP roundtrip.`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Claim renewal: MailTime has no way to know how long a transport will take, so a
|
|
310
|
+
// long-but-healthy send used to lose its lock to a "recovery" worker. While a send
|
|
311
|
+
// is in flight the claim is re-stamped every {renewClaim} ms — bounded by
|
|
312
|
+
// {maxRenewals} so a genuinely wedged send is still recovered, just later.
|
|
313
|
+
if (opts.renewClaim === false || opts.renewClaim === 0) {
|
|
314
|
+
this.renewClaim = 0;
|
|
315
|
+
} else if (typeof opts.renewClaim === 'number' && Number.isFinite(opts.renewClaim) && opts.renewClaim > 0) {
|
|
316
|
+
this.renewClaim = Math.floor(opts.renewClaim);
|
|
317
|
+
} else {
|
|
318
|
+
this.renewClaim = Math.max(1000, Math.floor(this.sendingTimeout / 3));
|
|
319
|
+
}
|
|
320
|
+
this.maxRenewals = (typeof opts.maxRenewals === 'number' && Number.isFinite(opts.maxRenewals) && opts.maxRenewals >= 0) ? Math.floor(opts.maxRenewals) : DEFAULT_MAX_RENEWALS;
|
|
321
|
+
|
|
322
|
+
// Transport fail-over policy. Rotating on *any* error can resubmit a letter the
|
|
323
|
+
// MX already accepted; a transport that knows better says so.
|
|
324
|
+
this.shouldFailOver = (typeof opts.shouldFailOver === 'function') ? opts.shouldFailOver.bind(this) : null;
|
|
325
|
+
|
|
326
|
+
this.strictPayload = opts.strictPayload === true;
|
|
327
|
+
this.allowedMailFields = new Set([
|
|
328
|
+
...STRICT_MAIL_FIELDS,
|
|
329
|
+
...(Array.isArray(opts.allowedMailFields) ? opts.allowedMailFields.filter((field) => typeof field === 'string') : []),
|
|
330
|
+
]);
|
|
331
|
+
|
|
252
332
|
this.__isDestroyed = false;
|
|
253
333
|
this.__abortInFlight = false;
|
|
254
334
|
this.__isPaused = false;
|
|
@@ -307,6 +387,8 @@ class MailTime {
|
|
|
307
387
|
this.__debug(`INITIALIZING [mode: ${this.mode}]`);
|
|
308
388
|
this.__debug(`INITIALIZING [concurrency: ${this.concurrency}]`);
|
|
309
389
|
this.__debug(`INITIALIZING [sendingTimeout: ${this.sendingTimeout}]`);
|
|
390
|
+
this.__debug(`INITIALIZING [renewClaim: ${this.renewClaim}]`);
|
|
391
|
+
this.__debug(`INITIALIZING [strictPayload: ${this.strictPayload}]`);
|
|
310
392
|
|
|
311
393
|
/** SERVER-SPECIFIC CHECKS AND CONFIG */
|
|
312
394
|
if (this.type === 'server') {
|
|
@@ -371,6 +453,7 @@ class MailTime {
|
|
|
371
453
|
};
|
|
372
454
|
}
|
|
373
455
|
|
|
456
|
+
/** @type {MailTimeScheduler | undefined} */
|
|
374
457
|
this.scheduler = new JoSk({
|
|
375
458
|
debug: this.debug,
|
|
376
459
|
...this.josk,
|
|
@@ -536,12 +619,17 @@ class MailTime {
|
|
|
536
619
|
* @returns {Promise<string>} uuid of the email
|
|
537
620
|
* @throws {Error}
|
|
538
621
|
*/
|
|
539
|
-
async sendMail(opts
|
|
622
|
+
async sendMail(opts) {
|
|
623
|
+
opts = (opts && typeof opts === 'object') ? opts : {};
|
|
540
624
|
this.__debug('[sendMail]', opts);
|
|
541
625
|
if (!opts.html && !opts.text) {
|
|
542
626
|
throw new Error('[mail-time] [sendMail] `html` nor `text` field is present, at least one of those fields is required');
|
|
543
627
|
}
|
|
544
628
|
|
|
629
|
+
if (opts.raw !== void 0) {
|
|
630
|
+
throw new Error('[mail-time] [sendMail] `raw` is not supported: it bypasses composition, so `template`, `concatEmails`, and the `from()` callback would silently stop applying — and nodemailer historically let a message-level `raw` bypass `disableFileAccess`/`disableUrlAccess`. Pass `html` / `text` instead.');
|
|
631
|
+
}
|
|
632
|
+
|
|
545
633
|
let sendAt = opts.sendAt;
|
|
546
634
|
if (sendAt instanceof Date) {
|
|
547
635
|
sendAt = +sendAt;
|
|
@@ -670,13 +758,16 @@ class MailTime {
|
|
|
670
758
|
task.isSending = false;
|
|
671
759
|
|
|
672
760
|
this.__debug(`[private handleError] Giving up trying send email after ${task.tries} attempts to: `, task.mailOptions[0].to, error);
|
|
673
|
-
this.onError
|
|
761
|
+
callHook('onError', this.onError, error, task, info);
|
|
674
762
|
return;
|
|
675
763
|
}
|
|
676
764
|
|
|
677
765
|
let transportIndex = task.transport;
|
|
678
766
|
|
|
679
|
-
if (this.strategy === 'backup'
|
|
767
|
+
if (this.strategy === 'backup'
|
|
768
|
+
&& this.transports.length > 1
|
|
769
|
+
&& (task.tries % this.failsToNext) === 0
|
|
770
|
+
&& this.___mayFailOver(error, info, task)) {
|
|
680
771
|
transportIndex = this.___nextHealthyTransport(transportIndex);
|
|
681
772
|
}
|
|
682
773
|
|
|
@@ -738,12 +829,18 @@ class MailTime {
|
|
|
738
829
|
* @internal
|
|
739
830
|
* @memberOf MailTime
|
|
740
831
|
* @name ___render
|
|
741
|
-
* @description Render Mustache-like placeholders
|
|
832
|
+
* @description Render Mustache-like placeholders. `{{{key}}}` is always verbatim.
|
|
833
|
+
* `{{key}}` is HTML-escaped in HTML contexts (`html`, `template`, `concatDelimiter`) —
|
|
834
|
+
* that is what double-brace means everywhere else, and the previous strip-tags pass was
|
|
835
|
+
* not a safety net: it needs a closing `>`, so an unterminated `<a href="…` survived
|
|
836
|
+
* into the rendered body. Text bodies and subject headers are not HTML, so `{{key}}`
|
|
837
|
+
* substitutes verbatim there.
|
|
742
838
|
* @param {string} _string - Template with Mustache-like placeholders
|
|
743
839
|
* @param {Record<string, any>} replacements - Blaze/Mustache-like helpers Object
|
|
840
|
+
* @param {boolean} [isHtml=true] - `false` for text/plain bodies and header values
|
|
744
841
|
* @returns {string}
|
|
745
842
|
*/
|
|
746
|
-
___render(_string, replacements) {
|
|
843
|
+
___render(_string, replacements, isHtml = true) {
|
|
747
844
|
let string = _string;
|
|
748
845
|
const matchHTML = string.match(/\{{3}\s?([a-zA-Z0-9\-_]+)\s?\}{3}/g);
|
|
749
846
|
if (matchHTML) {
|
|
@@ -760,7 +857,8 @@ class MailTime {
|
|
|
760
857
|
for (let i = 0; i < matchStr.length; i++) {
|
|
761
858
|
const key = matchStr[i].slice(2, -2).trim();
|
|
762
859
|
if (hasOwnProp(replacements, key) && replacements[key] !== null && replacements[key] !== void 0) {
|
|
763
|
-
|
|
860
|
+
const value = `${replacements[key]}`;
|
|
861
|
+
string = string.replace(matchStr[i], isHtml ? escapeHtml(value) : value);
|
|
764
862
|
}
|
|
765
863
|
}
|
|
766
864
|
}
|
|
@@ -799,7 +897,7 @@ class MailTime {
|
|
|
799
897
|
const isMulti = mailOptionsList.length > 1;
|
|
800
898
|
|
|
801
899
|
for (let i = 0; i < mailOptionsList.length; i++) {
|
|
802
|
-
const mailOption = { ...mailOptionsList[i] };
|
|
900
|
+
const mailOption = this.___applyPayloadPolicy({ ...mailOptionsList[i] });
|
|
803
901
|
|
|
804
902
|
if (mailOption.html) {
|
|
805
903
|
const rendered = this.___render(mailOption.html, mailOption);
|
|
@@ -812,7 +910,7 @@ class MailTime {
|
|
|
812
910
|
}
|
|
813
911
|
|
|
814
912
|
if (mailOption.text) {
|
|
815
|
-
const rendered = this.___render(mailOption.text, mailOption);
|
|
913
|
+
const rendered = this.___render(mailOption.text, mailOption, false);
|
|
816
914
|
if (isMulti) {
|
|
817
915
|
compiledOpts.text += '\r\n' + rendered;
|
|
818
916
|
} else {
|
|
@@ -830,11 +928,14 @@ class MailTime {
|
|
|
830
928
|
|
|
831
929
|
if (isMulti) {
|
|
832
930
|
const rawSubject = task.concatSubject || this.concatSubject || compiledOpts.subject;
|
|
833
|
-
compiledOpts.subject = this.___render(rawSubject, { count: mailOptionsList.length });
|
|
931
|
+
compiledOpts.subject = this.___render(rawSubject, { count: mailOptionsList.length }, false);
|
|
834
932
|
}
|
|
835
933
|
|
|
836
934
|
if (!compiledOpts.from && this.from) {
|
|
837
|
-
compiledOpts.from = this.from(transport
|
|
935
|
+
compiledOpts.from = this.from(transport, {
|
|
936
|
+
index: this.transports.indexOf(transport),
|
|
937
|
+
from: MailTime.transportFrom(transport),
|
|
938
|
+
});
|
|
838
939
|
}
|
|
839
940
|
|
|
840
941
|
const acceptedSet = collectAcceptedSet(task);
|
|
@@ -844,9 +945,84 @@ class MailTime {
|
|
|
844
945
|
compiledOpts.bcc = filterAddressField(compiledOpts.bcc, acceptedSet);
|
|
845
946
|
}
|
|
846
947
|
|
|
948
|
+
if (this.strictPayload) {
|
|
949
|
+
compiledOpts.disableFileAccess = true;
|
|
950
|
+
compiledOpts.disableUrlAccess = true;
|
|
951
|
+
}
|
|
952
|
+
|
|
847
953
|
return compiledOpts;
|
|
848
954
|
}
|
|
849
955
|
|
|
956
|
+
/**
|
|
957
|
+
* @internal
|
|
958
|
+
* @memberOf MailTime
|
|
959
|
+
* @name ___applyPayloadPolicy
|
|
960
|
+
* @description Narrow one queued mail option to what a task is allowed to set. `raw` is
|
|
961
|
+
* always dropped: it bypasses composition (so `template`, `concatEmails` and the
|
|
962
|
+
* `from()` callback silently stop applying) and historically bypassed nodemailer's
|
|
963
|
+
* `disableFileAccess`/`disableUrlAccess` with it. Under `{strictPayload: true}` only
|
|
964
|
+
* `allowedMailFields` survive.
|
|
965
|
+
* @param {MailTimeMailOptions} mailOption - a single queued mail option object
|
|
966
|
+
* @returns {MailTimeMailOptions}
|
|
967
|
+
*/
|
|
968
|
+
___applyPayloadPolicy(mailOption) {
|
|
969
|
+
if (mailOption.raw !== void 0) {
|
|
970
|
+
delete mailOption.raw;
|
|
971
|
+
logError('[___compileMailOpts] dropped {raw} from a queued task — `raw` bypasses composition and is never sent by MailTime');
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (!this.strictPayload) {
|
|
975
|
+
return mailOption;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const allowed = {};
|
|
979
|
+
for (const key of Object.keys(mailOption)) {
|
|
980
|
+
if (this.allowedMailFields.has(key)) {
|
|
981
|
+
allowed[key] = mailOption[key];
|
|
982
|
+
} else {
|
|
983
|
+
this.__debug('[___applyPayloadPolicy] dropped non-allowlisted field', key);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return allowed;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* @static
|
|
991
|
+
* @memberOf MailTime
|
|
992
|
+
* @name transportFrom
|
|
993
|
+
* @description Best-effort sender address for a transport. `nodemailer.createTransport()`
|
|
994
|
+
* only exposes `.options` for *plain-object* configs — for a class-instance transporter
|
|
995
|
+
* (anything with its own `.send()`) nodemailer's internal `Mail.options` is always `{}`,
|
|
996
|
+
* so `transport.options.from` silently reads `undefined`. This walks the places the
|
|
997
|
+
* address can actually live.
|
|
998
|
+
* @param {MailTimeTransport} transport
|
|
999
|
+
* @returns {string | undefined}
|
|
1000
|
+
*/
|
|
1001
|
+
static transportFrom(transport) {
|
|
1002
|
+
const candidates = [
|
|
1003
|
+
transport?.options?.from,
|
|
1004
|
+
transport?.transporter?.options?.from,
|
|
1005
|
+
transport?._defaults?.from,
|
|
1006
|
+
transport?._options?.from,
|
|
1007
|
+
];
|
|
1008
|
+
|
|
1009
|
+
for (const candidate of candidates) {
|
|
1010
|
+
// nodemailer accepts `from` as a string or as `{ name, address }`. Resolve the
|
|
1011
|
+
// first candidate that yields a usable address — not the first truthy slot, so a
|
|
1012
|
+
// malformed `{ name }` object cannot mask a real address further down the chain.
|
|
1013
|
+
const address = (typeof candidate === 'string')
|
|
1014
|
+
? candidate
|
|
1015
|
+
: (candidate && typeof candidate === 'object' && typeof candidate.address === 'string')
|
|
1016
|
+
? candidate.address
|
|
1017
|
+
: '';
|
|
1018
|
+
if (address.trim()) {
|
|
1019
|
+
return address;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
return void 0;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
850
1026
|
/**
|
|
851
1027
|
* @internal
|
|
852
1028
|
* @memberOf MailTime
|
|
@@ -911,6 +1087,124 @@ class MailTime {
|
|
|
911
1087
|
}
|
|
912
1088
|
}
|
|
913
1089
|
|
|
1090
|
+
/**
|
|
1091
|
+
* @internal
|
|
1092
|
+
* @memberOf MailTime
|
|
1093
|
+
* @name ___mayFailOver
|
|
1094
|
+
* @description Whether this failure may be retried on the *next* transport. Rotating on
|
|
1095
|
+
* any error can resubmit a letter the receiving MTA already accepted (or may have
|
|
1096
|
+
* accepted — a socket that died during `DATA` is indistinguishable from one that died
|
|
1097
|
+
* before it). A transport signals "do not fail over" with `error.mayFailOver = false`;
|
|
1098
|
+
* the `shouldFailOver(error, info, task)` option overrides both.
|
|
1099
|
+
* @param {unknown} error - error returned by the transport
|
|
1100
|
+
* @param {object|undefined} info - info object returned by the transport
|
|
1101
|
+
* @param {MailTimeTask} task - email's task object from Storage
|
|
1102
|
+
* @returns {boolean}
|
|
1103
|
+
*/
|
|
1104
|
+
___mayFailOver(error, info, task) {
|
|
1105
|
+
if (this.shouldFailOver) {
|
|
1106
|
+
try {
|
|
1107
|
+
return this.shouldFailOver(error, info, task) !== false;
|
|
1108
|
+
} catch (policyError) {
|
|
1109
|
+
logError('[shouldFailOver] callback failed; keeping the current transport', policyError);
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
return !(error && typeof error === 'object' && error.mayFailOver === false);
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* @internal
|
|
1118
|
+
* @memberOf MailTime
|
|
1119
|
+
* @name ___startClaimRenewal
|
|
1120
|
+
* @description Hold a claim alive while its SMTP roundtrip runs. MailTime 4.x stamped
|
|
1121
|
+
* `sendingAt` once at claim time, so any send slower than `sendingTimeout` lost its lock
|
|
1122
|
+
* to a "recovery" worker and the letter went out twice. The renewal is a lease-guarded
|
|
1123
|
+
* update — it succeeds only while *this* worker still owns the row — and is bounded by
|
|
1124
|
+
* `maxRenewals` so a genuinely wedged send is still recovered.
|
|
1125
|
+
* @param {MailTimeTask} task - claimed task, with `isSending`/`sendingAt`/`tries` set
|
|
1126
|
+
* @returns {{ stop: () => Promise<void> }}
|
|
1127
|
+
*/
|
|
1128
|
+
___startClaimRenewal(task) {
|
|
1129
|
+
if (!this.renewClaim || !this.maxRenewals || this.type !== 'server') {
|
|
1130
|
+
return { stop: async () => {} };
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
let stopped = false;
|
|
1134
|
+
let renewals = 0;
|
|
1135
|
+
let timer = null;
|
|
1136
|
+
let activeRenewal = null;
|
|
1137
|
+
const halt = () => {
|
|
1138
|
+
if (stopped) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
stopped = true;
|
|
1142
|
+
if (timer) {
|
|
1143
|
+
clearInterval(timer);
|
|
1144
|
+
timer = null;
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
const stop = async () => {
|
|
1148
|
+
halt();
|
|
1149
|
+
if (activeRenewal) {
|
|
1150
|
+
await activeRenewal;
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
const renew = async () => {
|
|
1155
|
+
if (stopped || this.__isDestroyed || this.__abortInFlight) {
|
|
1156
|
+
halt();
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (renewals >= this.maxRenewals) {
|
|
1161
|
+
this.__debug('[private renewClaim] renewal budget exhausted; letting the claim go stale', task.uuid);
|
|
1162
|
+
halt();
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
renewals += 1;
|
|
1166
|
+
|
|
1167
|
+
// Strictly greater than the current stamp: storages that report "modified" only on
|
|
1168
|
+
// an actual value change would otherwise read a same-millisecond renewal as a no-op.
|
|
1169
|
+
const renewedAt = Math.max(Date.now(), (typeof task.sendingAt === 'number' ? task.sendingAt : 0) + 1);
|
|
1170
|
+
let renewed = false;
|
|
1171
|
+
try {
|
|
1172
|
+
renewed = await this.queue.update(task, __withLease(task, {
|
|
1173
|
+
isSending: true,
|
|
1174
|
+
sendingAt: renewedAt,
|
|
1175
|
+
}));
|
|
1176
|
+
} catch (renewError) {
|
|
1177
|
+
logError('[private renewClaim] storage error during claim renewal', renewError);
|
|
1178
|
+
halt();
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (!renewed) {
|
|
1183
|
+
this.__debug('[private renewClaim] lease lost; stopping renewal', task.uuid);
|
|
1184
|
+
halt();
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
task.sendingAt = renewedAt;
|
|
1189
|
+
this.__debug(`[private renewClaim] renewed ${renewals}/${this.maxRenewals}`, task.uuid);
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
timer = setInterval(() => {
|
|
1193
|
+
if (activeRenewal) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
activeRenewal = renew().finally(() => {
|
|
1197
|
+
activeRenewal = null;
|
|
1198
|
+
});
|
|
1199
|
+
}, this.renewClaim);
|
|
1200
|
+
|
|
1201
|
+
if (typeof timer.unref === 'function') {
|
|
1202
|
+
timer.unref();
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
return { stop };
|
|
1206
|
+
}
|
|
1207
|
+
|
|
914
1208
|
/**
|
|
915
1209
|
* @async
|
|
916
1210
|
* @internal
|
|
@@ -991,131 +1285,142 @@ class MailTime {
|
|
|
991
1285
|
const transport = this.transports[transportIndex];
|
|
992
1286
|
|
|
993
1287
|
const compiledOpts = this.___compileMailOpts(transport, task);
|
|
1288
|
+
const renewal = this.___startClaimRenewal(task);
|
|
994
1289
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
if (error) {
|
|
1005
|
-
await this.___handleError(task, error, info);
|
|
1290
|
+
try {
|
|
1291
|
+
await new Promise((resolve) => {
|
|
1292
|
+
transport.sendMail(compiledOpts, async (error, info) => {
|
|
1293
|
+
// The roundtrip has settled — stop extending the lease before any completion
|
|
1294
|
+
// write, so the guard below sees the last `sendingAt` this worker persisted.
|
|
1295
|
+
await renewal.stop();
|
|
1296
|
+
if (this.__abortInFlight) {
|
|
1297
|
+
this.__debug('[private send] instance destroyed mid-send; leaving claim for stale-lock recovery', task.uuid);
|
|
1298
|
+
resolve();
|
|
1006
1299
|
return;
|
|
1007
1300
|
}
|
|
1301
|
+
this.__debug('[private send] [sending]', { error, info });
|
|
1302
|
+
try {
|
|
1303
|
+
if (error) {
|
|
1304
|
+
await this.___handleError(task, error, info);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1008
1307
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1308
|
+
const acceptedAddrs = Array.isArray(info?.accepted)
|
|
1309
|
+
? info.accepted.map(extractEmail).filter((addr) => typeof addr === 'string')
|
|
1310
|
+
: [];
|
|
1012
1311
|
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1312
|
+
if (acceptedAddrs.length === 0) {
|
|
1313
|
+
await this.___handleError(task, new Error('Message not accepted or Greeting never received'), info);
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1017
1316
|
|
|
1018
|
-
|
|
1317
|
+
this.___trackAcceptedRecipients(task, acceptedAddrs);
|
|
1019
1318
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1319
|
+
const allRecipients = collectAllRecipients(task);
|
|
1320
|
+
const allAccepted = collectAcceptedSet(task);
|
|
1321
|
+
let isFullyDelivered = true;
|
|
1322
|
+
for (const addr of allRecipients) {
|
|
1323
|
+
if (!allAccepted.has(addr)) {
|
|
1324
|
+
isFullyDelivered = false;
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1027
1327
|
}
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
if (isFullyDelivered) {
|
|
1031
|
-
this.__debug(`email successfully sent, attempts: #${task.tries}, transport #${transportIndex} to: `, compiledOpts.to);
|
|
1032
1328
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1329
|
+
if (isFullyDelivered) {
|
|
1330
|
+
this.__debug(`email successfully sent, attempts: #${task.tries}, transport #${transportIndex} to: `, compiledOpts.to);
|
|
1331
|
+
|
|
1332
|
+
const leaseRemove = __leaseRemoveOpts(task);
|
|
1333
|
+
const leaseUpdate = __withLease(task, {
|
|
1334
|
+
isSent: true,
|
|
1335
|
+
isSending: false,
|
|
1336
|
+
sendingAt: 0,
|
|
1337
|
+
mailOptions: task.mailOptions,
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
let completed = false;
|
|
1341
|
+
if (!this.keepHistory) {
|
|
1342
|
+
completed = await this.queue.remove(task, leaseRemove);
|
|
1343
|
+
} else {
|
|
1344
|
+
completed = await this.queue.update(task, leaseUpdate);
|
|
1345
|
+
}
|
|
1040
1346
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
completed = await this.queue.update(task, leaseUpdate);
|
|
1046
|
-
}
|
|
1347
|
+
if (!completed) {
|
|
1348
|
+
this.__debug('[private send] lease lost before success completion, skipping onSent', task.uuid);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1047
1351
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1352
|
+
// Persist first, then mirror to in-memory task — a superseded worker must not lie to onSent.
|
|
1353
|
+
task.isSent = true;
|
|
1354
|
+
task.isSending = false;
|
|
1355
|
+
task.sendingAt = 0;
|
|
1356
|
+
callHook('onSent', this.onSent, task, info);
|
|
1050
1357
|
return;
|
|
1051
1358
|
}
|
|
1052
1359
|
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1360
|
+
if (task.tries >= this.maxTries) {
|
|
1361
|
+
this.___finalizeRejected(task, info);
|
|
1362
|
+
|
|
1363
|
+
const leaseRemove = __leaseRemoveOpts(task);
|
|
1364
|
+
const leaseUpdate = __withLease(task, {
|
|
1365
|
+
isSent: false,
|
|
1366
|
+
isFailed: true,
|
|
1367
|
+
isSending: false,
|
|
1368
|
+
sendingAt: 0,
|
|
1369
|
+
mailOptions: task.mailOptions,
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
let finalized = false;
|
|
1373
|
+
if (!this.keepHistory) {
|
|
1374
|
+
finalized = await this.queue.remove(task, leaseRemove);
|
|
1375
|
+
} else {
|
|
1376
|
+
finalized = await this.queue.update(task, leaseUpdate);
|
|
1377
|
+
}
|
|
1063
1378
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
isSending: false,
|
|
1069
|
-
sendingAt: 0,
|
|
1070
|
-
mailOptions: task.mailOptions,
|
|
1071
|
-
});
|
|
1379
|
+
if (!finalized) {
|
|
1380
|
+
this.__debug('[private send] lease lost before partial-failure completion, skipping onError', task.uuid);
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1072
1383
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
finalized = await this.queue.update(task, leaseUpdate);
|
|
1078
|
-
}
|
|
1384
|
+
// Persist first, then mirror to in-memory task — a superseded worker must not lie to onError.
|
|
1385
|
+
task.isSent = false;
|
|
1386
|
+
task.isFailed = true;
|
|
1387
|
+
task.isSending = false;
|
|
1079
1388
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1389
|
+
const rejectedAddrs = [];
|
|
1390
|
+
for (const mo of task.mailOptions) {
|
|
1391
|
+
for (const r of (mo.rejected || [])) {
|
|
1392
|
+
rejectedAddrs.push(r.address);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
const partialError = new Error(`Recipients rejected after ${task.tries} attempts: ${rejectedAddrs.join(', ')}`);
|
|
1396
|
+
this.__debug('[private send] Partial delivery exhausted retries; rejected: ', rejectedAddrs);
|
|
1397
|
+
callHook('onError', this.onError, partialError, task, info);
|
|
1082
1398
|
return;
|
|
1083
1399
|
}
|
|
1084
1400
|
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1401
|
+
const nextSendAt = Date.now() + this.retryDelay;
|
|
1402
|
+
const released = await this.queue.update(task, __withLease(task, {
|
|
1403
|
+
isSending: false,
|
|
1404
|
+
sendingAt: 0,
|
|
1405
|
+
sendAt: nextSendAt,
|
|
1406
|
+
mailOptions: task.mailOptions,
|
|
1407
|
+
}));
|
|
1408
|
+
if (!released) {
|
|
1409
|
+
this.__debug('[private send] lease lost before partial retry release', task.uuid);
|
|
1410
|
+
return;
|
|
1095
1411
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
const nextSendAt = Date.now() + this.retryDelay;
|
|
1103
|
-
const released = await this.queue.update(task, __withLease(task, {
|
|
1104
|
-
isSending: false,
|
|
1105
|
-
sendingAt: 0,
|
|
1106
|
-
sendAt: nextSendAt,
|
|
1107
|
-
mailOptions: task.mailOptions,
|
|
1108
|
-
}));
|
|
1109
|
-
if (!released) {
|
|
1110
|
-
this.__debug('[private send] lease lost before partial retry release', task.uuid);
|
|
1111
|
-
return;
|
|
1412
|
+
this.__debug(`[private send] Partial delivery, next attempt at ${new Date(nextSendAt)}: #${task.tries}/${this.maxTries} for remaining recipients`);
|
|
1413
|
+
} catch (completionError) {
|
|
1414
|
+
logError('[private send] completion error after transport callback', completionError);
|
|
1415
|
+
} finally {
|
|
1416
|
+
resolve();
|
|
1112
1417
|
}
|
|
1113
|
-
|
|
1114
|
-
} finally {
|
|
1115
|
-
resolve();
|
|
1116
|
-
}
|
|
1418
|
+
});
|
|
1117
1419
|
});
|
|
1118
|
-
}
|
|
1420
|
+
} finally {
|
|
1421
|
+
// A transport that throws synchronously never reaches the callback above.
|
|
1422
|
+
await renewal.stop();
|
|
1423
|
+
}
|
|
1119
1424
|
} catch (e) {
|
|
1120
1425
|
if (this.__abortInFlight) {
|
|
1121
1426
|
this.__debug('[private send] instance destroyed mid-send; suppressing runtime exception', e);
|
|
@@ -1202,7 +1507,7 @@ class MailTime {
|
|
|
1202
1507
|
}
|
|
1203
1508
|
this.__unhealthyTransports.add(r.index);
|
|
1204
1509
|
logError(`[mail-time] [verifyTransports] transport #${r.index} failed verification`, r.error);
|
|
1205
|
-
this.onError
|
|
1510
|
+
callHook('onError', this.onError, r.error, null, { transportIndex: r.index, phase: 'verify' });
|
|
1206
1511
|
}
|
|
1207
1512
|
|
|
1208
1513
|
if (this.__unhealthyTransports.size === this.transports.length) {
|