# HG changeset patch # User Pascal Volk # Date 1199643730 0 # Node ID bb0aa2102206cd25a99e64d0b94bcd6c2018d3df Initial import @sf.net diff -r 000000000000 -r bb0aa2102206 COPYING --- /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. diff -r 000000000000 -r bb0aa2102206 INSTALL --- /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 + diff -r 000000000000 -r bb0aa2102206 README diff -r 000000000000 -r bb0aa2102206 TODO --- /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 + diff -r 000000000000 -r bb0aa2102206 VirtualMailManager/Account.py --- /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 ' +__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)) diff -r 000000000000 -r bb0aa2102206 VirtualMailManager/Alias.py --- /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 ' +__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)) + diff -r 000000000000 -r bb0aa2102206 VirtualMailManager/Config.py --- /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 ' +__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 diff -r 000000000000 -r bb0aa2102206 VirtualMailManager/Domain.py --- /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 ' +__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 diff -r 000000000000 -r bb0aa2102206 VirtualMailManager/Exceptions.py --- /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 ' +__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) diff -r 000000000000 -r bb0aa2102206 VirtualMailManager/VirtualMailManager.py --- /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 ' +__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() diff -r 000000000000 -r bb0aa2102206 VirtualMailManager/__init__.py --- /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 diff -r 000000000000 -r bb0aa2102206 VirtualMailManager/constants/ERROR.py --- /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 diff -r 000000000000 -r bb0aa2102206 VirtualMailManager/constants/EXIT.py --- /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 diff -r 000000000000 -r bb0aa2102206 VirtualMailManager/constants/__init__.py --- /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 diff -r 000000000000 -r bb0aa2102206 create_tables.pgsql --- /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; diff -r 000000000000 -r bb0aa2102206 install.sh --- /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 diff -r 000000000000 -r bb0aa2102206 pgsql-relocated_maps.cf --- /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' diff -r 000000000000 -r bb0aa2102206 pgsql-smtpd_sender_login_maps.cf --- /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 diff -r 000000000000 -r bb0aa2102206 pgsql-transport.cf --- /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' diff -r 000000000000 -r bb0aa2102206 pgsql-virtual_alias_maps.cf --- /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' diff -r 000000000000 -r bb0aa2102206 pgsql-virtual_gid_maps.cf --- /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' diff -r 000000000000 -r bb0aa2102206 pgsql-virtual_mailbox_maps.cf --- /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' diff -r 000000000000 -r bb0aa2102206 pgsql-virtual_uid_maps.cf --- /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' diff -r 000000000000 -r bb0aa2102206 setup.cfg --- /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 diff -r 000000000000 -r bb0aa2102206 setup.py --- /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'] + ) diff -r 000000000000 -r bb0aa2102206 update_tables_0.2.x-0.3.pgsql --- /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; + diff -r 000000000000 -r bb0aa2102206 vmm --- /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 ' +__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]) diff -r 000000000000 -r bb0aa2102206 vmm.cfg --- /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