Skip to Content

Polling multiple gmail inboxes

With every new client, I get a new gmail account to monitor. Yes, I can configure gmail to monitor multiple accounts. No, I don’t want to do that. I log into client email accounts in a different browser. Personal preference.

So, I’d like a way to monitor multiple gmail accounts and get a visual clue when I have unread mail. This post describes such a script. It monitors multiple gmail accounts, generating a short string consisting of an account id for each account with unread messages. I then use a modified version of i3status to display that inbox id string.

Purpose

The goal of this script (gbiff.rb) and changes to i3status is to provide visual feedback in the i3 status bar when one or several gmail account inboxes have unread messages.

The gbiff.rb script polls multiple gmail account inboxes, generating a string indicating which accounts have unread messages. This (short) string is written to a file where it can subsequently be displayed i3status. This acticle describes the gbiff.rb script.

The i3status changes were submitted to the i3 upstream through pull request https://github.com/i3/i3status/pull/313. While the pull request has been rejected, I’ve found it worth while maintaining a modified version. The i3 team suggests i3-dstatus as an alternative for users that want an enhanced status line. That probably works, too. The rest of this post is about generating the status file, not integration into i3status.

Overview

The script embedded below uses Google APIs to request the unread message count associated with multiple gmail accounts. In order to use the Google APIs, you must obtain a token that can be used to authenticate the script and authorize individual account access. This is the first step of three:

  • allocate token used to identify the application (our script)
  • use the token and a browser to authenticate each gmail account
  • access inbox message counts

The first two steps are interactive. They are carried out in preparing and executing the script the first time. The third step, subsequent invocations of the script do not require interaction.

So, we’ll start by describing how to obtain the Google API token. Then we’ll provide the script and describe how it is configured (with a text editor). Then we’ll describe the first run of the script; this is when account access is authorized. And we’ll wrap up with a description of how to automatically run the script at login using systemd.

Google API token needed

Before running the script, you need to generate a Google API token. This is only done once, before running the script for the first time.

Application tokens are obtained through The Google API Credentials page. From the Create credentials drop down, select OAuth client ID. On the Create OAuth client ID page, select application type Other. Then enter a name to represent the application. This string will be presented to you during the authentication process. Something like GBiff would make a good service name. Pick something that appeals to you.

Clicking Create will take you back the the Credentials page and post a dialog with the client ID and client secret associated with the newly defined service. You do not need to save ID and secret from the dialog; both can be downloaded. So simply press Ok to dismiss the dialog.

Click the download button at the far right of the line identifying the service you just created. Copy the resulting client_secret*.json file to client_id.json in the same directory containing gbiff.rb.

gbiff.rb

The gbiff.rb script that interrogates gmail services is based on Google’s API Quickstart demo program. The current version of the script is below. As is, this is a single use script; customization involves editing the script. Here is a copy of the script, customization suggestions follow.

Script source

#!/usr/bin/env ruby
# Copyright 2018 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# [START gmail_quickstart]
require 'google/apis/gmail_v1'
require 'googleauth'
require 'googleauth/stores/file_token_store'
require 'fileutils'

UserAccounts = [['gmail', 'P'], ['enmotus', 'E'], ['gigiaio', 'G']]
FileName = "/run/user/#{Process.uid}/gbiff.status"
IdleInterval = 5 * 60

## Adapted from
# https://github.com/gsuitedevs/ruby-samples/blob/master/gmail/quickstart/quickstart.rb

## create Google API token/client_id via
# https://console.developers.google.com/apis/credentials
## Explore, test APIs via
# https://developers.google.com/gmail/api/v1/reference/users/threads/list, et.al.

OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'.freeze
APPLICATION_NAME = 'Gmail API Ruby GBiff'.freeze
CREDENTIALS_PATH = 'client_id.json'.freeze
TOKEN_PATH = 'token.yaml'.freeze
SCOPE = Google::Apis::GmailV1::AUTH_GMAIL_READONLY

##
# Ensure valid credentials, either by restoring from the saved credentials
# files or intitiating an OAuth2 authorization. If authorization is required,
# the user's default browser will be launched to approve the request.
#
# @return [Google::Auth::UserRefreshCredentials] OAuth2 credentials
def authorize(account_name)
  client_id = Google::Auth::ClientId.from_file(CREDENTIALS_PATH)
  token_store = Google::Auth::Stores::FileTokenStore.new(file: TOKEN_PATH)
  authorizer = Google::Auth::UserAuthorizer.new(client_id, SCOPE, token_store)
  user_id = account_name
  credentials = authorizer.get_credentials(user_id)
  if credentials.nil?
    url = authorizer.get_authorization_url(base_url: OOB_URI)
    puts 'Open the following URL in the browser and enter the ' \
	 "resulting code after authorization:\n" + url
    code = gets
    credentials = authorizer.get_and_store_credentials_from_code(
      user_id: user_id, code: code, base_url: OOB_URI
    )
  end
  credentials
end

def check_mail(account_name, flag)
  service = Google::Apis::GmailV1::GmailService.new
  service.client_options.application_name = APPLICATION_NAME
  service.authorization = authorize(account_name)

  user_id = 'me'
  begin
    result = service.list_user_threads(user_id,
	     q: 'in:inbox -category:{social promotions updates forums} label:unread')
  rescue
    return ''
  end
  return result.result_size_estimate > 0 ? flag : ''
end

while true
  s = ''
  UserAccounts.each { |k, v|
    s = s + check_mail(k, v)
  }
  if s.length == 0 && File.exists?(FileName) && File.size(FileName) != 0
    File.unlink(FileName)
  else
    File.write(FileName, s)
  end
  sleep IdleInterval
end

Customization

Control variables

There are three variables to be customized:

  • UserAccounts:: Array of pairs. The first element of a pair is a symbolic name representing an account. The element item of the pair is the character generated in the status string if there are unread messages in the inbox.
  • FileName:: Name of the file shared with the i3status module that displays the string.
  • IdleInterval:: Interval between polling cycles

For example, from above:

UserAccounts = [['gmail', 'P'], ['enmotus', 'E'], ['gigiaio', 'G']]
FileName = "/run/user/#{Process.uid}/gbiff.status"
IdleInterval = 5 * 60

Each pair in the UserAccounts vector represents one email account. The name is arbitrary, it should be something suggestive of the email account it represents. The status string, similarly, is arbitrary. It is the string that gets displayed in the i3status bar. With the above definition, if there is email pending in the ‘gmail’ and ‘enmotus’ accounts, the string PE will be displayed.

Inbox query

Each configured mailbox is polled with the following query:

result = service.list_user_threads(user_id,
	 q: 'in:inbox -category:{social promotions updates forums} label:unread')

That is a request for unread messages in the account INBOX, ignoring mail that was filtered into the social, promotions, updates, or forums categories/tabs.

Authentication interface

In addition to the customizations, there are two other constants worth noting. These constants provide a link to Google’s authentication mechanism.

  • CREDENTIALS_PATH:: Contains the authentication tokens generated by and downloaded from the Google API Credentials page.
  • TOKEN_PATH:: Contains one line per gmail account, indexed by the first element of the pairs in the UserAccounts constant. This contains authentication state for each of the email accounts.

First Time Use - Authorizing Email Account Access

The first time gbiff.rb is run, it will attempt to connect each configured email account. For each it will either automatically open an authentication URL or print the URL on the terminal, asking that the URL be opened in a browser. Such a message looks something like:

Open the following URL in the browser and enter the resulting code after authorization: https://accounts.google.com/o/oauth2/auth?access%5Ftype=offline&approval%5Fprompt=force&client%5Fid=744055231739-1234567ai3fep4veuotn357m6e35vnub.apps.googleusercontent.com&include%5Fgranted%5Fscopes=true&redirect%5Furi=urn:ietf:wg:oauth:2.0:oob&response%5Ftype=code&scope=https://www.googleapis.com/auth/gmail.readonly

Pasting that newly displayed URL into a browser will take you to an authentication page asking that you authorize the gbiff.rb script access to the email account. Note: the sign in page will display the name of the service (GBiff suggested above) when the service was defined through The Google API Credentials page. Once authorized, the browser will display a “Sign in” page that contains a token representing access to that email account. That line looks something like:

4/cwBMSAZlhCy5xeE3DllcqBJsQDkyuCx4VoaGH3Fh26ZcAl_PeOoBI4E

copy that string and paste into the terminal running gbiff.rb. Press return if the paste operation does not submit the string. gbiff.rb will then advance to the next account.

This initial authorization step processes accounts in the same order specified in the UserAccounts constant. You may need to keep track of that order if your browser has been signed into more than one account. In that case, the authorization page delivers an account selection page; select the account based on the order defined in UserAccounts.

After all accounts have been authorized, gbiff.rb will start polling each email account. You can leave that running in the foreground, or run it in the background during login.

Start gbiff.rb during login with systemd

The much maligned systemd can be configured to start the script at login by defining a systemd user service. Define the user service as follows:

[Unit]
Description=Poll gmail mailboxes
# The name of the timer that's going to call this service file.

[Service]
Type=simple
WorkingDirectory=%h/src/gmail
ExecStart=%h/src/gmail/gbiff.rb

[Install]
WantedBy=basic.target

Then:

  • start service: systemctl start --user gbiff
  • service status: systemctl status --user gbiff
  • stop service: systemctl stop --user gbiff

In the future, gbiff.rb should automatically run whenever you log in.

Comment here.