Lots of little work
[theonering] / src / gvoice / dialer.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 _moduleLogger = logging.getLogger("gvoice.dialer")
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 interact with the GoogleVoice 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         def is_authed(self, force = False):
123                 """
124                 Attempts to detect a current session
125                 @note Once logged in try not to reauth more than once a minute.
126                 @returns If authenticated
127                 """
128                 if (time.time() - self._lastAuthed) < 120 and not force:
129                         return True
130
131                 try:
132                         page = self._browser.download(self._forwardURL)
133                         self._grab_account_info(page)
134                 except Exception, e:
135                         _moduleLogger.exception(str(e))
136                         return False
137
138                 self._browser.cookies.save()
139                 self._lastAuthed = time.time()
140                 return True
141
142         _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
143
144         def login(self, username, password):
145                 """
146                 Attempt to login to GoogleVoice
147                 @returns Whether login was successful or not
148                 """
149                 loginPostData = urllib.urlencode({
150                         'Email' : username,
151                         'Passwd' : password,
152                         'service': "grandcentral",
153                         "ltmpl": "mobile",
154                         "btmpl": "mobile",
155                         "PersistentCookie": "yes",
156                         "continue": self._forwardURL,
157                 })
158
159                 try:
160                         loginSuccessOrFailurePage = self._browser.download(self._loginURL, loginPostData)
161                 except urllib2.URLError, e:
162                         _moduleLogger.exception(str(e))
163                         raise RuntimeError("%s is not accesible" % self._loginURL)
164
165                 try:
166                         self._grab_account_info(loginSuccessOrFailurePage)
167                 except Exception, e:
168                         _moduleLogger.exception(str(e))
169                         return False
170
171                 self._browser.cookies.save()
172                 self._lastAuthed = time.time()
173                 return True
174
175         def logout(self):
176                 self._lastAuthed = 0.0
177                 self._browser.cookies.clear()
178                 self._browser.cookies.save()
179
180         _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
181         _clicktocallURL = "https://www.google.com/voice/m/sendcall"
182
183         def dial(self, number):
184                 """
185                 This is the main function responsible for initating the callback
186                 """
187                 number = self._send_validation(number)
188                 try:
189                         clickToCallData = urllib.urlencode({
190                                 "number": number,
191                                 "phone": self._callbackNumber,
192                                 "_rnr_se": self._token,
193                         })
194                         otherData = {
195                                 'Referer' : 'https://google.com/voice/m/callsms',
196                         }
197                         callSuccessPage = self._browser.download(self._clicktocallURL, clickToCallData, None, otherData)
198                 except urllib2.URLError, e:
199                         _moduleLogger.exception(str(e))
200                         raise RuntimeError("%s is not accesible" % self._clicktocallURL)
201
202                 if self._gvDialingStrRe.search(callSuccessPage) is None:
203                         raise RuntimeError("Google Voice returned an error")
204
205                 return True
206
207         _sendSmsURL = "https://www.google.com/voice/m/sendsms"
208
209         def send_sms(self, number, message):
210                 number = self._send_validation(number)
211                 try:
212                         smsData = urllib.urlencode({
213                                 "number": number,
214                                 "smstext": message,
215                                 "_rnr_se": self._token,
216                                 "id": "undefined",
217                                 "c": "undefined",
218                         })
219                         otherData = {
220                                 'Referer' : 'https://google.com/voice/m/sms',
221                         }
222                         smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
223                 except urllib2.URLError, e:
224                         _moduleLogger.exception(str(e))
225                         raise RuntimeError("%s is not accesible" % self._sendSmsURL)
226
227                 return True
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 get_callback_numbers(self):
244                 """
245                 @returns a dictionary mapping call back numbers to descriptions
246                 @note These results are cached for 30 minutes.
247                 """
248                 if not self.is_authed():
249                         return {}
250                 return self._callbackNumbers
251
252         _setforwardURL = "https://www.google.com//voice/m/setphone"
253
254         def set_callback_number(self, callbacknumber):
255                 """
256                 Set the number that GoogleVoice calls
257                 @param callbacknumber should be a proper 10 digit number
258                 """
259                 self._callbackNumber = callbacknumber
260                 return True
261
262         def get_callback_number(self):
263                 """
264                 @returns Current callback number or None
265                 """
266                 return self._callbackNumber
267
268         _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
269         _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
270         _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
271         _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
272
273         def get_recent(self):
274                 """
275                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
276                 """
277                 for action, url in (
278                         ("Received", self._receivedCallsURL),
279                         ("Missed", self._missedCallsURL),
280                         ("Placed", self._placedCallsURL),
281                 ):
282                         try:
283                                 flatXml = self._browser.download(url)
284                         except urllib2.URLError, e:
285                                 _moduleLogger.exception(str(e))
286                                 raise RuntimeError("%s is not accesible" % url)
287
288                         allRecentHtml = self._grab_html(flatXml)
289                         allRecentData = self._parse_voicemail(allRecentHtml)
290                         for recentCallData in allRecentData:
291                                 recentCallData["action"] = action
292                                 yield recentCallData
293
294         _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
295         _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
296         _contactsURL = "https://www.google.com/voice/mobile/contacts"
297
298         def get_contacts(self):
299                 """
300                 @returns Iterable of (contact id, contact name)
301                 """
302                 contactsPagesUrls = [self._contactsURL]
303                 for contactsPageUrl in contactsPagesUrls:
304                         try:
305                                 contactsPage = self._browser.download(contactsPageUrl)
306                         except urllib2.URLError, e:
307                                 _moduleLogger.exception(str(e))
308                                 raise RuntimeError("%s is not accesible" % contactsPageUrl)
309                         for contact_match in self._contactsRe.finditer(contactsPage):
310                                 contactId = contact_match.group(1)
311                                 contactName = saxutils.unescape(contact_match.group(2))
312                                 contact = contactId, contactName
313                                 yield contact
314
315                         next_match = self._contactsNextRe.match(contactsPage)
316                         if next_match is not None:
317                                 newContactsPageUrl = self._contactsURL + next_match.group(1)
318                                 contactsPagesUrls.append(newContactsPageUrl)
319
320         _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9+\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
321         _contactDetailURL = "https://www.google.com/voice/mobile/contact"
322
323         def get_contact_details(self, contactId):
324                 """
325                 @returns Iterable of (Phone Type, Phone Number)
326                 """
327                 try:
328                         detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
329                 except urllib2.URLError, e:
330                         _moduleLogger.exception(str(e))
331                         raise RuntimeError("%s is not accesible" % self._contactDetailURL)
332
333                 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
334                         phoneNumber = detail_match.group(1)
335                         phoneType = saxutils.unescape(detail_match.group(2))
336                         yield (phoneType, phoneNumber)
337
338         _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
339         _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
340
341         def get_messages(self):
342                 try:
343                         voicemailPage = self._browser.download(self._voicemailURL)
344                 except urllib2.URLError, e:
345                         _moduleLogger.exception(str(e))
346                         raise RuntimeError("%s is not accesible" % self._voicemailURL)
347                 voicemailHtml = self._grab_html(voicemailPage)
348                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
349                 decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
350
351                 try:
352                         smsPage = self._browser.download(self._smsURL)
353                 except urllib2.URLError, e:
354                         _moduleLogger.exception(str(e))
355                         raise RuntimeError("%s is not accesible" % self._smsURL)
356                 smsHtml = self._grab_html(smsPage)
357                 parsedSms = self._parse_sms(smsHtml)
358                 decoratedSms = self._decorate_sms(parsedSms)
359
360                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
361                 return allMessages
362
363         def _grab_json(self, flatXml):
364                 xmlTree = ElementTree.fromstring(flatXml)
365                 jsonElement = xmlTree.getchildren()[0]
366                 flatJson = jsonElement.text
367                 jsonTree = parse_json(flatJson)
368                 return jsonTree
369
370         def _grab_html(self, flatXml):
371                 xmlTree = ElementTree.fromstring(flatXml)
372                 htmlElement = xmlTree.getchildren()[1]
373                 flatHtml = htmlElement.text
374                 return flatHtml
375
376         _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
377         _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
378         _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
379         _forwardURL = "https://www.google.com/voice/mobile/phones"
380
381         def _grab_account_info(self, page):
382                 tokenGroup = self._tokenRe.search(page)
383                 if tokenGroup is None:
384                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
385                 self._token = tokenGroup.group(1)
386
387                 anGroup = self._accountNumRe.search(page)
388                 if anGroup is not None:
389                         self._accountNum = anGroup.group(1)
390                 else:
391                         _moduleLogger.debug("Could not extract account number from GoogleVoice")
392
393                 self._callbackNumbers = {}
394                 for match in self._callbackRe.finditer(page):
395                         callbackNumber = match.group(2)
396                         callbackName = match.group(1)
397                         self._callbackNumbers[callbackNumber] = callbackName
398
399         def _send_validation(self, number):
400                 if not self.is_valid_syntax(number):
401                         raise ValueError('Number is not valid: "%s"' % number)
402                 elif not self.is_authed():
403                         raise RuntimeError("Not Authenticated")
404
405                 if len(number) == 11 and number[0] == 1:
406                         # Strip leading 1 from 11 digit dialing
407                         number = number[1:]
408                 return number
409
410         _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
411         _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
412         _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
413         _voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
414         _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
415         _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
416         _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
417         _messagesContactID = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
418         #_voicemailMessageRegex = re.compile(r"""<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
419         #_voicemailMessageRegex = re.compile(r"""<a .*? class="gc-message-mni">(.*?)</a>""", re.MULTILINE)
420         _voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
421
422         @staticmethod
423         def _interpret_voicemail_regex(group):
424                 quality, content, number = group.group(2), group.group(3), group.group(4)
425                 if quality is not None and content is not None:
426                         return quality, content
427                 elif number is not None:
428                         return "high", number
429
430         def _parse_voicemail(self, voicemailHtml):
431                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
432                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
433                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
434                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
435                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
436                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
437                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
438                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
439                         location = locationGroup.group(1).strip() if locationGroup else ""
440
441                         nameGroup = self._voicemailNameRegex.search(messageHtml)
442                         name = nameGroup.group(1).strip() if nameGroup else ""
443                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
444                         number = numberGroup.group(1).strip() if numberGroup else ""
445                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
446                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
447                         contactIdGroup = self._messagesContactID.search(messageHtml)
448                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
449
450                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
451                         messageParts = (
452                                 self._interpret_voicemail_regex(group)
453                                 for group in messageGroups
454                         ) if messageGroups else ()
455
456                         yield {
457                                 "id": messageId.strip(),
458                                 "contactId": contactId,
459                                 "name": name,
460                                 "time": exactTime,
461                                 "relTime": relativeTime,
462                                 "prettyNumber": prettyNumber,
463                                 "number": number,
464                                 "location": location,
465                                 "messageParts": messageParts,
466                         }
467
468         def _decorate_voicemail(self, parsedVoicemails):
469                 messagePartFormat = {
470                         "med1": "<i>%s</i>",
471                         "med2": "%s",
472                         "high": "<b>%s</b>",
473                 }
474                 for voicemailData in parsedVoicemails:
475                         message = " ".join((
476                                 messagePartFormat[quality] % part
477                                 for (quality, part) in voicemailData["messageParts"]
478                         )).strip()
479                         if not message:
480                                 message = "No Transcription"
481                         whoFrom = voicemailData["name"]
482                         when = voicemailData["time"]
483                         voicemailData["messageParts"] = ((whoFrom, message, when), )
484                         yield voicemailData
485
486         _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
487         _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
488         _smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
489
490         def _parse_sms(self, smsHtml):
491                 splitSms = self._seperateVoicemailsRegex.split(smsHtml)
492                 for messageId, messageHtml in itergroup(splitSms[1:], 2):
493                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
494                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
495                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
496                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
497                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
498
499                         nameGroup = self._voicemailNameRegex.search(messageHtml)
500                         name = nameGroup.group(1).strip() if nameGroup else ""
501                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
502                         number = numberGroup.group(1).strip() if numberGroup else ""
503                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
504                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
505                         contactIdGroup = self._messagesContactID.search(messageHtml)
506                         contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
507
508                         fromGroups = self._smsFromRegex.finditer(messageHtml)
509                         fromParts = (group.group(1).strip() for group in fromGroups)
510                         textGroups = self._smsTextRegex.finditer(messageHtml)
511                         textParts = (group.group(1).strip() for group in textGroups)
512                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
513                         timeParts = (group.group(1).strip() for group in timeGroups)
514
515                         messageParts = itertools.izip(fromParts, textParts, timeParts)
516
517                         yield {
518                                 "id": messageId.strip(),
519                                 "contactId": contactId,
520                                 "name": name,
521                                 "time": exactTime,
522                                 "relTime": relativeTime,
523                                 "prettyNumber": prettyNumber,
524                                 "number": number,
525                                 "location": "",
526                                 "messageParts": messageParts,
527                         }
528
529         def _decorate_sms(self, parsedTexts):
530                 return parsedTexts
531
532
533 def set_sane_callback(backend):
534         """
535         Try to set a sane default callback number on these preferences
536         1) 1747 numbers ( Gizmo )
537         2) anything with gizmo in the name
538         3) anything with computer in the name
539         4) the first value
540         """
541         numbers = backend.get_callback_numbers()
542
543         priorityOrderedCriteria = [
544                 ("1747", None),
545                 (None, "gizmo"),
546                 (None, "computer"),
547                 (None, "sip"),
548                 (None, None),
549         ]
550
551         for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
552                 for number, description in numbers.iteritems():
553                         if numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
554                                 continue
555                         if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
556                                 continue
557                         backend.set_callback_number(number)
558                         return
559
560
561 def sort_messages(allMessages):
562         sortableAllMessages = [
563                 (message["time"], message)
564                 for message in allMessages
565         ]
566         sortableAllMessages.sort(reverse=True)
567         return (
568                 message
569                 for (exactTime, message) in sortableAllMessages
570         )
571
572
573 def decorate_recent(recentCallData):
574         """
575         @returns (personsName, phoneNumber, date, action)
576         """
577         if recentCallData["name"]:
578                 header = recentCallData["name"]
579         elif recentCallData["prettyNumber"]:
580                 header = recentCallData["prettyNumber"]
581         elif recentCallData["location"]:
582                 header = recentCallData["location"]
583         else:
584                 header = "Unknown"
585
586         number = recentCallData["number"]
587         relTime = recentCallData["relTime"]
588         action = recentCallData["action"]
589         return header, number, relTime, action
590
591
592 def decorate_message(messageData):
593         exactTime = messageData["time"]
594         if messageData["name"]:
595                 header = messageData["name"]
596         elif messageData["prettyNumber"]:
597                 header = messageData["prettyNumber"]
598         else:
599                 header = "Unknown"
600         number = messageData["number"]
601         relativeTime = messageData["relTime"]
602
603         messageParts = list(messageData["messageParts"])
604         if len(messageParts) == 0:
605                 messages = ("No Transcription", )
606         elif len(messageParts) == 1:
607                 messages = (messageParts[0][1], )
608         else:
609                 messages = [
610                         "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
611                         for messagePart in messageParts
612                 ]
613
614         decoratedResults = header, number, relativeTime, messages
615         return decoratedResults
616
617
618 def test_backend(username, password):
619         backend = GVDialer()
620         print "Authenticated: ", backend.is_authed()
621         if not backend.is_authed():
622                 print "Login?: ", backend.login(username, password)
623         print "Authenticated: ", backend.is_authed()
624
625         #print "Token: ", backend._token
626         #print "Account: ", backend.get_account_number()
627         #print "Callback: ", backend.get_callback_number()
628         #print "All Callback: ",
629         #import pprint
630         #pprint.pprint(backend.get_callback_numbers())
631
632         #print "Recent: "
633         #for data in backend.get_recent():
634         #       pprint.pprint(data)
635         #for data in sort_messages(backend.get_recent()):
636         #       pprint.pprint(decorate_recent(data))
637         #pprint.pprint(list(backend.get_recent()))
638
639         #print "Contacts: ",
640         #for contact in backend.get_contacts():
641         #       print contact
642         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
643
644         #print "Messages: ",
645         #for message in backend.get_messages():
646         #       pprint.pprint(message)
647         #for message in sort_messages(backend.get_messages()):
648         #       pprint.pprint(decorate_message(message))
649
650         return backend
651
652
653 if __name__ == "__main__":
654         import sys
655         logging.basicConfig(level=logging.DEBUG)
656         test_backend(sys.argv[1], sys.argv[2])