1
2
3
4
5
6
7
8 Version = '0.1.7'
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
46 PROXY_URL = None
47
48
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
54
55 U_SAVEDRAFT_VIEW = "sd"
56
57 D_DRAFTINFO = "di"
58
59 DI_BODY = 19
60
61 versionWarned = False
62
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
76 '''
77 Exception to throw if we're unable to send a message
78 '''
79 pass
80
81 -def _parsePage(pageContent):
82 """
83 Parse the supplied HTML page and extract useful information from
84 the embedded Javascript.
85
86 """
87 lines = pageContent.splitlines()
88 data = '\n'.join([x for x in lines if x and x[0] in ['D', ')', ',', ']']])
89
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
110
111
112
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
136 for group in infoItems:
137 if type(group) == tuple:
138 result.extend(group)
139 else:
140 result.append(group)
141 return result
142
145 self.cookiejar = cookiejar
146
148
149
150
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
171 """
172 """
173 self._cookies = {}
174
175
177 """
178 """
179
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
193 """
194 """
195 self._cookies[name] = value
196
197
199 """
200 """
201 request.add_header('Cookie',
202 ";".join(["%s=%s" % (k,v)
203 for k,v in self._cookies.items()]))
204
205
206
208 """
209 """
210 return "%s?%s" % (URL_GMAIL, urllib.urlencode(kwargs))
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
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
234 if isinstance(item, str):
235
236 filename = item
237 contentType = mimetypes.guess_type(filename)[0]
238 payload = open(filename, "rb").read()
239 else:
240
241
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
253 mimeItem.set_payload(payload)
254
255
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 """
278 self.message = message
280 return repr(self.message)
281
283 """
284 """
285
286 - def __init__(self, name = "", pw = "", state = None, domain = None):
287 global URL_LOGIN, URL_GMAIL
288 """
289 """
290 self.domain = domain
291 if self.domain:
292 URL_LOGIN = "https://www.google.com/a/" + self.domain + "/LoginAction"
293 URL_GMAIL = "http://mail.google.com/a/" + self.domain + "/"
294 else:
295 URL_LOGIN = GMAIL_URL_LOGIN
296 URL_GMAIL = GMAIL_URL_GMAIL
297 if name and pw:
298 self.name = name
299 self._pw = pw
300 self._cookieJar = CookieJar()
301
302 if PROXY_URL is not None:
303 import gmail_transport
304
305 self.opener = urllib2.build_opener(gmail_transport.ConnectHTTPHandler(proxy = PROXY_URL),
306 gmail_transport.ConnectHTTPSHandler(proxy = PROXY_URL),
307 SmartRedirectHandler(self._cookieJar))
308 else:
309 self.opener = urllib2.build_opener(
310 urllib2.HTTPHandler(debuglevel=0),
311 urllib2.HTTPSHandler(debuglevel=0),
312 SmartRedirectHandler(self._cookieJar))
313 elif state:
314
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
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
350
351
352 RE_PAGE_REDIRECT = 'CheckCookie\?continue=([^"\']+)'
353
354
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
363
364 pageData = self._retrievePage(redirectURL)
365
366 - def _retrievePage(self, urlOrRequest):
367 """
368 """
369 if self.opener is None:
370 raise "Cannot find urlopener"
371
372 if not isinstance(urlOrRequest, urllib2.Request):
373 req = urllib2.Request(urlOrRequest)
374 else:
375 req = urlOrRequest
376
377 self._cookieJar.setCookies(req)
378 req.add_header('User-Agent',
379 'Mozilla/5.0 (Compatible; libgmail-python)')
380
381 try:
382 resp = self.opener.open(req)
383 except urllib2.HTTPError,info:
384 print info
385 return None
386 pageData = resp.read()
387
388
389 self._cookieJar.extractCookies(resp.headers)
390
391
392
393 return pageData
394
395
396 - def _parsePage(self, urlOrRequest):
397 """
398 Retrieve & then parse the requested page content.
399
400 """
401 items = _parsePage(self._retrievePage(urlOrRequest))
402
403
404
405
406 try:
407 self._cachedQuotaInfo = items[D_QUOTA]
408 except KeyError:
409 pass
410
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
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
440 while (start == 0) or (allPages and
441 len(threadsInfo) < threadListSummary[TS_TOTAL]):
442
443 items = self._parseSearchResult(searchType, start, **kwargs)
444
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
455 threadListSummary = items[D_THREADLIST_SUMMARY][0]
456 threadsPerPage = threadListSummary[TS_NUM]
457
458 start += threadsPerPage
459
460
461 return GmailSearchResult(self, (searchType, kwargs), threadsInfo)
462
463
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
495
496
498 """
499
500 Return MB used, Total MB and percentage used.
501 """
502
503 if not self._cachedQuotaInfo or refresh:
504
505 self.getMessagesByFolder(U_INBOX_SEARCH)
506
507 return self._cachedQuotaInfo[0][:3]
508
509
519
520
526
535
541
542
553
554
565
566
567 - def sendMessage(self, msg, asDraft = False, _extraParams = None):
568 """
569
570 `msg` -- `GmailComposedMessage` instance.
571
572 `_extraParams` -- Dictionary containing additional parameters
573 to put into POST message. (Not officially
574 for external use, more to make feature
575 additional a little easier to play with.)
576
577 Note: Now returns `GmailMessageStub` instance with populated
578 `id` (and `_account`) fields on success or None on failure.
579
580 """
581
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
599
600
601 mimeMessage = _paramsToMime(params, msg.filenames, msg.files)
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622 origPayloads = {}
623 FMT_MARKER = "&&&&&&%s&&&&&&"
624
625 for i, m in enumerate(mimeMessage.get_payload()):
626 if not isinstance(m, MIMEText):
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
644
645
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
672
673
690
691
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
721
722 req = urllib2.Request(_buildURL(**params),
723 data = urllib.urlencode(data))
724
725 return req
726
727
728
738
739
750
751
763
764 - def storeFile(self, filename, label = None):
765 """
766 """
767
768
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
809
905
928
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
1088
1201
1203 """
1204 """
1205
1206 - def __init__(self, account, search, threadsInfo):
1207 """
1208
1209 `threadsInfo` -- As returned from Gmail but unbunched.
1210 """
1211
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
1220 self._threads = []
1221
1222 for thread in threadsInfo:
1223 self._threads.append(GmailThread(self, thread[0]))
1224
1225
1227 """
1228 """
1229 return iter(self._threads)
1230
1232 """
1233 """
1234 return len(self._threads)
1235
1237 """
1238 """
1239 return self._threads.__getitem__(key)
1240
1241
1243 """
1244 """
1245
1246 - def __init__(self, account = None, filename = ""):
1247 """
1248 """
1249 if account:
1250 self.state = (account.name, account._cookieJar)
1251 elif filename:
1252 self.state = load(open(filename, "rb"))
1253 else:
1254 raise ValueError("GmailSessionState must be instantiated with " \
1255 "either GmailAccount object or filename.")
1256
1257
1258 - def save(self, filename):
1259 """
1260 """
1261 dump(self.state, open(filename, "wb"), -1)
1262
1263
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
1274 self._labels = labelList
1275
1287
1288
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
1322 - def __init__(self, parent, threadsInfo):
1323 """
1324 """
1325 _LabelHandlerMixin.__init__(self)
1326
1327
1328 self._parent = parent
1329 self._account = self._parent._account
1330
1331 self.id = threadsInfo[T_THREADID]
1332 self.subject = threadsInfo[T_SUBJECT_HTML]
1333
1334 self.snippet = threadsInfo[T_SNIPPET_HTML]
1335
1336
1337
1338
1339
1340 self._authors = threadsInfo[T_AUTHORS_HTML]
1341 self.info = threadsInfo
1342
1343 try:
1344
1345
1346 self._length = int(re.search("\((\d+?)\)\Z",
1347 self._authors).group(1))
1348 except AttributeError,info:
1349
1350 self._length = 1
1351
1352
1353 self._messages = []
1354
1355
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
1379 """
1380 """
1381 return self._length
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
1407
1408 items = self._account._parseSearchResult(U_QUERY_SEARCH,
1409 view = U_CONVERSATION_VIEW,
1410 th = thread.id,
1411 q = "in:anywhere")
1412 result = []
1413
1414
1415 for key, isDraft in [(D_MSGINFO, False), (D_DRAFTINFO, True)]:
1416 try:
1417 msgsInfo = items[key]
1418 except KeyError:
1419
1420 continue
1421 else:
1422
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
1440
1441
1442 - def __init__(self, id = None, _account = None):
1448
1449
1450
1452 """
1453 """
1454
1455 - def __init__(self, parent, msgData, isDraft = False):
1456 """
1457
1458 Note: `msgData` can be from either D_MSGINFO or D_DRAFTINFO.
1459 """
1460
1461
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
1478
1479
1480 self.isDraft = isDraft
1481
1482 self._source = None
1483
1484
1486 """
1487 """
1488 if not self._source:
1489
1490
1491
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
1504 - def __init__(self, parent, attachmentInfo):
1505 """
1506 """
1507
1508 self._parent = parent
1509 self._account = self._parent._account
1510
1511 self.id = attachmentInfo[A_ID]
1512 self.filename = attachmentInfo[A_FILENAME]
1513 self.mimetype = attachmentInfo[A_MIMETYPE]
1514 self.filesize = attachmentInfo[A_FILESIZE]
1515
1516 self._content = None
1517
1518
1519 - def _getContent(self):
1520 """
1521 """
1522 if not self._content:
1523
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
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
1636
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