* Fixing GC support and applying it to GV
[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 = None
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) < 60 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 time.time() - self._lastAuthed < 1800 or self.is_authed():
279                         return self._callbackNumbers
280
281                 return {}
282
283         _setforwardURL = "https://www.google.com//voice/m/setphone"
284
285         def set_callback_number(self, callbacknumber):
286                 """
287                 Set the number that grandcental calls
288                 @param callbacknumber should be a proper 10 digit number
289                 """
290                 self._callbackNumber = callbacknumber
291                 callbackPostData = urllib.urlencode({
292                         '_rnr_se': self._token,
293                         'phone': callbacknumber
294                 })
295                 try:
296                         callbackSetPage = self._browser.download(self._setforwardURL, callbackPostData)
297                 except urllib2.URLError, e:
298                         warnings.warn(traceback.format_exc())
299                         raise RuntimeError("%s is not accesible" % self._setforwardURL)
300
301                 # @bug This does not seem to be keeping on my tablet (but works on the
302                 # desktop), or the reading isn't working too well
303                 self._browser.cookies.save()
304                 return True
305
306         def get_callback_number(self):
307                 """
308                 @returns Current callback number or None
309                 """
310                 for c in self._browser.cookies:
311                         if c.name == "gv-ph":
312                                 return c.value
313                 return self._callbackNumber
314
315         def get_recent(self):
316                 """
317                 @returns Iterable of (personsName, phoneNumber, date, action)
318                 """
319                 sortedRecent = [
320                         (exactDate, name, number, relativeDate, action)
321                         for (name, number, exactDate, relativeDate, action) in self._get_recent()
322                 ]
323                 sortedRecent.sort(reverse = True)
324                 for exactDate, name, number, relativeDate, action in sortedRecent:
325                         yield name, number, relativeDate, action
326
327         def get_addressbooks(self):
328                 """
329                 @returns Iterable of (Address Book Factory, Book Id, Book Name)
330                 """
331                 yield self, "", ""
332
333         def open_addressbook(self, bookId):
334                 return self
335
336         @staticmethod
337         def contact_source_short_name(contactId):
338                 return "GV"
339
340         @staticmethod
341         def factory_name():
342                 return "Google Voice"
343
344         _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
345         _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
346         _contactsURL = "https://www.google.com/voice/mobile/contacts"
347
348         def get_contacts(self):
349                 """
350                 @returns Iterable of (contact id, contact name)
351                 """
352                 if self.__contacts is None:
353                         self.__contacts = []
354
355                         contactsPagesUrls = [self._contactsURL]
356                         for contactsPageUrl in contactsPagesUrls:
357                                 try:
358                                         contactsPage = self._browser.download(contactsPageUrl)
359                                 except urllib2.URLError, e:
360                                         warnings.warn(traceback.format_exc())
361                                         raise RuntimeError("%s is not accesible" % contactsPageUrl)
362                                 for contact_match in self._contactsRe.finditer(contactsPage):
363                                         contactId = contact_match.group(1)
364                                         contactName = saxutils.unescape(contact_match.group(2))
365                                         contact = contactId, contactName
366                                         self.__contacts.append(contact)
367                                         yield contact
368
369                                 next_match = self._contactsNextRe.match(contactsPage)
370                                 if next_match is not None:
371                                         newContactsPageUrl = self._contactsURL + next_match.group(1)
372                                         contactsPagesUrls.append(newContactsPageUrl)
373                 else:
374                         for contact in self.__contacts:
375                                 yield contact
376
377         _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
378         _contactDetailURL = "https://www.google.com/voice/mobile/contact"
379
380         def get_contact_details(self, contactId):
381                 """
382                 @returns Iterable of (Phone Type, Phone Number)
383                 """
384                 try:
385                         detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
386                 except urllib2.URLError, e:
387                         warnings.warn(traceback.format_exc())
388                         raise RuntimeError("%s is not accesible" % self._contactDetailURL)
389
390                 for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
391                         phoneNumber = detail_match.group(1)
392                         phoneType = saxutils.unescape(detail_match.group(2))
393                         yield (phoneType, phoneNumber)
394
395         _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
396         _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
397
398         def get_messages(self):
399                 try:
400                         voicemailPage = self._browser.download(self._voicemailURL)
401                 except urllib2.URLError, e:
402                         warnings.warn(traceback.format_exc())
403                         raise RuntimeError("%s is not accesible" % self._voicemailURL)
404                 voicemailHtml = self._grab_html(voicemailPage)
405                 parsedVoicemail = self._parse_voicemail(voicemailHtml)
406                 decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
407
408                 try:
409                         smsPage = self._browser.download(self._smsURL)
410                 except urllib2.URLError, e:
411                         warnings.warn(traceback.format_exc())
412                         raise RuntimeError("%s is not accesible" % self._smsURL)
413                 smsHtml = self._grab_html(smsPage)
414                 parsedSms = self._parse_sms(smsHtml)
415                 decoratedSms = self._decorate_sms(parsedSms)
416
417                 allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
418                 sortedMessages = list(allMessages)
419                 sortedMessages.sort(reverse=True)
420                 for exactDate, header, number, relativeDate, message in sortedMessages:
421                         yield header, number, relativeDate, message
422
423         def _grab_json(self, flatXml):
424                 xmlTree = ElementTree.fromstring(flatXml)
425                 jsonElement = xmlTree.getchildren()[0]
426                 flatJson = jsonElement.text
427                 jsonTree = parse_json(flatJson)
428                 return jsonTree
429
430         def _grab_html(self, flatXml):
431                 xmlTree = ElementTree.fromstring(flatXml)
432                 htmlElement = xmlTree.getchildren()[1]
433                 flatHtml = htmlElement.text
434                 return flatHtml
435
436         _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
437         _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
438         _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
439         _forwardURL = "https://www.google.com/voice/mobile/phones"
440
441         def _grab_account_info(self):
442                 page = self._browser.download(self._forwardURL)
443
444                 tokenGroup = self._tokenRe.search(page)
445                 if tokenGroup is None:
446                         raise RuntimeError("Could not extract authentication token from GoogleVoice")
447                 self._token = tokenGroup.group(1)
448
449                 anGroup = self._accountNumRe.search(page)
450                 if anGroup is not None:
451                         self._accountNum = anGroup.group(1)
452                 else:
453                         warnings.warn("Could not extract account number from GoogleVoice", UserWarning, 2)
454
455                 self._callbackNumbers = {}
456                 for match in self._callbackRe.finditer(page):
457                         callbackNumber = match.group(2)
458                         callbackName = match.group(1)
459                         self._callbackNumbers[callbackNumber] = callbackName
460
461         def _send_validation(self, number):
462                 if not self.is_valid_syntax(number):
463                         raise ValueError('Number is not valid: "%s"' % number)
464                 elif not self.is_authed():
465                         raise RuntimeError("Not Authenticated")
466
467                 if len(number) == 11 and number[0] == 1:
468                         # Strip leading 1 from 11 digit dialing
469                         number = number[1:]
470                 return number
471
472         _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
473         _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
474         _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
475         _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
476
477         def _get_recent(self):
478                 """
479                 @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
480                 """
481                 for url in (
482                         self._receivedCallsURL,
483                         self._missedCallsURL,
484                         self._placedCallsURL,
485                 ):
486                         try:
487                                 flatXml = self._browser.download(url)
488                         except urllib2.URLError, e:
489                                 warnings.warn(traceback.format_exc())
490                                 raise RuntimeError("%s is not accesible" % url)
491
492                         allRecentData = self._grab_json(flatXml)
493                         for recentCallData in allRecentData["messages"].itervalues():
494                                 number = recentCallData["displayNumber"]
495                                 exactDate = recentCallData["displayStartDateTime"]
496                                 relativeDate = recentCallData["relativeStartTime"]
497                                 action = ", ".join((
498                                         label.title()
499                                         for label in recentCallData["labels"]
500                                                 if label.lower() != "all" and label.lower() != "inbox"
501                                 ))
502                                 number = saxutils.unescape(number)
503                                 exactDate = saxutils.unescape(exactDate)
504                                 exactDate = datetime.datetime.strptime(exactDate, "%m/%d/%y %I:%M %p")
505                                 relativeDate = saxutils.unescape(relativeDate)
506                                 action = saxutils.unescape(action)
507                                 yield "", number, exactDate, relativeDate, action
508
509         _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class="gc-message.*?">""", re.MULTILINE | re.DOTALL)
510         _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
511         _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
512         _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
513         _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
514         _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
515         _voicemailMessageRegex = re.compile(r"""<span class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
516
517         def _parse_voicemail(self, voicemailHtml):
518                 splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
519                 for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
520                         exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
521                         exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
522                         exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
523                         relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
524                         relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
525                         locationGroup = self._voicemailLocationRegex.search(messageHtml)
526                         location = locationGroup.group(1).strip() if locationGroup else ""
527
528                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
529                         number = numberGroup.group(1).strip() if numberGroup else ""
530                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
531                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
532
533                         messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
534                         messageParts = (
535                                 (group.group(1).strip(), group.group(2).strip())
536                                 for group in messageGroups
537                         ) if messageGroups else ()
538
539                         yield {
540                                 "id": messageId.strip(),
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                         header = "%s %s" % (voicemailData["prettyNumber"], voicemailData["location"])
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                         numberGroup = self._voicemailNumberRegex.search(messageHtml)
580                         number = numberGroup.group(1).strip() if numberGroup else ""
581                         prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
582                         prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
583
584                         fromGroups = self._smsFromRegex.finditer(messageHtml)
585                         fromParts = (group.group(1).strip() for group in fromGroups)
586                         textGroups = self._smsTextRegex.finditer(messageHtml)
587                         textParts = (group.group(1).strip() for group in textGroups)
588                         timeGroups = self._smsTimeRegex.finditer(messageHtml)
589                         timeParts = (group.group(1).strip() for group in timeGroups)
590
591                         messageParts = itertools.izip(fromParts, textParts, timeParts)
592
593                         yield {
594                                 "id": messageId.strip(),
595                                 "time": exactTime,
596                                 "relTime": relativeTime,
597                                 "prettyNumber": prettyNumber,
598                                 "number": number,
599                                 "messageParts": messageParts,
600                         }
601
602         def _decorate_sms(self, parsedSms):
603                 for messageData in parsedSms:
604                         exactTime = messageData["time"]
605                         header = "%s" % (messageData["prettyNumber"])
606                         number = messageData["number"]
607                         relativeTime = messageData["relTime"]
608                         message = "\n".join((
609                                 "<b>%s (%s)</b>: %s" % messagePart
610                                 for messagePart in messageData["messageParts"]
611                         ))
612                         if not message:
613                                 message = "No Transcription"
614                         yield exactTime, header, number, relativeTime, message
615
616
617 def test_backend(username, password):
618         import pprint
619         backend = GVDialer()
620         print "Authenticated: ", backend.is_authed()
621         print "Login?: ", backend.login(username, password)
622         print "Authenticated: ", backend.is_authed()
623         # print "Token: ", backend._token
624         print "Account: ", backend.get_account_number()
625         print "Callback: ", backend.get_callback_number()
626         # print "All Callback: ",
627         # pprint.pprint(backend.get_callback_numbers())
628         # print "Recent: ",
629         # pprint.pprint(list(backend.get_recent()))
630         # print "Contacts: ",
631         # for contact in backend.get_contacts():
632         #       print contact
633         #       pprint.pprint(list(backend.get_contact_details(contact[0])))
634
635         return backend