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/LICENSE +1 -1
- package/README.md +381 -607
- package/adapters/mongo.d.ts +118 -0
- package/adapters/mongo.js +204 -80
- package/adapters/postgres.d.ts +100 -0
- package/adapters/postgres.js +536 -0
- package/adapters/redis.d.ts +105 -0
- package/adapters/redis.js +342 -79
- package/helpers.js +286 -3
- package/index.cjs +2395 -457
- package/index.d.cts +322 -0
- package/index.d.ts +322 -0
- package/index.js +886 -303
- package/package.json +78 -17
- package/presets.d.ts +87 -0
- package/presets.js +193 -0
- package/adapters/blank-example.js +0 -230
package/README.md
CHANGED
|
@@ -1,81 +1,60 @@
|
|
|
1
|
-
[](https://github.com/sponsors/dr-dimitru)
|
|
2
|
-
[](https://paypal.me/veliovgroup)
|
|
3
|
-
<a href="https://ostr.io/info/built-by-developers-for-developers?ref=github-mail-time-repo-top"><img src="https://ostr.io/apple-touch-icon-60x60.png" height="20"></a>
|
|
4
|
-
<a href="https://meteor-files.com/?ref=github-mail-time-repo-top"><img src="https://meteor-files.com/apple-touch-icon-60x60.png" height="20"></a>
|
|
5
1
|
|
|
6
2
|
# MailTime
|
|
7
3
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
85
|
+
### Sending emails from a cluster
|
|
107
86
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Most modern application has scheduled or recurring emails. For example, once a day โ with recent news and updates. It won't be an issue with a single server setup โ the server would send emails at a daily interval via timer or CRON. But in "Cluster" implementation โ each server will attempt to send the same email. *MailTime* built to avoid sending the same email multiple times to a user from horizontally scaled applications.
|
|
111
|
-
|
|
112
|
-
For the maximum durability and agility each Application Server can run *MailTime* in the "Server" mode:
|
|
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
|
-
| |
|
|
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
|
-
|
|
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
|
-
| |
|
|
136
|
-
| |
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
134
|
+
# Bun:
|
|
135
|
+
bun add mail-time nodemailer
|
|
163
136
|
```
|
|
164
137
|
|
|
165
|
-
|
|
138
|
+
> [!NOTE]
|
|
139
|
+
> `nodemailer` and adapter drivers are peers (not bundled) so you can pin your own versions.
|
|
166
140
|
|
|
167
|
-
|
|
168
|
-
# for
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
+
## Quick start
|
|
185
168
|
|
|
186
|
-
|
|
187
|
-
2. [Create NodeMailer's transports](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#2-create-nodemailers-transports)
|
|
188
|
-
3. [Initiate `mail-time` *server*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3-initiate-mail-time)
|
|
189
|
-
- a. [Connect to Redis](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3a-initiate-and-connect-to-redis); Or
|
|
190
|
-
- b. [Connect to MongoDB](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3b-initiate-and-connect-to-mongodb); And
|
|
191
|
-
- c. [*optionally*] [initiate `mail-time` as *client*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3c-optionally-create-client-type-of-mailtime)
|
|
192
|
-
4. [Start sending emails](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#4-send-email)
|
|
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
|
-
|
|
171
|
+
### 1. Import
|
|
195
172
|
|
|
196
173
|
```js
|
|
197
|
-
//
|
|
198
|
-
import { MailTime, MongoQueue, RedisQueue } from 'mail-time';
|
|
174
|
+
// ESM
|
|
175
|
+
import { MailTime, MongoQueue, PostgresQueue, RedisQueue, mailTimePreset } from 'mail-time';
|
|
199
176
|
|
|
200
|
-
//
|
|
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
|
-
|
|
181
|
+
### 2. Create nodemailer transports
|
|
205
182
|
|
|
206
|
-
|
|
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
|
|
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
|
-
|
|
198
|
+
### 3. Initialize MailTime with a preset
|
|
265
199
|
|
|
266
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
343
|
-
console.
|
|
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
|
-
|
|
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
|
-
|
|
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@
|
|
236
|
+
const uuid = await mailQueue.sendMail({
|
|
237
|
+
to: 'user@example.com',
|
|
378
238
|
subject: 'You\'ve got an email!',
|
|
379
|
-
text: 'Plain text
|
|
380
|
-
html: '<h1>HTML</h1><p>Styled
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
249
|
+
### 5. Client-only mode
|
|
441
250
|
|
|
442
|
-
|
|
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
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
type: 'mongo',
|
|
457
|
-
db: (await MongoClient.connect('mongodb://url')).db('database'),
|
|
458
|
-
}
|
|
459
|
-
},
|
|
460
|
-
transports,
|
|
461
|
-
from(transport) {
|
|
462
|
-
return `"Awesome App" <${transport.options.from}>`;
|
|
463
|
-
}
|
|
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
|
-
###
|
|
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
|
-
|
|
473
|
-
|
|
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
|
-
|
|
274
|
+
Or explicitly: `mailQueue.destroy(); await mailQueue.drain();`
|
|
496
275
|
|
|
497
|
-
|
|
276
|
+
`destroy()` is idempotent and stops the scheduler. Always call it from tests; pair with `drain()` when iterate-driven sends ran.
|
|
498
277
|
|
|
499
|
-
|
|
500
|
-
import { MailTime, RedisQueue } from 'mail-time';
|
|
501
|
-
import { createClient } from 'redis';
|
|
502
|
-
import { transports } from './transports.js';
|
|
503
|
-
const redisClient = await createClient({ url: 'redis://url' }).connect();
|
|
278
|
+
#### Pause / resume a server (backpressure)
|
|
504
279
|
|
|
505
|
-
|
|
506
|
-
const mailQueue = new MailTime({
|
|
507
|
-
queue: new RedisQueue({
|
|
508
|
-
client: redisClient,
|
|
509
|
-
}),
|
|
510
|
-
transports,
|
|
511
|
-
strategy: 'backup',
|
|
512
|
-
failsToNext: 1,
|
|
513
|
-
concatEmails: true,
|
|
514
|
-
josk: {
|
|
515
|
-
adapter: {
|
|
516
|
-
type: 'redis',
|
|
517
|
-
client: redisClient
|
|
518
|
-
},
|
|
519
|
-
zombieTime: 120000
|
|
520
|
-
}
|
|
521
|
-
});
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
545
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
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
|
-
|
|
573
|
-
queue: new RedisQueue({ /* ... */ }),
|
|
574
|
-
template: templates.global
|
|
575
|
-
});
|
|
297
|
+
## Storage layouts
|
|
576
298
|
|
|
577
|
-
|
|
578
|
-
to: 'user@gmail.com',
|
|
579
|
-
subject: 'Sign-in request',
|
|
580
|
-
username: 'johndoe',
|
|
581
|
-
code: 'XXXXX-YY',
|
|
582
|
-
text: templates.signInCode.text,
|
|
583
|
-
html: templates.signInCode.html
|
|
584
|
-
});
|
|
585
|
-
```
|
|
299
|
+
Queue storage and scheduler storage can be the same store or different ones. Use this matrix:
|
|
586
300
|
|
|
587
|
-
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
*Create Redis Queue instance.* Use for `opts.queue` when creating *MailTime* instance
|
|
323
|
+
## Settings presets
|
|
680
324
|
|
|
681
|
-
|
|
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
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
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
|
-
|
|
347
|
+
Presets are equally useful on `type: 'client'` instances โ keys that don't apply to the client role are simply ignored.
|
|
698
348
|
|
|
699
|
-
|
|
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
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
+
## Dedicated mail host
|
|
716
360
|
|
|
717
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
+
## Tuning
|
|
726
366
|
|
|
727
|
-
-
|
|
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
|
-
|
|
730
|
-
import { mailQueue } from './mail-queue.js';
|
|
369
|
+
### Option reference (when to touch)
|
|
731
370
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
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
|
-
|
|
392
|
+
- `{{key}}` โ string interpolation, strips HTML from the value (safe for plain text).
|
|
393
|
+
- `{{{key}}}` โ raw HTML interpolation.
|
|
743
394
|
|
|
744
|
-
|
|
395
|
+
Every `sendMail` option is available inside `text`, `html`, and the wrapping template:
|
|
745
396
|
|
|
746
397
|
```js
|
|
747
|
-
|
|
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:
|
|
407
|
+
/* ... */
|
|
408
|
+
template: layouts.envelope,
|
|
753
409
|
});
|
|
754
410
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
-
|
|
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
|
-
|
|
423
|
+
## API
|
|
765
424
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
542
|
+
```js
|
|
543
|
+
import { MailTime } from 'mail-time'; // works in both
|
|
799
544
|
```
|
|
800
545
|
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
-
|
|
810
|
-
- [
|
|
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
|