# HG changeset patch # User Pascal Volk # Date 1340911610 0 # Node ID a4aead244f7542a33cac86ce79199608d89d532b # Parent c0e1fb1b0145e2cc50e76b12ef99950d024d5095# Parent 28230a8230bffbd41e7462337be677cd6a236dfc Merged changes from v0.6.x(28230a8230bf). diff -r c0e1fb1b0145 -r a4aead244f75 .hgignore --- a/.hgignore Mon Nov 07 03:22:15 2011 +0000 +++ b/.hgignore Thu Jun 28 19:26:50 2012 +0000 @@ -5,3 +5,12 @@ *.py? .*.swp .swp +doc/build +build +debian/vmm +debian/*.log +debian/*.debhelper +debian/files +debian/*substvars +.pc +*.egg-info diff -r c0e1fb1b0145 -r a4aead244f75 COPYING --- a/COPYING Mon Nov 07 03:22:15 2011 +0000 +++ b/COPYING Thu Jun 28 19:26:50 2012 +0000 @@ -1,4 +1,4 @@ -Copyright (c) 2007 - 2010, Pascal Volk +Copyright (c) 2007 - 2012, Pascal Volk All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff -r c0e1fb1b0145 -r a4aead244f75 Configure.Dovecot_2 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/Configure.Dovecot_2 Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,205 @@ +# This document contains a minimal configuration for a vmm setup with +# Dovecot v2.x. +# +# You could save this file as local.conf in the dovecot configuration directory +# (commonly /etc/dovecot or /usr/local/etc/dovecot). +# When you want to use this file as your configuration file for Dovecot, make +# sure you have commented out the line "!include conf.d/*.conf". The last line +# "!include_try local.conf" is sufficient. +# +# Otherwise you have to apply the following settings to the configuration files +# in the conf.d directory. + +### +# dovecot.conf +### +protocols = imap lmtp +# uncomment if your users should be able to manage their sieve scripts +#protocols = imap lmtp sieve + +# uncomment if you want to use the quota plugin +#dict { +# quota = pgsql:/usr/local/etc/dovecot/dovecot-dict-sql.conf.ext +#} + +### +# conf.d/10-auth.conf +### +auth_mechanisms = plain login cram-md5 +passdb { + driver = sql + args = /usr/local/etc/dovecot/dovecot-sql.conf.ext +} +userdb { + driver = sql + args = /usr/local/etc/dovecot/dovecot-sql.conf.ext +} +#!include auth-system.conf.ext + +### +# conf.d/10-mail.conf +### +first_valid_gid = 70000 +first_valid_uid = 70000 +mail_access_groups = dovemail +mail_location = maildir:~/Maildir + +# uncomment if you want to use the quota plugin +#mail_plugins = quota + +### +# conf.d/10-master.conf +### + +# if you don't want to use secure imap, you have to disable the imaps listener +##service imap-login { +## inet_listener imaps { +## port = 0 +## } +##} + +service lmtp { + unix_listener /var/spool/postfix/private/dovecot-lmtp { + user = postfix + group = postfix + mode = 0600 + } +} + +service auth { + user = doveauth + unix_listener auth-userdb { + } + unix_listener /var/spool/postfix/private/dovecot-auth { + user = postfix + group = postfix + mode = 0600 + } +} + +service auth-worker { + unix_listener auth-worker { + user = doveauth + group = $default_internal_user + mode = 0660 + } + user = doveauth +} + +service dict { + unix_listener dict { + group = dovemail + mode = 0660 + } +} + +### +# conf.d/10-ssl.conf +### +# SSL/TLS support: yes, no, required. +#ssl = yes + +ssl_cert = . + +The Virtual Mail Manager depends on: + - Python (>= 2.4.0) + - Psycopg 2¹ or pyPgSQL² + +If you are using Python <= 2.5.0: + - if you want to store your users' passwords as PLAIN-MD4 digest in + the database, vmm will try to use Crypto.Hash.MD4 from PyCrypto³. + - if you are using Dovecot >= v1.1.0 and you want to store your users' + passwords as SHA256 or SSHA256 hashes, vmm will try to use + Crypto.Hash.SHA256 from PyCrypto². For SHA256/SSHA256 you should have + at least use PyCrypto in version 2.1.0alpha1. + + When the Crypto.Hash module couldn't be imported, vmm will use + dovecotpw/doveadm, if the misc.password_scheme setting in the vmm.cfg + is set to PLAIN-MD4, SHA256 or SSHA256 -* = http://pypgsql.sourceforge.net/ (Debian: python-pgsql) +[1] Psycopg: (Debian: python-psycopg2) +[2] pyPgSQL: (Debian: python-pgsql) +[3] PyCrypto: (Debian: python-crypto) + + +Create additionally a user and groups for improved security + We will create the system user `doveauth'. This user is used in the + authentication process. On a Debian GNU/Linux System use this command: + + adduser --system --home /nonexistent --no-create-home --group \ + --disabled-login --gecos "Dovecot IMAP/POP3 authentication user" \ + doveauth + + This will create the doveauth user and group. + For Dovecot >= 2.0 we create also the group `dovemail'. Dovecot will assign + this group to all Dovecot processes. + On a Debian GNU/Linux bases system run: + + addgroup --system dovemail Configuring PostgreSQL +(for more details see: http://vmm.localdomain.org/PreparingPostgreSQL) -* /etc/postgresql/8.2/main/pg_hba.conf +* /etc/postgresql/8.4/main/pg_hba.conf + [ if you prefer to connect via TCP/IP ] # IPv4 local connections: host mailsys +mailsys 127.0.0.1/32 md5 + [ if you want to connect through a local Unix-domain socket ] + # "local" is for Unix domain socket connections only + local mailsys +mailsys md5 # reload configuration - /etc/init.d/postgresql-8.2 force-reload + /etc/init.d/postgresql-8.4 force-reload -* Create a DB user if necessary: - DB Superuser: +* Create a database superuser if necessary: + # as root run: su - postgres + # if you have sudo privileges run: sudo su - postgres + # create your superuser, which will be able to create users and databases 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 +* As superuser create the database and db users for vmm, Postfix and Dovecot connecting to PostgreSQL: psql template1 - # create database - CREATE DATABASE mailsys ENCODING 'UTF8'; - # connect to the new database - \c mailsys - # either import the database structure for Dovecot v1.0.x/v1.1.x - \i /path/to/create_tables.pgsql - # or import the database structure for Dovecot v1.2.x - \i /path/to/create_tables-dovecot-1.2.x.pgsql + # create users, group and the database + CREATE ROLE vmm LOGIN ENCRYPTED PASSWORD 'DB PASSWORD for vmm'; + CREATE ROLE dovecot LOGIN ENCRYPTED password 'DB PASSWORD for Dovecot'; + CREATE ROLE postfix LOGIN ENCRYPTED password 'DB PASSWORD for Postfix'; + CREATE ROLE mailsys WITH USER postfix, dovecot, vmm; + CREATE DATABASE mailsys WITH OWNER vmm ENCODING 'UTF8'; + \q - # 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_gid, postfix_maildir, - postfix_relocated, postfix_transport, postfix_uid TO postfix; - + # connect to the new database + psql mailsys vmm -W -h 127.0.0.1 + # either import the database structure for Dovecot v1.0.x/v1.1.x + \i vmm-y.x.z/pgsql/create_tables.pgsql + # or import the database structure for Dovecot v1.2.x/v2.x + \i vmm-x.y.z/pgsql/create_tables-dovecot-1.2.x.pgsql # leave psql \q + # set permissions for your Dovecot and Postfix users + # see python set-permissions.py -h for details + python vmm-x.y.z/pgsql/set-permissions.py -a -H 127.0.0.1 -U vmm + 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 + +For Dovecot >= 2.0 read the file Configure.Dovecot_2 +Configuring Dovecot v1.x * /etc/dovecot/dovecot.conf # all your other settings #disable_plaintext_auth = no mail_location = maildir:~/Maildir - mail_privileged_group = mail first_valid_uid = 70000 first_valid_gid = 70000 protocol lda { @@ -77,14 +112,14 @@ userdb sql { args = /etc/dovecot/dovecot-sql.conf } - user = nobody + user = doveauth socket listen { master { path = /var/run/dovecot/auth-master mode = 0600 } client { - path = /var/spool/postfix/private/auth + path = /var/spool/postfix/private/dovecot-auth mode = 0660 user = postfix group = postfix @@ -95,12 +130,15 @@ * /etc/dovecot/dovecot-sql.conf driver = pgsql connect = host=localhost dbname=mailsys user=dovecot password=$Dovecot_PASS - default_pass_scheme = PLAIN - password_query = SELECT "user", password FROM dovecot_password WHERE "user"='%Lu' AND %Ls - user_query = SELECT home, uid, gid, 'maildir:'||mail AS mail FROM dovecot_user WHERE userid = '%Lu' + default_pass_scheme = CRAM-MD5 + password_query = SELECT userid AS "user", password FROM dovecotpassword('%Ln', '%Ld') WHERE %Ls + user_query = SELECT home, uid, gid, mail FROM dovecotuser('%Ln', '%Ld') Provide a root SETUID copy of Dovecot's deliver agent for Postfix +/!\ Only required with Dovecot v.1.x. + With Dovecot >= v2.0 use Dovecot's lmtp! + mkdir -p /usr/local/lib/dovecot chmod 700 /usr/local/lib/dovecot chown nobody /usr/local/lib/dovecot @@ -114,10 +152,11 @@ Configuring Postfix's master.cf - + +/!\ Only required with Dovecot v.1.x. # Add Dovecot's deliver agent dovecot unix - n n - - pipe - flags=DRhu user=nobody argv=/usr/local/lib/dovecot/deliver -f ${sender} -d ${user}@${nexthop} -n -m ${extension} + flags=DORhu user=nobody argv=/usr/local/lib/dovecot/deliver -f ${sender} -d ${user}@${nexthop} -n -m ${extension} @@ -125,23 +164,28 @@ # relocated users from the database #relocated_maps = pgsql:/etc/postfix/pgsql-relocated_maps.cf + # transport settings from our database + transport_maps = pgsql:/etc/postfix/pgsql-transport_maps.cf + # virtual domains virtual_mailbox_domains = pgsql:/etc/postfix/pgsql-virtual_mailbox_domains.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 LDA (only recommended with Dovecot v1.x) + #dovecot_destination_recipient_limit = 1 + #virtual_transport = dovecot: + + # dovecot lmtp + virtual_transport = lmtp:unix:private/dovecot-lmtp # dovecot SASL smtpd_sasl_type = dovecot - smtpd_sasl_path = private/auth + smtpd_sasl_path = private/dovecot-auth smtpd_sasl_auth_enable = yes # Keep smtpd_sasl_local_domain identical to Dovecot's auth_default_realm: # empty. Both are empty by default. Let it commented out. @@ -159,17 +203,23 @@ Installing the Virtual Mail Manager and configure the rest - Installing from SVN or vmm-x.y.z.tar.bz2 - after checking out from svn or extracting the archive change into the new - directory and type: + Installing from Mercurial or vmm-x.y.z.tar.gz + after cloning from the hg repo or extracting the archive change into the + new directory and type: ./install.sh edit all the pgsql-*.cf files in /etc/postfix reload postfix # configure the Virtual Mail Manager + # vmm.cfg(5) - configuration file for vmm + # + # For Dovecot v1.x use 'dovecot:' as domain.transport + # When using Dovecot v2.x use 'lmtp:unix:private/dovecot-lmtp' as + # domain.transport vmm configure # for help type + # vmm(1) - command line tool to manage email domains/accounts/aliases vmm help diff -r c0e1fb1b0145 -r a4aead244f75 README --- a/README Mon Nov 07 03:22:15 2011 +0000 +++ b/README Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,151 @@ +.. -*- restructuredtext -*- + +============================ +vmm - a virtual mail manager +============================ + +Welcome to vmm! + +**vmm** is the easy to use and configurable command line tool for +administrators and postmasters, to manage domains, alias-domains, accounts and +relocated mail users. It allows the fast and easy management of mail servers. + +vmm is written in Python_. It's designed for installations using Dovecot_ and +Postfix_ with a PostgreSQL_ backend. + +Features +======== +• General features + + ‣ Unicode/UTF-8 capable (input/storage/output) + ‣ supports IDN_ + ‣ supports the mailbox format Maildir_ and Dovecot's own high-performance + mailbox formats single- and multi-\ dbox_ + ‣ configurable basic mailbox structure, including sub-mailboxes + ‣ multilingual — currently: + + * Dutch + * English + * Finnish + * French + * German + * Vietnamese + +• Domain features + + ‣ configurable transport_ setting per domain + ‣ unique group identifier (GID) per domain + ‣ each domain may have one or more alias domain names + ‣ activate or deactivate services (SMTP, POP, IMAP and ManageSieve) for new + or all accounts of a domain + ‣ configurable quota limits (size and/or number of messages) for the + domain's accounts + ‣ supports relocated_ users + ‣ the postmaster account can be created automatically when a new domain is + created + ‣ supports per-domain catch-all aliases + +• Alias domain features + + ‣ alias domain names can be switched between domains + +• Account features + + ‣ configurable transport per account + ‣ activate or deactivate one/more/all services (SMTP, POP, IMAP and + ManageSieve) per account + ‣ configurable quota limit (size and/or number of messages) per user + ‣ per-account configuration overrides defaults defined by the domain, + otherwise the setting is inherited + ‣ unique user identifier (UID) per user + +• Alias features + + ‣ supports multiple destinations per e-mail alias + ‣ destinations can be deleted separately + ‣ destinations can be interpolated using the original address' localpart + and domain, allowing aliases to have different meaning in aliasdomains, + e.g. with the following defined in example.org:: + + postmaster@example.org → postmaster+%d@admin.example.org + + If example.com is an aliasdomain of example.org, the alias will become:: + + postmaster@example.org → postmaster+example.org@admin.example.org + postmaster@example.com → postmaster+example.com@admin.example.org + +Installation Prerequisites +========================== +You already should have installed and configured Postfix and Dovecot with +PostgreSQL support. You also need access to a local or remote PostgreSQL +server. + +To verify that your Dovecot and Postfix installation has support for +PostgreSQL use the ``postconf`` and ``dovecot`` commands as shown below:: + + hostname ~ # postconf -m | grep pgsql + pgsql + hostname ~ # postconf -a | grep dovecot + dovecot + hostname ~ # dovecot --build-options | grep postgresql + SQL drivers: mysql postgresql sqlite + +vmm depends on Python (≥ 2.4.0) and Psycopg_ (≥ 2.0) or pyPgSQL_ (≥ 2.5.1). +Psycopg and pyPgSQL are depending on parts of the *eGenix.com mx Base +Distribution* (mxDateTime_ and mxTools_). + +If you are using Python ≤ 2.5.0: + + • if you want to store your users' passwords as ``PLAIN-MD4`` digest in the + database, vmm will try to use ``Crypto.Hash.MD4`` from PyCrypto_ + • if you are using Dovecot ≥ v1.1.0 and you want to store your users' + passwords as ``SHA256`` or ``SSHA256`` hashes, vmm will try to use + ``Crypto.Hash.SHA256`` from PyCrypto. For ``SHA256``/``SSHA256`` you + should have installed PyCrypto, at least in version 2.1.0alpha1. + + When the Crypto.Hash module couldn't be imported, vmm will use + dovecotpw/doveadm, if the *misc.password_scheme* setting in your *vmm.cfg* + is set to ``PLAIN-MD4``, ``SHA256`` or ``SSHA256``. + +Source code +=========== +vmm's source code is available from the Mercurial_ repositories: + +• main repository + + ‣ ``http://hg.localdomain.org/vmm`` + +• mirror repositories + + ‣ ``https://bitbucket.org/pvo/vmm`` + ‣ ``http://vmm.hg.sourceforge.net/hgweb/vmm/vmm`` + +Released versions are also available as gzip compressed tar archives at: +https://sourceforge.net/projects/vmm/files/vmm/ + +Installation/Upgrade +==================== +For installation or upgrading instructions read the `INSTALL` or `UPGRADE` +file. + +License +======= +In short: "**New BSD License**" aka "3-clause license". For a few more +details see the `COPYING` file. + +.. External references +.. _dbox: http://wiki2.dovecot.org/MailboxFormat/dbox +.. _Dovecot: http://dovecot.org/ +.. _IDN: http://en.wikipedia.org/wiki/Internationalized_domain_name +.. _Maildir: http://wiki2.dovecot.org/MailboxFormat/Maildir +.. _Mercurial: http://mercurial.selenic.com/ +.. _mxDateTime: http://www.egenix.com/products/python/mxBase/mxDateTime/ +.. _mxTools: http://www.egenix.com/products/python/mxBase/mxTools/ +.. _Postfix: http://www.postfix.org/ +.. _PostgreSQL: http://www.postgresql.org/ +.. _Psycopg: http://initd.org/psycopg/ +.. _PyCrypto: http://www.pycrypto.org/ +.. _pyPgSQL: http://pypgsql.sourceforge.net/ +.. _Python: http://www.python.org/ +.. _relocated: http://www.postfix.org/relocated.5.html +.. _transport: http://www.postfix.org/transport.5.html diff -r c0e1fb1b0145 -r a4aead244f75 TODO --- a/TODO Mon Nov 07 03:22:15 2011 +0000 +++ b/TODO Thu Jun 28 19:26:50 2012 +0000 @@ -1,4 +1,3 @@ - - Aliases - avoid looping aliases @@ -8,3 +7,33 @@ + aliases + destinations/alias + alias domains + +Database: + public.users.digestmd5: add "character varying(48)" + Outlook will love it. (`doveadm pw -s DIGEST-MD5.hex -p 1 -u 0`) + +- Non-root usage [madduck@madduck.net]: + - Provide /usr/share/vmm/vmm-{mkdirs,rmdirs,du} setuid wrappers that do + precisely what they have to and no more. The should probably even call + /usr/share/vmm/vmm-wrapper-helper as unprivileged user to parse the + arguments, match them with the database and obtain the actual data to + process, e.g. email@add.ress → home directory mapping from the DB, or + obtaining the set of precreatable maildirs from the config. + + - configset and configure need root, it is questionable whether these + can/should be wrapped, as they will be disabled anyway for Debian, and + they are root-like activities (unlike day-to-day postmaster work). + + - Pascal suggested to use hooks: + 07 23:36 zwei config settings handle_dir in domain und account + 07 23:37 und dann post-{domain,user}add scripte, die den wrapper aufrufen + +- relay_domains management [madduck@madduck.net]: + - should be in a separate table + +- default aliases [madduck@madduck.net] + - it should be possible to define a set of default aliases and their + destinations for a domain, e.g. postmaster@ and abuse@. For most + flexibility, there ought to be a m:n table connecting domains to sets of + default aliases. These aliases then get treated like normal aliases + unless they are overridden by a real alias. diff -r c0e1fb1b0145 -r a4aead244f75 UPGRADE --- a/UPGRADE Mon Nov 07 03:22:15 2011 +0000 +++ b/UPGRADE Thu Jun 28 19:26:50 2012 +0000 @@ -1,59 +1,83 @@ -If you still have installed vmm 0.3.x you have to proceed this step first: +If you still have installed vmm 0.4.x you have to proceed this step first: - * upgrade your vmm installation to version 0.4-r41 + * upgrade your vmm installation to version 0.5.2 -If you have installed vmm 0.4/0.4-r41 you have to proceed this steps: +If you have installed vmm 0.5.2 you have to proceed this steps: * stop Postfix and Dovecot * backup/dump your database. * backup/dump your database! * start psql and connect to the appropriate database - (ex. psql mailsys mailsys vmm -W -h localhost) - * update the database: \i update_tables_0.4.x-0.5.pgsql - * GRANT SELECT ON postfix_alias TO postfix; - ^^^^^^^^ <- your Postfix db user + (ex. psql mailsys vmm -W -h 127.0.0.1) + * update the database, + - Dovecot < 1.2.0 + \i vmm-x.y.z/pgsql/update_tables_0.5.x-0.6.pgsql + - Dovecot >= 1.2.0, 2.0.0 and 2.1.0 + \i vmm-x.y.z/pgsql/update_tables_0.5.x-0.6-dovecot-1.2.x.pgsql + * Set database permissions. + (see python set-permissions.py -h for details) + python vmm-x.y.z/pgsql/set-permissions.py -a -H 127.0.0.1 -U vmm + + + /!\ Important note /!\ + + All the views (dovecot_* and postfix_*) have been replaced by database + functions. So you have to adjust all your postfix/pgsql-*.cf files and + also your /etc/dovecot/dovecot-sql.conf or + /etc/dovecot/dovecot-sql.conf.ext. + (See the vmm-x.y.z postfix/pgsql-*.cf files and INSTALL/Configure.Dovecot_2 + files for the new query.) - Dovecot v1.2.x - -> Are you already using Dovecot v1.2.x? - * update the database for Dovecot v1.2.x: - \i update_tables_0.5.x_for_dovecot-1.2.x.pgsql - * GRANT SELECT ON dovecot_password TO dovecot; - ^^^^^^^^ <- your Dovecot db user + * execute upgrade.sh + This will also upgrade your vmm.cfg and apply the following modifications: - * When you are using the SQL function »dovecotpassword()« in your - dovecot-sql.conf, update it also: - \i update_types_and_functions_0.5.x_for_dovecot-1.2.x.pgsql + old new + ------------------------------------------------------------ + domdir.mode -> domain.directory_mode + domdir.delete -> domain.delete_directory + domdir.base -> misc.base_directory + domdir -> _section domdir deleted_ + + maildir.mode -> account.directory_mode + maildir.diskusage -> account.disk_usage + maildir.delete -> account.delete_directory + maildir.folders -> mailbox.folders + maildir.name -> mailbox.root + maildir -> _section maildir deleted_ - * You have also to adjust the permissions of the set-uid deliver copy: - on BSD: - chgrp nobody /usr/local/lib/dovecot/deliver - on Linux: - chgrp nogroup /usr/local/lib/dovecot/deliver - chmod u+s,o-rwx /usr/local/lib/dovecot/deliver + misc.forcedel -> domain.force_deletion + misc.transport -> domain.transport + misc.passwdscheme -> misc.password_scheme + misc.dovecotvers -> misc.dovecot_version (12 -> 1.2.11) + misc.gid_mail -> /dev/null + + services.smtp -> domain.smtp + services.pop3 -> domain.pop3 + services.imap -> domain.imap + services.sieve -> domain.sieve + services -> _section services deleted_ - * Check the master.cf from Postfix. - In prior setups the service dovecot was configured slightly different. - If you have set the command attribute 'user' to 'nobody:mail', so - either remove the group mail or replace it with the group nobody - or nogroup. + _NEW_.random_password -> account.random_password + _NEW_.password_length -> account.password_length + _NEW_.auto_postmaster -> domain.auto_postmaster + _NEW_.quota_bytes -> domain.quota_bytes + _NEW_.quota_messages -> domain.quota_messages + _NEW_.module -> database.module + _NEW_.port -> database.port + _NEW_.sslmode -> database.sslmode + _NEW_.format -> mailbox.format + _NEW_.crypt_blowfish_rounds -> misc.crypt_blowfish_rounds + _NEW_.crypt_sha256_rounds -> misc.crypt_sha256_rounds + _NEW_.crypt_sha512_rounds -> misc.crypt_sha512_rounds - e.g.: flags=DRhu user=nobody argv=/usr/local/lib/dovecot/deliver … - - * execute upgrade.sh - - * start Dovecot and Postfix + config.done -> /dev/null + config -> _section config deleted_ - -If you have installed vmm 0.5.x: - - * execute upgrade.sh - - -> Are you already using Dovecot v1.2.x? - See a few lines above. + * start Dovecot and Postfix again else diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/Account.py --- a/VirtualMailManager/Account.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,267 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2007 - 2010, Pascal Volk -# See COPYING for distribution information. - -"""Virtual Mail Manager's Account class to manage e-mail accounts.""" - -from __main__ import ERR -from Exceptions import VMMAccountException as AccE -from Domain import Domain -from Transport import Transport -from MailLocation import MailLocation -from EmailAddress import EmailAddress -import VirtualMailManager as VMM - -class Account(object): - """Class to manage e-mail accounts.""" - __slots__ = ('_addr','_base','_gid','_mid','_passwd','_tid','_uid','_dbh') - def __init__(self, dbh, address, password=None): - self._dbh = dbh - self._base = None - if isinstance(address, EmailAddress): - self._addr = address - else: - raise TypeError("Argument 'address' is not an EmailAddress") - self._uid = 0 - self._gid = 0 - self._mid = 0 - self._tid = 0 - self._passwd = password - self._setAddr() - self._exists() - if self._uid < 1 and VMM.VirtualMailManager.aliasExists(self._dbh, - self._addr): - # TP: Hm, what quotation marks should be used? - # If you are unsure have a look at: - # http://en.wikipedia.org/wiki/Quotation_mark,_non-English_usage - raise AccE(_(u"There is already an alias with the address “%s”.") %\ - self._addr, ERR.ALIAS_EXISTS) - if self._uid < 1 and VMM.VirtualMailManager.relocatedExists(self._dbh, - self._addr): - raise AccE( - _(u"There is already a relocated user with the address “%s”.") %\ - self._addr, ERR.RELOCATED_EXISTS) - - def _exists(self): - dbc = self._dbh.cursor() - dbc.execute("SELECT uid, mid, tid FROM users \ -WHERE gid=%s AND local_part=%s", - self._gid, self._addr._localpart) - result = dbc.fetchone() - dbc.close() - if result is not None: - self._uid, self._mid, self._tid = result - return True - else: - return False - - def _setAddr(self): - dom = Domain(self._dbh, self._addr._domainname) - self._gid = dom.getID() - if self._gid == 0: - raise AccE(_(u"The domain “%s” doesn't exist.") %\ - self._addr._domainname, ERR.NO_SUCH_DOMAIN) - self._base = dom.getDir() - self._tid = dom.getTransportID() - - def _setID(self): - dbc = self._dbh.cursor() - dbc.execute("SELECT nextval('users_uid')") - self._uid = dbc.fetchone()[0] - dbc.close() - - def _prepare(self, maillocation): - self._setID() - self._mid = MailLocation(self._dbh, maillocation=maillocation).getID() - - def _switchState(self, state, dcvers, service): - if not isinstance(state, bool): - return False - if not service in (None, 'all', 'imap', 'pop3', 'sieve', 'smtp'): - raise AccE(_(u"Unknown service “%s”.") % service, - ERR.UNKNOWN_SERVICE) - if self._uid < 1: - raise AccE(_(u"The account “%s” doesn't exist.") % self._addr, - ERR.NO_SUCH_ACCOUNT) - if dcvers > 11: - sieve_col = 'sieve' - else: - sieve_col = 'managesieve' - if service in ('smtp', 'pop3', 'imap'): - sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (service, state, - self._uid) - elif service == 'sieve': - sql = 'UPDATE users SET %s = %s WHERE uid = %d' % (sieve_col, - state, self._uid) - else: - sql = 'UPDATE users SET smtp = %(s)s, pop3 = %(s)s, imap = %(s)s,\ - %(col)s = %(s)s WHERE uid = %(uid)d' % { - 's': state, 'col': sieve_col, 'uid': self._uid} - dbc = self._dbh.cursor() - dbc.execute(sql) - if dbc.rowcount > 0: - self._dbh.commit() - dbc.close() - - def __aliaseCount(self): - dbc = self._dbh.cursor() - q = "SELECT COUNT(destination) FROM alias WHERE destination = '%s'"\ - %self._addr - dbc.execute(q) - a_count = dbc.fetchone()[0] - dbc.close() - return a_count - - def setPassword(self, password): - self._passwd = password - - 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, dcvers, service=None): - self._switchState(True, dcvers, service) - - def disable(self, dcvers, service=None): - self._switchState(False, dcvers, service) - - def save(self, maillocation, dcvers, smtp, pop3, imap, sieve): - if self._uid < 1: - if dcvers > 11: - sieve_col = 'sieve' - else: - sieve_col = 'managesieve' - self._prepare(maillocation) - sql = "INSERT INTO users (local_part, passwd, uid, gid, mid, tid,\ - smtp, pop3, imap, %s) VALUES ('%s', '%s', %d, %d, %d, %d, %s, %s, %s, %s)" % ( - sieve_col, self._addr._localpart, self._passwd, self._uid, - self._gid, self._mid, self._tid, smtp, pop3, imap, sieve) - dbc = self._dbh.cursor() - dbc.execute(sql) - self._dbh.commit() - dbc.close() - else: - raise AccE(_(u'The account “%s” already exists.') % self._addr, - ERR.ACCOUNT_EXISTS) - - def modify(self, what, value): - if self._uid == 0: - raise AccE(_(u"The account “%s” doesn't exist.") % self._addr, - ERR.NO_SUCH_ACCOUNT) - if what not in ['name', 'password', 'transport']: - return False - dbc = self._dbh.cursor() - if what == 'password': - dbc.execute('UPDATE users SET passwd = %s WHERE uid = %s', - value, self._uid) - elif what == 'transport': - self._tid = Transport(self._dbh, transport=value).getID() - dbc.execute('UPDATE users SET tid = %s WHERE uid = %s', - self._tid, self._uid) - else: - dbc.execute('UPDATE users SET name = %s WHERE uid = %s', - value, self._uid) - if dbc.rowcount > 0: - self._dbh.commit() - dbc.close() - - def getInfo(self, dcvers): - if dcvers > 11: - sieve_col = 'sieve' - else: - sieve_col = 'managesieve' - sql = 'SELECT name, uid, gid, mid, tid, smtp, pop3, imap, %s\ - FROM users WHERE uid = %d' % (sieve_col, self._uid) - dbc = self._dbh.cursor() - dbc.execute(sql) - info = dbc.fetchone() - dbc.close() - if info is None: - raise AccE(_(u"The account “%s” doesn't exist.") % self._addr, - ERR.NO_SUCH_ACCOUNT) - else: - keys = ['name', 'uid', 'gid', 'maildir', 'transport', 'smtp', - 'pop3', 'imap', sieve_col] - info = dict(zip(keys, info)) - for service in ('smtp', 'pop3', 'imap', sieve_col): - if bool(info[service]): - # TP: A service (pop3/imap/…) is enabled/usable for a user - info[service] = _('enabled') - else: - # TP: A service (pop3/imap) isn't enabled/usable for a user - info[service] = _('disabled') - info['address'] = self._addr - info['maildir'] = '%s/%s/%s' % (self._base, info['uid'], - MailLocation(self._dbh, - mid=info['maildir']).getMailLocation()) - info['transport'] = Transport(self._dbh, - tid=info['transport']).getTransport() - return info - - def getAliases(self): - dbc = self._dbh.cursor() - dbc.execute("SELECT address ||'@'|| domainname FROM alias, domain_name\ - WHERE destination = %s AND domain_name.gid = alias.gid\ - AND domain_name.is_primary ORDER BY address", str(self._addr)) - addresses = dbc.fetchall() - dbc.close() - aliases = [] - if len(addresses) > 0: - aliases = [alias[0] for alias in addresses] - return aliases - - def delete(self, delalias): - if self._uid < 1: - raise AccE(_(u"The account “%s” doesn't exist.") % self._addr, - ERR.NO_SUCH_ACCOUNT) - dbc = self._dbh.cursor() - if delalias == 'delalias': - dbc.execute('DELETE FROM users WHERE uid= %s', self._uid) - u_rc = dbc.rowcount - # delete also all aliases where the destination address is the same - # as for this account. - dbc.execute("DELETE FROM alias WHERE destination = %s", - str(self._addr)) - if u_rc > 0 or dbc.rowcount > 0: - self._dbh.commit() - else: # check first for aliases - a_count = self.__aliaseCount() - if a_count == 0: - dbc.execute('DELETE FROM users WHERE uid = %s', self._uid) - if dbc.rowcount > 0: - self._dbh.commit() - else: - dbc.close() - raise AccE( - _(u"There are %(count)d aliases with the destination address\ - “%(address)s”.") %{'count': a_count, 'address': self._addr}, ERR.ALIAS_PRESENT) - dbc.close() - -def getAccountByID(uid, dbh): - try: - uid = long(uid) - except ValueError: - raise AccE(_(u'uid must be an int/long.'), ERR.INVALID_AGUMENT) - if uid < 1: - raise AccE(_(u'uid must be greater than 0.'), ERR.INVALID_AGUMENT) - dbc = dbh.cursor() - dbc.execute("SELECT local_part||'@'|| domain_name.domainname AS address,\ - uid, users.gid FROM users LEFT JOIN domain_name ON (domain_name.gid \ - = users.gid AND is_primary) WHERE uid = %s;", uid) - info = dbc.fetchone() - dbc.close() - if info is None: - raise AccE(_(u"There is no account with the UID “%d”.") % uid, - ERR.NO_SUCH_ACCOUNT) - keys = ['address', 'uid', 'gid'] - info = dict(zip(keys, info)) - return info - diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/Alias.py --- a/VirtualMailManager/Alias.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,122 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2007 - 2010, Pascal Volk -# See COPYING for distribution information. - -"""Virtual Mail Manager's Alias class to manage e-mail aliases.""" - -from __main__ import ERR -from Exceptions import VMMAliasException as VMMAE -from Domain import Domain -from EmailAddress import EmailAddress -import VirtualMailManager as VMM - -class Alias(object): - """Class to manage e-mail aliases.""" - __slots__ = ('_addr', '_dest', '_gid', '_isNew', '_dbh') - def __init__(self, dbh, address, destination=None): - if isinstance(address, EmailAddress): - self._addr = address - else: - raise TypeError("Argument 'address' is not an EmailAddress") - if destination is None: - self._dest = None - elif isinstance(destination, EmailAddress): - self._dest = destination - else: - raise TypeError("Argument 'destination' is not an EmailAddress") - if address == destination: - raise VMMAE(_(u"Address and destination are identical."), - ERR.ALIAS_ADDR_DEST_IDENTICAL) - self._dbh = dbh - self._gid = 0 - self._isNew = False - self._setAddr() - if not self._dest is None: - self._exists() - if VMM.VirtualMailManager.accountExists(self._dbh, self._addr): - raise VMMAE(_(u"There is already an account with address “%s”.") %\ - self._addr, ERR.ACCOUNT_EXISTS) - if VMM.VirtualMailManager.relocatedExists(self._dbh, self._addr): - raise VMMAE( - _(u"There is already a relocated user with the address “%s”.") %\ - self._addr, ERR.RELOCATED_EXISTS) - - def _exists(self): - dbc = self._dbh.cursor() - dbc.execute("SELECT gid FROM alias WHERE gid=%s AND address=%s\ - AND destination=%s", self._gid, self._addr._localpart, str(self._dest)) - gid = dbc.fetchone() - dbc.close() - if gid is None: - self._isNew = True - - def _setAddr(self): - dom = Domain(self._dbh, self._addr._domainname) - self._gid = dom.getID() - if self._gid == 0: - raise VMMAE(_(u"The domain “%s” doesn't exist.") %\ - self._addr._domainname, ERR.NO_SUCH_DOMAIN) - - def _checkExpansion(self, limit): - dbc = self._dbh.cursor() - dbc.execute('SELECT count(gid) FROM alias where gid=%s AND address=%s', - self._gid, self._addr._localpart) - curEx = dbc.fetchone()[0] - dbc.close() - if curEx == limit: - errmsg = _(u"""Can't add new destination to alias “%(address)s”. -Currently this alias expands into %(count)i recipients. -One more destination will render this alias unusable. -Hint: Increase Postfix' virtual_alias_expansion_limit -""") % {'address': self._addr, 'count': curEx} - raise VMMAE(errmsg, ERR.ALIAS_EXCEEDS_EXPANSION_LIMIT) - - def save(self, expansion_limit): - if self._dest is None: - raise VMMAE(_(u"No destination address specified for alias."), - ERR.ALIAS_MISSING_DEST) - if self._isNew: - self._checkExpansion(expansion_limit) - dbc = self._dbh.cursor() - dbc.execute("INSERT INTO alias (gid, address, destination) VALUES\ - (%s, %s, %s)", self._gid, self._addr._localpart, str(self._dest)) - self._dbh.commit() - dbc.close() - else: - raise VMMAE( - _(u"The alias “%(a)s” with destination “%(d)s” already exists.")\ - % {'a': self._addr, 'd': self._dest}, 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._addr._localpart) - destinations = dbc.fetchall() - dbc.close() - if len(destinations) > 0: - targets = [destination[0] for destination in destinations] - return targets - else: - raise VMMAE(_(u"The alias “%s” doesn't exist.") % self._addr, - ERR.NO_SUCH_ALIAS) - - def delete(self): - dbc = self._dbh.cursor() - if self._dest is None: - dbc.execute("DELETE FROM alias WHERE gid=%s AND address=%s", - self._gid, self._addr._localpart) - else: - dbc.execute("DELETE FROM alias WHERE gid=%s AND address=%s AND \ - destination=%s", self._gid, self._addr._localpart, str(self._dest)) - rowcount = dbc.rowcount - dbc.close() - if rowcount > 0: - self._dbh.commit() - else: - if self._dest is None: - msg = _(u"The alias “%s” doesn't exist.") % self._addr - else: - msg = _(u"The alias “%(a)s” with destination “%(d)s” doesn't\ - exist.") % {'a': self._addr, 'd': self._dest} - raise VMMAE(msg, ERR.NO_SUCH_ALIAS) - diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/AliasDomain.py --- a/VirtualMailManager/AliasDomain.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,99 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2008 - 2010, Pascal Volk -# See COPYING for distribution information. - -"""Virtual Mail Manager's AliasDomain class to manage alias domains.""" - -from __main__ import ERR -from Exceptions import VMMAliasDomainException as VADE -import VirtualMailManager as VMM - -class AliasDomain(object): - """Class to manage e-mail alias domains.""" - __slots__ = ('__gid', '__name', '_domain', '_dbh') - def __init__(self, dbh, domainname, targetDomain=None): - self._dbh = dbh - self.__name = VMM.VirtualMailManager.chkDomainname(domainname) - self.__gid = 0 - self._domain = targetDomain - self._exists() - - def _exists(self): - dbc = self._dbh.cursor() - dbc.execute('SELECT gid, is_primary FROM domain_name WHERE domainname\ - = %s', self.__name) - alias = dbc.fetchone() - dbc.close() - if alias is not None: - self.__gid, primary = alias - if primary: - raise VADE(_(u"The domain “%s” is a primary domain.") % - self.__name, ERR.ALIASDOMAIN_ISDOMAIN) - - def save(self): - if self.__gid > 0: - raise VADE(_(u'The alias domain “%s” already exists.') %self.__name, - ERR.ALIASDOMAIN_EXISTS) - if self._domain is None: - raise VADE(_(u'No destination domain specified for alias domain.'), - ERR.ALIASDOMAIN_NO_DOMDEST) - if self._domain._id < 1: - raise VADE (_(u"The target domain “%s” doesn't exist.") % - self._domain._name, ERR.NO_SUCH_DOMAIN) - dbc = self._dbh.cursor() - dbc.execute('INSERT INTO domain_name (domainname, gid, is_primary)\ - VALUES (%s, %s, FALSE)', self.__name, self._domain._id) - self._dbh.commit() - dbc.close() - - def info(self): - if self.__gid > 0: - dbc = self._dbh.cursor() - dbc.execute('SELECT domainname FROM domain_name WHERE gid = %s\ - AND is_primary', self.__gid) - domain = dbc.fetchone() - dbc.close() - if domain is not None: - return {'alias': self.__name, 'domain': domain[0]} - else:# an almost unlikely case, isn't it? - raise VADE( - _(u'There is no primary domain for the alias domain “%s”.')\ - % self.__name, ERR.NO_SUCH_DOMAIN) - else: - raise VADE(_(u"The alias domain “%s” doesn't exist.") % - self.__name, ERR.NO_SUCH_ALIASDOMAIN) - - def switch(self): - if self._domain is None: - raise VADE(_(u'No destination domain specified for alias domain.'), - ERR.ALIASDOMAIN_NO_DOMDEST) - if self._domain._id < 1: - raise VADE (_(u"The target domain “%s” doesn't exist.") % - self._domain._name, ERR.NO_SUCH_DOMAIN) - if self.__gid < 1: - raise VADE(_(u"The alias domain “%s” doesn't exist.") % - self.__name, ERR.NO_SUCH_ALIASDOMAIN) - if self.__gid == self._domain._id: - raise VADE(_(u"The alias domain “%(alias)s” is already assigned to\ - the domain “%(domain)s”.") % - {'alias': self.__name, 'domain': self._domain._name}, - ERR.ALIASDOMAIN_EXISTS) - dbc = self._dbh.cursor() - dbc.execute('UPDATE domain_name SET gid = %s WHERE gid = %s\ - AND domainname = %s AND NOT is_primary', - self._domain._id, self.__gid, self.__name) - self._dbh.commit() - dbc.close() - - def delete(self): - if self.__gid > 0: - dbc = self._dbh.cursor() - dbc.execute('DELETE FROM domain_name WHERE domainname = %s \ - AND NOT is_primary', self.__name) - if dbc.rowcount > 0: - self._dbh.commit() - else: - raise VADE( - _(u"The alias domain “%s” doesn't exist.") % self.__name, - ERR.NO_SUCH_ALIASDOMAIN) - diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/Config.py --- a/VirtualMailManager/Config.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,187 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2007 - 2010, Pascal Volk -# See COPYING for distribution information. - -"""Configuration class for read, modify and write the -configuration from Virtual Mail Manager. - -""" - -from shutil import copy2 -from ConfigParser import ConfigParser, MissingSectionHeaderError, ParsingError -from cStringIO import StringIO - -from __main__ import ENCODING, ERR, w_std -from Exceptions import VMMConfigException - -class Config(ConfigParser): - """This class is for reading and modifying vmm's configuration file.""" - - def __init__(self, filename): - """Creates a new Config instance - - Arguments: - filename -- path to the configuration file - """ - ConfigParser.__init__(self) - self.__cfgFileName = filename - self.__cfgFile = None - self.__VMMsections = ['database', 'maildir', 'services', 'domdir', - 'bin', 'misc', 'config'] - self.__changes = False - self.__missing = {} - self.__dbopts = [ - ['host', 'localhot'], - ['user', 'vmm'], - ['pass', 'your secret password'], - ['name', 'mailsys'] - ] - self.__mdopts = [ - ['name', 'Maildir'], - ['folders', 'Drafts:Sent:Templates:Trash'], - ['mode', 448], - ['diskusage', 'false'], - ['delete', 'false'] - ] - self.__serviceopts = [ - ['smtp', 'true'], - ['pop3', 'true'], - ['imap', 'true'], - ['sieve', 'true'] - ] - self.__domdopts = [ - ['base', '/srv/mail'], - ['mode', 504], - ['delete', 'false'] - ] - self.__binopts = [ - ['dovecotpw', '/usr/sbin/dovecotpw'], - ['du', '/usr/bin/du'], - ['postconf', '/usr/sbin/postconf'] - ] - self.__miscopts = [ - ['passwdscheme', 'PLAIN'], - ['gid_mail', 8], - ['forcedel', 'false'], - ['transport', 'dovecot:'], - ['dovecotvers', '11'] - ] - - def load(self): - """Loads the configuration, read only. - - Raises a VMMConfigException if the configuration syntax is invalid. - """ - try: - self.__cfgFile = file(self.__cfgFileName, 'r') - self.readfp(self.__cfgFile) - except (MissingSectionHeaderError, ParsingError), e: - self.__cfgFile.close() - raise VMMConfigException(str(e), ERR.CONF_ERROR) - self.__cfgFile.close() - - def check(self): - """Performs a configuration check. - - Raises a VMMConfigException if the check fails. - """ - if not self.__chkSections(): - errmsg = StringIO() - errmsg.write(_("Using configuration file: %s\n") %\ - self.__cfgFileName) - for k,v in self.__missing.items(): - if v[0] is True: - errmsg.write(_(u"missing section: %s\n") % k) - else: - errmsg.write(_(u"missing options in section %s:\n") % k) - for o in v: - errmsg.write(" * %s\n" % o) - raise VMMConfigException(errmsg.getvalue(), ERR.CONF_ERROR) - - def getsections(self): - """Return a list with all configurable sections.""" - return self.__VMMsections[:-1] - - def get(self, section, option, raw=False, vars=None): - return unicode(ConfigParser.get(self, section, option, raw, vars), - ENCODING, 'replace') - - def configure(self, sections): - """Interactive method for configuring all options in the given sections - - Arguments: - sections -- list of strings with section names - """ - 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 - w_std(_(u'Using configuration file: %s\n') % self.__cfgFileName) - for s in sections: - if s != 'config': - w_std(_(u'* Config section: “%s”') % s ) - for opt, val in self.items(s): - newval = raw_input( - _('Enter new value for option %(opt)s [%(val)s]: ').encode( - ENCODING, 'replace') % {'opt': opt, 'val': 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.""" - errors = False - for s in self.__VMMsections: - if not self.has_section(s): - self.__missing[s] = [True] - errors = True - elif not self.__chkOptions(s): - errors = True - return not errors - - def __chkOptions(self, section): - """Checks if all configuration options in section are existing. - - Arguments: - section -- the section to be checked - """ - retval = True - missing = [] - if section == 'database': - opts = self.__dbopts - elif section == 'maildir': - opts = self.__mdopts - elif section == 'services': - opts = self.__serviceopts - elif section == 'domdir': - opts = self.__domdopts - elif section == 'bin': - opts = self.__binopts - elif section == 'misc': - opts = self.__miscopts - elif section == 'config': - opts = [['done', 'false']] - for o, v in opts: - if not self.has_option(section, o): - missing.append(o) - retval = False - if len(missing): - self.__missing[section] = missing - return retval diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/Domain.py --- a/VirtualMailManager/Domain.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,313 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2007 - 2010, Pascal Volk -# See COPYING for distribution information. - -"""Virtual Mail Manager's Domain class to manage e-mail domains.""" - -from random import choice - -from __main__ import ERR -from Exceptions import VMMDomainException as VMMDE -import VirtualMailManager as VMM -from Transport import Transport - -MAILDIR_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz' - -class Domain(object): - """Class to manage e-mail domains.""" - __slots__ = ('_basedir','_domaindir','_id','_name','_transport','_dbh') - def __init__(self, dbh, domainname, basedir=None, transport=None): - """Creates a new Domain instance. - - Keyword arguments: - dbh -- a pyPgSQL.PgSQL.connection - domainname -- name of the domain (str) - transport -- default vmm.cfg/misc/transport (str) - """ - self._dbh = dbh - self._name = VMM.VirtualMailManager.chkDomainname(domainname) - self._basedir = basedir - if transport is not None: - self._transport = Transport(self._dbh, transport=transport) - else: - self._transport = transport - self._id = 0 - self._domaindir = None - if not self._exists() and self._isAlias(): - raise VMMDE(_(u"The domain “%s” is an alias domain.") %self._name, - ERR.DOMAIN_ALIAS_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, tid, domaindir FROM domain_data WHERE gid =\ - (SELECT gid FROM domain_name WHERE domainname = %s AND is_primary)", - self._name) - result = dbc.fetchone() - dbc.close() - if result is not None: - self._id, self._domaindir = result[0], result[2] - self._transport = Transport(self._dbh, tid=result[1]) - return True - else: - return False - - def _isAlias(self): - """Checks if self._name is known for an alias domain.""" - dbc = self._dbh.cursor() - dbc.execute('SELECT is_primary FROM domain_name WHERE domainname = %s', - self._name) - result = dbc.fetchone() - dbc.close() - if result is not None and not result[0]: - return True - else: - return False - - def _setID(self): - """Sets the ID of the domain.""" - dbc = self._dbh.cursor() - dbc.execute("SELECT nextval('domain_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 VMMDE(_(u'There are accounts and aliases.'), - ERR.ACCOUNT_AND_ALIAS_PRESENT) - elif hasUser: - raise VMMDE(_(u'There are accounts.'), - ERR.ACCOUNT_PRESENT) - elif hasAlias: - raise VMMDE(_(u'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 domain_data (gid, tid, domaindir)\ - VALUES (%s, %s, %s)", self._id, self._transport.getID(), self._domaindir) - dbc.execute("INSERT INTO domain_name (domainname, gid, is_primary)\ - VALUES (%s, %s, %s)", self._name, self._id, True) - self._dbh.commit() - dbc.close() - else: - raise VMMDE(_(u'The domain “%s” already exists.') % self._name, - 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() - for t in ('alias','users','relocated','domain_name','domain_data'): - dbc.execute("DELETE FROM %s WHERE gid = %d" % (t, self._id)) - self._dbh.commit() - dbc.close() - else: - raise VMMDE(_(u"The domain “%s” doesn't exist.") % self._name, - ERR.NO_SUCH_DOMAIN) - - def updateTransport(self, transport, force=False): - """Sets a new transport for the domain. - - Keyword arguments: - transport -- the new transport (str) - force -- True/False force new transport for all accounts (bool) - """ - if self._id > 0: - if transport == self._transport.getTransport(): - return - trsp = Transport(self._dbh, transport=transport) - dbc = self._dbh.cursor() - dbc.execute("UPDATE domain_data SET tid = %s WHERE gid = %s", - trsp.getID(), self._id) - if dbc.rowcount > 0: - self._dbh.commit() - if force: - dbc.execute("UPDATE users SET tid = %s WHERE gid = %s", - trsp.getID(), self._id) - if dbc.rowcount > 0: - self._dbh.commit() - dbc.close() - else: - raise VMMDE(_(u"The domain “%s” doesn't exist.") % self._name, - 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 getTransport(self): - """Returns domain's transport.""" - return self._transport.getTransport() - - def getTransportID(self): - """Returns the ID from the domain's transport.""" - return self._transport.getID() - - def getInfo(self): - """Returns a dictionary with information about the domain.""" - sql = """\ -SELECT gid, domainname, transport, domaindir, aliasdomains, accounts, - aliases, relocated - FROM vmm_domain_info - WHERE gid = %i""" % self._id - dbc = self._dbh.cursor() - dbc.execute(sql) - info = dbc.fetchone() - dbc.close() - if info is None: - raise VMMDE(_(u"The domain “%s” doesn't exist.") % self._name, - ERR.NO_SUCH_DOMAIN) - else: - keys = ['gid', 'domainname', 'transport', 'domaindir', - 'aliasdomains', 'accounts', 'aliases', 'relocated'] - return dict(zip(keys, info)) - - def getAccounts(self): - """Returns a list with all accounts from the domain.""" - dbc = self._dbh.cursor() - dbc.execute("SELECT local_part from users where gid = %s ORDER BY\ - local_part", self._id) - users = dbc.fetchall() - dbc.close() - accounts = [] - if len(users) > 0: - addr = u'@'.join - _dom = self._name - accounts = [addr((account[0], _dom)) for account in users] - return accounts - - def getAliases(self): - """Returns a list with all aliases from the domain.""" - dbc = self._dbh.cursor() - dbc.execute("SELECT DISTINCT address FROM alias WHERE gid = %s\ - ORDER BY address", self._id) - addresses = dbc.fetchall() - dbc.close() - aliases = [] - if len(addresses) > 0: - addr = u'@'.join - _dom = self._name - aliases = [addr((alias[0], _dom)) for alias in addresses] - return aliases - - def getRelocated(self): - """Returns a list with all addresses from relocated users.""" - dbc = self._dbh.cursor() - dbc.execute("SELECT address FROM relocated WHERE gid = %s\ - ORDER BY address", self._id) - addresses = dbc.fetchall() - dbc.close() - relocated = [] - if len(addresses) > 0: - addr = u'@'.join - _dom = self._name - relocated = [addr((address[0], _dom)) for address in addresses] - return relocated - - def getAliaseNames(self): - """Returns a list with all alias names from the domain.""" - dbc = self._dbh.cursor() - dbc.execute("SELECT domainname FROM domain_name WHERE gid = %s\ - AND NOT is_primary ORDER BY domainname", self._id) - anames = dbc.fetchall() - dbc.close() - aliasdomains = [] - if len(anames) > 0: - aliasdomains = [aname[0] for aname in anames] - return aliasdomains - -def search(dbh, pattern=None, like=False): - if pattern is not None and like is False: - pattern = VMM.VirtualMailManager.chkDomainname(pattern) - sql = 'SELECT gid, domainname, is_primary FROM domain_name' - if pattern is None: - pass - elif like: - sql += " WHERE domainname LIKE '%s'" % pattern - else: - sql += " WHERE domainname = '%s'" % pattern - sql += ' ORDER BY is_primary DESC, domainname' - dbc = dbh.cursor() - dbc.execute(sql) - result = dbc.fetchall() - dbc.close() - - gids = [domain[0] for domain in result if domain[2]] - domains = {} - for gid, domain, is_primary in result: - if is_primary: - if not gid in domains: - domains[gid] = [domain] - else: - domains[gid].insert(0, domain) - else: - if gid in gids: - if gid in domains: - domains[gid].append(domain) - else: - domains[gid] = [domain] - else: - gids.append(gid) - domains[gid] = [None, domain] - return gids, domains - diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/EmailAddress.py --- a/VirtualMailManager/EmailAddress.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,76 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2008 - 2010, Pascal Volk -# See COPYING for distribution information. - -"""Virtual Mail Manager's EmailAddress class to handle e-mail addresses.""" - -from __main__ import re, ERR -from Exceptions import VMMEmailAddressException as VMMEAE -import VirtualMailManager as VMM - -RE_LOCALPART = """[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]""" - -class EmailAddress(object): - __slots__ = ('_localpart', '_domainname') - def __init__(self, address): - self._localpart = None - self._domainname = None - self.__chkAddress(address) - - def __eq__(self, other): - if isinstance(other, self.__class__): - return self._localpart == other._localpart\ - and self._domainname == other._domainname - return NotImplemented - - def __ne__(self, other): - if isinstance(other, self.__class__): - return self._localpart != other._localpart\ - or self._domainname != other._domainname - return NotImplemented - - def __repr__(self): - return "EmailAddress('%s@%s')" % (self._localpart, self._domainname) - - def __str__(self): - return "%s@%s" % (self._localpart, self._domainname) - - def __chkAddress(self, address): - try: - localpart, domain = address.split('@') - except ValueError: - raise VMMEAE(_(u"Missing '@' sign in e-mail address “%s”.") % - address, ERR.INVALID_ADDRESS) - except AttributeError: - raise VMMEAE(_(u"“%s” doesn't look like an e-mail address.") % - address, ERR.INVALID_ADDRESS) - if len(domain) > 0: - domain = VMM.VirtualMailManager.chkDomainname(domain) - else: - raise VMMEAE(_(u"Missing domain name after “%s@”.") % - localpart, ERR.DOMAIN_NO_NAME) - localpart = self.__chkLocalpart(localpart) - self._localpart, self._domainname = localpart, domain - - def __chkLocalpart(self, localpart): - """Validates the local-part of an e-mail address. - - Arguments: - localpart -- local-part of the e-mail address that should be validated (str) - """ - if len(localpart) < 1: - raise VMMEAE(_(u'No local-part specified.'), - ERR.LOCALPART_INVALID) - if len(localpart) > 64: - raise VMMEAE(_(u'The local-part “%s” is too long') % - localpart, ERR.LOCALPART_TOO_LONG) - ic = set(re.findall(RE_LOCALPART, localpart)) - if len(ic): - ichrs = '' - for c in ic: - ichrs += u"“%s” " % c - raise VMMEAE(_(u"The local-part “%(lpart)s” contains invalid\ - characters: %(ichrs)s") % {'lpart': localpart, 'ichrs': ichrs}, - ERR.LOCALPART_INVALID) - return localpart - diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/Exceptions.py --- a/VirtualMailManager/Exceptions.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2007 - 2010, Pascal Volk -# See COPYING for distribution information. - -"""Exception classes for Virtual Mail Manager""" - -class VMMException(Exception): - """Exception class for VirtualMailManager exceptions""" - def __init__(self, msg, code): - Exception.__init__(self, msg) - self._code = int(code) - ### for older python versions, like py 2.4.4 on OpenBSD 4.2 - if not hasattr(self, 'message'): - self.message = msg - - def msg(self): - """Returns the exception message.""" - return self.message - - def code(self): - """Returns the numeric exception error code.""" - return self._code - -class VMMConfigException(VMMException): - """Exception class for Configurtion exceptions""" - def __init__(self, msg, code): - VMMException.__init__(self, msg, code) - -class VMMPermException(VMMException): - """Exception class for permissions exceptions""" - def __init__(self, msg, code): - VMMException.__init__(self, msg, code) - -class VMMNotRootException(VMMException): - """Exception class for non-root exceptions""" - def __init__(self, msg, code): - VMMException.__init__(self, msg, code) - -class VMMDomainException(VMMException): - """Exception class for Domain exceptions""" - def __init__(self, msg, code): - VMMException.__init__(self, msg, code) - -class VMMAliasDomainException(VMMException): - """Exception class for AliasDomain exceptions""" - def __init__(self, msg, code): - VMMException.__init__(self, msg, code) - -class VMMAccountException(VMMException): - """Exception class for Account exceptions""" - def __init__(self, msg, code): - VMMException.__init__(self, msg, code) - -class VMMAliasException(VMMException): - """Exception class for Alias exceptions""" - def __init__(self, msg, code): - VMMException.__init__(self, msg, code) - -class VMMEmailAddressException(VMMException): - """Exception class for EmailAddress exceptions""" - def __init__(self, msg, code): - VMMException.__init__(self, msg, code) - -class VMMMailLocationException(VMMException): - """Exception class for MailLocation exceptions""" - def __init__(self, msg, code): - VMMException.__init__(self, msg, code) - -class VMMRelocatedException(VMMException): - """Exception class for Relocated exceptions""" - def __init__(self, msg, code): - VMMException.__init__(self, msg, code) - -class VMMTransportException(VMMException): - """Exception class for Transport exceptions""" - def __init__(self, msg, code): - VMMException.__init__(self, msg, code) diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/MailLocation.py --- a/VirtualMailManager/MailLocation.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,84 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2008 - 2010, Pascal Volk -# See COPYING for distribution information. - -"""Virtual Mail Manager's MailLocation class to manage the mail_location -for accounts.""" - -from __main__ import re, ERR -from Exceptions import VMMMailLocationException as MLE - -RE_MAILLOCATION = """^\w{1,20}$""" - -class MailLocation(object): - """A wrapper class thats provide access to the maillocation table""" - __slots__ = ('__id', '__maillocation', '_dbh') - def __init__(self, dbh, mid=None, maillocation=None): - """Creates a new MailLocation instance. - - Either mid or maillocation must be specified. - - Keyword arguments: - dbh -- a pyPgSQL.PgSQL.connection - mid -- the id of a maillocation (long) - maillocation -- the value of the maillocation (str) - """ - self._dbh = dbh - if mid is None and maillocation is None: - raise MLE(_('Either mid or maillocation must be specified.'), - ERR.MAILLOCATION_INIT) - elif mid is not None: - try: - self.__id = long(mid) - except ValueError: - raise MLE(_('mid must be an int/long.'), ERR.MAILLOCATION_INIT) - self._loadByID() - else: - if re.match(RE_MAILLOCATION, maillocation): - self.__maillocation = maillocation - self._loadByName() - else: - raise MLE( - _(u'Invalid folder name “%s”, it may consist only of\n\ -1 - 20 single byte characters (A-Z, a-z, 0-9 and _).') % maillocation, - ERR.MAILLOCATION_INIT) - - def _loadByID(self): - dbc = self._dbh.cursor() - dbc.execute('SELECT maillocation FROM maillocation WHERE mid = %s', - self.__id) - result = dbc.fetchone() - dbc.close() - if result is not None: - self.__maillocation = result[0] - else: - raise MLE(_('Unknown mid specified.'), ERR.UNKNOWN_MAILLOCATION_ID) - - def _loadByName(self): - dbc = self._dbh.cursor() - dbc.execute('SELECT mid FROM maillocation WHERE maillocation = %s', - self.__maillocation) - result = dbc.fetchone() - dbc.close() - if result is not None: - self.__id = result[0] - else: - self._save() - - def _save(self): - dbc = self._dbh.cursor() - dbc.execute("SELECT nextval('maillocation_id')") - self.__id = dbc.fetchone()[0] - dbc.execute('INSERT INTO maillocation(mid,maillocation) VALUES(%s,%s)', - self.__id, self.__maillocation) - self._dbh.commit() - dbc.close() - - def getID(self): - """Returns the unique ID of the maillocation.""" - return self.__id - - def getMailLocation(self): - """Returns the value of maillocation, ex: 'Maildir'""" - return self.__maillocation - diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/Relocated.py --- a/VirtualMailManager/Relocated.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,103 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2008 - 2010, Pascal Volk -# See COPYING for distribution information. - -"""Virtual Mail Manager's Relocated class to manage relocated users.""" - -from __main__ import ERR -from Exceptions import VMMRelocatedException as VMMRE -from Domain import Domain -from EmailAddress import EmailAddress -import VirtualMailManager as VMM - -class Relocated(object): - """Class to manage e-mail addresses of relocated users.""" - __slots__ = ('_addr', '_dest', '_gid', '_isNew', '_dbh') - def __init__(self, dbh, address, destination=None): - if isinstance(address, EmailAddress): - self._addr = address - else: - raise TypeError("Argument 'address' is not an EmailAddress") - if destination is None: - self._dest = None - elif isinstance(destination, EmailAddress): - self._dest = destination - else: - raise TypeError("Argument 'destination' is not an EmailAddress") - if address == destination: - raise VMMRE(_(u"Address and destination are identical."), - ERR.RELOCATED_ADDR_DEST_IDENTICAL) - self._dbh = dbh - self._gid = 0 - self._isNew = False - self._setAddr() - self._exists() - if self._isNew and VMM.VirtualMailManager.accountExists(self._dbh, - self._addr): - raise VMMRE(_(u"There is already an account with address “%s”.") %\ - self._addr, ERR.ACCOUNT_EXISTS) - if self._isNew and VMM.VirtualMailManager.aliasExists(self._dbh, - self._addr): - raise VMMRE( - _(u"There is already an alias with the address “%s”.") %\ - self._addr, ERR.ALIAS_EXISTS) - - def _exists(self): - dbc = self._dbh.cursor() - dbc.execute("SELECT gid FROM relocated WHERE gid = %s AND address = %s", - self._gid, self._addr._localpart) - gid = dbc.fetchone() - dbc.close() - if gid is None: - self._isNew = True - - def _setAddr(self): - dom = Domain(self._dbh, self._addr._domainname) - self._gid = dom.getID() - if self._gid == 0: - raise VMMRE(_(u"The domain “%s” doesn't exist.") %\ - self._addr._domainname, ERR.NO_SUCH_DOMAIN) - - def save(self): - if self._dest is None: - raise VMMRE( - _(u"No destination address specified for relocated user."), - ERR.RELOCATED_MISSING_DEST) - if self._isNew: - dbc = self._dbh.cursor() - dbc.execute("INSERT INTO relocated VALUES (%s, %s, %s)", - self._gid, self._addr._localpart, str(self._dest)) - self._dbh.commit() - dbc.close() - else: - raise VMMRE( - _(u"The relocated user “%s” already exists.") % self._addr, - ERR.RELOCATED_EXISTS) - - def getInfo(self): - dbc = self._dbh.cursor() - dbc.execute('SELECT destination FROM relocated WHERE gid=%s\ - AND address=%s', - self._gid, self._addr._localpart) - destination = dbc.fetchone() - dbc.close() - if destination is not None: - return destination[0] - else: - raise VMMRE( - _(u"The relocated user “%s” doesn't exist.") % self._addr, - ERR.NO_SUCH_RELOCATED) - - def delete(self): - dbc = self._dbh.cursor() - dbc.execute("DELETE FROM relocated WHERE gid = %s AND address = %s", - self._gid, self._addr._localpart) - rowcount = dbc.rowcount - dbc.close() - if rowcount > 0: - self._dbh.commit() - else: - raise VMMRE( - _(u"The relocated user “%s” doesn't exist.") % self._addr, - ERR.NO_SUCH_RELOCATED) - diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/Transport.py --- a/VirtualMailManager/Transport.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,90 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2008 - 2010, Pascal Volk -# See COPYING for distribution information. - -"""Virtual Mail Manager's Transport class to manage the transport for -domains and accounts.""" - -from __main__ import ERR -from Exceptions import VMMTransportException - -class Transport(object): - """A wrapper class that provides access to the transport table""" - __slots__ = ('__id', '__transport', '_dbh') - def __init__(self, dbh, tid=None, transport=None): - """Creates a new Transport instance. - - Either tid or transport must be specified. - - Keyword arguments: - dbh -- a pyPgSQL.PgSQL.connection - tid -- the id of a transport (long) - transport -- the value of the transport (str) - """ - self._dbh = dbh - if tid is None and transport is None: - raise VMMTransportException( - _('Either tid or transport must be specified.'), - ERR.TRANSPORT_INIT) - elif tid is not None: - try: - self.__id = long(tid) - except ValueError: - raise VMMTransportException(_('tid must be an int/long.'), - ERR.TRANSPORT_INIT) - self._loadByID() - else: - self.__transport = transport - self._loadByName() - - def __eq__(self, other): - if isinstance(other, self.__class__): - return self.__id == other.getID() - return NotImplemented - - def __ne__(self, other): - if isinstance(other, self.__class__): - return self.__id != other.getID() - return NotImplemented - - def __str__(self): - return self.__transport - - def _loadByID(self): - dbc = self._dbh.cursor() - dbc.execute('SELECT transport FROM transport WHERE tid = %s', self.__id) - result = dbc.fetchone() - dbc.close() - if result is not None: - self.__transport = result[0] - else: - raise VMMTransportException(_('Unknown tid specified.'), - ERR.UNKNOWN_TRANSPORT_ID) - - def _loadByName(self): - dbc = self._dbh.cursor() - dbc.execute('SELECT tid FROM transport WHERE transport = %s', - self.__transport) - result = dbc.fetchone() - dbc.close() - if result is not None: - self.__id = result[0] - else: - self._save() - - def _save(self): - dbc = self._dbh.cursor() - dbc.execute("SELECT nextval('transport_id')") - self.__id = dbc.fetchone()[0] - dbc.execute('INSERT INTO transport (tid, transport) VALUES (%s, %s)', - self.__id, self.__transport) - self._dbh.commit() - dbc.close() - - def getID(self): - """Returns the unique ID of the transport.""" - return self.__id - - def getTransport(self): - """Returns the value of transport, ex: 'dovecot:'""" - return self.__transport diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/VirtualMailManager.py --- a/VirtualMailManager/VirtualMailManager.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,709 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2007 - 2010, Pascal Volk -# See COPYING for distribution information. - -"""The main class for vmm.""" - - -from encodings.idna import ToASCII, ToUnicode -from getpass import getpass -from shutil import rmtree -from subprocess import Popen, PIPE - -from pyPgSQL import PgSQL # python-pgsql - http://pypgsql.sourceforge.net - -from __main__ import os, re, ENCODING, ERR, w_std -from ext.Postconf import Postconf -from Account import Account -from Alias import Alias -from AliasDomain import AliasDomain -from Config import Config as Cfg -from Domain import Domain -from EmailAddress import EmailAddress -from Exceptions import * -from Relocated import Relocated - -SALTCHARS = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' -RE_ASCII_CHARS = """^[\x20-\x7E]*$""" -RE_DOMAIN = """^(?:[a-z0-9-]{1,63}\.){1,}[a-z]{2,6}$""" -RE_DOMAIN_SRCH = """^[a-z0-9-\.]+$""" -RE_LOCALPART = """[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]""" -RE_MBOX_NAMES = """^[\x20-\x25\x27-\x7E]*$""" - -class VirtualMailManager(object): - """The main class for vmm""" - __slots__ = ('__Cfg', '__cfgFileName', '__cfgSections', '__dbh', '__scheme', - '__warnings', '_postconf') - def __init__(self): - """Creates a new VirtualMailManager instance. - Throws a VMMNotRootException if your uid is greater 0. - """ - self.__cfgFileName = '' - self.__warnings = [] - self.__Cfg = None - self.__dbh = None - - if os.geteuid(): - raise VMMNotRootException(_(u"You are not root.\n\tGood bye!\n"), - ERR.CONF_NOPERM) - if self.__chkCfgFile(): - self.__Cfg = Cfg(self.__cfgFileName) - self.__Cfg.load() - self.__Cfg.check() - self.__cfgSections = self.__Cfg.getsections() - self.__scheme = self.__Cfg.get('misc', 'passwdscheme') - self._postconf = Postconf(self.__Cfg.get('bin', 'postconf')) - if not os.sys.argv[1] in ['cf', 'configure']: - self.__chkenv() - - def __findCfgFile(self): - for path in ['/root', '/usr/local/etc', '/etc']: - tmp = os.path.join(path, 'vmm.cfg') - if os.path.isfile(tmp): - self.__cfgFileName = tmp - break - if not len(self.__cfgFileName): - raise VMMException( - _(u"No “vmm.cfg” found in: /root:/usr/local/etc:/etc"), - ERR.CONF_NOFILE) - - def __chkCfgFile(self): - """Checks the configuration file, returns bool""" - self.__findCfgFile() - fstat = os.stat(self.__cfgFileName) - fmode = int(oct(fstat.st_mode & 0777)) - if fmode % 100 and fstat.st_uid != fstat.st_gid \ - or fmode % 10 and fstat.st_uid == fstat.st_gid: - raise VMMPermException(_( - u'fix permissions (%(perms)s) for “%(file)s”\n\ -`chmod 0600 %(file)s` would be great.') % {'file': - self.__cfgFileName, 'perms': fmode}, ERR.CONF_WRONGPERM) - else: - return True - - def __chkenv(self): - """""" - if not os.path.exists(self.__Cfg.get('domdir', 'base')): - old_umask = os.umask(0006) - os.makedirs(self.__Cfg.get('domdir', 'base'), 0771) - os.chown(self.__Cfg.get('domdir', 'base'), 0, - self.__Cfg.getint('misc', 'gid_mail')) - os.umask(old_umask) - elif not os.path.isdir(self.__Cfg.get('domdir', 'base')): - raise VMMException(_(u'“%s” is not a directory.\n\ -(vmm.cfg: section "domdir", option "base")') % - self.__Cfg.get('domdir', 'base'), ERR.NO_SUCH_DIRECTORY) - for opt, val in self.__Cfg.items('bin'): - if not os.path.exists(val): - raise VMMException(_(u'“%(binary)s” doesn\'t exist.\n\ -(vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt}, - ERR.NO_SUCH_BINARY) - elif not os.access(val, os.X_OK): - raise VMMException(_(u'“%(binary)s” is not executable.\n\ -(vmm.cfg: section "bin", option "%(option)s")') %{'binary': val,'option': opt}, - ERR.NOT_EXECUTABLE) - - def __dbConnect(self): - """Creates a pyPgSQL.PgSQL.connection instance.""" - if self.__dbh is None or not self.__dbh._isOpen: - 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 idn2ascii(domainname): - """Converts an idn domainname in punycode. - - Arguments: - domainname -- the domainname to convert (unicode) - """ - return '.'.join([ToASCII(lbl) for lbl in domainname.split('.') if lbl]) - idn2ascii = staticmethod(idn2ascii) - - def ace2idna(domainname): - """Convertis a domainname from ACE according to IDNA - - Arguments: - domainname -- the domainname to convert (str) - """ - return u'.'.join([ToUnicode(lbl) for lbl in domainname.split('.')\ - if lbl]) - ace2idna = staticmethod(ace2idna) - - def chkDomainname(domainname): - """Validates the domain name of an e-mail address. - - Keyword arguments: - domainname -- the domain name that should be validated - """ - if not re.match(RE_ASCII_CHARS, domainname): - domainname = VirtualMailManager.idn2ascii(domainname) - if len(domainname) > 255: - raise VMMException(_(u'The domain name is too long.'), - ERR.DOMAIN_TOO_LONG) - if not re.match(RE_DOMAIN, domainname): - raise VMMException(_(u'The domain name “%s” is invalid.') %\ - domainname, ERR.DOMAIN_INVALID) - return domainname - chkDomainname = staticmethod(chkDomainname) - - def _exists(dbh, query): - dbc = dbh.cursor() - dbc.execute(query) - gid = dbc.fetchone() - dbc.close() - if gid is None: - return False - else: - return True - _exists = staticmethod(_exists) - - def accountExists(dbh, address): - sql = "SELECT gid FROM users WHERE gid = (SELECT gid FROM domain_name\ - WHERE domainname = '%s') AND local_part = '%s'" % (address._domainname, - address._localpart) - return VirtualMailManager._exists(dbh, sql) - accountExists = staticmethod(accountExists) - - def aliasExists(dbh, address): - sql = "SELECT DISTINCT gid FROM alias WHERE gid = (SELECT gid FROM\ - domain_name WHERE domainname = '%s') AND address = '%s'" %\ - (address._domainname, address._localpart) - return VirtualMailManager._exists(dbh, sql) - aliasExists = staticmethod(aliasExists) - - def relocatedExists(dbh, address): - sql = "SELECT gid FROM relocated WHERE gid = (SELECT gid FROM\ - domain_name WHERE domainname = '%s') AND address = '%s'" %\ - (address._domainname, address._localpart) - return VirtualMailManager._exists(dbh, sql) - relocatedExists = staticmethod(relocatedExists) - - def _readpass(self): - # TP: Please preserve the trailing space. - readp_msg0 = _(u'Enter new password: ').encode(ENCODING, 'replace') - # TP: Please preserve the trailing space. - readp_msg1 = _(u'Retype new password: ').encode(ENCODING, 'replace') - mismatched = True - flrs = 0 - while mismatched: - if flrs > 2: - raise VMMException(_(u'Too many failures - try again later.'), - ERR.VMM_TOO_MANY_FAILURES) - clear0 = getpass(prompt=readp_msg0) - clear1 = getpass(prompt=readp_msg1) - if clear0 != clear1: - flrs += 1 - w_std(_(u'Sorry, passwords do not match')) - continue - if len(clear0) < 1: - flrs += 1 - w_std(_(u'Sorry, empty passwords are not permitted')) - continue - mismatched = False - return clear0 - - def __getAccount(self, address, password=None): - self.__dbConnect() - address = EmailAddress(address) - if not password is None: - password = self.__pwhash(password) - return Account(self.__dbh, address, password) - - def __getAlias(self, address, destination=None): - self.__dbConnect() - address = EmailAddress(address) - if destination is not None: - destination = EmailAddress(destination) - return Alias(self.__dbh, address, destination) - - def __getRelocated(self,address, destination=None): - self.__dbConnect() - address = EmailAddress(address) - if destination is not None: - destination = EmailAddress(destination) - return Relocated(self.__dbh, address, destination) - - def __getDomain(self, domainname, transport=None): - if transport is None: - transport = self.__Cfg.get('misc', 'transport') - self.__dbConnect() - return Domain(self.__dbh, domainname, - self.__Cfg.get('domdir', '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 - """ - if self.__isdir(directory): - return Popen([self.__Cfg.get('bin', 'du'), "-hs", directory], - stdout=PIPE).communicate()[0].split('\t')[0] - else: - return 0 - - def __isdir(self, directory): - isdir = os.path.isdir(directory) - if not isdir: - self.__warnings.append(_('No such directory: %s') % directory) - return isdir - - 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('domdir', '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 __subscribeFL(self, folderlist, uid, gid): - fname = os.path.join(self.__Cfg.get('maildir','name'), 'subscriptions') - sf = file(fname, 'w') - for f in folderlist: - sf.write(f+'\n') - sf.flush() - sf.close() - os.chown(fname, uid, gid) - os.chmod(fname, 384) - - def __mailDirMake(self, domdir, uid, gid): - """Creates maildirs and maildir subfolders. - - Keyword arguments: - domdir -- the path to the domain directory - uid -- user id from the account - gid -- group id from the account - """ - os.umask(0007) - oldpwd = os.getcwd() - os.chdir(domdir) - - maildir = self.__Cfg.get('maildir', 'name') - folders = [maildir] - for folder in self.__Cfg.get('maildir', 'folders').split(':'): - folder = folder.strip() - if len(folder) and not folder.count('..')\ - and re.match(RE_MBOX_NAMES, folder): - folders.append('%s/.%s' % (maildir, folder)) - 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(os.path.join(folder, subdir), mode, uid, gid) - self.__subscribeFL([f.replace(maildir+'/.', '') for f in folders[1:]], - uid, gid) - os.chdir(oldpwd) - - def __userDirDelete(self, domdir, uid, gid): - if uid > 0 and gid > 0: - userdir = '%s' % uid - if userdir.count('..') or domdir.count('..'): - raise VMMException(_(u'Found ".." in home directory path.'), - ERR.FOUND_DOTS_IN_PATH) - if os.path.isdir(domdir): - os.chdir(domdir) - if os.path.isdir(userdir): - mdstat = os.stat(userdir) - if (mdstat.st_uid, mdstat.st_gid) != (uid, gid): - raise VMMException( - _(u'Detected owner/group mismatch in home directory.'), - ERR.MAILDIR_PERM_MISMATCH) - rmtree(userdir, ignore_errors=True) - else: - raise VMMException(_(u"No such directory: %s") % - os.path.join(domdir, userdir), ERR.NO_SUCH_DIRECTORY) - - def __domDirDelete(self, domdir, gid): - if gid > 0: - if not self.__isdir(domdir): - return - basedir = self.__Cfg.get('domdir', 'base') - domdirdirs = domdir.replace(basedir+'/', '').split('/') - domdirparent = os.path.join(basedir, domdirdirs[0]) - if basedir.count('..') or domdir.count('..'): - raise VMMException(_(u'Found ".." in domain directory path.'), - ERR.FOUND_DOTS_IN_PATH) - if os.path.isdir(domdirparent): - os.chdir(domdirparent) - if os.lstat(domdirdirs[1]).st_gid != gid: - raise VMMException(_( - u'Detected group mismatch in domain directory.'), - ERR.DOMAINDIR_GROUP_MISMATCH) - rmtree(domdirdirs[1], ignore_errors=True) - - def __getSalt(self): - from random import choice - salt = None - if self.__scheme == 'CRYPT': - salt = '%s%s' % (choice(SALTCHARS), choice(SALTCHARS)) - elif self.__scheme in ['MD5', 'MD5-CRYPT']: - salt = '$1$%s$' % ''.join([choice(SALTCHARS) for x in xrange(8)]) - return salt - - def __pwCrypt(self, password): - # for: CRYPT, MD5 and MD5-CRYPT - from crypt import crypt - return crypt(password, self.__getSalt()) - - def __pwSHA1(self, password): - # for: SHA/SHA1 - import sha - from base64 import standard_b64encode - sha1 = sha.new(password) - return standard_b64encode(sha1.digest()) - - def __pwMD5(self, password, emailaddress=None): - import md5 - _md5 = md5.new(password) - if self.__scheme == 'LDAP-MD5': - from base64 import standard_b64encode - return standard_b64encode(_md5.digest()) - elif self.__scheme == 'PLAIN-MD5': - return _md5.hexdigest() - elif self.__scheme == 'DIGEST-MD5' and emailaddress is not None: - # use an empty realm - works better with usenames like user@dom - _md5 = md5.new('%s::%s' % (emailaddress, password)) - return _md5.hexdigest() - - def __pwMD4(self, password): - # for: PLAIN-MD4 - from Crypto.Hash import MD4 - _md4 = MD4.new(password) - return _md4.hexdigest() - - def __pwhash(self, password, scheme=None, user=None): - if scheme is not None: - self.__scheme = scheme - if self.__scheme in ['CRYPT', 'MD5', 'MD5-CRYPT']: - return '{%s}%s' % (self.__scheme, self.__pwCrypt(password)) - elif self.__scheme in ['SHA', 'SHA1']: - return '{%s}%s' % (self.__scheme, self.__pwSHA1(password)) - elif self.__scheme in ['PLAIN-MD5', 'LDAP-MD5', 'DIGEST-MD5']: - return '{%s}%s' % (self.__scheme, self.__pwMD5(password, user)) - elif self.__scheme == 'PLAIN-MD4': - return '{%s}%s' % (self.__scheme, self.__pwMD4(password)) - elif self.__scheme in ['SMD5', 'SSHA', 'CRAM-MD5', 'HMAC-MD5', - 'LANMAN', 'NTLM', 'RPA']: - cmd_args = [self.__Cfg.get('bin', 'dovecotpw'), '-s', - self.__scheme, '-p', password] - if self.__Cfg.getint('misc', 'dovecotvers') >= 20: - cmd_args.insert(1, 'pw') - return Popen(cmd_args, stdout=PIPE).communicate()[0][:-1] - else: - return '{%s}%s' % (self.__scheme, password) - - 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 cfgGetBoolean(self, section, option): - return self.__Cfg.getboolean(section, option) - - def cfgGetInt(self, section, option): - return self.__Cfg.getint(section, option) - - def cfgGetString(self, section, option): - return self.__Cfg.get(section, option) - - def setupIsDone(self): - """Checks if vmm is configured, returns bool""" - try: - return self.__Cfg.getboolean('config', 'done') - except ValueError, e: - raise VMMConfigException(_(u"""Configuration error: "%s" -(in section "config", option "done") see also: vmm.cfg(5)\n""") % str(e), - ERR.CONF_ERROR) - - 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' - """ - if section is None: - self.__Cfg.configure(self.__cfgSections) - elif section in self.__cfgSections: - self.__Cfg.configure([section]) - else: - raise VMMException(_(u"Invalid section: “%s”") % section, - ERR.INVALID_SECTION) - - def domainAdd(self, domainname, transport=None): - dom = self.__getDomain(domainname, transport) - dom.save() - self.__domDirMake(dom.getDir(), dom.getID()) - - def domainTransport(self, domainname, transport, force=None): - if force is not None and force != 'force': - raise VMMDomainException(_(u"Invalid argument: “%s”") % force, - ERR.INVALID_OPTION) - dom = self.__getDomain(domainname, None) - if force is None: - dom.updateTransport(transport) - else: - dom.updateTransport(transport, force=True) - - def domainDelete(self, domainname, force=None): - if not force is None and force not in ['deluser','delalias','delall']: - raise VMMDomainException(_(u"Invalid argument: “%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 domainInfo(self, domainname, details=None): - if details not in [None, 'accounts', 'aliasdomains', 'aliases', 'full', - 'relocated', 'detailed']: - raise VMMException(_(u'Invalid argument: “%s”') % details, - ERR.INVALID_AGUMENT) - if details == 'detailed': - details = 'full' - self.__warnings.append(_(u'\ -The keyword “detailed” is deprecated and will be removed in a future release.\n\ - Please use the keyword “full” to get full details.')) - dom = self.__getDomain(domainname) - dominfo = dom.getInfo() - if dominfo['domainname'].startswith('xn--'): - dominfo['domainname'] += ' (%s)'\ - % VirtualMailManager.ace2idna(dominfo['domainname']) - if details is None: - return dominfo - elif details == 'accounts': - return (dominfo, dom.getAccounts()) - elif details == 'aliasdomains': - return (dominfo, dom.getAliaseNames()) - elif details == 'aliases': - return (dominfo, dom.getAliases()) - elif details == 'relocated': - return(dominfo, dom.getRelocated()) - else: - return (dominfo, dom.getAliaseNames(), dom.getAccounts(), - dom.getAliases(), dom.getRelocated()) - - def aliasDomainAdd(self, aliasname, domainname): - """Adds an alias domain to the domain. - - Keyword arguments: - aliasname -- the name of the alias domain (str) - domainname -- name of the target domain (str) - """ - dom = self.__getDomain(domainname) - aliasDom = AliasDomain(self.__dbh, aliasname, dom) - aliasDom.save() - - def aliasDomainInfo(self, aliasname): - self.__dbConnect() - aliasDom = AliasDomain(self.__dbh, aliasname, None) - return aliasDom.info() - - def aliasDomainSwitch(self, aliasname, domainname): - """Modifies the target domain of an existing alias domain. - - Keyword arguments: - aliasname -- the name of the alias domain (str) - domainname -- name of the new target domain (str) - """ - dom = self.__getDomain(domainname) - aliasDom = AliasDomain(self.__dbh, aliasname, dom) - aliasDom.switch() - - def aliasDomainDelete(self, aliasname): - """Deletes the specified alias domain. - - Keyword arguments: - aliasname -- the name of the alias domain (str) - """ - self.__dbConnect() - aliasDom = AliasDomain(self.__dbh, aliasname, None) - aliasDom.delete() - - def domainList(self, pattern=None): - from Domain import search - like = False - if pattern is not None: - if pattern.startswith('%') or pattern.endswith('%'): - like = True - domain = pattern.strip('%') - if not re.match(RE_DOMAIN_SRCH, domain): - raise VMMException( - _(u"The pattern “%s” contains invalid characters.") % - pattern, ERR.DOMAIN_INVALID) - self.__dbConnect() - return search(self.__dbh, pattern=pattern, like=like) - - def userAdd(self, emailaddress, password): - acc = self.__getAccount(emailaddress, password) - if password is None: - password = self._readpass() - acc.setPassword(self.__pwhash(password)) - acc.save(self.__Cfg.get('maildir', 'name'), - self.__Cfg.getint('misc', 'dovecotvers'), - self.__Cfg.getboolean('services', 'smtp'), - self.__Cfg.getboolean('services', 'pop3'), - self.__Cfg.getboolean('services', 'imap'), - self.__Cfg.getboolean('services', 'sieve')) - self.__mailDirMake(acc.getDir('domain'), acc.getUID(), acc.getGID()) - - def aliasAdd(self, aliasaddress, targetaddress): - alias = self.__getAlias(aliasaddress, targetaddress) - alias.save(long(self._postconf.read('virtual_alias_expansion_limit'))) - gid = self.__getDomain(alias._dest._domainname).getID() - if gid > 0 and not VirtualMailManager.accountExists(self.__dbh, - alias._dest) and not VirtualMailManager.aliasExists(self.__dbh, - alias._dest): - self.__warnings.append( - _(u"The destination account/alias “%s” doesn't exist.")%\ - alias._dest) - - def userDelete(self, emailaddress, force=None): - if force not in [None, 'delalias']: - raise VMMException(_(u"Invalid argument: “%s”") % force, - ERR.INVALID_AGUMENT) - acc = self.__getAccount(emailaddress) - uid = acc.getUID() - gid = acc.getGID() - acc.delete(force) - if self.__Cfg.getboolean('maildir', 'delete'): - try: - self.__userDirDelete(acc.getDir('domain'), uid, gid) - except VMMException, e: - if e.code() in [ERR.FOUND_DOTS_IN_PATH, - ERR.MAILDIR_PERM_MISMATCH, ERR.NO_SUCH_DIRECTORY]: - warning = _(u"""\ -The account has been successfully deleted from the database. - But an error occurred while deleting the following directory: - “%(directory)s” - Reason: %(reason)s""") % {'directory': acc.getDir('home'),'reason': e.msg()} - self.__warnings.append(warning) - else: - raise e - - def aliasInfo(self, aliasaddress): - alias = self.__getAlias(aliasaddress) - return alias.getInfo() - - def aliasDelete(self, aliasaddress, targetaddress=None): - alias = self.__getAlias(aliasaddress, targetaddress) - alias.delete() - - def userInfo(self, emailaddress, details=None): - if details not in [None, 'du', 'aliases', 'full']: - raise VMMException(_(u'Invalid argument: “%s”') % details, - ERR.INVALID_AGUMENT) - acc = self.__getAccount(emailaddress) - info = acc.getInfo(self.__Cfg.getint('misc', 'dovecotvers')) - if self.__Cfg.getboolean('maildir', 'diskusage')\ - or details in ['du', 'full']: - info['disk usage'] = self.__getDiskUsage('%(maildir)s' % info) - if details in [None, 'du']: - return info - if details in ['aliases', 'full']: - return (info, acc.getAliases()) - return info - - def userByID(self, uid): - from Account import getAccountByID - self.__dbConnect() - return getAccountByID(uid, self.__dbh) - - def userPassword(self, emailaddress, password): - acc = self.__getAccount(emailaddress) - if acc.getUID() == 0: - raise VMMException(_(u"Account doesn't exist"), ERR.NO_SUCH_ACCOUNT) - if password is None: - password = self._readpass() - acc.modify('password', self.__pwhash(password, user=emailaddress)) - - def userName(self, emailaddress, name): - acc = self.__getAccount(emailaddress) - acc.modify('name', name) - - def userTransport(self, emailaddress, transport): - acc = self.__getAccount(emailaddress) - acc.modify('transport', transport) - - def userDisable(self, emailaddress, service=None): - if service == 'managesieve': - service = 'sieve' - self.__warnings.append(_(u'\ -The service name “managesieve” is deprecated and will be removed\n\ - in a future release.\n\ - Please use the service name “sieve” instead.')) - acc = self.__getAccount(emailaddress) - acc.disable(self.__Cfg.getint('misc', 'dovecotvers'), service) - - def userEnable(self, emailaddress, service=None): - if service == 'managesieve': - service = 'sieve' - self.__warnings.append(_(u'\ -The service name “managesieve” is deprecated and will be removed\n\ - in a future release.\n\ - Please use the service name “sieve” instead.')) - acc = self.__getAccount(emailaddress) - acc.enable(self.__Cfg.getint('misc', 'dovecotvers'), service) - - def relocatedAdd(self, emailaddress, targetaddress): - relocated = self.__getRelocated(emailaddress, targetaddress) - relocated.save() - - def relocatedInfo(self, emailaddress): - relocated = self.__getRelocated(emailaddress) - return relocated.getInfo() - - def relocatedDelete(self, emailaddress): - relocated = self.__getRelocated(emailaddress) - relocated.delete() - - def __del__(self): - if not self.__dbh is None and self.__dbh._isOpen: - self.__dbh.close() diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/__init__.py --- a/VirtualMailManager/__init__.py Mon Nov 07 03:22:15 2011 +0000 +++ b/VirtualMailManager/__init__.py Thu Jun 28 19:26:50 2012 +0000 @@ -1,51 +1,35 @@ # -*- coding: UTF-8 -*- -# Copyright (c) 2007 - 2010, Pascal Volk +# Copyright (c) 2007 - 2012, Pascal Volk # See COPYING for distribution information. -# package initialization code -# +""" + VirtualMailManager + ~~~~~~~~~~~~~~~~~~ + + VirtualMailManager package initialization code +""" -import os -import re +import gettext import locale +import sys -from constants.VERSION import * -import constants.ERROR as ERR +from VirtualMailManager.constants import __author__, __date__, __version__ + +__all__ = [ + # version information from VERSION + '__author__', '__date__', '__version__', + # defined stuff + 'ENCODING', +] + # Try to set all of the locales according to the current # environment variables and get the character encoding. try: locale.setlocale(locale.LC_ALL, '') except locale.Error: + sys.stderr.write('warning: unsupported locale setting - ' + 'that may cause encoding problems.\n\n') locale.setlocale(locale.LC_ALL, 'C') ENCODING = locale.nl_langinfo(locale.CODESET) -def w_std(*args): - """Writes each arg of args, encoded in the current ENCODING, to stdout and - appends a newline.""" - _write = os.sys.stdout.write - for arg in args: - _write(arg.encode(ENCODING, 'replace')) - _write('\n') - -def w_err(code, *args): - """Writes each arg of args, encoded in the current ENCODING, to stderr and - appends a newline. - This function additional interrupts the program execution and uses 'code' - system exit status.""" - _write = os.sys.stderr.write - for arg in args: - _write(arg.encode(ENCODING, 'replace')) - _write('\n') - os.sys.exit(code) - -__all__ = [ - # imported modules - 'os', 're', 'locale', - # version information from VERSION - '__author__', '__date__', '__version__', - # error codes - 'ERR', - # defined stuff - 'ENCODING', 'w_std', 'w_err' - ] -# EOF +gettext.install('vmm', '/usr/local/share/locale', unicode=1) diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/account.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/account.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,506 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2007 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.account + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Virtual Mail Manager's Account class to manage e-mail accounts. +""" + +from VirtualMailManager.common import version_str, \ + format_domain_default +from VirtualMailManager.constants import \ + ACCOUNT_EXISTS, ACCOUNT_MISSING_PASSWORD, ALIAS_PRESENT, \ + INVALID_ARGUMENT, INVALID_MAIL_LOCATION, NO_SUCH_ACCOUNT, \ + NO_SUCH_DOMAIN, VMM_ERROR +from VirtualMailManager.domain import Domain +from VirtualMailManager.emailaddress import EmailAddress +from VirtualMailManager.errors import VMMError, AccountError as AErr +from VirtualMailManager.maillocation import MailLocation +from VirtualMailManager.password import pwhash +from VirtualMailManager.quotalimit import QuotaLimit +from VirtualMailManager.transport import Transport +from VirtualMailManager.serviceset import ServiceSet + +__all__ = ('Account', 'get_account_by_uid') + +_ = lambda msg: msg +cfg_dget = lambda option: None + + +class Account(object): + """Class to manage e-mail accounts.""" + __slots__ = ('_addr', '_dbh', '_domain', '_mail', '_new', '_passwd', + '_qlimit', '_services', '_transport', '_note', '_uid') + + def __init__(self, dbh, address): + """Creates a new Account instance. + + When an account with the given *address* could be found in the + database all relevant data will be loaded. + + Arguments: + + `dbh` : pyPgSQL.PgSQL.Connection + A database connection for the database access. + `address` : VirtualMailManager.EmailAddress.EmailAddress + The e-mail address of the (new) Account. + """ + if not isinstance(address, EmailAddress): + raise TypeError("Argument 'address' is not an EmailAddress") + self._addr = address + self._dbh = dbh + self._domain = Domain(self._dbh, self._addr.domainname) + if not self._domain.gid: + # TP: Hm, what “quotation marks” should be used? + # If you are unsure have a look at: + # http://en.wikipedia.org/wiki/Quotation_mark,_non-English_usage + raise AErr(_(u"The domain '%s' does not exist.") % + self._addr.domainname, NO_SUCH_DOMAIN) + self._uid = 0 + self._mail = None + self._qlimit = None + self._services = None + self._transport = None + self._note = None + self._passwd = None + self._new = True + self._load() + + def __nonzero__(self): + """Returns `True` if the Account is known, `False` if it's new.""" + return not self._new + + def _load(self): + """Load 'uid', 'mid', 'qid', 'ssid', 'tid' and 'note' from the + database and set _new to `False` - if the user could be found. """ + dbc = self._dbh.cursor() + dbc.execute('SELECT uid, mid, qid, ssid, tid, note FROM users ' + 'WHERE gid = %s AND local_part = %s', + (self._domain.gid, self._addr.localpart)) + result = dbc.fetchone() + dbc.close() + if result: + self._uid, _mid, _qid, _ssid, _tid, _note = result + + def load_helper(ctor, own, field, dbresult): + # Py25: cur = None if own is None else getattr(own, field) + if own is None: + cur = None + else: + cur = getattr(own, field) + if cur != dbresult: + kwargs = {field: dbresult} + if dbresult is None: + return dbresult + else: + return ctor(self._dbh, **kwargs) + + self._qlimit = load_helper(QuotaLimit, self._qlimit, 'qid', _qid) + self._services = load_helper(ServiceSet, self._services, 'ssid', + _ssid) + self._transport = load_helper(Transport, self._transport, 'tid', + _tid) + self._mail = MailLocation(self._dbh, mid=_mid) + self._note = _note + self._new = False + + def _set_uid(self): + """Set the unique ID for the new Account.""" + assert self._uid == 0 + dbc = self._dbh.cursor() + dbc.execute("SELECT nextval('users_uid')") + self._uid = dbc.fetchone()[0] + dbc.close() + + def _prepare(self, maillocation): + """Check and set different attributes - before we store the + information in the database. + """ + if maillocation.dovecot_version > cfg_dget('misc.dovecot_version'): + raise AErr(_(u"The mailbox format '%(mbfmt)s' requires Dovecot " + u">= v%(version)s.") % { + 'mbfmt': maillocation.mbformat, + 'version': version_str(maillocation.dovecot_version)}, + INVALID_MAIL_LOCATION) + if self._transport and not maillocation.postfix and \ + self._transport.transport.lower() in ('virtual:', 'virtual'): + raise AErr(_(u"Invalid transport '%(transport)s' for mailbox " + u"format '%(mbfmt)s'.") % + {'transport': self._transport, + 'mbfmt': maillocation.mbformat}, INVALID_MAIL_LOCATION) + self._mail = maillocation + self._set_uid() + + def _update_tables(self, column, value): + """Update various columns in the users table. + + Arguments: + + `column` : basestring + Name of the table column. Currently: qid, ssid and tid + `value` : long + The referenced key + """ + if column not in ('qid', 'ssid', 'tid'): + raise ValueError('Unknown column: %r' % column) + dbc = self._dbh.cursor() + dbc.execute('UPDATE users SET %s = %%s WHERE uid = %%s' % column, + (value, self._uid)) + if dbc.rowcount > 0: + self._dbh.commit() + dbc.close() + + def _count_aliases(self): + """Count all alias addresses where the destination address is the + address of the Account.""" + dbc = self._dbh.cursor() + dbc.execute('SELECT COUNT(destination) FROM alias WHERE destination ' + '= %s', (str(self._addr),)) + a_count = dbc.fetchone()[0] + dbc.close() + return a_count + + def _chk_state(self): + """Raise an AccountError if the Account is new - not yet saved in the + database.""" + if self._new: + raise AErr(_(u"The account '%s' does not exist.") % self._addr, + NO_SUCH_ACCOUNT) + + @property + def address(self): + """The Account's EmailAddress instance.""" + return self._addr + + @property + def domain(self): + """The Domain to which the Account belongs to.""" + if self._domain: + return self._domain + return None + + @property + def gid(self): + """The Account's group ID.""" + if self._domain: + return self._domain.gid + return None + + @property + def home(self): + """The Account's home directory.""" + if not self._new: + return '%s/%s' % (self._domain.directory, self._uid) + return None + + @property + def mail_location(self): + """The Account's MailLocation.""" + return self._mail + + @property + def note(self): + """The Account's note.""" + return self._note + + @property + def uid(self): + """The Account's unique ID.""" + return self._uid + + def set_password(self, password): + """Set a password for the new Account. + + If you want to update the password of an existing Account use + Account.modify(). + + Argument: + + `password` : basestring + The password for the new Account. + """ + if not self._new: + raise AErr(_(u"The account '%s' already exists.") % self._addr, + ACCOUNT_EXISTS) + if not isinstance(password, basestring) or not password: + raise AErr(_(u"Could not accept password: '%s'") % password, + ACCOUNT_MISSING_PASSWORD) + self._passwd = password + + def set_note(self, note): + """Set the account's (optional) note. + + Argument: + + `note` : basestring or None + The note, or None to remove + """ + assert note is None or isinstance(note, basestring) + self._note = note + + def save(self): + """Save the new Account in the database.""" + if not self._new: + raise AErr(_(u"The account '%s' already exists.") % self._addr, + ACCOUNT_EXISTS) + if not self._passwd: + raise AErr(_(u"No password set for account: '%s'") % self._addr, + ACCOUNT_MISSING_PASSWORD) + self._prepare(MailLocation(self._dbh, mbfmt=cfg_dget('mailbox.format'), + directory=cfg_dget('mailbox.root'))) + dbc = self._dbh.cursor() + qid = ssid = tid = None + if self._qlimit: + qid = self._qlimit.qid + if self._services: + ssid = self._services.ssid + if self._transport: + tid = self._transport.tid + dbc.execute('INSERT INTO users (local_part, passwd, uid, gid, mid, ' + 'qid, ssid, tid, note) ' + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)', + (self._addr.localpart, + pwhash(self._passwd, user=self._addr), self._uid, + self._domain.gid, self._mail.mid, qid, ssid, tid, +# self._qlimit.qid if self._qlimit else None, +# self._services.ssid if self._services else None, +# self._transport.tid if self._transport else None, + self._note)) + self._dbh.commit() + dbc.close() + self._new = False + + def modify(self, field, value): + """Update the Account's *field* to the new *value*. + + Possible values for *field* are: 'name', 'password', 'note'. + + Arguments: + + `field` : basestring + The attribute name: 'name', 'password' or 'note' + `value` : basestring + The new value of the attribute. + """ + if field not in ('name', 'password', 'note'): + raise AErr(_(u"Unknown field: '%s'") % field, INVALID_ARGUMENT) + self._chk_state() + dbc = self._dbh.cursor() + if field == 'password': + dbc.execute('UPDATE users SET passwd = %s WHERE uid = %s', + (pwhash(value, user=self._addr), self._uid)) + else: + dbc.execute('UPDATE users SET %s = %%s WHERE uid = %%s' % field, + (value, self._uid)) + if dbc.rowcount > 0: + self._dbh.commit() + dbc.close() + + def update_quotalimit(self, quotalimit): + """Update the user's quota limit. + + Arguments: + + `quotalimit` : VirtualMailManager.quotalimit.QuotaLimit + the new quota limit of the domain. + """ + if cfg_dget('misc.dovecot_version') < 0x10102f00: + raise VMMError(_(u'PostgreSQL-based dictionary quota requires ' + u'Dovecot >= v1.1.2.'), VMM_ERROR) + self._chk_state() + if quotalimit == self._qlimit: + return + self._qlimit = quotalimit + if quotalimit is not None: + assert isinstance(quotalimit, QuotaLimit) + quotalimit = quotalimit.qid + self._update_tables('qid', quotalimit) + + def update_serviceset(self, serviceset): + """Assign a different set of services to the Account. + + Argument: + + `serviceset` : VirtualMailManager.serviceset.ServiceSet + the new service set. + """ + self._chk_state() + if serviceset == self._services: + return + self._services = serviceset + if serviceset is not None: + assert isinstance(serviceset, ServiceSet) + serviceset = serviceset.ssid + self._update_tables('ssid', serviceset) + + def update_transport(self, transport): + """Sets a new transport for the Account. + + Arguments: + + `transport` : VirtualMailManager.transport.Transport + the new transport + """ + self._chk_state() + if transport == self._transport: + return + self._transport = transport + if transport is not None: + assert isinstance(transport, Transport) + if transport.transport.lower() in ('virtual', 'virtual:') and \ + not self._mail.postfix: + raise AErr(_(u"Invalid transport '%(transport)s' for mailbox " + u"format '%(mbfmt)s'.") % + {'transport': transport, 'mbfmt': self._mail.mbformat}, + INVALID_MAIL_LOCATION) + transport = transport.tid + self._update_tables('tid', transport) + + def _get_info_transport(self): + if self._transport: + return self._transport.transport + return format_domain_default(self._domain.transport.transport) + + def _get_info_serviceset(self): + if self._services: + services = self._services.services + fmt = lambda s: s + else: + services = self._domain.serviceset.services + fmt = format_domain_default + + ret = {} + for service, state in services.iteritems(): + # TP: A service (e.g. pop3 or imap) may be enabled/usable or + # disabled/unusable for a user. + ret[service] = fmt((_('disabled'), _('enabled'))[state]) + return ret + + def get_info(self): + """Returns a dict with some information about the Account. + + The keys of the dict are: 'address', 'gid', 'home', 'imap' + 'mail_location', 'name', 'pop3', 'sieve', 'smtp', transport', 'uid', + 'uq_bytes', 'uq_messages', 'ql_bytes', 'ql_messages', and + 'ql_domaindefault'. + """ + self._chk_state() + dbc = self._dbh.cursor() + dbc.execute('SELECT name, CASE WHEN bytes IS NULL THEN 0 ELSE bytes ' + 'END, CASE WHEN messages IS NULL THEN 0 ELSE messages END ' + 'FROM users LEFT JOIN userquota USING (uid) WHERE ' + 'users.uid = %s', (self._uid,)) + info = dbc.fetchone() + dbc.close() + if info: + info = dict(zip(('name', 'uq_bytes', 'uq_messages'), info)) + info.update(self._get_info_serviceset()) + info['address'] = self._addr + info['gid'] = self._domain.gid + info['home'] = '%s/%s' % (self._domain.directory, self._uid) + info['mail_location'] = self._mail.mail_location + if self._qlimit: + info['ql_bytes'] = self._qlimit.bytes + info['ql_messages'] = self._qlimit.messages + info['ql_domaindefault'] = False + else: + info['ql_bytes'] = self._domain.quotalimit.bytes + info['ql_messages'] = self._domain.quotalimit.messages + info['ql_domaindefault'] = True + info['transport'] = self._get_info_transport() + info['note'] = self._note + info['uid'] = self._uid + return info + # nearly impossible‽ + raise AErr(_(u"Could not fetch information for account: '%s'") % + self._addr, NO_SUCH_ACCOUNT) + + def get_aliases(self): + """Return a list with all alias e-mail addresses, whose destination + is the address of the Account.""" + self._chk_state() + dbc = self._dbh.cursor() + dbc.execute("SELECT address ||'@'|| domainname FROM alias, " + "domain_name WHERE destination = %s AND domain_name.gid = " + "alias.gid AND domain_name.is_primary ORDER BY address", + (str(self._addr),)) + addresses = dbc.fetchall() + dbc.close() + aliases = [] + if addresses: + aliases = [alias[0] for alias in addresses] + return aliases + + def delete(self, force=False): + """Delete the Account from the database. + + Argument: + + `force` : bool + if *force* is `True`, all aliases, which points to the Account, + will be also deleted. If there are aliases and *force* is + `False`, an AccountError will be raised. + """ + if not isinstance(force, bool): + raise TypeError('force must be a bool') + self._chk_state() + dbc = self._dbh.cursor() + if force: + dbc.execute('DELETE FROM users WHERE uid = %s', (self._uid),) + # delete also all aliases where the destination address is the same + # as for this account. + dbc.execute("DELETE FROM alias WHERE destination = %s", + (str(self._addr),)) + self._dbh.commit() + else: # check first for aliases + a_count = self._count_aliases() + if a_count > 0: + dbc.close() + raise AErr(_(u"There are %(count)d aliases with the " + u"destination address '%(address)s'.") % + {'count': a_count, 'address': self._addr}, + ALIAS_PRESENT) + dbc.execute('DELETE FROM users WHERE uid = %s', (self._uid,)) + self._dbh.commit() + dbc.close() + self._new = True + self._uid = 0 + self._addr = self._dbh = self._domain = self._passwd = None + self._mail = self._qlimit = self._services = self._transport = None + + +def get_account_by_uid(uid, dbh): + """Search an Account by its UID. + + This function returns a dict (keys: 'address', 'gid' and 'uid'), if an + Account with the given *uid* exists. + + Argument: + + `uid` : long + The Account unique ID. + `dbh` : pyPgSQL.PgSQL.Connection + a database connection for the database access. + """ + try: + uid = long(uid) + except ValueError: + raise AErr(_(u'UID must be an int/long.'), INVALID_ARGUMENT) + if uid < 1: + raise AErr(_(u'UID must be greater than 0.'), INVALID_ARGUMENT) + dbc = dbh.cursor() + dbc.execute("SELECT local_part||'@'|| domain_name.domainname AS address, " + "uid, users.gid, note FROM users LEFT JOIN domain_name ON " + "(domain_name.gid = users.gid AND is_primary) WHERE uid = %s", + (uid,)) + info = dbc.fetchone() + dbc.close() + if not info: + raise AErr(_(u"There is no account with the UID: '%d'") % uid, + NO_SUCH_ACCOUNT) + info = dict(zip(('address', 'uid', 'gid', 'note'), info)) + return info + +del _, cfg_dget diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/alias.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/alias.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,166 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2007 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.alias + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Virtual Mail Manager's Alias class to manage e-mail aliases. +""" + +from VirtualMailManager.domain import get_gid +from VirtualMailManager.emailaddress import \ + EmailAddress, DestinationEmailAddress as DestAddr +from VirtualMailManager.errors import AliasError as AErr +from VirtualMailManager.ext.postconf import Postconf +from VirtualMailManager.pycompat import all +from VirtualMailManager.constants import \ + ALIAS_EXCEEDS_EXPANSION_LIMIT, NO_SUCH_ALIAS, NO_SUCH_DOMAIN + + +_ = lambda msg: msg +cfg_dget = lambda option: None + + +class Alias(object): + """Class to manage e-mail aliases.""" + __slots__ = ('_addr', '_dests', '_gid', '_dbh') + + def __init__(self, dbh, address): + assert isinstance(address, EmailAddress) + self._addr = address + self._dbh = dbh + self._gid = get_gid(self._dbh, self._addr.domainname) + if not self._gid: + raise AErr(_(u"The domain '%s' does not exist.") % + self._addr.domainname, NO_SUCH_DOMAIN) + self._dests = [] + + self._load_dests() + + def _load_dests(self): + """Loads all known destination addresses into the _dests list.""" + dbc = self._dbh.cursor() + dbc.execute('SELECT destination FROM alias WHERE gid = %s AND ' + 'address = %s', (self._gid, self._addr.localpart)) + dests = dbc.fetchall() + if dbc.rowcount > 0: + self._dests.extend(DestAddr(dest[0], self._dbh) for dest in dests) + dbc.close() + + def _check_expansion(self, count_new): + """Checks the current expansion limit of the alias.""" + postconf = Postconf(cfg_dget('bin.postconf')) + limit = long(postconf.read('virtual_alias_expansion_limit')) + dcount = len(self._dests) + failed = False + if dcount == limit or dcount + count_new > limit: + failed = True + errmsg = _( +u"""Cannot add %(count_new)i new destination(s) to alias '%(address)s'. +Currently this alias expands into %(count)i/%(limit)i recipients. +%(count_new)i additional destination(s) will render this alias unusable. +Hint: Increase Postfix' virtual_alias_expansion_limit""") + elif dcount > limit: + failed = True + errmsg = _( +u"""Cannot add %(count_new)i new destination(s) to alias '%(address)s'. +This alias already exceeds its expansion limit (%(count)i/%(limit)i). +So its unusable, all messages addressed to this alias will be bounced. +Hint: Delete some destination addresses.""") + if failed: + raise AErr(errmsg % {'address': self._addr, 'count': dcount, + 'limit': limit, 'count_new': count_new}, + ALIAS_EXCEEDS_EXPANSION_LIMIT) + + def _delete(self, destination=None): + """Deletes a destination from the alias, if ``destination`` is + not ``None``. If ``destination`` is None, the alias with all + its destination addresses will be deleted. + + """ + dbc = self._dbh.cursor() + if not destination: + dbc.execute('DELETE FROM alias WHERE gid = %s AND address = %s', + (self._gid, self._addr.localpart)) + else: + dbc.execute('DELETE FROM alias WHERE gid = %s AND address = %s ' + 'AND destination = %s', + (self._gid, self._addr.localpart, str(destination))) + if dbc.rowcount > 0: + self._dbh.commit() + dbc.close() + + def __len__(self): + """Returns the number of destinations of the alias.""" + return len(self._dests) + + @property + def address(self): + """The Alias' EmailAddress instance.""" + return self._addr + + def add_destinations(self, destinations, warnings=None): + """Adds the `EmailAddress`es from *destinations* list to the + destinations of the alias. + + Destinations, that are already assigned to the alias, will be + removed from *destinations*. When done, this method will return + a set with all destinations, that were saved in the database. + """ + destinations = set(destinations) + assert destinations and \ + all(isinstance(dest, EmailAddress) for dest in destinations) + if not warnings is None: + assert isinstance(warnings, list) + if self._addr in destinations: + destinations.remove(self._addr) + if not warnings is None: + warnings.append(self._addr) + duplicates = destinations.intersection(set(self._dests)) + if duplicates: + destinations.difference_update(set(self._dests)) + if not warnings is None: + warnings.extend(duplicates) + if not destinations: + return destinations + self._check_expansion(len(destinations)) + dbc = self._dbh.cursor() + dbc.executemany("INSERT INTO alias (gid, address, destination) " + "VALUES (%d, '%s', %%s)" % (self._gid, + self._addr.localpart), + ((str(destination),) for destination in destinations)) + self._dbh.commit() + dbc.close() + self._dests.extend(destinations) + return destinations + + def del_destination(self, destination): + """Deletes the specified ``destination`` address from the alias.""" + assert isinstance(destination, EmailAddress) + if not self._dests: + raise AErr(_(u"The alias '%s' does not exist.") % self._addr, + NO_SUCH_ALIAS) + if not destination in self._dests: + raise AErr(_(u"The address '%(addr)s' is not a destination of " + u"the alias '%(alias)s'.") % {'addr': destination, + 'alias': self._addr}, NO_SUCH_ALIAS) + self._delete(destination) + self._dests.remove(destination) + + def get_destinations(self): + """Returns an iterator for all destinations of the alias.""" + if not self._dests: + raise AErr(_(u"The alias '%s' does not exist.") % self._addr, + NO_SUCH_ALIAS) + return iter(self._dests) + + def delete(self): + """Deletes the alias with all its destinations.""" + if not self._dests: + raise AErr(_(u"The alias '%s' does not exist.") % self._addr, + NO_SUCH_ALIAS) + self._delete() + del self._dests[:] + +del _, cfg_dget diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/aliasdomain.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/aliasdomain.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,143 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2008 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.aliasdomain + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Virtual Mail Manager's AliasDomain class to manage alias domains. +""" + +from VirtualMailManager.domain import Domain, check_domainname +from VirtualMailManager.constants import \ + ALIASDOMAIN_EXISTS, ALIASDOMAIN_ISDOMAIN, ALIASDOMAIN_NO_DOMDEST, \ + NO_SUCH_ALIASDOMAIN, NO_SUCH_DOMAIN +from VirtualMailManager.errors import AliasDomainError as ADErr + + +_ = lambda msg: msg + + +class AliasDomain(object): + """Class to manage e-mail alias domains.""" + __slots__ = ('_gid', '_name', '_domain', '_dbh') + + def __init__(self, dbh, domainname): + """Creates a new AliasDomain instance. + + Arguments: + + `dbh` : pyPgSQL.PgSQL.Connection + a database connection for the database access + `domainname` : basestring + the name of the AliasDomain""" + self._dbh = dbh + self._name = check_domainname(domainname) + self._gid = 0 + self._domain = None + self._load() + + def _load(self): + """Loads the AliasDomain's GID from the database and checks if the + domain name is marked as primary.""" + dbc = self._dbh.cursor() + dbc.execute('SELECT gid, is_primary FROM domain_name WHERE ' + 'domainname = %s', (self._name,)) + result = dbc.fetchone() + dbc.close() + if result: + if result[1]: + raise ADErr(_(u"The domain '%s' is a primary domain.") % + self._name, ALIASDOMAIN_ISDOMAIN) + self._gid = result[0] + + def set_destination(self, dest_domain): + """Set the destination of a new AliasDomain or updates the + destination of an existing AliasDomain. + + Argument: + + `dest_domain` : VirtualMailManager.Domain.Domain + the AliasDomain's destination domain + """ + assert isinstance(dest_domain, Domain) + self._domain = dest_domain + + def save(self): + """Stores information about the new AliasDomain in the database.""" + if self._gid > 0: + raise ADErr(_(u"The alias domain '%s' already exists.") % + self._name, ALIASDOMAIN_EXISTS) + if not self._domain: + raise ADErr(_(u'No destination domain set for the alias domain.'), + ALIASDOMAIN_NO_DOMDEST) + if self._domain.gid < 1: + raise ADErr(_(u"The target domain '%s' does not exist.") % + self._domain.name, NO_SUCH_DOMAIN) + dbc = self._dbh.cursor() + dbc.execute('INSERT INTO domain_name (domainname, gid, is_primary) ' + 'VALUES (%s, %s, FALSE)', (self._name, self._domain.gid)) + self._dbh.commit() + dbc.close() + self._gid = self._domain.gid + + def info(self): + """Returns a dict (keys: "alias" and "domain") with the names of the + AliasDomain and its primary domain.""" + if self._gid < 1: + raise ADErr(_(u"The alias domain '%s' does not exist.") % + self._name, NO_SUCH_ALIASDOMAIN) + dbc = self._dbh.cursor() + dbc.execute('SELECT domainname FROM domain_name WHERE gid = %s AND ' + 'is_primary', (self._gid,)) + domain = dbc.fetchone() + dbc.close() + if domain: + return {'alias': self._name, 'domain': domain[0]} + else: # an almost unlikely case, isn't it? + raise ADErr(_(u'There is no primary domain for the alias domain ' + u"'%s'.") % self._name, NO_SUCH_DOMAIN) + + def switch(self): + """Switch the destination of the AliasDomain to the new destination, + set with the method `set_destination()`. + """ + if not self._domain: + raise ADErr(_(u'No destination domain set for the alias domain.'), + ALIASDOMAIN_NO_DOMDEST) + if self._domain.gid < 1: + raise ADErr(_(u"The target domain '%s' does not exist.") % + self._domain.name, NO_SUCH_DOMAIN) + if self._gid < 1: + raise ADErr(_(u"The alias domain '%s' does not exist.") % + self._name, NO_SUCH_ALIASDOMAIN) + if self._gid == self._domain.gid: + raise ADErr(_(u"The alias domain '%(alias)s' is already assigned " + u"to the domain '%(domain)s'.") % + {'alias': self._name, 'domain': self._domain.name}, + ALIASDOMAIN_EXISTS) + dbc = self._dbh.cursor() + dbc.execute('UPDATE domain_name SET gid = %s WHERE gid = %s AND ' + 'domainname = %s AND NOT is_primary', (self._domain.gid, + self._gid, self._name)) + self._dbh.commit() + dbc.close() + self._gid = self._domain.gid + + def delete(self): + """Delete the AliasDomain's record form the database. + + Raises an AliasDomainError if the AliasDomain doesn't exist. + """ + if self._gid < 1: + raise ADErr(_(u"The alias domain '%s' does not exist.") % + self._name, NO_SUCH_ALIASDOMAIN) + dbc = self._dbh.cursor() + dbc.execute('DELETE FROM domain_name WHERE domainname = %s AND NOT ' + 'is_primary', (self._name,)) + if dbc.rowcount > 0: + self._dbh.commit() + self._gid = 0 + dbc.close() + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/catchall.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/catchall.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,171 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2012 martin f. krafft +# See COPYING for distribution information. +""" + VirtualMailManager.catchall + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Virtual Mail Manager's CatchallAlias class to manage domain catch-all + aliases. + + This is heavily based on (more or less a copy of) the Alias class, because + fundamentally, catchall aliases are aliases, but without a localpart. + While Alias could potentially derive from CatchallAlias to reuse some of + the functionality, it's probably not worth it. I found no sensible way to + derive CatchallAlias from Alias, or at least none that would harness the + powers of polymorphism. + + Yet, we reuse the AliasError exception class, which makes sense. +""" + +from VirtualMailManager.domain import get_gid +from VirtualMailManager.emailaddress import \ + EmailAddress, DestinationEmailAddress as DestAddr +from VirtualMailManager.errors import AliasError as AErr +from VirtualMailManager.ext.postconf import Postconf +from VirtualMailManager.pycompat import all +from VirtualMailManager.constants import \ + ALIAS_EXCEEDS_EXPANSION_LIMIT, NO_SUCH_ALIAS, NO_SUCH_DOMAIN + + +_ = lambda msg: msg +cfg_dget = lambda option: None + + +class CatchallAlias(object): + """Class to manage domain catch-all aliases.""" + __slots__ = ('_domain', '_dests', '_gid', '_dbh') + + def __init__(self, dbh, domain): + self._domain = domain + self._dbh = dbh + self._gid = get_gid(self._dbh, self.domain) + if not self._gid: + raise AErr(_(u"The domain '%s' does not exist.") % + self.domain, NO_SUCH_DOMAIN) + self._dests = [] + + self._load_dests() + + def _load_dests(self): + """Loads all known destination addresses into the _dests list.""" + dbc = self._dbh.cursor() + dbc.execute('SELECT destination FROM catchall WHERE gid = %s', + (self._gid,)) + dests = dbc.fetchall() + if dbc.rowcount > 0: + self._dests.extend(DestAddr(dest[0], self._dbh) for dest in dests) + dbc.close() + + def _check_expansion(self, count_new): + """Checks the current expansion limit of the alias.""" + postconf = Postconf(cfg_dget('bin.postconf')) + limit = long(postconf.read('virtual_alias_expansion_limit')) + dcount = len(self._dests) + failed = False + if dcount == limit or dcount + count_new > limit: + failed = True + errmsg = _( +u"""Cannot add %(count_new)i new destination(s) to catchall alias for +domain '%(domain)s'. Currently this alias expands into %(count)i/%(limit)i +recipients. %(count_new)i additional destination(s) will render this alias +unusable. +Hint: Increase Postfix' virtual_alias_expansion_limit""") + elif dcount > limit: + failed = True + errmsg = _( +u"""Cannot add %(count_new)i new destination(s) to catchall alias for +domain '%(domain)s'. This alias already exceeds its expansion limit (%(count)i/%(limit)i). +So its unusable, all messages addressed to this alias will be bounced. +Hint: Delete some destination addresses.""") + if failed: + raise AErr(errmsg % {'domain': self._domain, 'count': dcount, + 'limit': limit, 'count_new': count_new}, + ALIAS_EXCEEDS_EXPANSION_LIMIT) + + def _delete(self, destination=None): + """Deletes a destination from the catchall alias, if ``destination`` + is not ``None``. If ``destination`` is None, the catchall alias with + all its destination addresses will be deleted. + + """ + dbc = self._dbh.cursor() + if not destination: + dbc.execute('DELETE FROM catchall WHERE gid = %s', (self._gid,)) + else: + dbc.execute('DELETE FROM catchall WHERE gid = %s ' + 'AND destination = %s', (self._gid, str(destination))) + if dbc.rowcount > 0: + self._dbh.commit() + dbc.close() + + def __len__(self): + """Returns the number of destinations of the catchall alias.""" + return len(self._dests) + + @property + def domain(self): + """The Alias' domain.""" + return self._domain + + def add_destinations(self, destinations, warnings=None): + """Adds the `EmailAddress`es from *destinations* list to the + destinations of the catchall alias. + + Destinations, that are already assigned to the alias, will be + removed from *destinations*. When done, this method will return + a set with all destinations, that were saved in the database. + """ + destinations = set(destinations) + assert destinations and \ + all(isinstance(dest, EmailAddress) for dest in destinations) + if not warnings is None: + assert isinstance(warnings, list) + duplicates = destinations.intersection(set(self._dests)) + if duplicates: + destinations.difference_update(set(self._dests)) + if not warnings is None: + warnings.extend(duplicates) + if not destinations: + return destinations + self._check_expansion(len(destinations)) + dbc = self._dbh.cursor() + dbc.executemany("INSERT INTO catchall (gid, destination) " + "VALUES (%d, %%s)" % self._gid, + ((str(destination),) for destination in destinations)) + self._dbh.commit() + dbc.close() + self._dests.extend(destinations) + return destinations + + def del_destination(self, destination): + """Deletes the specified ``destination`` address from the catchall + alias.""" + assert isinstance(destination, EmailAddress) + if not self._dests: + raise AErr(_(u"There are no catchall aliases defined for " + u"domain '%s'.") % self._domain, NO_SUCH_ALIAS) + if not destination in self._dests: + raise AErr(_(u"The address '%(addr)s' is not a destination of " + u"the catchall alias for domain '%(domain)s'.") + % {'addr': destination, 'domain': self._domain}, + NO_SUCH_ALIAS) + self._delete(destination) + self._dests.remove(destination) + + def get_destinations(self): + """Returns an iterator for all destinations of the catchall alias.""" + if not self._dests: + raise AErr(_(u"There are no catchall aliases defined for " + u"domain '%s'.") % self._domain, NO_SUCH_ALIAS) + return iter(self._dests) + + def delete(self): + """Deletes all catchall destinations for the domain.""" + if not self._dests: + raise AErr(_(u"There are no catchall aliases defined for " + u"domain '%s'.") % self._domain, NO_SUCH_ALIAS) + self._delete() + del self._dests[:] + +del _, cfg_dget diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/cli/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/cli/__init__.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,100 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2010 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.cli + ~~~~~~~~~~~~~~~~~~~~~~ + + VirtualMailManager's command line interface. +""" + +import os +from array import array +from fcntl import ioctl +from getpass import getpass +from termios import TIOCGWINSZ + +from VirtualMailManager import ENCODING +from VirtualMailManager.constants import VMM_TOO_MANY_FAILURES +from VirtualMailManager.errors import VMMError + + +__all__ = ('prog', 'get_winsize', 'read_pass', 'w_err', 'w_std') + +_ = lambda msg: msg +_std_write = os.sys.stdout.write +_err_write = os.sys.stderr.write +prog = os.path.basename(os.sys.argv[0]) + + +def w_std(*args): + """Writes a line for each arg of *args*, encoded in the current + ENCODING, to stdout. + """ + _std_write('\n'.join(a.encode(ENCODING, 'replace') for a in args) + '\n') + + +def w_err(code, *args): + """Writes a line for each arg of *args*, encoded in the current + ENCODING, to stderr. + This function optionally interrupts the program execution if *code* + does not equal to 0. *code* will be used as the system exit status. + """ + _err_write('\n'.join(a.encode(ENCODING, 'replace') for a in args) + '\n') + if code: + os.sys.exit(code) + + +def get_winsize(): + """Returns a tuple of integers ``(ws_row, ws_col)`` with the height and + width of the terminal.""" + fd = None + for dev in (os.sys.stdout, os.sys.stderr, os.sys.stdin): + if hasattr(dev, 'fileno') and os.isatty(dev.fileno()): + fd = dev.fileno() + break + if fd is None: # everything seems to be redirected + # fall back to environment or assume some common defaults + ws_row, ws_col = 24, 80 + try: + ws_col = int(os.environ.get('COLUMNS', 80)) + ws_row = int(os.environ.get('LINES', 24)) + except ValueError: + pass + return ws_row, ws_col + #"struct winsize" with the ``unsigned short int``s ws_{row,col,{x,y}pixel} + ws = array('H', (0, 0, 0, 0)) + ioctl(fd, TIOCGWINSZ, ws, True) + ws_row, ws_col = ws[:2] + return ws_row, ws_col + + +def read_pass(): + """Interactive 'password chat', returns the password in plain format. + + Throws a VMMError after the third failure. + """ + # TP: Please preserve the trailing space. + readp_msg0 = _(u'Enter new password: ').encode(ENCODING, 'replace') + # TP: Please preserve the trailing space. + readp_msg1 = _(u'Retype new password: ').encode(ENCODING, 'replace') + mismatched = True + failures = 0 + while mismatched: + if failures > 2: + raise VMMError(_(u'Too many failures - try again later.'), + VMM_TOO_MANY_FAILURES) + clear0 = getpass(prompt=readp_msg0) + clear1 = getpass(prompt=readp_msg1) + if clear0 != clear1: + failures += 1 + w_err(0, _(u'Sorry, passwords do not match.')) + continue + if not clear0: + failures += 1 + w_err(0, _(u'Sorry, empty passwords are not permitted.')) + continue + mismatched = False + return clear0 + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/cli/config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/cli/config.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,96 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2010 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.cli.config + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Adds some interactive stuff to the Config class. +""" + +from ConfigParser import RawConfigParser +from shutil import copy2 + +from VirtualMailManager import ENCODING +from VirtualMailManager.config import Config, ConfigValueError, LazyConfig +from VirtualMailManager.errors import ConfigError, VMMError +from VirtualMailManager.cli import w_err, w_std +from VirtualMailManager.constants import CONF_ERROR, VMM_TOO_MANY_FAILURES + +_ = lambda msg: msg + + +class CliConfig(Config): + """Adds the interactive ``configure`` method to the `Config` class + and overwrites `LazyConfig.set(), in order to update a single option + in the configuration file with a single command line command. + """ + + def configure(self, sections): + """Interactive method for configuring all options of the given + iterable ``sections`` object.""" + input_fmt = _(u'Enter new value for option %(option)s ' + u'[%(current_value)s]: ') + failures = 0 + + w_std(_(u'Using configuration file: %s\n') % self._cfg_filename) + for section in sections: + w_std(_(u"* Configuration section: '%s'") % section) + for opt, val in self.items(section): + failures = 0 + while True: + newval = raw_input(input_fmt.encode(ENCODING, 'replace') % + {'option': opt, 'current_value': val}) + if newval and newval != val: + try: + LazyConfig.set(self, '%s.%s' % (section, opt), + newval) + break + except (ValueError, ConfigValueError, VMMError), err: + w_err(0, _(u'Warning: %s') % err) + failures += 1 + if failures > 2: + raise ConfigError(_(u'Too many failures - try ' + u'again later.'), + VMM_TOO_MANY_FAILURES) + else: + break + print + if self._modified: + self._save_changes() + + def set(self, option, value): + """Set the value of an option. + + If the new `value` has been set, the configuration file will be + immediately updated. + + Throws a ``ConfigError`` if `value` couldn't be converted to + ``LazyConfigOption.cls`` or ``LazyConfigOption.validate`` fails.""" + section, option_ = self._get_section_option(option) + try: + val = self._cfg[section][option_].cls(value) + if self._cfg[section][option_].validate: + val = self._cfg[section][option_].validate(val) + except (ValueError, ConfigValueError), err: + raise ConfigError(str(err), CONF_ERROR) + # Do not write default values also skip identical values + if not self._cfg[section][option_].default is None: + old_val = self.dget(option) + else: + old_val = self.pget(option) + if val == old_val: + return + if not RawConfigParser.has_section(self, section): + self.add_section(section) + RawConfigParser.set(self, section, option_, val) + self._save_changes() + + def _save_changes(self): + """Writes changes to the configuration file.""" + copy2(self._cfg_filename, self._cfg_filename + '.bak') + self._cfg_file = open(self._cfg_filename, 'w') + self.write(self._cfg_file) + self._cfg_file.close() + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/cli/handler.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/cli/handler.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,99 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2010 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.cli.handler + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + A derived Handler class with a few changes/additions for cli use. +""" + +import os + +from VirtualMailManager.errors import VMMError +from VirtualMailManager.handler import Handler +from VirtualMailManager.cli import read_pass +from VirtualMailManager.cli.config import CliConfig as Cfg +from VirtualMailManager.constants import ACCOUNT_EXISTS, INVALID_SECTION, \ + NO_SUCH_ACCOUNT, TYPE_ACCOUNT +from VirtualMailManager.password import randompw + +_ = lambda msg: msg + + +class CliHandler(Handler): + """This class uses a `CliConfig` for configuration stuff, instead of + the non-interactive `Config` class. + + It provides the additional methods cfgSet() and configure(). + + Additionally it uses `VirtualMailManager.cli.read_pass()` for for the + interactive password dialog. + """ + + __slots__ = () # nothing additional, also no __dict__/__weakref__ + + def __init__(self): + """Creates a new CliHandler instance. + + Throws a NotRootError if your uid is greater 0. + """ + # Overwrite the parent CTor partly, we use the CliConfig class + # and add some command line checks. + skip_some_checks = os.sys.argv[1] in ('cf', 'configure', 'h', 'help', + 'v', 'version') + super(CliHandler, self).__init__(skip_some_checks) + + self._cfg = Cfg(self._cfg_fname) + self._cfg.load() + + def cfg_set(self, option, value): + """Set a new value for the given option.""" + return self._cfg.set(option, value) + + def configure(self, section=None): + """Starts the 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. + """ + if section is None: + self._cfg.configure(self._cfg.sections()) + elif self._cfg.has_section(section): + self._cfg.configure([section]) + else: + raise VMMError(_(u"Invalid section: '%s'") % section, + INVALID_SECTION) + + def user_add(self, emailaddress, password=None): + """Override the parent user_add() - add the interactive password + dialog. + + Returns the generated password, if account.random_password == True. + """ + acc = self._get_account(emailaddress) + if acc: + raise VMMError(_(u"The account '%s' already exists.") % + acc.address, ACCOUNT_EXISTS) + self._is_other_address(acc.address, TYPE_ACCOUNT) + rand_pass = self._cfg.dget('account.random_password') + if password is None: + password = (read_pass, randompw)[rand_pass]() + acc.set_password(password) + acc.save() + self._make_account_dirs(acc) + return (None, password)[rand_pass] + + def user_password(self, emailaddress, password=None): + """Override the parent user_password() - add the interactive + password dialog.""" + acc = self._get_account(emailaddress) + if not acc: + raise VMMError(_(u"The account '%s' does not exist.") % + acc.address, NO_SUCH_ACCOUNT) + if not isinstance(password, basestring) or not password: + password = read_pass() + acc.modify('password', password) + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/cli/main.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/cli/main.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,80 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2007 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.cli.main + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + VirtualMailManager's command line interface. +""" + +from ConfigParser import NoOptionError, NoSectionError + +from VirtualMailManager import ENCODING, errors +from VirtualMailManager.config import BadOptionError, ConfigValueError +from VirtualMailManager.cli import w_err +from VirtualMailManager.cli.handler import CliHandler +from VirtualMailManager.constants import DATABASE_ERROR, EX_MISSING_ARGS, \ + EX_SUCCESS, EX_UNKNOWN_COMMAND, EX_USER_INTERRUPT, INVALID_ARGUMENT +from VirtualMailManager.cli.subcommands import RunContext, cmd_map, \ + update_cmd_map, usage + + +_ = lambda msg: msg + + +def _get_handler(): + """Try to get a CliHandler. Exit the program when an error occurs.""" + try: + handler = CliHandler() + except (errors.NotRootError, errors.PermissionError, errors.VMMError, + errors.ConfigError), err: + w_err(err.code, _(u'Error: %s') % err.msg) + else: + handler.cfg_install() + return handler + + +def run(argv): + update_cmd_map() + if len(argv) < 2: + usage(EX_MISSING_ARGS, _(u"You must specify a subcommand at least.")) + + sub_cmd = argv[1].lower() + if sub_cmd in cmd_map: + cmd_func = cmd_map[sub_cmd].func + else: + for cmd in cmd_map.itervalues(): + if cmd.alias == sub_cmd: + cmd_func = cmd.func + sub_cmd = cmd.name + break + else: + usage(EX_UNKNOWN_COMMAND, _(u"Unknown subcommand: '%s'") % sub_cmd) + + handler = _get_handler() + run_ctx = RunContext(argv, handler, sub_cmd) + try: + cmd_func(run_ctx) + except (EOFError, KeyboardInterrupt): + # TP: We have to cry, because root has killed/interrupted vmm + # with Ctrl+C or Ctrl+D. + w_err(EX_USER_INTERRUPT, '', _(u'Ouch!'), '') + except errors.VMMError, err: + if err.code != DATABASE_ERROR: + w_err(err.code, _(u'Error: %s') % err.msg) + w_err(err.code, unicode(err.msg, ENCODING, 'replace')) + except (BadOptionError, ConfigValueError), err: + w_err(INVALID_ARGUMENT, _(u'Error: %s') % err) + except NoSectionError, err: + w_err(INVALID_ARGUMENT, + _(u"Error: Unknown section: '%s'") % err.section) + except NoOptionError, err: + w_err(INVALID_ARGUMENT, + _(u"Error: No option '%(option)s' in section: '%(section)s'") % + {'option': err.option, 'section': err.section}) + if handler.has_warnings(): + w_err(0, _(u'Warnings:'), *handler.get_warnings()) + return EX_SUCCESS + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/cli/subcommands.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/cli/subcommands.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,1091 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2007 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.cli.subcommands + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + VirtualMailManager's cli subcommands. +""" + +import locale +import os + +from textwrap import TextWrapper +from time import strftime, strptime + +from VirtualMailManager import ENCODING +from VirtualMailManager.cli import get_winsize, prog, w_err, w_std +from VirtualMailManager.common import human_size, size_in_bytes, \ + version_str, format_domain_default +from VirtualMailManager.constants import __copyright__, __date__, \ + __version__, ACCOUNT_EXISTS, ALIAS_EXISTS, ALIASDOMAIN_ISDOMAIN, \ + DOMAIN_ALIAS_EXISTS, INVALID_ARGUMENT, EX_MISSING_ARGS, \ + RELOCATED_EXISTS, TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED +from VirtualMailManager.errors import VMMError +from VirtualMailManager.password import list_schemes +from VirtualMailManager.serviceset import SERVICES + +__all__ = ( + 'Command', 'RunContext', 'cmd_map', 'usage', 'alias_add', 'alias_delete', + 'alias_info', 'aliasdomain_add', 'aliasdomain_delete', 'aliasdomain_info', + 'aliasdomain_switch', 'catchall_add', 'catchall_info', 'catchall_delete', + 'config_get', 'config_set', 'configure', + 'domain_add', 'domain_delete', 'domain_info', 'domain_quota', + 'domain_services', 'domain_transport', 'domain_note', 'get_user', 'help_', + 'list_domains', 'list_pwschemes', 'list_users', 'list_aliases', + 'list_relocated', 'list_addresses', 'relocated_add', 'relocated_delete', + 'relocated_info', 'user_add', 'user_delete', 'user_info', 'user_name', + 'user_password', 'user_quota', 'user_services', 'user_transport', + 'user_note', 'version', +) + +_ = lambda msg: msg +txt_wrpr = TextWrapper(width=get_winsize()[1] - 1) +cmd_map = {} + + +class Command(object): + """Container class for command information.""" + __slots__ = ('name', 'alias', 'func', 'args', 'descr') + + def __init__(self, name, alias, func, args, descr): + """Create a new Command instance. + + Arguments: + + `name` : str + the command name, e.g. ``addalias`` + `alias` : str + the command's short alias, e.g. ``aa`` + `func` : callable + the function to handle the command + `args` : str + argument placeholders, e.g. ``aliasaddress`` + `descr` : str + short description of the command + """ + self.name = name + self.alias = alias + self.func = func + self.args = args + self.descr = descr + + @property + def usage(self): + """the command's usage info.""" + return u'%s %s %s' % (prog, self.name, self.args) + + +class RunContext(object): + """Contains all information necessary to run a subcommand.""" + __slots__ = ('argc', 'args', 'cget', 'hdlr', 'scmd') + plan_a_b = _(u'Plan A failed ... trying Plan B: %(subcommand)s %(object)s') + + def __init__(self, argv, handler, command): + """Create a new RunContext""" + self.argc = len(argv) + self.args = [unicode(arg, ENCODING) for arg in argv] + self.cget = handler.cfg_dget + self.hdlr = handler + self.scmd = command + + +def alias_add(ctx): + """create a new alias e-mail address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias address and destination.'), + ctx.scmd) + elif ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing destination address.'), ctx.scmd) + ctx.hdlr.alias_add(ctx.args[2].lower(), *ctx.args[3:]) + + +def alias_delete(ctx): + """delete the specified alias e-mail address or one of its destinations""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias address.'), ctx.scmd) + elif ctx.argc < 4: + ctx.hdlr.alias_delete(ctx.args[2].lower()) + else: + ctx.hdlr.alias_delete(ctx.args[2].lower(), ctx.args[3]) + + +def alias_info(ctx): + """show the destination(s) of the specified alias""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias address.'), ctx.scmd) + address = ctx.args[2].lower() + try: + _print_aliase_info(address, ctx.hdlr.alias_info(address)) + except VMMError, err: + if err.code is ACCOUNT_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'userinfo', + 'object': address}) + ctx.scmd = ctx.args[1] = 'userinfo' + user_info(ctx) + elif err.code is RELOCATED_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'relocatedinfo', + 'object': address}) + ctx.scmd = ctx.args[1] = 'relocatedinfo' + relocated_info(ctx) + else: + raise + + +def aliasdomain_add(ctx): + """create a new alias for an existing domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias domain name and destination ' + u'domain name.'), ctx.scmd) + elif ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing destination domain name.'), + ctx.scmd) + ctx.hdlr.aliasdomain_add(ctx.args[2].lower(), ctx.args[3].lower()) + + +def aliasdomain_delete(ctx): + """delete the specified alias domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias domain name.'), ctx.scmd) + ctx.hdlr.aliasdomain_delete(ctx.args[2].lower()) + + +def aliasdomain_info(ctx): + """show the destination of the given alias domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias domain name.'), ctx.scmd) + try: + _print_aliasdomain_info(ctx.hdlr.aliasdomain_info(ctx.args[2].lower())) + except VMMError, err: + if err.code is ALIASDOMAIN_ISDOMAIN: + w_err(0, ctx.plan_a_b % {'subcommand': u'domaininfo', + 'object': ctx.args[2].lower()}) + ctx.scmd = ctx.args[1] = 'domaininfo' + domain_info(ctx) + else: + raise + + +def aliasdomain_switch(ctx): + """assign the given alias domain to an other domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing alias domain name and destination ' + u'domain name.'), ctx.scmd) + elif ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing destination domain name.'), + ctx.scmd) + ctx.hdlr.aliasdomain_switch(ctx.args[2].lower(), ctx.args[3].lower()) + + +def catchall_add(ctx): + """create a new catchall alias e-mail address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain and destination.'), + ctx.scmd) + elif ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing destination address.'), ctx.scmd) + ctx.hdlr.catchall_add(ctx.args[2].lower(), *ctx.args[3:]) + + +def catchall_delete(ctx): + """delete the specified destination or all of the catchall destination""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain.'), ctx.scmd) + elif ctx.argc < 4: + ctx.hdlr.catchall_delete(ctx.args[2].lower()) + else: + ctx.hdlr.catchall_delete(ctx.args[2].lower(), ctx.args[3]) + + +def catchall_info(ctx): + """show the catchall destination(s) of the specified domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain.'), ctx.scmd) + address = ctx.args[2].lower() + _print_catchall_info(address, ctx.hdlr.catchall_info(address)) + + +def config_get(ctx): + """show the actual value of the configuration option""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u"Missing option name."), ctx.scmd) + + noop = lambda option: option + opt_formater = { + 'misc.dovecot_version': version_str, + 'domain.quota_bytes': human_size, + } + + option = ctx.args[2].lower() + w_std('%s = %s' % (option, opt_formater.get(option, + noop)(ctx.cget(option)))) + + +def config_set(ctx): + """set a new value for the configuration option""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing option and new value.'), ctx.scmd) + if ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing new configuration value.'), + ctx.scmd) + ctx.hdlr.cfg_set(ctx.args[2].lower(), ctx.args[3]) + + +def configure(ctx): + """start interactive configuration modus""" + if ctx.argc < 3: + ctx.hdlr.configure() + else: + ctx.hdlr.configure(ctx.args[2].lower()) + + +def domain_add(ctx): + """create a new domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name.'), ctx.scmd) + elif ctx.argc < 4: + ctx.hdlr.domain_add(ctx.args[2].lower()) + else: + ctx.hdlr.domain_add(ctx.args[2].lower(), ctx.args[3]) + if ctx.cget('domain.auto_postmaster'): + w_std(_(u'Creating account for postmaster@%s') % ctx.args[2].lower()) + ctx.scmd = 'useradd' + ctx.args = [prog, ctx.scmd, u'postmaster@' + ctx.args[2].lower()] + ctx.argc = 3 + user_add(ctx) + + +def domain_delete(ctx): + """delete the given domain and all its alias domains""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name.'), ctx.scmd) + elif ctx.argc < 4: + ctx.hdlr.domain_delete(ctx.args[2].lower()) + elif ctx.args[3].lower() == 'force': + ctx.hdlr.domain_delete(ctx.args[2].lower(), True) + else: + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % ctx.args[3], + ctx.scmd) + + +def domain_info(ctx): + """display information about the given domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name.'), ctx.scmd) + if ctx.argc < 4: + details = None + else: + details = ctx.args[3].lower() + if details not in ('accounts', 'aliasdomains', 'aliases', 'full', + 'relocated', 'catchall'): + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % details, + ctx.scmd) + try: + info = ctx.hdlr.domain_info(ctx.args[2].lower(), details) + except VMMError, err: + if err.code is DOMAIN_ALIAS_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'aliasdomaininfo', + 'object': ctx.args[2].lower()}) + ctx.scmd = ctx.args[1] = 'aliasdomaininfo' + aliasdomain_info(ctx) + else: + raise + else: + q_limit = u'Storage: %(bytes)s; Messages: %(messages)s' + if not details: + info['bytes'] = human_size(info['bytes']) + info['messages'] = locale.format('%d', info['messages'], True) + info['quota limit/user'] = q_limit % info + _print_info(ctx, info, _(u'Domain')) + else: + info[0]['bytes'] = human_size(info[0]['bytes']) + info[0]['messages'] = locale.format('%d', info[0]['messages'], + True) + info[0]['quota limit/user'] = q_limit % info[0] + _print_info(ctx, info[0], _(u'Domain')) + if details == u'accounts': + _print_list(info[1], _(u'accounts')) + elif details == u'aliasdomains': + _print_list(info[1], _(u'alias domains')) + elif details == u'aliases': + _print_list(info[1], _(u'aliases')) + elif details == u'relocated': + _print_list(info[1], _(u'relocated users')) + elif details == u'catchall': + _print_list(info[1], _(u'catch-all destinations')) + else: + _print_list(info[1], _(u'alias domains')) + _print_list(info[2], _(u'accounts')) + _print_list(info[3], _(u'aliases')) + _print_list(info[4], _(u'relocated users')) + _print_list(info[5], _(u'catch-all destinations')) + + +def domain_quota(ctx): + """update the quota limit of the specified domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name and storage value.'), + ctx.scmd) + if ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing storage value.'), ctx.scmd) + messages = 0 + force = None + try: + bytes_ = size_in_bytes(ctx.args[3]) + except (ValueError, TypeError): + usage(INVALID_ARGUMENT, _(u"Invalid storage value: '%s'") % + ctx.args[3], ctx.scmd) + if ctx.argc < 5: + pass + elif ctx.argc < 6: + try: + messages = int(ctx.args[4]) + except ValueError: + if ctx.args[4].lower() != 'force': + usage(INVALID_ARGUMENT, + _(u"Neither a valid number of messages nor the keyword " + u"'force': '%s'") % ctx.args[4], ctx.scmd) + force = 'force' + else: + try: + messages = int(ctx.args[4]) + except ValueError: + usage(INVALID_ARGUMENT, + _(u"Not a valid number of messages: '%s'") % ctx.args[4], + ctx.scmd) + if ctx.args[5].lower() != 'force': + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % ctx.args[5], + ctx.scmd) + force = 'force' + ctx.hdlr.domain_quotalimit(ctx.args[2].lower(), bytes_, messages, force) + + +def domain_services(ctx): + """allow all named service and block the uncredited.""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name.'), ctx.scmd) + services = [] + force = False + if ctx.argc is 3: + pass + elif ctx.argc is 4: + arg = ctx.args[3].lower() + if arg in SERVICES: + services.append(arg) + elif arg == 'force': + force = True + else: + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % arg, + ctx.scmd) + else: + services.extend([service.lower() for service in ctx.args[3:-1]]) + arg = ctx.args[-1].lower() + if arg == 'force': + force = True + else: + services.append(arg) + unknown = [service for service in services if service not in SERVICES] + if unknown: + usage(INVALID_ARGUMENT, _(u'Invalid service arguments: %s') % + ' '.join(unknown), ctx.scmd) + ctx.hdlr.domain_services(ctx.args[2].lower(), (None, 'force')[force], + *services) + + +def domain_transport(ctx): + """update the transport of the specified domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name and new transport.'), + ctx.scmd) + if ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing new transport.'), ctx.scmd) + if ctx.argc < 5: + ctx.hdlr.domain_transport(ctx.args[2].lower(), ctx.args[3]) + else: + force = ctx.args[4].lower() + if force != 'force': + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % force, + ctx.scmd) + ctx.hdlr.domain_transport(ctx.args[2].lower(), ctx.args[3], force) + + +def domain_note(ctx): + """update the note of the given domain""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing domain name.'), + ctx.scmd) + elif ctx.argc < 4: + note = None + else: + note = ' '.join(ctx.args[3:]) + ctx.hdlr.domain_note(ctx.args[2].lower(), note) + + +def get_user(ctx): + """get the address of the user with the given UID""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing UID.'), ctx.scmd) + _print_info(ctx, ctx.hdlr.user_by_uid(ctx.args[2]), _(u'Account')) + + +def help_(ctx): + """print help messages.""" + if ctx.argc > 2: + hlptpc = ctx.args[2].lower() + if hlptpc in cmd_map: + topic = hlptpc + else: + for scmd in cmd_map.itervalues(): + if scmd.alias == hlptpc: + topic = scmd.name + break + else: + usage(INVALID_ARGUMENT, _(u"Unknown help topic: '%s'") % + ctx.args[2], ctx.scmd) + # FIXME + w_err(1, "'help %s' not yet implemented." % topic, 'see also: vmm(1)') + + old_ii = txt_wrpr.initial_indent + old_si = txt_wrpr.subsequent_indent + txt_wrpr.initial_indent = ' ' + # len(max(_overview.iterkeys(), key=len)) #Py25 + txt_wrpr.subsequent_indent = 20 * ' ' + order = cmd_map.keys() + order.sort() + + w_std(_(u'List of available subcommands:') + '\n') + for key in order: + w_std('\n'.join(txt_wrpr.wrap('%-18s %s' % (key, cmd_map[key].descr)))) + + txt_wrpr.initial_indent = old_ii + txt_wrpr.subsequent_indent = old_si + txt_wrpr.initial_indent = '' + + +def list_domains(ctx): + """list all domains / search domains by pattern""" + matching = ctx.argc > 2 + if matching: + gids, domains = ctx.hdlr.domain_list(ctx.args[2].lower()) + else: + gids, domains = ctx.hdlr.domain_list() + _print_domain_list(gids, domains, matching) + + +def list_pwschemes(ctx_unused): + """Prints all usable password schemes and password encoding suffixes.""" + # TODO: Remove trailing colons from keys. + # For now it is to late, the translators has stared their work + keys = (_(u'Usable password schemes:'), _(u'Usable encoding suffixes:')) + old_ii, old_si = txt_wrpr.initial_indent, txt_wrpr.subsequent_indent + txt_wrpr.initial_indent = txt_wrpr.subsequent_indent = '\t' + txt_wrpr.width = txt_wrpr.width - 8 + + for key, value in zip(keys, list_schemes()): + if key.endswith(':'): # who knows … (see TODO above) + #key = key.rpartition(':')[0] + key = key[:-1] # This one is for Py24 + w_std(key, len(key) * '-') + w_std('\n'.join(txt_wrpr.wrap(' '.join(value))), '') + + txt_wrpr.initial_indent, txt_wrpr.subsequent_indent = old_ii, old_si + txt_wrpr.width = txt_wrpr.width + 8 + + +def list_addresses(ctx, limit=None): + """List all addresses / search addresses by pattern. The output can be + limited with TYPE_ACCOUNT, TYPE_ALIAS and TYPE_RELOCATED, which can be + bitwise ORed as a combination. Not specifying a limit is the same as + combining all three.""" + if limit is None: + limit = TYPE_ACCOUNT | TYPE_ALIAS | TYPE_RELOCATED + matching = ctx.argc > 2 + if matching: + gids, addresses = ctx.hdlr.address_list(limit, ctx.args[2].lower()) + else: + gids, addresses = ctx.hdlr.address_list(limit) + _print_address_list(limit, gids, addresses, matching) + + +def list_users(ctx): + """list all user accounts / search user accounts by pattern""" + return list_addresses(ctx, TYPE_ACCOUNT) + +def list_aliases(ctx): + """list all aliases / search aliases by pattern""" + return list_addresses(ctx, TYPE_ALIAS) + +def list_relocated(ctx): + """list all relocated records / search relocated records by pattern""" + return list_addresses(ctx, TYPE_RELOCATED) + + +def relocated_add(ctx): + """create a new record for a relocated user""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, + _(u'Missing relocated address and destination.'), ctx.scmd) + elif ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing destination address.'), ctx.scmd) + ctx.hdlr.relocated_add(ctx.args[2].lower(), ctx.args[3]) + + +def relocated_delete(ctx): + """delete the record of the relocated user""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing relocated address.'), ctx.scmd) + ctx.hdlr.relocated_delete(ctx.args[2].lower()) + + +def relocated_info(ctx): + """print information about a relocated user""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing relocated address.'), ctx.scmd) + relocated = ctx.args[2].lower() + try: + _print_relocated_info(addr=relocated, + dest=ctx.hdlr.relocated_info(relocated)) + except VMMError, err: + if err.code is ACCOUNT_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'userinfo', + 'object': relocated}) + ctx.scmd = ctx.args[1] = 'userinfoi' + user_info(ctx) + elif err.code is ALIAS_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'aliasinfo', + 'object': relocated}) + ctx.scmd = ctx.args[1] = 'aliasinfo' + alias_info(ctx) + else: + raise + + +def user_add(ctx): + """create a new e-mail user with the given address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address.'), ctx.scmd) + elif ctx.argc < 4: + password = None + else: + password = ctx.args[3] + gen_pass = ctx.hdlr.user_add(ctx.args[2].lower(), password) + if ctx.argc < 4 and gen_pass: + w_std(_(u"Generated password: %s") % gen_pass) + + +def user_delete(ctx): + """delete the specified user""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address.'), ctx.scmd) + elif ctx.argc < 4: + ctx.hdlr.user_delete(ctx.args[2].lower()) + elif ctx.args[3].lower() == 'force': + ctx.hdlr.user_delete(ctx.args[2].lower(), True) + else: + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % ctx.args[3], + ctx.scmd) + + +def user_info(ctx): + """display information about the given address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address.'), ctx.scmd) + if ctx.argc < 4: + details = None + else: + details = ctx.args[3].lower() + if details not in ('aliases', 'du', 'full'): + usage(INVALID_ARGUMENT, _(u"Invalid argument: '%s'") % details, + ctx.scmd) + try: + info = ctx.hdlr.user_info(ctx.args[2].lower(), details) + except VMMError, err: + if err.code is ALIAS_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'aliasinfo', + 'object': ctx.args[2].lower()}) + ctx.scmd = ctx.args[1] = 'aliasinfo' + alias_info(ctx) + elif err.code is RELOCATED_EXISTS: + w_err(0, ctx.plan_a_b % {'subcommand': u'relocatedinfo', + 'object': ctx.args[2].lower()}) + ctx.scmd = ctx.args[1] = 'relocatedinfo' + relocated_info(ctx) + else: + raise + else: + if details in (None, 'du'): + info['quota storage'] = _format_quota_usage(info['ql_bytes'], + info['uq_bytes'], True, info['ql_domaindefault']) + info['quota messages'] = _format_quota_usage(info['ql_messages'], + info['uq_messages'], domaindefault=info['ql_domaindefault']) + _print_info(ctx, info, _(u'Account')) + else: + info[0]['quota storage'] = _format_quota_usage(info[0]['ql_bytes'], + info[0]['uq_bytes'], True, info[0]['ql_domaindefault']) + info[0]['quota messages'] = \ + _format_quota_usage(info[0]['ql_messages'], + info[0]['uq_messages'], + domaindefault=info[0]['ql_domaindefault']) + _print_info(ctx, info[0], _(u'Account')) + _print_list(info[1], _(u'alias addresses')) + + +def user_name(ctx): + """set or update the real name for an address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u"Missing e-mail address and user's name."), + ctx.scmd) + elif ctx.argc < 4: + name = None + else: + name = ctx.args[3] + ctx.hdlr.user_name(ctx.args[2].lower(), name) + + +def user_password(ctx): + """update the password for the given address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address.'), ctx.scmd) + elif ctx.argc < 4: + password = None + else: + password = ctx.args[3] + ctx.hdlr.user_password(ctx.args[2].lower(), password) + + +def user_note(ctx): + """update the note of the given address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address.'), + ctx.scmd) + elif ctx.argc < 4: + note = None + else: + note = ' '.join(ctx.args[3:]) + ctx.hdlr.user_note(ctx.args[2].lower(), note) + + +def user_quota(ctx): + """update the quota limit for the given address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address and storage value.'), + ctx.scmd) + elif ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing storage value.'), ctx.scmd) + if ctx.args[3] != 'domain': + try: + bytes_ = size_in_bytes(ctx.args[3]) + except (ValueError, TypeError): + usage(INVALID_ARGUMENT, _(u"Invalid storage value: '%s'") % + ctx.args[3], ctx.scmd) + else: + bytes_ = ctx.args[3] + if ctx.argc < 5: + messages = 0 + else: + try: + messages = int(ctx.args[4]) + except ValueError: + usage(INVALID_ARGUMENT, + _(u"Not a valid number of messages: '%s'") % ctx.args[4], + ctx.scmd) + ctx.hdlr.user_quotalimit(ctx.args[2].lower(), bytes_, messages) + + +def user_services(ctx): + """allow all named service and block the uncredited.""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address.'), ctx.scmd) + services = [] + if ctx.argc >= 4: + services.extend([service.lower() for service in ctx.args[3:]]) + unknown = [service for service in services if service not in SERVICES] + if unknown and ctx.args[3] != 'domain': + usage(INVALID_ARGUMENT, _(u'Invalid service arguments: %s') % + ' '.join(unknown), ctx.scmd) + ctx.hdlr.user_services(ctx.args[2].lower(), *services) + + +def user_transport(ctx): + """update the transport of the given address""" + if ctx.argc < 3: + usage(EX_MISSING_ARGS, _(u'Missing e-mail address and transport.'), + ctx.scmd) + if ctx.argc < 4: + usage(EX_MISSING_ARGS, _(u'Missing transport.'), ctx.scmd) + ctx.hdlr.user_transport(ctx.args[2].lower(), ctx.args[3]) + + +def usage(errno, errmsg, subcommand=None): + """print usage message for the given command or all commands. + When errno > 0, sys,exit(errno) will interrupt the program. + """ + if subcommand and subcommand in cmd_map: + w_err(errno, _(u"Error: %s") % errmsg, + _(u"usage: ") + cmd_map[subcommand].usage) + + # TP: Please adjust translated words like the original text. + # (It's a table header.) Extract from usage text: + # usage: vmm subcommand arguments + # short long + # subcommand arguments + # + # da domainadd fqdn [transport] + # dd domaindelete fqdn [force] + u_head = _(u"""usage: %s subcommand arguments + short long + subcommand arguments\n""") % prog + order = cmd_map.keys() + order.sort() + w_err(0, u_head) + for key in order: + scmd = cmd_map[key] + w_err(0, ' %-5s %-19s %s' % (scmd.alias, scmd.name, scmd.args)) + w_err(errno, '', _(u"Error: %s") % errmsg) + + +def version(ctx_unused): + """Write version and copyright information to stdout.""" + w_std('%s, %s %s (%s %s)\nPython %s %s %s\n\n%s\n%s %s' % (prog, + # TP: The words 'from', 'version' and 'on' are used in + # the version information, e.g.: + # vmm, version 0.5.2 (from 09/09/09) + # Python 2.5.4 on FreeBSD + _(u'version'), __version__, _(u'from'), + strftime(locale.nl_langinfo(locale.D_FMT), + strptime(__date__, '%Y-%m-%d')).decode(ENCODING, 'replace'), + os.sys.version.split()[0], _(u'on'), os.uname()[0], + __copyright__, prog, + _(u'is free software and comes with ABSOLUTELY NO WARRANTY.'))) + + +def update_cmd_map(): + """Update the cmd_map, after gettext's _ was installed.""" + cmd = Command + cmd_map.update({ + # Account commands + 'getuser': cmd('getuser', 'gu', get_user, 'uid', + _(u'get the address of the user with the given UID')), + 'useradd': cmd('useradd', 'ua', user_add, 'address [password]', + _(u'create a new e-mail user with the given address')), + 'userdelete': cmd('userdelete', 'ud', user_delete, 'address [force]', + _(u'delete the specified user')), + 'userinfo': cmd('userinfo', 'ui', user_info, 'address [details]', + _(u'display information about the given address')), + 'username': cmd('username', 'un', user_name, 'address name', + _(u'set or update the real name for an address')), + 'userpassword': cmd('userpassword', 'up', user_password, + 'address [password]', + _(u'update the password for the given address')), + 'userquota': cmd('userquota', 'uq', user_quota, + 'address storage [messages] | address domain', + _(u'update the quota limit for the given address')), + 'userservices': cmd('userservices', 'us', user_services, + 'address [service ...] | address domain', + _(u'enables the specified services and disables all ' + u'not specified services')), + 'usertransport': cmd('usertransport', 'ut', user_transport, + 'address transport | address domain', + _(u'update the transport of the given address')), + 'usernote': cmd('usernote', 'uo', user_note, + 'address note', + _(u'update the note of the given address')), + # Alias commands + 'aliasadd': cmd('aliasadd', 'aa', alias_add, 'address destination ...', + _(u'create a new alias e-mail address with one or more ' + u'destinations')), + 'aliasdelete': cmd('aliasdelete', 'ad', alias_delete, + 'address [destination]', + _(u'delete the specified alias e-mail address or one ' + u'of its destinations')), + 'aliasinfo': cmd('aliasinfo', 'ai', alias_info, 'address', + _(u'show the destination(s) of the specified alias')), + # AliasDomain commands + 'aliasdomainadd': cmd('aliasdomainadd', 'ada', aliasdomain_add, + 'fqdn destination', + _(u'create a new alias for an existing domain')), + 'aliasdomaindelete': cmd('aliasdomaindelete', 'add', aliasdomain_delete, + 'fqdn', _(u'delete the specified alias domain')), + 'aliasdomaininfo': cmd('aliasdomaininfo', 'adi', aliasdomain_info, 'fqdn', + _(u'show the destination of the given alias domain')), + 'aliasdomainswitch': cmd('aliasdomainswitch', 'ads', aliasdomain_switch, + 'fqdn destination', _(u'assign the given alias ' + 'domain to an other domain')), + # CatchallAlias commands + 'catchalladd': cmd('catchalladd', 'caa', catchall_add, + 'fqdn destination ...', + _(u'add one or more catch-all destinations for a ' + u'domain')), + 'catchalldelete': cmd('catchalldelete', 'cad', catchall_delete, + 'fqdn [destination]', + _(u'delete the specified catch-all destination or all ' + u'of a domain\'s destinations')), + 'catchallinfo': cmd('catchallinfo', 'cai', catchall_info, 'fqdn', + _(u'show the catch-all destination(s) of the specified domain')), + # Domain commands + 'domainadd': cmd('domainadd', 'da', domain_add, 'fqdn [transport]', + _(u'create a new domain')), + 'domaindelete': cmd('domaindelete', 'dd', domain_delete, 'fqdn [force]', + _(u'delete the given domain and all its alias domains')), + 'domaininfo': cmd('domaininfo', 'di', domain_info, 'fqdn [details]', + _(u'display information about the given domain')), + 'domainquota': cmd('domainquota', 'dq', domain_quota, + 'fqdn storage [messages] [force]', + _(u'update the quota limit of the specified domain')), + 'domainservices': cmd('domainservices', 'ds', domain_services, + 'fqdn [service ...] [force]', + _(u'enables the specified services and disables all ' + u'not specified services of the given domain')), + 'domaintransport': cmd('domaintransport', 'dt', domain_transport, + 'fqdn transport [force]', + _(u'update the transport of the specified domain')), + 'domainnote': cmd('domainnote', 'do', domain_note, + 'fqdn note', + _(u'update the note of the given domain')), + # List commands + 'listdomains': cmd('listdomains', 'ld', list_domains, '[pattern]', + _(u'list all domains or search for domains by pattern')), + 'listaddresses': cmd('listaddresses', 'll', list_addresses, '[pattern]', + _(u'list all addresses or search for addresses by pattern')), + 'listusers': cmd('listusers', 'lu', list_users, '[pattern]', + _(u'list all user accounts or search for accounts by pattern')), + 'listaliases': cmd('listaliases', 'la', list_aliases, '[pattern]', + _(u'list all aliases or search for aliases by pattern')), + 'listrelocated': cmd('listrelocated', 'lr', list_relocated, '[pattern]', + _(u'list all relocated entries or search for entries by pattern')), + # Relocated commands + 'relocatedadd': cmd('relocatedadd', 'ra', relocated_add, + 'address newaddress', + _(u'create a new record for a relocated user')), + 'relocateddelete': cmd('relocateddelete', 'rd', relocated_delete, + 'address', + _(u'delete the record of the relocated user')), + 'relocatedinfo': cmd('relocatedinfo', 'ri', relocated_info, 'address', + _(u'print information about a relocated user')), + # cli commands + 'configget': cmd('configget', 'cg', config_get, 'option', + _('show the actual value of the configuration option')), + 'configset': cmd('configset', 'cs', config_set, 'option value', + _('set a new value for the configuration option')), + 'configure': cmd('configure', 'cf', configure, '[section]', + _(u'start interactive configuration modus')), + 'listpwschemes': cmd('listpwschemes', 'lp', list_pwschemes, '', + _(u'lists all usable password schemes and password ' + u'encoding suffixes')), + 'help': cmd('help', 'h', help_, '[subcommand]', + _(u'show a help overview or help for the given subcommand')), + 'version': cmd('version', 'v', version, '', + _(u'show version and copyright information')), + }) + + +def _get_order(ctx): + """returns a tuple with (key, 1||0) tuples. Used by functions, which + get a dict from the handler.""" + order = () + if ctx.scmd == 'domaininfo': + order = ((u'domain name', 0), (u'gid', 1), (u'domain directory', 0), + (u'quota limit/user', 0), (u'active services', 0), + (u'transport', 0), (u'alias domains', 0), (u'accounts', 0), + (u'aliases', 0), (u'relocated', 0), (u'catch-all dests', 0)) + elif ctx.scmd == 'userinfo': + if ctx.argc == 4 and ctx.args[3] != u'aliases' or \ + ctx.cget('account.disk_usage'): + order = ((u'address', 0), (u'name', 0), (u'uid', 1), (u'gid', 1), + (u'home', 0), (u'mail_location', 0), + (u'quota storage', 0), (u'quota messages', 0), + (u'disk usage', 0), (u'transport', 0), (u'smtp', 1), + (u'pop3', 1), (u'imap', 1), (u'sieve', 1)) + else: + order = ((u'address', 0), (u'name', 0), (u'uid', 1), (u'gid', 1), + (u'home', 0), (u'mail_location', 0), + (u'quota storage', 0), (u'quota messages', 0), + (u'transport', 0), (u'smtp', 1), (u'pop3', 1), + (u'imap', 1), (u'sieve', 1)) + elif ctx.scmd == 'getuser': + order = ((u'uid', 1), (u'gid', 1), (u'address', 0)) + return order + + +def _format_quota_usage(limit, used, human=False, domaindefault=False): + """Put quota's limit / usage / percentage in a formatted string.""" + if human: + q_usage = { + 'used': human_size(used), + 'limit': human_size(limit), + } + else: + q_usage = { + 'used': locale.format('%d', used, True), + 'limit': locale.format('%d', limit, True), + } + if limit: + q_usage['percent'] = locale.format('%6.2f', 100. / limit * used, True) + else: + q_usage['percent'] = locale.format('%6.2f', 0, True) + # Py25: fmt = format_domain_default if domaindefault else lambda s: s + if domaindefault: + fmt = format_domain_default + else: + fmt = lambda s: s + return fmt(_(u'[%(percent)s%%] %(used)s/%(limit)s') % q_usage) + + +def _print_info(ctx, info, title): + """Print info dicts.""" + # TP: used in e.g. 'Domain information' or 'Account information' + msg = u'%s %s' % (title, _(u'information')) + w_std(msg, u'-' * len(msg)) + for key, upper in _get_order(ctx): + if upper: + w_std(u'\t%s: %s' % (key.upper().ljust(17, u'.'), info[key])) + else: + w_std(u'\t%s: %s' % (key.title().ljust(17, u'.'), info[key])) + print + note = info.get('note', None) + if note is not None: + _print_note(note) + +def _print_note(note): + msg = _(u'Note') + w_std(msg, u'-' * len(msg)) + old_ii = txt_wrpr.initial_indent + old_si = txt_wrpr.subsequent_indent + txt_wrpr.initial_indent = txt_wrpr.subsequent_indent = '\t' + txt_wrpr.width -= 8 + for para in note.split('\n'): + w_std(txt_wrpr.fill(para)) + txt_wrpr.width += 8 + txt_wrpr.subsequent_indent = old_si + txt_wrpr.initial_indent = old_ii + +def _print_list(alist, title): + """Print a list.""" + # TP: used in e.g. 'Existing alias addresses' or 'Existing accounts' + msg = u'%s %s' % (_(u'Existing'), title) + w_std(msg, u'-' * len(msg)) + if alist: + if title != _(u'alias domains'): + w_std(*(u'\t%s' % item for item in alist)) + else: + for domain in alist: + if not domain.startswith('xn--'): + w_std(u'\t%s' % domain) + else: + w_std(u'\t%s (%s)' % (domain, domain.decode('idna'))) + print + else: + w_std(_(u'\tNone'), '') + + +def _print_aliase_info(alias, destinations): + """Print the alias address and all its destinations""" + title = _(u'Alias information') + w_std(title, u'-' * len(title)) + w_std(_(u'\tMail for %s will be redirected to:') % alias) + w_std(*(u'\t * %s' % dest for dest in destinations)) + print + + +def _print_catchall_info(domain, destinations): + """Print the catchall destinations of a domain""" + title = _(u'Catch-all information') + w_std(title, u'-' * len(title)) + w_std(_(u'\tMail to unknown localparts in domain %s will be sent to:') + % domain) + w_std(*(u'\t * %s' % dest for dest in destinations)) + print + + +def _print_relocated_info(**kwargs): + """Print the old and new addresses of a relocated user.""" + title = _(u'Relocated information') + w_std(title, u'-' * len(title)) + w_std(_(u"\tUser '%(addr)s' has moved to '%(dest)s'") % kwargs, '') + + +def _format_domain(domain, main=True): + """format (prefix/convert) the domain name.""" + if domain.startswith('xn--'): + domain = u'%s (%s)' % (domain, domain.decode('idna')) + if main: + return u'\t[+] %s' % domain + return u'\t[-] %s' % domain + + +def _print_domain_list(dids, domains, matching): + """Print a list of (matching) domains/alias domains.""" + if matching: + title = _(u'Matching domains') + else: + title = _(u'Existing domains') + w_std(title, '-' * len(title)) + if domains: + for did in dids: + if domains[did][0] is not None: + w_std(_format_domain(domains[did][0])) + if len(domains[did]) > 1: + w_std(*(_format_domain(a, False) for a in domains[did][1:])) + else: + w_std(_('\tNone')) + print + + +def _print_address_list(which, dids, addresses, matching): + """Print a list of (matching) addresses.""" + _trans = { TYPE_ACCOUNT : _('user accounts') + , TYPE_ALIAS : _('aliases') + , TYPE_RELOCATED : _('relocated entries') + , TYPE_ACCOUNT | TYPE_ALIAS + : _('user accounts and aliases') + , TYPE_ACCOUNT | TYPE_RELOCATED + : _('user accounts and relocated entries') + , TYPE_ALIAS | TYPE_RELOCATED + : _('aliases and relocated entries') + , TYPE_ACCOUNT | TYPE_ALIAS | TYPE_RELOCATED : _('addresses') + } + try: + if matching: + title = _(u'Matching %s') % _trans[which] + else: + title = _(u'Existing %s') % _trans[which] + w_std(title, '-' * len(title)) + except KeyError: + raise VMMError(_("Invalid address type for list: '%s'") % which, + INVALID_ARGUMENT) + if addresses: + if which & (which - 1) == 0: + # only one type is requested, so no type indicator + _trans = { TYPE_ACCOUNT : _('') + , TYPE_ALIAS : _('') + , TYPE_RELOCATED : _('') + } + else: + _trans = { TYPE_ACCOUNT : _('u') + , TYPE_ALIAS : _('a') + , TYPE_RELOCATED : _('r') + } + for did in dids: + for addr, atype, aliasdomain in addresses[did]: + if aliasdomain: + leader = '[%s-]' % _trans[atype] + else: + leader = '[%s+]' % _trans[atype] + w_std('\t%s %s' % (leader, addr)) + else: + w_std(_('\tNone')) + print + + +def _print_aliasdomain_info(info): + """Print alias domain information.""" + title = _(u'Alias domain information') + for key in ('alias', 'domain'): + if info[key].startswith('xn--'): + info[key] = u'%s (%s)' % (info[key], info[key].decode('idna')) + w_std(title, '-' * len(title), + _('\tThe alias domain %(alias)s belongs to:\n\t * %(domain)s') % + info, '') + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/common.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/common.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,262 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2010 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.common + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + Some common functions +""" + +import locale +import os +import re +import stat + +from VirtualMailManager import ENCODING +from VirtualMailManager.constants import NOT_EXECUTABLE, NO_SUCH_BINARY, \ + TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED +from VirtualMailManager.errors import VMMError + +VERSION_RE = re.compile(r'^(\d+)\.(\d+)\.(?:(\d+)|(alpha|beta|rc)(\d+))$') + +_version_level = dict(alpha=0xA, beta=0xB, rc=0xC) +_version_cache = {} +_ = lambda msg: msg + + +def expand_path(path): + """Expands paths, starting with ``.`` or ``~``, to an absolute path.""" + if path.startswith('.'): + return os.path.abspath(path) + if path.startswith('~'): + return os.path.expanduser(path) + return path + + +def get_unicode(string): + """Converts `string` to `unicode`, if necessary.""" + if isinstance(string, unicode): + return string + return unicode(string, ENCODING, 'replace') + + +def lisdir(path): + """Checks if `path` is a directory. Doesn't follow symbolic links. + Returns bool. + """ + try: + lstat = os.lstat(path) + except OSError: + return False + return stat.S_ISDIR(lstat.st_mode) + + +def exec_ok(binary): + """Checks if the `binary` exists and if it is executable. + + Throws a `VMMError` if the `binary` isn't a file or is not + executable. + """ + binary = expand_path(binary) + if not os.path.isfile(binary): + raise VMMError(_(u"No such file: '%s'") % get_unicode(binary), + NO_SUCH_BINARY) + if not os.access(binary, os.X_OK): + raise VMMError(_(u"File is not executable: '%s'") % + get_unicode(binary), NOT_EXECUTABLE) + return binary + + +def human_size(size): + """Converts the `size` in bytes in human readable format.""" + if not isinstance(size, (long, int)): + try: + size = long(size) + except ValueError: + raise TypeError("'size' must be a positive long or int.") + if size < 0: + raise ValueError("'size' must be a positive long or int.") + if size < 1024: + return str(size) + prefix_multiply = ((_(u'TiB'), 1 << 40), (_(u'GiB'), 1 << 30), + (_(u'MiB'), 1 << 20), (_(u'KiB'), 1 << 10)) + for prefix, multiply in prefix_multiply: + if size >= multiply: + # TP: e.g.: '%(size)s %(prefix)s' -> '118.30 MiB' + return _(u'%(size)s %(prefix)s') % { + 'size': locale.format('%.2f', float(size) / multiply, + True), + 'prefix': prefix} + + +def size_in_bytes(size): + """Converts the string `size` to a long (size in bytes). + + The string `size` can be suffixed with *b* (bytes), *k* (kilobytes), + *M* (megabytes) or *G* (gigabytes). + """ + if not isinstance(size, basestring) or not size: + raise TypeError('size must be a non empty string.') + if size[-1].upper() in ('B', 'K', 'M', 'G'): + try: + num = int(size[:-1]) + except ValueError: + raise ValueError('Not a valid integer value: %r' % size[:-1]) + unit = size[-1].upper() + if unit == 'B': + return num + elif unit == 'K': + return num << 10L + elif unit == 'M': + return num << 20L + else: + return num << 30L + else: + try: + num = int(size) + except ValueError: + raise ValueError('Not a valid size value: %r' % size) + return num + + +def version_hex(version_string): + """Converts a Dovecot version, e.g.: '1.2.3' or '2.0.beta4', to an int. + Raises a `ValueError` if the *version_string* has the wrong™ format. + + version_hex('1.2.3') -> 270548736 + hex(version_hex('1.2.3')) -> '0x10203f00' + """ + global _version_cache + if version_string in _version_cache: + return _version_cache[version_string] + version = 0 + version_mo = VERSION_RE.match(version_string) + if not version_mo: + raise ValueError('Invalid version string: %r' % version_string) + major, minor, patch, level, serial = version_mo.groups() + major = int(major) + minor = int(minor) + if patch: + patch = int(patch) + if serial: + serial = int(serial) + + if major > 0xFF or minor > 0xFF or \ + patch and patch > 0xFF or serial and serial > 0xFF: + raise ValueError('Invalid version string: %r' % version_string) + + version += major << 28 + version += minor << 20 + if patch: + version += patch << 12 + version += _version_level.get(level, 0xF) << 8 + if serial: + version += serial + + _version_cache[version_string] = version + return version + + +def version_str(version): + """Converts a Dovecot version previously converted with version_hex back to + a string. + Raises a `TypeError` if *version* is not an int/long. + Raises a `ValueError` if *version* is an incorrect int version. + """ + global _version_cache + if version in _version_cache: + return _version_cache[version] + if not isinstance(version, (int, long)): + raise TypeError('Argument is not a int/long: %r', version) + major = (version >> 28) & 0xFF + minor = (version >> 20) & 0xFF + patch = (version >> 12) & 0xFF + level = (version >> 8) & 0x0F + serial = version & 0xFF + + levels = dict(zip(_version_level.values(), _version_level.keys())) + if level == 0xF and not serial: + version_string = '%u.%u.%u' % (major, minor, patch) + elif level in levels and not patch: + version_string = '%u.%u.%s%u' % (major, minor, levels[level], serial) + else: + raise ValueError('Invalid version: %r' % hex(version)) + + _version_cache[version] = version_string + return version_string + +def format_domain_default(domaindata): + """Format info output when the value displayed is the domain default.""" + return _(u'%s [domain default]') % domaindata + + +def search_addresses(dbh, typelimit=None, lpattern=None, llike=False, + dpattern=None, dlike=False): + """'Search' for addresses by *pattern* in the database. + + The search is limited by *typelimit*, a bitfield with values TYPE_ACCOUNT, + TYPE_ALIAS, TYPE_RELOCATED, or a bitwise OR thereof. If no limit is + specified, all types will be searched. + + *lpattern* may be a local part or a partial local part - starting and/or + ending with a '%' sign. When the *lpattern* starts or ends with a '%' sign + *llike* has to be `True` to perform a wildcard search. To retrieve all + available addresses use the arguments' default values. + + *dpattern* and *dlike* behave analogously for the domain part of an + address, allowing for separate pattern matching: testuser%@example.% + + The return value of this function is a tuple. The first element is a list + of domain IDs sorted alphabetically by the corresponding domain names. The + second element is a dictionary indexed by domain ID, holding lists to + associated addresses. Each address is itself actually a tuple of address, + type, and boolean indicating whether the address stems from an alias + domain. + """ + if typelimit == None: + typelimit = TYPE_ACCOUNT | TYPE_ALIAS | TYPE_RELOCATED + queries = [] + if typelimit & TYPE_ACCOUNT: + queries.append('SELECT gid, local_part, %d AS type FROM users' + % TYPE_ACCOUNT) + if typelimit & TYPE_ALIAS: + queries.append('SELECT gid, address as local_part, %d AS type ' + 'FROM alias' % TYPE_ALIAS) + if typelimit & TYPE_RELOCATED: + queries.append('SELECT gid, address as local_part, %d AS type ' + 'FROM relocated' % TYPE_RELOCATED) + sql = "SELECT gid, local_part || '@' || domainname AS address, " + sql += 'type, NOT is_primary AS from_aliasdomain FROM (' + sql += ' UNION '.join(queries) + sql += ') a JOIN domain_name USING (gid)' + nextkw = 'WHERE' + sqlargs = [] + for like, field, pattern in ((dlike, 'domainname', dpattern), + (llike, 'local_part', lpattern)): + if like: + match = 'LIKE' + else: + if not pattern: continue + match = '=' + sql += ' %s %s %s %%s' % (nextkw, field, match) + sqlargs.append(pattern) + nextkw = 'AND' + sql += ' ORDER BY domainname, local_part' + dbc = dbh.cursor() + dbc.execute(sql, sqlargs) + result = dbc.fetchall() + dbc.close() + + gids = [] + daddrs = {} + lastgid = None + for gid, address, addrtype, aliasdomain in result: + if gid != lastgid: + gids.append(gid) + lastgid = gid + daddrs[gid] = [] + daddrs[gid].append((address, addrtype, aliasdomain)) + return gids, daddrs + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/config.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,540 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2007 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.config + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + VMM's configuration module for simplified configuration access. +""" + +from ConfigParser import \ + Error, MissingSectionHeaderError, NoOptionError, NoSectionError, \ + ParsingError, RawConfigParser +from cStringIO import StringIO + +from VirtualMailManager.common import VERSION_RE, \ + exec_ok, expand_path, get_unicode, lisdir, size_in_bytes, version_hex +from VirtualMailManager.constants import CONF_ERROR +from VirtualMailManager.errors import ConfigError, VMMError +from VirtualMailManager.maillocation import known_format +from VirtualMailManager.password import verify_scheme as _verify_scheme + +DB_MODULES = ('psycopg2', 'pypgsql') +DB_SSL_MODES = ('allow', 'disabled', 'prefer', 'require', 'verify-ca', + 'verify-full') + +_ = lambda msg: msg + + +class BadOptionError(Error): + """Raised when a option isn't in the format 'section.option'.""" + pass + + +class ConfigValueError(Error): + """Raised when creating or validating of new values fails.""" + pass + + +class NoDefaultError(Error): + """Raised when the requested option has no default value.""" + + def __init__(self, section, option): + Error.__init__(self, 'Option %r in section %r has no default value' % + (option, section)) + + +class LazyConfig(RawConfigParser): + """The **lazy** derivate of the `RawConfigParser`. + + There are two additional getters: + + `pget()` + The polymorphic getter, which returns a option's value with the + appropriate type. + `dget()` + Like `LazyConfig.pget()`, but returns the option's default, from + `LazyConfig._cfg['sectionname']['optionname'].default`, if the + option is not configured in a ini-like configuration file. + + `set()` differs from `RawConfigParser`'s `set()` method. `set()` + takes the `section` and `option` arguments combined to a single + string in the form "section.option". + """ + + def __init__(self): + RawConfigParser.__init__(self) + self._modified = False + # sample _cfg dict. Create your own in your derived class. + self._cfg = { + 'sectionname': { + 'optionname': LazyConfigOption(int, 1, self.getint), + } + } + + def bool_new(self, value): + """Converts the string `value` into a `bool` and returns it. + + | '1', 'on', 'yes' and 'true' will become `True` + | '0', 'off', 'no' and 'false' will become `False` + + Throws a `ConfigValueError` for all other values, except bools. + """ + if isinstance(value, bool): + return value + if value.lower() in self._boolean_states: + return self._boolean_states[value.lower()] + else: + raise ConfigValueError(_(u"Not a boolean: '%s'") % + get_unicode(value)) + + def getboolean(self, section, option): + """Returns the boolean value of the option, in the given + section. + + For a boolean True, the value must be set to '1', 'on', 'yes', + 'true' or True. For a boolean False, the value must set to '0', + 'off', 'no', 'false' or False. + If the option has another value assigned this method will raise + a ValueError. + """ + # if the setting was modified it may be still a boolean value lets see + tmp = self.get(section, option) + if isinstance(tmp, bool): + return tmp + if not tmp.lower() in self._boolean_states: + raise ValueError('Not a boolean: %s' % tmp) + return self._boolean_states[tmp.lower()] + + def _get_section_option(self, section_option): + """splits ``section_option`` (section.option) in two parts and + returns them as list ``[section, option]``, if: + + * it likes the format of ``section_option`` + * the ``section`` is known + * the ``option`` is known + + Else one of the following exceptions will be thrown: + + * `BadOptionError` + * `NoSectionError` + * `NoOptionError` + """ + sect_opt = section_option.lower().split('.') + # TODO: cache it + if len(sect_opt) != 2 or not sect_opt[0] or not sect_opt[1]: + raise BadOptionError(_(u"Bad format: '%s' - expected: " + u"section.option") % + get_unicode(section_option)) + if not sect_opt[0] in self._cfg: + raise NoSectionError(sect_opt[0]) + if not sect_opt[1] in self._cfg[sect_opt[0]]: + raise NoOptionError(sect_opt[1], sect_opt[0]) + return sect_opt + + def items(self, section): + """returns an iterable that returns key, value ``tuples`` from + the given ``section``. + """ + if section in self._sections: # check if the section was parsed + sect = self._sections[section] + elif not section in self._cfg: + raise NoSectionError(section) + else: + return ((k, self._cfg[section][k].default) \ + for k in self._cfg[section].iterkeys()) + # still here? Get defaults and merge defaults with configured setting + defaults = dict((k, self._cfg[section][k].default) \ + for k in self._cfg[section].iterkeys()) + defaults.update(sect) + if '__name__' in defaults: + del defaults['__name__'] + return defaults.iteritems() + + def dget(self, option): + """Returns the value of the `option`. + + If the option could not be found in the configuration file, the + configured default value, from ``LazyConfig._cfg`` will be + returned. + + Arguments: + + `option` : string + the configuration option in the form "section.option" + + Throws a `NoDefaultError`, if no default value was passed to + `LazyConfigOption.__init__()` for the `option`. + """ + section, option = self._get_section_option(option) + try: + return self._cfg[section][option].getter(section, option) + except (NoSectionError, NoOptionError): + if not self._cfg[section][option].default is None: # may be False + return self._cfg[section][option].default + else: + raise NoDefaultError(section, option) + + def pget(self, option): + """Returns the value of the `option`.""" + section, option = self._get_section_option(option) + return self._cfg[section][option].getter(section, option) + + def set(self, option, value): + """Set the `value` of the `option`. + + Throws a `ValueError` if `value` couldn't be converted using + `LazyConfigOption.cls`. + """ + # pylint: disable=W0221 + # @pylint: _L A Z Y_ + section, option = self._get_section_option(option) + val = self._cfg[section][option].cls(value) + if self._cfg[section][option].validate: + val = self._cfg[section][option].validate(val) + if not RawConfigParser.has_section(self, section): + self.add_section(section) + RawConfigParser.set(self, section, option, val) + self._modified = True + + def has_section(self, section): + """Checks if `section` is a known configuration section.""" + return section.lower() in self._cfg + + def has_option(self, option): + """Checks if the option (section.option) is a known + configuration option. + """ + # pylint: disable=W0221 + # @pylint: _L A Z Y_ + try: + self._get_section_option(option) + return True + except(BadOptionError, NoSectionError, NoOptionError): + return False + + def sections(self): + """Returns an iterator object for all configuration sections.""" + return self._cfg.iterkeys() + + +class LazyConfigOption(object): + """A simple container class for configuration settings. + + `LazyConfigOption` instances are required by `LazyConfig` instances, + and instances of classes derived from `LazyConfig`, like the + `Config` class. + """ + __slots__ = ('__cls', '__default', '__getter', '__validate') + + def __init__(self, cls, default, getter, validate=None): + """Creates a new `LazyConfigOption` instance. + + Arguments: + + `cls` : type + The class/type of the option's value + `default` + Default value of the option. Use ``None`` if the option should + not have a default value. + `getter` : callable + A method's name of `RawConfigParser` and derived classes, to + get a option's value, e.g. `self.getint`. + `validate` : NoneType or a callable + None or any method, that takes one argument, in order to + check the value, when `LazyConfig.set()` is called. + """ + self.__cls = cls + if not default is None: # enforce the type of the default value + self.__default = self.__cls(default) + else: + self.__default = default + if not callable(getter): + raise TypeError('getter has to be a callable, got a %r' % + getter.__class__.__name__) + self.__getter = getter + if validate and not callable(validate): + raise TypeError('validate has to be callable or None, got a %r' % + validate.__class__.__name__) + self.__validate = validate + + @property + def cls(self): + """The class of the option's value e.g. `str`, `unicode` or `bool`.""" + return self.__cls + + @property + def default(self): + """The option's default value, may be `None`""" + return self.__default + + @property + def getter(self): + """The getter method or function to get the option's value""" + return self.__getter + + @property + def validate(self): + """A method or function to validate the value""" + return self.__validate + + +class Config(LazyConfig): + """This class is for reading vmm's configuration file.""" + + def __init__(self, filename): + """Creates a new Config instance + + Arguments: + + `filename` : str + path to the configuration file + """ + LazyConfig.__init__(self) + self._cfg_filename = filename + self._cfg_file = None + self._missing = {} + + LCO = LazyConfigOption + bool_t = self.bool_new + self._cfg = { + 'account': { + 'delete_directory': LCO(bool_t, False, self.getboolean), + 'directory_mode': LCO(int, 448, self.getint), + 'disk_usage': LCO(bool_t, False, self.getboolean), + 'password_length': LCO(int, 8, self.getint), + 'random_password': LCO(bool_t, False, self.getboolean), + }, + 'bin': { + 'dovecotpw': LCO(str, '/usr/sbin/dovecotpw', self.get, + exec_ok), + 'du': LCO(str, '/usr/bin/du', self.get, exec_ok), + 'postconf': LCO(str, '/usr/sbin/postconf', self.get, exec_ok), + }, + 'database': { + 'host': LCO(str, 'localhost', self.get), + 'module': LCO(str, 'psycopg2', self.get, check_db_module), + 'name': LCO(str, 'mailsys', self.get), + 'pass': LCO(str, None, self.get), + 'port': LCO(int, 5432, self.getint), + 'sslmode': LCO(str, 'prefer', self.get, check_db_ssl_mode), + 'user': LCO(str, None, self.get), + }, + 'domain': { + 'auto_postmaster': LCO(bool_t, True, self.getboolean), + 'delete_directory': LCO(bool_t, False, self.getboolean), + 'directory_mode': LCO(int, 504, self.getint), + 'force_deletion': LCO(bool_t, False, self.getboolean), + 'imap': LCO(bool_t, True, self.getboolean), + 'pop3': LCO(bool_t, True, self.getboolean), + 'sieve': LCO(bool_t, True, self.getboolean), + 'smtp': LCO(bool_t, True, self.getboolean), + 'quota_bytes': LCO(str, '0', self.get_in_bytes, + check_size_value), + 'quota_messages': LCO(int, 0, self.getint), + 'transport': LCO(str, 'dovecot:', self.get), + }, + 'mailbox': { + 'folders': LCO(str, 'Drafts:Sent:Templates:Trash', + self.unicode), + 'format': LCO(str, 'maildir', self.get, check_mailbox_format), + 'root': LCO(str, 'Maildir', self.unicode), + 'subscribe': LCO(bool_t, True, self.getboolean), + }, + 'misc': { + 'base_directory': LCO(str, '/srv/mail', self.get, is_dir), + 'crypt_blowfish_rounds': LCO(int, 5, self.getint), + 'crypt_sha256_rounds': LCO(int, 5000, self.getint), + 'crypt_sha512_rounds': LCO(int, 5000, self.getint), + 'dovecot_version': LCO(str, None, self.hexversion, + check_version_format), + 'password_scheme': LCO(str, 'CRAM-MD5', self.get, + verify_scheme), + }, + } + + def load(self): + """Loads the configuration, read only. + + Raises a ConfigError if the configuration syntax is + invalid. + """ + self._cfg_file = open(self._cfg_filename, 'r') + try: + self.readfp(self._cfg_file) + except (MissingSectionHeaderError, ParsingError), err: + raise ConfigError(str(err), CONF_ERROR) + self._cfg_file.close() + + def check(self): + """Performs a configuration check. + + Raises a ConfigError if settings w/o a default value are missed. + Or some settings have a invalid value. + """ + def iter_dict(): + for section, options in self._missing.iteritems(): + errmsg.write(_(u'* Section: %s\n') % section) + errmsg.writelines(u' %s\n' % option for option in options) + self._missing.clear() + + errmsg = None + self._chk_non_default() + miss_vers = 'misc' in self._missing and \ + 'dovecot_version' in self._missing['misc'] + if self._missing: + errmsg = StringIO() + errmsg.write(_(u'Check of configuration file %s failed.\n') % + self._cfg_filename) + errmsg.write(_(u'Missing options, which have no default value.\n')) + iter_dict() + self._chk_possible_values(miss_vers) + if self._missing: + if not errmsg: + errmsg = StringIO() + errmsg.write(_(u'Check of configuration file %s failed.\n') % + self._cfg_filename) + errmsg.write(_(u'Invalid configuration values.\n')) + else: + errmsg.write('\n' + _(u'Invalid configuration values.\n')) + iter_dict() + if errmsg: + raise ConfigError(errmsg.getvalue(), CONF_ERROR) + + def hexversion(self, section, option): + """Converts the version number (e.g.: 1.2.3) from the *option*'s + value to an int.""" + return version_hex(self.get(section, option)) + + def get_in_bytes(self, section, option): + """Converts the size value (e.g.: 1024k) from the *option*'s + value to a long""" + return size_in_bytes(self.get(section, option)) + + def unicode(self, section, option): + """Returns the value of the `option` from `section`, converted + to Unicode.""" + return get_unicode(self.get(section, option)) + + def _chk_non_default(self): + """Checks all section's options for settings w/o a default + value. Missing items will be stored in _missing. + """ + for section in self._cfg.iterkeys(): + missing = [] + for option, value in self._cfg[section].iteritems(): + if (value.default is None and + not RawConfigParser.has_option(self, section, option)): + missing.append(option) + if missing: + self._missing[section] = missing + + def _chk_possible_values(self, miss_vers): + """Check settings for which the possible values are known.""" + if not miss_vers: + value = self.get('misc', 'dovecot_version') + if not VERSION_RE.match(value): + self._missing['misc'] = ['version: ' +\ + _(u"Not a valid Dovecot version: '%s'") % value] + # section database + db_err = [] + value = self.dget('database.module').lower() + if value not in DB_MODULES: + db_err.append('module: ' + \ + _(u"Unsupported database module: '%s'") % value) + if value == 'psycopg2': + value = self.dget('database.sslmode') + if value not in DB_SSL_MODES: + db_err.append('sslmode: ' + \ + _(u"Unknown pgsql SSL mode: '%s'") % value) + if db_err: + self._missing['database'] = db_err + # section mailbox + value = self.dget('mailbox.format') + if not known_format(value): + self._missing['mailbox'] = ['format: ' +\ + _(u"Unsupported mailbox format: '%s'") % value] + # section domain + try: + value = self.dget('domain.quota_bytes') + except (ValueError, TypeError), err: + self._missing['domain'] = [u'quota_bytes: ' + str(err)] + + +def is_dir(path): + """Check if the expanded path is a directory. When the expanded path + is a directory the expanded path will be returned. Otherwise a + ConfigValueError will be raised. + """ + path = expand_path(path) + if lisdir(path): + return path + raise ConfigValueError(_(u"No such directory: %s") % get_unicode(path)) + + +def check_db_module(module): + """Check if the *module* is a supported pgsql module.""" + if module.lower() in DB_MODULES: + return module + raise ConfigValueError(_(u"Unsupported database module: '%s'") % + get_unicode(module)) + + +def check_db_ssl_mode(ssl_mode): + """Check if the *ssl_mode* is one of the SSL modes, known by pgsql.""" + if ssl_mode in DB_SSL_MODES: + return ssl_mode + raise ConfigValueError(_(u"Unknown pgsql SSL mode: '%s'") % + get_unicode(ssl_mode)) + + +def check_mailbox_format(format): + """ + Check if the mailbox format *format* is supported. When the *format* + is supported it will be returned, otherwise a `ConfigValueError` will + be raised. + """ + format = format.lower() + if known_format(format): + return format + raise ConfigValueError(_(u"Unsupported mailbox format: '%s'") % + get_unicode(format)) + + +def check_size_value(value): + """Check if the size value *value* has the proper format, e.g.: 1024k. + Returns the validated value string if it has the expected format. + Otherwise a `ConfigValueError` will be raised.""" + try: + tmp = size_in_bytes(value) + except (TypeError, ValueError), err: + raise ConfigValueError(_(u"Not a valid size value: '%s'") % + get_unicode(value)) + return value + + +def check_version_format(version_string): + """Check if the *version_string* has the proper format, e.g.: '1.2.3'. + Returns the validated version string if it has the expected format. + Otherwise a `ConfigValueError` will be raised. + """ + if not VERSION_RE.match(version_string): + raise ConfigValueError(_(u"Not a valid Dovecot version: '%s'") % + get_unicode(version_string)) + return version_string + + +def verify_scheme(scheme): + """Checks if the password scheme *scheme* can be accepted and returns + the verified scheme. + """ + try: + scheme, encoding = _verify_scheme(scheme) + except VMMError, err: # 'cast' it + raise ConfigValueError(err.msg) + if not encoding: + return scheme + return '%s.%s' % (scheme, encoding) + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/constants.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/constants.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,89 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2007 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.constants + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + VirtualMailManager's constants: + * version information + * upper and lower limits MIN_* / MAX_* + * exit codes + * error codes +""" +# version information + +__all__ = ['__author__', '__date__', '__version__'] +AUTHOR = 'Pascal Volk ' +RELDATE = '2009-09-09' +VERSION = '0.5.2' +__author__ = AUTHOR +__copyright__ = 'Copyright (c) 2007-2012 %s' % __author__ +__date__ = RELDATE +__version__ = VERSION + + +# limits + +MIN_GID = 70000 +MIN_UID = 70000 + + +# exit codes + +EX_SUCCESS = 0 +EX_MISSING_ARGS = 1 +EX_UNKNOWN_COMMAND = 2 +EX_USER_INTERRUPT = 3 + + +# error codes + +ACCOUNT_AND_ALIAS_PRESENT = 20 +ACCOUNT_EXISTS = 21 +ACCOUNT_MISSING_PASSWORD = 69 +ALIASDOMAIN_EXISTS = 23 +ALIASDOMAIN_ISDOMAIN = 24 +ALIASDOMAIN_NO_DOMDEST = 25 +ALIAS_EXCEEDS_EXPANSION_LIMIT = 27 +ALIAS_EXISTS = 28 +ALIAS_PRESENT = 30 +CONF_ERROR = 31 +CONF_NOFILE = 32 +CONF_NOPERM = 33 +CONF_WRONGPERM = 34 +DATABASE_ERROR = 35 +DOMAINDIR_GROUP_MISMATCH = 36 +DOMAIN_ALIAS_EXISTS = 37 +DOMAIN_EXISTS = 38 +DOMAIN_INVALID = 39 +DOMAIN_NO_NAME = 40 +DOMAIN_TOO_LONG = 41 +FOUND_DOTS_IN_PATH = 42 +INVALID_ADDRESS = 43 +INVALID_ARGUMENT = 44 +INVALID_MAIL_LOCATION = 70 +INVALID_SECTION = 46 +LOCALPART_INVALID = 47 +LOCALPART_TOO_LONG = 48 +MAILDIR_PERM_MISMATCH = 49 +MAILLOCATION_INIT = 50 +NOT_EXECUTABLE = 51 +NO_SUCH_ACCOUNT = 52 +NO_SUCH_ALIAS = 53 +NO_SUCH_ALIASDOMAIN = 54 +NO_SUCH_BINARY = 55 +NO_SUCH_DIRECTORY = 56 +NO_SUCH_DOMAIN = 57 +NO_SUCH_RELOCATED = 58 +RELOCATED_ADDR_DEST_IDENTICAL = 59 +RELOCATED_EXISTS = 60 +UNKNOWN_SERVICE = 65 +VMM_ERROR = 67 +VMM_TOO_MANY_FAILURES = 68 + +# address types + +TYPE_ACCOUNT = 0x1 +TYPE_ALIAS = 0x2 +TYPE_RELOCATED = 0x4 diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/constants/ERROR.py --- a/VirtualMailManager/constants/ERROR.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,52 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2007 - 2010, Pascal Volk -# See COPYING for distribution information. - -ACCOUNT_AND_ALIAS_PRESENT = 20 -ACCOUNT_EXISTS = 21 -ACCOUNT_PRESENT = 22 -ALIASDOMAIN_EXISTS = 23 -ALIASDOMAIN_ISDOMAIN = 24 -ALIASDOMAIN_NO_DOMDEST = 25 -ALIAS_ADDR_DEST_IDENTICAL = 26 -ALIAS_EXCEEDS_EXPANSION_LIMIT = 27 -ALIAS_EXISTS = 28 -ALIAS_MISSING_DEST = 29 -ALIAS_PRESENT = 30 -CONF_ERROR = 31 -CONF_NOFILE = 32 -CONF_NOPERM = 33 -CONF_WRONGPERM = 34 -DATABASE_ERROR = 35 -DOMAINDIR_GROUP_MISMATCH = 36 -DOMAIN_ALIAS_EXISTS = 37 -DOMAIN_EXISTS = 38 -DOMAIN_INVALID = 39 -DOMAIN_NO_NAME = 40 -DOMAIN_TOO_LONG = 41 -FOUND_DOTS_IN_PATH = 42 -INVALID_ADDRESS = 43 -INVALID_AGUMENT = 44 -INVALID_OPTION = 45 -INVALID_SECTION = 46 -LOCALPART_INVALID = 47 -LOCALPART_TOO_LONG = 48 -MAILDIR_PERM_MISMATCH = 49 -MAILLOCATION_INIT = 50 -NOT_EXECUTABLE = 51 -NO_SUCH_ACCOUNT = 52 -NO_SUCH_ALIAS = 53 -NO_SUCH_ALIASDOMAIN = 54 -NO_SUCH_BINARY = 55 -NO_SUCH_DIRECTORY = 56 -NO_SUCH_DOMAIN = 57 -NO_SUCH_RELOCATED = 58 -RELOCATED_ADDR_DEST_IDENTICAL = 59 -RELOCATED_EXISTS = 60 -RELOCATED_MISSING_DEST = 61 -TRANSPORT_INIT = 62 -UNKNOWN_MAILLOCATION_ID = 63 -UNKNOWN_SERVICE = 64 -UNKNOWN_TRANSPORT_ID = 65 -VMM_ERROR = 66 -VMM_TOO_MANY_FAILURES = 67 diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/constants/EXIT.py --- a/VirtualMailManager/constants/EXIT.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,7 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2007 - 2010, Pascal Volk -# See COPYING for distribution information. - -MISSING_ARGS = 1 -UNKNOWN_COMMAND = 2 -USER_INTERRUPT = 3 diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/constants/VERSION.py --- a/VirtualMailManager/constants/VERSION.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,11 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2007 - 2010, Pascal Volk -# See COPYING for distribution information. - -AUTHOR = 'Pascal Volk ' -RELDATE = '2009-09-09' -VERSION = '0.5.2' -__author__ = AUTHOR -__date__ = RELDATE -__version__ = VERSION -__all__ = ['__author__', '__date__', '__version__'] diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/constants/__init__.py --- a/VirtualMailManager/constants/__init__.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,6 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2007 - 2010, Pascal Volk -# See COPYING for distribution information. -# package placeholder -# -# EOF diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/domain.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/domain.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,573 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2007 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.domain + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + Virtual Mail Manager's Domain class to manage e-mail domains. +""" + +import os +import re +from random import choice + +from VirtualMailManager.constants import \ + ACCOUNT_AND_ALIAS_PRESENT, DOMAIN_ALIAS_EXISTS, DOMAIN_EXISTS, \ + DOMAIN_INVALID, DOMAIN_TOO_LONG, NO_SUCH_DOMAIN, VMM_ERROR +from VirtualMailManager.errors import VMMError, DomainError as DomErr +from VirtualMailManager.pycompat import all, any +from VirtualMailManager.quotalimit import QuotaLimit +from VirtualMailManager.serviceset import ServiceSet +from VirtualMailManager.transport import Transport + + +MAILDIR_CHARS = '0123456789abcdefghijklmnopqrstuvwxyz' +RE_DOMAIN = re.compile(r"^(?:[a-z0-9-]{1,63}\.){1,}[a-z]{2,6}$") +_ = lambda msg: msg +cfg_dget = lambda option: None + + +class Domain(object): + """Class to manage e-mail domains.""" + __slots__ = ('_directory', '_gid', '_name', '_qlimit', '_services', + '_transport', '_note', '_dbh', '_new') + + def __init__(self, dbh, domainname): + """Creates a new Domain instance. + + Loads all relevant data from the database, if the domain could be + found. To create a new domain call the methods set_directory() and + set_transport() before save(). + + A DomainError will be thrown when the *domainname* is the name of + an alias domain. + + Arguments: + + `dbh` : pyPgSQL.PgSQL.Connection + a database connection for the database access + `domainname` : basestring + The name of the domain + """ + self._name = check_domainname(domainname) + self._dbh = dbh + self._gid = 0 + self._qlimit = None + self._services = None + self._transport = None + self._directory = None + self._note = None + self._new = True + self._load() + + def _load(self): + """Load information from the database and checks if the domain name + is the primary one. + + Raises a DomainError if Domain._name isn't the primary name of the + domain. + """ + dbc = self._dbh.cursor() + dbc.execute('SELECT dd.gid, qid, ssid, tid, domaindir, is_primary, ' + 'note ' + 'FROM domain_data dd, domain_name dn WHERE domainname = ' + '%s AND dn.gid = dd.gid', (self._name,)) + result = dbc.fetchone() + dbc.close() + if result: + if not result[5]: + raise DomErr(_(u"The domain '%s' is an alias domain.") % + self._name, DOMAIN_ALIAS_EXISTS) + self._gid, self._directory = result[0], result[4] + self._qlimit = QuotaLimit(self._dbh, qid=result[1]) + self._services = ServiceSet(self._dbh, ssid=result[2]) + self._transport = Transport(self._dbh, tid=result[3]) + self._note = result[6] + self._new = False + + def _set_gid(self): + """Sets the ID of the domain - if not set yet.""" + assert self._gid == 0 + dbc = self._dbh.cursor() + dbc.execute("SELECT nextval('domain_gid')") + self._gid = dbc.fetchone()[0] + dbc.close() + + def _check_for_addresses(self): + """Checks dependencies for deletion. Raises a DomainError if there + are accounts, aliases and/or relocated users. + """ + dbc = self._dbh.cursor() + dbc.execute('SELECT ' + '(SELECT count(gid) FROM users WHERE gid = %(gid)u)' + ' as account_count, ' + '(SELECT count(gid) FROM alias WHERE gid = %(gid)u)' + ' as alias_count, ' + '(SELECT count(gid) FROM relocated WHERE gid = %(gid)u)' + ' as relocated_count' + % {'gid': self._gid}) + result = dbc.fetchall() + dbc.close() + result = result[0] + if any(result): + keys = ('account_count', 'alias_count', 'relocated_count') + raise DomErr(_(u'There are %(account_count)u accounts, ' + u'%(alias_count)u aliases and %(relocated_count)u ' + u'relocated users.') % dict(zip(keys, result)), + ACCOUNT_AND_ALIAS_PRESENT) + + def _chk_state(self, must_exist=True): + """Checks the state of the Domain instance and will raise a + VirtualMailManager.errors.DomainError: + - if *must_exist* is `True` and the domain doesn't exist + - or *must_exist* is `False` and the domain exists + """ + if must_exist and self._new: + raise DomErr(_(u"The domain '%s' does not exist.") % self._name, + NO_SUCH_DOMAIN) + elif not must_exist and not self._new: + raise DomErr(_(u"The domain '%s' already exists.") % self._name, + DOMAIN_EXISTS) + + def _update_tables(self, column, value): + """Update table columns in the domain_data table.""" + dbc = self._dbh.cursor() + dbc.execute('UPDATE domain_data SET %s = %%s WHERE gid = %%s' % column, + (value, self._gid)) + if dbc.rowcount > 0: + self._dbh.commit() + dbc.close() + + def _update_tables_ref(self, column, value, force=False): + """Update various columns in the domain_data table. When *force* is + `True`, the corresponding column in the users table will be reset to + NULL. + + Arguments: + + `column` : basestring + Name of the table column. Currently: qid, ssid and tid + `value` : long + The referenced key + `force` : bool + reset existing users. Default: `False` + """ + if column not in ('qid', 'ssid', 'tid'): + raise ValueError('Unknown column: %r' % column) + self._update_tables(column, value) + if force: + dbc = self._dbh.cursor() + dbc.execute('UPDATE users SET %s = NULL WHERE gid = %%s' % column, + (self._gid,)) + if dbc.rowcount > 0: + self._dbh.commit() + dbc.close() + + @property + def gid(self): + """The GID of the Domain.""" + return self._gid + + @property + def name(self): + """The Domain's name.""" + return self._name + + @property + def directory(self): + """The Domain's directory.""" + return self._directory + + @property + def quotalimit(self): + """The Domain's quota limit.""" + return self._qlimit + + @property + def serviceset(self): + """The Domain's serviceset.""" + return self._services + + @property + def transport(self): + """The Domain's transport.""" + return self._transport + + @property + def note(self): + """The Domain's note.""" + return self._note + + def set_directory(self, basedir): + """Set the path value of the Domain's directory, inside *basedir*. + + Argument: + + `basedir` : basestring + The base directory of all domains + """ + self._chk_state(False) + assert self._directory is None + self._set_gid() + self._directory = os.path.join(basedir, choice(MAILDIR_CHARS), + str(self._gid)) + + def set_quotalimit(self, quotalimit): + """Set the quota limit for the new Domain. + + Argument: + + `quotalimit` : VirtualMailManager.quotalimit.QuotaLimit + The quota limit of the new Domain. + """ + self._chk_state(False) + assert isinstance(quotalimit, QuotaLimit) + self._qlimit = quotalimit + + def set_serviceset(self, serviceset): + """Set the services for the new Domain. + + Argument: + + `serviceset` : VirtualMailManager.serviceset.ServiceSet + The service set for the new Domain. + """ + self._chk_state(False) + assert isinstance(serviceset, ServiceSet) + self._services = serviceset + + def set_transport(self, transport): + """Set the transport for the new Domain. + + Argument: + + `transport` : VirtualMailManager.Transport + The transport of the new Domain + """ + self._chk_state(False) + assert isinstance(transport, Transport) + self._transport = transport + + def set_note(self, note): + """Set the domain's (optional) note. + + Argument: + + `note` : basestring or None + The note, or None to remove + """ + self._chk_state(False) + assert note is None or isinstance(note, basestring) + self._note = note + + def save(self): + """Stores the new domain in the database.""" + self._chk_state(False) + assert all((self._directory, self._qlimit, self._services, + self._transport)) + dbc = self._dbh.cursor() + dbc.execute('INSERT INTO domain_data (gid, qid, ssid, tid, domaindir, ' + 'note) ' + 'VALUES (%s, %s, %s, %s, %s, %s)', (self._gid, + self._qlimit.qid, self._services.ssid, self._transport.tid, + self._directory, self._note)) + dbc.execute('INSERT INTO domain_name (domainname, gid, is_primary) ' + 'VALUES (%s, %s, TRUE)', (self._name, self._gid)) + self._dbh.commit() + dbc.close() + self._new = False + + def delete(self, force=False): + """Deletes the domain. + + Arguments: + + `force` : bool + force the deletion of all available accounts, aliases and + relocated users. When *force* is `False` and there are accounts, + aliases and/or relocated users a DomainError will be raised. + Default `False` + """ + if not isinstance(force, bool): + raise TypeError('force must be a bool') + self._chk_state() + if not force: + self._check_for_addresses() + dbc = self._dbh.cursor() + for tbl in ('alias', 'users', 'relocated', 'domain_name', + 'domain_data'): + dbc.execute("DELETE FROM %s WHERE gid = %u" % (tbl, self._gid)) + self._dbh.commit() + dbc.close() + self._gid = 0 + self._directory = self._qlimit = self._transport = None + self._services = None + self._new = True + + def update_quotalimit(self, quotalimit, force=False): + """Update the quota limit of the Domain. + + If *force* is `True`, accounts-specific overrides will be reset + for all existing accounts of the domain. Otherwise, the limit + will only affect accounts that use the default. + + Arguments: + + `quotalimit` : VirtualMailManager.quotalimit.QuotaLimit + the new quota limit of the domain. + `force` : bool + enforce new quota limit for all accounts, default `False` + """ + if cfg_dget('misc.dovecot_version') < 0x10102f00: + raise VMMError(_(u'PostgreSQL-based dictionary quota requires ' + u'Dovecot >= v1.1.2.'), VMM_ERROR) + self._chk_state() + assert isinstance(quotalimit, QuotaLimit) + if not force and quotalimit == self._qlimit: + return + self._update_tables_ref('qid', quotalimit.qid, force) + self._qlimit = quotalimit + + def update_serviceset(self, serviceset, force=False): + """Assign a different set of services to the Domain, + + If *force* is `True`, accounts-specific overrides will be reset + for all existing accounts of the domain. Otherwise, the service + set will only affect accounts that use the default. + + Arguments: + `serviceset` : VirtualMailManager.serviceset.ServiceSet + the new set of services + `force` + enforce the serviceset for all accounts, default `False` + """ + self._chk_state() + assert isinstance(serviceset, ServiceSet) + if not force and serviceset == self._services: + return + self._update_tables_ref('ssid', serviceset.ssid, force) + self._services = serviceset + + def update_transport(self, transport, force=False): + """Sets a new transport for the Domain. + + If *force* is `True`, accounts-specific overrides will be reset + for all existing accounts of the domain. Otherwise, the transport + setting will only affect accounts that use the default. + + Arguments: + + `transport` : VirtualMailManager.Transport + the new transport + `force` : bool + enforce new transport setting for all accounts, default `False` + """ + self._chk_state() + assert isinstance(transport, Transport) + if not force and transport == self._transport: + return + self._update_tables_ref('tid', transport.tid, force) + self._transport = transport + + def update_note(self, note): + """Sets a new note for the Domain. + + Arguments: + + `transport` : basestring or None + the new note + """ + self._chk_state() + assert note is None or isinstance(note, basestring) + if note == self._note: + return + self._update_tables('note', note) + self._note = note + + def get_info(self): + """Returns a dictionary with information about the domain.""" + self._chk_state() + dbc = self._dbh.cursor() + dbc.execute('SELECT aliasdomains "alias domains", accounts, aliases, ' + 'relocated, catchall "catch-all dests" ' + 'FROM vmm_domain_info WHERE gid = %s', (self._gid,)) + info = dbc.fetchone() + dbc.close() + keys = ('alias domains', 'accounts', 'aliases', 'relocated', + 'catch-all dests') + info = dict(zip(keys, info)) + info['gid'] = self._gid + info['domain name'] = self._name + info['transport'] = self._transport.transport + info['domain directory'] = self._directory + info['bytes'] = self._qlimit.bytes + info['messages'] = self._qlimit.messages + services = self._services.services + services = [s.upper() for s in services if services[s]] + if services: + services.sort() + else: + services.append('None') + info['active services'] = ' '.join(services) + info['note'] = self._note + return info + + def get_accounts(self): + """Returns a list with all accounts of the domain.""" + self._chk_state() + dbc = self._dbh.cursor() + dbc.execute('SELECT local_part from users where gid = %s ORDER BY ' + 'local_part', (self._gid,)) + users = dbc.fetchall() + dbc.close() + accounts = [] + if users: + addr = u'@'.join + _dom = self._name + accounts = [addr((account[0], _dom)) for account in users] + return accounts + + def get_aliases(self): + """Returns a list with all aliases e-mail addresses of the domain.""" + self._chk_state() + dbc = self._dbh.cursor() + dbc.execute('SELECT DISTINCT address FROM alias WHERE gid = %s ORDER ' + 'BY address', (self._gid,)) + addresses = dbc.fetchall() + dbc.close() + aliases = [] + if addresses: + addr = u'@'.join + _dom = self._name + aliases = [addr((alias[0], _dom)) for alias in addresses] + return aliases + + def get_relocated(self): + """Returns a list with all addresses of relocated users.""" + self._chk_state() + dbc = self._dbh.cursor() + dbc.execute('SELECT address FROM relocated WHERE gid = %s ORDER BY ' + 'address', (self._gid,)) + addresses = dbc.fetchall() + dbc.close() + relocated = [] + if addresses: + addr = u'@'.join + _dom = self._name + relocated = [addr((address[0], _dom)) for address in addresses] + return relocated + + def get_catchall(self): + """Returns a list with all catchall e-mail addresses of the domain.""" + self._chk_state() + dbc = self._dbh.cursor() + dbc.execute('SELECT DISTINCT destination FROM catchall WHERE gid = %s ORDER ' + 'BY destination', (self._gid,)) + addresses = dbc.fetchall() + dbc.close() + return addresses + + def get_aliase_names(self): + """Returns a list with all alias domain names of the domain.""" + self._chk_state() + dbc = self._dbh.cursor() + dbc.execute('SELECT domainname FROM domain_name WHERE gid = %s AND ' + 'NOT is_primary ORDER BY domainname', (self._gid,)) + anames = dbc.fetchall() + dbc.close() + aliasdomains = [] + if anames: + aliasdomains = [aname[0] for aname in anames] + return aliasdomains + + +def check_domainname(domainname): + """Returns the validated domain name `domainname`. + + Throws an `DomainError`, if the domain name is too long or doesn't + look like a valid domain name (label.label.label). + + """ + if not RE_DOMAIN.match(domainname): + domainname = domainname.encode('idna') + if len(domainname) > 255: + raise DomErr(_(u'The domain name is too long'), DOMAIN_TOO_LONG) + if not RE_DOMAIN.match(domainname): + raise DomErr(_(u"The domain name '%s' is invalid") % domainname, + DOMAIN_INVALID) + return domainname + + +def get_gid(dbh, domainname): + """Returns the group id of the domain *domainname*. + + If the domain couldn't be found in the database 0 will be returned. + """ + domainname = check_domainname(domainname) + dbc = dbh.cursor() + dbc.execute('SELECT gid FROM domain_name WHERE domainname = %s', + (domainname,)) + gid = dbc.fetchone() + dbc.close() + if gid: + return gid[0] + return 0 + + +def search(dbh, pattern=None, like=False): + """'Search' for domains by *pattern* in the database. + + *pattern* may be a domain name or a partial domain name - starting + and/or ending with a '%' sign. When the *pattern* starts or ends with + a '%' sign *like* has to be `True` to perform a wildcard search. + To retrieve all available domains use the arguments' default values. + + This function returns a tuple with a list and a dict: (order, domains). + The order list contains the domains' gid, alphabetical sorted by the + primary domain name. The domains dict's keys are the gids of the + domains. The value of item is a list. The first list element contains + the primary domain name or `None`. The elements [1:] contains the + names of alias domains. + + Arguments: + + `pattern` : basestring + a (partial) domain name (starting and/or ending with a "%" sign) + `like` : bool + should be `True` when *pattern* starts/ends with a "%" sign + """ + if pattern and not like: + pattern = check_domainname(pattern) + sql = 'SELECT gid, domainname, is_primary FROM domain_name' + if pattern: + if like: + sql += " WHERE domainname LIKE '%s'" % pattern + else: + sql += " WHERE domainname = '%s'" % pattern + sql += ' ORDER BY is_primary DESC, domainname' + dbc = dbh.cursor() + dbc.execute(sql) + result = dbc.fetchall() + dbc.close() + + gids = [domain[0] for domain in result if domain[2]] + domains = {} + for gid, domain, is_primary in result: + if is_primary: + if not gid in domains: + domains[gid] = [domain] + else: + domains[gid].insert(0, domain) + else: + if gid in gids: + if gid in domains: + domains[gid].append(domain) + else: + domains[gid] = [domain] + else: + gids.append(gid) + domains[gid] = [None, domain] + return gids, domains + +del _, cfg_dget diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/emailaddress.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/emailaddress.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,155 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2008 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.emailaddress + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Virtual Mail Manager's EmailAddress class to handle e-mail addresses. +""" +import re + +from VirtualMailManager.domain import check_domainname, get_gid +from VirtualMailManager.constants import \ + DOMAIN_NO_NAME, INVALID_ADDRESS, LOCALPART_INVALID, LOCALPART_TOO_LONG, \ + DOMAIN_INVALID +from VirtualMailManager.errors import DomainError, EmailAddressError as EAErr + + +RE_LOCALPART = re.compile(r"[^\w!#$%&'\*\+-\.\/=?^_`{\|}~]") +_ = lambda msg: msg + + +class EmailAddress(object): + """Simple class for validated e-mail addresses.""" + __slots__ = ('_localpart', '_domainname') + + def __init__(self, address, _validate=True): + """Creates a new instance from the string/unicode ``address``.""" + assert isinstance(address, basestring) + self._localpart = None + self._domainname = None + if _validate: + self._chk_address(address) + + @property + def localpart(self): + """The local-part of the address *local-part@domain*""" + return self._localpart + + @property + def domainname(self): + """The domain part of the address *local-part@domain*""" + return self._domainname + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self._localpart == other._localpart and \ + self._domainname == other._domainname + return NotImplemented + + def __ne__(self, other): + if isinstance(other, self.__class__): + return self._localpart != other._localpart or \ + self._domainname != other._domainname + return NotImplemented + + def __hash__(self): + return hash((self._localpart.lower(), self._domainname.lower())) + + def __repr__(self): + return "EmailAddress('%s@%s')" % (self._localpart, self._domainname) + + def __str__(self): + return '%s@%s' % (self._localpart, self._domainname) + + def _chk_address(self, address): + """Checks if the string ``address`` could be used for an e-mail + address. If so, it will assign the corresponding values to the + attributes `_localpart` and `_domainname`.""" + parts = address.split('@') + p_len = len(parts) + if p_len < 2: + raise EAErr(_(u"Missing the '@' sign in address: '%s'") % address, + INVALID_ADDRESS) + elif p_len > 2: + raise EAErr(_(u"Too many '@' signs in address: '%s'") % address, + INVALID_ADDRESS) + if not parts[0]: + raise EAErr(_(u"Missing local-part in address: '%s'") % address, + LOCALPART_INVALID) + if not parts[1]: + raise EAErr(_(u"Missing domain name in address: '%s'") % address, + DOMAIN_NO_NAME) + self._localpart = check_localpart(parts[0]) + self._domainname = check_domainname(parts[1]) + + +class DestinationEmailAddress(EmailAddress): + """Provides additionally the domains group ID - when the domain is known + in the database.""" + __slots__ = ('_gid', '_localhost') + + def __init__(self, address, dbh, _validate=False): + """Creates a new DestinationEmailAddress instance + + Arguments: + + `address`: string/unicode + a e-mail address like user@example.com + `dbh`: pyPgSQL.PgSQL.Connection/pyPgSQL.PgSQL.connection + a database connection for the database access + """ + super(DestinationEmailAddress, self).__init__(address, _validate) + self._localhost = False + if not _validate: + try: + self._chk_address(address) + except DomainError, err: + if err.code is DOMAIN_INVALID and \ + address.split('@')[1] == 'localhost': + self._localhost = True + self._domainname = 'localhost' + else: + raise + self._gid = 0 + if not self._localhost: + self._find_domain(dbh) + else: + self._localpart = self._localpart.lower() + + def _find_domain(self, dbh): + """Checks if the domain is known""" + self._gid = get_gid(dbh, self._domainname) + if self._gid: + self._localpart = self._localpart.lower() + + @property + def at_localhost(self): + """True when the address is something@localhost.""" + return self._localhost + + @property + def gid(self): + """The domains group ID. 0 if the domain is not known.""" + return self._gid + + +def check_localpart(localpart): + """Returns the validated local-part `localpart`. + + Throws a `EmailAddressError` if the local-part is too long or contains + invalid characters. + """ + if len(localpart) > 64: + raise EAErr(_(u"The local-part '%s' is too long.") % localpart, + LOCALPART_TOO_LONG) + invalid_chars = set(RE_LOCALPART.findall(localpart)) + if invalid_chars: + i_chars = u''.join((u'"%s" ' % c for c in invalid_chars)) + raise EAErr(_(u"The local-part '%(l_part)s' contains invalid " + u"characters: %(i_chars)s") % {'l_part': localpart, + 'i_chars': i_chars}, LOCALPART_INVALID) + return localpart + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/errors.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/errors.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,71 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2007 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.errors + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + VMM's Exception classes +""" + + +class VMMError(Exception): + """Exception base class for VirtualMailManager exceptions""" + + def __init__(self, msg, code): + Exception.__init__(self, msg) + self.msg = msg + self.code = int(code) + + def __repr__(self): + return '%s(%r, %r)' % (self.__class__.__name__, self.msg, self.code) + + +class ConfigError(VMMError): + """Exception class for configuration exceptions""" + pass + + +class PermissionError(VMMError): + """Exception class for permissions exceptions""" + pass + + +class NotRootError(VMMError): + """Exception class for non-root exceptions""" + pass + + +class DomainError(VMMError): + """Exception class for Domain exceptions""" + pass + + +class AliasDomainError(VMMError): + """Exception class for AliasDomain exceptions""" + pass + + +class AccountError(VMMError): + """Exception class for Account exceptions""" + pass + + +class AliasError(VMMError): + """Exception class for Alias exceptions""" + pass + + +class EmailAddressError(VMMError): + """Exception class for EmailAddress exceptions""" + pass + + +class MailLocationError(VMMError): + """Exception class for MailLocation exceptions""" + pass + + +class RelocatedError(VMMError): + """Exception class for Relocated exceptions""" + pass diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/ext/Postconf.py --- a/VirtualMailManager/ext/Postconf.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +0,0 @@ -# -*- coding: UTF-8 -*- -# Copyright (c) 2008 - 2010, Pascal Volk -# See COPYING for distribution information. - -"""A small - r/o - wrapper class for Postfix' postconf.""" - -from subprocess import Popen, PIPE - -from __main__ import re, ERR -from VirtualMailManager.Exceptions import VMMException - -RE_PC_PARAMS = """^\w+$""" -RE_PC_VARIABLES = r"""\$\b\w+\b""" - -class Postconf(object): - __slots__ = ('__bin', '__val', '__varFinder') - def __init__(self, postconf_bin): - """Creates a new Postconf instance. - - Keyword arguments: - postconf_bin -- absolute path to the Postfix postconf binary (str) - """ - self.__bin = postconf_bin - self.__val = '' - self.__varFinder = re.compile(RE_PC_VARIABLES) - - def read(self, parameter, expand_vars=True): - """Returns the parameters value. - - If expand_vars is True (default), all variables in the value will be - expanded: - e.g. mydestination -> mail.example.com, localhost.example.com, localhost - Otherwise the value may contain one or more variables. - e.g. mydestination -> $myhostname, localhost.$mydomain, localhost - - Keyword arguments: - parameter -- the name of a Postfix configuration parameter (str) - expand_vars -- default True (bool) - """ - if not re.match(RE_PC_PARAMS, parameter): - raise VMMException(_(u'The value “%s” doesn\'t look like a valid\ - postfix configuration parameter name.') % parameter, ERR.VMM_ERROR) - self.__val = self.__read(parameter) - if expand_vars: - self.__expandVars() - return self.__val - - def __expandVars(self): - while True: - pvars = set(self.__varFinder.findall(self.__val)) - pvars_len = len(pvars) - if pvars_len < 1: - break - if pvars_len > 1: - self.__expandMultiVars(self.__readMulti(pvars)) - continue - pvars = pvars.pop() - self.__val = self.__val.replace(pvars, self.__read(pvars[1:])) - - def __expandMultiVars(self, old_new): - for old, new in old_new.items(): - self.__val = self.__val.replace('$'+old, new) - - def __read(self, parameter): - out, err = Popen([self.__bin, '-h', parameter], stdout=PIPE, - stderr=PIPE).communicate() - if len(err): - raise VMMException(err.strip(), ERR.VMM_ERROR) - return out.strip() - - def __readMulti(self, parameters): - cmd = [self.__bin] - for parameter in parameters: - cmd.append(parameter[1:]) - out, err = Popen(cmd, stdout=PIPE, stderr=PIPE).communicate() - if len(err): - raise VMMException(err.strip(), ERR.VMM_ERROR) - par_val = {} - for line in out.splitlines(): - par, val = line.split(' = ') - par_val[par] = val - return par_val - diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/ext/__init__.py --- a/VirtualMailManager/ext/__init__.py Mon Nov 07 03:22:15 2011 +0000 +++ b/VirtualMailManager/ext/__init__.py Thu Jun 28 19:26:50 2012 +0000 @@ -1,5 +1,5 @@ # -*- coding: UTF-8 -*- -# Copyright (c) 2008 - 2010, Pascal Volk +# Copyright (c) 2008 - 2012, Pascal Volk # See COPYING for distribution information. # package placeholder # diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/ext/postconf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/ext/postconf.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,127 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2008 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.ext.postconf + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Wrapper class for Postfix's postconf. + Postconf instances can be used to read actual values of configuration + parameters or edit the value of a configuration parameter. + + postconf.read(parameter) -> value + postconf.edit(parameter, value) +""" + +import re +from subprocess import Popen, PIPE + +from VirtualMailManager.errors import VMMError +from VirtualMailManager.constants import VMM_ERROR + +_ = lambda msg: msg + + +class Postconf(object): + """Wrapper class for Postfix's postconf.""" + __slots__ = ('_bin', '_val') + _parameter_re = re.compile(r'^\w+$') + _variables_re = re.compile(r'\$\b\w+\b') + + def __init__(self, postconf_bin): + """Creates a new Postconf instance. + + Argument: + + `postconf_bin` : str + absolute path to the Postfix postconf binary. + """ + self._bin = postconf_bin + self._val = '' + + def edit(self, parameter, value): + """Set the `parameter`'s value to `value`. + + Arguments: + + `parameter` : str + the name of a Postfix configuration parameter + `value` : str + the parameter's new value. + """ + self._check_parameter(parameter) + stderr = Popen((self._bin, '-e', parameter + '=' + str(value)), + stderr=PIPE).communicate()[1] + if stderr: + raise VMMError(stderr.strip(), VMM_ERROR) + + def read(self, parameter, expand_vars=True): + """Returns the parameters value. + + If expand_vars is True (default), all variables in the value will be + expanded: + e.g. mydestination: mail.example.com, localhost.example.com, localhost + Otherwise the value may contain one or more variables. + e.g. mydestination: $myhostname, localhost.$mydomain, localhost + + Arguments: + + `parameter` : str + the name of a Postfix configuration parameter. + `expand_vars` : bool + indicates if variables should be expanded or not, default True + """ + self._check_parameter(parameter) + self._val = self._read(parameter) + if expand_vars: + self._expand_vars() + return self._val + + def _check_parameter(self, parameter): + """Check that the `parameter` looks like a configuration parameter. + If not, a VMMError will be raised.""" + if not self.__class__._parameter_re.match(parameter): + raise VMMError(_(u"The value '%s' does not look like a valid " + u"postfix configuration parameter name.") % + parameter, VMM_ERROR) + + def _expand_vars(self): + """Expand the $variables in self._val to their values.""" + while True: + pvars = set(self.__class__._variables_re.findall(self._val)) + if not pvars: + break + if len(pvars) > 1: + self._expand_multi_vars(self._read_multi(pvars)) + continue + pvars = pvars.pop() + self._val = self._val.replace(pvars, self._read(pvars[1:])) + + def _expand_multi_vars(self, old_new): + """Replace all $vars in self._val with their values.""" + for old, new in old_new.iteritems(): + self._val = self._val.replace('$' + old, new) + + def _read(self, parameter): + """Ask postconf for the value of a single configuration parameter.""" + stdout, stderr = Popen([self._bin, '-h', parameter], stdout=PIPE, + stderr=PIPE).communicate() + if stderr: + raise VMMError(stderr.strip(), VMM_ERROR) + return stdout.strip() + + def _read_multi(self, parameters): + """Ask postconf for multiple configuration parameters. Returns a dict + parameter: value items.""" + cmd = [self._bin] + cmd.extend(parameter[1:] for parameter in parameters) + stdout, stderr = Popen(cmd, stdout=PIPE, stderr=PIPE).communicate() + if stderr: + raise VMMError(stderr.strip(), VMM_ERROR) + par_val = {} + for line in stdout.splitlines(): + par, val = line.split(' = ') + par_val[par] = val + return par_val + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/handler.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/handler.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,889 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2007 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.handler + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + A wrapper class. It wraps round all other classes and does some + dependencies checks. + + Additionally it communicates with the PostgreSQL database, creates + or deletes directories of domains or users. +""" + +import os +import re + +from shutil import rmtree +from subprocess import Popen, PIPE + +from VirtualMailManager.account import Account +from VirtualMailManager.alias import Alias +from VirtualMailManager.aliasdomain import AliasDomain +from VirtualMailManager.catchall import CatchallAlias +from VirtualMailManager.common import exec_ok, lisdir +from VirtualMailManager.config import Config as Cfg +from VirtualMailManager.constants import MIN_GID, MIN_UID, \ + ACCOUNT_EXISTS, ALIAS_EXISTS, CONF_NOFILE, CONF_NOPERM, CONF_WRONGPERM, \ + DATABASE_ERROR, DOMAINDIR_GROUP_MISMATCH, DOMAIN_INVALID, \ + FOUND_DOTS_IN_PATH, INVALID_ARGUMENT, MAILDIR_PERM_MISMATCH, \ + NOT_EXECUTABLE, NO_SUCH_ACCOUNT, NO_SUCH_ALIAS, NO_SUCH_BINARY, \ + NO_SUCH_DIRECTORY, NO_SUCH_RELOCATED, RELOCATED_EXISTS, UNKNOWN_SERVICE, \ + VMM_ERROR, LOCALPART_INVALID, TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED +from VirtualMailManager.domain import Domain +from VirtualMailManager.emailaddress import DestinationEmailAddress, \ + EmailAddress, RE_LOCALPART +from VirtualMailManager.errors import \ + DomainError, NotRootError, PermissionError, VMMError +from VirtualMailManager.mailbox import new as new_mailbox +from VirtualMailManager.pycompat import all, any +from VirtualMailManager.quotalimit import QuotaLimit +from VirtualMailManager.relocated import Relocated +from VirtualMailManager.serviceset import ServiceSet, SERVICES +from VirtualMailManager.transport import Transport + + +_ = lambda msg: msg +_db_mod = None + +CFG_FILE = 'vmm.cfg' +CFG_PATH = '/root:/usr/local/etc:/etc' +RE_DOMAIN_SEARCH = """^[a-z0-9-\.]+$""" +OTHER_TYPES = { + TYPE_ACCOUNT: (_(u'an account'), ACCOUNT_EXISTS), + TYPE_ALIAS: (_(u'an alias'), ALIAS_EXISTS), + TYPE_RELOCATED: (_(u'a relocated user'), RELOCATED_EXISTS), +} + +class Handler(object): + """Wrapper class to simplify the access on all the stuff from + VirtualMailManager""" + __slots__ = ('_cfg', '_cfg_fname', '_db_connect', '_dbh', '_warnings') + + def __init__(self, skip_some_checks=False): + """Creates a new Handler instance. + + ``skip_some_checks`` : bool + When a derived class knows how to handle all checks this + argument may be ``True``. By default it is ``False`` and + all checks will be performed. + + Throws a NotRootError if your uid is greater 0. + """ + self._cfg_fname = '' + self._warnings = [] + self._cfg = None + self._dbh = None + self._db_connect = None + + if os.geteuid(): + raise NotRootError(_(u"You are not root.\n\tGood bye!\n"), + CONF_NOPERM) + if self._check_cfg_file(): + self._cfg = Cfg(self._cfg_fname) + self._cfg.load() + if not skip_some_checks: + self._cfg.check() + self._chkenv() + self._set_db_connect() + + def _find_cfg_file(self): + """Search the CFG_FILE in CFG_PATH. + Raise a VMMError when no vmm.cfg could be found. + """ + for path in CFG_PATH.split(':'): + tmp = os.path.join(path, CFG_FILE) + if os.path.isfile(tmp): + self._cfg_fname = tmp + break + if not self._cfg_fname: + raise VMMError(_(u"Could not find '%(cfg_file)s' in: " + u"'%(cfg_path)s'") % {'cfg_file': CFG_FILE, + 'cfg_path': CFG_PATH}, CONF_NOFILE) + + def _check_cfg_file(self): + """Checks the configuration file, returns bool""" + self._find_cfg_file() + fstat = os.stat(self._cfg_fname) + fmode = int(oct(fstat.st_mode & 0777)) + if fmode % 100 and fstat.st_uid != fstat.st_gid or \ + fmode % 10 and fstat.st_uid == fstat.st_gid: + # TP: Please keep the backticks around the command. `chmod 0600 …` + raise PermissionError(_(u"wrong permissions for '%(file)s': " + u"%(perms)s\n`chmod 0600 %(file)s` would " + u"be great.") % {'file': self._cfg_fname, + 'perms': fmode}, CONF_WRONGPERM) + else: + return True + + def _chkenv(self): + """Make sure our base_directory is a directory and that all + required executables exists and are executable. + If not, a VMMError will be raised""" + dir_created = False + basedir = self._cfg.dget('misc.base_directory') + if not os.path.exists(basedir): + old_umask = os.umask(0006) + os.makedirs(basedir, 0771) + os.chown(basedir, 0, 0) + os.umask(old_umask) + dir_created = True + if not dir_created and not lisdir(basedir): + raise VMMError(_(u"'%(path)s' is not a directory.\n(%(cfg_file)s: " + u"section 'misc', option 'base_directory')") % + {'path': basedir, 'cfg_file': self._cfg_fname}, + NO_SUCH_DIRECTORY) + for opt, val in self._cfg.items('bin'): + try: + exec_ok(val) + except VMMError, err: + if err.code in (NO_SUCH_BINARY, NOT_EXECUTABLE): + raise VMMError(err.msg + _(u"\n(%(cfg_file)s: section " + u"'bin', option '%(option)s')") % + {'cfg_file': self._cfg_fname, + 'option': opt}, err.code) + else: + raise + + def _set_db_connect(self): + """check which module to use and set self._db_connect""" + global _db_mod + if self._cfg.dget('database.module').lower() == 'psycopg2': + try: + _db_mod = __import__('psycopg2') + except ImportError: + raise VMMError(_(u"Unable to import database module '%s'.") % + 'psycopg2', VMM_ERROR) + self._db_connect = self._psycopg2_connect + else: + try: + tmp = __import__('pyPgSQL', globals(), locals(), ['PgSQL']) + except ImportError: + raise VMMError(_(u"Unable to import database module '%s'.") % + 'pyPgSQL', VMM_ERROR) + _db_mod = tmp.PgSQL + self._db_connect = self._pypgsql_connect + + def _pypgsql_connect(self): + """Creates a pyPgSQL.PgSQL.connection instance.""" + if self._dbh is None or (isinstance(self._dbh, _db_mod.Connection) and + not self._dbh._isOpen): + try: + self._dbh = _db_mod.connect( + database=self._cfg.dget('database.name'), + user=self._cfg.pget('database.user'), + host=self._cfg.dget('database.host'), + port=self._cfg.dget('database.port'), + password=self._cfg.pget('database.pass'), + client_encoding='utf8', unicode_results=True) + dbc = self._dbh.cursor() + dbc.execute("SET NAMES 'UTF8'") + dbc.close() + except _db_mod.libpq.DatabaseError, err: + raise VMMError(str(err), DATABASE_ERROR) + + def _psycopg2_connect(self): + """Return a new psycopg2 connection object.""" + if self._dbh is None or \ + (isinstance(self._dbh, _db_mod.extensions.connection) and + self._dbh.closed): + try: + self._dbh = _db_mod.connect( + host=self._cfg.dget('database.host'), + sslmode=self._cfg.dget('database.sslmode'), + port=self._cfg.dget('database.port'), + database=self._cfg.dget('database.name'), + user=self._cfg.pget('database.user'), + password=self._cfg.pget('database.pass')) + self._dbh.set_client_encoding('utf8') + _db_mod.extensions.register_type(_db_mod.extensions.UNICODE) + dbc = self._dbh.cursor() + dbc.execute("SET NAMES 'UTF8'") + dbc.close() + except _db_mod.DatabaseError, err: + raise VMMError(str(err), DATABASE_ERROR) + + def _chk_other_address_types(self, address, exclude): + """Checks if the EmailAddress *address* is known as `TYPE_ACCOUNT`, + `TYPE_ALIAS` or `TYPE_RELOCATED`, but not as the `TYPE_*` specified + by *exclude*. If the *address* is known as one of the `TYPE_*`s + the according `TYPE_*` constant will be returned. Otherwise 0 will + be returned.""" + assert exclude in (TYPE_ACCOUNT, TYPE_ALIAS, TYPE_RELOCATED) and \ + isinstance(address, EmailAddress) + if exclude is not TYPE_ACCOUNT: + account = Account(self._dbh, address) + if account: + return TYPE_ACCOUNT + if exclude is not TYPE_ALIAS: + alias = Alias(self._dbh, address) + if alias: + return TYPE_ALIAS + if exclude is not TYPE_RELOCATED: + relocated = Relocated(self._dbh, address) + if relocated: + return TYPE_RELOCATED + return 0 + + def _is_other_address(self, address, exclude): + """Checks if *address* is known for an Account (TYPE_ACCOUNT), + Alias (TYPE_ALIAS) or Relocated (TYPE_RELOCATED), except for + *exclude*. Returns `False` if the address is not known for other + types. + + Raises a `VMMError` if the address is known. + """ + other = self._chk_other_address_types(address, exclude) + if not other: + return False + # TP: %(a_type)s will be one of: 'an account', 'an alias' or + # 'a relocated user' + msg = _(u"There is already %(a_type)s with the address '%(address)s'.") + raise VMMError(msg % {'a_type': OTHER_TYPES[other][0], + 'address': address}, OTHER_TYPES[other][1]) + + def _get_account(self, address): + """Return an Account instances for the given address (str).""" + address = EmailAddress(address) + self._db_connect() + return Account(self._dbh, address) + + def _get_alias(self, address): + """Return an Alias instances for the given address (str).""" + address = EmailAddress(address) + self._db_connect() + return Alias(self._dbh, address) + + def _get_catchall(self, domain): + """Return a CatchallAlias instances for the given domain (str).""" + self._db_connect() + return CatchallAlias(self._dbh, domain) + + def _get_relocated(self, address): + """Return a Relocated instances for the given address (str).""" + address = EmailAddress(address) + self._db_connect() + return Relocated(self._dbh, address) + + def _get_domain(self, domainname): + """Return a Domain instances for the given domain name (str).""" + self._db_connect() + return Domain(self._dbh, domainname) + + def _get_disk_usage(self, directory): + """Estimate file space usage for the given directory. + + Arguments: + + `directory` : basestring + The directory to summarize recursively disk usage for + """ + if lisdir(directory): + return Popen([self._cfg.dget('bin.du'), "-hs", directory], + stdout=PIPE).communicate()[0].split('\t')[0] + else: + self._warnings.append(_('No such directory: %s') % directory) + return 0 + + def _make_domain_dir(self, domain): + """Create a directory for the `domain` and its accounts.""" + cwd = os.getcwd() + hashdir, domdir = domain.directory.split(os.path.sep)[-2:] + dir_created = False + os.chdir(self._cfg.dget('misc.base_directory')) + old_umask = os.umask(0022) + if not os.path.exists(hashdir): + os.mkdir(hashdir, 0711) + os.chown(hashdir, 0, 0) + dir_created = True + if not dir_created and not lisdir(hashdir): + raise VMMError(_(u"'%s' is not a directory.") % hashdir, + NO_SUCH_DIRECTORY) + if os.path.exists(domain.directory): + raise VMMError(_(u"The file/directory '%s' already exists.") % + domain.directory, VMM_ERROR) + os.mkdir(os.path.join(hashdir, domdir), + self._cfg.dget('domain.directory_mode')) + os.chown(domain.directory, 0, domain.gid) + os.umask(old_umask) + os.chdir(cwd) + + def _make_home(self, account): + """Create a home directory for the new Account *account*.""" + domdir = account.domain.directory + if not lisdir(domdir): + self._make_domain_dir(account.domain) + os.umask(0007) + uid = account.uid + os.chdir(domdir) + os.mkdir('%s' % uid, self._cfg.dget('account.directory_mode')) + os.chown('%s' % uid, uid, account.gid) + + def _make_account_dirs(self, account): + """Create all necessary directories for the account.""" + oldpwd = os.getcwd() + self._make_home(account) + mailbox = new_mailbox(account) + mailbox.create() + folders = self._cfg.dget('mailbox.folders').split(':') + if any(folders): + bad = mailbox.add_boxes(folders, + self._cfg.dget('mailbox.subscribe')) + if bad: + self._warnings.append(_(u"Skipped mailbox folders:") + + '\n\t- ' + '\n\t- '.join(bad)) + os.chdir(oldpwd) + + def _delete_home(self, domdir, uid, gid): + """Delete a user's home directory. + + Arguments: + + `domdir` : basestring + The directory of the domain the user belongs to + (commonly AccountObj.domain.directory) + `uid` : int/long + The user's UID (commonly AccountObj.uid) + `gid` : int/long + The user's GID (commonly AccountObj.gid) + """ + assert all(isinstance(xid, (long, int)) for xid in (uid, gid)) and \ + isinstance(domdir, basestring) + if uid < MIN_UID or gid < MIN_GID: + raise VMMError(_(u"UID '%(uid)u' and/or GID '%(gid)u' are less " + u"than %(min_uid)u/%(min_gid)u.") % {'uid': uid, + 'gid': gid, 'min_gid': MIN_GID, 'min_uid': MIN_UID}, + MAILDIR_PERM_MISMATCH) + if domdir.count('..'): + raise VMMError(_(u'Found ".." in domain directory path: %s') % + domdir, FOUND_DOTS_IN_PATH) + if not lisdir(domdir): + raise VMMError(_(u"No such directory: %s") % domdir, + NO_SUCH_DIRECTORY) + os.chdir(domdir) + userdir = '%s' % uid + if not lisdir(userdir): + self._warnings.append(_(u"No such directory: %s") % + os.path.join(domdir, userdir)) + return + mdstat = os.lstat(userdir) + if (mdstat.st_uid, mdstat.st_gid) != (uid, gid): + raise VMMError(_(u'Detected owner/group mismatch in home ' + u'directory.'), MAILDIR_PERM_MISMATCH) + rmtree(userdir, ignore_errors=True) + + def _delete_domain_dir(self, domdir, gid): + """Delete a domain's directory. + + Arguments: + + `domdir` : basestring + The domain's directory (commonly DomainObj.directory) + `gid` : int/long + The domain's GID (commonly DomainObj.gid) + """ + assert isinstance(domdir, basestring) and isinstance(gid, (long, int)) + if gid < MIN_GID: + raise VMMError(_(u"GID '%(gid)u' is less than '%(min_gid)u'.") % + {'gid': gid, 'min_gid': MIN_GID}, + DOMAINDIR_GROUP_MISMATCH) + if domdir.count('..'): + raise VMMError(_(u'Found ".." in domain directory path: %s') % + domdir, FOUND_DOTS_IN_PATH) + if not lisdir(domdir): + self._warnings.append(_('No such directory: %s') % domdir) + return + dirst = os.lstat(domdir) + if dirst.st_gid != gid: + raise VMMError(_(u'Detected group mismatch in domain directory: ' + u'%s') % domdir, DOMAINDIR_GROUP_MISMATCH) + rmtree(domdir, ignore_errors=True) + + def has_warnings(self): + """Checks if warnings are present, returns bool.""" + return bool(len(self._warnings)) + + def get_warnings(self): + """Returns a list with all available warnings and resets all + warnings. + """ + ret_val = self._warnings[:] + del self._warnings[:] + return ret_val + + def cfg_dget(self, option): + """Get the configured value of the *option* (section.option). + When the option was not configured its default value will be + returned.""" + return self._cfg.dget(option) + + def cfg_pget(self, option): + """Get the configured value of the *option* (section.option).""" + return self._cfg.pget(option) + + def cfg_install(self): + """Installs the cfg_dget method as ``cfg_dget`` into the built-in + namespace.""" + import __builtin__ + assert 'cfg_dget' not in __builtin__.__dict__ + __builtin__.__dict__['cfg_dget'] = self._cfg.dget + + def domain_add(self, domainname, transport=None): + """Wrapper around Domain's set_quotalimit, set_transport and save.""" + dom = self._get_domain(domainname) + if transport is None: + dom.set_transport(Transport(self._dbh, + transport=self._cfg.dget('domain.transport'))) + else: + dom.set_transport(Transport(self._dbh, transport=transport)) + dom.set_quotalimit(QuotaLimit(self._dbh, + bytes=long(self._cfg.dget('domain.quota_bytes')), + messages=self._cfg.dget('domain.quota_messages'))) + dom.set_serviceset(ServiceSet(self._dbh, + imap=self._cfg.dget('domain.imap'), + pop3=self._cfg.dget('domain.pop3'), + sieve=self._cfg.dget('domain.sieve'), + smtp=self._cfg.dget('domain.smtp'))) + dom.set_directory(self._cfg.dget('misc.base_directory')) + dom.save() + self._make_domain_dir(dom) + + def domain_quotalimit(self, domainname, bytes_, messages=0, force=None): + """Wrapper around Domain.update_quotalimit().""" + if not all(isinstance(i, (int, long)) for i in (bytes_, messages)): + raise TypeError("'bytes_' and 'messages' have to be " + "integers or longs.") + if force is not None and force != 'force': + raise DomainError(_(u"Invalid argument: '%s'") % force, + INVALID_ARGUMENT) + dom = self._get_domain(domainname) + quotalimit = QuotaLimit(self._dbh, bytes=bytes_, messages=messages) + if force is None: + dom.update_quotalimit(quotalimit) + else: + dom.update_quotalimit(quotalimit, force=True) + + def domain_services(self, domainname, force=None, *services): + """Wrapper around Domain.update_serviceset().""" + kwargs = dict.fromkeys(SERVICES, False) + if force is not None and force != 'force': + raise DomainError(_(u"Invalid argument: '%s'") % force, + INVALID_ARGUMENT) + for service in set(services): + if service not in SERVICES: + raise DomainError(_(u"Unknown service: '%s'") % service, + UNKNOWN_SERVICE) + kwargs[service] = True + + dom = self._get_domain(domainname) + serviceset = ServiceSet(self._dbh, **kwargs) + dom.update_serviceset(serviceset, (True, False)[not force]) + + def domain_transport(self, domainname, transport, force=None): + """Wrapper around Domain.update_transport()""" + if force is not None and force != 'force': + raise DomainError(_(u"Invalid argument: '%s'") % force, + INVALID_ARGUMENT) + dom = self._get_domain(domainname) + trsp = Transport(self._dbh, transport=transport) + if force is None: + dom.update_transport(trsp) + else: + dom.update_transport(trsp, force=True) + + def domain_note(self, domainname, note): + """Wrapper around Domain.update_note()""" + dom = self._get_domain(domainname) + dom.update_note(note) + + def domain_delete(self, domainname, force=False): + """Wrapper around Domain.delete()""" + if not isinstance(force, bool): + raise TypeError('force must be a bool') + dom = self._get_domain(domainname) + gid = dom.gid + domdir = dom.directory + if self._cfg.dget('domain.force_deletion') or force: + dom.delete(True) + else: + dom.delete(False) + if self._cfg.dget('domain.delete_directory'): + self._delete_domain_dir(domdir, gid) + + def domain_info(self, domainname, details=None): + """Wrapper around Domain.get_info(), Domain.get_accounts(), + Domain.get_aliase_names(), Domain.get_aliases() and + Domain.get_relocated.""" + if details not in [None, 'accounts', 'aliasdomains', 'aliases', 'full', + 'relocated', 'catchall']: + raise VMMError(_(u"Invalid argument: '%s'") % details, + INVALID_ARGUMENT) + dom = self._get_domain(domainname) + dominfo = dom.get_info() + if dominfo['domain name'].startswith('xn--'): + dominfo['domain name'] += ' (%s)' % \ + dominfo['domain name'].decode('idna') + if details is None: + return dominfo + elif details == 'accounts': + return (dominfo, dom.get_accounts()) + elif details == 'aliasdomains': + return (dominfo, dom.get_aliase_names()) + elif details == 'aliases': + return (dominfo, dom.get_aliases()) + elif details == 'relocated': + return(dominfo, dom.get_relocated()) + elif details == 'catchall': + return(dominfo, dom.get_catchall()) + else: + return (dominfo, dom.get_aliase_names(), dom.get_accounts(), + dom.get_aliases(), dom.get_relocated(), dom.get_catchall()) + + def aliasdomain_add(self, aliasname, domainname): + """Adds an alias domain to the domain. + + Arguments: + + `aliasname` : basestring + The name of the alias domain + `domainname` : basestring + The name of the target domain + """ + dom = self._get_domain(domainname) + alias_dom = AliasDomain(self._dbh, aliasname) + alias_dom.set_destination(dom) + alias_dom.save() + + def aliasdomain_info(self, aliasname): + """Returns a dict (keys: "alias" and "domain") with the names of + the alias domain and its primary domain.""" + self._db_connect() + alias_dom = AliasDomain(self._dbh, aliasname) + return alias_dom.info() + + def aliasdomain_switch(self, aliasname, domainname): + """Modifies the target domain of an existing alias domain. + + Arguments: + + `aliasname` : basestring + The name of the alias domain + `domainname` : basestring + The name of the new target domain + """ + dom = self._get_domain(domainname) + alias_dom = AliasDomain(self._dbh, aliasname) + alias_dom.set_destination(dom) + alias_dom.switch() + + def aliasdomain_delete(self, aliasname): + """Deletes the given alias domain. + + Argument: + + `aliasname` : basestring + The name of the alias domain + """ + self._db_connect() + alias_dom = AliasDomain(self._dbh, aliasname) + alias_dom.delete() + + def domain_list(self, pattern=None): + """Wrapper around function search() from module Domain.""" + from VirtualMailManager.domain import search + like = False + if pattern and (pattern.startswith('%') or pattern.endswith('%')): + like = True + if not re.match(RE_DOMAIN_SEARCH, pattern.strip('%')): + raise VMMError(_(u"The pattern '%s' contains invalid " + u"characters.") % pattern, DOMAIN_INVALID) + self._db_connect() + return search(self._dbh, pattern=pattern, like=like) + + def address_list(self, typelimit, pattern=None): + """TODO""" + llike = dlike = False + lpattern = dpattern = None + if pattern: + parts = pattern.split('@', 2) + if len(parts) == 2: + # The pattern includes '@', so let's treat the + # parts separately to allow for pattern search like %@domain.% + lpattern = parts[0] + llike = lpattern.startswith('%') or lpattern.endswith('%') + dpattern = parts[1] + dlike = dpattern.startswith('%') or dpattern.endswith('%') + + if llike: + checkp = lpattern.strip('%') + else: + checkp = lpattern + if len(checkp) > 0 and re.search(RE_LOCALPART, checkp): + raise VMMError(_(u"The pattern '%s' contains invalid " + u"characters.") % pattern, LOCALPART_INVALID) + else: + # else just match on domains + # (or should that be local part, I don't know…) + dpattern = parts[0] + dlike = dpattern.startswith('%') or dpattern.endswith('%') + + if dlike: + checkp = dpattern.strip('%') + else: + checkp = dpattern + if len(checkp) > 0 and not re.match(RE_DOMAIN_SEARCH, checkp): + raise VMMError(_(u"The pattern '%s' contains invalid " + u"characters.") % pattern, DOMAIN_INVALID) + self._db_connect() + from VirtualMailManager.common import search_addresses + return search_addresses(self._dbh, typelimit=typelimit, + lpattern=lpattern, llike=llike, + dpattern=dpattern, dlike=dlike) + + def user_add(self, emailaddress, password): + """Wrapper around Account.set_password() and Account.save().""" + acc = self._get_account(emailaddress) + if acc: + raise VMMError(_(u"The account '%s' already exists.") % + acc.address, ACCOUNT_EXISTS) + self._is_other_address(acc.address, TYPE_ACCOUNT) + acc.set_password(password) + acc.save() + self._make_account_dirs(acc) + + def alias_add(self, aliasaddress, *targetaddresses): + """Creates a new `Alias` entry for the given *aliasaddress* with + the given *targetaddresses*.""" + alias = self._get_alias(aliasaddress) + if not alias: + self._is_other_address(alias.address, TYPE_ALIAS) + destinations = [DestinationEmailAddress(addr, self._dbh) \ + for addr in targetaddresses] + warnings = [] + destinations = alias.add_destinations(destinations, warnings) + if warnings: + self._warnings.append(_('Ignored destination addresses:')) + self._warnings.extend((' * %s' % w for w in warnings)) + for destination in destinations: + if destination.gid and \ + not self._chk_other_address_types(destination, TYPE_RELOCATED): + self._warnings.append(_(u"The destination account/alias '%s' " + u"does not exist.") % destination) + + def user_delete(self, emailaddress, force=False): + """Wrapper around Account.delete(...)""" + if not isinstance(force, bool): + raise TypeError('force must be a bool') + acc = self._get_account(emailaddress) + if not acc: + raise VMMError(_(u"The account '%s' does not exist.") % + acc.address, NO_SUCH_ACCOUNT) + uid = acc.uid + gid = acc.gid + dom_dir = acc.domain.directory + acc_dir = acc.home + acc.delete(force) + if self._cfg.dget('account.delete_directory'): + try: + self._delete_home(dom_dir, uid, gid) + except VMMError, err: + if err.code in (FOUND_DOTS_IN_PATH, MAILDIR_PERM_MISMATCH, + NO_SUCH_DIRECTORY): + warning = _(u"""\ +The account has been successfully deleted from the database. + But an error occurred while deleting the following directory: + '%(directory)s' + Reason: %(reason)s""") % {'directory': acc_dir, 'reason': err.msg} + self._warnings.append(warning) + else: + raise + + def alias_info(self, aliasaddress): + """Returns an iterator object for all destinations (`EmailAddress` + instances) for the `Alias` with the given *aliasaddress*.""" + alias = self._get_alias(aliasaddress) + if alias: + return alias.get_destinations() + if not self._is_other_address(alias.address, TYPE_ALIAS): + raise VMMError(_(u"The alias '%s' does not exist.") % + alias.address, NO_SUCH_ALIAS) + + def alias_delete(self, aliasaddress, targetaddress=None): + """Deletes the `Alias` *aliasaddress* with all its destinations from + the database. If *targetaddress* is not ``None``, only this + destination will be removed from the alias.""" + alias = self._get_alias(aliasaddress) + if targetaddress is None: + alias.delete() + else: + alias.del_destination(DestinationEmailAddress(targetaddress, + self._dbh)) + + def catchall_add(self, domain, *targetaddresses): + """Creates a new `CatchallAlias` entry for the given *domain* with + the given *targetaddresses*.""" + catchall = self._get_catchall(domain) + destinations = [DestinationEmailAddress(addr, self._dbh) \ + for addr in targetaddresses] + warnings = [] + destinations = catchall.add_destinations(destinations, warnings) + if warnings: + self._warnings.append(_('Ignored destination addresses:')) + self._warnings.extend((' * %s' % w for w in warnings)) + for destination in destinations: + if destination.gid and \ + not self._chk_other_address_types(destination, TYPE_RELOCATED): + self._warnings.append(_(u"The destination account/alias '%s' " + u"does not exist.") % destination) + + def catchall_info(self, domain): + """Returns an iterator object for all destinations (`EmailAddress` + instances) for the `CatchallAlias` with the given *domain*.""" + return self._get_catchall(domain).get_destinations() + + def catchall_delete(self, domain, targetaddress=None): + """Deletes the `CatchallAlias` for domain *domain* with all its + destinations from the database. If *targetaddress* is not ``None``, + only this destination will be removed from the alias.""" + catchall = self._get_catchall(domain) + if targetaddress is None: + catchall.delete() + else: + catchall.del_destination(DestinationEmailAddress(targetaddress, + self._dbh)) + + def user_info(self, emailaddress, details=None): + """Wrapper around Account.get_info(...)""" + if details not in (None, 'du', 'aliases', 'full'): + raise VMMError(_(u"Invalid argument: '%s'") % details, + INVALID_ARGUMENT) + acc = self._get_account(emailaddress) + if not acc: + if not self._is_other_address(acc.address, TYPE_ACCOUNT): + raise VMMError(_(u"The account '%s' does not exist.") % + acc.address, NO_SUCH_ACCOUNT) + info = acc.get_info() + if self._cfg.dget('account.disk_usage') or details in ('du', 'full'): + path = os.path.join(acc.home, acc.mail_location.directory) + info['disk usage'] = self._get_disk_usage(path) + if details in (None, 'du'): + return info + if details in ('aliases', 'full'): + return (info, acc.get_aliases()) + return info + + def user_by_uid(self, uid): + """Search for an Account by its *uid*. + Returns a dict (address, uid and gid) if a user could be found.""" + from VirtualMailManager.account import get_account_by_uid + self._db_connect() + return get_account_by_uid(uid, self._dbh) + + def user_password(self, emailaddress, password): + """Wrapper for Account.modify('password' ...).""" + if not isinstance(password, basestring) or not password: + raise VMMError(_(u"Could not accept password: '%s'") % password, + INVALID_ARGUMENT) + acc = self._get_account(emailaddress) + if not acc: + raise VMMError(_(u"The account '%s' does not exist.") % + acc.address, NO_SUCH_ACCOUNT) + acc.modify('password', password) + + def user_name(self, emailaddress, name): + """Wrapper for Account.modify('name', ...).""" + acc = self._get_account(emailaddress) + if not acc: + raise VMMError(_(u"The account '%s' does not exist.") % + acc.address, NO_SUCH_ACCOUNT) + acc.modify('name', name) + + def user_note(self, emailaddress, note): + """Wrapper for Account.modify('note', ...).""" + acc = self._get_account(emailaddress) + if not acc: + raise VMMError(_(u"The account '%s' does not exist.") % + acc.address, NO_SUCH_ACCOUNT) + acc.modify('note', note) + + def user_quotalimit(self, emailaddress, bytes_, messages=0): + """Wrapper for Account.update_quotalimit(QuotaLimit).""" + acc = self._get_account(emailaddress) + if not acc: + raise VMMError(_(u"The account '%s' does not exist.") % + acc.address, NO_SUCH_ACCOUNT) + if bytes_ == 'default': + quotalimit = None + else: + if not all(isinstance(i, (int, long)) for i in (bytes_, messages)): + raise TypeError("'bytes_' and 'messages' have to be " + "integers or longs.") + quotalimit = QuotaLimit(self._dbh, bytes=bytes_, + messages=messages) + acc.update_quotalimit(quotalimit) + + def user_transport(self, emailaddress, transport): + """Wrapper for Account.update_transport(Transport).""" + if not isinstance(transport, basestring) or not transport: + raise VMMError(_(u"Could not accept transport: '%s'") % transport, + INVALID_ARGUMENT) + acc = self._get_account(emailaddress) + if not acc: + raise VMMError(_(u"The account '%s' does not exist.") % + acc.address, NO_SUCH_ACCOUNT) + if transport == 'default': + transport = None + else: + transport = Transport(self._dbh, transport=transport) + acc.update_transport(transport) + + def user_services(self, emailaddress, *services): + """Wrapper around Account.update_serviceset().""" + acc = self._get_account(emailaddress) + if not acc: + raise VMMError(_(u"The account '%s' does not exist.") % + acc.address, NO_SUCH_ACCOUNT) + if len(services) == 1 and services[0] == 'default': + serviceset = None + else: + kwargs = dict.fromkeys(SERVICES, False) + for service in set(services): + if service not in SERVICES: + raise VMMError(_(u"Unknown service: '%s'") % service, + UNKNOWN_SERVICE) + kwargs[service] = True + serviceset = ServiceSet(self._dbh, **kwargs) + acc.update_serviceset(serviceset) + + def relocated_add(self, emailaddress, targetaddress): + """Creates a new `Relocated` entry in the database. If there is + already a relocated user with the given *emailaddress*, only the + *targetaddress* for the relocated user will be updated.""" + relocated = self._get_relocated(emailaddress) + if not relocated: + self._is_other_address(relocated.address, TYPE_RELOCATED) + destination = DestinationEmailAddress(targetaddress, self._dbh) + relocated.set_destination(destination) + if destination.gid and \ + not self._chk_other_address_types(destination, TYPE_RELOCATED): + self._warnings.append(_(u"The destination account/alias '%s' " + u"does not exist.") % destination) + + def relocated_info(self, emailaddress): + """Returns the target address of the relocated user with the given + *emailaddress*.""" + relocated = self._get_relocated(emailaddress) + if relocated: + return relocated.get_info() + if not self._is_other_address(relocated.address, TYPE_RELOCATED): + raise VMMError(_(u"The relocated user '%s' does not exist.") % + relocated.address, NO_SUCH_RELOCATED) + + def relocated_delete(self, emailaddress): + """Deletes the relocated user with the given *emailaddress* from + the database.""" + relocated = self._get_relocated(emailaddress) + relocated.delete() + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/mailbox.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/mailbox.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,294 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2010 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.mailbox + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + VirtualMailManager's mailbox classes for the Maildir, single dbox + (sdbox) and multi dbox (mdbox) mailbox formats. +""" + +import os +import re +from binascii import a2b_base64, b2a_base64 +from subprocess import Popen, PIPE + +from VirtualMailManager.account import Account +from VirtualMailManager.common import lisdir +from VirtualMailManager.errors import VMMError +from VirtualMailManager.constants import VMM_ERROR + + +__all__ = ('new', 'Maildir', 'SingleDbox', 'MultiDbox', + 'utf8_to_mutf7', 'mutf7_to_utf8') + +_ = lambda msg: msg +cfg_dget = lambda option: None + + +def _mbase64_encode(inp, dest): + if inp: + mb64 = b2a_base64(''.join(inp).encode('utf-16be')) + dest.append('&%s-' % mb64.rstrip('\n=').replace('/', ',')) + del inp[:] + + +def _mbase64_to_unicode(mb64): + return unicode(a2b_base64(mb64.replace(',', '/') + '==='), 'utf-16be') + + +def utf8_to_mutf7(src): + """ + Converts the international mailbox name `src` into a modified + version version of the UTF-7 encoding. + """ + ret = [] + tmp = [] + for c in src: + ordc = ord(c) + if 0x20 <= ordc <= 0x25 or 0x27 <= ordc <= 0x7E: + _mbase64_encode(tmp, ret) + ret.append(c) + elif ordc == 0x26: + _mbase64_encode(tmp, ret) + ret.append('&-') + else: + tmp.append(c) + _mbase64_encode(tmp, ret) + return ''.join(ret) + + +def mutf7_to_utf8(src): + """ + Converts the mailbox name `src` from modified UTF-7 encoding to UTF-8. + """ + ret = [] + tmp = [] + for c in src: + if c == '&' and not tmp: + tmp.append(c) + elif c == '-' and tmp: + if len(tmp) is 1: + ret.append('&') + else: + ret.append(_mbase64_to_unicode(''.join(tmp[1:]))) + tmp = [] + elif tmp: + tmp.append(c) + else: + ret.append(c) + if tmp: + ret.append(_mbase64_to_unicode(''.join(tmp[1:]))) + return ''.join(ret) + + +class Mailbox(object): + """Base class of all mailbox classes.""" + __slots__ = ('_boxes', '_root', '_sep', '_user') + FILE_MODE = 0600 + _ctrl_chr_re = re.compile('[\x00-\x1F\x7F-\x9F]') + _box_name_re = re.compile('^[\x20-\x25\x27-\x7E]+$') + + def __init__(self, account): + """ + Creates a new mailbox instance. + Use one of the `Maildir`, `SingleDbox` or `MultiDbox` classes. + """ + assert isinstance(account, Account) and lisdir(account.home) + self._user = account + self._boxes = [] + self._root = self._user.mail_location.directory + self._sep = '/' + os.chdir(self._user.home) + + def _add_boxes(self, mailboxes, subscribe): + """Create all mailboxes from the `mailboxes` list. + If `subscribe` is *True*, the mailboxes will be listed in the + subscriptions file.""" + raise NotImplementedError + + def _validate_box_name(self, name, good, bad): + """ + Validates the mailboxes name `name`. When the name is valid, it + will be added to the `good` set. Invalid mailbox names will be + appended to the `bad` list. + """ + name = name.strip() + if not name: + return + if self.__class__._ctrl_chr_re.search(name): # no control chars + bad.append(name) + return + if name[0] in (self._sep, '~'): + bad.append(name) + return + if self._sep == '/': + if '//' in name or '/./' in name or '/../' in name or \ + name.startswith('../'): + bad.append(name) + return + elif '/' in name or '..' in name: + bad.append(name) + return + if not self.__class__._box_name_re.match(name): + tmp = utf8_to_mutf7(name) + if name == mutf7_to_utf8(tmp): + if self._user.mail_location.mbformat == 'maildir': + good.add(tmp) + else: + good.add(name) + return + else: + bad.append(name) + return + good.add(name) + + def add_boxes(self, mailboxes, subscribe): + """ + Create all mailboxes from the `mailboxes` list in the user's + mail directory. When `subscribe` is ``True`` all created mailboxes + will be listed in the subscriptions file. + Returns a list of invalid mailbox names, if any. + """ + assert isinstance(mailboxes, list) and isinstance(subscribe, bool) + good = set() + bad = [] + for box in mailboxes: + if self._sep == '/': + box = box.replace('.', self._sep) + self._validate_box_name(box, good, bad) + self._add_boxes(good, subscribe) + return bad + + def create(self): + """Create the INBOX in the user's mail directory.""" + raise NotImplementedError + + +class Maildir(Mailbox): + """Class for Maildir++ mailboxes.""" + + __slots__ = ('_subdirs') + + def __init__(self, account): + """ + Create a new Maildir++ instance. + Call the instance's create() method, in order to create the INBOX. + For additional mailboxes use the add_boxes() method. + """ + super(self.__class__, self).__init__(account) + self._sep = '.' + self._subdirs = ('cur', 'new', 'tmp') + + def _create_maildirfolder_file(self, path): + """Mark the Maildir++ folder as Maildir folder.""" + maildirfolder_file = os.path.join(self._sep + path, 'maildirfolder') + os.close(os.open(maildirfolder_file, os.O_CREAT | os.O_WRONLY, + self.__class__.FILE_MODE)) + os.chown(maildirfolder_file, self._user.uid, self._user.gid) + + def _make_maildir(self, path): + """ + Create Maildir++ folders with the cur, new and tmp subdirectories. + """ + mode = cfg_dget('account.directory_mode') + uid = self._user.uid + gid = self._user.gid + os.mkdir(path, mode) + os.chown(path, uid, gid) + for subdir in self._subdirs: + dir_ = os.path.join(path, subdir) + os.mkdir(dir_, mode) + os.chown(dir_, uid, gid) + + def _subscribe_boxes(self): + """Writes all created mailboxes to the subscriptions file.""" + if not self._boxes: + return + subscriptions = open('subscriptions', 'w') + subscriptions.write('\n'.join(self._boxes)) + subscriptions.write('\n') + subscriptions.flush() + subscriptions.close() + os.chown('subscriptions', self._user.uid, self._user.gid) + os.chmod('subscriptions', self.__class__.FILE_MODE) + del self._boxes[:] + + def _add_boxes(self, mailboxes, subscribe): + for mailbox in mailboxes: + self._make_maildir(self._sep + mailbox) + self._create_maildirfolder_file(mailbox) + self._boxes.append(mailbox) + if subscribe: + self._subscribe_boxes() + + def create(self): + """Creates a Maildir++ INBOX.""" + self._make_maildir(self._root) + os.chdir(self._root) + + +class SingleDbox(Mailbox): + """ + Class for (single) dbox mailboxes. + See http://wiki.dovecot.org/MailboxFormat/dbox for details. + """ + + __slots__ = () + + def __init__(self, account): + """ + Create a new dbox instance. + Call the instance's create() method, in order to create the INBOX. + For additional mailboxes use the add_boxes() method. + """ + assert cfg_dget('misc.dovecot_version') >= \ + account.mail_location.dovecot_version + super(SingleDbox, self).__init__(account) + + def _doveadm_create(self, mailboxes, subscribe): + """Wrap around Dovecot's doveadm""" + cmd_args = [cfg_dget('bin.dovecotpw'), 'mailbox', 'create', '-u', + str(self._user.address)] + if subscribe: + cmd_args.append('-s') + cmd_args.extend(mailboxes) + process = Popen(cmd_args, stderr=PIPE) + stderr = process.communicate()[1] + if process.returncode: + e_msg = _(u'Failed to create mailboxes: %r\n') % mailboxes + raise VMMError(e_msg + stderr.strip(), VMM_ERROR) + + def create(self): + """Create a dbox INBOX""" + os.mkdir(self._root, cfg_dget('account.directory_mode')) + os.chown(self._root, self._user.uid, self._user.gid) + self._doveadm_create(('INBOX',), False) + os.chdir(self._root) + + def _add_boxes(self, mailboxes, subscribe): + self._doveadm_create(mailboxes, subscribe) + + +class MultiDbox(SingleDbox): + """ + Class for multi dbox mailboxes. + See http://wiki.dovecot.org/MailboxFormat/dbox#Multi-dbox for details. + """ + + __slots__ = () + + +def new(account): + """Create a new Mailbox instance for the given Account.""" + mbfmt = account.mail_location.mbformat + if mbfmt == 'maildir': + return Maildir(account) + elif mbfmt == 'mdbox': + return MultiDbox(account) + elif mbfmt == 'sdbox': + return SingleDbox(account) + raise ValueError('unsupported mailbox format: %r' % mbfmt) + +del _, cfg_dget diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/maillocation.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/maillocation.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,161 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2008 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.maillocation + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Virtual Mail Manager's maillocation module to handle Dovecot's + mail_location setting for accounts. + +""" + +from VirtualMailManager.constants import MAILLOCATION_INIT +from VirtualMailManager.errors import MailLocationError as MLErr +from VirtualMailManager.pycompat import all + + +__all__ = ('MailLocation', 'known_format') + +_ = lambda msg: msg +_format_info = { + 'maildir': dict(dovecot_version=0x10000f00, postfix=True), + 'mdbox': dict(dovecot_version=0x20000b05, postfix=False), + 'sdbox': dict(dovecot_version=0x20000c03, postfix=False), +} + + +class MailLocation(object): + """Class to handle mail_location relevant information.""" + __slots__ = ('_directory', '_mbfmt', '_mid', '_dbh') + _kwargs = ('mid', 'mbfmt', 'directory') + + def __init__(self, dbh, **kwargs): + """Creates a new MailLocation instance. + + Either the mid keyword or the mbfmt and directory keywords must be + specified. + + Arguments: + + `dbh` : pyPgSQL.PgSQL.Connection + A database connection for the database access. + + Keyword arguments: + + `mid` : int + the id of a mail_location + `mbfmt` : str + the mailbox format of the mail_location. One out of: ``maildir``, + ``sdbox`` and ``mdbox``. + `directory` : str + name of the mailbox root directory. + """ + self._dbh = dbh + self._directory = None + self._mbfmt = None + self._mid = 0 + + for key in kwargs.iterkeys(): + if key not in self.__class__._kwargs: + raise ValueError('unrecognized keyword: %r' % key) + mid = kwargs.get('mid') + if mid: + assert isinstance(mid, (int, long)) + self._load_by_mid(mid) + else: + args = kwargs.get('mbfmt'), kwargs.get('directory') + assert all(isinstance(arg, basestring) for arg in args) + if args[0].lower() not in _format_info: + raise MLErr(_(u"Unsupported mailbox format: '%s'") % args[0], + MAILLOCATION_INIT) + directory = args[1].strip() + if not directory: + raise MLErr(_(u"Empty directory name"), MAILLOCATION_INIT) + if len(directory) > 20: + raise MLErr(_(u"Directory name is too long: '%s'") % directory, + MAILLOCATION_INIT) + self._load_by_names(args[0].lower(), directory) + + def __str__(self): + return u'%s:~/%s' % (self._mbfmt, self._directory) + + @property + def directory(self): + """The mail_location's directory name.""" + return self._directory + + @property + def dovecot_version(self): + """The required Dovecot version for this mailbox format.""" + return _format_info[self._mbfmt]['dovecot_version'] + + @property + def postfix(self): + """`True` if Postfix supports this mailbox format, else `False`.""" + return _format_info[self._mbfmt]['postfix'] + + @property + def mbformat(self): + """The mail_location's mailbox format.""" + return self._mbfmt + + @property + def mail_location(self): + """The mail_location, e.g. ``maildir:~/Maildir``""" + return self.__str__() + + @property + def mid(self): + """The mail_location's unique ID.""" + return self._mid + + def _load_by_mid(self, mid): + """Load mail_location relevant information by *mid*""" + dbc = self._dbh.cursor() + dbc.execute('SELECT format, directory FROM mailboxformat, ' + 'maillocation WHERE mid = %u AND ' + 'maillocation.fid = mailboxformat.fid' % mid) + result = dbc.fetchone() + dbc.close() + if not result: + raise ValueError('Unknown mail_location id specified: %r' % mid) + self._mid = mid + self._mbfmt, self._directory = result + + def _load_by_names(self, mbfmt, directory): + """Try to load mail_location relevant information by *mbfmt* and + *directory* name. If it fails goto _save().""" + dbc = self._dbh.cursor() + dbc.execute("SELECT mid FROM maillocation WHERE fid = (SELECT fid " + "FROM mailboxformat WHERE format = %s) AND directory = %s", + (mbfmt, directory)) + result = dbc.fetchone() + dbc.close() + if not result: + self._save(mbfmt, directory) + else: + self._mid = result[0] + self._mbfmt = mbfmt + self._directory = directory + + def _save(self, mbfmt, directory): + """Save a new mail_location in the database.""" + dbc = self._dbh.cursor() + dbc.execute("SELECT nextval('maillocation_id')") + mid = dbc.fetchone()[0] + dbc.execute("INSERT INTO maillocation (fid, mid, directory) VALUES (" + "(SELECT fid FROM mailboxformat WHERE format = %s), %s, " + "%s)", (mbfmt, mid, directory)) + self._dbh.commit() + dbc.close() + self._mid = mid + self._mbfmt = mbfmt + self._directory = directory + + +def known_format(mbfmt): + """Checks if the mailbox format *mbfmt* is known, returns bool.""" + return mbfmt.lower() in _format_info + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/network.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/network.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,100 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2011 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.network + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Network/IP address related class and function +""" + +import socket + + +class NetInfo(object): + """Simple class for CIDR network addresses an IP addresses.""" + __slots__ = ('_addr', '_prefix', '_bits_max', '_family', '_nw_addr') + + def __init__(self, nw_address): + """Creates a new `NetInfo` instance. + + Argument: + + `nw_address` : basestring + string representation of an IPv4/IPv6 address or network address. + E.g. 192.0.2.13, 192.0.2.0/24, 2001:db8::/32 or ::1 + When the address has no netmask the prefix length will be set to + 32 for IPv4 addresses and 128 for IPv6 addresses. + """ + self._addr = None + self._prefix = 0 + self._bits_max = 0 + self._family = 0 + self._nw_addr = nw_address + self._parse_net_range() + + def __hash__(self): + return hash((self._addr, self._family, self._prefix)) + + def __repr__(self): + return "NetInfo('%s')" % self._nw_addr + + def _parse_net_range(self): + """Parse the network range of `self._nw_addr and assign values + to the class attributes. + `""" + sep = '/' + if self._nw_addr.count(sep): + ip_address, sep, self._prefix = self._nw_addr.partition(sep) + self._family, self._addr = get_ip_addr_info(ip_address) + else: + self._family, self._addr = get_ip_addr_info(self._nw_addr) + self._bits_max = (128, 32)[self._family is socket.AF_INET] + if self._prefix is 0: + self._prefix = self._bits_max + else: + try: + self._prefix = int(self._prefix) + except ValueError: + raise ValueError('Invalid prefix length: %r' % self._prefix) + if self._prefix > self._bits_max or self._prefix < 0: + raise ValueError('Invalid prefix length: %r' % self._prefix) + + @property + def family(self): + """Address family: `socket.AF_INET` or `socket.AF_INET6`""" + return self._family + + def address_in_net(self, ip_address): + """Checks if the `ip_address` belongs to the same subnet.""" + family, address = get_ip_addr_info(ip_address) + if family != self._family: + return False + return address >> self._bits_max - self._prefix == \ + self._addr >> self._bits_max - self._prefix + + +def get_ip_addr_info(ip_address): + """Checks if the string `ip_address` is a valid IPv4 or IPv6 address. + + When the `ip_address` could be validated successfully a tuple + `(address_family, address_as_long)` will be returned. The + `address_family`will be either `socket.AF_INET` or `socket.AF_INET6`. + """ + if not isinstance(ip_address, basestring) or not ip_address: + raise TypeError('ip_address must be a non empty string.') + if not ip_address.count(':'): + family = socket.AF_INET + try: + address = socket.inet_aton(ip_address) + except socket.error: + raise ValueError('Not a valid IPv4 address: %r' % ip_address) + elif not socket.has_ipv6: + raise ValueError('Unsupported IP address (IPv6): %r' % ip_address) + else: + family = socket.AF_INET6 + try: + address = socket.inet_pton(family, ip_address) + except socket.error: + raise ValueError('Not a valid IPv6 address: %r' % ip_address) + return (family, long(address.encode('hex'), 16)) diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/password.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/password.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,459 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2010 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.password + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + VirtualMailManager's password module to generate password hashes from + passwords or random passwords. This module provides following + functions: + + hashed_password = pwhash(password[, scheme][, user]) + random_password = randompw() + scheme, encoding = verify_scheme(scheme) + schemes, encodings = list_schemes() +""" + +from crypt import crypt +from random import SystemRandom +from subprocess import Popen, PIPE + +try: + import hashlib +except ImportError: + from VirtualMailManager.pycompat import hashlib + +from VirtualMailManager import ENCODING +from VirtualMailManager.emailaddress import EmailAddress +from VirtualMailManager.common import get_unicode, version_str +from VirtualMailManager.constants import VMM_ERROR +from VirtualMailManager.errors import VMMError + +COMPAT = hasattr(hashlib, 'compat') +SALTCHARS = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' +PASSWDCHARS = '._-+#*23456789abcdefghikmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' +DEFAULT_B64 = (None, 'B64', 'BASE64') +DEFAULT_HEX = (None, 'HEX') +CRYPT_ID_MD5 = 1 +CRYPT_ID_BLF = '2a' +CRYPT_ID_SHA256 = 5 +CRYPT_ID_SHA512 = 6 +CRYPT_SALT_LEN = 2 +CRYPT_BLF_ROUNDS_MIN = 4 +CRYPT_BLF_ROUNDS_MAX = 31 +CRYPT_BLF_SALT_LEN = 22 +CRYPT_MD5_SALT_LEN = 8 +CRYPT_SHA2_ROUNDS_DEFAULT = 5000 +CRYPT_SHA2_ROUNDS_MIN = 1000 +CRYPT_SHA2_ROUNDS_MAX = 999999999 +CRYPT_SHA2_SALT_LEN = 16 +SALTED_ALGO_SALT_LEN = 4 + + +_ = lambda msg: msg +cfg_dget = lambda option: None +_sys_rand = SystemRandom() +_choice = _sys_rand.choice +_get_salt = lambda s_len: ''.join(_choice(SALTCHARS) for x in xrange(s_len)) + + +def _dovecotpw(password, scheme, encoding): + """Communicates with dovecotpw (Dovecot 2.0: `doveadm pw`) and returns + the hashed password: {scheme[.encoding]}hash + """ + if encoding: + scheme = '.'.join((scheme, encoding)) + cmd_args = [cfg_dget('bin.dovecotpw'), '-s', scheme, '-p', + get_unicode(password)] + if cfg_dget('misc.dovecot_version') >= 0x20000a01: + cmd_args.insert(1, 'pw') + process = Popen(cmd_args, stdout=PIPE, stderr=PIPE) + stdout, stderr = process.communicate() + if process.returncode: + raise VMMError(stderr.strip(), VMM_ERROR) + hashed = stdout.strip() + if not hashed.startswith('{%s}' % scheme): + raise VMMError('Unexpected result from %s: %s' % + (cfg_dget('bin.dovecotpw'), hashed), VMM_ERROR) + return hashed + + +def _md4_new(): + """Returns an new MD4-hash object if supported by the hashlib or + provided by PyCrypto - other `None`. + """ + try: + return hashlib.new('md4') + except ValueError, err: + if str(err) == 'unsupported hash type': + if not COMPAT: + try: + from Crypto.Hash import MD4 + return MD4.new() + except ImportError: + return None + else: + raise + + +def _sha256_new(data=''): + """Returns a new sha256 object from the hashlib. + + Returns `None` if the PyCrypto in pycompat.hashlib is too old.""" + if not COMPAT: + return hashlib.sha256(data) + try: + return hashlib.new('sha256', data) + except ValueError, err: + if str(err) == 'unsupported hash type': + return None + else: + raise + + +def _format_digest(digest, scheme, encoding): + """Formats the arguments to a string: {scheme[.encoding]}digest.""" + if not encoding: + return '{%s}%s' % (scheme, digest) + return '{%s.%s}%s' % (scheme, encoding, digest) + + +def _clear_hash(password, scheme, encoding): + """Generates a (encoded) CLEARTEXT/PLAIN 'hash'.""" + if encoding: + if encoding == 'HEX': + password = password.encode('hex') + else: + password = password.encode('base64').replace('\n', '') + return _format_digest(password, scheme, encoding) + return get_unicode('{%s}%s' % (scheme, password)) + + +def _get_crypt_blowfish_salt(): + """Generates a salt for Blowfish crypt.""" + rounds = cfg_dget('misc.crypt_blowfish_rounds') + if rounds < CRYPT_BLF_ROUNDS_MIN: + rounds = CRYPT_BLF_ROUNDS_MIN + elif rounds > CRYPT_BLF_ROUNDS_MAX: + rounds = CRYPT_BLF_ROUNDS_MAX + return '$%s$%02d$%s' % (CRYPT_ID_BLF, rounds, + _get_salt(CRYPT_BLF_SALT_LEN)) + + +def _get_crypt_sha2_salt(crypt_id): + """Generates a salt for crypt using the SHA-256 or SHA-512 encryption + method. + *crypt_id* must be either `5` (SHA-256) or `6` (SHA-512). + """ + assert crypt_id in (CRYPT_ID_SHA256, CRYPT_ID_SHA512), 'invalid crypt ' \ + 'id: %r' % crypt_id + if crypt_id is CRYPT_ID_SHA512: + rounds = cfg_dget('misc.crypt_sha512_rounds') + else: + rounds = cfg_dget('misc.crypt_sha256_rounds') + if rounds < CRYPT_SHA2_ROUNDS_MIN: + rounds = CRYPT_SHA2_ROUNDS_MIN + elif rounds > CRYPT_SHA2_ROUNDS_MAX: + rounds = CRYPT_SHA2_ROUNDS_MAX + if rounds == CRYPT_SHA2_ROUNDS_DEFAULT: + return '$%d$%s' % (crypt_id, _get_salt(CRYPT_SHA2_SALT_LEN)) + return '$%d$rounds=%d$%s' % (crypt_id, rounds, + _get_salt(CRYPT_SHA2_SALT_LEN)) + + +def _crypt_hash(password, scheme, encoding): + """Generates (encoded) CRYPT/MD5/{BLF,MD5,SHA{256,512}}-CRYPT hashes.""" + if scheme == 'CRYPT': + salt = _get_salt(CRYPT_SALT_LEN) + elif scheme == 'BLF-CRYPT': + salt = _get_crypt_blowfish_salt() + elif scheme in ('MD5-CRYPT', 'MD5'): + salt = '$%d$%s' % (CRYPT_ID_MD5, _get_salt(CRYPT_MD5_SALT_LEN)) + elif scheme == 'SHA256-CRYPT': + salt = _get_crypt_sha2_salt(CRYPT_ID_SHA256) + else: + salt = _get_crypt_sha2_salt(CRYPT_ID_SHA512) + encrypted = crypt(password, salt) + if encoding: + if encoding == 'HEX': + encrypted = encrypted.encode('hex') + else: + encrypted = encrypted.encode('base64').replace('\n', '') + if scheme in ('BLF-CRYPT', 'SHA256-CRYPT', 'SHA512-CRYPT') and \ + cfg_dget('misc.dovecot_version') < 0x20000b06: + scheme = 'CRYPT' + return _format_digest(encrypted, scheme, encoding) + + +def _md4_hash(password, scheme, encoding): + """Generates encoded PLAIN-MD4 hashes.""" + md4 = _md4_new() + if md4: + md4.update(password) + if encoding in DEFAULT_HEX: + digest = md4.hexdigest() + else: + digest = md4.digest().encode('base64').rstrip() + return _format_digest(digest, scheme, encoding) + return _dovecotpw(password, scheme, encoding) + + +def _md5_hash(password, scheme, encoding, user=None): + """Generates DIGEST-MD5 aka PLAIN-MD5 and LDAP-MD5 hashes.""" + md5 = hashlib.md5() + if scheme == 'DIGEST-MD5': + # Prior to Dovecot v1.1.12/v1.2.beta2 there was a problem with a + # empty auth_realms setting in dovecot.conf and user@domain.tld + # usernames. So we have to generate different hashes for different + # versions. See also: + # http://dovecot.org/list/dovecot-news/2009-March/000103.html + # http://hg.dovecot.org/dovecot-1.1/rev/2b0043ba89ae + if cfg_dget('misc.dovecot_version') >= 0x1010cf00: + md5.update('%s:%s:' % (user.localpart, user.domainname)) + else: + md5.update('%s::' % user) + md5.update(password) + if (scheme in ('PLAIN-MD5', 'DIGEST-MD5') and encoding in DEFAULT_HEX) or \ + (scheme == 'LDAP-MD5' and encoding == 'HEX'): + digest = md5.hexdigest() + else: + digest = md5.digest().encode('base64').rstrip() + return _format_digest(digest, scheme, encoding) + + +def _ntlm_hash(password, scheme, encoding): + """Generates NTLM hashes.""" + md4 = _md4_new() + if md4: + password = ''.join('%s\x00' % c for c in password) + md4.update(password) + if encoding in DEFAULT_HEX: + digest = md4.hexdigest() + else: + digest = md4.digest().encode('base64').rstrip() + return _format_digest(digest, scheme, encoding) + return _dovecotpw(password, scheme, encoding) + + +def _sha1_hash(password, scheme, encoding): + """Generates SHA1 aka SHA hashes.""" + sha1 = hashlib.sha1(password) + if encoding in DEFAULT_B64: + digest = sha1.digest().encode('base64').rstrip() + else: + digest = sha1.hexdigest() + return _format_digest(digest, scheme, encoding) + + +def _sha256_hash(password, scheme, encoding): + """Generates SHA256 hashes.""" + sha256 = _sha256_new(password) + if sha256: + if encoding in DEFAULT_B64: + digest = sha256.digest().encode('base64').rstrip() + else: + digest = sha256.hexdigest() + return _format_digest(digest, scheme, encoding) + return _dovecotpw(password, scheme, encoding) + + +def _sha512_hash(password, scheme, encoding): + """Generates SHA512 hashes.""" + if not COMPAT: + sha512 = hashlib.sha512(password) + if encoding in DEFAULT_B64: + digest = sha512.digest().encode('base64').replace('\n', '') + else: + digest = sha512.hexdigest() + return _format_digest(digest, scheme, encoding) + return _dovecotpw(password, scheme, encoding) + + +def _smd5_hash(password, scheme, encoding): + """Generates SMD5 (salted PLAIN-MD5) hashes.""" + md5 = hashlib.md5(password) + salt = _get_salt(SALTED_ALGO_SALT_LEN) + md5.update(salt) + if encoding in DEFAULT_B64: + digest = (md5.digest() + salt).encode('base64').rstrip() + else: + digest = md5.hexdigest() + salt.encode('hex') + return _format_digest(digest, scheme, encoding) + + +def _ssha1_hash(password, scheme, encoding): + """Generates SSHA (salted SHA/SHA1) hashes.""" + sha1 = hashlib.sha1(password) + salt = _get_salt(SALTED_ALGO_SALT_LEN) + sha1.update(salt) + if encoding in DEFAULT_B64: + digest = (sha1.digest() + salt).encode('base64').rstrip() + else: + digest = sha1.hexdigest() + salt.encode('hex') + return _format_digest(digest, scheme, encoding) + + +def _ssha256_hash(password, scheme, encoding): + """Generates SSHA256 (salted SHA256) hashes.""" + sha256 = _sha256_new(password) + if sha256: + salt = _get_salt(SALTED_ALGO_SALT_LEN) + sha256.update(salt) + if encoding in DEFAULT_B64: + digest = (sha256.digest() + salt).encode('base64').rstrip() + else: + digest = sha256.hexdigest() + salt.encode('hex') + return _format_digest(digest, scheme, encoding) + return _dovecotpw(password, scheme, encoding) + + +def _ssha512_hash(password, scheme, encoding): + """Generates SSHA512 (salted SHA512) hashes.""" + if not COMPAT: + salt = _get_salt(SALTED_ALGO_SALT_LEN) + sha512 = hashlib.sha512(password + salt) + if encoding in DEFAULT_B64: + digest = (sha512.digest() + salt).encode('base64').replace('\n', + '') + else: + digest = sha512.hexdigest() + salt.encode('hex') + return _format_digest(digest, scheme, encoding) + return _dovecotpw(password, scheme, encoding) + +_scheme_info = { + 'CLEARTEXT': (_clear_hash, 0x10000f00), + 'CRAM-MD5': (_dovecotpw, 0x10000f00), + 'CRYPT': (_crypt_hash, 0x10000f00), + 'DIGEST-MD5': (_md5_hash, 0x10000f00), + 'HMAC-MD5': (_dovecotpw, 0x10000f00), + 'LANMAN': (_dovecotpw, 0x10000f00), + 'LDAP-MD5': (_md5_hash, 0x10000f00), + 'MD5': (_crypt_hash, 0x10000f00), + 'MD5-CRYPT': (_crypt_hash, 0x10000f00), + 'NTLM': (_ntlm_hash, 0x10000f00), + 'OTP': (_dovecotpw, 0x10100a01), + 'PLAIN': (_clear_hash, 0x10000f00), + 'PLAIN-MD4': (_md4_hash, 0x10000f00), + 'PLAIN-MD5': (_md5_hash, 0x10000f00), + 'RPA': (_dovecotpw, 0x10000f00), + 'SHA': (_sha1_hash, 0x10000f00), + 'SHA1': (_sha1_hash, 0x10000f00), + 'SHA256': (_sha256_hash, 0x10100a01), + 'SHA512': (_sha512_hash, 0x20000b03), + 'SKEY': (_dovecotpw, 0x10100a01), + 'SMD5': (_smd5_hash, 0x10000f00), + 'SSHA': (_ssha1_hash, 0x10000f00), + 'SSHA256': (_ssha256_hash, 0x10200a04), + 'SSHA512': (_ssha512_hash, 0x20000b03), +} + + +def list_schemes(): + """Returns the tuple (schemes, encodings). + + `schemes` is an iterator for all supported password schemes (depends on + the used Dovecot version and features of the libc). + `encodings` is a tuple with all usable encoding suffixes. The tuple may + be empty. + """ + dcv = cfg_dget('misc.dovecot_version') + schemes = (k for (k, v) in _scheme_info.iteritems() if v[1] <= dcv) + if dcv >= 0x10100a01: + encodings = ('.B64', '.BASE64', '.HEX') + else: + encodings = () + return schemes, encodings + + +def verify_scheme(scheme): + """Checks if the password scheme *scheme* is known and supported by the + configured `misc.dovecot_version`. + + The *scheme* maybe a password scheme's name (e.g.: 'PLAIN') or a scheme + name with a encoding suffix (e.g. 'PLAIN.BASE64'). If the scheme is + known and supported by the used Dovecot version, + a tuple ``(scheme, encoding)`` will be returned. + The `encoding` in the tuple may be `None`. + + Raises a `VMMError` if the password scheme: + * is unknown + * depends on a newer Dovecot version + * has a unknown encoding suffix + """ + assert isinstance(scheme, basestring), 'Not a str/unicode: %r' % scheme + scheme_encoding = scheme.upper().split('.') + scheme = scheme_encoding[0] + if scheme not in _scheme_info: + raise VMMError(_(u"Unsupported password scheme: '%s'") % scheme, + VMM_ERROR) + if cfg_dget('misc.dovecot_version') < _scheme_info[scheme][1]: + raise VMMError(_(u"The password scheme '%(scheme)s' requires Dovecot " + u">= v%(version)s.") % {'scheme': scheme, + 'version': version_str(_scheme_info[scheme][1])}, + VMM_ERROR) + if len(scheme_encoding) > 1: + if cfg_dget('misc.dovecot_version') < 0x10100a01: + raise VMMError(_(u'Encoding suffixes for password schemes require ' + u'Dovecot >= v1.1.alpha1.'), VMM_ERROR) + if scheme_encoding[1] not in ('B64', 'BASE64', 'HEX'): + raise VMMError(_(u"Unsupported password encoding: '%s'") % + scheme_encoding[1], VMM_ERROR) + encoding = scheme_encoding[1] + else: + encoding = None + return scheme, encoding + + +def pwhash(password, scheme=None, user=None): + """Generates a password hash from the plain text *password* string. + + If no *scheme* is given the password scheme from the configuration will + be used for the hash generation. When 'DIGEST-MD5' is used as scheme, + also an EmailAddress instance must be given as *user* argument. + """ + if not isinstance(password, basestring): + raise TypeError('Password is not a string: %r' % password) + if isinstance(password, unicode): + password = password.encode(ENCODING) + password = password.strip() + if not password: + raise ValueError("Could not accept empty password.") + if scheme is None: + scheme = cfg_dget('misc.password_scheme') + scheme, encoding = verify_scheme(scheme) + if scheme == 'DIGEST-MD5': + assert isinstance(user, EmailAddress) + return _md5_hash(password, scheme, encoding, user) + return _scheme_info[scheme][0](password, scheme, encoding) + + +def randompw(): + """Generates a plain text random password. + + The length of the password can be configured in the ``vmm.cfg`` + (account.password_length). + """ + pw_len = cfg_dget('account.password_length') + if pw_len < 8: + pw_len = 8 + return ''.join(_sys_rand.sample(PASSWDCHARS, pw_len)) + + +def _test_crypt_algorithms(): + """Check for Blowfish/SHA-256/SHA-512 support in crypt.crypt().""" + _blowfish = '$2a$04$0123456789abcdefABCDE.N.drYX5yIAL1LkTaaZotW3yI0hQhZru' + _sha256 = '$5$rounds=1000$0123456789abcdef$K/DksR0DT01hGc8g/kt9McEgrbFMKi\ +9qrb1jehe7hn4' + _sha512 = '$6$rounds=1000$0123456789abcdef$ZIAd5WqfyLkpvsVCVUU1GrvqaZTqvh\ +JoouxdSqJO71l9Ld3tVrfOatEjarhghvEYADkq//LpDnTeO90tcbtHR1' + + if crypt('08/15!test~4711', '$2a$04$0123456789abcdefABCDEF$') == _blowfish: + _scheme_info['BLF-CRYPT'] = (_crypt_hash, 0x10000f00) + if crypt('08/15!test~4711', '$5$rounds=1000$0123456789abcdef$') == _sha256: + _scheme_info['SHA256-CRYPT'] = (_crypt_hash, 0x10000f00) + if crypt('08/15!test~4711', '$6$rounds=1000$0123456789abcdef$') == _sha512: + _scheme_info['SHA512-CRYPT'] = (_crypt_hash, 0x10000f00) + +_test_crypt_algorithms() +del _, cfg_dget, _test_crypt_algorithms diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/pycompat/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/pycompat/__init__.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,38 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2010 - 2012, Pascal Volk +# See COPYING for distribution information. + +""" + VirtualMailManager.pycompat + + VirtualMailManager's compatibility stuff for Python 2.4 +""" + +# http://docs.python.org/library/functions.html#all +try: + all = all +except NameError: + def all(iterable): + """Return True if all elements of the *iterable* are true + (or if the iterable is empty). + + """ + for element in iterable: + if not element: + return False + return True + + +# http://docs.python.org/library/functions.html#any +try: + any = any +except NameError: + def any(iterable): + """Return True if any element of the *iterable* is true. If the + iterable is empty, return False. + + """ + for element in iterable: + if element: + return True + return False diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/pycompat/hashlib.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/pycompat/hashlib.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,58 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2010 - 2012, Pascal Volk +# See COPYING for distribution information. + +""" + VirtualMailManager.pycompat.hashlib + + VirtualMailManager's minimal hashlib emulation for Python 2.4 + + hashlib.md5(...), hashlib.sha1(...), hashlib.new('md5', ...) and + hashlib.new('sha1', ...) will work always. + + When the PyCrypto module could be found in + sys.path hashlib.new('md4', ...) will also work. + + With PyCrypto >= 2.1.0alpha1 hashlib.new('sha256', ...) and + hashlib.sha256(...) becomes functional. +""" + + +import md5 as _md5 +import sha as _sha1 + +try: + import Crypto +except ImportError: + _md4 = None + SHA256 = None +else: + from Crypto.Hash import MD4 as _md4 + if hasattr(Crypto, 'version_info'): # <- Available since v2.1.0alpha1 + from Crypto.Hash import SHA256 # SHA256 works since v2.1.0alpha1 + sha256 = SHA256.new + else: + SHA256 = None + del Crypto + + +compat = 0x01 +md5 = _md5.new +sha1 = _sha1.new + + +def new(name, string=''): + """Return a new hashing object using the named algorithm, optionally + initialized with the provided string. + """ + if name in ('md5', 'MD5'): + return _md5.new(string) + if name in ('sha1', 'SHA1'): + return _sha1.new(string) + if not _md4: + raise ValueError('unsupported hash type') + if name in ('md4', 'MD4'): + return _md4.new(string) + if name in ('sha256', 'SHA256') and SHA256: + return SHA256.new(string) + raise ValueError('unsupported hash type') diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/quotalimit.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/quotalimit.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,126 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2011 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.quotalimit + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Virtual Mail Manager's QuotaLimit class to manage quota limits + for domains and accounts. +""" + +from VirtualMailManager.pycompat import all + +_ = lambda msg: msg + + +class QuotaLimit(object): + """Class to handle quota limit specific data.""" + __slots__ = ('_dbh', '_qid', '_bytes', '_messages') + _kwargs = ('qid', 'bytes', 'messages') + + def __init__(self, dbh, **kwargs): + """Create a new QuotaLimit instance. + + Either the `qid` keyword or the `bytes` and `messages` keywords + must be specified. + + Arguments: + + `dbh` : pyPgSQL.PgSQL.Connection || psycopg2._psycopg.connection + A database connection for the database access. + + Keyword arguments: + + `qid` : int + The id of a quota limit + `bytes` : long + The quota limit in bytes. + `messages` : int + The quota limit in number of messages + """ + self._dbh = dbh + self._qid = 0 + self._bytes = 0 + self._messages = 0 + + for key in kwargs.iterkeys(): + if key not in self.__class__._kwargs: + raise ValueError('unrecognized keyword: %r' % key) + qid = kwargs.get('qid') + if qid is not None: + assert isinstance(qid, (int, long)) + self._load_by_qid(qid) + else: + bytes_, msgs = kwargs.get('bytes'), kwargs.get('messages') + assert all(isinstance(i, (int, long)) for i in (bytes_, msgs)) + if bytes_ < 0: + self._bytes = -bytes_ + else: + self._bytes = bytes_ + if msgs < 0: + self._messages = -msgs + else: + self._messages = msgs + self._load_by_limit() + + @property + def bytes(self): + """Quota limit in bytes.""" + return self._bytes + + @property + def messages(self): + """Quota limit in number of messages.""" + return self._messages + + @property + def qid(self): + """The quota limit's unique ID.""" + return self._qid + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self._qid == other._qid + return NotImplemented + + def __ne__(self, other): + if isinstance(other, self.__class__): + return self._qid != other._qid + return NotImplemented + + def _load_by_limit(self): + """Load the quota limit by limit values from the database.""" + dbc = self._dbh.cursor() + dbc.execute('SELECT qid FROM quotalimit WHERE bytes = %s AND ' + 'messages = %s', (self._bytes, self._messages)) + res = dbc.fetchone() + dbc.close() + if res: + self._qid = res[0] + else: + self._save() + + def _load_by_qid(self, qid): + """Load the quota limit by its unique ID from the database.""" + dbc = self._dbh.cursor() + dbc.execute('SELECT bytes, messages FROM quotalimit WHERE qid = %s', + (qid,)) + res = dbc.fetchone() + dbc.close() + if not res: + raise ValueError('Unknown quota limit id specified: %r' % qid) + self._qid = qid + self._bytes, self._messages = res + + def _save(self): + """Store a new quota limit in the database.""" + dbc = self._dbh.cursor() + dbc.execute("SELECT nextval('quotalimit_id')") + self._qid = dbc.fetchone()[0] + dbc.execute('INSERT INTO quotalimit (qid, bytes, messages) VALUES ' + '(%s, %s, %s)', (self._qid, self._bytes, self._messages)) + self._dbh.commit() + dbc.close() + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/relocated.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/relocated.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,123 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2008 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.relocated + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Virtual Mail Manager's Relocated class to handle relocated users. +""" + +from VirtualMailManager.domain import get_gid +from VirtualMailManager.emailaddress import EmailAddress, \ + DestinationEmailAddress +from VirtualMailManager.errors import RelocatedError as RErr +from VirtualMailManager.constants import DOMAIN_INVALID, NO_SUCH_DOMAIN, \ + NO_SUCH_RELOCATED, RELOCATED_ADDR_DEST_IDENTICAL, RELOCATED_EXISTS + + +_ = lambda msg: msg + + +class Relocated(object): + """Class to handle e-mail addresses of relocated users.""" + __slots__ = ('_addr', '_dest', '_gid', '_dbh') + + def __init__(self, dbh, address): + """Creates a new *Relocated* instance. The ``address`` is the + old e-mail address of the user. + + Use `setDestination()` to set/update the new address, where the + user has moved to. + + """ + assert isinstance(address, EmailAddress) + self._addr = address + self._dbh = dbh + self._gid = get_gid(self._dbh, self._addr.domainname) + if not self._gid: + raise RErr(_(u"The domain '%s' does not exist.") % + self._addr.domainname, NO_SUCH_DOMAIN) + self._dest = None + + self._load() + + def __nonzero__(self): + """Returns `True` if the Relocated is known, `False` if it's new.""" + return self._dest is not None + + def _load(self): + """Loads the destination address from the database into the + `_dest` attribute. + + """ + dbc = self._dbh.cursor() + dbc.execute('SELECT destination FROM relocated WHERE gid = %s AND ' + 'address = %s', (self._gid, self._addr.localpart)) + destination = dbc.fetchone() + dbc.close() + if destination: + destination = DestinationEmailAddress(destination[0], self._dbh) + if destination.at_localhost: + raise RErr(_(u"The destination address' domain name must not " + u"be localhost."), DOMAIN_INVALID) + self._dest = destination + + @property + def address(self): + """The Relocated's EmailAddress instance.""" + return self._addr + + def set_destination(self, destination): + """Sets/updates the new address of the relocated user.""" + update = False + assert isinstance(destination, DestinationEmailAddress) + if destination.at_localhost: + raise RErr(_(u"The destination address' domain name must not be " + u"localhost."), DOMAIN_INVALID) + if self._addr == destination: + raise RErr(_(u'Address and destination are identical.'), + RELOCATED_ADDR_DEST_IDENTICAL) + if self._dest: + if self._dest == destination: + raise RErr(_(u"The relocated user '%s' already exists.") % + self._addr, RELOCATED_EXISTS) + else: + self._dest = destination + update = True + else: + self._dest = destination + + dbc = self._dbh.cursor() + if not update: + dbc.execute('INSERT INTO relocated (gid, address, destination) ' + 'VALUES (%s, %s, %s)', + (self._gid, self._addr.localpart, str(self._dest))) + else: + dbc.execute('UPDATE relocated SET destination = %s WHERE gid = %s ' + 'AND address = %s', + (str(self._dest), self._gid, self._addr.localpart)) + self._dbh.commit() + dbc.close() + + def get_info(self): + """Returns the address to which mails should be sent.""" + if not self._dest: + raise RErr(_(u"The relocated user '%s' does not exist.") % + self._addr, NO_SUCH_RELOCATED) + return self._dest + + def delete(self): + """Deletes the relocated entry from the database.""" + if not self._dest: + raise RErr(_(u"The relocated user '%s' does not exist.") % + self._addr, NO_SUCH_RELOCATED) + dbc = self._dbh.cursor() + dbc.execute('DELETE FROM relocated WHERE gid = %s AND address = %s', + (self._gid, self._addr.localpart)) + if dbc.rowcount > 0: + self._dbh.commit() + dbc.close() + self._dest = None + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/serviceset.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/serviceset.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,168 @@ +# coding: utf-8 +# Copyright (c) 2011 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.serviceset + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Virtual Mail Manager's ServiceSet class for simplified database access + to the service_set table. +""" + +SERVICES = ('smtp', 'pop3', 'imap', 'sieve') + +cfg_dget = lambda option: None + + +class ServiceSet(object): + """A wrapper class that provides access to the service_set table. + + Each ServiceSet object provides following - read only - attributes: + + `ssid` : long + The id of the service set + `smtp` : bool + Boolean flag for service smtp + `pop3` : bool + Boolean flag for service pop3 + `imap` : bool + Boolean flag for service imap + `sieve` : bool + Boolean flag for service sieve + `services` : dict + The four services above with boolean values + """ + __slots__ = ('_ssid', '_services', '_sieve_col', '_dbh') + _kwargs = (('ssid',) + SERVICES) + + def __init__(self, dbh, **kwargs): + """Creates a new ServiceSet instance. + + Either the 'ssid' keyword argument or one or more of the service + arguments ('smtp', 'pop3', 'imap', 'sieve') must be provided. + + Arguments: + `dbh` : pyPgSQL.PgSQL.Connection or psycopg2.extensions.connection + A database connection for the database access. + + Keyword arguments: + `ssid` : int + The id of the service set (>0) + `smtp` : bool + Boolean flag for service smtp - default `True` + `pop3` : bool + Boolean flag for service pop3 - default `True` + `imap` : bool + Boolean flag for service imap - default `True` + `sieve` : bool + Boolean flag for service sieve - default `True` + """ + self._dbh = dbh + self._ssid = 0 + self._services = dict.fromkeys(SERVICES, True) + if cfg_dget('misc.dovecot_version') < 0x10200b02: + self._sieve_col = 'managesieve' + else: + self._sieve_col = 'sieve' + + for key in kwargs.iterkeys(): + if key not in self.__class__._kwargs: + raise ValueError('unrecognized keyword: %r' % key) + if key == 'ssid': + assert not isinstance(kwargs[key], bool) and \ + isinstance(kwargs[key], (int, long)) and kwargs[key] > 0 + self._load_by_ssid(kwargs[key]) + break + else: + assert isinstance(kwargs[key], bool) + if not kwargs[key]: + self._services[key] = kwargs[key] + if not self._ssid: + self._load_by_services() + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self._ssid == other._ssid + return NotImplemented + + def __ne__(self, other): + if isinstance(other, self.__class__): + return self._ssid != other._ssid + return NotImplemented + + def __getattr__(self, name): + if name not in self.__class__._kwargs: + raise AttributeError('%r object has no attribute %r' % ( + self.__class__.__name__, name)) + if name == 'ssid': + return self._ssid + else: + return self._services[name] + + def __repr__(self): + return '%s(%s, %s)' % (self.__class__.__name__, self._dbh, + ', '.join('%s=%r' % s for s in self._services.iteritems())) + + def _load_by_services(self): + """Try to load the service_set by it's service combination.""" + sql = ('SELECT ssid FROM service_set WHERE %s' % + ' AND '.join('%s = %s' % + (k, str(v).upper()) for k, v in self._services.iteritems())) + if self._sieve_col == 'managesieve': + sql.replace('sieve', self._sieve_col) + dbc = self._dbh.cursor() + dbc.execute(sql) + result = dbc.fetchone() + dbc.close() + if result: + self._ssid = result[0] + else: + self._save() + + def _load_by_ssid(self, ssid): + """Try to load the service_set by it's primary key.""" + dbc = self._dbh.cursor() + dbc.execute('SELECT ssid, smtp, pop3, imap, %s' % (self._sieve_col,) + + ' FROM service_set WHERE ssid = %s', (ssid,)) + result = dbc.fetchone() + dbc.close() + if not result: + raise ValueError('Unknown service_set id specified: %r' % ssid) + self._ssid = result[0] + #self._services.update(zip(SERVICES, result[1:])) + for key, value in zip(SERVICES, result[1:]): # pyPgSQL compatible + if value: + self._services[key] = True + else: + self._services[key] = False + + def _save(self): + """Store a new service_set in the database.""" + sql = ('INSERT INTO service_set (ssid, smtp, pop3, imap, %s) ' % + (self._sieve_col,) + + 'VALUES (%(ssid)s, %(smtp)s, %(pop3)s, %(imap)s, %(sieve)s)') + if self._sieve_col == 'managesieve': + sql.replace('sieve', self._sieve_col) + self._set_ssid() + values = {'ssid': self._ssid} + values.update(self._services) + dbc = self._dbh.cursor() + dbc.execute(sql, values) + self._dbh.commit() + dbc.close() + + def _set_ssid(self): + """Set the unique ID for the new service_set.""" + assert self._ssid == 0 + dbc = self._dbh.cursor() + dbc.execute("SELECT nextval('service_set_id')") + self._ssid = dbc.fetchone()[0] + dbc.close() + + @property + def services(self): + """A dictionary: Keys: `smtp`, `pop3`, `imap` and `sieve` with + boolean values.""" + return self._services.copy() + +del cfg_dget diff -r c0e1fb1b0145 -r a4aead244f75 VirtualMailManager/transport.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/VirtualMailManager/transport.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,100 @@ +# -*- coding: UTF-8 -*- +# Copyright (c) 2008 - 2012, Pascal Volk +# See COPYING for distribution information. +""" + VirtualMailManager.transport + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Virtual Mail Manager's Transport class to manage the transport for + domains and accounts. +""" + +from VirtualMailManager.pycompat import any + +_ = lambda msg: msg + + +class Transport(object): + """A wrapper class that provides access to the transport table""" + __slots__ = ('_tid', '_transport', '_dbh') + + def __init__(self, dbh, tid=None, transport=None): + """Creates a new Transport instance. + + Either tid or transport must be specified. When both arguments + are given, tid will be used. + + Keyword arguments: + dbh -- a pyPgSQL.PgSQL.connection + tid -- the id of a transport (int/long) + transport -- the value of the transport (str) + + """ + self._dbh = dbh + self._tid = 0 + assert any((tid, transport)) + if tid: + assert not isinstance(tid, bool) and isinstance(tid, (int, long)) + self._load_by_id(tid) + else: + assert isinstance(transport, basestring) + self._transport = transport + self._load_by_name() + + @property + def tid(self): + """The transport's unique ID.""" + return self._tid + + @property + def transport(self): + """The transport's value, ex: 'dovecot:'""" + return self._transport + + def __eq__(self, other): + if isinstance(other, self.__class__): + return self._tid == other._tid + return NotImplemented + + def __ne__(self, other): + if isinstance(other, self.__class__): + return self._tid != other._tid + return NotImplemented + + def __str__(self): + return self._transport + + def _load_by_id(self, tid): + """load a transport by its id from the database""" + dbc = self._dbh.cursor() + dbc.execute('SELECT transport FROM transport WHERE tid = %s', (tid,)) + result = dbc.fetchone() + dbc.close() + if not result: + raise ValueError('Unknown transport id specified: %r' % tid) + self._transport = result[0] + self._tid = tid + + def _load_by_name(self): + """Load a transport by its transport name from the database.""" + dbc = self._dbh.cursor() + dbc.execute('SELECT tid FROM transport WHERE transport = %s', + (self._transport,)) + result = dbc.fetchone() + dbc.close() + if result: + self._tid = result[0] + else: + self._save() + + def _save(self): + """Save the new transport in the database.""" + dbc = self._dbh.cursor() + dbc.execute("SELECT nextval('transport_id')") + self._tid = dbc.fetchone()[0] + dbc.execute('INSERT INTO transport (tid, transport) VALUES (%s, %s)', + (self._tid, self._transport)) + self._dbh.commit() + dbc.close() + +del _ diff -r c0e1fb1b0145 -r a4aead244f75 doc/Makefile --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/Makefile Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,89 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/vmm.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/vmm.qhc" + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff -r c0e1fb1b0145 -r a4aead244f75 doc/source/conf.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/source/conf.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +# +# vmm documentation build configuration file, created by +# sphinx-quickstart on Sun Feb 14 00:08:08 2010. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.append(os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.intersphinx', 'sphinx.ext.todo'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['.templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'vmm' +copyright = u'2010, Pascal Volk' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.6' +# The full version, including alpha/beta/rc tags. +release = '0.6.x' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +html_theme = 'default' +#html_theme = 'sphinxdoc' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['.static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'vmmdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'vmm.tex', u'vmm Documentation', + u'Pascal Volk', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} + +todo_include_todos = True diff -r c0e1fb1b0145 -r a4aead244f75 doc/source/index.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/source/index.rst Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,29 @@ +====================== +VirtualMailManager API +====================== + +:Author: Pascal Volk +:Date: |today| +:Release: |version| + +Contents: + +.. toctree:: + :maxdepth: 1 + :numbered: + + vmm.rst + vmm_config.rst + vmm_emailaddress.rst + vmm_alias.rst + vmm_relocated.rst + vmm_errors.rst + vmm_constants_error.rst + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff -r c0e1fb1b0145 -r a4aead244f75 doc/source/vmm.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/source/vmm.rst Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,132 @@ +:mod:`VirtualMailManager` --- Initialization code and some functions +===================================================================== + +.. module:: VirtualMailManager + :synopsis: Initialization code and some functions + +.. moduleauthor:: Pascal Volk + +.. toctree:: + :maxdepth: 2 + +When the VirtualMailManager module, or one of its sub modules, is imported, +the following actions will be performed: + + - :func:`locale.setlocale` (with :const:`locale.LC_ALL`) is called, to set + :const:`ENCODING` + - :func:`gettext.install` is called, to have 18N support. + +Constants and data +------------------ + +.. data:: ENCODING + + The systems current character encoding, e.g. ``'UTF-8'`` or + ``'ANSI_X3.4-1968'`` (aka ASCII). + + +Functions +--------- + +.. function:: ace2idna(domainname) + + Converts the idn domain name *domainname* into punycode. + + :param domainname: the domain-ace representation (``xn--…``) + :type domainname: str + :rtype: unicode + +.. function:: check_domainname(domainname) + + Returns the validated domain name *domainname*. + + It also converts the name of the domain from IDN to ASCII, if necessary. + + :param domainname: the name of the domain + :type domainname: :obj:`basestring` + :rtype: str + :raise VirtualMailManager.errors.VMMError: if the domain name is + too long or doesn't look like a valid domain name (label.label.label). + +.. function:: check_localpart(localpart) + + Returns the validated local-part *localpart* of an e-mail address. + + :param localpart: The local-part of an e-mail address. + :type localpart: str + :rtype: str + :raise VirtualMailManager.errors.VMMError: if the local-part is too + long or contains invalid characters. + +.. function:: exec_ok(binary) + + Checks if the *binary* exists and if it is executable. + + :param binary: path to the binary + :type binary: str + :rtype: str + :raise VirtualMailManager.errors.VMMError: if *binary* isn't a file + or is not executable. + +.. function:: expand_path(path) + + Expands paths, starting with ``.`` or ``~``, to an absolute path. + + :param path: Path to a file or directory + :type path: str + :rtype: str + +.. function:: get_unicode(string) + + Converts `string` to `unicode`, if necessary. + + :param string: The string taht should be converted + :type string: str + :rtype: unicode + +.. function:: idn2ascii(domainname) + + Converts the idn domain name *domainname* into punycode. + + :param domainname: the unicode representation of the domain name + :type domainname: unicode + :rtype: str + +.. function:: is_dir(path) + + Checks if *path* is a directory. + + :param path: Path to a directory + :type path: str + :rtype: str + :raise VirtualMailManager.errors.VMMError: if *path* is not a directory. + + +Examples +-------- + + >>> from VirtualMailManager import * + >>> ace2idna('xn--pypal-4ve.tld') + u'p\u0430ypal.tld' + >>> idn2ascii(u'öko.de') + 'xn--ko-eka.de' + >>> check_domainname(u'pаypal.tld') + 'xn--pypal-4ve.tld' + >>> check_localpart('john.doe') + 'john.doe' + >>> exec_ok('usr/bin/vim') + Traceback (most recent call last): + File "", line 1, in + File "./VirtualMailManager/__init__.py", line 93, in exec_ok + NO_SUCH_BINARY) + VirtualMailManager.errors.VMMError: 'usr/bin/vim' is not a file + >>> exec_ok('/usr/bin/vim') + '/usr/bin/vim' + >>> expand_path('.') + '/home/user/hg/vmm' + >>> get_unicode('hello world') + u'hello world' + >>> is_dir('~/hg') + '/home/user/hg' + >>> + diff -r c0e1fb1b0145 -r a4aead244f75 doc/source/vmm_alias.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/source/vmm_alias.rst Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,87 @@ +:mod:`VirtualMailManager.Alias` --- Handling of alias e-mail addresses +====================================================================== + +.. module:: VirtualMailManager.Alias + :synopsis: Handling of alias e-mail addresses + +.. moduleauthor:: Pascal Volk + +.. toctree:: + :maxdepth: 2 + + +This module provides the :class:`Alias` class. The data are read from/stored +in the ``alias`` table. This table is used by Postfix to rewrite recipient +addresses. + + +Alias +--------- +.. class:: Alias(dbh, address) + + Creates a new *Alias* instance. Alias instances provides the :func:`__len__` + method. So the existence of an alias in the database can be tested with a + simple if condition. + + :param dbh: a database connection + :type dbh: :class:`pyPgSQL.PgSQL.Connection` + :param address: the alias e-mail address. + :type address: :class:`VirtualMailManager.EmailAddress.EmailAddress` + + .. method:: add_destinations(destinations, expansion_limit [, warnings=None]) + + Adds the *destinations* to the destinations of the alias. This method + returns a ``set`` of all addresses which successfully were stored into the + database. + + If one of the e-mail addresses in *destinations* is the same as the alias + address, it will be silently discarded. Destination addresses, that are + already assigned to the alias, will be also ignored. + + When the optional *warnings* list is given, all ignored addresses will be + appended to it. + + :param destinations: The destination addresses of the alias + :type destinations: :obj:`list` of + :class:`VirtualMailManager.EmailAddress.EmailAddress` instances + :param expansion_limit: The maximal number of destinations (see also: + `virtual_alias_expansion_limit + `_) + :type expansion_limit: :obj:`int` + :param warnings: A optional list, to record all ignored addresses + :type warnings: :obj:`list` + :rtype: :obj:`set` + :raise VirtualMailManager.errors.AliasError: if the additional + *destinations* will exceed the *expansion_limit* or if the alias + already exceeds its *expansion_limit*. + + .. seealso:: :mod:`VirtualMailManager.ext.postconf` -- to read actual + values of Postfix configuration parameters. + + + .. method:: del_destination(destination) + + Deletes the given *destination* address from the alias. + + :param destination: a destination address of the alias + :type destination: :class:`VirtualMailManager.EmailAddress.EmailAddress` + :rtype: :obj:`None` + :raise VirtualMailManager.errors.AliasError: if the destination wasn't + assigned to the alias or the alias doesn't exist. + + + .. method:: delete() + + Deletes the alias with all its destinations. + + :rtype: :obj:`None` + :raise VirtualMailManager.errors.AliasError: if the alias doesn't exist. + + + .. method:: get_destinations() + + Returns an iterator for all destinations (``EmailAddress`` instances) of + the alias. + + :rtype: :obj:`listiterator` + :raise VirtualMailManager.errors.AliasError: if the alias doesn't exist. diff -r c0e1fb1b0145 -r a4aead244f75 doc/source/vmm_config.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/source/vmm_config.rst Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,275 @@ +:mod:`VirtualMailManager.Config` --- Simplified configuration access +====================================================================== + +.. module:: VirtualMailManager.Config + :synopsis: Simplified configuration access + +.. moduleauthor:: Pascal Volk + +.. toctree:: + :maxdepth: 2 + + +This module provides a few classes for simplified configuration handling +and the validation of the setting's *type* and *value*. + +:class:`LazyConfig` is derived from Python's +:class:`ConfigParser.RawConfigParser`. It doesn't use ``RawConfigParser``'s +``DEFAULT`` section. All settings and their defaults, if supposed, are +handled by :class:`LazyConfigOption` objects in the :attr:`LazyConfig._cfg` +*dict*. + +``LazyConfig``'s setters and getters for options are taking a single string +for the *section* and *option* argument, e.g. ``config.pget('database.user')`` +instead of ``config.get('database', 'user')``. + + + +LazyConfig +---------- +.. class:: LazyConfig + + Bases: :class:`ConfigParser.RawConfigParser` + + .. versionadded:: 0.6.0 + + .. attribute:: _cfg + + a multi dimensional :class:`dict`, containing *sections* and *options*, + represented by :class:`LazyConfigOption` objects. + + For example:: + + from VirtualMailManager.Config import LazyConfig, LazyConfigOption + + class FooConfig(LazyConfig): + def __init__(self, ...): + LazyConfig.__init__(self) + ... + LCO = LazyConfigOption + self._cfg = { + 'database': {# section database: + 'host': LCO(str, '::1', self.get), # options of the + 'name': LCO(str, 'dbx', self.get), # database section. + 'pass': LCO(str, None, self.get), # No defaults for the + 'user': LCO(str, None, self.get), # user and pass options + } + } + + ... + + + .. method:: bool_new(value) + + Converts the string *value* into a `bool` and returns it. + + | ``'1'``, ``'on'``, ``'yes'`` and ``'true'`` will become :const:`True` + | ``'0'``, ``'off'``, ``'no'`` and ``'false'`` will become :const:`False` + + :param value: one of the above mentioned strings + :type value: :obj:`basestring` + :rtype: bool + :raise ConfigValueError: for all other values, except ``bool``\ s + + .. method:: dget(option) + + Like :meth:`pget`, but returns the *option*'s default value, from + :attr:`_cfg` (defined by :attr:`LazyConfigOption.default`) if the *option* + is not configured in a ini-like configuration file. + + :param option: the section.option combination + :type option: :obj:`basestring` + :raise NoDefaultError: if the *option* couldn't be found in the + configuration file and no default value was passed to + :class:`LazyConfigOption`'s constructor for the requested *option*. + + .. method:: getboolean(section, option) + + Returns the boolean value of the *option*, in the given *section*. + + For a boolean :const:`True`, the value must be set to ``'1'``, ``'on'``, + ``'yes'``, ``'true'`` or :const:`True`. For a boolean :const:`False`, the + value must set to ``'0'``, ``'off'``, ``'no'``, ``'false'`` or + :const:`False`. + + :param section: The section's name + :type section: :obj:`basestring` + :param option: The option's name + :type option: :obj:`basestring` + :rtype: bool + :raise ValueError: if the option has an other value than the values + mentioned above. + + .. method:: has_option(option) + + Checks if the *option* (section\ **.**\ option) is a known configuration + option. + + :param option: The option's name + :type option: :obj:`basestring` + :rtype: bool + + .. method:: has_section(section) + + Checks if *section* is a known configuration section. + + :param section: The section's name + :type section: :obj:`basestring` + :rtype: bool + + .. method:: items(section) + + Returns an iterator for ``key, value`` :obj:`tuple`\ s for each option in + the given *section*. + + :param section: The section's name + :type section: :obj:`basestring` + :raise NoSectionError: if the given *section* is not known. + + .. method:: pget(option) + + Polymorphic getter which returns the *option*'s value (by calling + :attr:`LazyConfigOption.getter`) with the appropriate type, defined by + :attr:`LazyConfigOption.cls`. + + :param option: the section.option combination + :type option: :obj:`basestring` + + .. method:: sections() + + Returns an iterator object for all configuration sections from the + :attr:`_cfg` dictionary. + + :rtype: :obj:`dictionary-keyiterator` + + .. method:: set(option, value) + + Like :meth:`ConfigParser.RawConfigParser.set`, but converts the *option*'s + new *value* (by calling :attr:`LazyConfigOption.cls`) to the appropriate + type/class. When the ``LazyConfigOption``'s optional parameter *validate* + was not :const:`None`, the new *value* will be also validated. + + :param option: the section.option combination + :type option: :obj:`basestring` + :param value: the new value to be set + :type value: :obj:`basestring` + :rtype: :const:`None` + :raise ConfigValueError: if a boolean value shout be set (:meth:`bool_new`) + and it fails + :raise ValueError: if an other setter (:attr:`LazyConfigOption.cls`) or + validator (:attr:`LazyConfigOption.validate`) fails. + :raise VirtualMailManager.errors.VMMError: if + :attr:`LazyConfigOption.validate` is set to + :func:`VirtualMailManager.exec_ok` or :func:`VirtualMailManager.is_dir`. + + +LazyConfigOption +---------------- +LazyConfigOption instances are required by :class:`LazyConfig` instances, and +instances of classes derived from `LazyConfig`, like the :class:`Config` +class. + +.. class:: LazyConfigOption (cls, default, getter[, validate=None]) + + .. versionadded:: 0.6.0 + + The constructor's parameters are: + + ``cls`` : :obj:`type` + The class/type of the option's value. + ``default`` : :obj:`str` or the one defined by ``cls`` + Default value of the option. Use :const:`None` if the option shouldn't + have a default value. + ``getter``: :obj:`callable` + A method's name of :class:`ConfigParser.RawConfigParser` and derived + classes, to get a option's value, e.g. `self.getint`. + ``validate`` : :obj:`callable` or :const:`None` + :const:`None` or any function, which takes one argument and returns the + validated argument with the appropriate type (for example: + :meth:`LazyConfig.bool_new`). The function should raise a + :exc:`ConfigValueError` if the validation fails. This function checks the + new value when :meth:`LazyConfig.set()` is called. + + Each LazyConfigOption object has the following read-only attributes: + + .. attribute:: cls + + The class of the option's value e.g. `str`, `unicode` or `bool`. Used as + setter method when :meth:`LazyConfig.set` (or the ``set()`` method of a + derived class) is called. + + .. attribute:: default + + The option's default value, may be ``None`` + + .. attribute:: getter + + A method's name of :class:`ConfigParser.RawConfigParser` and derived + classes, to get a option's value, e.g. ``self.getint``. + + .. attribute:: validate + + A method or function to validate the option's new value. + + +Config +------ +The final configuration class of the virtual mail manager. + +.. class:: Config (filename) + + Bases: :class:`LazyConfig` + + :param filename: absolute path to the configuration file. + :type filename: :obj:`basestring` + + .. attribute:: _cfg + + The configuration ``dict``, containing all configuration sections and + options, as described in :attr:`LazyConfig._cfg`. + + .. method:: check() + + Checks all section's options for settings w/o a default value. + + :raise VirtualMailManager.errors.ConfigError: if the check fails + + .. method:: load() + + Loads the configuration read-only. + + :raise VirtualMailManager.errors.ConfigError: if the + configuration syntax is invalid + + .. method:: unicode(section, option) + + Returns the value of the *option* from *section*, converted to Unicode. + This method is intended for the :attr:`LazyConfigOption.getter`. + + :param section: The name of the configuration section + :type section: :obj:`basestring` + :param option: The name of the configuration option + :type option: :obj:`basestring` + :rtype: :obj:`unicode` + + +Exceptions +---------- + +.. exception:: BadOptionError(msg) + + Bases: :exc:`ConfigParser.Error` + + Raised when a option isn't in the format 'section.option'. + +.. exception:: ConfigValueError(msg) + + Bases: :exc:`ConfigParser.Error` + + Raised when creating or validating of new values fails. + +.. exception:: NoDefaultError(section, option) + + Bases: :exc:`ConfigParser.Error` + + Raised when the requested option has no default value. diff -r c0e1fb1b0145 -r a4aead244f75 doc/source/vmm_constants_error.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/source/vmm_constants_error.rst Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,227 @@ +:mod:`VirtualMailManager.constants.ERROR` --- Error codes +========================================================= + +.. module:: VirtualMailManager.constants.ERROR + :synopsis: VirtualMailManager's error codes + +.. moduleauthor:: Pascal Volk + +.. toctree:: + :maxdepth: 2 + +Error codes, used by all :mod:`VirtualMailManager.errors`. + +.. data:: ACCOUNT_AND_ALIAS_PRESENT + + Can't delete the Domain - there are accounts and aliases assigned + +.. data:: ACCOUNT_EXISTS + + The Account exists already + +.. data:: ACCOUNT_PRESENT + + Can't delete the Domain - there are accounts + +.. data:: ALIASDOMAIN_EXISTS + + Can't save/switch the destination of the AliasDomain - old and new destination + are the same. + +.. data:: ALIASDOMAIN_ISDOMAIN + + Can't create AliasDomain - there is already a Domain with the given name + + .. todo:: Move the related check to the Handler class + +.. data:: ALIASDOMAIN_NO_DOMDEST + + Can't save/switch the destination of an AliasDomain if the destination was + omitted + +.. data:: ALIAS_ADDR_DEST_IDENTICAL + + The alias address and its destination are the same + + obsolete? + +.. data:: ALIAS_EXCEEDS_EXPANSION_LIMIT + + The Alias has reached or exceeds its expansion limit + +.. data:: ALIAS_EXISTS + + Alias with the given destination exists already + + obsolete? + +.. data:: ALIAS_MISSING_DEST + + obsolete? + +.. data:: ALIAS_PRESENT + + Can't delete Domain or Account - there are aliases assigned + +.. data:: CONF_ERROR + + Syntax error in the configuration file or missing settings w/o a default value + +.. data:: CONF_NOFILE + + The configuration file couldn't be found + +.. data:: CONF_NOPERM + + The user's permissions are insufficient + +.. data:: CONF_WRONGPERM + + Configuration file has the wrong access mode + +.. data:: DATABASE_ERROR + + A database error occurred + +.. data:: DOMAINDIR_GROUP_MISMATCH + + Domain directory is owned by the wrong group + +.. data:: DOMAIN_ALIAS_EXISTS + + Can't create Domain - there is already an AliasDomain with the same name + + .. todo:: Move the related check to the Handler class + +.. data:: DOMAIN_EXISTS + + The Domain is already available in the database + +.. data:: DOMAIN_INVALID + + The domain name is invalid + +.. data:: DOMAIN_NO_NAME + + Missing the domain name + +.. data:: DOMAIN_TOO_LONG + + The length of domain is > 255 + +.. data:: FOUND_DOTS_IN_PATH + + Can't delete directory with ``.`` or ``..`` in path + + .. todo:: check if we can solve this issue with expand_path() + +.. data:: INVALID_ADDRESS + + The specified value doesn't look like a e-mail address + +.. data:: INVALID_AGUMENT + + The given argument is invalid + +.. data:: INVALID_OPTION + + The given option is invalid + +.. data:: INVALID_SECTION + + The section is not a known configuration section + +.. data:: LOCALPART_INVALID + + The local-part of an e-mail address was omitted or is invalid + +.. data:: LOCALPART_TOO_LONG + + The local-part (w/o a extension) is too long (> 64) + +.. data:: MAILDIR_PERM_MISMATCH + + The Maildir is owned by the wrong user/group + +.. data:: MAILLOCATION_INIT + + Can't create a new MailLocation instance + + obsolete? + +.. data:: NOT_EXECUTABLE + + The binary is not executable + +.. data:: NO_SUCH_ACCOUNT + + No Account with the given e-mail address + +.. data:: NO_SUCH_ALIAS + + No Alias with the given e-mail address + +.. data:: NO_SUCH_ALIASDOMAIN + + The given domain is not an AliasDomain + +.. data:: NO_SUCH_BINARY + + Can't find the file at the specified location + +.. data:: NO_SUCH_DIRECTORY + + There is no directory with the given path + +.. data:: NO_SUCH_DOMAIN + + No Domain with the given name + +.. data:: NO_SUCH_RELOCATED + + There is no Relocated user with the given e-mail address + +.. data:: RELOCATED_ADDR_DEST_IDENTICAL + + The e-mail address of the Relocated user an its destination are the same + +.. data:: RELOCATED_EXISTS + + Can't create Account or Alias, there is already a Relocated user with the + given e-mail address + +.. data:: RELOCATED_MISSING_DEST + + obsolete? + +.. data:: TRANSPORT_INIT + + Can't initialize a new Transport instance + + obsolete? + +.. data:: UNKNOWN_MAILLOCATION_ID + + There is no MailLocation entry with the given ID + + obsolete? + +.. data:: UNKNOWN_SERVICE + + The specified service is unknown + +.. data:: UNKNOWN_TRANSPORT_ID + + There is no Transport entry with the given ID + +.. data:: UNKNOWN_MAILLOCATION_NAME + + The given mail_location directory couldn't be accepted + +.. data:: VMM_ERROR + + Internal error + +.. data:: VMM_TOO_MANY_FAILURES + + Too many errors in interactive mode diff -r c0e1fb1b0145 -r a4aead244f75 doc/source/vmm_emailaddress.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/source/vmm_emailaddress.rst Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,61 @@ +:mod:`VirtualMailManager.EmailAddress` --- Handling of e-mail addresses +======================================================================= + +.. module:: VirtualMailManager.EmailAddress + :synopsis: Handling of e-mail addresses + +.. moduleauthor:: Pascal Volk + +.. toctree:: + :maxdepth: 2 + + +This module provides the :class:`EmailAddress` class to handle validated e-mail +addresses. + + +EmailAddress +------------ + +.. class:: EmailAddress(address) + + Creates a new EmailAddress instance. + + :param address: string representation of an e-mail addresses + :type address: :obj:`basestring` + :raise VirtualMailManager.errors.EmailAddressError: if the + *address* is syntactically wrong. + :raise VirtualMailManager.errors.VMMError: if the validation of the + local-part or domain name fails. + + An EmailAddress instance has the both read-only attributes: + + .. attribute:: localpart + + The local-part of the address *local-part@domain* + + + .. attribute:: domainname + + The domain part of the address *local-part@domain* + + +Examples +-------- + + >>> from VirtualMailManager.EmailAddress import EmailAddress + >>> john = EmailAddress('john.doe@example.com') + >>> john.localpart + 'john.doe' + >>> john.domainname + 'example.com' + >>> jane = EmailAddress('jane.doe@example.com') + >>> jane != john + True + >>> EmailAddress('info@xn--pypal-4ve.tld') == EmailAddress(u'info@pаypal.tld') + True + >>> jane + EmailAddress('jane.doe@example.com') + >>> print john + john.doe@example.com + >>> diff -r c0e1fb1b0145 -r a4aead244f75 doc/source/vmm_errors.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/source/vmm_errors.rst Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,122 @@ +:mod:`VirtualMailManager.errors` --- Exception classes +====================================================== + +.. module:: VirtualMailManager.errors + :synopsis: Exception classes + +.. moduleauthor:: Pascal Volk + +.. toctree:: + :maxdepth: 2 + +Exceptions, used by VirtualMailManager's classes. + + +Exceptions +---------- + +.. exception:: VMMError(msg, code) + + Bases: :exc:`exceptions.Exception` + + :param msg: the error message + :type msg: :obj:`basestring` + :param code: the error code (one of :mod:`VirtualMailManager.constants.ERROR`) + :type code: :obj:`int` + + Base class for all other Exceptions in the VirtualMailManager package. + + The *msg* and *code* are accessible via the both attributes: + + .. attribute:: msg + + The error message of the exception. + + + .. attribute:: code + + The numerical error code of the exception. + + +.. exception:: ConfigError(msg, code) + + Bases: :exc:`VirtualMailManager.errors.VMMError` + + Exception class for configuration (:mod:`VirtualMailManager.Config`) + exceptions. + + +.. exception:: PermissionError(msg, code) + + Bases: :exc:`VirtualMailManager.errors.VMMError` + + Exception class for file permission exceptions. + + +.. exception:: NotRootError(msg, code) + + Bases: :exc:`VirtualMailManager.errors.VMMError` + + Exception class for non-root exceptions. + + +.. exception:: DomainError(msg, code) + + Bases: :exc:`VirtualMailManager.errors.VMMError` + + Exception class for Domain (:mod:`VirtualMailManager.Domain`) exceptions. + + +.. exception:: AliasDomainError(msg, code) + + Bases: :exc:`VirtualMailManager.errors.VMMError` + + Exception class for AliasDomain (:mod:`VirtualMailManager.AliasDomain`) + exceptions. + + +.. exception:: AccountError(msg, code) + + Bases: :exc:`VirtualMailManager.errors.VMMError` + + Exception class for Account (:mod:`VirtualMailManager.Account`) exceptions. + + +.. exception:: AliasError(msg, code) + + Bases: :exc:`VirtualMailManager.errors.VMMError` + + Exception class for Alias (:mod:`VirtualMailManager.Alias`) exceptions. + + +.. exception:: EmailAddressError(msg, code) + + Bases: :exc:`VirtualMailManager.errors.VMMError` + + Exception class for EmailAddress (:mod:`VirtualMailManager.EmailAddress`) + exceptions. + + +.. exception:: MailLocationError(msg, code) + + Bases: :exc:`VirtualMailManager.errors.VMMError` + + Exception class for MailLocation (:mod:`VirtualMailManager.MailLocation`) + exceptions. + + +.. exception:: RelocatedError(msg, code) + + Bases: :exc:`VirtualMailManager.errors.VMMError` + + Exception class for Relocated (:mod:`VirtualMailManager.Relocated`) + exceptions. + + +.. exception:: TransportError(msg, code) + + Bases: :exc:`VirtualMailManager.errors.VMMError` + + Exception class for Transport (:mod:`VirtualMailManager.Transport`) + exceptions. + diff -r c0e1fb1b0145 -r a4aead244f75 doc/source/vmm_relocated.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/doc/source/vmm_relocated.rst Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,60 @@ +:mod:`VirtualMailManager.Relocated` --- Handling of relocated users +=================================================================== + +.. module:: VirtualMailManager.Relocated + :synopsis: Handling of relocated users + +.. moduleauthor:: Pascal Volk + +.. toctree:: + :maxdepth: 2 + + +This module provides the :class:`Relocated` class. The data are read +from/stored in the ``relocated`` table. An optional lookup table, used +by Postfix for the "``user has moved to new_location``" reject/bounce message. + + +Relocated +--------- +.. class:: Relocated(dbh, address) + + Creates a new *Relocated* instance. If the relocated user with the given + *address* is already stored in the database use :meth:`get_info` to get the + destination address of the relocated user. To set or update the destination + of the relocated user use :meth:`set_destination`. Use :meth:`delete` in + order to delete the relocated user from the database. + + :param dbh: a database connection + :type dbh: :class:`pyPgSQL.PgSQL.Connection` + :param address: the e-mail address of the relocated user. + :type address: :class:`VirtualMailManager.EmailAddress.EmailAddress` + + + .. method:: delete() + + :rtype: :obj:`None` + :raise VirtualMailManager.errors.RelocatedError: if the relocated user + doesn't exist. + + Deletes the relocated user from the database. + + + .. method:: get_info() + + :rtype: :class:`VirtualMailManager.EmailAddress.EmailAddress` + :raise VirtualMailManager.errors.RelocatedError: if the relocated user + doesn't exist. + + Returns the destination e-mail address of the relocated user. + + + .. method:: set_destination(destination) + + :param destination: the new address where the relocated user has moved to + :type destination: :class:`VirtualMailManager.EmailAddress.EmailAddress` + :rtype: :obj:`None` + :raise VirtualMailManager.errors.RelocatedError: if the *destination* + address is already saved or is the same as the relocated user's address. + + Sets or updates the *destination* address of the relocated user. diff -r c0e1fb1b0145 -r a4aead244f75 install.sh --- a/install.sh Mon Nov 07 03:22:15 2011 +0000 +++ b/install.sh Thu Jun 28 19:26:50 2012 +0000 @@ -16,7 +16,7 @@ else MANDIR=${PREFIX}/man fi -DOCS="ChangeLog COPYING INSTALL README" +DOCS="ChangeLog Configure.Dovecot_2 COPYING INSTALL NEWS README" INSTALL_OPTS="-g 0 -o 0 -p" INSTALL_OPTS_CF="-b -m 0640 -g ${PF_GID} -o 0 -p" @@ -26,7 +26,7 @@ exit 1 fi -python setup.py -q install --prefix ${PREFIX} +python setup.py -q install --force --prefix ${PREFIX} python setup.py clean --all >/dev/null install -b -m 0600 ${INSTALL_OPTS} vmm.cfg ${PREFIX}/etc/ @@ -50,7 +50,7 @@ [ -d ${MANDIR}/man5 ] || mkdir -m 0755 -p ${MANDIR}/man5 install -m 0644 ${INSTALL_OPTS} man5/vmm.cfg.5 ${MANDIR}/man5 -for l in $(find . -maxdepth 1 -mindepth 1 -type d \! -name man\? \! -name .svn) +for l in $(find . -maxdepth 1 -mindepth 1 -type d \! -name man\?) do for s in man1 man5; do [ -d ${MANDIR}/${l}/${s} ] || mkdir -m 0755 -p ${MANDIR}/${l}/${s} diff -r c0e1fb1b0145 -r a4aead244f75 man/de/man1/vmm.1 --- a/man/de/man1/vmm.1 Mon Nov 07 03:22:15 2011 +0000 +++ b/man/de/man1/vmm.1 Thu Jun 28 19:26:50 2012 +0000 @@ -1,447 +1,1015 @@ -.TH "VMM" "1" "17 Aug 2009" "Pascal Volk" +.TH "VMM" "1" "2012-04-15" "vmm 0.6" "vmm" .SH NAME -vmm \- Programm für die Kommandozeile, um E-Mail-Domains, -Konten und -Aliase zu -verwalten. -.SH SYNOPSIS +vmm \- Kommandozeilenprogramm zur Verwaltung von E\-Mail\-Domains/\-Konten +und \-Aliase. +.\" ----------------------------------------------------------------------- +.SH ÜBERSICHT .B vmm -\fIUnterbefehl\fP \fIObjekt\fP [ \fIArgumente\fP ] +.IR Unterbefehl " [" "Argument ..." ] +.\" ----------------------------------------------------------------------- .SH BESCHREIBUNG -\fBvmm\fP (Virtual Mail Manager) ist ein Kommandozeilen-Werkzeug für -Administratoren/Postmaster zur Verwaltung von Domains, Konten und Aliase. Es -wurde entwickelt für Dovecot und Postfix mit einem PostgreSQL-Backend. -.SH UNTERBEFEHLE -Von jedem Unterbefehl gibt es jeweils eine lange und kurze Variante. Bei beiden -Formen ist die Groß-/Kleinschreibung zu berücksichtigen. -.SS ALLGEMEINE UNTERBEFEHLE +.B vmm +(a virtual mail manager) ist das einfach zu bedienende +Kommandozeilenprogramm für Administratoren und Postmaster, zur Verwaltung +von (Alias\-) Domains, Konten, Alias\-Adressen und sogenannten Relocated +Users. +Es ermöglicht die schnelle und einfache Verwaltung des Mailservers. +.br +Es wurde für Dovecot und Postfix mit einem PostgreSQL\-Backend entwickelt. +.PP +Von jedem +.I Unterbefehl +gibt es jeweils eine lange und kurze Variante. +Die Kurzform ist in Klammern geschrieben. +Bei beiden Formen ist die Groß\-/Kleinschreibung zu berücksichtigen. +.PP +Die meisten +.IR Unterbefehl e +erwarten ein oder mehrere +.IR Argument e. +.\" ----------------------------------------------------------------------- +.SH ARGUMENTE +.TP 12 +.I address +Die komplette E\-Mail\-Adresse +.RI ( local\-part @ fqdn ) +eines Kontos, Aliases oder Relocated Users. +.\" -------------------------- .TP -\fBconfigure\fP (\fBcf\fP) [ \fIAbschnitt\fP ] -Startet den interaktiven Konfiguration-Modus für alle Konfigurations-Abschnitte. -.br -Wurde das optionale Argument \fIAbschnitt\fP angegeben, werden nur die Optionen -des angegebenen Abschnitts angezeigt und können geändert werden. Folgende -Abschnitte sind vorhanden: -.RS -.PD 0 +.I destination +Ist entweder eine E\-Mail\-Adresse, wenn sie in Verbindung mit +.I "ALIAS UNTERBEFEHLEN" +verwendet wird, oder ein +.I fqdn +in Verbindung mit +.IR "ALIASDOMAIN UNTERBEFEHLEN" . +.\" -------------------------- +.TP +.I fqdn +Der voll qualifizierten Domain\-Namen \(em ohne den abschließenden Punkt +\(em einer Domain oder Alias\-Domain. +.\" -------------------------- .TP -- -.B -database +.I messages +Ein Integer\-Wert, der das maximal nutzbare Kontingent als Anzahl von +Nachrichten festlegt. +.br +Der Wert +.B 0 +(null) bedeutet unbegrenzt \(em kein Quota\-Limit als Anzahl von +Nachrichten. +.\" -------------------------- .TP -- -.B -maildir +.I option +ist der Name einer Konfigurationsoption mit vorangestellter +Konfigurations\-Sektion, getrennt durch einen Punkt. +Zum Beispiel: +.IB misc . transport +.br +Alle Konfigurationsoptionen werden in +.BR vmm.cfg (5) +beschrieben. +.\" -------------------------- .TP -- -.B -services +.I service +Der Name eines Services, der gewöhnlicherweise in Verbindung mit Dovecot +genutzt wird. +Folgende Services werden unterstützt: +.BR imap ", " pop3 ", " sieve " und " smtp . +.\" -------------------------- .TP -- -.B -domdir +.I storage +Bestimmt das maximal nutzbare Kontingent in Bytes. +Eines der folgenden Präfixe kann dem dem ganzzahligen Wert angehängt +werden: +.BR b " (Bytes), " k " (Kilobytes), " M " (Megabytes) oder " G +(Gigabytes). +.br +Der Wert +.B 0 +(null) bedeutet unbegrenzt \(em kein Quota\-Limit in Bytes. +.\" -------------------------- .TP -- -.B -bin -.TP -- -.B -misc -.PD -.RE -.LP +.I transport +ein Transport für Postfix, angegeben in der Form: +.IB transport : +oder +.IB transport :\c +.IR nexthop . +Siehe +.BR transport (5) +für weitere Details. +.\" ----------------------------------------------------------------------- +.SH ALLGEMEINE UNTERBEFEHLE +.SS configget (cg) +.BI "vmm configget" " option" +.PP +Dieser Unterbefehl wird verwendet, um den aktuellen Wert der übergebenen +.I option +anzuzeigen. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm configure services\fP - Verwende Konfigurationsdatei: /usr/local/etc/vmm.cfg - - * Konfigurations Abschnitt: „services“ - Neuer Wert für Option pop3 [True]: - Neuer Wert für Option smtp [True]: - Neuer Wert für Option imap [True]: - Neuer Wert für Option sieve [True]: false +.B vmm configget misc.crypt_sha512_rounds +misc.crypt_sha512_rounds = 5000 +.fi +.\" -------------------------- +.SS configset (cs) +.B vmm configset +.I option value +.PP +Verwenden Sie diesen Unterbefehl, um einer einzelnen Konfigurationsoption +einen neuen Wert zuzuweisen. +.I option +ist der Name der Konfigurationsoption, +.I value +ist der Wert, der der Konfigurationsoption zugewiesen wird. +.IP Hinweis: +Diese Unterbefehl erstellt eine neue +.IR vmm.cfg , +ohne Kommentare. +Die aktuelle Konfigurationsdatei wird als +.IR vmm.cfg.bak +gesichert. +.PP +Beispiel: +.PP +.nf +.B vmm configget domain.transport +domain.transport = dovecot: +.B vmm configset domain.transport lmtp:unix:private/dovecot\-lmtp +.B vmm cg domain.transport +domain.transport = lmtp:unix:private/dovecot\-lmtp .fi +.\" ------------------------------------ +.SS configure (cf) +.B vmm configure +.RI [ section ] .PP +Startet die interaktiven Konfiguration für alle Konfigurationssektionen. +.PP +Dabei wird der aktuell konfigurierte Wert einer jeden Option in eckigen +Klammern ausgegeben. +Sollte kein Wert konfiguriert sein, wird der Vorgabewert der jeweiligen +Option in in eckigen Klammern angezeigt. +Um den angezeigten Wert unverändert zu übernehmen, ist dieser mit der +Eingabe\-Taste zu bestätigen. +.PP +Wurde das optionale Argument +.I section +angegeben, werden nur die Optionen der angegebenen Sektion angezeigt und +können geändert werden. +Folgende Sektionen sind vorhanden: +.RS +.TP 10 +.B account +Konto Einstellungen .TP -\fBgetuser\fP (\fBgu\fP) \fIuserid\fP -Wenn nur eine UserID vorhanden ist, z. B. aus der Prozessliste, kann mit dem -Unterbefehl \fBgetuser\fP die E-Mail-Adresse des Users ermittelt werden. +.B bin +Pfade zu externen Binär\-Dateien +.TP +.B database +Datenbank Einstellungen +.TP +.B domain +Domain Einstellungen +.TP +.B mailbox +Mailbox Einstellungen +.TP +.B misc +Verschiedene Einstellungen +.RE +.PP +Die Konfigurationsoptionen werden in +.BR vmm.cfg (5) +beschrieben. +.IP Hinweis: +Diese Unterbefehl erstellt eine neue +.IR vmm.cfg , +ohne Kommentare. +Die aktuelle Konfigurationsdatei wird als +.IR vmm.cfg.bak +gesichert. +.PP +Beispiel: .PP .nf - Beispiel: +.B vmm configure mailbox +Konfigurationsdatei wird verwendet: /root/vmm.cfg - \fBvmm getuser 70004\fP - Account Informationen - --------------------- - UID............: 70004 - GID............: 70000 - Address........: c.user@example.com +* Konfigurationsabschnitt: »mailbox« +Neuer Wert für Option folders [Drafts:Sent:Templates:Trash]: +Neuer Wert für Option format [maildir]: mdbox +Neuer Wert für Option subscribe [True]: +Neuer Wert für Option root [Maildir]: mdbox +.fi +.\" ------------------------------------ +.SS getuser (gu) +.BI "vmm getuser" " uid" +.PP +Wenn nur der +.I uid +eines Benutzers vorhanden ist, zum Beispiel aus der Prozessliste, kann mit +dem Unterbefehl +.B getuser +die E\-Mail\-Adresse des Benutzers ermittelt werden. +.PP +Beispiel: +.PP +.nf +.B vmm getuser 79876 +Konto Informationen +------------------- + UID............: 79876 + GID............: 70704 + Address........: a.user@example.com .fi -.\" -.TP -\fBlistdomains\fP (\fBld\fP) [ \fIMuster\fP ] -Dieser Unterbefehl listet alle verfügbaren Domains auf. Allen Domains wird ein -Präfix vorangestellt. Entweder ein '[+]', falls es sich um eine primäre Domain -handelt, oder ein '[-]', falls es sich um eine Alias-Domain handelt. Die -Ausgabe kann reduziert werden, indem ein optionales \fIMuster\fP angegeben wird. +.\" ------------------------------------ +.SS help (h) +.B vmm help +.RI [ subcommand ] +.PP +Gibt ein Liste aller vorhandenen Unterbefehle mit einer kurzen Beschreibung +aus. +Wurde ein +.I subcommand +angegeben, wird Hilfe zu diesem Unterbefehl ausgegeben. +Danach wird +.B vmm +beendet. +.\" ------------------------------------ +.SS listdomains (ld) +.B vmm listdomains +.RI [ pattern ] +.PP +Dieser Unterbefehl listet alle angelegten Domains auf. +Allen Domains wird ein Präfix vorangestellt. +Entweder ein `[+]', falls es sich um eine primäre Domain handelt, oder ein +`[-]', falls es sich um eine Alias\-Domain handelt. +Die Ausgabe kann reduziert werden, indem ein optionales Muster angegeben +wird. +.PP +Um eine Wildcard\-Suche durchzuführen kann das %\-Zeichen am Anfang +und/oder Ende des Musters verwendet werden. +.PP +Beispiel: +.PP +.nf +.B vmm listdomains %example% +Übereinstimmende Domains +------------------------ + [+] example.com + [\-] e.g.example.com + [\-] example.name + [+] example.org + [+] sales.example.com +.fi +.\" ------------------------------------ +.SS listpwschemes (lp) +.B vmm listpwschemes +.PP +Dieser Unterbefehl listet alle unterstützte Passwort\-Schemen, die als Wert +für +.I misc.password_scheme +in der +.I vmm.cfg +verwendet werden können. +Die Ausgabe variiert, je nach eingesetzter Dovecot Version und der libc des +Systems. .br -Um eine Wildcard-Suche durchzuführen kann das %-Zeichen am Anfang und/oder Ende -des \fIMusters\fP verwendet werden. +Sollte Ihre Dovecot\-Installation nicht zu alt sein, werden zusätzlich +die verwendbaren Encoding\-Suffixe ausgegeben. +Eines dieser Suffixe kann an das Passwort\-Schema angefügt werden. +.PP +Beispiel: .PP .nf - Beispiel: +.B vmm listpwschemes +Verfügbare Passwort-Schemata +---------------------------- + CRYPT SHA512-CRYPT LDAP-MD5 DIGEST-MD5 SHA256 SHA512 SSHA512 + SKEY SSHA NTLM RPA MD5-CRYPT HMAC-MD5 SHA1 PLAIN SHA CRAM-MD5 + SSHA256 MD5 LANMAN CLEARTEXT PLAIN-MD5 PLAIN-MD4 OTP SMD5 + SHA256-CRYPT - \fBvmm listdomains %example%\fP - Übereinstimmende Domains - ------------------------ - [+] example.com - [-] e.g.example.com - [-] example.name - [+] example.net - [+] example.org +Verwendbare Encoding-Suffixe +---------------------------- + .B64 .BASE64 .HEX .fi -.\" +.\" ------------------------------------ +.SS version (v) +.B vmm version +.PP +Gibt Versions\- und Copyright\-Informationen zu +.B vmm +aus. +Danach wird +.B vmm +beendet. +.\" ----------------------------------------------------------------------- +.SH DOMAIN UNTERBEFEHLE +.SS domainadd (da) +.B vmm domainadd +.IR fqdn " [" transport ] +.PP +Fügt eine neue Domain in die Datenbank ein und erstellt das +Domain\-Verzeichnis. +.PP +Wurde das optional Argument +.I transport +angegeben, ersetzt der angegebene Transport den konfigurierten Transport +.RI ( misc.transport ") aus " vmm.cfg . +Der angegebene +.I transport +ist der Vorgabe\-Transport für alle Konten, die dieser Domain zugeordnet +werden. +.PP +Konfigurationsbezogenes Verhalten: +.RS .TP -\fBhelp\fP (\fBh\fP) -Dieser Unterbefehl gibt alle verfügbaren Kommandos auf stdout aus. Danach -beendet sich \fBvmm\fP. -.TP -\fBversion\fP (\fBv\fP) -Gibt Versions-Informationen zu \fBvmm\fP aus. -.\" -.SS DOMAIN UNTERBEFEHLE +.I domain.auto_postmaster +Wenn diese Option den Wert +.B true +(Vorgabe) hat, wird +.B vmm +nach erfolgreichem Anlegen der Domain auch das Konto für +.BI postmaster@ fqdn +erstellen. .TP -\fBdomainadd\fP (\fBda\fP) \fIDomain\fP [ \fITransport\fP ] -Fügt eine neue \fIDomain\fP in die Datenbank ein. -.br -Ist das optionale Argument \fITransport\fP angegeben, wird der Vorgabe-Transport -aus \fBvmm.cfg\fP (misc/transport) für diese \fIDomain\fP ignoriert und der -angegebene \fITransport\fP verwendet. Der angegebene \fITransport\fP ist -gleichzeitig der Vorgabe-Transport für alle neuen Konten, die unter dieser -Domain eingerichtet werden. +.I account.random_password +Sollte dieser Option ebenfalls der Wert +.B true +zugewiesen sein, wird ein zufällig generiertes Passwort für den +Postmaster\-Account gesetzt und auf stdout ausgegeben. +.RE +.PP +Beispiele: .PP .nf - Beispiele: - - \fBvmm domainadd support.example.com smtp:mx1.example.com - vmm domainadd sales.example.com\fP +.B vmm domainadd support.example.com smtp:[mx1.example.com]:2025 +Konto für postmaster@support.example.com wird angelegt +Neues Passwort eingeben: +Neues Passwort wiederholen: +.B vmm cs account.random_password true +.B vmm domainadd vertrieb.example.com +Konto für postmaster@vertrieb.example.com wird angelegt +Erzeugtes Passwort: YoG3Uw*5aH .fi -.TP -\fBdomaininfo\fP (\fBdi\fP) \fIDomain\fP [ \fIdetails\fP ] -Dieser Unterbefehl zeigt Information zur angegeben \fIDomain\fP an. -.br -Um detaillierte Informationen über die \fIDomain\fP zu erhalten, kann das -optionale Argument \fIdetails\fP angegeben werden. Ein möglicher Wert für -\fIdetails\fP kann eines der folgenden fünf Schlüsselwörter sein: +.\" ------------------------------------ +.SS domaindelete (dd) +.BI "vmm domaindelete " fqdn +.RB [ force ] +.PP +Dieser Unterbefehl löscht die Domain mit dem angegebenen +.IR fqdn . +.PP +Sollten der zu löschenden Domain Konten, Aliase und/oder Relocated User +zugeordnet sein, wird +.B vmm +die Ausführung des Befehls mit einer entsprechenden Fehlermeldung beenden. +Sollten Sie sich Ihres Vorhabens sicher sein, so kann optional das +Schlüsselwort +.B force +angegeben werden. +.PP +Sollten Sie wirklich immer wissen was Sie tun, so editieren Sie Ihre +.I vmm.cfg +und setzen den Wert der Option +.I domain.force_deletion +auf +.BR true . +Dann werden Sie zukünftig beim Löschen von Domains nicht mehr wegen +vorhanden Konten, Aliase und/oder Relocated User gewarnt. +.\" ------------------------------------ +.SS domaininfo (di) +.B vmm domaininfo +.IR fqdn \ [ details ] +.PP +Dieser Unterbefehl zeigt Informationen zur Domain mit dem angegebenen +.I fqdn +an. +.PP +Um detaillierte Informationen über die Domain zu erhalten, kann das +optionale Argument +.I details +angegeben werden. +Ein möglicher Wert für +.I details +kann eines der folgenden fünf Schlüsselwörter sein: .RS -.PD 0 -.TP +.TP 13 .B accounts -um alle existierenden Konten aufzulisten +um alle eingerichteten Konten aufzulisten .TP .B aliasdomains -um alle zugeordneten Alias-Domains aufzulisten +um alle zugeordneten Alias\-Domains aufzulisten .TP .B aliases -um alle verfügbaren Alias-Adressen aufzulisten +um alle vorhandenen Alias\-Adressen aufzulisten .TP .B relocated -um alle Adressen der relocated Users aufzulisten +um alle Adressen der Relocated Users aufzulisten +.TP +.B catchall +um alle Catch\-all\-Ziele aufzulisten .TP .B full um alle oben genannten Informationen aufzulisten -.PD .RE -.LP +.PP +Beispiel: +.PP +.nf +.B vmm domaininfo sales.example.com +Domain Informationen +-------------------- + Domain Name......: sales.example.com + GID..............: 70708 + Domain Directory.: /srv/mail/c/70708 + Quota Limit/User.: Storage: 500,00 MiB; Messages: 10.000 + Active Services..: IMAP SIEVE + Transport........: lmtp:unix:private/dovecot-lmtp + Alias Domains....: 0 + Accounts.........: 1 + Aliases..........: 0 + Relocated........: 0 + Catch-All Dests..: 1 +.fi +.\" ------------------------------------ +.SS domainquota (dq) +.B vmm domainquota +.IR "fqdn storage" " [" messages ] +.RB [ force ] +.PP +Dieser Unterbefehl wird verwendet, um für die Konten der Domain ein neues +Quota\-Limit festzulegen. +.PP +Standardmäßig gilt für Konten das Quota\-Limit der +.IR vmm.cfg " (" domain.quota_bytes " und " domain.quota_messages ). +Das neue Quota\-Limit gilt für für alle bestehenden Konten, die nicht selbst +ein Quota\-Limit definieren. Soll das neue Quota\-Limit auch für Konten mit +eigenen Limiten angewendet werden, so ist das optionale Schlüsselwort +.B force +anzugeben. +.br +Wenn der Wert für das Argument +.I messages +ausgelassen wurde, wird der Vorgabewert +.B 0 +(null) als Anzahl von Nachrichten angewendet werden. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm domaininfo sales.example.com\fP - Domain Informationen - -------------------- - Domainname.....: sales.example.com - GID............: 70002 - Transport......: dovecot: - Domaindir......: /home/mail/5/70002 - Aliasdomains...: 0 - Accounts.......: 0 - Aliases........: 0 - Relocated......: 0 - +.B vmm domainquota example.com 1g force .fi -.TP -\fBdomaintransport\fP (\fBdt\fP) \fIDomain\fP \fITransport\fP [ \fIforce\fP ] -Ein neuer \fITransport\fP für die angegebene \fIDomain\fP kann mit diesem -Unterbefehl festgelegt werden. +.\" ------------------------------------ +.SS domainservices (ds) +.B vmm domainservices +.IR fqdn " [" "service ..." ] +.RB [ force ] +.PP +Um festzulegen, welche Services für die Anwender der Domain \(em mit dem +angegebenen +.I fqdn +\(em nutzbar sein sollen, wird dieser Unterbefehl verwendet. +.PP +Der Zugriff auf alle genannten Services wird den Anwender gestattet. +Der Zugriff auf nicht genannte Services wird verweigert werden. +Verwendbare +.IR service \-Namen +sind: +.BR imap ", " pop3 ", " sieve " und " smtp . .br -Wurde das optionale Schlüsselwort '\fBforce\fP' angegeben, so werden alle -bisherigen Transport-Einstellungen, der in dieser Domain vorhandenen Konten, -mit dem neuen \fITransport\fP überschrieben. -.br -Andernfalls gilt der neue \fITransport\fP nur für Konten, die neu erstellt -werden. +Wird das Schlüsselwort +.B force +angegeben, so werden alle kontospezifischen Einstellungen gelöscht und es +gelten fortan die Service\-Einstellungen der Domain für alle Konten. Ohne +dieses Schlüsselwort gelten die neuen Einstellungen nur für Konten, bei denen +die Service\-Einstellungen nicht individuell geändert wurden. +.\" ------------------------------------ +.SS domaintransport (dt) +.BI "vmm domaintransport" " fqdn transport" +.RB [ force ] +.PP +Ein neuer +.I transport +für die Domain mit dem angegebenen +.I fqdn +kann mit diesem Unterbefehl festgelegt werden. +.PP +Wird das Schlüsselwort +.B force +angegeben, so werden alle kontospezifischen Einstellungen gelöscht und es +gelten fortan die Transport\-Einstellungen der Domain für alle Konten. Ohne +dieses Schlüsselwort gelten die neuen Einstellungen nur für Konten, bei denen +die Transport\-Einstellungen nicht individuell geändert wurden. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm domaintransport support.example.com dovecot:\fP +.B vmm domaintransport support.example.com dovecot: .fi -.TP -\fBdomaindelete\fP (\fBdd\fP) \fIDomain\fP [ \fIdelalias\fP | \fIdeluser\fP | \fIdelall\fP ] -Mit diesem Unterbefehl wird die angegebene \fIDomain\fP gelöscht. -.br -Sollten der \fIDomain\fP Konten und/oder Aliase zugeordnet sein, wird \fBvmm\fP -die Ausführung des Befehls mit einer entsprechenden Fehlermeldung beenden. - -Sollten Sie sich Ihres Vorhabens sicher sein, so kann optional eines der -folgenden Schlüsselwörter angegeben werden: '\fBdelalias\fP', '\fBdeluser\fP' oder '\fBdelall\fP' - -Sollten Sie wirklich immer wissen was Sie tun, so editieren Sie Ihre -\fBvmm.cfg\fP und setzen den Wert der Option \fIforcedel\fP, im Abschnitt -\fImisc\fP, auf true. Dann werden Sie beim Löschen von Domains nicht mehr wegen -vorhanden Konten/Aliase gewarnt. -.\" -.SS ALIAS-DOMAIN UNTERBEFEHLE -.TP -\fBaliasdomainaddd\fP (\fBada\fP) \fIAliasdomain\fP \fIZieldomain\fP -Mit diesem Unterbefehl wird der \fIZieldomain\fP die Alias-Domain -\fIAliasdomain\fP zugewiesen. +.\" ------------------------------------ +.SS domainnote (do) +.BI "vmm domainnote" " fqdn" +.RI [ note ] +.PP +Mit diesem Unterbefehl kann eine Domain mit einer Notiz versehen werden. Um +die Notiz wieder zu löschen, läßt man sie einfach weg. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm aliasdomainadd example.name example.com\fP +.B vmm do example.com Gehört Robert .fi -.TP -\fBaliasdomaininfo (\fBadi\fP) \fIAliasdomain\fP -Dieser Unterbefehl informiert darüber, welcher Domain die Alias-Domain -\fIAliasdomain\fP zugeordnet ist. +.\" ----------------------------------------------------------------------- +.SH ALIAS\-DOMAIN UNTERBEFEHLE +Eine Alias\-Domain ist ein Alias für eine Domain, die zuvor mit dem +Unterbefehl +.B domainadd +erstellt wurde. +Alle Konten, Aliase und Relocated Users der Domain sind ebenfalls unter der +Alias\-Domain verfügbar. +.br +Im Folgenden wird angenommen, example.net sei ein Alias für example.com. +.PP +Postfix wird nicht erst fälschlicherweise E\-Mails für +unbekannten.user@example.net annehmen und später an den \(em oftmals +gefälschten \(em Absender bouncen. +Postfix wird E\-Mails an unbekannte Empfänger sofort ablehnen. +.br +Dieses Verhalten ist sichergestellt, solange die empfohlenen +Datenbankabfragen in +.I $config_directory/pgsql\-*.cf +konfiguriert sind. +.\" ------------------------------------ +.SS aliasdomainadd (ada) +.BI "vmm aliasdomainadd" " fqdn destination" +.PP +Dieser Unterbefehl legt die Alias\-Domain +.RI ( fqdn ) +als Alias für eine bestehende Domain +.RI ( destination ") an." +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm aliasdomaininfo example.name\fP - Alias-Domain Informationen - -------------------------- - Die Alias-Domain example.name gehört zu: - * example.com +.B vmm aliasdomainadd example.net example.com .fi -.TP -\fBaliasdomainswitch\fP (\fBads\fP) \fIAliasdomain\fP \fIZieldomain\fP -Wenn das Ziel der vorhandenen \fIAliasdomain\fP auf eine andere \fIZieldomain\fP -geändert werden soll, ist dieser Unterbefehl zu verwenden. +.\" ------------------------------------ +.SS aliasdomaindelete (add) +.BI "vmm aliasdomaindelete" " fqdn" +.PP +Verwenden Sie diesen Unterbefehl, um die Alias\-Domain +.I fqdn +zu löschen. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm aliasdomainswitch example.name example.org\fP +.B vmm aliasdomaindelete e.g.example.com .fi -.TP -\fBaliasdomaindelete\fP (\fBadd\fP) \fIAliasdomain\fP -Wenn die Alias-Domain mit dem Namen \fIAliasdomain\fP gelöscht werden soll, ist -dieser Unterbefehl zu verwenden. +.\" ------------------------------------ +.SS aliasdomaininfo (adi) +.BI "vmm aliasdomaininfo" " fqdn" +.PP +Dieser Unterbefehl gibt Informationen darüber aus, welcher Domain die +Alias\-Domain +.I fqdn +aktuell zugeordnet ist. +.PP +Beispiel: +.PP +.nf +.B vmm adi example.net +Alias\-Domain Informationen +-------------------------- + Die Alias\-Domain example.net gehört zu: + * example.com +.fi +.\" ------------------------------------ +.SS aliasdomainswitch (ads) +.BI "vmm aliasdomainswitch" " fqdn destination" +.PP +Wenn Sie die bereits vorhandene Alias\-Domain +.I fqdn +einer anderen Ziel\-Domain zuordnen wollen, verwenden Sie diesen +Unterbefehl. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm aliasdomaindelete e.g.example.com\fP +.B vmm aliasdomainswitch example.net example.org .fi -.\" -.SS KONTO UNTERBEFEHLE -.TP -\fBuseradd\fP (\fBua\fP) \fIAdresse\fP [ \fIPasswort\fP ] -Mit diesem Unterbefehl wird ein neues Konto für die angegebene \fIAdresse\fP +.\" ----------------------------------------------------------------------- +.SH KONTO UNTERBEFEHLE +.SS useradd (ua) +.B vmm useradd +.IR address " [" password ] +.PP +Mit diesem Unterbefehl wird ein neues Konto für die angegebene Adresse angelegt. -.br -Wurde kein \fIPasswort\fP angegeben wird \fBvmm\fP dieses im interaktiven -Modus erfragen. +.PP +Wurde kein Passwort angegeben wird +.B vmm +dieses im interaktiven Modus erfragen. +Falls kein Passwort angegeben wurde und +.I account.random_password +den Wert +.B true +hat, wird +.B vmm +ein zufälliges Passwort generieren und auf stdout ausgeben, nachdem das +Konto angelegt wurde. +.PP +Beispiele: .PP .nf - Beispiele: - - \fBvmm ua d.user@example.com 'A 5ecR3t P4s5\\/\\/0rd'\fP - \fBvmm ua e.user@example.com\fP - Neues Passwort eingeben: - Neues Passwort wiederholen: +.B vmm ua d.user@example.com \(dqA 5ecR3t P4s5\(rs/\(rs/0rd\(dq +.B vmm useradd e.user@example.com +Neues Passwort eingeben: +Neues Passwort wiederholen: .fi -.TP -\fBuserinfo\fP (\fBui\fP) \fIAdresse\fP [ \fIdetails\fP ] -Dieser Unterbefehl zeigt einige Informationen über das Konto mit der angegebenen -\fIAdresse\fP an. -.br -Wurde das optionale Argument \fIdetails\fP angegeben, werden weitere -Informationen ausgegeben. -.br -Mögliche Werte für \fIdetails\fP sind: +.\" ------------------------------------ +.SS userdelete (ud) +.BI "vmm userdelete" " address" +.RB [ force ] +.PP +Verwenden Sie diesen Unterbefehl, um das Konto mit der angegebenen Adresse +zu löschen. +.PP +Sollte es einen oder mehrere Aliase geben, deren Ziel\-Adresse mit der +Adresse des zu löschenden Kontos identisch ist, wird +.B vmm +die Ausführung des Befehls mit einer entsprechenden Fehlermeldung beenden. +Um dieses zu umgehen, kann das optionale Schlüsselwort +.B force +angegebenen werden. +.\" ------------------------------------ +.SS userinfo (ui) +.B "vmm userinfo" +.IR address " [" details ] +.PP +Dieser Unterbefehl zeigt einige Informationen über das Konto mit der +angegebenen Adresse an. +.PP +Wurde das optionale Argument +.I details +angegeben, werden weitere Informationen ausgegeben. +Mögliche Werte für +.I details +sind: .RS -.PD 0 -.TP +.TP 8 .B aliases -um alle Alias-Adressen, mit dem Ziel \fIAdresse\fP, aufzulisten +um alle Alias\-Adressen, mit dem Ziel +.IR address , +aufzulisten .TP .B du -um zusätzlich die Festplattenbelegung des Kontos anzuzeigen +um zusätzlich die Festplattenbelegung des Mail\-Verzeichnisses eines Kontos +anzuzeigen. +Soll die Festplattenbelegung jedes Mal mit der +.B userinfo +ermittelt werden, ist in der +.I vmm.cfg +der Wert der Option +.I account.disk_usage +auf +.B true +zu setzen. .TP .B full um alle oben genannten Informationen anzuzeigen -.PD .RE -.LP -.TP -\fBusername\fP (\fBun\fP) \fIAdresse\fP \fI'Bürgerlicher Name'\fP -Der Bürgerliche Name des Konto-Inhabers mit der angegebenen \fIAdresse\fP kann -mit diesem Unterbefehl gesetzt/aktualisiert werden. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm un d.user@example.com 'John Doe'\fP +.B vmm userinfo d.user@example.com +Konto Informationen +------------------- + Address..........: d.user@example.com + Name.............: None + UID..............: 79881 + GID..............: 70704 + Home.............: /srv/mail/2/70704/79881 + Mail_Location....: mdbox:~/mdbox + Quota Storage....: [ 0,00%] 0/500,00 MiB + Quota Messages...: [ 0,00%] 0/10.000 + Transport........: lmtp:unix:private/dovecot-lmtp + SMTP.............: deaktiviert + POP3.............: deaktiviert + IMAP.............: aktiviert + SIEVE............: aktiviert .fi -.TP -\fBuserpassword\fP (\fBup\fP) \fIAdresse\fP [ \fIPasswort\fP ] -Das \fIPasswort\fP eines Kontos kann mit diesem Unterbefehl aktualisiert werden. -.br -Wurde kein \fIPasswort\fP angegeben wird \fBvmm\fP dieses im interaktiven -Modus erfragen. +.\" ------------------------------------ +.SS username (un) +.BI "vmm username" " address" +.RI [ name ] +.PP +Der bürgerliche Name des Kontoinhabers mit der angegebenen Adresse kann mit +diesem Unterbefehl gesetzt/aktualisiert werden. +.PP +Wird kein +.I name +übergeben, so wird der Wert in den Kontoinformationen gelöscht. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm up d.user@example.com 'A |\\/|0r3 5ecur3 P4s5\\/\\/0rd?'\fP +.B vmm username d.user@example.com \(dqJohn Doe\(dq .fi -.TP -\fBusertransport\fP (\fBut\fP) \fIAdresse\fP \fITransport\fP -Mit diesem Unterbefehl kann ein abweichender \fITransport\fP für das Konto mit -der angegebenen \fIAdresse\fP festgelegt werden. +.\" ------------------------------------ +.SS userpassword (up) +.BI "vmm userpassword" " address" +.RI [ password ] +.PP +Das Passwort eines Kontos kann mit diesem Unterbefehl aktualisiert werden. +.PP +Wurde kein Passwort angegeben, wird +.B vmm +dieses im interaktiven Modus erfragen. +.PP +Beispiel: +.PP +.nf +.B vmm up d.user@example.com \(dqA |\(rs/|0r3 5ecur3 P4s5\(rs/\(rs/0rd?\(dq +.fi +.\" ------------------------------------ +.SS usernote (uo) +.BI "vmm usernote" " address" +.RI [ note ] +.PP +Mit diesem Unterbefehl kann ein Konto mit einer Notiz versehen werden. Um die +Notiz wieder zu löschen, läßt man sie einfach weg. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm ut d.user@example.com smtp:pc105.it.example.com\fP +.B vmm uo d.user@example.com Wird nur bis Ende Mai 2012 gebraucht .fi -.TP -\fBuserdisable\fP (\fBu0\fP) \fIAdresse\fP [ \fIService\fP ] -Soll ein Anwender keinen Zugriff auf einen oder alle Service haben, kann der -Zugriff mit diesem Unterbefehl beschränkt werden. -.br -Wurde weder ein \fIService\fP noch das Schlüsselwort '\fIall\fP' angegeben, -werden alle Services (\fIsmtp\fP, \fIpop3\fP, \fIimap\fP, und \fIsieve\fP) -für das Konto mit der angegebenen \fIAdresse\fP deaktiviert. -.br -Andernfalls wird nur der Zugriff auf den angegeben \fIService\fP gesperrt. +.\" ------------------------------------ +.SS userquota (uq) +.BI "vmm userquota" " address storage" +.RI [ messages ] +.PP +Um ein neues Quota\-Limit für das Konto mit der angegebenen Adresse +festzulegen, wird dieser Unterbefehl verwendet. +.PP +Wenn der Wert für das Argument +.I messages +ausgelassen wurde, wird der Vorgabewert +.B 0 +(null) als Anzahl von Nachrichten angewendet werden. +.PP +Anstelle einer Limite, bewirkt das Wort 'domain', daß die Limite des +Kontos gelöscht wird und somit wieder der in der Domain gespeicherte +Wert für das Konto gilt. +.PP +Beispiel: +.PP +.nf +.B vmm userquota d.user@example.com 750m +.B vmm userquote d.user@example.com domain +.fi +.\" ------------------------------------ +.SS userservices (us) +.B vmm userservices +.IR address " [" "service ..." ] +.PP +Verwenden Sie diesen Unterbefehl, um einem Anwender den Zugriff auf die +genannten Services zu gestatten. +.PP +Der Zugriff auf alle nicht genannten Services wird dem Anwender, mit der +angegebenen Adresse, verwehrt werden. +.PP +Anstelle einer Liste, bewirkt das Wort 'domain', daß die benutzerspezifische +Liste gelöscht wird und somit wieder die in der Domain gespeicherte +Liste für das Konto gilt. +.PP +Beispiel: .PP .nf - Beispiele: - - \fBvmm u0 b.user@example.com imap\fP - \fBvmm userdisable c.user@example.com\fP -.fi +.B vmm userservices d.user@example.com SMTP IMAP +.B vmm userservices d.user@example.com domain +.\" ------------------------------------ +.SS usertransport (ut) +.BI "vmm usertransport" " address transport" .PP -.TP -\fBuserenable\fP (\fBu1\fP) \fIAdresse\fP [ \fIService\fP ] -Um den Zugriff auf bestimmte oder alle gesperrten Service zu gewähren, wird -dieser Unterbefehl verwendet. -.br -Wurde weder ein \fIService\fP noch das Schlüsselwort '\fIall\fP' angegeben, -werden alle Services (\fIsmtp\fP, \fIpop3\fP, \fIimap\fP, und \fIsieve\fP) -für das Konto mit der angegebenen \fIAdresse\fP aktiviert. +Mit diesem Unterbefehl kann ein abweichender +.I transport +für das Konto mit der angegebenen Adresse bestimmt werden. +.PP +Wird als +.I transport +das Wort 'domain' übergeben, so wird der explizite Transport des Kontos +wieder gelöscht und der in der Domain gespeicherte Wert benutzt. +.PP +Beispiel: .br -Andernfalls wird nur der Zugriff auf den angegeben \fIService\fP gestattet. +Angenommen, Sie wollen mit Dovecots +.BR dsync (1) +die E\-Mails vom Maildir\-Format ins mdbox\-Format konvertieren, dann +können Sie Postfix, über den Transport, darüber informieren, es später +nochmals zu versuchen. .PP -.TP -\fBuserdelete\fP (\fBud\fP) \fIAdresse\fP [ \fIdelalias\fP ] -Verwenden Sie diesen Unterbefehl um, das Konto mit der angegebenen \fIAdresse\fP -zu löschen. -.br -Sollte es einen oder mehrere Aliase geben, deren Zieladresse mit der des Kontos -identisch ist, wird \fBvmm\fP die Ausführung des Befehls mit einer -entsprechenden Fehlermeldung beenden. Um dieses zu umgehen, kann das optionale -Schlüsselwort '\fIdelalias\fP' angegebenen werden. -.\" -.SS ALIAS UNTERBEFEHLE -.TP -\fBaliasadd\fP (\fBaa\fP) \fIAlias\fP \fIZiel\fP -Mit diesem Unterbefehl werden neue Aliase erstellt. +.nf +.B vmm ut d.user@example.com \(dqretry:4.0.0 Mailbox being migrated\(dq +# Konvertieren der Mailbox … +# … danach den Transport auf den Domainwert setzen +.B vmm usertransport d.user@example.com domain +.fi +.\" ----------------------------------------------------------------------- +.SH ALIAS UNTERBEFEHLE +.SS aliasadd (aa) +.BI "vmm aliasadd" " address destination ..." +.PP +Mit diesem Unterbefehl werden neue Alias\-Adressen, mit einer oder mehren +.IR destination (en), +erstellt. +.PP +Innerhalb der Zieladresse werden die Zeichenketten +.IR %n , +.IR %d +und +.IR %= +durch den ursprünglichen lokalen Teil, die Domain bzw. die Emailadresse mit +'=' anstelle von '@' ersetzt. Dies ermöglicht z.B. in Verbindung mit +Alias\-Domains domain\-spezifische Empfänger. +.PP +Beispiele: .PP .nf - Beispiele: - - \fBvmm aliasadd john.doe@example.com d.user@example.com\fP - \fBvmm aa support@example.com d.user@example.com\fP - \fBvmm aa support@example.com e.user@example.com\fP +.B vmm aliasadd john.doe@example.com d.user@example.com +.B vmm aa support@example.com d.user@example.com e.user@example.com +.B vmm aa postmaster@example.com postmaster+%d@example.org .fi -.TP -\fBaliasinfo\fP (\fBai\fP) \fIAlias\fP -Informationen zu einem Alias können mit diesem Unterbefehl ausgegeben werden. +.\" ------------------------------------ +.SS aliasdelete (ad) +.BI "vmm aliasdelete" " address" +.RI [ destination ] +.PP +Verwenden Sie diesen Unterbefehl um den Alias mit der angegebenen Adresse +zu löschen. +.PP +Wurde eine optionale +.I destination +angegeben, so wird nur diese +.I destination +vom angegebenen Alias entfernt. +.PP +Beispiel: +.PP +.nf +.B vmm aliasdelete support@example.com d.user@example.com +.fi +.\" ------------------------------------ +.SS aliasinfo (ai) +.BI "vmm aliasinfo" " address" +.PP +Informationen zum Alias mit der angegebenen Adresse können mit diesem +Unterbefehl ausgegeben werden. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm aliasinfo support@example.com\fP - Alias Informationen - ------------------- - E-Mails für support@example.com werden weitergeleitet an: - * d.user@example.com - * e.user@example.com +.B vmm aliasinfo support@example.com +Alias Informationen +------------------- + E\-Mails für support@example.com werden weitergeleitet an: + * e.user@example.com .fi -.TP -\fBaliasdelete\fP (\fBad\fP) \fIAlias\fP [ \fIZiel\fP ] -Verwenden Sie diesen Unterbefehl um den angegebenen \fIAlias\fP zu löschen. -.br -Wurde die optionale Zieladresse \fIZiel\fP angegeben, so wird nur diese -Zieladresse vom angegebenen \fIAlias\fP entfernt. +.\" ----------------------------------------------------------------------- +.SH RELOCATED UNTERBEFEHLE +.SS relocatedadd (ra) +.BI "vmm relocatedadd" " address newaddress" +.PP +Um einen neuen Relocated User anzulegen kann dieser Unterbefehl verwendet +werden. +.PP +Dabei ist +.I address +die ehemalige Adresse des Benutzers, zum Beispiel b.nutzer@example.com, und +.I newaddress +die neue Adresse, unter der die/der Benutzer/in erreichbar ist. +.PP +Beispiel: +.PP +.nf +.B vmm relocatedadd b.nutzer@example.com b\-nutzer@firma.tld +.fi +.\" ------------------------------------ +.SS relocatedinfo (ri) +.BI "vmm relocatedinfo " address +.PP +Dieser Unterbefehl zeigt die neue Adresse des Relocated Users mit mit der +angegebenen Adresse. +.PP +Beispiel: +.PP +.nf +.B vmm relocatedinfo b.nutzer@example.com +Verschiebe\-Informationen +------------------------ + Der Benutzer »b.nutzer@example.com« wurde nach »b\-nutzer@firma.tld« verschoben +.fi +.\" ------------------------------------ +.SS relocateddelete (rd) +.BI "vmm relocateddelete " address +.PP +Mit diesem Unterbefehl kann der Relocated User mit der angegebenen Adresse +gelöscht werden. +.PP +Beispiel: .PP .nf - Beispiel: - \fBvmm ad support@example.com d.user@example.com\fP +.B vmm relocateddelete b.nutzer@example.com .fi -.\" -.SS RELOCATED UNTERBEFEHLE -.TP -\fBrelocatedadd\fP (\fBra\fP) \fIalte_adresse\fP \fIneue_adresse\fP -Um einen neuen relocated User anzulegen kann dieser Unterbefehl verwendet -werden. -.br -Dabei ist \fIalte_adresse\fP die ehemalige Adresse des Benutzers, z. B. -b.user@example.com, und \fIneue_adresse\fP die neue Adresse, unter der Benutzer -erreichbar ist. +.\" ----------------------------------------------------------------------- +.SH CATCH\-ALL UNTERBEFEHLE +.SS catchalladd (caa) +.BI "vmm catchalladd" " fqdn destination ..." +.PP +Mit diesem Unterbefehl können für eine Domain Adressen definiert werden, an +die E\-Mails geleitet werden, die an nicht\-existente Adressen innerhalb +dieser Domains adressiert sind. +Diese Adressen \(dqfangen alle\(dq diese E\-Mails auf, es sei denn es +bestehen spezifischere Aliase, Mailboxen oder Relocated\-Einträge. +.PP +WARNUNG: Catch\-all Adressen können dazu führen, daß ein Mailserver von Spam +überflutet wird, da Spammer zuweilen gerne alle möglichen Emailadressen +ausprobieren und man auf einmal zig tausend Nachrichten gerichtet an +Adressen von abba@example.org bis zztop@example.org weitergeleitet bekommt. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm relocatedadd b.user@example.com b-user@company.tld\fP +.B vmm catchalladd example.com b.nutzer@example.org .fi -.TP -\fBrelocatedinfo\fP (\fBri\fP) \fIalte_adresse\fP -Dieser Unterbefehl zeigt die neue Adresse des relocated Users mit -\fIalte_adresse\fP. +.\" ------------------------------------ +.SS catchallinfo (cai) +.BI "vmm catchallinfo " fqdn +.PP +Dieser Unterbefehl zeigt die für eine Domain definierten Catch\-all Aliase +an. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm relocatedinfo b.user@example.com\fP - Relocated Informationen - ----------------------- - Der Benutzer „b.user@example.com“ ist erreichbar unter „b-user@company.tld“ +.B vmm catchallinfo example.com +Catch-all Informationen +----------------------- + Nachrichten an unbekannte Adressen innerhalb der example.com Domäne werden + weitergeleitet an: + * b.nutzer@example.org .fi -.TP -\fBrelocateddelete\fP (\fBrd\fP) \fIalte_adresse\fP -Mit diesem Unterbefehl kann der relocated User mit \fIalte_adresse\fP gelöscht -werden. +.\" ------------------------------------ +.SS catchalldelete (cad) +.BI "vmm catchalldelete " fqdn +.RI [ destination ] +.PP +Mit diesem Unterbefehl werden Catch\-all Aliase einer Domain wieder +gelöscht, entweder nur das angegebene Alias, oder alle, wenn keines +angegeben wurde. +.PP +Beispiel: .PP .nf - Beispiel: - - \fBvmm relocateddelete b.user@example.com\fP +.B vmm catchalldelete example.com b.nutzer@example.org .fi -.\" +.\" ----------------------------------------------------------------------- .SH DATEIEN -/usr/local/etc/vmm.cfg +.TP +.I /root/vmm.cfg +Wird verwendet, falls vorhanden. +.TP +.I /usr/local/etc/vmm.cfg +Wird verwendet, sollte obige Datei nicht gefunden werden. +.TP +.I /etc/vmm.cfg +Wird verwendet, falls die oben genannten Dateien nicht existieren. +.\" ----------------------------------------------------------------------- .SH SIEHE AUCH -vmm.cfg(5), Konfigurationsdatei für vmm -.SH AUTOR -\fBvmm\fP und die dazugehörigen Manualseiten wurden von Pascal Volk -<\fIneverseen@users.sourceforge.net\fP> geschrieben und sind unter den -Bedingungen der BSD Lizenz lizenziert. +.BR dsync (1), +.BR transport (5), +.BR vmm.cfg (5) +.\" ----------------------------------------------------------------------- +.SH INTERNET RESSOURCEN +.TP +Wiki +http://de.vmm.localdomain.org/ +.TP +Projekt\-Seite +http://sf.net/projects/vmm/ +.TP +Bugtracker +https://bitbucket.org/pvo/vmm/issues +.\" ----------------------------------------------------------------------- +.SH COPYING +vmm und die dazugehörigen Manualseiten wurden von Pascal Volk geschrieben und sind unter den Bedingungen der +BSD Lizenz lizenziert. diff -r c0e1fb1b0145 -r a4aead244f75 man/de/man5/vmm.cfg.5 --- a/man/de/man5/vmm.cfg.5 Mon Nov 07 03:22:15 2011 +0000 +++ b/man/de/man5/vmm.cfg.5 Thu Jun 28 19:26:50 2012 +0000 @@ -1,45 +1,86 @@ -.TH vmm.cfg 5 "17 Aug 2009" "Pascal Volk" +.TH "VMM.CFG" "5" "2011-11-12" "vmm 0.6" "vmm" .SH NAME vmm.cfg \- Konfigurationsdatei für vmm -.SH SYNOPSIS +.\" ----------------------------------------------------------------------- +.SH ÜBERSICHT vmm.cfg +.\" ----------------------------------------------------------------------- .SH BESCHREIBUNG -\fBvmm\fR(1) liest Konfigurationsparameter aus der Datei \fIvmm.cfg\fP. -.br -Die Konfigurationsdatei ist in mehrere Abschnitte unterteilt. Jeder Abschnitt -wird mit dem, in eckigen Klammern '[' und ']' eingefassten, Namen des Abschnitts -eingeleitet (z. B. \fB[database]\fP), gefolgt von \'Option = Wert\' Einträgen -(Z. B. \fBhost = 127.0.0.1\fP). -.br -Leerräume um das Gleichheitszeichen '=' und am Ende eine Wertes werden +.BR vmm (1) +liest seine Konfigurationsparameter aus der Datei +.IR vmm.cfg . +.PP +Die Konfigurationsdatei ist in mehrere Sektionen unterteilt. +Jede Sektion wird mit dem in eckigen Klammern +.RB ' [ "' und '" ] ' +eingefassten Namen der Sektion eingeleitet, gefolgt von +.RI ' Option " = " Wert ' +Einträgen. +.PP +Leerräume um das Gleichheitszeichen '=' und am Ende eines Wertes werden ignoriert. -.PP Leerzeilen und Zeilen, die mit einer '#' oder einem ';' anfangen, werden ignoriert. .PP Jeder Wert ist von einem der folgenden Datentypen: -.IP \(bu +.PP +.TP 8 .I Boolean -um zu bestimmen, ob etwas eingeschaltet/aktiviert (true) oder +um festzulegen, ob etwas eingeschaltet/aktiviert (true) oder ausgeschaltet/deaktiviert (false) ist. .br -Mögliche Werte für \fBtrue\fP sind: \fB1\fP, \fByes\fP, \fBtrue\fP und \fBon\fP. +Mögliche Werte für +.I true +sind: +.BR 1 , " yes" , " true" " und " on . .br -Mögliche Werte für \fBfalse\fP sind: \fB0\fP, \fBno\fP, \fBfalse\fP und -\fBoff\fP. -.IP \(bu +Mögliche Werte für +.I false +sind: +.BR 0 , " no" , " false" " und " off . +.TP .I Int -eine Integer-Zahl, geschrieben ohne eine gebrochene oder dezimale Komponente. +eine Integer\-Zahl, geschrieben ohne eine gebrochene oder dezimale +Komponente. .br -Beispielsweise sind \fB1\fP, \fB50\fP oder \fB321\fP Integer-Zahlen. -.IP \(bu +Beispielsweise +.BR 1 , " 50" " oder " 321 +sind Integer\-Zahlen. +.TP .I String eine Folge von Buchstaben und Zahlen. .br -Zum Beispiel: '\fBWort\fP', '\fBHallo Welt\fP', oder '\fB/usr/bin/strings\fP' -.SS SUCHREIHENFOLGE -Standardmäßig sucht vmm die \fIvmm.cfg\fP in folgenden Verzeichnissen, in dieser -Reihenfolge: +Zum Beispiel: +.RB ' Wort "', '" "Hallo Welt" "' oder '" /usr/bin/strings ' +.PP +Die meisten Optionen haben einen Vorgabewert. +Dieser ist nach dem Namen der Option in Klammern angegebenen. +Um den Vorgabewert einer Option zu verwenden, wird die entsprechende Zeile +entweder mit +.BR # " oder " ; +auskommentiert oder die Zeile wird einfach aus der +.I vmm.cfg +entfernt. +.PP +Eine minimale +.I vmm.cfg +könnte so aussehen: +.PP +.nf +[database] +user = ich +pass = xxxxxxxx + +[misc] +dovecot_version = 1.2.16 +.fi +.\" ----------------------------------------------------------------------- +.SH SUCHREIHENFOLGE +Standardmäßig sucht +.BR vmm (1) +die +.I vmm.cfg +in folgenden Verzeichnissen, in der angegebenen Reihenfolge: .RS .PD 0 .TP @@ -55,219 +96,530 @@ .RE .PP Die zuerst gefundene Datei wird verwendet. -.\" ----- -.SH DATABASE ABSCHNITT -Der \fBdatabase\fP-Abschnitt wird verwendet, um die für den Datenbankzugriff -benötigten Optionen festzulegen. -.TP -\fBhost\fP (\fIString\fP) -Der Hostname oder die IP-Adresse des Datenbank-Servers. -.TP -\fBuser\fP (\fIString\fP) -Der Name des Datenbank-Benutzers. -.TP -\fBpass\fP (\fIString\fP) -Das Passwort des Datenbank-Benutzers -.TP -\fBname\fP (\fIString\fP) -Name der zu verwendenden Datenbank. -.TP -\fBBeispiel\fP: -[database] -.br -host = localhost -.br -user = vmm -.br -pass = T~_:L4OYyl]TU?) +.\" ----------------------------------------------------------------------- +.SH SEKTION ACCOUNT +Die Optionen der Sektion +.B account +legen Konto\-spezifische Einstellungen fest. +.SS account.delete_directory +.BR delete_directory " (Vorgabe: false) :" +.I Boolean +.PP +Bestimmt das Verhalten von +.BR vmm (1) +beim Löschen eines Kontos (userdelete). +Wenn der Wert dieser Option +.I true +ist, wird das Home\-Verzeichnis des zu löschenden Anwenders rekursiv +gelöscht. +.\" ------------------------------------ +.SS account.directory_mode +.BR directory_mode " (Vorgabe: 448) :" +.I Int +.PP +Zugriffsbits des Home\-Verzeichnisses, sowie aller enthaltenen +Verzeichnisse, in Dezimal\-Schreibweise (Basis 10). .br -name = mailsys -.\" ----- -.SH MAILDIR ABSCHNITT -Im \fBmaildir\fP-Abschnitt werden die für die Maildirs erforderlichen Optionen -festgelegt. -.TP -\fBname\fP (\fIString\fP) -Standard-Name des Maildir-Verzeichnisses im Verzeichnis des jeweiligen -Anwenders. -.TP -\fBfolders\fP (\fIString\fP) -Eine durch Doppelpunkten getrennte Liste mit Verzeichnisnamen, die innerhalb des -Maildirs erstellt werden sollen. -.br -Sollen innerhalb des Maildirs keine Verzeichnisse angelegt werden, ist dieser -Optionen ein einzelner Doppelpunkt (':') als Wert zuzuweisen. -.TP -\fBmode\fP (\fIInt\fP) -Zugriffsbits des Maildirs in Dezimal-Schreibweise (Basis 10). -.br -Beispiel: \'drwx------' -> oktal 0700 -> dezimal 448 -.TP -\fBdiskusage\fP (\fIBoolean\fP) -Legt fest, ob die Festplattenbelegung des Maildirs jedes Mal, wenn -Konto-Informationen ausgegeben werden, ermittelt und mit ausgegeben werden -sollen. +Beispiel: 'drwx\-\-\-\-\-\-' \(-> oktal 0700 \(-> dezimal 448 +.\" ------------------------------------ +.SS account.disk_usage +.BR disk_usage " (Vorgabe: false) :" +.I Boolean +.PP +Legt fest, ob die Festplattenbelegung des Maildirs eines Benutzers jedes +Mal mit +.BR du (1) +ermittelt und mit den Konto\-Informationen ausgegeben werden soll. +.PP +Bei umfangreichen Maildirs kann das langsam sein. +Falls Sie Quotas aktiviert haben, wird der +.BR vmm\-Unterbefehl +userinfo ebenfalls die aktuelle Quota\-Nutzung des Kontos mit ausgegeben. +Sie können auch eines der optionalen Argumente +.BR du " oder " full +an userinfo übergeben, um sich die aktuelle Festplattenbelegung anzeigen zu +lassen. +.\" ------------------------------------ +.SS account.password_length +.BR password_length " (Vorgabe: 8) :" +.I Int +.PP +Diese Option legt die Anzahl der Zeichen für automatisch erzeugte +Passwörter fest. +Alle Werte kleiner als 8 werden auf 8 erhöht. +.\" ------------------------------------ +.SS account.random_password +.BR random_password " (Vorgabe: false) :" +.I Boolean +.PP +Mit dieser Option wird bestimmt, ob +.BR vmm (1) +ein zufälliges Passwort generieren soll, wenn kein Passwort an den +Unterbefehl useradd übergeben wurde. +Ist der Wert dieser Option +.IR false , +wird +.B vmm +Sie auffordern, ein Passwort für den neuen Account einzugeben. +.PP +Sie können die Länge für automatisch generierte Passwörter mit der Option +.I account.password_length +konfigurieren. +.\" ----------------------------------------------------------------------- +.SH SEKTION BIN +In der +.BR bin \-Sektion +werden die Pfade zu den von +.BR vmm (1) +benötigten Binaries angegeben. +.SS bin.dovecotpw +.BR dovecotpw " (Vorgabe: /usr/sbin/dovecotpw) :" +.I String +.PP +Der absolute Pfad zum dovecotpw Binary. +Geben Sie den absoluten Pfad zum +.BR doveadm (1) +Binary an, falls Sie Dovecot v2.0 verwenden. +.PP +Dieses Binary wird zur Hash\-Erzeugung verwendet, wenn +.I misc.password_scheme +einen der nachfolgenden Werte hat: 'CRAM\-MD5', 'HMAC\-MD5', 'LANMAN', +\(aqOTP', 'RPA' oder 'SKEY'. +Dieses Binary wird auch benötigt, wenn Ihre Python\-Installation einen der +folgenden Hash\-Algorithmen nicht unterstützt: +.IP \(bu 4 +md4: (hashlib + OpenSSL oder PyCrypto) verwendet für die Passwort\-Schemen: +\(aqPLAIN\-MD4' und 'NTLM' +.IP \(bu +sha256: (hashlib oder PyCrypto \(>= 2.1.0alpha1) verwendet für die +Passwort\-Schemen: 'SHA256' und 'SSHA256' +.IP \(bu +sha512: (hashlib) verwendet für die Passwort\-Schemen: 'SHA512' und +\(aqSSHA512' +.PP +Das +.BR doveadm (1) +Binary wird auch gebraucht, um die INBOX und zusätzliche Mailboxen +.RI ( mailbox.folders ) +für einen neuen Account zu erstellen, wenn die Option +.I mailbox.format +den Wert +.BR mdbox " oder " sdbox +hat. +.\" ------------------------------------ +.SS bin.du +.BR du " (Vorgabe: /usr/bin/du) :" +.I String +.PP +Der absolute Pfad zu +.BR du (1). +Dieses Binary wird verwendet, wenn die Festplattenbelegung eines Kontos +ermittelt wird. +.\" ------------------------------------ +.SS bin.postconf +.BR postconf " (Vorgabe: /usr/sbin/postconf) :" +.I String +.PP +Der absolute Pfad zu Postfix' +.BR postconf (1). +Dieses Binary wird verwendet, wenn +.BR vmm (1) +diverse Postfix\-Einstellungen prüft, zum Beispiel das +.IR virtual_alias_expansion_limit . +.\" ----------------------------------------------------------------------- +.SH SEKTION DATABASE +Die +.BR database \-Sektion +wird verwendet, um die für den Datenbankzugriff erforderlichen Optionen +festzulegen. +.SS database.host +.BR host " (Vorgabe: localhost) :" +.I String +.PP +Der Hostname oder die IP\-Adresse des Datenbankservers. +.\" ------------------------------------ +.SS database.module +.BR module " (Vorgabe: psycopg2) :" +.I String +.PP +Das für den Datenbankzugriff zu verwendende Python PostgreSQL Adapter +Modul. +Unterstützte Module sind +.BR psycopg2 " und " pyPgSQL . +.\" ------------------------------------ +.SS database.name +.BR name " (Vorgabe: mailsys) :" +.I String +.PP +Der Name der zu verwendenden Datenbank. +.\" ------------------------------------ +.SS database.pass +.BR pass " (Vorgabe: " None ") :" +.I String +.PP +Das Passwort des Datenbank\-Benutzers. +.\" ------------------------------------ +.SS database.port +.BR port " (Vorgabe: 5432) :" +.I Int +.PP +Der TCP\-Port, auf dem der Datenbankserver Verbindungen annimmt. +.\" ------------------------------------ +.SS database.sslmode +.BR sslmode " (Vorgabe: prefer) :" +.I String +.PP +Bestimmt, ob und mit welcher Priorität eine SSL\-Verbindung mit dem +Datenbankserver ausgehandelt wird. +Mögliche Werte sind: +.BR disabled ", " allow ", " prefer ", " require ", " verify\-ca " und " +.BR verify\-full . +Die Modi +.BR verify\-ca " und " verify\-full +stehen seit PostgreSQL 8.4 zur Verfügung. +.PP +Diese Option wird ignoriert, wenn das +.I database.module +.B pyPgSQL +verwendet wird. +.\" ------------------------------------ +.SS database.user +.BR user " (Vorgabe: " None ") :" +.I String +.PP +Der Name des Datenbank\-Benutzers. +.\" ----------------------------------------------------------------------- +.SH SEKTION DOMAIN +In der +.BR domain \-Sektion +werden Domain\-spezifische Einstellungen hinterlegt. +.PP +Das Quota\-Limit (quota_bytes und quota_messages), Service\-Einstellungen +(imap, pop3, sieve und smtp) und der Transport werden angewendet, wenn +eine Domain angelegt wird. +Um die Einstellungen einer vorhandenen Domain zu ändern, verwenden Sie +einen der folgenden +.BR vmm (1) +Unterbefehle: +.PP .TP -\fBdelete\fP (\fIBoolean\fP) -Bestimmt, ob das Maildir rekursiv gelöscht werden soll, wenn ein Konto gelöscht -wird. -.TP -\fBBeispiel\fP: -[maildir] -.br -name = Maildir -.br -folders = Drafts:Sent:Templates:Trash:INBOX.News -.br -mode = 448 -.br -diskusage = false -.br -delete = false -.\" ----- -.SH SERVICES ABSCHNITT -Im \fBservices\fP-Abschnitt werden die Standard-Beschränkungen für alle Konten -festgelegt. -.TP -\fBsmtp\fP (\fIBoolean\fP) -Legt fest, ob sich ein Anwender standardmäßig per SMTP einloggen kann. -.TP -\fBpop3\fP (\fIBoolean\fP) -Legt fest, ob sich ein Anwender standardmäßig per POP3 einloggen kann. +.B domainquota +um das Quota\-Limit einer Domain zu ändern .TP -\fBimap\fP (\fIBoolean\fP) -Legt fest, ob sich ein Anwender standardmäßig per IMAP einloggen kann. -.TP -\fBsieve\fP (\fIBoolean\fP) -Legt fest, ob sich ein Anwender standardmäßig per MANAGESIEVE einloggen kann. -.TP -\fBBeispiel\fP: -[services] -.br -smtp = true -.br -pop3 = true -.br -imap = false -.br -sieve = false -.\" ----- -.SH DOMDIR ABSCHNITT -Im \fBdomdir\fP-Abschnitt werden die Optionen der Domain-Verzeichnisse bestimmt. -.TP -\fBbase\fP (\fIString\fP) -Alle Domain-Verzeichnisse werden unterhalb dieses Basis-Verzeichnisses angelegt. -.TP -\fBmode\fP (\fIInt\fP) -Zugriffsbits des Domain-Verzeichnisses in Dezimal-Schreibweise (Basis 10). -.br -Beispiel: 'drwxrwx---' -> oktal 0770 -> dezimal 504 +.B domainservices +um einer Domain einen abweichenden Satz von nutzbaren Services zuzuweisen .TP -\fBdelete\fP (\fIBoolean\fP) -Bestimmt, ob beim Löschen einer Domain das Verzeichnis einer Domain, inklusive -aller Anwender-Verzeichnisse, rekursiv gelöscht werden soll. -.TP -\fBBeispiel\fP: -[domdir] +.B domaintransport +um einen neuen Vorgabe\-Transport für eine Domain festzulegen +.PP +Wenn ein Account angelegt wird, erbt er alle Einstellungen von der Domain, +zu der er hinzugefügt wird. +Abweichende Einstellungen für einen vorhandenen Account nehmen Sie mit +einem der Unterbefehle +.BR userquota ", " userservices " und " usertransport +vor. +.\" ------------------------------------ +.SS domain.auto_postmaster +.BR auto_postmaster " (Vorgabe: true) :" +.I Boolean +.PP +Ist der Wert dieser Option +.IR true , +wird +.BR vmm (1) +beim Anlegen einer Domain (domainadd) automatisch einen postmaster\-Account +erstellen. +.\" ------------------------------------ +.SS domain.delete_directory +.BR delete_directory " (Vorgabe: false) :" +.I Boolean +.PP +Legt fest, ob beim Löschen einer Domain (domaindelete) das Verzeichnis der +zu löschenden Domain, inklusive aller Anwender\-Verzeichnisse, rekursiv +gelöscht werden soll. +.\" ------------------------------------ +.SS domain.directory_mode +.BR directory_mode " (Vorgabe: 504) :" +.I Int +.PP +Zugriffsbits des Domain\-Verzeichnisses in Dezimal\-Schreibweise +(Basis 10). .br -base = /srv/mail -.br -mode = 504 +Beispiel: 'drwxrwx\-\-\-' \(-> oktal 0770 \(-> dezimal 504 +.\" ------------------------------------ +.SS domain.force_deletion +.BR force_deletion " (Vorgabe: false) :" +.I Boolean +.PP +Erzwingt das Löschen aller zugeordneten Konten und Aliase beim Löschen +einer Domain (domaindelete). +.\" ------------------------------------ +.SS domain.imap +.BR imap " (Vorgabe: true) :" +.I Boolean +.PP +Legt fest, ob sich neu angelegte Benutzer per IMAP anmelden können sollen. +.\" ------------------------------------ +.SS domain.pop3 +.BR pop3 " (Vorgabe: true) :" +.I Boolean +.PP +Legt fest, ob sich neu angelegte Benutzer per POP3 anmelden können sollen. +.\" ------------------------------------ +.SS domain.quota_bytes +.BR quota_bytes " (Vorgabe: 0) :" +.I String +.PP +Quota Limit in Bytes. +0 bedeutet unbegrenzt. +Dieses Limit wird beim Anlegen von Domains angewendet. +.PP +Der Wert dieser Option kann als Integer\-Wert, zum Beispiel +.B 20480 +geschrieben werden. +Es ist auch möglich dem Wert eines der folgenden +Suffixe anzuhängen: +.BR b " (Bytes), " k " (Kilobytes), " M " (Megabytes) oder " G +(Gigabytes). .br -delete = false -.\" ----- -.SH BIN ABSCHNITT -Der \fBbin\fP-Abschnitt wird verwendet, um Pfade zu Binaries, die von \fBvmm\fP -benötigt werden, anzugeben. -.TP -\fBdovecotpw\fP (\fIString\fP) -Der absolute Pfad zum dovecotpw-Binary. Dieses wird verwendet, wenn als -Passwort-Schema eines der folgenden verwendet wird: 'SMD5', 'SSHA', 'CRAM-MD5', -\'HMAC-MD5', 'LANMAN', 'NTLM' oder 'RPA'. +1024 entspricht 1024b oder 1k. +.\" ------------------------------------ +.SS domain.quota_messages +.BR quota_messages " (Vorgabe: 0) :" +.I Int +.PP +Quota Limit als Anzahl von Nachrichten. +0 bedeutet unbegrenzt. +Dieses Limit wird beim Anlegen neuer Domains angewendet. +.\" ------------------------------------ +.SS domain.sieve +.BR sieve " (Vorgabe: true) :" +.I Boolean +.PP +Legt fest, ob sich neu angelegte Benutzer per SIEVE (ManageSieve) anmelden +können sollen. +.\" ------------------------------------ +.SS domain.smtp +.BR smtp " (Vorgabe: true) :" +.I Boolean +.PP +Legt fest, ob sich neu angelegte Benutzer per SMTP (SMTP AUTH) anmelden +können sollen. +.\" ------------------------------------ +.SS domain.transport +.BR transport " (Vorgabe: dovecot:) :" +.I String +.PP +Der Standard\-Transport aller neuen Domains. +Siehe auch: +.BR transport (5). +.\" ----------------------------------------------------------------------- +.SH SEKTION MAILBOX +In der +.BR mailbox \-Sektion +werden die für die Erstellung von Mailboxen erforderlichen Optionen +festgelegt. +Die INBOX wird in jedem Fall erstellt. +.SS mailbox.folders +.BR folders " (Vorgabe: Drafts:Sent:Templates:Trash) :" +.I String +.PP +Eine durch Doppelpunkte getrennte Liste mit Namen der zu erstellenden +Mailboxen. +Sollen keine zusätzlichen Mailboxen angelegt werden, ist dieser Option ein +einzelner Doppelpunkt +.RB (' : ') +als Wert zuzuweisen. +.PP +Sollen Verzeichnisse mit Unterverzeichnissen angelegt werden, ist ein +einzelner Punkt +.RB (' . ') +als Separator zu verwenden. +.PP +Sollen Mailboxen mit internationalisierten Namen erstellt werden (zum +Beispiel: 'Wysłane' oder 'Gelöschte Objekte'), ist der Name UTF\-8 kodiert +anzugeben. +.BR vmm (1) +wird die internationalisierten Mailboxnamen in eine modifizierten Variante +des UTF\-7\-Zeichensatzes (siehe auch: RFC 3501, Sektion 5.1.3) +konvertieren. +.\" ------------------------------------ +.SS mailbox.format +.BR format " (Vorgabe: maildir) :" +.I String +.PP +Das zu verwendende Mailbox\-Format für die Mailboxen der Benutzer. +Abhängig von der verwendeten Dovecot\-Version +.RI ( misc.dovecot_version ), +unterstützt +.BR vmm (1) +bis zu drei Formate: +.TP 8 +.B maildir +Dovecot \(>= v1.0.0 .TP -\fBdu\fP (\fIString\fP) -Der absolute Pfad zu \fBdu\fR(1). Dieses Binary wird verwendet, wenn die -Festplattenbelegung eines Kontos ermittelt wird. -.TP -\fBpostconf\fP (\fIString\fP) -Der absolute Pfad zu Postfix' \fBpostconf\fR(1). -.br -Dieses Binary wird verwendet, wenn \fBvmm\fR(1) diverse Postfix-Einstellungen -prüft, zum Beispiel virtual_alias_expansion_limit. -.TP -\fBBeispiel\fP: -[bin] -.br -dovecotpw = /usr/sbin/dovecotpw -.br -du = /usr/bin/du -.br -postconf = /usr/sbin/postconf -.\" ----- -.SH MISC ABSCHNITT -Im \fBmisc\fP-Abschnitt werden verschiedene Einstellungen festgelegt. -.TP -\fBpasswdscheme\fP (\fIString\fP) -Das zu verwendende Passwort-Schema (siehe auch: dovecotpw -l) -.TP -\fBgid_mail\fP (\fIInt\fP) -Die numerische Gruppen-ID der Gruppe mail, bzw. der Gruppe aus -mail_privileged_group der Datei dovecot.conf. +.B mdbox +Dovecot \(>= v2.0.beta5 .TP -\fBforcedel\fP (\fIBoolean\fP) -Legt fest, ob beim Löschen einer Domain alle vorhanden Konten und/oder Aliase, -ohne Nachfrage, gelöscht werden sollen. -.TP -\fBtransport\fP (\fIString\fP) -Der Standard-Transport aller Domains und Konten. -.TP -\fBdovecotvers\fP (\fIInt\fP) -Die verketteten Major- und Minor-Teile der eingesetzten Dovecot-Version -(siehe: dovecot --version). -.br -Diese Option beeinflusst diverse Datenbankzugriffe. Da es zwischen Dovecot -v1.1.x und v1.2.x einige Änderungen gab. Zum Beispiel \fB11\fP, falls -\fBdovecot --version\fP den Wert \fB1.1\fP.18 ausgibt. -.TP -\fBBeispiel\fP: +.B sdbox +Dovecot \(>= v2.0.rc3 +.\" ------------------------------------ +.SS mailbox.root +.BR root " (Vorgabe: Maildir) :" +.I String +.PP +Name des Mailbox\-Wurzelverzeichnisses im Home\-Verzeichnis des jeweiligen +Benutzers. +Übliche Namen, je nach verwendetem +.IR mailbox.format , +sind +.BR Maildir ", " mdbox " or " sdbox . +.\" ------------------------------------ +.SS mailbox.subscribe +.BR subscribe " (Vorgabe: true) :" +.I Boolean +.PP +Wenn dieser Option der Wert +.B true +zugewiesen wurde, werden die, gemäß +.IR mailbox.folders , +erstellen Mailboxen in der subscriptions\-Datei des Benutzers gelistet. +Sollen die erstellen Mailboxen nicht nicht in der subscriptions\-Datei +gelistet werden, weisen Sie dieser Option den Wert +.B false +zu. +.\" ----------------------------------------------------------------------- +.SH SEKTION MISC +In der +.BR misc \-Sektion +werden Einstellungen für verschiedene Bereiche festgelegt. +.SS misc.base_directory +.BR base_directory " (Vorgabe: /srv/mail) :" +.I String +.PP +Alle Domain\-Verzeichnisse werden innerhalb dieses Basis\-Verzeichnisses +angelegt. +.\" ------------------------------------ +.SS misc.crypt_blowfish_rounds +.BR crypt_blowfish_rounds " (Vorgabe: 5) :" +.I Int +.PP +Anzahl der Verschlüsselungsdurchgänge für das +.I password_scheme +.BR BLF\-CRYPT . +.PP +Der Wert muss im Bereich von +.BR 4 " \- " 31 +liegen. +.\" ------------------------------------ +.SS misc.crypt_sha256_rounds +.BR crypt_sha256_rounds " (Vorgabe: 5000) :" +.I Int +.PP +Anzahl der Verschlüsselungdurchgänge für das +.I password_scheme +.BR SHA256\-CRYPT . +.PP +Der Wert muss im Bereich von +.BR 1000 " \- " 999999999 +liegen. +.\" ------------------------------------ +.SS misc.crypt_sha512_rounds +.BR crypt_sha512_rounds " (Vorgabe: 5000) :" +.I Int +.PP +Anzahl der Verschlüsselungdurchgänge für das +.I password_scheme +.BR SHA512\-CRYPT . +.PP +Der Wert muss im Bereich von +.BR 1000 " \- " 999999999 +liegen. +.\" ------------------------------------ +.SS misc.dovecot_version +.BR dovecot_version " (Vorgabe: " None ") :" +.I String +.PP +Die aktuell eingesetzte Dovecot\-Version. +(siehe: +.BR "dovecot \-\-version" ). + +Wenn das Kommando +.B dovecot \-\-version +zum Beispiel +.I 2.0.beta4 (8818db00d347) +ausgibt, ist dieser Option der Wert +.B 2.0.beta4 +zuzuweisen. +.\" ------------------------------------ +.SS misc.password_scheme +.BR password_scheme " (Vorgabe: CRAM\-MD5) :" +.I String +.PP +Das zu verwendende Passwort\-Schema. +Um eine Liste aller verwendbaren Passwort\-Schemen zu erhalten, führen Sie +das Kommando +.B vmm lp +aus. +.PP +Seit Dovecot \(>= v1.1.alpha1 ist es möglich, dem +.I password_scheme +ein Encoding\-Suffix anzufügen. +Unterstützte Encoding\-Suffixe: +.BR .b64 ", " .base64 " und " .hex . +Beispiel: PLAIN.BASE64 +.\" ----------------------------------------------------------------------- +.SH BEISPIEL +Eine Beispiel\-Konfiguration. +Alle Optionen, die nicht in der Konfigurationsdatei gelistet sind, haben +ihren Vorgabewert. +.PP +.nf +[account] +password_length = 10 +random_password = true + +[bin] +dovecotpw = /usr/bin/doveadm + +[database] +host = dbsrv8.example.net +pass = PY_SRJ}L/0p\-oOk +port = 5433 +sslmode = require +user = vmm + +[domain] +quota_bytes = 500M +quota_messages = 10000 +transport = lmtp:unix:private/dovecot\-lmtp + +[mailbox] +folders = Drafts:Sent:Templates:Trash:Lists.Dovecot:Lists.Postfix + [misc] -.br -passwdscheme = CRAM-MD5 -.br -gid_mail = 8 -.br -forcedel = false -.br -transport = dovecot: -.br -dovecotvers = 11 -.\" ----- -.SH CONFIG ABSCHNITT -Beim \fBconfig\fP-Abschnitt handelt es sich um einen internen -Steuerungs-Abschnitt. +crypt_sha512_rounds = 10000 +dovecot_version = 2.0.beta4 +password_scheme = SHA512\-CRYPT.hex +.fi +.\" ----------------------------------------------------------------------- +.SH SIEHE AUCH +.BR postconf (1), +.BR vmm (1), +.BR transport (5) +.\" ----------------------------------------------------------------------- +.SH INTERNET RESSOURCEN .TP -\fBdone\fP (\fIBoolean\fP) -Diese Option hat den den Wert \fIfalse\fP, wenn vmm zum ersten Mal installiert -wurde. Wenn die Datei \fIvmm.cfg\fP von Hand editiert wird, weisen Sie dieser -Option abschließend den Wert \fItrue\fP zu. -.br -Wird die Konfiguration über das Kommando \fBvmm configure\fP angepasst, wird der -Wert dieser Option automatisch auf \fItrue\fP gesetzt. -.br -Sollte diese Option den Wert \fIfalse\fP zugewiesen haben, so startet \fBvmm\fP -beim nächsten Aufruf im interaktiven Konfigurations-Modus. +Wiki +http://de.vmm.localdomain.org/ +.TP +Projekt\-Seite +http://sf.net/projects/vmm/ .TP -\fBBeispiel\fP: -[config] -.br -done = true -.\" ----- -.SH DATEIEN -vmm.cfg -.SH SIEHE AUCH -vmm(1), Programm für die Kommandozeile, um E-Mail-Domains, -Konten und -Aliase -zu verwalten. -.SH AUTOR -\fBvmm\fP und die dazugehörigen Manualseiten wurden von Pascal Volk -<\fIneverseen@users.sourceforge.net\fP> geschrieben und sind unter den -Bedingungen der BSD Lizenz lizenziert. +Bugtracker +https://bitbucket.org/pvo/vmm/issues +.\" ----------------------------------------------------------------------- +.SH COPYING +vmm und die dazugehörigen Manualseiten wurden von Pascal Volk geschrieben und sind unter den Bedingungen der +BSD Lizenz lizenziert. diff -r c0e1fb1b0145 -r a4aead244f75 man/man1/vmm.1 --- a/man/man1/vmm.1 Mon Nov 07 03:22:15 2011 +0000 +++ b/man/man1/vmm.1 Thu Jun 28 19:26:50 2012 +0000 @@ -1,423 +1,977 @@ -.TH "VMM" "1" "17 Aug 2009" "Pascal Volk" +.TH "VMM" "1" "2012-04-15" "vmm 0.6" "vmm" .SH NAME vmm \- command line tool to manage email domains/accounts/aliases +.\" ----------------------------------------------------------------------- .SH SYNOPSIS .B vmm -\fIsubcommand\fP \fIobject\fP [ \fIargs\fP ] +.IR subcommand " [" "argument ..." ] +.\" ----------------------------------------------------------------------- .SH DESCRIPTION -\fBvmm\fP (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. -.SH SUBCOMMANDS -Each subcommand has both a long and a short form. Both forms are case sensitive. -.SS GENERAL SUBCOMMANDS +.B vmm +(a virtual mail manager) is the easy to use command line tool for +administrators and postmasters to manage (alias) domains, accounts, aliases +and relocated users. +It allows you to simply and quickly administer your mail server. +.br +It's designed for Dovecot and Postfix with a PostgreSQL backend. +.PP +Each +.I subcommand +has both a long and a short form. +The short form is shown enclosed in parentheses. +Both forms are case sensitive. +.PP +Most of the +.IR subcommand s +take one or more +.IR argument s. +.\" ----------------------------------------------------------------------- +.SH ARGUMENTS +.TP 12 +.I address +The complete e\-mail address +.RI ( local\-part @ fqdn ) +of an user account, alias address or relocated user. +.\" -------------------------- .TP -\fBconfigure\fP (\fBcf\fP) [ \fIsection\fP ] -Starts the interactive configuration for all configuration sections. -.br -If the optional argument \fIsection\fP is given, only the configuration options -from the given section will be displayed and will be configurable. The following -sections are available: -.RS -.PD 0 +.I destination +Is either an e\-mail +.I address +when used with +.IR "ALIAS SUBCOMMANDS" . +Or a +.I fqdn +when used with +.IR "ALIASDOMAIN SUBCOMMANDS" . +.\" -------------------------- .TP -- -.B -database +.I fqdn +The fully qualified domain name \- without the trailing dot \- of a domain +or alias domain. +.\" -------------------------- .TP -- -.B -maildir +.I messages +An integer value which specifies a quota limit in number of messages. +.B 0 +(zero) means unlimited \- no quota limit for the number of messages. +.\" -------------------------- .TP -- -.B -services +.I option +is the name of a configuration option, prefixed with the section name and a +dot. +For example: +.IB misc . transport +.br +All configuration options are described in +.BR vmm.cfg (5). +.\" -------------------------- +.TP +.I service +The name of a service, commonly used with Dovecot. +Supported services are: +.BR imap ", " pop3 ", " sieve " and " smtp . +.\" -------------------------- .TP -- -.B -domdir +.I storage +Specifies a quota limit in bytes. +One of the following prefixes can be appended to the integer value: +.BR b " (bytes), " k " (kilobytes), " M " (megabytes) or " G +(gigabytes). +.B 0 +(zero) means unlimited \- no quota limit in bytes. +.\" -------------------------- .TP -- -.B -bin -.TP -- -.B -misc -.PD -.RE -.LP +.I transport +A transport for Postfix, written as: +.IB transport : +or +.IB transport :\c +.IR nexthop . +See +.BR transport (5) +for more details. +.\" ----------------------------------------------------------------------- +.SH GENERAL SUBCOMMANDS +.SS configget (cg) +.BI "vmm configget" " option" +.PP +This subcommand is used to display the actual value of the given +configuration +.IR option . +.PP +Example: .PP .nf - Example: - - \fBvmm configure services\fP - Using configuration file: /usr/local/etc/vmm.cfg - - * Config section: “services” - Enter new value for option pop3 [True]: - Enter new value for option smtp [True]: - Enter new value for option imap [True]: - Enter new value for option sieve [True]: false +.B vmm configget misc.crypt_sha512_rounds +misc.crypt_sha512_rounds = 5000 +.fi +.\" ------------------------------------ +.SS configset (cs) +.B vmm configset +.I option value +.PP +Use this subcommand to set or update a single configuration option's value. +.I option +is the configuration option, +.I value +is the +.IR option 's +new value. +.IP Note: +This subcommand will create a new +.I vmm.cfg +without any comments. +Your current configuration file will be backed as +.IR vmm.cfg.bak . +.PP +Example: +.PP +.nf +.B vmm configget domain.transport +domain.transport = dovecot: +.B vmm configset domain.transport lmtp:unix:private/dovecot\-lmtp +.B vmm cg domain.transport +domain.transport = lmtp:unix:private/dovecot\-lmtp .fi +.\" ------------------------------------ +.SS configure (cf) +.B vmm configure +.RI [ section ] .PP +Starts the interactive configuration for all configuration sections. +.PP +In this process the currently set value of each option will be displayed in +square brackets. +If no value is configured, the default value of each option will be +displayed in square brackets. +Press the return key, to accept the displayed value. +.PP +If the optional argument +.I section +is given, only the configuration options from the given section will be +displayed and will be configurable. +The following sections are available: +.RS +.TP 10 +.B account +Account settings .TP -\fBgetuser\fP (\fBgu\fP) \fIuserid\fP -If only the userid is available, for example from process list, the subcommand -\fBgetuser\fP will show the user's address. +.B bin +Paths to external binaries +.TP +.B database +Database settings +.TP +.B domain +Domain settings +.TP +.B mailbox +Mailbox settings +.TP +.B misc +Miscellaneous settings +.RE +.PP +All configuration options are described in +.BR vmm.cfg (5). +.IP Note: +This subcommand will create a new +.I vmm.cfg +without any comments. +Your current configuration file will be backed as +.IR vmm.cfg.bak . +.PP +Example: .PP .nf - Example: +.B vmm configure mailbox +Using configuration file: /usr/local/etc/vmm.cfg - \fBvmm getuser 70004\fP - Account information - ------------------- - UID............: 70004 - GID............: 70000 - Address........: c.user@example.com +* Configuration section: `mailbox' +Enter new value for option folders [Drafts:Sent:Templates:Trash]: +Enter new value for option format [maildir]: mdbox +Enter new value for option subscribe [True]: +Enter new value for option root [Maildir]: mdbox +.fi +.\" ------------------------------------ +.SS getuser (gu) +.BI "vmm getuser" " uid" +.PP +If only the +.I uid +is available, for example from process list, the subcommand +.B getuser +will show the user's address. +.PP +Example: +.PP +.nf +.B vmm getuser 79876 +Account information +------------------- + UID............: 79876 + GID............: 70704 + Address........: a.user@example.com .fi -.\" -.TP -\fBlistdomains\fP (\fBld\fP) [ \fIpattern\fP ] -This subcommand lists all available domains. All domain names will be prefixed -either with '[+]', if the domain is a primary domain, or with '[-]', if it is -an alias domain name. The output can be limited with an optional \fIpattern\fP. +.\" ------------------------------------ +.SS help (h) +.B vmm help +.RI [ subcommand ] +.PP +Prints a list of available subcommands with a short description to stdout. +When a +.I subcommand +was given, help for that +.I subcommand +will be displayed. +After this +.B vmm +exits. +.\" ------------------------------------ +.SS listdomains (ld) +.B vmm listdomains +.RI [ pattern ] +.PP +This subcommand lists all available domains. +All domain names will be prefixed either with `[+]', if the domain is a +primary domain, or with `[-]', if it is an alias domain name. +The output can be limited with an optional +.IR pattern . +.PP +To perform a wild card search, the % character can be used at the start +and/or the end of the +.IR pattern . +.PP +Example: +.PP +.nf +.B vmm listdomains %example% +Matching domains +---------------- + [+] example.com + [\-] e.g.example.com + [\-] example.name + [+] example.net + [+] example.org +.fi +.\" ------------------------------------ +.SS listpwschemes (lp) +.B vmm listpwschemes +.PP +This subcommand lists all password schemes which could be used in the +.I vmm.cfg +as value of the +.I misc.password_scheme +option. +The output varies, depending on the used Dovecot version and the system's +libc. .br -To perform a wild card search, the % character can be used at the start and/or -the end of the \fIpattern\fP. +When your Dovecot installation isn't too old, you will see additionally a +few usable encoding suffixes. +One of them can be appended to the password scheme. +.PP +Example: .PP .nf - Example: +.B vmm listpwschemes +Usable password schemes +----------------------- + CRYPT SHA512-CRYPT LDAP-MD5 DIGEST-MD5 SHA256 SHA512 SSHA512 + SKEY SSHA NTLM RPA MD5-CRYPT HMAC-MD5 SHA1 PLAIN SHA CRAM-MD5 + SSHA256 MD5 LANMAN CLEARTEXT PLAIN-MD5 PLAIN-MD4 OTP SMD5 + SHA256-CRYPT - \fBvmm listdomains %example%\fP - Matching domains - ---------------- - [+] example.com - [-] e.g.example.com - [-] example.name - [+] example.net - [+] example.org +Usable encoding suffixes +------------------------ + .B64 .BASE64 .HEX .fi -.\" -.TP -\fBhelp\fP (\fBh\fP) -Prints all available commands to stdout. After this \fBvmm\fP exits. -.TP -\fBversion\fP (\fBv\fP) -Prints the version information from \fBvmm\fP. -.\" -.SS DOMAIN SUBCOMMANDS -.TP -\fBdomainadd\fP (\fBda\fP) \fIdomain\fP [ \fItransport\fP ] -Adds the new \fIdomain\fP into the database. -.br -If the optional argument \fItransport\fP is given, it will overwrite the -default transport from \fBvmm.cfg\fP (misc/transport). The specified transport +.\" ------------------------------------ +.SS version (v) +.B vmm version +.PP +Prints +.BR vmm 's +version and copyright information to stdout. +After this +.B vmm +exits. +.\" ----------------------------------------------------------------------- +.SH DOMAIN SUBCOMMANDS +.SS domainadd (da) +.B vmm domainadd +.IR fqdn " [" transport ] +.PP +Adds the new domain into the database and creates the domain directory. +.PP +If the optional argument +.I transport +is given, it will override the default transport +.RI ( domain.transport ") from " vmm.cfg . +The specified +.I transport will be the default transport for all new accounts in this domain. .PP -.nf - Examples: - - \fBvmm domainadd support.example.com smtp:mx1.example.com - vmm domainadd sales.example.com\fP -.fi +Configuration\-related behavior: +.RS +.TP +.I domain.auto_postmaster +When that option is set to +.BR true " (default) " vmm +will automatically create the postmaster account for the new domain and +prompt for +.BI postmaster@ fqdn\c +\(aqs password. .TP -\fBdomaininfo\fP (\fBdi\fP) \fIdomain\fP [ \fIdetails\fP ] -This subcommand shows some information about the given \fIdomain\fP. -.br -For a more detailed information about the \fIdomain\fP the optional argument -\fIdetails\fP can be specified. A possible \fIdetails\fP value may be one of -the following five keywords: +.I account.random_password +When the value of that option is also set to +.BR true ", " vmm +will automatically create the postmaster account for the new domain and +print the generated postmaster password to stdout. +.RE +.PP +Examples: +.PP +.nf +.B vmm domainadd support.example.com smtp:[mx1.example.com]:2025 +Creating account for postmaster@support.example.com +Enter new password: +Retype new password: +.B vmm cs account.random_password true +.B vmm domainadd sales.example.com +Creating account for postmaster@sales.example.com +Generated password: pLJUQ6Xg_z +.fi +.\" ------------------------------------ +.SS domaindelete (dd) +.BI "vmm domaindelete " fqdn +.RB [ force ] +.PP +This subcommand deletes the domain specified by +.IR fqdn . +.PP +If there are accounts, aliases and/or relocated users assigned to the given +domain, +.B vmm +will abort the requested operation and show an error message. +If you know, what you are doing, you can specify the optional keyword +.BR force . +.PP +If you really always know what you are doing, edit your +.I vmm.cfg +and set the option +.I domain.force_deletion +to +.BR true . +.\" ------------------------------------ +.SS domaininfo (di) +.B vmm domaininfo +.IR fqdn \ [ details ] +.PP +This subcommand shows some information about the given domain. +.PP +For a more detailed information about the domain the optional argument +.I details +can be specified. +A possible +.I details +value can be one of the following five keywords: .RS -.PD 0 -.TP +.TP 14 .B accounts -to list all existing accounts +to list the e\-mail addresses of all existing user accounts .TP .B aliasdomains -to list all assigned alias domains +to list all assigned alias domain names .TP .B aliases -to list all available aliases addresses +to list all available alias e\-mail addresses .TP .B relocated -to list all relocated users +to list the e\-mail addresses of all relocated users +.TP +.B catchall +to list all catch\-all destinations .TP .B full to list all information mentioned above -.PD .RE -.LP +.PP +Example: +.PP .nf - Example: - - \fBvmm domaininfo sales.example.com\fP - Domain information - ------------------ - Domainname.....: sales.example.com - GID............: 70002 - Transport......: dovecot: - Domaindir......: /home/mail/5/70002 - Aliasdomains...: 0 - Accounts.......: 0 - Aliases........: 0 - Relocated......: 0 - +.B vmm domaininfo sales.example.com +Domain information +------------------ + Domain Name......: sales.example.com + GID..............: 70708 + Domain Directory.: /srv/mail/c/70708 + Quota Limit/User.: Storage: 500.00 MiB; Messages: 10,000 + Active Services..: IMAP SIEVE + Transport........: lmtp:unix:private/dovecot-lmtp + Alias Domains....: 0 + Accounts.........: 1 + Aliases..........: 0 + Relocated........: 0 + Catch-All Dests..: 1 .fi -.TP -\fBdomaintransport\fP (\fBdt\fP) \fIdomain\fP \fItransport\fP [ \fIforce\fP ] -A new transport for the indicated domain can be set with this subcommand. +.\" ------------------------------------ +.SS domainquota (dq) +.B vmm domainquota +.IR "fqdn storage" " [" messages ] +.RB [ force ] +.PP +This subcommand is used to configure a new quota limit for the accounts of +the domain - not for the domain itself. +.PP +The default quota limit for accounts is defined in the +.IR vmm.cfg " (" domain.quota_bytes " and " domain.quota_messages ). +.PP +The new quota limit will affect only those accounts for which the limit +has not been overridden. If you want to restore the default to all accounts, +you may pass the keyword +.BR force . .br -If the additional keyword '\fBforce\fP' is given all account specific transport -settings will be overwritten. -.br -Otherwise this setting will affect only new created accounts. +When the argument +.I messages +was omitted the default number of messages +.B 0 +(zero) will be applied. +.PP +Example: .PP .nf - Example: - - \fBvmm domaintransport support.example.com dovecot:\fP +.B vmm domainquota example.com 1g force .fi -.TP -\fBdomaindelete\fP (\fBdd\fP) \fIdomain\fP [ \fIdelalias\fP | \fIdeluser\fP | \fIdelall\fP ] -This subcommand deletes the specified \fIdomain\fP. +.\" ------------------------------------ +.SS domainservices (ds) +.B vmm domainservices +.IR fqdn " [" "service ..." ] +.RB [ force ] +.PP +To define which services could be used by the users of the domain \(em with +the given +.I fqdn +\(em use this subcommand. +.PP +Each specified +.I service +will be enabled/usable. +All other services will be deactivated/unusable. +Possible service names are: +.BR imap ", " pop3 ", " sieve " and " smtp . .br -If there are accounts and/or aliases assigned to the given domain, \fBvmm\fP -will abort the requested operation and show an error message. If you know, what -you are doing, you can specify one of the following keywords: '\fBdelalias\fP', '\fBdeluser\fP' or '\fBdelall\fP'. -.br - -If you really always know what you are doing, edit your \fBvmm.cfg\fP and set -the option \fIforcedel\fP, in section \fImisc\fP, to true. -.\" -.SS ALIAS DOMAIN SUBCOMMANDS -.TP -\fBaliasdomainaddd\fP (\fBada\fP) \fIaliasdomain\fP \fItargetdomain\fP -This subcommand adds the new \fIaliasdomain\fP to the \fItargetdomain\fP that -should be aliased. +The new service set will affect only those accounts for which the set has not +been overridden. If you want to restore the default to all accounts, you may +pass the keyword +.BR force . +.\" ------------------------------------ +.SS domaintransport (dt) +.BI "vmm domaintransport" " fqdn transport" +.RB [ force ] +.PP +A new transport for the indicated domain can be set with this subcommand. +.PP +The new transport will affect only those accounts for which the transport has +not been overridden. If you want to restore the default to all accounts, you +may pass the keyword +.BR force . +.PP +Example: .PP .nf - Example: - - \fBvmm aliasdomainadd example.name example.com\fP +.B vmm domaintransport support.example.com dovecot: .fi -.TP -\fBaliasdomaininfo (\fBadi\fP) \fIaliasdomain\fP -This subcommand shows to which domain the \fIaliasdomain\fP is assigned to. +.\" ------------------------------------ +.SS domainnote (do) +.BI "vmm domainnote" " fqdn" +.RI [ note ] +.PP +With this subcommand, it is possible to attach a note to the specified +domain. Without an argument, an existing note is removed. +.PP +Example: .PP .nf - Example: - - \fBvmm aliasdomaininfo example.name\fP - Alias domain information - ------------------------ - The alias domain example.name belongs to: - * example.com +.B vmm do example.com Belongs to Robert .fi -.TP -\fBaliasdomainswitch\fP (\fBads\fP) \fIaliasdomain\fP \fItargetdomain\fP -If the target of the existing \fIaliasdomain\fP should be switched to another -\fItargetdomain\fP use this subcommand. +.\" ----------------------------------------------------------------------- +.SH ALIAS DOMAIN SUBCOMMANDS +An alias domain is an alias for a domain that was previously added with the +subcommand +.BR domainadd . +All accounts, aliases and relocated users from the domain will be also +available in the alias domain. +.br +In the following is to be assumed that example.net is an alias for +example.com. +.PP +Postfix will not accept erroneously e\-mails for unknown.user@example.net +and bounce them back later to the mostly faked sender. +Postfix will immediately reject all e\-mails addressed to nonexistent +users. +.br +This behavior is ensured as long as you use the recommended database +queries in your +.I $config_directory/pgsql\-*.cf +configuration files. +.\" ------------------------------------ +.SS aliasdomainadd (ada) +.BI "vmm aliasdomainadd" " fqdn destination" +.PP +This subcommand adds the new alias domain +.RI ( fqdn ) +to the +.I destination +domain that should be aliased. +.PP +Example: .PP .nf - Example: - - \fBvmm aliasdomainswitch example.name example.org\fP +.B vmm aliasdomainadd example.net example.com .fi -.TP -\fBaliasdomaindelete\fP (\fBadd\fP) \fIaliasdomain\fP -Use this subcommand if the alias domain \fIaliasdomain\fP should be removed. +.\" ------------------------------------ +.SS aliasdomaindelete (add) +.BI "vmm aliasdomaindelete" " fqdn" +.PP +Use this subcommand if the alias domain +.I fqdn +should be removed. +.PP +Example: +.PP +.nf +.B vmm aliasdomaindelete e.g.example.com +.fi +.\" ------------------------------------ +.SS aliasdomaininfo (adi) +.BI "vmm aliasdomaininfo" " fqdn" +.PP +This subcommand shows to which domain the alias domain +.I fqdn +is assigned to. +.PP +Example: .PP .nf - Example: - - \fBvmm aliasdomaindelete e.g.example.com\fP +.B vmm adi example.net +Alias domain information +------------------------ + The alias domain example.net belongs to: + * example.com +.fi +.\" ------------------------------------ +.SS aliasdomainswitch (ads) +.BI "vmm aliasdomainswitch" " fqdn destination" +.PP +If the destination of the existing alias domain +.I fqdn +should be switched to another +.I destination +use this subcommand. +.nf +.PP +Example: +.PP +.B vmm aliasdomainswitch example.name example.org .fi -.\" -.SS ACCOUNT SUBCOMMANDS -.TP -\fBuseradd\fP (\fBua\fP) \fIaddress\fP [ \fIpassword\fP ] -Use this subcommand to create a new email account for the given \fIaddress\fP. -.br -If the \fIpassword\fP is not provided, \fBvmm\fP will prompt for it -interactively. +.\" ----------------------------------------------------------------------- +.SH ACCOUNT SUBCOMMANDS +.SS useradd (ua) +.B vmm useradd +.IR address " [" password ] +.PP +Use this subcommand to create a new e\-mail account for the given +.IR address . +.PP +If the +.I password +is not provided, +.B vmm +will prompt for it interactively. +When no +.I password +is provided and +.I account.random_password +is set to +.BR true ", " vmm +will generate a random password and print it to stdout after the account +has been created. +.PP +Examples: .PP .nf - Examples: - - \fBvmm ua d.user@example.com 'A 5ecR3t P4s5\\/\\/0rd'\fP - \fBvmm ua e.user@example.com\fP - Enter new password: - Retype new password: +.B vmm ua d.user@example.com \(dqA 5ecR3t P4s5\(rs/\(rs/0rd\(dq +.B vmm useradd e.user@example.com +Enter new password: +Retype new password: .fi -.TP -\fBuserinfo\fP (\fBui\fP) \fIaddress\fP [ \fIdetails\fP ] +.\" ------------------------------------ +.SS userdelete (ud) +.BI "vmm userdelete" " address" +.RB [ force ] +.PP +Use this subcommand to delete the account with the given +.IR address . +.PP +If there are one or more aliases with an identical destination address, +.B vmm +will abort the requested operation and show an error message. +To prevent this, specify the optional keyword +.BR force . +.\" ------------------------------------ +.SS userinfo (ui) +.B "vmm userinfo" +.IR address " [" details ] +.PP This subcommand displays some information about the account specified by -\fIaddress\fP. -.br -If the optional argument \fIdetails\fP is given some more information will be -displayed. -.br -Possible values for \fIdetails\fP are: +.IR address . +.PP +If the optional argument +.I details +is given some more information will be displayed. +Possible values for +.I details +are: .RS -.PD 0 -.TP +.TP 8 .B aliases -to list all alias addresses with the destination \fIaddress\fP +to list all alias addresses with the destination +.I address .TP .B du -to display the disk usage of users maildir +to display the disk usage of the user's mail directory. +In order to summarize the disk usage each time the this subcommand is +executed automatically, set +.I account.disk_usage +in your +.I vmm.cfg +to +.BR true . .TP .B full to list all information mentioned above -.PD .RE -.LP -.TP -\fBusername\fP (\fBun\fP) \fIaddress\fP \fI'Users Name'\fP -The user's real name can be set/updated with this subcommand. +.PP +Example: .PP .nf - Example: - - \fBvmm un d.user@example.com 'John Doe'\fP +.B vmm ui d.user@example.com +Account information +------------------- + Address..........: d.user@example.com + Name.............: None + UID..............: 79881 + GID..............: 70704 + Home.............: /srv/mail/2/70704/79881 + Mail_Location....: mdbox:~/mdbox + Quota Storage....: [ 0.00%] 0/500.00 MiB + Quota Messages...: [ 0.00%] 0/10,000 + Transport........: lmtp:unix:private/dovecot-lmtp + SMTP.............: disabled + POP3.............: disabled + IMAP.............: enabled + SIEVE............: enabled .fi -.TP -\fBuserpassword\fP (\fBup\fP) \fIaddress\fP [ \fIpassword\fP ] -The \fIpassword\fP from an account can be updated with this subcommand. -.br -If the \fIpassword\fP is not provided, \fBvmm\fP will prompt for it -interactively. +.\" ------------------------------------ +.SS username (un) +.BI "vmm username" " address" +.RI [ name ] +.PP +The user's real +.I name +can be set/updated with this subcommand. +.PP +If no +.I name +is given, the value stored for the account is erased. +.PP +Example: .PP .nf - Example: - - \fBvmm up d.user@example.com 'A |\\/|0r3 5ecur3 P4s5\\/\\/0rd?'\fP +.B vmm un d.user@example.com \(dqJohn Doe\(dq .fi -.TP -\fBusertransport\fP (\fBut\fP) \fIaddress\fP \fItransport\fP -A different transport for an account can be specified with this subcommand. +.\" ------------------------------------ +.SS userpassword (up) +.BI "vmm userpassword" " address" +.RI [ password ] +.PP +The password of an account can be updated with this subcommand. +.PP +If no +.I password +was provided, +.B vmm +will prompt for it interactively. +.PP +Example: +.PP +.nf +.B vmm up d.user@example.com \(dqA |\(rs/|0r3 5ecur3 P4s5\(rs/\(rs/0rd?\(dq +.fi +.\" ------------------------------------ +.SS usernote (uo) +.BI "vmm usernote" " address" +.RI [ note ] +.PP +With this subcommand, it is possible to attach a note to the specified +account. Without an argument, an existing note is removed. +.PP +Example: .PP .nf - Example: - - \fBvmm ut d.user@example.com smtp:pc105.it.example.com\fP +.B vmm uo d.user@example.com Only needed until end of May 2012 .fi -.TP -\fBuserdisable\fP (\fBu0\fP) \fIaddress\fP [ \fIservice\fP ] -If a user shouldn't have access to one or all services you can restrict the -access with this subcommand. -.br -If neither a \fIservice\fP nor the keyword '\fIall\fP' is given all services -(\fIsmtp\fP, \fIpop3\fP, \fIimap\fP, and \fIsieve\fP) will be disabled for the -account with the specified \fIaddress\fP. Otherwise only the specified -\fIservice\fP will be restricted. +.\" ------------------------------------ +.SS userquota (uq) +.BI "vmm userquota" " address storage" +.RI [ messages ] +.PP +This subcommand is used to set a new quota limit for the given account. +.PP +When the argument +.I messages +was omitted the default number of messages +.B 0 +(zero) will be applied. +.PP +Instead of +.I transport +pass 'domain' to remove the account\-specific override, causing the +domain's value to be in effect. +.PP +Example: .PP .nf - Examples: - - \fBvmm u0 b.user@example.com imap\fP - \fBvmm userdisable c.user@example.com\fP +.B vmm userquota d.user@example.com 750m .fi -.TP -\fBuserenable\fP (\fBu1\fP) \fIaddress\fP [ \fIservice\fP ] -To allow access to one or all restricted services use this subcommand. +.\" ------------------------------------ +.SS userservices (us) +.B vmm userservices +.IR address " [" "service ..." ] +.PP +To grant a user access to the specified services, use this command. +.PP +All omitted services will be deactivated/unusable for the user with the +given +.IR address . +.PP +Instead of +.I transport +pass 'domain' to remove the account\-specific override, causing the +domain's value to be in effect. +.PP +Example: +.PP\ +.nf +.B vmm userservices d.user@example.com SMTP IMAP +.\" ------------------------------------ +.SS usertransport (ut) +.BI "vmm usertransport" " address transport" +.PP +A different +.I transport +for an account can be specified with this subcommand. +.PP +Instead of +.I transport +pass 'domain' to remove the account\-specific override, causing the +domain's value to be in effect. +.PP +Example: .br -If neither a \fIservice\fP nor the keyword '\fIall\fP' is given all services -(\fIsmtp\fP, \fIpop3\fP, \fIimap\fP, and \fIsieve\fP) will be enabled for the -account with the specified \fIaddress\fP. Otherwise only the specified -\fIservice\fP will be enabled. +Assumed you want to use Dovecot's +.BR dsync (1) +to convert a user's mailbox from Maildir format to mdbox format, you +can tell Postfix to retry later. +.PP +.nf +.B vmm ut d.user@example.com \(dqretry:4.0.0 Mailbox being migrated\(dq +# convert the mailbox ... then set the transport to Dovecot's lmtp +.B vmm ut d.user@example.com lmtp:unix:private/dovecot\-lmtp +.fi +.\" ----------------------------------------------------------------------- +.SH ALIAS SUBCOMMANDS +.SS aliasadd (aa) +.BI "vmm aliasadd" " address destination ..." .PP -.TP -\fBuserdelete\fP (\fBud\fP) \fIaddress\fP [ \fIdelalias\fP ] -Use this subcommand to delete the account with the given \fIaddress\fP. -.br -If there are one or more aliases with an identical destination address, -\fBvmm\fP will abort the requested operation and show an error message. To -prevent this, specify the optional keyword '\fIdelalias\fP'. -.\" -.SS ALIAS SUBCOMMANDS -.TP -\fBaliasadd\fP (\fBaa\fP) \fIalias\fP \fItarget\fP -This subcommand is used to create a new alias. +This subcommand is used to create a new alias +.I address +with one or more +.I destination +addresses. +.PP +Within the destination address, the placeholders +.IR %n , +.IR %d , +and +.IR %= +will be replaced by the local part, the domain, or the email address with '@' +replaced by '=' respectively. In combination with alias domains, this enables +domain\-specific destinations. +.PP +Examples: .PP .nf - Examples: - - \fBvmm aliasadd john.doe@example.com d.user@example.com\fP - \fBvmm aa support@example.com d.user@example.com\fP - \fBvmm aa support@example.com e.user@example.com\fP +.B vmm aliasadd john.doe@example.com d.user@example.com +.B vmm aa support@example.com d.user@example.com e.user@example.com +.B vmm aa postmaster@example.com postmaster+%d@example.org .fi -.TP -\fBaliasinfo\fP (\fBai\fP) \fIalias\fP -Information about an alias can be displayed with this subcommand. +.\" ------------------------------------ +.SS aliasdelete (ad) +.BI "vmm aliasdelete" " address" +.RI [ destination ] +.PP +Use this subcommand to delete the alias with the given +.IR address . +.PP +If the optional +.I destination +address is given, only this +destination will be removed from the alias. +.PP +Example: +.PP +.nf +.B vmm ad support@example.com d.user@example.com +.fi +.\" ------------------------------------ +.SS aliasinfo (ai) +.BI "vmm aliasinfo" " address" +.PP +Information about the alias with the given +.I address +can be displayed with this subcommand. +.PP +Example: .PP .nf - Example: - - \fBvmm aliasinfo support@example.com\fP - Alias information - ----------------- - Mail for support@example.com will be redirected to: - * d.user@example.com - * e.user@example.com +.B vmm aliasinfo support@example.com +Alias information +----------------- + Mail for support@example.com will be redirected to: + * e.user@example.com +.fi +.\" ----------------------------------------------------------------------- +.SH RELOCATED SUBCOMMANDS +.SS relocatedadd (ra) +.BI "vmm relocatedadd" " address newaddress" +.PP +A new relocated user can be created with this subcommand. +.PP +.I address +is the user's ex\-email address, for example b.user@example.com, and +.I newaddress +points to the new email address where the user can be reached. +.PP +Example: +.PP +.nf +.B vmm relocatedadd b.user@example.com b\-user@company.tld .fi -.TP -\fBaliasdelete\fP (\fBad\fP) \fIalias\fP [ \fItarget\fP ] -Use this subcommand to delete the \fIalias\fP. -.br -If the optional destination address \fItarget\fP is given, only this -destination will be removed from the \fIalias\fP. +.\" ------------------------------------ +.SS relocatedinfo (ri) +.BI "vmm relocatedinfo " address +.PP +This subcommand shows the new address of the relocated user with the given +.IR address . +.PP +Example: +.PP +.nf +.B vmm relocatedinfo b.user@example.com +Relocated information +--------------------- + User `b.user@example.com' has moved to `b\-user@company.tld' +.fi +.\" ------------------------------------ +.SS relocateddelete (rd) +.BI "vmm relocateddelete " address +.PP +Use this subcommand in order to delete the relocated user with the given +.IR address . +.PP +Example: .PP .nf - Example: - - \fBvmm ad support@example.com d.user@example.com\fP +.B vmm relocateddelete b.user@example.com .fi -.\" -.SS RELOCATED SUBCOMMANDS -.TP -\fBrelocatedadd\fP (\fBra\fP) \fIold_address\fP \fInew_address\fP -A new relocated user can be created with this subcommand. -.br -\fIold_address\fP is the users ex-email address, for example b.user@example.com, -and \fInew_address\fP points to the new email address where the user can be -reached. +.\" ----------------------------------------------------------------------- +.SH CATCH\-ALL SUBCOMMANDS +.SS catchalladd (caa) +.BI "vmm catchalladd" " fqdn destination ..." +.PP +This subcommand allows to specify destination addresses for a domain, which +shall receive mail addressed to unknown local parts within that domain. +Those catch\-all aliases hence \(dqcatch all\(dq mail to any address in the +domain (unless a more specific alias, mailbox or relocated entry exists). +.PP +WARNING: Catch\-all addresses can cause mail server flooding because +spammers like to deliver mail to all possible combinations of names, e.g. +to all addresses between abba@example.org and zztop@example.org. +.PP +Example: .PP .nf - Example: - - \fBvmm relocatedadd b.user@example.com b-user@company.tld\fP +.B vmm catchalladd example.com user@example.org .fi -.TP -\fBrelocatedinfo\fP (\fBri\fP) \fIold_address\fP -This subcommand shows the new address of the relocated user with the -\fIold_address\fP. +.\" ------------------------------------ +.SS catchallinfo (cai) +.BI "vmm catchallinfo " fqdn +.PP +This subcommand displays information about catch\-all aliases defined for +a domain. +.PP +Example: .PP .nf - Example: - - \fBvmm relocatedinfo b.user@example.com\fP - Relocated information - --------------------- - User “b.user@example.com” has moved to “b-user@company.tld” +.B vmm catchallinfo example.com +Catch-all information +--------------------- + Mail to unknown localparts in domain example.com will be sent to: + * user@example.org .fi -.TP -\fBrelocateddelete\fP (\fBrd\fP) \fIold_address\fP -Use this subcommand in order to delete the relocated user with the -\fIold_address\fP. +.\" ------------------------------------ +.SS catchalldelete (cad) +.BI "vmm catchalldelete " fqdn +.RI [ destination ] +.PP +With this subcommand, catch\-all aliases defined for a domain can be +removed, either all of them, or a single one if specified explicitly. +.PP +Example: .PP .nf - Example: - - \fBvmm relocateddelete b.user@example.com\fP +.B vmm catchalldelete example.com user@example.com .fi -.\" +.\" ----------------------------------------------------------------------- .SH FILES -/usr/local/etc/vmm.cfg +.TP +.I /root/vmm.cfg +will be used when found. +.TP +.I /usr/local/etc/vmm.cfg +will be used when the above file doesn't exist. +.TP +.I /etc/vmm.cfg +will be used when none of the both above mentioned files exists. +.\" ----------------------------------------------------------------------- .SH SEE ALSO -vmm.cfg(5), configuration file for vmm -.SH AUTHOR -\fBvmm\fP and its man pages were written by Pascal Volk -<\fIneverseen@users.sourceforge.net\fP> and are licensed under the terms of the -BSD License. +.BR dsync (1), +.BR transport (5), +.BR vmm.cfg (5) +.\" ----------------------------------------------------------------------- +.SH INTERNET RESOURCES +.TP +Wiki +http://vmm.localdomain.org/ +.TP +Project site +http://sf.net/projects/vmm/ +.TP +Bug tracker +https://bitbucket.org/pvo/vmm/issues +.\" ----------------------------------------------------------------------- +.SH COPYING +vmm and its manual pages were written by Pascal Volk and are licensed under the terms of the BSD +License. diff -r c0e1fb1b0145 -r a4aead244f75 man/man5/vmm.cfg.5 --- a/man/man5/vmm.cfg.5 Mon Nov 07 03:22:15 2011 +0000 +++ b/man/man5/vmm.cfg.5 Thu Jun 28 19:26:50 2012 +0000 @@ -1,40 +1,82 @@ -.TH vmm.cfg 5 "17 Aug 2009" "Pascal Volk" +.TH "VMM.CFG" "5" "2011-11-12" "vmm 0.6" "vmm" .SH NAME vmm.cfg \- configuration file for vmm +.\" ----------------------------------------------------------------------- .SH SYNOPSIS vmm.cfg +.\" ----------------------------------------------------------------------- .SH DESCRIPTION -\fBvmm\fR(1) reads configuration data from \fIvmm.cfg\fP. +.BR vmm (1) +reads its configuration data from +.IR vmm.cfg . +.PP +The configuration file is split into multiple sections. +A section starts with the section name, enclosed in square brackets +.RB ` [ "' and `" ] ', +followed by +.RI ` option " = " value ' +pairs. .br -The configuration file is split in multiple sections. A section starts with the -section name, enclosed in square brackets '[' and ']' (e.g. \fB[database]\fP), -followed by \'option=value' pairs (e.g. \fBhost = 127.0.0.1\fP). -.br -Whitespace around the '=' and at the end of a value is ignored. -.PP -Empty lines and lines starting with '#' or ';' will be ignored. +Whitespace around the `=' and at the end of a value is ignored. +Empty lines and lines starting with `#' or `;' will be ignored. .PP Each value uses one of the following data types: -.IP \(bu +.TP 8 .I Boolean -to indicate if something is enabled/activated (true) or disabled/deactivated -(false). +to indicate if something is enabled/activated (true) or +disabled/deactivated (false). +.br +Accepted values for +.I true +are: +.BR 1 , " yes" , " true" " and " on . .br -Accepted values for \fBtrue\fP are: \fB1\fP, \fByes\fP, \fBtrue\fP and \fBon\fP. +Accepted values for +.I false +are: +.BR 0 , " no" , " false" " and " off . +.TP +.I Int +an integer number, written without a fractional or decimal component. +.br +For example +.BR 1 , " 50" " or " 321 +are integers. +.TP +.I String +a sequence of characters and/or numbers. .br -Accepted values for \fBfalse\fP are: \fB0\fP, \fBno\fP, \fBfalse\fP and -\fBoff\fP. -.IP \(bu -.I Int -an integer number, written without a fractional or decimal component. For -example \fB1\fP, \fB50\fP or \fB321\fP are integers. -.IP \(bu -.I String -a sequence of characters and numbers. For example '\fBword\fP', '\fBhello -world\fP' or '\fB/usr/bin/strings\fP' -.SS SEARCH ORDER -By default vmm looks for \fIvmm.cfg\fP in the following directories in the -order listed: +For example +.RB ` word "', `" "hello world" "' or `" /usr/bin/strings ' +are strings. +.PP +Most options have a default value, shown in parentheses after the option's +name. +In order to use a option's default setting, comment out the line, either +with a +.BR # " or " ; +or simply remove the setting from +.IR vmm.cfg . +.PP +A minimal +.I vmm.cfg +would be: +.PP +.nf +[database] +user = me +pass = xxxxxxxx + +[misc] +dovecot_version = 1.2.16 +.fi +.\" ----------------------------------------------------------------------- +.SH SEARCH ORDER +By default +.BR vmm (1) +looks for the +.I vmm.cfg +file in the following directories in the order listed: .RS .PD 0 .TP @@ -49,208 +91,500 @@ .PD .RE .PP -The first match it finds will be used. -.\" ----- -.SH DATABASE SECTION -The \fBdatabase\fP section is used to specify some options required to -connect to the database. -.TP -\fBhost\fP (\fIString\fP) -Hostname or IP address of the database server. -.TP -\fBuser\fP (\fIString\fP) -Name of the database user. -.TP -\fBpass\fP (\fIString\fP) -Database password -.TP -\fBname\fP (\fIString\fP) -Name of the database. -.TP -\fBExample\fP: -[database] -.br -host = localhost +The first configuration file found will be used. +.\" ----------------------------------------------------------------------- +.SH SECTION ACCOUNT +The options in the section +.B account +are used to specify user account related settings. +.SS account.delete_directory +.BR delete_directory " (default: false) :" +.I Boolean +.PP +Determines the behavior of +.BR vmm (1) +when an account is deleted (userdelete). +If this option is set to +.I true +the user's home directory will be deleted recursively. +.\" ------------------------------------ +.SS account.directory_mode +.BR directory_mode " (default: 448) :" +.I Int +.PP +Access mode for a user's home directory and all directories inside. +The value has to be specified in decimal (base 10) notation. .br -user = vmm -.br -pass = T~_:L4OYyl]TU?) -.br -name = mailsys -.\" ----- -.SH MAILDIR SECTION -The \fBmaildir\fP section is used to specify some options for the Maildirs. -.TP -\fBname\fP (\fIString\fP) -Default name of the maildir folder in users home directory. -.TP -\fBfolders\fP (\fIString\fP) -A colon separated list of folder names, that should be created. -.br -If no folders should be created inside the Maildir, set the value of this option -to a single colon (':'). -.TP -\fBmode\fP (\fIInt\fP) -Access mode for the maildir in decimal (base 10) notation. For example: -\'drwx------' -> octal 0700 -> decimal 448 -.TP -\fBdiskusage\fP (\fIBoolean\fP) -Decides if the disk usage of users maildir always should be summarized and -displayed with account information. +For example: `drwx\-\-\-\-\-\-' \(-> octal 0700 \(-> decimal 448 +.\" ------------------------------------ +.SS account.disk_usage +.BR disk_usage " (default: false) :" +.I Boolean +.PP +Determines whether the disk usage of a user's mail directory always should +be summarized, using +.BR du (1), +and displayed with the account information (userinfo). +.PP +This could be slow on large Maildirs. +When you have enabled quotas, +.BR vmm 's +userinfo subcommand will also display the current quota usage of the +account. +You may also use userinfo's optional details\-argument +.BR du " or " full , +in order to display the current disk usage of an account's mail directory. +.\" ------------------------------------ +.SS account.password_length +.BR password_length " (default: 8) :" +.I Int +.PP +Determines how many characters and/or numbers should be used for randomly +generated passwords. +Any value less than 8 will be increased to 8. +.\" ------------------------------------ +.SS account.random_password +.BR random_password " (default: false) :" +.I Boolean +.PP +Determines whether +.BR vmm (1) +should generate a random password when no password was given for the +useradd subcommand. +If this option is set to +.I false +.B vmm +will prompt you to enter a password for the new account. +.PP +You can specify the password length of generated passwords with the +.I account.password_length +option. +.\" ----------------------------------------------------------------------- +.SH SECTION BIN +The +.B bin +section is used to specify some paths to some binaries required by +.BR vmm (1). +.SS bin.dovecotpw +.BR dovecotpw " (default: /usr/sbin/dovecotpw) :" +.I String +.PP +The absolute path to the +.BR dovecotpw (1) +binary. +Use the absolute path to the +.BR doveadm (1) +binary, if you are using Dovecot v2.0. +.PP +This binary is used to generate a password hash, if +.I misc.password_scheme +is set to one of `CRAM\-MD5', `HMAC\-MD5', `LANMAN', `OTP', `RPA' or +`SKEY'. +This binary will be also required if your Python installation doesn't +support the: +.IP \(bu 4 +md4 hash algorithm (hashlib + OpenSSL or PyCrypto) used for the password +schemes: `PLAIN\-MD4' and `NTLM' +.IP \(bu +sha256 hash algorithm (hashlib or PyCrypto \(>= 2.1.0alpha1) used for the +password schemes: `SHA256' and `SSHA256' +.IP \(bu +sha512 hash algorithm (hashlib) used for the password schemes: `SHA512' and +`SSHA512' +.PP +The +.BR doveadm (1) +binary is also used to create a user's INBOX and additional mailboxes +.RI ( mailbox.folders ), +when the +.I mailbox.format +is set to +.BR mdbox " or " sdbox . +.\" ------------------------------------ +.SS bin.du +.BR du " (default: /usr/bin/du) :" +.I String +.PP +The absolute path to +.BR du (1). +This binary is used to summarize the disk usage of a user's mail directory. +.\" ------------------------------------ +.SS bin.postconf +.BR postconf " (default: /usr/sbin/postconf) :" +.I String +.PP +The absolute path to Postfix' +.BR postconf (1). +This binary is required when +.BR vmm (1) +has to check for some Postfix settings, e.g. the +.IR virtual_alias_expansion_limit . +.\" ----------------------------------------------------------------------- +.SH SECTION DATABASE +The +.B database +section is used to specify some options required to connect to the +database. +.SS database.host +.BR host " (default: localhost) :" +.I String +.PP +Hostname or IP address of the database server. +.\" ------------------------------------ +.SS database.module +.BR module " (default: psycopg2) :" +.I String +.PP +The Python PostgreSQL database adapter module to be used. +Supported modules are +.BR psycopg2 " and " pyPgSQL . +.\" ------------------------------------ +.SS database.name +.BR name " (default: mailsys) :" +.I String +.PP +Name of the database. +.\" ------------------------------------ +.SS database.pass +.BR pass " (default: " None ") :" +.I String +.PP +Database password. +.\" ------------------------------------ +.SS database.port +.BR port " (default: 5432) :" +.I Int +.PP +The TCP port, on which the database server is listening for connections. +.\" ------------------------------------ +.SS database.sslmode +.BR sslmode " (default: prefer) :" +.I String +.PP +Determines whether and with what priority an SSL connection will be +negotiated with the database server. +Possible values are: +.BR disabled ", " allow ", " prefer ", " require ", " verify\-ca " and " +.BR verify\-full . +The modes +.BR verify\-ca " and " verify\-full +are available since PostgreSQL 8.4 +.PP +This setting will be ignored when the +.I database.module +is set to +.BR pyPgSQL . +.\" ------------------------------------ +.SS database.user +.BR user " (default: " None ") :" +.I String +.PP +Name of the database user. +.\" ----------------------------------------------------------------------- +.SH SECTION DOMAIN +The +.B domain +section specifies some domain related settings. +.PP +The quota limit (quota_bytes and quota_messages), service settings (imap, +pop3, sieve and smtp) and the transport setting will be applied when a +domain is created. +In order to modify those settings for an existing domain, use one of +the following +.BR vmm (1) +subcommands: +.PP .TP -\fBdelete\fP (\fIBoolean\fP) -Decides if the maildir should be deleted recursive when the account is deleted. -.TP -\fBExample\fP: -[maildir] -.br -name = Maildir -.br -folders = Drafts:Sent:Templates:Trash:INBOX.News -.br -mode = 448 -.br -diskusage = false -.br -delete = false -.\" ----- -.SH SERVICES SECTION -The \fBservices\fP section is used to specify the default restrictions for -all accounts. -.TP -\fBsmtp\fP (\fIBoolean\fP) -Decides if users can login via smtp by default. -.TP -\fBpop3\fP (\fIBoolean\fP) -Decides if users can login via pop3 by default. +.B domainquota +in order to update a domain's quota limit .TP -\fBimap\fP (\fIBoolean\fP) -Decides if users can login via imap by default. -.TP -\fBsieve\fP (\fIBoolean\fP) -Decides if users can login via managesieve by default. -.TP -\fBExample\fP: -[services] -.br -smtp = true -.br -pop3 = true -.br -imap = false -.br -sieve = false -.\" ----- -.SH DOMDIR SECTION -The \fBdomdir\fP section is used to specify options for the directories of the -domains. -.TP -\fBbase\fP (\fIString\fP) -All domain directories will be created inside this directory. -.TP -\fBmode\fP (\fIInt\fP) -Access mode for the domain directory in decimal (base 10) notation. For -example: 'drwxrwx---' -> octal 0770 -> decimal 504 +.B domainservices +in order to assign a different service set to a domain .TP -\fBdelete\fP (\fIBoolean\fP) -Decides if the domain directory and all user directories inside should be -deleted when a domain is deleted. -.TP -\fBExample\fP: -[domdir] +.B domaintransport +in order to set a new default domain transport +.PP +When an account is created, it inherits all the settings of the domain +to which it is added. +Different settings for an existing account can be set using the subcommands +.BR userquota ", " userservices " and " usertransport . +.\" ------------------------------------ +.SS domain.auto_postmaster +.BR auto_postmaster " (default: true) :" +.I Boolean +.PP +Determines if +.BR vmm (1) +should create also a postmaster account when a new domain is created +(domainadd). +.\" ------------------------------------ +.SS domain.delete_directory +.BR delete_directory " (default: false) :" +.I Boolean +.PP +Specifies whether the domain directory and all user directories inside +should be deleted when a domain is deleted (domaindelete). +.\" ------------------------------------ +.SS domain.directory_mode +.BR directory_mode " (default: 504) :" +.I Int +.PP +Access mode for the domain directory in decimal (base 10) notation. .br -base = /srv/mail -.br -mode = 504 +For example: `drwxrwx\-\-\-' \(-> octal 0770 \(-> decimal 504 +.\" ------------------------------------ +.SS domain.force_deletion +.BR force_deletion " (default: false) :" +.I Boolean +.PP +Force the deletion of accounts and aliases when a domain is deleted +(domaindelete). +.\" ------------------------------------ +.SS domain.imap +.BR imap " (default: true) :" +.I Boolean +.PP +Determines whether newly created users can log in via IMAP. +.\" ------------------------------------ +.SS domain.pop3 +.BR pop3 " (default: true) :" +.I Boolean +.PP +Determines whether newly created users can log in via POP3. +.\" ------------------------------------ +.SS domain.quota_bytes +.BR quota_bytes " (default: 0) :" +.I String +.PP +Quota limit in bytes. +0 means unlimited. +This limit will be applied to all newly created domains. +.PP +The option's value can be written as an integer value, e.g.: +.BR 20480 . +It's also possible to append one of the following prefixes to the limit: +.BR b " (bytes), " k " (kilobytes), " M " (megabytes) or " G +(gigabytes). .br -delete = false -.\" ----- -.SH BIN SECTION -The \fBbin\fP section is used to specify some paths to some binaries required -by \fBvmm\fP. -.TP -\fBdovecotpw\fP (\fIString\fP) -The absolute path to the dovecotpw binary. This binary is used to generate a -password hash, if the \fIpasswdscheme\fP is one of 'SMD5', 'SSHA', 'CRAM-MD5', -\'HMAC-MD5', 'LANMAN', 'NTLM' or 'RPA'. +1024 is the same as 1024b or 1k. +.\" ------------------------------------ +.SS domain.quota_messages +.BR quota_messages " (default: 0) :" +.I Int +.PP +Quota limit in number of messages. +0 means unlimited. +This limit will be applied to all newly created domains. +.\" ------------------------------------ +.SS domain.sieve +.BR sieve " (default: true) :" +.I Boolean +.PP +Determines whether newly created users can log in via SIEVE (ManageSieve). +.\" ------------------------------------ +.SS domain.smtp +.BR smtp " (default: true) :" +.I Boolean +.PP +Determines whether newly created users can log in via SMTP (SMTP AUTH). +.\" ------------------------------------ +.SS domain.transport +.BR transport " (default: dovecot:) :" +.I String +.PP +Default transport for domains and accounts. +For details see +.BR transport (5). +.\" ----------------------------------------------------------------------- +.SH SECTION MAILBOX +The +.B mailbox +section is used to specify some options for new created mailboxes in the +users home directories. +The INBOX will be created always. +.SS mailbox.folders +.BR folders " (default: Drafts:Sent:Templates:Trash) :" +.I String +.PP +A colon separated list of mailboxes that should be created. +If no additionally mailboxes should be created, set the value of this +option to a single colon +.RB (` : '). +.PP +If you want to create folders containing one or more subfolders, separate +them with a single dot +.RB (` . '). +.PP +If you want to use internationalized mailbox names (e.g. `Wysłane' or +`Gelöschte Objekte'), write their names UTF\-8 encoded. +.BR vmm (1) +will convert internationalized mailbox names to a modified version of the +UTF\-7 encoding (see also: RFC 3501, section 5.1.3). +.\" ------------------------------------ +.SS mailbox.format +.BR format " (default: maildir) :" +.I String +.PP +The mailbox format to be used for a user's mailbox. +Depending on the used Dovecot version +.RI ( misc.dovecot_version ) +.BR vmm (1) +supports up to three formats: +.TP 8 +.B maildir +Dovecot \(>= v1.0.0 .TP -\fBdu\fP (\fIString\fP) -The absolute path to \fBdu\fR(1). This binary is used to summarize the disk -usage of a maildir. -.TP -\fBpostconf\fP (\fIString\fP) -The absolute path to Postfix' \fBpostconf\fR(1). -.br -This binary is required if \fBvmm\fR(1) has to check for some Postfix settings, -e.g. virtual_alias_expansion_limit. -.TP -\fBExample\fP: -[bin] -.br -dovecotpw = /usr/sbin/dovecotpw -.br -du = /usr/bin/du -.br -postconf = /usr/sbin/postconf -.\" ----- -.SH MISC SECTION -The \fBmisc\fP section is used to define miscellaneous settings. -.TP -\fBpasswdscheme\fP (\fIString\fP) -Password scheme to use (see also: dovecotpw -l) -.TP -\fBgid_mail\fP (\fIInt\fP) -Numeric group ID of group mail (mail_privileged_group from dovecot.conf) +.B mdbox +Dovecot \(>= v2.0.beta5 .TP -\fBforcedel\fP (\fIBoolean\fP) -Force deletion of accounts and aliases when a domain is deleted. -.TP -\fBtransport\fP (\fIString\fP) -Default transport for domains and accounts. -.TP -\fBdovecotvers\fP (\fIInt\fP) -The concatenated major and minor version number of the currently used Dovecot -version. (see: dovecot --version). -.br -This option affects various database operations. There are some differences -between Dovecot v1.1.x and v1.2.x. For example, when the command \fBdovecot ---version\fP shows \fB1.1\fP.18, set the value of this option to \fB11\fP. -.TP -\fBExample\fP: -[misc] -.br -passwdscheme = CRAM-MD5 -.br -gid_mail = 8 -.br -forcedel = false -.br -transport = dovecot: +.B sdbox +Dovecot \(>= v2.0.rc3 +.\" ------------------------------------ +.SS mailbox.root +.BR root " (default: Maildir) :" +.I String +.PP +Name of the mailbox root directory in a user's home directory. +Commonly used names, depending on the used +.IR mailbox.format , +are +.BR Maildir ", " mdbox " or " sdbox . +.\" ------------------------------------ +.SS mailbox.subscribe +.BR subscribe " (default: true) :" +.I Boolean +.PP +When this option is set to +.BR true , +the mailboxes from the +.I mailbox.folders +option will be listed in the user's subscriptions file. +If you don't want to subscribe the created mailboxes, set this option to +.BR false . +.\" ----------------------------------------------------------------------- +.SH SECTION MISC +The +.I misc +section is used to define miscellaneous settings. +.SS misc.base_directory +.BR base_directory " (default: /srv/mail) :" +.I String +.PP +All domain directories will be created inside this directory. +.\" ------------------------------------ +.SS misc.crypt_blowfish_rounds +.BR crypt_blowfish_rounds " (default: 5) :" +.I Int +.PP +Number of encryption rounds for the +.I password_scheme +.BR BLF\-CRYPT . +.PP +The value must be in range +.BR 4 " \- " 31 . +.\" ------------------------------------ +.SS misc.crypt_sha256_rounds +.BR crypt_sha256_rounds " (default: 5000) :" +.I Int +.PP +Number of encryption rounds for the +.I password_scheme +.BR SHA256\-CRYPT . +.PP +The value must be in range +.BR 1000 " \- " 999999999 . +.\" ------------------------------------ +.SS misc.crypt_sha512_rounds +.BR crypt_sha512_rounds " (default: 5000) :" +.I Int +.PP +Number of encryption rounds for the +.I password_scheme +.BR SHA512\-CRYPT . +.PP +The value must be in range +.BR 1000 " \- " 999999999 . +.\" ------------------------------------ +.SS misc.dovecot_version +.BR dovecot_version " (default: " None ") :" +.I String +.PP +The version number of the currently used Dovecot version. +(see: +.BR "dovecot \-\-version" ) .br -dovecotvers = 11 -.\" ----- -.SH CONFIG SECTION -The \fBconfig\fP section is a internal used control section. -.TP -\fBdone\fP (\fIBoolean\fP) -This option is set to \fIfalse\fP when \fBvmm\fP is installed for the first -time. When you edit \fIvmm.cfg\fP, set this option to \fItrue\fP. This option is -also set to \fItrue\fP when you configure vmm with the command \fBvmm -configure\fP. -.br -If this option is set to \fIfalse\fP, \fBvmm\fP will start in the interactive -configurations mode. +When, for example, the command +.B dovecot \-\-version +prints +.IR "2.0.beta4 (8818db00d347)" , +set the value of this option to +.BR 2.0.beta4 . +.\" ------------------------------------ +.SS misc.password_scheme +.BR password_scheme " (default: CRAM\-MD5) :" +.I String +.PP +Password scheme to use. +To get a list of all usable password schemes execute the command +.BR "vmm lp" . +.PP +With Dovecot \(>= v1.1.alpha1 it is also possible to append an encoding +suffix to the password_scheme. +Supported encoding suffixes are: +.BR .b64 ", " .base64 " and " .hex . +For example: PLAIN.BASE64 +.\" ----------------------------------------------------------------------- +.SH EXAMPLE +An example configuration. +All options that are not listed in the configuration file will have their +default values. +.PP +.nf +[account] +password_length = 10 +random_password = true + +[bin] +dovecotpw = /usr/bin/doveadm + +[database] +host = dbsrv8.example.net +pass = PY_SRJ}L/0p\-oOk +port = 5433 +sslmode = require +user = vmm + +[domain] +quota_bytes = 500M +quota_messages = 10000 +transport = lmtp:unix:private/dovecot\-lmtp + +[mailbox] +folders = Drafts:Sent:Templates:Trash:Lists.Dovecot:Lists.Postfix + +[misc] +crypt_sha512_rounds = 10000 +dovecot_version = 2.0.beta4 +password_scheme = SHA512\-CRYPT.hex +.fi +.\" ----------------------------------------------------------------------- +.SH SEE ALSO +.BR postconf (1), +.BR vmm (1), +.BR transport (5) +.\" ----------------------------------------------------------------------- +.SH INTERNET RESOURCES .TP -\fBExample\fP: -[config] -.br -done = true -.\" ----- -.SH FILES -vmm.cfg -.SH SEE ALSO -vmm(1), command line tool to manage email domains/accounts/aliases -.SH AUTHOR -\fBvmm\fP and its man pages were written by Pascal Volk -<\fIneverseen@users.sourceforge.net\fP> and are licensed under the terms of the -BSD License. +Wiki +http://vmm.localdomain.org/ +.TP +Project site +http://sf.net/projects/vmm/ +.TP +Bug tracker +https://bitbucket.org/pvo/vmm/issues +.\" ----------------------------------------------------------------------- +.SH COPYING +vmm and its manual pages were written by Pascal Volk and are licensed under the terms of the BSD +License. diff -r c0e1fb1b0145 -r a4aead244f75 pgsql/create_optional_types_and_functions-dovecot-1.2.x.pgsql --- a/pgsql/create_optional_types_and_functions-dovecot-1.2.x.pgsql Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,374 +0,0 @@ --- --- Information: --- This file contains some data types and functions these should speed up some --- operations. Read the comment on each data type/functions for more details. --- --- - --- --- --- Data type for function postfix_smtpd_sender_login_map(varchar, varchar) --- --- -CREATE TYPE sender_login AS ( - sender varchar(320), - login text -); - --- --- --- Parameters (from _sender_ address (MAIL FROM) [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: SASL _login_ names that own _sender_ addresses (MAIL FROM): --- set of sender_login records. --- --- Required access privileges for your postfix database user: --- GRANT SELECT ON domain_name, users, alias TO postfix; --- --- For more details see postconf(5) section smtpd_sender_login_maps --- --- -CREATE OR REPLACE FUNCTION postfix_smtpd_sender_login_map( - IN localpart varchar, IN the_domain varchar) RETURNS SETOF sender_login -AS $$ - DECLARE - rec sender_login; - did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); - sender varchar(320) := localpart || '@' || the_domain; - BEGIN - -- Get all addresses for 'localpart' in the primary and aliased domains - FOR rec IN - SELECT sender, local_part || '@' || domainname - FROM domain_name, users - WHERE domain_name.gid = did - AND users.gid = did - AND users.local_part = localpart - LOOP - RETURN NEXT rec; - END LOOP; - IF NOT FOUND THEN - -- Loop over the alias addresses for localpart@the_domain - FOR rec IN - SELECT DISTINCT sender, destination - FROM alias - WHERE alias.gid = did - AND alias.address = localpart - LOOP - RETURN NEXT rec; - END LOOP; - END IF; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Data type for function postfix_virtual_mailbox(varchar, varchar) --- --- -CREATE TYPE address_maildir AS ( - address varchar(320), - maildir text -); - --- --- --- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: address_maildir records --- --- Required access privileges for your postfix database user: --- GRANT SELECT ON domain_data,domain_name,maillocation,users TO postfix; --- --- For more details see postconf(5) section virtual_mailbox_maps --- --- -CREATE OR REPLACE FUNCTION postfix_virtual_mailbox_map( - IN localpart varchar, IN the_domain varchar) RETURNS SETOF address_maildir -AS $$ - DECLARE - rec address_maildir; - did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); - address varchar(320) := localpart || '@' || the_domain; - BEGIN - FOR rec IN - SELECT address, domaindir||'/'||users.uid||'/'||maillocation||'/' - FROM domain_data, users, maillocation - WHERE domain_data.gid = did - AND users.gid = did - AND users.local_part = localpart - AND maillocation.mid = users.mid - LOOP - RETURN NEXT rec; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Data type for functions: postfix_relocated_map(varchar, varchar) --- postfix_virtual_alias_map(varchar, varchar) --- --- --- -CREATE TYPE recipient_destination AS ( - recipient varchar(320), - destination text -); - --- --- --- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: recipient_destination records --- --- Required access privileges for your postfix database user: --- GRANT SELECT ON alias, domain_name TO postfix; --- --- For more details see postconf(5) section virtual_alias_maps and virtual(5) --- --- -CREATE OR REPLACE FUNCTION postfix_virtual_alias_map( - IN localpart varchar, IN the_domain varchar) - RETURNS SETOF recipient_destination -AS $$ - DECLARE - record recipient_destination; - recipient varchar(320) := localpart || '@' || the_domain; - did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); - BEGIN - FOR record IN - SELECT recipient, destination - FROM alias - WHERE gid = did - AND address = localpart - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: recipient_destination records --- --- Required access privileges for your postfix database user: --- GRANT SELECT ON domain_name, relocated TO postfix; --- --- For more details see postconf(5) section relocated_maps and relocated(5) --- --- -CREATE OR REPLACE FUNCTION postfix_relocated_map( - IN localpart varchar, IN the_domain varchar) - RETURNS SETOF recipient_destination -AS $$ - DECLARE - record recipient_destination; - recipient varchar(320) := localpart || '@' || the_domain; - did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); - BEGIN - FOR record IN - SELECT recipient, destination - FROM relocated - WHERE gid = did - AND address = localpart - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Data type for function postfix_transport_map(varchar, varchar) --- --- -CREATE TYPE recipient_transport AS ( - recipient varchar(320), - transport text -); - --- --- --- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: recipient_transport records --- --- Required access privileges for your postfix database user: --- GRANT SELECT ON users, transport, domain_name TO postfix; --- --- For more details see postconf(5) section transport_maps and transport(5) --- --- -CREATE OR REPLACE FUNCTION postfix_transport_map( - IN localpart varchar, IN the_domain varchar) - RETURNS SETOF recipient_transport -AS $$ - DECLARE - record recipient_transport; - recipient varchar(320) := localpart || '@' || the_domain; - BEGIN - FOR record IN - SELECT recipient, transport - FROM transport - WHERE tid = (SELECT tid - FROM users - WHERE gid = (SELECT gid - FROM domain_name - WHERE domainname = the_domain) - AND local_part = localpart) - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Data type for function postfix_virtual_uid_map(varchar, varchar) --- --- -CREATE TYPE recipient_uid AS ( - recipient varchar(320), - uid bigint -); - --- --- --- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: recipient_uid records --- --- Required access privileges for your postfix database user: --- GRANT SELECT ON users, domain_name TO postfix; --- --- For more details see postconf(5) section virtual_uid_maps --- --- -CREATE OR REPLACE FUNCTION postfix_virtual_uid_map( - IN localpart varchar, IN the_domain varchar) RETURNS SETOF recipient_uid -AS $$ - DECLARE - record recipient_uid; - recipient varchar(320) := localpart || '@' || the_domain; - BEGIN - FOR record IN - SELECT recipient, uid - FROM users - WHERE gid = (SELECT gid - FROM domain_name - WHERE domainname = the_domain) - AND local_part = localpart - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Data type for function dovecotuser(varchar, varchar) --- --- -CREATE TYPE dovecotuser AS ( - userid varchar(320), - uid bigint, - gid bigint, - home text, - mail text -); - --- --- --- Parameters (from login name [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: dovecotuser records --- --- Required access privileges for your dovecot database user: --- GRANT SELECT ON users,domain_data,domain_name,maillocation TO dovecot; --- --- For more details see http://wiki.dovecot.org/UserDatabase --- --- -CREATE OR REPLACE FUNCTION dovecotuser( - IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotuser -AS $$ - DECLARE - record dovecotuser; - userid varchar(320) := localpart || '@' || the_domain; - did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); - BEGIN - FOR record IN - SELECT userid, uid, did, domaindir ||'/'|| uid AS home, - '~/'|| maillocation AS mail - FROM users, domain_data, maillocation - WHERE users.gid = did - AND users.local_part = localpart - AND maillocation.mid = users.mid - AND domain_data.gid = did - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Data type for function dovecotpassword(varchar, varchar) --- --- -CREATE TYPE dovecotpassword AS ( - userid varchar(320), - password varchar(74), - smtp boolean, - pop3 boolean, - imap boolean, - sieve boolean -); - --- --- --- Parameters (from login name [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: dovecotpassword records --- --- Required access privileges for your dovecot database user: --- GRANT SELECT ON users, domain_name TO dovecot; --- --- For more details see http://wiki.dovecot.org/AuthDatabase/SQL --- --- -CREATE OR REPLACE FUNCTION dovecotpassword( - IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotpassword -AS $$ - DECLARE - record dovecotpassword; - userid varchar(320) := localpart || '@' || the_domain; - BEGIN - FOR record IN - SELECT userid, passwd, smtp, pop3, imap, sieve - FROM users - WHERE gid = (SELECT gid - FROM domain_name - WHERE domainname = the_domain) - AND local_part = localpart - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; diff -r c0e1fb1b0145 -r a4aead244f75 pgsql/create_optional_types_and_functions.pgsql --- a/pgsql/create_optional_types_and_functions.pgsql Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,374 +0,0 @@ --- --- Information: --- This file contains some data types and functions these should speed up some --- operations. Read the comment on each data type/functions for more details. --- --- - --- --- --- Data type for function postfix_smtpd_sender_login_map(varchar, varchar) --- --- -CREATE TYPE sender_login AS ( - sender varchar(320), - login text -); - --- --- --- Parameters (from _sender_ address (MAIL FROM) [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: SASL _login_ names that own _sender_ addresses (MAIL FROM): --- set of sender_login records. --- --- Required access privileges for your postfix database user: --- GRANT SELECT ON domain_name, users, alias TO postfix; --- --- For more details see postconf(5) section smtpd_sender_login_maps --- --- -CREATE OR REPLACE FUNCTION postfix_smtpd_sender_login_map( - IN localpart varchar, IN the_domain varchar) RETURNS SETOF sender_login -AS $$ - DECLARE - rec sender_login; - did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); - sender varchar(320) := localpart || '@' || the_domain; - BEGIN - -- Get all addresses for 'localpart' in the primary and aliased domains - FOR rec IN - SELECT sender, local_part || '@' || domainname - FROM domain_name, users - WHERE domain_name.gid = did - AND users.gid = did - AND users.local_part = localpart - LOOP - RETURN NEXT rec; - END LOOP; - IF NOT FOUND THEN - -- Loop over the alias addresses for localpart@the_domain - FOR rec IN - SELECT DISTINCT sender, destination - FROM alias - WHERE alias.gid = did - AND alias.address = localpart - LOOP - RETURN NEXT rec; - END LOOP; - END IF; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Data type for function postfix_virtual_mailbox(varchar, varchar) --- --- -CREATE TYPE address_maildir AS ( - address varchar(320), - maildir text -); - --- --- --- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: address_maildir records --- --- Required access privileges for your postfix database user: --- GRANT SELECT ON domain_data,domain_name,maillocation,users TO postfix; --- --- For more details see postconf(5) section virtual_mailbox_maps --- --- -CREATE OR REPLACE FUNCTION postfix_virtual_mailbox_map( - IN localpart varchar, IN the_domain varchar) RETURNS SETOF address_maildir -AS $$ - DECLARE - rec address_maildir; - did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); - address varchar(320) := localpart || '@' || the_domain; - BEGIN - FOR rec IN - SELECT address, domaindir||'/'||users.uid||'/'||maillocation||'/' - FROM domain_data, users, maillocation - WHERE domain_data.gid = did - AND users.gid = did - AND users.local_part = localpart - AND maillocation.mid = users.mid - LOOP - RETURN NEXT rec; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Data type for functions: postfix_relocated_map(varchar, varchar) --- postfix_virtual_alias_map(varchar, varchar) --- --- --- -CREATE TYPE recipient_destination AS ( - recipient varchar(320), - destination text -); - --- --- --- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: recipient_destination records --- --- Required access privileges for your postfix database user: --- GRANT SELECT ON alias, domain_name TO postfix; --- --- For more details see postconf(5) section virtual_alias_maps and virtual(5) --- --- -CREATE OR REPLACE FUNCTION postfix_virtual_alias_map( - IN localpart varchar, IN the_domain varchar) - RETURNS SETOF recipient_destination -AS $$ - DECLARE - record recipient_destination; - recipient varchar(320) := localpart || '@' || the_domain; - did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); - BEGIN - FOR record IN - SELECT recipient, destination - FROM alias - WHERE gid = did - AND address = localpart - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: recipient_destination records --- --- Required access privileges for your postfix database user: --- GRANT SELECT ON domain_name, relocated TO postfix; --- --- For more details see postconf(5) section relocated_maps and relocated(5) --- --- -CREATE OR REPLACE FUNCTION postfix_relocated_map( - IN localpart varchar, IN the_domain varchar) - RETURNS SETOF recipient_destination -AS $$ - DECLARE - record recipient_destination; - recipient varchar(320) := localpart || '@' || the_domain; - did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); - BEGIN - FOR record IN - SELECT recipient, destination - FROM relocated - WHERE gid = did - AND address = localpart - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Data type for function postfix_transport_map(varchar, varchar) --- --- -CREATE TYPE recipient_transport AS ( - recipient varchar(320), - transport text -); - --- --- --- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: recipient_transport records --- --- Required access privileges for your postfix database user: --- GRANT SELECT ON users, transport, domain_name TO postfix; --- --- For more details see postconf(5) section transport_maps and transport(5) --- --- -CREATE OR REPLACE FUNCTION postfix_transport_map( - IN localpart varchar, IN the_domain varchar) - RETURNS SETOF recipient_transport -AS $$ - DECLARE - record recipient_transport; - recipient varchar(320) := localpart || '@' || the_domain; - BEGIN - FOR record IN - SELECT recipient, transport - FROM transport - WHERE tid = (SELECT tid - FROM users - WHERE gid = (SELECT gid - FROM domain_name - WHERE domainname = the_domain) - AND local_part = localpart) - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Data type for function postfix_virtual_uid_map(varchar, varchar) --- --- -CREATE TYPE recipient_uid AS ( - recipient varchar(320), - uid bigint -); - --- --- --- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: recipient_uid records --- --- Required access privileges for your postfix database user: --- GRANT SELECT ON users, domain_name TO postfix; --- --- For more details see postconf(5) section virtual_uid_maps --- --- -CREATE OR REPLACE FUNCTION postfix_virtual_uid_map( - IN localpart varchar, IN the_domain varchar) RETURNS SETOF recipient_uid -AS $$ - DECLARE - record recipient_uid; - recipient varchar(320) := localpart || '@' || the_domain; - BEGIN - FOR record IN - SELECT recipient, uid - FROM users - WHERE gid = (SELECT gid - FROM domain_name - WHERE domainname = the_domain) - AND local_part = localpart - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Data type for function dovecotuser(varchar, varchar) --- --- -CREATE TYPE dovecotuser AS ( - userid varchar(320), - uid bigint, - gid bigint, - home text, - mail text -); - --- --- --- Parameters (from login name [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: dovecotuser records --- --- Required access privileges for your dovecot database user: --- GRANT SELECT ON users,domain_data,domain_name,maillocation TO dovecot; --- --- For more details see http://wiki.dovecot.org/UserDatabase --- --- -CREATE OR REPLACE FUNCTION dovecotuser( - IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotuser -AS $$ - DECLARE - record dovecotuser; - userid varchar(320) := localpart || '@' || the_domain; - did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); - BEGIN - FOR record IN - SELECT userid, uid, did, domaindir ||'/'|| uid AS home, - '~/'|| maillocation AS mail - FROM users, domain_data, maillocation - WHERE users.gid = did - AND users.local_part = localpart - AND maillocation.mid = users.mid - AND domain_data.gid = did - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - --- ########################################################################## -- - --- --- --- Data type for function dovecotpassword(varchar, varchar) --- --- -CREATE TYPE dovecotpassword AS ( - userid varchar(320), - password varchar(74), - smtp boolean, - pop3 boolean, - imap boolean, - managesieve boolean -); - --- --- --- Parameters (from login name [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: dovecotpassword records --- --- Required access privileges for your dovecot database user: --- GRANT SELECT ON users, domain_name TO dovecot; --- --- For more details see http://wiki.dovecot.org/AuthDatabase/SQL --- --- -CREATE OR REPLACE FUNCTION dovecotpassword( - IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotpassword -AS $$ - DECLARE - record dovecotpassword; - userid varchar(320) := localpart || '@' || the_domain; - BEGIN - FOR record IN - SELECT userid, passwd, smtp, pop3, imap, managesieve - FROM users - WHERE gid = (SELECT gid - FROM domain_name - WHERE domainname = the_domain) - AND local_part = localpart - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; diff -r c0e1fb1b0145 -r a4aead244f75 pgsql/create_tables-dovecot-1.2.x.pgsql --- a/pgsql/create_tables-dovecot-1.2.x.pgsql Mon Nov 07 03:22:15 2011 +0000 +++ b/pgsql/create_tables-dovecot-1.2.x.pgsql Thu Jun 28 19:26:50 2012 +0000 @@ -4,8 +4,14 @@ CREATE SEQUENCE transport_id; +CREATE SEQUENCE mailboxformat_id; + CREATE SEQUENCE maillocation_id; +CREATE SEQUENCE quotalimit_id; + +CREATE SEQUENCE service_set_id; + CREATE SEQUENCE domain_gid START WITH 70000 INCREMENT BY 1 @@ -30,20 +36,80 @@ -- Insert default transport INSERT INTO transport(transport) VALUES ('dovecot:'); -CREATE TABLE maillocation( - mid bigint NOT NULL DEFAULT nextval('maillocation_id'), - maillocation varchar(20) NOT NULL, +CREATE TABLE mailboxformat ( + fid bigint NOT NULL DEFAULT nextval('mailboxformat_id'), + format varchar(20) NOT NULL, + CONSTRAINT pkey_mailboxformat PRIMARY KEY (fid), + CONSTRAINT ukey_mailboxformat UNIQUE (format) +); +-- Insert supported mailbox formats +INSERT INTO mailboxformat(format) VALUES ('maildir'); +INSERT INTO mailboxformat(format) VALUES ('mdbox'); +INSERT INTO mailboxformat(format) VALUES ('sdbox'); + +CREATE TABLE maillocation ( + mid bigint NOT NULL DEFAULT nextval('maillocation_id'), + fid bigint NOT NULL DEFAULT 1, + directory varchar(20) NOT NULL, + extra varchar(1024), CONSTRAINT pkey_maillocation PRIMARY KEY (mid), - CONSTRAINT ukey_maillocation UNIQUE (maillocation) + CONSTRAINT fkey_maillocation_fid_mailboxformat FOREIGN KEY (fid) + REFERENCES mailboxformat (fid) ); -- Insert default Maildir-folder name -INSERT INTO maillocation(maillocation) VALUES ('Maildir'); +INSERT INTO maillocation(directory) VALUES ('Maildir'); + +CREATE TABLE quotalimit ( + qid bigint NOT NULL DEFAULT nextval('quotalimit_id'), + bytes bigint NOT NULL, + messages integer NOT NULL DEFAULT 0, + CONSTRAINT pkey_quotalimit PRIMARY KEY (qid), + CONSTRAINT ukey_quotalimit UNIQUE (bytes, messages) +); +-- Insert default (non) quota limit +INSERT INTO quotalimit(bytes, messages) VALUES (0, 0); + +CREATE TABLE service_set ( + ssid bigint NOT NULL DEFAULT nextval('service_set_id'), + smtp boolean NOT NULL DEFAULT TRUE, + pop3 boolean NOT NULL DEFAULT TRUE, + imap boolean NOT NULL DEFAULT TRUE, + sieve boolean NOT NULL DEFAULT TRUE, + CONSTRAINT pkey_service_set PRIMARY KEY (ssid), + CONSTRAINT ukey_service_set UNIQUE (smtp, pop3, imap, sieve) +); +-- Insert all possible service combinations +COPY service_set (smtp, pop3, imap, sieve) FROM stdin; +TRUE TRUE TRUE TRUE +FALSE TRUE TRUE TRUE +TRUE FALSE TRUE TRUE +FALSE FALSE TRUE TRUE +TRUE TRUE FALSE TRUE +FALSE TRUE FALSE TRUE +TRUE FALSE FALSE TRUE +FALSE FALSE FALSE TRUE +TRUE TRUE TRUE FALSE +FALSE TRUE TRUE FALSE +TRUE FALSE TRUE FALSE +FALSE FALSE TRUE FALSE +TRUE TRUE FALSE FALSE +FALSE TRUE FALSE FALSE +TRUE FALSE FALSE FALSE +FALSE FALSE FALSE FALSE +\. CREATE TABLE domain_data ( gid bigint NOT NULL DEFAULT nextval('domain_gid'), - tid bigint NOT NULL DEFAULT 1, -- defualt transport + qid bigint NOT NULL DEFAULT 1, -- default quota limit + ssid bigint NOT NULL DEFAULT 1, -- default service set + tid bigint NOT NULL DEFAULT 1, -- default transport domaindir varchar(40) NOT NULL, --/srv/mail/$RAND/4294967294 + note text NULL DEFAULT NULL, CONSTRAINT pkey_domain_data PRIMARY KEY (gid), + CONSTRAINT fkey_domain_data_qid_quotalimit FOREIGN KEY (qid) + REFERENCES quotalimit (qid), + CONSTRAINT fkey_domain_data_ssid_service_set FOREIGN KEY (ssid) + REFERENCES service_set (ssid), CONSTRAINT fkey_domain_data_tid_transport FOREIGN KEY (tid) REFERENCES transport (tid) ); @@ -59,26 +125,38 @@ CREATE TABLE users ( local_part varchar(64) NOT NULL,-- only localpart w/o '@' - passwd varchar(74) NOT NULL,-- {CRAM-MD5}+64hex numbers + passwd varchar(270) NOT NULL, name varchar(128) NULL, uid bigint NOT NULL DEFAULT nextval('users_uid'), gid bigint NOT NULL, mid bigint NOT NULL DEFAULT 1, - tid bigint NOT NULL DEFAULT 1, - smtp boolean NOT NULL DEFAULT TRUE, - pop3 boolean NOT NULL DEFAULT TRUE, - imap boolean NOT NULL DEFAULT TRUE, - sieve boolean NOT NULL DEFAULT TRUE, + qid bigint NULL DEFAULT NULL, + ssid bigint NULL DEFAULT NULL, + tid bigint NULL DEFAULT NULL, + note text NULL DEFAULT NULL, CONSTRAINT pkey_users PRIMARY KEY (local_part, gid), CONSTRAINT ukey_users_uid UNIQUE (uid), CONSTRAINT fkey_users_gid_domain_data FOREIGN KEY (gid) REFERENCES domain_data (gid), CONSTRAINT fkey_users_mid_maillocation FOREIGN KEY (mid) REFERENCES maillocation (mid), + CONSTRAINT fkey_users_qid_quotalimit FOREIGN KEY (qid) + REFERENCES quotalimit (qid), + CONSTRAINT fkey_users_ssid_service_set FOREIGN KEY (ssid) + REFERENCES service_set (ssid), CONSTRAINT fkey_users_tid_transport FOREIGN KEY (tid) REFERENCES transport (tid) ); +CREATE TABLE userquota ( + uid bigint NOT NULL, + bytes bigint NOT NULL DEFAULT 0, + messages integer NOT NULL DEFAULT 0, + CONSTRAINT pkey_userquota PRIMARY KEY (uid), + CONSTRAINT fkey_userquota_uid_users FOREIGN KEY (uid) + REFERENCES users (uid) ON DELETE CASCADE +); + CREATE TABLE alias ( gid bigint NOT NULL, address varchar(64) NOT NULL,-- only localpart w/o '@' @@ -97,59 +175,20 @@ REFERENCES domain_data (gid) ); -CREATE OR REPLACE VIEW dovecot_password AS - SELECT local_part || '@' || domain_name.domainname AS "user", - passwd AS "password", smtp, pop3, imap, sieve - FROM users - LEFT JOIN domain_name USING (gid); - -CREATE OR REPLACE VIEW dovecot_user AS - SELECT local_part || '@' || domain_name.domainname AS userid, - uid, gid, domain_data.domaindir || '/' || uid AS home, - '~/' || maillocation.maillocation AS mail - FROM users - LEFT JOIN domain_data USING (gid) - LEFT JOIN domain_name USING (gid) - LEFT JOIN maillocation USING (mid); +CREATE TABLE catchall ( + gid bigint NOT NULL, + destination varchar(320) NOT NULL, + CONSTRAINT pkey_catchall PRIMARY KEY (gid, destination), + CONSTRAINT fkey_catchall_gid_domain_data FOREIGN KEY (gid) + REFERENCES domain_data (gid) +); CREATE OR REPLACE VIEW postfix_gid AS SELECT gid, domainname FROM domain_name; -CREATE OR REPLACE VIEW postfix_uid AS - SELECT local_part || '@' || domain_name.domainname AS address, uid - FROM users - LEFT JOIN domain_name USING (gid); - -CREATE OR REPLACE VIEW postfix_maildir AS - SELECT local_part || '@' || domain_name.domainname AS address, - domain_data.domaindir||'/'||uid||'/'||maillocation.maillocation||'/' - AS maildir - FROM users - LEFT JOIN domain_data USING (gid) - LEFT JOIN domain_name USING (gid) - LEFT JOIN maillocation USING (mid); - -CREATE OR REPLACE VIEW postfix_relocated AS - SELECT address || '@' || domain_name.domainname AS address, destination - FROM relocated - LEFT JOIN domain_name USING (gid); - -CREATE OR REPLACE VIEW postfix_alias AS - SELECT address || '@' || domain_name.domainname AS address, destination, gid - FROM alias - LEFT JOIN domain_name USING (gid); - -CREATE OR REPLACE VIEW postfix_transport AS - SELECT local_part || '@' || domain_name.domainname AS address, - transport.transport - FROM users - LEFT JOIN transport USING (tid) - LEFT JOIN domain_name USING (gid); - CREATE OR REPLACE VIEW vmm_domain_info AS - SELECT gid, domainname, transport, domaindir, - count(uid) AS accounts, + SELECT gid, count(uid) AS accounts, (SELECT count(DISTINCT address) FROM alias WHERE alias.gid = domain_data.gid) AS aliases, @@ -159,17 +198,92 @@ (SELECT count(gid) FROM domain_name WHERE domain_name.gid = domain_data.gid - AND NOT domain_name.is_primary) AS aliasdomains + AND NOT domain_name.is_primary) AS aliasdomains, + (SELECT count(gid) + FROM catchall + WHERE catchall.gid = domain_data.gid) AS catchall FROM domain_data LEFT JOIN domain_name USING (gid) - LEFT JOIN transport USING (tid) LEFT JOIN users USING (gid) WHERE domain_name.is_primary - GROUP BY gid, domainname, transport, domaindir; + GROUP BY gid; +-- ########################################################################## -- CREATE LANGUAGE plpgsql; +-- ######################## TYPEs ########################################### -- + +-- --- +-- Data type for function postfix_virtual_mailbox(varchar, varchar) +-- --- +CREATE TYPE address_maildir AS ( + address varchar(320), + maildir text +); +-- --- +-- Data type for function dovecotpassword(varchar, varchar) +-- --- +CREATE TYPE dovecotpassword AS ( + userid varchar(320), + password varchar(270), + smtp boolean, + pop3 boolean, + imap boolean, + sieve boolean +); +-- --- +-- Data type for function dovecotquotauser(varchar, varchar) +-- --- +CREATE TYPE dovecotquotauser AS ( + userid varchar(320), + uid bigint, + gid bigint, + home text, + mail text, + quota_rule text +); +-- --- +-- Data type for function dovecotuser(varchar, varchar) +-- --- +CREATE TYPE dovecotuser AS ( + userid varchar(320), + uid bigint, + gid bigint, + home text, + mail text +); +-- --- +-- Data type for functions: postfix_relocated_map(varchar, varchar) +-- postfix_virtual_alias_map(varchar, varchar) +-- --- +CREATE TYPE recipient_destination AS ( + recipient varchar(320), + destination text +); +-- --- +-- Data type for function postfix_transport_map(varchar, varchar) +-- --- +CREATE TYPE recipient_transport AS ( + recipient varchar(320), + transport text +); +-- --- +-- Data type for function postfix_virtual_uid_map(varchar, varchar) +-- --- +CREATE TYPE recipient_uid AS ( + recipient varchar(320), + uid bigint +); +-- --- +-- Data type for function postfix_smtpd_sender_login_map(varchar, varchar) +-- --- +CREATE TYPE sender_login AS ( + sender varchar(320), + login text +); + +-- ######################## TRIGGERs ######################################## -- CREATE OR REPLACE FUNCTION domain_primary_trigger() RETURNS TRIGGER AS $$ DECLARE @@ -194,3 +308,463 @@ CREATE TRIGGER primary_count_upd AFTER UPDATE ON domain_name FOR EACH ROW EXECUTE PROCEDURE domain_primary_trigger(); + + +CREATE OR REPLACE FUNCTION merge_userquota() RETURNS TRIGGER AS $$ +BEGIN + IF NEW.messages < 0 OR NEW.messages IS NULL THEN + IF NEW.messages IS NULL THEN + NEW.messages = 0; + ELSE + NEW.messages = -NEW.messages; + END IF; + RETURN NEW; + END IF; + LOOP + UPDATE userquota + SET bytes = bytes + NEW.bytes, messages = messages + NEW.messages + WHERE uid = NEW.uid; + IF found THEN + RETURN NULL; + END IF; + BEGIN + IF NEW.messages = 0 THEN + INSERT INTO userquota VALUES (NEW.uid, NEW.bytes, NULL); + ELSE + INSERT INTO userquota VALUES (NEW.uid, NEW.bytes, -NEW.messages); + END IF; + RETURN NULL; + EXCEPTION + WHEN unique_violation THEN + -- do nothing, and loop to try the UPDATE again + WHEN foreign_key_violation THEN + -- break the loop: a non matching uid means no such user + RETURN NULL; + END; + END LOOP; +END; +$$ LANGUAGE plpgsql; + + +CREATE TRIGGER mergeuserquota BEFORE INSERT ON userquota + FOR EACH ROW EXECUTE PROCEDURE merge_userquota(); + +-- ######################## FUNCTIONs ####################################### -- + +-- --- +-- Parameters (from login name [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: dovecotpassword records +-- +-- Required access privileges for your dovecot database user: +-- GRANT SELECT ON users, domain_name, service_set TO dovecot; +-- +-- For more details see http://wiki.dovecot.org/AuthDatabase/SQL +-- --- +CREATE OR REPLACE FUNCTION dovecotpassword( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotpassword +AS $$ + DECLARE + record dovecotpassword; + userid varchar(320) := localpart || '@' || the_domain; + BEGIN + FOR record IN + SELECT userid, passwd, smtp, pop3, imap, sieve + FROM users, service_set, domain_data + WHERE users.gid = (SELECT gid + FROM domain_name + WHERE domainname = the_domain) + AND local_part = localpart + AND users.gid = domain_data.gid + AND CASE WHEN + users.ssid IS NOT NULL + THEN + service_set.ssid = users.ssid + ELSE + service_set.ssid = domain_data.ssid + END + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Nearly the same as function dovecotuser below. It returns additionally the +-- field quota_rule. +-- +-- Required access privileges for your dovecot database user: +-- GRANT SELECT +-- ON users, domain_data, domain_name, maillocation, mailboxformat, +-- quotalimit +-- TO dovecot; +-- --- +CREATE OR REPLACE FUNCTION dovecotquotauser( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotquotauser +AS $$ + DECLARE + record dovecotquotauser; + userid varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT userid, uid, did, domaindir || '/' || uid AS home, + format || ':~/' || directory AS mail, '*:bytes=' || + bytes || ':messages=' || messages AS quota_rule + FROM users, domain_data, mailboxformat, maillocation, quotalimit + WHERE users.gid = did + AND users.local_part = localpart + AND maillocation.mid = users.mid + AND mailboxformat.fid = maillocation.fid + AND domain_data.gid = did + AND CASE WHEN + users.qid IS NOT NULL + THEN + quotalimit.qid = users.qid + ELSE + quotalimit.qid = domain_data.qid + END + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from login name [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: dovecotuser records +-- +-- Required access privileges for your dovecot database user: +-- GRANT SELECT +-- ON users, domain_data, domain_name, maillocation, mailboxformat +-- TO dovecot; +-- +-- For more details see http://wiki.dovecot.org/UserDatabase +-- --- +CREATE OR REPLACE FUNCTION dovecotuser( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotuser +AS $$ + DECLARE + record dovecotuser; + userid varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT userid, uid, did, domaindir || '/' || uid AS home, + format || ':~/' || directory AS mail + FROM users, domain_data, mailboxformat, maillocation + WHERE users.gid = did + AND users.local_part = localpart + AND maillocation.mid = users.mid + AND mailboxformat.fid = maillocation.fid + AND domain_data.gid = did + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_destination records +-- +-- Required access privileges for your postfix database user: +-- GRANT SELECT ON domain_name, relocated TO postfix; +-- +-- For more details see postconf(5) section relocated_maps and relocated(5) +-- --- +CREATE OR REPLACE FUNCTION postfix_relocated_map( + IN localpart varchar, IN the_domain varchar) + RETURNS SETOF recipient_destination +AS $$ + DECLARE + record recipient_destination; + recipient varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT recipient, destination + FROM relocated + WHERE gid = did + AND address = localpart + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from _sender_ address (MAIL FROM) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: SASL _login_ names that own _sender_ addresses (MAIL FROM): +-- set of sender_login records. +-- +-- Required access privileges for your postfix database user: +-- GRANT SELECT ON domain_name, users, alias TO postfix; +-- +-- For more details see postconf(5) section smtpd_sender_login_maps +-- --- +CREATE OR REPLACE FUNCTION postfix_smtpd_sender_login_map( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF sender_login +AS $$ + DECLARE + rec sender_login; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + sender varchar(320) := localpart || '@' || the_domain; + BEGIN + -- Get all addresses for 'localpart' in the primary and aliased domains + FOR rec IN + SELECT sender, local_part || '@' || domainname + FROM domain_name, users + WHERE domain_name.gid = did + AND users.gid = did + AND users.local_part = localpart + LOOP + RETURN NEXT rec; + END LOOP; + IF NOT FOUND THEN + -- Loop over the alias addresses for localpart@the_domain + FOR rec IN + SELECT DISTINCT sender, destination + FROM alias + WHERE alias.gid = did + AND alias.address = localpart + LOOP + RETURN NEXT rec; + END LOOP; + END IF; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_transport records +-- +-- Required access privileges for your postfix database user: +-- GRANT SELECT ON users, transport, domain_name TO postfix; +-- +-- For more details see postconf(5) section transport_maps and transport(5) +-- --- +CREATE OR REPLACE FUNCTION postfix_transport_map( + IN localpart varchar, IN the_domain varchar) + RETURNS SETOF recipient_transport +AS $$ + DECLARE + record recipient_transport; + recipient varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname = the_domain); + transport_id bigint; + BEGIN + IF did IS NULL THEN + RETURN; + END IF; + + SELECT tid INTO transport_id + FROM users + WHERE gid = did AND local_part = localpart; + + IF transport_id IS NULL THEN + SELECT tid INTO STRICT transport_id + FROM domain_data + WHERE gid = did; + END IF; + + FOR record IN + SELECT recipient, transport + FROM transport + WHERE tid = transport_id + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_destination records +-- +-- Required access privileges for your postfix database user: +-- GRANT SELECT ON alias, domain_name TO postfix; +-- +-- For more details see postconf(5) section virtual_alias_maps and virtual(5) +-- --- +CREATE OR REPLACE FUNCTION _interpolate_destination( + IN destination varchar, localpart varchar, IN the_domain varchar) + RETURNS varchar +AS $$ + DECLARE + result varchar(320); + BEGIN + IF position('%' in destination) = 0 THEN + RETURN destination; + END IF; + result := replace(destination, '%n', localpart); + result := replace(result, '%d', the_domain); + result := replace(result, '%=', localpart || '=' || the_domain); + RETURN result; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; + +CREATE OR REPLACE FUNCTION postfix_virtual_alias_map( + IN localpart varchar, IN the_domain varchar) + RETURNS SETOF recipient_destination +AS $$ + DECLARE + recordc recipient_destination; + record recipient_destination; + catchall_cursor refcursor; + recipient varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT recipient, + _interpolate_destination(destination, localpart, the_domain) + FROM alias + WHERE gid = did + AND address = localpart + LOOP + RETURN NEXT record; + END LOOP; + + IF NOT FOUND THEN + -- There is no matching virtual_alias. If there are no catchall + -- records for this domain, we can just return NULL since Postfix + -- will then later consult mailboxes/relocated itself. But if + -- there is a catchall destination, then it would take precedence + -- over mailboxes/relocated, which is not what we want. Therefore, + -- we must first find out if the query is for an existing mailbox + -- or relocated entry and return the identity mapping if that is + -- the case + OPEN catchall_cursor FOR + SELECT recipient, + _interpolate_destination(destination, localpart, the_domain) + FROM catchall + WHERE gid = did; + FETCH NEXT FROM catchall_cursor INTO recordc; + + IF recordc IS NOT NULL THEN + -- Since there are catchall records for this domain + -- check the mailbox and relocated records and return identity + -- if a matching record exists. + FOR record IN + SELECT recipient, recipient as destination + FROM users + WHERE gid = did + AND local_part = localpart + UNION SELECT recipient, recipient as destination + FROM relocated + WHERE gid = did + AND address = localpart + LOOP + RETURN NEXT record; + END LOOP; + + IF NOT FOUND THEN + -- There were no records found for mailboxes/relocated, + -- so now we can actually iterate the cursor and populate + -- the return set + LOOP + RETURN NEXT recordc; + FETCH NEXT FROM catchall_cursor INTO recordc; + EXIT WHEN recordc IS NULL; + END LOOP; + END IF; + END IF; + CLOSE catchall_cursor; + END IF; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: address_maildir records +-- +-- Required access privileges for your postfix database user: +-- GRANT SELECT ON domain_data,domain_name,maillocation,users TO postfix; +-- +-- For more details see postconf(5) section virtual_mailbox_maps +-- --- +CREATE OR REPLACE FUNCTION postfix_virtual_mailbox_map( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF address_maildir +AS $$ + DECLARE + rec address_maildir; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + address varchar(320) := localpart || '@' || the_domain; + BEGIN + FOR rec IN + SELECT address, domaindir||'/'||users.uid||'/'||directory||'/' + FROM domain_data, users, maillocation + WHERE domain_data.gid = did + AND users.gid = did + AND users.local_part = localpart + AND maillocation.mid = users.mid + LOOP + RETURN NEXT rec; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_uid records +-- +-- Required access privileges for your postfix database user: +-- GRANT SELECT ON users, domain_name TO postfix; +-- +-- For more details see postconf(5) section virtual_uid_maps +-- --- +CREATE OR REPLACE FUNCTION postfix_virtual_uid_map( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF recipient_uid +AS $$ + DECLARE + record recipient_uid; + recipient varchar(320) := localpart || '@' || the_domain; + BEGIN + FOR record IN + SELECT recipient, uid + FROM users + WHERE gid = (SELECT gid + FROM domain_name + WHERE domainname = the_domain) + AND local_part = localpart + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; diff -r c0e1fb1b0145 -r a4aead244f75 pgsql/create_tables.pgsql --- a/pgsql/create_tables.pgsql Mon Nov 07 03:22:15 2011 +0000 +++ b/pgsql/create_tables.pgsql Thu Jun 28 19:26:50 2012 +0000 @@ -4,8 +4,14 @@ CREATE SEQUENCE transport_id; +CREATE SEQUENCE mailboxformat_id; + +CREATE SEQUENCE quotalimit_id; + CREATE SEQUENCE maillocation_id; +CREATE SEQUENCE service_set_id; + CREATE SEQUENCE domain_gid START WITH 70000 INCREMENT BY 1 @@ -30,20 +36,80 @@ -- Insert default transport INSERT INTO transport(transport) VALUES ('dovecot:'); -CREATE TABLE maillocation( - mid bigint NOT NULL DEFAULT nextval('maillocation_id'), - maillocation varchar(20) NOT NULL, +CREATE TABLE mailboxformat ( + fid bigint NOT NULL DEFAULT nextval('mailboxformat_id'), + format varchar(20) NOT NULL, + CONSTRAINT pkey_mailboxformat PRIMARY KEY (fid), + CONSTRAINT ukey_mailboxformat UNIQUE (format) +); +-- Insert supported mailbox formats +INSERT INTO mailboxformat(format) VALUES ('maildir'); +INSERT INTO mailboxformat(format) VALUES ('mdbox'); +INSERT INTO mailboxformat(format) VALUES ('sdbox'); + +CREATE TABLE maillocation ( + mid bigint NOT NULL DEFAULT nextval('maillocation_id'), + fid bigint NOT NULL DEFAULT 1, + directory varchar(20) NOT NULL, + extra varchar(1024), CONSTRAINT pkey_maillocation PRIMARY KEY (mid), - CONSTRAINT ukey_maillocation UNIQUE (maillocation) + CONSTRAINT fkey_maillocation_fid_mailboxformat FOREIGN KEY (fid) + REFERENCES mailboxformat (fid) ); -- Insert default Maildir-folder name -INSERT INTO maillocation(maillocation) VALUES ('Maildir'); +INSERT INTO maillocation(directory) VALUES ('Maildir'); + +CREATE TABLE quotalimit ( + qid bigint NOT NULL DEFAULT nextval('quotalimit_id'), + bytes bigint NOT NULL, + messages integer NOT NULL DEFAULT 0, + CONSTRAINT pkey_quotalimit PRIMARY KEY (qid), + CONSTRAINT ukey_quotalimit UNIQUE (bytes, messages) +); +-- Insert default (non) quota limit +INSERT INTO quotalimit(bytes, messages) VALUES (0, 0); + +CREATE TABLE service_set ( + ssid bigint NOT NULL DEFAULT nextval('service_set_id'), + smtp boolean NOT NULL DEFAULT TRUE, + pop3 boolean NOT NULL DEFAULT TRUE, + imap boolean NOT NULL DEFAULT TRUE, + managesieve boolean NOT NULL DEFAULT TRUE, + CONSTRAINT pkey_service_set PRIMARY KEY (ssid), + CONSTRAINT ukey_service_set UNIQUE (smtp, pop3, imap, managesieve) +); +-- Insert all possible service combinations +COPY service_set (smtp, pop3, imap, managesieve) FROM stdin; +TRUE TRUE TRUE TRUE +FALSE TRUE TRUE TRUE +TRUE FALSE TRUE TRUE +FALSE FALSE TRUE TRUE +TRUE TRUE FALSE TRUE +FALSE TRUE FALSE TRUE +TRUE FALSE FALSE TRUE +FALSE FALSE FALSE TRUE +TRUE TRUE TRUE FALSE +FALSE TRUE TRUE FALSE +TRUE FALSE TRUE FALSE +FALSE FALSE TRUE FALSE +TRUE TRUE FALSE FALSE +FALSE TRUE FALSE FALSE +TRUE FALSE FALSE FALSE +FALSE FALSE FALSE FALSE +\. CREATE TABLE domain_data ( gid bigint NOT NULL DEFAULT nextval('domain_gid'), - tid bigint NOT NULL DEFAULT 1, -- defualt transport + qid bigint NOT NULL DEFAULT 1, -- default quota limit + ssid bigint NOT NULL DEFAULT 1, -- default service_set + tid bigint NOT NULL DEFAULT 1, -- default transport domaindir varchar(40) NOT NULL, --/srv/mail/$RAND/4294967294 + note text NULL DEFAULT NULL, CONSTRAINT pkey_domain_data PRIMARY KEY (gid), + CONSTRAINT fkey_domain_data_qid_quotalimit FOREIGN KEY (qid) + REFERENCES quotalimit (qid), + CONSTRAINT fkey_domain_data_ssid_service_set FOREIGN KEY (ssid) + REFERENCES service_set (ssid), CONSTRAINT fkey_domain_data_tid_transport FOREIGN KEY (tid) REFERENCES transport (tid) ); @@ -59,26 +125,38 @@ CREATE TABLE users ( local_part varchar(64) NOT NULL,-- only localpart w/o '@' - passwd varchar(74) NOT NULL,-- {CRAM-MD5}+64hex numbers + passwd varchar(270) NOT NULL, name varchar(128) NULL, uid bigint NOT NULL DEFAULT nextval('users_uid'), gid bigint NOT NULL, mid bigint NOT NULL DEFAULT 1, - tid bigint NOT NULL DEFAULT 1, - smtp boolean NOT NULL DEFAULT TRUE, - pop3 boolean NOT NULL DEFAULT TRUE, - imap boolean NOT NULL DEFAULT TRUE, - managesieve boolean NOT NULL DEFAULT TRUE, + qid bigint NULL DEFAULT NULL, + ssid bigint NULL DEFAULT NULL, + tid bigint NULL DEFAULT NULL, + note text NULL DEFAULT NULL, CONSTRAINT pkey_users PRIMARY KEY (local_part, gid), CONSTRAINT ukey_users_uid UNIQUE (uid), CONSTRAINT fkey_users_gid_domain_data FOREIGN KEY (gid) REFERENCES domain_data (gid), CONSTRAINT fkey_users_mid_maillocation FOREIGN KEY (mid) REFERENCES maillocation (mid), + CONSTRAINT fkey_users_qid_quotalimit FOREIGN KEY (qid) + REFERENCES quotalimit (qid), + CONSTRAINT fkey_users_ssid_service_set FOREIGN KEY (ssid) + REFERENCES service_set (ssid), CONSTRAINT fkey_users_tid_transport FOREIGN KEY (tid) REFERENCES transport (tid) ); +CREATE TABLE userquota_11 ( + uid bigint NOT NULL, + path varchar(16) NOT NULL, + current bigint NOT NULL DEFAULT 0, + CONSTRAINT pkey_userquota_11 PRIMARY KEY (uid, path), + CONSTRAINT fkey_userquota_11_uid_users FOREIGN KEY (uid) + REFERENCES users (uid) ON DELETE CASCADE +); + CREATE TABLE alias ( gid bigint NOT NULL, address varchar(64) NOT NULL,-- only localpart w/o '@' @@ -97,59 +175,20 @@ REFERENCES domain_data (gid) ); -CREATE OR REPLACE VIEW dovecot_password AS - SELECT local_part || '@' || domain_name.domainname AS "user", - passwd AS "password", smtp, pop3, imap, managesieve - FROM users - LEFT JOIN domain_name USING (gid); - -CREATE OR REPLACE VIEW dovecot_user AS - SELECT local_part || '@' || domain_name.domainname AS userid, - uid, gid, domain_data.domaindir || '/' || uid AS home, - '~/' || maillocation.maillocation AS mail - FROM users - LEFT JOIN domain_data USING (gid) - LEFT JOIN domain_name USING (gid) - LEFT JOIN maillocation USING (mid); +CREATE TABLE catchall ( + gid bigint NOT NULL, + destination varchar(320) NOT NULL, + CONSTRAINT pkey_catchall PRIMARY KEY (gid, destination), + CONSTRAINT fkey_catchall_gid_domain_data FOREIGN KEY (gid) + REFERENCES domain_data (gid) +); CREATE OR REPLACE VIEW postfix_gid AS SELECT gid, domainname FROM domain_name; -CREATE OR REPLACE VIEW postfix_uid AS - SELECT local_part || '@' || domain_name.domainname AS address, uid - FROM users - LEFT JOIN domain_name USING (gid); - -CREATE OR REPLACE VIEW postfix_maildir AS - SELECT local_part || '@' || domain_name.domainname AS address, - domain_data.domaindir||'/'||uid||'/'||maillocation.maillocation||'/' - AS maildir - FROM users - LEFT JOIN domain_data USING (gid) - LEFT JOIN domain_name USING (gid) - LEFT JOIN maillocation USING (mid); - -CREATE OR REPLACE VIEW postfix_relocated AS - SELECT address || '@' || domain_name.domainname AS address, destination - FROM relocated - LEFT JOIN domain_name USING (gid); - -CREATE OR REPLACE VIEW postfix_alias AS - SELECT address || '@' || domain_name.domainname AS address, destination, gid - FROM alias - LEFT JOIN domain_name USING (gid); - -CREATE OR REPLACE VIEW postfix_transport AS - SELECT local_part || '@' || domain_name.domainname AS address, - transport.transport - FROM users - LEFT JOIN transport USING (tid) - LEFT JOIN domain_name USING (gid); - CREATE OR REPLACE VIEW vmm_domain_info AS - SELECT gid, domainname, transport, domaindir, - count(uid) AS accounts, + SELECT gid, count(uid) AS accounts, (SELECT count(DISTINCT address) FROM alias WHERE alias.gid = domain_data.gid) AS aliases, @@ -159,17 +198,92 @@ (SELECT count(gid) FROM domain_name WHERE domain_name.gid = domain_data.gid - AND NOT domain_name.is_primary) AS aliasdomains + AND NOT domain_name.is_primary) AS aliasdomains, + (SELECT count(gid) + FROM catchall + WHERE catchall.gid = domain_data.gid) AS catchall FROM domain_data LEFT JOIN domain_name USING (gid) - LEFT JOIN transport USING (tid) LEFT JOIN users USING (gid) WHERE domain_name.is_primary - GROUP BY gid, domainname, transport, domaindir; + GROUP BY gid; +-- ########################################################################## -- CREATE LANGUAGE plpgsql; +-- ######################## TYPEs ########################################### -- + +-- --- +-- Data type for function postfix_virtual_mailbox(varchar, varchar) +-- --- +CREATE TYPE address_maildir AS ( + address varchar(320), + maildir text +); +-- --- +-- Data type for function dovecotpassword(varchar, varchar) +-- --- +CREATE TYPE dovecotpassword AS ( + userid varchar(320), + password varchar(270), + smtp boolean, + pop3 boolean, + imap boolean, + managesieve boolean +); +-- --- +-- Data type for function dovecotquotauser(varchar, varchar) +-- --- +CREATE TYPE dovecotquotauser AS ( + userid varchar(320), + uid bigint, + gid bigint, + home text, + mail text, + quota_rule text +); +-- --- +-- Data type for function dovecotuser(varchar, varchar) +-- --- +CREATE TYPE dovecotuser AS ( + userid varchar(320), + uid bigint, + gid bigint, + home text, + mail text +); +-- --- +-- Data type for functions: postfix_relocated_map(varchar, varchar) +-- postfix_virtual_alias_map(varchar, varchar) +-- --- +CREATE TYPE recipient_destination AS ( + recipient varchar(320), + destination text +); +-- --- +-- Data type for function postfix_transport_map(varchar, varchar) +-- --- +CREATE TYPE recipient_transport AS ( + recipient varchar(320), + transport text +); +-- --- +-- Data type for function postfix_virtual_uid_map(varchar, varchar) +-- --- +CREATE TYPE recipient_uid AS ( + recipient varchar(320), + uid bigint +); +-- --- +-- Data type for function postfix_smtpd_sender_login_map(varchar, varchar) +-- --- +CREATE TYPE sender_login AS ( + sender varchar(320), + login text +); + +-- ######################## TRIGGERs ######################################## -- CREATE OR REPLACE FUNCTION domain_primary_trigger() RETURNS TRIGGER AS $$ DECLARE @@ -194,3 +308,441 @@ CREATE TRIGGER primary_count_upd AFTER UPDATE ON domain_name FOR EACH ROW EXECUTE PROCEDURE domain_primary_trigger(); + + +CREATE OR REPLACE FUNCTION merge_userquota_11() RETURNS TRIGGER AS $$ +BEGIN + UPDATE userquota_11 + SET current = current + NEW.current + WHERE uid = NEW.uid AND path = NEW.path; + IF found THEN + RETURN NULL; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER mergeuserquota_11 BEFORE INSERT ON userquota_11 + FOR EACH ROW EXECUTE PROCEDURE merge_userquota_11(); + +-- ######################## FUNCTIONs ####################################### -- + +-- --- +-- Parameters (from login name [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: dovecotpassword records +-- +-- Required access privileges for your dovecot database user: +-- GRANT SELECT ON users, domain_name, service_set TO dovecot; +-- +-- For more details see http://wiki.dovecot.org/AuthDatabase/SQL +-- --- +CREATE OR REPLACE FUNCTION dovecotpassword( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotpassword +AS $$ + DECLARE + record dovecotpassword; + userid varchar(320) := localpart || '@' || the_domain; + BEGIN + FOR record IN + SELECT userid, passwd, smtp, pop3, imap, managesieve + FROM users, service_set, domain_data + WHERE users.gid = (SELECT gid + FROM domain_name + WHERE domainname = the_domain) + AND local_part = localpart + AND service_set.ssid = users.ssid + AND users.gid = domain_data.gid + AND CASE WHEN + users.ssid IS NOT NULL + THEN + service_set.ssid = users.ssid + ELSE + service_set.ssid = domain_data.ssid + END + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Nearly the same as function dovecotuser below. It returns additionally the +-- field quota_rule. +-- +-- Required access privileges for your dovecot database user: +-- GRANT SELECT +-- ON users, domain_data, domain_name, maillocation, mailboxformat, +-- quotalimit +-- TO dovecot; +-- --- +CREATE OR REPLACE FUNCTION dovecotquotauser( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotquotauser +AS $$ + DECLARE + record dovecotquotauser; + userid varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT userid, uid, did, domaindir || '/' || uid AS home, + format || ':~/' || directory AS mail, '*:bytes=' || + bytes || ':messages=' || messages AS quota_rule + FROM users, domain_data, mailboxformat, maillocation, quotalimit + WHERE users.gid = did + AND users.local_part = localpart + AND maillocation.mid = users.mid + AND mailboxformat.fid = maillocation.fid + AND domain_data.gid = did + AND CASE WHEN + users.qid IS NOT NULL + THEN + quotalimit.qid = users.qid + ELSE + quotalimit.qid = domain_data.qid + END + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from login name [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: dovecotuser records +-- +-- Required access privileges for your dovecot database user: +-- GRANT SELECT +-- ON users, domain_data, domain_name, maillocation, mailboxformat +-- TO dovecot; +-- +-- For more details see http://wiki.dovecot.org/UserDatabase +-- --- +CREATE OR REPLACE FUNCTION dovecotuser( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotuser +AS $$ + DECLARE + record dovecotuser; + userid varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT userid, uid, did, domaindir || '/' || uid AS home, + format || ':~/' || directory AS mail + FROM users, domain_data, mailboxformat, maillocation + WHERE users.gid = did + AND users.local_part = localpart + AND maillocation.mid = users.mid + AND mailboxformat.fid = maillocation.fid + AND domain_data.gid = did + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_destination records +-- +-- Required access privileges for your postfix database user: +-- GRANT SELECT ON domain_name, relocated TO postfix; +-- +-- For more details see postconf(5) section relocated_maps and relocated(5) +-- --- +CREATE OR REPLACE FUNCTION postfix_relocated_map( + IN localpart varchar, IN the_domain varchar) + RETURNS SETOF recipient_destination +AS $$ + DECLARE + record recipient_destination; + recipient varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT recipient, destination + FROM relocated + WHERE gid = did + AND address = localpart + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from _sender_ address (MAIL FROM) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: SASL _login_ names that own _sender_ addresses (MAIL FROM): +-- set of sender_login records. +-- +-- Required access privileges for your postfix database user: +-- GRANT SELECT ON domain_name, users, alias TO postfix; +-- +-- For more details see postconf(5) section smtpd_sender_login_maps +-- --- +CREATE OR REPLACE FUNCTION postfix_smtpd_sender_login_map( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF sender_login +AS $$ + DECLARE + rec sender_login; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + sender varchar(320) := localpart || '@' || the_domain; + BEGIN + -- Get all addresses for 'localpart' in the primary and aliased domains + FOR rec IN + SELECT sender, local_part || '@' || domainname + FROM domain_name, users + WHERE domain_name.gid = did + AND users.gid = did + AND users.local_part = localpart + LOOP + RETURN NEXT rec; + END LOOP; + IF NOT FOUND THEN + -- Loop over the alias addresses for localpart@the_domain + FOR rec IN + SELECT DISTINCT sender, destination + FROM alias + WHERE alias.gid = did + AND alias.address = localpart + LOOP + RETURN NEXT rec; + END LOOP; + END IF; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_transport records +-- +-- Required access privileges for your postfix database user: +-- GRANT SELECT ON users, transport, domain_name TO postfix; +-- +-- For more details see postconf(5) section transport_maps and transport(5) +-- --- +CREATE OR REPLACE FUNCTION postfix_transport_map( + IN localpart varchar, IN the_domain varchar) + RETURNS SETOF recipient_transport +AS $$ + DECLARE + record recipient_transport; + recipient varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname = the_domain); + transport_id bigint; + BEGIN + IF did IS NULL THEN + RETURN; + END IF; + + SELECT tid INTO transport_id + FROM users + WHERE gid = did AND local_part = localpart; + + IF transport_id IS NULL THEN + SELECT tid INTO STRICT transport_id + FROM domain_data + WHERE gid = did; + END IF; + + FOR record IN + SELECT recipient, transport + FROM transport + WHERE tid = transport_id + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_destination records +-- +-- Required access privileges for your postfix database user: +-- GRANT SELECT ON alias, domain_name TO postfix; +-- +-- For more details see postconf(5) section virtual_alias_maps and virtual(5) +-- --- +CREATE OR REPLACE FUNCTION _interpolate_destination( + IN destination varchar, localpart varchar, IN the_domain varchar) + RETURNS varchar +AS $$ + DECLARE + result varchar(320); + BEGIN + IF position('%' in destination) = 0 THEN + RETURN destination; + END IF; + result := replace(destination, '%n', localpart); + result := replace(result, '%d', the_domain); + result := replace(result, '%=', localpart || '=' || the_domain); + RETURN result; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; + +CREATE OR REPLACE FUNCTION postfix_virtual_alias_map( + IN localpart varchar, IN the_domain varchar) + RETURNS SETOF recipient_destination +AS $$ + DECLARE + recordc recipient_destination; + record recipient_destination; + catchall_cursor refcursor; + recipient varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT recipient, + _interpolate_destination(destination, localpart, the_domain) + FROM alias + WHERE gid = did + AND address = localpart + LOOP + RETURN NEXT record; + END LOOP; + + IF NOT FOUND THEN + -- There is no matching virtual_alias. If there are no catchall + -- records for this domain, we can just return NULL since Postfix + -- will then later consult mailboxes/relocated itself. But if + -- there is a catchall destination, then it would take precedence + -- over mailboxes/relocated, which is not what we want. Therefore, + -- we must first find out if the query is for an existing mailbox + -- or relocated entry and return the identity mapping if that is + -- the case + OPEN catchall_cursor FOR + SELECT recipient, + _interpolate_destination(destination, localpart, the_domain) + FROM catchall + WHERE gid = did; + FETCH NEXT FROM catchall_cursor INTO recordc; + + IF recordc IS NOT NULL THEN + -- Since there are catchall records for this domain + -- check the mailbox and relocated records and return identity + -- if a matching record exists. + FOR record IN + SELECT recipient, recipient as destination + FROM users + WHERE gid = did + AND local_part = localpart + UNION SELECT recipient, recipient as destination + FROM relocated + WHERE gid = did + AND address = localpart + LOOP + RETURN NEXT record; + END LOOP; + + IF NOT FOUND THEN + -- There were no records found for mailboxes/relocated, + -- so now we can actually iterate the cursor and populate + -- the return set + LOOP + RETURN NEXT recordc; + FETCH NEXT FROM catchall_cursor INTO recordc; + EXIT WHEN recordc IS NULL; + END LOOP; + END IF; + END IF; + CLOSE catchall_cursor; + END IF; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: address_maildir records +-- +-- Required access privileges for your postfix database user: +-- GRANT SELECT ON domain_data,domain_name,maillocation,users TO postfix; +-- +-- For more details see postconf(5) section virtual_mailbox_maps +-- --- +CREATE OR REPLACE FUNCTION postfix_virtual_mailbox_map( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF address_maildir +AS $$ + DECLARE + rec address_maildir; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + address varchar(320) := localpart || '@' || the_domain; + BEGIN + FOR rec IN + SELECT address, domaindir||'/'||users.uid||'/'||directory||'/' + FROM domain_data, users, maillocation + WHERE domain_data.gid = did + AND users.gid = did + AND users.local_part = localpart + AND maillocation.mid = users.mid + LOOP + RETURN NEXT rec; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_uid records +-- +-- Required access privileges for your postfix database user: +-- GRANT SELECT ON users, domain_name TO postfix; +-- +-- For more details see postconf(5) section virtual_uid_maps +-- --- +CREATE OR REPLACE FUNCTION postfix_virtual_uid_map( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF recipient_uid +AS $$ + DECLARE + record recipient_uid; + recipient varchar(320) := localpart || '@' || the_domain; + BEGIN + FOR record IN + SELECT recipient, uid + FROM users + WHERE gid = (SELECT gid + FROM domain_name + WHERE domainname = the_domain) + AND local_part = localpart + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; diff -r c0e1fb1b0145 -r a4aead244f75 pgsql/set-permissions.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pgsql/set-permissions.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,169 @@ +#!/usr/bin/env python +# coding: utf-8 +# Copyright 2012, Pascal Volk +# See COPYING for distribution information. + +""" + Use this script in order to set database permissions for your Dovecot + and Postfix database users. + + Run `python set-permissions.py -h` for details. +""" + +import getpass +import sys + +from optparse import OptionParser + +has_psycopg2 = False +try: + import psycopg2 + has_psycopg2 = True +except ImportError: + try: + from pyPgSQL import PgSQL + except ImportError: + sys.stderr.write('error: no suitable database module found\n') + raise SystemExit(1) + +if has_psycopg2: + DBErr = psycopg2.DatabaseError +else: + DBErr = PgSQL.libpq.DatabaseError + + +def check_opts(opts, err_hdlr): + if not opts.postfix: + err_hdlr('missing Postfix database user name') + if not opts.dovecot: + err_hdlr('missing Dovecot database user name') + if opts.askp: + opts.dbpass = getpass.getpass() + + +def get_dbh(database, user, password, host, port): + if has_psycopg2: + return psycopg2.connect(database=database, user=user, + password=password, host=host, port=port) + return PgSQL.connect(user=user, password=password, host=host, + database=database, port=port) + + +def get_optparser(): + descr = 'Set permissions for Dovecot and Postfix in the vmm database.' + usage = 'usage: %prog OPTIONS' + parser = OptionParser(description=descr, usage=usage) + parser.add_option('-a', '--askpass', dest='askp', default=False, + action='store_true', help='Prompt for the database password.') + parser.add_option('-H', '--host', dest='host', metavar='HOST', + default=None, + help='Hostname or IP address of the database server. Leave ' + + 'blank in order to use the default Unix-domain socket.') + parser.add_option('-n', '--name', dest='name', metavar='NAME', + default='mailsys', + help='Specifies the name of the database to connect to. ' + + 'Default: %default') + parser.add_option('-p', '--pass', dest="dbpass", metavar='PASS', + default=None, help='Password for the database connection.') + parser.add_option('-P', '--port', dest='port', metavar='PORT', type='int', + default=5432, + help='Specifies the TCP port or the local Unix-domain socket ' + + 'file extension on which the server is listening for ' + + 'connections. Default: %default') + parser.add_option('-U', '--user', dest='user', metavar='USER', + default=getpass.getuser(), + help='Connect to the database as the user USER instead of the ' + + 'default: %default') + parser.add_option('-D', '--dovecot', dest='dovecot', metavar='USER', + default='dovecot', + help='Database user name of the Dovecot database user. Default: ' + + '%default') + parser.add_option('-M', '--postfix', dest='postfix', metavar='USER', + default='postfix', + help='Database user name of the Postfix (MTA) database user. ' + + 'Default: %default') + return parser + + +def set_permissions(dbh, dc_vers, dovecot, postfix): + dc_rw = ('userquota_11', 'userquota')[dc_vers == 12] + dbc = dbh.cursor() + dbc.execute('GRANT SELECT ON domain_data, domain_name, mailboxformat, ' + 'maillocation, quotalimit, service_set, users TO %s' % dovecot) + dbc.execute('GRANT SELECT, INSERT, UPDATE, DELETE ON %s TO %s' % + (dc_rw, dovecot)) + dbc.execute('GRANT SELECT ON alias, catchall, domain_data, domain_name, ' + 'maillocation, postfix_gid, relocated, transport, users TO %s' + % postfix) + dbc.close() + + +def set_permissions84(dbh, dc_vers, dovecot, postfix): + dc_rw_tbls = ('userquota_11', 'userquota')[dc_vers == 12] + dc_ro_tbls = 'mailboxformat, maillocation, service_set, quotalimit' + pf_ro_tbls = 'alias, catchall, postfix_gid, relocated, transport' + db = dict(dovecot=dovecot, postfix=postfix) + db['dovecot_tbls'] = { + 'domain_data': 'domaindir, gid, qid, ssid', + 'domain_name': 'domainname, gid', + 'users': 'gid, local_part, mid, passwd, qid, ssid, uid', + } + db['postfix_tbls'] = { + 'domain_data': 'domaindir, gid, tid', + 'domain_name': 'domainname, gid', + 'maillocation': 'directory, mid', + 'users': 'gid, local_part, mid, tid, uid', + } + dbc = dbh.cursor() + dbc.execute('GRANT SELECT, INSERT, UPDATE, DELETE ON %s TO %s' % + (dc_rw_tbls, db['dovecot'])) + dbc.execute('GRANT SELECT ON %s TO %s' % (dc_ro_tbls, db['dovecot'])) + dbc.execute('GRANT SELECT ON %s TO %s' % (pf_ro_tbls, db['postfix'])) + for table, columns in db['dovecot_tbls'].iteritems(): + dbc.execute('GRANT SELECT (%s) ON %s TO %s' % (columns, table, + db['dovecot'])) + for table, columns in db['postfix_tbls'].iteritems(): + dbc.execute('GRANT SELECT (%s) ON %s TO %s' % (columns, table, + db['postfix'])) + dbc.close() + + +def set_versions(dbh, versions): + dbc = dbh.cursor() + if hasattr(dbh, 'server_version'): + versions['pgsql'] = dbh.server_version + else: + try: + dbc.execute("SELECT current_setting('server_version_num')") + versions['pgsql'] = int(dbc.fetchone()[0]) + except DBErr: + versions['pgsql'] = 80199 + dbc.execute("SELECT relname FROM pg_stat_user_tables WHERE relname LIKE " + "'userquota%'") + res = dbc.fetchall() + dbc.close() + tbls = [tbl[0] for tbl in res] + if 'userquota' in tbls: + versions['dovecot'] = 12 + elif 'userquota_11' in tbls: + versions['dovecot'] = 11 + else: + sys.stderr.write('error: no userquota table found\nis "' + dbh.dsn + + '" correct? is the database up to date?\n') + dbh.close() + raise SystemExit(1) + + +if __name__ == '__main__': + optparser = get_optparser() + opts, args = optparser.parse_args() + check_opts(opts, optparser.error) + dbh = get_dbh(opts.name, opts.user, opts.dbpass, opts.host, opts.port) + versions = {} + set_versions(dbh, versions) + if versions['pgsql'] < 80400: + set_permissions(dbh, versions['dovecot'], opts.dovecot, opts.postfix) + else: + set_permissions84(dbh, versions['dovecot'], opts.dovecot, opts.postfix) + dbh.commit() + dbh.close() diff -r c0e1fb1b0145 -r a4aead244f75 pgsql/update_tables_0.4.x-0.5.pgsql --- a/pgsql/update_tables_0.4.x-0.5.pgsql Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,156 +0,0 @@ -SET client_encoding = 'UTF8'; -SET client_min_messages = warning; - -ALTER SEQUENCE domains_gid RENAME TO domain_gid; - - -CREATE TABLE domain_data ( - gid bigint NOT NULL DEFAULT nextval('domain_gid'), - tid bigint NOT NULL DEFAULT 1, - domaindir varchar(40) NOT NULL, - CONSTRAINT pkey_domain_data PRIMARY KEY (gid), - CONSTRAINT fkey_domain_data_tid_transport FOREIGN KEY (tid) - REFERENCES transport (tid) -); - -CREATE TABLE domain_name ( - domainname varchar(255) NOT NULL, - gid bigint NOT NULL, - is_primary boolean NOT NULL, - CONSTRAINT pkey_domain_name PRIMARY KEY (domainname), - CONSTRAINT fkey_domain_name_gid_domain_data FOREIGN KEY (gid) - REFERENCES domain_data (gid) -); - -INSERT INTO domain_data (gid, tid, domaindir) - SELECT gid, tid, domaindir - FROM domains; - -INSERT INTO domain_name (domainname, gid, is_primary) - SELECT domainname, gid, TRUE - FROM domains; - - -ALTER TABLE users DROP CONSTRAINT pkye_users; -ALTER TABLE users ADD CONSTRAINT pkey_users PRIMARY KEY (local_part, gid); -ALTER TABLE users DROP CONSTRAINT fkey_users_gid_domains; -ALTER TABLE users ADD CONSTRAINT fkey_users_gid_domain_data FOREIGN KEY (gid) - REFERENCES domain_data (gid); - -ALTER TABLE alias DROP CONSTRAINT fkey_alias_gid_domains; -ALTER TABLE alias DROP CONSTRAINT pkey_alias; -ALTER TABLE alias ADD CONSTRAINT fkey_alias_gid_domain_data FOREIGN KEY (gid) - REFERENCES domain_data (gid); - -ALTER TABLE relocated DROP CONSTRAINT fkey_relocated_gid_domains; -ALTER TABLE relocated ADD CONSTRAINT fkey_relocated_gid_domain_data - FOREIGN KEY (gid) REFERENCES domain_data (gid); - - -CREATE OR REPLACE VIEW dovecot_password AS - SELECT local_part || '@' || domain_name.domainname AS "user", - passwd AS "password", smtp, pop3, imap, managesieve - FROM users - LEFT JOIN domain_name USING (gid); - -CREATE OR REPLACE VIEW dovecot_user AS - SELECT local_part || '@' || domain_name.domainname AS userid, - uid, gid, domain_data.domaindir || '/' || uid AS home, - '~/' || maillocation.maillocation AS mail - FROM users - LEFT JOIN domain_data USING (gid) - LEFT JOIN domain_name USING (gid) - LEFT JOIN maillocation USING (mid); - -CREATE OR REPLACE VIEW postfix_gid AS - SELECT gid, domainname - FROM domain_name; - -CREATE OR REPLACE VIEW postfix_uid AS - SELECT local_part || '@' || domain_name.domainname AS address, uid - FROM users - LEFT JOIN domain_name USING (gid); - -CREATE OR REPLACE VIEW postfix_maildir AS - SELECT local_part || '@' || domain_name.domainname AS address, - domain_data.domaindir||'/'||uid||'/'||maillocation.maillocation||'/' - AS maildir - FROM users - LEFT JOIN domain_data USING (gid) - LEFT JOIN domain_name USING (gid) - LEFT JOIN maillocation USING (mid); - -CREATE OR REPLACE VIEW postfix_relocated AS - SELECT address || '@' || domain_name.domainname AS address, destination - FROM relocated - LEFT JOIN domain_name USING (gid); - -DROP VIEW postfix_alias; -DROP VIEW vmm_domain_info; -DROP VIEW vmm_alias_count; - -ALTER TABLE alias ALTER address TYPE varchar(64); -ALTER TABLE alias ADD CONSTRAINT pkey_alias - PRIMARY KEY (gid, address, destination); - -CREATE OR REPLACE VIEW postfix_alias AS - SELECT address || '@' || domain_name.domainname AS address, destination, gid - FROM alias - LEFT JOIN domain_name USING (gid); - -CREATE OR REPLACE VIEW postfix_transport AS - SELECT local_part || '@' || domain_name.domainname AS address, - transport.transport - FROM users - LEFT JOIN transport USING (tid) - LEFT JOIN domain_name USING (gid); - -CREATE OR REPLACE VIEW vmm_domain_info AS - SELECT gid, domainname, transport, domaindir, - count(uid) AS accounts, - (SELECT count(DISTINCT address) - FROM alias - WHERE alias.gid = domain_data.gid) AS aliases, - (SELECT count(gid) - FROM relocated - WHERE relocated.gid = domain_data.gid) AS relocated, - (SELECT count(gid) - FROM domain_name - WHERE domain_name.gid = domain_data.gid - AND NOT domain_name.is_primary) AS aliasdomains - FROM domain_data - LEFT JOIN domain_name USING (gid) - LEFT JOIN transport USING (tid) - LEFT JOIN users USING (gid) - WHERE domain_name.is_primary - GROUP BY gid, domainname, transport, domaindir; - - -DROP TABLE domains; - - -CREATE LANGUAGE plpgsql; - -CREATE OR REPLACE FUNCTION domain_primary_trigger() RETURNS TRIGGER AS $$ -DECLARE - primary_count bigint; -BEGIN - SELECT INTO primary_count count(gid) + NEW.is_primary::integer - FROM domain_name - WHERE domain_name.gid = NEW.gid - AND is_primary; - - IF (primary_count > 1) THEN - RAISE EXCEPTION 'There can only be one domain marked as primary.'; - END IF; - - RETURN NEW; -END; -$$ LANGUAGE plpgsql STABLE; - -DROP TRIGGER IF EXISTS primary_count ON domain_name; -CREATE TRIGGER primary_count_ins BEFORE INSERT ON domain_name - FOR EACH ROW EXECUTE PROCEDURE domain_primary_trigger(); - -CREATE TRIGGER primary_count_upd AFTER UPDATE ON domain_name - FOR EACH ROW EXECUTE PROCEDURE domain_primary_trigger(); diff -r c0e1fb1b0145 -r a4aead244f75 pgsql/update_tables_0.5.x-0.6-dovecot-1.2.x.pgsql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pgsql/update_tables_0.5.x-0.6-dovecot-1.2.x.pgsql Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,680 @@ +SET client_encoding = 'UTF8'; +SET client_min_messages = warning; + +-- --- +-- Create the new service_set table and insert all possible combinations +-- -- +CREATE SEQUENCE service_set_id; + +CREATE TABLE service_set ( + ssid bigint NOT NULL DEFAULT nextval('service_set_id'), + smtp boolean NOT NULL DEFAULT TRUE, + pop3 boolean NOT NULL DEFAULT TRUE, + imap boolean NOT NULL DEFAULT TRUE, + sieve boolean NOT NULL DEFAULT TRUE, + CONSTRAINT pkey_service_set PRIMARY KEY (ssid), + CONSTRAINT ukey_service_set UNIQUE (smtp, pop3, imap, sieve) +); + +COPY service_set (smtp, pop3, imap, sieve) FROM stdin; +TRUE TRUE TRUE TRUE +FALSE TRUE TRUE TRUE +TRUE FALSE TRUE TRUE +FALSE FALSE TRUE TRUE +TRUE TRUE FALSE TRUE +FALSE TRUE FALSE TRUE +TRUE FALSE FALSE TRUE +FALSE FALSE FALSE TRUE +TRUE TRUE TRUE FALSE +FALSE TRUE TRUE FALSE +TRUE FALSE TRUE FALSE +FALSE FALSE TRUE FALSE +TRUE TRUE FALSE FALSE +FALSE TRUE FALSE FALSE +TRUE FALSE FALSE FALSE +FALSE FALSE FALSE FALSE +\. + +-- --- +-- Drop the obsolete VIEWs, we've functions now. +-- --- +DROP VIEW dovecot_user; +DROP VIEW dovecot_password; +DROP VIEW postfix_alias; +DROP VIEW postfix_maildir; +DROP VIEW postfix_relocated; +DROP VIEW postfix_transport; +DROP VIEW postfix_uid; +-- the vmm_domain_info view will be restored later +DROP VIEW vmm_domain_info; + +CREATE SEQUENCE mailboxformat_id; +CREATE SEQUENCE quotalimit_id; + +CREATE TABLE mailboxformat ( + fid bigint NOT NULL DEFAULT nextval('mailboxformat_id'), + format varchar(20) NOT NULL, + CONSTRAINT pkey_mailboxformat PRIMARY KEY (fid), + CONSTRAINT ukey_mailboxformat UNIQUE (format) +); +-- Insert supported mailbox formats +INSERT INTO mailboxformat(format) VALUES ('maildir'); +INSERT INTO mailboxformat(format) VALUES ('mdbox'); +INSERT INTO mailboxformat(format) VALUES ('sdbox'); + +-- Adjust maillocation table +ALTER TABLE maillocation DROP CONSTRAINT ukey_maillocation; +ALTER TABLE maillocation RENAME COLUMN maillocation TO directory; +ALTER TABLE maillocation + ADD COLUMN fid bigint NOT NULL DEFAULT 1, + ADD COLUMN extra varchar(1024); +ALTER TABLE maillocation ADD CONSTRAINT fkey_maillocation_fid_mailboxformat + FOREIGN KEY (fid) REFERENCES mailboxformat (fid); + +ALTER TABLE users ALTER COLUMN passwd TYPE varchar(270); + +-- --- +-- Add quota stuff +-- --- +CREATE TABLE quotalimit ( + qid bigint NOT NULL DEFAULT nextval('quotalimit_id'), + bytes bigint NOT NULL, + messages integer NOT NULL DEFAULT 0, + CONSTRAINT pkey_quotalimit PRIMARY KEY (qid), + CONSTRAINT ukey_quotalimit UNIQUE (bytes, messages) +); +-- Insert default (non) quota limit +INSERT INTO quotalimit(bytes, messages) VALUES (0, 0); + +-- Adjust tables (quota) +ALTER TABLE domain_data ADD COLUMN qid bigint NOT NULL DEFAULT 1; +ALTER TABLE domain_data ADD CONSTRAINT fkey_domain_data_qid_quotalimit + FOREIGN KEY (qid) REFERENCES quotalimit (qid); + +ALTER TABLE users ADD COLUMN qid bigint NULL DEFAULT NULL; +ALTER TABLE users ADD CONSTRAINT fkey_users_qid_quotalimit + FOREIGN KEY (qid) REFERENCES quotalimit (qid); + +CREATE TABLE userquota ( + uid bigint NOT NULL, + bytes bigint NOT NULL DEFAULT 0, + messages integer NOT NULL DEFAULT 0, + CONSTRAINT pkey_userquota PRIMARY KEY (uid), + CONSTRAINT fkey_userquota_uid_users FOREIGN KEY (uid) + REFERENCES users (uid) ON DELETE CASCADE +); + +CREATE OR REPLACE FUNCTION merge_userquota() RETURNS TRIGGER AS $$ +BEGIN + IF NEW.messages < 0 OR NEW.messages IS NULL THEN + IF NEW.messages IS NULL THEN + NEW.messages = 0; + ELSE + NEW.messages = -NEW.messages; + END IF; + RETURN NEW; + END IF; + LOOP + UPDATE userquota + SET bytes = bytes + NEW.bytes, messages = messages + NEW.messages + WHERE uid = NEW.uid; + IF found THEN + RETURN NULL; + END IF; + BEGIN + IF NEW.messages = 0 THEN + INSERT INTO userquota VALUES (NEW.uid, NEW.bytes, NULL); + ELSE + INSERT INTO userquota VALUES (NEW.uid, NEW.bytes, -NEW.messages); + END IF; + RETURN NULL; + EXCEPTION + WHEN unique_violation THEN + -- do nothing, and loop to try the UPDATE again + WHEN foreign_key_violation THEN + -- break the loop: a non matching uid means no such user + RETURN NULL; + END; + END LOOP; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER mergeuserquota BEFORE INSERT ON userquota + FOR EACH ROW EXECUTE PROCEDURE merge_userquota(); + +-- Adjust tables (services) +ALTER TABLE domain_data ADD COLUMN ssid bigint NOT NULL DEFAULT 1; +ALTER TABLE domain_data ADD CONSTRAINT fkey_domain_data_ssid_service_set + FOREIGN KEY (ssid) REFERENCES service_set (ssid); + +ALTER TABLE users ADD COLUMN ssid bigint NULL DEFAULT NULL; +-- save current service sets +UPDATE users u + SET ssid = ss.ssid + FROM service_set ss + WHERE ss.smtp = u.smtp + AND ss.pop3 = u.pop3 + AND ss.imap = u.imap + AND ss.sieve = u.sieve; + +ALTER TABLE users DROP COLUMN smtp; +ALTER TABLE users DROP COLUMN pop3; +ALTER TABLE users DROP COLUMN imap; +ALTER TABLE users DROP COLUMN sieve; +ALTER TABLE users ADD CONSTRAINT fkey_users_ssid_service_set + FOREIGN KEY (ssid) REFERENCES service_set (ssid); + +-- --- +-- Catchall +-- --- + +CREATE TABLE catchall ( + gid bigint NOT NULL, + destination varchar(320) NOT NULL, + CONSTRAINT pkey_catchall PRIMARY KEY (gid, destination), + CONSTRAINT fkey_catchall_gid_domain_data FOREIGN KEY (gid) + REFERENCES domain_data (gid) +); + +-- --- +-- Quota/Service/Transport inheritance +-- --- +ALTER TABLE users ALTER COLUMN tid DROP NOT NULL; +ALTER TABLE users ALTER COLUMN tid SET DEFAULT NULL; +-- The qid and ssid columns have already been defined accordingly above. +-- The rest of the logic will take place in the functions. + +-- While qid and ssid are new and it's perfectly okay for existing users to +-- get NULL values (i.e. inherit from the domain's default), tid existed in +-- vmm 0.5.x. A sensible way forward seems thus to NULL all user records' tid +-- fields where the tid duplicates the value stored in the domain's record. +UPDATE users + SET tid = NULL + WHERE tid = (SELECT tid + FROM domain_data + WHERE domain_data.gid = users.gid); + +-- --- +-- Account/domain notes +-- --- + +ALTER TABLE users ADD COLUMN note text NULL DEFAULT NULL; +ALTER TABLE domain_data ADD COLUMN note text NULL DEFAULT NULL; + +-- --- +-- Restore view +-- --- +CREATE VIEW vmm_domain_info AS + SELECT gid, count(uid) AS accounts, + (SELECT count(DISTINCT address) + FROM alias + WHERE alias.gid = domain_data.gid) AS aliases, + (SELECT count(gid) + FROM relocated + WHERE relocated.gid = domain_data.gid) AS relocated, + (SELECT count(gid) + FROM domain_name + WHERE domain_name.gid = domain_data.gid + AND NOT domain_name.is_primary) AS aliasdomains, + (SELECT count(gid) + FROM catchall + WHERE catchall.gid = domain_data.gid) AS catchall + FROM domain_data + LEFT JOIN domain_name USING (gid) + LEFT JOIN users USING (gid) + WHERE domain_name.is_primary + GROUP BY gid; + +-- --- +-- Drop all known v0.5 types (the dirty way) +-- --- +DROP TYPE address_maildir CASCADE; +DROP TYPE dovecotpassword CASCADE; +DROP TYPE dovecotuser CASCADE; +DROP TYPE recipient_destination CASCADE; +DROP TYPE recipient_transport CASCADE; +DROP TYPE recipient_uid CASCADE; +DROP TYPE sender_login CASCADE; + +-- ######################## TYPEs ########################################### -- + +-- --- +-- Data type for function postfix_virtual_mailbox(varchar, varchar) +-- --- +CREATE TYPE address_maildir AS ( + address varchar(320), + maildir text +); +-- --- +-- Data type for function dovecotpassword(varchar, varchar) +-- --- +CREATE TYPE dovecotpassword AS ( + userid varchar(320), + password varchar(270), + smtp boolean, + pop3 boolean, + imap boolean, + sieve boolean +); +-- --- +-- Data type for function dovecotquotauser(varchar, varchar) +-- --- +CREATE TYPE dovecotquotauser AS ( + userid varchar(320), + uid bigint, + gid bigint, + home text, + mail text, + quota_rule text +); +-- --- +-- Data type for function dovecotuser(varchar, varchar) +-- --- +CREATE TYPE dovecotuser AS ( + userid varchar(320), + uid bigint, + gid bigint, + home text, + mail text +); +-- --- +-- Data type for functions: postfix_relocated_map(varchar, varchar) +-- postfix_virtual_alias_map(varchar, varchar) +-- --- +CREATE TYPE recipient_destination AS ( + recipient varchar(320), + destination text +); +-- --- +-- Data type for function postfix_transport_map(varchar, varchar) +-- --- +CREATE TYPE recipient_transport AS ( + recipient varchar(320), + transport text +); +-- --- +-- Data type for function postfix_virtual_uid_map(varchar, varchar) +-- --- +CREATE TYPE recipient_uid AS ( + recipient varchar(320), + uid bigint +); +-- --- +-- Data type for function postfix_smtpd_sender_login_map(varchar, varchar) +-- --- +CREATE TYPE sender_login AS ( + sender varchar(320), + login text +); + +-- ######################## FUNCTIONs ####################################### -- + +-- --- +-- Parameters (from login name [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: dovecotpassword records +-- --- +CREATE OR REPLACE FUNCTION dovecotpassword( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotpassword +AS $$ + DECLARE + record dovecotpassword; + userid varchar(320) := localpart || '@' || the_domain; + BEGIN + FOR record IN + SELECT userid, passwd, smtp, pop3, imap, sieve + FROM users, service_set, domain_data + WHERE users.gid = (SELECT gid + FROM domain_name + WHERE domainname = the_domain) + AND local_part = localpart + AND users.gid = domain_data.gid + AND CASE WHEN + users.ssid IS NOT NULL + THEN + service_set.ssid = users.ssid + ELSE + service_set.ssid = domain_data.ssid + END + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Nearly the same as function dovecotuser below. It returns additionally the +-- field quota_rule. +-- --- +CREATE OR REPLACE FUNCTION dovecotquotauser( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotquotauser +AS $$ + DECLARE + record dovecotquotauser; + userid varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT userid, uid, did, domaindir || '/' || uid AS home, + format || ':~/' || directory AS mail, '*:bytes=' || + bytes || ':messages=' || messages AS quota_rule + FROM users, domain_data, mailboxformat, maillocation, quotalimit + WHERE users.gid = did + AND users.local_part = localpart + AND maillocation.mid = users.mid + AND mailboxformat.fid = maillocation.fid + AND domain_data.gid = did + AND CASE WHEN + users.qid IS NOT NULL + THEN + quotalimit.qid = users.qid + ELSE + quotalimit.qid = domain_data.qid + END + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from login name [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: dovecotuser records +-- --- +CREATE OR REPLACE FUNCTION dovecotuser( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotuser +AS $$ + DECLARE + record dovecotuser; + userid varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT userid, uid, did, domaindir || '/' || uid AS home, + format || ':~/' || directory AS mail + FROM users, domain_data, mailboxformat, maillocation + WHERE users.gid = did + AND users.local_part = localpart + AND maillocation.mid = users.mid + AND mailboxformat.fid = maillocation.fid + AND domain_data.gid = did + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_destination records +-- --- +CREATE OR REPLACE FUNCTION postfix_relocated_map( + IN localpart varchar, IN the_domain varchar) + RETURNS SETOF recipient_destination +AS $$ + DECLARE + record recipient_destination; + recipient varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT recipient, destination + FROM relocated + WHERE gid = did + AND address = localpart + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from _sender_ address (MAIL FROM) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: SASL _login_ names that own _sender_ addresses (MAIL FROM): +-- set of sender_login records. +-- --- +CREATE OR REPLACE FUNCTION postfix_smtpd_sender_login_map( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF sender_login +AS $$ + DECLARE + rec sender_login; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + sender varchar(320) := localpart || '@' || the_domain; + BEGIN + -- Get all addresses for 'localpart' in the primary and aliased domains + FOR rec IN + SELECT sender, local_part || '@' || domainname + FROM domain_name, users + WHERE domain_name.gid = did + AND users.gid = did + AND users.local_part = localpart + LOOP + RETURN NEXT rec; + END LOOP; + IF NOT FOUND THEN + -- Loop over the alias addresses for localpart@the_domain + FOR rec IN + SELECT DISTINCT sender, destination + FROM alias + WHERE alias.gid = did + AND alias.address = localpart + LOOP + RETURN NEXT rec; + END LOOP; + END IF; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_transport records +-- --- +CREATE OR REPLACE FUNCTION postfix_transport_map( + IN localpart varchar, IN the_domain varchar) + RETURNS SETOF recipient_transport +AS $$ + DECLARE + record recipient_transport; + recipient varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname = the_domain); + transport_id bigint; + BEGIN + IF did IS NULL THEN + RETURN; + END IF; + + SELECT tid INTO transport_id + FROM users + WHERE gid = did AND local_part = localpart; + + IF transport_id IS NULL THEN + SELECT tid INTO STRICT transport_id + FROM domain_data + WHERE gid = did; + END IF; + + FOR record IN + SELECT recipient, transport + FROM transport + WHERE tid = transport_id + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_destination records +-- --- +CREATE OR REPLACE FUNCTION _interpolate_destination( + IN destination varchar, localpart varchar, IN the_domain varchar) + RETURNS varchar +AS $$ + DECLARE + result varchar(320); + BEGIN + IF position('%' in destination) = 0 THEN + RETURN destination; + END IF; + result := replace(destination, '%n', localpart); + result := replace(result, '%d', the_domain); + result := replace(result, '%=', localpart || '=' || the_domain); + RETURN result; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; + +CREATE OR REPLACE FUNCTION postfix_virtual_alias_map( + IN localpart varchar, IN the_domain varchar) + RETURNS SETOF recipient_destination +AS $$ + DECLARE + recordc recipient_destination; + record recipient_destination; + catchall_cursor refcursor; + recipient varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT recipient, + _interpolate_destination(destination, localpart, the_domain) + FROM alias + WHERE gid = did + AND address = localpart + LOOP + RETURN NEXT record; + END LOOP; + + IF NOT FOUND THEN + -- There is no matching virtual_alias. If there are no catchall + -- records for this domain, we can just return NULL since Postfix + -- will then later consult mailboxes/relocated itself. But if + -- there is a catchall destination, then it would take precedence + -- over mailboxes/relocated, which is not what we want. Therefore, + -- we must first find out if the query is for an existing mailbox + -- or relocated entry and return the identity mapping if that is + -- the case + OPEN catchall_cursor FOR + SELECT recipient, + _interpolate_destination(destination, localpart, the_domain) + FROM catchall + WHERE gid = did; + FETCH NEXT FROM catchall_cursor INTO recordc; + + IF recordc IS NOT NULL THEN + -- Since there are catchall records for this domain + -- check the mailbox and relocated records and return identity + -- if a matching record exists. + FOR record IN + SELECT recipient, recipient as destination + FROM users + WHERE gid = did + AND local_part = localpart + UNION SELECT recipient, recipient as destination + FROM relocated + WHERE gid = did + AND address = localpart + LOOP + RETURN NEXT record; + END LOOP; + + IF NOT FOUND THEN + -- There were no records found for mailboxes/relocated, + -- so now we can actually iterate the cursor and populate + -- the return set + LOOP + RETURN NEXT recordc; + FETCH NEXT FROM catchall_cursor INTO recordc; + EXIT WHEN recordc IS NULL; + END LOOP; + END IF; + END IF; + CLOSE catchall_cursor; + END IF; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: address_maildir records +-- --- +CREATE OR REPLACE FUNCTION postfix_virtual_mailbox_map( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF address_maildir +AS $$ + DECLARE + rec address_maildir; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + address varchar(320) := localpart || '@' || the_domain; + BEGIN + FOR rec IN + SELECT address, domaindir||'/'||users.uid||'/'||directory||'/' + FROM domain_data, users, maillocation + WHERE domain_data.gid = did + AND users.gid = did + AND users.local_part = localpart + AND maillocation.mid = users.mid + LOOP + RETURN NEXT rec; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_uid records +-- --- +CREATE OR REPLACE FUNCTION postfix_virtual_uid_map( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF recipient_uid +AS $$ + DECLARE + record recipient_uid; + recipient varchar(320) := localpart || '@' || the_domain; + BEGIN + FOR record IN + SELECT recipient, uid + FROM users + WHERE gid = (SELECT gid + FROM domain_name + WHERE domainname = the_domain) + AND local_part = localpart + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; diff -r c0e1fb1b0145 -r a4aead244f75 pgsql/update_tables_0.5.x-0.6.pgsql --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pgsql/update_tables_0.5.x-0.6.pgsql Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,659 @@ +SET client_encoding = 'UTF8'; +SET client_min_messages = warning; + +-- --- +-- Create the new service_set table and insert all possible combinations +-- -- +CREATE SEQUENCE service_set_id; + +CREATE TABLE service_set ( + ssid bigint NOT NULL DEFAULT nextval('service_set_id'), + smtp boolean NOT NULL DEFAULT TRUE, + pop3 boolean NOT NULL DEFAULT TRUE, + imap boolean NOT NULL DEFAULT TRUE, + managesieve boolean NOT NULL DEFAULT TRUE, + CONSTRAINT pkey_service_set PRIMARY KEY (ssid), + CONSTRAINT ukey_service_set UNIQUE (smtp, pop3, imap, managesieve) +); + +COPY service_set (smtp, pop3, imap, managesieve) FROM stdin; +TRUE TRUE TRUE TRUE +FALSE TRUE TRUE TRUE +TRUE FALSE TRUE TRUE +FALSE FALSE TRUE TRUE +TRUE TRUE FALSE TRUE +FALSE TRUE FALSE TRUE +TRUE FALSE FALSE TRUE +FALSE FALSE FALSE TRUE +TRUE TRUE TRUE FALSE +FALSE TRUE TRUE FALSE +TRUE FALSE TRUE FALSE +FALSE FALSE TRUE FALSE +TRUE TRUE FALSE FALSE +FALSE TRUE FALSE FALSE +TRUE FALSE FALSE FALSE +FALSE FALSE FALSE FALSE +\. + +-- --- +-- Drop the obsolete VIEWs, we've functions now. +-- --- +DROP VIEW dovecot_user; +DROP VIEW dovecot_password; +DROP VIEW postfix_alias; +DROP VIEW postfix_maildir; +DROP VIEW postfix_relocated; +DROP VIEW postfix_transport; +DROP VIEW postfix_uid; +-- the vmm_domain_info view will be restored later +DROP VIEW vmm_domain_info; + +CREATE SEQUENCE mailboxformat_id; +CREATE SEQUENCE quotalimit_id; + +CREATE TABLE mailboxformat ( + fid bigint NOT NULL DEFAULT nextval('mailboxformat_id'), + format varchar(20) NOT NULL, + CONSTRAINT pkey_mailboxformat PRIMARY KEY (fid), + CONSTRAINT ukey_mailboxformat UNIQUE (format) +); +-- Insert supported mailbox formats +INSERT INTO mailboxformat(format) VALUES ('maildir'); +INSERT INTO mailboxformat(format) VALUES ('mdbox'); +INSERT INTO mailboxformat(format) VALUES ('sdbox'); + +-- Adjust maillocation table +ALTER TABLE maillocation DROP CONSTRAINT ukey_maillocation; +ALTER TABLE maillocation RENAME COLUMN maillocation TO directory; +ALTER TABLE maillocation + ADD COLUMN fid bigint NOT NULL DEFAULT 1, + ADD COLUMN extra varchar(1024); +ALTER TABLE maillocation ADD CONSTRAINT fkey_maillocation_fid_mailboxformat + FOREIGN KEY (fid) REFERENCES mailboxformat (fid); + +ALTER TABLE users ALTER COLUMN passwd TYPE varchar(270); + +-- --- +-- Add quota stuff +-- --- +CREATE TABLE quotalimit ( + qid bigint NOT NULL DEFAULT nextval('quotalimit_id'), + bytes bigint NOT NULL, + messages integer NOT NULL DEFAULT 0, + CONSTRAINT pkey_quotalimit PRIMARY KEY (qid), + CONSTRAINT ukey_quotalimit UNIQUE (bytes, messages) +); +-- Insert default (non) quota limit +INSERT INTO quotalimit(bytes, messages) VALUES (0, 0); + +-- Adjust tables (quota) +ALTER TABLE domain_data ADD COLUMN qid bigint NOT NULL DEFAULT 1; +ALTER TABLE domain_data ADD CONSTRAINT fkey_domain_data_qid_quotalimit + FOREIGN KEY (qid) REFERENCES quotalimit (qid); + +ALTER TABLE users ADD COLUMN qid bigint NULL DEFAULT NULL; +ALTER TABLE users ADD CONSTRAINT fkey_users_qid_quotalimit + FOREIGN KEY (qid) REFERENCES quotalimit (qid); + +CREATE TABLE userquota_11 ( + uid bigint NOT NULL, + path varchar(16) NOT NULL, + current bigint NOT NULL DEFAULT 0, + CONSTRAINT pkey_userquota_11 PRIMARY KEY (uid, path), + CONSTRAINT fkey_userquota_11_uid_users FOREIGN KEY (uid) + REFERENCES users (uid) ON DELETE CASCADE +); + +CREATE OR REPLACE FUNCTION merge_userquota_11() RETURNS TRIGGER AS $$ +BEGIN + UPDATE userquota_11 + SET current = current + NEW.current + WHERE uid = NEW.uid AND path = NEW.path; + IF found THEN + RETURN NULL; + ELSE + RETURN NEW; + END IF; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER mergeuserquota_11 BEFORE INSERT ON userquota_11 + FOR EACH ROW EXECUTE PROCEDURE merge_userquota_11(); + +-- Adjust tables (services) +ALTER TABLE domain_data ADD COLUMN ssid bigint NOT NULL DEFAULT 1; +ALTER TABLE domain_data ADD CONSTRAINT fkey_domain_data_ssid_service_set + FOREIGN KEY (ssid) REFERENCES service_set (ssid); + +ALTER TABLE users ADD COLUMN ssid bigint NULL DEFAULT NULL; +-- save current service sets +UPDATE users u + SET ssid = ss.ssid + FROM service_set ss + WHERE ss.smtp = u.smtp + AND ss.pop3 = u.pop3 + AND ss.imap = u.imap + AND ss.managesieve = u.managesieve; + +ALTER TABLE users DROP COLUMN smtp; +ALTER TABLE users DROP COLUMN pop3; +ALTER TABLE users DROP COLUMN imap; +ALTER TABLE users DROP COLUMN managesieve; +ALTER TABLE users ADD CONSTRAINT fkey_users_ssid_service_set + FOREIGN KEY (ssid) REFERENCES service_set (ssid); + +-- --- +-- Catchall +-- --- +-- Quota/Service/Transport inheritance +-- --- +ALTER TABLE users ALTER COLUMN tid DROP NOT NULL; +ALTER TABLE users ALTER COLUMN tid SET DEFAULT NULL; +-- The qid and ssid columns have already been defined accordingly above. +-- The rest of the logic will take place in the functions. + +-- While qid and ssid are new and it's perfectly okay for existing users to +-- get NULL values (i.e. inherit from the domain's default), tid existed in +-- vmm 0.5.x. A sensible way forward seems thus to NULL all user records' tid +-- fields where the tid duplicates the value stored in the domain's record. +UPDATE users + SET tid = NULL + WHERE tid = (SELECT tid + FROM domain_data + WHERE domain_data.gid = users.gid); + +-- --- + +CREATE TABLE catchall ( + gid bigint NOT NULL, + destination varchar(320) NOT NULL, + CONSTRAINT pkey_catchall PRIMARY KEY (gid, destination), + CONSTRAINT fkey_catchall_gid_domain_data FOREIGN KEY (gid) + REFERENCES domain_data (gid) +); + +-- --- +-- Account/domain notes +-- --- + +ALTER TABLE users ADD COLUMN note text NULL DEFAULT NULL; +ALTER TABLE domain_data ADD COLUMN note text NULL DEFAULT NULL; + +-- --- +-- Restore view +-- --- +CREATE VIEW vmm_domain_info AS + SELECT gid, count(uid) AS accounts, + (SELECT count(DISTINCT address) + FROM alias + WHERE alias.gid = domain_data.gid) AS aliases, + (SELECT count(gid) + FROM relocated + WHERE relocated.gid = domain_data.gid) AS relocated, + (SELECT count(gid) + FROM domain_name + WHERE domain_name.gid = domain_data.gid + AND NOT domain_name.is_primary) AS aliasdomains, + (SELECT count(gid) + FROM catchall + WHERE catchall.gid = domain_data.gid) AS catchall + FROM domain_data + LEFT JOIN domain_name USING (gid) + LEFT JOIN users USING (gid) + WHERE domain_name.is_primary + GROUP BY gid; + +-- --- +-- Drop all known v0.5 types (the dirty way) +-- --- +DROP TYPE address_maildir CASCADE; +DROP TYPE dovecotpassword CASCADE; +DROP TYPE dovecotuser CASCADE; +DROP TYPE recipient_destination CASCADE; +DROP TYPE recipient_transport CASCADE; +DROP TYPE recipient_uid CASCADE; +DROP TYPE sender_login CASCADE; + +-- ######################## TYPEs ########################################### -- + +-- --- +-- Data type for function postfix_virtual_mailbox(varchar, varchar) +-- --- +CREATE TYPE address_maildir AS ( + address varchar(320), + maildir text +); +-- --- +-- Data type for function dovecotpassword(varchar, varchar) +-- --- +CREATE TYPE dovecotpassword AS ( + userid varchar(320), + password varchar(270), + smtp boolean, + pop3 boolean, + imap boolean, + managesieve boolean +); +-- --- +-- Data type for function dovecotquotauser(varchar, varchar) +-- --- +CREATE TYPE dovecotquotauser AS ( + userid varchar(320), + uid bigint, + gid bigint, + home text, + mail text, + quota_rule text +); +-- --- +-- Data type for function dovecotuser(varchar, varchar) +-- --- +CREATE TYPE dovecotuser AS ( + userid varchar(320), + uid bigint, + gid bigint, + home text, + mail text +); +-- --- +-- Data type for functions: postfix_relocated_map(varchar, varchar) +-- postfix_virtual_alias_map(varchar, varchar) +-- --- +CREATE TYPE recipient_destination AS ( + recipient varchar(320), + destination text +); +-- --- +-- Data type for function postfix_transport_map(varchar, varchar) +-- --- +CREATE TYPE recipient_transport AS ( + recipient varchar(320), + transport text +); +-- --- +-- Data type for function postfix_virtual_uid_map(varchar, varchar) +-- --- +CREATE TYPE recipient_uid AS ( + recipient varchar(320), + uid bigint +); +-- --- +-- Data type for function postfix_smtpd_sender_login_map(varchar, varchar) +-- --- +CREATE TYPE sender_login AS ( + sender varchar(320), + login text +); + +-- ######################## FUNCTIONs ####################################### -- + +-- --- +-- Parameters (from login name [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: dovecotpassword records +-- --- +CREATE OR REPLACE FUNCTION dovecotpassword( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotpassword +AS $$ + DECLARE + record dovecotpassword; + userid varchar(320) := localpart || '@' || the_domain; + BEGIN + FOR record IN + SELECT userid, passwd, smtp, pop3, imap, managesieve + FROM users, service_set, domain_data + WHERE users.gid = (SELECT gid + FROM domain_name + WHERE domainname = the_domain) + AND local_part = localpart + AND service_set.ssid = users.ssid + AND users.gid = domain_data.gid + AND CASE WHEN + users.ssid IS NOT NULL + THEN + service_set.ssid = users.ssid + ELSE + service_set.ssid = domain_data.ssid + END + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Nearly the same as function dovecotuser below. It returns additionally the +-- field quota_rule. +-- --- +CREATE OR REPLACE FUNCTION dovecotquotauser( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotquotauser +AS $$ + DECLARE + record dovecotquotauser; + userid varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT userid, uid, did, domaindir || '/' || uid AS home, + format || ':~/' || directory AS mail, '*:bytes=' || + bytes || ':messages=' || messages AS quota_rule + FROM users, domain_data, mailboxformat, maillocation, quotalimit + WHERE users.gid = did + AND users.local_part = localpart + AND maillocation.mid = users.mid + AND mailboxformat.fid = maillocation.fid + AND domain_data.gid = did + AND CASE WHEN + users.qid IS NOT NULL + THEN + quotalimit.qid = users.qid + ELSE + quotalimit.qid = domain_data.qid + END + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from login name [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: dovecotuser records +-- --- +CREATE OR REPLACE FUNCTION dovecotuser( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotuser +AS $$ + DECLARE + record dovecotuser; + userid varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT userid, uid, did, domaindir || '/' || uid AS home, + format || ':~/' || directory AS mail + FROM users, domain_data, mailboxformat, maillocation + WHERE users.gid = did + AND users.local_part = localpart + AND maillocation.mid = users.mid + AND mailboxformat.fid = maillocation.fid + AND domain_data.gid = did + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_destination records +-- --- +CREATE OR REPLACE FUNCTION postfix_relocated_map( + IN localpart varchar, IN the_domain varchar) + RETURNS SETOF recipient_destination +AS $$ + DECLARE + record recipient_destination; + recipient varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT recipient, destination + FROM relocated + WHERE gid = did + AND address = localpart + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from _sender_ address (MAIL FROM) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: SASL _login_ names that own _sender_ addresses (MAIL FROM): +-- set of sender_login records. +-- --- +CREATE OR REPLACE FUNCTION postfix_smtpd_sender_login_map( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF sender_login +AS $$ + DECLARE + rec sender_login; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + sender varchar(320) := localpart || '@' || the_domain; + BEGIN + -- Get all addresses for 'localpart' in the primary and aliased domains + FOR rec IN + SELECT sender, local_part || '@' || domainname + FROM domain_name, users + WHERE domain_name.gid = did + AND users.gid = did + AND users.local_part = localpart + LOOP + RETURN NEXT rec; + END LOOP; + IF NOT FOUND THEN + -- Loop over the alias addresses for localpart@the_domain + FOR rec IN + SELECT DISTINCT sender, destination + FROM alias + WHERE alias.gid = did + AND alias.address = localpart + LOOP + RETURN NEXT rec; + END LOOP; + END IF; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_transport records +-- --- +CREATE OR REPLACE FUNCTION postfix_transport_map( + IN localpart varchar, IN the_domain varchar) + RETURNS SETOF recipient_transport +AS $$ + DECLARE + record recipient_transport; + recipient varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname = the_domain); + transport_id bigint; + BEGIN + IF did IS NULL THEN + RETURN; + END IF; + + SELECT tid INTO transport_id + FROM users + WHERE gid = did AND local_part = localpart; + + IF transport_id IS NULL THEN + SELECT tid INTO STRICT transport_id + FROM domain_data + WHERE gid = did; + END IF; + + FOR record IN + SELECT recipient, transport + FROM transport + WHERE tid = transport_id + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_destination records +-- --- +CREATE OR REPLACE FUNCTION _interpolate_destination( + IN destination varchar, localpart varchar, IN the_domain varchar) + RETURNS varchar +AS $$ + DECLARE + result varchar(320); + BEGIN + IF position('%' in destination) = 0 THEN + RETURN destination; + END IF; + result := replace(destination, '%n', localpart); + result := replace(result, '%d', the_domain); + result := replace(result, '%=', localpart || '=' || the_domain); + RETURN result; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; + +CREATE OR REPLACE FUNCTION postfix_virtual_alias_map( + IN localpart varchar, IN the_domain varchar) + RETURNS SETOF recipient_destination +AS $$ + DECLARE + recordc recipient_destination; + record recipient_destination; + catchall_cursor refcursor; + recipient varchar(320) := localpart || '@' || the_domain; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + BEGIN + FOR record IN + SELECT recipient, + _interpolate_destination(destination, localpart, the_domain) + FROM alias + WHERE gid = did + AND address = localpart + LOOP + RETURN NEXT record; + END LOOP; + + IF NOT FOUND THEN + -- There is no matching virtual_alias. If there are no catchall + -- records for this domain, we can just return NULL since Postfix + -- will then later consult mailboxes/relocated itself. But if + -- there is a catchall destination, then it would take precedence + -- over mailboxes/relocated, which is not what we want. Therefore, + -- we must first find out if the query is for an existing mailbox + -- or relocated entry and return the identity mapping if that is + -- the case + OPEN catchall_cursor FOR + SELECT recipient, + _interpolate_destination(destination, localpart, the_domain) + FROM catchall + WHERE gid = did; + FETCH NEXT FROM catchall_cursor INTO recordc; + + IF recordc IS NOT NULL THEN + -- Since there are catchall records for this domain + -- check the mailbox and relocated records and return identity + -- if a matching record exists. + FOR record IN + SELECT recipient, recipient as destination + FROM users + WHERE gid = did + AND local_part = localpart + UNION SELECT recipient, recipient as destination + FROM relocated + WHERE gid = did + AND address = localpart + LOOP + RETURN NEXT record; + END LOOP; + + IF NOT FOUND THEN + -- There were no records found for mailboxes/relocated, + -- so now we can actually iterate the cursor and populate + -- the return set + LOOP + RETURN NEXT recordc; + FETCH NEXT FROM catchall_cursor INTO recordc; + EXIT WHEN recordc IS NULL; + END LOOP; + END IF; + END IF; + CLOSE catchall_cursor; + END IF; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: address_maildir records +-- --- +CREATE OR REPLACE FUNCTION postfix_virtual_mailbox_map( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF address_maildir +AS $$ + DECLARE + rec address_maildir; + did bigint := (SELECT gid FROM domain_name WHERE domainname=the_domain); + address varchar(320) := localpart || '@' || the_domain; + BEGIN + FOR rec IN + SELECT address, domaindir||'/'||users.uid||'/'||directory||'/' + FROM domain_data, users, maillocation + WHERE domain_data.gid = did + AND users.gid = did + AND users.local_part = localpart + AND maillocation.mid = users.mid + LOOP + RETURN NEXT rec; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; +-- --- +-- Parameters (from recipients address (MAIL TO) [localpart@the_domain]): +-- varchar localpart +-- varchar the_domain +-- Returns: recipient_uid records +-- --- +CREATE OR REPLACE FUNCTION postfix_virtual_uid_map( + IN localpart varchar, IN the_domain varchar) RETURNS SETOF recipient_uid +AS $$ + DECLARE + record recipient_uid; + recipient varchar(320) := localpart || '@' || the_domain; + BEGIN + FOR record IN + SELECT recipient, uid + FROM users + WHERE gid = (SELECT gid + FROM domain_name + WHERE domainname = the_domain) + AND local_part = localpart + LOOP + RETURN NEXT record; + END LOOP; + RETURN; + END; +$$ LANGUAGE plpgsql STABLE +RETURNS NULL ON NULL INPUT +EXTERNAL SECURITY INVOKER; diff -r c0e1fb1b0145 -r a4aead244f75 pgsql/update_tables_0.5.x_for_dovecot-1.2.x.pgsql --- a/pgsql/update_tables_0.5.x_for_dovecot-1.2.x.pgsql Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ --- --- --- with Dovecot v1.2.x the service managesieve was renamed to sieve --- --- -ALTER TABLE users RENAME managesieve TO sieve; - -DROP VIEW dovecot_password; -CREATE OR REPLACE VIEW dovecot_password AS - SELECT local_part || '@' || domain_name.domainname AS "user", - passwd AS "password", smtp, pop3, imap, sieve - FROM users - LEFT JOIN domain_name USING (gid); - diff -r c0e1fb1b0145 -r a4aead244f75 pgsql/update_types_and_functions_0.5.x_for_dovecot-1.2.x.pgsql --- a/pgsql/update_types_and_functions_0.5.x_for_dovecot-1.2.x.pgsql Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,51 +0,0 @@ --- --- --- Clean out the old stuff --- --- -DROP TYPE dovecotpassword CASCADE; - --- --- --- Data type for function dovecotpassword(varchar, varchar) --- --- -CREATE TYPE dovecotpassword AS ( - userid varchar(320), - password varchar(74), - smtp boolean, - pop3 boolean, - imap boolean, - sieve boolean -); - --- --- --- Parameters (from login name [localpart@the_domain]): --- varchar localpart --- varchar the_domain --- Returns: dovecotpassword records --- --- Required access privileges for your dovecot database user: --- GRANT SELECT ON users, domain_name TO dovecot; --- --- For more details see http://wiki.dovecot.org/AuthDatabase/SQL --- --- -CREATE OR REPLACE FUNCTION dovecotpassword( - IN localpart varchar, IN the_domain varchar) RETURNS SETOF dovecotpassword -AS $$ - DECLARE - record dovecotpassword; - userid varchar(320) := localpart || '@' || the_domain; - BEGIN - FOR record IN - SELECT userid, passwd, smtp, pop3, imap, sieve - FROM users - WHERE gid = (SELECT gid - FROM domain_name - WHERE domainname = the_domain) - AND local_part = localpart - LOOP - RETURN NEXT record; - END LOOP; - RETURN; - END; -$$ LANGUAGE plpgsql STABLE -RETURNS NULL ON NULL INPUT -EXTERNAL SECURITY INVOKER; - diff -r c0e1fb1b0145 -r a4aead244f75 po/de.po --- a/po/de.po Mon Nov 07 03:22:15 2011 +0000 +++ b/po/de.po Thu Jun 28 19:26:50 2012 +0000 @@ -1,515 +1,799 @@ # German translations for vmm package. -# Copyright (C) 2009 Pascal Volk +# Copyright (C) 2009 Free Software Foundation, Inc. +# This file is distributed under the same license as the vmm package. # Pascal Volk , 2009. +# Mario Blättermann , 2011. # msgid "" msgstr "" -"Project-Id-Version: vmm 0.5.2\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-08-25 06:07+0200\n" -"PO-Revision-Date: 2009-08-25 06:11+0200\n" -"Last-Translator: Pascal Volk \n" -"Language-Team: German\n" +"Project-Id-Version: vmm 0.6.0\n" +"Report-Msgid-Bugs-To: user+vmm/tp@localhost.localdomain.org\n" +"POT-Creation-Date: 2011-11-07 05:20+0100\n" +"PO-Revision-Date: 2011-11-07 12:57+0100\n" +"Last-Translator: Mario Blättermann \n" +"Language-Team: German \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-Language: German\n" +"X-Poedit-Country: GERMANY\n" -#: VirtualMailManager/Account.py:36 VirtualMailManager/Relocated.py:44 +#. TP: Hm, what “quotation marks” should be used? +#. If you are unsure have a look at: +#. http://en.wikipedia.org/wiki/Quotation_mark,_non-English_usage +#: VirtualMailManager/account.py:58 VirtualMailManager/alias.py:35 +#: VirtualMailManager/domain.py:120 VirtualMailManager/relocated.py:38 #, python-format -msgid "There is already an alias with the address “%s”." -msgstr "Es existiert bereits ein Alias mit der Adresse „%s“." +msgid "The domain '%s' does not exist." +msgstr "Die Domain »%s« existiert nicht." -#: VirtualMailManager/Account.py:41 VirtualMailManager/Alias.py:45 +#: VirtualMailManager/account.py:106 #, python-format -msgid "There is already a relocated user with the address “%s”." -msgstr "Es gibt bereits ein relocated User mit der Adresse „%s“." +msgid "The mailbox format '%(mbfmt)s' requires Dovecot >= v%(version)s." +msgstr "Das Postfachformat »%(mbfmt)s« benötigt Dovecot >= v%(version)s." -#: VirtualMailManager/Account.py:61 VirtualMailManager/Alias.py:61 -#: VirtualMailManager/Domain.py:163 VirtualMailManager/Domain.py:189 -#: VirtualMailManager/Domain.py:220 VirtualMailManager/Relocated.py:60 +#: VirtualMailManager/account.py:113 VirtualMailManager/account.py:305 #, python-format -msgid "The domain “%s” doesn't exist yet." -msgstr "Die Domain „%s“ existiert noch nicht." +msgid "Invalid transport '%(transport)s' for mailbox format '%(mbfmt)s'." +msgstr "Ungültiger Transport »%(transport)s« für Postfachformat »%(mbfmt)s«." -#: VirtualMailManager/Account.py:80 +#: VirtualMailManager/account.py:153 VirtualMailManager/cli/handler.py:93 +#: VirtualMailManager/handler.py:628 VirtualMailManager/handler.py:679 +#: VirtualMailManager/handler.py:705 VirtualMailManager/handler.py:716 +#: VirtualMailManager/handler.py:727 VirtualMailManager/handler.py:739 +#: VirtualMailManager/handler.py:753 #, python-format -msgid "Unknown service “%s”." -msgstr "Unbekannter Service „%s“." +msgid "The account '%s' does not exist." +msgstr "Das Konto »%s« existiert nicht." -#: VirtualMailManager/Account.py:83 VirtualMailManager/Account.py:150 -#: VirtualMailManager/Account.py:178 VirtualMailManager/Account.py:212 +#: VirtualMailManager/account.py:204 VirtualMailManager/account.py:214 +#: VirtualMailManager/cli/handler.py:77 VirtualMailManager/handler.py:596 #, python-format -msgid "The account “%s” doesn't exists." -msgstr "Der Account „%s“ existiert nicht." +msgid "The account '%s' already exists." +msgstr "Das Konto »%s« existiert bereits." + +#: VirtualMailManager/account.py:207 VirtualMailManager/handler.py:701 +#, python-format +msgid "Could not accept password: '%s'" +msgstr "Passwort konnte nicht akzeptiert werden: »%s«" -#: VirtualMailManager/Account.py:145 +#: VirtualMailManager/account.py:217 #, python-format -msgid "The account “%s” already exists." -msgstr "Der Account „%s“ existiert bereits." +msgid "No password set for account: '%s'" +msgstr "Kein Passwort festgelegt für Konto: »%s«" -#: VirtualMailManager/Account.py:186 -msgid "enabled" -msgstr "aktiviert" +#: VirtualMailManager/account.py:245 +#, python-format +msgid "Unknown field: '%s'" +msgstr "Unbekanntes Feld: »%s«" -#: VirtualMailManager/Account.py:188 +#: VirtualMailManager/account.py:267 VirtualMailManager/domain.py:292 +msgid "PostgreSQL-based dictionary quota requires Dovecot >= v1.1.2." +msgstr "PostgreSQL-basierte Datenbank-Nutzungsbeschränkung benötigt Dovecot >= v1.1.2." + +#. TP: A service (e.g. pop3 or imap) may be enabled/usable or +#. disabled/unusable for a user. +#: VirtualMailManager/account.py:332 msgid "disabled" msgstr "deaktiviert" -#: VirtualMailManager/Account.py:233 +#: VirtualMailManager/account.py:332 +msgid "enabled" +msgstr "aktiviert" + +#: VirtualMailManager/account.py:343 #, python-format -msgid "There are %(count)d aliases with the destination address “%(address)s”." -msgstr "Es gibt %(count)d Alias(e) mit der Zieladresse „%(address)s“." +msgid "Could not fetch information for account: '%s'" +msgstr "Informationen zum Konto konnten nicht geholt werden: »%s«" -#: VirtualMailManager/Account.py:241 -msgid "uid must be an int/long." +#: VirtualMailManager/account.py:387 +#, python-format +msgid "There are %(count)d aliases with the destination address '%(address)s'." +msgstr "Es gibt %(count)d Alias(e) mit der Zieladresse »%(address)s«." + +#: VirtualMailManager/account.py:416 +msgid "UID must be an int/long." msgstr "Die UID muss eine Ganzzahl sein." -#: VirtualMailManager/Account.py:243 -msgid "uid must be greater than 0." +#: VirtualMailManager/account.py:418 +msgid "UID must be greater than 0." msgstr "Die UID muss größer als 0 sein." -#: VirtualMailManager/Account.py:251 +#: VirtualMailManager/account.py:427 #, python-format -msgid "There is no account with the UID “%d”." -msgstr "Es existiert kein Account mit der UID „%d“." +msgid "There is no account with the UID: '%d'" +msgstr "Es existiert kein Konto mit der UID »%d«." -#: VirtualMailManager/Alias.py:30 VirtualMailManager/Relocated.py:30 -msgid "Address and destination are identical." -msgstr "Alias- und Ziel-Adresse sind identisch." - -#: VirtualMailManager/Alias.py:40 VirtualMailManager/Relocated.py:39 -#, python-format -msgid "There is already an account with address “%s”." -msgstr "Es gibt bereits einen Account mit der Adresse „%s“." - -#: VirtualMailManager/Alias.py:71 +#: VirtualMailManager/alias.py:60 #, python-format msgid "" -"Can't add new destination to alias “%(address)s”.\n" -"Currently this alias expands into %(count)i recipients.\n" -"One more destination will render this alias unusable.\n" -"Hint: Increase Postfix' virtual_alias_expansion_limit\n" +"Cannot add %(count_new)i new destination(s) to alias '%(address)s'.\n" +"Currently this alias expands into %(count)i/%(limit)i recipients.\n" +"%(count_new)i additional destination(s) will render this alias unusable.\n" +"Hint: Increase Postfix' virtual_alias_expansion_limit" msgstr "" -"Dem Alias „%(address)s“ kann keine weitere Ziel-Adresse hinzugefügt werden.\n" -"Derzeit verweist der Alias auf %(count)i Empfänger.\n" -"Eine weitere Ziel-Adresse würde diesen Alias unbrauchbar machen.\n" -"Tipp: Erhöhen Sie Postfix' virtual_alias_expansion_limit\n" - -#: VirtualMailManager/Alias.py:80 -msgid "No destination address for alias denoted." -msgstr "Keine Ziel-Adresse für den Alias angegeben." - -#: VirtualMailManager/Alias.py:91 -#, python-format -msgid "The alias “%(a)s” with destination “%(d)s” already exists." -msgstr "Der Alias „%(a)s“ mit der Ziel-Adresse „%(d)s“ existiert bereits." - -#: VirtualMailManager/Alias.py:106 VirtualMailManager/Alias.py:123 -#, python-format -msgid "The alias “%s” doesn't exists." -msgstr "Der Alias „%s“ existiert nicht." - -#: VirtualMailManager/Alias.py:125 -#, python-format -msgid "The alias “%(a)s” with destination “%(d)s” doesn't exists." -msgstr "Der Alias „%(a)s“ mit der Ziel-Adresse „%(d)s“ existiert nicht." +"%(count_new)i neu(e) Ziel(e) konnten nicht zum Alias »%(address)s«\n" +"hinzugefügt werden. Gegenwärtig gilt dieser Alias für %(count)i/%(limit)i\n" +"Empfänger. Durch Hinzufügen von %(count_new)i Ziel(en) wird dieser Alias\n" +"unbenutzbar.\n" +"Hinweis: Erhöhen Sie den Wert für »virtual_alias_expansion_limit« in Postfix." -#: VirtualMailManager/AliasDomain.py:32 -#, python-format -msgid "The domain “%s” is a primary domain." -msgstr "Die Domain „%s“ ist eine primäre Domain." - -#: VirtualMailManager/AliasDomain.py:37 -#, python-format -msgid "The alias domain “%s” already exists." -msgstr "Die Alias-Domain „%s“ existiert bereits." - -#: VirtualMailManager/AliasDomain.py:40 VirtualMailManager/AliasDomain.py:70 -msgid "No destination domain for alias domain denoted." -msgstr "Keine Ziel-Domain für die Alias-Domain angegeben." - -#: VirtualMailManager/AliasDomain.py:43 VirtualMailManager/AliasDomain.py:73 -#, python-format -msgid "The target domain “%s” doesn't exist yet." -msgstr "Die Ziel-Domain „%s“ existiert noch nicht." - -#: VirtualMailManager/AliasDomain.py:62 -#, python-format -msgid "There is no primary domain for the alias domain “%s”." -msgstr "Es gibt keine primäre Domain für die Alias-Domain „%s“." - -#: VirtualMailManager/AliasDomain.py:65 VirtualMailManager/AliasDomain.py:76 -#: VirtualMailManager/AliasDomain.py:99 -#, python-format -msgid "The alias domain “%s” doesn't exist yet." -msgstr "Die Alias-Domain „%s“ existiert noch nicht." - -#: VirtualMailManager/AliasDomain.py:79 +#: VirtualMailManager/alias.py:67 #, python-format msgid "" -"The alias domain “%(alias)s” is already assigned to the domain “%(domain)s”." +"Cannot add %(count_new)i new destination(s) to alias '%(address)s'.\n" +"This alias already exceeds its expansion limit (%(count)i/%(limit)i).\n" +"So its unusable, all messages addressed to this alias will be bounced.\n" +"Hint: Delete some destination addresses." msgstr "" -"Die Alias-Domain „%(alias)s“ ist bereits der Domain „%(domain)s“ zugeordnet." - -#: VirtualMailManager/Config.py:102 VirtualMailManager/Config.py:137 -#, python-format -msgid "Using configuration file: %s\n" -msgstr "Verwende Konfigurationsdatei: %s\n" - -#: VirtualMailManager/Config.py:106 -#, python-format -msgid "missing section: %s\n" -msgstr "Fehlender Abschnitt: %s\n" - -#: VirtualMailManager/Config.py:108 -#, python-format -msgid "missing options in section %s:\n" -msgstr "Fehlende Optionen im Abschnitt %s:\n" - -#: VirtualMailManager/Config.py:140 -#, python-format -msgid "* Config section: “%s”" -msgstr "* Konfigurations Abschnitt: „%s“" - -#: VirtualMailManager/Config.py:143 -#, python-format -msgid "Enter new value for option %(opt)s [%(val)s]: " -msgstr "Neuer Wert für Option %(opt)s [%(val)s]: " - -#: VirtualMailManager/Domain.py:39 -#, python-format -msgid "The domain “%s” is an alias domain." -msgstr "Die Domain „%s“ ist eine Alias-Domain." - -#: VirtualMailManager/Domain.py:124 -msgid "There are accounts and aliases." -msgstr "Es sind noch Accounts und Aliase vorhanden." - -#: VirtualMailManager/Domain.py:127 -msgid "There are accounts." -msgstr "Es sind noch Accounts vorhanden." +"%(count_new)i neu(e) Ziel(e) konnten nicht zu Alias »%(address)s«\n" +"hinzugefügt werden. Dieser Alias übersteigt bereits die Begrenzung\n" +"auf (%(count)i/%(limit)i). Daher wird er unbenutzbar, und alle an\n" +"diesen Alias adressierten Nachrichten werden zurückgehalten.\n" +"Hinweis: Löschen Sie einige Zieladressen." -#: VirtualMailManager/Domain.py:130 -msgid "There are aliases." -msgstr "Es sind noch Aliase vorhanden." - -#: VirtualMailManager/Domain.py:145 -#, python-format -msgid "The domain “%s” already exists." -msgstr "Die Domain „%s“ existiert bereits." - -#: VirtualMailManager/EmailAddress.py:46 +#: VirtualMailManager/alias.py:142 VirtualMailManager/alias.py:154 +#: VirtualMailManager/alias.py:161 VirtualMailManager/handler.py:657 #, python-format -msgid "Missing '@' sign in e-mail address “%s”." -msgstr "In der E-Mail-Adresse „%s“ fehlt das '@'-Zeichen." - -#: VirtualMailManager/EmailAddress.py:49 -#, python-format -msgid "“%s” looks not like an e-mail address." -msgstr "„%s“ sieht nicht wie eine E-Mail-Adresse aus." - -#: VirtualMailManager/EmailAddress.py:54 -#, python-format -msgid "Missing domain name after “%s@”." -msgstr "Der Domain-Name nach „%s@“ fehlt." +msgid "The alias '%s' does not exist." +msgstr "Der Alias »%s« existiert nicht." -#: VirtualMailManager/EmailAddress.py:66 -msgid "No localpart specified." -msgstr "Kein local-part angegeben." - -#: VirtualMailManager/EmailAddress.py:69 -#, python-format -msgid "The local part “%s” is too long" -msgstr "Der local-part „%s“ ist zu lang" - -#: VirtualMailManager/EmailAddress.py:76 +#: VirtualMailManager/alias.py:145 #, python-format -msgid "The local part “%(lpart)s” contains invalid characters: %(ichrs)s" -msgstr "Der local-part „%(lpart)s“ enthält ungültige Zeichen: %(ichrs)s" +msgid "The address '%(addr)s' is not a destination of the alias '%(alias)s'." +msgstr "Die Adresse »%(addr)s« ist kein Ziel für den Alias »%(alias)s«." -#: VirtualMailManager/MailLocation.py:32 -msgid "Either mid or maillocation must be specified." -msgstr "Entweder mid oder maillocation muss angegeben werden." +#: VirtualMailManager/aliasdomain.py:50 +#, python-format +msgid "The domain '%s' is a primary domain." +msgstr "Die Domain »%s« ist eine primäre Domain." -#: VirtualMailManager/MailLocation.py:38 -msgid "mid must be an int/long." -msgstr "Die MID muss eine Ganzzahl sein." - -#: VirtualMailManager/MailLocation.py:46 +#: VirtualMailManager/aliasdomain.py:69 #, python-format -msgid "" -"Invalid folder name “%s”, it may consist only of\n" -"1 - 20 single byte characters (A-Z, a-z, 0-9 and _)." -msgstr "" -"Unzulässiger Verzeichnisname „%s“, dieser darf nur aus\n" -"1 - 20 Einzelbytezeichen (A-Z, a-z, 0-9 und _) bestehen." - -#: VirtualMailManager/MailLocation.py:59 -msgid "Unknown mid specified." -msgstr "Unbekannte MID angegeben." +msgid "The alias domain '%s' already exists." +msgstr "Die Alias-Domain »%s« existiert bereits." -#: VirtualMailManager/Relocated.py:65 -msgid "No destination address for relocated user denoted." -msgstr "Keine Ziel-Adresse für den relocated User angegeben." +#: VirtualMailManager/aliasdomain.py:72 VirtualMailManager/aliasdomain.py:106 +msgid "No destination domain set for the alias domain." +msgstr "Keine Ziel-Domain für die Alias-Domain angegeben." -#: VirtualMailManager/Relocated.py:75 -#, python-format -msgid "The relocated user “%s” already exists." -msgstr "Der relocated User „%s“ existiert bereits." - -#: VirtualMailManager/Relocated.py:89 VirtualMailManager/Relocated.py:102 +#: VirtualMailManager/aliasdomain.py:75 VirtualMailManager/aliasdomain.py:109 #, python-format -msgid "The relocated user “%s” doesn't exists." -msgstr "Der relocated User „%s“ existiert nicht." - -#: VirtualMailManager/Transport.py:29 -msgid "Either tid or transport must be specified." -msgstr "Entweder tid oder transport muss angegeben werden." - -#: VirtualMailManager/Transport.py:35 -msgid "tid must be an int/long." -msgstr "Die tid muss eine Ganzzahl sein." - -#: VirtualMailManager/Transport.py:63 -msgid "Unknown tid specified." -msgstr "Unbekannte tid angegeben." - -#: VirtualMailManager/VirtualMailManager.py:54 -msgid "" -"You are not root.\n" -"\tGood bye!\n" -msgstr "" -"Sie sind nicht root.\n" -"\tAuf Wiedersehen.\n" +msgid "The target domain '%s' does not exist." +msgstr "Die Ziel-Domain »%s« existiert nicht." -#: VirtualMailManager/VirtualMailManager.py:74 -msgid "No “vmm.cfg” found in: /root:/usr/local/etc:/etc" -msgstr "Keine „vmm.cfg“ gefunden in: /root:/usr/local/etc:/etc“" - -#: VirtualMailManager/VirtualMailManager.py:85 +#: VirtualMailManager/aliasdomain.py:88 VirtualMailManager/aliasdomain.py:112 +#: VirtualMailManager/aliasdomain.py:133 #, python-format -msgid "" -"fix permissions (%(perms)s) for “%(file)s”\n" -"`chmod 0600 %(file)s` would be great." -msgstr "" -"Bitte Zugriffsrechte (%(perms)s) für „%(file)s“ anpassen\n" -"`chmod 0600 %(file)s` wäre großartig." +msgid "The alias domain '%s' does not exist." +msgstr "Die Alias-Domain »%s« existiert nicht." -#: VirtualMailManager/VirtualMailManager.py:100 -#, python-format -msgid "" -"“%s” is not a directory.\n" -"(vmm.cfg: section \"domdir\", option \"base\")" -msgstr "" -"„%s“ ist kein Verzeichnis.\n" -"(vmm.cfg: Abschnitt \"domdir\", Option \"base\")" - -#: VirtualMailManager/VirtualMailManager.py:105 +#: VirtualMailManager/aliasdomain.py:98 #, python-format -msgid "" -"“%(binary)s” doesn't exists.\n" -"(vmm.cfg: section \"bin\", option \"%(option)s\")" -msgstr "" -"„%(binary)s“ existiert nicht.\n" -"(vmm.cfg: Abschnitt \"bin\", Option \"%(option)s\")" +msgid "There is no primary domain for the alias domain '%s'." +msgstr "Es gibt keine primäre Domain für die Alias-Domain »%s«." -#: VirtualMailManager/VirtualMailManager.py:109 +#: VirtualMailManager/aliasdomain.py:115 #, python-format -msgid "" -"“%(binary)s” is not executable.\n" -"(vmm.cfg: section \"bin\", option \"%(option)s\")" -msgstr "" -"„%(binary)s“ ist nicht ausführbar.\n" -"(vmm.cfg: Abschnitt \"bin\", Option \"%(option)s\")" +msgid "The alias domain '%(alias)s' is already assigned to the domain '%(domain)s'." +msgstr "Die Alias-Domain »%(alias)s« ist bereits der Domain »%(domain)s« zugeordnet." -#: VirtualMailManager/VirtualMailManager.py:166 -msgid "The domain name is too long." -msgstr "Der Domain-Name ist zu lang." - -#: VirtualMailManager/VirtualMailManager.py:169 -#, python-format -msgid "The domain name “%s” is invalid." -msgstr "Der Domain-Name „%s“ ist ungültig." - -#: VirtualMailManager/VirtualMailManager.py:209 +#. TP: Please preserve the trailing space. +#: VirtualMailManager/cli/__init__.py:78 msgid "Enter new password: " msgstr "Neues Passwort eingeben: " -#: VirtualMailManager/VirtualMailManager.py:210 +#. TP: Please preserve the trailing space. +#: VirtualMailManager/cli/__init__.py:80 msgid "Retype new password: " msgstr "Neues Passwort wiederholen: " -#: VirtualMailManager/VirtualMailManager.py:212 -msgid "Sorry, passwords do not match" -msgstr "Entschuldigung, die Passwörter stimmen nicht überein" +#: VirtualMailManager/cli/__init__.py:85 VirtualMailManager/cli/config.py:53 +msgid "Too many failures - try again later." +msgstr "Zu viele Fehlschläge - versuchen Sie es später erneut." + +#: VirtualMailManager/cli/__init__.py:91 +msgid "Sorry, passwords do not match." +msgstr "Entschuldigung, die Passwörter stimmen nicht überein." + +#: VirtualMailManager/cli/__init__.py:95 +msgid "Sorry, empty passwords are not permitted." +msgstr "Entschuldigung, leere Passwörter sind nicht zulässig." + +#: VirtualMailManager/cli/config.py:32 +#, python-format +msgid "Enter new value for option %(option)s [%(current_value)s]: " +msgstr "Neuer Wert für Option %(option)s [%(current_value)s]: " + +#: VirtualMailManager/cli/config.py:36 +#, python-format +msgid "Using configuration file: %s\n" +msgstr "Konfigurationsdatei wird verwendet: %s\n" + +#: VirtualMailManager/cli/config.py:38 +#, python-format +msgid "* Configuration section: '%s'" +msgstr "* Konfigurationsabschnitt: »%s«" + +#: VirtualMailManager/cli/config.py:50 +#, python-format +msgid "Warning: %s" +msgstr "Warnung: %s" + +#: VirtualMailManager/cli/handler.py:66 +#, python-format +msgid "Invalid section: '%s'" +msgstr "Ungültiger Abschnitt: »%s«" + +#: VirtualMailManager/cli/main.py:32 VirtualMailManager/cli/main.py:65 +#: VirtualMailManager/cli/main.py:68 VirtualMailManager/cli/subcommands.py:629 +#: VirtualMailManager/cli/subcommands.py:649 +#, python-format +msgid "Error: %s" +msgstr "Fehler: %s" + +#: VirtualMailManager/cli/main.py:41 +msgid "You must specify a subcommand at least." +msgstr "Sie müssen mindestens einen Unterbefehl angeben." + +#: VirtualMailManager/cli/main.py:53 +#, python-format +msgid "Unknown subcommand: '%s'" +msgstr "Unbekannter Unterbefehl: »%s«" + +#. TP: We have to cry, because root has killed/interrupted vmm +#. with Ctrl+C or Ctrl+D. +#: VirtualMailManager/cli/main.py:62 +msgid "Ouch!" +msgstr "Autsch!" + +#: VirtualMailManager/cli/main.py:71 +#, python-format +msgid "Error: Unknown section: '%s'" +msgstr "Fehler: Unbekannter Abschnitt: »%s«" + +#: VirtualMailManager/cli/main.py:74 +#, python-format +msgid "Error: No option '%(option)s' in section: '%(section)s'" +msgstr "Fehler: Keine Option »%(option)s« im Abschnitt: »%(section)s«" -#: VirtualMailManager/VirtualMailManager.py:216 -msgid "Sorry, empty passwords are not permitted" -msgstr "Entschuldigung, leere Passwörter sind nicht zulässig" +#: VirtualMailManager/cli/main.py:77 +msgid "Warnings:" +msgstr "Warnungen:" + +#: VirtualMailManager/cli/subcommands.py:78 +#, python-format +msgid "Plan A failed ... trying Plan B: %(subcommand)s %(object)s" +msgstr "Plan A ist gescheitert … Plan B wird versucht: %(subcommand)s %(object)s" + +#: VirtualMailManager/cli/subcommands.py:92 +msgid "Missing alias address and destination." +msgstr "Alias- und Ziel-Adresse fehlen." + +#: VirtualMailManager/cli/subcommands.py:95 +#: VirtualMailManager/cli/subcommands.py:453 +msgid "Missing destination address." +msgstr "Die Ziel-Adresse fehlt." + +#: VirtualMailManager/cli/subcommands.py:102 +#: VirtualMailManager/cli/subcommands.py:112 +msgid "Missing alias address." +msgstr "Die Alias-Adresse fehlt." + +#: VirtualMailManager/cli/subcommands.py:134 +#: VirtualMailManager/cli/subcommands.py:168 +msgid "Missing alias domain name and destination domain name." +msgstr "Domain-Namen für Alias- und Ziel-Domain fehlen." + +#: VirtualMailManager/cli/subcommands.py:137 +#: VirtualMailManager/cli/subcommands.py:171 +msgid "Missing destination domain name." +msgstr "Keine Ziel-Domain angegeben." -#: VirtualMailManager/VirtualMailManager.py:265 -#: VirtualMailManager/VirtualMailManager.py:352 +#: VirtualMailManager/cli/subcommands.py:145 +#: VirtualMailManager/cli/subcommands.py:152 +msgid "Missing alias domain name." +msgstr "Keine Alias-Domain angegeben." + +#: VirtualMailManager/cli/subcommands.py:179 +msgid "Missing option name." +msgstr "Kein Optionsname angegeben." + +#: VirtualMailManager/cli/subcommands.py:195 +msgid "Missing option and new value." +msgstr "Option und neuer Wert fehlen." + +#: VirtualMailManager/cli/subcommands.py:197 +msgid "Missing new configuration value." +msgstr "Neuer Konfigurationswert fehlt." + +#: VirtualMailManager/cli/subcommands.py:213 +#: VirtualMailManager/cli/subcommands.py:229 +#: VirtualMailManager/cli/subcommands.py:242 +#: VirtualMailManager/cli/subcommands.py:331 +msgid "Missing domain name." +msgstr "Kein Domain-Name angegeben." + +#: VirtualMailManager/cli/subcommands.py:219 #, python-format -msgid "No such directory: %s" -msgstr "Verzeichnis nicht gefunden: %s" +msgid "Creating account for postmaster@%s" +msgstr "Konto für postmaster@%s wird angelegt" + +#: VirtualMailManager/cli/subcommands.py:235 +#: VirtualMailManager/cli/subcommands.py:249 +#: VirtualMailManager/cli/subcommands.py:322 +#: VirtualMailManager/cli/subcommands.py:343 +#: VirtualMailManager/cli/subcommands.py:372 +#: VirtualMailManager/cli/subcommands.py:509 +#: VirtualMailManager/cli/subcommands.py:522 VirtualMailManager/handler.py:453 +#: VirtualMailManager/handler.py:466 VirtualMailManager/handler.py:481 +#: VirtualMailManager/handler.py:510 VirtualMailManager/handler.py:674 +#, python-format +msgid "Invalid argument: '%s'" +msgstr "Ungültiges Argument: »%s«" -#: VirtualMailManager/VirtualMailManager.py:340 -msgid "Found \"..\" in home directory path." -msgstr "\"..\" im Pfad zum Benutzerverzeichnis entdeckt." +#: VirtualMailManager/cli/subcommands.py:267 +#: VirtualMailManager/cli/subcommands.py:273 +msgid "Domain" +msgstr "Domain" + +#: VirtualMailManager/cli/subcommands.py:275 +#: VirtualMailManager/cli/subcommands.py:284 +msgid "accounts" +msgstr "Konten" + +#: VirtualMailManager/cli/subcommands.py:277 +#: VirtualMailManager/cli/subcommands.py:283 +#: VirtualMailManager/cli/subcommands.py:831 +msgid "alias domains" +msgstr "Alias-Domains" + +#: VirtualMailManager/cli/subcommands.py:279 +#: VirtualMailManager/cli/subcommands.py:285 +msgid "aliases" +msgstr "Aliase" + +#: VirtualMailManager/cli/subcommands.py:281 +#: VirtualMailManager/cli/subcommands.py:286 +msgid "relocated users" +msgstr "Verschobene Benutzer" + +#: VirtualMailManager/cli/subcommands.py:292 +msgid "Missing domain name and storage value." +msgstr "Domain-Name und neuer Speicher-Wert fehlen." + +#: VirtualMailManager/cli/subcommands.py:295 +#: VirtualMailManager/cli/subcommands.py:582 +msgid "Missing storage value." +msgstr "Speicher-Wert fehlt." -#: VirtualMailManager/VirtualMailManager.py:348 -msgid "Owner/group mismatch in home directory detected." -msgstr "Benutzerverzeichnis gehört dem/der falschen Benutzer/Gruppe." +#: VirtualMailManager/cli/subcommands.py:301 +#: VirtualMailManager/cli/subcommands.py:586 +#, python-format +msgid "Invalid storage value: '%s'" +msgstr "Ungültiger Speicher-Wert: »%s«" + +#: VirtualMailManager/cli/subcommands.py:311 +#, python-format +msgid "Neither a valid number of messages nor the keyword 'force': '%s'" +msgstr "Weder eine gültige Nachrichtenanzahl noch das Schlüsselwort »force«: »%s«" + +#: VirtualMailManager/cli/subcommands.py:319 +#: VirtualMailManager/cli/subcommands.py:595 +#, python-format +msgid "Not a valid number of messages: '%s'" +msgstr "Ungültige Nachrichtenanzahl: »%s«" + +#: VirtualMailManager/cli/subcommands.py:354 +#: VirtualMailManager/cli/subcommands.py:609 +#, python-format +msgid "Invalid service arguments: %s" +msgstr "Ungültige Dienst-Argumente: »%s«" + +#: VirtualMailManager/cli/subcommands.py:363 +msgid "Missing domain name and new transport." +msgstr "Domain-Name und neuer Transport fehlen." + +#: VirtualMailManager/cli/subcommands.py:366 +msgid "Missing new transport." +msgstr "Neuer Transport fehlt." + +#: VirtualMailManager/cli/subcommands.py:380 +msgid "Missing UID." +msgstr "Keine UID angegeben." + +#: VirtualMailManager/cli/subcommands.py:381 +#: VirtualMailManager/cli/subcommands.py:545 +#: VirtualMailManager/cli/subcommands.py:551 +msgid "Account" +msgstr "Konto" -#: VirtualMailManager/VirtualMailManager.py:364 -msgid "FATAL: \"..\" in domain directory path detected." -msgstr "FATAL: \"..\" im Pfad zum Domain-Verzeichnis entdeckt." +#: VirtualMailManager/cli/subcommands.py:396 +#, python-format +msgid "Unknown help topic: '%s'" +msgstr "Unbekanntes Hilfethema: »%s«" + +#: VirtualMailManager/cli/subcommands.py:409 +msgid "List of available subcommands:" +msgstr "Liste der verfügbaren Unterbefehle:" + +#: VirtualMailManager/cli/subcommands.py:430 +msgid "Usable encoding suffixes:" +msgstr "Verwendbare Encoding-Suffixe:" + +#: VirtualMailManager/cli/subcommands.py:430 +msgid "Usable password schemes:" +msgstr "Verfügbare Passwort-Schemata:" + +#: VirtualMailManager/cli/subcommands.py:451 +msgid "Missing relocated address and destination." +msgstr "Die Adresse des verschobenen Benutzers und Ziel-Adresse fehlen." + +#: VirtualMailManager/cli/subcommands.py:460 +#: VirtualMailManager/cli/subcommands.py:467 +msgid "Missing relocated address." +msgstr "Die Adresse des verschobenen Benutzers fehlt." + +#: VirtualMailManager/cli/subcommands.py:490 +#: VirtualMailManager/cli/subcommands.py:503 +#: VirtualMailManager/cli/subcommands.py:516 +#: VirtualMailManager/cli/subcommands.py:568 +#: VirtualMailManager/cli/subcommands.py:603 +msgid "Missing e-mail address." +msgstr "E-Mail-Adresse fehlt." -#: VirtualMailManager/VirtualMailManager.py:370 -msgid "FATAL: group mismatch in domain directory detected" -msgstr "FATAL: Domain-Verzeichnis gehört der falschen Gruppe" +#: VirtualMailManager/cli/subcommands.py:497 +#, python-format +msgid "Generated password: %s" +msgstr "Erzeugtes Passwort: %s" + +#: VirtualMailManager/cli/subcommands.py:552 +msgid "alias addresses" +msgstr "Alias-Adressen" + +#: VirtualMailManager/cli/subcommands.py:558 +msgid "Missing e-mail address and user's name." +msgstr "E-Mail-Adresse und der Name des Benutzers fehlen." + +#: VirtualMailManager/cli/subcommands.py:561 +msgid "Missing user's name." +msgstr "Name des Benutzers fehlt." + +#: VirtualMailManager/cli/subcommands.py:579 +msgid "Missing e-mail address and storage value." +msgstr "E-Mail-Adresse und Speicher-Wert fehlen." -#: VirtualMailManager/VirtualMailManager.py:457 +#: VirtualMailManager/cli/subcommands.py:617 +msgid "Missing e-mail address and transport." +msgstr "E-Mail-Adresse und Transport fehlen." + +#: VirtualMailManager/cli/subcommands.py:620 +msgid "Missing transport." +msgstr "Transport fehlt." + +#: VirtualMailManager/cli/subcommands.py:630 +msgid "usage: " +msgstr "Aufruf:" + +#. TP: Please adjust translated words like the original text. +#. (It's a table header.) Extract from usage text: +#. usage: vmm subcommand arguments +#. short long +#. subcommand arguments +#. +#. da domainadd fqdn [transport] +#. dd domaindelete fqdn [force] +#: VirtualMailManager/cli/subcommands.py:640 #, python-format msgid "" -"Configurtion error: \"%s\"\n" -"(in section \"connfig\", option \"done\") see also: vmm.cfg(5)\n" +"usage: %s subcommand arguments\n" +" short long\n" +" subcommand arguments\n" msgstr "" -"Konfigurations Fehler: \"%s\"\n" -"(im Abschnitt \"connfig\", Option \"done\") Siehe auch: vmm.cfg(5)\n" +"Aufruf: %s Unterbefehl Argumente\n" +" kurz lang\n" +" Unterbefehl Argumente\n" + +#: VirtualMailManager/cli/subcommands.py:659 +msgid "from" +msgstr "vom" + +#. TP: The words 'from', 'version' and 'on' are used in +#. the version information, e.g.: +#. vmm, version 0.5.2 (from 09/09/09) +#. Python 2.5.4 on FreeBSD +#: VirtualMailManager/cli/subcommands.py:659 +msgid "version" +msgstr "Version" + +#: VirtualMailManager/cli/subcommands.py:662 +msgid "on" +msgstr "auf" + +#: VirtualMailManager/cli/subcommands.py:664 +msgid "is free software and comes with ABSOLUTELY NO WARRANTY." +msgstr "ist freie Software und wird OHNE JEGLICHE GARANTIE bereitgestellt." + +#: VirtualMailManager/cli/subcommands.py:672 +msgid "uid" +msgstr "uid" + +#: VirtualMailManager/cli/subcommands.py:673 +msgid "get the address of the user with the given UID" +msgstr "Die Adresse des Benutzers mit der angegebenen UID ermitteln" + +#: VirtualMailManager/cli/subcommands.py:674 +#: VirtualMailManager/cli/subcommands.py:684 +msgid "address [password]" +msgstr "address [Passwort]" + +#: VirtualMailManager/cli/subcommands.py:675 +msgid "create a new e-mail user with the given address" +msgstr "Einen neuen E-Mail-Benutzer mit der angegebenen Adresse anlegen" + +#: VirtualMailManager/cli/subcommands.py:677 +#: VirtualMailManager/cli/subcommands.py:704 +#: VirtualMailManager/cli/subcommands.py:744 +#: VirtualMailManager/cli/subcommands.py:746 +msgid "address" +msgstr "Adresse" + +#: VirtualMailManager/cli/subcommands.py:678 +msgid "delete the specified user" +msgstr "Den angegebenen Benutzer löschen" + +#: VirtualMailManager/cli/subcommands.py:679 +msgid "address [details]" +msgstr "address [Details]" -#: VirtualMailManager/VirtualMailManager.py:477 -#, python-format -msgid "Invalid section: “%s”" -msgstr "Ungültiger Abschnitt: „%s“" +#: VirtualMailManager/cli/subcommands.py:680 +msgid "display information about the given address" +msgstr "Informationen über die angegebene Adresse anzeigen" + +#: VirtualMailManager/cli/subcommands.py:681 +msgid "address name" +msgstr "address Name" + +#: VirtualMailManager/cli/subcommands.py:682 +msgid "set or update the real name for an address" +msgstr "Den echten Namen für eine Adresse festlegen oder aktualisieren" + +#: VirtualMailManager/cli/subcommands.py:685 +msgid "update the password for the given address" +msgstr "Das Passwort für die angegebene Adresse aktualisieren" + +#: VirtualMailManager/cli/subcommands.py:687 +msgid "address storage [messages]" +msgstr "address storage [Nachrichten]" + +#: VirtualMailManager/cli/subcommands.py:688 +msgid "update the quota limit for the given address" +msgstr "Die Nutzungsbeschränkung für die angegebene Adresse aktualisieren" + +#: VirtualMailManager/cli/subcommands.py:690 +msgid "address [service ...]" +msgstr "address [Dienst …]" + +#: VirtualMailManager/cli/subcommands.py:691 +msgid "enables the specified services and disables all not specified services" +msgstr "Die angegebenen Dienste aktivieren und alle nicht angegebenen Dienste deaktivieren" -#: VirtualMailManager/VirtualMailManager.py:487 -#: VirtualMailManager/VirtualMailManager.py:497 -#: VirtualMailManager/VirtualMailManager.py:516 -#: VirtualMailManager/VirtualMailManager.py:624 -#: VirtualMailManager/VirtualMailManager.py:655 -#, python-format -msgid "Invalid argument: “%s”" -msgstr "Ungültiges Argument: „%s“" +#: VirtualMailManager/cli/subcommands.py:694 +msgid "address transport" +msgstr "address Transport" + +#: VirtualMailManager/cli/subcommands.py:695 +msgid "update the transport of the given address" +msgstr "Den Transport für die angegebene Adresse aktualisieren" + +#: VirtualMailManager/cli/subcommands.py:697 +msgid "address destination ..." +msgstr "address Ziel …" + +#: VirtualMailManager/cli/subcommands.py:698 +msgid "create a new alias e-mail address with one or more destinations" +msgstr "Eine neue Alias-E-Mail-Adresse mit einem oder mehreren Zielen anlegen" + +#: VirtualMailManager/cli/subcommands.py:701 +msgid "address [destination]" +msgstr "address [Ziel]" + +#: VirtualMailManager/cli/subcommands.py:702 +msgid "delete the specified alias e-mail address or one of its destinations" +msgstr "Die angegebene Alias-E-Mail-Adresse oder eines ihrer Ziele löschen" + +#: VirtualMailManager/cli/subcommands.py:705 +msgid "show the destination(s) of the specified alias" +msgstr "Das/die Ziel(e) des angegebenen Alias anzeigen" + +#: VirtualMailManager/cli/subcommands.py:708 +#: VirtualMailManager/cli/subcommands.py:717 +msgid "fqdn destination" +msgstr "fqdn Ziel" -#: VirtualMailManager/VirtualMailManager.py:520 -msgid "" -"The keyword “detailed” is deprecated and will be removed in a future " -"release.\n" -" Please use the keyword “full” to get full details." -msgstr "" -"Das Schlüsselwort „detailed“ ist veraltet und wird in einer zukünftigen\n" -" Version entfernt werden.\n" -" Verwenden Sie bitte das Schlüsselwort „full“, um alle Details zu erhalten." +#: VirtualMailManager/cli/subcommands.py:709 +msgid "create a new alias for an existing domain" +msgstr "Einen neuen Alias für eine existierende Domain anlegen" + +#: VirtualMailManager/cli/subcommands.py:711 +#: VirtualMailManager/cli/subcommands.py:714 +#: VirtualMailManager/cli/subcommands.py:723 +msgid "fqdn" +msgstr "fqdn" + +#: VirtualMailManager/cli/subcommands.py:712 +msgid "delete the specified alias domain" +msgstr "Die angegebene Domain löschen" + +#: VirtualMailManager/cli/subcommands.py:715 +msgid "show the destination of the given alias domain" +msgstr "Das Ziel der angegebenen Alias-Domain anzeigen" + +#: VirtualMailManager/cli/subcommands.py:718 +msgid "assign the given alias domain to an other domain" +msgstr "Die angegebene Alias-Domain einer anderen Domain zuordnen" + +#: VirtualMailManager/cli/subcommands.py:720 +msgid "fqdn [transport]" +msgstr "fqdn [Transport]" + +#: VirtualMailManager/cli/subcommands.py:721 +msgid "create a new domain" +msgstr "Eine neue Domain anlegen" + +#: VirtualMailManager/cli/subcommands.py:724 +msgid "delete the given domain and all its alias domains" +msgstr "Die angegebene Domain und alle ihre Alias-Domains löschen" + +#: VirtualMailManager/cli/subcommands.py:725 +msgid "fqdn [details]" +msgstr "fqdn [Details]" + +#: VirtualMailManager/cli/subcommands.py:726 +msgid "display information about the given domain" +msgstr "Informationen über die angegebene Domain anzeigen" + +#: VirtualMailManager/cli/subcommands.py:728 +msgid "fqdn storage [messages]" +msgstr "fqdn storage [Nachrichten]" + +#: VirtualMailManager/cli/subcommands.py:729 +msgid "update the quota limit of the specified domain" +msgstr "Nutzungsbeschränkung der angegebenen Domain aktualisieren" + +#: VirtualMailManager/cli/subcommands.py:731 +msgid "fqdn [service ...]" +msgstr "fqdn [Dienst …]" + +#: VirtualMailManager/cli/subcommands.py:732 +msgid "enables the specified services and disables all not specified services of the given domain" +msgstr "Aktiviert die angegebenen Dienste und deaktiviert alle nicht angegebenen Dienste der angegebenen Domain" + +#: VirtualMailManager/cli/subcommands.py:735 +msgid "fqdn transport" +msgstr "fqdn Transport" + +#: VirtualMailManager/cli/subcommands.py:736 +msgid "update the transport of the specified domain" +msgstr "Den Transport der angegebenen Domain aktualisieren" -#: VirtualMailManager/VirtualMailManager.py:593 -#, python-format -msgid "The pattern “%s” contains invalid characters." -msgstr "Das Muster „%s“ enthält ungültige Zeichen." +#: VirtualMailManager/cli/subcommands.py:737 +msgid "[pattern]" +msgstr "[Muster]" + +#: VirtualMailManager/cli/subcommands.py:738 +msgid "list all domains / search domains by pattern" +msgstr "Alle Domains/Suchdomains nach Muster auflisten" + +#: VirtualMailManager/cli/subcommands.py:741 +msgid "address newaddress" +msgstr "address Neue_Adresse" + +#: VirtualMailManager/cli/subcommands.py:742 +msgid "create a new record for a relocated user" +msgstr "Einen neuen Datensatz für einen verschobenen Benutzer anlegen" + +#: VirtualMailManager/cli/subcommands.py:745 +msgid "delete the record of the relocated user" +msgstr "Den Datensatz eines verschobenen Benutzers löschen" + +#: VirtualMailManager/cli/subcommands.py:747 +msgid "print information about a relocated user" +msgstr "Informationen über einen verschobenen Benutzer anzeigen" + +#: VirtualMailManager/cli/subcommands.py:749 +msgid "option" +msgstr "Option" + +#: VirtualMailManager/cli/subcommands.py:750 +msgid "show the actual value of the configuration option" +msgstr "Den tatsächlichen Wert der Konfigurationsoption anzeigen" -#: VirtualMailManager/VirtualMailManager.py:619 -#, python-format -msgid "The destination account/alias “%s” doesn't exists yet." -msgstr "Der Ziel-Account/-Alias „%s“ existiert noch nicht." +#: VirtualMailManager/cli/subcommands.py:751 +msgid "option value" +msgstr "Optionswert" + +#: VirtualMailManager/cli/subcommands.py:752 +msgid "set a new value for the configuration option" +msgstr "Einen neuen Wert für die Konfigurationsoption festlegen" + +#: VirtualMailManager/cli/subcommands.py:753 +msgid "[section]" +msgstr "[Abschnitt]" + +#: VirtualMailManager/cli/subcommands.py:754 +msgid "start interactive configuration modus" +msgstr "Den interaktiven Konfigurationsmodus starten" -#: VirtualMailManager/VirtualMailManager.py:636 +#: VirtualMailManager/cli/subcommands.py:756 +msgid "lists all usable password schemes and password encoding suffixes" +msgstr "Alle verwendbaren Passwort-Schemata und Encodierungs-Suffixe für Passwörter auflisten" + +#: VirtualMailManager/cli/subcommands.py:758 +msgid "[subcommand]" +msgstr "[Unterbefehl]" + +#: VirtualMailManager/cli/subcommands.py:759 +msgid "show a help overview or help for the given subcommand" +msgstr "Eine Hilfe-Übersicht oder Hilfe für einen angegebenen Unterbefehl anzeigen" + +#: VirtualMailManager/cli/subcommands.py:761 +msgid "show version and copyright information" +msgstr "Version und urheberrechtliche Informationen anzeigen" + +#: VirtualMailManager/cli/subcommands.py:809 #, python-format -msgid "" -"The account has been successfully deleted from the database.\n" -" But an error occurred while deleting the following directory:\n" -" “%(directory)s”\n" -" Reason: %(raeson)s" -msgstr "" -"Der Account wurde erfolgreich aus der Datenbank gelöscht.\n" -" Aber es trat ein Fehler auf beim Löschen des folgenden Verzeichnisses:\n" -" „%(directory)s“\n" -" Grund: %(raeson)s" - -#: VirtualMailManager/VirtualMailManager.py:676 -msgid "Account doesn't exists" -msgstr "Der Account existiert nicht" +msgid "[%(percent)s%%] %(used)s/%(limit)s" +msgstr "[%(percent)s%%] %(used)s/%(limit)s" -#: VirtualMailManager/VirtualMailManager.py:692 -#: VirtualMailManager/VirtualMailManager.py:702 -msgid "" -"The service name “managesieve” is deprecated and will be removed\n" -" in a future release.\n" -" Please use the service name “sieve” instead." -msgstr "" -"Der Servicename „managesieve“ ist veraltet und wird in einer zukünftigen\n" -" Version entfernt werden.\n" -" Verwenden Sie stattdessen bitte den Servicename „sieve“." - -#: VirtualMailManager/ext/Postconf.py:44 -#, python-format -msgid "" -"The value “%s” looks not like a valid postfix configuration parameter name." -msgstr "" -"„%s“ sieht nicht wie ein gültiger Postfix Konfigurationsparametername aus." - -#: vmm:34 -#, python-format -msgid "" -"Usage: %s SUBCOMMAND OBJECT ARGS*\n" -" short long\n" -" subcommand object args (* = optional)\n" -msgstr "" -"Verwendung: %s UNTERBEFEHL OBJEKT ARGS*\n" -" kurz lang\n" -" Unterbefehl Objekt args (* = optional)\n" - -#: vmm:73 vmm:84 vmm:494 -msgid "Error" -msgstr "Fehler" - -#: vmm:111 +#. TP: used in e.g. 'Domain information' or 'Account information' +#: VirtualMailManager/cli/subcommands.py:815 msgid "information" msgstr "Informationen" -#: vmm:121 -msgid "Available" -msgstr "Verfügbare" +#. TP: used in e.g. 'Existing alias addresses' or 'Existing accounts' +#: VirtualMailManager/cli/subcommands.py:828 +msgid "Existing" +msgstr "Vorhandene" -#: vmm:124 vmm:223 vmm:229 -msgid "alias domains" -msgstr "Alias-Domains" - -#: vmm:134 vmm:145 vmm:169 +#: VirtualMailManager/cli/subcommands.py:841 +#: VirtualMailManager/cli/subcommands.py:883 msgid "\tNone" msgstr "\tKeine" -#: vmm:138 +#: VirtualMailManager/cli/subcommands.py:846 msgid "Alias information" -msgstr "Alias Informationen" +msgstr "Alias-Informationen" -#: vmm:140 +#: VirtualMailManager/cli/subcommands.py:848 #, python-format msgid "\tMail for %s will be redirected to:" msgstr "\tE-Mails für %s werden weitergeleitet an:" -#: vmm:149 +#: VirtualMailManager/cli/subcommands.py:855 msgid "Relocated information" -msgstr "Relocated Informationen" +msgstr "Verschiebe-Informationen" -#: vmm:151 +#: VirtualMailManager/cli/subcommands.py:857 #, python-format -msgid "\tUser “%(addr)s” has moved to “%(dest)s”" -msgstr "\tDer Benutzer „%(addr)s“ ist erreichbar unter „%(dest)s“" +msgid "\tUser '%(addr)s' has moved to '%(dest)s'" +msgstr "\tDer Benutzer »%(addr)s« wurde nach »%(dest)s« verschoben" -#: vmm:164 -msgid "Available domains" -msgstr "Verfügbare Domains" - -#: vmm:166 +#: VirtualMailManager/cli/subcommands.py:872 msgid "Matching domains" msgstr "Übereinstimmende Domains" -#: vmm:180 +#: VirtualMailManager/cli/subcommands.py:874 +msgid "Existing domains" +msgstr "Existierende Domains" + +#: VirtualMailManager/cli/subcommands.py:889 msgid "Alias domain information" msgstr "Alias-Domain Informationen" -#: vmm:186 +#: VirtualMailManager/cli/subcommands.py:894 #, python-format msgid "" "\tThe alias domain %(alias)s belongs to:\n" @@ -518,114 +802,346 @@ "\tDie Alias-Domain %(alias)s gehört zu:\n" "\t * %(domain)s" -#: vmm:197 vmm:205 vmm:213 -msgid "Missing domain name." -msgstr "Kein Domain-Name angegeben." +#: VirtualMailManager/common.py:63 +#, python-format +msgid "No such file: '%s'" +msgstr "Datei nicht gefunden: »%s«" + +#: VirtualMailManager/common.py:66 +#, python-format +msgid "File is not executable: '%s'" +msgstr "Datei ist nicht ausführbar: »%s«" + +#: VirtualMailManager/common.py:82 +msgid "GiB" +msgstr "GiB" + +#: VirtualMailManager/common.py:82 +msgid "TiB" +msgstr "TiB" -#: vmm:215 vmm:219 -msgid "Domain" -msgstr "Domain" +#: VirtualMailManager/common.py:83 +msgid "KiB" +msgstr "KiB" + +#: VirtualMailManager/common.py:83 +msgid "MiB" +msgstr "MiB" -#: vmm:221 vmm:230 -msgid "accounts" -msgstr "Accounts" +#. TP: e.g.: '%(size)s %(prefix)s' -> '118.30 MiB' +#: VirtualMailManager/common.py:87 +#, python-format +msgid "%(size)s %(prefix)s" +msgstr "%(size)s %(prefix)s" + +#: VirtualMailManager/config.py:89 +#, python-format +msgid "Not a boolean: '%s'" +msgstr "Kein boolescher Wert: »%s«" -#: vmm:225 vmm:231 -msgid "aliases" -msgstr "Aliase" +#: VirtualMailManager/config.py:127 +#, python-format +msgid "Bad format: '%s' - expected: section.option" +msgstr "Falsches Format: »%s« - erwartet: Abschnitt.Option" -#: vmm:227 vmm:232 -msgid "relocated users" -msgstr "Relocated Users" +#: VirtualMailManager/config.py:380 +#, python-format +msgid "* Section: %s\n" +msgstr "* Abschnitt: %s\n" + +#: VirtualMailManager/config.py:390 VirtualMailManager/config.py:398 +#, python-format +msgid "Check of configuration file %s failed.\n" +msgstr "Überprüfen der Konfigurationsdatei %s ist fehlgeschlagen.\n" + +#: VirtualMailManager/config.py:392 +msgid "Missing options, which have no default value.\n" +msgstr "Optionen fehlen, für die kein Vorgabewert besteht.\n" -#: vmm:236 -msgid "Missing domain name and new transport." -msgstr "Domain-Name und neuer Transport fehlen." +#: VirtualMailManager/config.py:400 VirtualMailManager/config.py:402 +msgid "Invalid configuration values.\n" +msgstr "Ungültige Konfigurationswerte.\n" + +#: VirtualMailManager/config.py:441 VirtualMailManager/config.py:525 +#, python-format +msgid "Not a valid Dovecot version: '%s'" +msgstr "Keine gültige Dovecot-Version: »%s«" -#: vmm:238 -msgid "Missing new transport." -msgstr "Neuer Transport fehlt." +#: VirtualMailManager/config.py:447 VirtualMailManager/config.py:482 +#, python-format +msgid "Unsupported database module: '%s'" +msgstr "Nicht unterstütztes Datenbankmodul: »%s«" + +#: VirtualMailManager/config.py:452 VirtualMailManager/config.py:490 +#, python-format +msgid "Unknown pgsql SSL mode: '%s'" +msgstr "Unbekannter SSL-Modus für PostgreSQL: »%s«" + +#: VirtualMailManager/config.py:459 VirtualMailManager/config.py:503 +#: VirtualMailManager/maillocation.py:70 +#, python-format +msgid "Unsupported mailbox format: '%s'" +msgstr "Nicht unterstütztes Postfachformat: »%s«" -#: vmm:247 vmm:262 -msgid "Missing alias domain name and target domain name." -msgstr "Domain-Namen für Alias- und Ziel-Domain fehlen." +#: VirtualMailManager/config.py:475 VirtualMailManager/handler.py:283 +#: VirtualMailManager/handler.py:357 VirtualMailManager/handler.py:362 +#: VirtualMailManager/handler.py:390 +#, python-format +msgid "No such directory: %s" +msgstr "Ordner nicht gefunden: %s" + +#: VirtualMailManager/config.py:514 +#, python-format +msgid "Not a valid size value: '%s'" +msgstr "Ungültiger Größenwert: »%s«" + +#: VirtualMailManager/domain.py:78 +#, python-format +msgid "The domain '%s' is an alias domain." +msgstr "Die Domain »%s« ist eine Alias-Domain." -#: vmm:249 vmm:264 -msgid "Missing target domain name." -msgstr "Keine Ziel-Domain angegeben." +#: VirtualMailManager/domain.py:108 +#, python-format +msgid "There are %(account_count)u accounts, %(alias_count)u aliases and %(relocated_count)u relocated users." +msgstr "Es gibt %(account_count)u Konten, %(alias_count)u Aliase und %(relocated_count)u verschobene Benutzer." + +#: VirtualMailManager/domain.py:123 +#, python-format +msgid "The domain '%s' already exists." +msgstr "Die Domain »%s« existiert bereits." -#: vmm:255 vmm:270 -msgid "Missing alias domain name." -msgstr "Keine Alias-Domain angegeben." +#: VirtualMailManager/domain.py:437 +msgid "The domain name is too long" +msgstr "Der Domain-Name ist zu lang." + +#: VirtualMailManager/domain.py:439 +#, python-format +msgid "The domain name '%s' is invalid" +msgstr "Der Domain-Name »%s« ist ungültig" + +#: VirtualMailManager/emailaddress.py:73 +#, python-format +msgid "Missing the '@' sign in address: '%s'" +msgstr "In der Adresse fehlt das @-Zeichen: »%s«" -#: vmm:276 vmm:285 vmm:293 vmm:323 vmm:331 vmm:339 -msgid "Missing e-mail address." -msgstr "E-Mail-Adresse fehlt." +#: VirtualMailManager/emailaddress.py:76 +#, python-format +msgid "Too many '@' signs in address: '%s'" +msgstr "Zu viele @-Zeichen in Adresse: »%s«" + +#: VirtualMailManager/emailaddress.py:79 +#, python-format +msgid "Missing local-part in address: '%s'" +msgstr "Fehlender local-part in Adresse: »%s«" -#: vmm:301 -msgid "alias addresses" -msgstr "Alias-Adressen" +#: VirtualMailManager/emailaddress.py:82 +#, python-format +msgid "Missing domain name in address: '%s'" +msgstr "Domain-Name in Adresse fehlt: »%s«" + +#: VirtualMailManager/emailaddress.py:145 +#, python-format +msgid "The local-part '%s' is too long." +msgstr "Der local-part »%s« ist zu lang." + +#: VirtualMailManager/emailaddress.py:150 +#, python-format +msgid "The local-part '%(l_part)s' contains invalid characters: %(i_chars)s" +msgstr "Der local-part »%(l_part)s« enthält ungültige Zeichen: %(i_chars)s" -#: vmm:307 -msgid "Missing e-mail address and users name." -msgstr "E-Mail-Adresse und der Name des Benutzers fehlen." +#: VirtualMailManager/ext/postconf.py:84 +#, python-format +msgid "The value '%s' does not look like a valid postfix configuration parameter name." +msgstr "»%s« sieht nicht wie ein gültiger Konfigurationsparametername für Postfix aus." + +#: VirtualMailManager/handler.py:56 +msgid "an account" +msgstr "ein Konto" -#: vmm:309 -msgid "Missing users name." -msgstr "Name des Benutzers fehlt." +#: VirtualMailManager/handler.py:57 +msgid "an alias" +msgstr "ein Alias" + +#: VirtualMailManager/handler.py:58 +msgid "a relocated user" +msgstr "ein verschobener Benutzer" + +#: VirtualMailManager/handler.py:84 +msgid "" +"You are not root.\n" +"\tGood bye!\n" +msgstr "" +"Sie sind kein Administrator.\n" +"\tAuf Wiedersehen.\n" -#: vmm:315 -msgid "Missing e-mail address and transport." -msgstr "E-Mail-Adresse und Transport fehlen." +#: VirtualMailManager/handler.py:104 +#, python-format +msgid "Could not find '%(cfg_file)s' in: '%(cfg_path)s'" +msgstr "»%(cfg_file)s« konnte nicht in »%(cfg_path)s« gefunden werden" + +#: VirtualMailManager/handler.py:115 +#, python-format +msgid "" +"wrong permissions for '%(file)s': %(perms)s\n" +"`chmod 0600 %(file)s` would be great." +msgstr "" +"Bitte Zugriffsrechte (%(perms)s) für »%(file)s« anpassen\n" +"»chmod 0600 %(file)s« wäre großartig." -#: vmm:317 -msgid "Missing transport." -msgstr "Transport fehlt." +#: VirtualMailManager/handler.py:135 +#, python-format +msgid "" +"'%(path)s' is not a directory.\n" +"(%(cfg_file)s: section 'misc', option 'base_directory')" +msgstr "" +"»%(path)s« ist kein Ordner.\n" +"(%(cfg_file)s: Abschnitt »misc«, Option »base_directory«)" -#: vmm:348 -msgid "Missing alias address and destination." -msgstr "Alias- und Ziel-Adresse fehlen." +#: VirtualMailManager/handler.py:144 +#, python-format +msgid "" +"\n" +"(%(cfg_file)s: section 'bin', option '%(option)s')" +msgstr "" +"\n" +"(%(cfg_file)s: Abschnitt »bin«, Option »%(option)s«)" + +#: VirtualMailManager/handler.py:158 VirtualMailManager/handler.py:165 +#, python-format +msgid "Unable to import database module '%s'." +msgstr "Datenbankmodul »%s« konnte nicht importiert werden." -#: vmm:350 vmm:373 -msgid "Missing destination address." -msgstr "Die Ziel-Adresse fehlt." +#. TP: %(a_type)s will be one of: 'an account', 'an alias' or +#. 'a relocated user' +#: VirtualMailManager/handler.py:244 +#, python-format +msgid "There is already %(a_type)s with the address '%(address)s'." +msgstr "Es existiert bereits %(a_type)s mit der Adresse »%(address)s«." -#: vmm:356 vmm:362 -msgid "Missing alias address" -msgstr "Die Alias-Adresse fehlt." +#: VirtualMailManager/handler.py:297 +#, python-format +msgid "'%s' is not a directory." +msgstr "»%s« ist kein Ordner." + +#: VirtualMailManager/handler.py:300 +#, python-format +msgid "The file/directory '%s' already exists." +msgstr "Die Datei oder der Ordner »%s« existiert bereits." + +#: VirtualMailManager/handler.py:329 +msgid "Skipped mailbox folders:" +msgstr "Übersprungene Postfach-Ordner:" -#: vmm:371 -msgid "Missing relocated address and destination." -msgstr "Die Adresse des relocated Users und Ziel-Adresse fehlen." +#: VirtualMailManager/handler.py:349 +#, python-format +msgid "UID '%(uid)u' and/or GID '%(gid)u' are less than %(min_uid)u/%(min_gid)u." +msgstr "UID »%(uid)u« und/oder GID »%(gid)u« sind kleiner als %(min_uid)u/%(min_gid)u." + +#: VirtualMailManager/handler.py:354 VirtualMailManager/handler.py:387 +#, python-format +msgid "Found \"..\" in domain directory path: %s" +msgstr "»..« im Pfad zum Benutzerordner entdeckt: %s" -#: vmm:379 vmm:387 -msgid "Missing relocated address" -msgstr "Die Adresse des relocated Users fehlt." +#: VirtualMailManager/handler.py:367 +msgid "Detected owner/group mismatch in home directory." +msgstr "Benutzerordner gehört dem/der falschen Benutzer/Gruppe." + +#: VirtualMailManager/handler.py:383 +#, python-format +msgid "GID '%(gid)u' is less than '%(min_gid)u'." +msgstr "GID »%(gid)u« ist kleiner als »%(min_gid)u«." + +#: VirtualMailManager/handler.py:394 +#, python-format +msgid "Detected group mismatch in domain directory: %s" +msgstr "Domain-Ordner gehört der falschen Gruppe: %s" -#: vmm:393 -msgid "Missing userid" -msgstr "Keine UID angegeben." +#: VirtualMailManager/handler.py:470 VirtualMailManager/handler.py:748 +#, python-format +msgid "Unknown service: '%s'" +msgstr "Unbekannter Dienst: »%s«." + +#: VirtualMailManager/handler.py:587 +#, python-format +msgid "The pattern '%s' contains invalid characters." +msgstr "Das Muster »%s« enthält ungültige Zeichen." + +#: VirtualMailManager/handler.py:614 +msgid "Ignored destination addresses:" +msgstr "Ziel-Adressen werden ignoriert:" + +#: VirtualMailManager/handler.py:619 VirtualMailManager/handler.py:769 +#, python-format +msgid "The destination account/alias '%s' does not exist." +msgstr "Das/der Ziel-konto/-Alias »%s« existiert nicht." -#: vmm:406 -msgid "Warnings:" -msgstr "Warnungen:" +#: VirtualMailManager/handler.py:641 +#, python-format +msgid "" +"The account has been successfully deleted from the database.\n" +" But an error occurred while deleting the following directory:\n" +" '%(directory)s'\n" +" Reason: %(reason)s" +msgstr "" +"Das Konto wurde erfolgreich aus der Datenbank gelöscht.\n" +" Aber es trat ein Fehler auf beim Löschen des folgenden Ordners:\n" +" „%(directory)s“\n" +" Grund: %(reason)s" -#: vmm:412 -msgid "from" -msgstr "vom" +#: VirtualMailManager/handler.py:712 +#, python-format +msgid "Could not accept name: '%s'" +msgstr "Name kann nicht akzeptiert werden: »%s«" + +#: VirtualMailManager/handler.py:735 +#, python-format +msgid "Could not accept transport: '%s'" +msgstr "Transport kann nicht akzeptiert werden: »%s«" -#: vmm:412 -msgid "version" -msgstr "Version" +#: VirtualMailManager/handler.py:779 VirtualMailManager/relocated.py:98 +#: VirtualMailManager/relocated.py:105 +#, python-format +msgid "The relocated user '%s' does not exist." +msgstr "Der verschobene Benutzer »%s« existiert nicht." -#: vmm:414 -msgid "on" -msgstr "auf" +#: VirtualMailManager/mailbox.py:260 +#, python-format +msgid "Failed to create mailboxes: %r\n" +msgstr "Postfächer konnten nicht angelegt werden: %r\n" + +#: VirtualMailManager/maillocation.py:74 +msgid "Empty directory name" +msgstr "Ordnername ist leer" + +#: VirtualMailManager/maillocation.py:76 +#, python-format +msgid "Directory name is too long: '%s'" +msgstr "Ordnername ist zu lang: »%s«" -#: vmm:488 -msgid "Unknown subcommand" -msgstr "Unbekannter Unterbefehl" +#: VirtualMailManager/password.py:388 +#, python-format +msgid "Unsupported password scheme: '%s'" +msgstr "Nicht unterstütztes Passwortschema: »%s«" + +#: VirtualMailManager/password.py:391 +#, python-format +msgid "The password scheme '%(scheme)s' requires Dovecot >= v%(version)s." +msgstr "Das Passworschema »%(scheme)s« benötigt Dovecot >= v%(version)s." + +#: VirtualMailManager/password.py:397 +msgid "Encoding suffixes for password schemes require Dovecot >= v1.1.alpha1." +msgstr "Encoding-Suffixe für Passwort-Schemata benötigen Dovecot >= v1.1.alpha1." -#: vmm:491 -msgid "Ouch" -msgstr "Autsch" +#: VirtualMailManager/password.py:400 +#, python-format +msgid "Unsupported password encoding: '%s'" +msgstr "Nicht unterstützte Encodierung des Passworts: »%s«" + +#: VirtualMailManager/relocated.py:71 +msgid "Address and destination are identical." +msgstr "Alias- und Ziel-Adresse sind identisch." + +#: VirtualMailManager/relocated.py:75 +#, python-format +msgid "The relocated user '%s' already exists." +msgstr "Der verschobene Benutzer »%s« existiert bereits." diff -r c0e1fb1b0145 -r a4aead244f75 po/fi.po --- a/po/fi.po Mon Nov 07 03:22:15 2011 +0000 +++ b/po/fi.po Thu Jun 28 19:26:50 2012 +0000 @@ -1,526 +1,801 @@ # Finnish messages for vmm. -# Copyright © 2010 Free Software Foundation, Inc. +# Copyright © 2010, 2012 Free Software Foundation, Inc. # This file is distributed under the same license as the vmm package. -# Jorma Karvonen , 2010. +# Jorma Karvonen , 2010, 2012. # msgid "" msgstr "" -"Project-Id-Version: vmm 0.5.2\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-08-25 06:07+0200\n" -"PO-Revision-Date: 2010-12-15 18:19+0200\n" +"Project-Id-Version: vmm 0.6.0\n" +"Report-Msgid-Bugs-To: user+vmm/tp@localhost.localdomain.org\n" +"POT-Creation-Date: 2011-11-07 05:20+0100\n" +"PO-Revision-Date: 2012-03-13 21:27+0200\n" "Last-Translator: Jorma Karvonen \n" "Language-Team: Finnish \n" +"Language: fi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Language: fi\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: VirtualMailManager/Account.py:36 VirtualMailManager/Relocated.py:44 +#. TP: Hm, what “quotation marks” should be used? +#. If you are unsure have a look at: +#. http://en.wikipedia.org/wiki/Quotation_mark,_non-English_usage +#: VirtualMailManager/account.py:58 VirtualMailManager/alias.py:35 +#: VirtualMailManager/domain.py:120 VirtualMailManager/relocated.py:38 #, python-format -msgid "There is already an alias with the address “%s”." -msgstr "On jo olemassa alias osoitteella “%s”." +msgid "The domain '%s' does not exist." +msgstr "Verkkotunnus ’%s’ ei ole vielä olemassa." -#: VirtualMailManager/Account.py:41 VirtualMailManager/Alias.py:45 +#: VirtualMailManager/account.py:106 #, python-format -msgid "There is already a relocated user with the address “%s”." -msgstr "On jo sijoitettu käyttäjä osoitteella “%s”." +msgid "The mailbox format '%(mbfmt)s' requires Dovecot >= v%(version)s." +msgstr "Sähköpostilaatikkomuoto ’%(mbfmt)s’ vaatii Dovecot >= v%(version)s." -#: VirtualMailManager/Account.py:61 VirtualMailManager/Alias.py:61 -#: VirtualMailManager/Domain.py:163 VirtualMailManager/Domain.py:189 -#: VirtualMailManager/Domain.py:220 VirtualMailManager/Relocated.py:60 +#: VirtualMailManager/account.py:113 VirtualMailManager/account.py:305 #, python-format -msgid "The domain “%s” doesn't exist yet." -msgstr "Verkkotunnus “%s” ei ole vielä olemassa." +msgid "Invalid transport '%(transport)s' for mailbox format '%(mbfmt)s'." +msgstr "Virheellinen siirto ’%(transport)s’ sähköpostilaatikkomuodolle ’%(mbfmt)s’." -#: VirtualMailManager/Account.py:80 +#: VirtualMailManager/account.py:153 VirtualMailManager/cli/handler.py:93 +#: VirtualMailManager/handler.py:628 VirtualMailManager/handler.py:679 +#: VirtualMailManager/handler.py:705 VirtualMailManager/handler.py:716 +#: VirtualMailManager/handler.py:727 VirtualMailManager/handler.py:739 +#: VirtualMailManager/handler.py:753 #, python-format -msgid "Unknown service “%s”." -msgstr "Tuntematon palvelu “%s”." +msgid "The account '%s' does not exist." +msgstr "Tiliä ’%s’ ei ole olemassa." -#: VirtualMailManager/Account.py:83 VirtualMailManager/Account.py:150 -#: VirtualMailManager/Account.py:178 VirtualMailManager/Account.py:212 +#: VirtualMailManager/account.py:204 VirtualMailManager/account.py:214 +#: VirtualMailManager/cli/handler.py:77 VirtualMailManager/handler.py:596 #, python-format -msgid "The account “%s” doesn't exists." -msgstr "Tiliä “%s” ei ole vielä olemassa." +msgid "The account '%s' already exists." +msgstr "Tili ’%s’ on jo olemassa." + +#: VirtualMailManager/account.py:207 VirtualMailManager/handler.py:701 +#, python-format +msgid "Could not accept password: '%s'" +msgstr "Ei voitu hyväksyä salasanaa: ’%s’" -#: VirtualMailManager/Account.py:145 +#: VirtualMailManager/account.py:217 #, python-format -msgid "The account “%s” already exists." -msgstr "Tili “%s” on jo olemassa." +msgid "No password set for account: '%s'" +msgstr "Tilille ei ole asetettu salasanaa: ’%s’" -#: VirtualMailManager/Account.py:186 -msgid "enabled" -msgstr "otettu käyttöön" +#: VirtualMailManager/account.py:245 +#, python-format +msgid "Unknown field: '%s'" +msgstr "Tuntematon kenttä: ’%s’" -#: VirtualMailManager/Account.py:188 +#: VirtualMailManager/account.py:267 VirtualMailManager/domain.py:292 +msgid "PostgreSQL-based dictionary quota requires Dovecot >= v1.1.2." +msgstr "PostgreSQL-perustainen sanakirjakiintiö vaatii Dovecot >= v1.1.2" + +#. TP: A service (e.g. pop3 or imap) may be enabled/usable or +#. disabled/unusable for a user. +#: VirtualMailManager/account.py:332 msgid "disabled" msgstr "poistettu käytöstä" -# Alla "count" ja "address" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. -#: VirtualMailManager/Account.py:233 -#, python-format -msgid "There are %(count)d aliases with the destination address “%(address)s”." -msgstr "Jo %(count)d aliaksella on kohdeosoite “%(address)s”." +#: VirtualMailManager/account.py:332 +msgid "enabled" +msgstr "otettu käyttöön" -#: VirtualMailManager/Account.py:241 -msgid "uid must be an int/long." -msgstr "uid-käyttäjätunnisteen on oltava tyypiltään int/long." - -#: VirtualMailManager/Account.py:243 -msgid "uid must be greater than 0." -msgstr "uid-käyttäjätunnisteen on oltava suurempi kuin 0." +#: VirtualMailManager/account.py:343 +#, python-format +msgid "Could not fetch information for account: '%s'" +msgstr "Ei voitu noutaa tietoa tilille: ’%s’" -#: VirtualMailManager/Account.py:251 +# Alla "count" ja "address" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. +#: VirtualMailManager/account.py:387 #, python-format -msgid "There is no account with the UID “%d”." -msgstr "Ei ole tiliä, jonka UID-käyttäjätunniste on “%d”." +msgid "There are %(count)d aliases with the destination address '%(address)s'." +msgstr "On %(count)d aliasta kohdeosoitteella ’%(address)s’." -#: VirtualMailManager/Alias.py:30 VirtualMailManager/Relocated.py:30 -msgid "Address and destination are identical." -msgstr "Osoite ja kohde ovat identtisiä." +#: VirtualMailManager/account.py:416 +msgid "UID must be an int/long." +msgstr "UID-käyttäjätunnisteen on oltava tyypiltään int/long." -#: VirtualMailManager/Alias.py:40 VirtualMailManager/Relocated.py:39 +#: VirtualMailManager/account.py:418 +msgid "UID must be greater than 0." +msgstr "UID-käyttäjätunnisteen on oltava suurempi kuin 0." + +#: VirtualMailManager/account.py:427 #, python-format -msgid "There is already an account with address “%s”." -msgstr "On jo olemassa tili osoitteella “%s”." +msgid "There is no account with the UID: '%d'" +msgstr "Ei ole tiliä UID-käyttäjätunnisteella: ’%d’" # Alla "address" ja "count" ovat muuttuja, jotka korvautuvat suorituksen yhteydessä koodilla. -#: VirtualMailManager/Alias.py:71 +#: VirtualMailManager/alias.py:60 +#, python-format +msgid "" +"Cannot add %(count_new)i new destination(s) to alias '%(address)s'.\n" +"Currently this alias expands into %(count)i/%(limit)i recipients.\n" +"%(count_new)i additional destination(s) will render this alias unusable.\n" +"Hint: Increase Postfix' virtual_alias_expansion_limit" +msgstr "" +"Ei voida lisätä %(count_new)i uutta kohdetta aliakselle ’%(address)s’.\n" +"Nykyisin tämä alias laajenee %(count)i/%(limit)i vastaanottajaan.\n" +"%(count_new)i lisäkohdetta tekee tämän aliaksen käyttökelvottomaksi.\n" +"Vihje: Kasvata Postfix-arvoa virtual_alias_expansion_limit" + +#: VirtualMailManager/alias.py:67 #, python-format msgid "" -"Can't add new destination to alias “%(address)s”.\n" -"Currently this alias expands into %(count)i recipients.\n" -"One more destination will render this alias unusable.\n" -"Hint: Increase Postfix' virtual_alias_expansion_limit\n" +"Cannot add %(count_new)i new destination(s) to alias '%(address)s'.\n" +"This alias already exceeds its expansion limit (%(count)i/%(limit)i).\n" +"So its unusable, all messages addressed to this alias will be bounced.\n" +"Hint: Delete some destination addresses." msgstr "" -"Aliakselle “%(address)s” ei voi lisätä uutta kohdetta,\n" -"koska sillä on %(count)i vastaanottajaa.\n" -"Yksi lisäys tekee tämän aliaksen käyttökelvottomaksi.\n" -"Vihje: Kasvata Postfix-arvoa virtual_alias_expansion_limit\n" - -#: VirtualMailManager/Alias.py:80 -msgid "No destination address for alias denoted." -msgstr "Aliakselle ei ole osoitettu kohdeosoitetta." +"Ei voida lisätä %(count_new)i uutta kohdetta aliakseen '%(address)s'.\n" +"Tämä alias ylittää jo laajennusrajansa (%(count)i/%(limit)i).\n" +"Joten se on käyttökelvoton. Kaikki tähän osoitteeseen suunnatut viestit\n" +"ponnahtavat takaisin.\n" +"Vihje: Poista joitakin kohdeosoitteita." -# Alla "a" ja "d" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. -#: VirtualMailManager/Alias.py:91 +#: VirtualMailManager/alias.py:142 VirtualMailManager/alias.py:154 +#: VirtualMailManager/alias.py:161 VirtualMailManager/handler.py:657 #, python-format -msgid "The alias “%(a)s” with destination “%(d)s” already exists." -msgstr "Alias “%(a)s” kohteella “%(d)s” on jo olemassa." +msgid "The alias '%s' does not exist." +msgstr "Alias ’%s’ ei ole olemassa." -#: VirtualMailManager/Alias.py:106 VirtualMailManager/Alias.py:123 +#: VirtualMailManager/alias.py:145 #, python-format -msgid "The alias “%s” doesn't exists." -msgstr "Alias “%s” ei ole olemassa." +msgid "The address '%(addr)s' is not a destination of the alias '%(alias)s'." +msgstr "Osoite ’%(addr)s’ ei ole aliaksen ’%(alias)s’ kohde." -# Alla "a" ja "d" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. -#: VirtualMailManager/Alias.py:125 +#: VirtualMailManager/aliasdomain.py:50 #, python-format -msgid "The alias “%(a)s” with destination “%(d)s” doesn't exists." -msgstr "Alias “%(a)s” kohteella “%(d)s” ei ole olemassa." +msgid "The domain '%s' is a primary domain." +msgstr "Verkkotunnus ’%s’ on ensisijainen verkkotunnus." -#: VirtualMailManager/AliasDomain.py:32 +#: VirtualMailManager/aliasdomain.py:69 #, python-format -msgid "The domain “%s” is a primary domain." -msgstr "Verkkotunnus “%s” on ensisijainen verkkotunnus." +msgid "The alias domain '%s' already exists." +msgstr "Aliasverkkotunnus ’%s’ on jo olemassa." -#: VirtualMailManager/AliasDomain.py:37 -#, python-format -msgid "The alias domain “%s” already exists." -msgstr "Aliasverkkotunnus “%s” on jo olemassa." +#: VirtualMailManager/aliasdomain.py:72 VirtualMailManager/aliasdomain.py:106 +msgid "No destination domain set for the alias domain." +msgstr "Ei kohdeverkkotunnusjoukkoa aliasverkkotunnukselle." -#: VirtualMailManager/AliasDomain.py:40 VirtualMailManager/AliasDomain.py:70 -msgid "No destination domain for alias domain denoted." -msgstr "Kohdeverkkotunnukselle ei ole osoitettu aliasverkkotunnusta." - -#: VirtualMailManager/AliasDomain.py:43 VirtualMailManager/AliasDomain.py:73 +#: VirtualMailManager/aliasdomain.py:75 VirtualMailManager/aliasdomain.py:109 #, python-format -msgid "The target domain “%s” doesn't exist yet." -msgstr "Kohdeverkkotunnusta “%s” ei vielä ole." +msgid "The target domain '%s' does not exist." +msgstr "Kohdeverkkotunnusta ’%s’ ei ole." -#: VirtualMailManager/AliasDomain.py:62 +#: VirtualMailManager/aliasdomain.py:88 VirtualMailManager/aliasdomain.py:112 +#: VirtualMailManager/aliasdomain.py:133 #, python-format -msgid "There is no primary domain for the alias domain “%s”." -msgstr "Ei ole ensisijaista verkkotunnusta aliasverkkotunnukselle “%s”." +msgid "The alias domain '%s' does not exist." +msgstr "Aliasverkkotunnusta ’%s’ ei ole." -#: VirtualMailManager/AliasDomain.py:65 VirtualMailManager/AliasDomain.py:76 -#: VirtualMailManager/AliasDomain.py:99 +#: VirtualMailManager/aliasdomain.py:98 #, python-format -msgid "The alias domain “%s” doesn't exist yet." -msgstr "Aliasverkkotunnusta “%s” ei vielä ole." +msgid "There is no primary domain for the alias domain '%s'." +msgstr "Ei ole ensisijaista verkkotunnusta aliasverkkotunnukselle ’%s’." # Alla "alias" ja "domain" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. -#: VirtualMailManager/AliasDomain.py:79 +#: VirtualMailManager/aliasdomain.py:115 #, python-format -msgid "The alias domain “%(alias)s” is already assigned to the domain “%(domain)s”." -msgstr "Aliasverkkotunnus “%(alias)s” on jo liitetty verkkotunnukseen “%(domain)s”." +msgid "The alias domain '%(alias)s' is already assigned to the domain '%(domain)s'." +msgstr "Aliasverkkotunnus ’%(alias)s’ on jo liitetty verkkotunnukseen ’%(domain)s’." + +#. TP: Please preserve the trailing space. +#: VirtualMailManager/cli/__init__.py:78 +msgid "Enter new password: " +msgstr "Uusi salasana: " + +#. TP: Please preserve the trailing space. +#: VirtualMailManager/cli/__init__.py:80 +msgid "Retype new password: " +msgstr "Uusi salasana uudelleen: " -#: VirtualMailManager/Config.py:102 VirtualMailManager/Config.py:137 +#: VirtualMailManager/cli/__init__.py:85 VirtualMailManager/cli/config.py:53 +msgid "Too many failures - try again later." +msgstr "Liian monia häiriöitä - yritä uudelleen myöhemmin." + +#: VirtualMailManager/cli/__init__.py:91 +msgid "Sorry, passwords do not match." +msgstr "Salasanat eivät täsmää." + +#: VirtualMailManager/cli/__init__.py:95 +msgid "Sorry, empty passwords are not permitted." +msgstr "Tyhjät salasanat eivät ole sallittuja." + +# Alla "opt" ja "val" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. +#: VirtualMailManager/cli/config.py:32 +#, python-format +msgid "Enter new value for option %(option)s [%(current_value)s]: " +msgstr "Uusi arvo valitsimelle %(option)s [%(current_value)s]: " + +#: VirtualMailManager/cli/config.py:36 #, python-format msgid "Using configuration file: %s\n" msgstr "Käytetään asetustiedostoa: %s\n" -#: VirtualMailManager/Config.py:106 +#: VirtualMailManager/cli/config.py:38 #, python-format -msgid "missing section: %s\n" -msgstr "puuttuva lohko: %s\n" +msgid "* Configuration section: '%s'" +msgstr "* Asetuslohko: ’%s’" -#: VirtualMailManager/Config.py:108 +#: VirtualMailManager/cli/config.py:50 +#, python-format +msgid "Warning: %s" +msgstr "Varoitus: %s" + +#: VirtualMailManager/cli/handler.py:66 #, python-format -msgid "missing options in section %s:\n" -msgstr "puuttuvia valitsimia lohkossa %s:\n" +msgid "Invalid section: '%s'" +msgstr "Virheellinen lohko: ’%s’" -#: VirtualMailManager/Config.py:140 +#: VirtualMailManager/cli/main.py:32 VirtualMailManager/cli/main.py:65 +#: VirtualMailManager/cli/main.py:68 VirtualMailManager/cli/subcommands.py:629 +#: VirtualMailManager/cli/subcommands.py:649 #, python-format -msgid "* Config section: “%s”" -msgstr "* Asetuslohko: “%s”" +msgid "Error: %s" +msgstr "Virhe: %s" -# Alla "opt" ja "val" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. -#: VirtualMailManager/Config.py:143 +#: VirtualMailManager/cli/main.py:41 +msgid "You must specify a subcommand at least." +msgstr "Sinun on määriteltävä vähintään alikomento." + +#: VirtualMailManager/cli/main.py:53 #, python-format -msgid "Enter new value for option %(opt)s [%(val)s]: " -msgstr "Uusi arvo valitsimelle %(opt)s [%(val)s]: " +msgid "Unknown subcommand: '%s'" +msgstr "Tuntematon alikomento: ’%s’" -#: VirtualMailManager/Domain.py:39 +#. TP: We have to cry, because root has killed/interrupted vmm +#. with Ctrl+C or Ctrl+D. +#: VirtualMailManager/cli/main.py:62 +msgid "Ouch!" +msgstr "Auh!" + +#: VirtualMailManager/cli/main.py:71 #, python-format -msgid "The domain “%s” is an alias domain." -msgstr "Verkkotunnus “%s” on aliasverkkotunnus." +msgid "Error: Unknown section: '%s'" +msgstr "Virhe: Tuntematon lohko: ’%s’" -#: VirtualMailManager/Domain.py:124 -msgid "There are accounts and aliases." -msgstr "On tilejä ja aliaksia." +#: VirtualMailManager/cli/main.py:74 +#, python-format +msgid "Error: No option '%(option)s' in section: '%(section)s'" +msgstr "Virhe: Ei valitsinta ’%(option)s’ lohkossa: ’%(section)s’" + +#: VirtualMailManager/cli/main.py:77 +msgid "Warnings:" +msgstr "Varoitukset:" -#: VirtualMailManager/Domain.py:127 -msgid "There are accounts." -msgstr "Tilejä on." +#: VirtualMailManager/cli/subcommands.py:78 +#, python-format +msgid "Plan A failed ... trying Plan B: %(subcommand)s %(object)s" +msgstr "Suunnitelma A epäonnistui ... yritetään suunnitelmaa B: %(subcommand)s %(object)s" -#: VirtualMailManager/Domain.py:130 -msgid "There are aliases." -msgstr "Aliaksia on." +#: VirtualMailManager/cli/subcommands.py:92 +msgid "Missing alias address and destination." +msgstr "Puuttuva aliasosoite ja -kohde." + +#: VirtualMailManager/cli/subcommands.py:95 +#: VirtualMailManager/cli/subcommands.py:453 +msgid "Missing destination address." +msgstr "Puuttuva kohdeosoite." -#: VirtualMailManager/Domain.py:145 -#, python-format -msgid "The domain “%s” already exists." -msgstr "Verkkotunnus “%s” on jo olemassa." +#: VirtualMailManager/cli/subcommands.py:102 +#: VirtualMailManager/cli/subcommands.py:112 +msgid "Missing alias address." +msgstr "Puuttuva aliasosoite." + +#: VirtualMailManager/cli/subcommands.py:134 +#: VirtualMailManager/cli/subcommands.py:168 +msgid "Missing alias domain name and destination domain name." +msgstr "Puuttuva aliasverkkotunnusnimi ja kohdeverkkotunnusnimi." + +#: VirtualMailManager/cli/subcommands.py:137 +#: VirtualMailManager/cli/subcommands.py:171 +msgid "Missing destination domain name." +msgstr "Puuttuva kohdeverkkotunnuksen nimi." + +#: VirtualMailManager/cli/subcommands.py:145 +#: VirtualMailManager/cli/subcommands.py:152 +msgid "Missing alias domain name." +msgstr "Puuttuva aliasverkkotunnuksen nimi." -#: VirtualMailManager/EmailAddress.py:46 -#, python-format -msgid "Missing '@' sign in e-mail address “%s”." -msgstr "Puuttuva ’@’-merkki sähköpostiosoitteessa “%s”." +#: VirtualMailManager/cli/subcommands.py:179 +msgid "Missing option name." +msgstr "Puuttuva valitsinnimi." + +#: VirtualMailManager/cli/subcommands.py:195 +msgid "Missing option and new value." +msgstr "Puuttuva valitsin ja uusi arvo." -#: VirtualMailManager/EmailAddress.py:49 -#, python-format -msgid "“%s” looks not like an e-mail address." -msgstr "“%s” ei näytä sähköpostiosoitteelta." +#: VirtualMailManager/cli/subcommands.py:197 +msgid "Missing new configuration value." +msgstr "Puuuttuva uusi asetusarvo." -#: VirtualMailManager/EmailAddress.py:54 +#: VirtualMailManager/cli/subcommands.py:213 +#: VirtualMailManager/cli/subcommands.py:229 +#: VirtualMailManager/cli/subcommands.py:242 +#: VirtualMailManager/cli/subcommands.py:331 +msgid "Missing domain name." +msgstr "Puuttuva verkkotunnusnimi." + +#: VirtualMailManager/cli/subcommands.py:219 #, python-format -msgid "Missing domain name after “%s@”." -msgstr "Puuttuu verkkotunnusnimi “%s@”-merkkijonon jäljestä." +msgid "Creating account for postmaster@%s" +msgstr "Luodaan tili postmaster@%s-osoitteelle" -#: VirtualMailManager/EmailAddress.py:66 -msgid "No localpart specified." -msgstr "Paikallisosaa ei ole määritelty." - -#: VirtualMailManager/EmailAddress.py:69 +#: VirtualMailManager/cli/subcommands.py:235 +#: VirtualMailManager/cli/subcommands.py:249 +#: VirtualMailManager/cli/subcommands.py:322 +#: VirtualMailManager/cli/subcommands.py:343 +#: VirtualMailManager/cli/subcommands.py:372 +#: VirtualMailManager/cli/subcommands.py:509 +#: VirtualMailManager/cli/subcommands.py:522 VirtualMailManager/handler.py:453 +#: VirtualMailManager/handler.py:466 VirtualMailManager/handler.py:481 +#: VirtualMailManager/handler.py:510 VirtualMailManager/handler.py:674 #, python-format -msgid "The local part “%s” is too long" -msgstr "Paikallisosa “%s” on liian pitkä" +msgid "Invalid argument: '%s'" +msgstr "Virheellinen argumentti: ’%s’" -# Alla "lpart" ja "ichrs" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. -#: VirtualMailManager/EmailAddress.py:76 -#, python-format -msgid "The local part “%(lpart)s” contains invalid characters: %(ichrs)s" -msgstr "Paikallisosa “%(lpart)s” sisältää virheellisiä merkkejä: %(ichrs)s" +#: VirtualMailManager/cli/subcommands.py:267 +#: VirtualMailManager/cli/subcommands.py:273 +msgid "Domain" +msgstr "Verkkotunnus" -#: VirtualMailManager/MailLocation.py:32 -msgid "Either mid or maillocation must be specified." -msgstr "Joko mid-tunniste tai sähköpostisijainti on määriteltävä." +#: VirtualMailManager/cli/subcommands.py:275 +#: VirtualMailManager/cli/subcommands.py:284 +msgid "accounts" +msgstr "tilit" -#: VirtualMailManager/MailLocation.py:38 -msgid "mid must be an int/long." -msgstr "tunnisteen mid on oltava tyypiltään int/long." +#: VirtualMailManager/cli/subcommands.py:277 +#: VirtualMailManager/cli/subcommands.py:283 +#: VirtualMailManager/cli/subcommands.py:831 +msgid "alias domains" +msgstr "aliasverkkotunnukset" + +#: VirtualMailManager/cli/subcommands.py:279 +#: VirtualMailManager/cli/subcommands.py:285 +msgid "aliases" +msgstr "aliakset" -#: VirtualMailManager/MailLocation.py:46 +#: VirtualMailManager/cli/subcommands.py:281 +#: VirtualMailManager/cli/subcommands.py:286 +msgid "relocated users" +msgstr "sijoitetut käyttäjät" + +#: VirtualMailManager/cli/subcommands.py:292 +msgid "Missing domain name and storage value." +msgstr "Puuttuva verkkotunnusnimi ja tallennusarvo." + +#: VirtualMailManager/cli/subcommands.py:295 +#: VirtualMailManager/cli/subcommands.py:582 +msgid "Missing storage value." +msgstr "Puuttuva tallennusarvo." + +#: VirtualMailManager/cli/subcommands.py:301 +#: VirtualMailManager/cli/subcommands.py:586 #, python-format -msgid "" -"Invalid folder name “%s”, it may consist only of\n" -"1 - 20 single byte characters (A-Z, a-z, 0-9 and _)." -msgstr "" -"Virheellinen kansionimi “%s”, se voi sisältää vain\n" -"1 - 20 yhden tavun mittaisia merkkejä (A-Z, a-z, 0-9 ja _)." - -#: VirtualMailManager/MailLocation.py:59 -msgid "Unknown mid specified." -msgstr "Tuntematon mid-määritelty." +msgid "Invalid storage value: '%s'" +msgstr "Virheellinen argumentti: ’%s’" -#: VirtualMailManager/Relocated.py:65 -msgid "No destination address for relocated user denoted." -msgstr "Sijoitetulle käyttäjälle ei ole osoitettu kohdeosoitetta." +#: VirtualMailManager/cli/subcommands.py:311 +#, python-format +msgid "Neither a valid number of messages nor the keyword 'force': '%s'" +msgstr "Ei ole kelvollinen viestien lukumäärä eikä avainsana ’force’: ’%s’" -#: VirtualMailManager/Relocated.py:75 +#: VirtualMailManager/cli/subcommands.py:319 +#: VirtualMailManager/cli/subcommands.py:595 #, python-format -msgid "The relocated user “%s” already exists." -msgstr "Sijoitettu käyttäjä “%s” on jo olemassa." +msgid "Not a valid number of messages: '%s'" +msgstr "Viestien lukumäärä ei ole kelvollinen: ’%s’" + +#: VirtualMailManager/cli/subcommands.py:354 +#: VirtualMailManager/cli/subcommands.py:609 +#, python-format +msgid "Invalid service arguments: %s" +msgstr "Virheelliset palveluargumentit: %s" -#: VirtualMailManager/Relocated.py:89 VirtualMailManager/Relocated.py:102 +#: VirtualMailManager/cli/subcommands.py:363 +msgid "Missing domain name and new transport." +msgstr "Puuttuva verkkotunnusnimi ja uusi siirto." + +#: VirtualMailManager/cli/subcommands.py:366 +msgid "Missing new transport." +msgstr "Puuttuva uusi siirto." + +#: VirtualMailManager/cli/subcommands.py:380 +msgid "Missing UID." +msgstr "Puuttuva käyttäjätunniste." + +#: VirtualMailManager/cli/subcommands.py:381 +#: VirtualMailManager/cli/subcommands.py:545 +#: VirtualMailManager/cli/subcommands.py:551 +msgid "Account" +msgstr "Tilit" + +#: VirtualMailManager/cli/subcommands.py:396 #, python-format -msgid "The relocated user “%s” doesn't exists." -msgstr "Sijoitettua käyttäjää “%s” ei ole olemassa." - -#: VirtualMailManager/Transport.py:29 -msgid "Either tid or transport must be specified." -msgstr "Joko tid-siirtotunniste tai siirto on määriteltävä." +msgid "Unknown help topic: '%s'" +msgstr "Tuntematon opasteaihe: ’%s’" -#: VirtualMailManager/Transport.py:35 -msgid "tid must be an int/long." -msgstr "tunnisteen tid on oltava tyypiltään int/long." +#: VirtualMailManager/cli/subcommands.py:409 +msgid "List of available subcommands:" +msgstr "Käytettävissä olevien alikomentojen luettelo:" + +#: VirtualMailManager/cli/subcommands.py:430 +msgid "Usable encoding suffixes:" +msgstr "Käyttökelpoiset koodausloppuliitteet:" -#: VirtualMailManager/Transport.py:63 -msgid "Unknown tid specified." -msgstr "Tuntematon tid-siirtotunniste määritelty." +#: VirtualMailManager/cli/subcommands.py:430 +msgid "Usable password schemes:" +msgstr "Käyttökelpoiset salasanakaavat:" + +#: VirtualMailManager/cli/subcommands.py:451 +msgid "Missing relocated address and destination." +msgstr "Puuttuva sijoitusosoite ja -kohde." -#: VirtualMailManager/VirtualMailManager.py:54 -msgid "" -"You are not root.\n" -"\tGood bye!\n" -msgstr "" -"Et ole root-käyttäjä.\n" -"\tNäkemiin!\n" +#: VirtualMailManager/cli/subcommands.py:460 +#: VirtualMailManager/cli/subcommands.py:467 +msgid "Missing relocated address." +msgstr "Puuttuva sijoitusosoite." -#: VirtualMailManager/VirtualMailManager.py:74 -msgid "No “vmm.cfg” found in: /root:/usr/local/etc:/etc" -msgstr "Tiedostoa “vmm.cfg” ei löytynyt hakemistoista: /root:/usr/local/etc:/etc" +#: VirtualMailManager/cli/subcommands.py:490 +#: VirtualMailManager/cli/subcommands.py:503 +#: VirtualMailManager/cli/subcommands.py:516 +#: VirtualMailManager/cli/subcommands.py:568 +#: VirtualMailManager/cli/subcommands.py:603 +msgid "Missing e-mail address." +msgstr "Puuttuva sähköpostiosoite." + +#: VirtualMailManager/cli/subcommands.py:497 +#, python-format +msgid "Generated password: %s" +msgstr "Tuotettu salasana: %s" -# Alla "perms" ja "file" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. Lainausmerkit jätettiin paikallistamatta, koska ne saattavat olla skriptin toiminnan kannalta kriittisiä. -#: VirtualMailManager/VirtualMailManager.py:85 -#, python-format -msgid "" -"fix permissions (%(perms)s) for “%(file)s”\n" -"`chmod 0600 %(file)s` would be great." -msgstr "" -"korjaa käyttöoikeudet (%(perms)s) tiedostolle “%(file)s”\n" -"`chmod 0600 %(file)s` olisi erinomainen." +#: VirtualMailManager/cli/subcommands.py:552 +msgid "alias addresses" +msgstr "aliasosoitteet" + +#: VirtualMailManager/cli/subcommands.py:558 +msgid "Missing e-mail address and user's name." +msgstr "Puuttuva sähköpostiosoite ja käyttäjän nimi." + +#: VirtualMailManager/cli/subcommands.py:561 +msgid "Missing user's name." +msgstr "Puuttuva käyttäjän nimi." + +#: VirtualMailManager/cli/subcommands.py:579 +msgid "Missing e-mail address and storage value." +msgstr "Puuttuva sähköpostiosoite ja käyttäjän nimi." -#: VirtualMailManager/VirtualMailManager.py:100 -#, python-format -msgid "" -"“%s” is not a directory.\n" -"(vmm.cfg: section \"domdir\", option \"base\")" -msgstr "" -"“%s” ei ole hakemisto.\n" -"(vmm.cfg: lohko ”domdir”, valitsin ”base”)" +#: VirtualMailManager/cli/subcommands.py:617 +msgid "Missing e-mail address and transport." +msgstr "Puuttuva sähköpostiosoite ja siirto." + +#: VirtualMailManager/cli/subcommands.py:620 +msgid "Missing transport." +msgstr "Puuttuva siirto." -# Alla "options" on muuttuja, joka korvautuu suorituksen yhteydessä koodilla. -#: VirtualMailManager/VirtualMailManager.py:105 +#: VirtualMailManager/cli/subcommands.py:630 +msgid "usage: " +msgstr "käyttö: " + +#. TP: Please adjust translated words like the original text. +#. (It's a table header.) Extract from usage text: +#. usage: vmm subcommand arguments +#. short long +#. subcommand arguments +#. +#. da domainadd fqdn [transport] +#. dd domaindelete fqdn [force] +#: VirtualMailManager/cli/subcommands.py:640 #, python-format msgid "" -"“%(binary)s” doesn't exists.\n" -"(vmm.cfg: section \"bin\", option \"%(option)s\")" +"usage: %s subcommand arguments\n" +" short long\n" +" subcommand arguments\n" msgstr "" -"“%(binary)s” ei ole olemassa.\n" -"(vmm.cfg: lohko ”bin”, valitsin ”%(option)s”)" +"käyttö: %s alikomento argumentit\n" +" lyhyt pitkä\n" +" alikomento argumentit\n" + +#: VirtualMailManager/cli/subcommands.py:659 +msgid "from" +msgstr "kohteesta" + +#. TP: The words 'from', 'version' and 'on' are used in +#. the version information, e.g.: +#. vmm, version 0.5.2 (from 09/09/09) +#. Python 2.5.4 on FreeBSD +#: VirtualMailManager/cli/subcommands.py:659 +msgid "version" +msgstr "versio" + +#: VirtualMailManager/cli/subcommands.py:662 +msgid "on" +msgstr "kohteessa" + +#: VirtualMailManager/cli/subcommands.py:664 +msgid "is free software and comes with ABSOLUTELY NO WARRANTY." +msgstr "on vapaa ohjelmisto ja tulee EHDOTTOMASTI ILMAN TAKUUTA." -# Alla "options" on muuttuja, joka korvautuu suorituksen yhteydessä koodilla. -#: VirtualMailManager/VirtualMailManager.py:109 -#, python-format -msgid "" -"“%(binary)s” is not executable.\n" -"(vmm.cfg: section \"bin\", option \"%(option)s\")" -msgstr "" -"“%(binary)s” ei ole suoritettava tiedosto.\n" -"(vmm.cfg: lohko ”bin”, valitsin ”%(option)s”)" +#: VirtualMailManager/cli/subcommands.py:672 +msgid "uid" +msgstr "käyttäjätunniste" + +#: VirtualMailManager/cli/subcommands.py:673 +msgid "get the address of the user with the given UID" +msgstr "hae käyttäjän osoite annetulla käyttäjätunnuksella" + +#: VirtualMailManager/cli/subcommands.py:674 +#: VirtualMailManager/cli/subcommands.py:684 +msgid "address [password]" +msgstr "osoite [salasana]" + +#: VirtualMailManager/cli/subcommands.py:675 +msgid "create a new e-mail user with the given address" +msgstr "luo uusi sähköpostikäyttäj annetulla osoitteella" + +#: VirtualMailManager/cli/subcommands.py:677 +#: VirtualMailManager/cli/subcommands.py:704 +#: VirtualMailManager/cli/subcommands.py:744 +#: VirtualMailManager/cli/subcommands.py:746 +msgid "address" +msgstr "osoite" + +#: VirtualMailManager/cli/subcommands.py:678 +msgid "delete the specified user" +msgstr "poista määritelty käyttäjä" + +#: VirtualMailManager/cli/subcommands.py:679 +msgid "address [details]" +msgstr "osoite [yksityiskohdat]" -#: VirtualMailManager/VirtualMailManager.py:166 -msgid "The domain name is too long." -msgstr "Verkkotunnusnimi on liian pitkä." +#: VirtualMailManager/cli/subcommands.py:680 +msgid "display information about the given address" +msgstr "näytä tietoja annetusta osoitteesta" + +#: VirtualMailManager/cli/subcommands.py:681 +msgid "address name" +msgstr "osoitenimi" + +#: VirtualMailManager/cli/subcommands.py:682 +msgid "set or update the real name for an address" +msgstr "aseta tai päivitä osoitteen todellinen nimi" + +#: VirtualMailManager/cli/subcommands.py:685 +msgid "update the password for the given address" +msgstr "päivitä annetun osoitteen salasana" -#: VirtualMailManager/VirtualMailManager.py:169 -#, python-format -msgid "The domain name “%s” is invalid." -msgstr "Verkkotunnusnimi “%s” on virheellinen." +#: VirtualMailManager/cli/subcommands.py:687 +msgid "address storage [messages]" +msgstr "osoitetallennus [viestit]" + +#: VirtualMailManager/cli/subcommands.py:688 +msgid "update the quota limit for the given address" +msgstr "päivitä annetun osoitteen kiintiöraja" + +#: VirtualMailManager/cli/subcommands.py:690 +msgid "address [service ...]" +msgstr "osoite [palvelu ...]" + +#: VirtualMailManager/cli/subcommands.py:691 +msgid "enables the specified services and disables all not specified services" +msgstr "ottaa käyttöön määritellyt palvelut ja ottaa pois käytöstä määrittelemättömät palvelut" -#: VirtualMailManager/VirtualMailManager.py:209 -msgid "Enter new password: " -msgstr "Uusi salasana: " +#: VirtualMailManager/cli/subcommands.py:694 +msgid "address transport" +msgstr "osoitesiirto" + +#: VirtualMailManager/cli/subcommands.py:695 +msgid "update the transport of the given address" +msgstr "päivitä annetun osoitteen siirto" + +#: VirtualMailManager/cli/subcommands.py:697 +msgid "address destination ..." +msgstr "osoitekohde ..." + +#: VirtualMailManager/cli/subcommands.py:698 +msgid "create a new alias e-mail address with one or more destinations" +msgstr "luo uusi aliassähköpostiosoite yhdellä tai useammalla kohteella" -#: VirtualMailManager/VirtualMailManager.py:210 -msgid "Retype new password: " -msgstr "Uusi salasana uudelleen: " +#: VirtualMailManager/cli/subcommands.py:701 +msgid "address [destination]" +msgstr "osoite [kohde]" + +#: VirtualMailManager/cli/subcommands.py:702 +msgid "delete the specified alias e-mail address or one of its destinations" +msgstr "poista määritellyn aliaksen sähköpostiosoite tai yksi sen kohteista" -#: VirtualMailManager/VirtualMailManager.py:212 -msgid "Sorry, passwords do not match" -msgstr "Salasanat eivät täsmää" +#: VirtualMailManager/cli/subcommands.py:705 +msgid "show the destination(s) of the specified alias" +msgstr "näytä määritellyn aliaksen kohteet" + +#: VirtualMailManager/cli/subcommands.py:708 +#: VirtualMailManager/cli/subcommands.py:717 +msgid "fqdn destination" +msgstr "fqdn-kohde" -#: VirtualMailManager/VirtualMailManager.py:216 -msgid "Sorry, empty passwords are not permitted" -msgstr "Tyhjät salasanat eivät ole sallittuja" +#: VirtualMailManager/cli/subcommands.py:709 +msgid "create a new alias for an existing domain" +msgstr "luotu uusi alias olemassaolevaan verkkotunnukseen" + +#: VirtualMailManager/cli/subcommands.py:711 +#: VirtualMailManager/cli/subcommands.py:714 +#: VirtualMailManager/cli/subcommands.py:723 +msgid "fqdn" +msgstr "fqdn" + +#: VirtualMailManager/cli/subcommands.py:712 +msgid "delete the specified alias domain" +msgstr "poista määritelty aliasverkkotunnus" + +#: VirtualMailManager/cli/subcommands.py:715 +msgid "show the destination of the given alias domain" +msgstr "näytä annetun aliasverkkotunnuksen kohde" + +#: VirtualMailManager/cli/subcommands.py:718 +msgid "assign the given alias domain to an other domain" +msgstr "liitä annettu aliasverkkotunnus toiseen verkkotunnukseen" + +#: VirtualMailManager/cli/subcommands.py:720 +msgid "fqdn [transport]" +msgstr "fqdn [siirto]" + +#: VirtualMailManager/cli/subcommands.py:721 +msgid "create a new domain" +msgstr "luo uusi verkkotunnus" -# Tässä argumentti saattaa olla hakemistonimi tai sitten virheilmoitus. -#: VirtualMailManager/VirtualMailManager.py:265 -#: VirtualMailManager/VirtualMailManager.py:352 -#, python-format -msgid "No such directory: %s" -msgstr "Ei löydy hakemistoa: %s" +#: VirtualMailManager/cli/subcommands.py:724 +msgid "delete the given domain and all its alias domains" +msgstr "poista annettu verkkotunnus ja kaikki sen aliasverkkotunnukset" + +#: VirtualMailManager/cli/subcommands.py:725 +msgid "fqdn [details]" +msgstr "fqdn [yksityiskohdat]" + +#: VirtualMailManager/cli/subcommands.py:726 +msgid "display information about the given domain" +msgstr "näytä tiedot annetusta verkkotunnuksesta" + +#: VirtualMailManager/cli/subcommands.py:728 +msgid "fqdn storage [messages]" +msgstr "fqdn-tallennus [viestit]" -#: VirtualMailManager/VirtualMailManager.py:340 -msgid "Found \"..\" in home directory path." -msgstr "Löytyi ”..” kotihakemistopolussa." +#: VirtualMailManager/cli/subcommands.py:729 +msgid "update the quota limit of the specified domain" +msgstr "päivitä määritellyn verkkotunnuksen kiintiöraja" + +#: VirtualMailManager/cli/subcommands.py:731 +msgid "fqdn [service ...]" +msgstr "fqdn [palvelu ...]" -#: VirtualMailManager/VirtualMailManager.py:348 -msgid "Owner/group mismatch in home directory detected." -msgstr "Kotihakemiston omistaja ja ryhmä eivät täsmää." +#: VirtualMailManager/cli/subcommands.py:732 +msgid "enables the specified services and disables all not specified services of the given domain" +msgstr "ottaa käyttöön määritellyt palvelut ja ottaa pois käytöstä kaikki määrittelemättömät annetun verkkotunnuksen palvelut" + +#: VirtualMailManager/cli/subcommands.py:735 +msgid "fqdn transport" +msgstr "fqdn-siirto" + +#: VirtualMailManager/cli/subcommands.py:736 +msgid "update the transport of the specified domain" +msgstr "päivitä määritellyn verkkotunnuksen siirto" -#: VirtualMailManager/VirtualMailManager.py:364 -msgid "FATAL: \"..\" in domain directory path detected." -msgstr "VAKAVA VIRHE: havaittu ”..” verkkotunnushakemistopolkussa." +#: VirtualMailManager/cli/subcommands.py:737 +msgid "[pattern]" +msgstr "[malli]" + +#: VirtualMailManager/cli/subcommands.py:738 +msgid "list all domains / search domains by pattern" +msgstr "luettele kaikki mallin mukaiset verkkotunnukset / hakuverkkotunnukset" + +#: VirtualMailManager/cli/subcommands.py:741 +msgid "address newaddress" +msgstr "osoite newaddress" + +#: VirtualMailManager/cli/subcommands.py:742 +msgid "create a new record for a relocated user" +msgstr "luo uusi tietue sijoitettavalle käyttäjälle" -#: VirtualMailManager/VirtualMailManager.py:370 -msgid "FATAL: group mismatch in domain directory detected" -msgstr "VAKAVA VIRHE: havaittu ryhmätäsmäämättömyys verkkotunnushakemistossa" +#: VirtualMailManager/cli/subcommands.py:745 +msgid "delete the record of the relocated user" +msgstr "poista sijoitetun käyttäjän tietue" + +#: VirtualMailManager/cli/subcommands.py:747 +msgid "print information about a relocated user" +msgstr "tulosta tiedot sijoitetusta käyttäjästä" + +#: VirtualMailManager/cli/subcommands.py:749 +msgid "option" +msgstr "valitsin" + +#: VirtualMailManager/cli/subcommands.py:750 +msgid "show the actual value of the configuration option" +msgstr "näytä asetusvalitsimen todellinen arvo" -# Tässä on ilmeisesti kirjoitusvirhe (Configurtion -> Configuration). -#: VirtualMailManager/VirtualMailManager.py:457 -#, python-format -msgid "" -"Configurtion error: \"%s\"\n" -"(in section \"connfig\", option \"done\") see also: vmm.cfg(5)\n" -msgstr "" -"Asetusvirhe: ”%s”\n" -"(lohkossa ”connfig”, valitsin ”done”) katso myös: vmm.cfg(5)\n" +#: VirtualMailManager/cli/subcommands.py:751 +msgid "option value" +msgstr "valitsinarvo" + +#: VirtualMailManager/cli/subcommands.py:752 +msgid "set a new value for the configuration option" +msgstr "aseta asetusvalitsimelle uusi arvo" + +#: VirtualMailManager/cli/subcommands.py:753 +msgid "[section]" +msgstr "[lohko]" + +#: VirtualMailManager/cli/subcommands.py:754 +msgid "start interactive configuration modus" +msgstr "aloita vuorovaikutteinen asetustila" -#: VirtualMailManager/VirtualMailManager.py:477 +#: VirtualMailManager/cli/subcommands.py:756 +msgid "lists all usable password schemes and password encoding suffixes" +msgstr "luettelee kaikki käytettävät salasanakaavat ja salasanakoodausloppuliitteet" + +#: VirtualMailManager/cli/subcommands.py:758 +msgid "[subcommand]" +msgstr "[alikomento]" + +#: VirtualMailManager/cli/subcommands.py:759 +msgid "show a help overview or help for the given subcommand" +msgstr "näytä opasteyhteenveto tai opaste annetusta alikomennosta" + +#: VirtualMailManager/cli/subcommands.py:761 +msgid "show version and copyright information" +msgstr "näytä versio ja copyright-tiedot" + +#: VirtualMailManager/cli/subcommands.py:809 #, python-format -msgid "Invalid section: “%s”" -msgstr "Virheellinen lohko: ’%s’" - -#: VirtualMailManager/VirtualMailManager.py:487 -#: VirtualMailManager/VirtualMailManager.py:497 -#: VirtualMailManager/VirtualMailManager.py:516 -#: VirtualMailManager/VirtualMailManager.py:624 -#: VirtualMailManager/VirtualMailManager.py:655 -#, python-format -msgid "Invalid argument: “%s”" -msgstr "Virheellinen argumentti: “%s“" - -#: VirtualMailManager/VirtualMailManager.py:520 -msgid "" -"The keyword “detailed” is deprecated and will be removed in a future release.\n" -" Please use the keyword “full” to get full details." -msgstr "" -"Avainsana “detailed” on vanhentunut ja poistetaan tulevassa julkaisussa.\n" -" Saat kaikki yksityiskohdat käyttämällä avainsanaa “full”." - -#: VirtualMailManager/VirtualMailManager.py:593 -#, python-format -msgid "The pattern “%s” contains invalid characters." -msgstr "Säännöllinen lauseke “%s” sisältää virheellisiä merkkejä." - -#: VirtualMailManager/VirtualMailManager.py:619 -#, python-format -msgid "The destination account/alias “%s” doesn't exists yet." -msgstr "Kohdetili/-alias “%s” ei ole vielä olemassa." +msgid "[%(percent)s%%] %(used)s/%(limit)s" +msgstr "[%(percent)s%%] %(used)s/%(limit)s" -# Alla "directory" ja "raeson" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. -#: VirtualMailManager/VirtualMailManager.py:636 -#, python-format -msgid "" -"The account has been successfully deleted from the database.\n" -" But an error occurred while deleting the following directory:\n" -" “%(directory)s”\n" -" Reason: %(raeson)s" -msgstr "" -"Tili on poistettu onnistuneesti tietokannasta.\n" -" Mutta tapahtui virhe, kun poistettiin seuraavaa hakemistoa:\n" -" “%(directory)s”\n" -" Syy: %(raeson)s" - -#: VirtualMailManager/VirtualMailManager.py:676 -msgid "Account doesn't exists" -msgstr "Tiliä ei ole" - -#: VirtualMailManager/VirtualMailManager.py:692 -#: VirtualMailManager/VirtualMailManager.py:702 -msgid "" -"The service name “managesieve” is deprecated and will be removed\n" -" in a future release.\n" -" Please use the service name “sieve” instead." -msgstr "" -"Palvelunimi “managesieve” on vanhentunut ja poistetaan\n" -" tulevassa julkaisussa.\n" -" Käytä sen sijaan palvelunimeä “sieve”." - -#: VirtualMailManager/ext/Postconf.py:44 -#, python-format -msgid "The value “%s” looks not like a valid postfix configuration parameter name." -msgstr "Arvo “%s” ei näytä olevan kelvollinen postfix-asetusparametrinimi." - -#: vmm:34 -#, python-format -msgid "" -"Usage: %s SUBCOMMAND OBJECT ARGS*\n" -" short long\n" -" subcommand object args (* = optional)\n" -msgstr "" -"Käyttö: %s ALIKOMENTO OBJEKTI ARGUMENTIT*\n" -" lyhyt pitkä\n" -" alikomento objekti argumentti (* = valinnainen)\n" - -#: vmm:73 vmm:84 vmm:494 -msgid "Error" -msgstr "Virhe" - -#: vmm:111 +#. TP: used in e.g. 'Domain information' or 'Account information' +#: VirtualMailManager/cli/subcommands.py:815 msgid "information" msgstr "tiedot" -#: vmm:121 -msgid "Available" -msgstr "Käytettävissä" +#. TP: used in e.g. 'Existing alias addresses' or 'Existing accounts' +#: VirtualMailManager/cli/subcommands.py:828 +msgid "Existing" +msgstr "Olemassa oleva" -#: vmm:124 vmm:223 vmm:229 -msgid "alias domains" -msgstr "aliasverkkotunnukset" - -#: vmm:134 vmm:145 vmm:169 +#: VirtualMailManager/cli/subcommands.py:841 +#: VirtualMailManager/cli/subcommands.py:883 msgid "\tNone" msgstr "\tEi mitään" -#: vmm:138 +#: VirtualMailManager/cli/subcommands.py:846 msgid "Alias information" msgstr "Aliastiedot" -#: vmm:140 +#: VirtualMailManager/cli/subcommands.py:848 #, python-format msgid "\tMail for %s will be redirected to:" msgstr "\tSähköposti %s ohjataan edelleen osoitteeseen:" -#: vmm:149 +#: VirtualMailManager/cli/subcommands.py:855 msgid "Relocated information" msgstr "Sijaintitiedot" # Alla "addr" ja "dest" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. -#: vmm:151 +#: VirtualMailManager/cli/subcommands.py:857 #, python-format -msgid "\tUser “%(addr)s” has moved to “%(dest)s”" -msgstr "\tKäyttäjä “%(addr)s” on siirtynyt kohteeseen “%(dest)s”" +msgid "\tUser '%(addr)s' has moved to '%(dest)s'" +msgstr "\tKäyttäjä ’%(addr)s’ on siirretty kohteeseen ’%(dest)s’" -#: vmm:164 -msgid "Available domains" -msgstr "Käytettävissä olevat verkkotunnukset" - -#: vmm:166 +#: VirtualMailManager/cli/subcommands.py:872 msgid "Matching domains" msgstr "Täsmäävät verkkotunnukset" -#: vmm:180 +#: VirtualMailManager/cli/subcommands.py:874 +msgid "Existing domains" +msgstr "Olemassaolevat verkkotunnukset" + +#: VirtualMailManager/cli/subcommands.py:889 msgid "Alias domain information" msgstr "Aliasverkkotunnustiedot" # Alla "alias" ja "domain" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. -#: vmm:186 +#: VirtualMailManager/cli/subcommands.py:894 #, python-format msgid "" "\tThe alias domain %(alias)s belongs to:\n" @@ -529,116 +804,474 @@ "\tAliasverkkotunnus %(alias)s kuuluu kohteeseen:\n" "\t * %(domain)s" -#: vmm:197 vmm:205 vmm:213 -msgid "Missing domain name." -msgstr "Puuttuva verkkotunnusnimi." +#: VirtualMailManager/common.py:63 +#, python-format +msgid "No such file: '%s'" +msgstr "Ei löydy tiedostoa: ’%s’" + +#: VirtualMailManager/common.py:66 +#, python-format +msgid "File is not executable: '%s'" +msgstr "Tiedosto ei ole suoritettava tiedosto: ’%s’" + +#: VirtualMailManager/common.py:82 +msgid "GiB" +msgstr "gibitavua" + +#: VirtualMailManager/common.py:82 +msgid "TiB" +msgstr "tebitavua" + +#: VirtualMailManager/common.py:83 +msgid "KiB" +msgstr "kibitavua" + +#: VirtualMailManager/common.py:83 +msgid "MiB" +msgstr "mebitavua" -#: vmm:215 vmm:219 -msgid "Domain" -msgstr "Verkkotunnus" +#. TP: e.g.: '%(size)s %(prefix)s' -> '118.30 MiB' +#: VirtualMailManager/common.py:87 +#, python-format +msgid "%(size)s %(prefix)s" +msgstr "%(size)s %(prefix)s" + +#: VirtualMailManager/config.py:89 +#, python-format +msgid "Not a boolean: '%s'" +msgstr "Ei ole boolean-arvo: ’%s’" -#: vmm:221 vmm:230 -msgid "accounts" -msgstr "tilit" +#: VirtualMailManager/config.py:127 +#, python-format +msgid "Bad format: '%s' - expected: section.option" +msgstr "Väärä muoto: ’%s’ - odotettiin: lohko.valitsin" + +#: VirtualMailManager/config.py:380 +#, python-format +msgid "* Section: %s\n" +msgstr "* Lohko: %s\n" + +#: VirtualMailManager/config.py:390 VirtualMailManager/config.py:398 +#, python-format +msgid "Check of configuration file %s failed.\n" +msgstr "Asetustiedoston %s tarkistus epäonnistui.\n" + +#: VirtualMailManager/config.py:392 +msgid "Missing options, which have no default value.\n" +msgstr "Puuttuu valitsimia, joilla ei ole oletusarvoa.\n" -#: vmm:225 vmm:231 -msgid "aliases" -msgstr "aliakset" +#: VirtualMailManager/config.py:400 VirtualMailManager/config.py:402 +msgid "Invalid configuration values.\n" +msgstr "Virheelliset asetusarvot.\n" + +#: VirtualMailManager/config.py:441 VirtualMailManager/config.py:525 +#, python-format +msgid "Not a valid Dovecot version: '%s'" +msgstr "Ei ole kelvollinen Dovecot-versio: ’%s’" + +#: VirtualMailManager/config.py:447 VirtualMailManager/config.py:482 +#, python-format +msgid "Unsupported database module: '%s'" +msgstr "Tukematon tietokantamoduuli: ’%s’" + +#: VirtualMailManager/config.py:452 VirtualMailManager/config.py:490 +#, python-format +msgid "Unknown pgsql SSL mode: '%s'" +msgstr "Tuntematon pgsql SSL -tila: ’%s’" + +#: VirtualMailManager/config.py:459 VirtualMailManager/config.py:503 +#: VirtualMailManager/maillocation.py:70 +#, python-format +msgid "Unsupported mailbox format: '%s'" +msgstr "Tukematon sähköpostilaatikkomuoto: ’%s’" -#: vmm:227 vmm:232 -msgid "relocated users" -msgstr "sijoitetut käyttäjät" +# Tässä argumentti saattaa olla hakemistonimi tai sitten virheilmoitus. +#: VirtualMailManager/config.py:475 VirtualMailManager/handler.py:283 +#: VirtualMailManager/handler.py:357 VirtualMailManager/handler.py:362 +#: VirtualMailManager/handler.py:390 +#, python-format +msgid "No such directory: %s" +msgstr "Ei löydy hakemistoa: %s" + +#: VirtualMailManager/config.py:514 +#, python-format +msgid "Not a valid size value: '%s'" +msgstr "Ei ole kelvollinen kokoarvo: ’%s’" -#: vmm:236 -msgid "Missing domain name and new transport." -msgstr "Puuttuva verkkotunnusnimi ja uusi siirto." +#: VirtualMailManager/domain.py:78 +#, python-format +msgid "The domain '%s' is an alias domain." +msgstr "Verkkotunnus ’%s’ on aliasverkkotunnus." + +#: VirtualMailManager/domain.py:108 +#, python-format +msgid "There are %(account_count)u accounts, %(alias_count)u aliases and %(relocated_count)u relocated users." +msgstr "On %(account_count)u -tiliä, %(alias_count)u -aliasta ja %(relocated_count)u sijoitettua käyttäjää." + +#: VirtualMailManager/domain.py:123 +#, python-format +msgid "The domain '%s' already exists." +msgstr "Verkkotunnus ’%s’ on jo olemassa." + +#: VirtualMailManager/domain.py:437 +msgid "The domain name is too long" +msgstr "Verkkotunnusnimi on liian pitkä" -#: vmm:238 -msgid "Missing new transport." -msgstr "Puuttuva uusi siirto." +#: VirtualMailManager/domain.py:439 +#, python-format +msgid "The domain name '%s' is invalid" +msgstr "Verkkotunnusnimi ’%s’ on virheellinen" + +#: VirtualMailManager/emailaddress.py:73 +#, python-format +msgid "Missing the '@' sign in address: '%s'" +msgstr "Merkki ’@’ puuttuu sähköpostiosoitteesta: ’%s’." + +#: VirtualMailManager/emailaddress.py:76 +#, python-format +msgid "Too many '@' signs in address: '%s'" +msgstr "Liian monta ’@’-merkkiä sähköpostiosoitteessa: ’%s’." + +#: VirtualMailManager/emailaddress.py:79 +#, python-format +msgid "Missing local-part in address: '%s'" +msgstr "Puuttuu paikallisosa osoitteessa: ’%s’" + +#: VirtualMailManager/emailaddress.py:82 +#, python-format +msgid "Missing domain name in address: '%s'" +msgstr "Puuttuu verkkotunnusnimi osoitteessa: ’%s’" -#: vmm:247 vmm:262 -msgid "Missing alias domain name and target domain name." -msgstr "Puuttuva aliasverkkotunnusnimi ja kohdeverkkotunnusnimi." +#: VirtualMailManager/emailaddress.py:145 +#, python-format +msgid "The local-part '%s' is too long." +msgstr "Paikallisosa ’%s’ on liian pitkä." + +# Alla "l_part" ja "i_chars" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. +#: VirtualMailManager/emailaddress.py:150 +#, python-format +msgid "The local-part '%(l_part)s' contains invalid characters: %(i_chars)s" +msgstr "Paikallisosa ’%(l_part)s’ sisältää virheellisiä merkkejä: %(i_chars)s" -#: vmm:249 vmm:264 -msgid "Missing target domain name." -msgstr "Puuttuva kohdeverkkotunnuksen nimi." +#: VirtualMailManager/ext/postconf.py:84 +#, python-format +msgid "The value '%s' does not look like a valid postfix configuration parameter name." +msgstr "Arvo ’%s’ ei näytä olevan kelvollinen postfix-asetusparametrinimi." + +#: VirtualMailManager/handler.py:56 +msgid "an account" +msgstr "tili" + +#: VirtualMailManager/handler.py:57 +msgid "an alias" +msgstr "alias" + +#: VirtualMailManager/handler.py:58 +msgid "a relocated user" +msgstr "sijoitettu käyttäjä" -#: vmm:255 vmm:270 -msgid "Missing alias domain name." -msgstr "Puuttuva aliasverkkotunnuksen nimi." +#: VirtualMailManager/handler.py:84 +msgid "" +"You are not root.\n" +"\tGood bye!\n" +msgstr "" +"Et ole root-käyttäjä.\n" +"\tNäkemiin!\n" + +#: VirtualMailManager/handler.py:104 +#, python-format +msgid "Could not find '%(cfg_file)s' in: '%(cfg_path)s'" +msgstr "Ei voiru löytää ’%(cfg_file)s’ kohteessa: ’%(cfg_path)s’" -#: vmm:276 vmm:285 vmm:293 vmm:323 vmm:331 vmm:339 -msgid "Missing e-mail address." -msgstr "Puuttuva sähköpostiosoite." +# Alla "perms" ja "file" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. Lainausmerkit jätettiin paikallistamatta, koska ne saattavat olla skriptin toiminnan kannalta kriittisiä. +#: VirtualMailManager/handler.py:115 +#, python-format +msgid "" +"wrong permissions for '%(file)s': %(perms)s\n" +"`chmod 0600 %(file)s` would be great." +msgstr "" +"korjaa käyttöoikeudet tiedostolle ’%(file)s’: %(perms)s\n" +"’chmod 0600 %(file)s’ olisi erinomainen." + +#: VirtualMailManager/handler.py:135 +#, python-format +msgid "" +"'%(path)s' is not a directory.\n" +"(%(cfg_file)s: section 'misc', option 'base_directory')" +msgstr "" +"’%(path)s’ ei ole hakemisto.\n" +"(%(cfg_file)s: lohko ’misc’, valitsin ’base_directory’)" -#: vmm:301 -msgid "alias addresses" -msgstr "aliasosoitteet" +#: VirtualMailManager/handler.py:144 +#, python-format +msgid "" +"\n" +"(%(cfg_file)s: section 'bin', option '%(option)s')" +msgstr "" +"\n" +"(%(cfg_file)s: lohko ’bin’, valitsin ’%(option)s’)" + +#: VirtualMailManager/handler.py:158 VirtualMailManager/handler.py:165 +#, python-format +msgid "Unable to import database module '%s'." +msgstr "Ei kyetä tuomaan tietokantamoduulia ’%s’." -# Tässä saattaa olla kirjoitusvirhe. Pitäisi ehkä olla "user's name". -#: vmm:307 -msgid "Missing e-mail address and users name." -msgstr "Puuttuva sähköpostiosoite ja käyttäjien nimi." +#. TP: %(a_type)s will be one of: 'an account', 'an alias' or +#. 'a relocated user' +#: VirtualMailManager/handler.py:244 +#, python-format +msgid "There is already %(a_type)s with the address '%(address)s'." +msgstr "On jo %(a_type)s osoitteessa ’%(address)s’." + +#: VirtualMailManager/handler.py:297 +#, python-format +msgid "'%s' is not a directory." +msgstr "’%s’ ei ole hakemisto." + +#: VirtualMailManager/handler.py:300 +#, python-format +msgid "The file/directory '%s' already exists." +msgstr "Tiedosto/hakemisto ’%s’ on jo olemassa." + +#: VirtualMailManager/handler.py:329 +msgid "Skipped mailbox folders:" +msgstr "Ohitettiin sähköpostilaatikkokansiot:" -# Tässä saattaa olla kirjoitusvirhe. Pitäisi ehkä olla "user's name". -#: vmm:309 -msgid "Missing users name." -msgstr "Puuttuva käyttäjien nimi." +#: VirtualMailManager/handler.py:349 +#, python-format +msgid "UID '%(uid)u' and/or GID '%(gid)u' are less than %(min_uid)u/%(min_gid)u." +msgstr "Käyttäjätunniste ’%(uid)u’ ja/tai käyttäjätunniste ’%(gid)u’ ovat pienempiä kuin %(min_uid)u/%(min_gid)u." + +#: VirtualMailManager/handler.py:354 VirtualMailManager/handler.py:387 +#, python-format +msgid "Found \"..\" in domain directory path: %s" +msgstr "Löytyi ”..” verkkotunnuksen hakemistopolussa: %s" + +#: VirtualMailManager/handler.py:367 +msgid "Detected owner/group mismatch in home directory." +msgstr "Havaittu omistaja-/ryhmätäsmäämättömyys kotihakemistossa." + +#: VirtualMailManager/handler.py:383 +#, python-format +msgid "GID '%(gid)u' is less than '%(min_gid)u'." +msgstr "Käyttäjätunniste ’%(gid)u’ on pienempi kuin ’%(min_gid)u’." + +#: VirtualMailManager/handler.py:394 +#, python-format +msgid "Detected group mismatch in domain directory: %s" +msgstr "Havaittu ryhmätäsmäämättömyys verkkotunnushakemistossa: %s" -#: vmm:315 -msgid "Missing e-mail address and transport." -msgstr "Puuttuva sähköpostiosoite ja siirto." +#: VirtualMailManager/handler.py:470 VirtualMailManager/handler.py:748 +#, python-format +msgid "Unknown service: '%s'" +msgstr "Tuntematon palvelu: ’%s’" + +#: VirtualMailManager/handler.py:587 +#, python-format +msgid "The pattern '%s' contains invalid characters." +msgstr "Säännöllinen lauseke ’%s’ sisältää virheellisiä merkkejä." + +#: VirtualMailManager/handler.py:614 +msgid "Ignored destination addresses:" +msgstr "Ohitetut kohdeosoitteet:" -#: vmm:317 -msgid "Missing transport." -msgstr "Puuttuva siirto." +#: VirtualMailManager/handler.py:619 VirtualMailManager/handler.py:769 +#, python-format +msgid "The destination account/alias '%s' does not exist." +msgstr "Kohdetili/-alias ’%s’ ei ole olemassa." + +# Alla "directory" ja "reason" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. +#: VirtualMailManager/handler.py:641 +#, python-format +msgid "" +"The account has been successfully deleted from the database.\n" +" But an error occurred while deleting the following directory:\n" +" '%(directory)s'\n" +" Reason: %(reason)s" +msgstr "" +"Tili on poistettu onnistuneesti tietokannasta.\n" +" Mutta tapahtui virhe, kun poistettiin seuraavaa hakemistoa:\n" +" ’%(directory)s’\n" +" Syy: %(reason)s" -#: vmm:348 -msgid "Missing alias address and destination." -msgstr "Puuttuva aliasosoite ja -kohde." +#: VirtualMailManager/handler.py:712 +#, python-format +msgid "Could not accept name: '%s'" +msgstr "Ei voitu hyväksyä nimeä: ’%s’" + +#: VirtualMailManager/handler.py:735 +#, python-format +msgid "Could not accept transport: '%s'" +msgstr "Ei voitu hyväksyä siirtoa: ’%s’" -#: vmm:350 vmm:373 -msgid "Missing destination address." -msgstr "Puuttuva kohdeosoite." +#: VirtualMailManager/handler.py:779 VirtualMailManager/relocated.py:98 +#: VirtualMailManager/relocated.py:105 +#, python-format +msgid "The relocated user '%s' does not exist." +msgstr "Sijoitettua käyttäjää ’%s’ ei ole olemassa." + +#: VirtualMailManager/mailbox.py:260 +#, python-format +msgid "Failed to create mailboxes: %r\n" +msgstr "Sähköpostilaatikoiden luominen epäonnistui: %r\n" + +#: VirtualMailManager/maillocation.py:74 +msgid "Empty directory name" +msgstr "Tyhjä hakemistonimi" + +#: VirtualMailManager/maillocation.py:76 +#, python-format +msgid "Directory name is too long: '%s'" +msgstr "Verkkotunnusnimi on liian pitkä: ’%s’" -#: vmm:356 vmm:362 -msgid "Missing alias address" -msgstr "Puuttuva aliasosoite" +#: VirtualMailManager/password.py:388 +#, python-format +msgid "Unsupported password scheme: '%s'" +msgstr "Tukematon salasanakaava: ’%s’" + +#: VirtualMailManager/password.py:391 +#, python-format +msgid "The password scheme '%(scheme)s' requires Dovecot >= v%(version)s." +msgstr "Salasanakaava '%(scheme)s' vaatii Dovecot >= v%(version)s." + +#: VirtualMailManager/password.py:397 +msgid "Encoding suffixes for password schemes require Dovecot >= v1.1.alpha1." +msgstr "Koodausloppuliitteet salasanakaavoille vaativat Dovecot >= v1.1.alpha1." -#: vmm:371 -msgid "Missing relocated address and destination." -msgstr "Puuttuva sijoitusosoite ja -kohde." +#: VirtualMailManager/password.py:400 +#, python-format +msgid "Unsupported password encoding: '%s'" +msgstr "Tukematon salasanakoodaus: ’%s’" + +#: VirtualMailManager/relocated.py:71 +msgid "Address and destination are identical." +msgstr "Osoite ja kohde ovat identtisiä." + +#: VirtualMailManager/relocated.py:75 +#, python-format +msgid "The relocated user '%s' already exists." +msgstr "Sijoitettu käyttäjä ’%s’ on jo olemassa." + +#~ msgid "There is already a relocated user with the address “%s”." +#~ msgstr "On jo sijoitettu käyttäjä osoitteella “%s”." -#: vmm:379 vmm:387 -msgid "Missing relocated address" -msgstr "Puuttuva sijoitusosoite" +#~ msgid "There is already an account with address “%s”." +#~ msgstr "On jo olemassa tili osoitteella “%s”." + +#~ msgid "No destination address for alias denoted." +#~ msgstr "Aliakselle ei ole osoitettu kohdeosoitetta." + +# Alla "a" ja "d" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. +#~ msgid "The alias “%(a)s” with destination “%(d)s” already exists." +#~ msgstr "Alias “%(a)s” kohteella “%(d)s” on jo olemassa." + +# Alla "a" ja "d" ovat muuttujia, jotka korvautuvat suorituksen yhteydessä koodilla. +#~ msgid "The alias “%(a)s” with destination “%(d)s” doesn't exists." +#~ msgstr "Alias “%(a)s” kohteella “%(d)s” ei ole olemassa." -#: vmm:393 -msgid "Missing userid" -msgstr "Puuttuva käyttäjätunniste" +#~ msgid "missing options in section %s:\n" +#~ msgstr "puuttuvia valitsimia lohkossa %s:\n" + +#~ msgid "There are accounts and aliases." +#~ msgstr "On tilejä ja aliaksia." + +#~ msgid "There are accounts." +#~ msgstr "Tilejä on." + +#~ msgid "There are aliases." +#~ msgstr "Aliaksia on." + +#~ msgid "“%s” looks not like an e-mail address." +#~ msgstr "“%s” ei näytä sähköpostiosoitteelta." -#: vmm:406 -msgid "Warnings:" -msgstr "Varoitukset:" +#~ msgid "No localpart specified." +#~ msgstr "Paikallisosaa ei ole määritelty." + +#~ msgid "Either mid or maillocation must be specified." +#~ msgstr "Joko mid-tunniste tai sähköpostisijainti on määriteltävä." + +#~ msgid "mid must be an int/long." +#~ msgstr "tunnisteen mid on oltava tyypiltään int/long." -#: vmm:412 -msgid "from" -msgstr "kohteesta" +#~ msgid "" +#~ "Invalid folder name “%s”, it may consist only of\n" +#~ "1 - 20 single byte characters (A-Z, a-z, 0-9 and _)." +#~ msgstr "" +#~ "Virheellinen kansionimi “%s”, se voi sisältää vain\n" +#~ "1 - 20 yhden tavun mittaisia merkkejä (A-Z, a-z, 0-9 ja _)." + +#~ msgid "Unknown mid specified." +#~ msgstr "Tuntematon mid-määritelty." + +#~ msgid "No destination address for relocated user denoted." +#~ msgstr "Sijoitetulle käyttäjälle ei ole osoitettu kohdeosoitetta." + +#~ msgid "tid must be an int/long." +#~ msgstr "tunnisteen tid on oltava tyypiltään int/long." + +#~ msgid "Unknown tid specified." +#~ msgstr "Tuntematon tid-siirtotunniste määritelty." + +#~ msgid "No “vmm.cfg” found in: /root:/usr/local/etc:/etc" +#~ msgstr "Tiedostoa “vmm.cfg” ei löytynyt hakemistoista: /root:/usr/local/etc:/etc" -#: vmm:412 -msgid "version" -msgstr "versio" +# Alla "options" on muuttuja, joka korvautuu suorituksen yhteydessä koodilla. +#~ msgid "" +#~ "“%(binary)s” doesn't exists.\n" +#~ "(vmm.cfg: section \"bin\", option \"%(option)s\")" +#~ msgstr "" +#~ "“%(binary)s” ei ole olemassa.\n" +#~ "(vmm.cfg: lohko ”bin”, valitsin ”%(option)s”)" -#: vmm:414 -msgid "on" -msgstr "kohteessa" +# Alla "options" on muuttuja, joka korvautuu suorituksen yhteydessä koodilla. +#~ msgid "" +#~ "“%(binary)s” is not executable.\n" +#~ "(vmm.cfg: section \"bin\", option \"%(option)s\")" +#~ msgstr "" +#~ "“%(binary)s” ei ole suoritettava tiedosto.\n" +#~ "(vmm.cfg: lohko ”bin”, valitsin ”%(option)s”)" + +#~ msgid "FATAL: \"..\" in domain directory path detected." +#~ msgstr "VAKAVA VIRHE: havaittu ”..” verkkotunnushakemistopolkussa." + +# Tässä on ilmeisesti kirjoitusvirhe (Configurtion -> Configuration). +#~ msgid "" +#~ "Configurtion error: \"%s\"\n" +#~ "(in section \"connfig\", option \"done\") see also: vmm.cfg(5)\n" +#~ msgstr "" +#~ "Asetusvirhe: ”%s”\n" +#~ "(lohkossa ”connfig”, valitsin ”done”) katso myös: vmm.cfg(5)\n" -#: vmm:488 -msgid "Unknown subcommand" -msgstr "Tuntematon alikomento" +#~ msgid "" +#~ "The keyword “detailed” is deprecated and will be removed in a future release.\n" +#~ " Please use the keyword “full” to get full details." +#~ msgstr "" +#~ "Avainsana “detailed” on vanhentunut ja poistetaan tulevassa julkaisussa.\n" +#~ " Saat kaikki yksityiskohdat käyttämällä avainsanaa “full”." + +#~ msgid "Account doesn't exists" +#~ msgstr "Tiliä ei ole" -#: vmm:491 -msgid "Ouch" -msgstr "Auh" +#~ msgid "" +#~ "The service name “managesieve” is deprecated and will be removed\n" +#~ " in a future release.\n" +#~ " Please use the service name “sieve” instead." +#~ msgstr "" +#~ "Palvelunimi “managesieve” on vanhentunut ja poistetaan\n" +#~ " tulevassa julkaisussa.\n" +#~ " Käytä sen sijaan palvelunimeä “sieve”." + +#~ msgid "" +#~ "Usage: %s SUBCOMMAND OBJECT ARGS*\n" +#~ " short long\n" +#~ " subcommand object args (* = optional)\n" +#~ msgstr "" +#~ "Käyttö: %s ALIKOMENTO OBJEKTI ARGUMENTIT*\n" +#~ " lyhyt pitkä\n" +#~ " alikomento objekti argumentti (* = valinnainen)\n" + +#~ msgid "Available" +#~ msgstr "Käytettävissä" + +#~ msgid "Available domains" +#~ msgstr "Käytettävissä olevat verkkotunnukset" diff -r c0e1fb1b0145 -r a4aead244f75 po/fr.po --- a/po/fr.po Mon Nov 07 03:22:15 2011 +0000 +++ b/po/fr.po Thu Jun 28 19:26:50 2012 +0000 @@ -1,510 +1,819 @@ -# traduction franaise +# traduction française # This file is distributed under the same license as the vmm package. # Copyright (C) 2009 Free Software Foundation, Inc. # Dimitri Duc , 2009. msgid "" msgstr "" -"Project-Id-Version: vmm 0.5.2\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2009-08-25 06:07+0200\n" -"PO-Revision-Date: 2009-09-02 14:59+0100\n" +"Project-Id-Version: vmm 0.6.0\n" +"Report-Msgid-Bugs-To: user+vmm/tp@localhost.localdomain.org\n" +"POT-Creation-Date: 2011-11-07 05:20+0100\n" +"PO-Revision-Date: 2012-01-01 14:59+0100\n" "Last-Translator: Dimitri Duc \n" "Language-Team: French \n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: French\n" "X-Poedit-Country: FRANCE\n" -#: VirtualMailManager/Account.py:36 VirtualMailManager/Relocated.py:44 -#, python-format -msgid "There is already an alias with the address “%s”." -msgstr "Il y a déjà un alias avec l'adresse “%s”." - -#: VirtualMailManager/Account.py:41 VirtualMailManager/Alias.py:45 -#, python-format -msgid "There is already a relocated user with the address “%s”." -msgstr "Il y a déjà un utilisateur relocalisé avec cette adresse “%s”." - -#: VirtualMailManager/Account.py:61 VirtualMailManager/Alias.py:61 -#: VirtualMailManager/Domain.py:163 VirtualMailManager/Domain.py:189 -#: VirtualMailManager/Domain.py:220 VirtualMailManager/Relocated.py:60 -#, python-format -msgid "The domain “%s” doesn't exist yet." +#. TP: Hm, what “quotation marks” should be used? +#. If you are unsure have a look at: +#. http://en.wikipedia.org/wiki/Quotation_mark,_non-English_usage +#: VirtualMailManager/account.py:58 VirtualMailManager/alias.py:35 +#: VirtualMailManager/domain.py:120 VirtualMailManager/relocated.py:38 +#, fuzzy, python-format +msgid "The domain '%s' does not exist." msgstr "Le domaine “%s” n'existe pas encore." -#: VirtualMailManager/Account.py:80 +#: VirtualMailManager/account.py:106 #, python-format -msgid "Unknown service “%s”." -msgstr "Service “%s” inconnu." +msgid "The mailbox format '%(mbfmt)s' requires Dovecot >= v%(version)s." +msgstr "" -#: VirtualMailManager/Account.py:83 VirtualMailManager/Account.py:150 -#: VirtualMailManager/Account.py:178 VirtualMailManager/Account.py:212 +#: VirtualMailManager/account.py:113 VirtualMailManager/account.py:305 #, python-format -msgid "The account “%s” doesn't exists." +msgid "Invalid transport '%(transport)s' for mailbox format '%(mbfmt)s'." +msgstr "" + +#: VirtualMailManager/account.py:153 VirtualMailManager/cli/handler.py:93 +#: VirtualMailManager/handler.py:628 VirtualMailManager/handler.py:679 +#: VirtualMailManager/handler.py:705 VirtualMailManager/handler.py:716 +#: VirtualMailManager/handler.py:727 VirtualMailManager/handler.py:739 +#: VirtualMailManager/handler.py:753 +#, fuzzy, python-format +msgid "The account '%s' does not exist." msgstr "Le compte “%s” n'existe pas." -#: VirtualMailManager/Account.py:145 -#, python-format -msgid "The account “%s” already exists." +#: VirtualMailManager/account.py:204 VirtualMailManager/account.py:214 +#: VirtualMailManager/cli/handler.py:77 VirtualMailManager/handler.py:596 +#, fuzzy, python-format +msgid "The account '%s' already exists." msgstr "Le compte “%s” existe déjà." -#: VirtualMailManager/Account.py:186 +#: VirtualMailManager/account.py:207 VirtualMailManager/handler.py:701 +#, python-format +msgid "Could not accept password: '%s'" +msgstr "" + +#: VirtualMailManager/account.py:217 +#, python-format +msgid "No password set for account: '%s'" +msgstr "" + +#: VirtualMailManager/account.py:245 +#, python-format +msgid "Unknown field: '%s'" +msgstr "" + +#: VirtualMailManager/account.py:267 VirtualMailManager/domain.py:292 +msgid "PostgreSQL-based dictionary quota requires Dovecot >= v1.1.2." +msgstr "" + +#. TP: A service (e.g. pop3 or imap) may be enabled/usable or +#. disabled/unusable for a user. +#: VirtualMailManager/account.py:332 +msgid "disabled" +msgstr "désactiver" + +#: VirtualMailManager/account.py:332 msgid "enabled" msgstr "activer" -#: VirtualMailManager/Account.py:188 -msgid "disabled" -msgstr "désactiver" +#: VirtualMailManager/account.py:343 +#, python-format +msgid "Could not fetch information for account: '%s'" +msgstr "" -#: VirtualMailManager/Account.py:233 -#, python-format -msgid "There are %(count)d aliases with the destination address “%(address)s”." +#: VirtualMailManager/account.py:387 +#, fuzzy, python-format +msgid "There are %(count)d aliases with the destination address '%(address)s'." msgstr "Il y a %(count)d alias ayant comme adresse de destination “%(address)s”." -#: VirtualMailManager/Account.py:241 -msgid "uid must be an int/long." +#: VirtualMailManager/account.py:416 +#, fuzzy +msgid "UID must be an int/long." msgstr "L'identifiant utilisateur doit être de type entier long." -#: VirtualMailManager/Account.py:243 -msgid "uid must be greater than 0." +#: VirtualMailManager/account.py:418 +#, fuzzy +msgid "UID must be greater than 0." msgstr "L'idenfiant utilisateur doit être plus grand que 0." -#: VirtualMailManager/Account.py:251 -#, python-format -msgid "There is no account with the UID “%d”." +#: VirtualMailManager/account.py:427 +#, fuzzy, python-format +msgid "There is no account with the UID: '%d'" msgstr "Il n'y a pas de compte avec l'identifiant utilisateur “%d”." -#: VirtualMailManager/Alias.py:30 VirtualMailManager/Relocated.py:30 -msgid "Address and destination are identical." -msgstr "L'adresse et la destination sont identique. " - -#: VirtualMailManager/Alias.py:40 VirtualMailManager/Relocated.py:39 -#, python-format -msgid "There is already an account with address “%s”." -msgstr "Il y a déjà un compte avec l'adresse “%s”." - -#: VirtualMailManager/Alias.py:71 -#, python-format +#: VirtualMailManager/alias.py:60 +#, fuzzy, python-format msgid "" -"Can't add new destination to alias “%(address)s”.\n" -"Currently this alias expands into %(count)i recipients.\n" -"One more destination will render this alias unusable.\n" -"Hint: Increase Postfix' virtual_alias_expansion_limit\n" +"Cannot add %(count_new)i new destination(s) to alias '%(address)s'.\n" +"Currently this alias expands into %(count)i/%(limit)i recipients.\n" +"%(count_new)i additional destination(s) will render this alias unusable.\n" +"Hint: Increase Postfix' virtual_alias_expansion_limit" msgstr "" "Impossible d'ajouter une nouvelle destination à l'alias “%(address)s”.\n" "Actuellement cet alias s'étend sur %(count)i destinataires.\n" "Une destination de plus rendra cet alias inutilisable.\n" "Conseil: Augmenter 'virtual_alias_expansion_limit' dans Postfix\n" -#: VirtualMailManager/Alias.py:80 -msgid "No destination address for alias denoted." -msgstr "Pas de destination à l'adresse de l'alias relevé." - -#: VirtualMailManager/Alias.py:91 +#: VirtualMailManager/alias.py:67 #, python-format -msgid "The alias “%(a)s” with destination “%(d)s” already exists." -msgstr "L'alias “%(a)s” avec la destination “%(d)s” existe déjà." +msgid "" +"Cannot add %(count_new)i new destination(s) to alias '%(address)s'.\n" +"This alias already exceeds its expansion limit (%(count)i/%(limit)i).\n" +"So its unusable, all messages addressed to this alias will be bounced.\n" +"Hint: Delete some destination addresses." +msgstr "" -#: VirtualMailManager/Alias.py:106 VirtualMailManager/Alias.py:123 -#, python-format -msgid "The alias “%s” doesn't exists." +#: VirtualMailManager/alias.py:142 VirtualMailManager/alias.py:154 +#: VirtualMailManager/alias.py:161 VirtualMailManager/handler.py:657 +#, fuzzy, python-format +msgid "The alias '%s' does not exist." msgstr "L'alias “%s” n'existe pas." -#: VirtualMailManager/Alias.py:125 +#: VirtualMailManager/alias.py:145 #, python-format -msgid "The alias “%(a)s” with destination “%(d)s” doesn't exists." -msgstr "L'alias “%(a)s” avec la destination “%(d)s” n'existe pas." +msgid "The address '%(addr)s' is not a destination of the alias '%(alias)s'." +msgstr "" -#: VirtualMailManager/AliasDomain.py:32 -#, python-format -msgid "The domain “%s” is a primary domain." +#: VirtualMailManager/aliasdomain.py:50 +#, fuzzy, python-format +msgid "The domain '%s' is a primary domain." msgstr "Le domaine “%s” est le domaine principale." -#: VirtualMailManager/AliasDomain.py:37 -#, python-format -msgid "The alias domain “%s” already exists." +#: VirtualMailManager/aliasdomain.py:69 +#, fuzzy, python-format +msgid "The alias domain '%s' already exists." msgstr "L'alias de domane “%s” existe déjà." -#: VirtualMailManager/AliasDomain.py:40 VirtualMailManager/AliasDomain.py:70 -msgid "No destination domain for alias domain denoted." +#: VirtualMailManager/aliasdomain.py:72 VirtualMailManager/aliasdomain.py:106 +#, fuzzy +msgid "No destination domain set for the alias domain." msgstr "Pas de destination pour l'alias de domaine relevé." -#: VirtualMailManager/AliasDomain.py:43 VirtualMailManager/AliasDomain.py:73 -#, python-format -msgid "The target domain “%s” doesn't exist yet." +#: VirtualMailManager/aliasdomain.py:75 VirtualMailManager/aliasdomain.py:109 +#, fuzzy, python-format +msgid "The target domain '%s' does not exist." msgstr "La domaine ciblé “%s” n'existe pas encore." -#: VirtualMailManager/AliasDomain.py:62 -#, python-format -msgid "There is no primary domain for the alias domain “%s”." +#: VirtualMailManager/aliasdomain.py:88 VirtualMailManager/aliasdomain.py:112 +#: VirtualMailManager/aliasdomain.py:133 +#, fuzzy, python-format +msgid "The alias domain '%s' does not exist." +msgstr "L'alias de domaine “%s” n'existe pas encore." + +#: VirtualMailManager/aliasdomain.py:98 +#, fuzzy, python-format +msgid "There is no primary domain for the alias domain '%s'." msgstr "Il n'y a pas de domaine principale pour l'alias de domaine “%s”." -#: VirtualMailManager/AliasDomain.py:65 VirtualMailManager/AliasDomain.py:76 -#: VirtualMailManager/AliasDomain.py:99 -#, python-format -msgid "The alias domain “%s” doesn't exist yet." -msgstr "L'alias de domaine “%s” n'existe pas encore." - -#: VirtualMailManager/AliasDomain.py:79 -#, python-format -msgid "The alias domain “%(alias)s” is already assigned to the domain “%(domain)s”." +#: VirtualMailManager/aliasdomain.py:115 +#, fuzzy, python-format +msgid "The alias domain '%(alias)s' is already assigned to the domain '%(domain)s'." msgstr "L'alias de domaine “%(alias)s” est déjà assigné au domaine “%(domain)s”." -#: VirtualMailManager/Config.py:102 VirtualMailManager/Config.py:137 +#. TP: Please preserve the trailing space. +#: VirtualMailManager/cli/__init__.py:78 +msgid "Enter new password: " +msgstr "Entrer un nouveau mot de passe:" + +#. TP: Please preserve the trailing space. +#: VirtualMailManager/cli/__init__.py:80 +msgid "Retype new password: " +msgstr "Re-écrire le mot de passe:" + +#: VirtualMailManager/cli/__init__.py:85 VirtualMailManager/cli/config.py:53 +msgid "Too many failures - try again later." +msgstr "" + +#: VirtualMailManager/cli/__init__.py:91 +#, fuzzy +msgid "Sorry, passwords do not match." +msgstr "Le mot de passe ne correspondent pas" + +#: VirtualMailManager/cli/__init__.py:95 +#, fuzzy +msgid "Sorry, empty passwords are not permitted." +msgstr "Les mots de passe vides ne sont pas autorisées" + +#: VirtualMailManager/cli/config.py:32 +#, fuzzy, python-format +msgid "Enter new value for option %(option)s [%(current_value)s]: " +msgstr "Entrer une nouvelle valeur pour l'option %(opt)s [%(val)s]: " + +#: VirtualMailManager/cli/config.py:36 #, python-format msgid "Using configuration file: %s\n" msgstr "Utilisation du fichier configuration: %s\n" -#: VirtualMailManager/Config.py:106 -#, python-format -msgid "missing section: %s\n" -msgstr "absence de la section: %s\n" - -#: VirtualMailManager/Config.py:108 -#, python-format -msgid "missing options in section %s:\n" -msgstr "absence des options dans la section %s:\n" - -#: VirtualMailManager/Config.py:140 -#, python-format -msgid "* Config section: “%s”" +#: VirtualMailManager/cli/config.py:38 +#, fuzzy, python-format +msgid "* Configuration section: '%s'" msgstr "* Configure la section: “%s”" -#: VirtualMailManager/Config.py:143 -#, python-format -msgid "Enter new value for option %(opt)s [%(val)s]: " -msgstr "Entrer une nouvelle valeur pour l'option %(opt)s [%(val)s]: " +#: VirtualMailManager/cli/config.py:50 +#, fuzzy, python-format +msgid "Warning: %s" +msgstr "Avertissements:" + +#: VirtualMailManager/cli/handler.py:66 +#, fuzzy, python-format +msgid "Invalid section: '%s'" +msgstr "Section invalide: “%s”" -#: VirtualMailManager/Domain.py:39 -#, python-format -msgid "The domain “%s” is an alias domain." -msgstr "Le domaine “%s” est un alias de domaine." +#: VirtualMailManager/cli/main.py:32 VirtualMailManager/cli/main.py:65 +#: VirtualMailManager/cli/main.py:68 VirtualMailManager/cli/subcommands.py:629 +#: VirtualMailManager/cli/subcommands.py:649 +#, fuzzy, python-format +msgid "Error: %s" +msgstr "Erreur" -#: VirtualMailManager/Domain.py:124 -msgid "There are accounts and aliases." -msgstr "Il y a des comptes et des alias." +#: VirtualMailManager/cli/main.py:41 +msgid "You must specify a subcommand at least." +msgstr "" + +#: VirtualMailManager/cli/main.py:53 +#, fuzzy, python-format +msgid "Unknown subcommand: '%s'" +msgstr "Sous-commande inconue." -#: VirtualMailManager/Domain.py:127 -msgid "There are accounts." -msgstr "Il y a des comptes." +#. TP: We have to cry, because root has killed/interrupted vmm +#. with Ctrl+C or Ctrl+D. +#: VirtualMailManager/cli/main.py:62 +#, fuzzy +msgid "Ouch!" +msgstr "Aie" -#: VirtualMailManager/Domain.py:130 -msgid "There are aliases." -msgstr "Il y a des alias." +#: VirtualMailManager/cli/main.py:71 +#, python-format +msgid "Error: Unknown section: '%s'" +msgstr "" -#: VirtualMailManager/Domain.py:145 +#: VirtualMailManager/cli/main.py:74 #, python-format -msgid "The domain “%s” already exists." -msgstr "Le domaine “%s” existe déjà." +msgid "Error: No option '%(option)s' in section: '%(section)s'" +msgstr "" -#: VirtualMailManager/EmailAddress.py:46 +#: VirtualMailManager/cli/main.py:77 +msgid "Warnings:" +msgstr "Avertissements:" + +#: VirtualMailManager/cli/subcommands.py:78 #, python-format -msgid "Missing '@' sign in e-mail address “%s”." -msgstr "Absence du signe '@' dans l'adresse e-mail “%s”. " +msgid "Plan A failed ... trying Plan B: %(subcommand)s %(object)s" +msgstr "" -#: VirtualMailManager/EmailAddress.py:49 -#, python-format -msgid "“%s” looks not like an e-mail address." -msgstr "“%s” ne ressemble pas à une adresse e-mail." +#: VirtualMailManager/cli/subcommands.py:92 +msgid "Missing alias address and destination." +msgstr "Absence de l'adresse alias et de la destination." + +#: VirtualMailManager/cli/subcommands.py:95 +#: VirtualMailManager/cli/subcommands.py:453 +msgid "Missing destination address." +msgstr "Absence de l'adresse destination." -#: VirtualMailManager/EmailAddress.py:54 -#, python-format -msgid "Missing domain name after “%s@”." -msgstr "Absence d'un nom de domaine après “%s@”" +#: VirtualMailManager/cli/subcommands.py:102 +#: VirtualMailManager/cli/subcommands.py:112 +#, fuzzy +msgid "Missing alias address." +msgstr "Absence de l'adresse alias" -#: VirtualMailManager/EmailAddress.py:66 -msgid "No localpart specified." -msgstr "Pas de partie locale spécifié." +#: VirtualMailManager/cli/subcommands.py:134 +#: VirtualMailManager/cli/subcommands.py:168 +#, fuzzy +msgid "Missing alias domain name and destination domain name." +msgstr "Absence du nom de l'alias de domaine et du nom cible de domaine" + +#: VirtualMailManager/cli/subcommands.py:137 +#: VirtualMailManager/cli/subcommands.py:171 +#, fuzzy +msgid "Missing destination domain name." +msgstr "Asbence du nom cible de domaine" -#: VirtualMailManager/EmailAddress.py:69 -#, python-format -msgid "The local part “%s” is too long" -msgstr "La partie locale “%s” est trop longue." +#: VirtualMailManager/cli/subcommands.py:145 +#: VirtualMailManager/cli/subcommands.py:152 +msgid "Missing alias domain name." +msgstr "Absence du nom de l'alias de domaine." -#: VirtualMailManager/EmailAddress.py:76 -#, python-format -msgid "The local part “%(lpart)s” contains invalid characters: %(ichrs)s" -msgstr "La partie locale “%(lpart)s” contient des caractères invalide: %(ichrs)s" +#: VirtualMailManager/cli/subcommands.py:179 +#, fuzzy +msgid "Missing option name." +msgstr "Absence de nom de domaine." -#: VirtualMailManager/MailLocation.py:32 -msgid "Either mid or maillocation must be specified." -msgstr "Soit l'identifiant de l'emplacement e-mail ou l'emplacement e-mail doit être spécifié. " +#: VirtualMailManager/cli/subcommands.py:195 +#, fuzzy +msgid "Missing option and new value." +msgstr "Absence de nom de domaine et de nouveau transport." -#: VirtualMailManager/MailLocation.py:38 -msgid "mid must be an int/long." -msgstr "L'identiant de l'emplcement e-mail doit être un entier long." +#: VirtualMailManager/cli/subcommands.py:197 +#, fuzzy +msgid "Missing new configuration value." +msgstr "Utilisation du fichier configuration: %s\n" -#: VirtualMailManager/MailLocation.py:46 +#: VirtualMailManager/cli/subcommands.py:213 +#: VirtualMailManager/cli/subcommands.py:229 +#: VirtualMailManager/cli/subcommands.py:242 +#: VirtualMailManager/cli/subcommands.py:331 +msgid "Missing domain name." +msgstr "Absence de nom de domaine." + +#: VirtualMailManager/cli/subcommands.py:219 #, python-format -msgid "" -"Invalid folder name “%s”, it may consist only of\n" -"1 - 20 single byte characters (A-Z, a-z, 0-9 and _)." +msgid "Creating account for postmaster@%s" msgstr "" -"Le nom du fichier “%s” est invalid, il peut comporté\n" -"entre 1 à 20 caractères (A-Z, a-z, 0-9 est _)." + +#: VirtualMailManager/cli/subcommands.py:235 +#: VirtualMailManager/cli/subcommands.py:249 +#: VirtualMailManager/cli/subcommands.py:322 +#: VirtualMailManager/cli/subcommands.py:343 +#: VirtualMailManager/cli/subcommands.py:372 +#: VirtualMailManager/cli/subcommands.py:509 +#: VirtualMailManager/cli/subcommands.py:522 VirtualMailManager/handler.py:453 +#: VirtualMailManager/handler.py:466 VirtualMailManager/handler.py:481 +#: VirtualMailManager/handler.py:510 VirtualMailManager/handler.py:674 +#, fuzzy, python-format +msgid "Invalid argument: '%s'" +msgstr "Paramètre invalide: “%s”" + +#: VirtualMailManager/cli/subcommands.py:267 +#: VirtualMailManager/cli/subcommands.py:273 +msgid "Domain" +msgstr "Domaine" -#: VirtualMailManager/MailLocation.py:59 -msgid "Unknown mid specified." -msgstr "Identifiant de l'emplacement e-mail specifié est inconnu." +#: VirtualMailManager/cli/subcommands.py:275 +#: VirtualMailManager/cli/subcommands.py:284 +msgid "accounts" +msgstr "comptes" -#: VirtualMailManager/Relocated.py:65 -msgid "No destination address for relocated user denoted." -msgstr "Pas d'adresse destination pour l'utilisateur relocalisé." +#: VirtualMailManager/cli/subcommands.py:277 +#: VirtualMailManager/cli/subcommands.py:283 +#: VirtualMailManager/cli/subcommands.py:831 +msgid "alias domains" +msgstr "Alias des domaines" + +#: VirtualMailManager/cli/subcommands.py:279 +#: VirtualMailManager/cli/subcommands.py:285 +msgid "aliases" +msgstr "Alias" + +#: VirtualMailManager/cli/subcommands.py:281 +#: VirtualMailManager/cli/subcommands.py:286 +msgid "relocated users" +msgstr "Utilisateurs relocalisés" -#: VirtualMailManager/Relocated.py:75 -#, python-format -msgid "The relocated user “%s” already exists." -msgstr "Le transfére de l'utilisateur “%s” existe déjà." +#: VirtualMailManager/cli/subcommands.py:292 +#, fuzzy +msgid "Missing domain name and storage value." +msgstr "Absence de nom de domaine et de nouveau transport." -#: VirtualMailManager/Relocated.py:89 VirtualMailManager/Relocated.py:102 +#: VirtualMailManager/cli/subcommands.py:295 +#: VirtualMailManager/cli/subcommands.py:582 +#, fuzzy +msgid "Missing storage value." +msgstr "Absence du nom des utilisateurs." + +#: VirtualMailManager/cli/subcommands.py:301 +#: VirtualMailManager/cli/subcommands.py:586 +#, fuzzy, python-format +msgid "Invalid storage value: '%s'" +msgstr "Paramètre invalide: “%s”" + +#: VirtualMailManager/cli/subcommands.py:311 #, python-format -msgid "The relocated user “%s” doesn't exists." -msgstr "L'emplacement de l'utilisateur “%s” n'existe pas." +msgid "Neither a valid number of messages nor the keyword 'force': '%s'" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:319 +#: VirtualMailManager/cli/subcommands.py:595 +#, python-format +msgid "Not a valid number of messages: '%s'" +msgstr "" -#: VirtualMailManager/Transport.py:29 -msgid "Either tid or transport must be specified." -msgstr "Soit l'identifiant du transport ou le transport doit être spécifié. " +#: VirtualMailManager/cli/subcommands.py:354 +#: VirtualMailManager/cli/subcommands.py:609 +#, fuzzy, python-format +msgid "Invalid service arguments: %s" +msgstr "Paramètre invalide: “%s”" -#: VirtualMailManager/Transport.py:35 -msgid "tid must be an int/long." -msgstr "L'identifiant du transport doit être un entier long." +#: VirtualMailManager/cli/subcommands.py:363 +msgid "Missing domain name and new transport." +msgstr "Absence de nom de domaine et de nouveau transport." + +#: VirtualMailManager/cli/subcommands.py:366 +msgid "Missing new transport." +msgstr "Absence de nouveau transport." -#: VirtualMailManager/Transport.py:63 -msgid "Unknown tid specified." -msgstr "L'identifiant du transport spécifié est inconnu." +#: VirtualMailManager/cli/subcommands.py:380 +#, fuzzy +msgid "Missing UID." +msgstr "Absence d'identiant utilisateur" -#: VirtualMailManager/VirtualMailManager.py:54 -msgid "" -"You are not root.\n" -"\tGood bye!\n" -msgstr "" -"Vous n'êtes pas super-utilisateur (root).\n" -"\tAu revoir!\n" +#: VirtualMailManager/cli/subcommands.py:381 +#: VirtualMailManager/cli/subcommands.py:545 +#: VirtualMailManager/cli/subcommands.py:551 +#, fuzzy +msgid "Account" +msgstr "comptes" -#: VirtualMailManager/VirtualMailManager.py:74 -msgid "No “vmm.cfg” found in: /root:/usr/local/etc:/etc" -msgstr "Pas de “vmm.cfg” trouvé dans: /root:/usr/local/etc:/etc" - -#: VirtualMailManager/VirtualMailManager.py:85 +#: VirtualMailManager/cli/subcommands.py:396 #, python-format -msgid "" -"fix permissions (%(perms)s) for “%(file)s”\n" -"`chmod 0600 %(file)s` would be great." +msgid "Unknown help topic: '%s'" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:409 +msgid "List of available subcommands:" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:430 +msgid "Usable encoding suffixes:" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:430 +msgid "Usable password schemes:" msgstr "" -"Changer les droits de permissions (%(perms)s) pour “%(file)s”\n" -"`chmod 0600 %(file)s` serait mieux." + +#: VirtualMailManager/cli/subcommands.py:451 +msgid "Missing relocated address and destination." +msgstr "Absence d'adresse relocalisée et de la destination." + +#: VirtualMailManager/cli/subcommands.py:460 +#: VirtualMailManager/cli/subcommands.py:467 +#, fuzzy +msgid "Missing relocated address." +msgstr "Absence d'adresse relocalisée" + +#: VirtualMailManager/cli/subcommands.py:490 +#: VirtualMailManager/cli/subcommands.py:503 +#: VirtualMailManager/cli/subcommands.py:516 +#: VirtualMailManager/cli/subcommands.py:568 +#: VirtualMailManager/cli/subcommands.py:603 +msgid "Missing e-mail address." +msgstr "Absence de l'adresse e-mail." + +#: VirtualMailManager/cli/subcommands.py:497 +#, fuzzy, python-format +msgid "Generated password: %s" +msgstr "Entrer un nouveau mot de passe:" -#: VirtualMailManager/VirtualMailManager.py:100 -#, python-format -msgid "" -"“%s” is not a directory.\n" -"(vmm.cfg: section \"domdir\", option \"base\")" +#: VirtualMailManager/cli/subcommands.py:552 +msgid "alias addresses" +msgstr "Adresses alias" + +#: VirtualMailManager/cli/subcommands.py:558 +#, fuzzy +msgid "Missing e-mail address and user's name." +msgstr "Absence de l'adresse e-mail et du nom des utilisateurs." + +#: VirtualMailManager/cli/subcommands.py:561 +#, fuzzy +msgid "Missing user's name." +msgstr "Absence du nom des utilisateurs." + +#: VirtualMailManager/cli/subcommands.py:579 +#, fuzzy +msgid "Missing e-mail address and storage value." +msgstr "Absence de l'adresse e-mail et du nom des utilisateurs." + +#: VirtualMailManager/cli/subcommands.py:617 +msgid "Missing e-mail address and transport." +msgstr "Absence de l'adresse e-mail et du transport." + +#: VirtualMailManager/cli/subcommands.py:620 +msgid "Missing transport." +msgstr "Absence du transport." + +#: VirtualMailManager/cli/subcommands.py:630 +msgid "usage: " msgstr "" -"“%s” n'est pas un répertoire.\n" -"(vmm.cfg: section \"domdir\", option \"base\")" -#: VirtualMailManager/VirtualMailManager.py:105 +#. TP: Please adjust translated words like the original text. +#. (It's a table header.) Extract from usage text: +#. usage: vmm subcommand arguments +#. short long +#. subcommand arguments +#. +#. da domainadd fqdn [transport] +#. dd domaindelete fqdn [force] +#: VirtualMailManager/cli/subcommands.py:640 #, python-format msgid "" -"“%(binary)s” doesn't exists.\n" -"(vmm.cfg: section \"bin\", option \"%(option)s\")" +"usage: %s subcommand arguments\n" +" short long\n" +" subcommand arguments\n" msgstr "" -"“%(binary)s” n'existe pas.\n" -"(vmm.cfg: section \"bin\", option \"%(option)s\")" + +#: VirtualMailManager/cli/subcommands.py:659 +msgid "from" +msgstr "de" -#: VirtualMailManager/VirtualMailManager.py:109 -#, python-format -msgid "" -"“%(binary)s” is not executable.\n" -"(vmm.cfg: section \"bin\", option \"%(option)s\")" +#. TP: The words 'from', 'version' and 'on' are used in +#. the version information, e.g.: +#. vmm, version 0.5.2 (from 09/09/09) +#. Python 2.5.4 on FreeBSD +#: VirtualMailManager/cli/subcommands.py:659 +msgid "version" +msgstr "version" + +#: VirtualMailManager/cli/subcommands.py:662 +msgid "on" +msgstr "sur" + +#: VirtualMailManager/cli/subcommands.py:664 +msgid "is free software and comes with ABSOLUTELY NO WARRANTY." msgstr "" -"“%(binary)s” n'est pas executable.\n" -"(vmm.cfg: section \"bin\", option \"%(option)s\")" + +#: VirtualMailManager/cli/subcommands.py:672 +msgid "uid" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:166 -msgid "The domain name is too long." -msgstr "Le nom du domaine est trop long." +#: VirtualMailManager/cli/subcommands.py:673 +msgid "get the address of the user with the given UID" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:169 -#, python-format -msgid "The domain name “%s” is invalid." -msgstr "Le nom du domaine “%s” est invalide." +#: VirtualMailManager/cli/subcommands.py:674 +#: VirtualMailManager/cli/subcommands.py:684 +msgid "address [password]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:675 +msgid "create a new e-mail user with the given address" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:209 -msgid "Enter new password: " -msgstr "Entrer un nouveau mot de passe:" +#: VirtualMailManager/cli/subcommands.py:677 +#: VirtualMailManager/cli/subcommands.py:704 +#: VirtualMailManager/cli/subcommands.py:744 +#: VirtualMailManager/cli/subcommands.py:746 +#, fuzzy +msgid "address" +msgstr "Adresses alias" -#: VirtualMailManager/VirtualMailManager.py:210 -msgid "Retype new password: " -msgstr "Re-écrire le mot de passe:" +#: VirtualMailManager/cli/subcommands.py:678 +msgid "delete the specified user" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:212 -msgid "Sorry, passwords do not match" -msgstr "Le mot de passe ne correspondent pas" +#: VirtualMailManager/cli/subcommands.py:679 +msgid "address [details]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:680 +msgid "display information about the given address" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:216 -msgid "Sorry, empty passwords are not permitted" -msgstr "Les mots de passe vides ne sont pas autorisées" +#: VirtualMailManager/cli/subcommands.py:681 +msgid "address name" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:682 +msgid "set or update the real name for an address" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:685 +msgid "update the password for the given address" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:687 +msgid "address storage [messages]" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:265 -#: VirtualMailManager/VirtualMailManager.py:352 -#, python-format -msgid "No such directory: %s" -msgstr "Pas de répertoire: %s" +#: VirtualMailManager/cli/subcommands.py:688 +msgid "update the quota limit for the given address" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:690 +msgid "address [service ...]" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:340 -msgid "Found \"..\" in home directory path." -msgstr "\"..\" trouvé dans le chemins d'accès du répertoire racine de l'utilisateur (home)" +#: VirtualMailManager/cli/subcommands.py:691 +msgid "enables the specified services and disables all not specified services" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:348 -msgid "Owner/group mismatch in home directory detected." -msgstr "Défaut d'appariement détecté entre les droits propriétaire/groupe avec le répertoire de l'utilisateur (home)" +#: VirtualMailManager/cli/subcommands.py:694 +#, fuzzy +msgid "address transport" +msgstr "Absence du transport." -#: VirtualMailManager/VirtualMailManager.py:364 -msgid "FATAL: \"..\" in domain directory path detected." -msgstr "FATAL: \"..\" dans le chemins d'accès du réperoire domaine." +#: VirtualMailManager/cli/subcommands.py:695 +msgid "update the transport of the given address" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:370 -msgid "FATAL: group mismatch in domain directory detected" -msgstr "Défaut d'appariement détecté dans le répertoire domaine" +#: VirtualMailManager/cli/subcommands.py:697 +#, fuzzy +msgid "address destination ..." +msgstr "L'adresse et la destination sont identique. " + +#: VirtualMailManager/cli/subcommands.py:698 +msgid "create a new alias e-mail address with one or more destinations" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:457 -#, python-format -msgid "" -"Configurtion error: \"%s\"\n" -"(in section \"connfig\", option \"done\") see also: vmm.cfg(5)\n" +#: VirtualMailManager/cli/subcommands.py:701 +#, fuzzy +msgid "address [destination]" +msgstr "Absence de l'adresse alias et de la destination." + +#: VirtualMailManager/cli/subcommands.py:702 +msgid "delete the specified alias e-mail address or one of its destinations" msgstr "" -"Erreur de configuration: \"%s\"\n" -"(dans la section \"connfig\", option \"done\") voir aussi: vmm.cfg(5)\n" + +#: VirtualMailManager/cli/subcommands.py:705 +msgid "show the destination(s) of the specified alias" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:477 -#, python-format -msgid "Invalid section: “%s”" -msgstr "Section invalide: “%s”" +#: VirtualMailManager/cli/subcommands.py:708 +#: VirtualMailManager/cli/subcommands.py:717 +msgid "fqdn destination" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:709 +msgid "create a new alias for an existing domain" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:487 -#: VirtualMailManager/VirtualMailManager.py:497 -#: VirtualMailManager/VirtualMailManager.py:516 -#: VirtualMailManager/VirtualMailManager.py:624 -#: VirtualMailManager/VirtualMailManager.py:655 -#, python-format -msgid "Invalid argument: “%s”" -msgstr "Paramètre invalide: “%s”" +#: VirtualMailManager/cli/subcommands.py:711 +#: VirtualMailManager/cli/subcommands.py:714 +#: VirtualMailManager/cli/subcommands.py:723 +msgid "fqdn" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:712 +msgid "delete the specified alias domain" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:715 +#, fuzzy +msgid "show the destination of the given alias domain" +msgstr "Pas de destination pour l'alias de domaine relevé." -#: VirtualMailManager/VirtualMailManager.py:520 -msgid "" -"The keyword “detailed” is deprecated and will be removed in a future release.\n" -" Please use the keyword “full” to get full details." +#: VirtualMailManager/cli/subcommands.py:718 +#, fuzzy +msgid "assign the given alias domain to an other domain" +msgstr "Absence du nom de l'alias de domaine et du nom cible de domaine" + +#: VirtualMailManager/cli/subcommands.py:720 +#, fuzzy +msgid "fqdn [transport]" +msgstr "Absence du transport." + +#: VirtualMailManager/cli/subcommands.py:721 +msgid "create a new domain" msgstr "" -"Le mot-clé “detailed” est obselète et va être supprimé dans la futur version.\n" -" Utiliser, plutôt, le mot “full” pour avoir tous les détails." + +#: VirtualMailManager/cli/subcommands.py:724 +msgid "delete the given domain and all its alias domains" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:593 -#, python-format -msgid "The pattern “%s” contains invalid characters." -msgstr "Le modèle “%s” contient des charactères invalides." +#: VirtualMailManager/cli/subcommands.py:725 +msgid "fqdn [details]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:726 +msgid "display information about the given domain" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:728 +msgid "fqdn storage [messages]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:729 +msgid "update the quota limit of the specified domain" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:619 -#, python-format -msgid "The destination account/alias “%s” doesn't exists yet." -msgstr "La destination compte/alias “%s” n'existe pas." +#: VirtualMailManager/cli/subcommands.py:731 +msgid "fqdn [service ...]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:732 +msgid "enables the specified services and disables all not specified services of the given domain" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:636 -#, python-format -msgid "" -"The account has been successfully deleted from the database.\n" -" But an error occurred while deleting the following directory:\n" -" “%(directory)s”\n" -" Reason: %(raeson)s" +#: VirtualMailManager/cli/subcommands.py:735 +#, fuzzy +msgid "fqdn transport" +msgstr "Absence du transport." + +#: VirtualMailManager/cli/subcommands.py:736 +#, fuzzy +msgid "update the transport of the specified domain" +msgstr "Soit l'identifiant du transport ou le transport doit être spécifié. " + +#: VirtualMailManager/cli/subcommands.py:737 +msgid "[pattern]" msgstr "" -"Le compte a été supprimé de la base de donnée avec succès.\n" -" Mais une erreur c'est produite lors de la suppression du répertoire suivant:\n" -" “%(directory)s”\n" -" Raison: %(raeson)s" + +#: VirtualMailManager/cli/subcommands.py:738 +msgid "list all domains / search domains by pattern" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:676 -msgid "Account doesn't exists" -msgstr "Le compte n'existe pas." +#: VirtualMailManager/cli/subcommands.py:741 +#, fuzzy +msgid "address newaddress" +msgstr "Adresses alias" + +#: VirtualMailManager/cli/subcommands.py:742 +msgid "create a new record for a relocated user" +msgstr "" -#: VirtualMailManager/VirtualMailManager.py:692 -#: VirtualMailManager/VirtualMailManager.py:702 -msgid "" -"The service name “managesieve” is deprecated and will be removed\n" -" in a future release.\n" -" Please use the service name “sieve” instead." +#: VirtualMailManager/cli/subcommands.py:745 +msgid "delete the record of the relocated user" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:747 +msgid "print information about a relocated user" msgstr "" -"Le nom de service “managesieve” est obselète et va être supprimé\n" -" dans la futur version.\n" -" Utiliser, plutôt, le nom de service “sieve”." + +#: VirtualMailManager/cli/subcommands.py:749 +msgid "option" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:750 +msgid "show the actual value of the configuration option" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:751 +msgid "option value" +msgstr "" -#: VirtualMailManager/ext/Postconf.py:44 -#, python-format -msgid "The value “%s” looks not like a valid postfix configuration parameter name." -msgstr "La valeur “%s” ne semble pas être un nom valide pour les paramètres de configuration postfix. " +#: VirtualMailManager/cli/subcommands.py:752 +msgid "set a new value for the configuration option" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:753 +msgid "[section]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:754 +msgid "start interactive configuration modus" +msgstr "" -#: vmm:34 +#: VirtualMailManager/cli/subcommands.py:756 +msgid "lists all usable password schemes and password encoding suffixes" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:758 +#, fuzzy +msgid "[subcommand]" +msgstr "Sous-commande inconue." + +#: VirtualMailManager/cli/subcommands.py:759 +msgid "show a help overview or help for the given subcommand" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:761 +msgid "show version and copyright information" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:809 #, python-format -msgid "" -"Usage: %s SUBCOMMAND OBJECT ARGS*\n" -" short long\n" -" subcommand object args (* = optional)\n" +msgid "[%(percent)s%%] %(used)s/%(limit)s" msgstr "" -"Usage: %s SOUSCOMMANDE OBJET ARGS*\n" -" short long\n" -" souscommande objet args (* = facultatif)\n" -#: vmm:73 vmm:84 vmm:494 -msgid "Error" -msgstr "Erreur" - -#: vmm:111 +#. TP: used in e.g. 'Domain information' or 'Account information' +#: VirtualMailManager/cli/subcommands.py:815 msgid "information" msgstr "information" -#: vmm:121 -msgid "Available" -msgstr "Disponible" +#. TP: used in e.g. 'Existing alias addresses' or 'Existing accounts' +#: VirtualMailManager/cli/subcommands.py:828 +msgid "Existing" +msgstr "" -#: vmm:124 vmm:223 vmm:229 -msgid "alias domains" -msgstr "Alias des domaines" - -#: vmm:134 vmm:145 vmm:169 +#: VirtualMailManager/cli/subcommands.py:841 +#: VirtualMailManager/cli/subcommands.py:883 msgid "\tNone" msgstr "\tAucun" -#: vmm:138 +#: VirtualMailManager/cli/subcommands.py:846 msgid "Alias information" msgstr "Information sur alias" -#: vmm:140 +#: VirtualMailManager/cli/subcommands.py:848 #, python-format msgid "\tMail for %s will be redirected to:" msgstr "\tLes courriels pour %s vont être redirigé à:" -#: vmm:149 +#: VirtualMailManager/cli/subcommands.py:855 msgid "Relocated information" msgstr "Information sur la relocalisation" -#: vmm:151 -#, python-format -msgid "\tUser “%(addr)s” has moved to “%(dest)s”" +#: VirtualMailManager/cli/subcommands.py:857 +#, fuzzy, python-format +msgid "\tUser '%(addr)s' has moved to '%(dest)s'" msgstr "\tUtilisateur “%(addr)s” a été déplacée à “%(dest)s”" -#: vmm:164 -msgid "Available domains" -msgstr "Domaines disponible" - -#: vmm:166 +#: VirtualMailManager/cli/subcommands.py:872 msgid "Matching domains" msgstr "Domaines concordant" -#: vmm:180 +#: VirtualMailManager/cli/subcommands.py:874 +#, fuzzy +msgid "Existing domains" +msgstr "Domaines concordant" + +#: VirtualMailManager/cli/subcommands.py:889 msgid "Alias domain information" msgstr "Information sur l'alias de domaine" -#: vmm:186 +#: VirtualMailManager/cli/subcommands.py:894 #, python-format msgid "" "\tThe alias domain %(alias)s belongs to:\n" @@ -513,114 +822,470 @@ "\tL'alias de domaine %(alias)s appartiennent à:\n" "\t * %(domain)s" -#: vmm:197 vmm:205 vmm:213 -msgid "Missing domain name." -msgstr "Absence de nom de domaine." +#: VirtualMailManager/common.py:63 +#, fuzzy, python-format +msgid "No such file: '%s'" +msgstr "Pas de répertoire: %s" + +#: VirtualMailManager/common.py:66 +#, python-format +msgid "File is not executable: '%s'" +msgstr "" + +#: VirtualMailManager/common.py:82 +msgid "GiB" +msgstr "" + +#: VirtualMailManager/common.py:82 +msgid "TiB" +msgstr "" + +#: VirtualMailManager/common.py:83 +msgid "KiB" +msgstr "" + +#: VirtualMailManager/common.py:83 +msgid "MiB" +msgstr "" + +#. TP: e.g.: '%(size)s %(prefix)s' -> '118.30 MiB' +#: VirtualMailManager/common.py:87 +#, python-format +msgid "%(size)s %(prefix)s" +msgstr "" + +#: VirtualMailManager/config.py:89 +#, python-format +msgid "Not a boolean: '%s'" +msgstr "" + +#: VirtualMailManager/config.py:127 +#, python-format +msgid "Bad format: '%s' - expected: section.option" +msgstr "" + +#: VirtualMailManager/config.py:380 +#, fuzzy, python-format +msgid "* Section: %s\n" +msgstr "absence de la section: %s\n" + +#: VirtualMailManager/config.py:390 VirtualMailManager/config.py:398 +#, fuzzy, python-format +msgid "Check of configuration file %s failed.\n" +msgstr "Utilisation du fichier configuration: %s\n" + +#: VirtualMailManager/config.py:392 +msgid "Missing options, which have no default value.\n" +msgstr "" + +#: VirtualMailManager/config.py:400 VirtualMailManager/config.py:402 +#, fuzzy +msgid "Invalid configuration values.\n" +msgstr "Utilisation du fichier configuration: %s\n" + +#: VirtualMailManager/config.py:441 VirtualMailManager/config.py:525 +#, python-format +msgid "Not a valid Dovecot version: '%s'" +msgstr "" + +#: VirtualMailManager/config.py:447 VirtualMailManager/config.py:482 +#, python-format +msgid "Unsupported database module: '%s'" +msgstr "" + +#: VirtualMailManager/config.py:452 VirtualMailManager/config.py:490 +#, python-format +msgid "Unknown pgsql SSL mode: '%s'" +msgstr "" -#: vmm:215 vmm:219 -msgid "Domain" -msgstr "Domaine" +#: VirtualMailManager/config.py:459 VirtualMailManager/config.py:503 +#: VirtualMailManager/maillocation.py:70 +#, python-format +msgid "Unsupported mailbox format: '%s'" +msgstr "" + +#: VirtualMailManager/config.py:475 VirtualMailManager/handler.py:283 +#: VirtualMailManager/handler.py:357 VirtualMailManager/handler.py:362 +#: VirtualMailManager/handler.py:390 +#, python-format +msgid "No such directory: %s" +msgstr "Pas de répertoire: %s" + +#: VirtualMailManager/config.py:514 +#, python-format +msgid "Not a valid size value: '%s'" +msgstr "" + +#: VirtualMailManager/domain.py:78 +#, fuzzy, python-format +msgid "The domain '%s' is an alias domain." +msgstr "Le domaine “%s” est un alias de domaine." + +#: VirtualMailManager/domain.py:108 +#, python-format +msgid "There are %(account_count)u accounts, %(alias_count)u aliases and %(relocated_count)u relocated users." +msgstr "" + +#: VirtualMailManager/domain.py:123 +#, fuzzy, python-format +msgid "The domain '%s' already exists." +msgstr "Le domaine “%s” existe déjà." + +#: VirtualMailManager/domain.py:437 +#, fuzzy +msgid "The domain name is too long" +msgstr "Le nom du domaine est trop long." -#: vmm:221 vmm:230 -msgid "accounts" +#: VirtualMailManager/domain.py:439 +#, fuzzy, python-format +msgid "The domain name '%s' is invalid" +msgstr "Le nom du domaine “%s” est invalide." + +#: VirtualMailManager/emailaddress.py:73 +#, fuzzy, python-format +msgid "Missing the '@' sign in address: '%s'" +msgstr "Absence du signe '@' dans l'adresse e-mail “%s”. " + +#: VirtualMailManager/emailaddress.py:76 +#, fuzzy, python-format +msgid "Too many '@' signs in address: '%s'" +msgstr "Absence du signe '@' dans l'adresse e-mail “%s”. " + +#: VirtualMailManager/emailaddress.py:79 +#, fuzzy, python-format +msgid "Missing local-part in address: '%s'" +msgstr "Absence d'adresse relocalisée" + +#: VirtualMailManager/emailaddress.py:82 +#, fuzzy, python-format +msgid "Missing domain name in address: '%s'" +msgstr "Absence d'un nom de domaine après “%s@”" + +#: VirtualMailManager/emailaddress.py:145 +#, fuzzy, python-format +msgid "The local-part '%s' is too long." +msgstr "La partie locale “%s” est trop longue." + +#: VirtualMailManager/emailaddress.py:150 +#, fuzzy, python-format +msgid "The local-part '%(l_part)s' contains invalid characters: %(i_chars)s" +msgstr "La partie locale “%(lpart)s” contient des caractères invalide: %(ichrs)s" + +#: VirtualMailManager/ext/postconf.py:84 +#, fuzzy, python-format +msgid "The value '%s' does not look like a valid postfix configuration parameter name." +msgstr "La valeur “%s” ne semble pas être un nom valide pour les paramètres de configuration postfix. " + +#: VirtualMailManager/handler.py:56 +#, fuzzy +msgid "an account" msgstr "comptes" -#: vmm:225 vmm:231 -msgid "aliases" +#: VirtualMailManager/handler.py:57 +#, fuzzy +msgid "an alias" msgstr "Alias" -#: vmm:227 vmm:232 -msgid "relocated users" +#: VirtualMailManager/handler.py:58 +#, fuzzy +msgid "a relocated user" msgstr "Utilisateurs relocalisés" -#: vmm:236 -msgid "Missing domain name and new transport." -msgstr "Absence de nom de domaine et de nouveau transport." +#: VirtualMailManager/handler.py:84 +msgid "" +"You are not root.\n" +"\tGood bye!\n" +msgstr "" +"Vous n'êtes pas super-utilisateur (root).\n" +"\tAu revoir!\n" -#: vmm:238 -msgid "Missing new transport." -msgstr "Absence de nouveau transport." +#: VirtualMailManager/handler.py:104 +#, python-format +msgid "Could not find '%(cfg_file)s' in: '%(cfg_path)s'" +msgstr "" -#: vmm:247 vmm:262 -msgid "Missing alias domain name and target domain name." -msgstr "Absence du nom de l'alias de domaine et du nom cible de domaine" +#: VirtualMailManager/handler.py:115 +#, fuzzy, python-format +msgid "" +"wrong permissions for '%(file)s': %(perms)s\n" +"`chmod 0600 %(file)s` would be great." +msgstr "" +"Changer les droits de permissions (%(perms)s) pour “%(file)s”\n" +"`chmod 0600 %(file)s` serait mieux." -#: vmm:249 vmm:264 -msgid "Missing target domain name." -msgstr "Asbence du nom cible de domaine" +#: VirtualMailManager/handler.py:135 +#, fuzzy, python-format +msgid "" +"'%(path)s' is not a directory.\n" +"(%(cfg_file)s: section 'misc', option 'base_directory')" +msgstr "" +"“%s” n'est pas un répertoire.\n" +"(vmm.cfg: section \"domdir\", option \"base\")" -#: vmm:255 vmm:270 -msgid "Missing alias domain name." -msgstr "Absence du nom de l'alias de domaine." +#: VirtualMailManager/handler.py:144 +#, python-format +msgid "" +"\n" +"(%(cfg_file)s: section 'bin', option '%(option)s')" +msgstr "" -#: vmm:276 vmm:285 vmm:293 vmm:323 vmm:331 vmm:339 -msgid "Missing e-mail address." -msgstr "Absence de l'adresse e-mail." +#: VirtualMailManager/handler.py:158 VirtualMailManager/handler.py:165 +#, python-format +msgid "Unable to import database module '%s'." +msgstr "" + +#. TP: %(a_type)s will be one of: 'an account', 'an alias' or +#. 'a relocated user' +#: VirtualMailManager/handler.py:244 +#, fuzzy, python-format +msgid "There is already %(a_type)s with the address '%(address)s'." +msgstr "Il y a déjà un alias avec l'adresse “%s”." -#: vmm:301 -msgid "alias addresses" -msgstr "Adresses alias" +#: VirtualMailManager/handler.py:297 +#, python-format +msgid "'%s' is not a directory." +msgstr "" + +#: VirtualMailManager/handler.py:300 +#, fuzzy, python-format +msgid "The file/directory '%s' already exists." +msgstr "Le transfére de l'utilisateur “%s” existe déjà." -#: vmm:307 -msgid "Missing e-mail address and users name." -msgstr "Absence de l'adresse e-mail et du nom des utilisateurs." +#: VirtualMailManager/handler.py:329 +msgid "Skipped mailbox folders:" +msgstr "" -#: vmm:309 -msgid "Missing users name." -msgstr "Absence du nom des utilisateurs." +#: VirtualMailManager/handler.py:349 +#, python-format +msgid "UID '%(uid)u' and/or GID '%(gid)u' are less than %(min_uid)u/%(min_gid)u." +msgstr "" + +#: VirtualMailManager/handler.py:354 VirtualMailManager/handler.py:387 +#, fuzzy, python-format +msgid "Found \"..\" in domain directory path: %s" +msgstr "\"..\" trouvé dans le chemins d'accès du répertoire racine de l'utilisateur (home)" -#: vmm:315 -msgid "Missing e-mail address and transport." -msgstr "Absence de l'adresse e-mail et du transport." +#: VirtualMailManager/handler.py:367 +#, fuzzy +msgid "Detected owner/group mismatch in home directory." +msgstr "Défaut d'appariement détecté entre les droits propriétaire/groupe avec le répertoire de l'utilisateur (home)" + +#: VirtualMailManager/handler.py:383 +#, python-format +msgid "GID '%(gid)u' is less than '%(min_gid)u'." +msgstr "" -#: vmm:317 -msgid "Missing transport." -msgstr "Absence du transport." +#: VirtualMailManager/handler.py:394 +#, fuzzy, python-format +msgid "Detected group mismatch in domain directory: %s" +msgstr "Défaut d'appariement détecté dans le répertoire domaine" -#: vmm:348 -msgid "Missing alias address and destination." -msgstr "Absence de l'adresse alias et de la destination." +#: VirtualMailManager/handler.py:470 VirtualMailManager/handler.py:748 +#, fuzzy, python-format +msgid "Unknown service: '%s'" +msgstr "Service “%s” inconnu." -#: vmm:350 vmm:373 -msgid "Missing destination address." +#: VirtualMailManager/handler.py:587 +#, fuzzy, python-format +msgid "The pattern '%s' contains invalid characters." +msgstr "Le modèle “%s” contient des charactères invalides." + +#: VirtualMailManager/handler.py:614 +#, fuzzy +msgid "Ignored destination addresses:" msgstr "Absence de l'adresse destination." -#: vmm:356 vmm:362 -msgid "Missing alias address" -msgstr "Absence de l'adresse alias" +#: VirtualMailManager/handler.py:619 VirtualMailManager/handler.py:769 +#, fuzzy, python-format +msgid "The destination account/alias '%s' does not exist." +msgstr "La destination compte/alias “%s” n'existe pas." + +#: VirtualMailManager/handler.py:641 +#, fuzzy, python-format +msgid "" +"The account has been successfully deleted from the database.\n" +" But an error occurred while deleting the following directory:\n" +" '%(directory)s'\n" +" Reason: %(reason)s" +msgstr "" +"Le compte a été supprimé de la base de donnée avec succès.\n" +" Mais une erreur c'est produite lors de la suppression du répertoire suivant:\n" +" “%(directory)s”\n" +" Raison: %(raeson)s" + +#: VirtualMailManager/handler.py:712 +#, python-format +msgid "Could not accept name: '%s'" +msgstr "" -#: vmm:371 -msgid "Missing relocated address and destination." -msgstr "Absence d'adresse relocalisée et de la destination." +#: VirtualMailManager/handler.py:735 +#, python-format +msgid "Could not accept transport: '%s'" +msgstr "" + +#: VirtualMailManager/handler.py:779 VirtualMailManager/relocated.py:98 +#: VirtualMailManager/relocated.py:105 +#, fuzzy, python-format +msgid "The relocated user '%s' does not exist." +msgstr "L'emplacement de l'utilisateur “%s” n'existe pas." + +#: VirtualMailManager/mailbox.py:260 +#, python-format +msgid "Failed to create mailboxes: %r\n" +msgstr "" + +#: VirtualMailManager/maillocation.py:74 +msgid "Empty directory name" +msgstr "" + +#: VirtualMailManager/maillocation.py:76 +#, fuzzy, python-format +msgid "Directory name is too long: '%s'" +msgstr "Le nom du domaine est trop long." -#: vmm:379 vmm:387 -msgid "Missing relocated address" -msgstr "Absence d'adresse relocalisée" +#: VirtualMailManager/password.py:388 +#, python-format +msgid "Unsupported password scheme: '%s'" +msgstr "" + +#: VirtualMailManager/password.py:391 +#, python-format +msgid "The password scheme '%(scheme)s' requires Dovecot >= v%(version)s." +msgstr "" + +#: VirtualMailManager/password.py:397 +msgid "Encoding suffixes for password schemes require Dovecot >= v1.1.alpha1." +msgstr "" + +#: VirtualMailManager/password.py:400 +#, python-format +msgid "Unsupported password encoding: '%s'" +msgstr "" + +#: VirtualMailManager/relocated.py:71 +msgid "Address and destination are identical." +msgstr "L'adresse et la destination sont identique. " -#: vmm:393 -msgid "Missing userid" -msgstr "Absence d'identiant utilisateur" +#: VirtualMailManager/relocated.py:75 +#, fuzzy, python-format +msgid "The relocated user '%s' already exists." +msgstr "Le transfére de l'utilisateur “%s” existe déjà." + +#~ msgid "There is already a relocated user with the address “%s”." +#~ msgstr "Il y a déjà un utilisateur relocalisé avec cette adresse “%s”." + +#~ msgid "There is already an account with address “%s”." +#~ msgstr "Il y a déjà un compte avec l'adresse “%s”." -#: vmm:406 -msgid "Warnings:" -msgstr "Avertissements:" +#~ msgid "No destination address for alias denoted." +#~ msgstr "Pas de destination à l'adresse de l'alias relevé." + +#~ msgid "The alias “%(a)s” with destination “%(d)s” already exists." +#~ msgstr "L'alias “%(a)s” avec la destination “%(d)s” existe déjà." + +#~ msgid "The alias “%(a)s” with destination “%(d)s” doesn't exists." +#~ msgstr "L'alias “%(a)s” avec la destination “%(d)s” n'existe pas." + +#~ msgid "missing options in section %s:\n" +#~ msgstr "absence des options dans la section %s:\n" + +#~ msgid "There are accounts and aliases." +#~ msgstr "Il y a des comptes et des alias." -#: vmm:412 -msgid "from" -msgstr "de" +#~ msgid "There are accounts." +#~ msgstr "Il y a des comptes." + +#~ msgid "There are aliases." +#~ msgstr "Il y a des alias." + +#~ msgid "“%s” looks not like an e-mail address." +#~ msgstr "“%s” ne ressemble pas à une adresse e-mail." + +#~ msgid "No localpart specified." +#~ msgstr "Pas de partie locale spécifié." + +#~ msgid "Either mid or maillocation must be specified." +#~ msgstr "Soit l'identifiant de l'emplacement e-mail ou l'emplacement e-mail doit être spécifié. " + +#~ msgid "mid must be an int/long." +#~ msgstr "L'identiant de l'emplcement e-mail doit être un entier long." -#: vmm:412 -msgid "version" -msgstr "version" +#~ msgid "" +#~ "Invalid folder name “%s”, it may consist only of\n" +#~ "1 - 20 single byte characters (A-Z, a-z, 0-9 and _)." +#~ msgstr "" +#~ "Le nom du fichier “%s” est invalid, il peut comporté\n" +#~ "entre 1 à 20 caractères (A-Z, a-z, 0-9 est _)." + +#~ msgid "Unknown mid specified." +#~ msgstr "Identifiant de l'emplacement e-mail specifié est inconnu." + +#~ msgid "No destination address for relocated user denoted." +#~ msgstr "Pas d'adresse destination pour l'utilisateur relocalisé." + +#~ msgid "tid must be an int/long." +#~ msgstr "L'identifiant du transport doit être un entier long." + +#~ msgid "Unknown tid specified." +#~ msgstr "L'identifiant du transport spécifié est inconnu." + +#~ msgid "No “vmm.cfg” found in: /root:/usr/local/etc:/etc" +#~ msgstr "Pas de “vmm.cfg” trouvé dans: /root:/usr/local/etc:/etc" + +#~ msgid "" +#~ "“%(binary)s” doesn't exists.\n" +#~ "(vmm.cfg: section \"bin\", option \"%(option)s\")" +#~ msgstr "" +#~ "“%(binary)s” n'existe pas.\n" +#~ "(vmm.cfg: section \"bin\", option \"%(option)s\")" -#: vmm:414 -msgid "on" -msgstr "sur" +#~ msgid "" +#~ "“%(binary)s” is not executable.\n" +#~ "(vmm.cfg: section \"bin\", option \"%(option)s\")" +#~ msgstr "" +#~ "“%(binary)s” n'est pas executable.\n" +#~ "(vmm.cfg: section \"bin\", option \"%(option)s\")" + +#~ msgid "FATAL: \"..\" in domain directory path detected." +#~ msgstr "FATAL: \"..\" dans le chemins d'accès du réperoire domaine." + +#~ msgid "" +#~ "Configurtion error: \"%s\"\n" +#~ "(in section \"connfig\", option \"done\") see also: vmm.cfg(5)\n" +#~ msgstr "" +#~ "Erreur de configuration: \"%s\"\n" +#~ "(dans la section \"connfig\", option \"done\") voir aussi: vmm.cfg(5)\n" + +#~ msgid "" +#~ "The keyword “detailed” is deprecated and will be removed in a future release.\n" +#~ " Please use the keyword “full” to get full details." +#~ msgstr "" +#~ "Le mot-clé “detailed” est obselète et va être supprimé dans la futur version.\n" +#~ " Utiliser, plutôt, le mot “full” pour avoir tous les détails." -#: vmm:488 -msgid "Unknown subcommand" -msgstr "Sous-commande inconue." +#~ msgid "Account doesn't exists" +#~ msgstr "Le compte n'existe pas." + +#~ msgid "" +#~ "The service name “managesieve” is deprecated and will be removed\n" +#~ " in a future release.\n" +#~ " Please use the service name “sieve” instead." +#~ msgstr "" +#~ "Le nom de service “managesieve” est obselète et va être supprimé\n" +#~ " dans la futur version.\n" +#~ " Utiliser, plutôt, le nom de service “sieve”." -#: vmm:491 -msgid "Ouch" -msgstr "Aie" +#~ msgid "" +#~ "Usage: %s SUBCOMMAND OBJECT ARGS*\n" +#~ " short long\n" +#~ " subcommand object args (* = optional)\n" +#~ msgstr "" +#~ "Usage: %s SOUSCOMMANDE OBJET ARGS*\n" +#~ " short long\n" +#~ " souscommande objet args (* = facultatif)\n" + +#~ msgid "Available" +#~ msgstr "Disponible" + +#~ msgid "Available domains" +#~ msgstr "Domaines disponible" diff -r c0e1fb1b0145 -r a4aead244f75 po/vmm.pot --- a/po/vmm.pot Mon Nov 07 03:22:15 2011 +0000 +++ b/po/vmm.pot Thu Jun 28 19:26:50 2012 +0000 @@ -6,634 +6,1120 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: vmm 0.5.2\n" -"Report-Msgid-Bugs-To: neverseen@users.sourceforge.net\n" -"POT-Creation-Date: 2009-10-20 19:19+0200\n" +"Project-Id-Version: vmm 0.6.0\n" +"Report-Msgid-Bugs-To: user+vmm/tp@localhost.localdomain.org\n" +"POT-Creation-Date: 2011-11-07 05:20+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" +"Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#. TP: Hm, what quotation marks should be used? +#. TP: Hm, what “quotation marks” should be used? #. If you are unsure have a look at: #. http://en.wikipedia.org/wiki/Quotation_mark,_non-English_usage -#: VirtualMailManager/Account.py:37 VirtualMailManager/Relocated.py:42 +#: VirtualMailManager/account.py:58 VirtualMailManager/alias.py:35 +#: VirtualMailManager/domain.py:120 VirtualMailManager/relocated.py:38 #, python-format -msgid "There is already an alias with the address “%s”." +msgid "The domain '%s' does not exist." msgstr "" -#: VirtualMailManager/Account.py:42 VirtualMailManager/Alias.py:41 +#: VirtualMailManager/account.py:106 #, python-format -msgid "There is already a relocated user with the address “%s”." +msgid "The mailbox format '%(mbfmt)s' requires Dovecot >= v%(version)s." msgstr "" -#: VirtualMailManager/Account.py:62 VirtualMailManager/Alias.py:57 -#: VirtualMailManager/Domain.py:161 VirtualMailManager/Domain.py:187 -#: VirtualMailManager/Domain.py:218 VirtualMailManager/Relocated.py:58 +#: VirtualMailManager/account.py:113 VirtualMailManager/account.py:305 #, python-format -msgid "The domain “%s” doesn't exist." +msgid "Invalid transport '%(transport)s' for mailbox format '%(mbfmt)s'." +msgstr "" + +#: VirtualMailManager/account.py:153 VirtualMailManager/cli/handler.py:93 +#: VirtualMailManager/handler.py:628 VirtualMailManager/handler.py:679 +#: VirtualMailManager/handler.py:705 VirtualMailManager/handler.py:716 +#: VirtualMailManager/handler.py:727 VirtualMailManager/handler.py:739 +#: VirtualMailManager/handler.py:753 +#, python-format +msgid "The account '%s' does not exist." msgstr "" -#: VirtualMailManager/Account.py:81 +#: VirtualMailManager/account.py:204 VirtualMailManager/account.py:214 +#: VirtualMailManager/cli/handler.py:77 VirtualMailManager/handler.py:596 #, python-format -msgid "Unknown service “%s”." +msgid "The account '%s' already exists." msgstr "" -#: VirtualMailManager/Account.py:84 VirtualMailManager/Account.py:157 -#: VirtualMailManager/Account.py:188 VirtualMailManager/Account.py:223 +#: VirtualMailManager/account.py:207 VirtualMailManager/handler.py:701 #, python-format -msgid "The account “%s” doesn't exist." +msgid "Could not accept password: '%s'" msgstr "" -#: VirtualMailManager/Account.py:152 +#: VirtualMailManager/account.py:217 #, python-format -msgid "The account “%s” already exists." +msgid "No password set for account: '%s'" msgstr "" -#. TP: A service (pop3/imap/…) is enabled/usable for a user -#: VirtualMailManager/Account.py:197 -msgid "enabled" +#: VirtualMailManager/account.py:245 +#, python-format +msgid "Unknown field: '%s'" msgstr "" -#. TP: A service (pop3/imap) isn't enabled/usable for a user -#: VirtualMailManager/Account.py:200 +#: VirtualMailManager/account.py:267 VirtualMailManager/domain.py:292 +msgid "PostgreSQL-based dictionary quota requires Dovecot >= v1.1.2." +msgstr "" + +#. TP: A service (e.g. pop3 or imap) may be enabled/usable or +#. disabled/unusable for a user. +#: VirtualMailManager/account.py:332 msgid "disabled" msgstr "" -#: VirtualMailManager/Account.py:244 -#, python-format -msgid "There are %(count)d aliases with the destination address “%(address)s”." +#: VirtualMailManager/account.py:332 +msgid "enabled" msgstr "" -#: VirtualMailManager/Account.py:252 -msgid "uid must be an int/long." +#: VirtualMailManager/account.py:343 +#, python-format +msgid "Could not fetch information for account: '%s'" msgstr "" -#: VirtualMailManager/Account.py:254 -msgid "uid must be greater than 0." +#: VirtualMailManager/account.py:387 +#, python-format +msgid "There are %(count)d aliases with the destination address '%(address)s'." msgstr "" -#: VirtualMailManager/Account.py:262 -#, python-format -msgid "There is no account with the UID “%d”." +#: VirtualMailManager/account.py:416 +msgid "UID must be an int/long." msgstr "" -#: VirtualMailManager/Alias.py:28 VirtualMailManager/Relocated.py:28 -msgid "Address and destination are identical." +#: VirtualMailManager/account.py:418 +msgid "UID must be greater than 0." +msgstr "" + +#: VirtualMailManager/account.py:427 +#, python-format +msgid "There is no account with the UID: '%d'" msgstr "" -#: VirtualMailManager/Alias.py:37 VirtualMailManager/Relocated.py:37 +#: VirtualMailManager/alias.py:60 #, python-format -msgid "There is already an account with address “%s”." +msgid "" +"Cannot add %(count_new)i new destination(s) to alias '%(address)s'.\n" +"Currently this alias expands into %(count)i/%(limit)i recipients.\n" +"%(count_new)i additional destination(s) will render this alias unusable.\n" +"Hint: Increase Postfix' virtual_alias_expansion_limit" msgstr "" -#: VirtualMailManager/Alias.py:67 +#: VirtualMailManager/alias.py:67 #, python-format msgid "" -"Can't add new destination to alias “%(address)s”.\n" -"Currently this alias expands into %(count)i recipients.\n" -"One more destination will render this alias unusable.\n" -"Hint: Increase Postfix' virtual_alias_expansion_limit\n" +"Cannot add %(count_new)i new destination(s) to alias '%(address)s'.\n" +"This alias already exceeds its expansion limit (%(count)i/%(limit)i).\n" +"So its unusable, all messages addressed to this alias will be bounced.\n" +"Hint: Delete some destination addresses." msgstr "" -#: VirtualMailManager/Alias.py:76 -msgid "No destination address specified for alias." +#: VirtualMailManager/alias.py:142 VirtualMailManager/alias.py:154 +#: VirtualMailManager/alias.py:161 VirtualMailManager/handler.py:657 +#, python-format +msgid "The alias '%s' does not exist." msgstr "" -#: VirtualMailManager/Alias.py:87 +#: VirtualMailManager/alias.py:145 #, python-format -msgid "The alias “%(a)s” with destination “%(d)s” already exists." +msgid "The address '%(addr)s' is not a destination of the alias '%(alias)s'." msgstr "" -#: VirtualMailManager/Alias.py:100 VirtualMailManager/Alias.py:117 +#: VirtualMailManager/aliasdomain.py:50 #, python-format -msgid "The alias “%s” doesn't exist." +msgid "The domain '%s' is a primary domain." msgstr "" -#: VirtualMailManager/Alias.py:119 +#: VirtualMailManager/aliasdomain.py:69 #, python-format -msgid "The alias “%(a)s” with destination “%(d)s” doesn't exist." +msgid "The alias domain '%s' already exists." +msgstr "" + +#: VirtualMailManager/aliasdomain.py:72 VirtualMailManager/aliasdomain.py:106 +msgid "No destination domain set for the alias domain." msgstr "" -#: VirtualMailManager/AliasDomain.py:30 +#: VirtualMailManager/aliasdomain.py:75 VirtualMailManager/aliasdomain.py:109 #, python-format -msgid "The domain “%s” is a primary domain." +msgid "The target domain '%s' does not exist." msgstr "" -#: VirtualMailManager/AliasDomain.py:35 +#: VirtualMailManager/aliasdomain.py:88 VirtualMailManager/aliasdomain.py:112 +#: VirtualMailManager/aliasdomain.py:133 #, python-format -msgid "The alias domain “%s” already exists." +msgid "The alias domain '%s' does not exist." msgstr "" -#: VirtualMailManager/AliasDomain.py:38 VirtualMailManager/AliasDomain.py:68 -msgid "No destination domain specified for alias domain." +#: VirtualMailManager/aliasdomain.py:98 +#, python-format +msgid "There is no primary domain for the alias domain '%s'." msgstr "" -#: VirtualMailManager/AliasDomain.py:41 VirtualMailManager/AliasDomain.py:71 +#: VirtualMailManager/aliasdomain.py:115 #, python-format -msgid "The target domain “%s” doesn't exist." +msgid "" +"The alias domain '%(alias)s' is already assigned to the domain '%(domain)s'." msgstr "" -#: VirtualMailManager/AliasDomain.py:60 -#, python-format -msgid "There is no primary domain for the alias domain “%s”." +#. TP: Please preserve the trailing space. +#: VirtualMailManager/cli/__init__.py:78 +msgid "Enter new password: " +msgstr "" + +#. TP: Please preserve the trailing space. +#: VirtualMailManager/cli/__init__.py:80 +msgid "Retype new password: " msgstr "" -#: VirtualMailManager/AliasDomain.py:63 VirtualMailManager/AliasDomain.py:74 -#: VirtualMailManager/AliasDomain.py:97 -#, python-format -msgid "The alias domain “%s” doesn't exist." +#: VirtualMailManager/cli/__init__.py:85 VirtualMailManager/cli/config.py:53 +msgid "Too many failures - try again later." +msgstr "" + +#: VirtualMailManager/cli/__init__.py:91 +msgid "Sorry, passwords do not match." msgstr "" -#: VirtualMailManager/AliasDomain.py:77 -#, python-format -msgid "" -"The alias domain “%(alias)s” is already assigned to the domain “%(domain)s”." +#: VirtualMailManager/cli/__init__.py:95 +msgid "Sorry, empty passwords are not permitted." msgstr "" -#: VirtualMailManager/Config.py:90 VirtualMailManager/Config.py:125 +#: VirtualMailManager/cli/config.py:32 +#, python-format +msgid "Enter new value for option %(option)s [%(current_value)s]: " +msgstr "" + +#: VirtualMailManager/cli/config.py:36 #, python-format msgid "Using configuration file: %s\n" msgstr "" -#: VirtualMailManager/Config.py:94 +#: VirtualMailManager/cli/config.py:38 #, python-format -msgid "missing section: %s\n" +msgid "* Configuration section: '%s'" msgstr "" -#: VirtualMailManager/Config.py:96 +#: VirtualMailManager/cli/config.py:50 #, python-format -msgid "missing options in section %s:\n" +msgid "Warning: %s" +msgstr "" + +#: VirtualMailManager/cli/handler.py:66 +#, python-format +msgid "Invalid section: '%s'" msgstr "" -#: VirtualMailManager/Config.py:128 +#: VirtualMailManager/cli/main.py:32 VirtualMailManager/cli/main.py:65 +#: VirtualMailManager/cli/main.py:68 VirtualMailManager/cli/subcommands.py:629 +#: VirtualMailManager/cli/subcommands.py:649 #, python-format -msgid "* Config section: “%s”" +msgid "Error: %s" msgstr "" -#: VirtualMailManager/Config.py:131 -#, python-format -msgid "Enter new value for option %(opt)s [%(val)s]: " +#: VirtualMailManager/cli/main.py:41 +msgid "You must specify a subcommand at least." msgstr "" -#: VirtualMailManager/Domain.py:37 +#: VirtualMailManager/cli/main.py:53 #, python-format -msgid "The domain “%s” is an alias domain." -msgstr "" - -#: VirtualMailManager/Domain.py:122 -msgid "There are accounts and aliases." +msgid "Unknown subcommand: '%s'" msgstr "" -#: VirtualMailManager/Domain.py:125 -msgid "There are accounts." -msgstr "" - -#: VirtualMailManager/Domain.py:128 -msgid "There are aliases." +#. TP: We have to cry, because root has killed/interrupted vmm +#. with Ctrl+C or Ctrl+D. +#: VirtualMailManager/cli/main.py:62 +msgid "Ouch!" msgstr "" -#: VirtualMailManager/Domain.py:143 +#: VirtualMailManager/cli/main.py:71 #, python-format -msgid "The domain “%s” already exists." +msgid "Error: Unknown section: '%s'" msgstr "" -#: VirtualMailManager/EmailAddress.py:42 +#: VirtualMailManager/cli/main.py:74 #, python-format -msgid "Missing '@' sign in e-mail address “%s”." +msgid "Error: No option '%(option)s' in section: '%(section)s'" msgstr "" -#: VirtualMailManager/EmailAddress.py:45 -#, python-format -msgid "“%s” doesn't look like an e-mail address." +#: VirtualMailManager/cli/main.py:77 +msgid "Warnings:" msgstr "" -#: VirtualMailManager/EmailAddress.py:50 +#: VirtualMailManager/cli/subcommands.py:78 #, python-format -msgid "Missing domain name after “%s@”." +msgid "Plan A failed ... trying Plan B: %(subcommand)s %(object)s" msgstr "" -#: VirtualMailManager/EmailAddress.py:62 -msgid "No local-part specified." +#: VirtualMailManager/cli/subcommands.py:92 +msgid "Missing alias address and destination." msgstr "" -#: VirtualMailManager/EmailAddress.py:65 -#, python-format -msgid "The local-part “%s” is too long" +#: VirtualMailManager/cli/subcommands.py:95 +#: VirtualMailManager/cli/subcommands.py:453 +msgid "Missing destination address." msgstr "" -#: VirtualMailManager/EmailAddress.py:72 -#, python-format -msgid "The local-part “%(lpart)s” contains invalid characters: %(ichrs)s" +#: VirtualMailManager/cli/subcommands.py:102 +#: VirtualMailManager/cli/subcommands.py:112 +msgid "Missing alias address." msgstr "" -#: VirtualMailManager/MailLocation.py:28 -msgid "Either mid or maillocation must be specified." +#: VirtualMailManager/cli/subcommands.py:134 +#: VirtualMailManager/cli/subcommands.py:168 +msgid "Missing alias domain name and destination domain name." msgstr "" -#: VirtualMailManager/MailLocation.py:34 -msgid "mid must be an int/long." +#: VirtualMailManager/cli/subcommands.py:137 +#: VirtualMailManager/cli/subcommands.py:171 +msgid "Missing destination domain name." msgstr "" -#: VirtualMailManager/MailLocation.py:42 -#, python-format -msgid "" -"Invalid folder name “%s”, it may consist only of\n" -"1 - 20 single byte characters (A-Z, a-z, 0-9 and _)." +#: VirtualMailManager/cli/subcommands.py:145 +#: VirtualMailManager/cli/subcommands.py:152 +msgid "Missing alias domain name." msgstr "" -#: VirtualMailManager/MailLocation.py:55 -msgid "Unknown mid specified." +#: VirtualMailManager/cli/subcommands.py:179 +msgid "Missing option name." msgstr "" -#: VirtualMailManager/Relocated.py:64 -msgid "No destination address specified for relocated user." +#: VirtualMailManager/cli/subcommands.py:195 +msgid "Missing option and new value." msgstr "" -#: VirtualMailManager/Relocated.py:74 -#, python-format -msgid "The relocated user “%s” already exists." +#: VirtualMailManager/cli/subcommands.py:197 +msgid "Missing new configuration value." msgstr "" -#: VirtualMailManager/Relocated.py:88 VirtualMailManager/Relocated.py:101 -#, python-format -msgid "The relocated user “%s” doesn't exist." +#: VirtualMailManager/cli/subcommands.py:213 +#: VirtualMailManager/cli/subcommands.py:229 +#: VirtualMailManager/cli/subcommands.py:242 +#: VirtualMailManager/cli/subcommands.py:331 +msgid "Missing domain name." msgstr "" -#: VirtualMailManager/Transport.py:27 -msgid "Either tid or transport must be specified." -msgstr "" - -#: VirtualMailManager/Transport.py:33 -msgid "tid must be an int/long." +#: VirtualMailManager/cli/subcommands.py:219 +#, python-format +msgid "Creating account for postmaster@%s" msgstr "" -#: VirtualMailManager/Transport.py:61 -msgid "Unknown tid specified." -msgstr "" - -#: VirtualMailManager/VirtualMailManager.py:47 -msgid "" -"You are not root.\n" -"\tGood bye!\n" +#: VirtualMailManager/cli/subcommands.py:235 +#: VirtualMailManager/cli/subcommands.py:249 +#: VirtualMailManager/cli/subcommands.py:322 +#: VirtualMailManager/cli/subcommands.py:343 +#: VirtualMailManager/cli/subcommands.py:372 +#: VirtualMailManager/cli/subcommands.py:509 +#: VirtualMailManager/cli/subcommands.py:522 VirtualMailManager/handler.py:453 +#: VirtualMailManager/handler.py:466 VirtualMailManager/handler.py:481 +#: VirtualMailManager/handler.py:510 VirtualMailManager/handler.py:674 +#, python-format +msgid "Invalid argument: '%s'" msgstr "" -#: VirtualMailManager/VirtualMailManager.py:67 -msgid "No “vmm.cfg” found in: /root:/usr/local/etc:/etc" +#: VirtualMailManager/cli/subcommands.py:267 +#: VirtualMailManager/cli/subcommands.py:273 +msgid "Domain" msgstr "" -#: VirtualMailManager/VirtualMailManager.py:78 -#, python-format -msgid "" -"fix permissions (%(perms)s) for “%(file)s”\n" -"`chmod 0600 %(file)s` would be great." +#: VirtualMailManager/cli/subcommands.py:275 +#: VirtualMailManager/cli/subcommands.py:284 +msgid "accounts" msgstr "" -#: VirtualMailManager/VirtualMailManager.py:93 -#, python-format -msgid "" -"“%s” is not a directory.\n" -"(vmm.cfg: section \"domdir\", option \"base\")" +#: VirtualMailManager/cli/subcommands.py:277 +#: VirtualMailManager/cli/subcommands.py:283 +#: VirtualMailManager/cli/subcommands.py:831 +msgid "alias domains" msgstr "" -#: VirtualMailManager/VirtualMailManager.py:98 -#, python-format -msgid "" -"“%(binary)s” doesn't exist.\n" -"(vmm.cfg: section \"bin\", option \"%(option)s\")" +#: VirtualMailManager/cli/subcommands.py:279 +#: VirtualMailManager/cli/subcommands.py:285 +msgid "aliases" msgstr "" -#: VirtualMailManager/VirtualMailManager.py:102 -#, python-format -msgid "" -"“%(binary)s” is not executable.\n" -"(vmm.cfg: section \"bin\", option \"%(option)s\")" +#: VirtualMailManager/cli/subcommands.py:281 +#: VirtualMailManager/cli/subcommands.py:286 +msgid "relocated users" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:292 +msgid "Missing domain name and storage value." msgstr "" -#: VirtualMailManager/VirtualMailManager.py:150 -msgid "The domain name is too long." +#: VirtualMailManager/cli/subcommands.py:295 +#: VirtualMailManager/cli/subcommands.py:582 +msgid "Missing storage value." msgstr "" -#: VirtualMailManager/VirtualMailManager.py:153 +#: VirtualMailManager/cli/subcommands.py:301 +#: VirtualMailManager/cli/subcommands.py:586 #, python-format -msgid "The domain name “%s” is invalid." +msgid "Invalid storage value: '%s'" msgstr "" -#. TP: Please preserve the trailing space. -#: VirtualMailManager/VirtualMailManager.py:192 -msgid "Enter new password: " +#: VirtualMailManager/cli/subcommands.py:311 +#, python-format +msgid "Neither a valid number of messages nor the keyword 'force': '%s'" msgstr "" -#. TP: Please preserve the trailing space. -#: VirtualMailManager/VirtualMailManager.py:194 -msgid "Retype new password: " +#: VirtualMailManager/cli/subcommands.py:319 +#: VirtualMailManager/cli/subcommands.py:595 +#, python-format +msgid "Not a valid number of messages: '%s'" msgstr "" -#: VirtualMailManager/VirtualMailManager.py:200 -msgid "Sorry, passwords do not match" +#: VirtualMailManager/cli/subcommands.py:354 +#: VirtualMailManager/cli/subcommands.py:609 +#, python-format +msgid "Invalid service arguments: %s" msgstr "" -#: VirtualMailManager/VirtualMailManager.py:203 -msgid "Sorry, empty passwords are not permitted" +#: VirtualMailManager/cli/subcommands.py:363 +msgid "Missing domain name and new transport." msgstr "" -#: VirtualMailManager/VirtualMailManager.py:251 -#: VirtualMailManager/VirtualMailManager.py:338 -#, python-format -msgid "No such directory: %s" +#: VirtualMailManager/cli/subcommands.py:366 +msgid "Missing new transport." msgstr "" -#: VirtualMailManager/VirtualMailManager.py:326 -msgid "Found \"..\" in home directory path." +#: VirtualMailManager/cli/subcommands.py:380 +msgid "Missing UID." msgstr "" -#: VirtualMailManager/VirtualMailManager.py:334 -msgid "Detected owner/group mismatch in home directory." -msgstr "" - -#: VirtualMailManager/VirtualMailManager.py:349 -msgid "Found \"..\" in domain directory path." +#: VirtualMailManager/cli/subcommands.py:381 +#: VirtualMailManager/cli/subcommands.py:545 +#: VirtualMailManager/cli/subcommands.py:551 +msgid "Account" msgstr "" -#: VirtualMailManager/VirtualMailManager.py:355 -msgid "Detected group mismatch in domain directory." +#: VirtualMailManager/cli/subcommands.py:396 +#, python-format +msgid "Unknown help topic: '%s'" msgstr "" -#: VirtualMailManager/VirtualMailManager.py:439 -#, python-format -msgid "" -"Configuration error: \"%s\"\n" -"(in section \"config\", option \"done\") see also: vmm.cfg(5)\n" +#: VirtualMailManager/cli/subcommands.py:409 +msgid "List of available subcommands:" msgstr "" -#: VirtualMailManager/VirtualMailManager.py:459 -#, python-format -msgid "Invalid section: “%s”" +#: VirtualMailManager/cli/subcommands.py:430 +msgid "Usable encoding suffixes:" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:430 +msgid "Usable password schemes:" msgstr "" -#: VirtualMailManager/VirtualMailManager.py:469 -#: VirtualMailManager/VirtualMailManager.py:479 -#: VirtualMailManager/VirtualMailManager.py:498 -#: VirtualMailManager/VirtualMailManager.py:606 -#: VirtualMailManager/VirtualMailManager.py:637 -#, python-format -msgid "Invalid argument: “%s”" +#: VirtualMailManager/cli/subcommands.py:451 +msgid "Missing relocated address and destination." msgstr "" -#: VirtualMailManager/VirtualMailManager.py:502 -msgid "" -"The keyword “detailed” is deprecated and will be removed in a future " -"release.\n" -" Please use the keyword “full” to get full details." +#: VirtualMailManager/cli/subcommands.py:460 +#: VirtualMailManager/cli/subcommands.py:467 +msgid "Missing relocated address." msgstr "" -#: VirtualMailManager/VirtualMailManager.py:575 -#, python-format -msgid "The pattern “%s” contains invalid characters." +#: VirtualMailManager/cli/subcommands.py:490 +#: VirtualMailManager/cli/subcommands.py:503 +#: VirtualMailManager/cli/subcommands.py:516 +#: VirtualMailManager/cli/subcommands.py:568 +#: VirtualMailManager/cli/subcommands.py:603 +msgid "Missing e-mail address." msgstr "" -#: VirtualMailManager/VirtualMailManager.py:601 +#: VirtualMailManager/cli/subcommands.py:497 #, python-format -msgid "The destination account/alias “%s” doesn't exist." +msgid "Generated password: %s" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:552 +msgid "alias addresses" msgstr "" -#: VirtualMailManager/VirtualMailManager.py:618 -#, python-format -msgid "" -"The account has been successfully deleted from the database.\n" -" But an error occurred while deleting the following directory:\n" -" “%(directory)s”\n" -" Reason: %(reason)s" +#: VirtualMailManager/cli/subcommands.py:558 +msgid "Missing e-mail address and user's name." msgstr "" -#: VirtualMailManager/VirtualMailManager.py:658 -msgid "Account doesn't exist" +#: VirtualMailManager/cli/subcommands.py:561 +msgid "Missing user's name." msgstr "" -#: VirtualMailManager/VirtualMailManager.py:674 -#: VirtualMailManager/VirtualMailManager.py:684 -msgid "" -"The service name “managesieve” is deprecated and will be removed\n" -" in a future release.\n" -" Please use the service name “sieve” instead." +#: VirtualMailManager/cli/subcommands.py:579 +msgid "Missing e-mail address and storage value." msgstr "" -#: VirtualMailManager/ext/Postconf.py:41 -#, python-format -msgid "" -"The value “%s” doesn't look like a valid postfix configuration parameter " -"name." +#: VirtualMailManager/cli/subcommands.py:617 +msgid "Missing e-mail address and transport." +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:620 +msgid "Missing transport." +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:630 +msgid "usage: " msgstr "" #. TP: Please adjust translated words like the original text. #. (It's a table header.) Extract from usage text: -#. Usage: vmm SUBCOMMAND OBJECT ARGS* +#. usage: vmm subcommand arguments #. short long -#. subcommand object args (* = optional) +#. subcommand arguments #. -#. da domainadd domain.tld transport* -#. di domaininfo domain.tld details* -#: vmm:26 +#. da domainadd fqdn [transport] +#. dd domaindelete fqdn [force] +#: VirtualMailManager/cli/subcommands.py:640 #, python-format msgid "" -"Usage: %s SUBCOMMAND OBJECT ARGS*\n" +"usage: %s subcommand arguments\n" " short long\n" -" subcommand object args (* = optional)\n" +" subcommand arguments\n" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:659 +msgid "from" +msgstr "" + +#. TP: The words 'from', 'version' and 'on' are used in +#. the version information, e.g.: +#. vmm, version 0.5.2 (from 09/09/09) +#. Python 2.5.4 on FreeBSD +#: VirtualMailManager/cli/subcommands.py:659 +msgid "version" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:662 +msgid "on" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:664 +msgid "is free software and comes with ABSOLUTELY NO WARRANTY." +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:672 +msgid "uid" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:673 +msgid "get the address of the user with the given UID" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:674 +#: VirtualMailManager/cli/subcommands.py:684 +msgid "address [password]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:675 +msgid "create a new e-mail user with the given address" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:677 +#: VirtualMailManager/cli/subcommands.py:704 +#: VirtualMailManager/cli/subcommands.py:744 +#: VirtualMailManager/cli/subcommands.py:746 +msgid "address" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:678 +msgid "delete the specified user" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:679 +msgid "address [details]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:680 +msgid "display information about the given address" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:681 +msgid "address name" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:682 +msgid "set or update the real name for an address" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:685 +msgid "update the password for the given address" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:687 +msgid "address storage [messages]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:688 +msgid "update the quota limit for the given address" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:690 +msgid "address [service ...]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:691 +msgid "enables the specified services and disables all not specified services" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:694 +msgid "address transport" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:695 +msgid "update the transport of the given address" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:697 +msgid "address destination ..." +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:698 +msgid "create a new alias e-mail address with one or more destinations" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:701 +msgid "address [destination]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:702 +msgid "delete the specified alias e-mail address or one of its destinations" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:705 +msgid "show the destination(s) of the specified alias" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:708 +#: VirtualMailManager/cli/subcommands.py:717 +msgid "fqdn destination" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:709 +msgid "create a new alias for an existing domain" msgstr "" -#: vmm:65 vmm:76 -#, python-format -msgid "Error: %s\n" +#: VirtualMailManager/cli/subcommands.py:711 +#: VirtualMailManager/cli/subcommands.py:714 +#: VirtualMailManager/cli/subcommands.py:723 +msgid "fqdn" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:712 +msgid "delete the specified alias domain" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:715 +msgid "show the destination of the given alias domain" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:718 +msgid "assign the given alias domain to an other domain" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:720 +msgid "fqdn [transport]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:721 +msgid "create a new domain" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:724 +msgid "delete the given domain and all its alias domains" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:725 +msgid "fqdn [details]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:726 +msgid "display information about the given domain" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:728 +msgid "fqdn storage [messages]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:729 +msgid "update the quota limit of the specified domain" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:731 +msgid "fqdn [service ...]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:732 +msgid "" +"enables the specified services and disables all not specified services of " +"the given domain" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:735 +msgid "fqdn transport" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:736 +msgid "update the transport of the specified domain" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:737 +msgid "[pattern]" msgstr "" -#. TP: e.g. 'Domain information' or 'Account information' -#: vmm:104 +#: VirtualMailManager/cli/subcommands.py:738 +msgid "list all domains / search domains by pattern" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:741 +msgid "address newaddress" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:742 +msgid "create a new record for a relocated user" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:745 +msgid "delete the record of the relocated user" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:747 +msgid "print information about a relocated user" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:749 +msgid "option" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:750 +msgid "show the actual value of the configuration option" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:751 +msgid "option value" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:752 +msgid "set a new value for the configuration option" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:753 +msgid "[section]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:754 +msgid "start interactive configuration modus" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:756 +msgid "lists all usable password schemes and password encoding suffixes" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:758 +msgid "[subcommand]" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:759 +msgid "show a help overview or help for the given subcommand" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:761 +msgid "show version and copyright information" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:809 +#, python-format +msgid "[%(percent)s%%] %(used)s/%(limit)s" +msgstr "" + +#. TP: used in e.g. 'Domain information' or 'Account information' +#: VirtualMailManager/cli/subcommands.py:815 msgid "information" msgstr "" -#. TP: e.g. 'Available alias addresses' or 'Available accounts' -#: vmm:115 -msgid "Available" +#. TP: used in e.g. 'Existing alias addresses' or 'Existing accounts' +#: VirtualMailManager/cli/subcommands.py:828 +msgid "Existing" msgstr "" -#: vmm:118 vmm:218 vmm:224 -msgid "alias domains" -msgstr "" - -#: vmm:128 vmm:139 vmm:163 +#: VirtualMailManager/cli/subcommands.py:841 +#: VirtualMailManager/cli/subcommands.py:883 msgid "\tNone" msgstr "" -#: vmm:132 +#: VirtualMailManager/cli/subcommands.py:846 msgid "Alias information" msgstr "" -#: vmm:134 +#: VirtualMailManager/cli/subcommands.py:848 #, python-format msgid "\tMail for %s will be redirected to:" msgstr "" -#: vmm:143 +#: VirtualMailManager/cli/subcommands.py:855 msgid "Relocated information" msgstr "" -#: vmm:145 +#: VirtualMailManager/cli/subcommands.py:857 #, python-format -msgid "\tUser “%(addr)s” has moved to “%(dest)s”" +msgid "\tUser '%(addr)s' has moved to '%(dest)s'" msgstr "" -#: vmm:158 -msgid "Available domains" -msgstr "" - -#: vmm:160 +#: VirtualMailManager/cli/subcommands.py:872 msgid "Matching domains" msgstr "" -#: vmm:174 +#: VirtualMailManager/cli/subcommands.py:874 +msgid "Existing domains" +msgstr "" + +#: VirtualMailManager/cli/subcommands.py:889 msgid "Alias domain information" msgstr "" -#: vmm:180 +#: VirtualMailManager/cli/subcommands.py:894 #, python-format msgid "" "\tThe alias domain %(alias)s belongs to:\n" "\t * %(domain)s" msgstr "" -#: vmm:191 vmm:199 vmm:207 -msgid "Missing domain name." +#: VirtualMailManager/common.py:63 +#, python-format +msgid "No such file: '%s'" +msgstr "" + +#: VirtualMailManager/common.py:66 +#, python-format +msgid "File is not executable: '%s'" +msgstr "" + +#: VirtualMailManager/common.py:82 +msgid "GiB" +msgstr "" + +#: VirtualMailManager/common.py:82 +msgid "TiB" msgstr "" -#: vmm:210 vmm:214 -msgid "Domain" +#: VirtualMailManager/common.py:83 +msgid "KiB" +msgstr "" + +#: VirtualMailManager/common.py:83 +msgid "MiB" msgstr "" -#: vmm:216 vmm:225 -msgid "accounts" +#. TP: e.g.: '%(size)s %(prefix)s' -> '118.30 MiB' +#: VirtualMailManager/common.py:87 +#, python-format +msgid "%(size)s %(prefix)s" +msgstr "" + +#: VirtualMailManager/config.py:89 +#, python-format +msgid "Not a boolean: '%s'" msgstr "" -#: vmm:220 vmm:226 -msgid "aliases" +#: VirtualMailManager/config.py:127 +#, python-format +msgid "Bad format: '%s' - expected: section.option" +msgstr "" + +#: VirtualMailManager/config.py:380 +#, python-format +msgid "* Section: %s\n" +msgstr "" + +#: VirtualMailManager/config.py:390 VirtualMailManager/config.py:398 +#, python-format +msgid "Check of configuration file %s failed.\n" +msgstr "" + +#: VirtualMailManager/config.py:392 +msgid "Missing options, which have no default value.\n" msgstr "" -#: vmm:222 vmm:227 -msgid "relocated users" +#: VirtualMailManager/config.py:400 VirtualMailManager/config.py:402 +msgid "Invalid configuration values.\n" +msgstr "" + +#: VirtualMailManager/config.py:441 VirtualMailManager/config.py:525 +#, python-format +msgid "Not a valid Dovecot version: '%s'" msgstr "" -#: vmm:238 -msgid "Missing domain name and new transport." +#: VirtualMailManager/config.py:447 VirtualMailManager/config.py:482 +#, python-format +msgid "Unsupported database module: '%s'" msgstr "" -#: vmm:240 -msgid "Missing new transport." +#: VirtualMailManager/config.py:452 VirtualMailManager/config.py:490 +#, python-format +msgid "Unknown pgsql SSL mode: '%s'" +msgstr "" + +#: VirtualMailManager/config.py:459 VirtualMailManager/config.py:503 +#: VirtualMailManager/maillocation.py:70 +#, python-format +msgid "Unsupported mailbox format: '%s'" msgstr "" -#: vmm:249 vmm:272 -msgid "Missing alias domain name and target domain name." +#: VirtualMailManager/config.py:475 VirtualMailManager/handler.py:283 +#: VirtualMailManager/handler.py:357 VirtualMailManager/handler.py:362 +#: VirtualMailManager/handler.py:390 +#, python-format +msgid "No such directory: %s" +msgstr "" + +#: VirtualMailManager/config.py:514 +#, python-format +msgid "Not a valid size value: '%s'" +msgstr "" + +#: VirtualMailManager/domain.py:78 +#, python-format +msgid "The domain '%s' is an alias domain." msgstr "" -#: vmm:251 vmm:274 -msgid "Missing target domain name." +#: VirtualMailManager/domain.py:108 +#, python-format +msgid "" +"There are %(account_count)u accounts, %(alias_count)u aliases and " +"%(relocated_count)u relocated users." +msgstr "" + +#: VirtualMailManager/domain.py:123 +#, python-format +msgid "The domain '%s' already exists." msgstr "" -#: vmm:257 vmm:280 -msgid "Missing alias domain name." +#: VirtualMailManager/domain.py:437 +msgid "The domain name is too long" msgstr "" -#: vmm:286 vmm:295 vmm:303 vmm:345 vmm:353 vmm:361 -msgid "Missing e-mail address." +#: VirtualMailManager/domain.py:439 +#, python-format +msgid "The domain name '%s' is invalid" +msgstr "" + +#: VirtualMailManager/emailaddress.py:73 +#, python-format +msgid "Missing the '@' sign in address: '%s'" msgstr "" -#: vmm:312 -msgid "alias addresses" +#: VirtualMailManager/emailaddress.py:76 +#, python-format +msgid "Too many '@' signs in address: '%s'" msgstr "" -#: vmm:329 -msgid "Missing e-mail address and user’s name." +#: VirtualMailManager/emailaddress.py:79 +#, python-format +msgid "Missing local-part in address: '%s'" +msgstr "" + +#: VirtualMailManager/emailaddress.py:82 +#, python-format +msgid "Missing domain name in address: '%s'" +msgstr "" + +#: VirtualMailManager/emailaddress.py:145 +#, python-format +msgid "The local-part '%s' is too long." msgstr "" -#: vmm:331 -msgid "Missing user’s name." +#: VirtualMailManager/emailaddress.py:150 +#, python-format +msgid "The local-part '%(l_part)s' contains invalid characters: %(i_chars)s" msgstr "" -#: vmm:337 -msgid "Missing e-mail address and transport." +#: VirtualMailManager/ext/postconf.py:84 +#, python-format +msgid "" +"The value '%s' does not look like a valid postfix configuration parameter " +"name." msgstr "" -#: vmm:339 -msgid "Missing transport." +#: VirtualMailManager/handler.py:56 +msgid "an account" +msgstr "" + +#: VirtualMailManager/handler.py:57 +msgid "an alias" +msgstr "" + +#: VirtualMailManager/handler.py:58 +msgid "a relocated user" msgstr "" -#: vmm:370 -msgid "Missing alias address and destination." +#: VirtualMailManager/handler.py:84 +msgid "" +"You are not root.\n" +"\tGood bye!\n" +msgstr "" + +#: VirtualMailManager/handler.py:104 +#, python-format +msgid "Could not find '%(cfg_file)s' in: '%(cfg_path)s'" +msgstr "" + +#: VirtualMailManager/handler.py:115 +#, python-format +msgid "" +"wrong permissions for '%(file)s': %(perms)s\n" +"`chmod 0600 %(file)s` would be great." msgstr "" -#: vmm:372 vmm:407 -msgid "Missing destination address." +#: VirtualMailManager/handler.py:135 +#, python-format +msgid "" +"'%(path)s' is not a directory.\n" +"(%(cfg_file)s: section 'misc', option 'base_directory')" msgstr "" -#: vmm:378 vmm:396 -msgid "Missing alias address" +#: VirtualMailManager/handler.py:144 +#, python-format +msgid "" +"\n" +"(%(cfg_file)s: section 'bin', option '%(option)s')" +msgstr "" + +#: VirtualMailManager/handler.py:158 VirtualMailManager/handler.py:165 +#, python-format +msgid "Unable to import database module '%s'." msgstr "" -#: vmm:405 -msgid "Missing relocated address and destination." +#. TP: %(a_type)s will be one of: 'an account', 'an alias' or +#. 'a relocated user' +#: VirtualMailManager/handler.py:244 +#, python-format +msgid "There is already %(a_type)s with the address '%(address)s'." msgstr "" -#: vmm:413 vmm:431 -msgid "Missing relocated address" +#: VirtualMailManager/handler.py:297 +#, python-format +msgid "'%s' is not a directory." +msgstr "" + +#: VirtualMailManager/handler.py:300 +#, python-format +msgid "The file/directory '%s' already exists." +msgstr "" + +#: VirtualMailManager/handler.py:329 +msgid "Skipped mailbox folders:" msgstr "" -#: vmm:437 -msgid "Missing userid" +#: VirtualMailManager/handler.py:349 +#, python-format +msgid "" +"UID '%(uid)u' and/or GID '%(gid)u' are less than %(min_uid)u/%(min_gid)u." +msgstr "" + +#: VirtualMailManager/handler.py:354 VirtualMailManager/handler.py:387 +#, python-format +msgid "Found \"..\" in domain directory path: %s" msgstr "" -#: vmm:450 -msgid "Warnings:" +#: VirtualMailManager/handler.py:367 +msgid "Detected owner/group mismatch in home directory." msgstr "" -#: vmm:460 -msgid "from" +#: VirtualMailManager/handler.py:383 +#, python-format +msgid "GID '%(gid)u' is less than '%(min_gid)u'." +msgstr "" + +#: VirtualMailManager/handler.py:394 +#, python-format +msgid "Detected group mismatch in domain directory: %s" msgstr "" -#. TP: The words 'from', 'version' and 'on' are used in the version -#. information: -#. vmm, version 0.5.2 (from 09/09/09) -#. Python 2.5.4 on FreeBSD -#: vmm:460 -msgid "version" +#: VirtualMailManager/handler.py:470 VirtualMailManager/handler.py:748 +#, python-format +msgid "Unknown service: '%s'" +msgstr "" + +#: VirtualMailManager/handler.py:587 +#, python-format +msgid "The pattern '%s' contains invalid characters." +msgstr "" + +#: VirtualMailManager/handler.py:614 +msgid "Ignored destination addresses:" msgstr "" -#: vmm:463 -msgid "on" +#: VirtualMailManager/handler.py:619 VirtualMailManager/handler.py:769 +#, python-format +msgid "The destination account/alias '%s' does not exist." msgstr "" -#: vmm:464 -msgid "is free software and comes with ABSOLUTELY NO WARRANTY." +#: VirtualMailManager/handler.py:641 +#, python-format +msgid "" +"The account has been successfully deleted from the database.\n" +" But an error occurred while deleting the following directory:\n" +" '%(directory)s'\n" +" Reason: %(reason)s" +msgstr "" + +#: VirtualMailManager/handler.py:712 +#, python-format +msgid "Could not accept name: '%s'" +msgstr "" + +#: VirtualMailManager/handler.py:735 +#, python-format +msgid "Could not accept transport: '%s'" msgstr "" -#: vmm:472 +#: VirtualMailManager/handler.py:779 VirtualMailManager/relocated.py:98 +#: VirtualMailManager/relocated.py:105 #, python-format -msgid "Plan A failed ... trying Plan B: %(subcommand)s %(object)s" +msgid "The relocated user '%s' does not exist." msgstr "" -#: vmm:535 +#: VirtualMailManager/mailbox.py:260 #, python-format -msgid "Unknown subcommand: “%s”" +msgid "Failed to create mailboxes: %r\n" +msgstr "" + +#: VirtualMailManager/maillocation.py:74 +msgid "Empty directory name" +msgstr "" + +#: VirtualMailManager/maillocation.py:76 +#, python-format +msgid "Directory name is too long: '%s'" msgstr "" -#. TP: We have to cry, because root has killed/interrupted vmm -#. with Ctrl+C or Ctrl+D. -#: vmm:540 -msgid "" -"\n" -"Ouch!\n" +#: VirtualMailManager/password.py:388 +#, python-format +msgid "Unsupported password scheme: '%s'" +msgstr "" + +#: VirtualMailManager/password.py:391 +#, python-format +msgid "The password scheme '%(scheme)s' requires Dovecot >= v%(version)s." +msgstr "" + +#: VirtualMailManager/password.py:397 +msgid "Encoding suffixes for password schemes require Dovecot >= v1.1.alpha1." msgstr "" -#: vmm:543 +#: VirtualMailManager/password.py:400 #, python-format -msgid "Error: %s" +msgid "Unsupported password encoding: '%s'" +msgstr "" + +#: VirtualMailManager/relocated.py:71 +msgid "Address and destination are identical." msgstr "" + +#: VirtualMailManager/relocated.py:75 +#, python-format +msgid "The relocated user '%s' already exists." +msgstr "" diff -r c0e1fb1b0145 -r a4aead244f75 postfix/pgsql-relocated_maps.cf --- a/postfix/pgsql-relocated_maps.cf Mon Nov 07 03:22:15 2011 +0000 +++ b/postfix/pgsql-relocated_maps.cf Thu Jun 28 19:26:50 2012 +0000 @@ -11,4 +11,4 @@ dbname = mailsys # The SQL query template used to search the database -query = SELECT destination FROM postfix_relocated WHERE address='%s' +query = SELECT destination FROM postfix_relocated_map('%u', '%d') diff -r c0e1fb1b0145 -r a4aead244f75 postfix/pgsql-smtpd_sender_login_maps.cf --- a/postfix/pgsql-smtpd_sender_login_maps.cf Mon Nov 07 03:22:15 2011 +0000 +++ b/postfix/pgsql-smtpd_sender_login_maps.cf Thu Jun 28 19:26:50 2012 +0000 @@ -15,4 +15,4 @@ # * line 26: function postfix_smtpd_sender_login_map + comment above # # The SQL query template used to search the database -#query = SELECT login FROM postfix_smtpd_sender_login_map('%u', '%d') +query = SELECT login FROM postfix_smtpd_sender_login_map('%u', '%d') diff -r c0e1fb1b0145 -r a4aead244f75 postfix/pgsql-transport.cf --- a/postfix/pgsql-transport.cf Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,14 +0,0 @@ -# All parameters are described in pgsql_table(5) / PGSQL PARAMETERS -# -# The hosts that Postfix will try to connect to and query from. -hosts = localhost - -# The user name and password to log into the pgsql server. -user = postfix -password = some_password - -# The database name on the servers. -dbname = mailsys - -# The SQL query template used to search the database -query = SELECT transport FROM postfix_transport WHERE address='%s' diff -r c0e1fb1b0145 -r a4aead244f75 postfix/pgsql-transport_maps.cf --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/postfix/pgsql-transport_maps.cf Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,14 @@ +# All parameters are described in pgsql_table(5) / PGSQL PARAMETERS +# +# The hosts that Postfix will try to connect to and query from. +hosts = localhost + +# The user name and password to log into the pgsql server. +user = postfix +password = some_password + +# The database name on the servers. +dbname = mailsys + +# The SQL query template used to search the database +query = SELECT transport FROM postfix_transport_map('%u', '%d') diff -r c0e1fb1b0145 -r a4aead244f75 postfix/pgsql-virtual_alias_maps.cf --- a/postfix/pgsql-virtual_alias_maps.cf Mon Nov 07 03:22:15 2011 +0000 +++ b/postfix/pgsql-virtual_alias_maps.cf Thu Jun 28 19:26:50 2012 +0000 @@ -11,4 +11,4 @@ dbname = mailsys # The SQL query template used to search the database -query = SELECT destination FROM postfix_alias WHERE address='%s' +query = SELECT destination FROM postfix_virtual_alias_map('%u', '%d') diff -r c0e1fb1b0145 -r a4aead244f75 postfix/pgsql-virtual_mailbox_maps.cf --- a/postfix/pgsql-virtual_mailbox_maps.cf Mon Nov 07 03:22:15 2011 +0000 +++ b/postfix/pgsql-virtual_mailbox_maps.cf Thu Jun 28 19:26:50 2012 +0000 @@ -11,4 +11,4 @@ dbname = mailsys # The SQL query template used to search the database -query = SELECT maildir FROM postfix_maildir WHERE address='%s' +query = SELECT maildir FROM postfix_virtual_mailbox_map('%u', '%d') diff -r c0e1fb1b0145 -r a4aead244f75 postfix/pgsql-virtual_uid_maps.cf --- a/postfix/pgsql-virtual_uid_maps.cf Mon Nov 07 03:22:15 2011 +0000 +++ b/postfix/pgsql-virtual_uid_maps.cf Thu Jun 28 19:26:50 2012 +0000 @@ -11,4 +11,4 @@ dbname = mailsys # The SQL query template used to search the database -query = SELECT uid FROM postfix_uid WHERE address='%s' +query = SELECT uid FROM postfix_virtual_uid_map('%u', '%d') diff -r c0e1fb1b0145 -r a4aead244f75 setup.py --- a/setup.py Mon Nov 07 03:22:15 2011 +0000 +++ b/setup.py Thu Jun 28 19:26:50 2012 +0000 @@ -1,55 +1,67 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -# Copyright 2007 - 2010, Pascal Volk +# Copyright 2007 - 2012, Pascal Volk # See COPYING for distribution information. import os from distutils.core import setup +from distutils.dist import DistributionMetadata VERSION = '0.5.2' +descr = 'Tool to manage mail domains/accounts/aliases for Dovecot and Postfix' long_description = """ vmm, a virtual mail manager, is a command line tool for administrators/postmasters to manage (alias-)domains, accounts, aliases and relocated users. It is designed for Dovecot and Postfix with a PostgreSQL backend. """ +packages = [ + 'VirtualMailManager', + 'VirtualMailManager.cli', + 'VirtualMailManager.ext', + 'VirtualMailManager.pycompat', +] +# http://pypi.python.org/pypi?%3Aaction=list_classifiers +classifiers = ['Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: Dutch', + 'Natural Language :: English', + 'Natural Language :: French', + 'Natural Language :: German', + 'Operating System :: POSIX', + 'Operating System :: POSIX :: BSD', + 'Operating System :: POSIX :: Linux', + 'Operating System :: POSIX :: Other', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Topic :: Communications :: Email', + 'Topic :: System :: Systems Administration', + 'Topic :: Utilities'] + +# sucessfuly tested on: +platforms = ['freebsd7', 'linux2', 'openbsd4'] # remove existing MANIFEST if os.path.exists('MANIFEST'): os.remove('MANIFEST') +setup_args = {'name': 'VirtualMailManager', + 'version': VERSION, + 'description': descr, + 'long_description': long_description, + 'packages': packages, + 'author': 'Pascal Volk', + 'author_email': 'user+vmm@localhost.localdomain.org', + 'license': 'BSD License', + 'url': 'http://vmm.localdomain.org/', + 'download_url':'http://sf.net/projects/vmm/files/', + 'platforms': platforms, + 'classifiers': classifiers} -setup(name='VirtualMailManager', - version=VERSION, - description='Tool to manage mail domains/accounts/aliases for Dovecot and Postfix', - long_description=long_description, - packages=['VirtualMailManager', 'VirtualMailManager.ext', - 'VirtualMailManager.constants'], - author='Pascal Volk', - author_email='neverseen@users.sourceforge.net', - license='BSD License', - url='http://vmm.localdomain.org/', - download_url='http://sf.net/projects/vmm/files/', - platforms=['freebsd7', 'linux2', 'openbsd4'], - classifiers=[ - 'Development Status :: 4 - Beta', - 'Development Status :: 5 - Production/Stable', - 'Environment :: Console', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: BSD License', - 'Natural Language :: Dutch', - 'Natural Language :: English', - 'Natural Language :: French', - 'Natural Language :: German', - 'Operating System :: POSIX', - 'Operating System :: POSIX :: BSD', - 'Operating System :: POSIX :: Linux', - 'Operating System :: POSIX :: Other', - 'Programming Language :: Python', - 'Topic :: Communications :: Email', - 'Topic :: System :: Systems Administration', - 'Topic :: Utilities' - ], - requires=['pyPgSQL'] - ) +if 'requires' in DistributionMetadata._METHOD_BASENAMES: + setup_args['requires'] = ['psycopg2 (>=2.0)', 'pyPgSQL (>=2.5.1)'] + +setup(**setup_args) diff -r c0e1fb1b0145 -r a4aead244f75 update_config.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/update_config.py Thu Jun 28 19:26:50 2012 +0000 @@ -0,0 +1,161 @@ +#!/usr/bin/env python +# -*- coding: UTF-8 -*- +# Copyright (c) 2008 - 2012, Pascal Volk +# See COPYING for distribution information. + +import os +os.sys.path.remove(os.sys.path[0]) +from time import time +from ConfigParser import ConfigParser +from shutil import copy2 +try: + from VirtualMailManager.constants.VERSION import VERSION +except ImportError: + os.sys.stderr.write('error: no pre 0.6.0 version information found\n') + raise SystemExit(2) + +# we have to remove the old CamelCase files +import VirtualMailManager +vmm_inst_dir = os.path.dirname(VirtualMailManager.__file__) +tmp_info = open('/tmp/vmm_inst_dir', 'w') +tmp_info.write(vmm_inst_dir) +tmp_info.close() + +try: + import psycopg2 +except ImportError: + has_psycopg2 = False +else: + has_psycopg2 = True + +def get_config_file(): + f = None + for d in ('/root', '/usr/local/etc', '/etc'): + tmp = os.path.join(d, 'vmm.cfg') + if os.path.isfile(tmp): + f = tmp + break + if f: + return f + else: + os.sys.stderr.write('error: vmm.cfg not found\n') + raise SystemExit(2) + +def update(cp): + if VERSION == '0.5.2': + upd_052(cp) + elif VERSION == '0.6.0': + os.sys.stdout.write('info: nothing to do for version %s\n' % VERSION) + return + else: + os.sys.stderr.write( + 'error: the version %s is not supported by this script\n' % VERSION) + raise SystemExit(3) + +def get_cfg_parser(cf): + fh = open(cf, 'r') + cp = ConfigParser() + cp.readfp(fh) + fh.close() + return cp + +def update_cfg_file(cp, cf): + copy2(cf, cf+'.bak.'+str(time())) + fh = open(cf, 'w') + cp.write(fh) + fh.close() + +def add_sections(cp, sections): + for section in sections: + if not cp.has_section(section): + cp.add_section(section) + +def move_option(cp, src, dst): + ds, do = dst.split('.') + if not cp.has_option(ds, do): + ss, so = src.split('.') + cp.set(ds, do, cp.get(ss, so)) + cp.remove_option(ss, so) + sect_opt.append((dst, 'R')) + +def add_option(cp, dst, val): + ds, do = dst.split('.') + if not cp.has_option(ds, do): + cp.set(ds, do, val) + sect_opt.append((dst, 'N')) + + +def set_dovecot_version(cp): + if len(os.sys.argv) > 1: + dovecot_version = os.sys.argv[1].strip() + if not dovecot_version: + dovecot_version = '1.2.11' + else: + dovecot_version = '1.2.11' + cp.set('misc', 'dovecot_version', dovecot_version) + sect_opt.append(('misc.dovecot_version', 'M')) + + +def get_option(cp, src): + ss, so = src.split('.') + return cp.get(ss, so) + +def upd_052(cp): + global had_config + global had_gid_mail + + had_config = cp.remove_section('config') + had_gid_mail = cp.remove_option('misc', 'gid_mail') + add_sections(cp, ('domain', 'account', 'mailbox')) + if cp.has_section('domdir'): + for src, dst in (('domdir.mode', 'domain.directory_mode'), + ('domdir.delete', 'domain.delete_directory'), + ('domdir.base', 'misc.base_directory')): + move_option(cp, src, dst) + cp.remove_section('domdir') + if cp.has_section('services'): + for service in cp.options('services'): + move_option(cp, 'services.%s' % service, 'domain.%s' % service) + cp.remove_section('services') + for src, dst in (('maildir.mode', 'account.directory_mode'), + ('maildir.diskusage', 'account.disk_usage'), + ('maildir.delete', 'account.delete_directory'), + ('maildir.folders', 'mailbox.folders'), + ('maildir.name', 'mailbox.root'), + ('misc.forcedel', 'domain.force_deletion'), + ('misc.transport', 'domain.transport'), + ('misc.passwdscheme', 'misc.password_scheme'), + ('misc.dovecotvers', 'misc.dovecot_version')): + move_option(cp, src, dst) + cp.remove_section('maildir') + if not has_psycopg2: + add_option(cp, 'database.module', 'pyPgSQL') + set_dovecot_version(cp) + + +# def main(): +if __name__ == '__main__': + sect_opt = [] + had_config = False + had_gid_mail = False + cf = get_config_file() + cp = get_cfg_parser(cf) + update(cp) + if len(sect_opt): + update_cfg_file(cp, cf) + sect_opt.sort() + print 'Please have a look at your configuration: %s' %cf + print 'This are your Modified/Renamed/New settings:' + for s_o, st in sect_opt: + print '%s %s = %s' % (st, s_o, get_option(cp, s_o)) + if had_config: + print '\nRemoved section "config" with option "done" (obsolte)' + if had_gid_mail: + print '\nRemoved option "gid_mail" from section "misc" (obsolte)\n' + os.sys.exit(0) + if had_config or had_gid_mail: + update_cfg_file(cp, cf) + if had_config: + print '\nRemoved section "config" with option "done" (obsolte)' + if had_gid_mail: + print '\nRemoved option "gid_mail" from section "misc" (obsolte)\n' diff -r c0e1fb1b0145 -r a4aead244f75 update_config_0.4.x-0.5.py --- a/update_config_0.4.x-0.5.py Mon Nov 07 03:22:15 2011 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,113 +0,0 @@ -#!/usr/bin/env python -# -*- coding: UTF-8 -*- -# Copyright (c) 2008 - 2010, Pascal Volk -# See COPYING for distribution information. - -import os -os.sys.path.remove(os.sys.path[0]) -from time import time -from ConfigParser import ConfigParser -from shutil import copy2 -from VirtualMailManager.constants.VERSION import VERSION - - -def get_config_file(): - f = None - for d in ('/root', '/usr/local/etc', '/etc'): - tmp = os.path.join(d, 'vmm.cfg') - if os.path.isfile(tmp): - f = tmp - break - if f: - return f - else: - os.sys.stderr.write('error: vmm.cfg not found\n') - os.sys.exit(2) - -def update(cp): - if VERSION == '0.4': - upd_040(cp) - elif VERSION == '0.5': - upd_050(cp) - elif VERSION == '0.5.1': - upd_051(cp) - elif VERSION == '0.5.2': - os.sys.stdout.write('info: nothing to do for version %s\n' % VERSION) - os.sys.exit(0) - else: - os.sys.stderr.write( - 'error: the version %s is not supported by this script\n' % VERSION) - os.sys.exit(3) - -def get_cfg_parser(cf): - fh = file(cf, 'r') - cp = ConfigParser() - cp.readfp(fh) - fh.close() - return cp - -def update_cfg_file(cp, cf): - copy2(cf, cf+'.bak.'+str(time())) - fh = file(cf, 'w') - cp.write(fh) - fh.close() - -def upd_040(cp): - if not cp.has_option('maildir', 'name') or not cp.has_option('maildir', - 'folders') or cp.has_option('maildir', 'folder'): - if not cp.has_option('maildir', 'name'): - if cp.has_option('maildir', 'folder'): - cp.set('maildir', 'name', cp.get('maildir', 'folder')) - cp.remove_option('maildir', 'folder') - sect_opt.append(('maildir', 'name')) - else: - cp.set('maildir', 'name', 'Maildir') - sect_opt.append(('maildir', 'name')) - if not cp.has_option('maildir', 'folders'): - cp.set('maildir', 'folders', 'Drafts:Sent:Templates:Trash') - sect_opt.append(('maildir', 'folders')) - if cp.has_option('maildir', 'folder'): - cp.remove_option('maildir', 'folder') - upd_050(cp) - -def upd_050(cp): - if not cp.has_option('bin', 'postconf'): - try: - postconf = os.sys.argv[1].strip() - if len(postconf): - cp.set('bin', 'postconf', postconf) - sect_opt.append(('bin', 'postconf')) - else: # possible? - cp.set('bin', 'postconf', '/usr/sbin/postconf') - sect_opt.append(('bin', 'postconf')) - except IndexError: - cp.set('bin', 'postconf', '/usr/sbin/postconf') - sect_opt.append(('bin', 'postconf')) - upd_051(cp) - -def upd_051(cp): - if not cp.has_option('misc', 'dovecotvers') or cp.has_option('services', - 'managesieve'): - if not cp.has_option('misc', 'dovecotvers'): - cp.set('misc', 'dovecotvers', os.sys.argv[2].strip()) - sect_opt.append(('misc', 'dovecotvers')) - if cp.has_option('services', 'managesieve'): - cp.set('services','sieve',cp.getboolean('services', 'managesieve')) - cp.remove_option('services', 'managesieve') - sect_opt.append(('services', 'sieve')) - -# def main(): -if __name__ == '__main__': - sect_opt = [] - cf = get_config_file() - cp = get_cfg_parser(cf) - update(cp) - if len(sect_opt): - update_cfg_file(cp, cf) - print 'Please have a look at your configuration: %s' %cf - print 'and verify the value from:' - for s_o in sect_opt: - print ' [%s] %s' % s_o - print - - diff -r c0e1fb1b0145 -r a4aead244f75 upgrade.sh --- a/upgrade.sh Mon Nov 07 03:22:15 2011 +0000 +++ b/upgrade.sh Thu Jun 28 19:26:50 2012 +0000 @@ -7,10 +7,7 @@ PATH=/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin PREFIX=/usr/local -PF_CONFDIR=$(postconf -h config_directory) -PF_GID=$(id -g $(postconf -h mail_owner)) -POSTCONF=$(which postconf) -DOVECOT_VERS=$(dovecot --version | awk -F . '{print $1 $2}') +DOVECOT_VERS=$(dovecot --version | awk '{print $1}') LOCALE_DIR=${PREFIX}/share/locale DOC_DIR=${PREFIX}/share/doc/vmm if [ ${PREFIX} = "/usr" ]; then @@ -18,10 +15,9 @@ else MANDIR=${PREFIX}/man fi -DOCS="ChangeLog COPYING NEWS INSTALL README" +DOCS="ChangeLog Configure.Dovecot_2 COPYING INSTALL NEWS README" INSTALL_OPTS="-g 0 -o 0 -p" -INSTALL_OPTS_CF="-b -m 0640 -g ${PF_GID} -o 0 -p" if [ $(id -u) -ne 0 ]; then echo "Run this script as root." @@ -29,7 +25,7 @@ fi # update config file before installing the new files. -./update_config_0.4.x-0.5.py ${POSTCONF} ${DOVECOT_VERS:-10} +./update_config.py ${DOVECOT_VERS:-'1.2.11'} rv=$? if [ $rv -eq 2 ]; then echo "please run the install.sh script" @@ -38,12 +34,18 @@ echo "please read the upgrade instructions at http://vmm.localdomain.org/" exit 1 elif [ $rv -ne 0 ]; then - echo "Sorry, something went wrong. Please file a bug:" - echo "https://sourceforge.net/tracker/?group_id=213727" + echo "Sorry, something went wrong. Please file a bug at:" + echo "https://bitbucket.org/pvo/vmm/issues" exit 1 fi -python setup.py -q install --prefix ${PREFIX} +# remove old CamelCase files +if [ -f /tmp/vmm_inst_dir ] ; then + rm -rf `cat /tmp/vmm_inst_dir` + rm -f /tmp/vmm_inst_dir +fi + +python setup.py -q install --force --prefix ${PREFIX} python setup.py clean --all >/dev/null install -m 0700 ${INSTALL_OPTS} vmm ${PREFIX}/sbin @@ -58,14 +60,6 @@ done cd - >/dev/null -# remove misplaced manual pages -if [ -f /usr/local/share/man/man1/vmm.1 ]; then - rm -f /usr/local/share/man/man1/vmm.1 -fi -if [ -f /usr/local/share/man/man5/vmm.cfg.5 ]; then - rm -f /usr/local/share/man/man5/vmm.cfg.5 -fi - # install manual pages cd man [ -d ${MANDIR}/man1 ] || mkdir -m 0755 -p ${MANDIR}/man1 @@ -74,7 +68,7 @@ [ -d ${MANDIR}/man5 ] || mkdir -m 0755 -p ${MANDIR}/man5 install -m 0644 ${INSTALL_OPTS} man5/vmm.cfg.5 ${MANDIR}/man5 -for l in $(find . -maxdepth 1 -mindepth 1 -type d \! -name man\? \! -name .svn) +for l in $(find . -maxdepth 1 -mindepth 1 -type d \! -name man\?) do for s in man1 man5; do [ -d ${MANDIR}/${l}/${s} ] || mkdir -m 0755 -p ${MANDIR}/${l}/${s} diff -r c0e1fb1b0145 -r a4aead244f75 vmm --- a/vmm Mon Nov 07 03:22:15 2011 +0000 +++ b/vmm Thu Jun 28 19:26:50 2012 +0000 @@ -1,545 +1,18 @@ #!/usr/bin/env python # -*- coding: UTF-8 -*- -# Copyright 2007 - 2010, Pascal Volk +# Copyright 2007 - 2012, Pascal Volk # See COPYING for distribution information. """This is the vmm main script.""" -import gettext -from time import strftime, strptime - -from VirtualMailManager import * -from VirtualMailManager.VirtualMailManager import VirtualMailManager -import VirtualMailManager.Exceptions as VMME -import VirtualMailManager.constants.EXIT as EXIT - - -def usage(excode=0, errMsg=None): - # TP: Please adjust translated words like the original text. - # (It's a table header.) Extract from usage text: - # Usage: vmm SUBCOMMAND OBJECT ARGS* - # short long - # subcommand object args (* = optional) - # - # da domainadd domain.tld transport* - # di domaininfo domain.tld details* - u_head = _(u"""\ -Usage: %s SUBCOMMAND OBJECT ARGS* - short long - subcommand object args (* = optional)\n""")\ - % __prog__ - - u_body = u"""\ - da domainadd domain.tld transport* - di domaininfo domain.tld details* - dt domaintransport domain.tld transport force* - dd domaindelete domain.tld delalias*|deluser*|delall* - ada aliasdomainadd aliasdomain.tld domain.tld - adi aliasdomaininfo aliasdomain.tld - ads aliasdomainswitch aliasdomain.tld domain.tld - add aliasdomaindelete aliasdomain.tld - ua useradd user@domain.tld password* - ui userinfo user@domain.tld details* - un username user@domain.tld "user’s name" - up userpassword user@domain.tld password* - ut usertransport user@domain.tld transport - u0 userdisable user@domain.tld service* - u1 userenable user@domain.tld service* - ud userdelete user@domain.tld delalias* - aa aliasadd alias@domain.tld user@domain.tld - ai aliasinfo alias@domain.tld - ad aliasdelete alias@domain.tld user@domain.tld* - ra relocatedadd exaddr@domain.tld user@domain.tld - ri relocatedinfo exaddr@domain.tld - rf relocateddelete exaddr@domain.tld - gu getuser userid - ld listdomains pattern* - cf configure section* - h help - v version -""" - if excode > 0: - if errMsg is None: - w_err(excode, u_head, u_body) - else: - w_err(excode, u_head, u_body, _(u'Error: %s\n') % errMsg) - else: - w_std(u_head, u_body) - os.sys.exit(excode) - -def get_vmm(): - try: - vmm = VirtualMailManager() - return vmm - except (VMME.VMMException, VMME.VMMNotRootException, VMME.VMMPermException, - VMME.VMMConfigException), e: - w_err(e.code(), _(u'Error: %s\n') % e.msg()) - -def _getOrder(): - order = () - if vmm.cfgGetInt('misc', 'dovecotvers') > 11: - sieve_name = u'sieve' - else: - sieve_name = u'managesieve' - if argv[1] in (u'di', u'domaininfo'): - order = ((u'domainname', 0), (u'gid', 1), (u'transport', 0), - (u'domaindir', 0), (u'aliasdomains', 0), (u'accounts', 0), - (u'aliases', 0), (u'relocated', 0)) - elif argv[1] in (u'ui', u'userinfo'): - if argc == 4 and argv[3] != u'aliases'\ - or vmm.cfgGetBoolean('maildir', 'diskusage'): - order = ((u'address', 0), (u'name', 0), (u'uid', 1), (u'gid', 1), - (u'transport', 0), (u'maildir', 0), (u'disk usage', 0), - (u'smtp', 1), (u'pop3', 1), (u'imap', 1), (sieve_name, 1)) - else: - order = ((u'address', 0), (u'name', 0), (u'uid', 1), (u'gid', 1), - (u'transport', 0), (u'maildir', 0), (u'smtp', 1), - (u'pop3', 1), (u'imap', 1), (sieve_name, 1)) - elif argv[1] in (u'gu', u'getuser'): - order = ((u'uid', 1), (u'gid', 1), (u'address', 0)) - return order - -def _printInfo(info, title): - # TP: e.g. 'Domain information' or 'Account information' - msg = u'%s %s' % (title, _(u'information')) - w_std (u'%s\n%s' % (msg, u'-'*len(msg))) - for k,u in _getOrder(): - if u: - w_std(u'\t%s: %s' % (k.upper().ljust(15, u'.'), info[k])) - else: - w_std(u'\t%s: %s' % (k.title().ljust(15, u'.'), info[k])) - print - -def _printList(alist, title): - # TP: e.g. 'Available alias addresses' or 'Available accounts' - msg = u'%s %s' % (_(u'Available'), title) - w_std(u'%s\n%s' % (msg, u'-'*len(msg))) - if len(alist) > 0: - if title != _(u'alias domains'): - for val in alist: - w_std(u'\t%s' % val) - else: - for dom in alist: - if not dom.startswith('xn--'): - w_std(u'\t%s' % dom) - else: - w_std(u'\t%s (%s)' % (dom, vmm.ace2idna(dom))) - else: - w_std(_(u'\tNone')) - print - -def _printAliases(alias, targets): - msg = _(u'Alias information') - w_std(u'%s\n%s' % (msg, u'-'*len(msg))) - w_std(_(u'\tMail for %s will be redirected to:') % alias) - if len(targets) > 0: - for target in targets: - w_std(u'\t * %s' % target) - else: - w_std(_(u'\tNone')) - print - -def _printRelocated(addr_dest): - msg = _(u'Relocated information') - w_std(u'%s\n%s' % (msg, u'-'*len(msg))) - w_std(_(u'\tUser “%(addr)s” has moved to “%(dest)s”') % addr_dest) - print - -def _formatDom(domain, main=True): - if domain.startswith('xn--'): - domain = u'%s (%s)' % (domain, vmm.ace2idna(domain)) - if main: - return u'\t[+] %s' % domain - else: - return u'\t[-] %s' % domain - -def _printDomList(dids, domains): - if argc < 3: - msg = _('Available domains') - else: - msg = _('Matching domains') - w_std('%s\n%s' % (msg, '-'*len(msg))) - if not len(domains): - w_std(_('\tNone')) - else: - for id in dids: - if domains[id][0] is not None: - w_std(_formatDom(domains[id][0])) - if len(domains[id]) > 1: - for alias in domains[id][1:]: - w_std(_formatDom(alias, main=False)) - print - -def _printAliasDomInfo(info): - msg = _('Alias domain information') - for k in ['alias', 'domain']: - if info[k].startswith('xn--'): - info[k] = "%s (%s)" % (info[k], vmm.ace2idna(info[k])) - w_std('%s\n%s' % (msg, '-'*len(msg))) - w_std( - _('\tThe alias domain %(alias)s belongs to:\n\t * %(domain)s')%info) - print - -def configure(): - if need_setup or len(argv) < 3: - vmm.configure() - else: - vmm.configure(argv[2]) - -def domain_add(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing domain name.')) - elif argc < 4: - vmm.domainAdd(argv[2].lower()) - else: - vmm.domainAdd(argv[2].lower(), argv[3]) - -def domain_delete(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing domain name.')) - elif argc < 4: - vmm.domainDelete(argv[2].lower()) - else: - vmm.domainDelete(argv[2].lower(), argv[3]) - -def domain_info(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing domain name.')) - try: - if argc < 4: - _printInfo(vmm.domainInfo(argv[2].lower()), _(u'Domain')) - else: - details = argv[3].lower() - infos = vmm.domainInfo(argv[2].lower(), details) - _printInfo(infos[0], _(u'Domain')) - if details == u'accounts': - _printList(infos[1], _(u'accounts')) - elif details == u'aliasdomains': - _printList(infos[1], _(u'alias domains')) - elif details == u'aliases': - _printList(infos[1], _(u'aliases')) - elif details == u'relocated': - _printList(infos[1], _(u'relocated users')) - else: - _printList(infos[1], _(u'alias domains')) - _printList(infos[2], _(u'accounts')) - _printList(infos[3], _(u'aliases')) - _printList(infos[4], _(u'relocated users')) - except VMME.VMMDomainException, e: - if e.code() is ERR.DOMAIN_ALIAS_EXISTS: - w_std(plan_a_b % {'subcommand': u'aliasdomaininfo', - 'object': argv[2].lower()}) - alias_domain_info() - else: - raise e - -def domain_transport(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing domain name and new transport.')) - if argc < 4: - usage(EXIT.MISSING_ARGS, _(u'Missing new transport.')) - elif argc < 5: - vmm.domainTransport(argv[2].lower(), argv[3]) - else: - vmm.domainTransport(argv[2].lower(), argv[3], argv[4]) - -def alias_domain_add(): - if argc < 3: - usage(EXIT.MISSING_ARGS, - _(u'Missing alias domain name and target domain name.')) - elif argc < 4: - usage(EXIT.MISSING_ARGS, _(u'Missing target domain name.')) - else: - vmm.aliasDomainAdd(argv[2].lower(), argv[3].lower()) - -def alias_domain_info(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing alias domain name.')) - try: - _printAliasDomInfo(vmm.aliasDomainInfo(argv[2].lower())) - except VMME.VMMAliasDomainException, e: - if e.code() is ERR.ALIASDOMAIN_ISDOMAIN: - w_std(plan_a_b % {'subcommand': u'domaininfo', - 'object': argv[2].lower()}) - argv[1] = u'di' # necessary manipulation to get the order - domain_info() - else: - raise e +import sys -def alias_domain_switch(): - if argc < 3: - usage(EXIT.MISSING_ARGS, - _(u'Missing alias domain name and target domain name.')) - elif argc < 4: - usage(EXIT.MISSING_ARGS, _(u'Missing target domain name.')) - else: - vmm.aliasDomainSwitch(argv[2].lower(), argv[3].lower()) - -def alias_domain_delete(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing alias domain name.')) - else: - vmm.aliasDomainDelete(argv[2].lower()) - -def user_add(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing e-mail address.')) - elif argc < 4: - password = None - else: - password = argv[3] - vmm.userAdd(argv[2].lower(), password) - -def user_delete(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing e-mail address.')) - elif argc < 4: - vmm.userDelete(argv[2].lower()) - else: - vmm.userDelete(argv[2].lower(), argv[3].lower()) - -def user_info(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing e-mail address.')) - try: - if argc < 4: - _printInfo(vmm.userInfo(argv[2].lower()), u'Account') - else: - arg3 = argv[3].lower() - info = vmm.userInfo(argv[2].lower(), arg3) - if arg3 in ['aliases', 'full']: - _printInfo(info[0], u'Account') - _printList(info[1], _(u'alias addresses')) - else: - _printInfo(info, u'Account') - except VMME.VMMAccountException, e: - if e.code() is ERR.ALIAS_EXISTS: - w_std(plan_a_b % {'subcommand': u'aliasinfo', - 'object': argv[2].lower()}) - alias_info() - elif e.code() is ERR.RELOCATED_EXISTS: - w_std(plan_a_b % {'subcommand': u'relocatedinfo', - 'object': argv[2].lower()}) - relocated_info() - else: - raise e - -def user_name(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing e-mail address and user’s name.')) - if argc < 4: - usage(EXIT.MISSING_ARGS, _(u'Missing user’s name.')) - else: - vmm.userName(argv[2].lower(), argv[3]) - -def user_transport(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing e-mail address and transport.')) - if argc <4: - usage(EXIT.MISSING_ARGS, _(u'Missing transport.')) - else: - vmm.userTransport(argv[2].lower(), argv[3]) - -def user_enable(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing e-mail address.')) - elif argc < 4: - vmm.userEnable(argv[2].lower()) - else: - vmm.userEnable(argv[2].lower(), argv[3].lower()) - -def user_disable(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing e-mail address.')) - elif argc < 4: - vmm.userDisable(argv[2].lower()) - else: - vmm.userDisable(argv[2].lower(), argv[3].lower()) - -def user_password(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing e-mail address.')) - elif argc < 4: - password = None - else: - password = argv[3] - vmm.userPassword(argv[2].lower(), password) - -def alias_add(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing alias address and destination.')) - elif argc < 4: - usage(EXIT.MISSING_ARGS, _(u'Missing destination address.')) - else: - vmm.aliasAdd(argv[2].lower(), argv[3]) - -def alias_info(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing alias address')) - try: - _printAliases(argv[2].lower(), vmm.aliasInfo(argv[2].lower())) - except VMME.VMMAliasException, e: - if e.code() is ERR.ACCOUNT_EXISTS: - w_std(plan_a_b % {'subcommand': u'userinfo', - 'object': argv[2].lower()}) - argv[1] = u'ui' # necessary manipulation to get the order - user_info() - elif e.code() is ERR.RELOCATED_EXISTS: - w_std(plan_a_b % {'subcommand': u'relocatedinfo', - 'object': argv[2].lower()}) - relocated_info() - else: - raise e - -def alias_delete(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing alias address')) - elif argc < 4: - vmm.aliasDelete(argv[2].lower()) - else: - vmm.aliasDelete(argv[2].lower(), argv[3].lower()) - -def relocated_add(): - if argc < 3: - usage(EXIT.MISSING_ARGS, - _(u'Missing relocated address and destination.')) - elif argc < 4: - usage(EXIT.MISSING_ARGS, _(u'Missing destination address.')) - else: - vmm.relocatedAdd(argv[2].lower(), argv[3]) - -def relocated_info(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing relocated address')) - relocated = argv[2].lower() - try: - _printRelocated({'addr': relocated, - 'dest': vmm.relocatedInfo(relocated)}) - except VMME.VMMRelocatedException, e: - if e.code() is ERR.ACCOUNT_EXISTS: - w_std(plan_a_b % {'subcommand': u'userinfo', 'object': relocated}) - argv[1] = u'ui' # necessary manipulation to get the order - user_info() - elif e.code() is ERR.ALIAS_EXISTS: - w_std(plan_a_b % {'subcommand': u'aliasinfo', 'object': relocated}) - alias_info() - else: - raise e - -def relocated_delete(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing relocated address')) - else: - vmm.relocatedDelete(argv[2].lower()) - -def user_byID(): - if argc < 3: - usage(EXIT.MISSING_ARGS, _(u'Missing userid')) - else: - _printInfo(vmm.userByID(argv[2]), u'Account') - -def domain_list(): - if argc < 3: - order, doms = vmm.domainList() - else: - order, doms = vmm.domainList(argv[2].lower()) - _printDomList(order, doms) - -def show_warnings(): - if vmm.hasWarnings(): - w_std(_(u'Warnings:')) - for warning in vmm.getWarnings(): - w_std( " * %s" % warning) - -def show_version(): - w_std('%s, %s %s (%s %s)\nPython %s %s %s\n\n%s %s' % (__prog__, - # TP: The words 'from', 'version' and 'on' are used in the version - # information: - # vmm, version 0.5.2 (from 09/09/09) - # Python 2.5.4 on FreeBSD - _(u'version'), __version__, _(u'from'), - strftime(locale.nl_langinfo(locale.D_FMT), - strptime(__date__, '%Y-%m-%d')).decode(ENCODING, 'replace'), - os.sys.version.split()[0], _(u'on'), os.uname()[0], __prog__, - _(u'is free software and comes with ABSOLUTELY NO WARRANTY.'))) - -#def main(): if __name__ == '__main__': - __prog__ = os.path.basename(os.sys.argv[0]) - gettext.install(__prog__, '/usr/local/share/locale', unicode=1) - argv = [unicode(arg, ENCODING) for arg in os.sys.argv] - argc = len(os.sys.argv) - plan_a_b =_(u'Plan A failed ... trying Plan B: %(subcommand)s %(object)s') - - if argc < 2: - usage(EXIT.MISSING_ARGS) - - vmm = get_vmm() - try: - need_setup = not vmm.setupIsDone() - if argv[1] in (u'cf', u'configure') or need_setup: - configure() - elif argv[1] in (u'da', u'domainadd'): - domain_add() - elif argv[1] in (u'di', u'domaininfo'): - domain_info() - elif argv[1] in (u'dt', u'domaintransport'): - domain_transport() - elif argv[1] in (u'dd', u'domaindelete'): - domain_delete() - elif argv[1] in (u'ada', u'aliasdomainadd'): - alias_domain_add() - elif argv[1] in (u'adi', u'aliasdomaininfo'): - alias_domain_info() - elif argv[1] in (u'ads', u'aliasdomainswitch'): - alias_domain_switch() - elif argv[1] in (u'add', u'aliasdomaindelete'): - alias_domain_delete() - elif argv[1] in (u'ua', u'useradd'): - user_add() - elif argv[1] in (u'ui', u'userinfo'): - user_info() - elif argv[1] in (u'un', u'username'): - user_name() - elif argv[1] in (u'up', u'userpassword'): - user_password() - elif argv[1] in (u'ut', u'usertransport'): - user_transport() - elif argv[1] in (u'u0', u'userdisable'): - user_disable() - elif argv[1] in (u'u1', u'userenable'): - user_enable() - elif argv[1] in (u'ud', u'userdelete'): - user_delete() - elif argv[1] in (u'aa', u'aliasadd'): - alias_add() - elif argv[1] in (u'ai', u'aliasinfo'): - alias_info() - elif argv[1] in (u'ad', u'aliasdelete'): - alias_delete() - elif argv[1] in (u'ra', u'relocatedadd'): - relocated_add() - elif argv[1] in (u'ri', u'relocatedinfo'): - relocated_info() - elif argv[1] in (u'rd', u'relocateddelete'): - relocated_delete() - elif argv[1] in (u'gu', u'getuser'): - user_byID() - elif argv[1] in (u'ld', u'listdomains'): - domain_list() - elif argv[1] in (u'h', u'help'): - usage() - elif argv[1] in (u'v', u'version'): - show_version() - else: - usage(EXIT.UNKNOWN_COMMAND, _(u'Unknown subcommand: “%s”')% argv[1]) - show_warnings() - except (EOFError, KeyboardInterrupt): - # TP: We have to cry, because root has killed/interrupted vmm - # with Ctrl+C or Ctrl+D. - w_err(EXIT.USER_INTERRUPT, _(u'\nOuch!\n')) - except (VMME.VMMConfigException, VMME.VMMException), e: - if e.code() != ERR.DATABASE_ERROR: - w_err(e.code(), _(u'Error: %s') % e.msg()) - else: - w_err(e.code(), unicode(e.msg(), ENCODING, 'replace')) + # replace the script's cwd (/usr/local/sbin) with our module dir + # (the location of the VirtualMailManager directory) - if it is + # not in sys.path + #sys.path[0] = '/usr/local/lib/vmm' + # Otherwise just remove /usr/local/sbin from sys.path + sys.path.remove(sys.path[0]) + from VirtualMailManager.cli.main import run + sys.exit(run(sys.argv)) diff -r c0e1fb1b0145 -r a4aead244f75 vmm.cfg --- a/vmm.cfg Mon Nov 07 03:22:15 2011 +0000 +++ b/vmm.cfg Thu Jun 28 19:26:50 2012 +0000 @@ -6,56 +6,123 @@ # Database settings # [database] +; The Python PostgreSQL database adapter module to be used (String) +; Supported modules are: +; * psycopg2 +; * pyPgSQL +module = psycopg2 ; Hostname or IP address of the database server (String) -host = 127.0.0.1 +host = localhost +; The TCP port, on which the database server is listening for connections (Int) +port = 5432 +; SSL mode for the database connection (String) +; Possible values are: +; * disabled +; * allow +; * prefer (default) +; * require +; * verify-ca (PostgreSQL >= 8.4) +; * verify-full (PostgreSQL >= 8.4) +sslmode = prefer ; Database user name (String) user = dbuser ; Database password (String) pass = dbpassword -; database name (String) +; Database name (String) name = mailsys # -# Mail directories +# mailbox settings # -[maildir] -; Default name of the Maildir folder (String) -name = Maildir -; A colon separated list of folder names, that should be created (String) -; e.g.: folders = Drafts:Sent:Templates:Trash +[mailbox] +; The mailbox format to be used for user's mailboxes. (String) +; Depending on the used Dovecot version there are up to 3 supported formats: +; * maildir - since Dovecot v1.0.0 +; * mdbox - since Dovecot v2.0.beta5 +; * sdbox - since Dovecot v2.0.rc3 +format = maildir +; A colon separated list of mailbox names, that should be created (String) +; e.g.: folders = Drafts:Sent:Templates:Trash:Lists.Dovecot:Lists.Postfix folders = Drafts:Sent:Templates:Trash -; 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 +; Name of the mailbox root directory in a user's home. (String) +; Usually used names (format: name): +; * maildir: Maildir +; * mdbox: mdbox +; * sdbox: sdbox +root = Maildir +; Set to true if the mailboxes from the folders option should be listed in +; the subscriptions file. (Boolean) +subscribe = true # -# Services per user +# Domain settings # -[services] -; allow smtp by default? (Boolean) +[domain] +; Should vmm create the postmaster account when a new domain is created? +; (Boolean) +auto_postmaster = true +; Delete domain directory recursive when deleting a domain? (Boolean) +delete_directory = false +; Permissions for domain directories (Int) +; octal 0770 -> decimal 504 +directory_mode = 504 +; Force deletion of accounts and aliases when deleting a domain (Boolean) +force_deletion = false +; +; The service settings will be evaluated and applied when a domain is +; created. The service settings of the domain will be applied when you +; create a new account. +; Use the subcommand domainservices to modify a domain's service settings. +; Or userservices in order to update the service setting of an account. +; Allow smtp by default? (Boolean) smtp = true -; allow pop3 by default? (Boolean) +; Allow pop3 by default? (Boolean) pop3 = true -; allow imap by default? (Boolean) +; Allow imap by default? (Boolean) imap = true -; allow managesieve by default? (Boolean) +; Allow managesieve by default? (Boolean) sieve = true +; +; The quota_* settings will be evaluated and applied when a domain is +; created. The domain's quota_* settings will be applied when an account +; is added to a domain. +; Use the subcommand domainquota to modify a domain's quota limits. +; Or userquota in order to update an account's quota limits. +; Quota limit in bytes. 0 means unlimited (String) +; The value can have one of the suffixes: +; * b: bytes +; * k: kilobytes +; * M: megabytes +; * G: gigabytes +; 1024 is the same as 1024b or 1k +quota_bytes = 0 +; Quota limit in number of messages. 0 means unlimited (Int) +quota_messages = 0 +; +; The transport setting will be evaluated and applied when a domain is +; created. The domain's transport setting will be applied when an account +; is added to a domain. +; Use the subcommand domaintransport to modify the transport of a domain. +; Or usertransport in order to update an account's transport setting. +; default transport for domains and accounts (String) +transport = dovecot: # -# domain directory settings +# Account settings # -[domdir] -; The base directory for all domains/accounts (String) -base = /srv/mail -; Permissions for domain directories (Int) -; octal 0770 -> decimal 504 -mode = 504 -; Delete domain directory recursive when deleting a domain? (Boolean) -delete = false +[account] +; Delete the user's home directory recursive when deleting an account? (Boolean) +delete_directory = false +; Permissions for the user's home directory and mail directories (Int) +; octal 0700 -> decimal 448 +directory_mode = 448 +; Display disk usage in account info by default? (Boolean) +disk_usage = false +; Should vmm generate a random password when no password was given for the +; useradd subcommand? (Boolean) +random_password = false +; How many characters to include in the generated passwords? (Int) +password_length = 8 # # external binaries @@ -72,21 +139,17 @@ # misc settings # [misc] +; The base directory for all domains/accounts (String) +base_directory = /srv/mail +; Number of encryption rounds for the password_scheme BLF-CRYPT (Int) +crypt_blowfish_rounds = 5 +; Number of encryption rounds for the password_scheme SHA256-CRYPT (Int) +crypt_sha256_rounds = 5000 +; Number of encryption rounds for the password_scheme SHA512-CRYPT (Int) +crypt_sha512_rounds = 5000 +; the version number from `dovecot --version` (String) +; e.g. 1.1.18; 1.2.11; 2.0.beta4 +dovecot_version = 1.2.11 ; Password scheme to use (see also: dovecotpw -l) (String) -passwdscheme = PLAIN -; numeric group ID of group mail (mail_privileged_group from dovecot.conf) (Int) -gid_mail = 8 -; force deletion of accounts and aliases (Boolean) -forcedel = false -; default transport for domains and accounts -transport = dovecot: -; the concatenated major and minor version number from `dovecot --version` (Int) -; e.g. 1.0.15 -> 10; 1.1.18 -> 11; 1.2.3 -> 12 -dovecotvers = 11 +password_scheme = CRAM-MD5 -# -# Configuration state -# -[config] -; finally set this to true (Boolean) -done = false