--- /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