DEV Community

Cover image for Sending Emails Securely Using Node.js, Nodemailer, SMTP, Gmail, and OAuth2
Chandra Panta Chhetri
Chandra Panta Chhetri

Posted on • Updated on

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

Discussion (11)

Collapse
kotwani2883 profile image
Palak Kotwani

I am getting this error . UnhandledPromiseRejectionWarning: Failed to create access token :(
(Use node --trace-warnings ... to show where the warning was created)
(node:7900) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag --unhandled-rejections=strict (see nodejs.org/api/cli.html#cli_unhand...). (rejection id: 1)
(node:7900) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise

Collapse
nokha_debbarma profile image
Nokha Debbarma

As sendEMail is a async function try add try-catch block inside sendEmail function.

Collapse
typicoul profile image
Luteya Coulston

I'm getting the same, did you find a solution?

Collapse
nokha_debbarma profile image
Nokha Debbarma • Edited

As sendEMail is a async function try adding try-catch block inside sendEmail function.

Collapse
nuzumpat profile image
Pat Nuzum

I am getting the following error:
Error: Invalid login: 535-5.7.8 Username and Password not accepted.

It works correct when I run your program in a standalone mode, but when I am your logic to my project, I get the above error.

Collapse
smozam profile image
SMoZam

Hey,
Thanks for this post.
Refresh token expires in one week for me is it normal ?
Thanks

Collapse
shmoji profile image
Joshua T Jackson

What's the point of using Nodemailer when you can send emails using just the Gmail API?

Collapse
alohe profile image
Ałohe

Its easier to implement i guess

Collapse
medeirosdev profile image
Rafael Medeiros

Thanks for the post bro, it helped me a lot

Collapse
nokha_debbarma profile image
Nokha Debbarma

issue!

google always override from:

with authenticated email address even though I have provided with other email address with gmail domain.

could you please help me out?
Collapse
raulbarriga profile image
raulbarriga

So if I want to use nodemailer for production deployment, the googleapis package will allow for the token to never expire?