Getting Started With Gmail API

June 28, 2018

Quick Start

Gmail's API is robust and useful. If you're building an application that interfaces with Gmail you might find these code snippets useful.

To access Gmail's API, you'll need to obtain credentials. This walk-through might come in handy with that.

Once you have credentials, the process is roughly as follows:

  • Install Google's official npm package: npm i googleapis --save
  • Create an instance of OAuth2: const auth = new require('googleapis').auth.OAuth2(client_id, client_secret, callback_urls[0]).
  • Obtain a url from auth.generateAuthUrl. Redirect the user to the aforementioned url. The user will land on a google-hosted page that prompts them to login with their Gmail account. The login page also alerts the user to the privileges that the app is requesting (see scopes below).
  • After the user has consented, Google redirects them back to a page you specify. The code sample below assumes that you've whitelisted http://localhost:3000/callback as the callback url.
  • Google appends a query string to the callback url, e.g., http://localhost:3000/callback?code=xxxx. This code is needed to request an access token with auth.getToken. 
const http = require('http')
const url = require('url')
const fetch = require('node-fetch')
const google = require('googleapis')
const OAuth2 = google.auth.OAuth2
const gmail = google.gmail({ version: 'v1' })
const credentials = require('./credentials.json')
const auth = new OAuth2(
   credentials.client_id,
   credentials.client_secret,
   credentials.redirect_uris[0]
)
const scopes = [
  'https://mail.google.com/',
  'https://www.googleapis.com/auth/gmail.compose',
  'https://www.googleapis.com/auth/gmail.modify',
  'https://www.googleapis.com/auth/gmail.send'
]
// get access tokens from the "code" query parameter provided by Google
const getToken = (auth, code) => new Promise((resolve, reject) => {
  auth.getToken(code, (err, tokens) => {
    resolve(tokens)
  })
}
const createFetch = (token, { endpoint }) => {
  return fetch('https://www.googleapis.com' + endpoint, {
    method: 'GET',
    headers: {
      Authorization: `Bearer ${token}`,
      'User-Agent': 'google-api-nodejs-client/0.10.0',
      host: 'www.googleapis.com',
      accept: 'application/json'
    }
  }).then(resp => resp.json())
}
const handler = async (req, res, next) => {
  if (req.url.startsWith('/callback')) {
    // 2. After the user grants privileges to your application,
    // Google will redirect them to an endpoint you specify. 
    // Next, use the code query parameter to request an access_token. 
    // This token can be be reused for up to a day. 
    const { code } = url.parse(req.url, true)
    const tokens = await getToken(auth, code)
    
    // 3. Now that we have tokens, we can fetch profile information
    // associated with the Gmail account.
    const endpoint = '/gmail/v1/users/me/profile'
    const profile = await createFetch(tokens.access_token, { endpoint }) 
  } else {
    // 1. Redirect user to url provided by generateAuthUrl
    const url = await auth.generateAuthUrl({
      access_type: 'offline',
      scope: scopes
    })
    res.writeHead(302, { 'Location': url })
  }
}
const server = http.createServer(handler)
server.listen(process.env.PORT || 3000)

Fetching A List of Emails

So far we haven't done anything fancy. Now that we have an access_token we can fetch a list of emails:

// pseudocode below
let BASE_URI = 'https://www.googleapis.com'
let messagesEndpoint = '/gmail/v1/users/me/messages'
fetch(BASE_URI + messagesEndpoint, {
    method: 'GET',
    headers: {
      Authorization: `Bearer ${token}`,
      'User-Agent': 'google-api-nodejs-client/0.10.0',
      host: 'www.googleapis.com',
      accept: 'application/json'
    }
  }).then(resp => resp.json())
    .then(resp => {
      console.log(resp)
      // { messages: ['1641febef6e6b8a1', '16420ce2f95226e5'] }
    })

As you might expect, the request to /gmail/v1/users/me/messages returns a list of message IDs corresponding to emails.

We can then fetch the actual content of emails and metadata as follows:

let BASE_URI = 'https://www.googleapis.com'
let messageEndpoint = `/gmail/v1/users/me/messages/${id}?format=full` // id comes from the previous code snippet
fetch(BASE_URI + messageEndpoint, {
    method: 'GET',
    headers: {
      Authorization: `Bearer ${token}`,
      'User-Agent': 'google-api-nodejs-client/0.10.0',
      host: 'www.googleapis.com',
      accept: 'application/json'
    }
  }).then(resp => resp.json())
    .then(resp => {
      console.log(resp)
      // {
      //   "id": "...", 
      //   "threadId": "...",
      //   "snippet": "", 
      //   "headers": [...],
      //   "payload": {
      //     "parts": [{
      //        "partId": "0",
      //        "mimeType": "text/plain",
      //        "filename": "",
      //        "headers": [
      //          {
      //            "name": "Content-Type",
      //            "value": "text/plain; charset=utf-8"
      //          },
      //          {
      //            "name": "Content-Transfer-Encoding",
      //            "value": "quoted-printable"
      //          }
      //        ],
      //        "body": {
      //          "size": 747,
      //          "data": "SGV5IHdoYXQncyBjb29raW4nIGdvb2QgbG9va2luJz8="
      //        }
      //      }]
      //   } 
      // }
    })
    

Note that the response shown in the above snippet is abbreviated. The Gmail API also includes other data like the timestamp and such.

The aspect of the response that requires transformation is payload.parts[n].body.data. The body is base64-encoded binary data.

Decoding Raw Base64-Encoded Binary Messages

If we make the same request as the code snippet above but instead specify the format "raw" you'll receive a response that looks vaguely like the following. The format is specified as a query parameter, e.g., https://www.googleapis.com/gmail/v1/users/me/messages/${id}?format=full.

{  
  id: '16441d1cdc8c73f9',
  threadId: '16441d1cdc8c73f9',
  labelIds: [ 'UNREAD', 'CATEGORY_PERSONAL', 'INBOX' ],
  snippet: 'Hey what\'s up?'',
  historyId: '9714321',
  internalDate: '1530112624000',
  sizeEstimate: 13035,
  raw: 'SGV5IHdoYXQncyBjb29raW4nIGdvb2QgbG9va2luJz8='
 }
 

If you're after the actual content of emails, it is easier to work with the raw format rather than full, because the full format segments the payload.

On the client side, we can decode the raw body with atob:

let text = window.atob('SGV5IHdoYXQncyBjb29raW4nIGdvb2QgbG9va2luJz8=')
console.log(text)
// what's cookin' good lookin'

On the server, you'll need to use a third-party package that duplicates this functionality like the aptly named btoa.

let text = require('bota')('SGV5IHdoYXQncyBjb29raW4nIGdvb2QgbG9va2luJz8=')