diff options
-rw-r--r-- | tests/functional/test_wallace/test_007_invitationpolicy.py | 63 | ||||
-rw-r--r-- | wallace/module_invitationpolicy.py | 55 |
2 files changed, 105 insertions, 13 deletions
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py index 0d2875c..2b669ff 100644 --- a/tests/functional/test_wallace/test_007_invitationpolicy.py +++ b/tests/functional/test_wallace/test_007_invitationpolicy.py @@ -177,6 +177,11 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ACT_SAVE_TO_CALENDAR','ACT_UPDATE'] } + 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']) @@ -239,7 +244,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase): return uid - def send_itip_reply(self, uid, attendee_email, mailto, start=None, template=None, summary="test", sequence=1, partstat='ACCEPTED'): + 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() @@ -586,3 +591,59 @@ class TestWallaceInvitationpolicy(unittest.TestCase): self.assertIn(self.jack['mail'], notification_text) self.assertNotIn(_("PENDING"), notification_text) + + 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) + + # send replies from jack and jane + # FIXME: replies should not be necessary if auto-replies get through wallace as well + self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=start, partstat='ACCEPTED') + time.sleep(10) # FIXME: implement locking in wallace + self.send_itip_reply(uid, self.jack['mail'], self.john['mail'], start=start, partstat='TENTATIVE') + + # 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 + 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) diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py index 41917da..9488dd1 100644 --- a/wallace/module_invitationpolicy.py +++ b/wallace/module_invitationpolicy.py @@ -316,7 +316,7 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece condition_fulfilled = True # find existing event in user's calendar - existing = find_existing_event(itip_event, receiving_user) + existing = find_existing_event(itip_event['uid'], receiving_user) # compare sequence number to determine a (re-)scheduling request if existing is not None: @@ -406,14 +406,20 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv return MESSAGE_FORWARD # find existing event in user's calendar - existing = find_existing_event(itip_event, receiving_user) + # TODO: set/check lock to avoid concurrent wallace processes trying to update the same event simultaneously + existing = find_existing_event(itip_event['uid'], receiving_user) if existing: - log.debug(_("Auto-updating event %r on iTip REPLY") % (existing.uid), level=8) + # compare sequence number to avoid outdated replies? + if not itip_event['sequence'] == existing.get_sequence(): + log.info(_("The iTip reply sequence (%r) doesn't match the referred event version (%r). Forwarding to Inbox.") % ( + itip_event['sequence'], existing.get_sequence() + )) + return MESSAGE_FORWARD - # TODO: compare sequence number to avoid outdated replies? + log.debug(_("Auto-updating event %r on iTip REPLY") % (existing.uid), level=8) try: - existing.set_attendee_participant_status(sender_email, sender_attendee.get_participant_status()) + existing.set_attendee_participant_status(sender_email, sender_attendee.get_participant_status(), False) except Exception, e: log.error("Could not find corresponding attende in organizer's event: %r" % (e)) @@ -425,7 +431,10 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv if policy & COND_NOTIFY: send_reply_notification(existing, receiving_user) - # TODO: update all other attendee's copies if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'): + # update all other attendee's copies + if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'): + propagate_changes_to_attendees_calendars(existing) + return MESSAGE_PROCESSED else: @@ -448,7 +457,7 @@ def process_itip_cancel(itip_event, policy, recipient_email, sender_email, recei # auto-update the local copy with STATUS=CANCELLED if policy & ACT_UPDATE: # find existing event in user's calendar - existing = find_existing_event(itip_event, receiving_user) + existing = find_existing_event(itip_event['uid'], receiving_user) if existing: existing.set_status('CANCELLED') @@ -606,7 +615,7 @@ def list_user_calendars(user_rec): return calendars -def find_existing_event(itip_event, user_rec): +def find_existing_event(uid, user_rec): """ Search user's calendar folders for the given event (by UID) """ @@ -614,10 +623,10 @@ def find_existing_event(itip_event, user_rec): event = None for folder in list_user_calendars(user_rec): - log.debug(_("Searching folder %r for event %r") % (folder, itip_event['uid']), level=8) + log.debug(_("Searching folder %r for event %r") % (folder, uid), level=8) imap.imap.m.select(imap.folder_utf7(folder)) - typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (itip_event['uid'])) + typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid)) for num in reversed(data[0].split()): typ, data = imap.imap.m.fetch(num, '(RFC822)') @@ -628,7 +637,7 @@ def find_existing_event(itip_event, user_rec): log.error(_("Failed to parse event from message %s/%s: %r") % (folder, num, e)) continue - if event and event.uid == itip_event['uid']: + if event and event.uid == uid: return event return event @@ -660,7 +669,6 @@ def check_availability(itip_event, receiving_user): try: event = event_from_message(message_from_string(data[0][1])) - setattr(event, '_imap_folder', folder) except Exception, e: log.error(_("Failed to parse event from message %s/%s: %r") % (folder, num, e)) continue @@ -811,6 +819,29 @@ def send_reply_notification(event, receiving_user): smtp.quit() +def propagate_changes_to_attendees_calendars(event): + """ + Find and update copies of this event in all attendee's calendars + """ + for attendee in event.get_attendees(): + attendee_user_dn = user_dn_from_email_address(attendee.get_email()) + if attendee_user_dn is not None: + log.debug(_("Update attendee copy of %r") % (attendee_user_dn), level=9) + + attendee_user = auth.get_entry_attributes(None, attendee_user_dn, ['*']) + attendee_event = find_existing_event(event.uid, attendee_user) # does IMAP authenticate + if attendee_event: + attendee_event.event.setAttendees(event.get_attendees()) + success = update_event(attendee_event, attendee_user) + log.debug(_("Updated %s's copy of %r: %r") % (attendee_user['mail'], event.uid, success), level=8) + + else: + log.debug(_("Attendee %s's copy of %r not found") % (attendee_user['mail'], event.uid), level=8) + + else: + log.debug(_("Attendee %r not found in LDAP") % (attendee.get_email()), level=8) + + def invitation_response_text(): return _(""" %(name)s has %(status)s your invitation for %(summary)s. |