r7151 - in bigboard/trunk/bigboard/stocks: . mail
- From: commits mugshot org
- To: online-desktop-list gnome org
- Subject: r7151 - in bigboard/trunk/bigboard/stocks: . mail
- Date: Tue, 8 Jan 2008 16:26:18 -0600 (CST)
Author: otaylor
Date: 2008-01-08 16:26:17 -0600 (Tue, 08 Jan 2008)
New Revision: 7151
Added:
bigboard/trunk/bigboard/stocks/mail.xml
bigboard/trunk/bigboard/stocks/mail/
bigboard/trunk/bigboard/stocks/mail/MailStock.py
bigboard/trunk/bigboard/stocks/mail/README.libgmail
bigboard/trunk/bigboard/stocks/mail/lgconstants.py
bigboard/trunk/bigboard/stocks/mail/libgmail-folder-counts.patch
bigboard/trunk/bigboard/stocks/mail/libgmail_patched.py
bigboard/trunk/bigboard/stocks/mail/thumbnail.png
Log:
Add mail stock by Natan Yellin
http://code.google.com/p/google-highly-open-participation-gnome/issues/detail?id=68
Added: bigboard/trunk/bigboard/stocks/mail/MailStock.py
===================================================================
--- bigboard/trunk/bigboard/stocks/mail/MailStock.py 2008-01-08 21:47:04 UTC (rev 7150)
+++ bigboard/trunk/bigboard/stocks/mail/MailStock.py 2008-01-08 22:26:17 UTC (rev 7151)
@@ -0,0 +1,210 @@
+import logging, re, htmlentitydefs
+
+import gobject, gtk
+import hippo
+
+from bigboard.stock import Stock
+from bigboard.slideout import ThemedSlideout
+import bigboard.google as google
+import bigboard.google_stock as google_stock
+from bigboard.big_widgets import CanvasHBox, CanvasVBox, Button, Header, ThemedText, PrelightingCanvasBox
+#TODO: add a scrollable view for emails
+#import bigboard.scroll_ribbon as scroll_ribbon
+
+import libgmail_patched as libgmail
+
+_logger = logging.getLogger('bigboard.stocks.MailStock')
+
+def remove_strange_tags(s, markup=False):
+ if "\\u003cb\\>" in s:
+ if markup == True:
+ b = "<b>"
+ e = "</b>"
+ else:
+ b = ""
+ e = ""
+ s = s.replace("\\u003cb\\>", b)
+ s = s.replace("\\u003c/b\\>", e)
+ return s
+
+def convert_entities(s):
+ exp = re.compile("&[#a-zA-Z0-9]*;")
+ for match in exp.finditer(s):
+ if match is not None:
+ html_entity = match.group()
+ try:
+ if html_entity[1] == '#':
+ entity_num = int(html_entity[2:-1])
+ replacement_entity = unichr(entity_num)
+ else:
+ entity_str = html_entity[1:-1]
+ replacement_entity = unichr(htmlentitydefs.name2codepoint[entity_str])
+ s = s.replace(html_entity, replacement_entity)
+ except KeyError:
+ pass
+ return s
+
+class LabelSlideout(ThemedSlideout):
+ __gsignals__ = {
+ 'changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_STRING, )),
+ }
+ def __init__(self, ga):
+ super(LabelSlideout, self).__init__()
+ vbox = CanvasVBox(border_color=0x0000000ff, spacing=4)
+ self.get_root().append(vbox)
+ header = Header(topborder=False)
+ account_text = ThemedText(theme_hints=['header'], text=ga.name, font="14px Bold")
+ header.append(account_text, hippo.PACK_EXPAND)
+ vbox.append(header)
+ folderCounts = ga.getFolderCounts()
+ folderCounts["unread"] = ga.getUnreadMsgCount()
+ for label, number in folderCounts.iteritems():
+ box = PrelightingCanvasBox()
+ box.connect('button-release-event', self.on_button_release_event, label)
+ vbox.append(box)
+ hbox = CanvasHBox(spacing=4, padding=4)
+ text= hippo.CanvasText(text=label, xalign=hippo.ALIGNMENT_START)
+ hbox.append(text)
+ text= hippo.CanvasText(text=number, xalign=hippo.ALIGNMENT_START)
+ hbox.append(text, flags=hippo.PACK_END)
+ box.append(hbox)
+
+ def on_button_release_event (self, hippo_item, event, label_text):
+ self.emit('changed', label_text)
+
+class EmailSlideout(ThemedSlideout):
+ def __init__(self, thread):
+ super(EmailSlideout, self).__init__()
+ vbox = CanvasVBox(border_color=0x0000000ff, spacing=4)
+ self.get_root().append(vbox)
+ self.__header = Header(topborder=False)
+
+ subject = remove_strange_tags(thread.subject)
+
+ subject_box = ThemedText(theme_hints=['header'], text=subject, font="14px Bold")
+ self.__header.append(subject_box, hippo.PACK_EXPAND)
+ vbox.append(self.__header)
+
+ for key in ("date", "categories", "snippet"):
+ value = getattr(thread, key, None)
+ if value:
+ if type(value) is list:
+ s = ", ".join(value)
+ if type(value) is str:
+ s = remove_strange_tags(value)
+
+ s = convert_entities(s)
+ box = hippo.CanvasText(text=s, xalign=hippo.ALIGNMENT_START)
+ vbox.append(box)
+
+ #todo: nicify email, strip out junk, and show actual email
+ #email_source = thread[len(thread)-1].source
+ #we could use a regular expression, but its not so simple.
+ #exp = "^\\nReceived:.?\\nMessage-ID:.?\\nDate:.?\\nFrom:.?\\nTo:.?\\nSubject:.?\\n"
+ # the following doesn't always work
+ #psr = email.parser.Parser()
+ #print psr.parsestr(email_source).get_payload()
+
+class MailStock(Stock, google_stock.GoogleStock):
+ """Shows recent emails"""
+ def __init__(self, *args, **kwargs):
+ print "starting mail stock"
+ Stock.__init__(self, *args, **kwargs)
+ google_stock.GoogleStock.__init__(self, 'gmail', **kwargs)
+
+ self._box = hippo.CanvasBox(orientation=hippo.ORIENTATION_VERTICAL, spacing=4, padding_top=2)
+
+ self.__slideout = None
+
+ self.__google_account = None
+ self.__folder = 'inbox'
+
+ self.__display_limit = 4
+
+ button = self._create_login_button()
+ self._box.append(button)
+
+ self._add_more_button(self.__on_more_button)
+
+ def get_content(self, size):
+ return self._box
+
+ def update_google_data(self, gobj):
+ username = gobj.get_account().get_username_as_google_email()
+ password = gobj.get_account().get_password()
+ self.__update_email_box (username, password)
+
+ def __update_email_box (self, username, password):
+ self._box.remove_all()
+
+ try:
+ if self.__google_account is None or username != self.__google_account.name:
+ self.__google_account = libgmail.GmailAccount(username, password)
+ self.__google_account.login()
+
+ account = ThemedText(theme_hints=['header'], text=self.__google_account.name, font="14px Bold Italic")
+ self._box.append(account)
+
+ box = PrelightingCanvasBox()
+ box.connect("button-release-event", self.create_label_slideout, self.__google_account)
+ self._box.append(box)
+ label = hippo.CanvasText(text=self.__folder, font="14px Bold Italic")
+ box.append(label)
+
+ if self.__folder == 'inbox':
+ threads = self.__google_account.getMessagesByFolder(self.__folder)
+
+ elif self.__folder == 'unread':
+ threads = self.__google_account.getUnreadMessages()
+ else:
+ threads = self.__google_account.getMessagesByLabel(self.__folder)
+
+ i = 0
+ for thread in threads:
+ if i >= self.__display_limit: break
+
+ subject = remove_strange_tags(thread.subject, True)
+
+ box = PrelightingCanvasBox()
+ box.connect("button-release-event", self.create_email_slideout, thread)
+ self._box.append(box)
+ email = hippo.CanvasText(markup=subject, xalign=hippo.ALIGNMENT_START)
+ box.append(email)
+ i += 1
+ labelsDict = self.__google_account.getFolderCounts()
+ footer = ThemedText(theme_hints=['footer'], text="%s unread" % labelsDict['inbox'], font="14px Bold Italic")
+ self._box.append(footer)
+ print "updated mailbox"
+
+ except libgmail.GmailLoginFailure:
+ error = hippo.CanvasText(text="Error: Could not connect to gmail.", size_mode=hippo.CANVAS_SIZE_WRAP_WORD)
+ self._box.append(error)
+
+ def show_slideout(self, widget):
+ def on_slideout_close(s, action_taken):
+ if action_taken:
+ self._panel.action_taken()
+ s.destroy()
+ self.__slideout = None
+ self.__slideout.connect('close', on_slideout_close)
+ y = widget.get_context().translate_to_screen(widget)[1]
+ if not self.__slideout.slideout_from(204, y):
+ self.__slideout.destroy()
+ self.__slideout = None
+ return
+
+ def create_label_slideout(self, widget, hippo_event, data):
+ self.__slideout = LabelSlideout(data)
+ self.__slideout.connect('changed', self.on_label_changed)
+ self.show_slideout(widget)
+
+ def create_email_slideout(self, widget, hippo_event, data):
+ self.__slideout = EmailSlideout(data)
+ self.show_slideout(widget)
+
+ def on_label_changed (self, slideout, label):
+ self.__folder = label
+ self.__update_email_box(self.__google_account.name, None)
+
+ def __on_more_button(self):
+ libbig.show_url("http://mail.google.com/mail")
Added: bigboard/trunk/bigboard/stocks/mail/README.libgmail
===================================================================
--- bigboard/trunk/bigboard/stocks/mail/README.libgmail 2008-01-08 21:47:04 UTC (rev 7150)
+++ bigboard/trunk/bigboard/stocks/mail/README.libgmail 2008-01-08 22:26:17 UTC (rev 7151)
@@ -0,0 +1,47 @@
+libgmail is licensed under the GPL.
+See the file named COPYING for more information.
+
+Please refer to the libgmail website or project page at sourceforge if
+you encounter problems using libgmail.
+http://libgmail.sf.net/
+http://sourceforge.net/projects/libgmail/
+
+You can contact us by email:
+libgmail-developer lists sf net,
+or, individually at
+stas AT linux DOT isbeter DOT nl
+wdaher AT mit DOT edu
+follower AT myrealbox DOT com
+
+-----------------------------------------------
+Possible usage:
+
+Run this:
+
+ python libgmail.py
+
+When you have the demos package installed you could do this:
+
+ python demos/archive.py
+
+or even this:
+
+ python demos/sendmsg.py <account> <to address> <subject> <body>
+
+or perhaps this:
+
+ python demos/gmailsmtp.py # (Then connect to SMTP proxy on local port 8025)
+
+or how about this:
+
+ python demos/gmailftpd.py # (Then connect to FTP proxy on local port 8021,
+ # after creating a label named 'ftp' and
+ # applying it to some messages with attachments.)
+
+or maybe this:
+
+ python demos/gmailpopd.py # (Then connect to POP3 proxy on local port 8110)
+
+for hours of fun!(*)
+
+(*) Note: Fun may not last for hours. Use at your own risk, blah, blah, etc...
\ No newline at end of file
Added: bigboard/trunk/bigboard/stocks/mail/lgconstants.py
===================================================================
--- bigboard/trunk/bigboard/stocks/mail/lgconstants.py 2008-01-08 21:47:04 UTC (rev 7150)
+++ bigboard/trunk/bigboard/stocks/mail/lgconstants.py 2008-01-08 22:26:17 UTC (rev 7151)
@@ -0,0 +1,231 @@
+#
+# Generated file -- DO NOT EDIT
+#
+# Note: This file is now edited! 2005-04-25
+#
+# constants.py -- Useful constants extracted from Gmail Javascript code
+#
+# Source version: 44f09303f2d4f76f
+#
+# Generated: 2004-08-10 13:08 UTC
+#
+
+
+URL_LOGIN = "https://www.google.com/accounts/ServiceLoginBoxAuth"
+URL_GMAIL = "https://mail.google.com/mail/"
+
+
+# Constants with names not from the Gmail Javascript:
+U_SAVEDRAFT_VIEW = "sd"
+
+D_DRAFTINFO = "di"
+# NOTE: All other DI_* field offsets seem to match the MI_* field offsets
+DI_BODY = 19
+
+versionWarned = False # If the Javascript version is different have we
+ # warned about it?
+
+
+js_version = '44f09303f2d4f76f'
+
+D_VERSION = "v"
+D_QUOTA = "qu"
+D_DEFAULTSEARCH_SUMMARY = "ds"
+D_THREADLIST_SUMMARY = "ts"
+D_THREADLIST_END = "te"
+D_THREAD = "t"
+D_CONV_SUMMARY = "cs"
+D_CONV_END = "ce"
+D_MSGINFO = "mi"
+D_MSGBODY = "mb"
+D_MSGATT = "ma"
+D_COMPOSE = "c"
+D_CONTACT = "co"
+D_CATEGORIES = "ct"
+D_CATEGORIES_COUNT_ALL = "cta"
+D_ACTION_RESULT = "ar"
+D_SENDMAIL_RESULT = "sr"
+D_PREFERENCES = "p"
+D_PREFERENCES_PANEL = "pp"
+D_FILTERS = "fi"
+D_GAIA_NAME = "gn"
+D_INVITE_STATUS = "i"
+D_END_PAGE = "e"
+D_LOADING = "l"
+D_LOADED_SUCCESS = "ld"
+D_LOADED_ERROR = "le"
+D_QUICKLOADED = "ql"
+QU_SPACEUSED = 0
+QU_QUOTA = 1
+QU_PERCENT = 2
+QU_COLOR = 3
+TS_START = 0
+TS_NUM = 1
+TS_TOTAL = 2
+TS_ESTIMATES = 3
+TS_TITLE = 4
+TS_TIMESTAMP = 5 + 1
+TS_TOTAL_MSGS = 6 + 1
+T_THREADID = 0
+T_UNREAD = 1
+T_STAR = 2
+T_DATE_HTML = 3
+T_AUTHORS_HTML = 4
+T_FLAGS = 5
+T_SUBJECT_HTML = 6
+T_SNIPPET_HTML = 7
+T_CATEGORIES = 8
+T_ATTACH_HTML = 9
+T_MATCHING_MSGID = 10
+T_EXTRA_SNIPPET = 11
+CS_THREADID = 0
+CS_SUBJECT = 1
+CS_TITLE_HTML = 2
+CS_SUMMARY_HTML = 3
+CS_CATEGORIES = 4
+CS_PREVNEXTTHREADIDS = 5
+CS_THREAD_UPDATED = 6
+CS_NUM_MSGS = 7
+CS_ADKEY = 8
+CS_MATCHING_MSGID = 9
+MI_FLAGS = 0
+MI_NUM = 1
+MI_MSGID = 2
+MI_STAR = 3
+MI_REFMSG = 4
+MI_AUTHORNAME = 5
+MI_AUTHORFIRSTNAME = 6 # ? -- Name supplied by rj
+MI_AUTHOREMAIL = 6 + 1
+MI_MINIHDRHTML = 7 + 1
+MI_DATEHTML = 8 + 1
+MI_TO = 9 + 1
+MI_CC = 10 + 1
+MI_BCC = 11 + 1
+MI_REPLYTO = 12 + 1
+MI_DATE = 13 + 1
+MI_SUBJECT = 14 + 1
+MI_SNIPPETHTML = 15 + 1
+MI_ATTACHINFO = 16 + 1
+MI_KNOWNAUTHOR = 17 + 1
+MI_PHISHWARNING = 18 + 1
+A_ID = 0
+A_FILENAME = 1
+A_MIMETYPE = 2
+A_FILESIZE = 3
+CT_NAME = 0
+CT_COUNT = 1
+AR_SUCCESS = 0
+AR_MSG = 1
+SM_COMPOSEID = 0
+SM_SUCCESS = 1
+SM_MSG = 2
+SM_NEWTHREADID = 3
+CMD_SEARCH = "SEARCH"
+ACTION_TOKEN_COOKIE = "GMAIL_AT"
+U_VIEW = "view"
+U_PAGE_VIEW = "page"
+U_THREADLIST_VIEW = "tl"
+U_CONVERSATION_VIEW = "cv"
+U_COMPOSE_VIEW = "cm"
+U_PRINT_VIEW = "pt"
+U_PREFERENCES_VIEW = "pr"
+U_JSREPORT_VIEW = "jr"
+U_UPDATE_VIEW = "up"
+U_SENDMAIL_VIEW = "sm"
+U_AD_VIEW = "ad"
+U_REPORT_BAD_RELATED_INFO_VIEW = "rbri"
+U_ADDRESS_VIEW = "address"
+U_ADDRESS_IMPORT_VIEW = "ai"
+U_SPELLCHECK_VIEW = "sc"
+U_INVITE_VIEW = "invite"
+U_ORIGINAL_MESSAGE_VIEW = "om"
+U_ATTACHMENT_VIEW = "att"
+U_DEBUG_ADS_RESPONSE_VIEW = "da"
+U_SEARCH = "search"
+U_INBOX_SEARCH = "inbox"
+U_STARRED_SEARCH = "starred"
+U_ALL_SEARCH = "all"
+U_DRAFTS_SEARCH = "drafts"
+U_SENT_SEARCH = "sent"
+U_SPAM_SEARCH = "spam"
+U_TRASH_SEARCH = "trash"
+U_QUERY_SEARCH = "query"
+U_ADVANCED_SEARCH = "adv"
+U_CREATEFILTER_SEARCH = "cf"
+U_CATEGORY_SEARCH = "cat"
+U_AS_FROM = "as_from"
+U_AS_TO = "as_to"
+U_AS_SUBJECT = "as_subj"
+U_AS_SUBSET = "as_subset"
+U_AS_HAS = "as_has"
+U_AS_HASNOT = "as_hasnot"
+U_AS_ATTACH = "as_attach"
+U_AS_WITHIN = "as_within"
+U_AS_DATE = "as_date"
+U_AS_SUBSET_ALL = "all"
+U_AS_SUBSET_INBOX = "inbox"
+U_AS_SUBSET_STARRED = "starred"
+U_AS_SUBSET_SENT = "sent"
+U_AS_SUBSET_DRAFTS = "drafts"
+U_AS_SUBSET_SPAM = "spam"
+U_AS_SUBSET_TRASH = "trash"
+U_AS_SUBSET_ALLSPAMTRASH = "ast"
+U_AS_SUBSET_READ = "read"
+U_AS_SUBSET_UNREAD = "unread"
+U_AS_SUBSET_CATEGORY_PREFIX = "cat_"
+U_THREAD = "th"
+U_PREV_THREAD = "prev"
+U_NEXT_THREAD = "next"
+U_DRAFT_MSG = "draft"
+U_START = "start"
+U_ACTION = "act"
+U_ACTION_TOKEN = "at"
+U_INBOX_ACTION = "ib"
+U_MARKREAD_ACTION = "rd"
+U_MARKUNREAD_ACTION = "ur"
+U_MARKSPAM_ACTION = "sp"
+U_UNMARKSPAM_ACTION = "us"
+U_MARKTRASH_ACTION = "tr"
+U_ADDCATEGORY_ACTION = "ac_"
+U_REMOVECATEGORY_ACTION = "rc_"
+U_ADDSTAR_ACTION = "st"
+U_REMOVESTAR_ACTION = "xst"
+U_ADDSENDERTOCONTACTS_ACTION = "astc"
+U_DELETEMESSAGE_ACTION = "dm"
+U_DELETE_ACTION = "dl"
+U_EMPTYSPAM_ACTION = "es_"
+U_EMPTYTRASH_ACTION = "et_"
+U_SAVEPREFS_ACTION = "prefs"
+U_ADDRESS_ACTION = "a"
+U_CREATECATEGORY_ACTION = "cc_"
+U_DELETECATEGORY_ACTION = "dc_"
+U_RENAMECATEGORY_ACTION = "nc_"
+U_CREATEFILTER_ACTION = "cf"
+U_REPLACEFILTER_ACTION = "rf"
+U_DELETEFILTER_ACTION = "df_"
+U_ACTION_THREAD = "t"
+U_ACTION_MESSAGE = "m"
+U_ACTION_PREF_PREFIX = "p_"
+U_REFERENCED_MSG = "rm"
+U_COMPOSEID = "cmid"
+U_COMPOSE_MODE = "cmode"
+U_COMPOSE_SUBJECT = "su"
+U_COMPOSE_TO = "to"
+U_COMPOSE_CC = "cc"
+U_COMPOSE_BCC = "bcc"
+U_COMPOSE_BODY = "body"
+U_PRINT_THREAD = "pth"
+CONV_VIEW = "conv"
+TLIST_VIEW = "tlist"
+PREFS_VIEW = "prefs"
+HIST_VIEW = "hist"
+COMPOSE_VIEW = "comp"
+HIDDEN_ACTION = 0
+USER_ACTION = 1
+BACKSPACE_ACTION = 2
+
+# TODO: Get these on the fly?
+STANDARD_FOLDERS = [U_INBOX_SEARCH, U_STARRED_SEARCH,
+ U_ALL_SEARCH, U_DRAFTS_SEARCH,
+ U_SENT_SEARCH, U_SPAM_SEARCH]
+
Added: bigboard/trunk/bigboard/stocks/mail/libgmail-folder-counts.patch
===================================================================
--- bigboard/trunk/bigboard/stocks/mail/libgmail-folder-counts.patch 2008-01-08 21:47:04 UTC (rev 7150)
+++ bigboard/trunk/bigboard/stocks/mail/libgmail-folder-counts.patch 2008-01-08 22:26:17 UTC (rev 7151)
@@ -0,0 +1,52 @@
+This is a patch to libgmail to add the ability to the count of unread messages in
+each "folder' (labels and standard folders such as "inbox").
+
+http://sourceforge.net/tracker/index.php?func=detail&aid=1852698&group_id=113492&atid=665333
+
+Copyright Red Hat, Inc. 2007 and licensed under the same terms as libgmail. (The GPL)
+
+Index: libgmail.py
+===================================================================
+RCS file: /cvsroot/libgmail/libgmail/libgmail.py,v
+retrieving revision 1.100
+diff -u -p -r1.100 libgmail.py
+--- libgmail.py 19 Nov 2007 01:23:59 -0000 1.100
++++ libgmail.py 17 Dec 2007 22:08:53 -0000
+@@ -320,6 +320,7 @@ class GmailAccount:
+
+ self._cachedQuotaInfo = None
+ self._cachedLabelNames = None
++ self._cachedFolderCounts = None
+
+
+ def login(self):
+@@ -411,6 +412,11 @@ class GmailAccount:
+
+ try:
+ self._cachedLabelNames = [category[CT_NAME] for category in items[D_CATEGORIES][0]]
++ self._cachedFolderCounts = {}
++ for category in items[D_CATEGORIES][0]:
++ self._cachedFolderCounts[category[CT_NAME]] = category[CT_COUNT]
++ for summary in items[D_DEFAULTSEARCH_SUMMARY][0]:
++ self._cachedFolderCounts[summary[CT_NAME]] = summary[CT_COUNT]
+ except KeyError:
+ pass
+
+@@ -518,6 +524,17 @@ class GmailAccount:
+ return self._cachedLabelNames
+
+
++ def getFolderCounts(self, refresh = False):
++ """
++ """
++ # TODO: Change this to a property?
++ if not self._cachedFolderCounts or refresh:
++ # TODO: Handle this better...
++ self.getMessagesByFolder(U_INBOX_SEARCH)
++
++ return self._cachedFolderCounts
++
++
+ def getMessagesByLabel(self, label, allPages = False):
+ """
+ """
Added: bigboard/trunk/bigboard/stocks/mail/libgmail_patched.py
===================================================================
--- bigboard/trunk/bigboard/stocks/mail/libgmail_patched.py 2008-01-08 21:47:04 UTC (rev 7150)
+++ bigboard/trunk/bigboard/stocks/mail/libgmail_patched.py 2008-01-08 22:26:17 UTC (rev 7151)
@@ -0,0 +1,1676 @@
+#!/usr/bin/env python
+#
+# libgmail -- Gmail access via Python
+#
+## To get the version number of the available libgmail version.
+## Reminder: add date before next release. This attribute is also
+## used in the setup script.
+Version = '0.1.8' # (Nov 2007)
+
+# Original author: follower myrealbox com
+# Maintainers: Waseem (wdaher mit edu) and Stas Z (stas linux isbeter nl)
+#
+# License: GPL 2.0
+#
+# NOTE:
+# You should ensure you are permitted to use this script before using it
+# to access Google's Gmail servers.
+#
+#
+# Gmail Implementation Notes
+# ==========================
+#
+# * Folders contain message threads, not individual messages. At present I
+# do not know any way to list all messages without processing thread list.
+#
+
+LG_DEBUG=0
+from lgconstants import *
+
+import os,pprint
+import re
+import urllib
+import urllib2
+import mimetypes
+import types
+from cPickle import load, dump
+
+from email.MIMEBase import MIMEBase
+from email.MIMEText import MIMEText
+from email.MIMEMultipart import MIMEMultipart
+
+GMAIL_URL_LOGIN = "https://www.google.com/accounts/ServiceLoginBoxAuth"
+GMAIL_URL_GMAIL = "https://mail.google.com/mail/?ui=1&"
+
+# Set to any value to use proxy.
+PROXY_URL = None # e.g. libgmail.PROXY_URL = 'myproxy.org:3128'
+
+# TODO: Get these on the fly?
+STANDARD_FOLDERS = [U_INBOX_SEARCH, U_STARRED_SEARCH,
+ U_ALL_SEARCH, U_DRAFTS_SEARCH,
+ U_SENT_SEARCH, U_SPAM_SEARCH]
+
+# Constants with names not from the Gmail Javascript:
+# TODO: Move to `lgconstants.py`?
+U_SAVEDRAFT_VIEW = "sd"
+
+D_DRAFTINFO = "di"
+# NOTE: All other DI_* field offsets seem to match the MI_* field offsets
+DI_BODY = 19
+
+versionWarned = False # If the Javascript version is different have we
+ # warned about it?
+
+
+RE_SPLIT_PAGE_CONTENT = re.compile("D\((.*?)\);", re.DOTALL)
+
+class GmailError(Exception):
+ '''
+ Exception thrown upon gmail-specific failures, in particular a
+ failure to log in and a failure to parse responses.
+
+ '''
+ pass
+
+class GmailSendError(Exception):
+ '''
+ Exception to throw if we're unable to send a message
+ '''
+ pass
+
+def _parsePage(pageContent):
+ """
+ Parse the supplied HTML page and extract useful information from
+ the embedded Javascript.
+
+ """
+ lines = pageContent.splitlines()
+ data = '\n'.join([x for x in lines if x and x[0] in ['D', ')', ',', ']']])
+ #data = data.replace(',,',',').replace(',,',',')
+ data = re.sub(',{2,}', ',', data)
+
+ result = []
+ try:
+ exec data in {'__builtins__': None}, {'D': lambda x: result.append(x)}
+ except SyntaxError,info:
+ print info
+ raise GmailError, 'Failed to parse data returned from gmail.'
+
+ items = result
+ itemsDict = {}
+ namesFoundTwice = []
+ for item in items:
+ name = item[0]
+ try:
+ parsedValue = item[1:]
+ except Exception:
+ parsedValue = ['']
+ if itemsDict.has_key(name):
+ # This handles the case where a name key is used more than
+ # once (e.g. mail items, mail body etc) and automatically
+ # places the values into list.
+ # TODO: Check this actually works properly, it's early... :-)
+
+ if len(parsedValue) and type(parsedValue[0]) is types.ListType:
+ for item in parsedValue:
+ itemsDict[name].append(item)
+ else:
+ itemsDict[name].append(parsedValue)
+ else:
+ if len(parsedValue) and type(parsedValue[0]) is types.ListType:
+ itemsDict[name] = []
+ for item in parsedValue:
+ itemsDict[name].append(item)
+ else:
+ itemsDict[name] = [parsedValue]
+
+ return itemsDict
+
+def _splitBunches(infoItems):# Is this still needed ?? Stas
+ """
+ Utility to help make it easy to iterate over each item separately,
+ even if they were bunched on the page.
+ """
+ result= []
+ # TODO: Decide if this is the best approach.
+ for group in infoItems:
+ if type(group) == tuple:
+ result.extend(group)
+ else:
+ result.append(group)
+ return result
+
+class SmartRedirectHandler(urllib2.HTTPRedirectHandler):
+ def __init__(self, cookiejar):
+ self.cookiejar = cookiejar
+
+ def http_error_302(self, req, fp, code, msg, headers):
+ # The location redirect doesn't seem to change
+ # the hostname header appropriately, so we do
+ # by hand. (Is this a bug in urllib2?)
+ new_host = re.match(r'http[s]*://(.*?\.google\.com)',
+ headers.getheader('Location'))
+ if new_host:
+ req.add_header("Host", new_host.groups()[0])
+ result = urllib2.HTTPRedirectHandler.http_error_302(
+ self, req, fp, code, msg, headers)
+ return result
+
+class CookieJar:
+ """
+ A rough cookie handler, intended to only refer to one domain.
+
+ Does no expiry or anything like that.
+
+ (The only reason this is here is so I don't have to require
+ the `ClientCookie` package.)
+
+ """
+
+ def __init__(self):
+ """
+ """
+ self._cookies = {}
+
+
+ def extractCookies(self, headers, nameFilter = None):
+ """
+ """
+ # TODO: Do this all more nicely?
+ for cookie in headers.getheaders('Set-Cookie'):
+ name, value = (cookie.split("=", 1) + [""])[:2]
+ if LG_DEBUG: print "Extracted cookie `%s`" % (name)
+ if not nameFilter or name in nameFilter:
+ self._cookies[name] = value.split(";")[0]
+ if LG_DEBUG: print "Stored cookie `%s` value `%s`" % (name, self._cookies[name])
+ if self._cookies[name] == "EXPIRED":
+ if LG_DEBUG:
+ print "We got an expired cookie: %s:%s, deleting." % (name, self._cookies[name])
+ del self._cookies[name]
+
+
+ def addCookie(self, name, value):
+ """
+ """
+ self._cookies[name] = value
+
+
+ def setCookies(self, request):
+ """
+ """
+ request.add_header('Cookie',
+ ";".join(["%s=%s" % (k,v)
+ for k,v in self._cookies.items()]))
+
+
+
+def _buildURL(**kwargs):
+ """
+ """
+ return "%s%s" % (URL_GMAIL, urllib.urlencode(kwargs))
+
+
+
+def _paramsToMime(params, filenames, files):
+ """
+ """
+ mimeMsg = MIMEMultipart("form-data")
+
+ for name, value in params.iteritems():
+ mimeItem = MIMEText(value)
+ mimeItem.add_header("Content-Disposition", "form-data", name=name)
+
+ # TODO: Handle this better...?
+ for hdr in ['Content-Type','MIME-Version','Content-Transfer-Encoding']:
+ del mimeItem[hdr]
+
+ mimeMsg.attach(mimeItem)
+
+ if filenames or files:
+ filenames = filenames or []
+ files = files or []
+ for idx, item in enumerate(filenames + files):
+ # TODO: This is messy, tidy it...
+ if isinstance(item, str):
+ # We assume it's a file path...
+ filename = item
+ contentType = mimetypes.guess_type(filename)[0]
+ payload = open(filename, "rb").read()
+ else:
+ # We assume it's an `email.Message.Message` instance...
+ # TODO: Make more use of the pre-encoded information?
+ filename = item.get_filename()
+ contentType = item.get_content_type()
+ payload = item.get_payload(decode=True)
+
+ if not contentType:
+ contentType = "application/octet-stream"
+
+ mimeItem = MIMEBase(*contentType.split("/"))
+ mimeItem.add_header("Content-Disposition", "form-data",
+ name="file%s" % idx, filename=filename)
+ # TODO: Encode the payload?
+ mimeItem.set_payload(payload)
+
+ # TODO: Handle this better...?
+ for hdr in ['MIME-Version','Content-Transfer-Encoding']:
+ del mimeItem[hdr]
+
+ mimeMsg.attach(mimeItem)
+
+ del mimeMsg['MIME-Version']
+
+ return mimeMsg
+
+
+class GmailLoginFailure(Exception):
+ """
+ Raised whenever the login process fails--could be wrong username/password,
+ or Gmail service error, for example.
+ Extract the error message like this:
+ try:
+ foobar
+ except GmailLoginFailure,e:
+ mesg = e.message# or
+ print e# uses the __str__
+ """
+ def __init__(self,message):
+ self.message = message
+ def __str__(self):
+ return repr(self.message)
+
+class GmailAccount:
+ """
+ """
+
+ def __init__(self, name = "", pw = "", state = None, domain = None):
+ global URL_LOGIN, URL_GMAIL
+ """
+ """
+ self.domain = domain
+ if self.domain:
+ URL_LOGIN = "https://www.google.com/a/" + self.domain + "/LoginAction"
+ URL_GMAIL = "http://mail.google.com/a/" + self.domain + "/?"
+ else:
+ URL_LOGIN = GMAIL_URL_LOGIN
+ URL_GMAIL = GMAIL_URL_GMAIL
+ if name and pw:
+ self.name = name
+ self._pw = pw
+ self._cookieJar = CookieJar()
+
+ if PROXY_URL is not None:
+ import gmail_transport
+
+ self.opener = urllib2.build_opener(gmail_transport.ConnectHTTPHandler(proxy = PROXY_URL),
+ gmail_transport.ConnectHTTPSHandler(proxy = PROXY_URL),
+ SmartRedirectHandler(self._cookieJar))
+ else:
+ self.opener = urllib2.build_opener(
+ urllib2.HTTPHandler(debuglevel=0),
+ urllib2.HTTPSHandler(debuglevel=0),
+ SmartRedirectHandler(self._cookieJar))
+ elif state:
+ # TODO: Check for stale state cookies?
+ self.name, self._cookieJar = state.state
+ else:
+ raise ValueError("GmailAccount must be instantiated with " \
+ "either GmailSessionState object or name " \
+ "and password.")
+
+ self._cachedQuotaInfo = None
+ self._cachedLabelNames = None
+ self._cachedFolderCounts = None
+
+
+ def login(self):
+ """
+ """
+ # TODO: Throw exception if we were instantiated with state?
+ if self.domain:
+ data = urllib.urlencode({'continue': URL_GMAIL,
+ 'at' : 'null',
+ 'service' : 'mail',
+ 'userName': self.name,
+ 'password': self._pw,
+ })
+ else:
+ data = urllib.urlencode({'continue': URL_GMAIL,
+ 'Email': self.name,
+ 'Passwd': self._pw,
+ })
+
+ headers = {'Host': 'www.google.com',
+ 'User-Agent': 'Mozilla/5.0 (Compatible; libgmail-python)'}
+
+ req = urllib2.Request(URL_LOGIN, data=data, headers=headers)
+ pageData = self._retrievePage(req)
+
+ if not self.domain:
+ # The GV cookie no longer comes in this page for
+ # "Apps", so this bottom portion is unnecessary for it.
+ # This requests the page that provides the required "GV" cookie.
+ RE_PAGE_REDIRECT = 'CheckCookie\?continue=([^"\']+)'
+
+ # TODO: Catch more failure exceptions here...?
+ try:
+ link = re.search(RE_PAGE_REDIRECT, pageData).group(1)
+ redirectURL = urllib2.unquote(link)
+ redirectURL = redirectURL.replace('\\x26', '&')
+
+ except AttributeError:
+ raise GmailLoginFailure("Login failed. (Wrong username/password?)")
+ # We aren't concerned with the actual content of this page,
+ # just the cookie that is returned with it.
+ pageData = self._retrievePage(redirectURL)
+
+ def _retrievePage(self, urlOrRequest):
+ """
+ """
+ if self.opener is None:
+ raise "Cannot find urlopener"
+
+ if not isinstance(urlOrRequest, urllib2.Request):
+ req = urllib2.Request(urlOrRequest)
+ else:
+ req = urlOrRequest
+
+ self._cookieJar.setCookies(req)
+ req.add_header('User-Agent',
+ 'Mozilla/5.0 (Compatible; libgmail-python)')
+
+ try:
+ resp = self.opener.open(req)
+ except urllib2.HTTPError,info:
+ print info
+ return None
+ pageData = resp.read()
+
+ # Extract cookies here
+ self._cookieJar.extractCookies(resp.headers)
+
+ # TODO: Enable logging of page data for debugging purposes?
+
+ return pageData
+
+
+ def _parsePage(self, urlOrRequest):
+ """
+ Retrieve & then parse the requested page content.
+
+ """
+ items = _parsePage(self._retrievePage(urlOrRequest))
+ # Automatically cache some things like quota usage.
+ # TODO: Cache more?
+ # TODO: Expire cached values?
+ # TODO: Do this better.
+ try:
+ self._cachedQuotaInfo = items[D_QUOTA]
+ except KeyError:
+ pass
+ #pprint.pprint(items)
+
+ try:
+ self._cachedLabelNames = [category[CT_NAME] for category in items[D_CATEGORIES][0]]
+ self._cachedFolderCounts = {}
+ for category in items[D_CATEGORIES][0]:
+ self._cachedFolderCounts[category[CT_NAME]] = category[CT_COUNT]
+ for summary in items[D_DEFAULTSEARCH_SUMMARY][0]:
+ self._cachedFolderCounts[summary[CT_NAME]] = summary[CT_COUNT]
+ except KeyError:
+ pass
+
+ return items
+
+
+ def _parseSearchResult(self, searchType, start = 0, **kwargs):
+ """
+ """
+ params = {U_SEARCH: searchType,
+ U_START: start,
+ U_VIEW: U_THREADLIST_VIEW,
+ }
+ params.update(kwargs)
+ return self._parsePage(_buildURL(**params))
+
+
+ def _parseThreadSearch(self, searchType, allPages = False, **kwargs):
+ """
+
+ Only works for thread-based results at present. # TODO: Change this?
+ """
+ start = 0
+ tot = 0
+ threadsInfo = []
+ # Option to get *all* threads if multiple pages are used.
+ while (start == 0) or (allPages and
+ len(threadsInfo) < threadListSummary[TS_TOTAL]):
+
+ items = self._parseSearchResult(searchType, start, **kwargs)
+ #TODO: Handle single & zero result case better? Does this work?
+ try:
+ threads = items[D_THREAD]
+ except KeyError:
+ break
+ else:
+ for th in threads:
+ if not type(th[0]) is types.ListType:
+ th = [th]
+ threadsInfo.append(th)
+ # TODO: Check if the total or per-page values have changed?
+ threadListSummary = items[D_THREADLIST_SUMMARY][0]
+ threadsPerPage = threadListSummary[TS_NUM]
+
+ start += threadsPerPage
+
+ # TODO: Record whether or not we retrieved all pages..?
+ return GmailSearchResult(self, (searchType, kwargs), threadsInfo)
+
+
+ def _retrieveJavascript(self, version = ""):
+ """
+
+ Note: `version` seems to be ignored.
+ """
+ return self._retrievePage(_buildURL(view = U_PAGE_VIEW,
+ name = "js",
+ ver = version))
+
+
+ def getMessagesByFolder(self, folderName, allPages = False):
+ """
+
+ Folders contain conversation/message threads.
+
+ `folderName` -- As set in Gmail interface.
+
+ Returns a `GmailSearchResult` instance.
+
+ *** TODO: Change all "getMessagesByX" to "getThreadsByX"? ***
+ """
+ return self._parseThreadSearch(folderName, allPages = allPages)
+
+
+ def getMessagesByQuery(self, query, allPages = False):
+ """
+
+ Returns a `GmailSearchResult` instance.
+ """
+ return self._parseThreadSearch(U_QUERY_SEARCH, q = query,
+ allPages = allPages)
+
+
+ def getQuotaInfo(self, refresh = False):
+ """
+
+ Return MB used, Total MB and percentage used.
+ """
+ # TODO: Change this to a property.
+ if not self._cachedQuotaInfo or refresh:
+ # TODO: Handle this better...
+ self.getMessagesByFolder(U_INBOX_SEARCH)
+
+ return self._cachedQuotaInfo[0][:3]
+
+
+ def getLabelNames(self, refresh = False):
+ """
+ """
+ # TODO: Change this to a property?
+ if not self._cachedLabelNames or refresh:
+ # TODO: Handle this better...
+ self.getMessagesByFolder(U_INBOX_SEARCH)
+
+ return self._cachedLabelNames
+
+
+ def getFolderCounts(self, refresh = False):
+ """
+ """
+ # TODO: Change this to a property?
+ if not self._cachedFolderCounts or refresh:
+ # TODO: Handle this better...
+ self.getMessagesByFolder(U_INBOX_SEARCH)
+
+ return self._cachedFolderCounts
+
+
+ def getMessagesByLabel(self, label, allPages = False):
+ """
+ """
+ return self._parseThreadSearch(U_CATEGORY_SEARCH,
+ cat=label, allPages = allPages)
+
+ def getRawMessage(self, msgId):
+ """
+ """
+ # U_ORIGINAL_MESSAGE_VIEW seems the only one that returns a page.
+ # All the other U_* results in a 404 exception. Stas
+ PageView = U_ORIGINAL_MESSAGE_VIEW
+ return self._retrievePage(
+ _buildURL(view=PageView, th=msgId))
+
+ def getUnreadMessages(self):
+ """
+ """
+ return self._parseThreadSearch(U_QUERY_SEARCH,
+ q = "is:" + U_AS_SUBSET_UNREAD)
+
+
+ def getUnreadMsgCount(self):
+ """
+ """
+ items = self._parseSearchResult(U_QUERY_SEARCH,
+ q = "is:" + U_AS_SUBSET_UNREAD)
+ try:
+ result = items[D_THREADLIST_SUMMARY][0][TS_TOTAL_MSGS]
+ except KeyError:
+ result = 0
+ return result
+
+
+ def _getActionToken(self):
+ """
+ """
+ try:
+ at = self._cookieJar._cookies[ACTION_TOKEN_COOKIE]
+ except KeyError:
+ self.getLabelNames(True)
+ at = self._cookieJar._cookies[ACTION_TOKEN_COOKIE]
+
+ return at
+
+
+ def sendMessage(self, msg, asDraft = False, _extraParams = None):
+ """
+
+ `msg` -- `GmailComposedMessage` instance.
+
+ `_extraParams` -- Dictionary containing additional parameters
+ to put into POST message. (Not officially
+ for external use, more to make feature
+ additional a little easier to play with.)
+
+ Note: Now returns `GmailMessageStub` instance with populated
+ `id` (and `_account`) fields on success or None on failure.
+
+ """
+ # TODO: Handle drafts separately?
+ params = {U_VIEW: [U_SENDMAIL_VIEW, U_SAVEDRAFT_VIEW][asDraft],
+ U_REFERENCED_MSG: "",
+ U_THREAD: "",
+ U_DRAFT_MSG: "",
+ U_COMPOSEID: "1",
+ U_ACTION_TOKEN: self._getActionToken(),
+ U_COMPOSE_TO: msg.to,
+ U_COMPOSE_CC: msg.cc,
+ U_COMPOSE_BCC: msg.bcc,
+ "subject": msg.subject,
+ "msgbody": msg.body,
+ }
+
+ if _extraParams:
+ params.update(_extraParams)
+
+ # Amongst other things, I used the following post to work out this:
+ # <http://groups.google.com/groups?
+ # selm=mailman.1047080233.20095.python-list%40python.org>
+ mimeMessage = _paramsToMime(params, msg.filenames, msg.files)
+
+ #### TODO: Ughh, tidy all this up & do it better...
+ ## This horrible mess is here for two main reasons:
+ ## 1. The `Content-Type` header (which also contains the boundary
+ ## marker) needs to be extracted from the MIME message so
+ ## we can send it as the request `Content-Type` header instead.
+ ## 2. It seems the form submission needs to use "\r\n" for new
+ ## lines instead of the "\n" returned by `as_string()`.
+ ## I tried changing the value of `NL` used by the `Generator` class
+ ## but it didn't work so I'm doing it this way until I figure
+ ## out how to do it properly. Of course, first try, if the payloads
+ ## contained "\n" sequences they got replaced too, which corrupted
+ ## the attachments. I could probably encode the submission,
+ ## which would probably be nicer, but in the meantime I'm kludging
+ ## this workaround that replaces all non-text payloads with a
+ ## marker, changes all "\n" to "\r\n" and finally replaces the
+ ## markers with the original payloads.
+ ## Yeah, I know, it's horrible, but hey it works doesn't it? If you've
+ ## got a problem with it, fix it yourself & give me the patch!
+ ##
+ origPayloads = {}
+ FMT_MARKER = "&&&&&&%s&&&&&&"
+
+ for i, m in enumerate(mimeMessage.get_payload()):
+ if not isinstance(m, MIMEText): #Do we care if we change text ones?
+ origPayloads[i] = m.get_payload()
+ m.set_payload(FMT_MARKER % i)
+
+ mimeMessage.epilogue = ""
+ msgStr = mimeMessage.as_string()
+ contentTypeHeader, data = msgStr.split("\n\n", 1)
+ contentTypeHeader = contentTypeHeader.split(":", 1)
+ data = data.replace("\n", "\r\n")
+ for k,v in origPayloads.iteritems():
+ data = data.replace(FMT_MARKER % k, v)
+ ####
+
+ req = urllib2.Request(_buildURL(), data = data)
+ req.add_header(*contentTypeHeader)
+ items = self._parsePage(req)
+
+ # TODO: Check composeid?
+ # Sometimes we get the success message
+ # but the id is 0 and no message is sent
+ result = None
+ resultInfo = items[D_SENDMAIL_RESULT][0]
+
+ if resultInfo[SM_SUCCESS]:
+ result = GmailMessageStub(id = resultInfo[SM_NEWTHREADID],
+ _account = self)
+ else:
+ raise GmailSendError, resultInfo[SM_MSG]
+ return result
+
+
+ def trashMessage(self, msg):
+ """
+ """
+ # TODO: Decide if we should make this a method of `GmailMessage`.
+ # TODO: Should we check we have been given a `GmailMessage` instance?
+ params = {
+ U_ACTION: U_DELETEMESSAGE_ACTION,
+ U_ACTION_MESSAGE: msg.id,
+ U_ACTION_TOKEN: self._getActionToken(),
+ }
+
+ items = self._parsePage(_buildURL(**params))
+
+ # TODO: Mark as trashed on success?
+ return (items[D_ACTION_RESULT][0][AR_SUCCESS] == 1)
+
+
+ def _doThreadAction(self, actionId, thread):
+ """
+ """
+ # TODO: Decide if we should make this a method of `GmailThread`.
+ # TODO: Should we check we have been given a `GmailThread` instance?
+ params = {
+ U_SEARCH: U_ALL_SEARCH, #TODO:Check this search value always works.
+ U_VIEW: U_UPDATE_VIEW,
+ U_ACTION: actionId,
+ U_ACTION_THREAD: thread.id,
+ U_ACTION_TOKEN: self._getActionToken(),
+ }
+
+ items = self._parsePage(_buildURL(**params))
+
+ return (items[D_ACTION_RESULT][0][AR_SUCCESS] == 1)
+
+
+ def trashThread(self, thread):
+ """
+ """
+ # TODO: Decide if we should make this a method of `GmailThread`.
+ # TODO: Should we check we have been given a `GmailThread` instance?
+
+ result = self._doThreadAction(U_MARKTRASH_ACTION, thread)
+
+ # TODO: Mark as trashed on success?
+ return result
+
+
+ def _createUpdateRequest(self, actionId): #extraData):
+ """
+ Helper method to create a Request instance for an update (view)
+ action.
+
+ Returns populated `Request` instance.
+ """
+ params = {
+ U_VIEW: U_UPDATE_VIEW,
+ }
+
+ data = {
+ U_ACTION: actionId,
+ U_ACTION_TOKEN: self._getActionToken(),
+ }
+
+ #data.update(extraData)
+
+ req = urllib2.Request(_buildURL(**params),
+ data = urllib.urlencode(data))
+
+ return req
+
+
+ # TODO: Extract additional common code from handling of labels?
+ def createLabel(self, labelName):
+ """
+ """
+ req = self._createUpdateRequest(U_CREATECATEGORY_ACTION + labelName)
+
+ # Note: Label name cache is updated by this call as well. (Handy!)
+ items = self._parsePage(req)
+ print items
+ return (items[D_ACTION_RESULT][0][AR_SUCCESS] == 1)
+
+
+ def deleteLabel(self, labelName):
+ """
+ """
+ # TODO: Check labelName exits?
+ req = self._createUpdateRequest(U_DELETECATEGORY_ACTION + labelName)
+
+ # Note: Label name cache is updated by this call as well. (Handy!)
+ items = self._parsePage(req)
+
+ return (items[D_ACTION_RESULT][0][AR_SUCCESS] == 1)
+
+
+ def renameLabel(self, oldLabelName, newLabelName):
+ """
+ """
+ # TODO: Check oldLabelName exits?
+ req = self._createUpdateRequest("%s%s^%s" % (U_RENAMECATEGORY_ACTION,
+ oldLabelName, newLabelName))
+
+ # Note: Label name cache is updated by this call as well. (Handy!)
+ items = self._parsePage(req)
+
+ return (items[D_ACTION_RESULT][0][AR_SUCCESS] == 1)
+
+ def storeFile(self, filename, label = None):
+ """
+ """
+ # TODO: Handle files larger than single attachment size.
+ # TODO: Allow file data objects to be supplied?
+ FILE_STORE_VERSION = "FSV_01"
+ FILE_STORE_SUBJECT_TEMPLATE = "%s %s" % (FILE_STORE_VERSION, "%s")
+
+ subject = FILE_STORE_SUBJECT_TEMPLATE % os.path.basename(filename)
+
+ msg = GmailComposedMessage(to="", subject=subject, body="",
+ filenames=[filename])
+
+ draftMsg = self.sendMessage(msg, asDraft = True)
+
+ if draftMsg and label:
+ draftMsg.addLabel(label)
+
+ return draftMsg
+
+ ## CONTACTS SUPPORT
+ def getContacts(self):
+ """
+ Returns a GmailContactList object
+ that has all the contacts in it as
+ GmailContacts
+ """
+ contactList = []
+ # pnl = a is necessary to get *all* contacts
+ myUrl = _buildURL(view='cl',search='contacts', pnl='a')
+ myData = self._parsePage(myUrl)
+ # This comes back with a dictionary
+ # with entry 'cl'
+ addresses = myData['cl']
+ for entry in addresses:
+ if len(entry) >= 6 and entry[0]=='ce':
+ newGmailContact = GmailContact(entry[1], entry[2], entry[4], entry[5])
+ #### new code used to get all the notes
+ #### not used yet due to lockdown problems
+ ##rawnotes = self._getSpecInfo(entry[1])
+ ##print rawnotes
+ ##newGmailContact = GmailContact(entry[1], entry[2], entry[4],rawnotes)
+ contactList.append(newGmailContact)
+
+ return GmailContactList(contactList)
+
+ def addContact(self, myContact, *extra_args):
+ """
+ Attempts to add a GmailContact to the gmail
+ address book. Returns true if successful,
+ false otherwise
+
+ Please note that after version 0.1.3.3,
+ addContact takes one argument of type
+ GmailContact, the contact to add.
+
+ The old signature of:
+ addContact(name, email, notes='') is still
+ supported, but deprecated.
+ """
+ if len(extra_args) > 0:
+ # The user has passed in extra arguments
+ # He/she is probably trying to invoke addContact
+ # using the old, deprecated signature of:
+ # addContact(self, name, email, notes='')
+ # Build a GmailContact object and use that instead
+ (name, email) = (myContact, extra_args[0])
+ if len(extra_args) > 1:
+ notes = extra_args[1]
+ else:
+ notes = ''
+ myContact = GmailContact(-1, name, email, notes)
+
+ # TODO: In the ideal world, we'd extract these specific
+ # constants into a nice constants file
+
+ # This mostly comes from the Johnvey Gmail API,
+ # but also from the gmail.py cited earlier
+ myURL = _buildURL(view='up')
+
+ myDataList = [ ('act','ec'),
+ ('at', self._cookieJar._cookies['GMAIL_AT']), # Cookie data?
+ ('ct_nm', myContact.getName()),
+ ('ct_em', myContact.getEmail()),
+ ('ct_id', -1 )
+ ]
+
+ notes = myContact.getNotes()
+ if notes != '':
+ myDataList.append( ('ctf_n', notes) )
+
+ validinfokeys = [
+ 'i', # IM
+ 'p', # Phone
+ 'd', # Company
+ 'a', # ADR
+ 'e', # Email
+ 'm', # Mobile
+ 'b', # Pager
+ 'f', # Fax
+ 't', # Title
+ 'o', # Other
+ ]
+
+ moreInfo = myContact.getMoreInfo()
+ ctsn_num = -1
+ if moreInfo != {}:
+ for ctsf,ctsf_data in moreInfo.items():
+ ctsn_num += 1
+ # data section header, WORK, HOME,...
+ sectionenum ='ctsn_%02d' % ctsn_num
+ myDataList.append( ( sectionenum, ctsf ))
+ ctsf_num = -1
+
+ if isinstance(ctsf_data[0],str):
+ ctsf_num += 1
+ # data section
+ subsectionenum = 'ctsf_%02d_%02d_%s' % (ctsn_num, ctsf_num, ctsf_data[0]) # ie. ctsf_00_01_p
+ myDataList.append( (subsectionenum, ctsf_data[1]) )
+ else:
+ for info in ctsf_data:
+ if validinfokeys.count(info[0]) > 0:
+ ctsf_num += 1
+ # data section
+ subsectionenum = 'ctsf_%02d_%02d_%s' % (ctsn_num, ctsf_num, info[0]) # ie. ctsf_00_01_p
+ myDataList.append( (subsectionenum, info[1]) )
+
+ myData = urllib.urlencode(myDataList)
+ request = urllib2.Request(myURL,
+ data = myData)
+ pageData = self._retrievePage(request)
+
+ if pageData.find("The contact was successfully added") == -1:
+ print pageData
+ if pageData.find("already has the email address") > 0:
+ raise Exception("Someone with same email already exists in Gmail.")
+ elif pageData.find("https://www.google.com/accounts/ServiceLogin"):
+ raise Exception("Login has expired.")
+ return False
+ else:
+ return True
+
+ def _removeContactById(self, id):
+ """
+ Attempts to remove the contact that occupies
+ id "id" from the gmail address book.
+ Returns True if successful,
+ False otherwise.
+
+ This is a little dangerous since you don't really
+ know who you're deleting. Really,
+ this should return the name or something of the
+ person we just killed.
+
+ Don't call this method.
+ You should be using removeContact instead.
+ """
+ myURL = _buildURL(search='contacts', ct_id = id, c=id, act='dc', at=self._cookieJar._cookies['GMAIL_AT'], view='up')
+ pageData = self._retrievePage(myURL)
+
+ if pageData.find("The contact has been deleted") == -1:
+ return False
+ else:
+ return True
+
+ def removeContact(self, gmailContact):
+ """
+ Attempts to remove the GmailContact passed in
+ Returns True if successful, False otherwise.
+ """
+ # Let's re-fetch the contact list to make
+ # sure we're really deleting the guy
+ # we think we're deleting
+ newContactList = self.getContacts()
+ newVersionOfPersonToDelete = newContactList.getContactById(gmailContact.getId())
+ # Ok, now we need to ensure that gmailContact
+ # is the same as newVersionOfPersonToDelete
+ # and then we can go ahead and delete him/her
+ if (gmailContact == newVersionOfPersonToDelete):
+ return self._removeContactById(gmailContact.getId())
+ else:
+ # We have a cache coherency problem -- someone
+ # else now occupies this ID slot.
+ # TODO: Perhaps signal this in some nice way
+ # to the end user?
+
+ print "Unable to delete."
+ print "Has someone else been modifying the contacts list while we have?"
+ print "Old version of person:",gmailContact
+ print "New version of person:",newVersionOfPersonToDelete
+ return False
+
+## Don't remove this. contact stas
+## def _getSpecInfo(self,id):
+## """
+## Return all the notes data.
+## This is currently not used due to the fact that it requests pages in
+## a dos attack manner.
+## """
+## myURL =_buildURL(search='contacts',ct_id=id,c=id,\
+## at=self._cookieJar._cookies['GMAIL_AT'],view='ct')
+## pageData = self._retrievePage(myURL)
+## myData = self._parsePage(myURL)
+## #print "\nmyData form _getSpecInfo\n",myData
+## rawnotes = myData['cov'][7]
+## return rawnotes
+
+class GmailContact:
+ """
+ Class for storing a Gmail Contacts list entry
+ """
+ def __init__(self, name, email, *extra_args):
+ """
+ Returns a new GmailContact object
+ (you can then call addContact on this to commit
+ it to the Gmail addressbook, for example)
+
+ Consider calling setNotes() and setMoreInfo()
+ to add extended information to this contact
+ """
+ # Support populating other fields if we're trying
+ # to invoke this the old way, with the old constructor
+ # whose signature was __init__(self, id, name, email, notes='')
+ id = -1
+ notes = ''
+
+ if len(extra_args) > 0:
+ (id, name) = (name, email)
+ email = extra_args[0]
+ if len(extra_args) > 1:
+ notes = extra_args[1]
+ else:
+ notes = ''
+
+ self.id = id
+ self.name = name
+ self.email = email
+ self.notes = notes
+ self.moreInfo = {}
+ def __str__(self):
+ return "%s %s %s %s" % (self.id, self.name, self.email, self.notes)
+ def __eq__(self, other):
+ if not isinstance(other, GmailContact):
+ return False
+ return (self.getId() == other.getId()) and \
+ (self.getName() == other.getName()) and \
+ (self.getEmail() == other.getEmail()) and \
+ (self.getNotes() == other.getNotes())
+ def getId(self):
+ return self.id
+ def getName(self):
+ return self.name
+ def getEmail(self):
+ return self.email
+ def getNotes(self):
+ return self.notes
+ def setNotes(self, notes):
+ """
+ Sets the notes field for this GmailContact
+ Note that this does NOT change the note
+ field on Gmail's end; only adding or removing
+ contacts modifies them
+ """
+ self.notes = notes
+
+ def getMoreInfo(self):
+ return self.moreInfo
+ def setMoreInfo(self, moreInfo):
+ """
+ moreInfo format
+ ---------------
+ Use special key values::
+ 'i' = IM
+ 'p' = Phone
+ 'd' = Company
+ 'a' = ADR
+ 'e' = Email
+ 'm' = Mobile
+ 'b' = Pager
+ 'f' = Fax
+ 't' = Title
+ 'o' = Other
+
+ Simple example::
+
+ moreInfo = {'Home': ( ('a','852 W Barry'),
+ ('p', '1-773-244-1980'),
+ ('i', 'aim:brianray34') ) }
+
+ Complex example::
+
+ moreInfo = {
+ 'Personal': (('e', 'Home Email'),
+ ('f', 'Home Fax')),
+ 'Work': (('d', 'Sample Company'),
+ ('t', 'Job Title'),
+ ('o', 'Department: Department1'),
+ ('o', 'Department: Department2'),
+ ('p', 'Work Phone'),
+ ('m', 'Mobile Phone'),
+ ('f', 'Work Fax'),
+ ('b', 'Pager')) }
+ """
+ self.moreInfo = moreInfo
+ def getVCard(self):
+ """Returns a vCard 3.0 for this
+ contact, as a string"""
+ # The \r is is to comply with the RFC2425 section 5.8.1
+ vcard = "BEGIN:VCARD\r\n"
+ vcard += "VERSION:3.0\r\n"
+ ## Deal with multiline notes
+ ##vcard += "NOTE:%s\n" % self.getNotes().replace("\n","\\n")
+ vcard += "NOTE:%s\r\n" % self.getNotes()
+ # Fake-out N by splitting up whatever we get out of getName
+ # This might not always do 'the right thing'
+ # but it's a *reasonable* compromise
+ fullname = self.getName().split()
+ fullname.reverse()
+ vcard += "N:%s" % ';'.join(fullname) + "\r\n"
+ vcard += "FN:%s\r\n" % self.getName()
+ vcard += "EMAIL;TYPE=INTERNET:%s\r\n" % self.getEmail()
+ vcard += "END:VCARD\r\n\r\n"
+ # Final newline in case we want to put more than one in a file
+ return vcard
+
+class GmailContactList:
+ """
+ Class for storing an entire Gmail contacts list
+ and retrieving contacts by Id, Email address, and name
+ """
+ def __init__(self, contactList):
+ self.contactList = contactList
+ def __str__(self):
+ return '\n'.join([str(item) for item in self.contactList])
+ def getCount(self):
+ """
+ Returns number of contacts
+ """
+ return len(self.contactList)
+ def getAllContacts(self):
+ """
+ Returns an array of all the
+ GmailContacts
+ """
+ return self.contactList
+ def getContactByName(self, name):
+ """
+ Gets the first contact in the
+ address book whose name is 'name'.
+
+ Returns False if no contact
+ could be found
+ """
+ nameList = self.getContactListByName(name)
+ if len(nameList) > 0:
+ return nameList[0]
+ else:
+ return False
+ def getContactByEmail(self, email):
+ """
+ Gets the first contact in the
+ address book whose name is 'email'.
+ As of this writing, Gmail insists
+ upon a unique email; i.e. two contacts
+ cannot share an email address.
+
+ Returns False if no contact
+ could be found
+ """
+ emailList = self.getContactListByEmail(email)
+ if len(emailList) > 0:
+ return emailList[0]
+ else:
+ return False
+ def getContactById(self, myId):
+ """
+ Gets the first contact in the
+ address book whose id is 'myId'.
+
+ 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
+ def search(self, searchTerm):
+ """
+ This function returns a LIST
+ of GmailContacts whose name or
+ email address matches the 'searchTerm'.
+
+ Returns an empty list if no matches
+ were found.
+ """
+ searchResults = []
+ for entry in self.contactList:
+ p = re.compile(searchTerm, re.IGNORECASE)
+ if p.search(entry.getName()) or p.search(entry.getEmail()):
+ searchResults.append(entry)
+ return searchResults
+
+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 __init__(self):
+ self._labels = None
+
+ 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):
+ """
+ """
+ _LabelHandlerMixin.__init__(self)
+
+ # 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):
+ """
+ """
+ _LabelHandlerMixin.__init__(self)
+ 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: "<thread_id>_<msg_id>_<attachment_id>"
+
+ """
+ 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: ")
+ domain = raw_input("Domain? [leave blank for Gmail]: ")
+
+ ga = GmailAccount(name, pw, domain=domain)
+
+ 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."
Added: bigboard/trunk/bigboard/stocks/mail/thumbnail.png
===================================================================
(Binary files differ)
Property changes on: bigboard/trunk/bigboard/stocks/mail/thumbnail.png
___________________________________________________________________
Name: svn:mime-type
+ application/octet-stream
Added: bigboard/trunk/bigboard/stocks/mail.xml
===================================================================
--- bigboard/trunk/bigboard/stocks/mail.xml 2008-01-08 21:47:04 UTC (rev 7150)
+++ bigboard/trunk/bigboard/stocks/mail.xml 2008-01-08 22:26:17 UTC (rev 7151)
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Module>
+ <ModulePrefs title="Mail"
+ author="Natan Yellin"
+ author_email="online-desktop-list gnome org"
+ author_location="Israel"
+ screenshot="mail.png"
+ thumbnail="thumbnail.png"
+ description="View your recent emails">
+ </ModulePrefs>
+ <Content type="online-desktop-builtin"></Content>
+</Module>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]