mail-time 1.3.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,39 +1,54 @@
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 NPM package for mail queue management. 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*. *Server* type of `MailTime` is great for creating an emailing micro-service app.
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 horizontally-scaled apps](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 examples](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#basic-usage)
24
+ - [Require/Import `mail-time`](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#1-require-package)
25
+ - [Create NodeMailer's transports](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#2-create-nodemailers-transports)
26
+ - [Initiate `mail-time`](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3-initiate-mail-time)
27
+ - [Connect to Redis](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3a-initiate-and-connect-to-redis)
28
+ - [Connect to MongoDB](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3b-initiate-and-connect-to-mongodb)
29
+ - [Initiate as *Client* instance](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3c-optionally-create-client-type-of-mailtime)
30
+ - [Initiate two `MailTime` instances within the single app](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#two-mailtime-instances-usage-example)
31
+ - [Use templates](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#passing-variables-to-the-template)
32
+ - Different storage configurations:
33
+ - [Use MongoDB for queue and scheduler](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#using-mongodb-for-queue-and-scheduler)
34
+ - [Use MongoDB for queue and Redis for scheduler](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#using-mongodb-for-queue-and-redis-for-scheduler)
35
+ - [Use Redis for queue and MongoDB for scheduler](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#using-redis-for-queue-and-mongodb-for-scheduler)
36
+ - [Use Redis for queue and scheduler](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#using-redis-for-queue-and-scheduler)
37
+ - [API](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#api)
38
+ - [`new MailTime` *Constructor*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#new-mailtimeopts-constructor)
39
+ - [`new RedisQueue` *Constructor*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#new-redisqueueopts-constructor)
40
+ - [`new MongoQueue` *Constructor*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#new-mongoqueueopts-constructor)
41
+ - [`.sendMail()`](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#sendmailopts)
42
+ - [`.cancelMail()`](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#cancelmailuuid)
43
+ - [Default Template](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#static-mailtimetemplate)
44
+ - [Custom Templates](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#template-example)
45
+ - [Running tests](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#testing)
31
46
 
32
47
  ## Main features:
33
48
 
34
- - 👨‍🔬 ~92% tests coverage;
49
+ - 👨‍🔬 ~94% tests coverage;
35
50
  - 📦 Two simple dependencies, written from scratch for top performance;
36
- - 🏢 Synchronize email queue across multiple servers;
51
+ - 🏢 Synchronize email queue across multiple (horizontally scaled) servers;
37
52
  - 💪 Bulletproof design, built-in retries.
38
53
 
39
54
  ## How does it work?
@@ -42,7 +57,7 @@ Redundant solution for email transmission.
42
57
 
43
58
  ### Single point of failure
44
59
 
45
- Issue - classic solution with a single point of failure:
60
+ Issue - mitigate a single point of failure via persistent queue and re-send attempts
46
61
 
47
62
  ```ascii
48
63
  |----------------| |------| |------------------|
@@ -72,7 +87,7 @@ it will keep the letter in the queue and retry to send it again
72
87
 
73
88
  ### Multiple SMTP providers
74
89
 
75
- Backup scheme with multiple SMTP providers
90
+ Rotate email transports by using multiple SMTP providers. *MailTime* support two strategies `backup` (*rotate when failed*) and `balancer` (*round-robin rotation*)
76
91
 
77
92
  ```ascii
78
93
  |--------|
@@ -88,43 +103,60 @@ Backup scheme with multiple SMTP providers
88
103
  |--------|
89
104
  ```
90
105
 
91
- ### Cluster issue
106
+ ### Sending emails from cluster of servers
92
107
 
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.
108
+ It is common to have horizontally scaled "Cluster" of servers for load-balancing and for durability.
94
109
 
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.
110
+ 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
111
 
97
- Here is how this issue is solved by using MailTime:
112
+ For the maximum durability and agility each Application Server can run *MailTime* in the "Server" mode:
113
+
114
+ ```ascii
115
+ |===================THE=CLUSTER===================| |=QUEUE=|
116
+ | |----------| |----------| |----------| | | | |--------|
117
+ | | App | | App | | App | | | |-->| SMTP 1 |------\
118
+ | | Server 1 | | Server 2 | | Server 3 | | | | |--------| \
119
+ | |-----\----| |----\-----| |----\-----| | | | |-------------|
120
+ | \---------------\----------------\----------> | |--------| | ^_^ |
121
+ | | | |-->| SMTP 2 |-->| Happy users |
122
+ | Each "App Server" | | | |--------| |-------------|
123
+ | runs MailTime as a "Server" | | | /
124
+ | for the maximum durability | | | |--------| /
125
+ | | | |-->| SMTP 3 |-----/
126
+ | | | | |--------|
127
+ |=================================================| |=======|
128
+ ```
129
+
130
+ 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
131
 
99
132
  ```ascii
100
133
  |===================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 | | | | | |--------|
134
+ | |----------| |----------| |----------| | | | | | |--------|
135
+ | | App | | App | | App | | | | | Micro-service |-->| SMTP 1 |------\
136
+ | | Server 1 | | Server 2 | | Server 3 | | | | | running | |--------| \
137
+ | |-----\----| |----\-----| |----\-----| | | | | MailTime as | |-------------|
138
+ | \---------------\----------------\----------> | | "Server" only | |--------| | ^_^ |
139
+ | | | | | sending |-->| SMTP 2 |-->| Happy users |
140
+ | Each "App Server" runs MailTime as | | | | emails | |--------| |-------------|
141
+ | a "Client" only placing emails to the queue. | | <-------- | /
142
+ | | | --------> | |--------| /
143
+ | | | | | |-->| SMTP 3 |-----/
144
+ | | | | | | |--------|
112
145
  |=================================================| |=======| |===============|
113
146
  ```
114
147
 
115
148
  ## Features
116
149
 
117
- - Queue - Managed via MongoDB, will survive server reboots and failures
118
- - Support for multiple server setups - "Cluster", Phusion Passenger instances, Load Balanced solutions, etc.
119
- - Emails concatenation by addressee email - Reduce amount of sent emails to a single user with concatenation, and avoid mistakenly doubled emails
120
- - When concatenation is enabled - Same emails won't be sent twice, if for any reason, due to bad logic or application failure emails are sent twice or more times - this is solution to solve this annoying behavior
121
- - Balancing for multiple nodemailer's transports, two modes - `backup` and `balancing`. This is the most useful feature — allowing to reduce the cost of SMTP services and add extra layer of durability. If one transport failing to send an email `mail-time` will switch to the next one
122
- - Sending retries for network and other failures
123
- - Templating support with [Mustache](https://mustache.github.io/)-like placeholders
150
+ - __Email Queue__ - Managed via MongoDB, Redis, or [Custom Queue](https://github.com/veliovgroup/mail-time/blob/master/docs/queue-api.md). Storage-based queue will survive server reboots and failures
151
+ - __Made for horizontally scaled multi-server setups__ - *MailTime* is made to run in multi-server environments, like "Clusters", multiple app instances, load balanced solutions, and replications. *MailTime* is the perfect fit for applications scaled on a single machine, multiple virtual servers, multiple "bare metal" servers, within single or multiple data centers
152
+ - __Email concatenation__ - Reduce amount of sent emails to a single user with concatenation, and avoid mistakenly duplicated emails. When "email concatenation" is enabled the same emails (*checked by addressee and content*) won't be sent twice. If emails are sent multiple times, due to issues in logic or application failures, - enable "email concatenation" to solve this behavior
153
+ - __Multiple NodeMailer/SMTP transports__ — Support for multiple SMPT transports implemented in two modes - `backup` and `balancing`. Use this feature to reduce the cost of SMTP services and add extra layer of durability. When one of the transports is failing to send an email — `mail-time` will switch to the next one
154
+ - __Sending retries__ — Built-in retries for failed to send emails due to network or other failures
155
+ - __Templating__ — Built with support of [Mustache](https://mustache.github.io/)-like placeholders, see [templating docs](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#passing-variables-to-the-template)
124
156
 
125
157
  ## Installation
126
158
 
127
- To implement Server functionality — as a first step install `nodemailer`, although this package meant to be used with `nodemailer`, it's not added as the dependency, as `nodemailer` not needed by Client, and you're free to choose `nodemailer`'s version to fit your project needs:
159
+ To implement Server functionality — begin with installing `nodemailer`, although this package meant to be used with `nodemailer`, it's not added as the dependency, as `nodemailer` not needed by Client, and to give freedom to choose `nodemailer`'s version to fit every project needs:
128
160
 
129
161
  ```shell
130
162
  npm install --save nodemailer
@@ -133,31 +165,57 @@ npm install --save nodemailer
133
165
  Install *MailTime* package:
134
166
 
135
167
  ```shell
136
- # for node@>=8.9.0
168
+ # for node@>=14.20.0
137
169
  npm install --save mail-time
138
170
 
171
+ # for node@<14.20.0
172
+ npm install --save mail-time@=1.3.4
173
+
139
174
  # for node@<8.9.0
140
175
  npm install --save mail-time@=0.1.7
141
176
  ```
142
177
 
143
178
  ## Basic usage
144
179
 
145
- Require package:
180
+ Setup Nodemailer's transports, Queue storage, and *MailTime* instance
181
+
182
+ ### Steps to get started
183
+
184
+ See steps 1-4 below to learn about different parts of *MailTime* library and how it can get used. From configuration options to sending email
185
+
186
+ 1. [Require `mail-time` package](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#1-require-package)
187
+ 2. [Create NodeMailer's transports](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#2-create-nodemailers-transports)
188
+ 3. [Initiate `mail-time` *server*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3-initiate-mail-time)
189
+ - a. [Connect to Redis](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3a-initiate-and-connect-to-redis); Or
190
+ - b. [Connect to MongoDB](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3b-initiate-and-connect-to-mongodb); And
191
+ - c. [*optionally*] [initiate `mail-time` as *client*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3c-optionally-create-client-type-of-mailtime)
192
+ 4. [Start sending emails](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#4-send-email)
193
+
194
+ #### 1. Require package
146
195
 
147
196
  ```js
148
- const MailTime = require('mail-time');
197
+ // import as ES Module
198
+ import { MailTime, MongoQueue, RedisQueue } from 'mail-time';
199
+
200
+ // require as CommonJS
201
+ const { MailTime, MongoQueue, RedisQueue } = require('mail-time');
149
202
  ```
150
203
 
151
- Create nodemailer's transports (see [`nodemailer` docs](https://github.com/nodemailer/nodemailer/tree/v2#setting-up)):
204
+ #### 2. Create NodeMailer's transports
152
205
 
153
- ```js
154
- const transports = [];
155
- const nodemailer = require('nodemailer');
206
+ For compatibility and flexibility *MailTime* has no dependency on `nodemailer` it should be installed and imported manually. Create one or more "SMTP transports" before initializing new *MailTime* instance.
156
207
 
208
+ For details and full list of options available in `.createTransport()` see [`nodemailer` docs](https://nodemailer.com/smtp/)
209
+
210
+ ```js
211
+ // transports.js
212
+ import nodemailer from 'nodemailer';
157
213
  // Use DIRECT transport
158
214
  // and enable sending email from localhost
159
215
  // install "nodemailer-direct-transport" NPM package:
160
- const directTransport = require('nodemailer-direct-transport');
216
+ import directTransport from 'nodemailer-direct-transport';
217
+
218
+ const transports = [];
161
219
  const directTransportOpts = {
162
220
  pool: false,
163
221
  direct: true,
@@ -199,62 +257,123 @@ transports.push(nodemailer.createTransport({
199
257
  pass: 'xxx'
200
258
  },
201
259
  }));
260
+
261
+ export { transports };
202
262
  ```
203
263
 
204
- As the next step initiate `mail-time` in the *Server* mode, it will be able to __send__ and __add__ emails to the queue. Connecting to a MongoDB before initiating `new MailTime` instance:
264
+ #### 3. Initiate `mail-time`
265
+
266
+ Create new instance of *MailTime* in the *Server* mode, — it will be able to __send__ and __add__ emails to the queue.
267
+
268
+ #### 3a. Initiate and connect to Redis
269
+
270
+ Connecting to Redis before initiating `new MailTime` instance:
205
271
 
206
272
  ```js
207
- const MailTime = require('mail-time');
208
- const MongoClient = require('mongodb').MongoClient;
273
+ // mail-queue.js
274
+ import { MailTime, RedisQueue } from 'mail-time';
275
+ import { createClient } from 'redis';
276
+ import { transports } from './transports.js';
277
+
278
+ // Use REDIS_URL environment variable to store connection string to MongoDB
279
+ // example: "REDIS_URL=redis://127.0.0.1:6379/myapp node mail-micro-service.js"
280
+ const redisClient = await createClient({ url: process.env.REDIS_URL }).connect();
281
+ const mailQueue = new MailTime({
282
+ transports,
283
+ queue: new MongoQueue({
284
+ client: redisClient,
285
+ }),
286
+ josk: {
287
+ adapter: {
288
+ type: 'redis',
289
+ client: redisClient,
290
+ }
291
+ },
292
+ template: MailTime.Template // Use default template
293
+ from(transport) {
294
+ // To pass spam-filters `from` field should be correctly set
295
+ // for each transport, check `transport` object for more options
296
+ return `"Awesome App" <${transport.options.from}>`;
297
+ },
298
+ onError(error, email, details) {
299
+ console.log(`Email "${email.mailOptions.subject}" wasn't sent to ${email.mailOptions.to}`, error, details);
300
+ },
301
+ onSent(email, details) {
302
+ console.log(`Email "${email.mailOptions.subject}" successfully sent to ${email.mailOptions.to}`, details);
303
+ },
304
+ });
305
+
306
+ export { mailQueue };
307
+ ```
308
+
309
+ #### 3b. Initiate and connect to MongoDB
209
310
 
210
- const dbName = 'databaseName';
311
+ Connecting to MongoDB before initiating `new MailTime` instance:
312
+
313
+ ```js
314
+ // mail-queue.js
315
+ import { MailTime, MongoQueue } from 'mail-time';
316
+ import { MongoClient } from 'mongodb';
317
+ import { transports } from './transports.js';
211
318
 
212
319
  // Use MONGO_URL environment variable to store connection string to MongoDB
213
320
  // example: "MONGO_URL=mongodb://127.0.0.1:27017/myapp node mail-micro-service.js"
214
- MongoClient.connect(process.env.MONGO_URL, (error, client) => {
215
- const db = client.db(dbName);
216
-
217
- const mailQueue = new MailTime({
218
- db, // MongoDB
219
- type: 'server',
220
- strategy: 'balancer', // Transports will be used in round robin chain
221
- transports,
222
- from(transport) {
223
- // To pass spam-filters `from` field should be correctly set
224
- // for each transport, check `transport` object for more options
225
- return `"Awesome App" <${transport.options.from}>`;
226
- },
227
- concatEmails: true, // Concatenate emails to the same addressee
228
- concatDelimiter: '<h1>{{{subject}}}</h1>', // Start each concatenated email with it's own subject
229
- template: MailTime.Template // Use default template
230
- });
321
+ const mongodb = (await MongoClient.connect(process.env.MONGO_URL)).db('database');
322
+ const mailQueue = new MailTime({
323
+ transports,
324
+ queue: new MongoQueue({
325
+ db: mongodb,
326
+ }),
327
+ josk: {
328
+ adapter: {
329
+ type: 'mongo',
330
+ db: mongodb,
331
+ }
332
+ },
333
+ template: MailTime.Template // Use default template
334
+ from(transport) {
335
+ // To pass spam-filters `from` field should be correctly set
336
+ // for each transport, check `transport` object for more options
337
+ return `"Awesome App" <${transport.options.from}>`;
338
+ },
339
+ onError(error, email, details) {
340
+ console.log(`Email "${email.mailOptions.subject}" wasn't sent to ${email.mailOptions.to}`, error, details);
341
+ },
342
+ onSent(email, details) {
343
+ console.log(`Email "${email.mailOptions.subject}" successfully sent to ${email.mailOptions.to}`, details);
344
+ },
231
345
  });
346
+
347
+ export { mailQueue };
232
348
  ```
233
349
 
234
- Only __one__ `MailTime` *Server* instance required to send email. In the other parts of an app (like UI units or in sub-apps) use `mail-time` in the *Client* mode to __add__ emails to queue:
350
+ #### 3c. Optionally create *Client* type of *MailTime*
235
351
 
236
- ```js
237
- const MailTime = require('mail-time');
238
- const MongoClient = require('mongodb').MongoClient;
352
+ Only __one__ `MailTime` *Server* instance required to send email. In the other parts of an app (like UI units or in sub-apps) use `mail-time` in the *Client* mode to __add__ emails to queue
239
353
 
240
- const dbName = 'databaseName';
241
-
242
- MongoClient.connect(process.env.MONGO_URL, (error, client) => {
243
- const db = client.db(dbName);
354
+ ```js
355
+ // mail-queue.js
356
+ import { MailTime, RedisQueue } from 'mail-time';
357
+ import { createClient } from 'redis';
244
358
 
245
- const mailQueue = new MailTime({
246
- db,
247
- type: 'client',
248
- strategy: 'balancer', // Transports will be used in round robin chain
249
- concatEmails: true // Concatenate emails to the same address
250
- });
359
+ const mailQueue = new MailTime({
360
+ type: 'client',
361
+ queue: new RedisQueue({
362
+ client: await createClient({ url: 'redis://url' }).connect()
363
+ }),
251
364
  });
365
+
366
+ export { mailQueue };
252
367
  ```
253
368
 
254
- Send email example:
369
+ #### 4. Send email
370
+
371
+ Import created `mailQueue` where needed and call `.sendMail()` method. See [NodeMailer's message configuration documentation](https://nodemailer.com/message/) for details
255
372
 
256
373
  ```js
257
- mailQueue.sendMail({
374
+ import { mailQueue } from './mail-queue.js';
375
+
376
+ await mailQueue.sendMail({
258
377
  to: 'user@gmail.com',
259
378
  subject: 'You\'ve got an email!',
260
379
  text: 'Plain text message',
@@ -262,41 +381,174 @@ mailQueue.sendMail({
262
381
  });
263
382
  ```
264
383
 
384
+ ### Using MongoDB for queue and scheduler
385
+
386
+ *MailTime* uses separate storage for Queue management and Scheduler. In the example below MongoDB is used for both
387
+
388
+ ```js
389
+ import { MailTime, MongoQueue } from 'mail-time';
390
+ import { MongoClient } from 'mongodb';
391
+ import { transports } from './transports.js';
392
+
393
+ const db = (await MongoClient.connect('mongodb://url')).db('database');
394
+ const mailQueue = new MailTime({
395
+ queue: new MongoQueue({
396
+ db: db,
397
+ }),
398
+ josk: {
399
+ adapter: {
400
+ type: 'mongo',
401
+ db: db,
402
+ }
403
+ },
404
+ transports,
405
+ from(transport) {
406
+ // To pass spam-filters `from` field should be correctly set
407
+ // for each transport, check `transport` object for more options
408
+ return `"Awesome App" <${transport.options.from}>`;
409
+ }
410
+ });
411
+ ```
412
+
413
+ ### Using MongoDB for queue and Redis for scheduler
414
+
415
+ *MailTime* uses separate storage for Queue management and Scheduler. In the example below MongoDB is used for queue and Redis is used for scheduler
416
+
417
+ ```js
418
+ import { MailTime, MongoQueue } from 'mail-time';
419
+ import { MongoClient } from 'mongodb';
420
+ import { createClient } from 'redis';
421
+ import { transports } from './transports.js';
422
+
423
+ const mailQueue = new MailTime({
424
+ queue: new MongoQueue({
425
+ db: (await MongoClient.connect('mongodb://url')).db('database'),
426
+ }),
427
+ josk: {
428
+ adapter: {
429
+ type: 'redis',
430
+ client: await createClient({ url: 'redis://url' }).connect(),
431
+ }
432
+ },
433
+ transports,
434
+ from(transport) {
435
+ return `"Awesome App" <${transport.options.from}>`;
436
+ }
437
+ });
438
+ ```
439
+
440
+ ### Using Redis for queue and MongoDB for scheduler
441
+
442
+ *MailTime* uses separate storage for Queue management and Scheduler. In the example below Redis is used for queue and MongoDB is used for scheduler
443
+
444
+ ```js
445
+ import { MailTime, RedisQueue } from 'mail-time';
446
+ import { MongoClient } from 'mongodb';
447
+ import { createClient } from 'redis';
448
+ import { transports } from './transports.js';
449
+
450
+ const mailQueue = new MailTime({
451
+ queue: new RedisQueue({
452
+ client: await createClient({ url: 'redis://url' }).connect(),
453
+ }),
454
+ josk: {
455
+ adapter: {
456
+ type: 'mongo',
457
+ db: (await MongoClient.connect('mongodb://url')).db('database'),
458
+ }
459
+ },
460
+ transports,
461
+ from(transport) {
462
+ return `"Awesome App" <${transport.options.from}>`;
463
+ }
464
+ });
465
+ ```
466
+
467
+ ### Using Redis for queue and scheduler
468
+
469
+ *MailTime* uses separate storage for Queue management and Scheduler. In the example below Redis is used for both
470
+
471
+ ```js
472
+ import { MailTime, RedisQueue } from 'mail-time';
473
+ import { MongoClient } from 'mongodb';
474
+ import { createClient } from 'redis';
475
+ import { transports } from './transports.js';
476
+
477
+ const redisClient = await createClient({ url: 'redis://url' }).connect();
478
+ const mailQueue = new MailTime({
479
+ queue: new RedisQueue({
480
+ client: redisClient,
481
+ }),
482
+ josk: {
483
+ adapter: {
484
+ type: 'redis',
485
+ client: redisClient,
486
+ }
487
+ },
488
+ transports,
489
+ from(transport) {
490
+ return `"Awesome App" <${transport.options.from}>`;
491
+ }
492
+ });
493
+ ```
494
+
265
495
  ### Two `MailTime` instances usage example
266
496
 
267
- Create two `MailTime` instances with different settings.
497
+ Create two `MailTime` instances with different settings. One for urgent (*e.g. "transactional" emails*), and another one for other types of emails (*e.g. "marketing" emails*)
268
498
 
269
499
  ```js
500
+ import { MailTime, RedisQueue } from 'mail-time';
501
+ import { createClient } from 'redis';
502
+ import { transports } from './transports.js';
503
+ const redisClient = await createClient({ url: 'redis://url' }).connect();
504
+
270
505
  // CREATE mailQueue FOR NON-URGENT EMAILS WHICH IS OKAY TO CONCATENATE
271
506
  const mailQueue = new MailTime({
272
- db: db,
273
- interval: 35,
507
+ queue: new RedisQueue({
508
+ client: redisClient,
509
+ }),
510
+ transports,
274
511
  strategy: 'backup',
275
512
  failsToNext: 1,
276
513
  concatEmails: true,
277
- concatThrottling: 16,
278
- zombieTime: 120000
514
+ josk: {
515
+ adapter: {
516
+ type: 'redis',
517
+ client: redisClient
518
+ },
519
+ zombieTime: 120000
520
+ }
279
521
  });
280
522
 
281
523
  // CREATE mailInstantQueue FOR TRANSACTIONAL EMAILS AND ALERTS
282
524
  const mailInstantQueue = new MailTime({
283
- db: db,
525
+ queue: new RedisQueue({
526
+ client: redisClient,
527
+ prefix: 'instant'
528
+ }),
529
+ transports,
284
530
  prefix: 'instant',
285
- interval: 2,
531
+ retryDelay: 2000,
286
532
  strategy: 'backup',
287
533
  failsToNext: 1,
288
534
  concatEmails: false,
289
- zombieTime: 20000
535
+ josk: {
536
+ adapter: {
537
+ type: 'redis',
538
+ client: redisClient
539
+ },
540
+ zombieTime: 20000
541
+ }
290
542
  });
291
543
 
292
- mailQueue.sendMail({
544
+ await mailQueue.sendMail({
293
545
  to: 'user@gmail.com',
294
546
  subject: 'You\'ve got an email!',
295
547
  text: 'Plain text message',
296
548
  html: '<h1>HTML</h1><p>Styled message</p>'
297
549
  });
298
550
 
299
- mailInstantQueue.sendMail({
551
+ await mailInstantQueue.sendMail({
300
552
  to: 'user@gmail.com',
301
553
  subject: 'Sign in request',
302
554
  text: 'Your OTP login code: xxxx:',
@@ -306,7 +558,7 @@ mailInstantQueue.sendMail({
306
558
 
307
559
  ### Passing variables to the template
308
560
 
309
- All options passed to the `.sendMail()` method is available inside `text`, `html`, and global templates
561
+ All options passed to the `.sendMail()` method are available inside `text`, `html`, and global templates
310
562
 
311
563
  ```js
312
564
  const templates = {
@@ -318,11 +570,11 @@ const templates = {
318
570
  };
319
571
 
320
572
  const mailQueue = new MailTime({
321
- db: db,
573
+ queue: new RedisQueue({ /* ... */ }),
322
574
  template: templates.global
323
575
  });
324
576
 
325
- mailQueue.sendMail({
577
+ await mailQueue.sendMail({
326
578
  to: 'user@gmail.com',
327
579
  subject: 'Sign-in request',
328
580
  username: 'johndoe',
@@ -338,53 +590,170 @@ All available constructor options and `.sendMail()` method API overview
338
590
 
339
591
  ### `new MailTime(opts)` constructor
340
592
 
341
- - `opts` {*Object*} - Configuration object
342
- - `opts.db` {*Db*} - [Required] Mongo's `Db` instance. For example returned in callback of `MongoClient.connect()`
343
- - `opts.type` {*String*} - [Optional] `client` or `server`, default - `server`
344
- - `opts.from` {*Function*} - [Optional] A function which returns *String* of `from` field, format: `"MyApp" <user@example.com>`
345
- - `opts.transports` {*Array*} - [Optional] An array of `nodemailer`'s transports, returned from `nodemailer.createTransport({})`
346
- - `opts.strategy` {*String*} - [Optional] `backup` or `balancer`, default - `backup`. If set to `backup`, first transport will be used unless failed to send `failsToNext` times. If set to `balancer` - transports will be used equally in round robin chain
347
- - `opts.failsToNext` {*Number*} - [Optional] After how many failed "send attempts" switch to next transport, applied only for `backup` strategy, default - `4`
348
- - `opts.prefix` {*String*} - [Optional] Use unique prefixes to create multiple `MailTime` instances on same MongoDB
349
- - `opts.maxTries` {*Number*} - [Optional] How many times resend failed emails, default - `60`
350
- - `opts.interval` {*Number*} - [Optional] Interval in *seconds* between send re-tries, default - `60`
351
- - `opts.zombieTime` {*Number*} - [Optional] Time in *milliseconds*, after this period - pending email will be interpreted as "*zombie*". This parameter allows to rescue pending email from "*zombie* mode" in case when: server was rebooted, exception during runtime was thrown, or caused by bad logic, default - `32786`. This option is used by package itself and passed directly to [`JoSk` package](https://github.com/veliovgroup/josk#api)
352
- - `opts.keepHistory` {*Boolean*} - [Optional] By default sent emails not stored in the database. Set `{ keepHistory: true }` to keep queue task as it is in the database, default - `false`
353
- - `opts.concatEmails` {*Boolean*} - [Optional] Concatenate email by `to` field, default - `false`
354
- - `opts.concatSubject` {*String*} - [Optional] Email subject used in concatenated email, default - `Multiple notifications`
355
- - `opts.concatDelimiter` {*String*} - [Optional] HTML or plain string delimiter used between concatenated email, default - `<hr>`
356
- - `opts.concatThrottling` {*Number*} - [Optional] Time in *seconds* while emails are waiting to be concatenated, default - `60`
357
- - `opts.revolvingInterval` {*Number*} - [Optional] Interval in *milliseconds* in between queue checks, default - `256`. Recommended value — between `opts.minRevolvingDelay` and `opts.maxRevolvingDelay`
358
- - `opts.minRevolvingDelay` {*Number*} - [Optional] Minimum revolving delay — the minimum delay between tasks executions in *milliseconds*, default - `64`. This option is passed directly to [`JoSk` package](https://github.com/veliovgroup/josk#api)
359
- - `opts.maxRevolvingDelay` {*Number*} - [Optional] Maximum revolving delay — the maximum delay between tasks executions in *milliseconds*, default - `256`. This option is passed directly to [`JoSk` package](https://github.com/veliovgroup/josk#api)
360
- - `opts.template` {*String*} - [Optional] Mustache-like template, default - `{{{html}}}`, all options passed to `sendMail` is available in Template, like `to`, `subject`, `text`, `html` or any other custom option. Use `{{opt}}` for string placeholders and `{{{opt}}}` for html placeholders
361
-
362
- ### `sendMail(opts [, callback])`
363
-
364
- - Alias - `send()`
365
- - `opts` {*Object*} - Configuration object
366
- - `opts.sendAt` {*Date*} - When email should be sent, default - `new Date()` use with caution on multi-server setup at different location with the different time-zones
367
- - `opts.template` - Email specific template, this will override default template passed to `MailTime` constructor
368
- - `opts.concatSubject` - Email specific concatenation subject, this will override default concatenation subject passed to `MailTime` constructor
369
- - `opts[key]` {*Mix*} - Other custom and NodeMailer specific options, like `text`, `html` and `to`, [learn more here](https://github.com/nodemailer/nodemailer/tree/v2#e-mail-message-fields). Note `attachments` should work only via `path`, and file must exists on all micro-services servers
370
- - `callback` {*Function*} - Callback called after the email was sent or failed to be sent. __Do not use on multi-server setup__
593
+ - `opts` {*object*} - Configuration object
594
+ - `opts.type` {*string*} - [Optional] `client` or `server`, default - `server`
595
+ - `opts.queue` {*RedisQueue*|*MongoQueue*|*CustomQueue*} - Queue storage driver instance
596
+ - `opts.transports` {*[object]*} - [*Required for "server"*] An array of `nodemailer`'s transports, returned from `nodemailer.createTransport({})`. Required for `{type: 'server'}`
597
+ - `opts.josk` {*object*} - [*Required for "server"*] [`JoSk` package](https://github.com/veliovgroup/josk#api) options
598
+ - `opts.josk.adapter` {*object*|*RedisAdapter*|*MongoAdapter*|*CustomAdapter*} - Config object or *Adapter* instance
599
+ - `opts.josk.adapter.type` {*string*} - One of `mongo` *or* `redis`; Pass `josk.adapter.type` to avoid burden of creating *Adapter* instance manually
600
+ - `opts.josk.adapter.client` {*RedisClient*} - *RedisClient* instance
601
+ - `opts.josk.adapter.db` {*Db*} - Mongo's *Db* instance
602
+ - `opts.josk[option]` {*mix*} - Any other options passed to [`JoSk` instance](https://github.com/veliovgroup/josk#api)
603
+ - `opts.from` {*function*} - [Optional] A function which returns *string* of `from` field, format: `"MyApp" <user@example.com>`
604
+ - `opts.strategy` {*string*} - [Optional] `backup` or `balancer`, default - `backup`. If set to `backup`, first transport will be used unless failed to send `failsToNext` times. If set to `balancer` - transports will be used equally in round robin chain
605
+ - `opts.failsToNext` {*number*} - [Optional] After how many failed "send attempts" switch to the next transport, applied only for `backup` strategy, default - `4`
606
+ - `opts.prefix` {*string*} - [Optional] Use unique prefixes to create multiple `MailTime` instances within the same application
607
+ - `opts.retries` {*number*} - [Optional] How many times resend failed emails, default - `60`
608
+ - `opts.retryDelay` {*number*} - [Optional] Interval in *milliseconds* between send re-tries, default - `60000`
609
+ - `opts.keepHistory` {*boolean*} - [Optional] By default sent emails not stored in the database. Set `{ keepHistory: true }` to keep queue task as it is in the database, default - `false`
610
+ - `opts.concatEmails` {*boolean*} - [Optional] Concatenate email by `to` field (*e.g. to the same addressee*), default - `false`
611
+ - `opts.concatSubject` {*string*} - [Optional] Email subject used in concatenated email, default - `Multiple notifications`
612
+ - `opts.concatDelimiter` {*string*} - [Optional] HTML or plain string delimiter used between concatenated email, default - `<hr>`
613
+ - `opts.concatDelay` {*number*} - [Optional] Time in *milliseconds* while emails are waiting to be concatenated, default - `60000`
614
+ - `opts.revolvingInterval` {*number*} - [Optional] Interval in *milliseconds* in between queue checks, default - `256`
615
+ - `opts.template` {*string*} - [Optional] Mustache-like template, default - `{{{html}}}`, all options passed to `sendMail` is available in Template, like `to`, `subject`, `text`, `html` or any other custom option. Use `{{opt}}` for string placeholders and `{{{opt}}}` for html placeholders
616
+ - `opts.onError(error, email, details)` {*function*} - [Optional] called when email has failed to get sent and exhausted all send attempts (`opts.retries`), called with 3 arguments:
617
+ - `error` {*Error*|*object*} - Error object
618
+ - `email` {*object*} - email's object
619
+ - `details` {*object*} - *not always present*, details from SMTP protocol
620
+ - `opts.onSent(email, details)` {*function*} - [Optional] called when email was successfully handed over to receiving/recipient's SMTP server, called with 2 arguments:
621
+ - `email` {*object*} - email's object
622
+ - `details` {*object*} - *not always present*, details from SMTP server/protocol
623
+
624
+ ```js
625
+ import { MailTime, MongoQueue, RedisQueue } from 'mail-time';
626
+ import nodemailer from 'nodemailer';
627
+ import { createClient } from 'redis';
628
+
629
+ const redisClient = await createClient({ url: 'redis://url' }).connect();
630
+
631
+ const mailQueue = new MailTime({
632
+ type: 'server',
633
+ strategy: 'backup',
634
+ prefix: 'appMailQueue',
635
+ transports: [nodemailer.createTransport({/* ... */})],
636
+ failsToNext: 4,
637
+ retries: 60,
638
+ retryDelay: 60000,
639
+ keepHistory: false,
640
+ concatEmails: false,
641
+ concatDelay: 60000,
642
+ concatDelimiter: '<hr>',
643
+ concatSubject: 'Multiple notifications',
644
+ revolvingInterval: 256,
645
+ template: '{{{html}}}',
646
+ queue: new RedisQueue({
647
+ client: redisClient,
648
+ prefix: 'appMailQueue',
649
+ }),
650
+ josk: {
651
+ adapter: {
652
+ type: 'redis',
653
+ client: redisClient,
654
+ }
655
+ },
656
+ from(transport) {
657
+ // To pass spam-filters `from` field should be correctly set
658
+ // for each transport, check `transport` object for more options
659
+ return `"App Name" <${transport.options.from}>`;
660
+ },
661
+ onError(error, email, details) {
662
+ console.log(`Email "${email.mailOptions.subject}" wasn't sent to ${email.mailOptions.to}`, error, details);
663
+ },
664
+ onSent(email, details) {
665
+ console.log(`Email "${email.mailOptions.subject}" successfully sent to ${email.mailOptions.to}`, details);
666
+ },
667
+ });
668
+
669
+ await mailQueue.sendMail({
670
+ to: 'johndoe@example.com',
671
+ subject: 'Email subject',
672
+ text: 'You have got email!',
673
+ html: '<p>You have got email!</p>',
674
+ });
675
+ ```
676
+
677
+ ### `new RedisQueue(opts)` constructor
678
+
679
+ *Create Redis Queue instance.* Use for `opts.queue` when creating *MailTime* instance
680
+
681
+ - `opts` {*object*} - Configuration object
682
+ - `opts.client` {*RedisClient*} - Required, Redis'es `RedisClient` instance, like one returned from `await redis.createClient().connect()` method
683
+ - `opts.prefix` {*string*} - Optional prefix for scope isolation; use when creating multiple `MailTime` instances within the single application
684
+
685
+ ```js
686
+ import { MailTime, RedisQueue } from 'mail-time';
687
+ import { createClient } from 'redis';
688
+
689
+ new RedisQueue({
690
+ client: await createClient({ url: 'redis://url' }).connect(),
691
+ prefix: 'appMailQueue',
692
+ });
693
+ ```
694
+
695
+ ### `new MongoQueue(opts)` constructor
696
+
697
+ *Create MongoDB Queue instance.* Use for `opts.queue` when creating *MailTime* instance
698
+
699
+ - `opts` {*object*} - Configuration object
700
+ - `opts.db` {*Db*} - Required, Mongo's `Db` instance, like one returned from `MongoClient#db()`
701
+ - `opts.prefix` {*string*} - Optional prefix for scope isolation; use when creating multiple `MailTime` instances within the single application
702
+
703
+ ```js
704
+ import { MailTime, MongoQueue } from 'mail-time';
705
+ import { MongoClient } from 'mongodb';
706
+
707
+ new MongoQueue({
708
+ db: (await MongoClient.connect('mongodb://url')).db('database'),
709
+ prefix: 'appMailQueue',
710
+ });
711
+ ```
712
+
713
+ ### `sendMail(opts)`
714
+
715
+ *Add email to the queue.* Returns `Promise<string>` unique email's `uuid`
716
+
717
+ - `opts` {*object*} - Configuration object
718
+ - `opts.sendAt` {*number*} - When email should be sent, default - `Date.now()`
719
+ - `opts.template` {*string*} - Email specific template, this will override default template passed to `MailTime` constructor
720
+ - `opts.concatSubject` {*string*} - Email specific concatenation subject, this will override default concatenation subject passed to `MailTime` constructor
721
+ - `opts[key]` {*mix*} - Other custom and NodeMailer specific options, like `text`, `html` and `to`, [learn more here](https://nodemailer.com/message/). __Note:__ if [`attachments`](https://nodemailer.com/message/attachments/) are used via `path` — file must exists on all micro-services servers
722
+
723
+ ### `cancelMail(uuid)`
724
+
725
+ *Remove email from queue.* Returns `Promise<boolean>` — `true` if cancelled or `false` if not found, was sent, or was cancelled previously. Throws *Error*
726
+
727
+ - `uuid` {*string*|*promise*} — email's `uuid` returned from `.sendEmail()` method
728
+
729
+ ```js
730
+ import { mailQueue } from './mail-queue.js';
731
+
732
+ const uuid = await mailQueue.sendMail({
733
+ to: 'johndoe@example.com',
734
+ subject: 'Email subject',
735
+ text: 'You have got email!',
736
+ html: '<p>You have got email!</p>',
737
+ });
738
+
739
+ await mailQueue.cancelMail(uuid);
740
+ ```
371
741
 
372
742
  ### `static MailTime.Template`
373
743
 
374
744
  Simple and bulletproof HTML template, see [its source](https://github.com/veliovgroup/mail-time/blob/master/template.html). Usage example:
375
745
 
376
746
  ```js
377
- const MailTime = require('mail-time');
747
+ import { MailTime, MongoQueue, RedisQueue } from 'mail-time';
748
+
378
749
  // Make it default
379
750
  const mailQueue = new MailTime({
380
- db: db, // MongoDB
381
751
  /* .. */
382
752
  template: MailTime.Template
383
753
  });
384
754
 
385
755
  // For single letter
386
756
  mailQueue.sendMail({
387
- to: 'user@gmail.com',
388
757
  /* .. */
389
758
  template: MailTime.Template
390
759
  });
@@ -408,28 +777,34 @@ mailQueue.sendMail({
408
777
  ## Testing
409
778
 
410
779
  1. Clone this package
411
- 2. In Terminal (*Console*) go to directory where package is cloned
412
- 3. Then run:
413
-
414
- Test NPM package:
780
+ 2. Start local or obtain URLs for remote MongoDB and Redis servers
781
+ 3. In Terminal (*Console*) go to directory where package was cloned
782
+ 4. Then run:
415
783
 
416
784
  ```shell
417
- # Before run tests make sure NODE_ENV === development
785
+ # Before running tests make sure NODE_ENV === development
418
786
  # Install NPM dependencies
419
787
  npm install --save-dev
420
788
 
421
- # Before run tests you need to have running MongoDB
422
- DEBUG="true" EMAIL_DOMAIN="example.com" MONGO_URL="mongodb://127.0.0.1:27017/npm-mail-time-test-001" npm test
789
+ # DEFAULT RUN
790
+ REDIS_URL="redis://127.0.0.1:6379" MONGO_URL="mongodb://127.0.0.1:27017/npm-mail-time-test-001" npm test
791
+
792
+ # OPTIONALLY RUN WITH CUSTOM DOMAIN
793
+ EMAIL_DOMAIN="your-domain.com" REDIS_URL="redis://127.0.0.1:6379" MONGO_URL="mongodb://127.0.0.1:27017/npm-mail-time-test-001" npm test
794
+
795
+ # IF SOME TESTS ARE FAILING: ENABLE DEBUG
796
+ DEBUG="true" REDIS_URL="redis://127.0.0.1:6379" MONGO_URL="mongodb://127.0.0.1:27017/npm-mail-time-test-001" npm test
423
797
 
424
- # Be patient, tests are taking around 2 mins
798
+ # Be patient, tests are taking around 8 mins
425
799
  ```
426
800
 
427
801
  ## Support this project:
428
802
 
803
+ - 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
804
+ - 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
805
  - Star on [GitHub](https://github.com/veliovgroup/mail-time)
430
806
  - Star on [NPM](https://www.npmjs.com/package/mail-time)
431
807
  - Star on [Atmosphere](https://atmospherejs.com/ostrio/mailer)
432
808
  - [Sponsor maintainer via GitHub](https://github.com/sponsors/dr-dimitru) — support open source with one-time contribution or on a regular basis
433
809
  - [Sponsor veliovgroup via GitHub](https://github.com/sponsors/veliovgroup) — support company behind this package
434
810
  - [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