Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/VeliovGroup/Mail-Time

📮 Email queue extending NodeMailer with multi SMTP transports and horizontally scaled applications support
https://github.com/VeliovGroup/Mail-Time

balancer cluster email email-sender mail mail-time mailer microservice nodejs npm server smtp

Last synced: 26 days ago
JSON representation

📮 Email queue extending NodeMailer with multi SMTP transports and horizontally scaled applications support

Awesome Lists containing this project

README

        

[![support](https://img.shields.io/badge/support-GitHub-white)](https://github.com/sponsors/dr-dimitru)
[![support](https://img.shields.io/badge/support-PayPal-white)](https://paypal.me/veliovgroup)

# MailTime

"Mail-Time" is NPM package for mail queue management. Build on top of the [`nodemailer`](https://github.com/nodemailer/nodemailer) package. Mail-Time made for single-server and horizontally scaled multi-server setups in mind.

Every `MailTime` instance can have `type` configured as *Server* or *Client*. *Server* type of `MailTime` is great for creating an emailing micro-service app.

The main difference between *Server* and *Client* `type` is that the *Server* handles the queue and __sends__ email. While the *Client* only __adds__ emails into the queue.

## ToC

- [How it works?](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#how-it-works)
- [With single SMTP](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#single-point-of-failure)
- [With multiple SMTP](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#multiple-smtp-providers)
- [For horizontally-scaled apps](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#sending-emails-from-cluster-of-servers)
- [Features](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#features)
- [Installation](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#installation)
- [Meteor.js usage](https://github.com/veliovgroup/mail-time/blob/master/docs/meteor.md)
- [Usage examples](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#basic-usage)
- [Require/Import `mail-time`](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#1-require-package)
- [Create NodeMailer's transports](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#2-create-nodemailers-transports)
- [Initiate `mail-time`](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3-initiate-mail-time)
- [Connect to Redis](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3a-initiate-and-connect-to-redis)
- [Connect to MongoDB](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3b-initiate-and-connect-to-mongodb)
- [Initiate as *Client* instance](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3c-optionally-create-client-type-of-mailtime)
- [Initiate two `MailTime` instances within the single app](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#two-mailtime-instances-usage-example)
- [Use templates](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#passing-variables-to-the-template)
- Different storage configurations:
- [Use MongoDB for queue and scheduler](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#using-mongodb-for-queue-and-scheduler)
- [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)
- [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)
- [Use Redis for queue and scheduler](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#using-redis-for-queue-and-scheduler)
- [API](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#api)
- [`new MailTime` *Constructor*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#new-mailtimeopts-constructor)
- [`new RedisQueue` *Constructor*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#new-redisqueueopts-constructor)
- [`new MongoQueue` *Constructor*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#new-mongoqueueopts-constructor)
- [`.sendMail()`](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#sendmailopts)
- [`.cancelMail()`](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#cancelmailuuid)
- [`.ping()`](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#ping)
- [Default Template](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#static-mailtimetemplate)
- [Custom Templates](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#template-example)
- [Running tests](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#testing)

## Main features:

- 👨‍🔬 ~94% tests coverage;
- 📦 Two simple dependencies, written from scratch for top performance;
- 🏢 Synchronize email queue across multiple (horizontally scaled) servers;
- 💪 Bulletproof design, built-in retries.

## How does it work?

Redundant solution for email transmission.

### Single point of failure

Issue - mitigate a single point of failure via persistent queue and re-send attempts

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

The scheme above will work as long as SMTP service is available
or connection between your server and SMPT is up. Once network
failure occurs or SMTP service is down - users won't be happy

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

Single SMTP solution may work in case of network or other failures
As long as MailTime has not received confirmation what email is sent
it will keep the letter in the queue and retry to send it again

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

### Multiple SMTP providers

Rotate email transports by using multiple SMTP providers. *MailTime* support two strategies `backup` (*rotate when failed*) and `balancer` (*round-robin rotation*)

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

### Sending emails from cluster of servers

It is common to have horizontally scaled "Cluster" of servers for load-balancing and for durability.

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.

For the maximum durability and agility each Application Server can run *MailTime* in the "Server" mode:

```ascii
|===================THE=CLUSTER===================| |=QUEUE=|
| |----------| |----------| |----------| | | | |--------|
| | App | | App | | App | | | |-->| 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 |-----/
| | | | |--------|
|=================================================| |=======|
```

To split roles *MailTime* can run on a dedicated machine as micro-service. This case is great for private email servers with implemented authentication via rDNS and PTR records:

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

## Features

- __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
- __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
- __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
- __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
- __Sending retries__ — Built-in retries for failed to send emails due to network or other failures
- __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)

## Installation

To implement Server functionality — begin with installing `nodemailer`, although this package meant to be used with `nodemailer`, it's not added as the dependency, as `nodemailer` not needed by Client, and to give freedom to choose `nodemailer`'s version to fit every project needs:

```shell
npm install --save nodemailer
```

Install *MailTime* package:

```shell
# for node@>=14.20.0
npm install --save mail-time

# for node@<14.20.0
npm install --save mail-time@=1.3.4

# for node@<8.9.0
npm install --save mail-time@=0.1.7
```

## Basic usage

Setup Nodemailer's transports, Queue storage, and *MailTime* instance

### Steps to get started

See steps 1-4 below to learn about different parts of *MailTime* library and how it can get used. From configuration options to sending email

1. [Require `mail-time` package](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#1-require-package)
2. [Create NodeMailer's transports](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#2-create-nodemailers-transports)
3. [Initiate `mail-time` *server*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3-initiate-mail-time)
- a. [Connect to Redis](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3a-initiate-and-connect-to-redis); Or
- b. [Connect to MongoDB](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3b-initiate-and-connect-to-mongodb); And
- c. [*optionally*] [initiate `mail-time` as *client*](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#3c-optionally-create-client-type-of-mailtime)
4. [Start sending emails](https://github.com/veliovgroup/mail-time?tab=readme-ov-file#4-send-email)

#### 1. Require package

```js
// import as ES Module
import { MailTime, MongoQueue, RedisQueue } from 'mail-time';

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

#### 2. Create NodeMailer's transports

For compatibility and flexibility *MailTime* has no dependency on `nodemailer` it should be installed and imported manually. Create one or more "SMTP transports" before initializing new *MailTime* instance.

For details and full list of options available in `.createTransport()` see [`nodemailer` docs](https://nodemailer.com/smtp/)

```js
// transports.js
import nodemailer from 'nodemailer';
// Use DIRECT transport
// and enable sending email from localhost
// install "nodemailer-direct-transport" NPM package:
import directTransport from 'nodemailer-direct-transport';

const transports = [];
const directTransportOpts = {
pool: false,
direct: true,
name: 'mail.example.com',
from: '[email protected]',
};
transports.push(nodemailer.createTransport(directTransport(directTransportOpts)));
// IMPORTANT: Add `.options` to a newly created transport,
// this is necessary to make sure options are available to MailTime package:
transports[0].options = directTransportOpts;

// Private SMTP
transports.push(nodemailer.createTransport({
host: 'smtp.example.com',
from: '[email protected]',
auth: {
user: 'no-reply',
pass: 'xxx'
},
}));

// Google Apps SMTP
transports.push(nodemailer.createTransport({
host: 'smtp.gmail.com',
from: '[email protected]',
auth: {
user: '[email protected]',
pass: 'xxx'
},
}));

// Mailing service (SparkPost as example)
transports.push(nodemailer.createTransport({
host: 'smtp.sparkpostmail.com',
port: 587,
from: '[email protected]',
auth: {
user: 'SMTP_Injection',
pass: 'xxx'
},
}));

export { transports };
```

#### 3. Initiate `mail-time`

Create new instance of *MailTime* in the *Server* mode, — it will be able to __send__ and __add__ emails to the queue.

#### 3a. Initiate and connect to Redis

Connecting to Redis before initiating `new MailTime` instance:

```js
// mail-queue.js
import { MailTime, RedisQueue } from 'mail-time';
import { createClient } from 'redis';
import { transports } from './transports.js';

// Use REDIS_URL environment variable to store connection string to MongoDB
// example: "REDIS_URL=redis://127.0.0.1:6379/myapp node mail-micro-service.js"
const redisClient = await createClient({ url: process.env.REDIS_URL }).connect();
const mailQueue = new MailTime({
transports,
queue: new MongoQueue({
client: redisClient,
}),
josk: {
adapter: {
type: 'redis',
client: redisClient,
}
},
template: MailTime.Template // Use default template
from(transport) {
// To pass spam-filters `from` field should be correctly set
// for each transport, check `transport` object for more options
return `"Awesome App" <${transport.options.from}>`;
},
onError(error, email, details) {
console.log(`Email "${email.mailOptions.subject}" wasn't sent to ${email.mailOptions.to}`, error, details);
},
onSent(email, details) {
console.log(`Email "${email.mailOptions.subject}" successfully sent to ${email.mailOptions.to}`, details);
},
});

export { mailQueue };
```

#### 3b. Initiate and connect to MongoDB

Connecting to MongoDB before initiating `new MailTime` instance:

```js
// mail-queue.js
import { MailTime, MongoQueue } from 'mail-time';
import { MongoClient } from 'mongodb';
import { transports } from './transports.js';

// Use MONGO_URL environment variable to store connection string to MongoDB
// example: "MONGO_URL=mongodb://127.0.0.1:27017/myapp node mail-micro-service.js"
const mongodb = (await MongoClient.connect(process.env.MONGO_URL)).db('database');
const mailQueue = new MailTime({
transports,
queue: new MongoQueue({
db: mongodb,
}),
josk: {
adapter: {
type: 'mongo',
db: mongodb,
}
},
template: MailTime.Template // Use default template
from(transport) {
// To pass spam-filters `from` field should be correctly set
// for each transport, check `transport` object for more options
return `"Awesome App" <${transport.options.from}>`;
},
onError(error, email, details) {
console.log(`Email "${email.mailOptions.subject}" wasn't sent to ${email.mailOptions.to}`, error, details);
},
onSent(email, details) {
console.log(`Email "${email.mailOptions.subject}" successfully sent to ${email.mailOptions.to}`, details);
},
});

export { mailQueue };
```

#### 3c. Optionally create *Client* type of *MailTime*

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

```js
// mail-queue.js
import { MailTime, RedisQueue } from 'mail-time';
import { createClient } from 'redis';

const mailQueue = new MailTime({
type: 'client',
queue: new RedisQueue({
client: await createClient({ url: 'redis://url' }).connect()
}),
});

export { mailQueue };
```

#### 4. Send email

Import created `mailQueue` where needed and call `.sendMail()` method. See [NodeMailer's message configuration documentation](https://nodemailer.com/message/) for details

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

await mailQueue.sendMail({
to: '[email protected]',
subject: 'You\'ve got an email!',
text: 'Plain text message',
html: '

HTML

Styled message

'
});
```

### Using MongoDB for queue and scheduler

*MailTime* uses separate storage for Queue management and Scheduler. In the example below MongoDB is used for both

```js
import { MailTime, MongoQueue } from 'mail-time';
import { MongoClient } from 'mongodb';
import { transports } from './transports.js';

const db = (await MongoClient.connect('mongodb://url')).db('database');
const mailQueue = new MailTime({
queue: new MongoQueue({
db: db,
}),
josk: {
adapter: {
type: 'mongo',
db: db,
}
},
transports,
from(transport) {
// To pass spam-filters `from` field should be correctly set
// for each transport, check `transport` object for more options
return `"Awesome App" <${transport.options.from}>`;
}
});
```

### Using MongoDB for queue and Redis for scheduler

*MailTime* uses separate storage for Queue management and Scheduler. In the example below MongoDB is used for queue and Redis is used for scheduler

```js
import { MailTime, MongoQueue } from 'mail-time';
import { MongoClient } from 'mongodb';
import { createClient } from 'redis';
import { transports } from './transports.js';

const mailQueue = new MailTime({
queue: new MongoQueue({
db: (await MongoClient.connect('mongodb://url')).db('database'),
}),
josk: {
adapter: {
type: 'redis',
client: await createClient({ url: 'redis://url' }).connect(),
}
},
transports,
from(transport) {
return `"Awesome App" <${transport.options.from}>`;
}
});
```

### Using Redis for queue and MongoDB for scheduler

*MailTime* uses separate storage for Queue management and Scheduler. In the example below Redis is used for queue and MongoDB is used for scheduler

```js
import { MailTime, RedisQueue } from 'mail-time';
import { MongoClient } from 'mongodb';
import { createClient } from 'redis';
import { transports } from './transports.js';

const mailQueue = new MailTime({
queue: new RedisQueue({
client: await createClient({ url: 'redis://url' }).connect(),
}),
josk: {
adapter: {
type: 'mongo',
db: (await MongoClient.connect('mongodb://url')).db('database'),
}
},
transports,
from(transport) {
return `"Awesome App" <${transport.options.from}>`;
}
});
```

### Using Redis for queue and scheduler

*MailTime* uses separate storage for Queue management and Scheduler. In the example below Redis is used for both

```js
import { MailTime, RedisQueue } from 'mail-time';
import { MongoClient } from 'mongodb';
import { createClient } from 'redis';
import { transports } from './transports.js';

const redisClient = await createClient({ url: 'redis://url' }).connect();
const mailQueue = new MailTime({
queue: new RedisQueue({
client: redisClient,
}),
josk: {
adapter: {
type: 'redis',
client: redisClient,
}
},
transports,
from(transport) {
return `"Awesome App" <${transport.options.from}>`;
}
});
```

### Two `MailTime` instances usage example

Create two `MailTime` instances with different settings. One for urgent (*e.g. "transactional" emails*), and another one for other types of emails (*e.g. "marketing" emails*)

```js
import { MailTime, RedisQueue } from 'mail-time';
import { createClient } from 'redis';
import { transports } from './transports.js';
const redisClient = await createClient({ url: 'redis://url' }).connect();

// CREATE mailQueue FOR NON-URGENT EMAILS WHICH IS OKAY TO CONCATENATE
const mailQueue = new MailTime({
queue: new RedisQueue({
client: redisClient,
}),
transports,
strategy: 'backup',
failsToNext: 1,
concatEmails: true,
josk: {
adapter: {
type: 'redis',
client: redisClient
},
zombieTime: 120000
}
});

// CREATE mailInstantQueue FOR TRANSACTIONAL EMAILS AND ALERTS
const mailInstantQueue = new MailTime({
queue: new RedisQueue({
client: redisClient,
prefix: 'instant'
}),
transports,
prefix: 'instant',
retryDelay: 2000,
strategy: 'backup',
failsToNext: 1,
concatEmails: false,
josk: {
adapter: {
type: 'redis',
client: redisClient
},
zombieTime: 20000
}
});

await mailQueue.sendMail({
to: '[email protected]',
subject: 'You\'ve got an email!',
text: 'Plain text message',
html: '

HTML

Styled message

'
});

await mailInstantQueue.sendMail({
to: '[email protected]',
subject: 'Sign in request',
text: 'Your OTP login code: xxxx:',
html: '

Code:

XXXX'
});
```

### Passing variables to the template

All options passed to the `.sendMail()` method are available inside `text`, `html`, and global templates

```js
const templates = {
global: '{{subject}}{{{html}}}Message sent to @{{username}} user ({{to}})',
signInCode: {
text: 'Hello @{{username}}! Here\'s your login code: {{code}}',
html: `

Sign-in request

Hello @{{username}}!

Copy your login code below:

{{code}}
`
}
};

const mailQueue = new MailTime({
queue: new RedisQueue({ /* ... */ }),
template: templates.global
});

await mailQueue.sendMail({
to: '[email protected]',
subject: 'Sign-in request',
username: 'johndoe',
code: 'XXXXX-YY',
text: templates.signInCode.text,
html: templates.signInCode.html
});
```

## API

All available constructor options and `.sendMail()` method API overview

### `new MailTime(opts)` constructor

- `opts` {*object*} - Configuration object
- `opts.type` {*string*} - [Optional] `client` or `server`, default - `server`
- `opts.queue` {*RedisQueue*|*MongoQueue*|*CustomQueue*} - Queue storage driver instance
- `opts.transports` {*[object]*} - [*Required for "server"*] An array of `nodemailer`'s transports, returned from `nodemailer.createTransport({})`. Required for `{type: 'server'}`
- `opts.josk` {*object*} - [*Required for "server"*] [`JoSk` package](https://github.com/veliovgroup/josk#api) options
- `opts.josk.adapter` {*object*|*RedisAdapter*|*MongoAdapter*|*CustomAdapter*} - Config object or *Adapter* instance
- `opts.josk.adapter.type` {*string*} - One of `mongo` *or* `redis`; Pass `josk.adapter.type` to avoid burden of creating *Adapter* instance manually
- `opts.josk.adapter.client` {*RedisClient*} - *RedisClient* instance
- `opts.josk.adapter.db` {*Db*} - Mongo's *Db* instance
- `opts.josk[option]` {*mix*} - Any other options passed to [`JoSk` instance](https://github.com/veliovgroup/josk#api)
- `opts.from` {*function*} - [Optional] A function which returns *string* of `from` field, format: `"MyApp" `
- `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
- `opts.failsToNext` {*number*} - [Optional] After how many failed "send attempts" switch to the next transport, applied only for `backup` strategy, default - `4`
- `opts.prefix` {*string*} - [Optional] Use unique prefixes to create multiple `MailTime` instances within the same application
- `opts.retries` {*number*} - [Optional] How many times resend failed emails, default - `60`
- `opts.retryDelay` {*number*} - [Optional] Interval in *milliseconds* between send re-tries, default - `60000`
- `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`
- `opts.concatEmails` {*boolean*} - [Optional] Concatenate email by `to` field (*e.g. to the same addressee*), default - `false`
- `opts.concatSubject` {*string*} - [Optional] Email subject used in concatenated email, default - `Multiple notifications`
- `opts.concatDelimiter` {*string*} - [Optional] HTML or plain string delimiter used between concatenated email, default - `


`
- `opts.concatDelay` {*number*} - [Optional] Time in *milliseconds* while emails are waiting to be concatenated, default - `60000`
- `opts.revolvingInterval` {*number*} - [Optional] Interval in *milliseconds* in between queue checks, default - `256`
- `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
- `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:
- `error` {*Error*|*object*} - Error object
- `email` {*object*} - email's object
- `details` {*object*} - *not always present*, details from SMTP protocol
- `opts.onSent(email, details)` {*function*} - [Optional] called when email was successfully handed over to receiving/recipient's SMTP server, called with 2 arguments:
- `email` {*object*} - email's object
- `details` {*object*} - *not always present*, details from SMTP server/protocol

```js
import { MailTime, MongoQueue, RedisQueue } from 'mail-time';
import nodemailer from 'nodemailer';
import { createClient } from 'redis';

const redisClient = await createClient({ url: 'redis://url' }).connect();

const mailQueue = new MailTime({
type: 'server',
strategy: 'backup',
prefix: 'appMailQueue',
transports: [nodemailer.createTransport({/* ... */})],
failsToNext: 4,
retries: 60,
retryDelay: 60000,
keepHistory: false,
concatEmails: false,
concatDelay: 60000,
concatDelimiter: '


',
concatSubject: 'Multiple notifications',
revolvingInterval: 256,
template: '{{{html}}}',
queue: new RedisQueue({
client: redisClient,
prefix: 'appMailQueue',
}),
josk: {
adapter: {
type: 'redis',
client: redisClient,
}
},
from(transport) {
// To pass spam-filters `from` field should be correctly set
// for each transport, check `transport` object for more options
return `"App Name" <${transport.options.from}>`;
},
onError(error, email, details) {
console.log(`Email "${email.mailOptions.subject}" wasn't sent to ${email.mailOptions.to}`, error, details);
},
onSent(email, details) {
console.log(`Email "${email.mailOptions.subject}" successfully sent to ${email.mailOptions.to}`, details);
},
});

await mailQueue.sendMail({
to: '[email protected]',
subject: 'Email subject',
text: 'You have got email!',
html: '

You have got email!

',
});
```

### `new RedisQueue(opts)` constructor

*Create Redis Queue instance.* Use for `opts.queue` when creating *MailTime* instance

- `opts` {*object*} - Configuration object
- `opts.client` {*RedisClient*} - Required, Redis'es `RedisClient` instance, like one returned from `await redis.createClient().connect()` method
- `opts.prefix` {*string*} - Optional prefix for scope isolation; use when creating multiple `MailTime` instances within the single application

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

new RedisQueue({
client: await createClient({ url: 'redis://url' }).connect(),
prefix: 'appMailQueue',
});
```

### `new MongoQueue(opts)` constructor

*Create MongoDB Queue instance.* Use for `opts.queue` when creating *MailTime* instance

- `opts` {*object*} - Configuration object
- `opts.db` {*Db*} - Required, Mongo's `Db` instance, like one returned from `MongoClient#db()`
- `opts.prefix` {*string*} - Optional prefix for scope isolation; use when creating multiple `MailTime` instances within the single application

```js
import { MailTime, MongoQueue } from 'mail-time';
import { MongoClient } from 'mongodb';

new MongoQueue({
db: (await MongoClient.connect('mongodb://url')).db('database'),
prefix: 'appMailQueue',
});
```

### `sendMail(opts)`

*Add email to the queue.* Returns `Promise` unique email's `uuid`

- `opts` {*object*} - Configuration object
- `opts.sendAt` {*number*} - When email should be sent, default - `Date.now()`
- `opts.template` {*string*} - Email specific template, this will override default template passed to `MailTime` constructor
- `opts.concatSubject` {*string*} - Email specific concatenation subject, this will override default concatenation subject passed to `MailTime` constructor
- `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

### `cancelMail(uuid)`

*Remove email from queue.* Returns `Promise` — `true` if cancelled or `false` if not found, was sent, or was cancelled previously. Throws *Error*

- `uuid` {*string*|*promise*} — email's `uuid` returned from `.sendEmail()` method

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

const uuid = await mailQueue.sendMail({
to: '[email protected]',
subject: 'Email subject',
text: 'You have got email!',
html: '

You have got email!

',
});

await mailQueue.cancelMail(uuid);
```

### `ping()`

*Ping MailTime instance, its scheduler, and its queue.* Returns `Promise`

```js
const mailQueue = new MailTime({ /* ... */ });

const pingResult = await mailQueue.ping();
console.log(pingResult)
/**
In case of the successful response
{
status: 'OK',
code: 200,
statusCode: 200,
}

Failed response
{
status: 'Error reason',
code: 500,
statusCode: 500,
error: ErrorObject
}
*/
```

### `static MailTime.Template`

Simple and bulletproof HTML template, see [its source](https://github.com/veliovgroup/mail-time/blob/master/template.html). Usage example:

```js
import { MailTime, MongoQueue, RedisQueue } from 'mail-time';

// Make it default
const mailQueue = new MailTime({
/* .. */
template: MailTime.Template
});

// For single letter
mailQueue.sendMail({
/* .. */
template: MailTime.Template
});
```

### Template Example

Pass custom template via `template` property to `.sendMail()` method

```js
mailQueue.sendMail({
to: '[email protected]',
userName: 'Mike',
subject: 'Sign up confirmation',
text: 'Hello {{userName}}, \r\n Thank you for registration \r\n Your login: {{to}}',
html: '


Hello {{userName}}




  • Thank you for registration

  • Your login: {{to}}


',
template: '{{{html}}}'
});
```

## Testing

1. Clone this package
2. Start local or obtain URLs for remote MongoDB and Redis servers
3. In Terminal (*Console*) go to directory where package was cloned
4. Then run:

```shell
# Before running tests make sure NODE_ENV === development
# Install NPM dependencies
npm install --save-dev

# DEFAULT RUN
REDIS_URL="redis://127.0.0.1:6379" MONGO_URL="mongodb://127.0.0.1:27017/npm-mail-time-test-001" npm test

# OPTIONALLY RUN WITH CUSTOM DOMAIN
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

# IF SOME TESTS ARE FAILING: ENABLE DEBUG
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

# Be patient, tests are taking around 8 mins
```

## Support this project:

- Upload and share files using [☄️ meteor-files.com](https://meteor-files.com/?ref=github-mail-time-repo-footer) — Continue interrupted file uploads without losing any progress. There is nothing that will stop Meteor from delivering your file to the desired destination
- Use [▲ ostr.io](https://ostr.io?ref=github-mail-time-repo-footer) for [Server Monitoring](https://snmp-monitoring.com), [Web Analytics](https://ostr.io/info/web-analytics?ref=github-mail-time-repo-footer), [WebSec](https://domain-protection.info), [Web-CRON](https://web-cron.info) and [SEO Pre-rendering](https://prerendering.com) of a website
- Star on [GitHub](https://github.com/veliovgroup/mail-time)
- Star on [NPM](https://www.npmjs.com/package/mail-time)
- Star on [Atmosphere](https://atmospherejs.com/ostrio/mailer)
- [Sponsor maintainer via GitHub](https://github.com/sponsors/dr-dimitru) — support open source with one-time contribution or on a regular basis
- [Sponsor veliovgroup via GitHub](https://github.com/sponsors/veliovgroup) — support company behind this package
- [Support via PayPal](https://paypal.me/veliovgroup) — support our open source contributions