diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py
index 20e4763..99a269b 100644
--- a/pykolab/xml/__init__.py
+++ b/pykolab/xml/__init__.py
@@ -1,63 +1,69 @@
from attendee import Attendee
from attendee import InvalidAttendeeParticipantStatusError
from attendee import participant_status_label
from contact import Contact
+from contact import ContactIntegrityError
+from contact import contact_from_string
+from contact import contact_from_message
from contact_reference import ContactReference
from recurrence_rule import RecurrenceRule
from event import Event
from event import EventIntegrityError
from event import InvalidEventDateError
from event import InvalidEventStatusError
from event import event_from_ical
from event import event_from_string
from event import event_from_message
from todo import Todo
from todo import TodoIntegrityError
from todo import todo_from_ical
from todo import todo_from_string
from todo import todo_from_message
from note import Note
from note import NoteIntegrityError
from note import note_from_string
from note import note_from_message
from utils import property_label
from utils import property_to_string
from utils import compute_diff
from utils import to_dt
__all__ = [
"Attendee",
"Contact",
"ContactReference",
"Event",
"Todo",
"Note",
"RecurrenceRule",
"event_from_ical",
"event_from_string",
"event_from_message",
"todo_from_ical",
"todo_from_string",
"todo_from_message",
"note_from_string",
"note_from_message",
+ "contact_from_string",
+ "contact_from_message",
"property_label",
"property_to_string",
"compute_diff",
"to_dt",
]
errors = [
"EventIntegrityError",
"InvalidEventDateError",
"InvalidAttendeeParticipantStatusError",
"TodoIntegrityError",
"NoteIntegrityError",
+ "ContactIntegrityError",
]
__all__.extend(errors)
diff --git a/pykolab/xml/contact.py b/pykolab/xml/contact.py
index 97987d9..2b08f9a 100644
--- a/pykolab/xml/contact.py
+++ b/pykolab/xml/contact.py
@@ -1,49 +1,336 @@
import kolabformat
+import datetime
+import pytz
+import base64
+
+from pykolab.xml import utils as xmlutils
+from pykolab.xml.utils import ustr
+
+def contact_from_vcard(string):
+ # TODO: implement this
+ pass
+
+def contact_from_string(string):
+ _xml = kolabformat.readContact(string, False)
+ return Contact(_xml)
+
+def contact_from_message(message):
+ contact = None
+ if message.is_multipart():
+ for part in message.walk():
+ if part.get_content_type() == "application/vcard+xml":
+ payload = part.get_payload(decode=True)
+ contact = contact_from_string(payload)
+
+ # append attachment parts to Contact object
+ elif contact and part.has_key('Content-ID'):
+ contact._attachment_parts.append(part)
+
+ return contact
+
class Contact(kolabformat.Contact):
type = 'contact'
+ related_map = {
+ 'manager': kolabformat.Related.Manager,
+ 'assistant': kolabformat.Related.Assistant,
+ 'spouse': kolabformat.Related.Spouse,
+ 'children': kolabformat.Related.Child,
+ None: kolabformat.Related.NoRelation,
+ }
+
+ addresstype_map = {
+ 'home': kolabformat.Address.Home,
+ 'work': kolabformat.Address.Work,
+ }
+
+ phonetype_map = {
+ 'home': kolabformat.Telephone.Home,
+ 'work': kolabformat.Telephone.Work,
+ 'text': kolabformat.Telephone.Text,
+ 'main': kolabformat.Telephone.Voice,
+ 'homefax': kolabformat.Telephone.Fax,
+ 'workfax': kolabformat.Telephone.Fax,
+ 'mobile': kolabformat.Telephone.Cell,
+ 'video': kolabformat.Telephone.Video,
+ 'pager': kolabformat.Telephone.Pager,
+ 'car': kolabformat.Telephone.Car,
+ 'other': kolabformat.Telephone.Textphone,
+ }
+
+ emailtype_map = {
+ 'home': kolabformat.Email.Home,
+ 'work': kolabformat.Email.Work,
+ 'other': kolabformat.Email.Work,
+ }
+
+ urltype_map = {
+ 'homepage': kolabformat.Url.NoType,
+ 'blog': kolabformat.Url.Blog,
+ }
+
+ keytype_map = {
+ 'pgp': kolabformat.Key.PGP,
+ 'pkcs7': kolabformat.Key.PKCS7_MIME,
+ None: kolabformat.Key.Invalid,
+ }
+
+ gender_map = {
+ 'female': kolabformat.Contact.Female,
+ 'male': kolabformat.Contact.Male,
+ None: kolabformat.Contact.NotSet,
+ }
+
+ properties_map = {
+ 'uid': 'get_uid',
+ 'lastmodified-date': 'get_lastmodified',
+ 'fn': 'name',
+ 'nickname': 'nickNames',
+ 'title': 'titles',
+ 'email': 'emailAddresses',
+ 'tel': 'telephones',
+ 'url': 'urls',
+ 'im': 'imAddresses',
+ 'address': 'addresses',
+ 'note': 'note',
+ 'freebusyurl': 'freeBusyUrl',
+ 'birthday': 'bDay',
+ 'anniversary': 'anniversary',
+ 'categories': 'categories',
+ 'lang': 'languages',
+ 'gender': 'get_gender',
+ 'gpspos': 'gpsPos',
+ 'key': 'keys',
+ }
+
def __init__(self, *args, **kw):
+ self._attachment_parts = []
kolabformat.Contact.__init__(self, *args, **kw)
def get_uid(self):
uid = self.uid()
if not uid == '':
return uid
else:
self.__str__()
return kolabformat.getSerializedUID()
+ def get_lastmodified(self):
+ try:
+ _datetime = self.lastModified()
+ if _datetime == None or not _datetime.isValid():
+ self.__str__()
+ except:
+ return datetime.datetime.now(pytz.utc)
+
+ return xmlutils.from_cdatetime(self.lastModified(), True)
+
def get_email(self, preferred=True):
if preferred:
return self.emailAddresses()[self.emailAddressPreferredIndex()]
else:
return [x for x in self.emailAddresses()]
def set_email(self, email, preferred_index=0):
if isinstance(email, basestring):
self.setEmailAddresses([email], preferred_index)
else:
self.setEmailAddresses(email, preferred_index)
def add_email(self, email):
if isinstance(email, basestring):
self.add_emails([email])
elif isinstance(email, list):
self.add_emails(email)
def add_emails(self, emails):
preferred_email = self.get_email()
emails = [x for x in set(self.get_email(preferred=False) + emails)]
preferred_email_index = emails.index(preferred_email)
self.setEmailAddresses(emails, preferred_email_index)
def set_name(self, name):
- self.setName(name)
+ self.setName(ustr(name))
+
+ def get_gender(self, translated=True):
+ _gender = self.gender()
+ if translated:
+ return self._translate_value(_gender, self.gender_map)
+ return _gender
+
+ def _translate_value(self, val, map):
+ name_map = dict([(v, k) for (k, v) in map.iteritems()])
+ return name_map[val] if name_map.has_key(val) else 'UNKNOWN'
+
+ def to_dict(self):
+ if not self.isValid():
+ return None
+
+ data = self._names2dict(self.nameComponents())
+
+ for p, getter in self.properties_map.iteritems():
+ val = None
+ if hasattr(self, getter):
+ val = getattr(self, getter)()
+ if isinstance(val, kolabformat.cDateTime):
+ val = xmlutils.from_cdatetime(val, True)
+ elif isinstance(val, kolabformat.vectori):
+ val = [int(x) for x in val]
+ elif isinstance(val, kolabformat.vectors):
+ val = [str(x) for x in val]
+ elif isinstance(val, kolabformat.vectortelephone):
+ val = [self._struct2dict(x, 'number', self.phonetype_map) for x in val]
+ elif isinstance(val, kolabformat.vectoremail):
+ val = [self._struct2dict(x, 'address', self.emailtype_map) for x in val]
+ elif isinstance(val, kolabformat.vectorurl):
+ val = [self._struct2dict(x, 'url', self.urltype_map) for x in val]
+ elif isinstance(val, kolabformat.vectorkey):
+ val = [self._struct2dict(x, 'key', self.keytype_map) for x in val]
+ elif isinstance(val, kolabformat.vectoraddress):
+ val = [self._address2dict(x) for x in val]
+ elif isinstance(val, kolabformat.vectorgeo):
+ val = [[x.latitude, x.longitude] for x in val]
+
+ if val is not None:
+ data[p] = val
+
+ affiliations = self.affiliations()
+ if len(affiliations) > 0:
+ _affiliation = self._affiliation2dict(affiliations[0])
+ if _affiliation.has_key('address'):
+ data['address'].extend(_affiliation['address'])
+ _affiliation.pop('address', None)
+ data.update(_affiliation)
+
+ data.update(self._relateds2dict(self.relateds()))
+
+ if self.photoMimetype():
+ data['photo'] = dict(mimetype=self.photoMimetype(), base64=base64.b64encode(self.photo()))
+ elif self.photo():
+ data['photo'] = dict(uri=self.photo())
+
+ return data
- def to_ditc(self):
- # TODO: implement this
- return dict(name=self.name())
+ def _names2dict(self, namecomp):
+ names_map = {
+ 'surname': 'surnames',
+ 'given': 'given',
+ 'additional': 'additional',
+ 'prefix': 'prefixes',
+ 'suffix': 'suffixes',
+ }
+
+ data = dict()
+
+ for p, getter in names_map.iteritems():
+ val = None
+ if hasattr(namecomp, getter):
+ val = getattr(namecomp, getter)()
+ if isinstance(val, kolabformat.vectors):
+ val = [str(x) for x in val][0] if len(val) > 0 else None
+ if val is not None:
+ data[p] = val
+
+ return data
+
+ def _affiliation2dict(self, affiliation):
+ props_map = {
+ 'organization': 'organisation',
+ 'department': 'organisationalUnits',
+ 'role': 'roles',
+ }
+
+ data = dict()
+
+ for p, getter in props_map.iteritems():
+ val = None
+ if hasattr(affiliation, getter):
+ val = getattr(affiliation, getter)()
+ if isinstance(val, kolabformat.vectors):
+ val = [str(x) for x in val][0] if len(val) > 0 else None
+ if val is not None:
+ data[p] = val
+
+ data.update(self._relateds2dict(affiliation.relateds(), True))
+
+ addresses = affiliation.addresses()
+ if len(addresses):
+ data['address'] = [self._address2dict(adr, 'office') for adr in addresses]
+
+ return data
+
+ def _address2dict(self, adr, adrtype=None):
+ props_map = {
+ 'label': 'label',
+ 'street': 'street',
+ 'locality': 'locality',
+ 'region': 'region',
+ 'code': 'code',
+ 'country': 'country',
+ }
+ addresstype_map = dict([(v, k) for (k, v) in self.addresstype_map.iteritems()])
+
+ data = dict()
+
+ if adrtype is None:
+ adrtype = addresstype_map.get(adr.types(), None)
+
+ if adrtype is not None:
+ data['type'] = adrtype
+
+ for p, getter in props_map.iteritems():
+ val = None
+ if hasattr(adr, getter):
+ val = getattr(adr, getter)()
+ if isinstance(val, kolabformat.vectors):
+ val = [str(x) for x in val][0] if len(val) > 0 else None
+ if val is not None:
+ data[p] = val
+
+ return data
+
+ def _relateds2dict(self, relateds, aslist=True):
+ data = dict()
+
+ related_map = dict([(v, k) for (k, v) in self.related_map.iteritems()])
+ for rel in relateds:
+ reltype = related_map.get(rel.relationTypes(), None)
+ val = rel.uri() if rel.type() == kolabformat.Related.Uid else rel.text()
+ if reltype and val is not None:
+ if aslist:
+ if not data.has_key(reltype):
+ data[reltype] = []
+ data[reltype].append(val)
+ else:
+ data[reltype] = val
+
+ return data
+
+ def _struct2dict(self, struct, propname, map):
+ type_map = dict([(v, k) for (k, v) in map.iteritems()])
+ result = dict()
+
+ if hasattr(struct, 'types'):
+ result['type'] = type_map.get(struct.types(), None)
+ elif hasattr(struct, 'type'):
+ result['type'] = type_map.get(struct.type(), None)
+
+ if hasattr(struct, propname):
+ result[propname] = getattr(struct, propname)()
+
+ return result
def __str__(self):
- return kolabformat.writeContact(self)
+ xml = kolabformat.writeContact(self)
+ error = kolabformat.error()
+
+ if error == None or not error:
+ return xml
+ else:
+ raise ContactIntegrityError, kolabformat.errorMessage()
+
+
+class ContactIntegrityError(Exception):
+ def __init__(self, message):
+ Exception.__init__(self, message)
diff --git a/tests/unit/test-019-contact.py b/tests/unit/test-019-contact.py
new file mode 100644
index 0000000..d5366f6
--- /dev/null
+++ b/tests/unit/test-019-contact.py
@@ -0,0 +1,351 @@
+import datetime
+import pytz
+import unittest
+import kolabformat
+
+from pykolab.xml import Contact
+from pykolab.xml import ContactIntegrityError
+from pykolab.xml import contact_from_string
+from pykolab.xml import contact_from_message
+from email import message_from_string
+
+xml_contact = """
+
+
+
+ urn:uuid:437656b2-d55e-11e4-a43b-080027b7afc5
+
+
+ 3.1.0
+
+
+ Roundcube-libkolab-1.1 Libkolabxml-1.2
+
+
+ 20150328T152236Z
+
+
+ individual
+
+
+ Sample Dude
+
+
+ Dude
+ Sample
+ M.
+ Dr.
+ Jr.
+
+
+ This is a sample contact for testing
+
+
+ Head of everything
+
+
+
+ Kolab Inc.
+ R&D Department
+
+
+
+
+ x-manager
+
+
+ Jane Manager
+
+
+
+
+ x-assistant
+
+
+ Mrs. Moneypenny
+
+
+
+
+
+ O-steet
+ San Francisco
+ CA
+ 55550
+ USA
+
+
+
+ www.kolab.org
+
+
+
+
+ home
+
+
+
+
+ Homestreet 11
+ Hometown
+
+ 12345
+ Germany
+
+
+
+
+ work
+
+
+
+
+ Workstreet 22
+ Worktown
+
+ 4567
+ Switzerland
+
+
+ the dude
+
+
+
+
+ spouse
+
+
+ Leia
+
+
+
+
+ child
+
+
+ Jay
+
+
+
+
+ child
+
+
+ Bob
+
+
+ 20010401
+
+
+ 20100705
+
+
+ data:image/gif;base64,R0lGODlhAQABAPAAAOjq6gAAACH/C1hNUCBEYXRhWE1QAT8AIfkEBQAAAAAsAAAAAAEAAQAAAgJEAQA7
+
+
+ M
+
+
+
+
+ home
+
+
+ +49-555-11223344
+
+
+
+
+ work
+
+
+ +49-555-44556677
+
+
+
+
+ cell
+
+
+ +41-777-55588899
+
+
+ jabber:dude@kolab.org
+
+
+
+
+ home
+
+
+ home@kolab.org
+
+
+
+
+ work
+
+
+ work@kolab.org
+
+
+ data:application/pgp-keys;base64,LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tDQpWZXJzaW9uOiBHbnVQRy9NYWNHUEcyIHYyLjAuMjINCg0KbVFHaUJFSVNOcUVSQkFDUnovb3J5L0JEY3pBWUFUR3JnTSt5WDgzV2pkaUVrNmZKNFFUekk2ZFZ1TkxTNy4uLg0KLS0tLS1FTkQgUEdQIFBVQkxJQyBLRVkgQkxPQ0stLS0tLQ==
+
+
+
+"""
+
+contact_mime_message = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_4ff5155d75dc1328b7f5fe10ddce8d24"
+From: john.doe@example.org
+To: john.doe@example.org
+Date: Mon, 13 Apr 2015 15:26:44 +0200
+X-Kolab-Type: application/x-vnd.kolab.contact
+X-Kolab-Mime-Version: 3.0
+Subject: 05cfc56d-2bb3-46d1-ada4-5f5310337fb2
+User-Agent: Roundcube Webmail/1.2-git
+
+--=_4ff5155d75dc1328b7f5fe10ddce8d24
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=ISO-8859-1
+
+This is a Kolab Groupware object. To view this object you will need an emai=
+l client that understands the Kolab Groupware format. For a list of such em=
+ail clients please visit http://www.kolab.org/
+
+--=_4ff5155d75dc1328b7f5fe10ddce8d24
+Content-Transfer-Encoding: 8bit
+Content-Type: application/vcard+xml; charset=UTF-8;
+ name=kolab.xml
+Content-Disposition: attachment;
+ filename=kolab.xml;
+ size=1636
+
+
+
+
+
+ urn:uuid:05cfc56d-2bb3-46d1-ada4-5f5310337fb2
+
+
+ 3.1.0
+
+
+ Roundcube-libkolab-1.1 Libkolabxml-1.1
+
+
+ 20150413T132644Z
+
+
+ individual
+
+
+ User One
+
+
+ User One
+ DAV
+
+
+ This is a Kolab contact
+
+
+
+
+ home
+
+
+ +1555224488
+
+
+
+
+ home
+
+
+ dav.user01@example.org
+
+
+
+
+ home
+
+
+ user.one@example.org
+
+
+
+
+--=_4ff5155d75dc1328b7f5fe10ddce8d24--
+"""
+
+class TestContactXML(unittest.TestCase):
+ contact = Contact()
+
+ def assertIsInstance(self, _value, _type):
+ if hasattr(unittest.TestCase, 'assertIsInstance'):
+ return unittest.TestCase.assertIsInstance(self, _value, _type)
+ else:
+ if (type(_value)) == _type:
+ return True
+ else:
+ raise AssertionError, "%s != %s" % (type(_value), _type)
+
+ def test_001_minimal(self):
+ self.contact.set_name("test")
+ self.assertEqual("test", self.contact.name())
+ self.assertIsInstance(self.contact.__str__(), str)
+
+ def test_002_full(self):
+ self.contact.set_name("test")
+ # TODO: add more setters and getter tests here
+
+ def test_010_load_from_xml(self):
+ contact = contact_from_string(xml_contact)
+ self.assertEqual(contact.get_uid(), '437656b2-d55e-11e4-a43b-080027b7afc5')
+ self.assertEqual(contact.name(), 'Sample Dude')
+
+ def test_011_load_from_message(self):
+ contact = contact_from_message(message_from_string(contact_mime_message))
+ self.assertEqual(contact.get_uid(), '05cfc56d-2bb3-46d1-ada4-5f5310337fb2')
+ self.assertEqual(contact.name(), 'User One')
+
+ def test_020_to_dict(self):
+ data = contact_from_string(xml_contact).to_dict()
+
+ self.assertIsInstance(data, dict)
+ self.assertIsInstance(data['lastmodified-date'], datetime.datetime)
+ self.assertEqual(data['uid'], '437656b2-d55e-11e4-a43b-080027b7afc5')
+ self.assertEqual(data['fn'], 'Sample Dude')
+ self.assertEqual(data['given'], 'Sample')
+ self.assertEqual(data['surname'], 'Dude')
+ self.assertEqual(data['prefix'], 'Dr.')
+ self.assertEqual(data['suffix'], 'Jr.')
+ self.assertIsInstance(data['birthday'], datetime.date)
+ self.assertIsInstance(data['anniversary'], datetime.date)
+ self.assertEqual(data['organization'], 'Kolab Inc.')
+ self.assertEqual(data['department'], 'R&D Department')
+ self.assertEqual(data['manager'], ['Jane Manager'])
+ self.assertEqual(data['note'], 'This is a sample contact for testing')
+ self.assertEqual(len(data['address']), 3)
+ self.assertEqual(data['address'][0]['type'], 'home')
+ self.assertEqual(data['address'][1]['type'], 'work')
+ self.assertEqual(data['address'][2]['type'], 'office')
+ self.assertEqual(len(data['tel']), 3)
+ self.assertEqual(data['tel'][0]['type'], 'home')
+ self.assertEqual(data['tel'][0]['number'], '+49-555-11223344')
+ self.assertEqual(data['tel'][1]['type'], 'work')
+ self.assertEqual(data['tel'][2]['type'], 'mobile')
+ self.assertEqual(len(data['email']), 2)
+ self.assertEqual(data['email'][0]['type'], 'home')
+ self.assertEqual(data['email'][0]['address'], 'home@kolab.org')
+ self.assertEqual(len(data['url']), 1)
+ self.assertEqual(len(data['key']), 1)
+ self.assertEqual(data['key'][0]['type'], 'pgp')
+ self.assertIsInstance(data['photo'], dict)
+ self.assertEqual(data['photo']['mimetype'], 'image/gif')
+
+
+if __name__ == '__main__':
+ unittest.main()
\ No newline at end of file