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

Top comments (30)

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.

Thread Thread
 
sailee14 profile image
sailee14

After adding try catch block getting "Error: No refresh token or refresh handler callback is set." this error. Any way to make it right?

Collapse
 
bytecodeman profile image
Tony Silvestri • Edited

Hi
I'm consistently getting this error:
"Failed to create access token: Error: invalid_grant" after having an app work fine for a week or so. After this period this error pops up.

I go to OAuth playground to regenerate the refreshToken. App works fine only to fail sometime later.

I pretty much followed (heck copied!! :-)) the code offered in this article.

Any ideas? Much appreciation in advance!

Collapse
 
abeertech01 profile image
Abdul Ahad Abeer • Edited

Add the email you are using to send email here in OAuth consent screen page as a test user

Collapse
 
bytecodeman profile image
Tony Silvestri

Hi. It's there already. But Thx for replying!!!

Thread Thread
 
abeertech01 profile image
Abdul Ahad Abeer

I will write a blog on it. once writing is completed, I will leave a link here

Thread Thread
 
bytecodeman profile image
Tony Silvestri

Thank you for your efforts! They are much appreciated.
Tony

Collapse
 
poziminski profile image
Pawel Oz

I actually dont understand what happens next, in production. I guess oauthplayground does not stay there and we need to implement our authorization endpoint (redirect_uri) which is not much covered in the article. I think we should not use oauthplayground refresh token in production?

Collapse
 
springboot20 profile image
springboot20

please am having a problem with sending dynamic email using nodemailer express handlebars here is my code
import nodemailer from "nodemailer"
import {
google
} from "googleapis"
import fs from "fs"
import path from "path"
import url from "url"
import expressHandlerbars from "nodemailer-express-handlebars"

const filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(
filename)

const OAuth2 = google.auth.OAuth2
const createTransporter = async () => {
const OAuth2Client = new OAuth2(
process.env.CLIENT_ID,
process.env.CLIENT_SECRET,
"developer.google.com/oauthplayground"
)

OAuth2Client.setCredentials({
refresh_token: process.env.REFRESH_TOKEN
})

const accessToken = await new Promise((resolve, reject)=> {
OAuth2Client.getAccessToken((error, token => {
if (error) {
console.log(error)
reject("Failed to get accessToken")
}
resolve(token)
}))
})

return await nodemailer.createTransport({
service: "gmail",
auth: {
type: "OAuth2",
accessToken,
user: process.env.EMAIL,
refreshToken: process.env.REFRESH_TOKEN,
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
}, tsl: {
rejectUnauthorized: false
}
})
}

const sendMail = async (email, subject, payload, template)=> {
const options = {
from: process.env.EMAIL,
to: email,
subject:subject,
template: template,
context: payload,
}

try {
const transporter = await createTransporter()

transporter.use("compile", expressHandlebars({
  viewEngine: {
    extName: '.hbs',
    partialsDir: 'views/partials',
    layoutsDir: 'views/layouts',
    defaultLayout: 'layout',
  },
  extName: '.hbs',
  viewPath: 'views',
}))

await transporter.sendMail(options) 
Enter fullscreen mode Exit fullscreen mode

} catch(error) {
console.log(${error})
}
}

export default sendMail
so I am receiving a reference error how can I fix it and do it in the right way

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
 
nandhamurali profile image
Nandhakumar

Were you able to fix the issue?

Collapse
 
jasonwarner profile image
Jason

Thank you so much, Chandra!

I was able to follow your directions for my Next.js application and got it to work!

This OAUTH stuff is definitely not intuitive, though.

For anyone struggling with implementing this, here is the GitHub link for my little project: github.com/jason-warner/hustlin-la...

And a few tips:

  • be extra careful to wrap your requests in try/catch
  • do not append your ENV variables with a semicolon ( ; )
  • use console logs to confirm every single one of your assumptions (you have access to environment variables, you have a an access token before you try to send mail, etc. and log any possible errors)
Collapse
 
benmneb profile image
benmneb

This is much easier to do now! stackoverflow.com/a/45479968/12104850

Collapse
 
smozam profile image
SMoZam

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

Collapse
 
lefis profile image
lefterispour

When I'm clicking to authorize API's and selecting my google account I'm getting this error: "appname has not completed the Google verification process. The app is currently being tested, and can only be accessed by developer-approved testers. If you think you should have access, contact the developer.
If you are a developer of appname, see error details.
Error 403: access_denied.

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
Alohe

Its easier to implement i guess