Sending Emails Securely Using Node.js, Nodemailer, SMTP, Gmail, and OAuth2
Chandra Panta Chhetri
Posted on December 16, 2020
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:
- Creating a transporter (object used to send emails) using either SMTP or some other transport mechanism
- Setting up message options (who sends what to whom)
- 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);
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.
After the create project window loads, click New Project.
Enter in the project name and click create.
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:
After clicking create credentials, a dropdown will appear. In the dropdown, select OAuth client ID.
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.
Select external for the User Type and then click create.
After the multi-step form appears, fill out the required fields for each step.
Once on the last step, click back to dashboard.
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).
In the Authorized redirect URIs section, make sure to add https://developers.google.com/oauthplayground.
Now click create!
Copy the client ID and client secret shown on the screen and save it for later.
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.
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.
If the following pages appear, click allow so that Google OAuth 2.0 Playground has access to your Google account.
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.
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
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
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;
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
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;
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
});
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
});
};
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);
});
});
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
}
});
Note: If you receive an "unauthorized client", try adding the following to the JS object above.
tls: {
rejectUnauthorized: false
}
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;
};
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);
};
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
});
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!
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
});
Posted on December 16, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.