2 commits - INSTALL pykolab/itip pykolab/xml tests/functional tests/unit wallace/module_invitationpolicy.py

Thomas Brüderli bruederli at kolabsys.com
Thu Aug 21 16:31:34 CEST 2014


 INSTALL                                                       |    2 
 pykolab/itip/__init__.py                                      |   28 
 pykolab/xml/attendee.py                                       |   11 
 pykolab/xml/contact.py                                        |    2 
 pykolab/xml/event.py                                          |   14 
 pykolab/xml/todo.py                                           |    5 
 pykolab/xml/utils.py                                          |   13 
 tests/functional/test_wallace/test_005_resource_invitation.py |    3 
 tests/functional/test_wallace/test_007_invitationpolicy.py    |  258 +++++-
 tests/unit/test-012-wallace_invitationpolicy.py               |   32 
 tests/unit/test-016-todo.py                                   |    1 
 wallace/module_invitationpolicy.py                            |  405 ++++++----
 12 files changed, 559 insertions(+), 215 deletions(-)

New commits:
commit b05296d7d41c7a42620d996641db9054e9da2f23
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Aug 21 10:31:16 2014 -0400

    Refactored the wallace invitationpolicy module to work for automated task iTip processing as well + add functional tests for task assignments (#3240)

diff --git a/INSTALL b/INSTALL
index b882a0a..0f83cf9 100644
--- a/INSTALL
+++ b/INSTALL
@@ -7,7 +7,7 @@
 * intltool
 * rpm-build
 
-* python-icalendar (version 3.8.x or higher)
+* python-icalendar (version 3.8.2 or higher)
 * python-kolabformat
 * python-kolab
 * python-nose
diff --git a/pykolab/itip/__init__.py b/pykolab/itip/__init__.py
index ddcb392..1a361c1 100644
--- a/pykolab/itip/__init__.py
+++ b/pykolab/itip/__init__.py
@@ -1,8 +1,10 @@
 import icalendar
 import pykolab
+import traceback
 
 from pykolab.xml import to_dt
 from pykolab.xml import event_from_ical
+from pykolab.xml import todo_from_ical
 from pykolab.xml import participant_status_label
 from pykolab.translate import _
 
@@ -10,13 +12,13 @@ log = pykolab.getLogger('pykolab.wallace')
 
 
 def events_from_message(message, methods=None):
-    return objects_from_message(message, "VEVENT", methods)
+    return objects_from_message(message, ["VEVENT"], methods)
 
 def todos_from_message(message, methods=None):
-    return objects_from_message(message, "VTODO", methods)
+    return objects_from_message(message, ["VTODO"], methods)
 
 
-def objects_from_message(message, objname, methods=None):
+def objects_from_message(message, objnames, methods=None):
     """
         Obtain the iTip payload from email.message <message>
     """
@@ -60,7 +62,7 @@ def objects_from_message(message, objname, methods=None):
                 return []
 
             for c in cal.walk():
-                if c.name == objname:
+                if c.name in objnames:
                     itip = {}
 
                     if c['uid'] in seen_uids:
@@ -80,13 +82,14 @@ def objects_from_message(message, objname, methods=None):
                     # - resources (if any)
                     #
 
+                    itip['type'] = 'task' if c.name == 'VTODO' else 'event'
                     itip['uid'] = str(c['uid'])
                     itip['method'] = str(cal['method']).upper()
                     itip['sequence'] = int(c['sequence']) if c.has_key('sequence') else 0
 
                     if c.has_key('dtstart'):
                         itip['start'] = c['dtstart'].dt
-                    else:
+                    elif itip['type'] == 'VEVENT':
                         log.error(_("iTip event without a start"))
                         continue
 
@@ -110,17 +113,20 @@ def objects_from_message(message, objname, methods=None):
                     itip['raw'] = itip_payload
 
                     try:
-                        # TODO: distinguish event and todo here
-                        itip['xml'] = event_from_ical(c.to_ical())
+                        # distinguish event and todo here
+                        if itip['type'] == 'task':
+                            itip['xml'] = todo_from_ical(c.to_ical())
+                        else:
+                            itip['xml'] = event_from_ical(c.to_ical())
                     except Exception, e:
-                        log.error("event_from_ical() exception: %r; iCal: %s" % (e, itip_payload))
+                        log.error("event|todo_from_ical() exception: %r; iCal: %s" % (e, itip_payload))
                         continue
 
                     itip_objects.append(itip)
 
                     seen_uids.append(c['uid'])
 
-                # end if c.name == "VEVENT"
+                # end if c.name in objnames
 
             # end for c in cal.walk()
 
@@ -212,6 +218,8 @@ def send_reply(from_address, itip_events, response_text, subject=None):
         attendee = itip_event['xml'].get_attendee_by_email(from_address)
         participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee)
 
+        log.debug(_("Send iTip reply %s for %s %r") % (participant_status, itip_event['xml'].type, itip_event['xml'].uid), level=8)
+
         event_summary = itip_event['xml'].get_summary()
         message_text = response_text % { 'summary':event_summary, 'status':participant_status_label(participant_status), 'name':attendee.get_name() }
 
@@ -226,7 +234,7 @@ def send_reply(from_address, itip_events, response_text, subject=None):
                 subject=subject
             )
         except Exception, e:
-            log.error(_("Failed to compose iTip reply message: %r") % (e))
+            log.error(_("Failed to compose iTip reply message: %r: %s") % (e, traceback.format_exc()))
             return
 
         smtp = smtplib.SMTP("localhost", 10026)  # replies go through wallace again
diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index a6384e9..cdfe86a 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -12,13 +12,16 @@ participant_status_labels = {
         "TENTATIVE": N_("Tentatively Accepted"),
         "DELEGATED": N_("Delegated"),
         "COMPLETED": N_("Completed"),
-        "IN-PROCESS": N_("In Process"),
+        "IN-PROCESS": N_("Started"),
         # support integer values, too
         kolabformat.PartNeedsAction: N_("Needs Action"),
         kolabformat.PartAccepted: N_("Accepted"),
         kolabformat.PartDeclined: N_("Declined"),
         kolabformat.PartTentative: N_("Tentatively Accepted"),
         kolabformat.PartDelegated: N_("Delegated"),
+        # waiting for libkolabxml to support these (#3472)
+        #kolabformat.PartCompleted: N_("Completed"),
+        #kolabformat.PartInProcess: N_("Started"),
     }
 
 def participant_status_label(status):
@@ -38,9 +41,9 @@ class Attendee(kolabformat.Attendee):
             "DECLINED": kolabformat.PartDeclined,
             "TENTATIVE": kolabformat.PartTentative,
             "DELEGATED": kolabformat.PartDelegated,
-            # Not yet implemented
-            #"COMPLETED": ,
-            #"IN-PROCESS": ,
+            # waiting for libkolabxml to support these (#3472)
+            #"COMPLETED": kolabformat.PartCompleted,
+            #"IN-PROCESS": kolabformat.PartInProcess,
         }
 
     # See RFC 2445, 5445
diff --git a/pykolab/xml/todo.py b/pykolab/xml/todo.py
index b04b233..0d34c63 100644
--- a/pykolab/xml/todo.py
+++ b/pykolab/xml/todo.py
@@ -117,6 +117,10 @@ class Todo(Event):
     def set_percentcomplete(self, percent):
         self.event.setPercentComplete(int(percent))
 
+    def set_transparency(self, transp):
+        # empty stub
+        pass
+
     def get_due(self):
         return xmlutils.from_cdatetime(self.event.due(), True)
 
diff --git a/tests/functional/test_wallace/test_005_resource_invitation.py b/tests/functional/test_wallace/test_005_resource_invitation.py
index a4e1ebe..4e69f2f 100644
--- a/tests/functional/test_wallace/test_005_resource_invitation.py
+++ b/tests/functional/test_wallace/test_005_resource_invitation.py
@@ -189,6 +189,9 @@ class TestResourceInvitation(unittest.TestCase):
 
     @classmethod
     def setup_class(self, *args, **kw):
+        # set language to default
+        pykolab.translate.setUserLanguage(conf.get('kolab','default_locale'))
+
         self.itip_reply_subject = _("Reservation Request for %(summary)s was %(status)s")
 
         from tests.functional.purge_users import purge_users
diff --git a/tests/functional/test_wallace/test_007_invitationpolicy.py b/tests/functional/test_wallace/test_007_invitationpolicy.py
index bdbfc98..dffc9df 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 todo_from_message
 from pykolab.xml import participant_status_label
 from email import message_from_string
 from twisted.trial import unittest
@@ -126,6 +127,75 @@ END:VEVENT
 END:VCALENDAR
 """
 
+itip_todo = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+ 2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:REQUEST
+BEGIN:VTODO
+UID:%(uid)s
+CREATED;VALUE=DATE-TIME:20140731T100704Z
+DTSTAMP;VALUE=DATE-TIME:20140820T101333Z
+DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:%(start)s
+DUE;VALUE=DATE-TIME;TZID=Europe/Berlin:%(end)s
+SUMMARY:%(summary)s
+SEQUENCE:%(sequence)d
+PRIORITY:1
+STATUS:NEEDS-ACTION
+PERCENT-COMPLETE:0
+ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
+ATTENDEE;PARTSTAT=%(partstat)s;ROLE=REQ-PARTICIPANT:mailto:%(mailto)s
+END:VTODO
+END:VCALENDAR
+"""
+
+itip_todo_reply = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+  2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:REPLY
+BEGIN:VTODO
+UID:%(uid)s
+CREATED;VALUE=DATE-TIME:20140731T100704Z
+DTSTAMP;VALUE=DATE-TIME:20140821T085424Z
+DTSTART;VALUE=DATE-TIME;TZID=Europe/Berlin:%(start)s
+DUE;VALUE=DATE-TIME;TZID=Europe/Berlin:%(end)s
+SUMMARY:%(summary)s
+SEQUENCE:%(sequence)d
+PRIORITY:1
+STATUS:NEEDS-ACTION
+PERCENT-COMPLETE:40
+ATTENDEE;PARTSTAT=%(partstat)s;ROLE=REQ-PARTICIPANT;CUTYPE=INDIVIDUAL:mailto:%(mailto)s
+ORGANIZER;CN="Doe, John":mailto:%(organizer)s
+END:VTODO
+END:VCALENDAR
+"""
+
+itip_todo_cancel = """
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
+ 2.1.3//EN
+CALSCALE:GREGORIAN
+METHOD:CANCEL
+BEGIN:VTODO
+UID:%(uid)s
+CREATED;VALUE=DATE-TIME:20140731T100704Z
+DTSTAMP;VALUE=DATE-TIME:20140820T101333Z
+SUMMARY:%(summary)s
+SEQUENCE:%(sequence)d
+PRIORITY:1
+STATUS:CANCELLED
+ORGANIZER;CN="Doe, John":mailto:john.doe at example.org
+ATTENDEE;PARTSTAT=ACCEPTED;ROLE=REQ-PARTICIPANT:mailto:%(mailto)s
+END:VTODO
+END:VCALENDAR
+"""
+
 mime_message = """MIME-Version: 1.0
 Content-Type: multipart/mixed;
  boundary="=_c8894dbdb8baeedacae836230e3436fd"
@@ -164,6 +234,9 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
     @classmethod
     def setup_class(self, *args, **kw):
+        # set language to default
+        pykolab.translate.setUserLanguage(conf.get('kolab','default_locale'))
+
         self.itip_reply_subject = _('"%(summary)s" has been %(status)s')
 
         from tests.functional.purge_users import purge_users
@@ -175,7 +248,8 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             'dn': 'uid=doe,ou=People,dc=example,dc=org',
             'preferredlanguage': 'en_US',
             'mailbox': 'user/john.doe at example.org',
-            'kolabtargetfolder': 'user/john.doe/Calendar at example.org',
+            'kolabcalendarfolder': 'user/john.doe/Calendar at example.org',
+            'kolabtasksfolder': 'user/john.doe/Tasks at example.org',
             'kolabinvitationpolicy': ['ACT_UPDATE_AND_NOTIFY','ACT_MANUAL']
         }
 
@@ -185,8 +259,9 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             'dn': 'uid=manager,ou=People,dc=example,dc=org',
             'preferredlanguage': 'en_US',
             'mailbox': 'user/jane.manager at example.org',
-            'kolabtargetfolder': 'user/jane.manager/Calendar at example.org',
-            'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT','ACT_UPDATE']
+            'kolabcalendarfolder': 'user/jane.manager/Calendar at example.org',
+            'kolabtasksfolder': 'user/jane.manager/Tasks at example.org',
+            'kolabinvitationpolicy': ['ACT_ACCEPT_IF_NO_CONFLICT','ACT_REJECT_IF_CONFLICT','TASK_ACCEPT','ACT_UPDATE']
         }
 
         self.jack = {
@@ -195,8 +270,9 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             'dn': 'uid=tentative,ou=People,dc=example,dc=org',
             'preferredlanguage': 'en_US',
             'mailbox': 'user/jack.tentative at example.org',
-            'kolabtargetfolder': 'user/jack.tentative/Calendar at example.org',
-            'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ACT_SAVE_TO_CALENDAR','ACT_UPDATE']
+            'kolabcalendarfolder': 'user/jack.tentative/Calendar at example.org',
+            'kolabtasksfolder': 'user/jack.tentative/Tasks at example.org',
+            'kolabinvitationpolicy': ['ACT_TENTATIVE_IF_NO_CONFLICT','ALL_SAVE_TO_FOLDER','ACT_UPDATE']
         }
 
         self.mark = {
@@ -205,7 +281,8 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             'dn': 'uid=german,ou=People,dc=example,dc=org',
             'preferredlanguage': 'de_DE',
             'mailbox': 'user/mark.german at example.org',
-            'kolabtargetfolder': 'user/mark.german/Calendar at example.org',
+            'kolabcalendarfolder': 'user/mark.german/Calendar at example.org',
+            'kolabtasksfolder': 'user/mark.german/Tasks at example.org',
             'kolabinvitationpolicy': ['ACT_ACCEPT','ACT_UPDATE_AND_NOTIFY']
         }
 
@@ -298,8 +375,8 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         return uid
 
-    def send_itip_cancel(self, attendee_email, uid, summary="test", sequence=1):
-        self.send_message(itip_cancellation % {
+    def send_itip_cancel(self, attendee_email, uid, template=None, summary="test", sequence=1):
+        self.send_message((template if template is not None else itip_cancellation) % {
                 'uid': uid,
                 'mailto': attendee_email,
                 'summary': summary,
@@ -342,7 +419,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         imap = IMAP()
         imap.connect()
 
-        mailbox = imap.folder_quote(user['kolabtargetfolder'])
+        mailbox = imap.folder_quote(user['kolabcalendarfolder'])
         imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
         imap.imap.m.select(mailbox)
 
@@ -355,11 +432,45 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         return event.get_uid()
 
+    def create_task_assignment(self, due=None, summary="test", sequence=0, user=None, attendees=None):
+        if due is None:
+            due = datetime.datetime.now(pytz.timezone("Europe/Berlin")) + datetime.timedelta(days=2)
+        if user is None:
+            user = self.john
+        if attendees is None:
+            attendees = [self.jane]
+
+        todo = pykolab.xml.Todo()
+        todo.set_due(due)
+        todo.set_organizer(user['mail'], user['displayname'])
+
+        for attendee in attendees:
+            todo.add_attendee(attendee['mail'], attendee['displayname'], role="REQ-PARTICIPANT", participant_status="NEEDS-ACTION", rsvp=True)
+
+        todo.set_summary(summary)
+        todo.set_sequence(sequence)
+
+        imap = IMAP()
+        imap.connect()
+
+        mailbox = imap.folder_quote(user['kolabtasksfolder'])
+        imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
+        imap.imap.m.select(mailbox)
+
+        result = imap.imap.m.append(
+            mailbox,
+            None,
+            None,
+            todo.to_message().as_string()
+        )
+
+        return todo.get_uid()
+
     def update_calendar_event(self, uid, start=None, summary=None, sequence=0, user=None):
         if user is None:
             user = self.john
 
-        event = self.check_user_calendar_event(user['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(user['kolabcalendarfolder'], uid)
         if event:
             if start is not None:
                 event.set_start(start)
@@ -371,7 +482,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
             imap = IMAP()
             imap.connect()
 
-            mailbox = imap.folder_quote(user['kolabtargetfolder'])
+            mailbox = imap.folder_quote(user['kolabcalendarfolder'])
             imap.set_acl(mailbox, "cyrus-admin", "lrswipkxtecda")
             imap.imap.m.select(mailbox)
 
@@ -416,6 +527,9 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         return found
 
     def check_user_calendar_event(self, mailbox, uid=None):
+        return self.check_user_imap_object(mailbox, uid)
+
+    def check_user_imap_object(self, mailbox, uid=None, type='event'):
         imap = IMAP()
         imap.connect()
 
@@ -429,16 +543,20 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         while not found and retries > 0:
             retries -= 1
 
-            typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid) if uid else '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.event")')
+            typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid) if uid else '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.' + type + '")')
             for num in data[0].split():
                 typ, data = imap.imap.m.fetch(num, '(RFC822)')
-                event_message = message_from_string(data[0][1])
+                object_message = message_from_string(data[0][1])
 
                 # return matching UID or first event found
-                if uid and event_message['subject'] != uid:
+                if uid and object_message['subject'] != uid:
                     continue
 
-                found = event_from_message(event_message)
+                if type == 'task':
+                    found = todo_from_message(object_message)
+                else:
+                    found = event_from_message(object_message)
+
                 if found:
                     break
 
@@ -468,7 +586,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_summary(), "test")
 
@@ -476,7 +594,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.send_itip_update(self.jane['mail'], uid, start, summary="test updated", sequence=0, partstat='ACCEPTED')
 
         time.sleep(10)
-        event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_summary(), "test updated")
         self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
@@ -489,7 +607,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_summary(), "test2")
 
@@ -515,7 +633,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.jack['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_summary(), "test2")
         self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction)
@@ -530,7 +648,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_summary(), "test")
 
@@ -543,7 +661,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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)
+        event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_start(), new_start)
         self.assertEqual(event.get_sequence(), 1)
@@ -551,7 +669,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
     def test_005_invite_rescheduling_reject(self):
         self.purge_mailbox(self.john['mailbox'])
-        self.purge_mailbox(self.jack['kolabtargetfolder'])
+        self.purge_mailbox(self.jack['kolabcalendarfolder'])
 
         start = datetime.datetime(2014,8,9, 17,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
         uid = self.send_itip_invitation(self.jack['mail'], start)
@@ -568,7 +686,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertEqual(response, None)
 
         # verify re-scheduled copy in jack's calendar with NEEDS-ACTION
-        event = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.jack['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_start(), new_start)
         self.assertEqual(event.get_sequence(), 1)
@@ -584,7 +702,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         start = datetime.datetime(2014,8,18, 14,30,0, tzinfo=pytz.timezone("Europe/Berlin"))
         uid = self.create_calendar_event(start, user=self.john)
 
-        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         # send a reply from jane to john
@@ -592,7 +710,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         # check for the updated event in john's calendar
         time.sleep(10)
-        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         attendee = event.get_attendee(self.jane['mail'])
@@ -611,7 +729,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         start = datetime.datetime(2014,8,28, 14,30,0, tzinfo=pytz.timezone("Europe/Berlin"))
         uid = self.create_calendar_event(start, user=self.john)
 
-        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         # send a reply from jane to john
@@ -619,7 +737,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         # check for the updated event in john's calendar
         time.sleep(10)
-        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         attendee = event.get_attendee(self.jane['mail'])
@@ -646,7 +764,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.send_itip_cancel(self.jane['mail'], uid, summary="cancelled")
 
         time.sleep(10)
-        event = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_summary(), "cancelled")
         self.assertEqual(event.get_status(True), 'CANCELLED')
@@ -723,7 +841,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         # verify jane's attendee status was not updated
         time.sleep(10)
-        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
         self.assertEqual(event.get_sequence(), 2)
         self.assertEqual(event.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartNeedsAction)
@@ -735,7 +853,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         start = datetime.datetime(2014,8,21, 13,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
         uid = self.create_calendar_event(start, user=self.john, attendees=[self.jane, self.jack, self.external])
 
-        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         # send invitations to jack and jane
@@ -745,7 +863,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         # wait for replies from jack and jane to be processed and propagated
         time.sleep(10)
-        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         # check updated event in organizer's calendar
@@ -753,12 +871,12 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
 
         # check updated partstats in jane's calendar
-        janes = self.check_user_calendar_event(self.jane['kolabtargetfolder'], uid)
+        janes = self.check_user_calendar_event(self.jane['kolabcalendarfolder'], uid)
         self.assertEqual(janes.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
         self.assertEqual(janes.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
 
         # check updated partstats in jack's calendar
-        jacks = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+        jacks = self.check_user_calendar_event(self.jack['kolabcalendarfolder'], uid)
         self.assertEqual(jacks.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
         self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
 
@@ -773,7 +891,7 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
         # wait for replies to be processed and propagated
         time.sleep(10)
-        event = self.check_user_calendar_event(self.john['kolabtargetfolder'], uid)
+        event = self.check_user_calendar_event(self.john['kolabcalendarfolder'], uid)
         self.assertIsInstance(event, pykolab.xml.Event)
 
         # check updated event in organizer's calendar (jack didn't reply yet)
@@ -781,8 +899,78 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         self.assertEqual(event.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartTentative)
 
         # check partstats in jack's calendar: jack's status should remain needs-action
-        jacks = self.check_user_calendar_event(self.jack['kolabtargetfolder'], uid)
+        jacks = self.check_user_calendar_event(self.jack['kolabcalendarfolder'], uid)
         self.assertEqual(jacks.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
         self.assertEqual(jacks.get_attendee(self.jack['mail']).get_participant_status(), kolabformat.PartNeedsAction)
 
 
+    def test_011_task_assignment_accept(self):
+        start = datetime.datetime(2014,9,10, 19,0,0)
+        uid = self.send_itip_invitation(self.jane['mail'], start, summary='work', template=itip_todo)
+
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'work', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
+        self.assertIsInstance(response, email.message.Message)
+
+        todo = self.check_user_imap_object(self.jane['kolabtasksfolder'], uid, 'task')
+        self.assertIsInstance(todo, pykolab.xml.Todo)
+        self.assertEqual(todo.get_summary(), "work")
+
+        # send update with the same sequence: no re-scheduling
+        self.send_itip_update(self.jane['mail'], uid, start, summary='work updated', template=itip_todo, sequence=0, partstat='ACCEPTED')
+
+        response = self.check_message_received(self.itip_reply_subject % { 'summary':'work updated', 'status':participant_status_label('ACCEPTED') }, self.jane['mail'])
+        self.assertEqual(response, None)
+
+        time.sleep(10)
+        todo = self.check_user_imap_object(self.jane['kolabtasksfolder'], uid, 'task')
+        self.assertIsInstance(todo, pykolab.xml.Todo)
+        self.assertEqual(todo.get_summary(), "work updated")
+        self.assertEqual(todo.get_attendee(self.jane['mail']).get_participant_status(), kolabformat.PartAccepted)
+
+
+    def test_012_task_assignment_reply(self):
+        self.purge_mailbox(self.john['mailbox'])
+
+        due = datetime.datetime(2014,9,12, 14,0,0, tzinfo=pytz.timezone("Europe/Berlin"))
+        uid = self.create_task_assignment(due, user=self.john)
+
+        todo = self.check_user_imap_object(self.john['kolabtasksfolder'], uid, 'task')
+        self.assertIsInstance(todo, pykolab.xml.Todo)
+
+        # send a reply from jane to john
+        partstat = 'DECLINED'
+        self.send_itip_reply(uid, self.jane['mail'], self.john['mail'], start=due, template=itip_todo_reply, partstat=partstat)
+
+        # check for the updated task in john's tasklist
+        time.sleep(10)
+        todo = self.check_user_imap_object(self.john['kolabtasksfolder'], uid, 'task')
+        self.assertIsInstance(todo, pykolab.xml.Todo)
+
+        attendee = todo.get_attendee(self.jane['mail'])
+        self.assertIsInstance(attendee, pykolab.xml.Attendee)
+        self.assertEqual(attendee.get_participant_status(True), partstat)
+
+        # this should trigger an update 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(participant_status_label(partstat), notification_text)
+
+
+    def test_013_task_cancellation(self):
+        uid = self.send_itip_invitation(self.jane['mail'], summary='more work', template=itip_todo)
+
+        time.sleep(10)
+        self.send_itip_cancel(self.jane['mail'], uid, template=itip_todo_cancel, summary="cancelled")
+
+        time.sleep(10)
+        todo = self.check_user_imap_object(self.jane['kolabtasksfolder'], uid, 'task')
+        self.assertIsInstance(todo, pykolab.xml.Todo)
+        self.assertEqual(todo.get_summary(), "more work")
+        self.assertEqual(todo.get_status(True), 'CANCELLED')
+
+        # this should trigger a notification message
+        notification = self.check_message_received(_('"%s" has been cancelled') % ('more work'), self.john['mail'], mailbox=self.jane['mailbox'])
+        self.assertIsInstance(notification, email.message.Message)
+
diff --git a/tests/unit/test-012-wallace_invitationpolicy.py b/tests/unit/test-012-wallace_invitationpolicy.py
index aabf676..3366950 100644
--- a/tests/unit/test-012-wallace_invitationpolicy.py
+++ b/tests/unit/test-012-wallace_invitationpolicy.py
@@ -117,16 +117,18 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
 
     def test_003_get_matching_invitation_policy(self):
         user = { 'kolabinvitationpolicy': [
-            'ACT_ACCEPT:example.org',
-            'ACT_REJECT:gmail.com',
-            'ACT_MANUAL:*'
+            'TASK_REJECT:*',
+            'EVENT_ACCEPT:example.org',
+            'EVENT_REJECT:gmail.com',
+            'ALL_MANUAL:*'
         ] }
-        self.assertEqual(MIP.get_matching_invitation_policies(user, 'a at fastmail.net'), [MIP.ACT_MANUAL])
-        self.assertEqual(MIP.get_matching_invitation_policies(user, 'b at example.org'),  [MIP.ACT_ACCEPT,MIP.ACT_MANUAL])
-        self.assertEqual(MIP.get_matching_invitation_policies(user, 'c at gmail.com'),    [MIP.ACT_REJECT,MIP.ACT_MANUAL])
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'a at fastmail.net',   MIP.COND_TYPE_EVENT), [MIP.ACT_MANUAL])
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'b at example.org',    MIP.COND_TYPE_EVENT), [MIP.ACT_ACCEPT, MIP.ACT_MANUAL])
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'c at gmail.com',      MIP.COND_TYPE_EVENT), [MIP.ACT_REJECT, MIP.ACT_MANUAL])
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'd at somedomain.net', MIP.COND_TYPE_TASK),  [MIP.ACT_REJECT, MIP.ACT_MANUAL])
 
         user = { 'kolabinvitationpolicy': ['ACT_ACCEPT:example.org', 'ACT_MANUAL:others'] }
-        self.assertEqual(MIP.get_matching_invitation_policies(user, 'd at somedomain.net'), [MIP.ACT_MANUAL])
+        self.assertEqual(MIP.get_matching_invitation_policies(user, 'd at somedomain.net', MIP.COND_TYPE_ALL), [MIP.ACT_MANUAL])
 
     def test_004_write_locks(self):
         user = { 'cn': 'John Doe', 'mail': "doe at example.org" }
@@ -150,12 +152,12 @@ class TestWallaceInvitationpolicy(unittest.TestCase):
         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 },   'user at domain.org'))
-        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_none },  'user at domain.org'))
-        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_all },   'user at domain.com'))
-        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_cond },  'user at domain.com'))
-        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some },  'user at domain.com'))
-        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some },  'sam at example.org'))
-        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'user at domain.com'))
-        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'john at example.org'))
+        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':all_manual },   'user at domain.org', 'event'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_none },  'user at domain.org', 'event'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_all },   'user at domain.com', 'event'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_cond },  'user at domain.com', 'event'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some },  'user at domain.com', 'event'))
+        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_some },  'sam at example.org', 'event'))
+        self.assertFalse( MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'user at domain.com', 'event'))
+        self.assertTrue(  MIP.is_auto_reply({ 'kolabinvitationpolicy':accept_avail }, 'john at example.org', 'event'))
         
\ No newline at end of file
diff --git a/tests/unit/test-016-todo.py b/tests/unit/test-016-todo.py
index a7e9394..c6a1178 100644
--- a/tests/unit/test-016-todo.py
+++ b/tests/unit/test-016-todo.py
@@ -18,7 +18,6 @@ VERSION:2.0
 PRODID:-//Roundcube//Roundcube libcalendaring 1.1-git//Sabre//Sabre VObject
  2.1.3//EN
 CALSCALE:GREGORIAN
-METHOD:REQUEST
 BEGIN:VTODO
 UID:18C2EBBD8B31D99F7AA578EDFDFB1AC0-FCBB6C4091F28CA0
 DTSTAMP;VALUE=DATE-TIME:20140820T101333Z
diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py
index 8e77335..e753c38 100644
--- a/wallace/module_invitationpolicy.py
+++ b/wallace/module_invitationpolicy.py
@@ -41,30 +41,32 @@ from pykolab.auth import Auth
 from pykolab.conf import Conf
 from pykolab.imap import IMAP
 from pykolab.xml import to_dt
+from pykolab.xml import todo_from_message
 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 objects_from_message
 from pykolab.itip import check_event_conflict
 from pykolab.itip import send_reply
 from pykolab.translate import _
 
 # define some contstants used in the code below
-COND_IF_AVAILABLE  = 32
-COND_IF_CONFLICT   = 64
-COND_TENTATIVE     = 128
-COND_NOTIFY        = 256
 ACT_MANUAL         = 1
 ACT_ACCEPT         = 2
 ACT_DELEGATE       = 4
 ACT_REJECT         = 8
 ACT_UPDATE         = 16
-ACT_TENTATIVE                = ACT_ACCEPT + COND_TENTATIVE
-ACT_ACCEPT_IF_NO_CONFLICT    = ACT_ACCEPT + COND_IF_AVAILABLE
-ACT_TENTATIVE_IF_NO_CONFLICT = ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE
-ACT_DELEGATE_IF_CONFLICT     = ACT_DELEGATE + COND_IF_CONFLICT
-ACT_REJECT_IF_CONFLICT       = ACT_REJECT + COND_IF_CONFLICT
-ACT_UPDATE_AND_NOTIFY        = ACT_UPDATE + COND_NOTIFY
-ACT_SAVE_TO_CALENDAR         = 512
+ACT_SAVE_TO_FOLDER = 32
+
+COND_IF_AVAILABLE  = 64
+COND_IF_CONFLICT   = 128
+COND_TENTATIVE     = 256
+COND_NOTIFY        = 512
+COND_TYPE_EVENT    = 1024
+COND_TYPE_TASK     = 2048
+COND_TYPE_ALL      = COND_TYPE_EVENT + COND_TYPE_TASK
+
+ACT_TENTATIVE         = ACT_ACCEPT + COND_TENTATIVE
+ACT_UPDATE_AND_NOTIFY = ACT_UPDATE + COND_NOTIFY
 
 FOLDER_TYPE_ANNOTATION = '/vendor/kolab/folder-type'
 
@@ -72,21 +74,56 @@ MESSAGE_PROCESSED = 1
 MESSAGE_FORWARD   = 2
 
 policy_name_map = {
-    'ACT_MANUAL':                   ACT_MANUAL,
-    'ACT_ACCEPT':                   ACT_ACCEPT,
-    'ACT_ACCEPT_IF_NO_CONFLICT':    ACT_ACCEPT_IF_NO_CONFLICT,
-    'ACT_TENTATIVE':                ACT_TENTATIVE,
-    'ACT_TENTATIVE_IF_NO_CONFLICT': ACT_TENTATIVE_IF_NO_CONFLICT,
-    'ACT_DELEGATE':                 ACT_DELEGATE,
-    'ACT_DELEGATE_IF_CONFLICT':     ACT_DELEGATE_IF_CONFLICT,
-    'ACT_REJECT':                   ACT_REJECT,
-    'ACT_REJECT_IF_CONFLICT':       ACT_REJECT_IF_CONFLICT,
-    'ACT_UPDATE':                   ACT_UPDATE,
-    'ACT_UPDATE_AND_NOTIFY':        ACT_UPDATE_AND_NOTIFY,
-    'ACT_SAVE_TO_CALENDAR':         ACT_SAVE_TO_CALENDAR
+    # policy values applying to all object types
+    'ALL_MANUAL':                     ACT_MANUAL + COND_TYPE_ALL,
+    'ALL_ACCEPT':                     ACT_ACCEPT + COND_TYPE_ALL,
+    'ALL_REJECT':                     ACT_REJECT + COND_TYPE_ALL,
+    'ALL_DELEGATE':                   ACT_DELEGATE + COND_TYPE_ALL,  # not implemented
+    'ALL_UPDATE':                     ACT_UPDATE + COND_TYPE_ALL,
+    'ALL_UPDATE_AND_NOTIFY':          ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL,
+    'ALL_SAVE_TO_FOLDER':             ACT_SAVE_TO_FOLDER + COND_TYPE_ALL,
+    # event related policy values
+    'EVENT_MANUAL':                   ACT_MANUAL + COND_TYPE_EVENT,
+    'EVENT_ACCEPT':                   ACT_ACCEPT + COND_TYPE_EVENT,
+    'EVENT_TENTATIVE':                ACT_TENTATIVE + COND_TYPE_EVENT,
+    'EVENT_REJECT':                   ACT_REJECT + COND_TYPE_EVENT,
+    'EVENT_DELEGATE':                 ACT_DELEGATE + COND_TYPE_EVENT,  # not implemented
+    'EVENT_UPDATE':                   ACT_UPDATE + COND_TYPE_EVENT,
+    'EVENT_UPDATE_AND_NOTIFY':        ACT_UPDATE_AND_NOTIFY + COND_TYPE_EVENT,
+    'EVENT_ACCEPT_IF_NO_CONFLICT':    ACT_ACCEPT + COND_IF_AVAILABLE + COND_TYPE_EVENT,
+    'EVENT_TENTATIVE_IF_NO_CONFLICT': ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE + COND_TYPE_EVENT,
+    'EVENT_DELEGATE_IF_CONFLICT':     ACT_DELEGATE + COND_IF_CONFLICT + COND_TYPE_EVENT,
+    'EVENT_REJECT_IF_CONFLICT':       ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT,
+    'EVENT_SAVE_TO_FOLDER':           ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT,
+    # task related policy values
+    'TASK_MANUAL':                    ACT_MANUAL + COND_TYPE_TASK,
+    'TASK_ACCEPT':                    ACT_ACCEPT + COND_TYPE_TASK,
+    'TASK_REJECT':                    ACT_REJECT + COND_TYPE_TASK,
+    'TASK_DELEGATE':                  ACT_DELEGATE + COND_TYPE_TASK,  # not implemented
+    'TASK_UPDATE':                    ACT_UPDATE + COND_TYPE_TASK,
+    'TASK_UPDATE_AND_NOTIFY':         ACT_UPDATE_AND_NOTIFY + COND_TYPE_TASK,
+    'TASK_SAVE_TO_FOLDER':            ACT_SAVE_TO_FOLDER + COND_TYPE_TASK,
+    # legacy values
+    'ACT_MANUAL':                     ACT_MANUAL + COND_TYPE_ALL,
+    'ACT_ACCEPT':                     ACT_ACCEPT + COND_TYPE_ALL,
+    'ACT_ACCEPT_IF_NO_CONFLICT':      ACT_ACCEPT + COND_IF_AVAILABLE + COND_TYPE_EVENT,
+    'ACT_TENTATIVE':                  ACT_TENTATIVE + COND_TYPE_EVENT,
+    'ACT_TENTATIVE_IF_NO_CONFLICT':   ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE + COND_TYPE_EVENT,
+    'ACT_DELEGATE':                   ACT_DELEGATE + COND_TYPE_ALL,
+    'ACT_DELEGATE_IF_CONFLICT':       ACT_DELEGATE + COND_IF_CONFLICT + COND_TYPE_EVENT,
+    'ACT_REJECT':                     ACT_REJECT + COND_TYPE_ALL,
+    'ACT_REJECT_IF_CONFLICT':         ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT,
+    'ACT_UPDATE':                     ACT_UPDATE + COND_TYPE_ALL,
+    'ACT_UPDATE_AND_NOTIFY':          ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL,
+    'ACT_SAVE_TO_CALENDAR':           ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT,
 }
 
-policy_value_map = dict([(v, k) for (k, v) in policy_name_map.iteritems()])
+policy_value_map = dict([(v &~ COND_TYPE_ALL, k) for (k, v) in policy_name_map.iteritems()])
+
+object_type_conditons = {
+    'event': COND_TYPE_EVENT,
+    'task':  COND_TYPE_TASK
+}
 
 log = pykolab.getLogger('pykolab.wallace')
 conf = pykolab.getConf()
@@ -210,17 +247,17 @@ def execute(*args, **kw):
     # An iTip message may contain multiple events. Later on, test if the message
     # is an iTip message by checking the length of this list.
     try:
-        itip_events = events_from_message(message, ['REQUEST', 'REPLY', 'CANCEL'])
+        itip_events = objects_from_message(message, ['VEVENT','VTODO'], ['REQUEST', 'REPLY', 'CANCEL'])
     except Exception, e:
-        log.error(_("Failed to parse iTip events from message: %r" % (e)))
+        log.error(_("Failed to parse iTip objects from message: %r" % (e)))
         itip_events = []
 
     if not len(itip_events) > 0:
-        log.info(_("Message is not an iTip message or does not contain any (valid) iTip events."))
+        log.info(_("Message is not an iTip message or does not contain any (valid) iTip objects."))
 
     else:
         any_itips = True
-        log.debug(_("iTip events attached to this message contain the following information: %r") % (itip_events), level=9)
+        log.debug(_("iTip objects attached to this message contain the following information: %r") % (itip_events), level=9)
 
     # See if any iTip actually allocates a user.
     if any_itips and len([x['uid'] for x in itip_events if x.has_key('attendees') or x.has_key('organizer')]) > 0:
@@ -239,7 +276,7 @@ def execute(*args, **kw):
         log.debug(_("iTips, but no users, pass along %r") % (filepath), level=5)
         return filepath
 
-    # we're looking at the first itip event object
+    # we're looking at the first itip object
     itip_event = itip_events[0]
 
     # for replies, the organizer is the recipient
@@ -267,7 +304,8 @@ def execute(*args, **kw):
         pykolab.translate.setUserLanguage(receiving_user['preferredlanguage'])
 
     # find user's kolabInvitationPolicy settings and the matching policy values
-    policies = get_matching_invitation_policies(receiving_user, sender_email)
+    type_condition = object_type_conditons.get(itip_event['type'], COND_TYPE_ALL)
+    policies = get_matching_invitation_policies(receiving_user, sender_email, type_condition)
 
     # select a processing function according to the iTip request method
     method_processing_map = {
@@ -326,31 +364,32 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
         return MESSAGE_FORWARD
 
     # process request to participating attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION
+    is_task = itip_event['type'] == 'task'
     nonpart = receiving_attendee.get_role() == kolabformat.NonParticipant
     partstat = receiving_attendee.get_participant_status()
-    save_event = not nonpart or not partstat == kolabformat.PartNeedsAction
+    save_object = not nonpart or not partstat == kolabformat.PartNeedsAction
     rsvp = receiving_attendee.get_rsvp()
     scheduling_required = rsvp or partstat == kolabformat.PartNeedsAction
     respond_with = receiving_attendee.get_participant_status(True)
     condition_fulfilled = True
 
     # find existing event in user's calendar
-    existing = find_existing_event(itip_event['uid'], receiving_user, True)
+    existing = find_existing_object(itip_event['uid'], itip_event['type'], receiving_user, True)
 
     # compare sequence number to determine a (re-)scheduling request
     if existing is not None:
-        log.debug(_("Existing event: %r") % (existing), level=9)
+        log.debug(_("Existing %s: %r") % (existing.type, existing), level=9)
         scheduling_required = itip_event['sequence'] > 0 and itip_event['sequence'] > existing.get_sequence()
-        save_event = True
+        save_object = True
 
-    # if scheduling: check availability
+    # if scheduling: check availability (skip that for tasks)
     if scheduling_required:
-        if policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT):
+        if not is_task and policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT):
             condition_fulfilled = check_availability(itip_event, receiving_user)
-        if policy & COND_IF_CONFLICT:
+        if not is_task and policy & COND_IF_CONFLICT:
             condition_fulfilled = not condition_fulfilled
 
-        log.debug(_("Precondition for event %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5)
+        log.debug(_("Precondition for object %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5)
 
         respond_with = None
         if policy & ACT_ACCEPT and condition_fulfilled:
@@ -373,13 +412,13 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
         # send iTip reply
         if respond_with is not None:
             receiving_attendee.set_participant_status(respond_with)
-            send_reply(recipient_email, itip_event, invitation_response_text(),
+            send_reply(recipient_email, itip_event, invitation_response_text(itip_event['type']),
                 subject=_('"%(summary)s" has been %(status)s'))
 
-        elif policy & ACT_SAVE_TO_CALENDAR:
-            # copy the invitation into the user's calendar with PARTSTAT=NEEDS-ACTION
+        elif policy & ACT_SAVE_TO_FOLDER:
+            # copy the invitation into the user's default folder with PARTSTAT=NEEDS-ACTION
             itip_event['xml'].set_attendee_participant_status(receiving_attendee, 'NEEDS-ACTION')
-            save_event = True
+            save_object = True
 
         else:
             # policy doesn't match, pass on to next one
@@ -389,17 +428,17 @@ def process_itip_request(itip_event, policy, recipient_email, sender_email, rece
         log.debug(_("No RSVP for recipient %r requested") % (receiving_user['mail']), level=8)
         # TODO: only update if policy & ACT_UPDATE ?
 
-    if save_event:
+    if save_object:
         targetfolder = None
 
         if existing:
             # delete old version from IMAP
             targetfolder = existing._imap_folder
-            delete_event(existing)
+            delete_object(existing)
 
         if not nonpart or existing:
             # save new copy from iTip
-            if store_event(itip_event['xml'], receiving_user, targetfolder):
+            if store_object(itip_event['xml'], receiving_user, targetfolder):
                 return MESSAGE_PROCESSED
 
     return None
@@ -426,23 +465,23 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
 
         # find existing event in user's calendar
         # sets/checks lock to avoid concurrent wallace processes trying to update the same event simultaneously
-        existing = find_existing_event(itip_event['uid'], receiving_user, True)
+        existing = find_existing_object(itip_event['uid'], itip_event['type'], receiving_user, True)
 
         if existing:
             # compare sequence number to avoid outdated replies?
             if not itip_event['sequence'] == existing.get_sequence():
-                log.info(_("The iTip reply sequence (%r) doesn't match the referred event version (%r). Forwarding to Inbox.") % (
+                log.info(_("The iTip reply sequence (%r) doesn't match the referred object version (%r). Forwarding to Inbox.") % (
                     itip_event['sequence'], existing.get_sequence()
                 ))
                 remove_write_lock(existing._lock_key)
                 return MESSAGE_FORWARD
 
-            log.debug(_("Auto-updating event %r on iTip REPLY") % (existing.uid), level=8)
+            log.debug(_("Auto-updating %s %r on iTip REPLY") % (existing.type, existing.uid), level=8)
             try:
                 existing_attendee = existing.get_attendee(sender_email)
                 existing.set_attendee_participant_status(sender_email, sender_attendee.get_participant_status(), rsvp=False)
             except Exception, e:
-                log.error("Could not find corresponding attende in organizer's event: %r" % (e))
+                log.error("Could not find corresponding attende in organizer's copy: %r" % (e))
 
                 # append delegated-from attendee ?
                 if len(sender_attendee.get_delegated_from()) > 0:
@@ -475,19 +514,19 @@ def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiv
                 except Exception, e:
                     log.error("Could not find delegated-to attendee: %r" % (e))
 
-            # update the organizer's copy of the event
-            if update_event(existing, receiving_user):
+            # update the organizer's copy of the object
+            if update_object(existing, receiving_user):
                 if policy & COND_NOTIFY:
                     send_reply_notification(existing, receiving_user)
 
                 # update all other attendee's copies
                 if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'):
-                    propagate_changes_to_attendees_calendars(existing)
+                    propagate_changes_to_attendees_accounts(existing)
 
                 return MESSAGE_PROCESSED
 
         else:
-            log.error(_("The event referred by this reply was not found in the user's calendars. Forwarding to Inbox."))
+            log.error(_("The object referred by this reply was not found in the user's folders. Forwarding to Inbox."))
             return MESSAGE_FORWARD
 
     return None
@@ -505,18 +544,21 @@ def process_itip_cancel(itip_event, policy, recipient_email, sender_email, recei
 
     # auto-update the local copy with STATUS=CANCELLED
     if policy & ACT_UPDATE:
-        # find existing event in user's calendar
-        existing = find_existing_event(itip_event['uid'], receiving_user, True)
+        # find existing object in user's folders
+        existing = find_existing_object(itip_event['uid'], itip_event['type'], receiving_user, True)
 
         if existing:
             existing.set_status('CANCELLED')
             existing.set_transparency(True)
-            if update_event(existing, receiving_user):
-                # TODO: send cancellation notification if policy & ACT_UPDATE_AND_NOTIFY: ?
+            if update_object(existing, receiving_user):
+                # send cancellation notification
+                if policy & ACT_UPDATE_AND_NOTIFY:
+                    send_cancel_notification(existing, receiving_user)
+
                 return MESSAGE_PROCESSED
 
         else:
-            log.error(_("The event referred by this reply was not found in the user's calendars. Forwarding to Inbox."))
+            log.error(_("The object referred by this reply was not found in the user's folders. Forwarding to Inbox."))
             return MESSAGE_FORWARD
 
     return None
@@ -562,7 +604,7 @@ def user_dn_from_email_address(email_address):
 user_dn_from_email_address.cache = {}
 
 
-def get_matching_invitation_policies(receiving_user, sender_email):
+def get_matching_invitation_policies(receiving_user, sender_email, type_condition=COND_TYPE_ALL):
     # get user's kolabInvitationPolicy settings
     policies = receiving_user['kolabinvitationpolicy'] if receiving_user.has_key('kolabinvitationpolicy') else []
     if policies and not isinstance(policies, list):
@@ -583,7 +625,10 @@ def get_matching_invitation_policies(receiving_user, sender_email):
         if domain == '' or domain == '*' or str(sender_email).endswith(domain):
             value = value.upper()
             if policy_name_map.has_key(value):
-                matches.append(policy_name_map[value])
+                val = policy_name_map[value]
+                # append if type condition matches
+                if val & type_condition:
+                    matches.append(val &~ COND_TYPE_ALL)
 
     # add manual as default action
     if len(matches) == 0:
@@ -594,7 +639,7 @@ def get_matching_invitation_policies(receiving_user, sender_email):
 
 def imap_proxy_auth(user_rec):
     """
-        
+        Perform IMAP login using proxy authentication with admin credentials
     """
     global imap
 
@@ -624,23 +669,23 @@ def imap_proxy_auth(user_rec):
     return True
 
 
-def list_user_calendars(user_rec):
+def list_user_folders(user_rec, type):
     """
-        Get a list of the given user's private calendar folders
+        Get a list of the given user's private calendar/tasks folders
     """
     global imap
 
     # return cached list
-    if user_rec.has_key('_calendar_folders'):
-        return user_rec['_calendar_folders'];
+    if user_rec.has_key('_imap_folders'):
+        return user_rec['_imap_folders'];
 
-    calendars = []
+    result = []
 
     if not imap_proxy_auth(user_rec):
-        return calendars
+        return result
 
     folders = imap.list_folders('*')
-    log.debug(_("List calendar folders for user %r: %r") % (user_rec['mail'], folders), level=8)
+    log.debug(_("List %r folders for user %r: %r") % (type, user_rec['mail'], folders), level=8)
 
     (ns_personal, ns_other, ns_shared) = imap.namespaces()
 
@@ -658,23 +703,23 @@ def list_user_calendars(user_rec):
         metadata = imap.get_metadata(folder)
         log.debug(_("IMAP metadata for %r: %r") % (folder, metadata), level=9)
         if metadata.has_key(folder) and ( \
-            metadata[folder].has_key('/shared' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/shared' + FOLDER_TYPE_ANNOTATION].startswith('event') \
-            or metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].startswith('event')):
-            calendars.append(folder)
+            metadata[folder].has_key('/shared' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/shared' + FOLDER_TYPE_ANNOTATION].startswith(type) \
+            or metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].startswith(type)):
+            result.append(folder)
 
-            # store default calendar folder in user record
+            # store default folder folder in user record
             if metadata[folder].has_key('/private' + FOLDER_TYPE_ANNOTATION) and metadata[folder]['/private' + FOLDER_TYPE_ANNOTATION].endswith('.default'):
-                user_rec['_default_calendar'] = folder
+                user_rec['_default_folder'] = folder
 
     # cache with user record
-    user_rec['_calendar_folders'] = calendars
+    user_rec['_imap_folders'] = result
 
-    return calendars
+    return result
 
 
-def find_existing_event(uid, user_rec, lock=False):
+def find_existing_object(uid, type, user_rec, lock=False):
     """
-        Search user's calendar folders for the given event (by UID)
+        Search user's private folders for the given object (by UID+type)
     """
     global imap
 
@@ -685,8 +730,8 @@ def find_existing_event(uid, user_rec, lock=False):
         set_write_lock(lock_key)
 
     event = None
-    for folder in list_user_calendars(user_rec):
-        log.debug(_("Searching folder %r for event %r") % (folder, uid), level=8)
+    for folder in list_user_folders(user_rec, type):
+        log.debug(_("Searching folder %r for %s %r") % (folder, type, uid), level=8)
         imap.imap.m.select(imap.folder_utf7(folder))
 
         typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid))
@@ -694,11 +739,15 @@ def find_existing_event(uid, user_rec, lock=False):
             typ, data = imap.imap.m.fetch(num, '(RFC822)')
 
             try:
-                event = event_from_message(message_from_string(data[0][1]))
+                if type == 'task':
+                    event = todo_from_message(message_from_string(data[0][1]))
+                else:
+                    event = event_from_message(message_from_string(data[0][1]))
+
                 setattr(event, '_imap_folder', folder)
                 setattr(event, '_lock_key', lock_key)
             except Exception, e:
-                log.error(_("Failed to parse event from message %s/%s: %s") % (folder, num, traceback.format_exc()))
+                log.error(_("Failed to parse %s from message %s/%s: %s") % (type, folder, num, traceback.format_exc()))
                 continue
 
             if event and event.uid == uid:
@@ -723,7 +772,7 @@ def check_availability(itip_event, receiving_user):
     if itip_event.has_key('_conflicts'):
         return not itip_event['_conflicts']
 
-    for folder in list_user_calendars(receiving_user):
+    for folder in list_user_folders(receiving_user, 'event'):
         log.debug(_("Listing events from folder %r") % (folder), level=8)
         imap.imap.m.select(imap.folder_utf7(folder))
 
@@ -810,39 +859,40 @@ def get_lock_key(user, uid):
     return hashlib.md5("%s/%s" % (user['mail'], uid)).hexdigest()
 
 
-def update_event(event, user_rec):
+def update_object(object, user_rec):
     """
-        Update the given event in IMAP (i.e. delete + append)
+        Update the given object in IMAP (i.e. delete + append)
     """
     success = False
 
-    if hasattr(event, '_imap_folder'):
-        delete_event(event)
-        success = store_event(event, user_rec, event._imap_folder)
+    if hasattr(object, '_imap_folder'):
+        delete_object(object)
+        object.set_lastmodified()  # update last-modified timestamp
+        success = store_object(object, user_rec, object._imap_folder)
 
         # remove write lock for this event
-        if hasattr(event, '_lock_key') and event._lock_key is not None:
-            remove_write_lock(event._lock_key)
+        if hasattr(object, '_lock_key') and object._lock_key is not None:
+            remove_write_lock(object._lock_key)
 
     return success
 
 
-def store_event(event, user_rec, targetfolder=None):
+def store_object(object, user_rec, targetfolder=None):
     """
-        Append the given event object to the user's default calendar
+        Append the given object to the user's default calendar/tasklist
     """
-
-    # find default calendar folder to save event to
+    
+    # find default calendar folder to save object to
     if targetfolder is None:
-        targetfolder = list_user_calendars(user_rec)[0]
-        if user_rec.has_key('_default_calendar'):
-            targetfolder = user_rec['_default_calendar']
+        targetfolder = list_user_folders(user_rec, object.type)[0]
+        if user_rec.has_key('_default_folder'):
+            targetfolder = user_rec['_default_folder']
 
     if not targetfolder:
-        log.error(_("Failed to save event: no calendar folder found for user %r") % (user_rec['mail']))
+        log.error(_("Failed to save %s: no target folder found for user %r") % (object.type, user_rec['mail']))
         return Fasle
 
-    log.debug(_("Save event %r to user calendar %r") % (event.uid, targetfolder), level=8)
+    log.debug(_("Save %s %r to user folder %r") % (object.type, object.uid, targetfolder), level=8)
 
     try:
         imap.imap.m.select(imap.folder_utf7(targetfolder))
@@ -850,29 +900,29 @@ def store_event(event, user_rec, targetfolder=None):
             imap.folder_utf7(targetfolder),
             None,
             None,
-            event.to_message(creator="Kolab Server <wallace at localhost>").as_string()
+            object.to_message(creator="Kolab Server <wallace at localhost>").as_string()
         )
         return result
 
     except Exception, e:
-        log.error(_("Failed to save event to user calendar at %r: %r") % (
-            targetfolder, e
+        log.error(_("Failed to save %s to user folder at %r: %r") % (
+            object.type, targetfolder, e
         ))
 
     return False
 
 
-def delete_event(existing):
+def delete_object(existing):
     """
-        Removes the IMAP object with the given UID from a user's calendar folder
+        Removes the IMAP object with the given UID from a user's folder
     """
     targetfolder = existing._imap_folder
     imap.imap.m.select(imap.folder_utf7(targetfolder))
 
     typ, data = imap.imap.m.search(None, '(HEADER SUBJECT "%s")' % existing.uid)
 
-    log.debug(_("Delete event %r in %r: %r") % (
-        existing.uid, targetfolder, data
+    log.debug(_("Delete %s %r in %r: %r") % (
+        existing.type, existing.uid, targetfolder, data
     ), level=8)
 
     for num in data[0].split():
@@ -881,7 +931,7 @@ def delete_event(existing):
     imap.imap.m.expunge()
 
 
-def send_reply_notification(event, receiving_user):
+def send_reply_notification(object, receiving_user):
     """
         Send a (consolidated) notification about the current participant status to organizer
     """
@@ -891,18 +941,18 @@ def send_reply_notification(event, receiving_user):
     from email.MIMEText import MIMEText
     from email.Utils import formatdate
 
-    log.debug(_("Compose participation status summary for event %r to user %r") % (
-        event.uid, receiving_user['mail']
+    log.debug(_("Compose participation status summary for %s %r to user %r") % (
+        object.type, object.uid, receiving_user['mail']
     ), level=8)
 
-    organizer = event.get_organizer()
+    organizer = object.get_organizer()
     orgemail = organizer.email()
     orgname = organizer.name()
 
     auto_replies_expected = 0
     auto_replies_received = 0
-    partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'PENDING':[] }
-    for attendee in event.get_attendees():
+    partstats = { 'ACCEPTED':[], 'TENTATIVE':[], 'DECLINED':[], 'DELEGATED':[], 'IN-PROCESS':[], 'COMPLETED':[], 'PENDING':[] }
+    for attendee in object.get_attendees():
         parstat = attendee.get_participant_status(True)
         if partstats.has_key(parstat):
             partstats[parstat].append(attendee.get_displayname())
@@ -921,7 +971,7 @@ def send_reply_notification(event, receiving_user):
 
         if attendee_dn:
             attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy'])
-            if is_auto_reply(attendee_rec, orgemail):
+            if is_auto_reply(attendee_rec, orgemail, object.type):
                 auto_replies_expected += 1
                 if not parstat == 'NEEDS-ACTION':
                     auto_replies_received += 1
@@ -938,21 +988,33 @@ def send_reply_notification(event, receiving_user):
         if len(attendees) > 0:
             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.
-        %(roundup)s
-    """ % {
-        'summary': event.get_summary(),
-        'start': event.get_start().strftime('%Y-%m-%d %H:%M %Z'),
-        'roundup': roundup
-    }
+    # compose different notification texts for events/tasks
+    if object.type == 'task':
+        message_text = """
+            The assignment for '%(summary)s' has been updated in your tasklist.
+            %(roundup)s
+        """ % {
+            'summary': object.get_summary(),
+            'roundup': roundup
+        }
+    else:
+        message_text = """
+            The event '%(summary)s' at %(start)s has been updated in your calendar.
+            %(roundup)s
+        """ % {
+            'summary': object.get_summary(),
+            'start': object.get_start().strftime('%Y-%m-%d %H:%M %Z'),
+            'roundup': roundup
+        }
+
+    message_text += "\n" + _("*** This is an automated message. Please do not reply. ***")
 
     # compose mime message
     msg = MIMEText(utils.stripped_message(message_text))
 
     msg['To'] = receiving_user['mail']
     msg['Date'] = formatdate(localtime=True)
-    msg['Subject'] = _('"%s" has been updated') % (event.get_summary())
+    msg['Subject'] = _('"%s" has been updated') % (object.get_summary())
     msg['From'] = '"%s" <%s>' % (orgname, orgemail) if orgname else orgemail
 
     smtp = smtplib.SMTP("localhost", 10027)
@@ -968,10 +1030,68 @@ def send_reply_notification(event, receiving_user):
     smtp.quit()
 
 
-def is_auto_reply(user, sender_email):
+def send_cancel_notification(object, receiving_user):
+    """
+        Send a notification about event/task cancellation
+    """
+    import smtplib
+    from email.MIMEText import MIMEText
+    from email.Utils import formatdate
+
+    log.debug(_("Send cancellation notification for %s %r to user %r") % (
+        object.type, object.uid, receiving_user['mail']
+    ), level=8)
+
+    organizer = object.get_organizer()
+    orgemail = organizer.email()
+    orgname = organizer.name()
+
+    # compose different notification texts for events/tasks
+    if object.type == 'task':
+        message_text = """
+            The assignment for '%(summary)s' has been cancelled by %(organizer)s.
+            The copy in your tasklist as been marked as cancelled accordingly.
+        """ % {
+            'summary': object.get_summary(),
+            'organizer': orgname if orgname else orgemail
+        }
+    else:
+        message_text = """
+            The event '%(summary)s' at %(start)s has been cancelled by %(organizer)s.
+            The copy in your calendar as been marked as cancelled accordingly.
+        """ % {
+            'summary': object.get_summary(),
+            'start': object.get_start().strftime('%Y-%m-%d %H:%M %Z'),
+            'organizer': orgname if orgname else orgemail
+        }
+
+    message_text += "\n" + _("*** This is an automated message. Please do not reply. ***")
+
+    # compose mime message
+    msg = MIMEText(utils.stripped_message(message_text))
+
+    msg['To'] = receiving_user['mail']
+    msg['Date'] = formatdate(localtime=True)
+    msg['Subject'] = _('"%s" has been cancelled') % (object.get_summary())
+    msg['From'] = '"%s" <%s>' % (orgname, orgemail) if orgname else orgemail
+
+    smtp = smtplib.SMTP("localhost", 10027)
+
+    if conf.debuglevel > 8:
+        smtp.set_debuglevel(True)
+
+    try:
+        smtp.sendmail(orgemail, receiving_user['mail'], msg.as_string())
+    except Exception, e:
+        log.error(_("SMTP sendmail error: %r") % (e))
+
+    smtp.quit()
+
+
+def is_auto_reply(user, sender_email, type):
     accept_available = False
     accept_conflicts = False
-    for policy in get_matching_invitation_policies(user, sender_email):
+    for policy in get_matching_invitation_policies(user, sender_email, object_type_conditons.get(type, COND_TYPE_EVENT)):
         if policy & (ACT_ACCEPT | ACT_REJECT | ACT_DELEGATE):
             if check_policy_condition(policy, True):
                 accept_available = True
@@ -983,7 +1103,7 @@ def is_auto_reply(user, sender_email):
             return True
 
         # manual action reached
-        if policy & (ACT_MANUAL | ACT_SAVE_TO_CALENDAR):
+        if policy & (ACT_MANUAL | ACT_SAVE_TO_FOLDER):
             return False
 
     return False
@@ -998,45 +1118,46 @@ def check_policy_condition(policy, available):
     return condition_fulfilled
 
 
-def propagate_changes_to_attendees_calendars(event):
+def propagate_changes_to_attendees_accounts(object):
     """
-        Find and update copies of this event in all attendee's calendars
+        Find and update copies of this object in all attendee's personal folders
     """
-    for attendee in event.get_attendees():
+    for attendee in object.get_attendees():
         attendee_user_dn = user_dn_from_email_address(attendee.get_email())
         if attendee_user_dn:
             attendee_user = auth.get_entry_attributes(None, attendee_user_dn, ['*'])
-            attendee_event = find_existing_event(event.uid, attendee_user, True)  # does IMAP authenticate
-            if attendee_event:
+            attendee_object = find_existing_object(object.uid, object.type, attendee_user, True)  # does IMAP authenticate
+            if attendee_object:
                 try:
-                    attendee_entry = attendee_event.get_attendee_by_email(attendee_user['mail'])
+                    attendee_entry = attendee_object.get_attendee_by_email(attendee_user['mail'])
                 except:
                     attendee_entry = None
 
-                # copy all attendees from master event (covers additions and removals)
+                # copy all attendees from master object (covers additions and removals)
                 new_attendees = kolabformat.vectorattendee();
-                for a in event.get_attendees():
+                for a in object.get_attendees():
                     # keep my own entry intact
                     if attendee_entry is not None and attendee_entry.get_email() == a.get_email():
                         new_attendees.append(attendee_entry)
                     else:
                         new_attendees.append(a)
 
-                attendee_event.event.setAttendees(new_attendees)
+                attendee_object.event.setAttendees(new_attendees)
 
-                success = update_event(attendee_event, attendee_user)
-                log.debug(_("Updated %s's copy of %r: %r") % (attendee_user['mail'], event.uid, success), level=8)
+                success = update_object(attendee_object, attendee_user)
+                log.debug(_("Updated %s's copy of %r: %r") % (attendee_user['mail'], object.uid, success), level=8)
 
             else:
-                log.debug(_("Attendee %s's copy of %r not found") % (attendee_user['mail'], event.uid), level=8)
+                log.debug(_("Attendee %s's copy of %r not found") % (attendee_user['mail'], object.uid), level=8)
 
         else:
             log.debug(_("Attendee %r not found in LDAP") % (attendee.get_email()), level=8)
 
 
-def invitation_response_text():
-    return _("""
-        %(name)s has %(status)s your invitation for %(summary)s.
+def invitation_response_text(type):
+    footer = "\n\n" + _("*** This is an automated message. Please do not reply. ***")
 
-        *** This is an automated response sent by the Kolab Invitation system ***
-    """)
+    if type == 'task':
+        return _("%(name)s has %(status)s your assignment for %(summary)s.") + footer
+    else:
+        return _("%(name)s has %(status)s your invitation for %(summary)s.") + footer


commit c5978eb22295fea9a09066dc177a5d167c58fb2c
Author: Thomas Bruederli <bruederli at kolabsys.com>
Date:   Thu Aug 21 07:14:10 2014 -0400

    Make sure created and last-modified dates are saved in UTC; add folder type property to groupware objects

diff --git a/pykolab/xml/contact.py b/pykolab/xml/contact.py
index 9a2c103..97987d9 100644
--- a/pykolab/xml/contact.py
+++ b/pykolab/xml/contact.py
@@ -1,6 +1,8 @@
 import kolabformat
 
 class Contact(kolabformat.Contact):
+    type = 'contact'
+
     def __init__(self, *args, **kw):
         kolabformat.Contact.__init__(self, *args, **kw)
 
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index 34f857a..24b026e 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -43,6 +43,8 @@ def event_from_message(message):
 
 
 class Event(object):
+    type = 'event'
+
     status_map = {
             "TENTATIVE": kolabformat.StatusTentative,
             "CONFIRMED": kolabformat.StatusConfirmed,
@@ -612,9 +614,9 @@ class Event(object):
 
     def set_created(self, _datetime=None):
         if _datetime == None:
-            _datetime = datetime.datetime.now()
+            _datetime = datetime.datetime.utcnow()
 
-        self.event.setCreated(xmlutils.to_cdatetime(_datetime, False))
+        self.event.setCreated(xmlutils.to_cdatetime(_datetime, False, True))
 
     def set_description(self, description):
         self.event.setDescription(str(description))
@@ -624,7 +626,7 @@ class Event(object):
             self.event.setComment(str(comment))
 
     def set_dtstamp(self, _datetime):
-        self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False))
+        self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False, True))
 
     def set_end(self, _datetime):
         valid_datetime = False
@@ -771,12 +773,12 @@ class Event(object):
 
         if _datetime == None:
             valid_datetime = True
-            _datetime = datetime.datetime.now()
+            _datetime = datetime.datetime.utcnow()
 
         if not valid_datetime:
             raise InvalidEventDateError, _("Event start needs datetime.date or datetime.datetime instance")
 
-        self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False))
+        self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False, True))
 
     def set_location(self, location):
         self.event.setLocation(str(location))
@@ -938,7 +940,7 @@ class Event(object):
         msg['Date'] = formatdate(localtime=True)
 
         msg.add_header('X-Kolab-MIME-Version', '3.0')
-        msg.add_header('X-Kolab-Type', 'application/x-vnd.kolab.event')
+        msg.add_header('X-Kolab-Type', 'application/x-vnd.kolab.' + self.type)
 
         text = utils.multiline_message("""
                     This is a Kolab Groupware object. To view this object you
diff --git a/pykolab/xml/todo.py b/pykolab/xml/todo.py
index 28a7b4d..b04b233 100644
--- a/pykolab/xml/todo.py
+++ b/pykolab/xml/todo.py
@@ -34,6 +34,7 @@ def todo_from_message(message):
 
 # FIXME: extend a generic pykolab.xml.Xcal class instead of Event
 class Todo(Event):
+    type = 'task'
 
     def __init__(self, from_ical="", from_string=""):
         self._attendees = []
diff --git a/pykolab/xml/utils.py b/pykolab/xml/utils.py
index bcaa480..aa05e11 100644
--- a/pykolab/xml/utils.py
+++ b/pykolab/xml/utils.py
@@ -62,10 +62,18 @@ def from_cdatetime(_cdatetime, with_timezone=True):
         return datetime.datetime(year, month, day, hour, minute, second)
 
 
-def to_cdatetime(_datetime, with_timezone=True):
+def to_cdatetime(_datetime, with_timezone=True, as_utc=False):
     """
         Convert a datetime.dateime object into a kolabformat.cDateTime instance
     """
+    # convert date into UTC timezone
+    if as_utc and hasattr(_datetime, "tzinfo"):
+        if _datetime.tzinfo is not None:
+            _datetime = _datetime.astimezone(pytz.utc)
+        else:
+            datetime = _datetime.replace(tzinfo=pytz.utc)
+        with_timezone = False
+
     (
         year,
         month,
@@ -97,4 +105,7 @@ def to_cdatetime(_datetime, with_timezone=True):
         else:
             _cdatetime.setTimezone(_datetime.tzinfo.__str__())
 
+    if as_utc:
+        _cdatetime.setUTC(True)
+
     return _cdatetime




More information about the commits mailing list