Automating email reminders with scheduled functions

avatar
Simon MacDonald
April 13, 2022

migrating Photo by Brett Jordan on Unsplash

Are there any repetitive tasks in your life that would benefit from automation? I run a weekly ice hockey game, and even though it is at the same place and time every week, the dummies that I play with still need a weekly reminder email. So I decided to create a scheduled function to send out that weekly reminder for me.

Provisioning the new scheduled function

The app.arc file in the root of your project is where you define all of your app’s infrastructure as code. Add an entry to the @scheduled section to provision a new scheduled function.

@scheduled
weekly-reminder cron(0 8 ? * WED *)

This will schedule a job to invoke the function every Wednesday at 8 am. You can also schedule functions to be invoked at a rate of your choosing (more information on setting cron and rate).

Then we run arc init to create the weekly reminder function at src/scheduled/weekly-reminder/index.js. The source code of the function is pretty basic:

// src/scheduled/weekly-reminder/index.js
exports.handler = async function scheduled (event) {
 console.log(JSON.stringify(event, null, 2))
 return
}

Install dependencies

Since this function will run 52 times a year, I’m going to bypass setting up AWS SES and instead use my Gmail account and nodemailer to send the weekly emails.

npm install google-auth-library nodemailer

We’ll use the google-auth-library to get authenticated with my Gmail account and nodemailer to send our email.

Note: when dealing with Google APIs in cloud functions, look for smaller dependencies than the 100 MB googleapis package.

Then we need to update our handler to require these new packages:

+ const nodemailer = require('nodemailer')
+ const { OAuth2Client } = require('google-auth-library')

exports.handler = async function scheduled(event) {
 console.log(JSON.stringify(event, null, 2))
 return
}

Authenticating with Google

First, follow these steps to create OAuth2 credentials for the scheduled function. Once you complete these steps, you will have a client ID and secret you can use to authenticate with Google. Next, you will need to generate a refresh token from the OAuth Playground.

Step 1: Enter credentials

  1. Open the Google OAuth Playground
  2. Fill in your Client ID and Client Secret
  3. And click Close.

oauth playground 1

Step 2: Authorize your app to access your Gmail account

  1. Select Gmail API in Side Panel
  2. Click Authorize APIs

oauth playground 2

Step 3: Get your refresh token

  1. Exchange authorization code for tokens
  2. Copy Refresh token

oauth playground 3

Woof! That is a bit of a pain but don’t worry; you won’t have to do it again anytime soon if you store your refresh token in a safe place. Also, I’m not writing this blog post to remember how to do this next time. 😅

Sending Emails

Let’s get back to some code and away from ClickOps.

Obtain an authorization token

We need to get an authorization token before sending an email via our Gmail account. Finally, all the pain of getting a refresh token pays off. First, we’ll create a new OAuth 2.0 Client using our client ID and secret. Then we’ll update the client by setting refresh token as our credentials. Finally, we can get the request headers and parse the Authorization header to return our access token.

 const oauth2Client = new OAuth2Client(
   clientId,
   clientSecret,
   'https://developers.google.com/oauthplayground'
 )

 oauth2Client.setCredentials({ refresh_token })

 const { Authorization } = await oauth2Client.getRequestHeaders()
 const accessToken =
   Authorization?.split(' ')[0] === 'Bearer'
     ? Authorization.split(' ')[1]
     : null

Send an email

After all this boilerplate code, we can finally send an email. We’ll use the nodemailer package to create an SMTP client that authenticates with Gmail using the previously retrieved credentials. Then we send our reminder email out to everyone and close the connection to Gmail.

 const smtpTransport = nodemailer.createTransport({
   service: 'gmail',
   auth: {
     type: 'OAuth2',
     user: mailUser,
     clientId,
     clientSecret,
     refreshToken: refresh_token,
     accessToken: accessToken
   }
 })

 const sendResponse = await smtpTransport.sendMail({
   from: mailUser,
   // comma separated list of email addresses
   to: 'test@begin.com, test2@begin.com',
   subject: 'Hockey Reminder',
   html: `<b>Don't forget we have hockey on Friday night!`,
   generateTextFromHTML: true
 })
 await smtpTransport.close()
Full source code of the weekly reminder function
const nodemailer = require('nodemailer')
const { OAuth2Client } = require('google-auth-library')

exports.handler = async function scheduled(event) {
  const clientId = process.env.GOOGLE_CLIENT_ID
  const clientSecret = process.env.GOOGLE_CLIENT_SECRET
  const refresh_token = process.env.GOOGLE_REFRESH_TOKEN
  const mailUser = process.env.MAIL_USER

  const oauth2Client = new OAuth2Client(
    clientId,
    clientSecret,
    'https://developers.google.com/oauthplayground'
  )

  oauth2Client.setCredentials({ refresh_token })

  const { Authorization } = await oauth2Client.getRequestHeaders()
  const accessToken =
    Authorization?.split(' ')[0] === 'Bearer'
      ? Authorization.split(' ')[1]
      : null

  const smtpTransport = nodemailer.createTransport({
    service: 'gmail',
    auth: {
      type: 'OAuth2',
      user: mailUser,
      clientId,
      clientSecret,
      refreshToken: refresh_token,
      accessToken: accessToken
    }
  })

  const sendResponse = await smtpTransport.sendMail({
    from: mailUser,
    // comma separated list of email addresses
    to: 'test@begin.com, test2@begin.com',
    subject: 'Hockey Reminder',
    html: `<b>Don't forget we have hockey on Friday night!</b>`,
    generateTextFromHTML: true
  })
  await smtpTransport.close()

  return
}

In closing

Well that was a fun diversion and now it has me thinking what other tasks I can automate by using scheduled functions. Let me know on Twitter what use cases you have for scheduled functions.