REMEMBER: ID IS A STRING Returns False if no contact could be found """ idList = self.getContactListById(myId) if len(idList) > 0: return idList[0] else: return False def getContactListByName(self, name): """ This function returns a LIST of GmailContacts whose name is 'name'. Returns an empty list if no contacts were found """ nameList = [] for entry in self.contactList: if entry.getName() == name: nameList.append(entry) return nameList def getContactListByEmail(self, email): """ This function returns a LIST of GmailContacts whose email is 'email'. As of this writing, two contacts cannot share an email address, so this should only return just one item. But it doesn't hurt to be prepared? Returns an empty list if no contacts were found """ emailList = [] for entry in self.contactList: if entry.getEmail() == email: emailList.append(entry) return emailList def getContactListById(self, myId): """ This function returns a LIST of GmailContacts whose id is 'myId'. We expect there only to be one, but just in case! Remember: ID IS A STRING Returns an empty list if no contacts were found """ idList = [] for entry in self.contactList: if entry.getId() == myId: idList.append(entry) return idList class GmailSearchResult: """ """ def __init__(self, account, search, threadsInfo): """ `threadsInfo` -- As returned from Gmail but unbunched. """ #print "\nthreadsInfo\n",threadsInfo try: if not type(threadsInfo[0]) is types.ListType: threadsInfo = [threadsInfo] except IndexError: print "No messages found" self._account = account self.search = search # TODO: Turn into object + format nicely. self._threads = [] for thread in threadsInfo: self._threads.append(GmailThread(self, thread[0])) def __iter__(self): """ """ return iter(self._threads) def __len__(self): """ """ return len(self._threads) def __getitem__(self,key): """ """ return self._threads.__getitem__(key) class GmailSessionState: """ """ def __init__(self, account = None, filename = ""): """ """ if account: self.state = (account.name, account._cookieJar) elif filename: self.state = load(open(filename, "rb")) else: raise ValueError("GmailSessionState must be instantiated with " \ "either GmailAccount object or filename.") def save(self, filename): """ """ dump(self.state, open(filename, "wb"), -1) class _LabelHandlerMixin(object): """ Note: Because a message id can be used as a thread id this works for messages as well as threads. """ def _makeLabelList(self, labelList): self._labels = labelList def addLabel(self, labelName): """ """ # Note: It appears this also automatically creates new labels. result = self._account._doThreadAction(U_ADDCATEGORY_ACTION+labelName, self) if not self._labels: self._makeLabelList([]) # TODO: Caching this seems a little dangerous; suppress duplicates maybe? self._labels.append(labelName) return result def removeLabel(self, labelName): """ """ # TODO: Check label is already attached? # Note: An error is not generated if the label is not already attached. result = \ self._account._doThreadAction(U_REMOVECATEGORY_ACTION+labelName, self) removeLabel = True try: self._labels.remove(labelName) except: removeLabel = False pass # If we don't check both, we might end up in some weird inconsistent state return result and removeLabel def getLabels(self): return self._labels class GmailThread(_LabelHandlerMixin): """ Note: As far as I can tell, the "canonical" thread id is always the same as the id of the last message in the thread. But it appears that the id of any message in the thread can be used to retrieve the thread information. """ def __init__(self, parent, threadsInfo): """ """ # TODO Handle this better? self._parent = parent self._account = self._parent._account self.id = threadsInfo[T_THREADID] # TODO: Change when canonical updated? self.subject = threadsInfo[T_SUBJECT_HTML] self.snippet = threadsInfo[T_SNIPPET_HTML] #self.extraSummary = threadInfo[T_EXTRA_SNIPPET] #TODO: What is this? # TODO: Store other info? # Extract number of messages in thread/conversation. self._authors = threadsInfo[T_AUTHORS_HTML] self.info = threadsInfo try: # TODO: Find out if this information can be found another way... # (Without another page request.) self._length = int(re.search("\((\d+?)\)\Z", self._authors).group(1)) except AttributeError,info: # If there's no message count then the thread only has one message. self._length = 1 # TODO: Store information known about the last message (e.g. id)? self._messages = [] # Populate labels self._makeLabelList(threadsInfo[T_CATEGORIES]) def __getattr__(self, name): """ Dynamically dispatch some interesting thread properties. """ attrs = { 'unread': T_UNREAD, 'star': T_STAR, 'date': T_DATE_HTML, 'authors': T_AUTHORS_HTML, 'flags': T_FLAGS, 'subject': T_SUBJECT_HTML, 'snippet': T_SNIPPET_HTML, 'categories': T_CATEGORIES, 'attach': T_ATTACH_HTML, 'matching_msgid': T_MATCHING_MSGID, 'extra_snippet': T_EXTRA_SNIPPET } if name in attrs: return self.info[ attrs[name] ]; raise AttributeError("no attribute %s" % name) def __len__(self): """ """ return self._length def __iter__(self): """ """ if not self._messages: self._messages = self._getMessages(self) return iter(self._messages) def __getitem__(self, key): """ """ if not self._messages: self._messages = self._getMessages(self) try: result = self._messages.__getitem__(key) except IndexError: result = [] return result def _getMessages(self, thread): """ """ # TODO: Do this better. # TODO: Specify the query folder using our specific search? items = self._account._parseSearchResult(U_QUERY_SEARCH, view = U_CONVERSATION_VIEW, th = thread.id, q = "in:anywhere") result = [] # TODO: Handle this better? # Note: This handles both draft & non-draft messages in a thread... for key, isDraft in [(D_MSGINFO, False), (D_DRAFTINFO, True)]: try: msgsInfo = items[key] except KeyError: # No messages of this type (e.g. draft or non-draft) continue else: # TODO: Handle special case of only 1 message in thread better? if type(msgsInfo[0]) != types.ListType: msgsInfo = [msgsInfo] for msg in msgsInfo: result += [GmailMessage(thread, msg, isDraft = isDraft)] return result class GmailMessageStub(_LabelHandlerMixin): """ Intended to be used where not all message information is known/required. NOTE: This may go away. """ # TODO: Provide way to convert this to a full `GmailMessage` instance # or allow `GmailMessage` to be created without all info? def __init__(self, id = None, _account = None): """ """ self.id = id self._account = _account class GmailMessage(object): """ """ def __init__(self, parent, msgData, isDraft = False): """ Note: `msgData` can be from either D_MSGINFO or D_DRAFTINFO. """ # TODO: Automatically detect if it's a draft or not? # TODO Handle this better? self._parent = parent self._account = self._parent._account self.author = msgData[MI_AUTHORFIRSTNAME] self.id = msgData[MI_MSGID] self.number = msgData[MI_NUM] self.subject = msgData[MI_SUBJECT] self.to = msgData[MI_TO] self.cc = msgData[MI_CC] self.bcc = msgData[MI_BCC] self.sender = msgData[MI_AUTHOREMAIL] self.attachments = [GmailAttachment(self, attachmentInfo) for attachmentInfo in msgData[MI_ATTACHINFO]] # TODO: Populate additional fields & cache...(?) # TODO: Handle body differently if it's from a draft? self.isDraft = isDraft self._source = None def _getSource(self): """ """ if not self._source: # TODO: Do this more nicely...? # TODO: Strip initial white space & fix up last line ending # to make it legal as per RFC? self._source = self._account.getRawMessage(self.id) return self._source source = property(_getSource, doc = "") class GmailAttachment: """ """ def __init__(self, parent, attachmentInfo): """ """ # TODO Handle this better? self._parent = parent self._account = self._parent._account self.id = attachmentInfo[A_ID] self.filename = attachmentInfo[A_FILENAME] self.mimetype = attachmentInfo[A_MIMETYPE] self.filesize = attachmentInfo[A_FILESIZE] self._content = None def _getContent(self): """ """ if not self._content: # TODO: Do this a more nicely...? self._content = self._account._retrievePage( _buildURL(view=U_ATTACHMENT_VIEW, disp="attd", attid=self.id, th=self._parent._parent.id)) return self._content content = property(_getContent, doc = "") def _getFullId(self): """ Returns the "full path"/"full id" of the attachment. (Used to refer to the file when forwarding.) The id is of the form: "__" """ return "%s_%s_%s" % (self._parent._parent.id, self._parent.id, self.id) _fullId = property(_getFullId, doc = "") class GmailComposedMessage: """ """ def __init__(self, to, subject, body, cc = None, bcc = None, filenames = None, files = None): """ `filenames` - list of the file paths of the files to attach. `files` - list of objects implementing sub-set of `email.Message.Message` interface (`get_filename`, `get_content_type`, `get_payload`). This is to allow use of payloads from Message instances. TODO: Change this to be simpler class we define ourselves? """ self.to = to self.subject = subject self.body = body self.cc = cc self.bcc = bcc self.filenames = filenames self.files = files if __name__ == "__main__": import sys from getpass import getpass try: name = sys.argv[1] except IndexError: name = raw_input("Gmail account name: ") pw = getpass("Password: ") ga = GmailAccount(name, pw) print "\nPlease wait, logging in..." try: ga.login() except GmailLoginFailure,e: print "\nLogin failed. (%s)" % e.message else: print "Login successful.\n" # TODO: Use properties instead? quotaInfo = ga.getQuotaInfo() quotaMbUsed = quotaInfo[QU_SPACEUSED] quotaMbTotal = quotaInfo[QU_QUOTA] quotaPercent = quotaInfo[QU_PERCENT] print "%s of %s used. (%s)\n" % (quotaMbUsed, quotaMbTotal, quotaPercent) searches = STANDARD_FOLDERS + ga.getLabelNames() name = None while 1: try: print "Select folder or label to list: (Ctrl-C to exit)" for optionId, optionName in enumerate(searches): print " %d. %s" % (optionId, optionName) while not name: try: name = searches[int(raw_input("Choice: "))] except ValueError,info: print info name = None if name in STANDARD_FOLDERS: result = ga.getMessagesByFolder(name, True) else: result = ga.getMessagesByLabel(name, True) if not len(result): print "No threads found in `%s`." % name break name = None tot = len(result) i = 0 for thread in result: print "%s messages in thread" % len(thread) print thread.id, len(thread), thread.subject for msg in thread: print "\n ", msg.id, msg.number, msg.author,msg.subject # Just as an example of other usefull things #print " ", msg.cc, msg.bcc,msg.sender i += 1 print print "number of threads:",tot print "number of messages:",i except KeyboardInterrupt: break print "\n\nDone."