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