34 commits - configure.ac conf/kolab.conf kolabd/__init__.py kolabd/kolabd.systemd kolabd/Makefile.am kolabd/process.py Makefile.am pykolab/auth pykolab/base.py pykolab/conf pykolab/imap pykolab/__init__.py pykolab/Makefile.am pykolab/plugins pykolab/setup pykolab.spec.in pykolab/wap_client saslauthd/__init__.py saslauthd/kolab-saslauthd.systemd saslauthd/Makefile.am wallace/__init__.py wallace/Makefile.am wallace/wallace.sysconfig wallace/wallace.systemd wallace/wallace.sysvinit

Jeroen van Meeuwen vanmeeuwen at kolabsys.com
Fri Apr 27 16:24:18 CEST 2012


 Makefile.am                                 |    1 
 conf/kolab.conf                             |  327 ++-
 configure.ac                                |    4 
 kolabd/Makefile.am                          |    1 
 kolabd/__init__.py                          |   99 -
 kolabd/kolabd.systemd                       |   15 
 kolabd/process.py                           |   37 
 pykolab.spec.in                             |   97 -
 pykolab/Makefile.am                         |    3 
 pykolab/__init__.py                         |   12 
 pykolab/auth/__init__.py                    |  148 -
 pykolab/auth/ldap/__init__.py               | 2380 ++++++++++++++--------------
 pykolab/auth/ldap/cache.py                  |  161 +
 pykolab/base.py                             |   93 +
 pykolab/conf/__init__.py                    |   10 
 pykolab/conf/defaults.py                    |    6 
 pykolab/imap/__init__.py                    |  367 ++--
 pykolab/imap/cyrus.py                       |   29 
 pykolab/plugins/recipientpolicy/__init__.py |    4 
 pykolab/setup/__init__.py                   |    8 
 pykolab/setup/components.py                 |   14 
 pykolab/setup/setup_imap.py                 |  136 +
 pykolab/setup/setup_kolabd.py               |   42 
 pykolab/setup/setup_ldap.py                 |  139 +
 pykolab/setup/setup_mta.py                  |  225 ++
 pykolab/setup/setup_mysql.py                |   60 
 pykolab/setup/setup_roundcube.py            |   40 
 pykolab/wap_client/__init__.py              |   75 
 saslauthd/Makefile.am                       |    2 
 saslauthd/__init__.py                       |    1 
 saslauthd/kolab-saslauthd.systemd           |   15 
 wallace/Makefile.am                         |   13 
 wallace/__init__.py                         |   31 
 wallace/wallace.sysconfig                   |    5 
 wallace/wallace.systemd                     |   15 
 wallace/wallace.sysvinit                    |  108 +
 36 files changed, 3025 insertions(+), 1698 deletions(-)

New commits:
commit 5d2036cd7d3ecb6d44ed1a54f4835fb85a067696
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Apr 27 15:22:33 2012 +0100

    Use the configured administrator login
    Correct some of the options set in /etc/imapd.conf

diff --git a/pykolab/setup/setup_imap.py b/pykolab/setup/setup_imap.py
index 683b4a4..a7c4810 100644
--- a/pykolab/setup/setup_imap.py
+++ b/pykolab/setup/setup_imap.py
@@ -50,10 +50,10 @@ def execute(*args, **kw):
             "pts_module": "ldap",
             "ldap_servers": conf.get('ldap', 'ldap_uri'),
             "ldap_sasl": "0",
-            "ldap_base_dn": conf.get('ldap', 'base_dn'),
+            "ldap_base": conf.get('ldap', 'base_dn'),
             "ldap_bind_dn": conf.get('ldap', 'service_bind_dn'),
             "ldap_password": conf.get('ldap', 'service_bind_pw'),
-            "ldap_filter": '(|(&(|(uid=cyrus-admin)(uid=cyrus-murder))(uid=%U))(&(|(uid=%U)(mail=%U@%d)(mail=%U@%r))(objectclass=kolabinetorgperson)))',
+            "ldap_filter": '(|(&(|(uid=%s)(uid=cyrus-murder))(uid=%%U))(&(|(uid=%%U)(mail=%%U@%%d)(mail=%%U@%%r))(objectclass=kolabinetorgperson)))' % (conf.get('cyrus-imap', 'admin_login')),
             "ldap_user_attribute": conf.get('cyrus-sasl', 'result_attribute'),
             "ldap_group_base": conf.get('ldap', 'base_dn'),
             "ldap_group_filter": "(&(cn=%u)(objectclass=ldapsubentry)(objectclass=nsroledefinition))",
@@ -66,7 +66,7 @@ def execute(*args, **kw):
             "ldap_time_limit": "10",
             "unixhierarchysep": "1",
             "virt_domains": "userid",
-            "admins": "cyrus-admin",
+            "admins": conf.get('cyrus-imap', 'admin_login'),
             "annotation_definitions": "/etc/imapd.annotations.conf",
             "sieve_extensions": "fileinto reject vacation imapflags notify envelope include relational regex subaddress copy",
             "allowallsubscribe": "0",
@@ -79,13 +79,13 @@ def execute(*args, **kw):
             "sieve_allowreferrals": "0",
             "lmtp_downcase_rcpt": "1",
             "lmtp_fuzzy_mailbox_match": "1",
-            "username_to_lower": "1",
-            "normalizeuid": "1",
+            "username_tolower": "1",
+            #"normalizeuid": "1",
             "deletedprefix": "DELETED",
             "delete_mode": "delayed",
             "expunge_mode": "delayed",
             "flushseenstate": "1",
-            "virt_domains": "userid",
+            "virtdomains": "userid",
         }
 
     myaugeas = Augeas()


commit 1ab25276c9b0425ac638ab0935a8c5d30d699048
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Apr 27 15:22:16 2012 +0100

    Use the configured administrator login

diff --git a/pykolab/setup/setup_ldap.py b/pykolab/setup/setup_ldap.py
index 2fb93ca..ee5f5ea 100644
--- a/pykolab/setup/setup_ldap.py
+++ b/pykolab/setup/setup_ldap.py
@@ -217,12 +217,12 @@ ServerAdminPwd = %(admin_pass)s
     auth._auth.connect()
     auth._auth._bind()
 
-    dn = 'uid=cyrus-admin,ou=Special Users,%s' % (_input['rootdn'])
+    dn = 'uid=%s,ou=Special Users,%s' % (conf.get('cyrus-imap', 'admin_login'), _input['rootdn'])
 
     # A dict to help build the "body" of the object
     attrs = {}
     attrs['objectclass'] = ['top','person','inetorgperson','organizationalperson']
-    attrs['uid'] = "cyrus-admin"
+    attrs['uid'] = conf.get('cyrus-imap', 'admin_login')
     attrs['givenname'] = "Cyrus"
     attrs['surname'] = "Administrator"
     attrs['cn'] = "Cyrus Administrator"


commit f2d68e09b3b536159a4929b5e0a812a018cc477d
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Apr 27 15:21:40 2012 +0100

    Use self.m over imap.m

diff --git a/pykolab/imap/cyrus.py b/pykolab/imap/cyrus.py
index 1eae6d7..ac580c9 100644
--- a/pykolab/imap/cyrus.py
+++ b/pykolab/imap/cyrus.py
@@ -129,6 +129,7 @@ class Cyrus(cyruslib.CYRUS):
             return self.server
 
         log.debug(_("Checking actual backend server for folder %s through annotations") % (mailfolder), level=8)
+
         if self.mbox.has_key(mailfolder):
             return self.mbox[mailfolder]
 
@@ -153,11 +154,6 @@ class Cyrus(cyruslib.CYRUS):
         server = annotations[mailfolder]['/vendor/cmu/cyrus-imapd/server']
         self.mbox[mailfolder] = server
 
-        if not server == self.server:
-            if imap._imap.has_key(server):
-                if not imap._imap[server].mbox.has_key(mailfolder):
-                    imap._imap[server].mbox[mailfolder] = server
-
         log.debug(_("Server for INBOX folder %s is %s") % (mailfolder,server), level=8)
 
         return server
@@ -168,11 +164,11 @@ class Cyrus(cyruslib.CYRUS):
         """
         server = self.find_mailfolder_server(mailfolder)
         #print "server:", server
-        imap.connect(self.uri.replace(self.server,server))
+        self.connect(self.uri.replace(self.server,server))
 
         log.debug(_("Setting quota for INBOX folder %s to %s") % (mailfolder,quota), level=8)
         try:
-            imap.setquota(mailfolder, quota)
+            self.m.setquota(mailfolder, quota)
         except:
             log.error(_("Could not set quota for mailfolder %s") % (mailfolder))
 
@@ -181,13 +177,13 @@ class Cyrus(cyruslib.CYRUS):
             Login to the actual backend server, then rename.
         """
         server = self.find_mailfolder_server(from_mailfolder)
-        imap.connect(self.uri.replace(self.server,server))
+        self.connect(self.uri.replace(self.server,server))
 
         log.debug(_("Moving INBOX folder %s to %s") % (from_mailfolder,to_mailfolder), level=8)
-        imap.rename(from_mailfolder, to_mailfolder, partition)
+        self.m.rename(from_mailfolder, to_mailfolder, partition)
 
     def _getannotation(self, *args, **kw):
-        return imap.getannotation(*args, **kw)
+        return self.getannotation(*args, **kw)
 
     def _setannotation(self, mailfolder, annotation, value):
         """
@@ -200,14 +196,14 @@ class Cyrus(cyruslib.CYRUS):
         #if annotation.startswith('/private'):
 
         try:
-            imap.setannotation(mailfolder, annotation, value)
+            self.setannotation(mailfolder, annotation, value)
         except cyruslib.CYRUSError, e:
             log.error(_("Could not set annotation %r on mail folder %r: %r") % (annotation,mailfolder,e))
 
     def _xfer(self, mailfolder, current_server, new_server):
-        imap.connect(self.uri.replace(self.server,current_server))
+        self.connect(self.uri.replace(self.server,current_server))
         log.debug(_("Transferring folder %s from %s to %s") % (mailfolder, current_server, new_server), level=8)
-        imap.xfer(mailfolder, new_server)
+        self.xfer(mailfolder, new_server)
 
     def undelete_mailfolder(self, mailfolder, to_mailfolder=None, recursive=True):
         """


commit 3bcd24035724cad66dd989a0007407d3a037c037
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Apr 27 15:02:34 2012 +0100

    Simplify use of caching

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index 5990a4d..c43700f 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -1025,21 +1025,7 @@ class LDAP(pykolab.base.Base):
         for key in rcpt_addrs.keys():
             entry[key] = rcpt_addrs[key]
 
-        db = cache.init_db(self.domain)
-        _entry = db.query(cache.Entry).filter_by(uniqueid=entry['id']).first()
-        if _entry == None:
-            db.add(cache.Entry(
-                    entry['id'],
-                    entry[result_attribute],
-                    entry['modifytimestamp']
-                ))
-
-            _entry = db.query(cache.Entry).filter_by(uniqueid=entry['id']).first()
-
-        db.commit()
-
-        if not conf.changelog.has_key(entry['id']):
-            conf.changelog[entry['id']] = entry[result_attribute]
+        cache.get_entry(entry)
 
         self.imap.connect(domain=self.domain)
 


commit 7735e7549ffee28d279d5cd49008c4095aca56b8
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Apr 27 15:00:10 2012 +0100

    Correct the comparison of potential secondary mail addresses with the known primary mail address
    Make use of caching so we can maintain some sort of state in between restarts

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index 73893f5..5990a4d 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -35,6 +35,8 @@ from pykolab.translate import _
 log = pykolab.getLogger('pykolab.auth')
 conf = pykolab.getConf()
 
+import cache
+
 # Catch python-ldap-2.4 changes
 from distutils import version
 
@@ -361,6 +363,9 @@ class LDAP(pykolab.base.Base):
 
         return _entry_dns
 
+    def get_latest_sync_timestamp(self):
+        return cache.last_modify_timestamp(self.domain)
+
     def list_secondary_domains(self):
         """
             List alias domain name spaces for the current domain name space.
@@ -583,8 +588,10 @@ class LDAP(pykolab.base.Base):
             if not secondary_mail_addresses == None:
                 secondary_mail_addresses = list(set(secondary_mail_addresses))
                 # Avoid duplicates
-                while primary_mail in secondary_mail_addresses:
-                    secondary_mail_addresses.pop(secondary_mail_addresses.index(primary_mail))
+                while primary_mail_address in secondary_mail_addresses:
+                    secondary_mail_addresses.pop(
+                            secondary_mail_addresses.index(primary_mail_address)
+                        )
 
                 if not entry.has_key(secondary_mail_attribute):
                     if not len(secondary_mail_addresses) == 0:
@@ -629,7 +636,7 @@ class LDAP(pykolab.base.Base):
 
         _filter = self._kolab_filter()
 
-        modified_after = datetime.datetime.utcfromtimestamp(time.time()-18600).strftime("%Y%m%d%H%M%SZ")
+        modified_after = self.get_latest_sync_timestamp()
         _filter = "(&%s(modifytimestamp>=%s))" % (_filter,modified_after)
 
         log.debug(_("Using filter %r") % (_filter), level=8)
@@ -637,7 +644,11 @@ class LDAP(pykolab.base.Base):
         self._search(
                 self.config_get('base_dn'),
                 filterstr=_filter,
-                attrlist=[ '*', self.config_get('unique_attribute') ],
+                attrlist=[
+                        '*',
+                        self.config_get('unique_attribute'),
+                        'modifytimestamp'
+                    ],
                 callback=self._synchronize_callback,
             )
 
@@ -678,13 +689,18 @@ class LDAP(pykolab.base.Base):
             entry[mailserver_attribute] = \
                 self.get_entry_attribute(entry, mailserver_attribute)
 
+        rcpt_addrs = self.recipient_policy(entry)
+        for key in rcpt_addrs:
+            entry[key] = rcpt_addrs[key]
+
         if not entry.has_key(result_attribute):
-            entry = self.recipient_policy(entry)
             return
 
         if entry[result_attribute] == None:
             return
 
+        cache.get_entry(self.domain, entry)
+
         self.imap.connect(domain=self.domain)
 
         if not self.imap.user_mailbox_exists(entry[result_attribute]):
@@ -834,6 +850,8 @@ class LDAP(pykolab.base.Base):
         if entry[result_attribute] == None:
             return None
 
+        cache.delete_entry(self.domain, entry)
+
         self.imap.user_mailbox_delete(entry[result_attribute])
         self.imap.cleanup_acls(entry[result_attribute])
 
@@ -855,8 +873,11 @@ class LDAP(pykolab.base.Base):
 
         result_attribute = conf.get('cyrus-sasl', 'result_attribute')
 
-        if conf.changelog.has_key(entry['id']):
-            old_canon_attr = conf.changelog[entry['id']]
+        old_canon_attr = None
+
+        cache_entry = cache.get_entry(self.domain, entry)
+        if not cache_entry == None:
+            old_canon_attr = cache_entry['result_attribute']
 
         # See if we have to trigger the recipient policy. Only really applies to
         # situations in which the result_attribute is used in the old or in the
@@ -877,12 +898,19 @@ class LDAP(pykolab.base.Base):
 
         if trigger_recipient_policy:
             entry_changes = self.recipient_policy(entry)
+
+            for key in entry_changes.keys():
+                entry[key] = entry_changes[key]
+
             # Now look at entry_changes and old_canon_attr, and see if they're the
             # same value.
             if entry_changes.has_key(result_attribute):
-                if not entry_changes[result_attribute] == old_canon_attr:
+                if not old_canon_attr == None:
+                    self.imap.user_mailbox_create(entry_changes[result_attribute])
+                elif not entry_changes[result_attribute] == old_canon_attr:
                     self.imap.user_mailbox_rename(old_canon_attr, entry_changes[result_attribute])
-                    conf.changelog[entry['id']] = entry_changes[result_attribute]
+
+        cache.get_entry(self.domain, entry)
 
     def _change_moddn_sharedfolder(self, entry, change):
         pass
@@ -993,15 +1021,22 @@ class LDAP(pykolab.base.Base):
         """
         result_attribute = conf.get('cyrus-sasl', 'result_attribute')
 
-        test = self.recipient_policy(entry)
+        rcpt_addrs = self.recipient_policy(entry)
+        for key in rcpt_addrs.keys():
+            entry[key] = rcpt_addrs[key]
 
-        if not entry.has_key(result_attribute):
-            # TODO: Execute plugin-hook
-            return
+        db = cache.init_db(self.domain)
+        _entry = db.query(cache.Entry).filter_by(uniqueid=entry['id']).first()
+        if _entry == None:
+            db.add(cache.Entry(
+                    entry['id'],
+                    entry[result_attribute],
+                    entry['modifytimestamp']
+                ))
 
-        if entry[result_attribute] == None:
-            # TODO: Execute plugin-hook
-            return
+            _entry = db.query(cache.Entry).filter_by(uniqueid=entry['id']).first()
+
+        db.commit()
 
         if not conf.changelog.has_key(entry['id']):
             conf.changelog[entry['id']] = entry[result_attribute]


commit 80e68deaff4f6bfa9875b6733c9cd7f9068b2ac8
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Apr 27 14:59:36 2012 +0100

    Add auth/ldap/cache.py to the dist

diff --git a/pykolab/Makefile.am b/pykolab/Makefile.am
index 4fc10e8..db236a9 100644
--- a/pykolab/Makefile.am
+++ b/pykolab/Makefile.am
@@ -7,7 +7,8 @@ pykolab_auth_PYTHON = \
 
 pykolab_auth_ldapdir = $(pythondir)/$(PACKAGE)/auth/ldap
 pykolab_auth_ldap_PYTHON = \
-	auth/ldap/__init__.py
+	auth/ldap/__init__.py \
+	auth/ldap/cache.py
 
 pykolab_clidir = $(pythondir)/$(PACKAGE)/cli
 pykolab_cli_PYTHON = \


commit c093465b37658350e00a488492219b634f9558df
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Apr 27 14:58:56 2012 +0100

    Add a local sqlite cache layer to track when the daemon last synchronized, and what the last state was exactly

diff --git a/pykolab/auth/ldap/cache.py b/pykolab/auth/ldap/cache.py
new file mode 100644
index 0000000..30f87fe
--- /dev/null
+++ b/pykolab/auth/ldap/cache.py
@@ -0,0 +1,161 @@
+# 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 datetime
+
+import sqlalchemy
+
+from sqlalchemy import Column
+from sqlalchemy import DateTime
+from sqlalchemy import Integer
+from sqlalchemy import MetaData
+from sqlalchemy import String
+from sqlalchemy import Table
+
+from sqlalchemy import desc
+from sqlalchemy import create_engine
+from sqlalchemy.orm import mapper
+
+try:
+    from sqlalchemy.orm import relationship
+except:
+    from sqlalchemy.orm import relation as relationship
+
+try:
+    from sqlalchemy.orm import sessionmaker
+except:
+    from sqlalchemy.orm import create_session
+
+import pykolab
+
+from pykolab import utils
+from pykolab.constants import KOLAB_LIB_PATH
+from pykolab.translate import _
+
+conf = pykolab.getConf()
+log = pykolab.getLogger('pykolab.auth_cache')
+
+metadata = MetaData()
+
+db = None
+
+##
+## Classes
+##
+
+class Entry(object):
+    def __init__(self, uniqueid, result_attr, last_change):
+        self.uniqueid = uniqueid
+        self.result_attribute = result_attr
+        self.last_change = datetime.datetime.strptime(
+                last_change,
+                "%Y%m%d%H%M%SZ"
+            )
+
+##
+## Tables
+##
+
+entry_table = Table(
+        'entry', metadata,
+        Column('id', Integer, primary_key=True),
+        Column('uniqueid', String(128), nullable=False),
+        Column('result_attribute', String(128), nullable=False),
+        Column('last_change', DateTime),
+    )
+
+##
+## Table <-> Class Mappers
+##
+
+mapper(Entry, entry_table)
+
+##
+## Functions
+##
+
+def delete_entry(domain, entry):
+    result_attribute = conf.get('cyrus-sasl', 'result_attribute')
+
+    db = init_db(domain)
+    _entry = db.query(Entry).filter_by(uniqueid=entry['id']).first()
+
+    if not _entry == None:
+        db.delete(_entry)
+        db.commit()
+
+def get_entry(domain, entry):
+    result_attribute = conf.get('cyrus-sasl', 'result_attribute')
+
+    db = init_db(domain)
+    _entry = db.query(Entry).filter_by(uniqueid=entry['id']).first()
+
+    if _entry == None:
+        log.debug(_("Inserting cache entry %r") % (entry['id']), level=8)
+        db.add(
+                Entry(
+                        entry['id'],
+                        entry[result_attribute],
+                        entry['modifytimestamp']
+                    )
+            )
+
+        db.commit()
+        _entry = db.query(Entry).filter_by(uniqueid=entry['id']).first()
+    else:
+        if not _entry.last_change.strtime("%Y%m%d%H%M%SZ") == entry['modifytimestamp']:
+            log.debug(_("Updating timestamp for cache entry %r") % (entry['id']), level=8)
+            entry.last_change = entry['modifytimestamp']
+            db.commit()
+            _entry = db.query(Entry).filter_by(uniqueid=entry['id']).first()
+
+        if not _entry.result_attribute == entry[result_attribute]:
+            log.debug(_("Updating result_attribute for cache entry %r") % (entry['id']), level=8)
+            _entry.result_attribute = entry[result_attribute]
+            db.commit()
+            _entry = db.query(Entry).filter_by(uniqueid=entry['id']).first()
+
+    return _entry
+
+def init_db(domain):
+    """
+        Returns a SQLAlchemy Session() instance.
+    """
+    global db
+
+    if not db == None:
+        return db
+
+    db_uri = 'sqlite:///%s/%s.db' % (KOLAB_LIB_PATH, domain)
+    echo = conf.debuglevel > 8
+    engine = create_engine(db_uri, echo=echo)
+
+    metadata.create_all(engine)
+
+    Session = sessionmaker(bind=engine)
+    db = Session()
+
+    return db
+
+def last_modify_timestamp(domain):
+    db = init_db(domain)
+    last_change = db.query(Entry).order_by(desc(Entry.last_change)).first()
+    if not last_change == None:
+        return last_change.last_change.strftime("%Y%m%d%H%M%SZ")
+
+    return 0


commit 4054fb9435ec9e95f89d90e3794caed251071ab7
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Apr 24 15:46:42 2012 +0200

    Add kolabd and mysql setup

diff --git a/pykolab/setup/setup_kolabd.py b/pykolab/setup/setup_kolabd.py
new file mode 100644
index 0000000..09a48ad
--- /dev/null
+++ b/pykolab/setup/setup_kolabd.py
@@ -0,0 +1,42 @@
+# -*- 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 os
+import subprocess
+
+import components
+
+import pykolab
+
+from pykolab import utils
+from pykolab.constants import *
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.setup')
+conf = pykolab.getConf()
+
+def __init__():
+    components.register('kolabd', execute, description=description(), after=['ldap','imap'])
+
+def description():
+    return _("Setup the Kolab daemon.")
+
+def execute(*args, **kw):
+    subprocess.call(['service', 'kolabd', 'start'])
+    subprocess.call(['service', 'kolab-saslauthd', 'start'])
diff --git a/pykolab/setup/setup_mysql.py b/pykolab/setup/setup_mysql.py
new file mode 100644
index 0000000..4ef3c22
--- /dev/null
+++ b/pykolab/setup/setup_mysql.py
@@ -0,0 +1,60 @@
+# -*- 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 os
+import subprocess
+
+import components
+
+import pykolab
+
+from pykolab import utils
+from pykolab.constants import *
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.setup')
+conf = pykolab.getConf()
+
+def __init__():
+    components.register('mysql', execute, description=description())
+
+def description():
+    return _("Setup MySQL.")
+
+def execute(*args, **kw):
+    schema_file = None
+    for root, directories, filenames in os.walk('/usr/share/doc/'):
+        for filename in filenames:
+            if filename.startswith('kolab_wap') and filename.endswith('.sql'):
+                schema_file = os.path.join(root,filename)
+
+    if not schema_file == None:
+        subprocess.call(['service', 'mysqld', 'start'])
+        p1 = subprocess.Popen(['echo', 'create database kolab;'], stdout=subprocess.PIPE)
+        p2 = subprocess.Popen(['mysql'], stdin=p1.stdout)
+        p1.stdout.close()
+        p2.communicate()
+
+        p1 = subprocess.Popen(['cat', schema_file], stdout=subprocess.PIPE)
+        p2 = subprocess.Popen(['mysql', 'kolab'], stdin=p1.stdout)
+        p1.stdout.close()
+        p2.communicate()
+    else:
+        log.warning(_("Could not find the Kolab schema file"))
+


commit 06ceb84a63d38cc2ab238c8a24efeb8c66f2a57b
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Apr 24 15:46:29 2012 +0200

    Bump prerelease

diff --git a/configure.ac b/configure.ac
index 5d939b0..23c091a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,5 +1,5 @@
-AC_INIT([pykolab], 0.3)
-AC_SUBST([RELEASE], 0.25)
+AC_INIT([pykolab], 0.3.99)
+AC_SUBST([RELEASE], 0.11)
 
 AC_CONFIG_SRCDIR(pykolab/constants.py.in)
 


commit 281e9b2eb9847bab32e12bd2ff796c897866f895
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Apr 24 15:46:10 2012 +0200

    Set the default for imap_virtual_domains

diff --git a/pykolab/conf/defaults.py b/pykolab/conf/defaults.py
index 9fd6e8b..669aee3 100644
--- a/pykolab/conf/defaults.py
+++ b/pykolab/conf/defaults.py
@@ -23,7 +23,7 @@ class Defaults(object):
     def __init__(self, plugins=None):
         self.loglevel = logging.CRITICAL
 
-        self.virtual_domains = 'userid'
+        self.imap_virtual_domains = 'userid'
 
         # An integer or float to indicate the interval at which the Cyrus IMAP
         # library should try to retrieve annotations


commit c178883c4220c0cea8736b758d5ee82f445457ad
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Apr 24 15:45:46 2012 +0200

    Correct the authentication function

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index 09fc2b5..73893f5 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -144,20 +144,48 @@ class LDAP(pykolab.base.Base):
             )
 
         self.connect()
+        self._bind()
+
+        user_filter = self.config_get('user_filter')
+
+        _filter = '(&(|'
+
+        auth_attrs = self.config_get_list('auth_attributes')
+
+        for attr in auth_attrs:
+            _filter += "(%s=%s)" % (attr, login[0])
+            _filter += "(%s=%s@%s)" % (attr, login[0], realm)
+
+        _filter += ')%s)' % (user_filter)
+
+        _search = self.ldap.search_ext(
+                self.config_get('base_dn'),
+                ldap.SCOPE_SUBTREE,
+                _filter,
+                ['entrydn']
+            )
 
-        user_dn = self._find_user_dn(login[0], realm)
+        (
+                _result_type,
+                _result_data,
+                _result_msgid,
+                _result_controls
+            ) = self.ldap.result3(_search)
+
+        if len(_result_data) >= 1:
+            (entry_dn, entry_attrs) = _result_data[0]
 
         try:
             log.debug(_("Binding with user_dn %s and password %s")
-                % (user_dn, login[1]))
+                % (entry_dn, login[1]))
 
             # Needs to be synchronous or succeeds and continues setting retval
             # to True!!
-            self.ldap.simple_bind_s(user_dn, login[1])
+            self.ldap.simple_bind_s(entry_dn, login[1])
             retval = True
         except:
             log.debug(
-                    _("Failed to authenticate as user %s") % (user_dn),
+                    _("Failed to authenticate as user %s") % (entry_dn),
                     level=8
                 )
 


commit 9d6b584e7826c0586895d974f91fd69dae645cd0
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Apr 24 15:45:05 2012 +0200

    Connect the instance of Auth() or authentication will fail

diff --git a/saslauthd/__init__.py b/saslauthd/__init__.py
index 634594d..6daebf3 100644
--- a/saslauthd/__init__.py
+++ b/saslauthd/__init__.py
@@ -147,6 +147,7 @@ class SASLAuthDaemon(object):
                 login.append(value)
 
             auth = Auth()
+            auth.connect()
             if auth.authenticate(login):
                 clientsocket.send(struct.pack("!H2s", 2, "OK"))
             else:


commit 934359775551443e95ecf67978dec07b86377478
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Apr 24 15:10:14 2012 +0200

    Allow domain=None to be passed to Auth()

diff --git a/pykolab/auth/__init__.py b/pykolab/auth/__init__.py
index 5686e4b..47f2322 100644
--- a/pykolab/auth/__init__.py
+++ b/pykolab/auth/__init__.py
@@ -33,7 +33,7 @@ class Auth(pykolab.base.Base):
         This is the Authentication and Authorization module for PyKolab.
     """
 
-    def __init__(self, domain):
+    def __init__(self, domain=None):
         """
             Initialize the authentication class.
         """
@@ -41,7 +41,10 @@ class Auth(pykolab.base.Base):
 
         self._auth = None
 
-        self.domain = domain
+        if not domain == None:
+            self.domain = domain
+        else:
+            self.domain = conf.get('kolab', 'primary_domain')
 
     def authenticate(self, login):
         """


commit 00cfbaceebb863148b21f62af2d9c7acd89f1fa3
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Apr 24 15:06:25 2012 +0200

    Admins include cyrus-admin, not cyrus-imapd

diff --git a/pykolab/setup/setup_imap.py b/pykolab/setup/setup_imap.py
index 5731be9..683b4a4 100644
--- a/pykolab/setup/setup_imap.py
+++ b/pykolab/setup/setup_imap.py
@@ -66,7 +66,7 @@ def execute(*args, **kw):
             "ldap_time_limit": "10",
             "unixhierarchysep": "1",
             "virt_domains": "userid",
-            "admins": "cyrus-imapd",
+            "admins": "cyrus-admin",
             "annotation_definitions": "/etc/imapd.annotations.conf",
             "sieve_extensions": "fileinto reject vacation imapflags notify envelope include relational regex subaddress copy",
             "allowallsubscribe": "0",


commit 733b7119c192673e950dec81bdbad22feba4ad0d
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Apr 24 14:47:56 2012 +0200

    Write out the configuration file to the default location.
    Start the server after setup.

diff --git a/pykolab/setup/setup_ldap.py b/pykolab/setup/setup_ldap.py
index f9123d3..2fb93ca 100644
--- a/pykolab/setup/setup_ldap.py
+++ b/pykolab/setup/setup_ldap.py
@@ -112,7 +112,6 @@ ConfigDirectoryAdminID = admin
 ConfigDirectoryAdminPwd = %(admin_pass)s
 
 [slapd]
-start_server = 0
 SlapdConfigForMC = Yes
 UseExistingMC = 0
 ServerPort = 389
@@ -208,6 +207,10 @@ ServerAdminPwd = %(admin_pass)s
     conf.command_set('ldap', 'service_bind_dn', 'uid=kolab-service,ou=Special Users,%s' % (_input['rootdn']))
     conf.command_set('ldap', 'service_bind_pw', _input['kolab_service_pass'])
 
+    fp = open(conf.defaults.config_file, "w+")
+    conf.cfg_parser.write(fp)
+    fp.close()
+
     # Insert service users
     auth = Auth(_input['domain'])
     auth.connect()


commit 0d474191803e5be746e0fedefa1683cb6cb7a1b8
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Apr 24 14:47:32 2012 +0200

    Use domain_filter over kolab_domain_filter

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index 17e6533..09fc2b5 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -1193,11 +1193,7 @@ class LDAP(pykolab.base.Base):
             return []
 
         # If we haven't returned already, let's continue searching
-        domain_filter = conf.get('ldap', 'kolab_domain_filter')
-        if not domain_filter == None:
-            log.warning(_("ldap/kolab_domain_filter deprecated, use ldap/domain_filter instead."))
-        else:
-            domain_filter = conf.get('ldap', 'domain_filter')
+        domain_filter = conf.get('ldap', 'domain_filter')
 
         if domain_base_dn == None or domain_filter == None:
             return []


commit 132e781c8741029624913a965472307128456de4
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Apr 24 10:27:59 2012 +0200

    Add --silent to the options passed along to setup-ds-admin.pl

diff --git a/pykolab/setup/setup_ldap.py b/pykolab/setup/setup_ldap.py
index 8e8dba8..f9123d3 100644
--- a/pykolab/setup/setup_ldap.py
+++ b/pykolab/setup/setup_ldap.py
@@ -136,6 +136,7 @@ ServerAdminPwd = %(admin_pass)s
     command = [
             '/usr/sbin/setup-ds-admin.pl',
             '--debug',
+            '--silent',
             '--force',
             '--file=%s' % (filename)
         ]


commit d6306920bfcf1ca4342666be289096b83774af3f
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Sat Apr 21 18:05:06 2012 +0100

    Supply some inline documentation in the default configuration file

diff --git a/conf/kolab.conf b/conf/kolab.conf
index fcce25f..2a0eacd 100644
--- a/conf/kolab.conf
+++ b/conf/kolab.conf
@@ -1,30 +1,130 @@
 [kolab]
-primary_domain = kolabsys.com
+; Set this to the primary domain name space served within this Kolab Groupware
+; deployment.
+primary_domain = example.org
+
+; This is the primary authentication mechanism used, and contains the list of
+; domain name spaces for this deployment. Each domain name space may have its
+; own auth_mechanism setting.
+;
+; Valid options currently include only 'ldap'.
 auth_mechanism = ldap
+
+; The IMAP backend to use - currently supported values include only
+; 'cyrus-imap'.
 imap_backend = cyrus-imap
 
 [ldap]
+; The URI to LDAP
 ldap_uri = ldap://localhost:389
-base_dn = dc=kolabsys,dc=com
+
+; The base dn for the deployment. Note that this is the highest level in the
+; tree Kolab will ever go. Should your OU structure allow it, you could set this
+; to ou=Kolab,ou=Not-So-Private,dc=example,dc=org.
+base_dn = dc=example,dc=org
+
+; The (administrative) bind dn and corresponding password.
+;
+; Feel free to set this to a DN with only read permissions on the tree. These
+; credentials are used by the Kolab Daemon only, as it might need to set
+; additional attributes in order to apply plugins successfully. Such attributes
+; could include the first two values in the 'mail_attributes' list (see further
+; down) to complete the 'recipient_policy' (see further down), mail quota,
+; the mail server attribute, and others.
 bind_dn = cn=Directory Manager
-bind_pw = 0B2NFj581H8kZgO
+bind_pw = Welcome123
+
+; Bind DN and password used for services. The DN should have read and search
+; privileges only, but should be able to read all relevant parts of the tree.
+;
+; These credentials are used by, among others, Postfix, Wallace, programs that
+; need to find the user DN before binding as the user (including the webadmin
+; API, Roundcube, Syncroton).
+service_bind_dn = uid=kolab-service,ou=Special Users,%(base_dn)s
+service_bind_pw = wc18bqshFmifGtN
+
+; The base DN, search scope and filter to use when searching for users of any
+; type. User types are of primary purpose to the web admin (API), but the
+; generic base DN, scope and filter allow us to configure other services as
+; well, including Address Books in Roundcube and for Syncroton, the list of
+; users in the web admin (API), etc.
 user_base_dn = ou=People,%(base_dn)s
 user_scope = sub
 user_filter = (objectclass=inetorgperson)
+
+; The base DN, scope and filter to use when searching for users of the 'kolab'
+; type. This filter is preferred when searching for Kolab users specifically,
+; such as in the synchronisation between LDAP and IMAP. Also, it is
+; (preferrably) only Kolab users that are allowed to login, use the SMTP server,
+; etc.
+;
+; Note that all user_* settings are valid, and those not available with a kolab_
+; prefix fall back to using the generic user_* equivalent setting.
+kolab_user_base_dn = ou=People,%(base_dn)s
 kolab_user_filter = (objectclass=kolabinetorgperson)
+
+; Add additional <key>_user_base_dn, <key>_user_scope and <key>_user_filter.
+; Useful for configuring sub-address books, and for the webadmin API when adding
+; new users of the example type key 'posix' - the new user will be added in the
+; OU configured below.
+;posix_user_base_dn = ou=POSIX Accounts,ou=People,%(base_dn)s
+;posix_user_scope = one
+;posix_user_filter = (&(objectclass=posixaccount)(uidnumber>=1000))
+
+; The same as for users, but applicable to groups
 group_base_dn = ou=Groups,%(base_dn)s
-group_scope = sub
 group_filter = (|(objectclass=groupofuniquenames)(objectclass=groupofurls))
+group_scope = sub
 kolab_group_filter = (|(objectclass=kolabgroupofuniquenames)(objectclass=kolabgroupofurls))
+
+; 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
 domain_filter = (&(associatedDomain=*))
 domain_name_attribute = associateddomain
+; Attribute that holds the root dn for the domain name space. If this attribute
+; does not exist, a standard root dn is formed from the primary domain name
+; space (the value in the RDN), as follows:
+;
+;     'dc=' + ',dc='.join(domainname.split('.'))
+;
+; or, in example:
+;
+;     domain: example.org
+;     root dn: dc=example,dc=org
 domain_rootdn_attribute = inetdomainbasedn
-service_bind_dn = uid=kolab-service,ou=Special Users,dc=kolabsys,dc=com
-service_bind_pw = wc18bqshFmifGtN
+
+; The attribute that holds the quota.
 quota_attribute = mailquota
+; A unique attribute that can be used to identify the entry beyond renames and
+; moves. Note that 'nsuniqueid' is specific to all Netscape-based directory
+; services.
+;
+; For OpenLDAP, use 'entrydn' - the 'entryUUID' can regrettably not be searched
+; with.
 unique_attribute = nsuniqueid
+
+; Attribute names that hold valid, internal recipient addresses. Note the use
+; of mail and alias frees up the use of mailAlternateAddress to contain a user's
+; external email address.
+;
+; Syntax is a comma- or comma-space separated list.
+;
+; The first value is used for the purpose of a single "primary" email address,
+; that could be subject to a recipient policy, the second is used for the
+; purpose of one or more secondary mail addresses, that could also be subject to
+; a recipient policy.
 mail_attributes = mail, alias
+
+; Attributes that hold valid authentication login names. Use 'mail', 'alias' and
+; optionally 'uid' (the uid is marked as an auth_attribute automatically), so
+; that a user can login with;
+;
+; - uid (i.e. 'jdoe'),
+; - mail, fully qualified and localpart only (i.e. "john.doe" and
+;   "john.doe at example.org"),
+; - alias, fully qualified and localpart only (i.e. "j.doe" and
+;   "j.doe at example.org).
 auth_attributes = mail, alias, uid
 
 [kolab_smtp_access_policy]
@@ -33,102 +133,112 @@ cache_retention = 30
 address_search_attrs = mail, alias
 
 [cyrus-imap]
+; The URI to use to connect to IMAP. Note that pykolab itself can detect whether
+; or not Cyrus IMAP is deployed in a Murder topology, and should be able to
+; connect to individual backends as well.
 uri = imaps://localhost:993
+; The login username to use for global administration.
 admin_login = cyrus-admin
-admin_password = xgbGH1xHSCFxPH2
+; The corresponding password.
+admin_password = Welcome123
 
 [cyrus-sasl]
+; The user canonification result attribute.
 result_attribute = mail
 
+; This is a domain name space specific section, that enables us to override
+; all settings, for example, the LDAP URI, base and bind DNs, scopes, filters,
+; etc. Note that overriding the LDAP settings for the primary domain name space
+; does not make any sense.
 [example.org]
 default_quota = 1048576
 primary_mail = %(givenname)s.%(surname)s@%(domain)s
 secondary_mail = {
-	0: {
-	"{0}.{1}@{2}": "format('%(givenname)s'[0:1].capitalize(), '%(surname)s', '%(domain)s')"
-	},
-	1: {
-	"{0}@{1}": "format('%(uid)s', '%(domain)s')"
-	},
-	2: {
-	"{0}@{1}": "format('%(givenname)s.%(surname)s', '%(domain)s')"
-	}
-	}
-autocreate_folders = {
-	'Archive': {
-	'quota': 0
-	},
-	'Calendar': {
-	'annotations': {
-	'/vendor/kolab/folder-test': "true",
-	'/vendor/kolab/folder-type': "event.default",
-	},
-	},
-	'Calendar/Personal Calendar': {
-	'annotations': {
-	'/vendor/kolab/folder-test': "true",
-	'/vendor/kolab/folder-type': "event",
-	},
-	},
-	'Configuration': {
-	'annotations': {
-	'/vendor/kolab/folder-test': "true",
-	'/vendor/kolab/folder-type': "configuration.default",
-	},
-	},
-	'Drafts': {
-	'annotations': {
-	'/vendor/kolab/folder-test': "true",
-	'/vendor/kolab/folder-type': "mail.drafts",
-	},
-	},
-	'Contacts': {
-	'annotations': {
-	'/vendor/kolab/folder-test': "true",
-	'/vendor/kolab/folder-type': "contact.default",
-	},
-	},
-	'Contacts/Personal Contacts': {
-	'annotations': {
-	'/vendor/kolab/folder-test': "true",
-	'/vendor/kolab/folder-type': "contact",
-	},
-	},
-	'Journal': {
-	'annotations': {
-	'/vendor/kolab/folder-test': "true",
-	'/vendor/kolab/folder-type': "journal.default",
-	},
-	},
-	'Notes': {
-	'annotations': {
-	'/vendor/kolab/folder-test': "true",
-	'/vendor/kolab/folder-type': 'note.default',
-	},
-	},
-	'Sent': {
-	'annotations': {
-	'/vendor/kolab/folder-test': "true",
-	'/vendor/kolab/folder-type': "mail.sentitems",
-	},
-	},
-	'Spam': {
-	'annotations': {
-	'/vendor/kolab/folder-test': "true",
-	'/vendor/kolab/folder-type': "mail.junkemail",
-	},
-	},
-	'Tasks': {
-	'annotations': {
-	'/vendor/kolab/folder-test': "true",
-	'/vendor/kolab/folder-type': "task.default",
-	},
-	},
-	'Trash': {
-	'annotations': {
-	'/vendor/kolab/folder-test': "true",
-	'/vendor/kolab/folder-type': "mail.trash",
-	},
-	},
-	}
+        0: {
+                "{0}.{1}@{2}": "format('%(givenname)s'[0:1].capitalize(), '%(surname)s', '%(domain)s')"
+            },
+        1: {
+                "{0}@{1}": "format('%(uid)s', '%(domain)s')"
+            },
+        2: {
+                "{0}@{1}": "format('%(givenname)s.%(surname)s', '%(domain)s')"
+        }
+    }
 
+autocreate_folders = {
+        'Archive': {
+                'quota': 0
+            },
+        'Calendar': {
+                'annotations': {
+                        '/vendor/kolab/folder-test': "true",
+                        '/vendor/kolab/folder-type': "event.default",
+                    },
+            },
+        'Calendar/Personal Calendar': {
+                'annotations': {
+                        '/vendor/kolab/folder-test': "true",
+                        '/vendor/kolab/folder-type': "event",
+                    },
+            },
+        'Configuration': {
+                'annotations': {
+                        '/vendor/kolab/folder-test': "true",
+                        '/vendor/kolab/folder-type': "configuration.default",
+                    },
+            },
+        'Drafts': {
+                'annotations': {
+                        '/vendor/kolab/folder-test': "true",
+                        '/vendor/kolab/folder-type': "mail.drafts",
+                    },
+            },
+        'Contacts': {
+                'annotations': {
+                        '/vendor/kolab/folder-test': "true",
+                        '/vendor/kolab/folder-type': "contact.default",
+                },
+            },
+        'Contacts/Personal Contacts': {
+                'annotations': {
+                        '/vendor/kolab/folder-test': "true",
+                        '/vendor/kolab/folder-type': "contact",
+                    },
+            },
+        'Journal': {
+                'annotations': {
+                        '/vendor/kolab/folder-test': "true",
+                        '/vendor/kolab/folder-type': "journal.default",
+                    },
+            },
+        'Notes': {
+                'annotations': {
+                        '/vendor/kolab/folder-test': "true",
+                        '/vendor/kolab/folder-type': 'note.default',
+                    },
+            },
+        'Sent': {
+                'annotations': {
+                        '/vendor/kolab/folder-test': "true",
+                        '/vendor/kolab/folder-type': "mail.sentitems",
+                    },
+            },
+        'Spam': {
+                'annotations': {
+                        '/vendor/kolab/folder-test': "true",
+                        '/vendor/kolab/folder-type': "mail.junkemail",
+                    },
+            },
+        'Tasks': {
+                'annotations': {
+                        '/vendor/kolab/folder-test': "true",
+                        '/vendor/kolab/folder-type': "task.default",
+                    },
+            },
+        'Trash': {
+                'annotations': {
+                        '/vendor/kolab/folder-test': "true",
+                        '/vendor/kolab/folder-type': "mail.trash",
+                    },
+            },
+    }


commit 9b8cc5b6819b2fe576d0fbe2a07e6da859bf76da
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Sat Apr 21 16:00:07 2012 +0100

    Update pykolab.spec for Fedora vs. Enterprise Linux installations (sysvinit vs. systemd, to be more precise)
    Install the Wallace init scripts

diff --git a/pykolab.spec.in b/pykolab.spec.in
index f0017dc..1094fed 100644
--- a/pykolab.spec.in
+++ b/pykolab.spec.in
@@ -26,7 +26,7 @@ Source0:            http://files.kolab.org/releases/%{name}-%{version}.tar.gz
 BuildRoot:          %{_tmppath}/%{name}-%{version}-%{release}-root
 BuildArch:          noarch
 Requires:           kolab-cli = %{version}-%{release}
-Requires:           python-ldap
+Requires:           python-ldap >= 2.4
 Requires(pre):      /usr/sbin/useradd
 Requires(pre):      /usr/sbin/usermod
 Requires(pre):      /usr/sbin/groupadd
@@ -53,6 +53,7 @@ Cyrus IMAP Telemetry logging handling capabilities for Kolab Groupware
 Summary:            Kolab CLI components
 Group:              Applications/System
 BuildRequires:      intltool, gettext, python
+Requires:           %{name} = %{version}-%{release}
 
 %description -n kolab-cli
 Kolab CLI utilities
@@ -64,7 +65,7 @@ Kolab CLI utilities
 Summary:            Kolab SASL Authentication Daemon
 Group:              Applications/System
 BuildRequires:      intltool, gettext, python
-Requires:           pykolab = %{version}-%{release}
+Requires:           %{name} = %{version}-%{release}
 
 %description -n kolab-saslauthd
 Kolab SASL Authentication Daemon for multi-domain, multi-authn database deployments
@@ -76,7 +77,7 @@ Kolab SASL Authentication Daemon for multi-domain, multi-authn database deployme
 Summary:            Kolab Server implemented in Python
 Group:              Applications/System
 BuildRequires:      intltool, gettext, python
-Requires:           pykolab = %{version}-%{release}
+Requires:           %{name} = %{version}-%{release}
 
 %description -n kolab-server
 Kolab Server implemented in Python
@@ -89,7 +90,7 @@ Summary:            Kolab SMTP Access Policy for Postfix
 Group:              Applications/System
 BuildRequires:      intltool, gettext, python
 Requires:           postfix
-Requires:           pykolab = %{version}-%{release}
+Requires:           %{name} = %{version}-%{release}
 Requires:           python-sqlalchemy
 Requires:           MySQL-python
 
@@ -102,7 +103,7 @@ Kolab SMTP Access Policy for Postfix
 %package -n wallace
 Summary:            Kolab Content-Filter
 Group:              Applications/System
-Requires:           pykolab = %{version}-%{release}
+Requires:           %{name} = %{version}-%{release}
 Requires:           python-sqlalchemy
 Requires:           MySQL-python
 
@@ -123,15 +124,18 @@ make install DESTDIR=%{buildroot}
 mkdir -p %{buildroot}/%{_unitdir}
 %{__install} -p -m 644 kolabd/kolabd.systemd %{buildroot}/%{_unitdir}/kolabd.service
 %{__install} -p -m 644 saslauthd/kolab-saslauthd.systemd %{buildroot}/%{_unitdir}/kolab-saslauthd.service
+%{__install} -p -m 644 wallace/wallace.systemd %{buildroot}/%{_unitdir}/wallace.service
 %else
 mkdir -p %{buildroot}/%{_initddir}
 %{__install} -p -m 755 kolabd/kolabd.sysvinit %{buildroot}/%{_initrddir}/kolabd
 %{__install} -p -m 755 saslauthd/kolab-saslauthd.sysvinit %{buildroot}/%{_initrddir}/kolab-saslauthd
+%{__install} -p -m 755 wallace/wallace.sysvinit %{buildroot}/%{_initrddir}/wallace
 %endif
 
 mkdir -p %{buildroot}/%{_sysconfdir}/sysconfig
 %{__install} -p -m 644 kolabd/kolabd.sysconfig %{buildroot}/%{_sysconfdir}/sysconfig/kolabd
 %{__install} -p -m 644 saslauthd/kolab-saslauthd.sysconfig %{buildroot}/%{_sysconfdir}/sysconfig/kolab-saslauthd
+%{__install} -p -m 644 wallace/wallace.sysconfig %{buildroot}/%{_sysconfdir}/sysconfig/wallace
 
 %find_lang pykolab
 
@@ -276,6 +280,7 @@ rm -rf %{buildroot}
 %{_bindir}/*
 %{_sbindir}/kolab
 %{_sbindir}/kolab-conf
+%{_sbindir}/setup-kolab
 %dir %{python_sitelib}/pykolab/cli/
 %{python_sitelib}/pykolab/cli/*.py
 %{python_sitelib}/pykolab/cli/*.pyc
@@ -288,7 +293,11 @@ rm -rf %{buildroot}
 %files -n kolab-saslauthd
 %defattr(-,root,root,-)
 %doc AUTHORS ChangeLog COPYING
+%if 0%{?fedora} >= 15
+%{_unitdir}/kolab-saslauthd.service
+%else
 %{_initrddir}/kolab-saslauthd
+%endif
 %config(noreplace) %{_sysconfdir}/sysconfig/kolab-saslauthd
 %{_sbindir}/kolab-saslauthd
 %{python_sitelib}/saslauthd/
@@ -298,7 +307,11 @@ rm -rf %{buildroot}
 %files -n kolab-server
 %defattr(-,root,root,-)
 %doc AUTHORS ChangeLog COPYING
+%if 0%{?fedora} >= 15
+%{_unitdir}/kolabd.service
+%else
 %{_initrddir}/kolabd
+%endif
 %config(noreplace) %{_sysconfdir}/sysconfig/kolabd
 %{_sbindir}/kolabd
 %{python_sitelib}/kolabd/
@@ -312,6 +325,12 @@ rm -rf %{buildroot}
 %files -n wallace
 %defattr(-,root,root,-)
 %doc AUTHORS ChangeLog COPYING
+%if 0%{?fedora} >= 15
+%{_unitdir}/wallace.service
+%else
+%{_initrddir}/wallace
+%endif
+%{_sysconfdir}/sysconfig/wallace
 %{_sbindir}/wallaced
 %{python_sitelib}/wallace
 


commit 9244a6c54b09fa841369b7aea26a201adfd10437
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Sat Apr 21 15:59:19 2012 +0100

    Allow overriding the fqdn and thus hostname and domain name used by adding --fqdn

diff --git a/pykolab/setup/setup_ldap.py b/pykolab/setup/setup_ldap.py
index 52e96ef..8e8dba8 100644
--- a/pykolab/setup/setup_ldap.py
+++ b/pykolab/setup/setup_ldap.py
@@ -39,6 +39,17 @@ conf = pykolab.getConf()
 def __init__():
     components.register('ldap', execute, description=description())
 
+def cli_options():
+    ldap_group = conf.add_cli_parser_option_group(_("LDAP Options"))
+
+    ldap_group.add_option(
+            "--fqdn",
+            dest    = "fqdn",
+            action  = "store",
+            default = fqdn,
+            help    = _("Specify FQDN (overriding defaults).")
+        )
+
 def description():
     return _("Setup LDAP.")
 
@@ -72,13 +83,23 @@ def execute(*args, **kw):
         _input['userid'] = "nobody"
         _input['group'] = "nobody"
 
-    _input['fqdn'] = fqdn
-    _input['hostname'] = hostname.split('.')[0]
-    _input['domain'] = domainname
+    # TODO: This takes the system fqdn, domainname and hostname, rather then
+    # the desired fqdn, domainname and hostname.
+    #
+    # TODO^2: This should be confirmed.
+
+    if conf.fqdn:
+        _input['fqdn'] = conf.fqdn
+        _input['hostname'] = conf.fqdn.split('.')[0]
+        _input['domain'] = '.'.join(conf.fqdn.split('.')[1:])
+    else:
+        _input['fqdn'] = fqdn
+        _input['hostname'] = hostname.split('.')[0]
+        _input['domain'] = domainname
 
-    _input['nodotdomain'] = domainname.replace('.','_')
+    _input['nodotdomain'] = _input['domain'].replace('.','_')
 
-    _input['rootdn'] = utils.standard_root_dn(domainname)
+    _input['rootdn'] = utils.standard_root_dn(_input['domain'])
 
     data = """
 [General]
@@ -91,6 +112,7 @@ ConfigDirectoryAdminID = admin
 ConfigDirectoryAdminPwd = %(admin_pass)s
 
 [slapd]
+start_server = 0
 SlapdConfigForMC = Yes
 UseExistingMC = 0
 ServerPort = 389
@@ -113,7 +135,8 @@ ServerAdminPwd = %(admin_pass)s
 
     command = [
             '/usr/sbin/setup-ds-admin.pl',
-            '--silent',
+            '--debug',
+            '--force',
             '--file=%s' % (filename)
         ]
 
@@ -125,6 +148,12 @@ ServerAdminPwd = %(admin_pass)s
 
     (stdoutdata, stderrdata) = setup_389.communicate()
 
+    log.debug(_("Setup DS stdout:"), level=8)
+    log.debug(stdoutdata, level=8)
+
+    log.debug(_("Setup DS stderr:"), level=8)
+    log.debug(stderrdata, level=8)
+
     # Find the kolab schema. It's installed as %doc in the kolab-schema package.
     # TODO: Chown nobody, nobody, chmod 440
     schema_file = None
@@ -242,10 +271,10 @@ ServerAdminPwd = %(admin_pass)s
     # TODO: Add kolab-admin role
     # TODO: Assign kolab-admin admin ACLs
     # TODO: Add the primary domain to cn=kolab,cn=config
-    dn = "associateddomain=%s,cn=kolab,cn=config" % (domainname)
+    dn = "associateddomain=%s,cn=kolab,cn=config" % (_input['domain'])
     attrs = {}
     attrs['objectclass'] = ['top','domainrelatedobject']
-    attrs['associateddomain'] = '%s' % (domainname)
+    attrs['associateddomain'] = '%s' % (_input['domain'])
 
     ldif = ldap.modlist.addModlist(attrs)
 


commit 8f4dbeeaf917f4fb6fa4cf693ab131fa444144ed
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Sat Apr 21 15:58:11 2012 +0100

    Allow setup components to add cli options, and suppress sequencing output

diff --git a/pykolab/setup/components.py b/pykolab/setup/components.py
index 432844b..4633160 100644
--- a/pykolab/setup/components.py
+++ b/pykolab/setup/components.py
@@ -128,7 +128,6 @@ def execute(component_name, *args, **kw):
                 execute_this = True
 
                 if component in executed_components:
-                    print "component", component, "already executed, continuing"
                     execute_this = False
 
                 if component == "help":
@@ -136,10 +135,8 @@ def execute(component_name, *args, **kw):
 
                 if execute_this:
                     if components[component].has_key('after'):
-                        print "component", component, "has after key, let's see what it holds"
                         for _component in components[component]['after']:
                             if not _component in executed_components:
-                                print "component", component, "is waiting for component", _component
                                 execute_this = False
 
                 if execute_this:
@@ -169,14 +166,14 @@ def execute(component_name, *args, **kw):
         group = components[component_name]['group']
         component_name = components[component_name]['component_name']
         try:
-            exec("from %s.cmd_%s import cli_options as %s_%s_cli_options" % (group,component_name,group,component_name))
+            exec("from %s.setup_%s import cli_options as %s_%s_cli_options" % (group,component_name,group,component_name))
             exec("%s_%s_cli_options()" % (group,component_name))
         except ImportError, e:
             pass
 
     else:
         try:
-            exec("from cmd_%s import cli_options as %s_cli_options" % (component_name,component_name))
+            exec("from setup_%s import cli_options as %s_cli_options" % (component_name,component_name))
             exec("%s_cli_options()" % (component_name))
         except ImportError, e:
             pass
@@ -200,9 +197,9 @@ def register_group(dirname, module):
             continue
 
         for filename in filenames:
-            if filename.startswith('cmd_') and filename.endswith('.py'):
+            if filename.startswith('setup_') and filename.endswith('.py'):
                 module_name = filename.replace('.py','')
-                component_name = module_name.replace('cmd_', '')
+                component_name = module_name.replace('setup_', '')
                 #print "exec(\"from %s.%s import __init__ as %s_%s_register\"" % (module,module_name,module,component_name)
                 exec("from %s.%s import __init__ as %s_%s_register" % (module,module_name,module,component_name))
                 exec("%s_%s_register()" % (module,component_name))


commit 0eab2f43ac6bcfb72b1f8fb970376aca11d2d328
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Sat Apr 21 15:57:28 2012 +0100

    Install setup-kolab script

diff --git a/Makefile.am b/Makefile.am
index 7668a02..cab8db0 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -191,6 +191,7 @@ endif
 	$(INSTALL) -p -m 755 saslauthd.py $(DESTDIR)/$(sbindir)/kolab-saslauthd
 	$(INSTALL) -p -m 755 wallace.py $(DESTDIR)/$(sbindir)/wallaced
 	$(INSTALL) -p -m 755 kolabtest.py $(DESTDIR)/$(bindir)/kolab-test
+	$(INSTALL) -p -m 755 setup-kolab.py $(DESTDIR)/$(sbindir)/setup-kolab
 	$(INSTALL) -p -m 644 cyruslib.py $(DESTDIR)/$(pythondir)
 	$(INSTALL) -p -m 755 bin/kolab_parse_telemetry.py $(DESTDIR)/$(sbindir)/kolab_parse_telemetry
 	$(INSTALL) -p -m 755 bin/kolab_smtp_access_policy.py $(DESTDIR)/$(libexecdir)/postfix/kolab_smtp_access_policy


commit 3a1ab1fb04fc2030776ad22d845f3c0c6ea77ace
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Apr 20 11:46:34 2012 +0100

    Correct condition

diff --git a/pykolab.spec.in b/pykolab.spec.in
index 08a9f17..f0017dc 100644
--- a/pykolab.spec.in
+++ b/pykolab.spec.in
@@ -119,7 +119,7 @@ This is the Kolab Content Filter, with plugins
 rm -rf %{buildroot}
 make install DESTDIR=%{buildroot}
 
-%if 0{?fedora} > 15
+%if 0%{?fedora} >= 15
 mkdir -p %{buildroot}/%{_unitdir}
 %{__install} -p -m 644 kolabd/kolabd.systemd %{buildroot}/%{_unitdir}/kolabd.service
 %{__install} -p -m 644 saslauthd/kolab-saslauthd.systemd %{buildroot}/%{_unitdir}/kolab-saslauthd.service


commit a752c475cda8cbb3b16a6204123810990f764488
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Apr 20 11:44:18 2012 +0100

    Update installation and %pre/%post installation routines
    Move CLI components to kolab-cli package

diff --git a/pykolab.spec.in b/pykolab.spec.in
index ba05198..08a9f17 100644
--- a/pykolab.spec.in
+++ b/pykolab.spec.in
@@ -166,42 +166,71 @@ if [ $1 -gt 1 ] ; then
 fi
 
 %post -n kolab-saslauthd
-if [ $1 -eq  1 ] ; then
-    chkconfig --add kolab-saslauthd
+if [ $1 -eq 1 ]; then
+%if 0%{?fedora} >= 15
+    /bin/systemctl daemon-reload >/dev/null 2>&1 || :
+%else
+    /sbin/chkconfig --add kolab-saslauthd
+%endif
 else
     /sbin/service kolab-saslauthd condrestart
 fi
 
 %preun -n kolab-saslauthd
 if [ $1 = 0 ]; then
+%if 0%{?fedora} >= 15
+    /bin/systemctl --no-reload disable kolab-saslauthd.service >/dev/null 2>&1 || :
+    /bin/systemctl stop kolab-saslauthd.service >/dev/null 2>&1 || :
+%else
     /sbin/service kolab-saslauthd stop > /dev/null 2>&1
     /sbin/chkconfig --del kolab-saslauthd
+%endif
 fi
 
 %post -n kolab-server
 if [ $1 -eq  1 ] ; then
-    chkconfig --add kolabd
-else
-    /sbin/service kolabd condrestart
+%if 0%{?fedora} >= 15
+    /bin/systemctl daemon-reload >/dev/null 2>&1 || :
+%else
+    /sbin/chkconfig --add kolabd
+%endif
 fi
 
 %preun -n kolab-server
 if [ $1 = 0 ]; then
+%if 0%{?fedora} >= 15
+    /bin/systemctl --no-reload disable kolabd.service >/dev/null 2>&1 || :
+    /bin/systemctl stop kolabd.service >/dev/null 2>&1 || :
+%else
     /sbin/service kolabd stop > /dev/null 2>&1
     /sbin/chkconfig --del kolabd
+%endif
 fi
 
 %post -n wallace
 if [ $1 -eq  1 ] ; then
+%if 0%{?fedora} >= 15
+    /bin/systemctl daemon-reload >/dev/null 2>&1 || :
+%else
     chkconfig --add wallace
+%endif
 else
+%if 0%{?fedora} >= 15
     /sbin/service wallace condrestart
+%else
+    /bin/systemctl reload-or-try-restart wallace.service
+%endif
 fi
 
 %preun -n wallace
 if [ $1 = 0 ]; then
+%if 0%{?fedora} >= 15
+    /bin/systemctl --no-reload disable wallace.service
+    /biin/systemctl stop wallace.service
+%else
     /sbin/service wallace stop > /dev/null 2>&1
     /sbin/chkconfig --del wallace
+%endif
 fi
 
 %clean
@@ -217,10 +246,6 @@ rm -rf %{buildroot}
 %{python_sitelib}/pykolab/*.pyc
 %{python_sitelib}/pykolab/*.pyo
 %{python_sitelib}/pykolab/auth/
-%dir %{python_sitelib}/pykolab/cli/
-%{python_sitelib}/pykolab/cli/*.py
-%{python_sitelib}/pykolab/cli/*.pyc
-%{python_sitelib}/pykolab/cli/*.pyo
 %{python_sitelib}/pykolab/conf/
 %{python_sitelib}/pykolab/imap/
 %dir %{python_sitelib}/pykolab/plugins/
@@ -230,7 +255,6 @@ rm -rf %{buildroot}
 %{python_sitelib}/pykolab/plugins/defaultfolders
 %{python_sitelib}/pykolab/plugins/dynamicquota
 %{python_sitelib}/pykolab/plugins/recipientpolicy
-%exclude %{python_sitelib}/pykolab/setup/
 %exclude %{python_sitelib}/pykolab/tests/
 %{python_sitelib}/kolab/
 %{python_sitelib}/cyruslib.py
@@ -252,6 +276,14 @@ rm -rf %{buildroot}
 %{_bindir}/*
 %{_sbindir}/kolab
 %{_sbindir}/kolab-conf
+%dir %{python_sitelib}/pykolab/cli/
+%{python_sitelib}/pykolab/cli/*.py
+%{python_sitelib}/pykolab/cli/*.pyc
+%{python_sitelib}/pykolab/cli/*.pyo
+%dir %{python_sitelib}/pykolab/setup/
+%{python_sitelib}/pykolab/setup/*.py
+%{python_sitelib}/pykolab/setup/*.pyc
+%{python_sitelib}/pykolab/setup/*.pyo
 
 %files -n kolab-saslauthd
 %defattr(-,root,root,-)


commit 1ec0bdeddb9919ab66f0f35248ccd36964135229
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Apr 20 11:34:04 2012 +0100

    Install systemd unit files for Fedora > 15

diff --git a/pykolab.spec.in b/pykolab.spec.in
index 574fa26..ba05198 100644
--- a/pykolab.spec.in
+++ b/pykolab.spec.in
@@ -119,15 +119,19 @@ This is the Kolab Content Filter, with plugins
 rm -rf %{buildroot}
 make install DESTDIR=%{buildroot}
 
-mkdir -p \
-    %{buildroot}/%{_initrddir} \
-    %{buildroot}/%{_sysconfdir}/sysconfig
-
-%{__install} -p -m 644 saslauthd/kolab-saslauthd.sysconfig %{buildroot}/%{_sysconfdir}/sysconfig/kolab-saslauthd
+%if 0{?fedora} > 15
+mkdir -p %{buildroot}/%{_unitdir}
+%{__install} -p -m 644 kolabd/kolabd.systemd %{buildroot}/%{_unitdir}/kolabd.service
+%{__install} -p -m 644 saslauthd/kolab-saslauthd.systemd %{buildroot}/%{_unitdir}/kolab-saslauthd.service
+%else
+mkdir -p %{buildroot}/%{_initddir}
+%{__install} -p -m 755 kolabd/kolabd.sysvinit %{buildroot}/%{_initrddir}/kolabd
 %{__install} -p -m 755 saslauthd/kolab-saslauthd.sysvinit %{buildroot}/%{_initrddir}/kolab-saslauthd
+%endif
 
+mkdir -p %{buildroot}/%{_sysconfdir}/sysconfig
 %{__install} -p -m 644 kolabd/kolabd.sysconfig %{buildroot}/%{_sysconfdir}/sysconfig/kolabd
-%{__install} -p -m 755 kolabd/kolabd.sysvinit %{buildroot}/%{_initrddir}/kolabd
+%{__install} -p -m 644 saslauthd/kolab-saslauthd.sysconfig %{buildroot}/%{_sysconfdir}/sysconfig/kolab-saslauthd
 
 %find_lang pykolab
 


commit 07f108b041d21880d6d1fababb78d1c33b447f1f
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Apr 20 11:33:34 2012 +0100

    Rename wallace system files back to the intended names

diff --git a/wallace/Makefile.am b/wallace/Makefile.am
index 3fcef69..78439c9 100644
--- a/wallace/Makefile.am
+++ b/wallace/Makefile.am
@@ -1,7 +1,7 @@
 EXTRA_DIST = \
-	wallaced.sysconfig \
-	wallaced.systemd \
-	wallaced.sysvinit
+	wallace.sysconfig \
+	wallace.systemd \
+	wallace.sysvinit
 
 wallacedir = $(pythondir)/wallace
 wallace_PYTHON = \
diff --git a/wallace/wallace.sysconfig b/wallace/wallace.sysconfig
new file mode 100644
index 0000000..7832bdb
--- /dev/null
+++ b/wallace/wallace.sysconfig
@@ -0,0 +1,5 @@
+# Configuration file for the Kolab Server daemon Wallace.
+#
+# See wallaced --help for more flags.
+#
+FLAGS="--fork -l warning"
diff --git a/wallace/wallace.systemd b/wallace/wallace.systemd
new file mode 100644
index 0000000..73c16d7
--- /dev/null
+++ b/wallace/wallace.systemd
@@ -0,0 +1,15 @@
+[Unit]
+Description=Wallace Content Filter
+After=syslog.target network.target
+
+[Service]
+Type=forking
+PIDFile=/var/run/wallaced/wallaced.pid
+EnvironmentFile=/etc/sysconfig/wallace
+ExecStart=/usr/sbin/wallace $FLAGS
+ExecReload=/bin/kill -HUP $MAINPID
+ExecStop=/bin/kill -TERM $MAINPID
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/wallace/wallace.sysvinit b/wallace/wallace.sysvinit
new file mode 100644
index 0000000..3b545a9
--- /dev/null
+++ b/wallace/wallace.sysvinit
@@ -0,0 +1,108 @@
+#! /bin/bash
+#
+# wallace  Start/Stop the Kolab server daemon Wallace
+#
+# chkconfig:        - 65 10
+# description:      The Kolab Wallace server daemon is a content filtering daemon.
+# processname: wallaced
+
+### BEGIN INIT INFO
+# Provides: wallaced
+# Required-Start: $local_fs $network
+# Required-Stop: $local_fs $network
+# Short-Description: Start/Stop the Kolab Server daemon
+# Description:      The Kolab Wallace server daemon is a content filtering daemon.
+### END INIT INFO
+
+# Source function library.
+. /etc/init.d/functions
+
+# Source our configuration file for these variables.
+FLAGS="--fork -l warning"
+
+if [ -f /etc/sysconfig/wallace ] ; then
+    . /etc/sysconfig/wallace
+fi
+
+RETVAL=0
+
+# Set up some common variables before we launch into what might be
+# considered boilerplate by now.
+prog=wallace
+path=/usr/sbin/wallaced
+lockfile=/var/lock/subsys/$prog
+pidfile=/var/run/wallace/wallaced.pid
+
+start() {
+    [ -x $path ] || exit 5
+    echo -n $"Starting $prog: "
+    daemon $DAEMONOPTS $path -p $pidfile $FLAGS
+    RETVAL=$?
+    echo
+    [ $RETVAL -eq 0 ] && touch $lockfile
+    return $RETVAL
+}
+
+stop() {
+    echo -n $"Stopping $prog: "
+    killproc $prog
+    RETVAL=$?
+    echo
+    [ $RETVAL -eq 0 ] && rm -f $lockfile
+    return $RETVAL
+}
+
+restart() {
+    stop
+    start
+}
+
+reload() {
+    restart
+}
+
+force_reload() {
+    restart
+}
+
+rh_status() {
+    # run checks to determine if the service is running or use generic status
+    status -p $pidfile $prog
+}
+
+rh_status_q() {
+    rh_status >/dev/null 2>&1
+}
+
+case "$1" in
+  start)
+    rh_status_q && exit 0
+    start
+    ;;
+  stop)
+    rh_status_q || exit 0
+    stop
+    ;;
+  restart)
+    restart
+    ;;
+  reload)
+    rh_status_q || exit 7
+    reload
+    ;;
+  force-reload)
+    force_reload
+    ;;
+  status)
+    rh_status
+    ;;
+  condrestart|try-restart)
+    rh_status_q || exit 0
+    restart
+    ;;
+  *)
+    echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload}"
+    exit 2
+esac
+
+exit $?
diff --git a/wallace/wallaced.sysconfig b/wallace/wallaced.sysconfig
deleted file mode 100644
index 7832bdb..0000000
--- a/wallace/wallaced.sysconfig
+++ /dev/null
@@ -1,5 +0,0 @@
-# Configuration file for the Kolab Server daemon Wallace.
-#
-# See wallaced --help for more flags.
-#
-FLAGS="--fork -l warning"
diff --git a/wallace/wallaced.systemd b/wallace/wallaced.systemd
deleted file mode 100644
index 73c16d7..0000000
--- a/wallace/wallaced.systemd
+++ /dev/null
@@ -1,15 +0,0 @@
-[Unit]
-Description=Wallace Content Filter
-After=syslog.target network.target
-
-[Service]
-Type=forking
-PIDFile=/var/run/wallaced/wallaced.pid
-EnvironmentFile=/etc/sysconfig/wallace
-ExecStart=/usr/sbin/wallace $FLAGS
-ExecReload=/bin/kill -HUP $MAINPID
-ExecStop=/bin/kill -TERM $MAINPID
-
-[Install]
-WantedBy=multi-user.target
-
diff --git a/wallace/wallaced.sysvinit b/wallace/wallaced.sysvinit
deleted file mode 100644
index 3b545a9..0000000
--- a/wallace/wallaced.sysvinit
+++ /dev/null
@@ -1,108 +0,0 @@
-#! /bin/bash
-#
-# wallace  Start/Stop the Kolab server daemon Wallace
-#
-# chkconfig:        - 65 10
-# description:      The Kolab Wallace server daemon is a content filtering daemon.
-# processname: wallaced
-
-### BEGIN INIT INFO
-# Provides: wallaced
-# Required-Start: $local_fs $network
-# Required-Stop: $local_fs $network
-# Short-Description: Start/Stop the Kolab Server daemon
-# Description:      The Kolab Wallace server daemon is a content filtering daemon.
-### END INIT INFO
-
-# Source function library.
-. /etc/init.d/functions
-
-# Source our configuration file for these variables.
-FLAGS="--fork -l warning"
-
-if [ -f /etc/sysconfig/wallace ] ; then
-    . /etc/sysconfig/wallace
-fi
-
-RETVAL=0
-
-# Set up some common variables before we launch into what might be
-# considered boilerplate by now.
-prog=wallace
-path=/usr/sbin/wallaced
-lockfile=/var/lock/subsys/$prog
-pidfile=/var/run/wallace/wallaced.pid
-
-start() {
-    [ -x $path ] || exit 5
-    echo -n $"Starting $prog: "
-    daemon $DAEMONOPTS $path -p $pidfile $FLAGS
-    RETVAL=$?
-    echo
-    [ $RETVAL -eq 0 ] && touch $lockfile
-    return $RETVAL
-}
-
-stop() {
-    echo -n $"Stopping $prog: "
-    killproc $prog
-    RETVAL=$?
-    echo
-    [ $RETVAL -eq 0 ] && rm -f $lockfile
-    return $RETVAL
-}
-
-restart() {
-    stop
-    start
-}
-
-reload() {
-    restart
-}
-
-force_reload() {
-    restart
-}
-
-rh_status() {
-    # run checks to determine if the service is running or use generic status
-    status -p $pidfile $prog
-}
-
-rh_status_q() {
-    rh_status >/dev/null 2>&1
-}
-
-case "$1" in
-  start)
-    rh_status_q && exit 0
-    start
-    ;;
-  stop)
-    rh_status_q || exit 0
-    stop
-    ;;
-  restart)
-    restart
-    ;;
-  reload)
-    rh_status_q || exit 7
-    reload
-    ;;
-  force-reload)
-    force_reload
-    ;;
-  status)
-    rh_status
-    ;;
-  condrestart|try-restart)
-    rh_status_q || exit 0
-    restart
-    ;;
-  *)
-    echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload}"
-    exit 2
-esac
-
-exit $?


commit 77d3804884bd6fc13182ec26f8d2d447cda3be61
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Apr 19 17:00:20 2012 +0100

    Rebase on specification

diff --git a/conf/kolab.conf b/conf/kolab.conf
index 2a7888c..fcce25f 100644
--- a/conf/kolab.conf
+++ b/conf/kolab.conf
@@ -2,51 +2,40 @@
 primary_domain = kolabsys.com
 auth_mechanism = ldap
 imap_backend = cyrus-imap
-default_quota = 2097152
-virtual_domains = userid
-quota_attribute = mailquota
-unique_attribute = nsunique
-mailserver_attribute = mailhost
-mail_attributes = mail, alias
-auth_attributes = mail, alias, uid
 
 [ldap]
-ldap_uri = ldap://localhost
+ldap_uri = ldap://localhost:389
 base_dn = dc=kolabsys,dc=com
+bind_dn = cn=Directory Manager
+bind_pw = 0B2NFj581H8kZgO
 user_base_dn = ou=People,%(base_dn)s
-user_filter = (objectclass=inetorgperson)
 user_scope = sub
+user_filter = (objectclass=inetorgperson)
 kolab_user_filter = (objectclass=kolabinetorgperson)
 group_base_dn = ou=Groups,%(base_dn)s
+group_scope = sub
 group_filter = (|(objectclass=groupofuniquenames)(objectclass=groupofurls))
 kolab_group_filter = (|(objectclass=kolabgroupofuniquenames)(objectclass=kolabgroupofurls))
-bind_dn = cn=Directory Manager
-bind_pw = 55WX2YOgYmt8EO5
 domain_base_dn = cn=kolab,cn=config
 domain_filter = (&(associatedDomain=*))
 domain_name_attribute = associateddomain
 domain_rootdn_attribute = inetdomainbasedn
 service_bind_dn = uid=kolab-service,ou=Special Users,dc=kolabsys,dc=com
-service_bind_pw = hAIOAhP8qIUEPbB
+service_bind_pw = wc18bqshFmifGtN
+quota_attribute = mailquota
+unique_attribute = nsuniqueid
+mail_attributes = mail, alias
+auth_attributes = mail, alias, uid
 
 [kolab_smtp_access_policy]
-uri = mysql://user:pass@localhost/kolab
-retention = 3600
+cache_uri = mysql://user:pass@localhost/database
+cache_retention = 30
 address_search_attrs = mail, alias
 
-[kolab_telemetry]
-uri = mysql://user:pass@localhost/kolab
-retention = 30
-
-[kolab_wap]
-sql_uri = mysql://user:pass@localhost/kolab
-skin = default
-admin_auto_fields_rw = false
-
 [cyrus-imap]
 uri = imaps://localhost:993
 admin_login = cyrus-admin
-admin_password = 5xosOaTm_Kg8Ax5
+admin_password = xgbGH1xHSCFxPH2
 
 [cyrus-sasl]
 result_attribute = mail
diff --git a/pykolab/__init__.py b/pykolab/__init__.py
index 22bf88e..3f0b520 100644
--- a/pykolab/__init__.py
+++ b/pykolab/__init__.py
@@ -26,6 +26,7 @@
 import logging
 import shutil
 import sys
+import threading
 import traceback
 
 from pykolab.logger import Logger
@@ -44,10 +45,11 @@ from pykolab.conf import Conf
 conf = Conf()
 
 def getConf():
-    return conf
+    _data = threading.local()
+    if hasattr(_data, 'conf'):
+        log.debug(_("Returning thread local configuration"))
+        return _data.conf
 
-from pykolab.auth import Auth
-auth = Auth()
+    return conf
 
-from pykolab.imap import IMAP
-imap = IMAP()
+import base
diff --git a/pykolab/auth/__init__.py b/pykolab/auth/__init__.py
index 04fd578..5686e4b 100644
--- a/pykolab/auth/__init__.py
+++ b/pykolab/auth/__init__.py
@@ -21,28 +21,27 @@ import os
 import time
 
 import pykolab
+import pykolab.base
 
 from pykolab.translate import _
 
-conf = pykolab.getConf()
 log = pykolab.getLogger('pykolab.auth')
+conf = pykolab.getConf()
 
-class Auth(object):
+class Auth(pykolab.base.Base):
     """
         This is the Authentication and Authorization module for PyKolab.
     """
 
-    def __init__(self):
+    def __init__(self, domain):
         """
             Initialize the authentication class.
-
-            self._auth is the placeholder for domain-specific authentication
-            backends. The keys are the primary domain names for each domain.
         """
+        pykolab.base.Base.__init__(self)
+
         self._auth = None
 
-        # Placeholder mapping back to the primary domain name space
-        self.secondary_domains = {}
+        self.domain = domain
 
     def authenticate(self, login):
         """
@@ -70,6 +69,8 @@ class Auth(object):
 
         if len(login[0].split('@')) > 1:
             domain = login[0].split('@')[1]
+        elif len(login) >= 4:
+            domain = login[3]
         else:
             domain = conf.get("kolab", "primary_domain")
 
@@ -77,9 +78,7 @@ class Auth(object):
         if len(login) == 4:
             domain = login[3]
 
-        self.connect(domain)
-
-        retval = self._auth._authenticate(login, domain)
+        retval = self._auth.authenticate(login, domain)
 
         return retval
 
@@ -89,6 +88,7 @@ class Auth(object):
             back to the primary domain specified by the configuration.
         """
 
+        log.debug(_("Called for domain %r") % (domain))
         if not self._auth == None:
             return
 
@@ -118,16 +118,18 @@ class Auth(object):
 
         if conf.get(section, 'auth_mechanism') == 'ldap':
             from pykolab.auth import ldap
-            self._auth = ldap.LDAP()
+            self._auth = ldap.LDAP(self.domain)
         elif conf.get(section, 'auth_mechanism') == 'sql':
             from pykolab.auth import sql
-            self._auth = sql.SQL()
+            self._auth = sql.SQL(self.domain)
 
         else:
             from pykolab.auth import ldap
-            self._auth = ldap.LDAP()
+            self._auth = ldap.LDAP(self.domain)
+
+        self._auth.connect()
 
-    def disconnect(self, domain=None):
+    def disconnect(self):
         """
             Connect to the domain authentication backend using domain, or fall
             back to the primary domain specified by the configuration.
@@ -144,55 +146,16 @@ class Auth(object):
 
         self._auth._disconnect()
 
-    def find_group(self, attr, value, domain=None, **kw):
-        self.connect(domain)
-
-        if self.secondary_domains.has_key(domain):
-            log.debug(
-                    _("Using primary domain %s instead of secondary domain %s")
-                    % (
-                            self.secondary_domains[domain],
-                            domain
-                        ),
-                    level=9
-                )
-
-            domain = self.secondary_domains[domain]
-
-        return self._auth._find_group(attr, value, domain=domain, **kw)
-
-    def find_user(self, attr, value, domain=None, **kw):
-        self.connect(domain)
-
-        if self.secondary_domains.has_key(domain):
-            log.debug(
-                    _("Using primary domain %s instead of secondary domain %s")
-                    % (
-                            self.secondary_domains[domain],
-                            domain
-                        ),
-                    level=9
-                )
-
-            domain = self.secondary_domains[domain]
+    def find_recipient(self, address):
+        """
+            Find one or more entries corresponding to the recipient address.
+        """
+        return self._auth.find_recipient(address)
 
+    def find_user(self, attr, value, **kw):
         return self._auth._find_user(attr, value, domain=domain, **kw)
 
-    def search_users(self, attr, value, domain=None, **kw):
-        self.connect(domain)
-
-        if self.secondary_domains.has_key(domain):
-            log.debug(
-                    _("Using primary domain %s instead of secondary domain %s")
-                    % (
-                            self.secondary_domains[domain],
-                            domain
-                        ),
-                    level=9
-                )
-
-            domain = self.secondary_domains[domain]
-
+    def search_users(self, attr, value, **kw):
         return self._auth._search_users(attr, value, domain=domain, **kw)
 
     def list_domains(self):
@@ -225,76 +188,20 @@ class Auth(object):
 
         return domains
 
-    def list_users(self, primary_domain, secondary_domains=[], callback=None):
-        self.connect(domain=primary_domain)
-        users = self._auth._list_users(
-                primary_domain,
-                secondary_domains,
-                callback
-            )
-        self.disconnect(domain=primary_domain)
-        return users
-
-    def synchronize(self, primary_domain, secondary_domains=[]):
-        self.connect(domain=primary_domain)
-        self.list_users(
-                primary_domain,
-                secondary_domains,
-                callback=self._auth.sync_user
-            )
+    def synchronize(self):
+        self._auth.synchronize()
 
     def domain_default_quota(self, domain):
-        self.connect(domain=domain)
-
-        if self.secondary_domains.has_key(domain):
-            domain = self.secondary_domains[domain]
-
         return self._auth._domain_default_quota(domain)
 
-    def domain_section(self, domain):
-        self.connect(domain=domain)
-
-        if self.secondary_domains.has_key(domain):
-            domain = self.secondary_domains[domain]
-
-        return self._auth._domain_section(domain)
-
-    def get_group_attribute(self, domain, group, attribute):
-        self.connect(domain=domain)
-
-        if self.secondary_domains.has_key(domain):
-            domain = self.secondary_domains[domain]
-
-        return self._auth._get_group_attribute(group, attribute)
-
     def get_user_attribute(self, domain, user, attribute):
-        self.connect(domain=domain)
-
-        if self.secondary_domains.has_key(domain):
-            domain = self.secondary_domains[domain]
-
         return self._auth._get_user_attribute(user, attribute)
 
     def get_user_attributes(self, domain, user, attributes):
-        self.connect(domain=domain)
-
-        if self.secondary_domains.has_key(domain):
-            domain = self.secondary_domains[domain]
-
         return self._auth._get_user_attributes(user, attributes)
 
     def search_mail_address(self, domain, mail_address):
-        self.connect(domain=domain)
-
-        if self.secondary_domains.has_key(domain):
-            domain = self.secondary_domains[domain]
-
         return self._auth._search_mail_address(domain, mail_address)
 
     def set_user_attribute(self, domain, user, attribute, value):
-        self.connect(domain=domain)
-
-        if self.secondary_domains.has_key(domain):
-            domain = self.secondary_domains[domain]
-
         self._auth._set_user_attribute(user, attribute, value)
diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index f8d4cdd..17e6533 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -16,6 +16,7 @@
 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 #
 
+import datetime
 import _ldap
 import ldap
 import ldap.async
@@ -24,6 +25,7 @@ import logging
 import time
 
 import pykolab
+import pykolab.base
 
 from pykolab import utils
 from pykolab.constants import *
@@ -95,22 +97,56 @@ class SimplePagedResultsControl(ldap.controls.SimplePagedResultsControl):
         else:
             return self.controlValue[0]
 
-class LDAP(object):
+class LDAP(pykolab.base.Base):
     """
         Abstraction layer for the LDAP authentication / authorization backend,
         for use with Kolab.
     """
 
-    def __init__(self):
+    def __init__(self, domain=None):
+        """
+            Initialize the LDAP object for domain. If no domain is specified,
+            domain name space configured as 'kolab'.'primary_domain' is used.
+        """
+        pykolab.base.Base.__init__(self)
+
         self.ldap = None
         self.bind = False
+        if domain == None:
+            self.domain = conf.get('kolab', 'primary_domain')
+        else:
+            self.domain = domain
+
+    def authenticate(self, login, realm):
+        """
+            Find the entry corresponding to login, and attempt a bind.
+
+            login is a tuple with 4 values. In order of appearance;
+
+            [0] - the login username.
+
+            [1] - the password
+
+            [2] - the service (optional)
+
+            [3] - the realm
+
+            Called from pykolab.auth.Auth, the realm parameter is derived, while
+            login[3] preserves the originally specified realm.
+        """
+
+        log.debug(
+                _("Attempting to authenticate user %s in realm %s") % (
+                        login,
+                        realm
+                    ),
+                level=8
+            )
+
+        self.connect()
 
-    def _authenticate(self, login, domain):
-        log.debug(_("Attempting to authenticate user %s in domain %s")
-            % (login, domain), level=8)
+        user_dn = self._find_user_dn(login[0], realm)
 
-        self._connect()
-        user_dn = self._find_dn(login[0], domain)
         try:
             log.debug(_("Binding with user_dn %s and password %s")
                 % (user_dn, login[1]))
@@ -129,31 +165,16 @@ class LDAP(object):
 
         return retval
 
-    def _connect(self, domain=None):
+    def connect(self):
         """
             Connect to the LDAP server through the uri configured.
-
-            Pass it a configuration section name to get the ldap options from
-            that section instead of the default [ldap] section.
         """
         if not self.ldap == None:
             return
 
-        if domain == None:
-            section = 'ldap'
-            key = 'ldap_uri'
-
-        if conf.has_option(domain, 'uri'):
-            log.warning(_("Deprecation: Setting 'uri' for LDAP in section %s needs to be updated to 'ldap_uri'") % (domain))
-            section = domain
-            key = 'uri'
-        elif conf.has_option(domain, 'ldap_uri'):
-            section = domain
-            key = 'ldap_uri'
-
         log.debug(_("Connecting to LDAP..."), level=8)
 
-        uri = conf.get(section, key)
+        uri = self.config_get('ldap_uri')
 
         log.debug(_("Attempting to use LDAP URI %s") % (uri), level=8)
 
@@ -166,906 +187,929 @@ class LDAP(object):
         self.ldap.protocol_version = 3
         self.ldap.supported_controls = []
 
-    def _bind(self):
-        # TODO: Implement some mechanism for r/o, r/w and mgmt binding.
-        self._connect()
+    def entry_dn(self, entry_id):
+        """
+            Get a entry's distinguished name for an entry ID.
 
-        if not self.bind:
-            # TODO: Use bind credentials for the domain itself.
-            bind_dn = conf.get('ldap', 'bind_dn')
-            bind_pw = conf.get('ldap', 'bind_pw')
+            The entry ID may be any of:
 
-            # TODO: Binding errors control
-            try:
-                self.ldap.simple_bind_s(bind_dn, bind_pw)
-                self.bind = True
-            except ldap.SERVER_DOWN:
-                # Can't contact LDAP server
-                #
-                # - Service not started
-                # - Service faulty
-                # - Firewall
-                pass
-            except ldap.INVALID_CREDENTIALS:
-                log.error(_("Invalid bind credentials"))
+            - an entry's value for the configured unique_attribute,
+            - a (syntactically valid) Distinguished Name,
+            - a dictionary such as previously returned as (part of) the result
+              of a search.
+        """
+        entry_dn = None
 
-    def _unbind(self):
-        self.ldap.unbind()
-        self.bind = False
+        if self._entry_dn(entry_id):
+            return entry_id
 
-    def _reconnect(self):
-        self._disconnect()
-        self._connect()
+        if self._entry_dict(entry_id):
+            return entry_id['dn']
 
-    def _disconnect(self):
-        self._unbind()
-        del self.ldap
-        self.ldap = None
-        self.bind = False
+        unique_attribute = self.config_get('unique_attribute')
+        base_dn = self.config_get('base_dn')
+
+        _search = self.ldap.search_ext(
+                base_dn,
+                ldap.SCOPE_SUBTREE,
+                '(%s=%s)' % (unique_attribute, entry_id),
+                ['entrydn']
+            )
+
+        (
+                _result_type,
+                _result_data,
+                _result_msgid,
+                _result_controls
+            ) = self.ldap.result3(_search)
+
+        if len(_result_data) >= 1:
+            (entry_dn, entry_attrs) = _result_data[0]
+
+        return entry_dn
 
-    def _find_dn(self, login, domain=None):
+    def get_entry_attribute(self, entry_id, attribute):
         """
-            Find the distinguished name (DN) for an entry in LDAP.
+            Get an attribute for an entry.
 
+            Return the attribute value if successful, or None if not.
         """
-        self._connect()
-        self._bind()
 
-        if domain == None:
-            domain = conf.get('kolab', 'primary_domain')
+        entry_attrs = self.get_entry_attributes(entry_id, [attribute])
+
+        if entry_attrs.has_key(attribute):
+            return entry_attrs[attribute]
+        else:
+            return None
+
+    def get_entry_attributes(self, entry_id, attributes):
+        """
+            Get multiple attributes for an entry.
+        """
+
+        entry_dn = self.entry_dn(entry_id)
+
+        _search = self.ldap.search_ext(
+                entry_dn,
+                ldap.SCOPE_BASE,
+                '(objectclass=*)',
+                [ 'dn' ] + attributes
+            )
 
-        domain_root_dn = self._kolab_domain_root_dn(domain)
+        (
+                _result_type,
+                _result_data,
+                _result_msgid,
+                _result_controls
+            ) = self.ldap.result3(_search)
 
-        if conf.has_option(domain, 'base_dn'):
-            section = domain
+        if len(_result_data) >= 1:
+            (_entry_dn, _entry_attrs) = _result_data[0]
         else:
-            section = 'ldap'
+            return None
+
+        return utils.normalize(_entry_attrs)
+
+    def find_recipient(self, address="*", exclude_entry_id=None):
+        """
+            Given an address string or list of addresses, find one or more valid
+            recipients.
 
-        user_base_dn = conf.get_raw(section, 'base_dn')
+            Use this function only to detect whether an address is already in
+            use by any entry in the tree.
+
+            Specify an additional entry_id to exclude to exclude matches against
+            the current entry.
+        """
+
+        if not exclude_entry_id == None:
+            __filter_prefix = "(&"
+            __filter_suffix = "(!(%s=%s)))" % (
+                    self.config_get('unique_attribute'),
+                    exclude_entry_id
+                )
 
-        if conf.has_option(domain, 'user_filter'):
-            section = domain
         else:
-            section = 'ldap'
-
-        user_filter = conf.get(section, 'user_filter', quiet=True)
-
-        if user_filter == "":
-            user_filter = conf.get('ldap', 'user_filter')
-
-        if conf.has_option(section, 'auth_attrs'):
-            auth_search_attrs = conf.get_list(section, 'auth_attrs')
-        elif conf.has_section('kolab_smtp_access_policy'):
-            if conf.has_option('kolab_smtp_access_policy', 'address_search_attrs'):
-                log.warning(
-                        _("Deprecation warning: The setting " + \
-                            "kolab_smtp_access_policy.address_search_attrs " + \
-                            "is to be replaced with the 'auth_attrs' key in " + \
-                            "the 'ldap' or '%s' domain section.") % (domain)
-                    )
+            __filter_prefix = ""
+            __filter_suffix = ""
 
-                auth_search_attrs = conf.get_list(
-                        'kolab_smtp_access_policy',
-                        'address_search_attrs'
-                    )
+        kolab_filter = self._kolab_filter()
+        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:
-                auth_search_attrs = [ 'uid', 'mail' ]
-        else:
-            auth_search_attrs = [ 'uid', 'mail' ]
+                for _address in address:
+                    _filter += "(%s=%s)" % (recipient_address_attr, _address)
 
-        if not 'uid' in auth_search_attrs:
-            auth_search_attrs.append('uid')
+        _filter += ")"
 
-        auth_search_filter = [ '(|' ]
+        _filter = "%s%s%s" % (__filter_prefix,_filter,__filter_suffix)
 
-        for auth_search_attr in auth_search_attrs:
-            auth_search_filter.append('(%s=%s)' % (auth_search_attr,login))
-            auth_search_filter.append('(%s=%s@%s)' % (auth_search_attr,login,domain))
 
-        auth_search_filter.append(')')
+        log.debug(_("Finding recipient with filter %r") % (_filter), level=8)
 
-        auth_search_filter = ''.join(auth_search_filter)
+        if len(_filter) <= 6:
+            return None
 
-        search_filter = "(&%s%s)" % (
-                auth_search_filter,
-                user_filter
+        _results = self.ldap.search_s(
+                self.config_get('base_dn'),
+                scope=ldap.SCOPE_SUBTREE,
+                filterstr=_filter,
+                attrlist=result_attributes,
+                attrsonly=True
             )
 
-        _results = self._search(
-                user_base_dn,
-                filterstr=search_filter,
-                attrlist=[ 'dn' ],
-                override_search='_regular_search'
-            )
+        _entry_dns = []
 
-        if len(_results) == 1:
-            (_user_dn, _user_attrs) = _results[0]
-        else:
-            # Retry to find the user_dn with just uid=%s against the root_dn,
-            # if the login is not fully qualified
-            if len(login.split('@')) < 2:
-                search_filter = "(uid=%s)" % (login)
-                _results = self._search(
-                        domain,
-                        filterstr=search_filter,
-                        attrlist=[ 'dn' ]
-                    )
+        for _result in _results:
+            (_entry_id, _entry_attrs) = _result
+            _entry_dns.append(_entry_id)
 
-                if len(_results) == 1:
-                    (_user_dn, _user_attrs) = _results[0]
-                else:
-                    # Overall fail
-                    return False
+        return _entry_dns
+
+    def list_secondary_domains(self):
+        """
+            List alias domain name spaces for the current domain name space.
+        """
+        return [s for s, p in self.secondary_domains.iteritems() if p == self.domain]
+
+    def recipient_policy(self, entry):
+        """
+            Apply a recipient policy, if configured.
+
+            Given an entry, returns the entry's attribute values to be set.
+        """
+
+        entry_dn = self.entry_dn(entry)
+        entry_modifications = {}
+        entry_type = self._entry_type(entry)
+        primary_mail = None
+        primary_mail_attribute = self.config_get_list('mail_attributes')[0]
+        secondary_mail = None
+        secondary_mail_attribute = self.config_get_list('mail_attributes')[1]
+        want_attrs = []
+
+        # See which mail attributes we would want to control.
+        #
+        # 'mail' is considered for primary_mail,
+        # 'alias' and 'mailalternateaddress' are considered for secondary mail.
+        #
+        primary_mail = self.config_get_raw('%s_primary_mail' % (entry_type))
+        if primary_mail == None and entry_type == 'user':
+            primary_mail = self.config_get_raw('primary_mail')
+
+        secondary_mail = self.config_get_raw('%s_secondary_mail' % (entry_type))
+        if secondary_mail == None and entry_type == 'user':
+            secondary_mail = self.config_get_raw('secondary_mail')
+
+        # See if the relevant mail attributes exist
+        _mail_attrs = self.config_get('mail_attributes')
+        for _mail_attr in _mail_attrs:
+            if not entry.has_key(_mail_attr):
+                if _mail_attr == primary_mail_attribute:
+                    if not primary_mail == None:
+                        want_attrs.append(_mail_attr)
+                elif _mail_attr == secondary_mail_attribute:
+                    if not secondary_mail == None:
+                        want_attrs.append(_mail_attr)
+
+        # Also append the preferredlanguage or 'native tongue' configured
+        # for the entry.
+        if not entry.has_key('preferredlanguage'):
+            want_attrs.append('preferredlanguage')
+
+        # If we wanted anything, now is the type to get it.
+        if len(want_attrs) > 0:
+            attributes = self.get_entry_attributes(entry_dn, want_attrs)
+
+            for attribute in attributes.keys():
+                entry[attribute] = attributes[attribute]
+
+        # Primary mail address
+        if not primary_mail == None:
+            if not entry.has_key(primary_mail_attribute) or entry[primary_mail_attribute] == None:
+                primary_mail_address = conf.plugins.exec_hook(
+                        "set_primary_mail",
+                        kw={
+                                'primary_mail': primary_mail,
+                                'entry': entry,
+                                'primary_domain': self.domain
+                            }
+                    )
             else:
-                return False
+                primary_mail_address = entry[primary_mail_attribute]
 
-        return _user_dn
+            i = 1
+            _primary_mail = primary_mail_address
 
-    def _find_group(self, attr, value, domain=None, additional_filter=None, base_dn=None):
-        self._connect()
-        self._bind()
+            done = False
+            while not done:
+                results = self.find_recipient(_primary_mail, entry['id'])
 
-        if domain == None:
-            domain = conf.get('kolab', 'primary_domain')
+                # Length of results should be 0 (no entry found)
+                # or 1 (which should be the entry we're looking at here)
+                if len(results) == 0:
+                    log.debug(
+                            _("No results for mail address %s found") % (
+                                    _primary_mail
+                                ),
+                            level=8
+                        )
 
-        domain_root_dn = self._kolab_domain_root_dn(domain)
+                    done = True
+                    continue
 
-        if conf.has_option(domain, 'group_base_dn'):
-            section = domain
-        else:
-            section = 'ldap'
+                if len(results) == 1:
+                    log.debug(
+                            _("1 result for address %s found, verifying") % (
+                                    _primary_mail
+                                ),
+                            level=8
+                        )
 
-        if base_dn == None:
-            group_base_dn = conf.get_raw(
-                    section,
-                    'group_base_dn'
-                ) % ({'base_dn': domain_root_dn})
-        else:
-            group_base_dn = base_dn
+                    almost_done = True
+                    for result in results:
+                        if not result == entry_dn:
+                            log.debug(
+                                    _("Too bad, primary email address %s " + \
+                                    "already in use for %s (we are %s)") % (
+                                            _primary_mail,
+                                            result,
+                                            entry_dn
+                                        ),
+                                    level=8
+                                )
 
-        if type(attr) == str:
-            search_filter = "(%s=%s)" % (
-                    attr,
-                    value
-                )
-        elif type(attr) == list:
-            search_filter = "(|"
-            for _attr in attr:
-                search_filter = "%s(%s=%s)" % (search_filter, _attr, value)
-            search_filter = "%s)" % (search_filter)
+                            almost_done = False
+                        else:
+                            log.debug(_("Address assigned to us"), level=8)
 
-        if additional_filter:
-            search_filter = additional_filter % {
-                    'search_filter': search_filter
-                }
+                    if almost_done:
+                        done = True
+                        continue
 
-        log.debug(
-                _("Attempting to find the group with search filter: %s") % (
-                        search_filter
-                    ),
-                level=8
-            )
+                i += 1
+                _primary_mail = "%s%d@%s" % (
+                        primary_mail_address.split('@')[0],
+                        i,
+                        primary_mail_address.split('@')[1]
+                    )
 
-        _results = self.ldap.search_s(
-                group_base_dn,
-                scope=ldap.SCOPE_SUBTREE,
-                filterstr=search_filter,
-                attrlist=[ 'dn' ]
-            )
+            primary_mail_address = _primary_mail
 
-        if len(_results) == 1:
-            (_group_dn, _group_attrs) = _results[0]
-        else:
-            return False
+            ###
+            ### FIXME
+            ###
+            if not primary_mail_address == None:
+                if not entry.has_key(primary_mail_attribute):
+                    self.set_entry_attribute(entry, primary_mail_attribute, primary_mail_address)
+                    entry_modifications[primary_mail_attribute] = primary_mail_address
+                else:
+                    if not primary_mail_address == entry[primary_mail_attribute]:
+                        self.set_entry_attribute(entry, primary_mail_attribute, primary_mail_address)
 
-        return _group_dn
+                        entry_modifications[primary_mail_attribute] = primary_mail_address
 
-    def _find_user(self, attr, value, domain=None, additional_filter=None, base_dn=None):
-        self._connect()
-        self._bind()
+        if not secondary_mail == None:
+            # Execute the plugin hook
+            suggested_secondary_mail = conf.plugins.exec_hook(
+                    "set_secondary_mail",
+                    kw={
+                            'secondary_mail': secondary_mail,
+                            'entry': entry,
+                            'domain': self.domain,
+                            'primary_domain': self.domain,
+                            'secondary_domains': self.list_secondary_domains()
+                        }
+                ) # end of conf.plugins.exec_hook() call
 
-        if domain == None:
-            domain = conf.get('kolab', 'primary_domain')
+            secondary_mail_addresses = []
 
-        domain_root_dn = self._kolab_domain_root_dn(domain)
+            for _secondary_mail in suggested_secondary_mail:
+                i = 1
+                __secondary_mail = _secondary_mail
 
-        if conf.has_option(domain, 'user_base_dn'):
-            section = domain
-        else:
-            section = 'ldap'
+                done = False
+                while not done:
+                    results = self.find_recipient(__secondary_mail, entry['id'])
 
-        if base_dn == None:
-            user_base_dn = conf.get_raw(
-                    section,
-                    'user_base_dn'
-                ) % ({'base_dn': domain_root_dn})
-        else:
-            user_base_dn = base_dn
+                    # Length of results should be 0 (no entry found)
+                    # or 1 (which should be the entry we're looking at here)
+                    if len(results) == 0:
+                        log.debug(
+                                _("No results for address %s found") % (
+                                        __secondary_mail
+                                    ),
+                                level=8
+                            )
 
-        if type(attr) == str:
-            search_filter = "(%s=%s)" % (
-                    attr,
-                    value
-                )
-        elif type(attr) == list:
-            search_filter = "(|"
-            for _attr in attr:
-                search_filter = "%s(%s=%s)" % (search_filter, _attr, value)
-            search_filter = "%s)" % (search_filter)
+                        done = True
+                        continue
 
-        if additional_filter:
-            search_filter = additional_filter % {
-                    'search_filter': search_filter
-                }
+                    if len(results) == 1:
+                        log.debug(
+                                _("1 result for address %s found, " + \
+                                "verifying...") % (
+                                        __secondary_mail
+                                    ),
+                                level=8
+                            )
 
-        log.debug(
-                _("Attempting to find the user with search filter: %s") % (
-                        search_filter
-                    ),
-                level=8
-            )
+                        almost_done = True
+                        for result in results:
+                            if not result == entry_dn:
+                                log.debug(
+                                        _("Too bad, secondary email " + \
+                                        "address %s already in use for " + \
+                                        "%s (we are %s)") % (
+                                                __secondary_mail,
+                                                result,
+                                                entry_dn
+                                            ),
+                                        level=8
+                                    )
+
+                                almost_done = False
+                            else:
+                                log.debug(_("Address assigned to us"), level=8)
+
+                        if almost_done:
+                            done = True
+                            continue
 
-        _results = self.ldap.search_s(
-                user_base_dn,
-                scope=ldap.SCOPE_SUBTREE,
-                filterstr=search_filter,
-                attrlist=[ 'dn' ]
-            )
+                    i += 1
+                    __secondary_mail = "%s%d@%s" % (
+                            _secondary_mail.split('@')[0],
+                            i,
+                            _secondary_mail.split('@')[1]
+                        )
 
-        if len(_results) == 1:
-            (_user_dn, _user_attrs) = _results[0]
-        else:
-            return False
+                secondary_mail_addresses.append(__secondary_mail)
 
-        return _user_dn
+            if not secondary_mail_addresses == None:
+                secondary_mail_addresses = list(set(secondary_mail_addresses))
+                # Avoid duplicates
+                while primary_mail in secondary_mail_addresses:
+                    secondary_mail_addresses.pop(secondary_mail_addresses.index(primary_mail))
 
-    def _search_users(self, attr, value, domain=None, additional_filter=None, base_dn=None):
-        self._connect()
-        self._bind()
+                if not entry.has_key(secondary_mail_attribute):
+                    if not len(secondary_mail_addresses) == 0:
+                        self.set_entry_attribute(
+                                entry,
+                                secondary_mail_attribute,
+                                secondary_mail_addresses
+                            )
 
-        if domain == None:
-            domain = conf.get('kolab', 'primary_domain')
+                        entry_modifications[secondary_mail_attribute] = secondary_mail_addresses
+                else:
+                    if not secondary_mail_addresses == entry[secondary_mail_attribute]:
+                        self.set_entry_attribute(
+                                entry,
+                                secondary_mail_attribute,
+                                secondary_mail_addresses
+                            )
 
-        domain_root_dn = self._kolab_domain_root_dn(domain)
+                        entry_modifications[secondary_mail_attribute] = secondary_mail_addresses
 
-        if conf.has_option(domain, 'user_base_dn'):
-            section = domain
-        else:
-            section = 'ldap'
+        return entry_modifications
 
-        if base_dn == None:
-            user_base_dn = conf.get_raw(
-                    section,
-                    'user_base_dn'
-                ) % ({'base_dn': domain_root_dn})
-        else:
-            user_base_dn = base_dn
+    def set_entry_attribute(self, entry_id, attribute, value):
+        self.set_entry_attributes(entry_id, { attribute: value })
 
-        if type(attr) == str:
-            search_filter = "(%s=%s)" % (
-                    attr,
-                    value
-                )
-        elif type(attr) == list:
-            search_filter = "(|"
-            for _attr in attr:
-                search_filter = "%s(%s=%s)" % (search_filter, _attr, value)
-            search_filter = "%s)" % (search_filter)
+    def set_entry_attributes(self, entry_id, attributes):
+        self._bind()
 
-        if additional_filter:
-            search_filter = additional_filter % {
-                    'search_filter': search_filter
-                }
+        entry_dn = self.entry_dn(entry_id)
 
-        log.debug(
-                _("Attempting to find entries with search filter: %s") % (
-                        search_filter
-                    ),
-                level=8
-            )
+        print entry_dn
 
-        _results = self.ldap.search_s(
-                user_base_dn,
-                scope=ldap.SCOPE_SUBTREE,
-                filterstr=search_filter,
-                attrlist=[ 'dn' ]
-            )
+        attrs = {}
+        for attribute in attributes.keys():
+            attrs[attribute.lower()] = attributes[attribute]
 
-        _user_dns = []
+    def synchronize(self):
+        """
+            Synchronize with LDAP
+        """
+        self._bind()
 
-        for _result in _results:
-            (_user_dn, _user_attrs) = _result
-            _user_dns.append(_user_dn)
+        _filter = self._kolab_filter()
 
-        return _user_dns
+        modified_after = datetime.datetime.utcfromtimestamp(time.time()-18600).strftime("%Y%m%d%H%M%SZ")
+        _filter = "(&%s(modifytimestamp>=%s))" % (_filter,modified_after)
 
-    def _persistent_search(self,
-            base_dn,
-            scope=ldap.SCOPE_SUBTREE,
-            filterstr="(objectClass=*)",
-            attrlist=None,
-            attrsonly=0,
-            timeout=-1,
-            callback=False,
-            primary_domain=None,
-            secondary_domains=[]
-        ):
-        _results = []
-
-        psearch_server_controls = []
+        log.debug(_("Using filter %r") % (_filter), level=8)
 
-        psearch_server_controls.append(psearch.PersistentSearchControl(
-                    criticality=True,
-                    changeTypes=[ 'add', 'delete', 'modify', 'modDN' ],
-                    changesOnly=False,
-                    returnECs=True
-                )
+        self._search(
+                self.config_get('base_dn'),
+                filterstr=_filter,
+                attrlist=[ '*', self.config_get('unique_attribute') ],
+                callback=self._synchronize_callback,
             )
 
-        _search = self.ldap.search_ext(
-                base_dn,
-                scope=scope,
-                filterstr=filterstr,
-                attrlist=attrlist,
-                attrsonly=attrsonly,
-                timeout=timeout,
-                serverctrls=psearch_server_controls
-            )
+    ###
+    ### API depth level increasing!
+    ###
 
-        ecnc = psearch.EntryChangeNotificationControl
+    def _bind(self):
+        if self.ldap == None:
+            self.connect()
 
-        while True:
-            res_type,res_data,res_msgid,_None,_None,_None = self.ldap.result4(
-                    _search,
-                    all=0,
-                    add_ctrls=1,
-                    add_intermediates=1,
-                    resp_ctrl_classes={ecnc.controlType:ecnc}
-                )
+        if not self.bind:
+            bind_dn = self.config_get('bind_dn')
+            bind_pw = self.config_get('bind_pw')
 
-            change_type = None
-            previous_dn = None
-            change_number = None
+            # TODO: Binding errors control
+            try:
+                self.ldap.simple_bind_s(bind_dn, bind_pw)
+                self.bind = True
+            except ldap.SERVER_DOWN:
+                # Can't contact LDAP server
+                #
+                # - Service not started
+                # - Service faulty
+                # - Firewall
+                pass
+            except ldap.INVALID_CREDENTIALS:
+                log.error(_("Invalid bind credentials"))
 
-            for dn,entry,srv_ctrls in res_data:
-                log.debug(_("LDAP Search Result Data Entry:"), level=8)
-                log.debug("    DN: %r" % (dn), level=8)
-                log.debug("    Entry: %r" % (entry), level=8)
+    def _change_add_user(self, entry, change):
+        """
+            An entry of type user was added.
+        """
+        mailserver_attribute = self.config_get('mailserver_attribute')
+        result_attribute = conf.get('cyrus-sasl', 'result_attribute')
 
-                ecn_ctrls = [
-                        c for c in srv_ctrls
-                        if c.controlType == ecnc.controlType
-                    ]
+        if not entry.has_key(mailserver_attribute):
+            entry[mailserver_attribute] = \
+                self.get_entry_attribute(entry, mailserver_attribute)
 
-                if ecn_ctrls:
-                    change_type = ecn_ctrls[0].changeType
-                    previous_dn = ecn_ctrls[0].previousDN
-                    change_number = ecn_ctrls[0].changeNumber
-                    change_type_desc = psearch.CHANGE_TYPES_STR[change_type]
+        if not entry.has_key(result_attribute):
+            entry = self.recipient_policy(entry)
+            return
 
-                    log.debug(
-                            _("Entry Change Notification attributes:"),
-                            level=8
-                        )
+        if entry[result_attribute] == None:
+            return
 
-                    log.debug(
-                            "    " + _("Change Type: %r (%r)") % (
-                                    change_type,
-                                    change_type_desc
-                                ),
-                            level=8
-                        )
+        self.imap.connect(domain=self.domain)
 
-                    log.debug(
-                            "    " + _("Previous DN: %r") % (previous_dn),
-                            level=8
-                        )
+        if not self.imap.user_mailbox_exists(entry[result_attribute]):
+            folder = self.imap.user_mailbox_create(
+                    entry[result_attribute],
+                    entry[mailserver_attribute]
+                )
+        else:
+            folder = "user%s%s" % (self.imap.separator,entry[result_attribute])
 
-                if callback:
-                    callback(
-                            dn=dn,
-                            entry=entry,
-                            previous_dn=previous_dn,
-                            change_type=change_type,
-                            change_number=change_number,
-                            primary_domain=primary_domain,
-                            secondary_domains=secondary_domains
-                        )
+        server = self.imap.user_mailbox_server(folder)
 
-    def _paged_search(self,
-            base_dn,
-            scope=ldap.SCOPE_SUBTREE,
-            filterstr="(objectClass=*)",
-            attrlist=None,
-            attrsonly=0,
-            timeout=-1,
-            callback=False,
-            primary_domain=None,
-            secondary_domains=[]
-        ):
+        if not entry[mailserver_attribute] == server:
+            self.set_entry_attribute(entry, mailserver_attribute, server)
 
-        page_size = 500
-        critical = True
-        _results = []
+    def _change_add_group(self, entry, change):
+        """
+            An entry of type group was added.
 
-        server_page_control = SimplePagedResultsControl(page_size=page_size)
+            The Kolab daemon has little to do for this type of action on this
+            type of entry.
+        """
+        pass
 
-        _search = self.ldap.search_ext(
-                base_dn,
-                scope=scope,
-                filterstr=filterstr,
-                attrlist=attrlist,
-                attrsonly=attrsonly,
-                serverctrls=[server_page_control]
-            )
+    def _change_add_role(self, entry, change):
+        """
+            An entry of type role was added.
 
-        pages = 0
-        while True:
-            pages += 1
-            try:
-                (
-                        _result_type,
-                        _result_data,
-                        _result_msgid,
-                        _result_controls
-                    ) = self.ldap.result3(_search)
+            The Kolab daemon has little to do for this type of action on this
+            type of entry.
+        """
+        pass
 
-            except ldap.NO_SUCH_OBJECT, e:
-                log.warning(_("Object %s searched no longer exists") % (base_dn))
-                break
+    def _change_add_sharedfolder(self, entry, change):
+        """
+            An entry of type sharedfolder was added.
+        """
+        self.imap.connect(domain=self.domain)
 
-            if callback:
-                callback(
-                        user=_result_data,
-                        primary_domain=primary_domain,
-                        secondary_domains=secondary_domains
-                    )
+        server = None
+        mailserver_attribute = self.config_get('mailserver_attribute')
 
-            _results.extend(_result_data)
-            if (pages % 2) == 0:
-                log.debug(_("%d results...") % (len(_results)))
+        if entry.has_key(mailserver_attribute):
+            server = entry['mailserver_attribute']
 
-            pctrls = [
-                    c for c in _result_controls
-                        if c.controlType == LDAP_CONTROL_PAGED_RESULTS
-                ]
+        if not entry.has_key('kolabtargetfolder'):
+            entry['kolabtargetfolder'] = self.get_entry_attribute(
+                    entry['id'],
+                    'kolabtargetfolder'
+                )
 
-            if pctrls:
-                size = pctrls[0].size
-                cookie = pctrls[0].cookie
-                if cookie:
-                    server_page_control.cookie = cookie
-                    _search = self.ldap.search_ext(
-                            base_dn,
-                            scope=scope,
-                            filterstr=filterstr,
-                            attrlist=attrlist,
-                            attrsonly=attrsonly,
-                            serverctrls=[server_page_control]
-                        )
-                else:
-                    # TODO: Error out more verbose
-                    break
-            else:
-                # TODO: Error out more verbose
-                print "Warning:  Server ignores RFC 2696 control."
-                break
+        if not entry.has_key('kolabfoldertype'):
+            entry['kolabfoldertype'] = self.get_entry_attribute(
+                    entry['id'],
+                    'kolabfoldertype'
+                )
 
-        return _results
+        #if not entry.has_key('kolabmailfolderaclentry'):
+            #entry['kolabmailfolderaclentry'] = self.get_entry_attribute(
+                    #entry['id'],
+                    #'kolabmailfolderaclentry'
+                #)
 
-    def _vlv_search(self,
-            base_dn,
-            scope=ldap.SCOPE_SUBTREE,
-            filterstr="(objectClass=*)",
-            attrlist=None,
-            attrsonly=0,
-            timeout=-1,
-            callback=False,
-            primary_domain=None,
-            secondary_domains=[]
-        ):
-        pass
+        if entry.has_key('kolabtargetfolder') and not entry['kolabtargetfolder'] == None:
+            folder_path = entry['kolabtargetfolder']
+        else:
+            # TODO: What is *the* way to see if we need to create an @domain
+            # shared mailbox?
+            # TODO^2: self.domain, really? Presumes any mail attribute is
+            # set to the primary domain name space...
+            # TODO^3: Test if the cn is already something at domain
+            result_attribute = conf.get('cyrus-sasl', 'result_attribute')
+            if result_attribute in ['mail']:
+                folder_path = "%s@%s" % (entry['cn'], self.domain)
+            else:
+                folder_path = entry['cn']
 
-    def _sync_repl(self,
-            base_dn,
-            scope=ldap.SCOPE_SUBTREE,
-            filterstr="(objectClass=*)",
-            attrlist=None,
-            attrsonly=0,
-            timeout=-1,
-            callback=False,
-            primary_domain=None,
-            secondary_domains=[]
-        ):
+        if not self.imap.shared_folder_exists(folder_path):
+            self.imap.shared_folder_create(folder_path, server)
 
-        import syncrepl
+        if entry.has_key('kolabfoldertype') and not entry['kolabfoldertype'] == None:
+            self.imap.shared_folder_set_type(folder_path, entry['kolabfoldertype'])
 
-        ldap_sync_conn = syncrepl.DNSync(
-                '/var/lib/pykolab/syncrepl.db',
-                ldap_url.initializeUrl(),
-                trace_level=ldapmodule_trace_level,
-                trace_file=ldapmodule_trace_file
-            )
+        #if entry.has_key('kolabmailfolderaclentry') and not entry['kolabmailfolderaclentry'] == None:
+            #self.imap._set_kolab_mailfolder_acls(entry['kolabmailfolderaclentry'])
 
-        msgid = ldap_sync_conn.syncrepl_search(
-                base_dn,
-                scope,
-                mode='refreshAndPersist',
-                filterstr=filterstr
-            )
+        #if server == None:
+            #self.entry_set_attribute(mailserver_attribute, server)
 
-        try:
-            # Here's where returns need to be taken into account...
-            while ldap_sync_conn.syncrepl_poll(all=1, msgid=msgid):
-                pass
-        except KeyboardInterrupt:
-            pass
+    def _change_delete_group(self, entry, change):
+        """
+            An entry of type group was deleted.
+        """
 
-    def _regular_search(self,
-            base_dn,
-            scope=ldap.SCOPE_SUBTREE,
-            filterstr="(objectClass=*)",
-            attrlist=None,
-            attrsonly=0,
-            timeout=-1,
-            callback=False,
-            primary_domain=None,
-            secondary_domains=[]
-        ):
-        self._connect()
-        self._bind()
+        result_attribute = conf.get('cyrus-sasl', 'result_attribute')
 
-        _search = self.ldap.search(
-                base_dn,
-                scope=scope,
-                filterstr=filterstr,
-                attrlist=attrlist,
-                attrsonly=attrsonly
-            )
+        if not entry.has_key(result_attribute):
+            return None
 
-        _results = []
-        _result_type = None
+        if entry[result_attribute] == None:
+            return None
 
-        while not _result_type == ldap.RES_SEARCH_RESULT:
-            (_result_type, _result) = self.ldap.result(_search, False, 0)
+        self.imap.cleanup_acls(entry[result_attribute])
 
-            if not _result == None:
-                for result in _result:
-                    _results.append(result)
 
-        return _results
+    def _change_delete_None(self, entry, change):
+        """
+            Redirect to _change_delete_unknown
+        """
+        self._change_delete_unknown(entry, change)
 
-    def _search(self,
-            base_dn,
-            scope=ldap.SCOPE_SUBTREE,
-            filterstr="(objectClass=*)",
-            attrlist=None,
-            attrsonly=0,
-            timeout=-1,
-            override_search=False,
-            callback=False,
-            primary_domain=None,
-            secondary_domains=[]
-        ):
+    def _change_delete_sharedfolder(self, entry, change):
+        pass
+
+    def _change_delete_unknown(self, entry, change):
         """
-            Search LDAP.
+            An entry has been deleted, and we do not know of what object type
+            the entry was - user, group, role or sharedfolder.
+        """
+        result_attribute = conf.get('cyrus-sasl', 'result_attribute')
+        if not entry.has_key(result_attribute):
+            return None
 
-            Use the priority ordered SUPPORTED_LDAP_CONTROLS and use
-            the first one supported.
+        if entry[result_attribute] == None:
+            return None
+
+        success = True
+        for _type in ['user','group','role','sharedfolder']:
+            try:
+                eval("_change_delete_%s(entry, change)" % (_type))
+            except:
+                success = False
+
+            if success:
+                break
+
+    def _change_delete_user(self, entry, change):
+        """
+            An entry of type user was deleted.
         """
 
-        if len(self.ldap.supported_controls) < 1:
-            for control_num in SUPPORTED_LDAP_CONTROLS.keys():
-                log.debug(
-                        _("Checking for support for %s") % (
-                                SUPPORTED_LDAP_CONTROLS[control_num]['desc']
-                            ),
-                        level=8
-                    )
+        result_attribute = conf.get('cyrus-sasl', 'result_attribute')
 
-            _search = self.ldap.search_s(
-                    '',
-                    scope=ldap.SCOPE_BASE,
-                    attrlist=['supportedControl']
-                )
+        if not entry.has_key(result_attribute):
+            return None
 
-            for (_result,_supported_controls) in _search:
-                supported_controls = _supported_controls.values()[0]
-                for control_num in SUPPORTED_LDAP_CONTROLS.keys():
-                    if SUPPORTED_LDAP_CONTROLS[control_num]['oid'] in \
-                            supported_controls:
+        if entry[result_attribute] == None:
+            return None
 
-                        self.ldap.supported_controls.append(
-                                SUPPORTED_LDAP_CONTROLS[control_num]['func']
-                            )
+        self.imap.user_mailbox_delete(entry[result_attribute])
+        self.imap.cleanup_acls(entry[result_attribute])
 
-        _results = []
 
-        if not override_search == False:
-            _use_ldap_controls = [ override_search ]
-        else:
-            _use_ldap_controls = self.ldap.supported_controls
+    def _change_moddn_group(self, entry, change):
+        # TODO: If the rdn attribute is the same as the result attribute...
+        pass
 
-        for supported_control in _use_ldap_controls:
-            exec("""_results = self.%s(
-                    %r,
-                    scope=%r,
-                    filterstr=%r,
-                    attrlist=%r,
-                    attrsonly=%r,
-                    timeout=%r,
-                    callback=callback,
-                    primary_domain=%r,
-                    secondary_domains=%r
-                )""" % (
-                        supported_control,
-                        base_dn,
-                        scope,
-                        filterstr,
-                        attrlist,
-                        attrsonly,
-                        timeout,
-                        primary_domain,
-                        secondary_domains
-                    )
-                )
+    def _change_moddn_role(self, entry, change):
+        pass
 
-        return _results
+    def _change_moddn_user(self, entry, change):
+        old_dn = change['previous_dn']
+        new_dn = change['dn']
 
-    def _result(self, msgid=ldap.RES_ANY, all=1, timeout=-1):
-        return self.ldap.result(msgid, all, timeout)
+        import ldap.dn
+        old_rdn = ldap.dn.explode_dn(old_dn)[0].split('=')[0]
+        new_rdn = ldap.dn.explode_dn(new_dn)[0].split('=')[0]
 
-    def _domain_default_quota(self, domain):
-        if conf.has_option(domain, 'default_quota'):
-            return conf.get(domain, 'default_quota', quiet=True)
-        elif conf.has_option('ldap', 'default_quota'):
-            return conf.get('ldap', 'default_quota', quiet=True)
-        elif conf.has_option('kolab', 'default_quota'):
-            return conf.get('kolab', 'default_quota', quiet=True)
+        result_attribute = conf.get('cyrus-sasl', 'result_attribute')
 
-    def _domain_section(self, domain):
-        if conf.has_section(domain):
-            return domain
+        if conf.changelog.has_key(entry['id']):
+            old_canon_attr = conf.changelog[entry['id']]
+
+        # See if we have to trigger the recipient policy. Only really applies to
+        # situations in which the result_attribute is used in the old or in the
+        # new DN.
+        trigger_recipient_policy = False
+
+        if old_rdn == result_attribute:
+            if new_rdn == result_attribute:
+                if new_rdn == old_rdn:
+                    trigger_recipient_policy = True
+            else:
+                if not new_rdn == old_rdn:
+                    trigger_recipient_policy = True
         else:
-            return 'ldap'
+            if new_rdn == result_attribute:
+                if not new_rdn == old_rdn:
+                    trigger_recipient_policy = True
+
+        if trigger_recipient_policy:
+            entry_changes = self.recipient_policy(entry)
+            # Now look at entry_changes and old_canon_attr, and see if they're the
+            # same value.
+            if entry_changes.has_key(result_attribute):
+                if not entry_changes[result_attribute] == old_canon_attr:
+                    self.imap.user_mailbox_rename(old_canon_attr, entry_changes[result_attribute])
+                    conf.changelog[entry['id']] = entry_changes[result_attribute]
+
+    def _change_moddn_sharedfolder(self, entry, change):
+        pass
 
-    def _get_group_attribute(self, group, attribute):
-        self._bind()
+    def _change_modify_group(self, entry, change):
+        pass
 
-        attribute = attribute.lower()
+    def _change_modify_role(self, entry, change):
+        pass
 
-        log.debug(
-                _("Getting attribute %s for group %s") % (attribute,group),
-                level=8
-            )
+    def _change_modify_sharedfolder(self, entry, change):
+        pass
 
-        _result_type = None
+    def _change_modify_user(self, entry, change):
+        for entry_key in conf.changelog.keys():
+            log.debug(_("Current changelog entry %s with %s") % (entry_key,conf.changelog[entry_key]), level=8)
 
-        _search = self.ldap.search_ext(
-                group['dn'],
-                ldap.SCOPE_BASE,
-                '(objectclass=*)',
-                [ 'dn', attribute ]
-            )
+        if conf.changelog.has_key(entry['id']):
+            old_canon_attr = conf.changelog[entry['id']]
 
-        (
-                _result_type,
-                _result_data,
-                _result_msgid,
-                _result_controls
-            ) = self.ldap.result3(_search)
+        entry_changes = self.recipient_policy(entry)
 
-        if len(_result_data) >= 1:
-            (group_dn, group_attrs) = _result_data[0]
-        else:
-            log.warning(_("Could not get attribute %s for group %s")
-                % (attribute,user['dn']))
+        log.debug(_("Result from recipient policy: %r") % (entry_changes), level=8)
 
-            return None
+        result_attribute = conf.get('cyrus-sasl','result_attribute')
+        if entry_changes.has_key(result_attribute):
+            if not entry_changes[result_attribute] == old_canon_attr:
+                self.imap.user_mailbox_rename(old_canon_attr, entry_changes[result_attribute])
+                conf.changelog[entry['id']] = entry_changes[result_attribute]
 
-        group_attrs = utils.normalize(group_attrs)
+    def _change_none_group(self, entry, change):
+        """
+            A group entry as part of the initial search result set.
 
-        if not group_attrs.has_key(attribute):
-            log.debug(
-                    _("Wanted attribute %s, which does not exist for group " + \
-                    "%r") % (
-                            attribute,
-                            group_dn
-                        ),
-                    level=8
-                )
+            The Kolab daemon has little to do for this type of action on this
+            type of entry.
+        """
+        pass
 
-            group_attrs[attribute] = None
+    def _change_none_role(self, entry, change):
+        """
+            A role entry as part of the initial search result set.
 
-        return group_attrs[attribute]
+            The Kolab daemon has little to do for this type of action on this
+            type of entry.
+        """
+        pass
 
-    def _get_user_attribute(self, user, attribute):
-        self._bind()
+    def _change_none_sharedfolder(self, entry, change):
+        """
+            A sharedfolder entry as part of the initial search result set.
+        """
+        self.imap.connect(domain=self.domain)
 
-        attribute = attribute.lower()
+        server = None
+        mailserver_attribute = self.config_get('mailserver_attribute')
 
-        log.debug(
-                _("Getting attribute %s for user %s") % (attribute,user),
-                level=8
-            )
+        if entry.has_key(mailserver_attribute):
+            server = entry['mailserver_attribute']
 
-        _result_type = None
+        if not entry.has_key('kolabtargetfolder'):
+            entry['kolabtargetfolder'] = self.get_entry_attribute(
+                    entry['id'],
+                    'kolabtargetfolder'
+                )
 
-        _search = self.ldap.search_ext(
-                user['dn'],
-                ldap.SCOPE_BASE,
-                '(objectclass=*)',
-                [ 'dn', attribute ]
-            )
+        if not entry.has_key('kolabfoldertype'):
+            entry['kolabfoldertype'] = self.get_entry_attribute(
+                    entry['id'],
+                    'kolabfoldertype'
+                )
 
-        try:
-            (
-                    _result_type,
-                    _result_data,
-                    _result_msgid,
-                    _result_controls
-                ) = self.ldap.result3(_search)
-        except ldap.NO_SUCH_OBJECT, e:
-            log.error(_("No such object %r in Auth::LDAP::_get_user_attribute") % (user['dn']))
-            return None
+        #if not entry.has_key('kolabmailfolderaclentry'):
+            #entry['kolabmailfolderaclentry'] = self.get_entry_attribute(
+                    #entry['id'],
+                    #'kolabmailfolderaclentry'
+                #)
 
-        if len(_result_data) >= 1:
-            (user_dn, user_attrs) = _result_data[0]
+        if entry.has_key('kolabtargetfolder') and not entry['kolabtargetfolder'] == None:
+            folder_path = entry['kolabtargetfolder']
         else:
-            log.warning(_("Could not get attribute %s for user %s")
-                % (attribute,user['dn']))
+            # TODO: What is *the* way to see if we need to create an @domain
+            # shared mailbox?
+            # TODO^2: self.domain, really? Presumes any mail attribute is
+            # set to the primary domain name space...
+            # TODO^3: Test if the cn is already something at domain
+            result_attribute = conf.get('cyrus-sasl', 'result_attribute')
+            if result_attribute in ['mail']:
+                folder_path = "%s@%s" % (entry['cn'], self.domain)
+            else:
+                folder_path = entry['cn']
 
-            return None
+        if not self.imap.shared_folder_exists(folder_path):
+            self.imap.shared_folder_create(folder_path, server)
 
-        user_attrs = utils.normalize(user_attrs)
+        if entry.has_key('kolabfoldertype') and not entry['kolabfoldertype'] == None:
+            self.imap.shared_folder_set_type(folder_path, entry['kolabfoldertype'])
 
-        if not user_attrs.has_key(attribute):
-            log.debug(
-                    _("Wanted attribute %s, which does not exist for user " + \
-                    "%r") % (
-                            attribute,
-                            user_dn
-                        ),
-                    level=8
-                )
+        #if entry.has_key('kolabmailfolderaclentry') and not entry['kolabmailfolderaclentry'] == None:
+            #self.imap._set_kolab_mailfolder_acls(entry['kolabmailfolderaclentry'])
 
-            user_attrs[attribute] = None
+        #if server == None:
+            #self.entry_set_attribute(mailserver_attribute, server)
 
-        return user_attrs[attribute]
+    def _change_none_user(self, entry, change):
+        """
+            A user entry as part of the initial search result set.
+        """
+        result_attribute = conf.get('cyrus-sasl', 'result_attribute')
 
-    def _get_user_attributes(self, user, attributes):
-        _user_attrs = {}
+        test = self.recipient_policy(entry)
 
-        for attribute in attributes:
-            _user_attrs[attribute] = self._get_user_attribute(user, attribute)
+        if not entry.has_key(result_attribute):
+            # TODO: Execute plugin-hook
+            return
 
-        return _user_attrs
+        if entry[result_attribute] == None:
+            # TODO: Execute plugin-hook
+            return
 
-    def _search_mail_address(self, domain, mail_address):
-        self._bind()
+        if not conf.changelog.has_key(entry['id']):
+            conf.changelog[entry['id']] = entry[result_attribute]
 
-        domain_root_dn = self._kolab_domain_root_dn(domain)
+        self.imap.connect(domain=self.domain)
 
-        return self._search(
-                domain_root_dn,
-                ldap.SCOPE_SUBTREE,
-                # TODO: Configurable
-                '(|(mail=%s)(mailalternateaddress=%s))' % (
-                        mail_address,
-                        mail_address
-                    ),
-                [ 'mail', 'mailalternateaddress' ],
-                override_search='_regular_search'
-            )
+        if not self.imap.user_mailbox_exists(entry[result_attribute]):
+            folder = self.imap.user_mailbox_create(entry[result_attribute])
+            server = self.imap.user_mailbox_server(folder)
+
+    def _disconnect(self):
+        self._unbind()
+        del self.ldap
+        self.ldap = None
+        self.bind = False
+
+    def _entry_dict(self, value):
+        """
+            Tests if 'value' is a valid entry dictionary with a DN contained
+            within key 'dn'.
+
+            Returns True or False
+        """
+        if isinstance(value, dict):
+            if value.has_key('dn'):
+                return True
+
+        return False
+
+    def _entry_dn(self, value):
+        """
+            Tests if 'value' is a valid DN.
 
-    def _set_user_attribute(self, user, attribute, value):
-        self._bind()
+            Returns True or False
+        """
 
-        if isinstance(user, basestring):
-            user = { "dn": user }
+        # Only basestrings can be DNs
+        if not isinstance(value, basestring):
+            return False
 
-        attribute = attribute.lower()
+        try:
+            import ldap.dn
+            ldap_dn = ldap.dn.explode_dn(value)
+        except ldap.DECODING_ERROR:
+            # This is not a DN.
+            return False
 
-        if not user.has_key(attribute):
-            user[attribute] = self._get_user_attribute(user, attribute)
+        return True
 
-        mode = None
+    def _entry_type(self, entry_id):
+        """
+            Return the type of object for an entry.
+        """
 
-        # TODO: This should be a schema check!
-        if attribute in [ 'mailquota', 'mailalternateaddress' ]:
-            if not user.has_key('objectclass'):
-                user['objectclass'] = self._get_user_attribute(
-                        user,
-                        'objectclass'
-                    )
+        entry_dn = self.entry_dn(entry_id)
 
-                if user['objectclass'] == None:
-                    return
+        base_dn = self.config_get('base_dn')
 
-            if not 'mailrecipient' in user['objectclass']:
-                user['objectclass'].append('mailrecipient')
-                self._set_user_attribute(
-                        user,
-                        'objectclass',
-                        user['objectclass']
-                    )
+        for _type in ['user', 'group', 'sharedfolder']:
+            __filter = self.config_get('kolab_%s_filter' % (_type))
+            if __filter == None:
+                __filter = self.config_get('%s_filter' % (_type))
 
-        if user.has_key(attribute) and not user[attribute] == None:
-            mode = ldap.MOD_REPLACE
-        else:
-            mode = ldap.MOD_ADD
+            if not __filter == None:
+                try:
+                    result = self._regular_search(entry_dn, filterstr=__filter)
+                except:
+                    result = self._regular_search(
+                            base_dn,
+                            filterstr="(%s=%s)" %(
+                                    self.config_get('unique_attribute'),
+                                    entry_id['id'])
+                                )
 
-        try:
-            if isinstance(value, int):
-                value = (str)(value)
-
-            self.ldap.modify(user['dn'], [(mode, attribute, value)])
-        except ldap.LDAPError, e:
-            log.warning(
-                    _("LDAP modification of attribute %s for %s to value " + \
-                    "%s failed: %r") % (attribute,user_dn,value,e.message['info'])
-                )
+                if not result:
+                    continue
+                else:
+                    return _type
 
-    def _list_domains(self):
+    def _find_user_dn(self, login, realm):
         """
-            Find the domains related to this Kolab setup, and return a list of
-            DNS domain names.
-
-            Returns a list of tuples, each tuple containing the primary domain
-            name and a list of secondary domain names.
+            Find the distinguished name (DN) for an entry in LDAP.
         """
 
-        log.debug(_("Listing domains..."), level=8)
+        domain_root_dn = self._kolab_domain_root_dn(self.domain)
 
-        self._connect()
+        base_dn = self.config_get('user_base_dn')
+        if base_dn == None:
+            base_dn = self.config_get('base_dn')
 
-        bind_dn = conf.get('ldap', 'bind_dn')
-        bind_pw = conf.get('ldap', 'bind_pw')
+        auth_attrs = self.config_get_list('auth_attributes')
 
-        domain_base_dn = conf.get('ldap', 'domain_base_dn', quiet=True)
+        auth_search_filter = [ '(|' ]
 
-        if domain_base_dn == "":
-            # No domains are to be found in LDAP, return an empty list.
-            # Note that the Auth() base itself handles this case.
-            return []
+        for auth_attr in auth_attrs:
+            auth_search_filter.append('(%s=%s)' % (auth_attr,login))
+            auth_search_filter.append('(%s=%s@%s)' % (auth_attr,login,self.domain))
 
-        # If we haven't returned already, let's continue searching
-        kolab_domain_filter = conf.get('ldap', 'kolab_domain_filter')
+        auth_search_filter.append(')')
 
-        # TODO: this function should be wrapped for error handling
-        try:
-            self.ldap.simple_bind_s(bind_dn, bind_pw)
-        except ldap.SERVER_DOWN, e:
-            raise AuthBackendError, _("Authentication database DOWN")
+        auth_search_filter = ''.join(auth_search_filter)
 
-        _search = self._search(
-                domain_base_dn,
-                ldap.SCOPE_SUBTREE,
-                kolab_domain_filter,
-                # TODO: Where we use associateddomain is actually configurable
-                [ 'associateddomain' ],
-                override_search='_regular_search'
+        search_filter = "(&%s%s)" % (
+                auth_search_filter,
+                user_filter
             )
 
-        domains = []
-
-        for domain_dn, domain_attrs in _search:
-            primary_domain = None
-            secondary_domains = []
+        _results = self._search(
+                user_base_dn,
+                filterstr=search_filter,
+                attrlist=[ 'dn' ],
+                override_search='_regular_search'
+            )
 
-            domain_attrs = utils.normalize(domain_attrs)
+        if len(_results) == 1:
+            (_user_dn, _user_attrs) = _results[0]
+        else:
+            # Retry to find the user_dn with just uid=%s against the root_dn,
+            # if the login is not fully qualified
+            if len(login.split('@')) < 2:
+                search_filter = "(uid=%s)" % (login)
+                _results = self._search(
+                        domain,
+                        filterstr=search_filter,
+                        attrlist=[ 'dn' ]
+                    )
 
-            # TODO: Where we use associateddomain is actually configurable
-            if type(domain_attrs['associateddomain']) == list:
-                primary_domain = domain_attrs['associateddomain'].pop(0)
-                secondary_domains = domain_attrs['associateddomain']
+                if len(_results) == 1:
+                    (_user_dn, _user_attrs) = _results[0]
+                else:
+                    # Overall fail
+                    return False
             else:
-                primary_domain = domain_attrs['associateddomain']
-
-            domains.append((primary_domain,secondary_domains))
+                return False
 
-        return domains
+        return _user_dn
 
     def _kolab_domain_root_dn(self, domain):
         self._bind()
@@ -1106,482 +1150,477 @@ class LDAP(object):
 
         return utils.standard_root_dn(domain)
 
-    def _list_users(self, primary_domain, secondary_domains=[], callback=None):
+    def _kolab_filter(self):
+        """
+            Compose a filter using the relevant settings from configuration.
+        """
+        _filter = "(|"
+        for _type in ['user', 'group', 'sharedfolder']:
+            __filter = self.config_get('kolab_%s_filter' % (_type))
+            if __filter == None:
+                __filter = self.config_get('%s_filter' % (_type))
 
-        # Track state for psearch and paged searches.
-        self._initial_sync_done = False
+            if not __filter == None:
+                _filter = "%s%s" % (_filter,__filter)
 
-        log.info(_("Listing users for domain %s (and %s)")
-            % (primary_domain, ', '.join(secondary_domains)))
+        _filter = "%s)" % (_filter)
 
-        self._bind()
+        return _filter
 
-        # TODO: Bind with read-only credentials, perhaps even domain-specific
-        bind_dn = conf.get('ldap', 'bind_dn')
-        #bind_dn = conf.get('ldap', 'ro_bind_dn')
-        bind_pw = conf.get('ldap', 'bind_pw')
-        #bind_pw = conf.get('ldap', 'ro_bind_pw')
+    def _list_domains(self):
+        """
+            Find the domains related to this Kolab setup, and return a list of
+            DNS domain names.
 
-        if conf.has_option(primary_domain, 'user_base_dn'):
-            section = primary_domain
-        else:
-            section = 'ldap'
+            Returns a list of tuples, each tuple containing the primary domain
+            name and a list of secondary domain names.
 
-        domain_root_dn = self._kolab_domain_root_dn(primary_domain)
+            This function should only be called by the primary instance of Auth.
+        """
 
-        user_base_dn = conf.get_raw(
-                section,
-                'user_base_dn'
-            ) % ({'base_dn': domain_root_dn})
+        log.debug(_("Listing domains..."), level=8)
 
-        if conf.has_option(primary_domain, 'kolab_user_filter'):
-            section = primary_domain
-        else:
-            section = 'ldap'
+        self.connect()
 
-        kolab_user_filter = conf.get(section, 'kolab_user_filter', quiet=True)
+        bind_dn = conf.get('ldap', 'bind_dn')
+        bind_pw = conf.get('ldap', 'bind_pw')
 
-        if conf.has_option(primary_domain, 'kolab_user_scope'):
-            section = primary_domain
-        else:
-            section = 'ldap'
+        domain_base_dn = conf.get('ldap', 'domain_base_dn', quiet=True)
 
-        _kolab_user_scope = conf.get(section, 'kolab_user_scope', quiet=True)
+        if domain_base_dn == "":
+            # No domains are to be found in LDAP, return an empty list.
+            # Note that the Auth() base itself handles this case.
+            return []
 
-        if LDAP_SCOPE.has_key(_kolab_user_scope):
-            kolab_user_scope = LDAP_SCOPE[_kolab_user_scope]
+        # If we haven't returned already, let's continue searching
+        domain_filter = conf.get('ldap', 'kolab_domain_filter')
+        if not domain_filter == None:
+            log.warning(_("ldap/kolab_domain_filter deprecated, use ldap/domain_filter instead."))
         else:
-            log.warning(
-                    _("LDAP Search scope %s not found, using 'sub'") % (
-                            _kolab_user_scope
-                        )
-                )
+            domain_filter = conf.get('ldap', 'domain_filter')
 
-            kolab_user_scope = ldap.SCOPE_SUBTREE
-
-        # TODO: Is, perhaps, a domain specific setting
-        result_attribute = conf.get(
-                'cyrus-sasl',
-                'result_attribute',
-                quiet=True
-            )
+        if domain_base_dn == None or domain_filter == None:
+            return []
 
+        # TODO: this function should be wrapped for error handling
         try:
             self.ldap.simple_bind_s(bind_dn, bind_pw)
         except ldap.SERVER_DOWN, e:
-            error = eval("%s" % (e))
-            log.error(_("Error binding to LDAP: %s") % (error['desc']))
-            # TODO: Exit the fork (if fork!)
-            return
+            raise AuthBackendError, _("Authentication database DOWN")
 
-        # TODO: The quota and alternative address attributes are actually
-        # supposed to be settings.
-        _search = self._search(
-                user_base_dn,
-                kolab_user_scope,
-                kolab_user_filter,
-                attrlist=[
-                        'dn',
-                        result_attribute,
-                        'sn',
-                        'givenname',
-                        'cn',
-                        'uid'
-                    ],
-                attrsonly=0,
-                callback=callback,
-                primary_domain=primary_domain,
-                secondary_domains=secondary_domains
-            )
+        try:
+            _search = self._search(
+                    domain_base_dn,
+                    ldap.SCOPE_SUBTREE,
+                    domain_filter,
+                    # TODO: Where we use associateddomain is actually configurable
+                    [ 'associateddomain' ],
+                    override_search='_regular_search'
+                )
+        except:
+            return []
+
+        domains = []
 
-        if callback == None:
-            log.info(_("Found %d users") % (len(_search)))
+        for domain_dn, domain_attrs in _search:
+            primary_domain = None
+            secondary_domains = []
 
-            log.debug(_("Iterating over %d users, making sure we have the " + \
-                "necessary attributes...") % (len(_search)), level=6)
+            domain_attrs = utils.normalize(domain_attrs)
 
-            users = []
+            # TODO: Where we use associateddomain is actually configurable
+            if type(domain_attrs['associateddomain']) == list:
+                primary_domain = domain_attrs['associateddomain'].pop(0)
+                secondary_domains = domain_attrs['associateddomain']
+            else:
+                primary_domain = domain_attrs['associateddomain']
 
-            num_users = len(_search)
-            num_user = 0
+            domains.append((primary_domain,secondary_domains))
 
-            for user_dn, user_attrs in _search:
-                num_user += 1
+        return domains
 
-                # Placeholder for the user attributes
-                user = user_attrs
-                user['dn'] = user_dn
+    def _reconnect(self):
+        """
+            Reconnect to LDAP
+        """
+        self._disconnect()
+        self.connect()
 
-                user = self._get_user_details(
-                        user,
-                        primary_domain,
-                        secondary_domains
-                    )
+    def _synchronize_callback(self, *args, **kw):
+        change_type = None
 
-                if user:
-                    users.append(user)
+        change_dict = {
+                'change_type': kw['change_type'],
+                'previous_dn': kw['previous_dn'],
+                'change_number': kw['change_number'],
+                'dn': kw['dn']
+            }
 
-                if (num_user % 1000) == 0:
-                    log.debug(
-                            _("Done iterating over user %d of %d")
-                                % (num_user,num_users),
-                            level=3
-                        )
+        entry = utils.normalize(kw['entry'])
+        entry['dn'] = kw['dn']
 
-            return users
+        unique_attr = self.config_get('unique_attribute')
+        entry['id'] = entry[unique_attr]
 
-    def _get_user_details(self, user, primary_domain, secondary_domains=[]):
+        try:
+            entry['type'] = self._entry_type(entry)
+        except:
+            entry['type'] = "unknown"
 
-        # TODO: Is, perhaps, a domain specific setting
-        result_attribute = conf.get(
-                'cyrus-sasl',
-                'result_attribute',
-                quiet=True
-            )
+        log.debug(_("Entry type: %s") % (entry['type']), level=8)
 
-        if not user.has_key('standard_domain'):
-            user['standard_domain'] = (primary_domain, secondary_domains)
-
-        user = utils.normalize(user)
-
-        _get_attrs = []
-        _wanted_attributes = [
-                result_attribute,
-                'mail',
-                'mailalternateaddress',
-                'sn',
-                'givenname',
-                'cn',
-                'uid',
-                'preferredLanguage'
-            ]
-
-        for attribute in _wanted_attributes:
-            if not user.has_key(attribute):
-                _get_attrs.append(attribute)
-                #user[attribute] = self._get_user_attribute(user, attribute)
-
-        if len(_get_attrs) > 0:
-            _user_attrs = self._get_user_attributes(user, _get_attrs)
-            for key in _user_attrs.keys():
-                user[key] = _user_attrs[key]
-
-        user = utils.normalize(user)
-
-        if not user.has_key('preferredlanguage') or user['preferredlanguage'] == None:
-            if conf.has_option(primary_domain, 'default_locale'):
-                default_locale = conf.get(primary_domain, 'default_locale')
-            else:
-                default_locale = conf.get('kolab','default_locale')
+        if change_dict['change_type'] == None:
+            # This entry was in the start result set
+            eval("self._change_none_%s(entry, change_dict)" % (entry['type']))
+        else:
+            change = psearch.CHANGE_TYPES_STR[change_dict['change_type']].lower()
+            eval("self._change_%s_%s(entry, change_dict)" % (change, entry['type']))
 
-            self._set_user_attribute(user, 'preferredlanguage', default_locale)
-            user['preferredlanguage'] = default_locale
+    def _unbind(self):
+        """
+            Discard the current set of bind credentials.
 
-        # Check to see if we want to apply a primary mail recipient policy
-        if conf.has_option(primary_domain, 'primary_mail'):
-            primary_mail = conf.plugins.exec_hook(
-                    "set_primary_mail",
-                    kw={
-                            'primary_mail':
-                                conf.get_raw(
-                                        primary_domain,
-                                        'primary_mail'
-                                    ),
-                            'user_attrs': user,
-                            'primary_domain': primary_domain,
-                            'secondary_domains': secondary_domains
-                        }
-                )
+            Virtually disconnects the LDAP connection, and should be followed by
+            a call to _bind() afterwards.
+        """
 
-            i = 1
-            _primary_mail = primary_mail
+        self.ldap.unbind()
+        self.bind = False
 
-            done = False
-            while not done:
-                results = self._search_mail_address(
-                        primary_domain,
-                        _primary_mail
-                    )
+    ###
+    ### Backend search functions
+    ###
 
-                # Length of results should be 0 (no entry found)
-                # or 1 (which should be the entry we're looking at here)
-                if len(results) == 0:
-                    log.debug(
-                            _("No results for mail address %s found") % (
-                                    _primary_mail
-                                ),
-                            level=8
-                        )
+    def _persistent_search(self,
+            base_dn,
+            scope=ldap.SCOPE_SUBTREE,
+            filterstr="(objectClass=*)",
+            attrlist=None,
+            attrsonly=0,
+            timeout=-1,
+            callback=False,
+            primary_domain=None,
+            secondary_domains=[]
+        ):
+        _results = []
 
-                    done = True
-                    continue
+        psearch_server_controls = []
 
-                if len(results) == 1:
-                    log.debug(
-                            _("1 result for address %s found, verifying") % (
-                                    _primary_mail
-                                ),
-                            level=8
-                        )
+        psearch_server_controls.append(psearch.PersistentSearchControl(
+                    criticality=True,
+                    changeTypes=[ 'add', 'delete', 'modify', 'modDN' ],
+                    changesOnly=False,
+                    returnECs=True
+                )
+            )
 
-                    almost_done = True
-                    for result in results:
-                        if not result[0] == user['dn']:
-                            log.debug(
-                                    _("Too bad, primary email address %s " + \
-                                    "already in use for %s (we are %s)") % (
-                                            _primary_mail,
-                                            result[0],
-                                            user['dn']
-                                        ),
-                                    level=8
-                                )
+        _search = self.ldap.search_ext(
+                base_dn,
+                scope=scope,
+                filterstr=filterstr,
+                attrlist=attrlist,
+                attrsonly=attrsonly,
+                timeout=timeout,
+                serverctrls=psearch_server_controls
+            )
 
-                            almost_done = False
+        ecnc = psearch.EntryChangeNotificationControl
 
-                    if almost_done:
-                        done = True
-                        continue
+        while True:
+            res_type,res_data,res_msgid,_None,_None,_None = self.ldap.result4(
+                    _search,
+                    all=0,
+                    add_ctrls=1,
+                    add_intermediates=1,
+                    resp_ctrl_classes={ecnc.controlType:ecnc}
+                )
 
-                i += 1
-                _primary_mail = "%s%d@%s" % (
-                        primary_mail.split('@')[0],
-                        i,
-                        primary_mail.split('@')[1]
-                    )
+            change_type = None
+            previous_dn = None
+            change_number = None
+
+            for dn,entry,srv_ctrls in res_data:
+                log.debug(_("LDAP Search Result Data Entry:"), level=8)
+                log.debug("    DN: %r" % (dn), level=8)
+                log.debug("    Entry: %r" % (entry), level=8)
+
+                ecn_ctrls = [
+                        c for c in srv_ctrls
+                        if c.controlType == ecnc.controlType
+                    ]
 
-            primary_mail = _primary_mail
+                if ecn_ctrls:
+                    change_type = ecn_ctrls[0].changeType
+                    previous_dn = ecn_ctrls[0].previousDN
+                    change_number = ecn_ctrls[0].changeNumber
+                    change_type_desc = psearch.CHANGE_TYPES_STR[change_type]
 
-            if not primary_mail == None:
-                if not user.has_key('mail'):
-                    self._set_user_attribute(user, 'mail', primary_mail)
-                    user['mail'] = primary_mail
-                else:
-                    if not primary_mail == user['mail']:
-                        self._set_user_attribute(user, 'mail', primary_mail)
+                    log.debug(
+                            _("Entry Change Notification attributes:"),
+                            level=8
+                        )
 
-                        if not user['mail'] == None:
-                            user['old_mail'] = user['mail']
+                    log.debug(
+                            "    " + _("Change Type: %r (%r)") % (
+                                    change_type,
+                                    change_type_desc
+                                ),
+                            level=8
+                        )
 
-                        user['mail'] = primary_mail
+                    log.debug(
+                            "    " + _("Previous DN: %r") % (previous_dn),
+                            level=8
+                        )
 
-            # Check to see if we want to apply a secondary mail recipient
-            # policy.
-            section = None
+                if callback:
+                    callback(
+                            dn=dn,
+                            entry=entry,
+                            previous_dn=previous_dn,
+                            change_type=change_type,
+                            change_number=change_number,
+                            primary_domain=primary_domain,
+                            secondary_domains=secondary_domains
+                        )
 
-            if conf.has_option(primary_domain, 'secondary_mail'):
-                section = primary_domain
-            elif conf.has_option('kolab', 'secondary_mail'):
-                section = 'kolab'
+    def _paged_search(self,
+            base_dn,
+            scope=ldap.SCOPE_SUBTREE,
+            filterstr="(objectClass=*)",
+            attrlist=None,
+            attrsonly=0,
+            timeout=-1,
+            callback=False,
+            primary_domain=None,
+            secondary_domains=[]
+        ):
 
-            if not section == None:
-                # Execute the plugin hook
-                suggested_secondary_mail = conf.plugins.exec_hook(
-                        "set_secondary_mail",
-                        kw={
-                                'secondary_mail':
-                                    conf.get_raw(
-                                            primary_domain,
-                                            'secondary_mail'
-                                        ),
-                                'user_attrs': user,
-                                'primary_domain': primary_domain,
-                                'secondary_domains': secondary_domains
-                            }
-                    ) # end of conf.plugins.exec_hook() call
+        page_size = 500
+        critical = True
+        _results = []
 
-                secondary_mail = []
+        server_page_control = SimplePagedResultsControl(page_size=page_size)
 
-                for _secondary_mail in suggested_secondary_mail:
-                    i = 1
-                    __secondary_mail = _secondary_mail
+        _search = self.ldap.search_ext(
+                base_dn,
+                scope=scope,
+                filterstr=filterstr,
+                attrlist=attrlist,
+                attrsonly=attrsonly,
+                serverctrls=[server_page_control]
+            )
 
-                    done = False
-                    while not done:
-                        results = self._search_mail_address(
-                                primary_domain,
-                                __secondary_mail
-                            )
+        pages = 0
+        while True:
+            pages += 1
+            try:
+                (
+                        _result_type,
+                        _result_data,
+                        _result_msgid,
+                        _result_controls
+                    ) = self.ldap.result3(_search)
 
-                        # Length of results should be 0 (no entry found)
-                        # or 1 (which should be the entry we're looking at here)
-                        if len(results) == 0:
-                            log.debug(
-                                    _("No results for address %s found") % (
-                                            __secondary_mail
-                                        ),
-                                    level=8
-                                )
+            except ldap.NO_SUCH_OBJECT, e:
+                log.warning(_("Object %s searched no longer exists") % (base_dn))
+                break
 
-                            done = True
-                            continue
+            if callback:
+                callback(
+                        user=_result_data,
+                        primary_domain=primary_domain,
+                        secondary_domains=secondary_domains
+                    )
 
-                        if len(results) == 1:
-                            log.debug(
-                                    _("1 result for address %s found, " + \
-                                    "verifying...") % (
-                                            __secondary_mail
-                                        ),
-                                    level=8
-                                )
+            _results.extend(_result_data)
+            if (pages % 2) == 0:
+                log.debug(_("%d results...") % (len(_results)))
 
-                            almost_done = True
-                            for result in results:
-                                if not result[0] == user['dn']:
-                                    log.debug(
-                                            _("Too bad, secondary email " + \
-                                            "address %s already in use for " + \
-                                            "%s (we are %s)") % (
-                                                    __secondary_mail,
-                                                    result[0],
-                                                    user['dn']
-                                                ),
-                                            level=8
-                                        )
-
-                                    almost_done = False
-
-                            if almost_done:
-                                done = True
-                                continue
-
-                        i += 1
-                        __secondary_mail = "%s%d@%s" % (
-                                _secondary_mail.split('@')[0],
-                                i,
-                                _secondary_mail.split('@')[1]
-                            )
+            pctrls = [
+                    c for c in _result_controls
+                        if c.controlType == LDAP_CONTROL_PAGED_RESULTS
+                ]
 
-                    secondary_mail.append(__secondary_mail)
+            if pctrls:
+                size = pctrls[0].size
+                cookie = pctrls[0].cookie
+                if cookie:
+                    server_page_control.cookie = cookie
+                    _search = self.ldap.search_ext(
+                            base_dn,
+                            scope=scope,
+                            filterstr=filterstr,
+                            attrlist=attrlist,
+                            attrsonly=attrsonly,
+                            serverctrls=[server_page_control]
+                        )
+                else:
+                    # TODO: Error out more verbose
+                    break
+            else:
+                # TODO: Error out more verbose
+                print "Warning:  Server ignores RFC 2696 control."
+                break
 
-                if not secondary_mail == None:
-                    secondary_mail = list(set(secondary_mail))
-                    # Avoid duplicates
-                    while primary_mail in secondary_mail:
-                        secondary_mail.pop(secondary_mail.index(primary_mail))
+        return _results
 
-                    if not user.has_key('mailalternateaddress'):
-                        if not len(secondary_mail) == 0:
-                            self._set_user_attribute(
-                                    user,
-                                    'mailalternateaddress',
-                                    secondary_mail
-                                )
+    def _vlv_search(self,
+            base_dn,
+            scope=ldap.SCOPE_SUBTREE,
+            filterstr="(objectClass=*)",
+            attrlist=None,
+            attrsonly=0,
+            timeout=-1,
+            callback=False,
+            primary_domain=None,
+            secondary_domains=[]
+        ):
+        pass
 
-                            user['mailalternateaddress'] = secondary_mail
-                    else:
-                        if not secondary_mail == user['mailalternateaddress']:
-                            self._set_user_attribute(
-                                    user,
-                                    'mailalternateaddress',
-                                    secondary_mail
-                                )
+    def _sync_repl(self,
+            base_dn,
+            scope=ldap.SCOPE_SUBTREE,
+            filterstr="(objectClass=*)",
+            attrlist=None,
+            attrsonly=0,
+            timeout=-1,
+            callback=False,
+            primary_domain=None,
+            secondary_domains=[]
+        ):
 
-                            user['mailalternateaddress'] = secondary_mail
+        import syncrepl
 
-        return user
+        ldap_sync_conn = syncrepl.DNSync(
+                '/var/lib/pykolab/syncrepl.db',
+                ldap_url.initializeUrl(),
+                trace_level=ldapmodule_trace_level,
+                trace_file=ldapmodule_trace_file
+            )
 
-    def sync_user(self, *args, **kw):
-        # See if kw['dn'] has been set.
+        msgid = ldap_sync_conn.syncrepl_search(
+                base_dn,
+                scope,
+                mode='refreshAndPersist',
+                filterstr=filterstr
+            )
 
-        if kw.has_key('dn'):
-            self.sync_ldap_user(*args, **kw)
-        elif kw.has_key('user'):
-            for user_dn, user_attrs in kw['user']:
-                _user = utils.normalize(user_attrs)
-                _user['dn'] = user_dn
-                kw['user'] = _user
-                self.sync_ldap_user(*args, **kw)
-        else:
-            # TODO: Not yet implemented
+        try:
+            # Here's where returns need to be taken into account...
+            while ldap_sync_conn.syncrepl_poll(all=1, msgid=msgid):
+                pass
+        except KeyboardInterrupt:
             pass
 
-    def sync_ldap_user(self, *args, **kw):
-        user = None
+    def _regular_search(self,
+            base_dn,
+            scope=ldap.SCOPE_SUBTREE,
+            filterstr="(objectClass=*)",
+            attrlist=None,
+            attrsonly=0,
+            timeout=-1,
+            callback=False,
+            primary_domain=None,
+            secondary_domains=[]
+        ):
 
-        done = False
+        log.debug(_("Searching with filter %r") % (filterstr), level=8)
 
-        if kw.has_key('change_type'):
-            # This is a EntryChangeControl notification
-            user = utils.normalize(kw['entry'])
-            user['dn'] = kw['dn']
+        _search = self.ldap.search(
+                base_dn,
+                scope=scope,
+                filterstr=filterstr,
+                attrlist=attrlist,
+                attrsonly=attrsonly
+            )
 
-            if kw['change_type'] == None:
-                # This user has not been changed, but existed already.
-                user = self._get_user_details(
-                        user,
-                        primary_domain=kw['primary_domain'],
-                        secondary_domains=kw['secondary_domains']
-                    )
+        _results = []
+        _result_type = None
 
-                pykolab.imap.synchronize(
-                        users=[user],
-                        primary_domain=kw['primary_domain'],
-                        secondary_domains=kw['secondary_domains']
-                    )
+        while not _result_type == ldap.RES_SEARCH_RESULT:
+            (_result_type, _result) = self.ldap.result(_search, False, 0)
 
-                done = True
+            if not _result == None:
+                for result in _result:
+                    _results.append(result)
 
-            elif kw['change_type'] == 1:
-                user = self._get_user_details(
-                        user,
-                        primary_domain=kw['primary_domain'],
-                        secondary_domains=kw['secondary_domains']
-                    )
+        return _results
 
-                pykolab.imap.synchronize(
-                        users=[user],
-                        primary_domain=kw['primary_domain'],
-                        secondary_domains=kw['secondary_domains']
-                    )
+    def _search(self,
+            base_dn,
+            scope=ldap.SCOPE_SUBTREE,
+            filterstr="(objectClass=*)",
+            attrlist=None,
+            attrsonly=0,
+            timeout=-1,
+            override_search=False,
+            callback=False,
+            primary_domain=None,
+            secondary_domains=[]
+        ):
+        """
+            Search LDAP.
 
-                done = True
+            Use the priority ordered SUPPORTED_LDAP_CONTROLS and use
+            the first one supported.
+        """
 
-            elif kw['change_type'] == 4:
-                # TODO: How do we know what has changed?
-                user = self._get_user_details(
-                        user,
-                        primary_domain=kw['primary_domain'],
-                        secondary_domains=kw['secondary_domains']
+        if len(self.ldap.supported_controls) < 1:
+            for control_num in SUPPORTED_LDAP_CONTROLS.keys():
+                log.debug(
+                        _("Checking for support for %s on %s") % (
+                                SUPPORTED_LDAP_CONTROLS[control_num]['desc'],
+                                self.domain
+                            ),
+                        level=8
                     )
 
-                pykolab.imap.synchronize(
-                        users=[user],
-                        primary_domain=kw['primary_domain'],
-                        secondary_domains=kw['secondary_domains']
-                    )
+            _search = self.ldap.search_s(
+                    '',
+                    scope=ldap.SCOPE_BASE,
+                    attrlist=['supportedControl']
+                )
 
-                done = True
-
-            elif kw['change_type'] == 2:
-                # TODO: Use Cyrus SASL authorization ID
-                folder = 'user/%s' % (user['mail'].lower())
-                # TODO: Verify if folder exists
-                pykolab.imap.delete_mailfolder(folder)
-                done = True
-
-            elif kw['change_type'] == 8:
-                # Object has had its rdn changed
-                user = self._get_user_details(
-                        user,
-                        primary_domain=kw['primary_domain'],
-                        secondary_domains=kw['secondary_domains']
-                    )
+            for (_result,_supported_controls) in _search:
+                supported_controls = _supported_controls.values()[0]
+                for control_num in SUPPORTED_LDAP_CONTROLS.keys():
+                    if SUPPORTED_LDAP_CONTROLS[control_num]['oid'] in \
+                            supported_controls:
 
-                pykolab.imap.synchronize(
-                        users=[user],
-                        primary_domain=kw['primary_domain'],
-                        secondary_domains=kw['secondary_domains']
-                    )
+                        self.ldap.supported_controls.append(
+                                SUPPORTED_LDAP_CONTROLS[control_num]['func']
+                            )
 
-                done = True
+        _results = []
 
-        if kw.has_key('user'):
-            user = kw['user']
+        if not override_search == False:
+            _use_ldap_controls = [ override_search ]
+        else:
+            _use_ldap_controls = self.ldap.supported_controls
 
-        if user and not done:
-            pykolab.imap.synchronize(
-                    users=[user],
-                    primary_domain=kw['primary_domain'],
-                    secondary_domains=kw['secondary_domains']
+        for supported_control in _use_ldap_controls:
+            exec("""_results = self.%s(
+                    %r,
+                    scope=%r,
+                    filterstr=%r,
+                    attrlist=%r,
+                    attrsonly=%r,
+                    timeout=%r,
+                    callback=callback,
+                    primary_domain=%r,
+                    secondary_domains=%r
+                )""" % (
+                        supported_control,
+                        base_dn,
+                        scope,
+                        filterstr,
+                        attrlist,
+                        attrsonly,
+                        timeout,
+                        primary_domain,
+                        secondary_domains
+                    )
                 )
+
+        return _results
diff --git a/pykolab/base.py b/pykolab/base.py
new file mode 100644
index 0000000..ebc1a59
--- /dev/null
+++ b/pykolab/base.py
@@ -0,0 +1,93 @@
+# 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 pykolab
+from pykolab.imap import IMAP
+
+conf = pykolab.getConf()
+
+class Base(object):
+    """
+        Abstraction class for functions commonly shared between auth, imap, etc.
+    """
+    def __init__(self, *args, **kw):
+        self.domain = conf.get('kolab', 'primary_domain')
+
+        # Placeholder primary_domain => [secondary_domains]. Should be updated
+        # on auth backend _connect().
+        self.secondary_domains = {}
+
+        self.imap = IMAP()
+
+    def config_get(self, key1, key2=None):
+        if not key2 == None:
+            return conf.get(key1, key2)
+
+        if conf.has_option(self.domain, key1):
+            return conf.get(self.domain, key1)
+
+        if conf.has_option(self.domain, 'auth_mechanism'):
+            if conf.has_option(conf.get(self.domain, 'auth_mechanism'), key1):
+                return conf.get(conf.get(self.domain, 'auth_mechanism'), key1)
+
+        if conf.has_option(conf.get('kolab', 'auth_mechanism'), key1):
+            return conf.get(conf.get('kolab', 'auth_mechanism'), key1)
+
+        if conf.has_option('kolab', key1):
+            return conf.get('kolab', key1)
+
+        return None
+
+    def config_get_list(self, key1, key2=None):
+        if not key2 == None:
+            return conf.get_list(key1, key2)
+
+        if conf.has_option(self.domain, key1):
+            return conf.get_list(self.domain, key1)
+
+        if conf.has_option(self.domain, 'auth_mechanism'):
+            if conf.has_option(conf.get(self.domain, 'auth_mechanism'), key1):
+                return conf.get_list(conf.get(self.domain, 'auth_mechanism'), key1)
+
+        if conf.has_option(conf.get('kolab', 'auth_mechanism'), key1):
+            return conf.get_list(conf.get('kolab', 'auth_mechanism'), key1)
+
+        if conf.has_option('kolab', key1):
+            return conf.get_list('kolab', key1)
+
+        return None
+
+    def config_get_raw(self, key1, key2=None):
+        if not key2 == None:
+            return conf.get_raw(key1, key2)
+
+        if conf.has_option(self.domain, key1):
+            return conf.get_raw(self.domain, key1)
+
+        if conf.has_option(self.domain, 'auth_mechanism'):
+            if conf.has_option(conf.get(self.domain, 'auth_mechanism'), key1):
+                return conf.get_raw(conf.get(self.domain, 'auth_mechanism'), key1)
+
+        if conf.has_option(conf.get('kolab', 'auth_mechanism'), key1):
+            return conf.get_raw(conf.get('kolab', 'auth_mechanism'), key1)
+
+        if conf.has_option('kolab', key1):
+            return conf.get_raw('kolab', key1)
+
+        return None
+
diff --git a/pykolab/conf/__init__.py b/pykolab/conf/__init__.py
index fdf723f..9092689 100644
--- a/pykolab/conf/__init__.py
+++ b/pykolab/conf/__init__.py
@@ -48,6 +48,8 @@ class Conf(object):
 
         self.entitlement = None
 
+        self.changelog = {}
+
         try:
             from pykolab.conf.entitlement import Entitlement
             entitlements = True
@@ -496,8 +498,14 @@ class Conf(object):
         if not self.cfg_parser:
             self.read_config()
 
+        #log.debug(_("Obtaining value for section %r, key %r") % (section, key), level=8)
+
         if self.cfg_parser.has_option(section, key):
-            return self.cfg_parser.get(section,key)
+            try:
+                return self.cfg_parser.get(section, key)
+            except:
+                self.read_config()
+                return self.cfg_parser.get(section, key)
 
         if hasattr(self, "get_%s_%s" % (section,key)):
             try:
diff --git a/pykolab/conf/defaults.py b/pykolab/conf/defaults.py
index 22e8cc4..9fd6e8b 100644
--- a/pykolab/conf/defaults.py
+++ b/pykolab/conf/defaults.py
@@ -29,6 +29,8 @@ class Defaults(object):
         # library should try to retrieve annotations
         self.cyrus_annotations_retry_interval = 1
 
-        self.address_search_attrs = "mail, alias"
+        self.address_search_attrs = ['mail', 'alias']
+        self.mail_attributes = ['mail', 'alias']
 
         self.kolab_default_locale = 'en_US'
+        self.ldap_unique_attribute = 'nsuniqueid'
\ No newline at end of file
diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py
index ee7e722..f544beb 100644
--- a/pykolab/imap/__init__.py
+++ b/pykolab/imap/__init__.py
@@ -31,8 +31,6 @@ from pykolab.translate import _
 log = pykolab.getLogger('pykolab.imap')
 conf = pykolab.getConf()
 
-auth = pykolab.auth
-
 class IMAP(object):
     def __init__(self):
         # Pool of named IMAP connections, by hostname
@@ -41,10 +39,50 @@ class IMAP(object):
         # Place holder for the current IMAP connection
         self.imap = None
 
-        self.users = []
-        self.inbox_folders = []
+    def cleanup_acls(self, aci_subject):
+        lm_suffix = ""
+
+        log.info(_("Cleaning up ACL entries for %s across all folders") % (aci_subject))
+
+        if len(aci_subject.split('@')) > 1:
+            lm_suffix = "@%s" % (aci_subject.split('@')[1])
+
+
+        shared_folders = self.imap.lm(
+                "shared/*%s" % (lm_suffix)
+            )
 
-    def connect(self, uri=None, domain=None, login=True):
+        user_folders = self.imap.lm(
+                "user/*%s" % (lm_suffix)
+            )
+
+        log.debug(
+                _("Cleaning up ACL entries referring to identifier %s") % (
+                        aci_subject
+                    ),
+                level=5
+            )
+
+        # For all folders (shared and user), ...
+        folders = user_folders + shared_folders
+
+        log.debug(_("Iterating over %d folders") % (len(folders)), level=5)
+
+        # ... loop through them and ...
+        for folder in folders:
+            # ... list the ACL entries
+            acls = self.imap.lam(folder)
+
+            # For each ACL entry, see if we think it is a current, valid entry
+            for acl_entry in acls.keys():
+                # If the key 'acl_entry' does not exist in the dictionary of valid
+                # ACL entries, this ACL entry has got to go.
+                if acl_entry == aci_subject:
+                    # Set the ACL to '' (effectively deleting the ACL entry)
+                    log.debug(_("Removing acl %r for subject %r from folder %r") % (acls[acl_entry],acl_entry,folder), level=8)
+                    self.imap.sam(folder, acl_entry, '')
+
+    def connect(self, uri=None, server=None, domain=None, login=True):
         """
             Connect to the appropriate IMAP backend.
 
@@ -64,6 +102,7 @@ class IMAP(object):
         backend = conf.get('kolab', 'imap_backend')
 
         if not domain == None:
+            self.domain = domain
             if conf.has_section(domain) and conf.has_option(domain, 'imap_backend'):
                 backend = conf.get(domain, 'imap_backend')
 
@@ -85,6 +124,9 @@ class IMAP(object):
             scheme = uri.split(':')[0]
             (hostname, port) = uri.split('/')[2].split(':')
 
+        if not server == None:
+            hostname = server
+
         if port == None:
             port = 993
 
@@ -155,117 +197,91 @@ class IMAP(object):
                 log.warning(_("Called imap.disconnect() on a server that " + \
                     "we had no connection to"))
 
+    def create_folder(self, folder_path, server=None):
+        if not server == None:
+            if not self._imap.has_key(server):
+                self.connect(server=server)
+
+            self._imap[server].cm(folder_path)
+        else:
+            self.imap.cm(folder_path)
+
     def __getattr__(self, name):
         if hasattr(self.imap, name):
             return getattr(self.imap, name)
         else:
             raise AttributeError, _("%r has no attribute %s") % (self,name)
 
-    def has_folder(self, folder):
-        folders = self.imap.lm(folder)
-        log.debug(_("Looking for folder '%s', we found folders: %r") % (folder,folders), level=8)
-        # Greater then one, this folder may have subfolders.
-        if len(folders) > 0:
-            return True
-        else:
-            return False
-
-    def move_user_folders(self, users=[], domain=None):
-        self.connect(domain=domain)
-
-        for user in users:
-            if type(user) == dict:
-                if user.has_key('old_mail'):
-                    inbox = "user/%s" % (user['mail'])
-                    old_inbox = "user/%s" % (user['old_mail'])
-
-                    if self.has_folder(old_inbox):
-                        log.debug(_("Found old INBOX folder %s") % (old_inbox), level=8)
-
-                        if not self.has_folder(inbox):
-                            log.info(_("Renaming INBOX from %s to %s") % (old_inbox,inbox))
-                            self.imap.rename(old_inbox,inbox)
-                            self.inbox_folders.append(inbox)
-                        else:
-                            log.warning(_("Moving INBOX folder %s won't succeed as target folder %s already exists") % (old_inbox,inbox))
-                    else:
-                        log.debug(_("Did not find old folder user/%s to rename") % (user['old_mail']), level=8)
-            else:
-                log.debug(_("Value for user is not a dictionary"), level=8)
-
-    def create_user_folders(self, users, primary_domain, secondary_domains):
-        self.connect(domain=primary_domain)
+    def shared_folder_create(self, folder_path, server=None):
+        """
+            Create a shared folder.
+        """
 
-        inbox_folders = []
+        folder_name = "shared%s%s" % (self.imap.separator, folder_path)
+        log.info(_("Creating new shared folder %s") %(folder_path))
+        self.create_folder(folder_name, server)
 
-        domain_section = auth.domain_section(primary_domain)
+    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))
 
-        folders = self.list_user_folders(primary_domain, secondary_domains)
+    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)
 
-        # See if the folder belongs to any of the users
-        _match_attr = conf.get('cyrus-sasl', 'result_attribute')
+    def shared_mailbox_create(self, mailbox_base_name, server=None):
+        """
+            Create a shared folder.
+        """
 
-        if not users:
-            users = auth.list_users(primary_domain)
+        folder_name = "shared%s%s" % (self.imap.separator, mailbox_base_name)
+        log.info(_("Creating new shared folder %s") %(mailbox_base_name))
+        self.create_folder(folder_name, server)
 
-        for user in users:
-            if type(user) == dict:
-                if user.has_key(_match_attr):
-                    inbox_folders.append(user[_match_attr].lower())
-                else:
-                    # If the user passed on to this function does not have
-                    # a key for _match_attr, then we have to bail out and
-                    # continue
-                    continue
+    def shared_mailbox_exists(self, mailbox_base_name):
+        """
+            Check if a shared mailbox exists.
+        """
+        return self.has_folder('shared%s%s' %(self.imap.separator, mailbox_base_name))
 
-            elif type(user) == str:
-                inbox_folders.append(user.lower())
-
-        for folder in inbox_folders:
-            additional_folders = None
-            if not self.has_folder("user%s%s" % (self.imap.separator, folder)):
-                # TODO: Perhaps this block is moot
-                log.info(_("Creating new INBOX for user (%d): %s")
-                    % (1,folder))
-                try:
-                    self.imap.cm("user%s%s" % (self.imap.separator, folder))
-                except:
-                    log.warning(
-                            _("Mailbox already exists: user%s%s") % (
-                                    self.imap.separator,folder
-                                )
-                        )
+    def user_mailbox_create(self, mailbox_base_name, server=None):
+        """
+            Create a user mailbox.
 
-                    continue
+            Returns the full path to the new mailbox folder.
+        """
+        folder_name = "user%s%s" % (self.imap.separator, mailbox_base_name)
+        log.info(_("Creating new mailbox for user %s") %(mailbox_base_name))
 
-                if conf.get('kolab', 'imap_backend') == 'cyrus-imap':
-                    self.imap._setquota(
-                            "user%s%s" % (self.imap.separator, folder),
-                            0
-                        )
+        self.create_folder(folder_name, server)
 
-                if conf.has_option(domain_section, "autocreate_folders"):
+        if not self.domain == None:
+            if conf.has_option(self.domain, "autocreate_folders"):
                     _additional_folders = conf.get_raw(
-                            domain_section,
+                            self.domain,
                             "autocreate_folders"
                         )
 
                     additional_folders = conf.plugins.exec_hook(
                             "create_user_folders",
                             kw={
-                                    'folder': folder,
+                                    'folder': folder_name,
                                     'additional_folders': _additional_folders
                                 }
                         )
 
-                if not additional_folders == None:
-                    self.create_user_additional_folders(folder, additional_folders)
+                    if not additional_folders == None:
+                        self.user_mailbox_create_additional_folders(mailbox_base_name, additional_folders)
 
-        return inbox_folders
+        return folder_name
+
+    def user_mailbox_create_additional_folders(self, folder, additional_folders):
+        log.debug(_("Creating additional folders for user %s") % (folder), level=8)
 
-    def create_user_additional_folders(self, folder, additional_folders):
         for additional_folder in additional_folders.keys():
             _add_folder = {}
+
             if len(folder.split('@')) > 1:
                 folder_name = "user%(separator)s%(username)s%(separator)s%(additional_folder_name)s@%(domainname)s"
                 _add_folder['username'] = folder.split('@')[0]
@@ -283,7 +299,7 @@ class IMAP(object):
             try:
                 self.imap.cm(folder_name)
             except:
-                log.warning(_("Mailbox already exists: user/%s") % (folder))
+                log.warning(_("Mailbox already exists: %s") % (folder_name))
 
             if additional_folders[additional_folder].has_key("annotations"):
                 for annotation in additional_folders[additional_folder]["annotations"].keys():
@@ -312,8 +328,10 @@ class IMAP(object):
 
         if len(folder.split('@')) > 1:
             domain = folder.split('@')[1]
+            domain_suffix = "@%s" % (domain)
         else:
             domain = None
+            domain_suffix = ""
 
         if not domain == None:
             if conf.has_section(domain) and conf.has_option(domain, 'imap_backend'):
@@ -324,6 +342,7 @@ class IMAP(object):
             else:
                 uri = None
 
+        log.debug(_("Subscribing user to the additional folders"), level=8)
         # Get the credentials
         admin_login = conf.get(backend, 'admin_login')
         admin_password = conf.get(backend, 'admin_password')
@@ -331,11 +350,122 @@ class IMAP(object):
         self.connect(login=False)
         self.login_plain(admin_login, admin_password, folder)
 
-        for _folder in self.lm():
+        for _folder in self.lm("%s/*%s" % (folder_name.split('@')[0],domain_suffix)):
             self.subscribe(_folder)
 
         self.logout()
 
+    def user_mailbox_delete(self, mailbox_base_name):
+        """
+            Delete a user mailbox.
+        """
+        folder = "user%s%s" %(self.imap.separator,mailbox_base_name)
+        self.delete_mailfolder(folder)
+        self.cleanup_acls(mailbox_base_name)
+
+    def user_mailbox_exists(self, mailbox_base_name):
+        """
+            Check if a user mailbox exists.
+        """
+        return self.has_folder('user%s%s' %(self.imap.separator, mailbox_base_name))
+
+    def user_mailbox_rename(self, old_name, new_name):
+        old_name = "user%s%s" % (self.imap.separator,old_name)
+        new_name = "user%s%s" % (self.imap.separator,new_name)
+
+        if old_name == new_name:
+            return
+
+        if not self.has_folder(new_name):
+            log.info(_("Renaming INBOX from %s to %s") % (old_name,new_name))
+            try:
+                self.imap.rename(old_name,new_name)
+            except:
+                log.error(_("Could not rename INBOX folder %s to %s") % (oldname,new_name))
+        else:
+            log.warning(_("Moving INBOX folder %s won't succeed as target folder %s already exists") % (old_name,new_name))
+
+    def user_mailbox_server(self, mailbox):
+        self.connect(domain=self.domain)
+        return self.imap.find_mailfolder_server(mailbox)
+
+    def has_folder(self, folder):
+        """
+            Check if the environment has a folder named folder.
+        """
+        self.connect(domain=self.domain)
+
+        folders = self.imap.lm(folder)
+        log.debug(_("Looking for folder '%s', we found folders: %r") % (folder,folders), level=8)
+        # Greater then one, this folder may have subfolders.
+        if len(folders) > 0:
+            return True
+        else:
+            return False
+
+    def _set_kolab_mailfolder_acls(self, acls):
+        if isinstance(acls, basestring):
+            acls = [ acls ]
+
+        for acl in acls:
+            exec("acl = %s" % (acl))
+            folder = acl[0]
+            subject = acl[1]
+            rights = acl[2]
+            if len(acl) == 4:
+                epoch = acl[3]
+            else:
+                epoch = (int)(time.time()) + 3600
+
+            if epoch > (int)(time.time()):
+                log.debug(
+                        _("Setting ACL rights %s for subject %s on folder " + \
+                            "%s") % (rights,subject,folder), level=8)
+
+                self.imap.sam(
+                        folder,
+                        "%s" % (subject),
+                        "%s" % (rights)
+                    )
+
+            else:
+                log.debug(
+                        _("Removing ACL rights %s for subject %s on folder " + \
+                            "%s") % (rights,subject,folder), level=8)
+
+                self.imap.sam(
+                        folder,
+                        "%s" % (subject),
+                        ""
+                    )
+
+        pass
+
+    """ Blah functions """
+
+    def move_user_folders(self, users=[], domain=None):
+        self.connect(domain=domain)
+
+        for user in users:
+            if type(user) == dict:
+                if user.has_key('old_mail'):
+                    inbox = "user/%s" % (user['mail'])
+                    old_inbox = "user/%s" % (user['old_mail'])
+
+                    if self.has_folder(old_inbox):
+                        log.debug(_("Found old INBOX folder %s") % (old_inbox), level=8)
+
+                        if not self.has_folder(inbox):
+                            log.info(_("Renaming INBOX from %s to %s") % (old_inbox,inbox))
+                            self.imap.rename(old_inbox,inbox)
+                            self.inbox_folders.append(inbox)
+                        else:
+                            log.warning(_("Moving INBOX folder %s won't succeed as target folder %s already exists") % (old_inbox,inbox))
+                    else:
+                        log.debug(_("Did not find old folder user/%s to rename") % (user['old_mail']), level=8)
+            else:
+                log.debug(_("Value for user is not a dictionary"), level=8)
+
     def set_user_folder_quota(self, users=[], primary_domain=None, secondary_domain=[], folders=[]):
         """
 
@@ -530,69 +660,6 @@ class IMAP(object):
 
         self.imap.dm(mailfolder_path)
 
-        clean_acls = False
-
-        section = False
-
-        if mbox_parts['domain']:
-            if conf.has_option(mbox_parts['domain'], 'delete_clean_acls'):
-                section = mbox_parts['domain']
-            elif conf.has_option('kolab', 'delete_clean_acls'):
-                section = 'kolab'
-        elif conf.has_option('kolab', 'delete_clean_acls'):
-            section = 'kolab'
-
-        if not section == False:
-            clean_acls = conf.get(section, 'delete_clean_acls')
-
-        if not clean_acls == False and not clean_acls == 0:
-            log.info(_("Cleaning up ACL entries across all folders"))
-
-            if mbox_parts['domain']:
-                # List the shared and user folders
-                shared_folders = self.imap.lm(
-                        "shared/*@%s" % (mbox_parts['domain'])
-                    )
-
-                user_folders = self.imap.lm(
-                        "user/*@%s" % (mbox_parts['domain'])
-                    )
-
-                aci_identifier = "%s@%s" % (
-                        mbox_parts['path_parts'][1],
-                        mbox_parts['domain']
-                    )
-
-            else:
-                shared_folders = self.imap.lm("shared/*")
-                user_folders = self.imap.lm("user/*")
-                aci_identifier = "%s" % (mbox_parts['path_parts'][1])
-
-            log.debug(
-                    _("Cleaning up ACL entries referring to identifier %s") % (
-                            aci_identifier
-                        ),
-                    level=5
-                )
-
-            # For all folders (shared and user), ...
-            folders = user_folders + shared_folders
-
-            log.debug(_("Iterating over %d folders") % (len(folders)), level=5)
-
-            # ... loop through them and ...
-            for folder in folders:
-                # ... list the ACL entries
-                acls = self.imap.lam(folder)
-
-                # For each ACL entry, see if we think it is a current, valid entry
-                for acl_entry in acls.keys():
-                    # If the key 'acl_entry' does not exist in the dictionary of valid
-                    # ACL entries, this ACL entry has got to go.
-                    if acl_entry == aci_identifier:
-                        # Set the ACL to '' (effectively deleting the ACL entry)
-                        self.imap.sam(folder, acl_entry, '')
-
     def list_user_folders(self, primary_domain=None, secondary_domains=[]):
         """
             List the INBOX folders in the IMAP backend. Returns a list of unique
diff --git a/pykolab/imap/cyrus.py b/pykolab/imap/cyrus.py
index 1d5b9b4..1eae6d7 100644
--- a/pykolab/imap/cyrus.py
+++ b/pykolab/imap/cyrus.py
@@ -29,8 +29,6 @@ from pykolab.translate import _
 log = pykolab.getLogger('pykolab.imap')
 conf = pykolab.getConf()
 
-imap = pykolab.imap
-
 class Cyrus(cyruslib.CYRUS):
     """
         Abstraction class for some common actions to do exclusively in Cyrus.
@@ -100,6 +98,7 @@ class Cyrus(cyruslib.CYRUS):
         cyruslib.CYRUS.login(self, *args, **kw)
         self.separator = self.SEP
 
+        log.debug(_("Continuing with separator: %r") % (self.separator), level=8)
         self.murder = False
 
         for capability in self.m.capabilities:
@@ -113,10 +112,7 @@ class Cyrus(cyruslib.CYRUS):
     def find_mailfolder_server(self, mailfolder):
         annotations = {}
 
-        #print "mailfolder:", mailfolder
-
         _mailfolder = self.parse_mailfolder(mailfolder)
-        #print "_mailfolder:", _mailfolder
 
         prefix = _mailfolder['path_parts'].pop(0)
         mbox = _mailfolder['path_parts'].pop(0)
@@ -198,7 +194,6 @@ class Cyrus(cyruslib.CYRUS):
             Login to the actual backend server, then set annotation.
         """
         server = self.find_mailfolder_server(mailfolder)
-        imap.connect(self.uri.replace(self.server,server))
 
         log.debug(_("Setting annotation %s on folder %s") % (annotation,mailfolder), level=8)
 
diff --git a/pykolab/plugins/recipientpolicy/__init__.py b/pykolab/plugins/recipientpolicy/__init__.py
index 6621947..ab72980 100644
--- a/pykolab/plugins/recipientpolicy/__init__.py
+++ b/pykolab/plugins/recipientpolicy/__init__.py
@@ -54,7 +54,7 @@ class KolabRecipientpolicy(object):
             Return the new primary mail address
         """
 
-        user_attrs = utils.normalize(kw['user_attrs'])
+        user_attrs = utils.normalize(kw['entry'])
 
         if not user_attrs.has_key('domain'):
             user_attrs['domain'] = kw['primary_domain']
@@ -84,7 +84,7 @@ class KolabRecipientpolicy(object):
             Return a list of secondary mail addresses
         """
 
-        user_attrs = utils.normalize(kw['user_attrs'])
+        user_attrs = utils.normalize(kw['entry'])
 
         if not user_attrs.has_key('domain'):
             user_attrs['domain'] = kw['primary_domain']
diff --git a/pykolab/wap_client/__init__.py b/pykolab/wap_client/__init__.py
index 9c3a245..31cfb57 100644
--- a/pykolab/wap_client/__init__.py
+++ b/pykolab/wap_client/__init__.py
@@ -193,6 +193,9 @@ def request(method, api_uri, params=None, headers={}):
     else:
         return response_data['result']
 
+def role_capabilities():
+    return request('GET', 'role.capabilities')
+
 def system_capabilities():
     return request('GET', 'system.capabilities')
 
@@ -261,6 +264,78 @@ def user_form_value_generate_mail(params=None):
 def form_value_generate_password(*args, **kw):
     return request('GET', 'form_value.generate_password')
 
+def form_value_list_options(attribute_name, *args, **kw):
+    params = json.dumps({'attribute': attribute_name})
+
+    return request('POST', 'form_value.list_options', params)
+
+def form_value_select_options(attribute_name, *args, **kw):
+    params = json.dumps({'attributes': [attribute_name]})
+
+    return request('POST', 'form_value.select_options', params)
+
+def role_find_by_attribute(params=None):
+    if params == None:
+        role_name = utils.ask_question("Role name")
+    else:
+        role_name = params['cn']
+
+    role = request('GET', 'role.find_by_attribute?cn=%s' % (role_name))
+
+    return role
+
+def role_add(params=None):
+    if params == None:
+        role_name = utils.ask_question("Role name")
+        params = {
+                'cn': role_name
+            }
+
+    params = json.dumps(params)
+
+    return request('POST', 'role.add', params)
+
+def role_delete(params=None):
+    if params == None:
+        role_name = utils.ask_question("Role name")
+        role = role_find_by_attribute({'cn': role_name})
+        params = {
+                'role': role.keys()[0]
+            }
+
+    if not params.has_key('role'):
+        role = role_find_by_attribute(params)
+        params = {
+                'role': role.keys()[0]
+            }
+
+    params = json.dumps(params)
+
+    return request('POST', 'role.delete', params)
+
+def role_info(params=None):
+    if params == None:
+        role_name = utils.ask_question("Role name")
+        role = role_find_by_attribute({'cn': role_name})
+        params = {
+                'role': role
+            }
+
+    if not params.has_key('role'):
+        role = role_find_by_attribute(params)
+        params = {
+                'role': role
+            }
+
+    print role
+
+    role = request('GET', 'role.info?role=%s' % (params['role'].keys()[0]))
+
+    return role
+
+def roles_list():
+    return request('GET', 'roles.list')
+
 def user_form_value_generate_uid(params=None):
     if params == None:
         params = get_user_input()


commit 1a2d4b809307316a59961811d3f252e0b50477fe
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Apr 19 16:58:46 2012 +0100

    Add systemd file for kolab-saslauthd

diff --git a/saslauthd/Makefile.am b/saslauthd/Makefile.am
index 1574115..21ae0a5 100644
--- a/saslauthd/Makefile.am
+++ b/saslauthd/Makefile.am
@@ -1,5 +1,6 @@
 EXTRA_DIST = \
 	kolab-saslauthd.sysconfig \
+	kolab-saslauthd.systemd \
 	kolab-saslauthd.sysvinit
 
 saslauthddir = $(pythondir)/saslauthd
@@ -7,6 +8,7 @@ saslauthd_PYTHON = $(wildcard *.py)
 
 install-exec-local:
 	mkdir -p $(DESTDIR)/$(localstatedir)/run/kolab-saslauthd \
+		$(DESTDIR)/$(localstatedir)/run/kolab-saslauthd \
 		$(DESTDIR)/$(localstatedir)/run/saslauthd \
 		$(DESTDIR)/$(localstatedir)/lib/kolab-saslauthd
 
diff --git a/saslauthd/kolab-saslauthd.systemd b/saslauthd/kolab-saslauthd.systemd
new file mode 100644
index 0000000..a39073c
--- /dev/null
+++ b/saslauthd/kolab-saslauthd.systemd
@@ -0,0 +1,15 @@
+[Unit]
+Description=Kolab Groupware SASL Authentication Daemon.
+After=syslog.target network.target
+
+[Service]
+Type=forking
+PIDFile=/var/run/kolab-saslauthd/kolab-saslauthd.pid
+EnvironmentFile=/etc/sysconfig/kolab-saslauthd
+ExecStart=/usr/sbin/kolab-saslauthd $FLAGS
+ExecReload=/bin/kill -HUP $MAINPID
+ExecStop=/bin/kill -TERM $MAINPID
+
+[Install]
+WantedBy=multi-user.target
+


commit 5d4c3e69fd9ed03703f2ccc7feb46396f0e6bad9
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Apr 19 16:57:56 2012 +0100

    Provide some preliminary setup.

diff --git a/pykolab/setup/components.py b/pykolab/setup/components.py
index 5e22b73..432844b 100644
--- a/pykolab/setup/components.py
+++ b/pykolab/setup/components.py
@@ -30,6 +30,7 @@ conf = pykolab.getConf()
 
 components = {}
 component_groups = {}
+executed_components = []
 
 executed_components = []
 
@@ -206,7 +207,7 @@ def register_group(dirname, module):
                 exec("from %s.%s import __init__ as %s_%s_register" % (module,module_name,module,component_name))
                 exec("%s_%s_register()" % (module,component_name))
 
-def register(component_name, func, group=None, description=None, after=[], before=[], aliases=[]):
+def register(component_name, func, group=None, description=None, aliases=[], after=[], before=[]):
     if not group == None:
         component = "%s_%s" % (group,component_name)
     else:
diff --git a/pykolab/setup/setup_imap.py b/pykolab/setup/setup_imap.py
new file mode 100644
index 0000000..5731be9
--- /dev/null
+++ b/pykolab/setup/setup_imap.py
@@ -0,0 +1,136 @@
+# -*- 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.
+#
+
+from augeas import Augeas
+import os
+import subprocess
+
+import components
+
+import pykolab
+
+from pykolab import utils
+from pykolab.constants import *
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.setup')
+conf = pykolab.getConf()
+
+def __init__():
+    components.register('imap', execute, description=description(), after=['ldap'])
+
+def description():
+    return _("Setup IMAP.")
+
+def execute(*args, **kw):
+    """
+        Apply the necessary settings to /etc/imapd.conf
+    """
+
+    imapd_settings = {
+            "sasl_pwcheck_method": "auxprop saslauthd",
+            "sasl_mech_list": "PLAIN LOGIN",
+            "auth_mech": "pts",
+            "pts_module": "ldap",
+            "ldap_servers": conf.get('ldap', 'ldap_uri'),
+            "ldap_sasl": "0",
+            "ldap_base_dn": conf.get('ldap', 'base_dn'),
+            "ldap_bind_dn": conf.get('ldap', 'service_bind_dn'),
+            "ldap_password": conf.get('ldap', 'service_bind_pw'),
+            "ldap_filter": '(|(&(|(uid=cyrus-admin)(uid=cyrus-murder))(uid=%U))(&(|(uid=%U)(mail=%U@%d)(mail=%U@%r))(objectclass=kolabinetorgperson)))',
+            "ldap_user_attribute": conf.get('cyrus-sasl', 'result_attribute'),
+            "ldap_group_base": conf.get('ldap', 'base_dn'),
+            "ldap_group_filter": "(&(cn=%u)(objectclass=ldapsubentry)(objectclass=nsroledefinition))",
+            "ldap_group_scope": "one",
+            "ldap_member_base": conf.get('ldap','user_base_dn'),
+            "ldap_member_method": "attribute",
+            "ldap_member_attribute": "nsrole",
+            "ldap_restart": "1",
+            "ldap_timeout": "10",
+            "ldap_time_limit": "10",
+            "unixhierarchysep": "1",
+            "virt_domains": "userid",
+            "admins": "cyrus-imapd",
+            "annotation_definitions": "/etc/imapd.annotations.conf",
+            "sieve_extensions": "fileinto reject vacation imapflags notify envelope include relational regex subaddress copy",
+            "allowallsubscribe": "0",
+            "allowusermoves": "1",
+            "altnamespace": "1",
+            "hashimapspool": "1",
+            "anysievefolder": "1",
+            "fulldirhash": "0",
+            "sieveusehomedir": "0",
+            "sieve_allowreferrals": "0",
+            "lmtp_downcase_rcpt": "1",
+            "lmtp_fuzzy_mailbox_match": "1",
+            "username_to_lower": "1",
+            "normalizeuid": "1",
+            "deletedprefix": "DELETED",
+            "delete_mode": "delayed",
+            "expunge_mode": "delayed",
+            "flushseenstate": "1",
+            "virt_domains": "userid",
+        }
+
+    myaugeas = Augeas()
+
+    setting_base = '/files/etc/imapd.conf/'
+    for setting_key in imapd_settings.keys():
+        setting = os.path.join(setting_base,setting_key)
+        current_value = myaugeas.get(setting)
+
+        if current_value == None:
+            insert_paths = myaugeas.match('/files/etc/imapd.conf/*')
+            insert_path = insert_paths[(len(insert_paths)-1)]
+            myaugeas.insert(insert_path, setting_key, False)
+
+        log.debug(_("Setting key %r to %r") % (setting_key, imapd_settings[setting_key]), level=8)
+        myaugeas.set(setting, imapd_settings[setting_key])
+
+    myaugeas.save()
+
+    annotations = [
+            "/vendor/horde/share-params,mailbox,string,backend,value.shared value.priv,a",
+            "/vendor/kolab/color,mailbox,string,backend,value.shared value.priv,a",
+            "/vendor/kolab/folder-test,mailbox,string,backend,value.shared value.priv,a",
+            "/vendor/kolab/folder-type,mailbox,string,backend,value.shared value.priv,a",
+            "/vendor/kolab/incidences-for,mailbox,string,backend,value.shared value.priv,a",
+            "/vendor/kolab/pxfb-readable-for,mailbox,string,backend,value.shared value.priv,a",
+            "/vendor/kolab/h-share-attr-desc,mailbox,string,backend,value.shared value.priv,a",
+            "/vendor/kolab/activesync,mailbox,string,backend,value.priv,r",
+            "/vendor/x-toltec/test,mailbox,string,backend,value.shared value.priv,a",
+        ]
+
+    fp = open('/etc/imapd.annotations.conf', 'w')
+    fp.write("\n".join(annotations))
+    fp.close()
+
+    if os.path.isfile('/bin/systemctl'):
+        subprocess.call(['systemctl', 'restart', 'cyrus-imapd.service'])
+        subprocess.call(['systemctl', 'enable', 'cyrus-imapd.service'])
+        subprocess.call(['systemctl', 'restart', 'kolab-saslauthd.service'])
+        subprocess.call(['systemctl', 'enable', 'kolab-saslauthd.service'])
+    elif os.path.isfile('/sbin/service'):
+        subprocess.call(['service', 'cyrus-imapd', 'restart'])
+        subprocess.call(['chkconfig', 'cyrus-imapd', 'on'])
+        subprocess.call(['service', 'kolab-saslauthd', 'restart'])
+        subprocess.call(['chkconfig', 'kolab-saslauthd', 'on'])
+    else:
+        log.error(_("Could not start and configure to start on boot, the " + \
+                "cyrus-imapd and kolab-saslauthd services."))
diff --git a/pykolab/setup/setup_ldap.py b/pykolab/setup/setup_ldap.py
index 9619e1a..52e96ef 100644
--- a/pykolab/setup/setup_ldap.py
+++ b/pykolab/setup/setup_ldap.py
@@ -29,6 +29,7 @@ import components
 import pykolab
 
 from pykolab import utils
+from pykolab.auth import Auth
 from pykolab.constants import *
 from pykolab.translate import _
 
@@ -42,22 +43,34 @@ def description():
     return _("Setup LDAP.")
 
 def execute(*args, **kw):
+    ask_questions = True
+
+    if not conf.config_file == conf.defaults.config_file:
+        ask_questions = False
+
     _input = {}
 
-    _input['admin_pass'] = utils.ask_question(
-            _("Administrator password"),
-            default=utils.generate_password(),
-            password=True
-        )
+    if ask_questions:
+        _input['admin_pass'] = utils.ask_question(
+                _("Administrator password"),
+                default=utils.generate_password(),
+                password=True
+            )
 
-    _input['dirmgr_pass'] = utils.ask_question(
-            _("Directory Manager password"),
-            default=utils.generate_password(),
-            password=True
-        )
+        _input['dirmgr_pass'] = utils.ask_question(
+                _("Directory Manager password"),
+                default=utils.generate_password(),
+                password=True
+            )
+
+        _input['userid'] = utils.ask_question(_("User"), default="nobody")
+        _input['group'] = utils.ask_question(_("Group"), default="nobody")
 
-    _input['userid'] = utils.ask_question(_("User"), default="nobody")
-    _input['group'] = utils.ask_question(_("Group"), default="nobody")
+    else:
+        _input['admin_pass'] = conf.get('ldap', 'bind_pw')
+        _input['dirmgr_pass'] = conf.get('ldap', 'bind_pw')
+        _input['userid'] = "nobody"
+        _input['group'] = "nobody"
 
     _input['fqdn'] = fqdn
     _input['hostname'] = hostname.split('.')[0]
@@ -130,19 +143,32 @@ ServerAdminPwd = %(admin_pass)s
     else:
         log.warning(_("Could not find the Kolab schema file"))
 
-    subprocess.call(['service', 'dirsrv@%s' % (_input['hostname']), 'restart'])
+    if os.path.isfile('/bin/systemctl'):
+        subprocess.call(['/bin/systemctl', 'restart', 'dirsrv.target'])
+        subprocess.call(['/bin/systemctl', 'enable', 'dirsrv.target'])
+    elif os.path.isfile('/sbin/service'):
+        subprocess.call(['/sbin/service', 'dirsrv', 'restart'])
+        subprocess.call(['/sbin/chkconfig', 'dirsrv', 'on'])
+    else:
+        log.error(_("Could not start and configure to start on boot, the " + \
+                "directory server service."))
+
+    if ask_questions:
+        _input['cyrus_admin_pass'] = utils.ask_question(
+                _("Cyrus Administrator password"),
+                default=utils.generate_password(),
+                password=True
+            )
 
-    _input['cyrus_admin_pass'] = utils.ask_question(
-            _("Cyrus Administrator password"),
-            default=utils.generate_password(),
-            password=True
-        )
+        _input['kolab_service_pass'] = utils.ask_question(
+                _("Kolab Service password"),
+                default=utils.generate_password(),
+                password=True
+            )
 
-    _input['kolab_service_pass'] = utils.ask_question(
-            _("Kolab Service password"),
-            default=utils.generate_password(),
-            password=True
-        )
+    else:
+        _input['cyrus_admin_pass'] = conf.get('cyrus-imap', 'admin_password')
+        _input['kolab_service_pass'] = conf.get('ldap', 'service_bind_pw')
 
     # Write out kolab configuration
     conf.command_set('kolab', 'primary_domain', _input['domain'])
@@ -153,9 +179,9 @@ ServerAdminPwd = %(admin_pass)s
     conf.command_set('ldap', 'service_bind_pw', _input['kolab_service_pass'])
 
     # Insert service users
-    auth = pykolab.auth
+    auth = Auth(_input['domain'])
     auth.connect()
-    auth._auth._connect()
+    auth._auth.connect()
     auth._auth._bind()
 
     dn = 'uid=cyrus-admin,ou=Special Users,%s' % (_input['rootdn'])
@@ -207,12 +233,14 @@ ServerAdminPwd = %(admin_pass)s
     # Do the actual synchronous add-operation to the ldapserver
     auth._auth.ldap.add_s(dn, ldif)
 
-    auth._auth._set_user_attribute(
+    auth._auth.set_entry_attribute(
             dn,
             'aci',
             '(targetattr = "*") (version 3.0;acl "Kolab Services";allow (read,compare,search)(userdn = "ldap:///%s");)' % ('uid=kolab-service,ou=Special Users,%s' % (_input['rootdn']))
         )
 
+    # TODO: Add kolab-admin role
+    # TODO: Assign kolab-admin admin ACLs
     # TODO: Add the primary domain to cn=kolab,cn=config
     dn = "associateddomain=%s,cn=kolab,cn=config" % (domainname)
     attrs = {}
@@ -278,3 +306,11 @@ ServerAdminPwd = %(admin_pass)s
     modlist = []
     modlist.append((ldap.MOD_REPLACE, "aci", aci))
     auth._auth.ldap.modify_s(dn, modlist)
+
+    if os.path.isfile('/bin/systemctl'):
+        subprocess.call(['/bin/systemctl', 'enable', 'dirsrv-admin.service'])
+    elif os.path.isfile('/sbin/service'):
+        subprocess.call(['/sbin/chkconfig', 'dirsrv-admin', 'on'])
+    else:
+        log.error(_("Could not start and configure to start on boot, the " + \
+                "directory server admin service."))
diff --git a/pykolab/setup/setup_mta.py b/pykolab/setup/setup_mta.py
new file mode 100644
index 0000000..b3d5175
--- /dev/null
+++ b/pykolab/setup/setup_mta.py
@@ -0,0 +1,225 @@
+# -*- 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.
+#
+
+from augeas import Augeas
+import os
+import subprocess
+
+import components
+
+import pykolab
+
+from pykolab import utils
+from pykolab.constants import *
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.setup')
+conf = pykolab.getConf()
+
+def __init__():
+    components.register('mta', execute, description=description(), after=['ldap'])
+
+def description():
+    return _("Setup MTA.")
+
+def execute(*args, **kw):
+
+    group_filter = conf.get('ldap','kolab_group_filter')
+    if group_filter == None:
+        group_filter = conf.get('ldap','group_filter')
+
+    user_filter = conf.get('ldap','kolab_user_filter')
+    if user_filter == None:
+        user_filter = conf.get('ldap','user_filter')
+
+    files = {
+            "/etc/postfix/ldap/local_recipients_maps.cf": """
+server_host = localhost
+server_port = 389
+version = 3
+search_base = %(base_dn)s
+scope = sub
+
+domain = ldap:/etc/postfix/ldap/mydestination.cf
+
+bind_dn = %(service_bind_dn)s
+bind_pw = %(service_bind_pw)s
+
+query_filter = (|(&(|(mail=%%s)(alias=%%s))%(kolab_user_filter)s)%(kolab_group_filter)s)
+result_attribute = mail
+""" % {
+                        "base_dn": conf.get('ldap', 'base_dn'),
+                        "service_bind_dn": conf.get('ldap', 'service_bind_dn'),
+                        "service_bind_pw": conf.get('ldap', 'service_bind_pw'),
+                        "kolab_user_filter": user_filter,
+                        "kolab_group_filter": group_filter,
+                    },
+            "/etc/postfix/ldap/mydestination.cf": """
+server_host = localhost
+server_port = 389
+version = 3
+search_base = %(domain_base_dn)s
+scope = sub
+
+bind_dn = %(service_bind_dn)s
+bind_pw = %(service_bind_pw)s
+
+query_filter = %(domain_filter)s
+result_attribute = %(domain_name_attribute)s
+""" % {
+                        "domain_base_dn": conf.get('ldap', 'domain_base_dn'),
+                        "domain_filter": conf.get('ldap', 'domain_filter').replace('*', '%s'),
+                        "domain_name_attribute": conf.get('ldap', 'domain_name_attribute'),
+                        "service_bind_dn": conf.get('ldap', 'service_bind_dn'),
+                        "service_bind_pw": conf.get('ldap', 'service_bind_pw'),
+                    },
+            "/etc/postfix/ldap/mailenabled_distgroup.cf": """
+server_host = localhost
+server_port = 389
+version = 3
+search_base = %(group_base_dn)s
+scope = sub
+
+domain = ldap:/etc/postfix/ldap/mydestination.cf
+
+bind_dn = %(service_bind_dn)s
+bind_pw = %(service_bind_pw)s
+
+# This finds the mail enabled distribution group LDAP entry
+query_filter = (&(mail=%%s)(objectClass=kolabgroupofuniquenames)(objectclass=groupofuniquenames))
+# From this type of group, get all uniqueMember DNs
+special_result_attribute = uniqueMember
+# Only from those DNs, get the mail
+result_attribute =
+leaf_result_attribute = mail
+""" % {
+                        "group_base_dn": conf.get('ldap', 'group_base_dn'),
+                        "service_bind_dn": conf.get('ldap', 'service_bind_dn'),
+                        "service_bind_pw": conf.get('ldap', 'service_bind_pw'),
+                    },
+            "/etc/postfix/ldap/mailenabled_dynamic_distgroup.cf": """
+server_host = localhost
+server_port = 389
+version = 3
+search_base = %(group_base_dn)s
+scope = sub
+
+domain = ldap:/etc/postfix/ldap/mydestination.cf
+
+bind_dn = %(service_bind_dn)s
+bind_pw = %(service_bind_pw)s
+
+# This finds the mail enabled dynamic distribution group LDAP entry
+query_filter = (&(mail=%%s)(objectClass=kolabgroupofuniquenames)(objectClass=groupOfURLs))
+# From this type of group, get all memberURL searches/references
+special_result_attribute = memberURL
+# Only from those DNs, get the mail
+result_attribute =
+leaf_result_attribute = mail
+""" % {
+                        "group_base_dn": conf.get('ldap', 'group_base_dn'),
+                        "service_bind_dn": conf.get('ldap', 'service_bind_dn'),
+                        "service_bind_pw": conf.get('ldap', 'service_bind_pw'),
+                    },
+            "/etc/postfix/ldap/transport_maps.cf": """
+server_host = localhost
+server_port = 389
+version = 3
+search_base = %(base_dn)s
+scope = sub
+
+domain = ldap:/etc/postfix/ldap/mydestination.cf
+
+bind_dn = %(service_bind_dn)s
+bind_pw = %(service_bind_pw)s
+
+query_filter = (&(|(mailAlternateAddress=%%s)(alias=%%s)(mail=%%s))(objectclass=kolabinetorgperson))
+result_attribute = mail
+result_format = lmtp:unix:/var/lib/imap/socket/lmtp.sock
+""" % {
+                        "base_dn": conf.get('ldap', 'base_dn'),
+                        "service_bind_dn": conf.get('ldap', 'service_bind_dn'),
+                        "service_bind_pw": conf.get('ldap', 'service_bind_pw'),
+                    },
+            "/etc/postfix/ldap/virtual_alias_maps.cf": """
+server_host = localhost
+server_port = 389
+version = 3
+search_base = %(base_dn)s
+scope = sub
+
+domain = ldap:/etc/postfix/ldap/mydestination.cf
+
+bind_dn = %(service_bind_dn)s
+bind_pw = %(service_bind_pw)s
+
+search_filter = (&(|(mail=%%s)(alias=%%s))(objectclass=kolabinetorgperson))
+result_attribute = mail
+""" % {
+                        "base_dn": conf.get('ldap', 'base_dn'),
+                        "service_bind_dn": conf.get('ldap', 'service_bind_dn'),
+                        "service_bind_pw": conf.get('ldap', 'service_bind_pw'),
+                    },
+        }
+
+    if not os.path.isdir('/etc/postfix/ldap'):
+        os.mkdir('/etc/postfix/ldap/', 0770)
+
+    for filename in files.keys():
+        fp = open(filename, 'w')
+        fp.write(files[filename])
+        fp.close()
+
+    postfix_main_settings = {
+            "local_recipient_maps": "ldap:/etc/postfix/ldap/local_recipient_maps.cf",
+            "mydestination": "ldap:/etc/postfix/ldap/mydestination.cf",
+            "transport_maps": "ldap:/etc/postfix/ldap/transport_maps.cf",
+            "virtual_alias_maps": "virtual_alias_maps = $alias_maps, ldap:/etc/postfix/ldap/virtual_alias_maps.cf, ldap:/etc/postfix/ldap/mailenabled_distgroups.cf, ldap:/etc/postfix/ldap/mailenabled_dynamic_distgroups.cf",
+            "smtpd_tls_auth_only": "no",
+            "smtpd_tls_cert_file": "/etc/pki/tls/certs/localhost.crt",
+            "smtpd_tls_key_file": "/etc/pki/tls/private/localhost.key",
+            "smtpd_recipient_restrictions": "permit_mynetworks, reject_unauth_pipelining, reject_rbl_client zen.spamhaus.org, reject_non_fqdn_recipient, reject_invalid_helo_hostname, reject_unknown_recipient_domain, reject_unauth_destination, check_policy_service unix:private/recipient_policy_incoming, permit",
+            "smtpd_sender_restrictions": "permit_mynetworks, check_policy_service unix:private/sender_policy_incoming",
+            "submission_recipient_restrictions": "check_policy_service unix:private/submission_policy, permit_sasl_authenticated, reject",
+            "submission_sender_restrictions": "reject_non_fqdn_sender, check_policy_service unix:private/submission_policy, permit_sasl_authenticated, reject",
+            "submission_data_restrictions": "check_policy_service unix:private/submission_policy",
+            "content-filter": "smtp-amavis:[127.0.0.1]:10024"
+
+        }
+
+    myaugeas = Augeas()
+
+    setting_base = '/files/etc/postfix/main.cf/'
+
+    for setting_key in postfix_main_settings.keys():
+        setting = os.path.join(setting_base,setting_key)
+        current_value = myaugeas.get(setting)
+
+        if current_value == None:
+            insert_paths = myaugeas.match('/files/etc/postfix/main.cf/*')
+            insert_path = insert_paths[(len(insert_paths)-1)]
+            myaugeas.insert(insert_path, setting_key, False)
+
+        log.debug(_("Setting key %r to %r") % (setting_key, postfix_main_settings[setting_key]), level=8)
+        myaugeas.set(setting, postfix_main_settings[setting_key])
+
+    myaugeas.save()
+
+    subprocess.call(['service', 'postfix', 'restart'])
+
diff --git a/pykolab/setup/setup_roundcube.py b/pykolab/setup/setup_roundcube.py
new file mode 100644
index 0000000..28d8be8
--- /dev/null
+++ b/pykolab/setup/setup_roundcube.py
@@ -0,0 +1,40 @@
+# -*- 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 os
+
+import components
+
+import pykolab
+
+from pykolab import utils
+from pykolab.constants import *
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.setup')
+conf = pykolab.getConf()
+
+def __init__():
+    components.register('roundcube', execute, description=description())
+
+def description():
+    return _("Setup Roundcube.")
+
+def execute(*args, **kw):
+    pass
\ No newline at end of file


commit 98237c3fa335d54469f435761689be2b7a4cbc8a
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Apr 19 16:57:13 2012 +0100

    Merge

diff --git a/pykolab/setup/__init__.py b/pykolab/setup/__init__.py
index 74d410b..cffe6b4 100644
--- a/pykolab/setup/__init__.py
+++ b/pykolab/setup/__init__.py
@@ -24,13 +24,13 @@ import pykolab
 log = pykolab.getLogger('pykolab.setup')
 conf = pykolab.getConf()
 
+to_execute = []
+
 class Setup(object):
     def __init__(self):
         import components
         components.__init__()
 
-        to_execute = []
-
         arg_num = 0
         for arg in sys.argv[1:]:
             arg_num += 1
@@ -38,7 +38,5 @@ class Setup(object):
                 if components.components.has_key(sys.argv[arg_num].replace('-','_')):
                     to_execute.append(sys.argv[arg_num].replace('-','_'))
 
-        components.execute('_'.join(to_execute))
-
     def run(self):
-        pass
+        components.execute('_'.join(to_execute))


commit 0a993d4b3c0b3afe76ed07d177b891791f6ae94f
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Apr 19 16:56:16 2012 +0100

    Add kolabd systemd file
    Refactor process management in kolabd

diff --git a/kolabd/Makefile.am b/kolabd/Makefile.am
index 95cd792..f8e4ca8 100644
--- a/kolabd/Makefile.am
+++ b/kolabd/Makefile.am
@@ -1,5 +1,6 @@
 EXTRA_DIST = \
 	kolabd.sysconfig \
+	kolabd.systemd \
 	kolabd.sysvinit
 
 kolabddir = $(pythondir)/kolabd
diff --git a/kolabd/__init__.py b/kolabd/__init__.py
index ec05238..2d3f94f 100644
--- a/kolabd/__init__.py
+++ b/kolabd/__init__.py
@@ -18,9 +18,6 @@
 
 """
     The Kolab daemon.
-
-    TODO: Write a pid file, check the pid file has a valid pid, and
-    consider providing an option to specify the pid file path.
 """
 
 import os
@@ -35,16 +32,15 @@ from pykolab.auth import Auth
 from pykolab import constants
 from pykolab.translate import _
 
-log = pykolab.getLogger('kolabd')
+from process import KolabdProcess as Process
+
+log = pykolab.getLogger('pykolab.daemon')
 conf = pykolab.getConf()
 
 class KolabDaemon(object):
     def __init__(self):
         """
-            self.args == Arguments passed on the CLI
-            self.cli_options == Parser results (again, CLI)
-            self.parser == The actual Parser (from OptionParser)
-            self.plugins == Our Kolab Plugins
+            The main Kolab Groupware daemon process.
         """
 
         daemon_group = conf.add_cli_parser_option_group(_("Daemon Options"))
@@ -63,18 +59,14 @@ class KolabDaemon(object):
 
         conf.finalize_conf()
 
-        self.thread_count = 0
-
     def run(self):
         """Run Forest, RUN!"""
 
         exitcode = 0
 
-        # TODO: Add a nosync option
         try:
             pid = 1
             if conf.fork_mode:
-                self.thread_count += 1
                 pid = os.fork()
 
             if pid == 0:
@@ -87,9 +79,11 @@ class KolabDaemon(object):
 
         except SystemExit, errcode:
             exitcode = errcode
+
         except KeyboardInterrupt:
             exitcode = 1
             log.info(_("Interrupted by user"))
+
         except AttributeError, errmsg:
             exitcode = 1
             traceback.print_exc()
@@ -100,6 +94,7 @@ class KolabDaemon(object):
             exitcode = 1
             traceback.print_exc()
             log.error(_("Type Error: %s") % errmsg)
+
         except:
             exitcode = 2
             traceback.print_exc()
@@ -113,72 +108,48 @@ class KolabDaemon(object):
 
         pid = os.getpid()
 
+        primary_domain = conf.get('kolab', 'primary_domain')
+
         while 1:
-            primary_auth = Auth()
+            primary_auth = Auth(primary_domain)
 
             log.debug(_("Listing domains..."), level=5)
 
             start = time.time()
+
             try:
                 domains = primary_auth.list_domains()
             except:
                 time.sleep(60)
                 continue
 
-            if len(domains) == len(domain_auth.keys()):
+            # domains now is a list of tuples, we want the primary_domains
+            primary_domains = []
+            for primary_domain, secondary_domains in domains:
+                primary_domains.append(primary_domain)
+
+            # Now we can check if any changes happened.
+            added_domains = []
+            removed_domains = []
+
+            all_domains = set(primary_domains + domain_auth.keys())
+
+            for domain in all_domains:
+                if domain in domain_auth.keys() and domain in primary_domains:
+                    continue
+                elif domain in domain_auth.keys():
+                    removed_domains.append(domain)
+                else:
+                    added_domains.append(domain)
+
+            if len(removed_domains) == 0 and len(added_domains) == 0:
                 time.sleep(600)
-            end = time.time()
 
-            log.debug(
-                    _("Found %d domains in %d seconds") % (
-                            len(domains),
-                            (end-start)
-                        ),
-                    level=8
-                )
+            log.debug(_("added domains: %r, removed domains: %r") % (added_domains, removed_domains), level=8)
 
-            for primary_domain, secondary_domains in domains:
-                log.debug(
-                        _("Running for domain %s") % (
-                                primary_domain
-                            ),
-                        level=5
-                    )
-
-                if not pid == 0 and not domain_auth.has_key(primary_domain):
-                    log.debug(
-                            _("Domain %s did not have a key yet") % (
-                                    primary_domain
-                                ),
-                            level=5
-                        )
-
-                    domain_auth[primary_domain] = Auth()
-                    # TODO: Consider threading for domain name space specific
-                    # Authn/Authz operations.
-                    pid = os.fork()
-                    if pid == 0:
-                        domain_auth[primary_domain].connect(primary_domain)
-                        start_time = time.time()
-                        domain_auth[primary_domain].synchronize(
-                                primary_domain,
-                                secondary_domains
-                            )
-
-                        end_time = time.time()
-
-                        log.info(
-                                _("Synchronizing users for %s took %d seconds")
-                                    % (
-                                        primary_domain,
-                                        (end_time-start_time)
-                                    )
-                            )
-
-                        domain_auth[primary_domain].synchronize(
-                                primary_domain,
-                                secondary_domains
-                            )
+            for domain in added_domains:
+                domain_auth[domain] = Process(domain)
+                domain_auth[domain].start()
 
     def reload_config(self, *args, **kw):
         pass
diff --git a/kolabd/kolabd.systemd b/kolabd/kolabd.systemd
new file mode 100644
index 0000000..80305f8
--- /dev/null
+++ b/kolabd/kolabd.systemd
@@ -0,0 +1,15 @@
+[Unit]
+Description=Kolab Groupware Server.
+After=syslog.target network.target
+
+[Service]
+Type=forking
+PIDFile=/var/run/kolabd/kolabd.pid
+EnvironmentFile=/etc/sysconfig/kolabd
+ExecStart=/usr/sbin/kolabd $FLAGS
+ExecReload=/bin/kill -HUP $MAINPID
+ExecStop=/bin/kill -TERM $MAINPID
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/kolabd/process.py b/kolabd/process.py
new file mode 100644
index 0000000..bf65623
--- /dev/null
+++ b/kolabd/process.py
@@ -0,0 +1,37 @@
+# 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 multiprocessing
+import time
+
+import pykolab
+from pykolab.auth import Auth
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.daemon')
+conf = pykolab.getConf()
+
+class KolabdProcess(multiprocessing.Process):
+    def __init__(self, domain):
+        self.domain = domain
+        multiprocessing.Process.__init__(self, target=self.synchronize, args=(domain,))
+
+    def synchronize(self, domain):
+        auth = Auth(domain)
+        auth.connect(domain)
+        auth.synchronize()


commit 9663ff263d90af9156112f46c9d7b1651664cb0f
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Apr 19 16:55:14 2012 +0100

    Wallace: Add pid file option and make sure we write it out and handle signals

diff --git a/wallace/__init__.py b/wallace/__init__.py
index c85e0fe..6677613 100644
--- a/wallace/__init__.py
+++ b/wallace/__init__.py
@@ -53,7 +53,15 @@ class WallaceDaemon(object):
             )
 
         daemon_group.add_option(
-                "-p", "--port",
+                "-p", "--pid-file",
+                dest    = "pidfile",
+                action  = "store",
+                default = "/var/run/wallaced/wallaced.pid",
+                help    = _("Path to the PID file to use.")
+            )
+
+        daemon_group.add_option(
+                "--port",
                 dest    = "wallace_port",
                 action  = "store",
                 default = 10026,
@@ -187,6 +195,8 @@ class WallaceDaemon(object):
             pid = 1
             if conf.fork_mode:
                 self.thread_count += 1
+                self.write_pid()
+                self.set_signal_handlers()
                 pid = os.fork()
 
             if pid == 0:
@@ -421,3 +431,22 @@ class WallaceDaemon(object):
                 traceback.print_exc()
                 s.shutdown(1)
                 s.close()
+
+    def reload_config(self, *args, **kw):
+        pass
+
+    def remove_pid(self, *args, **kw):
+        if os.access(conf.pidfile, os.R_OK):
+            os.remove(conf.pidfile)
+        raise SystemExit
+
+    def set_signal_handlers(self):
+        import signal
+        signal.signal(signal.SIGHUP, self.reload_config)
+        signal.signal(signal.SIGTERM, self.remove_pid)
+
+    def write_pid(self):
+        pid = os.getpid()
+        fp = open(conf.pidfile,'w')
+        fp.write("%d\n" % (pid))
+        fp.close()


commit 940d3985175be8ba977faf5559800963c81c64d6
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Apr 19 16:55:00 2012 +0100

    Add the sources for Wallace

diff --git a/wallace/Makefile.am b/wallace/Makefile.am
index 65f107c..3fcef69 100644
--- a/wallace/Makefile.am
+++ b/wallace/Makefile.am
@@ -1,3 +1,14 @@
+EXTRA_DIST = \
+	wallaced.sysconfig \
+	wallaced.systemd \
+	wallaced.sysvinit
+
 wallacedir = $(pythondir)/wallace
-wallace_PYTHON = $(wildcard *.py)
+wallace_PYTHON = \
+	__init__.py \
+	modules.py \
+	$(wildcard module_*.py)
 
+install-exec-local:
+	mkdir -p $(DESTDIR)/$(sbindir) \
+		$(DESTDIR)/$(localstatedir)/run/wallaced


commit c4581c05b1b7667d655b23612000d21fa700bfd3
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Apr 19 16:54:31 2012 +0100

    Add sysconfig, systemd and sysvinit files for Wallace

diff --git a/wallace/wallaced.sysconfig b/wallace/wallaced.sysconfig
new file mode 100644
index 0000000..7832bdb
--- /dev/null
+++ b/wallace/wallaced.sysconfig
@@ -0,0 +1,5 @@
+# Configuration file for the Kolab Server daemon Wallace.
+#
+# See wallaced --help for more flags.
+#
+FLAGS="--fork -l warning"
diff --git a/wallace/wallaced.systemd b/wallace/wallaced.systemd
new file mode 100644
index 0000000..73c16d7
--- /dev/null
+++ b/wallace/wallaced.systemd
@@ -0,0 +1,15 @@
+[Unit]
+Description=Wallace Content Filter
+After=syslog.target network.target
+
+[Service]
+Type=forking
+PIDFile=/var/run/wallaced/wallaced.pid
+EnvironmentFile=/etc/sysconfig/wallace
+ExecStart=/usr/sbin/wallace $FLAGS
+ExecReload=/bin/kill -HUP $MAINPID
+ExecStop=/bin/kill -TERM $MAINPID
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/wallace/wallaced.sysvinit b/wallace/wallaced.sysvinit
new file mode 100644
index 0000000..3b545a9
--- /dev/null
+++ b/wallace/wallaced.sysvinit
@@ -0,0 +1,108 @@
+#! /bin/bash
+#
+# wallace  Start/Stop the Kolab server daemon Wallace
+#
+# chkconfig:        - 65 10
+# description:      The Kolab Wallace server daemon is a content filtering daemon.
+# processname: wallaced
+
+### BEGIN INIT INFO
+# Provides: wallaced
+# Required-Start: $local_fs $network
+# Required-Stop: $local_fs $network
+# Short-Description: Start/Stop the Kolab Server daemon
+# Description:      The Kolab Wallace server daemon is a content filtering daemon.
+### END INIT INFO
+
+# Source function library.
+. /etc/init.d/functions
+
+# Source our configuration file for these variables.
+FLAGS="--fork -l warning"
+
+if [ -f /etc/sysconfig/wallace ] ; then
+    . /etc/sysconfig/wallace
+fi
+
+RETVAL=0
+
+# Set up some common variables before we launch into what might be
+# considered boilerplate by now.
+prog=wallace
+path=/usr/sbin/wallaced
+lockfile=/var/lock/subsys/$prog
+pidfile=/var/run/wallace/wallaced.pid
+
+start() {
+    [ -x $path ] || exit 5
+    echo -n $"Starting $prog: "
+    daemon $DAEMONOPTS $path -p $pidfile $FLAGS
+    RETVAL=$?
+    echo
+    [ $RETVAL -eq 0 ] && touch $lockfile
+    return $RETVAL
+}
+
+stop() {
+    echo -n $"Stopping $prog: "
+    killproc $prog
+    RETVAL=$?
+    echo
+    [ $RETVAL -eq 0 ] && rm -f $lockfile
+    return $RETVAL
+}
+
+restart() {
+    stop
+    start
+}
+
+reload() {
+    restart
+}
+
+force_reload() {
+    restart
+}
+
+rh_status() {
+    # run checks to determine if the service is running or use generic status
+    status -p $pidfile $prog
+}
+
+rh_status_q() {
+    rh_status >/dev/null 2>&1
+}
+
+case "$1" in
+  start)
+    rh_status_q && exit 0
+    start
+    ;;
+  stop)
+    rh_status_q || exit 0
+    stop
+    ;;
+  restart)
+    restart
+    ;;
+  reload)
+    rh_status_q || exit 7
+    reload
+    ;;
+  force-reload)
+    force_reload
+    ;;
+  status)
+    rh_status
+    ;;
+  condrestart|try-restart)
+    rh_status_q || exit 0
+    restart
+    ;;
+  *)
+    echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload}"
+    exit 2
+esac
+
+exit $?





More information about the commits mailing list