summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2014-08-05 00:53:26 -0400
committerThomas Bruederli <bruederli@kolabsys.com>2014-08-05 00:53:26 -0400
commita8555e3e8789fd02b7d5749ea4fc51b84e57285f (patch)
tree2ec2fc25c90471055dbd60b3b45c32399777c251
parent78b688519c8b73d66ab1f4fba74ab39acdf9552f (diff)
downloadpykolab-a8555e3e8789fd02b7d5749ea4fc51b84e57285f.tar.gz
Improve resource confirmation workflow:
- Use base64 encoding for original event UIDs - Compare sequence number on resource owner replies - Added confirmation test scenario with reservation update and outdated replies
-rw-r--r--tests/functional/test_wallace/test_005_resource_invitation.py77
-rw-r--r--tests/functional/test_wallace/test_007_invitationpolicy.py2
-rw-r--r--wallace/module_resources.py42
3 files changed, 104 insertions, 17 deletions
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index c21f12c..61b9402 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -239,11 +239,12 @@ class TestResourceInvitation(unittest.TestCase):
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
@@ -393,6 +394,7 @@ 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))
@@ -670,6 +672,7 @@ class TestResourceInvitation(unittest.TestCase):
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')
@@ -705,6 +708,74 @@ class TestResourceInvitation(unittest.TestCase):
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 removed from resource calendar
+ # 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
index 1af22ff..9bc808f 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -598,7 +598,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
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(), 'CANCELLED')
+ self.assertEqual(event.get_status(True), 'CANCELLED')
self.assertTrue(event.get_transparency())
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index f7c9441..47259da 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -25,7 +25,7 @@ import random
import tempfile
import time
from urlparse import urlparse
-import urllib
+import base64
import uuid
import re
@@ -204,9 +204,10 @@ def execute(*args, **kw):
for recipient in recipients:
# extract reference UID from recipients like resource+UID@domain.org
- if re.match('.+\+[A-Za-z0-9%/_-]+@', recipient):
+ if re.match('.+\+[A-Za-z0-9=/-]+@', recipient):
(prefix, host) = recipient.split('@')
- (local, reference_uid) = prefix.split('+')
+ (local, uid) = prefix.split('+')
+ reference_uid = base64.b64decode(uid, '-/')
recipient = local + '@' + host
if not len(resource_record_from_email_address(recipient)) == 0:
@@ -268,6 +269,13 @@ def execute(*args, **kw):
log.error("Could not find envelope sender attendee: %r" % (e))
continue
+ # compare sequence number to avoid outdated replies
+ if not itip_event['sequence'] == event.get_sequence():
+ log.info(_("The iTip reply sequence (%r) doesn't match the referred event version (%r). Ignoring.") % (
+ itip_event['sequence'], event.get_sequence()
+ ))
+ continue
+
# forward owner response comment
comment = itip_event['xml'].get_comment()
if comment:
@@ -276,10 +284,12 @@ def execute(*args, **kw):
itip_event_ = dict(xml=event, uid=event.get_uid())
if owner_reply == kolabformat.PartAccepted:
+ event.set_status(kolabformat.StatusConfirmed)
accept_reservation_request(itip_event_, receiving_resource, confirmed=True)
elif owner_reply == kolabformat.PartDeclined:
decline_reservation_request(itip_event_, receiving_resource)
- # TODO: set partstat=DECLINED and status=CANCELLED instead of deleting?
+ # TODO: set status=CANCELLED instead of deleting?
+ # event.set_status(kolabformat.StatusCancelled)
delete_resource_event(reference_uid, receiving_resource)
else:
log.info("Invalid response (%r) recieved from resource owner for event %r" % (
@@ -288,6 +298,9 @@ def execute(*args, **kw):
else:
log.info(_("Event referenced by this REPLY (%r) not found in resource calendar") % (reference_uid))
+ else:
+ log.info(_("No event reference found in this REPLY. Ignoring."))
+
# exit for-loop
break
@@ -615,16 +628,13 @@ def accept_reservation_request(itip_event, resource, delegator=None, confirmed=F
partstat = 'TENTATIVE' if confirmation_required else 'ACCEPTED'
+ itip_event['xml'].set_transparency(False);
itip_event['xml'].set_attendee_participant_status(
itip_event['xml'].get_attendee_by_email(resource['mail']),
partstat
)
- # remove old copy of the reservation
- if confirmed:
- delete_resource_event(itip_event['uid'], resource)
-
- saved = save_resource_event(itip_event, resource)
+ saved = save_resource_event(itip_event, resource, replace=confirmed)
log.debug(
_("Adding event to %r: %r") % (resource['kolabtargetfolder'], saved),
@@ -658,14 +668,20 @@ def decline_reservation_request(itip_event, resource):
send_owner_notification(resource, owner, itip_event, True)
-def save_resource_event(itip_event, resource):
+def save_resource_event(itip_event, resource, replace=False):
"""
Append the given event object to the resource's calendar
"""
try:
# Administrator login name comes from configuration.
targetfolder = imap.folder_quote(resource['kolabtargetfolder'])
- imap.imap.m.setacl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda")
+
+ # remove old copy of the reservation (also sets ACLs)
+ if replace:
+ delete_resource_event(itip_event['uid'], resource)
+ else:
+ imap.imap.m.setacl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda")
+
result = imap.imap.m.append(
targetfolder,
None,
@@ -788,7 +804,7 @@ def resource_records_from_itip_events(itip_events, recipient_email=None):
log.debug(_("Raw set of resources: %r") % (resources_raw), level=9)
# consider organizer (in REPLY messages), too
- organizers_raw = [re.sub('\+[A-Za-z0-9%/_-]+@', '@', str(y['organizer'])) for y in itip_events if y.has_key('organizer')]
+ organizers_raw = [re.sub('\+[A-Za-z0-9=/-]+@', '@', str(y['organizer'])) for y in itip_events if y.has_key('organizer')]
log.debug(_("Raw set of organizers: %r") % (organizers_raw), level=8)
@@ -1159,7 +1175,7 @@ def send_owner_confirmation(resource, owner, itip_event):
# generate new UID and set the resource as organizer
(mail, domain) = resource['mail'].split('@')
event.set_uid(str(uuid.uuid4()))
- event.set_organizer(mail + '+' + urllib.quote(uid) + '@' + domain, resource['cn'])
+ event.set_organizer(mail + '+' + base64.b64encode(uid, '-/') + '@' + domain, resource['cn'])
itip_event['uid'] = event.get_uid()
# add resource owner as (the sole) attendee