7 commits - conf/kolab.conf pykolab/imap pykolab/xml wallace/module_optout.py wallace/module_resources.py

Jeroen van Meeuwen vanmeeuwen at kolabsys.com
Tue May 22 04:47:24 CEST 2012


 conf/kolab.conf                  |   11 +
 pykolab/imap/__init__.py         |   36 +++-
 pykolab/xml/attendee.py          |   12 +
 pykolab/xml/contact_reference.py |    8 
 pykolab/xml/event.py             |  317 +++++++++++++++++++++++++++++++++------
 wallace/module_optout.py         |   10 -
 wallace/module_resources.py      |  200 +++++++++++++++++++++---
 7 files changed, 518 insertions(+), 76 deletions(-)

New commits:
commit 3e893314d7c0a6caf007c98b9089d596669416e0
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue May 22 03:46:42 2012 +0100

    Update Wallace resource module to send iTip responses to the organizer
    Create directory tree needed

diff --git a/wallace/module_resources.py b/wallace/module_resources.py
index 54cf85f..c9be1f1 100644
--- a/wallace/module_resources.py
+++ b/wallace/module_resources.py
@@ -17,6 +17,7 @@
 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 #
 
+import datetime
 import icalendar
 import json
 import os
@@ -50,15 +51,27 @@ auth = None
 imap = None
 
 def __init__():
-    if not os.path.isdir(mybasepath):
-        os.makedirs(mybasepath)
-
     modules.register('resources', execute, description=description())
 
+def accept(filepath):
+    new_filepath = os.path.join(mybasepath, 'ACCEPT', os.path.basename(filepath))
+    os.rename(filepath, new_filepath)
+    filepath = new_filepath
+    exec('modules.cb_action_ACCEPT(%r, %r)' % ('resources',filepath))
+
 def description():
     return """Resource management module."""
 
 def execute(*args, **kw):
+    if not os.path.isdir(mybasepath):
+        os.makedirs(mybasepath)
+
+    for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER' ]:
+        if not os.path.isdir(os.path.join(mybasepath, stage)):
+            os.makedirs(os.path.join(mybasepath, stage))
+
+    log.debug(_("Resource Management called for %r, %r") % (args, kw), level=9)
+
     auth = Auth()
     auth.connect()
 
@@ -92,8 +105,11 @@ def execute(*args, **kw):
                 )
 
             return
-
-    log.debug(_("Resource Management called for %r, %r") % (args, kw), level=8)
+    else:
+        # Move to incoming
+        new_filepath = os.path.join(mybasepath, 'incoming', os.path.basename(filepath))
+        os.rename(filepath, new_filepath)
+        filepath = new_filepath
 
     message = message_from_file(open(filepath, 'r'))
 
@@ -107,7 +123,7 @@ def execute(*args, **kw):
                     "iTip.")
             )
 
-        exec('modules.cb_action_ACCEPT(%r, %r)' % ('resources',filepath))
+        accept(filepath)
         return
 
     else:
@@ -120,12 +136,14 @@ def execute(*args, **kw):
     # See if a resource is actually being allocated
     if len([x['resources'] for x in itip_events if x.has_key('resources')]) == 0:
         if len([x['attendees'] for x in itip_events if x.has_key('attendees')]) == 0:
-            exec('modules.cb_action_ACCEPT(%r, %r)' % ('resources',filepath))
+            accept(filepath)
             return
 
-    # A simple list of merely resource entry IDs
+    # A simple list of merely resource entry IDs that hold any relevance to the
+    # iTip events
     resource_records = resource_records_from_itip_events(itip_events)
 
+    # Get the resource details, which includes details on the IMAP folder
     resources = {}
     for resource_record in list(set(resource_records)):
         # Get the attributes for the record
@@ -135,13 +153,21 @@ def execute(*args, **kw):
         resource_attrs = auth.get_entry_attributes(None, resource_record, ['*'])
         if not 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]:
             if resource_attrs.has_key('uniquemember'):
+                resources[resource_record] = resource_attrs
                 for uniquemember in resource_attrs['uniquemember']:
                     resource_attrs = auth.get_entry_attributes(None, uniquemember, ['*'])
                     if 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]:
-                        resources[resource_record] = resource_attrs
+                        resources[uniquemember] = resource_attrs
+                        resources[uniquemember]['memberof'] = resource_record
         else:
             resources[resource_record] = resource_attrs
 
+    log.debug(_("Resources: %r") % (resources), level=8)
+
+    # For each resource, determine if any of the events in question is in
+    # conflict.
+    #
+    # Store the (first) conflicting event(s) alongside the resource information.
     for resource in resources.keys():
         if not resources[resource].has_key('kolabtargetfolder'):
             continue
@@ -162,7 +188,7 @@ def execute(*args, **kw):
         typ, data = imap.imap.m.search(None, 'ALL')
 
         for num in data[0].split():
-            # For efficiency, non-deterministic
+            # For efficiency, makes the routine non-deterministic
             if resources[resource]['conflict']:
                 continue
 
@@ -182,9 +208,35 @@ def execute(*args, **kw):
                             log.debug(_("  event %r start: %r") % (event.get_uid(),event.get_start()), level=9)
                             log.debug(_("  event %r end: %r") % (event.get_uid(),event.get_end()), level=9)
 
-                            if event.get_start() < itip['start']:
-                                if event.get_start() <= itip['end']:
-                                    if event.get_end() <= itip['start']:
+                            _es = event.get_start()
+                            _is = itip['start']
+
+                            if type(_es) == 'datetime.date':
+                                log.debug(_("_es is datetime.date"))
+                                if type(_is) == 'datetime.datetime':
+                                    _is = datetime.date(_is.year, _is.month, _is.day)
+                                else:
+                                    pass
+                            else:
+                                log.debug(_("_es is datetime.datetime"))
+                                if type(_is) == 'datetime.date':
+                                    log.debug(_("_is is datetime.date"))
+                                    _es = datetime.date(_es.year, _es.month, _es.day)
+
+                            _ee = event.get_end()
+                            _ie = itip['end']
+                            if type(_ee) == 'datetime.date':
+                                if type(_ie) == 'datetime.datetime':
+                                    _ie = datetime.date(_ie.year, _ie.month, _ie.day)
+                                else:
+                                    pass
+                            else:
+                                if type(_ie) == 'datetime.date':
+                                    _ee = datetime.date(_ee.year, _ee.month, _ee.day)
+
+                            if _es < _is:
+                                if _es <= _ie:
+                                    if _ie <= _is:
                                         conflict = False
                                     else:
                                         log.debug(_("Event %r ends later than invitation") % (event.get_uid()), level=9)
@@ -192,11 +244,11 @@ def execute(*args, **kw):
                                 else:
                                     log.debug(_("Event %r starts before invitation ends") % (event.get_uid()), level=9)
                                     conflict = True
-                            elif event.get_start() == itip['start']:
+                            elif _es == _is:
                                 log.debug(_("Event %r and invitation start at the same time") % (event.get_uid()), level=9)
                                 conflict = True
-                            else: # event.get_start() > itip['start']
-                                if event.get_start() <= itip['end']:
+                            else: # _es > _is
+                                if _es <= _ie:
                                     log.debug(_("Event %r starts before invitation ends") % (event.get_uid()), level=9)
                                     conflict = True
                                 else:
@@ -209,6 +261,91 @@ def execute(*args, **kw):
 
         log.debug(_("Resource status information: %r") % (resources[resource]), level=8)
 
+
+    for resource in resources.keys():
+        log.debug(_("Polling for resource %r") % (resource), level=9)
+
+        if not resources.has_key(resource):
+            log.debug(_("Resource %r has been popped from the list") % (resource), level=9)
+            continue
+
+        if not resources[resource].has_key('conflicting_events'):
+            log.debug(_("Resource is a collection"), level=9)
+            continue
+
+        if len(resources[resource]['conflicting_events']) > 0:
+            # This is the event being conflicted with!
+            for itip_event in itip_events:
+                # Now we have the event that was conflicting
+                if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
+                    itip_event['xml'].set_attendee_participant_status([a for a in itip_event['xml'].get_attendees() if a.get_email() == resources[resource]['mail']][0], "DECLINED")
+
+                    send_response(resources[resource]['mail'], itip_events)
+                else:
+                    # This must have been a resource collection originally.
+                    # We have inserted the reference to the original resource
+                    # record in 'memberof'.
+                    if resources[resource].has_key('memberof'):
+                        original_resource = resources[resources[resource]['memberof']]
+
+                        _target_resource = resources[original_resource['uniquemember'][random.randint(0,(len(original_resource['uniquemember'])-1))]]
+
+                        # unset all
+                        for _r in original_resource['uniquemember']:
+                            del resources[_r]
+
+                    if original_resource['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
+                        itip_event['xml'].set_attendee_participant_status(
+                                [a for a in itip_event['xml'].get_attendees() if a.get_email() == original_resource['mail']][0],
+                                "DECLINED"
+                            )
+
+                        send_response(original_resource['mail'], itip_events)
+
+        else:
+            # No conflicts, go accept
+            for itip_event in itip_events:
+                if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
+                    itip_event['xml'].set_attendee_participant_status(
+                            [a for a in itip_event['xml'].get_attendees() if a.get_email() == resources[resource]['mail']][0],
+                            "ACCEPTED"
+                        )
+
+                    log.debug(_("Adding event to %r") % (resources[resource]['kolabtargetfolder']), level=9)
+                    imap.imap.m.append(resources[resource]['kolabtargetfolder'], None, None, itip_event['xml'].to_message().as_string())
+
+                    send_response(resources[resource]['mail'], itip_events)
+
+                else:
+                    # This must have been a resource collection originally.
+                    # We have inserted the reference to the original resource
+                    # record in 'memberof'.
+                    if resources[resource].has_key('memberof'):
+                        original_resource = resources[resources[resource]['memberof']]
+
+                        _target_resource = resources[original_resource['uniquemember'][random.randint(0,(len(original_resource['uniquemember'])-1))]]
+
+                        # unset all
+                        for _r in original_resource['uniquemember']:
+                            del resources[_r]
+
+                    if original_resource['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]:
+                        itip_event['xml'].set_attendee_participant_status(
+                                [a for a in itip_event['xml'].get_attendees() if a.get_email() == original_resource['mail']][0],
+                                "ACCEPTED"
+                            )
+
+                        log.debug(_("Adding event to %r") % (_target_resource['kolabtargetfolder']), level=9)
+                        imap.imap.m.append(_target_resource['kolabtargetfolder'], None, None, itip_event['xml'].to_message().as_string())
+
+                        send_response(original_resource['mail'], itip_events)
+
+    # Disconnect IMAP or we lock the mailbox almost constantly
+    imap.disconnect()
+    del imap
+
+    os.unlink(filepath)
+
 def itip_events_from_message(message):
     """
         Obtain the iTip payload from email.message <message>
@@ -229,19 +366,14 @@ def itip_events_from_message(message):
 
                 if part.get_param('method') == "REQUEST":
                     # Python iCalendar prior to 3.0 uses "from_string".
-                    itip_payload = part.get_payload()
+                    itip_payload = part.get_payload(decode=True)
                     if hasattr(icalendar.Calendar, 'from_ical'):
                         cal = icalendar.Calendar.from_ical(itip_payload)
                     elif hasattr(icalendar.Calendar, 'from_string'):
                         cal = icalendar.Calendar.from_string(itip_payload)
                     else:
                         log.error(_("Could not read iTip from message."))
-                        exec(
-                                'modules.cb_action_ACCEPT(%r, %r)' % (
-                                        'resources',
-                                        filepath
-                                    )
-                            )
+                        accept(filepath)
 
                         return
 
@@ -294,10 +426,14 @@ def resource_records_from_itip_events(itip_events):
 
     resource_records = []
 
+    log.debug(_("Raw itip_events: %r") % (itip_events), level=9)
     attendees_raw = []
-    for list_attendees_raw in [x for x in [y['attendees'] for y in itip_events if y.has_key('attendees')]]:
+    for list_attendees_raw in [x for x in [y['attendees'] for y in itip_events if y.has_key('attendees') and isinstance(y['attendees'], list)]]:
         attendees_raw.extend(list_attendees_raw)
 
+    for list_attendees_raw in [y['attendees'] for y in itip_events if y.has_key('attendees') and isinstance(y['attendees'], basestring)]:
+        attendees_raw.append(list_attendees_raw)
+
     log.debug(_("Raw set of attendees: %r") % (attendees_raw), level=9)
 
     # TODO: Resources are actually not implemented in the format. We reset this
@@ -364,4 +500,18 @@ def resource_records_from_itip_events(itip_events):
 
     log.debug(_("The following resources are being referred to in the iTip: %r") % (resource_records), level=8)
 
-    return resource_records
\ No newline at end of file
+    return resource_records
+
+def send_response(from_address, itip_events):
+    import smtplib
+    smtp = smtplib.SMTP("localhost", 10027)
+
+    if conf.debuglevel > 8:
+        smtp.set_debuglevel(True)
+
+    for itip_event in itip_events:
+        attendee = [a for a in itip_event['xml'].get_attendees() if a.get_email() == from_address][0]
+        participant_status = itip_event['xml'].get_attendee_participant_status(attendee)
+        message = itip_event['xml'].to_message_itip(from_address, method="REPLY", participant_status=participant_status)
+        smtp.sendmail(message['From'], message['To'], message.as_string())
+    smtp.quit()


commit 7184d5f07958887e500907d28a38659595ea62c6
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue May 22 03:46:06 2012 +0100

    Create the necessary directories way after init

diff --git a/wallace/module_optout.py b/wallace/module_optout.py
index 1aedc7e..5540d80 100644
--- a/wallace/module_optout.py
+++ b/wallace/module_optout.py
@@ -41,15 +41,19 @@ conf = pykolab.getConf()
 mybasepath = '/var/spool/pykolab/wallace/optout/'
 
 def __init__():
-    if not os.path.isdir(mybasepath):
-        os.makedirs(mybasepath)
-
     modules.register('optout', execute, description=description())
 
 def description():
     return """Consult the opt-out service."""
 
 def execute(*args, **kw):
+    if not os.path.isdir(mybasepath):
+        os.makedirs(mybasepath)
+
+    for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER' ]:
+        if not os.path.isdir(os.path.join(mybasepath, stage)):
+            os.makedirs(os.path.join(mybasepath, stage))
+
     # TODO: Test for correct call.
     filepath = args[0]
 


commit fd2cee5019d765fa28e1f2d9c33bd5c4af08c367
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue May 22 03:45:34 2012 +0100

    Update event to spec of replying with sensible iTip messages

diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
index a23b785..f516ead 100644
--- a/pykolab/xml/event.py
+++ b/pykolab/xml/event.py
@@ -3,11 +3,16 @@ import icalendar
 import kolabformat
 import time
 
+import pykolab
 from pykolab import constants
+from pykolab import utils
+from pykolab.translate import _
 
 from attendee import Attendee
 from contact_reference import ContactReference
 
+log = pykolab.getLogger('pykolab.xml_event')
+
 def event_from_ical(string):
     return Event(from_ical=string)
 
@@ -15,7 +20,12 @@ def event_from_string(string):
     return Event(from_string=string)
 
 class Event(object):
-    StatusTentative = kolabformat.StatusTentative
+    status_map = {
+            "TENTATIVE": kolabformat.StatusTentative,
+            "CONFIRMED": kolabformat.StatusConfirmed,
+            "CANCELLED": kolabformat.StatusCancelled,
+        }
+
     def __init__(self, from_ical="", from_string=""):
         self._attendees = []
 
@@ -27,12 +37,13 @@ class Event(object):
         else:
             self.from_ical(from_ical)
 
-    def add_attendee(self, email, name=None, rsvp=False, role=None):
-        attendee = Attendee(email, name, rsvp, role)
+    def add_attendee(self, email, name=None, rsvp=False, role=None, participant_status=None):
+        log.debug(_("adding attendee with email address %r") % (email), level=9)
+        attendee = Attendee(email, name, rsvp, role, participant_status)
         self._attendees.append(attendee)
         self.event.setAttendees(self._attendees)
 
-    def as_string_itip(self):
+    def as_string_itip(self, method="REQUEST"):
         cal = icalendar.Calendar()
         cal.add(
                 'prodid',
@@ -46,7 +57,7 @@ class Event(object):
         # TODO: Really?
         cal.add('calscale', 'GREGORIAN')
         # TODO: Not always a request...
-        cal.add('method', 'REQUEST')
+        cal.add('method', method)
 
         # TODO: Add timezone information using icalendar.?()
         #       Not sure if there is a class for it.
@@ -117,6 +128,8 @@ class Event(object):
         elif hasattr(icalendar.Event, 'from_string'):
             ical_event = icalendar.Event.from_string(ical)
 
+        # TODO: Clause the timestamps for zulu suffix causing datetime.datetime
+        # to fail substitution.
         for attr in list(set(ical_event.required)):
             if ical_event.has_key(attr):
                 if hasattr(self, 'set_ical_%s' % (attr.lower())):
@@ -140,8 +153,11 @@ class Event(object):
                 else:
                     print attr, "exists but no function exists"
 
+    def get_attendee_participant_status(self, attendee):
+        return attendee.get_participant_status()
+
     def get_attendees(self):
-        return self.event.attendees()
+        return self._attendees
 
     def get_created(self):
         _datetime = self.event.created()
@@ -199,6 +215,7 @@ class Event(object):
             contact = attendee.contact()
             rsvp = attendee.rsvp()
             role = attendee.role()
+            partstat = attendee.partStat()
 
             if rsvp:
                 _rsvp = "TRUE"
@@ -211,6 +228,7 @@ class Event(object):
             #NonParticipant = _kolabformat.NonParticipant
 
             # TODO: Check the role strings for validity
+            # TODO^2: Use map
             if role == kolabformat.Required:
                 _role = "REQ-PARTICIPANT"
             elif role == kolabformat.Chair:
@@ -219,8 +237,22 @@ class Event(object):
                 _role = "OPTIONAL"
             elif role == kolabformat.NonParticipant:
                 _role = "NON-PARTICIPANT"
+            else:
+                _role = "OPTIONAL"
+
+            if partstat == kolabformat.PartNeedsAction:
+                _partstat = "NEEDS-ACTION"
+            elif partstat == kolabformat.PartAccepted:
+                _partstat = "ACCEPTED"
+            elif partstat == kolabformat.PartDeclined:
+                _partstat = "DECLINED"
+            elif partstat == kolabformat.PartTentative:
+                _partstat = "TENTATIVE"
+            elif partstat == kolabformat.PartDelegated:
+                _partstat = "DELEGATED"
 
             _attendee = "RSVP=%s" % _rsvp
+            _attendee += ";PARTSTAT=%s" % _partstat
             _attendee += ";ROLE=%s" % _role
             _attendee += ";MAILTO:%s" % contact.email()
 
@@ -249,6 +281,7 @@ class Event(object):
     def get_ical_organizer(self):
         organizer = self.get_organizer()
         name = organizer.name()
+
         if not name:
             return "mailto:%s" % (organizer.email())
         else:
@@ -257,32 +290,14 @@ class Event(object):
     def get_ical_status(self):
         status = self.event.status()
 
-        # TODO: See which ones are actually valid for iTip
-        if status == kolabformat.StatusUndefined:
-            _status = "UNDEFINED"
-        elif status == kolabformat.StatusNeedsAction:
-            _status = "NEEDS-ACTION"
-        elif status == kolabformat.StatusCompleted:
-            _status = "COMPLETED"
-        elif status == kolabformat.StatusInProcess:
-            _status = "INPROCESS"
-        elif status == kolabformat.StatusCancelled:
-            _status = "CANCELLED"
-        elif status == kolabformat.StatusTentative:
-            _status = "TENTATIVE"
-        elif status == kolabformat.StatusConfirmed:
-            _status = "CONFIRMED"
-        elif status == kolabformat.StatusDraft:
-            _status = "DRAFT"
-        elif status == kolabformat.StatusFinal:
-            _status = "FINAL"
-        else:
-            _status = "UNDEFINED"
-
-        return _status
+        for key in self.status_map.keys():
+            if status == self.status_map[key]:
+                return key
 
     def get_organizer(self):
-        return self.event.organizer()
+        organizer = self.event.organizer()
+        #print organizer
+        return organizer
 
     def get_priority(self):
         return self.event.priority()
@@ -308,6 +323,12 @@ class Event(object):
 
         return datetime.datetime(year, month, day, hour, minute, second)
 
+    def get_status(self):
+        status = self.event.status()
+        for key in self.status_map.keys():
+            if self.status_map[key] == status:
+                return key
+
     def get_summary(self):
         return self.event.summary()
 
@@ -319,6 +340,10 @@ class Event(object):
             self.__str__()
             return kolabformat.getSerializedUID()
 
+    def set_attendee_participant_status(self, attendee, status):
+        attendee.set_participant_status(status)
+        self.event.setAttendees(self._attendees)
+
     def set_created(self, _datetime=None):
         if _datetime == None:
             _datetime = datetime.datetime.now()
@@ -343,7 +368,7 @@ class Event(object):
                 kolabformat.cDateTime(year, month, day, hour, minute, second)
             )
 
-    def set_end(self, _datetime):
+    def set_dtstamp(self, _datetime):
         (
                 year,
                 month,
@@ -360,23 +385,73 @@ class Event(object):
                     _datetime.second
                 )
 
+        self.event.setLastModified(
+                kolabformat.cDateTime(year, month, day, hour, minute, second)
+            )
+
+    def set_end(self, _datetime):
+        (
+                year,
+                month,
+                day,
+            ) = (
+                    _datetime.year,
+                    _datetime.month,
+                    _datetime.day,
+                )
+        if hasattr(_datetime, 'hour'):
+            (
+                    hour,
+                    minute,
+                    second
+                ) = (
+                        _datetime.hour,
+                        _datetime.minute,
+                        _datetime.second
+                    )
+        else:
+            (hour, minute, second) = (0,0,0)
+
         self.event.setEnd(
                 kolabformat.cDateTime(year, month, day, hour, minute, second)
             )
 
     def set_ical_attendee(self, _attendee):
+        log.debug(_("set attendees from ical: %r") % (_attendee), level=9)
+
         if isinstance(_attendee, list):
             for attendee in _attendee:
+                #print attendee
                 rsvp = False
                 role = None
                 cn = None
+                partstat = None
                 address = None
                 for param in attendee.split(';'):
                     if (len(param.split('=')) > 1):
                         exec("%s = %r" % (param.split('=')[0].lower(), param.split('=')[1]))
-                    if (len(param.split(':')) > 1):
-                        address = param.split(':')[1]
-                self.add_attendee(address, name=cn, rsvp=rsvp, role=role)
+                        #print "%s = %r" % (param.split('=')[0].lower(), param.split('=')[1])
+                if (len(attendee.split(':')) > 1):
+                    address = attendee.split(':')[-1]
+                    #print address
+
+                self.add_attendee(address, name=cn, rsvp=rsvp, role=role, participant_status=partstat)
+        else:
+            #print attendee
+            rsvp = False
+            role = None
+            cn = None
+            partstat = None
+            address = None
+            for param in _attendee.split(';'):
+                if (len(param.split('=')) > 1):
+                    exec("%s = %r" % (param.split('=')[0].lower(), param.split('=')[1]))
+                    #print "%s = %r" % (param.split('=')[0].lower(), param.split('=')[1])
+            if (len(_attendee.split(':')) > 1):
+                address = _attendee.split(':')[-1]
+                #print address
+
+            self.add_attendee(address, name=cn, rsvp=rsvp, role=role, participant_status=partstat)
 
     def set_ical_dtend(self, dtend):
         self.set_end(dtend)
@@ -388,7 +463,18 @@ class Event(object):
         self.set_start(dtstart)
 
     def set_ical_organizer(self, organizer):
-        self.set_organizer(organizer)
+        cn = None
+        address = None
+        #print organizer
+        for param in organizer.split(':'):
+            if (len(param.split('=')) > 1):
+                exec("%s = %r" % (param.split('=')[0].lower(), param.split('=')[1]))
+                #print "%s = %r" % (param.split('=')[0].lower(), param.split('=')[1])
+        if (len(organizer.split(':')) > 1):
+            address = organizer.split(':')[-1]
+            #print address
+
+        self.set_organizer(address, name=cn)
 
     def set_ical_priority(self, priority):
         self.set_priority(priority)
@@ -439,22 +525,33 @@ class Event(object):
                 year,
                 month,
                 day,
-                hour,
-                minute,
-                second
             ) = (
                     _datetime.year,
                     _datetime.month,
                     _datetime.day,
-                    _datetime.hour,
-                    _datetime.minute,
-                    _datetime.second
                 )
+        if hasattr(_datetime, 'hour'):
+            (
+                    hour,
+                    minute,
+                    second
+                ) = (
+                        _datetime.hour,
+                        _datetime.minute,
+                        _datetime.second
+                    )
+        else:
+            (hour, minute, second) = (0,0,0)
 
         self.event.setStart(kolabformat.cDateTime(year, month, day, hour, minute, second))
 
     def set_status(self, status):
-        self.event.setStatus(status)
+        if status in self.status_map.keys():
+            self.event.setStatus(self.status_map[status])
+        elif status in self.status_map.values():
+            self.event.setStatus(status)
+        else:
+            raise InvalidEventStatusError, _("Invalid status set: %r") % (status)
 
     def set_summary(self, summary):
         self.event.setSummary(summary)
@@ -465,6 +562,142 @@ class Event(object):
     def __str__(self):
         return kolabformat.writeEvent(self.event)
 
+    def to_message(self):
+        from email.MIMEMultipart import MIMEMultipart
+        from email.MIMEBase import MIMEBase
+        from email.MIMEText import MIMEText
+        from email.Utils import COMMASPACE, formatdate
+        from email import Encoders
+
+        msg = MIMEMultipart()
+        organizer = self.get_organizer()
+        email = organizer.email()
+        name = organizer.name()
+
+        if not name:
+            msg['From'] = email
+        else:
+            msg['From'] = '"%s" <%s>' % (name, email)
+
+        msg['To'] = ', '.join([x.__str__() for x in self.get_attendees()])
+        msg['Date'] = formatdate(localtime=True)
+
+        msg.add_header('X-Kolab-Type', 'application/x-vnd.kolab.event')
+
+        text = utils.multiline_message("""
+                    This is a Kolab Groupware object. To view this object you
+                    will need an email client that understands the Kolab
+                    Groupware format. For a list of such email clients please
+                    visit http://www.kolab.org/
+            """)
+
+        msg.attach( MIMEText(text) )
+
+        part = MIMEBase('application', "calendar+xml")
+        part.set_charset('UTF-8')
+
+        msg["Subject"] = self.get_uid()
+
+        part.set_payload(str(self))
+
+        part.add_header('Content-Disposition', 'attachment; filename="kolab.xml"')
+        part.replace_header('Content-Transfer-Encoding', '8bit')
+
+        msg.attach(part)
+
+        return msg
+
+    def to_message_itip(self, from_address, method="REQUEST", participant_status="ACCEPTED"):
+        from email.MIMEMultipart import MIMEMultipart
+        from email.MIMEBase import MIMEBase
+        from email.MIMEText import MIMEText
+        from email.Utils import COMMASPACE, formatdate
+        from email import Encoders
+
+        msg = MIMEMultipart()
+
+        msg_from = None
+
+        log.debug(_("MESSAGE ITIP method %r") % (method), level=9)
+
+        if method == "REPLY":
+            msg['To'] = self.get_organizer().email()
+
+            log.debug(_("IN ITIP MESSAGE REPLY: %r") % (msg['To']), level=9)
+
+            attendees = self.get_attendees()
+
+            for attendee in attendees:
+                if attendee.get_email() == from_address:
+                    # Only the attendee is supposed to be listed in a reply
+                    attendee.set_participant_status(participant_status)
+
+                    self._attendees = [attendee]
+                    self.event.setAttendees(self._attendees)
+
+                    name = attendee.get_name()
+                    email = attendee.get_email()
+
+                    if not name:
+                        msg_from = email
+                    else:
+                        msg_from = '"%s" <%s>' % (name, email)
+
+            if msg_from == None:
+                organizer = self.get_organizer()
+                email = organizer.get_email()
+                name = organizer.get_name()
+                if email == from_address:
+                    if not name:
+                        msg_from = email
+                    else:
+                        msg_from = '"%s" <%s>' % (name, email)
+
+        elif method == "REQUEST":
+            organizer = self.get_organizer()
+            email = organizer.get_email()
+            name = organizer.get_name()
+            if not name:
+                msg_from = email
+            else:
+                msg_from = '"%s" <%s>' % (name, email)
+
+
+        log.debug(_("Message sender: %r") % (msg_from), level=9)
+
+        if msg_from == None:
+            log.error(_("No sender specified"))
+
+        msg['From'] = msg_from
+
+        msg['Date'] = formatdate(localtime=True)
+
+        text = utils.multiline_message("""
+                    This is a response to one of your event requests.
+            """)
+
+        msg.attach( MIMEText(text) )
+
+        part = MIMEBase('text', "calendar")
+        part.set_charset('UTF-8')
+
+        msg["Subject"] = "Response to invitation"
+
+        part.set_payload(self.as_string_itip(method=method))
+
+        part.add_header('Content-Disposition', 'attachment; filename="event.ics"')
+        part.replace_header('Content-Transfer-Encoding', '8bit')
+
+        msg.attach(part)
+
+        print msg.as_string()
+
+        return msg
+
 class EventIntegrityError(Exception):
     def __init__(self, message):
         Exception.__init__(self, message)
+
+class InvalidEventStatusError(Exception):
+    def __init__(self, message):
+        Exception.__init__(self, message)


commit 17cab1bb4e5271ff7330c229f65161b4cca29b89
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue May 22 03:44:55 2012 +0100

    Add additional helper functions to contact references

diff --git a/pykolab/xml/contact_reference.py b/pykolab/xml/contact_reference.py
index ff41480..e3fbcb2 100644
--- a/pykolab/xml/contact_reference.py
+++ b/pykolab/xml/contact_reference.py
@@ -17,8 +17,14 @@ class ContactReference(kolabformat.ContactReference):
         else:
             kolabformat.ContactReference.__init__(self, email)
 
+    def get_email(self):
+        return self.email()
+
+    def get_name(self):
+        return self.name()
+
     def set_email(self, email):
-        self.email = email
+        kolabformat.ContactReference.__init__(self, email)
 
     def set_name(self, name):
         self.setName(name)


commit 45310fe30d302d30359c119d2a783e3223d2fb54
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue May 22 03:44:25 2012 +0100

    Update participant_status_map
    Add additional helper functions

diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
index 692524d..7beb123 100644
--- a/pykolab/xml/attendee.py
+++ b/pykolab/xml/attendee.py
@@ -3,7 +3,7 @@ import kolabformat
 from contact_reference import ContactReference
 
 class Attendee(kolabformat.Attendee):
-    partstat_map = {
+    participant_status_map = {
             "NEEDS-ACTION": kolabformat.PartNeedsAction,
             "ACCEPTED": kolabformat.PartAccepted,
             "DECLINED": kolabformat.PartDeclined,
@@ -48,8 +48,18 @@ class Attendee(kolabformat.Attendee):
         if not participant_status == None:
             self.set_participant_status(participant_status)
 
+    def get_email(self):
+        return self.email
+
+    def get_name(self):
+        return self.contact().name()
+
+    def get_participant_status(self):
+        return self.partStat()
+
     def set_participant_status(self, participant_status):
         if self.participant_status_map.has_key(participant_status):
+            #print "Setting participant status for %r to %r" %(str(self), participant_status)
             self.setPartStat(self.participant_status_map[participant_status])
 
     def set_role(self, role):


commit 33e3eb39fee78bc917ade346530ab9859603ee85
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Mon May 21 14:46:49 2012 +0100

    Be a little more robust about the shared folder strategy.
    
    The kolabTargetFolder attribute is not yet being value-checked, and even
    though the objectClass kolabSharedFolder is required, the value for the
    attribute may or may not have been prefixed with 'shared/' or 'shared.'

diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py
index f544beb..9735312 100644
--- a/pykolab/imap/__init__.py
+++ b/pykolab/imap/__init__.py
@@ -218,17 +218,34 @@ class IMAP(object):
         """
 
         folder_name = "shared%s%s" % (self.imap.separator, folder_path)
-        log.info(_("Creating new shared folder %s") %(folder_path))
+
+        # Correct folder_path being supplied with "shared/shared/" for example
+        if folder_name.startswith("shared%s" % (self.imap.separator) * 2):
+            folder_name = folder_name[7:]
+
+        log.info(_("Creating new shared folder %s") %(folder_name))
         self.create_folder(folder_name, server)
 
     def shared_folder_exists(self, folder_path):
         """
             Check if a shared mailbox exists.
         """
-        return self.has_folder('shared%s%s' % (self.imap.separator, folder_path))
+        folder_name = 'shared%s%s' % (self.imap.separator, folder_path)
+
+        # Correct folder_path being supplied with "shared/shared/" for example
+        if folder_name.startswith("shared%s" % (self.imap.separator) * 2):
+            folder_name = folder_name[7:]
+
+        return self.has_folder(folder_name)
 
     def shared_folder_set_type(self, folder_path, folder_type):
-        self.imap._setannotation('shared%s%s' % (self.imap.separator, folder_path), '/vendor/kolab/folder-type', folder_type)
+        folder_name = 'shared%s%s' % (self.imap.separator, folder_path)
+
+        # Correct folder_path being supplied with "shared/shared/" for example
+        if folder_name.startswith("shared%s" % (self.imap.separator) * 2):
+            folder_name = folder_name[7:]
+
+        self.imap._setannotation(folder_name, '/vendor/kolab/folder-type', folder_type)
 
     def shared_mailbox_create(self, mailbox_base_name, server=None):
         """
@@ -236,6 +253,11 @@ class IMAP(object):
         """
 
         folder_name = "shared%s%s" % (self.imap.separator, mailbox_base_name)
+
+        # Correct folder_path being supplied with "shared/shared/" for example
+        if folder_name.startswith("shared%s" % (self.imap.separator) * 2):
+            folder_name = folder_name[7:]
+
         log.info(_("Creating new shared folder %s") %(mailbox_base_name))
         self.create_folder(folder_name, server)
 
@@ -243,7 +265,13 @@ class IMAP(object):
         """
             Check if a shared mailbox exists.
         """
-        return self.has_folder('shared%s%s' %(self.imap.separator, mailbox_base_name))
+        folder_name = "shared%s%s" % (self.imap.separator, mailbox_base_name)
+
+        # Correct folder_path being supplied with "shared/shared/" for example
+        if folder_name.startswith("shared%s" % (self.imap.separator) * 2):
+            folder_name = folder_name[7:]
+
+        return self.has_folder(folder_name)
 
     def user_mailbox_create(self, mailbox_base_name, server=None):
         """


commit 726fcdf9a002f706d048a8dd135be328d0849010
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Mon May 21 14:44:55 2012 +0100

    Add the new settings to the default kolab.conf

diff --git a/conf/kolab.conf b/conf/kolab.conf
index 03bfd06..0e9d606 100644
--- a/conf/kolab.conf
+++ b/conf/kolab.conf
@@ -77,6 +77,17 @@ group_filter = (|(objectclass=groupofuniquenames)(objectclass=groupofurls))
 group_scope = sub
 kolab_group_filter = (|(objectclass=kolabgroupofuniquenames)(objectclass=kolabgroupofurls))
 
+; Same again
+sharedfolder_base_dn = ou=Shared Folders,%(base_dn)s
+
+; Same again. Resources live in a different OU structure or;
+;
+; - They would appear in the address book(s) as distribution lists or individual contacts,
+; - Groups or individual users would appear to be Resources.
+;
+resource_base_dn = ou=Resources,%(base_dn)s
+resource_filter = (|%(group_filter)s(objectclass=kolabsharedfolder))
+
 ; The base DN, scope and filter to use when searching for additional domain
 ; name spaces in this environment.
 domain_base_dn = cn=kolab,cn=config





More information about the commits mailing list