diff options
-rw-r--r-- | pykolab/xml/__init__.py | 1 | ||||
-rw-r--r-- | pykolab/xml/event.py | 57 | ||||
-rw-r--r-- | tests/unit/test-003-event.py | 161 | ||||
-rw-r--r-- | tests/unit/test-012-wallace_invitationpolicy.py | 2 |
4 files changed, 169 insertions, 52 deletions
diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py index 5ca2837..64b06ae 100644 --- a/pykolab/xml/__init__.py +++ b/pykolab/xml/__init__.py @@ -9,6 +9,7 @@ from event import EventIntegrityError from event import InvalidEventDateError from event import event_from_ical from event import event_from_string +from event import event_from_message from utils import to_dt diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py index fcb3a17..39034f6 100644 --- a/pykolab/xml/event.py +++ b/pykolab/xml/event.py @@ -6,6 +6,7 @@ import kolabformat import pytz import time import uuid +import base64 import pykolab from pykolab import constants @@ -13,6 +14,7 @@ from pykolab import utils from pykolab.xml import utils as xmlutils from pykolab.translate import _ +from os import path from attendee import Attendee from contact_reference import ContactReference @@ -24,6 +26,21 @@ def event_from_ical(string): def event_from_string(string): return Event(from_string=string) +def event_from_message(message): + event = None + if message.is_multipart(): + for part in message.walk(): + if part.get_content_type() == "application/calendar+xml": + payload = part.get_payload(decode=True) + event = event_from_string(payload) + + # append attachment parts to Event object + elif event and part.has_key('Content-ID'): + event._attachment_parts.append(part) + + return event + + class Event(object): status_map = { "TENTATIVE": kolabformat.StatusTentative, @@ -40,6 +57,7 @@ class Event(object): def __init__(self, from_ical="", from_string=""): self._attendees = [] self._categories = [] + self._attachment_parts = [] if from_ical == "": if from_string == "": @@ -751,15 +769,50 @@ class Event(object): msg["Subject"] = self.get_uid() - part.set_payload(str(self)) + # extract attachment data into separate MIME parts + vattach = self.event.attachments() + i = 0 + for attach in vattach: + if attach.uri(): + continue + + mimetype = attach.mimetype() + (primary, seconday) = mimetype.split('/') + name = attach.label() + if not name: + name = 'unknown.x' - # TODO: extract attachment data to separate MIME parts + (basename, suffix) = path.splitext(name) + t = datetime.datetime.now() + cid = "%s.%s.%s%s" % (basename, time.mktime(t.timetuple()), t.microsecond + len(self._attachment_parts), suffix) + + p = MIMEBase(primary, seconday) + p.add_header('Content-Disposition', 'attachment', filename=name) + p.add_header('Content-Transfer-Encoding', 'base64') + p.add_header('Content-ID', '<' + cid + '>') + p.set_payload(base64.b64encode(attach.data())) + + self._attachment_parts.append(p) + + # modify attachment object + attach.setData('', mimetype) + attach.setUri('cid:' + cid, mimetype) + vattach[i] = attach + i += 1 + + self.event.setAttachments(vattach) + + part.set_payload(str(self)) part.add_header('Content-Disposition', 'attachment; filename="kolab.xml"') part.replace_header('Content-Transfer-Encoding', '8bit') msg.attach(part) + # append attachment parts + for p in self._attachment_parts: + msg.attach(p) + return msg def to_message_itip(self, from_address, method="REQUEST", participant_status="ACCEPTED", subject=None, message_text=None): diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py index f9ef92e..363c75e 100644 --- a/tests/unit/test-003-event.py +++ b/tests/unit/test-003-event.py @@ -12,6 +12,58 @@ from pykolab.xml import InvalidAttendeeParticipantStatusError from pykolab.xml import InvalidEventDateError from pykolab.xml import event_from_ical from pykolab.xml import event_from_string +from pykolab.xml import event_from_message + +ical_event = """ +BEGIN:VEVENT +UID:7a35527d-f783-4b58-b404-b1389bd2fc57 +DTSTAMP;VALUE=DATE-TIME:20140407T122311Z +CREATED;VALUE=DATE-TIME:20140407T122245Z +LAST-MODIFIED;VALUE=DATE-TIME:20140407T122311Z +DTSTART;TZID=Europe/Zurich;VALUE=DATE-TIME:20140523T110000 +DURATION:PT1H30M0S +RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10 +EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140530T110000 +EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140620T110000 +SUMMARY:Summary +LOCATION:Location +DESCRIPTION:Description\\n2 lines +CATEGORIES:Personal +TRANSP:OPAQUE +PRIORITY:2 +SEQUENCE:2 +CLASS:PUBLIC +ATTENDEE;CN="Manager, Jane";PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYP + E=INDIVIDUAL;RSVP=TRUE:mailto:jane.manager@example.org +ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;ROLE=OPT-PARTICIPANT;RSVP=FA + LSE:MAILTO:max@imum.com +ORGANIZER;CN=Doe\, John:mailto:john.doe@example.org +URL:http://somelink.com/foo +ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=image/png;X-LABEL=silhouette.pn + g:iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAIAAADY27xgAAAAGXRFWHRTb2Z0d2FyZQBBZG9i + ZSBJbWFnZVJlYWR5ccllPAAAAsRJREFUeNrsmeluKjEMhTswrAWB4P3fECGx79CjsTDmOKRkpF + xxpfoHSmchX7ybFrfb7eszpPH1MfKH8ofyH6KUtd/c7/en0wmfWBdF0Wq1Op1Ou91uNGoer6iX + V1ar1Xa7xUJeB4qsr9frdyVlWWZH2VZyPp+xPXHIAoK70+m02+1m9JXj8bhcLi+Xi3J4xUCazS + bUltdtd7ud7ldUIhC3u+iTwF0sFhlR4Kds4LtRZK1w4te5UM6V6JaqhqC3CQ28OAsKggJfbZ3U + eozCqZ4koHIZCGmD9ivuos9YONFirmxrI0UNZG1kbZeUXdJQNJNa91RlqMn0ekYUMZDup6dXVV + m+1OSZhqLx6bVCELJGSsyFQtFrF15JGYMZgoxubWGDSDVhvTipDKWhoBOIpFobxtlbJ0Gh0/tg + lgXal4woUHi/36fQoBQncDAlupa8DeVwOPRe4lUyGAwQ+dl7W+xBXkJBhEUqR32UoJfYIKrR4d + ZBgcdIRqfEqn+mekl9FNRbSTA249la3ev1/kXHD47ZbEYR5L9kMplkd9vNZqMFyIYxxfN8Pk8q + QGlagT5QDtfrNYUMlWW9LiGNPPSmC/+OgpK2r4RO6dOatZd+4gAAemdIi6Fg9EKLD4vASWkzv3 + ew06NSCiA40CumAIoaIrhrcAwjF7aDo58gUchgNV+0n1BAcDgcoAZrXV9mI4qkhtK6FJFhi9Fo + ZKPsgQI1ACJieH/Kd570t+xFoIzHYzl5Q40CFGrSqGuks3qmYIKJfIl0nPKLxAMFw7Dv1+2QYf + vFSOBQubbOFDSc7ZcfWvHv6DzhOzT6IeOVPuz8Roex0f6EgsE/2IL4qdg7hIXz7/pBie7q1uWr + tp66xrif0l1KwUE4P7Y9Gci/ZgtNRFX+Rw06Q2RigsjuDc3urwKHxuNITaaxyD9mT2WvSDAXn/ + Pvhh8BBgBjyfPSGbSYcwAAAABJRU5ErkJggg== +ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/plain;X-LABEL=text.txt:VGh + pcyBpcyBhIHRleHQgZmlsZQo= +BEGIN:VALARM +ACTION:DISPLAY +TRIGGER:-PT30M +END:VALARM +END:VEVENT +""" + class TestEventXML(unittest.TestCase): event = Event() @@ -122,55 +174,8 @@ PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject 2.1.3//EN CALSCALE:GREGORIAN METHOD:REQUEST -BEGIN:VEVENT -UID:7a35527d-f783-4b58-b404-b1389bd2fc57 -DTSTAMP;VALUE=DATE-TIME:20140407T122311Z -CREATED;VALUE=DATE-TIME:20140407T122245Z -LAST-MODIFIED;VALUE=DATE-TIME:20140407T122311Z -DTSTART;TZID=Europe/Zurich;VALUE=DATE-TIME:20140523T110000 -DURATION:PT1H30M0S -RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10 -EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140530T110000 -EXDATE;TZID=Europe/Zurich;VALUE=DATE-TIME:20140620T110000 -SUMMARY:Summary -LOCATION:Location -DESCRIPTION:Description\\n2 lines -CATEGORIES:Personal -TRANSP:OPAQUE -PRIORITY:2 -SEQUENCE:2 -CLASS:PUBLIC -ATTENDEE;CN="Manager, Jane";PARTSTAT=NEEDS-ACTION;ROLE=REQ-PARTICIPANT;CUTYP - E=INDIVIDUAL;RSVP=TRUE:mailto:jane.manager@example.org -ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;ROLE=OPT-PARTICIPANT;RSVP=FA - LSE:MAILTO:max@imum.com -ORGANIZER;CN=Doe\, John:mailto:john.doe@example.org -URL:http://somelink.com/foo -ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=image/png;X-LABEL=silhouette.pn - g:iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAIAAADY27xgAAAAGXRFWHRTb2Z0d2FyZQBBZG9i - ZSBJbWFnZVJlYWR5ccllPAAAAsRJREFUeNrsmeluKjEMhTswrAWB4P3fECGx79CjsTDmOKRkpF - xxpfoHSmchX7ybFrfb7eszpPH1MfKH8ofyH6KUtd/c7/en0wmfWBdF0Wq1Op1Ou91uNGoer6iX - V1ar1Xa7xUJeB4qsr9frdyVlWWZH2VZyPp+xPXHIAoK70+m02+1m9JXj8bhcLi+Xi3J4xUCazS - bUltdtd7ud7ldUIhC3u+iTwF0sFhlR4Kds4LtRZK1w4te5UM6V6JaqhqC3CQ28OAsKggJfbZ3U - eozCqZ4koHIZCGmD9ivuos9YONFirmxrI0UNZG1kbZeUXdJQNJNa91RlqMn0ekYUMZDup6dXVV - m+1OSZhqLx6bVCELJGSsyFQtFrF15JGYMZgoxubWGDSDVhvTipDKWhoBOIpFobxtlbJ0Gh0/tg - lgXal4woUHi/36fQoBQncDAlupa8DeVwOPRe4lUyGAwQ+dl7W+xBXkJBhEUqR32UoJfYIKrR4d - ZBgcdIRqfEqn+mekl9FNRbSTA249la3ev1/kXHD47ZbEYR5L9kMplkd9vNZqMFyIYxxfN8Pk8q - QGlagT5QDtfrNYUMlWW9LiGNPPSmC/+OgpK2r4RO6dOatZd+4gAAemdIi6Fg9EKLD4vASWkzv3 - ew06NSCiA40CumAIoaIrhrcAwjF7aDo58gUchgNV+0n1BAcDgcoAZrXV9mI4qkhtK6FJFhi9Fo - ZKPsgQI1ACJieH/Kd570t+xFoIzHYzl5Q40CFGrSqGuks3qmYIKJfIl0nPKLxAMFw7Dv1+2QYf - vFSOBQubbOFDSc7ZcfWvHv6DzhOzT6IeOVPuz8Roex0f6EgsE/2IL4qdg7hIXz7/pBie7q1uWr - tp66xrif0l1KwUE4P7Y9Gci/ZgtNRFX+Rw06Q2RigsjuDc3urwKHxuNITaaxyD9mT2WvSDAXn/ - Pvhh8BBgBjyfPSGbSYcwAAAABJRU5ErkJggg== -ATTACH;VALUE=BINARY;ENCODING=BASE64;FMTTYPE=text/plain;X-LABEL=text.txt:VGh - pcyBpcyBhIHRleHQgZmlsZQo= -BEGIN:VALARM -ACTION:DISPLAY -TRIGGER:-PT30M -END:VALARM -END:VEVENT -END:VCALENDAR -""" + """ + ical_event + "END:VCALENDAR" + ical = icalendar.Calendar.from_ical(ical_str) event = event_from_ical(ical.walk('VEVENT')[0].to_ical()) @@ -193,6 +198,26 @@ END:VCALENDAR self.assertEqual(len(event.get_alarms()), 1) self.assertEqual(len(event.get_attachments()), 2) + def test_018_ical_to_message(self): + event = event_from_ical(ical_event) + message = event.to_message() + + self.assertTrue(message.is_multipart()) + self.assertEqual(message['Subject'], event.uid) + self.assertEqual(message['X-Kolab-Type'], 'application/x-vnd.kolab.event') + + parts = [p for p in message.walk()] + attachments = event.get_attachments(); + + self.assertEqual(len(parts), 5) + self.assertEqual(parts[1].get_content_type(), 'text/plain') + self.assertEqual(parts[2].get_content_type(), 'application/calendar+xml') + self.assertEqual(parts[3].get_content_type(), 'image/png') + self.assertEqual(parts[4].get_content_type(), 'text/plain') + self.assertEqual(parts[2]['Content-ID'], None) + self.assertEqual(parts[3]['Content-ID'].strip('<>'), attachments[0].uri()[4:]) + self.assertEqual(parts[4]['Content-ID'].strip('<>'), attachments[1].uri()[4:]) + def test_019_as_string_itip(self): self.event.set_summary("test") self.event.set_start(datetime.datetime(2014, 05, 23, 11, 00, 00, tzinfo=pytz.timezone("Europe/London"))) @@ -348,6 +373,44 @@ END:VCALENDAR self.assertIsInstance(event.get_start(), datetime.datetime) self.assertEqual(str(event.get_start()), "2014-08-13 10:00:00+00:00") + def test_023_load_from_message(self): + event = event_from_message(event_from_ical(ical_event).to_message()) + event.set_sequence(3) + + message = event.to_message() + self.assertTrue(message.is_multipart()) + + # check attachment MIME parts are kept + parts = [p for p in message.walk()] + attachments = event.get_attachments(); + + self.assertEqual(len(parts), 5) + self.assertEqual(parts[3].get_content_type(), 'image/png') + self.assertEqual(parts[3]['Content-ID'].strip('<>'), attachments[0].uri()[4:]) + self.assertEqual(parts[4].get_content_type(), 'text/plain') + self.assertEqual(parts[4]['Content-ID'].strip('<>'), attachments[1].uri()[4:]) + + def test_024_bogus_itip_data(self): + # DTSTAMP contains an invalid date/time value + vevent = """BEGIN:VEVENT +UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271 +DTSTAMP:20120713T1254140 +DTSTART;TZID=Europe/London:20120713T100000 +DTEND;TZID=Europe/London:20120713T110000 +SUMMARY:test +DESCRIPTION:test +ORGANIZER;CN="Doe, John":mailto:john.doe@example.org +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailt + o:jane.doe@example.org +ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailt + o:user.external@example.com +SEQUENCE:1 +TRANSP:OPAQUE +END:VEVENT +""" + event = event_from_ical(vevent) + self.assertRaises(EventIntegrityError, event.to_message) + if __name__ == '__main__': unittest.main() diff --git a/tests/unit/test-012-wallace_invitationpolicy.py b/tests/unit/test-012-wallace_invitationpolicy.py index 75939d0..650879b 100644 --- a/tests/unit/test-012-wallace_invitationpolicy.py +++ b/tests/unit/test-012-wallace_invitationpolicy.py @@ -44,7 +44,7 @@ CALSCALE:GREGORIAN METHOD:REQUEST BEGIN:VEVENT UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271 -DTSTAMP:20120713T1254140 +DTSTAMP:20120713T125414Z DTSTART;TZID=3DEurope/London:20120713T100000 DTEND;TZID=3DEurope/London:20120713T110000 SUMMARY:test |