mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
Merge pull request #707 from tonioo/split_applications
Split applications
This commit is contained in:
+10
-4
@@ -25,7 +25,9 @@ import sys, os
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = []
|
||||
extensions = [
|
||||
'sphinx.ext.intersphinx',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['.templates']
|
||||
@@ -41,16 +43,16 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Modoboa'
|
||||
copyright = u'2014, Antoine Nguyen'
|
||||
copyright = u'2015, Antoine Nguyen'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '1.2'
|
||||
version = '1.3'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '1.2.1'
|
||||
release = '1.3.0'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@@ -219,3 +221,7 @@ man_pages = [
|
||||
('index', 'modoboa', u'Modoboa Documentation',
|
||||
[u'Antoine Nguyen'], 1)
|
||||
]
|
||||
|
||||
intersphinx_mapping = {
|
||||
'amavis': ('http://modoboa-amavis.readthedocs.org/en/latest/', None)
|
||||
}
|
||||
|
||||
@@ -619,14 +619,16 @@ name, the model name and the permission name. Example::
|
||||
GetExtraRoles
|
||||
=============
|
||||
|
||||
Let extensions define new administrative roles.
|
||||
Let extensions define new administrative roles (will be used to create
|
||||
or modify an account).
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user): pass
|
||||
def callback(user, account): pass
|
||||
|
||||
* ``user`` is a ``User`` instance corresponding to the currently
|
||||
logged in user
|
||||
* ``account`` is the account being modified (None on creation)
|
||||
|
||||
Callbacks listening to this event must return a list of 2uple (two
|
||||
strings) which respect the following format: ``(value, label)``.
|
||||
@@ -922,11 +924,12 @@ account.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(account, role): pass
|
||||
def callback(user, role, account): pass
|
||||
|
||||
* ``user`` is the ``User`` instance corresponding to the currently
|
||||
logged in user
|
||||
* ``role`` is the role ``user`` tries to set
|
||||
* ``account`` is the account being modified (None on creation)
|
||||
|
||||
Must return a list containing ``True`` or ``False`` to indicate if
|
||||
this user can is allowed to set ``role``.
|
||||
|
||||
@@ -93,37 +93,31 @@ Deployment
|
||||
`modoboa-admin.py`, a command line tool, lets you deploy a
|
||||
*ready-to-use* Modoboa site using only one instruction::
|
||||
|
||||
$ modoboa-admin.py deploy modoboa_example --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.
|
||||
$ modoboa-admin.py deploy modoboa_example --collectstatic [--dburl default:database-url] [--extensions extensions]
|
||||
|
||||
.. note::
|
||||
|
||||
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, Ansible or whatever), it's possible to supply the database
|
||||
credentials as commandline arguments.
|
||||
|
||||
You can see the complete option list by running the following command::
|
||||
|
||||
$ modoboa-admin.py help deploy
|
||||
By default, only the core application of Modoboa is installed. To
|
||||
install extensions, use the ``--extensions`` option which accepts a
|
||||
list of extension names as argument (``--extensions ext1 ext2 ...``).
|
||||
If you want to use install all extensions, just use the ``all``
|
||||
shortcut like this ``--extensions all``.
|
||||
|
||||
.. note::
|
||||
|
||||
`--dburl database-url` for the modoboa database credentials
|
||||
`--amavis_dburl database-url` for the amavis database credentials
|
||||
You can specify more than one database connection using the
|
||||
``--dburl`` option. Multiple connections are differentiated by a
|
||||
prefix. The primary connection must use the ``default:`` prefix (as
|
||||
shown in the example above). For the `amavis extension
|
||||
<http://modoboa-amavis.readthedocs.org>`_ extension, use the
|
||||
``amavis:`` prefix like this ``--dburl amavis:<database url>``.
|
||||
|
||||
Your database-url should meet the following syntax:
|
||||
``scheme://[user:pass@][host:port]/dbname``
|
||||
Your database url should meet the following syntax
|
||||
``scheme://[user:pass@][host:port]/dbname`` **OR**
|
||||
``sqlite:////full/path/to/your/database/file.sqlite``.
|
||||
|
||||
**or**
|
||||
Available schemes are:
|
||||
|
||||
``sqlite:////full/path/to/your/database/file.sqlite``
|
||||
|
||||
Available `schemes` are:
|
||||
* postgres
|
||||
* postgresql
|
||||
* postgis
|
||||
@@ -131,6 +125,16 @@ You can see the complete option list by running the following command::
|
||||
* mysql2
|
||||
* sqlite
|
||||
|
||||
The command will ask you a few questions, answer them and you're
|
||||
done. You can now go to the :ref:`first_use` section.
|
||||
|
||||
In case you need a **silent installation** (e.g. if you're using
|
||||
Salt-Stack, Ansible or whatever), it's possible to supply the database
|
||||
credentials as commandline arguments.
|
||||
|
||||
You can see the complete option list by running the following command::
|
||||
|
||||
$ modoboa-admin.py help deploy
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
@@ -33,9 +33,8 @@ your instance was first deployed::
|
||||
If you customized your configuration file (:file:`settings.py`) with
|
||||
non-standard settings, you'll have to re-apply them.
|
||||
|
||||
Then, run ``modoboa-admin.py deploy``::
|
||||
|
||||
$ modoboa-admin.py deploy <modoboa_instance_dir> --collectstatic [--with-amavis] [--dburl database-url] [--amavis_dburl database-url] [--domain hostname] [--lang lang] [--timezone timezone]
|
||||
Finally, run the ``deploy`` comamand. Make sure to consult the
|
||||
:ref:`deployment` section to know more about the available options.
|
||||
|
||||
If you prefer the manual way, check if
|
||||
:ref:`specific_upgrade_instructions` are required according to the
|
||||
@@ -50,6 +49,64 @@ you did choose. See :ref:`webservers` for more details.
|
||||
Specific upgrade instructions
|
||||
*****************************
|
||||
|
||||
1.3.0
|
||||
=====
|
||||
|
||||
This release does not bring awesome new features but it is a necessary
|
||||
bridge to the future of Modoboa. All extensions now have their own git
|
||||
repository and the deploy process has been updated to reflect this
|
||||
change.
|
||||
|
||||
Another important update is the use of Django 1.7. Besides its new
|
||||
features, the migration system has been reworked and is now more
|
||||
robust than before.
|
||||
|
||||
Before we begin with the procedure, here is a table showing old
|
||||
extension names and their new name:
|
||||
|
||||
+----------------------------------------+--------------------------+--------------------------+
|
||||
|Old name |New package name |New module name |
|
||||
+========================================+==========================+==========================+
|
||||
|modoboa.extensions.admin |modoboa-admin |modoboa_admin |
|
||||
+----------------------------------------+--------------------------+--------------------------+
|
||||
|modoboa.extensions.limits |modoboa-admin-limits |modoboa_admin_limits |
|
||||
+----------------------------------------+--------------------------+--------------------------+
|
||||
|modoboa.extensions.postfix_autoreply |modoboa-postfix-autoreply |modoboa_postfix_autoreply |
|
||||
+----------------------------------------+--------------------------+--------------------------+
|
||||
|modoboa.extensions.postfix_relay_domains|modoboa-admin-relaydomains|modoboa_admin_relaydomains|
|
||||
+----------------------------------------+--------------------------+--------------------------+
|
||||
|modoboa.extensions.radicale |modoboa-radicale |modoboa_radicale |
|
||||
+----------------------------------------+--------------------------+--------------------------+
|
||||
|modoboa.extensions.sievefilters |modoboa-sievefilters |modoboa_sievefilters |
|
||||
+----------------------------------------+--------------------------+--------------------------+
|
||||
|modoboa.extensions.stats |modoboa-stats |modoboa_stats |
|
||||
+----------------------------------------+--------------------------+--------------------------+
|
||||
|modoboa.extensions.webmail |modoboa-webmail |modoboa_webmail |
|
||||
+----------------------------------------+--------------------------+--------------------------+
|
||||
|
||||
Here are the required steps:
|
||||
|
||||
#. Install the extensions using pip (look at the second column in the table above)::
|
||||
|
||||
$ pip install <the extensions you want>
|
||||
|
||||
#. Remove ``south`` from ``INSTALLED_APPS``
|
||||
|
||||
#. Rename old extension names inside ``MODOBOA_APPS`` (look at the third column in the table above)
|
||||
|
||||
#. Remove ``modoboa.lib.middleware.ExtControlMiddleware`` from ``MIDDLEWARE_CLASSES``
|
||||
|
||||
#. Change ``DATABASE_ROUTERS`` to::
|
||||
|
||||
DATABASE_ROUTERS = ["modoboa_amavis.dbrouter.AmavisRouter"]
|
||||
|
||||
#. Run the following commands::
|
||||
|
||||
$ cd <modoboa_instance_dir>
|
||||
$ python manage.py migrate
|
||||
$ python manage.py load_initial_data
|
||||
$ python manage.py collectstatic
|
||||
|
||||
1.2.0
|
||||
=====
|
||||
|
||||
@@ -105,7 +162,7 @@ If you plan to use the Radicale extension:
|
||||
A new feature allows administrators and users to manually train
|
||||
SpamAssassin in order to customize its behaviour.
|
||||
|
||||
Check :ref:`sa_manual_learning` to know more about this feature.
|
||||
Check :ref:`amavis:sa_manual_learning` to know more about this feature.
|
||||
|
||||
1.1.6: Few bugfixes
|
||||
===================
|
||||
|
||||
@@ -422,9 +422,9 @@ 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|sqlite> mapfiles
|
||||
$ modoboa-admin.py postfix_maps --dbtype <mysql|postgres|sqlite> --extensions all <directory>
|
||||
|
||||
:file:`mapfiles` is the directory where the files will be
|
||||
``<directory>`` is the directory where the files will be
|
||||
stored. Answer the few questions and you're done.
|
||||
|
||||
.. _postfix_config:
|
||||
|
||||
+1
-301
@@ -4,304 +4,4 @@
|
||||
Amavisd-new frontend
|
||||
####################
|
||||
|
||||
This plugin provides a simple management frontend for `amavisd-new
|
||||
<http://www.amavis.org>`_. The supported features are:
|
||||
|
||||
* SQL quarantine management : available to administrators or users,
|
||||
possibility to delete or release messages
|
||||
* Per domain customization (using policies): specify how amavisd-new
|
||||
will handle traffic
|
||||
|
||||
.. note::
|
||||
|
||||
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).
|
||||
|
||||
.. note::
|
||||
|
||||
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**.
|
||||
|
||||
Quick Amavis setup
|
||||
==================
|
||||
|
||||
By default, amavis doesn't use a database. To configure this
|
||||
behaviour, you first need to create a dedicated database. This step is a
|
||||
bit manual since no *ready-to-use* SQL schema is provided by
|
||||
amavis. The information is located inside README files, one for `MySQL
|
||||
<http://www.amavis.org/README.sql-mysql.txt>`_ and one for `PostgreSQL
|
||||
<http://www.amavis.org/README.sql-pg.txt>`_.
|
||||
|
||||
Then, you must tell amavis to use this database for lookups and
|
||||
quarantined messages storing. Here is a working configuration sample::
|
||||
|
||||
@lookup_sql_dsn =
|
||||
(['DBI:<driver>:database=<database>;host=<dbhost>;port=<dbport>', '<dbuser>', '<password>]']);
|
||||
|
||||
@storage_sql_dsn =
|
||||
(['DBI:<driver>:database=<database>;host=<dbhost>;port=<dbport>', '<dbuser>', '<password>]']);
|
||||
|
||||
# PostgreSQL users NEED this parameter!
|
||||
# MySQL users only need this parameter is email addresses are stored
|
||||
# using the VARBINARY type.
|
||||
$sql_allow_8bit_address = 1;
|
||||
|
||||
$virus_quarantine_method = 'sql:';
|
||||
$spam_quarantine_method = 'sql:';
|
||||
$banned_files_quarantine_method = 'sql:';
|
||||
$bad_header_quarantine_method = 'sql:';
|
||||
|
||||
$virus_quarantine_to = 'virus-quarantine';
|
||||
$banned_quarantine_to = 'banned-quarantine';
|
||||
$bad_header_quarantine_to = 'bad-header-quarantine';
|
||||
$spam_quarantine_to = 'spam-quarantine';
|
||||
|
||||
Replace values between ``<>`` by yours. To know how to configure
|
||||
amavis to allow quarantined messages release, read this :ref:`section
|
||||
<amavis_release>`.
|
||||
|
||||
.. note::
|
||||
|
||||
Amavis configuration allows for separate lookup and storage
|
||||
databases but Modoboa doesn't support it yet.
|
||||
|
||||
Connect Modoboa and Amavis
|
||||
==========================
|
||||
|
||||
You must tell to Modoboa where it can find the amavis
|
||||
database. Inside :file:`settings.py`, add a new connection to the
|
||||
``DATABASES`` variable like this::
|
||||
|
||||
DATABASES = {
|
||||
# Stuff before
|
||||
#
|
||||
"amavis": {
|
||||
"ENGINE" : "<your value>",
|
||||
"HOST" : "<your value>",
|
||||
"NAME" : "<your value>",
|
||||
"USER" : "<your value>",
|
||||
"PASSWORD" : "<your value>"
|
||||
}
|
||||
}
|
||||
|
||||
Replace values between ``<>`` with yours.
|
||||
|
||||
Cleanup
|
||||
-------
|
||||
|
||||
Storing quarantined messages to a database can quickly become a
|
||||
perfomance killer. Modoboa provides a simple script to periodically
|
||||
purge the quarantine database. To use it, add the following line
|
||||
inside root's crontab::
|
||||
|
||||
0 0 * * * <modoboa_site>/manage.py qcleanup
|
||||
#
|
||||
# Or like this if you use a virtual environment:
|
||||
# 0 0 * * * <virtualenv path/bin/python> <modoboa_site>/manage.py qcleanup
|
||||
|
||||
Replace ``modoboa_site`` with the path of your Modoboa instance.
|
||||
|
||||
By default, messages older than 14 days are automatically purged. You
|
||||
can modify this value by changing the ``MAX_MESSAGES_AGE`` parameter
|
||||
in the online panel.
|
||||
|
||||
.. _amavis_release:
|
||||
|
||||
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
|
||||
AM.PDP protocol. This protocol is used to send requests.
|
||||
|
||||
Below is an example of a working configuration::
|
||||
|
||||
$interface_policy{'SOCK'} = 'AM.PDP-SOCK';
|
||||
$interface_policy{'9998'} = 'AM.PDP-INET';
|
||||
|
||||
$policy_bank{'AM.PDP-SOCK'} = {
|
||||
protocol => 'AM.PDP',
|
||||
auth_required_release => 0,
|
||||
};
|
||||
$policy_bank{'AM.PDP-INET'} = {
|
||||
protocol => 'AM.PDP',
|
||||
inet_acl => [qw( 127.0.0.1 [::1] )],
|
||||
};
|
||||
|
||||
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
|
||||
the *release server* by modifying the following parameters in the
|
||||
online panel:
|
||||
|
||||
+--------------------+--------------------+------------------------+
|
||||
|Name |Description |Default value |
|
||||
+====================+====================+========================+
|
||||
|Amavis connection |Mode used to access |unix |
|
||||
|mode |the PDP server | |
|
||||
+--------------------+--------------------+------------------------+
|
||||
|PDP server address |PDP server address |localhost |
|
||||
| |(if inet mode) | |
|
||||
+--------------------+--------------------+------------------------+
|
||||
|PDP server port |PDP server port (if | |
|
||||
| |inet mode) 9998 | |
|
||||
+--------------------+--------------------+------------------------+
|
||||
|PDP server socket |Path to the PDP |/var/amavis/amavisd.sock|
|
||||
| |server socket (if | |
|
||||
| |unix mode) | |
|
||||
+--------------------+--------------------+------------------------+
|
||||
|
||||
Deferred release
|
||||
----------------
|
||||
|
||||
By default, simple users are not allowed to release messages
|
||||
themselves. They are only allowed to send release requests to
|
||||
administrators.
|
||||
|
||||
As administrators are not always available or logged into Modoboa, a
|
||||
notification tool is available. It sends reminder e-mails to every
|
||||
administrators or domain administrators. To use it, add the following
|
||||
example line to root's crontab::
|
||||
|
||||
0 12 * * * <modoboa_site>/manage.py amnotify --baseurl='<modoboa_url>'
|
||||
#
|
||||
# Or like this if you use a virtual environment:
|
||||
# 0 12 * * * <virtualenv path/bin/python> <modoboa_site>/manage.py amnotify --baseurl='<modoboa_url>'
|
||||
|
||||
You are free to change the frequency.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to let users release their messages alone (not
|
||||
recommended), go to the admin panel.
|
||||
|
||||
The following parameters are available to let you customize this
|
||||
feature:
|
||||
|
||||
+--------------------+--------------------+------------------------+
|
||||
|Name |Description |Default value |
|
||||
+====================+====================+========================+
|
||||
|Check requests |Interval between two|30 |
|
||||
|interval |release requests | |
|
||||
| |checks | |
|
||||
+--------------------+--------------------+------------------------+
|
||||
|Allow direct release|Allow users to |no |
|
||||
| |directly release | |
|
||||
| |their messages | |
|
||||
+--------------------+--------------------+------------------------+
|
||||
|Notifications sender|The e-mail address |notification@modoboa.org|
|
||||
| |used to send | |
|
||||
| |notitications | |
|
||||
+--------------------+--------------------+------------------------+
|
||||
|
||||
.. _selfservice:
|
||||
|
||||
Self-service mode
|
||||
=================
|
||||
|
||||
The *self-service* mode lets users act on quarantined messages without
|
||||
beeing authenticated. They can:
|
||||
|
||||
* View messages
|
||||
* Remove messages
|
||||
* Release messages (or send release requests)
|
||||
|
||||
To access a specific message, they only need the following information:
|
||||
|
||||
* Message's unique identifier
|
||||
* Message's secret identifier
|
||||
|
||||
This information is controlled by *amavis*, which is in charge of
|
||||
notifying users when new messages are put into quarantine. Each
|
||||
notification (one per message) must embark a direct link containing
|
||||
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
|
||||
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.
|
||||
|
||||
Here is a link example::
|
||||
|
||||
http://<modoboa_url>/quarantine/%i/?rcpt=%R&secret_id=[:secret_id]
|
||||
|
||||
.. _sa_manual_learning:
|
||||
|
||||
Manual SpamAssassin learning
|
||||
============================
|
||||
|
||||
It is possible to manually train `SpamAssassin
|
||||
<http://spamassassin.apache.org/>`_ using the quarantine's content. By
|
||||
train, we mean:
|
||||
|
||||
* Mark message(s) as spam (false negative(s))
|
||||
|
||||
* Mark message(s) as non-spam (false positive(s))
|
||||
|
||||
This feature is available to all users (from super administrators to
|
||||
simple users) but not enabled by default.
|
||||
|
||||
SpamAssassin configuration
|
||||
--------------------------
|
||||
|
||||
For better performance and to enable the per-user level, SpamAssassin
|
||||
must store bayes information into a SQL database.
|
||||
|
||||
Create a new database and a new user/password (using your favorite
|
||||
database server) and edit the default configuration file
|
||||
(:file:`/etc/spamassassin/local.cf`) to add the following lines
|
||||
inside:
|
||||
|
||||
.. sourcecode:: perl
|
||||
|
||||
bayes_store_module Mail::SpamAssassin::BayesStore::<Driver>
|
||||
bayes_sql_dsn <DSN>
|
||||
bayes_sql_username <db username>
|
||||
bayes_sql_password <db password>
|
||||
|
||||
Replace values between ``<>`` by yours. Possible values for ``Driver``
|
||||
are ``PgSQL`` or ``MySQL`` (non exhaustive list). The syntax for
|
||||
``DSN`` depends on the driver you choose. Please consult the official
|
||||
documentation.
|
||||
|
||||
Enable the feature through Modoboa
|
||||
----------------------------------
|
||||
|
||||
Manual learning is disabled by default. You can activate it through
|
||||
the administration panel (*Modoboa > Paremeters > Amavis*). There two
|
||||
learning levels:
|
||||
|
||||
#. Global: available to administrators only. A single (global) bayes
|
||||
database is shared between everyone.
|
||||
|
||||
#. Per domain: available to administrators and domain
|
||||
administrators. Each domain can have a dedicated database.
|
||||
|
||||
#. Per user: each user can create its own database to customize the
|
||||
way SpamAssassin will detect spam.
|
||||
|
||||
The domain and user levels are not activated by default, dedicated
|
||||
parameters are available through the panel.
|
||||
|
||||
.. note::
|
||||
|
||||
Domain and user databases are only created the first time someone
|
||||
calls the learning feature through the quarantine.
|
||||
|
||||
.. warning::
|
||||
|
||||
A bayes database needs to reach pre-defined thresholds before it
|
||||
can be used by SpamAssassin. The default values are **200** spams
|
||||
and **200** hams.
|
||||
|
||||
You will find other paramaters related to this feature. You won't need
|
||||
to change them most of the time, unless SpamAssassin is hosted on a
|
||||
different machine than Modoboa. (in this case, ``spamc`` will be used
|
||||
instead of ``sa-learn``).
|
||||
`Official documentation <http://modoboa-amavis.readthedocs.org>`_.
|
||||
|
||||
+1
-12
@@ -2,15 +2,4 @@
|
||||
Per-admin limits
|
||||
################
|
||||
|
||||
This plugin offers a way to define limits about how many objects
|
||||
(aliases, mailboxes) a domain administrator can create.
|
||||
|
||||
It also brings a new administrative role: ``Reseller``. A reseller is a domain
|
||||
administrator that can also manipulate domains and assign permissions
|
||||
to domain administrators.
|
||||
|
||||
If you don't want to limit a particular object type, just set the
|
||||
associated value to -1.
|
||||
|
||||
Default limits applied to new administrators can be changed through
|
||||
the *Modoboa > Parameters > Limits* page.
|
||||
`Official documentation <http://modoboa-admin-limits.readthedocs.org>`_.
|
||||
|
||||
@@ -4,48 +4,4 @@
|
||||
Postfix auto-reply messages
|
||||
###########################
|
||||
|
||||
This plugin let users define an auto-reply message (*vacation*). It is
|
||||
based on Postfix capabilities.
|
||||
|
||||
The user that executes the autoreply script needs to access
|
||||
: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
|
||||
configuration files as follows:
|
||||
|
||||
``/etc/postfix/main.cf``::
|
||||
|
||||
transport_maps = <driver>:/etc/postfix/sql-autoreplies-transport.cf
|
||||
virtual_alias_maps = <driver>:/etc/postfix/sql-aliases.cf
|
||||
<driver>:/etc/postfix/sql-domain-aliases-mailboxes.cf,
|
||||
<driver>:/etc/postfix/sql-autoreplies.cf,
|
||||
<driver>:/etc/postfix/sql-catchall-aliases.cf
|
||||
|
||||
.. note::
|
||||
|
||||
The order used to define alias maps is important, please respect it
|
||||
|
||||
``/etc/postfix/master.cf``::
|
||||
|
||||
autoreply unix - n n - - pipe
|
||||
flags= user=vmail:<group> argv=python <modoboa_site>/manage.py autoreply $sender $mailbox
|
||||
|
||||
Replace ``<driver>`` by the name of the database you
|
||||
use. ``<modoboa_site>`` is the path of your Modoboa instance.
|
||||
|
||||
Then, create the requested map files::
|
||||
|
||||
$ modoboa-admin.py postfix_maps mapfiles --categories autoreply
|
||||
|
||||
`mapfiles` is the directory where the files will be stored. Answer the
|
||||
few questions and you're done.
|
||||
|
||||
.. note::
|
||||
|
||||
Auto-reply messages are just sent one time per sender for a
|
||||
pre-defined time period. By default, this period is equal to 1 day
|
||||
(86400s), you can adjust this value by modifying the **Automatic
|
||||
reply timeout** parameter available in the online panel.
|
||||
`Official documentation <http://modoboa-postfix-autoreply.readthedocs.org>`_.
|
||||
|
||||
@@ -2,39 +2,4 @@
|
||||
Postfix relay domains support
|
||||
#############################
|
||||
|
||||
This plugin adds the support for relay domains using postfix. You can
|
||||
use it when the MTA managed by Modoboa is not the final destination of
|
||||
one or several domains.
|
||||
|
||||
If activated, two new objects will be available from the *Domains*
|
||||
listing page: *relay domain* and *relay domain alias*.
|
||||
|
||||
The extension is compatible with the *amavis* and *limits*
|
||||
ones. Resellers will be able to create both new objects.
|
||||
|
||||
Replace <driver> by the name of the database you use.To tell Postfix this feature exists, you must generate two new map
|
||||
files and then update your configuration.
|
||||
|
||||
To generate the map files, run the following command::
|
||||
|
||||
$ modoboa-admin.py postfix_maps --categories relaydomains --dbtype <the database you use> <path>
|
||||
|
||||
Replace values between ``<>`` by yours.
|
||||
|
||||
Edit the :file:`/etc/postfix/main.cf` file and copy the following
|
||||
lines inside::
|
||||
|
||||
relay_domains = <driver>:/etc/postfix/sql-relaydomains.cf
|
||||
transport_maps =
|
||||
<driver>:/etc/postfix/sql-relaydomains-transport.cf
|
||||
<driver>:/etc/postfix/sql-relaydomain-aliases-transport.cf
|
||||
|
||||
smtpd_recipient_restrictions =
|
||||
permit_mynetworks
|
||||
reject_unauth_destination
|
||||
check_recipient_access
|
||||
<driver>:/etc/postfix/sql-relay-recipient-verification.cf
|
||||
|
||||
Replace ``<driver>`` by the name of the database you use.
|
||||
|
||||
Reload postfix.
|
||||
`Official documentation <http://modoboa-admin-relaydomains.readthedocs.org>`_.
|
||||
|
||||
@@ -2,45 +2,4 @@
|
||||
Radicale frontend
|
||||
#################
|
||||
|
||||
This plugin provides a simple management frontend for `Radicale
|
||||
<http://radicale.org/>`_, a complete CalDAV (calendar) and CardDAV
|
||||
(contact) server solution.
|
||||
|
||||
Features currently supported are:
|
||||
|
||||
* Private calendar creation (available for simple users)
|
||||
* Rights management for private calendars
|
||||
* Shared calendar creation by domain (available for domain administrators)
|
||||
|
||||
.. note::
|
||||
|
||||
This plugin requires Radicale 0.9 or higher.
|
||||
|
||||
Setup
|
||||
=====
|
||||
|
||||
Once `Radicale is installed
|
||||
<http://radicale.org/user_documentation/>`_ on your server, you must
|
||||
tell Modoboa where it can find the file used to store rules.
|
||||
|
||||
Go to the *Modoboa > Parameters > Radicale* panel and fill the
|
||||
**Radicale rights file path** setting (an absolute path is required).
|
||||
|
||||
When the configuration is done, Modoboa will completly handles the
|
||||
file's content. It means every manual modification you could made on
|
||||
this file would be overriden.
|
||||
|
||||
To do so, a new cron job must be created. You can use the following
|
||||
example::
|
||||
|
||||
*/2 * * * * <modoboa_site>/manage.py generate_rights
|
||||
#
|
||||
# Or like this if you use a virtual environment:
|
||||
# */2 * * * * <virtualenv path/bin/python> <modoboa_site>/manage.py generate_rights
|
||||
|
||||
.. note::
|
||||
|
||||
In some cases, the modifications you make on Modoboa may not be
|
||||
applied to the rights file. In order to force the generation,
|
||||
manually run the ``generate_rights`` command using the ``--force``
|
||||
option.
|
||||
`Official documentation <http://modoboa-radicale.readthedocs.org>`_.
|
||||
|
||||
@@ -2,42 +2,4 @@
|
||||
Sieve filters
|
||||
#############
|
||||
|
||||
This plugin let users create server-side message filters, using the
|
||||
`sievelib module <http://pypi.python.org/pypi/sievelib>`_ (which
|
||||
provides Sieve and ManageSieve clients).
|
||||
|
||||
Two working modes are available:
|
||||
|
||||
* A raw mode: you create filters using the Sieve language directly
|
||||
(advanced users)
|
||||
* An assisted mode: you create filters using an intuitive form
|
||||
|
||||
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
|
||||
:ref:`dovecot` to know how to enable those features).
|
||||
|
||||
.. note::
|
||||
The sieve filters plugin requires that the :ref:`webmail` plugin is
|
||||
activated and configured.
|
||||
|
||||
Go the online panel and modify the following parameters in order to
|
||||
communicate with the *ManageSieve* server:
|
||||
|
||||
+--------------------+--------------------+--------------------+
|
||||
|Name |Description |Default value |
|
||||
+====================+====================+====================+
|
||||
|Server address |Address of your |127.0.0.1 |
|
||||
| |MANAGESIEVE server | |
|
||||
+--------------------+--------------------+--------------------+
|
||||
|Server port |Listening port of |4190 |
|
||||
| |your MANAGESIEVE | |
|
||||
| |server | |
|
||||
+--------------------+--------------------+--------------------+
|
||||
|Connect using |Use the STARTTLS |no |
|
||||
|STARTTLS |extension | |
|
||||
+--------------------+--------------------+--------------------+
|
||||
|Authentication |Prefered |auto |
|
||||
|mechanism |authentication | |
|
||||
| |mechanism | |
|
||||
+--------------------+--------------------+--------------------+
|
||||
`Official documentation <http://modoboa-sievefilters.readthedocs.org>`_.
|
||||
|
||||
@@ -4,44 +4,4 @@
|
||||
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
|
||||
generates graphics in PNG format.
|
||||
|
||||
To use it, go to the online parameters panel and adapt the following
|
||||
ones to your environment:
|
||||
|
||||
+--------------------+--------------------+--------------------------+
|
||||
|Name |Description |Default value |
|
||||
+====================+====================+==========================+
|
||||
|Path to the log file|Path to log file |/var/log/mail.log |
|
||||
| |used to collect | |
|
||||
| |statistics | |
|
||||
+--------------------+--------------------+--------------------------+
|
||||
|Directory to store |Path to directory |/tmp/modoboa |
|
||||
|RRD files |where RRD files are | |
|
||||
| |stored | |
|
||||
+--------------------+--------------------+--------------------------+
|
||||
|Directory to store |Path to directory |<modoboa_site>/media/stats|
|
||||
|PNG files |where PNG files are | |
|
||||
| |stored | |
|
||||
+--------------------+--------------------+--------------------------+
|
||||
|
||||
Make sure the directory that will contain RRD files exists. If not,
|
||||
create it before going further. For example (according to the previous
|
||||
parameters)::
|
||||
|
||||
$ mkdir /tmp/modoboa
|
||||
|
||||
To finish, you need to collect information periodically in order to
|
||||
feed the RRD files. Add the following line into root's crontab::
|
||||
|
||||
*/5 * * * * <modoboa_site>/manage.py logparser &> /dev/null
|
||||
#
|
||||
# Or like this if you use a virtual environment:
|
||||
# 0/5 * * * * <virtualenv path/bin/python> <modoboa_site>/manage.py logparser &> /dev/null
|
||||
|
||||
Replace ``<modoboa_site>`` with the path of your Modoboa instance.
|
||||
|
||||
Graphics will be automatically created after each parsing.
|
||||
`Official documentation <http://modoboa-stats.readthedocs.org>`_.
|
||||
|
||||
+1
-65
@@ -4,68 +4,4 @@
|
||||
Webmail
|
||||
#######
|
||||
|
||||
Modoboa provides a simple webmail:
|
||||
|
||||
* Browse, read and compose messages, attachments are supported
|
||||
* HTML messages are supported
|
||||
* `CKeditor <http://ckeditor.com/>`_ integration
|
||||
* Manipulate mailboxes (create, move, remove)
|
||||
* Quota display
|
||||
|
||||
To use it, go to the online panel and modify the following parameters
|
||||
to communicate with your *IMAP* server (under *IMAP settings*):
|
||||
|
||||
+--------------------+--------------------+--------------------+
|
||||
|Name |Description |Default value |
|
||||
+====================+====================+====================+
|
||||
|Server address |Address of your IMAP|127.0.0.1 |
|
||||
| |server | |
|
||||
+--------------------+--------------------+--------------------+
|
||||
|Use a secured |Use a secured |no |
|
||||
|connection |connection to access| |
|
||||
| |IMAP server | |
|
||||
+--------------------+--------------------+--------------------+
|
||||
|Server port |Listening port of |143 |
|
||||
| |your IMAP server | |
|
||||
+--------------------+--------------------+--------------------+
|
||||
|
||||
Do the same to communicate with your SMTP server (under *SMTP settings*):
|
||||
|
||||
+--------------------+--------------------+--------------------+
|
||||
|Name |Description |Default value |
|
||||
+====================+====================+====================+
|
||||
|Server address |Address of your SMTP|127.0.0.1 |
|
||||
| |server | |
|
||||
+--------------------+--------------------+--------------------+
|
||||
|Secured connection |Use a secured |None |
|
||||
|mode |connection to access| |
|
||||
| |SMTP server | |
|
||||
+--------------------+--------------------+--------------------+
|
||||
|Server port |Listening port of |25 |
|
||||
| |your SMTP server | |
|
||||
+--------------------+--------------------+--------------------+
|
||||
|Authentication |Server needs |no |
|
||||
|required |authentication | |
|
||||
+--------------------+--------------------+--------------------+
|
||||
|
||||
.. note::
|
||||
|
||||
The size of each attachment sent with messages is limited. You can
|
||||
change the default value by modifying the **Maximum attachment
|
||||
size** parameter.
|
||||
|
||||
Using CKeditor
|
||||
==============
|
||||
|
||||
Modoboa supports CKeditor to compose HTML messages. To use it, first
|
||||
download it from `the official website <http://ckeditor.com/>`_, then
|
||||
extract the tarball::
|
||||
|
||||
$ cd <modoboa_site_dir>
|
||||
$ tar xzf /path/to/ckeditor/tarball.tag.gz -C sitestatic/js/
|
||||
|
||||
And you're done!
|
||||
|
||||
Now, each user has the possibility to choose between CKeditor and the
|
||||
raw text editor to compose their messages. (see *User > Settings >
|
||||
Preferences > Webmail*)
|
||||
`Official documentation <http://modoboa-webmail.readthedocs.org>`_.
|
||||
|
||||
@@ -45,11 +45,6 @@ BASE_EVENTS = [
|
||||
]
|
||||
|
||||
PERMISSIONS = {
|
||||
"DomainAdmins": [
|
||||
["core", "user", "add_user"],
|
||||
["core", "user", "change_user"],
|
||||
["core", "user", "delete_user"],
|
||||
],
|
||||
"SimpleUsers": []
|
||||
}
|
||||
|
||||
@@ -72,15 +67,16 @@ def check_for_new_version(request, include_all):
|
||||
"""
|
||||
Check if a new version of Modoboa is available.
|
||||
"""
|
||||
from modoboa.core.utils import new_version_available
|
||||
from modoboa.core.utils import check_for_updates
|
||||
|
||||
if not request.user.is_superuser:
|
||||
return []
|
||||
if new_version_available(request) is None:
|
||||
status, extensions = check_for_updates(request)
|
||||
if not status:
|
||||
return [{"id": "newversionavailable"}] if include_all else []
|
||||
return [{
|
||||
"id": "newversionavailable",
|
||||
"url": reverse("core:index") + "#info/",
|
||||
"text": _("New Modoboa version available"),
|
||||
"text": _("One or more updates are available"),
|
||||
"level": "info",
|
||||
}]
|
||||
|
||||
@@ -5,9 +5,14 @@
|
||||
import getpass
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import subprocess
|
||||
try:
|
||||
import pip
|
||||
except ImportError:
|
||||
sys.stderr.write("Error: pip is required to install extensions.\n")
|
||||
sys.exit(2)
|
||||
|
||||
import django
|
||||
from django.core import management
|
||||
@@ -16,6 +21,7 @@ from django.template import Context, Template
|
||||
import dj_database_url
|
||||
|
||||
from modoboa.core.commands import Command
|
||||
from modoboa.lib.api_client import ModoAPIClient
|
||||
|
||||
DBCONN_TPL = """
|
||||
'{{ conn_name }}': {
|
||||
@@ -45,20 +51,13 @@ class DeployCommand(Command):
|
||||
super(DeployCommand, self).__init__(*args, **kwargs)
|
||||
self._parser.add_argument('name', type=str,
|
||||
help='The name of your Modoboa instance')
|
||||
self._parser.add_argument(
|
||||
'--with-amavis', action='store_true', default=False,
|
||||
help='Include amavis configuration'
|
||||
)
|
||||
self._parser.add_argument(
|
||||
'--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')
|
||||
'--dburl', type=str, nargs="+", default=None,
|
||||
help='A database-url with a name')
|
||||
self._parser.add_argument(
|
||||
'--domain', type=str, default=None,
|
||||
help='The domain under which you want to deploy modoboa')
|
||||
@@ -74,6 +73,14 @@ class DeployCommand(Command):
|
||||
'--devel', action='store_true', default=False,
|
||||
help='Create a development instance'
|
||||
)
|
||||
self._parser.add_argument(
|
||||
'--extensions', type=str, nargs='*',
|
||||
help="The list of extension to deploy"
|
||||
)
|
||||
self._parser.add_argument(
|
||||
'--dont-install-extensions', action='store_true', default=False,
|
||||
help='Do not install extensions using pip'
|
||||
)
|
||||
|
||||
def _exec_django_command(self, name, cwd, *args):
|
||||
"""Run a django command for the freshly created project
|
||||
@@ -136,6 +143,29 @@ class DeployCommand(Command):
|
||||
info['PASSWORD'] = getpass.getpass('Password: ')
|
||||
return info
|
||||
|
||||
def install_extensions(self, extensions):
|
||||
"""Install one or more extensions.
|
||||
|
||||
Return the list of extensions providing settings we must
|
||||
include in the final configuration.
|
||||
|
||||
"""
|
||||
if "all" in extensions:
|
||||
official_exts = ModoAPIClient().list_extensions()
|
||||
extensions = [extension["name"] for extension in official_exts]
|
||||
|
||||
pip_args = ["install"] + extensions
|
||||
pip.main(pip_args)
|
||||
extra_settings = []
|
||||
for extension in extensions:
|
||||
extension = extension.replace("-", "_")
|
||||
module = __import__(extension, locals(), globals(), [])
|
||||
basedir = os.path.dirname(module.__file__)
|
||||
if not os.path.exists("{0}/settings.py".format(basedir)):
|
||||
continue
|
||||
extra_settings.append(extension)
|
||||
return extra_settings
|
||||
|
||||
def handle(self, parsed_args):
|
||||
django.setup()
|
||||
management.call_command(
|
||||
@@ -144,40 +174,24 @@ class DeployCommand(Command):
|
||||
path = "%(name)s/%(name)s" % {'name': parsed_args.name}
|
||||
sys.path.append(parsed_args.name)
|
||||
|
||||
t = Template(DBCONN_TPL)
|
||||
|
||||
conn_tpl = Template(DBCONN_TPL)
|
||||
connections = {}
|
||||
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'
|
||||
for dburl in parsed_args.dburl:
|
||||
conn_name, url = dburl.split(":", 1)
|
||||
info = dj_database_url.config(default=url)
|
||||
# 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(conn_name)
|
||||
# If we set this earlier, our fallback method will never
|
||||
# be triggered
|
||||
info['conn_name'] = conn_name
|
||||
connections[conn_name] = conn_tpl.render(Context(info))
|
||||
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
|
||||
connections["default"] = conn_tpl.render(
|
||||
Context(self.ask_db_info()))
|
||||
|
||||
if parsed_args.domain:
|
||||
allowed_host = parsed_args.domain
|
||||
@@ -188,6 +202,10 @@ class DeployCommand(Command):
|
||||
if not allowed_host:
|
||||
allowed_host = "localhost"
|
||||
|
||||
extra_settings = []
|
||||
if parsed_args.dont_install_extensions and parsed_args.extensions:
|
||||
extra_settings = self.install_extensions(parsed_args.extensions)
|
||||
|
||||
bower_components_dir = os.path.realpath(
|
||||
os.path.join(os.path.dirname(__file__), "../../bower_components")
|
||||
)
|
||||
@@ -195,14 +213,16 @@ class DeployCommand(Command):
|
||||
mod = __import__(parsed_args.name, globals(), locals(), ['settings'])
|
||||
tpl = self._render_template(
|
||||
"%s/settings.py.tpl" % self._templates_dir, {
|
||||
'default_conn': default_conn, 'amavis_conn': amavis_conn,
|
||||
'db_connections': connections,
|
||||
'secret_key': mod.settings.SECRET_KEY,
|
||||
'name': parsed_args.name,
|
||||
'allowed_host': allowed_host,
|
||||
'lang': parsed_args.lang,
|
||||
'timezone': parsed_args.timezone,
|
||||
'bower_components_dir': bower_components_dir,
|
||||
'devmode': parsed_args.devel
|
||||
'devmode': parsed_args.devel,
|
||||
'extensions': parsed_args.extensions,
|
||||
'extra_settings': extra_settings
|
||||
}
|
||||
)
|
||||
with open("%s/settings.py" % path, "w") as fp:
|
||||
|
||||
@@ -4,25 +4,46 @@
|
||||
|
||||
import datetime
|
||||
import getpass
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
|
||||
from django.template import Context, Template
|
||||
|
||||
import dj_database_url
|
||||
|
||||
from modoboa.lib.api_client import ModoAPIClient
|
||||
from . import Command
|
||||
|
||||
|
||||
class MapFilesGenerator(object):
|
||||
|
||||
def __init__(self, dbinfo=None):
|
||||
"""Constructor."""
|
||||
self.template = Template("""user = {{ dbuser }}
|
||||
MAP_FILE_TEMPLATE = """user = {{ dbuser }}
|
||||
password = {{ dbpass }}
|
||||
dbname = {{ dbname }}
|
||||
hosts = {{ dbhost }}
|
||||
query = {{ query|safe }}
|
||||
""")
|
||||
"""
|
||||
|
||||
|
||||
class MapFilesRegistry(object):
|
||||
|
||||
"""A registry of all map files."""
|
||||
|
||||
def __init__(self):
|
||||
self.files = []
|
||||
|
||||
def add_files(self, files):
|
||||
"""Add map files to registry."""
|
||||
self.files += files
|
||||
|
||||
|
||||
registry = MapFilesRegistry()
|
||||
|
||||
|
||||
class MapFilesGenerator(object):
|
||||
|
||||
"""Map files generator."""
|
||||
|
||||
def __init__(self, dbinfo=None):
|
||||
"""Constructor."""
|
||||
self.template = Template(MAP_FILE_TEMPLATE)
|
||||
if dbinfo is None:
|
||||
self.tpl_context = {
|
||||
'dbhost': raw_input('Database host: '),
|
||||
@@ -61,21 +82,19 @@ query = {{ query|safe }}
|
||||
' '.join(sys.argv[1:]))
|
||||
print >> fp, content
|
||||
|
||||
def __render_std_maps(self, args):
|
||||
"""Render standard map files.
|
||||
def __load_extensions(self, extensions):
|
||||
"""Load specified extensions."""
|
||||
if "all" in extensions:
|
||||
# Retrieve extension list from the API
|
||||
official_exts = ModoAPIClient().list_extensions()
|
||||
extensions = [extension["name"] for extension in official_exts]
|
||||
|
||||
(ie. the ones belonging to the 'std' category)
|
||||
|
||||
:param args: command line arguments
|
||||
"""
|
||||
for v in globals().values():
|
||||
if not inspect.isclass(v) or not issubclass(v, MapFile):
|
||||
continue
|
||||
if v is MapFile:
|
||||
continue
|
||||
if v.category not in args.categories:
|
||||
continue
|
||||
self.__render_map(args, v)
|
||||
for extension in extensions:
|
||||
extension = extension.replace("-", "_")
|
||||
try:
|
||||
__import__(extension, locals(), globals(), ["postfix_maps"])
|
||||
except ImportError:
|
||||
sys.stderr.write("Unknown extension {0}".format(extension))
|
||||
|
||||
def render(self, args):
|
||||
"""Render all map files.
|
||||
@@ -86,11 +105,16 @@ query = {{ query|safe }}
|
||||
os.mkdir(args.destdir)
|
||||
except OSError:
|
||||
pass
|
||||
self.__render_std_maps(args)
|
||||
if args.extensions:
|
||||
self.__load_extensions(args.extensions)
|
||||
for mapfile in registry.files:
|
||||
self.__render_map(args, mapfile)
|
||||
|
||||
|
||||
class SQLiteMapFilesGenerator(MapFilesGenerator):
|
||||
|
||||
"""SQLite special generator."""
|
||||
|
||||
def __init__(self, dbinfo=None):
|
||||
self.template = Template("""dbpath = {{ dbpath }}
|
||||
query = {{ query|safe }}
|
||||
@@ -105,112 +129,10 @@ query = {{ query|safe }}
|
||||
}
|
||||
|
||||
|
||||
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 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 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 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 MailboxesSelfAliasesMap(MapFile):
|
||||
filename = "sql-mailboxes-self-aliases.cf"
|
||||
mysql = "SELECT email FROM core_user u INNER JOIN admin_mailbox mb ON mb.user_id=u.id WHERE u.email='%s' AND u.is_active=1"
|
||||
postgres = "SELECT email FROM core_user u INNER JOIN admin_mailbox mb ON mb.user_id=u.id WHERE u.email='%s' AND u.is_active"
|
||||
sqlite = "SELECT email FROM core_user u INNER JOIN admin_mailbox mb ON mb.user_id=u.id WHERE u.email='%s' AND u.is_active=1"
|
||||
|
||||
|
||||
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):
|
||||
category = "autoreply"
|
||||
filename = 'sql-autoreplies-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):
|
||||
category = "autoreply"
|
||||
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 RelayDomainsMap(MapFile):
|
||||
category = "relaydomains"
|
||||
filename = "sql-relaydomains.cf"
|
||||
mysql = "SELECT name FROM postfix_relay_domains_relaydomain WHERE name='%s' AND enabled=1"
|
||||
postgres = "SELECT name FROM postfix_relay_domains_relaydomain WHERE name='%s' AND enabled"
|
||||
sqlite = "SELECT name FROM postfix_relay_domains_relaydomain WHERE name='%s' AND enabled=1"
|
||||
|
||||
|
||||
class RelayDomainsTransportMap(MapFile):
|
||||
category = "relaydomains"
|
||||
filename = "sql-relaydomains-transport.cf"
|
||||
mysql = "SELECT CONCAT(srv.name, ':[', rdom.target_host, ']') FROM postfix_relay_domains_service AS srv INNER JOIN postfix_relay_domains_relaydomain AS rdom ON rdom.service_id=srv.id WHERE rdom.enabled=1 AND rdom.name='%s'"
|
||||
postgres = "SELECT srv.name || ':[' || rdom.target_host || ']' FROM postfix_relay_domains_service AS srv INNER JOIN postfix_relay_domains_relaydomain AS rdom ON rdom.service_id=srv.id WHERE rdom.enabled AND rdom.name='%s'"
|
||||
sqlite = "SELECT srv.name || ':[' || rdom.target_host || ']' FROM postfix_relay_domains_service AS srv INNER JOIN postfix_relay_domains_relaydomain AS rdom ON rdom.service_id=srv.id WHERE rdom.enabled=1 AND rdom.name='%s'"
|
||||
|
||||
|
||||
class RelayDomainAliasesTransportMap(MapFile):
|
||||
category = "relaydomains"
|
||||
filename = "sql-relaydomain-aliases-transport.cf"
|
||||
mysql = "SELECT CONCAT(srv.name, ':[', rdom.target_host, ']') FROM postfix_relay_domains_service AS srv INNER JOIN postfix_relay_domains_relaydomain AS rdom ON rdom.service_id=srv.id INNER JOIN postfix_relay_domains_relaydomainalias AS rdomalias ON rdom.id=rdomalias.target_id WHERE rdom.enabled=1 AND rdomalias.enabled=1 AND rdomalias.name='%s'"
|
||||
postgres = "SELECT srv.name || ':[' || rdom.target_host || ']' FROM postfix_relay_domains_service AS srv INNER JOIN postfix_relay_domains_relaydomain AS rdom ON rdom.service_id=srv.id INNER JOIN postfix_relay_domains_relaydomainalias AS rdomalias ON rdom.id=rdomalias.target_id WHERE rdom.enabled AND rdomalias.enabled AND rdomalias.name='%s'"
|
||||
sqlite = "SELECT srv.name || ':[' || rdom.target_host || ']' FROM postfix_relay_domains_service AS srv INNER JOIN postfix_relay_domains_relaydomain AS rdom ON rdom.service_id=srv.id INNER JOIN postfix_relay_domains_relaydomainalias AS rdomalias ON rdom.id=rdomalias.target_id WHERE rdom.enabled=1 AND rdomalias.enabled=1 AND rdomalias.name='%s'"
|
||||
|
||||
|
||||
class RelayRecipientVerification(MapFile):
|
||||
category = "relaydomains"
|
||||
filename = "sql-relay-recipient-verification.cf"
|
||||
mysql = "SELECT 'reject_unverified_recipient' FROM postfix_relay_domains_relaydomain WHERE verify_recipients=1 AND name='%d'"
|
||||
postgres = "SELECT 'reject_unverified_recipient' FROM postfix_relay_domains_relaydomain WHERE verify_recipients AND name='%d'"
|
||||
sqlite = (
|
||||
"SELECT 'reject_unverified_recipient' "
|
||||
"FROM postfix_relay_domains_relaydomain "
|
||||
"WHERE verify_recipients=1 AND name='%d'"
|
||||
)
|
||||
|
||||
|
||||
class PostfixMapsCommand(Command):
|
||||
|
||||
"""Base command to generate map files."""
|
||||
|
||||
help = "Generate ready-to-use postfix map files"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -229,16 +151,17 @@ class PostfixMapsCommand(Command):
|
||||
help="url of Modoboa's database"
|
||||
)
|
||||
self._parser.add_argument(
|
||||
'--categories', type=str, nargs='*', default=['std'],
|
||||
help='Map file categories to generate (choices: std, autoreply)'
|
||||
'--extensions', type=str, nargs='+',
|
||||
help=(
|
||||
'Generate map files for this extensions '
|
||||
'("all" shortcut available)'
|
||||
)
|
||||
)
|
||||
|
||||
def handle(self, parsed_args):
|
||||
"""Command entry point.
|
||||
"""
|
||||
"""Command entry point."""
|
||||
dbinfo = None
|
||||
if parsed_args.dburl:
|
||||
import dj_database_url
|
||||
dbinfo = dj_database_url.config(default=parsed_args.dburl[0])
|
||||
if 'sqlite' in dbinfo['ENGINE']:
|
||||
parsed_args.dbtype = 'sqlite'
|
||||
@@ -247,7 +170,7 @@ class PostfixMapsCommand(Command):
|
||||
else:
|
||||
parsed_args.dbtype = 'mysql'
|
||||
if parsed_args.dbtype == 'sqlite':
|
||||
g = SQLiteMapFilesGenerator(dbinfo)
|
||||
generator = SQLiteMapFilesGenerator(dbinfo)
|
||||
else:
|
||||
g = MapFilesGenerator(dbinfo)
|
||||
g.render(parsed_args)
|
||||
generator = MapFilesGenerator(dbinfo)
|
||||
generator.render(parsed_args)
|
||||
|
||||
@@ -57,18 +57,8 @@ MODOBOA_APPS = (
|
||||
'modoboa.core',
|
||||
'modoboa.lib',
|
||||
# Modoboa extensions here.
|
||||
'modoboa.extensions.admin',
|
||||
'modoboa.extensions.postfix_relay_domains',
|
||||
'modoboa.extensions.limits',
|
||||
'modoboa.extensions.postfix_autoreply',
|
||||
'modoboa.extensions.webmail',
|
||||
'modoboa.extensions.stats',
|
||||
'modoboa.extensions.sievefilters',
|
||||
'modoboa.extensions.radicale',
|
||||
{% if not amavis_conn %}#{% endif %}'modoboa.extensions.amavis',
|
||||
|
||||
# Tools from here
|
||||
#'modoboa.tools.pfxadmin_migrate',
|
||||
{% for extension in extensions %} '{{ extension }}',
|
||||
{% endfor %}
|
||||
)
|
||||
|
||||
INSTALLED_APPS += MODOBOA_APPS
|
||||
@@ -104,25 +94,10 @@ WSGI_APPLICATION = '{{ name }}.wsgi.application'
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.6/ref/settings/#databases
|
||||
|
||||
DATABASES = { {{ default_conn|safe }}
|
||||
# "pfxadmin" : {
|
||||
# "ENGINE" : "django.db.backends.",
|
||||
# "NAME" : "",
|
||||
# "USER" : "",
|
||||
# "PASSWORD" : ""
|
||||
# },{% if not amavis_conn %}
|
||||
# "amavis": {
|
||||
# "ENGINE" : "django.db.backends.",
|
||||
# "HOST" : "",
|
||||
# "NAME" : "",
|
||||
# "USER" : "",
|
||||
# "PASSWORD" : ""
|
||||
# }{% else %}
|
||||
{{ amavis_conn|safe }}{% endif %}
|
||||
DATABASES = {
|
||||
{% for conn in db_connections.values %}{{ conn|safe }}{% endfor %}
|
||||
}
|
||||
|
||||
DATABASE_ROUTERS = ["modoboa.extensions.amavis.dbrouter.AmavisRouter"]
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.6/topics/i18n/
|
||||
|
||||
@@ -188,3 +163,6 @@ LOGGING = {
|
||||
}
|
||||
}
|
||||
}
|
||||
{% for extension in extra_settings %}
|
||||
import {{ extension }}.settings
|
||||
{% endfor %}
|
||||
|
||||
+46
-17
@@ -3,6 +3,11 @@
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include
|
||||
|
||||
from versionfield.constants import DEFAULT_NUMBER_BITS
|
||||
from versionfield.version import Version
|
||||
|
||||
from modoboa.lib.api_client import ModoAPIClient
|
||||
|
||||
|
||||
class ModoExtension(object):
|
||||
|
||||
@@ -70,6 +75,28 @@ class ExtensionsPool(object):
|
||||
return None
|
||||
return instance.infos()
|
||||
|
||||
def load_extension(self, name):
|
||||
"""Load a registered extension."""
|
||||
__import__(name, locals(), globals(), ["modo_extension"])
|
||||
extinstance = self.get_extension(name)
|
||||
if extinstance is None:
|
||||
return None
|
||||
result = None
|
||||
try:
|
||||
baseurl = (
|
||||
extinstance.url if extinstance.url is not None
|
||||
else name
|
||||
)
|
||||
result = (
|
||||
r'^%s/' % (baseurl),
|
||||
include("{0}.urls".format(name), namespace=name)
|
||||
)
|
||||
except ImportError:
|
||||
# No urls for this extension
|
||||
pass
|
||||
extinstance.load()
|
||||
return result
|
||||
|
||||
def load_all(self):
|
||||
"""Load all defined extensions.
|
||||
|
||||
@@ -83,32 +110,34 @@ class ExtensionsPool(object):
|
||||
"""
|
||||
result = []
|
||||
for ext in settings.MODOBOA_APPS:
|
||||
__import__(ext, locals(), globals(), ["modo_extension"])
|
||||
extname = ext.split('.')[-1]
|
||||
extinstance = self.get_extension(extname)
|
||||
if extinstance is None:
|
||||
continue
|
||||
try:
|
||||
baseurl = extinstance.url \
|
||||
if extinstance.url is not None else extname
|
||||
result += [
|
||||
(r'^%s/' % (baseurl),
|
||||
include("{0}.urls".format(ext), namespace=extname))
|
||||
]
|
||||
except ImportError:
|
||||
# No urls for this extension
|
||||
pass
|
||||
extinstance.load()
|
||||
ext_urls = self.load_extension(ext)
|
||||
if ext_urls is not None:
|
||||
result += [ext_urls]
|
||||
return result
|
||||
|
||||
def list_all(self):
|
||||
def list_all(self, check_new_versions=False):
|
||||
"""List all defined extensions."""
|
||||
result = []
|
||||
if check_new_versions:
|
||||
new_extensions = ModoAPIClient().list_extensions()
|
||||
new_extensions = dict(
|
||||
(ext["name"], ext["version"]) for ext in new_extensions
|
||||
)
|
||||
else:
|
||||
new_extensions = {}
|
||||
for extname, extdef in self.extensions.iteritems():
|
||||
if not extdef["show"]:
|
||||
continue
|
||||
infos = self.get_extension_infos(extname)
|
||||
infos["id"] = extname
|
||||
local_version = Version(infos["version"], DEFAULT_NUMBER_BITS)
|
||||
pkgname = infos["name"].replace("_", "-")
|
||||
if pkgname in new_extensions:
|
||||
infos["last_version"] = new_extensions[pkgname]
|
||||
last_version = Version(
|
||||
new_extensions[pkgname], DEFAULT_NUMBER_BITS)
|
||||
if last_version > local_version:
|
||||
infos["update"] = True
|
||||
result += [infos]
|
||||
return sorted(result, key=lambda i: i["name"])
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class Command(BaseCommand, CloseConnectionMixin):
|
||||
superadmin = User.objects.filter(is_superuser=True).first()
|
||||
groups = PERMISSIONS.keys() + [
|
||||
role[0] for role
|
||||
in events.raiseQueryEvent("GetExtraRoles", superadmin)
|
||||
in events.raiseQueryEvent("GetExtraRoles", superadmin, None)
|
||||
]
|
||||
for groupname in groups:
|
||||
group, created = Group.objects.get_or_create(name=groupname)
|
||||
|
||||
@@ -335,11 +335,14 @@ class User(PermissionsMixin):
|
||||
:param row: a list containing the expected information
|
||||
:param crypt_password:
|
||||
"""
|
||||
from modoboa.lib.permissions import get_account_roles
|
||||
|
||||
if len(row) < 7:
|
||||
raise BadRequest(_("Invalid line"))
|
||||
allowed_roles = get_account_roles(user)
|
||||
allowed_roles = [role[0] for role in allowed_roles]
|
||||
role = row[6].strip()
|
||||
allowed_roles = ["SimpleUsers", "DomainAdmins"]
|
||||
if not user.is_superuser and role not in allowed_roles:
|
||||
if role not in allowed_roles:
|
||||
raise PermDeniedException(
|
||||
_("You can't import an account with a role greater than yours")
|
||||
)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% load url from future %}
|
||||
|
||||
<h2>{% trans "Extensions" %} <small>{% trans "The extension(s) you are using" %}</small></h2><hr />
|
||||
<table id="extensions" class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Version" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for extension in extensions %}
|
||||
<tr>
|
||||
<td>{{ extension.label }}</td>
|
||||
<td>{{ extension.version }}</td>
|
||||
<td>{{ extension.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1,25 +1,50 @@
|
||||
{% load i18n core_tags lib_tags %}
|
||||
<h2>{% trans "Information" %} <small>{% trans "Various information about Modoboa" %}</small></h2><hr>
|
||||
{% if new_version %}
|
||||
{% if update_avail %}
|
||||
<div class="alert alert-block alert-success">
|
||||
<h4>{% trans "An update is available!" %}</h4>
|
||||
{% blocktrans with version=new_version.version changelog_url=new_version.changelog_url %}
|
||||
Modoboa <strong>{{ version }}</strong> is available for download. The list of all changes is available <a href="{{ changelog_url }}" target="_blank">here</a>.
|
||||
{% endblocktrans %}
|
||||
<h4>{% trans "One or more updates are available!" %}</h4>
|
||||
<p>{% trans "Check the following list to find the component(s) that can be updated." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="alert alert-info">Modoboa <strong>{% get_version %}</strong> {% trans "is currently in use." %}</div>
|
||||
|
||||
<h3>{% trans "Applications" %}</h3>
|
||||
<table id="extensions" class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Installed version" %}</th>
|
||||
<th>{% trans "Last version" %}</th>
|
||||
<th>{% trans "Description" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for extension in extensions %}
|
||||
<tr{% if extension.update %} class="success"{% endif %}>
|
||||
<td>{{ extension.label }}</td>
|
||||
<td>{{ extension.version }}</td>
|
||||
<td>{% if extension.update %}<strong>{% if extension.changelog_url %}<a href="{{ extension.changelog_url }}">{{ extension.last_version }}</a>{% else %}{{ extension.last_version }}{% endif %}</strong>{% else %}{{ extension.last_version }}{% endif %}</td>
|
||||
<td>{{ extension.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>{% trans "Active users" %}</h3>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Group name" %}</th>
|
||||
<th>{% trans "Last login" %}</th>
|
||||
</tr>
|
||||
{% connected_users as users %}{% for u in users %}<tr>
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.group }}</td>
|
||||
<td>{{ u.last_login }}</td>
|
||||
</tr>{% endfor %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Group name" %}</th>
|
||||
<th>{% trans "Last login" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% connected_users as users %}{% for u in users %}
|
||||
<tr>
|
||||
<td>{{ u.username }}</td>
|
||||
<td>{{ u.group }}</td>
|
||||
<td>{{ u.last_login }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -46,11 +46,6 @@ def extensions_menu(selection, user):
|
||||
@register.simple_tag
|
||||
def admin_menu(selection, user):
|
||||
entries = [
|
||||
{"name": "extensions",
|
||||
"class": "ajaxnav",
|
||||
"url": "extensions/",
|
||||
"label": _("Extensions"),
|
||||
"img": ""},
|
||||
{"name": "info",
|
||||
"class": "ajaxnav",
|
||||
"url": "info/",
|
||||
@@ -151,7 +146,7 @@ def visirule(field):
|
||||
% (rule["field"], rule["value"])
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
@register.assignment_tag
|
||||
def get_version():
|
||||
import pkg_resources
|
||||
return pkg_resources.get_distribution("modoboa").version
|
||||
|
||||
+1
-61
@@ -1,16 +1,12 @@
|
||||
import os
|
||||
import tempfile
|
||||
"""Tests for core application."""
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
|
||||
from modoboa.lib.sysutils import exec_cmd
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
from . import factories
|
||||
|
||||
|
||||
class ProfileTestCase(ModoTestCase):
|
||||
fixtures = ['initial_users.json']
|
||||
|
||||
def setUp(self):
|
||||
super(ProfileTestCase, self).setUp()
|
||||
@@ -46,59 +42,3 @@ class ProfileTestCase(ModoTestCase):
|
||||
self.assertTrue(
|
||||
self.clt.login(username="user@test.com", password="tutu")
|
||||
)
|
||||
|
||||
|
||||
class ModoboaAdminCommandTestCase(TestCase):
|
||||
MAP_FILES = {
|
||||
"std": [
|
||||
"sql-domains.cf", "sql-domain-aliases.cf", "sql-aliases.cf",
|
||||
"sql-domain-aliases-mailboxes.cf", "sql-catchall-aliases.cf",
|
||||
"sql-maintain.cf"
|
||||
],
|
||||
"autoreply": ["sql-autoreplies-transport.cf", "sql-autoreplies.cf"],
|
||||
"relaydomains": [
|
||||
"sql-relaydomains.cf", "sql-relaydomains-transport.cf",
|
||||
"sql-relaydomain-aliases-transport.cf",
|
||||
"sql-relay-recipient-verification.cf"
|
||||
]
|
||||
}
|
||||
|
||||
def setUp(self):
|
||||
self.workdir = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
exec_cmd("rm -rf {0}".format(self.workdir))
|
||||
|
||||
def _test_maps_generation(self, engine, categories=None):
|
||||
if categories is None:
|
||||
categories = ["std", "autoreply", "relaydomains"]
|
||||
dburl = "{0}://user:password@localhost/testdb".format(engine)
|
||||
code, output = exec_cmd(
|
||||
"modoboa-admin.py postfix_maps --categories {0}"
|
||||
" --dburl {1} {2}".format(
|
||||
" ".join(categories), dburl, self.workdir
|
||||
)
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
for category in categories:
|
||||
for mapfile in self.MAP_FILES[category]:
|
||||
path = "{0}/{1}".format(self.workdir, mapfile)
|
||||
self.assertTrue(os.path.exists(path))
|
||||
with open(path) as fpo:
|
||||
content = fpo.read()
|
||||
if engine != "sqlite":
|
||||
self.assertIn("user = user", content)
|
||||
self.assertIn("password = password", content)
|
||||
self.assertIn("dbname = testdb", content)
|
||||
self.assertIn("hosts = localhost", content)
|
||||
else:
|
||||
self.assertIn("dbpath = testdb", content)
|
||||
|
||||
def test_postgres_maps(self):
|
||||
self._test_maps_generation("postgres")
|
||||
|
||||
def test_mysql_maps(self):
|
||||
self._test_maps_generation("mysql")
|
||||
|
||||
def test_sqlite_maps(self):
|
||||
self._test_maps_generation("sqlite")
|
||||
|
||||
@@ -15,8 +15,6 @@ urlpatterns = patterns(
|
||||
name="parameter_listl"),
|
||||
url(r'^core/parameters/save/$', 'modoboa.core.views.admin.saveparameters',
|
||||
name="parameter_save"),
|
||||
url(r'^core/extensions/$', 'modoboa.core.views.admin.viewextensions',
|
||||
name="extension_list"),
|
||||
url(r'^core/info/$', 'modoboa.core.views.admin.information',
|
||||
name="information"),
|
||||
url(r'^core/logs/$', 'modoboa.core.views.admin.logs', name="log_list"),
|
||||
|
||||
+22
-31
@@ -1,40 +1,31 @@
|
||||
"""
|
||||
Utility functions.
|
||||
"""
|
||||
import os
|
||||
import pkg_resources
|
||||
"""Utility functions."""
|
||||
|
||||
import requests
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from django.contrib.sites.models import get_current_site
|
||||
|
||||
from versionfield.constants import DEFAULT_NUMBER_BITS
|
||||
from versionfield.version import Version
|
||||
|
||||
from modoboa.core.extensions import exts_pool
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.api_client import ModoAPIClient
|
||||
|
||||
|
||||
def new_version_available(request):
|
||||
def check_for_updates(request):
|
||||
"""Check if a new version of Modoboa is available."""
|
||||
if parameters.get_admin("CHECK_NEW_VERSIONS") == "no":
|
||||
return None
|
||||
local_version = pkg_resources.get_distribution("modoboa").version
|
||||
url = os.path.join(settings.MODOBOA_API_URL, "current_version/")
|
||||
site = get_current_site(request)
|
||||
try:
|
||||
resp = requests.get(url, params={
|
||||
"client_version": pkg_resources.get_distribution("modoboa").version,
|
||||
"client_site": site
|
||||
})
|
||||
except RequestException:
|
||||
return None
|
||||
if resp.status_code != 200:
|
||||
return None
|
||||
resp = resp.json()
|
||||
version = Version(resp["version"], DEFAULT_NUMBER_BITS)
|
||||
if version <= Version(local_version, DEFAULT_NUMBER_BITS):
|
||||
return None
|
||||
return resp
|
||||
return False, []
|
||||
client = ModoAPIClient()
|
||||
extensions = exts_pool.list_all(True)
|
||||
extensions = [{
|
||||
"label": "Modoboa",
|
||||
"description": _("The core part of Modoboa"),
|
||||
"update": client.new_core_version(get_current_site(request)),
|
||||
"last_version": client.latest_core_version,
|
||||
"changelog_url": client.changelog_url,
|
||||
"version": client.local_core_version
|
||||
}] + extensions
|
||||
update_avail = False
|
||||
for extension in extensions:
|
||||
if "update" in extension:
|
||||
update_avail = True
|
||||
break
|
||||
return update_avail, extensions
|
||||
|
||||
@@ -8,8 +8,9 @@ from django.shortcuts import render
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.core.extensions import exts_pool
|
||||
from modoboa.core.models import Log
|
||||
from modoboa.core.utils import new_version_available
|
||||
from modoboa.core.utils import check_for_updates
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.lib.listing import get_sort_order, get_listing_page
|
||||
from modoboa.lib.web_utils import (
|
||||
@@ -51,25 +52,14 @@ def saveparameters(request):
|
||||
return render_to_json_response(_("Parameters saved"))
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def viewextensions(request, tplname='core/extensions.html'):
|
||||
"""List available extensions."""
|
||||
from modoboa.core.extensions import exts_pool
|
||||
|
||||
exts = exts_pool.list_all()
|
||||
return render_to_json_response({
|
||||
"callback": "extensions",
|
||||
"content": _render_to_string(request, tplname, {"extensions": exts})
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def information(request, tplname="core/information.html"):
|
||||
status, extensions = check_for_updates(request)
|
||||
return render_to_json_response({
|
||||
"content": render_to_string(tplname, {
|
||||
"new_version": new_version_available(request)
|
||||
"update_avail": status,
|
||||
"extensions": extensions,
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@@ -41,7 +41,8 @@ def dologin(request):
|
||||
if user.group == "SimpleUsers":
|
||||
nextlocation = reverse("topredirection")
|
||||
else:
|
||||
nextlocation = reverse("admin:domain_list")
|
||||
# FIXME
|
||||
nextlocation = reverse("modoboa_admin:domain_list")
|
||||
return HttpResponseRedirect(nextlocation)
|
||||
error = _(
|
||||
"Your username and password didn't match. Please try again.")
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from modoboa.lib.form_utils import YesNoField, SeparatorField
|
||||
from modoboa.lib.sysutils import exec_cmd
|
||||
from modoboa.lib import parameters
|
||||
import os
|
||||
|
||||
|
||||
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 owner"),
|
||||
initial="vmail",
|
||||
help_text=ugettext_lazy(
|
||||
"The UNIX account who owns mailboxes on the filesystem"
|
||||
)
|
||||
)
|
||||
|
||||
default_domain_quota = forms.IntegerField(
|
||||
label=ugettext_lazy("Default domain quota"),
|
||||
initial=0,
|
||||
help_text=ugettext_lazy(
|
||||
"Default quota (in MB) applied to freshly created domains with no "
|
||||
"value specified. A value of 0 means no quota."
|
||||
)
|
||||
)
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
auto_create_domain_and_mailbox = YesNoField(
|
||||
label=ugettext_lazy("Automatic domain/mailbox creation"),
|
||||
initial="yes",
|
||||
help_text=ugettext_lazy(
|
||||
"Create a domain and a mailbox when an account is automatically "
|
||||
"created."
|
||||
)
|
||||
)
|
||||
|
||||
# Visibility rules
|
||||
visibility_rules = {
|
||||
"mailboxes_owner": "handle_mailboxes=yes",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AdminParametersForm, self).__init__(*args, **kwargs)
|
||||
self.field_widths = {
|
||||
"default_domain_quota": 2
|
||||
}
|
||||
hide_fields = False
|
||||
dpath = None
|
||||
code, output = exec_cmd("which dovecot")
|
||||
if not code:
|
||||
dpath = output.strip()
|
||||
else:
|
||||
known_paths = getattr(
|
||||
settings, "DOVECOT_LOOKUP_PATH",
|
||||
("/usr/sbin/dovecot", "/usr/local/sbin/dovecot")
|
||||
)
|
||||
for fpath in known_paths:
|
||||
if os.path.isfile(fpath) and os.access(fpath, os.X_OK):
|
||||
dpath = fpath
|
||||
if dpath:
|
||||
try:
|
||||
code, version = exec_cmd("%s --version" % dpath)
|
||||
except OSError:
|
||||
hide_fields = True
|
||||
else:
|
||||
if code or not version.strip().startswith("2"):
|
||||
hide_fields = True
|
||||
else:
|
||||
hide_fields = True
|
||||
if hide_fields:
|
||||
del self.fields["handle_mailboxes"]
|
||||
del self.fields["mailboxes_owner"]
|
||||
|
||||
def clean_default_domain_quota(self):
|
||||
"""Ensure quota is a positive integer."""
|
||||
if self.cleaned_data['default_domain_quota'] < 0:
|
||||
raise forms.ValidationError(
|
||||
ugettext_lazy('Must be a positive integer')
|
||||
)
|
||||
return self.cleaned_data['default_domain_quota']
|
||||
@@ -1,79 +0,0 @@
|
||||
"""
|
||||
Fixtures factories.
|
||||
"""
|
||||
import factory
|
||||
|
||||
from modoboa.core.factories import PermissionFactory, UserFactory
|
||||
from . import models
|
||||
|
||||
|
||||
class DomainFactory(PermissionFactory):
|
||||
|
||||
"""Factory to create Domains."""
|
||||
|
||||
class Meta:
|
||||
model = models.Domain
|
||||
django_get_or_create = ("name", )
|
||||
|
||||
quota = 10
|
||||
enabled = True
|
||||
|
||||
|
||||
class MailboxFactory(PermissionFactory):
|
||||
|
||||
class Meta:
|
||||
model = models.Mailbox
|
||||
|
||||
domain = factory.SubFactory(DomainFactory)
|
||||
user = factory.SubFactory(UserFactory)
|
||||
quota = 10
|
||||
enabled = True
|
||||
|
||||
|
||||
class AliasFactory(PermissionFactory):
|
||||
|
||||
class Meta:
|
||||
model = models.Alias
|
||||
|
||||
enabled = True
|
||||
|
||||
|
||||
def populate_database():
|
||||
"""Create test data.
|
||||
|
||||
2 domains, 1 domain admin, 2 users
|
||||
"""
|
||||
dom = DomainFactory(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)
|
||||
@@ -1,310 +0,0 @@
|
||||
[
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "core.user",
|
||||
"fields": {
|
||||
"username": "admin",
|
||||
"first_name": "",
|
||||
"last_name": "",
|
||||
"is_active": true,
|
||||
"is_superuser": true,
|
||||
"is_staff": false,
|
||||
"last_login": "2012-05-03T16:58:51+01:00",
|
||||
"groups": [],
|
||||
"user_permissions": [],
|
||||
"password": "{CRYPT}dTTsGDkA5ZHKg",
|
||||
"email": "",
|
||||
"date_joined": "2012-01-26T15:19:25+01:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 2,
|
||||
"model": "core.user",
|
||||
"fields": {
|
||||
"username": "admin@demo.local",
|
||||
"first_name": "Demo",
|
||||
"last_name": "Administrator",
|
||||
"is_active": true,
|
||||
"is_superuser": false,
|
||||
"is_staff": false,
|
||||
"last_login": "2012-05-24T05:00:12+01:00",
|
||||
"groups": [1],
|
||||
"user_permissions": [],
|
||||
"password": "{CRYPT}SJ2bQWwabfH/6",
|
||||
"email": "admin@demo.local",
|
||||
"date_joined": "2012-05-03T16:50:15+01:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 4,
|
||||
"model": "core.user",
|
||||
"fields": {
|
||||
"username": "user@demo.local",
|
||||
"first_name": "Demo", "last_name": "User",
|
||||
"is_active": true,
|
||||
"is_superuser": false,
|
||||
"is_staff": false,
|
||||
"last_login": "2012-05-03T16:56:50+01:00",
|
||||
"groups": [2],
|
||||
"user_permissions": [],
|
||||
"password": "{CRYPT}2iAFXtxUA6.7Y",
|
||||
"email": "user@demo.local",
|
||||
"date_joined": "2012-05-03T16:56:50+01:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 2,
|
||||
"model": "core.objectaccess",
|
||||
"fields": {
|
||||
"is_owner": true,
|
||||
"user": 1,
|
||||
"content_type": ["admin", "domain"],
|
||||
"object_id": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 3,
|
||||
"model": "core.objectaccess",
|
||||
"fields": {
|
||||
"is_owner": true,
|
||||
"user": 1,
|
||||
"content_type": ["core", "user"],
|
||||
"object_id": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 4,
|
||||
"model": "core.objectaccess",
|
||||
"fields": {
|
||||
"is_owner": true,
|
||||
"user": 1,
|
||||
"content_type": ["admin", "mailbox"],
|
||||
"object_id": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 5,
|
||||
"model": "core.objectaccess",
|
||||
"fields": {
|
||||
"is_owner": true,
|
||||
"user": 1,
|
||||
"content_type": ["admin", "alias"],
|
||||
"object_id": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 6,
|
||||
"model": "core.objectaccess",
|
||||
"fields": {
|
||||
"is_owner": false,
|
||||
"user": 2,
|
||||
"content_type": ["admin", "domain"],
|
||||
"object_id": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 7,
|
||||
"model": "core.objectaccess",
|
||||
"fields": {
|
||||
"is_owner": false,
|
||||
"user": 2,
|
||||
"content_type": ["admin", "mailbox"],
|
||||
"object_id": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 8,
|
||||
"model": "core.objectaccess",
|
||||
"fields": {
|
||||
"is_owner": false,
|
||||
"user": 2,
|
||||
"content_type": ["core", "user"],
|
||||
"object_id": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 9,
|
||||
"model": "core.objectaccess",
|
||||
"fields": {
|
||||
"is_owner": false,
|
||||
"user": 2,
|
||||
"content_type": ["admin",
|
||||
"alias"],
|
||||
"object_id": 1}},
|
||||
{
|
||||
"pk": 13,
|
||||
"model": "core.objectaccess",
|
||||
"fields": {
|
||||
"is_owner": true,
|
||||
"user": 1,
|
||||
"content_type": ["core",
|
||||
"user"],
|
||||
"object_id": 4}},
|
||||
{
|
||||
"pk": 14,
|
||||
"model": "core.objectaccess",
|
||||
"fields": {
|
||||
"is_owner": true,
|
||||
"user": 1,
|
||||
"content_type": ["admin",
|
||||
"mailbox"],
|
||||
"object_id": 3}},
|
||||
{
|
||||
"pk": 15,
|
||||
"model": "core.objectaccess",
|
||||
"fields": {
|
||||
"is_owner": false,
|
||||
"user": 2,
|
||||
"content_type": ["admin",
|
||||
"mailbox"],
|
||||
"object_id": 3}},
|
||||
{
|
||||
"pk": 16,
|
||||
"model": "core.objectaccess",
|
||||
"fields": {
|
||||
"is_owner": false,
|
||||
"user": 2,
|
||||
"content_type": ["core",
|
||||
"user"],
|
||||
"object_id": 4}},
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "admin.objectdates",
|
||||
"fields": {
|
||||
"creation": "2012-05-03T16:49:40+01:00",
|
||||
"last_modification": "2012-05-03T16:49:40+01:00"}},
|
||||
{
|
||||
"pk": 2,
|
||||
"model": "admin.objectdates",
|
||||
"fields": {
|
||||
"creation": "2012-05-03T16:50:15+01:00",
|
||||
"last_modification": "2012-05-03T16:50:36+01:00"}},
|
||||
{
|
||||
"pk": 3,
|
||||
"model": "admin.objectdates",
|
||||
"fields": {
|
||||
"creation": "2012-05-03T16:50:15+01:00",
|
||||
"last_modification": "2012-05-03T16:50:15+01:00"}},
|
||||
{
|
||||
"pk": 4,
|
||||
"model": "admin.objectdates",
|
||||
"fields": {
|
||||
"creation": "2012-05-03T16:51:04+01:00",
|
||||
"last_modification": "2012-05-03T16:51:04+01:00"}},
|
||||
{
|
||||
"pk": 5,
|
||||
"model": "admin.objectdates",
|
||||
"fields": {
|
||||
"creation": "2012-05-03T16:56:50+01:00",
|
||||
"last_modification": "2012-05-03T16:56:50+01:00"}},
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "admin.domain",
|
||||
"fields": {
|
||||
"dates": 1,
|
||||
"enabled": true,
|
||||
"name": "demo.local",
|
||||
"quota": 10}},
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "admin.mailbox",
|
||||
"fields": {
|
||||
"dates": 2,
|
||||
"quota": 10,
|
||||
"user": 2,
|
||||
"domain": 1,
|
||||
"address": "admin"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 3,
|
||||
"model": "admin.mailbox",
|
||||
"fields": {
|
||||
"dates": 5,
|
||||
"quota": 10,
|
||||
"user": 4,
|
||||
"domain": 1,
|
||||
"address": "user"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "admin.alias",
|
||||
"fields": {
|
||||
"dates": 3,
|
||||
"enabled": true,
|
||||
"domain": 1,
|
||||
"mboxes": [1],
|
||||
"address": "postmaster",
|
||||
"extmboxes": ""}},
|
||||
{
|
||||
"pk": 1,
|
||||
"model": "core.extension",
|
||||
"fields": {
|
||||
"enabled": true,
|
||||
"name": "limits"}},
|
||||
{
|
||||
"pk": 2,
|
||||
"model": "core.extension",
|
||||
"fields": {
|
||||
"enabled": true,
|
||||
"name": "postfix_autoreply"}},
|
||||
{
|
||||
"pk": 3,
|
||||
"model": "core.extension",
|
||||
"fields": {
|
||||
"enabled": true,
|
||||
"name": "stats"}},
|
||||
{
|
||||
"pk": 4,
|
||||
"model": "core.extension",
|
||||
"fields": {
|
||||
"enabled": true,
|
||||
"name": "amavis"}},
|
||||
{
|
||||
"pk": 5,
|
||||
"model": "core.extension",
|
||||
"fields": {
|
||||
"enabled": true,
|
||||
"name": "webmail"}},
|
||||
{
|
||||
"pk": 6,
|
||||
"model": "core.extension",
|
||||
"fields": {
|
||||
"enabled": true,
|
||||
"name": "sievefilters"}},
|
||||
{
|
||||
"pk": 7,
|
||||
"model": "core.extension",
|
||||
"fields": {
|
||||
"enabled": true,
|
||||
"name": "postfix_relay_domains"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": 8,
|
||||
"model": "core.extension",
|
||||
"fields": {
|
||||
"enabled": true,
|
||||
"name": "radicale"
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": "admin@demo.local",
|
||||
"model": "admin.quota",
|
||||
"fields": {
|
||||
"username": "admin@demo.local",
|
||||
"bytes": 0,
|
||||
"messages": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"pk": "user@demo.local",
|
||||
"model": "admin.quota",
|
||||
"fields": {
|
||||
"username": "user@demo.local",
|
||||
"bytes": 0,
|
||||
"messages": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
from .domain import (
|
||||
DomainFormGeneral, DomainFormOptions, DomainForm, DomainWizard
|
||||
)
|
||||
from .account import (
|
||||
AccountFormGeneral, AccountFormMail, AccountPermissionsForm,
|
||||
AccountForm, AccountWizard
|
||||
)
|
||||
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', 'DomainWizard', 'AccountWizard'
|
||||
]
|
||||
@@ -1,478 +0,0 @@
|
||||
"""Forms related to accounts management."""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import QueryDict
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from modoboa.core.models import User
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, Mailbox, Alias
|
||||
)
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.lib.email_utils import split_mailbox
|
||||
from modoboa.lib.exceptions import PermDeniedException, Conflict, NotFound
|
||||
from modoboa.lib.form_utils import (
|
||||
DomainNameField, DynamicForm, TabForms, WizardForm
|
||||
)
|
||||
from modoboa.lib.permissions import get_account_roles
|
||||
from modoboa.lib.web_utils import render_to_json_response
|
||||
|
||||
|
||||
class AccountFormGeneral(forms.ModelForm):
|
||||
username = forms.CharField(
|
||||
label=ugettext_lazy("Username"),
|
||||
help_text=ugettext_lazy(
|
||||
"The user's name. Must be a valid e-mail address for simple users "
|
||||
"or administrators with a mailbox."
|
||||
)
|
||||
)
|
||||
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=ugettext_lazy("Password"), widget=forms.widgets.PasswordInput
|
||||
)
|
||||
password2 = forms.CharField(
|
||||
label=ugettext_lazy("Confirmation"),
|
||||
widget=forms.widgets.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 = OrderedDict(
|
||||
(key, self.fields[key]) for key in
|
||||
['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(attrs={"class": "form-control"}),
|
||||
required=False
|
||||
)
|
||||
else:
|
||||
self.fields["role"].choices = [('', ugettext_lazy("Choose"))]
|
||||
self.fields["role"].choices += \
|
||||
get_account_roles(user, kwargs['instance']) \
|
||||
if 'instance' in kwargs else get_account_roles(user)
|
||||
|
||||
if "instance" in kwargs:
|
||||
account = kwargs["instance"]
|
||||
domain_disabled = account.mailbox_set.count() and \
|
||||
not account.mailbox_set.all()[0].domain.enabled
|
||||
if domain_disabled:
|
||||
self.fields["is_active"].widget.attrs['disabled'] = "disabled"
|
||||
if args:
|
||||
if args[0].get("password1", "") == "" \
|
||||
and args[0].get("password2", "") == "":
|
||||
self.fields["password1"].required = False
|
||||
self.fields["password2"].required = False
|
||||
if domain_disabled:
|
||||
del self.fields["is_active"]
|
||||
self.fields["role"].initial = account.group
|
||||
if not account.is_local \
|
||||
and parameters.get_admin(
|
||||
"LDAP_AUTH_METHOD", app="core") == "directbind":
|
||||
del self.fields["password1"]
|
||||
del self.fields["password2"]
|
||||
|
||||
def domain_is_disabled(self):
|
||||
"""Little shortcut to get the domain's state.
|
||||
|
||||
We need this information inside a template and the form is the
|
||||
only object available...
|
||||
|
||||
"""
|
||||
if not self.instance.mailbox_set.count():
|
||||
return False
|
||||
return not self.instance.mailbox_set.all()[0].domain.enabled
|
||||
|
||||
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 "role" not 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 PermDeniedException(_("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):
|
||||
|
||||
"""Form to handle mail part."""
|
||||
|
||||
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": "form-control"})
|
||||
)
|
||||
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"]
|
||||
else:
|
||||
self.mb = None
|
||||
super(AccountFormMail, self).__init__(*args, **kwargs)
|
||||
self.field_widths = {
|
||||
"quota": 3
|
||||
}
|
||||
self.extra_fields = []
|
||||
result = events.raiseQueryEvent('ExtraFormFields', 'mailform', self.mb)
|
||||
for fname, field in result:
|
||||
self.fields[fname] = field
|
||||
self.extra_fields.append(fname)
|
||||
if 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 clean(self):
|
||||
"""Custom fields validation.
|
||||
|
||||
Check if quota is >= 0 only when the domain value is not used.
|
||||
"""
|
||||
super(AccountFormMail, self).clean()
|
||||
if not self.cleaned_data["quota_act"] \
|
||||
and self.cleaned_data['quota'] is not None:
|
||||
if self.cleaned_data["quota"] < 0:
|
||||
self.add_error("quota", _("Must be a positive integer"))
|
||||
return self.cleaned_data
|
||||
|
||||
def create_mailbox(self, user, account):
|
||||
"""Create a mailbox associated to :kw:`account`."""
|
||||
locpart, domname = split_mailbox(self.cleaned_data["email"])
|
||||
try:
|
||||
domain = Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
raise NotFound(_("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 Conflict(
|
||||
_("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)
|
||||
|
||||
def update_mailbox(self, user, account):
|
||||
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:
|
||||
self.mb.old_full_address = self.mb.full_address
|
||||
local_part, domname = split_mailbox(newaddress)
|
||||
try:
|
||||
domain = Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
raise NotFound(_("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()
|
||||
events.raiseEvent('MailboxModified', self.mb)
|
||||
|
||||
def _update_aliases(self, user, account):
|
||||
"""Update mailbox aliases."""
|
||||
aliases = []
|
||||
for name, value in self.cleaned_data.iteritems():
|
||||
if not name.startswith("aliases"):
|
||||
continue
|
||||
if value == "":
|
||||
continue
|
||||
aliases.append(value.lower())
|
||||
|
||||
for alias in self.mb.alias_set.all():
|
||||
if alias.full_address not in aliases:
|
||||
if len(alias.get_recipients()) >= 2:
|
||||
continue
|
||||
alias.delete()
|
||||
else:
|
||||
aliases.remove(alias.full_address)
|
||||
if not aliases:
|
||||
return
|
||||
events.raiseEvent(
|
||||
"CanCreate", user, "mailbox_aliases", len(aliases)
|
||||
)
|
||||
for alias in aliases:
|
||||
local_part, domname = split_mailbox(alias)
|
||||
try:
|
||||
self.mb.alias_set.get(address=local_part, domain__name=domname)
|
||||
except Alias.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
al = Alias(address=local_part, enabled=account.is_active)
|
||||
al.domain = Domain.objects.get(name=domname)
|
||||
al.save(int_rcpts=[self.mb])
|
||||
al.post_create(user)
|
||||
|
||||
def save(self, user, account):
|
||||
"""Save or update account mailbox."""
|
||||
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:
|
||||
self.create_mailbox(user, account)
|
||||
else:
|
||||
self.update_mailbox(user, account)
|
||||
events.raiseEvent(
|
||||
'SaveExtraFormFields', 'mailform', self.mb, self.cleaned_data
|
||||
)
|
||||
|
||||
account.email = self.cleaned_data["email"]
|
||||
account.save()
|
||||
|
||||
self._update_aliases(user, account)
|
||||
|
||||
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, dom in enumerate(Domain.objects.get_for_admin(self.account)):
|
||||
name = "domains_%d" % (pos + 1)
|
||||
self._create_field(DomainNameField, name, dom.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 value not 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 filter(lambda name: self.cleaned_data[name] == domain.name,
|
||||
self.cleaned_data.keys()):
|
||||
domain.remove_admin(self.account)
|
||||
|
||||
|
||||
class AccountForm(TabForms):
|
||||
|
||||
"""Account edition form."""
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
self.user = request.user
|
||||
self.forms = [
|
||||
dict(id="general", title=_("General"),
|
||||
formtpl="admin/account_general_form.html",
|
||||
cls=AccountFormGeneral,
|
||||
new_args=[self.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 = [self.user]
|
||||
if "instances" in kwargs:
|
||||
cbargs += [kwargs["instances"]["general"]]
|
||||
self.forms += events.raiseQueryEvent("ExtraAccountForm", *cbargs)
|
||||
|
||||
super(AccountForm, self).__init__(request, *args, **kwargs)
|
||||
|
||||
def extra_context(self, context):
|
||||
account = self.instances["general"]
|
||||
context.update({
|
||||
'title': account.username,
|
||||
'formid': 'accountform',
|
||||
'action': reverse("admin:account_change", args=[account.id]),
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
extra_forms = events.raiseQueryEvent(
|
||||
"CheckExtraAccountForm", self.account, form)
|
||||
if False in extra_forms:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_valid(self):
|
||||
"""Two steps validation.
|
||||
"""
|
||||
self.instances["general"].oldgroup = self.instances["general"].group
|
||||
if super(AccountForm, self).is_valid(mandatory_only=True):
|
||||
self.account = self.forms[0]["instance"].save()
|
||||
return super(AccountForm, self).is_valid(optional_only=True)
|
||||
return False
|
||||
|
||||
def save(self):
|
||||
"""Custom save method
|
||||
|
||||
As forms interact with each other, it is simpler to make
|
||||
custom code to save them.
|
||||
"""
|
||||
events.raiseEvent(
|
||||
"AccountModified", self.instances["general"], self.account
|
||||
)
|
||||
self.forms[1]["instance"].save(self.user, self.account)
|
||||
if len(self.forms) <= 2:
|
||||
return
|
||||
for f in self.forms[2:]:
|
||||
f["instance"].save()
|
||||
|
||||
def done(self):
|
||||
return render_to_json_response(_("Account updated"))
|
||||
|
||||
|
||||
class AccountWizard(WizardForm):
|
||||
|
||||
"""Account creation wizard.
|
||||
"""
|
||||
|
||||
def __init__(self, request):
|
||||
super(AccountWizard, self).__init__(request)
|
||||
self.add_step(
|
||||
AccountFormGeneral, _("General"), new_args=[request.user]
|
||||
)
|
||||
self.add_step(
|
||||
AccountFormMail, _("Mail"), formtpl="admin/mailform.html"
|
||||
)
|
||||
|
||||
def extra_context(self, context):
|
||||
context.update({
|
||||
'title': _("New account"),
|
||||
'action': reverse("admin:account_add"),
|
||||
'formid': 'newaccount_form'
|
||||
})
|
||||
|
||||
def done(self):
|
||||
from modoboa.lib.web_utils import render_to_json_response
|
||||
|
||||
account = self.first_step.form.save()
|
||||
account.post_create(self.request.user)
|
||||
mailform = self.steps[1].form
|
||||
mailform.save(self.request.user, account)
|
||||
return render_to_json_response(_("Account created"))
|
||||
@@ -1,152 +0,0 @@
|
||||
"""Forms related to aliases management."""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django import forms
|
||||
from django.http import QueryDict
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, Mailbox, Alias
|
||||
)
|
||||
from modoboa.lib.email_utils import split_mailbox
|
||||
from modoboa.lib.exceptions import BadRequest, NotFound, Conflict
|
||||
from modoboa.lib.form_utils import (
|
||||
DynamicForm
|
||||
)
|
||||
|
||||
|
||||
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)."
|
||||
),
|
||||
widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
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."
|
||||
),
|
||||
widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Alias
|
||||
fields = ("enabled",)
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.user = user
|
||||
super(AliasForm, self).__init__(*args, **kwargs)
|
||||
self.fields = OrderedDict(
|
||||
(key, self.fields[key]) for key in
|
||||
['email', 'recipients', 'enabled']
|
||||
)
|
||||
|
||||
if len(args) and isinstance(args[0], QueryDict):
|
||||
if "instance" in kwargs:
|
||||
if not kwargs["instance"].domain.enabled:
|
||||
del self.fields["enabled"]
|
||||
self._load_from_qdict(args[0], "recipients", forms.EmailField)
|
||||
elif "instance" in kwargs:
|
||||
dlist = kwargs["instance"]
|
||||
self.fields["email"].initial = dlist.full_address
|
||||
if not dlist.domain.enabled:
|
||||
self.fields["enabled"].widget.attrs["disabled"] = "disabled"
|
||||
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"].lower()
|
||||
|
||||
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 BadRequest(
|
||||
u"%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 NotFound(
|
||||
_("Local recipient %s not found" % v)
|
||||
)
|
||||
if rcpt in self.int_rcpts:
|
||||
raise Conflict(
|
||||
_("Recipient %s already present" % v)
|
||||
)
|
||||
self.int_rcpts += [rcpt]
|
||||
total += 1
|
||||
continue
|
||||
|
||||
if v in self.ext_rcpts:
|
||||
raise Conflict(
|
||||
_("Recipient %s already present" % v)
|
||||
)
|
||||
self.ext_rcpts += [v]
|
||||
total += 1
|
||||
|
||||
if total == 0:
|
||||
raise BadRequest(_("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
|
||||
@@ -1,362 +0,0 @@
|
||||
"""Forms related to domains management."""
|
||||
|
||||
from django import forms
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import QueryDict
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from modoboa.core.models import User
|
||||
from modoboa.extensions.admin.lib import check_if_domain_exists
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, DomainAlias, Mailbox, Alias, Quota
|
||||
)
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.lib.exceptions import ModoboaException, Conflict
|
||||
from modoboa.lib.form_utils import (
|
||||
DomainNameField, YesNoField, WizardForm, DynamicForm, TabForms
|
||||
)
|
||||
from modoboa.lib.web_utils import render_to_json_response
|
||||
|
||||
|
||||
class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
quota = forms.IntegerField(
|
||||
label=ugettext_lazy("Quota"),
|
||||
required=False,
|
||||
help_text=ugettext_lazy(
|
||||
"Default quota in MB applied to mailboxes. Leave empty to use the "
|
||||
"default value."
|
||||
)
|
||||
)
|
||||
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."
|
||||
)
|
||||
)
|
||||
name = DomainNameField(
|
||||
widget=forms.TextInput()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Domain
|
||||
fields = ("name", "quota", "aliases", "enabled")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.oldname = None
|
||||
if "instance" in kwargs:
|
||||
self.oldname = kwargs["instance"].name
|
||||
super(DomainFormGeneral, self).__init__(*args, **kwargs)
|
||||
|
||||
self.field_widths = {
|
||||
"quota": 3
|
||||
}
|
||||
|
||||
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_quota(self):
|
||||
if self.cleaned_data['quota'] is None:
|
||||
return int(parameters.get_admin('DEFAULT_DOMAIN_QUOTA'))
|
||||
if self.cleaned_data['quota'] < 0:
|
||||
raise forms.ValidationError(_("Must be a positive integer"))
|
||||
return self.cleaned_data['quota']
|
||||
|
||||
def clean(self):
|
||||
"""Custom fields validation.
|
||||
|
||||
We want to prevent duplicate names between domains and domain
|
||||
aliases. Extensions have the possibility to declare other
|
||||
objects (see *CheckDomainName* event).
|
||||
|
||||
The validation way is not very smart...
|
||||
"""
|
||||
super(DomainFormGeneral, self).clean()
|
||||
cleaned_data = self.cleaned_data
|
||||
name = cleaned_data["name"]
|
||||
label = check_if_domain_exists(name, [(DomainAlias, _('domain alias'))])
|
||||
if label is not None:
|
||||
self.add_error(
|
||||
"name", _("A %s with this name already exists") % unicode(label)
|
||||
)
|
||||
for k in cleaned_data.keys():
|
||||
if not k.startswith("aliases"):
|
||||
continue
|
||||
if cleaned_data[k] == "":
|
||||
del cleaned_data[k]
|
||||
continue
|
||||
if cleaned_data[k] == name:
|
||||
self._errors[k] = self.error_class(
|
||||
[_("A %s with this name already exists") % _("domain")]
|
||||
)
|
||||
del cleaned_data[k]
|
||||
continue
|
||||
label = check_if_domain_exists(
|
||||
cleaned_data[k], [(Domain, _("domain"))])
|
||||
if label is not None:
|
||||
self.add_error(
|
||||
k, _("A %s with this name already exists") % unicode(label))
|
||||
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):
|
||||
username = q.username.replace(
|
||||
'@%s' % self.oldname, '@%s' % domain.name)
|
||||
newq = Quota.objects.create(
|
||||
username=username, bytes=q.bytes, messages=q.messages)
|
||||
q.delete()
|
||||
|
||||
def save(self, user, commit=True, domalias_post_create=False):
|
||||
"""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)
|
||||
aliases = []
|
||||
for k, v in self.cleaned_data.iteritems():
|
||||
if not k.startswith("aliases"):
|
||||
continue
|
||||
if v in ["", None]:
|
||||
continue
|
||||
aliases.append(v)
|
||||
for dalias in d.domainalias_set.all():
|
||||
if dalias.name not in aliases:
|
||||
dalias.delete()
|
||||
else:
|
||||
aliases.remove(dalias.name)
|
||||
if aliases:
|
||||
events.raiseEvent(
|
||||
"CanCreate", user, "domain_aliases", len(aliases))
|
||||
for alias in aliases:
|
||||
try:
|
||||
d.domainalias_set.get(name=alias)
|
||||
except DomainAlias.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
al = DomainAlias(name=alias, target=d, enabled=d.enabled)
|
||||
al.save(creator=user) if domalias_post_create else al.save()
|
||||
|
||||
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."
|
||||
),
|
||||
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, user, *args, **kwargs):
|
||||
super(DomainFormOptions, self).__init__(*args, **kwargs)
|
||||
result = events.raiseQueryEvent('UserCanSetRole', user, 'DomainAdmins')
|
||||
if False in result:
|
||||
self.fields = {}
|
||||
return
|
||||
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):
|
||||
"""Ensure admin username is an email address."""
|
||||
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 not self.fields:
|
||||
return
|
||||
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 Conflict(_("User '%s' already exists" % username))
|
||||
events.raiseEvent("CanCreate", user, "mailboxes")
|
||||
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":
|
||||
events.raiseEvent("CanCreate", user, "mailbox_aliases")
|
||||
alias = Alias(address="postmaster", domain=domain, enabled=True)
|
||||
alias.save(int_rcpts=[mb])
|
||||
alias.post_create(user)
|
||||
|
||||
domain.add_admin(da)
|
||||
|
||||
|
||||
class DomainForm(TabForms):
|
||||
|
||||
"""Domain edition form."""
|
||||
|
||||
template_name = "admin/editdomainform.html"
|
||||
|
||||
def __init__(self, request, *args, **kwargs):
|
||||
self.user = request.user
|
||||
self.forms = []
|
||||
if self.user.has_perm("admin.change_domain"):
|
||||
self.forms.append({
|
||||
"id": "general",
|
||||
"title": _("General"),
|
||||
"formtpl": "admin/domain_general_form.html",
|
||||
"cls": DomainFormGeneral,
|
||||
"mandatory": True
|
||||
})
|
||||
|
||||
cbargs = [self.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__(request, *args, **kwargs)
|
||||
|
||||
def extra_context(self, context):
|
||||
domain = self.instances["general"]
|
||||
domadmins = [u for u in domain.admins
|
||||
if self.request.user.can_access(u) and not u.is_superuser]
|
||||
if not self.request.user.is_superuser:
|
||||
domadmins = [u for u in domadmins if u.group == "DomainAdmins"]
|
||||
context.update({
|
||||
"title": domain.name,
|
||||
"action": reverse("admin:domain_change", args=[domain.pk]),
|
||||
"formid": "domform",
|
||||
"domain": domain,
|
||||
"domadmins": domadmins
|
||||
})
|
||||
|
||||
def is_valid(self):
|
||||
"""Custom validation.
|
||||
|
||||
We just save the current name before it is potentially
|
||||
modified.
|
||||
|
||||
"""
|
||||
self.instances["general"].oldname = self.instances["general"].name
|
||||
return super(DomainForm, self).is_valid()
|
||||
|
||||
def save(self):
|
||||
"""Custom save method.
|
||||
|
||||
As forms interact with each other, it is easier to make custom
|
||||
code to save them.
|
||||
"""
|
||||
self.forms[0]['instance'].save(
|
||||
self.request.user, domalias_post_create=True
|
||||
)
|
||||
for f in self.forms[1:]:
|
||||
f["instance"].save(self.request.user)
|
||||
|
||||
def done(self):
|
||||
events.raiseEvent("DomainModified", self.instances["general"])
|
||||
return render_to_json_response(_("Domain modified"))
|
||||
|
||||
|
||||
class DomainWizard(WizardForm):
|
||||
|
||||
"""Domain creation wizard.
|
||||
"""
|
||||
|
||||
def __init__(self, request):
|
||||
super(DomainWizard, self).__init__(request)
|
||||
self.add_step(
|
||||
DomainFormGeneral, _("General"),
|
||||
formtpl="admin/domain_general_form.html"
|
||||
)
|
||||
self.add_step(
|
||||
DomainFormOptions, _("Options"),
|
||||
formtpl="admin/domain_options_form.html",
|
||||
new_args=[self.request.user]
|
||||
)
|
||||
|
||||
def extra_context(self, context):
|
||||
context.update({
|
||||
"title": _("New domain"),
|
||||
"action": reverse("admin:domain_add"),
|
||||
"formid": "domform"
|
||||
})
|
||||
|
||||
def done(self):
|
||||
genform = self.first_step.form
|
||||
domain = genform.save(self.request.user)
|
||||
domain.post_create(self.request.user)
|
||||
try:
|
||||
self.steps[1].form.save(self.request.user, domain)
|
||||
except ModoboaException:
|
||||
from django.db import transaction
|
||||
transaction.rollback()
|
||||
raise
|
||||
return render_to_json_response(_("Domain created"))
|
||||
@@ -1,45 +0,0 @@
|
||||
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,
|
||||
widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
sepchar = forms.CharField(
|
||||
label=ugettext_lazy("Separator"),
|
||||
max_length=1,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ExportDataForm, self).__init__(*args, **kwargs)
|
||||
self.fields["sepchar"].widget.attrs = {"class": "col-md-1 form-control"}
|
||||
|
||||
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"
|
||||
@@ -1,46 +0,0 @@
|
||||
"""Forms related to forwards management."""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain
|
||||
)
|
||||
from modoboa.lib.email_utils import split_mailbox
|
||||
from modoboa.lib.exceptions import BadRequest, PermDeniedException
|
||||
|
||||
|
||||
class ForwardForm(forms.Form):
|
||||
dest = forms.CharField(
|
||||
label=ugettext_lazy("Recipient(s)"),
|
||||
widget=forms.Textarea(attrs={"class": "form-control"}),
|
||||
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 BadRequest("Invalid mailbox syntax for %s" % d)
|
||||
try:
|
||||
Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
self.dests += [d]
|
||||
else:
|
||||
raise PermDeniedException(
|
||||
_("You can't define a forward to a local destination. "
|
||||
"Please ask your administrator to create an alias "
|
||||
"instead.")
|
||||
)
|
||||
@@ -1,43 +0,0 @@
|
||||
"""Forms related to import operations."""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
|
||||
class ImportDataForm(forms.Form):
|
||||
|
||||
"""Base form to import objects."""
|
||||
|
||||
sourcefile = forms.FileField(label=ugettext_lazy("Select a file"))
|
||||
sepchar = forms.CharField(
|
||||
label=ugettext_lazy("Separator"),
|
||||
max_length=1,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={"class": "form-control"})
|
||||
)
|
||||
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": "col-md-1 form-control"}
|
||||
|
||||
def clean_sepchar(self):
|
||||
if self.cleaned_data["sepchar"] == "":
|
||||
return ";"
|
||||
return str(self.cleaned_data["sepchar"])
|
||||
|
||||
|
||||
class ImportIdentitiesForm(ImportDataForm):
|
||||
|
||||
"""A form to import identities."""
|
||||
|
||||
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"
|
||||
)
|
||||
)
|
||||
@@ -1,114 +0,0 @@
|
||||
# coding: utf-8
|
||||
from functools import wraps
|
||||
from itertools import chain
|
||||
from django.db.models import Q
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import ugettext as _
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.exceptions import PermDeniedException
|
||||
from modoboa.lib.email_utils import split_mailbox
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, 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 PermDeniedException(_("A mailbox is required"))
|
||||
return wrapped_f
|
||||
return decorator
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
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)
|
||||
|
||||
|
||||
def get_domains(user, domfilter=None, searchquery=None, **extrafilters):
|
||||
"""Return all the domains the user can access.
|
||||
|
||||
:param ``User`` user: user object
|
||||
:param str searchquery: filter
|
||||
:rtype: list
|
||||
:return: a list of domains and/or relay domains
|
||||
"""
|
||||
domains = []
|
||||
if domfilter is None or not domfilter or domfilter == 'domain':
|
||||
domains = Domain.objects.get_for_admin(user)
|
||||
if searchquery is not None:
|
||||
q = Q(name__contains=searchquery)
|
||||
q |= Q(domainalias__name__contains=searchquery)
|
||||
domains = domains.filter(q).distinct()
|
||||
extra_domain_entries = events.raiseQueryEvent(
|
||||
'ExtraDomainEntries', user, domfilter, searchquery, **extrafilters
|
||||
)
|
||||
return chain(domains, extra_domain_entries)
|
||||
|
||||
|
||||
def check_if_domain_exists(name, extra_checks=None):
|
||||
"""Check if a domain already exists.
|
||||
|
||||
We not only look for domains, we also look for every object that
|
||||
could conflict with a domain (domain alias, etc.)
|
||||
|
||||
"""
|
||||
dtypes = events.raiseQueryEvent('CheckDomainName')
|
||||
if extra_checks is not None:
|
||||
dtypes = extra_checks + dtypes
|
||||
for dtype, label in dtypes:
|
||||
if dtype.objects.filter(name=name).exists():
|
||||
return label
|
||||
return None
|
||||
@@ -1,97 +0,0 @@
|
||||
|
||||
import logging
|
||||
from optparse import make_option
|
||||
import os
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from modoboa.core.management.commands import CloseConnectionMixin
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.sysutils import exec_cmd
|
||||
from modoboa.lib.exceptions import InternalError
|
||||
from modoboa.extensions.admin.models import MailboxOperation
|
||||
|
||||
|
||||
class OperationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Command(BaseCommand, CloseConnectionMixin):
|
||||
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):
|
||||
try:
|
||||
if parameters.get_admin("HANDLE_MAILBOXES") == 'no':
|
||||
return
|
||||
except parameters.NotDefined:
|
||||
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, InternalError) 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'])
|
||||
@@ -1,161 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Alias',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('address', models.CharField(help_text="The alias address (without the domain part). For a 'catch-all' address, just enter an * character.", max_length=254, verbose_name='address')),
|
||||
('extmboxes', models.TextField(blank=True)),
|
||||
('enabled', models.BooleanField(default=True, help_text='Check to activate this alias', verbose_name='enabled')),
|
||||
('aliases', models.ManyToManyField(help_text='The aliases this alias points to', to='admin.Alias')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['domain__name', 'address'],
|
||||
'permissions': (('view_aliases', 'View aliases'),),
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Domain',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(help_text='The domain name', unique=True, max_length=100, verbose_name='name')),
|
||||
('quota', models.IntegerField()),
|
||||
('enabled', models.BooleanField(default=True, help_text='Check to activate this domain', verbose_name='enabled')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
'permissions': (('view_domain', 'View domain'), ('view_domains', 'View domains')),
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DomainAlias',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('name', models.CharField(help_text='The alias name', unique=True, max_length=100, verbose_name='name')),
|
||||
('enabled', models.BooleanField(default=True, help_text='Check to activate this alias', verbose_name='enabled')),
|
||||
],
|
||||
options={
|
||||
'permissions': (('view_domaliases', 'View domain aliases'),),
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Mailbox',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('address', models.CharField(help_text='Mailbox address (without the @domain.tld part)', max_length=252, verbose_name='address')),
|
||||
('quota', models.PositiveIntegerField()),
|
||||
('use_domain_quota', models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
'permissions': (('view_mailboxes', 'View mailboxes'),),
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MailboxOperation',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('type', models.CharField(max_length=20, choices=[(b'rename', b'rename'), (b'delete', b'delete')])),
|
||||
('argument', models.TextField()),
|
||||
('mailbox', models.ForeignKey(blank=True, to='admin.Mailbox', null=True)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ObjectDates',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('creation', models.DateTimeField(auto_now_add=True)),
|
||||
('last_modification', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Quota',
|
||||
fields=[
|
||||
('username', models.EmailField(max_length=254, serialize=False, primary_key=True)),
|
||||
('bytes', models.BigIntegerField(default=0)),
|
||||
('messages', models.IntegerField(default=0)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mailbox',
|
||||
name='dates',
|
||||
field=models.ForeignKey(to='admin.ObjectDates'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mailbox',
|
||||
name='domain',
|
||||
field=models.ForeignKey(to='admin.Domain'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='mailbox',
|
||||
name='user',
|
||||
field=models.ForeignKey(to=settings.AUTH_USER_MODEL),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='domainalias',
|
||||
name='dates',
|
||||
field=models.ForeignKey(to='admin.ObjectDates'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='domainalias',
|
||||
name='target',
|
||||
field=models.ForeignKey(verbose_name='target', to='admin.Domain', help_text='The domain this alias points to'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='domain',
|
||||
name='dates',
|
||||
field=models.ForeignKey(to='admin.ObjectDates'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alias',
|
||||
name='dates',
|
||||
field=models.ForeignKey(to='admin.ObjectDates'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alias',
|
||||
name='domain',
|
||||
field=models.ForeignKey(to='admin.Domain'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='alias',
|
||||
name='mboxes',
|
||||
field=models.ManyToManyField(help_text='The mailboxes this alias points to', to='admin.Mailbox', verbose_name='mailboxes'),
|
||||
preserve_default=True,
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='alias',
|
||||
unique_together=set([('address', 'domain')]),
|
||||
),
|
||||
]
|
||||
@@ -1,10 +0,0 @@
|
||||
from .base import AdminObject
|
||||
from .domain import Domain
|
||||
from .domain_alias import DomainAlias
|
||||
from .mailbox import Mailbox, Quota, MailboxOperation
|
||||
from .alias import Alias
|
||||
|
||||
__all__ = [
|
||||
'Domain', 'DomainAlias', 'Mailbox', 'Quota', 'Alias',
|
||||
'MailboxOperation', 'AdminObject'
|
||||
]
|
||||
@@ -1,211 +0,0 @@
|
||||
"""Models related to aliases management."""
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
import reversion
|
||||
|
||||
from .base import AdminObject
|
||||
from .domain import Domain
|
||||
from .mailbox import Mailbox
|
||||
from modoboa.lib.email_utils import split_mailbox
|
||||
from modoboa.lib.exceptions import (
|
||||
PermDeniedException, BadRequest, Conflict
|
||||
)
|
||||
|
||||
|
||||
class Alias(AdminObject):
|
||||
|
||||
"""
|
||||
Mailbox alias.
|
||||
"""
|
||||
address = models.CharField(
|
||||
ugettext_lazy('address'), max_length=254,
|
||||
help_text=ugettext_lazy(
|
||||
"The alias address (without the domain part). For a 'catch-all' "
|
||||
"address, just enter an * character."
|
||||
)
|
||||
)
|
||||
domain = models.ForeignKey(Domain)
|
||||
mboxes = models.ManyToManyField(
|
||||
Mailbox, verbose_name=ugettext_lazy('mailboxes'),
|
||||
help_text=ugettext_lazy("The mailboxes this alias points to")
|
||||
)
|
||||
aliases = models.ManyToManyField(
|
||||
'Alias',
|
||||
help_text=ugettext_lazy("The aliases this alias points to")
|
||||
)
|
||||
extmboxes = models.TextField(blank=True)
|
||||
enabled = models.BooleanField(
|
||||
ugettext_lazy('enabled'),
|
||||
help_text=ugettext_lazy("Check to activate this alias"),
|
||||
default=True
|
||||
)
|
||||
_objectname = 'MailboxAlias'
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_aliases", "View aliases"),
|
||||
)
|
||||
unique_together = (("address", "domain"),)
|
||||
ordering = ["domain__name", "address"]
|
||||
app_label = 'admin'
|
||||
|
||||
def __unicode__(self):
|
||||
return self.full_address
|
||||
|
||||
@property
|
||||
def full_address(self):
|
||||
return "%s@%s" % (self.address, self.domain.name)
|
||||
|
||||
@property
|
||||
def identity(self):
|
||||
return self.full_address
|
||||
|
||||
@property
|
||||
def name_or_rcpt(self):
|
||||
rcpts_count = self.get_recipients_count()
|
||||
if not rcpts_count:
|
||||
return "---"
|
||||
rcpts = self.get_recipients()
|
||||
if rcpts_count > 1:
|
||||
return "%s, ..." % rcpts[0]
|
||||
return rcpts[0]
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
cpt = self.get_recipients_count()
|
||||
if cpt > 1:
|
||||
return "dlist"
|
||||
if self.extmboxes != "":
|
||||
return "forward"
|
||||
return "alias"
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
labels = {
|
||||
"dlist": _("distribution list"),
|
||||
"forward": _("forward"),
|
||||
"alias": _("alias")
|
||||
}
|
||||
altype = self.type
|
||||
return [{"name": altype, "label": labels[altype], "type": "idt"}]
|
||||
|
||||
def post_create(self, creator):
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
super(Alias, self).post_create(creator)
|
||||
if creator.is_superuser:
|
||||
for admin in self.domain.admins:
|
||||
grant_access_to_object(admin, self)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Custom save method."""
|
||||
if 'ext_rcpts' in kwargs:
|
||||
self.extmboxes = ",".join(kwargs['ext_rcpts']) \
|
||||
if kwargs['ext_rcpts'] else ""
|
||||
del kwargs['ext_rcpts']
|
||||
if 'int_rcpts' in kwargs:
|
||||
int_rcpts = kwargs['int_rcpts']
|
||||
del kwargs['int_rcpts']
|
||||
else:
|
||||
int_rcpts = []
|
||||
super(Alias, self).save(*args, **kwargs)
|
||||
curaliases = self.aliases.all()
|
||||
curmboxes = self.mboxes.all()
|
||||
for rcpt in int_rcpts:
|
||||
if isinstance(rcpt, Alias):
|
||||
if rcpt not in curaliases:
|
||||
self.aliases.add(rcpt)
|
||||
continue
|
||||
if rcpt not in curmboxes:
|
||||
self.mboxes.add(rcpt)
|
||||
for rcpt in curaliases:
|
||||
if rcpt not in int_rcpts:
|
||||
self.aliases.remove(rcpt)
|
||||
for rcpt in curmboxes:
|
||||
if rcpt not in int_rcpts:
|
||||
self.mboxes.remove(rcpt)
|
||||
|
||||
def get_recipients(self, with_external=True):
|
||||
"""Return the recipients list.
|
||||
|
||||
Internal and external addresses are mixed into a single list.
|
||||
|
||||
:param bool with_external: include external addresses or not
|
||||
:rtype: list
|
||||
:return: a list of addresses
|
||||
"""
|
||||
result = [al.full_address for al in self.aliases.all()]
|
||||
result += [mb.full_address for mb in self.mboxes.all()]
|
||||
if with_external and self.extmboxes != "":
|
||||
result += self.extmboxes.split(',')
|
||||
return result
|
||||
|
||||
def get_recipients_count(self):
|
||||
"""Return the number of recipients of this alias.
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
total = 0
|
||||
if self.extmboxes != "":
|
||||
total += len(self.extmboxes.split(','))
|
||||
return total + self.aliases.count() + self.mboxes.count()
|
||||
|
||||
def from_csv(self, user, row, expected_elements=5):
|
||||
"""Create a new alias from a CSV file entry
|
||||
|
||||
"""
|
||||
if len(row) < expected_elements:
|
||||
raise BadRequest(_("Invalid line: %s" % row))
|
||||
localpart, domname = split_mailbox(row[1].strip())
|
||||
try:
|
||||
domain = Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
raise BadRequest(_("Domain '%s' does not exist" % domname))
|
||||
if not user.can_access(domain):
|
||||
raise PermDeniedException
|
||||
try:
|
||||
Alias.objects.get(address=localpart, domain__name=domain)
|
||||
except Alias.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
raise Conflict
|
||||
self.address = localpart
|
||||
self.domain = domain
|
||||
self.enabled = (row[2].strip() in ["True", "1", "yes", "y"])
|
||||
int_rcpts = []
|
||||
ext_rcpts = []
|
||||
for rcpt in row[3:]:
|
||||
rcpt = rcpt.strip()
|
||||
if not rcpt:
|
||||
continue
|
||||
localpart, domname = split_mailbox(rcpt)
|
||||
try:
|
||||
Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
ext_rcpts += [rcpt]
|
||||
continue
|
||||
try:
|
||||
target = Alias.objects.get(
|
||||
domain__name=domname, address=localpart
|
||||
)
|
||||
if target.full_address == self.full_address:
|
||||
target = None
|
||||
except Alias.DoesNotExist:
|
||||
target = None
|
||||
if target is None:
|
||||
try:
|
||||
target = Mailbox.objects.get(address=localpart,
|
||||
domain__name=domname)
|
||||
except Mailbox.DoesNotExist:
|
||||
raise BadRequest(_("Local recipient %s not found" % rcpt))
|
||||
int_rcpts += [target]
|
||||
self.save(int_rcpts=int_rcpts, ext_rcpts=ext_rcpts)
|
||||
self.post_create(user)
|
||||
|
||||
def to_csv(self, csvwriter):
|
||||
row = [self.type, self.full_address.encode("utf-8"), self.enabled]
|
||||
row += self.get_recipients()
|
||||
csvwriter.writerow(row)
|
||||
|
||||
reversion.register(Alias)
|
||||
@@ -1,81 +0,0 @@
|
||||
from django.db import models
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.permissions import (
|
||||
grant_access_to_object, ungrant_access_to_object
|
||||
)
|
||||
|
||||
|
||||
class ObjectDates(models.Model):
|
||||
|
||||
"""Dates recording for admin objects
|
||||
|
||||
This table keeps creation and last modification dates for Domains,
|
||||
domain aliases, mailboxes and aliases objects.
|
||||
"""
|
||||
creation = models.DateTimeField(auto_now_add=True)
|
||||
last_modification = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
app_label = 'admin'
|
||||
|
||||
@staticmethod
|
||||
def set_for_object(obj):
|
||||
"""Initialize or update dates for a given object.
|
||||
|
||||
:param obj: an admin object (Domain, Mailbox, etc)
|
||||
"""
|
||||
try:
|
||||
dates = getattr(obj, "dates")
|
||||
except ObjectDates.DoesNotExist:
|
||||
dates = ObjectDates()
|
||||
dates.save()
|
||||
obj.dates = dates
|
||||
|
||||
|
||||
class AdminObject(models.Model):
|
||||
|
||||
"""Abstract model to support dates
|
||||
|
||||
Inherit from this model to automatically add the "dates" feature
|
||||
to another model. It defines the appropriate field and handles
|
||||
saves.
|
||||
"""
|
||||
dates = models.ForeignKey(ObjectDates)
|
||||
_objectname = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@property
|
||||
def creation(self):
|
||||
return self.dates.creation
|
||||
|
||||
@property
|
||||
def last_modification(self):
|
||||
return self.dates.last_modification
|
||||
|
||||
@property
|
||||
def objectname(self):
|
||||
if self._objectname is None:
|
||||
return self.__class__.__name__
|
||||
return self._objectname
|
||||
|
||||
def post_create(self, creator):
|
||||
grant_access_to_object(creator, self, is_owner=True)
|
||||
events.raiseEvent("%sCreated" % self.objectname, creator, self)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
ObjectDates.set_for_object(self)
|
||||
if "creator" in kwargs:
|
||||
creator = kwargs["creator"]
|
||||
del kwargs["creator"]
|
||||
else:
|
||||
creator = None
|
||||
super(AdminObject, self).save(*args, **kwargs)
|
||||
if creator is not None:
|
||||
self.post_create(creator)
|
||||
|
||||
def delete(self):
|
||||
events.raiseEvent("%sDeleted" % self.objectname, self)
|
||||
ungrant_access_to_object(self)
|
||||
super(AdminObject, self).delete()
|
||||
@@ -1,185 +0,0 @@
|
||||
"""Models related to domains management."""
|
||||
|
||||
from django.contrib.contenttypes import generic
|
||||
from django.db import models
|
||||
from django.db.models.manager import Manager
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
import reversion
|
||||
|
||||
from .base import AdminObject
|
||||
from modoboa.core.models import User, ObjectAccess
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.lib.exceptions import BadRequest, Conflict
|
||||
|
||||
|
||||
class DomainManager(Manager):
|
||||
|
||||
def get_for_admin(self, admin):
|
||||
"""Return the domains belonging to this admin
|
||||
|
||||
The result is a ``QuerySet`` object, so this function can be used
|
||||
to fill ``ModelChoiceField`` objects.
|
||||
"""
|
||||
if admin.is_superuser:
|
||||
return self.get_query_set()
|
||||
return self.get_query_set().filter(owners__user=admin)
|
||||
|
||||
|
||||
class Domain(AdminObject):
|
||||
|
||||
"""Mail domain."""
|
||||
|
||||
name = models.CharField(ugettext_lazy('name'), max_length=100, unique=True,
|
||||
help_text=ugettext_lazy("The domain name"))
|
||||
quota = models.IntegerField()
|
||||
enabled = models.BooleanField(
|
||||
ugettext_lazy('enabled'),
|
||||
help_text=ugettext_lazy("Check to activate this domain"),
|
||||
default=True
|
||||
)
|
||||
owners = generic.GenericRelation(ObjectAccess)
|
||||
|
||||
objects = DomainManager()
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_domain", "View domain"),
|
||||
("view_domains", "View domains"),
|
||||
)
|
||||
ordering = ["name"]
|
||||
app_label = 'admin'
|
||||
|
||||
@property
|
||||
def domainalias_count(self):
|
||||
return self.domainalias_set.count()
|
||||
|
||||
@property
|
||||
def mailbox_count(self):
|
||||
return self.mailbox_set.count()
|
||||
|
||||
@property
|
||||
def mbalias_count(self):
|
||||
return self.alias_set.count()
|
||||
|
||||
@property
|
||||
def identities_count(self):
|
||||
"""Total number of identities in this domain."""
|
||||
return self.mailbox_set.count() + self.alias_set.count()
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return [{"name": "domain", "label": _("Domain"), "type": "dom"}]
|
||||
|
||||
@property
|
||||
def admins(self):
|
||||
"""Return the domain administrators of this domain
|
||||
|
||||
:return: a list of User objects
|
||||
"""
|
||||
return [oa.user for oa in self.owners.filter(user__is_superuser=False)]
|
||||
|
||||
@property
|
||||
def aliases(self):
|
||||
return self.domainalias_set
|
||||
|
||||
def add_admin(self, account):
|
||||
"""Add a new administrator for this domain
|
||||
|
||||
:param User account: the administrotor to add
|
||||
"""
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
grant_access_to_object(account, self)
|
||||
for mb in self.mailbox_set.all():
|
||||
if mb.user.has_perm("admin.add_domain"):
|
||||
continue
|
||||
grant_access_to_object(account, mb)
|
||||
grant_access_to_object(account, mb.user)
|
||||
for al in self.alias_set.all():
|
||||
grant_access_to_object(account, al)
|
||||
|
||||
def remove_admin(self, account):
|
||||
"""Remove an administrator of this domain.
|
||||
|
||||
:param User account: administrator to remove
|
||||
"""
|
||||
from modoboa.lib.permissions import \
|
||||
ungrant_access_to_object, get_object_owner
|
||||
|
||||
if get_object_owner(self) == account:
|
||||
events.raiseEvent('DomainOwnershipRemoved', account, self)
|
||||
ungrant_access_to_object(self, account)
|
||||
for mb in self.mailbox_set.all():
|
||||
if mb.user.has_perm("admin.add_domain"):
|
||||
continue
|
||||
ungrant_access_to_object(mb, account)
|
||||
ungrant_access_to_object(mb.user, account)
|
||||
for al in self.alias_set.all():
|
||||
ungrant_access_to_object(al, account)
|
||||
|
||||
def delete(self, fromuser, keepdir=False):
|
||||
"""Custom delete method.
|
||||
"""
|
||||
from modoboa.lib.permissions import ungrant_access_to_objects
|
||||
from .mailbox import Quota
|
||||
|
||||
if self.domainalias_set.count():
|
||||
events.raiseEvent("DomainAliasDeleted", self.domainalias_set.all())
|
||||
ungrant_access_to_objects(self.domainalias_set.all())
|
||||
if self.alias_set.count():
|
||||
events.raiseEvent("MailboxAliasDeleted", self.alias_set.all())
|
||||
ungrant_access_to_objects(self.alias_set.all())
|
||||
if parameters.get_admin("AUTO_ACCOUNT_REMOVAL") == "yes":
|
||||
for account in User.objects.filter(mailbox__domain__name=self.name):
|
||||
account.delete(fromuser, keepdir)
|
||||
elif self.mailbox_set.count():
|
||||
Quota.objects.filter(username__contains='@%s' % self.name).delete()
|
||||
events.raiseEvent("MailboxDeleted", self.mailbox_set.all())
|
||||
ungrant_access_to_objects(self.mailbox_set.all())
|
||||
super(Domain, self).delete()
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def from_csv(self, user, row):
|
||||
"""Create a new domain from a CSV entry.
|
||||
|
||||
The expected fields order is the following::
|
||||
|
||||
"domain", name, quota, enabled
|
||||
|
||||
:param ``core.User`` user: user creating the domain
|
||||
:param str row: a list containing domain's definition
|
||||
"""
|
||||
if len(row) < 4:
|
||||
raise BadRequest(_("Invalid line"))
|
||||
self.name = row[1].strip()
|
||||
try:
|
||||
Domain.objects.get(name=self.name)
|
||||
except Domain.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
raise Conflict
|
||||
try:
|
||||
self.quota = int(row[2].strip())
|
||||
except ValueError:
|
||||
raise BadRequest(
|
||||
_("Invalid quota value for domain '%s'" % self.name))
|
||||
self.enabled = (row[3].strip() in ["True", "1", "yes", "y"])
|
||||
self.save(creator=user)
|
||||
|
||||
def to_csv(self, csvwriter):
|
||||
csvwriter.writerow(["domain", self.name, self.quota, self.enabled])
|
||||
for dalias in self.domainalias_set.all():
|
||||
dalias.to_csv(csvwriter)
|
||||
|
||||
def post_create(self, creator):
|
||||
"""Post creation actions.
|
||||
|
||||
:param ``User`` creator: user whos created this domain
|
||||
"""
|
||||
super(Domain, self).post_create(creator)
|
||||
for domalias in self.domainalias_set.all():
|
||||
domalias.post_create(creator)
|
||||
|
||||
reversion.register(Domain)
|
||||
@@ -1,72 +0,0 @@
|
||||
"""Models related to domain aliases management."""
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
import reversion
|
||||
|
||||
from .base import AdminObject
|
||||
from .domain import Domain
|
||||
from modoboa.lib.exceptions import BadRequest, Conflict
|
||||
|
||||
|
||||
class DomainAlias(AdminObject):
|
||||
|
||||
"""Domain aliases."""
|
||||
|
||||
name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True,
|
||||
help_text=ugettext_lazy("The alias name"))
|
||||
target = models.ForeignKey(
|
||||
Domain, verbose_name=ugettext_lazy('target'),
|
||||
help_text=ugettext_lazy("The domain this alias points to")
|
||||
)
|
||||
enabled = models.BooleanField(
|
||||
ugettext_lazy('enabled'),
|
||||
help_text=ugettext_lazy("Check to activate this alias"),
|
||||
default=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_domaliases", "View domain aliases"),
|
||||
)
|
||||
app_label = 'admin'
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def from_csv(self, user, row):
|
||||
"""Create a domain alias from a CSV row
|
||||
|
||||
Expected format: ["domainalias", domain alias name, targeted domain, enabled]
|
||||
|
||||
:param user: a ``User`` object
|
||||
:param row: a list containing the alias definition
|
||||
"""
|
||||
if len(row) < 4:
|
||||
raise BadRequest(_("Invalid line"))
|
||||
self.name = row[1].strip()
|
||||
for model in [DomainAlias, Domain]:
|
||||
try:
|
||||
model.objects.get(name=self.name)
|
||||
except model.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
raise Conflict
|
||||
domname = row[2].strip()
|
||||
try:
|
||||
self.target = Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
raise BadRequest(_("Unknown domain %s" % domname))
|
||||
self.enabled = row[3].strip() in ["True", "1", "yes", "y"]
|
||||
self.save(creator=user)
|
||||
|
||||
def to_csv(self, csvwriter):
|
||||
"""Export a domain alias using CSV format
|
||||
|
||||
:param csvwriter: a ``csv.writer`` object
|
||||
"""
|
||||
csvwriter.writerow(["domainalias", self.name,
|
||||
self.target.name, self.enabled])
|
||||
|
||||
reversion.register(DomainAlias)
|
||||
@@ -1,334 +0,0 @@
|
||||
"""Models related to mailboxes management."""
|
||||
|
||||
import os
|
||||
import pwd
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.db.models.manager import Manager
|
||||
from django.db.models.signals import pre_delete
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
import reversion
|
||||
|
||||
from .base import AdminObject
|
||||
from .domain import Domain
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib import parameters, events
|
||||
from modoboa.lib.exceptions import BadRequest, InternalError
|
||||
from modoboa.lib.sysutils import exec_cmd
|
||||
|
||||
|
||||
class Quota(models.Model):
|
||||
|
||||
"""Keeps track of Mailbox current quota."""
|
||||
|
||||
username = models.EmailField(primary_key=True, max_length=254)
|
||||
bytes = models.BigIntegerField(default=0)
|
||||
messages = models.IntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
app_label = 'admin'
|
||||
|
||||
|
||||
class MailboxManager(Manager):
|
||||
|
||||
"""Custom manager for Mailbox."""
|
||||
|
||||
def get_for_admin(self, admin, squery=None):
|
||||
"""Return the mailboxes that belong to this admin.
|
||||
|
||||
The result will contain the mailboxes defined for each domain that
|
||||
user can see.
|
||||
|
||||
:param string squery: a search query
|
||||
:return: a list of ``Mailbox`` objects
|
||||
"""
|
||||
qf = None
|
||||
if squery is not None:
|
||||
if '@' in squery:
|
||||
parts = squery.split('@')
|
||||
addrfilter = '@'.join(parts[:-1])
|
||||
domfilter = parts[-1]
|
||||
qf = (
|
||||
Q(address__contains=addrfilter) &
|
||||
Q(domain__name__contains=domfilter)
|
||||
)
|
||||
else:
|
||||
qf = (
|
||||
Q(address__contains=squery) |
|
||||
Q(domain__name__contains=squery)
|
||||
)
|
||||
ids = admin.objectaccess_set \
|
||||
.filter(content_type=ContentType.objects.get_for_model(Mailbox)) \
|
||||
.values_list('object_id', flat=True)
|
||||
if qf is not None:
|
||||
qf = Q(pk__in=ids) & qf
|
||||
else:
|
||||
qf = Q(pk__in=ids)
|
||||
return self.get_query_set().select_related().filter(qf)
|
||||
|
||||
|
||||
class Mailbox(AdminObject):
|
||||
|
||||
"""User mailbox."""
|
||||
|
||||
address = models.CharField(
|
||||
ugettext_lazy('address'), max_length=252,
|
||||
help_text=ugettext_lazy(
|
||||
"Mailbox address (without the @domain.tld part)")
|
||||
)
|
||||
quota = models.PositiveIntegerField()
|
||||
use_domain_quota = models.BooleanField(default=False)
|
||||
domain = models.ForeignKey(Domain)
|
||||
user = models.ForeignKey(User)
|
||||
|
||||
objects = MailboxManager()
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
("view_mailboxes", "View mailboxes"),
|
||||
)
|
||||
app_label = 'admin'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Mailbox, self).__init__(*args, **kwargs)
|
||||
self.__mail_home = None
|
||||
|
||||
def __str__(self):
|
||||
return self.full_address
|
||||
|
||||
def __full_address(self, localpart):
|
||||
return "%s@%s" % (localpart, self.domain.name)
|
||||
|
||||
@property
|
||||
def full_address(self):
|
||||
return self.__full_address(self.address)
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return self.user.is_active
|
||||
|
||||
@property
|
||||
def alias_count(self):
|
||||
return self.alias_set.count()
|
||||
|
||||
@property
|
||||
def mail_home(self):
|
||||
"""Retrieve the home directory of this mailbox.
|
||||
|
||||
The home directory refers to the place on the file system
|
||||
where the mailbox data is stored.
|
||||
|
||||
We ask dovecot to give us this information because there are
|
||||
several patterns to understand and we don't want to implement
|
||||
them.
|
||||
"""
|
||||
hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False)
|
||||
if hm is None or hm == "no":
|
||||
return None
|
||||
if self.__mail_home is None:
|
||||
curuser = pwd.getpwuid(os.getuid()).pw_name
|
||||
mbowner = parameters.get_admin("MAILBOXES_OWNER")
|
||||
options = {}
|
||||
if curuser != mbowner:
|
||||
options['sudo_user'] = mbowner
|
||||
code, output = exec_cmd(
|
||||
"doveadm user %s -f home" % self.full_address, **options
|
||||
)
|
||||
if code:
|
||||
raise InternalError(
|
||||
_("Failed to retrieve mailbox location (%s)" % output))
|
||||
self.__mail_home = output.strip()
|
||||
return self.__mail_home
|
||||
|
||||
@property
|
||||
def alias_addresses(self):
|
||||
"""Return all alias address of this mailbox.
|
||||
|
||||
:rtype: list of string
|
||||
"""
|
||||
aliases = []
|
||||
for alias in self.alias_set.all():
|
||||
aliases += [alias.full_address]
|
||||
return aliases
|
||||
|
||||
@property
|
||||
def quota_value(self):
|
||||
"""Retrieve the ``Quota`` instance associated to this mailbox."""
|
||||
if not hasattr(self, "_quota_value"):
|
||||
try:
|
||||
self._quota_value = Quota.objects.get(
|
||||
username=self.full_address)
|
||||
except Quota.DoesNotExist:
|
||||
return None
|
||||
return self._quota_value
|
||||
|
||||
@quota_value.setter
|
||||
def quota_value(self, instance):
|
||||
"""Set the ``Quota`` for this mailbox."""
|
||||
self._quota_value = instance
|
||||
|
||||
def rename_dir(self, old_mail_home):
|
||||
hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False)
|
||||
if hm is None or hm == "no":
|
||||
return
|
||||
MailboxOperation.objects.create(
|
||||
mailbox=self, type='rename', argument=old_mail_home
|
||||
)
|
||||
|
||||
def rename(self, address, domain):
|
||||
"""Rename the mailbox.
|
||||
|
||||
To update the associated Quota record, we must create a new
|
||||
one first, update the foreign key and then we can delete the
|
||||
original record!
|
||||
|
||||
:param string address: the new mailbox's address (local part)
|
||||
:param Domain domain: the new mailbox's domain
|
||||
|
||||
"""
|
||||
old_mail_home = self.mail_home
|
||||
old_qvalue = self.quota_value
|
||||
self.address = address
|
||||
self.domain = domain
|
||||
self.quota_value = Quota.objects.create(
|
||||
username=self.full_address, bytes=old_qvalue.bytes,
|
||||
messages=old_qvalue.messages
|
||||
)
|
||||
old_qvalue.delete()
|
||||
self.rename_dir(old_mail_home)
|
||||
|
||||
def delete_dir(self):
|
||||
hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False)
|
||||
if hm is None or hm == "no":
|
||||
return
|
||||
MailboxOperation.objects.create(type='delete', argument=self.mail_home)
|
||||
|
||||
def set_quota(self, value=None, override_rules=False):
|
||||
"""Set or update quota's value for this mailbox.
|
||||
|
||||
A value equal to 0 means the mailbox won't have any quota. The
|
||||
following cases allow people to define such behaviour:
|
||||
* The domain has no quota
|
||||
* :keyword:`override_rules` is True
|
||||
|
||||
:param integer value: the quota's value
|
||||
:param bool override_rules: allow to override defined quota rules
|
||||
"""
|
||||
if value is None:
|
||||
if self.use_domain_quota:
|
||||
self.quota = self.domain.quota
|
||||
else:
|
||||
self.quota = 0
|
||||
elif int(value) > self.domain.quota and not override_rules:
|
||||
raise BadRequest(
|
||||
_("Quota is greater than the allowed domain's limit (%dM)"
|
||||
% self.domain.quota)
|
||||
)
|
||||
else:
|
||||
self.quota = value
|
||||
if not self.quota and self.domain.quota and not override_rules:
|
||||
raise BadRequest(_("A quota is required"))
|
||||
|
||||
def get_quota(self):
|
||||
"""Get quota limit.
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
return int(self.quota_value.bytes / 1048576)
|
||||
|
||||
def get_quota_in_percent(self):
|
||||
"""Get current quota usage.
|
||||
|
||||
:rtype: int
|
||||
"""
|
||||
if not self.quota:
|
||||
return 0
|
||||
return int(
|
||||
self.quota_value.bytes / float(self.quota * 1048576) * 100
|
||||
)
|
||||
|
||||
def post_create(self, creator):
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
super(Mailbox, self).post_create(creator)
|
||||
if creator.is_superuser and not self.user.has_perm("admin.add_domain"):
|
||||
# A super user is creating a new mailbox. Give
|
||||
# access to that mailbox (and the associated
|
||||
# account) to the appropriate domain admins,
|
||||
# except if the new account has a more important
|
||||
# role (SuperAdmin, Reseller)
|
||||
for admin in self.domain.admins:
|
||||
grant_access_to_object(admin, self)
|
||||
grant_access_to_object(admin, self.user)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Custom save.
|
||||
|
||||
We just make sure a quota record is defined for this mailbox.
|
||||
"""
|
||||
if self.quota_value is None:
|
||||
self.quota_value, created = Quota.objects.get_or_create(
|
||||
username=self.full_address)
|
||||
super(Mailbox, self).save(*args, **kwargs)
|
||||
|
||||
def delete(self, keepdir=False):
|
||||
"""Custom delete method
|
||||
|
||||
We try to delete the associated quota in the same time (it may
|
||||
has already been removed if we're deleting a domain).
|
||||
|
||||
:param bool keepdir: delete the mailbox home dir on the
|
||||
filesystem or not
|
||||
|
||||
"""
|
||||
try:
|
||||
q = Quota.objects.get(username=self.full_address)
|
||||
except Quota.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
q.delete()
|
||||
if not keepdir:
|
||||
self.delete_dir()
|
||||
super(Mailbox, self).delete()
|
||||
|
||||
reversion.register(Mailbox)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Mailbox)
|
||||
def mailbox_deleted_handler(sender, **kwargs):
|
||||
"""``Mailbox`` pre_delete signal receiver
|
||||
|
||||
In order to properly handle deletions (ie. we don't want to leave
|
||||
orphan records into the db), we define this custom receiver.
|
||||
|
||||
It manually removes the mailbox from the aliases it is linked to
|
||||
and then remove all empty aliases.
|
||||
"""
|
||||
from modoboa.lib.permissions import ungrant_access_to_object
|
||||
|
||||
mb = kwargs['instance']
|
||||
events.raiseEvent("MailboxDeleted", mb)
|
||||
ungrant_access_to_object(mb)
|
||||
for alias in mb.alias_set.all():
|
||||
alias.mboxes.remove(mb)
|
||||
if alias.mboxes.count() == 0:
|
||||
alias.delete()
|
||||
|
||||
|
||||
class MailboxOperation(models.Model):
|
||||
mailbox = models.ForeignKey(Mailbox, blank=True, null=True)
|
||||
type = models.CharField(
|
||||
max_length=20, choices=(('rename', 'rename'), ('delete', 'delete'))
|
||||
)
|
||||
argument = models.TextField()
|
||||
|
||||
class Meta:
|
||||
app_label = 'admin'
|
||||
|
||||
def __str__(self):
|
||||
if self.type == 'rename':
|
||||
return 'Rename %s -> %s' % (self.argument, self.mailbox.mail_home)
|
||||
return 'Delete %s' % self.argument
|
||||
@@ -1,285 +0,0 @@
|
||||
"""Registration with Modoboa takes place here."""
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from modoboa.core.extensions import ModoExtension, exts_pool
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, DomainAlias, Mailbox, Alias
|
||||
)
|
||||
from modoboa.lib import parameters, events
|
||||
from modoboa.lib.email_utils import split_mailbox
|
||||
from modoboa.lib.exceptions import PermDeniedException, BadRequest, Conflict
|
||||
|
||||
|
||||
ADMIN_EVENTS = [
|
||||
"DomainCreated",
|
||||
"DomainModified",
|
||||
"DomainDeleted",
|
||||
"DomainOwnershipRemoved",
|
||||
"ExtraDomainEntries",
|
||||
"ExtraDomainMenuEntries",
|
||||
"ExtraDomainFilters",
|
||||
"GetDomainActions",
|
||||
"GetDomainModifyLink",
|
||||
"CheckDomainName",
|
||||
|
||||
"DomainAliasCreated",
|
||||
"DomainAliasDeleted",
|
||||
|
||||
"MailboxCreated",
|
||||
"MailboxDeleted",
|
||||
"MailboxModified",
|
||||
|
||||
"MailboxAliasCreated",
|
||||
"MailboxAliasDeleted",
|
||||
|
||||
"ExtraDomainForm",
|
||||
"FillDomainInstances",
|
||||
|
||||
"ExtraAccountForm",
|
||||
"CheckExtraAccountForm",
|
||||
"FillAccountInstances",
|
||||
|
||||
"ExtraDomainImportHelp",
|
||||
"ImportObject"
|
||||
]
|
||||
|
||||
PERMISSIONS = {
|
||||
"DomainAdmins": [
|
||||
["admin", "domain", "view_domain"],
|
||||
["admin", "mailbox", "add_mailbox"],
|
||||
["admin", "mailbox", "change_mailbox"],
|
||||
["admin", "mailbox", "delete_mailbox"],
|
||||
["admin", "alias", "add_alias"],
|
||||
["admin", "alias", "change_alias"],
|
||||
["admin", "alias", "delete_alias"],
|
||||
["admin", "mailbox", "view_mailboxes"],
|
||||
["admin", "alias", "view_aliases"],
|
||||
["admin", "domainalias", "view_domaliases"],
|
||||
["admin", "domainalias", "add_domainalias"],
|
||||
["admin", "domainalias", "change_domainalias"],
|
||||
["admin", "domainalias", "delete_domainalias"]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
exts_pool.register_extension(AdminConsole)
|
||||
|
||||
|
||||
@events.observe("GetExtraRolePermissions")
|
||||
def extra_permissions(rolename):
|
||||
"""Return extra permissions for :kw:`rolename`."""
|
||||
return PERMISSIONS.get(rolename, [])
|
||||
|
||||
|
||||
@events.observe("ExtraUprefsRoutes")
|
||||
def extra_routes():
|
||||
from django.conf.urls import url
|
||||
return [
|
||||
url(r'^user/forward/',
|
||||
'modoboa.extensions.admin.views.user.forward',
|
||||
name="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("admin:domain_list"),
|
||||
"label": _("Domains")}
|
||||
]
|
||||
if user.has_perm("core.add_user") or user.has_perm("admin.add_alias"):
|
||||
entries += [
|
||||
{"name": "identities",
|
||||
"url": reverse("admin:identity_list"),
|
||||
"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": "ajaxnav",
|
||||
"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):
|
||||
result = [admin.mailbox_set.all()[0].quota] \
|
||||
if admin.mailbox_set.count() else ['']
|
||||
if admin.group != "DomainAdmins":
|
||||
return result
|
||||
return result + [dom.name for dom in Domain.objects.get_for_admin(admin)]
|
||||
|
||||
|
||||
@events.observe("AccountImported")
|
||||
def import_account_mailbox(user, account, row):
|
||||
"""Handle extra fields when an account is imported.
|
||||
|
||||
Expected fields:
|
||||
|
||||
email address; quota; [domain; ...]
|
||||
|
||||
:param User user: user importing the account
|
||||
:param User account: account being imported
|
||||
:param list rom: list of fields (strings)
|
||||
"""
|
||||
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 BadRequest(
|
||||
_("Account import failed (%s): domain does not exist"
|
||||
% account.username)
|
||||
)
|
||||
if not user.can_access(domain):
|
||||
raise PermDeniedException
|
||||
try:
|
||||
mb = Mailbox.objects.get(address=mailbox, domain=domain)
|
||||
except Mailbox.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
raise Conflict(_("Mailbox %s already exists" % account.email))
|
||||
if len(row) == 1:
|
||||
quota = None
|
||||
else:
|
||||
try:
|
||||
quota = int(row[1].strip())
|
||||
except ValueError:
|
||||
raise BadRequest(
|
||||
_("Account import failed (%s): wrong quota value"
|
||||
% account.username)
|
||||
)
|
||||
use_domain_quota = True if not quota else False
|
||||
mb = Mailbox(address=mailbox, domain=domain,
|
||||
user=account, use_domain_quota=use_domain_quota)
|
||||
mb.set_quota(quota, override_rules=user.has_perm("admin.change_domain"))
|
||||
mb.save(creator=user)
|
||||
if account.group == "DomainAdmins":
|
||||
for domname in row[2:]:
|
||||
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.extensions.admin.lib import check_if_domain_exists
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
|
||||
if parameters.get_admin("AUTO_CREATE_DOMAIN_AND_MAILBOX") == "no":
|
||||
return
|
||||
localpart, domname = split_mailbox(user.username)
|
||||
if user.group != 'SimpleUsers' and domname is None:
|
||||
return
|
||||
sadmins = User.objects.filter(is_superuser=True)
|
||||
try:
|
||||
domain = Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
label = check_if_domain_exists(
|
||||
domname, [(DomainAlias, _('domain alias'))])
|
||||
if label is not None:
|
||||
return
|
||||
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):
|
||||
"""'AccountDeleted' listener.
|
||||
|
||||
When an account is deleted, we also need to remove its mailbox (if
|
||||
any).
|
||||
"""
|
||||
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)
|
||||
@@ -1,33 +0,0 @@
|
||||
#searchform {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
span.title {
|
||||
margin-left: 40px;
|
||||
margin-bottom: 10px;
|
||||
float: left;
|
||||
font-weight: bold;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
table th#actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table td[name="actions"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table .progress {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
table tbody tr:hover {
|
||||
background-color: #F9F9F9;
|
||||
}
|
||||
|
||||
.search {
|
||||
width: 85%;
|
||||
-webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px;
|
||||
padding-left: 14px;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
|
||||
#settings_content .form-horizontal .control-label {
|
||||
margin-left: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#settings_content > .form-horizontal .controls {
|
||||
margin-left: 240px;
|
||||
}
|
||||
@@ -1,556 +0,0 @@
|
||||
/**
|
||||
* Creates an instance of Admin.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} options - instance options
|
||||
* @classdesc This class contains utility methods used by the admin
|
||||
* application.
|
||||
*/
|
||||
var Admin = function(options) {
|
||||
Listing.call(this, options);
|
||||
};
|
||||
|
||||
Admin.prototype = {
|
||||
defaults: {
|
||||
deflocation: "list/",
|
||||
squery: null
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
this.options = $.extend({}, this.defaults, options);
|
||||
this.options.defcallback = $.proxy(this.list_cb, this);
|
||||
Listing.prototype.initialize.call(this, this.options);
|
||||
this.listen();
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback used when the initial content of the listing is
|
||||
* received.
|
||||
*
|
||||
* @this Admin
|
||||
* @param {Object} data - response of the ajax call (JSON)
|
||||
*/
|
||||
list_cb: function(data) {
|
||||
if (data.rows) {
|
||||
$("#objects_table tbody").html(data.rows);
|
||||
} else {
|
||||
$("#objects_table tbody").html("");
|
||||
}
|
||||
this.update_listing(data);
|
||||
if (data.length === 0) {
|
||||
this.get_load_page_args();
|
||||
this.end_of_list_reached();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Children must override this method.
|
||||
*/
|
||||
listen: function() {
|
||||
},
|
||||
|
||||
/**
|
||||
* Load a new page using an AJAX request.
|
||||
*
|
||||
* @this Admin
|
||||
* @param {Object} e - event object
|
||||
*/
|
||||
page_loader: function(e) {
|
||||
var $link = get_target(e);
|
||||
e.preventDefault();
|
||||
if ($link.hasClass("navigation")) {
|
||||
$(".sidebar li.active").removeClass("active");
|
||||
$link.parent().addClass("active");
|
||||
}
|
||||
this.navobj.baseurl($link.attr("href")).update();
|
||||
},
|
||||
|
||||
importform_cb: function() {
|
||||
$(".submit").one('click', function(e) {
|
||||
e.preventDefault();
|
||||
if ($("#id_sourcefile").val() === "") {
|
||||
return;
|
||||
}
|
||||
$("#import_status").css("display", "block");
|
||||
$("#import_result").html("").removeClass("alert alert-danger");
|
||||
$("#importform").submit();
|
||||
});
|
||||
},
|
||||
|
||||
importdone: function(status, msg) {
|
||||
$("#import_status").css("display", "none");
|
||||
if (status == "ok") {
|
||||
$("#modalbox").modal('hide');
|
||||
this.reload_listing(msg);
|
||||
} else {
|
||||
$("#import_result").addClass("alert alert-danger");
|
||||
$("#import_result").html(msg);
|
||||
this.importform_cb();
|
||||
}
|
||||
},
|
||||
|
||||
exportform_cb: function() {
|
||||
$(".submit").one('click', function(e) {
|
||||
e.preventDefault();
|
||||
$("#exportform").submit();
|
||||
$("#modalbox").modal('hide');
|
||||
});
|
||||
},
|
||||
|
||||
reload_listing: function(data) {
|
||||
this.navobj.update(true);
|
||||
if (data) {
|
||||
$("body").notify("success", data, 2000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Admin.prototype = $.extend({}, Listing.prototype, Admin.prototype);
|
||||
|
||||
/**
|
||||
* Creates an instance of Domains.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} options - instance options
|
||||
* @classdesc This class extends the Admin one by adding methods
|
||||
* specific to domains management.
|
||||
*/
|
||||
var Domains = function(options) {
|
||||
Admin.call(this, options);
|
||||
};
|
||||
|
||||
Domains.prototype = {
|
||||
initialize: function(options) {
|
||||
Admin.prototype.initialize.call(this, options);
|
||||
this.options.navigation_params.push("domfilter", "srvfilter");
|
||||
this.options.eor_message = gettext("No more domain to show");
|
||||
this.register_tag_handler("dom");
|
||||
},
|
||||
|
||||
/**
|
||||
* Custom callbacks declaration.
|
||||
*/
|
||||
listen: function() {
|
||||
Admin.prototype.listen.call(this);
|
||||
$(document).on("click", "a.ajaxnav", $.proxy(this.page_loader, this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the domain links embedded within a page.
|
||||
*
|
||||
* @param {Object} data - options
|
||||
*/
|
||||
init_domain_links: function(data) {
|
||||
var deloptions = (data.handle_mailboxes) ?
|
||||
{keepdir: gettext("Do not delete domain directory")} : {};
|
||||
var warnmsg = (data.auto_account_removal && data.auto_account_removal == "yes")
|
||||
? gettext("This operation will remove ALL data associated to this domain.")
|
||||
: gettext("This operation will remove all data associated to this domain, excepting accounts.");
|
||||
|
||||
$("a[name=deldomain]").confirm({
|
||||
question: function() { return this.$element.attr('title'); },
|
||||
method: "POST",
|
||||
warning: warnmsg,
|
||||
checkboxes: deloptions,
|
||||
success_cb: $.proxy(this.reload_listing, this)
|
||||
});
|
||||
$(document).trigger('domain_listing_refresh');
|
||||
},
|
||||
|
||||
/**
|
||||
* Navigation callback: default.
|
||||
*
|
||||
* @param {Object} data - response of the ajax call (JSON)
|
||||
*/
|
||||
list_cb: function(data) {
|
||||
Admin.prototype.list_cb.call(this, data);
|
||||
this.init_domain_links(data);
|
||||
},
|
||||
|
||||
/**
|
||||
* A new page has been received, inject it.
|
||||
*
|
||||
* @param {Object} data - page content
|
||||
* @param {string} direction -
|
||||
*/
|
||||
add_new_page: function(data, direction) {
|
||||
Admin.prototype.add_new_page.call(this, data, direction);
|
||||
this.init_domain_links(data);
|
||||
},
|
||||
|
||||
change_inputs_state: function(value) {
|
||||
$("#id_dom_admin_username").attr("disabled", value);
|
||||
$("input[name=create_aliases]").attr("disabled", value);
|
||||
},
|
||||
|
||||
create_dom_admin_changed: function(e) {
|
||||
var $target = $(e.target);
|
||||
this.change_inputs_state(($target.val() == "yes") ? false : true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the main form contained in the domain edition modal.
|
||||
*
|
||||
* @this Domains
|
||||
*/
|
||||
generalform_init: function() {
|
||||
$('input:text:visible:first').focus();
|
||||
$("#original_aliases").dynamic_input({
|
||||
input_added: function($row) {
|
||||
$row.find("label").html("");
|
||||
},
|
||||
input_removed: function($input) {
|
||||
$input.parents(".form-group").remove();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
optionsform_init: function() {
|
||||
$("input[name=create_dom_admin]").click($.proxy(this.create_dom_admin_changed, this));
|
||||
this.change_inputs_state(
|
||||
$("input[name=create_dom_admin]:checked").val() == "yes" ? false : true
|
||||
);
|
||||
this.optionsform_prefill();
|
||||
},
|
||||
|
||||
optionsform_prefill: function() {
|
||||
var $span = $("#id_dom_admin_username").next("span");
|
||||
$span.html("@" + $("#id_name").val());
|
||||
},
|
||||
|
||||
domadminsform_init: function() {
|
||||
$("a[name=removeperm]").click(function(e) {
|
||||
var $tr = $(this).parents('tr');
|
||||
simple_ajax_request.apply(this, [e, {
|
||||
ok_cb: function(resp) {
|
||||
$tr.remove();
|
||||
if (!$("#domadmins").find("tr").length) {
|
||||
$("#admins").html('<div class="alert alert-info">'
|
||||
+ gettext("No domain administrator defined") + "</div>");
|
||||
}
|
||||
}
|
||||
}]);
|
||||
});
|
||||
},
|
||||
|
||||
newdomain_cb: function() {
|
||||
this.generalform_init();
|
||||
this.optionsform_init();
|
||||
$("#wizard").cwizard({
|
||||
formid: "domform",
|
||||
transition_callbacks: {
|
||||
1: this.optionsform_prefill
|
||||
},
|
||||
error_callbacks: {
|
||||
2: $.proxy(this.optionsform_init, this)
|
||||
},
|
||||
success_callback: $.proxy(this.reload_listing, this)
|
||||
});
|
||||
},
|
||||
|
||||
domainform_cb: function() {
|
||||
this.generalform_init();
|
||||
this.domadminsform_init();
|
||||
$(".submit").one('click', $.proxy(function(e) {
|
||||
simple_ajax_form_post(e, {
|
||||
formid: "domform",
|
||||
error_cb: $.proxy(this.domainform_cb, this),
|
||||
reload_on_success: false,
|
||||
success_cb: $.proxy(this.reload_listing, this)
|
||||
});
|
||||
}, this));
|
||||
$(document).trigger('domform_init');
|
||||
}
|
||||
};
|
||||
|
||||
Domains.prototype = $.extend({}, Admin.prototype, Domains.prototype);
|
||||
|
||||
/**
|
||||
* Creates an instance of Identities.
|
||||
*
|
||||
* @constructor
|
||||
* @param {Object} options - instance options
|
||||
* @classdesc This class extends the Admin one by adding methods
|
||||
* specific to identities management.
|
||||
*/
|
||||
var Identities = function(options) {
|
||||
Admin.call(this, options);
|
||||
};
|
||||
|
||||
Identities.prototype = {
|
||||
initialize: function(options) {
|
||||
Admin.prototype.initialize.call(this, options);
|
||||
this.options.navigation_params.push("idtfilter", "grpfilter");
|
||||
this.options.eor_message = gettext("No more identity to show");
|
||||
this.domain_list = [];
|
||||
this.register_tag_handler("idt");
|
||||
this.register_tag_handler("grp", this.grp_tag_handler);
|
||||
},
|
||||
|
||||
listen: function() {
|
||||
Admin.prototype.listen.call(this);
|
||||
$(document).on("click", "a.ajaxnav", $.proxy(this.page_loader, this));
|
||||
$(document).on("click", "a.ajaxcall", $.proxy(this.send_call, this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Send an ajax call.
|
||||
*/
|
||||
send_call: function(evt) {
|
||||
var $link = get_target(evt, "a");
|
||||
var $this = this;
|
||||
var method = $link.attr("data-call_method");
|
||||
|
||||
if (method === undefined) {
|
||||
method = "GET";
|
||||
}
|
||||
evt.preventDefault();
|
||||
$.ajax({
|
||||
url: $link.attr("href"),
|
||||
type: method,
|
||||
dataType: "json"
|
||||
}).done(function(data) {
|
||||
$this.reload_listing(data.respmsg);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the links embedded within a page.
|
||||
*
|
||||
* @param {Object} data - options
|
||||
*/
|
||||
init_identity_links: function(data) {
|
||||
var deloptions = {};
|
||||
|
||||
if (data.handle_mailboxes == "yes") {
|
||||
deloptions = {keepdir: gettext("Do not delete mailbox directory")};
|
||||
}
|
||||
|
||||
$("a[name=delaccount]").confirm({
|
||||
question: function() { return this.$element.attr('title'); },
|
||||
method: "POST",
|
||||
checkboxes: deloptions,
|
||||
success_cb: $.proxy(this.reload_listing, this)
|
||||
});
|
||||
$("a[name=delalias]").confirm({
|
||||
question: function() { return this.$element.attr('title'); },
|
||||
method: "DELETE",
|
||||
success_cb: $.proxy(this.reload_listing, this)
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a type to let the server know what kind of object we expect.
|
||||
*
|
||||
* @this Listing
|
||||
*/
|
||||
get_load_page_args: function() {
|
||||
var args = Admin.prototype.get_load_page_args.call(this);
|
||||
|
||||
if (this.navobj.getbaseurl() == "list") {
|
||||
args.objtype = "identity";
|
||||
this.options.eor_message = gettext("No more identity to show");
|
||||
} else {
|
||||
args.objtype = "quota";
|
||||
this.options.eor_message = gettext("No more quota to show");
|
||||
}
|
||||
return args;
|
||||
},
|
||||
|
||||
/**
|
||||
* A new page has been received, inject it.
|
||||
*
|
||||
* @param {Object} data - page content
|
||||
* @param {string} direction -
|
||||
*/
|
||||
add_new_page: function(data, direction) {
|
||||
Admin.prototype.add_new_page.call(this, data, direction);
|
||||
this.init_identity_links(data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Navigation callback: default.
|
||||
*
|
||||
* @param {Object} data - response of the ajax call (JSON)
|
||||
*/
|
||||
list_cb: function(data) {
|
||||
if ($(".sidebar li.active").length === 0) {
|
||||
var menu = this.navobj.getbaseurl();
|
||||
if (menu == "list") {
|
||||
menu = "identities";
|
||||
}
|
||||
$("a[name={0}]".format(menu)).parent().addClass("active");
|
||||
}
|
||||
$("#objects_table thead tr").html(data.headers);
|
||||
Admin.prototype.list_cb.call(this, data);
|
||||
this.init_identity_links(data);
|
||||
},
|
||||
|
||||
grp_tag_handler: function(tag, $link) {
|
||||
if (this.navobj.getparam(tag + "filter") === undefined &&
|
||||
$link.hasClass(tag)) {
|
||||
var text = $link.attr("name");
|
||||
this.navobj
|
||||
.setparam("idtfilter", "account")
|
||||
.setparam(tag + "filter", text)
|
||||
.update();
|
||||
if ($("a[name=idt]").length === 0) {
|
||||
$("#taglist").append(this.make_tag("account", "idt"));
|
||||
}
|
||||
$("#taglist").append(this.make_tag(text, tag));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a list of domain from the server.
|
||||
*
|
||||
* @this Identities
|
||||
* @return {Array} a list of domain names
|
||||
*/
|
||||
get_domain_list: function() {
|
||||
if (!this.domain_list.length) {
|
||||
$.ajax({
|
||||
url: this.options.domain_list_url,
|
||||
dataType: "json",
|
||||
async: false
|
||||
}).done($.proxy(function(data) {
|
||||
this.domain_list = data;
|
||||
}, this));
|
||||
}
|
||||
return this.domain_list;
|
||||
},
|
||||
|
||||
simpleuser_mode: function() {
|
||||
$("#id_username").autocompleter({
|
||||
from_character: "@",
|
||||
choices: $.proxy(this.get_domain_list, this)
|
||||
});
|
||||
$("#id_email").addClass("disabled")
|
||||
.attr("readonly", "")
|
||||
.autocompleter("unbind");
|
||||
},
|
||||
|
||||
normal_mode: function() {
|
||||
$("#id_email").removeClass("disabled")
|
||||
.attr("readonly", null)
|
||||
.autocompleter("listen");
|
||||
},
|
||||
|
||||
generalform_init: function(notrigger) {
|
||||
$("#id_role").change($.proxy(function(e) {
|
||||
var $this = $(e.target);
|
||||
var value = $this.val();
|
||||
|
||||
if (value == "SimpleUsers" || value === "") {
|
||||
this.simpleuser_mode();
|
||||
} else {
|
||||
this.normal_mode();
|
||||
}
|
||||
}, this));
|
||||
if (notrigger !== undefined && notrigger) {
|
||||
return;
|
||||
}
|
||||
$("#id_role").trigger("change");
|
||||
},
|
||||
|
||||
mailform_init: function() {
|
||||
$("#id_aliases").autocompleter({
|
||||
from_character: "@",
|
||||
choices: $.proxy(this.get_domain_list, this)
|
||||
});
|
||||
$("#original_aliases").dynamic_input({
|
||||
input_added: function($row) {
|
||||
$row.find("label").html("");
|
||||
},
|
||||
input_removed: function($input) {
|
||||
$input.parents(".form-group").remove();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
$("#id_email").autocompleter({
|
||||
from_character: "@",
|
||||
choices: $.proxy(this.get_domain_list, this)
|
||||
});
|
||||
if ($("#id_role").length) {
|
||||
$("#id_role").trigger("change");
|
||||
} else {
|
||||
this.simpleuser_mode();
|
||||
}
|
||||
$("#id_domains").autocompleter({
|
||||
choices: $.proxy(this.get_domain_list, this)
|
||||
});
|
||||
$("#original_domains").dynamic_input({
|
||||
input_added: function($row) {
|
||||
$row.find("label").html("");
|
||||
},
|
||||
input_removed: function($input) {
|
||||
$input.parents(".form-group").remove();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
activate_widget.call($("#id_quota_act"));
|
||||
},
|
||||
|
||||
accountform_init: function() {
|
||||
this.generalform_init(true);
|
||||
this.mailform_init();
|
||||
},
|
||||
|
||||
mailform_prefill: function() {
|
||||
var $role = $("#id_role");
|
||||
if (!$role.length || $role.val() === "" || $role.val() == "SimpleUsers") {
|
||||
$("#id_email").val($("#id_username").val());
|
||||
}
|
||||
},
|
||||
|
||||
newaccount_cb: function() {
|
||||
this.accountform_init();
|
||||
$("#wizard").cwizard({
|
||||
formid: "newaccount_form",
|
||||
transition_callbacks: {
|
||||
1: this.mailform_prefill
|
||||
},
|
||||
success_callback: $.proxy(this.reload_listing, this)
|
||||
});
|
||||
},
|
||||
|
||||
editaccount_cb: function() {
|
||||
this.accountform_init();
|
||||
$('.submit').on('click', $.proxy(function(e) {
|
||||
simple_ajax_form_post(e, {
|
||||
formid: "accountform",
|
||||
reload_on_success: false,
|
||||
success_cb: $.proxy(this.reload_listing, this)
|
||||
});
|
||||
}, this));
|
||||
},
|
||||
|
||||
aliasform_cb: function() {
|
||||
$("#id_email").autocompleter({
|
||||
from_character: "@",
|
||||
choices: $.proxy(this.get_domain_list, this)
|
||||
});
|
||||
$("#original_recipients").dynamic_input({
|
||||
input_added: function($row) {
|
||||
$row.find("label").html("");
|
||||
},
|
||||
input_removed: function($input) {
|
||||
$input.parents(".form-group").remove();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
$(".submit").on('click', $.proxy(function(e) {
|
||||
simple_ajax_form_post(e, {
|
||||
formid: "aliasform",
|
||||
reload_on_success: false,
|
||||
success_cb: $.proxy(this.reload_listing, this)
|
||||
});
|
||||
}, this));
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Identities.prototype = $.extend({}, Admin.prototype, Identities.prototype);
|
||||
@@ -1,15 +0,0 @@
|
||||
# coding: utf-8
|
||||
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,12 +0,0 @@
|
||||
{% load form_tags %}
|
||||
{% render_field form.role %}
|
||||
{% render_field form.username %}
|
||||
{% render_field form.first_name %}
|
||||
{% render_field form.last_name %}
|
||||
{% render_field form.password1 %}
|
||||
{% render_field form.password2 %}
|
||||
{% if form.domain_is_disabled %}
|
||||
{% include "common/generic_field.html" with field=form.is_active label_width="col-sm-4" inline_message=_("Domain is disabled") %}
|
||||
{% else %}
|
||||
{% render_field form.is_active %}
|
||||
{% endif %}
|
||||
@@ -1,10 +0,0 @@
|
||||
{% extends "common/generic_modal_form.html" %}{% load form_tags %}
|
||||
{% block formbody %}
|
||||
{% render_field form.email %}
|
||||
{% render_fields_group form "recipients" %}
|
||||
{% if not form.instance.pk or form.instance.domain.enabled %}
|
||||
{% render_field form.enabled %}
|
||||
{% else %}
|
||||
{% include "common/generic_field.html" with field=form.enabled label_width="col-sm-4" inline_message=_("Domain is disabled") extra_classes="info" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -1,5 +0,0 @@
|
||||
{% load i18n form_tags %}
|
||||
{% render_field form.name %}
|
||||
{% render_field form.quota appended_text=_("MB") %}
|
||||
{% render_fields_group form "aliases" %}
|
||||
{% render_field form.enabled %}
|
||||
@@ -1,8 +0,0 @@
|
||||
{% load i18n form_tags %}
|
||||
{% if form.fields %}
|
||||
{% render_field form.create_dom_admin %}
|
||||
{% render_field form.dom_admin_username appended_text="@" %}
|
||||
{% render_field form.create_aliases %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">{% trans "You can't create a new domain administrator" %}</div>
|
||||
{% endif %}
|
||||
@@ -1,63 +0,0 @@
|
||||
{% extends "admin/header.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load admin_tags %}
|
||||
|
||||
{% block leftcol %}
|
||||
<ul class="nav nav-sidebar">
|
||||
{% domains_menu "statistics" user %}
|
||||
</ul>
|
||||
|
||||
{% get_extra_admin_content user "leftcol" selection %}
|
||||
{% endblock %}
|
||||
|
||||
{% block apparea %}
|
||||
<h2>{% trans "Statistics" %} <small>{% trans "Domains and identities" %}</small></h2>
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
{% if user.group == "SuperAdmins" %}
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading"><h3 class="panel-title">{% trans "Global counters" %}</h3></div>
|
||||
<div class="panel-body"></div>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Quantity" %}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans "Domains" %}</td><td>{{ domains_counter }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans "Domain aliases" %}</td><td>{{ domain_aliases_counter }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{% trans "Identities" %}</td><td>{{ identities_counter }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-xs-12 col-sm-6">
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading"><h3 class="panel-title">{% trans "Per-domain counters" %}</h3></div>
|
||||
<div class="panel-body"></div>
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th>{% trans "Domain" %}</th>
|
||||
<th>{% trans "Identities" %}</th>
|
||||
</tr>
|
||||
{% for domain in domains %}
|
||||
<tr>
|
||||
<td>{{ domain.name }}</td><td>{{ domain.identities_count }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,29 +0,0 @@
|
||||
{% extends "admin/listing.html" %}{% load i18n admin_tags lib_tags %}
|
||||
|
||||
{% block pagetitle %}{% trans "Domains" %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{{ block.super }}
|
||||
{% extra_static_content "domains" "css" user %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
admin = new Domains({
|
||||
load_page_url: "{% url 'admin:_domain_list' %}"
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% extra_static_content "domains" "js" user %}
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}{% domains_menu "domains" user %}{% endblock %}
|
||||
|
||||
{% block table_header %}
|
||||
<th id="name" class="sortable" data-sort_order="name" width="">{% trans "Name" %}</th>
|
||||
<th id="aliases" width="">{% trans "Alias(es)" %}</th>
|
||||
<th id="tags" class="sortable" data-sort_order="tags" width="">{% trans "Tags" %}</th>
|
||||
<th id="actions" width="">{% trans "Actions" %}</th>
|
||||
{% endblock %}
|
||||
@@ -1,9 +0,0 @@
|
||||
{% load i18n admin_tags lib_tags %}
|
||||
{% for domain in domains %}
|
||||
<tr id="{{ domain.id }}" class="{% if not domain.enabled %}text-muted{% endif %}" name="{{ forloop.counter }}">
|
||||
<td name="name">{% domain_modify_link domain %}{% if not domain.enabled %} ({% trans "disabled" %}){% endif %}</td>
|
||||
<td name="aliases">{% domain_aliases domain %} </td>
|
||||
<td name="tags">{% render_tags domain.tags %}</td>
|
||||
<td name="actions">{% domain_actions user domain %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -1,28 +0,0 @@
|
||||
{% extends "common/tabforms.html" %}{% load i18n lib_tags admin_tags %}
|
||||
{% block extratabs %}
|
||||
<li class="{% if tabs.active_id == 'admins' %}active{% endif %}">
|
||||
<a href="#admins" data-toggle="tab">{% trans "Administrators" %}</a>
|
||||
</li>
|
||||
<li class="{% if tabs.active_id == 'misc' %}active{% endif %}">
|
||||
<a href="#misc" data-toggle="tab">{% trans "Miscellaneous" %}</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
{% block extratabs_content %}
|
||||
<div class="tab-pane{% if tabs.active_id == 'admins' %} active{% endif %}" id="admins">{% if domadmins %}
|
||||
<table id="domadmins" class="table table-condensed">
|
||||
<tr><th>{% trans "Username" %}</th><th>{% trans "Name" %}</th><th></th></tr>
|
||||
{% for da in domadmins %}<tr>
|
||||
<td>{{ da.username }}</td>
|
||||
<td>{{ da.fullname }}</td>
|
||||
<td name="actions">{% if perms.admin.change_domain %}{% domadmin_actions da.id domain.id %}{% endif %}</td>
|
||||
</tr>{% endfor %}
|
||||
</table>{% else %}<div class="alert alert-info">{% trans "No domain administrator defined" %}</div>{% endif %}
|
||||
</div>
|
||||
<div class="tab-pane{% if tabs.active_id == 'misc' %} active{% endif %}" id="misc">
|
||||
<div class="row"><span class="title">{% trans "Creation date" %}</span><span>{{ domain.creation }}</span></div>
|
||||
<div class="row"><span class="title">{% trans "Last modification date" %}</span><span>{{ domain.last_modification }}</span></div>
|
||||
<div class="row"><span class="title">{% trans "Domain aliases" %}</span><span>{{ domain.domainalias_count }}</span></div>
|
||||
<div class="row"><span class="title">{% trans "Mailboxes" %}</span><span>{{ domain.mailbox_count }}</span></div>
|
||||
<div class="row"><span class="title">{% trans "Mailbox aliases" %}</span><span>{{ domain.mbalias_count }}</span></div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,11 +0,0 @@
|
||||
{% load i18n form_tags %}{% load url from future %}
|
||||
<h2>{% trans "Forward" %} <small>{% trans "Automatically forward emails to another address" %}</small></h2>
|
||||
<hr>
|
||||
<form class="form-horizontal" method="POST" action="{% url 'user_forward' %}"
|
||||
id="uprefs_form">
|
||||
{% csrf_token %}
|
||||
{% render_form form %}
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary col-lg-offset-8 col-md-offset-8 col-sm-offset-7 col-lg-1 col-md-1 col-sm-2" id="update" name="update">{% trans 'Update' %}</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,12 +0,0 @@
|
||||
{% extends "twocols.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/admin.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script type="text/javascript" src="{% static 'js/history.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/listing.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'admin/js/admin.js' %}"></script>
|
||||
{% endblock %}
|
||||
@@ -1,26 +0,0 @@
|
||||
{% extends "admin/listing.html" %}
|
||||
{% load admin_tags lib_tags i18n %}{% load url from future %}
|
||||
{% load static %}
|
||||
|
||||
{% block pagetitle %}{% trans "Identities" %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{{ block.super }}
|
||||
{% extra_static_content "identities" "css" user %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript" src="{% static 'js/autocompleter.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
admin = new Identities({
|
||||
domain_list_url: "{% url 'admin:domain_flat_list' %}",
|
||||
load_page_url: "{% url 'admin:identity_page' %}"
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% extra_static_content "identities" "js" user %}
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}{% identities_menu user "identities" %}{% endblock %}
|
||||
@@ -1,10 +0,0 @@
|
||||
{% load i18n admin_tags lib_tags %}
|
||||
{% for ident in identities %}
|
||||
{% check_identity_status ident as identity_is_enabled %}
|
||||
<tr id="{{ ident.id }}" class="{% if not identity_is_enabled %}text-muted{% endif %}" name="{{ forloop.counter }}">
|
||||
<td name="identity">{% identity_modify_link ident %}{% if not identity_is_enabled %} (disabled){% endif %}</td>
|
||||
<td name="name_or_rcpt">{{ ident.name_or_rcpt }}</td>
|
||||
<td name="tags">{% render_tags ident.tags %}</td>
|
||||
<td name="actions">{% identity_actions user ident %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -1,5 +0,0 @@
|
||||
{% load i18n %}
|
||||
<th id="identity" class="sortable" data-sort_order="identity" width="">{% trans "Name" %}</th>
|
||||
<th id="name_or_rcpt" class="sortable" data-sort_order="name_or_rcpt" width="">{% trans "Fullname/Recipient" %}</th>
|
||||
<th id="tags" class="sortable" data-sort_order="tags" width="">{% trans "Tags" %}</th>
|
||||
<th id="actions" width="">{% trans "Actions" %}</th>
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head profile="http://www.w3.org/2005/10/profile">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<script type="text/javascript">
|
||||
parent.admin.importdone("{{ status }}", "{{ msg|escapejs }}");
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -1,16 +0,0 @@
|
||||
{% extends "common/generic_modal_form.html" %}
|
||||
{% load form_tags i18n %}
|
||||
{% load static %}
|
||||
{% block formbody %}
|
||||
<div class="alert alert-info">
|
||||
<h4>{% trans "Help" %}</h4>
|
||||
{{ helptext|safe }}
|
||||
</div>
|
||||
{% render_form form %}
|
||||
<iframe id="import_target" name="import_target" src="#"
|
||||
style="width:0;height:0;border:0px solid #fff;"></iframe>
|
||||
<div id="import_status" style="text-align: center;display:none">
|
||||
<img src="{% static 'css/spinner.gif' %}" /> {% trans "Importing..." %}
|
||||
</div>
|
||||
<div id="import_result"></div>
|
||||
{% endblock %}
|
||||
@@ -1,47 +0,0 @@
|
||||
{% extends "admin/header.html" %}{% load i18n admin_tags %}
|
||||
{% load static %}
|
||||
|
||||
{% block extra_css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'css/jquery.sortable.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript" src="{% static 'js/creation_wizard.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/dynamic_input.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/jquery.sortable.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/jquery.infinitescroll.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
var admin;
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block leftcol %}
|
||||
<ul class="nav nav-sidebar">
|
||||
{% block actions %}{% endblock %}
|
||||
</ul>
|
||||
|
||||
{% get_extra_admin_content user "leftcol" selection %}
|
||||
{% endblock %}
|
||||
|
||||
{% block apparea %}
|
||||
<div class="row">
|
||||
<div class="col-sm-5">
|
||||
{% include "admin/searchform.html" %}
|
||||
</div>
|
||||
<div id="taglist" class="col-sm-5"> </div>
|
||||
</div>
|
||||
|
||||
<div id="listing">
|
||||
<table id="objects_table" class="table {{ styles }}">
|
||||
<thead>
|
||||
<tr>
|
||||
{% block table_header %}{% endblock %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,11 +0,0 @@
|
||||
{% load i18n %}
|
||||
{% if target == "permissions" %}
|
||||
<option value="">{% trans "Choose a value" %}</option>
|
||||
{% endif %}
|
||||
{% for mb in mailboxes %}
|
||||
{% if target == "permissions" %}
|
||||
<option value='{{ mb.id }}'>{{ mb.name }}</option>
|
||||
{% else %}
|
||||
<option value='{{ mb.id }}'>{{ mb.full_address }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@@ -1,5 +0,0 @@
|
||||
{% load form_tags %}
|
||||
{% render_field form.email %}
|
||||
{% include "common/generic_field.html" with field=form.quota help_display_mode="tooltip" label_width="col-sm-4" appended_text=_("MB") activator=True activator_value=form.quota_act.value deactivate_if_empty=False activator_label=_("Use domain default value") activator_size="col-sm-5" %}
|
||||
{% render_extra_fields form %}
|
||||
{% render_fields_group form "aliases" %}
|
||||
@@ -1,6 +0,0 @@
|
||||
{% extends "common/generic_modal_form.html" %}{% load form_tags %}{% block formbody %}
|
||||
{% render_field form.name %}
|
||||
{% render_field form.quota appended_text=_("MB") %}
|
||||
{% render_fields_group form "aliases" %}
|
||||
{% render_field form.enabled %}
|
||||
{% endblock %}
|
||||
@@ -1,2 +0,0 @@
|
||||
{% load form_tags %}
|
||||
{% render_fields_group form "domains" %}
|
||||
@@ -1,5 +0,0 @@
|
||||
{% load i18n %}
|
||||
<th width="50%" class="sortable" data-sort_order="address">{% trans "Address" %}</th>
|
||||
<th class="sortable" data-sort_order="quota_value__bytes">{% trans "Value" %}</th>
|
||||
<th class="sortable" data-sort_order="quota">{% trans "Limit" %}</th>
|
||||
<th class="sortable" data-sort_order="quota_usage" id="quotabar">{% trans "Usage (%)" %}</th>
|
||||
@@ -1,15 +0,0 @@
|
||||
{% load lib_tags admin_tags %}
|
||||
|
||||
{% for mbox in mboxes %}
|
||||
<tr>
|
||||
<td>{% identity_modify_link mbox.user "mail" %}</td>
|
||||
<td>{{ mbox.get_quota }}M</td>
|
||||
<td>{{ mbox.quota }}M</td>
|
||||
<td name="quota">
|
||||
<div id="quotabar" rel="tooltip" title="{{ mbox.get_quota_in_percent }}%"
|
||||
class="progress">
|
||||
<div class="{% progress_color mbox.get_quota_in_percent %}" style="width: {{ mbox.get_quota_in_percent }}%"></div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@@ -1,11 +0,0 @@
|
||||
{% load i18n %}
|
||||
<form id="searchform" class="form-horizontal" role="form">
|
||||
<div class="form-group has-feedback">
|
||||
<div class="col-sm-12">
|
||||
<input class="form-control" type="text"
|
||||
value={% if squery %}"{{ squery }}"{% else %}"{% trans 'Search' %}"{% endif %}
|
||||
id="searchquery" name="searchquery" />
|
||||
<span class="fa fa-search form-control-feedback"></span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,280 +0,0 @@
|
||||
"""
|
||||
Admin extension tags.
|
||||
"""
|
||||
|
||||
from django import template
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.templatetags.lib_tags import render_link
|
||||
from modoboa.lib.web_utils import render_actions
|
||||
|
||||
register = template.Library()
|
||||
|
||||
genders = {
|
||||
"Enabled": (ugettext_lazy("enabled_m"), ugettext_lazy("enabled_f"))
|
||||
}
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def domains_menu(selection, user):
|
||||
"""Specific menu for domain related operations.
|
||||
|
||||
Corresponds to the menu visible on the left column when you go to
|
||||
*Domains*.
|
||||
|
||||
:param str selection: menu entry currently selected
|
||||
:param ``User`` user: connected user
|
||||
:rtype: str
|
||||
:return: rendered menu (as HTML)
|
||||
"""
|
||||
domain_list_url = (
|
||||
"list/" if selection != "statistics" else reverse("admin:domain_list")
|
||||
)
|
||||
entries = [
|
||||
{"name": "domains",
|
||||
"label": _("List domains"),
|
||||
"img": "fa fa-user",
|
||||
"class": "ajaxnav navigation",
|
||||
"url": domain_list_url},
|
||||
{"name": "statistics",
|
||||
"label": _("Statistics"),
|
||||
"img": "fa fa-line-chart",
|
||||
"class": "navigation",
|
||||
"url": reverse("admin:domain_statistics")}
|
||||
]
|
||||
if user.has_perm("admin.add_domain"):
|
||||
entries += [
|
||||
{"name": "newdomain",
|
||||
"label": _("Add domain"),
|
||||
"img": "fa fa-plus",
|
||||
"modal": True,
|
||||
"modalcb": "admin.newdomain_cb",
|
||||
"url": reverse("admin:domain_add")},
|
||||
]
|
||||
entries += events.raiseQueryEvent("ExtraDomainMenuEntries", user)
|
||||
entries += [
|
||||
{"name": "import",
|
||||
"label": _("Import"),
|
||||
"img": "fa fa-folder-open",
|
||||
"url": reverse("admin:domain_import"),
|
||||
"modal": True,
|
||||
"modalcb": "admin.importform_cb"},
|
||||
{"name": "export",
|
||||
"label": _("Export"),
|
||||
"img": "fa fa-share-alt",
|
||||
"url": reverse("admin:domain_export"),
|
||||
"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, selection=None):
|
||||
"""Menu specific to the Identities page.
|
||||
|
||||
:param ``User`` user: the connecter user
|
||||
:rtype: str
|
||||
:return: the rendered menu
|
||||
"""
|
||||
entries = [
|
||||
{"name": "identities",
|
||||
"label": _("List identities"),
|
||||
"img": "fa fa-user",
|
||||
"class": "ajaxnav navigation",
|
||||
"url": "list/"},
|
||||
{"name": "quotas",
|
||||
"label": _("List quotas"),
|
||||
"img": "fa fa-hdd-o",
|
||||
"class": "ajaxnav navigation",
|
||||
"url": "quotas/"},
|
||||
{"name": "newaccount",
|
||||
"label": _("Add account"),
|
||||
"img": "fa fa-plus",
|
||||
"modal": True,
|
||||
"modalcb": "admin.newaccount_cb",
|
||||
"url": reverse("admin:account_add")},
|
||||
{"name": "newalias",
|
||||
"label": _("Add alias"),
|
||||
"img": "fa fa-plus",
|
||||
"modal": True,
|
||||
"modalcb": "admin.aliasform_cb",
|
||||
"url": reverse("admin:alias_add")},
|
||||
{"name": "newforward",
|
||||
"label": _("Add forward"),
|
||||
"img": "fa fa-plus",
|
||||
"modal": True,
|
||||
"modalcb": "admin.aliasform_cb",
|
||||
"url": reverse("admin:forward_add")},
|
||||
{"name": "newdlist",
|
||||
"label": _("Add distribution list"),
|
||||
"img": "fa fa-plus",
|
||||
"modal": True,
|
||||
"modalcb": "admin.aliasform_cb",
|
||||
"url": reverse("admin:dlist_add")},
|
||||
{"name": "import",
|
||||
"label": _("Import"),
|
||||
"img": "fa fa-folder-open",
|
||||
"url": reverse("admin:identity_import"),
|
||||
"modal": True,
|
||||
"modalcb": "admin.importform_cb"},
|
||||
{"name": "export",
|
||||
"label": _("Export"),
|
||||
"img": "fa fa-share-alt",
|
||||
"url": reverse("admin:identity_export"),
|
||||
"modal": True,
|
||||
"modalcb": "admin.exportform_cb"
|
||||
}
|
||||
]
|
||||
|
||||
return render_to_string('common/menulist.html', {
|
||||
"entries": entries,
|
||||
"user": user
|
||||
})
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def domain_actions(user, domain):
|
||||
actions = []
|
||||
if domain.__class__.__name__ == 'Domain':
|
||||
actions = [
|
||||
{"name": "listidentities",
|
||||
"url": "{0}#list/?searchquery=@{1}".format(
|
||||
reverse("admin:identity_list"), domain.name),
|
||||
"title": _("View the domain's identities"),
|
||||
"img": "fa fa-user"}
|
||||
]
|
||||
if user.has_perm("admin.delete_domain"):
|
||||
actions.append({
|
||||
"name": "deldomain",
|
||||
"url": reverse("admin:domain_delete", args=[domain.id]),
|
||||
"title": _("Delete %s?" % domain.name),
|
||||
"img": "fa fa-trash"
|
||||
})
|
||||
else:
|
||||
actions = events.raiseQueryEvent('GetDomainActions', user, domain)
|
||||
|
||||
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("admin:account_delete", args=[objid]),
|
||||
"img": "fa fa-trash",
|
||||
"title": _("Delete %s?" % ident.username)},
|
||||
]
|
||||
else:
|
||||
actions = [
|
||||
{"name": "delalias",
|
||||
"url": reverse("admin:alias_delete") + "?selection=%s" % objid,
|
||||
"img": "fa fa-trash",
|
||||
"title": _("Delete %s?" % ident.full_address)},
|
||||
]
|
||||
return render_actions(actions)
|
||||
|
||||
|
||||
@register.assignment_tag
|
||||
def check_identity_status(identity):
|
||||
"""Check if identity is enabled or not."""
|
||||
if identity.__class__.__name__ == "User":
|
||||
if identity.mailbox_set.count() \
|
||||
and not identity.mailbox_set.all()[0].domain.enabled:
|
||||
return False
|
||||
elif not identity.is_active:
|
||||
return False
|
||||
elif not identity.enabled or not identity.domain.enabled:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def domain_modify_link(domain):
|
||||
linkdef = {"label": domain.name, "modal": True}
|
||||
if domain.__class__.__name__ == "Domain":
|
||||
linkdef["url"] = reverse("admin:domain_change", args=[domain.id])
|
||||
linkdef["modalcb"] = "admin.domainform_cb"
|
||||
else:
|
||||
tmp = events.raiseDictEvent('GetDomainModifyLink', domain)
|
||||
for key in ['url', 'modalcb']:
|
||||
linkdef[key] = tmp[key]
|
||||
return render_link(linkdef)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def domain_aliases(domain):
|
||||
"""Display domain aliases of this domain.
|
||||
|
||||
:param domain:
|
||||
:rtype: str
|
||||
"""
|
||||
if not domain.aliases.count():
|
||||
return '---'
|
||||
res = ''
|
||||
for alias in domain.aliases.all():
|
||||
res += '%s<br/>' % alias.name
|
||||
return res
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def identity_modify_link(identity, active_tab='default'):
|
||||
"""Return the appropriate modification link.
|
||||
|
||||
According to the identity type, a specific modification link (URL)
|
||||
must be used.
|
||||
|
||||
:param identity: a ``User`` or ``Alias`` instance
|
||||
:param str active_tab: the tab to display
|
||||
:rtype: str
|
||||
"""
|
||||
linkdef = {"label": identity.identity, "modal": True}
|
||||
if identity.__class__.__name__ == "User":
|
||||
linkdef["url"] = reverse("admin:account_change", args=[identity.id])
|
||||
linkdef["url"] += "?active_tab=%s" % active_tab
|
||||
linkdef["modalcb"] = "admin.editaccount_cb"
|
||||
else:
|
||||
linkdef["url"] = reverse("admin:alias_change", args=[identity.id])
|
||||
linkdef["modalcb"] = "admin.aliasform_cb"
|
||||
return render_link(linkdef)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def domadmin_actions(daid, domid):
|
||||
actions = [{
|
||||
"name": "removeperm",
|
||||
"url": "{0}?domid={1}&daid={2}".format(
|
||||
reverse("admin:permission_remove"), domid, daid),
|
||||
"img": "fa fa-trash",
|
||||
"title": _("Remove this permission")
|
||||
}]
|
||||
return render_actions(actions)
|
||||
|
||||
|
||||
@register.filter
|
||||
def gender(value, target):
|
||||
if value in genders:
|
||||
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)
|
||||
@@ -1,14 +0,0 @@
|
||||
from .domain import DomainTestCase
|
||||
from .domain_alias import DomainAliasTestCase
|
||||
from .account import AccountTestCase, PermissionsTestCase
|
||||
from .alias import AliasTestCase
|
||||
from .import_ import ImportTestCase
|
||||
from .export import ExportTestCase
|
||||
from .password_schemes import PasswordSchemesTestCase
|
||||
from .user import ForwardTestCase
|
||||
|
||||
__all__ = [
|
||||
'DomainTestCase', 'DomainAliasTestCase', 'AccountTestCase',
|
||||
'PermissionsTestCase', 'AliasTestCase', 'ImportTestCase',
|
||||
'ExportTestCase', 'PasswordSchemesTestCase', 'ForwardTestCase'
|
||||
]
|
||||
@@ -1,197 +0,0 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from modoboa.core.models import User
|
||||
from modoboa.extensions.admin import factories
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, Mailbox
|
||||
)
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
|
||||
|
||||
class AccountTestCase(ModoTestCase):
|
||||
fixtures = ["initial_users.json"]
|
||||
|
||||
def setUp(self):
|
||||
super(AccountTestCase, self).setUp()
|
||||
factories.populate_database()
|
||||
|
||||
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='step2'
|
||||
)
|
||||
self.ajax_post(reverse("admin:account_add"), 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, 10)
|
||||
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, 3)
|
||||
|
||||
values["username"] = "pouet@test.com"
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", args=[account.id]), values
|
||||
)
|
||||
mb = Mailbox.objects.get(pk=mb.id)
|
||||
self.assertEqual(mb.full_address, "pouet@test.com")
|
||||
self.assertEqual(mb.quota_value.username, "pouet@test.com")
|
||||
self.ajax_post(
|
||||
reverse("admin:account_delete", args=[account.id]), {}
|
||||
)
|
||||
|
||||
def _set_quota(self, email, value, expected_status=200):
|
||||
account = User.objects.get(username=email)
|
||||
values = {
|
||||
"username": email, "role": "SimpleUsers", "quota_act": False,
|
||||
"is_active": True, "quota": value, "email": email
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", args=[account.id]),
|
||||
values, status=expected_status
|
||||
)
|
||||
|
||||
def test_set_nul_quota_as_superadmin(self):
|
||||
self._set_quota("user@test.com", 0)
|
||||
|
||||
def test_set_nul_quota_as_domainadmin(self):
|
||||
self.clt.logout()
|
||||
self.assertTrue(
|
||||
self.clt.login(username="admin@test.com", password="toto")
|
||||
)
|
||||
self._set_quota("user@test.com", 0, 400)
|
||||
self.clt.logout()
|
||||
self.assertTrue(
|
||||
self.clt.login(username="admin@test2.com", password="toto")
|
||||
)
|
||||
self._set_quota("user@test2.com", 0)
|
||||
|
||||
|
||||
class PermissionsTestCase(ModoTestCase):
|
||||
fixtures = ["initial_users.json"]
|
||||
|
||||
def setUp(self):
|
||||
super(PermissionsTestCase, self).setUp()
|
||||
factories.populate_database()
|
||||
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",
|
||||
quota_act=True
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.clt.logout()
|
||||
|
||||
def test_domain_admins(self):
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", args=[self.user.id]),
|
||||
self.values
|
||||
)
|
||||
self.assertEqual(self.user.group, "DomainAdmins")
|
||||
|
||||
self.values["role"] = "SimpleUsers"
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", args=[self.user.id]),
|
||||
self.values
|
||||
)
|
||||
self.assertNotEqual(self.user.group, 'DomainAdmins')
|
||||
|
||||
def test_superusers(self):
|
||||
self.values["role"] = "SuperAdmins"
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", args=[self.user.id]),
|
||||
self.values
|
||||
)
|
||||
self.assertEqual(
|
||||
User.objects.get(username="user@test.com").is_superuser, True
|
||||
)
|
||||
|
||||
self.values["role"] = "SimpleUsers"
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", 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.assertTrue(
|
||||
self.clt.login(username="admin@test.com", password="toto")
|
||||
)
|
||||
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.ajax_post(
|
||||
reverse("admin:account_change", args=[admin.id]),
|
||||
values
|
||||
)
|
||||
self.assertEqual(admin.group, "DomainAdmins")
|
||||
self.assertTrue(admin.can_access(Domain.objects.get(name="test.com")))
|
||||
|
||||
values["role"] = "SuperAdmins"
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", 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("admin:domain_list"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.clt.get(
|
||||
reverse("admin:account_change", args=[self.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='step2'
|
||||
)
|
||||
self.ajax_post(
|
||||
reverse("admin:account_add"),
|
||||
values
|
||||
)
|
||||
account = User.objects.get(username="superadmin2@test.com")
|
||||
self.clt.logout()
|
||||
self.clt.login(username="admin@test.com", password="toto")
|
||||
self.ajax_post(
|
||||
reverse("admin:account_delete", args=[account.id]), {}, 403
|
||||
)
|
||||
|
||||
def test_domainadmin_dlist_local_domain_not_owned(self):
|
||||
"""Check if a domain admin can use a local mailbox he can't
|
||||
access as a recipient in a distribution list"""
|
||||
values = dict(
|
||||
email="all@test.com",
|
||||
recipients="user@test.com",
|
||||
recipients_1="user@test2.com",
|
||||
enabled=True
|
||||
)
|
||||
self.ajax_post(reverse("admin:dlist_add"), values)
|
||||
@@ -1,134 +0,0 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from modoboa.core.models import User
|
||||
from modoboa.extensions.admin import factories
|
||||
from modoboa.extensions.admin.models import (
|
||||
Alias
|
||||
)
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
|
||||
|
||||
class AliasTestCase(ModoTestCase):
|
||||
fixtures = ["initial_users.json"]
|
||||
|
||||
def setUp(self):
|
||||
super(AliasTestCase, self).setUp()
|
||||
factories.populate_database()
|
||||
|
||||
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.ajax_post(
|
||||
reverse("admin:account_change", args=[user.id]),
|
||||
values
|
||||
)
|
||||
self.assertEqual(user.mailbox_set.all()[0].alias_set.count(), 2)
|
||||
|
||||
del values["aliases_1"]
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", args=[user.id]),
|
||||
values
|
||||
)
|
||||
self.assertEqual(user.mailbox_set.all()[0].alias_set.count(), 1)
|
||||
|
||||
def test_upper_case_alias(self):
|
||||
"""Try to create an upper case alias."""
|
||||
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"
|
||||
)
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", args=[user.id]),
|
||||
values
|
||||
)
|
||||
self.assertEqual(user.mailbox_set.first().alias_set.count(), 1)
|
||||
self.assertEqual(
|
||||
user.mailbox_set.first().alias_set.first().full_address,
|
||||
"toto@test.com"
|
||||
)
|
||||
|
||||
values = {
|
||||
"email": "Titi@test.com", "recipients": "user@test.com",
|
||||
"enabled": True
|
||||
}
|
||||
self.ajax_post(reverse("admin:alias_add"), values)
|
||||
self.assertEqual(
|
||||
user.mailbox_set.first().alias_set.first().full_address,
|
||||
"titi@test.com"
|
||||
)
|
||||
|
||||
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.ajax_post(
|
||||
reverse("admin:dlist_add"), values
|
||||
)
|
||||
user = User.objects.get(username="user@test.com")
|
||||
self.assertEqual(user.mailbox_set.all()[0].alias_set.count(), 2)
|
||||
admin = User.objects.get(username="admin@test.com")
|
||||
self.assertEqual(admin.mailbox_set.all()[0].alias_set.count(), 1)
|
||||
|
||||
dlist = Alias.objects.get(address="all", domain__name="test.com")
|
||||
self.assertEqual(len(dlist.get_recipients()), 3)
|
||||
del values["recipients_1"]
|
||||
self.ajax_post(
|
||||
reverse("admin:alias_change", args=[dlist.id]),
|
||||
values
|
||||
)
|
||||
self.assertEqual(dlist.get_recipients_count(), 2)
|
||||
|
||||
self.ajax_post(
|
||||
reverse("admin:alias_delete") + "?selection=%d" % dlist.id, {}
|
||||
)
|
||||
self.assertRaises(Alias.DoesNotExist, Alias.objects.get,
|
||||
address="all", domain__name="test.com")
|
||||
|
||||
def test_forward(self):
|
||||
values = dict(email="forward2@test.com", recipients="rcpt@dest.com")
|
||||
self.ajax_post(
|
||||
reverse("admin:forward_add"), values
|
||||
)
|
||||
fwd = Alias.objects.get(address="forward2", domain__name="test.com")
|
||||
self.assertEqual(fwd.get_recipients_count(), 1)
|
||||
|
||||
values["recipients"] = "rcpt2@dest.com"
|
||||
self.ajax_post(
|
||||
reverse("admin:alias_change",
|
||||
args=[fwd.id]),
|
||||
values
|
||||
)
|
||||
self.assertEqual(fwd.get_recipients_count(), 1)
|
||||
|
||||
self.ajax_post(
|
||||
reverse("admin:alias_delete") + "?selection=%d"
|
||||
% fwd.id, {}
|
||||
)
|
||||
self.assertRaises(Alias.DoesNotExist, Alias.objects.get,
|
||||
address="forward2", domain__name="test.com")
|
||||
|
||||
def test_forward_and_local_copies(self):
|
||||
values = dict(email="user@test.com", recipients="rcpt@dest.com")
|
||||
self.ajax_post(
|
||||
reverse("admin:forward_add"), 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.ajax_post(
|
||||
reverse("admin:alias_change", args=[fwd.id]),
|
||||
values
|
||||
)
|
||||
fwd = Alias.objects.get(pk=fwd.pk)
|
||||
self.assertEqual(fwd.get_recipients_count(), 2)
|
||||
self.assertEqual(fwd.aliases.count(), 0)
|
||||
@@ -1,112 +0,0 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, Alias
|
||||
)
|
||||
from modoboa.extensions.admin import factories
|
||||
|
||||
|
||||
class DomainTestCase(ModoTestCase):
|
||||
fixtures = ["initial_users.json"]
|
||||
|
||||
def setUp(self):
|
||||
super(DomainTestCase, self).setUp()
|
||||
factories.populate_database()
|
||||
|
||||
def test_create(self):
|
||||
"""Test the creation of a domain
|
||||
"""
|
||||
values = {
|
||||
"name": "pouet.com", "quota": 100, "create_dom_admin": "no",
|
||||
"stepid": 'step2'
|
||||
}
|
||||
self.ajax_post(reverse("admin:domain_add"), 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": 'step2'
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("admin:domain_add"),
|
||||
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": 'step2'
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("admin:domain_add"),
|
||||
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_using_default_quota(self):
|
||||
parameters.save_admin('DEFAULT_DOMAIN_QUOTA', 50, app='admin')
|
||||
values = {
|
||||
"name": "pouet.com", "create_dom_admin": "yes",
|
||||
"dom_admin_username": "toto", "create_aliases": "yes",
|
||||
"stepid": 'step2'
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("admin:domain_add"),
|
||||
values
|
||||
)
|
||||
dom = Domain.objects.get(name="pouet.com")
|
||||
self.assertEqual(dom.quota, 50)
|
||||
da = User.objects.get(username="toto@pouet.com")
|
||||
self.assertEqual(da.mailbox_set.all()[0].quota, 50)
|
||||
|
||||
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
|
||||
}
|
||||
dom = Domain.objects.get(name="test.com")
|
||||
self.ajax_post(
|
||||
reverse("admin:domain_change", args=[dom.id]),
|
||||
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
|
||||
"""
|
||||
dom = Domain.objects.get(name="test.com")
|
||||
self.ajax_post(
|
||||
reverse("admin:domain_delete", args=[dom.id]),
|
||||
{}
|
||||
)
|
||||
with self.assertRaises(Domain.DoesNotExist):
|
||||
Domain.objects.get(pk=1)
|
||||
@@ -1,48 +0,0 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, DomainAlias
|
||||
)
|
||||
from modoboa.extensions.admin import factories
|
||||
|
||||
|
||||
class DomainAliasTestCase(ModoTestCase):
|
||||
fixtures = ['initial_users.json']
|
||||
|
||||
def setUp(self):
|
||||
super(DomainAliasTestCase, self).setUp()
|
||||
factories.populate_database()
|
||||
self.dom = Domain.objects.get(name='test.com')
|
||||
|
||||
def test_model(self):
|
||||
dom = Domain.objects.get(name="test.com")
|
||||
domal = DomainAlias()
|
||||
domal.name = "domalias.net"
|
||||
domal.target = dom
|
||||
domal.enabled = True
|
||||
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.ajax_post(
|
||||
reverse("admin:domain_change",
|
||||
args=[dom.id]),
|
||||
values
|
||||
)
|
||||
self.assertEqual(dom.domainalias_set.count(), 2)
|
||||
|
||||
del values["aliases_1"]
|
||||
self.ajax_post(
|
||||
reverse("admin:domain_change",
|
||||
args=[dom.id]),
|
||||
values
|
||||
)
|
||||
self.assertEqual(dom.domainalias_set.count(), 1)
|
||||
@@ -1,84 +0,0 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from modoboa.extensions.admin import factories
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
|
||||
|
||||
class ExportTestCase(ModoTestCase):
|
||||
fixtures = ["initial_users.json"]
|
||||
|
||||
def setUp(self):
|
||||
super(ExportTestCase, self).setUp()
|
||||
factories.populate_database()
|
||||
|
||||
def __export_identities(self, idtfilter="", grpfilter=""):
|
||||
self.clt.get(
|
||||
reverse("admin:_identity_list")
|
||||
+ "?grpfilter=%s&idtfilter=%s" % (grpfilter, idtfilter)
|
||||
)
|
||||
return self.clt.post(
|
||||
reverse("admin:identity_export"),
|
||||
{"filename": "test.csv"}
|
||||
)
|
||||
|
||||
def assertListEqual(self, list1, list2):
|
||||
list2 = list2.split('\r\n')
|
||||
for entry in list1.split('\r\n'):
|
||||
if not entry:
|
||||
continue
|
||||
self.assertIn(entry, list2)
|
||||
|
||||
def test_export_identities(self):
|
||||
response = self.__export_identities()
|
||||
self.assertListEqual(
|
||||
"account;admin;{CRYPT}dTTsGDkA5ZHKg;;;True;SuperAdmins;;\r\naccount;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com\r\naccount;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10\r\nalias;alias@test.com;True;user@test.com\r\nforward;forward@test.com;True;user@external.com\r\ndlist;postmaster@test.com;True;toto@titi.com;test@truc.fr\r\n",
|
||||
response.content.strip()
|
||||
)
|
||||
|
||||
def test_export_simpleusers(self):
|
||||
response = self.__export_identities(
|
||||
idtfilter="account", grpfilter="SimpleUsers"
|
||||
)
|
||||
self.assertListEqual(
|
||||
"account;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com;10\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com;10",
|
||||
response.content.strip()
|
||||
)
|
||||
|
||||
def test_export_superadmins(self):
|
||||
response = self.__export_identities(
|
||||
idtfilter="account", grpfilter="SuperAdmins"
|
||||
)
|
||||
self.assertEqual(
|
||||
response.content.strip(),
|
||||
"account;admin;{CRYPT}dTTsGDkA5ZHKg;;;True;SuperAdmins;;"
|
||||
)
|
||||
|
||||
def test_export_domainadmins(self):
|
||||
response = self.__export_identities(
|
||||
idtfilter="account", grpfilter="DomainAdmins"
|
||||
)
|
||||
self.assertListEqual(
|
||||
"account;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;10;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;10;test2.com",
|
||||
response.content.strip()
|
||||
)
|
||||
|
||||
def test_export_aliases(self):
|
||||
response = self.__export_identities(idtfilter="alias")
|
||||
self.assertEqual(
|
||||
response.content.strip(),
|
||||
"alias;alias@test.com;True;user@test.com"
|
||||
)
|
||||
|
||||
def test_export_forwards(self):
|
||||
response = self.__export_identities(idtfilter="forward")
|
||||
self.assertEqual(
|
||||
response.content.strip(),
|
||||
"forward;forward@test.com;True;user@external.com"
|
||||
)
|
||||
|
||||
def test_export_dlists(self):
|
||||
response = self.__export_identities(idtfilter="dlist")
|
||||
self.assertEqual(
|
||||
response.content.strip(),
|
||||
"dlist;postmaster@test.com;True;toto@titi.com;test@truc.fr"
|
||||
)
|
||||
@@ -1,191 +0,0 @@
|
||||
# coding: utf-8
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from modoboa.core.models import User
|
||||
from modoboa.extensions.admin import factories
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, Alias, DomainAlias
|
||||
)
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
|
||||
|
||||
class ImportTestCase(ModoTestCase):
|
||||
fixtures = ["initial_users.json"]
|
||||
|
||||
def setUp(self):
|
||||
super(ImportTestCase, self).setUp()
|
||||
factories.populate_database()
|
||||
|
||||
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("admin:domain_import"), {
|
||||
"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_import_domains_with_conflict(self):
|
||||
f = ContentFile(b"""domain;test.alias;10;True
|
||||
domainalias;test.alias;test.com;True
|
||||
""", name="domains.csv")
|
||||
resp = self.clt.post(
|
||||
reverse("admin:domain_import"), {
|
||||
"sourcefile": f
|
||||
}
|
||||
)
|
||||
self.assertIn('Object already exists: domainalias', resp.content)
|
||||
|
||||
def test_identities_import(self):
|
||||
f = ContentFile(b"""
|
||||
account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 0
|
||||
account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 5; 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("admin:identity_import"),
|
||||
{"sourcefile": f, "crypt_password": True}
|
||||
)
|
||||
admin = User.objects.get(username="admin")
|
||||
u1 = User.objects.get(username="user1@test.com")
|
||||
mb1 = u1.mailbox_set.all()[0]
|
||||
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(mb1.use_domain_quota)
|
||||
self.assertEqual(mb1.quota, 0)
|
||||
self.assertTrue(admin.is_owner(mb1))
|
||||
self.assertEqual(mb1.full_address, "user1@test.com")
|
||||
self.assertTrue(
|
||||
self.clt.login(username="user1@test.com", password="toto")
|
||||
)
|
||||
|
||||
da = User.objects.get(username="truc@test.com")
|
||||
damb = da.mailbox_set.all()[0]
|
||||
self.assertEqual(da.first_name, u"René")
|
||||
self.assertEqual(da.group, "DomainAdmins")
|
||||
self.assertEqual(damb.quota, 5)
|
||||
self.assertFalse(damb.use_domain_quota)
|
||||
self.assertEqual(damb.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_invalid_quota(self):
|
||||
f = ContentFile(b"""
|
||||
account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; ; test.com
|
||||
""", name="identities.csv")
|
||||
resp = self.clt.post(
|
||||
reverse("admin:identity_import"),
|
||||
{"sourcefile": f, "crypt_password": True}
|
||||
)
|
||||
self.assertIn('wrong quota value', resp.content)
|
||||
|
||||
def test_import_quota_too_big(self):
|
||||
self.clt.logout()
|
||||
self.clt.login(username="admin@test.com", password="toto")
|
||||
f = ContentFile(b"""
|
||||
account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 20
|
||||
""", name="identities.csv")
|
||||
resp = self.clt.post(
|
||||
reverse("admin:identity_import"),
|
||||
{"sourcefile": f, "crypt_password": True}
|
||||
)
|
||||
self.assertIn('Quota is greater than the allowed', resp.content)
|
||||
|
||||
def test_import_missing_quota(self):
|
||||
f = ContentFile(b"""
|
||||
account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com
|
||||
""", name="identities.csv")
|
||||
resp = self.clt.post(
|
||||
reverse("admin:identity_import"),
|
||||
{"sourcefile": f, "crypt_password": True}
|
||||
)
|
||||
account = User.objects.get(username="user1@test.com")
|
||||
self.assertEqual(
|
||||
account.mailbox_set.all()[0].quota,
|
||||
account.mailbox_set.all()[0].domain.quota
|
||||
)
|
||||
|
||||
def test_import_duplicate(self):
|
||||
f = ContentFile(b"""
|
||||
account; admin@test.com; toto; Admin; ; True; DomainAdmins; admin@test.com; 0; test.com
|
||||
account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 0; test.com
|
||||
""", name="identities.csv")
|
||||
self.clt.post(
|
||||
reverse("admin:identity_import"),
|
||||
{"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; 50
|
||||
""", name="identities.csv")
|
||||
self.clt.post(
|
||||
reverse("admin:identity_import"),
|
||||
{"sourcefile": f, "crypt_password": True,
|
||||
"continue_if_exists": True}
|
||||
)
|
||||
with self.assertRaises(User.DoesNotExist):
|
||||
User.objects.get(username="sa@test.com")
|
||||
|
||||
def test_import_alias_with_empty_values(self):
|
||||
f = ContentFile(b"""
|
||||
alias;user.alias@test.com;True;user@test.com;;;;;;;;;;;;;;;;
|
||||
""", name="identities.csv")
|
||||
self.clt.post(
|
||||
reverse("admin:identity_import"),
|
||||
{"sourcefile": f, "crypt_password": True,
|
||||
"continue_if_exists": True}
|
||||
)
|
||||
alias = Alias.objects.get(
|
||||
domain__name="test.com", address="user.alias"
|
||||
)
|
||||
self.assertEqual(alias.type, "alias")
|
||||
@@ -1,55 +0,0 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from modoboa.core import load_core_settings
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.extensions.admin import factories
|
||||
|
||||
|
||||
class PasswordSchemesTestCase(ModoTestCase):
|
||||
fixtures = ['initial_users.json']
|
||||
|
||||
def setUp(self):
|
||||
super(PasswordSchemesTestCase, self).setUp()
|
||||
factories.populate_database()
|
||||
load_core_settings()
|
||||
|
||||
def _create_account(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='step2'
|
||||
)
|
||||
self.ajax_post(
|
||||
reverse("admin:account_add"),
|
||||
values
|
||||
)
|
||||
|
||||
def _test_scheme(self, name, startpattern):
|
||||
parameters.save_admin('PASSWORD_SCHEME', name, app='core')
|
||||
self._create_account()
|
||||
account = User.objects.get(username='tester@test.com')
|
||||
self.assertTrue(account.password.startswith(startpattern))
|
||||
self.assertTrue(account.check_password('toto'))
|
||||
|
||||
def test_sha512crypt_scheme(self):
|
||||
self._test_scheme('sha512crypt', '{SHA512-CRYPT}')
|
||||
|
||||
def test_sha256crypt_scheme(self):
|
||||
self._test_scheme('sha256crypt', '{SHA256-CRYPT}')
|
||||
|
||||
def test_md5crypt_scheme(self):
|
||||
self._test_scheme('md5crypt', '{MD5-CRYPT}')
|
||||
|
||||
def test_sha256_scheme(self):
|
||||
self._test_scheme('sha256', '{SHA256}')
|
||||
|
||||
def test_md5_scheme(self):
|
||||
self._test_scheme('md5', '{MD5}')
|
||||
|
||||
def test_crypt(self):
|
||||
self._test_scheme('crypt', '{CRYPT}')
|
||||
|
||||
def test_plain(self):
|
||||
self._test_scheme('plain', '{PLAIN}')
|
||||
@@ -1,26 +0,0 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib.tests import ModoTestCase
|
||||
from modoboa.extensions.admin.factories import populate_database
|
||||
from modoboa.extensions.admin.models import Alias
|
||||
|
||||
|
||||
class ForwardTestCase(ModoTestCase):
|
||||
fixtures = ['initial_users.json']
|
||||
|
||||
def setUp(self):
|
||||
super(ForwardTestCase, self).setUp()
|
||||
populate_database()
|
||||
|
||||
def test_forward_permissions(self):
|
||||
self.clt.logout()
|
||||
self.clt.login(username='user@test.com', password='toto')
|
||||
self.ajax_post(
|
||||
reverse('user_forward'),
|
||||
{'dest': 'user@extdomain.com', 'keepcopies': True}
|
||||
)
|
||||
forward = Alias.objects.get(address='user', domain__name='test.com')
|
||||
sadmin = User.objects.get(username='admin')
|
||||
self.assertTrue(sadmin.can_access(forward))
|
||||
domadmin = User.objects.get(username='admin@test.com')
|
||||
self.assertTrue(domadmin.can_access(forward))
|
||||
@@ -1,60 +0,0 @@
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
|
||||
urlpatterns = patterns(
|
||||
'modoboa.extensions.admin.views.domain',
|
||||
|
||||
url(r'^$', 'index', name="index"),
|
||||
url(r'^domains/$', 'domains', name="domain_list"),
|
||||
url(r'^domains/list/$', '_domains', name="_domain_list"),
|
||||
url(r'^domains/flatlist/$', 'domains_list', name="domain_flat_list"),
|
||||
url(r'^domains/stats/$', 'domain_statistics', name="domain_statistics"),
|
||||
url(r'^domains/new/', 'newdomain', name='domain_add'),
|
||||
url(r'^domains/(?P<dom_id>\d+)/edit/$', 'editdomain', name="domain_change"),
|
||||
url(r'^domains/(?P<dom_id>\d+)/delete/$', 'deldomain',
|
||||
name="domain_delete"),
|
||||
)
|
||||
|
||||
urlpatterns += patterns(
|
||||
'modoboa.extensions.admin.views.identity',
|
||||
|
||||
url(r'^permissions/remove/$', 'remove_permission',
|
||||
name="permission_remove"),
|
||||
|
||||
url(r'^identities/$', 'identities', name="identity_list"),
|
||||
url(r'^identities/list/$', '_identities', name="_identity_list"),
|
||||
url(r'^identities/quotas/$', 'list_quotas', name="quota_list"),
|
||||
url(r'^identities/page/$', 'get_next_page', name="identity_page"),
|
||||
|
||||
url(r'^accounts/list/$', 'accounts_list', name="account_list"),
|
||||
url(r'^accounts/new/$', 'newaccount', name="account_add"),
|
||||
url(r'^accounts/edit/(?P<accountid>\d+)/$', 'editaccount',
|
||||
name="account_change"),
|
||||
url(r'^accounts/delete/(?P<accountid>\d+)/$', 'delaccount',
|
||||
name="account_delete"),
|
||||
)
|
||||
|
||||
urlpatterns += patterns(
|
||||
'modoboa.extensions.admin.views.alias',
|
||||
|
||||
url(r'^distriblists/new/$', 'newdlist', name="dlist_add"),
|
||||
url(r'^forwards/new/$', 'newforward', name="forward_add"),
|
||||
|
||||
url(r'^aliases/new/$', 'newalias', name="alias_add"),
|
||||
url(r'^aliases/edit/(?P<alid>\d+)/$', 'editalias', name="alias_change"),
|
||||
url(r'^aliases/delete/$', 'delalias', name="alias_delete"),
|
||||
)
|
||||
|
||||
urlpatterns += patterns(
|
||||
'modoboa.extensions.admin.views.import',
|
||||
|
||||
url(r'^domains/import/$', 'import_domains', name="domain_import"),
|
||||
url(r'^identities/import/$', 'import_identities', name="identity_import"),
|
||||
)
|
||||
|
||||
urlpatterns += patterns(
|
||||
'modoboa.extensions.admin.views.export',
|
||||
|
||||
url(r'^domains/export/$', 'export_domains', name="domain_export"),
|
||||
url(r'^identities/export/$', 'export_identities', name="identity_export"),
|
||||
)
|
||||
@@ -1,135 +0,0 @@
|
||||
import reversion
|
||||
from django.db import IntegrityError
|
||||
from django.utils.translation import ugettext as _, ungettext
|
||||
from django.shortcuts import render
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.decorators import (
|
||||
login_required, permission_required
|
||||
)
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.web_utils import render_to_json_response
|
||||
from modoboa.lib.exceptions import PermDeniedException, Conflict
|
||||
from modoboa.extensions.admin.forms import AliasForm
|
||||
from modoboa.extensions.admin.models import Alias
|
||||
|
||||
|
||||
def _validate_alias(request, form, successmsg, callback=None):
|
||||
"""Alias validation
|
||||
|
||||
Common function shared between creation and modification actions.
|
||||
"""
|
||||
if form.is_valid():
|
||||
form.set_recipients()
|
||||
try:
|
||||
alias = form.save()
|
||||
except IntegrityError:
|
||||
raise Conflict(_("Alias with this name already exists"))
|
||||
if callback:
|
||||
callback(request.user, alias)
|
||||
return render_to_json_response(successmsg)
|
||||
|
||||
return render_to_json_response({'form_errors': form.errors}, status=400)
|
||||
|
||||
|
||||
def _new_alias(request, title, action, successmsg,
|
||||
tplname="admin/aliasform.html"):
|
||||
events.raiseEvent("CanCreate", request.user, "mailbox_aliases")
|
||||
if request.method == "POST":
|
||||
def callback(user, alias):
|
||||
alias.post_create(user)
|
||||
|
||||
form = AliasForm(request.user, request.POST)
|
||||
return _validate_alias(
|
||||
request, form, successmsg, callback
|
||||
)
|
||||
|
||||
ctx = {
|
||||
"title": title,
|
||||
"action": action,
|
||||
"formid": "aliasform",
|
||||
"action_label": _("Create"),
|
||||
"action_classes": "submit",
|
||||
"form": AliasForm(request.user)
|
||||
}
|
||||
return render(request, tplname, ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.add_alias")
|
||||
@reversion.create_revision()
|
||||
def newdlist(request):
|
||||
return _new_alias(
|
||||
request, _("New distribution list"), reverse("admin:dlist_add"),
|
||||
_("Distribution list created")
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.add_alias")
|
||||
@reversion.create_revision()
|
||||
def newalias(request):
|
||||
return _new_alias(
|
||||
request, _("New alias"), reverse("admin:alias_add"),
|
||||
_("Alias created")
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.add_alias")
|
||||
@reversion.create_revision()
|
||||
def newforward(request):
|
||||
return _new_alias(
|
||||
request, _("New forward"), reverse("admin:forward_add"),
|
||||
_("Forward created")
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.change_alias")
|
||||
@reversion.create_revision()
|
||||
def editalias(request, alid, tplname="admin/aliasform.html"):
|
||||
alias = Alias.objects.get(pk=alid)
|
||||
if not request.user.can_access(alias):
|
||||
raise PermDeniedException
|
||||
if request.method == "POST":
|
||||
if len(alias.get_recipients()) >= 2:
|
||||
successmsg = _("Distribution list modified")
|
||||
elif alias.extmboxes != "":
|
||||
successmsg = _("Forward modified")
|
||||
else:
|
||||
successmsg = _("Alias modified")
|
||||
form = AliasForm(request.user, request.POST, instance=alias)
|
||||
return _validate_alias(request, form, successmsg)
|
||||
|
||||
ctx = {
|
||||
'action': reverse("admin:alias_change", args=[alias.id]),
|
||||
'formid': 'aliasform',
|
||||
'title': alias.full_address,
|
||||
'action_label': _('Update'),
|
||||
'action_classes': 'submit',
|
||||
'form': AliasForm(request.user, instance=alias)
|
||||
}
|
||||
return render(request, tplname, ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.delete_alias")
|
||||
def delalias(request):
|
||||
selection = request.GET["selection"].split(",")
|
||||
for alid in selection:
|
||||
alias = Alias.objects.get(pk=alid)
|
||||
if not request.user.can_access(alias):
|
||||
raise PermDeniedException
|
||||
if alias.type == 'dlist':
|
||||
msg = "Distribution list deleted"
|
||||
msgs = "Distribution lists deleted"
|
||||
elif alias.type == 'forward':
|
||||
msg = "Forward deleted"
|
||||
msgs = "Forwards deleted"
|
||||
else:
|
||||
msg = "Alias deleted"
|
||||
msgs = "Aliases deleted"
|
||||
alias.delete()
|
||||
|
||||
msg = ungettext(msg, msgs, len(selection))
|
||||
return render_to_json_response(msg)
|
||||
@@ -1,154 +0,0 @@
|
||||
"""
|
||||
Domain related views.
|
||||
"""
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.utils.translation import ugettext as _, ungettext
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.contrib.auth.decorators import (
|
||||
login_required, permission_required, user_passes_test
|
||||
)
|
||||
|
||||
import reversion
|
||||
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib import parameters, events
|
||||
from modoboa.lib.web_utils import (
|
||||
_render_to_string, render_to_json_response
|
||||
)
|
||||
from modoboa.lib.exceptions import (
|
||||
PermDeniedException
|
||||
)
|
||||
from modoboa.lib.listing import get_sort_order, get_listing_page
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, DomainAlias, Mailbox, Alias
|
||||
)
|
||||
from modoboa.extensions.admin.forms import (
|
||||
DomainForm
|
||||
)
|
||||
from modoboa.extensions.admin.lib import get_domains
|
||||
|
||||
|
||||
@login_required
|
||||
def index(request):
|
||||
return HttpResponseRedirect(reverse("admin:domain_list"))
|
||||
|
||||
|
||||
@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")
|
||||
filters = dict(
|
||||
(flt, request.GET.get(flt, None))
|
||||
for flt in ['domfilter', 'searchquery']
|
||||
+ events.raiseQueryEvent('ExtraDomainFilters')
|
||||
)
|
||||
request.session['domains_filters'] = filters
|
||||
domainlist = get_domains(request.user, **filters)
|
||||
if sort_order == 'name':
|
||||
domainlist = sorted(
|
||||
domainlist,
|
||||
key=lambda d: getattr(d, sort_order), reverse=sort_dir == '-'
|
||||
)
|
||||
else:
|
||||
domainlist = sorted(domainlist, key=lambda d: d.tags[0],
|
||||
reverse=sort_dir == '-')
|
||||
context = {
|
||||
"handle_mailboxes": parameters.get_admin(
|
||||
"HANDLE_MAILBOXES", raise_error=False),
|
||||
"auto_account_removal": parameters.get_admin("AUTO_ACCOUNT_REMOVAL")
|
||||
}
|
||||
page = get_listing_page(domainlist, request.GET.get("page", 1))
|
||||
if page is None:
|
||||
context["length"] = 0
|
||||
else:
|
||||
context["rows"] = _render_to_string(
|
||||
request, 'admin/domains_table.html', {
|
||||
'domains': page.object_list,
|
||||
}
|
||||
)
|
||||
context["pages"] = [page.number]
|
||||
return render_to_json_response(context)
|
||||
|
||||
|
||||
@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("admin:identity_list")
|
||||
)
|
||||
return HttpResponseRedirect(reverse("core:user_index"))
|
||||
return render(request, tplname, {"selection": "domains"})
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("core.add_user")
|
||||
def domains_list(request):
|
||||
doms = [dom.name for dom in Domain.objects.get_for_admin(request.user)]
|
||||
return render_to_json_response(doms)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.add_domain")
|
||||
@reversion.create_revision()
|
||||
def newdomain(request):
|
||||
from modoboa.extensions.admin.forms import DomainWizard
|
||||
|
||||
events.raiseEvent("CanCreate", request.user, "domains")
|
||||
return DomainWizard(request).process()
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.view_domains")
|
||||
@reversion.create_revision()
|
||||
def editdomain(request, dom_id):
|
||||
"""Edit domain view."""
|
||||
domain = Domain.objects.get(pk=dom_id)
|
||||
if not request.user.can_access(domain):
|
||||
raise PermDeniedException
|
||||
|
||||
instances = dict(general=domain)
|
||||
events.raiseEvent("FillDomainInstances", request.user, domain, instances)
|
||||
return DomainForm(request, instances=instances).process()
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.delete_domain")
|
||||
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 PermDeniedException(_("You can't delete your own domain"))
|
||||
dom.delete(request.user, keepdir)
|
||||
|
||||
msg = ungettext("Domain deleted", "Domains deleted", 1)
|
||||
return render_to_json_response(msg)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.view_domains")
|
||||
def domain_statistics(request):
|
||||
"""A simple page to present domain statistics."""
|
||||
context = {}
|
||||
if request.user.group == "SuperAdmins":
|
||||
context.update({
|
||||
"domains_counter": Domain.objects.count(),
|
||||
"domain_aliases_counter": DomainAlias.objects.count(),
|
||||
"identities_counter": User.objects.count() + Alias.objects.count(),
|
||||
})
|
||||
context.update({"domains": Domain.objects.get_for_admin(request.user)})
|
||||
return render(request, "admin/domain_statistics.html", context)
|
||||
@@ -1,90 +0,0 @@
|
||||
"""Export related views."""
|
||||
|
||||
import cStringIO
|
||||
import csv
|
||||
|
||||
from rfc6266 import build_header
|
||||
|
||||
from django.contrib.auth.decorators import (
|
||||
login_required, permission_required, user_passes_test
|
||||
)
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.extensions.admin.forms import (
|
||||
ExportIdentitiesForm, ExportDomainsForm
|
||||
)
|
||||
from modoboa.extensions.admin.lib import get_domains, get_identities
|
||||
|
||||
|
||||
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"] = build_header(filename)
|
||||
return resp
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(
|
||||
lambda u: u.has_perm("core.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("admin:identity_export"),
|
||||
}
|
||||
|
||||
if request.method == "POST":
|
||||
form = ExportIdentitiesForm(request.POST)
|
||||
form.is_valid()
|
||||
fp = cStringIO.StringIO()
|
||||
csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"])
|
||||
identities = get_identities(
|
||||
request.user, **request.session['identities_filters'])
|
||||
for ident in 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")
|
||||
def export_domains(request):
|
||||
ctx = {
|
||||
"title": _("Export domains"),
|
||||
"action_label": _("Export"),
|
||||
"action_classes": "submit",
|
||||
"formid": "exportform",
|
||||
"action": reverse("admin:domain_export"),
|
||||
}
|
||||
|
||||
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 get_domains(request.user,
|
||||
**request.session['domains_filters']):
|
||||
dom.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,211 +0,0 @@
|
||||
"""Identity related views."""
|
||||
|
||||
from django.contrib.auth.decorators import (
|
||||
login_required, permission_required, user_passes_test
|
||||
)
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import ugettext as _, ungettext
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
import reversion
|
||||
|
||||
from modoboa.core.models import User
|
||||
from modoboa.extensions.admin.forms import (
|
||||
AccountForm
|
||||
)
|
||||
from modoboa.extensions.admin.lib import get_identities
|
||||
from modoboa.extensions.admin.models import Mailbox, Domain
|
||||
from modoboa.lib import parameters, events
|
||||
from modoboa.lib.exceptions import (
|
||||
PermDeniedException, BadRequest
|
||||
)
|
||||
from modoboa.lib.listing import (
|
||||
get_sort_order, get_listing_page
|
||||
)
|
||||
from modoboa.lib.web_utils import (
|
||||
_render_to_string, render_to_json_response
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(
|
||||
lambda u: u.has_perm("core.add_user") or u.has_perm("admin.add_alias")
|
||||
)
|
||||
def _identities(request):
|
||||
filters = dict((fname, request.GET.get(fname, None))
|
||||
for fname in ['searchquery', 'idtfilter', 'grpfilter'])
|
||||
request.session['identities_filters'] = filters
|
||||
idents_list = get_identities(request.user, **filters)
|
||||
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 == '-')
|
||||
context = {
|
||||
"handle_mailboxes": parameters.get_admin(
|
||||
"HANDLE_MAILBOXES", raise_error=False)
|
||||
}
|
||||
page = get_listing_page(objects, request.GET.get("page", 1))
|
||||
if page is None:
|
||||
context["length"] = 0
|
||||
else:
|
||||
context["headers"] = _render_to_string(
|
||||
request, "admin/identity_headers.html", {})
|
||||
context["rows"] = _render_to_string(
|
||||
request, "admin/identities_table.html", {
|
||||
"identities": page.object_list
|
||||
}
|
||||
)
|
||||
context["pages"] = [page.number]
|
||||
return render_to_json_response(context)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.add_mailbox")
|
||||
def list_quotas(request):
|
||||
from modoboa.lib.db_utils import db_type
|
||||
|
||||
sort_order, sort_dir = get_sort_order(request.GET, "address")
|
||||
mboxes = Mailbox.objects.get_for_admin(
|
||||
request.user, request.GET.get("searchquery", None)
|
||||
)
|
||||
mboxes = mboxes.exclude(quota=0)
|
||||
if sort_order in ["address", "quota"]:
|
||||
mboxes = mboxes.order_by("%s%s" % (sort_dir, sort_order))
|
||||
elif sort_order == "quota_value__bytes":
|
||||
where = "admin_mailbox.address||'@'||admin_domain.name"
|
||||
mboxes = mboxes.extra(
|
||||
select={"quota_value__bytes": "admin_quota.bytes"},
|
||||
where=["admin_quota.username=%s" % where],
|
||||
tables=["admin_quota", "admin_domain"],
|
||||
order_by=["%s%s" % (sort_dir, sort_order)]
|
||||
)
|
||||
elif sort_order == "quota_usage":
|
||||
where = "admin_mailbox.address||'@'||admin_domain.name"
|
||||
db_type = db_type()
|
||||
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"
|
||||
)
|
||||
if db_type == "mysql":
|
||||
where = "CONCAT(admin_mailbox.address,'@',admin_domain.name)"
|
||||
mboxes = mboxes.extra(
|
||||
select={'quota_usage': select},
|
||||
where=["admin_quota.username=%s" % where],
|
||||
tables=["admin_quota", "admin_domain"],
|
||||
order_by=["%s%s" % (sort_dir, sort_order)]
|
||||
)
|
||||
else:
|
||||
raise BadRequest(_("Invalid request"))
|
||||
page = get_listing_page(mboxes, request.GET.get("page", 1))
|
||||
context = {
|
||||
"headers": _render_to_string(
|
||||
request, "admin/quota_headers.html", {}
|
||||
)
|
||||
}
|
||||
if page is None:
|
||||
context["length"] = 0
|
||||
else:
|
||||
context["rows"] = _render_to_string(
|
||||
request, "admin/quotas.html", {
|
||||
"mboxes": page
|
||||
}
|
||||
)
|
||||
context["pages"] = [page.number]
|
||||
return render_to_json_response(context)
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(
|
||||
lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")
|
||||
or u.has_perm("admin.add_mailbox")
|
||||
)
|
||||
def get_next_page(request):
|
||||
"""Return the next page of the identity list."""
|
||||
if request.GET.get("objtype", "identity") == "identity":
|
||||
return _identities(request)
|
||||
return list_quotas(request)
|
||||
|
||||
|
||||
@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("core.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 render_to_json_response(res)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("core.add_user")
|
||||
@reversion.create_revision()
|
||||
def newaccount(request):
|
||||
"""Create a new account."""
|
||||
from modoboa.extensions.admin.forms import AccountWizard
|
||||
return AccountWizard(request).process()
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("core.change_user")
|
||||
@reversion.create_revision()
|
||||
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.mailbox_set.count():
|
||||
mb = account.mailbox_set.all()[0]
|
||||
|
||||
instances = dict(general=account, mail=mb, perms=account)
|
||||
events.raiseEvent("FillAccountInstances", request.user, account, instances)
|
||||
return AccountForm(request, instances=instances).process()
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("core.delete_user")
|
||||
def delaccount(request, accountid):
|
||||
keepdir = True if request.POST.get("keepdir", "false") == "true" else False
|
||||
User.objects.get(pk=accountid).delete(request.user, keep_mb_dir=keepdir)
|
||||
return render_to_json_response(
|
||||
ungettext("Account deleted", "Accounts deleted", 1)
|
||||
)
|
||||
|
||||
|
||||
@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 BadRequest(_("Invalid request"))
|
||||
try:
|
||||
account = User.objects.get(pk=daid)
|
||||
domain = Domain.objects.get(pk=domid)
|
||||
except (User.DoesNotExist, Domain.DoesNotExist):
|
||||
raise BadRequest(_("Invalid request"))
|
||||
if not request.user.can_access(account) or \
|
||||
not request.user.can_access(domain):
|
||||
raise PermDeniedException
|
||||
domain.remove_admin(account)
|
||||
return render_to_json_response({})
|
||||
@@ -1,177 +0,0 @@
|
||||
"""Import related views."""
|
||||
|
||||
import csv
|
||||
|
||||
from django.contrib.auth.decorators import (
|
||||
login_required, permission_required, user_passes_test
|
||||
)
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
import reversion
|
||||
|
||||
from modoboa.core.models import User
|
||||
from modoboa.extensions.admin.forms import ImportIdentitiesForm, ImportDataForm
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, DomainAlias, Alias
|
||||
)
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.exceptions import ModoboaException, Conflict
|
||||
|
||||
|
||||
def import_domain(user, row, formopts):
|
||||
"""Specific code for domains import"""
|
||||
dom = Domain()
|
||||
dom.from_csv(user, row)
|
||||
|
||||
|
||||
def import_domainalias(user, row, formopts):
|
||||
"""Specific code for domain aliases import"""
|
||||
domalias = DomainAlias()
|
||||
domalias.from_csv(user, row)
|
||||
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.add_domain")
|
||||
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>
|
||||
%s
|
||||
</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>
|
||||
""" % ''.join([unicode(hlp) for hlp in
|
||||
events.raiseQueryEvent('ExtraDomainImportHelp')]))
|
||||
|
||||
ctx = dict(
|
||||
title=_("Import domains"),
|
||||
action_label=_("Import"),
|
||||
action_classes="submit",
|
||||
action=reverse("admin:domain_import"),
|
||||
formid="importform",
|
||||
enctype="multipart/form-data",
|
||||
target="import_target",
|
||||
helptext=helptext,
|
||||
form=ImportDataForm()
|
||||
)
|
||||
return render(request, "admin/importform.html", ctx)
|
||||
|
||||
|
||||
def import_account(user, row, formopts):
|
||||
"""Specific code for accounts import"""
|
||||
account = User()
|
||||
account.from_csv(user, row, formopts["crypt_password"])
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@reversion.create_revision()
|
||||
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 as inst:
|
||||
error = str(inst)
|
||||
|
||||
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:
|
||||
fct = events.raiseQueryEvent(
|
||||
'ImportObject', row[0].strip()
|
||||
)
|
||||
if not fct:
|
||||
continue
|
||||
fct = fct[0]
|
||||
try:
|
||||
fct(request.user, row, form.cleaned_data)
|
||||
except Conflict:
|
||||
if form.cleaned_data["continue_if_exists"]:
|
||||
continue
|
||||
raise Conflict(
|
||||
_("Object already exists: %s"
|
||||
% form.cleaned_data['sepchar'].join(row[:2]))
|
||||
)
|
||||
cpt += 1
|
||||
msg = _("%d objects imported successfully" % cpt)
|
||||
return render(request, "admin/import_done.html", {
|
||||
"status": "ok", "msg": msg
|
||||
})
|
||||
except (ModoboaException) as 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("core.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; quota; [, 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("admin:identity_import"),
|
||||
formid="importform",
|
||||
enctype="multipart/form-data",
|
||||
target="import_target",
|
||||
form=ImportIdentitiesForm(),
|
||||
helptext=helptext
|
||||
)
|
||||
return render(request, "admin/importform.html", ctx)
|
||||
@@ -1,57 +0,0 @@
|
||||
import reversion
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.translation import ugettext as _
|
||||
from modoboa.lib.web_utils import (
|
||||
render_to_json_response, _render_to_string
|
||||
)
|
||||
from modoboa.extensions.admin.lib import needs_mailbox
|
||||
from modoboa.extensions.admin.models import Alias, Mailbox
|
||||
from modoboa.extensions.admin.forms import ForwardForm
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
@reversion.create_revision()
|
||||
def forward(request, tplname='admin/forward.html'):
|
||||
mb = request.user.mailbox_set.all()[0]
|
||||
try:
|
||||
al = Alias.objects.get(address=mb.address,
|
||||
domain__name=mb.domain.name)
|
||||
except Alias.DoesNotExist:
|
||||
al = None
|
||||
if request.method == "POST":
|
||||
form = ForwardForm(request.POST)
|
||||
error = None
|
||||
if form.is_valid():
|
||||
if al is None:
|
||||
al = Alias()
|
||||
al.address = mb.address
|
||||
al.domain = mb.domain
|
||||
al.enabled = mb.user.is_active
|
||||
intdests = []
|
||||
if form.cleaned_data["keepcopies"]:
|
||||
intdests += [mb]
|
||||
form.parse_dest()
|
||||
al.save(
|
||||
int_rcpts=intdests, ext_rcpts=form.dests, creator=request.user
|
||||
)
|
||||
return render_to_json_response(_("Forward updated"))
|
||||
|
||||
return render_to_json_response(
|
||||
{'form_errors': form.errors}, status=400
|
||||
)
|
||||
|
||||
form = ForwardForm()
|
||||
if al is not None:
|
||||
form.fields["dest"].initial = al.extmboxes
|
||||
try:
|
||||
al.mboxes.get(pk=mb.id)
|
||||
except Mailbox.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
form.fields["keepcopies"].initial = True
|
||||
return render_to_json_response({
|
||||
"content": _render_to_string(request, tplname, {
|
||||
"form": form
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user