mail-time 1.3.4 → 1.4.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/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2022, dr.dimitru (Dmitry A.; Veliov Group, LLC)
1
+ Copyright (c) 2024, dr.dimitru (Dmitry A.; Veliov Group, LLC)
2
2
  All rights reserved.
3
3
 
4
4
  Redistribution and use in source and binary forms,
package/README.md CHANGED
@@ -1,39 +1,38 @@
1
1
  [![support](https://img.shields.io/badge/support-GitHub-white)](https://github.com/sponsors/dr-dimitru)
2
2
  [![support](https://img.shields.io/badge/support-PayPal-white)](https://paypal.me/veliovgroup)
3
- <a href="https://ostr.io/info/built-by-developers-for-developers">
4
- <img src="https://ostr.io/apple-touch-icon-60x60.png" height="20">
5
- </a>
3
+ <a href="https://ostr.io/info/built-by-developers-for-developers?ref=github-mail-time-repo-top"><img src="https://ostr.io/apple-touch-icon-60x60.png" height="20"></a>
4
+ <a href="https://meteor-files.com/?ref=github-mail-time-repo-top"><img src="https://meteor-files.com/apple-touch-icon-60x60.png" height="20"></a>
6
5
 
7
6
  # MailTime
8
7
 
9
- "Mail-Time" is a micro-service package for mail queue, with *Server* and *Client* APIs. Build on top of the [`nodemailer`](https://github.com/nodemailer/nodemailer) package.
8
+ "Mail-Time" is a micro-service package for mail queue, with *Server* and *Client* APIs. Build on top of the [`nodemailer`](https://github.com/nodemailer/nodemailer) package. Mail-Time made for single-server and horizontally scaled multi-server setups in mind.
10
9
 
11
- Every `MailTime` instance can get `type` configured as *Server* or *Client*.
10
+ Every `MailTime` instance can have `type` configured as *Server* or *Client*.
12
11
 
13
12
  The main difference between *Server* and *Client* `type` is that the *Server* handles the queue and __sends__ email. While the *Client* only __adds__ emails into the queue.
14
13
 
15
14
  ## ToC
16
15
 
17
- - [How it works?](https://github.com/veliovgroup/mail-time#how-it-works)
18
- - [With single SMTP](https://github.com/veliovgroup/mail-time#single-point-of-failure)
19
- - [With multiple SMTP](https://github.com/veliovgroup/mail-time#multiple-smtp-providers)
20
- - [As Micro-Service](https://github.com/veliovgroup/mail-time#cluster-issue)
21
- - [Features](https://github.com/veliovgroup/mail-time#features)
22
- - [Installation](https://github.com/veliovgroup/mail-time#installation)
16
+ - [How it works?](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#how-it-works)
17
+ - [With single SMTP](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#single-point-of-failure)
18
+ - [With multiple SMTP](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#multiple-smtp-providers)
19
+ - [For Clusters](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#sending-emails-from-cluster-of-servers)
20
+ - [Features](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#features)
21
+ - [Installation](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#installation)
23
22
  - [Meteor.js usage](https://github.com/veliovgroup/mail-time/blob/master/docs/meteor.md)
24
- - [Usage example](https://github.com/veliovgroup/mail-time#basic-usage)
25
- - [API](https://github.com/veliovgroup/mail-time#api)
26
- - [*Constructor*](https://github.com/veliovgroup/mail-time#new-mailtimeopts-constructor)
27
- - [`.send()`](https://github.com/veliovgroup/mail-time#sendmailopts--callback)
28
- - [Default Template](https://github.com/veliovgroup/mail-time#static-mailtimetemplate)
29
- - [Custom Templates](https://github.com/veliovgroup/mail-time#template-example)
30
- - [~92% tests coverage](https://github.com/veliovgroup/mail-time#testing)
23
+ - [Usage example](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#basic-usage)
24
+ - [API](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#api)
25
+ - [*Constructor*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#new-mailtimeopts-constructor)
26
+ - [`.send()`](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#sendmailopts--callback)
27
+ - [Default Template](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#static-mailtimetemplate)
28
+ - [Custom Templates](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#template-example)
29
+ - [~92% tests coverage](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#testing)
31
30
 
32
31
  ## Main features:
33
32
 
34
33
  - 👨‍🔬 ~92% tests coverage;
35
34
  - 📦 Two simple dependencies, written from scratch for top performance;
36
- - 🏢 Synchronize email queue across multiple servers;
35
+ - 🏢 Synchronize email queue across multiple (horizontally scaled) servers;
37
36
  - 💪 Bulletproof design, built-in retries.
38
37
 
39
38
  ## How does it work?
@@ -42,7 +41,7 @@ Redundant solution for email transmission.
42
41
 
43
42
  ### Single point of failure
44
43
 
45
- Issue - classic solution with a single point of failure:
44
+ Issue - mitigate a single point of failure:
46
45
 
47
46
  ```ascii
48
47
  |----------------| |------| |------------------|
@@ -72,7 +71,7 @@ it will keep the letter in the queue and retry to send it again
72
71
 
73
72
  ### Multiple SMTP providers
74
73
 
75
- Backup scheme with multiple SMTP providers
74
+ Rotate or backup email transports by using multiple SMTP providers
76
75
 
77
76
  ```ascii
78
77
  |--------|
@@ -88,27 +87,45 @@ Backup scheme with multiple SMTP providers
88
87
  |--------|
89
88
  ```
90
89
 
91
- ### Cluster issue
90
+ ### Sending emails from cluster of servers
92
91
 
93
- It is common to create a "Cluster" of servers to balance the load and add a durability layer for horizontal scaling of quickly growing applications.
92
+ It is common to have horizontally scaled "Cluster" of servers for load-balancing and for durability.
94
93
 
95
- Most modern application has scheduled or recurring emails. For example, once a day — with recent news and updates. It won't be an issue with a single server setup — the server would send emails at a daily interval via timer or CRON. While in "Cluster" implementation — each server will attempt to send the same email. In such cases, users will receive multiple emails with the same content. We built MailTime to address this and other similar issues.
94
+ Most modern application has scheduled or recurring emails. For example, once a day — with recent news and updates. It won't be an issue with a single server setup — the server would send emails at a daily interval via timer or CRON. But in "Cluster" implementation — each server will attempt to send the same email. MailTime built to avoid sending the same email multiple times to a user from horizontally scaled applications.
96
95
 
97
- Here is how this issue is solved by using MailTime:
96
+ For the maximum durability and agility each Application Server can run MailTime in the "Server" mode:
97
+
98
+ ```ascii
99
+ |===================THE=CLUSTER===================| |=QUEUE=|
100
+ | |----------| |----------| |----------| | | | |--------|
101
+ | | App | | App | | App | | | |-->| SMTP 1 |------\
102
+ | | Server 1 | | Server 2 | | Server 3 | | | | |--------| \
103
+ | |-----\----| |----\-----| |----\-----| | | | |-------------|
104
+ | \---------------\----------------\----------> | |--------| | ^_^ |
105
+ | | | |-->| SMTP 2 |-->| Happy users |
106
+ | Each "App Server" or "Cluster Node" | | | |--------| |-------------|
107
+ | runs MailTime as a "Server" | | | /
108
+ | for the maximum durability | | | |--------| /
109
+ | | | |-->| SMTP 3 |-----/
110
+ | | | | |--------|
111
+ |=================================================| |=======|
112
+ ```
113
+
114
+ To split roles MailTime can run on a dedicated machine as micro-service. This case is great for private email servers with implemented authentication via rDNS and PTR records:
98
115
 
99
116
  ```ascii
100
117
  |===================THE=CLUSTER===================| |=QUEUE=| |===Mail=Time===|
101
- | |----------| |----------| |----------| | | | |=Micro-service=| |--------|
102
- | | App | | App | | App | | | | | |-->| SMTP 1 |------\
103
- | | Server 1 | | Server 2 | | Server 3 | | | <-------- | |--------| \
104
- | |-----\----| |----\-----| |----\-----| | | --------> | |-------------|
105
- | \---------------\----------------\----------> | | | |--------| | ^_^ |
106
- | Each of the "App Server" or "Cluster Node" | | | | |-->| SMTP 2 |-->| Happy users |
107
- | runs Mail Time as a Client which only puts | | | | | |--------| |-------------|
108
- | emails into the queue. Aside to "App Servers" | | | | | /
109
- | We suggest running Mail Time as a Micro-service | | | | | |--------| /
110
- | which will be responsible for making sure queue | | | | |-->| SMTP 3 |-----/
111
- | has no duplicates and to actually send emails | | | | | |--------|
118
+ | |----------| |----------| |----------| | | | | | |--------|
119
+ | | App | | App | | App | | | | | Micro-service |-->| SMTP 1 |------\
120
+ | | Server 1 | | Server 2 | | Server 3 | | | | | running | |--------| \
121
+ | |-----\----| |----\-----| |----\-----| | | | | MailTime as | |-------------|
122
+ | \---------------\----------------\----------> | | "Server" only | |--------| | ^_^ |
123
+ | | | | | sending |-->| SMTP 2 |-->| Happy users |
124
+ | Each "App Server" runs MailTime as | | | | emails | |--------| |-------------|
125
+ | a "Client" only placing emails to the queue. | | <-------- | /
126
+ | | | --------> | |--------| /
127
+ | | | | | |-->| SMTP 3 |-----/
128
+ | | | | | | |--------|
112
129
  |=================================================| |=======| |===============|
113
130
  ```
114
131
 
@@ -133,9 +150,12 @@ npm install --save nodemailer
133
150
  Install *MailTime* package:
134
151
 
135
152
  ```shell
136
- # for node@>=8.9.0
153
+ # for node@>=14.20.0
137
154
  npm install --save mail-time
138
155
 
156
+ # for node@<14.20.0
157
+ npm install --save mail-time@=1.3.4
158
+
139
159
  # for node@<8.9.0
140
160
  npm install --save mail-time@=0.1.7
141
161
  ```
@@ -145,19 +165,23 @@ npm install --save mail-time@=0.1.7
145
165
  Require package:
146
166
 
147
167
  ```js
168
+ // import as ES Module
169
+ import MailTime from 'mail-time';
170
+
171
+ // requires as CommonJS
148
172
  const MailTime = require('mail-time');
149
173
  ```
150
174
 
151
- Create nodemailer's transports (see [`nodemailer` docs](https://github.com/nodemailer/nodemailer/tree/v2#setting-up)):
175
+ Create nodemailer's transports, for details see [`nodemailer` docs](https://github.com/nodemailer/nodemailer/tree/v2#setting-up):
152
176
 
153
177
  ```js
154
- const transports = [];
155
- const nodemailer = require('nodemailer');
156
-
178
+ import nodemailer from 'nodemailer';
157
179
  // Use DIRECT transport
158
180
  // and enable sending email from localhost
159
181
  // install "nodemailer-direct-transport" NPM package:
160
- const directTransport = require('nodemailer-direct-transport');
182
+ import directTransport from 'nodemailer-direct-transport';
183
+
184
+ const transports = [];
161
185
  const directTransportOpts = {
162
186
  pool: false,
163
187
  direct: true,
@@ -426,10 +450,11 @@ DEBUG="true" EMAIL_DOMAIN="example.com" MONGO_URL="mongodb://127.0.0.1:27017/npm
426
450
 
427
451
  ## Support this project:
428
452
 
453
+ - Upload and share files using [☄️ meteor-files.com](https://meteor-files.com/?ref=github-mail-time-repo-footer) — Continue interrupted file uploads without losing any progress. There is nothing that will stop Meteor from delivering your file to the desired destination
454
+ - Use [▲ ostr.io](https://ostr.io?ref=github-mail-time-repo-footer) for [Server Monitoring](https://snmp-monitoring.com), [Web Analytics](https://ostr.io/info/web-analytics?ref=github-mail-time-repo-footer), [WebSec](https://domain-protection.info), [Web-CRON](https://web-cron.info) and [SEO Pre-rendering](https://prerendering.com) of a website
429
455
  - Star on [GitHub](https://github.com/veliovgroup/mail-time)
430
456
  - Star on [NPM](https://www.npmjs.com/package/mail-time)
431
457
  - Star on [Atmosphere](https://atmospherejs.com/ostrio/mailer)
432
458
  - [Sponsor maintainer via GitHub](https://github.com/sponsors/dr-dimitru) — support open source with one-time contribution or on a regular basis
433
459
  - [Sponsor veliovgroup via GitHub](https://github.com/sponsors/veliovgroup) — support company behind this package
434
460
  - [Support via PayPal](https://paypal.me/veliovgroup) — support our open source contributions
435
- - Use [ostr.io](https://ostr.io) — [Monitoring](https://snmp-monitoring.com), [Analytics](https://ostr.io/info/web-analytics), [WebSec](https://domain-protection.info), [Web-CRON](https://web-cron.info) and [Pre-rendering](https://prerendering.com) for a website
package/index.cjs ADDED
@@ -0,0 +1,658 @@
1
+ 'use strict';
2
+
3
+ const JoSk = require('josk');
4
+ const merge = require('deepmerge');
5
+
6
+ const noop = () => {};
7
+ const _debug = (isDebug, ...args) => {
8
+ if (isDebug) {
9
+ console.info.call(console, '[DEBUG] [mail-time]', `${new Date}`, ...args);
10
+ }
11
+ };
12
+ const _logError = (...args) => {
13
+ console.error.call(console, '[ERROR] [mail-time]', `${new Date}`, ...args);
14
+ };
15
+
16
+ const mongoErrorHandler = (error) => {
17
+ if (error) {
18
+ _logError('[mongoErrorHandler]:', error);
19
+ console.trace();
20
+ }
21
+ };
22
+
23
+ /**
24
+ * Ensure (create) index on MongoDB collection, catch and log exception if thrown
25
+ * @function ensureIndex
26
+ * @param {Collection} collection - Mongo's driver Collection instance
27
+ * @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
28
+ * @param {object} opts - Set of options that controls the creation of the index
29
+ * @returns {void 0}
30
+ */
31
+ const ensureIndex = async (collection, keys, opts) => {
32
+ try {
33
+ await collection.createIndex(keys, opts);
34
+ } catch (e) {
35
+ if (e.code === 85) {
36
+ let indexName;
37
+ const indexes = await collection.indexes();
38
+ for (const index of indexes) {
39
+ let drop = true;
40
+ for (const indexKey of Object.keys(keys)) {
41
+ if (typeof index.key[indexKey] === 'undefined') {
42
+ drop = false;
43
+ break;
44
+ }
45
+ }
46
+
47
+ for (const indexKey of Object.keys(index.key)) {
48
+ if (typeof keys[indexKey] === 'undefined') {
49
+ drop = false;
50
+ break;
51
+ }
52
+ }
53
+
54
+ if (drop) {
55
+ indexName = index.name;
56
+ break;
57
+ }
58
+ }
59
+
60
+ if (indexName) {
61
+ await collection.dropIndex(indexName);
62
+ await collection.createIndex(keys, opts);
63
+ }
64
+ } else {
65
+ _logError(`[ensureIndex] Can not set ${Object.keys(keys).join(' + ')} index on "${collection._name}" collection`, { keys, opts, details: e });
66
+ }
67
+ }
68
+ };
69
+
70
+ /**
71
+ * Check if entities of various types are equal
72
+ * including edge cases like unordered Objects and Array
73
+ * @function equals
74
+ * @param {mix} a
75
+ * @param {mix} b
76
+ * @returns {boolean}
77
+ */
78
+ const equals = (a, b) => {
79
+ let i;
80
+ if (a === b) {
81
+ return true;
82
+ }
83
+
84
+ if (!a || !b) {
85
+ return false;
86
+ }
87
+
88
+ if (!(typeof a === 'object' && typeof b === 'object')) {
89
+ return false;
90
+ }
91
+
92
+ if (a instanceof Date && b instanceof Date) {
93
+ return a.valueOf() === b.valueOf();
94
+ }
95
+
96
+ if (a instanceof Array) {
97
+ if (!(b instanceof Array)) {
98
+ return false;
99
+ }
100
+
101
+ if (a.length !== b.length) {
102
+ return false;
103
+ }
104
+
105
+ const _a = a.slice();
106
+ const _b = b.slice();
107
+ let j;
108
+ for (i = _a.length - 1; i >= 0; i--) {
109
+ let result = false;
110
+ for (j = _b.length - 1; j >= 0; j--) {
111
+ if (equals(_a[i], _b[j])) {
112
+ result = true;
113
+ _a.splice(i, 1);
114
+ _b.splice(j, 1);
115
+ break;
116
+ }
117
+ }
118
+
119
+ if (!result) {
120
+ return false;
121
+ }
122
+ }
123
+ return true;
124
+ }
125
+
126
+ i = 0;
127
+ if (typeof a === 'object' && typeof b === 'object') {
128
+ const akeys = Object.keys(a);
129
+ const bkeys = Object.keys(b);
130
+
131
+ if (akeys.length !== bkeys.length) {
132
+ return false;
133
+ }
134
+
135
+ for (i = akeys.length - 1; i >= 0; i--) {
136
+ if (!hasOwnProperty.call(b, akeys[i])) {
137
+ return false;
138
+ }
139
+
140
+ if (!equals(a[akeys[i]], b[akeys[i]])) {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ return true;
146
+ }
147
+ return false;
148
+ };
149
+
150
+ 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>';
151
+
152
+ /** Class of MailTime */
153
+ class MailTime {
154
+ constructor (opts) {
155
+ if (!opts || typeof opts !== 'object' || opts === null) {
156
+ throw new TypeError('[mail-time] Configuration object must be passed into MailTime constructor');
157
+ }
158
+
159
+ if (!opts.db) {
160
+ throw new Error('[mail-time] MongoDB database {db} option is required, like returned from `MongoClient.connect`');
161
+ }
162
+
163
+ this.callbacks = {};
164
+ this.type = (!opts.type || (opts.type !== 'client' && opts.type !== 'server')) ? 'server' : opts.type;
165
+ this.debug = (opts.debug !== true) ? false : true;
166
+ this.prefix = opts.prefix || '';
167
+ this.maxTries = ((opts.maxTries && !isNaN(opts.maxTries)) ? parseInt(opts.maxTries) : 59) + 1;
168
+ this.interval = ((opts.interval && !isNaN(opts.interval)) ? parseInt(opts.interval) : 60) * 1000;
169
+ this.template = (typeof opts.template === 'string') ? opts.template : '{{{html}}}';
170
+ this.zombieTime = opts.zombieTime || 32786;
171
+ this.keepHistory = opts.keepHistory || false;
172
+
173
+ this.revolvingInterval = opts.revolvingInterval || 1536;
174
+ this.minRevolvingDelay = opts.minRevolvingDelay || 512;
175
+ this.maxRevolvingDelay = opts.maxRevolvingDelay || 2048;
176
+
177
+ if (this.interval < 2048 || isNaN(this.interval)) {
178
+ this.interval = 3072;
179
+ }
180
+
181
+ if (this.zombieTime < 8192 || isNaN(this.zombieTime)) {
182
+ this.zombieTime = 8192;
183
+ }
184
+
185
+ this.failsToNext = (opts.failsToNext && !isNaN(opts.failsToNext)) ? parseInt(opts.failsToNext) : 4;
186
+ this.strategy = (opts.strategy === 'backup' || opts.strategy === 'balancer') ? opts.strategy : 'backup';
187
+ this.transports = opts.transports || [];
188
+ this.transport = 0;
189
+ this.from = (() => {
190
+ if (typeof opts.from === 'string') {
191
+ return () => {
192
+ return opts.from;
193
+ };
194
+ }
195
+
196
+ if (typeof opts.from === 'function') {
197
+ return opts.from;
198
+ }
199
+
200
+ return false;
201
+ })();
202
+
203
+ this.concatEmails = (opts.concatEmails !== true) ? false : true;
204
+ this.concatSubject = (opts.concatSubject && typeof opts.concatSubject === 'string') ? opts.concatSubject : 'Multiple notifications';
205
+ 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
+
208
+ if (this.concatThrottling < 2048) {
209
+ this.concatThrottling = 3072;
210
+ }
211
+
212
+ _debug(this.debug, 'DEBUG ON {debug: true}');
213
+ _debug(this.debug, `INITIALIZING [type: ${this.type}]`);
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`');
221
+ }
222
+
223
+ this.collection = opts.db.collection(`__mailTimeQueue__${this.prefix}`);
224
+ ensureIndex(this.collection, { isSent: 1, to: 1, sendAt: 1 });
225
+ ensureIndex(this.collection, { isSent: 1, sendAt: 1, tries: 1 }, { background: true });
226
+
227
+ // MongoDB Collection Schema:
228
+ // _id
229
+ // to {String|[String]}
230
+ // tries {Number} - qty of send attempts
231
+ // sendAt {Date} - When letter should be sent
232
+ // isSent {Boolean} - Email status
233
+ // template {String} - Template for this email
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
+
245
+ if (this.type === 'server') {
246
+ const scheduler = new JoSk({
247
+ db: opts.db,
248
+ debug: this.debug,
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
+ }
274
+
275
+ if (transport._options && typeof transport._options === 'object' && transport._options !== null && transport._options.mailOptions) {
276
+ _mailOpts = merge(_mailOpts, transport._options.mailOptions);
277
+ }
278
+
279
+ if (transport.options && typeof transport.options === 'object' && transport.options !== null && transport.options.mailOptions) {
280
+ _mailOpts = merge(_mailOpts, transport.options.mailOptions);
281
+ }
282
+
283
+ _mailOpts = merge(_mailOpts, {
284
+ html: '',
285
+ text: '',
286
+ subject: ''
287
+ });
288
+
289
+ for (let i = 0; i < task.mailOptions.length; i++) {
290
+ if (task.mailOptions[i].html) {
291
+ if (task.mailOptions.length > 1) {
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]);
295
+ }
296
+ delete task.mailOptions[i].html;
297
+ }
298
+
299
+ if (task.mailOptions[i].text) {
300
+ if (task.mailOptions.length > 1) {
301
+ _mailOpts.text += '\r\n' + this.___render(task.mailOptions[i].text, task.mailOptions[i]);
302
+ } else {
303
+ _mailOpts.text = this.___render(task.mailOptions[i].text, task.mailOptions[i]);
304
+ }
305
+ delete task.mailOptions[i].text;
306
+ }
307
+
308
+ _mailOpts = merge(_mailOpts, task.mailOptions[i]);
309
+ }
310
+
311
+ if (_mailOpts.html && (task.template || this.template)) {
312
+ _mailOpts.html = this.___render((task.template || this.template), _mailOpts);
313
+ }
314
+
315
+ if (task.mailOptions.length > 1) {
316
+ _mailOpts.subject = task.concatSubject || this.concatSubject || _mailOpts.subject;
317
+ }
318
+
319
+ if (!_mailOpts.from && this.from) {
320
+ _mailOpts.from = this.from(transport);
321
+ }
322
+
323
+ return _mailOpts;
324
+ }
325
+
326
+ /**
327
+ * @memberOf MailTime
328
+ * @name ___send
329
+ * @param ready {Function} - See JoSk NPM package
330
+ * @returns {void}
331
+ */
332
+ ___send(ready) {
333
+ const cursor = this.collection.find({
334
+ isSent: false,
335
+ sendAt: {
336
+ $lte: new Date()
337
+ },
338
+ tries: {
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
+ });
414
+ }
415
+
416
+ /**
417
+ * @memberOf MailTime
418
+ * @name send
419
+ * @description alias of `sendMail`
420
+ * @returns {void}
421
+ */
422
+ send(opts, callback) {
423
+ this.sendMail(opts, callback);
424
+ }
425
+
426
+ /**
427
+ * @memberOf MailTime
428
+ * @name sendMail
429
+ * @param opts {Object} - Letter options with next properties:
430
+ * @param opts.sendAt {Date} - When email should be sent
431
+ * @param opts.template {String} - Template string
432
+ * @param opts[key] {mix} - Other MailOptions according to NodeMailer lib documentation
433
+ * @param callback {Function} - [OPTIONAL] Callback function
434
+ * @returns {void}
435
+ */
436
+ sendMail(opts = {}, callback = noop) {
437
+ _debug(this.debug, '[sendMail] [attempt] To:', opts.to);
438
+ if (!opts.html && !opts.text) {
439
+ throw new Error('`html` nor `text` field is presented, at least one of those fields is required');
440
+ }
441
+
442
+ if (!opts.sendAt || Object.prototype.toString.call(opts.sendAt) !== '[object Date]') {
443
+ opts.sendAt = new Date();
444
+ }
445
+
446
+ if (typeof opts.template !== 'string') {
447
+ opts.template = false;
448
+ }
449
+
450
+ if (typeof opts.concatSubject !== 'string') {
451
+ opts.concatSubject = false;
452
+ }
453
+
454
+ let _sendAt = opts.sendAt;
455
+ const _template = opts.template;
456
+ const _concatSubject = opts.concatSubject;
457
+ delete opts.sendAt;
458
+ delete opts.template;
459
+ delete opts.concatSubject;
460
+
461
+ if (typeof opts.to !== 'string' && (!(opts.to instanceof Array) || !opts.to.length)) {
462
+ throw new Error('[mail-time] `mailOptions.to` is required and must be a string or non-empty Array');
463
+ }
464
+
465
+ if (this.concatEmails) {
466
+ _sendAt = new Date(+_sendAt + this.concatThrottling);
467
+ this.collection.findOne({
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
+ }
487
+
488
+ queue.push(opts);
489
+ this.collection.updateOne({
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
+ }
504
+
505
+ this.___addToQueue({
506
+ sendAt: _sendAt,
507
+ template: _template,
508
+ mailOptions: opts,
509
+ concatSubject: _concatSubject
510
+ }, callback);
511
+ }).catch(mongoErrorHandler);
512
+
513
+ return;
514
+ }
515
+
516
+ this.___addToQueue({
517
+ sendAt: _sendAt,
518
+ template: _template,
519
+ mailOptions: opts,
520
+ concatSubject: _concatSubject
521
+ }, callback);
522
+
523
+ return;
524
+ }
525
+
526
+ /**
527
+ * @memberOf MailTime
528
+ * @name ___handleError
529
+ * @param task {Object} - Email task record form Mongo
530
+ * @param error {mix} - Error String/Object/Error
531
+ * @param info {Object} - Info object returned from nodemailer
532
+ * @returns {void}
533
+ */
534
+ ___handleError(task, error, info) {
535
+ if (!task) {
536
+ return;
537
+ }
538
+
539
+ if (task.tries >= this.maxTries) {
540
+ if (!this.keepHistory) {
541
+ this.collection.deleteOne({
542
+ _id: task._id
543
+ }).catch(mongoErrorHandler);
544
+ }
545
+
546
+ _debug(this.debug, `Giving up trying send email after ${task.tries} attempts to: `, task.mailOptions[0].to, error);
547
+ this.___triggerCallbacks(error, task, info);
548
+ return;
549
+ }
550
+
551
+ let transportIndex = task.transport;
552
+
553
+ if (this.strategy === 'backup' && this.transports.length > 1 && (task.tries % this.failsToNext) === 0) {
554
+ ++transportIndex;
555
+ if (transportIndex > this.transports.length - 1) {
556
+ transportIndex = 0;
557
+ }
558
+ }
559
+
560
+ this.collection.updateOne({
561
+ _id: task._id
562
+ }, {
563
+ $set: {
564
+ isSent: false,
565
+ sendAt: new Date(Date.now() + this.interval),
566
+ transport: transportIndex
567
+ }
568
+ }).catch(mongoErrorHandler);
569
+
570
+ _debug(this.debug, `Next re-send attempt at ${new Date(Date.now() + this.interval)}: #${task.tries}/${this.maxTries}, transport #${transportIndex} to: `, task.mailOptions[0].to, error);
571
+ }
572
+
573
+ /**
574
+ * @memberOf MailTime
575
+ * @name ___addToQueue
576
+ * @param opts {Object} - Letter options with next properties:
577
+ * @param opts.sendAt {Date} - When email should be sent
578
+ * @param opts.template {String} - Email template
579
+ * @param opts.mailOptions {Object} - MailOptions according to NodeMailer lib
580
+ * @param opts.concatSubject {String} - Email subject used when sending multiple concatenated emails
581
+ * @param callback {Function} - [OPTIONAL] Callback function
582
+ * @returns {void}
583
+ */
584
+ ___addToQueue(opts, callback) {
585
+ _debug(this.debug, '[sendMail] [adding to queue] To:', opts.mailOptions.to);
586
+ const task = {
587
+ tries: 0,
588
+ isSent: false,
589
+ sendAt: opts.sendAt,
590
+ template: opts.template,
591
+ transport: this.transport,
592
+ mailOptions: [opts.mailOptions],
593
+ concatSubject: opts.concatSubject
594
+ };
595
+
596
+ if (this.concatEmails) {
597
+ task.to = opts.mailOptions.to;
598
+ }
599
+
600
+ this.collection.insertOne(task).then((r) => {
601
+ const _id = r.insertedId.toHexString();
602
+ if (!this.callbacks[_id]) {
603
+ this.callbacks[_id] = [];
604
+ }
605
+ this.callbacks[_id].push(callback);
606
+ }).catch(mongoErrorHandler);
607
+ }
608
+
609
+ /**
610
+ * @memberOf MailTime
611
+ * @name ___render
612
+ * @param string {String} - Template with Spacebars/Blaze/Mustache-like placeholders
613
+ * @param replacements {Object} - Blaze/Mustache-like helpers Object
614
+ * @returns {String}
615
+ */
616
+ ___render(_string, replacements) {
617
+ let i;
618
+ let string = _string;
619
+ const matchHTML = string.match(/\{{3}\s?([a-zA-Z0-9\-\_]+)\s?\}{3}/g);
620
+ if (matchHTML) {
621
+ for (i = 0; i < matchHTML.length; i++) {
622
+ if (replacements[matchHTML[i].replace('{{{', '').replace('}}}', '').trim()]) {
623
+ string = string.replace(matchHTML[i], replacements[matchHTML[i].replace('{{{', '').replace('}}}', '').trim()]);
624
+ }
625
+ }
626
+ }
627
+
628
+ const matchStr = string.match(/\{{2}\s?([a-zA-Z0-9\-\_]+)\s?\}{2}/g);
629
+ if (matchStr) {
630
+ for (i = 0; i < matchStr.length; i++) {
631
+ if (replacements[matchStr[i].replace('{{', '').replace('}}', '').trim()]) {
632
+ string = string.replace(matchStr[i], replacements[matchStr[i].replace('{{', '').replace('}}', '').trim()].replace(/<(?:.|\n)*?>/gm, ''));
633
+ }
634
+ }
635
+ }
636
+ return string;
637
+ }
638
+
639
+ /**
640
+ * @memberOf MailTime
641
+ * @name ___triggerCallbacks
642
+ * @param error {mix} - Error thrown during sending email
643
+ * @param task {Object} - Task record from mongodb
644
+ * @param info {Object} - Info object returned from NodeMailer's `sendMail` method
645
+ * @returns {void 0}
646
+ */
647
+ ___triggerCallbacks(error, task, info) {
648
+ const _id = task._id.toHexString();
649
+ if (this.callbacks[_id] && this.callbacks[_id].length) {
650
+ this.callbacks[_id].forEach((cb, i) => {
651
+ cb(error, info, task?.mailOptions?.[i]);
652
+ });
653
+ }
654
+ delete this.callbacks[_id];
655
+ }
656
+ }
657
+
658
+ module.exports = MailTime;
package/index.js CHANGED
@@ -1,6 +1,7 @@
1
- const JoSk = require('josk');
1
+ import JoSk from 'josk';
2
+ import merge from 'deepmerge';
3
+
2
4
  const noop = () => {};
3
- const merge = require('deepmerge');
4
5
  const _debug = (isDebug, ...args) => {
5
6
  if (isDebug) {
6
7
  console.info.call(console, '[DEBUG] [mail-time]', `${new Date}`, ...args);
@@ -17,8 +18,62 @@ const mongoErrorHandler = (error) => {
17
18
  }
18
19
  };
19
20
 
20
- let equals;
21
- equals = (a, b) => {
21
+ /**
22
+ * Ensure (create) index on MongoDB collection, catch and log exception if thrown
23
+ * @function ensureIndex
24
+ * @param {Collection} collection - Mongo's driver Collection instance
25
+ * @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
26
+ * @param {object} opts - Set of options that controls the creation of the index
27
+ * @returns {void 0}
28
+ */
29
+ const ensureIndex = async (collection, keys, opts) => {
30
+ try {
31
+ await collection.createIndex(keys, opts);
32
+ } catch (e) {
33
+ if (e.code === 85) {
34
+ let indexName;
35
+ const indexes = await collection.indexes();
36
+ for (const index of indexes) {
37
+ let drop = true;
38
+ for (const indexKey of Object.keys(keys)) {
39
+ if (typeof index.key[indexKey] === 'undefined') {
40
+ drop = false;
41
+ break;
42
+ }
43
+ }
44
+
45
+ for (const indexKey of Object.keys(index.key)) {
46
+ if (typeof keys[indexKey] === 'undefined') {
47
+ drop = false;
48
+ break;
49
+ }
50
+ }
51
+
52
+ if (drop) {
53
+ indexName = index.name;
54
+ break;
55
+ }
56
+ }
57
+
58
+ if (indexName) {
59
+ await collection.dropIndex(indexName);
60
+ await collection.createIndex(keys, opts);
61
+ }
62
+ } else {
63
+ _logError(`[ensureIndex] Can not set ${Object.keys(keys).join(' + ')} index on "${collection._name}" collection`, { keys, opts, details: e });
64
+ }
65
+ }
66
+ };
67
+
68
+ /**
69
+ * Check if entities of various types are equal
70
+ * including edge cases like unordered Objects and Array
71
+ * @function equals
72
+ * @param {mix} a
73
+ * @param {mix} b
74
+ * @returns {boolean}
75
+ */
76
+ const equals = (a, b) => {
22
77
  let i;
23
78
  if (a === b) {
24
79
  return true;
@@ -92,7 +147,8 @@ equals = (a, b) => {
92
147
 
93
148
  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>';
94
149
 
95
- module.exports = class MailTime {
150
+ /** Class of MailTime */
151
+ export default class MailTime {
96
152
  constructor (opts) {
97
153
  if (!opts || typeof opts !== 'object' || opts === null) {
98
154
  throw new TypeError('[mail-time] Configuration object must be passed into MailTime constructor');
@@ -163,17 +219,10 @@ module.exports = class MailTime {
163
219
  }
164
220
 
165
221
  this.collection = opts.db.collection(`__mailTimeQueue__${this.prefix}`);
166
- this.collection.createIndex({ isSent: 1, to: 1, sendAt: 1 }, (indexError) => {
167
- if (indexError) {
168
- _logError('[createIndex]', indexError);
169
- }
170
- });
171
- this.collection.createIndex({ isSent: 1, sendAt: 1, tries: 1 }, { background: true }, (indexError) => {
172
- if (indexError) {
173
- _logError('[createIndex]', indexError);
174
- }
175
- });
176
- // Schema:
222
+ ensureIndex(this.collection, { isSent: 1, to: 1, sendAt: 1 });
223
+ ensureIndex(this.collection, { isSent: 1, sendAt: 1, tries: 1 }, { background: true });
224
+
225
+ // MongoDB Collection Schema:
177
226
  // _id
178
227
  // to {String|[String]}
179
228
  // tries {Number} - qty of send attempts
@@ -298,72 +347,67 @@ module.exports = class MailTime {
298
347
  }
299
348
  });
300
349
 
301
- cursor.forEach((task) => {
302
- this.collection.updateOne({
303
- _id: task._id
304
- }, {
305
- $set: {
306
- isSent: true
307
- },
308
- $inc: {
309
- tries: 1
310
- }
311
- }, (updateError) => {
312
- if (updateError) {
313
- this.___handleError(task, updateError, {});
350
+ cursor.forEach(async (task) => {
351
+ try {
352
+ await this.collection.updateOne({
353
+ _id: task._id
354
+ }, {
355
+ $set: {
356
+ isSent: true
357
+ },
358
+ $inc: {
359
+ tries: 1
360
+ }
361
+ });
362
+
363
+ let transport;
364
+ let transportIndex;
365
+ if (this.strategy === 'balancer') {
366
+ this.transport = this.transport + 1;
367
+ if (this.transport >= this.transports.length) {
368
+ this.transport = 0;
369
+ }
370
+ transportIndex = this.transport;
371
+ transport = this.transports[transportIndex];
314
372
  } else {
315
- let transport;
316
- let transportIndex;
317
- if (this.strategy === 'balancer') {
318
- this.transport = this.transport + 1;
319
- if (this.transport >= this.transports.length) {
320
- this.transport = 0;
321
- }
322
- transportIndex = this.transport;
323
- transport = this.transports[transportIndex];
324
- } else {
325
- transportIndex = task.transport;
326
- transport = this.transports[transportIndex];
373
+ transportIndex = task.transport;
374
+ transport = this.transports[transportIndex];
375
+ }
376
+
377
+ const _mailOpts = this.___compileMailOpts(transport, task);
378
+
379
+ _debug(this.debug, '[sendMail] [sending] To:', _mailOpts.to);
380
+ transport.sendMail(_mailOpts, (error, info) => {
381
+ if (error) {
382
+ this.___handleError(task, error, info);
383
+ return;
384
+ }
385
+
386
+ if (info.accepted && !info.accepted.length) {
387
+ this.___handleError(task, 'Message not accepted or Greeting never received', info);
388
+ return;
327
389
  }
328
390
 
329
- try {
330
- const _mailOpts = this.___compileMailOpts(transport, task);
331
-
332
- _debug(this.debug, '[sendMail] [sending] To:', _mailOpts.to);
333
- transport.sendMail(_mailOpts, (error, info) => {
334
- if (error) {
335
- this.___handleError(task, error, info);
336
- return;
337
- }
338
-
339
- if (info.accepted && !info.accepted.length) {
340
- this.___handleError(task, 'Message not accepted or Greeting never received', info);
341
- return;
342
- }
343
-
344
- _debug(this.debug, `email successfully sent, attempts: #${task.tries}, transport #${transportIndex} to: `, _mailOpts.to);
345
- if (this.keepHistory) {
346
- this.___triggerCallbacks(void 0, task, info);
347
- } else {
348
- this.collection.deleteOne({ _id: task._id }, () => {
349
- this.___triggerCallbacks(void 0, task, info);
350
- });
351
- }
391
+ _debug(this.debug, `email successfully sent, attempts: #${task.tries}, transport #${transportIndex} to: `, _mailOpts.to);
352
392
 
353
- return;
354
- });
355
- } catch (e) {
356
- _logError('Exception during runtime:', e);
357
- this.___handleError(task, e, {});
393
+ if (!this.keepHistory) {
394
+ this.collection.deleteOne({
395
+ _id: task._id
396
+ }).catch(mongoErrorHandler);
358
397
  }
359
- }
360
- });
361
- }, (forEachError) => {
398
+
399
+ this.___triggerCallbacks(void 0, task, info);
400
+ return;
401
+ });
402
+ } catch (e) {
403
+ _logError('Exception during runtime:', e);
404
+ this.___handleError(task, e, {});
405
+ }
406
+ }).catch((forEachError) => {
407
+ _logError('[___send] [forEach] [forEachError]', forEachError);
408
+ }).finally(() => {
362
409
  ready();
363
410
  cursor.close();
364
- if (forEachError) {
365
- _logError('[___send] [forEach] [forEachError]', forEachError);
366
- }
367
411
  });
368
412
  }
369
413
 
@@ -417,24 +461,19 @@ module.exports = class MailTime {
417
461
  }
418
462
 
419
463
  if (this.concatEmails) {
464
+ _sendAt = new Date(+_sendAt + this.concatThrottling);
420
465
  this.collection.findOne({
421
466
  to: opts.to,
422
467
  isSent: false,
423
468
  sendAt: {
424
- $lte: new Date(+_sendAt + this.concatThrottling)
469
+ $lte: _sendAt
425
470
  }
426
471
  }, {
427
472
  projection: {
428
473
  _id: 1,
429
474
  mailOptions: 1
430
475
  }
431
- }, (findError, task) => {
432
- if (findError) {
433
- _debug(this.debug, 'something went wrong, can\'t send email to: ', opts.to, findError);
434
- callback(findError, void 0, task);
435
- return;
436
- }
437
-
476
+ }).then((task) => {
438
477
  if (task) {
439
478
  const queue = task.mailOptions || [];
440
479
 
@@ -445,32 +484,29 @@ module.exports = class MailTime {
445
484
  }
446
485
 
447
486
  queue.push(opts);
448
-
449
487
  this.collection.updateOne({
450
488
  _id: task._id
451
489
  }, {
452
490
  $set: {
453
491
  mailOptions: queue
454
492
  }
455
- }, (updateError) => {
456
- if (updateError) {
457
- _debug('something went wrong, can\'t send email to: ', task.mailOptions[0].to, updateError);
458
- callback(updateError, void 0, task);
493
+ }).then(() => {
494
+ const _id = task._id.toHexString();
495
+ if (!this.callbacks[_id]) {
496
+ this.callbacks[_id] = [];
459
497
  }
460
- });
461
-
498
+ this.callbacks[_id].push(callback);
499
+ }).catch(mongoErrorHandler);
462
500
  return;
463
501
  }
464
502
 
465
- _sendAt = new Date(+_sendAt + this.concatThrottling);
466
503
  this.___addToQueue({
467
504
  sendAt: _sendAt,
468
505
  template: _template,
469
506
  mailOptions: opts,
470
507
  concatSubject: _concatSubject
471
508
  }, callback);
472
- return;
473
- });
509
+ }).catch(mongoErrorHandler);
474
510
 
475
511
  return;
476
512
  }
@@ -502,33 +538,34 @@ module.exports = class MailTime {
502
538
  if (!this.keepHistory) {
503
539
  this.collection.deleteOne({
504
540
  _id: task._id
505
- }, (deleteError) => {
506
- _debug(this.debug, `Giving up trying send email after ${task.tries} attempts to: `, task.mailOptions[0].to, error, deleteError);
507
- this.___triggerCallbacks(error, task, info);
508
- });
541
+ }).catch(mongoErrorHandler);
509
542
  }
510
- } else {
511
- let transportIndex = task.transport;
512
543
 
513
- if (this.strategy === 'backup' && (task.tries % this.failsToNext) === 0) {
514
- ++transportIndex;
515
- if (transportIndex > this.transports.length - 1) {
516
- transportIndex = 0;
517
- }
518
- }
544
+ _debug(this.debug, `Giving up trying send email after ${task.tries} attempts to: `, task.mailOptions[0].to, error);
545
+ this.___triggerCallbacks(error, task, info);
546
+ return;
547
+ }
519
548
 
520
- this.collection.updateOne({
521
- _id: task._id
522
- }, {
523
- $set: {
524
- isSent: false,
525
- sendAt: new Date(Date.now() + this.interval),
526
- transport: transportIndex
527
- }
528
- }, mongoErrorHandler);
549
+ let transportIndex = task.transport;
529
550
 
530
- _debug(this.debug, `Next re-send attempt at ${new Date(Date.now() + this.interval)}: #${task.tries}/${this.maxTries}, transport #${transportIndex} to: `, task.mailOptions[0].to, error);
551
+ if (this.strategy === 'backup' && this.transports.length > 1 && (task.tries % this.failsToNext) === 0) {
552
+ ++transportIndex;
553
+ if (transportIndex > this.transports.length - 1) {
554
+ transportIndex = 0;
555
+ }
531
556
  }
557
+
558
+ this.collection.updateOne({
559
+ _id: task._id
560
+ }, {
561
+ $set: {
562
+ isSent: false,
563
+ sendAt: new Date(Date.now() + this.interval),
564
+ transport: transportIndex
565
+ }
566
+ }).catch(mongoErrorHandler);
567
+
568
+ _debug(this.debug, `Next re-send attempt at ${new Date(Date.now() + this.interval)}: #${task.tries}/${this.maxTries}, transport #${transportIndex} to: `, task.mailOptions[0].to, error);
532
569
  }
533
570
 
534
571
  /**
@@ -558,20 +595,13 @@ module.exports = class MailTime {
558
595
  task.to = opts.mailOptions.to;
559
596
  }
560
597
 
561
- this.collection.insertOne(task, (insertError, r) => {
562
- if (insertError) {
563
- _logError('something went wrong, can\'t send email to: ', opts.mailOptions[0].to, insertError);
564
- callback(insertError, void 0, opts);
565
- return;
566
- }
567
-
598
+ this.collection.insertOne(task).then((r) => {
568
599
  const _id = r.insertedId.toHexString();
569
600
  if (!this.callbacks[_id]) {
570
601
  this.callbacks[_id] = [];
571
602
  }
572
603
  this.callbacks[_id].push(callback);
573
- return;
574
- });
604
+ }).catch(mongoErrorHandler);
575
605
  }
576
606
 
577
607
  /**
@@ -616,9 +646,9 @@ module.exports = class MailTime {
616
646
  const _id = task._id.toHexString();
617
647
  if (this.callbacks[_id] && this.callbacks[_id].length) {
618
648
  this.callbacks[_id].forEach((cb, i) => {
619
- cb(error, info, task.mailOptions[i]);
649
+ cb(error, info, task?.mailOptions?.[i]);
620
650
  });
621
651
  }
622
652
  delete this.callbacks[_id];
623
653
  }
624
- };
654
+ }
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "mail-time",
3
- "version": "1.3.4",
3
+ "version": "1.4.0",
4
4
  "description": "Bulletproof email queue on top of NodeMailer for a single and multi-server setups",
5
5
  "main": "index.js",
6
+ "type": "module",
7
+ "exports": {
8
+ "import": "./index.js",
9
+ "require": "./index.cjs"
10
+ },
6
11
  "scripts": {
12
+ "prepublishOnly": "rollup --config rollup.config.js",
7
13
  "test": "mocha ./test/npm.js"
8
14
  },
9
15
  "keywords": [
@@ -11,11 +17,17 @@
11
17
  "balanced",
12
18
  "balancer",
13
19
  "bulletproof",
20
+ "cluster",
14
21
  "email",
22
+ "highload",
23
+ "high load",
24
+ "horizontal scaling",
15
25
  "mail",
16
26
  "mail-time",
17
27
  "mailer",
18
28
  "mailtime",
29
+ "microservice",
30
+ "micro-service",
19
31
  "nodemailer",
20
32
  "queue",
21
33
  "template"
@@ -25,11 +37,17 @@
25
37
  "balanced",
26
38
  "balancer",
27
39
  "bulletproof",
40
+ "cluster",
28
41
  "email",
42
+ "highload",
43
+ "high load",
44
+ "horizontal scaling",
29
45
  "mail",
30
46
  "mail-time",
31
47
  "mailer",
32
48
  "mailtime",
49
+ "microservice",
50
+ "micro-service",
33
51
  "nodemailer",
34
52
  "queue",
35
53
  "template"
@@ -45,19 +63,19 @@
45
63
  "author": "dr.dimitru (https://veliovgroup.com)",
46
64
  "license": "BSD-3-Clause",
47
65
  "engines": {
48
- "node": ">=8.9.0"
66
+ "node": ">=14.1.0"
49
67
  },
50
68
  "dependencies": {
51
- "deepmerge": "^4.2.2",
52
- "josk": "^3.0.2"
69
+ "deepmerge": "^4.3.1",
70
+ "josk": "^3.1.1"
53
71
  },
54
72
  "devDependencies": {
55
- "bson": "^4.7.0",
73
+ "bson": "^6.6.0",
56
74
  "bson-ext": "^4.0.3",
57
- "chai": "^4.3.6",
58
- "mocha": "^10.0.0",
59
- "mongodb": "^4.10.0",
60
- "nodemailer": "^6.7.8",
61
- "nodemailer-direct-transport": "^3.0.7"
75
+ "chai": "^4.4.1",
76
+ "mocha": "^10.3.0",
77
+ "mongodb": "^6.5.0",
78
+ "nodemailer": "^6.9.12",
79
+ "nodemailer-direct-transport": "^3.3.2"
62
80
  }
63
81
  }
package/CHANGELOG.md DELETED
@@ -1 +0,0 @@
1
- For full changelog see [releases](https://github.com/veliovgroup/mail-time/releases) in GitHub
package/HISTORY.md DELETED
@@ -1 +0,0 @@
1
- For full history see [releases](https://github.com/veliovgroup/mail-time/releases) in GitHub