Skip to content

veliovgroup/mail-time

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

298 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

MailTime

npm version npm downloads CI Coverage License: BSD-3-Clause Node.js TypeScript Bun dependencies bundle size Meteor GitHub Sponsors Donate

Bulletproof email queue for horizontally scaled Node.js & Bun apps. Built on top of nodemailer and josk. Single runtime dependency, ESM + CJS, full TypeScript declarations.

MailTime runs in one of two modes:

  • 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.
  • client โ€” only enqueues emails. Use for app servers in a "dedicated mail micro-service" topology.

Many clients + one or more servers coexist behind the same prefix in the same store.

Features

  • ๐Ÿข Horizontally scaled โ€” synchronize one queue across N processes/hosts/DCs.
  • ๐Ÿ” Multi-SMTP rotation โ€” backup (failover) and balancer (round-robin) strategies.
  • ๐Ÿ’ช Built-in retries โ€” storage-backed retry with per-letter transport pinning.
  • ๐ŸŽฏ Per-recipient retries โ€” when a multi-to send is partially rejected, only the un-accepted addresses are retried; delivered ones never see a duplicate.
  • ๐Ÿ“ฎ Email concatenation โ€” fold same-to emails arriving inside a window into one letter.
  • ๐ŸŽ›๏ธ One-line setup โ€” built-in presets for transactional, otp, newsletter, marketing, notifications, alerts.
  • ๐Ÿ›ข๏ธ Three first-party storages โ€” MongoDB, Redis, PostgreSQL. Plus a custom-adapter contract.
  • ๐Ÿ“ฆ Bun โ‰ฅ 1.1.0 & Node โ‰ฅ 20.9.0 โ€” same code, both runtimes.
  • ๐Ÿค– Ships with AI agent skills โ€” see AI agent skills below.
  • ๐Ÿ“ Hand-tuned ESM + CJS + TypeScript declarations.
  • ๐Ÿงช 95%+ Jest coverage (85% threshold enforced) + Mocha integration tests for every adapter.

How it works

Single point of failure

|----------------|         |------|         |------------------|
|  Other mailer  | ------> | SMTP | ------> |  ^_^ Happy user  |
|----------------|         |------|         |------------------|

The scheme above works only as long as SMTP is up.

|----------------|  \ /    |------|         |------------------|
|  Other mailer  | --X---> | SMTP | ------> | 0_o Disappointed |
|----------------|  / \    |------|         |------------------|
                     ^- email lost in vain

MailTime keeps every letter in the queue until SMTP confirms delivery.

|----------------|    /    |------|         |------------------|
|   Mail Time    | --X---> | SMTP | ------> |  ^_^ Happy user  |
|---^------------|  /      |------|         |------^-----------|
     \-------------/ ^- We will try later         /
      \- put it back into queue                  /
       \----------Once connection is back ------/

Multiple SMTP providers

backup falls over on failure; balancer round-robins:

                           |--------|
                     /--X--| SMTP 1 |
                    /   ^  |--------|
                   /    \--- Retry with next provider
|----------------|/        |--------|         |------------------|
|   Mail Time    | ---X--> | SMTP 2 |      /->|  ^_^ Happy user  |
|----------------|\   ^    |--------|     /   |------------------|
                   \  \--- Retry         /
                    \      |--------|   /
                     \---->| SMTP 3 |--/
                           |--------|

Sending emails from a cluster

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.

|===================THE=CLUSTER===================| |=QUEUE=|
| |----------|     |----------|     |----------|  | |       |   |--------|
| | MailTime |     | MailTime |     | MailTime |  | |       |-->| SMTP 1 |------\
| | Server 1 |     | Server 2 |     | Server 3 |  | |       |   |--------|       \
| |-----\----|     |----\-----|     |----\-----|  | |       |                |-------------|
|        \---------------\----------------\---------->      |   |--------|   |     ^_^     |
|                                                 | |       |-->| SMTP 2 |-->| Happy users |
| Each "App Server"                               | |       |   |--------|   |-------------|
| runs MailTime as a "Server"                     | |       |                    /
| for the maximum durability                      | |       |   |--------|      /
|                                                 | |       |-->| SMTP 3 |-----/
|                                                 | |       |   |--------|
|=================================================| |=======|

For a dedicated mail machine (rDNS / PTR records), use type: 'client' on app servers and a single type: 'server' micro-service:

|===================THE=CLUSTER===================| |=QUEUE=| |===Mail=Time===|
| |----------|     |----------|     |----------|  | |       | |               |   |--------|
| | MailTime |     | MailTime |     | MailTime |  | |       | | Micro-service |-->| SMTP 1 |------\
| | Client 1 |     | Client 2 |     | Client 3 |  | |       | | running       |   |--------|       \
| |-----\----|     |----\-----|     |----\-----|  | |       | | MailTime as   |                |-------------|
|        \---------------\----------------\---------->      | | "Server" only |   |--------|   |     ^_^     |
|                                                 | |       | | sending       |-->| SMTP 2 |-->| Happy users |
| Each "App" runs MailTime as                     | |       | | emails        |   |--------|   |-------------|
| a "Client" only placing emails to the queue.    | |    <--------            |                    /
|                                                 | |    -------->            |   |--------|      /
|                                                 | |       | |               |-->| SMTP 3 |-----/
|                                                 | |       | |               |   |--------|
|=================================================| |=======| |===============|

See docs/multi-instance.md and docs/dedicated-mail-host.md for full topologies.

Installation

npm install --save mail-time nodemailer
# pick at least one storage driver:
npm install --save redis        # for RedisQueue
npm install --save mongodb      # for MongoQueue
npm install --save pg           # for PostgresQueue

# Bun:
bun add mail-time nodemailer

Note

nodemailer and adapter drivers are peers (not bundled) so you can pin your own versions.

Important

Upgrading from v3? See Migration from 3.x and the full v4.0.0 release notes.

For Meteor.js usage see docs/meteor.md.

AI agent skills

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.

# Install the MailTime skill into the current project:
npx skills add veliovgroup/mail-time

# Recommended: also install the JoSk skill โ€” MailTime is built on JoSk,
# and deep scheduler questions resolve through JoSk's contract.
npx skills add veliovgroup/josk

The npx skills CLI (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.

Quick start

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 instead of hand-tuning every knob.

1. Import

// ESM
import { MailTime, MongoQueue, PostgresQueue, RedisQueue, mailTimePreset } from 'mail-time';

// CommonJS
const { MailTime, MongoQueue, PostgresQueue, RedisQueue, mailTimePreset } = require('mail-time');

2. Create nodemailer transports

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.

// transports.js
import nodemailer from 'nodemailer';

export const transports = [
  nodemailer.createTransport({
    host: 'smtp.example.com',
    from: 'no-reply@example.com',
    auth: { user: 'no-reply', pass: process.env.SMTP_PASS },
  }),
];

3. Initialize MailTime with a preset

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.

// mail-queue.js โ€” transactional emails on Redis
import { MailTime, RedisQueue, mailTimePreset } from 'mail-time';
import { createClient } from 'redis';
import { transports } from './transports.js';

const redisClient = await createClient({ url: process.env.REDIS_URL }).connect();

const mailQueue = new MailTime(mailTimePreset('transactional', {
  type: 'server',
  prefix: 'app',
  queue: new RedisQueue({ client: redisClient }),
  josk: { adapter: { type: 'redis', client: redisClient } },
  transports,
  from: (t) => `"Awesome App" <${t.options.from}>`,
  onSent(email, info) {
    console.log('sent', email.uuid, info);
  },
  onError(error, email, info) {
    console.error('failed', email.uuid, error, info);
  },
}));

await mailQueue.ready();
export { mailQueue };

Switching stores is one import change. The same pattern works with MongoQueue({ db }) + { type: 'mongo', db }, or PostgresQueue({ client: pgPool }) + { type: 'postgres', client: pgPool }.

4. Send and cancel

import { mailQueue } from './mail-queue.js';

const uuid = await mailQueue.sendMail({
  to: 'user@example.com',
  subject: 'You\'ve got an email!',
  text: 'Plain text body',
  html: '<h1>HTML</h1><p>Styled body</p>',
});

// later โ€” cancel before sendAt:
await mailQueue.cancelMail(uuid); // true | false

sendMail returns a stable uuid you can store for cancellation. Pass any nodemailer message option โ€” to, subject, text, html, attachments, cc, bcc, custom headers, etc.

5. Client-only mode

App servers that only enqueue need no transports, no josk โ€” just the queue. Use the same prefix as the server that drains the class.

import { MailTime, RedisQueue } from 'mail-time';
import { createClient } from 'redis';

const redisClient = await createClient({ url: process.env.REDIS_URL }).connect();

export const mailQueue = new MailTime({
  type: 'client',
  prefix: 'app',
  queue: new RedisQueue({ client: redisClient }),
});

6. Shutdown

process.on('SIGTERM', async () => {
  await mailQueue.destroy({ drain: true }); // stop scheduler, then wait for in-flight SMTPs
});

Or explicitly: mailQueue.destroy(); await mailQueue.drain();

destroy() is idempotent and stops the scheduler. Always call it from tests; pair with drain() when iterate-driven sends ran.

Storage layouts

Queue storage and scheduler storage can be the same store or different ones. Use this matrix:

Queue Scheduler (josk) Best for
Postgres Postgres Multi-DC, mixed clocks, strict exactly-once.
Redis Redis High-throughput single-region.
Mongo Mongo Apps already on Mongo (especially Meteor).
Mongo Redis Durable letter storage + sub-second polling.
Redis Mongo Hot Redis letters + Mongo for scheduler.

For split-store setups pass a different client to each:

const mailQueue = new MailTime({
  prefix: 'app',
  queue:  new MongoQueue({ db }),
  transports,
  josk: {
    adapter: { type: 'redis', client: redisClient },
  },
  /* ... */
});

Settings presets

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.

import { MailTime, RedisQueue, mailTimePreset } from 'mail-time';

const mailTime = new MailTime(mailTimePreset('otp', {
  prefix: 'otp',
  queue: new RedisQueue({ client: redisClient }),
  transports: [otpTransport],
  josk: { adapter: { type: 'redis', client: redisClient } },
}));
Preset Shape Best for
transactional retries: 30, retryDelay: 10s, concatEmails: false, concurrency: 1, josk.zombieTime: 120s Receipts, password resets, account changes, welcome emails.
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.
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.
marketing retries: 10, retryDelay: 30s, concatEmails: false, concurrency: 5, josk.zombieTime: 3min Promotional / campaign blasts where each letter is unique.
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.
alerts retries: 20, retryDelay: 5s, snappy revolvingInterval: 1024 + jitter 256/1024, concurrency: 2, sendingTimeout: 60s Ops / admin alerts: monitoring, error reports, escalations.

Presets are equally useful on type: 'client' instances โ€” keys that don't apply to the client role are simply ignored.

Multiple instances (recommended)

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 to keep the boilerplate to a single line per class.

  • Same prefix for every client and server that share one logical queue.
  • Different prefix per class so namespaces don't collide.
  • Never reuse prefix across two instances with different concatEmails, retryDelay, or other mail policy.

Full example wiring three classes (OTP / transactional / marketing) on one Redis connection, plus app-pod client setup, lives in docs/multi-instance.md.

Dedicated mail host

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.

Full systemd unit + worker layout: docs/dedicated-mail-host.md.

Tuning

Defaults fit moderate traffic in a single region. Reach for a preset first; tune individual knobs only when the preset doesn't cover your case. Full guide: docs/tuning.md.

Option reference (when to touch)

Option Default Change when
mode 'batch' 'one' to claim a single row per tick (fairness over throughput across cluster nodes)
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.
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.
revolvingInterval 1536 ms Lower โ†’ faster pickup; higher โ†’ less scheduler I/O
josk.minRevolvingDelay / maxRevolvingDelay 512 / 2048 Lower both โ†’ snappier polls, more storage load
josk.zombieTime 60000 Never below 60s. Iterate releases the JoSk lease as soon as scanning ends โ€” only a stalled storage scan can blow this.
josk.concurrency Infinity Set 1 if scheduler ticks overlap while iterate still runs
josk.execute 'batch' Usually leave default; MailTime only registers one JoSk task per instance
josk.lockOwnerId random Set in production for observability
retries / retryDelay 59 / 60s retries is after the first attempt; default 59 means 60 total attempts. Per email class โ€” transactional shorter, marketing longer.
concatEmails / concatDelay false / 60s On for notification batching; off for OTP and receipts
prefix '' Same on all client + server for one queue; different only per email class / shard

For deeper JoSk semantics (lease lifecycle, scheduler adapters, recurring tasks), install the JoSk skill: npx skills add veliovgroup/josk.

Templates

Two Mustache-like placeholder forms:

  • {{key}} โ€” string interpolation, strips HTML from the value (safe for plain text).
  • {{{key}}} โ€” raw HTML interpolation.

Every sendMail option is available inside text, html, and the wrapping template:

const layouts = {
  envelope: `<html><body>{{{html}}}<footer>Sent to @{{username}} ({{to}})</footer></body></html>`,
  otp: {
    text: 'Hello @{{username}}! Your code: {{code}}',
    html: '<h1>Sign-in</h1><p>Hello <b>@{{username}}</b></p><pre><code>{{code}}</code></pre>',
  },
};

const mailQueue = new MailTime({
  /* ... */
  template: layouts.envelope,
});

await mailQueue.sendMail({
  to: 'user@example.com',
  subject: 'Sign-in code',
  username: 'mike',
  code: 'A1B2-C3D4',
  text: layouts.otp.text,
  html: layouts.otp.html,
});

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.

API

new MailTime(opts)

Option Type Default Notes
queue MongoQueue | RedisQueue | PostgresQueue | CustomQueue โ€” Required. Storage adapter for letters. Custom adapters: see docs/queue-api.md.
type 'server' | 'client' 'server' 'client' only enqueues โ€” no transports / josk required.
transports nodemailer.Transport[] โ€” Required for server. Non-empty.
josk MailTimeJoSkOptions โ€” Required for server. See JoSk options below.
strategy 'backup' | 'balancer' 'backup' Multi-SMTP rotation policy.
failsToNext number 4 (backup) failures-in-a-row before rotating.
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.
retryDelay number (ms) 60000 Wait between attempts.
keepHistory boolean false Keep sent/failed/cancelled rows.
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.
concatSubject string 'Multiple notifications' Subject when folded. Supports {{count}} for the folded letter count.
concatDelimiter string '<hr>' Separator between folded bodies.
concatDelay number (ms) 60000 Fold window.
revolvingInterval number (ms) 1536 Queue iteration interval.
mode 'one' | 'batch' 'batch' 'batch' claims every due row per tick; 'one' claims a single row per tick.
concurrency number 1 Parallel SMTPs per instance. The CAS on isSending prevents duplicate delivery.
sendingTimeout number (ms) 300000 Window after which a stuck isSending=true row becomes eligible again. Must exceed worst-case SMTP roundtrip.
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.
template string '{{{html}}}' Default envelope.
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>.
from string | (transport) => string โ€” Strongly recommended for spam-passing From: formatting.
debug boolean false Verbose logs.
onSent(email, info) function โ€” Called once the task is fully delivered. email.mailOptions[i].accepted lists every address that got through (across all attempts).
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' }.

JoSk options

opts.josk is passed to the underlying JoSk constructor. Useful keys:

Key Default Notes
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.
minRevolvingDelay 512 Lower bound of poll window.
maxRevolvingDelay 2048 Upper bound.
zombieTime 60000 Re-claim if queue.iterate() runs longer than this. Do not drop below 60s.
execute 'batch' JoSk scheduler batching; low impact for MailTime (one interval task per instance).
concurrency Infinity Cap overlapping JoSk handler runs on this process (1 if ticks pile up).
autoClear false Remove orphan tasks from storage.
lockOwnerId josk-<uuid> Stable owner id; recommended per worker.
onError(title, details) (logs to console) Wire to your logger.
onExecuted(uid, details) โ€” Optional hook after each successful JoSk tick (observability).

For deeper JoSk semantics, install the JoSk skill: npx skills add veliovgroup/josk (the same author).

Methods

  • 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.
  • send(opts) โ€” alias of sendMail.
  • cancelMail(uuidOrPromise) โ†’ Promise<boolean>. Accepts the uuid or the Promise<string> from sendMail.
  • cancel(uuid) โ€” alias of cancelMail.
  • ping() โ†’ Promise<{status, code, statusCode, error?}>. Pings scheduler then queue.
  • ready() โ†’ Promise<MailTime>. Awaits all startup work; rejects with .cause on storage failure.
  • destroy(opts?) โ†’ boolean or Promise<boolean> when { drain: true }. Stops scheduler. Idempotent. Use destroy({ drain: true }) or await drain() after destroy() for graceful shutdown.
  • drain() โ†’ Promise<void>. Resolves once every in-flight SMTP attempt finishes. Useful in tests and graceful-shutdown paths.

Queue constructors

Constructor Required option Optional
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.
new MongoQueue({ db, prefix? }) db from MongoClient#db() prefix โ€” inherited from MailTime when omitted. Indexes auto-created on first ready().
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().

For custom adapters see docs/queue-api.md.

Module functions

  • mailTimePreset(name, overrides?) โ†’ fresh MailTime constructor config. Deep-clones the named preset and deep-merges your overrides (scalars win, nested josk composes). Throws on unknown name or non-object overrides.
  • presets โ€” read-only { [name]: partialConfig } map backing mailTimePreset.
  • presetNames โ€” read-only array of preset names.

Static

  • MailTime.Template โ€” get/set the default HTML envelope template.

Migration from 3.x

Full v4 highlights, adapter changes, and type exports live in docs/migration-v3-v4.md. Quick checklist:

  1. Node โ‰ฅ 20.9.0, Bun โ‰ฅ 1.1.0. Bump your runtime first.
  2. Swap adapter imports to the new MongoQueue / RedisQueue / PostgresQueue constructors.
  3. Pass josk โ€” it's now required for type: 'server'.
  4. josk.zombieTime default raised to 60000 ms (was 32786). Set it explicitly if you relied on the old value.
  5. Custom queue adapters โ€” update's claim guard now triggers on { isSending: true, sendingAt, tries } (was { isSent: true, tries }) and must include the stale-lock-recovery clause (isSending === false OR sendingAt <= now - sendingTimeout). The iterate path must call await mailTimeInstance.___dispatch(row) instead of ___send and honor opts.limit / opts.sendingTimeout. See docs/queue-api.md and adapters/blank-example.js.
  6. Default behavior unchanged โ€” concurrency: 1 keeps the post-upgrade send rate identical to v3. Opt into parallel sends by raising concurrency.

New v4 surface to opt into: mailTimePreset, concurrency, mode, sendingTimeout, drain(), per-recipient retries, and the AI agent skills bundle.

Testing

npm install
# DEFAULT RUN โ€” needs Redis + Mongo + Postgres up locally
REDIS_URL="redis://127.0.0.1:6379" \
MONGO_URL="mongodb://127.0.0.1:27017/mail-time-test" \
PG_URL="postgres://127.0.0.1:5432/postgres" \
  npm test

# Single suite
npm run test:redis
npm run test:mongo
npm run test:postgres

# Bun-native test runner (only Jest-shaped tests)
bun test ./test/jest

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.

Bun

MailTime ships pure ESM with a generated CJS bundle. Both runtimes (Bun โ‰ฅ 1.1.0, Node โ‰ฅ 20.9.0) load it directly:

import { MailTime } from 'mail-time'; // works in both

Mixed clusters (some Node, some Bun) share one schedule under the same prefix โ€” the lease lives in storage, runtime-agnostic.

License

BSD-3-Clause.

Support this project

About

๐Ÿ“ฎ Email queue extending NodeMailer with multi SMTP transports and horizontally scaled applications support

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Sponsor this project

  •  

Contributors