summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksander Machniak <machniak@kolabsys.com>2021-11-28 20:22:40 +0100
committerChristian Mollekopf <mollekopf@kolabsys.com>2021-12-16 13:54:47 +0100
commitd015860be2964834efe1080cf78ba8e1b3173b70 (patch)
tree7da1589f2a3c7685acce5cc6a1b16f225f3024a0
parent294ef5b076c42d029fdb58b8167124f0cfd9e4a8 (diff)
downloadpykolab-dev/resourcemanagement3.tar.gz
Implement ACT_STORE_AND_NOTIFY policy for resources, add webmail url to the notification bodydev/resourcemanagement3
Summary: Set the status to NEEDS_ACTION and don't send out an immediate reply to the organizer. Differential Revision: https://git.kolab.org/D3077
-rw-r--r--pykolab/imap/__init__.py3
-rw-r--r--tests/unit/test-011-wallace_resources.py56
-rw-r--r--wallace/module_resources.py82
3 files changed, 113 insertions, 28 deletions
diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py
index 8ea23e7..813d722 100644
--- a/pykolab/imap/__init__.py
+++ b/pykolab/imap/__init__.py
@@ -285,6 +285,9 @@ class IMAP(object):
else:
raise AttributeError(_("%r has no attribute %s") % (self, name))
+ def append(self, folder, message):
+ return self.imap.m.append(self.folder_utf7(folder), None, None, message)
+
def folder_utf7(self, folder):
from pykolab import imap_utf7
return imap_utf7.encode(folder)
diff --git a/tests/unit/test-011-wallace_resources.py b/tests/unit/test-011-wallace_resources.py
index 35d85a0..210d85d 100644
--- a/tests/unit/test-011-wallace_resources.py
+++ b/tests/unit/test-011-wallace_resources.py
@@ -3,6 +3,7 @@ import logging
import datetime
from pykolab import itip
+from pykolab.imap import IMAP
from icalendar import Calendar
from email import message
from email import message_from_string
@@ -41,7 +42,7 @@ CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VEVENT
UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
-DTSTAMP:20120713T1254140
+DTSTAMP:20120713T125414
DTSTART;TZID=3DEurope/London:20120713T100000
DTEND;TZID=3DEurope/London:20120713T110000
SUMMARY:test
@@ -75,7 +76,7 @@ CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VEVENT
UID:626421779C777FBE9C9B85A80D04DDFA-A4BF5BBB9FEAA271
-DTSTAMP:20120713T1254140
+DTSTAMP:20120713T125414
DTSTART;TZID=3DEurope/London:20120713T100000
DTEND;TZID=3DEurope/London:20120713T110000
SUMMARY:test
@@ -105,6 +106,12 @@ class TestWallaceResources(unittest.TestCase):
self.patch(pykolab.auth.Auth, "get_entry_attributes", self._mock_get_entry_attributes)
self.patch(pykolab.auth.Auth, "search_entry_by_attribute", self._mock_search_entry_by_attribute)
+ # Mock IMAP operations
+ self.patch(pykolab.imap.IMAP, "connect", self._mock_nop)
+ self.patch(pykolab.imap.IMAP, "disconnect", self._mock_nop)
+ self.patch(pykolab.imap.IMAP, "set_acl", self._mock_nop)
+ self.patch(pykolab.imap.IMAP, "append", self._mock_imap_append)
+
# intercept calls to smtplib.SMTP.sendmail()
import smtplib
self.patch(smtplib.SMTP, "__init__", self._mock_smtp_init)
@@ -113,8 +120,9 @@ class TestWallaceResources(unittest.TestCase):
self.patch(smtplib.SMTP, "sendmail", self._mock_smtp_sendmail)
self.smtplog = []
+ self.imap_append_log = []
- def _mock_nop(self, domain=None):
+ def _mock_nop(self, domain=None, arg3=None, arg4=None):
pass
def _mock_find_resource(self, address):
@@ -142,6 +150,10 @@ class TestWallaceResources(unittest.TestCase):
self.smtplog.append((from_addr, to_addr, message))
return []
+ def _mock_imap_append(self, folder, msg):
+ self.imap_append_log.append((folder, msg))
+ return True
+
def _get_ics_part(self, message):
ics_part = None
for part in message.walk():
@@ -234,3 +246,41 @@ class TestWallaceResources(unittest.TestCase):
self.assertIn("ACCEPTED".lower(), response2['subject'].lower(), "Delegation message subject: %r" % (response2['subject']))
self.assertEqual(ical2['attendee'].__str__(), "MAILTO:resource-car-audi-a4@example.org")
self.assertEqual(ical2['attendee'].params['PARTSTAT'], u"ACCEPTED")
+
+ def test_007_accept_reservation_request_store_and_notify(self):
+ itip_event = itip.events_from_message(message_from_string(itip_multipart))[0]
+
+ resource = {
+ 'mail': 'resource-collection-car@example.org',
+ 'kolabtargetfolder': 'shared/Resources/Test@example.org',
+ 'dn': 'cn=cars,ou=Resources,cd=example,dc=org',
+ 'cn': 'Cars',
+ 'owner': 'uid=foo,ou=People,cd=example,dc=org',
+ 'kolabinvitationpolicy': [module_resources.ACT_STORE_AND_NOTIFY]
+ }
+
+ conf.command_set('wallace', 'webmail_url', 'https://%(domain)s/roundcube')
+ module_resources.imap = IMAP()
+
+ module_resources.accept_reservation_request(itip_event, resource)
+
+ # Assert no reply message sent to the organizer
+ self.assertEqual(len(self.smtplog), 1)
+ self.assertEqual(len(self.imap_append_log), 1)
+
+ # Assert the notification sent to the resource owner
+ mail = message_from_string(self.smtplog[0][2])
+
+ self.assertEqual("resource-collection-car@example.org", self.smtplog[0][0])
+ self.assertEqual("resource-collection-car@example.org", mail['from'])
+ self.assertEqual("foo@example.org", self.smtplog[0][1])
+ self.assertEqual("foo@example.org", mail['to'])
+ self.assertFalse(mail.is_multipart())
+ self.assertIn("New booking request for Cars", mail['subject'])
+ body = mail.get_payload(decode=True)
+ self.assertIn("The resource booking request is for Cars by Doe, John <doe@example.org>", body)
+ self.assertIn("You can change the status via https://example.org/roundcube?_task=calendar", body)
+
+ # Assert the message appended to the resource folder
+ self.assertEqual(resource['kolabtargetfolder'], self.imap_append_log[0][0])
+ self.assertIn("<text>NEEDS-ACTION</text>", self.imap_append_log[0][1])
diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index 798577d..44b1db5 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -58,14 +58,17 @@ COND_NOTIFY = 256
ACT_MANUAL = 1
ACT_ACCEPT = 2
ACT_REJECT = 8
+ACT_STORE = 16
ACT_ACCEPT_AND_NOTIFY = ACT_ACCEPT + COND_NOTIFY
+ACT_STORE_AND_NOTIFY = ACT_STORE + COND_NOTIFY
# noqa: E241
policy_name_map = {
'ACT_MANUAL': ACT_MANUAL, # noqa: E241
'ACT_ACCEPT': ACT_ACCEPT, # noqa: E241
'ACT_REJECT': ACT_REJECT, # noqa: E241
- 'ACT_ACCEPT_AND_NOTIFY': ACT_ACCEPT_AND_NOTIFY
+ 'ACT_ACCEPT_AND_NOTIFY': ACT_ACCEPT_AND_NOTIFY,
+ 'ACT_STORE_AND_NOTIFY': ACT_STORE_AND_NOTIFY
}
# pylint: disable=invalid-name
@@ -1031,11 +1034,14 @@ def accept_reservation_request(
):
"""
Accepts the given iTip event by booking it into the resource's
- calendar. Then set the attendee status of the given resource to
- ACCEPTED and sends an iTip reply message to the organizer.
+ calendar. Then, depending on the policy, set the attendee status of the given resource to
+ ACCEPTED/TENTATIVE and send an iTip reply message to the organizer, or set the status to
+ NEEDS-ACTION and don't send a reply to the organizer.
"""
owner = get_resource_owner(resource)
confirmation_required = False
+ do_send_response = True
+ partstat = 'ACCEPTED'
if not confirmed and owner:
if invitationpolicy is None:
@@ -1046,9 +1052,13 @@ def accept_reservation_request(
for policy in invitationpolicy:
if policy & ACT_MANUAL and owner['mail']:
confirmation_required = True
+ partstat = 'TENTATIVE'
+ break
+ if policy & ACT_STORE:
+ partstat = 'NEEDS-ACTION'
+ # Do not send an immediate response to the organizer
+ do_send_response = False
break
-
- partstat = 'TENTATIVE' if confirmation_required else 'ACCEPTED'
itip_event['xml'].set_transparency(False)
itip_event['xml'].set_attendee_participant_status(
@@ -1063,7 +1073,7 @@ def accept_reservation_request(
level=8
)
- if saved:
+ if saved and do_send_response:
send_response(delegator['mail'] if delegator else resource['mail'], itip_event, owner)
if owner and confirmation_required:
@@ -1110,7 +1120,6 @@ def save_resource_event(itip_event, resource):
"""
try:
save_event = itip_event['xml']
- targetfolder = imap.folder_quote(resource['kolabtargetfolder'])
# add exception to existing recurring main event
if resource.get('existing_master') is not None:
@@ -1132,18 +1141,17 @@ def save_resource_event(itip_event, resource):
else:
imap.set_acl(
- targetfolder,
+ resource['kolabtargetfolder'],
conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'),
"lrswipkxtecda"
)
# append new version
- result = imap.imap.m.append(
- targetfolder,
- None,
- None,
+ result = imap.append(
+ resource['kolabtargetfolder'],
save_event.to_message(creator="Kolab Server <wallace@localhost>").as_string()
)
+
return result
# pylint: disable=broad-except
@@ -1642,16 +1650,21 @@ def send_owner_notification(resource, owner, itip_event, success=True):
if 'preferredlanguage' in owner:
pykolab.translate.setUserLanguage(owner['preferredlanguage'])
- message_text = owner_notification_text(resource, owner, itip_event['xml'], success)
+ message_text = owner_notification_text(resource, owner, itip_event['xml'], success, status)
msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8')
msg['To'] = owner['mail']
msg['From'] = resource['mail']
msg['Date'] = formatdate(localtime=True)
- msg['Subject'] = utils.str2unicode(_('Booking for %s has been %s') % (
- resource['cn'], participant_status_label(status) if success else _('failed')
- ))
+ if status == 'NEEDS-ACTION':
+ msg['Subject'] = utils.str2unicode(_('New booking request for %s') % (
+ resource['cn']
+ ))
+ else:
+ msg['Subject'] = utils.str2unicode(_('Booking for %s has been %s') % (
+ resource['cn'], participant_status_label(status) if success else _('failed')
+ ))
seed = random.randint(0, 6)
alarm_after = (seed * 10) + 60
@@ -1663,19 +1676,37 @@ def send_owner_notification(resource, owner, itip_event, success=True):
signal.alarm(0)
-def owner_notification_text(resource, owner, event, success):
+def owner_notification_text(resource, owner, event, success, status):
organizer = event.get_organizer()
status = event.get_attendee_by_email(resource['mail']).get_participant_status(True)
+ domain = resource['mail'].split('@')[1]
+ url = conf.get('wallace', 'webmail_url')
if success:
- message_text = _(
- """
- The resource booking for %(resource)s by %(orgname)s <%(orgemail)s> has been
- %(status)s for %(date)s.
+ if status == 'NEEDS-ACTION':
+ message_text = _(
+ """
+ The resource booking request is for %(resource)s by %(orgname)s <%(orgemail)s> for %(date)s.
- *** This is an automated message, sent to you as the resource owner. ***
- """
- )
+ *** This is an automated message, sent to you as the resource owner. ***
+ """
+ )
+ else:
+ message_text = _(
+ """
+ The resource booking for %(resource)s by %(orgname)s <%(orgemail)s> has been
+ %(status)s for %(date)s.
+
+ *** This is an automated message, sent to you as the resource owner. ***
+ """
+ )
+
+
+ if url:
+ message_text += (
+ "\n "
+ + _("You can change the status via %(url)s") % { 'url': url } + '?_task=calendar'
+ )
else:
message_text = _(
"""
@@ -1695,7 +1726,8 @@ def owner_notification_text(resource, owner, event, success):
'date': event.get_date_text(),
'status': participant_status_label(status),
'orgname': organizer.name(),
- 'orgemail': organizer.email()
+ 'orgemail': organizer.email(),
+ 'domain': domain
}