libgmail

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