mail-time 3.0.0 โ 4.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/LICENSE +1 -1
- package/README.md +325 -609
- package/adapters/mongo.d.ts +118 -0
- package/adapters/mongo.js +199 -78
- package/adapters/postgres.d.ts +100 -0
- package/adapters/postgres.js +520 -0
- package/adapters/redis.d.ts +105 -0
- package/adapters/redis.js +342 -79
- package/helpers.js +209 -3
- package/index.cjs +2219 -446
- package/index.d.cts +300 -0
- package/index.d.ts +300 -0
- package/index.js +817 -305
- package/package.json +66 -17
- package/presets.d.ts +87 -0
- package/presets.js +193 -0
- package/adapters/blank-example.js +0 -230
package/README.md
CHANGED
|
@@ -1,81 +1,52 @@
|
|
|
1
|
-
[](https://github.com/sponsors/dr-dimitru)
|
|
2
|
-
[](https://paypal.me/veliovgroup)
|
|
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>
|
|
5
1
|
|
|
6
2
|
# MailTime
|
|
7
3
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## ToC
|
|
15
|
-
|
|
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)
|
|
22
|
-
- [Meteor.js usage](https://github.com/veliovgroup/mail-time/blob/master/docs/meteor.md)
|
|
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)
|
|
46
|
-
|
|
47
|
-
## Main features:
|
|
48
|
-
|
|
49
|
-
- ๐จโ๐ฌ ~94% tests coverage;
|
|
50
|
-
- ๐ฆ Two simple dependencies, written from scratch for top performance;
|
|
51
|
-
- ๐ข Synchronize email queue across multiple (horizontally scaled) servers;
|
|
52
|
-
- ๐ช Bulletproof design, built-in retries.
|
|
53
|
-
|
|
54
|
-
## How does it work?
|
|
55
|
-
|
|
56
|
-
Redundant solution for email transmission.
|
|
4
|
+
[](https://www.npmjs.com/package/mail-time)
|
|
5
|
+
[](https://github.com/veliovgroup/mail-time/actions)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://github.com/sponsors/dr-dimitru)
|
|
8
|
+
[](https://paypal.me/veliovgroup)
|
|
57
9
|
|
|
58
|
-
|
|
10
|
+
Bulletproof email queue for [horizontally scaled](#sending-emails-from-a-cluster) Node.js & Bun apps. Built on top of [`nodemailer`](https://github.com/nodemailer/nodemailer) and [`josk`](https://github.com/veliovgroup/josk). Single runtime dependency, ESM + CJS, full TypeScript declarations.
|
|
11
|
+
|
|
12
|
+
`MailTime` runs in one of two modes:
|
|
13
|
+
|
|
14
|
+
- **`server`** โ drains the queue and sends emails via SMTP. The cluster-aware lease guarantees exactly one server sends each email even when many are running.
|
|
15
|
+
- **`client`** โ only enqueues emails. Use for app servers in a "dedicated mail micro-service" topology.
|
|
16
|
+
|
|
17
|
+
Many clients + one or more servers coexist behind the same `prefix` in the same store.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
59
20
|
|
|
60
|
-
|
|
21
|
+
- ๐ข **Horizontally scaled** โ synchronize one queue across N processes/hosts/DCs.
|
|
22
|
+
- ๐ **Multi-SMTP rotation** โ `backup` (failover) and `balancer` (round-robin) strategies.
|
|
23
|
+
- ๐ช **Built-in retries** โ storage-backed retry with per-letter transport pinning.
|
|
24
|
+
- ๐ฏ **Per-recipient retries** โ when a multi-`to` send is partially rejected, only the un-accepted addresses are retried; delivered ones never see a duplicate.
|
|
25
|
+
- ๐ฎ **Email concatenation** โ fold same-`to` emails arriving inside a window into one letter.
|
|
26
|
+
- ๐๏ธ **One-line setup** โ built-in [presets](#settings-presets) for `transactional`, `otp`, `newsletter`, `marketing`, `notifications`, `alerts`.
|
|
27
|
+
- ๐ข๏ธ **Three first-party storages** โ MongoDB, Redis, PostgreSQL. Plus a [custom-adapter contract](https://github.com/veliovgroup/mail-time/blob/master/docs/queue-api.md).
|
|
28
|
+
- ๐ฆ **Bun โฅ 1.1.0 & Node โฅ 20.9.0** โ same code, both runtimes.
|
|
29
|
+
- ๐ค **Ships with AI agent skills** โ see [AI agent skills](#ai-agent-skills) below.
|
|
30
|
+
- ๐ **Hand-tuned ESM + CJS + TypeScript declarations**.
|
|
31
|
+
- ๐งช **95%+ Jest coverage** (85% threshold enforced) + Mocha integration tests for every adapter.
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
### Single point of failure
|
|
61
36
|
|
|
62
37
|
```ascii
|
|
63
38
|
|----------------| |------| |------------------|
|
|
64
39
|
| Other mailer | ------> | SMTP | ------> | ^_^ Happy user |
|
|
65
40
|
|----------------| |------| |------------------|
|
|
66
41
|
|
|
67
|
-
The scheme above
|
|
68
|
-
or connection between your server and SMPT is up. Once network
|
|
69
|
-
failure occurs or SMTP service is down - users won't be happy
|
|
42
|
+
The scheme above works only as long as SMTP is up.
|
|
70
43
|
|
|
71
44
|
|----------------| \ / |------| |------------------|
|
|
72
45
|
| Other mailer | --X---> | SMTP | ------> | 0_o Disappointed |
|
|
73
46
|
|----------------| / \ |------| |------------------|
|
|
74
47
|
^- email lost in vain
|
|
75
48
|
|
|
76
|
-
|
|
77
|
-
As long as MailTime has not received confirmation what email is sent
|
|
78
|
-
it will keep the letter in the queue and retry to send it again
|
|
49
|
+
MailTime keeps every letter in the queue until SMTP confirms delivery.
|
|
79
50
|
|
|
80
51
|
|----------------| / |------| |------------------|
|
|
81
52
|
| Mail Time | --X---> | SMTP | ------> | ^_^ Happy user |
|
|
@@ -87,7 +58,7 @@ it will keep the letter in the queue and retry to send it again
|
|
|
87
58
|
|
|
88
59
|
### Multiple SMTP providers
|
|
89
60
|
|
|
90
|
-
|
|
61
|
+
`backup` falls over on failure; `balancer` round-robins:
|
|
91
62
|
|
|
92
63
|
```ascii
|
|
93
64
|
|--------|
|
|
@@ -103,18 +74,14 @@ Rotate email transports by using multiple SMTP providers. *MailTime* support two
|
|
|
103
74
|
|--------|
|
|
104
75
|
```
|
|
105
76
|
|
|
106
|
-
### Sending emails from cluster
|
|
77
|
+
### Sending emails from a cluster
|
|
107
78
|
|
|
108
|
-
|
|
109
|
-
|
|
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.
|
|
111
|
-
|
|
112
|
-
For the maximum durability and agility each Application Server can run *MailTime* in the "Server" mode:
|
|
79
|
+
Most apps schedule recurring emails (daily digest, weekly summary). On a single server this is trivial. In a cluster, every node would otherwise send the same email N times. MailTime's lease prevents the duplicate sends.
|
|
113
80
|
|
|
114
81
|
```ascii
|
|
115
82
|
|===================THE=CLUSTER===================| |=QUEUE=|
|
|
116
83
|
| |----------| |----------| |----------| | | | |--------|
|
|
117
|
-
| |
|
|
84
|
+
| | MailTime | | MailTime | | MailTime | | | |-->| SMTP 1 |------\
|
|
118
85
|
| | Server 1 | | Server 2 | | Server 3 | | | | |--------| \
|
|
119
86
|
| |-----\----| |----\-----| |----\-----| | | | |-------------|
|
|
120
87
|
| \---------------\----------------\----------> | |--------| | ^_^ |
|
|
@@ -127,17 +94,17 @@ For the maximum durability and agility each Application Server can run *MailTime
|
|
|
127
94
|
|=================================================| |=======|
|
|
128
95
|
```
|
|
129
96
|
|
|
130
|
-
|
|
97
|
+
For a dedicated mail machine (rDNS / PTR records), use `type: 'client'` on app servers and a single `type: 'server'` micro-service:
|
|
131
98
|
|
|
132
99
|
```ascii
|
|
133
100
|
|===================THE=CLUSTER===================| |=QUEUE=| |===Mail=Time===|
|
|
134
101
|
| |----------| |----------| |----------| | | | | | |--------|
|
|
135
|
-
| |
|
|
136
|
-
| |
|
|
102
|
+
| | MailTime | | MailTime | | MailTime | | | | | Micro-service |-->| SMTP 1 |------\
|
|
103
|
+
| | Client 1 | | Client 2 | | Client 3 | | | | | running | |--------| \
|
|
137
104
|
| |-----\----| |----\-----| |----\-----| | | | | MailTime as | |-------------|
|
|
138
105
|
| \---------------\----------------\----------> | | "Server" only | |--------| | ^_^ |
|
|
139
106
|
| | | | | sending |-->| SMTP 2 |-->| Happy users |
|
|
140
|
-
| Each "App
|
|
107
|
+
| Each "App" runs MailTime as | | | | emails | |--------| |-------------|
|
|
141
108
|
| a "Client" only placing emails to the queue. | | <-------- | /
|
|
142
109
|
| | | --------> | |--------| /
|
|
143
110
|
| | | | | |-->| SMTP 3 |-----/
|
|
@@ -145,666 +112,415 @@ To split roles *MailTime* can run on a dedicated machine as micro-service. This
|
|
|
145
112
|
|=================================================| |=======| |===============|
|
|
146
113
|
```
|
|
147
114
|
|
|
148
|
-
|
|
149
|
-
|
|
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)
|
|
115
|
+
See [docs/multi-instance.md](https://github.com/veliovgroup/mail-time/blob/master/docs/multi-instance.md) and [docs/dedicated-mail-host.md](https://github.com/veliovgroup/mail-time/blob/master/docs/dedicated-mail-host.md) for full topologies.
|
|
156
116
|
|
|
157
117
|
## Installation
|
|
158
118
|
|
|
159
|
-
|
|
119
|
+
```sh
|
|
120
|
+
npm install --save mail-time nodemailer
|
|
121
|
+
# pick at least one storage driver:
|
|
122
|
+
npm install --save redis # for RedisQueue
|
|
123
|
+
npm install --save mongodb # for MongoQueue
|
|
124
|
+
npm install --save pg # for PostgresQueue
|
|
160
125
|
|
|
161
|
-
|
|
162
|
-
|
|
126
|
+
# Bun:
|
|
127
|
+
bun add mail-time nodemailer
|
|
163
128
|
```
|
|
164
129
|
|
|
165
|
-
|
|
130
|
+
> [!NOTE]
|
|
131
|
+
> `nodemailer` and adapter drivers are peers (not bundled) so you can pin your own versions.
|
|
166
132
|
|
|
167
|
-
|
|
168
|
-
#
|
|
169
|
-
npm install --save mail-time
|
|
133
|
+
> [!IMPORTANT]
|
|
134
|
+
> Upgrading from v3? See [Migration from 3.x](#migration-from-3x) and the full [v4.0.0 release notes](https://github.com/veliovgroup/mail-time/blob/master/docs/migration-v3-v4.md).
|
|
170
135
|
|
|
171
|
-
|
|
172
|
-
npm install --save mail-time@=1.3.4
|
|
136
|
+
For Meteor.js usage see [docs/meteor.md](https://github.com/veliovgroup/mail-time/blob/master/docs/meteor.md).
|
|
173
137
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
138
|
+
## AI agent skills
|
|
139
|
+
|
|
140
|
+
MailTime ships a Claude / Copilot / Cursor / Codex / Gemini-ready skill bundle. Install it once in your project (or globally) and your AI agent will reach for the right preset, adapter, and pitfall list without you having to paste docs into the chat.
|
|
177
141
|
|
|
178
|
-
|
|
142
|
+
```sh
|
|
143
|
+
# Install the MailTime skill into the current project:
|
|
144
|
+
npx skills add veliovgroup/mail-time
|
|
179
145
|
|
|
180
|
-
|
|
146
|
+
# Recommended: also install the JoSk skill โ MailTime is built on JoSk,
|
|
147
|
+
# and deep scheduler questions resolve through JoSk's contract.
|
|
148
|
+
npx skills add veliovgroup/josk
|
|
149
|
+
```
|
|
181
150
|
|
|
182
|
-
|
|
151
|
+
The `npx skills` CLI ([vercel-labs/skills](https://github.com/vercel-labs/skills)) supports 50+ AI coding agents. Pass `-g` to install user-wide, or `-a claude-code` to target a specific agent. The bundled MailTime skill covers the public API, every queue adapter, the preset table, tuning levers, and common pitfalls โ it's the same material as the README and `docs/`, structured for an LLM.
|
|
183
152
|
|
|
184
|
-
|
|
153
|
+
## Quick start
|
|
185
154
|
|
|
186
|
-
|
|
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)
|
|
155
|
+
Three things every MailTime needs: a connected storage **client**, one or more **nodemailer transports** (server only), and a **`josk.adapter`** that points at the scheduler storage (server only). Past that, reach for a [preset](#settings-presets) instead of hand-tuning every knob.
|
|
193
156
|
|
|
194
|
-
|
|
157
|
+
### 1. Import
|
|
195
158
|
|
|
196
159
|
```js
|
|
197
|
-
//
|
|
198
|
-
import { MailTime, MongoQueue, RedisQueue } from 'mail-time';
|
|
160
|
+
// ESM
|
|
161
|
+
import { MailTime, MongoQueue, PostgresQueue, RedisQueue, mailTimePreset } from 'mail-time';
|
|
199
162
|
|
|
200
|
-
//
|
|
201
|
-
const { MailTime, MongoQueue, RedisQueue } = require('mail-time');
|
|
163
|
+
// CommonJS
|
|
164
|
+
const { MailTime, MongoQueue, PostgresQueue, RedisQueue, mailTimePreset } = require('mail-time');
|
|
202
165
|
```
|
|
203
166
|
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
167
|
+
### 2. Create nodemailer transports
|
|
207
168
|
|
|
208
|
-
|
|
169
|
+
Each transport must expose `.options` (set automatically by `nodemailer.createTransport({...})`). MailTime merges any `options.mailOptions` defaults onto every letter. To produce a `From:` header *from* a transport's `options.from`, set the constructor's `from: (t) => t.options.from` callback (the next example does this). Without that callback, `From:` falls back to per-letter `sendMail({ from })` or `options.mailOptions.from` on the transport itself.
|
|
209
170
|
|
|
210
171
|
```js
|
|
211
172
|
// transports.js
|
|
212
173
|
import nodemailer from 'nodemailer';
|
|
213
|
-
// Use DIRECT transport
|
|
214
|
-
// and enable sending email from localhost
|
|
215
|
-
// install "nodemailer-direct-transport" NPM package:
|
|
216
|
-
import directTransport from 'nodemailer-direct-transport';
|
|
217
|
-
|
|
218
|
-
const transports = [];
|
|
219
|
-
const directTransportOpts = {
|
|
220
|
-
pool: false,
|
|
221
|
-
direct: true,
|
|
222
|
-
name: 'mail.example.com',
|
|
223
|
-
from: 'no-reply@example.com',
|
|
224
|
-
};
|
|
225
|
-
transports.push(nodemailer.createTransport(directTransport(directTransportOpts)));
|
|
226
|
-
// IMPORTANT: Add `.options` to a newly created transport,
|
|
227
|
-
// this is necessary to make sure options are available to MailTime package:
|
|
228
|
-
transports[0].options = directTransportOpts;
|
|
229
|
-
|
|
230
|
-
// Private SMTP
|
|
231
|
-
transports.push(nodemailer.createTransport({
|
|
232
|
-
host: 'smtp.example.com',
|
|
233
|
-
from: 'no-reply@example.com',
|
|
234
|
-
auth: {
|
|
235
|
-
user: 'no-reply',
|
|
236
|
-
pass: 'xxx'
|
|
237
|
-
},
|
|
238
|
-
}));
|
|
239
|
-
|
|
240
|
-
// Google Apps SMTP
|
|
241
|
-
transports.push(nodemailer.createTransport({
|
|
242
|
-
host: 'smtp.gmail.com',
|
|
243
|
-
from: 'no-reply@mail.example.com',
|
|
244
|
-
auth: {
|
|
245
|
-
user: 'no-reply@mail.example.com',
|
|
246
|
-
pass: 'xxx'
|
|
247
|
-
},
|
|
248
|
-
}));
|
|
249
174
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
pass: 'xxx'
|
|
258
|
-
},
|
|
259
|
-
}));
|
|
260
|
-
|
|
261
|
-
export { transports };
|
|
175
|
+
export const transports = [
|
|
176
|
+
nodemailer.createTransport({
|
|
177
|
+
host: 'smtp.example.com',
|
|
178
|
+
from: 'no-reply@example.com',
|
|
179
|
+
auth: { user: 'no-reply', pass: process.env.SMTP_PASS },
|
|
180
|
+
}),
|
|
181
|
+
];
|
|
262
182
|
```
|
|
263
183
|
|
|
264
|
-
|
|
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
|
|
184
|
+
### 3. Initialize MailTime with a preset
|
|
269
185
|
|
|
270
|
-
|
|
186
|
+
Pick the preset that matches the email class. Supply your own `queue`, `transports`, `josk.adapter`, and `prefix`. **Setting `prefix` on the constructor propagates into the queue adapter and the JoSk adapter automatically** โ no need to repeat it.
|
|
271
187
|
|
|
272
188
|
```js
|
|
273
|
-
// mail-queue.js
|
|
274
|
-
import { MailTime, RedisQueue } from 'mail-time';
|
|
189
|
+
// mail-queue.js โ transactional emails on Redis
|
|
190
|
+
import { MailTime, RedisQueue, mailTimePreset } from 'mail-time';
|
|
275
191
|
import { createClient } from 'redis';
|
|
276
192
|
import { transports } from './transports.js';
|
|
277
193
|
|
|
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
194
|
const redisClient = await createClient({ url: process.env.REDIS_URL }).connect();
|
|
281
|
-
|
|
195
|
+
|
|
196
|
+
const mailQueue = new MailTime(mailTimePreset('transactional', {
|
|
197
|
+
type: 'server',
|
|
198
|
+
prefix: 'app',
|
|
199
|
+
queue: new RedisQueue({ client: redisClient }),
|
|
200
|
+
josk: { adapter: { type: 'redis', client: redisClient } },
|
|
282
201
|
transports,
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
josk: {
|
|
287
|
-
adapter: {
|
|
288
|
-
type: 'redis',
|
|
289
|
-
client: redisClient,
|
|
290
|
-
}
|
|
202
|
+
from: (t) => `"Awesome App" <${t.options.from}>`,
|
|
203
|
+
onSent(email, info) {
|
|
204
|
+
console.log('sent', email.uuid, info);
|
|
291
205
|
},
|
|
292
|
-
|
|
293
|
-
|
|
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}>`;
|
|
206
|
+
onError(error, email, info) {
|
|
207
|
+
console.error('failed', email.uuid, error, info);
|
|
297
208
|
},
|
|
298
|
-
|
|
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
|
-
});
|
|
209
|
+
}));
|
|
305
210
|
|
|
211
|
+
await mailQueue.ready();
|
|
306
212
|
export { mailQueue };
|
|
307
213
|
```
|
|
308
214
|
|
|
309
|
-
|
|
215
|
+
Switching stores is one import change. The same pattern works with `MongoQueue({ db })` + `{ type: 'mongo', db }`, or `PostgresQueue({ client: pgPool })` + `{ type: 'postgres', client: pgPool }`.
|
|
310
216
|
|
|
311
|
-
|
|
217
|
+
### 4. Send and cancel
|
|
312
218
|
|
|
313
219
|
```js
|
|
314
|
-
|
|
315
|
-
import { MailTime, MongoQueue } from 'mail-time';
|
|
316
|
-
import { MongoClient } from 'mongodb';
|
|
317
|
-
import { transports } from './transports.js';
|
|
220
|
+
import { mailQueue } from './mail-queue.js';
|
|
318
221
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
},
|
|
222
|
+
const uuid = await mailQueue.sendMail({
|
|
223
|
+
to: 'user@example.com',
|
|
224
|
+
subject: 'You\'ve got an email!',
|
|
225
|
+
text: 'Plain text body',
|
|
226
|
+
html: '<h1>HTML</h1><p>Styled body</p>',
|
|
345
227
|
});
|
|
346
228
|
|
|
347
|
-
|
|
229
|
+
// later โ cancel before sendAt:
|
|
230
|
+
await mailQueue.cancelMail(uuid); // true | false
|
|
348
231
|
```
|
|
349
232
|
|
|
350
|
-
|
|
233
|
+
`sendMail` returns a stable `uuid` you can store for cancellation. Pass any [nodemailer message option](https://nodemailer.com/message/) โ `to`, `subject`, `text`, `html`, `attachments`, `cc`, `bcc`, custom headers, etc.
|
|
351
234
|
|
|
352
|
-
|
|
235
|
+
### 5. Client-only mode
|
|
236
|
+
|
|
237
|
+
App servers that only enqueue need no `transports`, no `josk` โ just the queue. Use the **same `prefix`** as the server that drains the class.
|
|
353
238
|
|
|
354
239
|
```js
|
|
355
|
-
// mail-queue.js
|
|
356
240
|
import { MailTime, RedisQueue } from 'mail-time';
|
|
357
241
|
import { createClient } from 'redis';
|
|
358
242
|
|
|
359
|
-
const
|
|
243
|
+
const redisClient = await createClient({ url: process.env.REDIS_URL }).connect();
|
|
244
|
+
|
|
245
|
+
export const mailQueue = new MailTime({
|
|
360
246
|
type: 'client',
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}),
|
|
247
|
+
prefix: 'app',
|
|
248
|
+
queue: new RedisQueue({ client: redisClient }),
|
|
364
249
|
});
|
|
365
|
-
|
|
366
|
-
export { mailQueue };
|
|
367
250
|
```
|
|
368
251
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
Import created `mailQueue` where needed and call `.sendMail()` method. See [NodeMailer's message configuration documentation](https://nodemailer.com/message/) for details
|
|
252
|
+
### 6. Shutdown
|
|
372
253
|
|
|
373
254
|
```js
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
await mailQueue.sendMail({
|
|
377
|
-
to: 'user@gmail.com',
|
|
378
|
-
subject: 'You\'ve got an email!',
|
|
379
|
-
text: 'Plain text message',
|
|
380
|
-
html: '<h1>HTML</h1><p>Styled message</p>'
|
|
255
|
+
process.on('SIGTERM', async () => {
|
|
256
|
+
await mailQueue.destroy({ drain: true }); // stop scheduler, then wait for in-flight SMTPs
|
|
381
257
|
});
|
|
382
258
|
```
|
|
383
259
|
|
|
384
|
-
|
|
260
|
+
Or explicitly: `mailQueue.destroy(); await mailQueue.drain();`
|
|
385
261
|
|
|
386
|
-
|
|
262
|
+
`destroy()` is idempotent and stops the scheduler. Always call it from tests; pair with `drain()` when iterate-driven sends ran.
|
|
387
263
|
|
|
388
|
-
|
|
389
|
-
import { MailTime, MongoQueue } from 'mail-time';
|
|
390
|
-
import { MongoClient } from 'mongodb';
|
|
391
|
-
import { transports } from './transports.js';
|
|
264
|
+
## Storage layouts
|
|
392
265
|
|
|
393
|
-
|
|
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
|
-
```
|
|
266
|
+
Queue storage and scheduler storage can be the same store or different ones. Use this matrix:
|
|
412
267
|
|
|
413
|
-
|
|
268
|
+
| Queue | Scheduler (`josk`) | Best for |
|
|
269
|
+
| -------- | ------------------ | -------------------------------------------- |
|
|
270
|
+
| Postgres | Postgres | Multi-DC, mixed clocks, strict exactly-once. |
|
|
271
|
+
| Redis | Redis | High-throughput single-region. |
|
|
272
|
+
| Mongo | Mongo | Apps already on Mongo (especially Meteor). |
|
|
273
|
+
| Mongo | Redis | Durable letter storage + sub-second polling. |
|
|
274
|
+
| Redis | Mongo | Hot Redis letters + Mongo for scheduler. |
|
|
414
275
|
|
|
415
|
-
|
|
276
|
+
For split-store setups pass a different client to each:
|
|
416
277
|
|
|
417
278
|
```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
279
|
const mailQueue = new MailTime({
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
280
|
+
prefix: 'app',
|
|
281
|
+
queue: new MongoQueue({ db }),
|
|
282
|
+
transports,
|
|
427
283
|
josk: {
|
|
428
|
-
adapter: {
|
|
429
|
-
type: 'redis',
|
|
430
|
-
client: await createClient({ url: 'redis://url' }).connect(),
|
|
431
|
-
}
|
|
284
|
+
adapter: { type: 'redis', client: redisClient },
|
|
432
285
|
},
|
|
433
|
-
|
|
434
|
-
from(transport) {
|
|
435
|
-
return `"Awesome App" <${transport.options.from}>`;
|
|
436
|
-
}
|
|
286
|
+
/* ... */
|
|
437
287
|
});
|
|
438
288
|
```
|
|
439
289
|
|
|
440
|
-
|
|
290
|
+
## Settings presets
|
|
441
291
|
|
|
442
|
-
|
|
292
|
+
Each email class wants a different policy โ OTP must reach the inbox in seconds, a newsletter wants emails folded together, marketing tolerates retries spread over hours. `mailTimePreset(name, overrides)` applies a vetted shape in one line; you layer your own `queue` / `transports` / `josk.adapter` / `prefix` on top.
|
|
443
293
|
|
|
444
294
|
```js
|
|
445
|
-
import { MailTime, RedisQueue } from 'mail-time';
|
|
446
|
-
import { MongoClient } from 'mongodb';
|
|
447
|
-
import { createClient } from 'redis';
|
|
448
|
-
import { transports } from './transports.js';
|
|
295
|
+
import { MailTime, RedisQueue, mailTimePreset } from 'mail-time';
|
|
449
296
|
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
josk: {
|
|
455
|
-
|
|
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
|
-
});
|
|
297
|
+
const mailTime = new MailTime(mailTimePreset('otp', {
|
|
298
|
+
prefix: 'otp',
|
|
299
|
+
queue: new RedisQueue({ client: redisClient }),
|
|
300
|
+
transports: [otpTransport],
|
|
301
|
+
josk: { adapter: { type: 'redis', client: redisClient } },
|
|
302
|
+
}));
|
|
465
303
|
```
|
|
466
304
|
|
|
467
|
-
|
|
305
|
+
| Preset | Shape | Best for |
|
|
306
|
+
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
307
|
+
| `transactional` | `retries: 30`, `retryDelay: 10s`, `concatEmails: false`, `concurrency: 1`, `josk.zombieTime: 120s` | Receipts, password resets, account changes, welcome emails. |
|
|
308
|
+
| `otp` | `retries: 5`, `retryDelay: 2s`, snappy `revolvingInterval: 1024` + jitter `256/1024`, `concurrency: 4`, `sendingTimeout: 60s` | Sign-in codes, 2FA, verification codes โ stale OTPs aren't worth resending forever. |
|
|
309
|
+
| `newsletter` | `concatEmails: true` with a 5-minute fold window, `concatSubject: 'Your updates'`, `retries: 5`, `retryDelay: 60s`, `concurrency: 2`, `sendingTimeout: 10min`, `josk.zombieTime: 5min` | Scheduled digests, weekly summaries, "what's new" emails. |
|
|
310
|
+
| `marketing` | `retries: 10`, `retryDelay: 30s`, `concatEmails: false`, `concurrency: 5`, `josk.zombieTime: 3min` | Promotional / campaign blasts where each letter is unique. |
|
|
311
|
+
| `notifications` | `concatEmails: true` with a 60-second fold window, `concatSubject: 'New activity'`, `retries: 8`, `retryDelay: 30s`, `concurrency: 3`, `josk.zombieTime: 3min` | App / social activity (likes, mentions, follows) where bursts collapse into one letter. |
|
|
312
|
+
| `alerts` | `retries: 20`, `retryDelay: 5s`, snappy `revolvingInterval: 1024` + jitter `256/1024`, `concurrency: 2`, `sendingTimeout: 60s` | Ops / admin alerts: monitoring, error reports, escalations. |
|
|
468
313
|
|
|
469
|
-
|
|
314
|
+
Presets are equally useful on `type: 'client'` instances โ keys that don't apply to the client role are simply ignored.
|
|
470
315
|
|
|
471
|
-
|
|
472
|
-
import { MailTime, RedisQueue } from 'mail-time';
|
|
473
|
-
import { MongoClient } from 'mongodb';
|
|
474
|
-
import { createClient } from 'redis';
|
|
475
|
-
import { transports } from './transports.js';
|
|
316
|
+
## Multiple instances (recommended)
|
|
476
317
|
|
|
477
|
-
|
|
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
|
-
```
|
|
318
|
+
**Run one `MailTime` per email class** when policies differ (OTP vs marketing vs receipts). Each class gets its own `MailTime` options and, when policies differ, its own `prefix`. Combine with [presets](#settings-presets) to keep the boilerplate to a single line per class.
|
|
494
319
|
|
|
495
|
-
|
|
320
|
+
- **Same `prefix`** for every `client` and `server` that share one logical queue.
|
|
321
|
+
- **Different `prefix`** per class so namespaces don't collide.
|
|
322
|
+
- **Never** reuse `prefix` across two instances with different `concatEmails`, `retryDelay`, or other mail policy.
|
|
496
323
|
|
|
497
|
-
|
|
324
|
+
Full example wiring three classes (OTP / transactional / marketing) on one Redis connection, plus app-pod `client` setup, lives in [docs/multi-instance.md](https://github.com/veliovgroup/mail-time/blob/master/docs/multi-instance.md).
|
|
498
325
|
|
|
499
|
-
|
|
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();
|
|
326
|
+
## Dedicated mail host
|
|
504
327
|
|
|
505
|
-
|
|
506
|
-
const mailQueue = new MailTime({
|
|
507
|
-
queue: new RedisQueue({
|
|
508
|
-
client: redisClient,
|
|
509
|
-
}),
|
|
510
|
-
transports,
|
|
511
|
-
strategy: 'backup',
|
|
512
|
-
failsToNext: 1,
|
|
513
|
-
concatEmails: true,
|
|
514
|
-
josk: {
|
|
515
|
-
adapter: {
|
|
516
|
-
type: 'redis',
|
|
517
|
-
client: redisClient
|
|
518
|
-
},
|
|
519
|
-
zombieTime: 120000
|
|
520
|
-
}
|
|
521
|
-
});
|
|
328
|
+
On a **single mail VM** (good rDNS / PTR, fixed SMTP credentials), run **2โ8 `server` processes** (~one per CPU core) โ typically **one process per email class**. Same `prefix` cluster-wide = one JoSk lease tick at a time, so extra pods on the same `prefix` buy **failover/HA**, not throughput.
|
|
522
329
|
|
|
523
|
-
|
|
524
|
-
const mailInstantQueue = new MailTime({
|
|
525
|
-
queue: new RedisQueue({
|
|
526
|
-
client: redisClient,
|
|
527
|
-
prefix: 'instant'
|
|
528
|
-
}),
|
|
529
|
-
transports,
|
|
530
|
-
prefix: 'instant',
|
|
531
|
-
retryDelay: 2000,
|
|
532
|
-
strategy: 'backup',
|
|
533
|
-
failsToNext: 1,
|
|
534
|
-
concatEmails: false,
|
|
535
|
-
josk: {
|
|
536
|
-
adapter: {
|
|
537
|
-
type: 'redis',
|
|
538
|
-
client: redisClient
|
|
539
|
-
},
|
|
540
|
-
zombieTime: 20000
|
|
541
|
-
}
|
|
542
|
-
});
|
|
330
|
+
Full systemd unit + worker layout: [docs/dedicated-mail-host.md](https://github.com/veliovgroup/mail-time/blob/master/docs/dedicated-mail-host.md).
|
|
543
331
|
|
|
544
|
-
|
|
545
|
-
to: 'user@gmail.com',
|
|
546
|
-
subject: 'You\'ve got an email!',
|
|
547
|
-
text: 'Plain text message',
|
|
548
|
-
html: '<h1>HTML</h1><p>Styled message</p>'
|
|
549
|
-
});
|
|
332
|
+
## Tuning
|
|
550
333
|
|
|
551
|
-
|
|
552
|
-
to: 'user@gmail.com',
|
|
553
|
-
subject: 'Sign in request',
|
|
554
|
-
text: 'Your OTP login code: xxxx:',
|
|
555
|
-
html: '<h1>Code:</h1><code>XXXX</code>'
|
|
556
|
-
});
|
|
557
|
-
```
|
|
334
|
+
Defaults fit moderate traffic in a single region. Reach for a [preset](#settings-presets) first; tune individual knobs only when the preset doesn't cover your case. Full guide: [docs/tuning.md](https://github.com/veliovgroup/mail-time/blob/master/docs/tuning.md).
|
|
558
335
|
|
|
559
|
-
###
|
|
336
|
+
### Option reference (when to touch)
|
|
560
337
|
|
|
561
|
-
|
|
338
|
+
| Option | Default | Change when |
|
|
339
|
+
| ---------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
340
|
+
| `mode` | `'batch'` | `'one'` to claim a single row per tick (fairness over throughput across cluster nodes) |
|
|
341
|
+
| `concurrency` | `1` | Raise to send N emails in parallel per instance. Bounded by your SMTP / API rate limits. The CAS on `isSending` keeps it safe. |
|
|
342
|
+
| `sendingTimeout` | `300000` (5 min) | Stale-lock recovery window. Must exceed worst-case SMTP roundtrip; lower it only when you're confident sends never take that long. |
|
|
343
|
+
| `revolvingInterval` | `1536` ms | Lower โ faster pickup; higher โ less scheduler I/O |
|
|
344
|
+
| `josk.minRevolvingDelay` / `maxRevolvingDelay` | `512` / `2048` | Lower both โ snappier polls, more storage load |
|
|
345
|
+
| `josk.zombieTime` | `60000` | **Never below 60s.** Iterate releases the JoSk lease as soon as scanning ends โ only a stalled storage scan can blow this. |
|
|
346
|
+
| `josk.concurrency` | `Infinity` | Set `1` if scheduler ticks overlap while `iterate` still runs |
|
|
347
|
+
| `josk.execute` | `'batch'` | Usually leave default; MailTime only registers one JoSk task per instance |
|
|
348
|
+
| `josk.lockOwnerId` | random | Set in production for observability |
|
|
349
|
+
| `retries` / `retryDelay` | `59` / `60s` | `retries` is *after* the first attempt; default `59` means 60 total attempts. Per email class โ transactional shorter, marketing longer. |
|
|
350
|
+
| `concatEmails` / `concatDelay` | `false` / `60s` | On for notification batching; off for OTP and receipts |
|
|
351
|
+
| `prefix` | `''` | **Same** on all `client` + `server` for one queue; **different** only per email class / shard |
|
|
562
352
|
|
|
563
|
-
|
|
564
|
-
const templates = {
|
|
565
|
-
global: '<html xmlns="http://www.w3.org/1999/xhtml"><head><title>{{subject}}</title></head><body>{{{html}}}<footer>Message sent to @{{username}} user ({{to}})</footer></body></html>',
|
|
566
|
-
signInCode: {
|
|
567
|
-
text: 'Hello @{{username}}! Here\'s your login code: {{code}}',
|
|
568
|
-
html: `<h1>Sign-in request</h1><p>Hello @{{username}}! <p>Copy your login code below:</p> <pre><code>{{code}}</code></pre>`
|
|
569
|
-
}
|
|
570
|
-
};
|
|
353
|
+
For deeper JoSk semantics (lease lifecycle, scheduler adapters, recurring tasks), install the JoSk skill: **`npx skills add veliovgroup/josk`**.
|
|
571
354
|
|
|
572
|
-
|
|
573
|
-
queue: new RedisQueue({ /* ... */ }),
|
|
574
|
-
template: templates.global
|
|
575
|
-
});
|
|
355
|
+
## Templates
|
|
576
356
|
|
|
577
|
-
|
|
578
|
-
to: 'user@gmail.com',
|
|
579
|
-
subject: 'Sign-in request',
|
|
580
|
-
username: 'johndoe',
|
|
581
|
-
code: 'XXXXX-YY',
|
|
582
|
-
text: templates.signInCode.text,
|
|
583
|
-
html: templates.signInCode.html
|
|
584
|
-
});
|
|
585
|
-
```
|
|
357
|
+
Two Mustache-like placeholder forms:
|
|
586
358
|
|
|
587
|
-
|
|
359
|
+
- `{{key}}` โ string interpolation, strips HTML from the value (safe for plain text).
|
|
360
|
+
- `{{{key}}}` โ raw HTML interpolation.
|
|
588
361
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
### `new MailTime(opts)` constructor
|
|
592
|
-
|
|
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
|
|
362
|
+
Every `sendMail` option is available inside `text`, `html`, and the wrapping template:
|
|
623
363
|
|
|
624
364
|
```js
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
365
|
+
const layouts = {
|
|
366
|
+
envelope: `<html><body>{{{html}}}<footer>Sent to @{{username}} ({{to}})</footer></body></html>`,
|
|
367
|
+
otp: {
|
|
368
|
+
text: 'Hello @{{username}}! Your code: {{code}}',
|
|
369
|
+
html: '<h1>Sign-in</h1><p>Hello <b>@{{username}}</b></p><pre><code>{{code}}</code></pre>',
|
|
370
|
+
},
|
|
371
|
+
};
|
|
630
372
|
|
|
631
373
|
const mailQueue = new MailTime({
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
},
|
|
374
|
+
/* ... */
|
|
375
|
+
template: layouts.envelope,
|
|
667
376
|
});
|
|
668
377
|
|
|
669
378
|
await mailQueue.sendMail({
|
|
670
|
-
to: '
|
|
671
|
-
subject: '
|
|
672
|
-
|
|
673
|
-
|
|
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',
|
|
379
|
+
to: 'user@example.com',
|
|
380
|
+
subject: 'Sign-in code',
|
|
381
|
+
username: 'mike',
|
|
382
|
+
code: 'A1B2-C3D4',
|
|
383
|
+
text: layouts.otp.text,
|
|
384
|
+
html: layouts.otp.html,
|
|
692
385
|
});
|
|
693
386
|
```
|
|
694
387
|
|
|
695
|
-
|
|
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*
|
|
388
|
+
`MailTime.Template` is a bundled responsive HTML envelope you can use as the default โ set it on the constructor or per-letter via `opts.template`.
|
|
726
389
|
|
|
727
|
-
|
|
390
|
+
## API
|
|
728
391
|
|
|
729
|
-
|
|
730
|
-
|
|
392
|
+
### `new MailTime(opts)`
|
|
393
|
+
|
|
394
|
+
| Option | Type | Default | Notes |
|
|
395
|
+
| ----------------------------- | --------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
396
|
+
| `queue` | `MongoQueue \| RedisQueue \| PostgresQueue \| CustomQueue` | โ | **Required.** Storage adapter for letters. Custom adapters: see [docs/queue-api.md](https://github.com/veliovgroup/mail-time/blob/master/docs/queue-api.md). |
|
|
397
|
+
| `type` | `'server' \| 'client'` | `'server'` | `'client'` only enqueues โ no `transports` / `josk` required. |
|
|
398
|
+
| `transports` | `nodemailer.Transport[]` | โ | **Required for `server`**. Non-empty. |
|
|
399
|
+
| `josk` | `MailTimeJoSkOptions` | โ | **Required for `server`**. See [JoSk options](#josk-options) below. |
|
|
400
|
+
| `strategy` | `'backup' \| 'balancer'` | `'backup'` | Multi-SMTP rotation policy. |
|
|
401
|
+
| `failsToNext` | `number` | `4` | (`backup`) failures-in-a-row before rotating. |
|
|
402
|
+
| `retries` | `number` | `59` | Re-send attempts after first failure. Total attempts = `retries + 1` (defaults to 60). Legacy alias `maxTries` is honored when `retries` is absent: `new MailTime({ maxTries: N })` sets total attempts to `N`. |
|
|
403
|
+
| `retryDelay` | `number` (ms) | `60000` | Wait between attempts. |
|
|
404
|
+
| `keepHistory` | `boolean` | `false` | Keep sent/failed/cancelled rows. |
|
|
405
|
+
| `concatEmails` | `boolean \| { subject?: string }` | `false` | Fold same-`to` letters into one. Pass `{ subject: 'X' }` to set the folded-letter subject inline; the string supports the `{{count}}` placeholder and overrides `concatSubject`. |
|
|
406
|
+
| `concatSubject` | `string` | `'Multiple notifications'` | Subject when folded. Supports `{{count}}` for the folded letter count. |
|
|
407
|
+
| `concatDelimiter` | `string` | `'<hr>'` | Separator between folded bodies. |
|
|
408
|
+
| `concatDelay` | `number` (ms) | `60000` | Fold window. |
|
|
409
|
+
| `revolvingInterval` | `number` (ms) | `1536` | Queue iteration interval. |
|
|
410
|
+
| `mode` | `'one' \| 'batch'` | `'batch'` | `'batch'` claims every due row per tick; `'one'` claims a single row per tick. |
|
|
411
|
+
| `concurrency` | `number` | `1` | Parallel SMTPs per instance. The CAS on `isSending` prevents duplicate delivery. |
|
|
412
|
+
| `sendingTimeout` | `number` (ms) | `300000` | Window after which a stuck `isSending=true` row becomes eligible again. Must exceed worst-case SMTP roundtrip. |
|
|
413
|
+
| `verifyTransports` | `boolean` | `true` | Probe each transport via `transport.verify()` once at `ready()`. Failing transports are marked unusable, surfaced through `onError(error, null, { transportIndex, phase: 'verify' })`, and skipped during rotation/fallback. Throws from `ready()` if **every** transport fails. Transports without a `verify()` method are treated as healthy. |
|
|
414
|
+
| `template` | `string` | `'{{{html}}}'` | Default envelope. |
|
|
415
|
+
| `prefix` | `string` | `''` | Queue namespace. **Same** on every `client` and `server` for one logical queue; **different** per email class. Inherited by the queue adapter; JoSk scheduler uses `mailTimeQueue<prefix>`. |
|
|
416
|
+
| `from` | `string \| (transport) => string` | โ | Strongly recommended for spam-passing `From:` formatting. |
|
|
417
|
+
| `debug` | `boolean` | `false` | Verbose logs. |
|
|
418
|
+
| `onSent(email, info)` | `function` | โ | Called once the task is fully delivered. `email.mailOptions[i].accepted` lists every address that got through (across all attempts). |
|
|
419
|
+
| `onError(error, email, info)` | `function` | โ | Called once the retry budget is exhausted with at least one un-accepted recipient. `email.mailOptions[i].rejected` lists each un-delivered address with its last error. Also fires once per transport that fails `verify()` at startup with `email === null` and `info = { transportIndex, phase: 'verify' }`. |
|
|
420
|
+
|
|
421
|
+
### JoSk options
|
|
422
|
+
|
|
423
|
+
`opts.josk` is passed to the underlying [`JoSk`](https://github.com/veliovgroup/josk) constructor. Useful keys:
|
|
424
|
+
|
|
425
|
+
| Key | Default | Notes |
|
|
426
|
+
| ------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
427
|
+
| `adapter` | โ | Either a constructed adapter or a config object: `{ type: 'redis' \| 'mongo' \| 'postgres', client \| db, prefix?, resetOnInit?, useHashTags? }`. MailTime constructs the adapter from the config object. Set `useHashTags: true` on Redis/KeyDB Cluster. |
|
|
428
|
+
| `minRevolvingDelay` | `512` | Lower bound of poll window. |
|
|
429
|
+
| `maxRevolvingDelay` | `2048` | Upper bound. |
|
|
430
|
+
| `zombieTime` | `60000` | Re-claim if `queue.iterate()` runs longer than this. **Do not drop below 60s.** |
|
|
431
|
+
| `execute` | `'batch'` | JoSk scheduler batching; low impact for MailTime (one interval task per instance). |
|
|
432
|
+
| `concurrency` | `Infinity` | Cap overlapping JoSk handler runs on **this** process (`1` if ticks pile up). |
|
|
433
|
+
| `autoClear` | `false` | Remove orphan tasks from storage. |
|
|
434
|
+
| `lockOwnerId` | `josk-<uuid>` | Stable owner id; recommended per worker. |
|
|
435
|
+
| `onError(title, details)` | (logs to console) | Wire to your logger. |
|
|
436
|
+
| `onExecuted(uid, details)` | โ | Optional hook after each successful JoSk tick (observability). |
|
|
437
|
+
|
|
438
|
+
For deeper JoSk semantics, install the JoSk skill: **`npx skills add veliovgroup/josk`** (the same author).
|
|
439
|
+
|
|
440
|
+
### Methods
|
|
441
|
+
|
|
442
|
+
- `sendMail(opts)` โ `Promise<string>` uuid. Throws on missing `to` or on missing both `text` and `html`. Pass any nodemailer message option plus `sendAt` (Date or ms timestamp), `template`, `concatSubject`.
|
|
443
|
+
- `send(opts)` โ alias of `sendMail`.
|
|
444
|
+
- `cancelMail(uuidOrPromise)` โ `Promise<boolean>`. Accepts the `uuid` or the `Promise<string>` from `sendMail`.
|
|
445
|
+
- `cancel(uuid)` โ alias of `cancelMail`.
|
|
446
|
+
- `ping()` โ `Promise<{status, code, statusCode, error?}>`. Pings scheduler then queue.
|
|
447
|
+
- `ready()` โ `Promise<MailTime>`. Awaits all startup work; rejects with `.cause` on storage failure.
|
|
448
|
+
- `destroy(opts?)` โ `boolean` or `Promise<boolean>` when `{ drain: true }`. Stops scheduler. Idempotent. Use `destroy({ drain: true })` or `await drain()` after `destroy()` for graceful shutdown.
|
|
449
|
+
- `drain()` โ `Promise<void>`. Resolves once every in-flight SMTP attempt finishes. Useful in tests and graceful-shutdown paths.
|
|
450
|
+
|
|
451
|
+
### Queue constructors
|
|
452
|
+
|
|
453
|
+
| Constructor | Required option | Optional |
|
|
454
|
+
| ---------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
455
|
+
| `new RedisQueue({ client, prefix? })` | connected `redis@^4/^5` client with `watch()` + `multi()` | `prefix` โ inherited from `MailTime` when omitted. Redis Cluster prefixes must map to one hash slot. |
|
|
456
|
+
| `new MongoQueue({ db, prefix? })` | `db` from `MongoClient#db()` | `prefix` โ inherited from `MailTime` when omitted. Indexes auto-created on first `ready()`. |
|
|
457
|
+
| `new PostgresQueue({ client, prefix? })` | `pg.Pool` (recommended) or `pg.Client` | `prefix` โ inherited from `MailTime` when omitted. `mail_time_queue` table auto-migrated on first `ready()`. |
|
|
458
|
+
|
|
459
|
+
For custom adapters see [docs/queue-api.md](https://github.com/veliovgroup/mail-time/blob/master/docs/queue-api.md).
|
|
460
|
+
|
|
461
|
+
### Module functions
|
|
462
|
+
|
|
463
|
+
- `mailTimePreset(name, overrides?)` โ fresh MailTime constructor config. Deep-clones the named [preset](#settings-presets) and deep-merges your overrides (scalars win, nested `josk` composes). Throws on unknown `name` or non-object `overrides`.
|
|
464
|
+
- `presets` โ read-only `{ [name]: partialConfig }` map backing `mailTimePreset`.
|
|
465
|
+
- `presetNames` โ read-only array of preset names.
|
|
466
|
+
|
|
467
|
+
### Static
|
|
468
|
+
|
|
469
|
+
- `MailTime.Template` โ get/set the default HTML envelope template.
|
|
470
|
+
|
|
471
|
+
## Migration from 3.x
|
|
472
|
+
|
|
473
|
+
Full v4 highlights, adapter changes, and type exports live in [docs/migration-v3-v4.md](https://github.com/veliovgroup/mail-time/blob/master/docs/migration-v3-v4.md). Quick checklist:
|
|
474
|
+
|
|
475
|
+
1. **Node โฅ 20.9.0**, Bun โฅ 1.1.0. Bump your runtime first.
|
|
476
|
+
2. **Swap adapter imports** to the new `MongoQueue` / `RedisQueue` / `PostgresQueue` constructors.
|
|
477
|
+
3. **Pass `josk`** โ it's now required for `type: 'server'`.
|
|
478
|
+
4. **`josk.zombieTime` default raised to `60000` ms** (was `32786`). Set it explicitly if you relied on the old value.
|
|
479
|
+
5. **Custom queue adapters** โ `update`'s claim guard now triggers on `{ isSending: true, sendingAt, tries }` (was `{ isSent: true, tries }`) and must include the stale-lock-recovery clause `(isSending === false OR sendingAt <= now - sendingTimeout)`. The iterate path must call `await mailTimeInstance.___dispatch(row)` instead of `___send` and honor `opts.limit` / `opts.sendingTimeout`. See [docs/queue-api.md](https://github.com/veliovgroup/mail-time/blob/master/docs/queue-api.md) and [`adapters/blank-example.js`](https://github.com/veliovgroup/mail-time/blob/master/adapters/blank-example.js).
|
|
480
|
+
6. **Default behavior unchanged** โ `concurrency: 1` keeps the post-upgrade send rate identical to v3. Opt into parallel sends by raising `concurrency`.
|
|
481
|
+
|
|
482
|
+
New v4 surface to opt into: [`mailTimePreset`](#settings-presets), `concurrency`, `mode`, `sendingTimeout`, `drain()`, per-recipient retries, and the [AI agent skills](#ai-agent-skills) bundle.
|
|
731
483
|
|
|
732
|
-
|
|
733
|
-
to: 'johndoe@example.com',
|
|
734
|
-
subject: 'Email subject',
|
|
735
|
-
text: 'You have got email!',
|
|
736
|
-
html: '<p>You have got email!</p>',
|
|
737
|
-
});
|
|
484
|
+
## Testing
|
|
738
485
|
|
|
739
|
-
|
|
486
|
+
```sh
|
|
487
|
+
npm install
|
|
488
|
+
# DEFAULT RUN โ needs Redis + Mongo + Postgres up locally
|
|
489
|
+
REDIS_URL="redis://127.0.0.1:6379" \
|
|
490
|
+
MONGO_URL="mongodb://127.0.0.1:27017/mail-time-test" \
|
|
491
|
+
PG_URL="postgres://127.0.0.1:5432/postgres" \
|
|
492
|
+
npm test
|
|
493
|
+
|
|
494
|
+
# Single suite
|
|
495
|
+
npm run test:redis
|
|
496
|
+
npm run test:mongo
|
|
497
|
+
npm run test:postgres
|
|
498
|
+
|
|
499
|
+
# Bun-native test runner (only Jest-shaped tests)
|
|
500
|
+
bun test ./test/jest
|
|
740
501
|
```
|
|
741
502
|
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
Simple and bulletproof HTML template, see [its source](https://github.com/veliovgroup/mail-time/blob/master/template.html). Usage example:
|
|
745
|
-
|
|
746
|
-
```js
|
|
747
|
-
import { MailTime, MongoQueue, RedisQueue } from 'mail-time';
|
|
748
|
-
|
|
749
|
-
// Make it default
|
|
750
|
-
const mailQueue = new MailTime({
|
|
751
|
-
/* .. */
|
|
752
|
-
template: MailTime.Template
|
|
753
|
-
});
|
|
754
|
-
|
|
755
|
-
// For single letter
|
|
756
|
-
mailQueue.sendMail({
|
|
757
|
-
/* .. */
|
|
758
|
-
template: MailTime.Template
|
|
759
|
-
});
|
|
760
|
-
```
|
|
503
|
+
`npm test` runs Jest unit tests, then Mocha integration tests, then TypeScript declaration tests. Jest coverage threshold is **85%** across statements, branches, functions, and lines. GitHub Actions runs the matrix against `redis@^4` and `redis@^5`.
|
|
761
504
|
|
|
762
|
-
|
|
505
|
+
## Bun
|
|
763
506
|
|
|
764
|
-
|
|
507
|
+
MailTime ships pure ESM with a generated CJS bundle. Both runtimes (Bun โฅ 1.1.0, Node โฅ 20.9.0) load it directly:
|
|
765
508
|
|
|
766
509
|
```js
|
|
767
|
-
|
|
768
|
-
to: 'user@gmail.com',
|
|
769
|
-
userName: 'Mike',
|
|
770
|
-
subject: 'Sign up confirmation',
|
|
771
|
-
text: 'Hello {{userName}}, \r\n Thank you for registration \r\n Your login: {{to}}',
|
|
772
|
-
html: '<div style="text-align: center"><h1>Hello {{userName}}</h1><p><ul><li>Thank you for registration</li><li>Your login: {{to}}</li></ul></p></div>',
|
|
773
|
-
template: '<body>{{{html}}}</body>'
|
|
774
|
-
});
|
|
510
|
+
import { MailTime } from 'mail-time'; // works in both
|
|
775
511
|
```
|
|
776
512
|
|
|
777
|
-
|
|
513
|
+
Mixed clusters (some Node, some Bun) share one schedule under the same `prefix` โ the lease lives in storage, runtime-agnostic.
|
|
778
514
|
|
|
779
|
-
|
|
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:
|
|
515
|
+
## License
|
|
783
516
|
|
|
784
|
-
|
|
785
|
-
# Before running tests make sure NODE_ENV === development
|
|
786
|
-
# Install NPM dependencies
|
|
787
|
-
npm install --save-dev
|
|
788
|
-
|
|
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
|
|
797
|
-
|
|
798
|
-
# Be patient, tests are taking around 8 mins
|
|
799
|
-
```
|
|
517
|
+
[BSD-3-Clause](LICENSE).
|
|
800
518
|
|
|
801
|
-
## Support this project
|
|
519
|
+
## Support this project
|
|
802
520
|
|
|
803
|
-
-
|
|
804
|
-
-
|
|
805
|
-
-
|
|
806
|
-
-
|
|
807
|
-
-
|
|
808
|
-
- [
|
|
809
|
-
- [Sponsor veliovgroup via GitHub](https://github.com/sponsors/veliovgroup) โ support company behind this package
|
|
810
|
-
- [Support via PayPal](https://paypal.me/veliovgroup) โ support our open source contributions
|
|
521
|
+
- Star on [GitHub](https://github.com/veliovgroup/mail-time) and [NPM](https://www.npmjs.com/package/mail-time).
|
|
522
|
+
- [Sponsor maintainer on GitHub](https://github.com/sponsors/dr-dimitru).
|
|
523
|
+
- [Sponsor veliovgroup on GitHub](https://github.com/sponsors/veliovgroup).
|
|
524
|
+
- [PayPal](https://paypal.me/veliovgroup).
|
|
525
|
+
- Try [โ๏ธ meteor-files.com](https://meteor-files.com/?ref=github-mail-time-repo-footer).
|
|
526
|
+
- Try [โฒ ostr.io](https://ostr.io?ref=github-mail-time-repo-footer) for server monitoring, web analytics, web-CRON, and SEO pre-rendering.
|