diff options
-rw-r--r-- | tests/functional/test_wallace/test_005_resource_add.py | 58 | ||||
-rw-r--r-- | tests/functional/test_wallace/test_005_resource_invitation.py | 231 | ||||
-rw-r--r-- | wallace/module_resources.py | 50 |
3 files changed, 252 insertions, 87 deletions
diff --git a/tests/functional/test_wallace/test_005_resource_add.py b/tests/functional/test_wallace/test_005_resource_add.py new file mode 100644 index 0000000..2de60fb --- /dev/null +++ b/tests/functional/test_wallace/test_005_resource_add.py @@ -0,0 +1,58 @@ +import time +import pykolab + +from pykolab import wap_client +from pykolab.auth import Auth +from pykolab.imap import IMAP +from wallace import module_resources +from twisted.trial import unittest + +import tests.functional.resource_func as funcs + +conf = pykolab.getConf() + +class TestResourceAdd(unittest.TestCase): + + @classmethod + def setUp(self): + from tests.functional.purge_users import purge_users + #purge_users() + + self.john = { + 'local': 'john.doe', + 'domain': 'example.org' + } + + from tests.functional.user_add import user_add + #user_add("John", "Doe") + + 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'] ]) + + from tests.functional.synchronize import synchronize_once + synchronize_once() + + @classmethod + def tearDown(self): + from tests.functional.purge_users import purge_users + #funcs.purge_resources() + #purge_users() + + def test_001_resource_created(self): + resource = module_resources.resource_record_from_email_address(self.audi['mail']) + self.assertEqual(len(resource), 1) + self.assertEqual(resource[0], self.audi['dn']) + + collection = module_resources.resource_record_from_email_address(self.cars['mail']) + self.assertEqual(len(collection), 1) + self.assertEqual(collection[0], self.cars['dn']) + + def test_002_resource_collection(self): + auth = Auth() + auth.connect() + attrs = auth.get_entry_attributes(None, self.cars['dn'], ['*']) + self.assertIn('groupofuniquenames', attrs['objectclass']) + self.assertEqual(len(attrs['uniquemember']), 3) diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py index 8d6803d..5afde3c 100644 --- a/tests/functional/test_wallace/test_005_resource_invitation.py +++ b/tests/functional/test_wallace/test_005_resource_invitation.py @@ -18,27 +18,7 @@ import tests.functional.resource_func as funcs conf = pykolab.getConf() -itip_invitation = """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> -User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0 -To: %s -Subject: "test" has been created - ---=_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: 8bit - +itip_invitation = """ BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN @@ -56,30 +36,9 @@ ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:ma TRANSP:OPAQUE END:VEVENT END:VCALENDAR ---=_c8894dbdb8baeedacae836230e3436fd-- """ -itip_update = """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> -User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0 -To: %s -Subject: "test" has been updated - ---=_c8894dbdb8baeedacae836230e3436fd -Content-Type: text/plain; charset=UTF-8; format=flowed -Content-Transfer-Encoding: quoted-printable - -*test* updated - ---=_c8894dbdb8baeedacae836230e3436fd -Content-Type: text/calendar; charset=UTF-8; method=REQUEST; name=event.ics -Content-Disposition: attachment; filename=event.ics -Content-Transfer-Encoding: 8bit - +itip_update = """ BEGIN:VCALENDAR VERSION:2.0 PRODID:-//Roundcube Webmail 0.9-0.3.el6.kolab_3.0//NONSGML Calendar//EN @@ -98,18 +57,35 @@ ATTENDEE;ROLE=REQ-PARTICIPANT;CUTYPE=RESOURCE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:ma TRANSP:OPAQUE END:VEVENT END:VCALENDAR ---=_c8894dbdb8baeedacae836230e3436fd-- """ -itip_cancellation = """Return-Path: <john.doe@example.org> -Content-Type: text/calendar; method=CANCEL; charset=UTF-8 -Content-Transfer-Encoding: quoted-printable -To: %s -From: john.doe@example.org -Date: Mon, 24 Feb 2014 11:27:28 +0100 -Message-ID: <1a3aa8995e83dd24cf9247e538ac91ff@example.org> -Subject: "test" cancelled +itip_delegated = """ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Roundcube//Roundcube libcalendaring 1.0-git//Sabre//Sabre VObject + 2.1.3//EN +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VEVENT +UID:%s +DTSTAMP;VALUE=DATE-TIME:20140227T141939Z +DTSTART;VALUE=DATE-TIME;TZID=Europe/London:%s +DTEND;VALUE=DATE-TIME;TZID=Europe/London:%s +SUMMARY:test +SEQUENCE:4 +ATTENDEE;CN=Company Cars;PARTSTAT=DELEGATED;ROLE=NON-PARTICIPANT;CUTYPE=IND + IVIDUAL;RSVP=TRUE;DELEGATED-TO=resource-car-audia4@example.org:mailto:reso + urce-collection-companycars@example.org +ATTENDEE;CN=Audi A4;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUA + L;RSVP=TRUE;DELEGATED-FROM=resource-collection-companycars@example.org:mai + lto:resource-car-audia4@example.org +ORGANIZER;CN=:mailto:john.doe@example.org +DESCRIPTION:Sent to %s +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 @@ -118,19 +94,65 @@ METHOD:CANCEL BEGIN:VEVENT UID:%s DTSTAMP:20140218T1254140 -DTSTART;TZID=3DEurope/London:20120713T100000 -DTEND;TZID=3DEurope/London:20120713T110000 +DTSTART;TZID=Europe/London:20120713T100000 +DTEND;TZID=Europe/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:%s +ORGANIZER;CN="Doe, John":mailto:john.doe@example.org +ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;RSVP=TRUE:mailt= + o:%s TRANSP:OPAQUE SEQUENCE:3 END:VEVENT END:VCALENDAR """ +itip_allday = """ +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:%s +DTSTAMP:20140213T1254140 +DTSTART;VALUE=DATE:%s +DTEND;VALUE=DATE:%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 +TRANSP:OPAQUE +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> +User-Agent: Roundcube Webmail/0.9-0.3.el6.kolab_3.0 +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=REQUEST; name=event.ics +Content-Disposition: attachment; filename=event.ics +Content-Transfer-Encoding: 8bit + +%s +--=_c8894dbdb8baeedacae836230e3436fd-- +""" + class TestResourceInvitation(unittest.TestCase): john = None @@ -167,37 +189,44 @@ class TestResourceInvitation(unittest.TestCase): from tests.functional.synchronize import synchronize_once synchronize_once() - def send_message(self, msg_source, to_addr, from_addr=None): + def send_message(self, itip_payload, to_addr, from_addr=None): if from_addr is None: from_addr = self.john['mail'] smtp = smtplib.SMTP('localhost', 10026) - smtp.sendmail(from_addr, to_addr, msg_source) + smtp.sendmail(from_addr, to_addr, mime_message % (to_addr, itip_payload)) - def send_itip_invitation(self, resource_email, start=None): + def send_itip_invitation(self, resource_email, start=None, allday=False): if start is None: start = datetime.datetime.now() uid = str(uuid.uuid4()) - end = start + datetime.timedelta(hours=4) - self.send_message(itip_invitation % ( - resource_email, + + if allday: + template = itip_allday + end = start + datetime.timedelta(days=1) + date_format = '%Y%m%d' + else: + end = start + datetime.timedelta(hours=4) + template = itip_invitation + date_format = '%Y%m%dT%H%M%S' + + self.send_message(template % ( uid, - start.strftime('%Y%m%dT%H%M%S'), - end.strftime('%Y%m%dT%H%M%S'), + start.strftime(date_format), + end.strftime(date_format), resource_email ), resource_email) return uid - def send_itip_update(self, resource_email, uid, start=None): + def send_itip_update(self, resource_email, uid, start=None, template=None): if start is None: start = datetime.datetime.now() end = start + datetime.timedelta(hours=4) - self.send_message(itip_update % ( - resource_email, + self.send_message((template if template is not None else itip_update) % ( uid, start.strftime('%Y%m%dT%H%M%S'), end.strftime('%Y%m%dT%H%M%S'), @@ -209,7 +238,6 @@ class TestResourceInvitation(unittest.TestCase): def send_itip_cancel(self, resource_email, uid): self.send_message(itip_cancellation % ( - resource_email, uid, resource_email ), @@ -290,6 +318,18 @@ class TestResourceInvitation(unittest.TestCase): imap.imap.m.expunge() imap.disconnect() + time.sleep(1) + + + 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 + return resource def test_001_resource_from_email_address(self): @@ -331,7 +371,7 @@ class TestResourceInvitation(unittest.TestCase): accept = self.check_message_received("Reservation Request for test was ACCEPTED") self.assertIsInstance(accept, email.message.Message) - delegatee = self.passat if accept['from'].find(self.passat['mail']) >= 0 else self.boxter + delegatee = self.find_resource_by_email(accept['from']) self.assertIn(delegatee['mail'], accept['from']) # check booking in the delegatee's resource calendar @@ -343,13 +383,15 @@ class TestResourceInvitation(unittest.TestCase): def test_005_rescheduling_reservation(self): - uid = self.send_itip_invitation(self.audi['mail'], datetime.datetime(2014,5,1, 10,0,0)) + self.purge_mailbox(self.john['mailbox']) + + 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']) self.assertIsInstance(response, email.message.Message) self.purge_mailbox(self.john['mailbox']) - self.send_itip_update(self.audi['mail'], uid, datetime.datetime(2014,5,1, 12,0,0)) # conflict with myself + 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']) self.assertIsInstance(response, email.message.Message) @@ -361,6 +403,8 @@ class TestResourceInvitation(unittest.TestCase): def test_006_cancelling_revervation(self): + self.purge_mailbox(self.john['mailbox']) + uid = self.send_itip_invitation(self.boxter['mail'], datetime.datetime(2014,5,1, 10,0,0)) self.assertIsInstance(self.check_resource_calendar_event(self.boxter['kolabtargetfolder'], uid), pykolab.xml.Event) @@ -374,3 +418,46 @@ class TestResourceInvitation(unittest.TestCase): response = self.check_message_received("Reservation Request for test was ACCEPTED", self.boxter['mail']) self.assertIsInstance(response, email.message.Message) + + + def test_007_update_delegated(self): + self.purge_mailbox(self.john['mailbox']) + + dt = datetime.datetime(2014,8,1, 12,0,0) + uid = self.send_itip_invitation(self.cars['mail'], dt) + + # wait for accept notification + accept = self.check_message_received("Reservation Request for test was ACCEPTED") + self.assertIsInstance(accept, email.message.Message) + delegatee = self.find_resource_by_email(accept['from']) + + # send update message to all attendees (collection and delegatee) + self.purge_mailbox(self.john['mailbox']) + update_template = itip_delegated.replace("resource-car-audia4@example.org", delegatee['mail']) + self.send_itip_update(self.cars['mail'], uid, dt, template=update_template) + 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") + 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) + + + def test_008_allday_reservation(self): + self.purge_mailbox(self.john['mailbox']) + + 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") + self.assertIsInstance(accept, email.message.Message) + + event = self.check_resource_calendar_event(self.audi['kolabtargetfolder'], uid) + self.assertIsInstance(event, pykolab.xml.Event) + 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']) + self.assertIsInstance(response, email.message.Message) diff --git a/wallace/module_resources.py b/wallace/module_resources.py index b718ac4..6aaf64a 100644 --- a/wallace/module_resources.py +++ b/wallace/module_resources.py @@ -35,6 +35,7 @@ from email.utils import getaddresses import modules import pykolab +import kolabformat from pykolab.auth import Auth from pykolab.conf import Conf @@ -229,19 +230,31 @@ def execute(*args, **kw): else: resources[resource_dn] = resource_attrs - log.debug(_("Resources: %r, %r") % (resource_dns, resources), level=8) + log.debug(_("Resources: %r; %r") % (resource_dns, resources), level=8) - # process CANCEL messages done = False + receiving_resource = resources[resource_dns[0]] + for itip_event in itip_events: - if itip_event['method'] == "CANCEL": + try: + receiving_attendee = itip_event['xml'].get_attendee_by_email(receiving_resource['mail']) + log.debug(_("Receiving Resource: %r; %r") % (receiving_resource, receiving_attendee), level=9) + except Exception, e: + log.error("Could not find envelope attendee: %r" % (e)) + continue + + # ignore updates and cancellations to resource collections who already delegated the event + if receiving_attendee.get_delegated_to().size() > 0 or receiving_attendee.get_role() == kolabformat.NonParticipant: + done = True + log.debug(_("Recipient %r is non-participant, ignoring message") % (receiving_resource['mail']), level=8) + + # process CANCEL messages + if not done and itip_event['method'] == "CANCEL": for resource in resource_dns: if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]: delete_resource_event(itip_event['uid'], resources[resource]) - # TODO: handle cancellations sent to resource collections. Really? - done = True if done: @@ -263,14 +276,14 @@ def execute(*args, **kw): # sets the 'conflicting' flag and adds a list of conflicting events found try: - read_resource_calendar(resources[resource], itip_events) + num_messages = read_resource_calendar(resources[resource], itip_events) except Exception, e: log.error(_("Failed to read resource calendar for %r: %r") % (resource, e)) continue end = time.time() - log.debug(_("start: %r, end: %r, total: %r") % (start, end, (end-start)), level=1) + log.debug(_("start: %r, end: %r, total: %r, messages: %d") % (start, end, (end-start), num_messages), level=9) # For each resource (collections are first!) @@ -335,7 +348,7 @@ def execute(*args, **kw): for uid in resources[resource]['existing_events']: delete_resource_event(uid, resources[resource]) - log.debug(_("Accept invitation for individual resource %r / %r") % (resource, resources[resource]['mail']), level=9) + log.debug(_("Accept invitation for individual resource %r / %r") % (resource, resources[resource]['mail']), level=8) accept_reservation_request(itip_event, resources[resource]) done = True @@ -349,7 +362,7 @@ def execute(*args, **kw): # Randomly selects a target resource from the resource collection. _target_resource = resources[original_resource['uniquemember'][random.randint(0,(len(original_resource['uniquemember'])-1))]] - log.debug(_("Delegate invitation for resource collection %r to %r") % (original_resource['mail'], _target_resource['mail']), level=9) + log.debug(_("Delegate invitation for resource collection %r to %r") % (original_resource['mail'], _target_resource['mail']), level=8) if original_resource['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]: # @@ -395,6 +408,8 @@ def read_resource_calendar(resource_rec, itip_events): imap.imap.m.select(mailbox) typ, data = imap.imap.m.search(None, 'ALL') + num_messages = len(data[0].split()) + for num in data[0].split(): # For efficiency, makes the routine non-deterministic if resource_rec['conflict']: @@ -456,7 +471,7 @@ def read_resource_calendar(resource_rec, itip_events): resource_rec['conflicting_events'].append(event.get_uid()) resource_rec['conflict'] = True - return resource_rec['conflict'] + return num_messages def accept_reservation_request(itip_event, resource, delegator=None): @@ -570,7 +585,7 @@ def itip_events_from_message(message): # Get the itip_payload itip_payload = part.get_payload(decode=True) - log.debug(_("Raw iTip payload: %s") % (itip_payload)) + log.debug(_("Raw iTip payload: %s") % (itip_payload), level=9) # Python iCalendar prior to 3.0 uses "from_string". if hasattr(icalendar.Calendar, 'from_ical'): @@ -588,13 +603,14 @@ def itip_events_from_message(message): itip = {} if c['uid'] in seen_uids: - log.debug(_("Duplicate iTip event: %s") % (c['uid'])) + log.debug(_("Duplicate iTip event: %s") % (c['uid']), level=9) continue # From the event, take the following properties: # # - method # - uid + # - sequence # - start # - end (if any) # - duration (if any) @@ -607,6 +623,7 @@ def itip_events_from_message(message): itip['uid'] = str(c['uid']) itip['method'] = str(cal['method']).upper() + itip['sequence'] = int(c['sequence']) if c.has_key('sequence') else 0 if c.has_key('dtstart'): itip['start'] = c['dtstart'] @@ -629,7 +646,12 @@ def itip_events_from_message(message): itip['resources'] = c['resources'] itip['raw'] = itip_payload - itip['xml'] = event_from_ical(c.to_ical()) + + try: + itip['xml'] = event_from_ical(c.to_ical()) + except Exception, e: + log.error("event_from_ical() exception: %r" % (e)) + continue itip_events.append(itip) @@ -798,8 +820,6 @@ def resource_records_from_itip_events(itip_events, recipient_email=None): log.debug(_("The following resources are being referred to in the " + \ "iTip: %r") % (resource_records), level=8) - auth.disconnect() - return resource_records |