• Stars
    star
    125
  • Rank 286,277 (Top 6 %)
  • Language
    JavaScript
  • License
    BSD 3-Clause "New...
  • Created over 7 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

๐Ÿ“ฎ Bulletproof email queue on top of NodeMailer for a single and multi-server setup

support support

MailTime

"Mail-Time" is a micro-service package for mail queue, with Server and Client APIs. Build on top of the nodemailer package.

Every MailTime instance can get type configured as Server or Client.

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

Main features:

  • ๐Ÿ‘จโ€๐Ÿ”ฌ ~92% tests coverage;
  • ๐Ÿ“ฆ Two simple dependencies, written from scratch for top performance;
  • ๐Ÿข Synchronize email queue across multiple servers;
  • ๐Ÿ’ช Bulletproof design, built-in retries.

How does it work?

Redundant solution for email transmission.

Single point of failure

Issue - classic solution with a single point of failure:

|----------------|         |------|         |------------------|
|  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

Backup scheme with multiple SMTP providers

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

Cluster issue

It is common to create a "Cluster" of servers to balance the load and add a durability layer for horizontal scaling of quickly growing applications.

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. While in "Cluster" implementation โ€” each server will attempt to send the same email. In such cases, users will receive multiple emails with the same content. We built MailTime to address this and other similar issues.

Here is how this issue is solved by using MailTime:

|===================THE=CLUSTER===================| |=QUEUE=| |===Mail=Time===|
| |----------|     |----------|     |----------|  | |       | |=Micro-service=|   |--------|
| |   App    |     |   App    |     |   App    |  | |       | |               |-->| SMTP 1 |------\
| | Server 1 |     | Server 2 |     | Server 3 |  | |    <--------            |   |--------|       \
| |-----\----|     |----\-----|     |----\-----|  | |    -------->            |                |-------------|
|        \---------------\----------------\---------->      | |               |   |--------|   |     ^_^     |
| Each of the "App Server" or "Cluster Node"      | |       | |               |-->| SMTP 2 |-->| Happy users |
| runs Mail Time as a Client which only puts      | |       | |               |   |--------|   |-------------|
| emails into the queue. Aside to "App Servers"   | |       | |               |                    /
| We suggest running Mail Time as a Micro-service | |       | |               |   |--------|      /
| which will be responsible for making sure queue | |       | |               |-->| SMTP 3 |-----/
| has no duplicates and to actually send emails   | |       | |               |   |--------|
|=================================================| |=======| |===============|

Features

  • Queue - Managed via MongoDB, will survive server reboots and failures
  • Support for multiple server setups - "Cluster", Phusion Passenger instances, Load Balanced solutions, etc.
  • Emails concatenation by addressee email - Reduce amount of sent emails to a single user with concatenation, and avoid mistakenly doubled emails
  • When concatenation is enabled - Same emails won't be sent twice, if for any reason, due to bad logic or application failure emails are sent twice or more times - this is solution to solve this annoying behavior
  • Balancing for multiple nodemailer's transports, two modes - backup and balancing. This is the most useful feature โ€” allowing to reduce the cost of SMTP services and add extra layer of durability. If one transport failing to send an email mail-time will switch to the next one
  • Sending retries for network and other failures
  • Templating support with Mustache-like placeholders

Installation

To implement Server functionality โ€” as a first step install nodemailer, although this package meant to be used with nodemailer, it's not added as the dependency, as nodemailer not needed by Client, and you're free to choose nodemailer's version to fit your project needs:

npm install --save nodemailer

Install MailTime package:

# for node@>=8.9.0
npm install --save mail-time

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

Basic usage

Require package:

const MailTime = require('mail-time');

Create nodemailer's transports (see nodemailer docs):

const transports = [];
const nodemailer = require('nodemailer');

// Use DIRECT transport
// and enable sending email from localhost
// install "nodemailer-direct-transport" NPM package:
const directTransport = require('nodemailer-direct-transport');
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'
  },
}));

As the next step initiate mail-time in the Server mode, it will be able to send and add emails to the queue. Connecting to a MongoDB before initiating new MailTime instance:

const MailTime = require('mail-time');
const MongoClient = require('mongodb').MongoClient;

const dbName = 'databaseName';

// 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"
MongoClient.connect(process.env.MONGO_URL, (error, client) => {
  const db = client.db(dbName);

  const mailQueue = new MailTime({
    db, // MongoDB
    type: 'server',
    strategy: 'balancer', // Transports will be used in round robin chain
    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}>`;
    },
    concatEmails: true, // Concatenate emails to the same addressee
    concatDelimiter: '<h1>{{{subject}}}</h1>', // Start each concatenated email with it's own subject
    template: MailTime.Template // Use default template
  });
});

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:

const MailTime = require('mail-time');
const MongoClient = require('mongodb').MongoClient;

const dbName = 'databaseName';

MongoClient.connect(process.env.MONGO_URL, (error, client) => {
  const db = client.db(dbName);

  const mailQueue = new MailTime({
    db,
    type: 'client',
    strategy: 'balancer', // Transports will be used in round robin chain
    concatEmails: true // Concatenate emails to the same address
  });
});

Send email example:

mailQueue.sendMail({
  to: '[email protected]',
  subject: 'You\'ve got an email!',
  text: 'Plain text message',
  html: '<h1>HTML</h1><p>Styled message</p>'
});

Two MailTime instances usage example

Create two MailTime instances with different settings.

// CREATE mailQueue FOR NON-URGENT EMAILS WHICH IS OKAY TO CONCATENATE
const mailQueue = new MailTime({
  db: db,
  interval: 35,
  strategy: 'backup',
  failsToNext: 1,
  concatEmails: true,
  concatThrottling: 16,
  zombieTime: 120000
});

// CREATE mailInstantQueue FOR TRANSACTIONAL EMAILS AND ALERTS
const mailInstantQueue = new MailTime({
  db: db,
  prefix: 'instant',
  interval: 2,
  strategy: 'backup',
  failsToNext: 1,
  concatEmails: false,
  zombieTime: 20000
});

mailQueue.sendMail({
  to: '[email protected]',
  subject: 'You\'ve got an email!',
  text: 'Plain text message',
  html: '<h1>HTML</h1><p>Styled message</p>'
});

mailInstantQueue.sendMail({
  to: '[email protected]',
  subject: 'Sign in request',
  text: 'Your OTP login code: xxxx:',
  html: '<h1>Code:</h1><code>XXXX</code>'
});

Passing variables to the template

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

const templates = {
  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>',
  signInCode: {
    text: 'Hello @{{username}}! Here\'s your login code: {{code}}',
    html: `<h1>Sign-in request</h1><p>Hello @{{username}}! <p>Copy your login code below:</p> <pre><code>{{code}}</code></pre>`
  }
};

const mailQueue = new MailTime({
  db: db,
  template: templates.global
});

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.db {Db} - [Required] Mongo's Db instance. For example returned in callback of MongoClient.connect()
  • opts.type {String} - [Optional] client or server, default - server
  • opts.from {Function} - [Optional] A function which returns String of from field, format: "MyApp" <[email protected]>
  • opts.transports {Array} - [Optional] An array of nodemailer's transports, returned from nodemailer.createTransport({})
  • 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 next transport, applied only for backup strategy, default - 4
  • opts.prefix {String} - [Optional] Use unique prefixes to create multiple MailTime instances on same MongoDB
  • opts.maxTries {Number} - [Optional] How many times resend failed emails, default - 60
  • opts.interval {Number} - [Optional] Interval in seconds between send re-tries, default - 60
  • opts.zombieTime {Number} - [Optional] Time in milliseconds, after this period - pending email will be interpreted as "zombie". This parameter allows to rescue pending email from "zombie mode" in case when: server was rebooted, exception during runtime was thrown, or caused by bad logic, default - 32786. This option is used by package itself and passed directly to JoSk package
  • 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, 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 - <hr>
  • opts.concatThrottling {Number} - [Optional] Time in seconds while emails are waiting to be concatenated, default - 60
  • opts.revolvingInterval {Number} - [Optional] Interval in milliseconds in between queue checks, default - 256. Recommended value โ€” between opts.minRevolvingDelay and opts.maxRevolvingDelay
  • opts.minRevolvingDelay {Number} - [Optional] Minimum revolving delay โ€” the minimum delay between tasks executions in milliseconds, default - 64. This option is passed directly to JoSk package
  • opts.maxRevolvingDelay {Number} - [Optional] Maximum revolving delay โ€” the maximum delay between tasks executions in milliseconds, default - 256. This option is passed directly to JoSk package
  • 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

sendMail(opts [, callback])

  • Alias - send()
  • opts {Object} - Configuration object
  • opts.sendAt {Date} - When email should be sent, default - new Date() use with caution on multi-server setup at different location with the different time-zones
  • opts.template - Email specific template, this will override default template passed to MailTime constructor
  • opts.concatSubject - 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. Note attachments should work only via path, and file must exists on all micro-services servers
  • callback {Function} - Callback called after the email was sent or failed to be sent. Do not use on multi-server setup

static MailTime.Template

Simple and bulletproof HTML template, see its source. Usage example:

const MailTime = require('mail-time');
// Make it default
const mailQueue = new MailTime({
  db: db, // MongoDB
  /* .. */
  template: MailTime.Template
});

// For single letter
mailQueue.sendMail({
  to: '[email protected]',
  /* .. */
  template: MailTime.Template
});

Template Example

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

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: '<div style="text-align: center"><h1>Hello {{userName}}</h1><p><ul><li>Thank you for registration</li><li>Your login: {{to}}</li></ul></p></div>',
  template: '<body>{{{html}}}</body>'
});

Testing

  1. Clone this package
  2. In Terminal (Console) go to directory where package is cloned
  3. Then run:

Test NPM package:

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

# Before run tests you need to have running MongoDB
DEBUG="true" EMAIL_DOMAIN="example.com" MONGO_URL="mongodb://127.0.0.1:27017/npm-mail-time-test-001" npm test

# Be patient, tests are taking around 2 mins

Support this project:

More Repositories

1

Meteor-Files

๐Ÿš€ Upload files via DDP or HTTP to โ˜„๏ธ Meteor server FS, AWS, GridFS, DropBox or Google Drive. Fast, secure and robust.
JavaScript
1,111
star
2

ostrio

โ–ฒ Web services for JavaScript, Angular.js, React.js, Vue.js, Meteor.js, Node.js, and other JavaScript-based websites, web apps, single page applications (SPA), and progressive web applications (PWA). Our services: Pre-rendering, Monitoring, Web Analytics, WebSec, and Web-CRON
61
star
3

ostrio-neo4jdriver

Most advanced and efficient Neo4j REST API Driver, with support of https and GrapheneDB
CoffeeScript
54
star
4

Meteor-Files-Demos

Demos for ostrio:files package
JavaScript
52
star
5

ostrio-Neo4jreactivity

Meteor.js Neo4j database reactivity layer
CoffeeScript
51
star
6

Meteor-logger

๐Ÿงพ Meteor isomorphic logger. Store application logs in File (FS), MongoDB, or print in Console
JavaScript
51
star
7

meteor-cookies

๐Ÿช Isomorphic bulletproof cookie functions for client and server
JavaScript
41
star
8

Meteor-flow-router-meta

Reactive meta tags, JavaScript links and CSS for meteor within flow-router-extra
JavaScript
39
star
9

Meteor-Leaderboard-Neo4j

Neo4j database powered standard --example Leaderboard Meteor app
JavaScript
37
star
10

Meteor-Template-helpers

Template helpers for Session, logical operations and debug
JavaScript
35
star
11

jazeee-meteor-spiderable

Fork of Meteor Spiderable with longer timeout, caching, better server handling
JavaScript
33
star
12

spiderable-middleware

๐Ÿค– Prerendering for JavaScript powered websites. Great solution for PWAs (Progressive Web Apps), SPAs (Single Page Applications), and other websites based on top of front-end JavaScript frameworks
JavaScript
32
star
13

josk

๐Ÿค– Node.js setInterval for multi-server setup, managed via MongoDB
JavaScript
29
star
14

Meteor-flow-router-title

Change document.title on the fly within flow-router
JavaScript
24
star
15

Meteor-logger-file

๐Ÿ”– Meteor Logging: Store application log messages into file (FS)
JavaScript
24
star
16

Meteor-logger-mongo

๐Ÿƒ Meteor Logging: Store application log messages in MongoDB
JavaScript
22
star
17

Meteor-Mailer

๐Ÿ“ฎ Bulletproof email queue on top of NodeMailer with support of multiple clusters and servers setup
21
star
18

request-extra

โšก๏ธ Extremely stable HTTP request module built on top of libcurl with retries, timeouts and callback API
JavaScript
21
star
19

neo4j-fiber

๐Ÿ‘จโ€๐Ÿ”ฌ Neo4j REST API Driver, with support of https and all DB's features
JavaScript
21
star
20

Meteor-logger-console

๐Ÿ–ฅ Meteor Logging: Print Client's log messages into Server's console with nice highlighting
JavaScript
20
star
21

Meteor-root

[Server] โ˜„๏ธ Get path on a server where Meteor application is currently running
JavaScript
19
star
22

Meteor-Files-Demo

Demo application for ostrio:files package
JavaScript
17
star
23

Uniq.site

An unique name, domain and brand generator
CoffeeScript
17
star
24

Meteor-CRON-jobs

๐Ÿƒโ€โ™‚๏ธ๐Ÿค– Task and jobs runner. With support of clusters or multiple meteor.js instances.
15
star
25

Client-Storage

๐Ÿ—„ Bulletproof persistent Client storage, works with disabled Cookies and/or localStorage
JavaScript
15
star
26

Meteor-Internationalization

๐Ÿ™Š Super-Lightweight and fast i18n isomorphic driver for Meteor with support of placeholders.
JavaScript
14
star
27

ostrio-analytics

๐Ÿ“Š Visitor's analytics tracking code for ostr.io service
JavaScript
14
star
28

fps-meter

Efficient and accurate FPS meter, with minimalistic UI
JavaScript
13
star
29

meteor-files-website

File sharing PWA. Upload and share at files.veliov.com
JavaScript
13
star
30

meteor-user-status

Reactively track user's online, offline, and idle statuses
JavaScript
11
star
31

Meteor-iron-router-title

Reactive page title for meteor within iron-router
CoffeeScript
8
star
32

Meteor-Client-Storage

Bulletproof Client storage functions for Meteor, localStorage with fall-back to Cookies
JavaScript
7
star
33

Meteor-iron-router-meta

Change meta tags on the fly within iron-router
CoffeeScript
6
star
34

Meteor-UIBlocker

๐Ÿ–ฅ UI blocker and simple loading spinner for Meteor apps
CSS
5
star
35

Meteor-iron-router-protected

Create protected and meteor-roles restricted routes within iron-router
CSS
5
star
36

meteor-base64

Highly efficient isomorphic Base64 encoding and decoding with support of WebWorkers
JavaScript
5
star
37

meteor-python-files

File Uploads to Meteor Server in Python (meteor pyfiles)
Python
5
star
38

neo4j-demo

Demo application for Neo4j-fiber package
JavaScript
4
star
39

meteor-snippets

Tutorials, demos, tricks'n tips, code snippets for Meteor.js โ˜„๏ธ
JavaScript
4
star
40

Flow-Router-Demos

Demo apps for Flow-Router packages
HTML
3
star
41

meteor-aes-crypto

๐Ÿ” Simplified isomorphic API for AES cipher by CryptoJS
JavaScript
3
star
42

JavaScript-Objects-Extensions-for-Meteor

Useful JavaScript Objects Extensions for Meteor
JavaScript
3
star
43

iron-router-helpers-for-Meteor

An iron-router helpers for quickly adding classnames to your active navigation elements.
JavaScript
3
star
44

meteor-base64-replacement

"Enhanced Performance" base64 package for Meteor
JavaScript
2
star
45

instagram-node

Simple Instagram driver for Meteor (NPM)
JavaScript
1
star
46

best-practices

Software development best practices
JavaScript
1
star
47

meteor-i18n-library

Internationalization and localization (i18n & l10n) for Meteor
CoffeeScript
1
star
48

supporters

๐Ÿ“‹ The list of supporters and sponsors
1
star
49

careers

Job openings within veliovgroup
1
star