Merge branch 'master' into feature/score-and-sorting-in-quarantine

Conflicts:
	modoboa/extensions/amavis/views.py
This commit is contained in:
Antoine Nguyen
2013-11-03 14:49:46 +01:00
289 changed files with 13428 additions and 8552 deletions
+2 -1
View File
@@ -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 *
recursive-include modoboa/core/commands/templates *.tpl
recursive-include doc *
+8 -2
View File
@@ -11,8 +11,8 @@ as an administration panel or a webmail.
Modoboa integrates with well known software such as `Postfix
<http://postfix.org/>`_ or `Dovecot <http://dovecot.org/>`_. A SQL
database (`MySQL <http://www.mysql.com>`_, `PostgreSQL
<http://www.postgresql.org/>`_) is used as a central point of
communication between all components.
<http://www.postgresql.org/>`_ or `SQLite <http://www.sqlite.org>`_)
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
@@ -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()
+2 -2
View File
@@ -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
+5 -6
View File
@@ -4,9 +4,9 @@ Using the virtual machine
Introduction
============
A virtual machine with a ready-to-use *Modoboa* setup is available
`here <http://modoboa.org/resources/modoboa.vmdk.bz2>`_. It is composed
of the following components:
A virtual machine with a ready-to-use Modoboa setup is available `here
<http://modoboa.org/resources/modoboa.vmdk.bz2>`_. 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.
================ ======== ============================================
+1 -1
View File
@@ -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.
+3 -2
View File
@@ -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
===============
+3 -3
View File
@@ -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.
+8 -8
View File
@@ -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::
<user_that_runs_modoboa> ALL=(<mailboxes owner>) 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
<https://docs.djangoproject.com/en/dev/topics/http/sessions/?from=olddocs>`_
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 <http://www.python-ldap.org/>`_
* `Django LDAP authentication backend <http://pypi.python.org/pypi/django-auth-ldap>`_
@@ -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 <http://packages.python.org/django-auth-ldap/>`_.
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::
+40 -11
View File
@@ -30,8 +30,8 @@ You can choose between two options:
* Use the Python package available on the `PyPI <http://pypi.python.org/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 <http://www.virtualenv.org/en/latest/>`_ 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 <http://mysql-python.sourceforge.net/>`_ for `MySQL <http://www.mysql.com>`_
@@ -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``.
+14 -14
View File
@@ -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
<http://www.amavis.org/#doc>`_.
@@ -106,7 +106,7 @@ Release messages
To release messages, first take a look at `this page
<http://www.ijs.si/software/amavisd/amavisd-new-docs.html#quar-release>`_. 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 <http://amavis.org/README.customize.txt>`_ 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 <http://oss.oetiker.ch/rrdtool/>`_)and then
files (see `rrdtool <http://oss.oetiker.ch/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::
+82 -29
View File
@@ -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 <postfix_config>`. Look at the
``smtpd_recipient_restrictions`` setting.
Run ``modoboa-admin.py postfix_maps --dbtype <mysql|postgres|sqlite>
<tempdir>`` 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 <mysql|postgres> <tempdir>`` 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 <dovecot_quota>`
@@ -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
*<modoboa_dir>/static/stats* directory to *<modoboa_dir>/media/stats*
:file:`<modoboa_dir>/static/stats` directory to :file:`<modoboa_dir>/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 *<modoboa_dir>/templates/* directory, remove all symbolic links.
#. Download the latest release of ckeditor and extract it into *<modoboa_dir>/static/js/*. It should create a new directory named *ckeditor*.
#. Update the following variables inside *settings.py*::
#. Inside the :file:`<modoboa_dir>/templates/` directory, remove all symbolic links.
#. Download the latest release of ckeditor and extract it into :file:`<modoboa_dir>/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::
+119 -49
View File
@@ -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 <postfix_config>`).
Edit the crontab of the user who owns the mailboxes on the file system::
$ crontab -u <user> -e
And add the following line inside::
* * * * * python <modoboa_site>/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=<postgres socket> dbname=<database> user=<user> password=<password>
@@ -122,6 +163,23 @@ to your database engine.
iterate_query = SELECT email AS username FROM admin_user
SQLite users
------------
::
driver = sqlite
connect = <path to the sqlite db file>
default_pass_scheme = CRYPT
password_query = SELECT email AS user, password FROM admin_user WHERE email='%u' and is_active=1
user_query = SELECT '<mailboxes storage directory>/%d/%n' AS home, <uid> as uid, <gid> 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@<domain>
@@ -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 = <user owning mailboxes>
#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 = <driver>:/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=<db host> dbname=<db name> user=<db user> password=<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 <mysql|postgres> mapfiles
$ modoboa-admin.py postfix_maps --dbtype <mysql|postgres|sqlite> 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 = <vmail user id>
virtual_minimum_uid = <vmail user id>
virtual_gid_maps = static:<vmail group id>
virtual_uid_maps = static:<vmail user id>
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 = <driver>:/etc/postfix/sql-domains.cf
virtual_alias_domains = <driver>:/etc/postfix/sql-domain-aliases.cf
virtual_mailbox_maps = <driver>:/etc/postfix/sql-mailboxes.cf
virtual_alias_maps = <driver>:/etc/postfix/sql-aliases.cf,
<driver>:/etc/postfix/sql-domain-aliases-mailboxes.cf,
<driver>:/etc/postfix/sql-catchall-aliases.cf
smtpd_recipient_restrictions =
...
check_recipient_access <driver>:/etc/postfix/sql-maintain.cf
permit_mynetworks
...
# Stuff after
Then, edit the */etc/postfix/master.cf* file and add the following
Replace ``<driver>`` 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.
+201 -20
View File
@@ -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 / <path to your site's dir>/wsgi.py
</VirtualHost>
This is just one possible configuration.
This is just one possible configuration.
.. note::
*Django* 1.3 users, please consult this `page <https://docs.djangoproject.com/en/1.3/howto/deployment/modwsgi/>`_,
it contains an example *wsgi.py* file.
Django 1.3 users, please consult this `page <https://docs.djangoproject.com/en/1.3/howto/deployment/modwsgi/>`_,
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::
</Location>
</VirtualHost>
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 <http://nginx.org/>`_ is a really fast HTTP server. Associated
with `Green Unicorn <http://gunicorn.org/>`_, 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 <http://gunicorn.org/install.html>`_ and `install
nginx <http://wiki.nginx.org/Install>`_).
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
<http://wiki.nginx.org/Install>`__ and `gunicorn
<http://gunicorn.org/install.html>`_. 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 <modoboa dir>
$ gunicorn -c gunicorn.conf.py
$ gunicorn -c gunicorn.conf.py <modoboa dir>.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 <host fqdn>;
root <modoboa's root dir>;
access_log /var/log/nginx/<host fqdn>.access.log;
error_log /var/log/nginx/<host fqdn>.error.log;
ssl_certificate <ssl certificate for your site>;
ssl_certificate_key <ssl certificate key for your site>;
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 for your site>;
ssl_certificate_key <ssl certificate key for your site>;
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
<http://wiki.nginx.org/HttpSslModule#Generate_Certificates>`__
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
<http://wiki.nginx.org/Install>`__ and `uwsgi
<http://uwsgi-docs.readthedocs.org/en/latest/WSGIquickstart.html>`_.
Here is a sample nginx configuration::
server {
listen 443 ssl;
ssl on;
keepalive_timeout 70;
server_name <host fqdn>;
root <modoboa's settings dir>;
ssl_certificate <ssl certificate for your site>;
ssl_certificate_key <ssl certificate key for your site>;
access_log /var/log/nginx/<host fqdn>.access.log;
error_log /var/log/nginx/<host fqdn>.error.log;
location <modoboa's root url>/sitestatic/ {
autoindex on;
alias <location of sitestatic on your file system>;
}
# Whether or not Modoboa uses a media directory depends on how
# you configured Modoboa. It does not hurt to have this.
location <modoboa's root url>/media/ {
autoindex on;
alias <location of media on your file system>;
}
# 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 <modoba's root url>/ {
include uwsgi_params;
uwsgi_pass <uwsgi port>;
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 for your site>;
ssl_certificate_key <ssl certificate key for your site>;
uwsgi_param UWSGI_SCHEME https;
If you do plan to use SSL, you'll have to generate a certificate and a
key. `This article
<http://wiki.nginx.org/HttpSslModule#Generate_Certificates>`__
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:
``<modoboa's settings dir>``
Where Modoboa's :file:`settings.py` resides. This is also where the
:file:`sitestatic` and :file:`media` directories reside.
``<modoboa's root url>``
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 ``<modoboa's root url>`` 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 ``<modoboa root
url>`` is an empty string and can be deleted from the configuration
above. In this case, you probably do not need the ``alias``
directives.
``<uwsgi port>``
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/<app name>/socket
where ``<app name>`` is the name of the application.
Your uwsgi configuration should be::
[uwsgi]
# Not needed when using uwsgi from pip
# plugins = python
chdir = <modoboa's top dir>
module = <name>.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:
``<modoboa's top dir>``
The directory where :file:`manage.py` resides. This directory is the
parent of ``<modoboa's settings dir>``
``<name>``
The name that you passed to ``modoboa-admin.py deploy`` when you
created your Modoboa instance.
-32
View File
@@ -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")
-1
View File
@@ -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": []}}]
@@ -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}}]
-668
View File
@@ -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()
File diff suppressed because it is too large Load Diff
@@ -1,8 +0,0 @@
{% load i18n libextras %}{% load url from future %}
{% url "modoboa.admin.views.saveparameters" as action %}
<h2>{% trans "Parameters" %} <small>{% trans "Configure Modoboa" %}</small></h2><hr />
<form id="settings_form" method="POST" action="{% url 'modoboa.admin.views.saveparameters' %}" class="form-horizontal">{% csrf_token %}
{% include "common/parameters.html" %}
</form>
<hr>
<a href="#" class="btn btn-primary" id="update" name="update">{% trans 'Save' %}</a>
-344
View File
@@ -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 "<h5>%s</h5>" % (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 = """<div class='control-group'%s>
<label class="param-label" for="%s">%s</label>
""" % (extratags, name,
definition.has_key("label") and _(definition["label"])
or definition["name"])
result += "<div class='param-controls'>"
if definition["type"] in ["string", "int"]:
result += """
<input type='text' name='%s' id='%s' value='%s' />""" % (name, name, value)
if definition["type"] == "text":
result += "<textarea name='%s' id='%s'>%s</textarea>" \
% (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 += """<label for="%(id)s" class="radio inline">
<input type="radio" name="%(name)s" id="%(id)s" value="%(value)s"%(checked)s />
%(label)s
</label>
""" % dict(id="%s_%d" % (name, idx), name=name, value=v[0], label=v[1], checked=checked)
if definition["type"] == "list":
result += """
<select name='%s' id='%s'>""" % (name, name)
values = []
if definition.has_key("values"):
values = definition["values"]
for v in values:
selected = ""
if value == v[0]:
selected = " selected='selected'"
result += "<option value='%s'%s>%s</option>\n" % (v[0], selected, v[1].decode("utf-8"))
result += """
</select>
"""
if definition.has_key("help"):
result += """<a class="help" rel="popover" data-content="%s" data-original-title="%s">
<i class="icon-info-sign"></i>
</a>""" % (unicode(definition["help"]), _("Help"))
# result += """
# <span class="help-block">%s</span>
# """ % _(definition["help"])
result += """
</div>
</div>
"""
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 "<p class='%s'>%s</p>" % (classes[level], level)
@register.filter
def tohtml(message):
return re.sub("'(.*?)'", "<strong>\g<1></strong>", message)
-443
View File
@@ -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")
-889
View File
@@ -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:
<ul>
<li><em>account; loginname; password; first name; last name; enabled; group; address[, domain, ...]</em></li>
<li><em>alias; address; enabled; internal recipient</em></li>
<li><em>forward; address; enabled; external recipient</em></li>
<li><em>dlist; address; enabled; recipient; recipient; ...</em></li>
</ul>
<p>The first element of each line is mandatory and must be equal to one of the previous values.</p>
<p>You can use a different character as separator.</p>
""")
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:
<ul>
<li><em>domain; name; quota; enabled</em></li>
<li><em>domainalias; name; targeted domain; enabled</em></li>
</ul>
<p>The first element of each line is mandatory and must be equal to one of the previous values.</p>
<p>You can use a different character as separator.</p>
""")
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)
-17
View File
@@ -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
)
-3
View File
@@ -1,3 +0,0 @@
from django.db import models
# Create your models here.
-23
View File
@@ -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
"""}
+55
View File
@@ -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")
@@ -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")
)
+90
View File
@@ -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)
+88 -30
View File
@@ -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'
+2 -1
View File
@@ -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"
+85 -42
View File
@@ -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)
@@ -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': {
@@ -1,4 +1,3 @@
# coding: utf-8
"""
:mod:`exceptions` --- Custom exceptions of the admin. application
-----------------------------------------------------------------
+93
View File
@@ -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()
+35
View File
@@ -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))
+1
View File
@@ -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}}]
+71
View File
@@ -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
-88
View File
@@ -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)
@@ -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()
+161
View File
@@ -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']
@@ -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
View File
+483
View File
@@ -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)
+14
View File
@@ -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"]
@@ -1,6 +1,6 @@
{% load i18n %}{% load url from future %}
<h2>{% trans "Extensions" %} <small>{% trans "Choose the extension(s) to use" %}</small></h2><hr />
<form id="settings_form" method="POST" action="{% url 'modoboa.admin.views.saveextensions' %}">{% csrf_token %}
<form id="settings_form" method="POST" action="{% url 'modoboa.core.views.admin.saveextensions' %}">{% csrf_token %}
{{ extensions|safe }}
</form>
<a href="#" class="btn btn-primary" id="update" name="update">{% trans 'Save' %}</a>
@@ -1,4 +1,4 @@
{% load i18n libextras %}
{% load i18n core_tags lib_tags %}
<h2>{% trans "Information" %} <small>{% trans "Various information about Modoboa" %}</small></h2><hr>
<div class="alert alert-info">Modoboa <strong>{% get_version %}</strong> {% trans "is currently in use." %}</div>
@@ -1,4 +1,4 @@
{% load i18n admin_extras %}
{% load i18n core_tags %}
<h2>{% trans "Logs" %} <small>{% trans "Previously on Modoboa..." %}</small></h2><hr>
<table class="table">
<thead>
@@ -0,0 +1,7 @@
{% load i18n lib_tags %}{% load url from future %}
<h2>{% trans "Parameters" %} <small>{% trans "Configure Modoboa" %}</small></h2><hr />
<form id="settings_form" method="POST" action="{% url 'modoboa.core.views.admin.saveparameters' %}" class="form-horizontal">{% csrf_token %}
{% include "common/parameters.html" %}
</form>
<hr>
<a href="#" class="btn btn-primary" id="update" name="update">{% trans 'Save' %}</a>
@@ -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 %}
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}admin/css/settings.css" />
@@ -19,5 +19,5 @@ $(document).ready(function() {
});
</script>
{% endblock %}
{% block leftcol %}<div class="well sidebar-nav">{% settings_menu left_selection user %}</div>{% endblock %}
{% block leftcol %}<div class="well sidebar-nav">{% admin_menu left_selection user %}</div>{% endblock %}
{% block apparea %}<div id="settings_content"></div>{% endblock %}
@@ -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 %}
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}userprefs/css/userprefs.css" />
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}core/css/userprefs.css" />
<script type="text/javascript" src="{{ STATIC_URL }}js/history.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/listing.js"></script>
<script type="text/javascript" src="{{ STATIC_URL }}js/twocols_nav.js"></script>
@@ -1,7 +1,7 @@
{% load i18n %}{% load url from future %}
<h2>{% trans "Preferences" %} <small>{% trans "Customize Modoboa" %}</small></h2>
<hr>
<form class="form-horizontal" method="POST" action="{% url 'modoboa.userprefs.views.preferences' %}"
<form class="form-horizontal" method="POST" action="{% url 'modoboa.core.views.user.preferences' %}"
id="uprefs_form">{% csrf_token %}
{% include "common/parameters.html" %}
<hr>
@@ -1,7 +1,7 @@
{% load i18n libextras %}{% load url from future %}
{% load i18n lib_tags %}{% load url from future %}
<h2>{% trans "Profile" %} <small>{% trans "Update your personal information" %}</small></h2>
<hr>
<form class="form-horizontal" method="POST" action="{% url 'modoboa.userprefs.views.profile' %}"
<form class="form-horizontal" method="POST" action="{% url 'modoboa.core.views.user.profile' %}"
id="uprefs_form">{% csrf_token %}
{% render_form form %}
<div class="form-actions">
+280
View File
@@ -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 "<p class='%s'>%s</p>" % (classes[level], level)
@register.filter
def tohtml(message):
return re.sub("'(.*?)'", "<strong>\g<1></strong>", 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 """
<script type="text/javascript">
$(document).ready(function() {
$('body').notify('%s', '%s', %s);
});
</script>
""" % (level, text, timeout)
+42
View File
@@ -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
)
+21
View File
@@ -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'),
)
+128
View File
@@ -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),
})
+6 -7
View File
@@ -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")
+73
View File
@@ -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)
})
})
+19 -16
View File
@@ -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 """<script type="text/javascript">
$(document).ready(function() {
-91
View File
@@ -1,91 +0,0 @@
# coding: utf-8
import os
import sys
import re
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
version = None
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)
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)
return instance.infos()
def load_all(self):
from modoboa.admin.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()
+202
View File
@@ -0,0 +1,202 @@
# coding: utf-8
from django.utils.translation import ugettext as _, ugettext_lazy
from django.core.urlresolvers import reverse
from modoboa.core.extensions import ModoExtension, exts_pool
from modoboa.lib import parameters, events
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.emailutils import split_mailbox
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import (
Domain, DomainAlias, Mailbox, Alias
)
admin_events = [
"CreateDomain",
"DomainModified",
"DeleteDomain",
"DomainAliasCreated",
"DomainAliasDeleted",
"CreateMailbox",
"DeleteMailbox",
"ModifyMailbox",
"MailboxAliasCreated",
"MailboxAliasDeleted",
"ExtraDomainForm",
"FillDomainInstances",
"ExtraAccountForm",
"CheckExtraAccountForm",
"FillAccountInstances",
]
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 modoboa.extensions.admin.app_settings import AdminParametersForm
parameters.register(AdminParametersForm, ugettext_lazy("Administration"))
events.declare(admin_events)
def destroy(self):
parameters.unregister()
exts_pool.register_extension(AdminConsole, show=False)
@events.observe("ExtraUprefsRoutes")
def extra_routes():
return [(r'^user/forward/',
'modoboa.extensions.admin.views.user.forward'), ]
@events.observe("AdminMenuDisplay")
def admin_menu(target, user):
if target != "top_menu":
return []
entries = []
if user.has_perm("admin.view_domains"):
entries += [
{"name" : "domains",
"url" : reverse("modoboa.extensions.admin.views.domain.domains"),
"label" : _("Domains")}
]
if user.has_perm("core.add_user") or user.has_perm("admin.add_alias"):
entries += [
{"name" : "identities",
"url" : reverse("modoboa.extensions.admin.views.identity.identities"),
"label" : _("Identities")},
]
return entries
@events.observe("UserMenuDisplay")
def user_menu(target, user):
if target != "uprefs_menu":
return []
if not user.mailbox_set.count():
return []
return [
{"name": "forward",
"class": "ajaxlink",
"url": "forward/",
"label": ugettext_lazy("Forward")}
]
@events.observe("RoleChanged")
def grant_access_to_all_objects(user, role):
from django.contrib.contenttypes.models import ContentType
from modoboa.lib.permissions import grant_access_to_objects
from modoboa.core.models import User
if role != "SuperAdmins":
return
grant_access_to_objects(
user, User.objects.all(),
ContentType.objects.get_for_model(User)
)
grant_access_to_objects(
user, Domain.objects.all(),
ContentType.objects.get_for_model(Domain)
)
grant_access_to_objects(
user, DomainAlias.objects.all(),
ContentType.objects.get_for_model(DomainAlias)
)
grant_access_to_objects(
user, Mailbox.objects.all(),
ContentType.objects.get_for_model(Mailbox)
)
grant_access_to_objects(
user, Alias.objects.all(),
ContentType.objects.get_for_model(Alias)
)
@events.observe("AccountExported")
def export_admin_domains(admin):
if admin.group != "DomainAdmins":
return []
return [dom.name for dom in Domain.objects.get_for_admin(admin)]
@events.observe("AccountImported")
def import_account_mailbox(user, account, row):
account.email = row[0].strip()
if account.email != "":
account.save()
mailbox, domname = split_mailbox(account.email)
try:
domain = Domain.objects.get(name=domname)
except Domain.DoesNotExist:
raise AdminError(
_("Account import failed (%s): domain does not exist" % account.username)
)
if not user.can_access(domain):
raise PermDeniedException
mb = Mailbox(address=mailbox, domain=domain,
user=account, use_domain_quota=True)
mb.set_quota(override_rules=user.has_perm("admin.change_domain"))
mb.save(creator=user)
if account.group == "DomainAdmins":
for domname in row[1:]:
try:
dom = Domain.objects.get(name=domname.strip())
except Domain.DoesNotExist:
continue
dom.add_admin(account)
@events.observe("AccountAutoCreated")
def account_auto_created(user):
from modoboa.core.models import User
from modoboa.lib.permissions import grant_access_to_object
sadmins = User.objects.filter(is_superuser=True)
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)
@events.observe("UserLogin")
def user_logged_in(request, username, password):
from modoboa.lib.cryptutils import encrypt
if request.user.mailbox_set.count():
request.session["password"] = encrypt(password)
@events.observe("AccountDeleted")
def account_deleted(account, byuser, **kwargs):
if not account.mailbox_set.count():
return
mb = account.mailbox_set.all()[0]
if not byuser.can_access(mb):
raise PermDeniedException
keep_mb_dir = kwargs.get("keep_mb_dir", True)
mb.delete(keepdir=keep_mb_dir)
+48
View File
@@ -0,0 +1,48 @@
from django import forms
from django.utils.translation import ugettext_lazy
from modoboa.lib.formutils import YesNoField, SeparatorField
from modoboa.lib.sysutils import exec_cmd
from modoboa.lib import parameters
class AdminParametersForm(parameters.AdminParametersForm):
app = "admin"
mbsep = 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"),
)
# Visibility rules
visibility_rules = {
"mailboxes_owner": "handle_mailboxes=yes",
}
def __init__(self, *args, **kwargs):
super(AdminParametersForm, self).__init__(*args, **kwargs)
hide_fields = False
try:
code, version = exec_cmd("dovecot --version")
except OSError:
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"]
+19
View File
@@ -0,0 +1,19 @@
# coding: utf-8
"""
:mod:`exceptions` --- Custom exceptions of the admin. application
-----------------------------------------------------------------
"""
from modoboa.lib.exceptions import ModoboaException
class AdminError(ModoboaException):
"""
Just a Custom exception to identify errors coming from the admin
application.
"""
pass
class BadDestination(Exception):
pass
+58
View File
@@ -0,0 +1,58 @@
from modoboa.core.factories import PermissionFactory, UserFactory
from . import models
class DomainFactory(PermissionFactory):
FACTORY_FOR = models.Domain
quota = 10
enabled = True
class MailboxFactory(PermissionFactory):
FACTORY_FOR = models.Mailbox
quota = 10
class AliasFactory(PermissionFactory):
FACTORY_FOR = models.Alias
def populate_database():
"""Create test data.
2 domains, 1 domain admin, 2 users
"""
dom = DomainFactory.create(name="test.com")
admin = UserFactory(
username="admin@test.com", groups=('DomainAdmins', ), password='{PLAIN}toto'
)
MailboxFactory(address='admin', domain=dom, user=admin)
account = UserFactory.create(
username="user@test.com", groups=('SimpleUsers',),
)
MailboxFactory.create(address='user', domain=dom, user=account)
fwd = AliasFactory.create(
address='forward', domain=dom, extmboxes='user@external.com'
)
al = AliasFactory.create(
address='alias', domain=dom
)
al.mboxes.add(account.mailbox_set.all()[0])
AliasFactory.create(
address='postmaster', domain=dom, extmboxes='toto@titi.com,test@truc.fr'
)
dom.add_admin(admin)
dom2 = DomainFactory.create(name='test2.com', quota=0)
admin = UserFactory.create(
username='admin@test2.com', groups=('DomainAdmins',), password='{PLAIN}toto'
)
MailboxFactory.create(address='admin', domain=dom2, user=admin)
u = UserFactory.create(
username='user@test2.com', groups=('SimpleUsers',)
)
MailboxFactory.create(address='user', domain=dom2, user=u)
dom2.add_admin(admin)
@@ -0,0 +1 @@
[{"pk": 1, "model": "auth.group", "fields": {"name": "DomainAdmins", "permissions": [["add_user", "core", "user"], ["change_user", "core", "user"], ["delete_user", "core", "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": []}}]
@@ -0,0 +1 @@
[{"pk": 1, "model": "core.user", "fields": {"username": "admin", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": false, "is_local": true, "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}}]
@@ -0,0 +1,17 @@
from .domain import DomainFormGeneral, DomainFormOptions, DomainForm
from .account import (
AccountFormGeneral, AccountFormMail, AccountPermissionsForm,
AccountForm
)
from .alias import AliasForm
from .forward import ForwardForm
from .import_ import ImportDataForm, ImportIdentitiesForm
from .export import ExportDataForm, ExportDomainsForm, ExportIdentitiesForm
__all__ = [
'DomainFormGeneral', 'DomainFormOptions', 'DomainForm',
'AccountFormGeneral', 'AccountFormMail', 'AccountPermissionsForm',
'AccountForm', 'AliasForm', 'ImportDataForm', 'ImportIdentitiesForm',
'ExportDataForm', 'ExportDomainsForm', 'ExportIdentitiesForm',
'ForwardForm'
]
+316
View File
@@ -0,0 +1,316 @@
from django import forms
from django.utils.translation import ugettext as _, ugettext_lazy
from django.http import QueryDict
from modoboa.lib import events, parameters
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.permissions import get_account_roles
from modoboa.lib.emailutils import split_mailbox
from modoboa.lib.formutils import (
DomainNameField, DynamicForm, TabForms
)
from modoboa.core.models import User
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import (
Domain, Mailbox, Alias
)
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:
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(int_rcpts=[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(Domain.objects.get_for_admin(self.account)):
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 Domain.objects.get_for_admin(self.account)]
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 Domain.objects.get_for_admin(self.account):
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("core.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()
+120
View File
@@ -0,0 +1,120 @@
from django import forms
from django.utils.translation import ugettext as _, ugettext_lazy
from django.http import QueryDict
from modoboa.lib.emailutils import split_mailbox
from modoboa.lib.formutils import (
DynamicForm
)
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import (
Domain, Mailbox, Alias
)
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(int_rcpts=self.int_rcpts, ext_rcpts=self.ext_rcpts)
self.save_m2m()
return alias
+227
View File
@@ -0,0 +1,227 @@
from django import forms
from django.http import QueryDict
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib import events
from modoboa.lib.formutils import (
DomainNameField, YesNoField, DynamicForm, TabForms
)
from modoboa.core.models import User
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import (
Domain, DomainAlias, Mailbox, Alias, Quota
)
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 update_mailbox_quotas(self, domain):
"""Update all quota records associated to this domain
This method must be called only when a domain gets renamed. As
the primary key used for a quota is an email address, rename a
domain will change all associated email addresses, so it will
change the primary keys used for quotas. The consequence is we
can't issue regular UPDATE queries using the .save() method of
a Quota instance (it will trigger an INSERT as the primary key
has changed).
So, we use this ugly hack to bypass this behaviour. It is not
perfomant at all as it will generate one query per quota
record to update.
"""
for q in Quota.objects.filter(username__contains="@%s" % self.oldname).values('username'):
username = q['username'].replace('@%s' % self.oldname, '@%s' % domain.name)
Quota.objects.filter(username=q['username']).update(username=username)
def save(self, user, commit=True):
"""Custom save method
Updating a domain may have consequences on other objects
(domain alias, mailbox, quota). The most tricky part concerns
quotas update.
"""
d = super(DomainFormGeneral, self).save(commit=False)
if commit:
old_mail_homes = None
if self.oldname is not None and d.name != self.oldname:
d.name = self.oldname
old_mail_homes = \
dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all())
d.name = self.cleaned_data['name']
d.save()
Mailbox.objects.filter(domain=d, use_domain_quota=True) \
.update(quota=d.quota)
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()
if old_mail_homes is not None:
self.update_mailbox_quotas(d)
for mb in d.mailbox_set.all():
mb.rename_dir(old_mail_homes[mb.id])
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(int_rcpts=[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)
+37
View File
@@ -0,0 +1,37 @@
from django import forms
from django.utils.translation import ugettext_lazy
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"
+37
View File
@@ -0,0 +1,37 @@
from django import forms
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.lib.emailutils import split_mailbox
from modoboa.extensions.admin.exceptions import BadDestination
from modoboa.extensions.admin.models import (
Domain
)
class ForwardForm(forms.Form):
dest = forms.CharField(
label=ugettext_lazy("Recipient(s)"),
widget=forms.Textarea,
required=False,
help_text=ugettext_lazy("Indicate one or more recipients separated by a ','")
)
keepcopies = forms.BooleanField(
label=ugettext_lazy("Keep local copies"),
required=False,
help_text=ugettext_lazy("Forward messages and store copies into your local mailbox")
)
def parse_dest(self):
self.dests = []
rawdata = self.cleaned_data["dest"].strip()
if rawdata == "":
return
for d in rawdata.split(","):
local_part, domname = split_mailbox(d)
if not local_part or not domname or not len(domname):
raise BadDestination("Invalid mailbox syntax for %s" % d)
try:
Domain.objects.get(name=domname)
except Domain.DoesNotExist:
self.dests += [d]
else:
raise BadDestination(_("You can't define a forward to a local destination. Please ask your administrator to create an alias instead."))
+29
View File
@@ -0,0 +1,29 @@
from django import forms
from django.utils.translation import ugettext_lazy
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")
)
+106
View File
@@ -0,0 +1,106 @@
# coding: utf-8
from functools import wraps
from django.db.models import Q
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.contrib.contenttypes.models import ContentType
from modoboa.core.models import User
from modoboa.lib import parameters
from modoboa.lib.exceptions import ModoboaException
from modoboa.lib.emailutils import split_mailbox
from modoboa.extensions.admin.models import Alias
def needs_mailbox():
"""Check if the current user owns at least one mailbox
Some applications (the webmail for example) need a mailbox to
work.
"""
def decorator(f):
@wraps(f)
def wrapped_f(request, *args, **kwargs):
if request.user.mailbox_set.count():
return f(request, *args, **kwargs)
raise ModoboaException()
return wrapped_f
return decorator
def get_sort_order(qdict, default, allowed_values=None):
"""Return a sort order from a querydict object
:param QueryDict qdict: the object to analyse
:param string default: the default sort order if no one is found
:param list allowed_values: an optional list of allowed values
:return: a 2uple of strings
"""
sort_order = qdict.get("sort_order", default)
if sort_order.startswith("-"):
sort_dir = "-"
sort_order = sort_order[1:]
else:
sort_dir = ""
if allowed_values is not None and not sort_order in allowed_values:
return (default, "")
return (sort_order, sort_dir)
def get_listing_page(objects, pagenum):
paginator = Paginator(
objects, int(parameters.get_admin("ITEMS_PER_PAGE", app="core"))
)
try:
page = paginator.page(int(pagenum))
except (EmptyPage, PageNotAnInteger, ValueError):
page = paginator.page(paginator.num_pages)
return page
def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None):
"""Return all the identities owned by a user.
:param user: the desired user
:param str searchquery: search pattern
:param list idtfilter: identity type filters
:param list grpfilter: group names filters
:return: a queryset
"""
from itertools import chain
accounts = []
if idtfilter is None or not idtfilter or idtfilter == "account":
ids = user.objectaccess_set \
.filter(content_type=ContentType.objects.get_for_model(user)) \
.values_list('object_id', flat=True)
q = Q(pk__in=ids)
if searchquery is not None:
q &= Q(username__icontains=searchquery) \
| Q(email__icontains=searchquery)
if grpfilter is not None and grpfilter:
if grpfilter == "SuperAdmins":
q &= Q(is_superuser=True)
else:
q &= Q(groups__name=grpfilter)
accounts = User.objects.select_related().filter(q)
aliases = []
if idtfilter is None or not idtfilter \
or (idtfilter in ["alias", "forward", "dlist"]):
alct = ContentType.objects.get_for_model(Alias)
ids = user.objectaccess_set.filter(content_type=alct) \
.values_list('object_id', flat=True)
q = Q(pk__in=ids)
if searchquery is not None:
if '@' in searchquery:
local_part, domname = split_mailbox(searchquery)
if local_part:
q &= Q(address__icontains=local_part)
if domname:
q &= Q(domain__name__icontains=domname)
else:
q &= Q(address__icontains=searchquery) | \
Q(domain__name__icontains=searchquery)
aliases = Alias.objects.select_related().filter(q)
if idtfilter is not None and idtfilter:
aliases = [al for al in aliases if al.type == idtfilter]
return chain(accounts, aliases)
@@ -0,0 +1,92 @@
import os
import logging
from optparse import make_option
from django.core.management.base import BaseCommand
from modoboa.lib import parameters
from modoboa.lib.sysutils import exec_cmd
from modoboa.extensions.admin import AdminConsole
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import MailboxOperation
class OperationError(Exception):
pass
class Command(BaseCommand):
help = 'Handles rename and delete operations on mailboxes'
option_list = BaseCommand.option_list + (
make_option(
'--pidfile', default='/tmp/handle_mailbox_operations.pid',
help='Path to the file that will contain the PID of this process'
),
)
def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs)
self.logger = logging.getLogger('modoboa.admin')
def rename_mailbox(self, operation):
if not os.path.exists(operation.argument):
return
new_mail_home = operation.mailbox.mail_home
dirname = os.path.dirname(new_mail_home)
if not os.path.exists(dirname):
try:
os.makedirs(dirname)
except os.error as e:
raise OperationError(str(e))
code, output = exec_cmd(
"mv %s %s" % (operation.argument, new_mail_home)
)
if code:
raise OperationError(output)
def delete_mailbox(self, operation):
if not os.path.exists(operation.argument):
return
code, output = exec_cmd(
"rm -r %s" % operation.argument
)
if code:
raise OperationError(output)
def check_pidfile(self, path):
"""Check if this command is already running
:param str path: path to the file containing the PID
:return: a boolean, True means we can go further
"""
if os.path.exists(path):
with open(path) as fp:
pid = fp.read().strip()
code, output = exec_cmd(
"grep handle_mailbox_operations /proc/%s/cmdline" % pid
)
if not code:
return False
with open(path, 'w') as fp:
print >> fp, os.getpid()
return True
def handle(self, *args, **options):
AdminConsole().load()
if parameters.get_admin("HANDLE_MAILBOXES") == 'no':
return
if not self.check_pidfile(options['pidfile']):
return
for ope in MailboxOperation.objects.all():
try:
f = getattr(self, '%s_mailbox' % ope.type)
except AttributeError:
continue
try:
f(ope)
except (OperationError, AdminError) as e:
self.logger.critical('%s failed (reason: %s)',
ope, str(e).encode('utf-8'))
else:
self.logger.info('%s succeed', ope)
ope.delete()
os.unlink(options['pidfile'])
@@ -3,7 +3,6 @@ import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from modoboa.lib.compat import user_model_name
class Migration(SchemaMigration):
@@ -26,7 +25,7 @@ class Migration(SchemaMigration):
('quota', self.gf('django.db.models.fields.IntegerField')()),
('full_address', self.gf('django.db.models.fields.CharField')(max_length=150)),
('gid', self.gf('django.db.models.fields.IntegerField')()),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm[user_model_name])),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('address', self.gf('django.db.models.fields.CharField')(max_length=100)),
('path', self.gf('django.db.models.fields.CharField')(max_length=255)),
('password', self.gf('django.db.models.fields.CharField')(max_length=100)),
@@ -44,41 +43,6 @@ class Migration(SchemaMigration):
('address', self.gf('django.db.models.fields.CharField')(max_length=100)),
))
db.send_create_signal('admin', ['Alias'])
if user_model_name != 'admin.User':
return
# Adding model 'User'
db.create_table(u'admin_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)),
))
db.send_create_signal(u'admin', ['User'])
# Adding M2M table for field groups on 'User'
db.create_table(u'admin_user_groups', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('user', models.ForeignKey(orm[u'admin.user'], null=False)),
('group', models.ForeignKey(orm[u'auth.group'], null=False))
))
db.create_unique(u'admin_user_groups', ['user_id', 'group_id'])
# Adding M2M table for field user_permissions on 'User'
db.create_table(u'admin_user_user_permissions', (
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)),
('user', models.ForeignKey(orm[u'admin.user'], null=False)),
('permission', models.ForeignKey(orm[u'auth.permission'], null=False))
))
db.create_unique(u'admin_user_user_permissions', ['user_id', 'permission_id'])
def backwards(self, orm):
@@ -90,12 +54,6 @@ class Migration(SchemaMigration):
# Deleting model 'Alias'
db.delete_table('admin_alias')
if user_model_name != 'admin.User':
return
db.delete_table('admin_user')
db.delete_table('admin_user_groups')
db.delete_table('admin_user_user_permissions')
models = {
@@ -126,7 +84,7 @@ class Migration(SchemaMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
@@ -141,8 +99,8 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'Meta': {'object_name': 'User'},
'auth.user': {
'Meta': {'object_name': 'User', 'db_table': "'auth_user'"},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
@@ -3,7 +3,6 @@ import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from modoboa.lib.compat import user_model_name
class Migration(SchemaMigration):
@@ -63,7 +62,7 @@ class Migration(SchemaMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
@@ -78,7 +77,7 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
@@ -3,7 +3,6 @@ import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from modoboa.lib.compat import user_model_name
class Migration(SchemaMigration):
@@ -59,7 +58,7 @@ class Migration(SchemaMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
@@ -74,7 +73,7 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
@@ -3,22 +3,23 @@ import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from modoboa.lib.compat import user_model_name
#from django.contrib.auth.models import Permission
#from django.contrib.contenttypes.models import ContentType
class Migration(DataMigration):
def forwards(self, orm):
perm_ct = ContentType.objects.get(app_label="auth", model="permission")
#perm_ct = orm["contenttypes.ContentType"].objects.get(app_label="auth", model="permission")
view_perms = Permission(name="View permissions", codename="view_permissions",
content_type=perm_ct)
#perm_ct = ContentType.objects.get(app_label="auth", model="permission")
perm_ct = orm["contenttypes.ContentType"].objects.get(app_label="auth", model="permission")
view_perms = orm['auth.Permission'](
name="View permissions", codename="view_permissions",
content_type=perm_ct
)
view_perms.save()
def backwards(self, orm):
p = orm.Permission.objects.get(codename="view_permissions")
p = orm['auth.Permission'].objects.get(codename="view_permissions")
p.delete()
@@ -56,7 +57,7 @@ class Migration(DataMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
@@ -71,7 +72,7 @@ class Migration(DataMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
@@ -3,14 +3,13 @@ import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from modoboa.lib.compat import user_model_name, user_table_name
class Migration(SchemaMigration):
def forwards(self, orm):
db.alter_column(user_table_name, "username", models.CharField(unique=True, max_length=254))
db.alter_column(user_table_name, "email", models.CharField(max_length=254))
db.alter_column('auth_user', "username", models.CharField(unique=True, max_length=254))
db.alter_column('auth_user', "email", models.CharField(max_length=254))
def backwards(self, orm):
raise RuntimeError("Cannot revert this migration")
@@ -49,7 +48,7 @@ class Migration(SchemaMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
@@ -64,7 +63,7 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
@@ -3,7 +3,6 @@ import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from modoboa.lib.compat import user_model_name
class Migration(SchemaMigration):
@@ -71,7 +70,7 @@ class Migration(SchemaMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
@@ -86,7 +85,7 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
@@ -3,7 +3,6 @@ import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from modoboa.lib.compat import user_model_name
class Migration(SchemaMigration):
@@ -71,7 +70,7 @@ class Migration(SchemaMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
@@ -86,7 +85,7 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
@@ -3,7 +3,6 @@ import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
from modoboa.lib.compat import user_model_name
class Migration(DataMigration):
@@ -56,7 +55,7 @@ class Migration(DataMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
@@ -71,7 +70,7 @@ class Migration(DataMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
@@ -3,7 +3,6 @@ import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from modoboa.lib.compat import user_model_name
class Migration(SchemaMigration):
@@ -62,7 +61,7 @@ class Migration(SchemaMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
@@ -77,7 +76,7 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
@@ -3,7 +3,6 @@ import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
from modoboa.lib.compat import user_model_name
class Migration(DataMigration):
@@ -59,7 +58,7 @@ class Migration(DataMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'auth.group': {
'Meta': {'object_name': 'Group'},
@@ -74,7 +73,7 @@ class Migration(DataMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
@@ -3,7 +3,6 @@ import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
from modoboa.lib.compat import user_model_name
class Migration(SchemaMigration):
@@ -94,7 +93,7 @@ class Migration(SchemaMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'admin.objectdates': {
'Meta': {'object_name': 'ObjectDates'},
@@ -115,7 +114,7 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
@@ -3,7 +3,6 @@ import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
from modoboa.lib.compat import user_model_name
def create_od(orm, obj):
@@ -74,7 +73,7 @@ class Migration(DataMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'admin.objectdates': {
'Meta': {'object_name': 'ObjectDates'},
@@ -95,7 +94,7 @@ class Migration(DataMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
@@ -3,7 +3,6 @@ import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
from modoboa.lib.compat import user_model_name
class Migration(DataMigration):
@@ -63,7 +62,7 @@ class Migration(DataMigration):
'path': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'quota': ('django.db.models.fields.IntegerField', [], {}),
'uid': ('django.db.models.fields.IntegerField', [], {}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['%s']" % user_model_name})
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'admin.objectdates': {
'Meta': {'object_name': 'ObjectDates'},
@@ -84,7 +83,7 @@ class Migration(DataMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
user_model_name: {
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),

Some files were not shown because too many files have changed in this diff Show More