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 +1 -1
- package/README.md +68 -43
- package/index.cjs +658 -0
- package/index.js +156 -126
- package/package.json +28 -10
- package/CHANGELOG.md +0 -1
- package/HISTORY.md +0 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,39 +1,38 @@
|
|
|
1
1
|
[](https://github.com/sponsors/dr-dimitru)
|
|
2
2
|
[](https://paypal.me/veliovgroup)
|
|
3
|
-
<a href="https://ostr.io/info/built-by-developers-for-developers">
|
|
4
|
-
|
|
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
|
|
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
|
-
- [
|
|
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 -
|
|
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
|
-
|
|
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
|
-
###
|
|
90
|
+
### Sending emails from cluster of servers
|
|
92
91
|
|
|
93
|
-
It is common to
|
|
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.
|
|
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
|
-
|
|
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
|
-
| |----------| |----------| |----------| | | |
|
|
102
|
-
| | App | | App | | App | | | | |
|
|
103
|
-
| | Server 1 | | Server 2 | | Server 3 | | |
|
|
104
|
-
| |-----\----| |----\-----| |----\-----| | |
|
|
105
|
-
| \---------------\----------------\----------> | |
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
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@>=
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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:
|
|
469
|
+
$lte: _sendAt
|
|
425
470
|
}
|
|
426
471
|
}, {
|
|
427
472
|
projection: {
|
|
428
473
|
_id: 1,
|
|
429
474
|
mailOptions: 1
|
|
430
475
|
}
|
|
431
|
-
}
|
|
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
|
-
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
"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": ">=
|
|
66
|
+
"node": ">=14.1.0"
|
|
49
67
|
},
|
|
50
68
|
"dependencies": {
|
|
51
|
-
"deepmerge": "^4.
|
|
52
|
-
"josk": "^3.
|
|
69
|
+
"deepmerge": "^4.3.1",
|
|
70
|
+
"josk": "^3.1.1"
|
|
53
71
|
},
|
|
54
72
|
"devDependencies": {
|
|
55
|
-
"bson": "^
|
|
73
|
+
"bson": "^6.6.0",
|
|
56
74
|
"bson-ext": "^4.0.3",
|
|
57
|
-
"chai": "^4.
|
|
58
|
-
"mocha": "^10.
|
|
59
|
-
"mongodb": "^
|
|
60
|
-
"nodemailer": "^6.
|
|
61
|
-
"nodemailer-direct-transport": "^3.
|
|
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
|