Initial import @sf.net vmm-0.3
authorPascal Volk <neverseen@users.sourceforge.net>
Sun, 06 Jan 2008 18:22:10 +0000
changeset 0 bb0aa2102206
child 1 d08cda9d7c1a
Initial import @sf.net
COPYING
INSTALL
README
TODO
VirtualMailManager/Account.py
VirtualMailManager/Alias.py
VirtualMailManager/Config.py
VirtualMailManager/Domain.py
VirtualMailManager/Exceptions.py
VirtualMailManager/VirtualMailManager.py
VirtualMailManager/__init__.py
VirtualMailManager/constants/ERROR.py
VirtualMailManager/constants/EXIT.py
VirtualMailManager/constants/__init__.py
create_tables.pgsql
install.sh
pgsql-relocated_maps.cf
pgsql-smtpd_sender_login_maps.cf
pgsql-transport.cf
pgsql-virtual_alias_maps.cf
pgsql-virtual_gid_maps.cf
pgsql-virtual_mailbox_maps.cf
pgsql-virtual_uid_maps.cf
setup.cfg
setup.py
update_tables_0.2.x-0.3.pgsql
vmm
vmm.cfg
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/COPYING	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,25 @@
+Copyright (c) 2007 - 2008, VEB IT
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    1. Redistributions of source code must retain the above copyright notice,
+       this list of conditions and the following disclaimer.
+    2. Redistributions in binary form must reproduce the above copyright notice,
+       this list of conditions and the following disclaimer in the documentation
+       and/or other materials provided with the distribution.
+    3. Neither the name of the company nor the names of its contributors may be
+       used to endorse or promote products derived from this software without
+       specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
+EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/INSTALL	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,154 @@
+Installation Prerequisites
+You should already have installed and configured Postfix, Dovecot and
+PostgreSQL.
+You have to install Python and pyPgSQL to use the Virtual Mail Manager.
+
+
+Configuring PostgreSQL
+
+* /etc/postgresql/8.2/main/pg_hba.conf
+    # IPv4 local connections:
+    host    mailsys     +mailsys    127.0.0.1/32          md5
+
+    # reload configuration
+    /etc/init.d/postgresql-8.2 force-reload
+
+* Create a DB user if necessary:
+    DB Superuser:
+    createuser -s -d -r -E -e -P $USERNAME
+    DB User:
+    createuser -d -E -e -P $USERNAME
+
+* Create Database and db users for Postfix and Dovecot
+    connecting to PostgreSQL:
+    psql template1
+    
+    # create database
+    CREATE DATABASE mailsys ENCODING 'UTF8';
+    # connect to the new database
+    \c mailsys
+    # import db structure
+    \i /path/to/create_tables.pgsql
+
+    # create users and group
+    CREATE USER postfix ENCRYPTED password 'DB PASSWORD for Postfix';
+    CREATE USER dovecot ENCRYPTED password 'DB PASSWORD for Dovecot';
+    CREATE ROLE mailsys WITH USER postfix, dovecot;
+
+    # set permissions
+    GRANT SELECT ON dovecot_password, dovecot_user TO dovecot;
+    GRANT SELECT ON postfix_alias, postfix_maildir, postfix_relocated,
+    postfix_uid, postfix_gid, postfix_transport TO postfix;
+
+    # leave psql
+    \q
+
+Create directory for your mails
+  mkdir /srv/mail
+  cd /srv/mail/
+  mkdir 0 1 2 3 4 5 6 7 8 9 a b c d e f g h i j k l m n o p q r s t u v w x y z
+  chmod 771 /srv/mail
+  chgrp -R mail /srv/mail
+  chmod 751 /srv/mail/*
+
+Configuring Dovecot
+
+* /etc/dovecot/dovecot.conf
+    # all your other settings
+    mail_location = maildir:~/Maildir
+    mail_extra_groups = mail
+    first_valid_uid = 70000
+    first_valid_gid = 70000
+    protocol lda {
+      postmaster_address = postmaster@domain.tld
+    }
+    auth default {
+      mechanisms = cram-md5
+      passdb sql {
+        args = /etc/dovecot/dovecot-sql.conf
+      }
+      userdb sql {
+        args = /etc/dovecot/dovecot-sql.conf
+      }
+      user = nobody
+      socket listen {
+        master {
+          path = /var/run/dovecot/auth-master
+          mode = 0600
+        }
+        client {
+          path = /var/spool/postfix/private/auth
+          mode = 0660
+          user = postfix
+          group = postfix
+        }
+      }
+    }
+
+* /etc/dovecot/dovecot-sql.conf
+    driver = pgsql
+    connect = host=localhost dbname=mailsys user=dovecot password=$Dovecot_PASS
+    default_pass_scheme = HMAC-MD5
+    password_query = SELECT "user", password FROM dovecot_password WHERE "user"= '%u'
+    user_query = SELECT home, uid, gid FROM dovecot_user WHERE userid = '%u'
+
+Provide a root SETUID copy of Dovecot's deliver agent for Postfix
+
+    mkdir -p /usr/local/lib/dovecot
+    chmod 700 /usr/local/lib/dovecot
+    chown nobody /usr/local/lib/dovecot
+    cp /usr/lib/dovecot/deliver /usr/local/lib/dovecot/
+    chmod u+s /usr/local/lib/dovecot/deliver
+
+
+Start or restart Dovecot
+
+
+Configuring Postfix's master.cf
+
+    # Add Dovecot's deliver agent
+    dovecot   unix  -       n       n       -       -       pipe
+      flags=DRhu user=nobody:mail argv=/usr/local/lib/dovecot/deliver -f ${sender} -d ${user}@${nexthop} -n -m ${extension}
+
+
+
+Configuring Postfix's main.cf
+
+    # virtual domains
+    virtual_mailbox_domains = pgsql:/etc/postfix/pgsql-transport.cf
+    virtual_alias_maps = pgsql:/etc/postfix/pgsql-virtual_alias_maps.cf
+    transport_maps = pgsql:/etc/postfix/pgsql-transport.cf
+    virtual_minimum_uid = 70000
+    virtual_uid_maps = pgsql:/etc/postfix/pgsql-virtual_uid_maps.cf
+    virtual_gid_maps = pgsql:/etc/postfix/pgsql-virtual_gid_maps.cf
+    virtual_mailbox_base = /
+    virtual_mailbox_maps = pgsql:/etc/postfix/pgsql-virtual_mailbox_maps.cf
+
+    # dovecot LDA
+    dovecot_destination_recipient_limit = 1
+    virtual_transport = dovecot:
+
+    # dovecot SASL
+    smtpd_sasl_type = dovecot
+    smtpd_sasl_path = private/auth
+    smtpd_sasl_auth_enable = yes
+    smtpd_sasl_local_domain = $myhostname
+    smtpd_sasl_security_options = noplaintext, noanonymous
+
+
+
+Installing the Virtual Mail Manager and configure the rest
+
+    Installing from SVN
+    after checking out type
+        ./install
+    edit all the pgsql-*.cf files in /etc/postfix
+
+    reload postfix
+
+    # configure the Virtual Mail Manager
+    vmm configure
+
+    # for help type
+    vmm help
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/TODO	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,14 @@
+# $Id$
+
+- general
+    - add transport to user tbl (per account) / fall back transport via dom tbl
+    - make default transport configurable via vmm.cfg (for domains and
+      accounts
+    - write manpages
+
+- vmm
+    - add support for relocated_map
+
+- VirtualMailManager/Alias.py
+    - check if account exists, when destination is in the same domain
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/Account.py	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+# opyright 2007-2008 VEB IT
+# See COPYING for distribution information.
+# $Id$
+
+"""Virtual Mail Manager's Account class to manage email accounts."""
+
+__author__ = 'Pascal Volk <p.volk@veb-it.de>'
+__version__ = 'rev '+'$Rev$'.split()[1]
+__date__ = '$Date$'.split()[1]
+
+from Exceptions import VMMAccountException
+from Domain import Domain
+import constants.ERROR as ERR
+
+class Account:
+    """Class to manage email accounts."""
+    def __init__(self, dbh, basedir, address, password=None):
+        self._dbh = dbh
+        self._base = basedir
+        self._base = None
+        self._addr = address
+        self._localpart = None
+        self._name = None
+        self._uid = 0
+        self._gid = 0
+        self._passwd = password
+        self._home = None
+        self._setAddr(address)
+        self._exists()
+        if self._isAlias():
+            raise VMMAccountException(
+            ('There is already an alias with address «%s»' % address,
+                ERR.ALIAS_EXISTS))
+
+    def _exists(self):
+        dbc = self._dbh.cursor()
+        dbc.execute("SELECT uid FROM users WHERE gid=%s AND local_part=%s",
+                self._gid, self._localpart)
+        uid = dbc.fetchone()
+        dbc.close()
+        if uid is not None:
+            self._uid = uid[0]
+            return True
+        else:
+            return False
+
+    def _isAlias(self):
+        dbc = self._dbh.cursor()
+        dbc.execute("SELECT id FROM alias WHERE gid=%s AND address=%s",
+                self._gid, self._localpart)
+        aid = dbc.fetchone()
+        dbc.close()
+        if aid is not None:
+            return True
+        else:
+            return False
+
+    def _setAddr(self, address):
+        self._localpart, d = address.split('@')
+        dom = Domain(self._dbh, d, self._base)
+        self._gid = dom.getID()
+        self._base = dom.getDir()
+        if self._gid == 0:
+            raise VMMAccountException(("Domain %s doesn't exist." % d,
+                ERR.NO_SUCH_DOMAIN))
+
+    def _setID(self):
+        dbc = self._dbh.cursor()
+        dbc.execute("SELECT nextval('users_uid')")
+        self._uid = dbc.fetchone()[0]
+        dbc.close()
+
+    def _prepare(self):
+        self._setID()
+        self._home = "%i" % self._uid
+
+    def _switchState(self, state):
+        if not isinstance(state, bool):
+            return False
+        if self._uid < 1:
+            raise VMMAccountException(("Account doesn't exists",
+                ERR.NO_SUCH_ACCOUNT))
+        dbc = self._dbh.cursor()
+        dbc.execute("""UPDATE users SET disabled=%s WHERE local_part=%s\
+ AND gid=%s""", state, self._localpart, self._gid)
+        if dbc.rowcount > 0:
+            self._dbh.commit()
+        dbc.close()
+
+    def getUID(self):
+        return self._uid
+
+    def getGID(self):
+        return self._gid
+
+    def getDir(self, directory):
+        if directory == 'domain':
+            return '%s' % self._base
+        elif directory == 'home':
+            return '%s/%i' % (self._base, self._uid)
+
+    def enable(self):
+        self._switchState(False)
+
+    def disable(self):
+        self._switchState(True)
+
+    def save(self, mail):
+        if self._uid < 1:
+            self._prepare()
+            dbc = self._dbh.cursor()
+            dbc.execute("""INSERT INTO users (local_part, passwd, uid, gid,\
+ home, mail) VALUES (%s, %s, %s, %s, %s, %s)""", self._localpart,
+                    self._passwd, self._uid, self._gid, self._home, mail)
+            self._dbh.commit()
+            dbc.close()
+        else:
+            raise VMMAccountException(('Account already exists.',
+                ERR.ACCOUNT_EXISTS))
+       
+    def modify(self, what, value):
+        if self._uid == 0:
+            raise VMMAccountException(("Account doesn't exists",
+                ERR.NO_SUCH_ACCOUNT))
+        if what not in ['name', 'password']:
+            return False
+        dbc = self._dbh.cursor()
+        if what == 'password':
+            dbc.execute("UPDATE users SET passwd=%s WHERE local_part=%s AND\
+ gid=%s", value, self._localpart, self._gid)
+        else:
+            dbc.execute("UPDATE users SET name=%s WHERE local_part=%s AND\
+ gid=%s", value, self._localpart, self._gid)
+        if dbc.rowcount > 0:
+            self._dbh.commit()
+        dbc.close()
+
+    def getInfo(self):
+        dbc = self._dbh.cursor()
+        dbc.execute("SELECT name, uid, gid, home, mail, disabled FROM users\
+ WHERE local_part=%s AND gid=%s", self._localpart, self._gid)
+        info = dbc.fetchone()
+        dbc.close()
+        if info is None:
+            raise VMMAccountException(("Account doesn't exists",
+                ERR.NO_SUCH_ACCOUNT))
+        else:
+            keys = ['name', 'uid', 'gid', 'home', 'mail', 'disabled']
+            info = dict(zip(keys, info))
+            if bool(info['disabled']):
+                info['disabled'] = 'Yes'
+            else:
+                info['disabled'] = 'No'
+            info['address'] = self._addr
+            info['home'] = '%s/%s' % (self._base, info['home'])
+            return info
+
+    def delete(self):
+        if self._uid > 0:
+            dbc = self._dbh.cursor()
+            dbc.execute("DELETE FROM users WHERE gid=%s AND local_part=%s",
+                    self._gid, self._localpart)
+            if dbc.rowcount > 0:
+                self._dbh.commit()
+            dbc.close()
+        else:
+            raise VMMAccountException(("Account doesn't exists",
+                ERR.NO_SUCH_ACCOUNT))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/Alias.py	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+# opyright 2007-2008 VEB IT
+# See COPYING for distribution information.
+# $Id$
+
+"""Virtual Mail Manager's Alias class to manage email aliases."""
+
+__author__ = 'Pascal Volk <p.volk@veb-it.de>'
+__version__ = 'rev '+'$Rev$'.split()[1]
+__date__ = '$Date$'.split()[1]
+
+from Exceptions import VMMAliasException
+from Domain import Domain
+import constants.ERROR as ERR
+
+class Alias:
+    """Class to manage email accounts."""
+    def __init__(self, dbh, address, basedir, destination=None):
+        if address == destination:
+            raise VMMAliasException(('Address and destination are identical.',
+                ERR.ALIAS_ADDR_DEST_IDENTICAL))
+        self._dbh = dbh
+        self._addr = address
+        self._dest = destination
+        self._localpart = None
+        self._gid = 0
+        self._aid = 0
+        self._setAddr(basedir)
+        if not self._dest is None:
+            self._exists()
+        if self._isAccount():
+            raise VMMAliasException(
+            ('There is already an account with address «%s»' % self._addr,
+                ERR.ACCOUNT_EXISTS))
+
+    def _exists(self):
+        dbc = self._dbh.cursor()
+        dbc.execute("SELECT id FROM alias WHERE gid=%s AND address=%s\
+ AND destination=%s", self._gid, self._localpart, self._dest)
+        aid = dbc.fetchone()
+        dbc.close()
+        if aid is not None:
+            self._aid = aid[0]
+            return True
+        else:
+            return False
+
+    def _isAccount(self):
+        dbc = self._dbh.cursor()
+        dbc.execute("SELECT uid FROM users WHERE gid=%s AND local_part=%s",
+                self._gid, self._localpart)
+        uid = dbc.fetchone()
+        dbc.close()
+        if uid is not None:
+            return True
+        else:
+            return False
+        
+    def _setAddr(self, basedir):
+        self._localpart, d = self._addr.split('@')
+        dom = Domain(self._dbh, d, basedir)
+        self._gid = dom.getID()
+        if self._gid == 0:
+            raise VMMAliasException(("Domain «%s» doesn't exist." % d,
+                ERR.NO_SUCH_DOMAIN))
+
+    def save(self):
+        if self._dest is None:
+           raise VMMAliasException(('No destination address for alias denoted.',
+               ERR.ALIAS_MISSING_DEST))
+        if self._aid < 1:
+            dbc = self._dbh.cursor()
+            dbc.execute("INSERT INTO alias (gid, address, destination) VALUES\
+ (%s, %s, %s)", self._gid, self._localpart, self._dest)
+            self._dbh.commit()
+            dbc.close()
+        else:
+            raise VMMAliasException(("Alias already exists.", ERR.ALIAS_EXISTS))
+
+    def getInfo(self):
+        dbc = self._dbh.cursor()
+        dbc.execute('SELECT destination FROM alias WHERE gid=%s AND address=%s',
+                self._gid, self._localpart)
+        destinations = dbc.fetchall()
+        dbc.close()
+        if len(destinations) > 0:
+            targets = []
+            for destination in destinations:
+                targets.append(destination[0])
+            return targets
+        else:
+            raise VMMAliasException(("Alias doesn't exists", ERR.NO_SUCH_ALIAS))
+
+    def delete(self):
+        dbc = self._dbh.cursor()
+        dbc.execute("DELETE FROM alias WHERE gid=%s AND address=%s",
+                self._gid, self._localpart)
+        rowcount = dbc.rowcount
+        dbc.close()
+        if rowcount > 0:
+            self._dbh.commit()
+        else:
+            raise VMMAliasException(("Alias doesn't exists", ERR.NO_SUCH_ALIAS))
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/Config.py	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,154 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+# opyright 2007-2008 VEB IT
+# See COPYING for distribution information.
+# $Id$
+
+"""Configurtion class for read, modify and write the
+configuration from Virtual Mail Manager.
+
+"""
+
+__author__ = 'Pascal Volk <p.volk@veb-it.de>'
+__version__ = 'rev '+'$Rev$'.split()[1]
+__date__ = '$Date$'.split()[1]
+
+import sys
+from shutil import copy2
+from ConfigParser import ConfigParser
+
+from Exceptions import VMMConfigException
+import constants.EXIT as EXIT
+
+class VMMConfig(ConfigParser):
+    """This class is for configure the mailadmin.
+
+    You can specify settings for the database connection
+    and maildirectories.
+
+    """
+    missingOptCtr = -1
+
+    def __init__(self, filename):
+        """Creates a new VMMConfig instance
+
+        Keyword arguments:
+        filename -- name of the configuration file
+        """
+        ConfigParser.__init__(self)
+        self.__cfgFileName = filename
+        self.__cfgFile = None
+        self.__VMMsections = ['database', 'maildir', 'domdir', 'bin', 'misc',
+                'config']
+        self.__changes = False
+        self.__missingSect = []
+        self.__dbopts = [
+                ['host', 'localhot'],
+                ['user', 'vmm'],
+                ['pass', 'your secret password'],
+                ['name', 'mailsys']
+                ]
+        self.__mdopts = [
+                ['base', '/home/mail'],
+                ['folder', 'Maildir'],
+                ['mode', 448],
+                ['diskusage', 'false'],
+                ['delete', 'false']
+                ]
+        self.__domdopts = [
+                ['mode', 504],
+                ['delete', 'false']
+                ]
+        self.__binopts = [
+                ['dovecotpw', '/usr/sbin/dovecotpw'],
+                ['du', '/usr/bin/du']
+                ]
+        self.__miscopts = [
+                ['passwdscheme', 'CRAM-MD5'],
+                ['gid_mail', 8],
+                ['forcedel', 'false']
+                ]
+
+    def load(self):
+        """Loads the configuration, r/o"""
+        try:
+            self.__cfgFile = file(self.__cfgFileName, 'r')
+        except:
+            raise
+        self.readfp(self.__cfgFile)
+        self.__cfgFile.close()
+
+    def getsections(self):
+        """Return a list with all configurable sections."""
+        return self.__VMMsections[:-1]
+
+    def configure(self, sections):
+        """Interactive method for configuring all options in the given section
+
+        Keyword arguments:
+        sections -- list of strings
+        """
+        if not isinstance(sections, list):
+            raise TypeError("Argument 'sections' is not a list.")
+        # if [config] done = false (default at 1st run),
+        # then set changes true
+        try:
+            if not self.getboolean('config', 'done'):
+                self.__changes = True
+        except ValueError:
+            self.set('config', 'done', 'False')
+            self.__changes = True
+        for s in sections:
+            if s == 'config':
+                pass
+            else:
+                print '* Config section: %s' % s
+            for opt, val in self.items(s):
+                newval = raw_input('Enter new value for %s [%s]: ' %(opt, val))
+                if newval and newval != val:
+                    self.set(s, opt, newval)
+                    self.__changes = True
+            print
+        if self.__changes:
+            self.__saveChanges()
+
+    def __saveChanges(self):
+        """Writes changes to the configuration file."""
+        self.set('config', 'done', 'true')
+        copy2(self.__cfgFileName, self.__cfgFileName+'.bak')
+        self.__cfgFile = file(self.__cfgFileName, 'w')
+        self.write(self.__cfgFile)
+        self.__cfgFile.close()
+
+    def __chkSections(self):
+        """Checks if all configuration sections are existing."""
+        retval = False
+        for s in self.__VMMsections:
+            if not self.has_section(s):
+                self.__missingSect.append(s)
+            else:
+                retval = self.__chkOptions(s)
+        return retval
+
+    def __chkOptions(self, section):
+        """Checks if all configuration options in section are existing.
+
+        Keyword arguments:
+        section -- the section to be checked
+        """
+        retval = True
+        VMMConfig.missingOptCtr += 1
+        self.__missingOpt.append([])
+        if section == 'database':
+            opts = self.__dbopts
+        elif section == 'maildir':
+            opts = self.__mdopts
+        elif section == 'bin':
+            opts = self.__binopts
+        elif section == 'misc':
+            opts = self.__miscopts
+        for o, v in opts:
+            if not self.has_option(section, o):
+                self.__missingOpt[VMMConfig.missingOptCtr].append(o)
+                retval = False
+        return retval
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/Domain.py	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,221 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+# opyright 2007-2008 VEB IT
+# See COPYING for distribution information.
+# $Id$
+
+"""Virtual Mail Manager's Domain class to manage email domains."""
+
+__author__ = 'Pascal Volk <p.volk@veb-it.de>'
+__version__ = 'rev '+'$Rev$'.split()[1]
+__date__ = '$Date$'.split()[1]
+
+from random import choice
+
+from Exceptions import VMMDomainException
+import constants.ERROR as ERR
+
+MAILDIR_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz'
+
+class Domain:
+    """Class to manage email domains."""
+    def __init__(self, dbh, domainname, basedir, transport=None):
+        """Creates a new Domain instance.
+        
+        Keyword arguments:
+        dbh -- a pyPgSQL.PgSQL.connection
+        domainname -- name of the domain (str)
+        transport -- see transport(5), default 'dovecot:'  (str)
+        """
+        self._dbh = dbh
+        self._name = domainname
+        self._basedir = basedir
+        if transport is None:
+            self._transport = 'dovecot:'
+        else:
+            self._transport = transport
+        self._id = 0
+        self._domaindir = None
+        self._exists()
+
+    def _exists(self):
+        """Checks if the domain already exists.
+
+        If the domain exists _id will be set and returns True, otherwise False
+        will be returned.
+        """
+        dbc = self._dbh.cursor()
+        dbc.execute("SELECT gid, domaindir FROM domains WHERE domainname=%s",
+                self._name)
+        result = dbc.fetchone()
+        dbc.close()
+        if result is not None:
+            self._id, self._domaindir = result[0], result[1]
+            return True
+        else:
+            return False
+
+    def _setID(self):
+        """Sets the ID of the domain."""
+        dbc = self._dbh.cursor()
+        dbc.execute("SELECT nextval('domains_gid')")
+        self._id = dbc.fetchone()[0]
+        dbc.close()
+
+    def _prepare(self):
+        self._setID()
+        self._domaindir = "%s/%s/%i" % (self._basedir, choice(MAILDIR_CHARS),
+                self._id)
+
+    def _has(self, what):
+        """Checks if aliases or accounts are assigned to the domain.
+
+        If there are assigned accounts or aliases True will be returned,
+        otherwise False will be returned.
+
+        Keyword arguments:
+        what -- 'alias' or 'users' (strings)
+        """
+        if what not in ['alias', 'users']:
+            return False
+        dbc = self._dbh.cursor()
+        if what == 'users':
+            dbc.execute("SELECT count(gid) FROM users WHERE gid=%s", self._id)
+        else:
+            dbc.execute("SELECT count(gid) FROM alias WHERE gid=%s", self._id)
+        count = dbc.fetchone()
+        dbc.close()
+        if count[0] > 0:
+            return True
+        else:
+            return False
+
+    def _chkDelete(self, delUser, delAlias):
+        """Checks dependencies for deletion.
+        
+        Keyword arguments:
+        delUser -- ignore available accounts (bool)
+        delAlias -- ignore available aliases (bool)
+        """
+        if not delUser:
+            hasUser = self._has('users')
+        else:
+            hasUser = False
+        if not delAlias:
+            hasAlias = self._has('alias')
+        else:
+            hasAlias = False
+        if hasUser and hasAlias:
+            raise VMMDomainException(('There are accounts and aliases.',
+                ERR.ACCOUNT_AND_ALIAS_PRESENT))
+        elif hasUser:
+            raise VMMDomainException(('There are accounts.',
+                ERR.ACCOUNT_PRESENT))
+        elif hasAlias:
+            raise VMMDomainException(('There are aliases.', ERR.ALIAS_PRESENT))
+
+    def save(self):
+        """Stores the new domain in the database."""
+        if self._id < 1:
+            self._prepare()
+            dbc = self._dbh.cursor()
+            dbc.execute("INSERT INTO domains (gid, domainname, transport,\
+ domaindir) VALUES (%s, %s, %s, %s)", self._id, self._name, self._transport,
+                self._domaindir)
+            self._dbh.commit()
+            dbc.close()
+        else:
+            raise VMMDomainException(('Domain already exists.',
+                ERR.DOMAIN_EXISTS))
+
+    def delete(self, delUser=False, delAlias=False):
+        """Deletes the domain.
+
+        Keyword arguments:
+        delUser -- force deletion of available accounts (bool)
+        delAlias -- force deletion of available aliases (bool)
+        """
+        if self._id > 0:
+            self._chkDelete(delUser, delAlias)
+            dbc = self._dbh.cursor()
+            dbc.execute('DELETE FROM alias WHERE gid=%s', self._id)
+            dbc.execute('DELETE FROM users WHERE gid=%s', self._id)
+            dbc.execute('DELETE FROM relocated WHERE gid=%s', self._id)
+            dbc.execute('DELETE FROM domains WHERE gid=%s', self._id)
+            self._dbh.commit()
+            dbc.close()
+        else:
+            raise VMMDomainException(("Domain doesn't exist yet.",
+                ERR.NO_SUCH_DOMAIN))
+
+    def updateTransport(self, transport):
+        """Sets a new transport for the domain.
+
+        Keyword arguments:
+        transport -- the new transport (str)
+        """
+        if self._id > 0:
+            dbc = self._dbh.cursor()
+            dbc.execute("UPDATE domains SET transport=%s WHERE gid=%s",
+                    transport, self._id)
+            if dbc.rowcount > 0:
+                self._dbh.commit()
+            dbc.close()
+        else:
+            raise VMMDomainException(("Domain doesn't exist yet.",
+                ERR.NO_SUCH_DOMAIN))
+
+    def getID(self):
+        """Returns the ID of the domain."""
+        return self._id
+
+    def getDir(self):
+        """Returns the directory of the domain."""
+        return self._domaindir
+
+    def getInfo(self):
+        """Returns a dictionary with information about the domain."""
+        sql = """\
+SELECT gid, domainname, transport, domaindir, count(uid) AS accounts, aliases
+  FROM domains
+       LEFT JOIN users USING (gid)
+       LEFT JOIN vmm_alias_count USING (gid)
+ WHERE gid = %i
+GROUP BY gid, domainname, transport, domaindir, aliases""" % self._id
+        dbc = self._dbh.cursor()
+        dbc.execute(sql)
+        info = dbc.fetchone()
+        dbc.close()
+        if info is None:
+            raise VMMDomainException(("Domain doesn't exist yet.",
+                ERR.NO_SUCH_DOMAIN))
+        else:
+            keys = ['gid', 'domainname', 'transport', 'domaindir', 'accounts',
+                    'aliases']
+            return dict(zip(keys, info))
+
+    def getAccounts(self):
+        """Returns a list with all accounts from the domain."""
+        dbc = self._dbh.cursor()
+        dbc.execute("SELECT userid AS users FROM dovecot_user WHERE gid = %s",
+                self._id)
+        users = dbc.fetchall()
+        dbc.close()
+        accounts = []
+        if len(users) > 0:
+            for account in users:
+                accounts.append(account[0])
+        return accounts
+
+    def getAliases(self):
+        """Returns a list with all aliases from the domain."""
+        dbc = self._dbh.cursor()
+        dbc.execute("SELECT DISTINCT address FROM postfix_alias WHERE gid=%s",
+                self._id)
+        addresses = dbc.fetchall()
+        dbc.close()
+        aliases = []
+        if len(addresses) > 0:
+            for alias in addresses:
+                aliases.append(alias[0])
+        return aliases
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/Exceptions.py	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+# opyright 2007-2008 VEB IT
+# See COPYING for distribution information.
+# $Id$
+
+"""Exception classes for Virtual Mail Manager"""
+
+__author__ = 'Pascal Volk <p.volk@veb-it.de>'
+__version__ = 'rev '+'$Rev$'.split()[1]
+__date__ = '$Date$'.split()[1]
+
+class VMMException(Exception):
+    """Ausnahmeklasse für die Klasse VirtualMailManager"""
+    def __init__(self, msg):
+        Exception.__init__(self, msg)
+
+class VMMConfigException(Exception):
+    """Ausnahmeklasse für Konfigurationssausnamhem"""
+    def __init__(self, msg):
+        Exception.__init__(self, msg)
+
+class VMMPermException(Exception):
+    """Ausnahmeklasse für Berechtigungsausnamhem"""
+    pass
+
+class VMMNotRootException(Exception):
+    """Ausnahmeklasse für unberechtige Zugriffe"""
+    pass
+
+class VMMDomainException(VMMException):
+    """Ausnahmeklasse für Domainausnamhem"""
+    def __init__(self, msg):
+        VMMException.__init__(self, msg)
+
+class VMMAccountException(VMMException):
+    """Ausnahmeklasse für Accountausnamhem"""
+    def __init__(self, msg):
+        VMMException.__init__(self, msg)
+
+class VMMAliasException(VMMException):
+    """Ausnahmeklasse für Aliasausnamhem"""
+    def __init__(self, msg):
+        VMMException.__init__(self, msg)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/VirtualMailManager.py	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,440 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+# opyright 2007-2008 VEB IT
+# See COPYING for distribution information.
+# $Id$
+
+"""The main class for vmm."""
+
+__author__ = 'Pascal Volk <p.volk@veb-it.de>'
+__version__ = 'rev '+'$Rev$'.split()[1]
+__date__ = '$Date$'.split()[1]
+
+import os
+import re
+import sys
+from encodings.idna import ToASCII, ToUnicode
+from shutil import rmtree
+from subprocess import Popen, PIPE
+
+from pyPgSQL import PgSQL # python-pgsql - http://pypgsql.sourceforge.net
+
+from Exceptions import *
+import constants.ERROR as ERR
+from Config import VMMConfig as Cfg
+from Account import Account
+from Alias import Alias
+from Domain import Domain
+
+RE_ASCII_CHARS = """^[\x20-\x7E]*$"""
+RE_DOMAIN = """^(?:[a-z0-9-]{1,63}\.){1,}[a-z]{2,6}$"""
+RE_LOCALPART = """[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]"""
+re.compile(RE_ASCII_CHARS)
+re.compile(RE_DOMAIN)
+
+ENCODING_IN = sys.getfilesystemencoding()
+ENCODING_OUT = sys.stdout.encoding or sys.getfilesystemencoding()
+
+class VirtualMailManager:
+    """The main class for vmm"""
+    def __init__(self):
+        """Creates a new VirtualMailManager instance.
+        Throws a VMMNotRootException if your uid is greater 0.
+        """
+        self.__cfgFileName = '/usr/local/etc/vmm.cfg'
+        self.__permWarnMsg = "fix permissions for '"+self.__cfgFileName \
+            +"'.\n`chmod 0600 "+self.__cfgFileName+"` would be great.\n"
+        self.__warnings = []
+        self.__Cfg = None
+        self.__dbh = None
+
+        if os.geteuid():
+            raise VMMNotRootException("You are not root.\n\tGood bye!\n")
+        if self.__chkCfgFile():
+            self.__Cfg = Cfg(self.__cfgFileName)
+            self.__Cfg.load()
+            self.__cfgSections = self.__Cfg.getsections()
+        self.__chkenv()
+
+    def __chkCfgFile(self):
+        """Checks the configuration file, returns bool"""
+        if not os.path.isfile(self.__cfgFileName):
+            raise IOError("Fatal error: The file "+self.__cfgFileName+ \
+                    " does not exists.\n")
+        fstat = os.stat(self.__cfgFileName)
+        try:
+            fmode = self.__getFileMode()
+        except:
+            raise
+        if fmode % 100 and fstat.st_uid != fstat.st_gid \
+        or fmode % 10 and fstat.st_uid == fstat.st_gid:
+            raise VMMPermException(self.__permWarnMsg)
+        else:
+            return True
+
+    def __chkenv(self):
+        """"""
+        if not os.path.exists(self.__Cfg.get('maildir', 'base')):
+            old_umask = os.umask(0007)
+            os.makedirs(self.__Cfg.get('maildir', 'base'), 0770)
+            os.umask(old_umask)
+        elif not os.path.isdir(self.__Cfg.get('maildir', 'base')):
+            raise VMMException(('%s is not a directory' %
+                self.__Cfg.get('maildir', 'base'), ERR.NO_SUCH_DIRECTORY))
+        for opt, val in self.__Cfg.items('bin'):
+            if not os.path.exists(val):
+                raise VMMException(("%s doesn't exists.", ERR.NO_SUCH_BINARY))
+            elif not os.access(val, os.X_OK):
+                raise VMMException(("%s is not executable.", ERR.NOT_EXECUTABLE))
+
+    def __getFileMode(self):
+        """Determines the file access mode from file __cfgFileName,
+        returns int.
+        """
+        try:
+            return int(oct(os.stat(self.__cfgFileName).st_mode & 0777))
+        except:
+            raise
+
+    def __dbConnect(self):
+        """Creates a pyPgSQL.PgSQL.connection instance."""
+        try:
+            self.__dbh =  PgSQL.connect(
+                    database=self.__Cfg.get('database', 'name'),
+                    user=self.__Cfg.get('database', 'user'),
+                    host=self.__Cfg.get('database', 'host'),
+                    password=self.__Cfg.get('database', 'pass'),
+                    client_encoding='utf8', unicode_results=True)
+            dbc = self.__dbh.cursor()
+            dbc.execute("SET NAMES 'UTF8'")
+            dbc.close()
+        except PgSQL.libpq.DatabaseError, e:
+            raise VMMException((str(e), ERR.DATABASE_ERROR))
+
+    def __chkLocalpart(self, localpart):
+        """Validates the local part of an email address.
+        
+        Keyword arguments:
+        localpart -- the email address that should be validated (str)
+        """
+        if len(localpart) > 64:
+            raise VMMException(('The local part is too long',
+                ERR.LOCALPART_TOO_LONG))
+        if re.compile(RE_LOCALPART).search(localpart):
+            raise VMMException((
+                'The local part «%s» contains invalid characters.' % localpart,
+                ERR.LOCALPART_INVALID))
+        return localpart
+
+    def __idn2ascii(self, domainname):
+        """Converts an idn domainname in punycode.
+        
+        Keyword arguments:
+        domainname -- the domainname to convert (str)
+        """
+        tmp = []
+        for label in domainname.split('.'):
+            if len(label) == 0:
+                continue
+            tmp.append(ToASCII(unicode(label, ENCODING_IN)))
+        return '.'.join(tmp)
+
+    def __ace2idna(self, domainname):
+        """Convertis a domainname from ACE according to IDNA
+        
+        Keyword arguments:
+        domainname -- the domainname to convert (str)
+        """
+        tmp = []
+        for label in domainname.split('.'):
+            if len(label) == 0:
+                continue
+            tmp.append(ToUnicode(label))
+        return '.'.join(tmp)
+
+    def __chkDomainname(self, domainname):
+        """Validates the domain name of an email address.
+        
+        Keyword arguments:
+        domainname -- the domain name that should be validated
+        """
+        if not re.match(RE_ASCII_CHARS, domainname):
+            domainname = self.__idn2ascii(domainname)
+        if len(domainname) > 255:
+            raise VMMException(('The domain name is too long.',
+                ERR.DOMAIN_TOO_LONG))
+        if not re.match(RE_DOMAIN, domainname):
+            raise VMMException(('The domain name is invalid.',
+                ERR.DOMAIN_INVALID))
+        return domainname
+
+    def __chkEmailadress(self, address):
+        try:
+            localpart, domain = address.split('@')
+        except ValueError:
+            raise VMMException(("Missing '@' sign in emailaddress «%s»." %
+                address, ERR.INVALID_ADDRESS))
+        except AttributeError:
+            raise VMMException(("'%s' looks not like an email address." %
+                address, ERR.INVALID_ADDRESS))
+        domain = self.__chkDomainname(domain)
+        localpart = self.__chkLocalpart(localpart)
+        return '%s@%s' % (localpart, domain)
+
+    def __getAccount(self, address, password=None):
+        address = self.__chkEmailadress(address)
+        self.__dbConnect()
+        if not password is None:
+            password = self.__pwhash(password)
+        return Account(self.__dbh, self.__Cfg.get('maildir', 'base'), address,
+                password)
+
+    def __getAlias(self, address, destination=None):
+        address = self.__chkEmailadress(address)
+        if not destination is None:
+            if destination.count('@'):
+                destination = self.__chkEmailadress(destination)
+            else:
+                destination = self.__chkLocalpart(destination)
+        self.__dbConnect()
+        return Alias(self.__dbh, address, self.__Cfg.get('maildir', 'base'),
+                destination)
+
+    def __getDomain(self, domainname, transport=None):
+        domainname = self.__chkDomainname(domainname)
+        self.__dbConnect()
+        return Domain(self.__dbh, domainname,
+                self.__Cfg.get('maildir', 'base'), transport)
+
+    def __getDiskUsage(self, directory):
+        """Estimate file space usage for the given directory.
+        
+        Keyword arguments:
+        directory -- the directory to summarize recursively disk usage for
+        """
+        return Popen([self.__Cfg.get('bin', 'du'), "-hs", directory],
+                stdout=PIPE).communicate()[0].split('\t')[0]
+
+    def __makedir(self, directory, mode=None, uid=None, gid=None):
+        if mode is None:
+            mode = self.__Cfg.getint('maildir', 'mode')
+        if uid is None:
+            uid = 0
+        if gid is None:
+            gid = 0
+        os.makedirs(directory, mode)
+        os.chown(directory, uid, gid)
+
+    def __domdirmake(self, domdir, gid):
+        os.umask(0006)
+        oldpwd = os.getcwd()
+        basedir = self.__Cfg.get('maildir', 'base')
+        domdirdirs = domdir.replace(basedir+'/', '').split('/')
+
+        os.chdir(basedir)
+        if not os.path.isdir(domdirdirs[0]):
+            self.__makedir(domdirdirs[0], 489, 0,
+                    self.__Cfg.getint('misc', 'gid_mail'))
+        os.chdir(domdirdirs[0])
+        os.umask(0007)
+        self.__makedir(domdirdirs[1], self.__Cfg.getint('domdir', 'mode'), 0,
+                gid)
+        os.chdir(oldpwd)
+
+    def __maildirmake(self, domdir, uid, gid):
+        """Creates maildirs and maildir subfolders.
+
+        Keyword arguments:
+        uid -- user id from the account
+        gid -- group id from the account
+        """
+        os.umask(0007)
+        oldpwd = os.getcwd()
+        os.chdir(domdir)
+
+        maildir = '%s' % self.__Cfg.get('maildir', 'folder')
+        folders = [maildir , maildir+'/.Drafts', maildir+'/.Sent',
+                maildir+'/.Templates', maildir+'/.Trash']
+        subdirs = ['cur', 'new', 'tmp']
+        mode = self.__Cfg.getint('maildir', 'mode')
+
+        self.__makedir('%s' % uid, mode, uid, gid)
+        os.chdir('%s' % uid)
+        for folder in folders:
+            self.__makedir(folder, mode, uid, gid)
+            for subdir in subdirs:
+                self.__makedir(folder+'/'+subdir, mode, uid, gid)
+        os.chdir(oldpwd)
+
+    def __maildirdelete(self, domdir, uid, gid):
+        if uid > 0 and gid > 0:
+            maildir = '%s' % uid
+            if maildir.count('..') or domdir.count('..'):
+                raise VMMException(('FATAL: ".." in maildir path detected.',
+                    ERR.FOUND_DOTS_IN_PATH))
+            if os.path.isdir(domdir):
+                os.chdir(domdir)
+                if os.path.isdir(maildir):
+                    mdstat = os.stat(maildir)
+                    if (mdstat.st_uid, mdstat.st_gid) != (uid, gid):
+                        raise VMMException(
+                            ('FATAL: owner/group mismatch in maildir detected',
+                                ERR.MAILDIR_PERM_MISMATCH))
+                    rmtree(maildir, ignore_errors=True)
+
+    def __domdirdelete(self, domdir, gid):
+        if gid > 0:
+            basedir = '%s' % self.__Cfg.get('maildir', 'base')
+            domdirdirs = domdir.replace(basedir+'/', '').split('/')
+            if basedir.count('..') or domdir.count('..'):
+                raise VMMException(
+                        ('FATAL: ".." in domain directory path detected.',
+                            ERR.FOUND_DOTS_IN_PATH))
+            if os.path.isdir('%s/%s' % (basedir, domdirdirs[0])):
+                os.chdir('%s/%s' % (basedir, domdirdirs[0]))
+                if os.lstat(domdirdirs[1]).st_gid != gid:
+                    raise VMMException(
+                    ('FATAL: group mismatch in domain directory detected',
+                        ERR.DOMAINDIR_GROUP_MISMATCH))
+                rmtree(domdirdirs[1], ignore_errors=True)
+
+    def __pwhash(self, password, scheme=None, user=None):
+        # XXX alle Schemen berücksichtigen XXX
+        if scheme is None:
+            scheme = self.__Cfg.get('misc', 'passwdscheme')
+        return Popen([self.__Cfg.get('bin', 'dovecotpw'), '-s', scheme, '-p',
+            password], stdout=PIPE).communicate()[0][len(scheme)+2:-1]
+
+    def hasWarnings(self):
+        """Checks if warnings are present, returns bool."""
+        return bool(len(self.__warnings))
+
+    def getWarnings(self):
+        """Returns a list with all available warnings."""
+        return self.__warnings
+
+    def setupIsDone(self):
+        """Checks if vmm is configured, returns bool"""
+        try:
+            return self.__Cfg.getboolean('config', 'done')
+        except ValueError, e:
+            raise VMMConfigException('Configurtion error: "'+str(e)
+                +'"\n(in section "Connfig", option "done")'
+                +'\nsee also: vmm.cfg(5)\n')
+
+    def configure(self, section=None):
+        """Starts interactive configuration.
+
+        Configures in interactive mode options in the given section.
+        If no section is given (default) all options from all sections
+        will be prompted.
+
+        Keyword arguments:
+        section -- the section to configure (default None):
+            'database', 'maildir', 'bin' or 'misc'
+        """
+        try:
+            if not section:
+                self.__Cfg.configure(self.__cfgSections)
+            elif section not in self.__cfgSections:
+                raise VMMException(("Invalid section: «%s»" % section,
+                    ERR.INVALID_SECTION))
+            else:
+                self.__Cfg.configure([section])
+        except:
+            raise
+
+    def domain_add(self, domainname, transport=None):
+        dom = self.__getDomain(domainname, transport)
+        dom.save()
+        self.__domdirmake(dom.getDir(), dom.getID())
+
+    def domain_transport(self, domainname, transport):
+        dom = self.__getDomain(domainname, None)
+        dom.updateTransport(transport)
+
+    def domain_delete(self, domainname, force=None):
+        if not force is None and force not in ['deluser','delalias','delall']:
+            raise VMMDomainException(('Invalid option: «%s»' % force,
+                ERR.INVALID_OPTION))
+        dom = self.__getDomain(domainname)
+        gid = dom.getID()
+        domdir = dom.getDir()
+        if self.__Cfg.getboolean('misc', 'forcedel') or force == 'delall':
+            dom.delete(True, True)
+        elif force == 'deluser':
+            dom.delete(delUser=True)
+        elif force == 'delalias':
+            dom.delete(delAlias=True)
+        else:
+            dom.delete()
+        if self.__Cfg.getboolean('domdir', 'delete'):
+            self.__domdirdelete(domdir, gid)
+
+    def domain_info(self, domainname, detailed=None):
+        dom = self.__getDomain(domainname)
+        dominfo = dom.getInfo()
+        if dominfo['domainname'].startswith('xn--'):
+            dominfo['domainname'] += ' (%s)'\
+                % self.__ace2idna(dominfo['domainname'])
+        if dominfo['aliases'] is None:
+            dominfo['aliases'] = 0
+        if detailed is None:
+            return dominfo
+        elif detailed == 'detailed':
+            return dominfo, dom.getAccounts(), dom.getAliases()
+        else:
+            raise VMMDomainException(('Invalid option: «%s»' % detailed,
+                ERR.INVALID_OPTION))
+
+    def user_add(self, emailaddress, password):
+        acc = self.__getAccount(emailaddress, password)
+        acc.save(self.__Cfg.get('maildir', 'folder'))
+        self.__maildirmake(acc.getDir('domain'), acc.getUID(), acc.getGID())
+
+    def alias_add(self, aliasaddress, targetaddress):
+        alias = self.__getAlias(aliasaddress, targetaddress)
+        alias.save()
+
+    def user_delete(self, emailaddress):
+        acc = self.__getAccount(emailaddress)
+        uid = acc.getUID()
+        gid = acc.getGID()
+        acc.delete()
+        if self.__Cfg.getboolean('maildir', 'delete'):
+            self.__maildirdelete(acc.getDir('domain'), uid, gid)
+
+    def alias_info(self, aliasaddress):
+        alias = self.__getAlias(aliasaddress)
+        return alias.getInfo()
+
+    def alias_delete(self, aliasaddress):
+        alias = self.__getAlias(aliasaddress)
+        alias.delete()
+
+    def user_info(self, emailaddress, diskusage=False):
+        acc = self.__getAccount(emailaddress)
+        info = acc.getInfo()
+        if self.__Cfg.getboolean('maildir', 'diskusage') or diskusage:
+            info['disk usage'] = self.__getDiskUsage('%(home)s/%(mail)s' % info)
+        return info
+
+    def user_password(self, emailaddress, password):
+        acc = self.__getAccount(emailaddress)
+        acc.modify('password', self.__pwhash(password))
+
+    def user_name(self, emailaddress, name):
+        acc = self.__getAccount(emailaddress)
+        acc.modify('name', name)
+
+    def user_disable(self, emailaddress):
+        acc = self.__getAccount(emailaddress)
+        acc.disable()
+
+    def user_enable(self, emailaddress):
+        acc = self.__getAccount(emailaddress)
+        acc.enable()
+
+    def __del__(self):
+        if not self.__dbh is None and self.__dbh._isOpen:
+            self.__dbh.close()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/__init__.py	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+# opyright 2007-2008 VEB IT
+# See COPYING for distribution information.
+# $Id$
+# package placeholder
+#
+# EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/constants/ERROR.py	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+# opyright 2007-2008 VEB IT
+# See COPYING for distribution information.
+# $Id$
+
+ACCOUNT_AND_ALIAS_PRESENT = 20
+ACCOUNT_EXISTS = 21
+ACCOUNT_PRESENT = 22
+ALIAS_ADDR_DEST_IDENTICAL = 23
+ALIAS_EXISTS = 24
+ALIAS_MISSING_DEST = 25
+ALIAS_PRESENT = 26
+DATABASE_ERROR = 27
+DOMAINDIR_GROUP_MISMATCH = 28
+DOMAIN_EXISTS = 29
+DOMAIN_INVALID = 30
+DOMAIN_TOO_LONG = 31
+FOUND_DOTS_IN_PATH = 32
+INVALID_ADDRESS = 33
+INVALID_OPTION = 34
+INVALID_SECTION = 35
+LOCALPART_INVALID = 36
+LOCALPART_TOO_LONG = 37
+MAILDIR_PERM_MISMATCH = 38
+NOT_EXECUTABLE = 39
+NO_SUCH_ACCOUNT = 40
+NO_SUCH_ALIAS = 41
+NO_SUCH_BINARY = 42
+NO_SUCH_DIRECTORY = 43
+NO_SUCH_DOMAIN = 44
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/constants/EXIT.py	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,14 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+# opyright 2007-2008 VEB IT
+# See COPYING for distribution information.
+# $Id$
+
+MISSING_ARGS = 1
+UNKNOWN_OPTION = 2
+USER_INTERRUPT = 3
+
+CONF_WRONGPERM = 76
+CONF_NOPERM = 77
+CONF_NOFILE = 78
+CONF_ERROR = 79
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/constants/__init__.py	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+# opyright 2007-2008 VEB IT
+# See COPYING for distribution information.
+# $Id$
+# package placeholder
+#
+# EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/create_tables.pgsql	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,111 @@
+-- $Id$ 
+
+CREATE SEQUENCE domains_gid
+    START WITH 70000
+    INCREMENT BY 1
+    MINVALUE 70000
+    MAXVALUE 4294967294
+    NO CYCLE;
+
+CREATE SEQUENCE users_uid
+    START WITH 70000
+    INCREMENT BY 1
+    MINVALUE 70000
+    MAXVALUE 4294967294
+    NO CYCLE;
+
+CREATE TABLE domains (
+    gid         bigint NOT NULL DEFAULT nextval('domains_gid'),
+    domainname  varchar(255) NOT NULL,
+    transport   varchar(268) NOT NULL DEFAULT 'dovecot:', -- smtp:[255-char.host.name:50025]
+    domaindir   varchar(40) NOT NULL, --/srv/mail/$RAND/4294967294
+    CONSTRAINT pkey_domains PRIMARY KEY (gid),
+    CONSTRAINT ukey_domains UNIQUE (domainname)
+);
+
+CREATE TABLE users (
+    local_part  varchar(64) NOT NULL,-- only localpart w/o '@'
+    passwd      varchar(74) NOT NULL,-- {CRAM-MD5}+64hex numbers
+    name        varchar(128) NULL,
+    uid         bigint NOT NULL DEFAULT nextval('users_uid'),
+    gid         bigint NOT NULL,
+  --home        varchar(40) NOT NULL, --/home/virtualmail/4294967294/4294967294
+    home        bigint NOT NULL, -- 4294967294
+    mail        varchar(128) NOT NULL DEFAULT 'Maildir',
+    disabled    boolean NOT NULL DEFAULT FALSE,
+    CONSTRAINT pkye_users PRIMARY KEY (local_part, gid),
+    CONSTRAINT ukey_users_uid UNIQUE (uid),
+    CONSTRAINT fkey_users_gid_domains FOREIGN KEY (gid)
+        REFERENCES domains (gid)
+);
+
+CREATE SEQUENCE alias_id;
+CREATE TABLE alias (
+    id          bigint NOT NULL DEFAULT nextval('alias_id'),
+    gid         bigint NOT NULL,
+    address     varchar(256) NOT NULL,
+    destination varchar(320) NOT NULL,
+    CONSTRAINT pkey_alias PRIMARY KEY (gid, address, destination),
+    CONSTRAINT fkey_alias_gid_domains FOREIGN KEY (gid)
+        REFERENCES domains (gid)
+);
+
+CREATE SEQUENCE relocated_id;
+CREATE TABLE relocated (
+    id          bigint NOT NULL DEFAULT nextval('relocated_id'),
+    gid         bigint NOT NULL,
+    address     varchar(64) NOT NULL,
+    destination varchar(320) NOT NULL,
+    CONSTRAINT pkey_relocated PRIMARY KEY (gid, address),
+    CONSTRAINT fkey_relocated_gid_domains FOREIGN KEY (gid)
+        REFERENCES domains (gid)
+);
+
+CREATE OR REPLACE VIEW dovecot_password AS
+    SELECT local_part || '@' || domains.domainname AS user,
+           passwd AS password
+      FROM users
+           LEFT JOIN domains USING (gid);
+
+CREATE OR REPLACE VIEW dovecot_user AS
+    SELECT local_part || '@' || domains.domainname AS userid,
+           domains.domaindir || '/' || home AS home,
+           uid,
+           gid
+      FROM users
+           LEFT JOIN domains USING (gid);
+
+CREATE OR REPLACE VIEW postfix_gid AS
+    SELECT gid, domainname
+      FROM domains;
+
+CREATE OR REPLACE VIEW postfix_uid AS
+    SELECT local_part || '@' || domains.domainname AS address,
+           uid
+      FROM users
+           LEFT JOIN domains USING (gid);
+
+CREATE OR REPLACE VIEW postfix_maildir AS
+    SELECT local_part || '@' || domains.domainname AS address,
+           domains.domaindir || '/' || home || '/' || mail || '/' AS maildir
+      FROM users
+           LEFT JOIN domains USING (gid);
+
+CREATE OR REPLACE VIEW postfix_relocated AS
+    SELECT address || '@' || domains.domainname AS address, destination
+      FROM relocated
+           LEFT JOIN domains USING (gid);
+
+CREATE OR REPLACE VIEW postfix_alias AS
+    SELECT address || '@' || domains.domainname AS address, destination, gid
+      FROM alias
+           LEFT JOIN domains USING (gid);
+
+CREATE OR REPLACE VIEW postfix_transport AS
+    SELECT transport, domainname
+      FROM domains;
+
+CREATE OR REPLACE VIEW vmm_alias_count AS
+    SELECT count(DISTINCT address) AS aliases, gid
+      FROM alias 
+  GROUP BY gid;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/install.sh	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,29 @@
+#!/bin/bash
+# $Id$
+#
+# Installation script for the vmm
+# run: ./install.sh
+
+LANG=C
+PATH=/usr/sbin:/usr/bin
+INSTALL_OPTS="-g 0 -o 0 -p -v"
+PREFIX=/usr/local
+PF_CONFDIR=$(postconf -h config_directory)
+PF_GID=$(id -g postfix)
+
+if [ $(id -u) -ne 0 ]; then
+    echo "Run this script as root."
+    exit 1
+fi
+
+python setup.py install --prefix ${PREFIX}
+python setup.py clean --all >/dev/null
+
+install -b -m 0600 ${INSTALL_OPTS} vmm.cfg ${PREFIX}/etc/
+install -b -m 0640 -g ${PF_GID} -o 0 -p -v pgsql-*.cf ${PF_CONFDIR}/
+install -m 0700 ${INSTALL_OPTS} vmm ${PREFIX}/sbin/
+
+echo
+echo "Don't forget to edit ${PREFIX}/etc/vmm.cfg"
+echo "and ${PF_CONFDIR}/pgsql-*.cf files."
+echo
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgsql-relocated_maps.cf	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,12 @@
+# The hosts that Postfix will try to connect to
+hosts = host1.some.domain host2.some.domain
+
+# The user name and password to log into the pgsql server.
+user = postfix
+password = some_password
+
+# The database name on the servers.
+dbname = mailsys
+
+# Postfix 2.2 and later The SQL query template. See pgsql_table(5).
+query = SELECT destination FROM postfix_relocated WHERE address='%s'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgsql-smtpd_sender_login_maps.cf	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,13 @@
+# The hosts that Postfix will try to connect to
+hosts = host1.some.domain host2.some.domain
+
+# The user name and password to log into the pgsql server.
+user = postfix
+password = some_password
+
+# The database name on the servers.
+dbname = mailsys
+
+# Postfix 2.2 and later The SQL query template. See pgsql_table(5).
+query = SELECT address FROM postfix_maildir WHERE address='%s'
+ UNION SELECT destination FROM postfix_alias WHERE address='%s
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgsql-transport.cf	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,12 @@
+# The hosts that Postfix will try to connect to
+hosts = host1.some.domain host2.some.domain
+
+# The user name and password to log into the pgsql server.
+user = postfix
+password = some_password
+
+# The database name on the servers.
+dbname = mailsys
+
+# Postfix 2.2 and later The SQL query template. See pgsql_table(5).
+query = SELECT transport FROM postfix_transport WHERE domainname='%s'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgsql-virtual_alias_maps.cf	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,12 @@
+# The hosts that Postfix will try to connect to
+hosts = host1.some.domain host2.some.domain
+
+# The user name and password to log into the pgsql server.
+user = postfix
+password = some_password
+
+# The database name on the servers.
+dbname = mailsys
+
+# Postfix 2.2 and later The SQL query template. See pgsql_table(5).
+query = SELECT destination FROM postfix_alias WHERE address='%s'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgsql-virtual_gid_maps.cf	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,12 @@
+# The hosts that Postfix will try to connect to
+hosts = host1.some.domain host2.some.domain
+
+# The user name and password to log into the pgsql server.
+user = postfix
+password = some_password
+
+# The database name on the servers.
+dbname = mailsys
+
+# Postfix 2.2 and later The SQL query template. See pgsql_table(5).
+query = SELECT gid FROM postfix_gid WHERE domainname='%d'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgsql-virtual_mailbox_maps.cf	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,12 @@
+# The hosts that Postfix will try to connect to
+hosts = host1.some.domain host2.some.domain
+
+# The user name and password to log into the pgsql server.
+user = postfix
+password = some_password
+
+# The database name on the servers.
+dbname = mailsys
+
+# Postfix 2.2 and later The SQL query template. See pgsql_table(5).
+query = SELECT maildir FROM postfix_maildir WHERE address='%s'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pgsql-virtual_uid_maps.cf	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,12 @@
+# The hosts that Postfix will try to connect to
+hosts = host1.some.domain host2.some.domain
+
+# The user name and password to log into the pgsql server.
+user = postfix
+password = some_password
+
+# The database name on the servers.
+dbname = mailsys
+
+# Postfix 2.2 and later The SQL query template. See pgsql_table(5).
+query = SELECT uid FROM postfix_uid WHERE address='%s'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.cfg	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,7 @@
+# $Id$
+[install]
+compile = 1
+optimize = 1
+
+[sdist]
+formats = bztar
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,67 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# Copyright 2007-2008 VEB IT
+# See COPYING for distribution information.
+# $Id$
+
+import os
+from distutils.core import setup
+
+VERSION = '0.3'
+
+long_description = """
+Virtual Mail Manager is a command line tool for administrators/postmasters to
+manage domains, accounts and aliases. It's designed for Dovecot and Postfix
+with a PostgreSQL backend.
+"""
+
+libdir = '/usr/local/lib'
+
+# remove existing MANIFEST
+if os.path.exists('MANIFEST'):
+    os.remove('MANIFEST')
+
+
+setup(name='VirtualMailManager',
+      version=VERSION,
+      description='Tool to manage mail domains/accounts/aliases for Dovecot and Postfix',
+      long_description=long_description,
+      packages=['VirtualMailManager', 'VirtualMailManager.constants'],
+#      data_files=[(libdir, [
+#          'VirtualMailManager/Account.py',
+#          'VirtualMailManager/Alias.py',
+#          'VirtualMailManager/Config.py',
+#          'VirtualMailManager/Domain.py',
+#          'VirtualMailManager/Exceptions.py',
+#          'VirtualMailManager/__init__.py',
+#          'VirtualMailManager/VirtualMailManager.py']
+#          ),
+#          (libdir+'/constants', [
+#              'VirtualMailManager/constants/ERROR.py',
+#              'VirtualMailManager/constants/EXIT.py',
+#              'VirtualMailManager/constants/__init__.py']
+#          )
+#      ],
+      author='Pascal Volk',
+      author_email='p.volk@veb-it.de',
+      license='BSD License',
+      url='http://vmm.sf.net/',
+      download_url='http://sourceforge.net/project/showfiles.php?group_id=213727',
+      classifiers=[
+          'Development Status :: 4 - Beta',
+          'Development Status :: 5 - Production/Stable',
+          'Environment :: Console',
+          'Intended Audience :: System Administrators',
+          'License :: OSI Approved :: BSD License',
+          'Natural Language :: English',
+          'Operating System :: POSIX',
+          'Operating System :: POSIX :: BSD',
+          'Operating System :: POSIX :: Linux',
+          'Operating System :: POSIX :: Other',
+          'Programming Language :: Python',
+          'Topic :: Communications :: Email :: Mail Transport Agents',
+          'Topic :: Communications :: Email :: Post-Office :: IMAP',
+          'Topic :: Communications :: Email :: Post-Office :: POP3'
+      ],
+      requires=['pyPgSQL']
+      )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/update_tables_0.2.x-0.3.pgsql	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,8 @@
+-- $Id$
+
+DROP VIEW ma_aliases_count;
+CREATE OR REPLACE VIEW vmm_alias_count AS
+    SELECT count(DISTINCT address) AS aliases, gid
+      FROM alias
+  GROUP BY gid;
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vmm	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,304 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+# Copyright 2007-2008 VEB IT
+# See COPYING for distribution information.
+# $Id$
+
+"""This is the vmm main script."""
+
+__author__ = 'Pascal Volk <p.volk@veb-it.de>'
+__version__ = 'rev '+'$Rev$'.split()[1]
+__date__ = '$Date$'.split()[1]
+
+import os
+import sys
+from getpass import getpass
+
+#sys.path.insert(0, '/usr/local/lib/VirtualMailManager')
+
+from VirtualMailManager.VirtualMailManager import VirtualMailManager
+from VirtualMailManager.Config import VMMConfig
+import VirtualMailManager.Exceptions as VMME
+import VirtualMailManager.constants.EXIT as EXIT
+
+def usage(excode=0, errMsg=None):
+    sys.stderr.write("""\
+Usage: vmm OPTION OBJECT ARGS*
+  short long
+  option                object           args (* = optional)
+
+  da    domainadd       domain.tld       transport*
+  di    domaininfo      domain.tld       detailed*
+  dt    domaintransport domain.tld       transport
+  dd    domaindelete    domain.tld       delalias*|deluser*|delall*
+  ua    useradd         user@domain.tld  password*
+  ui    userinfo        user@domain.tld  du*
+  un    username        user@domain.tld  'Users Name'
+  up    userpassword    user@domain.tld  password*
+  u0    userdisable     user@domain.tld
+  u1    userenable      user@domain.tld
+  ud    userdelete      user@domain.tld
+  aa    aliasadd        alias@domain.tld user@domain.tld
+  ai    aliasinfo       alias@domain.tld
+  ad    aliasdelete     alias@domain.tld
+  cf    configure                        section*
+  h     help
+  v     version
+
+""")
+    if not errMsg is None:
+        sys.stderr.write('Error: %s\n' % errMsg)
+    sys.exit(excode)
+
+def getVMM():
+    try:
+        vmm = VirtualMailManager()
+        return vmm
+    except VMME.MANotRootException, e:
+        sys.stderr.write(str(e))
+        sys.exit(EXIT.CONF_NOPERM)
+    except IOError, e:
+        sys.stderr.write(str(e))
+        sys.exit(EXIT.CONF_NOFILE)
+    except VMME.MAPermException, e:
+        sys.stderr.write(str(e))
+        sys.exit(EXIT.CONF_WRONGPERM)
+
+def configure():
+    try:
+        if len(sys.argv) < 3:
+            vmm.configure()
+        else:
+            vmm.configure(sys.argv[2])
+    except (EOFError, KeyboardInterrupt):
+        sys.stderr.write('\nOuch!\n')
+        sys.exit(EXIT.USER_INTERRUPT)
+    except VMME.VMMConfigException, e:
+        sys.stderr.write(str(e))
+        sys.exit(EXIT.CONF_ERROR)
+    sys.exit(0)
+
+def _readpass():
+    clear0 = ''
+    clear1 = '1'
+    while clear0 != clear1:
+        while len(clear0) < 1:
+            clear0 = getpass(prompt='Enter new password: ')
+            if len(clear0) < 1:
+                sys.stderr.write('Sorry, empty passwords are not permitted\n')
+        clear1 = getpass(prompt='Retype new password: ')
+        if clear0 != clear1:
+            clear0 = ''
+            sys.stderr.write('Sorry, passwords do not match\n')
+    return clear0
+
+def _printInfo(info, title):
+    msg = title+' information'
+    print '%s\n%s' % (msg, '-'*len(msg))
+    for k,v in info.items():
+        print '\t%s: %s' % (k.title().ljust(15, '.'), v)
+    print
+
+def _printUsers(users, title):
+    msg = 'Available '+title
+    print '%s\n%s' % (msg, '-'*len(msg))
+    if len(users) > 0:
+        for user in users:
+            print '\t%s' % user
+    else:
+        print '\tNone'
+    print
+
+def _printAliases(alias, targets):
+    msg = 'Alias information'
+    print '%s\n%s' % (msg, '-'*len(msg))
+    print '\tMail for %s goes to:' % alias
+    if len(targets) > 0:
+        for target in targets:
+            print '\t     -> %s' % target
+    else:
+        print '\tNone'
+    print
+
+def domain_add():
+    global argc
+    if argc < 3:
+        usage(EXIT.MISSING_ARGS, 'Missing domain name.')
+    elif argc < 4:
+        vmm.domain_add(sys.argv[2].lower())
+    else:
+        vmm.domain_add(sys.argv[2].lower(), sys.argv[3])
+
+def domain_delete():
+    global argc
+    if argc < 3:
+        usage(EXIT.MISSING_ARGS, 'Missing domain name.')
+    elif argc < 4:
+        vmm.domain_delete(sys.argv[2].lower())
+    else:
+        vmm.domain_delete(sys.argv[2].lower(), sys.argv[3])
+
+def domain_info():
+    global argc
+    if argc < 3:
+        usage(EXIT.MISSING_ARGS, 'Missing domain name.')
+    elif argc < 4:
+        _printInfo(vmm.domain_info(sys.argv[2].lower()), 'Domain')
+    else:
+        infos = vmm.domain_info(sys.argv[2].lower(), sys.argv[3])
+        _printInfo(infos[0], 'Domain')
+        _printUsers(infos[1], 'accounts')
+        _printUsers(infos[2], 'aliases')
+
+def domain_transport():
+    global argc
+    if argc < 3:
+        usage(EXIT.MISSING_ARGS, 'Missing domain name and new transport.')
+    if argc < 4:
+        usage(EXIT.MISSING_ARGS, 'Missing new transport.')
+    else:
+        vmm.domain_transport(sys.argv[2].lower(), sys.argv[3])
+
+def user_add():
+    global argc
+    if argc < 3:
+        usage(EXIT.MISSING_ARGS, 'Missing email address.')
+    elif argc < 4:
+        password = _readpass()
+    else:
+        password = sys.argv[3]
+    vmm.user_add(sys.argv[2].lower(), password)
+
+def user_delete():
+    global argc
+    if argc < 3:
+        usage(EXIT.MISSING_ARGS, 'Missing email address.')
+    else:
+        vmm.user_delete(sys.argv[2].lower())
+
+def user_info():
+    global argc
+    if argc < 3:
+        usage(EXIT.MISSING_ARGS, 'Missing email address.')
+    elif argc < 4:
+        _printInfo(vmm.user_info(sys.argv[2].lower()), 'Account')
+    else:
+        _printInfo(vmm.user_info(sys.argv[2].lower(), True), 'Account')
+
+def user_name():
+    global argc
+    if argc < 4:
+        usage(EXIT.MISSING_ARGS, 'Missing email address an users name.')
+    else:
+        vmm.user_name(sys.argv[2].lower(), sys.argv[3])
+
+def user_enable():
+    global argc
+    if argc < 3:
+        usage(EXIT.MISSING_ARGS, 'Missing email address.')
+    else:
+        vmm.user_enable(sys.argv[2].lower())
+
+def user_disable():
+    global argc
+    if argc < 3:
+        usage(EXIT.MISSING_ARGS, 'Missing email address.')
+    else:
+        vmm.user_disable(sys.argv[2].lower())
+
+def user_password():
+    global argc
+    if argc < 3:
+        usage(EXIT.MISSING_ARGS, 'Missing email address.')
+    elif argc < 4:
+        password = _readpass()
+    else:
+        password = sys.argv[3]
+    vmm.user_password(sys.argv[2].lower(), password)
+
+def alias_add():
+    global argc
+    if argc < 4:
+        usage(EXIT.MISSING_ARGS, 'Missing alias address and destination.')
+    else:
+        vmm.alias_add(sys.argv[2].lower(), sys.argv[3])
+
+def alias_info():
+    global argc
+    if argc < 3:
+        usage(EXIT.MISSING_ARGS, 'Missing alias address')
+    else:
+        _printAliases(sys.argv[2], vmm.alias_info(sys.argv[2].lower()))
+
+def alias_delete():
+    global argc
+    if argc < 3:
+        usage(EXIT.MISSING_ARGS, 'Missing alias address')
+    else:
+        vmm.alias_delete(sys.argv[2].lower())
+
+def showWarnings():
+    if vmm.hasWarnings():
+        print '\nWarnings:'
+        for w in vmm.getWarnings():
+            print " * ",w
+
+#def main():
+if __name__ == '__main__':
+    argc = len(sys.argv)
+    if argc < 2:
+        usage(EXIT.MISSING_ARGS) # -> exit
+    vmm = getVMM()
+    try:
+        if sys.argv[1] in ['cf',  'configure'] or not vmm.setupIsDone():
+            configure()
+    except VMME.VMMConfigException, e:
+        sys.stderr.write(str(e))
+        sys.exit(EXIT.CONF_ERROR)
+    except VMME.VMMException, e:
+        sys.stderr.write("\aERROR: %s\n" % e[0][0])
+        sys.exit(e[0][1])
+    try:
+        if sys.argv[1] in ['da', 'domainadd']:
+            domain_add()
+        elif sys.argv[1] in ['di', 'domaininfo']:
+            domain_info()
+        elif sys.argv[1] in ['dt', 'domaintransport']:
+            domain_transport()
+        elif sys.argv[1] in ['dd', 'domaindelete']:
+            domain_delete()
+        elif sys.argv[1] in ['ua', 'useradd']:
+            user_add()
+        elif sys.argv[1] in ['ui', 'userinfo']:
+            user_info()
+        elif sys.argv[1] in ['un', 'username']:
+            user_name()
+        elif sys.argv[1] in ['up', 'userpassword']:
+            user_password()
+        elif sys.argv[1] in ['u0', 'userdisable']:
+            user_disable()
+        elif sys.argv[1] in ['u1', 'userenable']:
+            user_enable()
+        elif sys.argv[1] in ['ud', 'userdelete']:
+            user_delete()
+        elif sys.argv[1] in ['aa', 'aliasadd']:
+            alias_add()
+        elif sys.argv[1] in ['ai', 'aliasinfo']:
+            alias_info()
+        elif sys.argv[1] in ['ad', 'aliasdelete']:
+            alias_delete()
+        elif sys.argv[1] in ['h', 'help']:
+            usage()
+        elif sys.argv[1] in ['v', 'version']:
+            print "%s: %s (Date: %s)\n" % (os.path.basename(sys.argv[0]),
+                    __version__, __date__)
+        else:
+            sys.stderr.write('Unknown option: "%s"\n' % sys.argv[1])
+            usage(EXIT.UNKNOWN_OPTION)
+        showWarnings()
+    except (EOFError, KeyboardInterrupt):
+        sys.stderr.write('\nOuch!\n')
+        sys.exit(EXIT.USER_INTERRUPT)
+    except VMME.VMMException, e:
+        sys.stderr.write("\aERROR: %s\n" % e[0][0])
+        sys.exit(e[0][1])
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vmm.cfg	Sun Jan 06 18:22:10 2008 +0000
@@ -0,0 +1,70 @@
+# $Id$
+# This is the Virtual Mail Manager (vmm) configuration file.
+# location: /usr/local/etc/vmm.cfg
+#
+
+#
+# Database settings
+#
+[database]
+; Hostname or IP address of the database server (String)
+host = 127.0.0.1
+; Database user name (String)
+user = dbuser
+; Database password (String)
+pass = dbpassword
+; database name (String)
+name = mailsys
+
+#
+# Mail directories
+#
+[maildir]
+; The base directory for all domains/accounts (String)
+base = /home/mail
+; name of the  Maildir folder
+folder = Maildir
+; Permissions for maildirs (Int)
+; octal 0700 -> decimal 448
+mode = 448
+; Display disk usage in account info by default? (Boolean)
+diskusage = false
+; Delete maildir recursive when deleting an account? (Boolean)
+delete = false
+
+#
+# domain directory settings
+#
+[domdir]
+; Permissions for domain directories (Int)
+; octal 0770 -> decimal 504
+mode = 504
+; Delete domain directory recursive when deleting a domain? (Boolean)
+delete = false
+
+#
+# external binaries
+#
+[bin]
+; location of dovecotpw (String)
+dovecotpw = /usr/sbin/dovecotpw
+; location of disk usage (String)
+du = /usr/bin/du
+
+#
+# misc settings
+#
+[misc]
+; Password scheme to use (see also: dovecotpw -l) (String)
+passwdscheme = CRAM-MD5
+; numeric  group ID of group mail (mail_extra_groups from dovecot.conf) (Int)
+gid_mail = 8
+; force deletion of accounts and aliases (Boolean)
+forcedel = false
+
+#
+# Configuration state
+#
+[config]
+; finally set this to true (Boolean)
+done = false