Sending Emails Securely Using Node.js, Nodemailer, SMTP, Gmail, and OAuth2

chandrapantachhetri

Chandra Panta Chhetri

Posted on December 16, 2020

Sending Emails Securely Using Node.js, Nodemailer, SMTP, Gmail, and OAuth2

Many solutions online regarding configuring Nodemailer to use your Gmail requires you to enable less secure app access. If that sounds too scary for you, then you have come to the right place! In this article, you will learn how to securely configure Nodemailer and Gmail.

Let's start by understanding what Nodemailer is.

Nodemailer is a module that makes sending emails from Node.js applications ridiculously easy.

The following are the main steps required to send emails:

  1. Creating a transporter (object used to send emails) using either SMTP or some other transport mechanism
  2. Setting up message options (who sends what to whom)
  3. Sending the email by calling sendMail method on the transporter

Less Secure Configuration

Before we look at the secure solution for configuring Nodemailer and Gmail, let's look at the less secure solution.

Using the steps above as a reference, here is the corresponding code:



//Step 1: Creating the transporter
const transporter = nodemailer.createTransport({
    service: "Gmail",
    auth: {
          user: "******@gmail.com",
          pass: "gmail_password"
        }
});

//Step 2: Setting up message options
const messageOptions = {
  subject: "Test",
  text: "I am sending an email from nodemailer!",
  to: "put_email_of_the_recipient",
  from: "put_email_of_sender"
};

//Step 3: Sending email
transporter.sendMail(messageOptions);


Enter fullscreen mode Exit fullscreen mode

Note: the solution above won't work until you enable less secure app access in Google account settings.

Now, let's look at the more secure solution.

Step 1: Creating a Google Project

Visit Google Developer Console to create a project. A project is needed so that we can create the necessary API credentials.

Once in the console, click the dropdown in the top left corner.

Project Dropdown

After the create project window loads, click New Project.

Project Window

Enter in the project name and click create.

3

Step 2: Creating OAuth 2.0 API Credentials

To get the client secret and client id, we need to create OAuth credentials. A client id identifies our app to Google's OAuth servers so that we can securely send emails from Nodemailer.

Start by selecting credentials in the sidebar on the left. Once selected, the following screen should appear:

4

After clicking create credentials, a dropdown will appear. In the dropdown, select OAuth client ID.

5

Before proceeding, we need to configure the consent screen. The consent screen configuration is important when an application offers Google Sign In. Nevertheless, it must be completed so we can create a client id and secret.

Click configure consent screen.

6

Select external for the User Type and then click create.

7

After the multi-step form appears, fill out the required fields for each step.

Alt Text

Once on the last step, click back to dashboard.

8

Go back to the Create OAuth client ID screen (page with the configure consent screen button). If the consent screen has been configured successfully, an application type dropdown should appear. Select Web application and fill in the required field(s).

9

In the Authorized redirect URIs section, make sure to add https://developers.google.com/oauthplayground.

Now click create!

9.1

Copy the client ID and client secret shown on the screen and save it for later.

9.2

Step 3: OAuth 2.0 Playground

We also need a refresh token and access token which can be generated from the client id and secret.

Start by visiting https://developers.google.com/oauthplayground.
Once on the page, click the gear icon and check the Use your own OAuth credentials box. Then paste in the client id and secret from before.

9.3

On the left, under the Select & authorize APIs section, find Gmail API v1 and select https://mail.google.com/. Alternately, you can also type https://mail.google.com/ into the Input your own scopes field.

Now click Authorize APIs.

9.4

If the following pages appear, click allow so that Google OAuth 2.0 Playground has access to your Google account.

9.41

After being redirected back to the OAuth 2.0 Playground,
click the Exchange authorization code for tokens button under the Exchange authorization code for tokens section.

Once the refresh and access token is generated, copy the refresh token and save it for later.

9.5

Step 4: Writing Code

Now that we have the client id, client secret, and refresh token, we can now use them to send emails!

Start by making a new folder for the application and cd into the folder.



mkdir sendEmails
cd sendEmails


Enter fullscreen mode Exit fullscreen mode

To initialize the app as a node project, run npm init.

Next, let's install the npm packages.



//Note: dotenv is a dev dependency
npm i nodemailer googleapis && npm i dotenv --save-dev


Enter fullscreen mode Exit fullscreen mode

googleapis

  • library for using Google APIs
  • Will be used to dynamically generate access token

dotenv

  • library for using environment variables
  • Will be used to avoid having API keys in our code

Like with any NPM packages, we start by requiring the packages. So, create an index.js file and add the following:



const nodemailer = require("nodemailer");
const { google } = require("googleapis");
const OAuth2 = google.auth.OAuth2;


Enter fullscreen mode Exit fullscreen mode

Environment Variables Setup

Typically when using sensitive info in code (e.g. API keys), the best practice is to use environment variables.

Create a .env file in the root directory of the project and add the following:



EMAIL=YOUR_GOOGLE_EMAIL_HERE
REFRESH_TOKEN=PASTE_REFRESH_TOKEN_HERE
CLIENT_SECRET=PASTE_CLIENT_SECRET_HERE
CLIENT_ID=PASTE_CLIENT_ID_HERE


Enter fullscreen mode Exit fullscreen mode

Now, we need to require and call the config method before requiring all the packages:



require('dotenv').config();
const nodemailer = require("nodemailer");
const { google } = require("googleapis");
const OAuth2 = google.auth.OAuth2;


Enter fullscreen mode Exit fullscreen mode

process.env now has the keys and values defined in the .env file. For example, we can access client id via process.env.CLIENT_ID

Creating a transporter

We first need to create an OAuth client with all of our info from before (client ID, client secret, and the OAuth Playground URL). The OAuth client will allow us to dynamically create an access token from a refresh token.

“But wait, why can't we just use the access token from the OAuth Playground? Or why are we creating the access token dynamically?”

Well, if you noticed earlier, there was a message indicating the access token would expire after 3582 seconds.

The following code creates the OAuth client and provides it with the refresh token:



const oauth2Client = new OAuth2(
    process.env.CLIENT_ID,
    process.env.CLIENT_SECRET,
    "https://developers.google.com/oauthplayground"
);

oauth2Client.setCredentials({
    refresh_token: process.env.REFRESH_TOKEN
});


Enter fullscreen mode Exit fullscreen mode

Since getting the access token through the OAuth client is an asynchronous process, we need to wrap the above in an async function.



const createTransporter = async () => {
  const oauth2Client = new OAuth2(
    process.env.CLIENT_ID,
    process.env.CLIENT_SECRET,
    "https://developers.google.com/oauthplayground"
  );

  oauth2Client.setCredentials({
    refresh_token: process.env.REFRESH_TOKEN
  });
};


Enter fullscreen mode Exit fullscreen mode

Now, we can get the access token by calling the getAccessToken method.



const accessToken = await new Promise((resolve, reject) => {
  oauth2Client.getAccessToken((err, token) => {
    if (err) {
      reject("Failed to create access token :(");
    }
    resolve(token);
  });
});


Enter fullscreen mode Exit fullscreen mode

You might be wondering, why are we wrapping the getAccessToken method call in a promise? This is because getAccessToken requires a callback and does not support using async await. Thus, we can either wrap it in a promise or create the transporter inside the callback. I prefer the former as it is more readable.

Now for the main part, creating the transporter object itself. To create it, we pass some configurations to the createTransport method.



const transporter = nodemailer.createTransport({
  service: "gmail",
  auth: {
    type: "OAuth2",
    user: process.env.EMAIL,
    accessToken,
    clientId: process.env.CLIENT_ID,
    clientSecret: process.env.CLIENT_SECRET,
    refreshToken: process.env.REFRESH_TOKEN
  }
});


Enter fullscreen mode Exit fullscreen mode

Note: If you receive an "unauthorized client", try adding the following to the JS object above.



tls: {
  rejectUnauthorized: false
}


Enter fullscreen mode Exit fullscreen mode

After the transporter is created, the completed createTransporter function should look like this:



const createTransporter = async () => {
  const oauth2Client = new OAuth2(
    process.env.CLIENT_ID,
    process.env.CLIENT_SECRET,
    "https://developers.google.com/oauthplayground"
  );

  oauth2Client.setCredentials({
    refresh_token: process.env.REFRESH_TOKEN
  });

  const accessToken = await new Promise((resolve, reject) => {
    oauth2Client.getAccessToken((err, token) => {
      if (err) {
        reject();
      }
      resolve(token);
    });
  });

  const transporter = nodemailer.createTransport({
    service: "gmail",
    auth: {
      type: "OAuth2",
      user: process.env.EMAIL,
      accessToken,
      clientId: process.env.CLIENT_ID,
      clientSecret: process.env.CLIENT_SECRET,
      refreshToken: process.env.REFRESH_TOKEN
    }
  });

  return transporter;
};


Enter fullscreen mode Exit fullscreen mode

Notice we are returning the transporter instead of writing the code to send an email. We will create another function for sending the email for the sake of code readability and separations of concerns.

Let's now create the sendEmail function. This function calls the createTransporter function and then the sendMail method that exists on the transporter.



//emailOptions - who sends what to whom
const sendEmail = async (emailOptions) => {
  let emailTransporter = await createTransporter();
  await emailTransporter.sendMail(emailOptions);
};


Enter fullscreen mode Exit fullscreen mode

All that is left now is to send the email by calling the sendEmail function:



sendEmail({
  subject: "Test",
  text: "I am sending an email from nodemailer!",
  to: "put_email_of_the_recipient",
  from: process.env.EMAIL
});


Enter fullscreen mode Exit fullscreen mode

The complete list of the email options can be found at https://nodemailer.com/message/.

Run node index.js from the terminal/command line and Voila! Here is the email we sent from the application!

Alt Text

For reference, here is the completed index.js file:



require("dotenv").config();
const nodemailer = require("nodemailer");
const { google } = require("googleapis");
const OAuth2 = google.auth.OAuth2;

const createTransporter = async () => {
  const oauth2Client = new OAuth2(
    process.env.CLIENT_ID,
    process.env.CLIENT_SECRET,
    "https://developers.google.com/oauthplayground"
  );

  oauth2Client.setCredentials({
    refresh_token: process.env.REFRESH_TOKEN
  });

  const accessToken = await new Promise((resolve, reject) => {
    oauth2Client.getAccessToken((err, token) => {
      if (err) {
        reject("Failed to create access token :(");
      }
      resolve(token);
    });
  });

  const transporter = nodemailer.createTransport({
    service: "gmail",
    auth: {
      type: "OAuth2",
      user: process.env.EMAIL,
      accessToken,
      clientId: process.env.CLIENT_ID,
      clientSecret: process.env.CLIENT_SECRET,
      refreshToken: process.env.REFRESH_TOKEN
    }
  });

  return transporter;
};

const sendEmail = async (emailOptions) => {
  let emailTransporter = await createTransporter();
  await emailTransporter.sendMail(emailOptions);
};

sendEmail({
  subject: "Test",
  text: "I am sending an email from nodemailer!",
  to: "put_email_of_the_recipient",
  from: process.env.EMAIL
});


Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
chandrapantachhetri
Chandra Panta Chhetri

Posted on December 16, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related