Skip to content

Instantly share code, notes, and snippets.

@brunobraga
Last active June 15, 2023 11:49
Show Gist options
  • Save brunobraga/6076658 to your computer and use it in GitHub Desktop.
Save brunobraga/6076658 to your computer and use it in GitHub Desktop.
A simple Gmail unread count script for i3status or conky bars, taking care of its own time frequency (avoid overhead in simplistic approaches such as i3status). See --help for documentation and usage details.
#!/usr/bin/python
###############################################################################
#
# file: gmail-count
#
# Purpose: generates a string value representing the Gmail unread email count.
#
# Usage: pipe the i3status with this script (see i3status manpage)
# or use conky.
#
###############################################################################
#
# Copyright 2013 Bruno Braga
#
# 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
#
# http://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.
#
###############################################################################
#
# Soft Dependencies:
# * python-gnomekeyring
# (if you do not want to use it, just remove the keyring import
# and place the plain password - read below)
#
# * python-lxml
# (if not available, will try to parse the hard way)
#
###############################################################################
__NAME__ = 'gmail-count'
"""
Defines the name of this script. Used as the service name of keyring store,
if applicable.
"""
__VERSION__ = 0.1
"""
Defines the version of this script.
"""
#
# Built-in Libraries
#
import urllib2
import sys
import os
import datetime
import optparse
import traceback
# #############################
# CONFIGURABLE SETTINGS: BEGIN
# #############################
#
# First time? Add password to keyring
# keyring.set_password('Gmail', 'bruno.braga@gmail.com', '{password}')
#
# Don't want to use keyring (or don't have it)?
# Just remove the keyring import and replace this password line for your
# raw password (do not recommend doing this, but if you choose to do so,
# remember to change the file permissions to 700 to ensure you alone can
# access it).
#
password = ''
#
# A cache file to avoid making repetitive queries (which take long) in same
# frequency as i3 status (usually every 5 seconds)
# The expiration is defined in seconds (2 minutes by default).
#
cacheFile = '/tmp/gmail-count.cache'
cacheExpiresNSec = 120
# #############################
# CONFIGURABLE SETTINGS: END
# #############################
verbose = False
username = ''
useCache = True
def dieGracefully(message):
"""
Exits in error, but outputs a question mark instead of the expected number,
to inform the user something is wrong. Message will only be printed out if
in verbose mode.
"""
if verbose:
print >> sys.stderr, message
else:
print '?'
sys.exit(1)
def setKeyringPassword():
"""
Prompts and stores a user password in Gnome Keyring.
"""
import getpass
kPassword = getpass.getpass(prompt='Type in password for user [%s]: ' % username)
if not kPassword or len(kPassword) == 0:
print 'Must set a valid password'
sys.exit(1)
else:
try:
import keyring
keyring.set_password(__NAME__, username, kPassword)
print "Done."
sys.exit(0)
except Exception, e:
dieGracefully('keyring module not found. See --help for details.')
def parseArgs():
# override the default parser to allow new lines
class Parser(optparse.OptionParser):
def format_epilog(self, formatter):
return self.epilog
global username, password, verbose, cacheExpiresNSec, useCache
parser = Parser(usage="usage: %prog [options]",
description="A simple Gmail unread count script for i3status or conky \
bars, using simple caching (avoid overhead in simplistic approaches such as \
i3status).",
version="%%prog %s" % __VERSION__,
epilog="""
Notes:
This script will attempt to access the account's password in the following manner:
- if --password is informed, will use it
- if not, will see if it has been hard-coded within the script (some people
may prefer that)
- lastly, if none of the above satisfies, it will retrieve it from keyring,
if available. To add your password to the keyring, use option --add-keyring
""")
parser.add_option("-u", "--username", dest="username", action="store",
help="The Gmail username (without the gmail.com). \
If Google Apps account, need to inform the complete email address.")
parser.add_option("-p", "--password", dest="password", action="store",
help="The plain text password associated with the \
username. If not informed, this script will try to retrieve it from keyring. \
Alternatively, you can set the password by hand in this script source, if you \
don't want to send it as option. See source code for details.")
parser.add_option("-r", "--refresh", dest="refresh", action="store",
help="minimum period, in seconds, to allow another call \
to Google (this is just to avoid HTTP overhead). Default value is %d. If using \
conky, you can use --no-cache option if you define execution time rules in \
conkyrc." % cacheExpiresNSec)
parser.add_option("-v", "--verbose", dest="verbose", default=False,
action="store_true",
help="Used for troubleshooting problems (do not use this \
with output bars, which are usually expecting very few chars).")
parser.add_option("-a", "--add-keyring", dest="add", default=False,
action="store_true",
help="Prompts for the password to store it in Gnome \
Keyring. Useful if it is the first time using this script.")
parser.add_option("-n", "--no-cache", dest="cache", default=True,
action="store_false",
help="Does not use cache (overrides --refresh option). \
Only use this if you are controlling the calls to this script by yourself or \
another program (such as conky, etc).")
(options, args) = parser.parse_args()
verbose = options.verbose
username = options.username
useCache = options.cache
# validate username
if not username or len(username) == 0:
dieGracefully("The --username option is required!")
if options.add:
setKeyringPassword()
# validate password
if not password or len(password) == 0:
# no passwords stored locally, try to get from options
password = options.password
if not password:
# still nothing, try from keyring
try:
import keyring
password = keyring.get_password(__NAME__, username)
if not password:
dieGracefully("""Password not available (not in keyring).
Try to either:
a) use option --password;
b) add your password to keyring (see --help for details); or
c) hard-code your password in this script (see password variable).
""")
except Exception, e:
dieGracefully("keyring module not found. See --help for details.")
# validate refresh
refresh = options.refresh
if refresh:
try:
cacheExpiresNSec = int(refresh)
if cacheExpiresNSec <= 0:
dieGracefully("--refresh must be higher than zero!")
except Exception, e:
dieGracefully("--refresh must be an integer value, higher than zero.")
def main():
"""
Executes the main code of this script
"""
parseArgs()
# validate settings
if not username or not password:
dieGracefully()
if os.path.exists(cacheFile):
if datetime.datetime.now() - \
datetime.datetime.fromtimestamp(os.path.getmtime(cacheFile)) > \
datetime.timedelta(seconds=cacheExpiresNSec):
# remove if old enough
os.remove(cacheFile)
else:
if verbose:
print 'Accessing cache file.'
# still looks good
with open(cacheFile, 'r') as f:
print f.read()
sys.exit(0)
url = 'https://mail.google.com/mail/feed/atom'
try:
if verbose:
print 'Refreshing data from Google.'
ah = urllib2.HTTPBasicAuthHandler()
ah.add_password('New mail feed', 'https://mail.google.com', username, password)
op = urllib2.build_opener(ah)
urllib2.install_opener(op)
response = urllib2.urlopen('https://mail.google.com/mail/feed/atom')
contents = response.read().decode('utf-8')
unread = '?'
try:
from xml.etree.ElementTree import fromstring
e = fromstring(contents)
fc = e.find('{http://purl.org/atom/ns#}fullcount')
unread = fc.text
except Exception, e:
# XML parsing failed, do it the hard-way (ugly, but works)
ifrom = contents.index('<fullcount>') + 11
ito = contents.index('</fullcount>')
unread = contents[ifrom:ito]
print unread
# save to cache, if applicable
if useCache:
with open(cacheFile, 'w') as f:
f.write(unread)
sys.exit(0)
except Exception, e:
dieGracefully("Unexpected error has occurred. %s" % traceback.format_exc())
if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
print # just to avoid password input interruption.
sys.exit(0)
@brunobraga
Copy link
Author

Here is an example of implementing this script into i3status (obeying the color formats for i3bar):

#!/bin/bash

function gmail() {
    count=`~/.i3/gmail-count.py -u bruno.braga`
    if [ "$count" == "0" ]; then
        color="#585858"
    elif [ $count -gt 0 ]; then
        color="#00FF00"
    else
        color="#FF0000"
    fi
    echo "{\"name\":\"gmail\",\"color\":\"$color\",\"full_text\":\"$count\"}"
}

skip=3
count=0
i3status  --config ~/.i3/i3status.conf | while :
do
       count=`expr $count + 1`
       read line
       if [ $count -lt $skip ]; then
            echo $line
            continue
       elif  [ $count -eq $skip ]; then
            echo "${line:0:-1},`gmail`]"
       else 
            echo "${line:0:-1},`gmail`]"
       fi
done

@kuzzmi
Copy link

kuzzmi commented Apr 5, 2015

Does it work now? I've tried to use but all the time I receive the same:

Refreshing data from Google.
Unexpected error has occurred. Traceback (most recent call last):
  File "./gmail-count", line 257, in main
    response = urllib2.urlopen('https://mail.google.com/mail/feed/atom')
  File "/usr/lib/python2.7/urllib2.py", line 127, in urlopen
    return _opener.open(url, data, timeout)
  File "/usr/lib/python2.7/urllib2.py", line 410, in open
    response = meth(req, response)
  File "/usr/lib/python2.7/urllib2.py", line 523, in http_response
    'http', request, response, code, msg, hdrs)
  File "/usr/lib/python2.7/urllib2.py", line 448, in error
    return self._call_chain(*args)
  File "/usr/lib/python2.7/urllib2.py", line 382, in _call_chain
    result = func(*args)
  File "/usr/lib/python2.7/urllib2.py", line 531, in http_error_default
    raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
HTTPError: HTTP Error 401: Unauthorized

@erichlf
Copy link

erichlf commented Aug 5, 2015

I get the same error. I will see if I can get it working.

@erichlf
Copy link

erichlf commented Aug 5, 2015

Change

  ah.add_password('New mail feed', 'https://mail.google.com', username, password)

to

  ah.add_password(realm='mail.google.com', 
                                uri='https://mail.google.com', 
                                username=username, 
                                passwd=password)

@andoniu
Copy link

andoniu commented Aug 10, 2015

for me it worked with user=username,

@su8
Copy link

su8 commented Apr 2, 2018

/*
   04/02/2018 https://github.com/su8

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
   MA 02110-1301, USA.
*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>

#include <curl/curl.h>

#define GMAIL_ACC "foo"
#define GMAIL_PASS "bar"

static size_t read_gmail_data_cb(char *, size_t size, size_t nmemb, char *);

static size_t
read_gmail_data_cb(char *data, size_t size, size_t nmemb, char *str1) {
  char *ptr = data;
  size_t sz = nmemb * size, x = 0;

  for (; *ptr; ptr++, x++) {
    if ((x+17) < sz) { /* Verifying up to *(ptr+17) */

      if ('f' == *ptr) { /* fullcount */
        if ('f' == *ptr && 'u' == *(ptr+1) && 'l' == *(ptr+2)) {
          *str1++ = *(ptr+10); /* 1 email */
          if (0 != (isdigit((unsigned char) *(ptr+11)))) {
            *str1++ = *(ptr+11); /* 10 emails */
          }
          if (0 != (isdigit((unsigned char) *(ptr+12)))) {
            *str1++ = *(ptr+12); /* 100 emails */
          }
          if (0 != (isdigit((unsigned char) *(ptr+13)))) {
            *str1++ = *(ptr+13); /* 1000 emails */
          }
          if (0 != (isdigit((unsigned char) *(ptr+14)))) {
            *str1++ = *(ptr+14); /* 10 000 emails */
          }
          if (0 != (isdigit((unsigned char) *(ptr+15)))) {
            *str1++ = *(ptr+15); /* 100 000 emails */
          }
          *str1 = '\0';
          break;
        }
      }

    }
  }

  if ('\0' != *str1) {
    *str1++ = '\0';
  }
  return sz;
}


int main(void) {
  char str[1000] = "0";
  const char *const da_url = "https://mail.google.com/mail/feed/atom";

  CURL *curl = NULL;
  CURLcode res;

  curl_global_init(CURL_GLOBAL_ALL);

  if (NULL == (curl = curl_easy_init())) {
    goto error;
  }

  curl_easy_setopt(curl, CURLOPT_USERNAME, GMAIL_ACC);
  curl_easy_setopt(curl, CURLOPT_PASSWORD, GMAIL_PASS);
  curl_easy_setopt(curl, CURLOPT_URL, da_url);
  curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip");
  curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL); 
  curl_easy_setopt(curl, CURLOPT_TIMEOUT, 20L);
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, read_gmail_data_cb);
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, str);

  res = curl_easy_perform(curl);
  if (CURLE_OK != res) {
    goto error;
  }

error:
  if (NULL != curl) {
    curl_easy_cleanup(curl);
  }
  curl_global_cleanup();

  printf("Unread Mails %s\n", str);
  return EXIT_SUCCESS;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment