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 + + + <text>Head of everything</text> + + + + 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