After some testing, fixing the csv support
[theonering] / src / gvoice / backend.py
index 034b7ab..69e46e4 100755 (executable)
@@ -35,8 +35,9 @@ import time
 import datetime
 import itertools
 import logging
-from xml.sax import saxutils
+import inspect
 
+from xml.sax import saxutils
 from xml.etree import ElementTree
 
 try:
@@ -48,13 +49,97 @@ except ImportError:
 import browser_emu
 
 
-_moduleLogger = logging.getLogger("gvoice.backend")
+_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
@@ -79,7 +164,7 @@ class GVoiceBackend(object):
 
                # Suprisingly, moving all of these from class to self sped up startup time
 
-               self._validateRe = re.compile("^[0-9]{10,}$")
+               self._validateRe = re.compile("^\+?[0-9]{10,}$")
 
                self._loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
 
@@ -96,11 +181,14 @@ class GVoiceBackend(object):
                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._XML_RECENT_URL = SECURE_URL_BASE + "inbox/recent/"
 
                self.XML_FEEDS = (
@@ -149,6 +237,7 @@ class GVoiceBackend(object):
                Attempts to detect a current session
                @note Once logged in try not to reauth more than once a minute.
                @returns If authenticated
+               @blocks
                """
                isRecentledAuthed = (time.time() - self._lastAuthed) < 120
                isPreviouslyAuthed = self._token is not None
@@ -196,6 +285,7 @@ class GVoiceBackend(object):
                """
                Attempt to login to GoogleVoice
                @returns Whether login was successful or not
+               @blocks
                """
                self.logout()
                galxToken = self._get_token()
@@ -216,6 +306,14 @@ class GVoiceBackend(object):
                self._lastAuthed = time.time()
                return True
 
+       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()
@@ -223,6 +321,9 @@ class GVoiceBackend(object):
                self._lastAuthed = 0.0
 
        def is_dnd(self):
+               """
+               @blocks
+               """
                isDndPage = self._get_page(self._isDndURL)
 
                dndGroup = self._isDndRe.search(isDndPage)
@@ -233,16 +334,19 @@ class GVoiceBackend(object):
                return isDnd
 
        def set_dnd(self, doNotDisturb):
+               """
+               @blocks
+               """
                dndPostData = {
                        "doNotDisturb": 1 if doNotDisturb else 0,
-                       "_rnr_se": self._token,
                }
 
-               dndPage = self._get_page(self._setDndURL, dndPostData)
+               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
@@ -268,6 +372,7 @@ class GVoiceBackend(object):
                """
                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,
@@ -279,13 +384,20 @@ class GVoiceBackend(object):
                )
                self._parse_with_validation(page)
 
-       def send_sms(self, phoneNumber, message):
-               phoneNumber = self._send_validation(phoneNumber)
+       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': phoneNumber,
-                               'text': message
+                               'phoneNumber': flattenedPhoneNumbers,
+                               'text': unicode(message).encode("utf-8"),
                        },
                )
                self._parse_with_validation(page)
@@ -294,6 +406,7 @@ class GVoiceBackend(object):
                """
                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,
@@ -303,6 +416,9 @@ class GVoiceBackend(object):
                return json
 
        def get_feed(self, feed):
+               """
+               @blocks
+               """
                actualFeed = "_XML_%s_URL" % feed.upper()
                feedUrl = getattr(self, actualFeed)
 
@@ -317,7 +433,8 @@ class GVoiceBackend(object):
                which can either be a ``Message`` instance, or a SHA1 identifier. 
                Saves files to ``adir`` (defaults to current directory). 
                Message hashes can be found in ``self.voicemail().messages`` for example. 
-               Returns location of saved file.
+               @returns location of saved file.
+               @blocks
                """
                page = self._get_page(self._downloadVoicemailURL, {"id": messageId})
                fn = os.path.join(adir, '%s.mp3' % messageId)
@@ -352,6 +469,7 @@ class GVoiceBackend(object):
                @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):
@@ -363,51 +481,82 @@ class GVoiceBackend(object):
        def get_recent(self):
                """
                @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+               @blocks
                """
-               for action, url in (
-                       ("Received", self._XML_RECEIVED_URL),
-                       ("Missed", self._XML_MISSED_URL),
-                       ("Placed", self._XML_PLACED_URL),
-               ):
-                       flatXml = self._get_page(url)
-
-                       allRecentHtml = self._grab_html(flatXml)
-                       allRecentData = self._parse_voicemail(allRecentHtml)
-                       for recentCallData in allRecentData:
-                               recentCallData["action"] = action
-                               yield recentCallData
+               recentPages = [
+                       (action, self._get_page(url))
+                       for action, url in (
+                               ("Received", self._XML_RECEIVED_URL),
+                               ("Missed", self._XML_MISSED_URL),
+                               ("Placed", self._XML_PLACED_URL),
+                       )
+               ]
+               return self._parse_recent(recentPages)
 
        def get_contacts(self):
                """
                @returns Iterable of (contact id, contact name)
+               @blocks
                """
                page = self._get_page(self._XML_CONTACTS_URL)
-               contactsBody = self._contactsBodyRe.search(page)
-               if contactsBody is None:
-                       raise RuntimeError("Could not extract contact information")
-               accountData = _fake_parse_json(contactsBody.group(1))
-               for contactId, contactDetails in accountData["contacts"].iteritems():
-                       # A zero contact id is the catch all for unknown contacts
-                       if contactId != "0":
-                               yield contactId, contactDetails
+               return self._process_contacts(page)
+
+       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_messages(self):
+       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_messages(parsedVoicemail, voicemailJson)
-               decoratedVoicemails = self._decorate_voicemail(voicemails)
+               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_messages(parsedSms, smsJson)
-               decoratedSms = self._decorate_sms(smss)
+               smss = self._merge_conversation_sources(parsedSms, smsJson)
+               return smss
+
+       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,
+               }
 
-               allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
-               return allMessages
+               markPage = self._get_page(self._archiveMessageURL, postData)
 
        def _grab_json(self, flatXml):
                xmlTree = ElementTree.fromstring(flatXml)
@@ -447,26 +596,32 @@ class GVoiceBackend(object):
                        raise ValueError('Number is not valid: "%s"' % number)
                elif not self.is_authed():
                        raise RuntimeError("Not Authenticated")
-
-               if len(number) == 11 and number[0] == 1:
-                       # Strip leading 1 from 11 digit dialing
-                       number = number[1:]
                return number
 
-       @staticmethod
-       def _interpret_voicemail_regex(group):
-               quality, content, number = group.group(2), group.group(3), group.group(4)
-               if quality is not None and content is not None:
-                       return quality, content
-               elif number is not None:
-                       return "high", number
+       def _parse_recent(self, recentPages):
+               for action, flatXml in recentPages:
+                       allRecentHtml = self._grab_html(flatXml)
+                       allRecentData = self._parse_history(allRecentHtml)
+                       for recentCallData in allRecentData:
+                               recentCallData["action"] = action
+                               yield recentCallData
 
-       def _parse_voicemail(self, voicemailHtml):
-               splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
+       def _process_contacts(self, page):
+               contactsBody = self._contactsBodyRe.search(page)
+               if contactsBody is None:
+                       raise RuntimeError("Could not extract contact information")
+               accountData = _fake_parse_json(contactsBody.group(1))
+               for contactId, contactDetails in accountData["contacts"].iteritems():
+                       # A zero contact id is the catch all for unknown contacts
+                       if contactId != "0":
+                               yield contactId, contactDetails
+
+       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 = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
+                       exactTime = google_strptime(exactTime)
                        relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
                        relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
                        locationGroup = self._voicemailLocationRegex.search(messageHtml)
@@ -481,60 +636,102 @@ class GVoiceBackend(object):
                        contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
                        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 ()
-
                        yield {
                                "id": messageId.strip(),
                                "contactId": contactId,
-                               "name": name,
+                               "name": unescape(name),
                                "time": exactTime,
                                "relTime": relativeTime,
                                "prettyNumber": prettyNumber,
                                "number": number,
-                               "location": location,
-                               "messageParts": messageParts,
-                               "type": "Voicemail",
+                               "location": unescape(location),
                        }
 
-       def _decorate_voicemail(self, parsedVoicemails):
-               messagePartFormat = {
-                       "med1": "<i>%s</i>",
-                       "med2": "%s",
-                       "high": "<b>%s</b>",
-               }
-               for voicemailData in parsedVoicemails:
-                       message = " ".join((
-                               messagePartFormat[quality] % part
-                               for (quality, part) in voicemailData["messageParts"]
-                       )).strip()
-                       if not message:
-                               message = "No Transcription"
-                       whoFrom = voicemailData["name"]
-                       when = voicemailData["time"]
-                       voicemailData["messageParts"] = ((whoFrom, message, when), )
-                       yield voicemailData
+       @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 = 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
+                       message.when = conv.time.strftime("%I:%M %p")
+                       conv.messages = (message, )
+
+                       yield conv
+
+       @staticmethod
+       def _interpret_sms_message_parts(fromPart, textPart, timePart):
+               text = MessageText()
+               text.accuracy = MessageText.ACCURACY_MEDIUM
+               text.text = 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)
-                       exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
-                       exactTime = datetime.datetime.strptime(exactTime, "%m/%d/%y %I:%M %p")
+                       exactTimeText = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+                       conv.time = google_strptime(exactTimeText)
                        relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
-                       relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
+                       conv.relTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup else ""
+                       conv.location = ""
 
                        nameGroup = self._voicemailNameRegex.search(messageHtml)
-                       name = nameGroup.group(1).strip() if nameGroup else ""
+                       conv.name = unescape(nameGroup.group(1).strip() if nameGroup else "")
                        numberGroup = self._voicemailNumberRegex.search(messageHtml)
-                       number = numberGroup.group(1).strip() if numberGroup else ""
+                       conv.number = numberGroup.group(1).strip() if numberGroup else ""
                        prettyNumberGroup = self._prettyVoicemailNumberRegex.search(messageHtml)
-                       prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
+                       conv.prettyNumber = prettyNumberGroup.group(1).strip() if prettyNumberGroup else ""
                        contactIdGroup = self._messagesContactIDRegex.search(messageHtml)
-                       contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
+                       conv.contactId = contactIdGroup.group(1).strip() if contactIdGroup else ""
 
                        fromGroups = self._smsFromRegex.finditer(messageHtml)
                        fromParts = (group.group(1).strip() for group in fromGroups)
@@ -544,32 +741,19 @@ class GVoiceBackend(object):
                        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 {
-                               "id": messageId.strip(),
-                               "contactId": contactId,
-                               "name": name,
-                               "time": exactTime,
-                               "relTime": relativeTime,
-                               "prettyNumber": prettyNumber,
-                               "number": number,
-                               "location": "",
-                               "messageParts": messageParts,
-                               "type": "Texts",
-                       }
-
-       def _decorate_sms(self, parsedTexts):
-               return parsedTexts
+                       yield conv
 
        @staticmethod
-       def _merge_messages(parsedMessages, json):
+       def _merge_conversation_sources(parsedMessages, json):
                for message in parsedMessages:
-                       id = message["id"]
-                       jsonItem = json["messages"][id]
-                       message["isRead"] = jsonItem["isRead"]
-                       message["isSpam"] = jsonItem["isSpam"]
-                       message["isTrash"] = jsonItem["isTrash"]
-                       message["isArchived"] = "inbox" not in jsonItem["labels"]
+                       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):
@@ -602,6 +786,31 @@ class GVoiceBackend(object):
                return json
 
 
+_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
@@ -638,9 +847,16 @@ def itergroup(iterator, count, padValue = None):
 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)
-       return eval(s, {}, {})
+       s = _COMMENT_REGEX.sub("#", s)
+       try:
+               results = eval(s, {}, {})
+       except SyntaxError:
+               _moduleLogger.exception("Oops")
+               results = None
+       return results
 
 
 def _fake_parse_json(flattened):
@@ -675,19 +891,21 @@ def validate_response(response):
        Validates that the JSON response is A-OK
        """
        try:
-               assert 'ok' in response and response['ok']
+               assert response is not None
+               assert 'ok' in response
+               assert response['ok']
        except AssertionError:
                raise RuntimeError('There was a problem with GV: %s' % response)
 
 
 def guess_phone_type(number):
-       if number.startswith("747") or number.startswith("1747"):
+       if number.startswith("747") or number.startswith("1747") or number.startswith("+1747"):
                return GVoiceBackend.PHONE_TYPE_GIZMO
        else:
                return GVoiceBackend.PHONE_TYPE_MOBILE
 
 
-def set_sane_callback(backend):
+def get_sane_callback(backend):
        """
        Try to set a sane default callback number on these preferences
        1) 1747 numbers ( Gizmo )
@@ -698,7 +916,9 @@ def set_sane_callback(backend):
        numbers = backend.get_callback_numbers()
 
        priorityOrderedCriteria = [
+               ("\+1747", None),
                ("1747", None),
+               ("747", None),
                (None, "gizmo"),
                (None, "computer"),
                (None, "sip"),
@@ -706,111 +926,40 @@ def set_sane_callback(backend):
        ]
 
        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 numberCriteria is not None and re.compile(numberCriteria).match(number) is None:
+                       if numberMatcher is not None and numberMatcher.match(number) is None:
                                continue
-                       if descriptionCriteria is not None and re.compile(descriptionCriteria).match(description) is None:
+                       if descriptionMatcher is not None and descriptionMatcher.match(description) is None:
                                continue
-                       backend.set_callback_number(number)
-                       return
-
-
-def sort_messages(allMessages):
-       sortableAllMessages = [
-               (message["time"], message)
-               for message in allMessages
-       ]
-       sortableAllMessages.sort(reverse=True)
-       return (
-               message
-               for (exactTime, message) in sortableAllMessages
-       )
+                       return number
 
 
-def decorate_recent(recentCallData):
+def set_sane_callback(backend):
        """
-       @returns (personsName, phoneNumber, date, action)
+       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
        """
-       contactId = recentCallData["contactId"]
-       if recentCallData["name"]:
-               header = recentCallData["name"]
-       elif recentCallData["prettyNumber"]:
-               header = recentCallData["prettyNumber"]
-       elif recentCallData["location"]:
-               header = recentCallData["location"]
-       else:
-               header = "Unknown"
+       number = get_sane_callback(backend)
+       backend.set_callback_number(number)
 
-       number = recentCallData["number"]
-       relTime = recentCallData["relTime"]
-       action = recentCallData["action"]
-       return contactId, header, number, relTime, action
 
+def _is_not_special(name):
+       return not name.startswith("_") and name[0].lower() == name[0] and "_" not in name
 
-def decorate_message(messageData):
-       contactId = messageData["contactId"]
-       exactTime = messageData["time"]
-       if messageData["name"]:
-               header = messageData["name"]
-       elif messageData["prettyNumber"]:
-               header = messageData["prettyNumber"]
-       else:
-               header = "Unknown"
-       number = messageData["number"]
-       relativeTime = messageData["relTime"]
-
-       messageParts = list(messageData["messageParts"])
-       if len(messageParts) == 0:
-               messages = ("No Transcription", )
-       elif len(messageParts) == 1:
-               messages = (messageParts[0][1], )
-       else:
-               messages = [
-                       "<b>%s</b>: %s" % (messagePart[0], messagePart[1])
-                       for messagePart in messageParts
-               ]
 
-       decoratedResults = contactId, header, number, relativeTime, messages
-       return decoratedResults
-
-
-def test_backend(username, password):
-       backend = GVoiceBackend()
-       print "Authenticated: ", backend.is_authed()
-       if not backend.is_authed():
-               print "Login?: ", backend.login(username, password)
-       print "Authenticated: ", backend.is_authed()
-       #print "Is Dnd: ", backend.is_dnd()
-       #print "Setting Dnd", backend.set_dnd(True)
-       #print "Is Dnd: ", backend.is_dnd()
-       #print "Setting Dnd", backend.set_dnd(False)
-       #print "Is Dnd: ", backend.is_dnd()
-
-       #print "Token: ", backend._token
-       #print "Account: ", backend.get_account_number()
-       #print "Callback: ", backend.get_callback_number()
-       #print "All Callback: ",
-       import pprint
-       #pprint.pprint(backend.get_callback_numbers())
-
-       #print "Recent: "
-       #for data in backend.get_recent():
-       #       pprint.pprint(data)
-       #for data in sort_messages(backend.get_recent()):
-       #       pprint.pprint(decorate_recent(data))
-       #pprint.pprint(list(backend.get_recent()))
-
-       print "Contacts: ",
-       for contact in backend.get_contacts():
-               pprint.pprint(contact)
-
-       #print "Messages: ",
-       #for message in backend.get_messages():
-       #       pprint.pprint(message)
-       #for message in sort_messages(backend.get_messages()):
-       #       pprint.pprint(decorate_message(message))
-
-       return backend
+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):
@@ -830,6 +979,7 @@ def grab_debug_info(username, password):
                ("isdnd", backend._isDndURL),
                ("account", backend._XML_ACCOUNT_URL),
                ("contacts", backend._XML_CONTACTS_URL),
+               ("csv", backend._CSV_CONTACTS_URL),
 
                ("voicemail", backend._XML_VOICEMAIL_URL),
                ("sms", backend._XML_SMS_URL),
@@ -890,10 +1040,16 @@ def grab_debug_info(username, password):
                )
 
 
-if __name__ == "__main__":
+def main():
        import sys
        logging.basicConfig(level=logging.DEBUG)
-       if True:
-               grab_debug_info(sys.argv[1], sys.argv[2])
-       else:
-               test_backend(sys.argv[1], sys.argv[2])
+       args = sys.argv
+       if 3 <= len(args):
+               username = args[1]
+               password = args[2]
+
+       grab_debug_info(username, password)
+
+
+if __name__ == "__main__":
+       main()