diff options
author | Thomas Bruederli <bruederli@kolabsys.com> | 2014-07-10 05:09:11 -0400 |
---|---|---|
committer | Thomas Bruederli <bruederli@kolabsys.com> | 2014-07-10 05:09:11 -0400 |
commit | c395789e553531a4a565bcc61a422255ef5385b4 (patch) | |
tree | 23c6402afd3af5dbf57fce05504a5bd6cde5c686 | |
parent | d13dd3849d09d1eaa4fdd906686cb1217cc23ad4 (diff) | |
download | pykolab-c395789e553531a4a565bcc61a422255ef5385b4.tar.gz |
Send consolidated update notifications to an event organizer. This means suppressing notifications triggered by wallace replies as long as more automated replies can be expected; Use localized participant status texts in iTip messages
-rw-r--r-- | tests/functional/test_wallace/test_007_invitationpolicy.py | 48 | ||||
-rw-r--r-- | tests/unit/test-012-wallace_invitationpolicy.py | 16 | ||||
-rw-r--r-- | wallace/module_invitationpolicy.py | 82 |
3 files changed, 123 insertions, 23 deletions
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py index 8dc3ff7..dd419f0 100644 --- a/tests/functional/test_wallace/test_007_invitationpolicy.py +++ b/tests/functional/test_wallace/test_007_invitationpolicy.py @@ -12,6 +12,7 @@ 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 @@ -177,6 +178,16 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ACT_SAVE_TO_CALENDAR','ACT_UPDATE'] } + self.bob = { + 'displayname': 'Bob Auto', + 'mail': 'bob.auto@example.org', + 'dn': 'uid=auto,ou=People,dc=example,dc=org', + 'preferredlanguage': 'en_US', + 'mailbox': 'user/bob.auto@example.org', + 'kolabtargetfolder': 'user/bob.auto/Calendar@example.org', + 'kolabinvitationpolicy': ['ACT_ACCEPT','ACT_UPDATE'] + } + self.external = { 'displayname': 'Bob External', 'mail': 'bob.external@gmail.com' @@ -186,6 +197,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 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("Bob", "Auto", kolabinvitationpolicy=self.bob['kolabinvitationpolicy'], preferredlanguage=self.bob['preferredlanguage']) time.sleep(1) from tests.functional.synchronize import synchronize_once @@ -432,7 +444,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 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':_('ACCEPTED') }, self.jane['mail']) + 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) @@ -453,7 +465,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 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':_('DECLINED') }, self.jane['mail']) + 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) @@ -466,7 +478,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 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':_('TENTATIVE') }, self.jack['mail']) + 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) @@ -474,12 +486,12 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 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':_('TENTATIVE') }, self.jack['mail']) + 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':_('DECLINED') }, self.jack['mail']) + 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) @@ -494,7 +506,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 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':_('ACCEPTED') }, self.jane['mail']) + 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) @@ -507,7 +519,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 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':_('ACCEPTED') }, self.jane['mail']) + 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) @@ -523,7 +535,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 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':_('TENTATIVE') }, self.jack['mail']) + 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 @@ -531,7 +543,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 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':_('DECLINED') }, self.jack['mail']) + 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 @@ -577,7 +589,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase): uid = self.send_itip_invitation(self.jane['mail'], summary="cancelled") - response = self.check_message_received(self.itip_reply_subject % { 'summary':'cancelled', 'status':_('ACCEPTED') }, self.jane['mail']) + 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") @@ -594,13 +606,19 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 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.jack]) + uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.bob, 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 - # TODO: this notification should be suppressed until jack has replied, too + # this notification should be suppressed until bob has replied, too + notification = self.check_message_received(_('"%s" has been updated') % ('test'), self.john['mail']) + self.assertEqual(notification, None) + + # send a reply from bob to john + self.send_itip_reply(uid, self.bob['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) @@ -610,14 +628,14 @@ class TestWallaceInvitationpolicy(unittest.TestCase): self.purge_mailbox(self.john['mailbox']) - # send a reply from jack to john - self.send_itip_reply(uid, self.jack['mail'], self.john['mail'], start=start, partstat='TENTATIVE') + # send a reply from bob 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.assertIn(self.jack['mail'], notification_text) self.assertNotIn(_("PENDING"), notification_text) diff --git a/tests/unit/test-012-wallace_invitationpolicy.py b/tests/unit/test-012-wallace_invitationpolicy.py index 0b64f6a..dbe0713 100644 --- a/tests/unit/test-012-wallace_invitationpolicy.py +++ b/tests/unit/test-012-wallace_invitationpolicy.py @@ -142,4 +142,20 @@ class TestWallaceInvitationpolicy(unittest.TestCase): 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/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py index 1426e2b..03585ee 100644 --- a/wallace/module_invitationpolicy.py +++ b/wallace/module_invitationpolicy.py @@ -41,6 +41,7 @@ from pykolab.conf import Conf from pykolab.imap import IMAP from pykolab.xml import to_dt from pykolab.xml import event_from_message +from pykolab.xml import participant_status_label from pykolab.itip import events_from_message from pykolab.itip import check_event_conflict from pykolab.itip import send_reply @@ -500,12 +501,17 @@ def user_dn_from_email_address(email_address): auth = Auth() auth.connect() + # return cached value + if user_dn_from_email_address.cache.has_key(email_address): + return user_dn_from_email_address.cache[email_address] + local_domains = auth.list_domains() if not local_domains == None: local_domains = list(set(local_domains.keys())) if not email_address.split('@')[1] in local_domains: + user_dn_from_email_address.cache[email_address] = None return None log.debug(_("Checking if email address %r belongs to a local user") % (email_address), level=8) @@ -517,8 +523,13 @@ def user_dn_from_email_address(email_address): else: log.debug(_("No user record(s) found for %r") % (email_address), level=9) + # remember this lookup + user_dn_from_email_address.cache[email_address] = user_dn + return user_dn +user_dn_from_email_address.cache = {} + def get_matching_invitation_policies(receiving_user, sender_domain): # get user's kolabInvitationPolicy settings @@ -843,6 +854,8 @@ def send_reply_notification(event, receiving_user): """ Send a (consolidated) notification about the current participant status to organizer """ + global auth + import smtplib from email.MIMEText import MIMEText from email.Utils import formatdate @@ -851,6 +864,13 @@ def send_reply_notification(event, receiving_user): event.uid, receiving_user['mail'] ), level=8) + organizer = event.get_organizer() + orgemail = organizer.email() + orgname = organizer.name() + sender_domain = orgemail.split('@')[-1] + + auto_replies_expected = 0 + auto_replies_received = 0 partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'PENDING':[] } for attendee in event.get_attendees(): parstat = attendee.get_participant_status(True) @@ -859,13 +879,34 @@ def send_reply_notification(event, receiving_user): else: partstats['PENDING'].append(attendee.get_displayname()) - # TODO: for every attendee, look-up its kolabinvitationpolicy and skip notification - # until we got replies from all automatically responding attendees + # 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, sender_domain): + 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" + _(status) + ":\n" + "\n".join(attendees) + "\n" + roundup += "\n" + participant_status_label(status) + ":\n" + "\n".join(attendees) + "\n" message_text = """ The event '%(summary)s' at %(start)s has been updated in your calendar. @@ -882,11 +923,6 @@ def send_reply_notification(event, receiving_user): msg['To'] = receiving_user['mail'] msg['Date'] = formatdate(localtime=True) msg['Subject'] = _('"%s" has been updated') % (event.get_summary()) - - organizer = event.get_organizer() - orgemail = organizer.email() - orgname = organizer.name() - msg['From'] = '"%s" <%s>' % (orgname, orgemail) if orgname else orgemail smtp = smtplib.SMTP("localhost", 10027) @@ -902,6 +938,36 @@ def send_reply_notification(event, receiving_user): smtp.quit() +def is_auto_reply(user, sender_domain): + accept_available = False + accept_conflicts = False + for policy in get_matching_invitation_policies(user, sender_domain): + if policy & (ACT_ACCEPT | ACT_REJECT | ACT_DELEGATE): + if check_policy_condition(policy, True): + accept_available = True + if check_policy_condition(policy, False): + accept_conflicts = True + + # we have both cases covered by a policy + if accept_available and accept_conflicts: + return True + + # manual action reached + if policy & (ACT_MANUAL | ACT_SAVE_TO_CALENDAR): + return False + + return False + + +def check_policy_condition(policy, available): + condition_fulfilled = True + if policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT): + condition_fulfilled = available + if policy & COND_IF_CONFLICT: + condition_fulfilled = not condition_fulfilled + return condition_fulfilled + + def propagate_changes_to_attendees_calendars(event): """ Find and update copies of this event in all attendee's calendars |