diff --git a/MANIFEST.in b/MANIFEST.in index 38f115085..02a707cf3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,5 @@ include README.rst COPYING requirements.txt recursive-include modoboa *.html *.js *.css *.png *.po *.mo *.json *.gif recursive-include modoboa/core/templates *.tpl -recursive-include doc * \ No newline at end of file +recursive-include modoboa/core/commands/templates *.tpl +recursive-include doc * diff --git a/README.rst b/README.rst index 7c17f005f..956639ad1 100644 --- a/README.rst +++ b/README.rst @@ -11,8 +11,8 @@ as an administration panel or a webmail. Modoboa integrates with well known software such as `Postfix `_ or `Dovecot `_. A SQL database (`MySQL `_, `PostgreSQL -`_) is used as a central point of -communication between all components. +`_ or `SQLite `_) +is used as a central point of communication between all components. Modoboa is developed with modularity in mind, expanding it is really easy. Actually, all current features are extensions. @@ -65,3 +65,9 @@ The following external libraries are provided with Modoboa: .. |downloads| image:: https://pypip.in/d/modoboa/badge.png :alt: Downloads from Pypi :target: https://crate.io/packages/modoboa/ + + +.. image:: https://d2weczhvl823v0.cloudfront.net/tonioo/modoboa/trend.png + :alt: Bitdeli badge + :target: https://bitdeli.com/free + diff --git a/modoboa/bin/modoboa-admin.py b/bin/modoboa-admin.py similarity index 62% rename from modoboa/bin/modoboa-admin.py rename to bin/modoboa-admin.py index 6da3810af..9309b6cc8 100644 --- a/modoboa/bin/modoboa-admin.py +++ b/bin/modoboa-admin.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # coding: utf-8 -from modoboa.core.management import handle_command_line +from modoboa.core.commands import handle_command_line if __name__ == "__main__": handle_command_line() diff --git a/doc/additional/migrating.rst b/doc/additional/migrating.rst index beaaa66c1..448e7fdd1 100644 --- a/doc/additional/migrating.rst +++ b/doc/additional/migrating.rst @@ -16,7 +16,7 @@ existing `PostfixAdmin (version 2.3.3+) First, you must follow the :ref:`installation` step to create a fresh Modoboa database. -Once done, edit the *setting.py* file. First, add a new database +Once done, edit the :file:`settings.py` file. First, add a new database connection named ``pfxadmin`` into the ``DATABASES`` variable corresponding to your PostfixAdmin setup:: @@ -56,7 +56,7 @@ used by PostfixAdmin, check its configuration file if you're not sure) Click on the *Save* button. -The procedure is over, edit the *settings.py* file and: +The procedure is over, edit the :file:`settings.py` file and: * remove the ``pfxadmin`` database connection from the ``DATABASES`` variable diff --git a/doc/additional/virtualmachine.rst b/doc/additional/virtualmachine.rst index 942da5a10..471c2c418 100644 --- a/doc/additional/virtualmachine.rst +++ b/doc/additional/virtualmachine.rst @@ -4,9 +4,9 @@ Using the virtual machine Introduction ============ -A virtual machine with a ready-to-use *Modoboa* setup is available -`here `_. It is composed -of the following components: +A virtual machine with a ready-to-use Modoboa setup is available `here +`_. It is composed of +the following components: * Debian 6.0 (squeeze) * Modoboa and its prerequisites @@ -32,7 +32,7 @@ If you can't use the vmdk format, you can use `qemu Then, just use your prefered virtualization software (qemu, kvm, virtualbox, etc.) to start the machine. You'll need to configure at least one bridged network interface if you want to be able to play -with *Modoboa*, ie. your machine must be visible from your network. +with Modoboa, ie. your machine must be visible from your network. The default network interface of the machine (``eth0``) is configured to use the DHCP protocol. @@ -49,7 +49,7 @@ root demo the root user demo demo an unpriviliged user ===== ======== ==================== -To connect to *Modoboa*, first connect to the system and retrieve its +To connect to Modoboa, first connect to the system and retrieve its current network address like this:: $ /sbin/ifconfig eth0 @@ -72,4 +72,3 @@ admin@demo.local admin Administrator of the domain *demo.local*. user@demo.local user Simple user. Can access to applications. ================ ======== ============================================ - diff --git a/doc/conf.py b/doc/conf.py index cb4be0a42..13b3fddff 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -50,7 +50,7 @@ copyright = u'2013, Antoine Nguyen' # The short X.Y version. version = '1.0' # The full version, including alpha/beta/rc tags. -release = '1.0.0' +release = '1.0.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/doc/extending/events.rst b/doc/extending/events.rst index 48b0570a7..76bcf0493 100644 --- a/doc/extending/events.rst +++ b/doc/extending/events.rst @@ -65,9 +65,10 @@ Raised when an existing account is deleted. *Callback prototype*:: - def callback(account): pass + def callback(account, byuser, **options): pass -* ``oldaccount`` is the account that is going to be deleted +* ``account`` is the account that is going to be deleted +* ``byuser`` is the adminstrator deleting ``account`` AccountModified =============== diff --git a/doc/extending/plugin_api.rst b/doc/extending/plugin_api.rst index b6d2e5f27..a96381405 100644 --- a/doc/extending/plugin_api.rst +++ b/doc/extending/plugin_api.rst @@ -33,12 +33,12 @@ The required glue ***************** To create a new plugin, just start a new django application like -this (into modoboa's directory):: +this (into Modoboa's directory):: $ python manage.py startapp Then, you need to register this application using the provided -API. Just copy/paste the following example into the *__init__.py* file +API. Just copy/paste the following example into the :file:`__init__.py` file of the future extension:: from modoboa.extensions import ModoExtension, exts_pool @@ -72,7 +72,7 @@ of the future extension:: exts_pool.register_extension(MyExtension) Once done, simply add your plugin's module name to the -``INSTALLED_APPS`` variable located inside *settings.py*. Optionaly, +``INSTALLED_APPS`` variable located inside :file:`settings.py`. Optionaly, run ``python manage.py syncdb`` if your plugin provides custom tables and ``python manage.py collectstatic`` to update static files. diff --git a/doc/getting_started/configuration.rst b/doc/getting_started/configuration.rst index cad72e254..efcf39658 100644 --- a/doc/getting_started/configuration.rst +++ b/doc/getting_started/configuration.rst @@ -86,7 +86,7 @@ Custom logo You have the possibility to use a custom logo instead of the default one on the login page. -To do so, open the *settings.py* file and add a +To do so, open the :file:`settings.py` file and add a ``MODOBOA_CUSTOM_LOGO`` variable. This variable must contain the relative URL of your logo under ``MEDIA_URL``. For example:: @@ -107,7 +107,7 @@ Host configuration To manipulate mailboxes on the filesystem, you must allow the user who runs Modoboa to execute commands as the user who owns mailboxes. -To do so, edit the */etc/sudoers* file and add the following inside:: +To do so, edit the :file:`/etc/sudoers` file and add the following inside:: ALL=() NOPASSWD: ALL @@ -121,7 +121,7 @@ Time zone and language Modoboa is available in many languages. -To specify the default language to use, edit the *settings.py* file +To specify the default language to use, edit the :file:`settings.py` file and modify the ``LANGUAGE_CODE`` variable:: LANGUAGE_CODE = 'fr' # or 'en' for english, etc. @@ -143,7 +143,7 @@ Modoboa uses `Django's session framework `_ to store per-user information. -Few parameters need to be set in the *settings.py* configuration +Few parameters need to be set in the :file:`settings.py` configuration file to make Modoboa behave as expected:: SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Default value @@ -151,7 +151,7 @@ file to make Modoboa behave as expected:: This parameter is optional but you must ensure it is set to ``False`` (the default value). -The default configuration file provided by the *modoboa-admin.py* +The default configuration file provided by the ``modoboa-admin.py`` command is properly configured. **** @@ -163,7 +163,7 @@ LDAP Authentication ============== -*Modoboa* supports external LDAP authentication using the following extra components: +Modoboa supports external LDAP authentication using the following extra components: * `Python LDAP client `_ * `Django LDAP authentication backend `_ @@ -172,7 +172,7 @@ If you want to use this feature, you must first install those components:: $ pip install python-ldap django-auth-ldap -Then, all you have to do is to modify the *settings.py* file. Add a +Then, all you have to do is to modify the :file:`settings.py` file. Add a new authentication backend to the `AUTHENTICATION_BACKENDS` variable, like this:: @@ -247,7 +247,7 @@ If you need additional parameters, you will find a detailled documentation `here `_. Once the authentication is properly configured, the users defined in -your LDAP directory will be able to connect to *Modoboa*, the associated +your LDAP directory will be able to connect to Modoboa, the associated domain and mailboxes will be automatically created if needed. .. note:: diff --git a/doc/getting_started/install.rst b/doc/getting_started/install.rst index 426f6b89d..8a5afff43 100644 --- a/doc/getting_started/install.rst +++ b/doc/getting_started/install.rst @@ -30,8 +30,8 @@ You can choose between two options: * Use the Python package available on the `PyPI `_ * Download the sources tarball -The easiest one is to install it from the *PyPI*. Just run the -following command and you're done:: +The easiest one is to install it from PyPI. Just run the following +command and you're done:: $ pip install modoboa @@ -56,18 +56,19 @@ package. On Debian/Ubuntu:: `virtualenv `_ users ========================================================== -When you deploy an application using *virtualenv*, you may have to -compile some dependencies. For example, *modoboa* relies on *lxml*, +When you deploy an application using virtualenv, you may have to +compile some dependencies. For example, modoboa relies on lxml, which is a C python module. In order to install it, you will need to install the following requirements: * python development files * libxslt development files * libxml2 development files +* libz development files -On a *Debian* like system, just run the following command:: +On a Debian like system, just run the following command:: - $ apt-get install python-dev libxml2-dev libxslt-dev + $ apt-get install python-dev libxml2-dev libxslt-dev zlib1g-dev .. _database: @@ -75,7 +76,7 @@ On a *Debian* like system, just run the following command:: Database ******** -Thanks to *django*, Modoboa supports several databases. Depending on +Thanks to Django, Modoboa supports several databases. Depending on the one you will use, you must install the appropriate python package: * `mysqldb `_ for `MySQL `_ @@ -93,7 +94,7 @@ Deployment `modoboa-admin.py`, a command line tool, let you deploy a *ready-to-use* Modoboa site using only one instruction:: - $ modoboa-admin.py deploy modoboa_example --syncdb --collectstatic [--with-amavis] + $ modoboa-admin.py deploy modoboa_example --syncdb --collectstatic [--with-amavis] [--dburl database-url] [--amavis_dburl database-url] Just answer the few questions and you're done. You can now go to the :ref:`first_use` section. @@ -103,6 +104,31 @@ Just answer the few questions and you're done. You can now go to the The `--with-amavis` option must be set only if you intend to use the :ref:`amavis_frontend`. + +In case you need a **silent installation**, e.g. if you're using Salt-Stack. +It's possible to supply the database credentials as commandline arguments. + +.. note:: + + `--dburl database-url` for the modoboa database credentials + `--amavis_dburl database-url` for the amavis database credentials + + Your database-url should meet the following syntax: + ``scheme://[user:pass@][host:port]/dbname`` + + **or** + + ``sqlite:////full/path/to/your/database/file.sqlite`` + + Available `schemes` are: + * postgres + * postgresql + * postgis + * mysql + * mysql2 + * sqlite + + .. note:: If you plan to serve Modoboa using a URL prefix, you must change the @@ -127,7 +153,10 @@ server:: $ python manage.py runserver -You should be able to access Modoboa at http://locahost:8000/. +You should be able to access Modoboa at http://localhost:8000/. -For a production environnement, we recommend using a stable webserver -like :ref:`apache2` or :ref:`nginx-label`. +For a fully working interface using the embedded HTTP server, you need to set the ``DEBUG`` +parameter in settings.py to ``True``. + +For a production environment, we recommend using a stable webserver +like :ref:`apache2` or :ref:`nginx-label`. Don't forget to set ``DEBUG`` back to ``False``. diff --git a/doc/getting_started/plugins.rst b/doc/getting_started/plugins.rst index 5fcfcffb6..cb47c879f 100644 --- a/doc/getting_started/plugins.rst +++ b/doc/getting_started/plugins.rst @@ -50,11 +50,11 @@ This plugin provides a simple management frontend for `amavisd-new The per-domain policies feature only works for new installations. Currently, you can't use modoboa with an existing - database (ie. with data in *users* and *policies* tables). + database (ie. with data in ``users`` and ``policies`` tables). .. note:: - This plugin requires *amavisd-new* version **2.7.0** or higher. If + This plugin requires amavisd-new version **2.7.0** or higher. If you're planning to use the :ref:`selfservice`, you'll need version **2.8.0**. @@ -62,7 +62,7 @@ Database ======== You must tell to Modoboa where it can find the amavis -database. Inside *settings.py*, add a new connection to the +database. Inside :file:`settings.py`, add a new connection to the ``DATABASES`` variable like this:: DATABASES = { @@ -81,7 +81,7 @@ Replace values between ``<>`` with yours. .. note:: - Modoboa doesn't create *amavis* tables. You need to install them + Modoboa doesn't create amavis tables. You need to install them following the `official documentation `_. @@ -106,7 +106,7 @@ Release messages To release messages, first take a look at `this page `_. It -explains how to configure *amavisd-new* to listen somewhere for the +explains how to configure amavisd-new to listen somewhere for the AM.PDP protocol. This protocol is used to send requests. Below is an example of a working configuration:: @@ -126,7 +126,7 @@ Below is an example of a working configuration:: Don't forget to update the ``inet_acl`` list if you plan to release from the network. -Once *amavisd-new* is configured, just tell Modoboa where it can find +Once amavisd-new is configured, just tell Modoboa where it can find the *release server* by modifying the following parameters in the online panel: @@ -212,7 +212,7 @@ the required identifiers. To activate this feature, go the administration panel and set the **Enable self-service mode** parameter to yes. -The last step is to customize the notification messages *amavis* +The last step is to customize the notification messages amavis sends. The most important is to embark a direct link. Take a look at the `README.customize `_ file to learn what you're allowed to do. @@ -229,7 +229,7 @@ Graphical statistics This plugin collects various statistics about emails traffic on your server. It parses a log file to collect information, store it into RRD -files (see `rrdtool `_)and then +files (see `rrdtool `_) and then generates graphics in PNG format. To use it, go to the online parameters panel and adapt the following @@ -273,15 +273,15 @@ Postifx auto-reply messages *************************** This plugin let users define an auto-reply message (*vacation*). It is -based on *postfix* capabilities. +based on Postfix capabilities. The user that executes the autoreply script needs to access -*settings.py*. You must apply proper permissions on this file. For -example, if *settings.py* belongs to *www-data:www-data*, you can add -the *vmail* user to the *www-data* group and set the read permission +:file:`settings.py`. You must apply proper permissions on this file. For +example, if :file:`settings.py` belongs to ``www-data:www-data``, you can add +the ``vmail`` user to the ``www-data`` group and set the read permission for the group. -To make *postfix* use this feature, you need to update your +To make Postfix use this feature, you need to update your configuration files as follows: ``/etc/postfix/main.cf``:: @@ -334,7 +334,7 @@ Two working modes are available: To use this plugin, your hosting setup must include a *ManageSieve* server and your local delivery agent must understand the *Sieve* -language. Don't panic, *Dovecot* supports both :-) (refer to +language. Don't panic, Dovecot supports both :-) (refer to :ref:`dovecot` to know how to enable those features). .. note:: diff --git a/doc/getting_started/upgrade.rst b/doc/getting_started/upgrade.rst index b9ac53826..dfde23e49 100644 --- a/doc/getting_started/upgrade.rst +++ b/doc/getting_started/upgrade.rst @@ -33,6 +33,11 @@ install it. $ pip install --upgrade modoboa +.. warning:: + + If you migrate to **1.1.0**, please follow the :ref:`dedicated migration + procedure <1.1.0>` and skip the usual one. + Then, refer to this page to check if the version you're installing requires specific operations. If the version you're looking for is not present, it means nothing special is required. @@ -43,13 +48,61 @@ Finally, follow the common procedure:: $ python manage.py syncdb --migrate $ python manage.py collectstatic +.. _1.1.0: + +1.1.0: FIXME +============ + +Due to code refactoring, some modifications need to be done into +:file:`settings.py`: + +#. ``MODOBOA_APPS`` must contain the following applications:: + + MODOBOA_APPS = ( + 'modoboa', + 'modoboa.core', + 'modoboa.lib', + + 'modoboa.extensions.admin', + 'modoboa.extensions.limits', + 'modoboa.extensions.postfix_autoreply', + 'modoboa.extensions.webmail', + 'modoboa.extensions.stats', + 'modoboa.extensions.amavis', + 'modoboa.extensions.sievefilters', + ) + +#. ``AUTH_USER_MODEL`` must be set to ``core.User`` + +#. Into ``LOGGING``, replace ``modoboa.lib.logutils.SQLHandler`` by + ``modoboa.core.loggers.SQLHandler`` + +Then, run the following commands to migrate your installation:: + + $ python manage.py migrate core 0001 --fake + $ python manage.py migrate + $ python manage.py collectstatic + + +1.0.1: operations on mailboxes +============================== + +The way Modoboa handles **rename** and **delete** operations on +mailboxes has been improved. Make sure to consult :ref:`fs_operations` +and :ref:`Postfix configuration `. Look at the +``smtpd_recipient_restrictions`` setting. + +Run ``modoboa-admin.py postfix_maps --dbtype +`` and compare the files with those that postfix currently +use. Make necessary updates in light of the differences + 1.0.0: production ready, at last ================================ Configuration file update ------------------------- -Several modifications need to be done into *settings.py*. +Several modifications need to be done into :file:`settings.py`. #. Add the following import statement:: @@ -183,7 +236,7 @@ It is necessary to update the queries used to retrieve users and mailboxes: #. Run ``modoboa-admin.py postfix_maps --dbtype `` and compare the files with those that postfix currently use. Make necessary updates in light of the differences -#. Into *dovecot-sql.conf*, update the ``user_query`` query, refer to +#. Into :file:`dovecot-sql.conf`, update the ``user_query`` query, refer to :ref:`dovecot_mysql_queries` or :ref:`dovecot_pg_queries` #. Update dovecot's configuration to activate the new :ref:`quota related features ` @@ -200,7 +253,7 @@ may encounter the following issues: #. South fails to migrate ``reversion`` - Due to the admin user model change, the script *0001_initial.py* + Due to the admin user model change, the script :file:`0001_initial.py` may fail. Just deactivate ``reversion`` from ``INSTALLED_APPS`` and run the command again. Once done, reactivate ``reversion`` and run the command one last time. @@ -209,7 +262,7 @@ may encounter the following issues: 0.9.4: administrative panel performance improved ================================================ -#. Edit the *settings.py* file and remove +#. Edit the :file:`settings.py` file and remove ``'django.contrib.auth.backends.ModelBackend'`` from the ``AUTHENTICATION_BACKENDS`` variable @@ -222,10 +275,10 @@ For this version, we recommend to install a new instance (see Then, copy the following content from the old installation to the new one: -* The ``media`` directory +* The :file:`media` directory * The directory containing RRD files if you use the :ref:`stats` plugin -Don't copy the old *settings.py* file, just keep the new one and +Don't copy the old :file:`settings.py` file, just keep the new one and modify it (see :ref:`database` and :ref:`timezone_lang`). Migrate your database (see :ref:`latestversion`). @@ -253,16 +306,16 @@ requires a migration. .. note:: Many files have been renamed/removed for this version. I recommend that you backup important files (*settings.py*, etc.) elsewhere - (ie. */tmp* for example). Then, remove the *modoboa* directory, + (ie. :file:`/tmp` for example). Then, remove the :file:`modoboa` directory, extract the new tarball at the same place, rename the new directory - to *modoboa* and copy the files you've just backup into it. + to :file:`modoboa` and copy the files you've just backup into it. .. note:: If the first super administrator you created is named ``admin``, its password will be changed to ``password`` at the end of this upgrade. Don't forget to modify it! -#. Edit the *settings.py* file and update the following variables +#. Edit the :file:`settings.py` file and update the following variables (just copy/paste their new content):: MIDDLEWARE_CLASSES = ( @@ -319,10 +372,10 @@ requires a migration. $ ./manage.py collectstatic #. If you are using the *stats* extension, please rename the - */static/stats* directory to */media/stats* + :file:`/static/stats` directory to :file:`/media/stats` and change the value of the ``IMG_ROOTDIR`` parameter (go to the adminstration panel) -#. Restart the python instance(s) that serve *Modoboa* +#. Restart the python instance(s) that serve Modoboa #. Log into Modoboa, go to *Modoboa > Extensions*, uncheck all extensions, save. Then, check the extensions you want to use and @@ -332,7 +385,7 @@ requires a migration. (see :ref:`webservers`) #. **For Dovecot users only**, you need to modify the - ``password_query`` (file */etc/dovecot/dovecot-sql.conf* by default + ``password_query`` (file :file:`/etc/dovecot/dovecot-sql.conf` by default on a Debian system) like this:: password_query = SELECT email AS user, password FROM auth_user WHERE email='%u' @@ -340,7 +393,7 @@ requires a migration. 0.8.8: CSV import feature and minor fixes ========================================= -#. Edit the *settings.py* file and add +#. Edit the :file:`settings.py` file and add ``'modoboa.lib.middleware.AjaxLoginRedirect'`` to the ``MIDDLEWARE_CLASSES`` variable like this:: @@ -355,7 +408,7 @@ requires a migration. 'modoboa.extensions.webmail.middleware.WebmailErrorMiddleware', ) -#. Still inside *settings.py*, modify the ``DATABASE_ROUTERS`` +#. Still inside :file:`settings.py`, modify the ``DATABASE_ROUTERS`` variable like this:: DATABASE_ROUTERS = ["modoboa.extensions.amavis_quarantine.dbrouter.AmavisRouter"] @@ -364,7 +417,7 @@ requires a migration. 0.8.7: per-user language selection ================================== -#. Edit the *settings.py* file and add the +#. Edit the :file:`settings.py` file and add the ``'django.middleware.locale.LocaleMiddleware'`` middleware to the ``MIDDLEWARE_CLASSES`` variable like this:: @@ -397,7 +450,7 @@ requires a migration. ========================================================= #. Just update your configuration if you are using the quarantine - plugin. Open *settings.py*, move the database configuration from + plugin. Open :file:`settings.py`, move the database configuration from the ``DB_CONNECTIONS`` variable to the ``DATABASES`` variable, like this:: @@ -419,7 +472,7 @@ requires a migration. DATABASE_ROUTERS = ["modoboa.extensions.amavis_quarantine.dbrouter.AmavisRouter"] -#. Remove the deprecated ``DB_CONNECTIONS`` variable from *settings.py*. +#. Remove the deprecated ``DB_CONNECTIONS`` variable from :file:`settings.py`. 0.8.5: new "Sieve filters" plugin, improved admin app ===================================================== @@ -430,7 +483,7 @@ requires a migration. $ python manage.py migrate admin #. Add ``modoboa.auth`` and ``modoboa.extensions.sievefilters`` to the - ``INSTALLED_APPS`` variable in *settings.py*. + ``INSTALLED_APPS`` variable in :file:`settings.py`. #. Go the *Settings/Extensions* panel, deactivate and activate your extensions, it will update all the symbolic links. @@ -438,7 +491,7 @@ requires a migration. 0.8.4: folders manipulation support (webmail) and bugfixes ========================================================== -#. Update the ``MIDDLEWARE_CLASSES`` variable in *settings.py*:: +#. Update the ``MIDDLEWARE_CLASSES`` variable in :file:`settings.py`:: MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', @@ -453,7 +506,7 @@ requires a migration. extensions, it will update all the symbolic links to the new format. #. Optional: update the ``DATABASES`` and ``TEMPLATE_LOADERS`` - variables in *settings.py* to remove warning messages (appearing with + variables in :file:`settings.py` to remove warning messages (appearing with Django 1.3):: DATABASES = { @@ -475,14 +528,14 @@ requires a migration. 0.8.3: admin application refactoring and more ============================================= -#. Migrate the *admin* application:: +#. Migrate the ``admin`` application:: $ python manage.py migrate admin #. Update SQL queries used in your environnement (see :ref:`postfix` or :ref:`dovecot`). -#. Update *postfix* configuration so that it can handle domain aliases +#. Update Postfix configuration so that it can handle domain aliases (see :ref:`postfix`). @@ -495,9 +548,9 @@ requires a migration. #. Update your config file and add all extensions to ``INSTALLED_APPS`` (even those you are not going to use). -#. Inside the */templates/* directory, remove all symbolic links. -#. Download the latest release of ckeditor and extract it into */static/js/*. It should create a new directory named *ckeditor*. -#. Update the following variables inside *settings.py*:: +#. Inside the :file:`/templates/` directory, remove all symbolic links. +#. Download the latest release of ckeditor and extract it into :file:`/static/js/`. It should create a new directory named ``ckeditor``. +#. Update the following variables inside :file:`settings.py`:: MEDIA_ROOT = os.path.join(MODOBOA_DIR, 'static') MEDIA_URL = '/static/' @@ -511,9 +564,9 @@ requires a migration. 0.8.1 : project renamed ======================= -#. First, rename the *mailng* directory to *modoboa* and copy all the - content from *modoboa-0.8.1* to *modoboa*. -#. Edit *settings.py* and replace all occurences of mailng by +#. First, rename the ``mailng`` directory to ``modoboa`` and copy all the + content from ``modoboa-0.8.1`` to ``modoboa``. +#. Edit :file:`settings.py` and replace all occurences of mailng by modoboa. Make sure you don't modify the ``DATABASE`` section as you're not going to rename your database. #. Rename the ``MAILNG_DIR`` variable to ``MODOBOA_DIR``. @@ -547,7 +600,7 @@ Before you start the migration, make sure you have updated your 'mailng.userprefs', ) -Starting with 0.8, mailng.main doesn't exist anymore. You must remove +Starting with 0.8, ``mailng.main`` doesn't exist anymore. You must remove it from your ``INSTALLED_APPS``. Finally, run the following commands:: diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index bbcab775e..521371b0c 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -12,12 +12,12 @@ Modoboa works better with Dovecot 2.0 so the following documentation is suitable for this combination. In this section, we assume dovecot's configuration resides in -*/etc/dovecot*, all pathes will be relative to this directory. +:file:`/etc/dovecot`, all pathes will be relative to this directory. Mailboxes ========= -First, edit the *conf.d/10-mail.conf* and set the ``mail_location`` +First, edit the :file:`conf.d/10-mail.conf` and set the ``mail_location`` variable:: # maildir @@ -25,6 +25,8 @@ variable:: Then, edit the ``inbox`` namespace and add the following lines:: + inbox = yes + mailbox Drafts { auto = create special_use = \Drafts @@ -45,14 +47,53 @@ Then, edit the ``inbox`` namespace and add the following lines:: It ensures all the special mailboxes will be automaticaly created for new accounts. -.. note:: +.. _fs_operations: + +Operations on the file system +----------------------------- + +Three operation types are considered: + +#. Mailbox creation +#. Mailbox renaming +#. Mailbox deletion + +The first one is managed by Dovecot. The last two ones may be managed +by Modoboa if it can access the file system where the mailboxes are +stored (see :ref:`admin-params` to activate this feature). + +Those operations are treated asynchronously by a cron script. For +example, when you rename an e-mail address through the web UI, the +associated mailbox on the file system is not modified +directly. Instead of that, a *rename* order is created for this +mailbox. The mailbox will be considered unavailable until the order is +not executed (see :ref:`Postfix configuration `). + +Edit the crontab of the user who owns the mailboxes on the file system:: + + $ crontab -u -e + +And add the following line inside:: + + * * * * * python /manage.py handle_mailbox_operations + +.. warning:: + + The cron script must be executed by the system user owning the mailboxes. + +.. warning:: + + The user running the cron script must have access to the + :file:`settings.py` file of the modoboa instance. + +The result of each order is recorded into Modoboa's log. Go to +*Modoboa > Logs* to consult them. - Modoboa is not responsible for mailboxes creation, Dovecot is. Authentication ============== -To make the authentication work, edit the *conf.d/10-auth.conf* and +To make the authentication work, edit the :file:`conf.d/10-auth.conf` and uncomment the following line at the end:: #!include auth-system.conf.ext @@ -64,7 +105,7 @@ uncomment the following line at the end:: #!include auth-static.conf.ext -Then, edit the *conf.d/auth-sql.conf.ext* file and add the following +Then, edit the :file:`conf.d/auth-sql.conf.ext` file and add the following content inside:: passdb sql { @@ -72,7 +113,7 @@ content inside:: # Path for SQL configuration file, see example-config/dovecot-sql.conf.ext args = /etc/dovecot/dovecot-sql.conf.ext } - + userdb sql { driver = sql args = /etc/dovecot/dovecot-sql.conf.ext @@ -81,13 +122,13 @@ content inside:: Make sure to activate only one backend (per type) inside your configuration (just comment the other ones). -Edit the *dovecot-sql.conf.ext* and modify the configuration according +Edit the :file:`dovecot-sql.conf.ext` and modify the configuration according to your database engine. .. _dovecot_mysql_queries: -*MySQL* users -------------- +MySQL users +----------- :: @@ -105,12 +146,12 @@ to your database engine. .. _dovecot_pg_queries: -*PostgreSQL* users ------------------- +PostgreSQL users +---------------- :: - driver = postgresql + driver = pgsql connect = host= dbname= user= password= @@ -122,6 +163,23 @@ to your database engine. iterate_query = SELECT email AS username FROM admin_user +SQLite users +------------ + +:: + + driver = sqlite + + connect = + + default_pass_scheme = CRYPT + + password_query = SELECT email AS user, password FROM admin_user WHERE email='%u' and is_active=1 + + user_query = SELECT '/%d/%n' AS home, as uid, as gid, ('*:bytes=' || mb.quota || 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%n' AND dom.name='%d' + + iterate_query = SELECT email AS username FROM admin_user + .. note:: Replace values between ``<>`` with yours. @@ -130,7 +188,7 @@ LDA === The LDA is activated by default but you must define a *postmaster* -address. Open the *conf.d/15-lda.conf* file modify the following line:: +address. Open the :file:`conf.d/15-lda.conf` file modify the following line:: postmaster_address = postmaster@ @@ -143,12 +201,12 @@ Modoboa lets adminstrators define per-domain and/or per-account limits (quota). It also lists the current quota usage of each account. Those features require Dovecot to be configured in a specific way. -Inside *conf.d/10-mail.conf*, add the ``quota`` plugin to the default +Inside :file:`conf.d/10-mail.conf`, add the ``quota`` plugin to the default activated ones:: mail_plugins = quota -Inside *conf.d/10-master.conf*, update the *dict* service to set +Inside :file:`conf.d/10-master.conf`, update the ``dict`` service to set proper permissions:: service dict { @@ -157,11 +215,11 @@ proper permissions:: unix_listener dict { mode = 0600 user = - #group = + #group = } } -Inside *conf.d/20-imap.conf*, activate the ``imap_quota`` plugin:: +Inside :file:`conf.d/20-imap.conf`, activate the ``imap_quota`` plugin:: protocol imap { # ... @@ -171,13 +229,13 @@ Inside *conf.d/20-imap.conf*, activate the ``imap_quota`` plugin:: # ... } -Inside *dovecot.conf*, activate the quota SQL dictionary backend:: +Inside :file:`dovecot.conf`, activate the quota SQL dictionary backend:: dict { quota = :/etc/dovecot/dovecot-dict-sql.conf.ext } -Inside *conf.d/90-quota.conf*, activate the *quota dictionary* backend:: +Inside :file:`conf.d/90-quota.conf`, activate the *quota dictionary* backend:: plugin { quota = dict:User quota::proxy::quota @@ -185,9 +243,12 @@ Inside *conf.d/90-quota.conf*, activate the *quota dictionary* backend:: It will tell Dovecot to keep quota usage in the SQL dictionary. -Finally, edit the *dovecot-dict-sql.conf.ext* file and put the following content inside:: +Finally, edit the :file:`dovecot-dict-sql.conf.ext` file and put the +following content inside:: connect = host= dbname= user= password= + # SQLite users + # connect = /path/to/the/database.db map { pattern = priv/quota/storage @@ -208,9 +269,9 @@ Finally, edit the *dovecot-dict-sql.conf.ext* file and put the following content Database schema update ^^^^^^^^^^^^^^^^^^^^^^ -The `admin_quota` table is created by *Django* but unfortunately it -doesn't support `DEFAULT` constraints (it only simulates them when the -ORM is used). As *PostgreSQL* is a bit strict about constraint +The ``admin_quota`` table is created by Django but unfortunately it +doesn't support ``DEFAULT`` constraints (it only simulates them when the +ORM is used). As PostgreSQL is a bit strict about constraint violations, you must execute the following query manually:: db=> ALTER TABLE admin_quota ALTER COLUMN bytes SET DEFAULT 0; @@ -279,8 +340,8 @@ work for Modoboa. You should use the following one instead: CREATE TRIGGER setmboxid AFTER INSERT ON admin_quota FOR EACH ROW EXECUTE PROCEDURE set_mboxid(); -Copy this example into a file (for example: *quota-trigger.sql*) on -server running *postgres* and execute the following commands:: +Copy this example into a file (for example: :file:`quota-trigger.sql`) on +server running postgres and execute the following commands:: $ su - postgres $ psql < /path/to/quota-trigger.sql @@ -293,7 +354,7 @@ For existing installations, *Dovecot* (> 2) offers a command to recalculate the current quota usages. For example, if you want to update all usages, run the following command:: - $ doveadmin quota recalc -A + $ doveadm quota recalc -A Be carefull, it can take a while to execute. @@ -303,7 +364,7 @@ ManageSieve/Sieve Modoboa lets users define filtering rules from the web interface. To do so, it requires *ManageSieve* to be activated on your server. -Inside *conf.d/20-managesieve.conf*, make sure the following lines are +Inside :file:`conf.d/20-managesieve.conf`, make sure the following lines are uncommented:: protocols = $protocols sieve @@ -322,7 +383,7 @@ uncommented:: Messages filtering using Sieve is done by the LDA. -Inside *conf.d/15-lda.conf*, activate the ``sieve`` plugin like this:: +Inside :file:`conf.d/15-lda.conf`, activate the ``sieve`` plugin like this:: protocol lda { # Space separated list of plugins to load (default is global mail_plugins). @@ -330,20 +391,20 @@ Inside *conf.d/15-lda.conf*, activate the ``sieve`` plugin like this:: } Finally, configure the ``sieve`` plugin by editing the -*conf.d/90-sieve.conf* file. Put the follwing content inside:: +:file:`conf.d/90-sieve.conf` file. Put the follwing caontent inside:: plugin { - # Location of the active script. When ManageSieve is used this is actually + # Location of the active script. When ManageSieve is used this is actually # a symlink pointing to the active script in the sieve storage directory. sieve = ~/.dovecot.sieve # - # The path to the directory where the personal Sieve scripts are stored. For + # The path to the directory where the personal Sieve scripts are stored. For # ManageSieve this is where the uploaded scripts are stored. sieve_dir = ~/sieve } -Restart *Dovecot*. +Restart Dovecot. .. _postfix: @@ -359,46 +420,55 @@ Map files ========= You first need to create configuration files (or map files) that will -be used by *postfix* to lookup into Modoboa tables. +be used by Postfix to lookup into Modoboa tables. To automaticaly generate the requested map files and store them in a directory, run the following command:: - $ modoboa-admin.py postfix_maps --dbtype mapfiles + $ modoboa-admin.py postfix_maps --dbtype mapfiles -``mapfiles`` is the directory where the files will be stored. Answer the -few questions and you're done. +:file:`mapfiles` is the directory where the files will be +stored. Answer the few questions and you're done. + +.. _postfix_config: Configuration ============= -Use the following configuration in the */etc/postfix/main.cf* file +Use the following configuration in the :file:`/etc/postfix/main.cf` file (this is just one possible configuration):: # Stuff before virtual_transport = dovecot dovecot_destination_recipient_limit = 1 - virtual_minimum_uid = + virtual_minimum_uid = virtual_gid_maps = static: virtual_uid_maps = static: virtual_mailbox_base = /var/vmail - relay_domains = - virtual_mailbox_domains = mysql:/etc/postfix/sql-domains.cf - virtual_alias_domains = mysql:/etc/postfix/sql-domain-aliases.cf - virtual_mailbox_maps = mysql:/etc/postfix/sql-mailboxes.cf - virtual_alias_maps = mysql:/etc/postfix/sql-aliases.cf, - mysql:/etc/postfix/sql-domain-aliases-mailboxes.cf, - mysql:/etc/postfix/sql-email2email.cf, - mysql:/etc/postfix/sql-catchall-aliases.cf + relay_domains = + virtual_mailbox_domains = :/etc/postfix/sql-domains.cf + virtual_alias_domains = :/etc/postfix/sql-domain-aliases.cf + virtual_mailbox_maps = :/etc/postfix/sql-mailboxes.cf + virtual_alias_maps = :/etc/postfix/sql-aliases.cf, + :/etc/postfix/sql-domain-aliases-mailboxes.cf, + :/etc/postfix/sql-catchall-aliases.cf + + smtpd_recipient_restrictions = + ... + check_recipient_access :/etc/postfix/sql-maintain.cf + permit_mynetworks + ... # Stuff after -Then, edit the */etc/postfix/master.cf* file and add the following +Replace ```` by the name of the database you use. + +Then, edit the :file:`/etc/postfix/master.cf` file and add the following definition at the end:: dovecot unix - n n - - pipe flags=DRhu user=vmail:vmail argv=/usr/lib/dovecot/deliver -f ${sender} -d ${recipient} -Restart *Postfix*. +Restart Postfix. diff --git a/doc/integration/web_servers.rst b/doc/integration/web_servers.rst index ff5ca5712..2c2d11185 100644 --- a/doc/integration/web_servers.rst +++ b/doc/integration/web_servers.rst @@ -10,10 +10,19 @@ Web servers Apache2 ******* +.. note:: + + The following instructions are meant to help you get your site up + and running quickly. However it is not possible for the people + contributing documentation to Modoboa to test every single + combination of web server, wsgi server, distribution, etc. So it is + possible that **your** installation of uwsgi or nginx or Apache or + what-have-you works differently. Keep this in mind. + mod_wgsi ======== -First, make sure that *mod_wsgi* is installed on your server. +First, make sure that mod_wsgi is installed on your server. Create a new virtualhost in your Apache configuration and put the following content inside:: @@ -37,20 +46,20 @@ following content inside:: WSGIScriptAlias / /wsgi.py -This is just one possible configuration. +This is just one possible configuration. .. note:: - *Django* 1.3 users, please consult this `page `_, - it contains an example *wsgi.py* file. + Django 1.3 users, please consult this `page `_, + it contains an example :file:`wsgi.py` file. -.. note:: +.. note:: You will certainly need more configuration in order to launch Apache. mod_python ========== -First, make sure that *mod_python* is installed on your server. +First, make sure that mod_python is installed on your server. Create a new virtualhost in your Apache configuration and put the following content inside:: @@ -77,9 +86,9 @@ following content inside:: -This is just one possible configuration. +This is just one possible configuration. -.. note:: +.. note:: You will certainly need more configuration in order to launch Apache. @@ -89,17 +98,28 @@ This is just one possible configuration. Nginx ***** +.. note:: + + The following instructions are meant to help you get your site up + and running quickly. However it is not possible for the people + contributing documentation to Modoboa to test every single + combination of web server, wsgi server, distribution, etc. So it is + possible that **your** installation of uwsgi or nginx or Apache or + what-have-you works differently. Keep this in mind. + +Green Unicorn +============= + `Nginx `_ is a really fast HTTP server. Associated with `Green Unicorn `_, it gives you one of the -best setup to serve python/Django applications. Modoboa's -performances are really good with this configuration. +best setups to serve python/Django applications. Modoboa's +performance is really good with this configuration. -To use this setup, first download and install those softwares -(`Install gunicorn `_ and `install -nginx `_). - -Then, use the following sample *gunicorn* configuration (create a new -file named *gunicorn.conf.py* inside Modoboa's root dir):: +To use this setup, first download and install `nginx +`__ and `gunicorn +`_. Then, use the following sample +gunicorn configuration (create a new file named :file:`gunicorn.conf.py` +inside Modoboa's root dir):: backlog = 2048 bind = "unix:/var/run/gunicorn/modoboa.sock" @@ -113,9 +133,9 @@ file named *gunicorn.conf.py* inside Modoboa's root dir):: To start gunicorn, execute the following commands:: $ cd - $ gunicorn -c gunicorn.conf.py + $ gunicorn -c gunicorn.conf.py .wsgi:application -Now the *nginx* part. Just create a new virtual host and use the +Now the nginx part. Just create a new virtual host and use the following configuration:: upstream modoboa { @@ -123,13 +143,19 @@ following configuration:: } server { - listen 443; + listen 443 ssl; + ssl on; + keepalive_timeout 70; + server_name ; root ; access_log /var/log/nginx/.access.log; error_log /var/log/nginx/.error.log; + ssl_certificate ; + ssl_certificate_key ; + location /sitestatic/ { autoindex on; } @@ -147,6 +173,161 @@ following configuration:: } } +If you do not plan to use SSL then change the listen directive to +``listen 80;`` and delete each of the following directives:: + + ssl on; + keepalive_timeout 70; + ssl_certificate ; + ssl_certificate_key ; + proxy_set_header X-Forwarded-Protocol ssl; + +If you do plan to use SSL, you'll have to generate a certificate and a +key. `This article +`__ +contains information about how to do it. + Paste this content to your configuration (replace values between -``<>`` with yours), restart *nginx* and enjoy a really fast +``<>`` with yours), restart nginx and enjoy a really fast application! + +uwsgi +===== + +The following setup is meant to get you started quickly. You should +read the documentation of both nginx and uwsgi to understand how to +optimize their configuration for your site. + +The Django documentation includes the following warning regarding +uwsgi: + +.. warning:: + + Some distributions, including Debian and Ubuntu, ship an outdated + version of uWSGI that does not conform to the WSGI + specification. Versions prior to 1.2.6 do not call close on the + response object after handling a request. In those cases the + request_finished signal isn’t sent. This can result in idle + connections to database and memcache servers. + +Use uwsgi 1.2.6 or newer. If you do not, you *will* run into +problems. Modoboa will fail in obscure ways. + +To use this setup, first download and install `nginx +`__ and `uwsgi +`_. + +Here is a sample nginx configuration:: + + server { + listen 443 ssl; + ssl on; + keepalive_timeout 70; + + server_name ; + root ; + + ssl_certificate ; + ssl_certificate_key ; + + access_log /var/log/nginx/.access.log; + error_log /var/log/nginx/.error.log; + + location /sitestatic/ { + autoindex on; + alias ; + } + + # Whether or not Modoboa uses a media directory depends on how + # you configured Modoboa. It does not hurt to have this. + location /media/ { + autoindex on; + alias ; + } + + # This denies access to any file that begins with + # ".ht". Apache's .htaccess and .htpasswd are such files. A + # Modoboa installed from scratch would not contain any such + # files, but you never know what the future holds. + location ~ /\.ht { + deny all; + } + + location / { + include uwsgi_params; + uwsgi_pass ; + uwsgi_param UWSGI_SCRIPT modoboa_server.wsgi:application + uwsgi_param UWSGI_SCHEME https; + } + } + +If you do not plan to use SSL then change the listen directive to +``listen 80;`` and delete each of the following directives:: + + ssl on; + keepalive_timeout 70; + ssl_certificate ; + ssl_certificate_key ; + uwsgi_param UWSGI_SCHEME https; + +If you do plan to use SSL, you'll have to generate a certificate and a +key. `This article +`__ +contains information about how to do it. + +Make sure to replace the ``<...>`` in the sample configuration with +appropriate values. Here are some explanations for the cases that may +not be completely self-explanatory: + +```` + Where Modoboa's :file:`settings.py` resides. This is also where the + :file:`sitestatic` and :file:`media` directories reside. + +```` + This is the URL which will be the root of your Modoboa site at your + domain. For instance, if your Modoboa installation is reachable at + at ``https://foo/modoboa`` then ```` is + ``/modoboa``. In this case you probably also have to set the + ``alias`` directives to point to where Modoboa's sitestatic and + media directories are because otherwise nginx won't be able to find + them. + + If Modoboa is at the root of your domain, then ```` is an empty string and can be deleted from the configuration + above. In this case, you probably do not need the ``alias`` + directives. + +```` + The location where uwsig is listening. It could be a unix domain + socket or an address:port combination. Ubuntu configures uwsgi so + that the port is:: + + unix:/run/uwsgi/app//socket + + where ```` is the name of the application. + +Your uwsgi configuration should be:: + + [uwsgi] + # Not needed when using uwsgi from pip + # plugins = python + chdir = + module = .wsgi:application + master = true + harakiri = 30 + sharedarea = 4 + processes = 4 + vhost = true + no-default-app = true + +The plugins directive should be turned on if you use a uwsgi +installation that requires it. If uwsgi was installed from pip, it +does not require it. In the configuration above: + +```` + The directory where :file:`manage.py` resides. This directory is the + parent of ```` + +```` + The name that you passed to ``modoboa-admin.py deploy`` when you + created your Modoboa instance. diff --git a/modoboa/admin/__init__.py b/modoboa/admin/__init__.py deleted file mode 100644 index 5ebe1de28..000000000 --- a/modoboa/admin/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# coding: utf-8 -from django.utils.translation import ugettext as _, ugettext_lazy -from modoboa.extensions import ModoExtension, exts_pool -from modoboa.lib import parameters, events - - -class AdminConsole(ModoExtension): - name = "admin" - label = ugettext_lazy("Administration console") - version = "1.0" - description = ugettext_lazy("Web based console to manage domains, accounts and aliases") - always_active = True - - def load(self): - from app_settings import GeneralParametersForm - parameters.register(GeneralParametersForm, ugettext_lazy("General")) - - def destroy(self): - parameters.unregister() - -exts_pool.register_extension(AdminConsole, show=False) - - -@events.observe("ExtDisabled") -def unset_default_topredirection(extension): - """ - Simple callback to change the default redirection if the - corresponding extension is being disabled. - """ - topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION") - if topredirection == extension.name: - parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs") diff --git a/modoboa/admin/fixtures/initial_data.json b/modoboa/admin/fixtures/initial_data.json deleted file mode 100644 index cb8d94e14..000000000 --- a/modoboa/admin/fixtures/initial_data.json +++ /dev/null @@ -1 +0,0 @@ -[{"pk": 1, "model": "auth.group", "fields": {"name": "DomainAdmins", "permissions": [["add_user", "admin", "user"], ["change_user", "admin", "user"], ["delete_user", "admin", "user"], ["view_domains", "admin", "domain"], ["add_mailbox", "admin", "mailbox"], ["change_mailbox", "admin", "mailbox"], ["delete_mailbox", "admin", "mailbox"], ["add_alias", "admin", "alias"], ["change_alias", "admin", "alias"], ["delete_alias", "admin", "alias"], ["view_mailboxes", "admin", "mailbox"], ["view_aliases", "admin", "alias"], ["view_domaliases", "admin", "domainalias"], ["add_domainalias", "admin", "domainalias"], ["change_domainalias", "admin", "domainalias"], ["delete_domainalias", "admin", "domainalias"]]}},{"pk": 2, "model": "auth.group", "fields": {"name": "SimpleUsers", "permissions": []}}] diff --git a/modoboa/admin/fixtures/initial_users.json b/modoboa/admin/fixtures/initial_users.json deleted file mode 100644 index 3c8921f65..000000000 --- a/modoboa/admin/fixtures/initial_users.json +++ /dev/null @@ -1 +0,0 @@ -[{"pk": 1, "model": "admin.user", "fields": {"username": "admin", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": false, "last_login": "2012-01-26 15:19:25", "groups": [], "user_permissions": [], "password": "{CRYPT}dTTsGDkA5ZHKg", "email": "", "date_joined": "2012-01-26 15:19:25"}},{"pk": 1, "model": "admin.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["admin", "user"], "object_id": 1}}] \ No newline at end of file diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py deleted file mode 100644 index 18e878c75..000000000 --- a/modoboa/admin/forms.py +++ /dev/null @@ -1,668 +0,0 @@ -# coding: utf-8 -from django import forms -from django.contrib.auth.models import Group -from django.utils.translation import ugettext as _, ugettext_lazy -from django.http import QueryDict -from modoboa.admin.templatetags.admin_extras import gender -from modoboa.admin.exceptions import AdminError -from modoboa.lib import events, parameters -from modoboa.lib.exceptions import PermDeniedException -from modoboa.lib.emailutils import split_mailbox -from modoboa.lib.permissions import get_account_roles -from modoboa.lib.formutils import * -from modoboa.admin.models import User, Domain, DomainAlias, Mailbox, Alias - - -class DomainFormGeneral(forms.ModelForm, DynamicForm): - aliases = DomainNameField( - label=ugettext_lazy("Alias(es)"), - required=False, - help_text=ugettext_lazy("Alias(es) of this domain. Indicate only one name per input, press ENTER to add a new input.") - ) - - class Meta: - model = Domain - fields = ("name", "quota", "aliases", "enabled") - widgets = dict( - quota=forms.widgets.TextInput(attrs={"class": "span1"}) - ) - - def __init__(self, *args, **kwargs): - self.oldname = None - if "instance" in kwargs: - self.oldname = kwargs["instance"].name - super(DomainFormGeneral, self).__init__(*args, **kwargs) - - if len(args) and isinstance(args[0], QueryDict): - self._load_from_qdict(args[0], "aliases", DomainNameField) - elif "instance" in kwargs: - d = kwargs["instance"] - for pos, dalias in enumerate(d.domainalias_set.all()): - name = "aliases_%d" % (pos + 1) - self._create_field(forms.CharField, name, dalias.name, 3) - - def clean(self): - super(DomainFormGeneral, self).clean() - if len(self._errors): - raise forms.ValidationError(self._errors) - - cleaned_data = self.cleaned_data - name = cleaned_data["name"] - - try: - DomainAlias.objects.get(name=name) - except DomainAlias.DoesNotExist: - pass - else: - self._errors["name"] = self.error_class([_("An alias with this name already exists")]) - del cleaned_data["name"] - - for k in cleaned_data.keys(): - if not k.startswith("aliases"): - continue - if cleaned_data[k] == "": - del cleaned_data[k] - continue - try: - Domain.objects.get(name=cleaned_data[k]) - except Domain.DoesNotExist: - pass - else: - self._errors[k] = self.error_class([_("A domain with this name already exists")]) - del cleaned_data[k] - - return cleaned_data - - def save(self, user, commit=True): - d = super(DomainFormGeneral, self).save(commit=False) - if commit: - old_mail_homes = None - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm == "yes": - if self.oldname is not None and d.name != self.oldname: - for q in Quota.objects.filter(username__contains="@%s" % self.oldname): - q.username = q.username.replace('@%s' % self.oldname, '@%s' % d.name) - q.save() - old_mail_homes = dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all()) - d.save() - Mailbox.objects.filter(domain=d, use_domain_quota=True).update(quota=d.quota) - if old_mail_homes is not None: - for mb in d.mailbox_set.all(): - mb.rename_dir(old_mail_homes[mb.id]) - for k, v in self.cleaned_data.iteritems(): - if not k.startswith("aliases"): - continue - if v in ["", None]: - continue - try: - d.domainalias_set.get(name=v) - except DomainAlias.DoesNotExist: - pass - else: - continue - events.raiseEvent("CanCreate", user, "domain_aliases") - al = DomainAlias(name=v, target=d, enabled=d.enabled) - al.save(creator=user) - - for dalias in d.domainalias_set.all(): - if not len(filter(lambda name: self.cleaned_data[name] == dalias.name, - self.cleaned_data.keys())): - dalias.delete() - return d - - -class DomainFormOptions(forms.Form): - create_dom_admin = YesNoField( - label=ugettext_lazy("Create a domain administrator"), - initial="no", - help_text=ugettext_lazy("Automatically create an administrator for this domain") - ) - - dom_admin_username = forms.CharField( - label=ugettext_lazy("Name"), - initial="admin", - help_text=ugettext_lazy("The administrator's name. Don't include the domain's name here, it will be automatically appended."), - widget=forms.widgets.TextInput(attrs={"class": "input-small"}), - required=False - ) - - create_aliases = YesNoField( - label=ugettext_lazy("Create aliases"), - initial="yes", - help_text=ugettext_lazy("Automatically create standard aliases for this domain"), - required=False - ) - - def __init__(self, *args, **kwargs): - super(DomainFormOptions, self).__init__(*args, **kwargs) - if args: - if args[0].get("create_dom_admin", "no") == "yes": - self.fields["dom_admin_username"].required = True - self.fields["create_aliases"].required = True - - def clean_dom_admin_username(self): - if '@' in self.cleaned_data["dom_admin_username"]: - raise forms.ValidationError(_("Invalid format")) - return self.cleaned_data["dom_admin_username"] - - def save(self, user, domain): - if self.cleaned_data["create_dom_admin"] == "no": - return - username = "%s@%s" % (self.cleaned_data["dom_admin_username"], domain.name) - try: - da = User.objects.get(username=username) - except User.DoesNotExist: - pass - else: - raise AdminError(_("User '%s' already exists" % username)) - da = User(username=username, email=username, is_active=True) - da.set_password("password") - da.save() - da.set_role("DomainAdmins") - da.post_create(user) - mb = Mailbox(address=self.cleaned_data["dom_admin_username"], domain=domain, - user=da, use_domain_quota=True) - mb.set_quota(override_rules=user.has_perm("admin.change_domain")) - mb.save(creator=user) - - if self.cleaned_data["create_aliases"] == "yes": - al = Alias(address="postmaster", domain=domain, enabled=True) - al.save([mb], [], creator=user) - - domain.add_admin(da) - - -class DomainForm(TabForms): - def __init__(self, user, *args, **kwargs): - self.user = user - self.forms = [] - if user.has_perm("admin.change_domain"): - self.forms.append(dict( - id="general", title=_("General"), formtpl="admin/domain_general_form.html", - cls=DomainFormGeneral, mandatory=True - )) - - cbargs = [user] - if "instances" in kwargs: - cbargs += [kwargs["instances"]["general"]] - self.forms += events.raiseQueryEvent("ExtraDomainForm", *cbargs) - if not self.forms: - self.active_id = "admins" - super(DomainForm, self).__init__(*args, **kwargs) - - def save(self, user): - """Custom save method - - As forms interact with each other, it is easier to make custom - code to save them. - """ - for f in self.forms: - f["instance"].save(user) - - -class AliasForm(forms.ModelForm, DynamicForm): - email = forms.EmailField( - label=ugettext_lazy("Email address"), - help_text=ugettext_lazy("The distribution list address. Use the '*' character to create a 'catchall' address (ex: *@domain.tld).") - ) - recipients = forms.EmailField( - label=ugettext_lazy("Recipients"), required=False, - help_text=ugettext_lazy("Mailbox(es) this alias will point to. Indicate only one address per input, press ENTER to add a new input.") - ) - - class Meta: - model = Alias - fields = ("enabled",) - - def __init__(self, user, *args, **kwargs): - self.user = user - super(AliasForm, self).__init__(*args, **kwargs) - self.fields.keyOrder = ['email', 'recipients', 'enabled'] - - if len(args) and isinstance(args[0], QueryDict): - self._load_from_qdict(args[0], "recipients", forms.EmailField) - elif "instance" in kwargs: - dlist = kwargs["instance"] - self.fields["email"].initial = dlist.full_address - cpt = 1 - for al in dlist.aliases.all(): - name = "recipients_%d" % cpt - self._create_field(forms.EmailField, name, al.full_address, 2) - cpt += 1 - for mb in dlist.mboxes.all(): - name = "recipients_%d" % (cpt) - self._create_field(forms.EmailField, name, mb.full_address, 2) - cpt += 1 - for addr in dlist.extmboxes.split(','): - if addr == "": - continue - name = "recipients_%d" % (cpt) - self._create_field(forms.EmailField, name, addr, 2) - cpt += 1 - - def clean_email(self): - localpart, domname = split_mailbox(self.cleaned_data["email"]) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise forms.ValidationError(_("Domain does not exist")) - if not self.user.can_access(domain): - raise forms.ValidationError(_("You don't have access to this domain")) - return self.cleaned_data["email"] - - def set_recipients(self): - """Recipients dispatching - - We make a difference between 'local' recipients (the ones hosted - by Modoboa) and 'external' recipients. - """ - self.ext_rcpts = [] - self.int_rcpts = [] - total = 0 - - for k, v in self.cleaned_data.items(): - if not k.startswith("recipients"): - continue - if v == "": - continue - local_part, domname = split_mailbox(v) - if domname is None: - raise AdminError("%s %s" % (_("Invalid mailbox"), v)) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - domain = None - if domain is not None: - try: - rcpt = Alias.objects.get(domain=domain, address=local_part) - if rcpt.full_address == self.cleaned_data["email"]: - rcpt = None - except Alias.DoesNotExist: - rcpt = None - if rcpt is None: - try: - rcpt = Mailbox.objects.get(domain=domain, address=local_part) - except Mailbox.DoesNotExist: - raise AdminError(_("Local recipient %s not found" % v)) - if rcpt in self.int_rcpts: - raise AdminError(_("Recipient %s already present" % v)) - self.int_rcpts += [rcpt] - total += 1 - continue - - if v in self.ext_rcpts: - raise AdminError(_("Recipient %s already present" % v)) - self.ext_rcpts += [v] - total += 1 - - if total == 0: - raise AdminError(_("No recipient defined")) - - def save(self, commit=True): - alias = super(AliasForm, self).save(commit=False) - localpart, domname = split_mailbox(self.cleaned_data["email"]) - alias.address = localpart - alias.domain = Domain.objects.get(name=domname) - if commit: - alias.save(self.int_rcpts, self.ext_rcpts) - self.save_m2m() - return alias - - -class ImportDataForm(forms.Form): - sourcefile = forms.FileField(label=ugettext_lazy("Select a file")) - sepchar = forms.CharField( - label=ugettext_lazy("Separator"), max_length=1, required=False - ) - continue_if_exists = forms.BooleanField( - label=ugettext_lazy("Continue on error"), required=False, - help_text=ugettext_lazy("Don't treat duplicated objects as error") - ) - - def __init__(self, *args, **kwargs): - super(ImportDataForm, self).__init__(*args, **kwargs) - self.fields["sepchar"].widget.attrs = {"class": "span1"} - - def clean_sepchar(self): - if self.cleaned_data["sepchar"] == "": - return ";" - return str(self.cleaned_data["sepchar"]) - - -class ImportIdentitiesForm(ImportDataForm): - crypt_password = forms.BooleanField( - label=ugettext_lazy("Crypt passwords"), required=False, - help_text=ugettext_lazy("Check this option if passwords contained in your file are not crypted") - ) - - -class ExportDataForm(forms.Form): - filename = forms.CharField( - label=ugettext_lazy("File name"), max_length=100, required=False - ) - sepchar = forms.CharField(label=ugettext_lazy("Separator"), max_length=1, required=False) - - def __init__(self, *args, **kwargs): - super(ExportDataForm, self).__init__(*args, **kwargs) - self.fields["sepchar"].widget.attrs = {"class": "span1"} - - def clean_sepchar(self): - if self.cleaned_data["sepchar"] == "": - return ";" - return str(self.cleaned_data["sepchar"]) - - def clean_filename(self): - if self.cleaned_data["filename"] == "": - return self.fields["filename"].initial - return str(self.cleaned_data["filename"]) - - -class ExportDomainsForm(ExportDataForm): - def __init__(self, *args, **kwargs): - super(ExportDomainsForm, self).__init__(*args, **kwargs) - self.fields["filename"].initial = "modoboa-domains.csv" - - -class ExportIdentitiesForm(ExportDataForm): - def __init__(self, *args, **kwargs): - super(ExportIdentitiesForm, self).__init__(*args, **kwargs) - self.fields["filename"].initial = "modoboa-identities.csv" - - -class AccountFormGeneral(forms.ModelForm): - username = forms.CharField(label=ugettext_lazy("Username")) - role = forms.ChoiceField( - label=ugettext_lazy("Role"), - choices=[('', ugettext_lazy("Choose"))], - help_text=ugettext_lazy("What level of permission this user will have") - ) - password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput) - password2 = forms.CharField( - label=ugettext_lazy("Confirmation"), - widget=forms.PasswordInput, - help_text=ugettext_lazy("Enter the same password as above, for verification.") - ) - - class Meta: - model = User - fields = ("username", "first_name", "last_name", "role", "is_active") - - def __init__(self, user, *args, **kwargs): - super(AccountFormGeneral, self).__init__(*args, **kwargs) - self.fields.keyOrder = ['role', 'username', 'first_name', 'last_name', - 'password1', 'password2', 'is_active'] - self.fields["is_active"].label = _("Enabled") - self.user = user - if user.group == "DomainAdmins": - self.fields["role"] = forms.CharField( - label="", - widget=forms.HiddenInput, required=False - ) - else: - self.fields["role"].choices = \ - [('', ugettext_lazy("Choose"))] + get_account_roles(user) - - if "instance" in kwargs: - if len(args) \ - and (args[0].get("password1", "") == "" - and args[0].get("password2", "") == ""): - self.fields["password1"].required = False - self.fields["password2"].required = False - - u = kwargs["instance"] - self.fields["role"].initial = u.group - - if not u.is_local \ - and parameters.get_admin("LDAP_AUTH_METHOD") == "directbind": - del self.fields["password1"] - del self.fields["password2"] - - def clean_role(self): - if self.user.group == "DomainAdmins": - if self.instance == self.user: - return "DomainAdmins" - return "SimpleUsers" - return self.cleaned_data["role"] - - def clean_username(self): - from django.core.validators import validate_email - if not "role" in self.cleaned_data: - return self.cleaned_data["username"] - if self.cleaned_data["role"] != "SimpleUsers": - return self.cleaned_data["username"] - uname = self.cleaned_data["username"].lower() - validate_email(uname) - return uname - - def clean_password2(self): - password1 = self.cleaned_data.get("password1", "") - password2 = self.cleaned_data["password2"] - if password1 != password2: - raise forms.ValidationError(_("The two password fields didn't match.")) - return password2 - - def save(self, commit=True): - account = super(AccountFormGeneral, self).save(commit=False) - if self.user == account and not self.cleaned_data["is_active"]: - raise AdminError(_("You can't disable your own account")) - if commit: - if "password1" in self.cleaned_data \ - and self.cleaned_data["password1"] != "": - account.set_password(self.cleaned_data["password1"]) - account.save() - account.set_role(self.cleaned_data["role"]) - return account - - -class AccountFormMail(forms.Form, DynamicForm): - email = forms.EmailField(label=ugettext_lazy("E-mail"), required=False) - quota = forms.IntegerField( - label=ugettext_lazy("Quota"), - required=False, - help_text=_("Quota in MB for this mailbox. Define a custom value or use domain's default one. Leave empty to define an unlimited value (not allowed for domain administrators)."), - widget=forms.widgets.TextInput(attrs={"class": "span1"}) - ) - quota_act = forms.BooleanField(required=False) - aliases = forms.EmailField( - label=ugettext_lazy("Alias(es)"), - required=False, - help_text=ugettext_lazy("Alias(es) of this mailbox. Indicate only one address per input, press ENTER to add a new input. Use the '*' character to create a 'catchall' alias (ex: *@domain.tld).") - ) - - def __init__(self, *args, **kwargs): - if "instance" in kwargs: - self.mb = kwargs["instance"] - del kwargs["instance"] - super(AccountFormMail, self).__init__(*args, **kwargs) - if hasattr(self, "mb") and self.mb is not None: - self.fields["email"].required = True - cpt = 1 - for alias in self.mb.alias_set.all(): - if len(alias.get_recipients()) >= 2: - continue - name = "aliases_%d" % cpt - self._create_field(forms.EmailField, name, alias.full_address) - cpt += 1 - self.fields["email"].initial = self.mb.full_address - self.fields["quota_act"].initial = self.mb.use_domain_quota - if not self.mb.use_domain_quota and self.mb.quota: - self.fields["quota"].initial = self.mb.quota - else: - self.fields["quota_act"].initial = True - - if len(args) and isinstance(args[0], QueryDict): - self._load_from_qdict(args[0], "aliases", forms.EmailField) - - def clean_email(self): - """Ensure lower case emails""" - return self.cleaned_data["email"].lower() - - def save(self, user, account): - if self.cleaned_data["email"] == "": - return None - - if self.cleaned_data["quota_act"]: - self.cleaned_data["quota"] = None - - if not hasattr(self, "mb") or self.mb is None: - locpart, domname = split_mailbox(self.cleaned_data["email"]) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise AdminError(_("Domain does not exist")) - if not user.can_access(domain): - raise PermDeniedException - try: - mb = Mailbox.objects.get(address=locpart, domain=domain) - except Mailbox.DoesNotExist: - pass - else: - raise AdminError(_("Mailbox %s already exists" % self.cleaned_data["email"])) - events.raiseEvent("CanCreate", user, "mailboxes") - self.mb = Mailbox(address=locpart, domain=domain, user=account, - use_domain_quota=self.cleaned_data["quota_act"]) - self.mb.set_quota(self.cleaned_data["quota"], - user.has_perm("admin.add_domain")) - self.mb.save(creator=user) - else: - newaddress = None - if self.cleaned_data["email"] != self.mb.full_address: - newaddress = self.cleaned_data["email"] - elif account.group == "SimpleUsers" and account.username != self.mb.full_address: - newaddress = account.username - if newaddress is not None: - local_part, domname = split_mailbox(newaddress) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise AdminError(_("Domain does not exist")) - if not user.can_access(domain): - raise PermDeniedException - self.mb.rename(local_part, domain) - - self.mb.use_domain_quota = self.cleaned_data["quota_act"] - override_rules = True if not self.mb.quota or user.has_perm("admin.add_domain") else False - self.mb.set_quota(self.cleaned_data["quota"], override_rules) - self.mb.save() - - account.email = self.cleaned_data["email"] - account.save() - - for name, value in self.cleaned_data.iteritems(): - if not name.startswith("aliases"): - continue - if value == "": - continue - local_part, domname = split_mailbox(value) - try: - self.mb.alias_set.get(address=local_part, domain__name=domname) - except Alias.DoesNotExist: - pass - else: - continue - events.raiseEvent("CanCreate", user, "mailbox_aliases") - al = Alias(address=local_part, enabled=account.is_active) - al.domain = Domain.objects.get(name=domname) - al.save([self.mb], [], creator=user) - - for alias in self.mb.alias_set.all(): - if len(alias.get_recipients()) >= 2: - continue - if not len(filter(lambda name: self.cleaned_data[name] == alias.full_address, - self.cleaned_data.keys())): - alias.delete() - - return self.mb - - -class AccountPermissionsForm(forms.Form, DynamicForm): - domains = DomainNameField( - label=ugettext_lazy("Domain(s)"), - required=False, - help_text=ugettext_lazy("Domain(s) that user administrates") - ) - - def __init__(self, *args, **kwargs): - if "instance" in kwargs: - self.account = kwargs["instance"] - del kwargs["instance"] - - super(AccountPermissionsForm, self).__init__(*args, **kwargs) - - if not hasattr(self, "account") or self.account is None: - return - for pos, domain in enumerate(self.account.get_domains()): - name = "domains_%d" % (pos + 1) - self._create_field(DomainNameField, name, domain.name) - if len(args) and isinstance(args[0], QueryDict): - self._load_from_qdict(args[0], "domains", DomainNameField) - - def save(self): - current_domains = [dom.name for dom in self.account.get_domains()] - for name, value in self.cleaned_data.items(): - if not name.startswith("domains"): - continue - if value in ["", None]: - continue - if not value in current_domains: - domain = Domain.objects.get(name=value) - domain.add_admin(self.account) - - for domain in self.account.get_domains(): - if not len(filter(lambda name: self.cleaned_data[name] == domain.name, - self.cleaned_data.keys())): - domain.remove_admin(self.account) - - -class AccountForm(TabForms): - - def __init__(self, user, *args, **kwargs): - self.user = user - self.forms = [ - dict(id="general", title=_("General"), cls=AccountFormGeneral, - new_args=[user], mandatory=True), - dict(id="mail", title=_("Mail"), formtpl="admin/mailform.html", - cls=AccountFormMail), - dict(id="perms", title=_("Permissions"), formtpl="admin/permsform.html", - cls=AccountPermissionsForm) - ] - cbargs = [user] - if "instances" in kwargs: - cbargs += [kwargs["instances"]["general"]] - self.forms += events.raiseQueryEvent("ExtraAccountForm", *cbargs) - - super(AccountForm, self).__init__(*args, **kwargs) - - def check_perms(self, account): - if account.is_superuser: - return False - return self.user.has_perm("admin.add_domain") \ - and account.has_perm("admin.add_user") - - def _before_is_valid(self, form): - if form["id"] == "general": - return True - - if hasattr(self, "check_%s" % form["id"]): - if not getattr(self, "check_%s" % form["id"])(self.account): - return False - return True - - if False in events.raiseQueryEvent("CheckExtraAccountForm", self.account, form): - return False - return True - - def save_general_form(self): - self.account = self.forms[0]["instance"].save() - - def save(self): - """Custom save method - - As forms interact with each other, it is simpler to make - custom code to save them. - """ - self.forms[1]["instance"].save(self.user, self.account) - if len(self.forms) <= 2: - return - for f in self.forms[2:]: - f["instance"].save() diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py deleted file mode 100644 index 8b395d800..000000000 --- a/modoboa/admin/models.py +++ /dev/null @@ -1,1322 +0,0 @@ -# coding: utf-8 -from django.db import models -from django.db.models import Q -from django.db.models.signals import pre_delete -from django.dispatch import receiver -from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes import generic -from django.contrib.auth.models import ( - UserManager, Group, AbstractBaseUser, PermissionsMixin -) -from django.utils.translation import ugettext as _, ugettext_lazy -from django.utils.crypto import constant_time_compare -from django.utils import timezone -from django.conf import settings -from modoboa.lib import md5crypt, parameters, events -from modoboa.lib.exceptions import PermDeniedException -from modoboa.lib.sysutils import exec_cmd -from modoboa.lib.emailutils import split_mailbox -from modoboa.extensions import exts_pool -from exceptions import AdminError -import reversion -import os -import sys -import pwd -import re -import crypt, hashlib, string, base64 -from random import Random - -try: - from modoboa.lib.ldaputils import * - ldap_available = True -except ImportError: - ldap_available = False - - -class User(AbstractBaseUser, PermissionsMixin): - """Custom User model. - - It overloads the way passwords are stored into the database. The - main reason to change this mechanism is to ensure the - compatibility with the way Dovecot stores passwords. - - It also adds new attributes and methods. - """ - username = models.CharField(max_length=254, unique=True) - first_name = models.CharField(max_length=30, blank=True) - last_name = models.CharField(max_length=30, blank=True) - email = models.EmailField(max_length=254, blank=True) - is_staff = models.BooleanField(default=False) - is_active = models.BooleanField(default=True) - date_joined = models.DateTimeField(default=timezone.now) - is_local = models.BooleanField(default=True) - - objects = UserManager() - - USERNAME_FIELD = 'username' - REQUIRED_FIELDS = ['email'] - - class Meta: - ordering = ["username"] - - password_expr = re.compile(r'(\{(\w+)\}|(\$1\$))(.+)') - - def delete(self, fromuser, keep_mb_dir, *args, **kwargs): - """Custom delete method - - To check permissions properly, we need to make a distinction - between 2 cases: - - * If the user owns a mailbox, the check is made on that object - (useful for domain admins) - - * Otherwise, the check is made on the user - """ - from modoboa.lib.permissions import \ - get_object_owner, grant_access_to_object, ungrant_access_to_object - - if fromuser == self: - raise AdminError(_("You can't delete your own account")) - - if not fromuser.can_access(self): - raise PermDeniedException - - if self.has_mailbox: - mb = self.mailbox_set.all()[0] - if not fromuser.can_access(mb): - raise PermDeniedException - mb.delete(keepdir=keep_mb_dir) - - owner = get_object_owner(self) - for ooentry in self.objectaccess_set.filter(is_owner=True): - if ooentry.content_object is not None: - grant_access_to_object(owner, ooentry.content_object, True) - - events.raiseEvent("AccountDeleted", self) - ungrant_access_to_object(self) - super(User, self).delete(*args, **kwargs) - - @staticmethod - def get_content_type(): - """An uggly hack to retrieve the appropriate content_type! - - The explanation is available here: - https://code.djangoproject.com/ticket/11154 - - Quickly, the content_types framework does not retrieve the - appropriate content type for proxy models, it retrieves the - one of the first parent that is not a proxy. - """ - if not hasattr(User, "ct"): - User.ct = ContentType.objects.get(app_label="admin", model="user") - return User.ct - - def _crypt_password(self, raw_value): - scheme = parameters.get_admin("PASSWORD_SCHEME") - if type(raw_value) is unicode: - raw_value = raw_value.encode("utf-8") - if scheme == "crypt": - salt = "".join(Random().sample(string.letters + string.digits, 2)) - result = crypt.crypt(raw_value, salt) - prefix = "{CRYPT}" - elif scheme == "md5": - obj = hashlib.md5(raw_value) - result = obj.hexdigest() - prefix = "{MD5}" - # The md5crypt scheme is the only supported method that has both: - # (a) a salt ("crypt" has this too), - # (b) supports passwords lengths of more than 8 characters (all except - # "crypt"). - elif scheme == "md5crypt": - # The salt may vary from 12 to 48 bits. (Using all six bytes here - # with a subset of characters means we get only 35 random bits.) - salt = "".join(Random().sample(string.letters + string.digits, 6)) - result = md5crypt(raw_value, salt) - prefix = "" # the result already has $1$ prepended to it to signify what this is - elif scheme == "sha256": - obj = hashlib.sha256(raw_value) - result = base64.b64encode(obj.digest()) - prefix = "{SHA256}" - else: - scheme = "plain" - result = raw_value - prefix = "{PLAIN}" - return "%s%s" % (prefix, result) - - def set_password(self, raw_value, curvalue=None): - """Password update - - Update the current mailbox's password with the given clear - value. This value is encrypted according to the defined method - before it is saved. - - :param raw_value: the new password's value - :param curvalue: the current password (for LDAP authentication) - """ - if self.is_local: - self.password = self._crypt_password(raw_value) - else: - if not ldap_available: - raise AdminError(_("Failed to update password: LDAP module not installed")) - - ab = LDAPAuthBackend() - try: - ab.update_user_password(self.username, curvalue, raw_value) - except LDAPException, e: - raise AdminError(_("Failed to update password: %s" % str(e))) - events.raiseEvent("PasswordUpdated", self, raw_value, self.pk is None) - - def check_password(self, raw_value): - m = self.password_expr.match(self.password) - if m is None: - return False - if type(raw_value) is unicode: - raw_value = raw_value.encode("utf-8") - scheme = (m.group(2) or m.group(3)).lower() - val2 = m.group(4) - if scheme == u"crypt": - val1 = crypt.crypt(raw_value, val2) - elif scheme == u"md5": - val1 = hashlib.md5(raw_value).hexdigest() - elif scheme == u"sha256": - val1 = base64.b64encode(hashlib.sha256(raw_value).digest()) - elif scheme == u"$1$": # md5crypt - salt, hashed = val2.split('$') - val1 = md5crypt(raw_value, str(salt)) - val2 = self.password # re-add scheme for comparison below - else: - val1 = raw_value - return constant_time_compare(val1, val2) - - @property - def tags(self): - return [{"name": "account", "label": _("account"), "type": "idt"}, - {"name": self.group, "label": self.group, "type": "grp", "color": "info"}] - - @property - def has_mailbox(self): - return self.mailbox_set.count() != 0 - - @property - def fullname(self): - if self.first_name != u"": - return u"%s %s" % (self.first_name, self.last_name) - return self.username - - @property - def identity(self): - return self.username - - @property - def name_or_rcpt(self): - if self.first_name != "": - return "%s %s" % (self.first_name, self.last_name) - return "----" - - @property - def group(self): - if self.is_superuser: - return "SuperAdmins" - try: - return self.groups.all()[0].name - except IndexError: - return "SimpleUsers" - - @property - def enabled(self): - return self.is_active - - @property - def encoded_address(self): - from email.header import Header - if self.first_name != "" or self.last_name != "": - return "%s <%s>" % \ - (Header(self.fullname, 'utf8').encode(), self.email) - return self.email - - def belongs_to_group(self, name): - """Simple shortcut to check if this user is a member of a - specific group. - - :param name: the group's name - :return: a boolean - """ - try: - self.groups.get(name=name) - except Group.DoesNotExist: - return False - return True - - def get_identities(self, querydict=None): - """Return all identities owned by this user - - :param querydict: a querydict object - :return: a queryset - """ - from modoboa.lib.permissions import get_content_type - from itertools import chain - - if querydict: - squery = querydict.get("searchquery", None) - idtfilter = querydict.getlist("idtfilter", None) - grpfilter = querydict.getlist("grpfilter", None) - else: - squery = None - idtfilter = None - grpfilter = None - - accounts = [] - if not idtfilter or "account" in idtfilter: - userct = get_content_type(self) - ids = self.objectaccess_set.filter(content_type=userct) \ - .values_list('object_id', flat=True) - q = Q(pk__in=ids) - if squery: - q &= Q(username__icontains=squery) | Q(email__icontains=squery) - if grpfilter and len(grpfilter): - if "SuperAdmins" in grpfilter: - q &= Q(is_superuser=True) - grpfilter.remove("SuperAdmins") - if len(grpfilter): - q |= Q(groups__name__in=grpfilter) - else: - q &= Q(groups__name__in=grpfilter) - accounts = User.objects.select_related().filter(q) - - aliases = [] - if not idtfilter or ("alias" in idtfilter - or "forward" in idtfilter - or "dlist" in idtfilter): - alct = get_content_type(Alias) - ids = self.objectaccess_set.filter(content_type=alct) \ - .values_list('object_id', flat=True) - q = Q(pk__in=ids) - if squery: - if '@' in squery: - local_part, domname = split_mailbox(squery) - if local_part: - q &= Q(address__icontains=local_part) - if domname: - q &= Q(domain__name__icontains=domname) - else: - q &= Q(address__icontains=squery) | Q(domain__name__icontains=squery) - aliases = Alias.objects.select_related().filter(q) - if idtfilter: - aliases = [al for al in aliases if al.type in idtfilter] - - return chain(accounts, aliases) - - def get_domains(self): - """Return the domains belonging to this user - - The result is a ``QuerySet`` object, so this function can be used - to fill ``ModelChoiceField`` objects. - """ - if self.is_superuser: - return Domain.objects.all() - return Domain.objects.filter(owners__user=self) - - def get_domaliases(self): - """Return the domain aliases that belong to this user - - The result will contain the domain aliases defined for each domain - that user can see. - - :return: a list of ``DomainAlias`` objects - """ - if self.is_superuser: - return DomainAlias.objects.all() - domains = self.get_domains() - domaliases = [] - for dom in domains: - domaliases += dom.domainalias_set.all() - return domaliases - - def get_mailboxes(self, squery=None): - """Return the mailboxes that belong to this user - - The result will contain the mailboxes defined for each domain that - user can see. - - :param string squery: a search query - :return: a list of ``Mailbox`` objects - """ - from modoboa.lib.permissions import get_content_type - - qf = None - if squery is not None: - if '@' in squery: - parts = squery.split('@') - addrfilter = '@'.join(parts[:-1]) - domfilter = parts[-1] - qf = Q(address__contains=addrfilter) & Q(domain__name__contains=domfilter) - else: - qf = Q(address__contains=squery) | Q(domain__name__contains=squery) - ids = self.objectaccess_set.filter(content_type=get_content_type(Mailbox)) \ - .values_list('object_id', flat=True) - if qf is not None: - qf = Q(pk__in=ids) & qf - else: - qf = Q(pk__in=ids) - return Mailbox.objects.filter(qf) - - def get_aliases(self): - from modoboa.lib.permissions import get_content_type - ids = self.objectaccess_set.filter(content_type=get_content_type(Alias)) \ - .values_list('object_id', flat=True) - return Alias.objects.filter(pk__in=ids) - - def get_mbaliases(self): - """Return the mailbox aliases that belong to this user - - The result will contain the mailbox aliases defined for each - domain that user can see. - - :return: a list of ``Alias`` objects - """ - if self.is_superuser: - return Alias.objects.all() - domains = self.get_domains() - mbaliases = [] - for dom in domains: - mbaliases += dom.alias_set.all() - return mbaliases - - def is_owner(self, obj): - """Tell is the user is the unique owner of this object - - :param obj: an object inheriting from ``models.Model`` - :return: a boolean - """ - from modoboa.lib.permissions import get_content_type - - ct = get_content_type(obj) - try: - ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) - except ObjectAccess.DoesNotExist: - return False - return ooentry.is_owner - - def can_access(self, obj): - """Check if the user can access a specific object - - This function is recursive : if the given user hasn't got direct - access to this object and if he has got access other ``User`` - objects, we check if one of those users owns the object. - - :param obj: a admin object - :return: a boolean - """ - from modoboa.lib.permissions import get_content_type - - if self.is_superuser: - return True - - ct = get_content_type(obj) - try: - ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) - except ObjectAccess.DoesNotExist: - pass - else: - return True - if ct.model == "user": - return False - - ct = self.get_content_type() - qs = self.objectaccess_set.filter(content_type=ct) - for ooentry in qs.all(): - if ooentry.content_object.is_owner(obj): - return True - return False - - def grant_access_to_all_objects(self): - """Give access to all objects defined in the database - - Must be used when an account is promoted as a super user. - """ - from modoboa.lib.permissions import grant_access_to_objects, get_content_type - grant_access_to_objects(self, User.objects.all(), get_content_type(User)) - grant_access_to_objects(self, Domain.objects.all(), get_content_type(Domain)) - grant_access_to_objects(self, DomainAlias.objects.all(), get_content_type(DomainAlias)) - grant_access_to_objects(self, Mailbox.objects.all(), get_content_type(Mailbox)) - grant_access_to_objects(self, Alias.objects.all(), get_content_type(Alias)) - - def set_role(self, role): - """Set administrative role for this account - - :param string role: the role to set - """ - if role is None or self.group == role: - return - self.groups.clear() - if role == "SuperAdmins": - self.is_superuser = True - self.grant_access_to_all_objects() - else: - if self.is_superuser: - ObjectAccess.objects.filter(user=self).delete() - self.is_superuser = False - try: - self.groups.add(Group.objects.get(name=role)) - except Group.DoesNotExist: - self.groups.add(Group.objects.get(name="SimpleUsers")) - if self.group != "SimpleUsers" and not self.can_access(self): - from modoboa.lib.permissions import grant_access_to_object - grant_access_to_object(self, self) - self.save() - - def post_create(self, creator): - from modoboa.lib.permissions import grant_access_to_object - grant_access_to_object(creator, self, is_owner=True) - events.raiseEvent("AccountCreated", self) - - def save(self, *args, **kwargs): - if "creator" in kwargs: - creator = kwargs["creator"] - del kwargs["creator"] - else: - creator = None - super(User, self).save(*args, **kwargs) - if creator is not None: - self.post_create(creator) - - def from_csv(self, user, row, crypt_password=True): - """Create a new account from a CSV file entry - - The expected order is the following:: - - "account", loginname, password, first name, last name, enabled, group, address[, domain, ...] - - :param user: the user importing a new account - :param row: a list containing the expected information - :param crypt_password: - """ - if len(row) < 7: - raise AdminError(_("Invalid line")) - role = row[6].strip() - if not user.is_superuser and not role in ["SimpleUsers", "DomainAdmins"]: - raise PermDeniedException( - _("You can't import an account with a role greater than yours") - ) - self.username = row[1].strip() - if role == "SimpleUsers": - if (len(row) < 8 or not row[7].strip()): - raise AdminError( - _("The simple user '%s' must have a valid email address" % self.username) - ) - if self.username != row[7].strip(): - raise AdminError( - _("username and email fields must not differ for '%s'" % self.username) - ) - - if crypt_password: - self.set_password(row[2].strip()) - else: - self.password = row[2].strip() - self.first_name = row[3].strip() - self.last_name = row[4].strip() - self.is_active = (row[5].strip() == 'True') - self.save(creator=user) - self.set_role(role) - - if len(row) < 8: - return - - self.email = row[7].strip() - if self.email != "": - self.save() - mailbox, domname = split_mailbox(self.email) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise AdminError( - _("Account import failed (%s): domain does not exist" % self.username) - ) - if not user.can_access(domain): - raise PermDeniedException - mb = Mailbox(address=mailbox, domain=domain, user=self, use_domain_quota=True) - mb.set_quota(override_rules=user.has_perm("admin.change_domain")) - mb.save(creator=user) - - if self.group == "DomainAdmins": - for domname in row[8:]: - try: - dom = Domain.objects.get(name=domname.strip()) - except Domain.DoesNotExist: - continue - dom.add_admin(self) - - def to_csv(self, csvwriter): - row = ["account", self.username.encode("utf-8"), self.password.encode("utf-8"), - self.first_name.encode("utf-8"), self.last_name.encode("utf-8"), - self.is_active, self.group, self.email.encode("utf-8")] - if self.group == "DomainAdmins": - row += [dom.name for dom in self.get_domains()] - csvwriter.writerow(row) - -reversion.register(User) - - -class ObjectAccess(models.Model): - user = models.ForeignKey(User) - content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey('content_type', 'object_id') - is_owner = models.BooleanField(default=False) - - class Meta: - unique_together = (("user", "content_type", "object_id"),) - - def __unicode__(self): - return "%s => %s (%s)" % (self.user, self.content_object, self.content_type) - - -class ObjectDates(models.Model): - """Dates recording for admin objects - - This table keeps creation and last modification dates for Domains, - domain aliases, mailboxes and aliases objects. - """ - creation = models.DateTimeField(auto_now_add=True) - last_modification = models.DateTimeField(auto_now=True) - - @staticmethod - def set_for_object(obj): - """Initialize or update dates for a given object. - - :param obj: an admin object (Domain, Mailbox, etc) - """ - try: - dates = getattr(obj, "dates") - except ObjectDates.DoesNotExist: - dates = ObjectDates() - dates.save() - obj.dates = dates - - -class DatesAware(models.Model): - """Abstract model to support dates - - Inherit from this model to automatically add the "dates" feature - to another model. It defines the appropriate field and handles - saves. - """ - dates = models.ForeignKey(ObjectDates) - - class Meta: - abstract = True - - def save(self, *args, **kwargs): - ObjectDates.set_for_object(self) - super(DatesAware, self).save(*args, **kwargs) - - @property - def creation(self): - return self.dates.creation - - @property - def last_modification(self): - return self.dates.last_modification - - -class Domain(DatesAware): - name = models.CharField(ugettext_lazy('name'), max_length=100, unique=True, - help_text=ugettext_lazy("The domain name")) - quota = models.IntegerField( - help_text=ugettext_lazy("Default quota in MB applied to mailboxes") - ) - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy("Check to activate this domain") - ) - owners = generic.GenericRelation(ObjectAccess) - - class Meta: - permissions = ( - ("view_domain", "View domain"), - ("view_domains", "View domains"), - ) - ordering = ["name"] - - @property - def domainalias_count(self): - return self.domainalias_set.count() - - @property - def mailbox_count(self): - return self.mailbox_set.count() - - @property - def mbalias_count(self): - return self.alias_set.count() - - @property - def admins(self): - """Return the domain administrators of this domain - - :return: a list of User objects - """ - return [oa.user for oa in self.owners.filter(user__is_superuser=False)] - - def add_admin(self, account): - """Add a new administrator for this domain - - :param User account: the administrotor to add - """ - from modoboa.lib.permissions import grant_access_to_object - grant_access_to_object(account, self) - for mb in self.mailbox_set.all(): - if mb.user.has_perm("admin.add_domain"): - continue - grant_access_to_object(account, mb) - grant_access_to_object(account, mb.user) - for al in self.alias_set.all(): - grant_access_to_object(account, al) - - def remove_admin(self, account): - """Remove an administrator for this domain - - :param User account: the administrotor to remove - """ - from modoboa.lib.permissions import ungrant_access_to_object - ungrant_access_to_object(self, account) - for mb in self.mailbox_set.all(): - if mb.user.has_perm("admin.add_domain"): - continue - ungrant_access_to_object(mb, account) - ungrant_access_to_object(mb.user, account) - for al in self.alias_set.all(): - ungrant_access_to_object(al, account) - - def delete(self, fromuser, keepdir=False): - from modoboa.lib.permissions import \ - ungrant_access_to_object, ungrant_access_to_objects - - if self.domainalias_set.count(): - events.raiseEvent("DomainAliasDeleted", self.domainalias_set.all()) - ungrant_access_to_objects(self.domainalias_set.all()) - if self.mailbox_set.count(): - Quota.objects.filter(username__contains='@%s' % self.name).delete() - events.raiseEvent("DeleteMailbox", self.mailbox_set.all()) - ungrant_access_to_objects(self.mailbox_set.all()) - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm == "yes" and not keepdir: - for mb in self.mailbox_set.all(): - mb.delete_dir() - if self.alias_set.count(): - events.raiseEvent("MailboxAliasDelete", self.alias_set.all()) - ungrant_access_to_objects(self.alias_set.all()) - if parameters.get_admin("AUTO_ACCOUNT_REMOVAL") == "yes": - for account in User.objects.filter(mailbox__domain__name=self.name): - account.delete(fromuser, keepdir) - events.raiseEvent("DeleteDomain", self) - ungrant_access_to_object(self) - super(Domain, self).delete() - - def __str__(self): - return self.name - - def post_create(self, creator): - from modoboa.lib.permissions import grant_access_to_object - grant_access_to_object(creator, self, is_owner=True) - events.raiseEvent("CreateDomain", creator, self) - - def save(self, *args, **kwargs): - if "creator" in kwargs: - creator = kwargs["creator"] - del kwargs["creator"] - else: - creator = None - super(Domain, self).save(*args, **kwargs) - if creator is not None: - self.post_create(creator) - - def from_csv(self, user, row): - if len(row) < 4: - raise AdminError(_("Invalid line")) - self.name = row[1].strip() - self.quota = int(row[2].strip()) - self.enabled = (row[3].strip() == 'True') - self.save(creator=user) - - def to_csv(self, csvwriter): - csvwriter.writerow(["domain", self.name, self.quota, self.enabled]) - -reversion.register(Domain) - - -class DomainAlias(DatesAware): - name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True, - help_text=ugettext_lazy("The alias name")) - target = models.ForeignKey( - Domain, verbose_name=ugettext_lazy('target'), - help_text=ugettext_lazy("The domain this alias points to") - ) - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy("Check to activate this alias") - ) - - class Meta: - permissions = ( - ("view_domaliases", "View domain aliases"), - ) - - def __unicode__(self): - return self.name - - def post_create(self, creator): - from modoboa.lib.permissions import grant_access_to_object - grant_access_to_object(creator, self, is_owner=True) - events.raiseEvent("DomainAliasCreated", creator, self) - - def save(self, *args, **kwargs): - if "creator" in kwargs: - creator = kwargs["creator"] - del kwargs["creator"] - else: - creator = None - super(DomainAlias, self).save(*args, **kwargs) - if creator is not None: - self.post_create(creator) - - def delete(self): - from modoboa.lib.permissions import ungrant_access_to_object - events.raiseEvent("DomainAliasDeleted", self) - ungrant_access_to_object(self) - super(DomainAlias, self).delete() - - def from_csv(self, user, row): - """Create a domain alias from a CSV row - - Expected format: ["domainalias", domain alias name, targeted domain, enabled] - - :param user: a ``User`` object - :param row: a list containing the alias definition - """ - if len(row) < 4: - raise AdminError(_("Invalid line")) - self.name = row[1].strip() - domname = row[2].strip() - try: - self.target = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise AdminError(_("Unknown domain %s" % domname)) - self.enabled = row[3].strip() == 'True' - self.save(creator=user) - - def to_csv(self, csvwriter): - """Export a domain alias using CSV format - - :param csvwriter: a ``csv.writer`` object - """ - csvwriter.writerow(["domainalias", self.name, - self.target.name, self.enabled]) - -reversion.register(DomainAlias) - - -class Mailbox(DatesAware): - address = models.CharField( - ugettext_lazy('address'), max_length=252, - help_text=ugettext_lazy("Mailbox address (without the @domain.tld part)") - ) - quota = models.PositiveIntegerField() - use_domain_quota = models.BooleanField(default=False) - domain = models.ForeignKey(Domain) - user = models.ForeignKey(User) - - class Meta: - permissions = ( - ("view_mailboxes", "View mailboxes"), - ) - - def __init__(self, *args, **kwargs): - super(Mailbox, self).__init__(*args, **kwargs) - self.__mail_home = None - - def __str__(self): - return self.full_address - - def __full_address(self, localpart): - return "%s@%s" % (localpart, self.domain.name) - - @property - def full_address(self): - return self.__full_address(self.address) - - @property - def enabled(self): - return self.user.is_active - - @property - def alias_count(self): - return self.alias_set.count() - - @property - def mail_home(self): - """ - - """ - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm is None or hm == "no": - return None - if self.__mail_home is None: - code, output = exec_cmd("doveadm user %s -f home" % self.full_address, - sudo_user=parameters.get_admin("MAILBOXES_OWNER")) - if code: - raise AdminError(_("Failed to retrieve mailbox location (%s)" % output)) - self.__mail_home = output.strip() - return self.__mail_home - - def rename_dir(self, old_mail_home): - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm is None or hm == "no": - return - self.__mail_home = None - if not os.path.exists(old_mail_home): - return - code, output = exec_cmd( - "mv %s %s" % (old_mail_home, self.mail_home), - sudo_user=parameters.get_admin("MAILBOXES_OWNER") - ) - if code: - raise AdminError(_("Failed to rename mailbox: %s" % output)) - - def rename(self, address, domain): - """Rename the mailbox - - :param string address: the new mailbox's address (local part) - :param Domain domain: the new mailbox's domain - """ - old_mail_home = self.mail_home - qs = Quota.objects.filter(username=self.full_address) - self.address = address - self.domain = domain - self.rename_dir(old_mail_home) - qs.update(username=self.full_address) - - def delete_dir(self): - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm is None or hm == "no": - return - if not os.path.exists(self.mail_home): - return - code, output = exec_cmd( - "rm -r %s" % self.mail_home, - sudo_user=parameters.get_admin("MAILBOXES_OWNER") - ) - if code: - raise AdminError(_("Failed to remove mailbox: %s" % output)) - - def set_quota(self, value=None, override_rules=False): - """Set or update quota's value for this mailbox. - - :param integer value: the quota's value - :param bool override_rules: allow to override defined quota rules - """ - if value is None: - if self.use_domain_quota: - self.quota = self.domain.quota - else: - self.quota = 0 - elif int(value) > self.domain.quota and not override_rules: - raise AdminError( - _("Quota is greater than the allowed domain's limit (%dM)" % self.domain.quota) - ) - else: - self.quota = value - if not self.quota and not override_rules: - raise AdminError(_("A quota is required")) - - def get_quota(self): - q = Quota.objects.get(username=self.full_address) - return int(q.bytes / 1048576) - - def get_quota_in_percent(self): - if not self.quota: - return 0 - q = Quota.objects.get(username=self.full_address) - return int(q.bytes / float(self.quota * 1048576) * 100) - - def post_create(self, creator): - from modoboa.lib.permissions import grant_access_to_object - grant_access_to_object(creator, self, True) - events.raiseEvent("CreateMailbox", creator, self) - if creator.is_superuser and not self.user.has_perm("admin.add_domain"): - # A super user is creating a new mailbox. Give - # access to that mailbox (and the associated - # account) to the appropriate domain admins, - # except if the new account has a more important - # role (SuperAdmin, Reseller) - for admin in self.domain.admins: - grant_access_to_object(admin, self) - grant_access_to_object(admin, self.user) - - def save(self, *args, **kwargs): - if "creator" in kwargs: - creator = kwargs["creator"] - del kwargs["creator"] - else: - creator = None - super(Mailbox, self).save(*args, **kwargs) - if creator is not None: - self.post_create(creator) - try: - q = self.quota_value - except Quota.DoesNotExist: - Quota.objects.create(mbox=self, username=self.full_address) - - def delete(self, keepdir=False): - """Custom delete method - - We try to delete the associated quota in the same time (it may - has already been removed if we're deleting a domain). - - :param bool keepdir: delete the mailbox home dir on the filesystem or not - """ - try: - q = Quota.objects.get(username=self.full_address) - except Quota.DoesNotExist: - pass - else: - q.delete() - if not keepdir: - self.delete_dir() - super(Mailbox, self).delete() - -reversion.register(Mailbox) - - -@receiver(pre_delete, sender=Mailbox) -def mailbox_deleted_handler(sender, **kwargs): - """``Mailbox`` pre_delete signal receiver - - In order to properly handle deletions (ie. we don't want to leave - orphan records into the db), we define this custom receiver. - - It manually removes the mailbox from the aliases it is linked to - and then remove all empty aliases. - """ - from modoboa.lib.permissions import ungrant_access_to_object - - mb = kwargs['instance'] - events.raiseEvent("DeleteMailbox", mb) - ungrant_access_to_object(mb) - for alias in mb.alias_set.all(): - alias.mboxes.remove(mb) - if alias.mboxes.count() == 0: - alias.delete() - - -class Quota(models.Model): - username = models.EmailField(primary_key=True, max_length=254) - bytes = models.BigIntegerField(default=0) - messages = models.IntegerField(default=0) - - mbox = models.OneToOneField(Mailbox, related_name="quota_value", null=True) - - -class Alias(DatesAware): - address = models.CharField( - ugettext_lazy('address'), max_length=254, - help_text=ugettext_lazy("The alias address (without the domain part). For a 'catch-all' address, just enter an * character.") - ) - domain = models.ForeignKey(Domain) - mboxes = models.ManyToManyField( - Mailbox, verbose_name=ugettext_lazy('mailboxes'), - help_text=ugettext_lazy("The mailboxes this alias points to") - ) - aliases = models.ManyToManyField( - 'Alias', - help_text=ugettext_lazy("The aliases this alias points to") - ) - extmboxes = models.TextField(blank=True) - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy("Check to activate this alias") - ) - - class Meta: - permissions = ( - ("view_aliases", "View aliases"), - ) - unique_together = (("address", "domain"),) - ordering = ["domain__name", "address"] - - def __unicode__(self): - return self.full_address - - @property - def full_address(self): - return "%s@%s" % (self.address, self.domain.name) - - @property - def identity(self): - return self.full_address - - @property - def name_or_rcpt(self): - rcpts_count = self.get_recipients_count() - if not rcpts_count: - return "---" - rcpts = self.get_recipients() - if rcpts_count > 1: - return "%s, ..." % rcpts[0] - return rcpts[0] - - @property - def type(self): - cpt = self.get_recipients_count() - if cpt > 1: - return "dlist" - if self.extmboxes != "": - return "forward" - return "alias" - - @property - def tags(self): - labels = { - "dlist": _("distribution list"), - "forward": _("forward"), - "alias": _("alias") - } - altype = self.type - return [{"name": altype, "label": labels[altype], "type": "idt"}] - - def post_create(self, creator): - from modoboa.lib.permissions import grant_access_to_object - grant_access_to_object(creator, self, is_owner=True) - events.raiseEvent("MailboxAliasCreated", creator, self) - if creator.is_superuser: - for admin in self.domain.admins: - grant_access_to_object(admin, self) - - def save(self, int_rcpts, ext_rcpts, *args, **kwargs): - if len(ext_rcpts): - self.extmboxes = ",".join(ext_rcpts) - else: - self.extmboxes = "" - if "creator" in kwargs: - creator = kwargs["creator"] - del kwargs["creator"] - else: - creator = None - super(Alias, self).save(*args, **kwargs) - if creator is not None: - self.post_create(creator) - curaliases = self.aliases.all() - curmboxes = self.mboxes.all() - for t in int_rcpts: - if isinstance(t, Alias): - if not t in curaliases: - self.aliases.add(t) - continue - if not t in curmboxes: - self.mboxes.add(t) - for t in curaliases: - if not t in int_rcpts: - self.aliases.remove(t) - for t in curmboxes: - if not t in int_rcpts: - self.mboxes.remove(t) - - def delete(self): - from modoboa.lib.permissions import ungrant_access_to_object - - events.raiseEvent("MailboxAliasDeleted", self) - ungrant_access_to_object(self) - super(Alias, self).delete() - - def get_recipients(self): - """Return the recipients list - - Internal and external addresses are mixed into a single list. - """ - result = [al.full_address for al in self.aliases.all()] - result += [mb.full_address for mb in self.mboxes.all()] - if self.extmboxes != "": - result += self.extmboxes.split(',') - return result - - def get_recipients_count(self): - total = 0 - if self.extmboxes != "": - total += len(self.extmboxes.split(',')) - return total + self.aliases.count() + self.mboxes.count() - - def ui_disabled(self, user): - if user.is_superuser: - return False - for mb in self.mboxes.all(): - if not user.is_owner(mb.domain): - return True - return False - - def from_csv(self, user, row, expected_elements=5): - """Create a new alias from a CSV file entry - - """ - if len(row) < expected_elements: - raise AdminError(_("Invalid line: %s" % row)) - localpart, domname = split_mailbox(row[1].strip()) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise AdminError(_("Domain '%s' does not exist" % domname)) - if not user.can_access(domain): - raise PermDeniedException - self.address = localpart - self.domain = domain - self.enabled = (row[2].strip() == 'True') - int_rcpts = [] - ext_rcpts = [] - for rcpt in row[3:]: - rcpt = rcpt.strip() - localpart, domname = split_mailbox(rcpt) - try: - Domain.objects.get(name=domname) - except Domain.DoesNotExist: - ext_rcpts += [rcpt] - continue - try: - target = Alias.objects.get(domain__name=domname, address=localpart) - if target.full_address == self.full_address: - target = None - except Alias.DoesNotExist: - target = None - if target is None: - try: - target = Mailbox.objects.get(address=localpart, - domain__name=domname) - except Mailbox.DoesNotExist: - raise AdminError(_("Local recipient %s not found" % rcpt)) - int_rcpts += [target] - self.save(int_rcpts, ext_rcpts, creator=user) - - def to_csv(self, csvwriter): - row = [self.type, self.full_address.encode("utf-8"), self.enabled] - row += self.get_recipients() - csvwriter.writerow(row) - -reversion.register(Alias) - - -class Extension(models.Model): - name = models.CharField(max_length=150) - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy("Check to enable this extension") - ) - - def __init__(self, *args, **kwargs): - super(Extension, self).__init__(*args, **kwargs) - - def __unicode__(self): - return self.name - - def __get_ext_instance(self): - if not self.name: - return None - if hasattr(self, "instance") and self.instance: - return - self.instance = exts_pool.get_extension(self.name) - if self.instance: - self.__get_ext_dir() - - def __get_ext_dir(self): - modname = self.instance.__module__ - path = os.path.realpath(sys.modules[modname].__file__) - self.extdir = os.path.dirname(path) - - def on(self): - self.enabled = True - self.save() - - self.__get_ext_instance() - self.instance.load() - self.instance.init() - - if self.instance.needs_media: - path = os.path.join(settings.MEDIA_ROOT, self.name) - exec_cmd("mkdir %s" % path) - - events.raiseEvent("ExtEnabled", self) - - def off(self): - self.__get_ext_instance() - if self.instance is None: - return - self.instance.destroy() - - self.enabled = False - self.save() - - if self.instance.needs_media: - path = os.path.join(settings.MEDIA_ROOT, self.name) - exec_cmd("rm -r %s" % path) - - events.raiseEvent("ExtDisabled", self) - -reversion.register(Extension) - - -def populate_callback(user): - """Populate callback - - If the LDAP authentication backend is in use, this callback will - be called each time a new user authenticates succesfuly to - Modoboa. This function is in charge of creating the mailbox - associated to the provided ``User`` object. - - :param user: a ``User`` instance - """ - from modoboa.lib.permissions import grant_access_to_object - - sadmins = User.objects.filter(is_superuser=True) - user.set_role("SimpleUsers") - user.post_create(sadmins[0]) - for su in sadmins[1:]: - grant_access_to_object(su, user) - - localpart, domname = split_mailbox(user.username) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - domain = Domain(name=domname, enabled=True, quota=0) - domain.save(creator=sadmins[0]) - for su in sadmins[1:]: - grant_access_to_object(su, domain) - try: - mb = Mailbox.objects.get(domain=domain, address=localpart) - except Mailbox.DoesNotExist: - mb = Mailbox(address=localpart, domain=domain, user=user, use_domain_quota=True) - mb.set_quota(override_rules=True) - mb.save(creator=sadmins[0]) - for su in sadmins[1:]: - grant_access_to_object(su, mb) - - -@receiver(reversion.post_revision_commit) -def post_revision_commit(sender, **kwargs): - import logging - - if kwargs["revision"].user is None: - return - logger = logging.getLogger("modoboa.admin") - for version in kwargs["versions"]: - if version.type == reversion.models.VERSION_ADD: - action = _("added") - level = "info" - elif version.type == reversion.models.VERSION_CHANGE: - action = _("modified") - level = "warning" - else: - action = _("deleted") - level = "critical" - message = _("%(object)s '%(name)s' %(action)s by user %(user)s") % { - "object": unicode(version.content_type).capitalize(), - "name": version.object_repr, "action": action, - "user": kwargs["revision"].user.username - } - getattr(logger, level)(message) diff --git a/modoboa/admin/templates/admin/parameters.html b/modoboa/admin/templates/admin/parameters.html deleted file mode 100644 index 0ae46dfc2..000000000 --- a/modoboa/admin/templates/admin/parameters.html +++ /dev/null @@ -1,8 +0,0 @@ -{% load i18n libextras %}{% load url from future %} -{% url "modoboa.admin.views.saveparameters" as action %} -

{% trans "Parameters" %} {% trans "Configure Modoboa" %}


-
{% csrf_token %} - {% include "common/parameters.html" %} -
-
-{% trans 'Save' %} diff --git a/modoboa/admin/templatetags/admin_extras.py b/modoboa/admin/templatetags/admin_extras.py deleted file mode 100644 index 5cdf8b245..000000000 --- a/modoboa/admin/templatetags/admin_extras.py +++ /dev/null @@ -1,344 +0,0 @@ -import re -from django import template -from django.template.loader import render_to_string -from django.utils.translation import ugettext as _, ugettext_lazy -from django.core.urlresolvers import reverse -from modoboa.lib import events -from modoboa.lib.webutils import static_url, render_actions -from modoboa.lib.templatetags.libextras import render_link - -register = template.Library() - -genders = { - "Enabled" : (ugettext_lazy("enabled_m"), ugettext_lazy("enabled_f")) -} - -@register.simple_tag -def admin_menu(selection, user): - entries = [] - if user.has_perm("admin.view_domains"): - entries += [ - {"name" : "domains", - "url" : reverse("modoboa.admin.views.domains"), - "label" : _("Domains")} - ] - entries += \ - events.raiseQueryEvent("AdminMenuDisplay", "top_menu", user) - if user.has_perm("admin.add_user") or user.has_perm("admin.add_alias"): - entries += [ - {"name" : "identities", - "url" : reverse("modoboa.admin.views.identities"), - "label" : _("Identities")}, - ] - if user.is_superuser: - entries += [ - {"name" : "settings", - "label" : _("Modoboa"), - "url" : reverse("modoboa.admin.views.viewsettings")} - ] - - if not len(entries): - return "" - return render_to_string("common/menulist.html", - {"entries" : entries, "selection" : selection, "user" : user}) - -@register.simple_tag -def settings_menu(selection, user): - entries = [ - {"name" : "extensions", - "class" : "ajaxlink", - "url" : "extensions/", - "label" : _("Extensions"), - "img" : ""}, - {"name" : "info", - "class" : "ajaxlink", - "url" : "info/", - "label" : _("Information")}, - {"name" : "logs", - "class" : "ajaxlink", - "url" : "logs/", - "label" : _("Logs")}, - {"name" : "parameters", - "class" : "ajaxlink", - "url" : "parameters/", - "img" : "", - "label" : _("Parameters")}, - ] - return render_to_string('common/menu.html', { - "entries" : entries, - "css" : "nav nav-list", - "selection" : selection, - "user" : user - }) - -@register.simple_tag -def domains_menu(selection, user): - if not user.has_perm("admin.add_domain"): - return "" - - entries = [ - {"name" : "newdomain", - "label" : _("Add domain"), - "img" : "icon-plus", - "modal" : True, - "modalcb" : "admin.newdomain_cb", - "url" : reverse("modoboa.admin.views.newdomain")}, - {"name" : "import", - "label" : _("Import"), - "img" : "icon-folder-open", - "url" : reverse("modoboa.admin.views.import_domains"), - "modal" : True, - "modalcb" : "admin.importform_cb"}, - {"name" : "export", - "label" : _("Export"), - "img" : "icon-share-alt", - "url" : reverse("modoboa.admin.views.export_domains"), - "modal" : True, - "modalcb" : "admin.exportform_cb"} - ] - - return render_to_string('common/menulist.html', { - "entries" : entries, - "selection" : selection, - "user" : user - }) - -@register.simple_tag -def identities_menu(user): - entries = [ - {"name": "identities", - "label": _("List identities"), - "img": "icon-user", - "class": "ajaxlink", - "url": "list/"}, - {"name": "quotas", - "label": _("List quotas"), - "img": "icon-hdd", - "class": "ajaxlink", - "url": "quotas/"}, - {"name" : "newaccount", - "label" : _("Add account"), - "img" : "icon-plus", - "modal" : True, - "modalcb" : "admin.newaccount_cb", - "url" : reverse("modoboa.admin.views.newaccount")}, - {"name" : "newalias", - "label" : _("Add alias"), - "img" : "icon-plus", - "modal" : True, - "modalcb" : "admin.aliasform_cb", - "url" : reverse("modoboa.admin.views.newalias")}, - {"name" : "newforward", - "label" : _("Add forward"), - "img" : "icon-plus", - "modal" : True, - "modalcb" : "admin.aliasform_cb", - "url" : reverse("modoboa.admin.views.newforward")}, - {"name" : "newdlist", - "label" : _("Add distribution list"), - "img" : "icon-plus", - "modal" : True, - "modalcb" : "admin.aliasform_cb", - "url" : reverse("modoboa.admin.views.newdlist")}, - {"name" : "import", - "label" : _("Import"), - "img" : "icon-folder-open", - "url" : reverse("modoboa.admin.views.import_identities"), - "modal" : True, - "modalcb" : "admin.importform_cb"}, - {"name" : "export", - "label" : _("Export"), - "img" : "icon-share-alt", - "url" : reverse("modoboa.admin.views.export_identities"), - "modal" : True, - "modalcb" : "admin.exportform_cb"} - ] - - return render_to_string('common/menulist.html', { - "entries" : entries, - "user" : user - }) - - -@register.simple_tag -def domain_actions(user, domid): - from modoboa.admin.models import Domain - - domain = Domain.objects.get(pk=domid) - actions = [ - {"name": "listidentities", - "url": reverse("modoboa.admin.views.identities") + "#list/?searchquery=@%s" % domain.name, - "title": _("View the domain's identities"), - "img": "icon-user"} - ] - if user.has_perm("admin.delete_domain"): - actions.append({ - "name": "deldomain", - "url": reverse("modoboa.admin.views.deldomain", args=[domid]), - "title": _("Delete the domain"), - "img": "icon-trash" - }) - return render_actions(actions) - - -@register.simple_tag -def identity_actions(user, ident): - name = ident.__class__.__name__ - objid = ident.id - if name == "User": - actions = events.raiseQueryEvent("ExtraAccountActions", ident) - actions += [ - {"name" : "delaccount", - "url" : reverse("modoboa.admin.views.delaccount", args=[objid]), - "img" : "icon-trash", - "title" : _("Delete this account")}, - ] - else: - if ident.get_recipients_count() >= 2: - actions = [ - {"name" : "deldlist", - "url" : reverse("modoboa.admin.views.deldlist") + "?selection=%s" % objid, - "img" : "icon-trash", - "title" : _("Delete this distribution list")}, - ] - elif ident.extmboxes != "": - actions = [ - {"name" : "delforward", - "url" : reverse("modoboa.admin.views.delforward") + "?selection=%s" % objid, - "img" : "icon-trash", - "title" : _("Delete this forward")}, - ] - else: - actions = [ - {"name" : "delalias", - "url" : reverse("modoboa.admin.views.delalias") + "?selection=%s" % objid, - "img" : "icon-trash", - "title" : _("Delete this alias")}, - ] - return render_actions(actions) - -@register.simple_tag -def identity_modify_link(identity, active_tab='default'): - linkdef = {"label" : identity.identity, "modal" : True} - if identity.__class__.__name__ == "User": - linkdef["url"] = reverse("modoboa.admin.views.editaccount", args=[identity.id]) - linkdef["url"] += "?active_tab=%s" % active_tab - linkdef["modalcb"] = "admin.editaccount_cb" - else: - linkdef["url"] = reverse("modoboa.admin.views.editalias", args=[identity.id]) - linkdef["modalcb"] = "admin.aliasform_cb" - return render_link(linkdef) - -@register.simple_tag -def domadmin_actions(daid, domid): - actions = [dict( - name="removeperm", - url=reverse("modoboa.admin.views.remove_permission") + "?domid=%s&daid=%s" % (domid, daid), - img="icon-trash", - title=_("Remove this permission") - )] - return render_actions(actions) - -@register.simple_tag -def loadadminextmenu(user): - menu = events.raiseQueryEvent("AdminMenuDisplay", "admin_menu_box", user) - return render_to_string('common/menulist.html', - {"entries" : menu, "user" : user}) - -@register.simple_tag -def param(app, definition): - name = "%s.%s" % (app, definition["name"]) - value = definition.has_key("value") \ - and definition["value"] or definition["deflt"] - - if definition["type"] == "separator": - return "
%s
" % (unicode(definition["label"])) - - extratags = "" - if "visible_if" in definition: - vfield, vvalue = definition["visible_if"].split("=") - extratags = "data-visibility-field='%s.%s' data-visibility-value='%s'" \ - % (app, vfield, vvalue) - - result = """
- -""" % (extratags, name, - definition.has_key("label") and _(definition["label"]) - or definition["name"]) - - result += "
" - if definition["type"] in ["string", "int"]: - result += """ - """ % (name, name, value) - - if definition["type"] == "text": - result += "" \ - % (name, name, value) - - if definition["type"] == "list_yesno": - for idx, v in enumerate([("yes", _("Yes")), ("no", _("No"))]): - checked = "checked" if value == v[0] else "" - result += """ -""" % dict(id="%s_%d" % (name, idx), name=name, value=v[0], label=v[1], checked=checked) - - if definition["type"] == "list": - result += """ - -""" - - if definition.has_key("help"): - result += """ - -""" % (unicode(definition["help"]), _("Help")) -# result += """ -# %s -# """ % _(definition["help"]) - - result += """ -
-
-""" - return result - -@register.filter -def gender(value, target): - if genders.has_key(value): - trans = target == "m" and genders[value][0] or genders[value][1] - if trans.find("_") == -1: - return trans - return value - -@register.simple_tag -def get_extra_admin_content(user, target, currentpage): - res = events.raiseQueryEvent("ExtraAdminContent", user, target, currentpage) - return "".join(res) - - -@register.filter -def colorize_level(level): - classes = { - "INFO": "text-info", - "WARNING": "text-warning", - "CRITICAL": "text-error" - } - if not level in classes: - return level - return "

%s

" % (classes[level], level) - - -@register.filter -def tohtml(message): - return re.sub("'(.*?)'", "\g<1>", message) diff --git a/modoboa/admin/tests.py b/modoboa/admin/tests.py deleted file mode 100644 index 60d380a20..000000000 --- a/modoboa/admin/tests.py +++ /dev/null @@ -1,443 +0,0 @@ -# coding: utf-8 -""" -Basic tests for the 'admin' application -======================================= - -TODO: - -* Add new tests to check domain/mailbox manipulations when - create_directories == yes - -""" -import os -from django.test import TestCase -from django.test.client import Client -from django.utils import simplejson -from django.core.urlresolvers import reverse -from django.core.files.base import ContentFile -from models import * -from modoboa.admin.lib import * -from modoboa.lib import parameters -from modoboa.lib.tests import ModoTestCase - - -class DomainTestCase(ModoTestCase): - fixtures = ["initial_users.json", "test_content.json"] - - def test_create(self): - """Test the creation of a domain - """ - values = { - "name": "pouet.com", "quota": 100, "create_dom_admin": "no", - "stepid": 2 - } - self.check_ajax_post(reverse("modoboa.admin.views.newdomain"), values) - dom = Domain.objects.get(name="pouet.com") - self.assertEqual(dom.name, "pouet.com") - self.assertEqual(dom.quota, 100) - self.assertEqual(dom.enabled, False) - self.assertFalse(dom.admins) - - def test_create_with_template(self): - """Test the creation of a domain with a template - - """ - values = { - "name": "pouet.com", "quota": 100, "create_dom_admin": "yes", - "dom_admin_username": "toto", "create_aliases": "yes", - "stepid": 2 - } - self.check_ajax_post(reverse("modoboa.admin.views.newdomain"), values) - dom = Domain.objects.get(name="pouet.com") - da = User.objects.get(username="toto@pouet.com") - self.assertIn(da, dom.admins) - al = Alias.objects.get(address="postmaster", domain__name="pouet.com") - self.assertIn(da.mailbox_set.all()[0], al.mboxes.all()) - self.assertTrue(da.can_access(al)) - - def test_create_with_template_and_empty_quota(self): - """Test the creation of a domain with a template and no quota""" - values = { - "name": "pouet.com", "quota": 0, "create_dom_admin": "yes", - "dom_admin_username": "toto", "create_aliases": "yes", - "stepid": 2 - } - self.check_ajax_post(reverse("modoboa.admin.views.newdomain"), values) - dom = Domain.objects.get(name="pouet.com") - da = User.objects.get(username="toto@pouet.com") - self.assertIn(da, dom.admins) - al = Alias.objects.get(address="postmaster", domain__name="pouet.com") - self.assertIn(da.mailbox_set.all()[0], al.mboxes.all()) - self.assertTrue(da.can_access(al)) - - def test_modify(self): - """Test the modification of a domain - - Rename 'test.com' domain to 'pouet.com' - """ - values = { - "name": "pouet.com", "quota": 100, "enabled": True - } - self.check_ajax_post(reverse("modoboa.admin.views.editdomain", args=[1]), values) - dom = Domain.objects.get(name="pouet.com") - self.assertEqual(dom.name, "pouet.com") - self.assertTrue(dom.enabled) - - def test_delete(self): - """Test the removal of a domain - """ - self.check_ajax_get(reverse("modoboa.admin.views.deldomain", args=[1]), {}) - with self.assertRaises(Domain.DoesNotExist): - Domain.objects.get(pk=1) - - -class DomainAliasTestCase(ModoTestCase): - fixtures = ["initial_users.json", "test_content.json"] - - def test_model(self): - dom = Domain.objects.get(name="test.com") - domal = DomainAlias() - domal.name = "domalias.net" - domal.target = dom - domal.save() - self.assertEqual(dom.domainalias_count, 1) - - domal.name = "domalias.org" - domal.save() - - domal.delete() - - def test_form(self): - dom = Domain.objects.get(name="test.com") - values = dict(name=dom.name, quota=dom.quota, enabled=dom.enabled, - aliases="domalias.net", aliases_1="domalias.com") - self.check_ajax_post(reverse("modoboa.admin.views.editdomain", args=[1]), values) - self.assertEqual(dom.domainalias_set.count(), 2) - - del values["aliases_1"] - self.check_ajax_post(reverse("modoboa.admin.views.editdomain", args=[1]), values) - self.assertEqual(dom.domainalias_set.count(), 1) - - -class AccountTestCase(ModoTestCase): - fixtures = ["initial_users.json", "test_content.json",] - - def test_crud(self): - values = dict(username="tester@test.com", first_name="Tester", last_name="Toto", - password1="toto", password2="toto", role="SimpleUsers", - quota_act=True, - is_active=True, email="tester@test.com", stepid=2) - self.check_ajax_post(reverse("modoboa.admin.views.newaccount"), values) - - account = User.objects.get(username="tester@test.com") - mb = account.mailbox_set.all()[0] - self.assertEqual(mb.full_address, "tester@test.com") - self.assertEqual(mb.quota, 100) - self.assertEqual(mb.enabled, True) - self.assertEqual(mb.quota_value.username, "tester@test.com") - self.assertEqual(account.username, mb.full_address) - self.assertTrue(account.check_password("toto")) - self.assertEqual(account.first_name, "Tester") - self.assertEqual(account.last_name, "Toto") - self.assertEqual(mb.domain.mailbox_count, 4) - - values["username"] = "pouet@test.com" - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[account.id]), values) - - mb = Mailbox.objects.get(pk=mb.id) - self.assertEqual(mb.full_address, "pouet@test.com") - - self.check_ajax_get(reverse("modoboa.admin.views.delaccount", args=[account.id]), {}) - - def test_update_password(self): - """Password update - - Two cases: - * The default admin changes his password (no associated Mailbox) - * A normal user changes his password - """ - self.check_ajax_post(reverse("modoboa.userprefs.views.profile"), - {"oldpassword" : "password", - "newpassword" : "titi", "confirmation" : "titi"}) - self.clt.logout() - - self.assertEqual(self.clt.login(username="admin", password="titi"), True) - self.assertEqual(self.clt.login(username="user@test.com", password="toto"), - True) - - self.check_ajax_post(reverse("modoboa.userprefs.views.profile"), - {"oldpassword" : "toto", - "newpassword" : "tutu", "confirmation" : "tutu"}) - self.clt.logout() - self.assertEqual(self.clt.login(username="user@test.com", password="tutu"), True) - - -class AliasTestCase(ModoTestCase): - fixtures = ["initial_users.json", "test_content.json"] - - def test_alias(self): - user = User.objects.get(username="user@test.com") - values = dict( - username="user@test.com", role=user.group, - is_active=user.is_active, email="user@test.com", - aliases="toto@test.com", aliases_1="titi@test.com" - ) - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[user.id]), values) - self.assertEqual(user.mailbox_set.all()[0].alias_set.count(), 2) - - del values["aliases_1"] - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[user.id]), values) - self.assertEqual(user.mailbox_set.all()[0].alias_set.count(), 1) - - def test_dlist(self): - values = dict(email="all@test.com", - recipients="user@test.com", - recipients_1="admin@test.com", - recipients_2="ext@titi.com", - enabled=True) - self.check_ajax_post(reverse("modoboa.admin.views.newdlist"), values) - user = User.objects.get(username="user@test.com") - self.assertEqual(len(user.mailbox_set.all()[0].alias_set.all()), 1) - admin = User.objects.get(username="admin@test.com") - self.assertEqual(len(admin.mailbox_set.all()[0].alias_set.all()), 1) - - dlist = Alias.objects.get(address="all", domain__name="test.com") - self.assertEqual(len(dlist.get_recipients()), 3) - del values["recipients_1"] - self.check_ajax_post(reverse("modoboa.admin.views.editalias", args=[dlist.id]), - values) - self.assertEqual(dlist.get_recipients_count(), 2) - - self.check_ajax_get(reverse("modoboa.admin.views.deldlist") + "?selection=%d" \ - % dlist.id, {}) - self.assertRaises(Alias.DoesNotExist, Alias.objects.get, - address="all", domain__name="test.com") - - def test_forward(self): - values = dict(email="forward@test.com", recipients="rcpt@dest.com") - self.check_ajax_post(reverse("modoboa.admin.views.newforward"), values) - fwd = Alias.objects.get(address="forward", domain__name="test.com") - self.assertEqual(fwd.get_recipients_count(), 1) - - values["recipients"] = "rcpt2@dest.com" - self.check_ajax_post(reverse("modoboa.admin.views.editalias", args=[fwd.id]), - values) - self.assertEqual(fwd.get_recipients_count(), 1) - - self.check_ajax_get(reverse("modoboa.admin.views.delforward") + "?selection=%d" \ - % fwd.id, {}) - self.assertRaises(Alias.DoesNotExist, Alias.objects.get, - address="forward", domain__name="test.com") - - def test_forward_and_local_copies(self): - values = dict(email="user@test.com", recipients="rcpt@dest.com") - self.check_ajax_post(reverse("modoboa.admin.views.newforward"), values) - fwd = Alias.objects.get(address="user", domain__name="test.com") - self.assertEqual(fwd.get_recipients_count(), 1) - - values["recipients"] = "rcpt@dest.com" - values["recipients_1"] = "user@test.com" - self.check_ajax_post(reverse("modoboa.admin.views.editalias", args=[fwd.id]), - values) - fwd = Alias.objects.get(pk=fwd.pk) - self.assertEqual(fwd.get_recipients_count(), 2) - self.assertEqual(fwd.aliases.count(), 0) - - -class PermissionsTestCase(ModoTestCase): - fixtures = ["initial_users.json", "test_content.json"] - - def setUp(self): - """ - FIXME: je n'arrive pas à générer des données de test incluant - déjà les permissions (cf. table ObjectAccess). C'est pour - cette raison que j'assigne le domaine test.com ici... - """ - super(PermissionsTestCase, self).setUp() - self.user = User.objects.get(username="user@test.com") - self.values = dict( - username=self.user.username, role="DomainAdmins", - is_active=self.user.is_active, email="user@test.com" - ) - self.admin = User.objects.get(username="admin@test.com") - dom = Domain.objects.get(name="test.com") - dom.add_admin(self.admin) - - def tearDown(self): - self.clt.logout() - - def test_domain_admins(self): - self.values["role"] = "DomainAdmins" - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[self.user.id]), - self.values) - self.assertEqual(self.user.group == "DomainAdmins", True) - - self.values["role"] = "SimpleUsers" - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[self.user.id]), - self.values) - self.assertEqual(self.user.group == 'DomainAdmins', False) - - def test_superusers(self): - self.values["role"] = "SuperAdmins" - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[self.user.id]), - self.values) - self.assertEqual(User.objects.get(username="user@test.com").is_superuser, True) - - self.values["role"] = "SimpleUsers" - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[self.user.id]), - self.values) - self.assertEqual(User.objects.get(username="user@test.com").is_superuser, False) - - def test_self_modif(self): - self.clt.logout() - self.assertEqual(self.clt.login(username="admin@test.com", password="toto"), - True) - admin = User.objects.get(username="admin@test.com") - values = dict(username="admin@test.com", first_name="Admin", password1="", password2="", - quota=10, is_active=True, email="admin@test.com") - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[admin.id]), values) - self.assertEqual(admin.group, "DomainAdmins") - self.assertEqual(admin.can_access(Domain.objects.get(name="test.com")), True) - - values["role"] = "SuperAdmins" - self.check_ajax_post(reverse("modoboa.admin.views.editaccount", args=[admin.id]), values) - admin = User.objects.get(username="admin@test.com") - self.assertEqual(admin.group, "DomainAdmins") - - def test_domadmin_access(self): - self.clt.logout() - self.assertEqual(self.clt.login(username="admin@test.com", password="toto"), - True) - response = self.clt.get(reverse("modoboa.admin.views.domains")) - self.assertEqual(response.status_code, 200) - - user = User.objects.get(username="user@test.com") - response = self.clt.get(reverse("modoboa.admin.views.editaccount", args=[user.id]), - HTTP_X_REQUESTED_WITH='XMLHttpRequest') - self.assertNotEqual(response["Content-Type"], "application/json") - - def test_domainadmin_deletes_superadmin(self): - """Check domain admins restrictions about super admins - - When a super admin owns a mailbox and a domain admin exists - for the associated domain, this domain admin must not be able - to access the super admin. - """ - values = dict(username="superadmin2@test.com", first_name="Super", last_name="Admin", - password1="toto", password2="toto", role="SuperAdmins", - is_active=True, email="superadmin2@test.com", stepid=2) - self.check_ajax_post(reverse("modoboa.admin.views.newaccount"), values) - - account = User.objects.get(username="superadmin2@test.com") - self.clt.logout() - self.clt.login(username="admin@test.com", password="toto") - self.check_ajax_get(reverse("modoboa.admin.views.delaccount", args=[account.id]), {}, - status="ko", respmsg="Permission denied") - self.check_ajax_get(reverse("modoboa.admin.views.delaccount", args=[account.id]), {}, - status="ko", respmsg="Permission denied") - - -class ImportTestCase(ModoTestCase): - fixtures = ["initial_users.json", "test_content.json"] - - def setUp(self): - super(ImportTestCase, self).setUp() - self.admin = User.objects.get(username="admin@test.com") - dom = Domain.objects.get(name="test.com") - dom.add_admin(self.admin) - - def test_domains_import(self): - f = ContentFile(b"""domain; domain1.com; 100; True -domain; domain2.com; 200; False -domainalias; domalias1.com; domain1.com; True -""", name="domains.csv") - self.clt.post(reverse("modoboa.admin.views.import_domains"), { - "sourcefile": f - }) - admin = User.objects.get(username="admin") - dom = Domain.objects.get(name="domain1.com") - self.assertEqual(dom.quota, 100) - self.assertTrue(dom.enabled) - self.assertTrue(admin.is_owner(dom)) - domalias = DomainAlias.objects.get(name="domalias1.com") - self.assertEqual(domalias.target, dom) - self.assertTrue(dom.enabled) - self.assertTrue(admin.is_owner(domalias)) - dom = Domain.objects.get(name="domain2.com") - self.assertEqual(dom.quota, 200) - self.assertFalse(dom.enabled) - self.assertTrue(admin.is_owner(dom)) - - def test_identities_import(self): - f = ContentFile(b""" -account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com -account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; test.com -alias; alias1@test.com; True; user1@test.com -forward; fwd1@test.com; True; user@extdomain.com -dlist; dlist@test.com; True; user1@test.com; user@extdomain.com -""", name="identities.csv") - self.clt.post(reverse("modoboa.admin.views.import_identities"), { - "sourcefile": f, "crypt_password": True - }) - admin = User.objects.get(username="admin") - u1 = User.objects.get(username="user1@test.com") - self.assertTrue(admin.is_owner(u1)) - self.assertEqual(u1.email, "user1@test.com") - self.assertEqual(u1.first_name, "User") - self.assertEqual(u1.last_name, "One") - self.assertTrue(u1.is_active) - self.assertEqual(u1.group, "SimpleUsers") - self.assertTrue(admin.is_owner(u1.mailbox_set.all()[0])) - self.assertEqual(u1.mailbox_set.all()[0].full_address, "user1@test.com") - self.assertTrue(self.clt.login(username="user1@test.com", password="toto")) - - da = User.objects.get(username="truc@test.com") - self.assertEqual(da.first_name, u"René") - self.assertEqual(da.group, "DomainAdmins") - self.assertEqual(da.mailbox_set.all()[0].full_address, "truc@test.com") - dom = Domain.objects.get(name="test.com") - self.assertIn(da, dom.admins) - u = User.objects.get(username="user@test.com") - self.assertTrue(da.can_access(u)) - - al = Alias.objects.get(address="alias1", domain__name="test.com") - self.assertIn(u1.mailbox_set.all()[0], al.mboxes.all()) - self.assertTrue(admin.is_owner(al)) - - fwd = Alias.objects.get(address="fwd1", domain__name="test.com") - self.assertIn("user@extdomain.com", fwd.extmboxes) - self.assertTrue(admin.is_owner(fwd)) - - dlist = Alias.objects.get(address="dlist", domain__name="test.com") - self.assertIn(u1.mailbox_set.all()[0], dlist.mboxes.all()) - self.assertIn("user@extdomain.com", dlist.extmboxes) - self.assertTrue(admin.is_owner(dlist)) - - def test_import_duplicate(self): - f = ContentFile(b""" -account; admin@test.com; toto; Admin; ; True; DomainAdmins; admin@test.com; test.com -account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; test.com -""", name="identities.csv") - self.clt.post(reverse("modoboa.admin.views.import_identities"), { - "sourcefile": f, "crypt_password": True, "continue_if_exists": True - }) - - admin = User.objects.get(username="admin") - u1 = User.objects.get(username="truc@test.com") - self.assertTrue(admin.is_owner(u1)) - - def test_import_superadmin(self): - """Check if a domain admin can import a superadmin - - Expected result: no - """ - self.clt.logout() - self.assertTrue(self.clt.login(username="admin@test.com", password="toto")) - f = ContentFile(b""" -account; sa@test.com; toto; Super; Admin; True; SuperAdmins; superadmin@test.com -""", name="identities.csv") - self.clt.post(reverse("modoboa.admin.views.import_identities"), { - "sourcefile": f, "crypt_password": True, "continue_if_exists": True - }) - with self.assertRaises(User.DoesNotExist): - User.objects.get(username="sa@test.com") diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py deleted file mode 100644 index b34136387..000000000 --- a/modoboa/admin/views.py +++ /dev/null @@ -1,889 +0,0 @@ -# coding: utf-8 -from django.http import HttpResponse, HttpResponseRedirect -from django.utils.translation import ugettext as _, ungettext -from django.core.urlresolvers import reverse -from django.contrib.auth.decorators \ - import login_required, permission_required, user_passes_test -from django.db.models import Q -from django.shortcuts import render -from django.template.loader import render_to_string -from django.db import transaction, IntegrityError -from django.views.decorators.csrf import ensure_csrf_cookie -import cStringIO -import csv - -from lib import get_sort_order, get_listing_page -from forms import * -from models import * -from modoboa.admin.tables import DomainsTable, ExtensionsTable -from modoboa.lib import events, parameters -from modoboa.lib.exceptions import ModoboaException, PermDeniedException -from modoboa.lib.webutils \ - import _render, ajax_response, ajax_simple_response, _render_to_string -from modoboa.lib.emailutils import split_mailbox -from modoboa.lib.models import Parameter, Log -from modoboa.lib.formutils import CreationWizard -from modoboa.lib.templatetags.libextras import pagination_bar - - -@login_required -def index(request): - return HttpResponseRedirect(reverse(domains)) - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.view_domains") or u.has_perm("admin.view_mailboxes")) -def _domains(request): - sort_order, sort_dir = get_sort_order(request.GET, "name") - domains = request.user.get_domains() - squery = request.GET.get("searchquery", None) - if squery is not None: - q = Q(name__contains=squery) - q |= Q(domainalias__name__contains=squery) - domains = domains.filter(q).distinct() - if sort_order in ["name", "domainalias__name"]: - domains = domains.order_by("%s%s" % (sort_dir, sort_order)) - page = get_listing_page(domains, request.GET.get("page", 1)) - return ajax_simple_response({ - "table": DomainsTable(request, page.object_list).render(), - "page": page.number, - "paginbar": pagination_bar(page), - "handle_mailboxes": parameters.get_admin("HANDLE_MAILBOXES", - raise_error=False), - "auto_account_removal": parameters.get_admin("AUTO_ACCOUNT_REMOVAL") - }) - - -@login_required -@ensure_csrf_cookie -def domains(request, tplname="admin/domains.html"): - if not request.user.has_perm("admin.view_domains"): - if request.user.has_perm("admin.view_mailboxes"): - return HttpResponseRedirect(reverse(identities)) - return HttpResponseRedirect(reverse("modoboa.userprefs.views.index")) - return render(request, tplname, {"selection": "domains"}) - - -@login_required -@permission_required("admin.add_user") -def domains_list(request): - doms = [dom.name for dom in request.user.get_domains()] - return ajax_simple_response(doms) - - -@login_required -@permission_required("admin.add_domain") -@transaction.commit_on_success -def newdomain(request, tplname="common/wizard_forms.html"): - events.raiseEvent("CanCreate", request.user, "domains") - - def newdomain_cb(steps): - genform = steps[0]["form"] - genform.is_valid() - domain = genform.save(request.user) - domain.post_create(request.user) - steps[1]["form"].save(request.user, domain) - - commonctx = {"title": _("New domain"), - "action_label": _("Create"), - "action_classes": "submit", - "action": reverse(newdomain), - "formid": "domform"} - cwizard = CreationWizard(newdomain_cb) - cwizard.add_step(DomainFormGeneral, _("General"), - [dict(classes="btn-inverse next", label=_("Next"))], - formtpl="admin/domain_general_form.html") - cwizard.add_step(DomainFormOptions, _("Options"), - [dict(classes="btn-primary submit", label=_("Create")), - dict(classes="btn-inverse prev", label=_("Previous"))], - formtpl="admin/domain_options_form.html") - - if request.method == "POST": - retcode, data = cwizard.validate_step(request) - if retcode == -1: - raise AdminError(data) - if retcode == 1: - return ajax_simple_response(dict( - status="ok", title=cwizard.get_title(data + 1), stepid=data - )) - if retcode == 2: - return ajax_simple_response( - dict(status="ok", respmsg=_("Domain created")) - ) - - from modoboa.lib.templatetags.libextras import render_form - return ajax_simple_response(dict( - status="ko", stepid=data, - form=render_form(cwizard.steps[data]["form"]) - )) - - cwizard.create_forms() - commonctx.update(steps=cwizard.steps) - commonctx.update(subtitle="1. %s" % cwizard.steps[0]['title']) - return render(request, tplname, commonctx) - - -@login_required -@permission_required("admin.view_domains") -def editdomain(request, dom_id, tplname="admin/editdomainform.html"): - domain = Domain.objects.get(pk=dom_id) - if not request.user.can_access(domain): - raise PermDeniedException - - domadmins = filter( - lambda u: request.user.can_access(u) and not u.is_superuser, - domain.admins - ) - if not request.user.is_superuser: - domadmins = filter(lambda u: u.group == "DomainAdmins", domadmins) - - instances = dict(general=domain) - events.raiseEvent("FillDomainInstances", request.user, domain, instances) - - commonctx = {"title": domain.name, - "action_label": _("Update"), - "action_classes": "submit", - "action": reverse(editdomain, args=[dom_id]), - "formid": "domform", - "domain": domain} - if request.method == "POST": - error = None - domain.oldname = domain.name - form = DomainForm(request.user, request.POST, instances=instances) - if form.is_valid(): - try: - form.save(request.user) - except AdminError, e: - error = str(e) - else: - events.raiseEvent("DomainModified", domain) - return ajax_simple_response({"status": "ok", "respmsg": _("Domain modified")}) - - commonctx["tabs"] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) - - commonctx["tabs"] = DomainForm(request.user, instances=instances) - commonctx["domadmins"] = domadmins - return render(request, tplname, commonctx) - - -@login_required -@permission_required("admin.delete_domain") -@transaction.commit_on_success -def deldomain(request, dom_id): - keepdir = True if request.POST.get("keepdir", "false") == "true" else False - try: - mb = Mailbox.objects.get(user__id=request.user.id) - except Mailbox.DoesNotExist: - mb = None - - dom = Domain.objects.get(pk=dom_id) - if not request.user.can_access(dom): - raise PermDeniedException - if mb and mb.domain == dom: - raise AdminError(_("You can't delete your own domain")) - dom.delete(request.user, keepdir) - - msg = ungettext("Domain deleted", "Domains deleted", 1) - return ajax_simple_response({"status": "ok", "respmsg": msg}) - - -def _validate_alias(request, form, successmsg, tplname, commonctx, callback=None): - """Alias validation - - Common function shared between creation and modification actions. - """ - error = None - if form.is_valid(): - form.set_recipients() - try: - alias = form.save() - except IntegrityError: - raise AdminError(_("Alias with this name already exists")) - if callback: - callback(request.user, alias) - return ajax_simple_response({"status": "ok", "respmsg": successmsg}) - - if "targets" in request.POST: - targets = request.POST.getlist("targets") - commonctx["targets"] = targets[:-1] - - commonctx["form"] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) - - -def _new_alias(request, title, action, successmsg, - tplname="common/generic_modal_form.html"): - events.raiseEvent("CanCreate", request.user, "mailbox_aliases") - ctx = { - "title": title, - "action": action, - "formid": "aliasform", - "action_label": _("Create"), - "action_classes": "submit" - } - if request.method == "POST": - def callback(user, alias): - alias.post_create(user) - - form = AliasForm(request.user, request.POST) - return _validate_alias( - request, form, successmsg, tplname, ctx, callback - ) - - form = AliasForm(request.user) - ctx["form"] = form - return render(request, tplname, ctx) - - -@login_required -@permission_required("admin.add_alias") -@transaction.commit_on_success -def newdlist(request): - return _new_alias( - request, _("New distribution list"), reverse(newdlist), - _("Distribution list created") - ) - - -@login_required -@permission_required("admin.add_alias") -@transaction.commit_on_success -def newalias(request): - return _new_alias( - request, _("New alias"), reverse(newalias), - _("Alias created") - ) - - -@login_required -@permission_required("admin.add_alias") -@transaction.commit_on_success -def newforward(request): - return _new_alias( - request, _("New forward"), reverse(newforward), - _("Forward created") - ) - - -@login_required -@permission_required("admin.change_alias") -def editalias(request, alid, tplname="common/generic_modal_form.html"): - alias = Alias.objects.get(pk=alid) - if not request.user.can_access(alias): - raise PermDeniedException - ctx = dict( - action=reverse(editalias, args=[alias.id]), - formid="aliasform", - title=alias.full_address, - action_label=_("Update"), - action_classes="submit" - ) - if len(alias.get_recipients()) >= 2: - successmsg = _("Distribution list modified") - elif alias.extmboxes != "": - successmsg = _("Forward modified") - else: - successmsg = _("Alias modified") - if request.method == "POST": - form = AliasForm(request.user, request.POST, instance=alias) - return _validate_alias(request, form, successmsg, tplname, ctx) - - form = AliasForm(request.user, instance=alias) - ctx["form"] = form - return render(request, tplname, ctx) - - -def _del_alias(request, msg, msgs): - selection = request.GET["selection"].split(",") - for alid in selection: - alias = Alias.objects.get(pk=alid) - if not request.user.can_access(alias): - raise PermDeniedException - alias.delete() - - msg = ungettext(msg, msgs, len(selection)) - return ajax_simple_response({"status": "ok", "respmsg": msg}) - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def delalias(request): - return _del_alias(request, "Alias deleted", "Aliases deleted") - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def deldlist(request): - return _del_alias(request, "Distribution list deleted", - "Distribution lists deleted") - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def delforward(request): - return _del_alias(request, "Forward deleted", "Forwards deleted") - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) -def _identities(request): - idents_list = request.user.get_identities(request.GET) - sort_order, sort_dir = get_sort_order(request.GET, "identity", - ["identity", "name_or_rcpt", "tags"]) - if sort_order in ["identity", "name_or_rcpt"]: - objects = sorted(idents_list, key=lambda o: getattr(o, sort_order), - reverse=sort_dir == '-') - else: - objects = sorted(idents_list, key=lambda o: o.tags[0], - reverse=sort_dir == '-') - page = get_listing_page(objects, request.GET.get("page", 1)) - return ajax_simple_response({ - "table": _render_to_string(request, "admin/identities_table.html", { - "identities": page.object_list, - "tableid": "objects_table" - }), - "handle_mailboxes": parameters.get_admin("HANDLE_MAILBOXES", - raise_error=False), - "page": page.number, - "paginbar": pagination_bar(page) - }) - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) -@ensure_csrf_cookie -def identities(request, tplname="admin/identities.html"): - return render(request, tplname, { - "selection": "identities", - "deflocation": "list/" - }) - - -@login_required -@permission_required("admin.add_user") -def accounts_list(request): - accs = User.objects.filter(is_superuser=False) \ - .exclude(groups__name='SimpleUsers') - res = [a.username for a in accs.all()] - return ajax_simple_response(res) - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") - or u.has_perm("admin.add_alias")) -def allowed_recipients_list(request): - ret = [mb.full_address for mb in request.user.get_mailboxes()] - ret += [al.full_address for al in request.user.get_aliases()] - return ajax_simple_response(ret) - - -@login_required -@permission_required("admin.add_mailbox") -def list_quotas(request, tplname="admin/quotas.html"): - from modoboa.lib.dbutils import db_type - - sort_order, sort_dir = get_sort_order(request.GET, "address") - mboxes = request.user.get_mailboxes(request.GET.get("searchquery", None)) - mboxes = mboxes.exclude(quota=0) - if sort_order in ["address", "quota", "quota_value__bytes"]: - mboxes = mboxes.order_by("%s%s" % (sort_dir, sort_order)) - elif sort_order == "quota_usage": - if db_type() == "postgres": - select = '(admin_quota.bytes::float / (CAST(admin_mailbox.quota AS BIGINT) * 1048576)) * 100' - else: - select = 'admin_quota.bytes / (admin_mailbox.quota * 1048576) * 100' - mboxes = mboxes.extra( - select={'quota_usage': select}, - where=["admin_quota.mbox_id=admin_mailbox.id"], - tables=["admin_quota"], - order_by=["%s%s" % (sort_dir, sort_order)] - ) - else: - raise AdminError(_("Invalid request")) - page = get_listing_page(mboxes, request.GET.get("page", 1)) - return ajax_simple_response({ - "status": "ok", - "page": page.number, - "paginbar": pagination_bar(page), - "table": _render_to_string(request, tplname, { - "mboxes": page - }) - }) - - -@login_required -@permission_required("admin.add_user") -@transaction.commit_on_success -def newaccount(request, tplname='common/wizard_forms.html'): - def create_account(steps): - """Account creation callback - - Called when all creation steps have been validated. - - :param steps: the steps data - """ - genform = steps[0]["form"] - genform.is_valid() - account = genform.save() - account.post_create(request.user) - - mailform = steps[1]["form"] - try: - mailform.save(request.user, account) - except AdminError: - # A bit uggly: transaction management doesn't work very - # well with nested functions. Need to wait for django 1.6 - # and atomicity. - account.delete(request.user, False) - raise - - ctx = dict( - title=_("New account"), - action=reverse(newaccount), - formid="newaccount_form", - submit_label=_("Create") - ) - cwizard = CreationWizard(create_account) - cwizard.add_step(AccountFormGeneral, _("General"), - [dict(classes="btn-inverse next", label=_("Next"))], - new_args=[request.user]) - cwizard.add_step(AccountFormMail, _("Mail"), - [dict(classes="btn-primary submit", label=_("Create")), - dict(classes="btn-inverse prev", label=_("Previous"))], - formtpl="admin/mailform.html") - - if request.method == "POST": - retcode, data = cwizard.validate_step(request) - if retcode == -1: - raise AdminError(data) - if retcode == 1: - return ajax_simple_response(dict( - status="ok", title=cwizard.get_title(data + 1), stepid=data - )) - if retcode == 2: - return ajax_simple_response(dict( - status="ok", respmsg=_("Account created") - )) - - from modoboa.lib.templatetags.libextras import render_form - return ajax_simple_response(dict( - status="ko", stepid=data, - form=render_form(cwizard.steps[data]["form"]) - )) - - cwizard.create_forms() - ctx.update(steps=cwizard.steps) - ctx.update(subtitle="1. %s" % cwizard.steps[0]['title']) - return render(request, tplname, ctx) - - -@login_required -@permission_required("admin.change_user") -@transaction.commit_on_success -def editaccount(request, accountid, tplname="common/tabforms.html"): - account = User.objects.get(pk=accountid) - if not request.user.can_access(account): - raise PermDeniedException - mb = None - if account.has_mailbox: - mb = account.mailbox_set.all()[0] - - instances = dict(general=account, mail=mb, perms=account) - events.raiseEvent("FillAccountInstances", request.user, account, instances) - - ctx = dict( - title=account.username, - formid="accountform", - action=reverse(editaccount, args=[accountid]), - action_label=_("Update"), - action_classes="submit" - ) - - if request.method == "POST": - classes = {} - form = AccountForm(request.user, request.POST, - instances=instances, classes=classes) - account.oldgroup = account.group - if form.is_valid(mandatory_only=True): - form.save_general_form() - if form.is_valid(optional_only=True): - events.raiseEvent("AccountModified", account, form.account) - form.save() - return ajax_simple_response( - dict(status="ok", respmsg=_("Account updated")) - ) - transaction.rollback() - - ctx["tabs"] = form - return ajax_response(request, status="ko", template=tplname, **ctx) - - ctx["tabs"] = AccountForm(request.user, instances=instances) - active_tab_id = request.GET.get("active_tab", "default") - if active_tab_id != "default": - ctx["tabs"].active_id = active_tab_id - return render(request, tplname, ctx) - - -@login_required -@permission_required("admin.delete_user") -@transaction.commit_on_success -def delaccount(request, accountid): - keepdir = True if request.POST.get("keepdir", "false") == "true" else False - - User.objects.get(pk=accountid).delete(request.user, keepdir) - - msg = ungettext("Account deleted", "Accounts deleted", 1) - return ajax_simple_response({"status": "ok", "respmsg": msg}) - - -@login_required -@permission_required("admin.add_domain") -def remove_permission(request): - domid = request.GET.get("domid", None) - daid = request.GET.get("daid", None) - if domid is None or daid is None: - raise AdminError(_("Invalid request")) - try: - account = User.objects.get(pk=daid) - domain = Domain.objects.get(pk=domid) - except (User.DoesNotExist, Domain.DoesNotExist): - raise AdminError(_("Invalid request")) - if not request.user.can_access(account) or not request.user.can_access(domain): - raise PermDeniedException - domain.remove_admin(account) - return ajax_simple_response(dict(status="ok")) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def viewsettings(request, tplname='admin/settings_header.html'): - return render(request, tplname, { - "selection": "settings" - }) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def viewparameters(request, tplname='admin/parameters.html'): - return ajax_simple_response({ - "status": "ok", - "left_selection": "parameters", - "content": _render_to_string(request, tplname, { - "forms": parameters.get_admin_forms - }) - }) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def saveparameters(request): - for formdef in parameters.get_admin_forms(request.POST): - form = formdef["form"] - if form.is_valid(): - form.save() - form.to_django_settings() - continue - return ajax_simple_response({ - "status": "ko", "prefix": form.app, "errors": form.errors - }) - return ajax_simple_response(dict( - status="ok", respmsg=_("Parameters saved") - )) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def viewextensions(request, tplname='admin/extensions.html'): - from modoboa.extensions import exts_pool - - exts = exts_pool.list_all() - for ext in exts: - try: - dbext = Extension.objects.get(name=ext["id"]) - ext["selection"] = dbext.enabled - except Extension.DoesNotExist: - dbext = Extension() - dbext.name = ext["id"] - dbext.enabled = False - dbext.save() - ext["selection"] = False - - tbl = ExtensionsTable(request, exts) - return ajax_simple_response({ - "status": "ok", - "content": _render_to_string(request, tplname, {"extensions": tbl}) - }) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def saveextensions(request): - actived_exts = Extension.objects.filter(enabled=True) - found = [] - for k in request.POST.keys(): - if k.startswith("select_"): - parts = k.split("_", 1) - dbext = Extension.objects.get(name=parts[1]) - if not dbext in actived_exts: - dbext.on() - else: - found += [dbext] - for ext in actived_exts: - if not ext in found: - ext.off() - - return ajax_simple_response(dict( - status="ok", respmsg=_("Modifications applied.") - )) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def information(request, tplname="admin/information.html"): - return ajax_simple_response({ - "status": "ok", - "content": render_to_string(tplname, {}) - }) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def logs(request, tplname="admin/logs.html"): - sort_order, sort_dir = get_sort_order( - request.GET, "date_created", - allowed_values=['date_created', 'level', 'logger', 'message'] - ) - page = get_listing_page( - Log.objects.all().order_by("%s%s" % (sort_dir, sort_order)), - request.GET.get("page", 1) - ) - return ajax_simple_response({ - "status": "ok", - "content": render_to_string(tplname, { - "logs": page.object_list, - }), - "page": page.number, - "paginbar": pagination_bar(page), - }) - - -@transaction.commit_on_success -def import_domain(user, row, formopts): - """Specific code for domains import""" - dom = Domain() - dom.from_csv(user, row) - - -@transaction.commit_on_success -def import_domainalias(user, row, formopts): - """Specific code for domain aliases import""" - domalias = DomainAlias() - domalias.from_csv(user, row) - - -@transaction.commit_on_success -def import_account(user, row, formopts): - """Specific code for accounts import""" - account = User() - account.from_csv(user, row, formopts["crypt_password"]) - - -@transaction.commit_on_success -def _import_alias(user, row, **kwargs): - """Specific code for aliases import""" - alias = Alias() - alias.from_csv(user, row, **kwargs) - - -def import_alias(user, row, formopts): - _import_alias(user, row, expected_elements=4) - - -def import_forward(user, row, formopts): - _import_alias(user, row, expected_elements=4) - - -def import_dlist(user, row, formopts): - _import_alias(user, row) - -def importdata(request, formclass=ImportDataForm): - """Generic import function - - As the process of importing data from a CSV file is the same - whatever the type, we do a maximum of the work here. - - :param request: a ``Request`` instance - :param typ: a string indicating the object type being imported - :return: a ``Response`` instance - """ - error = None - form = formclass(request.POST, request.FILES) - if form.is_valid(): - try: - reader = csv.reader(request.FILES['sourcefile'], - delimiter=form.cleaned_data['sepchar']) - except csv.Error, e: - error = str(e) - - if error is None: - try: - cpt = 0 - for row in reader: - if not row: - continue - try: - fct = globals()["import_%s" % row[0].strip()] - except KeyError: - continue - try: - fct(request.user, row, form.cleaned_data) - except IntegrityError, e: - if form.cleaned_data["continue_if_exists"]: - continue - raise ModoboaException(_("Object already exists: %s" % row)) - cpt += 1 - msg = _("%d objects imported successfully" % cpt) - return render(request, "admin/import_done.html", { - "status": "ok", "msg": msg - }) - except (ModoboaException), e: - error = str(e) - - return render(request, "admin/import_done.html", { - "status": "ko", "msg": error - }) - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) -def import_identities(request): - if request.method == "POST": - return importdata(request, ImportIdentitiesForm) - - helptext = _("""Provide a CSV file where lines respect one of the following formats: -
    -
  • account; loginname; password; first name; last name; enabled; group; address[, domain, ...]
  • -
  • alias; address; enabled; internal recipient
  • -
  • forward; address; enabled; external recipient
  • -
  • dlist; address; enabled; recipient; recipient; ...
  • -
-

The first element of each line is mandatory and must be equal to one of the previous values.

- -

You can use a different character as separator.

-""") - ctx = dict( - title=_("Import identities"), - action_label=_("Import"), - action_classes="submit", - action=reverse(import_identities), - formid="importform", - enctype="multipart/form-data", - target="import_target", - form=ImportIdentitiesForm(), - helptext=helptext - ) - return render(request, "admin/importform.html", ctx) - - -def _export(content, filename): - """Export a csv file's content - - :param content: the content to export (string) - :param filename: the name that will appear into the response - :return: an ``HttpResponse`` object - """ - resp = HttpResponse(content) - resp["Content-Type"] = "text/csv" - resp["Content-Length"] = len(content) - resp["Content-Disposition"] = 'attachment; filename="%s"' % filename - return resp - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) -def export_identities(request): - ctx = { - "title": _("Export identities"), - "action_label": _("Export"), - "action_classes": "submit", - "formid": "exportform", - "action": reverse(export_identities), - } - - if request.method == "POST": - form = ExportIdentitiesForm(request.POST) - form.is_valid() - fp = cStringIO.StringIO() - csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) - for ident in request.user.get_identities(): - ident.to_csv(csvwriter) - content = fp.getvalue() - fp.close() - return _export(content, form.cleaned_data["filename"]) - - ctx["form"] = ExportIdentitiesForm() - return render(request, "common/generic_modal_form.html", ctx) - - -@login_required -@permission_required("admin.add_domain") -@transaction.commit_on_success -def import_domains(request): - if request.method == "POST": - return importdata(request) - - helptext = _("""Provide a CSV file where lines respect one of the following formats: -
    -
  • domain; name; quota; enabled
  • -
  • domainalias; name; targeted domain; enabled
  • -
-

The first element of each line is mandatory and must be equal to one of the previous values.

-

You can use a different character as separator.

-""") - - ctx = dict( - title=_("Import domains"), - action_label=_("Import"), - action_classes="submit", - action=reverse(import_domains), - formid="importform", - enctype="multipart/form-data", - target="import_target", - helptext=helptext, - form=ImportDataForm() - ) - return render(request, "admin/importform.html", ctx) - - -@login_required -@permission_required("admin.add_domain") -def export_domains(request): - ctx = { - "title": _("Export domains"), - "action_label": _("Export"), - "action_classes": "submit", - "formid": "exportform", - "action": reverse(export_domains), - } - - if request.method == "POST": - form = ExportDomainsForm(request.POST) - form.is_valid() - fp = cStringIO.StringIO() - csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) - for dom in request.user.get_domains(): - dom.to_csv(csvwriter) - for da in dom.domainalias_set.all(): - da.to_csv(csvwriter) - content = fp.getvalue() - fp.close() - return _export(content, form.cleaned_data["filename"]) - - ctx["form"] = ExportDomainsForm() - return render(request, "common/generic_modal_form.html", ctx) diff --git a/modoboa/auth/forms.py b/modoboa/auth/forms.py deleted file mode 100644 index 143299654..000000000 --- a/modoboa/auth/forms.py +++ /dev/null @@ -1,17 +0,0 @@ -# coding: utf-8 -from django import forms -from django.utils.translation import ugettext_lazy as _ - -class LoginForm(forms.Form): - username = forms.CharField( - label=_("Username"), - widget=forms.TextInput(attrs={"class": "input-block-level"}) - ) - password = forms.CharField( - label=_("Password"), - widget=forms.PasswordInput(attrs={"class": "input-block-level"}) - ) - rememberme = forms.BooleanField( - initial=False, - required=False - ) diff --git a/modoboa/auth/models.py b/modoboa/auth/models.py deleted file mode 100755 index 71a836239..000000000 --- a/modoboa/auth/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/modoboa/auth/tests.py b/modoboa/auth/tests.py deleted file mode 100755 index 082b0c52d..000000000 --- a/modoboa/auth/tests.py +++ /dev/null @@ -1,23 +0,0 @@ -# coding: utf-8 -""" -This file demonstrates two different styles of tests (one doctest and one -unittest). These will both pass when you run "manage.py test". - -Replace these with more appropriate tests for your application. -""" - -from django.test import TestCase - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.failUnlessEqual(1 + 1, 2) - -__test__ = {"doctest": """ -Another way to test that 1 + 1 is equal to 2. - ->>> 1 + 1 == 2 -True -"""} diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index e69de29bb..824c49add 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -0,0 +1,55 @@ +from django.utils.translation import ugettext_lazy +from modoboa.lib import parameters, events + +base_events = [ + "CanCreate", + + "AccountCreated", + "AccountAutoCreated", + "AccountModified", + "AccountDeleted", + "AccountExported", + "AccountImported", + "PasswordUpdated", + "ExtraAccountActions", + "RoleChanged", + "GetExtraRoles", + "PasswordChange", + + "UserMenuDisplay", + "AdminMenuDisplay", + "GetStaticContent", + + "ExtEnabled", + "ExtDisabled", + + "UserLogin", + "UserLogout", + + "GetAnnouncement", + + "TopNotifications", + "ExtraAdminContent", + + "ExtraUprefsRoutes", + "ExtraUprefsJS" +] + + +def load_settings(): + from .app_settings import GeneralParametersForm, UserSettings + + parameters.register(GeneralParametersForm, ugettext_lazy("General")) + parameters.register(UserSettings, ugettext_lazy("General")) + events.declare(base_events) + + +@events.observe("ExtDisabled") +def unset_default_topredirection(extension): + """ + Simple callback to change the default redirection if the + corresponding extension is being disabled. + """ + topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION") + if topredirection == extension.name: + parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs") diff --git a/modoboa/admin/app_settings.py b/modoboa/core/app_settings.py similarity index 83% rename from modoboa/admin/app_settings.py rename to modoboa/core/app_settings.py index 1f02db899..9acfafae5 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/core/app_settings.py @@ -1,9 +1,10 @@ +# coding: utf-8 from django import forms +from django.conf import settings from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.lib.formutils import YesNoField, SeparatorField, InlineRadioSelect -from modoboa.lib.sysutils import exec_cmd +from modoboa.lib.cryptutils import random_key from modoboa.lib import parameters -from modoboa.auth.lib import random_key def enabled_applications(): @@ -15,18 +16,18 @@ def enabled_applications(): :return: a list """ - from modoboa.admin.models import Extension + from modoboa.core.models import Extension from modoboa.lib.dbutils import db_table_exists - result = [("userprefs", "userprefs")] - if db_table_exists("admin_extension"): + result = [("core", "core")] + if db_table_exists("core_extension"): exts = Extension.objects.filter(enabled=True) result += [(ext.name, ext.name) for ext in exts] return sorted(result, key=lambda e: e[0]) class GeneralParametersForm(parameters.AdminParametersForm): - app = "admin" + app = "core" sep1 = SeparatorField(label=ugettext_lazy("Authentication")) @@ -97,7 +98,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): label=ugettext_lazy("Bind password"), initial='', help_text=ugettext_lazy("The password to use when binding to the LDAP server (with 'Bind DN')"), - widget=forms.PasswordInput(render_value=True), + widget=forms.PasswordInput, required=False ) @@ -134,26 +135,6 @@ class GeneralParametersForm(parameters.AdminParametersForm): help_text=ugettext_lazy("Tell if the LDAP server is an Active Directory one") ) - sep2 = SeparatorField(label=ugettext_lazy("Mailboxes")) - - handle_mailboxes = YesNoField( - label=ugettext_lazy("Handle mailboxes on filesystem"), - initial="no", - help_text=ugettext_lazy("Rename or remove mailboxes on the filesystem when they get renamed or removed within Modoboa") - ) - - mailboxes_owner = forms.CharField( - label=ugettext_lazy("Mailboxes ower"), - initial="vmail", - help_text=ugettext_lazy("The UNIX account who owns mailboxes on the filesystem") - ) - - auto_account_removal = YesNoField( - label=ugettext_lazy("Automatic account removal"), - initial="no", - help_text=ugettext_lazy("When a mailbox is removed, also remove the associated account"), - ) - sep3 = SeparatorField(label=ugettext_lazy("Miscellaneous")) log_maximum_age = forms.IntegerField( @@ -171,13 +152,12 @@ class GeneralParametersForm(parameters.AdminParametersForm): default_top_redirection = forms.ChoiceField( label=ugettext_lazy("Default top redirection"), choices=[], - initial="userprefs", + initial="core", help_text=ugettext_lazy("The default redirection used when no application is specified") ) # Visibility rules visibility_rules = { - "mailboxes_owner": "handle_mailboxes=yes", "ldap_sep": "authentication_type=ldap", "ldap_server_address": "authentication_type=ldap", "ldap_server_port": "authentication_type=ldap", @@ -195,24 +175,6 @@ class GeneralParametersForm(parameters.AdminParametersForm): def __init__(self, *args, **kwargs): super(GeneralParametersForm, self).__init__(*args, **kwargs) self.fields["default_top_redirection"].choices = enabled_applications() - hide_fields = False - try: - code, version = exec_cmd("dovecot --version") - except OSError, e: - hide_fields = True - else: - if code or not version.strip().startswith("2"): - hide_fields = True - if hide_fields: - del self.fields["handle_mailboxes"] - del self.fields["mailboxes_owner"] - - def clean_secret_key(self): - if len(self.cleaned_data["secret_key"]) not in [16, 24, 32]: - raise forms.ValidationError( - _("Key must be either 16, 24, or 32 bytes long") - ) - return self.cleaned_data["secret_key"] def clean_ldap_user_dn_template(self): tpl = self.cleaned_data["ldap_user_dn_template"] @@ -239,7 +201,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): required_fields = ["ldap_search_base", "ldap_search_filter"] else: required_fields = ["ldap_user_dn_template"] - + for f in required_fields: if not f in cleaned_data or cleaned_data[f] == u'': self._errors[f] = self.error_class([_("This field is required")]) @@ -251,7 +213,6 @@ class GeneralParametersForm(parameters.AdminParametersForm): Doing so, we can use the django_auth_ldap module. """ - from django.conf import settings try: import ldap from django_auth_ldap.config import LDAPSearch @@ -289,3 +250,25 @@ class GeneralParametersForm(parameters.AdminParametersForm): }) else: settings.AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_REFERRALS] = False + + +def translate_language_code(value): + if "-" in value: + return value.split("-")[0] + return value + + +class UserSettings(parameters.UserParametersForm): + app = "core" + + sep = SeparatorField(label=ugettext_lazy("Display")) + + lang = forms.ChoiceField( + initial=translate_language_code(settings.LANGUAGE_CODE), + label=ugettext_lazy("Prefered language"), + choices=[("cs", "čeština"), ("de", "deutsch"), + ("en", "english"), ("es", "español"), + ("fr", "français"), ("it", "italiano"), + ("pt", "português"), ("sv", "svenska")], + help_text=ugettext_lazy("Prefered language to display pages") + ) diff --git a/modoboa/core/commands/__init__.py b/modoboa/core/commands/__init__.py index e69de29bb..9b2b1c9f2 100644 --- a/modoboa/core/commands/__init__.py +++ b/modoboa/core/commands/__init__.py @@ -0,0 +1,90 @@ +# coding: utf-8 +import os +import sys +import argparse +from django.conf import settings +from django.template import Context, Template + + +class Command(object): + """Base command class + + A valid administrative command must inherit from this class. + """ + help = "No help available." + + def __init__(self, commands, verbose=False, **kwargs): + self._commands = commands + self._parser = argparse.ArgumentParser() + self._verbose = verbose + if not settings.configured: + settings.configure() + self._templates_dir = "%s/templates" % os.path.dirname(__file__) + + def _render_template(self, tplfile, env): + fp = open(tplfile) + t = Template(fp.read()) + fp.close() + return t.render(Context(env)) + + def run(self, cmdline): + args = self._parser.parse_args(cmdline) + self.handle(args) + + def handle(self, parsed_args): + """A command must overload this method to be called + + :param parsed_args: + """ + raise NotImplementedError + + +def scan_for_commands(dirname=""): + """Build a dictionnary containing all commands + + :param str dirname: the directory where commands are located + :return: a dict of commands (name : class) + """ + path = os.path.join(os.path.dirname(__file__), dirname) + result = {} + for f in os.listdir(path): + if f in ['.', '..', '__init__.py']: + continue + if not f.endswith('.py'): + continue + if os.path.isfile(f): + continue + cmdname = f.replace('.py', '') + cmdmod = __import__("modoboa.core.commands", globals(), locals(), + [cmdname]) + cmdmod = getattr(cmdmod, cmdname) + if '_' in cmdname: + cmdclassname = ''.join([s.capitalize() for s in cmdname.split('_')]) + else: + cmdclassname = cmdname.capitalize() + try: + cmdclass = getattr(cmdmod, "%sCommand" % cmdclassname) + except AttributeError: + continue + result[cmdname] = cmdclass + return result + + +def handle_command_line(): + commands = scan_for_commands() + parser = argparse.ArgumentParser( + description="A set of utilities to ease the installation of Modoboa.", + epilog="""Available commands: +%s +""" % "\n".join(["\t%s" % c for c in sorted(commands)])) + parser.add_argument('--verbose', action='store_true', + help='Activate verbose output') + parser.add_argument('command', type=str, + help='A valid command name') + (args, remaining) = parser.parse_known_args() + + if not args.command in commands: + print >> sys.stderr, "Unknown command '%s'" % args.command + sys.exit(1) + + commands[args.command](commands, verbose=args.verbose).run(remaining) diff --git a/modoboa/core/commands/deploy.py b/modoboa/core/commands/deploy.py index 3c9c15107..5d10ad69b 100644 --- a/modoboa/core/commands/deploy.py +++ b/modoboa/core/commands/deploy.py @@ -4,18 +4,21 @@ import os import shutil import getpass import subprocess +import dj_database_url + from django.core import management from django.template import Context, Template -from modoboa.core.management import Command +from modoboa.lib.sysutils import exec_cmd +from modoboa.core.commands import Command dbconn_tpl = """ '{{ conn_name }}': { - 'ENGINE': 'django.db.backends.{{ dbtype }}', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. - 'NAME': '{{ dbname }}', # Or path to database file if using sqlite3. - 'USER': '{{ username }}', # Not used with sqlite3. - 'PASSWORD': '{{ password }}', # Not used with sqlite3. - 'HOST': '{{ dbhost }}', # Set to empty string for localhost. Not used with sqlite3. - 'PORT': '', # Set to empty string for default. Not used with sqlite3.{% if dbtype == 'mysql' %} + 'ENGINE': '{{ ENGINE }}', + 'NAME': '{{ NAME }}', # Or path to database file if using sqlite3. + 'USER': '{% if USER %}{{ USER }}{% endif %}', # Not used with sqlite3. + 'PASSWORD': '{% if PASSWORD %}{{ PASSWORD }}{% endif %}', # Not used with sqlite3. + 'HOST': '{% if HOST %}{{ HOST }}{% endif %}', # Set to empty string for localhost. Not used with sqlite3. + 'PORT': '{% if PORT %}{{ PORT }}{% endif %}', # Set to empty string for default. Not used with sqlite3.{% if ENGINE == 'django.db.backends.mysql' %} 'OPTIONS' : { "init_command" : 'SET foreign_key_checks = 0;', },{% endif %} @@ -42,6 +45,16 @@ class DeployCommand(Command): '--collectstatic', action='store_true', default=False, help='Run django collectstatic command' ) + self._parser.add_argument( + '--dburl', type=str, nargs=1, default=None, + help='The database-url for your modoboa instance') + self._parser.add_argument( + '--amavis_dburl', type=str, nargs=1, default=None, + help='The database-url for your amavis instance') + self._parser.add_argument( + '--domain', type=str, nargs=1, default=None, + help='The domain under which you want to deploy modoboa') + def _exec_django_command(self, name, cwd, *args): """Run a django command for the freshly created project @@ -75,19 +88,29 @@ class DeployCommand(Command): """ print "Configuring database connection: %s" % name info = {'conn_name': name, - 'dbtype': raw_input('Database type (mysql or postgres): ')} - if info['dbtype'] not in ['mysql', 'postgres']: - info['dbtype'] = 'mysql' - if info['dbtype'] == 'postgres': - info['dbtype'] = 'postgresql_psycopg2' - default_host = 'localhost' if info['dbtype'] == 'mysql' else '' - info['dbhost'] = \ - raw_input("Database host (default: '%s'): " % default_host) - if info['dbhost'] == '': - info['dbhost'] = default_host - info['dbname'] = raw_input('Database name: ') - info['username'] = raw_input('Username: ') - info['password'] = getpass.getpass('Password: ') + 'ENGINE': raw_input('Database type (mysql, postgres or sqlite3): ')} + if info['ENGINE'] not in ['mysql', 'postgres', 'sqlite3']: + raise RuntimeError('Unsupported database engine') + + if info['ENGINE'] == 'sqlite3': + info['ENGINE'] = 'django.db.backends.sqlite3' + info['NAME'] = '%s.db' % name + return info + if info['ENGINE'] == 'postgres': + info['ENGINE'] = 'django.db.backends.postgresql_psycopg2' + default_port = 5432 + else: + info['ENGINE'] = 'django.db.backends.mysql' + default_port = 3306 + info['HOST'] = raw_input("Database host (default: 'localhost'): ") + info['PORT'] = raw_input("Database port (default: '%s'): " % default_port) + # leave port setting empty, if default value is supplied and + # leave it to django + if info['PORT'] == default_port: + info['PORT'] = '' + info['NAME'] = raw_input('Database name: ') + info['USER'] = raw_input('Username: ') + info['PASSWORD'] = getpass.getpass('Password: ') return info def handle(self, parsed_args): @@ -105,13 +128,41 @@ class DeployCommand(Command): django14 = False t = Template(dbconn_tpl) - default_conn = t.render(Context(self.ask_db_info())) - amavis_conn = t.render(Context(self.ask_db_info('amavis'))) \ - if parsed_args.with_amavis else None - allowed_host = raw_input( - 'Under which domain do you want to deploy modoboa? ' - ) + if parsed_args.dburl: + info = dj_database_url.config(default=parsed_args.dburl[0]) + # In case the user fails to supply a valid database url, fallback to manual mode + if not info: + print "There was a problem with your database-url. \n" + info = self.ask_db_info() + #If we set this earlier, our fallback method will never be triggered + info['conn_name'] = 'default' + else: + info = self.ask_db_info() + + default_conn = t.render(Context(info)) + + if parsed_args.with_amavis: + if parsed_args.amavis_dburl: + amavis_info = dj_database_url.config(default=parsed_args.amavis_dburl[0]) + # In case the user fails to supply a valid database url, fallback to manual mode + if not amavis_info: + amavis_info = self.ask_db_info('amavis') + #If we set this earlier, our fallback method will never be triggered + amavis_info['conn_name'] = 'amavis' + else: + amavis_info = self.ask_db_info('amavis') + + amavis_conn = t.render(Context(amavis_info)) + else: + amavis_conn = None + + if parsed_args.domain: + allowed_host = parsed_args.domain[0] + else: + allowed_host = raw_input( + 'Under which domain do you want to deploy modoboa? ' + ) mod = __import__(parsed_args.name, globals(), locals(), ['settings']) tpl = self._render_template( @@ -122,17 +173,24 @@ class DeployCommand(Command): 'allowed_host': allowed_host } ) - fp = open("%s/settings.py" % path, "w") - fp.write(tpl) - fp.close() + with open("%s/settings.py" % path, "w") as fp: + fp.write(tpl) shutil.copyfile( "%s/urls.py.tpl" % self._templates_dir, "%s/urls.py" % path ) os.mkdir("%s/media" % path) if parsed_args.syncdb: + os.unlink("%s/settings.pyc" % path) self._exec_django_command( - "syncdb", parsed_args.name, '--migrate', '--noinput' + "syncdb", parsed_args.name, '--noinput' + ) + exec_cmd('sed -ri "s|^#(\s+\'south)|\\1|" %s/settings.py' % path) + self._exec_django_command( + "syncdb", parsed_args.name, + ) + self._exec_django_command( + 'migrate', parsed_args.name, '--fake' ) self._exec_django_command( "loaddata", parsed_args.name, 'initial_users.json' diff --git a/modoboa/core/commands/help.py b/modoboa/core/commands/help.py index cd1ada463..dfe46307c 100644 --- a/modoboa/core/commands/help.py +++ b/modoboa/core/commands/help.py @@ -1,6 +1,7 @@ # coding: utf-8 import sys -from modoboa.core.management import Command +from . import Command + class HelpCommand(Command): help = "Display the help message associated to a specific command" diff --git a/modoboa/core/commands/postfix_maps.py b/modoboa/core/commands/postfix_maps.py index f697db4e2..eda26f4fc 100644 --- a/modoboa/core/commands/postfix_maps.py +++ b/modoboa/core/commands/postfix_maps.py @@ -3,66 +3,114 @@ import getpass import inspect import os from django.template import Context, Template -from modoboa.core.management import Command +from . import Command -class MapFile(object): - category = 'std' +class MapFilesGenerator(object): def __init__(self): - self.base_tpl = Template("""user = {{ dbuser }} -password = {{ dbpassword }} + self.template = Template("""user = {{ dbuser }} +password = {{ dbpass }} dbname = {{ dbname }} hosts = {{ dbhost }} query = {{ query|safe }} """) + self.tpl_context = { + 'dbhost': raw_input('Database host: '), + 'dbname': raw_input('Database name: '), + 'dbuser': raw_input('Username: '), + 'dbpass': getpass.getpass('Password: ') + } + if not self.tpl_context['dbhost']: + self.tpl_context['dbhost'] = '127.0.0.1' - def render(self, destdir, dbtype, **options): + def render(self, args): + """Render all map files. + + :param args: arguments received from the command line + """ try: - options['query'] = getattr(self, dbtype) - except AttributeError: - return + os.mkdir(args.destdir) + except OSError: + pass - content = self.base_tpl.render(Context(options)) - fp = open("%s/%s" % (destdir, self.filename), "w") - fp.write(content) - fp.close() + for v in globals().values(): + if not inspect.isclass(v) or not issubclass(v, MapFile): + continue + if v is MapFile: + continue + if not v.category in args.categories: + continue + content = self.template.render( + Context(dict(self.tpl_context.items(), + query=getattr(v, args.dbtype))) + ) + with open("%s/%s" % (args.destdir, v.filename), "w") as fp: + print >> fp, content + + +class SQLiteMapFilesGenerator(MapFilesGenerator): + + def __init__(self): + self.template = Template("""dbpath = {{ dbpath }} +query = {{ query|safe }} +""") + self.tpl_context = { + 'dbpath': raw_input('Database path: ') + } + + +class MapFile(object): + category = 'std' class DomainsMap(MapFile): filename = 'sql-domains.cf' mysql = "SELECT name FROM admin_domain WHERE name='%s' AND enabled=1" postgres = "SELECT name FROM admin_domain WHERE name='%s' AND enabled" + sqlite = "SELECT name FROM admin_domain WHERE name='%s' AND enabled=1" class DomainsAliasesMap(MapFile): filename = 'sql-domain-aliases.cf' - mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND mb.address='%u') UNION (SELECT concat(al.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND al.address='%u')" - postgres = "(SELECT mb.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND mb.address='%u') UNION (SELECT al.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND al.address='%u')" + mysql = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled=1 AND dom.enabled=1" + postgres = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled AND dom.enabled" + sqlite = "SELECT dom.name FROM admin_domain dom INNER JOIN admin_domainalias domal ON dom.id=domal.target_id WHERE domal.name='%s' AND domal.enabled=1 AND dom.enabled=1" class MailboxesMap(MapFile): filename = 'sql-mailboxes.cf' mysql = "SELECT 1 FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_user user ON mb.user_id=user.id WHERE dom.enabled=1 AND dom.name='%d' AND user.is_active=1 AND mb.address='%u'" postgres = "SELECT 1 FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_user u ON mb.user_id=u.id WHERE dom.enabled AND dom.name='%d' AND u.is_active AND mb.address='%u'" + sqlite = "SELECT 1 FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_user user ON mb.user_id=user.id WHERE dom.enabled=1 AND dom.name='%d' AND user.is_active=1 AND mb.address='%u'" class AliasesMap(MapFile): filename = 'sql-aliases.cf' mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT concat(al.address, '@', dom.name) FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.id IN (SELECT al_al.to_alias_id FROM admin_alias_aliases al_al INNER JOIN admin_alias al ON al_al.from_alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1 AND al.extmboxes<>'')" postgres = "(SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled)) UNION (SELECT al.address || '@' || dom.name FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.id IN (SELECT al_al.to_alias_id FROM admin_alias_aliases al_al INNER JOIN admin_alias al ON al_al.from_alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled AND al.address='%u' AND al.enabled AND al.extmboxes<>'')" + sqlite = "(SELECT (mb.address || '@' || dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias_mboxes al_mb INNER JOIN admin_alias al ON al_mb.alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT (al.address || '@' || dom.name) FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.id IN (SELECT al_al.to_alias_id FROM admin_alias_aliases al_al INNER JOIN admin_alias al ON al_al.from_alias_id=al.id INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE dom.name='%d' AND dom.enabled=1 AND al.address='%u' AND al.enabled=1 AND al.extmboxes<>'')" class DomainAliasesMailboxesMap(MapFile): filename = 'sql-domain-aliases-mailboxes.cf' - mysql = "SELECT DISTINCT concat(mb.address, '@', dom.name) FROM admin_alias al INNER JOIN admin_domain dom ON dom.id=al.domain_id INNER JOIN admin_domainalias domal ON domal.target_id=dom.id INNER JOIN (admin_alias_mboxes almb, admin_mailbox mb) ON (almb.alias_id=al.id AND almb.mailbox_id=mb.id) WHERE domal.name='%d' AND domal.enabled=1 AND (al.address='%u' OR mb.address='%u')" - postgres = "SELECT DISTINCT mb.address || '@' || dom.name FROM admin_alias al INNER JOIN admin_domain dom ON dom.id=al.domain_id INNER JOIN admin_domainalias domal ON domal.target_id=dom.id INNER JOIN admin_alias_mboxes almb ON almb.alias_id=al.id INNER JOIN admin_mailbox mb ON almb.mailbox_id=mb.id WHERE domal.name='%d' AND domal.enabled AND (al.address='%u' OR mb.address='%u')" + mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND mb.address='%u') UNION (SELECT concat(al.address, '@', dom.name) FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND al.address='%u')" + postgres = "(SELECT mb.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND mb.address='%u') UNION (SELECT al.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled AND al.address='%u')" + sqlite = "(SELECT mb.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_mailbox mb ON mb.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND mb.address='%u') UNION (SELECT al.address || '@' || dom.name FROM admin_domainalias domal INNER JOIN admin_domain dom ON domal.target_id=dom.id INNER JOIN admin_alias al ON al.domain_id=dom.id WHERE domal.name='%d' AND dom.enabled=1 AND al.address='%u')" class CatchallAliasesMap(MapFile): filename = 'sql-catchall-aliases.cf' mysql = "(SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled=1 AND al.address='*' AND dom.name='%d' AND dom.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled='1' AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled=1)" postgres = "(SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled AND al.address='*' AND dom.name='%d' AND dom.enabled)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled)" + sqlite = "(SELECT (mb.address || '@' || dom.name) FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled=1 AND al.address='*' AND dom.name='%d' AND dom.enabled=1)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled='1' AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled=1)" + + +class MaintainMap(MapFile): + filename = 'sql-maintain.cf' + mysql = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" + postgres = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" + sqlite = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" class TransportMap(MapFile): @@ -70,6 +118,7 @@ class TransportMap(MapFile): filename = 'sql-transport.cf' mysql = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" postgres = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" + sqlite = "SELECT method FROM postfix_autoreply_transport WHERE domain='%s'" class AutoRepliesMap(MapFile): @@ -77,6 +126,7 @@ class AutoRepliesMap(MapFile): filename = 'sql-autoreplies.cf' mysql = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" postgres = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" + sqlite = "SELECT full_address, autoreply_address FROM postfix_autoreply_alias WHERE full_address='%s'" class PostfixMapsCommand(Command): @@ -84,30 +134,23 @@ class PostfixMapsCommand(Command): def __init__(self, *args, **kwargs): super(PostfixMapsCommand, self).__init__(*args, **kwargs) - self._parser.add_argument('destdir', type=str, - help='Directory that will contain generated map files') - self._parser.add_argument('--dbtype', type=str, choices=['mysql', 'postgres'], default='mysql', - help='Used database type') - self._parser.add_argument('--dbhost', type=str, default='127.0.0.1', - help='Database host address') - self._parser.add_argument('--categories', type=str, nargs='*', default=['std'], - help='Map file categories to generate (choices: std, autoreply)') + self._parser.add_argument( + 'destdir', type=str, + help='Directory that will contain generated map files' + ) + self._parser.add_argument( + '--dbtype', type=str, choices=['mysql', 'postgres', 'sqlite'], + default='mysql', + help='Used database type' + ) + self._parser.add_argument( + '--categories', type=str, nargs='*', default=['std'], + help='Map file categories to generate (choices: std, autoreply)' + ) def handle(self, parsed_args): - dbname = raw_input('Database name: ') - dbuser = raw_input('Username: ') - dbpass = getpass.getpass('Password: ') - - try: - os.mkdir(parsed_args.destdir) - except OSError: - pass - - for k, v in globals().iteritems(): - if not inspect.isclass(v) or not issubclass(v, MapFile): - continue - if not v.category in parsed_args.categories: - continue - v().render(parsed_args.destdir, parsed_args.dbtype, - dbname=dbname, dbuser=dbuser, dbpassword=dbpass, - dbhost=parsed_args.dbhost) + if parsed_args.dbtype == 'sqlite': + g = SQLiteMapFilesGenerator() + else: + g = MapFilesGenerator() + g.render(parsed_args) diff --git a/modoboa/core/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl similarity index 96% rename from modoboa/core/templates/settings.py.tpl rename to modoboa/core/commands/templates/settings.py.tpl index 4f926f712..f86ad1e39 100644 --- a/modoboa/core/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -141,10 +141,8 @@ INSTALLED_APPS = ( # 'django.contrib.admin', # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', - 'south', - - # Extra tools - # 'modoboa.tools.pfxadmin_migrate', +# 'south', + 'reversion', ) # A dedicated place to register Modoboa applications @@ -152,26 +150,23 @@ INSTALLED_APPS = ( # Do not change the order. MODOBOA_APPS = ( 'modoboa', - 'modoboa.auth', - 'modoboa.admin', + 'modoboa.core', 'modoboa.lib', - 'modoboa.userprefs', - + # Modoboa extensions here. + 'modoboa.extensions.admin', 'modoboa.extensions.limits', 'modoboa.extensions.postfix_autoreply', 'modoboa.extensions.webmail', 'modoboa.extensions.stats', - {% if not amavis_conn %}#{% endif %}'modoboa.extensions.amavis', 'modoboa.extensions.sievefilters', + {% if not amavis_conn %}#{% endif %}'modoboa.extensions.amavis', ) INSTALLED_APPS += MODOBOA_APPS -INSTALLED_APPS += ( - 'reversion', -) +AUTH_USER_MODEL = 'core.User' -AUTH_USER_MODEL = 'admin.User' +SOUTH_TESTS_MIGRATE = False #MODOBOA_CUSTOM_LOGO = os.path.join(MEDIA_URL, "custom_logo.png") @@ -209,7 +204,7 @@ LOGGING = { 'formatter': 'syslog' }, 'modoboa': { - 'class': 'modoboa.lib.logutils.SQLHandler', + 'class': 'modoboa.core.loggers.SQLHandler', } }, 'loggers': { diff --git a/modoboa/core/templates/urls.py.tpl b/modoboa/core/commands/templates/urls.py.tpl similarity index 100% rename from modoboa/core/templates/urls.py.tpl rename to modoboa/core/commands/templates/urls.py.tpl diff --git a/modoboa/admin/exceptions.py b/modoboa/core/exceptions.py similarity index 95% rename from modoboa/admin/exceptions.py rename to modoboa/core/exceptions.py index 9ee8ced0d..063db7ad9 100644 --- a/modoboa/admin/exceptions.py +++ b/modoboa/core/exceptions.py @@ -1,4 +1,3 @@ -# coding: utf-8 """ :mod:`exceptions` --- Custom exceptions of the admin. application ----------------------------------------------------------------- diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py new file mode 100644 index 000000000..4b399bd24 --- /dev/null +++ b/modoboa/core/extensions.py @@ -0,0 +1,93 @@ +from django.conf import settings +from django.conf.urls import include + + +class ModoExtension(object): + """Base extension class + + Each Modoboa extension must inherit from this class to be + considered as valid. + """ + name = None + label = None + version = "NA" + description = "" + url = None + needs_media = False + always_active = False + + def infos(self): + return dict( + name=self.name, label=self.label, version=self.version, + description=self.description, url=self.url, + always_active=self.always_active + ) + + def init(self): + pass + + def destroy(self): + pass + + def load(self): + pass + + +class ExtensionsPool(object): + """The extensions manager""" + + def __init__(self): + self.extensions = dict() + + def register_extension(self, ext, show=True): + self.extensions[ext.name] = dict(cls=ext, show=show) + + def get_extension(self, name): + if not name in self.extensions: + return None + if not "instance" in self.extensions[name]: + self.extensions[name]["instance"] = self.extensions[name]["cls"]() + return self.extensions[name]["instance"] + + def get_extension_infos(self, name): + instance = self.get_extension(name) + if instance is None: + return None + return instance.infos() + + def load_all(self): + from modoboa.core.models import Extension + + for ext in settings.MODOBOA_APPS: + __import__(ext) + result = [] + for extname in self.extensions.keys(): + extinstance = self.get_extension(extname) + if not extinstance.always_active: + try: + ext = Extension.objects.get(name=extname) + if not ext.enabled: + continue + except Extension.DoesNotExist: + continue + extinstance.load() + try: + baseurl = extinstance.url if extinstance.url is not None else extname + result += [(r'^%s/' % (baseurl), + include("%s.urls" % extinstance.__module__))] + except ImportError: + # No urls for this extension + pass + return result + + def list_all(self): + result = [] + for extname, extdef in self.extensions.iteritems(): + if not extdef["show"]: + continue + infos = extdef["instance"].infos() + infos["id"] = extname + result += [infos] + return sorted(result, key=lambda i: i["name"]) + +exts_pool = ExtensionsPool() diff --git a/modoboa/core/factories.py b/modoboa/core/factories.py new file mode 100644 index 000000000..df37f5a47 --- /dev/null +++ b/modoboa/core/factories.py @@ -0,0 +1,35 @@ +import factory +from django.contrib.auth.models import Group +from . import models + + +class PermissionFactory(factory.DjangoModelFactory): + ABSTRACT_FACTORY = True + + @factory.post_generation + def set_permission(self, create, extracted, **kwargs): + if not create: + return + self.post_create(models.User.objects.get(pk=1)) + + +class GroupFactory(factory.DjangoModelFactory): + FACTORY_FOR = Group + + name = 'DefaultGroup' + + +class UserFactory(PermissionFactory): + FACTORY_FOR = models.User + + email = factory.LazyAttribute(lambda a: a.username) + password = '{PLAIN}toto' + + @factory.post_generation + def groups(self, create, extracted, **kwargs): + if not create: + return + + if extracted: + for group in extracted: + self.groups.add(Group.objects.get(name=group)) diff --git a/modoboa/core/fixtures/initial_users.json b/modoboa/core/fixtures/initial_users.json new file mode 100644 index 000000000..bf60843ba --- /dev/null +++ b/modoboa/core/fixtures/initial_users.json @@ -0,0 +1 @@ +[{"pk": 1, "model": "core.user", "fields": {"username": "admin", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": false, "last_login": "2012-01-26 15:19:25", "groups": [], "user_permissions": [], "password": "{CRYPT}dTTsGDkA5ZHKg", "email": "", "date_joined": "2012-01-26 15:19:25"}},{"pk": 1, "model": "core.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["core", "user"], "object_id": 1}}] \ No newline at end of file diff --git a/modoboa/core/forms.py b/modoboa/core/forms.py new file mode 100644 index 000000000..4dc7d38ae --- /dev/null +++ b/modoboa/core/forms.py @@ -0,0 +1,71 @@ +# coding: utf-8 +from django import forms +from django.utils.translation import ugettext as _, ugettext_lazy +from modoboa.core.models import User +from modoboa.lib import parameters + + +class LoginForm(forms.Form): + username = forms.CharField( + label=ugettext_lazy("Username"), + widget=forms.TextInput(attrs={"class": "input-block-level"}) + ) + password = forms.CharField( + label=ugettext_lazy("Password"), + widget=forms.PasswordInput(attrs={"class": "input-block-level"}) + ) + rememberme = forms.BooleanField( + initial=False, + required=False + ) + + +class ProfileForm(forms.ModelForm): + oldpassword = forms.CharField( + label=ugettext_lazy("Old password"), required=False, + widget=forms.PasswordInput + ) + newpassword = forms.CharField( + label=ugettext_lazy("New password"), required=False, + widget=forms.PasswordInput + ) + confirmation = forms.CharField( + label=ugettext_lazy("Confirmation"), required=False, + widget=forms.PasswordInput + ) + + class Meta: + model = User + fields = ("first_name", "last_name") + + def __init__(self, update_password, *args, **kwargs): + super(ProfileForm, self).__init__(*args, **kwargs) + if not update_password: + del self.fields["oldpassword"] + del self.fields["newpassword"] + del self.fields["confirmation"] + + def clean_oldpassword(self): + if self.cleaned_data["oldpassword"] == "": + return self.cleaned_data["oldpassword"] + + if parameters.get_admin("AUTHENTICATION_TYPE") != "local": + return self.cleaned_data["oldpassword"] + + if not self.instance.check_password(self.cleaned_data["oldpassword"]): + raise forms.ValidationError(_("Old password mismatchs")) + return self.cleaned_data["oldpassword"] + + def clean_confirmation(self): + if self.cleaned_data["newpassword"] != self.cleaned_data["confirmation"]: + raise forms.ValidationError(_("Passwords mismatch")) + return self.cleaned_data["confirmation"] + + def save(self, commit=True): + user = super(ProfileForm, self).save(commit=False) + if commit: + if self.cleaned_data.has_key("confirmation") and \ + self.cleaned_data["confirmation"] != "": + user.set_password(self.cleaned_data["confirmation"], self.cleaned_data["oldpassword"]) + user.save() + return user diff --git a/modoboa/lib/logutils.py b/modoboa/core/loggers.py similarity index 100% rename from modoboa/lib/logutils.py rename to modoboa/core/loggers.py diff --git a/modoboa/core/management.py b/modoboa/core/management.py deleted file mode 100644 index 82db79f11..000000000 --- a/modoboa/core/management.py +++ /dev/null @@ -1,88 +0,0 @@ -# coding: utf-8 - -from django.conf import settings -from django.template import Context, Template -import argparse -import os -import sys - -class Command(object): - """Base command class - - A valid administrative command must inherit from this class. - """ - help = "No help available." - - def __init__(self, commands, verbose=False, **kwargs): - self._commands = commands - self._parser = argparse.ArgumentParser() - self._verbose = verbose - if not settings.configured: - settings.configure() - self._templates_dir = "%s/templates" % os.path.dirname(__file__) - - def _render_template(self, tplfile, env): - fp = open(tplfile) - t = Template(fp.read()) - fp.close() - return t.render(Context(env)) - - def run(self, cmdline): - args = self._parser.parse_args(cmdline) - self.handle(args) - - def handle(self, parsed_args): - """A command must overload this method to be called - - :param parsed_args: - """ - raise NotImplemented - -def scan_for_commands(dirname="commands"): - """Build a dictionnary containing all commands - - :param dirname: the directory where commands are located - :return: a dict of commands (name : class) - """ - path = os.path.join(os.path.dirname(__file__), dirname) - result = {} - for f in os.listdir(path): - if f in ['.', '..', '__init__.py']: - continue - if not f.endswith('.py'): - continue - if os.path.isfile(f): - continue - cmdname = f.replace('.py', '') - cmdmod = __import__("modoboa.core.commands", globals(), locals(), - [cmdname]) - cmdmod = getattr(cmdmod, cmdname) - if '_' in cmdname: - cmdclassname = ''.join(map(lambda s: s.capitalize(), cmdname.split('_'))) - else: - cmdclassname = cmdname.capitalize() - try: - cmdclass = getattr(cmdmod, "%sCommand" % cmdclassname) - except AttributeError: - continue - result[cmdname] = cmdclass - return result - -def handle_command_line(): - commands = scan_for_commands() - parser = argparse.ArgumentParser( - description="A set of utilities to ease the installation of Modoboa.", - epilog="""Available commands: -%s -""" % "\n".join(map(lambda c: "\t" + c, sorted(commands)))) - parser.add_argument('--verbose', action='store_true', - help='Activate verbose output') - parser.add_argument('command', type=str, - help='A valid command name') - (args, remaining) = parser.parse_known_args() - - if not args.command in commands: - print >>sys.stderr, "Unknown command '%s'" % args.command - sys.exit(1) - - commands[args.command](commands, verbose=args.verbose).run(remaining) diff --git a/modoboa/admin/migrations/__init__.py b/modoboa/core/management/__init__.py similarity index 100% rename from modoboa/admin/migrations/__init__.py rename to modoboa/core/management/__init__.py diff --git a/modoboa/admin/templatetags/__init__.py b/modoboa/core/management/commands/__init__.py similarity index 100% rename from modoboa/admin/templatetags/__init__.py rename to modoboa/core/management/commands/__init__.py diff --git a/modoboa/lib/management/commands/cleanlogs.py b/modoboa/core/management/commands/cleanlogs.py similarity index 91% rename from modoboa/lib/management/commands/cleanlogs.py rename to modoboa/core/management/commands/cleanlogs.py index e25ba7604..3da28798f 100644 --- a/modoboa/lib/management/commands/cleanlogs.py +++ b/modoboa/core/management/commands/cleanlogs.py @@ -1,13 +1,12 @@ #!/usr/bin/env python # coding: utf-8 - import datetime from optparse import make_option from django.core.management.base import BaseCommand from django.utils import timezone +from modoboa.core import load_settings +from modoboa.core.models import Log from modoboa.lib import parameters -from modoboa.lib.models import Log -from modoboa.admin import AdminConsole class Command(BaseCommand): @@ -38,8 +37,8 @@ class Command(BaseCommand): l.addHandler(logging.StreamHandler()) self.verbose = options["verbose"] - AdminConsole().load() - log_maximum_age = int(parameters.get_admin("LOG_MAXIMUM_AGE", app="admin")) + load_settings() + log_maximum_age = int(parameters.get_admin("LOG_MAXIMUM_AGE")) self.__vprint("Deleting logs older than %d days..." % log_maximum_age) limit = timezone.now() - datetime.timedelta(log_maximum_age) Log.objects.filter(date_created__lt=limit).delete() diff --git a/modoboa/core/migrations/0001_initial.py b/modoboa/core/migrations/0001_initial.py new file mode 100644 index 000000000..4b0bbe9ed --- /dev/null +++ b/modoboa/core/migrations/0001_initial.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'User' + db.create_table(u'core_user', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('password', self.gf('django.db.models.fields.CharField')(max_length=128)), + ('last_login', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + ('is_superuser', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('username', self.gf('django.db.models.fields.CharField')(unique=True, max_length=254)), + ('first_name', self.gf('django.db.models.fields.CharField')(max_length=30, blank=True)), + ('last_name', self.gf('django.db.models.fields.CharField')(max_length=30, blank=True)), + ('email', self.gf('django.db.models.fields.EmailField')(max_length=254, blank=True)), + ('is_staff', self.gf('django.db.models.fields.BooleanField')(default=False)), + ('is_active', self.gf('django.db.models.fields.BooleanField')(default=True)), + ('date_joined', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)), + ('is_local', self.gf('django.db.models.fields.BooleanField')(default=True)), + )) + db.send_create_signal(u'core', ['User']) + + # Adding M2M table for field groups on 'User' + db.create_table(u'core_user_groups', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('user', models.ForeignKey(orm[u'core.user'], null=False)), + ('group', models.ForeignKey(orm[u'auth.group'], null=False)) + )) + db.create_unique(u'core_user_groups', ['user_id', 'group_id']) + + # Adding M2M table for field user_permissions on 'User' + db.create_table(u'core_user_user_permissions', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('user', models.ForeignKey(orm[u'core.user'], null=False)), + ('permission', models.ForeignKey(orm[u'auth.permission'], null=False)) + )) + db.create_unique(u'core_user_user_permissions', ['user_id', 'permission_id']) + + # Adding model 'ObjectAccess' + db.create_table(u'core_objectaccess', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['core.User'])), + ('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])), + ('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()), + ('is_owner', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal(u'core', ['ObjectAccess']) + + # Adding unique constraint on 'ObjectAccess', fields ['user', 'content_type', 'object_id'] + db.create_unique(u'core_objectaccess', ['user_id', 'content_type_id', 'object_id']) + + # Adding model 'Extension' + db.create_table(u'core_extension', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.CharField')(max_length=150)), + ('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)), + )) + db.send_create_signal(u'core', ['Extension']) + + # Adding model 'Log' + db.create_table(u'core_log', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), + ('message', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('level', self.gf('django.db.models.fields.CharField')(max_length=15)), + ('logger', self.gf('django.db.models.fields.CharField')(max_length=30)), + )) + db.send_create_signal(u'core', ['Log']) + + + def backwards(self, orm): + # Removing unique constraint on 'ObjectAccess', fields ['user', 'content_type', 'object_id'] + db.delete_unique(u'core_objectaccess', ['user_id', 'content_type_id', 'object_id']) + + # Deleting model 'User' + db.delete_table(u'core_user') + + # Removing M2M table for field groups on 'User' + db.delete_table('core_user_groups') + + # Removing M2M table for field user_permissions on 'User' + db.delete_table('core_user_user_permissions') + + # Deleting model 'ObjectAccess' + db.delete_table(u'core_objectaccess') + + # Deleting model 'Extension' + db.delete_table(u'core_extension') + + # Deleting model 'Log' + db.delete_table(u'core_log') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'core.extension': { + 'Meta': {'object_name': 'Extension'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + u'core.log': { + 'Meta': {'object_name': 'Log'}, + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '15'}), + 'logger': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + u'core.objectaccess': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) + }, + u'core.user': { + 'Meta': {'ordering': "['username']", 'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_local': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'}) + } + } + + complete_apps = ['core'] \ No newline at end of file diff --git a/modoboa/core/migrations/0002_update_permissions.py b/modoboa/core/migrations/0002_update_permissions.py new file mode 100644 index 000000000..31bc46068 --- /dev/null +++ b/modoboa/core/migrations/0002_update_permissions.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models + +class Migration(DataMigration): + depends_on = ( + ('admin', '0037_rename_tables'), + ) + + def forwards(self, orm): + "Write your forwards methods here." + orm['contenttypes.ContentType'].objects.filter( + app_label='admin', model__in=['user', 'extension', 'objectaccess'] + ).update(app_label='core') + + def backwards(self, orm): + "Write your backwards methods here." + orm['contenttypes.ContentType'].objects.filter( + app_label='core', model__in=['user', 'extension', 'objectaccess'] + ).update(app_label='admin') + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'core.extension': { + 'Meta': {'object_name': 'Extension'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + u'core.log': { + 'Meta': {'object_name': 'Log'}, + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'level': ('django.db.models.fields.CharField', [], {'max_length': '15'}), + 'logger': ('django.db.models.fields.CharField', [], {'max_length': '30'}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '255'}) + }, + u'core.objectaccess': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"}) + }, + u'core.user': { + 'Meta': {'ordering': "['username']", 'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_local': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'}) + }, + u'reversion.revision': { + 'Meta': {'object_name': 'Revision'}, + 'comment': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'manager_slug': ('django.db.models.fields.CharField', [], {'default': "u'default'", 'max_length': '200', 'db_index': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']", 'null': 'True', 'blank': 'True'}) + }, + u'reversion.version': { + 'Meta': {'object_name': 'Version'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + 'format': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'object_id': ('django.db.models.fields.TextField', [], {}), + 'object_id_int': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}), + 'object_repr': ('django.db.models.fields.TextField', [], {}), + 'revision': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['reversion.Revision']"}), + 'serialized_data': ('django.db.models.fields.TextField', [], {}), + 'type': ('django.db.models.fields.PositiveSmallIntegerField', [], {'db_index': 'True'}) + } + } + + complete_apps = ['reversion', 'core'] + symmetrical = True diff --git a/modoboa/auth/__init__.py b/modoboa/core/migrations/__init__.py old mode 100755 new mode 100644 similarity index 100% rename from modoboa/auth/__init__.py rename to modoboa/core/migrations/__init__.py diff --git a/modoboa/core/models.py b/modoboa/core/models.py new file mode 100644 index 000000000..4af548ad3 --- /dev/null +++ b/modoboa/core/models.py @@ -0,0 +1,483 @@ +import sys +import os +import re +import hashlib +import crypt +import base64 +from random import Random +import reversion +from django.db import models +from django.dispatch import receiver +from django.utils import timezone +from django.utils.translation import ugettext as _, ugettext_lazy +from django.utils.crypto import constant_time_compare +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +from django.contrib.auth.models import ( + UserManager, Group, AbstractBaseUser, PermissionsMixin +) +from modoboa.lib import events, md5crypt, parameters +from modoboa.lib.exceptions import PermDeniedException +from modoboa.lib.sysutils import exec_cmd +from modoboa.core.extensions import exts_pool +from modoboa.core.exceptions import AdminError + +try: + from modoboa.lib.ldaputils import * + ldap_available = True +except ImportError: + ldap_available = False + + +class User(AbstractBaseUser, PermissionsMixin): + """Custom User model. + + It overloads the way passwords are stored into the database. The + main reason to change this mechanism is to ensure the + compatibility with the way Dovecot stores passwords. + + It also adds new attributes and methods. + """ + username = models.CharField(max_length=254, unique=True) + first_name = models.CharField(max_length=30, blank=True) + last_name = models.CharField(max_length=30, blank=True) + email = models.EmailField(max_length=254, blank=True) + is_staff = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + date_joined = models.DateTimeField(default=timezone.now) + is_local = models.BooleanField(default=True) + + objects = UserManager() + + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['email'] + + class Meta: + ordering = ["username"] + + password_expr = re.compile(r'(\{(\w+)\}|(\$1\$))(.+)') + + def delete(self, fromuser, *args, **kwargs): + """Custom delete method + + To check permissions properly, we need to make a distinction + between 2 cases: + + * If the user owns a mailbox, the check is made on that object + (useful for domain admins) + + * Otherwise, the check is made on the user + """ + from modoboa.lib.permissions import \ + get_object_owner, grant_access_to_object, ungrant_access_to_object + + if fromuser == self: + raise AdminError(_("You can't delete your own account")) + + if not fromuser.can_access(self): + raise PermDeniedException + + owner = get_object_owner(self) + for ooentry in self.objectaccess_set.filter(is_owner=True): + if ooentry.content_object is not None: + grant_access_to_object(owner, ooentry.content_object, True) + + events.raiseEvent("AccountDeleted", self, fromuser, **kwargs) + ungrant_access_to_object(self) + super(User, self).delete() + + def _crypt_password(self, raw_value): + scheme = parameters.get_admin("PASSWORD_SCHEME") + if type(raw_value) is unicode: + raw_value = raw_value.encode("utf-8") + if scheme == "crypt": + salt = "".join(Random().sample(string.letters + string.digits, 2)) + result = crypt.crypt(raw_value, salt) + prefix = "{CRYPT}" + elif scheme == "md5": + obj = hashlib.md5(raw_value) + result = obj.hexdigest() + prefix = "{MD5}" + # The md5crypt scheme is the only supported method that has both: + # (a) a salt ("crypt" has this too), + # (b) supports passwords lengths of more than 8 characters (all except + # "crypt"). + elif scheme == "md5crypt": + # The salt may vary from 12 to 48 bits. (Using all six bytes here + # with a subset of characters means we get only 35 random bits.) + salt = "".join(Random().sample(string.letters + string.digits, 6)) + result = md5crypt(raw_value, salt) + prefix = "" # the result already has $1$ prepended to it + # to signify what this is + elif scheme == "sha256": + obj = hashlib.sha256(raw_value) + result = base64.b64encode(obj.digest()) + prefix = "{SHA256}" + else: + scheme = "plain" + result = raw_value + prefix = "{PLAIN}" + return "%s%s" % (prefix, result) + + def set_password(self, raw_value, curvalue=None): + """Password update + + Update the current mailbox's password with the given clear + value. This value is encrypted according to the defined method + before it is saved. + + :param raw_value: the new password's value + :param curvalue: the current password (for LDAP authentication) + """ + if parameters.get_admin("AUTHENTICATION_TYPE") == "local": + self.password = self._crypt_password(raw_value) + else: + if not ldap_available: + raise AdminError( + _("Failed to update password: LDAP module not installed") + ) + + ab = LDAPAuthBackend() + try: + ab.update_user_password(self.username, curvalue, raw_value) + except LDAPException, e: + raise AdminError(_("Failed to update password: %s" % str(e))) + events.raiseEvent( + "PasswordUpdated", self, raw_value, self.pk is None + ) + + def check_password(self, raw_value): + m = self.password_expr.match(self.password) + if m is None: + return False + if type(raw_value) is unicode: + raw_value = raw_value.encode("utf-8") + scheme = (m.group(2) or m.group(3)).lower() + val2 = m.group(4) + if scheme == u"crypt": + val1 = crypt.crypt(raw_value, val2) + elif scheme == u"md5": + val1 = hashlib.md5(raw_value).hexdigest() + elif scheme == u"sha256": + val1 = base64.b64encode(hashlib.sha256(raw_value).digest()) + elif scheme == u"$1$": # md5crypt + salt, hashed = val2.split('$') + val1 = md5crypt(raw_value, str(salt)) + val2 = self.password # re-add scheme for comparison below + else: + val1 = raw_value + return constant_time_compare(val1, val2) + + @property + def tags(self): + return [{"name": "account", "label": _("account"), "type": "idt"}, + {"name": self.group, "label": self.group, + "type": "grp", "color": "info"}] + + @property + def fullname(self): + if self.first_name != u"": + return u"%s %s" % (self.first_name, self.last_name) + return self.username + + @property + def identity(self): + return self.username + + @property + def name_or_rcpt(self): + if self.first_name != "": + return "%s %s" % (self.first_name, self.last_name) + return "----" + + @property + def group(self): + if self.is_superuser: + return "SuperAdmins" + try: + return self.groups.all()[0].name + except IndexError: + return "SimpleUsers" + + @property + def enabled(self): + return self.is_active + + @property + def encoded_address(self): + from email.header import Header + if self.first_name != "" or self.last_name != "": + return "%s <%s>" % \ + (Header(self.fullname, 'utf8').encode(), self.email) + return self.email + + def belongs_to_group(self, name): + """Simple shortcut to check if this user is a member of a + specific group. + + :param name: the group's name + :return: a boolean + """ + try: + self.groups.get(name=name) + except Group.DoesNotExist: + return False + return True + + def is_owner(self, obj): + """Tell is the user is the unique owner of this object + + :param obj: an object inheriting from ``models.Model`` + :return: a boolean + """ + ct = ContentType.objects.get_for_model(obj) + try: + ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) + except ObjectAccess.DoesNotExist: + return False + return ooentry.is_owner + + def can_access(self, obj): + """Check if the user can access a specific object + + This function is recursive : if the given user hasn't got direct + access to this object and if he has got access other ``User`` + objects, we check if one of those users owns the object. + + :param obj: a admin object + :return: a boolean + """ + if self.is_superuser: + return True + + ct = ContentType.objects.get_for_model(obj) + try: + ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) + except ObjectAccess.DoesNotExist: + pass + else: + return True + if ct.model == "user": + return False + + ct = ContentType.objects.get_for_model(self) + qs = self.objectaccess_set.filter(content_type=ct) + for ooentry in qs.all(): + if ooentry.content_object.is_owner(obj): + return True + return False + + def set_role(self, role): + """Set administrative role for this account + + :param string role: the role to set + """ + if role is None or self.group == role: + return + events.raiseEvent("RoleChanged", self, role) + self.groups.clear() + if role == "SuperAdmins": + self.is_superuser = True + else: + if self.is_superuser: + ObjectAccess.objects.filter(user=self).delete() + self.is_superuser = False + try: + self.groups.add(Group.objects.get(name=role)) + except Group.DoesNotExist: + self.groups.add(Group.objects.get(name="SimpleUsers")) + if self.group != "SimpleUsers" and not self.can_access(self): + from modoboa.lib.permissions import grant_access_to_object + grant_access_to_object(self, self) + self.save() + + def post_create(self, creator): + from modoboa.lib.permissions import grant_access_to_object + grant_access_to_object(creator, self, is_owner=True) + events.raiseEvent("AccountCreated", self) + + def save(self, *args, **kwargs): + if "creator" in kwargs: + creator = kwargs["creator"] + del kwargs["creator"] + else: + creator = None + super(User, self).save(*args, **kwargs) + if creator is not None: + self.post_create(creator) + + def from_csv(self, user, row, crypt_password=True): + """Create a new account from a CSV file entry + + The expected order is the following:: + + "account", loginname, password, first name, last name, enabled, group, address[, domain, ...] + + :param user: a ``core.User`` instance + :param row: a list containing the expected information + :param crypt_password: + """ + if len(row) < 7: + raise AdminError(_("Invalid line")) + role = row[6].strip() + if not user.is_superuser and not role in ["SimpleUsers", "DomainAdmins"]: + raise PermDeniedException( + _("You can't import an account with a role greater than yours") + ) + self.username = row[1].strip() + if role == "SimpleUsers": + if (len(row) < 8 or not row[7].strip()): + raise AdminError( + _("The simple user '%s' must have a valid email address" % self.username) + ) + if self.username != row[7].strip(): + raise AdminError( + _("username and email fields must not differ for '%s'" % self.username) + ) + + if crypt_password: + self.set_password(row[2].strip()) + else: + self.password = row[2].strip() + self.first_name = row[3].strip() + self.last_name = row[4].strip() + self.is_active = (row[5].strip() == 'True') + self.save(creator=user) + self.set_role(role) + if len(row) < 8: + return + events.raiseEvent("AccountImported", user, self, row[7:]) + + def to_csv(self, csvwriter): + row = ["account", self.username.encode("utf-8"), self.password.encode("utf-8"), + self.first_name.encode("utf-8"), self.last_name.encode("utf-8"), + self.is_active, self.group, self.email.encode("utf-8")] + row += events.raiseQueryEvent("AccountExported", self) + csvwriter.writerow(row) + +reversion.register(User) + + +def populate_callback(user): + """Populate callback + + If the LDAP authentication backend is in use, this callback will + be called each time a new user authenticates succesfuly to + Modoboa. This function is in charge of creating the mailbox + associated to the provided ``User`` object. + + :param user: a ``User`` instance + """ + from modoboa.lib.permissions import grant_access_to_object + + sadmins = User.objects.filter(is_superuser=True) + user.set_role("SimpleUsers") + user.post_create(sadmins[0]) + for su in sadmins[1:]: + grant_access_to_object(su, user) + events.raiseEvent("AccountAutoCreated", user) + + +class ObjectAccess(models.Model): + user = models.ForeignKey(User) + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + is_owner = models.BooleanField(default=False) + + class Meta: + unique_together = (("user", "content_type", "object_id"),) + + def __unicode__(self): + return "%s => %s (%s)" % (self.user, self.content_object, self.content_type) + + +class Extension(models.Model): + name = models.CharField(max_length=150) + enabled = models.BooleanField( + ugettext_lazy('enabled'), + help_text=ugettext_lazy("Check to enable this extension") + ) + + def __init__(self, *args, **kwargs): + super(Extension, self).__init__(*args, **kwargs) + + def __unicode__(self): + return self.name + + def __get_ext_instance(self): + if not self.name: + return None + if hasattr(self, "instance") and self.instance: + return + self.instance = exts_pool.get_extension(self.name) + if self.instance: + self.__get_ext_dir() + + def __get_ext_dir(self): + modname = self.instance.__module__ + path = os.path.realpath(sys.modules[modname].__file__) + self.extdir = os.path.dirname(path) + + def on(self): + self.enabled = True + self.save() + + self.__get_ext_instance() + self.instance.load() + self.instance.init() + + if self.instance.needs_media: + path = os.path.join(settings.MEDIA_ROOT, self.name) + exec_cmd("mkdir %s" % path) + + events.raiseEvent("ExtEnabled", self) + + def off(self): + self.__get_ext_instance() + if self.instance is None: + return + self.instance.destroy() + + self.enabled = False + self.save() + + if self.instance.needs_media: + path = os.path.join(settings.MEDIA_ROOT, self.name) + exec_cmd("rm -r %s" % path) + + events.raiseEvent("ExtDisabled", self) + +reversion.register(Extension) + + +class Log(models.Model): + date_created = models.DateTimeField(auto_now_add=True) + message = models.CharField(max_length=255) + level = models.CharField(max_length=15) + logger = models.CharField(max_length=30) + + +@receiver(reversion.post_revision_commit) +def post_revision_commit(sender, **kwargs): + import logging + + if kwargs["revision"].user is None: + return + logger = logging.getLogger("modoboa.admin") + for version in kwargs["versions"]: + if version.type == reversion.models.VERSION_ADD: + action = _("added") + level = "info" + elif version.type == reversion.models.VERSION_CHANGE: + action = _("modified") + level = "warning" + else: + action = _("deleted") + level = "critical" + message = _("%(object)s '%(name)s' %(action)s by user %(user)s") % { + "object": unicode(version.content_type).capitalize(), + "name": version.object_repr, "action": action, + "user": kwargs["revision"].user.username + } + getattr(logger, level)(message) diff --git a/modoboa/userprefs/static/userprefs/css/userprefs.css b/modoboa/core/static/core/css/userprefs.css similarity index 100% rename from modoboa/userprefs/static/userprefs/css/userprefs.css rename to modoboa/core/static/core/css/userprefs.css diff --git a/modoboa/core/tables.py b/modoboa/core/tables.py new file mode 100644 index 000000000..e35868cab --- /dev/null +++ b/modoboa/core/tables.py @@ -0,0 +1,14 @@ +from django.utils.translation import ugettext_lazy +from modoboa.lib import tables + + +class ExtensionsTable(tables.Table): + idkey = "id" + selection = tables.SelectionColumn("selection", width="4%", header=False) + label = tables.Column("label", label=ugettext_lazy("Name"), width="15%") + version = tables.Column( + "version", label=ugettext_lazy("Version"), width="6%" + ) + descr = tables.Column("description", label=ugettext_lazy("Description")) + + cols_order = ["selection", "label", "version", "descr"] diff --git a/modoboa/admin/templates/admin/extensions.html b/modoboa/core/templates/core/extensions.html similarity index 68% rename from modoboa/admin/templates/admin/extensions.html rename to modoboa/core/templates/core/extensions.html index a419d20ad..276524756 100644 --- a/modoboa/admin/templates/admin/extensions.html +++ b/modoboa/core/templates/core/extensions.html @@ -1,6 +1,6 @@ {% load i18n %}{% load url from future %}

{% trans "Extensions" %} {% trans "Choose the extension(s) to use" %}


-
{% csrf_token %} +{% csrf_token %} {{ extensions|safe }}
{% trans 'Save' %} diff --git a/modoboa/admin/templates/admin/information.html b/modoboa/core/templates/core/information.html similarity index 94% rename from modoboa/admin/templates/admin/information.html rename to modoboa/core/templates/core/information.html index 80f7500a3..f74c13139 100644 --- a/modoboa/admin/templates/admin/information.html +++ b/modoboa/core/templates/core/information.html @@ -1,4 +1,4 @@ -{% load i18n libextras %} +{% load i18n core_tags lib_tags %}

{% trans "Information" %} {% trans "Various information about Modoboa" %}


Modoboa {% get_version %} {% trans "is currently in use." %}
diff --git a/modoboa/admin/templates/admin/logs.html b/modoboa/core/templates/core/logs.html similarity index 96% rename from modoboa/admin/templates/admin/logs.html rename to modoboa/core/templates/core/logs.html index b77458a89..741e9e6eb 100644 --- a/modoboa/admin/templates/admin/logs.html +++ b/modoboa/core/templates/core/logs.html @@ -1,4 +1,4 @@ -{% load i18n admin_extras %} +{% load i18n core_tags %}

{% trans "Logs" %} {% trans "Previously on Modoboa..." %}


diff --git a/modoboa/core/templates/core/parameters.html b/modoboa/core/templates/core/parameters.html new file mode 100644 index 000000000..7a8130925 --- /dev/null +++ b/modoboa/core/templates/core/parameters.html @@ -0,0 +1,7 @@ +{% load i18n lib_tags %}{% load url from future %} +

{% trans "Parameters" %} {% trans "Configure Modoboa" %}


+{% csrf_token %} + {% include "common/parameters.html" %} + +
+{% trans 'Save' %} diff --git a/modoboa/admin/templates/admin/settings_header.html b/modoboa/core/templates/core/settings_header.html similarity index 86% rename from modoboa/admin/templates/admin/settings_header.html rename to modoboa/core/templates/core/settings_header.html index 9a957a28b..590ad50da 100644 --- a/modoboa/admin/templates/admin/settings_header.html +++ b/modoboa/core/templates/core/settings_header.html @@ -1,5 +1,5 @@ {% extends "twocols.html" %} -{% load i18n libextras admin_extras %} +{% load i18n lib_tags core_tags %} {% block pagetitle %}{% trans "Modoboa settings" %}{% endblock %} {% block html_head %} @@ -19,5 +19,5 @@ $(document).ready(function() { }); {% endblock %} -{% block leftcol %}{% endblock %} +{% block leftcol %}{% endblock %} {% block apparea %}
{% endblock %} diff --git a/modoboa/userprefs/templates/userprefs/index.html b/modoboa/core/templates/core/user_index.html similarity index 82% rename from modoboa/userprefs/templates/userprefs/index.html rename to modoboa/core/templates/core/user_index.html index 6a240e0c9..9128e65b6 100644 --- a/modoboa/userprefs/templates/userprefs/index.html +++ b/modoboa/core/templates/core/user_index.html @@ -1,7 +1,7 @@ -{% extends "twocols.html" %}{% load i18n uprefs_extras libextras %} +{% extends "twocols.html" %}{% load i18n core_tags lib_tags %} {% block pagetitle %}{% trans "Settings" %}{% endblock %} {% block html_head %} - + diff --git a/modoboa/userprefs/templates/userprefs/preferences.html b/modoboa/core/templates/core/user_preferences.html similarity index 91% rename from modoboa/userprefs/templates/userprefs/preferences.html rename to modoboa/core/templates/core/user_preferences.html index 26a514dd7..c63f285c0 100644 --- a/modoboa/userprefs/templates/userprefs/preferences.html +++ b/modoboa/core/templates/core/user_preferences.html @@ -1,7 +1,7 @@ {% load i18n %}{% load url from future %}

{% trans "Preferences" %} {% trans "Customize Modoboa" %}


-{% csrf_token %} {% include "common/parameters.html" %}
diff --git a/modoboa/userprefs/templates/userprefs/profile.html b/modoboa/core/templates/core/user_profile.html similarity index 81% rename from modoboa/userprefs/templates/userprefs/profile.html rename to modoboa/core/templates/core/user_profile.html index f5df0b64e..0572e45e5 100644 --- a/modoboa/userprefs/templates/userprefs/profile.html +++ b/modoboa/core/templates/core/user_profile.html @@ -1,7 +1,7 @@ -{% load i18n libextras %}{% load url from future %} +{% load i18n lib_tags %}{% load url from future %}

{% trans "Profile" %} {% trans "Update your personal information" %}


-{% csrf_token %} {% render_form form %}
diff --git a/modoboa/lib/management/__init__.py b/modoboa/core/templatetags/__init__.py similarity index 100% rename from modoboa/lib/management/__init__.py rename to modoboa/core/templatetags/__init__.py diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py new file mode 100644 index 000000000..91238b489 --- /dev/null +++ b/modoboa/core/templatetags/core_tags.py @@ -0,0 +1,280 @@ +import re +from django import template +from django.template.loader import render_to_string +from django.utils import timezone +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +from django.contrib.sessions.models import Session +from modoboa.lib import events + + +register = template.Library() + + +@register.simple_tag +def core_menu(selection, user): + entries = \ + events.raiseQueryEvent("AdminMenuDisplay", "top_menu", user) + if user.is_superuser: + entries += [ + {"name": "settings", + "label": _("Modoboa"), + "url": reverse("modoboa.core.views.admin.viewsettings")} + ] + + if not len(entries): + return "" + return render_to_string("common/menulist.html", { + "entries": entries, + "selection": selection, + "user": user} + ) + + +@register.simple_tag +def settings_menu(selection, user): + entries = [ + {"name": "extensions", + "class": "ajaxlink", + "url": "extensions/", + "label": _("Extensions"), + "img": ""}, + {"name": "info", + "class": "ajaxlink", + "url": "info/", + "label": _("Information")}, + {"name": "logs", + "class": "ajaxlink", + "url": "logs/", + "label": _("Logs")}, + {"name": "parameters", + "class": "ajaxlink", + "url": "parameters/", + "img": "", + "label": _("Parameters")}, + ] + return render_to_string('common/menu.html', { + "entries": entries, + "css": "nav nav-list", + "selection": selection, + "user": user + }) + + +@register.simple_tag +def extensions_menu(selection, user): + menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu", user) + return render_to_string('common/menulist.html', { + "selection": selection, "entries": menu, "user": user + }) + + +@register.simple_tag +def admin_menu(selection, user): + entries = [ + {"name": "extensions", + "class": "ajaxlink", + "url": "extensions/", + "label": _("Extensions"), + "img": ""}, + {"name": "info", + "class": "ajaxlink", + "url": "info/", + "label": _("Information")}, + {"name": "logs", + "class": "ajaxlink", + "url": "logs/", + "label": _("Logs")}, + {"name": "parameters", + "class": "ajaxlink", + "url": "parameters/", + "img": "", + "label": _("Parameters")}, + ] + return render_to_string('common/menu.html', { + "entries": entries, + "css": "nav nav-list", + "selection": selection, + "user": user + }) + + +@register.simple_tag +def user_menu(user, selection): + entries = [ + {"name": "user", + "img": "icon-user icon-white", + "label": user.fullname, + "menu": [ + {"name": "settings", + "img": "icon-list", + "label": _("Settings"), + "url": reverse("modoboa.core.views.user.index")} + ]} + ] + + entries[0]["menu"] += \ + events.raiseQueryEvent("UserMenuDisplay", "options_menu", user) \ + + [{"name": "logout", + "url": reverse("modoboa.core.views.auth.dologout"), + "label": _("Logout"), + "img": "icon-off"}] + + return render_to_string("common/menulist.html", { + "selection": selection, "entries": entries, "user": user + }) + + +@register.simple_tag +def uprefs_menu(selection, user): + entries = [ + {"name": "profile", + "class": "ajaxlink", + "url": "profile/", + "label": _("Profile")}, + {"name": "preferences", + "class": "ajaxlink", + "url": "preferences/", + "label": _("Preferences")}, + ] + entries += events.raiseQueryEvent("UserMenuDisplay", "uprefs_menu", user) + entries = sorted(entries, key=lambda e: e["label"]) + return render_to_string('common/menu.html', { + "entries": entries, + "css": "nav nav-list", + "selection": selection, + "user": user + }) + + +@register.filter +def colorize_level(level): + classes = { + "INFO": "text-info", + "WARNING": "text-warning", + "CRITICAL": "text-error" + } + if not level in classes: + return level + return "

%s

" % (classes[level], level) + + +@register.filter +def tohtml(message): + return re.sub("'(.*?)'", "\g<1>", message) + + +@register.simple_tag +def visirule(field): + if not hasattr(field.form, "visirules") or not field.html_name in field.form.visirules: + return "" + rule = field.form.visirules[field.html_name] + return " data-visibility-field='%s' data-visibility-value='%s' " \ + % (rule["field"], rule["value"]) + + +@register.simple_tag +def get_version(): + import pkg_resources + return pkg_resources.get_distribution("modoboa").version + + +class ConnectedUsers(template.Node): + def __init__(self, varname): + self.varname = varname + + def render(self, context): + from modoboa.core.models import User + + sessions = Session.objects.filter(expire_date__gte=timezone.now()) + uid_list = [] + # Build a list of user ids from that query + for session in sessions: + data = session.get_decoded() + uid = data.get('_auth_user_id', None) + if uid: + uid_list.append(uid) + + # Query all logged in users based on id list + context[self.varname] = [] + for uid in uid_list: + try: + context[self.varname].append(User.objects.get(pk=uid)) + except User.DoesNotExist: + pass + return '' + + +@register.tag +def connected_users(parser, token): + try: + tag, a, varname = token.split_contents() + except ValueError: + raise template.TemplateSyntaxError( + 'connected_users usage: {% connected_users as users %}' + ) + return ConnectedUsers(varname) + + +@register.simple_tag +def get_modoboa_logo(): + import os + from django.conf import settings + + try: + logo = settings.MODOBOA_CUSTOM_LOGO + except AttributeError: + logo = None + if logo is None: + return os.path.join(settings.STATIC_URL, "css/modoboa.png") + return logo + + +@register.simple_tag +def extra_head_content(user): + tpl = template.Template( + "{% for sc in static_content %}{{ sc|safe }}{% endfor %}" + ) + return tpl.render( + template.Context({ + 'static_content': events.raiseQueryEvent("GetStaticContent", user) + }) + ) + + +@register.simple_tag +def load_optionalmenu(user): + menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu_middle", user) + return template.loader.render_to_string( + 'common/menulist.html', + {"entries": menu, "user": user} + ) + + +@register.simple_tag +def load_notifications(user): + content = events.raiseQueryEvent("TopNotifications", user) + return "".join(content) + + +@register.simple_tag +def display_messages(msgs): + text = "" + level = "info" + for m in msgs: + level = m.tags + text += unicode(m) + "\\\n" + + if level == "info": + level = "success" + timeout = "2000" + else: + timeout = "undefined" + + return """ + +""" % (level, text, timeout) diff --git a/modoboa/core/tests.py b/modoboa/core/tests.py new file mode 100644 index 000000000..5b3491ba2 --- /dev/null +++ b/modoboa/core/tests.py @@ -0,0 +1,42 @@ +from django.core.urlresolvers import reverse +from modoboa.lib.tests import ModoTestCase +from . import factories + + +class ProfileTestCase(ModoTestCase): + fixtures = ['initial_users.json'] + + def setUp(self): + super(ProfileTestCase, self).setUp() + self.account = factories.UserFactory( + username="user@test.com", groups=('SimpleUsers',) + ) + + def test_update_password(self): + """Password update + + Two cases: + * The default admin changes his password (no associated Mailbox) + * A normal user changes his password + """ + self.check_ajax_post(reverse("modoboa.core.views.user.profile"), + {"oldpassword": "password", + "newpassword": "titi", "confirmation": "titi"}) + self.clt.logout() + + self.assertEqual( + self.clt.login(username="admin", password="titi"), True + ) + self.assertEqual( + self.clt.login(username="user@test.com", password="toto"), True + ) + + self.check_ajax_post( + reverse("modoboa.core.views.user.profile"), + {"oldpassword": "toto", + "newpassword": "tutu", "confirmation": "tutu"} + ) + self.clt.logout() + self.assertEqual( + self.clt.login(username="user@test.com", password="tutu"), True + ) diff --git a/modoboa/core/urls.py b/modoboa/core/urls.py new file mode 100644 index 000000000..c677f73c4 --- /dev/null +++ b/modoboa/core/urls.py @@ -0,0 +1,21 @@ +from django.conf.urls import patterns + + +urlpatterns = patterns( + '', + + (r'^accounts/login/$', 'modoboa.core.views.auth.dologin'), + (r'^accounts/logout/$', 'modoboa.core.views.auth.dologout'), + + (r'^core/$', 'modoboa.core.views.admin.viewsettings'), + (r'^core/parameters/$', 'modoboa.core.views.admin.viewparameters'), + (r'^core/parameters/save/$', 'modoboa.core.views.admin.saveparameters'), + (r'^core/extensions/$', 'modoboa.core.views.admin.viewextensions'), + (r'^core/extensions/save/$', 'modoboa.core.views.admin.saveextensions'), + (r'^core/info/$', 'modoboa.core.views.admin.information'), + (r'^core/logs/$', 'modoboa.core.views.admin.logs'), + + (r'^user/$', 'modoboa.core.views.user.index'), + (r'^user/preferences/$', 'modoboa.core.views.user.preferences'), + (r'^user/profile/$', 'modoboa.core.views.user.profile'), +) diff --git a/modoboa/lib/management/commands/__init__.py b/modoboa/core/views/__init__.py similarity index 100% rename from modoboa/lib/management/commands/__init__.py rename to modoboa/core/views/__init__.py diff --git a/modoboa/core/views/admin.py b/modoboa/core/views/admin.py new file mode 100644 index 000000000..fdd377cb2 --- /dev/null +++ b/modoboa/core/views/admin.py @@ -0,0 +1,128 @@ +from django.shortcuts import render +from django.template.loader import render_to_string +from django.utils.translation import ugettext as _ +from django.contrib.auth.decorators import ( + login_required, user_passes_test +) +from modoboa.lib import parameters +from modoboa.lib.webutils import ( + ajax_simple_response, _render_to_string +) +from modoboa.lib.listing import get_sort_order, get_listing_page +from modoboa.core.models import Extension, Log +from modoboa.core.tables import ExtensionsTable + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def viewsettings(request, tplname='core/settings_header.html'): + return render(request, tplname, { + "selection": "settings" + }) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def viewparameters(request, tplname='core/parameters.html'): + return ajax_simple_response({ + "status": "ok", + "left_selection": "parameters", + "content": _render_to_string(request, tplname, { + "forms": parameters.get_admin_forms + }) + }) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def saveparameters(request): + for formdef in parameters.get_admin_forms(request.POST): + form = formdef["form"] + if form.is_valid(): + form.save() + form.to_django_settings() + continue + return ajax_simple_response({ + "status": "ko", "prefix": form.app, "errors": form.errors + }) + return ajax_simple_response(dict( + status="ok", respmsg=_("Parameters saved") + )) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def viewextensions(request, tplname='core/extensions.html'): + from modoboa.core.extensions import exts_pool + + exts = exts_pool.list_all() + for ext in exts: + try: + dbext = Extension.objects.get(name=ext["id"]) + ext["selection"] = dbext.enabled + except Extension.DoesNotExist: + dbext = Extension() + dbext.name = ext["id"] + dbext.enabled = False + dbext.save() + ext["selection"] = False + + tbl = ExtensionsTable(request, exts) + return ajax_simple_response({ + "status": "ok", + "content": _render_to_string(request, tplname, {"extensions": tbl}) + }) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def saveextensions(request): + actived_exts = Extension.objects.filter(enabled=True) + found = [] + for k in request.POST.keys(): + if k.startswith("select_"): + parts = k.split("_", 1) + dbext = Extension.objects.get(name=parts[1]) + if not dbext in actived_exts: + dbext.on() + else: + found += [dbext] + for ext in actived_exts: + if not ext in found: + ext.off() + + return ajax_simple_response(dict( + status="ok", respmsg=_("Modifications applied.") + )) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def information(request, tplname="core/information.html"): + return ajax_simple_response({ + "status": "ok", + "content": render_to_string(tplname, {}) + }) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def logs(request, tplname="core/logs.html"): + from modoboa.lib.templatetags.lib_tags import pagination_bar + + sort_order, sort_dir = get_sort_order( + request.GET, "date_created", + allowed_values=['date_created', 'level', 'logger', 'message'] + ) + page = get_listing_page( + Log.objects.all().order_by("%s%s" % (sort_dir, sort_order)), + request.GET.get("page", 1) + ) + return ajax_simple_response({ + "status": "ok", + "content": render_to_string(tplname, { + "logs": page.object_list, + }), + "page": page.number, + "paginbar": pagination_bar(page), + }) diff --git a/modoboa/auth/views.py b/modoboa/core/views/auth.py old mode 100755 new mode 100644 similarity index 86% rename from modoboa/auth/views.py rename to modoboa/core/views/auth.py index 9589e3414..f4d132d9e --- a/modoboa/auth/views.py +++ b/modoboa/core/views/auth.py @@ -7,8 +7,7 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from modoboa.lib import events, parameters from modoboa.lib.webutils import _render_to_string -from forms import LoginForm -from modoboa.auth.lib import encrypt +from modoboa.core.forms import LoginForm def dologin(request): @@ -23,20 +22,20 @@ def dologin(request): login(request, user) if not form.cleaned_data["rememberme"]: request.session.set_expiry(0) - if request.user.has_mailbox: - request.session["password"] = encrypt(form.cleaned_data["password"]) request.session["django_language"] = \ - parameters.get_user(request.user, "LANG", app="general") + parameters.get_user(request.user, "LANG") - logger.info(_("User '%s' successfully logged in" % user.username)) + logger.info( + _("User '%s' successfully logged in" % user.username) + ) events.raiseEvent("UserLogin", request, form.cleaned_data["username"], form.cleaned_data["password"]) nextlocation = request.POST.get("next", None) if nextlocation is None or nextlocation == "None": - if user.group == "SimpleUsers": + if user.group != "SimpleUsers": nextlocation = reverse("modoboa.lib.webutils.topredirection") else: nextlocation = reverse("domains") diff --git a/modoboa/core/views/user.py b/modoboa/core/views/user.py new file mode 100644 index 000000000..c42054526 --- /dev/null +++ b/modoboa/core/views/user.py @@ -0,0 +1,73 @@ +from django.shortcuts import render +from django.utils.translation import ugettext as _ +from django.contrib.auth.decorators import login_required +from modoboa.lib import events, parameters +from modoboa.lib.webutils import ( + ajax_simple_response, _render_to_string +) +from modoboa.lib.cryptutils import encrypt +from modoboa.core.forms import ProfileForm + + +@login_required +def index(request, tplname="core/user_index.html"): + extrajs = events.raiseQueryEvent("ExtraUprefsJS", request.user) + return render(request, tplname, { + "selection": "user", + "extrajs": "".join(extrajs) + }) + + +@login_required +def profile(request, tplname='core/user_profile.html'): + update_password = True + if True in events.raiseQueryEvent("PasswordChange", request.user): + update_password = False + + if request.method == "POST": + form = ProfileForm( + update_password, request.POST, instance=request.user + ) + if form.is_valid(): + form.save() + if update_password and form.cleaned_data["confirmation"] != "": + request.session["password"] = encrypt(form.cleaned_data["confirmation"]) + return ajax_simple_response(dict( + status="ok", respmsg=_("Profile updated") + )) + return ajax_simple_response({ + "status": "ko", + "errors": form.errors + }) + + form = ProfileForm(update_password, instance=request.user) + return ajax_simple_response({ + "status": "ok", + "content": _render_to_string(request, tplname, { + "form": form + }) + }) + + +@login_required +def preferences(request): + if request.method == "POST": + for formdef in parameters.get_user_forms(request.user, request.POST)(): + form = formdef["form"] + if form.is_valid(): + form.save() + continue + return ajax_simple_response({ + "status": "ko", "prefix": form.app, "errors": form.errors + }) + + return ajax_simple_response({ + "status": "ok", "respmsg": _("Preferences saved") + }) + + return ajax_simple_response({ + "status": "ok", + "content": _render_to_string(request, "core/user_preferences.html", { + "forms": parameters.get_user_forms(request.user) + }) + }) diff --git a/modoboa/demo/__init__.py b/modoboa/demo/__init__.py index 739076d53..76f5c05b4 100755 --- a/modoboa/demo/__init__.py +++ b/modoboa/demo/__init__.py @@ -1,32 +1,33 @@ # coding: utf-8 -from django.conf import settings from django.core.urlresolvers import reverse from django.utils.translation import ugettext_lazy as _ from django.template.loader import render_to_string from modoboa.lib import events from modoboa.lib.webutils import static_url + @events.observe("UserMenuDisplay") def menu(target, user): if target != "top_menu_middle": return [] - if not user.has_mailbox: + if not user.mailbox_set.count(): return [] return [ - {"name" : "demo", - "label" : _("Test messages"), - "img" : static_url("pics/demo.png"), - "class" : "topdropdown", - "menu" : [ - {"name" : "sendvirus", - "label" : _("Send virus"), - "url" : reverse("modoboa.demo.views.send_virus")}, - {"name" : "sendspam", - "label" : _("Send spam"), - "url" : reverse("modoboa.demo.views.send_spam")} - ] + {"name": "demo", + "label": _("Test messages"), + "img": static_url("pics/demo.png"), + "class": "topdropdown", + "menu": [ + {"name": "sendvirus", + "label": _("Send virus"), + "url": reverse("modoboa.demo.views.send_virus")}, + {"name": "sendspam", + "label": _("Send spam"), + "url": reverse("modoboa.demo.views.send_spam")} + ] } - ] + ] + @events.observe("GetAnnouncement") def announcement(target): @@ -35,13 +36,15 @@ def announcement(target): return [txt] return "" + @events.observe("PasswordChange") def password_change(user): return [user.id == 1] + @events.observe("GetStaticContent") def get_static_content(user): - if not user.has_mailbox: + if not user.mailbox_set.count(): return [] return """ -""" % (level, text, timeout) - -@register.simple_tag -def extra_head_content(user): - tpl = Template("{% for sc in static_content %}{{ sc|safe }}{% endfor %}") - return tpl.render( - Context( - dict(static_content=events.raiseQueryEvent("GetStaticContent", user)) - ) - ) - -@register.simple_tag -def load_optionalmenu(user): - menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu_middle", user) - return template.loader.render_to_string('common/menulist.html', - {"entries" : menu, "user" : user}) - -@register.simple_tag -def load_notifications(user): - content = events.raiseQueryEvent("TopNotifications", user) - return "".join(content) - -@register.simple_tag -def render_form(form, tpl=None): - if tpl is not None: - return render_to_string(tpl, dict(form=form)) - - ret = "" - for field in form: - ret += "%s\n" % render_field(field) - return ret - -@register.simple_tag -def render_field(field, help_display_mode="tooltip"): - if type(field.form.fields[field.name]) is SeparatorField: - return "%s" % (visirule(field), unicode(field.label)) - - return render_to_string("common/generic_field.html", dict( - field=field, help_display_mode=help_display_mode - )) - -@register.simple_tag -def render_field_appended(field, text): - return render_to_string("common/generic_field.html", dict( - field=field, help_display_mode="tooltip", appended_text=text - )) - -@register.simple_tag -def render_field_with_activator(field, activator_label=ugettext_lazy("activate")): - return render_to_string("common/generic_field.html", { - "field": field, "help_display_mode": "tooltip", "activator": True, - "activator_label": activator_label, "deactivate_if_empty": True - }) - -@register.simple_tag -def render_and_hide_field(field): - return render_to_string("common/generic_field.html", dict( - field=field, hidden=True - )) - -@register.simple_tag -def render_fields_group(form, pattern): - from django.forms import forms - - first = forms.BoundField(form, form.fields[pattern], pattern) - label = first.label - group = [first] - cpt = 1 - haserror = len(first.errors) != 0 - while True: - fname = "%s_%d" % (pattern, cpt) - if not form.fields.has_key(fname): - break - bfield = forms.BoundField(form, form.fields[fname], fname) - if len(bfield.errors): - haserror = True - group += [bfield] - cpt += 1 - - return render_to_string("common/generic_fields_group.html", dict( - label=label, help_text=first.help_text, group=group, haserror=haserror - )) - -@register.simple_tag -def pagination_bar(page): - return render_to_string("common/pagination_bar.html", dict( - page=page, baseurl="?" - )) - -@register.simple_tag -def alert(msg, typ): - t = Template("""
-× -{{ msg }} -
""") - return t.render(Context(dict(type=typ, msg=msg))) - -@register.simple_tag -def render_link(linkdef, mdclass=""): - t = Template(""" -{% if link.img %}{% endif %} -{{ link.label }}""") - return t.render(Context(dict(link=linkdef, mdclass=mdclass))) - -@register.simple_tag -def progress_color(value): - value = int(value) - if value < 50: - return "progress-success" - if value < 80: - return "progress-warning" - return "progress-danger" - -@register.filter -def fromunix(value): - return datetime.datetime.fromtimestamp(int(value)) - - -@register.simple_tag -def visirule(field): - if not hasattr(field.form, "visirules") or not field.html_name in field.form.visirules: - return "" - rule = field.form.visirules[field.html_name] - return " data-visibility-field='%s' data-visibility-value='%s' " \ - % (rule["field"], rule["value"]) - - -class ConnectedUsers(template.Node): - def __init__(self, varname): - self.varname = varname - - def render(self, context): - from modoboa.admin.models import User - - sessions = Session.objects.filter(expire_date__gte=timezone.now()) - uid_list = [] - - # Build a list of user ids from that query - for session in sessions: - data = session.get_decoded() - uid = data.get('_auth_user_id', None) - if uid: - uid_list.append(uid) - - # Query all logged in users based on id list - context[self.varname] = [] - for uid in uid_list: - try: - context[self.varname].append(User.objects.get(pk=uid)) - except User.DoesNotExist: - pass - return '' - -@register.tag -def connected_users(parser, token): - try: - tag, a, varname = token.split_contents() - except ValueError: - raise template.TemplateSyntaxError('connected_users usage: {% connected_users as users %}') - return ConnectedUsers(varname) - -@register.simple_tag -def render_tags(tags): - t = Template("""{% for tag in tags %} - - {{ tag.label }} - -{% endfor %} -""") - return t.render(Context({"tags" : tags})) - - -@register.simple_tag -def get_modoboa_logo(): - try: - logo = settings.MODOBOA_CUSTOM_LOGO - except AttributeError: - logo = None - if logo is None: - return os.path.join(settings.STATIC_URL, "css/modoboa.png") - return logo - diff --git a/modoboa/lib/tests.py b/modoboa/lib/tests.py index d99d2e6a1..0a0330294 100644 --- a/modoboa/lib/tests.py +++ b/modoboa/lib/tests.py @@ -1,5 +1,4 @@ # coding: utf-8 -import unittest from django.test import TestCase from django.test.client import Client from django.utils import simplejson @@ -9,7 +8,7 @@ from modoboa.lib import parameters class ModoTestCase(TestCase): - + def setUp(self, username="admin", password="password"): self.clt = Client() self.assertEqual(self.clt.login(username=username, password=password), True) @@ -35,8 +34,8 @@ class ExtTestCase(ModoTestCase): def setUp(self, *args, **kwargs): super(ExtTestCase, self).setUp(*args, **kwargs) - self.clt.get(reverse("modoboa.admin.views.viewextensions")) - self.clt.post(reverse("modoboa.admin.views.saveextensions"), + self.clt.get(reverse("modoboa.core.views.admin.viewextensions")) + self.clt.post(reverse("modoboa.core.views.admin.saveextensions"), {"select_%s" % self.name : "1"}) @@ -65,7 +64,7 @@ class ParameterTestCase(TestCase): """ def setUp(self): - from modoboa.admin.models import User + from modoboa.core.models import User parameters.register(TestParams, "Test") parameters.register(TestUserParams, "TestUser") self.user = User.objects.create(username="tester") diff --git a/modoboa/lib/webutils.py b/modoboa/lib/webutils.py index 3e7ceec92..aa47ea23e 100644 --- a/modoboa/lib/webutils.py +++ b/modoboa/lib/webutils.py @@ -3,7 +3,7 @@ """ This module contains extra functions/shortcuts used to render HTML. """ -import os, sys +import sys import re from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth.decorators import login_required @@ -13,49 +13,44 @@ from django import template from django.utils import simplejson from django.conf import settings -def _render(request, tpl, user_context): - """Custom rendering function + +def _render_to_string(request, tpl, user_context): + """Custom rendering function. Just a wrapper which automatically adds a RequestContext instance (useful to use settings variables like STATIC_URL inside templates) """ - return render_to_response(tpl, user_context, - context_instance=template.RequestContext(request)) - -def _render_to_string(request, tpl, user_context): - """Custom rendering function - - Same as _render. - """ return render_to_string(tpl, user_context, context_instance=template.RequestContext(request)) -def _render_error(request, errortpl="error", user_context={}): - return render_to_response("common/%s.html" % errortpl, user_context, - context_instance=template.RequestContext(request)) + +def _render_error(request, errortpl="error", user_context=None): + if user_context is None: + user_context = {} + return render_to_response( + "common/%s.html" % errortpl, user_context, + context_instance=template.RequestContext(request) + ) + def render_actions(actions): - t = template.Template("""{% load libextras %} + t = template.Template("""{% load lib_tags %} {% for a in actions %}{% render_link a %}{% endfor %} """) return t.render(template.Context(dict(actions=actions))) -def _ctx_ok(url): - return {"status" : "ok", "url" : url} - -def _ctx_ko(tpl, ctx): - return {"status" : "ko", "content" : render_to_string(tpl, ctx)} def getctx(status, level=1, callback=None, **kwargs): if not callback: callername = sys._getframe(level).f_code.co_name else: callername = callback - ctx = {"status" : status, "callback" : callername} + ctx = {"status": status, "callback": callername} for kw, v in kwargs.iteritems(): ctx[kw] = v return ctx + def ajax_response(request, status="ok", respmsg=None, url=None, ajaxnav=False, norefresh=False, template=None, **kwargs): @@ -83,7 +78,7 @@ def ajax_response(request, status="ok", respmsg=None, content = kwargs["content"] else: content = "" - jsonctx = {"status" : status, "content" : content} + jsonctx = {"status": status, "content": content} if respmsg is not None: jsonctx["respmsg"] = respmsg if ajaxnav: @@ -93,6 +88,7 @@ def ajax_response(request, status="ok", respmsg=None, jsonctx["norefresh"] = norefresh return HttpResponse(simplejson.dumps(jsonctx), mimetype="application/json") + def ajax_simple_response(content, **response_kwargs): """Simple AJAX response @@ -103,6 +99,7 @@ def ajax_simple_response(content, **response_kwargs): response_kwargs["content_type"] = "application/json" return HttpResponse(simplejson.dumps(content), **response_kwargs) + def static_url(path): """Returns the correct static url for a given file @@ -112,11 +109,12 @@ def static_url(path): path = path[1:] return "%s%s" % (settings.STATIC_URL, path) + def size2integer(value): """Try to convert a string representing a size to an integer value in bytes. - Supported formats: + Supported formats: * K|k for KB * M|m for MB * G|g for GB @@ -137,6 +135,7 @@ def size2integer(value): return int(m.group(1)) * 2 ** 30 return 0 + @login_required def topredirection(request): """Simple view to redirect the request when no application is specified @@ -149,16 +148,16 @@ def topredirection(request): :param request: a Request object """ from modoboa.lib import parameters - from modoboa.extensions import exts_pool + from modoboa.core.extensions import exts_pool - topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="admin") - if not topredir in ["admin", "userprefs"]: + topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="core") + if not topredir in ["core"]: infos = exts_pool.get_extension_infos(topredir) path = infos["url"] if infos["url"] else infos["name"] else: - path = topredir + path = "admin" # topredir - if topredir in ["admin", "stats"] and \ + if topredir in ["core", "stats"] and \ request.user.belongs_to_group('SimpleUsers'): path = "userprefs" diff --git a/modoboa/locale/cs_CZ/LC_MESSAGES/django.po b/modoboa/locale/cs_CZ/LC_MESSAGES/django.po new file mode 100644 index 000000000..fef7d5376 --- /dev/null +++ b/modoboa/locale/cs_CZ/LC_MESSAGES/django.po @@ -0,0 +1,2605 @@ +# Modoboa Czech translation +# Copyright (C) 2013 +# This file is distributed under the same license as the Modoboa package. +# +# Miroslav Abrahám , 2013. +msgid "" +msgstr "" +"Project-Id-Version: modoboa-1.0.0\n" +"Report-Msgid-Bugs-To: miris@kunago.com\n" +"POT-Creation-Date: 2013-09-20 22:30+0200\n" +"PO-Revision-Date: 2013-09-29 22:08+0100\n" +"Last-Translator: Miroslav Abrahám \n" +"Language-Team: CS \n" +"Language: Čeština\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Poedit 1.5.7\n" + +#: admin/__init__.py:9 +msgid "Administration console" +msgstr "Konzola správce" + +#: admin/__init__.py:11 +msgid "Web based console to manage domains, accounts and aliases" +msgstr "Webová konzola pro správu domén, účtů a aliasů" + +#: admin/__init__.py:16 admin/forms.py:211 admin/forms.py:652 +#: admin/views.py:94 admin/views.py:447 +#: extensions/postfix_autoreply/app_settings.py:11 +#: extensions/sievefilters/app_settings.py:50 +#: extensions/stats/app_settings.py:11 extensions/webmail/app_settings.py:11 +#: userprefs/__init__.py:15 +msgid "General" +msgstr "Obecné" + +#: admin/app_settings.py:31 +msgid "Authentication" +msgstr "Ověření" + +#: admin/app_settings.py:34 +msgid "Authentication type" +msgstr "Typ ověření" + +#: admin/app_settings.py:35 +msgid "Local" +msgstr "Lokální" + +#: admin/app_settings.py:38 +msgid "The backend used for authentication" +msgstr "Rozhraní pro ověřování přístupu" + +#: admin/app_settings.py:43 +msgid "Default password scheme" +msgstr "Výchozí schéma hesel" + +#: admin/app_settings.py:50 +msgid "Scheme used to crypt mailbox passwords" +msgstr "Použité schéma pro šifrování hesel schránek" + +#: admin/app_settings.py:54 +msgid "Secret key" +msgstr "Tajný klíč" + +#: admin/app_settings.py:56 +msgid "Key used to encrypt/decrypt passwords" +msgstr "Klíč používaný k šifrování/dešifrování hesel" + +#: admin/app_settings.py:60 +msgid "LDAP settings" +msgstr "Nastavení LDAP" + +#: admin/app_settings.py:63 extensions/sievefilters/app_settings.py:22 +#: extensions/webmail/app_settings.py:22 extensions/webmail/app_settings.py:42 +msgid "Server address" +msgstr "Adresa serveru" + +#: admin/app_settings.py:65 +msgid "The IP address of the DNS name of the LDAP server" +msgstr "IP adresa DNS jména LDAP serveru" + +#: admin/app_settings.py:69 extensions/sievefilters/app_settings.py:28 +#: extensions/webmail/app_settings.py:34 extensions/webmail/app_settings.py:58 +msgid "Server port" +msgstr "Port serveru" + +#: admin/app_settings.py:71 +msgid "The TCP port number used by the LDAP server" +msgstr "Port TCP, který používá LDAP server" + +#: admin/app_settings.py:75 extensions/webmail/app_settings.py:28 +msgid "Use a secured connection" +msgstr "Použít zabezpečené připojení" + +#: admin/app_settings.py:77 +msgid "Use an SSL/TLS connection to access the LDAP server" +msgstr "Použít připojení SSL/TLS pro přístup k LDAP serveru" + +#: admin/app_settings.py:81 +msgid "Authentication method" +msgstr "Metoda ověření" + +#: admin/app_settings.py:82 +msgid "Search and bind" +msgstr "Vyhledat a spojit" + +#: admin/app_settings.py:83 +msgid "Direct bind" +msgstr "Přímé spojení" + +#: admin/app_settings.py:85 +msgid "Choose the authentication method to use" +msgstr "Zvolte si metodu ověření, která se použije" + +#: admin/app_settings.py:90 +msgid "Bind DN" +msgstr "Spojení pomocí DN" + +#: admin/app_settings.py:92 +msgid "" +"The distinguished name to use when binding to the LDAP server. Leave empty " +"for an anonymous bind" +msgstr "" +"Rozlišovací jméno, které se použije při spojování s LDAP serverem. " +"Ponechejte prázdné pro anonymní spojení." + +#: admin/app_settings.py:97 +msgid "Bind password" +msgstr "Heslo spojení" + +#: admin/app_settings.py:99 +msgid "The password to use when binding to the LDAP server (with 'Bind DN')" +msgstr "" +"Heslo, které se použije při spojování s LDAP serverem (při výběru DN " +"spojení)." + +#: admin/app_settings.py:105 +msgid "Search base" +msgstr "Základna struktury" + +#: admin/app_settings.py:107 +msgid "The distinguished name of the search base" +msgstr "Rozlišovací jméno (DN) základny struktury" + +#: admin/app_settings.py:112 +msgid "Search filter" +msgstr "Vyhledávací filtr" + +#: admin/app_settings.py:114 +msgid "" +"An optional filter string (e.g. '(objectClass=person)'). In order to be " +"valid, it must be enclosed in parentheses." +msgstr "" +"Volitelný filtr (např. '(objectClass=osoba)'). Aby byl filtr platný, je " +"nezbytné jen uzavřít do závorek." + +#: admin/app_settings.py:119 +msgid "User DN template" +msgstr "Šablona uživatelského DN" + +#: admin/app_settings.py:121 +#, python-format +msgid "" +"The template used to construct a user's DN. It should contain one " +"placeholder (ie. %(user)s)" +msgstr "" +"Šablona použitá pro tvorbu uživatelského rozlišovacího jména (DN). Měla by " +"obsahovat jeden zástupný řetězec (tj. %(user)s)." + +#: admin/app_settings.py:126 +msgid "Password attribute" +msgstr "Atribut hesla" + +#: admin/app_settings.py:128 +msgid "The attribute used to store user passwords" +msgstr "Atribut používaný pro ukládání hesel uživatelů" + +#: admin/app_settings.py:132 +msgid "Active Directory" +msgstr "Active Directory" + +#: admin/app_settings.py:134 +msgid "Tell if the LDAP server is an Active Directory one" +msgstr "" +"Volba určující, jestli je nebo není LDAP server serverem Active Directory" + +#: admin/app_settings.py:137 admin/templates/admin/editdomainform.html:25 +#: extensions/limits/app_settings.py:31 extensions/limits/forms.py:24 +#: extensions/limits/models.py:14 extensions/webmail/app_settings.py:101 +#: extensions/webmail/templates/webmail/folders.html:4 +msgid "Mailboxes" +msgstr "Schránky" + +#: admin/app_settings.py:140 +msgid "Handle mailboxes on filesystem" +msgstr "Spravovat schránky v souborovém systému" + +#: admin/app_settings.py:142 +msgid "" +"Rename or remove mailboxes on the filesystem when they get renamed or " +"removed within Modoboa" +msgstr "" +"Přejmenovat nebo odstranit schránky v souborovém systému, pokud jsou " +"přejmenovány nebo odstraněny v Modoboa" + +#: admin/app_settings.py:146 +msgid "Mailboxes ower" +msgstr "Vlastník schránek" + +#: admin/app_settings.py:148 +msgid "The UNIX account who owns mailboxes on the filesystem" +msgstr "Účet UNIXu, který vlastní schránky v souborovém systému" + +#: admin/app_settings.py:152 +msgid "Automatic account removal" +msgstr "Automatické odstranění účtu" + +#: admin/app_settings.py:154 +msgid "When a mailbox is removed, also remove the associated account" +msgstr "Když je schránka odstraněná, odstranit také přiřazený účet uživatele" + +#: admin/app_settings.py:157 admin/templates/admin/editdomainform.html:7 +msgid "Miscellaneous" +msgstr "Různé" + +#: admin/app_settings.py:160 +msgid "Maximum log record age" +msgstr "Maximální stáří záznamu" + +#: admin/app_settings.py:162 +msgid "The maximum age in days of a log record" +msgstr "Maximální stáří záznamu ve dnech" + +#: admin/app_settings.py:166 +msgid "Items per page" +msgstr "Položek na stránce" + +#: admin/app_settings.py:168 +msgid "Number of displayed items per page" +msgstr "Počet zobrazených položek na stránce" + +#: admin/app_settings.py:172 +msgid "Default top redirection" +msgstr "Výchozí přesměrování aplikace" + +#: admin/app_settings.py:175 +msgid "The default redirection used when no application is specified" +msgstr "" +"Výchozí přesměrování, které se použije, pokud není žádná aplikace " +"specifikována" + +#: admin/app_settings.py:213 +msgid "Key must be either 16, 24, or 32 bytes long" +msgstr "Klíč musí mít délku 16, 24 nebo 32 znaků" + +#: admin/app_settings.py:222 +msgid "Invalid syntax" +msgstr "Neplatná syntaxe" + +#: admin/app_settings.py:245 +msgid "This field is required" +msgstr "Toto pole je vyžadováno" + +#: admin/forms.py:20 admin/forms.py:497 admin/tables.py:18 +msgid "Alias(es)" +msgstr "Alias(y)" + +#: admin/forms.py:22 +msgid "" +"Alias(es) of this domain. Indicate only one name per input, press ENTER to " +"add a new input." +msgstr "" +"Alias(y) této domény. Uveďte na řádek pouze jeden alias; pro vložení dalšího " +"aliasu stiskněte po vložení ENTER." + +#: admin/forms.py:59 +msgid "An alias with this name already exists" +msgstr "Alias s tímto názvem již existuje" + +#: admin/forms.py:73 +msgid "A domain with this name already exists" +msgstr "Doména s tímto názvem již existuje" + +#: admin/forms.py:146 +msgid "Create a domain administrator" +msgstr "Vytvořit správce domény" + +#: admin/forms.py:148 +msgid "Automatically create an administrator for this domain" +msgstr "Automaticky vytvořit správce této domény" + +#: admin/forms.py:152 admin/tables.py:12 admin/tables.py:47 +#: admin/templates/admin/editdomainform.html:13 +#: admin/templates/admin/identities_table.html:5 +#: admin/templates/admin/information.html:8 +#: extensions/sievefilters/forms.py:53 +#: extensions/sievefilters/templates/sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Jméno" + +#: admin/forms.py:154 +msgid "" +"The administrator's name. Don't include the domain's name here, it will be " +"automatically appended." +msgstr "" +"Jméno správce. Nevkládejte sem název domény, název bude automaticky doplněn." + +#: admin/forms.py:160 +msgid "Create aliases" +msgstr "Vytvořit aliasy" + +#: admin/forms.py:162 +msgid "Automatically create standard aliases for this domain" +msgstr "Automaticky vytvořit běžné aliasy pro tuto doménu" + +#: admin/forms.py:175 +msgid "Invalid format" +msgstr "Neplatný formát" + +#: admin/forms.py:187 +#, python-format +msgid "User '%s' already exists" +msgstr "Uživatel '%s' již existuje" + +#: admin/forms.py:235 +msgid "Email address" +msgstr "Emailová adresa" + +#: admin/forms.py:236 +msgid "" +"The distribution list address. Use the '*' character to create a 'catchall' " +"address (ex: *@domain.tld)." +msgstr "" +"Distribuční seznam adres. Pro vytvoření doménového koše vložte znak " +"'*' (např. *@domena.cz)." + +#: admin/forms.py:239 +msgid "Recipients" +msgstr "Příjemci" + +#: admin/forms.py:240 +msgid "" +"Mailbox(es) this alias will point to. Indicate only one address per input, " +"press ENTER to add a new input." +msgstr "" +"Schránka/Schránky, na kterou/které bude tento alias odkazovat. Uveďte na " +"řádek pouze jednu adresu; pro vložení další adresy stiskněte po vložení " +"ENTER." + +#: admin/forms.py:278 admin/forms.py:542 admin/forms.py:568 +msgid "Domain does not exist" +msgstr "Doména neexistuje" + +#: admin/forms.py:280 +msgid "You don't have access to this domain" +msgstr "Nemáte oprávnění k přístupu k této doméně" + +#: admin/forms.py:300 +msgid "Invalid mailbox" +msgstr "Neplatná schránka" + +#: admin/forms.py:316 admin/models.py:1202 +#, python-format +msgid "Local recipient %s not found" +msgstr "Místní příjemce %s nebyl nalezen" + +#: admin/forms.py:318 admin/forms.py:324 +#, python-format +msgid "Recipient %s already present" +msgstr "Příjemce %s je již vložen" + +#: admin/forms.py:329 +msgid "No recipient defined" +msgstr "Nebyl určen příjemce" + +#: admin/forms.py:343 extensions/webmail/forms.py:19 +msgid "Select a file" +msgstr "Vybrat soubor" + +#: admin/forms.py:345 admin/forms.py:373 +msgid "Separator" +msgstr "Oddělovač" + +#: admin/forms.py:348 +msgid "Continue on error" +msgstr "Při výskytu chyby pokračovat" + +#: admin/forms.py:349 +msgid "Don't treat duplicated objects as error" +msgstr "Nepovažovat duplikáty objektů za chybu" + +#: admin/forms.py:364 +msgid "Crypt passwords" +msgstr "Zašifrovat hesla" + +#: admin/forms.py:365 +msgid "Check this option if passwords contained in your file are not crypted" +msgstr "Zatrhněte toto pole, pokud hesla ve vašem souboru nejsou zašifrována" + +#: admin/forms.py:371 +msgid "File name" +msgstr "Název souboru" + +#: admin/forms.py:403 admin/templates/admin/editdomainform.html:13 +#: auth/forms.py:7 +msgid "Username" +msgstr "Uživatelské jméno" + +#: admin/forms.py:405 +msgid "Role" +msgstr "Role" + +#: admin/forms.py:406 admin/forms.py:433 +msgid "Choose" +msgstr "Vybrat" + +#: admin/forms.py:407 +msgid "What level of permission this user will have" +msgstr "Jaká úroveň oprávnění bude tomuto uživateli nastavena" + +#: admin/forms.py:409 auth/forms.py:11 +msgid "Password" +msgstr "Heslo" + +#: admin/forms.py:411 userprefs/forms.py:18 +msgid "Confirmation" +msgstr "Potvrzení" + +#: admin/forms.py:413 +msgid "Enter the same password as above, for verification." +msgstr "Vložte stejné heslo jako v poli výše, pro jeho ověření." + +#: admin/forms.py:424 +msgid "Enabled" +msgstr "Povoleno" + +#: admin/forms.py:471 +msgid "The two password fields didn't match." +msgstr "Obě pole s heslem se neshodují." + +#: admin/forms.py:477 +msgid "You can't disable your own account" +msgstr "Nemůžete zakázat váš vlastní účet" + +#: admin/forms.py:488 +msgid "E-mail" +msgstr "E-mail" + +#: admin/forms.py:490 +msgid "Quota" +msgstr "Kvóta" + +#: admin/forms.py:492 +msgid "" +"Quota in MB for this mailbox. Define a custom value or use domain's default " +"one. Leave empty to define an unlimited value (not allowed for domain " +"administrators)." +msgstr "" +"Kvóta pro tuto chránku v MB. Definujte vlastní hodnotu nebo použijte výchozí " +"hodnotu domény. Ponechejte prázdné pro neomezenou kvótu (není povoleno " +"správcům domény)." + +#: admin/forms.py:499 +msgid "" +"Alias(es) of this mailbox. Indicate only one address per input, press ENTER " +"to add a new input. Use the '*' character to create a 'catchall' alias (ex: " +"*@domain.tld)." +msgstr "" +"Alias(y) této schránky. Vložte pouze jeden alias na řádek, stiskněte ENTER " +"pro vložení dalšího aliasu. Pro vytvoření doménového koše vložte '*' (např. " +"*@domena.cz)." + +#: admin/forms.py:550 +#, python-format +msgid "Mailbox %s already exists" +msgstr "Schránka %s již existuje" + +#: admin/forms.py:610 +msgid "Domain(s)" +msgstr "Doména(y)" + +#: admin/forms.py:612 +msgid "Domain(s) that user administrates" +msgstr "Doména(y) pod správou uživatele" + +#: admin/forms.py:654 admin/views.py:450 +msgid "Mail" +msgstr "Mail" + +#: admin/forms.py:656 +msgid "Permissions" +msgstr "Oprávnění" + +#: admin/models.py:80 +msgid "You can't delete your own account" +msgstr "Nemůžete smazat vlastní účet" + +#: admin/models.py:161 +msgid "Failed to update password: LDAP module not installed" +msgstr "Nebylo možné změnit heslo: LDAP modul není instalován" + +#: admin/models.py:167 +#, python-format +msgid "Failed to update password: %s" +msgstr "Nebylo možné změnit heslo: %s" + +#: admin/models.py:194 +msgid "account" +msgstr "účet" + +#: admin/models.py:496 admin/models.py:733 admin/models.py:795 +msgid "Invalid line" +msgstr "Neplatný řádek" + +#: admin/models.py:500 +msgid "You can't import an account with a role greater than yours" +msgstr "Nemůžete importvat účet s vyšším oprávněním, než máte vy sám" + +#: admin/models.py:506 +#, python-format +msgid "The simple user '%s' must have a valid email address" +msgstr "Uživatel '%s' musí mít nastavenou platnou emailovou adresu" + +#: admin/models.py:510 +#, python-format +msgid "username and email fields must not differ for '%s'" +msgstr "pole uživatelského jména a emailu se nesmí pro '%s' lišit" + +#: admin/models.py:534 +#, python-format +msgid "Account import failed (%s): domain does not exist" +msgstr "Import účtu nebyl úspěšný (%s): doména neexistuje" + +#: admin/models.py:624 admin/models.py:746 +msgid "name" +msgstr "název" + +#: admin/models.py:625 +msgid "The domain name" +msgstr "Doménové jméno" + +#: admin/models.py:627 +msgid "Default quota in MB applied to mailboxes" +msgstr "Výchozí kvóta schránek v MB" + +#: admin/models.py:630 admin/models.py:753 admin/models.py:1045 +#: admin/models.py:1217 extensions/postfix_autoreply/models.py:27 +msgid "enabled" +msgstr "povoleno" + +#: admin/models.py:631 +msgid "Check to activate this domain" +msgstr "Zatrhněte pole pro povolení této domény" + +#: admin/models.py:747 +msgid "The alias name" +msgstr "Název aliasu" + +#: admin/models.py:749 +msgid "target" +msgstr "cíl" + +#: admin/models.py:750 +msgid "The domain this alias points to" +msgstr "Doména, na kterou tento alias odkazuje" + +#: admin/models.py:754 admin/models.py:1046 +msgid "Check to activate this alias" +msgstr "Zatrhněte pole pro povolení tohoto aliasu" + +#: admin/models.py:801 +#, python-format +msgid "Unknown domain %s" +msgstr "Neznámá doména %s" + +#: admin/models.py:818 admin/models.py:1031 +msgid "address" +msgstr "adresa" + +#: admin/models.py:819 +msgid "Mailbox address (without the @domain.tld part)" +msgstr "Adresa schránky (bez části @doména.tld)" + +#: admin/models.py:877 +#, python-format +msgid "Failed to retrieve mailbox location (%s)" +msgstr "Nebylo možné zjistit umístění schránky (%s)" + +#: admin/models.py:921 +#, python-format +msgid "Quota is greater than the allowed domain's limit (%dM)" +msgstr "Zvolená kvóta je větší než povolené omezení domény (%dM)" + +#: admin/models.py:926 +msgid "A quota is required" +msgstr "Kvóta je vyžadována" + +#: admin/models.py:1032 +msgid "" +"The alias address (without the domain part). For a 'catch-all' address, just " +"enter an * character." +msgstr "" +"Alias adresy (bez části označující doménu). Pro vytvoření doménového koše " +"vložte znak '*'." + +#: admin/models.py:1036 +msgid "mailboxes" +msgstr "schránky" + +#: admin/models.py:1037 +msgid "The mailboxes this alias points to" +msgstr "Schránky, na které tento alias odkazuje" + +#: admin/models.py:1041 +msgid "The aliases this alias points to" +msgstr "Aliasy, na které tento alias odkazuje" + +#: admin/models.py:1089 +msgid "distribution list" +msgstr "distribuční seznam" + +#: admin/models.py:1090 +msgid "forward" +msgstr "přeposlat" + +#: admin/models.py:1091 +msgid "alias" +msgstr "alias" + +#: admin/models.py:1170 +#, python-format +msgid "Invalid line: %s" +msgstr "Neplatný řádek: %s" + +#: admin/models.py:1175 +#, python-format +msgid "Domain '%s' does not exist" +msgstr "Doména '%s' neexistuje" + +#: admin/models.py:1218 +msgid "Check to enable this extension" +msgstr "Zatrhněte pole pro povolení tohoto rozšíření" + +#: admin/models.py:1318 +msgid "added" +msgstr "přidáno" + +#: admin/models.py:1321 +msgid "modified" +msgstr "upraveno" + +#: admin/models.py:1324 +msgid "deleted" +msgstr "smazáno" + +#: admin/models.py:1326 +#, python-format +msgid "%(object)s '%(name)s' %(action)s by user %(user)s" +msgstr "%(object)s '%(name)s' %(action)s uživatelem %(user)s" + +#: admin/tables.py:14 +msgid "Edit domain" +msgstr "Upravit doménu" + +#: admin/tables.py:21 admin/templates/admin/identities_table.html:8 +#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:83 +#: extensions/sievefilters/templates/sievefilters/filter.html:17 +#: extensions/sievefilters/templates/sievefilters/guieditor.html:5 +#: extensions/webmail/templatetags/webextras.py:97 +msgid "Actions" +msgstr "Akce" + +#: admin/tables.py:48 +msgid "Version" +msgstr "Verze" + +#: admin/tables.py:49 +msgid "Description" +msgstr "Popis" + +#: admin/views.py:88 +msgid "New domain" +msgstr "Nová doména" + +#: admin/views.py:89 admin/views.py:98 admin/views.py:228 admin/views.py:444 +#: admin/views.py:451 extensions/sievefilters/views.py:129 +#: extensions/sievefilters/views.py:198 extensions/webmail/views.py:136 +msgid "Create" +msgstr "Vytvořit" + +#: admin/views.py:95 admin/views.py:448 +msgid "Next" +msgstr "Další" + +#: admin/views.py:97 +msgid "Options" +msgstr "Možnosti" + +#: admin/views.py:99 admin/views.py:452 +msgid "Previous" +msgstr "Předchozí" + +#: admin/views.py:112 +msgid "Domain created" +msgstr "Doména vytvořena" + +#: admin/views.py:146 admin/views.py:285 admin/views.py:498 +#: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:9 +#: extensions/sievefilters/views.py:148 extensions/webmail/views.py:168 +#: userprefs/templates/userprefs/forward.html:8 +#: userprefs/templates/userprefs/profile.html:8 +msgid "Update" +msgstr "Aktualizovat" + +#: admin/views.py:162 +msgid "Domain modified" +msgstr "Doména upravena" + +#: admin/views.py:189 +msgid "You can't delete your own domain" +msgstr "Nemůžete smazat vlastní doménu" + +#: admin/views.py:192 +msgid "Domain deleted" +msgid_plural "Domains deleted" +msgstr[0] "Doména smazána" +msgstr[1] "Domény smazány" +msgstr[2] "Domén smazáno" + +#: admin/views.py:207 +msgid "Alias with this name already exists" +msgstr "Alias s tímto názvem již existuje" + +#: admin/views.py:250 +msgid "New distribution list" +msgstr "Nový distribuční seznam" + +#: admin/views.py:251 +msgid "Distribution list created" +msgstr "Distribuční seznam vytvořen" + +#: admin/views.py:260 +msgid "New alias" +msgstr "Nový alias" + +#: admin/views.py:261 +msgid "Alias created" +msgstr "Alias vytvořen" + +#: admin/views.py:270 +msgid "New forward" +msgstr "Nové přeposílání" + +#: admin/views.py:271 +msgid "Forward created" +msgstr "Přeposílání vytvořeno" + +#: admin/views.py:289 +msgid "Distribution list modified" +msgstr "Distribuční seznam upraven" + +#: admin/views.py:291 +msgid "Forward modified" +msgstr "Přeposílání upraveno" + +#: admin/views.py:293 +msgid "Alias modified" +msgstr "Alias upraven" + +#: admin/views.py:402 admin/views.py:545 admin/views.py:550 +#: extensions/amavis/views.py:204 extensions/amavis/views.py:210 +#: extensions/amavis/views.py:243 extensions/amavis/views.py:247 +#: extensions/amavis/views.py:249 extensions/webmail/views.py:41 +#: extensions/webmail/views.py:68 extensions/webmail/views.py:81 +#: extensions/webmail/views.py:98 extensions/webmail/views.py:114 +#: extensions/webmail/views.py:200 extensions/webmail/views.py:427 +#: extensions/webmail/views.py:438 extensions/webmail/views.py:465 +#: lib/formutils.py:32 lib/formutils.py:37 +msgid "Invalid request" +msgstr "Neplatný požadavek" + +#: admin/views.py:441 +msgid "New account" +msgstr "Nový účet" + +#: admin/views.py:465 +msgid "Account created" +msgstr "Účet vytvořen" + +#: admin/views.py:513 +msgid "Account updated" +msgstr "Účet aktualizován" + +#: admin/views.py:535 +msgid "Account deleted" +msgid_plural "Accounts deleted" +msgstr[0] "Účet smazán" +msgstr[1] "Účty smazány" +msgstr[2] "Účtů smazáno" + +#: admin/views.py:590 +msgid "Parameters saved" +msgstr "Parametry uloženy" + +#: admin/views.py:636 +msgid "Modifications applied." +msgstr "Úpravy byly uloženy." + +#: admin/views.py:743 +#, python-format +msgid "Object already exists: %s" +msgstr "Objekt již existuje: %s" + +#: admin/views.py:745 +#, python-format +msgid "%d objects imported successfully" +msgstr "%d objektů úspěšně importováno" + +#: admin/views.py:763 +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
    \n" +"
  • account; loginname; password; first name; last name; enabled; group; " +"address[, domain, ...]
  • \n" +"
  • alias; address; enabled; internal recipient
  • \n" +"
  • forward; address; enabled; external recipient
  • \n" +"
  • dlist; address; enabled; recipient; recipient; ...
  • \n" +"
\n" +"

The first element of each line is mandatory and must be equal to one of " +"the previous values.

\n" +"\n" +"

You can use a different character as separator.

\n" +msgstr "" +"Nahrajte CSV soubor, který bude mít jednotlivé řádky v některém z uvedených " +"formátů:\n" +"
    \n" +"
  • account; přihlašovacíjméno; heslo; křestní jméno; příjmení; " +"povoleno; skupina; adresa[, doména, ...]
  • \n" +"
  • alias; adresa; povoleno; interní příjemce
  • \n" +"
  • forward; adresa; povoleno; externí příjemce
  • \n" +"
  • dlist; adresa; povoleno; příjemce; příjemce; ...
  • \n" +"
\n" +"

První element každého řádku je povinný a musí být shodný s některou z " +"předchozích hodnot.

\n" +"\n" +"

Můžete také použít odlišný oddělovač hodnot.

\n" + +#: admin/views.py:775 +msgid "Import identities" +msgstr "Importovat identity" + +#: admin/views.py:776 admin/views.py:846 admin/templatetags/admin_extras.py:87 +#: admin/templatetags/admin_extras.py:144 +msgid "Import" +msgstr "Import" + +#: admin/views.py:806 +msgid "Export identities" +msgstr "Exportovat identity" + +#: admin/views.py:807 admin/views.py:863 admin/templatetags/admin_extras.py:93 +#: admin/templatetags/admin_extras.py:150 +msgid "Export" +msgstr "Export" + +#: admin/views.py:835 +msgid "" +"Provide a CSV file where lines respect one of the following formats:\n" +"
    \n" +"
  • domain; name; quota; enabled
  • \n" +"
  • domainalias; name; targeted domain; enabled
  • \n" +"
\n" +"

The first element of each line is mandatory and must be equal to one of " +"the previous values.

\n" +"

You can use a different character as separator.

\n" +msgstr "" +"Nahrajte CSV soubor, který bude mít jednotlivé řádky v některém z uvedených " +"formátů:\n" +"
    \n" +"
  • domain; název; kvóta; povoleno
  • \n" +"
  • domainalias; název; cílová doména; povoleno
  • \n" +"
\n" +"

První element každého řádku je povinný a musí být shodný s některou z " +"předchozích hodnot.

\n" +"

Můžete také použít odlišný oddělovač hodnot.

\n" + +#: admin/views.py:845 +msgid "Import domains" +msgstr "Importovat domény" + +#: admin/views.py:862 +msgid "Export domains" +msgstr "Exportovat domény" + +#: admin/templates/admin/domain_general_form.html:3 +#: admin/templates/admin/mailform.html:3 +#: admin/templates/admin/newdomainform.html:3 +msgid "MB" +msgstr "MB" + +#: admin/templates/admin/domains.html:2 admin/templatetags/admin_extras.py:23 +#: extensions/limits/app_settings.py:19 extensions/limits/forms.py:16 +#: extensions/limits/models.py:12 +msgid "Domains" +msgstr "Domény" + +#: admin/templates/admin/editdomainform.html:4 +msgid "Administrators" +msgstr "Správci" + +#: admin/templates/admin/editdomainform.html:19 +msgid "No domain administrator defined" +msgstr " Nebyl určen správce domény" + +#: admin/templates/admin/editdomainform.html:22 +msgid "Creation date" +msgstr "Datum vytvoření" + +#: admin/templates/admin/editdomainform.html:23 +msgid "Last modification date" +msgstr "Datum poslední změny" + +#: admin/templates/admin/editdomainform.html:24 +#: extensions/limits/app_settings.py:25 extensions/limits/forms.py:20 +#: extensions/limits/models.py:13 +msgid "Domain aliases" +msgstr "Doménové aliasy" + +#: admin/templates/admin/editdomainform.html:26 +#: extensions/limits/app_settings.py:37 extensions/limits/forms.py:28 +#: extensions/limits/models.py:15 +msgid "Mailbox aliases" +msgstr "Aliasy schránek" + +#: admin/templates/admin/extensions.html:2 +#: admin/templatetags/admin_extras.py:51 +msgid "Extensions" +msgstr "Rozšíření" + +#: admin/templates/admin/extensions.html:2 +msgid "Choose the extension(s) to use" +msgstr "Zvolte rozšíření, která chcete použít" + +#: admin/templates/admin/extensions.html:6 +#: admin/templates/admin/parameters.html:8 +#: userprefs/templates/userprefs/preferences.html:8 +msgid "Save" +msgstr "Uložit" + +#: admin/templates/admin/identities.html:2 +#: admin/templatetags/admin_extras.py:31 +msgid "Identities" +msgstr "Identity" + +#: admin/templates/admin/identities_table.html:6 +msgid "Fullname/Recipient" +msgstr "Celé jméno/Příjemce" + +#: admin/templates/admin/identities_table.html:7 +msgid "Tags" +msgstr "Značky" + +#: admin/templates/admin/importform.html:5 +#: admin/templatetags/admin_extras.py:290 +msgid "Help" +msgstr "Nápověda" + +#: admin/templates/admin/importform.html:12 +msgid "Importing..." +msgstr "Importuji..." + +#: admin/templates/admin/information.html:2 +#: admin/templatetags/admin_extras.py:56 +msgid "Information" +msgstr "Informace" + +#: admin/templates/admin/information.html:2 +msgid "Various information about Modoboa" +msgstr "Různé informace o Modoboa" + +#: admin/templates/admin/information.html:3 +msgid "is currently in use." +msgstr "se aktuálně používá." + +#: admin/templates/admin/information.html:5 +msgid "Active users" +msgstr "Aktivní uživatelé" + +#: admin/templates/admin/information.html:9 +msgid "Group name" +msgstr "Název skupiny" + +#: admin/templates/admin/information.html:10 +msgid "Last login" +msgstr "Poslední přihlášení" + +#: admin/templates/admin/listing.html:14 +#: extensions/stats/templates/stats/index.html:28 +msgid "Filter" +msgstr "Filtr" + +#: admin/templates/admin/logs.html:2 admin/templatetags/admin_extras.py:60 +msgid "Logs" +msgstr "Záznamy" + +#: admin/templates/admin/logs.html:2 +msgid "Previously on Modoboa..." +msgstr "Dříve v Modoboa..." + +#: admin/templates/admin/logs.html:6 extensions/amavis/sql_listing.py:25 +#: extensions/webmail/lib.py:38 extensions/webmail/lib.py:411 +msgid "Date" +msgstr "Datum" + +#: admin/templates/admin/logs.html:7 +msgid "Level" +msgstr "Úroveň" + +#: admin/templates/admin/logs.html:8 +msgid "Logger" +msgstr "Zaznamenal" + +#: admin/templates/admin/logs.html:9 +msgid "Message" +msgstr "Zpráva" + +#: admin/templates/admin/mailboxes_raw.html:3 +msgid "Choose a value" +msgstr "Zvolte hodnotu" + +#: admin/templates/admin/mailform.html:3 +msgid "Use domain default value" +msgstr "Použít výchozí hodnotu domény" + +#: admin/templates/admin/parameters.html:3 +#: admin/templatetags/admin_extras.py:65 +msgid "Parameters" +msgstr "Parametry" + +#: admin/templates/admin/parameters.html:3 +msgid "Configure Modoboa" +msgstr "Nastavit Modoboa" + +#: admin/templates/admin/quotas.html:5 +msgid "Address" +msgstr "Adresa" + +#: admin/templates/admin/quotas.html:6 +msgid "Value" +msgstr "Hodnota" + +#: admin/templates/admin/quotas.html:7 +msgid "Limit" +msgstr "Omezení" + +#: admin/templates/admin/quotas.html:8 +#, python-format +msgid "Usage (%%)" +msgstr "Využití (%%)" + +#: admin/templates/admin/searchform.html:6 +msgid "Search" +msgstr "Hledat" + +#: admin/templates/admin/settings_header.html:3 +msgid "Modoboa settings" +msgstr "Nastavení Modoboa" + +#: admin/templatetags/admin_extras.py:13 +msgid "enabled_m" +msgstr "Aktivovaný" + +#: admin/templatetags/admin_extras.py:13 +msgid "enabled_f" +msgstr "Aktivovaná" + +#: admin/templatetags/admin_extras.py:36 +msgid "Modoboa" +msgstr "Modoboa" + +#: admin/templatetags/admin_extras.py:81 +msgid "Add domain" +msgstr "Přidat doménu" + +#: admin/templatetags/admin_extras.py:110 +msgid "List identities" +msgstr "Seznam identit" + +#: admin/templatetags/admin_extras.py:115 +msgid "List quotas" +msgstr "Seznam kvót" + +#: admin/templatetags/admin_extras.py:120 +msgid "Add account" +msgstr "Přidat účet" + +#: admin/templatetags/admin_extras.py:126 +msgid "Add alias" +msgstr "Přidat alias" + +#: admin/templatetags/admin_extras.py:132 +msgid "Add forward" +msgstr "Přidat přeposílání" + +#: admin/templatetags/admin_extras.py:138 +msgid "Add distribution list" +msgstr "Přidat distribuční seznam" + +#: admin/templatetags/admin_extras.py:171 +msgid "View the domain's identities" +msgstr "Zobrazit seznam identit domény" + +#: admin/templatetags/admin_extras.py:178 +#: admin/templatetags/admin_extras.py:194 +#: admin/templatetags/admin_extras.py:201 +#, python-format +msgid "Delete %s?" +msgstr "Smazat %s?" + +#: admin/templatetags/admin_extras.py:223 +msgid "Remove this permission" +msgstr "Odstranit toto oprávnění" + +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 +msgid "Yes" +msgstr "Ano" + +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 +msgid "No" +msgstr "Ne" + +#: auth/views.py:32 +#, python-format +msgid "User '%s' successfully logged in" +msgstr "Uživatel '%s' se úspěšně přihlásil" + +#: auth/views.py:44 +msgid "Your username and password didn't match. Please try again." +msgstr "" +"Vaše uživatelské jméno nebo heslo se neshodují. Zkuste to prosím znovu." + +#: auth/views.py:67 +#, python-format +msgid "User '%s' logged out" +msgstr "Uživatel '%s' se odhlásil" + +#: demo/__init__.py:17 +msgid "Test messages" +msgstr "Testovací zprávy" + +#: demo/__init__.py:22 +msgid "Send virus" +msgstr "Poslat vir" + +#: demo/__init__.py:25 +msgid "Send spam" +msgstr "Poslat spam" + +#: demo/views.py:17 demo/views.py:41 +msgid "Message sent" +msgstr "Zpráva odeslána" + +#: demo/templates/demo/login_announcement.html:2 +msgid "" +"\n" +"

Welcome to the online demo!

\n" +"

Warning: the demo's concent is resetted every two hours.

\n" +"

To try the demo, the following users are available (username/password):

\n" +"
    \n" +"
  • A super administrator : admin/password
  • \n" +"
  • A domain administrator : admin@demo.local/admin
  • \n" +"
  • A simple user : user@demo.local/user
  • \n" +"
\n" +"

Please report any issue or give any suggestion to \n" +" the " +"mailing list.

\n" +msgstr "" +"\n" +"

Vítejte v online demo verzi!

\n" +"

Upozornění: obsah této demo verze je automaticky obnoven každé 2 " +"hodiny.

\n" +"

Pro vyzkoušení demo verze jsou dostupní tito uživatelé (jméno/heslo):

\n" +"
    \n" +"
  • Super správce : admin/password
  • \n" +"
  • Správce domény: admin@demo.local/admin
  • \n" +"
  • Uživatel: user@demo.local/user
  • \n" +"
\n" +"

Nahlašte prosím jakékoli objevené problémy nebo vaše návrhy přes\n" +" emailový " +"seznam.

\n" + +#: extensions/amavis/__init__.py:22 +msgid "Simple amavis management frontend" +msgstr "Jednoduché rozhraní pro správu amavis" + +#: extensions/amavis/__init__.py:45 extensions/amavis/__init__.py:61 +msgid "Quarantine" +msgstr "Karanténa" + +#: extensions/amavis/__init__.py:129 +msgid "pending requests" +msgstr "nevyřízené požadavky" + +#: extensions/amavis/__init__.py:149 +#, python-format +msgid "%d pending requests" +msgstr "%d nevyřízených požadavků" + +#: extensions/amavis/__init__.py:161 +msgid "Content filter" +msgstr "Filtr obsahu" + +#: extensions/amavis/app_settings.py:11 +msgid "Quarantine settings" +msgstr "Nastavení karantény" + +#: extensions/amavis/app_settings.py:14 +msgid "Maximum message age" +msgstr "Maximální stáří zprávy" + +#: extensions/amavis/app_settings.py:16 +msgid "Quarantine messages maximum age (in days) before deletion" +msgstr "Maximální doba karantény zpráv (ve dnech) před jejich smazáním" + +#: extensions/amavis/app_settings.py:19 +msgid "Messages releasing" +msgstr "Odblokování zpráv" + +#: extensions/amavis/app_settings.py:22 +msgid "Remove released messages" +msgstr "Odstranit odblokované zprávy" + +#: extensions/amavis/app_settings.py:24 +msgid "Remove messages marked as released while cleaning up the database" +msgstr "" +"Odstranit zprávy, které jsou označeny jako odblokované, při čištění databáze" + +#: extensions/amavis/app_settings.py:28 +msgid "Amavis connection mode" +msgstr "Režim připojení amavis" + +#: extensions/amavis/app_settings.py:31 +msgid "Mode used to access the PDP server" +msgstr "Používaný režim pro přístup k PDP serveru" + +#: extensions/amavis/app_settings.py:36 +msgid "PDP server address" +msgstr "adresa PDP serveru" + +#: extensions/amavis/app_settings.py:38 +msgid "PDP server address (if inet mode)" +msgstr "adresa PDP serveru (v režimu inet)" + +#: extensions/amavis/app_settings.py:42 +msgid "PDP server port" +msgstr "port PDP serveru" + +#: extensions/amavis/app_settings.py:44 +msgid "PDP server port (if inet mode)" +msgstr "port PDP serveru (v režimu inet)" + +#: extensions/amavis/app_settings.py:48 +msgid "PDP server socket" +msgstr "socket PDP serveru" + +#: extensions/amavis/app_settings.py:50 +msgid "Path to the PDP server socket (if unix mode)" +msgstr "Cesta k socketu PDP serveru (v režimu unix)" + +#: extensions/amavis/app_settings.py:54 +msgid "Check requests interval" +msgstr "Interval kontroly požadavků" + +#: extensions/amavis/app_settings.py:56 +msgid "Interval between two release requests checks" +msgstr "" +"Časový interval mezi jednotlivými kontrolami na požadavky pro odblokování" + +#: extensions/amavis/app_settings.py:60 +msgid "Allow direct release" +msgstr "Umožnit přímé odbokování" + +#: extensions/amavis/app_settings.py:62 +msgid "Allow users to directly release their messages" +msgstr "Umožnit napřímo uživatelům doblokovat jejich zprávy" + +#: extensions/amavis/app_settings.py:66 +msgid "Enable self-service mode" +msgstr "Povolit samoobslužný režim" + +#: extensions/amavis/app_settings.py:68 +msgid "Activate the 'self-service' mode" +msgstr "Aktivovat samoobslužný režim" + +#: extensions/amavis/app_settings.py:72 +msgid "Notifications sender" +msgstr "Odesílatel upozornění" + +#: extensions/amavis/app_settings.py:74 +msgid "The e-mail address used to send notitications" +msgstr "E-mailová adresa používaná k zasílání upozornění" + +#: extensions/amavis/app_settings.py:87 extensions/webmail/app_settings.py:73 +#: userprefs/app_settings.py:18 +msgid "Display" +msgstr "Zobrazení" + +#: extensions/amavis/app_settings.py:91 extensions/webmail/app_settings.py:91 +msgid "Number of displayed emails per page" +msgstr "Počet zobrazených zpráv na stránce" + +#: extensions/amavis/app_settings.py:92 +msgid "Set the maximum number of messages displayed in a page" +msgstr "Nastavit maximální počet zobrazených zpráv na stránce" + +#: extensions/amavis/lib.py:53 +#, python-format +msgid "Connection to amavis failed: %s" +msgstr "Spojení s amavis selhalo: %s" + +#: extensions/amavis/models.py:93 +msgid "Virus filter" +msgstr "Filtr virů" + +#: extensions/amavis/models.py:94 extensions/amavis/models.py:102 +#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:281 +msgid "yes" +msgstr "ano" + +#: extensions/amavis/models.py:95 extensions/amavis/models.py:103 +#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:285 +msgid "no" +msgstr "ne" + +#: extensions/amavis/models.py:96 extensions/amavis/models.py:104 +#: extensions/amavis/models.py:112 +#: extensions/amavis/templates/amavis/domain_content_filter.html:11 +msgid "default" +msgstr "výchozí" + +#: extensions/amavis/models.py:98 +msgid "Bypass virus checks or not. Choose 'default' to use global settings." +msgstr "" +"Obcházet nebo neobcházet kontrolu zpráv na viry. Pokud zvolíte 'výchozí', " +"bude se nastavení řídit globálním nastavením." + +#: extensions/amavis/models.py:101 +msgid "Spam filter" +msgstr "Filtr nevyžádané pošty" + +#: extensions/amavis/models.py:106 +msgid "Bypass spam checks or not. Choose 'default' to use global settings." +msgstr "" +"Obcházet nebo neobcházet kontrolu zpráv na nevyžádanou poštu. Pokud zvolíte " +"'výchozí', bude se nastavení řídit globálním nastavením." + +#: extensions/amavis/models.py:109 +msgid "Banned filter" +msgstr "Filtr zakázané pošty" + +#: extensions/amavis/models.py:114 +msgid "Bypass banned checks or not. Choose 'default' to use global settings." +msgstr "" +"Obcházet nebo neobcházet kontrolu zpráv na zakázanou poštu. Pokud zvolíte " +"'výchozí', bude se nastavení řídit globálním nastavením." + +#: extensions/amavis/models.py:144 +msgid "Spam marker" +msgstr "Označení spamu" + +#: extensions/amavis/models.py:146 +msgid "" +"Modify spam subject using the specified text. Choose 'default' to use global " +"settings." +msgstr "" +"Upravit předmět nevyžádané zprávy uvedeným textem. Pro zachování globálního " +"nastavení serveru zatrhněte 'výchozí'." + +#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:37 +msgid "From" +msgstr "Od" + +#: extensions/amavis/sql_listing.py:24 extensions/sievefilters/forms.py:68 +#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:36 +#: extensions/webmail/lib.py:409 templates/common/email_searchbar.html:6 +msgid "Subject" +msgstr "Předmět" + +#: extensions/amavis/sql_listing.py:26 +#: extensions/amavis/templatetags/amextras.py:102 +#: extensions/webmail/forms.py:7 +msgid "To" +msgstr "Komu" + +#: extensions/amavis/views.py:43 +msgid "Empty quarantine" +msgstr "Vyprázdnit karanténu" + +#: extensions/amavis/views.py:211 +msgid "Message deleted" +msgstr "Zpráva smazána" + +#: extensions/amavis/views.py:232 +#, python-format +msgid "%(count)d message deleted successfully" +msgid_plural "%(count)d messages deleted successfully" +msgstr[0] "%(count)d zpráva úspěšně smazána" +msgstr[1] "%(count)d zprávy úspěšně smazány" +msgstr[2] "%(count)d zpráv úspěšně smazáno" + +#: extensions/amavis/views.py:252 +msgid "Request sent" +msgstr "Požadavek odeslán" + +#: extensions/amavis/views.py:258 +msgid "Message released" +msgstr "Zprávy odblokována" + +#: extensions/amavis/views.py:280 +#, python-format +msgid "%(count)d request sent" +msgid_plural "%(count)d requests sent" +msgstr[0] "%(count)d požadavek odeslán" +msgstr[1] "%(count)d požadavky odeslány" +msgstr[2] "%(count)d požadavků odesláno" + +#: extensions/amavis/views.py:304 +#, python-format +msgid "%(count)d message released successfully" +msgid_plural "%(count)d messages released successfully" +msgstr[0] "%(count)d zpráva úspěšně odblokována" +msgstr[1] "%(count)d zprávy úspěšně odblokovány" +msgstr[2] "%(count)d zpráv úspěšně odblokováno" + +#: extensions/amavis/management/commands/amnotify.py:57 +msgid "[modoboa] Pending release requests" +msgstr "[modoboa] Nevyřízené požadavky na odblokování" + +#: extensions/amavis/templates/amavis/domain_content_filter.html:5 +msgid "Thresholds" +msgstr "Mezní hodnoty" + +#: extensions/amavis/templates/amavis/domain_content_filter.html:7 +msgid "or more is spam" +msgstr "nebo více je spam" + +#: extensions/amavis/templates/amavis/domain_content_filter.html:8 +msgid "or more throw spam message away" +msgstr "nebo více zahodí spam zprávu" + +#: extensions/amavis/templates/amavis/index.html:2 +msgid "Quarantine management" +msgstr "Nastavení karantény" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:1 +#, python-format +msgid "" +"\n" +"%(counter)s release request is pending for action.\n" +msgid_plural "" +"\n" +"%(counter)s release requests are pending for action.\n" +msgstr[0] "" +"\n" +"%(counter)s požadavek na odblokování čeká na vyřízení.\n" +msgstr[1] "" +"\n" +"%(counter)s požadavky na odblokování čekají na vyřízení.\n" +msgstr[2] "" +"\n" +"%(counter)s požadavků na odblokování čeká na vyřízení.\n" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:6 +msgid "Sketch:" +msgstr "Náhled:" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:8 +msgid "From:" +msgstr "Od:" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:9 +msgid "To:" +msgstr "Komu:" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:10 +msgid "Date:" +msgstr "Datum:" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:11 +msgid "Subject:" +msgstr "Předmět" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:12 +msgid "Act on this message:" +msgstr "Úkony na zprávě:" + +#: extensions/amavis/templates/amavis/notifications/pending_requests.html:14 +#, python-format +msgid "Please visit %(listingurl)s for a full listing." +msgstr "Navštivte stránku %(listingurl)s pro zobrazení kompletního seznamu." + +#: extensions/amavis/templatetags/amextras.py:20 +#: extensions/amavis/templatetags/amextras.py:52 +#: extensions/amavis/templatetags/amextras.py:72 +msgid "Release" +msgstr "Odblokovat" + +#: extensions/amavis/templatetags/amextras.py:26 +#: extensions/amavis/templatetags/amextras.py:59 +#: extensions/amavis/templatetags/amextras.py:77 +#: extensions/webmail/templatetags/webextras.py:31 +msgid "Delete" +msgstr "Smazat" + +#: extensions/amavis/templatetags/amextras.py:29 +msgid "View full headers" +msgstr "Zobrazit úplné záhlaví" + +#: extensions/amavis/templatetags/amextras.py:80 +msgid "Content Class" +msgstr "Třída obsahu" + +#: extensions/amavis/templatetags/amextras.py:84 +msgid "Nothing" +msgstr "Nic" + +#: extensions/amavis/templatetags/amextras.py:87 +msgid "Spam" +msgstr "Spam" + +#: extensions/amavis/templatetags/amextras.py:90 +msgid "Virus" +msgstr "Vir" + +#: extensions/amavis/templatetags/amextras.py:93 +msgid "Bad header" +msgstr "Chybné záhlaví" + +#: extensions/amavis/templatetags/amextras.py:96 +msgid "Bad MIME" +msgstr "Špatný MIME" + +#: extensions/limits/__init__.py:23 +msgid "" +"Per administrator resources to limit the number of objects they can create" +msgstr "" +"Každému správci dostupné zdroje omezující počet objektů, které mohou správci " +"vytvořit" + +#: extensions/limits/__init__.py:54 +msgid "Limits" +msgstr "Omezení" + +#: extensions/limits/__init__.py:65 +msgid "Reseller" +msgstr "Distributor" + +#: extensions/limits/__init__.py:94 +msgid "Resources" +msgstr "Zdroje" + +#: extensions/limits/app_settings.py:10 +msgid "Default limits" +msgstr "Výchozí omezení" + +#: extensions/limits/app_settings.py:13 extensions/limits/forms.py:12 +#: extensions/limits/models.py:11 +msgid "Domain admins" +msgstr "Správci domén" + +#: extensions/limits/app_settings.py:15 +msgid "Maximum number of allowed domain administrators for a new administrator" +msgstr "Maximální povolený počet správců domény (nastavení nového správce)" + +#: extensions/limits/app_settings.py:21 +msgid "Maximum number of allowed domains for a new administrator" +msgstr "Maximální povolený počet domén (nastavení nového správce)" + +#: extensions/limits/app_settings.py:27 +msgid "Maximum number of allowed domain aliases for a new administrator" +msgstr "Maximální povolený počet doménových aliasů (nastavení nového správce)" + +#: extensions/limits/app_settings.py:33 +msgid "Maximum number of allowed mailboxes for a new administrator" +msgstr "Maximální povolený počet schránek (nastavení nového správce)" + +#: extensions/limits/app_settings.py:39 +msgid "Maximum number of allowed aliases for a new administrator" +msgstr "Maximální povolený počet aliasů (nastavení nového správce)" + +#: extensions/limits/forms.py:13 +msgid "Maximum number of domain administrators this user can create" +msgstr "" +"Maximální povolený počet správců domény, které může tento uživatel vytvořit" + +#: extensions/limits/forms.py:17 +msgid "Maximum number of domains this user can create" +msgstr "Maximální povolený počet domén, které může tento uživatel vytvořit" + +#: extensions/limits/forms.py:21 +msgid "Maximum number of domain aliases this user can create" +msgstr "" +"Maximální povolený počet doménových aliasů, které může tento uživatel " +"vytvořit" + +#: extensions/limits/forms.py:25 +msgid "Maximum number of mailboxes this user can create" +msgstr "Maximální povolený počet schránek, které může tento uživatel vytvořit" + +#: extensions/limits/forms.py:29 +msgid "Maximum number of mailbox aliases this user can create" +msgstr "" +"Maximální povolený počet aliasů chránek, které může tento uživatel vytvořit" + +#: extensions/limits/forms.py:46 +msgid "Invalid limit" +msgstr "Neplatné omezení" + +#: extensions/limits/forms.py:74 +msgid "Your resources are not initialized yet" +msgstr "Vaše zdroje nebyly dosud definovány" + +#: extensions/limits/forms.py:78 +msgid "You're not allowed to define unlimited values" +msgstr "Nemáte oprávnění nastavovat neomezené hodnoty" + +#: extensions/limits/lib.py:16 +#, python-format +msgid "%s: limit reached" +msgstr "%s: limit byl dosažen" + +#: extensions/limits/lib.py:23 +msgid "Not enough resources" +msgstr "Nedostatek zdrojů" + +#: extensions/limits/models.py:103 +msgid "undefined" +msgstr "nedefinováno" + +#: extensions/limits/models.py:105 +msgid "unlimited" +msgstr "neomezeno" + +#: extensions/limits/templates/limits/poolusage.html:4 +msgid "Resources usage" +msgstr "Využití zdrojů" + +#: extensions/postfix_autoreply/__init__.py:20 +msgid "Auto-reply (vacation) functionality using Postfix" +msgstr "Funkce automatické odpovědi (vacation) Postfix" + +#: extensions/postfix_autoreply/__init__.py:41 +msgid "Automatic replies" +msgstr "Automatické odpovědi" + +#: extensions/postfix_autoreply/__init__.py:79 +#: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:2 +msgid "Auto-reply message" +msgstr "Automatická odpověď" + +#: extensions/postfix_autoreply/app_settings.py:14 +msgid "Automatic reply timeout" +msgstr "Interval automatické odpovědi" + +#: extensions/postfix_autoreply/app_settings.py:16 +msgid "Timeout in seconds between two auto-replies to the same recipient" +msgstr "" +"Interval v sekundách mezi dvěmi automatickými odpověďmi stejnému příjemci" + +#: extensions/postfix_autoreply/forms.py:9 +msgid "Until" +msgstr "Do" + +#: extensions/postfix_autoreply/forms.py:11 +msgid "Activate your auto reply until this date" +msgstr "Povolit automatické odpovědi až do uvedeného data" + +#: extensions/postfix_autoreply/forms.py:25 +msgid "This date is over" +msgstr "Zvolené datum je v minulosti" + +#: extensions/postfix_autoreply/models.py:19 +msgid "subject" +msgstr "předmět" + +#: extensions/postfix_autoreply/models.py:20 +msgid "The subject that will appear in sent emails" +msgstr "Předmět, který se vloží do odesílaných emailů" + +#: extensions/postfix_autoreply/models.py:23 +msgid "content" +msgstr "obsah" + +#: extensions/postfix_autoreply/models.py:24 +msgid "The content that will appear in sent emails" +msgstr "Obsah, který se vloží do odesílaných emailů" + +#: extensions/postfix_autoreply/models.py:28 +msgid "Activate/Deactivate your auto reply" +msgstr "Povolit/Zakázat automatickou odpověď" + +#: extensions/postfix_autoreply/views.py:37 +msgid "Auto reply message updated successfully." +msgstr "Automatická odpověď byla aktualizována." + +#: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:2 +msgid "Define a message to automatically send when you are off" +msgstr "Vložte zprávu, která se během vaší nepřítomnosti automaticky odešle" + +#: extensions/sievefilters/__init__.py:12 +msgid "Plugin to easily create server-side filters" +msgstr "Doplněk pro jednoduchou tvorbu filtrů zpráv na serveru" + +#: extensions/sievefilters/__init__.py:17 +msgid "Sieve filters" +msgstr "Sieve filtry" + +#: extensions/sievefilters/__init__.py:18 +#: extensions/sievefilters/__init__.py:36 +#: extensions/sievefilters/templates/sievefilters/index.html:2 +msgid "Message filters" +msgstr "Filtry zpráv" + +#: extensions/sievefilters/app_settings.py:19 +msgid "ManageSieve settings" +msgstr "Nastavení ManageSieve" + +#: extensions/sievefilters/app_settings.py:24 +msgid "Address of your MANAGESIEVE server" +msgstr "Adresa vašeho serveru ManegeSieve" + +#: extensions/sievefilters/app_settings.py:30 +msgid "Listening port of your MANAGESIEVE server" +msgstr "Port, na kterém naslouchá váš ManageSieve server" + +#: extensions/sievefilters/app_settings.py:34 +msgid "Connect using STARTTLS" +msgstr "Připojit pomocí STARTTLS" + +#: extensions/sievefilters/app_settings.py:36 +msgid "Use the STARTTLS extension" +msgstr "Použít rozšíření STARTTLS" + +#: extensions/sievefilters/app_settings.py:40 +msgid "Authentication mechanism" +msgstr "Režim ověření" + +#: extensions/sievefilters/app_settings.py:43 +msgid "Prefered authentication mechanism" +msgstr "Preferovaný režim ověření" + +#: extensions/sievefilters/app_settings.py:54 +msgid "Editor mode" +msgstr "Režim úpravy" + +#: extensions/sievefilters/app_settings.py:56 +msgid "Select the mode you want the editor to work in" +msgstr "Zvolte režim editoru textu, ve kterém si přejete pracovat" + +#: extensions/sievefilters/forms.py:14 +msgid "Check to activate this filters set" +msgstr "Zatrhněte pole pro povolení této sady filtrů" + +#: extensions/sievefilters/forms.py:55 +msgid "All of the following" +msgstr "Všechna pravidla" + +#: extensions/sievefilters/forms.py:56 +msgid "Any of the following" +msgstr "Alespoň jedno z pravidel" + +#: extensions/sievefilters/forms.py:57 +msgid "All messages" +msgstr "Všechny zprávy" + +#: extensions/sievefilters/forms.py:62 +msgid "contains" +msgstr "obsahuje" + +#: extensions/sievefilters/forms.py:63 +msgid "does not contain" +msgstr "neobsahuje" + +#: extensions/sievefilters/forms.py:64 +msgid "is" +msgstr "je" + +#: extensions/sievefilters/forms.py:65 +msgid "is not" +msgstr "není" + +#: extensions/sievefilters/forms.py:69 +msgid "Sender" +msgstr "Odesílatel" + +#: extensions/sievefilters/forms.py:70 +msgid "Recipient" +msgstr "Příjemce" + +#: extensions/sievefilters/forms.py:71 extensions/webmail/forms.py:8 +msgid "Cc" +msgstr "Kopie" + +#: extensions/sievefilters/forms.py:72 +msgid "Size" +msgstr "Velikost" + +#: extensions/sievefilters/forms.py:73 +msgid "is greater than" +msgstr "Je větší než" + +#: extensions/sievefilters/forms.py:74 +msgid "is less than" +msgstr "Je menší než" + +#: extensions/sievefilters/forms.py:78 +msgid "Move message to" +msgstr "Přesunout zprávu do" + +#: extensions/sievefilters/forms.py:80 +msgid "Redirect message to" +msgstr "Přesměrovat zprávu na" + +#: extensions/sievefilters/lib.py:37 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "Spojení se serverem ManageSieve selhalo, zkontrolujte nastavení" + +#: extensions/sievefilters/lib.py:82 +msgid "Not enough space on server" +msgstr "Nedostatek prostoru na serveru" + +#: extensions/sievefilters/views.py:68 +msgid "Failed to retrieve filters set" +msgstr "Nebylo možné získat sadu filtrů" + +#: extensions/sievefilters/views.py:126 +#: extensions/sievefilters/templatetags/sfilters_tags.py:33 +msgid "New filter" +msgstr "Nový filtr" + +#: extensions/sievefilters/views.py:133 +msgid "Filter created" +msgstr "Filtr vytvořen" + +#: extensions/sievefilters/views.py:145 +#: extensions/sievefilters/templatetags/sfilters_tags.py:77 +msgid "Edit filter" +msgstr "Upravit filtr" + +#: extensions/sievefilters/views.py:154 +msgid "Filter modified" +msgstr "Filtr upraven" + +#: extensions/sievefilters/views.py:176 +msgid "Filter removed" +msgstr "Filtr odstraněn" + +#: extensions/sievefilters/views.py:177 +msgid "Failed to remove filter" +msgstr "Nezdařilo se odstranit filtr" + +#: extensions/sievefilters/views.py:191 +msgid "Filters set saved" +msgstr "Sada filtrů uložena" + +#: extensions/sievefilters/views.py:196 +msgid "Create a new filters set" +msgstr "Vytvořit novou sadu filtrů" + +#: extensions/sievefilters/views.py:219 +msgid "Filters set created" +msgstr "Sada filtrů vytvořena" + +#: extensions/sievefilters/views.py:240 +msgid "Filters set deleted" +msgstr "Sada filtrů vymazána" + +#: extensions/sievefilters/views.py:252 +msgid "Filters set activated" +msgstr "Sada filtrů povolena" + +#: extensions/sievefilters/templates/sievefilters/filter.html:6 +msgid "Conditions" +msgstr "Podmínky" + +#: extensions/sievefilters/templates/sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Aktivní" + +#: extensions/sievefilters/templates/sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Dosud nebyl vytvořen žádný filtr" + +#: extensions/sievefilters/templates/sievefilters/index.html:56 +msgid "Filter sets" +msgstr "Sady filtrů" + +#: extensions/sievefilters/templates/sievefilters/index.html:60 +msgid "active" +msgstr "aktivní" + +#: extensions/sievefilters/templates/sievefilters/index.html:74 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "" +"Dosud nebyla vytvořena žádná sada filtrů. Aby bylo možné vytvořit filtry, je " +"nejprve potřeba vytvořit sadu filtrů." + +#: extensions/sievefilters/templatetags/sfilters_tags.py:16 +msgid "New filters set" +msgstr "Nová sada filtrů" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:44 +msgid "Save filters set" +msgstr "Uložit sadu filtrů" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:52 +msgid "Activate filters set" +msgstr "Aktitovat sadu filtrů" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:57 +msgid "Remove filters set" +msgstr "Odstranit sadu filtrů" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:62 +msgid "Download" +msgstr "Stáhnout" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:85 +msgid "Remove this filter" +msgstr "Odstranit tento filtr" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:93 +msgid "Move this filter up" +msgstr "Posunout tento filtr nahoru" + +#: extensions/sievefilters/templatetags/sfilters_tags.py:101 +msgid "Move this filter down" +msgstr "Posunout tento filtr dolů" + +#: extensions/stats/__init__.py:20 +msgid "Graphical statistics about emails traffic using RRDtool" +msgstr "Grafické statistiky emailového provozu s použitím RRDtool" + +#: extensions/stats/__init__.py:26 +#: extensions/stats/templates/stats/index.html:2 +msgid "Graphical statistics" +msgstr "Grafické statistiky" + +#: extensions/stats/__init__.py:40 +msgid "Statistics" +msgstr "Statistiky" + +#: extensions/stats/app_settings.py:14 +msgid "Path to the log file" +msgstr "Cesta k souboru záznamů" + +#: extensions/stats/app_settings.py:16 +msgid "Path to log file used to collect statistics" +msgstr "" +"Cesta k souboru záznamů, který se používá pro sběr statistických informací" + +#: extensions/stats/app_settings.py:20 +msgid "Directory to store RRD files" +msgstr "Adresář pro ukládání RRD souborů" + +#: extensions/stats/app_settings.py:22 +msgid "Path to directory where RRD files are stored" +msgstr "Cesta k adresáři, ve kterém budou ukládány RRD soubory" + +#: extensions/stats/app_settings.py:26 +msgid "Directory to store PNG files" +msgstr "Adresář pro ukládání PNG souborů" + +#: extensions/stats/app_settings.py:28 +msgid "Path to directory where PNG files are stored" +msgstr "Cesta k adresáři, ve kterém budou ukládány PNG soubory" + +#: extensions/stats/graph_templates.py:20 +msgid "Average normal traffic" +msgstr "Průměrný běžný provoz" + +#: extensions/stats/graph_templates.py:21 +#: extensions/stats/graph_templates.py:33 +msgid "msgs/min" +msgstr "zpráv/min" + +#: extensions/stats/graph_templates.py:26 +msgid "sent messages" +msgstr "odeslané zprávy" + +#: extensions/stats/graph_templates.py:28 +msgid "received messages" +msgstr "přijaté zprávy" + +#: extensions/stats/graph_templates.py:32 +msgid "Average bad traffic" +msgstr "Průměrný závadný provoz" + +#: extensions/stats/graph_templates.py:38 +msgid "bounced messages" +msgstr "nedoručené zprávy" + +#: extensions/stats/graph_templates.py:40 +msgid "rejected messages" +msgstr "odmítnuté zprávy" + +#: extensions/stats/graph_templates.py:44 +msgid "Average normal traffic size" +msgstr "Průměrná velikost běžného provozu" + +#: extensions/stats/graph_templates.py:45 +msgid "bytes/min" +msgstr "Bajtů/min" + +#: extensions/stats/graph_templates.py:50 +msgid "received size" +msgstr "doručená velikost" + +#: extensions/stats/graph_templates.py:52 +msgid "sent size" +msgstr "odeslaná velikost" + +#: extensions/stats/graph_templates.py:73 +msgid "Mail traffic" +msgstr "Provoz emailů" + +#: extensions/stats/grapher.py:9 +msgid "Day" +msgstr "Den" + +#: extensions/stats/grapher.py:10 +msgid "Week" +msgstr "Týden" + +#: extensions/stats/grapher.py:11 +msgid "Month" +msgstr "Měsíc" + +#: extensions/stats/grapher.py:12 +msgid "Year" +msgstr "Rok" + +#: extensions/stats/grapher.py:59 +msgid "Total" +msgstr "Celkem" + +#: extensions/stats/views.py:21 extensions/stats/templates/stats/graphs.html:4 +msgid "No statistics available" +msgstr "Žádné statistiky nejsou dostupné" + +#: extensions/stats/views.py:39 +msgid "Unknown graphic set" +msgstr "Nastaveno neznámé zobrazení" + +#: extensions/stats/views.py:61 +msgid "Bad custom period" +msgstr "Neplatně nastavený vlastní časový interval" + +#: extensions/stats/templates/stats/index.html:33 +msgid "Search a domain" +msgstr "Hledat doménu" + +#: extensions/stats/templates/stats/index.html:37 +msgid "Predefined time ranges" +msgstr "Předdefinované časové intervaly" + +#: extensions/stats/templates/stats/index.html:44 +msgid "Custom time range" +msgstr "Vlastní časový interval" + +#: extensions/stats/templates/stats/index.html:55 +msgid "Apply" +msgstr "Uložit" + +#: extensions/webmail/__init__.py:12 +msgid "Simple IMAP webmail" +msgstr "Jednoduchý IMAP webmail" + +#: extensions/webmail/__init__.py:35 +msgid "Webmail" +msgstr "Webmail" + +#: extensions/webmail/app_settings.py:14 +msgid "Maximum attachment size" +msgstr "Maximální velikost přílohy" + +#: extensions/webmail/app_settings.py:16 +msgid "Maximum attachment size in bytes (or KB, MB, GB if specified)" +msgstr "" +"Maximální velikost přílohy v Bajtech (nebo KB, MB, GB, pokud je uvedeno)" + +#: extensions/webmail/app_settings.py:19 +msgid "IMAP settings" +msgstr "Nastavení IMAP" + +#: extensions/webmail/app_settings.py:24 +msgid "Address of your IMAP server" +msgstr "Adresa vašeho IMAP serveru" + +#: extensions/webmail/app_settings.py:30 +msgid "Use a secured connection to access IMAP server" +msgstr "Použít zabezpečené připojení pro přístup k IMAP serveru" + +#: extensions/webmail/app_settings.py:36 +msgid "Listening port of your IMAP server" +msgstr "Port, na kterém naslouchá váš IMAP server" + +#: extensions/webmail/app_settings.py:39 +msgid "SMTP settings" +msgstr "Nastavení SMTP" + +#: extensions/webmail/app_settings.py:44 +msgid "Address of your SMTP server" +msgstr "Adresa vašeho SMTP serveru" + +#: extensions/webmail/app_settings.py:48 +msgid "Secured connection mode" +msgstr "Režim zabezpečeného připojení" + +#: extensions/webmail/app_settings.py:49 +msgid "None" +msgstr "Nic" + +#: extensions/webmail/app_settings.py:53 +msgid "Use a secured connection to access SMTP server" +msgstr "Použít zabezpečené připojení pro přístup k SMTP serveru" + +#: extensions/webmail/app_settings.py:60 +msgid "Listening port of your SMTP server" +msgstr "Port, na kterém naslouchá váš SMTP server" + +#: extensions/webmail/app_settings.py:64 +msgid "Authentication required" +msgstr "Ověření vyžadováno" + +#: extensions/webmail/app_settings.py:66 +msgid "Server needs authentication" +msgstr "Server vyžaduje ověření" + +#: extensions/webmail/app_settings.py:77 +msgid "Default message display mode" +msgstr "Výchozí režim zobrazení zprávy" + +#: extensions/webmail/app_settings.py:79 +msgid "The default mode used when displaying a message" +msgstr "Výchozí režim, který se použije pro zobrazení zprávy" + +#: extensions/webmail/app_settings.py:85 +msgid "Enable HTML links display" +msgstr "Povolit zobrazení HTML odkazů" + +#: extensions/webmail/app_settings.py:86 +msgid "Enable/Disable HTML links display" +msgstr "Povolit/Zakázat zobrazení HTML odkazů" + +#: extensions/webmail/app_settings.py:92 +msgid "Sets the maximum number of messages displayed in a page" +msgstr "Nastavuje maximální počet zobrazených zpráv na stránce" + +#: extensions/webmail/app_settings.py:97 +msgid "Listing refresh rate" +msgstr "Automatické obnovení stránky" + +#: extensions/webmail/app_settings.py:98 +msgid "Automatic folder refresh rate (in seconds)" +msgstr "Automatické načítání obsahu složky (v sekundách)" + +#: extensions/webmail/app_settings.py:105 +msgid "Trash folder" +msgstr "Složka koše" + +#: extensions/webmail/app_settings.py:106 +msgid "Folder where deleted messages go" +msgstr "Složka pro smazané zprávy" + +#: extensions/webmail/app_settings.py:111 +msgid "Sent folder" +msgstr "Složka odeslané pošty" + +#: extensions/webmail/app_settings.py:112 +msgid "Folder where copies of sent messages go" +msgstr "Složka pro kopie odeslaných zpráv" + +#: extensions/webmail/app_settings.py:117 +msgid "Drafts folder" +msgstr "Složka konceptů" + +#: extensions/webmail/app_settings.py:118 +msgid "Folder where drafts go" +msgstr "Složka pro koncepty zpráv" + +#: extensions/webmail/app_settings.py:121 +msgid "Composing messages" +msgstr "Vytváření zpráv" + +#: extensions/webmail/app_settings.py:125 +msgid "Default editor" +msgstr "Výchozí editor" + +#: extensions/webmail/app_settings.py:127 +msgid "The default editor to use when composing a message" +msgstr "Výchozí editor textu, který se použije pro vytváření zpráv" + +#: extensions/webmail/app_settings.py:133 +msgid "Signature text" +msgstr "Text podpisu" + +#: extensions/webmail/app_settings.py:134 +msgid "User defined email signature" +msgstr "Emailový podpis uživatele" + +#: extensions/webmail/exceptions.py:19 +msgid "Server response" +msgstr "Odpověď serveru" + +#: extensions/webmail/forms.py:9 +msgid "Cci" +msgstr "BCC" + +#: extensions/webmail/imaputils.py:237 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Nebylo možné zjistit hierarchický oddělovač" + +#: extensions/webmail/imaputils.py:278 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "Spojení se serverem IMAP selhalo: %s" + +#: extensions/webmail/lib.py:358 +msgid "wrote:" +msgstr "napsal/a:" + +#: extensions/webmail/lib.py:423 extensions/webmail/lib.py:427 +msgid "Original message" +msgstr "Původní zpráva" + +#: extensions/webmail/views.py:86 +#, python-format +msgid "%(count)d message deleted" +msgid_plural "%(count)d messages deleted" +msgstr[0] "%(count)d zpráva smazána" +msgstr[1] "%(count)d zprávy smazány" +msgstr[2] "%(count)d zpráv smazáno" + +#: extensions/webmail/views.py:103 +msgid "Unknown action" +msgstr "Neznámá akce" + +#: extensions/webmail/views.py:116 lib/email_listing.py:137 +msgid "Empty mailbox" +msgstr "Vyprázdnit schránku" + +#: extensions/webmail/views.py:133 +#: extensions/webmail/templatetags/webextras.py:170 +msgid "Create a new mailbox" +msgstr "Vytvořit novou schránku" + +#: extensions/webmail/views.py:149 +msgid "Mailbox created" +msgstr "Schránka vytvořena" + +#: extensions/webmail/views.py:165 +msgid "Edit mailbox" +msgstr "Upravit schránku" + +#: extensions/webmail/views.py:180 +msgid "Mailbox updated" +msgstr "Schránka aktualizována" + +#: extensions/webmail/views.py:214 extensions/webmail/views.py:396 +#: extensions/webmail/views.py:505 +msgid "Bad request" +msgstr "Neplatný požadavek" + +#: extensions/webmail/views.py:246 +msgid "Failed to save attachment: " +msgstr "Nebylo možné uložit přílohu:" + +#: extensions/webmail/views.py:249 +#, python-format +msgid "Attachment is too big (limit: %s)" +msgstr "Příloha je příliš velká (omezení: %s)" + +#: extensions/webmail/views.py:255 +#: extensions/webmail/templates/webmail/compose.html:15 +#: extensions/webmail/templates/webmail/headers.html:4 +msgid "Attachments" +msgstr "Přílohy" + +#: extensions/webmail/views.py:271 +msgid "Bad query" +msgstr "Neplatný dotaz" + +#: extensions/webmail/views.py:281 +msgid "Failed to remove attachment: " +msgstr "Nebylo možné odstranit přílohu:" + +#: extensions/webmail/views.py:286 +msgid "Unknown attachment" +msgstr "Neznámá příloha" + +#: extensions/webmail/views.py:501 +msgid "Undefined action" +msgstr "Neznámá akce" + +#: extensions/webmail/templates/webmail/attachments.html:6 +msgid "Attach" +msgstr "Připojit" + +#: extensions/webmail/templates/webmail/attachments.html:9 +msgid "Uploading.." +msgstr "Nahrávám.." + +#: extensions/webmail/templates/webmail/compose.html:7 +msgid "Add Cc field" +msgstr "Přidat pole CC" + +#: extensions/webmail/templates/webmail/compose.html:7 +msgid "Add Cci field" +msgstr "Přidat pole BCC" + +#: extensions/webmail/templates/webmail/compose.html:18 +#, python-format +msgid "%(counter)s file" +msgid_plural "%(counter)s files" +msgstr[0] "%(counter)s soubor" +msgstr[1] "%(counter)s soubory" +msgstr[2] "%(counter)s souborů" + +#: extensions/webmail/templates/webmail/folder.html:6 +msgid "Parent mailbox" +msgstr "Nadřazená schránka" + +#: extensions/webmail/templates/webmail/index.html:38 +msgid "Mailbox quota" +msgstr "Kvóta schránky" + +#: extensions/webmail/templatetags/webextras.py:19 +msgid "Reply" +msgstr "Odpovědět" + +#: extensions/webmail/templatetags/webextras.py:23 +msgid "Reply all" +msgstr "Odpovědět všem" + +#: extensions/webmail/templatetags/webextras.py:27 +#: userprefs/templates/userprefs/forward.html:2 +#: userprefs/templatetags/uprefs_extras.py:27 +msgid "Forward" +msgstr "Přeposlat" + +#: extensions/webmail/templatetags/webextras.py:33 +msgid "Display options" +msgstr "Možnosti zobrazení" + +#: extensions/webmail/templatetags/webextras.py:36 +msgid "Activate links" +msgstr "Povolit odkazy" + +#: extensions/webmail/templatetags/webextras.py:38 +msgid "Disable links" +msgstr "Zakákaz odkazy" + +#: extensions/webmail/templatetags/webextras.py:47 +msgid "Close this message" +msgstr "Zavřít tuto zprávu" + +#: extensions/webmail/templatetags/webextras.py:60 +msgid "Back" +msgstr "Zpět" + +#: extensions/webmail/templatetags/webextras.py:64 +msgid "Send" +msgstr "Odeslat" + +#: extensions/webmail/templatetags/webextras.py:76 +msgid "New message" +msgstr "Nová zpráva" + +#: extensions/webmail/templatetags/webextras.py:85 +msgid "Mark messages" +msgstr "Označit zprávy" + +#: extensions/webmail/templatetags/webextras.py:89 +msgid "As read" +msgstr "Jako přečtené" + +#: extensions/webmail/templatetags/webextras.py:92 +msgid "As unread" +msgstr "Jako nepřečtené" + +#: extensions/webmail/templatetags/webextras.py:101 +msgid "Compress folder" +msgstr "Stlačit složku" + +#: extensions/webmail/templatetags/webextras.py:110 +msgid "Empty folder" +msgstr "Vyprázdnit složku" + +#: extensions/webmail/templatetags/webextras.py:178 +msgid "Edit the selected mailbox" +msgstr "Upravit vybranou schránku" + +#: extensions/webmail/templatetags/webextras.py:183 +msgid "Remove the selected mailbox" +msgstr "Odstranit označené schránky" + +#: lib/exceptions.py:18 +#, python-format +msgid "Permission denied: %s" +msgstr "Přístup zamítnut: %s" + +#: lib/exceptions.py:19 +msgid "Permission denied" +msgstr "Přístup zamítnut" + +#: lib/formutils.py:157 lib/formutils.py:165 +msgid "Enter a valid domain name" +msgstr "Vložte platné doménové jméno" + +#: lib/permissions.py:27 +msgid "Simple user" +msgstr "Uživatel" + +#: lib/permissions.py:29 +msgid "Super administrator" +msgstr "Super správce" + +#: lib/permissions.py:31 +msgid "Domain administrator" +msgstr "Správce domény" + +#: lib/permissions.py:75 +#, python-format +msgid "Failed to grant access (%s)" +msgstr "Nebylo možné povolit přístup (%s)" + +#: lib/tables.py:241 +msgid "No entries to display" +msgstr "Žádné položky k zobrazení" + +#: lib/templatetags/libextras.py:108 +msgid "activate" +msgstr "aktivovat" + +#: templates/404.html:4 templates/500.html:4 +msgid "Sorry" +msgstr "Omlouváme se" + +#: templates/404.html:5 +msgid "The requested page does not exist." +msgstr "Požadovaná stránka neexistuje." + +#: templates/500.html:5 +msgid "An internal error occured." +msgstr "Objevila se interní chyba." + +#: templates/nlayout.html:62 +msgid "Powered by Modoboa" +msgstr "Běží na systému Modoboa" + +#: templates/common/email_searchbar.html:5 +msgid "From address" +msgstr "Z adresy" + +#: templates/common/email_searchbar.html:10 +msgid "Both" +msgstr "Obojí" + +#: templates/common/email_searchbar.html:12 +msgid "Search..." +msgstr "Hledat..." + +#: templates/common/error.html:4 +msgid "Error:" +msgstr "Chyba:" + +#: templates/common/generic_field.html:3 +#: templates/common/generic_fields_group.html:3 +msgid "Help:" +msgstr "Nápověda:" + +#: templates/common/generic_modal.html:11 +#: templates/common/wizard_forms.html:29 +msgid "Close" +msgstr "Zavřít" + +#: templates/registration/login.html:5 +msgid "Welcome to Modoboa" +msgstr "Vítejte v Modoboa" + +#: templates/registration/login.html:34 +msgid "Remember me" +msgstr "Zapamatovat si" + +#: templates/registration/login.html:36 +msgid "Log in" +msgstr "Přihlásit" + +#: userprefs/__init__.py:8 +msgid "User preferences" +msgstr "Uživatelské možnosti" + +#: userprefs/__init__.py:10 +msgid "Per-user settings" +msgstr "Uživatelské nastavení" + +#: userprefs/app_settings.py:22 +msgid "Prefered language" +msgstr "Preferovaný jazyk" + +#: userprefs/app_settings.py:27 +msgid "Prefered language to display pages" +msgstr "Preferovaný jazyk pro zobrazení stránek" + +#: userprefs/forms.py:14 +msgid "Old password" +msgstr "Původní heslo" + +#: userprefs/forms.py:16 +msgid "New password" +msgstr "Nové heslo" + +#: userprefs/forms.py:40 +msgid "Old password mismatchs" +msgstr "Původní heslo se neshoduje" + +#: userprefs/forms.py:45 +msgid "Passwords mismatch" +msgstr "Hesla se neshodují" + +#: userprefs/forms.py:59 +msgid "Recipient(s)" +msgstr "Příjemce(i)" + +#: userprefs/forms.py:62 +msgid "Indicate one or more recipients separated by a ','" +msgstr "Uveďte jednoho nebo více příjemců oddělených pomocí ','" + +#: userprefs/forms.py:65 +msgid "Keep local copies" +msgstr "Ponechat místní kopie" + +#: userprefs/forms.py:67 +msgid "Forward messages and store copies into your local mailbox" +msgstr "Přeposílat zprávy a ukládat kopie do místní schránky" + +#: userprefs/forms.py:84 +msgid "" +"You can't define a forward to a local destination. Please ask your " +"administrator to create an alias instead." +msgstr "" +"Nemůžete nastavit přeposílání na místní adresu. Požádejte správce o " +"vytvoření aliasu." + +#: userprefs/views.py:32 +msgid "You need a mailbox in order to define a forward" +msgstr "Pro nastavení přeposílání nejprve potřebujete schránku" + +#: userprefs/views.py:56 +msgid "Forward updated" +msgstr "Přeposílání aktualizováno" + +#: userprefs/views.py:98 +msgid "Profile updated" +msgstr "Profil aktualizován" + +#: userprefs/views.py:127 +msgid "Preferences saved" +msgstr "Možnosti uloženy" + +#: userprefs/templates/userprefs/forward.html:2 +msgid "Automatically forward emails to another address" +msgstr "Automaticky přesměrovat emaily na jinou adresu" + +#: userprefs/templates/userprefs/index.html:2 +#: userprefs/templatetags/uprefs_extras.py:48 +msgid "Settings" +msgstr "Nastavení" + +#: userprefs/templates/userprefs/preferences.html:2 +#: userprefs/templatetags/uprefs_extras.py:21 +msgid "Preferences" +msgstr "Možnosti" + +#: userprefs/templates/userprefs/preferences.html:2 +msgid "Customize Modoboa" +msgstr "Nastavit Modoboa" + +#: userprefs/templates/userprefs/profile.html:2 +#: userprefs/templatetags/uprefs_extras.py:17 +msgid "Profile" +msgstr "Profil" + +#: userprefs/templates/userprefs/profile.html:2 +msgid "Update your personal information" +msgstr "Aktualizovat osobní informace" + +#: userprefs/templatetags/uprefs_extras.py:57 +msgid "Logout" +msgstr "Odhlásit se" diff --git a/modoboa/locale/cs_CZ/LC_MESSAGES/djangojs.po b/modoboa/locale/cs_CZ/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..473badd33 --- /dev/null +++ b/modoboa/locale/cs_CZ/LC_MESSAGES/djangojs.po @@ -0,0 +1,165 @@ +# Modoboa Czech translation +# Copyright (C) 2013 +# This file is distributed under the same license as the Modoboa package. +# +# Miroslav Abrahám , 2013. +msgid "" +msgstr "" +"Project-Id-Version: modoboa-1.0.0\n" +"Report-Msgid-Bugs-To: miris@kunago.com\n" +"POT-Creation-Date: 2013-09-20 22:30+0200\n" +"PO-Revision-Date: 2013-09-29 22:13+0100\n" +"Last-Translator: Miroslav Abrahám \n" +"Language-Team: CS \n" +"Language: Čeština\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Poedit 1.5.7\n" + +#: admin/static/admin/js/admin.js:26 +msgid "Search" +msgstr "Hledat" + +#: admin/static/admin/js/admin.js:110 +msgid "Do not delete domain directory" +msgstr "Nemazat adresář domény" + +#: admin/static/admin/js/admin.js:113 +msgid "This operation will remove ALL data associated to this domain." +msgstr "Tento krok odstraní VŠECHNA data související s touto doménou." + +#: admin/static/admin/js/admin.js:114 +msgid "" +"This operation will remove all data associated to this domain, excepting " +"accounts." +msgstr "" +"Tento krok odstraní VŠECHNA data související s touto doménou, vyjma účtů." + +#: admin/static/admin/js/admin.js:117 +msgid "Delete this domain?" +msgstr "Smazat tuto doménu?" + +#: admin/static/admin/js/admin.js:161 +msgid "No domain administrator defined" +msgstr "Není určen správce domény" + +#: admin/static/admin/js/admin.js:238 +msgid "Do not delete mailbox directory" +msgstr "Nemazat adresář schránky" + +#: admin/static/admin/js/admin.js:243 +msgid "Delete this account?" +msgstr "Smazat tento účet?" + +#: admin/static/admin/js/admin.js:249 +msgid "Delete this list?" +msgstr "Smazat tento seznam?" + +#: admin/static/admin/js/admin.js:254 +msgid "Delete this forward?" +msgstr "Smazat toto přeposílání?" + +#: admin/static/admin/js/admin.js:259 +msgid "Delete this alias?" +msgstr "Smazat tento alias?" + +#: extensions/amavis/static/amavis/js/quarantine.js:126 +msgid "Release this selection?" +msgstr "Odblokovat tento výběr?" + +#: extensions/amavis/static/amavis/js/quarantine.js:130 +msgid "Delete this selection?" +msgstr "Smazat tento výběr?" + +#: extensions/amavis/static/amavis/js/quarantine.js:148 +#: extensions/amavis/static/amavis/js/selfservice.js:22 +msgid "Release this message?" +msgstr "Odblokovat tuto zprávu?" + +#: extensions/amavis/static/amavis/js/quarantine.js:152 +#: extensions/amavis/static/amavis/js/selfservice.js:26 +msgid "Delete this message?" +msgstr "Smazat tuto zprávu?" + +#: extensions/amavis/static/amavis/js/quarantine.js:169 +msgid "View full headers" +msgstr "Zobrazit úplné záhlaví" + +#: extensions/amavis/static/amavis/js/quarantine.js:174 +msgid "Hide full headers" +msgstr "Skrýt záhlaví" + +#: extensions/sievefilters/static/sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "Odstranit tento filtr?" + +#: extensions/sievefilters/static/sievefilters/js/lib.js:117 +msgid "Save changes?" +msgstr "Uložit změny?" + +#: extensions/sievefilters/static/sievefilters/js/lib.js:144 +msgid "Remove filters set?" +msgstr "Smazat sadu filtrů?" + +#: extensions/sievefilters/static/sievefilters/js/lib.js:161 +msgid "Activate this filters set?" +msgstr "Aktivovat tuto sadu filtrů?" + +#: extensions/sievefilters/static/sievefilters/js/lib.js:169 +#: extensions/sievefilters/static/sievefilters/js/lib.js:224 +msgid "active" +msgstr "aktivní" + +#: extensions/stats/static/stats/js/stats.js:37 +msgid "Search a domain" +msgstr "Vyhledat doménu" + +#: extensions/webmail/static/webmail/js/webmail.js:485 +msgid "Remove the selected mailbox?" +msgstr "Odstranit vybranou schránku?" + +#: extensions/webmail/static/webmail/js/webmail.js:499 +msgid "Mailbox removed" +msgstr "Schránka odstraněna" + +#: extensions/webmail/static/webmail/js/webmail.js:823 +msgid "Message sent" +msgstr "Zpráva odeslána" + +#: extensions/webmail/static/webmail/js/webmail.js:856 +msgid "Message deleted" +msgstr "Zpráva smazána" + +#: extensions/webmail/static/webmail/js/webmail.js:981 +#, c-format +msgid "%s file" +msgid_plural "%s files" +msgstr[0] "%s soubor" +msgstr[1] "%s soubory" +msgstr[2] "%s souborů" + +#: extensions/webmail/static/webmail/js/webmail.js:1013 +#, c-format +msgid "Moving %s message" +msgid_plural "Moving %s messages" +msgstr[0] "Přesunuji %s zprávu" +msgstr[1] "Přesunuji %s zprávy" +msgstr[2] "Přesunuji %s zpráv" + +#: static/js/confirmation.js:48 +msgid "Warning" +msgstr "Upozornění" + +#: static/js/confirmation.js:57 +msgid "Cancel" +msgstr "Zrušit" + +#: static/js/confirmation.js:99 static/js/history.js:239 +msgid "Internal Error" +msgstr "Interní chyba" + +#: static/js/searchbar.js:20 +msgid "Search..." +msgstr "Hledat..." diff --git a/modoboa/locale/de/LC_MESSAGES/django.po b/modoboa/locale/de/LC_MESSAGES/django.po index 588099c21..f79002853 100644 --- a/modoboa/locale/de/LC_MESSAGES/django.po +++ b/modoboa/locale/de/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Modoboa-0.9\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-06-01 16:35+0200\n" -"PO-Revision-Date: 2013-05-24 22:37+0100\n" +"POT-Creation-Date: 2013-09-20 22:30+0200\n" +"PO-Revision-Date: 2013-10-02 21:51+0100\n" "Last-Translator: Patrick Ben Koetter \n" "Language-Team: DE \n" "Language: de\n" @@ -28,11 +28,15 @@ msgstr "Verwaltungskonsole" msgid "Web based console to manage domains, accounts and aliases" msgstr "Webinterface zur Verwaltung von Domains, Mailkonten und -aliasen" -#: admin/__init__.py:16 admin/forms.py:179 admin/forms.py:614 -#: admin/views.py:93 admin/views.py:441 +#: admin/__init__.py:16 +#: admin/forms.py:211 +#: admin/forms.py:652 +#: admin/views.py:94 +#: admin/views.py:447 #: extensions/postfix_autoreply/app_settings.py:11 #: extensions/sievefilters/app_settings.py:50 -#: extensions/stats/app_settings.py:11 extensions/webmail/app_settings.py:11 +#: extensions/stats/app_settings.py:11 +#: extensions/webmail/app_settings.py:11 #: userprefs/__init__.py:15 msgid "General" msgstr "Grundsätzlich" @@ -73,8 +77,10 @@ msgstr "Verwendeter Schlüssel, um Passworte zu ver-/entschlüsseln" msgid "LDAP settings" msgstr "LDAP-Einstellungen" -#: admin/app_settings.py:63 extensions/sievefilters/app_settings.py:22 -#: extensions/webmail/app_settings.py:22 extensions/webmail/app_settings.py:42 +#: admin/app_settings.py:63 +#: extensions/sievefilters/app_settings.py:22 +#: extensions/webmail/app_settings.py:22 +#: extensions/webmail/app_settings.py:42 msgid "Server address" msgstr "Server-Adresse" @@ -82,8 +88,10 @@ msgstr "Server-Adresse" msgid "The IP address of the DNS name of the LDAP server" msgstr "Die IP-Adresse des LDAP-Server Hostnamens" -#: admin/app_settings.py:69 extensions/sievefilters/app_settings.py:28 -#: extensions/webmail/app_settings.py:34 extensions/webmail/app_settings.py:58 +#: admin/app_settings.py:69 +#: extensions/sievefilters/app_settings.py:28 +#: extensions/webmail/app_settings.py:34 +#: extensions/webmail/app_settings.py:58 msgid "Server port" msgstr "Server-Port" @@ -91,7 +99,8 @@ msgstr "Server-Port" msgid "The TCP port number used by the LDAP server" msgstr "Der TCP Port des LDAP-Servers" -#: admin/app_settings.py:75 extensions/webmail/app_settings.py:28 +#: admin/app_settings.py:75 +#: extensions/webmail/app_settings.py:28 msgid "Use a secured connection" msgstr "Gesicherte Verbindung verwenden" @@ -120,11 +129,8 @@ msgid "Bind DN" msgstr "Bind DN" #: admin/app_settings.py:92 -msgid "" -"The distinguished name to use when binding to the LDAP server. Leave empty " -"for an anonymous bind" -msgstr "" -"Der DN für einen Bind an den LDAP-Server. Nichts eingeben für anonymen Bind." +msgid "The distinguished name to use when binding to the LDAP server. Leave empty for an anonymous bind" +msgstr "Der DN für einen Bind an den LDAP-Server. Nichts eingeben für anonymen Bind." #: admin/app_settings.py:97 msgid "Bind password" @@ -147,12 +153,8 @@ msgid "Search filter" msgstr "Suchfilter" #: admin/app_settings.py:114 -msgid "" -"An optional filter string (e.g. '(objectClass=person)'). In order to be " -"valid, it must be enclosed in parentheses." -msgstr "" -"Ein optionaler Filter z.B. (objectClass=person). Das Filter muss " -"entsprechend der LDAP-Konvention in Klammern gesetzt werden." +msgid "An optional filter string (e.g. '(objectClass=person)'). In order to be valid, it must be enclosed in parentheses." +msgstr "Ein optionaler Filter z.B. (objectClass=person). Das Filter muss entsprechend der LDAP-Konvention in Klammern gesetzt werden." #: admin/app_settings.py:119 msgid "User DN template" @@ -160,12 +162,8 @@ msgstr "Vorlage für User DN" #: admin/app_settings.py:121 #, python-format -msgid "" -"The template used to construct a user's DN. It should contain one " -"placeholder (ie. %(user)s)" -msgstr "" -"Die Vorlage für einen User DN. Sie sollte einen Platzhalter z.B. %(user)s " -"enthalten." +msgid "The template used to construct a user's DN. It should contain one placeholder (ie. %(user)s)" +msgstr "Die Vorlage für einen User DN. Sie sollte einen Platzhalter z.B. %(user)s enthalten." #: admin/app_settings.py:126 msgid "Password attribute" @@ -183,9 +181,12 @@ msgstr "Active Directory" msgid "Tell if the LDAP server is an Active Directory one" msgstr "Auswählen wenn der LDAP-Server ein Active Directory ist." -#: admin/app_settings.py:137 admin/templates/admin/editdomainform.html:25 -#: extensions/limits/app_settings.py:31 extensions/limits/forms.py:24 -#: extensions/limits/models.py:14 extensions/webmail/app_settings.py:101 +#: admin/app_settings.py:137 +#: admin/templates/admin/editdomainform.html:25 +#: extensions/limits/app_settings.py:31 +#: extensions/limits/forms.py:24 +#: extensions/limits/models.py:14 +#: extensions/webmail/app_settings.py:101 #: extensions/webmail/templates/webmail/folders.html:4 msgid "Mailboxes" msgstr "Mailboxen" @@ -195,12 +196,8 @@ msgid "Handle mailboxes on filesystem" msgstr "Mailbox im Dateisystem verwalten" #: admin/app_settings.py:142 -msgid "" -"Rename or remove mailboxes on the filesystem when they get renamed or " -"removed within Modoboa" -msgstr "" -"Mailboxen auch in Dateisystem umbenennen oder entfernen wenn sie in Modoboa " -"umbenannt oder entfernt werden" +msgid "Rename or remove mailboxes on the filesystem when they get renamed or removed within Modoboa" +msgstr "Mailboxen auch in Dateisystem umbenennen oder entfernen wenn sie in Modoboa umbenannt oder entfernt werden" #: admin/app_settings.py:146 msgid "Mailboxes ower" @@ -208,9 +205,7 @@ msgstr "Mailbox-Besitzer" #: admin/app_settings.py:148 msgid "The UNIX account who owns mailboxes on the filesystem" -msgstr "" -"UID des virtuellen Benutzers, der Besitzer der Domains, Mailboxen und " -"Nachrichten im Dateisystem ist." +msgstr "UID des virtuellen Benutzers, der Besitzer der Domains, Mailboxen und Nachrichten im Dateisystem ist." #: admin/app_settings.py:152 msgid "Automatic account removal" @@ -220,7 +215,8 @@ msgstr "Automatische Konto-Entfernung" msgid "When a mailbox is removed, also remove the associated account" msgstr "Beim Löschen der Mailbox auch das Konto entfernen." -#: admin/app_settings.py:157 admin/templates/admin/editdomainform.html:7 +#: admin/app_settings.py:157 +#: admin/templates/admin/editdomainform.html:7 msgid "Miscellaneous" msgstr "Verschiedenes" @@ -248,40 +244,47 @@ msgstr "Standard Top-Redirect" msgid "The default redirection used when no application is specified" msgstr "Die Standardumleitung wenn kein Programm angegeben wurde" -#: admin/app_settings.py:230 +#: admin/app_settings.py:213 +msgid "Key must be either 16, 24, or 32 bytes long" +msgstr "Schlüssel muss 16, 24 oder 32 Byte lang sein" + +#: admin/app_settings.py:222 +msgid "Invalid syntax" +msgstr "Ungültige Syntax" + +#: admin/app_settings.py:245 msgid "This field is required" msgstr "Dieses Feld ist erforderlich" -#: admin/forms.py:16 admin/forms.py:459 admin/tables.py:18 +#: admin/forms.py:20 +#: admin/forms.py:497 +#: admin/tables.py:18 msgid "Alias(es)" msgstr "Aliase" -#: admin/forms.py:18 -msgid "" -"Alias(es) of this domain. Indicate only one name per input, press ENTER to " -"add a new input." -msgstr "" -"Alias(e) für diese Mailbox. Geben Sie jeweils nur eine Adresse pro " -"Eingabefeld an. Bestätigen Sie mit ENTER, um ein neues Eingabefeld zu " -"erhalten." +#: admin/forms.py:22 +msgid "Alias(es) of this domain. Indicate only one name per input, press ENTER to add a new input." +msgstr "Alias(e) für diese Mailbox. Geben Sie jeweils nur eine Adresse pro Eingabefeld an. Bestätigen Sie mit ENTER, um ein neues Eingabefeld zu erhalten." -#: admin/forms.py:55 +#: admin/forms.py:59 msgid "An alias with this name already exists" msgstr "Ein Alias mit diesem Namen existiert bereits" -#: admin/forms.py:69 +#: admin/forms.py:73 msgid "A domain with this name already exists" msgstr "Eine Domäne mit diesem Namen existiert bereits" -#: admin/forms.py:114 +#: admin/forms.py:146 msgid "Create a domain administrator" msgstr "Domänen-Administrator erstellen" -#: admin/forms.py:116 +#: admin/forms.py:148 msgid "Automatically create an administrator for this domain" msgstr "Automatisch Administrator für diese Domain anlegen" -#: admin/forms.py:120 admin/tables.py:12 admin/tables.py:47 +#: admin/forms.py:152 +#: admin/tables.py:12 +#: admin/tables.py:47 #: admin/templates/admin/editdomainform.html:13 #: admin/templates/admin/identities_table.html:5 #: admin/templates/admin/information.html:8 @@ -290,363 +293,353 @@ msgstr "Automatisch Administrator für diese Domain anlegen" msgid "Name" msgstr "Name" -#: admin/forms.py:122 -msgid "" -"The administrator's name. Don't include the domain's name here, it will be " -"automatically appended." -msgstr "" -"Name des Administrators. Geben Sie nicht den Domainnamen mit an. Er wird " -"automatisch hinzugefügt." +#: admin/forms.py:154 +msgid "The administrator's name. Don't include the domain's name here, it will be automatically appended." +msgstr "Name des Administrators. Geben Sie nicht den Domainnamen mit an. Er wird automatisch hinzugefügt." -#: admin/forms.py:128 +#: admin/forms.py:160 msgid "Create aliases" msgstr "Aliase erstellen" -#: admin/forms.py:130 +#: admin/forms.py:162 msgid "Automatically create standard aliases for this domain" msgstr "Automatisch Standard-Aliase für Domain anlegen" -#: admin/forms.py:143 +#: admin/forms.py:175 msgid "Invalid format" msgstr "Ungültiges Format" -#: admin/forms.py:155 +#: admin/forms.py:187 #, python-format msgid "User '%s' already exists" msgstr "Identität %s existiert bereits" -#: admin/forms.py:203 +#: admin/forms.py:235 msgid "Email address" msgstr "E-Mail Adresse" -#: admin/forms.py:204 -msgid "" -"The distribution list address. Use the '*' character to create a 'catchall' " -"address (ex: *@domain.tld)." -msgstr "" -"Adresse einer Verteilerliste. Geben Sie '*' an, um einen 'catchall'-Alias " -"(Bsp: *@example.com) zu erstellen." +#: admin/forms.py:236 +msgid "The distribution list address. Use the '*' character to create a 'catchall' address (ex: *@domain.tld)." +msgstr "Adresse einer Verteilerliste. Geben Sie '*' an, um einen 'catchall'-Alias (Bsp: *@example.com) zu erstellen." -#: admin/forms.py:207 +#: admin/forms.py:239 msgid "Recipients" msgstr "Empfänger" -#: admin/forms.py:208 -msgid "" -"Mailbox(es) this alias will point to. Indicate only one address per input, " -"press ENTER to add a new input." -msgstr "" -"Mailboxen auf die der Alias verweist. Geben Sie jeweils nur eine Adresse ein " -"und bestätigen Sie mit ENTER." +#: admin/forms.py:240 +msgid "Mailbox(es) this alias will point to. Indicate only one address per input, press ENTER to add a new input." +msgstr "Mailboxen auf die der Alias verweist. Geben Sie jeweils nur eine Adresse ein und bestätigen Sie mit ENTER." -#: admin/forms.py:246 admin/forms.py:504 admin/forms.py:530 -#: admin/models.py:1147 +#: admin/forms.py:278 +#: admin/forms.py:542 +#: admin/forms.py:568 msgid "Domain does not exist" msgstr "Domain existiert nicht" -#: admin/forms.py:248 +#: admin/forms.py:280 msgid "You don't have access to this domain" msgstr "Der Zugriff auf diese Domain ist ihnen nicht gestattet" -#: admin/forms.py:268 +#: admin/forms.py:300 msgid "Invalid mailbox" msgstr "Ungültige Mailbox" -#: admin/forms.py:284 +#: admin/forms.py:316 +#: admin/models.py:1202 #, python-format msgid "Local recipient %s not found" msgstr "Lokaler Empfänger %s nicht gefunden" -#: admin/forms.py:286 admin/forms.py:292 +#: admin/forms.py:318 +#: admin/forms.py:324 #, python-format msgid "Recipient %s already present" msgstr "Empfänger %s existiert bereits" -#: admin/forms.py:297 +#: admin/forms.py:329 msgid "No recipient defined" msgstr "Keine Empfänger angegeben" -#: admin/forms.py:311 extensions/webmail/forms.py:19 +#: admin/forms.py:343 +#: extensions/webmail/forms.py:19 msgid "Select a file" msgstr "Wählen Sie eine Datei" -#: admin/forms.py:313 admin/forms.py:341 +#: admin/forms.py:345 +#: admin/forms.py:373 msgid "Separator" msgstr "Separator" -#: admin/forms.py:316 +#: admin/forms.py:348 msgid "Continue on error" msgstr "Bei Fehler fortfahren" -#: admin/forms.py:317 +#: admin/forms.py:349 msgid "Don't treat duplicated objects as error" msgstr "Doppelte Objekte nicht als Fehler behandeln" -#: admin/forms.py:332 +#: admin/forms.py:364 msgid "Crypt passwords" msgstr "Passworte verschlüsseln" -#: admin/forms.py:333 +#: admin/forms.py:365 msgid "Check this option if passwords contained in your file are not crypted" msgstr "Auswählen wenn Kennwörter in Datei nicht verschlüsselt sind" -#: admin/forms.py:339 +#: admin/forms.py:371 msgid "File name" msgstr "Dateiname" -#: admin/forms.py:371 admin/templates/admin/editdomainform.html:13 +#: admin/forms.py:403 +#: admin/templates/admin/editdomainform.html:13 #: auth/forms.py:7 msgid "Username" msgstr "Benutzername" -#: admin/forms.py:373 +#: admin/forms.py:405 msgid "Role" msgstr "Rolle" -#: admin/forms.py:374 admin/forms.py:401 +#: admin/forms.py:406 +#: admin/forms.py:433 msgid "Choose" msgstr "Auswählen" -#: admin/forms.py:375 +#: admin/forms.py:407 msgid "What level of permission this user will have" msgstr "Rechte, die der Benutzer haben wird" -#: admin/forms.py:377 auth/forms.py:11 +#: admin/forms.py:409 +#: auth/forms.py:11 msgid "Password" msgstr "Passwort" -#: admin/forms.py:379 userprefs/forms.py:18 +#: admin/forms.py:411 +#: userprefs/forms.py:18 msgid "Confirmation" msgstr "Bestätigung" -#: admin/forms.py:381 +#: admin/forms.py:413 msgid "Enter the same password as above, for verification." msgstr "Kennwort bestätigen" -#: admin/forms.py:392 +#: admin/forms.py:424 msgid "Enabled" msgstr "Aktiviert" -#: admin/forms.py:434 +#: admin/forms.py:471 msgid "The two password fields didn't match." msgstr "Die angegebenen Kennworte stimmen nicht überein" -#: admin/forms.py:440 +#: admin/forms.py:477 msgid "You can't disable your own account" msgstr "Sie können Ihr eigenes Konto nicht deaktivieren" -#: admin/forms.py:450 +#: admin/forms.py:488 msgid "E-mail" msgstr "E-Mail" -#: admin/forms.py:452 +#: admin/forms.py:490 msgid "Quota" msgstr "Quota" -#: admin/forms.py:454 -msgid "" -"Quota in MB for this mailbox. Define a custom value or use domain's default " -"one. Leave empty to define an unlimited value (not allowed for domain " -"administrators)." -msgstr "" -"Quota in MB für diese Mailbox. Geben Sie einen eigenen Wert an oder nutzen " -"Sie den Standardwert der Domain. Lassen Sie das Feld leer für unbegrenztes " -"Quota - Domänen-Administratoren ist dies nicht gestattet." +#: admin/forms.py:492 +msgid "Quota in MB for this mailbox. Define a custom value or use domain's default one. Leave empty to define an unlimited value (not allowed for domain administrators)." +msgstr "Quota in MB für diese Mailbox. Geben Sie einen eigenen Wert an oder nutzen Sie den Standardwert der Domain. Lassen Sie das Feld leer für unbegrenztes Quota - Domänen-Administratoren ist dies nicht gestattet." -#: admin/forms.py:461 -msgid "" -"Alias(es) of this mailbox. Indicate only one address per input, press ENTER " -"to add a new input. Use the '*' character to create a 'catchall' alias (ex: " -"*@domain.tld)." -msgstr "" -"Alias(e) für diese Mailbox. Geben Sie jeweils nur eine Adresse pro Eingabe " -"an. Bestätigen Sie mit ENTER um die Eingabe zu bestätigen. Geben Sie '*' an, " -"um einen 'catchall'-Alias (Bsp: *@example.com) zu erstellen." +#: admin/forms.py:499 +msgid "Alias(es) of this mailbox. Indicate only one address per input, press ENTER to add a new input. Use the '*' character to create a 'catchall' alias (ex: *@domain.tld)." +msgstr "Alias(e) für diese Mailbox. Geben Sie jeweils nur eine Adresse pro Eingabe an. Bestätigen Sie mit ENTER um die Eingabe zu bestätigen. Geben Sie '*' an, um einen 'catchall'-Alias (Bsp: *@example.com) zu erstellen." -#: admin/forms.py:512 +#: admin/forms.py:550 #, python-format msgid "Mailbox %s already exists" msgstr "Mailbox %s existiert bereits" -#: admin/forms.py:572 +#: admin/forms.py:610 msgid "Domain(s)" msgstr "Domain(s)" -#: admin/forms.py:574 +#: admin/forms.py:612 msgid "Domain(s) that user administrates" msgstr "Domänen, die dieser Anwender administriert" -#: admin/forms.py:616 admin/views.py:444 +#: admin/forms.py:654 +#: admin/views.py:450 msgid "Mail" msgstr "E-Mail" -#: admin/forms.py:618 +#: admin/forms.py:656 msgid "Permissions" msgstr "Berechtigungen" -#: admin/models.py:78 +#: admin/models.py:80 msgid "You can't delete your own account" msgstr "Sie können Ihr eigenes Konto nicht entfernen" -#: admin/models.py:159 +#: admin/models.py:161 msgid "Failed to update password: LDAP module not installed" msgstr "Konnte Kennwort nicht aktualisieren: LDAP-Modul nicht installiert" -#: admin/models.py:165 +#: admin/models.py:167 #, python-format msgid "Failed to update password: %s" msgstr "Konnte Kennwort nicht aktualisieren: %s" -#: admin/models.py:192 +#: admin/models.py:194 msgid "account" msgstr "Konto" -#: admin/models.py:494 admin/models.py:720 admin/models.py:782 +#: admin/models.py:496 +#: admin/models.py:733 +#: admin/models.py:795 msgid "Invalid line" msgstr "Ungültige Zeile" -#: admin/models.py:498 +#: admin/models.py:500 msgid "You can't import an account with a role greater than yours" msgstr "Sie können kein Konto einer höheren Rolle importieren" -#: admin/models.py:518 +#: admin/models.py:506 +#, python-format +msgid "The simple user '%s' must have a valid email address" +msgstr "Der normale Benutzer '%s' muss eine gültige E-Mail-Adresse besitzen" + +#: admin/models.py:510 +#, python-format +msgid "username and email fields must not differ for '%s'" +msgstr "Die Felder für Benutzername und E-Mail für '%s' dürfen sich nicht unterscheiden" + +#: admin/models.py:534 #, python-format msgid "Account import failed (%s): domain does not exist" msgstr "Kann Konto nicht importieren (%s): Die Domain existiert nicht" -#: admin/models.py:607 admin/models.py:733 +#: admin/models.py:624 +#: admin/models.py:746 msgid "name" msgstr "Name" -#: admin/models.py:608 +#: admin/models.py:625 msgid "The domain name" msgstr "Name der Domäne" -#: admin/models.py:610 +#: admin/models.py:627 msgid "Default quota in MB applied to mailboxes" msgstr "Standard-Quota für Mailboxen (in MB)" -#: admin/models.py:613 admin/models.py:740 admin/models.py:1020 -#: admin/models.py:1181 extensions/postfix_autoreply/models.py:19 +#: admin/models.py:630 +#: admin/models.py:753 +#: admin/models.py:1045 +#: admin/models.py:1217 +#: extensions/postfix_autoreply/models.py:27 msgid "enabled" msgstr "aktiviert" -#: admin/models.py:614 +#: admin/models.py:631 msgid "Check to activate this domain" msgstr "Auswählen, um diese Domäne zu aktivieren" -#: admin/models.py:734 +#: admin/models.py:747 msgid "The alias name" msgstr "Der Alias-Name" -#: admin/models.py:736 +#: admin/models.py:749 msgid "target" msgstr "Ziel" -#: admin/models.py:737 +#: admin/models.py:750 msgid "The domain this alias points to" msgstr "Domäne auf die dieser Alias verweist" -#: admin/models.py:741 admin/models.py:1021 +#: admin/models.py:754 +#: admin/models.py:1046 msgid "Check to activate this alias" msgstr "Auswählen um den Alias zu aktivieren" -#: admin/models.py:788 +#: admin/models.py:801 #, python-format msgid "Unknown domain %s" msgstr "Unbekannte Domain %s" -#: admin/models.py:805 admin/models.py:1006 +#: admin/models.py:818 +#: admin/models.py:1031 msgid "address" msgstr "Adresse" -#: admin/models.py:806 +#: admin/models.py:819 msgid "Mailbox address (without the @domain.tld part)" msgstr "Mailbox-Adresse (ohne den @domain.tld-Anteil)" -#: admin/models.py:852 +#: admin/models.py:877 #, python-format msgid "Failed to retrieve mailbox location (%s)" msgstr "Konnte Speicherort der Mailbox (%s) nicht bestimmen" -#: admin/models.py:868 -#, python-format -msgid "Failed to rename mailbox: %s" -msgstr "Mailbox konnte nicht umbenannt werden: %s" - -#: admin/models.py:894 -#, python-format -msgid "Failed to remove mailbox: %s" -msgstr "Konnte Mailbox nicht entfernen: %s" - -#: admin/models.py:909 +#: admin/models.py:921 #, python-format msgid "Quota is greater than the allowed domain's limit (%dM)" msgstr "Das Quota ist größer als das maximale Quota der Domain (%dM)" -#: admin/models.py:914 +#: admin/models.py:926 msgid "A quota is required" msgstr "Ein Quota ist erforderlich" -#: admin/models.py:1007 -msgid "" -"The alias address (without the domain part). For a 'catch-all' address, just " -"enter an * character." -msgstr "" -"Die Alias-Adresse (ohne den Domain-Anteil). Geben sie für eine Adresse an, " -"die alles annimmt, * an." +#: admin/models.py:1032 +msgid "The alias address (without the domain part). For a 'catch-all' address, just enter an * character." +msgstr "Die Alias-Adresse (ohne den Domain-Anteil). Geben sie für eine Adresse an, die alles annimmt, * an." -#: admin/models.py:1011 +#: admin/models.py:1036 msgid "mailboxes" msgstr "Mailboxen" -#: admin/models.py:1012 +#: admin/models.py:1037 msgid "The mailboxes this alias points to" msgstr "Mailbox auf den dieser Alias verweist" -#: admin/models.py:1016 +#: admin/models.py:1041 msgid "The aliases this alias points to" msgstr "Aliase auf die dieser Alias verweist" -#: admin/models.py:1064 +#: admin/models.py:1089 msgid "distribution list" msgstr "Verteilerliste" -#: admin/models.py:1065 +#: admin/models.py:1090 msgid "forward" msgstr "Weiterleitung" -#: admin/models.py:1066 +#: admin/models.py:1091 msgid "alias" msgstr "Alias" -#: admin/models.py:1142 +#: admin/models.py:1170 #, python-format msgid "Invalid line: %s" msgstr "Ungültige Zeile: %s" -#: admin/models.py:1167 +#: admin/models.py:1175 #, python-format -msgid "Mailbox %s does not exist" -msgstr "Mailbox %s existiert nicht" +msgid "Domain '%s' does not exist" +msgstr "Domain '%s' existiert nicht" -#: admin/models.py:1182 +#: admin/models.py:1218 msgid "Check to enable this extension" msgstr "Auswählen, um diese Erweiterung zu aktivieren" -#: admin/models.py:1282 +#: admin/models.py:1318 msgid "added" msgstr "hinzugefügt" -#: admin/models.py:1285 +#: admin/models.py:1321 msgid "modified" msgstr "verändert" -#: admin/models.py:1288 +#: admin/models.py:1324 msgid "deleted" msgstr "entfernt" -#: admin/models.py:1290 +#: admin/models.py:1326 #, python-format msgid "%(object)s '%(name)s' %(action)s by user %(user)s" msgstr "%(object)s '%(name)s' %(action)s der Identität %(user)s" @@ -655,8 +648,10 @@ msgstr "%(object)s '%(name)s' %(action)s der Identität %(user)s" msgid "Edit domain" msgstr "Domäne bearbeiten" -#: admin/tables.py:21 admin/templates/admin/identities_table.html:8 -#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:79 +#: admin/tables.py:21 +#: admin/templates/admin/identities_table.html:8 +#: admin/templates/admin/listing.html:17 +#: extensions/sievefilters/views.py:83 #: extensions/sievefilters/templates/sievefilters/filter.html:17 #: extensions/sievefilters/templates/sievefilters/guieditor.html:5 #: extensions/webmail/templatetags/webextras.py:97 @@ -671,197 +666,216 @@ msgstr "Version" msgid "Description" msgstr "Beschreibung" -#: admin/views.py:87 +#: admin/views.py:88 msgid "New domain" msgstr "Neue Domäne" -#: admin/views.py:88 admin/views.py:97 admin/views.py:225 admin/views.py:438 -#: admin/views.py:445 extensions/sievefilters/views.py:120 -#: extensions/sievefilters/views.py:187 extensions/webmail/views.py:131 +#: admin/views.py:89 +#: admin/views.py:98 +#: admin/views.py:228 +#: admin/views.py:444 +#: admin/views.py:451 +#: extensions/sievefilters/views.py:129 +#: extensions/sievefilters/views.py:198 +#: extensions/webmail/views.py:136 msgid "Create" msgstr "Erstellen" -#: admin/views.py:94 admin/views.py:442 +#: admin/views.py:95 +#: admin/views.py:448 msgid "Next" msgstr "Nächstes" -#: admin/views.py:96 +#: admin/views.py:97 msgid "Options" msgstr "optionen" -#: admin/views.py:98 admin/views.py:446 +#: admin/views.py:99 +#: admin/views.py:452 msgid "Previous" msgstr "Vorheriges" -#: admin/views.py:111 +#: admin/views.py:112 msgid "Domain created" msgstr "Domäne erstellt." -#: admin/views.py:144 admin/views.py:282 admin/views.py:492 +#: admin/views.py:146 +#: admin/views.py:285 +#: admin/views.py:498 #: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:9 -#: extensions/sievefilters/views.py:139 extensions/webmail/views.py:162 +#: extensions/sievefilters/views.py:148 +#: extensions/webmail/views.py:168 #: userprefs/templates/userprefs/forward.html:8 #: userprefs/templates/userprefs/profile.html:8 msgid "Update" msgstr "Aktualisieren" -#: admin/views.py:160 +#: admin/views.py:162 msgid "Domain modified" msgstr "Domäne verändert" -#: admin/views.py:186 +#: admin/views.py:189 msgid "You can't delete your own domain" msgstr "Sie können Ihre eigene Domäne nicht löschen" -#: admin/views.py:189 +#: admin/views.py:192 msgid "Domain deleted" msgid_plural "Domains deleted" msgstr[0] "Domäne gelöscht" msgstr[1] "Domänen gelöscht" -#: admin/views.py:204 +#: admin/views.py:207 msgid "Alias with this name already exists" msgstr "Ein Alias mit diesem Namen existiert bereits" -#: admin/views.py:247 +#: admin/views.py:250 msgid "New distribution list" msgstr "Neue Verteilerliste" -#: admin/views.py:248 +#: admin/views.py:251 msgid "Distribution list created" msgstr "Verteilerliste erstellt" -#: admin/views.py:257 +#: admin/views.py:260 msgid "New alias" msgstr "Neuer Alias" -#: admin/views.py:258 +#: admin/views.py:261 msgid "Alias created" msgstr "Alias erstellt" -#: admin/views.py:267 +#: admin/views.py:270 msgid "New forward" msgstr "Neue Weiterleitung" -#: admin/views.py:268 +#: admin/views.py:271 msgid "Forward created" msgstr "Weiterleitung erstellt" -#: admin/views.py:286 +#: admin/views.py:289 msgid "Distribution list modified" msgstr "Verteilerliste verändert" -#: admin/views.py:288 +#: admin/views.py:291 msgid "Forward modified" msgstr "Weiterleitung verändert" -#: admin/views.py:290 +#: admin/views.py:293 msgid "Alias modified" msgstr "Alias verändert" -#: admin/views.py:403 admin/views.py:541 admin/views.py:546 -#: extensions/amavis/views.py:195 extensions/amavis/views.py:201 -#: extensions/amavis/views.py:234 extensions/amavis/views.py:238 -#: extensions/amavis/views.py:240 extensions/webmail/views.py:36 -#: extensions/webmail/views.py:64 extensions/webmail/views.py:77 -#: extensions/webmail/views.py:93 extensions/webmail/views.py:109 -#: extensions/webmail/views.py:193 extensions/webmail/views.py:419 -#: extensions/webmail/views.py:430 extensions/webmail/views.py:457 -#: lib/formutils.py:32 lib/formutils.py:37 +#: admin/views.py:402 +#: admin/views.py:545 +#: admin/views.py:550 +#: extensions/amavis/views.py:204 +#: extensions/amavis/views.py:210 +#: extensions/amavis/views.py:243 +#: extensions/amavis/views.py:247 +#: extensions/amavis/views.py:249 +#: extensions/webmail/views.py:41 +#: extensions/webmail/views.py:68 +#: extensions/webmail/views.py:81 +#: extensions/webmail/views.py:98 +#: extensions/webmail/views.py:114 +#: extensions/webmail/views.py:200 +#: extensions/webmail/views.py:427 +#: extensions/webmail/views.py:438 +#: extensions/webmail/views.py:465 +#: lib/formutils.py:32 +#: lib/formutils.py:37 msgid "Invalid request" msgstr "Ungültige Anfrage" -#: admin/views.py:435 +#: admin/views.py:441 msgid "New account" msgstr "Neues Konto" -#: admin/views.py:459 +#: admin/views.py:465 msgid "Account created" msgstr "Konto erstellt" -#: admin/views.py:507 +#: admin/views.py:513 msgid "Account updated" msgstr "Konto aktualisiert" -#: admin/views.py:531 +#: admin/views.py:535 msgid "Account deleted" msgid_plural "Accounts deleted" msgstr[0] "Konto gelöscht" msgstr[1] "Konten gelöscht" -#: admin/views.py:586 +#: admin/views.py:590 msgid "Parameters saved" msgstr "Parameter gespeichert" -#: admin/views.py:632 +#: admin/views.py:636 msgid "Modifications applied." msgstr "Änderungen übernommen." -#: admin/views.py:740 +#: admin/views.py:743 #, python-format msgid "Object already exists: %s" msgstr "Objekt existiert bereits: %s" -#: admin/views.py:742 +#: admin/views.py:745 #, python-format msgid "%d objects imported successfully" msgstr "%d Objekte erfolgreich importiert" -#: admin/views.py:760 +#: admin/views.py:763 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
    \n" -"
  • account; loginname; password; first name; last name; enabled; group; " -"address[, domain, ...]
  • \n" +"
  • account; loginname; password; first name; last name; enabled; group; address[, domain, ...]
  • \n" "
  • alias; address; enabled; internal recipient
  • \n" "
  • forward; address; enabled; external recipient
  • \n" "
  • dlist; address; enabled; recipient; recipient; ...
  • \n" "
\n" -"

The first element of each line is mandatory and must be equal to one of " -"the previous values.

\n" +"

The first element of each line is mandatory and must be equal to one of the previous values.

\n" "\n" "

You can use a different character as separator.

\n" msgstr "" "CSV-Datei in einem der folgenden Formate hinzufügen:\n" "
    \n" -"
  • account; loginname; password; first name; last name; enabled; group; " -"address[, domain, ...]
  • \n" +"
  • account; loginname; password; first name; last name; enabled; group; address[, domain, ...]
  • \n" "
  • alias; address; enabled; internal recipient
  • \n" "
  • forward; address; enabled; external recipient
  • \n" "
  • dlist; address; enabled; recipient; recipient; ...
  • \n" "
\n" -"

Das erste Element einer Zeile ist zwingend und muss gleich mit einem der " -"vorhergehenden sein.

\n" +"

Das erste Element einer Zeile ist zwingend und muss gleich mit einem der vorhergehenden sein.

\n" "\n" "

Ein anderes Zeichen als Separator ist gestattet.

\n" -#: admin/views.py:772 +#: admin/views.py:775 msgid "Import identities" msgstr "Identitäten importieren" -#: admin/views.py:773 admin/views.py:843 admin/templatetags/admin_extras.py:87 +#: admin/views.py:776 +#: admin/views.py:846 +#: admin/templatetags/admin_extras.py:87 #: admin/templatetags/admin_extras.py:144 msgid "Import" msgstr "Importiere" -#: admin/views.py:803 +#: admin/views.py:806 msgid "Export identities" msgstr "Identitäten exportieren" -#: admin/views.py:804 admin/views.py:860 admin/templatetags/admin_extras.py:93 +#: admin/views.py:807 +#: admin/views.py:863 +#: admin/templatetags/admin_extras.py:93 #: admin/templatetags/admin_extras.py:150 msgid "Export" msgstr "Exportiere" -#: admin/views.py:832 +#: admin/views.py:835 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
    \n" "
  • domain; name; quota; enabled
  • \n" "
  • domainalias; name; targeted domain; enabled
  • \n" "
\n" -"

The first element of each line is mandatory and must be equal to one of " -"the previous values.

\n" +"

The first element of each line is mandatory and must be equal to one of the previous values.

\n" "

You can use a different character as separator.

\n" msgstr "" "CSV-Datei in einem der folgenden Formate hinzufügen:\n" @@ -869,15 +883,14 @@ msgstr "" "
  • domain; name; quota; enabled
  • \n" "
  • domainalias; name; targeted domain; enabled
  • \n" "\n" -"

    Das erste Element einer Zeile ist zwinged und muss gleich zum " -"vorhergehenden sein.

    \n" +"

    Das erste Element einer Zeile ist zwinged und muss gleich zum vorhergehenden sein.

    \n" "

    Ein anderes Zeichen als Trenner ist gestattet.

    \n" -#: admin/views.py:842 +#: admin/views.py:845 msgid "Import domains" msgstr "Domains importieren" -#: admin/views.py:859 +#: admin/views.py:862 msgid "Export domains" msgstr "Domains exportieren" @@ -887,8 +900,10 @@ msgstr "Domains exportieren" msgid "MB" msgstr "MB" -#: admin/templates/admin/domains.html:2 admin/templatetags/admin_extras.py:23 -#: extensions/limits/app_settings.py:19 extensions/limits/forms.py:16 +#: admin/templates/admin/domains.html:2 +#: admin/templatetags/admin_extras.py:23 +#: extensions/limits/app_settings.py:19 +#: extensions/limits/forms.py:16 #: extensions/limits/models.py:12 msgid "Domains" msgstr "Domänen" @@ -910,13 +925,15 @@ msgid "Last modification date" msgstr "Datum der letzten Änderung" #: admin/templates/admin/editdomainform.html:24 -#: extensions/limits/app_settings.py:25 extensions/limits/forms.py:20 +#: extensions/limits/app_settings.py:25 +#: extensions/limits/forms.py:20 #: extensions/limits/models.py:13 msgid "Domain aliases" msgstr "Domänen-Aliase" #: admin/templates/admin/editdomainform.html:26 -#: extensions/limits/app_settings.py:37 extensions/limits/forms.py:28 +#: extensions/limits/app_settings.py:37 +#: extensions/limits/forms.py:28 #: extensions/limits/models.py:15 msgid "Mailbox aliases" msgstr "Mailboxen-Aliase" @@ -950,7 +967,7 @@ msgid "Tags" msgstr "Tags" #: admin/templates/admin/importform.html:5 -#: admin/templatetags/admin_extras.py:305 +#: admin/templatetags/admin_extras.py:290 msgid "Help" msgstr "Hilfe" @@ -988,7 +1005,8 @@ msgstr "Letzte Anmeldung" msgid "Filter" msgstr "Filter" -#: admin/templates/admin/logs.html:2 admin/templatetags/admin_extras.py:60 +#: admin/templates/admin/logs.html:2 +#: admin/templatetags/admin_extras.py:60 msgid "Logs" msgstr "Logs" @@ -996,8 +1014,10 @@ msgstr "Logs" msgid "Previously on Modoboa..." msgstr "Vorher auf Modoboa…" -#: admin/templates/admin/logs.html:6 extensions/amavis/sql_listing.py:25 -#: extensions/webmail/lib.py:37 extensions/webmail/lib.py:406 +#: admin/templates/admin/logs.html:6 +#: extensions/amavis/sql_listing.py:25 +#: extensions/webmail/lib.py:38 +#: extensions/webmail/lib.py:411 msgid "Date" msgstr "Datum" @@ -1100,34 +1120,23 @@ msgid "View the domain's identities" msgstr "Domain-Identitäten anzeigen" #: admin/templatetags/admin_extras.py:178 -msgid "Delete the domain" -msgstr "Domain entfernen" - #: admin/templatetags/admin_extras.py:194 -msgid "Delete this account" -msgstr "Dieses Konto löschen" +#: admin/templatetags/admin_extras.py:201 +#, python-format +msgid "Delete %s?" +msgstr "%s löschen?" -#: admin/templatetags/admin_extras.py:202 -msgid "Delete this distribution list" -msgstr "Diese Verteilerliste löschen" - -#: admin/templatetags/admin_extras.py:209 -msgid "Delete this forward" -msgstr "Weiterleitung entfernen" - -#: admin/templatetags/admin_extras.py:216 -msgid "Delete this alias" -msgstr " Alias entfernen" - -#: admin/templatetags/admin_extras.py:238 +#: admin/templatetags/admin_extras.py:223 msgid "Remove this permission" msgstr "Berechtigung entfernen" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 +#: lib/formutils.py:206 msgid "Yes" msgstr "Ja" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 +#: lib/formutils.py:206 msgid "No" msgstr "Nein" @@ -1136,13 +1145,11 @@ msgstr "Nein" msgid "User '%s' successfully logged in" msgstr "Identität '%s' hat sich erfolgreich angemeldet" -#: auth/views.py:41 +#: auth/views.py:44 msgid "Your username and password didn't match. Please try again." -msgstr "" -"Ihr Benutzername und Passwort stimmen nicht überein. Bitte versuchen Sie es " -"noch einmal." +msgstr "Ihr Benutzername und Passwort stimmen nicht überein. Bitte versuchen Sie es noch einmal." -#: auth/views.py:64 +#: auth/views.py:67 #, python-format msgid "User '%s' logged out" msgstr "Identität '%s' hat sich abgemeldet" @@ -1159,7 +1166,8 @@ msgstr "Virus senden" msgid "Send spam" msgstr "Spam senden" -#: demo/views.py:17 demo/views.py:41 +#: demo/views.py:17 +#: demo/views.py:41 msgid "Message sent" msgstr "Nachricht gesendet" @@ -1168,50 +1176,46 @@ msgid "" "\n" "

    Welcome to the online demo!

    \n" "

    Warning: the demo's concent is resetted every two hours.

    \n" -"

    To try the demo, the following users are available (username/password):

    \n" +"

    To try the demo, the following users are available (username/password):

    \n" "
      \n" "
    • A super administrator : admin/password
    • \n" "
    • A domain administrator : admin@demo.local/admin
    • \n" "
    • A simple user : user@demo.local/user
    • \n" "
    \n" "

    Please report any issue or give any suggestion to \n" -" the " -"mailing list.

    \n" +" the mailing list.

    \n" msgstr "" "\n" "

    Willkommen zur Online Demo!

    \n" -"

    Warnung: die Inhalte dieser Demo werden alle zwei Stunden gelöscht." -"

    \n" -"

    Folgende User (Benutzername/Kennwort) stehen zum Testen zur " -"Verfügung:

    \n" +"

    Warnung: die Inhalte dieser Demo werden alle zwei Stunden gelöscht.

    \n" +"

    Folgende User (Benutzername/Kennwort) stehen zum Testen zur Verfügung:

    \n" "
      \n" "
    • Super-Administrator : admin/password
    • \n" "
    • Domänen-Administrator : admin@demo.local/admin
    • \n" "
    • Normaler Benutzer: user@demo.local/user
    • \n" "
    \n" "

    Probleme und Anregungen bitte an die\n" -" Mailingliste berichten.

    \n" +" Mailingliste berichten.

    \n" #: extensions/amavis/__init__.py:22 msgid "Simple amavis management frontend" msgstr "Einfache amavis Management Oberfläche" -#: extensions/amavis/__init__.py:45 extensions/amavis/__init__.py:61 +#: extensions/amavis/__init__.py:45 +#: extensions/amavis/__init__.py:61 msgid "Quarantine" msgstr "Quarantäne" -#: extensions/amavis/__init__.py:127 +#: extensions/amavis/__init__.py:129 msgid "pending requests" msgstr "Offene Anfragen" -#: extensions/amavis/__init__.py:144 +#: extensions/amavis/__init__.py:149 #, python-format msgid "%d pending requests" msgstr "%d offene Anfragen" -#: extensions/amavis/__init__.py:156 +#: extensions/amavis/__init__.py:161 msgid "Content filter" msgstr "Inhaltsfilter" @@ -1225,9 +1229,7 @@ msgstr "Maximales Alter einer Nachricht" #: extensions/amavis/app_settings.py:16 msgid "Quarantine messages maximum age (in days) before deletion" -msgstr "" -"Maximales Alter (in Tagen) von Nachrichten in der Quarantäne bevor sie " -"gelöscht werden" +msgstr "Maximales Alter (in Tagen) von Nachrichten in der Quarantäne bevor sie gelöscht werden" #: extensions/amavis/app_settings.py:19 msgid "Messages releasing" @@ -1303,23 +1305,22 @@ msgstr "Absender von Benachrichtigungen" #: extensions/amavis/app_settings.py:74 msgid "The e-mail address used to send notitications" -msgstr "" -"Die Mailadresse, die verwendet wird, um Benachrichtigungen zu versenden" +msgstr "Die Mailadresse, die verwendet wird, um Benachrichtigungen zu versenden" -#: extensions/amavis/app_settings.py:87 extensions/webmail/app_settings.py:73 +#: extensions/amavis/app_settings.py:87 +#: extensions/webmail/app_settings.py:73 #: userprefs/app_settings.py:18 msgid "Display" msgstr "Darstellung" -#: extensions/amavis/app_settings.py:91 extensions/webmail/app_settings.py:91 +#: extensions/amavis/app_settings.py:91 +#: extensions/webmail/app_settings.py:91 msgid "Number of displayed emails per page" msgstr "Anzahl dargestellter Nachrichten pro Seite" #: extensions/amavis/app_settings.py:92 msgid "Set the maximum number of messages displayed in a page" -msgstr "" -"Legt die maximale Anzahl von Nachrichten fest, die in einer Seite " -"dargestellt werden" +msgstr "Legt die maximale Anzahl von Nachrichten fest, die in einer Seite dargestellt werden" #: extensions/amavis/lib.py:53 #, python-format @@ -1330,17 +1331,22 @@ msgstr "Verbindung zu amavis fehlgeschlagen: %s" msgid "Virus filter" msgstr "Virusfilter" -#: extensions/amavis/models.py:94 extensions/amavis/models.py:102 -#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:268 +#: extensions/amavis/models.py:94 +#: extensions/amavis/models.py:102 +#: extensions/amavis/models.py:110 +#: extensions/sievefilters/views.py:281 msgid "yes" msgstr "Ja" -#: extensions/amavis/models.py:95 extensions/amavis/models.py:103 -#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:272 +#: extensions/amavis/models.py:95 +#: extensions/amavis/models.py:103 +#: extensions/amavis/models.py:111 +#: extensions/sievefilters/views.py:285 msgid "no" msgstr "Nein" -#: extensions/amavis/models.py:96 extensions/amavis/models.py:104 +#: extensions/amavis/models.py:96 +#: extensions/amavis/models.py:104 #: extensions/amavis/models.py:112 #: extensions/amavis/templates/amavis/domain_content_filter.html:11 msgid "default" @@ -1348,9 +1354,7 @@ msgstr "Standard" #: extensions/amavis/models.py:98 msgid "Bypass virus checks or not. Choose 'default' to use global settings." -msgstr "" -"Virus-Prüfungen umgehen oder nicht. Geben Sie, um den Domain-Standard zu " -"nutzen, keinen individuellen Wert an." +msgstr "Virus-Prüfungen umgehen oder nicht. Geben Sie, um den Domain-Standard zu nutzen, keinen individuellen Wert an." #: extensions/amavis/models.py:101 msgid "Spam filter" @@ -1358,9 +1362,7 @@ msgstr "SPAM-Filter" #: extensions/amavis/models.py:106 msgid "Bypass spam checks or not. Choose 'default' to use global settings." -msgstr "" -"Spam-Prüfungen umgehen oder nicht. Geben Sie, um den Domain-Standard zu " -"nutzen, keinen individuellen Wert an." +msgstr "Spam-Prüfungen umgehen oder nicht. Geben Sie, um den Domain-Standard zu nutzen, keinen individuellen Wert an." #: extensions/amavis/models.py:109 msgid "Banned filter" @@ -1368,29 +1370,27 @@ msgstr "Filter für unerwünschte Dateien" #: extensions/amavis/models.py:114 msgid "Bypass banned checks or not. Choose 'default' to use global settings." -msgstr "" -"Unerwünschte Dateien umgehen oder nicht. Geben Sie, um den Domain-Standard " -"zu nutzen, keinen individuellen Wert an." +msgstr "Unerwünschte Dateien umgehen oder nicht. Geben Sie, um den Domain-Standard zu nutzen, keinen individuellen Wert an." #: extensions/amavis/models.py:144 msgid "Spam marker" msgstr "SPAM-Markierung" #: extensions/amavis/models.py:146 -msgid "" -"Modify spam subject using the specified text. Choose 'default' to use global " -"settings." -msgstr "" -"Betreff:-Zeile einer Spam-Nachricht mit dem angegebenen Text ergänzen. " -"Wählen Sie 'Standard' für den systemweiten Standard." +msgid "Modify spam subject using the specified text. Choose 'default' to use global settings." +msgstr "Betreff:-Zeile einer Spam-Nachricht mit dem angegebenen Text ergänzen. Wählen Sie 'Standard' für den systemweiten Standard." -#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:36 +#: extensions/amavis/sql_listing.py:23 +#: extensions/webmail/lib.py:37 msgid "From" msgstr "Von" -#: extensions/amavis/sql_listing.py:24 extensions/sievefilters/forms.py:68 -#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:35 -#: extensions/webmail/lib.py:404 templates/common/email_searchbar.html:6 +#: extensions/amavis/sql_listing.py:24 +#: extensions/sievefilters/forms.py:68 +#: extensions/webmail/forms.py:10 +#: extensions/webmail/lib.py:36 +#: extensions/webmail/lib.py:409 +#: templates/common/email_searchbar.html:6 msgid "Subject" msgstr "Betreff" @@ -1400,37 +1400,37 @@ msgstr "Betreff" msgid "To" msgstr "An" -#: extensions/amavis/views.py:34 +#: extensions/amavis/views.py:43 msgid "Empty quarantine" msgstr "Quarantäne leeren" -#: extensions/amavis/views.py:202 +#: extensions/amavis/views.py:211 msgid "Message deleted" msgstr "Nachricht gelöscht" -#: extensions/amavis/views.py:223 +#: extensions/amavis/views.py:232 #, python-format msgid "%(count)d message deleted successfully" msgid_plural "%(count)d messages deleted successfully" msgstr[0] "%(count)d Nachricht erfolgreich gelöscht" msgstr[1] "%(count)d Nachrichten erfolgreich gelöscht" -#: extensions/amavis/views.py:243 +#: extensions/amavis/views.py:252 msgid "Request sent" msgstr "Anfrage gesendet" -#: extensions/amavis/views.py:249 +#: extensions/amavis/views.py:258 msgid "Message released" msgstr "Nachricht freigegeben" -#: extensions/amavis/views.py:271 +#: extensions/amavis/views.py:280 #, python-format msgid "%(count)d request sent" msgid_plural "%(count)d requests sent" msgstr[0] "%(count)d Anfrage gesendet" msgstr[1] "%(count)d Anfragen gesendet" -#: extensions/amavis/views.py:295 +#: extensions/amavis/views.py:304 #, python-format msgid "%(count)d message released successfully" msgid_plural "%(count)d messages released successfully" @@ -1499,8 +1499,7 @@ msgstr "Rektion auf diese Nachricht:" #: extensions/amavis/templates/amavis/notifications/pending_requests.html:14 #, python-format msgid "Please visit %(listingurl)s for a full listing." -msgstr "" -"Bitte besuchen sie %(listingurl)s um eine vollständige Ausgabe zu erhalten." +msgstr "Bitte besuchen sie %(listingurl)s um eine vollständige Ausgabe zu erhalten." #: extensions/amavis/templatetags/amextras.py:20 #: extensions/amavis/templatetags/amextras.py:52 @@ -1544,10 +1543,8 @@ msgid "Bad MIME" msgstr "Schlechtes MIME" #: extensions/limits/__init__.py:23 -msgid "" -"Per administrator resources to limit the number of objects they can create" -msgstr "" -"Administrator-Ressourcen, um die Anzahl erstellbarer Objekte einzuschränken" +msgid "Per administrator resources to limit the number of objects they can create" +msgstr "Administrator-Ressourcen, um die Anzahl erstellbarer Objekte einzuschränken" #: extensions/limits/__init__.py:54 msgid "Limits" @@ -1557,7 +1554,7 @@ msgstr "Grenzwerte" msgid "Reseller" msgstr "Reseller" -#: extensions/limits/__init__.py:96 +#: extensions/limits/__init__.py:94 msgid "Resources" msgstr "Ressourcen" @@ -1565,16 +1562,15 @@ msgstr "Ressourcen" msgid "Default limits" msgstr "Standard-Grenzwerte" -#: extensions/limits/app_settings.py:13 extensions/limits/forms.py:12 +#: extensions/limits/app_settings.py:13 +#: extensions/limits/forms.py:12 #: extensions/limits/models.py:11 msgid "Domain admins" msgstr "Domänen-Administratoren" #: extensions/limits/app_settings.py:15 msgid "Maximum number of allowed domain administrators for a new administrator" -msgstr "" -"Maximale Anzahl Domänenadministratoren, die ein neuer Administrator " -"erstellen darf" +msgstr "Maximale Anzahl Domänenadministratoren, die ein neuer Administrator erstellen darf" #: extensions/limits/app_settings.py:21 msgid "Maximum number of allowed domains for a new administrator" @@ -1582,8 +1578,7 @@ msgstr "Maximale Anzahl Domains, die ein neuer Administrator erstellen darf" #: extensions/limits/app_settings.py:27 msgid "Maximum number of allowed domain aliases for a new administrator" -msgstr "" -"Maximale Anzahl Domain Aliase, die ein neuer Administrator erstellen darf" +msgstr "Maximale Anzahl Domain Aliase, die ein neuer Administrator erstellen darf" #: extensions/limits/app_settings.py:33 msgid "Maximum number of allowed mailboxes for a new administrator" @@ -1623,8 +1618,7 @@ msgstr "Ihre Ressourcen wurde noch nicht initialisiert" #: extensions/limits/forms.py:78 msgid "You're not allowed to define unlimited values" -msgstr "" -"Sie verfügen nicht über ausreichend Rechte, um unbegrenzte Werte anzugeben" +msgstr "Sie verfügen nicht über ausreichend Rechte, um unbegrenzte Werte anzugeben" #: extensions/limits/lib.py:16 #, python-format @@ -1649,8 +1643,7 @@ msgstr "Ressourcen-Verbrauch" #: extensions/postfix_autoreply/__init__.py:20 msgid "Auto-reply (vacation) functionality using Postfix" -msgstr "" -"Automatische Antwort (Abwesenheitsbenachrichtigung) über Postfix-Programm" +msgstr "Automatische Antwort (Abwesenheitsbenachrichtigung) über Postfix-Programm" #: extensions/postfix_autoreply/__init__.py:41 msgid "Automatic replies" @@ -1667,9 +1660,7 @@ msgstr "Zeitüberschreitung für automatische Antworten" #: extensions/postfix_autoreply/app_settings.py:16 msgid "Timeout in seconds between two auto-replies to the same recipient" -msgstr "" -"Zeit in Sekunden zwischen zwei automatisierten Antworten an denselben " -"Empfänger" +msgstr "Zeit in Sekunden zwischen zwei automatisierten Antworten an denselben Empfänger" #: extensions/postfix_autoreply/forms.py:9 msgid "Until" @@ -1683,23 +1674,23 @@ msgstr "Aktivieren Sie ihre automatisierte Antwort bis zu diesem Datum" msgid "This date is over" msgstr "Das Datum ist vorbei" -#: extensions/postfix_autoreply/models.py:15 +#: extensions/postfix_autoreply/models.py:19 msgid "subject" msgstr "Betreff" -#: extensions/postfix_autoreply/models.py:16 +#: extensions/postfix_autoreply/models.py:20 msgid "The subject that will appear in sent emails" msgstr "Der Betreff, der in den versendeten Nachrichten verwendet wird" -#: extensions/postfix_autoreply/models.py:17 +#: extensions/postfix_autoreply/models.py:23 msgid "content" msgstr "Inhalt" -#: extensions/postfix_autoreply/models.py:18 +#: extensions/postfix_autoreply/models.py:24 msgid "The content that will appear in sent emails" msgstr "Der Inhalt, der in den versendeten Nachrichten verwendet wird" -#: extensions/postfix_autoreply/models.py:20 +#: extensions/postfix_autoreply/models.py:28 msgid "Activate/Deactivate your auto reply" msgstr "Aktivieren/Deaktivieren Sie die automatisierte Antwort" @@ -1709,9 +1700,7 @@ msgstr "Automatische Antwort erfolgreich aktualisiert." #: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:2 msgid "Define a message to automatically send when you are off" -msgstr "" -"Gegen sie eine Nachricht an, die gesendet werden soll, wenn Sie nicht " -"erreichbar sind" +msgstr "Gegen sie eine Nachricht an, die gesendet werden soll, wenn Sie nicht erreichbar sind" #: extensions/sievefilters/__init__.py:12 msgid "Plugin to easily create server-side filters" @@ -1803,7 +1792,8 @@ msgstr "Sender" msgid "Recipient" msgstr "Empfänger" -#: extensions/sievefilters/forms.py:71 extensions/webmail/forms.py:8 +#: extensions/sievefilters/forms.py:71 +#: extensions/webmail/forms.py:8 msgid "Cc" msgstr "Cc" @@ -1829,61 +1819,59 @@ msgstr "Leite Nachricht um zu" #: extensions/sievefilters/lib.py:37 msgid "Connection to MANAGESIEVE server failed, check your configuration" -msgstr "" -"Verbindung zu MANAGESIEVE-Server fehlgeschlagen. Überprüfen Sie Ihre " -"Konfiguration." +msgstr "Verbindung zu MANAGESIEVE-Server fehlgeschlagen. Überprüfen Sie Ihre Konfiguration." #: extensions/sievefilters/lib.py:82 msgid "Not enough space on server" msgstr "Kein weiterer Platz auf Server verfügbar" -#: extensions/sievefilters/views.py:64 +#: extensions/sievefilters/views.py:68 msgid "Failed to retrieve filters set" msgstr "Konnte Filter-Satz nicht laden" -#: extensions/sievefilters/views.py:117 +#: extensions/sievefilters/views.py:126 #: extensions/sievefilters/templatetags/sfilters_tags.py:33 msgid "New filter" msgstr "Neuer Filter" -#: extensions/sievefilters/views.py:124 +#: extensions/sievefilters/views.py:133 msgid "Filter created" msgstr "Filter erstellt" -#: extensions/sievefilters/views.py:136 +#: extensions/sievefilters/views.py:145 #: extensions/sievefilters/templatetags/sfilters_tags.py:77 msgid "Edit filter" msgstr "Filter bearbeiten" -#: extensions/sievefilters/views.py:145 +#: extensions/sievefilters/views.py:154 msgid "Filter modified" msgstr "Filter verändert" -#: extensions/sievefilters/views.py:165 +#: extensions/sievefilters/views.py:176 msgid "Filter removed" msgstr "Filter entfernt" -#: extensions/sievefilters/views.py:166 +#: extensions/sievefilters/views.py:177 msgid "Failed to remove filter" msgstr "Konnte Filter nicht entfernen" -#: extensions/sievefilters/views.py:180 +#: extensions/sievefilters/views.py:191 msgid "Filters set saved" msgstr "Filter-Satz gespeichert" -#: extensions/sievefilters/views.py:185 +#: extensions/sievefilters/views.py:196 msgid "Create a new filters set" msgstr "Neuen Filter-Satz erstellen" -#: extensions/sievefilters/views.py:208 +#: extensions/sievefilters/views.py:219 msgid "Filters set created" msgstr "Filter erstellt" -#: extensions/sievefilters/views.py:229 +#: extensions/sievefilters/views.py:240 msgid "Filters set deleted" msgstr "Filter entfernt" -#: extensions/sievefilters/views.py:241 +#: extensions/sievefilters/views.py:252 msgid "Filters set activated" msgstr "Filter-Satz aktiviert" @@ -1899,20 +1887,17 @@ msgstr "Aktiv" msgid "No filter defined yet" msgstr "Noch keine Filter definiert" -#: extensions/sievefilters/templates/sievefilters/index.html:55 +#: extensions/sievefilters/templates/sievefilters/index.html:56 msgid "Filter sets" msgstr "Filter-Sätze" -#: extensions/sievefilters/templates/sievefilters/index.html:59 +#: extensions/sievefilters/templates/sievefilters/index.html:60 msgid "active" msgstr "aktiv" -#: extensions/sievefilters/templates/sievefilters/index.html:73 -msgid "" -"No filters set defined yet. You must create one in order to define filters." -msgstr "" -"Nich keine Filter-Sätze definiert. Sie müssen einen erstellen, damit sie " -"Filter erstellen können." +#: extensions/sievefilters/templates/sievefilters/index.html:74 +msgid "No filters set defined yet. You must create one in order to define filters." +msgstr "Nich keine Filter-Sätze definiert. Sie müssen einen erstellen, damit sie Filter erstellen können." #: extensions/sievefilters/templatetags/sfilters_tags.py:16 msgid "New filters set" @@ -2052,7 +2037,8 @@ msgstr "Jahr" msgid "Total" msgstr "Gesamt" -#: extensions/stats/views.py:21 extensions/stats/templates/stats/graphs.html:4 +#: extensions/stats/views.py:21 +#: extensions/stats/templates/stats/graphs.html:4 msgid "No statistics available" msgstr "Keine Statistik-Daten verfügbar" @@ -2162,9 +2148,7 @@ msgstr "Aktiviere/Deaktiviere Darstellung von HTML-Links" #: extensions/webmail/app_settings.py:92 msgid "Sets the maximum number of messages displayed in a page" -msgstr "" -"Legt die maximale Anzahl von Nachrichten fest, die in einer Seite " -"dargestellt werden" +msgstr "Legt die maximale Anzahl von Nachrichten fest, die in einer Seite dargestellt werden" #: extensions/webmail/app_settings.py:97 msgid "Listing refresh rate" @@ -2226,84 +2210,91 @@ msgstr "Server-Antwort" msgid "Cci" msgstr "Cci" -#: extensions/webmail/imaputils.py:259 +#: extensions/webmail/imaputils.py:237 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Konnte Trenner für Hierarchie nicht in Erfahrung bringen" + +#: extensions/webmail/imaputils.py:278 #, python-format msgid "Connection to IMAP server failed: %s" msgstr "Verbindung zu IMAP-Server fehlgeschlagen: %s" -#: extensions/webmail/lib.py:353 +#: extensions/webmail/lib.py:358 msgid "wrote:" msgstr "schrieb:" -#: extensions/webmail/lib.py:418 extensions/webmail/lib.py:422 +#: extensions/webmail/lib.py:423 +#: extensions/webmail/lib.py:427 msgid "Original message" msgstr "Gesendete Nachrichten" -#: extensions/webmail/views.py:81 +#: extensions/webmail/views.py:86 #, python-format msgid "%(count)d message deleted" msgid_plural "%(count)d messages deleted" msgstr[0] "%(count)d Nachricht gelöscht" msgstr[1] "%(count)d Nachrichten gelöscht" -#: extensions/webmail/views.py:98 +#: extensions/webmail/views.py:103 msgid "Unknown action" msgstr "Unbekannte Aktion" -#: extensions/webmail/views.py:111 lib/email_listing.py:137 +#: extensions/webmail/views.py:116 +#: lib/email_listing.py:137 msgid "Empty mailbox" msgstr "Mailbox leer" -#: extensions/webmail/views.py:128 -#: extensions/webmail/templatetags/webextras.py:169 +#: extensions/webmail/views.py:133 +#: extensions/webmail/templatetags/webextras.py:170 msgid "Create a new mailbox" msgstr "Neue Mailbox erstellen" -#: extensions/webmail/views.py:143 +#: extensions/webmail/views.py:149 msgid "Mailbox created" msgstr "Mailbox erstellt." -#: extensions/webmail/views.py:159 +#: extensions/webmail/views.py:165 msgid "Edit mailbox" msgstr "Mailbox bearbeiten" -#: extensions/webmail/views.py:173 +#: extensions/webmail/views.py:180 msgid "Mailbox updated" msgstr "Mailbox aktualisiert" -#: extensions/webmail/views.py:207 extensions/webmail/views.py:388 -#: extensions/webmail/views.py:497 +#: extensions/webmail/views.py:214 +#: extensions/webmail/views.py:396 +#: extensions/webmail/views.py:505 msgid "Bad request" msgstr "Fehlerhafte Anfrage" -#: extensions/webmail/views.py:238 +#: extensions/webmail/views.py:246 msgid "Failed to save attachment: " msgstr "Konnte Anhang nicht speichern" -#: extensions/webmail/views.py:241 +#: extensions/webmail/views.py:249 #, python-format msgid "Attachment is too big (limit: %s)" msgstr "Anhang ist zu groß (Limit: %s)" -#: extensions/webmail/views.py:247 +#: extensions/webmail/views.py:255 #: extensions/webmail/templates/webmail/compose.html:15 #: extensions/webmail/templates/webmail/headers.html:4 msgid "Attachments" msgstr "Dateianhänge" -#: extensions/webmail/views.py:263 +#: extensions/webmail/views.py:271 msgid "Bad query" msgstr "Fehlerhafte Anfrage" -#: extensions/webmail/views.py:273 +#: extensions/webmail/views.py:281 msgid "Failed to remove attachment: " msgstr "Konnte Anhang nicht entfernen:" -#: extensions/webmail/views.py:278 +#: extensions/webmail/views.py:286 msgid "Unknown attachment" msgstr "Unbekannter Anhang" -#: extensions/webmail/views.py:493 +#: extensions/webmail/views.py:501 msgid "Undefined action" msgstr "Undefinierte Aktion" @@ -2334,7 +2325,7 @@ msgstr[1] "%(counter)s Dateien" msgid "Parent mailbox" msgstr "Eltern-Mailbox" -#: extensions/webmail/templates/webmail/index.html:36 +#: extensions/webmail/templates/webmail/index.html:38 msgid "Mailbox quota" msgstr "Mailbox Quota" @@ -2396,15 +2387,15 @@ msgstr "Als ungelesen" msgid "Compress folder" msgstr "Ordner komprimieren" -#: extensions/webmail/templatetags/webextras.py:109 +#: extensions/webmail/templatetags/webextras.py:110 msgid "Empty folder" msgstr "Leerer Ordner" -#: extensions/webmail/templatetags/webextras.py:177 +#: extensions/webmail/templatetags/webextras.py:178 msgid "Edit the selected mailbox" msgstr "Ausgewählte Mailbox bearbeiten" -#: extensions/webmail/templatetags/webextras.py:182 +#: extensions/webmail/templatetags/webextras.py:183 msgid "Remove the selected mailbox" msgstr "Markierte Mailbox entfernen" @@ -2417,7 +2408,8 @@ msgstr "Erlaubnis verweigert: %s" msgid "Permission denied" msgstr "Berechtigung verweigert" -#: lib/formutils.py:157 lib/formutils.py:165 +#: lib/formutils.py:157 +#: lib/formutils.py:165 msgid "Enter a valid domain name" msgstr "Geben Sie einen gültigen Domain-Namen an" @@ -2446,7 +2438,8 @@ msgstr "Keine Einträge darzustellen" msgid "activate" msgstr "aktivieren" -#: templates/404.html:4 templates/500.html:4 +#: templates/404.html:4 +#: templates/500.html:4 msgid "Sorry" msgstr "Entschuldigung" @@ -2458,7 +2451,7 @@ msgstr "Die angeforderte Seite existiert nicht" msgid "An internal error occured." msgstr "Ein interne Fehler ist aufgetreten" -#: templates/nlayout.html:56 +#: templates/nlayout.html:62 msgid "Powered by Modoboa" msgstr "Powered by Modoboa" @@ -2492,11 +2485,11 @@ msgstr "Schliessen" msgid "Welcome to Modoboa" msgstr "Willkommen zu Modoboa" -#: templates/registration/login.html:33 +#: templates/registration/login.html:34 msgid "Remember me" msgstr "Erinnere mich" -#: templates/registration/login.html:35 +#: templates/registration/login.html:36 msgid "Log in" msgstr "Anmelden" @@ -2512,7 +2505,7 @@ msgstr "Anwenderspezifische Einstellungen" msgid "Prefered language" msgstr "Bevorzugte Sprache" -#: userprefs/app_settings.py:26 +#: userprefs/app_settings.py:27 msgid "Prefered language to display pages" msgstr "Bevorzugte Sprache für Seitendarstellung" @@ -2549,12 +2542,8 @@ msgid "Forward messages and store copies into your local mailbox" msgstr "Nachricht weiterleiten und Kopie in Mailbox ablegen" #: userprefs/forms.py:84 -msgid "" -"You can't define a forward to a local destination. Please ask your " -"administrator to create an alias instead." -msgstr "" -"Sie können keine Weiterleitung auf ein lokales Ziel anlegen. Bitten Sie " -"Ihren Administrator stattdessen einen Alias anzulegen." +msgid "You can't define a forward to a local destination. Please ask your administrator to create an alias instead." +msgstr "Sie können keine Weiterleitung auf ein lokales Ziel anlegen. Bitten Sie Ihren Administrator stattdessen einen Alias anzulegen." #: userprefs/views.py:32 msgid "You need a mailbox in order to define a forward" @@ -2603,6 +2592,30 @@ msgstr "Aktualisieren sie ihre persönlichen Daten" msgid "Logout" msgstr "Logout" +#~ msgid "Failed to rename mailbox: %s" +#~ msgstr "Mailbox konnte nicht umbenannt werden: %s" + +#~ msgid "Failed to remove mailbox: %s" +#~ msgstr "Konnte Mailbox nicht entfernen: %s" + +#~ msgid "Mailbox %s does not exist" +#~ msgstr "Mailbox %s existiert nicht" + +#~ msgid "Delete the domain" +#~ msgstr "Domain entfernen" + +#~ msgid "Delete this account" +#~ msgstr "Dieses Konto löschen" + +#~ msgid "Delete this distribution list" +#~ msgstr "Diese Verteilerliste löschen" + +#~ msgid "Delete this forward" +#~ msgstr "Weiterleitung entfernen" + +#~ msgid "Delete this alias" +#~ msgstr " Alias entfernen" + #~ msgid "Path to the root directory where messages are stored" #~ msgstr "Pfad zum root-Verzeichnis unterhalb dessen E-Mails abgelegt werden" diff --git a/modoboa/locale/en/LC_MESSAGES/django.po b/modoboa/locale/en/LC_MESSAGES/django.po index 1fb68f940..30fda01ce 100644 --- a/modoboa/locale/en/LC_MESSAGES/django.po +++ b/modoboa/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-06-01 16:35+0200\n" +"POT-Creation-Date: 2013-09-20 22:30+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -25,8 +25,8 @@ msgstr "" msgid "Web based console to manage domains, accounts and aliases" msgstr "" -#: admin/__init__.py:16 admin/forms.py:179 admin/forms.py:614 -#: admin/views.py:93 admin/views.py:441 +#: admin/__init__.py:16 admin/forms.py:211 admin/forms.py:652 +#: admin/views.py:94 admin/views.py:447 #: extensions/postfix_autoreply/app_settings.py:11 #: extensions/sievefilters/app_settings.py:50 #: extensions/stats/app_settings.py:11 extensions/webmail/app_settings.py:11 @@ -236,37 +236,45 @@ msgstr "" msgid "The default redirection used when no application is specified" msgstr "" -#: admin/app_settings.py:230 +#: admin/app_settings.py:213 +msgid "Key must be either 16, 24, or 32 bytes long" +msgstr "" + +#: admin/app_settings.py:222 +msgid "Invalid syntax" +msgstr "" + +#: admin/app_settings.py:245 msgid "This field is required" msgstr "" -#: admin/forms.py:16 admin/forms.py:459 admin/tables.py:18 +#: admin/forms.py:20 admin/forms.py:497 admin/tables.py:18 msgid "Alias(es)" msgstr "" -#: admin/forms.py:18 +#: admin/forms.py:22 msgid "" "Alias(es) of this domain. Indicate only one name per input, press ENTER to " "add a new input." msgstr "" -#: admin/forms.py:55 +#: admin/forms.py:59 msgid "An alias with this name already exists" msgstr "" -#: admin/forms.py:69 +#: admin/forms.py:73 msgid "A domain with this name already exists" msgstr "" -#: admin/forms.py:114 +#: admin/forms.py:146 msgid "Create a domain administrator" msgstr "" -#: admin/forms.py:116 +#: admin/forms.py:148 msgid "Automatically create an administrator for this domain" msgstr "" -#: admin/forms.py:120 admin/tables.py:12 admin/tables.py:47 +#: admin/forms.py:152 admin/tables.py:12 admin/tables.py:47 #: admin/templates/admin/editdomainform.html:13 #: admin/templates/admin/identities_table.html:5 #: admin/templates/admin/information.html:8 @@ -275,349 +283,348 @@ msgstr "" msgid "Name" msgstr "" -#: admin/forms.py:122 +#: admin/forms.py:154 msgid "" "The administrator's name. Don't include the domain's name here, it will be " "automatically appended." msgstr "" -#: admin/forms.py:128 +#: admin/forms.py:160 msgid "Create aliases" msgstr "" -#: admin/forms.py:130 +#: admin/forms.py:162 msgid "Automatically create standard aliases for this domain" msgstr "" -#: admin/forms.py:143 +#: admin/forms.py:175 msgid "Invalid format" msgstr "" -#: admin/forms.py:155 +#: admin/forms.py:187 #, python-format msgid "User '%s' already exists" msgstr "" -#: admin/forms.py:203 +#: admin/forms.py:235 msgid "Email address" msgstr "" -#: admin/forms.py:204 +#: admin/forms.py:236 msgid "" "The distribution list address. Use the '*' character to create a 'catchall' " "address (ex: *@domain.tld)." msgstr "" -#: admin/forms.py:207 +#: admin/forms.py:239 msgid "Recipients" msgstr "" -#: admin/forms.py:208 +#: admin/forms.py:240 msgid "" "Mailbox(es) this alias will point to. Indicate only one address per input, " "press ENTER to add a new input." msgstr "" -#: admin/forms.py:246 admin/forms.py:504 admin/forms.py:530 -#: admin/models.py:1147 +#: admin/forms.py:278 admin/forms.py:542 admin/forms.py:568 msgid "Domain does not exist" msgstr "" -#: admin/forms.py:248 +#: admin/forms.py:280 msgid "You don't have access to this domain" msgstr "" -#: admin/forms.py:268 +#: admin/forms.py:300 msgid "Invalid mailbox" msgstr "" -#: admin/forms.py:284 +#: admin/forms.py:316 admin/models.py:1202 #, python-format msgid "Local recipient %s not found" msgstr "" -#: admin/forms.py:286 admin/forms.py:292 +#: admin/forms.py:318 admin/forms.py:324 #, python-format msgid "Recipient %s already present" msgstr "" -#: admin/forms.py:297 +#: admin/forms.py:329 msgid "No recipient defined" msgstr "" -#: admin/forms.py:311 extensions/webmail/forms.py:19 +#: admin/forms.py:343 extensions/webmail/forms.py:19 msgid "Select a file" msgstr "" -#: admin/forms.py:313 admin/forms.py:341 +#: admin/forms.py:345 admin/forms.py:373 msgid "Separator" msgstr "" -#: admin/forms.py:316 +#: admin/forms.py:348 msgid "Continue on error" msgstr "" -#: admin/forms.py:317 +#: admin/forms.py:349 msgid "Don't treat duplicated objects as error" msgstr "" -#: admin/forms.py:332 +#: admin/forms.py:364 msgid "Crypt passwords" msgstr "" -#: admin/forms.py:333 +#: admin/forms.py:365 msgid "Check this option if passwords contained in your file are not crypted" msgstr "" -#: admin/forms.py:339 +#: admin/forms.py:371 msgid "File name" msgstr "" -#: admin/forms.py:371 admin/templates/admin/editdomainform.html:13 +#: admin/forms.py:403 admin/templates/admin/editdomainform.html:13 #: auth/forms.py:7 msgid "Username" msgstr "" -#: admin/forms.py:373 +#: admin/forms.py:405 msgid "Role" msgstr "" -#: admin/forms.py:374 admin/forms.py:401 +#: admin/forms.py:406 admin/forms.py:433 msgid "Choose" msgstr "" -#: admin/forms.py:375 +#: admin/forms.py:407 msgid "What level of permission this user will have" msgstr "" -#: admin/forms.py:377 auth/forms.py:11 +#: admin/forms.py:409 auth/forms.py:11 msgid "Password" msgstr "" -#: admin/forms.py:379 userprefs/forms.py:18 +#: admin/forms.py:411 userprefs/forms.py:18 msgid "Confirmation" msgstr "" -#: admin/forms.py:381 +#: admin/forms.py:413 msgid "Enter the same password as above, for verification." msgstr "" -#: admin/forms.py:392 +#: admin/forms.py:424 msgid "Enabled" msgstr "" -#: admin/forms.py:434 +#: admin/forms.py:471 msgid "The two password fields didn't match." msgstr "" -#: admin/forms.py:440 +#: admin/forms.py:477 msgid "You can't disable your own account" msgstr "" -#: admin/forms.py:450 +#: admin/forms.py:488 msgid "E-mail" msgstr "" -#: admin/forms.py:452 +#: admin/forms.py:490 msgid "Quota" msgstr "" -#: admin/forms.py:454 +#: admin/forms.py:492 msgid "" "Quota in MB for this mailbox. Define a custom value or use domain's default " "one. Leave empty to define an unlimited value (not allowed for domain " "administrators)." msgstr "" -#: admin/forms.py:461 +#: admin/forms.py:499 msgid "" "Alias(es) of this mailbox. Indicate only one address per input, press ENTER " "to add a new input. Use the '*' character to create a 'catchall' alias (ex: " "*@domain.tld)." msgstr "" -#: admin/forms.py:512 +#: admin/forms.py:550 #, python-format msgid "Mailbox %s already exists" msgstr "" -#: admin/forms.py:572 +#: admin/forms.py:610 msgid "Domain(s)" msgstr "" -#: admin/forms.py:574 +#: admin/forms.py:612 msgid "Domain(s) that user administrates" msgstr "" -#: admin/forms.py:616 admin/views.py:444 +#: admin/forms.py:654 admin/views.py:450 msgid "Mail" msgstr "" -#: admin/forms.py:618 +#: admin/forms.py:656 msgid "Permissions" msgstr "" -#: admin/models.py:78 +#: admin/models.py:80 msgid "You can't delete your own account" msgstr "" -#: admin/models.py:159 +#: admin/models.py:161 msgid "Failed to update password: LDAP module not installed" msgstr "" -#: admin/models.py:165 +#: admin/models.py:167 #, python-format msgid "Failed to update password: %s" msgstr "" -#: admin/models.py:192 +#: admin/models.py:194 msgid "account" msgstr "" -#: admin/models.py:494 admin/models.py:720 admin/models.py:782 +#: admin/models.py:496 admin/models.py:733 admin/models.py:795 msgid "Invalid line" msgstr "" -#: admin/models.py:498 +#: admin/models.py:500 msgid "You can't import an account with a role greater than yours" msgstr "" -#: admin/models.py:518 +#: admin/models.py:506 +#, python-format +msgid "The simple user '%s' must have a valid email address" +msgstr "" + +#: admin/models.py:510 +#, python-format +msgid "username and email fields must not differ for '%s'" +msgstr "" + +#: admin/models.py:534 #, python-format msgid "Account import failed (%s): domain does not exist" msgstr "" -#: admin/models.py:607 admin/models.py:733 +#: admin/models.py:624 admin/models.py:746 msgid "name" msgstr "" -#: admin/models.py:608 +#: admin/models.py:625 msgid "The domain name" msgstr "" -#: admin/models.py:610 +#: admin/models.py:627 msgid "Default quota in MB applied to mailboxes" msgstr "" -#: admin/models.py:613 admin/models.py:740 admin/models.py:1020 -#: admin/models.py:1181 extensions/postfix_autoreply/models.py:19 +#: admin/models.py:630 admin/models.py:753 admin/models.py:1045 +#: admin/models.py:1217 extensions/postfix_autoreply/models.py:27 msgid "enabled" msgstr "" -#: admin/models.py:614 +#: admin/models.py:631 msgid "Check to activate this domain" msgstr "" -#: admin/models.py:734 +#: admin/models.py:747 msgid "The alias name" msgstr "" -#: admin/models.py:736 +#: admin/models.py:749 msgid "target" msgstr "" -#: admin/models.py:737 +#: admin/models.py:750 msgid "The domain this alias points to" msgstr "" -#: admin/models.py:741 admin/models.py:1021 +#: admin/models.py:754 admin/models.py:1046 msgid "Check to activate this alias" msgstr "" -#: admin/models.py:788 +#: admin/models.py:801 #, python-format msgid "Unknown domain %s" msgstr "" -#: admin/models.py:805 admin/models.py:1006 +#: admin/models.py:818 admin/models.py:1031 msgid "address" msgstr "" -#: admin/models.py:806 +#: admin/models.py:819 msgid "Mailbox address (without the @domain.tld part)" msgstr "" -#: admin/models.py:852 +#: admin/models.py:877 #, python-format msgid "Failed to retrieve mailbox location (%s)" msgstr "" -#: admin/models.py:868 -#, python-format -msgid "Failed to rename mailbox: %s" -msgstr "" - -#: admin/models.py:894 -#, python-format -msgid "Failed to remove mailbox: %s" -msgstr "" - -#: admin/models.py:909 +#: admin/models.py:921 #, python-format msgid "Quota is greater than the allowed domain's limit (%dM)" msgstr "" -#: admin/models.py:914 +#: admin/models.py:926 msgid "A quota is required" msgstr "" -#: admin/models.py:1007 +#: admin/models.py:1032 msgid "" "The alias address (without the domain part). For a 'catch-all' address, just " "enter an * character." msgstr "" -#: admin/models.py:1011 +#: admin/models.py:1036 msgid "mailboxes" msgstr "" -#: admin/models.py:1012 +#: admin/models.py:1037 msgid "The mailboxes this alias points to" msgstr "" -#: admin/models.py:1016 +#: admin/models.py:1041 msgid "The aliases this alias points to" msgstr "" -#: admin/models.py:1064 +#: admin/models.py:1089 msgid "distribution list" msgstr "" -#: admin/models.py:1065 +#: admin/models.py:1090 msgid "forward" msgstr "" -#: admin/models.py:1066 +#: admin/models.py:1091 msgid "alias" msgstr "" -#: admin/models.py:1142 +#: admin/models.py:1170 #, python-format msgid "Invalid line: %s" msgstr "" -#: admin/models.py:1167 +#: admin/models.py:1175 #, python-format -msgid "Mailbox %s does not exist" +msgid "Domain '%s' does not exist" msgstr "" -#: admin/models.py:1182 +#: admin/models.py:1218 msgid "Check to enable this extension" msgstr "" -#: admin/models.py:1282 +#: admin/models.py:1318 msgid "added" msgstr "" -#: admin/models.py:1285 +#: admin/models.py:1321 msgid "modified" msgstr "" -#: admin/models.py:1288 +#: admin/models.py:1324 msgid "deleted" msgstr "" -#: admin/models.py:1290 +#: admin/models.py:1326 #, python-format msgid "%(object)s '%(name)s' %(action)s by user %(user)s" msgstr "" @@ -627,7 +634,7 @@ msgid "Edit domain" msgstr "" #: admin/tables.py:21 admin/templates/admin/identities_table.html:8 -#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:79 +#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:83 #: extensions/sievefilters/templates/sievefilters/filter.html:17 #: extensions/sievefilters/templates/sievefilters/guieditor.html:5 #: extensions/webmail/templatetags/webextras.py:97 @@ -642,143 +649,143 @@ msgstr "" msgid "Description" msgstr "" -#: admin/views.py:87 +#: admin/views.py:88 msgid "New domain" msgstr "" -#: admin/views.py:88 admin/views.py:97 admin/views.py:225 admin/views.py:438 -#: admin/views.py:445 extensions/sievefilters/views.py:120 -#: extensions/sievefilters/views.py:187 extensions/webmail/views.py:131 +#: admin/views.py:89 admin/views.py:98 admin/views.py:228 admin/views.py:444 +#: admin/views.py:451 extensions/sievefilters/views.py:129 +#: extensions/sievefilters/views.py:198 extensions/webmail/views.py:136 msgid "Create" msgstr "" -#: admin/views.py:94 admin/views.py:442 +#: admin/views.py:95 admin/views.py:448 msgid "Next" msgstr "" -#: admin/views.py:96 +#: admin/views.py:97 msgid "Options" msgstr "" -#: admin/views.py:98 admin/views.py:446 +#: admin/views.py:99 admin/views.py:452 msgid "Previous" msgstr "" -#: admin/views.py:111 +#: admin/views.py:112 msgid "Domain created" msgstr "" -#: admin/views.py:144 admin/views.py:282 admin/views.py:492 +#: admin/views.py:146 admin/views.py:285 admin/views.py:498 #: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:9 -#: extensions/sievefilters/views.py:139 extensions/webmail/views.py:162 +#: extensions/sievefilters/views.py:148 extensions/webmail/views.py:168 #: userprefs/templates/userprefs/forward.html:8 #: userprefs/templates/userprefs/profile.html:8 msgid "Update" msgstr "" -#: admin/views.py:160 +#: admin/views.py:162 msgid "Domain modified" msgstr "" -#: admin/views.py:186 +#: admin/views.py:189 msgid "You can't delete your own domain" msgstr "" -#: admin/views.py:189 +#: admin/views.py:192 msgid "Domain deleted" msgid_plural "Domains deleted" msgstr[0] "" msgstr[1] "" -#: admin/views.py:204 +#: admin/views.py:207 msgid "Alias with this name already exists" msgstr "" -#: admin/views.py:247 +#: admin/views.py:250 msgid "New distribution list" msgstr "" -#: admin/views.py:248 +#: admin/views.py:251 msgid "Distribution list created" msgstr "" -#: admin/views.py:257 +#: admin/views.py:260 msgid "New alias" msgstr "" -#: admin/views.py:258 +#: admin/views.py:261 msgid "Alias created" msgstr "" -#: admin/views.py:267 +#: admin/views.py:270 msgid "New forward" msgstr "" -#: admin/views.py:268 +#: admin/views.py:271 msgid "Forward created" msgstr "" -#: admin/views.py:286 +#: admin/views.py:289 msgid "Distribution list modified" msgstr "" -#: admin/views.py:288 +#: admin/views.py:291 msgid "Forward modified" msgstr "" -#: admin/views.py:290 +#: admin/views.py:293 msgid "Alias modified" msgstr "" -#: admin/views.py:403 admin/views.py:541 admin/views.py:546 -#: extensions/amavis/views.py:195 extensions/amavis/views.py:201 -#: extensions/amavis/views.py:234 extensions/amavis/views.py:238 -#: extensions/amavis/views.py:240 extensions/webmail/views.py:36 -#: extensions/webmail/views.py:64 extensions/webmail/views.py:77 -#: extensions/webmail/views.py:93 extensions/webmail/views.py:109 -#: extensions/webmail/views.py:193 extensions/webmail/views.py:419 -#: extensions/webmail/views.py:430 extensions/webmail/views.py:457 +#: admin/views.py:402 admin/views.py:545 admin/views.py:550 +#: extensions/amavis/views.py:204 extensions/amavis/views.py:210 +#: extensions/amavis/views.py:243 extensions/amavis/views.py:247 +#: extensions/amavis/views.py:249 extensions/webmail/views.py:41 +#: extensions/webmail/views.py:68 extensions/webmail/views.py:81 +#: extensions/webmail/views.py:98 extensions/webmail/views.py:114 +#: extensions/webmail/views.py:200 extensions/webmail/views.py:427 +#: extensions/webmail/views.py:438 extensions/webmail/views.py:465 #: lib/formutils.py:32 lib/formutils.py:37 msgid "Invalid request" msgstr "" -#: admin/views.py:435 +#: admin/views.py:441 msgid "New account" msgstr "" -#: admin/views.py:459 +#: admin/views.py:465 msgid "Account created" msgstr "" -#: admin/views.py:507 +#: admin/views.py:513 msgid "Account updated" msgstr "" -#: admin/views.py:531 +#: admin/views.py:535 msgid "Account deleted" msgid_plural "Accounts deleted" msgstr[0] "" msgstr[1] "" -#: admin/views.py:586 +#: admin/views.py:590 msgid "Parameters saved" msgstr "" -#: admin/views.py:632 +#: admin/views.py:636 msgid "Modifications applied." msgstr "" -#: admin/views.py:740 +#: admin/views.py:743 #, python-format msgid "Object already exists: %s" msgstr "" -#: admin/views.py:742 +#: admin/views.py:745 #, python-format msgid "%d objects imported successfully" msgstr "" -#: admin/views.py:760 +#: admin/views.py:763 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
      \n" @@ -794,25 +801,25 @@ msgid "" "

      You can use a different character as separator.

      \n" msgstr "" -#: admin/views.py:772 +#: admin/views.py:775 msgid "Import identities" msgstr "" -#: admin/views.py:773 admin/views.py:843 admin/templatetags/admin_extras.py:87 +#: admin/views.py:776 admin/views.py:846 admin/templatetags/admin_extras.py:87 #: admin/templatetags/admin_extras.py:144 msgid "Import" msgstr "" -#: admin/views.py:803 +#: admin/views.py:806 msgid "Export identities" msgstr "" -#: admin/views.py:804 admin/views.py:860 admin/templatetags/admin_extras.py:93 +#: admin/views.py:807 admin/views.py:863 admin/templatetags/admin_extras.py:93 #: admin/templatetags/admin_extras.py:150 msgid "Export" msgstr "" -#: admin/views.py:832 +#: admin/views.py:835 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
        \n" @@ -824,11 +831,11 @@ msgid "" "

        You can use a different character as separator.

        \n" msgstr "" -#: admin/views.py:842 +#: admin/views.py:845 msgid "Import domains" msgstr "" -#: admin/views.py:859 +#: admin/views.py:862 msgid "Export domains" msgstr "" @@ -901,7 +908,7 @@ msgid "Tags" msgstr "" #: admin/templates/admin/importform.html:5 -#: admin/templatetags/admin_extras.py:305 +#: admin/templatetags/admin_extras.py:290 msgid "Help" msgstr "" @@ -948,7 +955,7 @@ msgid "Previously on Modoboa..." msgstr "" #: admin/templates/admin/logs.html:6 extensions/amavis/sql_listing.py:25 -#: extensions/webmail/lib.py:37 extensions/webmail/lib.py:406 +#: extensions/webmail/lib.py:38 extensions/webmail/lib.py:411 msgid "Date" msgstr "" @@ -1051,34 +1058,21 @@ msgid "View the domain's identities" msgstr "" #: admin/templatetags/admin_extras.py:178 -msgid "Delete the domain" -msgstr "" - #: admin/templatetags/admin_extras.py:194 -msgid "Delete this account" +#: admin/templatetags/admin_extras.py:201 +#, python-format +msgid "Delete %s?" msgstr "" -#: admin/templatetags/admin_extras.py:202 -msgid "Delete this distribution list" -msgstr "" - -#: admin/templatetags/admin_extras.py:209 -msgid "Delete this forward" -msgstr "" - -#: admin/templatetags/admin_extras.py:216 -msgid "Delete this alias" -msgstr "" - -#: admin/templatetags/admin_extras.py:238 +#: admin/templatetags/admin_extras.py:223 msgid "Remove this permission" msgstr "" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 msgid "Yes" msgstr "" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 msgid "No" msgstr "" @@ -1087,11 +1081,11 @@ msgstr "" msgid "User '%s' successfully logged in" msgstr "" -#: auth/views.py:41 +#: auth/views.py:44 msgid "Your username and password didn't match. Please try again." msgstr "" -#: auth/views.py:64 +#: auth/views.py:67 #, python-format msgid "User '%s' logged out" msgstr "" @@ -1137,16 +1131,16 @@ msgstr "" msgid "Quarantine" msgstr "" -#: extensions/amavis/__init__.py:127 +#: extensions/amavis/__init__.py:129 msgid "pending requests" msgstr "" -#: extensions/amavis/__init__.py:144 +#: extensions/amavis/__init__.py:149 #, python-format msgid "%d pending requests" msgstr "" -#: extensions/amavis/__init__.py:156 +#: extensions/amavis/__init__.py:161 msgid "Content filter" msgstr "" @@ -1261,12 +1255,12 @@ msgid "Virus filter" msgstr "" #: extensions/amavis/models.py:94 extensions/amavis/models.py:102 -#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:268 +#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:281 msgid "yes" msgstr "" #: extensions/amavis/models.py:95 extensions/amavis/models.py:103 -#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:272 +#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:285 msgid "no" msgstr "" @@ -1306,13 +1300,13 @@ msgid "" "settings." msgstr "" -#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:36 +#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:37 msgid "From" msgstr "" #: extensions/amavis/sql_listing.py:24 extensions/sievefilters/forms.py:68 -#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:35 -#: extensions/webmail/lib.py:404 templates/common/email_searchbar.html:6 +#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:36 +#: extensions/webmail/lib.py:409 templates/common/email_searchbar.html:6 msgid "Subject" msgstr "" @@ -1322,37 +1316,37 @@ msgstr "" msgid "To" msgstr "" -#: extensions/amavis/views.py:34 +#: extensions/amavis/views.py:43 msgid "Empty quarantine" msgstr "" -#: extensions/amavis/views.py:202 +#: extensions/amavis/views.py:211 msgid "Message deleted" msgstr "" -#: extensions/amavis/views.py:223 +#: extensions/amavis/views.py:232 #, python-format msgid "%(count)d message deleted successfully" msgid_plural "%(count)d messages deleted successfully" msgstr[0] "" msgstr[1] "" -#: extensions/amavis/views.py:243 +#: extensions/amavis/views.py:252 msgid "Request sent" msgstr "" -#: extensions/amavis/views.py:249 +#: extensions/amavis/views.py:258 msgid "Message released" msgstr "" -#: extensions/amavis/views.py:271 +#: extensions/amavis/views.py:280 #, python-format msgid "%(count)d request sent" msgid_plural "%(count)d requests sent" msgstr[0] "" msgstr[1] "" -#: extensions/amavis/views.py:295 +#: extensions/amavis/views.py:304 #, python-format msgid "%(count)d message released successfully" msgid_plural "%(count)d messages released successfully" @@ -1473,7 +1467,7 @@ msgstr "" msgid "Reseller" msgstr "" -#: extensions/limits/__init__.py:96 +#: extensions/limits/__init__.py:94 msgid "Resources" msgstr "" @@ -1592,23 +1586,23 @@ msgstr "" msgid "This date is over" msgstr "" -#: extensions/postfix_autoreply/models.py:15 +#: extensions/postfix_autoreply/models.py:19 msgid "subject" msgstr "" -#: extensions/postfix_autoreply/models.py:16 +#: extensions/postfix_autoreply/models.py:20 msgid "The subject that will appear in sent emails" msgstr "" -#: extensions/postfix_autoreply/models.py:17 +#: extensions/postfix_autoreply/models.py:23 msgid "content" msgstr "" -#: extensions/postfix_autoreply/models.py:18 +#: extensions/postfix_autoreply/models.py:24 msgid "The content that will appear in sent emails" msgstr "" -#: extensions/postfix_autoreply/models.py:20 +#: extensions/postfix_autoreply/models.py:28 msgid "Activate/Deactivate your auto reply" msgstr "" @@ -1742,53 +1736,53 @@ msgstr "" msgid "Not enough space on server" msgstr "" -#: extensions/sievefilters/views.py:64 +#: extensions/sievefilters/views.py:68 msgid "Failed to retrieve filters set" msgstr "" -#: extensions/sievefilters/views.py:117 +#: extensions/sievefilters/views.py:126 #: extensions/sievefilters/templatetags/sfilters_tags.py:33 msgid "New filter" msgstr "" -#: extensions/sievefilters/views.py:124 +#: extensions/sievefilters/views.py:133 msgid "Filter created" msgstr "" -#: extensions/sievefilters/views.py:136 +#: extensions/sievefilters/views.py:145 #: extensions/sievefilters/templatetags/sfilters_tags.py:77 msgid "Edit filter" msgstr "" -#: extensions/sievefilters/views.py:145 +#: extensions/sievefilters/views.py:154 msgid "Filter modified" msgstr "" -#: extensions/sievefilters/views.py:165 +#: extensions/sievefilters/views.py:176 msgid "Filter removed" msgstr "" -#: extensions/sievefilters/views.py:166 +#: extensions/sievefilters/views.py:177 msgid "Failed to remove filter" msgstr "" -#: extensions/sievefilters/views.py:180 +#: extensions/sievefilters/views.py:191 msgid "Filters set saved" msgstr "" -#: extensions/sievefilters/views.py:185 +#: extensions/sievefilters/views.py:196 msgid "Create a new filters set" msgstr "" -#: extensions/sievefilters/views.py:208 +#: extensions/sievefilters/views.py:219 msgid "Filters set created" msgstr "" -#: extensions/sievefilters/views.py:229 +#: extensions/sievefilters/views.py:240 msgid "Filters set deleted" msgstr "" -#: extensions/sievefilters/views.py:241 +#: extensions/sievefilters/views.py:252 msgid "Filters set activated" msgstr "" @@ -1804,15 +1798,15 @@ msgstr "" msgid "No filter defined yet" msgstr "" -#: extensions/sievefilters/templates/sievefilters/index.html:55 +#: extensions/sievefilters/templates/sievefilters/index.html:56 msgid "Filter sets" msgstr "" -#: extensions/sievefilters/templates/sievefilters/index.html:59 +#: extensions/sievefilters/templates/sievefilters/index.html:60 msgid "active" msgstr "" -#: extensions/sievefilters/templates/sievefilters/index.html:73 +#: extensions/sievefilters/templates/sievefilters/index.html:74 msgid "" "No filters set defined yet. You must create one in order to define filters." msgstr "" @@ -2127,84 +2121,88 @@ msgstr "" msgid "Cci" msgstr "" -#: extensions/webmail/imaputils.py:259 +#: extensions/webmail/imaputils.py:237 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "" + +#: extensions/webmail/imaputils.py:278 #, python-format msgid "Connection to IMAP server failed: %s" msgstr "" -#: extensions/webmail/lib.py:353 +#: extensions/webmail/lib.py:358 msgid "wrote:" msgstr "" -#: extensions/webmail/lib.py:418 extensions/webmail/lib.py:422 +#: extensions/webmail/lib.py:423 extensions/webmail/lib.py:427 msgid "Original message" msgstr "" -#: extensions/webmail/views.py:81 +#: extensions/webmail/views.py:86 #, python-format msgid "%(count)d message deleted" msgid_plural "%(count)d messages deleted" msgstr[0] "" msgstr[1] "" -#: extensions/webmail/views.py:98 +#: extensions/webmail/views.py:103 msgid "Unknown action" msgstr "" -#: extensions/webmail/views.py:111 lib/email_listing.py:137 +#: extensions/webmail/views.py:116 lib/email_listing.py:137 msgid "Empty mailbox" msgstr "" -#: extensions/webmail/views.py:128 -#: extensions/webmail/templatetags/webextras.py:169 +#: extensions/webmail/views.py:133 +#: extensions/webmail/templatetags/webextras.py:170 msgid "Create a new mailbox" msgstr "" -#: extensions/webmail/views.py:143 +#: extensions/webmail/views.py:149 msgid "Mailbox created" msgstr "" -#: extensions/webmail/views.py:159 +#: extensions/webmail/views.py:165 msgid "Edit mailbox" msgstr "" -#: extensions/webmail/views.py:173 +#: extensions/webmail/views.py:180 msgid "Mailbox updated" msgstr "" -#: extensions/webmail/views.py:207 extensions/webmail/views.py:388 -#: extensions/webmail/views.py:497 +#: extensions/webmail/views.py:214 extensions/webmail/views.py:396 +#: extensions/webmail/views.py:505 msgid "Bad request" msgstr "" -#: extensions/webmail/views.py:238 +#: extensions/webmail/views.py:246 msgid "Failed to save attachment: " msgstr "" -#: extensions/webmail/views.py:241 +#: extensions/webmail/views.py:249 #, python-format msgid "Attachment is too big (limit: %s)" msgstr "" -#: extensions/webmail/views.py:247 +#: extensions/webmail/views.py:255 #: extensions/webmail/templates/webmail/compose.html:15 #: extensions/webmail/templates/webmail/headers.html:4 msgid "Attachments" msgstr "" -#: extensions/webmail/views.py:263 +#: extensions/webmail/views.py:271 msgid "Bad query" msgstr "" -#: extensions/webmail/views.py:273 +#: extensions/webmail/views.py:281 msgid "Failed to remove attachment: " msgstr "" -#: extensions/webmail/views.py:278 +#: extensions/webmail/views.py:286 msgid "Unknown attachment" msgstr "" -#: extensions/webmail/views.py:493 +#: extensions/webmail/views.py:501 msgid "Undefined action" msgstr "" @@ -2235,7 +2233,7 @@ msgstr[1] "" msgid "Parent mailbox" msgstr "" -#: extensions/webmail/templates/webmail/index.html:36 +#: extensions/webmail/templates/webmail/index.html:38 msgid "Mailbox quota" msgstr "" @@ -2297,15 +2295,15 @@ msgstr "" msgid "Compress folder" msgstr "" -#: extensions/webmail/templatetags/webextras.py:109 +#: extensions/webmail/templatetags/webextras.py:110 msgid "Empty folder" msgstr "" -#: extensions/webmail/templatetags/webextras.py:177 +#: extensions/webmail/templatetags/webextras.py:178 msgid "Edit the selected mailbox" msgstr "" -#: extensions/webmail/templatetags/webextras.py:182 +#: extensions/webmail/templatetags/webextras.py:183 msgid "Remove the selected mailbox" msgstr "" @@ -2359,7 +2357,7 @@ msgstr "" msgid "An internal error occured." msgstr "" -#: templates/nlayout.html:56 +#: templates/nlayout.html:62 msgid "Powered by Modoboa" msgstr "" @@ -2393,11 +2391,11 @@ msgstr "" msgid "Welcome to Modoboa" msgstr "" -#: templates/registration/login.html:33 +#: templates/registration/login.html:34 msgid "Remember me" msgstr "" -#: templates/registration/login.html:35 +#: templates/registration/login.html:36 msgid "Log in" msgstr "" @@ -2413,7 +2411,7 @@ msgstr "" msgid "Prefered language" msgstr "" -#: userprefs/app_settings.py:26 +#: userprefs/app_settings.py:27 msgid "Prefered language to display pages" msgstr "" diff --git a/modoboa/locale/es/LC_MESSAGES/django.po b/modoboa/locale/es/LC_MESSAGES/django.po index eac45d1bf..b29cc231a 100644 --- a/modoboa/locale/es/LC_MESSAGES/django.po +++ b/modoboa/locale/es/LC_MESSAGES/django.po @@ -9,8 +9,8 @@ msgid "" msgstr "" "Project-Id-Version: Modoboa\n" "Report-Msgid-Bugs-To: http://dev.modoboa.org/\n" -"POT-Creation-Date: 2013-06-01 16:35+0200\n" -"PO-Revision-Date: 2013-06-03 16:33+0000\n" +"POT-Creation-Date: 2013-09-20 22:30+0200\n" +"PO-Revision-Date: 2013-10-01 09:56+0000\n" "Last-Translator: Pedro Gracia \n" "Language-Team: Spanish (http://www.transifex.com/projects/p/modoboa/language/es/)\n" "MIME-Version: 1.0\n" @@ -27,8 +27,8 @@ msgstr "Consola de administración" msgid "Web based console to manage domains, accounts and aliases" msgstr "Consola web para administrar dominios, cuentas y alias" -#: admin/__init__.py:16 admin/forms.py:179 admin/forms.py:614 -#: admin/views.py:93 admin/views.py:441 +#: admin/__init__.py:16 admin/forms.py:211 admin/forms.py:652 +#: admin/views.py:94 admin/views.py:447 #: extensions/postfix_autoreply/app_settings.py:11 #: extensions/sievefilters/app_settings.py:50 #: extensions/stats/app_settings.py:11 extensions/webmail/app_settings.py:11 @@ -238,37 +238,45 @@ msgstr "Redirección principal por defecto" msgid "The default redirection used when no application is specified" msgstr "La redirección que será usada cuando ninguna aplicación sea especificada." -#: admin/app_settings.py:230 +#: admin/app_settings.py:213 +msgid "Key must be either 16, 24, or 32 bytes long" +msgstr "La clave debe tener 16, 24 o 32 bytes de longitud" + +#: admin/app_settings.py:222 +msgid "Invalid syntax" +msgstr "Sintaxis inválida" + +#: admin/app_settings.py:245 msgid "This field is required" msgstr "Este campo es obligatorio" -#: admin/forms.py:16 admin/forms.py:459 admin/tables.py:18 +#: admin/forms.py:20 admin/forms.py:497 admin/tables.py:18 msgid "Alias(es)" msgstr "Alias" -#: admin/forms.py:18 +#: admin/forms.py:22 msgid "" "Alias(es) of this domain. Indicate only one name per input, press ENTER to " "add a new input." msgstr "Alia(s) de este dominio. Indique únicamente un nombre por entrada, presione ENTER para añadir una nueva entrada." -#: admin/forms.py:55 +#: admin/forms.py:59 msgid "An alias with this name already exists" msgstr "Ya existe un alias con este nombre" -#: admin/forms.py:69 +#: admin/forms.py:73 msgid "A domain with this name already exists" msgstr "Ya existe un dominio con este nombre" -#: admin/forms.py:114 +#: admin/forms.py:146 msgid "Create a domain administrator" msgstr "Crear un administrador del dominio" -#: admin/forms.py:116 +#: admin/forms.py:148 msgid "Automatically create an administrator for this domain" msgstr "Automáticamente crear una administrador para este dominio" -#: admin/forms.py:120 admin/tables.py:12 admin/tables.py:47 +#: admin/forms.py:152 admin/tables.py:12 admin/tables.py:47 #: admin/templates/admin/editdomainform.html:13 #: admin/templates/admin/identities_table.html:5 #: admin/templates/admin/information.html:8 @@ -277,349 +285,348 @@ msgstr "Automáticamente crear una administrador para este dominio" msgid "Name" msgstr "Nombre" -#: admin/forms.py:122 +#: admin/forms.py:154 msgid "" "The administrator's name. Don't include the domain's name here, it will be " "automatically appended." msgstr "El nombre del administrador. No incluya el nombre del dominio aquí, será añadido automáticamente." -#: admin/forms.py:128 +#: admin/forms.py:160 msgid "Create aliases" msgstr "Crear alias" -#: admin/forms.py:130 +#: admin/forms.py:162 msgid "Automatically create standard aliases for this domain" msgstr "Automáticamente crear alias estándar para este dominio" -#: admin/forms.py:143 +#: admin/forms.py:175 msgid "Invalid format" msgstr "Formato inválido" -#: admin/forms.py:155 +#: admin/forms.py:187 #, python-format msgid "User '%s' already exists" msgstr "El usuario '%s' ya existe" -#: admin/forms.py:203 +#: admin/forms.py:235 msgid "Email address" msgstr "Dirección de email" -#: admin/forms.py:204 +#: admin/forms.py:236 msgid "" "The distribution list address. Use the '*' character to create a 'catchall' " "address (ex: *@domain.tld)." msgstr "La dirección de la lista de distribución. Use el carácter '*' para crear una dirección 'catchall' (recogerlos todos, ej. *@domain.tld)" -#: admin/forms.py:207 +#: admin/forms.py:239 msgid "Recipients" msgstr "Destinatarios" -#: admin/forms.py:208 +#: admin/forms.py:240 msgid "" "Mailbox(es) this alias will point to. Indicate only one address per input, " "press ENTER to add a new input." msgstr "Mailbox(es) a los que apuntará este alias. Indique únicamente una dirección por entrada, presione ENTER para añadir una nueva entrada." -#: admin/forms.py:246 admin/forms.py:504 admin/forms.py:530 -#: admin/models.py:1147 +#: admin/forms.py:278 admin/forms.py:542 admin/forms.py:568 msgid "Domain does not exist" msgstr "El dominio no existe" -#: admin/forms.py:248 +#: admin/forms.py:280 msgid "You don't have access to this domain" msgstr "No tienes acceso a este dominio" -#: admin/forms.py:268 +#: admin/forms.py:300 msgid "Invalid mailbox" msgstr "mailbox inválido" -#: admin/forms.py:284 +#: admin/forms.py:316 admin/models.py:1202 #, python-format msgid "Local recipient %s not found" msgstr "Destinatario local %s no encontrado" -#: admin/forms.py:286 admin/forms.py:292 +#: admin/forms.py:318 admin/forms.py:324 #, python-format msgid "Recipient %s already present" msgstr "EL destinatario %s ya existe" -#: admin/forms.py:297 +#: admin/forms.py:329 msgid "No recipient defined" msgstr "No ha puesto destinatario" -#: admin/forms.py:311 extensions/webmail/forms.py:19 +#: admin/forms.py:343 extensions/webmail/forms.py:19 msgid "Select a file" msgstr "Seleccione un fichero" -#: admin/forms.py:313 admin/forms.py:341 +#: admin/forms.py:345 admin/forms.py:373 msgid "Separator" msgstr "Separador" -#: admin/forms.py:316 +#: admin/forms.py:348 msgid "Continue on error" msgstr "Continúe en error." -#: admin/forms.py:317 +#: admin/forms.py:349 msgid "Don't treat duplicated objects as error" msgstr "No tratar objetos duplicados como error" -#: admin/forms.py:332 +#: admin/forms.py:364 msgid "Crypt passwords" msgstr "Contraseñas cifradas" -#: admin/forms.py:333 +#: admin/forms.py:365 msgid "Check this option if passwords contained in your file are not crypted" msgstr "Marque esta opción si las contraseñas que contiene su fichero no están cifradas" -#: admin/forms.py:339 +#: admin/forms.py:371 msgid "File name" msgstr "Nombre del fichero" -#: admin/forms.py:371 admin/templates/admin/editdomainform.html:13 +#: admin/forms.py:403 admin/templates/admin/editdomainform.html:13 #: auth/forms.py:7 msgid "Username" msgstr "Nombre de usuario" -#: admin/forms.py:373 +#: admin/forms.py:405 msgid "Role" msgstr "Rol" -#: admin/forms.py:374 admin/forms.py:401 +#: admin/forms.py:406 admin/forms.py:433 msgid "Choose" msgstr "Elija" -#: admin/forms.py:375 +#: admin/forms.py:407 msgid "What level of permission this user will have" msgstr "Qué nivel de permiso tendrá este usuario" -#: admin/forms.py:377 auth/forms.py:11 +#: admin/forms.py:409 auth/forms.py:11 msgid "Password" msgstr "Contraseña" -#: admin/forms.py:379 userprefs/forms.py:18 +#: admin/forms.py:411 userprefs/forms.py:18 msgid "Confirmation" msgstr "Confirmación" -#: admin/forms.py:381 +#: admin/forms.py:413 msgid "Enter the same password as above, for verification." msgstr "Introduzca la misma contraseña que arriba, para verificación." -#: admin/forms.py:392 +#: admin/forms.py:424 msgid "Enabled" msgstr "Activado" -#: admin/forms.py:434 +#: admin/forms.py:471 msgid "The two password fields didn't match." msgstr "Las contraseñas no concuerdan" -#: admin/forms.py:440 +#: admin/forms.py:477 msgid "You can't disable your own account" msgstr "No puede deshabilitar su propia cuenta" -#: admin/forms.py:450 +#: admin/forms.py:488 msgid "E-mail" msgstr "Email" -#: admin/forms.py:452 +#: admin/forms.py:490 msgid "Quota" msgstr "Cuota" -#: admin/forms.py:454 +#: admin/forms.py:492 msgid "" "Quota in MB for this mailbox. Define a custom value or use domain's default " "one. Leave empty to define an unlimited value (not allowed for domain " "administrators)." msgstr "Cuota en MB para este mailbox. Defina un valor por defecto o use el del dominio por defecto. Déjelo vacío para definir un valor ilimitado (no permitido para los administradores del dominio)" -#: admin/forms.py:461 +#: admin/forms.py:499 msgid "" "Alias(es) of this mailbox. Indicate only one address per input, press ENTER " "to add a new input. Use the '*' character to create a 'catchall' alias (ex: " "*@domain.tld)." msgstr "Alias de este mailbox. Indique sólo una dirección por entrada, presione ENTER para añadir una nueva entrada. Use el carácter '*' para crear una dirección 'catchall' (recogerlos todos, ej. *@domain.tld)" -#: admin/forms.py:512 +#: admin/forms.py:550 #, python-format msgid "Mailbox %s already exists" msgstr "El mailbox %s ya existe" -#: admin/forms.py:572 +#: admin/forms.py:610 msgid "Domain(s)" msgstr "Dominio(s)" -#: admin/forms.py:574 +#: admin/forms.py:612 msgid "Domain(s) that user administrates" msgstr "Dominio(s) que el usuario administra" -#: admin/forms.py:616 admin/views.py:444 +#: admin/forms.py:654 admin/views.py:450 msgid "Mail" msgstr "Correo" -#: admin/forms.py:618 +#: admin/forms.py:656 msgid "Permissions" msgstr "Permisos" -#: admin/models.py:78 +#: admin/models.py:80 msgid "You can't delete your own account" msgstr "No puede eliminar su propia cuenta" -#: admin/models.py:159 +#: admin/models.py:161 msgid "Failed to update password: LDAP module not installed" msgstr "Falló al actualizar la contraseña: módulo LDAP no instalado" -#: admin/models.py:165 +#: admin/models.py:167 #, python-format msgid "Failed to update password: %s" msgstr "Falló la actualización de la contraseña: %s" -#: admin/models.py:192 +#: admin/models.py:194 msgid "account" msgstr "cuenta" -#: admin/models.py:494 admin/models.py:720 admin/models.py:782 +#: admin/models.py:496 admin/models.py:733 admin/models.py:795 msgid "Invalid line" msgstr "Línea inválida" -#: admin/models.py:498 +#: admin/models.py:500 msgid "You can't import an account with a role greater than yours" msgstr "No puede importar una cuenta con un rol mayor que el suyo" -#: admin/models.py:518 +#: admin/models.py:506 +#, python-format +msgid "The simple user '%s' must have a valid email address" +msgstr "El usuario normal '%s' debe tener una dirección de email válida" + +#: admin/models.py:510 +#, python-format +msgid "username and email fields must not differ for '%s'" +msgstr "Los campos de nombre de usuario y de email no deben ser diferentes para '%s'" + +#: admin/models.py:534 #, python-format msgid "Account import failed (%s): domain does not exist" msgstr "Falló la importación de la cuenta (%s): el dominio no existe" -#: admin/models.py:607 admin/models.py:733 +#: admin/models.py:624 admin/models.py:746 msgid "name" msgstr "nombre" -#: admin/models.py:608 +#: admin/models.py:625 msgid "The domain name" msgstr "Nombre del dominio" -#: admin/models.py:610 +#: admin/models.py:627 msgid "Default quota in MB applied to mailboxes" msgstr "Cuota por defecto en MB que se aplicará a los mailboxes" -#: admin/models.py:613 admin/models.py:740 admin/models.py:1020 -#: admin/models.py:1181 extensions/postfix_autoreply/models.py:19 +#: admin/models.py:630 admin/models.py:753 admin/models.py:1045 +#: admin/models.py:1217 extensions/postfix_autoreply/models.py:27 msgid "enabled" msgstr "activado" -#: admin/models.py:614 +#: admin/models.py:631 msgid "Check to activate this domain" msgstr "Marcar para activar este dominio" -#: admin/models.py:734 +#: admin/models.py:747 msgid "The alias name" msgstr "Nombre del alias" -#: admin/models.py:736 +#: admin/models.py:749 msgid "target" msgstr "objetivo" -#: admin/models.py:737 +#: admin/models.py:750 msgid "The domain this alias points to" msgstr "El dominio al que este alias apunta" -#: admin/models.py:741 admin/models.py:1021 +#: admin/models.py:754 admin/models.py:1046 msgid "Check to activate this alias" msgstr "Marcar para activar este alias" -#: admin/models.py:788 +#: admin/models.py:801 #, python-format msgid "Unknown domain %s" msgstr "Dominio desconocido %s" -#: admin/models.py:805 admin/models.py:1006 +#: admin/models.py:818 admin/models.py:1031 msgid "address" msgstr "dirección" -#: admin/models.py:806 +#: admin/models.py:819 msgid "Mailbox address (without the @domain.tld part)" msgstr "Dirección del mailbox (sin la parte @dominio.tld)" -#: admin/models.py:852 +#: admin/models.py:877 #, python-format msgid "Failed to retrieve mailbox location (%s)" msgstr "Fallo al recuperar la localización del mailbox (%s)" -#: admin/models.py:868 -#, python-format -msgid "Failed to rename mailbox: %s" -msgstr "Fallo al renombrar el mailbox: %s" - -#: admin/models.py:894 -#, python-format -msgid "Failed to remove mailbox: %s" -msgstr "Fallo al eliminar el mailbox: %s" - -#: admin/models.py:909 +#: admin/models.py:921 #, python-format msgid "Quota is greater than the allowed domain's limit (%dM)" msgstr "La cuota es mayor que el límite permitido para el dominio (%dM)" -#: admin/models.py:914 +#: admin/models.py:926 msgid "A quota is required" msgstr "Una cuota es obligatoria" -#: admin/models.py:1007 +#: admin/models.py:1032 msgid "" "The alias address (without the domain part). For a 'catch-all' address, just" " enter an * character." msgstr "La dirección del alias (sin la parte del dominio). Para una dirección que lo recoja todo, ponga un asterisco (*)." -#: admin/models.py:1011 +#: admin/models.py:1036 msgid "mailboxes" msgstr "mailboxes" -#: admin/models.py:1012 +#: admin/models.py:1037 msgid "The mailboxes this alias points to" msgstr "Los 'mailboxes' de este alias apuntan a" -#: admin/models.py:1016 +#: admin/models.py:1041 msgid "The aliases this alias points to" msgstr "Los alias de este alias apuntan a" -#: admin/models.py:1064 +#: admin/models.py:1089 msgid "distribution list" msgstr "Lista de distribución" -#: admin/models.py:1065 +#: admin/models.py:1090 msgid "forward" msgstr "reenvío" -#: admin/models.py:1066 +#: admin/models.py:1091 msgid "alias" msgstr "alias" -#: admin/models.py:1142 +#: admin/models.py:1170 #, python-format msgid "Invalid line: %s" msgstr "Línea inválida: %s" -#: admin/models.py:1167 +#: admin/models.py:1175 #, python-format -msgid "Mailbox %s does not exist" -msgstr "El mailbox %s no existe" +msgid "Domain '%s' does not exist" +msgstr "El dominio '%s' no existe" -#: admin/models.py:1182 +#: admin/models.py:1218 msgid "Check to enable this extension" msgstr "Marcar para activar esta extensión" -#: admin/models.py:1282 +#: admin/models.py:1318 msgid "added" msgstr "añadido" -#: admin/models.py:1285 +#: admin/models.py:1321 msgid "modified" msgstr "modificado" -#: admin/models.py:1288 +#: admin/models.py:1324 msgid "deleted" msgstr "eliminado" -#: admin/models.py:1290 +#: admin/models.py:1326 #, python-format msgid "%(object)s '%(name)s' %(action)s by user %(user)s" msgstr "%(object)s '%(name)s' %(action)s por el usuario %(user)s" @@ -629,7 +636,7 @@ msgid "Edit domain" msgstr "Editar el dominio" #: admin/tables.py:21 admin/templates/admin/identities_table.html:8 -#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:79 +#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:83 #: extensions/sievefilters/templates/sievefilters/filter.html:17 #: extensions/sievefilters/templates/sievefilters/guieditor.html:5 #: extensions/webmail/templatetags/webextras.py:97 @@ -644,143 +651,143 @@ msgstr "Versión" msgid "Description" msgstr "Descripción" -#: admin/views.py:87 +#: admin/views.py:88 msgid "New domain" msgstr "Nuevo dominio" -#: admin/views.py:88 admin/views.py:97 admin/views.py:225 admin/views.py:438 -#: admin/views.py:445 extensions/sievefilters/views.py:120 -#: extensions/sievefilters/views.py:187 extensions/webmail/views.py:131 +#: admin/views.py:89 admin/views.py:98 admin/views.py:228 admin/views.py:444 +#: admin/views.py:451 extensions/sievefilters/views.py:129 +#: extensions/sievefilters/views.py:198 extensions/webmail/views.py:136 msgid "Create" msgstr "Crear" -#: admin/views.py:94 admin/views.py:442 +#: admin/views.py:95 admin/views.py:448 msgid "Next" msgstr "Siguiente" -#: admin/views.py:96 +#: admin/views.py:97 msgid "Options" msgstr "Opciones" -#: admin/views.py:98 admin/views.py:446 +#: admin/views.py:99 admin/views.py:452 msgid "Previous" msgstr "Anterior" -#: admin/views.py:111 +#: admin/views.py:112 msgid "Domain created" msgstr "Dominio creado" -#: admin/views.py:144 admin/views.py:282 admin/views.py:492 +#: admin/views.py:146 admin/views.py:285 admin/views.py:498 #: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:9 -#: extensions/sievefilters/views.py:139 extensions/webmail/views.py:162 +#: extensions/sievefilters/views.py:148 extensions/webmail/views.py:168 #: userprefs/templates/userprefs/forward.html:8 #: userprefs/templates/userprefs/profile.html:8 msgid "Update" msgstr "Actualizar" -#: admin/views.py:160 +#: admin/views.py:162 msgid "Domain modified" msgstr "Dominio modificado" -#: admin/views.py:186 +#: admin/views.py:189 msgid "You can't delete your own domain" msgstr "No puedo eliminar su propio dominio" -#: admin/views.py:189 +#: admin/views.py:192 msgid "Domain deleted" msgid_plural "Domains deleted" msgstr[0] "Dominio eliminado" msgstr[1] "Dominios eliminados" -#: admin/views.py:204 +#: admin/views.py:207 msgid "Alias with this name already exists" msgstr "Ya existe un alias con este nombre" -#: admin/views.py:247 +#: admin/views.py:250 msgid "New distribution list" msgstr "Nueva lista de distribución" -#: admin/views.py:248 +#: admin/views.py:251 msgid "Distribution list created" msgstr "Lista de distribución creada" -#: admin/views.py:257 +#: admin/views.py:260 msgid "New alias" msgstr "Nuevo alias" -#: admin/views.py:258 +#: admin/views.py:261 msgid "Alias created" msgstr "Alias creado" -#: admin/views.py:267 +#: admin/views.py:270 msgid "New forward" msgstr "Nuevo reenvío" -#: admin/views.py:268 +#: admin/views.py:271 msgid "Forward created" msgstr "Reenvío creado" -#: admin/views.py:286 +#: admin/views.py:289 msgid "Distribution list modified" msgstr "Lista de distribución modificada" -#: admin/views.py:288 +#: admin/views.py:291 msgid "Forward modified" msgstr "Reenvío modificado" -#: admin/views.py:290 +#: admin/views.py:293 msgid "Alias modified" msgstr "Alias modificado" -#: admin/views.py:403 admin/views.py:541 admin/views.py:546 -#: extensions/amavis/views.py:195 extensions/amavis/views.py:201 -#: extensions/amavis/views.py:234 extensions/amavis/views.py:238 -#: extensions/amavis/views.py:240 extensions/webmail/views.py:36 -#: extensions/webmail/views.py:64 extensions/webmail/views.py:77 -#: extensions/webmail/views.py:93 extensions/webmail/views.py:109 -#: extensions/webmail/views.py:193 extensions/webmail/views.py:419 -#: extensions/webmail/views.py:430 extensions/webmail/views.py:457 +#: admin/views.py:402 admin/views.py:545 admin/views.py:550 +#: extensions/amavis/views.py:204 extensions/amavis/views.py:210 +#: extensions/amavis/views.py:243 extensions/amavis/views.py:247 +#: extensions/amavis/views.py:249 extensions/webmail/views.py:41 +#: extensions/webmail/views.py:68 extensions/webmail/views.py:81 +#: extensions/webmail/views.py:98 extensions/webmail/views.py:114 +#: extensions/webmail/views.py:200 extensions/webmail/views.py:427 +#: extensions/webmail/views.py:438 extensions/webmail/views.py:465 #: lib/formutils.py:32 lib/formutils.py:37 msgid "Invalid request" msgstr "Petición inválida" -#: admin/views.py:435 +#: admin/views.py:441 msgid "New account" msgstr "Nueva cuenta" -#: admin/views.py:459 +#: admin/views.py:465 msgid "Account created" msgstr "Cuenta creada" -#: admin/views.py:507 +#: admin/views.py:513 msgid "Account updated" msgstr "Cuenta actualizada" -#: admin/views.py:531 +#: admin/views.py:535 msgid "Account deleted" msgid_plural "Accounts deleted" msgstr[0] "Cuenta eliminada" msgstr[1] "Cuentas eliminadas" -#: admin/views.py:586 +#: admin/views.py:590 msgid "Parameters saved" msgstr "Parámetros guardados" -#: admin/views.py:632 +#: admin/views.py:636 msgid "Modifications applied." msgstr "Modificaciones aplicadas." -#: admin/views.py:740 +#: admin/views.py:743 #, python-format msgid "Object already exists: %s" msgstr "El objeto ya existe: %s" -#: admin/views.py:742 +#: admin/views.py:745 #, python-format msgid "%d objects imported successfully" msgstr "%d objetos importados con éxito" -#: admin/views.py:760 +#: admin/views.py:763 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
          \n" @@ -794,25 +801,25 @@ msgid "" "

          You can use a different character as separator.

          \n" msgstr "Importe un fichero CSV dónde las líneas respetan uno de los siguientes formatos:\n
            \n
          • account; loginname; password; first name; last name; enabled; group; address[, domain, ...]
          • ⏎\n
          • alias; address; enabled; internal recipient
          • ⏎\n
          • forward; address; enabled; external recipient
          • ⏎\n
          • dlist; address; enabled; recipient; recipient; ...
          • ⏎\n
          ⏎\n

          El primer elemento de cada línea es obligatorio y debe ser igual al de los valores previos.

          \n\n

          Puede usar un carácter diferente como separador.

          \n" -#: admin/views.py:772 +#: admin/views.py:775 msgid "Import identities" msgstr "Importar identidades" -#: admin/views.py:773 admin/views.py:843 admin/templatetags/admin_extras.py:87 +#: admin/views.py:776 admin/views.py:846 admin/templatetags/admin_extras.py:87 #: admin/templatetags/admin_extras.py:144 msgid "Import" msgstr "Importar" -#: admin/views.py:803 +#: admin/views.py:806 msgid "Export identities" msgstr "Exportar identidades" -#: admin/views.py:804 admin/views.py:860 admin/templatetags/admin_extras.py:93 +#: admin/views.py:807 admin/views.py:863 admin/templatetags/admin_extras.py:93 #: admin/templatetags/admin_extras.py:150 msgid "Export" msgstr "Exportar" -#: admin/views.py:832 +#: admin/views.py:835 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
            \n" @@ -823,11 +830,11 @@ msgid "" "

            You can use a different character as separator.

            \n" msgstr "Provea un fichero CSV dónde las líneas respetan uno de los siguientes formatos:⏎
              ⏎
            • dominio; nombre; cuota; activado
            • ⏎
            • aliasdeldominio; nombre; dominio objetivo; activado
            • ⏎
            ⏎

            El primer elemento de cada línea es obligatorio y debe ser igual a uno de los valores anteriores.

            ⏎

            Puede usar un carácter diferente como separador.

            ⏎\n" -#: admin/views.py:842 +#: admin/views.py:845 msgid "Import domains" msgstr "Importar dominios" -#: admin/views.py:859 +#: admin/views.py:862 msgid "Export domains" msgstr "Exportar dominios" @@ -900,7 +907,7 @@ msgid "Tags" msgstr "Tags" #: admin/templates/admin/importform.html:5 -#: admin/templatetags/admin_extras.py:305 +#: admin/templatetags/admin_extras.py:290 msgid "Help" msgstr "Ayuda" @@ -947,7 +954,7 @@ msgid "Previously on Modoboa..." msgstr "Anteriormente en Modoboa..." #: admin/templates/admin/logs.html:6 extensions/amavis/sql_listing.py:25 -#: extensions/webmail/lib.py:37 extensions/webmail/lib.py:406 +#: extensions/webmail/lib.py:38 extensions/webmail/lib.py:411 msgid "Date" msgstr "Fecha" @@ -1050,34 +1057,21 @@ msgid "View the domain's identities" msgstr "Ver las identidades del dominio" #: admin/templatetags/admin_extras.py:178 -msgid "Delete the domain" -msgstr "Eliminar el dominio" - #: admin/templatetags/admin_extras.py:194 -msgid "Delete this account" -msgstr "Eliminar esta cuenta" +#: admin/templatetags/admin_extras.py:201 +#, python-format +msgid "Delete %s?" +msgstr "¿Eliminar %s?" -#: admin/templatetags/admin_extras.py:202 -msgid "Delete this distribution list" -msgstr "Eliminar esta lista de distribución" - -#: admin/templatetags/admin_extras.py:209 -msgid "Delete this forward" -msgstr "Eliminar este reenvío" - -#: admin/templatetags/admin_extras.py:216 -msgid "Delete this alias" -msgstr "Eliminar este alias" - -#: admin/templatetags/admin_extras.py:238 +#: admin/templatetags/admin_extras.py:223 msgid "Remove this permission" msgstr "Eliminar este permiso" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 msgid "Yes" msgstr "Sí" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 msgid "No" msgstr "No " @@ -1086,11 +1080,11 @@ msgstr "No " msgid "User '%s' successfully logged in" msgstr "Usuario '%s' ha entrado con éxito" -#: auth/views.py:41 +#: auth/views.py:44 msgid "Your username and password didn't match. Please try again." msgstr "Su nombre de usuario y contraseña no concuerdan. Por favor, inténtelo de nuevo." -#: auth/views.py:64 +#: auth/views.py:67 #, python-format msgid "User '%s' logged out" msgstr "Usuario '%s' ha salido" @@ -1134,16 +1128,16 @@ msgstr "Panel de gestión sencilla para amavis" msgid "Quarantine" msgstr "Cuarentena" -#: extensions/amavis/__init__.py:127 +#: extensions/amavis/__init__.py:129 msgid "pending requests" msgstr "Peticiones pendientes" -#: extensions/amavis/__init__.py:144 +#: extensions/amavis/__init__.py:149 #, python-format msgid "%d pending requests" msgstr "%d peticiones pendientes" -#: extensions/amavis/__init__.py:156 +#: extensions/amavis/__init__.py:161 msgid "Content filter" msgstr "Filtro de contenido" @@ -1258,12 +1252,12 @@ msgid "Virus filter" msgstr "Filtro de virus" #: extensions/amavis/models.py:94 extensions/amavis/models.py:102 -#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:268 +#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:281 msgid "yes" msgstr "sí" #: extensions/amavis/models.py:95 extensions/amavis/models.py:103 -#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:272 +#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:285 msgid "no" msgstr "no" @@ -1303,13 +1297,13 @@ msgid "" " settings." msgstr "Modificar el asunto del spam usando el texto especificado. Elija 'por defecto' para usar la configuración global." -#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:36 +#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:37 msgid "From" msgstr "Desde" #: extensions/amavis/sql_listing.py:24 extensions/sievefilters/forms.py:68 -#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:35 -#: extensions/webmail/lib.py:404 templates/common/email_searchbar.html:6 +#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:36 +#: extensions/webmail/lib.py:409 templates/common/email_searchbar.html:6 msgid "Subject" msgstr "Tema" @@ -1319,37 +1313,37 @@ msgstr "Tema" msgid "To" msgstr "A" -#: extensions/amavis/views.py:34 +#: extensions/amavis/views.py:43 msgid "Empty quarantine" msgstr "Vaciar cuarentena" -#: extensions/amavis/views.py:202 +#: extensions/amavis/views.py:211 msgid "Message deleted" msgstr "Mensaje eliminado" -#: extensions/amavis/views.py:223 +#: extensions/amavis/views.py:232 #, python-format msgid "%(count)d message deleted successfully" msgid_plural "%(count)d messages deleted successfully" msgstr[0] "%(count)d mensaje eliminado correctamente" msgstr[1] "%(count)d mensajes eliminados correctamente" -#: extensions/amavis/views.py:243 +#: extensions/amavis/views.py:252 msgid "Request sent" msgstr "Petición enviada" -#: extensions/amavis/views.py:249 +#: extensions/amavis/views.py:258 msgid "Message released" msgstr "Mensaje publicado" -#: extensions/amavis/views.py:271 +#: extensions/amavis/views.py:280 #, python-format msgid "%(count)d request sent" msgid_plural "%(count)d requests sent" msgstr[0] "%(count)d petición enviada" msgstr[1] "%(count)d peticiones enviadas" -#: extensions/amavis/views.py:295 +#: extensions/amavis/views.py:304 #, python-format msgid "%(count)d message released successfully" msgid_plural "%(count)d messages released successfully" @@ -1470,7 +1464,7 @@ msgstr "Límites" msgid "Reseller" msgstr "Revendedor" -#: extensions/limits/__init__.py:96 +#: extensions/limits/__init__.py:94 msgid "Resources" msgstr "Recursos" @@ -1590,23 +1584,23 @@ msgstr "Activar su autorespuesta hasta esta fecha" msgid "This date is over" msgstr "Esta fecha ha pasado" -#: extensions/postfix_autoreply/models.py:15 +#: extensions/postfix_autoreply/models.py:19 msgid "subject" msgstr "asunto" -#: extensions/postfix_autoreply/models.py:16 +#: extensions/postfix_autoreply/models.py:20 msgid "The subject that will appear in sent emails" msgstr "El asunto que aparecerán en los correos enviados" -#: extensions/postfix_autoreply/models.py:17 +#: extensions/postfix_autoreply/models.py:23 msgid "content" msgstr "contenido" -#: extensions/postfix_autoreply/models.py:18 +#: extensions/postfix_autoreply/models.py:24 msgid "The content that will appear in sent emails" msgstr "El contenido que aparecerá en los emails enviados" -#: extensions/postfix_autoreply/models.py:20 +#: extensions/postfix_autoreply/models.py:28 msgid "Activate/Deactivate your auto reply" msgstr "Activar/desactivar su autorespuesta" @@ -1740,53 +1734,53 @@ msgstr "La conexión al servidor MANAGESIEVE falló, verifique su configuración msgid "Not enough space on server" msgstr "No hay suficiente espacio en el servidor" -#: extensions/sievefilters/views.py:64 +#: extensions/sievefilters/views.py:68 msgid "Failed to retrieve filters set" msgstr "Fallo en la recuperación de un conjunto de filtros" -#: extensions/sievefilters/views.py:117 +#: extensions/sievefilters/views.py:126 #: extensions/sievefilters/templatetags/sfilters_tags.py:33 msgid "New filter" msgstr "Nuevo filtro" -#: extensions/sievefilters/views.py:124 +#: extensions/sievefilters/views.py:133 msgid "Filter created" msgstr "Filtro creado" -#: extensions/sievefilters/views.py:136 +#: extensions/sievefilters/views.py:145 #: extensions/sievefilters/templatetags/sfilters_tags.py:77 msgid "Edit filter" msgstr "Editar filtro" -#: extensions/sievefilters/views.py:145 +#: extensions/sievefilters/views.py:154 msgid "Filter modified" msgstr "Filtro modificado" -#: extensions/sievefilters/views.py:165 +#: extensions/sievefilters/views.py:176 msgid "Filter removed" msgstr "Filtro eliminado" -#: extensions/sievefilters/views.py:166 +#: extensions/sievefilters/views.py:177 msgid "Failed to remove filter" msgstr "La eliminación del filtro falló" -#: extensions/sievefilters/views.py:180 +#: extensions/sievefilters/views.py:191 msgid "Filters set saved" msgstr "Filtros guardados" -#: extensions/sievefilters/views.py:185 +#: extensions/sievefilters/views.py:196 msgid "Create a new filters set" msgstr "Crear un nuevo conjunto de filtros" -#: extensions/sievefilters/views.py:208 +#: extensions/sievefilters/views.py:219 msgid "Filters set created" msgstr "Conjunto de filtros creado" -#: extensions/sievefilters/views.py:229 +#: extensions/sievefilters/views.py:240 msgid "Filters set deleted" msgstr "Conjunto de filtros eliminado" -#: extensions/sievefilters/views.py:241 +#: extensions/sievefilters/views.py:252 msgid "Filters set activated" msgstr "Conjunto de filtros activado" @@ -1802,15 +1796,15 @@ msgstr "Activo" msgid "No filter defined yet" msgstr "No hay un filtro definido aún" -#: extensions/sievefilters/templates/sievefilters/index.html:55 +#: extensions/sievefilters/templates/sievefilters/index.html:56 msgid "Filter sets" msgstr "Conjunto de filtros" -#: extensions/sievefilters/templates/sievefilters/index.html:59 +#: extensions/sievefilters/templates/sievefilters/index.html:60 msgid "active" msgstr "activo" -#: extensions/sievefilters/templates/sievefilters/index.html:73 +#: extensions/sievefilters/templates/sievefilters/index.html:74 msgid "" "No filters set defined yet. You must create one in order to define filters." msgstr "No hay un conjunto de filtros definido aún. Debe crear uno para poder definir filtros." @@ -2125,84 +2119,88 @@ msgstr "Respuesta del servidor" msgid "Cci" msgstr "Cco" -#: extensions/webmail/imaputils.py:259 +#: extensions/webmail/imaputils.py:237 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Fallo al recuperar el delimitador jerárquico" + +#: extensions/webmail/imaputils.py:278 #, python-format msgid "Connection to IMAP server failed: %s" msgstr "Falló la conexión con el servidor IMAP: %s" -#: extensions/webmail/lib.py:353 +#: extensions/webmail/lib.py:358 msgid "wrote:" msgstr "escribió:" -#: extensions/webmail/lib.py:418 extensions/webmail/lib.py:422 +#: extensions/webmail/lib.py:423 extensions/webmail/lib.py:427 msgid "Original message" msgstr "Mensaje original" -#: extensions/webmail/views.py:81 +#: extensions/webmail/views.py:86 #, python-format msgid "%(count)d message deleted" msgid_plural "%(count)d messages deleted" msgstr[0] "%(count)d mensaje eliminado" msgstr[1] "%(count)d mensajes eliminados" -#: extensions/webmail/views.py:98 +#: extensions/webmail/views.py:103 msgid "Unknown action" msgstr "Acción desconocida" -#: extensions/webmail/views.py:111 lib/email_listing.py:137 +#: extensions/webmail/views.py:116 lib/email_listing.py:137 msgid "Empty mailbox" msgstr "Mailbox vacío" -#: extensions/webmail/views.py:128 -#: extensions/webmail/templatetags/webextras.py:169 +#: extensions/webmail/views.py:133 +#: extensions/webmail/templatetags/webextras.py:170 msgid "Create a new mailbox" msgstr "Crear un nuevo mailbox" -#: extensions/webmail/views.py:143 +#: extensions/webmail/views.py:149 msgid "Mailbox created" msgstr "Mailbox creado." -#: extensions/webmail/views.py:159 +#: extensions/webmail/views.py:165 msgid "Edit mailbox" msgstr "Editar el mailbox" -#: extensions/webmail/views.py:173 +#: extensions/webmail/views.py:180 msgid "Mailbox updated" msgstr "Mailbox actualizado" -#: extensions/webmail/views.py:207 extensions/webmail/views.py:388 -#: extensions/webmail/views.py:497 +#: extensions/webmail/views.py:214 extensions/webmail/views.py:396 +#: extensions/webmail/views.py:505 msgid "Bad request" msgstr "Petición incorrecta" -#: extensions/webmail/views.py:238 +#: extensions/webmail/views.py:246 msgid "Failed to save attachment: " msgstr "Fallo al guardar el adjunto:" -#: extensions/webmail/views.py:241 +#: extensions/webmail/views.py:249 #, python-format msgid "Attachment is too big (limit: %s)" msgstr "EL adjunto es demasiado grande (límite: %s)" -#: extensions/webmail/views.py:247 +#: extensions/webmail/views.py:255 #: extensions/webmail/templates/webmail/compose.html:15 #: extensions/webmail/templates/webmail/headers.html:4 msgid "Attachments" msgstr "Adjuntos" -#: extensions/webmail/views.py:263 +#: extensions/webmail/views.py:271 msgid "Bad query" msgstr "Consulta errónea" -#: extensions/webmail/views.py:273 +#: extensions/webmail/views.py:281 msgid "Failed to remove attachment: " msgstr "Fallo al eliminar el adjunto:" -#: extensions/webmail/views.py:278 +#: extensions/webmail/views.py:286 msgid "Unknown attachment" msgstr "Adjunto desconocido" -#: extensions/webmail/views.py:493 +#: extensions/webmail/views.py:501 msgid "Undefined action" msgstr "Acción no definida" @@ -2233,7 +2231,7 @@ msgstr[1] "%(counter)s ficheros" msgid "Parent mailbox" msgstr "Mailbox padre" -#: extensions/webmail/templates/webmail/index.html:36 +#: extensions/webmail/templates/webmail/index.html:38 msgid "Mailbox quota" msgstr "Cuota del mailbox" @@ -2295,15 +2293,15 @@ msgstr "Como no leído" msgid "Compress folder" msgstr "Comprimir la carpeta" -#: extensions/webmail/templatetags/webextras.py:109 +#: extensions/webmail/templatetags/webextras.py:110 msgid "Empty folder" msgstr "Vaciar la carpeta" -#: extensions/webmail/templatetags/webextras.py:177 +#: extensions/webmail/templatetags/webextras.py:178 msgid "Edit the selected mailbox" msgstr "Editar el mailbox seleccionado" -#: extensions/webmail/templatetags/webextras.py:182 +#: extensions/webmail/templatetags/webextras.py:183 msgid "Remove the selected mailbox" msgstr "Eliminar el mailbox seleccionado" @@ -2357,7 +2355,7 @@ msgstr "La página pedida no existe." msgid "An internal error occured." msgstr "Ocurrió un error interno." -#: templates/nlayout.html:56 +#: templates/nlayout.html:62 msgid "Powered by Modoboa" msgstr "Tecnología Modoboa" @@ -2391,11 +2389,11 @@ msgstr "Cerrar" msgid "Welcome to Modoboa" msgstr "Bienvenido a Modoboa" -#: templates/registration/login.html:33 +#: templates/registration/login.html:34 msgid "Remember me" msgstr "Recuerdame" -#: templates/registration/login.html:35 +#: templates/registration/login.html:36 msgid "Log in" msgstr "Entrar" @@ -2411,7 +2409,7 @@ msgstr "Configuración por usuario" msgid "Prefered language" msgstr "Idioma preferido" -#: userprefs/app_settings.py:26 +#: userprefs/app_settings.py:27 msgid "Prefered language to display pages" msgstr "Idioma preferido para mostrar las páginas" diff --git a/modoboa/locale/fr/LC_MESSAGES/django.po b/modoboa/locale/fr/LC_MESSAGES/django.po index 40f10043d..bed98c974 100644 --- a/modoboa/locale/fr/LC_MESSAGES/django.po +++ b/modoboa/locale/fr/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: modoboa-1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-06-01 16:35+0200\n" +"POT-Creation-Date: 2013-09-20 22:30+0200\n" "PO-Revision-Date: 2011-01-07 10:00+100\n" "Last-Translator: Antoine Nguyen \n" "Language-Team: FR \n" @@ -25,8 +25,8 @@ msgstr "Console d'administration" msgid "Web based console to manage domains, accounts and aliases" msgstr "Console web pour gérer les domains, comptes et alias" -#: admin/__init__.py:16 admin/forms.py:179 admin/forms.py:614 -#: admin/views.py:93 admin/views.py:441 +#: admin/__init__.py:16 admin/forms.py:211 admin/forms.py:652 +#: admin/views.py:94 admin/views.py:447 #: extensions/postfix_autoreply/app_settings.py:11 #: extensions/sievefilters/app_settings.py:50 #: extensions/stats/app_settings.py:11 extensions/webmail/app_settings.py:11 @@ -250,15 +250,23 @@ msgid "The default redirection used when no application is specified" msgstr "" "La redirection appliquée par défaut quand aucune application n'est spécifiée" -#: admin/app_settings.py:230 +#: admin/app_settings.py:213 +msgid "Key must be either 16, 24, or 32 bytes long" +msgstr "La longueur d'une clé doit être de 16, 24 ou 32 octets" + +#: admin/app_settings.py:222 +msgid "Invalid syntax" +msgstr "Syntaxe invalide" + +#: admin/app_settings.py:245 msgid "This field is required" msgstr "Ce champ est requis" -#: admin/forms.py:16 admin/forms.py:459 admin/tables.py:18 +#: admin/forms.py:20 admin/forms.py:497 admin/tables.py:18 msgid "Alias(es)" msgstr "Alias" -#: admin/forms.py:18 +#: admin/forms.py:22 msgid "" "Alias(es) of this domain. Indicate only one name per input, press ENTER to " "add a new input." @@ -266,23 +274,23 @@ msgstr "" "Alias de ce domaine. Indiquer seulement un nom par champ, appuyer sur ENTREE " "pour ajouter un nouveau champ." -#: admin/forms.py:55 +#: admin/forms.py:59 msgid "An alias with this name already exists" msgstr "Un alias de ce nom existe déjà" -#: admin/forms.py:69 +#: admin/forms.py:73 msgid "A domain with this name already exists" msgstr "Un domaine de ce nom existe déjà" -#: admin/forms.py:114 +#: admin/forms.py:146 msgid "Create a domain administrator" msgstr "Créer un administrateur de domaine" -#: admin/forms.py:116 +#: admin/forms.py:148 msgid "Automatically create an administrator for this domain" msgstr "Créer automatiquement un administrateur pour ce domaine" -#: admin/forms.py:120 admin/tables.py:12 admin/tables.py:47 +#: admin/forms.py:152 admin/tables.py:12 admin/tables.py:47 #: admin/templates/admin/editdomainform.html:13 #: admin/templates/admin/identities_table.html:5 #: admin/templates/admin/information.html:8 @@ -291,7 +299,7 @@ msgstr "Créer automatiquement un administrateur pour ce domaine" msgid "Name" msgstr "Nom" -#: admin/forms.py:122 +#: admin/forms.py:154 msgid "" "The administrator's name. Don't include the domain's name here, it will be " "automatically appended." @@ -299,28 +307,28 @@ msgstr "" "Le nom de l'administrateur. Ne pas include un nom de domaine ici, il sera " "ajouté automatiquement." -#: admin/forms.py:128 +#: admin/forms.py:160 msgid "Create aliases" msgstr "Créer des alias" -#: admin/forms.py:130 +#: admin/forms.py:162 msgid "Automatically create standard aliases for this domain" msgstr "Créer automatiquement des alias standard pour ce domaine" -#: admin/forms.py:143 +#: admin/forms.py:175 msgid "Invalid format" msgstr "Format invalide" -#: admin/forms.py:155 +#: admin/forms.py:187 #, python-format msgid "User '%s' already exists" msgstr "L'utilisateur '%s' existe déjà" -#: admin/forms.py:203 +#: admin/forms.py:235 msgid "Email address" msgstr "Adresse email" -#: admin/forms.py:204 +#: admin/forms.py:236 msgid "" "The distribution list address. Use the '*' character to create a 'catchall' " "address (ex: *@domain.tld)." @@ -328,11 +336,11 @@ msgstr "" "L'adresse de la liste de distribution. Utiliser le caractère '*' pour créer " "une adresse 'attrape tout' (ex: *@domain.tld)." -#: admin/forms.py:207 +#: admin/forms.py:239 msgid "Recipients" msgstr "Destinataires" -#: admin/forms.py:208 +#: admin/forms.py:240 msgid "" "Mailbox(es) this alias will point to. Indicate only one address per input, " "press ENTER to add a new input." @@ -340,113 +348,112 @@ msgstr "" "Destinataire(s) de cet alias. Indiquer seulement une adresse par champ, " "appuyer sur ENTREE pour ajouter un nouveau champ." -#: admin/forms.py:246 admin/forms.py:504 admin/forms.py:530 -#: admin/models.py:1147 +#: admin/forms.py:278 admin/forms.py:542 admin/forms.py:568 msgid "Domain does not exist" msgstr "Domain inexistant" -#: admin/forms.py:248 +#: admin/forms.py:280 msgid "You don't have access to this domain" msgstr "Vous n'avez pas accès à ce domaine" -#: admin/forms.py:268 +#: admin/forms.py:300 msgid "Invalid mailbox" msgstr "Boite aux lettres invalide" -#: admin/forms.py:284 +#: admin/forms.py:316 admin/models.py:1202 #, python-format msgid "Local recipient %s not found" msgstr "Destinataire local %s introuvable" -#: admin/forms.py:286 admin/forms.py:292 +#: admin/forms.py:318 admin/forms.py:324 #, python-format msgid "Recipient %s already present" msgstr "Destinataire %s déjà présent" -#: admin/forms.py:297 +#: admin/forms.py:329 msgid "No recipient defined" msgstr "Aucune destinataire défini" -#: admin/forms.py:311 extensions/webmail/forms.py:19 +#: admin/forms.py:343 extensions/webmail/forms.py:19 msgid "Select a file" msgstr "Sélectionner un fichier" -#: admin/forms.py:313 admin/forms.py:341 +#: admin/forms.py:345 admin/forms.py:373 msgid "Separator" msgstr "Séparateur" -#: admin/forms.py:316 +#: admin/forms.py:348 msgid "Continue on error" msgstr "Continuer en cas d'erreur" -#: admin/forms.py:317 +#: admin/forms.py:349 msgid "Don't treat duplicated objects as error" msgstr "Ne pas traiter les objets dupliqués comme des erreurs" -#: admin/forms.py:332 +#: admin/forms.py:364 msgid "Crypt passwords" msgstr "Chiffrer les mots de passe" -#: admin/forms.py:333 +#: admin/forms.py:365 msgid "Check this option if passwords contained in your file are not crypted" msgstr "" "Cocher cette option si les mots de passe contenus dans votre fichier ne sont " "pas chiffrés" -#: admin/forms.py:339 +#: admin/forms.py:371 msgid "File name" msgstr "Nom du fichier" -#: admin/forms.py:371 admin/templates/admin/editdomainform.html:13 +#: admin/forms.py:403 admin/templates/admin/editdomainform.html:13 #: auth/forms.py:7 msgid "Username" msgstr "Nom d'utilisateur" -#: admin/forms.py:373 +#: admin/forms.py:405 msgid "Role" msgstr "Rôle" -#: admin/forms.py:374 admin/forms.py:401 +#: admin/forms.py:406 admin/forms.py:433 msgid "Choose" msgstr "Choisir" -#: admin/forms.py:375 +#: admin/forms.py:407 msgid "What level of permission this user will have" msgstr "Quel niveau de permission cet utilisateur aura" -#: admin/forms.py:377 auth/forms.py:11 +#: admin/forms.py:409 auth/forms.py:11 msgid "Password" msgstr "Mot de passe" -#: admin/forms.py:379 userprefs/forms.py:18 +#: admin/forms.py:411 userprefs/forms.py:18 msgid "Confirmation" msgstr "Confirmation" -#: admin/forms.py:381 +#: admin/forms.py:413 msgid "Enter the same password as above, for verification." msgstr "Entrer le même mot de passe, pour vérification." -#: admin/forms.py:392 +#: admin/forms.py:424 msgid "Enabled" msgstr "Activé" -#: admin/forms.py:434 +#: admin/forms.py:471 msgid "The two password fields didn't match." msgstr "Les deux mots de passe ne correspondent pas." -#: admin/forms.py:440 +#: admin/forms.py:477 msgid "You can't disable your own account" msgstr "Vous ne pouvez pas désactiver votre propre compte" -#: admin/forms.py:450 +#: admin/forms.py:488 msgid "E-mail" msgstr "E-mail" -#: admin/forms.py:452 +#: admin/forms.py:490 msgid "Quota" msgstr "Quota" -#: admin/forms.py:454 +#: admin/forms.py:492 msgid "" "Quota in MB for this mailbox. Define a custom value or use domain's default " "one. Leave empty to define an unlimited value (not allowed for domain " @@ -456,7 +463,7 @@ msgstr "" "valeur par défaut du domaine. Laisser vide pour définir une valeur illimitée " "(interdit pour les admins. de domaine)." -#: admin/forms.py:461 +#: admin/forms.py:499 msgid "" "Alias(es) of this mailbox. Indicate only one address per input, press ENTER " "to add a new input. Use the '*' character to create a 'catchall' alias (ex: " @@ -466,133 +473,133 @@ msgstr "" "ENTREE pour ajouter un nouveau champ. Utiliser le caractère '*' pour créer " "une adresse 'attrape tout' (ex: *@domain.tld)." -#: admin/forms.py:512 +#: admin/forms.py:550 #, python-format msgid "Mailbox %s already exists" msgstr "La boite %s existe déjà" -#: admin/forms.py:572 +#: admin/forms.py:610 msgid "Domain(s)" msgstr "Domaine(s)" -#: admin/forms.py:574 +#: admin/forms.py:612 msgid "Domain(s) that user administrates" msgstr "Domaine(s) que cet utilisateur administre" -#: admin/forms.py:616 admin/views.py:444 +#: admin/forms.py:654 admin/views.py:450 msgid "Mail" msgstr "Courrier" -#: admin/forms.py:618 +#: admin/forms.py:656 msgid "Permissions" msgstr "Permissions" -#: admin/models.py:78 +#: admin/models.py:80 msgid "You can't delete your own account" msgstr "Vous ne pouvez pas supprimer votre propre compte" -#: admin/models.py:159 +#: admin/models.py:161 msgid "Failed to update password: LDAP module not installed" msgstr "" "Echec de la mise à jour du mot de passe : le module LDAP n'est pas installé" -#: admin/models.py:165 +#: admin/models.py:167 #, python-format msgid "Failed to update password: %s" msgstr "Echec de la mise à jour du mot de passe : %s" -#: admin/models.py:192 +#: admin/models.py:194 msgid "account" msgstr "compte" -#: admin/models.py:494 admin/models.py:720 admin/models.py:782 +#: admin/models.py:496 admin/models.py:733 admin/models.py:795 msgid "Invalid line" msgstr "Ligne invalide" -#: admin/models.py:498 +#: admin/models.py:500 msgid "You can't import an account with a role greater than yours" msgstr "Vous ne pouvez pas importer un compte avec un rôle supérieur au votre" -#: admin/models.py:518 +#: admin/models.py:506 +#, python-format +msgid "The simple user '%s' must have a valid email address" +msgstr "L'utilisateur simple '%s' doit avoir une adresse email valide" + +#: admin/models.py:510 +#, python-format +msgid "username and email fields must not differ for '%s'" +msgstr "Les champs username et email de '%s' ne doivent pas différer" + +#: admin/models.py:534 #, python-format msgid "Account import failed (%s): domain does not exist" msgstr "L'import du compte a échoué (%s): le domaine n'existe pas" -#: admin/models.py:607 admin/models.py:733 +#: admin/models.py:624 admin/models.py:746 msgid "name" msgstr "Nom" -#: admin/models.py:608 +#: admin/models.py:625 msgid "The domain name" msgstr "Le nom du domaine" -#: admin/models.py:610 +#: admin/models.py:627 msgid "Default quota in MB applied to mailboxes" msgstr "Quota par défaut en Mo appliqué aux boites aux lettres" -#: admin/models.py:613 admin/models.py:740 admin/models.py:1020 -#: admin/models.py:1181 extensions/postfix_autoreply/models.py:19 +#: admin/models.py:630 admin/models.py:753 admin/models.py:1045 +#: admin/models.py:1217 extensions/postfix_autoreply/models.py:27 msgid "enabled" msgstr "Activé" -#: admin/models.py:614 +#: admin/models.py:631 msgid "Check to activate this domain" msgstr "Cocher pour activer ce domaine" -#: admin/models.py:734 +#: admin/models.py:747 msgid "The alias name" msgstr "Le nom de l'alias" -#: admin/models.py:736 +#: admin/models.py:749 msgid "target" msgstr "cible" -#: admin/models.py:737 +#: admin/models.py:750 msgid "The domain this alias points to" msgstr "Le domaine vers lequel cet alias pointe" -#: admin/models.py:741 admin/models.py:1021 +#: admin/models.py:754 admin/models.py:1046 msgid "Check to activate this alias" msgstr "Cocher pour activer cet alias" -#: admin/models.py:788 +#: admin/models.py:801 #, python-format msgid "Unknown domain %s" msgstr "Domain %s inconnu" -#: admin/models.py:805 admin/models.py:1006 +#: admin/models.py:818 admin/models.py:1031 msgid "address" msgstr "Adresse" -#: admin/models.py:806 +#: admin/models.py:819 msgid "Mailbox address (without the @domain.tld part)" msgstr "Adresse de la boite aux lettres (sans la partie @domain.tld)" -#: admin/models.py:852 +#: admin/models.py:877 #, python-format msgid "Failed to retrieve mailbox location (%s)" msgstr "Impossible de récuperer l'emplacement de la boite (%s)" -#: admin/models.py:868 -#, python-format -msgid "Failed to rename mailbox: %s" -msgstr "Impossible de renommer la boite : %s" - -#: admin/models.py:894 -#, python-format -msgid "Failed to remove mailbox: %s" -msgstr "Impossible de supprimer la boite : %s" - -#: admin/models.py:909 +#: admin/models.py:921 #, python-format msgid "Quota is greater than the allowed domain's limit (%dM)" msgstr "Le quota est supérieur à la limité autorisée pour le domaine (%dM)" -#: admin/models.py:914 +#: admin/models.py:926 msgid "A quota is required" msgstr "Un quota est requis" -#: admin/models.py:1007 +#: admin/models.py:1032 msgid "" "The alias address (without the domain part). For a 'catch-all' address, just " "enter an * character." @@ -600,57 +607,57 @@ msgstr "" "L'adresse de l'alias (sans la partie domaine). Pour une adresse 'attrape-" "tout', entrer juste un caractère *." -#: admin/models.py:1011 +#: admin/models.py:1036 msgid "mailboxes" msgstr "Boites aux lettres" -#: admin/models.py:1012 +#: admin/models.py:1037 msgid "The mailboxes this alias points to" msgstr "Les boites aux lettres sur lesquelles pointe cet alias" -#: admin/models.py:1016 +#: admin/models.py:1041 msgid "The aliases this alias points to" msgstr "Les alias vers lesquels pointe cet alias" -#: admin/models.py:1064 +#: admin/models.py:1089 msgid "distribution list" msgstr "liste de distribution" -#: admin/models.py:1065 +#: admin/models.py:1090 msgid "forward" msgstr "transfert" -#: admin/models.py:1066 +#: admin/models.py:1091 msgid "alias" msgstr "alias" -#: admin/models.py:1142 +#: admin/models.py:1170 #, python-format msgid "Invalid line: %s" msgstr "Ligne invalide : %s" -#: admin/models.py:1167 +#: admin/models.py:1175 #, python-format -msgid "Mailbox %s does not exist" -msgstr "La boite %s n'existe pas" +msgid "Domain '%s' does not exist" +msgstr "Domain '%s' inexistant" -#: admin/models.py:1182 +#: admin/models.py:1218 msgid "Check to enable this extension" msgstr "Cocher pour activer cette extension" -#: admin/models.py:1282 +#: admin/models.py:1318 msgid "added" msgstr "ajouté" -#: admin/models.py:1285 +#: admin/models.py:1321 msgid "modified" msgstr "modifié" -#: admin/models.py:1288 +#: admin/models.py:1324 msgid "deleted" msgstr "Supprimé" -#: admin/models.py:1290 +#: admin/models.py:1326 #, python-format msgid "%(object)s '%(name)s' %(action)s by user %(user)s" msgstr "%(object)s '%(name)s' %(action)s par l'utilisateur %(user)s" @@ -660,7 +667,7 @@ msgid "Edit domain" msgstr "Editer le domaine" #: admin/tables.py:21 admin/templates/admin/identities_table.html:8 -#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:79 +#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:83 #: extensions/sievefilters/templates/sievefilters/filter.html:17 #: extensions/sievefilters/templates/sievefilters/guieditor.html:5 #: extensions/webmail/templatetags/webextras.py:97 @@ -675,143 +682,143 @@ msgstr "Version" msgid "Description" msgstr "Description" -#: admin/views.py:87 +#: admin/views.py:88 msgid "New domain" msgstr "Nouveau domaine" -#: admin/views.py:88 admin/views.py:97 admin/views.py:225 admin/views.py:438 -#: admin/views.py:445 extensions/sievefilters/views.py:120 -#: extensions/sievefilters/views.py:187 extensions/webmail/views.py:131 +#: admin/views.py:89 admin/views.py:98 admin/views.py:228 admin/views.py:444 +#: admin/views.py:451 extensions/sievefilters/views.py:129 +#: extensions/sievefilters/views.py:198 extensions/webmail/views.py:136 msgid "Create" msgstr "Créer" -#: admin/views.py:94 admin/views.py:442 +#: admin/views.py:95 admin/views.py:448 msgid "Next" msgstr "Suivante" -#: admin/views.py:96 +#: admin/views.py:97 msgid "Options" msgstr "Options" -#: admin/views.py:98 admin/views.py:446 +#: admin/views.py:99 admin/views.py:452 msgid "Previous" msgstr "Précédente" -#: admin/views.py:111 +#: admin/views.py:112 msgid "Domain created" msgstr "Domaine créé" -#: admin/views.py:144 admin/views.py:282 admin/views.py:492 +#: admin/views.py:146 admin/views.py:285 admin/views.py:498 #: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:9 -#: extensions/sievefilters/views.py:139 extensions/webmail/views.py:162 +#: extensions/sievefilters/views.py:148 extensions/webmail/views.py:168 #: userprefs/templates/userprefs/forward.html:8 #: userprefs/templates/userprefs/profile.html:8 msgid "Update" msgstr "Mettre à jour" -#: admin/views.py:160 +#: admin/views.py:162 msgid "Domain modified" msgstr "Domaine modifié" -#: admin/views.py:186 +#: admin/views.py:189 msgid "You can't delete your own domain" msgstr "Vous ne pouvez pas supprimer votre propre domaine" -#: admin/views.py:189 +#: admin/views.py:192 msgid "Domain deleted" msgid_plural "Domains deleted" msgstr[0] "Domaine supprimé" msgstr[1] "Domaines supprimés" -#: admin/views.py:204 +#: admin/views.py:207 msgid "Alias with this name already exists" msgstr "Un alias de ce nom existe déjà" -#: admin/views.py:247 +#: admin/views.py:250 msgid "New distribution list" msgstr "Nouvelle liste de distribution" -#: admin/views.py:248 +#: admin/views.py:251 msgid "Distribution list created" msgstr "Liste de distribution créée" -#: admin/views.py:257 +#: admin/views.py:260 msgid "New alias" msgstr "Nouvel alias" -#: admin/views.py:258 +#: admin/views.py:261 msgid "Alias created" msgstr "Alias créé" -#: admin/views.py:267 +#: admin/views.py:270 msgid "New forward" msgstr "Nouveau transfert" -#: admin/views.py:268 +#: admin/views.py:271 msgid "Forward created" msgstr "Transfert créé" -#: admin/views.py:286 +#: admin/views.py:289 msgid "Distribution list modified" msgstr "Liste de distribution modifiée" -#: admin/views.py:288 +#: admin/views.py:291 msgid "Forward modified" msgstr "Transfert modifié" -#: admin/views.py:290 +#: admin/views.py:293 msgid "Alias modified" msgstr "Alias modifié" -#: admin/views.py:403 admin/views.py:541 admin/views.py:546 -#: extensions/amavis/views.py:195 extensions/amavis/views.py:201 -#: extensions/amavis/views.py:234 extensions/amavis/views.py:238 -#: extensions/amavis/views.py:240 extensions/webmail/views.py:36 -#: extensions/webmail/views.py:64 extensions/webmail/views.py:77 -#: extensions/webmail/views.py:93 extensions/webmail/views.py:109 -#: extensions/webmail/views.py:193 extensions/webmail/views.py:419 -#: extensions/webmail/views.py:430 extensions/webmail/views.py:457 +#: admin/views.py:402 admin/views.py:545 admin/views.py:550 +#: extensions/amavis/views.py:204 extensions/amavis/views.py:210 +#: extensions/amavis/views.py:243 extensions/amavis/views.py:247 +#: extensions/amavis/views.py:249 extensions/webmail/views.py:41 +#: extensions/webmail/views.py:68 extensions/webmail/views.py:81 +#: extensions/webmail/views.py:98 extensions/webmail/views.py:114 +#: extensions/webmail/views.py:200 extensions/webmail/views.py:427 +#: extensions/webmail/views.py:438 extensions/webmail/views.py:465 #: lib/formutils.py:32 lib/formutils.py:37 msgid "Invalid request" msgstr "Requête invalide" -#: admin/views.py:435 +#: admin/views.py:441 msgid "New account" msgstr "Nouveau compte" -#: admin/views.py:459 +#: admin/views.py:465 msgid "Account created" msgstr "Compte créé" -#: admin/views.py:507 +#: admin/views.py:513 msgid "Account updated" msgstr "Compte mis à jour" -#: admin/views.py:531 +#: admin/views.py:535 msgid "Account deleted" msgid_plural "Accounts deleted" msgstr[0] "Compte supprimé" msgstr[1] "Comptes supprimés" -#: admin/views.py:586 +#: admin/views.py:590 msgid "Parameters saved" msgstr "Paramètres enregistrés" -#: admin/views.py:632 +#: admin/views.py:636 msgid "Modifications applied." msgstr "Modifications appliquées." -#: admin/views.py:740 +#: admin/views.py:743 #, python-format msgid "Object already exists: %s" msgstr "L'objet existe déjà : %s" -#: admin/views.py:742 +#: admin/views.py:745 #, python-format msgid "%d objects imported successfully" msgstr "%d objets importés avec succès" -#: admin/views.py:760 +#: admin/views.py:763 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
              \n" @@ -839,25 +846,25 @@ msgstr "" "\n" "

              Vous pouvez utiliser un caractère différent comme séparateur.

              \n" -#: admin/views.py:772 +#: admin/views.py:775 msgid "Import identities" msgstr "Importer des identités" -#: admin/views.py:773 admin/views.py:843 admin/templatetags/admin_extras.py:87 +#: admin/views.py:776 admin/views.py:846 admin/templatetags/admin_extras.py:87 #: admin/templatetags/admin_extras.py:144 msgid "Import" msgstr "Importer" -#: admin/views.py:803 +#: admin/views.py:806 msgid "Export identities" msgstr "Export d'identités" -#: admin/views.py:804 admin/views.py:860 admin/templatetags/admin_extras.py:93 +#: admin/views.py:807 admin/views.py:863 admin/templatetags/admin_extras.py:93 #: admin/templatetags/admin_extras.py:150 msgid "Export" msgstr "Exporter" -#: admin/views.py:832 +#: admin/views.py:835 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
                \n" @@ -877,11 +884,11 @@ msgstr "" "l'une des valeurs précédentes.

                \n" "

                Vous pouvez utiliser un caractère différent comme séparateur.

                \n" -#: admin/views.py:842 +#: admin/views.py:845 msgid "Import domains" msgstr "Importer des domaines" -#: admin/views.py:859 +#: admin/views.py:862 msgid "Export domains" msgstr "Export de domaines" @@ -954,7 +961,7 @@ msgid "Tags" msgstr "Tags" #: admin/templates/admin/importform.html:5 -#: admin/templatetags/admin_extras.py:305 +#: admin/templatetags/admin_extras.py:290 msgid "Help" msgstr "Aide" @@ -1001,7 +1008,7 @@ msgid "Previously on Modoboa..." msgstr "Précédemment dans Modoboa..." #: admin/templates/admin/logs.html:6 extensions/amavis/sql_listing.py:25 -#: extensions/webmail/lib.py:37 extensions/webmail/lib.py:406 +#: extensions/webmail/lib.py:38 extensions/webmail/lib.py:411 msgid "Date" msgstr "Date" @@ -1104,34 +1111,21 @@ msgid "View the domain's identities" msgstr "Voir les identités du domaine" #: admin/templatetags/admin_extras.py:178 -msgid "Delete the domain" -msgstr "Supprimer ce domaine" - #: admin/templatetags/admin_extras.py:194 -msgid "Delete this account" -msgstr "Supprimer ce compte" +#: admin/templatetags/admin_extras.py:201 +#, python-format +msgid "Delete %s?" +msgstr "Supprimer %s ?" -#: admin/templatetags/admin_extras.py:202 -msgid "Delete this distribution list" -msgstr "Supprimer cette liste de distribution" - -#: admin/templatetags/admin_extras.py:209 -msgid "Delete this forward" -msgstr "Supprimer ce transfert" - -#: admin/templatetags/admin_extras.py:216 -msgid "Delete this alias" -msgstr "Supprimer cet alias" - -#: admin/templatetags/admin_extras.py:238 +#: admin/templatetags/admin_extras.py:223 msgid "Remove this permission" msgstr "Supprimer cette permission" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 msgid "Yes" msgstr "Oui" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 msgid "No" msgstr "Non" @@ -1140,13 +1134,13 @@ msgstr "Non" msgid "User '%s' successfully logged in" msgstr "Utiliseur '%s' connecté avec succès" -#: auth/views.py:41 +#: auth/views.py:44 msgid "Your username and password didn't match. Please try again." msgstr "" "Votre nom d'utilisateur et mot de passe ne correspondent pas. Veuillez " "réessayer." -#: auth/views.py:64 +#: auth/views.py:67 #, python-format msgid "User '%s' logged out" msgstr "Utilisateur '%s' déconnecté" @@ -1206,16 +1200,16 @@ msgstr "Interface simplifiée pour la gestion d'amavis" msgid "Quarantine" msgstr "Quarantaine" -#: extensions/amavis/__init__.py:127 +#: extensions/amavis/__init__.py:129 msgid "pending requests" msgstr "requêtes en attente" -#: extensions/amavis/__init__.py:144 +#: extensions/amavis/__init__.py:149 #, python-format msgid "%d pending requests" msgstr "%d requêtes en attente" -#: extensions/amavis/__init__.py:156 +#: extensions/amavis/__init__.py:161 msgid "Content filter" msgstr "Filtrage de contenu" @@ -1332,12 +1326,12 @@ msgid "Virus filter" msgstr "Filtrage des virus" #: extensions/amavis/models.py:94 extensions/amavis/models.py:102 -#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:268 +#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:281 msgid "yes" msgstr "oui" #: extensions/amavis/models.py:95 extensions/amavis/models.py:103 -#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:272 +#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:285 msgid "no" msgstr "non" @@ -1385,13 +1379,13 @@ msgstr "" "Modifier le sujet du spam en utilisant le texte spécifié. Choisir 'défaut' " "pour utiliser les paramètres globaux." -#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:36 +#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:37 msgid "From" msgstr "De" #: extensions/amavis/sql_listing.py:24 extensions/sievefilters/forms.py:68 -#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:35 -#: extensions/webmail/lib.py:404 templates/common/email_searchbar.html:6 +#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:36 +#: extensions/webmail/lib.py:409 templates/common/email_searchbar.html:6 msgid "Subject" msgstr "Sujet" @@ -1401,37 +1395,37 @@ msgstr "Sujet" msgid "To" msgstr "A" -#: extensions/amavis/views.py:34 +#: extensions/amavis/views.py:43 msgid "Empty quarantine" msgstr "Quarantaine vide" -#: extensions/amavis/views.py:202 +#: extensions/amavis/views.py:211 msgid "Message deleted" msgstr "Message supprimé" -#: extensions/amavis/views.py:223 +#: extensions/amavis/views.py:232 #, python-format msgid "%(count)d message deleted successfully" msgid_plural "%(count)d messages deleted successfully" msgstr[0] "%(count)d message supprimé correctement" msgstr[1] "%(count)d messages supprimés correctement" -#: extensions/amavis/views.py:243 +#: extensions/amavis/views.py:252 msgid "Request sent" msgstr "Requête envoyée" -#: extensions/amavis/views.py:249 +#: extensions/amavis/views.py:258 msgid "Message released" msgstr "Message relâché" -#: extensions/amavis/views.py:271 +#: extensions/amavis/views.py:280 #, python-format msgid "%(count)d request sent" msgid_plural "%(count)d requests sent" msgstr[0] "%(count)d requête envoyée" msgstr[1] "%(count)d requêtes envoyées" -#: extensions/amavis/views.py:295 +#: extensions/amavis/views.py:304 #, python-format msgid "%(count)d message released successfully" msgid_plural "%(count)d messages released successfully" @@ -1558,7 +1552,7 @@ msgstr "Limites" msgid "Reseller" msgstr "Revendeur" -#: extensions/limits/__init__.py:96 +#: extensions/limits/__init__.py:94 msgid "Resources" msgstr "Ressources" @@ -1679,23 +1673,23 @@ msgstr "Activer votre réponse automatique jusqu'à cette date" msgid "This date is over" msgstr "Cette date est dépassée" -#: extensions/postfix_autoreply/models.py:15 +#: extensions/postfix_autoreply/models.py:19 msgid "subject" msgstr "sujet" -#: extensions/postfix_autoreply/models.py:16 +#: extensions/postfix_autoreply/models.py:20 msgid "The subject that will appear in sent emails" msgstr "Le sujet qui apparaitra dans les emails envoyés" -#: extensions/postfix_autoreply/models.py:17 +#: extensions/postfix_autoreply/models.py:23 msgid "content" msgstr "contenu" -#: extensions/postfix_autoreply/models.py:18 +#: extensions/postfix_autoreply/models.py:24 msgid "The content that will appear in sent emails" msgstr "Le contenu qui apparaitra dans les emails envoyés" -#: extensions/postfix_autoreply/models.py:20 +#: extensions/postfix_autoreply/models.py:28 msgid "Activate/Deactivate your auto reply" msgstr "Activer/Désactiver votre réponse automatique" @@ -1830,53 +1824,53 @@ msgstr "" msgid "Not enough space on server" msgstr "Pas assez d'espace sur le serveur" -#: extensions/sievefilters/views.py:64 +#: extensions/sievefilters/views.py:68 msgid "Failed to retrieve filters set" msgstr "Impossible de récuperer le jeu de filtres" -#: extensions/sievefilters/views.py:117 +#: extensions/sievefilters/views.py:126 #: extensions/sievefilters/templatetags/sfilters_tags.py:33 msgid "New filter" msgstr "Nouveau filtre" -#: extensions/sievefilters/views.py:124 +#: extensions/sievefilters/views.py:133 msgid "Filter created" msgstr "Filtre créé" -#: extensions/sievefilters/views.py:136 +#: extensions/sievefilters/views.py:145 #: extensions/sievefilters/templatetags/sfilters_tags.py:77 msgid "Edit filter" msgstr "Editer le filtre" -#: extensions/sievefilters/views.py:145 +#: extensions/sievefilters/views.py:154 msgid "Filter modified" msgstr "Filtre modifié" -#: extensions/sievefilters/views.py:165 +#: extensions/sievefilters/views.py:176 msgid "Filter removed" msgstr "Filtre supprimé" -#: extensions/sievefilters/views.py:166 +#: extensions/sievefilters/views.py:177 msgid "Failed to remove filter" msgstr "Impossible de supprimer le filtre" -#: extensions/sievefilters/views.py:180 +#: extensions/sievefilters/views.py:191 msgid "Filters set saved" msgstr "Jeu de filtres sauvegardé" -#: extensions/sievefilters/views.py:185 +#: extensions/sievefilters/views.py:196 msgid "Create a new filters set" msgstr "Créer un nouveau jeu de filtres" -#: extensions/sievefilters/views.py:208 +#: extensions/sievefilters/views.py:219 msgid "Filters set created" msgstr "Jeu de filtres créé" -#: extensions/sievefilters/views.py:229 +#: extensions/sievefilters/views.py:240 msgid "Filters set deleted" msgstr "Jeu de filtres supprimé" -#: extensions/sievefilters/views.py:241 +#: extensions/sievefilters/views.py:252 msgid "Filters set activated" msgstr "Jeu de filtres activé" @@ -1892,15 +1886,15 @@ msgstr "Actif" msgid "No filter defined yet" msgstr "Aucun filtre défini." -#: extensions/sievefilters/templates/sievefilters/index.html:55 +#: extensions/sievefilters/templates/sievefilters/index.html:56 msgid "Filter sets" msgstr "Jeux de filtres" -#: extensions/sievefilters/templates/sievefilters/index.html:59 +#: extensions/sievefilters/templates/sievefilters/index.html:60 msgid "active" msgstr "actif" -#: extensions/sievefilters/templates/sievefilters/index.html:73 +#: extensions/sievefilters/templates/sievefilters/index.html:74 msgid "" "No filters set defined yet. You must create one in order to define filters." msgstr "" @@ -2218,84 +2212,88 @@ msgstr "Réponse du serveur" msgid "Cci" msgstr "Cci" -#: extensions/webmail/imaputils.py:259 +#: extensions/webmail/imaputils.py:237 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Impossible de récuperer le séparateur de hiérarchie" + +#: extensions/webmail/imaputils.py:278 #, python-format msgid "Connection to IMAP server failed: %s" msgstr "La connexion au serveur IMAP a échoué : %s" -#: extensions/webmail/lib.py:353 +#: extensions/webmail/lib.py:358 msgid "wrote:" msgstr "a écrit :" -#: extensions/webmail/lib.py:418 extensions/webmail/lib.py:422 +#: extensions/webmail/lib.py:423 extensions/webmail/lib.py:427 msgid "Original message" msgstr "Message original" -#: extensions/webmail/views.py:81 +#: extensions/webmail/views.py:86 #, python-format msgid "%(count)d message deleted" msgid_plural "%(count)d messages deleted" msgstr[0] "%(count)d message supprimé" msgstr[1] "%(count)d messages supprimés" -#: extensions/webmail/views.py:98 +#: extensions/webmail/views.py:103 msgid "Unknown action" msgstr "Action inconnue" -#: extensions/webmail/views.py:111 lib/email_listing.py:137 +#: extensions/webmail/views.py:116 lib/email_listing.py:137 msgid "Empty mailbox" msgstr "Boite aux lettre vide" -#: extensions/webmail/views.py:128 -#: extensions/webmail/templatetags/webextras.py:169 +#: extensions/webmail/views.py:133 +#: extensions/webmail/templatetags/webextras.py:170 msgid "Create a new mailbox" msgstr "Créer une nouvelle boite aux lettres" -#: extensions/webmail/views.py:143 +#: extensions/webmail/views.py:149 msgid "Mailbox created" msgstr "Boite aux lettres créée" -#: extensions/webmail/views.py:159 +#: extensions/webmail/views.py:165 msgid "Edit mailbox" msgstr "Editer la boite aux lettres" -#: extensions/webmail/views.py:173 +#: extensions/webmail/views.py:180 msgid "Mailbox updated" msgstr "Boite aux lettres mise à jour" -#: extensions/webmail/views.py:207 extensions/webmail/views.py:388 -#: extensions/webmail/views.py:497 +#: extensions/webmail/views.py:214 extensions/webmail/views.py:396 +#: extensions/webmail/views.py:505 msgid "Bad request" msgstr "Mauvaise requête" -#: extensions/webmail/views.py:238 +#: extensions/webmail/views.py:246 msgid "Failed to save attachment: " msgstr "Impossible d'enregistrer la pièce jointe : " -#: extensions/webmail/views.py:241 +#: extensions/webmail/views.py:249 #, python-format msgid "Attachment is too big (limit: %s)" msgstr "Pièce jointe trop grosse (limite : %s)" -#: extensions/webmail/views.py:247 +#: extensions/webmail/views.py:255 #: extensions/webmail/templates/webmail/compose.html:15 #: extensions/webmail/templates/webmail/headers.html:4 msgid "Attachments" msgstr "Pièces jointes" -#: extensions/webmail/views.py:263 +#: extensions/webmail/views.py:271 msgid "Bad query" msgstr "Mauvaise requête" -#: extensions/webmail/views.py:273 +#: extensions/webmail/views.py:281 msgid "Failed to remove attachment: " msgstr "Impossible de supprimer la pièce jointe :" -#: extensions/webmail/views.py:278 +#: extensions/webmail/views.py:286 msgid "Unknown attachment" msgstr "Pièce jointe inconnue" -#: extensions/webmail/views.py:493 +#: extensions/webmail/views.py:501 msgid "Undefined action" msgstr "Action indéfinie" @@ -2326,7 +2324,7 @@ msgstr[1] "%(counter)s fichiers" msgid "Parent mailbox" msgstr "Boite aux lettres parente" -#: extensions/webmail/templates/webmail/index.html:36 +#: extensions/webmail/templates/webmail/index.html:38 msgid "Mailbox quota" msgstr "Quota de la boite aux lettres" @@ -2388,15 +2386,15 @@ msgstr "Comme non lus" msgid "Compress folder" msgstr "Compresser ce dossier" -#: extensions/webmail/templatetags/webextras.py:109 +#: extensions/webmail/templatetags/webextras.py:110 msgid "Empty folder" msgstr "Vider ce dossier" -#: extensions/webmail/templatetags/webextras.py:177 +#: extensions/webmail/templatetags/webextras.py:178 msgid "Edit the selected mailbox" msgstr "Editer la boite aux lettres sélectionnée" -#: extensions/webmail/templatetags/webextras.py:182 +#: extensions/webmail/templatetags/webextras.py:183 msgid "Remove the selected mailbox" msgstr "Supprimer la boite aux lettres sélectionnée" @@ -2450,7 +2448,7 @@ msgstr "La page demandée n'existe pas." msgid "An internal error occured." msgstr "Une erreur interne est survenue" -#: templates/nlayout.html:56 +#: templates/nlayout.html:62 msgid "Powered by Modoboa" msgstr "Propulsé par Modoboa" @@ -2484,11 +2482,11 @@ msgstr "Fermer" msgid "Welcome to Modoboa" msgstr "Bienvenue dans Modoboa" -#: templates/registration/login.html:33 +#: templates/registration/login.html:34 msgid "Remember me" msgstr "Se souvenir de moi" -#: templates/registration/login.html:35 +#: templates/registration/login.html:36 msgid "Log in" msgstr "Se connecter" @@ -2504,7 +2502,7 @@ msgstr "Paramètres par utilisateur" msgid "Prefered language" msgstr "Langage préféré" -#: userprefs/app_settings.py:26 +#: userprefs/app_settings.py:27 msgid "Prefered language to display pages" msgstr "Langage préféré pour afficher les pages" diff --git a/modoboa/locale/it/LC_MESSAGES/django.po b/modoboa/locale/it/LC_MESSAGES/django.po index f7bc75730..dcf26d52d 100644 --- a/modoboa/locale/it/LC_MESSAGES/django.po +++ b/modoboa/locale/it/LC_MESSAGES/django.po @@ -7,9 +7,9 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-06-27 17:24+0200\n" -"PO-Revision-Date: 2013-06-28 17:03+0200\n" -"Last-Translator: Giancarlo Gaifas \n" +"POT-Creation-Date: 2013-09-26 23:42+0200\n" +"PO-Revision-Date: 2013-09-27 00:07+0200\n" +"Last-Translator: Alberto Berti \n" "Language-Team: ITALIANO \n" "Language: it\n" "MIME-Version: 1.0\n" @@ -25,8 +25,8 @@ msgstr "Interfaccia amministrativa" msgid "Web based console to manage domains, accounts and aliases" msgstr "Interfaccia web per l'amministrazione di domini, account e alias" -#: admin/__init__.py:16 admin/forms.py:180 admin/forms.py:615 -#: admin/views.py:93 admin/views.py:453 +#: admin/__init__.py:16 admin/forms.py:211 admin/forms.py:652 +#: admin/views.py:94 admin/views.py:447 #: extensions/postfix_autoreply/app_settings.py:11 #: extensions/sievefilters/app_settings.py:50 #: extensions/stats/app_settings.py:11 extensions/webmail/app_settings.py:11 @@ -56,7 +56,8 @@ msgstr "Schema password predefinito" #: admin/app_settings.py:50 msgid "Scheme used to crypt mailbox passwords" -msgstr "Schema utilizzato per la criptazione delle password delle caselle di posta" +msgstr "" +"Schema utilizzato per la criptazione delle password delle caselle di posta" #: admin/app_settings.py:54 msgid "Secret key" @@ -120,7 +121,9 @@ msgstr "collega DN" msgid "" "The distinguished name to use when binding to the LDAP server. Leave empty " "for an anonymous bind" -msgstr "Nome particolare da usare quando ci si collega al server LDAP. Lasciare vuoto per un collegamento anonimo" +msgstr "" +"Nome particolare da usare quando ci si collega al server LDAP. Lasciare " +"vuoto per un collegamento anonimo" #: admin/app_settings.py:97 msgid "Bind password" @@ -128,7 +131,9 @@ msgstr "Password del collegamento" #: admin/app_settings.py:99 msgid "The password to use when binding to the LDAP server (with 'Bind DN')" -msgstr "La password da utilizzare quando ci si collega al server LDAP (con 'Collega DN')" +msgstr "" +"La password da utilizzare quando ci si collega al server LDAP (con 'Collega " +"DN')" #: admin/app_settings.py:105 msgid "Search base" @@ -146,7 +151,9 @@ msgstr "Filtro ricerca" msgid "" "An optional filter string (e.g. '(objectClass=person)'). In order to be " "valid, it must be enclosed in parentheses." -msgstr "Stringa di filtraggio opzionale (p.es '(objectClass=person)'). Per essere valida deve essere racchiusa tra parentesi." +msgstr "" +"Stringa di filtraggio opzionale (p.es '(objectClass=person)'). Per essere " +"valida deve essere racchiusa tra parentesi." #: admin/app_settings.py:119 msgid "User DN template" @@ -157,7 +164,9 @@ msgstr "Modello DN dell'utente" msgid "" "The template used to construct a user's DN. It should contain one " "placeholder (ie. %(user)s)" -msgstr "Il modello utilizzato per comporre un DN dell'utente. Deve contenere un segnaposto (p.es. %(user)s)" +msgstr "" +"Il modello utilizzato per comporre un DN dell'utente. Deve contenere un " +"segnaposto (p.es. %(user)s)" #: admin/app_settings.py:126 msgid "Password attribute" @@ -190,7 +199,9 @@ msgstr "Gestione delle caselle di posta nel filsystem" msgid "" "Rename or remove mailboxes on the filesystem when they get renamed or " "removed within Modoboa" -msgstr "Rinomina o rimuovi le caselle di posta nel filsystem quando sono rinominate o rimosse in Modoboa" +msgstr "" +"Rinomina o rimuovi le caselle di posta nel filsystem quando sono rinominate " +"o rimosse in Modoboa" #: admin/app_settings.py:146 msgid "Mailboxes ower" @@ -206,7 +217,8 @@ msgstr "Rimozione automatica dell'account" #: admin/app_settings.py:154 msgid "When a mailbox is removed, also remove the associated account" -msgstr "Quando una casella postale viene rimossa rimuovi anche l'account associato" +msgstr "" +"Quando una casella postale viene rimossa rimuovi anche l'account associato" #: admin/app_settings.py:157 admin/templates/admin/editdomainform.html:7 msgid "Miscellaneous" @@ -234,43 +246,51 @@ msgstr "Reindirizzamento principale predefinito" #: admin/app_settings.py:175 msgid "The default redirection used when no application is specified" -msgstr "Redirezione predefinita utilizzata quando non è specificata alcuna applicazione" +msgstr "" +"Redirezione predefinita utilizzata quando non è specificata alcuna " +"applicazione" -#: admin/app_settings.py:215 +#: admin/app_settings.py:213 +msgid "Key must be either 16, 24, or 32 bytes long" +msgstr "La chiave può essere lunga solo 16, 24 o 32 bytes" + +#: admin/app_settings.py:222 msgid "Invalid syntax" msgstr "Sintassi errata" -#: admin/app_settings.py:238 +#: admin/app_settings.py:245 msgid "This field is required" msgstr "Questo campo è obbligatorio" -#: admin/forms.py:17 admin/forms.py:460 admin/tables.py:18 +#: admin/forms.py:20 admin/forms.py:497 admin/tables.py:18 msgid "Alias(es)" msgstr "Alias" -#: admin/forms.py:19 +#: admin/forms.py:22 msgid "" "Alias(es) of this domain. Indicate only one name per input, press ENTER to " "add a new input." -msgstr "Alias per questo dominio. Inserire un solo nome alla volta, premere INVIO per aggiungere un nuovo elemento" +msgstr "" +"Alias per questo dominio. Inserire un solo nome alla volta, premere INVIO " +"per aggiungere un nuovo elemento" -#: admin/forms.py:56 +#: admin/forms.py:59 msgid "An alias with this name already exists" msgstr "Esiste già un alias con questo nome" -#: admin/forms.py:70 +#: admin/forms.py:73 msgid "A domain with this name already exists" msgstr "Esiste già un dominio con questo nome" -#: admin/forms.py:115 +#: admin/forms.py:146 msgid "Create a domain administrator" msgstr "Crea un amministratore di dominio" -#: admin/forms.py:117 +#: admin/forms.py:148 msgid "Automatically create an administrator for this domain" msgstr "Crea automaticamente un amministratore per questo dominio" -#: admin/forms.py:121 admin/tables.py:12 admin/tables.py:47 +#: admin/forms.py:152 admin/tables.py:12 admin/tables.py:47 #: admin/templates/admin/editdomainform.html:13 #: admin/templates/admin/identities_table.html:5 #: admin/templates/admin/information.html:8 @@ -279,348 +299,362 @@ msgstr "Crea automaticamente un amministratore per questo dominio" msgid "Name" msgstr "Nome" -#: admin/forms.py:123 +#: admin/forms.py:154 msgid "" "The administrator's name. Don't include the domain's name here, it will be " "automatically appended." -msgstr "Il nome dell'amministratore. Non includere il nome del dominio, verrà aggiunto automaticamente." +msgstr "" +"Il nome dell'amministratore. Non includere il nome del dominio, verrà " +"aggiunto automaticamente." -#: admin/forms.py:129 +#: admin/forms.py:160 msgid "Create aliases" msgstr "Crea alias" -#: admin/forms.py:131 +#: admin/forms.py:162 msgid "Automatically create standard aliases for this domain" msgstr "Crea automaticamente gli alias standard per questo dominio" -#: admin/forms.py:144 +#: admin/forms.py:175 msgid "Invalid format" msgstr "Formato non valido" -#: admin/forms.py:156 +#: admin/forms.py:187 #, python-format msgid "User '%s' already exists" msgstr "L'utente '%s' esiste già" -#: admin/forms.py:204 +#: admin/forms.py:235 msgid "Email address" msgstr "Indirizzo email" -#: admin/forms.py:205 +#: admin/forms.py:236 msgid "" "The distribution list address. Use the '*' character to create a 'catchall' " "address (ex: *@domain.tld)." msgstr "Elenco degli indirizzi di smistamento" -#: admin/forms.py:208 +#: admin/forms.py:239 msgid "Recipients" msgstr "Destinatari" -#: admin/forms.py:209 +#: admin/forms.py:240 msgid "" "Mailbox(es) this alias will point to. Indicate only one address per input, " "press ENTER to add a new input." -msgstr "Casella(e) di posta a cui l'alias punterà. Inserire un solo indirizzo alla volta, premere INVIO per aggiungere un nuovo elemento." +msgstr "" +"Casella(e) di posta a cui l'alias punterà. Inserire un solo indirizzo alla " +"volta, premere INVIO per aggiungere un nuovo elemento." -#: admin/forms.py:247 admin/forms.py:505 admin/forms.py:531 +#: admin/forms.py:278 admin/forms.py:542 admin/forms.py:568 msgid "Domain does not exist" msgstr "Il dominio non esiste" -#: admin/forms.py:249 +#: admin/forms.py:280 msgid "You don't have access to this domain" msgstr "Non hai accesso a questo dominio" -#: admin/forms.py:269 +#: admin/forms.py:300 msgid "Invalid mailbox" msgstr "Casella di posta non valida" -#: admin/forms.py:285 admin/models.py:1175 +#: admin/forms.py:316 admin/models.py:1202 #, python-format msgid "Local recipient %s not found" msgstr "Il destinatario locale %s non è stato trovato" -#: admin/forms.py:287 admin/forms.py:293 +#: admin/forms.py:318 admin/forms.py:324 #, python-format msgid "Recipient %s already present" msgstr "Il destinatario %s è già esistente" -#: admin/forms.py:298 +#: admin/forms.py:329 msgid "No recipient defined" msgstr "Non è stato definito un destinatario" -#: admin/forms.py:312 extensions/webmail/forms.py:19 +#: admin/forms.py:343 extensions/webmail/forms.py:19 msgid "Select a file" msgstr "Seleziona un file" -#: admin/forms.py:314 admin/forms.py:342 +#: admin/forms.py:345 admin/forms.py:373 msgid "Separator" msgstr "Separatore" -#: admin/forms.py:317 +#: admin/forms.py:348 msgid "Continue on error" msgstr "Continuare in caso di errore" -#: admin/forms.py:318 +#: admin/forms.py:349 msgid "Don't treat duplicated objects as error" msgstr "Non considerare errore gli oggetti duplicati" -#: admin/forms.py:333 +#: admin/forms.py:364 msgid "Crypt passwords" msgstr "Cripta le password" -#: admin/forms.py:334 +#: admin/forms.py:365 msgid "Check this option if passwords contained in your file are not crypted" -msgstr "Seleziona questa opzione se le password contenute nel file non sono criptate" +msgstr "" +"Seleziona questa opzione se le password contenute nel file non sono criptate" -#: admin/forms.py:340 +#: admin/forms.py:371 msgid "File name" msgstr "Nome del file" -#: admin/forms.py:372 admin/templates/admin/editdomainform.html:13 +#: admin/forms.py:403 admin/templates/admin/editdomainform.html:13 #: auth/forms.py:7 msgid "Username" msgstr "Nome utente" -#: admin/forms.py:374 +#: admin/forms.py:405 msgid "Role" msgstr "Ruolo" -#: admin/forms.py:375 admin/forms.py:402 +#: admin/forms.py:406 admin/forms.py:433 msgid "Choose" msgstr "Scegli" -#: admin/forms.py:376 +#: admin/forms.py:407 msgid "What level of permission this user will have" msgstr "Il livello di permessi che avrà questo utente" -#: admin/forms.py:378 auth/forms.py:11 +#: admin/forms.py:409 auth/forms.py:11 msgid "Password" msgstr "Password" -#: admin/forms.py:380 userprefs/forms.py:18 +#: admin/forms.py:411 userprefs/forms.py:18 msgid "Confirmation" msgstr "Conferma" -#: admin/forms.py:382 +#: admin/forms.py:413 msgid "Enter the same password as above, for verification." -msgstr "Inserisci la medesima password che hai inserito sopra, per la verifica." +msgstr "" +"Inserisci la medesima password che hai inserito sopra, per la verifica." -#: admin/forms.py:393 +#: admin/forms.py:424 msgid "Enabled" msgstr "Abilitato" -#: admin/forms.py:435 +#: admin/forms.py:471 msgid "The two password fields didn't match." msgstr "Le password nei due campi non corrispondono" -#: admin/forms.py:441 +#: admin/forms.py:477 msgid "You can't disable your own account" msgstr "Tu non puoi disabilitare il tuo account" -#: admin/forms.py:451 +#: admin/forms.py:488 msgid "E-mail" msgstr "E-mail" -#: admin/forms.py:453 +#: admin/forms.py:490 msgid "Quota" msgstr "Quota" -#: admin/forms.py:455 +#: admin/forms.py:492 msgid "" "Quota in MB for this mailbox. Define a custom value or use domain's default " "one. Leave empty to define an unlimited value (not allowed for domain " "administrators)." -msgstr "Quota in MB per questo account. Stabilire un valore personalizzato o usare quello predefinito. Lasciare vuoto per non impostare limiti (non consentito per gli amministratori di dominio)." +msgstr "" +"Quota in MB per questo account. Stabilire un valore personalizzato o usare " +"quello predefinito. Lasciare vuoto per non impostare limiti (non consentito " +"per gli amministratori di dominio)." -#: admin/forms.py:462 +#: admin/forms.py:499 msgid "" "Alias(es) of this mailbox. Indicate only one address per input, press ENTER " "to add a new input. Use the '*' character to create a 'catchall' alias (ex: " "*@domain.tld)." -msgstr "Alias per questa casella di posta. Inserire un solo indirizzo alla volta, premere INVIO per aggiungere un nuovo elemento. Utilizzare il carattere '*' per creare un alias 'prenditutto' (p.es.: *@dominio.org)." +msgstr "" +"Alias per questa casella di posta. Inserire un solo indirizzo alla volta, " +"premere INVIO per aggiungere un nuovo elemento. Utilizzare il carattere '*' " +"per creare un alias 'prenditutto' (p.es.: *@dominio.org)." -#: admin/forms.py:513 +#: admin/forms.py:550 #, python-format msgid "Mailbox %s already exists" msgstr "La casella di posta %s esiste già" -#: admin/forms.py:573 +#: admin/forms.py:610 msgid "Domain(s)" msgstr "Dominio(i)" -#: admin/forms.py:575 +#: admin/forms.py:612 msgid "Domain(s) that user administrates" msgstr "Dominio(i) amministrato(i) dall'utente" -#: admin/forms.py:617 admin/views.py:456 +#: admin/forms.py:654 admin/views.py:450 msgid "Mail" msgstr "Posta" -#: admin/forms.py:619 +#: admin/forms.py:656 msgid "Permissions" msgstr "Permessi" -#: admin/models.py:78 +#: admin/models.py:80 msgid "You can't delete your own account" msgstr "Non puoi cancellare il tuo account" -#: admin/models.py:159 +#: admin/models.py:161 msgid "Failed to update password: LDAP module not installed" msgstr "Aggiornamento della password fallito: il modulo LDAP non è installato" -#: admin/models.py:165 +#: admin/models.py:167 #, python-format msgid "Failed to update password: %s" msgstr "Aggiornamento della password fallito: %s" -#: admin/models.py:192 +#: admin/models.py:194 msgid "account" msgstr "account" -#: admin/models.py:494 admin/models.py:720 admin/models.py:782 +#: admin/models.py:496 admin/models.py:733 admin/models.py:795 msgid "Invalid line" msgstr "Riga non valida" -#: admin/models.py:498 +#: admin/models.py:500 msgid "You can't import an account with a role greater than yours" msgstr "Non puoi importare un account con un ruolo superiore al tuo" -#: admin/models.py:518 +#: admin/models.py:506 +#, python-format +msgid "The simple user '%s' must have a valid email address" +msgstr "L'utente '%s' deve avere un indirizzo email valido" + +#: admin/models.py:510 +#, python-format +msgid "username and email fields must not differ for '%s'" +msgstr "I campi nome utente e email di '%s' devono avere lo stesso valore" + +#: admin/models.py:534 #, python-format msgid "Account import failed (%s): domain does not exist" msgstr "Importazione dell'account fallito (%s): il dominio non esiste" -#: admin/models.py:607 admin/models.py:733 +#: admin/models.py:624 admin/models.py:746 msgid "name" msgstr "nome" -#: admin/models.py:608 +#: admin/models.py:625 msgid "The domain name" msgstr "Il nome del dominio" -#: admin/models.py:610 +#: admin/models.py:627 msgid "Default quota in MB applied to mailboxes" msgstr "Quota in MB predefinita applicata alle caselle di posta" -#: admin/models.py:613 admin/models.py:740 admin/models.py:1020 -#: admin/models.py:1190 extensions/postfix_autoreply/models.py:19 +#: admin/models.py:630 admin/models.py:753 admin/models.py:1045 +#: admin/models.py:1217 extensions/postfix_autoreply/models.py:27 msgid "enabled" msgstr "Attivato" -#: admin/models.py:614 +#: admin/models.py:631 msgid "Check to activate this domain" msgstr "Seleziona per attivare questo dominio" -#: admin/models.py:734 +#: admin/models.py:747 msgid "The alias name" msgstr "Il nome dell'alias" -#: admin/models.py:736 +#: admin/models.py:749 msgid "target" msgstr "destinazione" -#: admin/models.py:737 +#: admin/models.py:750 msgid "The domain this alias points to" msgstr "Il dominio a cui punta questo alias" -#: admin/models.py:741 admin/models.py:1021 +#: admin/models.py:754 admin/models.py:1046 msgid "Check to activate this alias" msgstr "Seleziona per attivare questo alias" -#: admin/models.py:788 +#: admin/models.py:801 #, python-format msgid "Unknown domain %s" msgstr "Dominio sconosciuto %s" -#: admin/models.py:805 admin/models.py:1006 +#: admin/models.py:818 admin/models.py:1031 msgid "address" msgstr "indirizzo" -#: admin/models.py:806 +#: admin/models.py:819 msgid "Mailbox address (without the @domain.tld part)" msgstr "Indirizzo della casella di posta (senza la parte @dominio.org)" -#: admin/models.py:852 +#: admin/models.py:877 #, python-format msgid "Failed to retrieve mailbox location (%s)" msgstr "Impossibile recuperare la posizione della casella di posta (%s)" -#: admin/models.py:868 -#, python-format -msgid "Failed to rename mailbox: %s" -msgstr "Impossibile rinominare la casella di posta: %s" - -#: admin/models.py:894 -#, python-format -msgid "Failed to remove mailbox: %s" -msgstr "Impossibile rimuovere la casella di posta: %s" - -#: admin/models.py:909 +#: admin/models.py:921 #, python-format msgid "Quota is greater than the allowed domain's limit (%dM)" msgstr "La quota è maggiore del limite stabilito per il dominio (%dM)" -#: admin/models.py:914 +#: admin/models.py:926 msgid "A quota is required" msgstr "È richiesta una quota" -#: admin/models.py:1007 +#: admin/models.py:1032 msgid "" "The alias address (without the domain part). For a 'catch-all' address, just " "enter an * character." -msgstr "L'indirizzo alternativo (senza la parte del dominio). Per un indirizzo 'prenditutto' inserisci un carattere '*'." +msgstr "" +"L'indirizzo alternativo (senza la parte del dominio). Per un indirizzo " +"'prenditutto' inserisci un carattere '*'." -#: admin/models.py:1011 +#: admin/models.py:1036 msgid "mailboxes" msgstr "caselle di posta" -#: admin/models.py:1012 +#: admin/models.py:1037 msgid "The mailboxes this alias points to" msgstr "Le caselle di posta a cui punta questo alias" -#: admin/models.py:1016 +#: admin/models.py:1041 msgid "The aliases this alias points to" msgstr "Gli alias a cui questo alias puntano" -#: admin/models.py:1064 +#: admin/models.py:1089 msgid "distribution list" msgstr "elenco di smistamento" -#: admin/models.py:1065 +#: admin/models.py:1090 msgid "forward" msgstr "inoltra" -#: admin/models.py:1066 +#: admin/models.py:1091 msgid "alias" msgstr "alias" -#: admin/models.py:1145 +#: admin/models.py:1170 #, python-format msgid "Invalid line: %s" msgstr "Riga non valida: %s" -#: admin/models.py:1150 +#: admin/models.py:1175 #, python-format msgid "Domain '%s' does not exist" msgstr "Il dominio '%s' non esiste" -#: admin/models.py:1191 +#: admin/models.py:1218 msgid "Check to enable this extension" msgstr "Seleziona per abilitare questa estensione" -#: admin/models.py:1291 +#: admin/models.py:1318 msgid "added" msgstr "aggiunto" -#: admin/models.py:1294 +#: admin/models.py:1321 msgid "modified" msgstr "modificato" -#: admin/models.py:1297 +#: admin/models.py:1324 msgid "deleted" msgstr "cancellato" -#: admin/models.py:1299 +#: admin/models.py:1326 #, python-format msgid "%(object)s '%(name)s' %(action)s by user %(user)s" msgstr "%(object)s '%(name)s' %(action)s dell'utente %(user)s" @@ -630,7 +664,7 @@ msgid "Edit domain" msgstr "Modifica dominio" #: admin/tables.py:21 admin/templates/admin/identities_table.html:8 -#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:79 +#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:83 #: extensions/sievefilters/templates/sievefilters/filter.html:17 #: extensions/sievefilters/templates/sievefilters/guieditor.html:5 #: extensions/webmail/templatetags/webextras.py:97 @@ -645,143 +679,143 @@ msgstr "Versione" msgid "Description" msgstr "Descrizione" -#: admin/views.py:87 +#: admin/views.py:88 msgid "New domain" msgstr "Nuovo dominio" -#: admin/views.py:88 admin/views.py:97 admin/views.py:224 admin/views.py:450 -#: admin/views.py:457 extensions/sievefilters/views.py:120 -#: extensions/sievefilters/views.py:187 extensions/webmail/views.py:133 +#: admin/views.py:89 admin/views.py:98 admin/views.py:228 admin/views.py:444 +#: admin/views.py:451 extensions/sievefilters/views.py:129 +#: extensions/sievefilters/views.py:198 extensions/webmail/views.py:128 msgid "Create" msgstr "Crea" -#: admin/views.py:94 admin/views.py:454 +#: admin/views.py:95 admin/views.py:448 msgid "Next" msgstr "Successivo" -#: admin/views.py:96 +#: admin/views.py:97 msgid "Options" msgstr "Opzioni" -#: admin/views.py:98 admin/views.py:458 +#: admin/views.py:99 admin/views.py:452 msgid "Previous" msgstr "Precedente" -#: admin/views.py:111 +#: admin/views.py:112 msgid "Domain created" msgstr "Dominio creato" -#: admin/views.py:144 admin/views.py:281 admin/views.py:504 +#: admin/views.py:146 admin/views.py:285 admin/views.py:498 #: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:9 -#: extensions/sievefilters/views.py:139 extensions/webmail/views.py:164 +#: extensions/sievefilters/views.py:148 extensions/webmail/views.py:160 #: userprefs/templates/userprefs/forward.html:8 #: userprefs/templates/userprefs/profile.html:8 msgid "Update" msgstr "Aggiorna" -#: admin/views.py:160 +#: admin/views.py:162 msgid "Domain modified" msgstr "Dominio modificato" -#: admin/views.py:185 +#: admin/views.py:189 msgid "You can't delete your own domain" msgstr "Non puoi cancellare il tuo dominio" -#: admin/views.py:188 +#: admin/views.py:192 msgid "Domain deleted" msgid_plural "Domains deleted" msgstr[0] "Dominio cancellato" msgstr[1] "Domini cancellati" -#: admin/views.py:203 +#: admin/views.py:207 msgid "Alias with this name already exists" msgstr "Un alias con questo nome esiste già" -#: admin/views.py:246 +#: admin/views.py:250 msgid "New distribution list" msgstr "Nuovo elenco di smistamento" -#: admin/views.py:247 +#: admin/views.py:251 msgid "Distribution list created" msgstr "Elenco di smistamento creato" -#: admin/views.py:256 +#: admin/views.py:260 msgid "New alias" msgstr "Nuovo alias" -#: admin/views.py:257 +#: admin/views.py:261 msgid "Alias created" msgstr "Alias creato" -#: admin/views.py:266 +#: admin/views.py:270 msgid "New forward" msgstr "Nuovo inoltro" -#: admin/views.py:267 +#: admin/views.py:271 msgid "Forward created" msgstr "Inoltro creato" -#: admin/views.py:285 +#: admin/views.py:289 msgid "Distribution list modified" msgstr "Elenco di smistamento modificato" -#: admin/views.py:287 +#: admin/views.py:291 msgid "Forward modified" msgstr "Inoltro modificato" -#: admin/views.py:289 +#: admin/views.py:293 msgid "Alias modified" msgstr "Alias modificato" -#: admin/views.py:408 admin/views.py:551 admin/views.py:556 -#: extensions/amavis/views.py:195 extensions/amavis/views.py:201 -#: extensions/amavis/views.py:234 extensions/amavis/views.py:238 -#: extensions/amavis/views.py:240 extensions/webmail/views.py:37 -#: extensions/webmail/views.py:65 extensions/webmail/views.py:78 -#: extensions/webmail/views.py:95 extensions/webmail/views.py:111 -#: extensions/webmail/views.py:195 extensions/webmail/views.py:422 -#: extensions/webmail/views.py:433 extensions/webmail/views.py:460 +#: admin/views.py:402 admin/views.py:545 admin/views.py:550 +#: extensions/amavis/views.py:204 extensions/amavis/views.py:210 +#: extensions/amavis/views.py:243 extensions/amavis/views.py:247 +#: extensions/amavis/views.py:249 extensions/webmail/views.py:42 +#: extensions/webmail/views.py:60 extensions/webmail/views.py:73 +#: extensions/webmail/views.py:90 extensions/webmail/views.py:106 +#: extensions/webmail/views.py:192 extensions/webmail/views.py:419 +#: extensions/webmail/views.py:430 extensions/webmail/views.py:457 #: lib/formutils.py:32 lib/formutils.py:37 msgid "Invalid request" msgstr "Richiesta non valida" -#: admin/views.py:447 +#: admin/views.py:441 msgid "New account" msgstr "Nuovo account" -#: admin/views.py:471 +#: admin/views.py:465 msgid "Account created" msgstr "Account creato" -#: admin/views.py:519 +#: admin/views.py:513 msgid "Account updated" msgstr "Account aggiornato" -#: admin/views.py:541 +#: admin/views.py:535 msgid "Account deleted" msgid_plural "Accounts deleted" msgstr[0] "Account cancellato" msgstr[1] "Account cancellati" -#: admin/views.py:596 +#: admin/views.py:590 msgid "Parameters saved" msgstr "Parametri salvati" -#: admin/views.py:642 +#: admin/views.py:636 msgid "Modifications applied." msgstr "Modifiche applicate" -#: admin/views.py:749 +#: admin/views.py:743 #, python-format msgid "Object already exists: %s" msgstr "L'oggetto esiste già: %s" -#: admin/views.py:751 +#: admin/views.py:745 #, python-format msgid "%d objects imported successfully" msgstr "%d oggetti importati correttamente" -#: admin/views.py:769 +#: admin/views.py:763 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
                  \n" @@ -798,34 +832,37 @@ msgid "" msgstr "" "Fornire un file CSV le cui righe rispettino uno dei seguenti formati:\n" "
                    \n" -"
                  • account; loginname; password; nome; cognome; abilitato; group; indirizzo[, dominio, ...]
                  • \n" +"
                  • account; loginname; password; nome; cognome; abilitato; group; " +"indirizzo[, dominio, ...]
                  • \n" "
                  • alias; indirizzo; abilitato; destinatario interno
                  • \n" "
                  • forward; indirizzo; abilitato; destinatario esterno
                  • \n" -"
                  • dlist; indirizzo; abilitato; destinatario; destinatario; ...
                  • \n" +"
                  • dlist; indirizzo; abilitato; destinatario; destinatario; ...\n" "
                  \n" -"

                  Il primo elemento di ogni riga è obbligatorio e deve essere uguale a quello dei valori precedenti.

                  \n" +"

                  Il primo elemento di ogni riga è obbligatorio e deve essere uguale a " +"quello dei valori precedenti.

                  \n" "\n" "

                  Come separatore puoi utilizzare un carattere diverso.

                  \n" -#: admin/views.py:781 +#: admin/views.py:775 msgid "Import identities" msgstr "Identità di importazione" -#: admin/views.py:782 admin/views.py:852 admin/templatetags/admin_extras.py:87 +#: admin/views.py:776 admin/views.py:846 admin/templatetags/admin_extras.py:87 #: admin/templatetags/admin_extras.py:144 msgid "Import" msgstr "Importa" -#: admin/views.py:812 +#: admin/views.py:806 msgid "Export identities" msgstr "Identità di esportazione" -#: admin/views.py:813 admin/views.py:869 admin/templatetags/admin_extras.py:93 +#: admin/views.py:807 admin/views.py:863 admin/templatetags/admin_extras.py:93 #: admin/templatetags/admin_extras.py:150 msgid "Export" msgstr "Esporta" -#: admin/views.py:841 +#: admin/views.py:835 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
                    \n" @@ -841,15 +878,16 @@ msgstr "" "
                  • dominio; nome; quota; abilitato
                  • \n" "
                  • aliasdominio; nome; dominio target; abilitato
                  • \n" "
                  \n" -"

                  Il primo elemento di ogni riga è obbligatorio e deve essere uguale a quello dei valori precedenti.

                  \n" +"

                  Il primo elemento di ogni riga è obbligatorio e deve essere uguale a " +"quello dei valori precedenti.

                  \n" "\n" "

                  Come separatore puoi utilizzare un carattere diverso.

                  \n" -#: admin/views.py:851 +#: admin/views.py:845 msgid "Import domains" msgstr "Importa domini" -#: admin/views.py:868 +#: admin/views.py:862 msgid "Export domains" msgstr "Esporta domini" @@ -922,7 +960,7 @@ msgid "Tags" msgstr "Tag" #: admin/templates/admin/importform.html:5 -#: admin/templatetags/admin_extras.py:305 +#: admin/templatetags/admin_extras.py:290 msgid "Help" msgstr "Aiuto" @@ -969,7 +1007,7 @@ msgid "Previously on Modoboa..." msgstr "Precedentemente su Modoboa ..." #: admin/templates/admin/logs.html:6 extensions/amavis/sql_listing.py:25 -#: extensions/webmail/lib.py:37 extensions/webmail/lib.py:406 +#: extensions/webmail/lib.py:38 extensions/webmail/lib.py:413 msgid "Date" msgstr "Data" @@ -1029,11 +1067,11 @@ msgstr "Impostazioni Modoboa" #: admin/templatetags/admin_extras.py:13 msgid "enabled_m" -msgstr "" +msgstr "Abilitato" #: admin/templatetags/admin_extras.py:13 msgid "enabled_f" -msgstr "" +msgstr "Abilitata" #: admin/templatetags/admin_extras.py:36 msgid "Modoboa" @@ -1072,34 +1110,21 @@ msgid "View the domain's identities" msgstr "Visualizza le identita del dominio" #: admin/templatetags/admin_extras.py:178 -msgid "Delete the domain" -msgstr "Cancella il dominio" - #: admin/templatetags/admin_extras.py:194 -msgid "Delete this account" -msgstr "Cancella questo account" +#: admin/templatetags/admin_extras.py:201 +#, python-format +msgid "Delete %s?" +msgstr "Eliminare %s?" -#: admin/templatetags/admin_extras.py:202 -msgid "Delete this distribution list" -msgstr "Cancella l'elenco di smistamento" - -#: admin/templatetags/admin_extras.py:209 -msgid "Delete this forward" -msgstr "Cancella questo inoltro" - -#: admin/templatetags/admin_extras.py:216 -msgid "Delete this alias" -msgstr "Cancella questo alias" - -#: admin/templatetags/admin_extras.py:238 +#: admin/templatetags/admin_extras.py:223 msgid "Remove this permission" msgstr "Rimuovi questo permesso" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 msgid "Yes" msgstr "Si" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 msgid "No" msgstr "No" @@ -1151,8 +1176,10 @@ msgid "" msgstr "" "\n" "

                  Benvenuti alla demo online!

                  \n" -"

                  Attenzione: il contenuto della demo viene rinnovato ogni due ore.

                  \n" -"

                  Per provare la demo sono disponibili i seguenti utenti (username/passwordAttenzione: il contenuto della demo viene rinnovato ogni due ore.\n" +"

                  Per provare la demo sono disponibili i seguenti utenti (username/" +"password):

                  \n" "
                    \n" "
                  • Un super amministratore : admin/password
                  • \n" @@ -1206,7 +1233,8 @@ msgstr "Rimozione dei messaggi rilasciati" #: extensions/amavis/app_settings.py:24 msgid "Remove messages marked as released while cleaning up the database" -msgstr "Rimuovi i messaggi marcati come rilasciati durante la pulizia del database" +msgstr "" +"Rimuovi i messaggi marcati come rilasciati durante la pulizia del database" #: extensions/amavis/app_settings.py:28 msgid "Amavis connection mode" @@ -1295,12 +1323,12 @@ msgid "Virus filter" msgstr "Filtro virus" #: extensions/amavis/models.py:94 extensions/amavis/models.py:102 -#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:268 +#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:281 msgid "yes" msgstr "si" #: extensions/amavis/models.py:95 extensions/amavis/models.py:103 -#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:272 +#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:285 msgid "no" msgstr "no" @@ -1312,7 +1340,9 @@ msgstr "predefinito" #: extensions/amavis/models.py:98 msgid "Bypass virus checks or not. Choose 'default' to use global settings." -msgstr "Aggirare il controllo virus o no. Scegliere 'predefinito' per utilizzare le impostazioni globali." +msgstr "" +"Aggirare il controllo virus o no. Scegliere 'predefinito' per utilizzare le " +"impostazioni globali." #: extensions/amavis/models.py:101 msgid "Spam filter" @@ -1320,7 +1350,9 @@ msgstr "Filtro spam" #: extensions/amavis/models.py:106 msgid "Bypass spam checks or not. Choose 'default' to use global settings." -msgstr "Aggirare il controllo spam o no. Scegliere 'predefinito' per utilizzare le impostazioni globali." +msgstr "" +"Aggirare il controllo spam o no. Scegliere 'predefinito' per utilizzare le " +"impostazioni globali." #: extensions/amavis/models.py:109 msgid "Banned filter" @@ -1328,7 +1360,9 @@ msgstr "Filtro banned" #: extensions/amavis/models.py:114 msgid "Bypass banned checks or not. Choose 'default' to use global settings." -msgstr "Aggirare il controllo banned o no. Scegliere 'predefinito' per utilizzare le impostazioni globali." +msgstr "" +"Aggirare il controllo banned o no. Scegliere 'predefinito' per utilizzare le " +"impostazioni globali." #: extensions/amavis/models.py:144 msgid "Spam marker" @@ -1338,15 +1372,17 @@ msgstr "Contrassegno spam" msgid "" "Modify spam subject using the specified text. Choose 'default' to use global " "settings." -msgstr "Modifica dell'oggetto spam utilizzando un testo specifico. Scegliere 'predefinito' per utilizzare le impostazioni globali." +msgstr "" +"Modifica dell'oggetto spam utilizzando un testo specifico. Scegliere " +"'predefinito' per utilizzare le impostazioni globali." -#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:36 +#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:37 msgid "From" msgstr "Da" #: extensions/amavis/sql_listing.py:24 extensions/sievefilters/forms.py:68 -#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:35 -#: extensions/webmail/lib.py:404 templates/common/email_searchbar.html:6 +#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:36 +#: extensions/webmail/lib.py:411 templates/common/email_searchbar.html:6 msgid "Subject" msgstr "Oggetto" @@ -1356,37 +1392,37 @@ msgstr "Oggetto" msgid "To" msgstr "A" -#: extensions/amavis/views.py:34 +#: extensions/amavis/views.py:43 msgid "Empty quarantine" msgstr "Svuota quarantena" -#: extensions/amavis/views.py:202 +#: extensions/amavis/views.py:211 msgid "Message deleted" msgstr "Messaggio eliminato" -#: extensions/amavis/views.py:223 +#: extensions/amavis/views.py:232 #, python-format msgid "%(count)d message deleted successfully" msgid_plural "%(count)d messages deleted successfully" msgstr[0] "%(count)d messaggio eliminato con successo" msgstr[1] "%(count)d messaggi eliminati con successo" -#: extensions/amavis/views.py:243 +#: extensions/amavis/views.py:252 msgid "Request sent" msgstr "Request inviata" -#: extensions/amavis/views.py:249 +#: extensions/amavis/views.py:258 msgid "Message released" msgstr "Messaggio rilasciato" -#: extensions/amavis/views.py:271 +#: extensions/amavis/views.py:280 #, python-format msgid "%(count)d request sent" msgid_plural "%(count)d requests sent" msgstr[0] "%(count)d request inviata" msgstr[1] "%(count)d request inviate" -#: extensions/amavis/views.py:295 +#: extensions/amavis/views.py:304 #, python-format msgid "%(count)d message released successfully" msgid_plural "%(count)d messages released successfully" @@ -1421,8 +1457,12 @@ msgid "" msgid_plural "" "\n" "%(counter)s release requests are pending for action.\n" -msgstr[0] "\n%(counter)s richiesta di rilascio è in attesa di azioni\n" -msgstr[1] "\n%(counter)s richieste di rilascio sono in attesa di azioni\n" +msgstr[0] "" +"\n" +"%(counter)s richiesta di rilascio è in attesa di azioni\n" +msgstr[1] "" +"\n" +"%(counter)s richieste di rilascio sono in attesa di azioni\n" #: extensions/amavis/templates/amavis/notifications/pending_requests.html:6 msgid "Sketch:" @@ -1497,7 +1537,9 @@ msgstr "MIME errato" #: extensions/limits/__init__.py:23 msgid "" "Per administrator resources to limit the number of objects they can create" -msgstr "Risorse per amministratore. Limitazione del numero di oggetti che possono creare " +msgstr "" +"Risorse per amministratore. Limitazione del numero di oggetti che possono " +"creare " #: extensions/limits/__init__.py:54 msgid "Limits" @@ -1522,7 +1564,9 @@ msgstr "Amministratori di domini" #: extensions/limits/app_settings.py:15 msgid "Maximum number of allowed domain administrators for a new administrator" -msgstr "Numero massimo di amministratori di domini consentiti per un nuovo amministratore" +msgstr "" +"Numero massimo di amministratori di domini consentiti per un nuovo " +"amministratore" #: extensions/limits/app_settings.py:21 msgid "Maximum number of allowed domains for a new administrator" @@ -1530,11 +1574,13 @@ msgstr "Numero massimo di domini consentiti per un nuovo amministratore" #: extensions/limits/app_settings.py:27 msgid "Maximum number of allowed domain aliases for a new administrator" -msgstr "Numero massimo di alias di dominio consentiti per un nuovo amministratore" +msgstr "" +"Numero massimo di alias di dominio consentiti per un nuovo amministratore" #: extensions/limits/app_settings.py:33 msgid "Maximum number of allowed mailboxes for a new administrator" -msgstr "Numero massimo di caselle di posta consentite per un nuovo amministratore" +msgstr "" +"Numero massimo di caselle di posta consentite per un nuovo amministratore" #: extensions/limits/app_settings.py:39 msgid "Maximum number of allowed aliases for a new administrator" @@ -1542,7 +1588,8 @@ msgstr "Numero massimo di alias consentiti per un nuovo amministratore" #: extensions/limits/forms.py:13 msgid "Maximum number of domain administrators this user can create" -msgstr "Numero massimo di amministratori di dominio che questo utente può creare" +msgstr "" +"Numero massimo di amministratori di dominio che questo utente può creare" #: extensions/limits/forms.py:17 msgid "Maximum number of domains this user can create" @@ -1558,7 +1605,8 @@ msgstr "Numero massimo di caselle di posta che questo utente può creare" #: extensions/limits/forms.py:29 msgid "Maximum number of mailbox aliases this user can create" -msgstr "Numero massimo di alias di caselle di posta che questo utente può creare" +msgstr "" +"Numero massimo di alias di caselle di posta che questo utente può creare" #: extensions/limits/forms.py:46 msgid "Invalid limit" @@ -1612,7 +1660,9 @@ msgstr "Tempo massimo della risposta automatica" #: extensions/postfix_autoreply/app_settings.py:16 msgid "Timeout in seconds between two auto-replies to the same recipient" -msgstr "Tempo massimo in secondi tra due risposte automatiche al medesimo destinatario" +msgstr "" +"Tempo massimo in secondi tra due risposte automatiche al medesimo " +"destinatario" #: extensions/postfix_autoreply/forms.py:9 msgid "Until" @@ -1626,23 +1676,23 @@ msgstr "Attiva la tua risposta automatica fino a questa data" msgid "This date is over" msgstr "Questa data è passata" -#: extensions/postfix_autoreply/models.py:15 +#: extensions/postfix_autoreply/models.py:19 msgid "subject" msgstr "oggetto" -#: extensions/postfix_autoreply/models.py:16 +#: extensions/postfix_autoreply/models.py:20 msgid "The subject that will appear in sent emails" msgstr "L'oggetto che apparirà nelle mail inviate" -#: extensions/postfix_autoreply/models.py:17 +#: extensions/postfix_autoreply/models.py:23 msgid "content" msgstr "contenuto" -#: extensions/postfix_autoreply/models.py:18 +#: extensions/postfix_autoreply/models.py:24 msgid "The content that will appear in sent emails" msgstr "Il contenuto che apparirà nelle mail inviate" -#: extensions/postfix_autoreply/models.py:20 +#: extensions/postfix_autoreply/models.py:28 msgid "Activate/Deactivate your auto reply" msgstr "Attiva/disattiva la tua risposta automatica" @@ -1770,59 +1820,60 @@ msgstr "Ridireziona il messaggio a" #: extensions/sievefilters/lib.py:37 msgid "Connection to MANAGESIEVE server failed, check your configuration" -msgstr "Connessione al server MANAGESIEVE fallita, controlla la tua configurazione" +msgstr "" +"Connessione al server MANAGESIEVE fallita, controlla la tua configurazione" #: extensions/sievefilters/lib.py:82 msgid "Not enough space on server" msgstr "Non c'è spazio sufficiente nel server" -#: extensions/sievefilters/views.py:64 +#: extensions/sievefilters/views.py:68 msgid "Failed to retrieve filters set" msgstr "Recupero delle impostazioni di filtro fallito" -#: extensions/sievefilters/views.py:117 +#: extensions/sievefilters/views.py:126 #: extensions/sievefilters/templatetags/sfilters_tags.py:33 msgid "New filter" msgstr "Nuovo filtro" -#: extensions/sievefilters/views.py:124 +#: extensions/sievefilters/views.py:133 msgid "Filter created" msgstr "Filtro creato" -#: extensions/sievefilters/views.py:136 +#: extensions/sievefilters/views.py:145 #: extensions/sievefilters/templatetags/sfilters_tags.py:77 msgid "Edit filter" msgstr "Modifica filtro" -#: extensions/sievefilters/views.py:145 +#: extensions/sievefilters/views.py:154 msgid "Filter modified" msgstr "Filtro modificato" -#: extensions/sievefilters/views.py:165 +#: extensions/sievefilters/views.py:176 msgid "Filter removed" msgstr "Filtro eliminato" -#: extensions/sievefilters/views.py:166 +#: extensions/sievefilters/views.py:177 msgid "Failed to remove filter" msgstr "Rimozione del filtro fallita" -#: extensions/sievefilters/views.py:180 +#: extensions/sievefilters/views.py:191 msgid "Filters set saved" msgstr "Gruppo filtri salvato" -#: extensions/sievefilters/views.py:185 +#: extensions/sievefilters/views.py:196 msgid "Create a new filters set" msgstr "Crea un nuovo gruppo di filtri" -#: extensions/sievefilters/views.py:208 +#: extensions/sievefilters/views.py:219 msgid "Filters set created" msgstr "Gruppo filtri creato" -#: extensions/sievefilters/views.py:229 +#: extensions/sievefilters/views.py:240 msgid "Filters set deleted" msgstr "Gruppo filtri eliminato" -#: extensions/sievefilters/views.py:241 +#: extensions/sievefilters/views.py:252 msgid "Filters set activated" msgstr "Gruppo filtri attivato" @@ -1838,18 +1889,20 @@ msgstr "Attivo" msgid "No filter defined yet" msgstr "Non ci sono ancora filtri definiti" -#: extensions/sievefilters/templates/sievefilters/index.html:55 +#: extensions/sievefilters/templates/sievefilters/index.html:56 msgid "Filter sets" msgstr "Impostazioni dei filtri" -#: extensions/sievefilters/templates/sievefilters/index.html:59 +#: extensions/sievefilters/templates/sievefilters/index.html:60 msgid "active" msgstr "attivo" -#: extensions/sievefilters/templates/sievefilters/index.html:73 +#: extensions/sievefilters/templates/sievefilters/index.html:74 msgid "" "No filters set defined yet. You must create one in order to define filters." -msgstr "Non è ancora stato definito alcun gruppo di filtri. Devi crearne uno per definire dei filtri" +msgstr "" +"Non è ancora stato definito alcun gruppo di filtri. Devi crearne uno per " +"definire dei filtri" #: extensions/sievefilters/templatetags/sfilters_tags.py:16 msgid "New filters set" @@ -2031,7 +2084,8 @@ msgstr "Dimensione massima degli allegati" #: extensions/webmail/app_settings.py:16 msgid "Maximum attachment size in bytes (or KB, MB, GB if specified)" -msgstr "Dimensione massima degli allegati in bytes (o KB, MB, GB se specificato)" +msgstr "" +"Dimensione massima degli allegati in bytes (o KB, MB, GB se specificato)" #: extensions/webmail/app_settings.py:19 msgid "IMAP settings" @@ -2087,7 +2141,8 @@ msgstr "Modalità di visualizzazione del messaggio predefinita" #: extensions/webmail/app_settings.py:79 msgid "The default mode used when displaying a message" -msgstr "La modalità di visualizzazione predefinita quando viene mostrato il messaggio" +msgstr "" +"La modalità di visualizzazione predefinita quando viene mostrato il messaggio" #: extensions/webmail/app_settings.py:85 msgid "Enable HTML links display" @@ -2161,84 +2216,88 @@ msgstr "Risposta del server" msgid "Cci" msgstr "Cci" -#: extensions/webmail/imaputils.py:259 +#: extensions/webmail/imaputils.py:237 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Impossibile recuperare il delimitatore delle gerarchie" + +#: extensions/webmail/imaputils.py:278 #, python-format msgid "Connection to IMAP server failed: %s" msgstr "Connessione al server IMAP fallita: %s" -#: extensions/webmail/lib.py:353 +#: extensions/webmail/lib.py:360 msgid "wrote:" msgstr "scritto:" -#: extensions/webmail/lib.py:418 extensions/webmail/lib.py:422 +#: extensions/webmail/lib.py:425 extensions/webmail/lib.py:429 msgid "Original message" msgstr "Messaggio originale" -#: extensions/webmail/views.py:83 +#: extensions/webmail/views.py:78 #, python-format msgid "%(count)d message deleted" msgid_plural "%(count)d messages deleted" msgstr[0] "%(count)d messaggio cancellato" msgstr[1] "%(count)d messaggi cancellati" -#: extensions/webmail/views.py:100 +#: extensions/webmail/views.py:95 msgid "Unknown action" msgstr "Azione sconosciuta" -#: extensions/webmail/views.py:113 lib/email_listing.py:137 +#: extensions/webmail/views.py:108 lib/email_listing.py:137 msgid "Empty mailbox" msgstr "Casella di posta vuota" -#: extensions/webmail/views.py:130 +#: extensions/webmail/views.py:125 #: extensions/webmail/templatetags/webextras.py:170 msgid "Create a new mailbox" msgstr "Crea una nuova casella di posta" -#: extensions/webmail/views.py:145 +#: extensions/webmail/views.py:141 msgid "Mailbox created" msgstr "Casella di posta creata" -#: extensions/webmail/views.py:161 +#: extensions/webmail/views.py:157 msgid "Edit mailbox" msgstr "Modifica casella di posta" -#: extensions/webmail/views.py:175 +#: extensions/webmail/views.py:172 msgid "Mailbox updated" msgstr "Casella di posta aggiornata" -#: extensions/webmail/views.py:209 extensions/webmail/views.py:391 -#: extensions/webmail/views.py:500 +#: extensions/webmail/views.py:206 extensions/webmail/views.py:388 +#: extensions/webmail/views.py:497 msgid "Bad request" msgstr "Richiesta sbagliata" -#: extensions/webmail/views.py:241 +#: extensions/webmail/views.py:238 msgid "Failed to save attachment: " msgstr "Salvataggio allegato fallito: " -#: extensions/webmail/views.py:244 +#: extensions/webmail/views.py:241 #, python-format msgid "Attachment is too big (limit: %s)" msgstr "L'allegato è troppo grande (limite: %s)" -#: extensions/webmail/views.py:250 +#: extensions/webmail/views.py:247 #: extensions/webmail/templates/webmail/compose.html:15 #: extensions/webmail/templates/webmail/headers.html:4 msgid "Attachments" msgstr "Allegati" -#: extensions/webmail/views.py:266 +#: extensions/webmail/views.py:263 msgid "Bad query" msgstr "Domanda sbagliata" -#: extensions/webmail/views.py:276 +#: extensions/webmail/views.py:273 msgid "Failed to remove attachment: " msgstr "Rimozione allegato fallita: " -#: extensions/webmail/views.py:281 +#: extensions/webmail/views.py:278 msgid "Unknown attachment" msgstr "Allegato sconosciuto" -#: extensions/webmail/views.py:496 +#: extensions/webmail/views.py:493 msgid "Undefined action" msgstr "Azione non definita" @@ -2269,7 +2328,7 @@ msgstr[1] "%(counter)s file" msgid "Parent mailbox" msgstr "Casella di posta principale" -#: extensions/webmail/templates/webmail/index.html:37 +#: extensions/webmail/templates/webmail/index.html:38 msgid "Mailbox quota" msgstr "Quota casella di posta" @@ -2395,11 +2454,11 @@ msgstr "È intervenuto un errore interno" #: templates/nlayout.html:62 msgid "Powered by Modoboa" -msgstr "" +msgstr "Powered by Modoboa" #: templates/common/email_searchbar.html:5 msgid "From address" -msgstr "Indirizzo from" +msgstr "Mittente" #: templates/common/email_searchbar.html:10 msgid "Both" @@ -2447,7 +2506,7 @@ msgstr "Impostazioni per-utente" msgid "Prefered language" msgstr "Lingua preferita" -#: userprefs/app_settings.py:26 +#: userprefs/app_settings.py:27 msgid "Prefered language to display pages" msgstr "Lingua preferita per mostrare le pagine" @@ -2481,13 +2540,16 @@ msgstr "Mantieni copie locali" #: userprefs/forms.py:67 msgid "Forward messages and store copies into your local mailbox" -msgstr "Inoltra i messaggi e memorizza le copie nella tua casella di posta locale" +msgstr "" +"Inoltra i messaggi e memorizza le copie nella tua casella di posta locale" #: userprefs/forms.py:84 msgid "" "You can't define a forward to a local destination. Please ask your " "administrator to create an alias instead." -msgstr "Non puoi definire l'inoltro con destinazione una cartella locale. Chiedi piuttosto all'amministratore di creare un alias." +msgstr "" +"Non puoi definire l'inoltro con destinazione una cartella locale. Chiedi " +"piuttosto all'amministratore di creare un alias." #: userprefs/views.py:32 msgid "You need a mailbox in order to define a forward" diff --git a/modoboa/locale/it/LC_MESSAGES/djangojs.po b/modoboa/locale/it/LC_MESSAGES/djangojs.po index 17330283a..086b29052 100644 --- a/modoboa/locale/it/LC_MESSAGES/djangojs.po +++ b/modoboa/locale/it/LC_MESSAGES/djangojs.po @@ -6,9 +6,9 @@ msgid "" msgstr "" "Project-Id-Version: Modoboa\n" -"Report-Msgid-Bugs-To: http://dev.modoboa.org\n" -"POT-Creation-Date: 2013-06-28 16:37+0200\n" -"PO-Revision-Date: 2013-06-28 16:58+0200\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2013-09-26 23:44+0200\n" +"PO-Revision-Date: 2013-09-27 00:07+0200\n" "Last-Translator: Giancarlo Gaifas \n" "Language-Team: Italiano \n" "Language: it\n" @@ -33,11 +33,9 @@ msgstr "Questa operazione eliminerà TUTTI i dati associati a questo dominio." msgid "" "This operation will remove all data associated to this domain, excepting " "accounts." -msgstr "Questa operazione eliminerà tutti i dati associati a questo dominio tranne gli account" - -#: admin/static/admin/js/admin.js:117 -msgid "Delete this domain?" -msgstr "Elimino questo dominio?" +msgstr "" +"Questa operazione eliminerà tutti i dati associati a questo dominio tranne " +"gli account" #: admin/static/admin/js/admin.js:161 msgid "No domain administrator defined" @@ -47,22 +45,6 @@ msgstr "Non è stato definito un amministratore di dominio" msgid "Do not delete mailbox directory" msgstr "Non eliminare la cartella della casella posta" -#: admin/static/admin/js/admin.js:243 -msgid "Delete this account?" -msgstr "Eliminare questo account" - -#: admin/static/admin/js/admin.js:249 -msgid "Delete this list?" -msgstr "Eliminare questo elenco?" - -#: admin/static/admin/js/admin.js:254 -msgid "Delete this forward?" -msgstr "Eliminare questo inoltro?" - -#: admin/static/admin/js/admin.js:259 -msgid "Delete this alias?" -msgstr "Eliminare questo alias?" - #: extensions/amavis/static/amavis/js/quarantine.js:126 msgid "Release this selection?" msgstr "Rilasciare questa selezione?" @@ -114,11 +96,11 @@ msgstr "attivo" msgid "Search a domain" msgstr "Cerca un dominio" -#: extensions/webmail/static/webmail/js/webmail.js:475 +#: extensions/webmail/static/webmail/js/webmail.js:476 msgid "Remove the selected mailbox?" msgstr "Rimuovere la casella di posta selezionata?" -#: extensions/webmail/static/webmail/js/webmail.js:489 +#: extensions/webmail/static/webmail/js/webmail.js:490 msgid "Mailbox removed" msgstr "Casella di posta rimossa" @@ -144,15 +126,15 @@ msgid_plural "Moving %s messages" msgstr[0] "Spostato %s messaggio" msgstr[1] "Spostati %s messaggi" -#: static/js/confirmation.js:48 +#: static/js/confirmation.js:50 msgid "Warning" msgstr "Attenzione" -#: static/js/confirmation.js:57 +#: static/js/confirmation.js:59 msgid "Cancel" msgstr "Elimina" -#: static/js/confirmation.js:99 static/js/history.js:239 +#: static/js/confirmation.js:101 static/js/history.js:235 msgid "Internal Error" msgstr "Errore interno" diff --git a/modoboa/locale/pt_PT/LC_MESSAGES/django.po b/modoboa/locale/pt_PT/LC_MESSAGES/django.po index a5de42e61..d0e672cb5 100644 --- a/modoboa/locale/pt_PT/LC_MESSAGES/django.po +++ b/modoboa/locale/pt_PT/LC_MESSAGES/django.po @@ -3,6 +3,7 @@ # This file is distributed under the same license as the PACKAGE package. # # Translators: +# ddu17 , 2013 # mikec , 2012 # mikec , 2013 # Sandra Ribeiro , 2012-2013 @@ -11,9 +12,9 @@ msgid "" msgstr "" "Project-Id-Version: Modoboa\n" "Report-Msgid-Bugs-To: http://dev.modoboa.org/\n" -"POT-Creation-Date: 2013-06-01 16:35+0200\n" -"PO-Revision-Date: 2013-06-21 13:19+0000\n" -"Last-Translator: mikec \n" +"POT-Creation-Date: 2013-09-20 22:30+0200\n" +"PO-Revision-Date: 2013-10-04 21:10+0000\n" +"Last-Translator: ddu17 \n" "Language-Team: Portuguese (Portugal) (http://www.transifex.com/projects/p/modoboa/language/pt_PT/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -29,8 +30,8 @@ msgstr "Consola de Administração" msgid "Web based console to manage domains, accounts and aliases" msgstr "Consola baseada na web para gestão de domínios, contas e aliases" -#: admin/__init__.py:16 admin/forms.py:179 admin/forms.py:614 -#: admin/views.py:93 admin/views.py:441 +#: admin/__init__.py:16 admin/forms.py:211 admin/forms.py:652 +#: admin/views.py:94 admin/views.py:447 #: extensions/postfix_autoreply/app_settings.py:11 #: extensions/sievefilters/app_settings.py:50 #: extensions/stats/app_settings.py:11 extensions/webmail/app_settings.py:11 @@ -240,37 +241,45 @@ msgstr "Redirecionamento pré-definido" msgid "The default redirection used when no application is specified" msgstr "O redirecionamento padrão usado quando nenhuma aplicação se encontra especificada" -#: admin/app_settings.py:230 +#: admin/app_settings.py:213 +msgid "Key must be either 16, 24, or 32 bytes long" +msgstr "A chave deve ter tamanho de 16, 24, ou 32 bytes" + +#: admin/app_settings.py:222 +msgid "Invalid syntax" +msgstr "Sintax inválida" + +#: admin/app_settings.py:245 msgid "This field is required" msgstr "Este campo é obrigatório" -#: admin/forms.py:16 admin/forms.py:459 admin/tables.py:18 +#: admin/forms.py:20 admin/forms.py:497 admin/tables.py:18 msgid "Alias(es)" msgstr "Alias(es)" -#: admin/forms.py:18 +#: admin/forms.py:22 msgid "" "Alias(es) of this domain. Indicate only one name per input, press ENTER to " "add a new input." msgstr "Alias(es) deste domínio. Indicar apenas um nome por entrada, pressionar ENTER para acrescentar uma nova entrada." -#: admin/forms.py:55 +#: admin/forms.py:59 msgid "An alias with this name already exists" msgstr "Já existe um alias com este nome" -#: admin/forms.py:69 +#: admin/forms.py:73 msgid "A domain with this name already exists" msgstr "Este nome de domínio já existe" -#: admin/forms.py:114 +#: admin/forms.py:146 msgid "Create a domain administrator" msgstr "Criar um administrador de domínio" -#: admin/forms.py:116 +#: admin/forms.py:148 msgid "Automatically create an administrator for this domain" msgstr "Criar automaticamente um administrador para este domínio" -#: admin/forms.py:120 admin/tables.py:12 admin/tables.py:47 +#: admin/forms.py:152 admin/tables.py:12 admin/tables.py:47 #: admin/templates/admin/editdomainform.html:13 #: admin/templates/admin/identities_table.html:5 #: admin/templates/admin/information.html:8 @@ -279,349 +288,348 @@ msgstr "Criar automaticamente um administrador para este domínio" msgid "Name" msgstr "Nome" -#: admin/forms.py:122 +#: admin/forms.py:154 msgid "" "The administrator's name. Don't include the domain's name here, it will be " "automatically appended." msgstr "O nome do administrador. Não inclua o nome do domínio já que este será automaticamente anexado." -#: admin/forms.py:128 +#: admin/forms.py:160 msgid "Create aliases" msgstr " Criar aliases" -#: admin/forms.py:130 +#: admin/forms.py:162 msgid "Automatically create standard aliases for this domain" msgstr "Criar automaticamente aliases para este domínio" -#: admin/forms.py:143 +#: admin/forms.py:175 msgid "Invalid format" msgstr "Formato inválido" -#: admin/forms.py:155 +#: admin/forms.py:187 #, python-format msgid "User '%s' already exists" msgstr "O utilizador '%s' já existe" -#: admin/forms.py:203 +#: admin/forms.py:235 msgid "Email address" msgstr "Endereço de e-mail" -#: admin/forms.py:204 +#: admin/forms.py:236 msgid "" "The distribution list address. Use the '*' character to create a 'catchall' " "address (ex: *@domain.tld)." msgstr "Os endereços da lista de distribuição. Use o '*' para criar um endereço 'catchall' (Ex: *@domain.tld)." -#: admin/forms.py:207 +#: admin/forms.py:239 msgid "Recipients" msgstr "Destinatários" -#: admin/forms.py:208 +#: admin/forms.py:240 msgid "" "Mailbox(es) this alias will point to. Indicate only one address per input, " "press ENTER to add a new input." msgstr "Caixa(s) de correio para o qual este 'alias' aponta. Indicar apenas um endereço por entrada, pressionar ENTER para acrescentar uma nova entrada." -#: admin/forms.py:246 admin/forms.py:504 admin/forms.py:530 -#: admin/models.py:1147 +#: admin/forms.py:278 admin/forms.py:542 admin/forms.py:568 msgid "Domain does not exist" msgstr "O domínio não existe" -#: admin/forms.py:248 +#: admin/forms.py:280 msgid "You don't have access to this domain" msgstr "Não tem acesso a este domínio" -#: admin/forms.py:268 +#: admin/forms.py:300 msgid "Invalid mailbox" msgstr "Caixa de correio inválida" -#: admin/forms.py:284 +#: admin/forms.py:316 admin/models.py:1202 #, python-format msgid "Local recipient %s not found" msgstr "O destinatário local %s não foi encontrado" -#: admin/forms.py:286 admin/forms.py:292 +#: admin/forms.py:318 admin/forms.py:324 #, python-format msgid "Recipient %s already present" msgstr "O destinatário %s já existe" -#: admin/forms.py:297 +#: admin/forms.py:329 msgid "No recipient defined" msgstr "Sem destinatário definido" -#: admin/forms.py:311 extensions/webmail/forms.py:19 +#: admin/forms.py:343 extensions/webmail/forms.py:19 msgid "Select a file" msgstr "Escolha um ficheiro" -#: admin/forms.py:313 admin/forms.py:341 +#: admin/forms.py:345 admin/forms.py:373 msgid "Separator" msgstr "Separador" -#: admin/forms.py:316 +#: admin/forms.py:348 msgid "Continue on error" msgstr "Continua com erro" -#: admin/forms.py:317 +#: admin/forms.py:349 msgid "Don't treat duplicated objects as error" msgstr "Não tratar objetos duplicados como erro" -#: admin/forms.py:332 +#: admin/forms.py:364 msgid "Crypt passwords" msgstr "Passwords encriptadas" -#: admin/forms.py:333 +#: admin/forms.py:365 msgid "Check this option if passwords contained in your file are not crypted" msgstr "Marque esta opção se as senhas contidas no arquivo não são encriptadas" -#: admin/forms.py:339 +#: admin/forms.py:371 msgid "File name" msgstr "Nome do ficheiro" -#: admin/forms.py:371 admin/templates/admin/editdomainform.html:13 +#: admin/forms.py:403 admin/templates/admin/editdomainform.html:13 #: auth/forms.py:7 msgid "Username" msgstr "Nome de Utilizador" -#: admin/forms.py:373 +#: admin/forms.py:405 msgid "Role" msgstr "Papel" -#: admin/forms.py:374 admin/forms.py:401 +#: admin/forms.py:406 admin/forms.py:433 msgid "Choose" msgstr "Escolher" -#: admin/forms.py:375 +#: admin/forms.py:407 msgid "What level of permission this user will have" msgstr "Que nível de permissões terá o utilizador" -#: admin/forms.py:377 auth/forms.py:11 +#: admin/forms.py:409 auth/forms.py:11 msgid "Password" msgstr "Password" -#: admin/forms.py:379 userprefs/forms.py:18 +#: admin/forms.py:411 userprefs/forms.py:18 msgid "Confirmation" msgstr "Confirmação" -#: admin/forms.py:381 +#: admin/forms.py:413 msgid "Enter the same password as above, for verification." msgstr "Repita a password acima, para verificação." -#: admin/forms.py:392 +#: admin/forms.py:424 msgid "Enabled" msgstr "Ativo" -#: admin/forms.py:434 +#: admin/forms.py:471 msgid "The two password fields didn't match." msgstr "Os dois campos de password não são idênticos." -#: admin/forms.py:440 +#: admin/forms.py:477 msgid "You can't disable your own account" msgstr "Não pode desativar a sua própria conta" -#: admin/forms.py:450 +#: admin/forms.py:488 msgid "E-mail" msgstr "E-mail" -#: admin/forms.py:452 +#: admin/forms.py:490 msgid "Quota" msgstr "Quota" -#: admin/forms.py:454 +#: admin/forms.py:492 msgid "" "Quota in MB for this mailbox. Define a custom value or use domain's default " "one. Leave empty to define an unlimited value (not allowed for domain " "administrators)." msgstr "Quota em MB para esta caixa de correio. Definir uma valor personalizado ou usar o valor predefinido pelo domínio. Deixar em branco para definir uma valor ilimitado (não permitido para administradores do domínio)." -#: admin/forms.py:461 +#: admin/forms.py:499 msgid "" "Alias(es) of this mailbox. Indicate only one address per input, press ENTER " "to add a new input. Use the '*' character to create a 'catchall' alias (ex: " "*@domain.tld)." msgstr "Alias(es) desta caixa de correio. Indique apenas um endereço por entrada, clique ENTER para adicionar uma nova entrada. Use o '*' para criar um alias 'catchall' (ex: *@domain.tld)." -#: admin/forms.py:512 +#: admin/forms.py:550 #, python-format msgid "Mailbox %s already exists" msgstr "Caixa de correio %s já existe" -#: admin/forms.py:572 +#: admin/forms.py:610 msgid "Domain(s)" msgstr "Domínio(s)" -#: admin/forms.py:574 +#: admin/forms.py:612 msgid "Domain(s) that user administrates" msgstr "Domínio(s) administrado pelo utilizador" -#: admin/forms.py:616 admin/views.py:444 +#: admin/forms.py:654 admin/views.py:450 msgid "Mail" msgstr "Correio" -#: admin/forms.py:618 +#: admin/forms.py:656 msgid "Permissions" msgstr "Permissões" -#: admin/models.py:78 +#: admin/models.py:80 msgid "You can't delete your own account" msgstr "Não pode apagar a sua própria conta" -#: admin/models.py:159 +#: admin/models.py:161 msgid "Failed to update password: LDAP module not installed" msgstr "Falha ao actualizar password: Modulo LDAP não instalado" -#: admin/models.py:165 +#: admin/models.py:167 #, python-format msgid "Failed to update password: %s" msgstr "Falha ao actualizar a password: %s" -#: admin/models.py:192 +#: admin/models.py:194 msgid "account" msgstr "conta" -#: admin/models.py:494 admin/models.py:720 admin/models.py:782 +#: admin/models.py:496 admin/models.py:733 admin/models.py:795 msgid "Invalid line" msgstr "Linha inválida" -#: admin/models.py:498 +#: admin/models.py:500 msgid "You can't import an account with a role greater than yours" msgstr "Não é possível importa uma conta com permissões superiores" -#: admin/models.py:518 +#: admin/models.py:506 +#, python-format +msgid "The simple user '%s' must have a valid email address" +msgstr "O simple utilizador '%s' deve ter endereço de e-mail válido" + +#: admin/models.py:510 +#, python-format +msgid "username and email fields must not differ for '%s'" +msgstr "Os campos nome de utilizador e endereço de email não deve ser diferente para '%s'" + +#: admin/models.py:534 #, python-format msgid "Account import failed (%s): domain does not exist" msgstr "Importação da conta falhou (%s): domínio não existe" -#: admin/models.py:607 admin/models.py:733 +#: admin/models.py:624 admin/models.py:746 msgid "name" msgstr "nome" -#: admin/models.py:608 +#: admin/models.py:625 msgid "The domain name" msgstr "O nome do domínio" -#: admin/models.py:610 +#: admin/models.py:627 msgid "Default quota in MB applied to mailboxes" msgstr "Quota em MB aplicada por defeito às caixas de correio" -#: admin/models.py:613 admin/models.py:740 admin/models.py:1020 -#: admin/models.py:1181 extensions/postfix_autoreply/models.py:19 +#: admin/models.py:630 admin/models.py:753 admin/models.py:1045 +#: admin/models.py:1217 extensions/postfix_autoreply/models.py:27 msgid "enabled" msgstr "ativo" -#: admin/models.py:614 +#: admin/models.py:631 msgid "Check to activate this domain" msgstr "Marcar para ativar este domínio" -#: admin/models.py:734 +#: admin/models.py:747 msgid "The alias name" msgstr "O nome do 'alias'" -#: admin/models.py:736 +#: admin/models.py:749 msgid "target" msgstr "alvo" -#: admin/models.py:737 +#: admin/models.py:750 msgid "The domain this alias points to" msgstr "O domínio para o qual este alias irá apontar" -#: admin/models.py:741 admin/models.py:1021 +#: admin/models.py:754 admin/models.py:1046 msgid "Check to activate this alias" msgstr "Marcar para ativar este alias" -#: admin/models.py:788 +#: admin/models.py:801 #, python-format msgid "Unknown domain %s" msgstr "Domínio desconhecido: %s" -#: admin/models.py:805 admin/models.py:1006 +#: admin/models.py:818 admin/models.py:1031 msgid "address" msgstr "endereços" -#: admin/models.py:806 +#: admin/models.py:819 msgid "Mailbox address (without the @domain.tld part)" msgstr "Endereços das caixas de correio (sem a parte @domain.tld)" -#: admin/models.py:852 +#: admin/models.py:877 #, python-format msgid "Failed to retrieve mailbox location (%s)" msgstr "Falha ao recuperar a localização da caixa de correio (%s)" -#: admin/models.py:868 -#, python-format -msgid "Failed to rename mailbox: %s" -msgstr "Falha ao renomear a caixa de correio: %s" - -#: admin/models.py:894 -#, python-format -msgid "Failed to remove mailbox: %s" -msgstr "Falha ao remover a caixa de correio:%s" - -#: admin/models.py:909 +#: admin/models.py:921 #, python-format msgid "Quota is greater than the allowed domain's limit (%dM)" msgstr "A quota excede o limite permitido pelo domínio (%dM)" -#: admin/models.py:914 +#: admin/models.py:926 msgid "A quota is required" msgstr "É necessário uma quota" -#: admin/models.py:1007 +#: admin/models.py:1032 msgid "" "The alias address (without the domain part). For a 'catch-all' address, just" " enter an * character." msgstr "O endereço do alias (sem a parte do domínio). Para um endereço 'cath-all', digite apenas o caractere *" -#: admin/models.py:1011 +#: admin/models.py:1036 msgid "mailboxes" msgstr "caixas de correio" -#: admin/models.py:1012 +#: admin/models.py:1037 msgid "The mailboxes this alias points to" msgstr "As caixas de correio para onde este alias irá apontar" -#: admin/models.py:1016 +#: admin/models.py:1041 msgid "The aliases this alias points to" msgstr "Este 'aliases' aponta para" -#: admin/models.py:1064 +#: admin/models.py:1089 msgid "distribution list" msgstr "lista de distribuição" -#: admin/models.py:1065 +#: admin/models.py:1090 msgid "forward" msgstr "reencaminhar" -#: admin/models.py:1066 +#: admin/models.py:1091 msgid "alias" msgstr "alias" -#: admin/models.py:1142 +#: admin/models.py:1170 #, python-format msgid "Invalid line: %s" msgstr "Linha inválida: %s" -#: admin/models.py:1167 +#: admin/models.py:1175 #, python-format -msgid "Mailbox %s does not exist" -msgstr "Caixa de correio %s não existe" +msgid "Domain '%s' does not exist" +msgstr "O domínio '%s' não existe" -#: admin/models.py:1182 +#: admin/models.py:1218 msgid "Check to enable this extension" msgstr "Marque para ativar esta extensão" -#: admin/models.py:1282 +#: admin/models.py:1318 msgid "added" msgstr "acrescentado" -#: admin/models.py:1285 +#: admin/models.py:1321 msgid "modified" msgstr "modificado" -#: admin/models.py:1288 +#: admin/models.py:1324 msgid "deleted" msgstr "apagado" -#: admin/models.py:1290 +#: admin/models.py:1326 #, python-format msgid "%(object)s '%(name)s' %(action)s by user %(user)s" msgstr "%(object)s '%(name)s' %(action)s pelo utilizador %(user)s" @@ -631,7 +639,7 @@ msgid "Edit domain" msgstr "Editar domínio" #: admin/tables.py:21 admin/templates/admin/identities_table.html:8 -#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:79 +#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:83 #: extensions/sievefilters/templates/sievefilters/filter.html:17 #: extensions/sievefilters/templates/sievefilters/guieditor.html:5 #: extensions/webmail/templatetags/webextras.py:97 @@ -646,143 +654,143 @@ msgstr "Versão" msgid "Description" msgstr "Descrição" -#: admin/views.py:87 +#: admin/views.py:88 msgid "New domain" msgstr "Novo domínio" -#: admin/views.py:88 admin/views.py:97 admin/views.py:225 admin/views.py:438 -#: admin/views.py:445 extensions/sievefilters/views.py:120 -#: extensions/sievefilters/views.py:187 extensions/webmail/views.py:131 +#: admin/views.py:89 admin/views.py:98 admin/views.py:228 admin/views.py:444 +#: admin/views.py:451 extensions/sievefilters/views.py:129 +#: extensions/sievefilters/views.py:198 extensions/webmail/views.py:136 msgid "Create" msgstr "Criar" -#: admin/views.py:94 admin/views.py:442 +#: admin/views.py:95 admin/views.py:448 msgid "Next" msgstr "Seguinte" -#: admin/views.py:96 +#: admin/views.py:97 msgid "Options" msgstr "Opções" -#: admin/views.py:98 admin/views.py:446 +#: admin/views.py:99 admin/views.py:452 msgid "Previous" msgstr "Anterior" -#: admin/views.py:111 +#: admin/views.py:112 msgid "Domain created" msgstr "Domínio criado" -#: admin/views.py:144 admin/views.py:282 admin/views.py:492 +#: admin/views.py:146 admin/views.py:285 admin/views.py:498 #: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:9 -#: extensions/sievefilters/views.py:139 extensions/webmail/views.py:162 +#: extensions/sievefilters/views.py:148 extensions/webmail/views.py:168 #: userprefs/templates/userprefs/forward.html:8 #: userprefs/templates/userprefs/profile.html:8 msgid "Update" msgstr "Actualizar" -#: admin/views.py:160 +#: admin/views.py:162 msgid "Domain modified" msgstr "Domínio alterado" -#: admin/views.py:186 +#: admin/views.py:189 msgid "You can't delete your own domain" msgstr "Não é possível eliminar o seu próprio domínio" -#: admin/views.py:189 +#: admin/views.py:192 msgid "Domain deleted" msgid_plural "Domains deleted" msgstr[0] "Domínio criado" msgstr[1] "Domínios eliminados" -#: admin/views.py:204 +#: admin/views.py:207 msgid "Alias with this name already exists" msgstr "Já existe um 'alias' com este nome" -#: admin/views.py:247 +#: admin/views.py:250 msgid "New distribution list" msgstr "Nova lista de distribuição" -#: admin/views.py:248 +#: admin/views.py:251 msgid "Distribution list created" msgstr "A lista de distribuição foi criada" -#: admin/views.py:257 +#: admin/views.py:260 msgid "New alias" msgstr "Novo 'alias'" -#: admin/views.py:258 +#: admin/views.py:261 msgid "Alias created" msgstr "Alias criado" -#: admin/views.py:267 +#: admin/views.py:270 msgid "New forward" msgstr "Novo reencaminhamento" -#: admin/views.py:268 +#: admin/views.py:271 msgid "Forward created" msgstr "Reencaminhamento criado" -#: admin/views.py:286 +#: admin/views.py:289 msgid "Distribution list modified" msgstr "Lista de distribuição modificada" -#: admin/views.py:288 +#: admin/views.py:291 msgid "Forward modified" msgstr "Reencaminhamento modificado" -#: admin/views.py:290 +#: admin/views.py:293 msgid "Alias modified" msgstr "'Alias' modificado" -#: admin/views.py:403 admin/views.py:541 admin/views.py:546 -#: extensions/amavis/views.py:195 extensions/amavis/views.py:201 -#: extensions/amavis/views.py:234 extensions/amavis/views.py:238 -#: extensions/amavis/views.py:240 extensions/webmail/views.py:36 -#: extensions/webmail/views.py:64 extensions/webmail/views.py:77 -#: extensions/webmail/views.py:93 extensions/webmail/views.py:109 -#: extensions/webmail/views.py:193 extensions/webmail/views.py:419 -#: extensions/webmail/views.py:430 extensions/webmail/views.py:457 +#: admin/views.py:402 admin/views.py:545 admin/views.py:550 +#: extensions/amavis/views.py:204 extensions/amavis/views.py:210 +#: extensions/amavis/views.py:243 extensions/amavis/views.py:247 +#: extensions/amavis/views.py:249 extensions/webmail/views.py:41 +#: extensions/webmail/views.py:68 extensions/webmail/views.py:81 +#: extensions/webmail/views.py:98 extensions/webmail/views.py:114 +#: extensions/webmail/views.py:200 extensions/webmail/views.py:427 +#: extensions/webmail/views.py:438 extensions/webmail/views.py:465 #: lib/formutils.py:32 lib/formutils.py:37 msgid "Invalid request" msgstr "Pedido inválido" -#: admin/views.py:435 +#: admin/views.py:441 msgid "New account" msgstr "Nova conta" -#: admin/views.py:459 +#: admin/views.py:465 msgid "Account created" msgstr "Conta criada" -#: admin/views.py:507 +#: admin/views.py:513 msgid "Account updated" msgstr "Conta actualizada" -#: admin/views.py:531 +#: admin/views.py:535 msgid "Account deleted" msgid_plural "Accounts deleted" msgstr[0] "Conta eliminada" msgstr[1] "Contas eliminadas" -#: admin/views.py:586 +#: admin/views.py:590 msgid "Parameters saved" msgstr "Parâmetros salvos" -#: admin/views.py:632 +#: admin/views.py:636 msgid "Modifications applied." msgstr "Alterações efectuadas." -#: admin/views.py:740 +#: admin/views.py:743 #, python-format msgid "Object already exists: %s" msgstr "Este objeto já existe: %s" -#: admin/views.py:742 +#: admin/views.py:745 #, python-format msgid "%d objects imported successfully" msgstr "%d objetos importados com sucesso" -#: admin/views.py:760 +#: admin/views.py:763 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
                      \n" @@ -796,25 +804,25 @@ msgid "" "

                      You can use a different character as separator.

                      \n" msgstr "Forneça um ficheiro CSV cujas linhas respeitem um dos seguintes formatos:⏎\n
                        ⏎\n
                      • account; loginname; password; first name; last name; enabled; group; address[, domain, ...]
                      • ⏎\n
                      • alias; address; enabled; internal recipient
                      • ⏎\n
                      • forward; address; enabled; external recipient
                      • ⏎\n
                      • dlist; address; enabled; recipient; recipient; ...
                      • ⏎\n
                      ⏎\n

                      O primeiro elemento de cada linha é obrigatório e deve ser igual a um dos valores anteriores..

                      ⏎\n⏎\n

                      Pode utilizar um caractere diferente como separador.

                      ⏎\n" -#: admin/views.py:772 +#: admin/views.py:775 msgid "Import identities" msgstr "Importar identidades" -#: admin/views.py:773 admin/views.py:843 admin/templatetags/admin_extras.py:87 +#: admin/views.py:776 admin/views.py:846 admin/templatetags/admin_extras.py:87 #: admin/templatetags/admin_extras.py:144 msgid "Import" msgstr "Importar" -#: admin/views.py:803 +#: admin/views.py:806 msgid "Export identities" msgstr "Exportar identidades" -#: admin/views.py:804 admin/views.py:860 admin/templatetags/admin_extras.py:93 +#: admin/views.py:807 admin/views.py:863 admin/templatetags/admin_extras.py:93 #: admin/templatetags/admin_extras.py:150 msgid "Export" msgstr "Exportar" -#: admin/views.py:832 +#: admin/views.py:835 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
                        \n" @@ -825,11 +833,11 @@ msgid "" "

                        You can use a different character as separator.

                        \n" msgstr "Forneça um ficheiro CSV cujas linhas respeitem um dos seguintes formatos:⏎
                          ⏎
                        • domain; name; quota; enabled
                        • ⏎
                        • domainalias; name; targeted domain; enabled
                        • ⏎
                        ⏎

                        O primeiro elemento de cada linha é obrigatório e deve ser igual a um dos valores anteriores.

                        ⏎

                        Pode utilizar um caractere diferente como separador.

                        ⏎\n" -#: admin/views.py:842 +#: admin/views.py:845 msgid "Import domains" msgstr "Importar domínios" -#: admin/views.py:859 +#: admin/views.py:862 msgid "Export domains" msgstr "Exportar domínios" @@ -902,7 +910,7 @@ msgid "Tags" msgstr "Tags" #: admin/templates/admin/importform.html:5 -#: admin/templatetags/admin_extras.py:305 +#: admin/templatetags/admin_extras.py:290 msgid "Help" msgstr "Ajuda" @@ -949,7 +957,7 @@ msgid "Previously on Modoboa..." msgstr "Anteriormente no Modoboa..." #: admin/templates/admin/logs.html:6 extensions/amavis/sql_listing.py:25 -#: extensions/webmail/lib.py:37 extensions/webmail/lib.py:406 +#: extensions/webmail/lib.py:38 extensions/webmail/lib.py:411 msgid "Date" msgstr "Data" @@ -1052,34 +1060,21 @@ msgid "View the domain's identities" msgstr "Visualizar as identidades do domínio" #: admin/templatetags/admin_extras.py:178 -msgid "Delete the domain" -msgstr "Apagar o domínio" - #: admin/templatetags/admin_extras.py:194 -msgid "Delete this account" -msgstr "Apagar esta conta" +#: admin/templatetags/admin_extras.py:201 +#, python-format +msgid "Delete %s?" +msgstr "Eliminar %s?" -#: admin/templatetags/admin_extras.py:202 -msgid "Delete this distribution list" -msgstr "Apagar esta lista de distribuição" - -#: admin/templatetags/admin_extras.py:209 -msgid "Delete this forward" -msgstr "Apagar este reencaminhamento" - -#: admin/templatetags/admin_extras.py:216 -msgid "Delete this alias" -msgstr "Apagar este 'alias'" - -#: admin/templatetags/admin_extras.py:238 +#: admin/templatetags/admin_extras.py:223 msgid "Remove this permission" msgstr "Remover esta permissão" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 msgid "Yes" msgstr "Sim" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 msgid "No" msgstr "Não" @@ -1088,11 +1083,11 @@ msgstr "Não" msgid "User '%s' successfully logged in" msgstr "O utilizador '%s' fez login com sucesso" -#: auth/views.py:41 +#: auth/views.py:44 msgid "Your username and password didn't match. Please try again." msgstr "O seu nome de utilizador" -#: auth/views.py:64 +#: auth/views.py:67 #, python-format msgid "User '%s' logged out" msgstr "O utilizador '%s' fez logout" @@ -1136,16 +1131,16 @@ msgstr "Interface de gestão básica do amavis" msgid "Quarantine" msgstr "Quarentena" -#: extensions/amavis/__init__.py:127 +#: extensions/amavis/__init__.py:129 msgid "pending requests" msgstr "pedidos pendentes" -#: extensions/amavis/__init__.py:144 +#: extensions/amavis/__init__.py:149 #, python-format msgid "%d pending requests" msgstr "%d pedidos pendentes" -#: extensions/amavis/__init__.py:156 +#: extensions/amavis/__init__.py:161 msgid "Content filter" msgstr "Filtro de conteúdo" @@ -1260,12 +1255,12 @@ msgid "Virus filter" msgstr "Filtro de vírus" #: extensions/amavis/models.py:94 extensions/amavis/models.py:102 -#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:268 +#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:281 msgid "yes" msgstr "sim" #: extensions/amavis/models.py:95 extensions/amavis/models.py:103 -#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:272 +#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:285 msgid "no" msgstr "não" @@ -1305,13 +1300,13 @@ msgid "" " settings." msgstr "Modifique o assunto de spam usando o texto especificado. Escolha 'default' para usar as configurações globais." -#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:36 +#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:37 msgid "From" msgstr "De" #: extensions/amavis/sql_listing.py:24 extensions/sievefilters/forms.py:68 -#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:35 -#: extensions/webmail/lib.py:404 templates/common/email_searchbar.html:6 +#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:36 +#: extensions/webmail/lib.py:409 templates/common/email_searchbar.html:6 msgid "Subject" msgstr "Assunto" @@ -1321,37 +1316,37 @@ msgstr "Assunto" msgid "To" msgstr "Para" -#: extensions/amavis/views.py:34 +#: extensions/amavis/views.py:43 msgid "Empty quarantine" msgstr "Quarentena vazia" -#: extensions/amavis/views.py:202 +#: extensions/amavis/views.py:211 msgid "Message deleted" msgstr "Mensagem apagada" -#: extensions/amavis/views.py:223 +#: extensions/amavis/views.py:232 #, python-format msgid "%(count)d message deleted successfully" msgid_plural "%(count)d messages deleted successfully" msgstr[0] "%(count)d mensagem eliminada com sucesso" msgstr[1] "%(count)d mensagens eliminadas com sucesso" -#: extensions/amavis/views.py:243 +#: extensions/amavis/views.py:252 msgid "Request sent" msgstr "Pedido enviado" -#: extensions/amavis/views.py:249 +#: extensions/amavis/views.py:258 msgid "Message released" msgstr "Mensagem libertada" -#: extensions/amavis/views.py:271 +#: extensions/amavis/views.py:280 #, python-format msgid "%(count)d request sent" msgid_plural "%(count)d requests sent" msgstr[0] "%(count)d pedido enviado" msgstr[1] "%(count)d pedidos enviados" -#: extensions/amavis/views.py:295 +#: extensions/amavis/views.py:304 #, python-format msgid "%(count)d message released successfully" msgid_plural "%(count)d messages released successfully" @@ -1472,7 +1467,7 @@ msgstr "Limites" msgid "Reseller" msgstr "Revendedor" -#: extensions/limits/__init__.py:96 +#: extensions/limits/__init__.py:94 msgid "Resources" msgstr "Recursos" @@ -1592,23 +1587,23 @@ msgstr "Ativar o seu aviso de ausência até esta data" msgid "This date is over" msgstr "Esta data ultrapassa" -#: extensions/postfix_autoreply/models.py:15 +#: extensions/postfix_autoreply/models.py:19 msgid "subject" msgstr "assunto" -#: extensions/postfix_autoreply/models.py:16 +#: extensions/postfix_autoreply/models.py:20 msgid "The subject that will appear in sent emails" msgstr "O assunto que irá constar nos e-mails enviados" -#: extensions/postfix_autoreply/models.py:17 +#: extensions/postfix_autoreply/models.py:23 msgid "content" msgstr "conteúdo" -#: extensions/postfix_autoreply/models.py:18 +#: extensions/postfix_autoreply/models.py:24 msgid "The content that will appear in sent emails" msgstr "O conteúdo que irá constar nos e-mails enviados" -#: extensions/postfix_autoreply/models.py:20 +#: extensions/postfix_autoreply/models.py:28 msgid "Activate/Deactivate your auto reply" msgstr "Ativar/desativar repostas automáticas" @@ -1742,53 +1737,53 @@ msgstr "Ligação ao servidor MANAGESIEVE falhou, valide a configuração" msgid "Not enough space on server" msgstr "Espaço insuficiente no servidor" -#: extensions/sievefilters/views.py:64 +#: extensions/sievefilters/views.py:68 msgid "Failed to retrieve filters set" msgstr "Falha ao recuperar os filtros definidos" -#: extensions/sievefilters/views.py:117 +#: extensions/sievefilters/views.py:126 #: extensions/sievefilters/templatetags/sfilters_tags.py:33 msgid "New filter" msgstr "Novo filtro" -#: extensions/sievefilters/views.py:124 +#: extensions/sievefilters/views.py:133 msgid "Filter created" msgstr "Filtro criado" -#: extensions/sievefilters/views.py:136 +#: extensions/sievefilters/views.py:145 #: extensions/sievefilters/templatetags/sfilters_tags.py:77 msgid "Edit filter" msgstr "Editar filtro" -#: extensions/sievefilters/views.py:145 +#: extensions/sievefilters/views.py:154 msgid "Filter modified" msgstr "Filtro modificado" -#: extensions/sievefilters/views.py:165 +#: extensions/sievefilters/views.py:176 msgid "Filter removed" msgstr "Filtro removido" -#: extensions/sievefilters/views.py:166 +#: extensions/sievefilters/views.py:177 msgid "Failed to remove filter" msgstr "Falha ao remover filtro" -#: extensions/sievefilters/views.py:180 +#: extensions/sievefilters/views.py:191 msgid "Filters set saved" msgstr "Grupo de filtros guardado" -#: extensions/sievefilters/views.py:185 +#: extensions/sievefilters/views.py:196 msgid "Create a new filters set" msgstr "Criar novo grupo de filtros" -#: extensions/sievefilters/views.py:208 +#: extensions/sievefilters/views.py:219 msgid "Filters set created" msgstr "Grupo de filtros criado" -#: extensions/sievefilters/views.py:229 +#: extensions/sievefilters/views.py:240 msgid "Filters set deleted" msgstr "Grupo de filtros eliminado" -#: extensions/sievefilters/views.py:241 +#: extensions/sievefilters/views.py:252 msgid "Filters set activated" msgstr "Grupo de filtros ativado" @@ -1804,15 +1799,15 @@ msgstr "Ativo" msgid "No filter defined yet" msgstr "Ainda não existe nenhum filtro definido" -#: extensions/sievefilters/templates/sievefilters/index.html:55 +#: extensions/sievefilters/templates/sievefilters/index.html:56 msgid "Filter sets" msgstr "Grupos de filtros" -#: extensions/sievefilters/templates/sievefilters/index.html:59 +#: extensions/sievefilters/templates/sievefilters/index.html:60 msgid "active" msgstr "ativo" -#: extensions/sievefilters/templates/sievefilters/index.html:73 +#: extensions/sievefilters/templates/sievefilters/index.html:74 msgid "" "No filters set defined yet. You must create one in order to define filters." msgstr "Não existem grupos de filtros. Terá de criar um para poder definir filtros." @@ -2127,84 +2122,88 @@ msgstr "Resposta do servidor" msgid "Cci" msgstr "Cci" -#: extensions/webmail/imaputils.py:259 +#: extensions/webmail/imaputils.py:237 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Falha ao recuperar hierarquia delimitador" + +#: extensions/webmail/imaputils.py:278 #, python-format msgid "Connection to IMAP server failed: %s" msgstr "A ligação ao servidor IMAP falhou: %s" -#: extensions/webmail/lib.py:353 +#: extensions/webmail/lib.py:358 msgid "wrote:" msgstr "escreveu:" -#: extensions/webmail/lib.py:418 extensions/webmail/lib.py:422 +#: extensions/webmail/lib.py:423 extensions/webmail/lib.py:427 msgid "Original message" msgstr "Mensagem original" -#: extensions/webmail/views.py:81 +#: extensions/webmail/views.py:86 #, python-format msgid "%(count)d message deleted" msgid_plural "%(count)d messages deleted" msgstr[0] "%(count)d mensagens eliminadas" msgstr[1] "%(count)d mensagens eliminadas" -#: extensions/webmail/views.py:98 +#: extensions/webmail/views.py:103 msgid "Unknown action" msgstr "Acção desconhecida" -#: extensions/webmail/views.py:111 lib/email_listing.py:137 +#: extensions/webmail/views.py:116 lib/email_listing.py:137 msgid "Empty mailbox" msgstr "Caixa de correio vazia" -#: extensions/webmail/views.py:128 -#: extensions/webmail/templatetags/webextras.py:169 +#: extensions/webmail/views.py:133 +#: extensions/webmail/templatetags/webextras.py:170 msgid "Create a new mailbox" msgstr "Criar nova caixa de correio" -#: extensions/webmail/views.py:143 +#: extensions/webmail/views.py:149 msgid "Mailbox created" msgstr "Caixa de correio criada" -#: extensions/webmail/views.py:159 +#: extensions/webmail/views.py:165 msgid "Edit mailbox" msgstr "Editar caixa de correio" -#: extensions/webmail/views.py:173 +#: extensions/webmail/views.py:180 msgid "Mailbox updated" msgstr "Caixa de correio actualizada" -#: extensions/webmail/views.py:207 extensions/webmail/views.py:388 -#: extensions/webmail/views.py:497 +#: extensions/webmail/views.py:214 extensions/webmail/views.py:396 +#: extensions/webmail/views.py:505 msgid "Bad request" msgstr "Pedido inválido" -#: extensions/webmail/views.py:238 +#: extensions/webmail/views.py:246 msgid "Failed to save attachment: " msgstr "Falha ao salvar o anexo:" -#: extensions/webmail/views.py:241 +#: extensions/webmail/views.py:249 #, python-format msgid "Attachment is too big (limit: %s)" msgstr "Anexo demasiado grande (limite %s)" -#: extensions/webmail/views.py:247 +#: extensions/webmail/views.py:255 #: extensions/webmail/templates/webmail/compose.html:15 #: extensions/webmail/templates/webmail/headers.html:4 msgid "Attachments" msgstr "Anexos" -#: extensions/webmail/views.py:263 +#: extensions/webmail/views.py:271 msgid "Bad query" msgstr "Pesquisa inválida" -#: extensions/webmail/views.py:273 +#: extensions/webmail/views.py:281 msgid "Failed to remove attachment: " msgstr "Falha ao remover os anexos:" -#: extensions/webmail/views.py:278 +#: extensions/webmail/views.py:286 msgid "Unknown attachment" msgstr "Anexo desconhecido" -#: extensions/webmail/views.py:493 +#: extensions/webmail/views.py:501 msgid "Undefined action" msgstr "Ação indefinida" @@ -2235,7 +2234,7 @@ msgstr[1] "%(counter)s ficheiros" msgid "Parent mailbox" msgstr "Caixa de correio principal" -#: extensions/webmail/templates/webmail/index.html:36 +#: extensions/webmail/templates/webmail/index.html:38 msgid "Mailbox quota" msgstr "Quota da caixa de correio" @@ -2297,15 +2296,15 @@ msgstr "Como não lidas" msgid "Compress folder" msgstr "Comprimir pasta" -#: extensions/webmail/templatetags/webextras.py:109 +#: extensions/webmail/templatetags/webextras.py:110 msgid "Empty folder" msgstr "Esvaziar pasta" -#: extensions/webmail/templatetags/webextras.py:177 +#: extensions/webmail/templatetags/webextras.py:178 msgid "Edit the selected mailbox" msgstr "Editar caixa de correio selecionada" -#: extensions/webmail/templatetags/webextras.py:182 +#: extensions/webmail/templatetags/webextras.py:183 msgid "Remove the selected mailbox" msgstr "Eliminar caixa de correio selecionada" @@ -2359,7 +2358,7 @@ msgstr "A página solicitada não existe." msgid "An internal error occured." msgstr "Ocorreu um erro interno." -#: templates/nlayout.html:56 +#: templates/nlayout.html:62 msgid "Powered by Modoboa" msgstr "Powered by Modoboa" @@ -2393,11 +2392,11 @@ msgstr "Fechar" msgid "Welcome to Modoboa" msgstr "Benvindo ao Modoboa" -#: templates/registration/login.html:33 +#: templates/registration/login.html:34 msgid "Remember me" msgstr "Lembrar-me" -#: templates/registration/login.html:35 +#: templates/registration/login.html:36 msgid "Log in" msgstr "Entrar" @@ -2413,7 +2412,7 @@ msgstr "Configurações por usuário" msgid "Prefered language" msgstr "Idioma preferido" -#: userprefs/app_settings.py:26 +#: userprefs/app_settings.py:27 msgid "Prefered language to display pages" msgstr "Idioma preferido para exibir páginas" diff --git a/modoboa/locale/sv/LC_MESSAGES/django.po b/modoboa/locale/sv/LC_MESSAGES/django.po index f8a7c0833..8373e44ff 100644 --- a/modoboa/locale/sv/LC_MESSAGES/django.po +++ b/modoboa/locale/sv/LC_MESSAGES/django.po @@ -1,22 +1,22 @@ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. -# +# # Translators: # ogg1980 , 2013 +# ogg1980 , 2013 msgid "" msgstr "" "Project-Id-Version: Modoboa\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-06-01 16:40+0200\n" -"PO-Revision-Date: 2013-06-01 14:36+0000\n" -"Last-Translator: Antoine Nguyen \n" -"Language-Team: Swedish (http://www.transifex.com/projects/p/modoboa/language/" -"sv/)\n" -"Language: sv\n" +"Report-Msgid-Bugs-To: http://dev.modoboa.org/\n" +"POT-Creation-Date: 2013-09-20 22:30+0200\n" +"PO-Revision-Date: 2013-09-27 08:19+0000\n" +"Last-Translator: ogg1980 \n" +"Language-Team: Swedish (http://www.transifex.com/projects/p/modoboa/language/sv/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Language: sv\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: admin/__init__.py:9 @@ -27,8 +27,8 @@ msgstr "Administrerings konsoll" msgid "Web based console to manage domains, accounts and aliases" msgstr "Webbaserad konsoll för att hantera domäner, konton och alias" -#: admin/__init__.py:16 admin/forms.py:179 admin/forms.py:614 -#: admin/views.py:93 admin/views.py:441 +#: admin/__init__.py:16 admin/forms.py:211 admin/forms.py:652 +#: admin/views.py:94 admin/views.py:447 #: extensions/postfix_autoreply/app_settings.py:11 #: extensions/sievefilters/app_settings.py:50 #: extensions/stats/app_settings.py:11 extensions/webmail/app_settings.py:11 @@ -122,9 +122,7 @@ msgstr "Bind DN" msgid "" "The distinguished name to use when binding to the LDAP server. Leave empty " "for an anonymous bind" -msgstr "" -"Det unika namnet för att använda vid bindningar till LDAP-servern. Lämna " -"tomt för en anonym bindning" +msgstr "Det unika namnet för att använda vid bindningar till LDAP-servern. Lämna tomt för en anonym bindning" #: admin/app_settings.py:97 msgid "Bind password" @@ -132,8 +130,7 @@ msgstr "Bind lösenord" #: admin/app_settings.py:99 msgid "The password to use when binding to the LDAP server (with 'Bind DN')" -msgstr "" -"Lösenordet ska användas vid bindning till LDAP-servern (med \"Bind DN ')" +msgstr "Lösenordet ska användas vid bindning till LDAP-servern (med \"Bind DN ')" #: admin/app_settings.py:105 msgid "Search base" @@ -151,9 +148,7 @@ msgstr "Sökfilter" msgid "" "An optional filter string (e.g. '(objectClass=person)'). In order to be " "valid, it must be enclosed in parentheses." -msgstr "" -"En valfri filtersträng (t.ex. \"(objectClass = personer) '). För att vara " -"giltigt måste det omges av parenteser." +msgstr "En valfri filtersträng (t.ex. \"(objectClass = personer) '). För att vara giltigt måste det omges av parenteser." #: admin/app_settings.py:119 msgid "User DN template" @@ -164,9 +159,7 @@ msgstr "Anvädar DN mall" msgid "" "The template used to construct a user's DN. It should contain one " "placeholder (ie. %(user)s)" -msgstr "" -"Mallen som används för att konstruera en användares DN. Den bör innehålla en " -"platshållare (dvs. %(user)s)" +msgstr "Mallen som används för att konstruera en användares DN. Den bör innehålla en platshållare (dvs. %(user)s)" #: admin/app_settings.py:126 msgid "Password attribute" @@ -199,9 +192,7 @@ msgstr "Hantera brevlådor på filsystem" msgid "" "Rename or remove mailboxes on the filesystem when they get renamed or " "removed within Modoboa" -msgstr "" -"Byt namn på eller ta bort brevlådor på filsystemet när de kommer byta namn " -"eller tas bort inom Modoboa" +msgstr "Byt namn på eller ta bort brevlådor på filsystemet när de kommer byta namn eller tas bort inom Modoboa" #: admin/app_settings.py:146 msgid "Mailboxes ower" @@ -247,39 +238,45 @@ msgstr "Standard topp omdirigering" msgid "The default redirection used when no application is specified" msgstr "Standard omdirigering när ingen applikation angetts" -#: admin/app_settings.py:230 +#: admin/app_settings.py:213 +msgid "Key must be either 16, 24, or 32 bytes long" +msgstr "Nyckel måste vara endera 16, 24 eller 32 bytes lång" + +#: admin/app_settings.py:222 +msgid "Invalid syntax" +msgstr "Ogiltig syntax" + +#: admin/app_settings.py:245 msgid "This field is required" msgstr "Detta fält är obligatoriskt" -#: admin/forms.py:16 admin/forms.py:459 admin/tables.py:18 +#: admin/forms.py:20 admin/forms.py:497 admin/tables.py:18 msgid "Alias(es)" msgstr "Alias" -#: admin/forms.py:18 +#: admin/forms.py:22 msgid "" "Alias(es) of this domain. Indicate only one name per input, press ENTER to " "add a new input." -msgstr "" -"Alias för denna domän. Ange endast ett namn per rad, tryck ENTER för att " -"lägga till en rad." +msgstr "Alias för denna domän. Ange endast ett namn per rad, tryck ENTER för att lägga till en rad." -#: admin/forms.py:55 +#: admin/forms.py:59 msgid "An alias with this name already exists" msgstr "Ett alias med detta namn finns redan" -#: admin/forms.py:69 +#: admin/forms.py:73 msgid "A domain with this name already exists" msgstr "En domän med detta namn existerar redan" -#: admin/forms.py:114 +#: admin/forms.py:146 msgid "Create a domain administrator" msgstr "Skapa en domän administratör" -#: admin/forms.py:116 +#: admin/forms.py:148 msgid "Automatically create an administrator for this domain" msgstr "Skapa en domän administratör automatiskt för denna domän" -#: admin/forms.py:120 admin/tables.py:12 admin/tables.py:47 +#: admin/forms.py:152 admin/tables.py:12 admin/tables.py:47 #: admin/templates/admin/editdomainform.html:13 #: admin/templates/admin/identities_table.html:5 #: admin/templates/admin/information.html:8 @@ -288,363 +285,348 @@ msgstr "Skapa en domän administratör automatiskt för denna domän" msgid "Name" msgstr "Namn" -#: admin/forms.py:122 +#: admin/forms.py:154 msgid "" "The administrator's name. Don't include the domain's name here, it will be " "automatically appended." -msgstr "" -"Administratörens namn. Inkludera inte domänens namn här, den kommer att " -"läggas till automatiskt." +msgstr "Administratörens namn. Inkludera inte domänens namn här, den kommer att läggas till automatiskt." -#: admin/forms.py:128 +#: admin/forms.py:160 msgid "Create aliases" msgstr "Skapa alias." -#: admin/forms.py:130 +#: admin/forms.py:162 msgid "Automatically create standard aliases for this domain" msgstr "Skapa automatiskt standard alias för den här domänen" -#: admin/forms.py:143 +#: admin/forms.py:175 msgid "Invalid format" msgstr "Ogiltigt format" -#: admin/forms.py:155 +#: admin/forms.py:187 #, python-format msgid "User '%s' already exists" msgstr "Användare '%s' finns redan" -#: admin/forms.py:203 +#: admin/forms.py:235 msgid "Email address" msgstr "E-post adress" -#: admin/forms.py:204 +#: admin/forms.py:236 msgid "" "The distribution list address. Use the '*' character to create a 'catchall' " "address (ex: *@domain.tld)." -msgstr "" -"Distributionslistans adress. Använd '*' för att skapa en 'catchall' adress " -"(ex: *@domain.tld)." +msgstr "Distributionslistans adress. Använd '*' för att skapa en 'catchall' adress (ex: *@domain.tld)." -#: admin/forms.py:207 +#: admin/forms.py:239 msgid "Recipients" msgstr "Mottagare" -#: admin/forms.py:208 +#: admin/forms.py:240 msgid "" "Mailbox(es) this alias will point to. Indicate only one address per input, " "press ENTER to add a new input." -msgstr "" -"Brevlåda/or detta alias kommer att peka på. Ange endast en adress per " -"inlägg, tryck ENTER för att lägga till en ny inlägg." +msgstr "Brevlåda/or detta alias kommer att peka på. Ange endast en adress per inlägg, tryck ENTER för att lägga till en ny inlägg." -#: admin/forms.py:246 admin/forms.py:504 admin/forms.py:530 -#: admin/models.py:1147 +#: admin/forms.py:278 admin/forms.py:542 admin/forms.py:568 msgid "Domain does not exist" msgstr "Domän existerar inte" -#: admin/forms.py:248 +#: admin/forms.py:280 msgid "You don't have access to this domain" msgstr "Du har inte åtkomst till denna domän" -#: admin/forms.py:268 +#: admin/forms.py:300 msgid "Invalid mailbox" msgstr "Ogiltig brevlåda" -#: admin/forms.py:284 +#: admin/forms.py:316 admin/models.py:1202 #, python-format msgid "Local recipient %s not found" msgstr "Lokal mottagare %s kunde inte hittas" -#: admin/forms.py:286 admin/forms.py:292 +#: admin/forms.py:318 admin/forms.py:324 #, python-format msgid "Recipient %s already present" msgstr "Mottagare %s finns redan" -#: admin/forms.py:297 +#: admin/forms.py:329 msgid "No recipient defined" msgstr "Ingen mottagare definierad" -#: admin/forms.py:311 extensions/webmail/forms.py:19 +#: admin/forms.py:343 extensions/webmail/forms.py:19 msgid "Select a file" msgstr "Välj en fil" -#: admin/forms.py:313 admin/forms.py:341 +#: admin/forms.py:345 admin/forms.py:373 msgid "Separator" msgstr "Separator" -#: admin/forms.py:316 +#: admin/forms.py:348 msgid "Continue on error" msgstr "Fortsätt vid fel" -#: admin/forms.py:317 +#: admin/forms.py:349 msgid "Don't treat duplicated objects as error" msgstr "Behandla inte duplicerade objekt som fel" -#: admin/forms.py:332 +#: admin/forms.py:364 msgid "Crypt passwords" msgstr "Krypterade lösenord" -#: admin/forms.py:333 +#: admin/forms.py:365 msgid "Check this option if passwords contained in your file are not crypted" msgstr "Markera det här alternativet om lösenord i filen inte är krypterad" -#: admin/forms.py:339 +#: admin/forms.py:371 msgid "File name" msgstr "Filnamn" -#: admin/forms.py:371 admin/templates/admin/editdomainform.html:13 +#: admin/forms.py:403 admin/templates/admin/editdomainform.html:13 #: auth/forms.py:7 msgid "Username" msgstr "Användarnamn" -#: admin/forms.py:373 +#: admin/forms.py:405 msgid "Role" msgstr "Roll" -#: admin/forms.py:374 admin/forms.py:401 +#: admin/forms.py:406 admin/forms.py:433 msgid "Choose" msgstr "Välj" -#: admin/forms.py:375 +#: admin/forms.py:407 msgid "What level of permission this user will have" msgstr "Vilken nivå av behörighet den här användaren har" -#: admin/forms.py:377 auth/forms.py:11 +#: admin/forms.py:409 auth/forms.py:11 msgid "Password" msgstr "Lösenord" -#: admin/forms.py:379 userprefs/forms.py:18 +#: admin/forms.py:411 userprefs/forms.py:18 msgid "Confirmation" msgstr "Bekräftelse" -#: admin/forms.py:381 +#: admin/forms.py:413 msgid "Enter the same password as above, for verification." msgstr "Ange samma lösenord som ovan för verifiering." -#: admin/forms.py:392 +#: admin/forms.py:424 msgid "Enabled" msgstr "Aktiverad" -#: admin/forms.py:434 +#: admin/forms.py:471 msgid "The two password fields didn't match." msgstr "Dessa två lösnordsfält matchar inte." -#: admin/forms.py:440 +#: admin/forms.py:477 msgid "You can't disable your own account" msgstr "Du kan inte inaktivera ditt eget konto" -#: admin/forms.py:450 +#: admin/forms.py:488 msgid "E-mail" msgstr "E-post" -#: admin/forms.py:452 +#: admin/forms.py:490 msgid "Quota" msgstr "Kvota" -#: admin/forms.py:454 +#: admin/forms.py:492 msgid "" "Quota in MB for this mailbox. Define a custom value or use domain's default " "one. Leave empty to define an unlimited value (not allowed for domain " "administrators)." -msgstr "" -"Kvot i MB för denna brevlåda. Definiera ett anpassat värde eller använda " -"domänens förvalda. Lämna tomt för att definiera ett obegränsat värde (ej " -"tillåtet för domänadministratörer)." +msgstr "Kvot i MB för denna brevlåda. Definiera ett anpassat värde eller använda domänens förvalda. Lämna tomt för att definiera ett obegränsat värde (ej tillåtet för domänadministratörer)." -#: admin/forms.py:461 +#: admin/forms.py:499 msgid "" "Alias(es) of this mailbox. Indicate only one address per input, press ENTER " "to add a new input. Use the '*' character to create a 'catchall' alias (ex: " "*@domain.tld)." -msgstr "" -"Alias för denna brevlåda. Ange endast en adress per rad, tryck ENTER för att " -"lägga till ny rad. Använd '*' för att skapa ett 'catchall' alias (ex: " -"*@domain.tld)." +msgstr "Alias för denna brevlåda. Ange endast en adress per rad, tryck ENTER för att lägga till ny rad. Använd '*' för att skapa ett 'catchall' alias (ex: *@domain.tld)." -#: admin/forms.py:512 +#: admin/forms.py:550 #, python-format msgid "Mailbox %s already exists" msgstr "Brevlåda %s finns redan" -#: admin/forms.py:572 +#: admin/forms.py:610 msgid "Domain(s)" msgstr "Domän(er)" -#: admin/forms.py:574 +#: admin/forms.py:612 msgid "Domain(s) that user administrates" msgstr "Domän(er) användaren administrerar" -#: admin/forms.py:616 admin/views.py:444 +#: admin/forms.py:654 admin/views.py:450 msgid "Mail" msgstr "Post" -#: admin/forms.py:618 +#: admin/forms.py:656 msgid "Permissions" msgstr "Rättigheter" -#: admin/models.py:78 +#: admin/models.py:80 msgid "You can't delete your own account" msgstr "Du kan inte radera ditt eget konto" -#: admin/models.py:159 +#: admin/models.py:161 msgid "Failed to update password: LDAP module not installed" msgstr "Misslyckades med att uppdatera lösenord: LDAP modul ej installerad" -#: admin/models.py:165 +#: admin/models.py:167 #, python-format msgid "Failed to update password: %s" msgstr "Misslyckades med att uppdatera lösenord: %s" -#: admin/models.py:192 +#: admin/models.py:194 msgid "account" msgstr "konto" -#: admin/models.py:494 admin/models.py:720 admin/models.py:782 +#: admin/models.py:496 admin/models.py:733 admin/models.py:795 msgid "Invalid line" msgstr "Ogiltig rad" -#: admin/models.py:498 +#: admin/models.py:500 msgid "You can't import an account with a role greater than yours" msgstr "Du kan inte importera konto med en roll större än din" -#: admin/models.py:518 +#: admin/models.py:506 +#, python-format +msgid "The simple user '%s' must have a valid email address" +msgstr "Den enkla användaren '%s' måste ha en gällande e-postadress" + +#: admin/models.py:510 +#, python-format +msgid "username and email fields must not differ for '%s'" +msgstr "användarnamn och e-post fält får inte avvika för '%s'" + +#: admin/models.py:534 #, python-format msgid "Account import failed (%s): domain does not exist" msgstr "Konto import misslyckades (%s): domänen existerar inte" -#: admin/models.py:607 admin/models.py:733 +#: admin/models.py:624 admin/models.py:746 msgid "name" msgstr "namn" -#: admin/models.py:608 +#: admin/models.py:625 msgid "The domain name" msgstr "Domännamnet" -#: admin/models.py:610 +#: admin/models.py:627 msgid "Default quota in MB applied to mailboxes" msgstr "Standard kvota i MB applicerad för brevlådor" -#: admin/models.py:613 admin/models.py:740 admin/models.py:1020 -#: admin/models.py:1181 extensions/postfix_autoreply/models.py:19 +#: admin/models.py:630 admin/models.py:753 admin/models.py:1045 +#: admin/models.py:1217 extensions/postfix_autoreply/models.py:27 msgid "enabled" msgstr "aktiverad" -#: admin/models.py:614 +#: admin/models.py:631 msgid "Check to activate this domain" msgstr "Markera för att aktivera denna domän" -#: admin/models.py:734 +#: admin/models.py:747 msgid "The alias name" msgstr "Alias namn" -#: admin/models.py:736 +#: admin/models.py:749 msgid "target" msgstr "mål" -#: admin/models.py:737 +#: admin/models.py:750 msgid "The domain this alias points to" msgstr "Domän dit detta alias pekar mot" -#: admin/models.py:741 admin/models.py:1021 +#: admin/models.py:754 admin/models.py:1046 msgid "Check to activate this alias" msgstr "Markera för aktivera detta alias" -#: admin/models.py:788 +#: admin/models.py:801 #, python-format msgid "Unknown domain %s" msgstr "Okänt kommando %s" -#: admin/models.py:805 admin/models.py:1006 +#: admin/models.py:818 admin/models.py:1031 msgid "address" msgstr "adress" -#: admin/models.py:806 +#: admin/models.py:819 msgid "Mailbox address (without the @domain.tld part)" msgstr "Brevlåde adress (utan @domain.tld)" -#: admin/models.py:852 +#: admin/models.py:877 #, python-format msgid "Failed to retrieve mailbox location (%s)" msgstr "Kunde inte hitta brevlådans sökväg (%s)" -#: admin/models.py:868 -#, python-format -msgid "Failed to rename mailbox: %s" -msgstr "Kunde byta namn på brevlåda: %s" - -#: admin/models.py:894 -#, python-format -msgid "Failed to remove mailbox: %s" -msgstr "Kunde inte radera brevlåda: %s" - -#: admin/models.py:909 +#: admin/models.py:921 #, python-format msgid "Quota is greater than the allowed domain's limit (%dM)" msgstr "Kvotan är större än domänens gräns (%dM)" -#: admin/models.py:914 +#: admin/models.py:926 msgid "A quota is required" msgstr "En kvota krävs" -#: admin/models.py:1007 +#: admin/models.py:1032 msgid "" -"The alias address (without the domain part). For a 'catch-all' address, just " -"enter an * character." -msgstr "" -"Alias adress (utan domän delen). För en \"catch-all\"-adress, ange bara " -"asterisk *." +"The alias address (without the domain part). For a 'catch-all' address, just" +" enter an * character." +msgstr "Alias adress (utan domän delen). För en \"catch-all\"-adress, ange bara asterisk *." -#: admin/models.py:1011 +#: admin/models.py:1036 msgid "mailboxes" msgstr "brevlådor" -#: admin/models.py:1012 +#: admin/models.py:1037 msgid "The mailboxes this alias points to" msgstr "Brevlådorna detta alias pekar till" -#: admin/models.py:1016 +#: admin/models.py:1041 msgid "The aliases this alias points to" msgstr "Det alias detta alias pekar till" -#: admin/models.py:1064 +#: admin/models.py:1089 msgid "distribution list" msgstr "distributionslista" -#: admin/models.py:1065 +#: admin/models.py:1090 msgid "forward" msgstr "vidarebefordra" -#: admin/models.py:1066 +#: admin/models.py:1091 msgid "alias" msgstr "alias" -#: admin/models.py:1142 +#: admin/models.py:1170 #, python-format msgid "Invalid line: %s" msgstr "Ogiltig rad: %s" -#: admin/models.py:1167 +#: admin/models.py:1175 #, python-format -msgid "Mailbox %s does not exist" -msgstr "Brevlådan %s finns inte" +msgid "Domain '%s' does not exist" +msgstr "Domänen '%s' existerar inte" -#: admin/models.py:1182 +#: admin/models.py:1218 msgid "Check to enable this extension" msgstr "Markera för att aktivera detta tillägg" -#: admin/models.py:1282 +#: admin/models.py:1318 msgid "added" msgstr "tillagd" -#: admin/models.py:1285 +#: admin/models.py:1321 msgid "modified" msgstr "ändrad" -#: admin/models.py:1288 +#: admin/models.py:1324 msgid "deleted" msgstr "raderad" -#: admin/models.py:1290 +#: admin/models.py:1326 #, python-format msgid "%(object)s '%(name)s' %(action)s by user %(user)s" msgstr "%(object)s '%(name)s' %(action)s av användare %(user)s" @@ -654,7 +636,7 @@ msgid "Edit domain" msgstr "Redigera domän" #: admin/tables.py:21 admin/templates/admin/identities_table.html:8 -#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:79 +#: admin/templates/admin/listing.html:17 extensions/sievefilters/views.py:83 #: extensions/sievefilters/templates/sievefilters/filter.html:17 #: extensions/sievefilters/templates/sievefilters/guieditor.html:5 #: extensions/webmail/templatetags/webextras.py:97 @@ -669,213 +651,190 @@ msgstr "Version" msgid "Description" msgstr "Beskrivning" -#: admin/views.py:87 +#: admin/views.py:88 msgid "New domain" msgstr "Ny domän" -#: admin/views.py:88 admin/views.py:97 admin/views.py:225 admin/views.py:438 -#: admin/views.py:445 extensions/sievefilters/views.py:120 -#: extensions/sievefilters/views.py:187 extensions/webmail/views.py:131 +#: admin/views.py:89 admin/views.py:98 admin/views.py:228 admin/views.py:444 +#: admin/views.py:451 extensions/sievefilters/views.py:129 +#: extensions/sievefilters/views.py:198 extensions/webmail/views.py:136 msgid "Create" msgstr "Skapa" -#: admin/views.py:94 admin/views.py:442 +#: admin/views.py:95 admin/views.py:448 msgid "Next" msgstr "Nästa" -#: admin/views.py:96 +#: admin/views.py:97 msgid "Options" msgstr "Inställningar" -#: admin/views.py:98 admin/views.py:446 +#: admin/views.py:99 admin/views.py:452 msgid "Previous" msgstr "Föregående" -#: admin/views.py:111 +#: admin/views.py:112 msgid "Domain created" msgstr "Domän skapad" -#: admin/views.py:144 admin/views.py:282 admin/views.py:492 +#: admin/views.py:146 admin/views.py:285 admin/views.py:498 #: extensions/postfix_autoreply/templates/postfix_autoreply/autoreply.html:9 -#: extensions/sievefilters/views.py:139 extensions/webmail/views.py:162 +#: extensions/sievefilters/views.py:148 extensions/webmail/views.py:168 #: userprefs/templates/userprefs/forward.html:8 #: userprefs/templates/userprefs/profile.html:8 msgid "Update" msgstr "Uppdatera" -#: admin/views.py:160 +#: admin/views.py:162 msgid "Domain modified" msgstr "Domän ändrad" -#: admin/views.py:186 +#: admin/views.py:189 msgid "You can't delete your own domain" msgstr "Du kan inte radera din egen domän" -#: admin/views.py:189 +#: admin/views.py:192 msgid "Domain deleted" msgid_plural "Domains deleted" msgstr[0] "Domän raderad" msgstr[1] "Domäner raderade" -#: admin/views.py:204 +#: admin/views.py:207 msgid "Alias with this name already exists" msgstr "Alias med detta namn existerar redan" -#: admin/views.py:247 +#: admin/views.py:250 msgid "New distribution list" msgstr "Ny distributionslista" -#: admin/views.py:248 +#: admin/views.py:251 msgid "Distribution list created" msgstr "Distributionslista skapad" -#: admin/views.py:257 +#: admin/views.py:260 msgid "New alias" msgstr "Nytt alias" -#: admin/views.py:258 +#: admin/views.py:261 msgid "Alias created" msgstr "Alias skapat" -#: admin/views.py:267 +#: admin/views.py:270 msgid "New forward" msgstr "Ny vidarebefodring" -#: admin/views.py:268 +#: admin/views.py:271 msgid "Forward created" msgstr "Vidarebefodring skapad" -#: admin/views.py:286 +#: admin/views.py:289 msgid "Distribution list modified" msgstr "Distributionslista ändrad" -#: admin/views.py:288 +#: admin/views.py:291 msgid "Forward modified" msgstr "Vidarebefodring ändrad" -#: admin/views.py:290 +#: admin/views.py:293 msgid "Alias modified" msgstr "Alias ändrat" -#: admin/views.py:403 admin/views.py:541 admin/views.py:546 -#: extensions/amavis/views.py:195 extensions/amavis/views.py:201 -#: extensions/amavis/views.py:234 extensions/amavis/views.py:238 -#: extensions/amavis/views.py:240 extensions/webmail/views.py:36 -#: extensions/webmail/views.py:64 extensions/webmail/views.py:77 -#: extensions/webmail/views.py:93 extensions/webmail/views.py:109 -#: extensions/webmail/views.py:193 extensions/webmail/views.py:419 -#: extensions/webmail/views.py:430 extensions/webmail/views.py:457 +#: admin/views.py:402 admin/views.py:545 admin/views.py:550 +#: extensions/amavis/views.py:204 extensions/amavis/views.py:210 +#: extensions/amavis/views.py:243 extensions/amavis/views.py:247 +#: extensions/amavis/views.py:249 extensions/webmail/views.py:41 +#: extensions/webmail/views.py:68 extensions/webmail/views.py:81 +#: extensions/webmail/views.py:98 extensions/webmail/views.py:114 +#: extensions/webmail/views.py:200 extensions/webmail/views.py:427 +#: extensions/webmail/views.py:438 extensions/webmail/views.py:465 #: lib/formutils.py:32 lib/formutils.py:37 msgid "Invalid request" msgstr "Ogiltig förfrågan" -#: admin/views.py:435 +#: admin/views.py:441 msgid "New account" msgstr "Nytt konto" -#: admin/views.py:459 +#: admin/views.py:465 msgid "Account created" msgstr "Konto skapat" -#: admin/views.py:507 +#: admin/views.py:513 msgid "Account updated" msgstr "Konto uppdaterat" -#: admin/views.py:531 +#: admin/views.py:535 msgid "Account deleted" msgid_plural "Accounts deleted" msgstr[0] "Konto raderat" msgstr[1] "Konton raderade" -#: admin/views.py:586 +#: admin/views.py:590 msgid "Parameters saved" msgstr "Parametrar sparade" -#: admin/views.py:632 +#: admin/views.py:636 msgid "Modifications applied." msgstr "Modifieringar tillämpade." -#: admin/views.py:740 +#: admin/views.py:743 #, python-format msgid "Object already exists: %s" msgstr "Objektet existerar redan: %s" -#: admin/views.py:742 +#: admin/views.py:745 #, python-format msgid "%d objects imported successfully" msgstr "%d objekt importerade" -#: admin/views.py:760 +#: admin/views.py:763 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
                          \n" -"
                        • account; loginname; password; first name; last name; enabled; group; " -"address[, domain, ...]
                        • \n" +"
                        • account; loginname; password; first name; last name; enabled; group; address[, domain, ...]
                        • \n" "
                        • alias; address; enabled; internal recipient
                        • \n" "
                        • forward; address; enabled; external recipient
                        • \n" "
                        • dlist; address; enabled; recipient; recipient; ...
                        • \n" "
                        \n" -"

                        The first element of each line is mandatory and must be equal to one of " -"the previous values.

                        \n" +"

                        The first element of each line is mandatory and must be equal to one of the previous values.

                        \n" "\n" "

                        You can use a different character as separator.

                        \n" -msgstr "" -"Tillhandahåll en CSV-fil där raderna har följande format:⏎\n" -"
                          ⏎\n" -"
                        • account; loginname; password; first name; last name; enabled; group; " -"address[, domain, ...]
                        • ⏎\n" -"
                        • alias; address; enabled; internal recipient
                        • ⏎\n" -"
                        • forward; address; enabled; external recipient
                        • ⏎\n" -"
                        • dlist; address; enabled; recipient; recipient; ...
                        • ⏎\n" -"
                        ⏎\n" -"

                        Det första elementet i varje rad är obligatorisk och måste vara lika med " -"en av de tidigare värdena.

                        ⏎\n" -"⏎\n" -"

                        Du kan använda ett annat tecken separator.

                        ⏎\n" +msgstr "Tillhandahåll en CSV-fil där raderna har följande format:⏎\n
                          ⏎\n
                        • account; loginname; password; first name; last name; enabled; group; address[, domain, ...]
                        • ⏎\n
                        • alias; address; enabled; internal recipient
                        • ⏎\n
                        • forward; address; enabled; external recipient
                        • ⏎\n
                        • dlist; address; enabled; recipient; recipient; ...
                        • ⏎\n
                        ⏎\n

                        Det första elementet i varje rad är obligatorisk och måste vara lika med en av de tidigare värdena.

                        ⏎\n⏎\n

                        Du kan använda ett annat tecken separator.

                        ⏎\n" -#: admin/views.py:772 +#: admin/views.py:775 msgid "Import identities" msgstr "Importera konton" -#: admin/views.py:773 admin/views.py:843 admin/templatetags/admin_extras.py:87 +#: admin/views.py:776 admin/views.py:846 admin/templatetags/admin_extras.py:87 #: admin/templatetags/admin_extras.py:144 msgid "Import" msgstr "Importera" -#: admin/views.py:803 +#: admin/views.py:806 msgid "Export identities" msgstr "Exportera konton" -#: admin/views.py:804 admin/views.py:860 admin/templatetags/admin_extras.py:93 +#: admin/views.py:807 admin/views.py:863 admin/templatetags/admin_extras.py:93 #: admin/templatetags/admin_extras.py:150 msgid "Export" msgstr "Exportera" -#: admin/views.py:832 +#: admin/views.py:835 msgid "" "Provide a CSV file where lines respect one of the following formats:\n" "
                          \n" "
                        • domain; name; quota; enabled
                        • \n" "
                        • domainalias; name; targeted domain; enabled
                        • \n" "
                        \n" -"

                        The first element of each line is mandatory and must be equal to one of " -"the previous values.

                        \n" +"

                        The first element of each line is mandatory and must be equal to one of the previous values.

                        \n" "

                        You can use a different character as separator.

                        \n" -msgstr "" -"Tillhandahåll en CSV fil där raderna har följande format:\n" -"
                          \n" -"
                        • domän, namn, kvot, aktiverade\n" -"
                        • domainalias, namn, riktad domän, aktiverade\n" -"\n" -"

                          det första elementet i varje rad är obligatoriskt och måste vara lika med " -"en av de tidigare värdena.\n" -"

                          Du kan använda ett annat tecken som separator.\n" +msgstr "Tillhandahåll en CSV fil där raderna har följande format:\n

                            \n
                          • domän, namn, kvot, aktiverade\n
                          • domainalias, namn, riktad domän, aktiverade\n\n

                            det första elementet i varje rad är obligatoriskt och måste vara lika med en av de tidigare värdena.\n

                            Du kan använda ett annat tecken som separator.\n" -#: admin/views.py:842 +#: admin/views.py:845 msgid "Import domains" msgstr "Importera domäner" -#: admin/views.py:859 +#: admin/views.py:862 msgid "Export domains" msgstr "Exportera domäner" @@ -948,7 +907,7 @@ msgid "Tags" msgstr "Tagg" #: admin/templates/admin/importform.html:5 -#: admin/templatetags/admin_extras.py:305 +#: admin/templatetags/admin_extras.py:290 msgid "Help" msgstr "Hjälp" @@ -995,7 +954,7 @@ msgid "Previously on Modoboa..." msgstr "Tidigare på Modoboa..." #: admin/templates/admin/logs.html:6 extensions/amavis/sql_listing.py:25 -#: extensions/webmail/lib.py:37 extensions/webmail/lib.py:406 +#: extensions/webmail/lib.py:38 extensions/webmail/lib.py:411 msgid "Date" msgstr "Datum" @@ -1098,34 +1057,21 @@ msgid "View the domain's identities" msgstr "Visa domänens konton" #: admin/templatetags/admin_extras.py:178 -msgid "Delete the domain" -msgstr "Radera domänen" - #: admin/templatetags/admin_extras.py:194 -msgid "Delete this account" -msgstr "Radera detta konto" +#: admin/templatetags/admin_extras.py:201 +#, python-format +msgid "Delete %s?" +msgstr "Radera %s?" -#: admin/templatetags/admin_extras.py:202 -msgid "Delete this distribution list" -msgstr "Radera denna distributionslista" - -#: admin/templatetags/admin_extras.py:209 -msgid "Delete this forward" -msgstr "Radera denna vidarebefodring" - -#: admin/templatetags/admin_extras.py:216 -msgid "Delete this alias" -msgstr "Radera detta alia" - -#: admin/templatetags/admin_extras.py:238 +#: admin/templatetags/admin_extras.py:223 msgid "Remove this permission" msgstr "Radera detta tillstånd" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 msgid "Yes" msgstr "Ja" -#: admin/templatetags/admin_extras.py:279 lib/formutils.py:206 +#: admin/templatetags/admin_extras.py:264 lib/formutils.py:206 msgid "No" msgstr "Nej" @@ -1134,11 +1080,11 @@ msgstr "Nej" msgid "User '%s' successfully logged in" msgstr "Användare '%s' har loggat in" -#: auth/views.py:41 +#: auth/views.py:44 msgid "Your username and password didn't match. Please try again." msgstr "Ditt användarnamn och lösenord matchar inte. Var vänlig försök igen." -#: auth/views.py:64 +#: auth/views.py:67 #, python-format msgid "User '%s' logged out" msgstr "Användare '%s' har loggat ut" @@ -1164,30 +1110,15 @@ msgid "" "\n" "

                            Welcome to the online demo!

                            \n" "

                            Warning: the demo's concent is resetted every two hours.

                            \n" -"

                            To try the demo, the following users are available (username/password):

                            \n" +"

                            To try the demo, the following users are available (username/password):

                            \n" "
                              \n" "
                            • A super administrator : admin/password
                            • \n" "
                            • A domain administrator : admin@demo.local/admin
                            • \n" "
                            • A simple user : user@demo.local/user
                            • \n" "
                            \n" "

                            Please report any issue or give any suggestion to \n" -" the " -"mailing list.

                            \n" -msgstr "" -"\n" -"

                            Välkommen till online-demot!

                            \n" -"

                            Varning: demots innehåll återställs varannan timme

                            \n" -"

                            För att prova demot, följande användare finns tillgängliga " -"(användarnamn/lösenord):

                            \n" -"
                              \n" -"
                            • En super administratör: admin/password
                            • \n" -"
                            • En domän administratölr: admin@demo.local/admin
                            • \n" -"
                            • En enkel användare: user@demo.local/user
                            • \n" -"
                            \n" -"

                            Var vänlig rapportera eventuella fel eller ge förslag på mailinglistan." -"

                            \n" +" the mailing list.

                            \n" +msgstr "\n

                            Välkommen till online-demot!

                            \n

                            Varning: demots innehåll återställs varannan timme

                            \n

                            För att prova demot, följande användare finns tillgängliga (användarnamn/lösenord):

                            \n
                              \n
                            • En super administratör: admin/password
                            • \n
                            • En domän administratölr: admin@demo.local/admin
                            • \n
                            • En enkel användare: user@demo.local/user
                            • \n
                            \n

                            Var vänlig rapportera eventuella fel eller ge förslag på mailinglistan.

                            \n" #: extensions/amavis/__init__.py:22 msgid "Simple amavis management frontend" @@ -1197,16 +1128,16 @@ msgstr "Enkel amavis hantering frontend" msgid "Quarantine" msgstr "Karantän" -#: extensions/amavis/__init__.py:127 +#: extensions/amavis/__init__.py:129 msgid "pending requests" msgstr "väntande förfrågningar" -#: extensions/amavis/__init__.py:144 +#: extensions/amavis/__init__.py:149 #, python-format msgid "%d pending requests" msgstr "%d väntande förfrågningar" -#: extensions/amavis/__init__.py:156 +#: extensions/amavis/__init__.py:161 msgid "Content filter" msgstr "Innehålls filter" @@ -1321,12 +1252,12 @@ msgid "Virus filter" msgstr "Virus filter" #: extensions/amavis/models.py:94 extensions/amavis/models.py:102 -#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:268 +#: extensions/amavis/models.py:110 extensions/sievefilters/views.py:281 msgid "yes" msgstr "ja" #: extensions/amavis/models.py:95 extensions/amavis/models.py:103 -#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:272 +#: extensions/amavis/models.py:111 extensions/sievefilters/views.py:285 msgid "no" msgstr "nej" @@ -1338,9 +1269,7 @@ msgstr "standard" #: extensions/amavis/models.py:98 msgid "Bypass virus checks or not. Choose 'default' to use global settings." -msgstr "" -"Kringgå virus kontroller eller ej. Välj \"standard\" för att använda globala " -"inställningar." +msgstr "Kringgå virus kontroller eller ej. Välj \"standard\" för att använda globala inställningar." #: extensions/amavis/models.py:101 msgid "Spam filter" @@ -1348,9 +1277,7 @@ msgstr "Skräp filter" #: extensions/amavis/models.py:106 msgid "Bypass spam checks or not. Choose 'default' to use global settings." -msgstr "" -"Kringgå skräp filter eller ej. Välj 'standard' för att använda globala " -"inställningar." +msgstr "Kringgå skräp filter eller ej. Välj 'standard' för att använda globala inställningar." #: extensions/amavis/models.py:109 msgid "Banned filter" @@ -1358,9 +1285,7 @@ msgstr "Förbjudna filter" #: extensions/amavis/models.py:114 msgid "Bypass banned checks or not. Choose 'default' to use global settings." -msgstr "" -"Kringgå förbjudna filter eller inte. Välj 'standard' för att använda globala " -"inställningar." +msgstr "Kringgå förbjudna filter eller inte. Välj 'standard' för att använda globala inställningar." #: extensions/amavis/models.py:144 msgid "Spam marker" @@ -1368,19 +1293,17 @@ msgstr "Skräp markering" #: extensions/amavis/models.py:146 msgid "" -"Modify spam subject using the specified text. Choose 'default' to use global " -"settings." -msgstr "" -"Ändra skräppost ämnesrad med vald text. Välj 'standard' för att använd " -"globala inställningar." +"Modify spam subject using the specified text. Choose 'default' to use global" +" settings." +msgstr "Ändra skräppost ämnesrad med vald text. Välj 'standard' för att använd globala inställningar." -#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:36 +#: extensions/amavis/sql_listing.py:23 extensions/webmail/lib.py:37 msgid "From" msgstr "Från" #: extensions/amavis/sql_listing.py:24 extensions/sievefilters/forms.py:68 -#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:35 -#: extensions/webmail/lib.py:404 templates/common/email_searchbar.html:6 +#: extensions/webmail/forms.py:10 extensions/webmail/lib.py:36 +#: extensions/webmail/lib.py:409 templates/common/email_searchbar.html:6 msgid "Subject" msgstr "Ämne" @@ -1390,37 +1313,37 @@ msgstr "Ämne" msgid "To" msgstr "Till" -#: extensions/amavis/views.py:34 +#: extensions/amavis/views.py:43 msgid "Empty quarantine" msgstr "Karantän tom" -#: extensions/amavis/views.py:202 +#: extensions/amavis/views.py:211 msgid "Message deleted" msgstr "Meddelande raderat" -#: extensions/amavis/views.py:223 +#: extensions/amavis/views.py:232 #, python-format msgid "%(count)d message deleted successfully" msgid_plural "%(count)d messages deleted successfully" msgstr[0] "%(count)d meddelande raderadat" msgstr[1] "%(count)d meddelanden raderade" -#: extensions/amavis/views.py:243 +#: extensions/amavis/views.py:252 msgid "Request sent" msgstr "Begäran skickad" -#: extensions/amavis/views.py:249 +#: extensions/amavis/views.py:258 msgid "Message released" msgstr "Meddelande släppt" -#: extensions/amavis/views.py:271 +#: extensions/amavis/views.py:280 #, python-format msgid "%(count)d request sent" msgid_plural "%(count)d requests sent" msgstr[0] "%(count)d förfrågning skickad" msgstr[1] "%(count)d förfrågningar skickadade" -#: extensions/amavis/views.py:295 +#: extensions/amavis/views.py:304 #, python-format msgid "%(count)d message released successfully" msgid_plural "%(count)d messages released successfully" @@ -1455,12 +1378,8 @@ msgid "" msgid_plural "" "\n" "%(counter)s release requests are pending for action.\n" -msgstr[0] "" -"\n" -"%(counter)s frisläpps förfrågning väntar på åtgärd.\n" -msgstr[1] "" -"\n" -"%(counter)s frisläpps förfrågningar väntar på åtgärd.'\n" +msgstr[0] "\n%(counter)s frisläpps förfrågning väntar på åtgärd.\n" +msgstr[1] "\n%(counter)s frisläpps förfrågningar väntar på åtgärd.'\n" #: extensions/amavis/templates/amavis/notifications/pending_requests.html:6 msgid "Sketch:" @@ -1535,8 +1454,7 @@ msgstr "Dålig MIME" #: extensions/limits/__init__.py:23 msgid "" "Per administrator resources to limit the number of objects they can create" -msgstr "" -"Per administratör resurser för att begränsa antalet objekt som de kan skapa" +msgstr "Per administratör resurser för att begränsa antalet objekt som de kan skapa" #: extensions/limits/__init__.py:54 msgid "Limits" @@ -1546,7 +1464,7 @@ msgstr "Gräns" msgid "Reseller" msgstr "Återförsäljare" -#: extensions/limits/__init__.py:96 +#: extensions/limits/__init__.py:94 msgid "Resources" msgstr "Resurser" @@ -1560,7 +1478,8 @@ msgid "Domain admins" msgstr "Domänadministratörer" #: extensions/limits/app_settings.py:15 -msgid "Maximum number of allowed domain administrators for a new administrator" +msgid "" +"Maximum number of allowed domain administrators for a new administrator" msgstr "Maximalt antal domänadministratörer den här användaren kan skapa" #: extensions/limits/app_settings.py:21 @@ -1665,23 +1584,23 @@ msgstr "Aktivera auto-svar fram till detta datum" msgid "This date is over" msgstr "Detta datum har varit" -#: extensions/postfix_autoreply/models.py:15 +#: extensions/postfix_autoreply/models.py:19 msgid "subject" msgstr "ämne" -#: extensions/postfix_autoreply/models.py:16 +#: extensions/postfix_autoreply/models.py:20 msgid "The subject that will appear in sent emails" msgstr "Ämnesraden i skickade e-post meddelanden" -#: extensions/postfix_autoreply/models.py:17 +#: extensions/postfix_autoreply/models.py:23 msgid "content" msgstr "innehåll" -#: extensions/postfix_autoreply/models.py:18 +#: extensions/postfix_autoreply/models.py:24 msgid "The content that will appear in sent emails" msgstr "Innehållet i skickade e-post meddelanden" -#: extensions/postfix_autoreply/models.py:20 +#: extensions/postfix_autoreply/models.py:28 msgid "Activate/Deactivate your auto reply" msgstr "Aktivera / inaktivera din autosvar" @@ -1809,61 +1728,59 @@ msgstr "Omdirigera meddelande till" #: extensions/sievefilters/lib.py:37 msgid "Connection to MANAGESIEVE server failed, check your configuration" -msgstr "" -"Anslutning till MANAGESIEVE servern misslyckades, kontrollera din " -"konfiguration" +msgstr "Anslutning till MANAGESIEVE servern misslyckades, kontrollera din konfiguration" #: extensions/sievefilters/lib.py:82 msgid "Not enough space on server" msgstr "Inte tillräckligt med utrymme på servern" -#: extensions/sievefilters/views.py:64 +#: extensions/sievefilters/views.py:68 msgid "Failed to retrieve filters set" msgstr "Det gick inte att hämta filteruppsättning" -#: extensions/sievefilters/views.py:117 +#: extensions/sievefilters/views.py:126 #: extensions/sievefilters/templatetags/sfilters_tags.py:33 msgid "New filter" msgstr "Nytt filter" -#: extensions/sievefilters/views.py:124 +#: extensions/sievefilters/views.py:133 msgid "Filter created" msgstr "Filter krashade" -#: extensions/sievefilters/views.py:136 +#: extensions/sievefilters/views.py:145 #: extensions/sievefilters/templatetags/sfilters_tags.py:77 msgid "Edit filter" msgstr "Redigera filter" -#: extensions/sievefilters/views.py:145 +#: extensions/sievefilters/views.py:154 msgid "Filter modified" msgstr "Filter modifierat" -#: extensions/sievefilters/views.py:165 +#: extensions/sievefilters/views.py:176 msgid "Filter removed" msgstr "Filter raderat" -#: extensions/sievefilters/views.py:166 +#: extensions/sievefilters/views.py:177 msgid "Failed to remove filter" msgstr "Det gick inte att radera filtret" -#: extensions/sievefilters/views.py:180 +#: extensions/sievefilters/views.py:191 msgid "Filters set saved" msgstr "Filter uppsättning sparat" -#: extensions/sievefilters/views.py:185 +#: extensions/sievefilters/views.py:196 msgid "Create a new filters set" msgstr "Skapa ny filteruppsättning" -#: extensions/sievefilters/views.py:208 +#: extensions/sievefilters/views.py:219 msgid "Filters set created" msgstr "Filter uppsättning skapat" -#: extensions/sievefilters/views.py:229 +#: extensions/sievefilters/views.py:240 msgid "Filters set deleted" msgstr "Filter uppsättning raderad" -#: extensions/sievefilters/views.py:241 +#: extensions/sievefilters/views.py:252 msgid "Filters set activated" msgstr "Filter uppsättning aktiverad" @@ -1879,20 +1796,18 @@ msgstr "Aktivera" msgid "No filter defined yet" msgstr "Inga filter definierade än" -#: extensions/sievefilters/templates/sievefilters/index.html:55 +#: extensions/sievefilters/templates/sievefilters/index.html:56 msgid "Filter sets" msgstr "Filter uppsättning" -#: extensions/sievefilters/templates/sievefilters/index.html:59 +#: extensions/sievefilters/templates/sievefilters/index.html:60 msgid "active" msgstr "aktivera" -#: extensions/sievefilters/templates/sievefilters/index.html:73 +#: extensions/sievefilters/templates/sievefilters/index.html:74 msgid "" "No filters set defined yet. You must create one in order to define filters." -msgstr "" -"Inga filter uppsättningar definierade. Du måste skapa en uppsättning för att " -"kunna definiera filter." +msgstr "Inga filter uppsättningar definierade. Du måste skapa en uppsättning för att kunna definiera filter." #: extensions/sievefilters/templatetags/sfilters_tags.py:16 msgid "New filters set" @@ -2204,84 +2119,88 @@ msgstr "Server svar" msgid "Cci" msgstr "BCC" -#: extensions/webmail/imaputils.py:259 +#: extensions/webmail/imaputils.py:237 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Det gick inte att hämta hierarki avgränsare" + +#: extensions/webmail/imaputils.py:278 #, python-format msgid "Connection to IMAP server failed: %s" msgstr "Anslutning till IMAP servern misslyckades: %s" -#: extensions/webmail/lib.py:353 +#: extensions/webmail/lib.py:358 msgid "wrote:" msgstr "skrev:" -#: extensions/webmail/lib.py:418 extensions/webmail/lib.py:422 +#: extensions/webmail/lib.py:423 extensions/webmail/lib.py:427 msgid "Original message" msgstr "Orginalmeddelande" -#: extensions/webmail/views.py:81 +#: extensions/webmail/views.py:86 #, python-format msgid "%(count)d message deleted" msgid_plural "%(count)d messages deleted" -msgstr[0] "" -msgstr[1] "" +msgstr[0] "%(count)d meddelande raderat" +msgstr[1] "%(count)d meddelanden raderade" -#: extensions/webmail/views.py:98 +#: extensions/webmail/views.py:103 msgid "Unknown action" msgstr "Okänd åtgärd" -#: extensions/webmail/views.py:111 lib/email_listing.py:137 +#: extensions/webmail/views.py:116 lib/email_listing.py:137 msgid "Empty mailbox" msgstr "Tom brevlåda" -#: extensions/webmail/views.py:128 -#: extensions/webmail/templatetags/webextras.py:169 +#: extensions/webmail/views.py:133 +#: extensions/webmail/templatetags/webextras.py:170 msgid "Create a new mailbox" msgstr "Skapa en ny brevlåda" -#: extensions/webmail/views.py:143 +#: extensions/webmail/views.py:149 msgid "Mailbox created" msgstr "Brevlåda skapad" -#: extensions/webmail/views.py:159 +#: extensions/webmail/views.py:165 msgid "Edit mailbox" msgstr "Redigera brevlåda" -#: extensions/webmail/views.py:173 +#: extensions/webmail/views.py:180 msgid "Mailbox updated" msgstr "Brevlåda uppdaterad" -#: extensions/webmail/views.py:207 extensions/webmail/views.py:388 -#: extensions/webmail/views.py:497 +#: extensions/webmail/views.py:214 extensions/webmail/views.py:396 +#: extensions/webmail/views.py:505 msgid "Bad request" msgstr "Ogiltig fråga" -#: extensions/webmail/views.py:238 +#: extensions/webmail/views.py:246 msgid "Failed to save attachment: " msgstr "Det gick inte att spara bilaga:" -#: extensions/webmail/views.py:241 +#: extensions/webmail/views.py:249 #, python-format msgid "Attachment is too big (limit: %s)" msgstr "Bilagan är för stor (max:% s)" -#: extensions/webmail/views.py:247 +#: extensions/webmail/views.py:255 #: extensions/webmail/templates/webmail/compose.html:15 #: extensions/webmail/templates/webmail/headers.html:4 msgid "Attachments" msgstr "Bilagor" -#: extensions/webmail/views.py:263 +#: extensions/webmail/views.py:271 msgid "Bad query" msgstr "Dålig fråga" -#: extensions/webmail/views.py:273 +#: extensions/webmail/views.py:281 msgid "Failed to remove attachment: " msgstr "Misslyckades med att radera bilaga:" -#: extensions/webmail/views.py:278 +#: extensions/webmail/views.py:286 msgid "Unknown attachment" msgstr "Okänd bilaa" -#: extensions/webmail/views.py:493 +#: extensions/webmail/views.py:501 msgid "Undefined action" msgstr "Odefinierad åtgärd" @@ -2312,7 +2231,7 @@ msgstr[1] " %(counter)s filer" msgid "Parent mailbox" msgstr "Brevlådas förälder" -#: extensions/webmail/templates/webmail/index.html:36 +#: extensions/webmail/templates/webmail/index.html:38 msgid "Mailbox quota" msgstr "Brevlåde kvota" @@ -2374,15 +2293,15 @@ msgstr "Som oläst" msgid "Compress folder" msgstr "Komprimera katalog" -#: extensions/webmail/templatetags/webextras.py:109 +#: extensions/webmail/templatetags/webextras.py:110 msgid "Empty folder" msgstr "Töm katalog" -#: extensions/webmail/templatetags/webextras.py:177 +#: extensions/webmail/templatetags/webextras.py:178 msgid "Edit the selected mailbox" msgstr "Redigera vald brevlåda" -#: extensions/webmail/templatetags/webextras.py:182 +#: extensions/webmail/templatetags/webextras.py:183 msgid "Remove the selected mailbox" msgstr "Radera vald brevlåda" @@ -2436,7 +2355,7 @@ msgstr "Den begärda sidan finns inte." msgid "An internal error occured." msgstr "Ett internt fel uppstod." -#: templates/nlayout.html:56 +#: templates/nlayout.html:62 msgid "Powered by Modoboa" msgstr "Powered by Modoboa" @@ -2470,11 +2389,11 @@ msgstr "Stäng" msgid "Welcome to Modoboa" msgstr "Välkommen till Modoboa" -#: templates/registration/login.html:33 +#: templates/registration/login.html:34 msgid "Remember me" msgstr "Kom ihåg mig" -#: templates/registration/login.html:35 +#: templates/registration/login.html:36 msgid "Log in" msgstr "Logga in" @@ -2490,7 +2409,7 @@ msgstr "Per användare inställningar" msgid "Prefered language" msgstr "Önskat språk" -#: userprefs/app_settings.py:26 +#: userprefs/app_settings.py:27 msgid "Prefered language to display pages" msgstr "Önskat språk för att visa sidor" @@ -2530,9 +2449,7 @@ msgstr "Vidarebefordra meddelande och spara kopia i din lokala brevlåda" msgid "" "You can't define a forward to a local destination. Please ask your " "administrator to create an alias instead." -msgstr "" -"Du kan inte definiera en vidarebefordring till en lokal destination. Be din " -"administratör att skapa ett alias istället." +msgstr "Du kan inte definiera en vidarebefordring till en lokal destination. Be din administratör att skapa ett alias istället." #: userprefs/views.py:32 msgid "You need a mailbox in order to define a forward" diff --git a/modoboa/static/js/confirmation.js b/modoboa/static/js/confirmation.js index e360eba04..59bd9314d 100644 --- a/modoboa/static/js/confirmation.js +++ b/modoboa/static/js/confirmation.js @@ -36,9 +36,11 @@ id: "confirmbox", 'class': "modal" }); + var question = (typeof this.options.question == 'function') + ? this.options.question.apply(this) : this.options.question; box.append($('
                          • diff --git a/modoboa/templates/common/parameters.html b/modoboa/templates/common/parameters.html index 98f4430d1..1a3c0ab16 100644 --- a/modoboa/templates/common/parameters.html +++ b/modoboa/templates/common/parameters.html @@ -1,4 +1,4 @@ -{% load libextras %} +{% load lib_tags %}