mail-time 1.4.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 +367 -301
- package/adapters/mongo.d.ts +118 -0
- package/adapters/mongo.js +431 -0
- package/adapters/postgres.d.ts +100 -0
- package/adapters/postgres.js +520 -0
- package/adapters/redis.d.ts +105 -0
- package/adapters/redis.js +565 -0
- package/helpers.js +217 -0
- package/index.cjs +2862 -462
- package/index.d.cts +300 -0
- package/index.d.ts +300 -0
- package/index.js +1002 -475
- package/package.json +66 -16
- package/presets.d.ts +87 -0
- package/presets.js +193 -0
package/README.md
CHANGED
|
@@ -1,65 +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
|
-
|
|
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)
|
|
9
9
|
|
|
10
|
-
|
|
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
11
|
|
|
12
|
-
|
|
12
|
+
`MailTime` runs in one of two modes:
|
|
13
13
|
|
|
14
|
-
|
|
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.
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
- [With single SMTP](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#single-point-of-failure)
|
|
18
|
-
- [With multiple SMTP](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#multiple-smtp-providers)
|
|
19
|
-
- [For Clusters](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#sending-emails-from-cluster-of-servers)
|
|
20
|
-
- [Features](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#features)
|
|
21
|
-
- [Installation](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#installation)
|
|
22
|
-
- [Meteor.js usage](https://github.com/veliovgroup/mail-time/blob/master/docs/meteor.md)
|
|
23
|
-
- [Usage example](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#basic-usage)
|
|
24
|
-
- [API](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#api)
|
|
25
|
-
- [*Constructor*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#new-mailtimeopts-constructor)
|
|
26
|
-
- [`.send()`](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#sendmailopts--callback)
|
|
27
|
-
- [Default Template](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#static-mailtimetemplate)
|
|
28
|
-
- [Custom Templates](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#template-example)
|
|
29
|
-
- [~92% tests coverage](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#testing)
|
|
17
|
+
Many clients + one or more servers coexist behind the same `prefix` in the same store.
|
|
30
18
|
|
|
31
|
-
##
|
|
32
|
-
|
|
33
|
-
- 👨🔬 ~92% tests coverage;
|
|
34
|
-
- 📦 Two simple dependencies, written from scratch for top performance;
|
|
35
|
-
- 🏢 Synchronize email queue across multiple (horizontally scaled) servers;
|
|
36
|
-
- 💪 Bulletproof design, built-in retries.
|
|
19
|
+
## Features
|
|
37
20
|
|
|
38
|
-
|
|
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.
|
|
39
32
|
|
|
40
|
-
|
|
33
|
+
## How it works
|
|
41
34
|
|
|
42
35
|
### Single point of failure
|
|
43
36
|
|
|
44
|
-
Issue - mitigate a single point of failure:
|
|
45
|
-
|
|
46
37
|
```ascii
|
|
47
38
|
|----------------| |------| |------------------|
|
|
48
39
|
| Other mailer | ------> | SMTP | ------> | ^_^ Happy user |
|
|
49
40
|
|----------------| |------| |------------------|
|
|
50
41
|
|
|
51
|
-
The scheme above
|
|
52
|
-
or connection between your server and SMPT is up. Once network
|
|
53
|
-
failure occurs or SMTP service is down - users won't be happy
|
|
42
|
+
The scheme above works only as long as SMTP is up.
|
|
54
43
|
|
|
55
44
|
|----------------| \ / |------| |------------------|
|
|
56
45
|
| Other mailer | --X---> | SMTP | ------> | 0_o Disappointed |
|
|
57
46
|
|----------------| / \ |------| |------------------|
|
|
58
47
|
^- email lost in vain
|
|
59
48
|
|
|
60
|
-
|
|
61
|
-
As long as MailTime has not received confirmation what email is sent
|
|
62
|
-
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.
|
|
63
50
|
|
|
64
51
|
|----------------| / |------| |------------------|
|
|
65
52
|
| Mail Time | --X---> | SMTP | ------> | ^_^ Happy user |
|
|
@@ -71,7 +58,7 @@ it will keep the letter in the queue and retry to send it again
|
|
|
71
58
|
|
|
72
59
|
### Multiple SMTP providers
|
|
73
60
|
|
|
74
|
-
|
|
61
|
+
`backup` falls over on failure; `balancer` round-robins:
|
|
75
62
|
|
|
76
63
|
```ascii
|
|
77
64
|
|--------|
|
|
@@ -87,23 +74,19 @@ Rotate or backup email transports by using multiple SMTP providers
|
|
|
87
74
|
|--------|
|
|
88
75
|
```
|
|
89
76
|
|
|
90
|
-
### Sending emails from cluster
|
|
91
|
-
|
|
92
|
-
It is common to have horizontally scaled "Cluster" of servers for load-balancing and for durability.
|
|
93
|
-
|
|
94
|
-
Most modern application has scheduled or recurring emails. For example, once a day — with recent news and updates. It won't be an issue with a single server setup — the server would send emails at a daily interval via timer or CRON. But in "Cluster" implementation — each server will attempt to send the same email. MailTime built to avoid sending the same email multiple times to a user from horizontally scaled applications.
|
|
77
|
+
### Sending emails from a cluster
|
|
95
78
|
|
|
96
|
-
|
|
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.
|
|
97
80
|
|
|
98
81
|
```ascii
|
|
99
82
|
|===================THE=CLUSTER===================| |=QUEUE=|
|
|
100
83
|
| |----------| |----------| |----------| | | | |--------|
|
|
101
|
-
| |
|
|
84
|
+
| | MailTime | | MailTime | | MailTime | | | |-->| SMTP 1 |------\
|
|
102
85
|
| | Server 1 | | Server 2 | | Server 3 | | | | |--------| \
|
|
103
86
|
| |-----\----| |----\-----| |----\-----| | | | |-------------|
|
|
104
87
|
| \---------------\----------------\----------> | |--------| | ^_^ |
|
|
105
88
|
| | | |-->| SMTP 2 |-->| Happy users |
|
|
106
|
-
| Each "App Server"
|
|
89
|
+
| Each "App Server" | | | |--------| |-------------|
|
|
107
90
|
| runs MailTime as a "Server" | | | /
|
|
108
91
|
| for the maximum durability | | | |--------| /
|
|
109
92
|
| | | |-->| SMTP 3 |-----/
|
|
@@ -111,17 +94,17 @@ For the maximum durability and agility each Application Server can run MailTime
|
|
|
111
94
|
|=================================================| |=======|
|
|
112
95
|
```
|
|
113
96
|
|
|
114
|
-
|
|
97
|
+
For a dedicated mail machine (rDNS / PTR records), use `type: 'client'` on app servers and a single `type: 'server'` micro-service:
|
|
115
98
|
|
|
116
99
|
```ascii
|
|
117
100
|
|===================THE=CLUSTER===================| |=QUEUE=| |===Mail=Time===|
|
|
118
101
|
| |----------| |----------| |----------| | | | | | |--------|
|
|
119
|
-
| |
|
|
120
|
-
| |
|
|
102
|
+
| | MailTime | | MailTime | | MailTime | | | | | Micro-service |-->| SMTP 1 |------\
|
|
103
|
+
| | Client 1 | | Client 2 | | Client 3 | | | | | running | |--------| \
|
|
121
104
|
| |-----\----| |----\-----| |----\-----| | | | | MailTime as | |-------------|
|
|
122
105
|
| \---------------\----------------\----------> | | "Server" only | |--------| | ^_^ |
|
|
123
106
|
| | | | | sending |-->| SMTP 2 |-->| Happy users |
|
|
124
|
-
| Each "App
|
|
107
|
+
| Each "App" runs MailTime as | | | | emails | |--------| |-------------|
|
|
125
108
|
| a "Client" only placing emails to the queue. | | <-------- | /
|
|
126
109
|
| | | --------> | |--------| /
|
|
127
110
|
| | | | | |-->| SMTP 3 |-----/
|
|
@@ -129,332 +112,415 @@ To split roles MailTime can run on a dedicated machine as micro-service. This ca
|
|
|
129
112
|
|=================================================| |=======| |===============|
|
|
130
113
|
```
|
|
131
114
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
- Queue - Managed via MongoDB, will survive server reboots and failures
|
|
135
|
-
- Support for multiple server setups - "Cluster", Phusion Passenger instances, Load Balanced solutions, etc.
|
|
136
|
-
- Emails concatenation by addressee email - Reduce amount of sent emails to a single user with concatenation, and avoid mistakenly doubled emails
|
|
137
|
-
- 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
|
|
138
|
-
- 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
|
|
139
|
-
- Sending retries for network and other failures
|
|
140
|
-
- Templating support with [Mustache](https://mustache.github.io/)-like placeholders
|
|
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.
|
|
141
116
|
|
|
142
117
|
## Installation
|
|
143
118
|
|
|
144
|
-
|
|
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
|
|
145
125
|
|
|
146
|
-
|
|
147
|
-
|
|
126
|
+
# Bun:
|
|
127
|
+
bun add mail-time nodemailer
|
|
148
128
|
```
|
|
149
129
|
|
|
150
|
-
|
|
130
|
+
> [!NOTE]
|
|
131
|
+
> `nodemailer` and adapter drivers are peers (not bundled) so you can pin your own versions.
|
|
132
|
+
|
|
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).
|
|
135
|
+
|
|
136
|
+
For Meteor.js usage see [docs/meteor.md](https://github.com/veliovgroup/mail-time/blob/master/docs/meteor.md).
|
|
137
|
+
|
|
138
|
+
## AI agent skills
|
|
151
139
|
|
|
152
|
-
|
|
153
|
-
# for node@>=14.20.0
|
|
154
|
-
npm install --save mail-time
|
|
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.
|
|
155
141
|
|
|
156
|
-
|
|
157
|
-
|
|
142
|
+
```sh
|
|
143
|
+
# Install the MailTime skill into the current project:
|
|
144
|
+
npx skills add veliovgroup/mail-time
|
|
158
145
|
|
|
159
|
-
#
|
|
160
|
-
|
|
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
|
|
161
149
|
```
|
|
162
150
|
|
|
163
|
-
|
|
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.
|
|
164
152
|
|
|
165
|
-
|
|
153
|
+
## Quick start
|
|
154
|
+
|
|
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.
|
|
156
|
+
|
|
157
|
+
### 1. Import
|
|
166
158
|
|
|
167
159
|
```js
|
|
168
|
-
//
|
|
169
|
-
import MailTime from 'mail-time';
|
|
160
|
+
// ESM
|
|
161
|
+
import { MailTime, MongoQueue, PostgresQueue, RedisQueue, mailTimePreset } from 'mail-time';
|
|
170
162
|
|
|
171
|
-
//
|
|
172
|
-
const MailTime = require('mail-time');
|
|
163
|
+
// CommonJS
|
|
164
|
+
const { MailTime, MongoQueue, PostgresQueue, RedisQueue, mailTimePreset } = require('mail-time');
|
|
173
165
|
```
|
|
174
166
|
|
|
175
|
-
Create nodemailer
|
|
167
|
+
### 2. Create nodemailer transports
|
|
168
|
+
|
|
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.
|
|
176
170
|
|
|
177
171
|
```js
|
|
172
|
+
// transports.js
|
|
178
173
|
import nodemailer from 'nodemailer';
|
|
179
|
-
// Use DIRECT transport
|
|
180
|
-
// and enable sending email from localhost
|
|
181
|
-
// install "nodemailer-direct-transport" NPM package:
|
|
182
|
-
import directTransport from 'nodemailer-direct-transport';
|
|
183
|
-
|
|
184
|
-
const transports = [];
|
|
185
|
-
const directTransportOpts = {
|
|
186
|
-
pool: false,
|
|
187
|
-
direct: true,
|
|
188
|
-
name: 'mail.example.com',
|
|
189
|
-
from: 'no-reply@example.com',
|
|
190
|
-
};
|
|
191
|
-
transports.push(nodemailer.createTransport(directTransport(directTransportOpts)));
|
|
192
|
-
// IMPORTANT: Add `.options` to a newly created transport,
|
|
193
|
-
// this is necessary to make sure options are available to MailTime package:
|
|
194
|
-
transports[0].options = directTransportOpts;
|
|
195
|
-
|
|
196
|
-
// Private SMTP
|
|
197
|
-
transports.push(nodemailer.createTransport({
|
|
198
|
-
host: 'smtp.example.com',
|
|
199
|
-
from: 'no-reply@example.com',
|
|
200
|
-
auth: {
|
|
201
|
-
user: 'no-reply',
|
|
202
|
-
pass: 'xxx'
|
|
203
|
-
},
|
|
204
|
-
}));
|
|
205
174
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}));
|
|
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
|
+
];
|
|
182
|
+
```
|
|
215
183
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
184
|
+
### 3. Initialize MailTime with a preset
|
|
185
|
+
|
|
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.
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
// mail-queue.js — transactional emails on Redis
|
|
190
|
+
import { MailTime, RedisQueue, mailTimePreset } from 'mail-time';
|
|
191
|
+
import { createClient } from 'redis';
|
|
192
|
+
import { transports } from './transports.js';
|
|
193
|
+
|
|
194
|
+
const redisClient = await createClient({ url: process.env.REDIS_URL }).connect();
|
|
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 } },
|
|
201
|
+
transports,
|
|
202
|
+
from: (t) => `"Awesome App" <${t.options.from}>`,
|
|
203
|
+
onSent(email, info) {
|
|
204
|
+
console.log('sent', email.uuid, info);
|
|
205
|
+
},
|
|
206
|
+
onError(error, email, info) {
|
|
207
|
+
console.error('failed', email.uuid, error, info);
|
|
224
208
|
},
|
|
225
209
|
}));
|
|
210
|
+
|
|
211
|
+
await mailQueue.ready();
|
|
212
|
+
export { mailQueue };
|
|
226
213
|
```
|
|
227
214
|
|
|
228
|
-
|
|
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 }`.
|
|
216
|
+
|
|
217
|
+
### 4. Send and cancel
|
|
229
218
|
|
|
230
219
|
```js
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
MongoClient.connect(process.env.MONGO_URL, (error, client) => {
|
|
239
|
-
const db = client.db(dbName);
|
|
240
|
-
|
|
241
|
-
const mailQueue = new MailTime({
|
|
242
|
-
db, // MongoDB
|
|
243
|
-
type: 'server',
|
|
244
|
-
strategy: 'balancer', // Transports will be used in round robin chain
|
|
245
|
-
transports,
|
|
246
|
-
from(transport) {
|
|
247
|
-
// To pass spam-filters `from` field should be correctly set
|
|
248
|
-
// for each transport, check `transport` object for more options
|
|
249
|
-
return `"Awesome App" <${transport.options.from}>`;
|
|
250
|
-
},
|
|
251
|
-
concatEmails: true, // Concatenate emails to the same addressee
|
|
252
|
-
concatDelimiter: '<h1>{{{subject}}}</h1>', // Start each concatenated email with it's own subject
|
|
253
|
-
template: MailTime.Template // Use default template
|
|
254
|
-
});
|
|
220
|
+
import { mailQueue } from './mail-queue.js';
|
|
221
|
+
|
|
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>',
|
|
255
227
|
});
|
|
228
|
+
|
|
229
|
+
// later — cancel before sendAt:
|
|
230
|
+
await mailQueue.cancelMail(uuid); // true | false
|
|
256
231
|
```
|
|
257
232
|
|
|
258
|
-
|
|
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.
|
|
259
234
|
|
|
260
|
-
|
|
261
|
-
const MailTime = require('mail-time');
|
|
262
|
-
const MongoClient = require('mongodb').MongoClient;
|
|
235
|
+
### 5. Client-only mode
|
|
263
236
|
|
|
264
|
-
|
|
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.
|
|
265
238
|
|
|
266
|
-
|
|
267
|
-
|
|
239
|
+
```js
|
|
240
|
+
import { MailTime, RedisQueue } from 'mail-time';
|
|
241
|
+
import { createClient } from 'redis';
|
|
242
|
+
|
|
243
|
+
const redisClient = await createClient({ url: process.env.REDIS_URL }).connect();
|
|
268
244
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
concatEmails: true // Concatenate emails to the same address
|
|
274
|
-
});
|
|
245
|
+
export const mailQueue = new MailTime({
|
|
246
|
+
type: 'client',
|
|
247
|
+
prefix: 'app',
|
|
248
|
+
queue: new RedisQueue({ client: redisClient }),
|
|
275
249
|
});
|
|
276
250
|
```
|
|
277
251
|
|
|
278
|
-
|
|
252
|
+
### 6. Shutdown
|
|
279
253
|
|
|
280
254
|
```js
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
subject: 'You\'ve got an email!',
|
|
284
|
-
text: 'Plain text message',
|
|
285
|
-
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
|
|
286
257
|
});
|
|
287
258
|
```
|
|
288
259
|
|
|
289
|
-
|
|
260
|
+
Or explicitly: `mailQueue.destroy(); await mailQueue.drain();`
|
|
261
|
+
|
|
262
|
+
`destroy()` is idempotent and stops the scheduler. Always call it from tests; pair with `drain()` when iterate-driven sends ran.
|
|
263
|
+
|
|
264
|
+
## Storage layouts
|
|
290
265
|
|
|
291
|
-
|
|
266
|
+
Queue storage and scheduler storage can be the same store or different ones. Use this matrix:
|
|
267
|
+
|
|
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. |
|
|
275
|
+
|
|
276
|
+
For split-store setups pass a different client to each:
|
|
292
277
|
|
|
293
278
|
```js
|
|
294
|
-
// CREATE mailQueue FOR NON-URGENT EMAILS WHICH IS OKAY TO CONCATENATE
|
|
295
279
|
const mailQueue = new MailTime({
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
280
|
+
prefix: 'app',
|
|
281
|
+
queue: new MongoQueue({ db }),
|
|
282
|
+
transports,
|
|
283
|
+
josk: {
|
|
284
|
+
adapter: { type: 'redis', client: redisClient },
|
|
285
|
+
},
|
|
286
|
+
/* ... */
|
|
303
287
|
});
|
|
288
|
+
```
|
|
304
289
|
|
|
305
|
-
|
|
306
|
-
const mailInstantQueue = new MailTime({
|
|
307
|
-
db: db,
|
|
308
|
-
prefix: 'instant',
|
|
309
|
-
interval: 2,
|
|
310
|
-
strategy: 'backup',
|
|
311
|
-
failsToNext: 1,
|
|
312
|
-
concatEmails: false,
|
|
313
|
-
zombieTime: 20000
|
|
314
|
-
});
|
|
290
|
+
## Settings presets
|
|
315
291
|
|
|
316
|
-
|
|
317
|
-
to: 'user@gmail.com',
|
|
318
|
-
subject: 'You\'ve got an email!',
|
|
319
|
-
text: 'Plain text message',
|
|
320
|
-
html: '<h1>HTML</h1><p>Styled message</p>'
|
|
321
|
-
});
|
|
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.
|
|
322
293
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
})
|
|
294
|
+
```js
|
|
295
|
+
import { MailTime, RedisQueue, mailTimePreset } from 'mail-time';
|
|
296
|
+
|
|
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
|
+
}));
|
|
329
303
|
```
|
|
330
304
|
|
|
331
|
-
|
|
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. |
|
|
332
313
|
|
|
333
|
-
|
|
314
|
+
Presets are equally useful on `type: 'client'` instances — keys that don't apply to the client role are simply ignored.
|
|
334
315
|
|
|
335
|
-
|
|
336
|
-
const templates = {
|
|
337
|
-
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>',
|
|
338
|
-
signInCode: {
|
|
339
|
-
text: 'Hello @{{username}}! Here\'s your login code: {{code}}',
|
|
340
|
-
html: `<h1>Sign-in request</h1><p>Hello @{{username}}! <p>Copy your login code below:</p> <pre><code>{{code}}</code></pre>`
|
|
341
|
-
}
|
|
342
|
-
};
|
|
316
|
+
## Multiple instances (recommended)
|
|
343
317
|
|
|
344
|
-
|
|
345
|
-
db: db,
|
|
346
|
-
template: templates.global
|
|
347
|
-
});
|
|
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.
|
|
348
319
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
username: 'johndoe',
|
|
353
|
-
code: 'XXXXX-YY',
|
|
354
|
-
text: templates.signInCode.text,
|
|
355
|
-
html: templates.signInCode.html
|
|
356
|
-
});
|
|
357
|
-
```
|
|
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.
|
|
358
323
|
|
|
359
|
-
|
|
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).
|
|
325
|
+
|
|
326
|
+
## Dedicated mail host
|
|
327
|
+
|
|
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.
|
|
329
|
+
|
|
330
|
+
Full systemd unit + worker layout: [docs/dedicated-mail-host.md](https://github.com/veliovgroup/mail-time/blob/master/docs/dedicated-mail-host.md).
|
|
331
|
+
|
|
332
|
+
## Tuning
|
|
333
|
+
|
|
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).
|
|
360
335
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
- `
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
- Alias - `send()`
|
|
389
|
-
- `opts` {*Object*} - Configuration object
|
|
390
|
-
- `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
|
|
391
|
-
- `opts.template` - Email specific template, this will override default template passed to `MailTime` constructor
|
|
392
|
-
- `opts.concatSubject` - Email specific concatenation subject, this will override default concatenation subject passed to `MailTime` constructor
|
|
393
|
-
- `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
|
|
394
|
-
- `callback` {*Function*} - Callback called after the email was sent or failed to be sent. __Do not use on multi-server setup__
|
|
395
|
-
|
|
396
|
-
### `static MailTime.Template`
|
|
397
|
-
|
|
398
|
-
Simple and bulletproof HTML template, see [its source](https://github.com/veliovgroup/mail-time/blob/master/template.html). Usage example:
|
|
336
|
+
### Option reference (when to touch)
|
|
337
|
+
|
|
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 |
|
|
352
|
+
|
|
353
|
+
For deeper JoSk semantics (lease lifecycle, scheduler adapters, recurring tasks), install the JoSk skill: **`npx skills add veliovgroup/josk`**.
|
|
354
|
+
|
|
355
|
+
## Templates
|
|
356
|
+
|
|
357
|
+
Two Mustache-like placeholder forms:
|
|
358
|
+
|
|
359
|
+
- `{{key}}` — string interpolation, strips HTML from the value (safe for plain text).
|
|
360
|
+
- `{{{key}}}` — raw HTML interpolation.
|
|
361
|
+
|
|
362
|
+
Every `sendMail` option is available inside `text`, `html`, and the wrapping template:
|
|
399
363
|
|
|
400
364
|
```js
|
|
401
|
-
const
|
|
402
|
-
|
|
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
|
+
};
|
|
372
|
+
|
|
403
373
|
const mailQueue = new MailTime({
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
template: MailTime.Template
|
|
374
|
+
/* ... */
|
|
375
|
+
template: layouts.envelope,
|
|
407
376
|
});
|
|
408
377
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
378
|
+
await mailQueue.sendMail({
|
|
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,
|
|
414
385
|
});
|
|
415
386
|
```
|
|
416
387
|
|
|
417
|
-
|
|
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`.
|
|
418
389
|
|
|
419
|
-
|
|
390
|
+
## API
|
|
420
391
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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.
|
|
431
483
|
|
|
432
484
|
## Testing
|
|
433
485
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
501
|
+
```
|
|
437
502
|
|
|
438
|
-
|
|
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`.
|
|
439
504
|
|
|
440
|
-
|
|
441
|
-
# Before run tests make sure NODE_ENV === development
|
|
442
|
-
# Install NPM dependencies
|
|
443
|
-
npm install --save-dev
|
|
505
|
+
## Bun
|
|
444
506
|
|
|
445
|
-
|
|
446
|
-
DEBUG="true" EMAIL_DOMAIN="example.com" MONGO_URL="mongodb://127.0.0.1:27017/npm-mail-time-test-001" npm test
|
|
507
|
+
MailTime ships pure ESM with a generated CJS bundle. Both runtimes (Bun ≥ 1.1.0, Node ≥ 20.9.0) load it directly:
|
|
447
508
|
|
|
448
|
-
|
|
509
|
+
```js
|
|
510
|
+
import { MailTime } from 'mail-time'; // works in both
|
|
449
511
|
```
|
|
450
512
|
|
|
451
|
-
|
|
513
|
+
Mixed clusters (some Node, some Bun) share one schedule under the same `prefix` — the lease lives in storage, runtime-agnostic.
|
|
514
|
+
|
|
515
|
+
## License
|
|
516
|
+
|
|
517
|
+
[BSD-3-Clause](LICENSE).
|
|
518
|
+
|
|
519
|
+
## Support this project
|
|
452
520
|
|
|
453
|
-
-
|
|
454
|
-
-
|
|
455
|
-
-
|
|
456
|
-
-
|
|
457
|
-
-
|
|
458
|
-
- [
|
|
459
|
-
- [Sponsor veliovgroup via GitHub](https://github.com/sponsors/veliovgroup) — support company behind this package
|
|
460
|
-
- [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.
|