Beginner's Guide On Sending Automated Emails With Node.js, Nodemailer, and Cron Jobs.

Beginner's Guide On Sending Automated Emails With Node.js, Nodemailer, and Cron Jobs.

Sending scheduled, automated emails is a common need for many web apps and services. For example, you may want to send weekly digests to users, automatic reminders or notifications at set times, deliver scheduled reports, and so on.

In this comprehensive tutorial, I will teach how to use Node.js, Nodemailer, and cron jobs to send automated emails on a recurring schedule. We’ll walk through a step-by-step process for setting up everything you need, including:

  • Nodemailer for sending emails from Node.js

  • HTML email templates and styles

  • A Bull queue for processing email jobs

  • Cron jobs to schedule adding emails to the queue

  • Handling errors and logging

By the end, you’ll know how to integrate scheduled email sending into your Node.js apps!

Are you ready?

Prerequisites

This tutorial assumes:

We’ll use Nodemailer to send emails, Bull for queues, and node-cron for cron jobs. You’ll also need access to a MongoDB or similar database to store logs.

Setting Up Nodemailer

First, we need to set up Nodemailer, which will handle sending the actual emails from our Node.js application.

Install Nodemailer via npm:

npm install nodemailer

Then, require it in your app:

const nodemailer = require('nodemailer');

Next, we must create a transporter object from nodemailer configured to use our chosen email-sending service.

For example, if using Mailgun, it would look like this:

const transporter = nodemailer.createTransport({
  service: 'Mailgun',
  auth: {
    user: process.env.MAILGUN_USER,
    pass: process.env.MAILGUN_PASS  
  }
});

Make sure to store your service credentials securely in environment variables.

You can configure Nodemailer for any SMTP service, including SendGrid, Mailjet, Amazon SES, Gmail, and more. Just refer to the respective Nodemailer transport setup docs for any service.

Optionally, you may also want to create and use email templates for the body content of the emails you send. Handlebars templates work great for this.

First, install the nodemailer-express-handlebars package:

npm install nodemailer-express-handlebars

Then you can configure Nodemailer to use it:

const handlebarsOptions = {
  viewEngine: {
    extName: '.html',
    partialsDir: path.resolve('./emails'),
    defaultLayout: false,
  },
  viewPath: path.resolve('./emails'),
  extName: '.html',
};
transporter.use('compile', hbs(handlebarsOptions));

This allows you to create .html email templates that will be used to generate the email body content.

With Nodemailer configured, we’re ready to start creating email templates.

Creating the Email Templates

For maximum reusability and consistency, you’ll want to create HTML email templates that can be used to generate the content for scheduled emails.

Start by creating an emails directory where the templates will live:

mkdir emails

Inside emails, you can create templates like:

  • digest.html - for a weekly digest

  • notification.html - for notifications

  • report.html - for reports

And so on, depending on your needs.

The templates should contain your emails' basic HTML structure and any placeholders for dynamic data like {firstName}, {digestReports} etc.

For example:

<html>
  <body>
    <p>Hi {firstName}, here's your weekly digest:</p>
    {{{digestReports}}}
    <p>Let us know if you have any questions!</p>
  </body>
</html>

The templates can also include CSS styling for layout and formatting:

<style>
  /* CSS rules */
</style>

This allows you to create reusable templates that can be rendered with different data to generate the full emails.

Building the Email Queue

We'll use a queue system to process sending the scheduled emails efficiently. This allows concurrently processing jobs without getting rate-limited or overwhelmed.

We’ll use the bull package for implementing a queue in our app.

Install Bull:

npm install bull

Then, create a Queue instance:

const Queue = require('bull');
const emailQueue = new Queue('email queue', {
  redis: {
    host: '127.0.0.1',
    port: 6379,
  }
});

This creates a emailQueue that Redis will go back to store the jobs.

To add a job to the queue, call:

emailQueue.add({
  // job data
});

The job object can contain any data needed to process the email, like recipients, templates, variables, etc.

Bull will then process these jobs in the background. To start processing, call:

emailQueue.process(async (job) => {
  // send email
});

This processes jobs sequentially by default. You can handle concurrency like:

emailQueue.process(5, async (job) => {
  // send email 
});

This will process 5 jobs at once. Tweak based on your app’s needs and volume.

Now, add it to the queue whenever you need to send an email! The queue will make sure it gets sent smoothly.

Configuring Cron Jobs

To schedule running jobs that add emails to our queue, we’ll use cron jobs.

Cron allows scheduling tasks using time-based syntax like:

* * * * *

┬ ┬ ┬ ┬ ┬

│ │ │ │  │

│ │ │ │ │

│ │ │ │ │

│ │ │ │ └───── day of week (0 - 7) (0 to 6 are Sunday to Saturday, or use names)

│ │ │ └────────── month (1 - 12)

│ │ └─────────────── day of month (1 - 31)

│ └──────────────────── hour (0 - 23)

└───────────────────────── min (0 - 59)

So 0 0 * * * would run at midnight every day.

We’ll use the node-cron package to create cron-powered tasks in JavaScript.

Install it:

npm install node-cron

Then you can create a cron task like:

const cron = require('node-cron');
cron.schedule('0 0 * * *', () => {
  // run at midnight every day
});

For our email scheduler, we likely want to create a few different cron tasks for different intervals like:

// Daily digest at 9am

cron.schedule('0 9 * * *', () => {

  addDailyDigestEmail(); 

});


// Weekly reports each Monday

cron.schedule('0 9 * * 1', () => {

  addWeeklyReportEmail();

}); 


// Monthly newsletter on 1st of month

cron.schedule('0 9 1 * *', () => {

  addMonthlyNewsletter();

});

This allows us to schedule repeating email jobs easily!

Sending the Emails

With our queue processing setup, templates defined, and cron jobs scheduled - we're ready to handle email sending.

In each cron job, we need to add a job to the queue with the necessary data:

// Add daily digest 

cron.schedule('0 9 * * *', () => {
  const data = {
    template: 'digest',
    recipients: ['user1@email.com', 'user2@email.com'],
  };
  emailQueue.add(data);
});

The queue processor will pick this up, with concurrency defined:

emailQueue.process(5, async (job) => {
  const { template, recipients } = job.data;
  // Build email from template
  // Send with nodemailer
  // Handle result
});

Inside the processor, use Nodemailer to send the actual email:

// Create Nodemailer email options
const mailOptions = {
  from: 'Sender Name <sender@email.com',
  to: recipients,
  subject: 'Your Daily Digest',
  html: output, // rendered template 
};

// Send email
transporter.sendMail(mailOptions, (error, info) => {
  if (error) {
    // handle error
  } else {
    // email sent!
  }
});

And that's the core process! The cron jobs add jobs to the queue, the queue processes them concurrently, uses the templates to generate email content, and then sends them with Nodemailer.

Make sure to handle any errors from Nodemailer sending appropriately. When done, you may also want to send a success/failure response back to the queue.

This allows the processing of many scheduled emails to smoothly and efficiently.

Handling Errors

It's important to properly handle any errors that occur during the email automation process.

For errors that happen when adding jobs to the queue from cron, you can try catching and logging the error:

cron.schedule('...', () => {
  try {
    // add job 
  } catch (error) {
    // log error
  }
});

Inside the queue processor, you may want to listen for events like failed from Bull:

emailQueue.on('failed', (error) => {
  // job failed, handle error
});

As mentioned, Nodemailer handles sending errors:

transporter.sendMail(mailOptions, (error) => {
  if (error) {
    // log and handle error
  }
});

Based on the error, you may want to retry sending the email if it's transient. Or if an error continues occurring, pause the cron jobs and queue until you resolve.

Robust error handling will ensure failures don't cause the automation to go haywire.

Storing Email Logs

It can be useful to store logs of all scheduled emails sent through the system for analytics and records.

For example, you can use MongoDB and create an EmailLog schema:

const EmailLogSchema = new Schema({
  timestamp: Date,
  recipient: String,
  template: String
});
const EmailLog = mongoose.model('EmailLog', EmailLogSchema);

Then, whenever an email is sent, save a log:

// Inside queue processor
transporter.sendMail(options, (err) => {

  if (err) {
    // handle error
  } else {
    // email sent successfully 

    const log = new EmailLog({
      timestamp: new Date(),
      recipient: recipientEmail,
      template: templateName
    });
    log.save();
  }
});

Having these logs stored allows you to:

  • Analyze how many emails are being sent

  • See errors and trends

  • Optimize templates

  • Identify bad addresses by failures

  • And more!

Testing and Debugging

While developing your automated email process, testing and debugging thoroughly before deploying is helpful.

For testing, one easy technique is adding your email addresses to recipients and sending emails locally.

You can even use a tool like Mailhog to catch the emails sent from your local system during development.

Mailhog gives you a local SMTP server and a web UI to view the emails caught. Super useful for testing!

To further help with debugging, consider:

  • Logging key info like cron task running, jobs added, email sending success/failure, etc.

  • Enabling ES6 stack traces in Node

  • Attaching request IDs and passing them through the system

  • Using a debugger tool like ndb to step through code

Taking time to test and debug your automation flow will pay off with a smoothly-running email scheduler in the long run.

Conclusion

And there we have it - a complete system for scheduled, automated emails with Node.js, Nodemailer, Bull, and Corn!

The key pieces are:

  • Nodemailer transports for sending from Node.js

  • Easy-to-reuse Handlebars email templates

  • A Bull queue for concurrent background processing

  • Cron jobs powered by node-cron to schedule emails

  • Handling and logging errors

  • Saving email logs to the database

  • Rigorous testing and debugging

This allows you to create recurring automated emails for your users, no matter the use case. Customizable templates and flexible scheduling opens up many possibilities.

A few ways you could extend the system further include:

  • Sending webhooks on failures to alert your team

  • Exposing APIs to initiate sending emails manually

  • Integrating with external event systems to trigger emails reactively

  • Building a front to manage templates and schedules

  • Adding deep analytics on sent emails and user engagement

Hopefully, this gives you a comprehensive foundation to build automated emails into your Node.js apps! Reach out if you have any other questions.

Resource