cleaned up how main is called
[gmail.py.git] / gmail.py
bloba6cd7ac6529f9a23d5438219399e162317318ed3
1 #!/usr/bin/env python
3 import sys
4 import datetime
6 from urllib import FancyURLopener
7 from optparse import OptionParser
9 from xml.etree import ElementTree as ET
11 class TooManyPasswordsError(BaseException):
12 def __init__(self, message=None):
13 BaseException.__init__(self, message)
15 class UnauthorizedAccessError(BaseException):
16 def __init__(self, message=None):
17 BaseException.__init__(self, message)
19 class FancyURLopenerMod(FancyURLopener):
20 """
21 This overrides the default prompt_user_password, allowing for username and
22 password to be passed from the command line.
23 """
24 def __init__(self, *args, **kwargs):
25 try: self.username = kwargs['username']
26 except KeyError: self.username = None
27 try: self.password = kwargs['password']
28 except KeyError: self.password = None
30 # once urllib uses new style classes, or in python 3.0+, use:
31 # super(FancyURLopenerMod, self).__init__(*args, **kwargs)
32 # till then this will work, but not in python 3.0+:
33 FancyURLopener.__init__(self, *args, **kwargs)
35 # only try opening the account once
36 #self.authtries = 0
37 #self.maxauthtries = 3
39 self.flag = False
41 def prompt_user_passwd(self,host,realm):
42 """
43 overridden method of FancyURLopener, allowing for command line arguments
44 """
45 if self.flag: return None, None
46 self.flag = True
48 #self.authtries += 1
49 #if self.maxauthtries and self.authtries > self.maxauthtries:
50 # #TODO print a final 'giving up on %username' message
51 # return TooManyPasswordsError
53 import getpass
54 try:
55 if self.username is not None: user = self.username
56 else: user = raw_input("Enter username for %s at %s: " % (realm, host))
57 if self.password is not None: passwd = self.password
58 else: passwd = getpass.getpass("Enter password for %s in %s at %s: " %
59 (user, realm, host))
60 return user, passwd
61 except KeyboardInterrupt:
62 print
63 return None, None
65 class GMail(object):
67 def __init__(self, timezone=(0,0)):
68 self.timezone = timezone
69 self.xml_tags = {
70 'entry' : "{http://purl.org/atom/ns#}entry",
71 'fullcount' : "{http://purl.org/atom/ns#}fullcount",
73 'title' : "{http://purl.org/atom/ns#}title",
74 'summary' : "{http://purl.org/atom/ns#}summary",
75 'link' : "{http://purl.org/atom/ns#}link",
76 'modified' : "{http://purl.org/atom/ns#}modified",
77 'issued' : "{http://purl.org/atom/ns#}issued",
78 'id' : "{http://purl.org/atom/ns#}id",
79 'name' : "{http://purl.org/atom/ns#}author/{http://purl.org/atom/ns#}name",
80 'email' : "{http://purl.org/atom/ns#}author/{http://purl.org/atom/ns#}email"
83 def open_feed(self, username=None, password=None):
84 """
85 uses gmail's atom feed to retrieve a summary of the inbox, returns an XML
86 element tree
88 FancyURLopenerMod is simply urllib.FancyURLopener with a modified
89 prompt_user_password in order to accept command line arguments
90 """
91 url = 'https://mail.google.com/mail/feed/atom/'
92 opener = FancyURLopenerMod(username = username, password = password)
93 f = opener.open(url)
94 feed = f.read()
95 f.close()
96 self.tree = ET.fromstring(feed)
97 if self.tree.tag == 'HTML':
98 #TODO make sure that unauthorized is in the page title
99 raise UnauthorizedAccessError
101 def _timezone(self):
103 calculates the timezone offsetin seconds, and the value of 1 day in seconds
105 # negative_day is used later in order to check whether the email came
106 # 'Today','Yesterday', or 'The day before'
107 timezone_tuple = (int(self.timezone[:-2]), int(self.timezone[-2:]))
108 self.TZ = (timezone_tuple[0]*60 + timezone_tuple[1])*60
109 self.negative_day = (-24)*60*60
110 assert self.negative_day < 0
112 def _sanitize_datetime(self, datestring):
114 accepts the datestring as obtained from the atom feed and returns the time
115 in your timezone and the date as one of 'Today','Yesterday', 'The day
116 before' or the date itself (eg. January 1, 2011)
118 self._timezone()
120 # the date in the feed is in the format 2011-11-20T05:13:59Z
121 emaildate = datestring.split('T')[0]
122 emailtime = datestring.split('T')[1].split('Z')[0]
124 datetime_str = []
125 datetime_str.extend([int(x) for x in emaildate.split('-')])
126 datetime_str.extend([int(x) for x in emailtime.split(':')])
128 email_datetime = datetime.datetime(*datetime_str)
129 timezone_correction = datetime.timedelta(0,self.TZ)
130 email_datetime += timezone_correction
132 # calculate today, yesterday and the day before using the seconds value of 1 day
133 today = datetime.datetime.today()
134 yesterday = datetime.datetime.today() \
135 + datetime.timedelta(0,self.negative_day)
136 daybefore = datetime.datetime.today() \
137 + datetime.timedelta(0,self.negative_day) \
138 + datetime.timedelta(0,self.negative_day)
140 # %F displays the full date as %Y-%m-%d
141 email_datetime_date = email_datetime.strftime('%F')
142 today_date = today.strftime('%F')
143 yesterday_date = yesterday.strftime('%F')
144 daybefore_date = daybefore.strftime('%F')
146 if email_datetime_date == today_date: sanitized_date = 'Today'
147 elif email_datetime_date == yesterday_date: sanitized_date = 'Yesterday'
148 elif email_datetime_date == daybefore_date: sanitized_date = 'Day before yesterday'
149 else: sanitized_date = email_datetime.strftime('%B %d, %Y')
151 sanitized_time = email_datetime.strftime('%l:%M %P')
152 return sanitized_date, sanitized_time
154 def printmail(self, summary=False, printcount=10, printall=False):
156 Returns the number of emails retrieved
158 print '\n', self.tree.find(self.xml_tags['title']).text, '\n'
160 count = int(self.tree.find(self.xml_tags['fullcount']).text)
161 if not count:
162 print 'No new messages!'
163 return 0
165 printcount = int(printcount)
166 if printall: printcount = -1
168 for n, k in enumerate(self.tree.findall(self.tree[-1].tag)):
169 if printcount == 0: break
170 printcount -= 1
171 sdate, stime = self._sanitize_datetime(k.find(self.xml_tags['issued']).text)
172 print '%s. %s at %s: %s <%s> wrote "%s"' % (n+1, sdate,stime,
173 k.find(self.xml_tags['name']).text,
174 k.find(self.xml_tags['email']).text,
175 k.find(self.xml_tags['title']).text)
176 if summary: print '\tText: %s' % (k.find(self.xml_tags['summary']).text)
178 return count
181 def parse_cmd_line():
183 usage = '%prog [options] [username1] [username2] ...'
184 parser = OptionParser(usage=usage)
185 parser.add_option("-u", "--username", dest="username",
186 help="your gmail username. use this if you also want to specify the password on the command line. otherwise simply pass the usernames as arguments")
187 parser.add_option("-p", "--password", dest="password", help="your gmail password")
188 parser.add_option("-t", "--timezone", dest="timezone", default="+0530", help="specify your timezone in +/-HHMM format [default: %default]")
189 parser.add_option("-n", dest="printcount", default=10,
190 help="print 'n' messages")
191 parser.add_option("-s", "--summary", action="store_true", dest="summary",
192 default=False, help="prints the summary text that is generally visible in your gmail inbox [default: %default]")
193 parser.add_option("-a", "--all", action="store_true", dest="printall",
194 default=False, help="prints all messages in your inbox")
196 (options,args) = parser.parse_args(sys.argv[1:])
198 return options, args
200 def main():
202 options, args = parse_cmd_line()
204 try: int(options.printcount)
205 except ValueError:
206 print 'The parameter to -n needs to be a number'
207 exit()
209 # accounts is a list of (username, password) tuples. if username/password
210 # flags are provided they are added to the list before adding username/password
211 # obtained from the args list. all accounts thus obtained are worked on in one go
212 accounts = []
214 # verifing that username and password flags are both provided, or neither are
215 if options.username or options.password:
216 try:
217 assert options.username
218 assert options.password
219 except AssertionError:
220 print 'The --username flag and the --password flag MUST be used in \
221 conjunction or avoided altogether'
222 else:
223 accounts = [(options.username, options.password)]
225 import getpass
226 for username in args:
227 accounts.append((username, getpass.getpass('Enter password for account %s: ' % username)))
229 count = []
230 for username, password in accounts:
231 mailchecker = GMail(timezone=options.timezone)
232 try: mailchecker.open_feed(username=username, password = password)
233 except UnauthorizedAccessError:
234 print 'Incorrect password for account %s, ignoring.' % username
236 #TODO implement a retry mechanism for incorrect password
238 #print 'Incorrect password for %s, retrying: ' % username
239 #try: mailchecker.open_feed(username=username, password = getpass.getpass('Incorrect password for %s, retrying: ' % username))
240 #except TooManyPasswordsError:
241 # print 'Invalid password entered too many times, giving up.'
242 #else:
243 # count.append(mailchecker.printmail(options.summary, options.printcount, options.printall))
244 else:
245 count.append(mailchecker.printmail(options.summary, options.printcount, options.printall))
247 print '\n%s%s messages recieved' % (reduce(lambda x, y: x+y, count) if count else 'No',
248 ' ( %s )' % ' + '.join([str(i) for i in count]) if len(count)>1 else '' )
251 if __name__ == '__main__':
252 main()