mail-time 1.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +468 -118
- package/adapters/blank-example.js +230 -0
- package/adapters/mongo.js +310 -0
- package/adapters/redis.js +302 -0
- package/helpers.js +11 -0
- package/index.cjs +968 -341
- package/index.js +407 -392
- package/package.json +8 -7
package/index.cjs
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const josk = require('josk');
|
|
4
4
|
const merge = require('deepmerge');
|
|
5
|
+
const crypto = require('crypto');
|
|
5
6
|
|
|
6
|
-
const
|
|
7
|
-
const _debug = (isDebug, ...args) => {
|
|
7
|
+
const debug = (isDebug, ...args) => {
|
|
8
8
|
if (isDebug) {
|
|
9
9
|
console.info.call(console, '[DEBUG] [mail-time]', `${new Date}`, ...args);
|
|
10
10
|
}
|
|
11
11
|
};
|
|
12
|
-
const _logError = (...args) => {
|
|
13
|
-
console.error.call(console, '[ERROR] [mail-time]', `${new Date}`, ...args);
|
|
14
|
-
};
|
|
15
12
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
_logError('[mongoErrorHandler]:', error);
|
|
19
|
-
console.trace();
|
|
20
|
-
}
|
|
13
|
+
const logError = (...args) => {
|
|
14
|
+
console.error.call(console, '[ERROR] [mail-time]', `${new Date}`, ...args);
|
|
21
15
|
};
|
|
22
16
|
|
|
23
17
|
/**
|
|
@@ -62,11 +56,572 @@ const ensureIndex = async (collection, keys, opts) => {
|
|
|
62
56
|
await collection.createIndex(keys, opts);
|
|
63
57
|
}
|
|
64
58
|
} else {
|
|
65
|
-
|
|
59
|
+
logError(`[ensureIndex] Can not set ${Object.keys(keys).join(' + ')} index on "${collection._name || 'MongoDB'}" collection`, { keys, opts, details: e });
|
|
66
60
|
}
|
|
67
61
|
}
|
|
68
62
|
};
|
|
69
63
|
|
|
64
|
+
/** Class representing MongoDB Queue for MailTime */
|
|
65
|
+
class MongoQueue {
|
|
66
|
+
/**
|
|
67
|
+
* Create a MongoQueue instance
|
|
68
|
+
* @param {object} opts - configuration object
|
|
69
|
+
* @param {Db} opts.db - Required, Mongo's `Db` instance, like one returned from `MongoClient#db()` method
|
|
70
|
+
* @param {string} [opts.prefix] - Optional prefix for scope isolation; use when creating multiple MailTime instances within the single application
|
|
71
|
+
*/
|
|
72
|
+
constructor (opts) {
|
|
73
|
+
this.name = 'mongo-queue';
|
|
74
|
+
if (!opts || typeof opts !== 'object' || opts === null) {
|
|
75
|
+
throw new TypeError('[mail-time] Configuration object must be passed into MongoQueue constructor');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!opts.db) {
|
|
79
|
+
throw new Error('[mail-time] [MongoQueue] requires MongoDB database {db} option, like returned from `MongoClient.connect`');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.prefix = (typeof opts.prefix === 'string') ? opts.prefix : '';
|
|
83
|
+
this.db = opts.db;
|
|
84
|
+
this.collection = opts.db.collection(`__mailTimeQueue__${this.prefix}`);
|
|
85
|
+
ensureIndex(this.collection, { uuid: 1 }, { background: false });
|
|
86
|
+
ensureIndex(this.collection, { isSent: 1, isFailed: 1, isCancelled: 1, to: 1, sendAt: 1 }, { background: false });
|
|
87
|
+
ensureIndex(this.collection, { isSent: 1, isFailed: 1, isCancelled: 1, sendAt: 1, tries: 1 }, { background: false });
|
|
88
|
+
|
|
89
|
+
// MongoDB Collection Schema:
|
|
90
|
+
// _id
|
|
91
|
+
// uuid {string}
|
|
92
|
+
// to {string|[string]}
|
|
93
|
+
// tries {number} - qty of send attempts
|
|
94
|
+
// sendAt {number} - When letter should be sent
|
|
95
|
+
// isSent {boolean} - Email status
|
|
96
|
+
// isCancelled {boolean} - `true` if email was cancelled before it was sent
|
|
97
|
+
// isFailed {boolean} - `true` if email has failed to send
|
|
98
|
+
// template {string} - Template for this email
|
|
99
|
+
// transport {number} - Last used transport
|
|
100
|
+
// concatSubject {string|boolean} - Email concatenation subject
|
|
101
|
+
// ---
|
|
102
|
+
// mailOptions {[object]} - Array of nodeMailer's `mailOptions`
|
|
103
|
+
// mailOptions.to {string|[string]} - [REQUIRED]
|
|
104
|
+
// mailOptions.from {string}
|
|
105
|
+
// mailOptions.text {string|boolean}
|
|
106
|
+
// mailOptions.html {string}
|
|
107
|
+
// mailOptions.subject {string}
|
|
108
|
+
// mailOptions.Other nodeMailer `sendMail` options...
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @async
|
|
113
|
+
* @memberOf MongoQueue
|
|
114
|
+
* @name ping
|
|
115
|
+
* @description Check connection to Storage
|
|
116
|
+
* @returns {Promise<object>}
|
|
117
|
+
*/
|
|
118
|
+
async ping() {
|
|
119
|
+
if (!this.mailTimeInstance) {
|
|
120
|
+
return {
|
|
121
|
+
status: 'Service Unavailable',
|
|
122
|
+
code: 503,
|
|
123
|
+
statusCode: 503,
|
|
124
|
+
error: new Error('MailTime instance not yet assigned to {mailTimeInstance} of Queue Adapter context'),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
const ping = await this.db.command({ ping: 1 });
|
|
130
|
+
if (ping?.ok === 1) {
|
|
131
|
+
return {
|
|
132
|
+
status: 'OK',
|
|
133
|
+
code: 200,
|
|
134
|
+
statusCode: 200,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
} catch (pingError) {
|
|
138
|
+
return {
|
|
139
|
+
status: 'Internal Server Error',
|
|
140
|
+
code: 500,
|
|
141
|
+
statusCode: 500,
|
|
142
|
+
error: pingError
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
status: 'Service Unavailable',
|
|
148
|
+
code: 503,
|
|
149
|
+
statusCode: 503,
|
|
150
|
+
error: new Error('Service Unavailable')
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @memberOf MongoQueue
|
|
156
|
+
* @name iterate
|
|
157
|
+
* @description iterate over queued tasks passing to `mailTimeInstance.___send` method
|
|
158
|
+
* @returns {void 0}
|
|
159
|
+
*/
|
|
160
|
+
async iterate() {
|
|
161
|
+
try {
|
|
162
|
+
const cursor = this.collection.find({
|
|
163
|
+
isSent: false,
|
|
164
|
+
isFailed: false,
|
|
165
|
+
isCancelled: false,
|
|
166
|
+
sendAt: {
|
|
167
|
+
$lte: Date.now()
|
|
168
|
+
},
|
|
169
|
+
tries: {
|
|
170
|
+
$lt: this.mailTimeInstance.maxTries
|
|
171
|
+
}
|
|
172
|
+
}, {
|
|
173
|
+
projection: {
|
|
174
|
+
_id: 1,
|
|
175
|
+
uuid: 1,
|
|
176
|
+
tries: 1,
|
|
177
|
+
template: 1,
|
|
178
|
+
transport: 1,
|
|
179
|
+
isSent: 1,
|
|
180
|
+
isFailed: 1,
|
|
181
|
+
isCancelled: 1,
|
|
182
|
+
mailOptions: 1,
|
|
183
|
+
concatSubject: 1,
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
while (await cursor.hasNext()) {
|
|
188
|
+
await this.mailTimeInstance.___send(await cursor.next());
|
|
189
|
+
}
|
|
190
|
+
await cursor.close();
|
|
191
|
+
} catch (iterateError) {
|
|
192
|
+
logError('[iterate] [while/await] [iterateError]', iterateError);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @async
|
|
198
|
+
* @memberOf MongoQueue
|
|
199
|
+
* @name getPendingTo
|
|
200
|
+
* @description get queued task by `to` field (addressee)
|
|
201
|
+
* @param to {string} - email address
|
|
202
|
+
* @param sendAt {number} - timestamp
|
|
203
|
+
* @returns {Promise<object|null>}
|
|
204
|
+
*/
|
|
205
|
+
async getPendingTo(to, sendAt) {
|
|
206
|
+
if (typeof to !== 'string' || typeof sendAt !== 'number') {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return await this.collection.findOne({
|
|
211
|
+
to: to,
|
|
212
|
+
isSent: false,
|
|
213
|
+
isFailed: false,
|
|
214
|
+
isCancelled: false,
|
|
215
|
+
sendAt: {
|
|
216
|
+
$lte: sendAt
|
|
217
|
+
}
|
|
218
|
+
}, {
|
|
219
|
+
projection: {
|
|
220
|
+
_id: 1,
|
|
221
|
+
to: 1,
|
|
222
|
+
uuid: 1,
|
|
223
|
+
tries: 1,
|
|
224
|
+
isSent: 1,
|
|
225
|
+
isFailed: 1,
|
|
226
|
+
isCancelled: 1,
|
|
227
|
+
mailOptions: 1,
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* @async
|
|
234
|
+
* @memberOf MongoQueue
|
|
235
|
+
* @name push
|
|
236
|
+
* @description push task to the queue/storage
|
|
237
|
+
* @param task {object} - task's object
|
|
238
|
+
* @returns {Promise<void 0>}
|
|
239
|
+
*/
|
|
240
|
+
async push(task) {
|
|
241
|
+
if (typeof task !== 'object') {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (task.sendAt instanceof Date) {
|
|
246
|
+
task.sendAt = +task.sendAt;
|
|
247
|
+
}
|
|
248
|
+
await this.collection.insertOne(task);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @async
|
|
253
|
+
* @memberOf MongoQueue
|
|
254
|
+
* @name cancel
|
|
255
|
+
* @description cancel scheduled email
|
|
256
|
+
* @param uuid {string} - email's uuid
|
|
257
|
+
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
|
|
258
|
+
*/
|
|
259
|
+
async cancel(uuid) {
|
|
260
|
+
if (typeof uuid !== 'string') {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const task = await this.collection.findOne({ uuid }, {
|
|
265
|
+
projection: {
|
|
266
|
+
_id: 1,
|
|
267
|
+
uuid: 1,
|
|
268
|
+
isSent: 1,
|
|
269
|
+
isCancelled: 1,
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
if (!task || task.isSent === true || task.isCancelled === true) {
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!this.mailTimeInstance.keepHistory) {
|
|
278
|
+
return await this.remove(task);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return await this.update(task, {
|
|
282
|
+
isCancelled: true,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @async
|
|
288
|
+
* @memberOf MongoQueue
|
|
289
|
+
* @name remove
|
|
290
|
+
* @description remove task from queue
|
|
291
|
+
* @param task {object} - task's object
|
|
292
|
+
* @returns {Promise<boolean>} returns `true` if removed or `false` if not found
|
|
293
|
+
*/
|
|
294
|
+
async remove(task) {
|
|
295
|
+
if (typeof task !== 'object') {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return (await this.collection.deleteOne({ _id: task._id }))?.deletedCount >= 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* @async
|
|
304
|
+
* @memberOf MongoQueue
|
|
305
|
+
* @name update
|
|
306
|
+
* @description remove task from queue
|
|
307
|
+
* @param task {object} - task's object
|
|
308
|
+
* @param updateObj {object} - fields with new values to update
|
|
309
|
+
* @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
|
|
310
|
+
*/
|
|
311
|
+
async update(task, updateObj) {
|
|
312
|
+
if (typeof task !== 'object' || typeof updateObj !== 'object') {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return (await this.collection.updateOne({
|
|
317
|
+
_id: task._id
|
|
318
|
+
}, {
|
|
319
|
+
$set: updateObj
|
|
320
|
+
}))?.modifiedCount >= 1;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** Class representing Redis Queue for MailTime */
|
|
325
|
+
class RedisQueue {
|
|
326
|
+
/**
|
|
327
|
+
* Create a RedisQueue instance
|
|
328
|
+
* @param {object} opts - configuration object
|
|
329
|
+
* @param {RedisClient} opts.client - Required, Redis'es `RedisClient` instance, like one returned from `await redis.createClient().connect()` method
|
|
330
|
+
* @param {string} [opts.prefix] - Optional prefix for scope isolation; use when creating multiple MailTime instances within the single application
|
|
331
|
+
*/
|
|
332
|
+
constructor (opts) {
|
|
333
|
+
this.name = 'redis-queue';
|
|
334
|
+
if (!opts || typeof opts !== 'object' || opts === null) {
|
|
335
|
+
throw new TypeError('[mail-time] Configuration object must be passed into RedisQueue constructor');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!opts.client) {
|
|
339
|
+
throw new Error('[mail-time] [RedisQueue] required {client} option is missing, e.g. returned from `redis.createClient()` or `redis.createCluster()` method');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
this.prefix = (typeof opts.prefix === 'string') ? opts.prefix : 'default';
|
|
343
|
+
this.uniqueName = `mailtime:${this.prefix}`;
|
|
344
|
+
this.client = opts.client;
|
|
345
|
+
|
|
346
|
+
// 3 types of keys are stored in Redis:
|
|
347
|
+
// 'letter' - JSON with email details
|
|
348
|
+
// 'sendat' - Timestamp used to scan/find/iterate over scheduled emails
|
|
349
|
+
// 'concatletter' — uuid of "pending" email for concatenation used when {concatEmails: true}
|
|
350
|
+
|
|
351
|
+
// Stored JSON structure:
|
|
352
|
+
// to {string|[string]}
|
|
353
|
+
// uuid {string}
|
|
354
|
+
// tries {number} - qty of send attempts
|
|
355
|
+
// sendAt {number} - When letter should be sent
|
|
356
|
+
// isSent {boolean} - Email status
|
|
357
|
+
// isCancelled {boolean} - `true` if email was cancelled before it was sent
|
|
358
|
+
// isFailed {boolean} - `true` if email has failed to send
|
|
359
|
+
// template {string} - Template for this email
|
|
360
|
+
// transport {number} - Last used transport
|
|
361
|
+
// concatSubject {string|boolean} - Email concatenation subject
|
|
362
|
+
// ---
|
|
363
|
+
// mailOptions {[object]} - Array of nodeMailer's `mailOptions`
|
|
364
|
+
// mailOptions.to {string|[string]} - [REQUIRED]
|
|
365
|
+
// mailOptions.from {string}
|
|
366
|
+
// mailOptions.text {string|boolean}
|
|
367
|
+
// mailOptions.html {string}
|
|
368
|
+
// mailOptions.subject {string}
|
|
369
|
+
// mailOptions.Other nodeMailer `sendMail` options...
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* @async
|
|
374
|
+
* @memberOf RedisQueue
|
|
375
|
+
* @name ping
|
|
376
|
+
* @description Check connection to Storage
|
|
377
|
+
* @returns {Promise<object>}
|
|
378
|
+
*/
|
|
379
|
+
async ping() {
|
|
380
|
+
if (!this.mailTimeInstance) {
|
|
381
|
+
return {
|
|
382
|
+
status: 'Service Unavailable',
|
|
383
|
+
code: 503,
|
|
384
|
+
statusCode: 503,
|
|
385
|
+
error: new Error('MailTime instance not yet assigned to {mailTimeInstance} of Queue Adapter context'),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
const ping = await this.client.ping();
|
|
391
|
+
if (ping === 'PONG') {
|
|
392
|
+
return {
|
|
393
|
+
status: 'OK',
|
|
394
|
+
code: 200,
|
|
395
|
+
statusCode: 200,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
} catch (pingError) {
|
|
399
|
+
return {
|
|
400
|
+
status: 'Internal Server Error',
|
|
401
|
+
code: 500,
|
|
402
|
+
statusCode: 500,
|
|
403
|
+
error: pingError
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
status: 'Service Unavailable',
|
|
409
|
+
code: 503,
|
|
410
|
+
statusCode: 503,
|
|
411
|
+
error: new Error('Service Unavailable')
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* @memberOf RedisQueue
|
|
417
|
+
* @name iterate
|
|
418
|
+
* @description iterate over queued tasks passing to `mailTimeInstance.___send` method
|
|
419
|
+
* @returns {void 0}
|
|
420
|
+
*/
|
|
421
|
+
async iterate() {
|
|
422
|
+
try {
|
|
423
|
+
const now = Date.now();
|
|
424
|
+
const cursor = this.client.scanIterator({
|
|
425
|
+
TYPE: 'string',
|
|
426
|
+
MATCH: this.__getKey('*', 'sendat'),
|
|
427
|
+
COUNT: 9999,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
for await (const sendatKey of cursor) {
|
|
431
|
+
if (parseInt(await this.client.get(sendatKey)) <= now) {
|
|
432
|
+
await this.mailTimeInstance.___send(JSON.parse(await this.client.get(this.__getKey(sendatKey.split(':')[3]))));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
} catch (iterateError) {
|
|
436
|
+
logError('[iterate] [for/await] [iterateError]', iterateError);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @async
|
|
442
|
+
* @memberOf RedisQueue
|
|
443
|
+
* @name getPendingTo
|
|
444
|
+
* @description get queued task by `to` field (addressee)
|
|
445
|
+
* @param to {string} - email address
|
|
446
|
+
* @param sendAt {number} - timestamp
|
|
447
|
+
* @returns {Promise<object|null>}
|
|
448
|
+
*/
|
|
449
|
+
async getPendingTo(to, sendAt) {
|
|
450
|
+
if (typeof to !== 'string' || typeof sendAt !== 'number') {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const concatKey = this.__getKey(to, 'concatletter');
|
|
455
|
+
let exists = await this.client.exists(concatKey);
|
|
456
|
+
if (!exists) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const uuid = await this.client.get(concatKey);
|
|
461
|
+
const letterKey = this.__getKey(uuid, 'letter');
|
|
462
|
+
exists = await this.client.exists(letterKey);
|
|
463
|
+
if (!exists) {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const task = JSON.parse(await this.client.get(letterKey));
|
|
468
|
+
if (!task || task.isSent === true || task.isCancelled === true || task.isFailed === true) {
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return task;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* @async
|
|
477
|
+
* @memberOf RedisQueue
|
|
478
|
+
* @name push
|
|
479
|
+
* @description push task to the queue/storage
|
|
480
|
+
* @param task {object} - task's object
|
|
481
|
+
* @returns {Promise<void 0>}
|
|
482
|
+
*/
|
|
483
|
+
async push(task) {
|
|
484
|
+
if (typeof task !== 'object') {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (task.sendAt instanceof Date) {
|
|
489
|
+
task.sendAt = +task.sendAt;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
await this.client.set(this.__getKey(task.uuid, 'letter'), JSON.stringify(task));
|
|
493
|
+
await this.client.set(this.__getKey(task.uuid, 'sendat'), `${task.sendAt}`);
|
|
494
|
+
if (task.to) {
|
|
495
|
+
await this.client.set(this.__getKey(task.to, 'concatletter'), task.uuid, {
|
|
496
|
+
PXAT: task.sendAt - 128
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* @async
|
|
503
|
+
* @memberOf RedisQueue
|
|
504
|
+
* @name cancel
|
|
505
|
+
* @description cancel scheduled email
|
|
506
|
+
* @param uuid {string} - email's uuid
|
|
507
|
+
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found, was sent, or was cancelled previously
|
|
508
|
+
*/
|
|
509
|
+
async cancel(uuid) {
|
|
510
|
+
if (typeof uuid !== 'string') {
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
await this.client.del(this.__getKey(uuid, 'sendat'));
|
|
515
|
+
const letterKey = this.__getKey(uuid, 'letter');
|
|
516
|
+
const exists = await this.client.exists(letterKey);
|
|
517
|
+
if (!exists) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const task = JSON.parse(await this.client.get(letterKey));
|
|
522
|
+
if (!task || task.isSent === true || task.isCancelled === true) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (!this.mailTimeInstance.keepHistory) {
|
|
527
|
+
return await this.remove(task);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return await this.update(task, {
|
|
531
|
+
isCancelled: true,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* @async
|
|
537
|
+
* @memberOf RedisQueue
|
|
538
|
+
* @name remove
|
|
539
|
+
* @description remove task from queue
|
|
540
|
+
* @param task {object} - task's object
|
|
541
|
+
* @returns {Promise<boolean>} returns `true` if removed or `false` if not found
|
|
542
|
+
*/
|
|
543
|
+
async remove(task) {
|
|
544
|
+
if (typeof task !== 'object' || typeof task.uuid !== 'string') {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const letterKey = this.__getKey(task.uuid, 'letter');
|
|
549
|
+
const exists = await this.client.exists(letterKey);
|
|
550
|
+
if (!exists) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
await this.client.del([
|
|
555
|
+
letterKey,
|
|
556
|
+
this.__getKey(task.uuid, 'sendat'),
|
|
557
|
+
]);
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* @async
|
|
563
|
+
* @memberOf RedisQueue
|
|
564
|
+
* @name update
|
|
565
|
+
* @description remove task from queue
|
|
566
|
+
* @param task {object} - task's object
|
|
567
|
+
* @param updateObj {object} - fields with new values to update
|
|
568
|
+
* @returns {Promise<boolean>} returns `true` if updated or `false` if not found or no changes was made
|
|
569
|
+
*/
|
|
570
|
+
async update(task, updateObj) {
|
|
571
|
+
if (typeof task !== 'object' || typeof task.uuid !== 'string' || typeof updateObj !== 'object') {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const letterKey = this.__getKey(task.uuid, 'letter');
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
const exists = await this.client.exists(letterKey);
|
|
579
|
+
if (!exists) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
const updatedTask = { ...task, ...updateObj };
|
|
583
|
+
await this.client.set(letterKey, JSON.stringify(updatedTask));
|
|
584
|
+
|
|
585
|
+
const sendatKey = this.__getKey(task.uuid, 'sendat');
|
|
586
|
+
if (updatedTask.isSent === true || updatedTask.isFailed === true || updatedTask.isCancelled === true) {
|
|
587
|
+
await this.client.del(sendatKey);
|
|
588
|
+
} else if (task.sendAt) {
|
|
589
|
+
await this.client.set(sendatKey, `${+task.sendAt}`);
|
|
590
|
+
}
|
|
591
|
+
return true;
|
|
592
|
+
} catch (opError) {
|
|
593
|
+
logError('[update] [try/catch] [opError]', opError);
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* @memberOf RedisQueue
|
|
601
|
+
* @name __getKey
|
|
602
|
+
* @description helper to generate scoped key
|
|
603
|
+
* @param uuid {string} - letter's uuid
|
|
604
|
+
* @param type {string} - "letter" or "sendat" or "concatletter"
|
|
605
|
+
* @returns {string} returns key used by Redis
|
|
606
|
+
*/
|
|
607
|
+
__getKey(uuid, type = 'letter') {
|
|
608
|
+
if (!this.__keyTypes.includes(type)) {
|
|
609
|
+
throw new Error(`[mail-time] [RedisQueue] [__getKey] unsupported key "${type}" passed into the second argument`);
|
|
610
|
+
}
|
|
611
|
+
return `${this.uniqueName}:${type}:${uuid}`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* @memberOf RedisQueue
|
|
616
|
+
* @name __keyTypes
|
|
617
|
+
* @description list of supported key type
|
|
618
|
+
* @returns {string} returns key used by Redis
|
|
619
|
+
*/
|
|
620
|
+
__keyTypes = ['letter', 'sendat', 'concatletter'];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const noop = () => {};
|
|
624
|
+
|
|
70
625
|
/**
|
|
71
626
|
* Check if entities of various types are equal
|
|
72
627
|
* including edge cases like unordered Objects and Array
|
|
@@ -151,38 +706,80 @@ let DEFAULT_TEMPLATE = '<!DOCTYPE html><html xmlns=http://www.w3.org/1999/xhtml>
|
|
|
151
706
|
|
|
152
707
|
/** Class of MailTime */
|
|
153
708
|
class MailTime {
|
|
709
|
+
/**
|
|
710
|
+
* Create a MailTime instance
|
|
711
|
+
* @param {object} opts - configuration object
|
|
712
|
+
* @param {RedisQueue|MongoQueue|CustomQueue} opts.queue - Queue Storage Driver instance
|
|
713
|
+
* @param {string} [opts.type] - "server" or "client" type of MailTime instance
|
|
714
|
+
* @param {function} [opts.from] - A function returning *String* for `from` header, format: `"MyApp" <user@example.com>`
|
|
715
|
+
* @param {[object]} [opts.transports] - An array of `nodemailer`'s transports, returned from `nodemailer.createTransport({})`; Required for `{type: 'server'}`
|
|
716
|
+
* @param {string} [opts.strategy] - `backup` or `balancer`
|
|
717
|
+
* @param {number} [opts.failsToNext] - After how many failed "send attempts" switch to the next transport? Applied only for `backup` strategy, default - `4`
|
|
718
|
+
* @param {number} [opts.retries] - How many times resend failed emails
|
|
719
|
+
* @param {number} [opts.retryDelay] - Interval in milliseconds between send re-tries
|
|
720
|
+
* @param {boolean} [opts.keepHistory] - Keep queue task as it is in the database
|
|
721
|
+
* @param {boolean} [opts.concatEmails] - Concatenate email by `to` field, default - `false`
|
|
722
|
+
* @param {string} [opts.concatSubject] - Email subject used in concatenated email, default - `Multiple notifications`
|
|
723
|
+
* @param {string} [opts.concatDelimiter] - HTML or plain string delimiter used between concatenated email, default - `<hr>`
|
|
724
|
+
* @param {number} [opts.concatDelay] - HTML or plain string delimiter used between concatenated email, default - `<hr>`
|
|
725
|
+
* @param {number} [opts.revolvingInterval] - Interval in *milliseconds* in between queue checks, default - `256`
|
|
726
|
+
* @param {object|RedisAdapter|MongoAdapter|CustomAdapter} [opts.josk.adapter] - Interval in milliseconds between send re-tries
|
|
727
|
+
* @param {string} [opts.josk.adapter.type] - One of `mongo` or `redis`
|
|
728
|
+
* @param {string} [opts.prefix] - Optional prefix for scope isolation; use when creating multiple MailTime instances within the single application; By default prefix inherited from MailTime instance
|
|
729
|
+
*/
|
|
154
730
|
constructor (opts) {
|
|
155
731
|
if (!opts || typeof opts !== 'object' || opts === null) {
|
|
156
732
|
throw new TypeError('[mail-time] Configuration object must be passed into MailTime constructor');
|
|
157
733
|
}
|
|
158
734
|
|
|
159
|
-
if (!opts.
|
|
160
|
-
throw new Error('[mail-time]
|
|
735
|
+
if (!opts.queue || typeof opts.queue !== 'object') {
|
|
736
|
+
throw new Error('[mail-time] {queue} option is required', {
|
|
737
|
+
description: 'MailTime requires MongoQueue, RedisQueue, or CustomQueue to connect to an intermediate database'
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
this.queue = opts.queue;
|
|
742
|
+
this.queue.mailTimeInstance = this;
|
|
743
|
+
const queueMethods = ['ping', 'iterate', 'getPendingTo', 'push', 'remove', 'update', 'cancel'];
|
|
744
|
+
|
|
745
|
+
for (let i = queueMethods.length - 1; i >= 0; i--) {
|
|
746
|
+
if (typeof this.queue[queueMethods[i]] !== 'function') {
|
|
747
|
+
throw new Error(`{queue} instance is missing {${queueMethods[i]}} method that is required!`);
|
|
748
|
+
}
|
|
161
749
|
}
|
|
162
750
|
|
|
163
|
-
this.callbacks = {};
|
|
164
|
-
this.type = (!opts.type || (opts.type !== 'client' && opts.type !== 'server')) ? 'server' : opts.type;
|
|
165
751
|
this.debug = (opts.debug !== true) ? false : true;
|
|
166
|
-
this.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
this.template = (typeof opts.template === 'string') ? opts.template : '{{{html}}}';
|
|
170
|
-
this.zombieTime = opts.zombieTime || 32786;
|
|
171
|
-
this.keepHistory = opts.keepHistory || false;
|
|
752
|
+
this._debug = (...args) => {
|
|
753
|
+
debug(this.debug, ...args);
|
|
754
|
+
};
|
|
172
755
|
|
|
173
|
-
this.
|
|
174
|
-
this.
|
|
175
|
-
this.maxRevolvingDelay = opts.maxRevolvingDelay || 2048;
|
|
756
|
+
this.type = (typeof opts.type !== 'string' || (opts.type !== 'client' && opts.type !== 'server')) ? 'server' : opts.type;
|
|
757
|
+
this.prefix = (typeof opts.prefix === 'string') ? opts.prefix : '';
|
|
176
758
|
|
|
177
|
-
if (
|
|
178
|
-
this.
|
|
759
|
+
if (typeof opts.retries === 'number') {
|
|
760
|
+
this.maxTries = opts.retries + 1;
|
|
761
|
+
} else if (typeof opts.maxTries === 'number') {
|
|
762
|
+
this.maxTries = (opts.maxTries < 1) ? 1 : 0;
|
|
763
|
+
} else {
|
|
764
|
+
this.maxTries = 60;
|
|
179
765
|
}
|
|
180
766
|
|
|
181
|
-
if (
|
|
182
|
-
this.
|
|
767
|
+
if (typeof opts.retryDelay === 'number') {
|
|
768
|
+
this.retryDelay = opts.retryDelay;
|
|
769
|
+
} else if (typeof opts.interval === 'number') {
|
|
770
|
+
this.retryDelay = opts.interval * 1000;
|
|
771
|
+
} else {
|
|
772
|
+
this.retryDelay = 60000;
|
|
183
773
|
}
|
|
184
774
|
|
|
185
|
-
this.
|
|
775
|
+
this.template = (typeof opts.template === 'string') ? opts.template : '{{{html}}}';
|
|
776
|
+
this.keepHistory = (typeof opts.keepHistory === 'boolean') ? opts.keepHistory : false;
|
|
777
|
+
this.onSent = opts.onSent || noop;
|
|
778
|
+
this.onError = opts.onError || noop;
|
|
779
|
+
|
|
780
|
+
this.revolvingInterval = opts.revolvingInterval || 1536;
|
|
781
|
+
|
|
782
|
+
this.failsToNext = (typeof opts.failsToNext === 'number') ? opts.failsToNext : 4;
|
|
186
783
|
this.strategy = (opts.strategy === 'backup' || opts.strategy === 'balancer') ? opts.strategy : 'backup';
|
|
187
784
|
this.transports = opts.transports || [];
|
|
188
785
|
this.transport = 0;
|
|
@@ -203,348 +800,247 @@ class MailTime {
|
|
|
203
800
|
this.concatEmails = (opts.concatEmails !== true) ? false : true;
|
|
204
801
|
this.concatSubject = (opts.concatSubject && typeof opts.concatSubject === 'string') ? opts.concatSubject : 'Multiple notifications';
|
|
205
802
|
this.concatDelimiter = (opts.concatDelimiter && typeof opts.concatDelimiter === 'string') ? opts.concatDelimiter : '<hr>';
|
|
206
|
-
this.concatThrottling = ((opts.concatThrottling && !isNaN(opts.concatThrottling)) ? parseInt(opts.concatThrottling) : 60) * 1000;
|
|
207
803
|
|
|
208
|
-
if (
|
|
209
|
-
this.
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
_debug(this.debug, `INITIALIZING [strategy: ${this.strategy}]`);
|
|
215
|
-
_debug(this.debug, `INITIALIZING [prefix: ${this.prefix}]`);
|
|
216
|
-
_debug(this.debug, `INITIALIZING [maxTries: ${this.maxTries}]`);
|
|
217
|
-
_debug(this.debug, `INITIALIZING [failsToNext: ${this.failsToNext}]`);
|
|
218
|
-
|
|
219
|
-
if (opts.type === 'server' && (this.transports.constructor !== Array || !this.transports.length)) {
|
|
220
|
-
throw new Error('[mail-time] transports is required and must be an Array, like returned from `nodemailer.createTransport`');
|
|
804
|
+
if (typeof opts.concatDelay === 'number') {
|
|
805
|
+
this.concatDelay = opts.concatDelay;
|
|
806
|
+
} else if (typeof opts.concatThrottling === 'number') {
|
|
807
|
+
this.concatDelay = opts.concatThrottling * 1000;
|
|
808
|
+
} else {
|
|
809
|
+
this.concatDelay = 60000;
|
|
221
810
|
}
|
|
222
811
|
|
|
223
|
-
this.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
// transport {Number} - Last used transport
|
|
235
|
-
// concatSubject {String|Boolean} - Email concatenation subject
|
|
236
|
-
// ---
|
|
237
|
-
// mailOptions {[Object]} - Array of nodeMailer's `mailOptions`
|
|
238
|
-
// mailOptions.to {String|Array} - [REQUIRED]
|
|
239
|
-
// mailOptions.from {String}
|
|
240
|
-
// mailOptions.text {String|Boolean}
|
|
241
|
-
// mailOptions.html (String)
|
|
242
|
-
// mailOptions.subject {String}
|
|
243
|
-
// mailOptions.Other nodeMailer `sendMail` options...
|
|
244
|
-
|
|
812
|
+
this._debug('DEBUG ON {debug: true}');
|
|
813
|
+
this._debug(`INITIALIZING [type: ${this.type}]`);
|
|
814
|
+
this._debug(`INITIALIZING [strategy: ${this.strategy}]`);
|
|
815
|
+
this._debug(`INITIALIZING [josk.adapter: ${opts?.josk?.adapter}]`);
|
|
816
|
+
this._debug(`INITIALIZING [prefix: ${this.prefix}]`);
|
|
817
|
+
this._debug(`INITIALIZING [retries: ${this.retries}]`);
|
|
818
|
+
this._debug(`INITIALIZING [failsToNext: ${this.failsToNext}]`);
|
|
819
|
+
this._debug(`INITIALIZING [onError: ${this.onError}]`);
|
|
820
|
+
this._debug(`INITIALIZING [onSent: ${this.onSent}]`);
|
|
821
|
+
|
|
822
|
+
/** SERVER-SPECIFIC CHECKS AND CONFIG */
|
|
245
823
|
if (this.type === 'server') {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
prefix: `mailTimeQueue${this.prefix}`,
|
|
250
|
-
resetOnInit: false,
|
|
251
|
-
zombieTime: this.zombieTime,
|
|
252
|
-
minRevolvingDelay: this.minRevolvingDelay,
|
|
253
|
-
maxRevolvingDelay: this.maxRevolvingDelay
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
scheduler.setInterval(this.___send.bind(this), this.revolvingInterval, `mailTimeQueue${this.prefix}`);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
static get Template() {
|
|
261
|
-
return DEFAULT_TEMPLATE;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
static set Template(newVal) {
|
|
265
|
-
DEFAULT_TEMPLATE = newVal;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
___compileMailOpts(transport, task) {
|
|
269
|
-
let _mailOpts = {};
|
|
270
|
-
|
|
271
|
-
if (!transport) {
|
|
272
|
-
throw new Error('[mail-time] [sendMail] [___compileMailOpts] transport not available or misconfiguration is in place!');
|
|
273
|
-
}
|
|
824
|
+
if (this.transports.constructor !== Array || !this.transports.length) {
|
|
825
|
+
throw new Error('[mail-time] {transports} is required for {type: "server"} and must be an Array, like returned from `nodemailer.createTransport`');
|
|
826
|
+
}
|
|
274
827
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
828
|
+
if (typeof opts.josk !== 'object') {
|
|
829
|
+
throw new Error('[mail-time] {josk} option is required {object} for {type: "server"}');
|
|
830
|
+
}
|
|
278
831
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
832
|
+
if (typeof opts.josk.adapter !== 'object') {
|
|
833
|
+
throw new Error('[mail-time] {josk.adapter} option is required {object} *or* custom adapter Class');
|
|
834
|
+
}
|
|
282
835
|
|
|
283
|
-
|
|
284
|
-
html: '',
|
|
285
|
-
text: '',
|
|
286
|
-
subject: ''
|
|
287
|
-
});
|
|
836
|
+
this.josk = { ...opts.josk };
|
|
288
837
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
_mailOpts.html += this.___render(this.concatDelimiter, task.mailOptions[i]) + this.___render(task.mailOptions[i].html, task.mailOptions[i]);
|
|
293
|
-
} else {
|
|
294
|
-
_mailOpts.html = this.___render(task.mailOptions[i].html, task.mailOptions[i]);
|
|
838
|
+
if (typeof opts.josk.adapter?.type === 'string' && opts.josk.adapter?.type === 'mongo') {
|
|
839
|
+
if (!opts.josk.adapter.db) {
|
|
840
|
+
throw new Error('[mail-time] {josk.adapter.db} option required for {josk.adapter.type: "mongo"}');
|
|
295
841
|
}
|
|
296
|
-
|
|
842
|
+
this.josk.adapter = new josk.MongoAdapter({ prefix: `mailTimeQueue${this.prefix}`, ...opts.josk.adapter });
|
|
297
843
|
}
|
|
298
844
|
|
|
299
|
-
if (
|
|
300
|
-
if (
|
|
301
|
-
|
|
302
|
-
} else {
|
|
303
|
-
_mailOpts.text = this.___render(task.mailOptions[i].text, task.mailOptions[i]);
|
|
845
|
+
if (typeof opts.josk.adapter?.type === 'string' && opts.josk.adapter?.type === 'redis') {
|
|
846
|
+
if (!opts.josk.adapter.client) {
|
|
847
|
+
throw new Error('[mail-time] {josk.adapter.client} option required for {josk.adapter.type: "redis"}');
|
|
304
848
|
}
|
|
305
|
-
|
|
849
|
+
this.josk.adapter = new josk.RedisAdapter({ prefix: `mailTimeQueue${this.prefix}`, ...opts.josk.adapter });
|
|
306
850
|
}
|
|
307
851
|
|
|
308
|
-
|
|
309
|
-
|
|
852
|
+
this.josk.minRevolvingDelay = (typeof opts.josk.minRevolvingDelay === 'number') ? opts.josk.minRevolvingDelay : 512;
|
|
853
|
+
this.josk.maxRevolvingDelay = (typeof opts.josk.maxRevolvingDelay === 'number') ? opts.josk.maxRevolvingDelay : 2048;
|
|
854
|
+
this.josk.zombieTime = (typeof opts.josk.zombieTime === 'number') ? opts.josk.zombieTime : 32786;
|
|
310
855
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
856
|
+
this.scheduler = new josk.JoSk({
|
|
857
|
+
debug: this.debug,
|
|
858
|
+
...this.josk,
|
|
859
|
+
});
|
|
314
860
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
861
|
+
process.nextTick(async () => {
|
|
862
|
+
if ((await this.scheduler.ping()).status !== 'OK') {
|
|
863
|
+
throw new Error('[mail-time] [MailTime#ping] can not connect to storage, make sure it is available and properly configured');
|
|
864
|
+
}
|
|
865
|
+
});
|
|
318
866
|
|
|
319
|
-
|
|
320
|
-
_mailOpts.from = this.from(transport);
|
|
867
|
+
this.scheduler.setInterval(this.___iterate.bind(this), this.revolvingInterval, `mailTimeQueue${this.prefix}`);
|
|
321
868
|
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
static get Template() {
|
|
872
|
+
return DEFAULT_TEMPLATE;
|
|
873
|
+
}
|
|
322
874
|
|
|
323
|
-
|
|
875
|
+
static set Template(newVal) {
|
|
876
|
+
DEFAULT_TEMPLATE = newVal;
|
|
324
877
|
}
|
|
325
878
|
|
|
326
879
|
/**
|
|
880
|
+
* @async
|
|
327
881
|
* @memberOf MailTime
|
|
328
|
-
* @name
|
|
329
|
-
* @
|
|
330
|
-
* @returns {
|
|
882
|
+
* @name ping
|
|
883
|
+
* @description Check package readiness and connection to Storage
|
|
884
|
+
* @returns {Promise<object>}
|
|
885
|
+
* @throws {mix}
|
|
331
886
|
*/
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
$lt: this.maxTries
|
|
340
|
-
}
|
|
341
|
-
}, {
|
|
342
|
-
projection: {
|
|
343
|
-
_id: 1,
|
|
344
|
-
tries: 1,
|
|
345
|
-
template: 1,
|
|
346
|
-
transport: 1,
|
|
347
|
-
mailOptions: 1,
|
|
348
|
-
concatSubject: 1
|
|
349
|
-
}
|
|
350
|
-
});
|
|
351
|
-
|
|
352
|
-
cursor.forEach(async (task) => {
|
|
353
|
-
try {
|
|
354
|
-
await this.collection.updateOne({
|
|
355
|
-
_id: task._id
|
|
356
|
-
}, {
|
|
357
|
-
$set: {
|
|
358
|
-
isSent: true
|
|
359
|
-
},
|
|
360
|
-
$inc: {
|
|
361
|
-
tries: 1
|
|
362
|
-
}
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
let transport;
|
|
366
|
-
let transportIndex;
|
|
367
|
-
if (this.strategy === 'balancer') {
|
|
368
|
-
this.transport = this.transport + 1;
|
|
369
|
-
if (this.transport >= this.transports.length) {
|
|
370
|
-
this.transport = 0;
|
|
371
|
-
}
|
|
372
|
-
transportIndex = this.transport;
|
|
373
|
-
transport = this.transports[transportIndex];
|
|
374
|
-
} else {
|
|
375
|
-
transportIndex = task.transport;
|
|
376
|
-
transport = this.transports[transportIndex];
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const _mailOpts = this.___compileMailOpts(transport, task);
|
|
380
|
-
|
|
381
|
-
_debug(this.debug, '[sendMail] [sending] To:', _mailOpts.to);
|
|
382
|
-
transport.sendMail(_mailOpts, (error, info) => {
|
|
383
|
-
if (error) {
|
|
384
|
-
this.___handleError(task, error, info);
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (info.accepted && !info.accepted.length) {
|
|
389
|
-
this.___handleError(task, 'Message not accepted or Greeting never received', info);
|
|
390
|
-
return;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
_debug(this.debug, `email successfully sent, attempts: #${task.tries}, transport #${transportIndex} to: `, _mailOpts.to);
|
|
394
|
-
|
|
395
|
-
if (!this.keepHistory) {
|
|
396
|
-
this.collection.deleteOne({
|
|
397
|
-
_id: task._id
|
|
398
|
-
}).catch(mongoErrorHandler);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
this.___triggerCallbacks(void 0, task, info);
|
|
402
|
-
return;
|
|
403
|
-
});
|
|
404
|
-
} catch (e) {
|
|
405
|
-
_logError('Exception during runtime:', e);
|
|
406
|
-
this.___handleError(task, e, {});
|
|
407
|
-
}
|
|
408
|
-
}).catch((forEachError) => {
|
|
409
|
-
_logError('[___send] [forEach] [forEachError]', forEachError);
|
|
410
|
-
}).finally(() => {
|
|
411
|
-
ready();
|
|
412
|
-
cursor.close();
|
|
413
|
-
});
|
|
887
|
+
async ping() {
|
|
888
|
+
this._debug('[ping]');
|
|
889
|
+
const schedulerPing = await this.scheduler.ping();
|
|
890
|
+
if (schedulerPing.status !== 'OK') {
|
|
891
|
+
return schedulerPing;
|
|
892
|
+
}
|
|
893
|
+
return await this.queue.ping();
|
|
414
894
|
}
|
|
415
895
|
|
|
416
896
|
/**
|
|
417
897
|
* @memberOf MailTime
|
|
418
898
|
* @name send
|
|
419
899
|
* @description alias of `sendMail`
|
|
420
|
-
* @returns {
|
|
900
|
+
* @returns {Promise<string>} uuid of the email
|
|
421
901
|
*/
|
|
422
|
-
send(opts
|
|
423
|
-
this.
|
|
902
|
+
async send(opts) {
|
|
903
|
+
this._debug('[send]', opts);
|
|
904
|
+
return await this.sendMail(opts);
|
|
424
905
|
}
|
|
425
906
|
|
|
426
907
|
/**
|
|
908
|
+
* @async
|
|
427
909
|
* @memberOf MailTime
|
|
428
910
|
* @name sendMail
|
|
429
|
-
* @
|
|
430
|
-
* @param opts
|
|
431
|
-
* @param opts.
|
|
432
|
-
* @param opts
|
|
433
|
-
* @param
|
|
434
|
-
* @returns {
|
|
911
|
+
* @description add email to the queue or append to existing letter if {concatEmails: true}
|
|
912
|
+
* @param opts {object} - email options
|
|
913
|
+
* @param opts.sendAt {Date|number} - When email should be sent
|
|
914
|
+
* @param opts.template {string} - Email-specific template
|
|
915
|
+
* @param opts[key] {mix} - Other NodeMailer's options
|
|
916
|
+
* @returns {Promise<string>} uuid of the email
|
|
917
|
+
* @throws {Error}
|
|
435
918
|
*/
|
|
436
|
-
sendMail(opts = {}
|
|
437
|
-
_debug(
|
|
919
|
+
async sendMail(opts = {}) {
|
|
920
|
+
this._debug('[sendMail]', opts);
|
|
438
921
|
if (!opts.html && !opts.text) {
|
|
439
922
|
throw new Error('`html` nor `text` field is presented, at least one of those fields is required');
|
|
440
923
|
}
|
|
441
924
|
|
|
442
|
-
|
|
443
|
-
|
|
925
|
+
let sendAt = opts.sendAt;
|
|
926
|
+
if (!sendAt) {
|
|
927
|
+
sendAt = Date.now();
|
|
444
928
|
}
|
|
445
929
|
|
|
446
|
-
if (
|
|
447
|
-
|
|
930
|
+
if (sendAt instanceof Date) {
|
|
931
|
+
sendAt = +sendAt;
|
|
448
932
|
}
|
|
449
933
|
|
|
450
|
-
if (typeof
|
|
451
|
-
|
|
934
|
+
if (typeof sendAt !== 'number') {
|
|
935
|
+
sendAt = Date.now();
|
|
452
936
|
}
|
|
453
937
|
|
|
454
|
-
let
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
delete opts.template;
|
|
459
|
-
delete opts.concatSubject;
|
|
938
|
+
let template = opts.template;
|
|
939
|
+
if (typeof template !== 'string') {
|
|
940
|
+
template = false;
|
|
941
|
+
}
|
|
460
942
|
|
|
461
|
-
|
|
943
|
+
let concatSubject = opts.concatSubject;
|
|
944
|
+
if (typeof concatSubject !== 'string') {
|
|
945
|
+
concatSubject = false;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const mailOptions = { ...opts };
|
|
949
|
+
delete mailOptions.sendAt;
|
|
950
|
+
delete mailOptions.template;
|
|
951
|
+
delete mailOptions.concatSubject;
|
|
952
|
+
|
|
953
|
+
if (typeof mailOptions.to !== 'string' && (!(mailOptions.to instanceof Array) || !mailOptions.to.length)) {
|
|
462
954
|
throw new Error('[mail-time] `mailOptions.to` is required and must be a string or non-empty Array');
|
|
463
955
|
}
|
|
464
956
|
|
|
465
957
|
if (this.concatEmails) {
|
|
466
|
-
|
|
467
|
-
this.
|
|
468
|
-
to: opts.to,
|
|
469
|
-
isSent: false,
|
|
470
|
-
sendAt: {
|
|
471
|
-
$lte: _sendAt
|
|
472
|
-
}
|
|
473
|
-
}, {
|
|
474
|
-
projection: {
|
|
475
|
-
_id: 1,
|
|
476
|
-
mailOptions: 1
|
|
477
|
-
}
|
|
478
|
-
}).then((task) => {
|
|
479
|
-
if (task) {
|
|
480
|
-
const queue = task.mailOptions || [];
|
|
481
|
-
|
|
482
|
-
for (let i = 0; i < queue.length; i++) {
|
|
483
|
-
if (equals(queue[i], opts)) {
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
958
|
+
sendAt = sendAt + this.concatDelay;
|
|
959
|
+
const task = await this.queue.getPendingTo(mailOptions.to, sendAt);
|
|
487
960
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
_id: task._id
|
|
491
|
-
}, {
|
|
492
|
-
$set: {
|
|
493
|
-
mailOptions: queue
|
|
494
|
-
}
|
|
495
|
-
}).then(() => {
|
|
496
|
-
const _id = task._id.toHexString();
|
|
497
|
-
if (!this.callbacks[_id]) {
|
|
498
|
-
this.callbacks[_id] = [];
|
|
499
|
-
}
|
|
500
|
-
this.callbacks[_id].push(callback);
|
|
501
|
-
}).catch(mongoErrorHandler);
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
961
|
+
if (task) {
|
|
962
|
+
const pendingMailOptions = task.mailOptions || [];
|
|
504
963
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
}, callback);
|
|
511
|
-
}).catch(mongoErrorHandler);
|
|
964
|
+
for (let i = 0; i < pendingMailOptions.length; i++) {
|
|
965
|
+
if (equals(pendingMailOptions[i], mailOptions)) {
|
|
966
|
+
return task.uuid;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
512
969
|
|
|
513
|
-
|
|
970
|
+
pendingMailOptions.push(opts);
|
|
971
|
+
await this.queue.update(task, {
|
|
972
|
+
mailOptions: pendingMailOptions
|
|
973
|
+
});
|
|
974
|
+
return task.uuid;
|
|
975
|
+
}
|
|
514
976
|
}
|
|
515
977
|
|
|
516
|
-
this.___addToQueue({
|
|
517
|
-
sendAt
|
|
518
|
-
template
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
978
|
+
return await this.___addToQueue({
|
|
979
|
+
sendAt,
|
|
980
|
+
template,
|
|
981
|
+
concatSubject,
|
|
982
|
+
mailOptions: mailOptions,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/**
|
|
987
|
+
* @memberOf MailTime
|
|
988
|
+
* @name cancel
|
|
989
|
+
* @description alias of `cancelMail`
|
|
990
|
+
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found was sent or was cancelled previously
|
|
991
|
+
*/
|
|
992
|
+
async cancel(uuid) {
|
|
993
|
+
this._debug('[cancel]', uuid);
|
|
994
|
+
return await this.cancelMail(uuid);
|
|
995
|
+
}
|
|
522
996
|
|
|
523
|
-
|
|
997
|
+
/**
|
|
998
|
+
* @async
|
|
999
|
+
* @memberOf MailTime
|
|
1000
|
+
* @name cancelMail
|
|
1001
|
+
* @description remove email from the queue or mark as `isCancelled`
|
|
1002
|
+
* @param uuid {string|Promise<string>} - uuid returned from `send` or `sendMail`
|
|
1003
|
+
* @returns {Promise<boolean>} returns `true` if cancelled or `false` if not found was sent or was cancelled previously
|
|
1004
|
+
*/
|
|
1005
|
+
async cancelMail(uuid) {
|
|
1006
|
+
this._debug('[cancelMail]', uuid);
|
|
1007
|
+
if (typeof uuid === 'object' && uuid instanceof Promise) {
|
|
1008
|
+
return await this.queue.cancel(await uuid);
|
|
1009
|
+
}
|
|
1010
|
+
return await this.queue.cancel(uuid);
|
|
524
1011
|
}
|
|
525
1012
|
|
|
526
1013
|
/**
|
|
1014
|
+
* @async
|
|
527
1015
|
* @memberOf MailTime
|
|
528
1016
|
* @name ___handleError
|
|
529
|
-
* @
|
|
530
|
-
* @param
|
|
531
|
-
* @param
|
|
532
|
-
* @
|
|
1017
|
+
* @description Handle runtime errors and pass to `onError` callback
|
|
1018
|
+
* @param task {object} - Email task record form Storage
|
|
1019
|
+
* @param error {mix} - Error String/Object/Error
|
|
1020
|
+
* @param info {object} - Info object returned from nodemailer
|
|
1021
|
+
* @returns {Promise<void 0>}
|
|
533
1022
|
*/
|
|
534
|
-
___handleError(task, error, info) {
|
|
1023
|
+
async ___handleError(task, error, info) {
|
|
1024
|
+
this._debug('[private handleError]', { task, error, info });
|
|
535
1025
|
if (!task) {
|
|
536
1026
|
return;
|
|
537
1027
|
}
|
|
538
1028
|
|
|
539
1029
|
if (task.tries >= this.maxTries) {
|
|
1030
|
+
task.isSent = false;
|
|
1031
|
+
task.isFailed = true;
|
|
1032
|
+
|
|
540
1033
|
if (!this.keepHistory) {
|
|
541
|
-
this.
|
|
542
|
-
|
|
543
|
-
|
|
1034
|
+
await this.queue.remove(task);
|
|
1035
|
+
} else {
|
|
1036
|
+
await this.queue.update(task, {
|
|
1037
|
+
isSent: task.isSent,
|
|
1038
|
+
isFailed: task.isFailed,
|
|
1039
|
+
});
|
|
544
1040
|
}
|
|
545
1041
|
|
|
546
|
-
_debug(
|
|
547
|
-
this.
|
|
1042
|
+
this._debug(`[private handleError] Giving up trying send email after ${task.tries} attempts to: `, task.mailOptions[0].to, error);
|
|
1043
|
+
this.onError(error, task, info);
|
|
548
1044
|
return;
|
|
549
1045
|
}
|
|
550
1046
|
|
|
@@ -557,61 +1053,57 @@ class MailTime {
|
|
|
557
1053
|
}
|
|
558
1054
|
}
|
|
559
1055
|
|
|
560
|
-
this.
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
sendAt: new Date(Date.now() + this.interval),
|
|
566
|
-
transport: transportIndex
|
|
567
|
-
}
|
|
568
|
-
}).catch(mongoErrorHandler);
|
|
1056
|
+
await this.queue.update(task, {
|
|
1057
|
+
isSent: false,
|
|
1058
|
+
sendAt: Date.now() + this.retryDelay,
|
|
1059
|
+
transport: transportIndex,
|
|
1060
|
+
});
|
|
569
1061
|
|
|
570
|
-
_debug(
|
|
1062
|
+
this._debug(`[private handleError] Next re-send attempt at ${new Date(Date.now() + this.retryDelay)}: #${task.tries}/${this.maxTries}, transport #${transportIndex} to: `, task.mailOptions[0].to, error);
|
|
571
1063
|
}
|
|
572
1064
|
|
|
573
1065
|
/**
|
|
1066
|
+
* @async
|
|
574
1067
|
* @memberOf MailTime
|
|
575
1068
|
* @name ___addToQueue
|
|
576
|
-
* @
|
|
577
|
-
* @param opts
|
|
578
|
-
* @param opts.
|
|
579
|
-
* @param opts.
|
|
580
|
-
* @param opts.
|
|
581
|
-
* @param
|
|
582
|
-
* @returns {
|
|
1069
|
+
* @description Prepare task's object and push to the queue
|
|
1070
|
+
* @param opts {object} - Email options with next properties:
|
|
1071
|
+
* @param opts.sendAt {number} - When email should be sent
|
|
1072
|
+
* @param opts.template {string} - Email-specific template
|
|
1073
|
+
* @param opts.mailOptions {object} - MailOptions according to NodeMailer lib
|
|
1074
|
+
* @param opts.concatSubject {string} - Email subject used when sending concatenated email
|
|
1075
|
+
* @returns {Promise<string>} message uuid
|
|
583
1076
|
*/
|
|
584
|
-
___addToQueue(opts
|
|
585
|
-
_debug(
|
|
1077
|
+
async ___addToQueue(opts) {
|
|
1078
|
+
this._debug('[private addToQueue]', opts);
|
|
586
1079
|
const task = {
|
|
1080
|
+
uuid: crypto.randomUUID(),
|
|
587
1081
|
tries: 0,
|
|
588
1082
|
isSent: false,
|
|
589
1083
|
sendAt: opts.sendAt,
|
|
1084
|
+
isFailed: false,
|
|
590
1085
|
template: opts.template,
|
|
591
1086
|
transport: this.transport,
|
|
1087
|
+
isCancelled: false,
|
|
592
1088
|
mailOptions: [opts.mailOptions],
|
|
593
|
-
concatSubject: opts.concatSubject
|
|
1089
|
+
concatSubject: opts.concatSubject,
|
|
594
1090
|
};
|
|
595
1091
|
|
|
596
1092
|
if (this.concatEmails) {
|
|
597
1093
|
task.to = opts.mailOptions.to;
|
|
598
1094
|
}
|
|
599
1095
|
|
|
600
|
-
this.
|
|
601
|
-
|
|
602
|
-
if (!this.callbacks[_id]) {
|
|
603
|
-
this.callbacks[_id] = [];
|
|
604
|
-
}
|
|
605
|
-
this.callbacks[_id].push(callback);
|
|
606
|
-
}).catch(mongoErrorHandler);
|
|
1096
|
+
await this.queue.push(task);
|
|
1097
|
+
return task.uuid;
|
|
607
1098
|
}
|
|
608
1099
|
|
|
609
1100
|
/**
|
|
610
1101
|
* @memberOf MailTime
|
|
611
1102
|
* @name ___render
|
|
612
|
-
* @
|
|
613
|
-
* @param
|
|
614
|
-
* @
|
|
1103
|
+
* @description Render templates
|
|
1104
|
+
* @param string {string} - Template with Mustache-like placeholders
|
|
1105
|
+
* @param replacements {object} - Blaze/Mustache-like helpers Object
|
|
1106
|
+
* @returns {string}
|
|
615
1107
|
*/
|
|
616
1108
|
___render(_string, replacements) {
|
|
617
1109
|
let i;
|
|
@@ -638,21 +1130,156 @@ class MailTime {
|
|
|
638
1130
|
|
|
639
1131
|
/**
|
|
640
1132
|
* @memberOf MailTime
|
|
641
|
-
* @name
|
|
642
|
-
* @
|
|
643
|
-
* @param
|
|
644
|
-
* @param info
|
|
1133
|
+
* @name ___compileMailOpts
|
|
1134
|
+
* @description Run various checks, compile options, and render template
|
|
1135
|
+
* @param transport {object} - Current transport
|
|
1136
|
+
* @param info {object} - Info object returned from NodeMailer's `sendMail` method
|
|
645
1137
|
* @returns {void 0}
|
|
646
1138
|
*/
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
1139
|
+
___compileMailOpts(transport, task) {
|
|
1140
|
+
let compiledOpts = {};
|
|
1141
|
+
|
|
1142
|
+
if (!transport) {
|
|
1143
|
+
throw new Error('[mail-time] [sendMail] [___compileMailOpts] {transport} is not available or misconfigured!');
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (transport._options && typeof transport._options === 'object' && transport._options !== null && transport._options.mailOptions) {
|
|
1147
|
+
compiledOpts = merge(compiledOpts, transport._options.mailOptions);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (transport.options && typeof transport.options === 'object' && transport.options !== null && transport.options.mailOptions) {
|
|
1151
|
+
compiledOpts = merge(compiledOpts, transport.options.mailOptions);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
compiledOpts = merge(compiledOpts, {
|
|
1155
|
+
html: '',
|
|
1156
|
+
text: '',
|
|
1157
|
+
subject: ''
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
for (let i = 0; i < task.mailOptions.length; i++) {
|
|
1161
|
+
if (task.mailOptions[i].html) {
|
|
1162
|
+
if (task.mailOptions.length > 1) {
|
|
1163
|
+
compiledOpts.html += this.___render(this.concatDelimiter, task.mailOptions[i]) + this.___render(task.mailOptions[i].html, task.mailOptions[i]);
|
|
1164
|
+
} else {
|
|
1165
|
+
compiledOpts.html = this.___render(task.mailOptions[i].html, task.mailOptions[i]);
|
|
1166
|
+
}
|
|
1167
|
+
delete task.mailOptions[i].html;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (task.mailOptions[i].text) {
|
|
1171
|
+
if (task.mailOptions.length > 1) {
|
|
1172
|
+
compiledOpts.text += '\r\n' + this.___render(task.mailOptions[i].text, task.mailOptions[i]);
|
|
1173
|
+
} else {
|
|
1174
|
+
compiledOpts.text = this.___render(task.mailOptions[i].text, task.mailOptions[i]);
|
|
1175
|
+
}
|
|
1176
|
+
delete task.mailOptions[i].text;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
compiledOpts = merge(compiledOpts, task.mailOptions[i]);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
if (compiledOpts.html && (task.template || this.template)) {
|
|
1183
|
+
compiledOpts.html = this.___render((task.template || this.template), compiledOpts);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
if (task.mailOptions.length > 1) {
|
|
1187
|
+
compiledOpts.subject = task.concatSubject || this.concatSubject || compiledOpts.subject;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (!compiledOpts.from && this.from) {
|
|
1191
|
+
compiledOpts.from = this.from(transport);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return compiledOpts;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/**
|
|
1198
|
+
* @async
|
|
1199
|
+
* @memberOf MailTime
|
|
1200
|
+
* @name ___send
|
|
1201
|
+
* @description send email using nodemailer's transport
|
|
1202
|
+
* @param task {object} - email's task object from Storage
|
|
1203
|
+
* @returns {Promise<void 0>}
|
|
1204
|
+
*/
|
|
1205
|
+
async ___send(task) {
|
|
1206
|
+
this._debug('[private send]', task);
|
|
1207
|
+
try {
|
|
1208
|
+
if (task.isSent === true || task.isFailed === true || task.isCancelled === true) {
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
task.tries++;
|
|
1213
|
+
const isUpdated = await this.queue.update(task, {
|
|
1214
|
+
isSent: true,
|
|
1215
|
+
tries: task.tries
|
|
652
1216
|
});
|
|
1217
|
+
|
|
1218
|
+
if (!isUpdated) {
|
|
1219
|
+
logError('[private send] [queue.update] Not updated!');
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
let transport;
|
|
1224
|
+
let transportIndex;
|
|
1225
|
+
if (this.strategy === 'balancer') {
|
|
1226
|
+
this.transport = this.transport + 1;
|
|
1227
|
+
if (this.transport >= this.transports.length) {
|
|
1228
|
+
this.transport = 0;
|
|
1229
|
+
}
|
|
1230
|
+
transportIndex = this.transport;
|
|
1231
|
+
transport = this.transports[transportIndex];
|
|
1232
|
+
} else {
|
|
1233
|
+
transportIndex = task.transport;
|
|
1234
|
+
transport = this.transports[transportIndex];
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
const compiledOpts = this.___compileMailOpts(transport, task);
|
|
1238
|
+
|
|
1239
|
+
await new Promise((resolve) => {
|
|
1240
|
+
transport.sendMail(compiledOpts, async (error, info) => {
|
|
1241
|
+
this._debug('[private send] [sending]', { error, info });
|
|
1242
|
+
if (error) {
|
|
1243
|
+
await this.___handleError(task, error, info);
|
|
1244
|
+
resolve();
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (info.accepted && !info.accepted.length) {
|
|
1249
|
+
await this.___handleError(task, new Error('Message not accepted or Greeting never received'), info);
|
|
1250
|
+
resolve();
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
this._debug(`email successfully sent, attempts: #${task.tries}, transport #${transportIndex} to: `, compiledOpts.to);
|
|
1255
|
+
|
|
1256
|
+
if (!this.keepHistory) {
|
|
1257
|
+
await this.queue.remove(task);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
task.isSent = true;
|
|
1261
|
+
this.onSent(task, info);
|
|
1262
|
+
resolve();
|
|
1263
|
+
});
|
|
1264
|
+
});
|
|
1265
|
+
} catch (e) {
|
|
1266
|
+
logError('Exception during runtime:', e);
|
|
1267
|
+
this.___handleError(task, e, {});
|
|
653
1268
|
}
|
|
654
|
-
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* @memberOf MailTime
|
|
1273
|
+
* @name ___iterate
|
|
1274
|
+
* @description Iterate over queued tasks
|
|
1275
|
+
* @returns {Promise}
|
|
1276
|
+
*/
|
|
1277
|
+
async ___iterate() {
|
|
1278
|
+
this._debug('[private iterate]');
|
|
1279
|
+
return await this.queue.iterate();
|
|
655
1280
|
}
|
|
656
1281
|
}
|
|
657
1282
|
|
|
658
|
-
|
|
1283
|
+
exports.MailTime = MailTime;
|
|
1284
|
+
exports.MongoQueue = MongoQueue;
|
|
1285
|
+
exports.RedisQueue = RedisQueue;
|