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 thei3status
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 theUserAccounts
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.