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,51 @@ 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_tentative_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_TENTATIVE_AND_NOTIFY] + } + + conf.command_set('wallace', 'webmail_url', 'https://%(domain)s/roundcube') + module_resources.imap = IMAP() + + module_resources.accept_reservation_request(itip_event, resource) + + self.assertEqual(len(self.smtplog), 2) + self.assertEqual(len(self.imap_append_log), 1) + + # Assert the reply message sent to the organizer + 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("doe@example.org", self.smtplog[0][1]) + self.assertEqual("doe@example.org", mail['to']) + self.assertTrue(mail.is_multipart()) + self.assertIn("Reservation Request for test was Tentatively Accepted", mail['subject']) + self.assertIn("PARTSTAT=TENTATIVE", self.smtplog[0][2]) + + # Assert the notification sent to the resource owner + mail = message_from_string(self.smtplog[1][2]) + + self.assertEqual("resource-collection-car@example.org", self.smtplog[1][0]) + self.assertEqual("resource-collection-car@example.org", mail['from']) + self.assertEqual("foo@example.org", self.smtplog[1][1]) + self.assertEqual("foo@example.org", mail['to']) + self.assertFalse(mail.is_multipart()) + self.assertIn("Booking for Cars has been Tentatively Accepted", mail['subject']) + body = mail.get_payload(decode=True) + self.assertIn("The resource booking 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("TENTATIVE", 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_TENTATIVE = 16 ACT_ACCEPT_AND_NOTIFY = ACT_ACCEPT + COND_NOTIFY +ACT_TENTATIVE_AND_NOTIFY = ACT_TENTATIVE + 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_TENTATIVE_AND_NOTIFY': ACT_TENTATIVE_AND_NOTIFY } # pylint: disable=invalid-name @@ -1032,10 +1035,11 @@ """ 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. + ACCEPTED/TENTATIVE and sends an iTip reply message to the organizer. """ owner = get_resource_owner(resource) confirmation_required = False + partstat = 'ACCEPTED' if not confirmed and owner: if invitationpolicy is None: @@ -1046,9 +1050,10 @@ for policy in invitationpolicy: if policy & ACT_MANUAL and owner['mail']: confirmation_required = True + partstat = 'TENTATIVE' break - - partstat = 'TENTATIVE' if confirmation_required else 'ACCEPTED' + if policy & ACT_TENTATIVE: + partstat = 'TENTATIVE' itip_event['xml'].set_transparency(False) itip_event['xml'].set_attendee_participant_status( @@ -1110,7 +1115,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 +1136,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 @@ -1666,6 +1669,8 @@ def owner_notification_text(resource, owner, event, success): 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 = _( @@ -1676,6 +1681,12 @@ *** 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 +1706,8 @@ 'date': event.get_date_text(), 'status': participant_status_label(status), 'orgname': organizer.name(), - 'orgemail': organizer.email() + 'orgemail': organizer.email(), + 'domain': domain }