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