summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorPaul Boddie <paul@boddie.org.uk>2014-08-06 17:48:26 +0200
committerPaul Boddie <paul@boddie.org.uk>2014-08-06 17:48:26 +0200
commitfee17e6f7a5fa995f7d77c7822e3ed1f2f8bffb5 (patch)
treeb401d3b6c748c09895ec4f0a887f3f2f0cdb6e0d /tests
parente0e89b980b8671eabf682cd83efc603775048228 (diff)
parenta8555e3e8789fd02b7d5749ea4fc51b84e57285f (diff)
downloadpykolab-dev/boddie.tar.gz
Merge branch 'master' of git://git.kolab.org/git/pykolab into dev/boddiedev/boddie
Conflicts: conf/kolab.conf po/POTFILES.in pykolab/auth/ldap/__init__.py pykolab/auth/ldap/cache.py pykolab/cli/cmd_rename_mailbox.py pykolab/imap/__init__.py pykolab/imap/cyrus.py pykolab/wap_client/__init__.py pykolab/xml/event.py
Diffstat (limited to 'tests')
-rw-r--r--tests/functional/resource_func.py4
-rw-r--r--tests/functional/test_wallace/test_005_resource_add.py17
-rw-r--r--tests/functional/test_wallace/test_005_resource_invitation.py265
-rw-r--r--tests/functional/test_wallace/test_007_invitationpolicy.py737
-rw-r--r--tests/functional/user_add.py4
-rw-r--r--tests/unit/test-002-attendee.py27
-rw-r--r--tests/unit/test-003-event.py391
-rw-r--r--tests/unit/test-011-itip.py412
-rw-r--r--tests/unit/test-011-wallace_resources.py208
-rw-r--r--tests/unit/test-012-wallace_invitationpolicy.py161
-rw-r--r--tests/unit/test-015-translate.py23
11 files changed, 1976 insertions, 273 deletions
diff --git a/tests/functional/resource_func.py b/tests/functional/resource_func.py
index 43aca96..ac80360 100644
--- a/tests/functional/resource_func.py
+++ b/tests/functional/resource_func.py
@@ -4,7 +4,7 @@ from pykolab import wap_client
conf = pykolab.getConf()
-def resource_add(type, cn, members=None, owner=None):
+def resource_add(type, cn, members=None, owner=None, **kw):
if type == None or type == '':
raise Exception
@@ -18,6 +18,8 @@ def resource_add(type, cn, members=None, owner=None):
'owner': owner
}
+ resource_details.update(kw)
+
result = wap_client.authenticate(conf.get('ldap', 'bind_dn'), conf.get('ldap', 'bind_pw'), conf.get('kolab', 'primary_domain'))
type_id = 0
diff --git a/tests/functional/test_wallace/test_005_resource_add.py b/tests/functional/test_wallace/test_005_resource_add.py
index 2de60fb..fc7f3ed 100644
--- a/tests/functional/test_wallace/test_005_resource_add.py
+++ b/tests/functional/test_wallace/test_005_resource_add.py
@@ -29,8 +29,8 @@ class TestResourceAdd(unittest.TestCase):
funcs.purge_resources()
self.audi = funcs.resource_add("car", "Audi A4")
self.passat = funcs.resource_add("car", "VW Passat")
- self.boxter = funcs.resource_add("car", "Porsche Boxter S")
- self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ])
+ self.boxter = funcs.resource_add("car", "Porsche Boxter S", kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY')
+ self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ], kolabinvitationpolicy='ACT_ACCEPT')
from tests.functional.synchronize import synchronize_once
synchronize_once()
@@ -56,3 +56,16 @@ class TestResourceAdd(unittest.TestCase):
attrs = auth.get_entry_attributes(None, self.cars['dn'], ['*'])
self.assertIn('groupofuniquenames', attrs['objectclass'])
self.assertEqual(len(attrs['uniquemember']), 3)
+ self.assertEqual(attrs['kolabinvitationpolicy'], 'ACT_ACCEPT')
+
+ def test_003_get_resource_records(self):
+ resource_dns = module_resources.resource_record_from_email_address(self.cars['mail'])
+ self.assertEqual(resource_dns[0], self.cars['dn'])
+
+ resources = module_resources.get_resource_records(resource_dns)
+ self.assertEqual(len(resources), 4)
+
+ # check for (inherited) kolabinvitationpolicy values (bitmasks)
+ self.assertEqual(resources[self.cars['dn']]['kolabinvitationpolicy'], [module_resources.ACT_ACCEPT])
+ self.assertEqual(resources[self.audi['dn']]['kolabinvitationpolicy'], [module_resources.ACT_ACCEPT])
+ self.assertEqual(resources[self.boxter['dn']]['kolabinvitationpolicy'], [module_resources.ACT_ACCEPT_AND_NOTIFY])
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index 946cb5f..61b9402 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -8,6 +8,10 @@ import uuid
from pykolab.imap import IMAP
from wallace import module_resources
+from pykolab.translate import _
+from pykolab.xml import event_from_message
+from pykolab.xml import participant_status_label
+from pykolab.itip import events_from_message
from email import message_from_string
from twisted.trial import unittest
@@ -23,13 +27,14 @@ CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VEVENT
UID:%s
-DTSTAMP:20140213T1254140
+DTSTAMP:20140213T125414Z
DTSTART;TZID=Europe/London:%s
DTEND;TZID=Europe/London:%s
SUMMARY:test
DESCRIPTION:test
ORGANIZER;CN="Doe, John":mailto:john.doe@example.org
ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:%s
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Somebody Else:mailto:somebody@else.com
TRANSP:OPAQUE
END:VEVENT
END:VCALENDAR
@@ -43,7 +48,7 @@ CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VEVENT
UID:%s
-DTSTAMP:20140215T1254140
+DTSTAMP:20140215T125414Z
DTSTART;TZID=Europe/London:%s
DTEND;TZID=Europe/London:%s
SEQUENCE:2
@@ -90,7 +95,7 @@ CALSCALE:GREGORIAN
METHOD:CANCEL
BEGIN:VEVENT
UID:%s
-DTSTAMP:20140218T1254140
+DTSTAMP:20140218T125414Z
DTSTART;TZID=Europe/London:20120713T100000
DTEND;TZID=Europe/London:20120713T110000
SUMMARY:test
@@ -112,7 +117,7 @@ CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VEVENT
UID:%s
-DTSTAMP:20140213T1254140
+DTSTAMP:20140213T125414Z
DTSTART;VALUE=DATE:%s
DTEND;VALUE=DATE:%s
SUMMARY:test
@@ -133,7 +138,7 @@ CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VEVENT
UID:%s
-DTSTAMP:20140213T1254140
+DTSTAMP:20140213T125414Z
DTSTART;TZID=Europe/Zurich:%s
DTEND;TZID=Europe/Zurich:%s
RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10
@@ -184,6 +189,8 @@ class TestResourceInvitation(unittest.TestCase):
@classmethod
def setup_class(self, *args, **kw):
+ self.itip_reply_subject = _("Reservation Request for %(summary)s was %(status)s")
+
from tests.functional.purge_users import purge_users
purge_users()
@@ -213,9 +220,12 @@ class TestResourceInvitation(unittest.TestCase):
self.boxter = funcs.resource_add("car", "Porsche Boxter S")
self.cars = funcs.resource_add("collection", "Company Cars", [ self.audi['dn'], self.passat['dn'], self.boxter['dn'] ])
- self.room1 = funcs.resource_add("confroom", "Room 101", owner=self.jane['dn'])
+ self.room1 = funcs.resource_add("confroom", "Room 101", owner=self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY')
self.room2 = funcs.resource_add("confroom", "Conference Room B-222")
- self.rooms = funcs.resource_add("collection", "Rooms", [ self.room1['dn'], self.room2['dn'] ], self.jane['dn'])
+ self.rooms = funcs.resource_add("collection", "Rooms", [ self.room1['dn'], self.room2['dn'] ], self.jane['dn'], kolabinvitationpolicy='ACT_ACCEPT_AND_NOTIFY')
+
+ self.room3 = funcs.resource_add("confroom", "CEOs Office 303")
+ self.viprooms = funcs.resource_add("collection", "VIP Rooms", [ self.room3['dn'] ], self.jane['dn'], kolabinvitationpolicy='ACT_MANUAL')
time.sleep(1)
from tests.functional.synchronize import synchronize_once
@@ -227,12 +237,14 @@ class TestResourceInvitation(unittest.TestCase):
smtp = smtplib.SMTP('localhost', 10026)
smtp.sendmail(from_addr, to_addr, mime_message % (to_addr, itip_payload))
+ smtp.quit()
- def send_itip_invitation(self, resource_email, start=None, allday=False, template=None):
+ def send_itip_invitation(self, resource_email, start=None, allday=False, template=None, uid=None):
if start is None:
start = datetime.datetime.now()
- uid = str(uuid.uuid4())
+ if uid is None:
+ uid = str(uuid.uuid4())
if allday:
default_template = itip_allday
@@ -328,17 +340,14 @@ class TestResourceInvitation(unittest.TestCase):
if uid and event_message['subject'] != uid:
continue
- for part in event_message.walk():
- if part.get_content_type() == "application/calendar+xml":
- payload = part.get_payload(decode=True)
- found = pykolab.xml.event_from_string(payload)
- break
-
+ found = event_from_message(event_message)
if found:
break
time.sleep(1)
+ imap.disconnect()
+
return found
def purge_mailbox(self, mailbox):
@@ -357,12 +366,10 @@ class TestResourceInvitation(unittest.TestCase):
def find_resource_by_email(self, email):
resource = None
- if (email.find(self.audi['mail']) >= 0):
- resource = self.audi
- if (email.find(self.passat['mail']) >= 0):
- resource = self.passat
- if (email.find(self.boxter['mail']) >= 0):
- resource = self.boxter
+ for r in [self.audi, self.passat, self.boxter, self.room1, self.room2]:
+ if (email.find(r['mail']) >= 0):
+ resource = r
+ break
return resource
@@ -379,7 +386,7 @@ class TestResourceInvitation(unittest.TestCase):
def test_002_invite_resource(self):
uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,7,13, 10,0,0))
- response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid)
@@ -387,10 +394,11 @@ class TestResourceInvitation(unittest.TestCase):
self.assertEqual(event.get_summary(), "test")
+ # @depends test_002_invite_resource
def test_003_invite_resource_conflict(self):
uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,7,13, 12,0,0))
- response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
self.assertEqual(self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid), None)
@@ -402,7 +410,7 @@ class TestResourceInvitation(unittest.TestCase):
uid = self.send_itip_invitation(self.cars['mail'], datetime.datetime(2014,7,13, 12,0,0))
# one of the collection members accepted the reservation
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
delegatee = self.find_resource_by_email(accept['from'])
@@ -412,7 +420,7 @@ class TestResourceInvitation(unittest.TestCase):
self.assertIsInstance(self.check_resource_calendar_event(delegatee['kolabtargetfolder'], uid), pykolab.xml.Event)
# resource collection responds with a DELEGATED message
- response = self.check_message_received("Reservation Request for test was DELEGATED", self.cars['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DELEGATED') }, self.cars['mail'])
self.assertIsInstance(response, email.message.Message)
self.assertIn("ROLE=NON-PARTICIPANT;RSVP=FALSE", str(response))
@@ -422,13 +430,13 @@ class TestResourceInvitation(unittest.TestCase):
uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,4,1, 10,0,0))
- response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
self.purge_mailbox(self.john['mailbox'])
self.send_itip_update(self.audi['mail'], uid, datetime.datetime(2014,4,1, 12,0,0)) # conflict with myself
- response = self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid)
@@ -443,13 +451,13 @@ class TestResourceInvitation(unittest.TestCase):
uid = self.send_itip_invitation(self.cars['mail'], datetime.datetime(2014,4,24, 12,0,0))
# one of the collection members accepted the reservation
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
delegatee = self.find_resource_by_email(accept['from'])
# book that resource for the next day
self.send_itip_invitation(delegatee['mail'], datetime.datetime(2014,4,25, 14,0,0))
- accept2 = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept2 = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
# re-schedule first booking to a conflicting date
self.purge_mailbox(self.john['mailbox'])
@@ -457,7 +465,7 @@ class TestResourceInvitation(unittest.TestCase):
self.send_itip_update(delegatee['mail'], uid, datetime.datetime(2014,4,25, 12,0,0), template=update_template)
# expect response from another member of the initially delegated collection
- new_accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ new_accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(new_accept, email.message.Message)
new_delegatee = self.find_resource_by_email(new_accept['from'])
@@ -468,7 +476,7 @@ class TestResourceInvitation(unittest.TestCase):
self.assertIsInstance(event, pykolab.xml.Event)
# old resource responds with a DELEGATED message
- response = self.check_message_received("Reservation Request for test was DELEGATED", delegatee['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DELEGATED') }, delegatee['mail'])
self.assertIsInstance(response, email.message.Message)
# old reservation was removed from old delegate's calendar
@@ -489,7 +497,7 @@ class TestResourceInvitation(unittest.TestCase):
# make new reservation to the now free'd slot
self.send_itip_invitation(self.boxter['mail'], datetime.datetime(2014,5,1, 9,0,0))
- response = self.check_message_received("Reservation Request for test was ACCEPTED", self.boxter['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.boxter['mail'])
self.assertIsInstance(response, email.message.Message)
@@ -500,7 +508,7 @@ class TestResourceInvitation(unittest.TestCase):
uid = self.send_itip_invitation(self.cars['mail'], dt)
# wait for accept notification
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
delegatee = self.find_resource_by_email(accept['from'])
@@ -511,12 +519,12 @@ class TestResourceInvitation(unittest.TestCase):
self.send_itip_update(delegatee['mail'], uid, dt, template=update_template)
# get response from delegatee
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
self.assertIn(delegatee['mail'], accept['from'])
# no delegation response on updates
- self.assertEqual(self.check_message_received("Reservation Request for test was DELEGATED", self.cars['mail']), None)
+ self.assertEqual(self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DELEGATED') }, self.cars['mail']), None)
def test_008_allday_reservation(self):
@@ -524,7 +532,7 @@ class TestResourceInvitation(unittest.TestCase):
uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,6,2), True)
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid)
@@ -532,7 +540,7 @@ class TestResourceInvitation(unittest.TestCase):
self.assertIsInstance(event.get_start(), datetime.date)
uid2 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,6,2, 16,0,0))
- response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
@@ -543,19 +551,19 @@ class TestResourceInvitation(unittest.TestCase):
uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,2,20, 12,0,0),
template=itip_recurring.replace(";COUNT=10", ""))
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
# check non-recurring against recurring
uid2 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,3,13, 10,0,0))
- response = self.check_message_received("Reservation Request for test was DECLINED", self.audi['mail'])
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.audi['mail'])
self.assertIsInstance(response, email.message.Message)
self.purge_mailbox(self.john['mailbox'])
# check recurring against recurring
uid3 = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,2,22, 8,0,0), template=itip_recurring)
- accept = self.check_message_received("Reservation Request for test was ACCEPTED")
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
self.assertIsInstance(accept, email.message.Message)
@@ -570,7 +578,7 @@ class TestResourceInvitation(unittest.TestCase):
itip_invalid = itip_invitation.replace("DTSTART;", "X-DTSTART;")
self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,3,24, 19,30,0), template=itip_invalid)
- self.assertEqual(self.check_message_received("Reservation Request for test was ACCEPTED", self.audi['mail']), None)
+ self.assertEqual(self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.audi['mail']), None)
def test_011_owner_info(self):
@@ -578,7 +586,7 @@ class TestResourceInvitation(unittest.TestCase):
self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,6,19, 16,0,0))
- accept = self.check_message_received("Reservation Request for test was ACCEPTED", self.room1['mail'])
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room1['mail'])
self.assertIsInstance(accept, email.message.Message)
respose_text = str(accept.get_payload(0))
self.assertIn(self.jane['mail'], respose_text)
@@ -590,27 +598,184 @@ class TestResourceInvitation(unittest.TestCase):
self.send_itip_invitation(self.room2['mail'], datetime.datetime(2014,6,19, 16,0,0))
- accept = self.check_message_received("Reservation Request for test was ACCEPTED", self.room2['mail'])
+ accept = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room2['mail'])
self.assertIsInstance(accept, email.message.Message)
respose_text = str(accept.get_payload(0))
self.assertIn(self.jane['mail'], respose_text)
self.assertIn(self.jane['displayname'], respose_text)
- def TODO_test_012_owner_notification(self):
+ def test_012_owner_notification(self):
self.purge_mailbox(self.john['mailbox'])
self.purge_mailbox(self.jane['mailbox'])
- self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,5,4, 13,0,0))
+ self.send_itip_invitation(self.room1['mail'], datetime.datetime(2014,8,4, 13,0,0))
# check notification message sent to resource owner (jane)
- notify = self.check_message_received("Reservation Request for test was ACCEPTED", self.room1['mail'], self.jane['mailbox'])
+ notify = self.check_message_received(_('Booking for %s has been %s') % (self.room1['cn'], participant_status_label('ACCEPTED')), self.room1['mail'], self.jane['mailbox'])
self.assertIsInstance(notify, email.message.Message)
- self.assertEqual(notify['From'], self.room1['mail'])
- self.assertEqual(notify['Cc'], self.jane['mail'])
+
+ notification_text = str(notify.get_payload())
+ self.assertIn(self.john['mail'], notification_text)
+ self.assertIn(participant_status_label('ACCEPTED'), notification_text)
+
+ self.purge_mailbox(self.john['mailbox'])
# check notification sent to collection owner (jane)
- self.send_itip_invitation(self.rooms['mail'], datetime.datetime(2014,5,4, 12,30,0))
+ self.send_itip_invitation(self.rooms['mail'], datetime.datetime(2014,8,4, 12,30,0))
+
+ # one of the collection members accepted the reservation
+ accepted = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') })
+ delegatee = self.find_resource_by_email(accepted['from'])
+
+ notify = self.check_message_received(_('Booking for %s has been %s') % (delegatee['cn'], participant_status_label('ACCEPTED')), delegatee['mail'], self.jane['mailbox'])
+ self.assertIsInstance(notify, email.message.Message)
+ self.assertIn(self.john['mail'], notification_text)
+
+
+ def test_013_owner_confirmation_accept(self):
+ self.purge_mailbox(self.john['mailbox'])
+ self.purge_mailbox(self.jane['mailbox'])
+
+ uid = self.send_itip_invitation(self.room3['mail'], datetime.datetime(2014,9,12, 14,0,0))
+
+ # requester (john) gets a TENTATIVE confirmation
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.room3['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_summary(), "test")
+ self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'TENTATIVE')
+
+ # check confirmation message sent to resource owner (jane)
+ notify = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox'])
+ self.assertIsInstance(notify, email.message.Message)
+
+ itip_event = events_from_message(notify)[0]
+
+ # resource owner confirms reservation request
+ itip_reply = itip_event['xml'].to_message_itip(self.jane['mail'],
+ method="REPLY",
+ participant_status='ACCEPTED',
+ message_text="Request accepted",
+ subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('ACCEPTED'))
+ )
+
+ smtp = smtplib.SMTP('localhost', 10026)
+ smtp.sendmail(self.jane['mail'], str(itip_event['organizer']), str(itip_reply))
+ smtp.quit()
+
+ # requester (john) now gets the ACCEPTED response
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room3['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_status(True), 'CONFIRMED')
+ self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'ACCEPTED')
+
- notify = self.check_message_received("Reservation Request for test was ACCEPTED", self.room2['mail'], self.jane['mailbox'])
+ def test_014_owner_confirmation_decline(self):
+ self.purge_mailbox(self.john['mailbox'])
+ self.purge_mailbox(self.jane['mailbox'])
+
+ uid = self.send_itip_invitation(self.room3['mail'], datetime.datetime(2014,9,14, 9,0,0))
+
+ # requester (john) gets a TENTATIVE confirmation
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.room3['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ # check confirmation message sent to resource owner (jane)
+ notify = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox'])
self.assertIsInstance(notify, email.message.Message)
+
+ itip_event = events_from_message(notify)[0]
+
+ # resource owner declines reservation request
+ itip_reply = itip_event['xml'].to_message_itip(self.jane['mail'],
+ method="REPLY",
+ participant_status='DECLINED',
+ message_text="Request declined",
+ subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('DECLINED'))
+ )
+
+ smtp = smtplib.SMTP('localhost', 10026)
+ smtp.sendmail(self.jane['mail'], str(itip_event['organizer']), str(itip_reply))
+ smtp.quit()
+
+ # requester (john) now gets the DECLINED response
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.room3['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ # tentative reservation was set to cancelled
+ event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
+ self.assertEqual(event, None)
+ #self.assertEqual(event.get_status(True), 'CANCELLED')
+ #self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'DECLINED')
+
+
+ def test_015_owner_confirmation_update(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ uid = self.send_itip_invitation(self.room3['mail'], datetime.datetime(2014,8,19, 9,0,0), uid="http://a-totally.stupid/?uid")
+
+ # requester (john) gets a TENTATIVE confirmation
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.room3['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ # check first confirmation message sent to resource owner (jane)
+ notify1 = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox'])
+ self.assertIsInstance(notify1, email.message.Message)
+
+ itip_event1 = events_from_message(notify1)[0]
+ self.assertEqual(itip_event1['start'].hour, 9)
+
+ self.purge_mailbox(self.jane['mailbox'])
+ self.purge_mailbox(self.john['mailbox'])
+
+ # send update with new date (and sequence)
+ self.send_itip_update(self.room3['mail'], uid, datetime.datetime(2014,8,19, 16,0,0))
+
+ event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'TENTATIVE')
+
+ # check second confirmation message sent to resource owner (jane)
+ notify2 = self.check_message_received(_('Booking request for %s requires confirmation') % (self.room3['cn']), mailbox=self.jane['mailbox'])
+ self.assertIsInstance(notify2, email.message.Message)
+
+ itip_event2 = events_from_message(notify2)[0]
+ self.assertEqual(itip_event2['start'].hour, 16)
+
+ # resource owner declines the first reservation request
+ itip_reply = itip_event1['xml'].to_message_itip(self.jane['mail'],
+ method="REPLY",
+ participant_status='DECLINED',
+ message_text="Request declined",
+ subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('DECLINED'))
+ )
+ smtp = smtplib.SMTP('localhost', 10026)
+ smtp.sendmail(self.jane['mail'], str(itip_event1['organizer']), str(itip_reply))
+ smtp.quit()
+
+ time.sleep(5)
+
+ # resource owner accpets the second reservation request
+ itip_reply = itip_event2['xml'].to_message_itip(self.jane['mail'],
+ method="REPLY",
+ participant_status='ACCEPTED',
+ message_text="Request accepred",
+ subject=_('Booking for %s has been %s') % (self.room3['cn'], participant_status_label('ACCEPTED'))
+ )
+ smtp = smtplib.SMTP('localhost', 10026)
+ smtp.sendmail(self.jane['mail'], str(itip_event2['organizer']), str(itip_reply))
+ smtp.quit()
+
+ # requester (john) now gets the ACCEPTED response
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.room3['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ event = self.check_resource_calendar_event(self.room3['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_attendee_by_email(self.room3['mail']).get_participant_status(True), 'ACCEPTED')
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
new file mode 100644
index 0000000..9bc808f
--- /dev/null
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -0,0 +1,737 @@
+import time
+import pykolab
+import smtplib
+import email
+import datetime
+import pytz
+import uuid
+import kolabformat
+
+from pykolab.imap import IMAP
+from wallace import module_resources
+
+from pykolab.translate import _
+from pykolab.xml import event_from_message
+from pykolab.xml import participant_status_label
+from email import message_from_string
+from twisted.trial import unittest
+
+import tests.functional.resource_func as funcs
+
+conf = pykolab.getConf()
+
+itip_invitation = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:%(uid)s
+DTSTAMP:20140213T125414Z
+DTSTART;TZID=Europe/Berlin:%(start)s
+DTEND;TZID=Europe/Berlin:%(end)s
+SUMMARY:%(summary)s
+DESCRIPTION:test
+ORGANIZER;CN="Doe, John":mailto:john.doe@example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=%(partstat)s;RSVP=TRUE:mailto:%(mailto)s
+ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=TENTATIVE;RSVP=FALSE:mailto:somebody@else.com
+TRANSP:OPAQUE
+SEQUENCE:%(sequence)d
+END:VEVENT
+END:VCALENDAR
+"""
+
+itip_cancellation = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:CANCEL
+BEGIN:VEVENT
+UID:%(uid)s
+DTSTAMP:20140218T125414Z
+DTSTART;TZID=Europe/Berlin:20120713T100000
+DTEND;TZID=Europe/Berlin:20120713T110000
+SUMMARY:%(summary)s
+DESCRIPTION:test
+ORGANIZER;CN="Doe, John":mailto:john.doe@example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE:mailto:%(mailto)s
+TRANSP:OPAQUE
+SEQUENCE:%(sequence)d
+END:VEVENT
+END:VCALENDAR
+"""
+
+itip_recurring = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:%(uid)s
+DTSTAMP:20140213T125414Z
+DTSTART;TZID=Europe/Zurich:%(start)s
+DTEND;TZID=Europe/Zurich:%(end)s
+RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=10
+SUMMARY:%(summary)s
+DESCRIPTION:test
+ORGANIZER;CN="Doe, John":mailto:john.doe@example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=%(partstat)s;RSVP=TRUE:mailto:%(mailto)s
+TRANSP:OPAQUE
+SEQUENCE:%(sequence)d
+END:VEVENT
+END:VCALENDAR
+"""
+
+itip_reply = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//pykolab-0.6.9-1//kolab.org//
+CALSCALE:GREGORIAN
+METHOD:REPLY
+BEGIN:VEVENT
+SUMMARY:%(summary)s
+UID:%(uid)s
+DTSTART;TZID=Europe/Berlin;VALUE=DATE-TIME:%(start)s
+DTEND;TZID=Europe/Berlin;VALUE=DATE-TIME:%(end)s
+DTSTAMP;VALUE=DATE-TIME:20140706T171038Z
+ORGANIZER;CN="Doe, John":MAILTO:%(organizer)s
+ATTENDEE;CUTYPE=INDIVIDUAL;PARTSTAT=%(partstat)s;ROLE=REQ-PARTICIPANT:mailto:%(mailto)s
+PRIORITY:0
+SEQUENCE:%(sequence)d
+END:VEVENT
+END:VCALENDAR
+"""
+
+mime_message = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_c8894dbdb8baeedacae836230e3436fd"
+From: "Doe, John" <john.doe@example.org>
+Date: Tue, 25 Feb 2014 13:54:14 +0100
+Message-ID: <240fe7ae7e139129e9eb95213c1016d7@example.org>
+To: %s
+Subject: "test"
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+*test*
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/calendar; charset=UTF-8; method=%s; name=event.ics
+Content-Disposition: attachment; filename=event.ics
+Content-Transfer-Encoding: 8bit
+
+%s
+--=_c8894dbdb8baeedacae836230e3436fd--
+"""
+
+class TestWallaceInvitationpolicy(unittest.TestCase):
+
+ john = None
+ itip_reply_subject = None
+
+ @classmethod
+ def setUp(self):
+ """ Compatibility for twisted.trial.unittest
+ """
+ if not self.john:
+ self.setup_class()
+
+ @classmethod
+ def setup_class(self, *args, **kw):
+ self.itip_reply_subject = _('"%(summary)s" has been %(status)s')
+
+ from tests.functional.purge_users import purge_users
+ purge_users()
+
+ self.john = {
+ 'displayname': 'John Doe',
+ 'mail': 'john.doe@example.org',
+ 'dn': 'uid=doe,ou=People,dc=example,dc=org',
+ 'preferredlanguage': 'en_US',
+ 'mailbox': 'user/john.doe@example.org',
+ 'kolabtargetfolder': 'user/john.doe/Calendar@example.org',
+ 'kolabinvitationpolicy': ['ACT_UPDATE_AND_NOTIFY','ACT_MANUAL']
+ }
+
+ self.jane = {
+ 'displayname': 'Jane Manager',
+ 'mail': 'jane.manager@example.org',
+ 'dn': 'uid=manager,ou=People,dc=example,dc=org',
+ 'preferredlanguage': 'en_US',
+ 'mailbox': 'user/jane.manager@example.org',
+ 'kolabtargetfolder': 'user/jane.manager/Calendar@example.org',
+ 'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT','ACT_UPDATE']
+ }
+
+ self.jack = {
+ 'displayname': 'Jack Tentative',
+ 'mail': 'jack.tentative@example.org',
+ 'dn': 'uid=tentative,ou=People,dc=example,dc=org',
+ 'preferredlanguage': 'en_US',
+ 'mailbox': 'user/jack.tentative@example.org',
+ 'kolabtargetfolder': 'user/jack.tentative/Calendar@example.org',
+ 'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ACT_SAVE_TO_CALENDAR','ACT_UPDATE']
+ }
+
+ self.mark = {
+ 'displayname': 'Mark German',
+ 'mail': 'mark.german@example.org',
+ 'dn': 'uid=german,ou=People,dc=example,dc=org',
+ 'preferredlanguage': 'de_DE',
+ 'mailbox': 'user/mark.german@example.org',
+ 'kolabtargetfolder': 'user/mark.german/Calendar@example.org',
+ 'kolabinvitationpolicy': ['ACT_ACCEPT','ACT_UPDATE_AND_NOTIFY']
+ }
+
+ self.external = {
+ 'displayname': 'Bob External',
+ 'mail': 'bob.external@gmail.com'
+ }
+
+ from tests.functional.user_add import user_add
+ user_add("John", "Doe", kolabinvitationpolicy=self.john['kolabinvitationpolicy'], preferredlanguage=self.john['preferredlanguage'])
+ user_add("Jane", "Manager", kolabinvitationpolicy=self.jane['kolabinvitationpolicy'], preferredlanguage=self.jane['preferredlanguage'])
+ user_add("Jack", "Tentative", kolabinvitationpolicy=self.jack['kolabinvitationpolicy'], preferredlanguage=self.jack['preferredlanguage'])
+ user_add("Mark", "German", kolabinvitationpolicy=self.mark['kolabinvitationpolicy'], preferredlanguage=self.mark['preferredlanguage'])
+
+ time.sleep(1)
+ from tests.functional.synchronize import synchronize_once
+ synchronize_once()
+
+ def send_message(self, itip_payload, to_addr, from_addr=None, method="REQUEST"):
+ if from_addr is None:
+ from_addr = self.john['mail']
+
+ smtp = smtplib.SMTP('localhost', 10026)
+ smtp.sendmail(from_addr, to_addr, mime_message % (to_addr, method, itip_payload))
+
+ def send_itip_invitation(self, attendee_email, start=None, allday=False, template=None, summary="test", sequence=0, partstat='NEEDS-ACTION'):
+ if start is None:
+ start = datetime.datetime.now()
+
+ uid = str(uuid.uuid4())
+
+ if allday:
+ default_template = itip_allday
+ end = start + datetime.timedelta(days=1)
+ date_format = '%Y%m%d'
+ else:
+ end = start + datetime.timedelta(hours=4)
+ default_template = itip_invitation
+ date_format = '%Y%m%dT%H%M%S'
+
+ self.send_message((template if template is not None else default_template) % {
+ 'uid': uid,
+ 'start': start.strftime(date_format),
+ 'end': end.strftime(date_format),
+ 'mailto': attendee_email,
+ 'summary': summary,
+ 'sequence': sequence,
+ 'partstat': partstat
+ },
+ attendee_email)
+
+ return uid
+
+ def send_itip_update(self, attendee_email, uid, start=None, template=None, summary="test", sequence=1, partstat='ACCEPTED'):
+ if start is None:
+ start = datetime.datetime.now()
+
+ end = start + datetime.timedelta(hours=4)
+ self.send_message((template if template is not None else itip_invitation) % {
+ 'uid': uid,
+ 'start': start.strftime('%Y%m%dT%H%M%S'),
+ 'end': end.strftime('%Y%m%dT%H%M%S'),
+ 'mailto': attendee_email,
+ 'summary': summary,
+ 'sequence': sequence,
+ 'partstat': partstat
+ },
+ attendee_email)
+
+ return uid
+
+ def send_itip_reply(self, uid, attendee_email, mailto, start=None, template=None, summary="test", sequence=0, partstat='ACCEPTED'):
+ if start is None:
+ start = datetime.datetime.now()
+
+ end = start + datetime.timedelta(hours=4)
+ self.send_message((template if template is not None else itip_reply) % {
+ 'uid': uid,
+ 'start': start.strftime('%Y%m%dT%H%M%S'),
+ 'end': end.strftime('%Y%m%dT%H%M%S'),
+ 'mailto': attendee_email,
+ 'organizer': mailto,
+ 'summary': summary,
+ 'sequence': sequence,
+ 'partstat': partstat
+ },
+ mailto,
+ attendee_email,
+ method='REPLY')
+
+ return uid
+
+ def send_itip_cancel(self, attendee_email, uid, summary="test", sequence=1):
+ self.send_message(itip_cancellation % {
+ 'uid': uid,
+ 'mailto': attendee_email,
+ 'summary': summary,
+ 'sequence': sequence,
+ },
+ attendee_email,
+ method='CANCEL')
+
+ return uid
+
+ def create_calendar_event(self, start=None, summary="test", sequence=0, user=None, attendees=None):
+ if start is None:
+ start = datetime.datetime.now(pytz.timezone("Europe/Berlin"))
+ if user is None:
+ user = self.john
+ if attendees is None:
+ attendees = [self.jane]
+
+ end = start + datetime.timedelta(hours=4)
+
+ event = pykolab.xml.Event()
+ event.set_start(start)
+ event.set_end(end)
+ event.set_organizer(user['mail'], user['displayname'])
+
+ for attendee in attendees:
+ event.add_attendee(attendee['mail'], attendee['displayname'], role="REQ-PARTICIPANT", participant_status="NEEDS-ACTION", rsvp=True)
+
+ event.set_summary(summary)
+ event.set_sequence(sequence)
+
+ # create event with attachment
+ vattach = event.get_attachments()
+ attachment = kolabformat.Attachment()
+ attachment.setLabel('attach.txt')
+ attachment.setData('This is a text attachment', 'text/plain')
+ vattach.append(attachment)
+ event.event.setAttachments(vattach)
+
+ imap = IMAP()
+ imap.connect()
+
+ mailbox = imap.folder_quote(user['kolabtargetfolder'])
+ imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
+ imap.imap.m.select(mailbox)
+
+ result = imap.imap.m.append(
+ mailbox,
+ None,
+ None,
+ event.to_message().as_string()
+ )
+
+ return event.get_uid()
+
+ def update_calendar_event(self, uid, start=None, summary=None, sequence=0, user=None):
+ if user is None:
+ user = self.john
+
+ event = self.check_user_calendar_event(user['kolabtargetfolder'], uid)
+ if event:
+ if start is not None:
+ event.set_start(start)
+ if summary is not None:
+ event.set_summary(summary)
+ if sequence is not None:
+ event.set_sequence(sequence)
+
+ imap = IMAP()
+ imap.connect()
+
+ mailbox = imap.folder_quote(user['kolabtargetfolder'])
+ imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
+ imap.imap.m.select(mailbox)
+
+ return imap.imap.m.append(
+ mailbox,
+ None,
+ None,
+ event.to_message().as_string()
+ )
+
+ return False
+
+ def check_message_received(self, subject, from_addr=None, mailbox=None):
+ if mailbox is None:
+ mailbox = self.john['mailbox']
+
+ imap = IMAP()
+ imap.connect()
+
+ mailbox = imap.folder_quote(mailbox)
+ imap.set_acl(mailbox, "cyrus-admin", "lrs")
+ imap.imap.m.select(mailbox)
+
+ found = None
+ retries = 15
+
+ while not found and retries > 0:
+ retries -= 1
+
+ typ, data = imap.imap.m.search(None, '(UNDELETED HEADER FROM "%s")' % (from_addr) if from_addr else 'UNDELETED')
+ for num in data[0].split():
+ typ, msg = imap.imap.m.fetch(num, '(RFC822)')
+ message = message_from_string(msg[0][1])
+ if message['Subject'] == subject:
+ found = message
+ break
+
+ time.sleep(1)
+
+ imap.disconnect()
+
+ return found
+
+ def check_user_calendar_event(self, mailbox, uid=None):
+ imap = IMAP()
+ imap.connect()
+
+ mailbox = imap.folder_quote(mailbox)
+ imap.set_acl(mailbox, "cyrus-admin", "lrs")
+ imap.imap.m.select(mailbox)
+
+ found = None
+ retries = 15
+
+ while not found and retries > 0:
+ retries -= 1
+
+ typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid) if uid else '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.event")')
+ for num in data[0].split():
+ typ, data = imap.imap.m.fetch(num, '(RFC822)')
+ event_message = message_from_string(data[0][1])
+
+ # return matching UID or first event found
+ if uid and event_message['subject'] != uid:
+ continue
+
+ found = event_from_message(event_message)
+ if found:
+ break
+
+ time.sleep(1)
+
+ return found
+
+ def purge_mailbox(self, mailbox):
+ imap = IMAP()
+ imap.connect()
+ mailbox = imap.folder_quote(mailbox)
+ imap.set_acl(mailbox, "cyrus-admin", "lrwcdest")
+ imap.imap.m.select(mailbox)
+
+ typ, data = imap.imap.m.search(None, 'ALL')
+ for num in data[0].split():
+ imap.imap.m.store(num, '+FLAGS', '\\Deleted')
+
+ imap.imap.m.expunge()
+ imap.disconnect()
+
+
+ def test_001_invite_accept_udate(self):
+ start = datetime.datetime(2014,8,13, 10,0,0)
+ uid = self.send_itip_invitation(self.jane['mail'], start)
+
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_summary(), "test")
+
+ # send update with the same sequence: no re-scheduling
+ self.send_itip_update(self.jane['mail'], uid, start, summary="test updated", sequence=0, partstat='ACCEPTED')
+
+ time.sleep(10)
+ event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_summary(), "test updated")
+ self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+
+
+ # @depends on test_001_invite_user
+ def test_002_invite_conflict_reject(self):
+ uid = self.send_itip_invitation(self.jane['mail'], datetime.datetime(2014,8,13, 11,0,0), summary="test2")
+
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':participant_status_label('DECLINED') }, self.jane['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_summary(), "test2")
+
+
+ def test_003_invite_accept_tentative(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ uid = self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,24, 8,0,0))
+
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.jack['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+
+ def test_004_copy_to_calendar(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,29, 8,0,0))
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.jack['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ # send conflicting request to jack
+ uid = self.send_itip_invitation(self.jack['mail'], datetime.datetime(2014,7,29, 10,0,0), summary="test2")
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test2', 'status':participant_status_label('DECLINED') }, self.jack['mail'])
+ self.assertEqual(response, None, "No reply expected")
+
+ event = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_summary(), "test2")
+ self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction)
+
+
+ def test_005_invite_rescheduling_accept(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ start = datetime.datetime(2014,8,14, 9,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+ uid = self.send_itip_invitation(self.jane['mail'], start)
+
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_summary(), "test")
+
+ self.purge_mailbox(self.john['mailbox'])
+
+ # send update with new date and incremented sequence
+ new_start = datetime.datetime(2014,8,15, 15,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+ self.send_itip_update(self.jane['mail'], uid, new_start, summary="test", sequence=1)
+
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_start(), new_start)
+ self.assertEqual(event.get_sequence(), 1)
+
+
+ def test_005_invite_rescheduling_reject(self):
+ self.purge_mailbox(self.john['mailbox'])
+ self.purge_mailbox(self.jack['kolabtargetfolder'])
+
+ start = datetime.datetime(2014,8,9, 17,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+ uid = self.send_itip_invitation(self.jack['mail'], start)
+
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('TENTATIVE') }, self.jack['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ # send update with new but conflicting date and incremented sequence
+ self.create_calendar_event(datetime.datetime(2014,8,10, 10,30,0, tzinfo=pytz.timezone("Europe/Berlin")), user=self.jack)
+ new_start = datetime.datetime(2014,8,10, 9,30,0, tzinfo=pytz.timezone("Europe/Berlin"))
+ self.send_itip_update(self.jack['mail'], uid, new_start, summary="test (updated)", sequence=1)
+
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'test', 'status':participant_status_label('DECLINED') }, self.jack['mail'])
+ self.assertEqual(response, None)
+
+ # verify re-scheduled copy in jack's calendar with NEEDS-ACTION
+ event = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_start(), new_start)
+ self.assertEqual(event.get_sequence(), 1)
+
+ attendee = event.get_attendee(self.jack['mail'])
+ self.assertTrue(attendee.get_rsvp())
+ self.assertEqual(attendee.get_participant_status(), kolabformat.PartNeedsAction)
+
+
+ def test_006_invitation_reply(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ start = datetime.datetime(2014,8,18, 14,30,0, tzinfo=pytz.timezone("Europe/Berlin"))
+ uid = self.create_calendar_event(start, user=self.john)
+
+ event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+
+ # send a reply from jane to john
+ self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start)
+
+ # check for the updated event in john's calendar
+ time.sleep(10)
+ event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+
+ attendee = event.get_attendee(self.jane['mail'])
+ self.assertIsInstance(attendee, pykolab.xml.Attendee)
+ self.assertEqual(attendee.get_participant_status(), kolabformat.PartAccepted)
+
+ # check attachments in update event
+ attachments = event.get_attachments()
+ self.assertEqual(len(attachments), 1)
+ self.assertEqual(event.get_attachment_data(0), 'This is a text attachment')
+
+
+ def test_007_invitation_cancel(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ uid = self.send_itip_invitation(self.jane['mail'], summary="cancelled")
+
+ response = self.check_message_received(self.itip_reply_subject % { 'summary':'cancelled', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
+ self.assertIsInstance(response, email.message.Message)
+
+ self.send_itip_cancel(self.jane['mail'], uid, summary="cancelled")
+
+ time.sleep(10)
+ event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_summary(), "cancelled")
+ self.assertEqual(event.get_status(True), 'CANCELLED')
+ self.assertTrue(event.get_transparency())
+
+
+ def test_008_inivtation_reply_notify(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ start = datetime.datetime(2014,8,12, 16,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+ uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.mark, self.jack])
+
+ # send a reply from jane to john
+ self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start)
+
+ # check for notification message
+ # this notification should be suppressed until mark has replied, too
+ notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
+ self.assertEqual(notification, None)
+
+ # send a reply from mark to john
+ self.send_itip_reply(uid, self.mark['mail'], self.john['mail'], start=start, partstat='ACCEPTED')
+
+ notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
+ self.assertIsInstance(notification, email.message.Message)
+
+ notification_text = str(notification.get_payload());
+ self.assertIn(self.jane['mail'], notification_text)
+ self.assertIn(_("PENDING"), notification_text)
+
+ self.purge_mailbox(self.john['mailbox'])
+
+ # send a reply from mark to john
+ self.send_itip_reply(uid, self.jack['mail'], self.john['mail'], start=start, partstat='ACCEPTED')
+
+ # this triggers an additional notification
+ notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail'])
+ self.assertIsInstance(notification, email.message.Message)
+
+ notification_text = str(notification.get_payload());
+ self.assertNotIn(_("PENDING"), notification_text)
+
+
+ def test_008_notify_translated(self):
+ self.purge_mailbox(self.mark['mailbox'])
+
+ start = datetime.datetime(2014,8,12, 16,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+ uid = self.create_calendar_event(start, user=self.mark, attendees=[self.jane])
+
+ # send a reply from jane to mark
+ self.send_itip_reply(uid, self.jane['mail'], self.mark['mail'], start=start)
+
+ # change translations to de_DE
+ pykolab.translate.setUserLanguage(self.mark['preferredlanguage'])
+ notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.mark['mail'], self.mark['mailbox'])
+ self.assertIsInstance(notification, email.message.Message)
+
+ notification_text = str(notification.get_payload());
+ self.assertIn(self.jane['mail'], notification_text)
+ self.assertIn(participant_status_label("ACCEPTED")+":", notification_text)
+
+ # reset localization
+ pykolab.translate.setUserLanguage(conf.get('kolab','default_locale'))
+
+
+ def test_009_outdated_reply(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ start = datetime.datetime(2014,9,2, 11,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+ uid = self.create_calendar_event(start, user=self.john, sequence=2)
+
+ # send a reply from jane to john
+ self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start, sequence=1)
+
+ # verify jane's attendee status was not updated
+ time.sleep(10)
+ event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_sequence(), 2)
+ self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartNeedsAction)
+
+
+ def test_010_partstat_update_propagation(self):
+ # ATTENTION: this test requires wallace.invitationpolicy_autoupdate_other_attendees_on_reply to be enabled in config
+
+ start = datetime.datetime(2014,8,21, 13,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+ uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.jack, self.external])
+
+ event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+
+ # send invitations to jack and jane
+ event_itip = event.as_string_itip()
+ self.send_itip_invitation(self.jane['mail'], start, template=event_itip)
+ self.send_itip_invitation(self.jack['mail'], start, template=event_itip)
+
+ # wait for replies from jack and jane to be processed and propagated
+ time.sleep(10)
+ event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+
+ # check updated event in organizer's calendar
+ self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+ self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
+
+ # check updated partstats in jane's calendar
+ janes = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+ self.assertEqual(janes.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+ self.assertEqual(janes.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
+
+ # check updated partstats in jack's calendar
+ jacks = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+ self.assertEqual(jacks.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+ self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
+
+ # PART 2: create conflicting event in jack's calendar
+ new_start = datetime.datetime(2014,8,21, 6,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+ self.create_calendar_event(new_start, user=self.jack, attendees=[], summary="blocker")
+
+ # re-schedule initial event to new date
+ self.update_calendar_event(uid, start=new_start, sequence=1, user=self.john)
+ self.send_itip_update(self.jane['mail'], uid, new_start, summary="test (updated)", sequence=1)
+ self.send_itip_update(self.jack['mail'], uid, new_start, summary="test (updated)", sequence=1)
+
+ # wait for replies to be processed and propagated
+ time.sleep(10)
+ event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+
+ # check updated event in organizer's calendar (jack didn't reply yet)
+ self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+ self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
+
+ # check partstats in jack's calendar: jack's status should remain needs-action
+ jacks = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+ self.assertEqual(jacks.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+ self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction)
+
+
diff --git a/tests/functional/user_add.py b/tests/functional/user_add.py
index 4939f93..b1b37f1 100644
--- a/tests/functional/user_add.py
+++ b/tests/functional/user_add.py
@@ -4,7 +4,7 @@ from pykolab import wap_client
conf = pykolab.getConf()
-def user_add(givenname, sn, preferredlanguage='en_US'):
+def user_add(givenname, sn, preferredlanguage='en_US', **kw):
if givenname == None:
raise Exception
@@ -25,6 +25,8 @@ def user_add(givenname, sn, preferredlanguage='en_US'):
'userpassword': 'Welcome2KolabSystems'
}
+ user_details.update(kw)
+
login = conf.get('ldap', 'bind_dn')
password = conf.get('ldap', 'bind_pw')
domain = conf.get('kolab', 'primary_domain')
diff --git a/tests/unit/test-002-attendee.py b/tests/unit/test-002-attendee.py
index 9da93c7..d7584e3 100644
--- a/tests/unit/test-002-attendee.py
+++ b/tests/unit/test-002-attendee.py
@@ -1,7 +1,9 @@
import datetime
import unittest
+import kolabformat
from pykolab.xml import Attendee
+from pykolab.xml import participant_status_label
class TestEventXML(unittest.TestCase):
attendee = Attendee("jane@doe.org")
@@ -101,5 +103,30 @@ class TestEventXML(unittest.TestCase):
self.assertEqual([k for k,v in self.attendee.cutype_map.iteritems() if v == 2][0], "INDIVIDUAL")
self.assertEqual([k for k,v in self.attendee.cutype_map.iteritems() if v == 3][0], "RESOURCE")
+ def test_018_partstat_label(self):
+ self.assertEqual(participant_status_label('NEEDS-ACTION'), "Needs Action")
+ self.assertEqual(participant_status_label(kolabformat.PartTentative), "Tentatively Accepted")
+ self.assertEqual(participant_status_label('UNKNOWN'), "UNKNOWN")
+
+ def test_020_to_dict(self):
+ name = "Doe, Jane"
+ role = 'OPT-PARTICIPANT'
+ cutype = 'RESOURCE'
+ partstat = 'ACCEPTED'
+ self.attendee.set_name(name)
+ self.attendee.set_rsvp(True)
+ self.attendee.set_role(role)
+ self.attendee.set_cutype(cutype)
+ self.attendee.set_participant_status(partstat)
+
+ data = self.attendee.to_dict()
+ self.assertIsInstance(data, dict)
+ self.assertEqual(data['role'], role)
+ self.assertEqual(data['cutype'], cutype)
+ self.assertEqual(data['partstat'], partstat)
+ self.assertEqual(data['name'], name)
+ self.assertEqual(data['email'], 'jane@doe.org')
+ self.assertTrue(data['rsvp'])
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/unit/test-003-event.py b/tests/unit/test-003-event.py
index 61ea8ec..1f54419 100644
--- a/tests/unit/test-003-event.py
+++ b/tests/unit/test-003-event.py
@@ -1,3 +1,4 @@
+import re
import datetime
import pytz
import sys
@@ -11,6 +12,233 @@ from pykolab.xml import EventIntegrityError
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
+"""
+
+xml_event = """
+<icalendar xmlns="urn:ietf:params:xml:ns:icalendar-2.0">
+ <vcalendar>
+ <properties>
+ <prodid>
+ <text>Libkolabxml-1.1</text>
+ </prodid>
+ <version>
+ <text>2.0</text>
+ </version>
+ <x-kolab-version>
+ <text>3.1.0</text>
+ </x-kolab-version>
+ </properties>
+ <components>
+ <vevent>
+ <properties>
+ <uid>
+ <text>75c740bb-b3c6-442c-8021-ecbaeb0a025e</text>
+ </uid>
+ <created>
+ <date-time>2014-07-07T01:28:23Z</date-time>
+ </created>
+ <dtstamp>
+ <date-time>2014-07-07T01:28:23Z</date-time>
+ </dtstamp>
+ <sequence>
+ <integer>1</integer>
+ </sequence>
+ <class>
+ <text>PUBLIC</text>
+ </class>
+ <dtstart>
+ <parameters>
+ <tzid>
+ <text>/kolab.org/Europe/London</text>
+ </tzid>
+ </parameters>
+ <date-time>2014-08-13T10:00:00</date-time>
+ </dtstart>
+ <dtend>
+ <parameters>
+ <tzid><text>/kolab.org/Europe/London</text></tzid>
+ </parameters>
+ <date-time>2014-08-13T14:00:00</date-time>
+ </dtend>
+ <rrule>
+ <recur>
+ <freq>DAILY</freq>
+ <until>
+ <date>2014-07-25</date>
+ </until>
+ </recur>
+ </rrule>
+ <exdate>
+ <parameters>
+ <tzid>
+ <text>/kolab.org/Europe/Berlin</text>
+ </tzid>
+ </parameters>
+ <date>2014-07-19</date>
+ <date>2014-07-26</date>
+ <date>2014-07-12</date>
+ <date>2014-07-13</date>
+ <date>2014-07-20</date>
+ <date>2014-07-27</date>
+ <date>2014-07-05</date>
+ <date>2014-07-06</date>
+ </exdate>
+ <summary>
+ <text>test</text>
+ </summary>
+ <description>
+ <text>test</text>
+ </description>
+ <priority>
+ <integer>5</integer>
+ </priority>
+ <status>
+ <text>CANCELLED</text>
+ </status>
+ <location>
+ <text>Room 101</text>
+ </location>
+ <organizer>
+ <parameters>
+ <cn><text>Doe, John</text></cn>
+ </parameters>
+ <cal-address>mailto:%3Cjohn%40example.org%3E</cal-address>
+ </organizer>
+ <attendee>
+ <parameters>
+ <partstat><text>ACCEPTED</text></partstat>
+ <role><text>REQ-PARTICIPANT</text></role>
+ <rsvp><boolean>true</boolean></rsvp>
+ </parameters>
+ <cal-address>mailto:%3Cjane%40example.org%3E</cal-address>
+ </attendee>
+ <attendee>
+ <parameters>
+ <partstat><text>TENTATIVE</text></partstat>
+ <role><text>OPT-PARTICIPANT</text></role>
+ </parameters>
+ <cal-address>mailto:%3Csomebody%40else.com%3E</cal-address>
+ </attendee>
+ <attach>
+ <parameters>
+ <fmttype>
+ <text>text/html</text>
+ </fmttype>
+ <x-label>
+ <text>noname.1395223627.5555</text>
+ </x-label>
+ </parameters>
+ <uri>cid:noname.1395223627.5555</uri>
+ </attach>
+ <x-custom>
+ <identifier>X-MOZ-RECEIVED-DTSTAMP</identifier>
+ <value>20140224T155612Z</value>
+ </x-custom>
+ <x-custom>
+ <identifier>X-GWSHOW-AS</identifier>
+ <value>BUSY</value>
+ </x-custom>
+ </properties>
+ <components>
+ <valarm>
+ <properties>
+ <action>
+ <text>DISPLAY</text>
+ </action>
+ <description>
+ <text>alarm 1</text>
+ </description>
+ <trigger>
+ <parameters>
+ <related>
+ <text>START</text>
+ </related>
+ </parameters>
+ <duration>-PT2H</duration>
+ </trigger>
+ </properties>
+ </valarm>
+ <valarm>
+ <properties>
+ <action>
+ <text>EMAIL</text>
+ </action>
+ <summary>
+ <text>test</text>
+ </summary>
+ <description>
+ <text>alarm 2</text>
+ </description>
+ <attendee>
+ <cal-address>mailto:%3Cjohn.die%40example.org%3E</cal-address>
+ </attendee>
+ <trigger>
+ <parameters>
+ <related>
+ <text>START</text>
+ </related>
+ </parameters>
+ <duration>-P1D</duration>
+ </trigger>
+ </properties>
+ </valarm>
+ </components>
+ </vevent>
+ </components>
+ </vcalendar>
+</icalendar>
+"""
class TestEventXML(unittest.TestCase):
event = Event()
@@ -117,25 +345,23 @@ class TestEventXML(unittest.TestCase):
def test_018_load_from_ical(self):
ical_str = """BEGIN:VCALENDAR
VERSION:2.0
-PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+ 2.1.3//EN
CALSCALE:GREGORIAN
-BEGIN:VEVENT
-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
-UID:7a35527d-f783-4b58-b404-b1389bd2fc57
-ATTENDEE;CN="Doe, Jane";CUTYPE=INDIVIDUAL;PARTSTAT=ACCEPTED
- ;ROLE=REQ-PARTICIPANT;RSVP=FALSE:MAILTO:jane@doe.org
-ATTENDEE;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION
- ;ROLE=OPT-PARTICIPANT;RSVP=FALSE:MAILTO:max@imum.com
-SEQUENCE:2
-END:VEVENT
-END:VCALENDAR
-"""
+METHOD:REQUEST
+ """ + ical_event + "END:VCALENDAR"
+
ical = icalendar.Calendar.from_ical(ical_str)
event = event_from_ical(ical.walk('VEVENT')[0].to_ical())
+
+ self.assertEqual(event.get_location(), "Location")
+ self.assertEqual(str(event.get_lastmodified()), "2014-04-07 12:23:11+00:00")
+ self.assertEqual(event.get_description(), "Description\n2 lines")
+ self.assertEqual(event.get_url(), "http://somelink.com/foo")
+ self.assertEqual(event.get_transparency(), False)
+ self.assertEqual(event.get_categories(), ["Personal"])
+ self.assertEqual(event.get_priority(), '2')
+ self.assertEqual(event.get_classification(), kolabformat.ClassPublic)
self.assertEqual(event.get_attendee_by_email("max@imum.com").get_cutype(), kolabformat.CutypeResource)
self.assertEqual(event.get_sequence(), 2)
self.assertTrue(event.is_recurring())
@@ -144,17 +370,62 @@ END:VCALENDAR
self.assertEqual(str(event.get_end()), "2014-05-23 12:30:00+01:00")
self.assertEqual(len(event.get_exception_dates()), 2)
self.assertIsInstance(event.get_exception_dates()[0], datetime.datetime)
+ 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_018_ical_allday_events(self):
+ ical = """BEGIN:VEVENT
+UID:ffffffff-f783-4b58-b404-b1389bd2ffff
+DTSTAMP;VALUE=DATE-TIME:20140407T122311Z
+CREATED;VALUE=DATE-TIME:20140407T122245Z
+DTSTART;VALUE=DATE:20140823
+DTEND;VALUE=DATE:20140824
+SUMMARY:All day
+DESCRIPTION:One single day
+TRANSP:OPAQUE
+CLASS:PUBLIC
+END:VEVENT
+"""
+ event = event_from_ical(ical)
+ self.assertEqual(str(event.get_start()), "2014-08-23")
+ self.assertEqual(str(event.get_end()), "2014-08-23")
+ self.assertEqual(str(event.get_ical_dtend()), "2014-08-24")
+ self.assertTrue(re.match('.*<dtend>\s*<date>2014-08-23</date>', str(event), re.DOTALL))
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")))
self.event.set_end(datetime.datetime(2014, 05, 23, 12, 30, 00, tzinfo=pytz.timezone("Europe/London")))
+ self.event.set_sequence(3)
+ self.event.add_custom_property('X-Custom', 'check')
ical = icalendar.Calendar.from_ical(self.event.as_string_itip())
event = ical.walk('VEVENT')[0]
self.assertEqual(event['uid'], self.event.get_uid())
self.assertEqual(event['summary'], "test")
+ self.assertEqual(event['sequence'], 3)
+ self.assertEqual(event['X-CUSTOM'], "check")
self.assertIsInstance(event['dtstamp'].dt, datetime.datetime)
def test_020_calendaring_recurrence(self):
@@ -214,6 +485,94 @@ END:VCALENDAR
self.assertEqual(self.event.get_next_occurence(_start), None)
self.assertEqual(self.event.get_last_occurrence(), None)
+ def test_022_load_from_xml(self):
+ event = event_from_string(xml_event)
+ self.assertEqual(event.uid, '75c740bb-b3c6-442c-8021-ecbaeb0a025e')
+ self.assertEqual(event.get_attendee_by_email("jane@example.org").get_participant_status(), kolabformat.PartAccepted)
+ self.assertEqual(event.get_sequence(), 1)
+ 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:])
+ self.assertEqual(event.get_attachment_data(1), 'This is a text file')
+
+ 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)
+
+ def test_025_to_dict(self):
+ data = event_from_string(xml_event).to_dict()
+
+ self.assertIsInstance(data, dict)
+ self.assertIsInstance(data['start'], datetime.datetime)
+ self.assertIsInstance(data['end'], datetime.datetime)
+ self.assertIsInstance(data['created'], datetime.datetime)
+ self.assertIsInstance(data['lastmodified-date'], datetime.datetime)
+ self.assertEqual(data['uid'], '75c740bb-b3c6-442c-8021-ecbaeb0a025e')
+ self.assertEqual(data['summary'], 'test')
+ self.assertEqual(data['location'], 'Room 101')
+ self.assertEqual(data['description'], 'test')
+ self.assertEqual(data['priority'], 5)
+ self.assertEqual(data['status'], 'CANCELLED')
+ self.assertEqual(data['sequence'], 1)
+ self.assertEqual(data['transparency'], False)
+ self.assertEqual(data['X-GWSHOW-AS'], 'BUSY')
+
+ self.assertIsInstance(data['organizer'], dict)
+ self.assertEqual(data['organizer']['email'], 'john@example.org')
+
+ self.assertEqual(len(data['attendee']), 2)
+ self.assertIsInstance(data['attendee'][0], dict)
+
+ self.assertEqual(len(data['attach']), 1)
+ self.assertIsInstance(data['attach'][0], dict)
+ self.assertEqual(data['attach'][0]['fmttype'], 'text/html')
+
+ self.assertIsInstance(data['rrule'], dict)
+ self.assertEqual(data['rrule']['frequency'], 'DAILY')
+ self.assertEqual(data['rrule']['interval'], 1)
+ self.assertEqual(data['rrule']['wkst'], 'MO')
+ self.assertIsInstance(data['rrule']['until'], datetime.date)
+
+ self.assertIsInstance(data['alarm'], list)
+ self.assertEqual(len(data['alarm']), 2)
+ self.assertEqual(data['alarm'][0]['action'], 'DISPLAY')
+ self.assertEqual(data['alarm'][1]['action'], 'EMAIL')
+ self.assertEqual(data['alarm'][1]['trigger']['value'], '-P1D')
+ self.assertEqual(len(data['alarm'][1]['attendee']), 1)
+
if __name__ == '__main__':
unittest.main()
diff --git a/tests/unit/test-011-itip.py b/tests/unit/test-011-itip.py
new file mode 100644
index 0000000..a08d05f
--- /dev/null
+++ b/tests/unit/test-011-itip.py
@@ -0,0 +1,412 @@
+import pykolab
+import datetime
+import pytz
+import kolabformat
+
+from pykolab import itip
+from pykolab.xml import Event
+from pykolab.xml import participant_status_label
+from pykolab.translate import _
+
+from icalendar import Calendar
+from email import message
+from email import message_from_string
+from wallace import module_resources
+from twisted.trial import unittest
+
+# define some iTip MIME messages
+
+itip_multipart = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_c8894dbdb8baeedacae836230e3436fd"
+From: "Doe, John" <john.doe@example.org>
+Date: Fri, 13 Jul 2012 13:54:14 +0100
+Message-ID: <240fe7ae7e139129e9eb95213c1016d7@example.org>
+User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
+To: resource-collection-car@example.org
+Subject: "test" has been updated
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+*test*
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/calendar; charset=UTF-8; method=REQUEST;
+ name=event.ics
+Content-Disposition: attachment;
+ filename=event.ics
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+DTSTAMP:20120713T1254140
+DTSTART;TZID=3DEurope/London:20120713T100000
+DTEND;TZID=3DEurope/London:20120713T110000
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN=3D"Doe, John":mailto:john.doe@example.org
+ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt=
+o:resource-collection-car@example.org
+ATTENDEE;ROLE=3DOPT-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailto:anoth=
+er-resource@example.org
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--=_c8894dbdb8baeedacae836230e3436fd--
+"""
+
+itip_non_multipart = """Return-Path: <john.doe@example.org>
+Sender: john.doe@example.org
+Content-Type: text/calendar; method=REQUEST; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+To: resource-collection-car@example.org
+From: john.doe@example.org
+Date: Mon, 24 Feb 2014 11:27:28 +0100
+Message-ID: <1a3aa8995e83dd24cf9247e538ac913a@example.org>
+Subject: test
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+DTSTAMP:20120713T1254140
+DTSTART;TZID=3DEurope/London:20120713T100000
+DTEND;TZID=3DEurope/London:20120713T110000
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN=3D"Doe, John":mailto:john.doe@example.org
+ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DACCEPTED;RSVP=3DTRUE:mailt=
+o:resource-collection-car@example.org
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+itip_google_multipart = """MIME-Version: 1.0
+Message-ID: <001a11c2ad84243e0604f3246bae@google.com>
+Date: Mon, 24 Feb 2014 10:27:28 +0000
+Subject: =?ISO-8859-1?Q?Invitation=3A_iTip_from_Apple_=40_Mon_Feb_24=2C_2014_12pm_?=
+ =?ISO-8859-1?Q?=2D_1pm_=28Tom_=26_T=E4m=29?=
+From: "john.doe" <john.doe@gmail.com>
+To: <john.sample@example.org>
+Content-Type: multipart/mixed; boundary=001a11c2ad84243df004f3246bad
+
+--001a11c2ad84243df004f3246bad
+Content-Type: multipart/alternative; boundary=001a11c2ad84243dec04f3246bab
+
+--001a11c2ad84243dec04f3246bab
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed; delsp=yes
+
+<some text content here>
+
+--001a11c2ad84243dec04f3246bab
+Content-Type: text/html; charset=ISO-8859-1
+Content-Transfer-Encoding: quoted-printable
+
+<div style=3D""><!-- some HTML message content here --></div>
+--001a11c2ad84243dec04f3246bab
+Content-Type: text/calendar; charset=UTF-8; method=REQUEST
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+PRODID:-//Google Inc//Google Calendar 70.9054//EN
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+DTSTART:20140224T110000Z
+DTEND:20140224T120000Z
+DTSTAMP:20140224T102728Z
+ORGANIZER:mailto:kepjllr6mcq7d0959u4cdc7000@group.calendar.google.com
+UID:0BE2F640-5814-47C9-ABAE-E7E959204E76
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE
+ ;X-NUM-GUESTS=0:mailto:kepjllr6mcq7d0959u4cdc7000@group.calendar.google.com
+ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
+ TRUE;CN=John Sample;X-NUM-GUESTS=0:mailto:john.sample@example.org
+CREATED:20140224T102728Z
+DESCRIPTION:Testing Multipart structure\\nView your event at http://www.goog
+ le.com/calendar/event?action=VIEW&eid=XzYxMTRhY2k2Nm9xMzBiOWw3MG9qOGI5azZ0M
+ WppYmExODkwa2FiYTU2dDJqaWQ5cDY4bzM4aDluNm8gdGhvbWFzQGJyb3RoZXJsaS5jaA&tok=N
+ TIja2VwamxscjZtY3E3ZDA5NTl1NGNkYzcwMDBAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbTkz
+ NTcyYTU2YmUwNWMxNjY0Zjc3OTU0MzhmMDcwY2FhN2NjZjIzYWM&ctz=Europe/Zurich&hl=en
+ .
+LAST-MODIFIED:20140224T102728Z
+LOCATION:
+SEQUENCE:5
+STATUS:CONFIRMED
+SUMMARY:iTip from Apple
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--001a11c2ad84243dec04f3246bab--
+--001a11c2ad84243df004f3246bad
+Content-Type: application/ics; name="invite.ics"
+Content-Disposition: attachment; filename="invite.ics"
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw
+LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT
+VA0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTQwMjI0VDExMDAwMFoNCkRURU5EOjIwMTQwMjI0
+VDEyMDAwMFoNCkRUU1RBTVA6MjAxNDAyMjRUMTAyNzI4Wg0KT1JHQU5JWkVSOm1haWx0bzprZXBq
+bGxyNm1jcTdkMDk1OXU0Y2RjNzAwMEBncm91cC5jYWxlbmRhci5nb29nbGUuY29tDQpVSUQ6MEJF
+MkY2NDAtNTgxNC00N0M5LUFCQUUtRTdFOTU5MjA0RTc2DQpBVFRFTkRFRTtDVVRZUEU9SU5ESVZJ
+RFVBTDtST0xFPVJFUS1QQVJUSUNJUEFOVDtQQVJUU1RBVD1BQ0NFUFRFRDtSU1ZQPVRSVUUNCiA7
+WC1OVU0tR1VFU1RTPTA6bWFpbHRvOmtlcGpsbHI2bWNxN2QwOTU5dTRjZGM3MDAwQGdyb3VwLmNh
+bGVuZGFyLmdvb2dsZS5jb20NCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFMO1JPTEU9UkVRLVBB
+UlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7WC1OVU0tR1VFU1RT
+PTA6bWFpbHRvOnRob21hc0Bicm90aGVybGkuY2gNCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFM
+O1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7
+Q049VGhvbWFzIEJydWVkZXJsaTtYLU5VTS1HVUVTVFM9MDptYWlsdG86cm91bmRjdWJlQGdtYWls
+LmNvbQ0KQ1JFQVRFRDoyMDE0MDIyNFQxMDI3MjhaDQpERVNDUklQVElPTjpUZXN0aW5nIE11bHRp
+cGFydCBzdHJ1Y3R1cmVcblZpZXcgeW91ciBldmVudCBhdCBodHRwOi8vd3d3Lmdvb2cNCiBsZS5j
+b20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPVh6WXhNVFJoWTJrMk5tOXhNekJpT1d3
+M01HOXFPR0k1YXpaME0NCiBXcHBZbUV4T0Rrd2EyRmlZVFUyZERKcWFXUTVjRFk0YnpNNGFEbHVO
+bThnZEdodmJXRnpRR0p5YjNSb1pYSnNhUzVqYUEmdG9rPU4NCiBUSWphMlZ3YW14c2NqWnRZM0Uz
+WkRBNU5UbDFOR05rWXpjd01EQkFaM0p2ZFhBdVkyRnNaVzVrWVhJdVoyOXZaMnhsTG1OdmJUa3oN
+CiBOVGN5WVRVMlltVXdOV014TmpZMFpqYzNPVFUwTXpobU1EY3dZMkZoTjJOalpqSXpZV00mY3R6
+PUV1cm9wZS9adXJpY2gmaGw9ZW4NCiAuDQpMQVNULU1PRElGSUVEOjIwMTQwMjI0VDEwMjcyOFoN
+CkxPQ0FUSU9OOg0KU0VRVUVOQ0U6NQ0KU1RBVFVTOkNPTkZJUk1FRA0KU1VNTUFSWTppVGlwIGZy
+b20gQXBwbGUNClRSQU5TUDpPUEFRVUUNCkVORDpWRVZFTlQNCkVORDpWQ0FMRU5EQVINCg==
+--001a11c2ad84243df004f3246bad--
+"""
+
+itip_application_ics = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_c8894dbdb8baeedacae836230e3436fd"
+From: "Doe, John" <john.doe@example.org>
+Date: Fri, 13 Jul 2012 13:54:14 +0100
+Message-ID: <240fe7ae7e139129e9eb95213c101622@example.org>
+User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
+To: resource-collection-car@example.org
+Subject: "test" has been updated
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset=UTF-8; format=flowed
+
+<some text here>
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: application/ics; charset=UTF-8; method=REQUEST;
+ name=event.ics
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+DTSTAMP:20120713T1254140
+DTSTART;TZID=3DEurope/London:20120713T100000
+DTEND;TZID=3DEurope/London:20120713T110000
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN=3D"Doe, John":mailto:john.doe@example.org
+ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt=
+o:resource-collection-car@example.org
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--=_c8894dbdb8baeedacae836230e3436fd--
+"""
+
+itip_recurring = """Return-Path: <john.doe@example.org>
+Sender: john.doe@example.org
+Content-Type: text/calendar; method=REQUEST; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+From: john.doe@example.org
+Date: Mon, 24 Feb 2014 11:27:28 +0100
+Message-ID: <1a3aa8995e83dd24cf9247e538ac913a@example.org>
+Subject: Recurring
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Apple Inc.//Mac OS X 10.9.2//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:dbdb8baeedacae836230e3436fd-5e83dd24cf92
+DTSTAMP:20140213T1254140
+DTSTART;TZID=Europe/London:20120709T100000
+DTEND;TZID=Europe/London:20120709T120000
+RRULE:FREQ=DAILY;INTERVAL=1;COUNT=5
+SUMMARY:Recurring
+ORGANIZER;CN="Doe, John":mailto:john.doe@example.org
+ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:jane@example.com
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+"""
+
+itip_empty = """MIME-Version: 1.0
+Date: Fri, 17 Jan 2014 13:51:50 +0100
+From: <john.doe@example.org>
+User-Agent: Roundcube Webmail/0.9.5
+To: john.sample@example.org
+Subject: "test" has been sent
+Message-ID: <52D92766.5040508@somedomain.com>
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+Message plain text goes here...
+"""
+
+conf = pykolab.getConf()
+
+if not hasattr(conf, 'defaults'):
+ conf.finalize_conf()
+
+class TestITip(unittest.TestCase):
+
+ def setUp(self):
+ # intercept calls to smtplib.SMTP.sendmail()
+ import smtplib
+ self.patch(smtplib.SMTP, "__init__", self._mock_smtp_init)
+ self.patch(smtplib.SMTP, "quit", self._mock_nop)
+ self.patch(smtplib.SMTP, "sendmail", self._mock_smtp_sendmail)
+
+ self.smtplog = [];
+
+ def _mock_nop(self, domain=None):
+ pass
+
+ def _mock_smtp_init(self, host=None, port=None, local_hostname=None, timeout=0):
+ pass
+
+ def _mock_smtp_sendmail(self, from_addr, to_addr, message, mail_options=None, rcpt_options=None):
+ self.smtplog.append((from_addr, to_addr, message))
+
+
+ def test_001_itip_events_from_message(self):
+ itips1 = itip.events_from_message(message_from_string(itip_multipart))
+ self.assertEqual(len(itips1), 1, "Multipart iTip message with text/calendar")
+ self.assertEqual(itips1[0]['method'], "REQUEST", "iTip request method property")
+
+ itips2 = itip.events_from_message(message_from_string(itip_non_multipart))
+ self.assertEqual(len(itips2), 1, "Detect non-multipart iTip messages")
+
+ itips3 = itip.events_from_message(message_from_string(itip_application_ics))
+ self.assertEqual(len(itips3), 1, "Multipart iTip message with application/ics attachment")
+
+ itips4 = itip.events_from_message(message_from_string(itip_google_multipart))
+ self.assertEqual(len(itips4), 1, "Multipart iTip message from Google")
+
+ itips5 = itip.events_from_message(message_from_string(itip_empty))
+ self.assertEqual(len(itips5), 0, "Simple plain text message")
+
+ # invalid itip blocks
+ self.assertRaises(Exception, itip.events_from_message, message_from_string(itip_multipart.replace("BEGIN:VEVENT", "")))
+
+ itips6 = itip.events_from_message(message_from_string(itip_multipart.replace("DTSTART;", "X-DTSTART;")))
+ self.assertEqual(len(itips6), 0, "Event with not DTSTART")
+
+ itips7 = itip.events_from_message(message_from_string(itip_non_multipart.replace("METHOD:REQUEST", "METHOD:PUBLISH").replace("method=REQUEST", "method=PUBLISH")))
+ self.assertEqual(len(itips7), 0, "Invalid METHOD")
+
+
+ def test_002_check_date_conflict(self):
+ astart = datetime.datetime(2014,7,13, 10,0,0)
+ aend = astart + datetime.timedelta(hours=2)
+
+ bstart = datetime.datetime(2014,7,13, 10,0,0)
+ bend = astart + datetime.timedelta(hours=1)
+ self.assertTrue(itip.check_date_conflict(astart, aend, bstart, bend))
+
+ bstart = datetime.datetime(2014,7,13, 11,0,0)
+ bend = astart + datetime.timedelta(minutes=30)
+ self.assertTrue(itip.check_date_conflict(astart, aend, bstart, bend))
+
+ bend = astart + datetime.timedelta(hours=2)
+ self.assertTrue(itip.check_date_conflict(astart, aend, bstart, bend))
+
+ bstart = datetime.datetime(2014,7,13, 12,0,0)
+ bend = astart + datetime.timedelta(hours=1)
+ self.assertFalse(itip.check_date_conflict(astart, aend, bstart, bend))
+
+ bstart = datetime.datetime(2014,6,13, 10,0,0)
+ bend = datetime.datetime(2014,6,14, 12,0,0)
+ self.assertFalse(itip.check_date_conflict(astart, aend, bstart, bend))
+
+ bstart = datetime.datetime(2014,7,10, 12,0,0)
+ bend = datetime.datetime(2014,7,14, 14,0,0)
+ self.assertTrue(itip.check_date_conflict(astart, aend, bstart, bend))
+
+
+ def test_002_check_event_conflict(self):
+ itip_event = itip.events_from_message(message_from_string(itip_non_multipart))[0]
+
+ event = Event()
+ event.set_start(datetime.datetime(2012,7,13, 9,30,0, tzinfo=itip_event['start'].tzinfo))
+ event.set_end(datetime.datetime(2012,7,13, 10,30,0, tzinfo=itip_event['start'].tzinfo))
+
+ self.assertTrue(itip.check_event_conflict(event, itip_event), "Conflicting dates")
+
+ event.set_uid(itip_event['uid'])
+ self.assertFalse(itip.check_event_conflict(event, itip_event), "No conflict for same UID")
+
+ allday = Event()
+ allday.set_start(datetime.date(2012,7,13))
+ allday.set_end(datetime.date(2012,7,13))
+
+ self.assertTrue(itip.check_event_conflict(allday, itip_event), "Conflicting allday event")
+
+ allday.set_transparency(True)
+ self.assertFalse(itip.check_event_conflict(allday, itip_event), "No conflict if event is set to transparent")
+
+ event2 = Event()
+ event2.set_start(datetime.datetime(2012,7,13, 10,0,0, tzinfo=pytz.timezone("US/Central")))
+ event2.set_end(datetime.datetime(2012,7,13, 11,0,0, tzinfo=pytz.timezone("US/Central")))
+
+ self.assertFalse(itip.check_event_conflict(event, itip_event), "No conflict with timezone shift")
+
+ rrule = kolabformat.RecurrenceRule()
+ rrule.setFrequency(kolabformat.RecurrenceRule.Weekly)
+ rrule.setCount(10)
+
+ event3 = Event()
+ event3.set_recurrence(rrule);
+ event3.set_start(datetime.datetime(2012,6,29, 9,30,0, tzinfo=pytz.utc))
+ event3.set_end(datetime.datetime(2012,6,29, 10,30,0, tzinfo=pytz.utc))
+
+ self.assertTrue(itip.check_event_conflict(event3, itip_event), "Conflict in (3rd) recurring event instance")
+
+ itip_event = itip.events_from_message(message_from_string(itip_recurring))[0]
+ self.assertTrue(itip.check_event_conflict(event3, itip_event), "Conflict in two recurring events")
+
+ event4 = Event()
+ event4.set_recurrence(rrule);
+ event4.set_start(datetime.datetime(2012,7,1, 9,30,0, tzinfo=pytz.utc))
+ event4.set_end(datetime.datetime(2012,7,1, 10,30,0, tzinfo=pytz.utc))
+ self.assertFalse(itip.check_event_conflict(event4, itip_event), "No conflict in two recurring events")
+
+
+ def test_003_send_reply(self):
+ itip_events = itip.events_from_message(message_from_string(itip_non_multipart))
+ itip.send_reply("resource-collection-car@example.org", itip_events, "SUMMARY=%(summary)s; STATUS=%(status)s; NAME=%(name)s;")
+
+ self.assertEqual(len(self.smtplog), 1)
+ self.assertEqual(self.smtplog[0][0], 'resource-collection-car@example.org', "From attendee")
+ self.assertEqual(self.smtplog[0][1], 'john.doe@example.org', "To organizer")
+
+ _accepted = participant_status_label('ACCEPTED')
+ message = message_from_string(self.smtplog[0][2])
+ self.assertEqual(message.get('Subject'), _("Invitation for %(summary)s was %(status)s") % { 'summary':'test', 'status':_accepted })
+
+ text = str(message.get_payload(0));
+ self.assertIn('SUMMARY=test', text)
+ self.assertIn('STATUS=' + _accepted, text)
diff --git a/tests/unit/test-011-wallace_resources.py b/tests/unit/test-011-wallace_resources.py
index 62bfd27..9c42317 100644
--- a/tests/unit/test-011-wallace_resources.py
+++ b/tests/unit/test-011-wallace_resources.py
@@ -2,6 +2,7 @@ import pykolab
import logging
import datetime
+from pykolab import itip
from icalendar import Calendar
from email import message
from email import message_from_string
@@ -87,152 +88,6 @@ END:VEVENT
END:VCALENDAR
"""
-itip_google_multipart = """MIME-Version: 1.0
-Message-ID: <001a11c2ad84243e0604f3246bae@google.com>
-Date: Mon, 24 Feb 2014 10:27:28 +0000
-Subject: =?ISO-8859-1?Q?Invitation=3A_iTip_from_Apple_=40_Mon_Feb_24=2C_2014_12pm_?=
- =?ISO-8859-1?Q?=2D_1pm_=28Tom_=26_T=E4m=29?=
-From: "john.doe" <john.doe@gmail.com>
-To: <john.sample@example.org>
-Content-Type: multipart/mixed; boundary=001a11c2ad84243df004f3246bad
-
---001a11c2ad84243df004f3246bad
-Content-Type: multipart/alternative; boundary=001a11c2ad84243dec04f3246bab
-
---001a11c2ad84243dec04f3246bab
-Content-Type: text/plain; charset=ISO-8859-1; format=flowed; delsp=yes
-
-<some text content here>
-
---001a11c2ad84243dec04f3246bab
-Content-Type: text/html; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-
-<div style=3D""><!-- some HTML message content here --></div>
---001a11c2ad84243dec04f3246bab
-Content-Type: text/calendar; charset=UTF-8; method=REQUEST
-Content-Transfer-Encoding: 7bit
-
-BEGIN:VCALENDAR
-PRODID:-//Google Inc//Google Calendar 70.9054//EN
-VERSION:2.0
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-BEGIN:VEVENT
-DTSTART:20140224T110000Z
-DTEND:20140224T120000Z
-DTSTAMP:20140224T102728Z
-ORGANIZER:mailto:kepjllr6mcq7d0959u4cdc7000@group.calendar.google.com
-UID:0BE2F640-5814-47C9-ABAE-E7E959204E76
-ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE
- ;X-NUM-GUESTS=0:mailto:kepjllr6mcq7d0959u4cdc7000@group.calendar.google.com
-ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=
- TRUE;CN=John Sample;X-NUM-GUESTS=0:mailto:john.sample@example.org
-CREATED:20140224T102728Z
-DESCRIPTION:Testing Multipart structure\\nView your event at http://www.goog
- le.com/calendar/event?action=VIEW&eid=XzYxMTRhY2k2Nm9xMzBiOWw3MG9qOGI5azZ0M
- WppYmExODkwa2FiYTU2dDJqaWQ5cDY4bzM4aDluNm8gdGhvbWFzQGJyb3RoZXJsaS5jaA&tok=N
- TIja2VwamxscjZtY3E3ZDA5NTl1NGNkYzcwMDBAZ3JvdXAuY2FsZW5kYXIuZ29vZ2xlLmNvbTkz
- NTcyYTU2YmUwNWMxNjY0Zjc3OTU0MzhmMDcwY2FhN2NjZjIzYWM&ctz=Europe/Zurich&hl=en
- .
-LAST-MODIFIED:20140224T102728Z
-LOCATION:
-SEQUENCE:5
-STATUS:CONFIRMED
-SUMMARY:iTip from Apple
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-
---001a11c2ad84243dec04f3246bab--
---001a11c2ad84243df004f3246bad
-Content-Type: application/ics; name="invite.ics"
-Content-Disposition: attachment; filename="invite.ics"
-Content-Transfer-Encoding: base64
-
-QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw
-LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT
-VA0KQkVHSU46VkVWRU5UDQpEVFNUQVJUOjIwMTQwMjI0VDExMDAwMFoNCkRURU5EOjIwMTQwMjI0
-VDEyMDAwMFoNCkRUU1RBTVA6MjAxNDAyMjRUMTAyNzI4Wg0KT1JHQU5JWkVSOm1haWx0bzprZXBq
-bGxyNm1jcTdkMDk1OXU0Y2RjNzAwMEBncm91cC5jYWxlbmRhci5nb29nbGUuY29tDQpVSUQ6MEJF
-MkY2NDAtNTgxNC00N0M5LUFCQUUtRTdFOTU5MjA0RTc2DQpBVFRFTkRFRTtDVVRZUEU9SU5ESVZJ
-RFVBTDtST0xFPVJFUS1QQVJUSUNJUEFOVDtQQVJUU1RBVD1BQ0NFUFRFRDtSU1ZQPVRSVUUNCiA7
-WC1OVU0tR1VFU1RTPTA6bWFpbHRvOmtlcGpsbHI2bWNxN2QwOTU5dTRjZGM3MDAwQGdyb3VwLmNh
-bGVuZGFyLmdvb2dsZS5jb20NCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFMO1JPTEU9UkVRLVBB
-UlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7WC1OVU0tR1VFU1RT
-PTA6bWFpbHRvOnRob21hc0Bicm90aGVybGkuY2gNCkFUVEVOREVFO0NVVFlQRT1JTkRJVklEVUFM
-O1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElPTjtSU1ZQPQ0KIFRSVUU7
-Q049VGhvbWFzIEJydWVkZXJsaTtYLU5VTS1HVUVTVFM9MDptYWlsdG86cm91bmRjdWJlQGdtYWls
-LmNvbQ0KQ1JFQVRFRDoyMDE0MDIyNFQxMDI3MjhaDQpERVNDUklQVElPTjpUZXN0aW5nIE11bHRp
-cGFydCBzdHJ1Y3R1cmVcblZpZXcgeW91ciBldmVudCBhdCBodHRwOi8vd3d3Lmdvb2cNCiBsZS5j
-b20vY2FsZW5kYXIvZXZlbnQ/YWN0aW9uPVZJRVcmZWlkPVh6WXhNVFJoWTJrMk5tOXhNekJpT1d3
-M01HOXFPR0k1YXpaME0NCiBXcHBZbUV4T0Rrd2EyRmlZVFUyZERKcWFXUTVjRFk0YnpNNGFEbHVO
-bThnZEdodmJXRnpRR0p5YjNSb1pYSnNhUzVqYUEmdG9rPU4NCiBUSWphMlZ3YW14c2NqWnRZM0Uz
-WkRBNU5UbDFOR05rWXpjd01EQkFaM0p2ZFhBdVkyRnNaVzVrWVhJdVoyOXZaMnhsTG1OdmJUa3oN
-CiBOVGN5WVRVMlltVXdOV014TmpZMFpqYzNPVFUwTXpobU1EY3dZMkZoTjJOalpqSXpZV00mY3R6
-PUV1cm9wZS9adXJpY2gmaGw9ZW4NCiAuDQpMQVNULU1PRElGSUVEOjIwMTQwMjI0VDEwMjcyOFoN
-CkxPQ0FUSU9OOg0KU0VRVUVOQ0U6NQ0KU1RBVFVTOkNPTkZJUk1FRA0KU1VNTUFSWTppVGlwIGZy
-b20gQXBwbGUNClRSQU5TUDpPUEFRVUUNCkVORDpWRVZFTlQNCkVORDpWQ0FMRU5EQVINCg==
---001a11c2ad84243df004f3246bad--
-"""
-
-itip_application_ics = """MIME-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="=_c8894dbdb8baeedacae836230e3436fd"
-From: "Doe, John" <john.doe@example.org>
-Date: Fri, 13 Jul 2012 13:54:14 +0100
-Message-ID: <240fe7ae7e139129e9eb95213c101622@example.org>
-User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
-To: resource-collection-car@example.org
-Subject: "test" has been updated
-
---=_c8894dbdb8baeedacae836230e3436fd
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain; charset=UTF-8; format=flowed
-
-<some text here>
-
---=_c8894dbdb8baeedacae836230e3436fd
-Content-Type: application/ics; charset=UTF-8; method=REQUEST;
- name=event.ics
-Content-Transfer-Encoding: quoted-printable
-
-BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN
-CALSCALE:GREGORIAN
-METHOD:REQUEST
-BEGIN:VEVENT
-UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
-DTSTAMP:20120713T1254140
-DTSTART;TZID=3DEurope/London:20120713T100000
-DTEND;TZID=3DEurope/London:20120713T110000
-SUMMARY:test
-DESCRIPTION:test
-ORGANIZER;CN=3D"Doe, John":mailto:john.doe@example.org
-ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt=
-o:resource-collection-car@example.org
-TRANSP:OPAQUE
-END:VEVENT
-END:VCALENDAR
-
---=_c8894dbdb8baeedacae836230e3436fd--
-"""
-
-itip_empty = """MIME-Version: 1.0
-Date: Fri, 17 Jan 2014 13:51:50 +0100
-From: <john.doe@example.org>
-User-Agent: Roundcube Webmail/0.9.5
-To: john.sample@example.org
-Subject: "test" has been sent
-Message-ID: <52D92766.5040508@somedomain.com>
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 7bit
-
-Message plain text goes here...
-"""
-
-
conf = pykolab.getConf()
if not hasattr(conf, 'defaults'):
@@ -272,7 +127,7 @@ class TestWallaceResources(unittest.TestCase):
def _mock_search_entry_by_attribute(self, attr, value, **kw):
results = []
if value == "cn=Room 101,ou=Resources,dc=example,dc=org":
- results.append({ 'dn': 'cn=Rooms,ou=Resources,dc=example,dc=org', attr: value, 'owner': 'uid=doe,ou=People,dc=example,dc=org' })
+ results.append(('cn=Rooms,ou=Resources,dc=example,dc=org', { attr: value, 'owner': 'uid=doe,ou=People,dc=example,dc=org' }))
return results
def _mock_smtp_init(self, host=None, port=None, local_hostname=None, timeout=0):
@@ -301,32 +156,6 @@ class TestWallaceResources(unittest.TestCase):
return None
- def test_001_itip_events_from_message(self):
- itips1 = module_resources.itip_events_from_message(message_from_string(itip_multipart))
- self.assertEqual(len(itips1), 1, "Multipart iTip message with text/calendar")
- self.assertEqual(itips1[0]['method'], "REQUEST", "iTip request method property")
-
- itips2 = module_resources.itip_events_from_message(message_from_string(itip_non_multipart))
- self.assertEqual(len(itips2), 1, "Detect non-multipart iTip messages")
-
- itips3 = module_resources.itip_events_from_message(message_from_string(itip_application_ics))
- self.assertEqual(len(itips3), 1, "Multipart iTip message with application/ics attachment")
-
- itips4 = module_resources.itip_events_from_message(message_from_string(itip_google_multipart))
- self.assertEqual(len(itips4), 1, "Multipart iTip message from Google")
-
- itips5 = module_resources.itip_events_from_message(message_from_string(itip_empty))
- self.assertEqual(len(itips5), 0, "Simple plain text message")
-
- # invalid itip blocks
- self.assertRaises(Exception, module_resources.itip_events_from_message, message_from_string(itip_multipart.replace("BEGIN:VEVENT", "")))
-
- itips6 = module_resources.itip_events_from_message(message_from_string(itip_multipart.replace("DTSTART;", "X-DTSTART;")))
- self.assertEqual(len(itips6), 0, "Event with not DTSTART")
-
- itips7 = module_resources.itip_events_from_message(message_from_string(itip_non_multipart.replace("METHOD:REQUEST", "METHOD:PUBLISH").replace("method=REQUEST", "method=PUBLISH")))
- self.assertEqual(len(itips7), 0, "Invalid METHOD")
-
def test_002_resource_record_from_email_address(self):
res = module_resources.resource_record_from_email_address("doe@example.org")
@@ -337,7 +166,7 @@ class TestWallaceResources(unittest.TestCase):
def test_003_resource_records_from_itip_events(self):
message = message_from_string(itip_multipart)
- itips = module_resources.itip_events_from_message(message)
+ itips = itip.events_from_message(message)
res = module_resources.resource_records_from_itip_events(itips)
self.assertEqual(len(res), 2, "Return all attendee resources");
@@ -365,7 +194,7 @@ class TestWallaceResources(unittest.TestCase):
def test_005_send_response_accept(self):
- itip_event = module_resources.itip_events_from_message(message_from_string(itip_non_multipart))
+ itip_event = itip.events_from_message(message_from_string(itip_non_multipart))
module_resources.send_response("resource-collection-car@example.org", itip_event)
self.assertEqual(len(self.smtplog), 1);
@@ -384,7 +213,7 @@ class TestWallaceResources(unittest.TestCase):
def test_006_send_response_delegate(self):
# delegate resource-collection-car@example.org => resource-car-audi-a4@example.org
- itip_event = module_resources.itip_events_from_message(message_from_string(itip_non_multipart))[0]
+ itip_event = itip.events_from_message(message_from_string(itip_non_multipart))[0]
itip_event['xml'].delegate('resource-collection-car@example.org', 'resource-car-audi-a4@example.org')
itip_event['xml'].set_attendee_participant_status(itip_event['xml'].get_attendee('resource-car-audi-a4@example.org'), "ACCEPTED")
@@ -408,30 +237,3 @@ class TestWallaceResources(unittest.TestCase):
self.assertEqual(ical2['attendee'].params['PARTSTAT'], "DELEGATED")
- def test_007_check_date_conflict(self):
- astart = datetime.datetime(2014,7,13, 10,0,0)
- aend = astart + datetime.timedelta(hours=2)
-
- bstart = datetime.datetime(2014,7,13, 10,0,0)
- bend = astart + datetime.timedelta(hours=1)
- self.assertTrue(module_resources.check_date_conflict(astart, aend, bstart, bend))
-
- bstart = datetime.datetime(2014,7,13, 11,0,0)
- bend = astart + datetime.timedelta(minutes=30)
- self.assertTrue(module_resources.check_date_conflict(astart, aend, bstart, bend))
-
- bend = astart + datetime.timedelta(hours=2)
- self.assertTrue(module_resources.check_date_conflict(astart, aend, bstart, bend))
-
- bstart = datetime.datetime(2014,7,13, 12,0,0)
- bend = astart + datetime.timedelta(hours=1)
- self.assertFalse(module_resources.check_date_conflict(astart, aend, bstart, bend))
-
- bstart = datetime.datetime(2014,6,13, 10,0,0)
- bend = datetime.datetime(2014,6,14, 12,0,0)
- self.assertFalse(module_resources.check_date_conflict(astart, aend, bstart, bend))
-
- bstart = datetime.datetime(2014,7,10, 12,0,0)
- bend = datetime.datetime(2014,7,14, 14,0,0)
- self.assertTrue(module_resources.check_date_conflict(astart, aend, bstart, bend))
-
diff --git a/tests/unit/test-012-wallace_invitationpolicy.py b/tests/unit/test-012-wallace_invitationpolicy.py
new file mode 100644
index 0000000..dbe0713
--- /dev/null
+++ b/tests/unit/test-012-wallace_invitationpolicy.py
@@ -0,0 +1,161 @@
+import os
+import pykolab
+import logging
+import time
+
+from icalendar import Calendar
+from email import message
+from email import message_from_string
+from wallace import module_invitationpolicy as MIP
+from twisted.trial import unittest
+
+from pykolab.auth.ldap import LDAP
+from pykolab.constants import *
+
+
+# define some iTip MIME messages
+
+itip_multipart = """MIME-Version: 1.0
+Content-Type: multipart/mixed;
+ boundary="=_c8894dbdb8baeedacae836230e3436fd"
+From: "Doe, John" <john.doe@example.org>
+Date: Fri, 13 Jul 2012 13:54:14 +0100
+Message-ID: <240fe7ae7e139129e9eb95213c1016d7@example.org>
+User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0
+To: jane.doe@example.org
+Subject: "test" has been updated
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/plain; charset=UTF-8; format=flowed
+Content-Transfer-Encoding: quoted-printable
+
+*test*
+
+--=_c8894dbdb8baeedacae836230e3436fd
+Content-Type: text/calendar; charset=UTF-8; method=REQUEST;
+ name=event.ics
+Content-Disposition: attachment;
+ filename=event.ics
+Content-Transfer-Encoding: quoted-printable
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube Webmail 1.0.1//NONSGML Calendar//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VEVENT
+UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
+DTSTAMP:20120713T125414Z
+DTSTART;TZID=3DEurope/London:20120713T100000
+DTEND;TZID=3DEurope/London:20120713T110000
+SUMMARY:test
+DESCRIPTION:test
+ORGANIZER;CN=3D"Doe, John":mailto:john.doe@example.org
+ATTENDEE;ROLE=3DREQ-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt=
+o:jane.doe@example.org
+ATTENDEE;ROLE=3DOPT-PARTICIPANT;PARTSTAT=3DNEEDS-ACTION;RSVP=3DTRUE:mailt=
+user.external@example.com
+SEQUENCE:1
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+
+--=_c8894dbdb8baeedacae836230e3436fd--
+"""
+
+conf = pykolab.getConf()
+
+if not hasattr(conf, 'defaults'):
+ conf.finalize_conf()
+
+class TestWallaceInvitationpolicy(unittest.TestCase):
+
+ def setUp(self):
+ # monkey-patch the pykolab.auth module to check API calls
+ # without actually connecting to LDAP
+ self.patch(pykolab.auth.Auth, "connect", self._mock_nop)
+ self.patch(pykolab.auth.Auth, "disconnect", self._mock_nop)
+ self.patch(pykolab.auth.Auth, "find_user_dn", self._mock_find_user_dn)
+ self.patch(pykolab.auth.Auth, "get_entry_attributes", self._mock_get_entry_attributes)
+
+ # intercept calls to smtplib.SMTP.sendmail()
+ import smtplib
+ self.patch(smtplib.SMTP, "__init__", self._mock_smtp_init)
+ self.patch(smtplib.SMTP, "quit", self._mock_nop)
+ self.patch(smtplib.SMTP, "sendmail", self._mock_smtp_sendmail)
+
+ self.smtplog = [];
+
+ def _mock_find_user_dn(self, value, kolabuser=False):
+ (prefix, domain) = value.split('@')
+ return "uid=" + prefix + ",ou=People,dc=" + ",dc=".join(domain.split('.'))
+
+ def _mock_get_entry_attributes(self, domain, entry, attributes):
+ (_, uid) = entry.split(',')[0].split('=')
+ return { 'cn': uid, 'mail': uid + "@example.org", '_attrib': attributes }
+
+ def _mock_nop(self, domain=None):
+ pass
+
+ def _mock_smtp_init(self, host=None, port=None, local_hostname=None, timeout=0):
+ pass
+
+ def _mock_smtp_sendmail(self, from_addr, to_addr, message, mail_options=None, rcpt_options=None):
+ self.smtplog.append((from_addr, to_addr, message))
+
+ def test_001_itip_events_from_message(self):
+ itips = pykolab.itip.events_from_message(message_from_string(itip_multipart))
+ self.assertEqual(len(itips), 1, "Multipart iTip message with text/calendar")
+ self.assertEqual(itips[0]['method'], "REQUEST", "iTip request method property")
+ self.assertEqual(len(itips[0]['attendees']), 2, "List attendees from iTip")
+ self.assertEqual(itips[0]['attendees'][0], "mailto:jane.doe@example.org", "First attendee from iTip")
+
+ def test_002_user_dn_from_email_address(self):
+ res = MIP.user_dn_from_email_address("doe@example.org")
+ # assert call to (patched) pykolab.auth.Auth.find_resource()
+ self.assertEqual("uid=doe,ou=People,dc=example,dc=org", res);
+
+ def test_003_get_matching_invitation_policy(self):
+ user = { 'kolabinvitationpolicy': [
+ 'ACT_ACCEPT:example.org',
+ 'ACT_REJECT:gmail.com',
+ 'ACT_MANUAL:*'
+ ] }
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'fastmail.net'), [MIP.ACT_MANUAL])
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'example.org'), [MIP.ACT_ACCEPT,MIP.ACT_MANUAL])
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'gmail.com'), [MIP.ACT_REJECT,MIP.ACT_MANUAL])
+
+ user = { 'kolabinvitationpolicy': ['ACT_ACCEPT:example.org', 'ACT_MANUAL:others'] }
+ self.assertEqual(MIP.get_matching_invitation_policies(user, 'somedomain.net'), [MIP.ACT_MANUAL])
+
+ def test_004_write_locks(self):
+ user = { 'cn': 'John Doe', 'mail': "doe@example.org" }
+
+ lock_key = MIP.get_lock_key(user, '1234567890-abcdef')
+ lock_file = os.path.join(MIP.mybasepath, 'locks', lock_key + '.lock')
+ MIP.set_write_lock(lock_key)
+
+ time.sleep(1)
+ self.assertTrue(os.path.isfile(lock_file))
+ self.assertFalse(MIP.set_write_lock(lock_key, False))
+
+ MIP.remove_write_lock(lock_key)
+ self.assertFalse(os.path.isfile(lock_file))
+
+ def test_005_is_auto_reply(self):
+ all_manual = [ 'ACT_MANUAL' ]
+ accept_none = [ 'ACT_REJECT' ]
+ accept_all = [ 'ACT_ACCEPT', 'ACT_UPDATE' ]
+ accept_cond = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_REJECT_IF_CONFLICT' ]
+ accept_some = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_SAVE_TO_CALENDAR:example.org', 'ACT_REJECT_IF_CONFLICT' ]
+ accept_avail = [ 'ACT_ACCEPT_IF_NO_CONFLICT', 'ACT_REJECT_IF_CONFLICT:example.org' ]
+
+ self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':all_manual }, 'domain.org'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_none }, 'domain.org'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_all }, 'domain.com'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_cond }, 'domain.com'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'domain.com'))
+ self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some }, 'example.org'))
+ self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'domain.com'))
+ self.assertTrue( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'example.org'))
+ \ No newline at end of file
diff --git a/tests/unit/test-015-translate.py b/tests/unit/test-015-translate.py
new file mode 100644
index 0000000..6819b80
--- /dev/null
+++ b/tests/unit/test-015-translate.py
@@ -0,0 +1,23 @@
+import unittest
+import gettext
+from pykolab import translate
+
+class TestTranslate(unittest.TestCase):
+
+ def test_001_default_langs(self):
+ self.assertTrue(len(translate.getDefaultLangs()) > 0)
+
+ def test_002_translate(self):
+ from pykolab.translate import _
+ self.assertEqual(_("Folder name"), "Folder name")
+
+ def test_003_set_lang(self):
+ from pykolab.translate import _
+ self.assertEqual(_("Folder name"), "Folder name")
+ translate.setUserLanguage('de_DE')
+ self.assertEqual(_("Folder name"), "Ordnername", "German Translation found")
+ translate.setUserLanguage('foo_bar')
+ self.assertEqual(_("Folder name"), "Folder name", "Unkonwn language falls back to NullTranslations")
+
+if __name__ == '__main__':
+ unittest.main()