tutorial // Mar 16, 2021

How to Send Email with Nodemailer

Learn how to configure an SMTP server and send email from your app using Nodemailer. Also learn how to use EJS to create dynamic HTML templates for sending email.

How to Send Email with Nodemailer

To get started, we need to install the nodemailer package via NPM:

npm install nodemailer

This will add Nodemailer to your app. If you're using a recent version of NPM, this should also add nodemailer as a dependency in your app's package.json file.

Choosing an SMTP Provider

Before we move forward, we need to make sure we have access to an SMTP provider. An SMTP provider is a service that provides access to the SMTP server we need to physically send our emails. While you can create an SMTP server on your own, it's usually more trouble than it's worth due to regulatory compliance and technical overhead.

SMTP stands for Simple Mail Transfer Protocol. It's an internet standard communication protcol that describes the protocol used for sending email over the internet.

When it comes to using SMTP in your app, the standard is to use a third-party SMTP service to handle the compliance and technical parts for you so you can just focus on your app. There are a lot of different SMTP providers out there, each with their own advantages, disadvantages, and costs.

Our recommendation? Postmark. It's a paid service, however, it has a great user interface and excellent documentation that save you a lot of time and trouble. If you're trying to avoid paying, an alternative and comparable service is Mailgun.

Before you continue, set up an account with Postmark and then follow this quick tutorial to access your SMTP credentials (we'll need these next).

Alternatively, set up an account with Mailgun and then follow this tutorial to access your SMTP credentials.

Once you have your SMTP provider and credentials ready, let's keep moving.

Configuring Your SMTP Server

Before we start sending email, the first step is to configure an SMTP transport. A transport is the term Nodemailer uses to describe the method it will use to actually send your email.

import nodemailer from 'nodemailer';

const smtp = nodemailer.createTransport({
  host: '',
  port: 587,
  secure: process.env.NODE_ENV !== "development",
  auth: {
    user: '',
    pass: '',
  },
});

First, we import nodemailer from the nodemailer package we installed above. Next, we define a variable const smtp and assign it to a call to nodemailer.createTransport(). This is the important part.

Here, we're passing an options object that tells Nodemailer what SMTP service we want to use to send our email.

Wait, aren't we sending email using our app?

Technically, yes. But sending email on the internet requires a functioning SMTP server. With Nodemailer, we're not creating a server, but instead an SMTP client. The difference is that a server acts as the actual sender (in the technical sense), while the client connects to the server to use it as a relay to perform the actual send.

In our app, then, calling nodemailer.createTransport() establishes the client connection to our SMTP provider.

Using the credentials you obtained from your SMTP provider earlier, let's update this options object. While they may not be exact, your SMTP provider should use similar terminology to describe each of the settings we need to pass:

{
  host: 'smtp.postmarkapp.com',
  port: 587,
  secure: process.env.NODE_ENV !== "development",
  auth: {
    user: 'postmark-api-key-123',
    pass: 'postmark-api-key-123',
  },
}

Here, we want to replace host, port, and the user and pass under the nested auth object.

host should look something like smtp.postmarkapp.com. port should be set to 587 (the secure port for sending email with SMTP).

Note: If you're using Postmark, you will use your API key for both the username and password here.

Double-check and make sure you have the correct settings and then we're ready to move on to sending.

Sending Email

Sending email with Nodemailer is straightforward: all we need to do is call the sendMail method on the value returned from nodemailer.createTransport() that we stored in the smtp variable above, like this:

smtp.sendMail({ ... })

Next, we need to pass the appropriate message configuration for sending our email. The message configuration object is passed to smtp.sendMail() and contains settings like to, from, subject, and html.

As a quick example, let's pass the bare minimum settings we'll need to fire off an email:

[...]

smtp.sendMail({
  to: 'somebody@gmail.com',
  from: 'support@myapp.com',
  subject: 'Testing Email Sends',
  html: '<p>Sending some HTML to test.</p>',
});

Pretty clear. Here we pass in a to, from, subject, and html setting to specify who our email is going to, where it's coming from, a subject to help the recipient identify the email, and some HTML to send in the body of the email.

That's it! Well, that's the basic version. If you take a look at the message configuration documentation for Nodemailer, you'll see that there are several options that you can pass.

To make sure this is all clear, let's look at our full example code so far:

import nodemailer from 'nodemailer';

const smtp = nodemailer.createTransport({
  host: 'smtp.someprovider.com',
  port: 587,
  secure: process.env.NODE_ENV !== "development",
  auth: {
    user: 'smtp-username',
    pass: 'smtp-password',
  },
});

smtp.sendMail({
  to: 'somebody@gmail.com',
  from: 'support@myapp.com',
  subject: 'Testing Email Sends',
  html: '<p>Sending some HTML to test.</p>',
});

Now, while this technically will work, if we copy and paste it verbatim into a plain file, when we run the code, we'll send our email immediately. That's likely a big oops.

Let's modify this code slightly:

import nodemailer from 'nodemailer';

const smtp = nodemailer.createTransport({
  host: 'smtp.someprovider.com',
  port: 587,
  secure: process.env.NODE_ENV !== "development",
  auth: {
    user: 'smtp-username',
    pass: 'smtp-password',
  },
});

export default (options = {}) => {
  return smtp.sendMail(options);
}

Wait! Where did our example options go?

It's very unlikely that we'll want to send an email as soon as our app starts up. To make it so that we can send an email manually, here, we wrap our call to smtp.sendMail() with another function that takes an options object as an argument.

Can you guess what that options object contains? That's right, our missing options.

The difference between this code and the above is that we can import this file elsewhere in our app, calling the exported function at the point where we want to send our email.

For example, let's assume the code above lives at the path /lib/email/send.js in our application:

import sendEmail from '/lib/email/send.js';
import generateId from '/lib/generateId.js';

export default {
  createCustomer: (parent, args, context) => {
    const customerId = generateId();
    await Customers.insertOne({ _id: customerId, ...args.customer });
    
    await sendEmail({
      to: 'admin@myapp.com',
      from: 'support@myapp.com',
      subject: 'You have a new customer!',
      text: 'Hooray! A new customer has signed up for the app.',
    });

    return true;
  },
};

This should look familiar. Again, we're using the same exact message configuration object from Nodemailer here. The only difference is that now, Nodemailer won't send our email until we call the sendEmail() function.

Awesome. So, now that we know how to actually send email, let's take this a step further and make it more usable in our application.

Creating Dynamic Templates with EJS

If you're a Pro Subscriber and have access to the repo for this tutorial, you'll notice that this functionality is built-in to the boilerplate that the repo is based on, the CheatCode Node.js Boilerplate.

The difference between that code and the examples we've looked at so far is that it includes a special feature: the ability to define custom HTML templates and have them compile automatically with dynamic data passed when we call to sendEmail.

Note: The paths above the code blocks below map to paths available in the repo for this tutorial. If you're a CheatCode Pro subscriber and have connected your Github account on the account page, click the "View on Github" button at the top of this tutorial to view the source.

Let's take a look at the entire setup and walk through it.

/lib/email/send.js

import nodemailer from "nodemailer";
import fs from "fs";
import ejs from "ejs";
import { htmlToText } from "html-to-text";
import juice from "juice";
import settings from "../settings";

const smtp = nodemailer.createTransport({
  host: settings?.smtp?.host,
  port: settings?.smtp?.port,
  secure: process.env.NODE_ENV !== "development",
  auth: {
    user: settings?.smtp?.username,
    pass: settings?.smtp?.password,
  },
});

export default ({ template: templateName, templateVars, ...restOfOptions }) => {
  const templatePath = `lib/email/templates/${templateName}.html`;
  const options = {
    ...restOfOptions,
  };

  if (templateName && fs.existsSync(templatePath)) {
    const template = fs.readFileSync(templatePath, "utf-8");
    const html = ejs.render(template, templateVars);
    const text = htmlToText(html);
    const htmlWithStylesInlined = juice(html);

    options.html = htmlWithStylesInlined;
    options.text = text;
  }

  return smtp.sendMail(options);
};

There's a lot of extras here, so let's focus on the familiar stuff first.

Starting with the call to nodemailer.createTransport(), notice that we're calling the exact same code above. The only difference is that here, instead of passing our settings directly, we're relying on the built-in settings convention in the CheatCode Node.js Boilerplate.

Code Tip

Notice that weird syntax we're using in our call to createTransport() like settings?.smtp?.host? Though it may look like a mistake, this is known as optional chaining and is a recent edition to the ECMAScript language specification (the technical name for the specification that JavaScript is based on).

Optional chaining helps us to avoid runtime errors when accessing nested properties on objects. Instead of writing settings && settings.smtp && settings.smtp.host, we can simiplify our code by writing settings?.smtp?.host to achieve the same result.

Next, we want to look at the very bottom of the file. That call to smtp.sendMail(options) should look familiar. In fact, this is the exact same pattern we saw above when we wrapped our call in the function that took the options object.

Adding the Templating Functionality

Now for the tricky part. You'll notice that we've added quite a few imports to the top of our file. In addition to nodemailer, we've added:

  • fs - No install required. This is the File System package that's built-in to the Node.js core. It gives us access to the file system for things like reading and writing files.
  • ejs - The library we'll use for replacing dynamic content inside of our HTML email template.
  • html-to-text - A library that we'll use to automatically convert our compiled HTML into text to improve the accessibility of our emails for users.
  • juice - A library used for automatically inlining any <style></style> tags in our HTML email template.

If you're not using the CheatCode Node.js Boilerplate, go ahead and install those last three dependencies now:

npm install ejs html-to-text juice

Now, let's look a bit closer at the function being exported at the bottom of this example. This function is technically identical to the wrapper function we looked at earlier, with one big difference: we now anticipate a possible template and templateVars value being passed in addition to the message configuration we've seen so far.

Instead of just taking in the options object blindly, though, we're using JavaScript object destructuring to "pluck off" the properties we want from the options object—kind of like grapes. Once we have the template and templateVars properties (grapes), we collect the rest of the options in a new variable called restOfOptions using the ... JavaScript spread operator.

Next, just inside the function body at the top of the function we define a variable templatePath that points to the planned location of our HTML email templates: /lib/email/templates/${templateName}.html.

Here, we pass the templateName property that we destructured from the options object passed to our new function (again, the one that's already included in the CheatCode Node.js Boilerplate). It's important to note: even though we're using the name templateName here, that value is assigned to the options object we pass as template.

Why the name change? Well, if we look a bit further down, we want to make sure that the variable name template is still accessible to us. So, we take advantage of the ability to rename destructured properties in JavaScript by writing { template: templateName }. Here, the : after template tells JavaScript that we want to assign the value in that variable to a new name, in the scope of our current function.

To be clear: we're not permanently changing or mutating the options object here. We're only changing the name—giving it an alias—temporarily within the body of this function; nowhere else.

Next, once we have our template path, we get to work.

First, we set up a new options object containing the "unpacked" version of our restOfOptions variable using the JavaScript spread operator. We do this here because at this point, we can only know for certain the options object passed to our function contains the Nodemailer message configuration options.

In order to determine if we're sending our email using a template, we writing an if statement to say "if there's a templateName present and fs.existsSync(templatePath) returns true for the templatePath we wrote above, assume we have a template to compile."

If either templateName or the fs.existsSync() check were to fail, we'd skip any template compilation and hand off our options object directly to smtp.sendMail().

If, however, we do have a template and it does exist at the path, next, we use fs.readFileSync() to get the raw contents of the HTML template and store them in the template variable. Next, we use the ejs.render() method, passing the HTML template we want to replace content within, followed by the templateVars object containing the replacements for that file.

Because we're writing our code to support any template (not a specific one), let's take a quick look at an example HTML template to ensure this isn't confusing:

/lib/email/templates/reset-password.html

<html>
  <head>
    <title>Reset Password</title>
  </head>
  <style>
    body {
      color: #000;
      font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif;
      font-size: 16px;
      line-height: 24px;
    }
  </style>
  <body>
    <p>Hello,</p>
    <p>A password reset was requested for this email address (<%= emailAddress %>). If you requested this reset, click the link below to reset your password:</p>
    <p><a href="<%= resetLink %>">Reset Your Password</a></p>
  </body>
</html>

Here, we have a plain HTML file with a <style></style> tag containing some generic color and font styles and a short <body></body> containing the contents of our email.

Notice that inside we have some strange, non-standard HTML tags like <%= emailAddress =>. Those are known as EJS tags and are designed to be placeholders where EJS will "spit out" the corresponding values from our templateVars object into the template.

In other words, if our templateVars object looks like this:

{
  emailAddress: 'pizza@test.com',
  resetLink: 'https://justatest.com',
}

We'd expect to get HTML back like this from EJS:

<body>
  <p>Hello,</p>
  <p>A password reset was requested for this email address (pizza@test.com). If you requested this reset, click the link below to reset your password:</p>
  <p><a href="https://justatest.com">Reset Your Password</a></p>
</body>

Now, back in our JavaScript code, after we've gotten back our html string from ejs.render(), we pass it to the htmlToText() method we imported to get back an HTML-free, plain-text string (again, this is used for accessibility—email clients fall back to the text version of an email in the event that there's an issue with the HTML version).

Finally, we take the html once more and pass it to juice() to inline the <style></style> tag we saw at the top. Inlining is the process of adding styles contained in a <style></style> tag directly to an HTML element via its style attribute. This is done to ensure styles are compatible with all email clients which, unfortunately, are all over the map.

Once we have our compiled htmlWithStylesInlined and our text, as our final step, at the bottom of our if statement, we assign options.html and options.text to our htmlWithStylesInlined and our text values, respectively.

Done! Now, when we call our function, we can pass in a template name (corresponding to the name of the HTML file in the /lib/email/templates directory) along with some templateVars to send a dynamically rendered HTML email to our users.

Let's take a look at using this function to wrap things up:

await sendEmail({
  to: args.emailAddress,
  from: settings?.support?.email,
  subject: "Reset Your Password",
  template: "reset-password",
  templateVars: {
    emailAddress: args.emailAddress,
    resetLink,
  },
});

Nearly identical to what we saw before, but notice: this time we pass a template name and templateVars to signal to our function that we want to use the reset-password.html template and to replace its EJS tags with the values in the templateVars object.

Make sense? If not, feel free to share a comment below and we'll help you out!

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode