summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Bruederli <bruederli@kolabsys.com>2014-07-10 05:09:11 -0400
committerThomas Bruederli <bruederli@kolabsys.com>2014-07-10 05:09:11 -0400
commitc395789e553531a4a565bcc61a422255ef5385b4 (patch)
tree23c6402afd3af5dbf57fce05504a5bd6cde5c686
parentd13dd3849d09d1eaa4fdd906686cb1217cc23ad4 (diff)
downloadpykolab-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.py48
-rw-r--r--tests/unit/test-012-wallace_invitationpolicy.py16
-rw-r--r--wallace/module_invitationpolicy.py82
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