A SMS bug fix and further work on displaying SMS messages
[gc-dialer] / src / gv_backend.py
index a6efd77..44adba1 100644 (file)
@@ -1,23 +1,23 @@
 #!/usr/bin/python
 
-# DialCentral - Front end for Google's Grand Central 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
-
 """
+DialCentral - Front end for Google's Grand Central 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
@@ -31,22 +31,21 @@ import re
 import urllib
 import urllib2
 import time
+import datetime
+import itertools
 import warnings
 import traceback
+from xml.sax import saxutils
 
 from xml.etree import ElementTree
 
-from browser_emu import MozillaEmulator
-
-import socket
+import browser_emu
 
 try:
        import simplejson
 except ImportError:
        simplejson = None
 
-socket.setdefaulttimeout(5)
-
 
 _TRUE_REGEX = re.compile("true")
 _FALSE_REGEX = re.compile("false")
@@ -66,50 +65,57 @@ else:
                return simplejson.loads(flattened)
 
 
+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)
+
+
 class GVDialer(object):
        """
        This class encapsulates all of the knowledge necessary to interace with the grandcentral servers
        the functions include login, setting up a callback number, and initalting a callback
        """
 
-       _isNotLoginPageRe = re.compile(r"""I cannot access my account""")
-       _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
-       _accountNumRe = re.compile(r"""<b class="ms2">(.{14})</b></div>""")
-       _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
-       _validateRe = re.compile("^[0-9]{10,}$")
-       _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
-
-       _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
-       _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
-       _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
-
-       _clicktocallURL = "https://www.google.com/voice/m/sendcall"
-       _contactsURL = "https://www.google.com/voice/mobile/contacts"
-       _contactDetailURL = "https://www.google.com/voice/mobile/contact"
-
-       _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
-       _setforwardURL = "https://www.google.com//voice/m/setphone"
-       _accountNumberURL = "https://www.google.com/voice/mobile"
-       _forwardURL = "https://www.google.com/voice/mobile/phones"
-
-       _inboxURL = "https://www.google.com/voice/inbox/"
-       _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
-       _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
-       _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
-       _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
-
        def __init__(self, cookieFile = None):
                # Important items in this function are the setup of the browser emulation and cookie file
-               self._browser = MozillaEmulator(None, 0)
+               self._browser = browser_emu.MozillaEmulator(1)
                if cookieFile is None:
                        cookieFile = os.path.join(os.path.expanduser("~"), ".gv_cookies.txt")
                self._browser.cookies.filename = cookieFile
                if os.path.isfile(cookieFile):
                        self._browser.cookies.load()
 
+               self._token = ""
                self._accountNum = None
                self._lastAuthed = 0.0
-               self._token = ""
                self._callbackNumber = ""
                self._callbackNumbers = {}
 
@@ -126,19 +132,17 @@ class GVDialer(object):
                        return True
 
                try:
-                       inboxPage = self._browser.download(self._inboxURL)
-               except urllib2.URLError, e:
+                       self._grab_account_info()
+               except StandardError, e:
                        warnings.warn(traceback.format_exc())
-                       raise RuntimeError("%s is not accesible" % self._inboxURL)
-
-               self._browser.cookies.save()
-               if self._isNotLoginPageRe.search(inboxPage) is not None:
                        return False
 
-               self._grab_account_info()
+               self._browser.cookies.save()
                self._lastAuthed = time.time()
                return True
 
+       _loginURL = "https://www.google.com/accounts/ServiceLoginAuth"
+
        def login(self, username, password):
                """
                Attempt to login to grandcentral
@@ -151,6 +155,9 @@ class GVDialer(object):
                        'Email' : username,
                        'Passwd' : password,
                        'service': "grandcentral",
+                       "ltmpl": "mobile",
+                       "btmpl": "mobile",
+                       "PersistentCookie": "yes",
                })
 
                try:
@@ -168,19 +175,14 @@ class GVDialer(object):
 
                self.clear_caches()
 
+       _gvDialingStrRe = re.compile("This may take a few seconds", re.M)
+       _clicktocallURL = "https://www.google.com/voice/m/sendcall"
+
        def dial(self, number):
                """
                This is the main function responsible for initating the callback
                """
-               if not self.is_valid_syntax(number):
-                       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:]
-
+               number = self._send_validation(number)
                try:
                        clickToCallData = urllib.urlencode({
                                "number": number,
@@ -200,9 +202,33 @@ class GVDialer(object):
 
                return True
 
+       _sendSmsURL = "https://www.google.com/voice/m/sendsms"
+
+       def send_sms(self, number, message):
+               number = self._send_validation(number)
+               try:
+                       smsData = urllib.urlencode({
+                               "number": number,
+                               "smstext": message,
+                               "_rnr_se": self._token,
+                               "id": "undefined",
+                               "c": "undefined",
+                       })
+                       otherData = {
+                               'Referer' : 'https://google.com/voice/m/sms',
+                       }
+                       smsSuccessPage = self._browser.download(self._sendSmsURL, smsData, None, otherData)
+               except urllib2.URLError, e:
+                       warnings.warn(traceback.format_exc())
+                       raise RuntimeError("%s is not accesible" % self._sendSmsURL)
+
+               return True
+
        def clear_caches(self):
                self.__contacts = None
 
+       _validateRe = re.compile("^[0-9]{10,}$")
+
        def is_valid_syntax(self, number):
                """
                @returns If This number be called ( syntax validation only )
@@ -254,6 +280,8 @@ class GVDialer(object):
 
                return {}
 
+       _setforwardURL = "https://www.google.com//voice/m/setphone"
+
        def set_callback_number(self, callbacknumber):
                """
                Set the number that grandcental calls
@@ -270,6 +298,8 @@ class GVDialer(object):
                        warnings.warn(traceback.format_exc())
                        raise RuntimeError("%s is not accesible" % self._setforwardURL)
 
+               # @bug This does not seem to be keeping on my tablet (but works on the
+               # desktop), or the reading isn't working too well
                self._browser.cookies.save()
                return True
 
@@ -277,32 +307,23 @@ class GVDialer(object):
                """
                @returns Current callback number or None
                """
+               for c in self._browser.cookies:
+                       if c.name == "gv-ph":
+                               return c.value
                return self._callbackNumber
 
        def get_recent(self):
                """
+               @todo Sort this stuff
                @returns Iterable of (personsName, phoneNumber, date, action)
                """
-               for url in (
-                       self._receivedCallsURL,
-                       self._missedCallsURL,
-                       self._placedCallsURL,
-               ):
-                       try:
-                               allRecentData = self._grab_json(url)
-                       except urllib2.URLError, e:
-                               warnings.warn(traceback.format_exc())
-                               raise RuntimeError("%s is not accesible" % self._clicktocallURL)
-
-                       for recentCallData in allRecentData["messages"].itervalues():
-                               number = recentCallData["displayNumber"]
-                               date = recentCallData["relativeStartTime"]
-                               action = ", ".join((
-                                       label.title()
-                                       for label in recentCallData["labels"]
-                                               if label.lower() != "all" and label.lower() != "inbox"
-                               ))
-                               yield "", number, date, action
+               sortedRecent = [
+                       (exactDate, name, number, relativeDate, action)
+                       for (name, number, exactDate, relativeDate, action) in self._get_recent()
+               ]
+               sortedRecent.sort(reverse = True)
+               for exactDate, name, number, relativeDate, action in sortedRecent:
+                       yield name, number, relativeDate, action
 
        def get_addressbooks(self):
                """
@@ -321,6 +342,10 @@ class GVDialer(object):
        def factory_name():
                return "Google Voice"
 
+       _contactsRe = re.compile(r"""<a href="/voice/m/contact/(\d+)">(.*?)</a>""", re.S)
+       _contactsNextRe = re.compile(r""".*<a href="/voice/m/contacts(\?p=\d+)">Next.*?</a>""", re.S)
+       _contactsURL = "https://www.google.com/voice/mobile/contacts"
+
        def get_contacts(self):
                """
                @returns Iterable of (contact id, contact name)
@@ -334,10 +359,10 @@ class GVDialer(object):
                                        contactsPage = self._browser.download(contactsPageUrl)
                                except urllib2.URLError, e:
                                        warnings.warn(traceback.format_exc())
-                                       raise RuntimeError("%s is not accesible" % self._clicktocallURL)
+                                       raise RuntimeError("%s is not accesible" % contactsPageUrl)
                                for contact_match in self._contactsRe.finditer(contactsPage):
                                        contactId = contact_match.group(1)
-                                       contactName = contact_match.group(2)
+                                       contactName = saxutils.unescape(contact_match.group(2))
                                        contact = contactId, contactName
                                        self.__contacts.append(contact)
                                        yield contact
@@ -350,6 +375,9 @@ class GVDialer(object):
                        for contact in self.__contacts:
                                yield contact
 
+       _contactDetailPhoneRe = re.compile(r"""<div.*?>([0-9\-\(\) \t]+?)<span.*?>\((\w+)\)</span>""", re.S)
+       _contactDetailURL = "https://www.google.com/voice/mobile/contact"
+
        def get_contact_details(self, contactId):
                """
                @returns Iterable of (Phone Type, Phone Number)
@@ -358,40 +386,229 @@ class GVDialer(object):
                        detailPage = self._browser.download(self._contactDetailURL + '/' + contactId)
                except urllib2.URLError, e:
                        warnings.warn(traceback.format_exc())
-                       raise RuntimeError("%s is not accesible" % self._clicktocallURL)
+                       raise RuntimeError("%s is not accesible" % self._contactDetailURL)
 
                for detail_match in self._contactDetailPhoneRe.finditer(detailPage):
                        phoneNumber = detail_match.group(1)
-                       phoneType = detail_match.group(2)
+                       phoneType = saxutils.unescape(detail_match.group(2))
                        yield (phoneType, phoneNumber)
 
-       def _grab_json(self, url):
-               flatXml = self._browser.download(url)
+       _voicemailURL = "https://www.google.com/voice/inbox/recent/voicemail/"
+       _smsURL = "https://www.google.com/voice/inbox/recent/sms/"
+
+       def get_messages(self):
+               try:
+                       voicemailPage = self._browser.download(self._voicemailURL)
+               except urllib2.URLError, e:
+                       warnings.warn(traceback.format_exc())
+                       raise RuntimeError("%s is not accesible" % self._voicemailURL)
+               voicemailHtml = self._grab_html(voicemailPage)
+               parsedVoicemail = self._parse_voicemail(voicemailHtml)
+               decoratedVoicemails = self._decorate_voicemail(parsedVoicemail)
+
+               try:
+                       smsPage = self._browser.download(self._smsURL)
+               except urllib2.URLError, e:
+                       warnings.warn(traceback.format_exc())
+                       raise RuntimeError("%s is not accesible" % self._smsURL)
+               smsHtml = self._grab_html(smsPage)
+               parsedSms = self._parse_sms(smsHtml)
+               decoratedSms = self._decorate_sms(parsedSms)
+
+               allMessages = itertools.chain(decoratedVoicemails, decoratedSms)
+               sortedMessages = list(allMessages)
+               for exactDate, header, number, relativeDate, message in sortedMessages:
+                       yield header, number, relativeDate, message
+
+       def _grab_json(self, flatXml):
                xmlTree = ElementTree.fromstring(flatXml)
                jsonElement = xmlTree.getchildren()[0]
                flatJson = jsonElement.text
                jsonTree = parse_json(flatJson)
                return jsonTree
 
-       def _grab_account_info(self, loginPage = None):
-               if loginPage is None:
-                       accountNumberPage = self._browser.download(self._accountNumberURL)
-               else:
-                       accountNumberPage = loginPage
-               tokenGroup = self._tokenRe.search(accountNumberPage)
-               if tokenGroup is not None:
-                       self._token = tokenGroup.group(1)
-               anGroup = self._accountNumRe.search(accountNumberPage)
-               if anGroup is not None:
-                       self._accountNum = anGroup.group(1)
-
-               callbackPage = self._browser.download(self._forwardURL)
+       def _grab_html(self, flatXml):
+               xmlTree = ElementTree.fromstring(flatXml)
+               htmlElement = xmlTree.getchildren()[1]
+               flatHtml = htmlElement.text
+               return flatHtml
+
+       _tokenRe = re.compile(r"""<input.*?name="_rnr_se".*?value="(.*?)"\s*/>""")
+       _accountNumRe = re.compile(r"""<b class="ms\d">(.{14})</b></div>""")
+       _callbackRe = re.compile(r"""\s+(.*?):\s*(.*?)<br\s*/>\s*$""", re.M)
+       _forwardURL = "https://www.google.com/voice/mobile/phones"
+
+       def _grab_account_info(self):
+               page = self._browser.download(self._forwardURL)
+
+               tokenGroup = self._tokenRe.search(page)
+               if tokenGroup is None:
+                       raise RuntimeError("Could not extract authentication token from GoogleVoice")
+               self._token = tokenGroup.group(1)
+
+               anGroup = self._accountNumRe.search(page)
+               if anGroup is None:
+                       raise RuntimeError("Could not extract account number from GoogleVoice")
+               self._accountNum = anGroup.group(1)
+
                self._callbackNumbers = {}
-               for match in self._callbackRe.finditer(callbackPage):
-                       self._callbackNumbers[match.group(2)] = match.group(1)
+               for match in self._callbackRe.finditer(page):
+                       callbackNumber = match.group(2)
+                       callbackName = match.group(1)
+                       self._callbackNumbers[callbackNumber] = callbackName
+
+       def _send_validation(self, number):
+               if not self.is_valid_syntax(number):
+                       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
+
+       _recentCallsURL = "https://www.google.com/voice/inbox/recent/"
+       _placedCallsURL = "https://www.google.com/voice/inbox/recent/placed/"
+       _receivedCallsURL = "https://www.google.com/voice/inbox/recent/received/"
+       _missedCallsURL = "https://www.google.com/voice/inbox/recent/missed/"
+
+       def _get_recent(self):
+               """
+               @returns Iterable of (personsName, phoneNumber, exact date, relative date, action)
+               """
+               for url in (
+                       self._receivedCallsURL,
+                       self._missedCallsURL,
+                       self._placedCallsURL,
+               ):
+                       try:
+                               flatXml = self._browser.download(url)
+                       except urllib2.URLError, e:
+                               warnings.warn(traceback.format_exc())
+                               raise RuntimeError("%s is not accesible" % url)
+
+                       allRecentData = self._grab_json(flatXml)
+                       for recentCallData in allRecentData["messages"].itervalues():
+                               number = recentCallData["displayNumber"]
+                               exactDate = recentCallData["displayStartDateTime"]
+                               relativeDate = recentCallData["relativeStartTime"]
+                               action = ", ".join((
+                                       label.title()
+                                       for label in recentCallData["labels"]
+                                               if label.lower() != "all" and label.lower() != "inbox"
+                               ))
+                               number = saxutils.unescape(number)
+                               exactDate = saxutils.unescape(exactDate)
+                               exactDate = datetime.datetime.strptime(exactDate, "%m/%d/%y %I:%M %p")
+                               relativeDate = saxutils.unescape(relativeDate)
+                               action = saxutils.unescape(action)
+                               yield "", number, exactDate, relativeDate, action
+
+       _seperateVoicemailsRegex = re.compile(r"""^\s*<div id="(\w+)"\s* class="gc-message.*?">""", re.MULTILINE | re.DOTALL)
+       _exactVoicemailTimeRegex = re.compile(r"""<span class="gc-message-time">(.*?)</span>""", re.MULTILINE)
+       _relativeVoicemailTimeRegex = re.compile(r"""<span class="gc-message-relative">(.*?)</span>""", re.MULTILINE)
+       _voicemailNumberRegex = re.compile(r"""<input type="hidden" class="gc-text gc-quickcall-ac" value="(.*?)"/>""", re.MULTILINE)
+       _prettyVoicemailNumberRegex = re.compile(r"""<span class="gc-message-type">(.*?)</span>""", re.MULTILINE)
+       _voicemailLocationRegex = re.compile(r"""<span class="gc-message-location">(.*?)</span>""", re.MULTILINE)
+       _voicemailMessageRegex = re.compile(r"""<span class="gc-word-(.*?)">(.*?)</span>""", re.MULTILINE)
+
+       def _parse_voicemail(self, voicemailHtml):
+               splitVoicemail = self._seperateVoicemailsRegex.split(voicemailHtml)
+               for id, messageHtml in itergroup(splitVoicemail[1:], 2):
+                       exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
+                       exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+                       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 ""
+
+                       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 ""
+
+                       messageGroups = self._voicemailMessageRegex.finditer(messageHtml)
+                       messageParts = (
+                               (group.group(1).strip(), group.group(2).strip())
+                               for group in messageGroups
+                       ) if messageGroups else ()
+
+                       yield {
+                               "id": id.strip(),
+                               "time": exactTime,
+                               "relTime": relativeTime,
+                               "prettyNumber": prettyNumber,
+                               "number": number,
+                               "location": location,
+                               "messageParts": messageParts,
+                       }
+
+       def _decorate_voicemail(self, parsedVoicemail):
+               messagePartFormat = {
+                       "med1": "<i>%s</i>",
+                       "med2": "%s",
+                       "high": "<b>%s</b>",
+               }
+               for voicemailData in parsedVoicemail:
+                       exactTime = voicemailData["time"] # @todo Parse This
+                       header = "%s %s" % (voicemailData["prettyNumber"], voicemailData["location"])
+                       message = " ".join((
+                               messagePartFormat[quality] % part
+                               for (quality, part) in voicemailData["messageParts"]
+                       )).strip()
+                       if not message:
+                               message = "No Transcription"
+                       yield exactTime, header, voicemailData["number"], voicemailData["relTime"], message
+
+       _smsFromRegex = re.compile(r"""<span class="gc-message-sms-from">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+       _smsTextRegex = re.compile(r"""<span class="gc-message-sms-time">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+       _smsTimeRegex = re.compile(r"""<span class="gc-message-sms-text">(.*?)</span>""", re.MULTILINE | re.DOTALL)
+
+       def _parse_sms(self, smsHtml):
+               splitSms = self._seperateVoicemailsRegex.split(smsHtml)
+               for id, messageHtml in itergroup(splitSms[1:], 2):
+                       exactTimeGroup = self._exactVoicemailTimeRegex.search(messageHtml)
+                       exactTime = exactTimeGroup.group(1).strip() if exactTimeGroup else ""
+                       relativeTimeGroup = self._relativeVoicemailTimeRegex.search(messageHtml)
+                       relativeTime = relativeTimeGroup.group(1).strip() if relativeTimeGroup 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 ""
+
+                       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)
+
+                       yield {
+                               "id": id.strip(),
+                               "time": exactTime,
+                               "relTime": relativeTime,
+                               "prettyNumber": prettyNumber,
+                               "number": number,
+                               "messageParts": messageParts,
+                       }
 
-               if len(self._callbackNumber) == 0:
-                       self.set_sane_callback()
+       def _decorate_sms(self, parsedSms):
+               for messageData in parsedSms:
+                       exactTime = messageData["time"] # @todo Parse This
+                       header = "%s" % (messageData["prettyNumber"])
+                       number = messageData["number"]
+                       relativeTime = messageData["relTime"]
+                       message = "\n".join((
+                               "<b>%s (%s)</b>: %s" % messagePart
+                               for messagePart in messageData["messageParts"]
+                       ))
+                       if not message:
+                               message = "No Transcription"
+                       yield exactTime, header, number, relativeTime, message
 
 
 def test_backend(username, password):