summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2014-08-21 13:46:18 -0400
committerThomas Bruederli <bruederli@kolabsys.com>2014-08-21 13:46:18 -0400
commit44bde53ddb4eadd4fc3fd653fcefe48c39285d5a (patch)
tree47cee9b0d8b3b72df1a813293d37d37cf8254944
parentb05296d7d41c7a42620d996641db9054e9da2f23 (diff)
downloadpykolab-44bde53ddb4eadd4fc3fd653fcefe48c39285d5a.tar.gz
Apply ACT_UPDATE policy on iTip REQUESTs with no re-scheduling (i.e. unchanged sequence number) (#3447)
-rw-r--r--tests/functional/test_wallace/test_007_invitationpolicy.py39
-rw-r--r--wallace/module_invitationpolicy.py110
2 files changed, 99 insertions, 50 deletions
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index dffc9df..8feeff0 100644
--- a/tests/functional/test_wallace/test_007_invitationpolicy.py
+++ b/tests/functional/test_wallace/test_007_invitationpolicy.py
@@ -387,13 +387,15 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
return uid
- def create_calendar_event(self, start=None, summary="test", sequence=0, user=None, attendees=None):
+ def create_calendar_event(self, start=None, summary="test", sequence=0, user=None, attendees=None, folder=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]
+ if folder is None:
+ folder = user['kolabcalendarfolder']
end = start + datetime.timedelta(hours=4)
@@ -419,7 +421,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
imap = IMAP()
imap.connect()
- mailbox = imap.folder_quote(user['kolabcalendarfolder'])
+ mailbox = imap.folder_quote(folder)
imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
imap.imap.m.select(mailbox)
@@ -904,7 +906,34 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction)
- def test_011_task_assignment_accept(self):
+ def test_011_manual_schedule_auto_update(self):
+ self.purge_mailbox(self.john['mailbox'])
+
+ # create an event in john's calendar as it was manually accepted
+ start = datetime.datetime(2014,9,2, 11,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+ uid = self.create_calendar_event(start, user=self.jane, sequence=1, folder=self.john['kolabcalendarfolder'])
+
+ # send update with the same sequence: no re-scheduling
+ templ = itip_invitation.replace("RSVP=TRUE", "RSVP=FALSE").replace("Doe, John", self.jane['displayname']).replace("john.doe@example.org", self.jane['mail'])
+ self.send_itip_update(self.john['mail'], uid, start, summary="test updated", sequence=1, partstat='ACCEPTED', template=templ)
+
+ time.sleep(10)
+ event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
+ self.assertIsInstance(event, pykolab.xml.Event)
+ self.assertEqual(event.get_summary(), "test updated")
+ self.assertEqual(event.get_attendee(self.john['mail']).get_participant_status(), kolabformat.PartAccepted)
+
+ # this should also trigger an update notification
+ notification = self.check_message_received(_('"%s" has been updated') % ('test updated'), self.jane['mail'], mailbox=self.john['mailbox'])
+ self.assertIsInstance(notification, email.message.Message)
+
+ # send outdated update: should not be saved
+ self.send_itip_update(self.john['mail'], uid, start, summary="old test", sequence=0, partstat='NEEDS-ACTION', template=templ)
+ notification = self.check_message_received(_('"%s" has been updated') % ('old test'), self.jane['mail'], mailbox=self.john['mailbox'])
+ self.assertEqual(notification, None)
+
+
+ def test_020_task_assignment_accept(self):
start = datetime.datetime(2014,9,10, 19,0,0)
uid = self.send_itip_invitation(self.jane['mail'], start, summary='work', template=itip_todo)
@@ -928,7 +957,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
self.assertEqual(todo.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
- def test_012_task_assignment_reply(self):
+ def test_021_task_assignment_reply(self):
self.purge_mailbox(self.john['mailbox'])
due = datetime.datetime(2014,9,12, 14,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
@@ -958,7 +987,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
self.assertIn(participant_status_label(partstat), notification_text)
- def test_013_task_cancellation(self):
+ def test_022_task_cancellation(self):
uid = self.send_itip_invitation(self.jane['mail'], summary='more work', template=itip_todo)
time.sleep(10)
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index e753c38..9ba1490 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -403,6 +403,26 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
# TODO: delegate (but to whom?)
return None
+ # auto-update changes if enabled for this user
+ elif policy & ACT_UPDATE and existing:
+ # compare sequence number to avoid outdated updates
+ if not itip_event['sequence'] == existing.get_sequence():
+ log.info(_("The iTip request sequence (%r) doesn't match the referred object version (%r). Ignoring.") % (
+ itip_event['sequence'], existing.get_sequence()
+ ))
+ return None
+
+ log.debug(_("Auto-updating %s %r on iTip REQUEST (no re-scheduling)") % (existing.type, existing.uid), level=8)
+ save_object = True
+
+ # retain task status and percent-complete properties from my old copy
+ if is_task:
+ itip_event['xml'].set_status(existing.get_status())
+ itip_event['xml'].set_percentcomplete(existing.get_percentcomplete())
+
+ if policy & COND_NOTIFY:
+ send_update_notification(itip_event['xml'], receiving_user, False)
+
# if RSVP, send an iTip REPLY
if rsvp or scheduling_required:
# set attendee's CN from LDAP record if yet missing
@@ -424,10 +444,6 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
# policy doesn't match, pass on to next one
return None
- else:
- log.debug(_("No RSVP for recipient %r requested") % (receiving_user['mail']), level=8)
- # TODO: only update if policy & ACT_UPDATE ?
-
if save_object:
targetfolder = None
@@ -517,7 +533,7 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
# update the organizer's copy of the object
if update_object(existing, receiving_user):
if policy & COND_NOTIFY:
- send_reply_notification(existing, receiving_user)
+ send_update_notification(existing, receiving_user, True)
# update all other attendee's copies
if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
@@ -931,7 +947,7 @@ def delete_object(existing):
imap.imap.m.expunge()
-def send_reply_notification(object, receiving_user):
+def send_update_notification(object, receiving_user, reply=True):
"""
Send a (consolidated) notification about the current participant status to organizer
"""
@@ -941,52 +957,56 @@ def send_reply_notification(object, receiving_user):
from email.MIMEText import MIMEText
from email.Utils import formatdate
- log.debug(_("Compose participation status summary for %s %r to user %r") % (
- object.type, object.uid, receiving_user['mail']
- ), level=8)
-
organizer = object.get_organizer()
orgemail = organizer.email()
orgname = organizer.name()
- auto_replies_expected = 0
- auto_replies_received = 0
- partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'IN-PROCESS':[], 'COMPLETED':[], 'PENDING':[] }
- for attendee in object.get_attendees():
- parstat = attendee.get_participant_status(True)
- if partstats.has_key(parstat):
- partstats[parstat].append(attendee.get_displayname())
- else:
- partstats['PENDING'].append(attendee.get_displayname())
+ if reply:
+ log.debug(_("Compose participation status summary for %s %r to user %r") % (
+ object.type, object.uid, receiving_user['mail']
+ ), level=8)
- # look-up kolabinvitationpolicy for this attendee
- if attendee.get_cutype() == kolabformat.CutypeResource:
- resource_dns = auth.find_resource(attendee.get_email())
- if isinstance(resource_dns, list):
- attendee_dn = resource_dns[0] if len(resource_dns) > 0 else None
+ auto_replies_expected = 0
+ auto_replies_received = 0
+ partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'IN-PROCESS':[], 'COMPLETED':[], 'PENDING':[] }
+ for attendee in object.get_attendees():
+ parstat = attendee.get_participant_status(True)
+ if partstats.has_key(parstat):
+ partstats[parstat].append(attendee.get_displayname())
else:
- attendee_dn = resource_dns
- else:
- attendee_dn = user_dn_from_email_address(attendee.get_email())
-
- if attendee_dn:
- attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy'])
- if is_auto_reply(attendee_rec, orgemail, object.type):
- auto_replies_expected += 1
- if not parstat == 'NEEDS-ACTION':
- auto_replies_received += 1
-
- # skip notification until we got replies from all automatically responding attendees
- if auto_replies_received < auto_replies_expected:
- log.debug(_("Waiting for more automated replies (got %d of %d); skipping notification") % (
- auto_replies_received, auto_replies_expected
- ), level=8)
- return
+ partstats['PENDING'].append(attendee.get_displayname())
- roundup = ''
- for status,attendees in partstats.iteritems():
- if len(attendees) > 0:
- roundup += "\n" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n"
+ # look-up kolabinvitationpolicy for this attendee
+ if attendee.get_cutype() == kolabformat.CutypeResource:
+ resource_dns = auth.find_resource(attendee.get_email())
+ if isinstance(resource_dns, list):
+ attendee_dn = resource_dns[0] if len(resource_dns) > 0 else None
+ else:
+ attendee_dn = resource_dns
+ else:
+ attendee_dn = user_dn_from_email_address(attendee.get_email())
+
+ if attendee_dn:
+ attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy'])
+ if is_auto_reply(attendee_rec, orgemail, object.type):
+ auto_replies_expected += 1
+ if not parstat == 'NEEDS-ACTION':
+ auto_replies_received += 1
+
+ # skip notification until we got replies from all automatically responding attendees
+ if auto_replies_received < auto_replies_expected:
+ log.debug(_("Waiting for more automated replies (got %d of %d); skipping notification") % (
+ auto_replies_received, auto_replies_expected
+ ), level=8)
+ return
+
+ roundup = ''
+ for status,attendees in partstats.iteritems():
+ if len(attendees) > 0:
+ roundup += "\n" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n"
+ else:
+ # TODO: compose a diff of changes to previous version
+ roundup = "\n" + _("Minor changes submitted by %s have been automatically applied.") % (orgname if orgname else orgemail)
# compose different notification texts for events/tasks
if object.type == 'task':