BROKEN: Moved everything
[gc-dialer] / src / backends / gvoice / gvoice.py
diff --git a/src/backends/gvoice/gvoice.py b/src/backends/gvoice/gvoice.py
deleted file mode 100755 (executable)
index b0825ef..0000000
+++ /dev/null
@@ -1,1050 +0,0 @@
-#!/usr/bin/python
-
-"""
-DialCentral - Front end for Google's GoogleVoice service.
-Copyright (C) 2008  Eric Warnke ericew AT gmail DOT com
-
-This library is free software; you can redistribute it and/or
-modify it under the terms of the GNU Lesser General Public
-License as published by the Free Software Foundation; either
-version 2.1 of the License, or (at your option) any later version.
-
-This library is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
-Lesser General Public License for more details.
-
-You should have received a copy of the GNU Lesser General Public
-License along with this library; if not, write to the Free Software
-Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
-
-Google Voice backend code
-
-Resources
-       http://thatsmith.com/2009/03/google-voice-addon-for-firefox/
-       http://posttopic.com/topic/google-voice-add-on-development
-"""
-
-from __future__ import with_statement
-
-import os
-import re
-import urllib
-import urllib2
-import time
-import datetime
-import itertools
-import logging
-import inspect
-
-from xml.sax import saxutils
-from xml.etree import ElementTree
-
-try:
-       import simplejson as _simplejson
-       simplejson = _simplejson
-except ImportError:
-       simplejson = None
-
-import browser_emu
-
-
-_moduleLogger = logging.getLogger(__name__)
-
-
-class NetworkError(RuntimeError):
-       pass
-
-
-class MessageText(object):
-
-       ACCURACY_LOW = "med1"
-       ACCURACY_MEDIUM = "med2"
-       ACCURACY_HIGH = "high"
-
-       def __init__(self):
-               self.accuracy = None
-               self.text = None
-
-       def __str__(self):
-               return self.text
-
-       def to_dict(self):
-               return to_dict(self)
-
-       def __eq__(self, other):
-               return self.accuracy == other.accuracy and self.text == other.text
-
-
-class Message(object):
-
-       def __init__(self):
-               self.whoFrom = None
-               self.body = None
-               self.when = None
-
-       def __str__(self):
-               return "%s (%s): %s" % (
-                       self.whoFrom,
-                       self.when,
-                       "".join(unicode(part) for part in self.body)
-               )
-
-       def to_dict(self):
-               selfDict = to_dict(self)
-               selfDict["body"] = [text.to_dict() for text in self.body] if self.body is not None else None
-               return selfDict
-
-       def __eq__(self, other):
-               return self.whoFrom == other.whoFrom and self.when == other.when and self.body == other.body
-
-
-class Conversation(object):
-
-       TYPE_VOICEMAIL = "Voicemail"
-       TYPE_SMS = "SMS"
-
-       def __init__(self):
-               self.type = None
-               self.id = None
-               self.contactId = None
-               self.name = None
-               self.location = None
-               self.prettyNumber = None
-               self.number = None
-
-               self.time = None
-               self.relTime = None
-               self.messages = None
-               self.isRead = None
-               self.isSpam = None
-               self.isTrash = None
-               self.isArchived = None
-
-       def __cmp__(self, other):
-               cmpValue = cmp(self.contactId, other.contactId)
-               if cmpValue != 0:
-                       return cmpValue
-
-               cmpValue = cmp(self.time, other.time)
-               if cmpValue != 0:
-                       return cmpValue
-
-               cmpValue = cmp(self.id, other.id)
-               if cmpValue != 0:
-                       return cmpValue
-
-       def to_dict(self):
-               selfDict = to_dict(self)
-               selfDict["messages"] = [message.to_dict() for message in self.messages] if self.messages is not None else None
-               return selfDict
-
-
-class GVoiceBackend(object):
-       """
-       This class encapsulates all of the knowledge necessary to interact with the GoogleVoice servers
-       the functions include login, setting up a callback number, and initalting a callback
-       """
-
-       PHONE_TYPE_HOME = 1
-       PHONE_TYPE_MOBILE = 2
-       PHONE_TYPE_WORK = 3
-       PHONE_TYPE_GIZMO = 7
-
-       def __init__(self, cookieFile = None):
-               # Important items in this function are the setup of the browser emulation and cookie file
-               self._browser = browser_emu.MozillaEmulator(1)
-               self._loadedFromCookies = self._browser.load_cookies(cookieFile)
-
-               self._token = ""
-               self._accountNum = ""
-               self._lastAuthed = 0.0
-               self._callbackNumber = ""
-               self._callbackNumbers = {}
-
-               # Suprisingly, moving all of these from class to self sped up startup time
-
-               self._validateRe = re.compile("^\+?[0-9]{10,}$")
-
-               self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
-
-               SECURE_URL_BASE = "https://www.google.com/voice/"
-               SECURE_MOBILE_URL_BASE = SECURE_URL_BASE + "mobile/"
-               self._tokenURL = SECURE_URL_BASE + "m"
-               self._callUrl = SECURE_URL_BASE + "call/connect"
-               self._callCancelURL = SECURE_URL_BASE + "call/cancel"
-               self._sendSmsURL = SECURE_URL_BASE + "sms/send"
-
-               self._isDndURL = "https://www.google.com/voice/m/donotdisturb"
-               self._isDndRe = re.compile(r"""<input.*?id="doNotDisturb".*?checked="(.*?)"\s*/>""")
-               self._setDndURL = "https://www.google.com/voice/m/savednd"
-
-               self._downloadVoicemailURL = SECURE_URL_BASE + "media/send_voicemail/"
-               self._markAsReadURL = SECURE_URL_BASE + "m/mark"
-               self._archiveMessageURL = SECURE_URL_BASE + "m/archive"
-
-               self._XML_SEARCH_URL = SECURE_URL_BASE + "inbox/search/"
-               self._XML_ACCOUNT_URL = SECURE_URL_BASE + "contacts/"
-               # HACK really this redirects to the main pge and we are grabbing some javascript
-               self._XML_CONTACTS_URL = "http://www.google.com/voice/inbox/search/contact"
-               self._CSV_CONTACTS_URL = "http://mail.google.com/mail/contacts/data/export"
-               self._JSON_CONTACTS_URL = SECURE_URL_BASE + "b/0/request/user"
-               self._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
-
-               self.XML_FEEDS = (
-                       'inbox', 'starred', 'all', 'spam', 'trash', 'voicemail', 'sms',
-                       'recorded', 'placed', 'received', 'missed'
-               )
-               self._XML_INBOX_URL = SECURE_URL_BASE + "inbox/recent/inbox"
-               self._XML_STARRED_URL = SECURE_URL_BASE + "inbox/recent/starred"
-               self._XML_ALL_URL = SECURE_URL_BASE + "inbox/recent/all"
-               self._XML_SPAM_URL = SECURE_URL_BASE + "inbox/recent/spam"
-               self._XML_TRASH_URL = SECURE_URL_BASE + "inbox/recent/trash"
-               self._XML_VOICEMAIL_URL = SECURE_URL_BASE + "inbox/recent/voicemail/"
-               self._XML_SMS_URL = SECURE_URL_BASE + "inbox/recent/sms/"
-               self._JSON_SMS_URL = SECURE_URL_BASE + "b/0/request/messages/"
-               self._JSON_SMS_COUNT_URL = SECURE_URL_BASE + "b/0/request/unread"
-               self._XML_RECORDED_URL = SECURE_URL_BASE + "inbox/recent/recorded/"
-               self._XML_PLACED_URL = SECURE_URL_BASE + "inbox/recent/placed/"
-               self._XML_RECEIVED_URL = SECURE_URL_BASE + "inbox/recent/received/"
-               self._XML_MISSED_URL = SECURE_URL_BASE + "inbox/recent/missed/"
-
-               self._galxRe = re.compile(r"""<input.*?name="GALX".*?value="(.*?)".*?/>""", re.MULTILINE | re.DOTALL)
-
-               self._seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class=".*?gc-message.*?">""", re.MULTILINE | re.DOTALL)
-               self._exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
-               self._relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
-               self._voicemailNameRegex = re.compile(r"""<a class=.*?gc-message-name-link.*?>(.*?)</a>""", re.MULTILINE | re.DOTALL)
-               self._voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
-               self._prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
-               self._voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">.*?<a.*?>(.*?)</a></span>""", re.MULTILINE)
-               self._messagesContactIDRegex = re.compile(r"""<a class=".*?gc-message-name-link.*?">.*?</a>\s*?<span .*?>(.*?)</span>""", re.MULTILINE)
-               self._voicemailMessageRegex = re.compile(r"""(<span id="\d+-\d+" class="gc-word-(.*?)">(.*?)</span>|<a .*? class="gc-message-mni">(.*?)</a>)""", re.MULTILINE)
-               self._smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
-               self._smsTimeRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
-               self._smsTextRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
-
-       def is_quick_login_possible(self):
-               """
-               @returns True then refresh_account_info might be enough to login, else full login is required
-               """
-               return self._loadedFromCookies or 0.0 < self._lastAuthed
-
-       def refresh_account_info(self):
-               try:
-                       page = self._get_page(self._JSON_CONTACTS_URL)
-                       accountData = self._grab_account_info(page)
-               except Exception, e:
-                       _moduleLogger.exception(str(e))
-                       return None
-
-               self._browser.save_cookies()
-               self._lastAuthed = time.time()
-               return accountData
-
-       def _get_token(self):
-               tokenPage = self._get_page(self._tokenURL)
-
-               galxTokens = self._galxRe.search(tokenPage)
-               if galxTokens is not None:
-                       galxToken = galxTokens.group(1)
-               else:
-                       galxToken = ""
-                       _moduleLogger.debug("Could not grab GALX token")
-               return galxToken
-
-       def _login(self, username, password, token):
-               loginData = {
-                       'Email' : username,
-                       'Passwd' : password,
-                       'service': "grandcentral",
-                       "ltmpl": "mobile",
-                       "btmpl": "mobile",
-                       "PersistentCookie": "yes",
-                       "GALX": token,
-                       "continue": self._JSON_CONTACTS_URL,
-               }
-
-               loginSuccessOrFailurePage = self._get_page(self._loginURL, loginData)
-               return loginSuccessOrFailurePage
-
-       def login(self, username, password):
-               """
-               Attempt to login to GoogleVoice
-               @returns Whether login was successful or not
-               @blocks
-               """
-               self.logout()
-               galxToken = self._get_token()
-               loginSuccessOrFailurePage = self._login(username, password, galxToken)
-
-               try:
-                       accountData = self._grab_account_info(loginSuccessOrFailurePage)
-               except Exception, e:
-                       # Retry in case the redirect failed
-                       # luckily refresh_account_info does everything we need for a retry
-                       accountData = self.refresh_account_info()
-                       if accountData is None:
-                               _moduleLogger.exception(str(e))
-                               return None
-                       _moduleLogger.info("Redirection failed on initial login attempt, auto-corrected for this")
-
-               self._browser.save_cookies()
-               self._lastAuthed = time.time()
-               return accountData
-
-       def persist(self):
-               self._browser.save_cookies()
-
-       def shutdown(self):
-               self._browser.save_cookies()
-               self._token = None
-               self._lastAuthed = 0.0
-
-       def logout(self):
-               self._browser.clear_cookies()
-               self._browser.save_cookies()
-               self._token = None
-               self._lastAuthed = 0.0
-               self._callbackNumbers = {}
-
-       def is_dnd(self):
-               """
-               @blocks
-               """
-               isDndPage = self._get_page(self._isDndURL)
-
-               dndGroup = self._isDndRe.search(isDndPage)
-               if dndGroup is None:
-                       return False
-               dndStatus = dndGroup.group(1)
-               isDnd = True if dndStatus.strip().lower() == "true" else False
-               return isDnd
-
-       def set_dnd(self, doNotDisturb):
-               """
-               @blocks
-               """
-               dndPostData = {
-                       "doNotDisturb": 1 if doNotDisturb else 0,
-               }
-
-               dndPage = self._get_page_with_token(self._setDndURL, dndPostData)
-
-       def call(self, outgoingNumber):
-               """
-               This is the main function responsible for initating the callback
-               @blocks
-               """
-               outgoingNumber = self._send_validation(outgoingNumber)
-               subscriberNumber = None
-               phoneType = guess_phone_type(self._callbackNumber) # @todo Fix this hack
-
-               callData = {
-                               'outgoingNumber': outgoingNumber,
-                               'forwardingNumber': self._callbackNumber,
-                               'subscriberNumber': subscriberNumber or 'undefined',
-                               'phoneType': str(phoneType),
-                               'remember': '1',
-               }
-               _moduleLogger.info("%r" % callData)
-
-               page = self._get_page_with_token(
-                       self._callUrl,
-                       callData,
-               )
-               self._parse_with_validation(page)
-               return True
-
-       def cancel(self, outgoingNumber=None):
-               """
-               Cancels a call matching outgoing and forwarding numbers (if given). 
-               Will raise an error if no matching call is being placed
-               @blocks
-               """
-               page = self._get_page_with_token(
-                       self._callCancelURL,
-                       {
-                       'outgoingNumber': outgoingNumber or 'undefined',
-                       'forwardingNumber': self._callbackNumber or 'undefined',
-                       'cancelType': 'C2C',
-                       },
-               )
-               self._parse_with_validation(page)
-
-       def send_sms(self, phoneNumbers, message):
-               """
-               @blocks
-               """
-               validatedPhoneNumbers = [
-                       self._send_validation(phoneNumber)
-                       for phoneNumber in phoneNumbers
-               ]
-               flattenedPhoneNumbers = ",".join(validatedPhoneNumbers)
-               page = self._get_page_with_token(
-                       self._sendSmsURL,
-                       {
-                               'phoneNumber': flattenedPhoneNumbers,
-                               'text': unicode(message).encode("utf-8"),
-                       },
-               )
-               self._parse_with_validation(page)
-
-       def search(self, query):
-               """
-               Search your Google Voice Account history for calls, voicemails, and sms
-               Returns ``Folder`` instance containting matching messages
-               @blocks
-               """
-               page = self._get_page(
-                       self._XML_SEARCH_URL,
-                       {"q": query},
-               )
-               json, html = extract_payload(page)
-               return json
-
-       def get_feed(self, feed):
-               """
-               @blocks
-               """
-               actualFeed = "_XML_%s_URL" % feed.upper()
-               feedUrl = getattr(self, actualFeed)
-
-               page = self._get_page(feedUrl)
-               json, html = extract_payload(page)
-
-               return json
-
-       def recording_url(self, messageId):
-               url = self._downloadVoicemailURL+messageId
-               return url
-
-       def download(self, messageId, targetPath):
-               """
-               Download a voicemail or recorded call MP3 matching the given ``msg``
-               which can either be a ``Message`` instance, or a SHA1 identifier. 
-               Message hashes can be found in ``self.voicemail().messages`` for example. 
-               @returns location of saved file.
-               @blocks
-               """
-               page = self._get_page(self.recording_url(messageId))
-               with open(targetPath, 'wb') as fo:
-                       fo.write(page)
-
-       def is_valid_syntax(self, number):
-               """
-               @returns If This number be called ( syntax validation only )
-               """
-               return self._validateRe.match(number) is not None
-
-       def get_account_number(self):
-               """
-               @returns The GoogleVoice phone number
-               """
-               return self._accountNum
-
-       def get_callback_numbers(self):
-               """
-               @returns a dictionary mapping call back numbers to descriptions
-               @note These results are cached for 30 minutes.
-               """
-               return self._callbackNumbers
-
-       def set_callback_number(self, callbacknumber):
-               """
-               Set the number that GoogleVoice calls
-               @param callbacknumber should be a proper 10 digit number
-               """
-               self._callbackNumber = callbacknumber
-               _moduleLogger.info("Callback number changed: %r" % self._callbackNumber)
-               return True
-
-       def get_callback_number(self):
-               """
-               @returns Current callback number or None
-               """
-               return self._callbackNumber
-
-       def get_received_calls(self):
-               """
-               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
-               @blocks
-               """
-               return self._parse_recent(self._get_page(self._XML_RECEIVED_URL))
-
-       def get_missed_calls(self):
-               """
-               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
-               @blocks
-               """
-               return self._parse_recent(self._get_page(self._XML_MISSED_URL))
-
-       def get_placed_calls(self):
-               """
-               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
-               @blocks
-               """
-               return self._parse_recent(self._get_page(self._XML_PLACED_URL))
-
-       def get_csv_contacts(self):
-               data = {
-                       "groupToExport": "mine",
-                       "exportType": "ALL",
-                       "out": "OUTLOOK_CSV",
-               }
-               encodedData = urllib.urlencode(data)
-               contacts = self._get_page(self._CSV_CONTACTS_URL+"?"+encodedData)
-               return contacts
-
-       def get_voicemails(self):
-               """
-               @blocks
-               """
-               voicemailPage = self._get_page(self._XML_VOICEMAIL_URL)
-               voicemailHtml = self._grab_html(voicemailPage)
-               voicemailJson = self._grab_json(voicemailPage)
-               if voicemailJson is None:
-                       return ()
-               parsedVoicemail = self._parse_voicemail(voicemailHtml)
-               voicemails = self._merge_conversation_sources(parsedVoicemail, voicemailJson)
-               return voicemails
-
-       def get_texts(self):
-               """
-               @blocks
-               """
-               smsPage = self._get_page(self._XML_SMS_URL)
-               smsHtml = self._grab_html(smsPage)
-               smsJson = self._grab_json(smsPage)
-               if smsJson is None:
-                       return ()
-               parsedSms = self._parse_sms(smsHtml)
-               smss = self._merge_conversation_sources(parsedSms, smsJson)
-               return smss
-
-       def get_unread_counts(self):
-               countPage = self._get_page(self._JSON_SMS_COUNT_URL)
-               counts = parse_json(countPage)
-               counts = counts["unreadCounts"]
-               return counts
-
-       def mark_message(self, messageId, asRead):
-               """
-               @blocks
-               """
-               postData = {
-                       "read": 1 if asRead else 0,
-                       "id": messageId,
-               }
-
-               markPage = self._get_page(self._markAsReadURL, postData)
-
-       def archive_message(self, messageId):
-               """
-               @blocks
-               """
-               postData = {
-                       "id": messageId,
-               }
-
-               markPage = self._get_page(self._archiveMessageURL, postData)
-
-       def _grab_json(self, flatXml):
-               xmlTree = ElementTree.fromstring(flatXml)
-               jsonElement = xmlTree.getchildren()[0]
-               flatJson = jsonElement.text
-               jsonTree = parse_json(flatJson)
-               return jsonTree
-
-       def _grab_html(self, flatXml):
-               xmlTree = ElementTree.fromstring(flatXml)
-               htmlElement = xmlTree.getchildren()[1]
-               flatHtml = htmlElement.text
-               return flatHtml
-
-       def _grab_account_info(self, page):
-               accountData = parse_json(page)
-               self._token = accountData["r"]
-               self._accountNum = accountData["number"]["raw"]
-               for callback in accountData["phones"].itervalues():
-                       self._callbackNumbers[callback["phoneNumber"]] = callback["name"]
-               if len(self._callbackNumbers) == 0:
-                       _moduleLogger.debug("Could not extract callback numbers from GoogleVoice (the troublesome page follows):\n%s" % page)
-               return accountData
-
-       def _send_validation(self, number):
-               if not self.is_valid_syntax(number):
-                       raise ValueError('Number is not valid: "%s"' % number)
-               return number
-
-       def _parse_recent(self, recentPage):
-               allRecentHtml = self._grab_html(recentPage)
-               allRecentData = self._parse_history(allRecentHtml)
-               for recentCallData in allRecentData:
-                       yield recentCallData
-
-       def _parse_history(self, historyHtml):
-               splitVoicemail = self._seperateVoicemailsRegex.split(historyHtml)
-               for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
-                       exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
-                       exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
-                       exactTime = google_strptime(exactTime)
-                       relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
-                       relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
-                       locationGroup = self._voicemailLocationRegex.search(messageHtml)
-                       location = locationGroup.group(1).strip() if locationGroup else ""
-
-                       nameGroup = self._voicemailNameRegex.search(messageHtml)
-                       name = nameGroup.group(1).strip() if nameGroup else ""
-                       numberGroup = self._voicemailNumberRegex.search(messageHtml)
-                       number = numberGroup.group(1).strip() if numberGroup else ""
-                       prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
-                       prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
-                       contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
-                       contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
-
-                       yield {
-                               "id": messageId.strip(),
-                               "contactId": contactId,
-                               "name": unescape(name),
-                               "time": exactTime,
-                               "relTime": relativeTime,
-                               "prettyNumber": prettyNumber,
-                               "number": number,
-                               "location": unescape(location),
-                       }
-
-       @staticmethod
-       def _interpret_voicemail_regex(group):
-               quality, content, number = group.group(2), group.group(3), group.group(4)
-               text = MessageText()
-               if quality is not None and content is not None:
-                       text.accuracy = quality
-                       text.text = unescape(content)
-                       return text
-               elif number is not None:
-                       text.accuracy = MessageText.ACCURACY_HIGH
-                       text.text = number
-                       return text
-
-       def _parse_voicemail(self, voicemailHtml):
-               splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
-               for messageId, messageHtml in itergroup(splitVoicemail[1:], 2):
-                       conv = Conversation()
-                       conv.type = Conversation.TYPE_VOICEMAIL
-                       conv.id = messageId.strip()
-
-                       exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
-                       exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
-                       conv.time = google_strptime(exactTimeText)
-                       relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
-                       conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
-                       locationGroup = self._voicemailLocationRegex.search(messageHtml)
-                       conv.location = unescape(locationGroup.group(1).strip() if locationGroup else "")
-
-                       nameGroup = self._voicemailNameRegex.search(messageHtml)
-                       conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
-                       numberGroup = self._voicemailNumberRegex.search(messageHtml)
-                       conv.number = numberGroup.group(1).strip() if numberGroup else ""
-                       prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
-                       conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
-                       contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
-                       conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
-
-                       messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
-                       messageParts = [
-                               self._interpret_voicemail_regex(group)
-                               for group in messageGroups
-                       ] if messageGroups else ((MessageText.ACCURACY_LOW, "No Transcription"), )
-                       message = Message()
-                       message.body = messageParts
-                       message.whoFrom = conv.name
-                       try:
-                               message.when = conv.time.strftime("%I:%M %p")
-                       except ValueError:
-                               _moduleLogger.exception("Confusing time provided: %r" % conv.time)
-                               message.when = "Unknown"
-                       conv.messages = (message, )
-
-                       yield conv
-
-       @staticmethod
-       def _interpret_sms_message_parts(fromPart, textPart, timePart):
-               text = MessageText()
-               text.accuracy = MessageText.ACCURACY_MEDIUM
-               text.text = unescape(textPart)
-
-               message = Message()
-               message.body = (text, )
-               message.whoFrom = fromPart
-               message.when = timePart
-
-               return message
-
-       def _parse_sms(self, smsHtml):
-               splitSms = self._seperateVoicemailsRegex.split(smsHtml)
-               for messageId, messageHtml in itergroup(splitSms[1:], 2):
-                       conv = Conversation()
-                       conv.type = Conversation.TYPE_SMS
-                       conv.id = messageId.strip()
-
-                       exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
-                       exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
-                       conv.time = google_strptime(exactTimeText)
-                       relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
-                       conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
-                       conv.location = ""
-
-                       nameGroup = self._voicemailNameRegex.search(messageHtml)
-                       conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
-                       numberGroup = self._voicemailNumberRegex.search(messageHtml)
-                       conv.number = numberGroup.group(1).strip() if numberGroup else ""
-                       prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
-                       conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
-                       contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
-                       conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
-
-                       fromGroups = self._smsFromRegex.finditer(messageHtml)
-                       fromParts = (group.group(1).strip() for group in fromGroups)
-                       textGroups = self._smsTextRegex.finditer(messageHtml)
-                       textParts = (group.group(1).strip() for group in textGroups)
-                       timeGroups = self._smsTimeRegex.finditer(messageHtml)
-                       timeParts = (group.group(1).strip() for group in timeGroups)
-
-                       messageParts = itertools.izip(fromParts, textParts, timeParts)
-                       messages = [self._interpret_sms_message_parts(*parts) for parts in messageParts]
-                       conv.messages = messages
-
-                       yield conv
-
-       @staticmethod
-       def _merge_conversation_sources(parsedMessages, json):
-               for message in parsedMessages:
-                       jsonItem = json["messages"][message.id]
-                       message.isRead = jsonItem["isRead"]
-                       message.isSpam = jsonItem["isSpam"]
-                       message.isTrash = jsonItem["isTrash"]
-                       message.isArchived = "inbox" not in jsonItem["labels"]
-                       yield message
-
-       def _get_page(self, url, data = None, refererUrl = None):
-               headers = {}
-               if refererUrl is not None:
-                       headers["Referer"] = refererUrl
-
-               encodedData = urllib.urlencode(data) if data is not None else None
-
-               try:
-                       page = self._browser.download(url, encodedData, None, headers)
-               except urllib2.URLError, e:
-                       _moduleLogger.error("Translating error: %s" % str(e))
-                       raise NetworkError("%s is not accesible" % url)
-
-               return page
-
-       def _get_page_with_token(self, url, data = None, refererUrl = None):
-               if data is None:
-                       data = {}
-               data['_rnr_se'] = self._token
-
-               page = self._get_page(url, data, refererUrl)
-
-               return page
-
-       def _parse_with_validation(self, page):
-               json = parse_json(page)
-               self._validate_response(json)
-               return json
-
-       def _validate_response(self, response):
-               """
-               Validates that the JSON response is A-OK
-               """
-               try:
-                       assert response is not None, "Response not provided"
-                       assert 'ok' in response, "Response lacks status"
-                       assert response['ok'], "Response not good"
-               except AssertionError:
-                       try:
-                               if response["data"]["code"] == 20:
-                                       raise RuntimeError(
-"""Ambiguous error 20 returned by Google Voice.
-Please verify you have configured your callback number (currently "%s").  If it is configured some other suspected causes are: non-verified callback numbers, and Gizmo5 callback numbers.""" % self._callbackNumber)
-                       except KeyError:
-                               pass
-                       raise RuntimeError('There was a problem with GV: %s' % response)
-
-
-_UNESCAPE_ENTITIES = {
- "&quot;": '"',
- "&nbsp;": " ",
- "&#39;": "'",
-}
-
-
-def unescape(text):
-       plain = saxutils.unescape(text, _UNESCAPE_ENTITIES)
-       return plain
-
-
-def google_strptime(time):
-       """
-       Hack: Google always returns the time in the same locale.  Sadly if the
-       local system's locale is different, there isn't a way to perfectly handle
-       the time.  So instead we handle implement some time formatting
-       """
-       abbrevTime = time[:-3]
-       parsedTime = datetime.datetime.strptime(abbrevTime, "%m/%d/%y %I:%M")
-       if time.endswith("PM"):
-               parsedTime += datetime.timedelta(hours=12)
-       return parsedTime
-
-
-def itergroup(iterator, count, padValue = None):
-       """
-       Iterate in groups of 'count' values. If there
-       aren't enough values, the last result is padded with
-       None.
-
-       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
-       ...     print tuple(val)
-       (1, 2, 3)
-       (4, 5, 6)
-       >>> for val in itergroup([1, 2, 3, 4, 5, 6], 3):
-       ...     print list(val)
-       [1, 2, 3]
-       [4, 5, 6]
-       >>> for val in itergroup([1, 2, 3, 4, 5, 6, 7], 3):
-       ...     print tuple(val)
-       (1, 2, 3)
-       (4, 5, 6)
-       (7, None, None)
-       >>> for val in itergroup("123456", 3):
-       ...     print tuple(val)
-       ('1', '2', '3')
-       ('4', '5', '6')
-       >>> for val in itergroup("123456", 3):
-       ...     print repr("".join(val))
-       '123'
-       '456'
-       """
-       paddedIterator = itertools.chain(iterator, itertools.repeat(padValue, count-1))
-       nIterators = (paddedIterator, ) * count
-       return itertools.izip(*nIterators)
-
-
-def safe_eval(s):
-       _TRUE_REGEX = re.compile("true")
-       _FALSE_REGEX = re.compile("false")
-       _COMMENT_REGEX = re.compile("^\s+//.*$", re.M)
-       s = _TRUE_REGEX.sub("True", s)
-       s = _FALSE_REGEX.sub("False", s)
-       s = _COMMENT_REGEX.sub("#", s)
-       try:
-               results = eval(s, {}, {})
-       except SyntaxError:
-               _moduleLogger.exception("Oops")
-               results = None
-       return results
-
-
-def _fake_parse_json(flattened):
-       return safe_eval(flattened)
-
-
-def _actual_parse_json(flattened):
-       return simplejson.loads(flattened)
-
-
-if simplejson is None:
-       parse_json = _fake_parse_json
-else:
-       parse_json = _actual_parse_json
-
-
-def extract_payload(flatXml):
-       xmlTree = ElementTree.fromstring(flatXml)
-
-       jsonElement = xmlTree.getchildren()[0]
-       flatJson = jsonElement.text
-       jsonTree = parse_json(flatJson)
-
-       htmlElement = xmlTree.getchildren()[1]
-       flatHtml = htmlElement.text
-
-       return jsonTree, flatHtml
-
-
-def guess_phone_type(number):
-       if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
-               return GVoiceBackend.PHONE_TYPE_GIZMO
-       else:
-               return GVoiceBackend.PHONE_TYPE_MOBILE
-
-
-def get_sane_callback(backend):
-       """
-       Try to set a sane default callback number on these preferences
-       1) 1747 numbers ( Gizmo )
-       2) anything with gizmo in the name
-       3) anything with computer in the name
-       4) the first value
-       """
-       numbers = backend.get_callback_numbers()
-
-       priorityOrderedCriteria = [
-               ("\+1747", None),
-               ("1747", None),
-               ("747", None),
-               (None, "gizmo"),
-               (None, "computer"),
-               (None, "sip"),
-               (None, None),
-       ]
-
-       for numberCriteria, descriptionCriteria in priorityOrderedCriteria:
-               numberMatcher = None
-               descriptionMatcher = None
-               if numberCriteria is not None:
-                       numberMatcher = re.compile(numberCriteria)
-               elif descriptionCriteria is not None:
-                       descriptionMatcher = re.compile(descriptionCriteria, re.I)
-
-               for number, description in numbers.iteritems():
-                       if numberMatcher is not None and numberMatcher.match(number) is None:
-                               continue
-                       if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
-                               continue
-                       return number
-
-
-def set_sane_callback(backend):
-       """
-       Try to set a sane default callback number on these preferences
-       1) 1747 numbers ( Gizmo )
-       2) anything with gizmo in the name
-       3) anything with computer in the name
-       4) the first value
-       """
-       number = get_sane_callback(backend)
-       backend.set_callback_number(number)
-
-
-def _is_not_special(name):
-       return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
-
-
-def to_dict(obj):
-       members = inspect.getmembers(obj)
-       return dict((name, value) for (name, value) in members if _is_not_special(name))
-
-
-def grab_debug_info(username, password):
-       cookieFile = os.path.join(".", "raw_cookies.txt")
-       try:
-               os.remove(cookieFile)
-       except OSError:
-               pass
-
-       backend = GVoiceBackend(cookieFile)
-       browser = backend._browser
-
-       _TEST_WEBPAGES = [
-               ("token", backend._tokenURL),
-               ("login", backend._loginURL),
-               ("isdnd", backend._isDndURL),
-               ("account", backend._XML_ACCOUNT_URL),
-               ("html_contacts", backend._XML_CONTACTS_URL),
-               ("contacts", backend._JSON_CONTACTS_URL),
-               ("csv", backend._CSV_CONTACTS_URL),
-
-               ("voicemail", backend._XML_VOICEMAIL_URL),
-               ("html_sms", backend._XML_SMS_URL),
-               ("sms", backend._JSON_SMS_URL),
-               ("count", backend._JSON_SMS_COUNT_URL),
-
-               ("recent", backend._XML_RECENT_URL),
-               ("placed", backend._XML_PLACED_URL),
-               ("recieved", backend._XML_RECEIVED_URL),
-               ("missed", backend._XML_MISSED_URL),
-       ]
-
-       # Get Pages
-       print "Grabbing pre-login pages"
-       for name, url in _TEST_WEBPAGES:
-               try:
-                       page = browser.download(url)
-               except StandardError, e:
-                       print e.message
-                       continue
-               print "\tWriting to file"
-               with open("not_loggedin_%s.txt" % name, "w") as f:
-                       f.write(page)
-
-       # Login
-       print "Attempting login"
-       galxToken = backend._get_token()
-       loginSuccessOrFailurePage = backend._login(username, password, galxToken)
-       with open("loggingin.txt", "w") as f:
-               print "\tWriting to file"
-               f.write(loginSuccessOrFailurePage)
-       try:
-               backend._grab_account_info(loginSuccessOrFailurePage)
-       except Exception:
-               # Retry in case the redirect failed
-               # luckily refresh_account_info does everything we need for a retry
-               loggedIn = backend.refresh_account_info() is not None
-               if not loggedIn:
-                       raise
-
-       # Get Pages
-       print "Grabbing post-login pages"
-       for name, url in _TEST_WEBPAGES:
-               try:
-                       page = browser.download(url)
-               except StandardError, e:
-                       print str(e)
-                       continue
-               print "\tWriting to file"
-               with open("loggedin_%s.txt" % name, "w") as f:
-                       f.write(page)
-
-       # Cookies
-       browser.save_cookies()
-       print "\tWriting cookies to file"
-       with open("cookies.txt", "w") as f:
-               f.writelines(
-                       "%s: %s\n" % (c.name, c.value)
-                       for c in browser._cookies
-               )
-
-
-def grab_voicemails(username, password):
-       cookieFile = os.path.join(".", "raw_cookies.txt")
-       try:
-               os.remove(cookieFile)
-       except OSError:
-               pass
-
-       backend = GVoiceBackend(cookieFile)
-       backend.login(username, password)
-       voicemails = list(backend.get_voicemails())
-       for voicemail in voicemails:
-               print voicemail.id
-               backend.download(voicemail.id, ".")
-
-
-def main():
-       import sys
-       logging.basicConfig(level=logging.DEBUG)
-       args = sys.argv
-       if 3 <= len(args):
-               username = args[1]
-               password = args[2]
-
-       grab_debug_info(username, password)
-       grab_voicemails(username, password)
-
-
-if __name__ == "__main__":
-       main()