summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pykolab/xml/__init__.py1
-rw-r--r--pykolab/xml/event.py57
-rw-r--r--tests/unit/test-003-event.py161
-rw-r--r--tests/unit/test-012-wallace_invitationpolicy.py2
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