| Home | Trees | Indices | Help |
|---|
|
|
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
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
80
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
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
146
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
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
174
175
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
196
197
204
205
206
211
212
213
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
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 """
281
283 """
284 """
285
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
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
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
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
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
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
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
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
489 """
490
491 Returns a `GmailSearchResult` instance.
492 """
493 return self._parseThreadSearch(U_QUERY_SEARCH, q = query,
494 allPages = allPages)
495
496
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
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
522 """
523 """
524 return self._parseThreadSearch(U_CATEGORY_SEARCH,
525 cat=label, allPages = allPages)
526
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
537 """
538 """
539 return self._parseThreadSearch(U_QUERY_SEARCH,
540 q = "is:" + U_AS_SUBSET_UNREAD)
541
542
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
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
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
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
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
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
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?
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
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
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
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
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
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
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
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
972 """
973 Class for storing a Gmail Contacts list entry
974 """
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 = {}
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())
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
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
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
1090 """
1091 Class for storing an entire Gmail contacts list
1092 and retrieving contacts by Id, Email address, and name
1093 """
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
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
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
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
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
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
1203 """
1204 """
1205
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
1230
1235
1240
1241
1243 """
1244 """
1245
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
1262
1263
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 """
1272
1275
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
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
1310
1311
1312
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
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
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
1382
1383
1385 """
1386 """
1387 if not self._messages:
1388 self._messages = self._getMessages(self)
1389
1390 return iter(self._messages)
1391
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
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
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
1443 """
1444 """
1445 _LabelHandlerMixin.__init__(self)
1446 self.id = id
1447 self._account = _account
1448
1449
1450
1452 """
1453 """
1454
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
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
1501 """
1502 """
1503
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
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
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
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
| Home | Trees | Indices | Help |
|---|
| Generated by Epydoc 3.0beta1 on Mon Oct 8 21:20:32 2007 | http://epydoc.sourceforge.net |