Baseline for work is Dialcentral 1.0.6-10
[theonering] / src / gv_backend.py
1 #!/usr/bin/python
2
3 """
4 DialCentral - Front end for Google's GoogleVoice service.
5 Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
6
7 This library is free software; you can redistribute it and/or
8 modify it under the terms of the GNU Lesser General Public
9 License as published by the Free Software Foundation; either
10 version 2.1 of the License, or (at your option) any later version.
11
12 This library is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15 Lesser General Public License for more details.
16
17 You should have received a copy of the GNU Lesser General Public
18 License along with this library; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
20
21 Google Voice backend code
22
23 Resources
24         http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
25         http://posttopic.com/topic/google-voice-add-on-development
26 """
27
28
29 import os
30 import re
31 import urllib
32 import urllib2
33 import time
34 import datetime
35 import itertools
36 import logging
37 from xml.sax import saxutils
38
39 from xml.etree import ElementTree
40
41 import browser_emu
42
43 try:
44         import simplejson
45 except ImportError:
46         simplejson = None
47
48
49 _TRUE_REGEX = re.compile("true")
50 _FALSE_REGEX = re.compile("false")
51
52
53 def safe_eval(s):
54         s = _TRUE_REGEX.sub("True", s)
55         s = _FALSE_REGEX.sub("False", s)
56         return eval(s, {}, {})
57
58
59 if simplejson is None:
60         def parse_json(flattened):
61                 return safe_eval(flattened)
62 else:
63         def parse_json(flattened):
64                 return simplejson.loads(flattened)
65
66
67 def itergroup(iterator, count, padValue = None):
68         """
69         Iterate in groups of 'count' values. If there
70         aren't enough values, the last result is padded with
71         None.
72
73         >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
74         ...     print tuple(val)
75         (1, 2, 3)
76         (4, 5, 6)
77         >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
78         ...     print list(val)
79         [1, 2, 3]
80         [4, 5, 6]
81         >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
82         ...     print tuple(val)
83         (1, 2, 3)
84         (4, 5, 6)
85         (7, None, None)
86         >>> for val in itergroup("123456", 3):
87         ...     print tuple(val)
88         ('1', '2', '3')
89         ('4', '5', '6')
90         >>> for val in itergroup("123456", 3):
91         ...     print repr("".join(val))
92         '123'
93         '456'
94         """
95         paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
96         nIterators = (paddedIterator, ) * count
97         return itertools.izip(*nIterators)
98
99
100 class GVDialer(object):
101         """
102         This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
103         the functions include login, setting up a callback number, and initalting a callback
104         """
105
106         def __init__(self, cookieFile = None):
107                 # Important items in this function are the setup of the browser emulation and cookie file
108                 self._browser = browser_emu.MozillaEmulator(1)
109                 if cookieFile is None:
110                         cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt")
111                 self._browser.cookies.filename = cookieFile
112                 if os.path.isfile(cookieFile):
113                         self._browser.cookies.load()
114
115                 self._token = ""
116                 self._accountNum = ""
117                 self._lastAuthed = 0.0
118                 self._callbackNumber = ""
119                 self._callbackNumbers = {}
120
121                 self.__contacts = None
122
123         def is_authed(self, force = False):
124                 """
125                 Attempts to detect a current session
126                 @note Once logged in try not to reauth more than once a minute.
127                 @returns If authenticated
128                 """
129
130                 if (time.time() - self._lastAuthed) < 120 and not force:
131                         return True
132
133                 try:
134                         self._grab_account_info()
135                 except Exception, e:
136                         logging.exception(str(e))
137                         return False
138
139                 self._browser.cookies.save()
140                 self._lastAuthed = time.time()
141                 return True
142
143         _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
144
145         def login(self, username, password):
146                 """
147                 Attempt to login to GoogleVoice
148                 @returns Whether login was successful or not
149                 """
150                 if self.is_authed():
151                         return True
152
153                 loginPostData = urllib.urlencode({
154                         'Email' : username,
155                         'Passwd' : password,
156                         'service': "grandcentral",
157                         "ltmpl": "mobile",
158                         "btmpl": "mobile",
159                         "PersistentCookie": "yes",
160                 })
161
162                 try:
163                         loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
164                 except urllib2.URLError, e:
165                         logging.exception(str(e))
166                         raise RuntimeError("%s is not accesible" % self._loginURL)
167
168                 return self.is_authed()
169
170         def logout(self):
171                 self._lastAuthed = 0.0
172                 self._browser.cookies.clear()
173                 self._browser.cookies.save()
174
175                 self.clear_caches()
176
177         _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
178         _clicktocallURL = "https://www.google.com/voice/m/sendcall"
179
180         def dial(self, number):
181                 """
182                 This is the main function responsible for initating the callback
183                 """
184                 number = self._send_validation(number)
185                 try:
186                         clickToCallData = urllib.urlencode({
187                                 "number": number,
188                                 "phone": self._callbackNumber,
189                                 "_rnr_se": self._token,
190                         })
191                         otherData = {
192                                 'Referer' : 'https://google.com/voice/m/callsms',
193                         }
194                         callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
195                 except urllib2.URLError, e:
196                         logging.exception(str(e))
197                         raise RuntimeError("%s is not accesible" % self._clicktocallURL)
198
199                 if self._gvDialingStrRe.search(callSuccessPage) is None:
200                         raise RuntimeError("Google Voice returned an error")
201
202                 return True
203
204         _sendSmsURL = "https://www.google.com/voice/m/sendsms"
205
206         def send_sms(self, number, message):
207                 number = self._send_validation(number)
208                 try:
209                         smsData = urllib.urlencode({
210                                 "number": number,
211                                 "smstext": message,
212                                 "_rnr_se": self._token,
213                                 "id": "undefined",
214                                 "c": "undefined",
215                         })
216                         otherData = {
217                                 'Referer' : 'https://google.com/voice/m/sms',
218                         }
219                         smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
220                 except urllib2.URLError, e:
221                         logging.exception(str(e))
222                         raise RuntimeError("%s is not accesible" % self._sendSmsURL)
223
224                 return True
225
226         def clear_caches(self):
227                 self.__contacts = None
228
229         _validateRe = re.compile("^[0-9]{10,}$")
230
231         def is_valid_syntax(self, number):
232                 """
233                 @returns If This number be called ( syntax validation only )
234                 """
235                 return self._validateRe.match(number) is not None
236
237         def get_account_number(self):
238                 """
239                 @returns The GoogleVoice phone number
240                 """
241                 return self._accountNum
242
243         def set_sane_callback(self):
244                 """
245                 Try to set a sane default callback number on these preferences
246                 1) 1747 numbers ( Gizmo )
247                 2) anything with gizmo in the name
248                 3) anything with computer in the name
249                 4) the first value
250                 """
251                 numbers = self.get_callback_numbers()
252
253                 for number, description in numbers.iteritems():
254                         if re.compile(r"""1747""").match(number) is not None:
255                                 self.set_callback_number(number)
256                                 return
257
258                 for number, description in numbers.iteritems():
259                         if re.compile(r"""gizmo""", re.I).search(description) is not None:
260                                 self.set_callback_number(number)
261                                 return
262
263                 for number, description in numbers.iteritems():
264                         if re.compile(r"""computer""", re.I).search(description) is not None:
265                                 self.set_callback_number(number)
266                                 return
267
268                 for number, description in numbers.iteritems():
269                         self.set_callback_number(number)
270                         return
271
272         def get_callback_numbers(self):
273                 """
274                 @returns a dictionary mapping call back numbers to descriptions
275                 @note These results are cached for 30 minutes.
276                 """
277                 if not self.is_authed():
278                         return {}
279                 return self._callbackNumbers
280
281         _setforwardURL = "https://www.google.com//voice/m/setphone"
282
283         def set_callback_number(self, callbacknumber):
284                 """
285                 Set the number that GoogleVoice calls
286                 @param callbacknumber should be a proper 10 digit number
287                 """
288                 self._callbackNumber = callbacknumber
289                 return True
290
291         def get_callback_number(self):
292                 """
293                 @returns Current callback number or None
294                 """
295                 return self._callbackNumber
296
297         def get_recent(self):
298                 """
299                 @returns Iterable of (personsName, phoneNumber, date, action)
300                 """
301                 sortedRecent = [
302                         (exactDate, name, number, relativeDate, action)
303                         for (name, number, exactDate, relativeDate, action) in self._get_recent()
304                 ]
305                 sortedRecent.sort(reverse = True)
306                 for exactDate, name, number, relativeDate, action in sortedRecent:
307                         yield name, number, relativeDate, action
308
309         def get_addressbooks(self):
310                 """
311                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
312                 """
313                 yield self, "", ""
314
315         def open_addressbook(self, bookId):
316                 return self
317
318         @staticmethod
319         def contact_source_short_name(contactId):
320                 return "GV"
321
322         @staticmethod
323         def factory_name():
324                 return "Google Voice"
325
326         _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
327         _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
328         _contactsURL = "https://www.google.com/voice/mobile/contacts"
329
330         def get_contacts(self):
331                 """
332                 @returns Iterable of (contact id, contact name)
333                 """
334                 if self.__contacts is None:
335                         self.__contacts = []
336
337                         contactsPagesUrls = [self._contactsURL]
338                         for contactsPageUrl in contactsPagesUrls:
339                                 try:
340                                         contactsPage = self._browser.download(contactsPageUrl)
341                                 except urllib2.URLError, e:
342                                         logging.exception(str(e))
343                                         raise RuntimeError("%s is not accesible" % contactsPageUrl)
344                                 for contact_match in self._contactsRe.finditer(contactsPage):
345                                         contactId = contact_match.group(1)
346                                         contactName = saxutils.unescape(contact_match.group(2))
347                                         contact = contactId, contactName
348                                         self.__contacts.append(contact)
349                                         yield contact
350
351                                 next_match = self._contactsNextRe.match(contactsPage)
352                                 if next_match is not None:
353                                         newContactsPageUrl = self._contactsURL + next_match.group(1)
354                                         contactsPagesUrls.append(newContactsPageUrl)
355                 else:
356                         for contact in self.__contacts:
357                                 yield contact
358
359         _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
360         _contactDetailURL = "https://www.google.com/voice/mobile/contact"
361
362         def get_contact_details(self, contactId):
363                 """
364                 @returns Iterable of (Phone Type, Phone Number)
365                 """
366                 try:
367                         detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
368                 except urllib2.URLError, e:
369                         logging.exception(str(e))
370                         raise RuntimeError("%s is not accesible" % self._contactDetailURL)
371
372                 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
373                         phoneNumber = detail_match.group(1)
374                         phoneType = saxutils.unescape(detail_match.group(2))
375                         yield (phoneType, phoneNumber)
376
377         _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
378         _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
379
380         def get_messages(self):
381                 try:
382                         voicemailPage = self._browser.download(self._voicemailURL)
383                 except urllib2.URLError, e:
384                         logging.exception(str(e))
385                         raise RuntimeError("%s is not accesible" % self._voicemailURL)
386                 voicemailHtml = self._grab_html(voicemailPage)
387                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
388                 decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
389
390                 try:
391                         smsPage = self._browser.download(self._smsURL)
392                 except urllib2.URLError, e:
393                         logging.exception(str(e))
394                         raise RuntimeError("%s is not accesible" % self._smsURL)
395                 smsHtml = self._grab_html(smsPage)
396                 parsedSms = self._parse_sms(smsHtml)
397                 decoratedSms = self._decorate_sms(parsedSms)
398
399                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
400                 sortedMessages = list(allMessages)
401                 sortedMessages.sort(reverse=True)
402                 for exactDate, header, number, relativeDate, message in sortedMessages:
403                         yield header, number, relativeDate, message
404
405         def _grab_json(self, flatXml):
406                 xmlTree = ElementTree.fromstring(flatXml)
407                 jsonElement = xmlTree.getchildren()[0]
408                 flatJson = jsonElement.text
409                 jsonTree = parse_json(flatJson)
410                 return jsonTree
411
412         def _grab_html(self, flatXml):
413                 xmlTree = ElementTree.fromstring(flatXml)
414                 htmlElement = xmlTree.getchildren()[1]
415                 flatHtml = htmlElement.text
416                 return flatHtml
417
418         _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
419         _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
420         _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
421         _forwardURL = "https://www.google.com/voice/mobile/phones"
422
423         def _grab_account_info(self):
424                 page = self._browser.download(self._forwardURL)
425
426                 tokenGroup = self._tokenRe.search(page)
427                 if tokenGroup is None:
428                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
429                 self._token = tokenGroup.group(1)
430
431                 anGroup = self._accountNumRe.search(page)
432                 if anGroup is not None:
433                         self._accountNum = anGroup.group(1)
434                 else:
435                         logging.debug("Could not extract account number from GoogleVoice")
436
437                 self._callbackNumbers = {}
438                 for match in self._callbackRe.finditer(page):
439                         callbackNumber = match.group(2)
440                         callbackName = match.group(1)
441                         self._callbackNumbers[callbackNumber] = callbackName
442
443         def _send_validation(self, number):
444                 if not self.is_valid_syntax(number):
445                         raise ValueError('Number is not valid: "%s"' % number)
446                 elif not self.is_authed():
447                         raise RuntimeError("Not Authenticated")
448
449                 if len(number) == 11 and number[0] == 1:
450                         # Strip leading 1 from 11 digit dialing
451                         number = number[1:]
452                 return number
453
454         _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
455         _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
456         _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
457         _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
458
459         def _get_recent(self):
460                 """
461                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
462                 """
463                 for action, url in (
464                         ("Received", self._receivedCallsURL),
465                         ("Missed", self._missedCallsURL),
466                         ("Placed", self._placedCallsURL),
467                 ):
468                         try:
469                                 flatXml = self._browser.download(url)
470                         except urllib2.URLError, e:
471                                 logging.exception(str(e))
472                                 raise RuntimeError("%s is not accesible" % url)
473
474                         allRecentHtml = self._grab_html(flatXml)
475                         allRecentData = self._parse_voicemail(allRecentHtml)
476                         for recentCallData in allRecentData:
477                                 exactTime = recentCallData["time"]
478                                 if recentCallData["name"]:
479                                         header = recentCallData["name"]
480                                 elif recentCallData["prettyNumber"]:
481                                         header = recentCallData["prettyNumber"]
482                                 elif recentCallData["location"]:
483                                         header = recentCallData["location"]
484                                 else:
485                                         header = "Unknown"
486                                 yield header, recentCallData["number"], exactTime, recentCallData["relTime"], action
487
488         _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
489         _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
490         _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
491         _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
492         _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
493         _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
494         _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
495         #_voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
496         #_voicemailMessageRegex = re.compile(r"""<a .*? class="gc-message-mni">(.*?)</a>""", re.MULTILINE)
497         _voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
498
499         @staticmethod
500         def _interpret_voicemail_regex(group):
501                 quality, content, number = group.group(2), group.group(3), group.group(4)
502                 if quality is not None and content is not None:
503                         return quality, content
504                 elif number is not None:
505                         return "high", number
506
507         def _parse_voicemail(self, voicemailHtml):
508                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
509                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
510                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
511                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
512                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
513                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
514                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
515                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
516                         location = locationGroup.group(1).strip() if locationGroup else ""
517
518                         nameGroup = self._voicemailNameRegex.search(messageHtml)
519                         name = nameGroup.group(1).strip() if nameGroup else ""
520                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
521                         number = numberGroup.group(1).strip() if numberGroup else ""
522                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
523                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
524
525                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
526                         messageParts = (
527                                 self._interpret_voicemail_regex(group)
528                                 for group in messageGroups
529                         ) if messageGroups else ()
530
531                         yield {
532                                 "id": messageId.strip(),
533                                 "name": name,
534                                 "time": exactTime,
535                                 "relTime": relativeTime,
536                                 "prettyNumber": prettyNumber,
537                                 "number": number,
538                                 "location": location,
539                                 "messageParts": messageParts,
540                         }
541
542         def _decorate_voicemail(self, parsedVoicemail):
543                 messagePartFormat = {
544                         "med1": "<i>%s</i>",
545                         "med2": "%s",
546                         "high": "<b>%s</b>",
547                 }
548                 for voicemailData in parsedVoicemail:
549                         exactTime = voicemailData["time"]
550                         if voicemailData["name"]:
551                                 header = voicemailData["name"]
552                         elif voicemailData["prettyNumber"]:
553                                 header = voicemailData["prettyNumber"]
554                         elif voicemailData["location"]:
555                                 header = voicemailData["location"]
556                         else:
557                                 header = "Unknown"
558                         message = " ".join((
559                                 messagePartFormat[quality] % part
560                                 for (quality, part) in voicemailData["messageParts"]
561                         )).strip()
562                         if not message:
563                                 message = "No Transcription"
564                         yield exactTime, header, voicemailData["number"], voicemailData["relTime"], (message, )
565
566         _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
567         _smsTextRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
568         _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
569
570         def _parse_sms(self, smsHtml):
571                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
572                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
573                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
574                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
575                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
576                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
577                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
578
579                         nameGroup = self._voicemailNameRegex.search(messageHtml)
580                         name = nameGroup.group(1).strip() if nameGroup else ""
581                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
582                         number = numberGroup.group(1).strip() if numberGroup else ""
583                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
584                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
585
586                         fromGroups = self._smsFromRegex.finditer(messageHtml)
587                         fromParts = (group.group(1).strip() for group in fromGroups)
588                         textGroups = self._smsTextRegex.finditer(messageHtml)
589                         textParts = (group.group(1).strip() for group in textGroups)
590                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
591                         timeParts = (group.group(1).strip() for group in timeGroups)
592
593                         messageParts = itertools.izip(fromParts, textParts, timeParts)
594
595                         yield {
596                                 "id": messageId.strip(),
597                                 "name": name,
598                                 "time": exactTime,
599                                 "relTime": relativeTime,
600                                 "prettyNumber": prettyNumber,
601                                 "number": number,
602                                 "messageParts": messageParts,
603                         }
604
605         def _decorate_sms(self, parsedSms):
606                 for messageData in parsedSms:
607                         exactTime = messageData["time"]
608                         if messageData["name"]:
609                                 header = messageData["name"]
610                         elif messageData["prettyNumber"]:
611                                 header = messageData["prettyNumber"]
612                         else:
613                                 header = "Unknown"
614                         number = messageData["number"]
615                         relativeTime = messageData["relTime"]
616                         messages = [
617                                 "<b>%s</b>: %s" % (messagePart[0], messagePart[-1])
618                                 for messagePart in messageData["messageParts"]
619                         ]
620                         if not messages:
621                                 messages = ("No Transcription", )
622                         yield exactTime, header, number, relativeTime, messages
623
624
625 def test_backend(username, password):
626         backend = GVDialer()
627         print "Authenticated: ", backend.is_authed()
628         print "Login?: ", backend.login(username, password)
629         print "Authenticated: ", backend.is_authed()
630         # print "Token: ", backend._token
631         print "Account: ", backend.get_account_number()
632         print "Callback: ", backend.get_callback_number()
633         # print "All Callback: ",
634         import pprint
635         # pprint.pprint(backend.get_callback_numbers())
636         # print "Recent: ",
637         # pprint.pprint(list(backend.get_recent()))
638         # print "Contacts: ",
639         # for contact in backend.get_contacts():
640         #       print contact
641         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
642         for message in backend.get_messages():
643           pprint.pprint(message)
644
645         return backend