[conduit/iphone: 4/4] Import iPhoneModule



commit fe93c8ae8a7b4b63207108bb3c6fa7c729e2cf78
Author: Martin Szulecki <info sukimashita com>
Date:   Thu Sep 23 02:15:32 2010 +1200

    Import iPhoneModule

 conduit/modules/iPodModule/iPodModule.py           |   11 +-
 conduit/modules/iPodModule/idevice/XBELOutput.py   |   74 ++++++
 .../modules/iPodModule/idevice/XMLNotesOutput.py   |   43 ++++
 conduit/modules/iPodModule/idevice/__init__.py     |  249 ++++++++++++++++++++
 .../modules/iPodModule/idevice/iCalendarOutput.py  |  108 +++++++++
 .../iPodModule/idevice/iPhoneDataStorage.py        |  210 +++++++++++++++++
 .../iPodModule/idevice/iPhoneRecordEntities.py     |   96 ++++++++
 .../modules/iPodModule/idevice/iPhoneSyncAgent.py  |  220 +++++++++++++++++
 conduit/modules/iPodModule/idevice/iphonesync.py   |  154 ++++++++++++
 conduit/modules/iPodModule/idevice/vCardOutput.py  |   93 ++++++++
 10 files changed, 1256 insertions(+), 2 deletions(-)
---
diff --git a/conduit/modules/iPodModule/iPodModule.py b/conduit/modules/iPodModule/iPodModule.py
index 5348b62..8212967 100644
--- a/conduit/modules/iPodModule/iPodModule.py
+++ b/conduit/modules/iPodModule/iPodModule.py
@@ -52,9 +52,14 @@ except ImportError:
 except locale.Error:
     errormsg = "iPod support disabled (Incorrect locale)"
 
+Utils.dataprovider_add_dir_to_path(__file__)
+from idevice import iPhoneContactsTwoWay, iPhoneCalendarsTwoWay
+
 PROPS_KEY_MOUNT = "CONDUIT_MOUNTPOINT"
 PROPS_KEY_NAME  = "CONDUIT_NAME"
 PROPS_KEY_ICON  = "CONDUIT_ICON"
+PROPS_KEY_UUID  = "CONDUIT_UUID"
+PROPS_KEY_TYPE  = "CONDUIT_TYPE"
 
 if errormsg:
     MODULES = {}
@@ -113,6 +118,8 @@ class iPhoneFactory(HalFactory.HalFactory):
                         props[PROPS_KEY_MOUNT] = root.get_path()
                         props[PROPS_KEY_NAME]  = m.get_name()
                         props[PROPS_KEY_ICON]  = "phone"
+                        props[PROPS_KEY_UUID]  = props["ID_SERIAL_SHORT"]
+                        props[PROPS_KEY_TYPE]  = props["ID_MODEL"]
                         return True
                 log.warning("iPhone not mounted by gvfs")
             else:
@@ -128,10 +135,10 @@ class iPhoneFactory(HalFactory.HalFactory):
     
     def get_dataproviders(self, key, **props):
         """ Return a list of dataproviders for this class of device """
-        return [IPodDummy, IPodPhotoSink]
+        return [IPodDummy, IPodPhotoSink, iPhoneCalendarsTwoWay]
 
     def get_args(self, key, **props):
-        return (props[PROPS_KEY_MOUNT], key)
+        return (props[PROPS_KEY_UUID], props[PROPS_KEY_TYPE])
 
 class iPodFactory(MediaPlayerFactory.MediaPlayerFactory):
 
diff --git a/conduit/modules/iPodModule/idevice/XBELOutput.py b/conduit/modules/iPodModule/idevice/XBELOutput.py
new file mode 100644
index 0000000..8a6ac62
--- /dev/null
+++ b/conduit/modules/iPodModule/idevice/XBELOutput.py
@@ -0,0 +1,74 @@
+#! /usr/bin/env python
+
+from plist import *
+from imobiledevice import *
+
+from iPhoneRecordEntities import *
+
+import sys, re, time, datetime, base64
+
+class XBELOutput():
+    XBEL_DOC = """<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE xbel PUBLIC "+//IDN python.org//DTD XML Bookmark Exchange Language 1.0//EN//XML" "http://pyxml.sourceforge.net/topics/dtds/xbel-1.0.dtd";>
+<xbel version="1.0">\n%s\n</xbel>"""
+
+    INDENT_CHAR = "\t"
+
+    def __init__(self, data_class):
+        self.data_class = data_class
+
+    def serialize(self):
+        result = ""
+
+        # get all bookmarks and folders
+        bookmark_storage = self.data_class.get_storage_for_entity(iPhoneBookmarkRecordEntity)
+        folder_storage = self.data_class.get_storage_for_entity(iPhoneFolderRecordEntity)
+        self.storage = {}
+        self.storage.update(folder_storage)
+        self.storage.update(bookmark_storage)
+        self.l = len(self.storage)
+
+        result = self._to_bookmarks()
+        return self.XBEL_DOC % (result)
+
+    def _to_bookmarks(self, parent_record=None, indent=""):
+        result = ""
+        skip = 0
+        for k, i in self.storage.iteritems():
+            if i.has_parents():
+                if not parent_record:
+                    continue
+                elif not i.is_parent(parent_record):
+                    continue
+            elif parent_record:
+                continue
+
+            if skip > 0 and skip < self.l:
+                result += "\n"
+            result += self._to_bookmark(i, indent)
+            skip += 1
+        return result
+
+    def _record_has_children(self, record):
+        for k, i in self.storage.iteritems():
+            if i.is_parent(record):
+                return True
+        return False
+
+    def _to_bookmark(self, record, indent=""):
+        result = ""
+        bookmark_type = record.name.rsplit(".")[-1].lower()
+        result += "%s<%s id=\"%s\"" % (indent, bookmark_type, record.id)
+        if isinstance(record, iPhoneBookmarkRecordEntity):
+            result += " href=\"%s\"" % (record.data.url)
+        result += ">\n"
+        result += "%s<title>%s</title>\n" % (indent, record.data.name)
+
+        # recurse for parents
+        if self._record_has_children(record):
+            result += self._to_bookmarks(record, indent + self.INDENT_CHAR)
+            result += "\n"
+
+        result += "%s</%s>" % (indent, bookmark_type)
+        return result
+
diff --git a/conduit/modules/iPodModule/idevice/XMLNotesOutput.py b/conduit/modules/iPodModule/idevice/XMLNotesOutput.py
new file mode 100644
index 0000000..1968b40
--- /dev/null
+++ b/conduit/modules/iPodModule/idevice/XMLNotesOutput.py
@@ -0,0 +1,43 @@
+#! /usr/bin/env python
+
+from plist import *
+from imobiledevice import *
+
+from iPhoneRecordEntities import *
+
+import sys, re, time, datetime, base64
+
+ODD_EPOCH = 978307200 # For some reason Apple decided the epoch was on a different day?
+
+class XMLNotesOutput():
+    XML_DOC = """<?xml version="1.0" encoding="UTF-8"?>
+<notes>\n%s\n</notes>"""
+
+    def __init__(self, data_class):
+        self.data_class = data_class
+
+    def serialize(self):
+        storage = self.data_class.get_storage_for_entity(iPhoneNoteRecordEntity)
+        l = len(storage)
+
+        result = ''
+        skip = 0
+        for k, i in storage.iteritems():
+            if skip > 0 and skip < l:
+                result += "\n"
+            result += self._to_html_note(i)
+            skip += 1
+        return self.XML_DOC % (result)
+
+    def _format_date(self, date):
+        return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(time.mktime(time.strptime(date, "%Y-%m-%d %H:%M:%S"))+ODD_EPOCH))
+
+    def _to_html_note(self, record):
+        result = "\t<note id=\"%s\" created=\"%s\" modified=\"%s\">\n" % (record.id, self._format_date(record.data.dateCreated), self._format_date(record.data.dateModified))
+        result += "\t\t<author>%s</author>\n" % (record.data.author)
+        result += "\t\t<subject>%s</subject>\n" % (record.data.subject)
+        result += "\t\t<content type=\"%s\"><![CDATA[%s]]></content>\n" % (record.data.contentType, record.data.content)
+        result += "\t</note>"
+
+        return result
+
diff --git a/conduit/modules/iPodModule/idevice/__init__.py b/conduit/modules/iPodModule/idevice/__init__.py
new file mode 100644
index 0000000..f4d8a8b
--- /dev/null
+++ b/conduit/modules/iPodModule/idevice/__init__.py
@@ -0,0 +1,249 @@
+# -*- coding: utf-8 -*-
+import logging
+log = logging.getLogger("modules.iPhone")
+
+import conduit
+import conduit.dataproviders.DataProvider as DataProvider
+import conduit.datatypes.DataType as DataType
+import conduit.utils as Utils
+import conduit.Exceptions as Exceptions
+
+import conduit.datatypes.Contact as Contact
+import conduit.datatypes.Event as Event
+import conduit.datatypes.Event as Bookmark
+
+from iPhoneDataStorage import *
+from iPhoneSyncAgent import *
+from vCardOutput import *
+from iCalendarOutput import *
+
+class iPhoneBaseTwoWay(DataProvider.TwoWay):
+    def __init__(self, *args):
+        self.agent = iPhoneSyncAgent()
+        self.uuid = str(args[0])
+        self.model = str(args[1])
+        if self.agent.connect(self.uuid):
+            log.info("Connected to %s with uuid %s" % (self.model, self.uuid))
+        else:
+            log.info("Failed to connect to iPhone/iPod Touch")
+
+    def uninitialize(self):
+        self.agent.disconnect()
+
+    def _replace_data(self, LUID, data):
+        #FIXME implement replace data
+        return LUID
+
+    def _put_data(self, data):
+        #FIXME implement put data
+        i = 0
+        return "%s iphone-%s" % (i, self._phone_uuid)
+
+    def _get_data(self, LUID):
+        storage = self.data_class.get_storage_for_entity(self.storage_entity)
+        return storage[LUID.split("@iphone")[0]]
+
+    def _data_exists(self, LUID):
+        if (None != _get_data(LUID)):
+            return true
+        return false
+
+    def refresh(self):
+        DataProvider.TwoWay.refresh(self)
+        self._refresh()
+
+    def _refresh(self):
+        self.agent.set_data_storage(self.data_class);
+        if self.agent.start_session():
+            self._phone_uuid = self.agent._phone.get_uuid()
+            self.agent.synchronize()
+        self.agent.finish_session()
+
+    def get_all(self):
+        res = []
+        storage = self.data_class.get_storage_for_entity(self.storage_entity)
+        for k, i in storage.iteritems():
+            res.append("%s iphone-%s" % (i.id, self._phone_uuid))
+        return res
+
+    def put(self, data, overwrite, LUID=None):
+        DataProvider.TwoWay.put(self, data, overwrite, LUID)
+        if overwrite and LUID:
+            LUID = self._replace_data(LUID, data)
+        else:
+            if LUID and self._data_exists(LUID):
+                oldData = self._get_data(LUID)
+                comp = data.compare(oldData)
+                #Possibility 1: If LUID != None (i.e this is a modification/update of a
+                #previous sync, and we are newer, the go ahead an put the data
+                if LUID != None and comp == conduit.datatypes.COMPARISON_NEWER:
+                    LUID = self._replace_data(LUID, data)
+                #Possibility 3: We are the same, so return either rid
+                elif comp == conduit.datatypes.COMPARISON_EQUAL:
+                    return oldData.get_rid()
+                #Possibility 2, 4: All that remains are conflicts
+                else:
+                    raise Exceptions.SynchronizeConflictError(comp, data, oldData)
+            else:
+                #Possibility 5:
+                LUID = self._put_data(data)
+
+        #now return the rid
+        if not LUID:
+            raise Exceptions.SyncronizeError("Error putting/updating data")
+        else:
+            return self._get_data(LUID).get_rid()
+
+
+
+class iPhoneContactsTwoWay(iPhoneBaseTwoWay):
+    """
+    Contact syncing for iPhone and iPod Touch
+    """
+
+    _name_ = "iPhone Contacts"
+    _description_ = "iPhone and iPod Touch Contact Dataprovider"
+    _category_ = conduit.dataproviders.CATEGORY_MISC
+    _module_type_ = "twoway"
+    _in_type_ = "contact"
+    _out_type_ = "contact"
+    _icon_ = "contact-new"
+
+    def __init__(self, *args):
+        iPhoneBaseTwoWay.__init__(self, *args)
+        DataProvider.TwoWay.__init__(self)
+        self.selectedGroup = None
+        self.data_class = iPhoneContactsDataStorage()
+        self.storage_entity = iPhoneContactRecordEntity
+
+    def get(self, LUID):
+        DataProvider.TwoWay.get(self, LUID)
+        c = None
+        i = self._get_data(LUID)
+        if (None != i):
+            vcard = vCardOutput(self.data_class)
+            c = Contact.Contact()
+            c.set_from_vcard_string(vcard._to_vcard(i))
+        return c
+
+    def get_UID(self):
+        return "iPhoneContactsTwoWay"
+
+    def get(self, LUID):
+        # FIXME: This should be rewritten to translate like iPhoneCalendarTwoWay
+        # After doing that we can get rid of this method.
+        DataProvider.TwoWay.get(self, LUID)
+        c = None
+        i = self._get_data(LUID)
+        if (None != i):
+            vcard = vCardOutput(self.data_class)
+            c = Contact.Contact()
+            c.set_from_vcard_string(vcard._to_vcard(i))
+        return c
+
+class iPhoneConduitEvent(DataType.DataType):
+    _name_ = 'event/iphone'
+
+    def __init__(self, record):
+        super(iPhoneConduitEvent, self).__init__()
+        self.record = record
+    
+    def get_hash(self):
+        return hash("".join(self.record.data.values()))
+
+    def get_ical_string(self):
+        formatter = iCalendarOutput(self.record.data_storage)
+        return formatter.to_vevent(self.record)
+
+    def set_from_ical_string(self, string):
+        pass
+
+class iPhoneCalendarsTwoWay(iPhoneBaseTwoWay):
+    """
+    Contact syncing for iPhone and iPod Touch
+    """
+
+    _name_ = "iPhone Calendar"
+    _description_ = "iPhone and iPod Touch Calendar Dataprovider"
+    _category_ = conduit.dataproviders.CATEGORY_MISC
+    _module_type_ = "twoway"
+    _in_type_ = "event/iphone"
+    _out_type_ = "event/iphone"
+    _icon_ = "appointment-new"
+    _configurable_ = True
+
+    def __init__(self, *args):
+        iPhoneBaseTwoWay.__init__(self, *args)
+        DataProvider.TwoWay.__init__(self)
+        self.data_class = iPhoneCalendarsDataStorage()
+        self.storage_entity = iPhoneEventRecordEntity
+
+        self.update_configuration(
+            _calendarId = ""
+        )
+
+    def get_UID(self):
+        # FIXME: This should include the UUID of the phone as well
+        return "iPhoneCalendarsTwoWay-%s" % (self._calendarId)
+
+    # Implement this for faster syncs
+    #def get_changes(self):
+    #    return (new, changed, deleted)
+
+    def get_all(self):
+        """
+        Returns a list of all event ids for the configured calendar.
+        """
+        res = []
+        
+        events = self.data_class.get_children_of_record_by_type(self.calendar, self.storage_entity)
+        for i in events:
+            res.append("%s iphone-%s" % (i.id, self._phone_uuid))
+        return res
+    
+    def get(self, LUID):
+        """
+        Returns an iPhoneConduitEvent for a given LUID.
+        """
+        DataProvider.TwoWay.get(self, LUID)
+        record = self._get_data(LUID)
+        e = None
+        if record is not None:
+            e = iPhoneConduitEvent(record)
+        return e
+
+    def _calendar_names(self):
+        """
+        Returns a dictionary of calendar ids to their names.
+        """
+        if not hasattr(self, '_calendarNames'):
+            self._calendarNames = []
+            
+            # Collect the calendars from the phone
+            self._refresh()
+            calendars = self.data_class.get_storage_for_entity(iPhoneCalendarRecordEntity).values()
+            for calendar in calendars:
+                self._calendarNames.append((calendar.id, calendar.data.title))
+        
+        return self._calendarNames
+
+    @property
+    def calendar(self):
+        """
+        Returns the currently configured iPhoneCalendarRecordEntity
+        """
+        if not hasattr(self, '_calendarNames'):
+            self._refresh()
+
+        calendars = self.data_class.get_storage_for_entity(iPhoneCalendarRecordEntity).values()
+        matching_calendars = [c for c in calendars if c.id == self._calendarId]
+        
+        if matching_calendars:
+            return matching_calendars[0]
+        else:
+            return calendars[0]
+
+    def config_setup(self, config):
+        config.add_section("Calendar Name")
+        config.add_item("Calendar", "combo", config_name = "_calendarId", choices = self._calendar_names())
+
diff --git a/conduit/modules/iPodModule/idevice/iCalendarOutput.py b/conduit/modules/iPodModule/idevice/iCalendarOutput.py
new file mode 100644
index 0000000..09f7f7e
--- /dev/null
+++ b/conduit/modules/iPodModule/idevice/iCalendarOutput.py
@@ -0,0 +1,108 @@
+#! /usr/bin/env python
+
+from plist import *
+from imobiledevice import *
+
+from iPhoneRecordEntities import *
+
+import sys, re, time, datetime
+
+ODD_EPOCH = 978307200 # For some reason Apple decided the epoch was on a different day?
+
+class iCalendarOutput():
+    def __init__(self, data_class):
+        self.data_class = data_class
+
+    def serialize(self):
+        storage = self.data_class.get_storage_for_entity(iPhoneCalendarRecordEntity)
+        l = len(storage)
+
+        result = ""
+        skip = 0
+        for key, i in storage.iteritems():
+            if skip > 0 and skip < l:
+                result += "\n"
+            result += "BEGIN:VCALENDAR\n"
+            result += "VERSION:2.0\n"
+            result += "PRODID:-//iphonesync//NONSGML %s//EN\n" % (i.data.title)
+            result += "X-WR-CALNAME:%s\n" % (i.data.title)
+            events = self.data_class.get_children_of_record_by_type(i, iPhoneEventRecordEntity)
+            for event in events:
+                result += self.to_vevent(event) + "\n"
+            result += "END:VCALENDAR"
+            skip += 1
+        return result
+
+    def _format_date(self, date_format, date):
+        return time.strftime(date_format, time.localtime(time.mktime(time.strptime(date, "%Y-%m-%d %H:%M:%S"))+ODD_EPOCH))
+
+    def to_vevent(self, record):
+        result = "BEGIN:VEVENT\n"
+        result += "UID:%s iphone\n" % record.id
+
+        # Determine if it is an all day event
+        date_format = ":%Y%m%dT%H%M%S"
+
+        if record.has_value("all day"):
+            date_format = ";VALUE=DATE:%Y%m%d"
+
+        # Handle all of the event information
+        result += "DTSTART%s\n" % (self._format_date(date_format, record.data.start_date))
+        result += "DTEND%s\n" % (self._format_date(date_format, record.data.end_date))
+        summary = record.data.summary
+        if summary != "":
+            result += "SUMMARY:%s\n" % (summary)
+        if record.has_value("location"):
+            result += "LOCATION:%s\n" % (record.data.location)
+        if record.has_value("description"):
+            result += "DESCRIPTION:%s\n" % (record.data.description)
+
+        # Handle recurrences
+        rrule = ""
+        children = self.data_class.get_children_of_record_by_type(record, iPhoneRecurrenceRecordEntity)
+        for recurrence in children:
+            v = recurrence.data.frequency
+            if v != '':
+                rrule += "FREQ=%s;" % (v.upper())
+            v = recurrence.data.interval
+            if v != '':
+                rrule += "INTERVAL=%s;" % (v)
+            v = recurrence.data.bymonth
+            if v != '':
+                rrule += "BYMONTH=%s;" % (v)
+            v = recurrence.data.bymonthday
+            if v != '':
+                recurrence += "BYMONTHDAY=%s;" % (v)
+        if rrule != "":
+            result += "RRULE:%s\n" % (rrule[0:-1])
+
+        # Handle alarms
+        children = self.data_class.get_children_of_record_by_type(record, iPhoneDisplayAlarmRecordEntity)
+        for child in children:
+            result += self._to_valarm(child, summary)
+
+        children = self.data_class.get_children_of_record_by_type(record, iPhoneAudioAlarmRecordEntity)
+        for child in children:
+            result += self._to_valarm(child, summary)
+
+        result += "END:VEVENT"
+
+        return result
+
+    def _to_valarm(self, record, summary=""):
+        result = "BEGIN:VALARM\n"
+        if isinstance(record, iPhoneDisplayAlarmRecordEntity):
+            result += "ACTION:DISPLAY\n"
+        elif isinstance(record, iPhoneAudioAlarmRecordEntity):
+            result += "ACTION:AUDIO\n"
+        if summary != "":
+            result += "DESCRIPTION:%s\n" % (summary)
+        if record.has_value("triggerduration"):
+            # Convert from the silly unsigned int represent a signed int
+            time_before = int(record.data.triggerduration)
+            minutes_before = ((2**64) - time_before) / 60
+            result += "TRIGGER;VALUE=DURATION;RELATED=START:-P%sM\n" % (minutes_before)
+        result += "END:VALARM\n"
+
+        return result
+
diff --git a/conduit/modules/iPodModule/idevice/iPhoneDataStorage.py b/conduit/modules/iPodModule/idevice/iPhoneDataStorage.py
new file mode 100644
index 0000000..dfd6e29
--- /dev/null
+++ b/conduit/modules/iPodModule/idevice/iPhoneDataStorage.py
@@ -0,0 +1,210 @@
+#! /usr/bin/env python
+
+from plist import *
+from imobiledevice import *
+
+from iPhoneRecordEntities import *
+
+import sys, re, time, datetime, base64
+
+''' Data Classes '''
+
+RECORD_ENTITY_NAME_KEY = "com.apple.syncservices.RecordEntityName"
+
+RECORD_ENTITIES = {
+    'com.apple.contacts.Contact': iPhoneContactRecordEntity,
+    'com.apple.contacts.Group': iPhoneGroupRecordEntity,
+    'com.apple.contacts.Email Address': iPhoneEMailAddressRecordEntity,
+    'com.apple.contacts.Phone Number': iPhonePhoneNumberRecordEntity,
+    'com.apple.contacts.Street Address': iPhoneStreetAddressRecordEntity,
+    'com.apple.contacts.URL': iPhoneURLRecordEntity,
+    'com.apple.contacts.Related Name': iPhoneRelatedNameRecordEntity,
+    'com.apple.contacts.IM': iPhoneIMRecordEntity,
+    'com.apple.contacts.Date': iPhoneDateRecordEntity,
+    'com.apple.bookmarks.Bookmark': iPhoneBookmarkRecordEntity,
+    'com.apple.bookmarks.Folder': iPhoneFolderRecordEntity,
+    'com.apple.calendars.Calendar': iPhoneCalendarRecordEntity,
+    'com.apple.calendars.Event': iPhoneEventRecordEntity,
+    'com.apple.calendars.DisplayAlarm': iPhoneDisplayAlarmRecordEntity,
+    'com.apple.calendars.AudioAlarm': iPhoneAudioAlarmRecordEntity,
+    'com.apple.calendars.Recurrence': iPhoneRecurrenceRecordEntity,
+    'com.apple.calendars.Attendee': iPhoneAttendeeRecordEntity,
+    'com.apple.calendars.Organizer': iPhoneOrganizerRecordEntity,
+    'com.apple.mail.Account': iPhoneAccountRecordEntity,
+    'com.apple.notes.Note': iPhoneNoteRecordEntity
+}
+
+class iPhoneDataStorage():
+    EMPTY_PARAM = "___EmptyParameterString___"
+
+    name = None
+    host_anchor = None
+    device_anchor = None
+    supported_entities = []
+    storage = {}
+
+    def __init__(self):
+        # load supported entity types
+        if len(self.supported_entities) > 0:
+            index = 0
+            for entity in self.supported_entities:
+                self.storage[index] = {}
+                index += 1
+
+        self.device_anchor = self._get_default_anchor()
+
+    def get_name(self):
+        return self.name
+
+    def get_version(self):
+        return self.version
+
+    def get_children_of_record_by_type(self, record, entity_class):
+        """
+        Returns a list of all the children of type entity class for the given
+        record.
+        """
+        ids = self.get_storage_for_entity(entity_class)
+        parents = []
+        for k, i in ids.iteritems():
+            if record.id in i.parent_ids:
+                parents.append(i)
+        return parents
+
+    def get_storage_for_entity(self, entity_class):
+        """
+        Returns a dictionary containing all the elements of the type
+        entity_class. The keys are the record ids and the values are the
+        records themselves.
+        """
+        for index, i in enumerate(self.supported_entities):
+            if (i != None) and (entity_class == RECORD_ENTITIES[i]):
+                return self.storage[index]
+            index += 1
+        return {}
+
+    def commit_records(self, node):
+        # check message type
+        if node[0].get_value() != "SDMessageProcessChanges":
+            return False
+
+        # check data storage name
+        if node[1].get_value() != self.get_name():
+            return False
+
+        # make sure we got a dict node next
+        if (node[2].get_type() == PLIST_STRING) and (node[2].get_value() == self.EMPTY_PARAM):
+            # skip this record set since we got no change data
+            return True
+        elif node[2].get_type() != PLIST_DICT:
+            return False
+
+        records = node[2]
+        for record_id in records:
+            record_dict = records[record_id]
+
+            # get entity name
+            record_entity_name = record_dict[RECORD_ENTITY_NAME_KEY].get_value()
+
+            # get storage
+            storage = self.get_storage_for_entity(RECORD_ENTITIES[record_entity_name])
+
+            # update or create record
+            if record_id in storage:
+                record = storage[record_id]
+            else:
+                record = RECORD_ENTITIES[record_entity_name](data_storage=self)
+                record.id = record_id
+
+            # map data
+            for key_name in record_dict:
+                value_node = record_dict[key_name]
+                if key_name != RECORD_ENTITY_NAME_KEY:
+                    if key_name in ["contact", "parent", "calendar", "owner", "members"]:
+                        parents = value_node
+                        for i in range(len(parents)):
+                            record.parent_ids.append(parents[i].get_value())
+                    elif value_node.get_type() in [PLIST_DATA, PLIST_UINT, PLIST_STRING, PLIST_DATE, PLIST_BOOLEAN]:
+                        record.data[key_name] = str(value_node.get_value())
+
+            # store record
+            storage[record.id] = record
+
+        return True
+
+    def get_next_change_for_device(self):
+        return None
+
+    def remap_record_identifiers(self, plist):
+        return True
+
+    def _get_default_anchor(self, remote = True):
+        return "%s-%s-Anchor" % (self.name.rsplit(".", 1)[1], ("Device" if remote else "Computer"))
+
+    def get_device_anchor(self):
+        return self.device_anchor
+
+    def get_host_anchor(self):
+        if not self.host_anchor:
+            self.host_anchor = time.strftime("%Y-%m-%d %H:%M:%S %z")
+        return self.host_anchor
+
+    def set_device_anchor(self, anchor):
+        self.device_anchor = anchor
+
+    def set_host_anchor(self, anchor):
+        self.host_anchor = anchor
+
+class iPhoneContactsDataStorage(iPhoneDataStorage):
+    name = "com.apple.Contacts"
+    version = 106
+    supported_entities = [
+        'com.apple.contacts.Contact',
+        'com.apple.contacts.Group',
+        'com.apple.contacts.Email Address',
+        'com.apple.contacts.Phone Number',
+        'com.apple.contacts.Street Address',
+        'com.apple.contacts.URL',
+        'com.apple.contacts.Related Name',
+        'com.apple.contacts.IM',
+        'com.apple.contacts.Date'
+    ]
+
+class iPhoneBookmarksDataStorage(iPhoneDataStorage):
+    name = "com.apple.Bookmarks"
+    version = 102
+    supported_entities = [
+        'com.apple.bookmarks.Bookmark',
+        'com.apple.bookmarks.Folder'
+    ]
+
+class iPhoneCalendarsDataStorage(iPhoneDataStorage):
+    name = "com.apple.Calendars"
+    version = 107
+    supported_entities = [
+        'com.apple.calendars.Calendar',
+        'com.apple.calendars.Event',
+        'com.apple.calendars.AudioAlarm',
+        'com.apple.calendars.DisplayAlarm',
+        'com.apple.calendars.Recurrence',
+        'com.apple.calendars.Attendee',
+        'com.apple.calendars.Organizer'
+    ]
+
+class iPhoneMailAccountsDataStorage(iPhoneDataStorage):
+    name = "com.apple.MailAccounts"
+    version = 102
+    supported_entities = [
+        'com.apple.mail.Account'
+    ]
+
+    def get_host_anchor(self):
+        return self._get_default_anchor(False)
+
+class iPhoneNotesDataStorage(iPhoneDataStorage):
+    name = "com.apple.Notes"
+    version = 102
+    supported_entities = [
+        'com.apple.notes.Note'
+    ]
+
diff --git a/conduit/modules/iPodModule/idevice/iPhoneRecordEntities.py b/conduit/modules/iPodModule/idevice/iPhoneRecordEntities.py
new file mode 100644
index 0000000..0250a97
--- /dev/null
+++ b/conduit/modules/iPodModule/idevice/iPhoneRecordEntities.py
@@ -0,0 +1,96 @@
+#! /usr/bin/env python
+
+from plist import *
+from imobiledevice import *
+
+import sys, re, time, datetime
+
+''' Record Entity Classes '''
+
+class iPhoneRecordEntityData(dict):
+    def __getattr__(self, name):
+        nname = name.replace("_", " ")
+        if nname in self:
+            return self[nname].replace("\n", '\\n')
+        return ""
+
+class iPhoneRecordEntityBase():
+    def __init__(self, data_storage=None):
+        """
+        :param data_storage: The data storage model with which this record is
+            associated.
+        """
+        self.id = ''
+        self.data = iPhoneRecordEntityData()
+        self.parent_ids = []
+        self.data_storage = data_storage
+
+    def has_parents(self):
+        return (len(self.parent_ids) > 0)
+
+    def is_parent(self, parent):
+        return (parent.id in self.parent_ids)
+
+    def has_value(self, key):
+        return (key in self.data)
+
+class iPhoneCalendarRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.calendars.Calendar"
+
+class iPhoneEventRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.calendars.Event"
+
+class iPhoneDisplayAlarmRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.calendars.DisplayAlarm"
+
+class iPhoneAudioAlarmRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.calendars.AudioAlarm"
+
+class iPhoneRecurrenceRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.calendars.Recurrence"
+
+class iPhoneAttendeeRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.calendars.Attendee"
+
+class iPhoneOrganizerRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.calendars.Organizer"
+
+class iPhoneBookmarkRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.bookmarks.Bookmark"
+
+class iPhoneFolderRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.bookmarks.Folder"
+
+class iPhoneContactRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.contacts.Contact"
+
+class iPhoneGroupRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.contacts.Group"
+
+class iPhoneStreetAddressRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.contacts.Street Address"
+
+class iPhoneEMailAddressRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.contacts.EMail Address"
+
+class iPhonePhoneNumberRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.contacts.Phone Number"
+
+class iPhoneURLRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.contacts.URL"
+
+class iPhoneRelatedNameRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.contacts.Related Name"
+
+class iPhoneIMRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.contacts.IM"
+
+class iPhoneDateRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.contacts.Date"
+
+class iPhoneAccountRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.mail.Account"
+
+class iPhoneNoteRecordEntity(iPhoneRecordEntityBase):
+    name = "com.apple.mail.Note"
+
diff --git a/conduit/modules/iPodModule/idevice/iPhoneSyncAgent.py b/conduit/modules/iPodModule/idevice/iPhoneSyncAgent.py
new file mode 100644
index 0000000..af17211
--- /dev/null
+++ b/conduit/modules/iPodModule/idevice/iPhoneSyncAgent.py
@@ -0,0 +1,220 @@
+#! /usr/bin/env python
+
+from plist import *
+from imobiledevice import *
+
+import sys, re, time, datetime
+
+''' Synchronization Classes '''
+
+class iPhoneSyncAgent():
+    EMPTY_PARAM = "___EmptyParameterString___"
+    SYNC_TYPES = [
+        "SDSyncTypeFast",
+        "SDSyncTypeSlow",
+        "SDSyncTypeReset"
+    ]
+    _phone = None
+    _lckd = None
+    _mobile_sync = None
+
+    def __init__(self):
+        pass
+
+    def connect(self, uuid=None):
+        self._phone = idevice()
+        if uuid:
+            if not self._phone.init_device_by_uuid(uuid):
+                print "Couldn't connect to iPhone/iPod touch with uuid %s." % (uuid)
+                return False
+        else:
+            if not self._phone.init_device():
+                print "Couldn't connect to iPhone/iPod touch."
+                return False
+
+        self._lckd = self._phone.get_lockdown_client()
+        if not self._lckd:
+            print "Lockdown session couldn't be established."
+            return False
+
+        self._mobile_sync = self._lckd.get_mobilesync_client()
+        if not self._mobile_sync:
+            print "Mobilesync session couldn't be established."
+            return False
+
+        # Lockdown is not needed anymore and has to be closed or it times out
+        del self._lckd
+
+        return True
+
+    def get_uuid(self):
+        self._phone.get_uuid()
+
+    def set_debug_level(self, mask):
+        self._phone.set_debug_level(1)
+
+    def synchronize(self, sync_type):
+        # we request just the changes only on fast sync
+        if sync_type == self.SYNC_TYPES[0]:
+            self.get_changes_from_device()
+        else:
+            self.get_all_records_from_device()
+
+        # from device
+        if not self.receive_changes_from_device():
+            return False
+
+        # switch sync direction
+        self.ping()
+
+        # to device
+        # FIXME: Functionality to send records to device
+        #if not self.send_changes_to_device():
+        #    return False
+
+        return True
+
+    def receive_changes_from_device(self):
+        msg = self._get_next_message()
+        while not self._is_device_ready_to_receive_changes(msg):
+            if not self.sync_data_storage.commit_records(msg):
+                self.cancel_session("Could not commit changes from device.")
+                return False
+            self.acknowledge_changes()
+            msg = self._get_next_message()
+        return True
+
+    def send_changes_to_device(self):
+        msg = self.sync_data_storage.get_next_change_for_device()
+        while msg:
+            self._mobile_sync.send(msg)
+
+            response = self._mobile_sync.receive()
+            if response[0].get_value() == "SDMessageRemapRecordIdentifiers":
+                self.sync_data_storage.remap_record_identifiers(response)
+
+        return True
+
+    def set_data_storage(self, sync_data_storage):
+        self.sync_data_storage = sync_data_storage
+
+    def start_session(self):
+        # Start the synchronization
+        start_sync_msg = Array()
+        start_sync_msg.append( String("SDMessageSyncDataClassWithDevice") )
+        start_sync_msg.append( String(self.sync_data_storage.get_name()) )
+
+        device_anchor = self.sync_data_storage.get_device_anchor()
+        host_anchor = self.sync_data_storage.get_host_anchor()
+
+        # exchange anchors
+        start_sync_msg.append( String(device_anchor) )
+        start_sync_msg.append( String(host_anchor) )
+
+        start_sync_msg.append( Integer(self.sync_data_storage.get_version() ) )
+        start_sync_msg.append( String(self.EMPTY_PARAM) )
+        self._mobile_sync.send(start_sync_msg)
+
+        response = self._mobile_sync.receive()
+        if response[0].get_value() == "SDMessageSyncDataClassWithComputer":
+            sync_type = response[4].get_value()
+            if sync_type in self.SYNC_TYPES:
+                # set new anchor for next sync
+                new_device_anchor = response[2].get_value()
+                self.sync_data_storage.set_host_anchor(host_anchor)
+                self.sync_data_storage.set_device_anchor(new_device_anchor)
+                return sync_type
+
+        if response[0].get_value() == "SDMessageRefuseToSyncDataClassWithComputer":
+            print "Device refused synchronization. Reason: %s" % (response[2].get_value())
+
+        self._report_if_device_cancelled_session(response)
+
+        return False
+
+    # FIXME: send: {SDMessageProcessChanges, com.apple.Bookmarks, SyncDeviceLinkEntityNamesKey, SyncDeviceLinkAllRecordsOfPulledEntityTypeSentKey, com.apple.bookmarks.Folder}
+    # FIXME: recv: {SDMessageRemapRecordIdentifiers, com.apple.Bookmarks, ___EmptyParameterString___}
+
+    def get_changes_from_device(self):
+        msg = Array()
+        msg.append( String("SDMessageGetChangesFromDevice") )
+        msg.append( String(self.sync_data_storage.get_name()) )
+        self._mobile_sync.send(msg)
+
+    def get_all_records_from_device(self):
+        msg = Array()
+        msg.append( String("SDMessageGetAllRecordsFromDevice") )
+        msg.append( String(self.sync_data_storage.get_name()) )
+        self._mobile_sync.send(msg)
+
+    def clear_all_records_on_device(self):
+        msg = Array()
+        msg.append( String("SDMessageClearAllRecordsOnDevice") )
+        msg.append( String(self.sync_data_storage.get_name()) )
+        self._mobile_sync.send(msg)
+
+        response = self._mobile_sync.receive()
+        if response[0].get_value() == "SDMessageDeviceWillClearAllRecords":
+            return True
+        return False
+
+    def _get_next_message(self):
+        msg = self._mobile_sync.receive()
+        return msg
+
+    def _is_device_ready_to_receive_changes(self, msg):
+        if msg[0].get_value() == "SDMessageDeviceReadyToReceiveChanges":
+            return True
+        return False
+
+    def _report_if_device_cancelled_session(self, msg):
+        if msg[0].get_value() == "SDMessageCancelSession":
+            print "Device cancelled session. Reason: %s" % (msg[2].get_value())
+
+    def acknowledge_changes(self):
+        msg = Array()
+        msg.append( String("SDMessageAcknowledgeChangesFromDevice") )
+        msg.append( String(self.sync_data_storage.get_name()) )
+        self._mobile_sync.send(msg)
+
+    def cancel_session(self, reason):
+        msg = Array()
+        msg.append( String("SDMessageCancelSession") )
+        msg.append( String(self.sync_data_storage.get_name()) )
+        msg.append( String(reason) )
+        self._mobile_sync.send(msg)
+
+    def finish_session(self):
+        msg = Array()
+        msg.append( String("SDMessageFinishSessionOnDevice") )
+        msg.append( String(self.sync_data_storage.get_name()) )
+        self._mobile_sync.send(msg)
+
+        response = self._mobile_sync.receive()
+        if response[0].get_value() == "SDMessageDeviceFinishedSession":
+            return True
+
+        self._report_if_device_cancelled_session(response)
+
+        return False
+
+    def ping(self):
+        msg = Array()
+        msg.append( String("DLMessagePing") )
+        msg.append( String("Preparing to get changes for device") )
+        self._mobile_sync.send(msg)
+
+    def disconnect(self):
+        msg = Array()
+        msg.append( String("DLMessageDisconnect") )
+        msg.append( String("All done, thanks for the memories") )
+        self._mobile_sync.send(msg)
+
+    def __del__(self):
+        if self._mobile_sync:
+            del(self._mobile_sync)
+        if self._lckd:
+            del(self._lckd)
+        if self._phone:
+            del(self._phone)
+
diff --git a/conduit/modules/iPodModule/idevice/iphonesync.py b/conduit/modules/iPodModule/idevice/iphonesync.py
new file mode 100755
index 0000000..0c21562
--- /dev/null
+++ b/conduit/modules/iPodModule/idevice/iphonesync.py
@@ -0,0 +1,154 @@
+#! /usr/bin/env python
+
+from plist import *
+from imobiledevice import *
+
+from iCalendarOutput import *
+from vCardOutput import *
+from XMLNotesOutput import *
+from iPhoneDataStorage import *
+from iPhoneSyncAgent import *
+from XBELOutput import *
+
+import sys, re, time, datetime, base64
+from lxml import etree
+
+# FIXME: The notification_proxy should be used to show sync screen on device
+
+''' Main '''
+
+class Application():
+    def __init__(self):
+        # init the data classes
+        self.contacts = iPhoneContactsDataStorage()
+        self.calendars = iPhoneCalendarsDataStorage()
+        self.bookmarks = iPhoneBookmarksDataStorage()
+        self.mailaccounts = iPhoneMailAccountsDataStorage()
+        self.notes = iPhoneNotesDataStorage()
+
+        self.supported_data_storage_types = [
+            self.contacts,
+            self.calendars,
+            self.bookmarks,
+            self.mailaccounts,
+            self.notes
+        ]
+
+        if (("-h" in sys.argv) or ("--help" in sys.argv)):
+            self.print_usage()
+            sys.exit()
+
+        if (len(sys.argv) < 2):
+            print "ERROR: You must specify at least one data storage type."
+            self.print_usage()
+            sys.exit(1)
+
+        # remove data classes not specified on command line
+        sync_data_storage_types = []
+        for data_storage in self.supported_data_storage_types:
+            if data_storage.get_name() in sys.argv:
+                sync_data_storage_types.append(data_storage)
+
+        if len(sync_data_storage_types) == 0:
+            print "ERROR: The data storage type you passed is not supported."
+            self.print_usage()
+            sys.exit(1)
+
+        # create client for mobilesync message protocol
+        uuid = None
+        if ("-u" in sys.argv):
+            uuid = sys.argv[sys.argv.index("-u") + 1]
+        elif ("--uuid" in sys.argv):
+            uuid = sys.argv[sys.argv.index("--uuid") + 1]
+
+        agent = iPhoneSyncAgent()
+
+        # try to connect to device and mobilesync service
+        if not agent.connect(uuid):
+            sys.exit(1)
+
+        if ("-d" in sys.argv) or ("--debug" in sys.argv):
+            agent.set_debug_level(1)
+
+        # sync all data tyoes
+        for data_storage in sync_data_storage_types:
+            agent.set_data_storage(data_storage);
+            sync_type = agent.start_session()
+            if sync_type:
+               if ("-c" in sys.argv) or ("--clear" in sys.argv):
+                   if agent.clear_all_records_on_device():
+                       print "Successfully cleared all records of %s from device." % (data_storage.get_name())
+                   else:
+                       print "ERROR: Failed to clear records of %s from device." % (data_storage.get_name())
+               else:
+                   agent.synchronize(sync_type)
+            agent.finish_session()
+
+        # disconnect from mobilesync (important, as it makes the SyncAgent quit)
+        agent.disconnect()
+
+        # FIXME: Just dump received records for testing purposes
+        self.print_results()
+
+    def print_usage(self):
+        print "Usage: %s [OPTION]... [TYPE]..." % (sys.argv[0])
+        print "Synchronize data TYPEs on an iPhone or iPod Touch with this computer."
+        print ""
+        print "Supported data storage TYPEs are:"
+        for data_storage in self.supported_data_storage_types:
+            print "  %s (version %d)" % (data_storage.get_name(), data_storage.get_version())
+        print ""
+        print "Options:"
+        print "  -o, --output FILE\tSave received records to FILE"
+        print "\t\t\tthe saved data will be saved as:"
+        print "\t\t\t  vCard for contacts"
+        print "\t\t\t  iCalendar for calendars"
+        print "\t\t\t  XBEL for bookmarks"
+        print "  -u, --uuid UUID\ttarget specifc device by UUID"
+        print "  -c, --clear\t\tclear all records from device"
+        print "  -d, --debug\t\tenable libiphone debug mode"
+        print "  -h, --help\t\tdisplay this help and exit"
+
+    def get_data_storage_type_by_name(self, name):
+        for t in self.supported_data_storage_types:
+            if t.get_name() == name:
+                return t;
+        return None
+
+    def print_results(self):
+        filename = None
+        output = ""
+
+        output_map = {
+            'com.apple.Contacts': vCardOutput,
+            'com.apple.Calendars': iCalendarOutput,
+            'com.apple.Bookmarks': XBELOutput,
+            'com.apple.MailAccounts': None,
+            'com.apple.Notes': XMLNotesOutput
+        }
+
+        if ("-o" in sys.argv):
+            filename = sys.argv[sys.argv.index("-o") + 1]
+        elif ("--output" in sys.argv):
+            filename = sys.argv[sys.argv.index("--output") + 1]
+
+        for name, output_class in output_map.iteritems():
+            if name in sys.argv:
+               if output_class:
+                   data_storage = self.get_data_storage_type_by_name(name)
+                   output += output_class(data_storage).serialize()
+               else:
+                   print "WARNING: Output for type %s is not implemented." % (name)
+                   continue
+
+        if filename:
+            f = open(filename, "wb")
+            f.write( output )
+            f.close()
+        else:
+            print output
+
+
+if __name__ == '__main__':
+    application = Application()
+
diff --git a/conduit/modules/iPodModule/idevice/vCardOutput.py b/conduit/modules/iPodModule/idevice/vCardOutput.py
new file mode 100644
index 0000000..728758a
--- /dev/null
+++ b/conduit/modules/iPodModule/idevice/vCardOutput.py
@@ -0,0 +1,93 @@
+#! /usr/bin/env python
+
+from plist import *
+from imobiledevice import *
+
+from iPhoneRecordEntities import *
+
+import sys, re, time, datetime, base64
+
+ODD_EPOCH = 978307200 # For some reason Apple decided the epoch was on a different day?
+
+class vCardOutput():
+    def __init__(self, data_class):
+        self.data_class = data_class
+
+    def serialize(self):
+        storage = self.data_class.get_storage_for_entity(iPhoneContactRecordEntity)
+        l = len(storage)
+
+        result = ''
+        skip = 0
+        for k, i in storage.iteritems():
+            if skip > 0 and skip < l:
+                result += "\n"
+            result += self._to_vcard(i)
+            skip += 1
+        return result
+
+    def _to_vcard(self, record):
+        result = "BEGIN:VCARD\n"
+        result += "VERSION:3.0\n"
+        result += "UID:%s iphone\n" % record.id
+
+        # contact groups
+        children = self.data_class.get_children_of_record_by_type(record, iPhoneGroupRecordEntity)
+        if len(children) > 0:
+            categories = ""
+            for child in children:
+                if categories != "":
+                    categories += ","
+                categories += child.data.name
+            result += "CATEGORIES:%s\n" % (categories)
+
+        if record.has_value("display as company"):
+            if record.data.display_as_company == "person":
+                result += "N:%s;%s\n" % (record.data.last_name, record.data.first_name)
+                result += "FN:%s %s\n" % (record.data.first_name, record.data.last_name)
+
+        if record.has_value("company name"):
+            result += "ORG:%s;\n" % (record.data.company_name)
+
+        children = self.data_class.get_children_of_record_by_type(record, iPhonePhoneNumberRecordEntity)
+        for child in children:
+            teltype = child.data.type.upper()
+            # MobileSync sends "mobile" for phones, which according to RFCs is wrong
+            if teltype == "MOBILE":
+                teltype = "CELL"
+            result += "TEL;%s:%s\n" % (teltype, child.data.value)
+
+        children = self.data_class.get_children_of_record_by_type(record, iPhoneStreetAddressRecordEntity)
+        for child in children:
+            result += "ADR;%s:" % (child.data.type.upper())
+            result += ";;%s;" % (child.data.street)
+            result += "%s;%s;%s;" % (child.data.city, child.data.country_code, child.data.postal_code)
+            result += "%s\n" % (child.data.country)
+
+        children = self.data_class.get_children_of_record_by_type(record, iPhoneEMailAddressRecordEntity)
+        for child in children:
+            result += "EMAIL;TYPE=%s:%s\n" % (child.data.type.upper(), child.data.value)
+
+        children = self.data_class.get_children_of_record_by_type(record, iPhoneURLRecordEntity)
+        for child in children:
+            result += "URL:%s\n" % (child.data.value)
+
+        children = self.data_class.get_children_of_record_by_type(record, iPhoneDateRecordEntity)
+        for child in children:
+            t = child.data.type
+            date_str = time.strftime("%Y-%m-%d", time.localtime(time.mktime(time.strptime(child.data.value, "%Y-%m-%dT%H:%M:%SZ"))+ODD_EPOCH))
+            if t == "anniversary":
+                result += "X-EVOLUTION-ANNIVERSARY:%s\n" % (date_str)
+            elif t == "birthday":
+                result += "BDAY:%s\n" % (date_str)
+
+        if 'image' in record.data:
+            result += "PHOTO;ENCODING=BASE64;TYPE=JPEG:"
+            result += base64.b64encode(record.data['image']) + "\n"
+
+        if record.has_value("notes"):
+            result += "NOTE:%s\n" % (record.data.notes)
+
+        result += "END:VCARD"
+        return result
+



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]