Email with Gmail, NodeJS, and OAuth2

If you look around for examples of how to send an email via Gmail with NodeJS, they generally end up mentioning you should flip the toggle to Allow less secure apps:

Screenshot of Gmail Less secure apps setting page

This doesn’t seem like a good idea – I mean it SAYS “less secure”. I looked around at the documentation, and while Google has tons of documentation, I found it a bit overwhelming. As promised, the NodeJS quickstart is a great place. It shows how to set up a client to authenticate with Google in the “more secure” fashion. I’ll go through that quickstart here, with a couple tweaks to send email.

First things first, install the necessary dependencies:

yarn add google-auth-library googleapis js-base64

Then steal most of the quickstart.js, swapping out enough to send an email. Note that this is my first time ever interacting with the Gmail API, so while this worked to send an email for me, no guarantees…

Pull in all the dependencies:

const fs = require('fs');
const readline = require('readline');
const google = require('googleapis');
const googleAuth = require('google-auth-library');
const Base64 = require('js-base64').Base64;

Choose the appropriate Auth Scopes for what you’re trying to accomplish:

const SCOPES = ['https://mail.google.com/',
  'https://www.googleapis.com/auth/gmail.modify',
  'https://www.googleapis.com/auth/gmail.compose',
  'https://www.googleapis.com/auth/gmail.send'
];

Define where you’re going to store the auth token once you get it:

const TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
  process.env.USERPROFILE) + '/.credentials/';
const TOKEN_PATH = TOKEN_DIR + 'gmail-nodejs-quickstart.json';

First, we’ll want to read the client secret that was created in the manual set up phase.

/**
 * Read the contents of the client secret JSON file
 * 
 * @param {String} filename - name of the file containing the client secrets
 */
function readClientSecret(filename) {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, (err, content) => {
      if (err) {
        return reject('Error loading client secret from ' + filename +
          ' due to ' + err);
      }
      return resolve(content);
    });
  });
}

Then after parsing that JSON file, we’ll want to build the Google’s OAuth2 client, as they’re nice and provide one for us.

/**
 * Create an OAuth2 client with the given credentials
 *
 * @param {Object} credentials The authorization client credentials.
 */
function authorize(credentials) {
  let clientSecret = credentials.installed.client_secret;
  let clientId = credentials.installed.client_id;
  let redirectUrl = credentials.installed.redirect_uris[0];
  let auth = new googleAuth();
  let oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);

  return new Promise((resolve, reject) => {
    // Try reading the existing token
    fs.readFile(TOKEN_PATH, function (err, token) {
      if (err) {
        // If there isn't an existing token, get a new one
        resolve(getNewToken(oauth2Client));
      } else {
        oauth2Client.credentials = JSON.parse(token);
        resolve(oauth2Client);
      }
    });
  });
}

If this is the first time executing the program, or you’ve deleted the cached token, you’ll need to get a new one.

/**
 * Get and store new token after prompting for user authorization, then return
 * authorized OAuth2 client.
 *
 * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
 */
function getNewToken(oauth2Client) {
  let authUrl = oauth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: SCOPES
  });

  console.log('Authorize this app by visiting this url: ', authUrl);

  let readlineInterface = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });

  return new Promise((resolve, reject) => {
    readlineInterface.question('Enter the code from that page here: ',
      (code) => {
        readlineInterface.close();
        oauth2Client.getToken(code, (err, token) => {
          if (err) {
            return reject('Error while trying to retrieve access token', err);
          }

          oauth2Client.credentials = token;
          storeToken(token);
          return resolve(oauth2Client);
        });
      });
  });
}

To avoid having to do this on every call, it makes sense to write it out to the disk.

/**
 * Store token to disk be used in later program executions.
 *
 * @param {Object} token The token to store to disk.
 */
function storeToken(token) {
  try {
    fs.mkdirSync(TOKEN_DIR);
  } catch (err) {
    if (err.code != 'EEXIST') {
      throw err;
    }
  }
  fs.writeFile(TOKEN_PATH, JSON.stringify(token));
  console.log('Token stored to ' + TOKEN_PATH);
}

At this point, our OAuth2 client is authenticated and ready to role! If we’ve set up the Auth Scopes properly, our client should also be authorized to do whatever we want it to do. There are a handful of libraries that make this easier, but for simplicity’s sake we’ll just hand roll an email string.

/**
 * Build an email as an RFC 5322 formatted, Base64 encoded string
 * 
 * @param {String} to email address of the receiver
 * @param {String} from email address of the sender
 * @param {String} subject email subject
 * @param {String} message body of the email message
 */
function createEmail(to, from, subject, message) {
  let email = ["Content-Type: text/plain; charset=\"UTF-8\"\n",
    "MIME-Version: 1.0\n",
    "Content-Transfer-Encoding: 7bit\n",
    "to: ", to, "\n",
    "from: ", from, "\n",
    "subject: ", subject, "\n\n",
    message
  ].join('');

  return Base64.encodeURI(email);
}

Then the actual magic! Using our authenticated client and our formatted email to send the email. I’m not positive on this part, as I didn’t find a specific example that did it exactly as I was expecting (I also didn’t look too hard…)

/**
 * Send Message.
 *
 * @param  {String} userId User's email address. The special value 'me'
 * can be used to indicate the authenticated user.
 * @param  {String} email RFC 5322 formatted, Base64 encoded string.
 * @param {google.auth.OAuth2} oauth2Client The authorized OAuth2 client
 */
function sendMessage(email, oauth2Client) {
  google.oauth2("v2").google.gmail('v1').users.messages.send({
    auth: oauth2Client,
    userId: 'me',
    'resource': {
      'raw': email
    }
  });
}

Then it’s just a matter of stringing everything together. The invocation part of the script:

let to = 'mmonroe@gmail.com';
let from = 'ckent@gmail.com';
let subject = 'Email subject generated with NodeJS';
let message = 'Big long email body that has lots of interesting content';

readClientSecret('client_secret.json')
  .then(clientSecretJson => {
    let clientSecret = JSON.parse(clientSecretJson);
    return authorize(clientSecret);
  }).then(oauth2client => {
    let email = createEmail(to, from, subject, message);
    sendMessage(email, oauth2client);
  }).catch(error => {
    console.error(error);
  });

And that’s all. Executing this the first time prompts for the value shown in the output URL then sends the email, executing it subsequent times just sends the email. Easy enough!

 


const fs = require('fs');
const readline = require('readline');
const google = require('googleapis');
const googleAuth = require('google-auth-library');
const Base64 = require('js-base64').Base64;
const SCOPES = ['https://mail.google.com/',
'https://www.googleapis.com/auth/gmail.modify',
'https://www.googleapis.com/auth/gmail.compose',
'https://www.googleapis.com/auth/gmail.send'
];
const TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
process.env.USERPROFILE) + '/.credentials/';
const TOKEN_PATH = TOKEN_DIR + 'gmail-nodejs-quickstart.json';
/**
* Read the contents of the client secret JSON file
*
* @param {String} filename – name of the file containing the client secrets
*/
function readClientSecret(filename) {
return new Promise((resolve, reject) => {
fs.readFile(filename, (err, content) => {
if (err) {
return reject('Error loading client secret from ' + filename +
' due to ' + err);
}
return resolve(content);
});
});
}
/**
* Create an OAuth2 client with the given credentials
*
* @param {Object} credentials The authorization client credentials.
*/
function authorize(credentials) {
let clientSecret = credentials.installed.client_secret;
let clientId = credentials.installed.client_id;
let redirectUrl = credentials.installed.redirect_uris[0];
let auth = new googleAuth();
let oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl);
return new Promise((resolve, reject) => {
// Try reading the existing token
fs.readFile(TOKEN_PATH, function (err, token) {
if (err) {
// If there isn't an existing token, get a new one
resolve(getNewToken(oauth2Client));
} else {
oauth2Client.credentials = JSON.parse(token);
resolve(oauth2Client);
}
});
});
}
/**
* Get and store new token after prompting for user authorization, then return
* authorized OAuth2 client.
*
* @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
*/
function getNewToken(oauth2Client) {
let authUrl = oauth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES
});
console.log('Authorize this app by visiting this url: ', authUrl);
let readlineInterface = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve, reject) => {
readlineInterface.question('Enter the code from that page here: ',
(code) => {
readlineInterface.close();
oauth2Client.getToken(code, (err, token) => {
if (err) {
return reject('Error while trying to retrieve access token', err);
}
oauth2Client.credentials = token;
storeToken(token);
return resolve(oauth2Client);
});
});
});
}
/**
* Store token to disk be used in later program executions.
*
* @param {Object} token The token to store to disk.
*/
function storeToken(token) {
try {
fs.mkdirSync(TOKEN_DIR);
} catch (err) {
if (err.code != 'EEXIST') {
throw err;
}
}
fs.writeFile(TOKEN_PATH, JSON.stringify(token));
console.log('Token stored to ' + TOKEN_PATH);
}
/**
* Build an email as an RFC 5322 formatted, Base64 encoded string
*
* @param {String} to email address of the receiver
* @param {String} from email address of the sender
* @param {String} subject email subject
* @param {String} message body of the email message
*/
function createEmail(to, from, subject, message) {
let email = ["Content-Type: text/plain; charset=\"UTF-8\"\n",
"MIME-Version: 1.0\n",
"Content-Transfer-Encoding: 7bit\n",
"to: ", to, "\n",
"from: ", from, "\n",
"subject: ", subject, "\n\n",
message
].join('');
return Base64.encodeURI(email);
}
/**
* Send Message.
*
* @param {String} userId User's email address. The special value 'me'
* can be used to indicate the authenticated user.
* @param {String} email RFC 5322 formatted, Base64 encoded string.
* @param {google.auth.OAuth2} oauth2Client The authorized OAuth2 client
*/
function sendMessage(email, oauth2Client) {
let request = google.oauth2("v2").google.gmail('v1').users.messages.send({
auth: oauth2Client,
userId: 'me',
'resource': {
'raw': email
}
});
}
// Do everything!
let to = 'receiver@gmail.com';
let from = 'sender@gmail.com';
let subject = 'Subject the email generated with NodeJS';
let message = 'Big long email body that has lots of interesting content';
readClientSecret('client_secret.json')
.then(clientSecretJson => {
let clientSecret = JSON.parse(clientSecretJson);
return authorize(clientSecret);
}).then(oauth2client => {
let email = createEmail(to, from, subject, message);
sendMessage(email, oauth2client);
}).catch(error => {
console.error(error);
});

view raw

sendEmail.js

hosted with ❤ by GitHub

1 thought on “Email with Gmail, NodeJS, and OAuth2

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s