diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py --- a/pykolab/imap/__init__.py +++ b/pykolab/imap/__init__.py @@ -285,6 +285,9 @@ 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 --- a/tests/unit/test-011-wallace_resources.py +++ b/tests/unit/test-011-wallace_resources.py @@ -3,6 +3,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 ", 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("NEEDS-ACTION", self.imap_append_log[0][1]) diff --git a/wallace/module_resources.py b/wallace/module_resources.py --- a/wallace/module_resources.py +++ b/wallace/module_resources.py @@ -58,14 +58,17 @@ 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 @@ ): """ 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 @@ 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 @@ 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 @@ """ 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 @@ 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 ").as_string() ) + return result # pylint: disable=broad-except @@ -1642,16 +1650,21 @@ 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 @@ 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 @@ 'date': event.get_date_text(), 'status': participant_status_label(status), 'orgname': organizer.name(), - 'orgemail': organizer.email() + 'orgemail': organizer.email(), + 'domain': domain }