Module libgmail
[hide private]
[frames] | no frames]

Source Code for Module libgmail

   1  #!/usr/bin/env python 
   2  # 
   3  # libgmail -- Gmail access via Python 
   4  # 
   5  ## To get the version number of the available libgmail version. 
   6  ## Reminder: add date before next release. This attribute is also 
   7  ## used in the setup script. 
   8  Version = '0.1.7' # (Oct 2007) 
   9   
  10  # Original author: follower@myrealbox.com 
  11  # Maintainers: Waseem (wdaher@mit.edu) and Stas Z (stas@linux.isbeter.nl) 
  12  # 
  13  # License: GPL 2.0 
  14  # 
  15  # NOTE: 
  16  #   You should ensure you are permitted to use this script before using it 
  17  #   to access Google's Gmail servers. 
  18  # 
  19  # 
  20  # Gmail Implementation Notes 
  21  # ========================== 
  22  # 
  23  # * Folders contain message threads, not individual messages. At present I 
  24  #   do not know any way to list all messages without processing thread list. 
  25  # 
  26   
  27  LG_DEBUG=0 
  28  from lgconstants import * 
  29   
  30  import os,pprint 
  31  import re 
  32  import urllib 
  33  import urllib2 
  34  import mimetypes 
  35  import types 
  36  from cPickle import load, dump 
  37   
  38  from email.MIMEBase import MIMEBase 
  39  from email.MIMEText import MIMEText 
  40  from email.MIMEMultipart import MIMEMultipart 
  41   
  42  GMAIL_URL_LOGIN = "https://www.google.com/accounts/ServiceLoginBoxAuth" 
  43  GMAIL_URL_GMAIL = "https://mail.google.com/mail/" 
  44   
  45  #  Set to any value to use proxy. 
  46  PROXY_URL = None  # e.g. libgmail.PROXY_URL = 'myproxy.org:3128' 
  47   
  48  # TODO: Get these on the fly? 
  49  STANDARD_FOLDERS = [U_INBOX_SEARCH, U_STARRED_SEARCH, 
  50                      U_ALL_SEARCH, U_DRAFTS_SEARCH, 
  51                      U_SENT_SEARCH, U_SPAM_SEARCH] 
  52   
  53  # Constants with names not from the Gmail Javascript: 
  54  # TODO: Move to `lgconstants.py`? 
  55  U_SAVEDRAFT_VIEW = "sd" 
  56   
  57  D_DRAFTINFO = "di" 
  58  # NOTE: All other DI_* field offsets seem to match the MI_* field offsets 
  59  DI_BODY = 19 
  60   
  61  versionWarned = False # If the Javascript version is different have we 
  62                        # warned about it? 
  63   
  64   
  65  RE_SPLIT_PAGE_CONTENT = re.compile("D\((.*?)\);", re.DOTALL) 
  66   
67 -class GmailError(Exception):
68 ''' 69 Exception thrown upon gmail-specific failures, in particular a 70 failure to log in and a failure to parse responses. 71 72 ''' 73 pass
74
75 -class GmailSendError(Exception):
76 ''' 77 Exception to throw if we're unable to send a message 78 ''' 79 pass
80
81 -def _parsePage(pageContent):
82 """ 83 Parse the supplied HTML page and extract useful information from 84 the embedded Javascript. 85 86 """ 87 lines = pageContent.splitlines() 88 data = '\n'.join([x for x in lines if x and x[0] in ['D', ')', ',', ']']]) 89 #data = data.replace(',,',',').replace(',,',',') 90 data = re.sub(',{2,}', ',', data) 91 92 result = [] 93 try: 94 exec data in {'__builtins__': None}, {'D': lambda x: result.append(x)} 95 except SyntaxError,info: 96 print info 97 raise GmailError, 'Failed to parse data returned from gmail.' 98 99 items = result 100 itemsDict = {} 101 namesFoundTwice = [] 102 for item in items: 103 name = item[0] 104 try: 105 parsedValue = item[1:] 106 except Exception: 107 parsedValue = [''] 108 if itemsDict.has_key(name): 109 # This handles the case where a name key is used more than 110 # once (e.g. mail items, mail body etc) and automatically 111 # places the values into list. 112 # TODO: Check this actually works properly, it's early... :-) 113 114 if len(parsedValue) and type(parsedValue[0]) is types.ListType: 115 for item in parsedValue: 116 itemsDict[name].append(item) 117 else: 118 itemsDict[name].append(parsedValue) 119 else: 120 if len(parsedValue) and type(parsedValue[0]) is types.ListType: 121 itemsDict[name] = [] 122 for item in parsedValue: 123 itemsDict[name].append(item) 124 else: 125 itemsDict[name] = [parsedValue] 126 127 return itemsDict
128
129 -def _splitBunches(infoItems):# Is this still needed ?? Stas
130 """ 131 Utility to help make it easy to iterate over each item separately, 132 even if they were bunched on the page. 133 """ 134 result= [] 135 # TODO: Decide if this is the best approach. 136 for group in infoItems: 137 if type(group) == tuple: 138 result.extend(group) 139 else: 140 result.append(group) 141 return result 142
143 -class SmartRedirectHandler(urllib2.HTTPRedirectHandler):
144 - def __init__(self, cookiejar):
145 self.cookiejar = cookiejar
146
147 - def http_error_302(self, req, fp, code, msg, headers):
148 # The location redirect doesn't seem to change 149 # the hostname header appropriately, so we do 150 # by hand. (Is this a bug in urllib2?) 151 new_host = re.match(r'http[s]*://(.*?\.google\.com)', 152 headers.getheader('Location')) 153 if new_host: 154 req.add_header("Host", new_host.groups()[0]) 155 result = urllib2.HTTPRedirectHandler.http_error_302( 156 self, req, fp, code, msg, headers) 157 return result
158
159 -class CookieJar:
160 """ 161 A rough cookie handler, intended to only refer to one domain. 162 163 Does no expiry or anything like that. 164 165 (The only reason this is here is so I don't have to require 166 the `ClientCookie` package.) 167 168 """ 169
170 - def __init__(self):
171 """ 172 """ 173 self._cookies = {}
174 175
176 - def extractCookies(self, headers, nameFilter = None):
177 """ 178 """ 179 # TODO: Do this all more nicely? 180 for cookie in headers.getheaders('Set-Cookie'): 181 name, value = (cookie.split("=", 1) + [""])[:2] 182 if LG_DEBUG: print "Extracted cookie `%s`" % (name) 183 if not nameFilter or name in nameFilter: 184 self._cookies[name] = value.split(";")[0] 185 if LG_DEBUG: print "Stored cookie `%s` value `%s`" % (name, self._cookies[name]) 186 if self._cookies[name] == "EXPIRED": 187 if LG_DEBUG: 188 print "We got an expired cookie: %s:%s, deleting." % (name, self._cookies[name]) 189 del self._cookies[name]
190 191
192 - def addCookie(self, name, value):
193 """ 194 """ 195 self._cookies[name] = value
196 197
198 - def setCookies(self, request):
199 """ 200 """ 201 request.add_header('Cookie', 202 ";".join(["%s=%s" % (k,v) 203 for k,v in self._cookies.items()]))
204 205 206
207 -def _buildURL(**kwargs):
208 """ 209 """ 210 return "%s?%s" % (URL_GMAIL, urllib.urlencode(kwargs))
211 212 213
214 -def _paramsToMime(params, filenames, files):
215 """ 216 """ 217 mimeMsg = MIMEMultipart("form-data") 218 219 for name, value in params.iteritems(): 220 mimeItem = MIMEText(value) 221 mimeItem.add_header("Content-Disposition", "form-data", name=name) 222 223 # TODO: Handle this better...? 224 for hdr in ['Content-Type','MIME-Version','Content-Transfer-Encoding']: 225 del mimeItem[hdr] 226 227 mimeMsg.attach(mimeItem) 228 229 if filenames or files: 230 filenames = filenames or [] 231 files = files or [] 232 for idx, item in enumerate(filenames + files): 233 # TODO: This is messy, tidy it... 234 if isinstance(item, str): 235 # We assume it's a file path... 236 filename = item 237 contentType = mimetypes.guess_type(filename)[0] 238 payload = open(filename, "rb").read() 239 else: 240 # We assume it's an `email.Message.Message` instance... 241 # TODO: Make more use of the pre-encoded information? 242 filename = item.get_filename() 243 contentType = item.get_content_type() 244 payload = item.get_payload(decode=True) 245 246 if not contentType: 247 contentType = "application/octet-stream" 248 249 mimeItem = MIMEBase(*contentType.split("/")) 250 mimeItem.add_header("Content-Disposition", "form-data", 251 name="file%s" % idx, filename=filename) 252 # TODO: Encode the payload? 253 mimeItem.set_payload(payload) 254 255 # TODO: Handle this better...? 256 for hdr in ['MIME-Version','Content-Transfer-Encoding']: 257 del mimeItem[hdr] 258 259 mimeMsg.attach(mimeItem) 260 261 del mimeMsg['MIME-Version'] 262 263 return mimeMsg
264 265
266 -class GmailLoginFailure(Exception):
267 """ 268 Raised whenever the login process fails--could be wrong username/password, 269 or Gmail service error, for example. 270 Extract the error message like this: 271 try: 272 foobar 273 except GmailLoginFailure,e: 274 mesg = e.message# or 275 print e# uses the __str__ 276 """
277 - def __init__(self,message):
278 self.message = message
279 - def __str__(self):
280 return repr(self.message)
281
282 -class GmailAccount:
283 """ 284 """ 285
286 - def __init__(self, name = "", pw = "", state = None, domain = None):
287 global URL_LOGIN, URL_GMAIL 288 """ 289 """ 290 self.domain = domain 291 if self.domain: 292 URL_LOGIN = "https://www.google.com/a/" + self.domain + "/LoginAction" 293 URL_GMAIL = "http://mail.google.com/a/" + self.domain + "/" 294 else: 295 URL_LOGIN = GMAIL_URL_LOGIN 296 URL_GMAIL = GMAIL_URL_GMAIL 297 if name and pw: 298 self.name = name 299 self._pw = pw 300 self._cookieJar = CookieJar() 301 302 if PROXY_URL is not None: 303 import gmail_transport 304 305 self.opener = urllib2.build_opener(gmail_transport.ConnectHTTPHandler(proxy = PROXY_URL), 306 gmail_transport.ConnectHTTPSHandler(proxy = PROXY_URL), 307 SmartRedirectHandler(self._cookieJar)) 308 else: 309 self.opener = urllib2.build_opener( 310 urllib2.HTTPHandler(debuglevel=0), 311 urllib2.HTTPSHandler(debuglevel=0), 312 SmartRedirectHandler(self._cookieJar)) 313 elif state: 314 # TODO: Check for stale state cookies? 315 self.name, self._cookieJar = state.state 316 else: 317 raise ValueError("GmailAccount must be instantiated with " \ 318 "either GmailSessionState object or name " \ 319 "and password.") 320 321 self._cachedQuotaInfo = None 322 self._cachedLabelNames = None
323 324
325 - def login(self):
326 """ 327 """ 328 # TODO: Throw exception if we were instantiated with state? 329 if self.domain: 330 data = urllib.urlencode({'continue': URL_GMAIL, 331 'at' : 'null', 332 'service' : 'mail', 333 'userName': self.name, 334 'password': self._pw, 335 }) 336 else: 337 data = urllib.urlencode({'continue': URL_GMAIL, 338 'Email': self.name, 339 'Passwd': self._pw, 340 }) 341 342 headers = {'Host': 'www.google.com', 343 'User-Agent': 'Mozilla/5.0 (Compatible; libgmail-python)'} 344 345 req = urllib2.Request(URL_LOGIN, data=data, headers=headers) 346 pageData = self._retrievePage(req) 347 348 if not self.domain: 349 # The GV cookie no longer comes in this page for 350 # "Apps", so this bottom portion is unnecessary for it. 351 # This requests the page that provides the required "GV" cookie. 352 RE_PAGE_REDIRECT = 'CheckCookie\?continue=([^"\']+)' 353 354 # TODO: Catch more failure exceptions here...? 355 try: 356 link = re.search(RE_PAGE_REDIRECT, pageData).group(1) 357 redirectURL = urllib2.unquote(link) 358 redirectURL = redirectURL.replace('\\x26', '&') 359 360 except AttributeError: 361 raise GmailLoginFailure("Login failed. (Wrong username/password?)") 362 # We aren't concerned with the actual content of this page, 363 # just the cookie that is returned with it. 364 pageData = self._retrievePage(redirectURL)
365
366 - def _retrievePage(self, urlOrRequest):
367 """ 368 """ 369 if self.opener is None: 370 raise "Cannot find urlopener" 371 372 if not isinstance(urlOrRequest, urllib2.Request): 373 req = urllib2.Request(urlOrRequest) 374 else: 375 req = urlOrRequest 376 377 self._cookieJar.setCookies(req) 378 req.add_header('User-Agent', 379 'Mozilla/5.0 (Compatible; libgmail-python)') 380 381 try: 382 resp = self.opener.open(req) 383 except urllib2.HTTPError,info: 384 print info 385 return None 386 pageData = resp.read() 387 388 # Extract cookies here 389 self._cookieJar.extractCookies(resp.headers) 390 391 # TODO: Enable logging of page data for debugging purposes? 392 393 return pageData
394 395
396 - def _parsePage(self, urlOrRequest):
397 """ 398 Retrieve & then parse the requested page content. 399 400 """ 401 items = _parsePage(self._retrievePage(urlOrRequest)) 402 # Automatically cache some things like quota usage. 403 # TODO: Cache more? 404 # TODO: Expire cached values? 405 # TODO: Do this better. 406 try: 407 self._cachedQuotaInfo = items[D_QUOTA] 408 except KeyError: 409 pass 410 #pprint.pprint(items) 411 412 try: 413 self._cachedLabelNames = [category[CT_NAME] for category in items[D_CATEGORIES][0]] 414 except KeyError: 415 pass 416 417 return items
418 419
420 - def _parseSearchResult(self, searchType, start = 0, **kwargs):
421 """ 422 """ 423 params = {U_SEARCH: searchType, 424 U_START: start, 425 U_VIEW: U_THREADLIST_VIEW, 426 } 427 params.update(kwargs) 428 return self._parsePage(_buildURL(**params))
429 430
431 - def _parseThreadSearch(self, searchType, allPages = False, **kwargs):
432 """ 433 434 Only works for thread-based results at present. # TODO: Change this? 435 """ 436 start = 0 437 tot = 0 438 threadsInfo = [] 439 # Option to get *all* threads if multiple pages are used. 440 while (start == 0) or (allPages and 441 len(threadsInfo) < threadListSummary[TS_TOTAL]): 442 443 items = self._parseSearchResult(searchType, start, **kwargs) 444 #TODO: Handle single & zero result case better? Does this work? 445 try: 446 threads = items[D_THREAD] 447 except KeyError: 448 break 449 else: 450 for th in threads: 451 if not type(th[0]) is types.ListType: 452 th = [th] 453 threadsInfo.append(th) 454 # TODO: Check if the total or per-page values have changed? 455 threadListSummary = items[D_THREADLIST_SUMMARY][0] 456 threadsPerPage = threadListSummary[TS_NUM] 457 458 start += threadsPerPage 459 460 # TODO: Record whether or not we retrieved all pages..? 461 return GmailSearchResult(self, (searchType, kwargs), threadsInfo)
462 463
464 - def _retrieveJavascript(self, version = ""):
465 """ 466 467 Note: `version` seems to be ignored. 468 """ 469 return self._retrievePage(_buildURL(view = U_PAGE_VIEW, 470 name = "js", 471 ver = version))
472 473
474 - def getMessagesByFolder(self, folderName, allPages = False):
475 """ 476 477 Folders contain conversation/message threads. 478 479 `folderName` -- As set in Gmail interface. 480 481 Returns a `GmailSearchResult` instance. 482 483 *** TODO: Change all "getMessagesByX" to "getThreadsByX"? *** 484 """ 485 return self._parseThreadSearch(folderName, allPages = allPages)
486 487
488 - def getMessagesByQuery(self, query, allPages = False):
489 """ 490 491 Returns a `GmailSearchResult` instance. 492 """ 493 return self._parseThreadSearch(U_QUERY_SEARCH, q = query, 494 allPages = allPages)
495 496
497 - def getQuotaInfo(self, refresh = False):
498 """ 499 500 Return MB used, Total MB and percentage used. 501 """ 502 # TODO: Change this to a property. 503 if not self._cachedQuotaInfo or refresh: 504 # TODO: Handle this better... 505 self.getMessagesByFolder(U_INBOX_SEARCH) 506 507 return self._cachedQuotaInfo[0][:3]
508 509
510 - def getLabelNames(self, refresh = False):
511 """ 512 """ 513 # TODO: Change this to a property? 514 if not self._cachedLabelNames or refresh: 515 # TODO: Handle this better... 516 self.getMessagesByFolder(U_INBOX_SEARCH) 517 518 return self._cachedLabelNames
519 520
521 - def getMessagesByLabel(self, label, allPages = False):
522 """ 523 """ 524 return self._parseThreadSearch(U_CATEGORY_SEARCH, 525 cat=label, allPages = allPages)
526
527 - def getRawMessage(self, msgId):
528 """ 529 """ 530 # U_ORIGINAL_MESSAGE_VIEW seems the only one that returns a page. 531 # All the other U_* results in a 404 exception. Stas 532 PageView = U_ORIGINAL_MESSAGE_VIEW 533 return self._retrievePage( 534 _buildURL(view=PageView, th=msgId))
535
536 - def getUnreadMessages(self):
537 """ 538 """ 539 return self._parseThreadSearch(U_QUERY_SEARCH, 540 q = "is:" + U_AS_SUBSET_UNREAD)
541 542
543 - def getUnreadMsgCount(self):
544 """ 545 """ 546 items = self._parseSearchResult(U_QUERY_SEARCH, 547 q = "is:" + U_AS_SUBSET_UNREAD) 548 try: 549 result = items[D_THREADLIST_SUMMARY][0][TS_TOTAL_MSGS] 550 except KeyError: 551 result = 0 552 return result
553 554
555 - def _getActionToken(self):
556 """ 557 """ 558 try: 559 at = self._cookieJar._cookies[ACTION_TOKEN_COOKIE] 560 except KeyError: 561 self.getLabelNames(True) 562 at = self._cookieJar._cookies[ACTION_TOKEN_COOKIE] 563 564 return at
565 566
567 - def sendMessage(self, msg, asDraft = False, _extraParams = None):
568 """ 569 570 `msg` -- `GmailComposedMessage` instance. 571 572 `_extraParams` -- Dictionary containing additional parameters 573 to put into POST message. (Not officially 574 for external use, more to make feature 575 additional a little easier to play with.) 576 577 Note: Now returns `GmailMessageStub` instance with populated 578 `id` (and `_account`) fields on success or None on failure. 579 580 """ 581 # TODO: Handle drafts separately? 582 params = {U_VIEW: [U_SENDMAIL_VIEW, U_SAVEDRAFT_VIEW][asDraft], 583 U_REFERENCED_MSG: "", 584 U_THREAD: "", 585 U_DRAFT_MSG: "", 586 U_COMPOSEID: "1", 587 U_ACTION_TOKEN: self._getActionToken(), 588 U_COMPOSE_TO: msg.to, 589 U_COMPOSE_CC: msg.cc, 590 U_COMPOSE_BCC: msg.bcc, 591 "subject": msg.subject, 592 "msgbody": msg.body, 593 } 594 595 if _extraParams: 596 params.update(_extraParams) 597 598 # Amongst other things, I used the following post to work out this: 599 # <http://groups.google.com/groups? 600 # selm=mailman.1047080233.20095.python-list%40python.org> 601 mimeMessage = _paramsToMime(params, msg.filenames, msg.files) 602 603 #### TODO: Ughh, tidy all this up & do it better... 604 ## This horrible mess is here for two main reasons: 605 ## 1. The `Content-Type` header (which also contains the boundary 606 ## marker) needs to be extracted from the MIME message so 607 ## we can send it as the request `Content-Type` header instead. 608 ## 2. It seems the form submission needs to use "\r\n" for new 609 ## lines instead of the "\n" returned by `as_string()`. 610 ## I tried changing the value of `NL` used by the `Generator` class 611 ## but it didn't work so I'm doing it this way until I figure 612 ## out how to do it properly. Of course, first try, if the payloads 613 ## contained "\n" sequences they got replaced too, which corrupted 614 ## the attachments. I could probably encode the submission, 615 ## which would probably be nicer, but in the meantime I'm kludging 616 ## this workaround that replaces all non-text payloads with a 617 ## marker, changes all "\n" to "\r\n" and finally replaces the 618 ## markers with the original payloads. 619 ## Yeah, I know, it's horrible, but hey it works doesn't it? If you've 620 ## got a problem with it, fix it yourself & give me the patch! 621 ## 622 origPayloads = {} 623 FMT_MARKER = "&&&&&&%s&&&&&&" 624 625 for i, m in enumerate(mimeMessage.get_payload()): 626 if not isinstance(m, MIMEText): #Do we care if we change text ones? 627 origPayloads[i] = m.get_payload() 628 m.set_payload(FMT_MARKER % i) 629 630 mimeMessage.epilogue = "" 631 msgStr = mimeMessage.as_string() 632 contentTypeHeader, data = msgStr.split("\n\n", 1) 633 contentTypeHeader = contentTypeHeader.split(":", 1) 634 data = data.replace("\n", "\r\n") 635 for k,v in origPayloads.iteritems(): 636 data = data.replace(FMT_MARKER % k, v) 637 #### 638 639 req = urllib2.Request(_buildURL(), data = data) 640 req.add_header(*contentTypeHeader) 641 items = self._parsePage(req) 642 643 # TODO: Check composeid? 644 # Sometimes we get the success message 645 # but the id is 0 and no message is sent 646 result = None 647 resultInfo = items[D_SENDMAIL_RESULT][0] 648 649 if resultInfo[SM_SUCCESS]: 650 result = GmailMessageStub(id = resultInfo[SM_NEWTHREADID], 651 _account = self) 652 else: 653 raise GmailSendError, resultInfo[SM_MSG] 654 return result
655 656
657 - def trashMessage(self, msg):
658 """ 659 """ 660 # TODO: Decide if we should make this a method of `GmailMessage`. 661 # TODO: Should we check we have been given a `GmailMessage` instance? 662 params = { 663 U_ACTION: U_DELETEMESSAGE_ACTION, 664 U_ACTION_MESSAGE: msg.id, 665 U_ACTION_TOKEN: self._getActionToken(), 666 } 667 668 items = self._parsePage(_buildURL(**params)) 669 670 # TODO: Mark as trashed on success? 671 return (items[D_ACTION_RESULT][0][AR_SUCCESS] == 1)
672 673
674 - def _doThreadAction(self, actionId, thread):
675 """ 676 """ 677 # TODO: Decide if we should make this a method of `GmailThread`. 678 # TODO: Should we check we have been given a `GmailThread` instance? 679 params = { 680 U_SEARCH: U_ALL_SEARCH, #TODO:Check this search value always works. 681 U_VIEW: U_UPDATE_VIEW, 682 U_ACTION: actionId, 683 U_ACTION_THREAD: thread.id, 684 U_ACTION_TOKEN: self._getActionToken(), 685 } 686 687 items = self._parsePage(_buildURL(**params)) 688 689 return (items[D_ACTION_RESULT][0][AR_SUCCESS] == 1)
690 691
692 - def trashThread(self, thread):
693 """ 694 """ 695 # TODO: Decide if we should make this a method of `GmailThread`. 696 # TODO: Should we check we have been given a `GmailThread` instance? 697 698 result = self._doThreadAction(U_MARKTRASH_ACTION, thread) 699 700 # TODO: Mark as trashed on success? 701 return result
702 703
704 - def _createUpdateRequest(self, actionId): #extraData):
705 """ 706 Helper method to create a Request instance for an update (view) 707 action. 708 709 Returns populated `Request` instance. 710 """ 711 params = { 712 U_VIEW: U_UPDATE_VIEW, 713 } 714 715 data = { 716 U_ACTION: actionId, 717 U_ACTION_TOKEN: self._getActionToken(), 718 } 719 720 #data.update(extraData) 721 722 req = urllib2.Request(_buildURL(**params), 723 data = urllib.urlencode(data)) 724 725 return req
726 727 728 # TODO: Extract additional common code from handling of labels?
729 - def createLabel(self, labelName):
730 """ 731 """ 732 req = self._createUpdateRequest(U_CREATECATEGORY_ACTION + labelName) 733 734 # Note: Label name cache is updated by this call as well. (Handy!) 735 items = self._parsePage(req) 736 print items 737 return (items[D_ACTION_RESULT][0][AR_SUCCESS] == 1)
738 739
740 - def deleteLabel(self, labelName):
741 """ 742 """ 743 # TODO: Check labelName exits? 744 req = self._createUpdateRequest(U_DELETECATEGORY_ACTION + labelName) 745 746 # Note: Label name cache is updated by this call as well. (Handy!) 747 items = self._parsePage(req) 748 749 return (items[D_ACTION_RESULT][0][AR_SUCCESS] == 1)
750 751
752 - def renameLabel(self, oldLabelName, newLabelName):
753 """ 754 """ 755 # TODO: Check oldLabelName exits? 756 req = self._createUpdateRequest("%s%s^%s" % (U_RENAMECATEGORY_ACTION, 757 oldLabelName, newLabelName)) 758 759 # Note: Label name cache is updated by this call as well. (Handy!) 760 items = self._parsePage(req) 761 762 return (items[D_ACTION_RESULT][0][AR_SUCCESS] == 1)
763
764 - def storeFile(self, filename, label = None):
765 """ 766 """ 767 # TODO: Handle files larger than single attachment size. 768 # TODO: Allow file data objects to be supplied? 769 FILE_STORE_VERSION = "FSV_01" 770 FILE_STORE_SUBJECT_TEMPLATE = "%s %s" % (FILE_STORE_VERSION, "%s") 771 772 subject = FILE_STORE_SUBJECT_TEMPLATE % os.path.basename(filename) 773 774 msg = GmailComposedMessage(to="", subject=subject, body="", 775 filenames=[filename]) 776 777 draftMsg = self.sendMessage(msg, asDraft = True) 778 779 if draftMsg and label: 780 draftMsg.addLabel(label) 781 782 return draftMsg
783 784 ## CONTACTS SUPPORT
785 - def getContacts(self):
786 """ 787 Returns a GmailContactList object 788 that has all the contacts in it as 789 GmailContacts 790 """ 791 contactList = [] 792 # pnl = a is necessary to get *all* contacts 793 myUrl = _buildURL(view='cl',search='contacts', pnl='a') 794 myData = self._parsePage(myUrl) 795 # This comes back with a dictionary 796 # with entry 'cl' 797 addresses = myData['cl'] 798 for entry in addresses: 799 if len(entry) >= 6 and entry[0]=='ce': 800 newGmailContact = GmailContact(entry[1], entry[2], entry[4], entry[5]) 801 #### new code used to get all the notes 802 #### not used yet due to lockdown problems 803 ##rawnotes = self._getSpecInfo(entry[1]) 804 ##print rawnotes 805 ##newGmailContact = GmailContact(entry[1], entry[2], entry[4],rawnotes) 806 contactList.append(newGmailContact) 807 808 return GmailContactList(contactList)
809
810 - def addContact(self, myContact, *extra_args):
811 """ 812 Attempts to add a GmailContact to the gmail 813 address book. Returns true if successful, 814 false otherwise 815 816 Please note that after version 0.1.3.3, 817 addContact takes one argument of type 818 GmailContact, the contact to add. 819 820 The old signature of: 821 addContact(name, email, notes='') is still 822 supported, but deprecated. 823 """ 824 if len(extra_args) > 0: 825 # The user has passed in extra arguments 826 # He/she is probably trying to invoke addContact 827 # using the old, deprecated signature of: 828 # addContact(self, name, email, notes='') 829 # Build a GmailContact object and use that instead 830 (name, email) = (myContact, extra_args[0]) 831 if len(extra_args) > 1: 832 notes = extra_args[1] 833 else: 834 notes = '' 835 myContact = GmailContact(-1, name, email, notes) 836 837 # TODO: In the ideal world, we'd extract these specific 838 # constants into a nice constants file 839 840 # This mostly comes from the Johnvey Gmail API, 841 # but also from the gmail.py cited earlier 842 myURL = _buildURL(view='up') 843 844 myDataList = [ ('act','ec'), 845 ('at', self._cookieJar._cookies['GMAIL_AT']), # Cookie data? 846 ('ct_nm', myContact.getName()), 847 ('ct_em', myContact.getEmail()), 848 ('ct_id', -1 ) 849 ] 850 851 notes = myContact.getNotes() 852 if notes != '': 853 myDataList.append( ('ctf_n', notes) ) 854 855 validinfokeys = [ 856 'i', # IM 857 'p', # Phone 858 'd', # Company 859 'a', # ADR 860 'e', # Email 861 'm', # Mobile 862 'b', # Pager 863 'f', # Fax 864 't', # Title 865 'o', # Other 866 ] 867 868 moreInfo = myContact.getMoreInfo() 869 ctsn_num = -1 870 if moreInfo != {}: 871 for ctsf,ctsf_data in moreInfo.items(): 872 ctsn_num += 1 873 # data section header, WORK, HOME,... 874 sectionenum ='ctsn_%02d' % ctsn_num 875 myDataList.append( ( sectionenum, ctsf )) 876 ctsf_num = -1 877 878 if isinstance(ctsf_data[0],str): 879 ctsf_num += 1 880 # data section 881 subsectionenum = 'ctsf_%02d_%02d_%s' % (ctsn_num, ctsf_num, ctsf_data[0]) # ie. ctsf_00_01_p 882 myDataList.append( (subsectionenum, ctsf_data[1]) ) 883 else: 884 for info in ctsf_data: 885 if validinfokeys.count(info[0]) > 0: 886 ctsf_num += 1 887 # data section 888 subsectionenum = 'ctsf_%02d_%02d_%s' % (ctsn_num, ctsf_num, info[0]) # ie. ctsf_00_01_p 889 myDataList.append( (subsectionenum, info[1]) ) 890 891 myData = urllib.urlencode(myDataList) 892 request = urllib2.Request(myURL, 893 data = myData) 894 pageData = self._retrievePage(request) 895 896 if pageData.find("The contact was successfully added") == -1: 897 print pageData 898 if pageData.find("already has the email address") > 0: 899 raise Exception("Someone with same email already exists in Gmail.") 900 elif pageData.find("https://www.google.com/accounts/ServiceLogin"): 901 raise Exception("Login has expired.") 902 return False 903 else: 904 return True
905
906 - def _removeContactById(self, id):
907 """ 908 Attempts to remove the contact that occupies 909 id "id" from the gmail address book. 910 Returns True if successful, 911 False otherwise. 912 913 This is a little dangerous since you don't really 914 know who you're deleting. Really, 915 this should return the name or something of the 916 person we just killed. 917 918 Don't call this method. 919 You should be using removeContact instead. 920 """ 921 myURL = _buildURL(search='contacts', ct_id = id, c=id, act='dc', at=self._cookieJar._cookies['GMAIL_AT'], view='up') 922 pageData = self._retrievePage(myURL) 923 924 if pageData.find("The contact has been deleted") == -1: 925 return False 926 else: 927 return True
928
929 - def removeContact(self, gmailContact):
930 """ 931 Attempts to remove the GmailContact passed in 932 Returns True if successful, False otherwise. 933 """ 934 # Let's re-fetch the contact list to make 935 # sure we're really deleting the guy 936 # we think we're deleting 937 newContactList = self.getContacts() 938 newVersionOfPersonToDelete = newContactList.getContactById(gmailContact.getId()) 939 # Ok, now we need to ensure that gmailContact 940 # is the same as newVersionOfPersonToDelete 941 # and then we can go ahead and delete him/her 942 if (gmailContact == newVersionOfPersonToDelete): 943 return self._removeContactById(gmailContact.getId()) 944 else: 945 # We have a cache coherency problem -- someone 946 # else now occupies this ID slot. 947 # TODO: Perhaps signal this in some nice way 948 # to the end user? 949 950 print "Unable to delete." 951 print "Has someone else been modifying the contacts list while we have?" 952 print "Old version of person:",gmailContact 953 print "New version of person:",newVersionOfPersonToDelete 954 return False
955 956 ## Don't remove this. contact stas 957 ## def _getSpecInfo(self,id): 958 ## """ 959 ## Return all the notes data. 960 ## This is currently not used due to the fact that it requests pages in 961 ## a dos attack manner. 962 ## """ 963 ## myURL =_buildURL(search='contacts',ct_id=id,c=id,\ 964 ## at=self._cookieJar._cookies['GMAIL_AT'],view='ct') 965 ## pageData = self._retrievePage(myURL) 966 ## myData = self._parsePage(myURL) 967 ## #print "\nmyData form _getSpecInfo\n",myData 968 ## rawnotes = myData['cov'][7] 969 ## return rawnotes 970
971 -class GmailContact:
972 """ 973 Class for storing a Gmail Contacts list entry 974 """
975 - def __init__(self, name, email, *extra_args):
976 """ 977 Returns a new GmailContact object 978 (you can then call addContact on this to commit 979 it to the Gmail addressbook, for example) 980 981 Consider calling setNotes() and setMoreInfo() 982 to add extended information to this contact 983 """ 984 # Support populating other fields if we're trying 985 # to invoke this the old way, with the old constructor 986 # whose signature was __init__(self, id, name, email, notes='') 987 id = -1 988 notes = '' 989 990 if len(extra_args) > 0: 991 (id, name) = (name, email) 992 email = extra_args[0] 993 if len(extra_args) > 1: 994 notes = extra_args[1] 995 else: 996 notes = '' 997 998 self.id = id 999 self.name = name 1000 self.email = email 1001 self.notes = notes 1002 self.moreInfo = {}
1003 - def __str__(self):
1004 return "%s %s %s %s" % (self.id, self.name, self.email, self.notes)
1005 - def __eq__(self, other):
1006 if not isinstance(other, GmailContact): 1007 return False 1008 return (self.getId() == other.getId()) and \ 1009 (self.getName() == other.getName()) and \ 1010 (self.getEmail() == other.getEmail()) and \ 1011 (self.getNotes() == other.getNotes())
1012 - def getId(self):
1013 return self.id
1014 - def getName(self):
1015 return self.name
1016 - def getEmail(self):
1017 return self.email
1018 - def getNotes(self):
1019 return self.notes
1020 - def setNotes(self, notes):
1021 """ 1022 Sets the notes field for this GmailContact 1023 Note that this does NOT change the note 1024 field on Gmail's end; only adding or removing 1025 contacts modifies them 1026 """ 1027 self.notes = notes
1028
1029 - def getMoreInfo(self):
1030 return self.moreInfo
1031 - def setMoreInfo(self, moreInfo):
1032 """ 1033 moreInfo format 1034 --------------- 1035 Use special key values:: 1036 'i' = IM 1037 'p' = Phone 1038 'd' = Company 1039 'a' = ADR 1040 'e' = Email 1041 'm' = Mobile 1042 'b' = Pager 1043 'f' = Fax 1044 't' = Title 1045 'o' = Other 1046 1047 Simple example:: 1048 1049 moreInfo = {'Home': ( ('a','852 W Barry'), 1050 ('p', '1-773-244-1980'), 1051 ('i', 'aim:brianray34') ) } 1052 1053 Complex example:: 1054 1055 moreInfo = { 1056 'Personal': (('e', 'Home Email'), 1057 ('f', 'Home Fax')), 1058 'Work': (('d', 'Sample Company'), 1059 ('t', 'Job Title'), 1060 ('o', 'Department: Department1'), 1061 ('o', 'Department: Department2'), 1062 ('p', 'Work Phone'), 1063 ('m', 'Mobile Phone'), 1064 ('f', 'Work Fax'), 1065 ('b', 'Pager')) } 1066 """ 1067 self.moreInfo = moreInfo
1068 - def getVCard(self):
1069 """Returns a vCard 3.0 for this 1070 contact, as a string""" 1071 # The \r is is to comply with the RFC2425 section 5.8.1 1072 vcard = "BEGIN:VCARD\r\n" 1073 vcard += "VERSION:3.0\r\n" 1074 ## Deal with multiline notes 1075 ##vcard += "NOTE:%s\n" % self.getNotes().replace("\n","\\n") 1076 vcard += "NOTE:%s\r\n" % self.getNotes() 1077 # Fake-out N by splitting up whatever we get out of getName 1078 # This might not always do 'the right thing' 1079 # but it's a *reasonable* compromise 1080 fullname = self.getName().split() 1081 fullname.reverse() 1082 vcard += "N:%s" % ';'.join(fullname) + "\r\n" 1083 vcard += "FN:%s\r\n" % self.getName() 1084 vcard += "EMAIL;TYPE=INTERNET:%s\r\n" % self.getEmail() 1085 vcard += "END:VCARD\r\n\r\n" 1086 # Final newline in case we want to put more than one in a file 1087 return vcard
1088
1089 -class GmailContactList:
1090 """ 1091 Class for storing an entire Gmail contacts list 1092 and retrieving contacts by Id, Email address, and name 1093 """
1094 - def __init__(self, contactList):
1095 self.contactList = contactList
1096 - def __str__(self):
1097 return '\n'.join([str(item) for item in self.contactList])
1098 - def getCount(self):
1099 """ 1100 Returns number of contacts 1101 """ 1102 return len(self.contactList)
1103 - def getAllContacts(self):
1104 """ 1105 Returns an array of all the 1106 GmailContacts 1107 """ 1108 return self.contactList
1109 - def getContactByName(self, name):
1110 """ 1111 Gets the first contact in the 1112 address book whose name is 'name'. 1113 1114 Returns False if no contact 1115 could be found 1116 """ 1117 nameList = self.getContactListByName(name) 1118 if len(nameList) > 0: 1119 return nameList[0] 1120 else: 1121 return False
1122 - def getContactByEmail(self, email):
1123 """ 1124 Gets the first contact in the 1125 address book whose name is 'email'. 1126 As of this writing, Gmail insists 1127 upon a unique email; i.e. two contacts 1128 cannot share an email address. 1129 1130 Returns False if no contact 1131 could be found 1132 """ 1133 emailList = self.getContactListByEmail(email) 1134 if len(emailList) > 0: 1135 return emailList[0] 1136 else: 1137 return False
1138 - def getContactById(self, myId):
1139 """ 1140 Gets the first contact in the 1141 address book whose id is 'myId'. 1142 1143 REMEMBER: ID IS A STRING 1144 1145 Returns False if no contact 1146 could be found 1147 """ 1148 idList = self.getContactListById(myId) 1149 if len(idList) > 0: 1150 return idList[0] 1151 else: 1152 return False
1153 - def getContactListByName(self, name):
1154 """ 1155 This function returns a LIST 1156 of GmailContacts whose name is 1157 'name'. 1158 1159 Returns an empty list if no contacts 1160 were found 1161 """ 1162 nameList = [] 1163 for entry in self.contactList: 1164 if entry.getName() == name: 1165 nameList.append(entry) 1166 return nameList
1167 - def getContactListByEmail(self, email):
1168 """ 1169 This function returns a LIST 1170 of GmailContacts whose email is 1171 'email'. As of this writing, two contacts 1172 cannot share an email address, so this 1173 should only return just one item. 1174 But it doesn't hurt to be prepared? 1175 1176 Returns an empty list if no contacts 1177 were found 1178 """ 1179 emailList = [] 1180 for entry in self.contactList: 1181 if entry.getEmail() == email: 1182 emailList.append(entry) 1183 return emailList
1184 - def getContactListById(self, myId):
1185 """ 1186 This function returns a LIST 1187 of GmailContacts whose id is 1188 'myId'. We expect there only to 1189 be one, but just in case! 1190 1191 Remember: ID IS A STRING 1192 1193 Returns an empty list if no contacts 1194 were found 1195 """ 1196 idList = [] 1197 for entry in self.contactList: 1198 if entry.getId() == myId: 1199 idList.append(entry) 1200 return idList
1201
1202 -class GmailSearchResult:
1203 """ 1204 """ 1205
1206 - def __init__(self, account, search, threadsInfo):
1207 """ 1208 1209 `threadsInfo` -- As returned from Gmail but unbunched. 1210 """ 1211 #print "\nthreadsInfo\n",threadsInfo 1212 try: 1213 if not type(threadsInfo[0]) is types.ListType: 1214 threadsInfo = [threadsInfo] 1215 except IndexError: 1216 print "No messages found" 1217 1218 self._account = account 1219 self.search = search # TODO: Turn into object + format nicely. 1220 self._threads = [] 1221 1222 for thread in threadsInfo: 1223 self._threads.append(GmailThread(self, thread[0]))
1224 1225
1226 - def __iter__(self):
1227 """ 1228 """ 1229 return iter(self._threads)
1230
1231 - def __len__(self):
1232 """ 1233 """ 1234 return len(self._threads)
1235
1236 - def __getitem__(self,key):
1237 """ 1238 """ 1239 return self._threads.__getitem__(key)
1240 1241
1242 -class GmailSessionState:
1243 """ 1244 """ 1245
1246 - def __init__(self, account = None, filename = ""):
1247 """ 1248 """ 1249 if account: 1250 self.state = (account.name, account._cookieJar) 1251 elif filename: 1252 self.state = load(open(filename, "rb")) 1253 else: 1254 raise ValueError("GmailSessionState must be instantiated with " \ 1255 "either GmailAccount object or filename.")
1256 1257
1258 - def save(self, filename):
1259 """ 1260 """ 1261 dump(self.state, open(filename, "wb"), -1)
1262 1263
1264 -class _LabelHandlerMixin(object):
1265 """ 1266 1267 Note: Because a message id can be used as a thread id this works for 1268 messages as well as threads. 1269 """
1270 - def __init__(self):
1271 self._labels = None
1272
1273 - def _makeLabelList(self, labelList):
1274 self._labels = labelList
1275
1276 - def addLabel(self, labelName):
1277 """ 1278 """ 1279 # Note: It appears this also automatically creates new labels. 1280 result = self._account._doThreadAction(U_ADDCATEGORY_ACTION+labelName, 1281 self) 1282 if not self._labels: 1283 self._makeLabelList([]) 1284 # TODO: Caching this seems a little dangerous; suppress duplicates maybe? 1285 self._labels.append(labelName) 1286 return result
1287 1288
1289 - def removeLabel(self, labelName):
1290 """ 1291 """ 1292 # TODO: Check label is already attached? 1293 # Note: An error is not generated if the label is not already attached. 1294 result = \ 1295 self._account._doThreadAction(U_REMOVECATEGORY_ACTION+labelName, 1296 self) 1297 1298 removeLabel = True 1299 try: 1300 self._labels.remove(labelName) 1301 except: 1302 removeLabel = False 1303 pass 1304 1305 # If we don't check both, we might end up in some weird inconsistent state 1306 return result and removeLabel
1307
1308 - def getLabels(self):
1309 return self._labels
1310 1311 1312
1313 -class GmailThread(_LabelHandlerMixin):
1314 """ 1315 Note: As far as I can tell, the "canonical" thread id is always the same 1316 as the id of the last message in the thread. But it appears that 1317 the id of any message in the thread can be used to retrieve 1318 the thread information. 1319 1320 """ 1321
1322 - def __init__(self, parent, threadsInfo):
1323 """ 1324 """ 1325 _LabelHandlerMixin.__init__(self) 1326 1327 # TODO Handle this better? 1328 self._parent = parent 1329 self._account = self._parent._account 1330 1331 self.id = threadsInfo[T_THREADID] # TODO: Change when canonical updated? 1332 self.subject = threadsInfo[T_SUBJECT_HTML] 1333 1334 self.snippet = threadsInfo[T_SNIPPET_HTML] 1335 #self.extraSummary = threadInfo[T_EXTRA_SNIPPET] #TODO: What is this? 1336 1337 # TODO: Store other info? 1338 # Extract number of messages in thread/conversation. 1339 1340 self._authors = threadsInfo[T_AUTHORS_HTML] 1341 self.info = threadsInfo 1342 1343 try: 1344 # TODO: Find out if this information can be found another way... 1345 # (Without another page request.) 1346 self._length = int(re.search("\((\d+?)\)\Z", 1347 self._authors).group(1)) 1348 except AttributeError,info: 1349 # If there's no message count then the thread only has one message. 1350 self._length = 1 1351 1352 # TODO: Store information known about the last message (e.g. id)? 1353 self._messages = [] 1354 1355 # Populate labels 1356 self._makeLabelList(threadsInfo[T_CATEGORIES])
1357
1358 - def __getattr__(self, name):
1359 """ 1360 Dynamically dispatch some interesting thread properties. 1361 """ 1362 attrs = { 'unread': T_UNREAD, 1363 'star': T_STAR, 1364 'date': T_DATE_HTML, 1365 'authors': T_AUTHORS_HTML, 1366 'flags': T_FLAGS, 1367 'subject': T_SUBJECT_HTML, 1368 'snippet': T_SNIPPET_HTML, 1369 'categories': T_CATEGORIES, 1370 'attach': T_ATTACH_HTML, 1371 'matching_msgid': T_MATCHING_MSGID, 1372 'extra_snippet': T_EXTRA_SNIPPET } 1373 if name in attrs: 1374 return self.info[ attrs[name] ]; 1375 1376 raise AttributeError("no attribute %s" % name)
1377
1378 - def __len__(self):
1379 """ 1380 """ 1381 return self._length
1382 1383
1384 - def __iter__(self):
1385 """ 1386 """ 1387 if not self._messages: 1388 self._messages = self._getMessages(self) 1389 1390 return iter(self._messages)
1391
1392 - def __getitem__(self, key):
1393 """ 1394 """ 1395 if not self._messages: 1396 self._messages = self._getMessages(self) 1397 try: 1398 result = self._messages.__getitem__(key) 1399 except IndexError: 1400 result = [] 1401 return result
1402
1403 - def _getMessages(self, thread):
1404 """ 1405 """ 1406 # TODO: Do this better. 1407 # TODO: Specify the query folder using our specific search? 1408 items = self._account._parseSearchResult(U_QUERY_SEARCH, 1409 view = U_CONVERSATION_VIEW, 1410 th = thread.id, 1411 q = "in:anywhere") 1412 result = [] 1413 # TODO: Handle this better? 1414 # Note: This handles both draft & non-draft messages in a thread... 1415 for key, isDraft in [(D_MSGINFO, False), (D_DRAFTINFO, True)]: 1416 try: 1417 msgsInfo = items[key] 1418 except KeyError: 1419 # No messages of this type (e.g. draft or non-draft) 1420 continue 1421 else: 1422 # TODO: Handle special case of only 1 message in thread better? 1423 if type(msgsInfo[0]) != types.ListType: 1424 msgsInfo = [msgsInfo] 1425 for msg in msgsInfo: 1426 result += [GmailMessage(thread, msg, isDraft = isDraft)] 1427 1428 1429 return result
1430
1431 -class GmailMessageStub(_LabelHandlerMixin):
1432 """ 1433 1434 Intended to be used where not all message information is known/required. 1435 1436 NOTE: This may go away. 1437 """ 1438 1439 # TODO: Provide way to convert this to a full `GmailMessage` instance 1440 # or allow `GmailMessage` to be created without all info? 1441
1442 - def __init__(self, id = None, _account = None):
1443 """ 1444 """ 1445 _LabelHandlerMixin.__init__(self) 1446 self.id = id 1447 self._account = _account
1448 1449 1450
1451 -class GmailMessage(object):
1452 """ 1453 """ 1454
1455 - def __init__(self, parent, msgData, isDraft = False):
1456 """ 1457 1458 Note: `msgData` can be from either D_MSGINFO or D_DRAFTINFO. 1459 """ 1460 # TODO: Automatically detect if it's a draft or not? 1461 # TODO Handle this better? 1462 self._parent = parent 1463 self._account = self._parent._account 1464 1465 self.author = msgData[MI_AUTHORFIRSTNAME] 1466 self.id = msgData[MI_MSGID] 1467 self.number = msgData[MI_NUM] 1468 self.subject = msgData[MI_SUBJECT] 1469 self.to = msgData[MI_TO] 1470 self.cc = msgData[MI_CC] 1471 self.bcc = msgData[MI_BCC] 1472 self.sender = msgData[MI_AUTHOREMAIL] 1473 1474 self.attachments = [GmailAttachment(self, attachmentInfo) 1475 for attachmentInfo in msgData[MI_ATTACHINFO]] 1476 1477 # TODO: Populate additional fields & cache...(?) 1478 1479 # TODO: Handle body differently if it's from a draft? 1480 self.isDraft = isDraft 1481 1482 self._source = None
1483 1484
1485 - def _getSource(self):
1486 """ 1487 """ 1488 if not self._source: 1489 # TODO: Do this more nicely...? 1490 # TODO: Strip initial white space & fix up last line ending 1491 # to make it legal as per RFC? 1492 self._source = self._account.getRawMessage(self.id) 1493 1494 return self._source
1495 1496 source = property(_getSource, doc = "")
1497 1498 1499
1500 -class GmailAttachment:
1501 """ 1502 """ 1503
1504 - def __init__(self, parent, attachmentInfo):
1505 """ 1506 """ 1507 # TODO Handle this better? 1508 self._parent = parent 1509 self._account = self._parent._account 1510 1511 self.id = attachmentInfo[A_ID] 1512 self.filename = attachmentInfo[A_FILENAME] 1513 self.mimetype = attachmentInfo[A_MIMETYPE] 1514 self.filesize = attachmentInfo[A_FILESIZE] 1515 1516 self._content = None
1517 1518
1519 - def _getContent(self):
1520 """ 1521 """ 1522 if not self._content: 1523 # TODO: Do this a more nicely...? 1524 self._content = self._account._retrievePage( 1525 _buildURL(view=U_ATTACHMENT_VIEW, disp="attd", 1526 attid=self.id, th=self._parent._parent.id)) 1527 1528 return self._content
1529 1530 content = property(_getContent, doc = "") 1531 1532
1533 - def _getFullId(self):
1534 """ 1535 1536 Returns the "full path"/"full id" of the attachment. (Used 1537 to refer to the file when forwarding.) 1538 1539 The id is of the form: "<thread_id>_<msg_id>_<attachment_id>" 1540 1541 """ 1542 return "%s_%s_%s" % (self._parent._parent.id, 1543 self._parent.id, 1544 self.id)
1545 1546 _fullId = property(_getFullId, doc = "")
1547 1548 1549
1550 -class GmailComposedMessage:
1551 """ 1552 """ 1553
1554 - def __init__(self, to, subject, body, cc = None, bcc = None, 1555 filenames = None, files = None):
1556 """ 1557 1558 `filenames` - list of the file paths of the files to attach. 1559 `files` - list of objects implementing sub-set of 1560 `email.Message.Message` interface (`get_filename`, 1561 `get_content_type`, `get_payload`). This is to 1562 allow use of payloads from Message instances. 1563 TODO: Change this to be simpler class we define ourselves? 1564 """ 1565 self.to = to 1566 self.subject = subject 1567 self.body = body 1568 self.cc = cc 1569 self.bcc = bcc 1570 self.filenames = filenames 1571 self.files = files
1572 1573 1574 1575 if __name__ == "__main__": 1576 import sys 1577 from getpass import getpass 1578 1579 try: 1580 name = sys.argv[1] 1581 except IndexError: 1582 name = raw_input("Gmail account name: ") 1583 1584 pw = getpass("Password: ") 1585 domain = raw_input("Domain? [leave blank for Gmail]: ") 1586 1587 ga = GmailAccount(name, pw, domain=domain) 1588 1589 print "\nPlease wait, logging in..." 1590 1591 try: 1592 ga.login() 1593 except GmailLoginFailure,e: 1594 print "\nLogin failed. (%s)" % e.message 1595 else: 1596 print "Login successful.\n" 1597 1598 # TODO: Use properties instead? 1599 quotaInfo = ga.getQuotaInfo() 1600 quotaMbUsed = quotaInfo[QU_SPACEUSED] 1601 quotaMbTotal = quotaInfo[QU_QUOTA] 1602 quotaPercent = quotaInfo[QU_PERCENT] 1603 print "%s of %s used. (%s)\n" % (quotaMbUsed, quotaMbTotal, quotaPercent) 1604 1605 searches = STANDARD_FOLDERS + ga.getLabelNames() 1606 name = None 1607 while 1: 1608 try: 1609 print "Select folder or label to list: (Ctrl-C to exit)" 1610 for optionId, optionName in enumerate(searches): 1611 print " %d. %s" % (optionId, optionName) 1612 while not name: 1613 try: 1614 name = searches[int(raw_input("Choice: "))] 1615 except ValueError,info: 1616 print info 1617 name = None 1618 if name in STANDARD_FOLDERS: 1619 result = ga.getMessagesByFolder(name, True) 1620 else: 1621 result = ga.getMessagesByLabel(name, True) 1622 1623 if not len(result): 1624 print "No threads found in `%s`." % name 1625 break 1626 name = None 1627 tot = len(result) 1628 1629 i = 0 1630 for thread in result: 1631 print "%s messages in thread" % len(thread) 1632 print thread.id, len(thread), thread.subject 1633 for msg in thread: 1634 print "\n ", msg.id, msg.number, msg.author,msg.subject 1635 # Just as an example of other usefull things 1636 #print " ", msg.cc, msg.bcc,msg.sender 1637 i += 1 1638 print 1639 print "number of threads:",tot 1640 print "number of messages:",i 1641 except KeyboardInterrupt: 1642 break 1643 1644 print "\n\nDone." 1645