11 commits - pykolab/auth pykolab/setup pykolab.spec.in pykolab/xml wallace/__init__.py wallace/module_resources.py wallace/wallace.sysconfig wallace/wallace.systemd

Jeroen van Meeuwen vanmeeuwen at kolabsys.com
Mon May 21 15:40:44 CEST 2012


 pykolab.spec.in                  |    6 
 pykolab/auth/__init__.py         |   19 +
 pykolab/auth/ldap/__init__.py    |   73 +++++-
 pykolab/setup/setup_imap.py      |    1 
 pykolab/xml/__init__.py          |   16 +
 pykolab/xml/attendee.py          |   60 ++++
 pykolab/xml/contact.py           |   43 +++
 pykolab/xml/contact_reference.py |   24 +
 pykolab/xml/event.py             |  470 +++++++++++++++++++++++++++++++++++++++
 wallace/__init__.py              |   95 +++++++
 wallace/module_resources.py      |  367 ++++++++++++++++++++++++++++++
 wallace/wallace.sysconfig        |    1 
 wallace/wallace.systemd          |    4 
 13 files changed, 1169 insertions(+), 10 deletions(-)

New commits:
commit 0387b1dc726f4b7e55f6fa01f42808a0135755c1
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Mon May 21 14:40:24 2012 +0100

    Initial, rough version of resource handling

diff --git a/wallace/module_resources.py b/wallace/module_resources.py
new file mode 100644
index 0000000..54cf85f
--- /dev/null
+++ b/wallace/module_resources.py
@@ -0,0 +1,367 @@
+# -*- coding: utf-8 -*-
+# Copyright 2010-2012 Kolab Systems AG (http://www.kolabsys.com)
+#
+# Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen a kolabsys.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 3 or, at your option, any later version
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Library General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+
+import icalendar
+import json
+import os
+import random
+import tempfile
+import time
+from urlparse import urlparse
+import urllib
+
+from email import message_from_file
+from email import message_from_string
+from email.utils import formataddr
+from email.utils import getaddresses
+
+import modules
+
+import pykolab
+
+from pykolab.auth import Auth
+from pykolab.imap import IMAP
+from pykolab.xml import event_from_ical
+from pykolab.xml import event_from_string
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.wallace')
+conf = pykolab.getConf()
+
+mybasepath = '/var/spool/pykolab/wallace/resources/'
+
+auth = None
+imap = None
+
+def __init__():
+    if not os.path.isdir(mybasepath):
+        os.makedirs(mybasepath)
+
+    modules.register('resources', execute, description=description())
+
+def description():
+    return """Resource management module."""
+
+def execute(*args, **kw):
+    auth = Auth()
+    auth.connect()
+
+    imap = IMAP()
+    imap.connect()
+
+    # TODO: Test for correct call.
+    filepath = args[0]
+
+    if kw.has_key('stage'):
+        log.debug(
+                _("Issuing callback after processing to stage %s") % (
+                        kw['stage']
+                    ),
+                level=8
+            )
+
+        log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8)
+        if hasattr(modules, 'cb_action_%s' % (kw['stage'])):
+            log.debug(
+                    _("Attempting to execute cb_action_%s()") % (kw['stage']),
+                    level=8
+                )
+
+            exec(
+                    'modules.cb_action_%s(%r, %r)' % (
+                            kw['stage'],
+                            'resources',
+                            filepath
+                        )
+                )
+
+            return
+
+    log.debug(_("Resource Management called for %r, %r") % (args, kw), level=8)
+
+    message = message_from_file(open(filepath, 'r'))
+
+    # An iTip message may contain multiple events. Later on, test if the message
+    # is an iTip message by checking the length of this list.
+    itip_events = itip_events_from_message(message)
+
+    if not len(itip_events) > 0:
+        log.info(
+                _("Message is not an iTip message or does not contain any " + \
+                    "iTip.")
+            )
+
+        exec('modules.cb_action_ACCEPT(%r, %r)' % ('resources',filepath))
+        return
+
+    else:
+        log.debug(
+                _("iTip events attached to this message contain the " + \
+                    "following information: %r") % (itip_events),
+                level=9
+            )
+
+    # 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))
+            return
+
+    # A simple list of merely resource entry IDs
+    resource_records = resource_records_from_itip_events(itip_events)
+
+    resources = {}
+    for resource_record in list(set(resource_records)):
+        # Get the attributes for the record
+        # See if it is a resource collection
+        #   If it is, expand to individual resources
+        #   If it is not, ...
+        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'):
+                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
+        else:
+            resources[resource_record] = resource_attrs
+
+    for resource in resources.keys():
+        if not resources[resource].has_key('kolabtargetfolder'):
+            continue
+
+        mailbox = resources[resource]['kolabtargetfolder']
+
+        resources[resource]['conflict'] = False
+        resources[resource]['conflicting_events'] = []
+
+        log.debug(_("Checking events in resource folder %r") % (mailbox), level=8)
+
+        try:
+            imap.imap.m.select(mailbox)
+        except:
+            log.error(_("Mailbox for resource %r doesn't exist") % (resource))
+            continue
+
+        typ, data = imap.imap.m.search(None, 'ALL')
+
+        for num in data[0].split():
+            # For efficiency, non-deterministic
+            if resources[resource]['conflict']:
+                continue
+
+            log.debug(_("Fetching message UID %r from folder %r") %(num, mailbox), level=9)
+            typ, data = imap.imap.m.fetch(num, '(RFC822)')
+
+            event_message = message_from_string(data[0][1])
+
+            if event_message.is_multipart():
+                for part in event_message.walk():
+                    if part.get_content_type() == "application/calendar+xml":
+                        payload = part.get_payload()
+                        event = pykolab.xml.event_from_string(payload)
+
+                        for itip in itip_events:
+                            log.debug(_("comparing %r to event %r (%r message UID %r)") % (itip, event.get_uid(), mailbox, num), level=9)
+                            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']:
+                                        conflict = False
+                                    else:
+                                        log.debug(_("Event %r ends later than invitation") % (event.get_uid()), level=9)
+                                        conflict = True
+                                else:
+                                    log.debug(_("Event %r starts before invitation ends") % (event.get_uid()), level=9)
+                                    conflict = True
+                            elif event.get_start() == itip['start']:
+                                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']:
+                                    log.debug(_("Event %r starts before invitation ends") % (event.get_uid()), level=9)
+                                    conflict = True
+                                else:
+                                    conflict = False
+
+                            if conflict:
+                                log.debug(_("Conflict with event %r") % (event.get_uid()), level=8)
+                                resources[resource]['conflicting_events'].append(event)
+                                resources[resource]['conflict'] = True
+
+        log.debug(_("Resource status information: %r") % (resources[resource]), level=8)
+
+def itip_events_from_message(message):
+    """
+        Obtain the iTip payload from email.message <message>
+    """
+
+    itip_events = []
+
+    # TODO: Are all iTip messages multipart? RFC 6047, section 2.4 states "A
+    # MIME body part containing content information that conforms to this
+    # document MUST have (...)" but does not state whether an iTip message must
+    # therefore also be multipart.
+    if message.is_multipart():
+        # Check each part
+        for part in message.walk():
+            # The iTip part MUST be Content-Type: text/calendar (RFC 6047,
+            # section 2.4)
+            if part.get_content_type() == "text/calendar":
+
+                if part.get_param('method') == "REQUEST":
+                    # Python iCalendar prior to 3.0 uses "from_string".
+                    itip_payload = part.get_payload()
+                    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
+                                    )
+                            )
+
+                        return
+
+                    for c in cal.walk():
+                        itip = {}
+                        if c.name == "VEVENT":
+                            # From the event, take the following properties:
+                            #
+                            # - start
+                            # - end (if any)
+                            # - duration (if any)
+                            # - organizer
+                            # - attendees (if any)
+                            # - resources (if any)
+                            # - TODO: recurrence rules (if any)
+                            #   Where are these stored actually?
+                            #
+                            itip['start'] = c.decoded('dtstart')
+                            if c.has_key('dtend'):
+                                itip['end'] = c.decoded('dtend')
+                            if c.has_key('duration'):
+                                itip['duration'] = c.decoded('duration')
+                            itip['organizer'] = c.decoded('organizer')
+                            itip['attendees'] = c.decoded('attendee')
+                            if c.has_key('resources'):
+                                itip['resources'] = c.decoded('resources')
+                            itip['raw'] = itip_payload
+                            itip['xml'] = event_from_ical(c.__str__())
+                            itip_events.append(itip)
+                else:
+                    log.error(
+                            _("Method %r not yet implemented") % (
+                                    part.get_param('method')
+                                )
+                        )
+
+    else:
+        log.error(_("Non-multipart iTip messages are not accepted"))
+
+    return itip_events
+
+def resource_records_from_itip_events(itip_events):
+    """
+        Given a list of itip_events, determine which resources have been
+        invited as attendees and/or resources.
+    """
+
+    auth = Auth()
+    auth.connect()
+
+    resource_records = []
+
+    attendees_raw = []
+    for list_attendees_raw in [x for x in [y['attendees'] for y in itip_events if y.has_key('attendees')]]:
+        attendees_raw.extend(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
+    # list later.
+    resources_raw = []
+    for list_resources_raw in [x for x in [y['resources'] for y in itip_events if y.has_key('resources')]]:
+        resources_raw.extend(list_resources_raw)
+
+    log.debug(_("Raw set of resources: %r") % (resources_raw), level=9)
+
+    # TODO: We expect the format of an attendee line to literally be:
+    #
+    #   ATTENDEE:RSVP=TRUE;ROLE=REQ-PARTICIPANT;MAILTO:lydia.bossers at kolabsys.com
+    #
+    # which makes the attendees_raw contain:
+    #
+    #   RSVP=TRUE;ROLE=REQ-PARTICIPANT;MAILTO:lydia.bossers at kolabsys.com
+    #
+    attendees = [x.split(':')[-1] for x in attendees_raw]
+
+    for attendee in attendees:
+        log.debug(_("Checking if attendee %r is a resource (collection)") % (attendee), level=8)
+        _resource_records = auth.find_resource(attendee)
+
+        if isinstance(_resource_records, list):
+            if len(_resource_records) == 0:
+                log.debug(_("No resource (collection) records found for %r") % (attendee), level=9)
+            else:
+                log.debug(_("Resource record(s): %r") % (_resource_records), level=8)
+                resource_records.extend(_resource_records)
+        elif isinstance(_resource_records, basestring):
+            resource_records.append(_resource_records)
+            log.debug(_("Resource record: %r") % (_resource_records), level=8)
+        else:
+            log.warning(_("Resource reservation made but no resource records found"))
+
+    # TODO: Escape the non-implementation of the free-form, undefined RESOURCES
+    # list(s) in iTip. We don't know how to handle this yet!
+    resources_raw = []
+
+    # TODO: We expect the format of an resource line to literally be:
+    #
+    #   RESOURCES:MAILTO:resource-car at kolabsys.com
+    #
+    # which makes the resources_raw contain:
+    #
+    #   MAILTO:resource-car at kolabsys.com
+    #
+    resources = [x.split(':')[-1] for x in resources_raw]
+    for resource in resources:
+        log.debug(_("Checking if resource %r is a resource (collection)") % (resource), level=8)
+        _resource_records = auth.find_resource(resource)
+        if isinstance(_resource_records, list):
+            if len(_resource_records) == 0:
+                log.debug(_("No resource (collection) records found for %r") % (resource), level=8)
+            else:
+                log.debug(_("Resource record(s): %r") % (_resource_records), level=8)
+                resource_records.extend(_resource_records)
+        elif isinstance(_resource_records, basestring):
+            resource_records.append(_resource_records)
+            log.debug(_("Resource record: %r") % (_resource_records), level=8)
+        else:
+            log.warning(_("Resource reservation made but no resource records found"))
+
+    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


commit 8b6068013328566e52540d4e9dcc91e80b3708f5
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Mon May 21 14:40:00 2012 +0100

    If possible, switch user/groups when starting

diff --git a/wallace/__init__.py b/wallace/__init__.py
index 6677613..edd47a6 100644
--- a/wallace/__init__.py
+++ b/wallace/__init__.py
@@ -18,7 +18,9 @@
 #
 
 import asyncore
+import grp
 import os
+import pwd
 from smtpd import SMTPChannel
 import sys
 import tempfile
@@ -53,6 +55,16 @@ class WallaceDaemon(object):
             )
 
         daemon_group.add_option(
+                "-g",
+                "--group",
+                dest    = "process_groupname",
+                action  = "store",
+                default = "kolab",
+                help    = _("Run as group GROUPNAME"),
+                metavar = "GROUPNAME"
+            )
+
+        daemon_group.add_option(
                 "-p", "--pid-file",
                 dest    = "pidfile",
                 action  = "store",
@@ -68,6 +80,16 @@ class WallaceDaemon(object):
                 help    = _("Port that Wallace is supposed to use.")
             )
 
+        daemon_group.add_option(
+                "-u",
+                "--user",
+                dest    = "process_username",
+                action  = "store",
+                default = "kolab",
+                help    = _("Run as user USERNAME"),
+                metavar = "USERNAME"
+            )
+
         conf.finalize_conf()
 
         import modules
@@ -178,7 +200,9 @@ class WallaceDaemon(object):
 
         wallace_modules = conf.get_list('wallace', 'modules')
         if wallace_modules == None:
-            wallace_modules = []
+            wallace_modules = ['resources']
+        elif not 'resources' in wallace_modules:
+            wallace_modules.append('resources')
 
         for module in wallace_modules:
             log.debug(_("Executing module %s") % (module), level=8)
@@ -192,6 +216,75 @@ class WallaceDaemon(object):
         exitcode = 0
 
         try:
+            (ruid, euid, suid) = os.getresuid()
+            (rgid, egid, sgid) = os.getresgid()
+
+            if ruid == 0:
+                # Means we can setreuid() / setregid() / setgroups()
+                if egid == 0:
+                    # Get group entry details
+                    try:
+                        (
+                                group_name,
+                                group_password,
+                                group_gid,
+                                group_members
+                            ) = grp.getgrnam(conf.process_groupname)
+
+                    except KeyError:
+                        print >> sys.stderr, _("Group %s does not exist") % (
+                                conf.process_groupname
+                            )
+
+                        sys.exit(1)
+
+                    # Set real and effective group if not the same as current.
+                    if not group_gid == egid:
+                        log.debug(
+                                _("Switching real and effective group id to %d") % (
+                                        group_gid
+                                    ),
+                                level=8
+                            )
+
+                        os.setregid(group_gid, group_gid)
+
+                if euid == 0:
+                    # Means we haven't switched yet.
+                    try:
+                        (
+                                user_name,
+                                user_password,
+                                user_uid,
+                                user_gid,
+                                user_gecos,
+                                user_homedir,
+                                user_shell
+                            ) = pwd.getpwnam(conf.process_username)
+
+                    except KeyError:
+                        print >> sys.stderr, _("User %s does not exist") % (
+                                conf.process_username
+                            )
+
+                        sys.exit(1)
+
+
+                    # Set real and effective user if not the same as current.
+                    if not user_uid == euid:
+                        log.debug(
+                                _("Switching real and effective user id to %d") % (
+                                        user_uid
+                                    ),
+                                level=8
+                            )
+
+                        os.setreuid(user_uid, user_uid)
+
+        except:
+            log.error(_("Could not change real and effective uid and/or gid"))
+
+        try:
             pid = 1
             if conf.fork_mode:
                 self.thread_count += 1


commit 0412e7428532533e3f57c01b6f5f8e2d853d7b0a
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Mon May 21 14:39:34 2012 +0100

    Start wallace as non-root

diff --git a/wallace/wallace.sysconfig b/wallace/wallace.sysconfig
index 7832bdb..2bc8856 100644
--- a/wallace/wallace.sysconfig
+++ b/wallace/wallace.sysconfig
@@ -3,3 +3,4 @@
 # See wallaced --help for more flags.
 #
 FLAGS="--fork -l warning"
+DAEMONOPTS="--user kolab"
\ No newline at end of file
diff --git a/wallace/wallace.systemd b/wallace/wallace.systemd
index 814f4c5..38a6604 100644
--- a/wallace/wallace.systemd
+++ b/wallace/wallace.systemd
@@ -5,6 +5,8 @@ After=syslog.target network.target
 [Service]
 Type=forking
 PIDFile=/var/run/wallaced/wallaced.pid
+User=kolab
+Group=kolab
 EnvironmentFile=/etc/sysconfig/wallace
 ExecStart=/usr/sbin/wallaced $FLAGS
 ExecReload=/bin/kill -HUP $MAINPID


commit 5b46eef3ef59edba8b936d8f4ced49917a18020d
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Mon May 21 14:39:05 2012 +0100

    Rough implementation of kolabformat wrapper

diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py
new file mode 100644
index 0000000..f73f5e9
--- /dev/null
+++ b/pykolab/xml/__init__.py
@@ -0,0 +1,16 @@
+from attendee import Attendee
+from contact import Contact
+from contact_reference import ContactReference
+
+from event import Event
+from event import event_from_ical
+from event import event_from_string
+
+__all__ = [
+        "Attendee",
+        "Contact",
+        "ContactReference",
+        "Event",
+        "event_from_ical",
+        "event_from_string",
+    ]
diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py
new file mode 100644
index 0000000..692524d
--- /dev/null
+++ b/pykolab/xml/attendee.py
@@ -0,0 +1,60 @@
+import kolabformat
+
+from contact_reference import ContactReference
+
+class Attendee(kolabformat.Attendee):
+    partstat_map = {
+            "NEEDS-ACTION": kolabformat.PartNeedsAction,
+            "ACCEPTED": kolabformat.PartAccepted,
+            "DECLINED": kolabformat.PartDeclined,
+            "TENTATIVE": kolabformat.PartTentative,
+            "DELEGATED": kolabformat.PartDelegated,
+            # Not yet implemented
+            #"COMPLETED": ,
+            #"IN-PROCESS": ,
+        }
+
+    role_map = {
+            "REQ-PARTICIPANT": kolabformat.Required,
+            "CHAIR": kolabformat.Chair,
+            "OPTIONAL": kolabformat.Optional,
+            "NONPARTICIPANT": kolabformat.NonParticipant,
+        }
+
+    rsvp_map = {
+            "TRUE": True,
+            "FALSE": False,
+        }
+
+    def __init__(self, email, name=None, rsvp=False, role=None, participant_status=None):
+        self.email = email
+
+        contactreference = ContactReference(email)
+
+        if not name == None:
+            contactreference.set_name(name)
+
+        kolabformat.Attendee.__init__(self, contactreference)
+
+        if isinstance(rsvp, bool):
+            self.setRSVP(rsvp)
+        else:
+            if self.rsvp_map.has_key(rsvp):
+                self.setRSVP(self.rsvp_map[rsvp])
+
+        if not role == None:
+            self.set_role(role)
+
+        if not participant_status == None:
+            self.set_participant_status(participant_status)
+
+    def set_participant_status(self, participant_status):
+        if self.participant_status_map.has_key(participant_status):
+            self.setPartStat(self.participant_status_map[participant_status])
+
+    def set_role(self, role):
+        if self.role_map.has_key(role):
+            self.setRole(self.role_map[role])
+
+    def __str__(self):
+        return self.email
diff --git a/pykolab/xml/contact.py b/pykolab/xml/contact.py
new file mode 100644
index 0000000..1577b58
--- /dev/null
+++ b/pykolab/xml/contact.py
@@ -0,0 +1,43 @@
+import kolabformat
+
+class Contact(kolabformat.Contact):
+    def __init__(self, *args, **kw):
+        kolabformat.Contact.__init__(self, *args, **kw)
+
+    def get_uid(self):
+        uid = self.uid()
+        if not uid == '':
+            return uid
+        else:
+            self.__str__()
+            return kolabformat.getSerializedUID()
+
+    def get_email(self, preferred=True):
+        if preferred:
+            return self.emailAddresses()[self.emailAddressPreferredIndex()]
+        else:
+            return [x for x in self.emailAddresses()]
+
+    def set_email(self, email, preferred_index=0):
+        if isinstance(email, basestring):
+            self.setEmailAddresses([email], preferred_index)
+        else:
+            self.setEmailAddresses(email, preferred_index)
+
+    def add_email(self, email):
+        if isinstance(email, basestring):
+            self.add_emails([email])
+        elif isinstance(email, list):
+            self.add_emails(email)
+
+    def add_emails(self, emails):
+        preferred_email = self.get_email()
+        emails = [x for x in set(self.get_email(preferred=False) + emails)]
+        preferred_email_index = emails.index(preferred_email)
+        self.setEmailAddresses(emails, preferred_email_index)
+
+    def set_name(self, name):
+        self.setName(name)
+
+    def __str__(self):
+        return kolabformat.writeContact(self)
diff --git a/pykolab/xml/contact_reference.py b/pykolab/xml/contact_reference.py
new file mode 100644
index 0000000..ff41480
--- /dev/null
+++ b/pykolab/xml/contact_reference.py
@@ -0,0 +1,24 @@
+import kolabformat
+
+"""
+    def __eq__(self, *args): return _kolabformat.ContactReference___eq__(self, *args)
+    def isValid(self): return _kolabformat.ContactReference_isValid(self)
+    def setName(self, *args): return _kolabformat.ContactReference_setName(self, *args)
+    def email(self): return _kolabformat.ContactReference_email(self)
+    def uid(self): return _kolabformat.ContactReference_uid(self)
+    def name(self): return _kolabformat.ContactReference_name(self)
+    def type(self): return _kolabformat.ContactReference_type(self)
+"""
+
+class ContactReference(kolabformat.ContactReference):
+    def __init__(self, email=None):
+        if email == None:
+            kolabformat.ContactReference.__init__(self)
+        else:
+            kolabformat.ContactReference.__init__(self, email)
+
+    def set_email(self, email):
+        self.email = email
+
+    def set_name(self, name):
+        self.setName(name)
diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py
new file mode 100644
index 0000000..a23b785
--- /dev/null
+++ b/pykolab/xml/event.py
@@ -0,0 +1,470 @@
+import datetime
+import icalendar
+import kolabformat
+import time
+
+from pykolab import constants
+
+from attendee import Attendee
+from contact_reference import ContactReference
+
+def event_from_ical(string):
+    return Event(from_ical=string)
+
+def event_from_string(string):
+    return Event(from_string=string)
+
+class Event(object):
+    StatusTentative = kolabformat.StatusTentative
+    def __init__(self, from_ical="", from_string=""):
+        self._attendees = []
+
+        if from_ical == "":
+            if from_string == "":
+                self.event = kolabformat.Event()
+            else:
+                self.event = kolabformat.readEvent(from_string, False)
+        else:
+            self.from_ical(from_ical)
+
+    def add_attendee(self, email, name=None, rsvp=False, role=None):
+        attendee = Attendee(email, name, rsvp, role)
+        self._attendees.append(attendee)
+        self.event.setAttendees(self._attendees)
+
+    def as_string_itip(self):
+        cal = icalendar.Calendar()
+        cal.add(
+                'prodid',
+                '-//pykolab-%s-%s//kolab.org//' % (
+                        constants.__version__,
+                        constants.__release__
+                    )
+            )
+
+        cal.add('version', '2.0')
+        # TODO: Really?
+        cal.add('calscale', 'GREGORIAN')
+        # TODO: Not always a request...
+        cal.add('method', 'REQUEST')
+
+        # TODO: Add timezone information using icalendar.?()
+        #       Not sure if there is a class for it.
+
+        event = icalendar.Event()
+
+        # Required
+        event['uid'] = self.get_uid()
+
+        # NOTE: Make sure to list(set()) or duplicates may arise
+        for attr in list(set(event.singletons)):
+            if hasattr(self, 'get_ical_%s' % (attr.lower())):
+                exec("retval = self.get_ical_%s()" % (attr.lower()))
+                if not retval == None and not retval == "":
+                    event.add(attr.lower(), retval)
+
+            elif hasattr(self, 'get_%s' % (attr.lower())):
+                exec("retval = self.get_%s()" % (attr.lower()))
+                if not retval == None and not retval == "":
+                    event.add(attr.lower(), retval)
+
+            #else:
+                #print "no function for", attr.lower()
+
+        # NOTE: Make sure to list(set()) or duplicates may arise
+        for attr in list(set(event.multiple)):
+            if hasattr(self, 'get_ical_%s' % (attr.lower())):
+                exec("retval = self.get_ical_%s()" % (attr.lower()))
+                if isinstance(retval, list) and not len(retval) == 0:
+                    for _retval in retval:
+                        event.add(attr.lower(), _retval)
+
+            elif hasattr(self, 'get_%s' % (attr.lower())):
+                exec("retval = self.get_%s()" % (attr.lower()))
+                if isinstance(retval, list) and not len(retval) == 0:
+                    for _retval in retval:
+                        event.add(attr.lower(), _retval)
+
+            #else:
+                #print "no function for", attr.lower()
+
+        #event.add('attendee', self.get_attendees())
+
+        #BEGIN:VEVENT
+        #DESCRIPTION:Project XYZ Review Meeting
+        #CATEGORIES:MEETING
+        #CLASS:PUBLIC
+        #CREATED:19980309T130000Z
+        #SUMMARY:XYZ Project Review
+        #DTSTART;TZID=US-Eastern:19980312T083000
+        #DTEND;TZID=US-Eastern:19980312T093000
+        #LOCATION:1CP Conference Room 4350
+        #END:VEVENT
+
+        #event['description'] =
+
+        cal.add_component(event)
+
+        if hasattr(cal, 'to_ical'):
+            return cal.to_ical()
+        elif hasattr(cal, 'as_string'):
+            return cal.as_string()
+
+    def from_ical(self, ical):
+        self.event = kolabformat.Event()
+        if hasattr(icalendar.Event, 'from_ical'):
+            ical_event = icalendar.Event.from_ical(ical)
+        elif hasattr(icalendar.Event, 'from_string'):
+            ical_event = icalendar.Event.from_string(ical)
+
+        for attr in list(set(ical_event.required)):
+            if ical_event.has_key(attr):
+                if hasattr(self, 'set_ical_%s' % (attr.lower())):
+                    exec("self.set_ical_%s(%r)" % (attr.lower(),ical_event.decoded(attr)))
+                else:
+                    print attr, "exists but no function exists"
+
+        # NOTE: Make sure to list(set()) or duplicates may arise
+        for attr in list(set(ical_event.singletons)):
+            if ical_event.has_key(attr):
+                if hasattr(self, 'set_ical_%s' % (attr.lower())):
+                    exec("self.set_ical_%s(%r)" % (attr.lower(),ical_event.decoded(attr)))
+                else:
+                    print attr, "exists but no function exists"
+
+        # NOTE: Make sure to list(set()) or duplicates may arise
+        for attr in list(set(ical_event.multiple)):
+            if ical_event.has_key(attr):
+                if hasattr(self, 'set_ical_%s' % (attr.lower())):
+                    exec("self.set_ical_%s(%r)" % (attr.lower(),ical_event.decoded(attr)))
+                else:
+                    print attr, "exists but no function exists"
+
+    def get_attendees(self):
+        return self.event.attendees()
+
+    def get_created(self):
+        _datetime = self.event.created()
+
+        (
+                year,
+                month,
+                day,
+                hour,
+                minute,
+                second
+            ) = (
+                    _datetime.year(),
+                    _datetime.month(),
+                    _datetime.day(),
+                    _datetime.hour(),
+                    _datetime.minute(),
+                    _datetime.second()
+                )
+
+        try:
+            result = datetime.datetime(year, month, day, hour, minute, second)
+        except ValueError:
+            result = datetime.datetime.now()
+
+    def get_end(self):
+        _datetime = self.event.end()
+
+        (
+                year,
+                month,
+                day,
+                hour,
+                minute,
+                second
+            ) = (
+                    _datetime.year(),
+                    _datetime.month(),
+                    _datetime.day(),
+                    _datetime.hour(),
+                    _datetime.minute(),
+                    _datetime.second()
+                )
+
+        return datetime.datetime(year, month, day, hour, minute, second)
+
+    def get_ical_attendee(self):
+        # TODO: Formatting, aye? See also the example snippet:
+        #
+        # ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;CUTYPE=GROUP:
+        # MAILTO:employee-A at host.com
+
+        attendees = []
+        for attendee in self.get_attendees():
+            contact = attendee.contact()
+            rsvp = attendee.rsvp()
+            role = attendee.role()
+
+            if rsvp:
+                _rsvp = "TRUE"
+            else:
+                _rsvp = "FALSE"
+
+            #Required = _kolabformat.Required
+            #Chair = _kolabformat.Chair
+            #Optional = _kolabformat.Optional
+            #NonParticipant = _kolabformat.NonParticipant
+
+            # TODO: Check the role strings for validity
+            if role == kolabformat.Required:
+                _role = "REQ-PARTICIPANT"
+            elif role == kolabformat.Chair:
+                _role = "CHAIR"
+            elif role == kolabformat.Optional:
+                _role = "OPTIONAL"
+            elif role == kolabformat.NonParticipant:
+                _role = "NON-PARTICIPANT"
+
+            _attendee = "RSVP=%s" % _rsvp
+            _attendee += ";ROLE=%s" % _role
+            _attendee += ";MAILTO:%s" % contact.email()
+
+            attendees.append(_attendee)
+
+        return attendees
+
+    def get_ical_created(self):
+        return self.get_created()
+
+    def get_ical_dtend(self):
+        return self.get_end()
+
+    def get_ical_dtstamp(self):
+        return
+        try:
+            retval = self.event.lastModified()
+            if retval == None or retval == "":
+                return datetime.datetime.now()
+        except:
+            return datetime.datetime.now()
+
+    def get_ical_dtstart(self):
+        return self.get_start()
+
+    def get_ical_organizer(self):
+        organizer = self.get_organizer()
+        name = organizer.name()
+        if not name:
+            return "mailto:%s" % (organizer.email())
+        else:
+            return "CN=%s:mailto:%s" %(name, organizer.email())
+
+    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
+
+    def get_organizer(self):
+        return self.event.organizer()
+
+    def get_priority(self):
+        return self.event.priority()
+
+    def get_start(self):
+        _datetime = self.event.start()
+
+        (
+                year,
+                month,
+                day,
+                hour,
+                minute,
+                second
+            ) = (
+                    _datetime.year(),
+                    _datetime.month(),
+                    _datetime.day(),
+                    _datetime.hour(),
+                    _datetime.minute(),
+                    _datetime.second()
+                )
+
+        return datetime.datetime(year, month, day, hour, minute, second)
+
+    def get_summary(self):
+        return self.event.summary()
+
+    def get_uid(self):
+        uid = self.event.uid()
+        if not uid == '':
+            return uid
+        else:
+            self.__str__()
+            return kolabformat.getSerializedUID()
+
+    def set_created(self, _datetime=None):
+        if _datetime == None:
+            _datetime = datetime.datetime.now()
+
+        (
+                year,
+                month,
+                day,
+                hour,
+                minute,
+                second
+            ) = (
+                    _datetime.year,
+                    _datetime.month,
+                    _datetime.day,
+                    _datetime.hour,
+                    _datetime.minute,
+                    _datetime.second
+                )
+
+        self.event.setCreated(
+                kolabformat.cDateTime(year, month, day, hour, minute, second)
+            )
+
+    def set_end(self, _datetime):
+        (
+                year,
+                month,
+                day,
+                hour,
+                minute,
+                second
+            ) = (
+                    _datetime.year,
+                    _datetime.month,
+                    _datetime.day,
+                    _datetime.hour,
+                    _datetime.minute,
+                    _datetime.second
+                )
+
+        self.event.setEnd(
+                kolabformat.cDateTime(year, month, day, hour, minute, second)
+            )
+
+    def set_ical_attendee(self, _attendee):
+        if isinstance(_attendee, list):
+            for attendee in _attendee:
+                rsvp = False
+                role = None
+                cn = 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)
+
+    def set_ical_dtend(self, dtend):
+        self.set_end(dtend)
+
+    def set_ical_dtstamp(self, dtstamp):
+        self.set_dtstamp(dtstamp)
+
+    def set_ical_dtstart(self, dtstart):
+        self.set_start(dtstart)
+
+    def set_ical_organizer(self, organizer):
+        self.set_organizer(organizer)
+
+    def set_ical_priority(self, priority):
+        self.set_priority(priority)
+
+    def set_ical_status(self, status):
+        # TODO: See which ones are actually valid for iTip
+        if status == "UNDEFINED":
+            _status = kolabformat.StatusUndefined
+        elif status == "NEEDS-ACTION":
+            _status = kolabformat.StatusNeedsAction
+        elif status == "COMPLETED":
+            _status = kolabformat.StatusCompleted
+        elif status == "INPROCESS":
+            _status = kolabformat.StatusInProcess
+        elif status == "CANCELLED":
+            _status = kolabformat.StatusCancelled
+        elif status == "TENTATIVE":
+            _status = kolabformat.StatusTentative
+        elif status == "CONFIRMED":
+            _status = kolabformat.StatusConfirmed
+        elif status == "DRAFT":
+            _status = kolabformat.StatusDraft
+        elif status == "FINAL":
+            _status = kolabformat.StatusFinal
+        else:
+            _status = kolabformat.StatusUndefined
+
+        self.event.setStatus(_status)
+
+    def set_ical_summary(self, summary):
+        self.set_summary(str(summary))
+
+    def set_ical_uid(self, uid):
+        self.set_uid(str(uid))
+
+    def set_organizer(self, email, name=None):
+        contactreference = ContactReference(email)
+        if not name == None:
+            contactreference.set_name(name)
+
+        self.event.setOrganizer(contactreference)
+
+    def set_priority(self, priority):
+        self.event.setPriority(priority)
+
+    def set_start(self, _datetime):
+        (
+                year,
+                month,
+                day,
+                hour,
+                minute,
+                second
+            ) = (
+                    _datetime.year,
+                    _datetime.month,
+                    _datetime.day,
+                    _datetime.hour,
+                    _datetime.minute,
+                    _datetime.second
+                )
+
+        self.event.setStart(kolabformat.cDateTime(year, month, day, hour, minute, second))
+
+    def set_status(self, status):
+        self.event.setStatus(status)
+
+    def set_summary(self, summary):
+        self.event.setSummary(summary)
+
+    def set_uid(self, uid):
+        self.event.setUid(str(uid))
+
+    def __str__(self):
+        return kolabformat.writeEvent(self.event)
+
+class EventIntegrityError(Exception):
+    def __init__(self, message):
+        Exception.__init__(self, message)


commit 90379b582f5ae828af3ee915c7bcf89bb3af6545
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Mon May 21 14:38:45 2012 +0100

    Add function find_resource()

diff --git a/pykolab/auth/__init__.py b/pykolab/auth/__init__.py
index 3441394..e05ea80 100644
--- a/pykolab/auth/__init__.py
+++ b/pykolab/auth/__init__.py
@@ -191,11 +191,19 @@ class Auth(pykolab.base.Base):
         else:
             return result
 
-    def find_user(self, attr, value, **kw):
-        return self._auth._find_user(attr, value, domain=domain, **kw)
+    def find_resource(self, address):
+        """
+            Find one or more resources corresponding to the recipient address.
+        """
+        result = self._auth.find_resource(address)
 
-    def search_users(self, attr, value, **kw):
-        return self._auth._search_users(attr, value, domain=domain, **kw)
+        if isinstance(result, list) and len(result) == 1:
+            return result[0]
+        else:
+            return result
+
+    def find_user(self, attr, value, **kw):
+        return self._auth._find_user(attr, value, **kw)
 
     def list_domains(self):
         """
@@ -248,6 +256,9 @@ class Auth(pykolab.base.Base):
     def search_mail_address(self, domain, mail_address):
         return self._auth._search_mail_address(domain, mail_address)
 
+    def search_users(self, attr, value, **kw):
+        return self._auth._search_users(attr, value, **kw)
+
     def set_entry_attribute(self, domain, entry, attribute):
         return self._auth.set_entry_attribute(entry, attribute)
 


commit 18e7a55b4c030b23d502d73b7bffa91be3b80f2a
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Mon May 21 14:38:01 2012 +0100

    Add find_resource()
    Bind before actually getting entry attributes

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index 41a5c0d..c075742 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -277,6 +277,8 @@ class LDAP(pykolab.base.Base):
             Get multiple attributes for an entry.
         """
 
+        self._bind()
+
         #print entry_id
         entry_dn = self.entry_dn(entry_id)
         #print entry_dn
@@ -367,6 +369,74 @@ class LDAP(pykolab.base.Base):
 
         return _entry_dns
 
+    def find_resource(self, address="*", exclude_entry_id=None):
+        """
+            Given an address string or list of addresses, find one or more valid
+            resources.
+
+            Specify an additional entry_id to exclude to exclude matches.
+        """
+
+        self._bind()
+
+        if not exclude_entry_id == None:
+            __filter_prefix = "(&"
+            __filter_suffix = "(!(%s=%s)))" % (
+                    self.config_get('unique_attribute'),
+                    exclude_entry_id
+                )
+
+        else:
+            __filter_prefix = ""
+            __filter_suffix = ""
+
+        resource_filter = self.config_get('resource_filter')
+        if not resource_filter == None:
+            __filter_prefix = "(&%s" % resource_filter
+            __filter_suffix = ")"
+
+        resource_base_dn = self.config_get('resource_base_dn')
+
+        recipient_address_attrs = self.config_get_list("mail_attributes")
+
+        result_attributes = recipient_address_attrs
+        result_attributes.append(self.config_get('unique_attribute'))
+
+        _filter = "(|"
+
+        for recipient_address_attr in recipient_address_attrs:
+            if isinstance(address, basestring):
+                _filter += "(%s=%s)" % (recipient_address_attr, address)
+            else:
+                for _address in address:
+                    _filter += "(%s=%s)" % (recipient_address_attr, _address)
+
+        _filter += ")"
+
+        _filter = "%s%s%s" % (__filter_prefix,_filter,__filter_suffix)
+
+
+        log.debug(_("Finding resource with filter %r") % (_filter), level=8)
+
+        if len(_filter) <= 6:
+            return None
+
+        _results = self.ldap.search_s(
+                resource_base_dn,
+                scope=ldap.SCOPE_SUBTREE,
+                filterstr=_filter,
+                attrlist=result_attributes,
+                attrsonly=True
+            )
+
+        _entry_dns = []
+
+        for _result in _results:
+            (_entry_id, _entry_attrs) = _result
+            _entry_dns.append(_entry_id)
+
+        return _entry_dns
+
     def get_latest_sync_timestamp(self):
         return cache.last_modify_timestamp(self.domain)
 


commit b9ca026e2c9d98277be635f2264f48747190194e
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri May 18 12:48:30 2012 +0100

    Correct the permissions on /var/lib/kolab and /var/log/kolab

diff --git a/pykolab.spec.in b/pykolab.spec.in
index 79cf6c1..75adfc0 100644
--- a/pykolab.spec.in
+++ b/pykolab.spec.in
@@ -267,8 +267,8 @@ rm -rf %{buildroot}
 %{python_sitelib}/pykolab/plugins/recipientpolicy
 %{python_sitelib}/kolab/
 %{python_sitelib}/cyruslib.py*
-%dir %{_localstatedir}/lib/kolab/
-%dir %{_localstatedir}/log/kolab/
+%attr(0775,kolab,kolab-n) %dir %{_localstatedir}/lib/kolab/
+%attr(0775,kolab,kolab-n) %dir %{_localstatedir}/log/kolab/
 
 %files telemetry
 %defattr(-,root,root,-)


commit 32c1c0720ec1ef3b0d8e84e2dfc39e7e59e01bdf
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri May 18 12:43:54 2012 +0100

    Prevent attempting to modify an entry with an empty modlist (#786)

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index a4b60be..41a5c0d 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -694,7 +694,8 @@ class LDAP(pykolab.base.Base):
                     modlist.append((ldap.MOD_REPLACE, attribute, attrs[attribute]))
 
         dn = entry_dn
-        self.ldap.modify_s(dn, modlist)
+        if len(modlist) > 0:
+            self.ldap.modify_s(dn, modlist)
 
     def synchronize(self):
         """


commit 04076f9edd1edccf01cbc956ee880ff935effd4c
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri May 18 10:50:53 2012 +0100

    Correct command name

diff --git a/wallace/wallace.systemd b/wallace/wallace.systemd
index 73c16d7..814f4c5 100644
--- a/wallace/wallace.systemd
+++ b/wallace/wallace.systemd
@@ -6,7 +6,7 @@ After=syslog.target network.target
 Type=forking
 PIDFile=/var/run/wallaced/wallaced.pid
 EnvironmentFile=/etc/sysconfig/wallace
-ExecStart=/usr/sbin/wallace $FLAGS
+ExecStart=/usr/sbin/wallaced $FLAGS
 ExecReload=/bin/kill -HUP $MAINPID
 ExecStop=/bin/kill -TERM $MAINPID
 


commit bccd7c16fbc4ae1eb4aaa1dff4a4a8adbfebba41
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri May 18 10:50:06 2012 +0100

    Set shared to be the postuser

diff --git a/pykolab/setup/setup_imap.py b/pykolab/setup/setup_imap.py
index de927f3..750e43d 100644
--- a/pykolab/setup/setup_imap.py
+++ b/pykolab/setup/setup_imap.py
@@ -62,6 +62,7 @@ def execute(*args, **kw):
             "ldap_member_method": "attribute",
             "ldap_member_attribute": "nsrole",
             "admins": conf.get('cyrus-imap', 'admin_login'),
+            "postuser": "shared",
         }
 
     template_file = None


commit 58eb1a94395e5113680b31b22e821201bffdc836
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri May 18 10:29:12 2012 +0100

    Set the permissions on /var/run/kolabd/ to the kolab user

diff --git a/pykolab.spec.in b/pykolab.spec.in
index 668cb36..79cf6c1 100644
--- a/pykolab.spec.in
+++ b/pykolab.spec.in
@@ -320,7 +320,7 @@ rm -rf %{buildroot}
 %config(noreplace) %{_sysconfdir}/sysconfig/kolabd
 %{_sbindir}/kolabd
 %{python_sitelib}/kolabd/
-%dir %{_localstatedir}/run/kolabd
+%attr(0770,kolab,kolab) %dir %{_localstatedir}/run/kolabd
 
 %files -n postfix-kolab
 %defattr(-,root,root,-)





More information about the commits mailing list