mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
Merge branch 'master' into feature/score-and-sorting-in-quarantine
Conflicts: modoboa/extensions/amavis/views.py
This commit is contained in:
+2
-1
@@ -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
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
===============
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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::
|
||||
|
||||
@@ -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``.
|
||||
|
||||
@@ -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::
|
||||
|
||||
@@ -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::
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 +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}}]
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -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
|
||||
"""}
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
+9
-14
@@ -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
|
||||
-----------------------------------------------------------------
|
||||
@@ -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()
|
||||
@@ -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))
|
||||
@@ -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}}]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
+4
-5
@@ -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()
|
||||
@@ -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
|
||||
Executable → Regular
@@ -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)
|
||||
@@ -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
-1
@@ -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
-1
@@ -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>
|
||||
+2
-2
@@ -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 %}
|
||||
+2
-2
@@ -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
-1
@@ -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>
|
||||
+2
-2
@@ -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">
|
||||
@@ -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)
|
||||
@@ -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
|
||||
)
|
||||
@@ -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'),
|
||||
)
|
||||
@@ -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),
|
||||
})
|
||||
Executable → Regular
+6
-7
@@ -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")
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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'
|
||||
]
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
@@ -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."))
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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'])
|
||||
+4
-46
@@ -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'}),
|
||||
+2
-3
@@ -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'}),
|
||||
+2
-3
@@ -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'}),
|
||||
+11
-10
@@ -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'}),
|
||||
+4
-5
@@ -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'}),
|
||||
+2
-3
@@ -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'}),
|
||||
+2
-3
@@ -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'}),
|
||||
+2
-3
@@ -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'}),
|
||||
+2
-3
@@ -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'}),
|
||||
+2
-3
@@ -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'}),
|
||||
+2
-3
@@ -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'}),
|
||||
+2
-3
@@ -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'}),
|
||||
+2
-3
@@ -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
Reference in New Issue
Block a user