[conduit/iphone: 4/4] Import iPhoneModule
- From: John Stowers <jstowers src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [conduit/iphone: 4/4] Import iPhoneModule
- Date: Wed, 22 Sep 2010 14:16:43 +0000 (UTC)
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]