mail-time 1.3.4 → 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/index.cjs ADDED
@@ -0,0 +1,1285 @@
1
+ 'use strict';
2
+
3
+ const josk = require('josk');
4
+ const merge = require('deepmerge');
5
+ const crypto = require('crypto');
6
+
7
+ const debug = (isDebug, ...args) => {
8
+ if (isDebug) {
9
+ console.info.call(console, '[DEBUG] [mail-time]', `${new Date}`, ...args);
10
+ }
11
+ };
12
+
13
+ const logError = (...args) => {
14
+ console.error.call(console, '[ERROR] [mail-time]', `${new Date}`, ...args);
15
+ };
16
+
17
+ /**
18
+ * Ensure (create) index on MongoDB collection, catch and log exception if thrown
19
+ * @function ensureIndex
20
+ * @param {Collection} collection - Mongo's driver Collection instance
21
+ * @param {object} keys - Field and value pairs where the field is the index key and the value describes the type of index for that field
22
+ * @param {object} opts - Set of options that controls the creation of the index
23
+ * @returns {void 0}
24
+ */
25
+ const ensureIndex = async (collection, keys, opts) => {
26
+ try {
27
+ await collection.createIndex(keys, opts);
28
+ } catch (e) {
29
+ if (e.code === 85) {
30
+ let indexName;
31
+ const indexes = await collection.indexes();
32
+ for (const index of indexes) {
33
+ let drop = true;
34
+ for (const indexKey of Object.keys(keys)) {
35
+ if (typeof index.key[indexKey] === 'undefined') {
36
+ drop = false;
37
+ break;
38
+ }
39
+ }
40
+
41
+ for (const indexKey of Object.keys(index.key)) {
42
+ if (typeof keys[indexKey] === 'undefined') {
43
+ drop = false;
44
+ break;
45
+ }
46
+ }
47
+
48
+ if (drop) {
49
+ indexName = index.name;
50
+ break;
51
+ }
52
+ }
53
+
54
+ if (indexName) {
55
+ await collection.dropIndex(indexName);
56
+ await collection.createIndex(keys, opts);
57
+ }
58
+ } else {
59
+ logError(`[ensureIndex] Can not set ${Object.keys(keys).join(' + ')} index on "${collection._name || 'MongoDB'}" collection`, { keys, opts, details: e });
60
+ }
61
+ }
62
+ };
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
+
625
+ /**
626
+ * Check if entities of various types are equal
627
+ * including edge cases like unordered Objects and Array
628
+ * @function equals
629
+ * @param {mix} a
630
+ * @param {mix} b
631
+ * @returns {boolean}
632
+ */
633
+ const equals = (a, b) => {
634
+ let i;
635
+ if (a === b) {
636
+ return true;
637
+ }
638
+
639
+ if (!a || !b) {
640
+ return false;
641
+ }
642
+
643
+ if (!(typeof a === 'object' && typeof b === 'object')) {
644
+ return false;
645
+ }
646
+
647
+ if (a instanceof Date && b instanceof Date) {
648
+ return a.valueOf() === b.valueOf();
649
+ }
650
+
651
+ if (a instanceof Array) {
652
+ if (!(b instanceof Array)) {
653
+ return false;
654
+ }
655
+
656
+ if (a.length !== b.length) {
657
+ return false;
658
+ }
659
+
660
+ const _a = a.slice();
661
+ const _b = b.slice();
662
+ let j;
663
+ for (i = _a.length - 1; i >= 0; i--) {
664
+ let result = false;
665
+ for (j = _b.length - 1; j >= 0; j--) {
666
+ if (equals(_a[i], _b[j])) {
667
+ result = true;
668
+ _a.splice(i, 1);
669
+ _b.splice(j, 1);
670
+ break;
671
+ }
672
+ }
673
+
674
+ if (!result) {
675
+ return false;
676
+ }
677
+ }
678
+ return true;
679
+ }
680
+
681
+ i = 0;
682
+ if (typeof a === 'object' && typeof b === 'object') {
683
+ const akeys = Object.keys(a);
684
+ const bkeys = Object.keys(b);
685
+
686
+ if (akeys.length !== bkeys.length) {
687
+ return false;
688
+ }
689
+
690
+ for (i = akeys.length - 1; i >= 0; i--) {
691
+ if (!hasOwnProperty.call(b, akeys[i])) {
692
+ return false;
693
+ }
694
+
695
+ if (!equals(a[akeys[i]], b[akeys[i]])) {
696
+ return false;
697
+ }
698
+ }
699
+
700
+ return true;
701
+ }
702
+ return false;
703
+ };
704
+
705
+ 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>';
706
+
707
+ /** Class of MailTime */
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
+ */
730
+ constructor (opts) {
731
+ if (!opts || typeof opts !== 'object' || opts === null) {
732
+ throw new TypeError('[mail-time] Configuration object must be passed into MailTime constructor');
733
+ }
734
+
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
+ }
749
+ }
750
+
751
+ this.debug = (opts.debug !== true) ? false : true;
752
+ this._debug = (...args) => {
753
+ debug(this.debug, ...args);
754
+ };
755
+
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 : '';
758
+
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;
765
+ }
766
+
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;
773
+ }
774
+
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;
783
+ this.strategy = (opts.strategy === 'backup' || opts.strategy === 'balancer') ? opts.strategy : 'backup';
784
+ this.transports = opts.transports || [];
785
+ this.transport = 0;
786
+ this.from = (() => {
787
+ if (typeof opts.from === 'string') {
788
+ return () => {
789
+ return opts.from;
790
+ };
791
+ }
792
+
793
+ if (typeof opts.from === 'function') {
794
+ return opts.from;
795
+ }
796
+
797
+ return false;
798
+ })();
799
+
800
+ this.concatEmails = (opts.concatEmails !== true) ? false : true;
801
+ this.concatSubject = (opts.concatSubject && typeof opts.concatSubject === 'string') ? opts.concatSubject : 'Multiple notifications';
802
+ this.concatDelimiter = (opts.concatDelimiter && typeof opts.concatDelimiter === 'string') ? opts.concatDelimiter : '<hr>';
803
+
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;
810
+ }
811
+
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 */
823
+ if (this.type === 'server') {
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
+ }
827
+
828
+ if (typeof opts.josk !== 'object') {
829
+ throw new Error('[mail-time] {josk} option is required {object} for {type: "server"}');
830
+ }
831
+
832
+ if (typeof opts.josk.adapter !== 'object') {
833
+ throw new Error('[mail-time] {josk.adapter} option is required {object} *or* custom adapter Class');
834
+ }
835
+
836
+ this.josk = { ...opts.josk };
837
+
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"}');
841
+ }
842
+ this.josk.adapter = new josk.MongoAdapter({ prefix: `mailTimeQueue${this.prefix}`, ...opts.josk.adapter });
843
+ }
844
+
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"}');
848
+ }
849
+ this.josk.adapter = new josk.RedisAdapter({ prefix: `mailTimeQueue${this.prefix}`, ...opts.josk.adapter });
850
+ }
851
+
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;
855
+
856
+ this.scheduler = new josk.JoSk({
857
+ debug: this.debug,
858
+ ...this.josk,
859
+ });
860
+
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
+ });
866
+
867
+ this.scheduler.setInterval(this.___iterate.bind(this), this.revolvingInterval, `mailTimeQueue${this.prefix}`);
868
+ }
869
+ }
870
+
871
+ static get Template() {
872
+ return DEFAULT_TEMPLATE;
873
+ }
874
+
875
+ static set Template(newVal) {
876
+ DEFAULT_TEMPLATE = newVal;
877
+ }
878
+
879
+ /**
880
+ * @async
881
+ * @memberOf MailTime
882
+ * @name ping
883
+ * @description Check package readiness and connection to Storage
884
+ * @returns {Promise<object>}
885
+ * @throws {mix}
886
+ */
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();
894
+ }
895
+
896
+ /**
897
+ * @memberOf MailTime
898
+ * @name send
899
+ * @description alias of `sendMail`
900
+ * @returns {Promise<string>} uuid of the email
901
+ */
902
+ async send(opts) {
903
+ this._debug('[send]', opts);
904
+ return await this.sendMail(opts);
905
+ }
906
+
907
+ /**
908
+ * @async
909
+ * @memberOf MailTime
910
+ * @name sendMail
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}
918
+ */
919
+ async sendMail(opts = {}) {
920
+ this._debug('[sendMail]', opts);
921
+ if (!opts.html && !opts.text) {
922
+ throw new Error('`html` nor `text` field is presented, at least one of those fields is required');
923
+ }
924
+
925
+ let sendAt = opts.sendAt;
926
+ if (!sendAt) {
927
+ sendAt = Date.now();
928
+ }
929
+
930
+ if (sendAt instanceof Date) {
931
+ sendAt = +sendAt;
932
+ }
933
+
934
+ if (typeof sendAt !== 'number') {
935
+ sendAt = Date.now();
936
+ }
937
+
938
+ let template = opts.template;
939
+ if (typeof template !== 'string') {
940
+ template = false;
941
+ }
942
+
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)) {
954
+ throw new Error('[mail-time] `mailOptions.to` is required and must be a string or non-empty Array');
955
+ }
956
+
957
+ if (this.concatEmails) {
958
+ sendAt = sendAt + this.concatDelay;
959
+ const task = await this.queue.getPendingTo(mailOptions.to, sendAt);
960
+
961
+ if (task) {
962
+ const pendingMailOptions = task.mailOptions || [];
963
+
964
+ for (let i = 0; i < pendingMailOptions.length; i++) {
965
+ if (equals(pendingMailOptions[i], mailOptions)) {
966
+ return task.uuid;
967
+ }
968
+ }
969
+
970
+ pendingMailOptions.push(opts);
971
+ await this.queue.update(task, {
972
+ mailOptions: pendingMailOptions
973
+ });
974
+ return task.uuid;
975
+ }
976
+ }
977
+
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
+ }
996
+
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);
1011
+ }
1012
+
1013
+ /**
1014
+ * @async
1015
+ * @memberOf MailTime
1016
+ * @name ___handleError
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>}
1022
+ */
1023
+ async ___handleError(task, error, info) {
1024
+ this._debug('[private handleError]', { task, error, info });
1025
+ if (!task) {
1026
+ return;
1027
+ }
1028
+
1029
+ if (task.tries >= this.maxTries) {
1030
+ task.isSent = false;
1031
+ task.isFailed = true;
1032
+
1033
+ if (!this.keepHistory) {
1034
+ await this.queue.remove(task);
1035
+ } else {
1036
+ await this.queue.update(task, {
1037
+ isSent: task.isSent,
1038
+ isFailed: task.isFailed,
1039
+ });
1040
+ }
1041
+
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);
1044
+ return;
1045
+ }
1046
+
1047
+ let transportIndex = task.transport;
1048
+
1049
+ if (this.strategy === 'backup' && this.transports.length > 1 && (task.tries % this.failsToNext) === 0) {
1050
+ ++transportIndex;
1051
+ if (transportIndex > this.transports.length - 1) {
1052
+ transportIndex = 0;
1053
+ }
1054
+ }
1055
+
1056
+ await this.queue.update(task, {
1057
+ isSent: false,
1058
+ sendAt: Date.now() + this.retryDelay,
1059
+ transport: transportIndex,
1060
+ });
1061
+
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);
1063
+ }
1064
+
1065
+ /**
1066
+ * @async
1067
+ * @memberOf MailTime
1068
+ * @name ___addToQueue
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
1076
+ */
1077
+ async ___addToQueue(opts) {
1078
+ this._debug('[private addToQueue]', opts);
1079
+ const task = {
1080
+ uuid: crypto.randomUUID(),
1081
+ tries: 0,
1082
+ isSent: false,
1083
+ sendAt: opts.sendAt,
1084
+ isFailed: false,
1085
+ template: opts.template,
1086
+ transport: this.transport,
1087
+ isCancelled: false,
1088
+ mailOptions: [opts.mailOptions],
1089
+ concatSubject: opts.concatSubject,
1090
+ };
1091
+
1092
+ if (this.concatEmails) {
1093
+ task.to = opts.mailOptions.to;
1094
+ }
1095
+
1096
+ await this.queue.push(task);
1097
+ return task.uuid;
1098
+ }
1099
+
1100
+ /**
1101
+ * @memberOf MailTime
1102
+ * @name ___render
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}
1107
+ */
1108
+ ___render(_string, replacements) {
1109
+ let i;
1110
+ let string = _string;
1111
+ const matchHTML = string.match(/\{{3}\s?([a-zA-Z0-9\-\_]+)\s?\}{3}/g);
1112
+ if (matchHTML) {
1113
+ for (i = 0; i < matchHTML.length; i++) {
1114
+ if (replacements[matchHTML[i].replace('{{{', '').replace('}}}', '').trim()]) {
1115
+ string = string.replace(matchHTML[i], replacements[matchHTML[i].replace('{{{', '').replace('}}}', '').trim()]);
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ const matchStr = string.match(/\{{2}\s?([a-zA-Z0-9\-\_]+)\s?\}{2}/g);
1121
+ if (matchStr) {
1122
+ for (i = 0; i < matchStr.length; i++) {
1123
+ if (replacements[matchStr[i].replace('{{', '').replace('}}', '').trim()]) {
1124
+ string = string.replace(matchStr[i], replacements[matchStr[i].replace('{{', '').replace('}}', '').trim()].replace(/<(?:.|\n)*?>/gm, ''));
1125
+ }
1126
+ }
1127
+ }
1128
+ return string;
1129
+ }
1130
+
1131
+ /**
1132
+ * @memberOf MailTime
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
1137
+ * @returns {void 0}
1138
+ */
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
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, {});
1268
+ }
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();
1280
+ }
1281
+ }
1282
+
1283
+ exports.MailTime = MailTime;
1284
+ exports.MongoQueue = MongoQueue;
1285
+ exports.RedisQueue = RedisQueue;