Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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

This comment has been minimized.

Copy link
Owner Author

brunobraga commented Aug 21, 2013

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

This comment has been minimized.

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

This comment has been minimized.

Copy link

erichlf commented Aug 5, 2015

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

@erichlf

This comment has been minimized.

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

This comment has been minimized.

Copy link

andoniu commented Aug 10, 2015

for me it worked with user=username,

@su8

This comment has been minimized.

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
You can’t perform that action at this time.