Merge pull request #707 from tonioo/split_applications

Split applications
This commit is contained in:
Antoine Nguyen
2015-04-10 16:39:38 +02:00
301 changed files with 2889 additions and 55021 deletions
+10 -4
View File
@@ -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)
}
+6 -3
View File
@@ -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``.
+26 -22
View File
@@ -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::
+61 -4
View File
@@ -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
===================
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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>`_.
+1 -45
View File
@@ -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>`_.
+1 -36
View File
@@ -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>`_.
+1 -42
View File
@@ -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>`_.
+1 -39
View File
@@ -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>`_.
+1 -41
View File
@@ -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
View File
@@ -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>`_.
+4 -8
View File
@@ -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",
}]
+64 -44
View File
@@ -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:
+59 -136
View File
@@ -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
View File
@@ -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)
+5 -2
View File
@@ -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>
+42 -17
View File
@@ -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>
+1 -6
View File
@@ -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
View File
@@ -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")
-2
View File
@@ -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
View File
@@ -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
+5 -15
View File
@@ -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,
}),
})
+2 -1
View File
@@ -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.")
View File
-100
View File
@@ -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']
-79
View File
@@ -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)
-310
View File
@@ -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'
]
-478
View File
@@ -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"))
-152
View File
@@ -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
-362
View File
@@ -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"))
-45
View File
@@ -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"
-46
View File
@@ -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.")
)
-43
View File
@@ -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"
)
)
-114
View File
@@ -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'
]
-211
View File
@@ -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)
-81
View File
@@ -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()
-185
View File
@@ -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)
-334
View File
@@ -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
-285
View File
@@ -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);
-15
View File
@@ -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'
]
-197
View File
@@ -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)
-134
View File
@@ -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)
-112
View File
@@ -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)
-84
View File
@@ -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"
)
-191
View File
@@ -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}')
-26
View File
@@ -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))
-60
View File
@@ -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"),
)
-135
View File
@@ -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)
-154
View File
@@ -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)
-90
View File
@@ -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)
-211
View File
@@ -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({})
-177
View File
@@ -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)
-57
View File
@@ -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