mail-time 3.0.0 โ†’ 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,81 +1,60 @@
1
- [![support](https://img.shields.io/badge/support-GitHub-white)](https://github.com/sponsors/dr-dimitru)
2
- [![support](https://img.shields.io/badge/support-PayPal-white)](https://paypal.me/veliovgroup)
3
- <a href="https://ostr.io/info/built-by-developers-for-developers?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
- "Mail-Time" is NPM package for mail queue management. Build on top of the [`nodemailer`](https://github.com/nodemailer/nodemailer) package. Mail-Time made for single-server and horizontally scaled multi-server setups in mind.
9
-
10
- Every `MailTime` instance can have `type` configured as *Server* or *Client*. *Server* type of `MailTime` is great for creating an emailing micro-service app.
11
-
12
- The main difference between *Server* and *Client* `type` is that the *Server* handles the queue and __sends__ email. While the *Client* only __adds__ emails into the queue.
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
+ [![npm version][badge-npm-v]][npm-url]
5
+ [![npm downloads][badge-npm-dm]][npm-url]
6
+ [![CI][badge-ci]][ci-url]
7
+ [![bundle size][badge-size]][size-url]
8
+ [![Coverage][badge-cov]](#testing)
9
+ [![License: BSD-3-Clause][badge-license]][license-url]
10
+ [![Node.js][badge-node]][node-url]
11
+ [![TypeScript][badge-ts]][ts-url]
12
+ [![Bun][badge-bun]][bun-url]
13
+ [![Meteor][badge-meteor]][meteor-url]
14
+ [![dependencies][badge-deps]][deps-url]
15
+ [![Sponsor][badge-sponsor]][sponsor-url]
16
+ [![Donate][badge-donate]][donate-url]
57
17
 
58
- ### Single point of failure
18
+ 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.
19
+
20
+ `MailTime` runs in one of two modes:
21
+
22
+ - **`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.
23
+ - **`client`** โ€” only enqueues emails. Use for app servers in a "dedicated mail micro-service" topology.
24
+
25
+ Many clients + one or more servers coexist behind the same `prefix` in the same store.
26
+
27
+ ## Features
59
28
 
60
- Issue - mitigate a single point of failure via persistent queue and re-send attempts
29
+ - ๐Ÿข **Horizontally scaled** โ€” synchronize one queue across N processes/hosts/DCs.
30
+ - ๐Ÿ” **Multi-SMTP rotation** โ€” `backup` (failover) and `balancer` (round-robin) strategies.
31
+ - ๐Ÿ’ช **Built-in retries** โ€” storage-backed retry with per-letter transport pinning.
32
+ - ๐ŸŽฏ **Per-recipient retries** โ€” when a multi-`to` send is partially rejected, only the un-accepted addresses are retried; delivered ones never see a duplicate.
33
+ - ๐Ÿ“ฎ **Email concatenation** โ€” fold same-`to` emails arriving inside a window into one letter.
34
+ - ๐ŸŽ›๏ธ **One-line setup** โ€” built-in [presets](#settings-presets) for `transactional`, `otp`, `newsletter`, `marketing`, `notifications`, `alerts`.
35
+ - ๐Ÿ›ข๏ธ **Three first-party storages** โ€” MongoDB, Redis, PostgreSQL. Plus a [custom-adapter contract](https://github.com/veliovgroup/mail-time/blob/master/docs/queue-api.md).
36
+ - ๐Ÿ“ฆ **Bun โ‰ฅ 1.1.0 & Node โ‰ฅ 20.9.0** โ€” same code, both runtimes.
37
+ - ๐Ÿค– **Ships with AI agent skills** โ€” see [AI agent skills](#ai-agent-skills) below.
38
+ - ๐Ÿ“ **Hand-tuned ESM + CJS + TypeScript declarations**.
39
+ - ๐Ÿงช **99%+ Jest line coverage** (85% threshold enforced) + Mocha integration tests for every adapter.
40
+
41
+ ## How it works
42
+
43
+ ### Single point of failure
61
44
 
62
45
  ```ascii
63
46
  |----------------| |------| |------------------|
64
47
  | Other mailer | ------> | SMTP | ------> | ^_^ Happy user |
65
48
  |----------------| |------| |------------------|
66
49
 
67
- The scheme above will work as long as SMTP service is available
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
50
+ The scheme above works only as long as SMTP is up.
70
51
 
71
52
  |----------------| \ / |------| |------------------|
72
53
  | Other mailer | --X---> | SMTP | ------> | 0_o Disappointed |
73
54
  |----------------| / \ |------| |------------------|
74
55
  ^- email lost in vain
75
56
 
76
- Single SMTP solution may work in case of network or other failures
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
57
+ MailTime keeps every letter in the queue until SMTP confirms delivery.
79
58
 
80
59
  |----------------| / |------| |------------------|
81
60
  | Mail Time | --X---> | SMTP | ------> | ^_^ Happy user |
@@ -87,7 +66,7 @@ it will keep the letter in the queue and retry to send it again
87
66
 
88
67
  ### Multiple SMTP providers
89
68
 
90
- Rotate email transports by using multiple SMTP providers. *MailTime* support two strategies `backup` (*rotate when failed*) and `balancer` (*round-robin rotation*)
69
+ `backup` falls over on failure; `balancer` round-robins:
91
70
 
92
71
  ```ascii
93
72
  |--------|
@@ -103,18 +82,14 @@ Rotate email transports by using multiple SMTP providers. *MailTime* support two
103
82
  |--------|
104
83
  ```
105
84
 
106
- ### Sending emails from cluster of servers
85
+ ### Sending emails from a cluster
107
86
 
108
- It is common to have horizontally scaled "Cluster" of servers for load-balancing and for durability.
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:
87
+ 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
88
 
114
89
  ```ascii
115
90
  |===================THE=CLUSTER===================| |=QUEUE=|
116
91
  | |----------| |----------| |----------| | | | |--------|
117
- | | App | | App | | App | | | |-->| SMTP 1 |------\
92
+ | | MailTime | | MailTime | | MailTime | | | |-->| SMTP 1 |------\
118
93
  | | Server 1 | | Server 2 | | Server 3 | | | | |--------| \
119
94
  | |-----\----| |----\-----| |----\-----| | | | |-------------|
120
95
  | \---------------\----------------\----------> | |--------| | ^_^ |
@@ -127,17 +102,17 @@ For the maximum durability and agility each Application Server can run *MailTime
127
102
  |=================================================| |=======|
128
103
  ```
129
104
 
130
- To split roles *MailTime* can run on a dedicated machine as micro-service. This case is great for private email servers with implemented authentication via rDNS and PTR records:
105
+ For a dedicated mail machine (rDNS / PTR records), use `type: 'client'` on app servers and a single `type: 'server'` micro-service:
131
106
 
132
107
  ```ascii
133
108
  |===================THE=CLUSTER===================| |=QUEUE=| |===Mail=Time===|
134
109
  | |----------| |----------| |----------| | | | | | |--------|
135
- | | App | | App | | App | | | | | Micro-service |-->| SMTP 1 |------\
136
- | | Server 1 | | Server 2 | | Server 3 | | | | | running | |--------| \
110
+ | | MailTime | | MailTime | | MailTime | | | | | Micro-service |-->| SMTP 1 |------\
111
+ | | Client 1 | | Client 2 | | Client 3 | | | | | running | |--------| \
137
112
  | |-----\----| |----\-----| |----\-----| | | | | MailTime as | |-------------|
138
113
  | \---------------\----------------\----------> | | "Server" only | |--------| | ^_^ |
139
114
  | | | | | sending |-->| SMTP 2 |-->| Happy users |
140
- | Each "App Server" runs MailTime as | | | | emails | |--------| |-------------|
115
+ | Each "App" runs MailTime as | | | | emails | |--------| |-------------|
141
116
  | a "Client" only placing emails to the queue. | | <-------- | /
142
117
  | | | --------> | |--------| /
143
118
  | | | | | |-->| SMTP 3 |-----/
@@ -145,666 +120,465 @@ To split roles *MailTime* can run on a dedicated machine as micro-service. This
145
120
  |=================================================| |=======| |===============|
146
121
  ```
147
122
 
148
- ## Features
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)
123
+ 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
124
 
157
125
  ## Installation
158
126
 
159
- To implement Server functionality โ€” begin with installing `nodemailer`, although this package meant to be used with `nodemailer`, it's not added as the dependency, as `nodemailer` not needed by Client, and to give freedom to choose `nodemailer`'s version to fit every project needs:
127
+ ```sh
128
+ npm install --save mail-time nodemailer
129
+ # pick at least one storage driver:
130
+ npm install --save redis # for RedisQueue
131
+ npm install --save mongodb # for MongoQueue
132
+ npm install --save pg # for PostgresQueue
160
133
 
161
- ```shell
162
- npm install --save nodemailer
134
+ # Bun:
135
+ bun add mail-time nodemailer
163
136
  ```
164
137
 
165
- Install *MailTime* package:
138
+ > [!NOTE]
139
+ > `nodemailer` and adapter drivers are peers (not bundled) so you can pin your own versions.
166
140
 
167
- ```shell
168
- # for node@>=14.20.0
169
- npm install --save mail-time
141
+ > [!IMPORTANT]
142
+ > Upgrading? See the [Migrations](#migrations) table below for links to the detailed guides.
170
143
 
171
- # for node@<14.20.0
172
- npm install --save mail-time@=1.3.4
144
+ For Meteor.js usage see [docs/meteor.md](https://github.com/veliovgroup/mail-time/blob/master/docs/meteor.md).
173
145
 
174
- # for node@<8.9.0
175
- npm install --save mail-time@=0.1.7
176
- ```
146
+ ## AI agent skills
147
+
148
+ 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
149
 
178
- ## Basic usage
150
+ ```sh
151
+ # Install the MailTime skill globally:
152
+ npx skills add veliovgroup/mail-time -g
153
+
154
+ # Or install the MailTime skill into the current project:
155
+ npx skills add veliovgroup/mail-time
156
+
157
+ # Recommended: also install the JoSk skill โ€” MailTime is built on JoSk,
158
+ # and deep scheduler questions resolve through JoSk's contract.
159
+ npx skills add veliovgroup/josk -g
160
+ ```
179
161
 
180
- Setup Nodemailer's transports, Queue storage, and *MailTime* instance
162
+ 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.
181
163
 
182
- ### Steps to get started
164
+ > [!NOTE]
165
+ > The skill source is **not** shipped in the npm tarball โ€” it's distributed via GitHub and consumed only by AI tooling.
183
166
 
184
- See steps 1-4 below to learn about different parts of *MailTime* library and how it can get used. From configuration options to sending email
167
+ ## Quick start
185
168
 
186
- 1. [Require `mail-time` package](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#1-require-package)
187
- 2. [Create NodeMailer's transports](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#2-create-nodemailers-transports)
188
- 3. [Initiate `mail-time` *server*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3-initiate-mail-time)
189
- - a. [Connect to Redis](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3a-initiate-and-connect-to-redis); Or
190
- - b. [Connect to MongoDB](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3b-initiate-and-connect-to-mongodb); And
191
- - c. [*optionally*] [initiate `mail-time` as *client*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3c-optionally-create-client-type-of-mailtime)
192
- 4. [Start sending emails](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#4-send-email)
169
+ 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
170
 
194
- #### 1. Require package
171
+ ### 1. Import
195
172
 
196
173
  ```js
197
- // import as ES Module
198
- import { MailTime, MongoQueue, RedisQueue } from 'mail-time';
174
+ // ESM
175
+ import { MailTime, MongoQueue, PostgresQueue, RedisQueue, mailTimePreset } from 'mail-time';
199
176
 
200
- // require as CommonJS
201
- const { MailTime, MongoQueue, RedisQueue } = require('mail-time');
177
+ // CommonJS
178
+ const { MailTime, MongoQueue, PostgresQueue, RedisQueue, mailTimePreset } = require('mail-time');
202
179
  ```
203
180
 
204
- #### 2. Create NodeMailer's transports
181
+ ### 2. Create nodemailer transports
205
182
 
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.
207
-
208
- For details and full list of options available in `.createTransport()` see [`nodemailer` docs](https://nodemailer.com/smtp/)
183
+ 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
184
 
210
185
  ```js
211
186
  // transports.js
212
187
  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
-
250
- // Mailing service (SparkPost as example)
251
- transports.push(nodemailer.createTransport({
252
- host: 'smtp.sparkpostmail.com',
253
- port: 587,
254
- from: 'no-reply@mail2.example.com',
255
- auth: {
256
- user: 'SMTP_Injection',
257
- pass: 'xxx'
258
- },
259
- }));
260
188
 
261
- export { transports };
189
+ export const transports = [
190
+ nodemailer.createTransport({
191
+ host: 'smtp.example.com',
192
+ from: 'no-reply@example.com',
193
+ auth: { user: 'no-reply', pass: process.env.SMTP_PASS },
194
+ }),
195
+ ];
262
196
  ```
263
197
 
264
- #### 3. Initiate `mail-time`
198
+ ### 3. Initialize MailTime with a preset
265
199
 
266
- Create new instance of *MailTime* in the *Server* mode, โ€” it will be able to __send__ and __add__ emails to the queue.
267
-
268
- #### 3a. Initiate and connect to Redis
269
-
270
- Connecting to Redis before initiating `new MailTime` instance:
200
+ 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
201
 
272
202
  ```js
273
- // mail-queue.js
274
- import { MailTime, RedisQueue } from 'mail-time';
203
+ // mail-queue.js โ€” transactional emails on Redis
204
+ import { MailTime, RedisQueue, mailTimePreset } from 'mail-time';
275
205
  import { createClient } from 'redis';
276
206
  import { transports } from './transports.js';
277
207
 
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
208
  const redisClient = await createClient({ url: process.env.REDIS_URL }).connect();
281
- const mailQueue = new MailTime({
282
- transports,
283
- queue: new MongoQueue({
284
- client: redisClient,
285
- }),
286
- josk: {
287
- adapter: {
288
- type: 'redis',
289
- client: redisClient,
290
- }
291
- },
292
- template: MailTime.Template // Use default template
293
- from(transport) {
294
- // To pass spam-filters `from` field should be correctly set
295
- // for each transport, check `transport` object for more options
296
- return `"Awesome App" <${transport.options.from}>`;
297
- },
298
- onError(error, email, details) {
299
- console.log(`Email "${email.mailOptions.subject}" wasn't sent to ${email.mailOptions.to}`, error, details);
300
- },
301
- onSent(email, details) {
302
- console.log(`Email "${email.mailOptions.subject}" successfully sent to ${email.mailOptions.to}`, details);
303
- },
304
- });
305
-
306
- export { mailQueue };
307
- ```
308
-
309
- #### 3b. Initiate and connect to MongoDB
310
-
311
- Connecting to MongoDB before initiating `new MailTime` instance:
312
209
 
313
- ```js
314
- // mail-queue.js
315
- import { MailTime, MongoQueue } from 'mail-time';
316
- import { MongoClient } from 'mongodb';
317
- import { transports } from './transports.js';
318
-
319
- // Use MONGO_URL environment variable to store connection string to MongoDB
320
- // example: "MONGO_URL=mongodb://127.0.0.1:27017/myapp node mail-micro-service.js"
321
- const mongodb = (await MongoClient.connect(process.env.MONGO_URL)).db('database');
322
- const mailQueue = new MailTime({
210
+ const mailQueue = new MailTime(mailTimePreset('transactional', {
211
+ type: 'server',
212
+ prefix: 'app',
213
+ queue: new RedisQueue({ client: redisClient }),
214
+ josk: { adapter: { type: 'redis', client: redisClient } },
323
215
  transports,
324
- queue: new MongoQueue({
325
- db: mongodb,
326
- }),
327
- josk: {
328
- adapter: {
329
- type: 'mongo',
330
- db: mongodb,
331
- }
332
- },
333
- template: MailTime.Template // Use default template
334
- from(transport) {
335
- // To pass spam-filters `from` field should be correctly set
336
- // for each transport, check `transport` object for more options
337
- return `"Awesome App" <${transport.options.from}>`;
338
- },
339
- onError(error, email, details) {
340
- console.log(`Email "${email.mailOptions.subject}" wasn't sent to ${email.mailOptions.to}`, error, details);
216
+ from: (t) => `"Awesome App" <${t.options.from}>`,
217
+ onSent(email, info) {
218
+ console.log('sent', email.uuid, info);
341
219
  },
342
- onSent(email, details) {
343
- console.log(`Email "${email.mailOptions.subject}" successfully sent to ${email.mailOptions.to}`, details);
220
+ onError(error, email, info) {
221
+ console.error('failed', email.uuid, error, info);
344
222
  },
345
- });
346
-
347
- export { mailQueue };
348
- ```
349
-
350
- #### 3c. Optionally create *Client* type of *MailTime*
351
-
352
- Only __one__ `MailTime` *Server* instance required to send email. In the other parts of an app (like UI units or in sub-apps) use `mail-time` in the *Client* mode to __add__ emails to queue
353
-
354
- ```js
355
- // mail-queue.js
356
- import { MailTime, RedisQueue } from 'mail-time';
357
- import { createClient } from 'redis';
358
-
359
- const mailQueue = new MailTime({
360
- type: 'client',
361
- queue: new RedisQueue({
362
- client: await createClient({ url: 'redis://url' }).connect()
363
- }),
364
- });
223
+ }));
365
224
 
225
+ await mailQueue.ready();
366
226
  export { mailQueue };
367
227
  ```
368
228
 
369
- #### 4. Send email
229
+ Switching stores is one import change. The same pattern works with `MongoQueue({ db })` + `{ type: 'mongo', db }`, or `PostgresQueue({ client: pgPool })` + `{ type: 'postgres', client: pgPool }`.
370
230
 
371
- Import created `mailQueue` where needed and call `.sendMail()` method. See [NodeMailer's message configuration documentation](https://nodemailer.com/message/) for details
231
+ ### 4. Send and cancel
372
232
 
373
233
  ```js
374
234
  import { mailQueue } from './mail-queue.js';
375
235
 
376
- await mailQueue.sendMail({
377
- to: 'user@gmail.com',
236
+ const uuid = await mailQueue.sendMail({
237
+ to: 'user@example.com',
378
238
  subject: 'You\'ve got an email!',
379
- text: 'Plain text message',
380
- html: '<h1>HTML</h1><p>Styled message</p>'
239
+ text: 'Plain text body',
240
+ html: '<h1>HTML</h1><p>Styled body</p>',
381
241
  });
382
- ```
383
-
384
- ### Using MongoDB for queue and scheduler
385
242
 
386
- *MailTime* uses separate storage for Queue management and Scheduler. In the example below MongoDB is used for both
387
-
388
- ```js
389
- import { MailTime, MongoQueue } from 'mail-time';
390
- import { MongoClient } from 'mongodb';
391
- import { transports } from './transports.js';
392
-
393
- const db = (await MongoClient.connect('mongodb://url')).db('database');
394
- const mailQueue = new MailTime({
395
- queue: new MongoQueue({
396
- db: db,
397
- }),
398
- josk: {
399
- adapter: {
400
- type: 'mongo',
401
- db: db,
402
- }
403
- },
404
- transports,
405
- from(transport) {
406
- // To pass spam-filters `from` field should be correctly set
407
- // for each transport, check `transport` object for more options
408
- return `"Awesome App" <${transport.options.from}>`;
409
- }
410
- });
243
+ // later โ€” cancel before sendAt:
244
+ await mailQueue.cancelMail(uuid); // true | false
411
245
  ```
412
246
 
413
- ### Using MongoDB for queue and Redis for scheduler
414
-
415
- *MailTime* uses separate storage for Queue management and Scheduler. In the example below MongoDB is used for queue and Redis is used for scheduler
416
-
417
- ```js
418
- import { MailTime, MongoQueue } from 'mail-time';
419
- import { MongoClient } from 'mongodb';
420
- import { createClient } from 'redis';
421
- import { transports } from './transports.js';
422
-
423
- const mailQueue = new MailTime({
424
- queue: new MongoQueue({
425
- db: (await MongoClient.connect('mongodb://url')).db('database'),
426
- }),
427
- josk: {
428
- adapter: {
429
- type: 'redis',
430
- client: await createClient({ url: 'redis://url' }).connect(),
431
- }
432
- },
433
- transports,
434
- from(transport) {
435
- return `"Awesome App" <${transport.options.from}>`;
436
- }
437
- });
438
- ```
247
+ `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.
439
248
 
440
- ### Using Redis for queue and MongoDB for scheduler
249
+ ### 5. Client-only mode
441
250
 
442
- *MailTime* uses separate storage for Queue management and Scheduler. In the example below Redis is used for queue and MongoDB is used for scheduler
251
+ App servers that only enqueue need no `transports`, no `josk` โ€” just the queue. Use the **same `prefix`** as the server that drains the class.
443
252
 
444
253
  ```js
445
254
  import { MailTime, RedisQueue } from 'mail-time';
446
- import { MongoClient } from 'mongodb';
447
255
  import { createClient } from 'redis';
448
- import { transports } from './transports.js';
449
256
 
450
- const mailQueue = new MailTime({
451
- queue: new RedisQueue({
452
- client: await createClient({ url: 'redis://url' }).connect(),
453
- }),
454
- josk: {
455
- adapter: {
456
- type: 'mongo',
457
- db: (await MongoClient.connect('mongodb://url')).db('database'),
458
- }
459
- },
460
- transports,
461
- from(transport) {
462
- return `"Awesome App" <${transport.options.from}>`;
463
- }
257
+ const redisClient = await createClient({ url: process.env.REDIS_URL }).connect();
258
+
259
+ export const mailQueue = new MailTime({
260
+ type: 'client',
261
+ prefix: 'app',
262
+ queue: new RedisQueue({ client: redisClient }),
464
263
  });
465
264
  ```
466
265
 
467
- ### Using Redis for queue and scheduler
468
-
469
- *MailTime* uses separate storage for Queue management and Scheduler. In the example below Redis is used for both
266
+ ### 6. Shutdown
470
267
 
471
268
  ```js
472
- import { MailTime, RedisQueue } from 'mail-time';
473
- import { MongoClient } from 'mongodb';
474
- import { createClient } from 'redis';
475
- import { transports } from './transports.js';
476
-
477
- const redisClient = await createClient({ url: 'redis://url' }).connect();
478
- const mailQueue = new MailTime({
479
- queue: new RedisQueue({
480
- client: redisClient,
481
- }),
482
- josk: {
483
- adapter: {
484
- type: 'redis',
485
- client: redisClient,
486
- }
487
- },
488
- transports,
489
- from(transport) {
490
- return `"Awesome App" <${transport.options.from}>`;
491
- }
269
+ process.on('SIGTERM', async () => {
270
+ await mailQueue.destroy({ drain: true }); // stop scheduler, then wait for in-flight SMTPs
492
271
  });
493
272
  ```
494
273
 
495
- ### Two `MailTime` instances usage example
274
+ Or explicitly: `mailQueue.destroy(); await mailQueue.drain();`
496
275
 
497
- Create two `MailTime` instances with different settings. One for urgent (*e.g. "transactional" emails*), and another one for other types of emails (*e.g. "marketing" emails*)
276
+ `destroy()` is idempotent and stops the scheduler. Always call it from tests; pair with `drain()` when iterate-driven sends ran.
498
277
 
499
- ```js
500
- import { MailTime, RedisQueue } from 'mail-time';
501
- import { createClient } from 'redis';
502
- import { transports } from './transports.js';
503
- const redisClient = await createClient({ url: 'redis://url' }).connect();
278
+ #### Pause / resume a server (backpressure)
504
279
 
505
- // CREATE mailQueue FOR NON-URGENT EMAILS WHICH IS OKAY TO CONCATENATE
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
- });
280
+ `pause()` stops this **server** instance from competing for the queue-drain lease without tearing it down (unlike `destroy()`). In-flight SMTP sends finish; other `server` instances keep draining. `resume()` resumes and triggers an immediate scan.
522
281
 
523
- // CREATE mailInstantQueue FOR TRANSACTIONAL EMAILS AND ALERTS
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
- });
282
+ ```js
283
+ mailQueue.pause(); // stop draining on this pod
284
+ // ...SMTP provider rate-limit clears / maintenance window ends...
285
+ mailQueue.resume(); // resume; an immediate scan kicks off
543
286
 
544
- await mailQueue.sendMail({
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
- });
287
+ mailQueue.isPaused; // boolean
288
+ (await mailQueue.ping()).paused // boolean โ€” observable in health checks
550
289
 
551
- await mailInstantQueue.sendMail({
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
- });
290
+ // Stop draining AND wait for in-flight sends to settle:
291
+ mailQueue.pause();
292
+ await mailQueue.drain();
557
293
  ```
558
294
 
559
- ### Passing variables to the template
560
-
561
- All options passed to the `.sendMail()` method are available inside `text`, `html`, and global templates
562
-
563
- ```js
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
- };
295
+ Both return `boolean` and are no-ops (returning `false`) on `client` instances or after `destroy()`. Use for SMTP rate-limit backpressure, rolling deploys, or quota windows.
571
296
 
572
- const mailQueue = new MailTime({
573
- queue: new RedisQueue({ /* ... */ }),
574
- template: templates.global
575
- });
297
+ ## Storage layouts
576
298
 
577
- await mailQueue.sendMail({
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
- ```
299
+ Queue storage and scheduler storage can be the same store or different ones. Use this matrix:
586
300
 
587
- ## API
301
+ | Queue | Scheduler (`josk`) | Best for |
302
+ | -------- | ------------------ | -------------------------------------------- |
303
+ | Postgres | Postgres | Multi-DC, mixed clocks, strict exactly-once. |
304
+ | Redis | Redis | High-throughput single-region. |
305
+ | Mongo | Mongo | Apps already on Mongo (especially Meteor). |
306
+ | Mongo | Redis | Durable letter storage + sub-second polling. |
307
+ | Redis | Mongo | Hot Redis letters + Mongo for scheduler. |
588
308
 
589
- All available constructor options and `.sendMail()` method API overview
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
309
+ For split-store setups pass a different client to each:
623
310
 
624
311
  ```js
625
- import { MailTime, MongoQueue, RedisQueue } from 'mail-time';
626
- import nodemailer from 'nodemailer';
627
- import { createClient } from 'redis';
628
-
629
- const redisClient = await createClient({ url: 'redis://url' }).connect();
630
-
631
312
  const mailQueue = new MailTime({
632
- type: 'server',
633
- strategy: 'backup',
634
- prefix: 'appMailQueue',
635
- transports: [nodemailer.createTransport({/* ... */})],
636
- failsToNext: 4,
637
- retries: 60,
638
- retryDelay: 60000,
639
- keepHistory: false,
640
- concatEmails: false,
641
- concatDelay: 60000,
642
- concatDelimiter: '<hr>',
643
- concatSubject: 'Multiple notifications',
644
- revolvingInterval: 256,
645
- template: '{{{html}}}',
646
- queue: new RedisQueue({
647
- client: redisClient,
648
- prefix: 'appMailQueue',
649
- }),
313
+ prefix: 'app',
314
+ queue: new MongoQueue({ db }),
315
+ transports,
650
316
  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);
317
+ adapter: { type: 'redis', client: redisClient },
666
318
  },
667
- });
668
-
669
- await mailQueue.sendMail({
670
- to: 'johndoe@example.com',
671
- subject: 'Email subject',
672
- text: 'You have got email!',
673
- html: '<p>You have got email!</p>',
319
+ /* ... */
674
320
  });
675
321
  ```
676
322
 
677
- ### `new RedisQueue(opts)` constructor
678
-
679
- *Create Redis Queue instance.* Use for `opts.queue` when creating *MailTime* instance
323
+ ## Settings presets
680
324
 
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
325
+ 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.
684
326
 
685
327
  ```js
686
- import { MailTime, RedisQueue } from 'mail-time';
687
- import { createClient } from 'redis';
328
+ import { MailTime, RedisQueue, mailTimePreset } from 'mail-time';
688
329
 
689
- new RedisQueue({
690
- client: await createClient({ url: 'redis://url' }).connect(),
691
- prefix: 'appMailQueue',
692
- });
330
+ const mailTime = new MailTime(mailTimePreset('otp', {
331
+ prefix: 'otp',
332
+ queue: new RedisQueue({ client: redisClient }),
333
+ transports: [otpTransport],
334
+ josk: { adapter: { type: 'redis', client: redisClient } },
335
+ }));
693
336
  ```
694
337
 
695
- ### `new MongoQueue(opts)` constructor
338
+ | Preset | Shape | Best for |
339
+ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
340
+ | `transactional` | `retries: 30`, `retryDelay: 10s`, `concatEmails: false`, `concurrency: 1`, `josk.zombieTime: 120s` | Receipts, password resets, account changes, welcome emails. |
341
+ | `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. |
342
+ | `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. |
343
+ | `marketing` | `retries: 10`, `retryDelay: 30s`, `concatEmails: false`, `concurrency: 5`, `josk.zombieTime: 3min` | Promotional / campaign blasts where each letter is unique. |
344
+ | `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. |
345
+ | `alerts` | `retries: 20`, `retryDelay: 5s`, snappy `revolvingInterval: 1024` + jitter `256/1024`, `concurrency: 2`, `sendingTimeout: 60s` | Ops / admin alerts: monitoring, error reports, escalations. |
696
346
 
697
- *Create MongoDB Queue instance.* Use for `opts.queue` when creating *MailTime* instance
347
+ Presets are equally useful on `type: 'client'` instances โ€” keys that don't apply to the client role are simply ignored.
698
348
 
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
349
+ ## Multiple instances (recommended)
702
350
 
703
- ```js
704
- import { MailTime, MongoQueue } from 'mail-time';
705
- import { MongoClient } from 'mongodb';
351
+ **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.
706
352
 
707
- new MongoQueue({
708
- db: (await MongoClient.connect('mongodb://url')).db('database'),
709
- prefix: 'appMailQueue',
710
- });
711
- ```
353
+ - **Same `prefix`** for every `client` and `server` that share one logical queue.
354
+ - **Different `prefix`** per class so namespaces don't collide.
355
+ - **Never** reuse `prefix` across two instances with different `concatEmails`, `retryDelay`, or other mail policy.
712
356
 
713
- ### `sendMail(opts)`
357
+ 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).
714
358
 
715
- *Add email to the queue.* Returns `Promise<string>` unique email's `uuid`
359
+ ## Dedicated mail host
716
360
 
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
361
+ 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.
722
362
 
723
- ### `cancelMail(uuid)`
363
+ Full systemd unit + worker layout: [docs/dedicated-mail-host.md](https://github.com/veliovgroup/mail-time/blob/master/docs/dedicated-mail-host.md).
724
364
 
725
- *Remove email from queue.* Returns `Promise<boolean>` โ€” `true` if cancelled or `false` if not found, was sent, or was cancelled previously. Throws *Error*
365
+ ## Tuning
726
366
 
727
- - `uuid` {*string*|*promise*} โ€” email's `uuid` returned from `.sendEmail()` method
367
+ 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).
728
368
 
729
- ```js
730
- import { mailQueue } from './mail-queue.js';
369
+ ### Option reference (when to touch)
731
370
 
732
- const uuid = await mailQueue.sendMail({
733
- to: 'johndoe@example.com',
734
- subject: 'Email subject',
735
- text: 'You have got email!',
736
- html: '<p>You have got email!</p>',
737
- });
371
+ | Option | Default | Change when |
372
+ | ---------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
373
+ | `mode` | `'batch'` | `'one'` to claim a single row per tick (fairness over throughput across cluster nodes) |
374
+ | `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. |
375
+ | `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. |
376
+ | `revolvingInterval` | `1536` ms | Lower โ†’ faster pickup; higher โ†’ less scheduler I/O |
377
+ | `josk.minRevolvingDelay` / `maxRevolvingDelay` | `512` / `2048` | Lower both โ†’ snappier polls, more storage load |
378
+ | `josk.zombieTime` | `60000` | **Never below 60s.** Iterate releases the JoSk lease as soon as scanning ends โ€” only a stalled storage scan can blow this. |
379
+ | `josk.concurrency` | `Infinity` | Set `1` if scheduler ticks overlap while `iterate` still runs |
380
+ | `josk.execute` | `'batch'` | Usually leave default; MailTime only registers one JoSk task per instance |
381
+ | `josk.lockOwnerId` | random | Set in production for observability |
382
+ | `retries` / `retryDelay` | `59` / `60s` | `retries` is *after* the first attempt; default `59` means 60 total attempts. Per email class โ€” transactional shorter, marketing longer. |
383
+ | `concatEmails` / `concatDelay` | `false` / `60s` | On for notification batching; off for OTP and receipts |
384
+ | `prefix` | `''` | **Same** on all `client` + `server` for one queue; **different** only per email class / shard |
738
385
 
739
- await mailQueue.cancelMail(uuid);
740
- ```
386
+ For deeper JoSk semantics (lease lifecycle, scheduler adapters, recurring tasks), install the JoSk skill: **`npx skills add veliovgroup/josk`**.
387
+
388
+ ## Templates
389
+
390
+ Two Mustache-like placeholder forms:
741
391
 
742
- ### `static MailTime.Template`
392
+ - `{{key}}` โ€” string interpolation, strips HTML from the value (safe for plain text).
393
+ - `{{{key}}}` โ€” raw HTML interpolation.
743
394
 
744
- Simple and bulletproof HTML template, see [its source](https://github.com/veliovgroup/mail-time/blob/master/template.html). Usage example:
395
+ Every `sendMail` option is available inside `text`, `html`, and the wrapping template:
745
396
 
746
397
  ```js
747
- import { MailTime, MongoQueue, RedisQueue } from 'mail-time';
398
+ const layouts = {
399
+ envelope: `<html><body>{{{html}}}<footer>Sent to @{{username}} ({{to}})</footer></body></html>`,
400
+ otp: {
401
+ text: 'Hello @{{username}}! Your code: {{code}}',
402
+ html: '<h1>Sign-in</h1><p>Hello <b>@{{username}}</b></p><pre><code>{{code}}</code></pre>',
403
+ },
404
+ };
748
405
 
749
- // Make it default
750
406
  const mailQueue = new MailTime({
751
- /* .. */
752
- template: MailTime.Template
407
+ /* ... */
408
+ template: layouts.envelope,
753
409
  });
754
410
 
755
- // For single letter
756
- mailQueue.sendMail({
757
- /* .. */
758
- template: MailTime.Template
411
+ await mailQueue.sendMail({
412
+ to: 'user@example.com',
413
+ subject: 'Sign-in code',
414
+ username: 'mike',
415
+ code: 'A1B2-C3D4',
416
+ text: layouts.otp.text,
417
+ html: layouts.otp.html,
759
418
  });
760
419
  ```
761
420
 
762
- ### Template Example
421
+ `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`.
763
422
 
764
- Pass custom template via `template` property to `.sendMail()` method
423
+ ## API
765
424
 
766
- ```js
767
- mailQueue.sendMail({
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
- });
775
- ```
425
+ ### `new MailTime(opts)`
426
+
427
+ | Option | Type | Default | Notes |
428
+ | ----------------------------- | --------------------------------------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
429
+ | `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). |
430
+ | `type` | `'server' \| 'client'` | `'server'` | `'client'` only enqueues โ€” no `transports` / `josk` required. |
431
+ | `transports` | `nodemailer.Transport[]` | โ€” | **Required for `server`**. Non-empty. |
432
+ | `josk` | `MailTimeJoSkOptions` | โ€” | **Required for `server`**. See [JoSk options](#josk-options) below. |
433
+ | `strategy` | `'backup' \| 'balancer'` | `'backup'` | Multi-SMTP rotation policy. |
434
+ | `failsToNext` | `number` | `4` | (`backup`) failures-in-a-row before rotating. |
435
+ | `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`. |
436
+ | `retryDelay` | `number` (ms) | `60000` | Wait between attempts. |
437
+ | `keepHistory` | `boolean` | `false` | Keep sent/failed/cancelled rows. |
438
+ | `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`. |
439
+ | `concatSubject` | `string` | `'Multiple notifications'` | Subject when folded. Supports `{{count}}` for the folded letter count. |
440
+ | `concatDelimiter` | `string` | `'<hr>'` | Separator between folded bodies. |
441
+ | `concatDelay` | `number` (ms) | `60000` | Fold window. |
442
+ | `revolvingInterval` | `number` (ms) | `1536` | Queue iteration interval. |
443
+ | `mode` | `'one' \| 'batch'` | `'batch'` | `'batch'` claims every due row per tick; `'one'` claims a single row per tick. |
444
+ | `concurrency` | `number` | `1` | Parallel SMTPs per instance. The CAS on `isSending` prevents duplicate delivery. |
445
+ | `sendingTimeout` | `number` (ms) | `300000` | Window after which a stuck `isSending=true` row becomes eligible again. Must exceed worst-case SMTP roundtrip. |
446
+ | `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. |
447
+ | `template` | `string` | `'{{{html}}}'` | Default envelope. |
448
+ | `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>`. |
449
+ | `from` | `string \| (transport) => string` | โ€” | Strongly recommended for spam-passing `From:` formatting. |
450
+ | `debug` | `boolean` | `false` | Verbose logs. |
451
+ | `onSent(email, info)` | `function` | โ€” | Called once the task is fully delivered. `email.mailOptions[i].accepted` lists every address that got through (across all attempts). |
452
+ | `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' }`. |
453
+
454
+ ### JoSk options
455
+
456
+ `opts.josk` is passed to the underlying [`JoSk`](https://github.com/veliovgroup/josk) constructor. Useful keys:
457
+
458
+ | Key | Default | Notes |
459
+ | ------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------- |
460
+ | `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. |
461
+ | `minRevolvingDelay` | `512` | Lower bound of poll window. |
462
+ | `maxRevolvingDelay` | `2048` | Upper bound. |
463
+ | `zombieTime` | `60000` | Re-claim if `queue.iterate()` runs longer than this. **Do not drop below 60s.** |
464
+ | `execute` | `'batch'` | JoSk scheduler batching; low impact for MailTime (one interval task per instance). |
465
+ | `concurrency` | `Infinity` | Cap overlapping JoSk handler runs on **this** process (`1` if ticks pile up). |
466
+ | `autoClear` | `false` | Remove orphan tasks from storage. |
467
+ | `lockOwnerId` | `josk-<uuid>` | Stable owner id; recommended per worker. |
468
+ | `onError(title, details)` | (logs to console) | Wire to your logger. |
469
+ | `onExecuted(uid, details)` | โ€” | Optional hook after each successful JoSk tick (observability). |
470
+
471
+ For deeper JoSk semantics, install the JoSk skill: **`npx skills add veliovgroup/josk`** (the same author).
472
+
473
+ ### Methods
474
+
475
+ - `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`.
476
+ - `send(opts)` โ€” alias of `sendMail`.
477
+ - `cancelMail(uuidOrPromise)` โ†’ `Promise<boolean>`. Accepts the `uuid` or the `Promise<string>` from `sendMail`.
478
+ - `cancel(uuid)` โ€” alias of `cancelMail`.
479
+ - `ping()` โ†’ `Promise<{status, code, statusCode, paused?, error?}>`. Pings scheduler then queue; `paused` reflects `isPaused`.
480
+ - `ready()` โ†’ `Promise<MailTime>`. Awaits all startup work; rejects with `.cause` on storage failure.
481
+ - `destroy(opts?)` โ†’ `boolean` or `Promise<boolean>` when `{ drain: true }`. Stops scheduler. Idempotent. Use `destroy({ drain: true })` or `await drain()` after `destroy()` for graceful shutdown.
482
+ - `drain()` โ†’ `Promise<void>`. Resolves once every in-flight SMTP attempt finishes. Useful in tests and graceful-shutdown paths.
483
+ - `pause()` / `resume()` โ†’ `boolean`. Server-only reversible backpressure; no-ops on `client` or after `destroy()`. See [Pause / resume](#pause--resume-a-server-backpressure).
484
+ - `isPaused` โ†’ `boolean`. Read-only; always `false` on `client`.
485
+
486
+ ### Queue constructors
487
+
488
+ | Constructor | Required option | Optional |
489
+ | ---------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
490
+ | `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. |
491
+ | `new MongoQueue({ db, prefix? })` | `db` from `MongoClient#db()` | `prefix` โ€” inherited from `MailTime` when omitted. Indexes auto-created on first `ready()`. |
492
+ | `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()`. |
493
+
494
+ For custom adapters see [docs/queue-api.md](https://github.com/veliovgroup/mail-time/blob/master/docs/queue-api.md).
495
+
496
+ ### Module functions
497
+
498
+ - `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`.
499
+ - `presets` โ€” read-only `{ [name]: partialConfig }` map backing `mailTimePreset`.
500
+ - `presetNames` โ€” read-only array of preset names.
501
+
502
+ ### Static
503
+
504
+ - `MailTime.Template` โ€” get/set the default HTML envelope template.
505
+
506
+ ## Migrations
507
+
508
+ Upgrade checklists, adapter contract changes, and rollout notes live in the docs (no duplication here):
509
+
510
+ | From | To | Full guide |
511
+ |------|-----|------------|
512
+ | 3.x | 4.1 | [v3 โ†’ v4](https://github.com/veliovgroup/mail-time/blob/master/docs/migration-v3-v4.md) |
513
+ | 4.0 | 4.1 | [v4.0 โ†’ v4.1](https://github.com/veliovgroup/mail-time/blob/master/docs/migration-v4-v4.1.md) |
514
+
515
+ New in v4 (opt-in): `mailTimePreset`, `concurrency`/`mode`, `sendingTimeout`, `drain()`/`pause()`/`resume()`, per-recipient handling, AI skills bundle.
776
516
 
777
517
  ## Testing
778
518
 
779
- 1. Clone this package
780
- 2. Start local or obtain URLs for remote MongoDB and Redis servers
781
- 3. In Terminal (*Console*) go to directory where package was cloned
782
- 4. Then run:
783
-
784
- ```shell
785
- # Before running tests make sure NODE_ENV === development
786
- # Install NPM dependencies
787
- npm install --save-dev
519
+ ```sh
520
+ npm install
521
+ # DEFAULT RUN โ€” needs Redis + Mongo + Postgres up locally
522
+ REDIS_URL="redis://127.0.0.1:6379" \
523
+ MONGO_URL="mongodb://127.0.0.1:27017/mail-time-test" \
524
+ PG_URL="postgres://127.0.0.1:5432/postgres" \
525
+ npm test
526
+
527
+ # Single suite
528
+ npm run test:redis
529
+ npm run test:mongo
530
+ npm run test:postgres
531
+
532
+ # Bun-native test runner (only Jest-shaped tests)
533
+ bun test ./test/jest
534
+ ```
788
535
 
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
536
+ `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`.
791
537
 
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
538
+ ## Bun
794
539
 
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
540
+ MailTime ships pure ESM with a generated CJS bundle. Both runtimes (Bun โ‰ฅ 1.1.0, Node โ‰ฅ 20.9.0) load it directly:
797
541
 
798
- # Be patient, tests are taking around 8 mins
542
+ ```js
543
+ import { MailTime } from 'mail-time'; // works in both
799
544
  ```
800
545
 
801
- ## Support this project:
802
-
803
- - Upload and share files using [โ˜„๏ธ meteor-files.com](https://meteor-files.com/?ref=github-mail-time-repo-footer) โ€” Continue interrupted file uploads without losing any progress. There is nothing that will stop Meteor from delivering your file to the desired destination
804
- - Use [โ–ฒ ostr.io](https://ostr.io?ref=github-mail-time-repo-footer) for [Server Monitoring](https://snmp-monitoring.com), [Web Analytics](https://ostr.io/info/web-analytics?ref=github-mail-time-repo-footer), [WebSec](https://domain-protection.info), [Web-CRON](https://web-cron.info) and [SEO Pre-rendering](https://prerendering.com) of a website
805
- - Star on [GitHub](https://github.com/veliovgroup/mail-time)
806
- - Star on [NPM](https://www.npmjs.com/package/mail-time)
807
- - Star on [Atmosphere](https://atmospherejs.com/ostrio/mailer)
808
- - [Sponsor maintainer via GitHub](https://github.com/sponsors/dr-dimitru) โ€” support open source with one-time contribution or on a regular basis
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
546
+ Mixed clusters (some Node, some Bun) share one schedule under the same `prefix` โ€” the lease lives in storage, runtime-agnostic.
547
+
548
+ ## License
549
+
550
+ [BSD-3-Clause](LICENSE).
551
+
552
+ ## Support this project
553
+
554
+ - Star on [GitHub](https://github.com/veliovgroup/mail-time) and [NPM](https://www.npmjs.com/package/mail-time).
555
+ - [Sponsor maintainer on GitHub](https://github.com/sponsors/dr-dimitru).
556
+ - [Sponsor veliovgroup on GitHub](https://github.com/sponsors/veliovgroup).
557
+ - [PayPal](https://paypal.me/veliovgroup).
558
+ - Try [โ˜„๏ธ meteor-files.com](https://meteor-files.com/?ref=github-mail-time-repo-footer).
559
+ - Try [โ–ฒ ostr.io](https://ostr.io?ref=github-mail-time-repo-footer) for server monitoring, web analytics, web-CRON, and SEO pre-rendering.
560
+
561
+ [npm-url]: https://www.npmjs.com/package/mail-time
562
+ [badge-npm-v]: https://img.shields.io/npm/v/mail-time.svg
563
+ [badge-npm-dm]: https://img.shields.io/npm/dm/mail-time.svg
564
+ [badge-ci]: https://github.com/veliovgroup/mail-time/actions/workflows/ci.yml/badge.svg?branch=master
565
+ [ci-url]: https://github.com/veliovgroup/mail-time/actions/workflows/ci.yml
566
+ [badge-size]: https://img.shields.io/bundlephobia/minzip/mail-time
567
+ [size-url]: https://bundlephobia.com/package/mail-time
568
+ [badge-cov]: https://img.shields.io/badge/coverage-~99%25-brightgreen
569
+ [badge-license]: https://img.shields.io/badge/License-BSD%203--Clause-blue.svg
570
+ [license-url]: LICENSE
571
+ [badge-node]: https://img.shields.io/node/v/mail-time
572
+ [node-url]: https://nodejs.org/
573
+ [badge-ts]: https://img.shields.io/badge/TypeScript-ready-blue
574
+ [ts-url]: #quick-start
575
+ [badge-bun]: https://img.shields.io/badge/Bun-%3E%3D1.1.0-black?logo=bun
576
+ [bun-url]: #bun
577
+ [badge-meteor]: https://img.shields.io/badge/Meteor-ostrio%3Amailer-DE4F4F
578
+ [meteor-url]: https://packosphere.com/ostrio/mailer
579
+ [badge-deps]: https://img.shields.io/badge/dependencies-1-brightgreen
580
+ [deps-url]: https://www.npmjs.com/package/mail-time?activeTab=dependencies
581
+ [badge-sponsor]: https://img.shields.io/github/sponsors/dr-dimitru?label=Sponsor
582
+ [sponsor-url]: https://github.com/sponsors/dr-dimitru
583
+ [badge-donate]: https://img.shields.io/badge/Donate-PayPal-00457C?logo=paypal&logoColor=white
584
+ [donate-url]: https://paypal.me/veliovgroup