16 commits - configure.ac kolabd/kolabd.sysconfig kolabtest.py pykolab/auth pykolab/cli pykolab/conf pykolab/constants.py.in pykolab/imap pykolab/plugins pykolab/tests pykolab/wap_client test-wallace.py wallace/__init__.py wallace/module_optout.py wallace/modules.py wallace.py

Jeroen van Meeuwen vanmeeuwen at kolabsys.com
Fri Mar 2 16:49:36 CET 2012


 configure.ac                              |    2 
 kolabd/kolabd.sysconfig                   |    5 
 kolabtest.py                              |    2 
 pykolab/auth/ldap/__init__.py             |   46 ++++
 pykolab/cli/cmd_list_domains.py           |   25 +-
 pykolab/cli/cmd_remove_mailaddress.py     |   59 +++++
 pykolab/cli/cmd_set_language.py           |   44 ++++
 pykolab/cli/cmd_set_mail.py               |   44 ++++
 pykolab/cli/cmd_transfer_mailbox.py       |   65 +++++
 pykolab/conf/__init__.py                  |    4 
 pykolab/constants.py.in                   |   13 +
 pykolab/imap/cyrus.py                     |    5 
 pykolab/plugins/__init__.py               |    4 
 pykolab/tests/__init__.py                 |   57 ++---
 pykolab/tests/calendar.py                 |  231 ---------------------
 pykolab/tests/constants.py                |   92 --------
 pykolab/tests/contacts.py                 |  147 -------------
 pykolab/tests/create-contacts.py          |  106 ---------
 pykolab/tests/imap/test_create_mailbox.py |   44 ++++
 pykolab/tests/imap/test_login.py          |   61 +++++
 pykolab/tests/imap/test_login_admin.py    |   40 +++
 pykolab/tests/mail.py                     |  125 -----------
 pykolab/tests/tests.py                    |  192 +++++++++++++++++
 pykolab/tests/wap/test_login.py           |   40 +++
 pykolab/tests/zpush/__init__.py           |   63 -----
 pykolab/tests/zpush/test_000_000.py       |   93 --------
 pykolab/tests/zpush/test_000_001.py       |   55 -----
 pykolab/wap_client/__init__.py            |  281 +++++++++++++++++++++++++
 test-wallace.py                           |   93 ++++++++
 wallace.py                                |   40 +++
 wallace/__init__.py                       |  328 ++++++++++++++++++++++++++++++
 wallace/module_optout.py                  |  150 +++++++++++++
 wallace/modules.py                        |  170 +++++++++++++++
 33 files changed, 1758 insertions(+), 968 deletions(-)

New commits:
commit 91102c4fd11e4f4f4539bb1522d32d306e1cd904
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Mar 2 15:49:04 2012 +0000

    Add the wallace run code

diff --git a/wallace.py b/wallace.py
new file mode 100755
index 0000000..3a5bd01
--- /dev/null
+++ b/wallace.py
@@ -0,0 +1,40 @@
+#!/usr/bin/python
+#
+# 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 sys
+
+# For development purposes
+sys.path.extend(['.', '..'])
+
+from pykolab.translate import _
+
+try:
+    from pykolab.constants import *
+except ImportError, e:
+    print >> sys.stderr, _("Cannot load pykolab/constants.py:")
+    print >> sys.stderr, "%s" % e
+    sys.exit(1)
+
+import wallace
+
+if __name__ == "__main__":
+    wallace = wallace.WallaceDaemon()
+    wallace.run()
+


commit b481dac3b79ed078879516d1d720448e3591d138
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Mar 2 15:48:33 2012 +0000

    Add kolabd.sysconfig

diff --git a/kolabd/kolabd.sysconfig b/kolabd/kolabd.sysconfig
new file mode 100644
index 0000000..0705f32
--- /dev/null
+++ b/kolabd/kolabd.sysconfig
@@ -0,0 +1,5 @@
+# Configuration file for the Kolab Server daemon.
+#
+# See kolabd --help for more flags.
+#
+FLAGS="--fork -l warning"
\ No newline at end of file


commit f68403e44926e0ea3cbcf4f6f153aedade5b8dec
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Mar 2 15:47:38 2012 +0000

    Update kolabtest.py to the new test suite semantics

diff --git a/kolabtest.py b/kolabtest.py
index 6a348e8..4c3e954 100755
--- a/kolabtest.py
+++ b/kolabtest.py
@@ -23,7 +23,7 @@ import os
 import sys
 
 # For development purposes
-sys.path.extend(['.', '..'])
+sys.path.append('.')
 
 from pykolab.translate import _
 from pykolab.tests import *


commit 219e2a96d15e867b220cc92ee91a03ede0984cfc
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Mar 2 15:47:14 2012 +0000

    Add the bare bones of Wallace with the optout module

diff --git a/test-wallace.py b/test-wallace.py
new file mode 100755
index 0000000..84d3488
--- /dev/null
+++ b/test-wallace.py
@@ -0,0 +1,93 @@
+#!/usr/bin/python
+#
+# 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 smtplib
+import socket
+import sys
+
+# For development purposes
+sys.path.extend(['.', '..'])
+
+from email.MIMEMultipart import MIMEMultipart
+from email.MIMEBase import MIMEBase
+from email.MIMEText import MIMEText
+from email.Utils import COMMASPACE, formatdate
+from email import Encoders
+
+def send_mail(send_from, send_to, send_with=None):
+    smtp = smtplib.SMTP("localhost", 8025)
+    smtp.set_debuglevel(True)
+    subject = "This is a Kolab load test mail"
+    text = """Hi there,
+
+I am a Kolab Groupware test email, generated by a script that makes
+me send lots of email to lots of people using one account and a bunch
+of delegation blah.
+
+Your response, though completely unnecessary, would be appreciated, because
+being a fictitious character doesn't do my address book of friends any good.
+
+Kind regards,
+
+Lucy Meier.
+"""
+
+    msg = MIMEMultipart()
+    msg['From'] = send_from
+    msg['To'] = COMMASPACE.join(send_to)
+    msg['Date'] = formatdate(localtime=True)
+    msg['Subject'] = subject
+
+    msg.attach( MIMEText(text) )
+
+    #msg.attach( MIMEBase('application', open('/boot/initrd-plymouth.img').read()) )
+
+    smtp.sendmail(send_from, send_to, msg.as_string())
+
+if __name__ == "__main__":
+    #send_to = [
+            #'Jeroen van Meeuwen <jeroen.vanmeeuwen at klab.cc>',
+            #'Aleksander Machniak <aleksander.machniak at klab.cc>',
+            #'Georg Greve <georg.greve at klab.cc>',
+            #'Paul Adams <paul.adams at klab.cc>',
+            #'Thomas Broderli <thomas.broderli at klab.cc>',
+            #'Christoph Wickert <christoph.wickert at klab.cc>',
+            #'Lucy Meier <lucy.meier at klab.cc>',
+        #]
+
+
+    #send_mail(
+            #'Jeroen van Meeuwen <jeroen.vanmeeuwen at klab.cc>',
+            #send_to
+        #)
+
+    #send_mail(
+            #'Lucy Meier on behalf of Paul Adams <paul.adams at test90.kolabsys.com>',
+            #send_to
+        #)
+
+    #send_mail(
+            #'Lucy Meier on behalf of Georg Greve <georg.greve at test90.kolabsys.com>',
+            #send_to
+        #)
+
+    send_to = [ 'Jeroen van Meeuwen <jeroen.vanmeeuwen at klab.cc>' ]
+
+    send_mail('Jeroen van Meeuwen <vanmeeuwen at kolabsys.com>', send_to)
diff --git a/wallace/__init__.py b/wallace/__init__.py
new file mode 100644
index 0000000..b9df940
--- /dev/null
+++ b/wallace/__init__.py
@@ -0,0 +1,328 @@
+# -*- 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 asyncore
+import os
+from smtpd import SMTPChannel
+import sys
+import tempfile
+import threading
+import time
+import traceback
+
+import pykolab
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.wallace')
+conf = pykolab.getConf()
+
+class WallaceDaemon(object):
+    def __init__(self):
+        daemon_group = conf.add_cli_parser_option_group(_("Daemon Options"))
+
+        daemon_group.add_option(
+                "--fork",
+                dest    = "fork_mode",
+                action  = "store_true",
+                default = False,
+                help    = _("Fork to the background.")
+            )
+
+        conf.finalize_conf()
+
+        import modules
+        modules.__init__()
+
+    def process_message(self, peer, mailfrom, rcpttos, data):
+        """
+            We have retrieved the message.
+
+            - Dispatch to virus-scanning and anti-spam filtering?
+            - Apply access policies;
+                - Maximum number of recipients,
+                - kolabAllowSMTPSender,
+                - kolabAllowSMTPRecipient,
+                - Rule-based matching against white- and/or blacklist
+                - ...
+            - Accounting
+            - Data Loss Prevention
+        """
+        inheaders = 1
+
+        (fp, filename) = tempfile.mkstemp(dir="/var/spool/pykolab/wallace/")
+
+        os.write(fp, data)
+        os.close(fp)
+
+        while threading.active_count() > 25:
+            log.debug(_("Number of threads currently running: %d") %(threading.active_count()), level=8)
+            time.sleep(10)
+
+        log.debug(_("Continuing with %d threads currently running") %(threading.active_count()), level=8)
+
+        # TODO: Apply throttling
+        log.debug(_("Creating thread for message in %s") %(filename), level=8)
+
+        thread = threading.Thread(target=self.thread_run, args=[ filename ])
+        thread.start()
+
+    def thread_run(self, filename, *args, **kw):
+        while threading.active_count() > 25:
+            log.debug(_("Number of threads currently running: %d") %(threading.active_count()), level=8)
+            time.sleep(10)
+
+        log.debug(_("Continuing with %d threads currently running") %(threading.active_count()), level=8)
+
+        log.debug(_("Running thread %s for message file %s") %(threading.current_thread().name,filename), level=8)
+
+        if kw.has_key('module'):
+            log.debug(_("This message was already in module %s, delegating specifically to that module") %(kw['module']), level=8)
+
+            if kw.has_key('stage'):
+                log.debug(_("It was also in a certain stage: %s, letting module %s know that") %(kw['stage'],kw['module']), level=8)
+
+                log.debug(_("Executing module %s") %(kw['module']), level=8)
+
+                modules.execute(kw['module'], filename, stage=kw['stage'])
+
+                return
+
+            log.debug(_("Executing module %s") %(kw['module']), level=8)
+            modules.execute(kw['module'], filename, stage=kw['stage'])
+
+            return
+
+        wallace_modules = conf.get_list('wallace', 'modules')
+        if wallace_modules == None:
+            wallace_modules = []
+
+        for module in wallace_modules:
+            log.debug(_("Executing module %s") %(module), level=8)
+            modules.execute(module, filename)
+
+    def run(self):
+        """
+            Run the SASL authentication daemon.
+        """
+
+        exitcode = 0
+
+        try:
+            pid = 1
+            if conf.fork_mode:
+                self.thread_count += 1
+                pid = os.fork()
+
+            if pid == 0:
+                log.remove_stdout_handler()
+
+            self.do_wallace()
+
+        except SystemExit, e:
+            exitcode = e
+        except KeyboardInterrupt:
+            exitcode = 1
+            log.info(_("Interrupted by user"))
+        except AttributeError, e:
+            exitcode = 1
+            traceback.print_exc()
+            print >> sys.stderr, _("Traceback occurred, please report a bug at http://bugzilla.kolabsys.com")
+        except TypeError, e:
+            exitcode = 1
+            traceback.print_exc()
+            log.error(_("Type Error: %s") % e)
+        except:
+            exitcode = 2
+            traceback.print_exc()
+            print >> sys.stderr, _("Traceback occurred, please report a bug at http://bugzilla.kolabsys.com")
+        sys.exit(exitcode)
+
+    def pickup_defer(self):
+        wallace_modules = conf.get_list('wallace', 'modules')
+
+        if wallace_modules == None:
+            wallace_modules = []
+
+        base_path = '/var/spool/pykolab/wallace/'
+
+        while 1:
+            file_count = 0
+
+            log.debug(_("Picking up deferred messages for wallace"), level=8)
+
+            defer_path = os.path.join(base_path, 'DEFER')
+
+            if os.path.isdir(defer_path):
+                for root, directory, files in os.walk(defer_path):
+                    for filename in files:
+                        filepath = os.path.join(root, filename)
+
+                        file_count += 1
+
+                        for module in wallace_modules:
+                            modules.execute(module, filepath)
+
+                        time.sleep(1)
+
+            time.sleep(1)
+
+            for module in wallace_modules:
+                log.debug(_("Picking up deferred messages for module %s") %(module), level=8)
+
+                module_defer_path = os.path.join(base_path, module, 'DEFER')
+
+                if os.path.isdir(module_defer_path):
+                    for root, directory, files in os.walk(module_defer_path):
+                        for filename in files:
+                            filepath = os.path.join(root, filename)
+
+                            file_count += 1
+
+                            modules.execute(module, filepath)
+
+                            time.sleep(1)
+
+                time.sleep(1)
+
+            # Sleep longer if last time around we didn't find any deferred
+            # message files
+            if file_count > 0:
+                log.debug(_("Sleeping for 1 second"), level=8)
+                time.sleep(1)
+            else:
+                log.debug(_("Sleeping for 10 seconds"), level=8)
+                time.sleep(10)
+
+
+    def do_wallace(self):
+        import binascii
+        import socket
+        import struct
+
+        #s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+
+        ## TODO: The wallace socket path could be a setting.
+        #try:
+            #os.remove('/var/run/kolab/wallace')
+        #except:
+            ## TODO: Do the "could not remove, could not start dance"
+            #pass
+
+        bound = False
+        while not bound:
+            try:
+                s.bind(('localhost', 8025))
+                bound = True
+            except Exception, e:
+                log.warning(_("Could not bind to socket on port 8025"))
+                try:
+                    s.shutdown(1)
+                except Exception, e:
+                    log.warning(_("Could not shut down socket"))
+
+                s.close()
+
+                time.sleep(1)
+
+        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        #os.chmod('/var/run/kolab/wallace', 0777)
+        #os.chgrp('/var/run/wallace/mux', 'kolab')
+        #os.chown('/var/run/wallace/mux', 'kolab')
+
+        s.listen(5)
+
+        # Mind you to include the trailing slash
+        pickup_path = '/var/spool/pykolab/wallace/'
+        for root, directory, files in os.walk(pickup_path):
+            for filename in files:
+                filepath = os.path.join(root, filename)
+
+                if not root == pickup_path:
+                    module = os.path.dirname(root).replace(pickup_path, '')
+
+                    # Compare uppercase status (specifically, DEFER) with lowercase
+                    # (plugin names).
+                    #
+                    # The messages in DEFER are supposed to be picked up by
+                    # another thread, whereas the messages in other directories
+                    # are pending being handled by their respective plugins.
+                    #
+                    # TODO: Handle messages in spool directories for which a
+                    # plugin had been enabled, but is not enabled any longer.
+                    #
+
+                    if module.lower() == "defer":
+                        # Wallace was unable to deliver to re-injection smtpd.
+                        # Skip it, another thread is picking up the defers.
+                        continue
+
+                    stage = root.replace(pickup_path, '').split('/')
+                    if len(stage) < 2:
+                        stage = None
+                    else:
+                        stage = stage[1]
+
+                    if stage.lower() == "hold":
+                        continue
+
+                    # Do not handle messages in a defer state.
+                    if stage.lower() == "defer":
+                        continue
+
+                    log.debug(_("Number of threads currently running: %d") %(threading.active_count()), level=8)
+                    thread = threading.Thread(
+                            target = self.thread_run,
+                            args = [ filepath ],
+                            kwargs = {
+                                    "module": '%s' %(module),
+                                    "stage": '%s' %(stage)
+                                }
+                        )
+
+                    thread.start()
+                    time.sleep(0.5)
+
+                    continue
+
+                log.debug(_("Picking up spooled email file %s") %(filepath), level=8)
+                log.debug(_("Number of threads currently running: %d") %(threading.active_count()), level=8)
+                thread = threading.Thread(target=self.thread_run, args=[ filepath ])
+                thread.start()
+                time.sleep(0.5)
+
+        pid = os.fork()
+
+        if pid == 0:
+            self.pickup_defer()
+        else:
+
+            try:
+                while 1:
+                    pair = s.accept()
+                    log.info(_("Accepted connection"))
+                    if not pair == None:
+                        connection, address = pair
+                        #print "Accepted connection from %r" %(address)
+                        channel = SMTPChannel(self, connection, address)
+                        asyncore.loop()
+            except Exception, e:
+                traceback.print_exc()
+                s.shutdown(1)
+                s.close()
diff --git a/wallace/module_optout.py b/wallace/module_optout.py
new file mode 100644
index 0000000..d4bbf33
--- /dev/null
+++ b/wallace/module_optout.py
@@ -0,0 +1,150 @@
+# -*- 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 random
+import tempfile
+import time
+
+import modules
+
+import pykolab
+
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.wallace')
+conf = pykolab.getConf()
+
+mybasepath = '/var/spool/pykolab/wallace/optout/'
+
+def __init__():
+    if not os.path.isdir(mybasepath):
+        os.makedirs(mybasepath)
+
+    modules.register('optout', execute, description=description())
+
+def description():
+    return """Consult the opt-out service."""
+
+def execute(*args, **kw):
+    filepath = args[0]
+
+    if kw.has_key('stage'):
+        log.debug(_("Issuing callback after processing to stage %s") %(kw['stage']), level=8)
+        log.debug(_("Testing cb_action_%s()") %(kw['stage']), level=8)
+        if hasattr(modules, 'cb_action_%s' %(kw['stage'])):
+            log.debug(_("Attempting to execute cb_action_%s()") %(kw['stage']), level=8)
+            exec('modules.cb_action_%s(%r, %r)' %(kw['stage'],'optout',filepath))
+            return
+
+        #modules.next_module('optout')
+
+    log.debug(_("Consulting opt-out service for %r, %r") %(args, kw), level=8)
+
+
+    import email
+    message = email.message_from_file(open(filepath, 'r'))
+    envelope_sender = email.utils.getaddresses(message.get_all('From', []))
+
+    recipients = {
+            "To": email.utils.getaddresses(message.get_all('To', [])),
+            "Cc": email.utils.getaddresses(message.get_all('Cc', []))
+            # TODO: Are those all recipient addresses?
+        }
+
+    # optout answers are ACCEPT, REJECT, HOLD or DEFER
+    answers = [ 'ACCEPT', 'REJECT', 'HOLD', 'DEFER' ]
+
+    # Initialize our results placeholders.
+    _recipients = {}
+
+    for answer in answers:
+        _recipients[answer] = {
+                "To": [],
+                "Cc": []
+            }
+
+    for recipient_type in recipients.keys():
+        for recipient in recipients[recipient_type]:
+            log.debug(
+                    _("Running opt-out consult from envelope sender '%s " + \
+                        "<%s>' to recipient %s <%s>") %(
+                            envelope_sender[0][0],
+                            envelope_sender[0][1],
+                            recipient[0],
+                            recipient[1]
+                        ),
+                    level=8
+                )
+
+            optout_answer = answers[random.randint(0,(len(answers)-1))]
+            # Let's pretend it takes two seconds to get an answer, shall we?
+            time.sleep(2)
+
+            _recipients[optout_answer][recipient_type].append(recipient)
+
+    #print _recipients
+
+    ##
+    ## TODO
+    ##
+    ## If one of them all is DEFER, DEFER the entire message and discard the
+    ## other results.
+    ##
+
+    for answer in answers:
+        if not os.path.isdir(os.path.join(mybasepath, answer)):
+            os.makedirs(os.path.join(mybasepath, answer))
+
+        # Consider using a new mktemp()-like call
+        new_filepath = os.path.join(mybasepath, answer, os.path.basename(filepath))
+
+        # Write out a message file representing the new contents for the message
+        # use email.formataddr(recipient)
+        _message = email.message_from_file(open(filepath, 'r'))
+
+        use_this = False
+
+        for recipient_type in _recipients[answer].keys():
+            _message.__delitem__(recipient_type)
+            if not len(_recipients[answer][recipient_type]) == 0:
+                _message.__setitem__(recipient_type, ',\n'.join([email.utils.formataddr(x) for x in _recipients[answer][recipient_type]]))
+
+                use_this = True
+
+        if use_this:
+            # TODO: Do not set items with an empty list.
+            (fp, filename) = tempfile.mkstemp(dir="/var/spool/pykolab/wallace/optout/%s" %(answer))
+            os.write(fp, _message.__str__())
+            os.close(fp)
+
+            # Callback with new filename
+            if hasattr(modules, 'cb_action_%s' %(answer)):
+                log.debug(_("Attempting to execute cb_action_%s()") %(answer), level=8)
+                exec('modules.cb_action_%s(%r, %r)' %(answer,'optout', filename))
+
+    os.unlink(filepath)
+
+    #print "Moving filepath %s to new_filepath %s" %(filepath, new_filepath)
+    #os.rename(filepath, new_filepath)
+
+    #if hasattr(modules, 'cb_action_%s' %(optout_answer)):
+        #log.debug(_("Attempting to execute cb_action_%s()") %(optout_answer), level=8)
+        #exec('modules.cb_action_%s(%r, %r)' %(optout_answer,'optout', new_filepath))
+        #return
diff --git a/wallace/modules.py b/wallace/modules.py
new file mode 100644
index 0000000..374f440
--- /dev/null
+++ b/wallace/modules.py
@@ -0,0 +1,170 @@
+# -*- 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 sys
+
+import pykolab
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.wallace')
+conf = pykolab.getConf()
+
+modules = {}
+
+def __init__():
+    # We only want the base path
+    modules_base_path = os.path.dirname(__file__)
+
+    for modules_path, dirnames, filenames in os.walk(modules_base_path):
+        if not modules_path == modules_base_path:
+            continue
+
+        for filename in filenames:
+            if filename.startswith('module_') and filename.endswith('.py'):
+                module_name = filename.replace('.py','')
+                name = module_name.replace('module_', '')
+                #print "exec(\"from %s import __init__ as %s_register\"" %(module_name,name)
+                exec("from %s import __init__ as %s_register" %(module_name,name))
+                exec("%s_register()" %(name))
+
+        for dirname in dirnames:
+            register_group(modules_path, dirname)
+
+def list_modules(*args, **kw):
+    """
+        List modules
+    """
+
+    __modules = {}
+
+    for module in modules.keys():
+        if isinstance(module, tuple):
+            module_group, module = module
+            __modules[module_group] = {
+                    module: modules[(module_group,module)]
+                }
+        else:
+            __modules[module] = modules[module]
+
+    _modules = __modules.keys()
+    _modules.sort()
+
+    for _module in _modules:
+        if __modules[_module].has_key('function'):
+            # This is a top-level module
+            if not __modules[_module]['description'] == None:
+                print "%-25s - %s" %(_module.replace('_','-'),__modules[_module]['description'])
+            else:
+                print "%-25s" %(_module.replace('_','-'))
+
+    for _module in _modules:
+        if not __modules[_module].has_key('function'):
+            # This is a nested module
+            print "\n" + _("Module Group: %s") %(_module) + "\n"
+            ___modules = __modules[_module].keys()
+            ___modules.sort()
+            for __module in ___modules:
+                if not __modules[_module][__module]['description'] == None:
+                    print "%-4s%-21s - %s" %('',__module.replace('_','-'),__modules[_module][__module]['description'])
+                else:
+                    print "%-4s%-21s" %('',__module.replace('_','-'))
+
+def execute(name, *args, **kw):
+    if not modules.has_key(name):
+        log.error(_("No such module."))
+        sys.exit(1)
+
+    if not modules[name].has_key('function') and \
+        not modules[name].has_key('group'):
+        log.error(_("No such module."))
+        sys.exit(1)
+
+    modules[name]['function'](*args, **kw)
+
+def cb_action_HOLD(module, filepath):
+    log.info(_("Holding message in queue for manual review (%s by %s)") %(filepath, module))
+    ## Actually just unlink the file for now
+    #os.unlink(filepath)
+
+def cb_action_DEFER(module, filepath):
+    log.info(_("Deferring message in %s (by module %s)") %(filepath, module))
+
+def cb_action_REJECT(module, filepath):
+    log.info(_("Rejecting message in %s (by module %s)") %(filepath, module))
+    # Send NDR, unlink file
+    os.unlink(filepath)
+
+def cb_action_ACCEPT(module, filepath):
+    log.info(_("Accepting message in %s (by module %s)") %(filepath, module))
+    # Deliver for final delivery (use re-injection smtpd), unlink file
+    os.unlink(filepath)
+
+def register_group(dirname, module):
+    modules_base_path = os.path.join(os.path.dirname(__file__), module)
+
+    modules[module] = {}
+
+    for modules_path, dirnames, filenames in os.walk(modules_base_path):
+        if not modules_path == modules_base_path:
+            continue
+
+        for filename in filenames:
+            if filename.startswith('module_') and filename.endswith('.py'):
+                module_name = filename.replace('.py','')
+                name = module_name.replace('module_', '')
+                #print "exec(\"from %s.%s import __init__ as %s_%s_register\"" %(module,module_name,module,name)
+                exec("from %s.%s import __init__ as %s_%s_register" %(module,module_name,module,name))
+                exec("%s_%s_register()" %(module,name))
+
+def register(name, func, group=None, description=None, aliases=[]):
+    if not group == None:
+        module = "%s_%s" %(group,name)
+    else:
+        module = name
+
+    if isinstance(aliases, basestring):
+        aliases = [aliases]
+
+    if modules.has_key(module):
+        log.fatal(_("Module '%s' already registered") %(module))
+        sys.exit(1)
+
+    if callable(func):
+        if group == None:
+            modules[name] = {
+                    'function': func,
+                    'description': description
+                }
+        else:
+            modules[group][name] = {
+                    'function': func,
+                    'description': description
+                }
+
+            modules[module] = modules[group][name]
+            modules[module]['group'] = group
+            modules[module]['name'] = name
+
+        for alias in aliases:
+            modules[alias] = {
+                    'function': func,
+                    'description': _("Alias for %s") %(name)
+                }
+


commit 2d7c188c7bea6932918d74715c32b2575358dd4a
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Mar 2 15:46:35 2012 +0000

    Redo test suites

diff --git a/pykolab/tests/__init__.py b/pykolab/tests/__init__.py
index 9b902be..3aefdc3 100644
--- a/pykolab/tests/__init__.py
+++ b/pykolab/tests/__init__.py
@@ -26,7 +26,6 @@ import time
 import pykolab
 
 from pykolab.constants import *
-from pykolab.tests.constants import *
 from pykolab.translate import _
 
 log = pykolab.getLogger('pykolab.tests')
@@ -34,16 +33,11 @@ conf = pykolab.getConf()
 
 class Tests(object):
     def __init__(self):
+        import tests
+        tests.__init__()
 
         test_group = conf.add_cli_parser_option_group(_("Test Options"))
 
-        for item in TEST_ITEMS:
-            test_group.add_option(  "--%s" %(item['name']),
-                                    dest    = "%s" %(item['name']),
-                                    action  = "store_true",
-                                    default = False,
-                                    help    = _("Submit a number of items to the %s") %(item['mailbox']))
-
         test_group.add_option(  "--suite",
                                 dest    = "test_suites",
                                 action  = "append",
@@ -51,33 +45,28 @@ class Tests(object):
                                 help    = _("Run tests in suite SUITE. Implies a certain set of items being tested."),
                                 metavar = "SUITE")
 
-        delivery_group = conf.add_cli_parser_option_group(_("Content Delivery Options"))
-
-        delivery_group.add_option(  "--use-mail",
-                                    dest    = "use_mail",
-                                    action  = "store_true",
-                                    default = False,
-                                    help    = _("Send messages containing the items through mail (requires proper infrastructure)"))
+        conf.finalize_conf()
 
-        delivery_group.add_option(  "--use-imap",
-                                    dest    = "use_imap",
-                                    action  = "store_true",
-                                    default = False,
-                                    help    = _("Inject messages containing the items through IMAP"))
+    def run(self):
+        if len(conf.test_suites) > 0:
+            for test_suite in conf.test_suites:
+                print test_suite
+        else:
+            to_execute = []
 
-        delivery_group.add_option(  "--use-lmtp",
-                                    dest    = "use_lmtp",
-                                    action  = "store_true",
-                                    default = False,
-                                    help    = _("Deliver messages containing the items through LMTP"))
+            arg_num = 0
+            for arg in sys.argv[1:]:
+                print "arg", arg
+                arg_num += 1
+                if not arg.startswith('-') and len(sys.argv) >= arg_num:
+                    if tests.tests.has_key(sys.argv[arg_num].replace('-','_')):
+                        print "tests.tests.has_key", sys.argv[arg_num].replace('-','_')
+                        to_execute.append(sys.argv[arg_num].replace('-','_'))
 
-        conf.finalize_conf()
+            print "to_execute", to_execute
+            if len(to_execute) > 0:
+                print "'_'.join(to_execute)", '_'.join(to_execute)
+                tests.execute('_'.join(to_execute))
+            else:
+                tests.execute('help')
 
-    def run(self):
-        # Execute the suites first.
-        for suite in conf.test_suites:
-            try:
-                exec("from pykolab.tests.%s import %sTest" %(suite,suite.capitalize()))
-                exec("%stest = %sTest()" %(suite,suite.capitalize()))
-            except ImportError, e:
-                conf.log.error(_("Tests for suite %s failed to load. Aborting.") %(suite.capitalize()))
diff --git a/pykolab/tests/calendar.py b/pykolab/tests/calendar.py
deleted file mode 100644
index f5b8f68..0000000
--- a/pykolab/tests/calendar.py
+++ /dev/null
@@ -1,231 +0,0 @@
-# -*- 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 calendar
-import datetime
-import os
-import random
-import time
-
-from pykolab.conf import Conf
-from pykolab.constants import *
-from pykolab.tests.constants import *
-from pykolab.translate import _
-
-class CalendarItem(object):
-    def __init__(self, item_num=0, total_num=1, start=0, end=0, folder=None, user=None):
-        """
-            A calendar item is created from a template.
-
-            The attributes that can be modified are set to defaults first.
-        """
-
-        if user == None:
-            user = TEST_USERS[random.randint(0,(len(TEST_USERS)-1))]
-
-        # Used for some randomization
-        self.item_num = item_num
-        self.total_num = total_num
-        self.event_boundary_start = int(start)
-        self.event_boundary_end = int(end)
-
-        # Initial event data
-        self.event_location = "one or the other meeting room"
-        self.event_recurrence = ""
-        self.event_summary = "Test Event %d" %(item_num)
-
-        from_user = TEST_USERS[random.randint(0,(len(TEST_USERS)-1))]
-        self.from_name_str = "%s %s" %(from_user['givenname'].capitalize(),from_user['sn'].capitalize())
-        self.from_email_str = "%(givenname)s@%(domain)s" %(from_user)
-        self.kolab_event_date_creation = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
-        self.kolab_event_date_start = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
-        self.kolab_event_date_end = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
-        self.rfc_2822_sent_date = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())
-        self.to_name_str = "%s %s" %(user['givenname'].capitalize(),user['sn'].capitalize())
-        self.to_email_str = "%(givenname)s@%(domain)s" %(user)
-
-        self.uid = "%s.%s" %(str(random.randint(1000000000,9999999999)),str(random.randint(0,999)).zfill(3))
-
-        if folder:
-            self.mailbox = folder
-        else:
-            self.mailbox = "INBOX/Calendar"
-
-        # Status information
-        self.dates_randomized = False
-
-        self.randomize_recurrence()
-        self.randomize_dates()
-
-    def randomize_dates(self):
-        """
-            Randomize all dates in the event but make sure they
-            do make sense.
-
-            Ergo, the start date is before the end date, and such.
-
-            Also, take into account the total number of calendar items, so that
-            we do not clutter.
-        """
-
-        if self.dates_randomized:
-            return
-
-        # The ratio is 1/2 goes to the past, 1/8 goes to this month, 3/8 goes to the future months.
-        # Over 10.000 items, that is 5000 in the past -> 250 days a year, 4 appointments a day, 5 year
-        #                           125 this month
-        #                           4875 to future
-
-        # We have two integers (epoch values), a start and an end
-        mystart = random.randint(self.event_boundary_start,self.event_boundary_end)
-        myend = mystart + (1800 * random.randint(1,4))
-
-        self.kolab_event_date_start = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(mystart))
-        self.kolab_event_date_end = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(myend))
-
-        # Calculate the timespan we're serving assuming 4-6 appointments a day;
-        self.dates_randomized = True
-
-    def randomize_recurrence(self):
-        """
-            Randomize the recurrence of this event.
-
-            One every so many events has recurrence.
-        """
-        return
-
-        if not random.randint(1,15) == 1:
-            return
-
-        # The recurrence day (of the week)
-        recur_day = random.randint(0,6)
-
-#        # Seek a day somewhere in the past with this day_number
-#        for days_in_week in calendar.itermonthdates():
-#            # day_of_week is a list of weeks, where 0 is outside the month
-#            # described in the call to calendar.itermonthdays(), and so
-#            # we are able now to find the first day for this recurrence.
-#            for day_date in days_in_week:
-#                print day_date
-
-        self.dates_randomized = True
-
-    def __str__(self):
-        for tpl_file_location in [ '/usr/share/kolab/tests/kcal-event.tpl', './share/tests/kcal-event.tpl' ]:
-            if os.path.isfile(tpl_file_location):
-                tpl_file = open(tpl_file_location, 'r')
-                tpl_orig = tpl_file.read()
-                tpl_file.close()
-                break
-        return tpl_orig % self.__dict__
-
-def create_items(conf, num=None, folder=None):
-    for item in TEST_ITEMS:
-        if item['name'] == 'calendar':
-            info = item
-
-    if num:
-        info['number'] = int(num)
-
-    conf.log.debug(_("Creating %d Events") %(info['number']), level=3)
-
-    alloc_uids = []
-
-    if os.path.isfile('./share/tests/kcal-event.tpl'):
-        tpl_file = open('./share/tests/kcal-event.tpl', 'r')
-        tpl_orig = tpl_file.read()
-        tpl_file.close()
-
-    (start_boundary,end_boundary) = set_bounds(num=num)
-
-    imap = True
-
-    for user in conf.testing_users:
-        if conf.use_mail:
-            pass
-        elif conf.use_lmtp:
-            pass
-        elif conf.use_imap:
-            import imaplib
-            if imap:
-                del imap
-            imap = imaplib.IMAP4(conf.testing_server)
-            imap.login("%(givenname)s@%(domain)s" %(user), user['password'])
-        else:
-            pass
-
-        #print "Running for user %(givenname)s@%(domain)s" %(user)
-        item_num = 0
-
-        while item_num < int(info['number']):
-            conf.log.debug(_("Creating Calendar item number %d") %(item_num+1), level=5)
-
-            item = CalendarItem(item_num=(item_num+1), total_num=num, start=start_boundary, end=end_boundary, folder=folder, user=user)
-
-            if not item.uid in alloc_uids:
-                alloc_uids.append(item.uid)
-            else:
-                continue
-
-            msg = str(item)
-
-            if conf.use_mail:
-                conf.log.debug(_("Sending UID message %s through SMTP targeting user %s@%s") %(item.uid,user['givenname'],user['domain']), level=9)
-
-            elif conf.use_lmtp:
-                conf.log.debug(_("Sending UID message %s through LMTP targeting user %s@%s") %(item.uid,user['givenname'],user['domain']), level=9)
-
-            elif conf.use_imap:
-                conf.log.debug(_("Saving UID message %s to IMAP (user %s, folder %s)") %(item.uid,user['givenname'],item.mailbox), level=9)
-                imap.append(item.mailbox, '', imaplib.Time2Internaldate(time.time()), msg)
-            else:
-                conf.log.debug(_("Somehow ended up NOT sending these messages"), level=9)
-
-            item_num +=1
-
-def set_bounds(num=0):
-    """
-        Set the lower and upper boundaries for this event, using the
-        total number of events and a reasonable but random average number
-        of appointments.
-
-        returns a tuple epoch (start, end)
-    """
-
-    # Pretend anywhere between 0 and 5 events per workday,
-    # Multiply by the number of workdays a week,
-    # Divide that by 7 for a nice, float average.
-    events_per_week_avg = float(random.randint(10,25))
-    events_per_day_avg = events_per_week_avg / 7
-
-    ratio = [ 6, 4 ]
-
-    # Given the total number of events to be created, and the average number
-    # of events per day, we can now look at what the lower boundary would be,
-    # compared to today.
-    days_to_go_back = (((num / events_per_day_avg) / 10 ) * ratio[0])
-    days_to_go_forward = (((num / events_per_day_avg) / 10 ) * ratio[1])
-
-    now = time.time()
-    start_of_day = now - (now % (24 * 60 * 60))
-
-    boundary_start = start_of_day - (days_to_go_back * 24 * 60 * 60)
-    boundary_end = start_of_day + (days_to_go_forward * 24 * 60 * 60)
-
-    return (boundary_start,boundary_end)
diff --git a/pykolab/tests/constants.py b/pykolab/tests/constants.py
deleted file mode 100644
index 6a9853f..0000000
--- a/pykolab/tests/constants.py
+++ /dev/null
@@ -1,92 +0,0 @@
-# -*- 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 datetime
-import os
-import random
-import time
-
-TEST_ALPHABET = ""
-for num in range(33,256):
-    TEST_ALPHABET = "%s%s" %(TEST_ALPHABET,unichr(num))
-
-TEST_ITEMS = [
-        {
-                'name': 'calendar',
-                'mailbox': 'Calendar',
-                'template': 'kcal-event.tpl',
-                'number': 10,
-                # 6 years ago
-                'calendar_start': "%d" %(time.time() - (60*60*24*365*6)),
-                # 4 years forward
-                'calendar_end': "%d" %(time.time() - (60*60*24*365*6)),
-            },
-        {
-                'name': 'contacts',
-                'mailbox': 'Contacts',
-                'template': 'kaddress-contact.tpl',
-                'number': 10,
-            },
-
-        {
-                'name': 'mail',
-                'mailbox': 'INBOX',
-                'template': 'kaddress-contact.tpl',
-                'number': 10,
-            },
-    ]
-
-TEST_USERS = [
-        #{
-                #'givenname': 'john',
-                #'sn': 'doe',
-                #'domain': 'doe.org'
-            #},
-        {
-                'givenname': 'joe',
-                'sn': 'sixpack',
-                'domain': 'sixpack.com'
-            },
-        #{
-                #'givenname': 'max',
-                #'sn': 'sixpack',
-                #'domain': 'sixpack.com'
-            #},
-        #{
-                #'givenname': 'min',
-                #'sn': 'sixpack',
-                #'domain': 'sixpack.com'
-            #},
-        #{
-                #'givenname': 'joe',
-                #'sn': 'imum',
-                #'domain': 'imum.net'
-            #},
-        {
-                'givenname': 'max',
-                'sn': 'imum',
-                'domain': 'imum.net'
-            },
-        #{
-                #'givenname': 'min',
-                #'sn': 'imum',
-                #'domain': 'imum.net'
-            #},
-    ]
-
diff --git a/pykolab/tests/contacts.py b/pykolab/tests/contacts.py
deleted file mode 100644
index 156253c..0000000
--- a/pykolab/tests/contacts.py
+++ /dev/null
@@ -1,147 +0,0 @@
-# -*- 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 calendar
-import datetime
-import imaplib
-import os
-import random
-import time
-
-import pykolab
-
-from pykolab.constants import *
-from pykolab.tests.constants import *
-from pykolab.translate import _
-
-log = pykolab.getLogger('pykolab.tests.contacts')
-
-conf = pykolab.getConf()
-
-imap = pykolab.imap
-
-class ContactsItem(object):
-    def __init__(self, item_num=0, total_num=1, folder=None, user=None):
-        """
-            A contact item is created from a template.
-
-            The attributes that can be modified are set to defaults first.
-        """
-
-        if user == None:
-            user = TEST_USERS[random.randint(0,(len(TEST_USERS)-1))]
-
-        # Used for some randomization
-        self.item_num = item_num
-        self.total_num = total_num
-
-        # Initial event data
-        self.kolab_contact_given_name = "John"
-        self.kolab_contact_last_name = "von Test"
-        self.kolab_contact_email_str = "john at von.test"
-        self.kolab_contact_mobile_number = "+31612345678"
-
-        from_user = TEST_USERS[random.randint(0,(len(TEST_USERS)-1))]
-
-        self.from_name_str = "%s %s" %(from_user['givenname'].capitalize(),from_user['sn'].capitalize())
-        self.from_email_str = "%(givenname)s@%(domain)s" %(from_user)
-
-        self.kolab_creation_date = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
-
-        self.rfc_2822_sent_date = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())
-
-        self.to_name_str = "%s %s" %(user['givenname'].capitalize(),user['sn'].capitalize())
-        self.to_email_str = "%(givenname)s@%(domain)s" %(user)
-
-        self.uid = "%s.%s" %(str(random.randint(1000000000,9999999999)),str(random.randint(0,999)).zfill(3))
-
-        if folder:
-            self.mailbox = folder
-        else:
-            self.mailbox = "Contacts"
-
-        self.randomize_contact()
-
-    def randomize_contact(self):
-        """
-            Randomize the contact's information.
-
-        """
-        pass
-
-    def __str__(self):
-        for tpl_file_location in [ '/usr/share/kolab/tests/kaddress-contact.tpl', './share/tests/kaddress-contact.tpl', '../share/tests/kaddress-contact.tpl' ]:
-            if os.path.isfile(tpl_file_location):
-                tpl_file = open(tpl_file_location, 'r')
-                tpl_orig = tpl_file.read()
-                tpl_file.close()
-                break
-        return tpl_orig % self.__dict__
-
-def create_items(conf, num=None, folder=None):
-    for item in TEST_ITEMS:
-        if item['name'] == 'contacts':
-            info = item
-
-    if num:
-        info['number'] = int(num)
-
-    log.debug(_("Creating %d Contacts") %(info['number']), level=3)
-
-    alloc_uids = []
-
-    for user in eval(conf.get('testing','users')):
-        if conf.use_mail:
-            pass
-        elif conf.use_lmtp:
-            pass
-        elif conf.use_imap:
-            imap.connect(login=False)
-            imap.login("%(givenname)s.%(sn)s@%(domain)s" %(user), user['password'])
-        else:
-            pass
-
-        #print "Running for user %(givenname)s@%(domain)s" %(user)
-        item_num = 0
-
-        while item_num < int(info['number']):
-            log.debug(_("Creating Contact item number %d") %(item_num+1), level=5)
-
-            item = ContactsItem(item_num=(item_num+1), total_num=num, folder=folder, user=user)
-
-            if not item.uid in alloc_uids:
-                alloc_uids.append(item.uid)
-            else:
-                continue
-
-            msg = str(item)
-
-            if conf.use_mail:
-                log.debug(_("Sending UID message %s through SMTP targeting user %s@%s") %(item.uid,user['givenname'],user['domain']), level=9)
-
-            elif conf.use_lmtp:
-                log.debug(_("Sending UID message %s through LMTP targeting user %s@%s") %(item.uid,user['givenname'],user['domain']), level=9)
-
-            elif conf.use_imap:
-                log.debug(_("Saving UID message %s to IMAP (user %s, folder %s)") %(item.uid,user['givenname'],item.mailbox), level=9)
-                imap.imap.m.append(item.mailbox, '', imaplib.Time2Internaldate(time.time()), msg)
-            else:
-                log.debug(_("Somehow ended up NOT sending these messages"), level=9)
-
-            item_num +=1
diff --git a/pykolab/tests/create-contacts.py b/pykolab/tests/create-contacts.py
deleted file mode 100644
index 8b09a25..0000000
--- a/pykolab/tests/create-contacts.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-# -*- coding: utf-8 -*-
-# Copyright 2010-2012 Kolab Systems AG (http://www.kolabsys.com)
-#
-# Paul James Adams <adams a 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, random, sys
-
-if __name__ == "__main__":
-    wanted_num = int(sys.argv[1])
-
-    contact_tpl_file = open('./kaddress-contact.tpl', 'r')
-    contact_tpl_orig = contact_tpl_file.read()
-    contact_tpl_file.close()
-
-    users = ['john', 'joe', 'max']
-    domains = ['doe.org', 'sixpack.com', 'imum.net']
-    uid_alloc = []
-
-    alphabet = "abcdefghijklmnopqrstuvwxwz"
-
-    user_num = 0
-
-    for user in users:
-        num = 0
-        while num <= wanted_num:
-            uid = "%s.%s" %(str(random.randint(1000000000,9999999999)),str(random.randint(0,999)).zfill(3))
-            if not uid in uid_alloc:
-                uid_alloc.append(uid)
-            else:
-                continue
-
-            domain = domains[random.randint(0,2)]
-
-            contact_tpl = contact_tpl_orig
-
-            birthday = ""
-            if random.randint(0,100) >= 75:
-                year = str(random.randint(1960, 2010))
-                month = str(random.randint(1,12)).zfill(2)
-                day = str(random.randint(1,27)).zfill(2)
-                birthday = "%s-%s-%s" % (year, month, day)
-
-            middle_names = ""
-            if random.randint(0,100) >= 50:
-                middle_names = ''.join(random.sample(alphabet, random.randint(4, 8))).capitalize()
-
-            number = ""
-            if random.randint(0,100) >= 25:
-                number = "+441234567890"
-
-            given_name = ''.join(random.sample(alphabet, random.randint(4, 8))).capitalize()
-            last_name  = ''.join(random.sample(alphabet, random.randint(4, 8))).capitalize()
-
-            contact = {
-                'uid': uid,
-                'user': user,
-                'user_email': "%s@%s" % (user, domain),
-                'given_name': given_name,
-                'middle_names': middle_names,
-                'last_name': last_name,
-                'full_name': "%s %s %s" % (given_name, middle_names, last_name),
-                'display_name': "%s %s" % (given_name, last_name),
-                'email_address': "%s@%s" % (given_name, domain),
-                'number': number,
-                'birthday': birthday
-                }
-
-            directory = "/kolab/var/imapd/spool/domain/%s/%s/%s/user/%s/Contacts" %(domains[user_num][0],domains[user_num],user[0],user)
-            if not os.path.isdir(directory):
-                directory = "./kolab/var/imapd/spool/domain/%s/%s/%s/user/%s/Contacts" %(domains[user_num][0],domains[user_num],user[0],user)
-                if not os.path.isdir(directory):
-                    os.makedirs(directory)
-
-            out = open("%s/%d." %(directory,num), 'w')
-
-            for key in contact.keys():
-                contact_tpl = contact_tpl.replace("@@%s@@" % key, '%s' % contact[key])
-
-            out.write(contact_tpl)
-            out.close()
-
-            try:
-                os.chown("%s/%d." %(directory,num), 19415, 19415)
-            except:
-                pass
-            num += 1
-
-        user_num += 1
diff --git a/pykolab/tests/imap/__init__.py b/pykolab/tests/imap/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pykolab/tests/imap/test_create_mailbox.py b/pykolab/tests/imap/test_create_mailbox.py
new file mode 100644
index 0000000..e9a4901
--- /dev/null
+++ b/pykolab/tests/imap/test_create_mailbox.py
@@ -0,0 +1,44 @@
+# -*- 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 pykolab.tests import tests
+
+import pykolab
+
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.tests')
+conf = pykolab.getConf()
+
+auth = pykolab.auth
+imap = pykolab.imap
+
+def __init__():
+    tests.register('create_mailbox', execute, group='imap', description=description())
+
+def description():
+    return """Create a mailbox."""
+
+def execute(*args, **kw):
+    return
+    mailbox = conf.cli_args.pop(0)
+
+    imap.connect()
+    imap.cm(mailbox)
+
diff --git a/pykolab/tests/imap/test_login.py b/pykolab/tests/imap/test_login.py
new file mode 100644
index 0000000..951abd9
--- /dev/null
+++ b/pykolab/tests/imap/test_login.py
@@ -0,0 +1,61 @@
+# -*- 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 time
+import traceback
+
+from pykolab.tests import tests
+
+import pykolab
+
+from pykolab.translate import _
+from pykolab import utils
+
+log = pykolab.getLogger('pykolab.tests')
+conf = pykolab.getConf()
+
+auth = pykolab.auth
+imap = pykolab.imap
+
+def __init__():
+    tests.register('login', execute, group='imap', description=description())
+
+def description():
+    return """Connect to IMAP and login."""
+
+def execute(*args, **kw):
+    try:
+        log.debug(_("Connecting at %s") %(time.time()), level=8)
+        imap.connect(login=False)
+        log.debug(_("Connected at %s") %(time.time()), level=8)
+    except:
+        raise TestFailureException, __file__
+
+    try:
+        log.debug(_("Logging in at %s") %(time.time()), level=8)
+        imap.login('doe', password='0cvRKSdluPU4ewN')
+        log.debug(_("Logged in at %s") %(time.time()), level=8)
+        #imap.login('doe', password='bla')
+    except:
+        raise TestFailureException(__file__)
+
+class TestFailureException(BaseException):
+    def __init__(self, test_file):
+        log.error(_("Test failure in %s") %(test_file))
+        utils.ask_confirmation('Would you like to log this as a bug?')
\ No newline at end of file
diff --git a/pykolab/tests/imap/test_login_admin.py b/pykolab/tests/imap/test_login_admin.py
new file mode 100644
index 0000000..6344bc7
--- /dev/null
+++ b/pykolab/tests/imap/test_login_admin.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.
+#
+
+from pykolab.tests import tests
+
+import pykolab
+
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.tests')
+conf = pykolab.getConf()
+
+auth = pykolab.auth
+imap = pykolab.imap
+
+def __init__():
+    tests.register('login_admin', execute, group='imap', description=description())
+
+def description():
+    return """Connect to IMAP and login as an administrator."""
+
+def execute(*args, **kw):
+    imap.connect()
+
diff --git a/pykolab/tests/mail.py b/pykolab/tests/mail.py
deleted file mode 100644
index 4304149..0000000
--- a/pykolab/tests/mail.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# -*- 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 calendar
-import datetime
-import mailbox
-import os
-import random
-import time
-
-from pykolab.conf import Conf
-from pykolab.constants import *
-from pykolab.tests.constants import *
-from pykolab.translate import _
-
-class MailItem(object):
-    def __init__(self, item_num=0, total_num=1, folder=None, user=None):
-        """
-            A mail item is created from a template.
-
-            The attributes that can be modified are set to defaults first.
-        """
-
-        if user == None:
-            user = TEST_USERS[random.randint(0,(len(TEST_USERS)-1))]
-
-        # Used for some randomization
-        self.item_num = item_num
-        self.total_num = total_num
-
-        # Initial event data
-        self.kolab_contact_given_name = "John"
-        self.kolab_contact_last_name = "von Test"
-        self.kolab_contact_email_str = "john at von.test"
-        self.kolab_contact_mobile_number = "+31612345678"
-
-        from_user = TEST_USERS[random.randint(0,(len(TEST_USERS)-1))]
-
-        self.from_name_str = "%s %s" %(from_user['givenname'].capitalize(),from_user['sn'].capitalize())
-        self.from_email_str = "%(givenname)s@%(domain)s" %(from_user)
-
-        self.kolab_creation_date = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
-
-        self.rfc_2822_sent_date = time.strftime("%a, %d %b %Y %H:%M:%S +0000", time.gmtime())
-
-        self.to_name_str = "%s %s" %(user['givenname'].capitalize(),user['sn'].capitalize())
-        self.to_email_str = "%(givenname)s@%(domain)s" %(user)
-
-        self.uid = "%s.%s" %(str(random.randint(1000000000,9999999999)),str(random.randint(0,999)).zfill(3))
-
-        if folder:
-            self.mailbox = folder
-        else:
-            self.mailbox = "INBOX"
-
-        self.randomize_mail()
-
-    def randomize_mail(self):
-        """
-            Randomize some of the contents of the mail.
-        """
-
-        pass
-
-    def __str__(self):
-        return ""
-
-def create_items(conf, num=None, folder=None):
-    for item in TEST_ITEMS:
-        if item['name'] == 'mail':
-            info = item
-
-    if num:
-        info['number'] = int(num)
-
-    conf.log.debug(_("Creating %d Mails") %(info['number']), level=3)
-
-    imap = True
-
-    for user in conf.testing_users:
-        if conf.use_mail:
-            pass
-        elif conf.use_lmtp:
-            pass
-        elif conf.use_imap:
-            import imaplib
-            if imap:
-                del imap
-            imap = imaplib.IMAP4(conf.testing_server)
-            imap.login("%(givenname)s@%(domain)s" %(user), user['password'])
-        else:
-            pass
-
-        mb = mailbox.mbox('./share/tests/mail/lists.fedoraproject.org/devel/2010-September.txt')
-        for key in mb.keys():
-
-            msg = mb.get_string(key)
-
-            if conf.use_mail:
-                conf.log.debug(_("Sending message %s through SMTP targeting user %s@%s") %(key,user['givenname'],user['domain']), level=9)
-
-            elif conf.use_lmtp:
-                conf.log.debug(_("Sending message %s through LMTP targeting user %s@%s") %(key,user['givenname'],user['domain']), level=9)
-
-            elif conf.use_imap:
-                conf.log.debug(_("Saving message %s to IMAP (user %s, folder %s)") %(key,user['givenname'],"INBOX"), level=9)
-                imap.append("INBOX", '', imaplib.Time2Internaldate(time.time()), msg)
-            else:
-                conf.log.debug(_("Somehow ended up NOT sending these messages"), level=9)
diff --git a/pykolab/tests/tests.py b/pykolab/tests/tests.py
new file mode 100644
index 0000000..8a3c915
--- /dev/null
+++ b/pykolab/tests/tests.py
@@ -0,0 +1,192 @@
+# -*- 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 sys
+
+import pykolab
+
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.tests')
+conf = pykolab.getConf()
+
+tests = {}
+test_groups = {}
+
+def __init__():
+    # We only want the base path
+    tests_base_path = os.path.dirname(__file__)
+
+    for tests_path, dirnames, filenames in os.walk(tests_base_path):
+        if not tests_path == tests_base_path:
+            continue
+
+        for filename in filenames:
+            #print filename
+            if filename.startswith('test_') and filename.endswith('.py'):
+                module_name = filename.replace('.py','')
+                test_name = module_name.replace('test_', '')
+                #print "exec(\"from %s import __init__ as %s_register\"" %(module_name,test_name)
+                exec("from %s import __init__ as %s_register" %(module_name,test_name))
+                exec("%s_register()" %(test_name))
+
+        for dirname in dirnames:
+            register_group(tests_path, dirname)
+
+    register('help', list_tests, description=_("List tests"))
+
+def list_tests(*args, **kw):
+    """
+        List tests
+    """
+
+    __tests = {}
+
+    for test in tests.keys():
+        if isinstance(test, tuple):
+            test_group, test = test
+            __tests[test_group] = {
+                    test: tests[(test_group,test)]
+                }
+        else:
+            __tests[test] = tests[test]
+
+    _tests = __tests.keys()
+    _tests.sort()
+
+    for _test in _tests:
+        if not __tests[_test].has_key('group'):
+            if __tests[_test].has_key('function'):
+                # This is a top-level test
+                if not __tests[_test]['description'] == None:
+                    print "%-25s - %s" %(_test.replace('_','-'),__tests[_test]['description'])
+                else:
+                    print "%-25s" %(_test.replace('_','-'))
+
+    for _test in _tests:
+        if not __tests[_test].has_key('function'):
+            # This is a nested test
+            print "\n" + _("Test Suite: %s") %(_test) + "\n"
+            ___tests = __tests[_test].keys()
+            ___tests.sort()
+            for __test in ___tests:
+                if not __tests[_test][__test]['description'] == None:
+                    print "%-4s%-21s - %s" %('',__test.replace('_','-'),__tests[_test][__test]['description'])
+                else:
+                    print "%-4s%-21s" %('',__test.replace('_','-'))
+
+def execute(test_name, *args, **kw):
+    print "tests:", tests
+    print "test_name:", test_name
+
+    if not tests.has_key(test_name):
+        log.error(_("No such test."))
+        sys.exit(1)
+
+    if not tests[test_name].has_key('function') and \
+        not tests[test_name].has_key('group'):
+        log.error(_("No such test."))
+        sys.exit(1)
+
+    if tests[test_name].has_key('group'):
+        group = tests[test_name]['group']
+        _test_name = tests[test_name]['test_name']
+        try:
+            exec("from %s.test_%s import cli_options as %s_%s_cli_options" %(group,_test_name,group,test_name))
+            exec("%s_%s_cli_options()" %(group,test_name))
+        except ImportError, e:
+            pass
+
+    else:
+        try:
+            exec("from test_%s import cli_options as %s_cli_options" %(test_name,test_name))
+            exec("%s_cli_options()" %(test_name))
+        except ImportError, e:
+            pass
+
+    conf.finalize_conf()
+
+    tests[test_name]['function'](conf.cli_args, kw)
+
+def register_group(dirname, module):
+    tests_base_path = os.path.join(os.path.dirname(__file__), module)
+
+    tests[module] = {}
+
+    for tests_path, dirnames, filenames in os.walk(tests_base_path):
+        if not tests_path == tests_base_path:
+            continue
+
+        for filename in filenames:
+            if filename.startswith('test_') and filename.endswith('.py'):
+                module_name = filename.replace('.py','')
+                test_name = module_name.replace('test_', '')
+                #print "exec(\"from %s.%s import __init__ as %s_%s_register\"" %(module,module_name,module,test_name)
+                exec("from %s.%s import __init__ as %s_%s_register" %(module,module_name,module,test_name))
+                exec("%s_%s_register()" %(module,test_name))
+
+def register(test_name, func, group=None, description=None, aliases=[]):
+    if not group == None:
+        test = "%s_%s" %(group,test_name)
+    else:
+        test = test_name
+
+    #print "registering", test
+
+    if isinstance(aliases, basestring):
+        aliases = [aliases]
+
+    if tests.has_key(test):
+        log.fatal(_("Test '%s' already registered") %(test))
+        sys.exit(1)
+
+    if tests.has_key(test):
+        log.fatal(_("Test '%s' already registered") %(test))
+        sys.exit(1)
+
+    if callable(func):
+        if group == None:
+            tests[test_name] = {
+                    'function': func,
+                    'description': description
+                }
+        else:
+            tests[group][test_name] = {
+                    'function': func,
+                    'description': description
+                }
+
+            tests[test] = tests[group][test_name]
+            tests[test]['group'] = group
+            tests[test]['test_name'] = test_name
+
+        for alias in aliases:
+            tests[alias] = {
+                    'function': func,
+                    'description': _("Alias for %s") %(test_name)
+                }
+
+##
+## Tests not yet implemented
+##
+
+def not_yet_implemented(*args, **kw):
+    print _("Not yet implemented")
+    sys.exit(1)
\ No newline at end of file
diff --git a/pykolab/tests/wap/__init__.py b/pykolab/tests/wap/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/pykolab/tests/wap/test_login.py b/pykolab/tests/wap/test_login.py
new file mode 100644
index 0000000..5eed57b
--- /dev/null
+++ b/pykolab/tests/wap/test_login.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.
+#
+
+from pykolab.tests import tests
+
+import pykolab
+
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.tests')
+conf = pykolab.getConf()
+
+auth = pykolab.auth
+imap = pykolab.imap
+
+def __init__():
+    tests.register('login', execute, group='wap', description=description())
+
+def description():
+    return """Log in to the Kolab Web Administration Panel API."""
+
+def execute(*args, **kw):
+    return
+
diff --git a/pykolab/tests/zpush/__init__.py b/pykolab/tests/zpush/__init__.py
deleted file mode 100644
index d0af89e..0000000
--- a/pykolab/tests/zpush/__init__.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# -*- 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 imp
-import os
-import sys
-
-import pykolab
-
-from pykolab.constants import *
-from pykolab.tests.constants import *
-from pykolab.translate import _
-
-log = pykolab.getLogger('pykolab.tests.zpush')
-conf = pykolab.getConf()
-
-class ZpushTest(object):
-    def __init__(self):
-        self.tests = []
-
-        # Make sure we parse the [testing] section of the configuration file, if
-        # available.
-        conf.set_options_from_testing_section()
-
-        # Attempt to create a list of modules
-        for x in range(0,8):
-            for y in range(0,8):
-                test_num = "%s_%s" %(str(x).zfill(3),str(y).zfill(3))
-                try:
-                    exec("from test_%s import Test_%s" %(test_num,test_num))
-                    self.tests.append("Test_%s" %(test_num))
-                except ImportError, e:
-                    pass
-
-        for test in self.tests:
-            exec("result = %s()" %(test))
-
-        #name = "from pykolab.tests.zpush.test_%s import Test_%s" %(test_num,test_num)
-        #file, pathname, description = imp.find_module(name, sys.path)
-
-        #try:
-            #plugin = imp.load_module(mod_name, file, pathname, description)
-        #finally:
-            #file.close()
-        #plugins[name] = plugin
-
-#print plugins
diff --git a/pykolab/tests/zpush/test_000_000.py b/pykolab/tests/zpush/test_000_000.py
deleted file mode 100644
index d5197a3..0000000
--- a/pykolab/tests/zpush/test_000_000.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# -*- 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 pykolab
-
-from pykolab import utils
-from pykolab.constants import *
-from pykolab.tests.constants import *
-from pykolab.translate import _
-
-log = pykolab.getLogger('pykolab.tests.zpush')
-conf = pykolab.getConf()
-
-class Test_000_000(object):
-    """
-        Preparations for the Test 000 series.
-    """
-
-    def __init__(self):
-        self.suite_num = "000"
-        self.suite_test_num = "000"
-
-        log.info("About to execute preperation task #000 in Test Suite #000");
-        log.info("We will assume the start situation has been configured");
-        log.info("such as is described in the documentation.");
-
-        utils.ask_confirmation("Continue?")
-
-        # Delete all mailboxes
-        #imap.connect()
-        #for folder in imap.lm("user/%"):
-            #imap.dm(folder)
-
-        #for user in auth.list_users(domain):
-            #for mailbox in imap.lm("user%s%s" %(imap.SEP,"%(givenname)s@%(domain)s" %(user))):
-                #log.debug(_("Deleting mailbox: %s") %(mailbox), level=3)
-                #try:
-                    #imap.dm(mailbox)
-                #except cyruslib.CYRUSError, e:
-                    #pass
-
-        ## Recreate the user top-level mailboxes
-        #for user in conf.testing_users:
-            #mailbox = "user%s%s" %(imap.SEP,"%(givenname)s@%(domain)s" %(user))
-            #log.debug(_("Creating mailbox: %s") %(mailbox), level=3)
-            #imap.cm(mailbox)
-
-        #imap.logout()
-
-        #del imap
-
-        # Have the user themselves:
-        # - create the standard folders
-        # - set the standard annotations
-        # - subscribe
-        for user in conf.testing_users:
-            imap = cyruslib.CYRUS("imap://%s:143" %(conf.testing_server))
-            try:
-                imap.login("%(givenname)s.%(surname)s@%(domain)s" %(user), user['password'])
-            except:
-                log.error(_("Authentication failure for %s") %("%(givenname)s.%(surname)s@%(domain)s" %(user)))
-                continue
-
-            if conf.debuglevel > 3:
-                imap.VERBOSE = True
-
-            imap.subscribe("INBOX")
-
-            for mailbox in TEST_FOLDERS.keys():
-                imap.cm("%s" %(mailbox))
-                for annotation in TEST_FOLDERS[mailbox]['annotations'].keys():
-                    imap.setannotation("%s" %(mailbox),annotation,TEST_FOLDERS[mailbox]['annotations'][annotation])
-
-            imap.subscribe("%s" %(mailbox))
-
-            imap.logout()
-            del imap
\ No newline at end of file
diff --git a/pykolab/tests/zpush/test_000_001.py b/pykolab/tests/zpush/test_000_001.py
deleted file mode 100644
index cadd9e5..0000000
--- a/pykolab/tests/zpush/test_000_001.py
+++ /dev/null
@@ -1,55 +0,0 @@
-# -*- 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 pykolab
-
-from pykolab import utils
-from pykolab.constants import *
-from pykolab.tests.constants import *
-from pykolab.translate import _
-
-log = pykolab.getLogger('pykolab.tests.zpush')
-conf = pykolab.getConf()
-
-class Test_000_001(object):
-    """
-        First, basic test.
-
-        Populate the previously created and existing INBOX, Calendar and Contact
-        folders with a limited amount of data.
-    """
-
-    def __init__(self, conf):
-        self.suite_num = "000"
-        self.suite_test_num = "001"
-
-        # Create some test calendar items
-        for item in TEST_ITEMS:
-            try:
-                exec("from pykolab.tests.%s import %sItem, create_items as create_%s_items" %(item['name'],item['name'].capitalize(),item['name']))
-            except ImportError, e:
-                self.conf.log.warning(_("Could not load %sItem from %s, skipping the testing.") %(item['name'].capitalize(),item['name']))
-                continue
-
-            self.conf.log.debug("self.conf.%s = %r" %(item['name'], getattr(self.conf, "%s" %(item['name']))), level=9)
-
-            if getattr(self.conf, "%s" %(item['name'])):
-                exec("create_%s_items(self.conf, num=%d)" %(item['name'],item['number']))
-            else:
-                self.conf.log.info("not executing %s" %(item['name'].capitalize()))


commit 3533710ef014bf541daa2d0107bae8b34a8e9a20
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Mar 2 15:46:00 2012 +0000

    Add additional commands

diff --git a/pykolab/cli/cmd_remove_mailaddress.py b/pykolab/cli/cmd_remove_mailaddress.py
new file mode 100644
index 0000000..6a4daea
--- /dev/null
+++ b/pykolab/cli/cmd_remove_mailaddress.py
@@ -0,0 +1,59 @@
+# -*- 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 commands
+
+import pykolab
+
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.cli')
+conf = pykolab.getConf()
+
+auth = pykolab.auth
+imap = pykolab.imap
+
+def __init__():
+    commands.register('remove_mail', execute, description=description())
+
+def description():
+    return """Remove a user's mail address."""
+
+def execute(*args, **kw):
+    uid = conf.cli_args.pop(0)
+    email_address = conf.cli_args.pop(0)
+
+    user = auth.find_user('uid', uid)
+    user = {
+            'dn': user
+        }
+
+    user['mail'] = auth.get_user_attribute('klab.cc', user, 'mail')
+    user['mailalternateaddress'] = auth.get_user_attribute('klab.cc', user, 'mailalternateaddress')
+
+    if user['mail'] == email_address:
+        auth.set_user_attribute('klab.cc', user, 'mail', '')
+
+    if email_address in user['mailalternateaddress']:
+        _user_addresses = []
+        for address in user['mailalternateaddress']:
+            if not address == email_address:
+                _user_addresses.append(address)
+
+        auth.set_user_attribute('klab.cc', user, 'mailAlternateAddress', _user_addresses)
\ No newline at end of file
diff --git a/pykolab/cli/cmd_set_language.py b/pykolab/cli/cmd_set_language.py
new file mode 100644
index 0000000..6b94d42
--- /dev/null
+++ b/pykolab/cli/cmd_set_language.py
@@ -0,0 +1,44 @@
+# -*- 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 commands
+
+import pykolab
+
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.cli')
+conf = pykolab.getConf()
+
+auth = pykolab.auth
+imap = pykolab.imap
+
+def __init__():
+    commands.register('set_language', execute, description=description())
+
+def description():
+    return """Set the user's preferred language."""
+
+def execute(*args, **kw):
+    uid = conf.cli_args.pop(0)
+    language = conf.cli_args.pop(0)
+
+    user = auth.find_user('uid', uid)
+    auth.set_user_attribute('klab.cc', user, 'preferredlanguage', language)
+
diff --git a/pykolab/cli/cmd_set_mail.py b/pykolab/cli/cmd_set_mail.py
new file mode 100644
index 0000000..2d4e75c
--- /dev/null
+++ b/pykolab/cli/cmd_set_mail.py
@@ -0,0 +1,44 @@
+# -*- 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 commands
+
+import pykolab
+
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.cli')
+conf = pykolab.getConf()
+
+auth = pykolab.auth
+imap = pykolab.imap
+
+def __init__():
+    commands.register('set_mail', execute, description=description())
+
+def description():
+    return """Set the user's primary email address."""
+
+def execute(*args, **kw):
+    uid = conf.cli_args.pop(0)
+    primary_mail = conf.cli_args.pop(0)
+
+    user = auth.find_user('uid', uid)
+    auth.set_user_attribute('klab.cc', user, 'mail', primary_mail)
+
diff --git a/pykolab/cli/cmd_transfer_mailbox.py b/pykolab/cli/cmd_transfer_mailbox.py
new file mode 100644
index 0000000..4f0de09
--- /dev/null
+++ b/pykolab/cli/cmd_transfer_mailbox.py
@@ -0,0 +1,65 @@
+# -*- 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 commands
+
+import pykolab
+
+from pykolab.translate import _
+
+log = pykolab.getLogger('pykolab.cli')
+conf = pykolab.getConf()
+
+auth = pykolab.auth
+imap = pykolab.imap
+
+def __init__():
+    commands.register('transfer_mailbox', execute, description="Transfer a mailbox to another server.")
+
+def execute(*args, **kw):
+    """
+        Transfer mailbox
+    """
+
+    if len(conf.cli_args) > 1:
+        mailfolder = conf.cli_args.pop(0)
+        target_server = conf.cli_args.pop(0)
+
+    if len(conf.cli_args) > 0:
+        target_partition = conf.cli_args.pop(0)
+
+    mbox_parts = imap.parse_mailfolder(mailfolder)
+
+    print "Mailbox parts:", mbox_parts
+
+    if mbox_parts['domain'] == None:
+        user_identifier = mbox_parts['path_parts'][1]
+    else:
+        user_identifier = "%s@%s" %(mbox_parts['path_parts'][1], mbox_parts['domain'])
+
+    print "User Identifier:", user_identifier
+
+    user = auth.find_user("mail", user_identifier)
+
+    print "User:", user
+
+    imap.connect()
+    imap.imap.xfer(mailfolder, target_server)
+
+    auth.set_user_attribute(mbox_parts['domain'], user, "mailHost", target_server)


commit 3334c2ed460ef138f1ed3a9f3e01c2cc6ab24531
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Mar 2 15:45:29 2012 +0000

    Convert command list-domains to using the wap client

diff --git a/pykolab/cli/cmd_list_domains.py b/pykolab/cli/cmd_list_domains.py
index 7a2bfd8..0bc2d27 100644
--- a/pykolab/cli/cmd_list_domains.py
+++ b/pykolab/cli/cmd_list_domains.py
@@ -22,33 +22,30 @@ import commands
 import pykolab
 
 from pykolab.translate import _
+from pykolab import wap_client
 
 log = pykolab.getLogger('pykolab.cli')
 conf = pykolab.getConf()
 
-auth = pykolab.auth
-imap = pykolab.imap
-
 def __init__():
     commands.register('list_domains', execute, description="List Kolab domains.")
 
 def execute(*args, **kw):
-    auth.connect()
-
     # Create the authentication object.
     # TODO: Binds with superuser credentials!
-    domains = auth.list_domains()
+    wap_client.authenticate()
+    domains = wap_client.domains_list()
+
+    #print "domains:", domains['list']
 
     print "%-39s %-40s" %("Primary Domain Name Space","Secondary Domain Name Space(s)")
 
     # TODO: Take a hint in --quiet, and otherwise print out a nice table
     # with headers and such.
-    for domain,domain_aliases in domains:
-        if len(domain_aliases) > 0:
-            print _("%-39s %-40s") %(
-                    domain,
-                    ', '.join(domain_aliases)
-                )
+    for domain_dn in domains['list'].keys():
+        if isinstance(domains['list'][domain_dn]['associateddomain'], list):
+            print domains['list'][domain_dn]['associateddomain'][0]
+            for domain_alias in domains['list'][domain_dn]['associateddomain'][1:]:
+                print "%-39s %-40s" %('', domain_alias)
         else:
-            print _("%-39s") %(domain)
-
+            print domains['list'][domain_dn]['associateddomain']


commit 00d655a36452fd023062ae5cafe2080c46f79441
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Mar 2 15:44:56 2012 +0000

    Add the command-line wap client

diff --git a/pykolab/wap_client/__init__.py b/pykolab/wap_client/__init__.py
new file mode 100644
index 0000000..c84790d
--- /dev/null
+++ b/pykolab/wap_client/__init__.py
@@ -0,0 +1,281 @@
+
+import json
+import httplib
+import sys
+
+sys.path.append('../..')
+
+from pykolab import utils
+
+API_HOSTNAME = "admin.klab.cc"
+API_PORT = "80"
+API_SCHEME = "http"
+API_BASE = "/~vanmeeuwen/kolab-wap/public_html/api"
+
+session_id = None
+
+conn = None
+
+def authenticate(username=None, password=None):
+    global session_id
+
+    if username == None:
+        username = utils.ask_question("Login", "cn=Directory Manager")
+
+    if password == None:
+        password = utils.ask_question("Password", "5auTYwxBK1uGTpy", password=True)
+
+    params = json.dumps(
+            {
+                    'username': username,
+                    'password': password
+                }
+        )
+
+    response = request('POST', "system.authenticate", params)
+
+    if response.has_key('session_token'):
+        session_id = response['session_token']
+
+def connect():
+    global conn
+
+    if conn == None:
+        conn = httplib.HTTPConnection(API_HOSTNAME, API_PORT)
+        conn.connect()
+
+    return conn
+
+def domains_capabilities():
+    return request('GET', 'domains.capabilities')
+
+def domains_list():
+    return request('GET', 'domains.list')
+
+def get_group_input():
+    group_types = group_types_list()
+
+    if len(group_types.keys()) > 1:
+        for key in group_types.keys():
+            if not key == "status":
+                print "%s) %s" %(key,group_types[key]['name'])
+
+        group_type_id = utils.ask_question("Please select the group type")
+
+    elif len(group_types.keys()) > 0:
+        print "Automatically selected the only group type available"
+        group_type_id = group_types.keys()[0]
+
+    else:
+        print "No group types available"
+        sys.exit(1)
+
+    if group_types.has_key(group_type_id):
+        group_type_info = group_types[group_type_id]['attributes']
+    else:
+        print "No such group type"
+        sys.exit(1)
+
+    params = {
+            'group_type_id': group_type_id
+        }
+
+    for attribute in group_type_info['form_fields'].keys():
+        params[attribute] = utils.ask_question(attribute)
+
+    for attribute in group_type_info['auto_form_fields'].keys():
+        exec("retval = group_form_value_generate_%s(params)" %(attribute))
+        params[attribute] = retval[attribute]
+
+    return params
+
+def get_user_input():
+    user_types = user_types_list()
+
+    if len(user_types.keys()) > 1:
+        for key in user_types.keys():
+            if not key == "status":
+                print "%s) %s" %(key,user_types[key]['name'])
+
+        user_type_id = utils.ask_question("Please select the user type")
+
+    elif len(user_types.keys()) > 0:
+        print "Automatically selected the only user type available"
+        user_type_id = user_types.keys()[0]
+
+    else:
+        print "No user types available"
+        sys.exit(1)
+
+    if user_types.has_key(user_type_id):
+        user_type_info = user_types[user_type_id]['attributes']
+    else:
+        print "No such user type"
+        sys.exit(1)
+
+    params = {
+            'user_type_id': user_type_id
+        }
+
+    for attribute in user_type_info['form_fields'].keys():
+        params[attribute] = utils.ask_question(attribute)
+
+    for attribute in user_type_info['auto_form_fields'].keys():
+        exec("retval = form_value_generate_%s(params)" %(attribute))
+        params[attribute] = retval[attribute]
+
+    return params
+
+def group_add(params=None):
+    if params == None:
+        params = get_group_input()
+
+    params = json.dumps(params)
+
+    return request('POST', 'group.add', params)
+
+def group_form_value_generate_mail(params=None):
+    if params == None:
+        params = get_user_input()
+
+    params = json.dumps(params)
+
+    return request('POST', 'group_form_value.generate_mail', params)
+
+def group_info():
+    group = utils.ask_question("Group email address")
+    group = request('GET', 'group.info?group=%s' %(group))
+    return group
+
+def group_members_list(group=None):
+    if group == None:
+        group = utils.ask_question("Group email address")
+    group = request('GET', 'group.members_list?group=%s' %(group))
+    return group
+
+def group_types_list():
+    return request('GET', 'group_types.list')
+
+def groups_list():
+    return request('GET', 'groups.list')
+
+def request(method, api_uri, params=None, headers={}):
+    global session_id
+
+    if not session_id == None:
+        headers["X-Session-Token"] = session_id
+
+    conn = connect()
+    conn.request(method.upper(), "%s/%s" %(API_BASE,api_uri), params, headers)
+    response = conn.getresponse()
+    data = response.read()
+
+    #print method, api_uri, params
+    #print data
+
+    try:
+        response_data = json.loads(data)
+    except ValueError, e:
+        # Some data is not JSON
+        print "Response data is not JSON"
+        sys.exit(1)
+
+    #print response_data
+
+    if response_data['status'] == "OK":
+        del response_data['status']
+        return response_data['result']
+    else:
+        return response_data['result']
+
+def system_capabilities():
+    return request('GET', 'system.capabilities')
+
+def system_get_domain():
+    return request('GET', 'system.get_domain')
+
+def system_select_domain(domain=None):
+    if domain == None:
+        domain = utils.ask_question("Domain name")
+    return request('GET', 'system.select_domain?domain=%s' %(domain))
+
+def user_add(params=None):
+    if params == None:
+        params = get_user_input()
+
+    params = json.dumps(params)
+
+    return request('POST', 'user.add', params)
+
+def user_delete(params=None):
+    if params == None:
+        params = {
+                'user': utils.ask_question("Username for user to delete", "user")
+            }
+
+    params = json.dumps(params)
+
+    return request('POST', 'user.delete', params)
+
+def user_edit(params=None):
+    if params == None:
+        params = {
+                'user': utils.ask_question("Username for user to edit", "user")
+            }
+
+    params = json.dumps(params)
+
+    user = request('GET', 'user.info', params)
+
+    return user
+
+def user_form_value_generate_cn(params=None):
+    if params == None:
+        params = get_user_input()
+
+    params = json.dumps(params)
+
+    return request('POST', 'user_form_value.generate_cn', params)
+
+def user_form_value_generate_displayname(params=None):
+    if params == None:
+        params = get_user_input()
+
+    params = json.dumps(params)
+
+    return request('POST', 'user_form_value.generate_displayname', params)
+
+def user_form_value_generate_mail(params=None):
+    if params == None:
+        params = get_user_input()
+
+    params = json.dumps(params)
+
+    return request('POST', 'user_form_value.generate_mail', params)
+
+def user_form_value_generate_password(*args, **kw):
+    return request('GET', 'user_form_value.generate_password')
+
+def user_form_value_generate_uid(params=None):
+    if params == None:
+        params = get_user_input()
+
+    params = json.dumps(params)
+
+    return request('POST', 'user_form_value.generate_uid', params)
+
+def user_form_value_generate_userpassword(*args, **kw):
+    result = user_form_value_generate_password()
+    return { 'userpassword': result['password'] }
+
+def user_info():
+    user = utils.ask_question("User email address")
+    user = request('GET', 'user.info?user=%s' %(user))
+    return user
+
+def user_types_list():
+    return request('GET', 'user_types.list')
+
+def users_list():
+    return request('GET', 'users.list')
+


commit 5c3cb04cbc195d6e20824cb80fa12173623eb9db
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Mar 2 12:48:59 2012 +0000

    conf.get_list() should always return a list

diff --git a/pykolab/conf/__init__.py b/pykolab/conf/__init__.py
index e6db94e..01282f2 100644
--- a/pykolab/conf/__init__.py
+++ b/pykolab/conf/__init__.py
@@ -463,12 +463,12 @@ class Conf(object):
 
         setting = self.get_raw(section, key)
         if setting == None:
-            return None
+            return []
 
         raw_values = setting.split(',')
 
         if raw_values == None:
-            return None
+            return []
 
         for raw_value in raw_values:
             untrimmed_values.extend(raw_value.split(' '))


commit ca23a3074be34a28b1595e9db167b7a6f7459645
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Feb 24 13:40:29 2012 +0000

    Do not reset the retval for each plugin

diff --git a/pykolab/plugins/__init__.py b/pykolab/plugins/__init__.py
index d308b6a..e21c1cc 100644
--- a/pykolab/plugins/__init__.py
+++ b/pykolab/plugins/__init__.py
@@ -222,8 +222,6 @@ class KolabPlugins(object):
             if not hasattr(self,plugin):
                 continue
 
-            retval = None
-
             if hasattr(getattr(self,plugin),hook):
                 try:
                     log.debug(_("Executing hook %s for plugin %s") %(hook,plugin), level=8)


commit b3d17f8b87a9b9a9864ec701bbf3d2105f613914
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Feb 24 13:28:52 2012 +0000

    Bump prerelease

diff --git a/configure.ac b/configure.ac
index 7ec8451..ffd2f35 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,5 +1,5 @@
 AC_INIT([pykolab], 0.3)
-AC_SUBST([RELEASE], 0.15)
+AC_SUBST([RELEASE], 0.16)
 
 AC_CONFIG_SRCDIR(pykolab/constants.py.in)
 


commit 8e8f0ce73e30e07f6edc9089bdc8f3445fbc57bc
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Feb 24 13:28:26 2012 +0000

    Remove additional print statement

diff --git a/pykolab/plugins/__init__.py b/pykolab/plugins/__init__.py
index 2dff5b8..d308b6a 100644
--- a/pykolab/plugins/__init__.py
+++ b/pykolab/plugins/__init__.py
@@ -227,7 +227,7 @@ class KolabPlugins(object):
             if hasattr(getattr(self,plugin),hook):
                 try:
                     log.debug(_("Executing hook %s for plugin %s") %(hook,plugin), level=8)
-                    print "retval = self.%s.%s(%r, %r)" %(plugin,hook, args, kw)
+                    #print "retval = self.%s.%s(%r, %r)" %(plugin,hook, args, kw)
                     exec("retval = self.%s.%s(*args, **kw)" %(plugin,hook))
                 except TypeError, e:
                     log.error(_("Cannot execute hook %s for plugin %s: %s") %(hook,plugin,e))


commit c81b96a5c629dc20cc4a79eda9f124e78f807ea6
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Feb 24 13:22:17 2012 +0000

    Bump prerelease

diff --git a/configure.ac b/configure.ac
index 47a49fa..7ec8451 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,5 +1,5 @@
 AC_INIT([pykolab], 0.3)
-AC_SUBST([RELEASE], 0.14)
+AC_SUBST([RELEASE], 0.15)
 
 AC_CONFIG_SRCDIR(pykolab/constants.py.in)
 


commit a20f3891f03a8509fd5f23025fb945995bcdde18
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Feb 24 13:21:59 2012 +0000

    Do not solely support syncrepl (this is a development thing)

diff --git a/pykolab/constants.py.in b/pykolab/constants.py.in
index 68fb9b2..85f5c38 100644
--- a/pykolab/constants.py.in
+++ b/pykolab/constants.py.in
@@ -106,5 +106,18 @@ SUPPORTED_LDAP_CONTROLS = {
                 'desc': 'Virtual List View Control',
                 'oid': '2.16.840.1.113730.3.4.9',
                 'func': '_vlv_search'
+            },
+        3: {
+                'desc': 'OpenLDAP Syncrepl (RFC4533)',
+                'oid': '1.3.6.1.4.1.4203.1.9.1.1',
+                'func': '_sync_repl'
             }
     }
+
+#SUPPORTED_LDAP_CONTROLS = {
+#        0: {
+#                'desc': 'OpenLDAP Syncrepl (RFC4533)',
+#                'oid': '1.3.6.1.4.1.4203.1.9.1.1',
+#                'func': '_sync_repl'
+#            }
+#    }


commit ec88785a50e045fced5ed7f86efd0c6e91fc7c61
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Feb 24 13:21:18 2012 +0000

    Mandatorily add uid to the list of authentication attributes to search for
    Start on sync_repl support

diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py
index c0cd60a..c52bd55 100644
--- a/pykolab/auth/ldap/__init__.py
+++ b/pykolab/auth/ldap/__init__.py
@@ -254,6 +254,9 @@ class LDAP(object):
         else:
             auth_search_attrs = [ 'uid', 'mail' ]
 
+        if not 'uid' in auth_search_attrs:
+            auth_search_attrs.append('uid')
+
         auth_search_filter = [ '(|' ]
 
         for auth_search_attr in auth_search_attrs:
@@ -542,6 +545,41 @@ class LDAP(object):
         ):
         pass
 
+    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=[]
+        ):
+
+        import syncrepl
+
+        ldap_sync_conn = syncrepl.DNSync(
+                '/var/lib/pykolab/syncrepl.db',
+                ldap_url.initializeUrl(),
+                trace_level=ldapmodule_trace_level,
+                trace_file=ldapmodule_trace_file
+            )
+
+        msgid = ldap_sync_conn.syncrepl_search(
+                base_dn,
+                scope,
+                mode='refreshAndPersist',
+                filterstr=filterstr
+            )
+
+        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 _regular_search(self,
             base_dn,
             scope=ldap.SCOPE_SUBTREE,
@@ -947,7 +985,13 @@ class LDAP(object):
                 quiet=True
             )
 
-        self.ldap.simple_bind_s(bind_dn, bind_pw)
+        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
 
         # TODO: The quota and alternative address attributes are actually
         # supposed to be settings.


commit bab75f963d922fdbac6b86290f29312d74763c09
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Fri Feb 24 13:20:58 2012 +0000

    Remove print statements

diff --git a/pykolab/imap/cyrus.py b/pykolab/imap/cyrus.py
index 8141dce..b9fb6e3 100644
--- a/pykolab/imap/cyrus.py
+++ b/pykolab/imap/cyrus.py
@@ -107,7 +107,11 @@ 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)
         if not _mailfolder['domain'] == None:
@@ -161,6 +165,7 @@ class Cyrus(cyruslib.CYRUS):
             Login to the actual backend server.
         """
         server = self.find_mailfolder_server(mailfolder)
+        #print "server:", server
         imap.connect('imap://%s:143' %(server))
 
         log.debug(_("Setting quota for INBOX folder %s to %s") %(mailfolder,quota), level=8)





More information about the commits mailing list