mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
Merge branch 'master' into feature/resizable_mailbox_list
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
|
||||
env:
|
||||
- DJANGO=1.5
|
||||
|
||||
install:
|
||||
- pip install -q Django==$DJANGO --use-mirrors
|
||||
- python setup.py -q install
|
||||
- pip install -q MySQL-python --use-mirrors
|
||||
- pip install -q pexpect --use-mirrors
|
||||
|
||||
before_script:
|
||||
- mysql -e 'create database modoboa_test;'
|
||||
|
||||
script:
|
||||
- PYTHONPATH=tests python -m unittest install_from_scratch.DeployTest.test_silent
|
||||
+13
-13
@@ -2,7 +2,7 @@
|
||||
Modoboa
|
||||
#######
|
||||
|
||||
|latest-version| |downloads|
|
||||
|travis| |latest-version| |downloads|
|
||||
|
||||
Modoboa is a mail hosting and management platform including a modern
|
||||
and simplified Web User Interface. It provides useful components such
|
||||
@@ -26,12 +26,12 @@ frameworks.
|
||||
Main features
|
||||
*************
|
||||
|
||||
* Administration panel
|
||||
* `Amavisd-new <http://www.amavis.org>`_ frontend
|
||||
* Webmail
|
||||
* Per-user Sieve filters
|
||||
* Autoreply messages for Postfix
|
||||
* Graphical statistics about email traffic
|
||||
* Administration panel
|
||||
* `Amavisd-new <http://www.amavis.org>`_ frontend
|
||||
* Webmail
|
||||
* Per-user Sieve filters
|
||||
* Autoreply messages for Postfix
|
||||
* Graphical statistics about email traffic
|
||||
|
||||
*************
|
||||
Documentation
|
||||
@@ -54,10 +54,10 @@ External code
|
||||
|
||||
The following external libraries are provided with Modoboa:
|
||||
|
||||
* jQuery version 1.9.1 (http://www.jquery.org/)
|
||||
* jQuery-UI 1.10+ (http://jqueryui.com/)
|
||||
* Bootstrap, from Twitter version 2.3.1 (http://twitter.github.com/bootstrap/)
|
||||
* Bootstrap datepicker (https://bitbucket.org/eternicode/bootstrap-datepicker)
|
||||
* `jQuery version 1.9.1 <http://www.jquery.org/>`_
|
||||
* `jQuery-UI 1.10+ <http://jqueryui.com/>`_
|
||||
* `Bootstrap, from Twitter version 2.3.2 <http://twitter.github.com/bootstrap/>`_
|
||||
* `Bootstrap datetimepicker <http://www.malot.fr/bootstrap-datetimepicker/index.php>`_
|
||||
|
||||
.. |latest-version| image:: https://pypip.in/v/modoboa/badge.png
|
||||
:alt: Latest version on Pypi
|
||||
@@ -65,9 +65,9 @@ The following external libraries are provided with Modoboa:
|
||||
.. |downloads| image:: https://pypip.in/d/modoboa/badge.png
|
||||
:alt: Downloads from Pypi
|
||||
:target: https://crate.io/packages/modoboa/
|
||||
|
||||
.. |travis| image:: https://travis-ci.org/tonioo/modoboa.png?branch=master
|
||||
:target: https://travis-ci.org/tonioo/modoboa
|
||||
|
||||
.. image:: https://d2weczhvl823v0.cloudfront.net/tonioo/modoboa/trend.png
|
||||
:alt: Bitdeli badge
|
||||
:target: https://bitdeli.com/free
|
||||
|
||||
|
||||
+447
-64
@@ -11,10 +11,10 @@ Introduction
|
||||
Modoboa provides a simple API to interact with events. It understands
|
||||
two kinds of events:
|
||||
|
||||
* Those that return a value
|
||||
* Those that return nothing
|
||||
* Those returning a value
|
||||
* Those returning nothing
|
||||
|
||||
Listening to a specific event is achieved as follow::
|
||||
Listening to a specific event is achieved as follows::
|
||||
|
||||
from modoboa.lib import events
|
||||
|
||||
@@ -41,12 +41,32 @@ To stop listening to as specific event, you must use the
|
||||
|
||||
The parameters are the same than those used with ``register``.
|
||||
|
||||
To unregister all events declared by a specific extension, use the
|
||||
``unregister_extension`` function::
|
||||
|
||||
events.unregister_extension([name])
|
||||
|
||||
``name`` is the extension's name but it is optional. Leave it empty to
|
||||
let the function guess the name.
|
||||
|
||||
Read further to get a complete list and description of all available events.
|
||||
|
||||
****************
|
||||
Supported events
|
||||
****************
|
||||
|
||||
AccountAutoCreated
|
||||
==================
|
||||
|
||||
Raised when a new account is automatically created (example: LDAP
|
||||
synchronization).
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(account): pass
|
||||
|
||||
* ``account`` is the newly created account (``User`` instance)
|
||||
|
||||
AccountCreated
|
||||
==============
|
||||
|
||||
@@ -69,6 +89,32 @@ Raised when an existing account is deleted.
|
||||
|
||||
* ``account`` is the account that is going to be deleted
|
||||
* ``byuser`` is the adminstrator deleting ``account``
|
||||
|
||||
AccountExported
|
||||
===============
|
||||
|
||||
Raised when an account is exported to CSV.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(account): pass
|
||||
|
||||
* ``account`` is the account being exported
|
||||
|
||||
Must return a list of values to include in the export.
|
||||
|
||||
AccountImported
|
||||
===============
|
||||
|
||||
Raised when an account is imported from CSV.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user, account, row): pass
|
||||
|
||||
* ``user`` is the user importing the account
|
||||
* ``account`` is the account being imported
|
||||
* ``row`` is a list containing what remains from the CSV definition
|
||||
|
||||
AccountModified
|
||||
===============
|
||||
@@ -83,18 +129,6 @@ Raised when an existing account is modified.
|
||||
|
||||
* ``newaccount`` is the account after the modification
|
||||
|
||||
ExtraAccountActions
|
||||
===================
|
||||
|
||||
Raised when the account list is displayed. Let developers define new
|
||||
actions to act on a specific user.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(account): pass
|
||||
|
||||
* ``account`` is the account being listed
|
||||
|
||||
AdminMenuDisplay
|
||||
================
|
||||
|
||||
@@ -127,6 +161,21 @@ Raised just before a user tries to create a new object.
|
||||
Return ``True`` or ``False`` to indicate if this user can respectively
|
||||
create or not create a new ``Domain`` object.
|
||||
|
||||
CheckDomainName
|
||||
===============
|
||||
|
||||
Raised before the unicity of a domain name is checked. By default,
|
||||
modoboa prevents duplicate names between domains and domain aliases
|
||||
but extensions have the possibility to extend this rule using this
|
||||
event.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(): pass
|
||||
|
||||
Must return a list of 2uple, each one containing a model class and an
|
||||
associated label.
|
||||
|
||||
CheckExtraAccountForm
|
||||
=====================
|
||||
|
||||
@@ -144,52 +193,6 @@ this account is concerned by a specific form.
|
||||
Callbacks listening to this event must return a list containing one
|
||||
Boolean.
|
||||
|
||||
CreateDomain
|
||||
============
|
||||
|
||||
Raised when a new domain is created.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user, domain): pass
|
||||
|
||||
* ``user`` corresponds to the ``User`` object creating the domain (its owner)
|
||||
* ``domain`` is a ``Domain`` instance
|
||||
|
||||
CreateMailbox
|
||||
=============
|
||||
|
||||
Raised when a new mailbox is created.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user, mailbox): pass
|
||||
|
||||
* ``user`` is the new mailbox's owner (``User`` instance)
|
||||
* ``mailbox`` is the new mailbox (``Mailbox`` instance)
|
||||
|
||||
DeleteDomain
|
||||
============
|
||||
|
||||
Raised when an existing domain is about to be deleted.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(domain): pass
|
||||
|
||||
* ``domain`` is a ``Domain`` instance
|
||||
|
||||
DeleteMailbox
|
||||
=============
|
||||
|
||||
Raised when an existing mailbox is about to be deleted.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(mailbox): pass
|
||||
|
||||
* ``mailbox`` is a ``Mailbox`` instance
|
||||
|
||||
DomainAliasCreated
|
||||
==================
|
||||
|
||||
@@ -213,6 +216,29 @@ Raised when an existing domain alias is about to be deleted.
|
||||
|
||||
* ``domain_alias`` is a ``DomainAlias`` instance
|
||||
|
||||
DomainCreated
|
||||
=============
|
||||
|
||||
Raised when a new domain is created.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user, domain): pass
|
||||
|
||||
* ``user`` corresponds to the ``User`` object creating the domain (its owner)
|
||||
* ``domain`` is a ``Domain`` instance
|
||||
|
||||
DomainDeleted
|
||||
=============
|
||||
|
||||
Raised when an existing domain is about to be deleted.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(domain): pass
|
||||
|
||||
* ``domain`` is a ``Domain`` instance
|
||||
|
||||
DomainModified
|
||||
==============
|
||||
|
||||
@@ -223,7 +249,20 @@ Raised when a domain has been modified.
|
||||
def callback(domain): pass
|
||||
|
||||
* ``domain`` is the modified ``Domain`` instance, it contains an extra
|
||||
``oldname`` field which contains the old domain name
|
||||
``oldname`` field which contains the old name
|
||||
|
||||
DomainOwnershipRemoved
|
||||
======================
|
||||
|
||||
Raised before the ownership of a domain is removed from its original
|
||||
creator.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(owner, domain): pass
|
||||
|
||||
* ``owner`` is the original creator
|
||||
* ``domain`` is the ``Domain`` instance being modified
|
||||
|
||||
ExtDisabled
|
||||
===========
|
||||
@@ -247,6 +286,18 @@ Raised just after an extension has been activated.
|
||||
|
||||
* ``extension`` is an ``Extension`` instance
|
||||
|
||||
ExtraAccountActions
|
||||
===================
|
||||
|
||||
Raised when the account list is displayed. Let developers define new
|
||||
actions to act on a specific user.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(account): pass
|
||||
|
||||
* ``account`` is the account being listed
|
||||
|
||||
ExtraAccountForm
|
||||
================
|
||||
|
||||
@@ -289,6 +340,37 @@ Let extensions add extra content into the admin panel.
|
||||
|
||||
Callbacks listening to this event must return a list of string.
|
||||
|
||||
ExtraDomainEntries
|
||||
==================
|
||||
|
||||
Raised to request extra entries to display inside the *domains*
|
||||
listing.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user, domfilter, searchquery, **extrafilters): pass
|
||||
|
||||
* ``user`` is the ``User`` instance corresponding to the currently
|
||||
logged in user
|
||||
* ``domfilter`` is a string indicating which domain type the user needs
|
||||
* ``searchquery`` is a string containing a search query
|
||||
* ``extrafilters`` is a set of keyword arguments that may contain additional filters
|
||||
|
||||
Must return a valid ``QuerySet``.
|
||||
|
||||
ExtraDomainFilters
|
||||
==================
|
||||
|
||||
Raised to request extra filters for the *domains* listing page. For
|
||||
example, the *postfix_relay_domains* extension let users filter
|
||||
entries based on service types.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(): pass
|
||||
|
||||
Must return a list of valid filter names (string).
|
||||
|
||||
ExtraDomainForm
|
||||
===============
|
||||
|
||||
@@ -304,6 +386,78 @@ tabs).
|
||||
|
||||
* ``domain`` is the domain beeing modified (``Domain`` instance)
|
||||
|
||||
Callbacks listening to the event must return a list of dictionnaries,
|
||||
each one must contain at least three keys::
|
||||
|
||||
{"id" : "<the form's id>",
|
||||
"title" : "<the title used to present the form>",
|
||||
"cls" : TheFormClassName}
|
||||
|
||||
ExtraDomainImportHelp
|
||||
=====================
|
||||
|
||||
Raised to request extra help text to display inside the domain import
|
||||
form.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(): pass
|
||||
|
||||
Must return a list a string.
|
||||
|
||||
ExtraDomainMenuEntries
|
||||
======================
|
||||
|
||||
Raised to request extra entries to include in the left menu of the
|
||||
*domains* listing page.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user): pass
|
||||
|
||||
* ``user`` is the ``User`` instance corresponding to the currently
|
||||
logged in user
|
||||
|
||||
Must return a list of dictionaries. Each dictionary must contain at
|
||||
least three keys::
|
||||
|
||||
{"name": "<menu name>",
|
||||
"label": "<menu label>",
|
||||
"url": "<menu url>"}
|
||||
|
||||
.. _extraformfields:
|
||||
|
||||
ExtraFormFields
|
||||
===============
|
||||
|
||||
Raised to request extra fields to include in a django form.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(form_name, instance=None): pass
|
||||
|
||||
* ``form_name`` is a string used to distinguish a specific form
|
||||
* ``instance`` is a django model instance related to ``form_name``
|
||||
|
||||
Must return a list of 2uple, each one containing the following
|
||||
information::
|
||||
|
||||
('field name', <Django form field instance>)
|
||||
|
||||
ExtraRelayDomainForm
|
||||
====================
|
||||
|
||||
Let extensions add new forms to the relay domain edition form (the one
|
||||
with tabs).
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user, rdomain): pass
|
||||
|
||||
* ``user`` is the ``User`` instance corresponding to the currently
|
||||
logged in user
|
||||
* ``rdomain`` is the relay domain being modified (``RelayDomain`` instance)
|
||||
|
||||
Callbacks listening to the event must return a list of dictionnaries,
|
||||
each one must contain at least three keys::
|
||||
|
||||
@@ -344,6 +498,24 @@ forms.
|
||||
|
||||
* ``domain`` is the ``Domain`` instance beeing modified
|
||||
|
||||
* ``instances`` is a dictionnary where the callback will add
|
||||
information needed to fill a specific form
|
||||
|
||||
FillRelayDomainInstances
|
||||
========================
|
||||
|
||||
When a relay domain is being modified, this event is raised to fill extra
|
||||
forms.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user, rdomain, instances): pass
|
||||
|
||||
* ``user`` is the ``User`` instance corresponding to the currently
|
||||
logged in user
|
||||
|
||||
* ``rdomain`` is the ``RelayDomain`` instance being modified
|
||||
|
||||
* ``instances`` is a dictionnary where the callback will add
|
||||
information needed to fill a specific form
|
||||
|
||||
@@ -364,6 +536,73 @@ Some places in the interface let plugins add their own announcement
|
||||
|
||||
Callbacks listening to this event must return a list of string.
|
||||
|
||||
GetDomainActions
|
||||
================
|
||||
|
||||
Raised to request the list of actions available for the *domains*
|
||||
listing entry being displayed.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user, rdomain): pass
|
||||
|
||||
* ``user`` is the ``User`` instance corresponding to the currently
|
||||
logged in user
|
||||
* ``rdomain`` is the ``RelayDomain`` instance being displayed
|
||||
|
||||
Must return a list of dictionaries, each dictionary containing at
|
||||
least the following entries::
|
||||
|
||||
{"name": "<action name>",
|
||||
"url": "<action url>",
|
||||
"title": "<action title>",
|
||||
"img": "<action icon>"}
|
||||
|
||||
GetDomainModifyLink
|
||||
===================
|
||||
|
||||
Raised to request the modification url of the *domains* listing entry
|
||||
being displayed.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(domain): pass
|
||||
|
||||
* ``domain`` is a model instance (``RelayDomain`` for example)
|
||||
|
||||
Must return a dictionary containing at least the following entry::
|
||||
|
||||
{'url': '<modification url>'}
|
||||
|
||||
GetExtraLimitTemplates
|
||||
======================
|
||||
|
||||
Raised to request extra limit templates. For example, the
|
||||
*postfix_relay_domains* extension define a template to limit the
|
||||
number of relay domains an administrator can create.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(): pass
|
||||
|
||||
Must return a list of set. Each set must contain at least three entries::
|
||||
|
||||
[('<limit_name>', '<limit label>', '<limit help text>')]
|
||||
|
||||
GetExtraParameters
|
||||
==================
|
||||
|
||||
Raised to request extra parameters for a given parameters form.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(application, level): pass
|
||||
|
||||
* ``application`` is the name of the form's application (ie. admin, amavis, etc.)
|
||||
* ``level`` is the form's level: ``A`` for admin or ``U`` for user
|
||||
|
||||
Must return a dictionary. Each entry must be a valid Django form field.
|
||||
|
||||
.. _getextraroles:
|
||||
|
||||
GetExtraRoles
|
||||
@@ -397,6 +636,27 @@ template but need javascript stuff.
|
||||
|
||||
Callbacks listening to this event must return a list of string.
|
||||
|
||||
ImportObject
|
||||
============
|
||||
|
||||
Raised to request the function handling an object being imported from CSV.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(objtype): pass
|
||||
|
||||
``objtype`` is the type of object being imported
|
||||
|
||||
Must return a list of function. A valid import function must respect
|
||||
the following prototype::
|
||||
|
||||
def import_function(user, row, formopts): pass
|
||||
|
||||
* ``user`` is the ``User`` instance corresponding to the currently
|
||||
logged in user
|
||||
* ``row`` is a string containing the object's definition (CSV format)
|
||||
* ``formopts`` is a dictionary that may contain options
|
||||
|
||||
MailboxAliasCreated
|
||||
===================
|
||||
|
||||
@@ -420,8 +680,31 @@ Raised when an existing mailbox alias is about to be deleted.
|
||||
|
||||
* ``mailbox_alias`` is an ``Alias`` instance
|
||||
|
||||
ModifyMailbox
|
||||
=============
|
||||
MailboxCreated
|
||||
==============
|
||||
|
||||
Raised when a new mailbox is created.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user, mailbox): pass
|
||||
|
||||
* ``user`` is the new mailbox's owner (``User`` instance)
|
||||
* ``mailbox`` is the new mailbox (``Mailbox`` instance)
|
||||
|
||||
MailboxDeleted
|
||||
==============
|
||||
|
||||
Raised when an existing mailbox is about to be deleted.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(mailbox): pass
|
||||
|
||||
* ``mailbox`` is a ``Mailbox`` instance
|
||||
|
||||
MailboxModified
|
||||
===============
|
||||
|
||||
Raised when an existing mailbox is modified.
|
||||
|
||||
@@ -506,3 +789,103 @@ contain at least the following keys::
|
||||
{"name" : "a_name_without_spaces",
|
||||
"label" : _("The menu label"),
|
||||
"url" : reverse("your_view")}
|
||||
|
||||
RelayDomainAliasCreated
|
||||
=======================
|
||||
|
||||
Raised when a new relay domain alias is created.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user, rdomain_alias): pass
|
||||
|
||||
* ``user`` is the new relay domain alias owner (``User`` instance)
|
||||
* ``rdomain_alias`` is the new relay domain alias (``DomainAlias`` instance)
|
||||
|
||||
RelayDomainAliasDeleted
|
||||
=======================
|
||||
|
||||
Raised when an existing relay domain alias is about to be deleted.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(rdomain_alias): pass
|
||||
|
||||
* ``rdomain_alias`` is a ``RelayDomainAlias`` instance
|
||||
|
||||
RelayDomainCreated
|
||||
==================
|
||||
|
||||
Raised when a new relay domain is created.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(user, rdomain): pass
|
||||
|
||||
* ``user`` corresponds to the ``User`` object creating the relay domain (its owner)
|
||||
* ``rdomain`` is a ``RelayDomain`` instance
|
||||
|
||||
RelayDomainDeleted
|
||||
==================
|
||||
|
||||
Raised when an existing relay domain is about to be deleted.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(rdomain): pass
|
||||
|
||||
* ``rdomain`` is a ``RelayDomain`` instance
|
||||
|
||||
RelayDomainModified
|
||||
===================
|
||||
|
||||
Raised when a relay domain has been modified.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(rdomain): pass
|
||||
|
||||
* ``rdomain`` is the modified ``RelayDomain`` instance, it contains an
|
||||
extra ``oldname`` field which contains the old name
|
||||
|
||||
RoleChanged
|
||||
===========
|
||||
|
||||
Raised when the role of an account is about to be changed.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(account, role): pass
|
||||
|
||||
* ``account`` is the account being modified
|
||||
* ``role`` is the new role (string)
|
||||
|
||||
SaveExtraFormFields
|
||||
===================
|
||||
|
||||
Raised to save extra fields declared using :ref:`extraformfields`.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(form_name, instance, values): pass
|
||||
|
||||
* ``form_name`` is a string used to distinguish a specific form
|
||||
* ``instance`` is a django model instance related to ``form_name``
|
||||
* ``values`` is a dictionary containing the form's values
|
||||
|
||||
UserCanSetRole
|
||||
==============
|
||||
|
||||
Raised to check if a user is allowed to set a given role to an
|
||||
account.
|
||||
|
||||
*Callback prototype*::
|
||||
|
||||
def callback(account, role): pass
|
||||
|
||||
* ``user`` is the ``User`` instance corresponding to the currently
|
||||
logged in user
|
||||
* ``role`` is the role ``user`` tries to set
|
||||
|
||||
Must return a list containing ``True`` or ``False`` to indicate if
|
||||
this user can is allowed to set ``role``.
|
||||
|
||||
@@ -32,6 +32,20 @@ associated value to -1.
|
||||
Default limits applied to new administrators can be changed through
|
||||
the *Modoboa > Parameters > Limits* page.
|
||||
|
||||
*****************************
|
||||
Postfix relay domains support
|
||||
*****************************
|
||||
|
||||
This plugin adds the support for relay domains using postfix. This
|
||||
feature is useful when the MTA managed by Modoboa is not the final
|
||||
destination of one or many domains.
|
||||
|
||||
If activated, two new objects will be available from the *domains*
|
||||
listing page: *relay domain* and *relay domain alias*.
|
||||
|
||||
This extension is compatible with the *amavis* and *limits*
|
||||
ones. Resellers will be able to create both new objects.
|
||||
|
||||
.. _amavis_frontend:
|
||||
|
||||
********************
|
||||
|
||||
@@ -53,6 +53,8 @@ Finally, follow the common procedure::
|
||||
1.1.0: FIXME
|
||||
============
|
||||
|
||||
#. Add ``'modoboa.extensions.postfix_relay_domains'`` to ``MODOBOA_APPS``
|
||||
|
||||
Due to code refactoring, some modifications need to be done into
|
||||
:file:`settings.py`:
|
||||
|
||||
|
||||
@@ -138,11 +138,11 @@ MySQL users
|
||||
|
||||
default_pass_scheme = CRYPT
|
||||
|
||||
password_query = SELECT email AS user, password FROM admin_user WHERE email='%u' and is_active=1
|
||||
password_query = SELECT email AS user, password FROM core_user WHERE email='%u' and is_active=1
|
||||
|
||||
user_query = SELECT '<mailboxes storage directory>/%d/%n' AS home, <uid> as uid, <gid> as gid, concat('*:bytes=', mb.quota, 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%n' AND dom.name='%d'
|
||||
|
||||
iterate_query = SELECT email AS username FROM admin_user
|
||||
iterate_query = SELECT email AS username FROM core_user
|
||||
|
||||
.. _dovecot_pg_queries:
|
||||
|
||||
@@ -157,11 +157,11 @@ PostgreSQL users
|
||||
|
||||
default_pass_scheme = CRYPT
|
||||
|
||||
password_query = SELECT email AS user, password FROM admin_user WHERE email='%u' and is_active
|
||||
password_query = SELECT email AS user, password FROM core_user WHERE email='%u' and is_active
|
||||
|
||||
user_query = SELECT '<mailboxes storage directory>/%d/%n' AS home, <uid> as uid, <gid> as gid, '*:bytes=' || mb.quota || 'M' AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%n' AND dom.name='%d'
|
||||
|
||||
iterate_query = SELECT email AS username FROM admin_user
|
||||
iterate_query = SELECT email AS username FROM core_user
|
||||
|
||||
SQLite users
|
||||
------------
|
||||
@@ -174,11 +174,11 @@ SQLite users
|
||||
|
||||
default_pass_scheme = CRYPT
|
||||
|
||||
password_query = SELECT email AS user, password FROM admin_user WHERE email='%u' and is_active=1
|
||||
password_query = SELECT email AS user, password FROM core_user WHERE email='%u' and is_active=1
|
||||
|
||||
user_query = SELECT '<mailboxes storage directory>/%d/%n' AS home, <uid> as uid, <gid> as gid, ('*:bytes=' || mb.quota || 'M') AS quota_rule FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.address='%n' AND dom.name='%d'
|
||||
|
||||
iterate_query = SELECT email AS username FROM admin_user
|
||||
iterate_query = SELECT email AS username FROM core_user
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -325,7 +325,7 @@ work for Modoboa. You should use the following one instead:
|
||||
DECLARE
|
||||
mboxid INTEGER;
|
||||
BEGIN
|
||||
SELECT admin_mailbox.id INTO STRICT mboxid FROM admin_mailbox INNER JOIN admin_user ON admin_mailbox.user_id=admin_user.id WHERE admin_user.username=NEW.username;
|
||||
SELECT admin_mailbox.id INTO STRICT mboxid FROM admin_mailbox INNER JOIN core_user ON admin_mailbox.user_id=core_user.id WHERE core_user.username=NEW.username;
|
||||
UPDATE admin_quota SET mbox_id = mboxid
|
||||
WHERE username = NEW.username;
|
||||
RETURN NULL;
|
||||
|
||||
@@ -15,6 +15,7 @@ base_events = [
|
||||
"RoleChanged",
|
||||
"GetExtraRoles",
|
||||
"PasswordChange",
|
||||
"UserCanSetRole",
|
||||
|
||||
"UserMenuDisplay",
|
||||
"AdminMenuDisplay",
|
||||
@@ -32,12 +33,21 @@ base_events = [
|
||||
"ExtraAdminContent",
|
||||
|
||||
"ExtraUprefsRoutes",
|
||||
"ExtraUprefsJS"
|
||||
"ExtraUprefsJS",
|
||||
|
||||
"GetExtraParameters",
|
||||
"ExtraFormFields",
|
||||
"SaveExtraFormFields"
|
||||
]
|
||||
|
||||
|
||||
def load_settings():
|
||||
from .app_settings import GeneralParametersForm, UserSettings
|
||||
def load_core_settings():
|
||||
"""Load core settings.
|
||||
|
||||
This function must be manually called (see :file:`urls.py`) in
|
||||
order to load base settings.
|
||||
"""
|
||||
from modoboa.core.app_settings import GeneralParametersForm, UserSettings
|
||||
|
||||
parameters.register(GeneralParametersForm, ugettext_lazy("General"))
|
||||
parameters.register(UserSettings, ugettext_lazy("General"))
|
||||
|
||||
@@ -46,15 +46,26 @@ class GeneralParametersForm(parameters.AdminParametersForm):
|
||||
|
||||
password_scheme = forms.ChoiceField(
|
||||
label=ugettext_lazy("Default password scheme"),
|
||||
choices=[("crypt", "crypt"),
|
||||
("md5", "md5"),
|
||||
("md5crypt", "md5crypt"),
|
||||
("sha256", "sha256"),
|
||||
("plain", "plain")],
|
||||
initial="md5crypt",
|
||||
choices=[("sha512crypt", "sha512crypt"),
|
||||
("sha256crypt", "sha256crypt"),
|
||||
("md5crypt", ugettext_lazy("md5crypt (weak)")),
|
||||
("sha256", ugettext_lazy("sha256 (weak)")),
|
||||
("md5", ugettext_lazy("md5 (weak)")),
|
||||
("crypt", ugettext_lazy("crypt (weak)")),
|
||||
("plain", ugettext_lazy("plain (weak)"))],
|
||||
initial="sha512crypt",
|
||||
help_text=ugettext_lazy("Scheme used to crypt mailbox passwords")
|
||||
)
|
||||
|
||||
rounds_number = forms.IntegerField(
|
||||
label=ugettext_lazy("Rounds"),
|
||||
initial=70000,
|
||||
help_text=ugettext_lazy(
|
||||
"Number of rounds to use (only used by sha256crypt and "
|
||||
"sha512crypt). Must be between 1000 and 999999999, inclusive."
|
||||
)
|
||||
)
|
||||
|
||||
secret_key = forms.CharField(
|
||||
label=ugettext_lazy("Secret key"),
|
||||
initial=random_key(),
|
||||
@@ -107,9 +118,9 @@ class GeneralParametersForm(parameters.AdminParametersForm):
|
||||
)
|
||||
|
||||
ldap_search_base = forms.CharField(
|
||||
label=ugettext_lazy("Search base"),
|
||||
label=ugettext_lazy("Users search base"),
|
||||
initial="",
|
||||
help_text=ugettext_lazy("The distinguished name of the search base"),
|
||||
help_text=ugettext_lazy("The distinguished name of the search base used to find users"),
|
||||
required=False
|
||||
)
|
||||
|
||||
@@ -139,6 +150,25 @@ class GeneralParametersForm(parameters.AdminParametersForm):
|
||||
help_text=ugettext_lazy("Tell if the LDAP server is an Active Directory one")
|
||||
)
|
||||
|
||||
ldap_admin_groups = forms.CharField(
|
||||
label=ugettext_lazy("Administrator groups"),
|
||||
initial="",
|
||||
help_text=ugettext_lazy(
|
||||
"Members of those LDAP Posix groups will be created as domain "
|
||||
"administrators. Use ';' characters to separate groups."
|
||||
),
|
||||
required=False
|
||||
)
|
||||
|
||||
ldap_groups_search_base = forms.CharField(
|
||||
label=ugettext_lazy("Groups search base"),
|
||||
initial="",
|
||||
help_text=ugettext_lazy(
|
||||
"The distinguished name of the search base used to find groups"
|
||||
),
|
||||
required=False
|
||||
)
|
||||
|
||||
sep3 = SeparatorField(label=ugettext_lazy("Miscellaneous"))
|
||||
|
||||
log_maximum_age = forms.IntegerField(
|
||||
@@ -173,7 +203,9 @@ class GeneralParametersForm(parameters.AdminParametersForm):
|
||||
"ldap_search_filter": "ldap_auth_method=searchbind",
|
||||
"ldap_user_dn_template": "ldap_auth_method=directbind",
|
||||
"ldap_password_attribute": "authentication_type=ldap",
|
||||
"ldap_is_active_directory": "authentication_type=ldap"
|
||||
"ldap_is_active_directory": "authentication_type=ldap",
|
||||
"ldap_admin_groups": "authentication_type=ldap",
|
||||
"ldap_groups_search_base": "authentication_type=ldap",
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -191,10 +223,16 @@ class GeneralParametersForm(parameters.AdminParametersForm):
|
||||
tpl = self.cleaned_data["ldap_user_dn_template"]
|
||||
try:
|
||||
test = tpl % {"user": "toto"}
|
||||
except ValueError:
|
||||
except (KeyError, ValueError):
|
||||
raise forms.ValidationError(_("Invalid syntax"))
|
||||
return tpl
|
||||
|
||||
def clean_rounds_number(self):
|
||||
value = self.cleaned_data["rounds_number"]
|
||||
if value < 1000 or value > 999999999:
|
||||
raise forms.ValidationError(_("Invalid rounds number"))
|
||||
return value
|
||||
|
||||
def clean(self):
|
||||
"""Custom validation method
|
||||
|
||||
@@ -226,7 +264,7 @@ class GeneralParametersForm(parameters.AdminParametersForm):
|
||||
"""
|
||||
try:
|
||||
import ldap
|
||||
from django_auth_ldap.config import LDAPSearch
|
||||
from django_auth_ldap.config import LDAPSearch, PosixGroupType
|
||||
ldap_available = True
|
||||
except ImportError:
|
||||
ldap_available = False
|
||||
@@ -243,6 +281,11 @@ class GeneralParametersForm(parameters.AdminParametersForm):
|
||||
ldap_uri = 'ldaps://' if values["ldap_secured"] == "yes" else "ldap://"
|
||||
ldap_uri += "%s:%s" % (values["ldap_server_address"], values["ldap_server_port"])
|
||||
setattr(settings, "AUTH_LDAP_SERVER_URI", ldap_uri)
|
||||
setattr(settings, "AUTH_LDAP_GROUP_TYPE", PosixGroupType())
|
||||
setattr(settings, "AUTH_LDAP_GROUP_SEARCH", LDAPSearch(
|
||||
values["ldap_groups_search_base"], ldap.SCOPE_SUBTREE,
|
||||
"(objectClass=posixGroup)"
|
||||
))
|
||||
if values["ldap_auth_method"] == "searchbind":
|
||||
setattr(settings, "AUTH_LDAP_BIND_DN", values["ldap_bind_dn"])
|
||||
setattr(settings, "AUTH_LDAP_BIND_PASSWORD", values["ldap_bind_password"])
|
||||
@@ -253,7 +296,6 @@ class GeneralParametersForm(parameters.AdminParametersForm):
|
||||
setattr(settings, "AUTH_LDAP_USER_SEARCH", search)
|
||||
else:
|
||||
setattr(settings, "AUTH_LDAP_USER_DN_TEMPLATE", values["ldap_user_dn_template"])
|
||||
|
||||
if values["ldap_is_active_directory"] == "yes":
|
||||
if not hasattr(settings, "AUTH_LDAP_GLOBAL_OPTIONS"):
|
||||
setattr(settings, "AUTH_LDAP_GLOBAL_OPTIONS", {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import getpass
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import datetime
|
||||
from django.template import Context, Template
|
||||
from . import Command
|
||||
|
||||
@@ -46,6 +48,12 @@ query = {{ query|safe }}
|
||||
query=getattr(v, args.dbtype)))
|
||||
)
|
||||
with open("%s/%s" % (args.destdir, v.filename), "w") as fp:
|
||||
print >> fp, """# This file was generated on %s by running:
|
||||
# %s %s
|
||||
# DO NOT EDIT!
|
||||
""" % (datetime.datetime.now().isoformat(),
|
||||
os.path.basename(sys.argv[0]),
|
||||
' '.join(sys.argv[1:]))
|
||||
print >> fp, content
|
||||
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@ TEMPLATE_LOADERS = (
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.transaction.TransactionMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
@@ -154,6 +155,7 @@ MODOBOA_APPS = (
|
||||
'modoboa.lib',
|
||||
# Modoboa extensions here.
|
||||
'modoboa.extensions.admin',
|
||||
'modoboa.extensions.postfix_relay_domains',
|
||||
'modoboa.extensions.limits',
|
||||
'modoboa.extensions.postfix_autoreply',
|
||||
'modoboa.extensions.webmail',
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
"""
|
||||
:mod:`exceptions` --- Custom exceptions of the admin. application
|
||||
-----------------------------------------------------------------
|
||||
"""
|
||||
|
||||
from modoboa.lib.exceptions import ModoboaException
|
||||
|
||||
|
||||
class AdminError(ModoboaException):
|
||||
"""
|
||||
Just a Custom exception to identify errors coming from the admin
|
||||
application.
|
||||
"""
|
||||
pass
|
||||
+33
-11
@@ -38,7 +38,7 @@ class ExtensionsPool(object):
|
||||
"""The extensions manager"""
|
||||
|
||||
def __init__(self):
|
||||
self.extensions = dict()
|
||||
self.extensions = {}
|
||||
|
||||
def register_extension(self, ext, show=True):
|
||||
self.extensions[ext.name] = dict(cls=ext, show=show)
|
||||
@@ -50,6 +50,13 @@ class ExtensionsPool(object):
|
||||
self.extensions[name]["instance"] = self.extensions[name]["cls"]()
|
||||
return self.extensions[name]["instance"]
|
||||
|
||||
def is_extension_enabled(self, name):
|
||||
from modoboa.core.models import Extension
|
||||
|
||||
if not name in self.extensions:
|
||||
return False
|
||||
return Extension.objects.get(name=name).enabled
|
||||
|
||||
def get_extension_infos(self, name):
|
||||
instance = self.get_extension(name)
|
||||
if instance is None:
|
||||
@@ -57,13 +64,33 @@ class ExtensionsPool(object):
|
||||
return instance.infos()
|
||||
|
||||
def load_all(self):
|
||||
"""Load all enabled extensions.
|
||||
|
||||
Each extension must be loaded in order to integrate with
|
||||
Modoboa. Only enabled and special extensions are loaded but
|
||||
urls are always returned. The reason is urls are imported only
|
||||
once so must know all of them when the python process
|
||||
starts. Otherwise, it would lead to unexpected 404 errors :p
|
||||
|
||||
:return: a list of url maps
|
||||
"""
|
||||
from modoboa.core.models import Extension
|
||||
|
||||
result = []
|
||||
for ext in settings.MODOBOA_APPS:
|
||||
__import__(ext)
|
||||
result = []
|
||||
for extname in self.extensions.keys():
|
||||
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("%s.urls" % extinstance.__module__))]
|
||||
except ImportError:
|
||||
# No urls for this extension
|
||||
pass
|
||||
if not extinstance.always_active:
|
||||
try:
|
||||
ext = Extension.objects.get(name=extname)
|
||||
@@ -72,21 +99,16 @@ class ExtensionsPool(object):
|
||||
except Extension.DoesNotExist:
|
||||
continue
|
||||
extinstance.load()
|
||||
try:
|
||||
baseurl = extinstance.url if extinstance.url is not None else extname
|
||||
result += [(r'^%s/' % (baseurl),
|
||||
include("%s.urls" % extinstance.__module__))]
|
||||
except ImportError:
|
||||
# No urls for this extension
|
||||
pass
|
||||
return result
|
||||
|
||||
def list_all(self):
|
||||
"""List all defined extensions.
|
||||
"""
|
||||
result = []
|
||||
for extname, extdef in self.extensions.iteritems():
|
||||
if not extdef["show"]:
|
||||
continue
|
||||
infos = extdef["instance"].infos()
|
||||
infos = self.get_extension_infos(extname)
|
||||
infos["id"] = extname
|
||||
result += [infos]
|
||||
return sorted(result, key=lambda i: i["name"])
|
||||
|
||||
@@ -1 +1 @@
|
||||
[{"pk": 1, "model": "core.user", "fields": {"username": "admin", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": false, "last_login": "2012-01-26 15:19:25", "groups": [], "user_permissions": [], "password": "{CRYPT}dTTsGDkA5ZHKg", "email": "", "date_joined": "2012-01-26 15:19:25"}},{"pk": 1, "model": "core.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["core", "user"], "object_id": 1}}]
|
||||
[{"pk": 1, "model": "core.user", "fields": {"username": "admin", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": false, "last_login": "2012-01-26T15:19:26+01:00", "groups": [], "user_permissions": [], "password": "{CRYPT}dTTsGDkA5ZHKg", "email": "", "date_joined": "2012-01-26T15:19:26+01:00"}},{"pk": 1, "model": "core.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["core", "user"], "object_id": 1}}]
|
||||
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import DataMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(DataMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
"Write your forwards methods here."
|
||||
for user in orm['core.User'].objects.all():
|
||||
if user.password.startswith('$1$'):
|
||||
user.password = '{MD5-CRYPT}%s' % user.password
|
||||
user.save()
|
||||
|
||||
def backwards(self, orm):
|
||||
"Write your backwards methods here."
|
||||
for user in orm['core.User'].objects.all():
|
||||
if user.password.startswith('{MD5-CRYPT}'):
|
||||
user.password = user.password.replace('{MD5-CRYPT}', '')
|
||||
user.save()
|
||||
|
||||
models = {
|
||||
u'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
u'auth.permission': {
|
||||
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
u'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
u'core.extension': {
|
||||
'Meta': {'object_name': 'Extension'},
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '150'})
|
||||
},
|
||||
u'core.log': {
|
||||
'Meta': {'object_name': 'Log'},
|
||||
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'level': ('django.db.models.fields.CharField', [], {'max_length': '15'}),
|
||||
'logger': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
|
||||
'message': ('django.db.models.fields.CharField', [], {'max_length': '255'})
|
||||
},
|
||||
u'core.objectaccess': {
|
||||
'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"})
|
||||
},
|
||||
u'core.user': {
|
||||
'Meta': {'ordering': "['username']", 'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_local': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['core']
|
||||
symmetrical = True
|
||||
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Changing field 'User.password'
|
||||
db.alter_column(u'core_user', 'password', self.gf('django.db.models.fields.CharField')(max_length=256))
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Changing field 'User.password'
|
||||
db.alter_column(u'core_user', 'password', self.gf('django.db.models.fields.CharField')(max_length=128))
|
||||
|
||||
models = {
|
||||
u'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
u'auth.permission': {
|
||||
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
u'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
u'core.extension': {
|
||||
'Meta': {'object_name': 'Extension'},
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '150'})
|
||||
},
|
||||
u'core.log': {
|
||||
'Meta': {'object_name': 'Log'},
|
||||
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'level': ('django.db.models.fields.CharField', [], {'max_length': '15'}),
|
||||
'logger': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
|
||||
'message': ('django.db.models.fields.CharField', [], {'max_length': '255'})
|
||||
},
|
||||
u'core.objectaccess': {
|
||||
'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"})
|
||||
},
|
||||
u'core.user': {
|
||||
'Meta': {'ordering': "['username']", 'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_local': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['core']
|
||||
+109
-84
@@ -1,35 +1,35 @@
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
import hashlib
|
||||
import crypt
|
||||
import base64
|
||||
from random import Random
|
||||
import logging
|
||||
import reversion
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.utils.crypto import constant_time_compare
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes import generic
|
||||
from django.contrib.auth.models import (
|
||||
UserManager, Group, AbstractBaseUser, PermissionsMixin
|
||||
UserManager, Group, PermissionsMixin
|
||||
)
|
||||
from django.contrib.auth.hashers import make_password, is_password_usable
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.lib.exceptions import (
|
||||
PermDeniedException, InternalError, BadRequest
|
||||
)
|
||||
from modoboa.lib import events, md5crypt, parameters
|
||||
from modoboa.lib.exceptions import PermDeniedException
|
||||
from modoboa.lib.sysutils import exec_cmd
|
||||
from modoboa.core.extensions import exts_pool
|
||||
from modoboa.core.exceptions import AdminError
|
||||
from modoboa.core.password_hashers import get_password_hasher
|
||||
|
||||
try:
|
||||
from modoboa.lib.ldaputils import *
|
||||
from modoboa.lib.ldaputils import LDAPAuthBackend
|
||||
ldap_available = True
|
||||
except ImportError:
|
||||
ldap_available = False
|
||||
|
||||
|
||||
class User(AbstractBaseUser, PermissionsMixin):
|
||||
class User(PermissionsMixin):
|
||||
"""Custom User model.
|
||||
|
||||
It overloads the way passwords are stored into the database. The
|
||||
@@ -46,6 +46,10 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
is_active = models.BooleanField(default=True)
|
||||
date_joined = models.DateTimeField(default=timezone.now)
|
||||
is_local = models.BooleanField(default=True)
|
||||
password = models.CharField(ugettext_lazy('password'), max_length=256)
|
||||
last_login = models.DateTimeField(
|
||||
ugettext_lazy('last login'), default=timezone.now
|
||||
)
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
@@ -55,7 +59,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
class Meta:
|
||||
ordering = ["username"]
|
||||
|
||||
password_expr = re.compile(r'(\{(\w+)\}|(\$1\$))(.+)')
|
||||
password_expr = re.compile(r'\{([\w\-]+)\}(.+)')
|
||||
|
||||
def delete(self, fromuser, *args, **kwargs):
|
||||
"""Custom delete method
|
||||
@@ -72,7 +76,9 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
get_object_owner, grant_access_to_object, ungrant_access_to_object
|
||||
|
||||
if fromuser == self:
|
||||
raise AdminError(_("You can't delete your own account"))
|
||||
raise PermDeniedException(
|
||||
_("You can't delete your own account")
|
||||
)
|
||||
|
||||
if not fromuser.can_access(self):
|
||||
raise PermDeniedException
|
||||
@@ -90,34 +96,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
scheme = parameters.get_admin("PASSWORD_SCHEME")
|
||||
if type(raw_value) is unicode:
|
||||
raw_value = raw_value.encode("utf-8")
|
||||
if scheme == "crypt":
|
||||
salt = "".join(Random().sample(string.letters + string.digits, 2))
|
||||
result = crypt.crypt(raw_value, salt)
|
||||
prefix = "{CRYPT}"
|
||||
elif scheme == "md5":
|
||||
obj = hashlib.md5(raw_value)
|
||||
result = obj.hexdigest()
|
||||
prefix = "{MD5}"
|
||||
# The md5crypt scheme is the only supported method that has both:
|
||||
# (a) a salt ("crypt" has this too),
|
||||
# (b) supports passwords lengths of more than 8 characters (all except
|
||||
# "crypt").
|
||||
elif scheme == "md5crypt":
|
||||
# The salt may vary from 12 to 48 bits. (Using all six bytes here
|
||||
# with a subset of characters means we get only 35 random bits.)
|
||||
salt = "".join(Random().sample(string.letters + string.digits, 6))
|
||||
result = md5crypt(raw_value, salt)
|
||||
prefix = "" # the result already has $1$ prepended to it
|
||||
# to signify what this is
|
||||
elif scheme == "sha256":
|
||||
obj = hashlib.sha256(raw_value)
|
||||
result = base64.b64encode(obj.digest())
|
||||
prefix = "{SHA256}"
|
||||
else:
|
||||
scheme = "plain"
|
||||
result = raw_value
|
||||
prefix = "{PLAIN}"
|
||||
return "%s%s" % (prefix, result)
|
||||
return get_password_hasher(scheme.upper())().encrypt(raw_value)
|
||||
|
||||
def set_password(self, raw_value, curvalue=None):
|
||||
"""Password update
|
||||
@@ -129,44 +108,61 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
:param raw_value: the new password's value
|
||||
:param curvalue: the current password (for LDAP authentication)
|
||||
"""
|
||||
if parameters.get_admin("AUTHENTICATION_TYPE") == "local":
|
||||
if self.is_local:
|
||||
self.password = self._crypt_password(raw_value)
|
||||
else:
|
||||
if not ldap_available:
|
||||
raise AdminError(
|
||||
raise InternalError(
|
||||
_("Failed to update password: LDAP module not installed")
|
||||
)
|
||||
|
||||
ab = LDAPAuthBackend()
|
||||
try:
|
||||
ab.update_user_password(self.username, curvalue, raw_value)
|
||||
except LDAPException, e:
|
||||
raise AdminError(_("Failed to update password: %s" % str(e)))
|
||||
LDAPAuthBackend().update_user_password(
|
||||
self.username, curvalue, raw_value
|
||||
)
|
||||
events.raiseEvent(
|
||||
"PasswordUpdated", self, raw_value, self.pk is None
|
||||
)
|
||||
|
||||
def check_password(self, raw_value):
|
||||
m = self.password_expr.match(self.password)
|
||||
if m is None:
|
||||
match = self.password_expr.match(self.password)
|
||||
if match is None:
|
||||
return False
|
||||
if type(raw_value) is unicode:
|
||||
raw_value = raw_value.encode("utf-8")
|
||||
scheme = (m.group(2) or m.group(3)).lower()
|
||||
val2 = m.group(4)
|
||||
if scheme == u"crypt":
|
||||
val1 = crypt.crypt(raw_value, val2)
|
||||
elif scheme == u"md5":
|
||||
val1 = hashlib.md5(raw_value).hexdigest()
|
||||
elif scheme == u"sha256":
|
||||
val1 = base64.b64encode(hashlib.sha256(raw_value).digest())
|
||||
elif scheme == u"$1$": # md5crypt
|
||||
salt, hashed = val2.split('$')
|
||||
val1 = md5crypt(raw_value, str(salt))
|
||||
val2 = self.password # re-add scheme for comparison below
|
||||
else:
|
||||
val1 = raw_value
|
||||
return constant_time_compare(val1, val2)
|
||||
scheme = match.group(1)
|
||||
val2 = match.group(2)
|
||||
hasher = get_password_hasher(scheme)
|
||||
return hasher().verify(raw_value, val2)
|
||||
|
||||
def get_username(self):
|
||||
"Return the identifying username for this User"
|
||||
return getattr(self, self.USERNAME_FIELD)
|
||||
|
||||
def __str__(self):
|
||||
return self.get_username()
|
||||
|
||||
def natural_key(self):
|
||||
return (self.get_username(),)
|
||||
|
||||
def is_anonymous(self):
|
||||
"""
|
||||
Always returns False. This is a way of comparing User objects to
|
||||
anonymous users.
|
||||
"""
|
||||
return False
|
||||
|
||||
def is_authenticated(self):
|
||||
"""
|
||||
Always return True. This is a way to tell if the user has been
|
||||
authenticated in templates.
|
||||
"""
|
||||
return True
|
||||
|
||||
def set_unusable_password(self):
|
||||
# Sets a value that will never be a valid hash
|
||||
self.password = make_password(None)
|
||||
|
||||
def has_usable_password(self):
|
||||
return is_password_usable(self.password)
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
@@ -197,7 +193,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
try:
|
||||
return self.groups.all()[0].name
|
||||
except IndexError:
|
||||
return "SimpleUsers"
|
||||
return "---"
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
@@ -240,9 +236,10 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
def can_access(self, obj):
|
||||
"""Check if the user can access a specific object
|
||||
|
||||
This function is recursive : if the given user hasn't got direct
|
||||
access to this object and if he has got access other ``User``
|
||||
objects, we check if one of those users owns the object.
|
||||
This function is recursive: if the given user hasn't got
|
||||
direct access to this object and if he has got access to other
|
||||
``User`` objects, we check if one of those users owns the
|
||||
object.
|
||||
|
||||
:param obj: a admin object
|
||||
:return: a boolean
|
||||
@@ -307,18 +304,20 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
self.post_create(creator)
|
||||
|
||||
def from_csv(self, user, row, crypt_password=True):
|
||||
"""Create a new account from a CSV file entry
|
||||
"""Create a new account from a CSV file entry.
|
||||
|
||||
The expected order is the following::
|
||||
|
||||
"account", loginname, password, first name, last name, enabled, group, address[, domain, ...]
|
||||
"account", loginname, password, first name, last name, enabled, group
|
||||
|
||||
Additional fields can be added using the *AccountImported* event.
|
||||
|
||||
:param user: a ``core.User`` instance
|
||||
:param row: a list containing the expected information
|
||||
:param crypt_password:
|
||||
"""
|
||||
if len(row) < 7:
|
||||
raise AdminError(_("Invalid line"))
|
||||
raise BadRequest(_("Invalid line"))
|
||||
role = row[6].strip()
|
||||
if not user.is_superuser and not role in ["SimpleUsers", "DomainAdmins"]:
|
||||
raise PermDeniedException(
|
||||
@@ -327,11 +326,11 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
self.username = row[1].strip()
|
||||
if role == "SimpleUsers":
|
||||
if (len(row) < 8 or not row[7].strip()):
|
||||
raise AdminError(
|
||||
raise BadRequest(
|
||||
_("The simple user '%s' must have a valid email address" % self.username)
|
||||
)
|
||||
if self.username != row[7].strip():
|
||||
raise AdminError(
|
||||
raise BadRequest(
|
||||
_("username and email fields must not differ for '%s'" % self.username)
|
||||
)
|
||||
|
||||
@@ -349,6 +348,12 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
events.raiseEvent("AccountImported", user, self, row[7:])
|
||||
|
||||
def to_csv(self, csvwriter):
|
||||
"""Export this account.
|
||||
|
||||
The CSV format is used to export.
|
||||
|
||||
:param csvwriter: csv object
|
||||
"""
|
||||
row = ["account", self.username.encode("utf-8"), self.password.encode("utf-8"),
|
||||
self.first_name.encode("utf-8"), self.last_name.encode("utf-8"),
|
||||
self.is_active, self.group, self.email.encode("utf-8")]
|
||||
@@ -358,7 +363,7 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
reversion.register(User)
|
||||
|
||||
|
||||
def populate_callback(user):
|
||||
def populate_callback(user, group='SimpleUsers'):
|
||||
"""Populate callback
|
||||
|
||||
If the LDAP authentication backend is in use, this callback will
|
||||
@@ -371,7 +376,7 @@ def populate_callback(user):
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
|
||||
sadmins = User.objects.filter(is_superuser=True)
|
||||
user.set_role("SimpleUsers")
|
||||
user.set_role(group)
|
||||
user.post_create(sadmins[0])
|
||||
for su in sadmins[1:]:
|
||||
grant_access_to_object(su, user)
|
||||
@@ -460,24 +465,44 @@ class Log(models.Model):
|
||||
|
||||
@receiver(reversion.post_revision_commit)
|
||||
def post_revision_commit(sender, **kwargs):
|
||||
import logging
|
||||
|
||||
if kwargs["revision"].user is None:
|
||||
return
|
||||
logger = logging.getLogger("modoboa.admin")
|
||||
for version in kwargs["versions"]:
|
||||
if version.type == reversion.models.VERSION_ADD:
|
||||
prev_revisions = reversion.get_for_object(version.object)
|
||||
if prev_revisions.count() == 1:
|
||||
action = _("added")
|
||||
level = "info"
|
||||
elif version.type == reversion.models.VERSION_CHANGE:
|
||||
else:
|
||||
action = _("modified")
|
||||
level = "warning"
|
||||
else:
|
||||
action = _("deleted")
|
||||
level = "critical"
|
||||
message = _("%(object)s '%(name)s' %(action)s by user %(user)s") % {
|
||||
"object": unicode(version.content_type).capitalize(),
|
||||
"name": version.object_repr, "action": action,
|
||||
"user": kwargs["revision"].user.username
|
||||
}
|
||||
getattr(logger, level)(message)
|
||||
|
||||
|
||||
@receiver(post_delete)
|
||||
def post_delete_hook(sender, instance, **kwargs):
|
||||
"""Custom post-delete hook.
|
||||
|
||||
We want to know who was responsible for an object deletion.
|
||||
"""
|
||||
from reversion.models import Version
|
||||
|
||||
if not reversion.is_registered(sender):
|
||||
return
|
||||
del_list = reversion.get_deleted(sender)
|
||||
try:
|
||||
version = del_list.get(object_id=instance.id)
|
||||
except Version.DoesNotExist:
|
||||
return
|
||||
logger = logging.getLogger("modoboa.admin")
|
||||
msg = _("%(object)s '%(name)s' %(action)s by user %(user)s") % {
|
||||
"object": unicode(version.content_type).capitalize(),
|
||||
"name": version.object_repr, "action": _("deleted"),
|
||||
"user": version.revision.user.username
|
||||
}
|
||||
logger.critical(msg)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Password hashers for Modoboa.
|
||||
"""
|
||||
from modoboa.core.password_hashers.base import (
|
||||
PLAINHasher, CRYPTHasher, MD5Hasher, SHA256Hasher
|
||||
)
|
||||
from modoboa.core.password_hashers.advanced import (
|
||||
MD5CRYPTHasher, SHA256CRYPTHasher, SHA512CRYPTHasher
|
||||
)
|
||||
|
||||
|
||||
def get_password_hasher(scheme):
|
||||
"""Retrieve the hasher corresponding to :keyword:`scheme`.
|
||||
|
||||
If no class is found, `PLAINHasher` is returned.
|
||||
|
||||
:param str scheme: a valid scheme name
|
||||
:rtype: PasswordHasher sub class
|
||||
:return: The hasher class
|
||||
"""
|
||||
try:
|
||||
scheme = scheme.replace('-', '')
|
||||
hasher = globals()['%sHasher' % scheme]
|
||||
except AttributeError:
|
||||
hasher = PLAINHasher
|
||||
return hasher
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Advanced (ie. stronger) password hashers.
|
||||
|
||||
This module relies on `passlib` to provide more secure hashers.
|
||||
"""
|
||||
from passlib.hash import md5_crypt, sha256_crypt, sha512_crypt
|
||||
from modoboa.core.password_hashers.base import PasswordHasher
|
||||
from modoboa.lib import parameters
|
||||
|
||||
|
||||
class MD5CRYPTHasher(PasswordHasher):
|
||||
"""
|
||||
MD5-CRYPT password hasher.
|
||||
|
||||
This scheme can't be considered as secure anymore.
|
||||
"""
|
||||
@property
|
||||
def scheme(self):
|
||||
return '{MD5-CRYPT}' if self._target == 'local' else '{CRYPT}'
|
||||
|
||||
def _b64encode(self, pwhash):
|
||||
return pwhash
|
||||
|
||||
def _encrypt(self, clearvalue, salt=None):
|
||||
return md5_crypt.encrypt(clearvalue)
|
||||
|
||||
def verify(self, clearvalue, hashed_value):
|
||||
return md5_crypt.verify(clearvalue, hashed_value)
|
||||
|
||||
|
||||
class SHA256CRYPTHasher(PasswordHasher):
|
||||
"""
|
||||
SHA256-CRYPT password hasher.
|
||||
|
||||
Supports rounds and is a good compromise between security and
|
||||
performance.
|
||||
"""
|
||||
@property
|
||||
def scheme(self):
|
||||
return '{SHA256-CRYPT}' if self._target == 'local' else '{CRYPT}'
|
||||
|
||||
def _b64encode(self, pwhash):
|
||||
return pwhash
|
||||
|
||||
def _encrypt(self, clearvalue, salt=None):
|
||||
rounds = int(parameters.get_admin('ROUNDS_NUMBER'))
|
||||
return sha256_crypt.encrypt(clearvalue, rounds=rounds)
|
||||
|
||||
def verify(self, clearvalue, hashed_value):
|
||||
return sha256_crypt.verify(clearvalue, hashed_value)
|
||||
|
||||
|
||||
class SHA512CRYPTHasher(PasswordHasher):
|
||||
"""
|
||||
SHA512-CRYPT password hasher.
|
||||
|
||||
Supports rounds and is the strongest scheme provided by
|
||||
Modoboa. Requires more resource than SHA256-CRYPT.
|
||||
"""
|
||||
@property
|
||||
def scheme(self):
|
||||
return '{SHA512-CRYPT}' if self._target == 'local' else '{CRYPT}'
|
||||
|
||||
def _b64encode(self, pwhash):
|
||||
return pwhash
|
||||
|
||||
def _encrypt(self, clearvalue, salt=None):
|
||||
rounds = int(parameters.get_admin('ROUNDS_NUMBER'))
|
||||
return sha512_crypt.encrypt(clearvalue, rounds=rounds)
|
||||
|
||||
def verify(self, clearvalue, hashed_value):
|
||||
return sha512_crypt.verify(clearvalue, hashed_value)
|
||||
@@ -0,0 +1,125 @@
|
||||
"""
|
||||
Base password hashers.
|
||||
|
||||
Contains weak hashers (the original ones) available with Modoboa.
|
||||
"""
|
||||
import crypt
|
||||
import hashlib
|
||||
import base64
|
||||
import string
|
||||
from random import Random
|
||||
from django.utils.crypto import constant_time_compare
|
||||
|
||||
|
||||
class PasswordHasher(object):
|
||||
"""
|
||||
Base class of all hashers.
|
||||
"""
|
||||
def __init__(self, target='local'):
|
||||
self._target = target
|
||||
|
||||
def _encrypt(self, clearvalue, salt=None):
|
||||
raise NotImplementedError
|
||||
|
||||
def _b64encode(self, pwhash):
|
||||
"""Encode :keyword:`pwhash` using base64 if needed.
|
||||
|
||||
:param str pwhash: password hash
|
||||
:return: base64 encoded hash or original hash
|
||||
"""
|
||||
if self._target == 'ldap':
|
||||
return base64.b64encode(pwhash)
|
||||
return pwhash
|
||||
|
||||
def encrypt(self, clearvalue):
|
||||
"""Encrypt a password.
|
||||
|
||||
The format used to store passwords is the same than dovecot's one::
|
||||
|
||||
{SCHEME}<hash>
|
||||
|
||||
<hash> may differ according to the used scheme.
|
||||
|
||||
:param str clearvalue: clear password
|
||||
:rtype: str
|
||||
:return: encrypted password
|
||||
"""
|
||||
pwhash = self._b64encode(self._encrypt(clearvalue))
|
||||
return '%s%s' % (self.scheme, pwhash)
|
||||
|
||||
def verify(self, clearvalue, hashed_value):
|
||||
"""Verify a password against a hashed value.
|
||||
|
||||
:param str clearvalue: clear password
|
||||
:param str hashed_value: password previously hashed
|
||||
:return: True if passwords match, False otherwise
|
||||
"""
|
||||
return constant_time_compare(
|
||||
self._encrypt(clearvalue, hashed_value),
|
||||
hashed_value
|
||||
)
|
||||
|
||||
|
||||
class PLAINHasher(PasswordHasher):
|
||||
"""
|
||||
Plain (ie. clear) password hasher.
|
||||
"""
|
||||
@property
|
||||
def scheme(self):
|
||||
return '{PLAIN}'
|
||||
|
||||
def _encrypt(self, clearvalue, salt=None):
|
||||
return clearvalue
|
||||
|
||||
|
||||
class CRYPTHasher(PasswordHasher):
|
||||
"""
|
||||
crypt password hasher.
|
||||
|
||||
Uses python `crypt` standard module.
|
||||
"""
|
||||
@property
|
||||
def scheme(self):
|
||||
return '{CRYPT}'
|
||||
|
||||
def _encrypt(self, clearvalue, salt=None):
|
||||
if salt is None:
|
||||
salt = "".join(Random().sample(string.letters + string.digits, 2))
|
||||
return crypt.crypt(clearvalue, salt)
|
||||
|
||||
|
||||
class MD5Hasher(PasswordHasher):
|
||||
"""
|
||||
MD5 password hasher.
|
||||
|
||||
Uses python `hashlib` standard module.
|
||||
"""
|
||||
@property
|
||||
def scheme(self):
|
||||
return '{MD5}'
|
||||
|
||||
def _encrypt(self, clearvalue, salt=None):
|
||||
obj = hashlib.md5(clearvalue)
|
||||
return obj.hexdigest()
|
||||
|
||||
|
||||
class SHA256Hasher(PasswordHasher):
|
||||
"""
|
||||
SHA256 password hasher.
|
||||
|
||||
Uses python `hashlib` and `base64` standard modules.
|
||||
"""
|
||||
@property
|
||||
def scheme(self):
|
||||
return '{SHA256}'
|
||||
|
||||
def _encrypt(self, clearvalue, salt=None):
|
||||
return hashlib.sha256(clearvalue).digest()
|
||||
|
||||
def _b64encode(self, pwhash):
|
||||
"""Encode :keyword:`pwhash` using base64.
|
||||
|
||||
:param str pwhash: password hash
|
||||
:return: base64 encoded hash
|
||||
"""
|
||||
return base64.b64encode(pwhash)
|
||||
@@ -31,36 +31,6 @@ def core_menu(selection, user):
|
||||
)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def settings_menu(selection, user):
|
||||
entries = [
|
||||
{"name": "extensions",
|
||||
"class": "ajaxlink",
|
||||
"url": "extensions/",
|
||||
"label": _("Extensions"),
|
||||
"img": ""},
|
||||
{"name": "info",
|
||||
"class": "ajaxlink",
|
||||
"url": "info/",
|
||||
"label": _("Information")},
|
||||
{"name": "logs",
|
||||
"class": "ajaxlink",
|
||||
"url": "logs/",
|
||||
"label": _("Logs")},
|
||||
{"name": "parameters",
|
||||
"class": "ajaxlink",
|
||||
"url": "parameters/",
|
||||
"img": "",
|
||||
"label": _("Parameters")},
|
||||
]
|
||||
return render_to_string('common/menu.html', {
|
||||
"entries": entries,
|
||||
"css": "nav nav-list",
|
||||
"selection": selection,
|
||||
"user": user
|
||||
})
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def extensions_menu(selection, user):
|
||||
menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu", user)
|
||||
@@ -83,7 +53,7 @@ def admin_menu(selection, user):
|
||||
"label": _("Information")},
|
||||
{"name": "logs",
|
||||
"class": "ajaxlink",
|
||||
"url": "logs/",
|
||||
"url": "logs/?sort_order=-date_created",
|
||||
"label": _("Logs")},
|
||||
{"name": "parameters",
|
||||
"class": "ajaxlink",
|
||||
@@ -166,7 +136,9 @@ def tohtml(message):
|
||||
|
||||
@register.simple_tag
|
||||
def visirule(field):
|
||||
if not hasattr(field.form, "visirules") or not field.html_name in field.form.visirules:
|
||||
if not hasattr(field, 'form') or \
|
||||
not hasattr(field.form, "visirules") or \
|
||||
not field.html_name in field.form.visirules:
|
||||
return ""
|
||||
rule = field.form.visirules[field.html_name]
|
||||
return " data-visibility-field='%s' data-visibility-value='%s' " \
|
||||
@@ -230,18 +202,6 @@ def get_modoboa_logo():
|
||||
return logo
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def extra_head_content(user):
|
||||
tpl = template.Template(
|
||||
"{% for sc in static_content %}{{ sc|safe }}{% endfor %}"
|
||||
)
|
||||
return tpl.render(
|
||||
template.Context({
|
||||
'static_content': events.raiseQueryEvent("GetStaticContent", user)
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def load_optionalmenu(user):
|
||||
menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu_middle", user)
|
||||
|
||||
@@ -19,9 +19,9 @@ class ProfileTestCase(ModoTestCase):
|
||||
* The default admin changes his password (no associated Mailbox)
|
||||
* A normal user changes his password
|
||||
"""
|
||||
self.check_ajax_post(reverse("modoboa.core.views.user.profile"),
|
||||
{"oldpassword": "password",
|
||||
"newpassword": "titi", "confirmation": "titi"})
|
||||
self.ajax_post(reverse("modoboa.core.views.user.profile"),
|
||||
{"oldpassword": "password",
|
||||
"newpassword": "titi", "confirmation": "titi"})
|
||||
self.clt.logout()
|
||||
|
||||
self.assertEqual(
|
||||
@@ -31,12 +31,12 @@ class ProfileTestCase(ModoTestCase):
|
||||
self.clt.login(username="user@test.com", password="toto"), True
|
||||
)
|
||||
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.core.views.user.profile"),
|
||||
{"oldpassword": "toto",
|
||||
"newpassword": "tutu", "confirmation": "tutu"}
|
||||
)
|
||||
self.clt.logout()
|
||||
self.assertEqual(
|
||||
self.clt.login(username="user@test.com", password="tutu"), True
|
||||
self.assertTrue(
|
||||
self.clt.login(username="user@test.com", password="tutu")
|
||||
)
|
||||
|
||||
+11
-19
@@ -6,7 +6,7 @@ from django.contrib.auth.decorators import (
|
||||
)
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.webutils import (
|
||||
ajax_simple_response, _render_to_string
|
||||
_render_to_string, render_to_json_response
|
||||
)
|
||||
from modoboa.lib.listing import get_sort_order, get_listing_page
|
||||
from modoboa.core.models import Extension, Log
|
||||
@@ -24,8 +24,7 @@ def viewsettings(request, tplname='core/settings_header.html'):
|
||||
@login_required
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def viewparameters(request, tplname='core/parameters.html'):
|
||||
return ajax_simple_response({
|
||||
"status": "ok",
|
||||
return render_to_json_response({
|
||||
"left_selection": "parameters",
|
||||
"content": _render_to_string(request, tplname, {
|
||||
"forms": parameters.get_admin_forms
|
||||
@@ -42,12 +41,10 @@ def saveparameters(request):
|
||||
form.save()
|
||||
form.to_django_settings()
|
||||
continue
|
||||
return ajax_simple_response({
|
||||
"status": "ko", "prefix": form.app, "errors": form.errors
|
||||
})
|
||||
return ajax_simple_response(dict(
|
||||
status="ok", respmsg=_("Parameters saved")
|
||||
))
|
||||
return render_to_json_response(
|
||||
{'form_errors': form.errors, 'prefix': form.app}, status=400
|
||||
)
|
||||
return render_to_json_response(_("Parameters saved"))
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -68,8 +65,7 @@ def viewextensions(request, tplname='core/extensions.html'):
|
||||
ext["selection"] = False
|
||||
|
||||
tbl = ExtensionsTable(request, exts)
|
||||
return ajax_simple_response({
|
||||
"status": "ok",
|
||||
return render_to_json_response({
|
||||
"content": _render_to_string(request, tplname, {"extensions": tbl})
|
||||
})
|
||||
|
||||
@@ -82,7 +78,7 @@ def saveextensions(request):
|
||||
for k in request.POST.keys():
|
||||
if k.startswith("select_"):
|
||||
parts = k.split("_", 1)
|
||||
dbext = Extension.objects.get(name=parts[1])
|
||||
dbext = Extension.objects.get(name=parts[1])
|
||||
if not dbext in actived_exts:
|
||||
dbext.on()
|
||||
else:
|
||||
@@ -91,16 +87,13 @@ def saveextensions(request):
|
||||
if not ext in found:
|
||||
ext.off()
|
||||
|
||||
return ajax_simple_response(dict(
|
||||
status="ok", respmsg=_("Modifications applied.")
|
||||
))
|
||||
return render_to_json_response(_("Modifications applied."))
|
||||
|
||||
|
||||
@login_required
|
||||
@user_passes_test(lambda u: u.is_superuser)
|
||||
def information(request, tplname="core/information.html"):
|
||||
return ajax_simple_response({
|
||||
"status": "ok",
|
||||
return render_to_json_response({
|
||||
"content": render_to_string(tplname, {})
|
||||
})
|
||||
|
||||
@@ -118,8 +111,7 @@ def logs(request, tplname="core/logs.html"):
|
||||
Log.objects.all().order_by("%s%s" % (sort_dir, sort_order)),
|
||||
request.GET.get("page", 1)
|
||||
)
|
||||
return ajax_simple_response({
|
||||
"status": "ok",
|
||||
return render_to_json_response({
|
||||
"content": render_to_string(tplname, {
|
||||
"logs": page.object_list,
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,7 @@ from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.lib.webutils import (
|
||||
ajax_simple_response, _render_to_string
|
||||
_render_to_string, render_to_json_response
|
||||
)
|
||||
from modoboa.lib.cryptutils import encrypt
|
||||
from modoboa.core.forms import ProfileForm
|
||||
@@ -32,17 +32,11 @@ def profile(request, tplname='core/user_profile.html'):
|
||||
form.save()
|
||||
if update_password and form.cleaned_data["confirmation"] != "":
|
||||
request.session["password"] = encrypt(form.cleaned_data["confirmation"])
|
||||
return ajax_simple_response(dict(
|
||||
status="ok", respmsg=_("Profile updated")
|
||||
))
|
||||
return ajax_simple_response({
|
||||
"status": "ko",
|
||||
"errors": form.errors
|
||||
})
|
||||
return render_to_json_response(_("Profile updated"))
|
||||
return render_to_json_response({'form_errors': form.errors}, status=400)
|
||||
|
||||
form = ProfileForm(update_password, instance=request.user)
|
||||
return ajax_simple_response({
|
||||
"status": "ok",
|
||||
return render_to_json_response({
|
||||
"content": _render_to_string(request, tplname, {
|
||||
"form": form
|
||||
})
|
||||
@@ -57,16 +51,13 @@ def preferences(request):
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
continue
|
||||
return ajax_simple_response({
|
||||
"status": "ko", "prefix": form.app, "errors": form.errors
|
||||
})
|
||||
return render_to_json_response({
|
||||
"prefix": form.app, "form_errors": form.errors
|
||||
}, status=400)
|
||||
|
||||
return ajax_simple_response({
|
||||
"status": "ok", "respmsg": _("Preferences saved")
|
||||
})
|
||||
return render_to_json_response(_("Preferences saved"))
|
||||
|
||||
return ajax_simple_response({
|
||||
"status": "ok",
|
||||
return render_to_json_response({
|
||||
"content": _render_to_string(request, "core/user_preferences.html", {
|
||||
"forms": parameters.get_user_forms(request.user)
|
||||
})
|
||||
|
||||
@@ -43,12 +43,13 @@ def password_change(user):
|
||||
|
||||
|
||||
@events.observe("GetStaticContent")
|
||||
def get_static_content(user):
|
||||
if not user.mailbox_set.count():
|
||||
def get_static_content(caller, user):
|
||||
if caller != 'top' and not user.mailbox_set.count():
|
||||
return []
|
||||
return """<script type="text/javascript">
|
||||
|
||||
return ["""<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$(document).on('click', 'a[name=sendspam]', simple_ajax_request);
|
||||
$(document).on('click', 'a[name=sendvirus]', simple_ajax_request);
|
||||
});
|
||||
</script>"""
|
||||
</script>"""]
|
||||
|
||||
@@ -3,10 +3,11 @@ import os
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.translation import ugettext as _
|
||||
from modoboa.lib.webutils import ajax_simple_response
|
||||
from modoboa.lib.webutils import render_to_json_response
|
||||
from modoboa.lib.emailutils import sendmail_simple, sendmail_fromfile
|
||||
from modoboa.lib.decorators import needs_mailbox
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def send_virus(request):
|
||||
@@ -14,8 +15,9 @@ def send_virus(request):
|
||||
os.path.join(settings.MODOBOA_DIR,
|
||||
"tmp/virus.msg"))
|
||||
if status:
|
||||
return ajax_simple_response(dict(status="ok", respmsg=_("Message sent")))
|
||||
return ajax_simple_response(dict(status="ko", respmsg=error))
|
||||
return render_to_json_response(_("Message sent"))
|
||||
return render_to_json_response(error, status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
@@ -38,5 +40,5 @@ XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X
|
||||
You should send this test mail from an account outside of your network.
|
||||
""")
|
||||
if status:
|
||||
return ajax_simple_response(dict(status="ok", respmsg=_("Message sent")))
|
||||
return ajax_simple_response(dict(status="ko", respmsg=error))
|
||||
return render_to_json_response(_("Message sent"))
|
||||
return render_to_json_response(respmsg=error, status=500)
|
||||
|
||||
@@ -3,25 +3,30 @@ from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.core.urlresolvers import reverse
|
||||
from modoboa.core.extensions import ModoExtension, exts_pool
|
||||
from modoboa.lib import parameters, events
|
||||
from modoboa.lib.exceptions import PermDeniedException
|
||||
from modoboa.lib.exceptions import PermDeniedException, BadRequest
|
||||
from modoboa.lib.emailutils import split_mailbox
|
||||
from modoboa.extensions.admin.exceptions import AdminError
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, DomainAlias, Mailbox, Alias
|
||||
)
|
||||
|
||||
|
||||
admin_events = [
|
||||
"CreateDomain",
|
||||
"DomainCreated",
|
||||
"DomainModified",
|
||||
"DeleteDomain",
|
||||
"DomainDeleted",
|
||||
"DomainOwnershipRemoved",
|
||||
"ExtraDomainEntries",
|
||||
"ExtraDomainMenuEntries",
|
||||
"ExtraDomainFilters",
|
||||
"GetDomainActions",
|
||||
"GetDomainModifyLink",
|
||||
"CheckDomainName",
|
||||
|
||||
"DomainAliasCreated",
|
||||
"DomainAliasDeleted",
|
||||
|
||||
"CreateMailbox",
|
||||
"DeleteMailbox",
|
||||
"ModifyMailbox",
|
||||
"MailboxCreated",
|
||||
"MailboxDeleted",
|
||||
"MailboxModified",
|
||||
|
||||
"MailboxAliasCreated",
|
||||
"MailboxAliasDeleted",
|
||||
@@ -32,6 +37,9 @@ admin_events = [
|
||||
"ExtraAccountForm",
|
||||
"CheckExtraAccountForm",
|
||||
"FillAccountInstances",
|
||||
|
||||
"ExtraDomainImportHelp",
|
||||
"ImportObject"
|
||||
]
|
||||
|
||||
|
||||
@@ -125,13 +133,25 @@ def grant_access_to_all_objects(user, role):
|
||||
|
||||
@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 []
|
||||
return [dom.name for dom in Domain.objects.get_for_admin(admin)]
|
||||
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()
|
||||
@@ -139,17 +159,19 @@ def import_account_mailbox(user, account, row):
|
||||
try:
|
||||
domain = Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
raise AdminError(
|
||||
raise BadRequest(
|
||||
_("Account import failed (%s): domain does not exist" % account.username)
|
||||
)
|
||||
if not user.can_access(domain):
|
||||
raise PermDeniedException
|
||||
quota = int(row[1].strip())
|
||||
use_domain_quota = True if not quota else False
|
||||
mb = Mailbox(address=mailbox, domain=domain,
|
||||
user=account, use_domain_quota=True)
|
||||
mb.set_quota(override_rules=user.has_perm("admin.change_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[1:]:
|
||||
for domname in row[2:]:
|
||||
try:
|
||||
dom = Domain.objects.get(name=domname.strip())
|
||||
except Domain.DoesNotExist:
|
||||
@@ -162,8 +184,10 @@ def account_auto_created(user):
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
|
||||
sadmins = User.objects.filter(is_superuser=True)
|
||||
localpart, domname = split_mailbox(user.username)
|
||||
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:
|
||||
@@ -193,6 +217,11 @@ def user_logged_in(request, username, 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]
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
:mod:`exceptions` --- Custom exceptions of the admin. application
|
||||
-----------------------------------------------------------------
|
||||
"""
|
||||
|
||||
from modoboa.lib.exceptions import ModoboaException
|
||||
|
||||
|
||||
class AdminError(ModoboaException):
|
||||
"""
|
||||
Just a Custom exception to identify errors coming from the admin
|
||||
application.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BadDestination(Exception):
|
||||
pass
|
||||
@@ -13,11 +13,13 @@ class MailboxFactory(PermissionFactory):
|
||||
FACTORY_FOR = models.Mailbox
|
||||
|
||||
quota = 10
|
||||
enabled = True
|
||||
|
||||
|
||||
class AliasFactory(PermissionFactory):
|
||||
FACTORY_FOR = models.Alias
|
||||
|
||||
enabled = True
|
||||
|
||||
def populate_database():
|
||||
"""Create test data.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
[{"pk": 1, "model": "core.user", "fields": {"username": "admin", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": false, "is_local": true, "last_login": "2012-01-26 15:19:25", "groups": [], "user_permissions": [], "password": "{CRYPT}dTTsGDkA5ZHKg", "email": "", "date_joined": "2012-01-26 15:19:25"}},{"pk": 1, "model": "core.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["core", "user"], "object_id": 1}}]
|
||||
@@ -1 +0,0 @@
|
||||
[{"pk": 2, "model": "admin.user", "fields": {"username": "user@test.com", "is_active": "True", "is_superuser": "False", "password": "{PLAIN}toto", "groups": [2]}},{"pk": 3, "model": "admin.user", "fields": {"username": "admin@test.com", "is_active": "True", "is_superuser": "False", "password": "{PLAIN}toto", "groups": [1]}},{"pk": 4, "model": "admin.user", "fields": {"username": "superadmin@test.com", "is_active": "True", "is_superuser": "True", "password": "{PLAIN}toto", "groups": []}},{"pk": 1, "model": "admin.ObjectDates", "fields": {"creation": "2011-09-07 09:27:07.579603", "last_modification": "2011-09-07 09:27:07.579603"}}, {"pk": 2, "model": "admin.ObjectDates", "fields": {"creation": "2011-09-07 09:27:07.579603", "last_modification": "2011-09-07 09:27:07.579603"}}, {"pk": 3, "model": "admin.ObjectDates", "fields": {"creation": "2011-09-07 09:27:07.579603", "last_modification": "2011-09-07 09:27:07.579603"}}, {"pk": 1, "model": "admin.Domain", "fields": {"name" : "test.com", "quota" : "100", "dates": "1"}}, {"pk": 1, "model": "admin.Mailbox", "fields": {"address": "user", "domain": "1", "dates": "1", "quota": "10", "user": "2"}}, {"pk": 2, "model": "admin.Mailbox", "fields": {"address": "admin", "domain": "1", "dates": "2", "quota": "10", "user": "3"}}, {"pk": 3, "model": "admin.Mailbox", "fields": {"address": "superadmin", "domain": "1", "dates": "3", "quota": "10", "user": "4"}}, {"pk": "user@test.com", "model": "admin.Quota", "fields": {"mbox": 1, "username": "user@test.com", "bytes": 0, "messages": 0}}, {"pk": "admin@test.com", "model": "admin.Quota", "fields": {"mbox": 2, "username": "admin@test.com", "bytes": 0, "messages": 0}}, {"pk": "superadmin@test.com", "model": "admin.Quota", "fields": {"mbox": 3, "username": "superadmin@test.com", "bytes": 0, "messages": 0}},{"pk": 2, "model": "admin.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["admin", "user"], "object_id": 2}},{"pk": 3, "model": "admin.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["admin", "user"], "object_id": 3}}]
|
||||
@@ -2,27 +2,33 @@ from django import forms
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.http import QueryDict
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.lib.exceptions import PermDeniedException
|
||||
from modoboa.lib.exceptions import PermDeniedException, Conflict, NotFound
|
||||
from modoboa.lib.permissions import get_account_roles
|
||||
from modoboa.lib.emailutils import split_mailbox
|
||||
from modoboa.lib.formutils import (
|
||||
DomainNameField, DynamicForm, TabForms
|
||||
DomainNameField, DynamicForm, TabForms, YesNoField
|
||||
)
|
||||
from modoboa.core.models import User
|
||||
from modoboa.extensions.admin.exceptions import AdminError
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, Mailbox, Alias
|
||||
)
|
||||
|
||||
|
||||
class AccountFormGeneral(forms.ModelForm):
|
||||
username = forms.CharField(label=ugettext_lazy("Username"))
|
||||
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."
|
||||
)
|
||||
)
|
||||
role = forms.ChoiceField(
|
||||
label=ugettext_lazy("Role"),
|
||||
choices=[('', ugettext_lazy("Choose"))],
|
||||
help_text=ugettext_lazy("What level of permission this user will have")
|
||||
)
|
||||
password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
||||
password1 = forms.CharField(
|
||||
label=_("Password"), widget=forms.PasswordInput
|
||||
)
|
||||
password2 = forms.CharField(
|
||||
label=ugettext_lazy("Confirmation"),
|
||||
widget=forms.PasswordInput,
|
||||
@@ -45,21 +51,21 @@ class AccountFormGeneral(forms.ModelForm):
|
||||
widget=forms.HiddenInput, required=False
|
||||
)
|
||||
else:
|
||||
self.fields["role"].choices = \
|
||||
[('', ugettext_lazy("Choose"))] + get_account_roles(user)
|
||||
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:
|
||||
if len(args) \
|
||||
if args \
|
||||
and (args[0].get("password1", "") == ""
|
||||
and args[0].get("password2", "") == ""):
|
||||
self.fields["password1"].required = False
|
||||
self.fields["password2"].required = False
|
||||
|
||||
u = kwargs["instance"]
|
||||
self.fields["role"].initial = u.group
|
||||
|
||||
if not u.is_local \
|
||||
and parameters.get_admin("LDAP_AUTH_METHOD") == "directbind":
|
||||
account = kwargs["instance"]
|
||||
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"]
|
||||
|
||||
@@ -90,7 +96,7 @@ class AccountFormGeneral(forms.ModelForm):
|
||||
def save(self, commit=True):
|
||||
account = super(AccountFormGeneral, self).save(commit=False)
|
||||
if self.user == account and not self.cleaned_data["is_active"]:
|
||||
raise AdminError(_("You can't disable your own account"))
|
||||
raise PermDeniedException(_("You can't disable your own account"))
|
||||
if commit:
|
||||
if "password1" in self.cleaned_data \
|
||||
and self.cleaned_data["password1"] != "":
|
||||
@@ -105,22 +111,35 @@ class AccountFormMail(forms.Form, DynamicForm):
|
||||
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)."),
|
||||
help_text=_("Quota in MB for this mailbox. Define a custom value or "
|
||||
"use domain's default one. Leave empty to define an "
|
||||
"unlimited value (not allowed for domain "
|
||||
"administrators)."),
|
||||
widget=forms.widgets.TextInput(attrs={"class": "span1"})
|
||||
)
|
||||
quota_act = forms.BooleanField(required=False)
|
||||
aliases = forms.EmailField(
|
||||
label=ugettext_lazy("Alias(es)"),
|
||||
required=False,
|
||||
help_text=ugettext_lazy("Alias(es) of this mailbox. Indicate only one address per input, press ENTER to add a new input. Use the '*' character to create a 'catchall' alias (ex: *@domain.tld).")
|
||||
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)
|
||||
if hasattr(self, "mb") and self.mb is not None:
|
||||
self.extra_fields = []
|
||||
for fname, field in events.raiseQueryEvent('ExtraFormFields', 'mailform', self.mb):
|
||||
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():
|
||||
@@ -143,6 +162,54 @@ class AccountFormMail(forms.Form, DynamicForm):
|
||||
"""Ensure lower case emails"""
|
||||
return self.cleaned_data["email"].lower()
|
||||
|
||||
def create_mailbox(self, user, 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 save(self, user, account):
|
||||
if self.cleaned_data["email"] == "":
|
||||
return None
|
||||
@@ -151,74 +218,46 @@ class AccountFormMail(forms.Form, DynamicForm):
|
||||
self.cleaned_data["quota"] = None
|
||||
|
||||
if not hasattr(self, "mb") or self.mb is None:
|
||||
locpart, domname = split_mailbox(self.cleaned_data["email"])
|
||||
try:
|
||||
domain = Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
raise AdminError(_("Domain does not exist"))
|
||||
if not user.can_access(domain):
|
||||
raise PermDeniedException
|
||||
try:
|
||||
Mailbox.objects.get(address=locpart, domain=domain)
|
||||
except Mailbox.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
raise AdminError(_("Mailbox %s already exists" % self.cleaned_data["email"]))
|
||||
events.raiseEvent("CanCreate", user, "mailboxes")
|
||||
self.mb = Mailbox(address=locpart, domain=domain, user=account,
|
||||
use_domain_quota=self.cleaned_data["quota_act"])
|
||||
self.mb.set_quota(self.cleaned_data["quota"],
|
||||
user.has_perm("admin.add_domain"))
|
||||
self.mb.save(creator=user)
|
||||
self.create_mailbox(user, account)
|
||||
else:
|
||||
newaddress = None
|
||||
if self.cleaned_data["email"] != self.mb.full_address:
|
||||
newaddress = self.cleaned_data["email"]
|
||||
elif account.group == "SimpleUsers" and account.username != self.mb.full_address:
|
||||
newaddress = account.username
|
||||
if newaddress is not None:
|
||||
local_part, domname = split_mailbox(newaddress)
|
||||
try:
|
||||
domain = Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
raise AdminError(_("Domain does not exist"))
|
||||
if not user.can_access(domain):
|
||||
raise PermDeniedException
|
||||
self.mb.rename(local_part, domain)
|
||||
|
||||
self.mb.use_domain_quota = self.cleaned_data["quota_act"]
|
||||
override_rules = True \
|
||||
if not self.mb.quota or user.has_perm("admin.add_domain") \
|
||||
else False
|
||||
self.mb.set_quota(self.cleaned_data["quota"], override_rules)
|
||||
self.mb.save()
|
||||
self.update_mailbox(user, account)
|
||||
events.raiseEvent(
|
||||
'SaveExtraFormFields', 'mailform', self.mb, self.cleaned_data
|
||||
)
|
||||
|
||||
account.email = self.cleaned_data["email"]
|
||||
account.save()
|
||||
|
||||
aliases = []
|
||||
for name, value in self.cleaned_data.iteritems():
|
||||
if not name.startswith("aliases"):
|
||||
continue
|
||||
if value == "":
|
||||
continue
|
||||
local_part, domname = split_mailbox(value)
|
||||
try:
|
||||
self.mb.alias_set.get(address=local_part, domain__name=domname)
|
||||
except Alias.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
events.raiseEvent("CanCreate", user, "mailbox_aliases")
|
||||
al = Alias(address=local_part, enabled=account.is_active)
|
||||
al.domain = Domain.objects.get(name=domname)
|
||||
al.save(int_rcpts=[self.mb], creator=user)
|
||||
aliases.append(value)
|
||||
|
||||
for alias in self.mb.alias_set.all():
|
||||
if len(alias.get_recipients()) >= 2:
|
||||
continue
|
||||
if not len(filter(lambda name: self.cleaned_data[name] == alias.full_address,
|
||||
self.cleaned_data.keys())):
|
||||
if not alias.full_address in aliases:
|
||||
if len(alias.get_recipients()) >= 2:
|
||||
continue
|
||||
alias.delete()
|
||||
else:
|
||||
aliases.remove(alias.full_address)
|
||||
if aliases:
|
||||
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], creator=user)
|
||||
|
||||
return self.mb
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.http import QueryDict
|
||||
from modoboa.lib.exceptions import BadRequest, NotFound, Conflict
|
||||
from modoboa.lib.emailutils import split_mailbox
|
||||
from modoboa.lib.formutils import (
|
||||
DynamicForm
|
||||
)
|
||||
from modoboa.extensions.admin.exceptions import AdminError
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, Mailbox, Alias
|
||||
)
|
||||
@@ -14,11 +14,17 @@ from modoboa.extensions.admin.models import (
|
||||
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).")
|
||||
help_text=ugettext_lazy(
|
||||
"The distribution list address. Use the '*' character to create a "
|
||||
"'catchall' address (ex: *@domain.tld)."
|
||||
)
|
||||
)
|
||||
recipients = forms.EmailField(
|
||||
label=ugettext_lazy("Recipients"), required=False,
|
||||
help_text=ugettext_lazy("Mailbox(es) this alias will point to. Indicate only one address per input, press ENTER to add a new input.")
|
||||
help_text=ugettext_lazy(
|
||||
"Mailbox(es) this alias will point to. Indicate only one address "
|
||||
"per input, press ENTER to add a new input."
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -58,7 +64,9 @@ class AliasForm(forms.ModelForm, DynamicForm):
|
||||
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"))
|
||||
raise forms.ValidationError(
|
||||
_("You don't have access to this domain")
|
||||
)
|
||||
return self.cleaned_data["email"]
|
||||
|
||||
def set_recipients(self):
|
||||
@@ -78,7 +86,9 @@ class AliasForm(forms.ModelForm, DynamicForm):
|
||||
continue
|
||||
local_part, domname = split_mailbox(v)
|
||||
if domname is None:
|
||||
raise AdminError("%s %s" % (_("Invalid mailbox"), v))
|
||||
raise BadRequest(
|
||||
u"%s %s" % (_("Invalid mailbox"), v)
|
||||
)
|
||||
try:
|
||||
domain = Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
@@ -94,20 +104,26 @@ class AliasForm(forms.ModelForm, DynamicForm):
|
||||
try:
|
||||
rcpt = Mailbox.objects.get(domain=domain, address=local_part)
|
||||
except Mailbox.DoesNotExist:
|
||||
raise AdminError(_("Local recipient %s not found" % v))
|
||||
raise NotFound(
|
||||
_("Local recipient %s not found" % v)
|
||||
)
|
||||
if rcpt in self.int_rcpts:
|
||||
raise AdminError(_("Recipient %s already present" % v))
|
||||
raise Conflict(
|
||||
_("Recipient %s already present" % v)
|
||||
)
|
||||
self.int_rcpts += [rcpt]
|
||||
total += 1
|
||||
continue
|
||||
|
||||
if v in self.ext_rcpts:
|
||||
raise AdminError(_("Recipient %s already present" % v))
|
||||
raise Conflict(
|
||||
_("Recipient %s already present" % v)
|
||||
)
|
||||
self.ext_rcpts += [v]
|
||||
total += 1
|
||||
|
||||
if total == 0:
|
||||
raise AdminError(_("No recipient defined"))
|
||||
raise BadRequest(_("No recipient defined"))
|
||||
|
||||
def save(self, commit=True):
|
||||
alias = super(AliasForm, self).save(commit=False)
|
||||
|
||||
@@ -2,11 +2,11 @@ from django import forms
|
||||
from django.http import QueryDict
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.exceptions import Conflict
|
||||
from modoboa.lib.formutils import (
|
||||
DomainNameField, YesNoField, DynamicForm, TabForms
|
||||
)
|
||||
from modoboa.core.models import User
|
||||
from modoboa.extensions.admin.exceptions import AdminError
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, DomainAlias, Mailbox, Alias, Quota
|
||||
)
|
||||
@@ -16,7 +16,10 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
aliases = DomainNameField(
|
||||
label=ugettext_lazy("Alias(es)"),
|
||||
required=False,
|
||||
help_text=ugettext_lazy("Alias(es) of this domain. Indicate only one name per input, press ENTER to add a new input.")
|
||||
help_text=ugettext_lazy(
|
||||
"Alias(es) of this domain. Indicate only one name per input, "
|
||||
"press ENTER to add a new input."
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -41,35 +44,48 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
self._create_field(forms.CharField, name, dalias.name, 3)
|
||||
|
||||
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()
|
||||
if len(self._errors):
|
||||
if self._errors:
|
||||
raise forms.ValidationError(self._errors)
|
||||
|
||||
cleaned_data = self.cleaned_data
|
||||
name = cleaned_data["name"]
|
||||
|
||||
try:
|
||||
DomainAlias.objects.get(name=name)
|
||||
except DomainAlias.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
self._errors["name"] = self.error_class([_("An alias with this name already exists")])
|
||||
del cleaned_data["name"]
|
||||
|
||||
dtypes = events.raiseQueryEvent('CheckDomainName')
|
||||
for dtype, label in [(DomainAlias, _('domain alias'))] + dtypes:
|
||||
try:
|
||||
dtype.objects.get(name=name)
|
||||
except dtype.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
self._errors["name"] = self.error_class(
|
||||
[_("A %s with this name already exists" % unicode(label))]
|
||||
)
|
||||
del cleaned_data["name"]
|
||||
break
|
||||
for k in cleaned_data.keys():
|
||||
if not k.startswith("aliases"):
|
||||
continue
|
||||
if cleaned_data[k] == "":
|
||||
del cleaned_data[k]
|
||||
continue
|
||||
try:
|
||||
Domain.objects.get(name=cleaned_data[k])
|
||||
except Domain.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
self._errors[k] = self.error_class([_("A domain with this name already exists")])
|
||||
for dtype, label in [(Domain, _('domain'))] + dtypes:
|
||||
try:
|
||||
dtype.objects.get(name=cleaned_data[k])
|
||||
except dtype.DoesNotExist:
|
||||
continue
|
||||
self._errors[k] = self.error_class(
|
||||
[_("A %s with this name already exists" % unicode(label))]
|
||||
)
|
||||
del cleaned_data[k]
|
||||
|
||||
break
|
||||
return cleaned_data
|
||||
|
||||
def update_mailbox_quotas(self, domain):
|
||||
@@ -91,7 +107,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
username = q['username'].replace('@%s' % self.oldname, '@%s' % domain.name)
|
||||
Quota.objects.filter(username=q['username']).update(username=username)
|
||||
|
||||
def save(self, user, commit=True):
|
||||
def save(self, user, commit=True, domalias_post_create=False):
|
||||
"""Custom save method
|
||||
|
||||
Updating a domain may have consequences on other objects
|
||||
@@ -110,25 +126,29 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
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
|
||||
try:
|
||||
d.domainalias_set.get(name=v)
|
||||
except DomainAlias.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
events.raiseEvent("CanCreate", user, "domain_aliases")
|
||||
al = DomainAlias(name=v, target=d, enabled=d.enabled)
|
||||
al.save(creator=user)
|
||||
|
||||
aliases.append(v)
|
||||
for dalias in d.domainalias_set.all():
|
||||
if not len(filter(lambda name: self.cleaned_data[name] == dalias.name,
|
||||
self.cleaned_data.keys())):
|
||||
if not dalias.name 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)
|
||||
@@ -148,7 +168,10 @@ class DomainFormOptions(forms.Form):
|
||||
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."),
|
||||
help_text=ugettext_lazy(
|
||||
"The administrator's name. Don't include the domain's name here, "
|
||||
"it will be automatically appended."
|
||||
),
|
||||
widget=forms.widgets.TextInput(attrs={"class": "input-small"}),
|
||||
required=False
|
||||
)
|
||||
@@ -160,8 +183,11 @@ class DomainFormOptions(forms.Form):
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super(DomainFormOptions, self).__init__(*args, **kwargs)
|
||||
if False in events.raiseQueryEvent('UserCanSetRole', user, 'DomainAdmins'):
|
||||
self.fields = {}
|
||||
return
|
||||
if args:
|
||||
if args[0].get("create_dom_admin", "no") == "yes":
|
||||
self.fields["dom_admin_username"].required = True
|
||||
@@ -173,6 +199,8 @@ class DomainFormOptions(forms.Form):
|
||||
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)
|
||||
@@ -181,7 +209,8 @@ class DomainFormOptions(forms.Form):
|
||||
except User.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
raise AdminError(_("User '%s' already exists" % username))
|
||||
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()
|
||||
@@ -193,6 +222,7 @@ class DomainFormOptions(forms.Form):
|
||||
mb.save(creator=user)
|
||||
|
||||
if self.cleaned_data["create_aliases"] == "yes":
|
||||
events.raiseEvent("CanCreate", user, "mailbox_aliases")
|
||||
al = Alias(address="postmaster", domain=domain, enabled=True)
|
||||
al.save(int_rcpts=[mb], creator=user)
|
||||
|
||||
@@ -223,5 +253,6 @@ class DomainForm(TabForms):
|
||||
As forms interact with each other, it is easier to make custom
|
||||
code to save them.
|
||||
"""
|
||||
for f in self.forms:
|
||||
self.forms[0]['instance'].save(user, domalias_post_create=True)
|
||||
for f in self.forms[1:]:
|
||||
f["instance"].save(user)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from modoboa.lib.exceptions import BadRequest, PermDeniedException
|
||||
from modoboa.lib.emailutils import split_mailbox
|
||||
from modoboa.extensions.admin.exceptions import BadDestination
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain
|
||||
)
|
||||
@@ -28,10 +28,14 @@ class ForwardForm(forms.Form):
|
||||
for d in rawdata.split(","):
|
||||
local_part, domname = split_mailbox(d)
|
||||
if not local_part or not domname or not len(domname):
|
||||
raise BadDestination("Invalid mailbox syntax for %s" % d)
|
||||
raise BadRequest("Invalid mailbox syntax for %s" % d)
|
||||
try:
|
||||
Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
self.dests += [d]
|
||||
else:
|
||||
raise BadDestination(_("You can't define a forward to a local destination. Please ask your administrator to create an alias instead."))
|
||||
raise PermDeniedException(
|
||||
_("You can't define a forward to a local destination. "
|
||||
"Please ask your administrator to create an alias "
|
||||
"instead.")
|
||||
)
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
# coding: utf-8
|
||||
from functools import wraps
|
||||
from itertools import chain
|
||||
from django.db.models import Q
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import ugettext as _
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.exceptions import ModoboaException
|
||||
from modoboa.lib import parameters, events
|
||||
from modoboa.lib.exceptions import PermDeniedException
|
||||
from modoboa.lib.emailutils import split_mailbox
|
||||
from modoboa.extensions.admin.models import Alias
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, Alias
|
||||
)
|
||||
|
||||
|
||||
def needs_mailbox():
|
||||
@@ -21,7 +25,7 @@ def needs_mailbox():
|
||||
def wrapped_f(request, *args, **kwargs):
|
||||
if request.user.mailbox_set.count():
|
||||
return f(request, *args, **kwargs)
|
||||
raise ModoboaException()
|
||||
raise PermDeniedException(_("A mailbox is required"))
|
||||
return wrapped_f
|
||||
return decorator
|
||||
|
||||
@@ -46,6 +50,16 @@ def get_sort_order(qdict, default, allowed_values=None):
|
||||
|
||||
|
||||
def get_listing_page(objects, pagenum):
|
||||
"""Return specific a listing page.
|
||||
|
||||
A page contains a limited number of elements (see
|
||||
ITEMS_PER_PAGE). If the given page number is wrong, the first page
|
||||
will be always returned.
|
||||
|
||||
:param list objects: object list to paginate
|
||||
:param int pagenum: page number
|
||||
:return: a ``Page`` object
|
||||
"""
|
||||
paginator = Paginator(
|
||||
objects, int(parameters.get_admin("ITEMS_PER_PAGE", app="core"))
|
||||
)
|
||||
@@ -65,8 +79,6 @@ def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None):
|
||||
:param list grpfilter: group names filters
|
||||
:return: a queryset
|
||||
"""
|
||||
from itertools import chain
|
||||
|
||||
accounts = []
|
||||
if idtfilter is None or not idtfilter or idtfilter == "account":
|
||||
ids = user.objectaccess_set \
|
||||
@@ -104,3 +116,24 @@ def get_identities(user, searchquery=None, idtfilter=None, grpfilter=None):
|
||||
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)
|
||||
|
||||
@@ -4,8 +4,8 @@ from optparse import make_option
|
||||
from django.core.management.base import BaseCommand
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.sysutils import exec_cmd
|
||||
from modoboa.lib.exceptions import InternalError
|
||||
from modoboa.extensions.admin import AdminConsole
|
||||
from modoboa.extensions.admin.exceptions import AdminError
|
||||
from modoboa.extensions.admin.models import MailboxOperation
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ class Command(BaseCommand):
|
||||
continue
|
||||
try:
|
||||
f(ope)
|
||||
except (OperationError, AdminError) as e:
|
||||
except (OperationError, InternalError) as e:
|
||||
self.logger.critical('%s failed (reason: %s)',
|
||||
ope, str(e).encode('utf-8'))
|
||||
else:
|
||||
|
||||
@@ -75,6 +75,13 @@ class Migration(SchemaMigration):
|
||||
'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"})
|
||||
},
|
||||
u'admin.mailboxoperation': {
|
||||
'Meta': {'object_name': 'MailboxOperation'},
|
||||
'argument': ('django.db.models.fields.TextField', [], {}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.Mailbox']", 'null': 'True', 'blank': 'True'}),
|
||||
'type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
|
||||
},
|
||||
'admin.objectdates': {
|
||||
'Meta': {'object_name': 'ObjectDates'},
|
||||
'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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'
|
||||
'Domain', 'DomainAlias', 'Mailbox', 'Quota', 'Alias',
|
||||
'MailboxOperation', 'AdminObject'
|
||||
]
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import reversion
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.emailutils import split_mailbox
|
||||
from modoboa.lib.exceptions import PermDeniedException
|
||||
from modoboa.extensions.admin.exceptions import AdminError
|
||||
from .base import DatesAware
|
||||
from modoboa.lib.exceptions import PermDeniedException, BadRequest
|
||||
from .base import AdminObject
|
||||
from .domain import Domain
|
||||
from .mailbox import Mailbox
|
||||
|
||||
|
||||
class Alias(DatesAware):
|
||||
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.")
|
||||
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(
|
||||
@@ -29,6 +33,7 @@ class Alias(DatesAware):
|
||||
ugettext_lazy('enabled'),
|
||||
help_text=ugettext_lazy("Check to activate this alias")
|
||||
)
|
||||
_objectname = 'MailboxAlias'
|
||||
|
||||
class Meta:
|
||||
permissions = (
|
||||
@@ -80,8 +85,7 @@ class Alias(DatesAware):
|
||||
|
||||
def post_create(self, creator):
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
grant_access_to_object(creator, self, is_owner=True)
|
||||
events.raiseEvent("MailboxAliasCreated", creator, self)
|
||||
super(Alias, self).post_create(creator)
|
||||
if creator.is_superuser:
|
||||
for admin in self.domain.admins:
|
||||
grant_access_to_object(admin, self)
|
||||
@@ -96,14 +100,7 @@ class Alias(DatesAware):
|
||||
del kwargs['int_rcpts']
|
||||
else:
|
||||
int_rcpts = []
|
||||
if "creator" in kwargs:
|
||||
creator = kwargs["creator"]
|
||||
del kwargs["creator"]
|
||||
else:
|
||||
creator = None
|
||||
super(Alias, self).save(*args, **kwargs)
|
||||
if creator is not None:
|
||||
self.post_create(creator)
|
||||
curaliases = self.aliases.all()
|
||||
curmboxes = self.mboxes.all()
|
||||
for t in int_rcpts:
|
||||
@@ -120,13 +117,6 @@ class Alias(DatesAware):
|
||||
if not t in int_rcpts:
|
||||
self.mboxes.remove(t)
|
||||
|
||||
def delete(self):
|
||||
from modoboa.lib.permissions import ungrant_access_to_object
|
||||
|
||||
events.raiseEvent("MailboxAliasDeleted", self)
|
||||
ungrant_access_to_object(self)
|
||||
super(Alias, self).delete()
|
||||
|
||||
def get_recipients(self):
|
||||
"""Return the recipients list
|
||||
|
||||
@@ -153,12 +143,12 @@ class Alias(DatesAware):
|
||||
|
||||
"""
|
||||
if len(row) < expected_elements:
|
||||
raise AdminError(_("Invalid line: %s" % row))
|
||||
raise BadRequest(_("Invalid line: %s" % row))
|
||||
localpart, domname = split_mailbox(row[1].strip())
|
||||
try:
|
||||
domain = Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
raise AdminError(_("Domain '%s' does not exist" % domname))
|
||||
raise BadRequest(_("Domain '%s' does not exist" % domname))
|
||||
if not user.can_access(domain):
|
||||
raise PermDeniedException
|
||||
self.address = localpart
|
||||
@@ -185,7 +175,7 @@ class Alias(DatesAware):
|
||||
target = Mailbox.objects.get(address=localpart,
|
||||
domain__name=domname)
|
||||
except Mailbox.DoesNotExist:
|
||||
raise AdminError(_("Local recipient %s not found" % rcpt))
|
||||
raise BadRequest(_("Local recipient %s not found" % rcpt))
|
||||
int_rcpts += [target]
|
||||
self.save(int_rcpts=int_rcpts, ext_rcpts=ext_rcpts, creator=user)
|
||||
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
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):
|
||||
@@ -27,7 +31,7 @@ class ObjectDates(models.Model):
|
||||
obj.dates = dates
|
||||
|
||||
|
||||
class DatesAware(models.Model):
|
||||
class AdminObject(models.Model):
|
||||
"""Abstract model to support dates
|
||||
|
||||
Inherit from this model to automatically add the "dates" feature
|
||||
@@ -35,14 +39,11 @@ class DatesAware(models.Model):
|
||||
saves.
|
||||
"""
|
||||
dates = models.ForeignKey(ObjectDates)
|
||||
_objectname = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
ObjectDates.set_for_object(self)
|
||||
super(DatesAware, self).save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def creation(self):
|
||||
return self.dates.creation
|
||||
@@ -50,3 +51,29 @@ class DatesAware(models.Model):
|
||||
@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()
|
||||
|
||||
@@ -4,9 +4,9 @@ from django.db.models.manager import Manager
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.contrib.contenttypes import generic
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.lib.exceptions import BadRequest
|
||||
from modoboa.core.models import User, ObjectAccess
|
||||
from modoboa.extensions.admin.exceptions import AdminError
|
||||
from .base import DatesAware
|
||||
from .base import AdminObject
|
||||
|
||||
|
||||
class DomainManager(Manager):
|
||||
@@ -22,7 +22,9 @@ class DomainManager(Manager):
|
||||
return self.get_query_set().filter(owners__user=admin)
|
||||
|
||||
|
||||
class Domain(DatesAware):
|
||||
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(
|
||||
@@ -56,6 +58,10 @@ class Domain(DatesAware):
|
||||
def mbalias_count(self):
|
||||
return 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
|
||||
@@ -64,6 +70,10 @@ class Domain(DatesAware):
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -80,11 +90,15 @@ class Domain(DatesAware):
|
||||
grant_access_to_object(account, al)
|
||||
|
||||
def remove_admin(self, account):
|
||||
"""Remove an administrator for this domain
|
||||
"""Remove an administrator of this domain.
|
||||
|
||||
:param User account: the administrotor to remove
|
||||
:param User account: administrator to remove
|
||||
"""
|
||||
from modoboa.lib.permissions import ungrant_access_to_object
|
||||
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"):
|
||||
@@ -92,60 +106,55 @@ class Domain(DatesAware):
|
||||
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)
|
||||
ungrant_access_to_object(al, account)
|
||||
|
||||
def delete(self, fromuser, keepdir=False):
|
||||
from modoboa.lib.permissions import \
|
||||
ungrant_access_to_object, ungrant_access_to_objects
|
||||
"""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.mailbox_set.count():
|
||||
Quota.objects.filter(username__contains='@%s' % self.name).delete()
|
||||
events.raiseEvent("DeleteMailbox", self.mailbox_set.all())
|
||||
ungrant_access_to_objects(self.mailbox_set.all())
|
||||
if self.alias_set.count():
|
||||
events.raiseEvent("MailboxAliasDelete", self.alias_set.all())
|
||||
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)
|
||||
events.raiseEvent("DeleteDomain", self)
|
||||
ungrant_access_to_object(self)
|
||||
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 post_create(self, creator):
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
grant_access_to_object(creator, self, is_owner=True)
|
||||
events.raiseEvent("CreateDomain", creator, self)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if "creator" in kwargs:
|
||||
creator = kwargs["creator"]
|
||||
del kwargs["creator"]
|
||||
else:
|
||||
creator = None
|
||||
super(Domain, self).save(*args, **kwargs)
|
||||
if creator is not None:
|
||||
self.post_create(creator)
|
||||
|
||||
def from_csv(self, user, row):
|
||||
if len(row) < 4:
|
||||
raise AdminError(_("Invalid line"))
|
||||
raise BadRequest(_("Invalid line"))
|
||||
self.name = row[1].strip()
|
||||
try:
|
||||
self.quota = int(row[2].strip())
|
||||
except ValueError:
|
||||
raise AdminError(_("Invalid quota value for domain '%s'" % self.name))
|
||||
raise BadRequest(_("Invalid quota value for domain '%s'" % self.name))
|
||||
self.enabled = (row[3].strip() == 'True')
|
||||
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,13 +1,12 @@
|
||||
import reversion
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from modoboa.lib import events
|
||||
from modoboa.extensions.admin.exceptions import AdminError
|
||||
from .base import DatesAware
|
||||
from modoboa.lib.exceptions import BadRequest, Conflict
|
||||
from .base import AdminObject
|
||||
from .domain import Domain
|
||||
|
||||
|
||||
class DomainAlias(DatesAware):
|
||||
class DomainAlias(AdminObject):
|
||||
name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True,
|
||||
help_text=ugettext_lazy("The alias name"))
|
||||
target = models.ForeignKey(
|
||||
@@ -28,27 +27,6 @@ class DomainAlias(DatesAware):
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
||||
def post_create(self, creator):
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
grant_access_to_object(creator, self, is_owner=True)
|
||||
events.raiseEvent("DomainAliasCreated", creator, self)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if "creator" in kwargs:
|
||||
creator = kwargs["creator"]
|
||||
del kwargs["creator"]
|
||||
else:
|
||||
creator = None
|
||||
super(DomainAlias, self).save(*args, **kwargs)
|
||||
if creator is not None:
|
||||
self.post_create(creator)
|
||||
|
||||
def delete(self):
|
||||
from modoboa.lib.permissions import ungrant_access_to_object
|
||||
events.raiseEvent("DomainAliasDeleted", self)
|
||||
ungrant_access_to_object(self)
|
||||
super(DomainAlias, self).delete()
|
||||
|
||||
def from_csv(self, user, row):
|
||||
"""Create a domain alias from a CSV row
|
||||
|
||||
@@ -58,13 +36,19 @@ class DomainAlias(DatesAware):
|
||||
:param row: a list containing the alias definition
|
||||
"""
|
||||
if len(row) < 4:
|
||||
raise AdminError(_("Invalid line"))
|
||||
raise BadRequest(_("Invalid line"))
|
||||
self.name = row[1].strip()
|
||||
try:
|
||||
Domain.objects.get(name=self.name)
|
||||
except Domain.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
raise Conflict(_("A domain %s already exists" % self.name))
|
||||
domname = row[2].strip()
|
||||
try:
|
||||
self.target = Domain.objects.get(name=domname)
|
||||
except Domain.DoesNotExist:
|
||||
raise AdminError(_("Unknown domain %s" % domname))
|
||||
raise BadRequest(_("Unknown domain %s" % domname))
|
||||
self.enabled = row[3].strip() == 'True'
|
||||
self.save(creator=user)
|
||||
|
||||
|
||||
@@ -9,10 +9,10 @@ from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.db.models.signals import pre_delete
|
||||
from django.dispatch import receiver
|
||||
from modoboa.lib import parameters, events
|
||||
from modoboa.lib.exceptions import BadRequest, InternalError
|
||||
from modoboa.lib.sysutils import exec_cmd
|
||||
from modoboa.core.models import User
|
||||
from modoboa.extensions.admin.exceptions import AdminError
|
||||
from .base import DatesAware
|
||||
from .base import AdminObject
|
||||
from .domain import Domain
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class MailboxManager(Manager):
|
||||
return self.get_query_set().filter(qf)
|
||||
|
||||
|
||||
class Mailbox(DatesAware):
|
||||
class Mailbox(AdminObject):
|
||||
address = models.CharField(
|
||||
ugettext_lazy('address'), max_length=252,
|
||||
help_text=ugettext_lazy("Mailbox address (without the @domain.tld part)")
|
||||
@@ -110,7 +110,7 @@ class Mailbox(DatesAware):
|
||||
"doveadm user %s -f home" % self.full_address, **options
|
||||
)
|
||||
if code:
|
||||
raise AdminError(_("Failed to retrieve mailbox location (%s)" % output))
|
||||
raise InternalError(_("Failed to retrieve mailbox location (%s)" % output))
|
||||
self.__mail_home = output.strip()
|
||||
return self.__mail_home
|
||||
|
||||
@@ -158,13 +158,13 @@ class Mailbox(DatesAware):
|
||||
else:
|
||||
self.quota = 0
|
||||
elif int(value) > self.domain.quota and not override_rules:
|
||||
raise AdminError(
|
||||
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 AdminError(_("A quota is required"))
|
||||
raise BadRequest(_("A quota is required"))
|
||||
|
||||
def get_quota(self):
|
||||
"""Get quota limit.
|
||||
@@ -186,8 +186,7 @@ class Mailbox(DatesAware):
|
||||
|
||||
def post_create(self, creator):
|
||||
from modoboa.lib.permissions import grant_access_to_object
|
||||
grant_access_to_object(creator, self, True)
|
||||
events.raiseEvent("CreateMailbox", creator, self)
|
||||
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
|
||||
@@ -199,14 +198,7 @@ class Mailbox(DatesAware):
|
||||
grant_access_to_object(admin, self.user)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if "creator" in kwargs:
|
||||
creator = kwargs["creator"]
|
||||
del kwargs["creator"]
|
||||
else:
|
||||
creator = None
|
||||
super(Mailbox, self).save(*args, **kwargs)
|
||||
if creator is not None:
|
||||
self.post_create(creator)
|
||||
try:
|
||||
q = self.quota_value
|
||||
except Quota.DoesNotExist:
|
||||
@@ -246,7 +238,7 @@ def mailbox_deleted_handler(sender, **kwargs):
|
||||
from modoboa.lib.permissions import ungrant_access_to_object
|
||||
|
||||
mb = kwargs['instance']
|
||||
events.raiseEvent("DeleteMailbox", mb)
|
||||
events.raiseEvent("MailboxDeleted", mb)
|
||||
ungrant_access_to_object(mb)
|
||||
for alias in mb.alias_set.all():
|
||||
alias.mboxes.remove(mb)
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/**
|
||||
* @module admin
|
||||
* @desc Utility classes for the admin application
|
||||
*/
|
||||
|
||||
var Admin = function(options) {
|
||||
Listing.call(this, options);
|
||||
};
|
||||
@@ -12,6 +17,7 @@ Admin.prototype = {
|
||||
Listing.prototype.initialize.call(this, options);
|
||||
this.options = $.extend({}, this.defaults, this.options);
|
||||
this.options.defcallback = $.proxy(this.list_cb, this);
|
||||
this.tag_handlers = {};
|
||||
|
||||
this.navobj = new History(this.options);
|
||||
|
||||
@@ -37,12 +43,61 @@ Admin.prototype = {
|
||||
list_cb: function(data) {
|
||||
$("#listing").html(data.table);
|
||||
this.update_listing(data);
|
||||
$("a.filter").click($.proxy(this.filter_by_tag, this));
|
||||
},
|
||||
|
||||
listen: function() {
|
||||
$("#searchform").submit($.proxy(this.do_search, this));
|
||||
},
|
||||
|
||||
register_tag_handler: function(name, handler) {
|
||||
this.tag_handlers[name] = handler;
|
||||
if (this.navobj.getparam(name + "filter") != undefined) {
|
||||
var text = this.navobj.getparam(name + "filter");
|
||||
$("#searchform").parent().after(this.make_tag(text, name));
|
||||
}
|
||||
},
|
||||
|
||||
generic_tag_handler: function(tag, $link) {
|
||||
if (this.navobj.getparam(tag + "filter") == undefined && $link.hasClass(tag)) {
|
||||
var text = $link.attr("name");
|
||||
this.navobj.setparam(tag + "filter", text).update();
|
||||
$("#searchform").parent().after(this.make_tag(text, tag));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
make_tag: function(text, type) {
|
||||
var $tag = $("<a />", {"name": type, "class" : "btn btn-mini", "html": text});
|
||||
var $i = $("<i />", {"class" : "icon-remove"}).prependTo($tag);
|
||||
|
||||
$tag.click($.proxy(this.remove_tag, this));
|
||||
return $tag;
|
||||
},
|
||||
|
||||
remove_tag: function(e) {
|
||||
var $tag = $(e.target);
|
||||
|
||||
if ($tag.is("i")) {
|
||||
$tag = $tag.parent();
|
||||
}
|
||||
e.preventDefault();
|
||||
this.navobj.delparam($tag.attr("name") + "filter").update();
|
||||
$tag.remove();
|
||||
},
|
||||
|
||||
filter_by_tag: function(e) {
|
||||
var $link = $(e.target);
|
||||
e.preventDefault();
|
||||
|
||||
for (var name in this.tag_handlers) {
|
||||
if (this.tag_handlers[name].apply(this, [name, $link])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
do_search: function(e) {
|
||||
e.preventDefault();
|
||||
var squery = $("#searchquery").val();
|
||||
@@ -70,7 +125,7 @@ Admin.prototype = {
|
||||
$("#import_status").css("display", "none");
|
||||
if (status == "ok") {
|
||||
$("#modalbox").modal('hide');
|
||||
this.reload_listing({respmsg: msg});
|
||||
this.reload_listing(msg);
|
||||
} else {
|
||||
$("#import_result").addClass("alert alert-error");
|
||||
$("#import_result").html(msg);
|
||||
@@ -88,8 +143,8 @@ Admin.prototype = {
|
||||
|
||||
reload_listing: function(data) {
|
||||
this.navobj.update(true);
|
||||
if (data.respmsg) {
|
||||
$("body").notify("success", data.respmsg, 2000);
|
||||
if (data) {
|
||||
$("body").notify("success", data, 2000);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -104,6 +159,11 @@ var Domains = function(options) {
|
||||
};
|
||||
|
||||
Domains.prototype = {
|
||||
initialize: function(options) {
|
||||
Admin.prototype.initialize.call(this, options);
|
||||
this.register_tag_handler("dom", this.generic_tag_handler);
|
||||
},
|
||||
|
||||
list_cb: function(data) {
|
||||
Admin.prototype.list_cb.call(this, data);
|
||||
var deloptions = (data.handle_mailboxes)
|
||||
@@ -120,6 +180,7 @@ Domains.prototype = {
|
||||
checkboxes: deloptions,
|
||||
success_cb: $.proxy(this.reload_listing, this)
|
||||
});
|
||||
$(document).trigger('domain_listing_refresh');
|
||||
},
|
||||
|
||||
change_inputs_state: function(value) {
|
||||
@@ -152,11 +213,11 @@ Domains.prototype = {
|
||||
|
||||
domadminsform_init: function() {
|
||||
$("a[name=removeperm]").click(function(e) {
|
||||
var $tr = $(this).parent().parent();
|
||||
var $tr = $(this).parents('tr');
|
||||
simple_ajax_request.apply(this, [e, {
|
||||
ok_cb: function(resp) {
|
||||
$tr.remove();
|
||||
if (!$("#domadmins").children("tr").length) {
|
||||
if (!$("#domadmins").find("tr").length) {
|
||||
$("#admins").html('<div class="alert alert-info">'
|
||||
+ gettext("No domain administrator defined") + "</div>");
|
||||
}
|
||||
@@ -174,7 +235,6 @@ Domains.prototype = {
|
||||
1: this.optionsform_prefill
|
||||
},
|
||||
error_callbacks: {
|
||||
1: this.generalform_init,
|
||||
2: $.proxy(this.optionsform_init, this)
|
||||
},
|
||||
success_callback: $.proxy(this.reload_listing, this)
|
||||
@@ -194,7 +254,6 @@ Domains.prototype = {
|
||||
}, this));
|
||||
$(document).trigger('domform_init');
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Domains.prototype = $.extend({}, Admin.prototype, Domains.prototype);
|
||||
@@ -209,14 +268,8 @@ var Identities = function(options) {
|
||||
Identities.prototype = {
|
||||
initialize: function(options) {
|
||||
Admin.prototype.initialize.call(this, options);
|
||||
if (this.navobj.getparam("idtfilter") != undefined) {
|
||||
var text = this.navobj.getparam("idtfilter");
|
||||
$("#searchform").parent().after(this.make_tag(text, "idt"));
|
||||
}
|
||||
if (this.navobj.getparam("grpfilter") != undefined) {
|
||||
var text = this.navobj.getparam("grpfilter");
|
||||
$("#searchform").parent().after(this.make_tag(text, "grp"));
|
||||
}
|
||||
this.register_tag_handler("idt", this.generic_tag_handler);
|
||||
this.register_tag_handler("grp", this.grp_tag_handler);
|
||||
},
|
||||
|
||||
listen: function() {
|
||||
@@ -237,7 +290,6 @@ Identities.prototype = {
|
||||
if (data.handle_mailboxes == "yes") {
|
||||
deloptions = {keepdir: gettext("Do not delete mailbox directory")};
|
||||
}
|
||||
$("a.filter").click($.proxy(this.filter_by_tag, this));
|
||||
|
||||
$("a[name=delaccount]").confirm({
|
||||
question: function() { return this.$element.attr('title'); },
|
||||
@@ -252,46 +304,20 @@ Identities.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
make_tag: function(text, type) {
|
||||
var $tag = $("<a />", {"name": type, "class" : "btn btn-mini", "html": text});
|
||||
var $i = $("<i />", {"class" : "icon-remove"}).prependTo($tag);
|
||||
|
||||
$tag.click($.proxy(this.remove_tag, this));
|
||||
return $tag;
|
||||
},
|
||||
|
||||
remove_tag: function(e) {
|
||||
var $tag = $(e.target);
|
||||
|
||||
if ($tag.is("i")) {
|
||||
$tag = $tag.parent();
|
||||
}
|
||||
e.preventDefault();
|
||||
this.navobj.delparam($tag.attr("name") + "filter").update();
|
||||
$tag.remove();
|
||||
},
|
||||
|
||||
filter_by_tag: function(e) {
|
||||
var $link = $(e.target);
|
||||
e.preventDefault();
|
||||
|
||||
if (this.navobj.getparam("idtfilter") == undefined && $link.hasClass("idt")) {
|
||||
var text = $link.attr("name");
|
||||
this.navobj.setparam("idtfilter", text).update();
|
||||
$("#searchform").parent().after(this.make_tag(text, "idt"));
|
||||
return;
|
||||
}
|
||||
if (this.navobj.getparam("grpfilter") == undefined && $link.hasClass("grp")) {
|
||||
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("grpfilter", text)
|
||||
.setparam(tag + "filter", text)
|
||||
.update();
|
||||
if ($("a[name=idt]").length == 0) {
|
||||
$("#searchform").parent().after(this.make_tag("account", "idt"));
|
||||
}
|
||||
$("#searchform").parent().after(this.make_tag(text, "grp"));
|
||||
$("#searchform").parent().after(this.make_tag(text, tag));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
simpleuser_mode: function() {
|
||||
@@ -368,20 +394,15 @@ Identities.prototype = {
|
||||
transition_callbacks: {
|
||||
1: this.mailform_prefill
|
||||
},
|
||||
error_callbacks: {
|
||||
1: $.proxy(this.generalform_init, this),
|
||||
2: $.proxy(this.mailform_init, this)
|
||||
},
|
||||
success_callback: $.proxy(this.reload_listing, this)
|
||||
});
|
||||
},
|
||||
|
||||
editaccount_cb: function() {
|
||||
this.accountform_init();
|
||||
$('.submit').one('click', $.proxy(function(e) {
|
||||
$('.submit').on('click', $.proxy(function(e) {
|
||||
simple_ajax_form_post(e, {
|
||||
formid: "accountform",
|
||||
error_cb: $.proxy(this.editaccount_cb, this),
|
||||
reload_on_success: false,
|
||||
success_cb: $.proxy(this.reload_listing, this)
|
||||
});
|
||||
@@ -394,10 +415,9 @@ Identities.prototype = {
|
||||
choices: get_domains_list
|
||||
});
|
||||
$("#id_recipients").dynamic_input();
|
||||
$(".submit").one('click', $.proxy(function(e) {
|
||||
$(".submit").on('click', $.proxy(function(e) {
|
||||
simple_ajax_form_post(e, {
|
||||
formid: "aliasform",
|
||||
error_cb: $.proxy(this.aliasform_cb, this),
|
||||
reload_on_success: false,
|
||||
success_cb: $.proxy(this.reload_listing, this)
|
||||
});
|
||||
|
||||
@@ -1,45 +1,6 @@
|
||||
# coding: utf-8
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from modoboa.lib import tables
|
||||
from templatetags.admin_tags import domain_actions
|
||||
|
||||
|
||||
class DomainsTable(tables.Table):
|
||||
tableid = "objects_table"
|
||||
idkey = "id"
|
||||
styles = "table"
|
||||
|
||||
name = tables.LinkColumn(
|
||||
"name", label=ugettext_lazy("Name"),
|
||||
urlpattern="modoboa.extensions.admin.views.domain.editdomain",
|
||||
title=_("Edit domain"), modal=True, modalcb="admin.domainform_cb",
|
||||
sortable=True, sort_order="name"
|
||||
)
|
||||
domaliases = tables.Column(
|
||||
"domainalias_set", label=ugettext_lazy("Alias(es)"), safe=True,
|
||||
sortable=True, sort_order="domainalias__name"
|
||||
)
|
||||
actions = tables.ActionColumn("actions", label=ugettext_lazy("Actions"),
|
||||
defvalue=domain_actions)
|
||||
|
||||
cols_order = ["name", "domaliases", "actions"]
|
||||
|
||||
def __init__(self, request, doms):
|
||||
super(DomainsTable, self).__init__(request)
|
||||
self.populate(self._rows_from_model(doms))
|
||||
|
||||
def parse_domainalias_set(self, aliases):
|
||||
if not len(aliases.all()):
|
||||
return "---"
|
||||
res = ""
|
||||
for da in aliases.all():
|
||||
res += "%s<br/>" % da.name
|
||||
return res
|
||||
|
||||
def row_class(self, request, domain):
|
||||
if not domain.enabled:
|
||||
return "muted"
|
||||
return ""
|
||||
|
||||
|
||||
class ExtensionsTable(tables.Table):
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
{% extends "common/generic_modal_form.html" %}
|
||||
{% load lib_tags %}
|
||||
{% extends "common/generic_modal_form.html" %}{% load lib_tags %}
|
||||
{% block formbody %}
|
||||
{% render_field form.email %}
|
||||
{% render_fields_group form "recipients" %}
|
||||
@@ -1,4 +1,5 @@
|
||||
{% load i18n lib_tags %}
|
||||
{% load i18n lib_tags %}{% if form.fields %}
|
||||
{% render_field form.create_dom_admin %}
|
||||
{% render_field_appended form.dom_admin_username "@" %}
|
||||
{% render_field form.create_aliases %}
|
||||
{% else %}<div class="alert alert-warning">{% trans "You can't create a new domain administrator" %}</div>{% endif %}
|
||||
|
||||
@@ -7,5 +7,6 @@ $(document).ready(function() {
|
||||
admin = new Domains();
|
||||
});
|
||||
</script>
|
||||
{% extra_static_content "domains" user %}
|
||||
{% endblock %}
|
||||
{% block actions %}{% domains_menu "domains" user %}{% endblock %}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{% load i18n admin_tags lib_tags %}
|
||||
<table id="{{ tableid }}" class="table {{ styles }}">
|
||||
<thead>
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for domain in domains %}
|
||||
<tr id="{{ domain.id }}" class="{% if not domain.enabled %}muted{% endif %}" name="{{ forloop.counter }}">
|
||||
<td name="name">{% domain_modify_link domain %}</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 %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -20,5 +20,6 @@ $(document).ready(function() {
|
||||
admin = new Identities();
|
||||
});
|
||||
</script>
|
||||
{% extra_static_content "identities" user %}
|
||||
{% endblock %}
|
||||
{% block actions %}{% identities_menu user %}{% endblock %}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "admin/header.html" %}{% load i18n admin_tags lib_tags %}
|
||||
{% extends "admin/header.html" %}{% load i18n admin_tags %}
|
||||
{% block html_head %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/jquery.sortable.css" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% load lib_tags %}
|
||||
{% render_field form.email %}
|
||||
{% include "common/generic_field.html" with field=form.quota help_display_mode="tooltip" appended_text=_("MB") activator=True activator_value=form.quota_act.value deactivate_if_empty=False activator_label=_("Use domain default value") %}
|
||||
{% render_extra_fields form %}
|
||||
{% render_fields_group form "aliases" %}
|
||||
|
||||
@@ -15,6 +15,16 @@ genders = {
|
||||
|
||||
@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)
|
||||
"""
|
||||
if not user.has_perm("admin.add_domain"):
|
||||
return ""
|
||||
|
||||
@@ -25,6 +35,9 @@ def domains_menu(selection, user):
|
||||
"modal": True,
|
||||
"modalcb": "admin.newdomain_cb",
|
||||
"url": reverse("modoboa.extensions.admin.views.domain.newdomain")},
|
||||
]
|
||||
entries += events.raiseQueryEvent("ExtraDomainMenuEntries", user)
|
||||
entries += [
|
||||
{"name": "import",
|
||||
"label": _("Import"),
|
||||
"img": "icon-folder-open",
|
||||
@@ -111,23 +124,28 @@ def identities_menu(user):
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def domain_actions(user, domid):
|
||||
from modoboa.extensions.admin.models import Domain
|
||||
def domain_actions(user, domain):
|
||||
actions = []
|
||||
if domain.__class__.__name__ == 'Domain':
|
||||
actions = [
|
||||
{"name": "listidentities",
|
||||
"url": reverse("modoboa.extensions.admin.views.identity.identities") + "#list/?searchquery=@%s" % domain.name,
|
||||
"title": _("View the domain's identities"),
|
||||
"img": "icon-user"}
|
||||
]
|
||||
if user.has_perm("admin.delete_domain"):
|
||||
actions.append({
|
||||
"name": "deldomain",
|
||||
"url": reverse(
|
||||
"modoboa.extensions.admin.views.domain.deldomain",
|
||||
args=[domain.id]
|
||||
),
|
||||
"title": _("Delete %s?" % domain.name),
|
||||
"img": "icon-trash"
|
||||
})
|
||||
else:
|
||||
actions = events.raiseQueryEvent('GetDomainActions', user, domain)
|
||||
|
||||
domain = Domain.objects.get(pk=domid)
|
||||
actions = [
|
||||
{"name": "listidentities",
|
||||
"url": reverse("modoboa.extensions.admin.views.identity.identities") + "#list/?searchquery=@%s" % domain.name,
|
||||
"title": _("View the domain's identities"),
|
||||
"img": "icon-user"}
|
||||
]
|
||||
if user.has_perm("admin.delete_domain"):
|
||||
actions.append({
|
||||
"name": "deldomain",
|
||||
"url": reverse("modoboa.extensions.admin.views.domain.deldomain", args=[domid]),
|
||||
"title": _("Delete %s?" % domain.name),
|
||||
"img": "icon-trash"
|
||||
})
|
||||
return render_actions(actions)
|
||||
|
||||
|
||||
@@ -153,8 +171,48 @@ def identity_actions(user, ident):
|
||||
return render_actions(actions)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def domain_modify_link(domain):
|
||||
linkdef = {"label": domain.name, "modal": True}
|
||||
if domain.__class__.__name__ == "Domain":
|
||||
linkdef["url"] = reverse(
|
||||
"modoboa.extensions.admin.views.domain.editdomain",
|
||||
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(
|
||||
|
||||
@@ -4,9 +4,11 @@ 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'
|
||||
'ExportTestCase', 'PasswordSchemesTestCase', 'ForwardTestCase'
|
||||
]
|
||||
|
||||
@@ -19,9 +19,9 @@ class AccountTestCase(ModoTestCase):
|
||||
username="tester@test.com", first_name="Tester", last_name="Toto",
|
||||
password1="toto", password2="toto", role="SimpleUsers",
|
||||
quota_act=True,
|
||||
is_active=True, email="tester@test.com", stepid=2
|
||||
is_active=True, email="tester@test.com", stepid='step2'
|
||||
)
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.newaccount"),
|
||||
values
|
||||
)
|
||||
@@ -39,26 +39,26 @@ class AccountTestCase(ModoTestCase):
|
||||
self.assertEqual(mb.domain.mailbox_count, 3)
|
||||
|
||||
values["username"] = "pouet@test.com"
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[account.id]),
|
||||
values
|
||||
)
|
||||
mb = Mailbox.objects.get(pk=mb.id)
|
||||
self.assertEqual(mb.full_address, "pouet@test.com")
|
||||
self.check_ajax_get(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.delaccount",
|
||||
args=[account.id]),
|
||||
{}
|
||||
)
|
||||
|
||||
def _set_quota(self, email, value, expected_status="ok"):
|
||||
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.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[account.id]),
|
||||
values, status=expected_status
|
||||
@@ -70,7 +70,7 @@ class AccountTestCase(ModoTestCase):
|
||||
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, "ko")
|
||||
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)
|
||||
@@ -92,25 +92,24 @@ class PermissionsTestCase(ModoTestCase):
|
||||
self.clt.logout()
|
||||
|
||||
def test_domain_admins(self):
|
||||
self.values["role"] = "DomainAdmins"
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[self.user.id]),
|
||||
self.values
|
||||
)
|
||||
self.assertEqual(self.user.group == "DomainAdmins", True)
|
||||
self.assertEqual(self.user.group, "DomainAdmins")
|
||||
|
||||
self.values["role"] = "SimpleUsers"
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[self.user.id]),
|
||||
self.values
|
||||
)
|
||||
self.assertEqual(self.user.group == 'DomainAdmins', False)
|
||||
self.assertNotEqual(self.user.group, 'DomainAdmins')
|
||||
|
||||
def test_superusers(self):
|
||||
self.values["role"] = "SuperAdmins"
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[self.user.id]),
|
||||
self.values
|
||||
@@ -118,7 +117,7 @@ class PermissionsTestCase(ModoTestCase):
|
||||
self.assertEqual(User.objects.get(username="user@test.com").is_superuser, True)
|
||||
|
||||
self.values["role"] = "SimpleUsers"
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[self.user.id]),
|
||||
self.values
|
||||
@@ -135,7 +134,7 @@ class PermissionsTestCase(ModoTestCase):
|
||||
password1="", password2="",
|
||||
quota=10, is_active=True, email="admin@test.com"
|
||||
)
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[admin.id]),
|
||||
values
|
||||
@@ -144,7 +143,7 @@ class PermissionsTestCase(ModoTestCase):
|
||||
self.assertEqual(admin.can_access(Domain.objects.get(name="test.com")), True)
|
||||
|
||||
values["role"] = "SuperAdmins"
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[admin.id]),
|
||||
values
|
||||
@@ -178,25 +177,23 @@ class PermissionsTestCase(ModoTestCase):
|
||||
username="superadmin2@test.com", first_name="Super",
|
||||
last_name="Admin", password1="toto", password2="toto",
|
||||
role="SuperAdmins", is_active=True,
|
||||
email="superadmin2@test.com", stepid=2
|
||||
email="superadmin2@test.com", stepid='step2'
|
||||
)
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.newaccount"),
|
||||
values
|
||||
)
|
||||
account = User.objects.get(username="superadmin2@test.com")
|
||||
self.clt.logout()
|
||||
self.clt.login(username="admin@test.com", password="toto")
|
||||
self.check_ajax_get(
|
||||
self.ajax_get(
|
||||
reverse("modoboa.extensions.admin.views.identity.delaccount",
|
||||
args=[account.id]),
|
||||
{},
|
||||
status="ko", respmsg="Permission denied"
|
||||
{}, 403
|
||||
)
|
||||
self.check_ajax_get(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.delaccount",
|
||||
args=[account.id]), {},
|
||||
status="ko", respmsg="Permission denied"
|
||||
args=[account.id]), {}, 403
|
||||
)
|
||||
|
||||
def test_domainadmin_dlist_local_domain_not_owned(self):
|
||||
@@ -208,6 +205,6 @@ class PermissionsTestCase(ModoTestCase):
|
||||
recipients_1="user@test2.com",
|
||||
enabled=True
|
||||
)
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.alias.newdlist"), values
|
||||
)
|
||||
|
||||
@@ -21,7 +21,7 @@ class AliasTestCase(ModoTestCase):
|
||||
is_active=user.is_active, email="user@test.com",
|
||||
aliases="toto@test.com", aliases_1="titi@test.com"
|
||||
)
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[user.id]),
|
||||
values
|
||||
@@ -29,7 +29,7 @@ class AliasTestCase(ModoTestCase):
|
||||
self.assertEqual(user.mailbox_set.all()[0].alias_set.count(), 2)
|
||||
|
||||
del values["aliases_1"]
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[user.id]),
|
||||
values
|
||||
@@ -42,7 +42,7 @@ class AliasTestCase(ModoTestCase):
|
||||
recipients_1="admin@test.com",
|
||||
recipients_2="ext@titi.com",
|
||||
enabled=True)
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.alias.newdlist"), values
|
||||
)
|
||||
user = User.objects.get(username="user@test.com")
|
||||
@@ -53,14 +53,14 @@ class AliasTestCase(ModoTestCase):
|
||||
dlist = Alias.objects.get(address="all", domain__name="test.com")
|
||||
self.assertEqual(len(dlist.get_recipients()), 3)
|
||||
del values["recipients_1"]
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.alias.editalias",
|
||||
args=[dlist.id]),
|
||||
values
|
||||
)
|
||||
self.assertEqual(dlist.get_recipients_count(), 2)
|
||||
|
||||
self.check_ajax_get(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.alias.delalias") + "?selection=%d" \
|
||||
% dlist.id, {}
|
||||
)
|
||||
@@ -69,21 +69,21 @@ class AliasTestCase(ModoTestCase):
|
||||
|
||||
def test_forward(self):
|
||||
values = dict(email="forward2@test.com", recipients="rcpt@dest.com")
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.alias.newforward"), values
|
||||
)
|
||||
fwd = Alias.objects.get(address="forward2", domain__name="test.com")
|
||||
self.assertEqual(fwd.get_recipients_count(), 1)
|
||||
|
||||
values["recipients"] = "rcpt2@dest.com"
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.alias.editalias",
|
||||
args=[fwd.id]),
|
||||
values
|
||||
)
|
||||
self.assertEqual(fwd.get_recipients_count(), 1)
|
||||
|
||||
self.check_ajax_get(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.alias.delalias") + "?selection=%d" \
|
||||
% fwd.id, {}
|
||||
)
|
||||
@@ -92,7 +92,7 @@ class AliasTestCase(ModoTestCase):
|
||||
|
||||
def test_forward_and_local_copies(self):
|
||||
values = dict(email="user@test.com", recipients="rcpt@dest.com")
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.alias.newforward"), values
|
||||
)
|
||||
fwd = Alias.objects.get(address="user", domain__name="test.com")
|
||||
@@ -100,7 +100,7 @@ class AliasTestCase(ModoTestCase):
|
||||
|
||||
values["recipients"] = "rcpt@dest.com"
|
||||
values["recipients_1"] = "user@test.com"
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.alias.editalias", args=[fwd.id]),
|
||||
values
|
||||
)
|
||||
|
||||
@@ -19,9 +19,9 @@ class DomainTestCase(ModoTestCase):
|
||||
"""
|
||||
values = {
|
||||
"name": "pouet.com", "quota": 100, "create_dom_admin": "no",
|
||||
"stepid": 2
|
||||
"stepid": 'step2'
|
||||
}
|
||||
self.check_ajax_post(reverse("modoboa.extensions.admin.views.domain.newdomain"), values)
|
||||
self.ajax_post(reverse("modoboa.extensions.admin.views.domain.newdomain"), values)
|
||||
dom = Domain.objects.get(name="pouet.com")
|
||||
self.assertEqual(dom.name, "pouet.com")
|
||||
self.assertEqual(dom.quota, 100)
|
||||
@@ -35,9 +35,9 @@ class DomainTestCase(ModoTestCase):
|
||||
values = {
|
||||
"name": "pouet.com", "quota": 100, "create_dom_admin": "yes",
|
||||
"dom_admin_username": "toto", "create_aliases": "yes",
|
||||
"stepid": 2
|
||||
"stepid": 'step2'
|
||||
}
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.domain.newdomain"),
|
||||
values
|
||||
)
|
||||
@@ -53,9 +53,9 @@ class DomainTestCase(ModoTestCase):
|
||||
values = {
|
||||
"name": "pouet.com", "quota": 0, "create_dom_admin": "yes",
|
||||
"dom_admin_username": "toto", "create_aliases": "yes",
|
||||
"stepid": 2
|
||||
"stepid": 'step2'
|
||||
}
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.domain.newdomain"),
|
||||
values
|
||||
)
|
||||
@@ -75,7 +75,7 @@ class DomainTestCase(ModoTestCase):
|
||||
"name": "pouet.com", "quota": 100, "enabled": True
|
||||
}
|
||||
dom = Domain.objects.get(name="test.com")
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.domain.editdomain",
|
||||
args=[dom.id]),
|
||||
values
|
||||
@@ -88,7 +88,7 @@ class DomainTestCase(ModoTestCase):
|
||||
"""Test the removal of a domain
|
||||
"""
|
||||
dom = Domain.objects.get(name="test.com")
|
||||
self.check_ajax_get(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.domain.deldomain",
|
||||
args=[dom.id]),
|
||||
{}
|
||||
|
||||
@@ -31,7 +31,7 @@ class DomainAliasTestCase(ModoTestCase):
|
||||
dom = Domain.objects.get(name="test.com")
|
||||
values = dict(name=dom.name, quota=dom.quota, enabled=dom.enabled,
|
||||
aliases="domalias.net", aliases_1="domalias.com")
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.domain.editdomain",
|
||||
args=[dom.id]),
|
||||
values
|
||||
@@ -39,7 +39,7 @@ class DomainAliasTestCase(ModoTestCase):
|
||||
self.assertEqual(dom.domainalias_set.count(), 2)
|
||||
|
||||
del values["aliases_1"]
|
||||
self.check_ajax_post(
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.domain.editdomain",
|
||||
args=[dom.id]),
|
||||
values
|
||||
|
||||
@@ -22,7 +22,7 @@ class ExportTestCase(ModoTestCase):
|
||||
|
||||
def test_export_identities(self):
|
||||
response = self.__export_identities()
|
||||
self.assertEqual(response.content, "account;admin;{CRYPT}dTTsGDkA5ZHKg;;;True;SuperAdmins;\r\naccount;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;test2.com\r\naccount;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com\r\nalias;alias@test.com;False;user@test.com\r\nforward;forward@test.com;False;user@external.com\r\ndlist;postmaster@test.com;False;toto@titi.com;test@truc.fr\r\n")
|
||||
self.assertEqual(response.content, "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")
|
||||
|
||||
def test_export_simpleusers(self):
|
||||
response = self.__export_identities(
|
||||
@@ -30,7 +30,7 @@ class ExportTestCase(ModoTestCase):
|
||||
)
|
||||
self.assertEqual(
|
||||
response.content.strip(),
|
||||
"account;user@test.com;{PLAIN}toto;;;True;SimpleUsers;user@test.com\r\naccount;user@test2.com;{PLAIN}toto;;;True;SimpleUsers;user@test2.com"
|
||||
"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"
|
||||
)
|
||||
|
||||
def test_export_superadmins(self):
|
||||
@@ -39,7 +39,7 @@ class ExportTestCase(ModoTestCase):
|
||||
)
|
||||
self.assertEqual(
|
||||
response.content.strip(),
|
||||
"account;admin;{CRYPT}dTTsGDkA5ZHKg;;;True;SuperAdmins;"
|
||||
"account;admin;{CRYPT}dTTsGDkA5ZHKg;;;True;SuperAdmins;;"
|
||||
)
|
||||
|
||||
def test_export_domainadmins(self):
|
||||
@@ -48,26 +48,26 @@ class ExportTestCase(ModoTestCase):
|
||||
)
|
||||
self.assertEqual(
|
||||
response.content.strip(),
|
||||
"account;admin@test.com;{PLAIN}toto;;;True;DomainAdmins;admin@test.com;test.com\r\naccount;admin@test2.com;{PLAIN}toto;;;True;DomainAdmins;admin@test2.com;test2.com"
|
||||
"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"
|
||||
)
|
||||
|
||||
def test_export_aliases(self):
|
||||
response = self.__export_identities(idtfilter="alias")
|
||||
self.assertEqual(
|
||||
response.content.strip(),
|
||||
"alias;alias@test.com;False;user@test.com"
|
||||
"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;False;user@external.com"
|
||||
"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;False;toto@titi.com;test@truc.fr"
|
||||
"dlist;postmaster@test.com;True;toto@titi.com;test@truc.fr"
|
||||
)
|
||||
|
||||
@@ -40,10 +40,21 @@ domainalias; domalias1.com; domain1.com; True
|
||||
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("modoboa.extensions.admin.views.import.import_domains"), {
|
||||
"sourcefile": f
|
||||
}
|
||||
)
|
||||
self.assertIn('A domain test.alias already exists', resp.content)
|
||||
|
||||
def test_identities_import(self):
|
||||
f = ContentFile(b"""
|
||||
account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com
|
||||
account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; test.com
|
||||
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
|
||||
@@ -54,20 +65,26 @@ dlist; dlist@test.com; True; user1@test.com; user@extdomain.com
|
||||
)
|
||||
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(admin.is_owner(u1.mailbox_set.all()[0]))
|
||||
self.assertEqual(u1.mailbox_set.all()[0].full_address, "user1@test.com")
|
||||
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(da.mailbox_set.all()[0].full_address, "truc@test.com")
|
||||
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")
|
||||
@@ -86,10 +103,22 @@ dlist; dlist@test.com; True; user1@test.com; user@extdomain.com
|
||||
self.assertIn("user@extdomain.com", dlist.extmboxes)
|
||||
self.assertTrue(admin.is_owner(dlist))
|
||||
|
||||
def test_import_invalid_quota(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("modoboa.extensions.admin.views.import.import_identities"),
|
||||
{"sourcefile": f, "crypt_password": True}
|
||||
)
|
||||
self.assertIn('Quota is greater than the allowed', resp.content)
|
||||
|
||||
def test_import_duplicate(self):
|
||||
f = ContentFile(b"""
|
||||
account; admin@test.com; toto; Admin; ; True; DomainAdmins; admin@test.com; test.com
|
||||
account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; test.com
|
||||
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("modoboa.extensions.admin.views.import.import_identities"),
|
||||
@@ -108,7 +137,7 @@ account; truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; te
|
||||
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
|
||||
account; sa@test.com; toto; Super; Admin; True; SuperAdmins; superadmin@test.com; 50
|
||||
""", name="identities.csv")
|
||||
self.clt.post(
|
||||
reverse("modoboa.extensions.admin.views.import.import_identities"),
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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("modoboa.extensions.admin.views.identity.newaccount"),
|
||||
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}')
|
||||
@@ -0,0 +1,26 @@
|
||||
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('modoboa.extensions.admin.views.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))
|
||||
@@ -6,59 +6,50 @@ from django.contrib.auth.decorators import (
|
||||
login_required, permission_required
|
||||
)
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.webutils import ajax_response, ajax_simple_response
|
||||
from modoboa.lib.exceptions import PermDeniedException
|
||||
from modoboa.extensions.admin.exceptions import AdminError
|
||||
from modoboa.lib.webutils 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, tplname, commonctx, callback=None):
|
||||
def _validate_alias(request, form, successmsg, callback=None):
|
||||
"""Alias validation
|
||||
|
||||
Common function shared between creation and modification actions.
|
||||
"""
|
||||
error = None
|
||||
if form.is_valid():
|
||||
form.set_recipients()
|
||||
try:
|
||||
alias = form.save()
|
||||
except IntegrityError:
|
||||
raise AdminError(_("Alias with this name already exists"))
|
||||
raise Conflict(_("Alias with this name already exists"))
|
||||
if callback:
|
||||
callback(request.user, alias)
|
||||
return ajax_simple_response({"status": "ok", "respmsg": successmsg})
|
||||
return render_to_json_response(successmsg)
|
||||
|
||||
if "targets" in request.POST:
|
||||
targets = request.POST.getlist("targets")
|
||||
commonctx["targets"] = targets[:-1]
|
||||
|
||||
commonctx["form"] = form
|
||||
commonctx["error"] = error
|
||||
return ajax_response(request, status="ko", template=tplname, **commonctx)
|
||||
return render_to_json_response({'form_errors': form.errors}, status=400)
|
||||
|
||||
|
||||
def _new_alias(request, title, action, successmsg,
|
||||
tplname="common/generic_modal_form.html"):
|
||||
tplname="admin/aliasform.html"):
|
||||
events.raiseEvent("CanCreate", request.user, "mailbox_aliases")
|
||||
ctx = {
|
||||
"title": title,
|
||||
"action": action,
|
||||
"formid": "aliasform",
|
||||
"action_label": _("Create"),
|
||||
"action_classes": "submit"
|
||||
}
|
||||
if request.method == "POST":
|
||||
def callback(user, alias):
|
||||
alias.post_create(user)
|
||||
|
||||
form = AliasForm(request.user, request.POST)
|
||||
return _validate_alias(
|
||||
request, form, successmsg, tplname, ctx, callback
|
||||
request, form, successmsg, callback
|
||||
)
|
||||
|
||||
form = AliasForm(request.user)
|
||||
ctx["form"] = form
|
||||
ctx = {
|
||||
"title": title,
|
||||
"action": action,
|
||||
"formid": "aliasform",
|
||||
"action_label": _("Create"),
|
||||
"action_classes": "submit",
|
||||
"form": AliasForm(request.user)
|
||||
}
|
||||
return render(request, tplname, ctx)
|
||||
|
||||
|
||||
@@ -94,29 +85,29 @@ def newforward(request):
|
||||
|
||||
@login_required
|
||||
@permission_required("admin.change_alias")
|
||||
def editalias(request, alid, tplname="common/generic_modal_form.html"):
|
||||
@transaction.commit_on_success
|
||||
def editalias(request, alid, tplname="admin/aliasform.html"):
|
||||
alias = Alias.objects.get(pk=alid)
|
||||
if not request.user.can_access(alias):
|
||||
raise PermDeniedException
|
||||
ctx = dict(
|
||||
action=reverse(editalias, args=[alias.id]),
|
||||
formid="aliasform",
|
||||
title=alias.full_address,
|
||||
action_label=_("Update"),
|
||||
action_classes="submit"
|
||||
)
|
||||
if len(alias.get_recipients()) >= 2:
|
||||
successmsg = _("Distribution list modified")
|
||||
elif alias.extmboxes != "":
|
||||
successmsg = _("Forward modified")
|
||||
else:
|
||||
successmsg = _("Alias modified")
|
||||
if request.method == "POST":
|
||||
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, tplname, ctx)
|
||||
return _validate_alias(request, form, successmsg)
|
||||
|
||||
form = AliasForm(request.user, instance=alias)
|
||||
ctx["form"] = form
|
||||
ctx = {
|
||||
'action': reverse(editalias, 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)
|
||||
|
||||
|
||||
@@ -141,4 +132,4 @@ def delalias(request):
|
||||
alias.delete()
|
||||
|
||||
msg = ungettext(msg, msgs, len(selection))
|
||||
return ajax_simple_response({"status": "ok", "respmsg": msg})
|
||||
return render_to_json_response(msg)
|
||||
|
||||
@@ -2,7 +2,6 @@ from django.http import HttpResponseRedirect
|
||||
from django.utils.translation import ugettext as _, ungettext
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import render
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.contrib.auth.decorators import (
|
||||
@@ -10,18 +9,19 @@ from django.contrib.auth.decorators import (
|
||||
)
|
||||
from modoboa.lib import parameters, events
|
||||
from modoboa.lib.webutils import (
|
||||
ajax_simple_response, _render_to_string
|
||||
_render_to_string, render_to_json_response
|
||||
)
|
||||
from modoboa.lib.formutils import CreationWizard
|
||||
from modoboa.lib.exceptions import PermDeniedException
|
||||
from modoboa.lib.exceptions import (
|
||||
ModoboaException, PermDeniedException, BadRequest
|
||||
)
|
||||
from modoboa.lib.templatetags.lib_tags import pagination_bar
|
||||
from modoboa.extensions.admin.tables import DomainsTable
|
||||
from modoboa.extensions.admin.lib import get_sort_order, get_listing_page
|
||||
from modoboa.extensions.admin.exceptions import AdminError
|
||||
from modoboa.extensions.admin.models import Domain, Mailbox
|
||||
from modoboa.extensions.admin.forms import (
|
||||
DomainForm, DomainFormGeneral, DomainFormOptions
|
||||
)
|
||||
from modoboa.extensions.admin.lib import get_domains
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -35,17 +35,29 @@ def index(request):
|
||||
)
|
||||
def _domains(request):
|
||||
sort_order, sort_dir = get_sort_order(request.GET, "name")
|
||||
domains = Domain.objects.get_for_admin(request.user)
|
||||
squery = request.GET.get("searchquery", None)
|
||||
if squery is not None:
|
||||
q = Q(name__contains=squery)
|
||||
q |= Q(domainalias__name__contains=squery)
|
||||
domains = domains.filter(q).distinct()
|
||||
if sort_order in ["name", "domainalias__name"]:
|
||||
domains = domains.order_by("%s%s" % (sort_dir, sort_order))
|
||||
page = get_listing_page(domains, request.GET.get("page", 1))
|
||||
return ajax_simple_response({
|
||||
"table": DomainsTable(request, page.object_list).render(),
|
||||
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 == '-')
|
||||
page = get_listing_page(domainlist, request.GET.get("page", 1))
|
||||
return render_to_json_response({
|
||||
"table": _render_to_string(request, 'admin/domains_table.html', {
|
||||
'domains': page.object_list,
|
||||
'tableid': 'domains'
|
||||
}),
|
||||
"page": page.number,
|
||||
"paginbar": pagination_bar(page),
|
||||
"handle_mailboxes": parameters.get_admin("HANDLE_MAILBOXES",
|
||||
@@ -70,7 +82,7 @@ def domains(request, tplname="admin/domains.html"):
|
||||
@permission_required("core.add_user")
|
||||
def domains_list(request):
|
||||
doms = [dom.name for dom in Domain.objects.get_for_admin(request.user)]
|
||||
return ajax_simple_response(doms)
|
||||
return render_to_json_response(doms)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -79,50 +91,49 @@ def domains_list(request):
|
||||
def newdomain(request, tplname="common/wizard_forms.html"):
|
||||
events.raiseEvent("CanCreate", request.user, "domains")
|
||||
|
||||
def newdomain_cb(steps):
|
||||
genform = steps[0]["form"]
|
||||
genform.is_valid()
|
||||
domain = genform.save(request.user)
|
||||
domain.post_create(request.user)
|
||||
steps[1]["form"].save(request.user, domain)
|
||||
|
||||
commonctx = {"title": _("New domain"),
|
||||
"action_label": _("Create"),
|
||||
"action_classes": "submit",
|
||||
"action": reverse(newdomain),
|
||||
"formid": "domform"}
|
||||
cwizard = CreationWizard(newdomain_cb)
|
||||
cwizard = CreationWizard()
|
||||
cwizard.add_step(DomainFormGeneral, _("General"),
|
||||
[dict(classes="btn-inverse next", label=_("Next"))],
|
||||
formtpl="admin/domain_general_form.html")
|
||||
cwizard.add_step(DomainFormOptions, _("Options"),
|
||||
[dict(classes="btn-primary submit", label=_("Create")),
|
||||
dict(classes="btn-inverse prev", label=_("Previous"))],
|
||||
formtpl="admin/domain_options_form.html")
|
||||
cwizard.add_step(
|
||||
DomainFormOptions, _("Options"),
|
||||
[dict(classes="btn-primary submit", label=_("Create")),
|
||||
dict(classes="btn-inverse prev", label=_("Previous"))],
|
||||
formtpl="admin/domain_options_form.html",
|
||||
new_args=[request.user]
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
retcode, data = cwizard.validate_step(request)
|
||||
if retcode == -1:
|
||||
raise AdminError(data)
|
||||
raise BadRequest(data)
|
||||
if retcode == 1:
|
||||
return ajax_simple_response(dict(
|
||||
status="ok", title=cwizard.get_title(data + 1), stepid=data
|
||||
))
|
||||
if retcode == 2:
|
||||
return ajax_simple_response(
|
||||
dict(status="ok", respmsg=_("Domain created"))
|
||||
return render_to_json_response(
|
||||
{'title': cwizard.get_title(data + 1), 'stepid': data}
|
||||
)
|
||||
if retcode == 2:
|
||||
genform = cwizard.steps[0]["form"]
|
||||
domain = genform.save(request.user)
|
||||
domain.post_create(request.user)
|
||||
try:
|
||||
cwizard.steps[1]["form"].save(request.user, domain)
|
||||
except ModoboaException as e:
|
||||
transaction.rollback()
|
||||
raise
|
||||
return render_to_json_response(_("Domain created"))
|
||||
return render_to_json_response({
|
||||
'stepid': data, 'form_errors': cwizard.errors
|
||||
}, status=400)
|
||||
|
||||
from modoboa.lib.templatetags.lib_tags import render_form
|
||||
return ajax_simple_response(dict(
|
||||
status="ko", stepid=data,
|
||||
form=render_form(cwizard.steps[data]["form"])
|
||||
))
|
||||
|
||||
ctx = {"title": _("New domain"),
|
||||
"action_label": _("Create"),
|
||||
"action_classes": "submit",
|
||||
"action": reverse(newdomain),
|
||||
"formid": "domform"}
|
||||
cwizard.create_forms()
|
||||
commonctx.update(steps=cwizard.steps)
|
||||
commonctx.update(subtitle="1. %s" % cwizard.steps[0]['title'])
|
||||
return render(request, tplname, commonctx)
|
||||
ctx.update(steps=cwizard.steps)
|
||||
ctx.update(subtitle="1. %s" % cwizard.steps[0]['title'])
|
||||
return render(request, tplname, ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -133,44 +144,32 @@ def editdomain(request, dom_id, tplname="admin/editdomainform.html"):
|
||||
if not request.user.can_access(domain):
|
||||
raise PermDeniedException
|
||||
|
||||
instances = dict(general=domain)
|
||||
events.raiseEvent("FillDomainInstances", request.user, domain, instances)
|
||||
if request.method == "POST":
|
||||
domain.oldname = domain.name
|
||||
form = DomainForm(request.user, request.POST, instances=instances)
|
||||
if form.is_valid():
|
||||
form.save(request.user)
|
||||
events.raiseEvent("DomainModified", domain)
|
||||
return render_to_json_response(_("Domain modified"))
|
||||
return render_to_json_response({
|
||||
'form_errors': form.errors
|
||||
}, status=400)
|
||||
|
||||
domadmins = [u for u in domain.admins
|
||||
if request.user.can_access(u) and not u.is_superuser]
|
||||
if not request.user.is_superuser:
|
||||
domadmins = [u for u in domadmins if u.group == "DomainAdmins"]
|
||||
|
||||
instances = dict(general=domain)
|
||||
events.raiseEvent("FillDomainInstances", request.user, domain, instances)
|
||||
|
||||
commonctx = {"title": domain.name,
|
||||
"action_label": _("Update"),
|
||||
"action_classes": "submit",
|
||||
"action": reverse(editdomain, args=[dom_id]),
|
||||
"formid": "domform",
|
||||
"domain": domain}
|
||||
if request.method == "POST":
|
||||
error = None
|
||||
domain.oldname = domain.name
|
||||
form = DomainForm(request.user, request.POST, instances=instances)
|
||||
if form.is_valid():
|
||||
try:
|
||||
form.save(request.user)
|
||||
except AdminError, e:
|
||||
error = str(e)
|
||||
else:
|
||||
events.raiseEvent("DomainModified", domain)
|
||||
return ajax_simple_response({
|
||||
"status": "ok", "respmsg": _("Domain modified")
|
||||
})
|
||||
|
||||
commonctx["tabs"] = form
|
||||
return ajax_simple_response({
|
||||
"status": "ko", "respmsg": error,
|
||||
"content": _render_to_string(request, tplname, commonctx)
|
||||
})
|
||||
|
||||
commonctx["tabs"] = DomainForm(request.user, instances=instances)
|
||||
commonctx["domadmins"] = domadmins
|
||||
return render(request, tplname, commonctx)
|
||||
ctx = {"title": domain.name,
|
||||
"action_label": _("Update"),
|
||||
"action_classes": "submit",
|
||||
"action": reverse(editdomain, args=[dom_id]),
|
||||
"formid": "domform",
|
||||
"domain": domain,
|
||||
"tabs": DomainForm(request.user, instances=instances),
|
||||
"domadmins": domadmins}
|
||||
return render(request, tplname, ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -187,8 +186,8 @@ def deldomain(request, dom_id):
|
||||
if not request.user.can_access(dom):
|
||||
raise PermDeniedException
|
||||
if mb and mb.domain == dom:
|
||||
raise AdminError(_("You can't delete your own domain"))
|
||||
raise PermDeniedException(_("You can't delete your own domain"))
|
||||
dom.delete(request.user, keepdir)
|
||||
|
||||
msg = ungettext("Domain deleted", "Domains deleted", 1)
|
||||
return ajax_simple_response({"status": "ok", "respmsg": msg})
|
||||
return render_to_json_response(msg)
|
||||
|
||||
@@ -8,8 +8,7 @@ from django.utils.translation import ugettext as _
|
||||
from django.contrib.auth.decorators import (
|
||||
login_required, permission_required, user_passes_test
|
||||
)
|
||||
from modoboa.extensions.admin.lib import get_identities
|
||||
from modoboa.extensions.admin.models import Domain
|
||||
from modoboa.extensions.admin.lib import get_domains, get_identities
|
||||
from modoboa.extensions.admin.forms import (
|
||||
ExportIdentitiesForm, ExportDomainsForm
|
||||
)
|
||||
@@ -73,10 +72,9 @@ def export_domains(request):
|
||||
form.is_valid()
|
||||
fp = cStringIO.StringIO()
|
||||
csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"])
|
||||
for dom in Domain.objects.get_for_admin(request.user):
|
||||
for dom in get_domains(request.user,
|
||||
**request.session['domains_filters']):
|
||||
dom.to_csv(csvwriter)
|
||||
for da in dom.domainalias_set.all():
|
||||
da.to_csv(csvwriter)
|
||||
content = fp.getvalue()
|
||||
fp.close()
|
||||
return _export(content, form.cleaned_data["filename"])
|
||||
|
||||
@@ -7,9 +7,11 @@ from django.contrib.auth.decorators import (
|
||||
)
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from modoboa.lib import parameters, events
|
||||
from modoboa.lib.exceptions import PermDeniedException
|
||||
from modoboa.lib.exceptions import (
|
||||
ModoboaException, PermDeniedException, BadRequest
|
||||
)
|
||||
from modoboa.lib.webutils import (
|
||||
ajax_simple_response, _render_to_string, ajax_response
|
||||
_render_to_string, render_to_json_response
|
||||
)
|
||||
from modoboa.lib.formutils import CreationWizard
|
||||
from modoboa.lib.templatetags.lib_tags import pagination_bar
|
||||
@@ -18,7 +20,6 @@ from modoboa.extensions.admin.models import Mailbox, Domain
|
||||
from modoboa.extensions.admin.lib import (
|
||||
get_sort_order, get_listing_page, get_identities
|
||||
)
|
||||
from modoboa.extensions.admin.exceptions import AdminError
|
||||
from modoboa.extensions.admin.forms import (
|
||||
AccountForm, AccountFormGeneral, AccountFormMail
|
||||
)
|
||||
@@ -42,7 +43,7 @@ def _identities(request):
|
||||
objects = sorted(idents_list, key=lambda o: o.tags[0],
|
||||
reverse=sort_dir == '-')
|
||||
page = get_listing_page(objects, request.GET.get("page", 1))
|
||||
return ajax_simple_response({
|
||||
return render_to_json_response({
|
||||
"table": _render_to_string(request, "admin/identities_table.html", {
|
||||
"identities": page.object_list,
|
||||
"tableid": "objects_table"
|
||||
@@ -72,7 +73,7 @@ def accounts_list(request):
|
||||
accs = User.objects.filter(is_superuser=False) \
|
||||
.exclude(groups__name='SimpleUsers')
|
||||
res = [a.username for a in accs.all()]
|
||||
return ajax_simple_response(res)
|
||||
return render_to_json_response(res)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -99,10 +100,9 @@ def list_quotas(request, tplname="admin/quotas.html"):
|
||||
order_by=["%s%s" % (sort_dir, sort_order)]
|
||||
)
|
||||
else:
|
||||
raise AdminError(_("Invalid request"))
|
||||
raise BadRequest(_("Invalid request"))
|
||||
page = get_listing_page(mboxes, request.GET.get("page", 1))
|
||||
return ajax_simple_response({
|
||||
"status": "ok",
|
||||
return render_to_json_response({
|
||||
"page": page.number,
|
||||
"paginbar": pagination_bar(page),
|
||||
"table": _render_to_string(request, tplname, {
|
||||
@@ -115,35 +115,15 @@ def list_quotas(request, tplname="admin/quotas.html"):
|
||||
@permission_required("core.add_user")
|
||||
@transaction.commit_on_success
|
||||
def newaccount(request, tplname='common/wizard_forms.html'):
|
||||
def create_account(steps):
|
||||
"""Account creation callback
|
||||
"""Create a new account.
|
||||
|
||||
Called when all creation steps have been validated.
|
||||
|
||||
:param steps: the steps data
|
||||
"""
|
||||
genform = steps[0]["form"]
|
||||
genform.is_valid()
|
||||
account = genform.save()
|
||||
account.post_create(request.user)
|
||||
|
||||
mailform = steps[1]["form"]
|
||||
try:
|
||||
mailform.save(request.user, account)
|
||||
except AdminError:
|
||||
# A bit uggly: transaction management doesn't work very
|
||||
# well with nested functions. Need to wait for django 1.6
|
||||
# and atomicity.
|
||||
account.delete(request.user, False)
|
||||
raise
|
||||
|
||||
ctx = dict(
|
||||
title=_("New account"),
|
||||
action=reverse(newaccount),
|
||||
formid="newaccount_form",
|
||||
submit_label=_("Create")
|
||||
)
|
||||
cwizard = CreationWizard(create_account)
|
||||
.. note:: An issue still remains int this code: if all validation
|
||||
steps are successful but an error occurs after we call 'save',
|
||||
the account will be created. It happens transaction management
|
||||
doesn't work very well with nested functions. Need to wait for
|
||||
django 1.6 and atomicity.
|
||||
"""
|
||||
cwizard = CreationWizard()
|
||||
cwizard.add_step(AccountFormGeneral, _("General"),
|
||||
[dict(classes="btn-inverse next", label=_("Next"))],
|
||||
new_args=[request.user])
|
||||
@@ -155,22 +135,28 @@ def newaccount(request, tplname='common/wizard_forms.html'):
|
||||
if request.method == "POST":
|
||||
retcode, data = cwizard.validate_step(request)
|
||||
if retcode == -1:
|
||||
raise AdminError(data)
|
||||
raise BadRequest(data)
|
||||
if retcode == 1:
|
||||
return ajax_simple_response(dict(
|
||||
status="ok", title=cwizard.get_title(data + 1), stepid=data
|
||||
))
|
||||
return render_to_json_response(
|
||||
{'title': cwizard.get_title(data + 1), 'stepid': data}
|
||||
)
|
||||
if retcode == 2:
|
||||
return ajax_simple_response(dict(
|
||||
status="ok", respmsg=_("Account created")
|
||||
))
|
||||
|
||||
from modoboa.lib.templatetags.lib_tags import render_form
|
||||
return ajax_simple_response(dict(
|
||||
status="ko", stepid=data,
|
||||
form=render_form(cwizard.steps[data]["form"])
|
||||
))
|
||||
genform = cwizard.steps[0]["form"]
|
||||
account = genform.save()
|
||||
account.post_create(request.user)
|
||||
mailform = cwizard.steps[1]["form"]
|
||||
mailform.save(request.user, account)
|
||||
return render_to_json_response(_("Account created"))
|
||||
return render_to_json_response({
|
||||
'stepid': data, 'form_errors': cwizard.errors
|
||||
}, status=400)
|
||||
|
||||
ctx = {
|
||||
'title': _("New account"),
|
||||
'action': reverse(newaccount),
|
||||
'formid': 'newaccount_form',
|
||||
'submit_label': _("Create")
|
||||
}
|
||||
cwizard.create_forms()
|
||||
ctx.update(steps=cwizard.steps)
|
||||
ctx.update(subtitle="1. %s" % cwizard.steps[0]['title'])
|
||||
@@ -191,14 +177,6 @@ def editaccount(request, accountid, tplname="common/tabforms.html"):
|
||||
instances = dict(general=account, mail=mb, perms=account)
|
||||
events.raiseEvent("FillAccountInstances", request.user, account, instances)
|
||||
|
||||
ctx = dict(
|
||||
title=account.username,
|
||||
formid="accountform",
|
||||
action=reverse(editaccount, args=[accountid]),
|
||||
action_label=_("Update"),
|
||||
action_classes="submit"
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
classes = {}
|
||||
form = AccountForm(request.user, request.POST,
|
||||
@@ -209,15 +187,17 @@ def editaccount(request, accountid, tplname="common/tabforms.html"):
|
||||
if form.is_valid(optional_only=True):
|
||||
events.raiseEvent("AccountModified", account, form.account)
|
||||
form.save()
|
||||
return ajax_simple_response(
|
||||
dict(status="ok", respmsg=_("Account updated"))
|
||||
)
|
||||
transaction.rollback()
|
||||
return render_to_json_response(_("Account updated"))
|
||||
return render_to_json_response({'form_errors': form.errors}, status=400)
|
||||
|
||||
ctx["tabs"] = form
|
||||
return ajax_response(request, status="ko", template=tplname, **ctx)
|
||||
|
||||
ctx["tabs"] = AccountForm(request.user, instances=instances)
|
||||
ctx = {
|
||||
'title': account.username,
|
||||
'formid': 'accountform',
|
||||
'action': reverse(editaccount, args=[accountid]),
|
||||
'action_label': _('Update'),
|
||||
'action_classes': 'submit',
|
||||
'tabs': AccountForm(request.user, instances=instances)
|
||||
}
|
||||
active_tab_id = request.GET.get("active_tab", "default")
|
||||
if active_tab_id != "default":
|
||||
ctx["tabs"].active_id = active_tab_id
|
||||
@@ -229,11 +209,10 @@ def editaccount(request, accountid, tplname="common/tabforms.html"):
|
||||
@transaction.commit_on_success
|
||||
def delaccount(request, accountid):
|
||||
keepdir = True if request.POST.get("keepdir", "false") == "true" else False
|
||||
|
||||
User.objects.get(pk=accountid).delete(request.user, keep_mb_dir=keepdir)
|
||||
|
||||
msg = ungettext("Account deleted", "Accounts deleted", 1)
|
||||
return ajax_simple_response({"status": "ok", "respmsg": msg})
|
||||
return render_to_json_response(
|
||||
ungettext("Account deleted", "Accounts deleted", 1)
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -242,13 +221,13 @@ def remove_permission(request):
|
||||
domid = request.GET.get("domid", None)
|
||||
daid = request.GET.get("daid", None)
|
||||
if domid is None or daid is None:
|
||||
raise AdminError(_("Invalid request"))
|
||||
raise BadRequest(_("Invalid request"))
|
||||
try:
|
||||
account = User.objects.get(pk=daid)
|
||||
domain = Domain.objects.get(pk=domid)
|
||||
except (User.DoesNotExist, Domain.DoesNotExist):
|
||||
raise AdminError(_("Invalid request"))
|
||||
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 ajax_simple_response({"status": "ok"})
|
||||
return render_to_json_response({})
|
||||
|
||||
@@ -6,7 +6,8 @@ from django.db import transaction, IntegrityError
|
||||
from django.contrib.auth.decorators import (
|
||||
login_required, permission_required, user_passes_test
|
||||
)
|
||||
from modoboa.lib.exceptions import ModoboaException
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.exceptions import ModoboaException, Conflict
|
||||
from modoboa.core.models import User
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain, DomainAlias, Alias
|
||||
@@ -39,10 +40,11 @@ def import_domains(request):
|
||||
<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"),
|
||||
@@ -100,8 +102,8 @@ def importdata(request, formclass=ImportDataForm):
|
||||
try:
|
||||
reader = csv.reader(request.FILES['sourcefile'],
|
||||
delimiter=form.cleaned_data['sepchar'])
|
||||
except csv.Error, e:
|
||||
error = str(e)
|
||||
except csv.Error as inst:
|
||||
error = str(inst)
|
||||
|
||||
if error is None:
|
||||
try:
|
||||
@@ -112,13 +114,21 @@ def importdata(request, formclass=ImportDataForm):
|
||||
try:
|
||||
fct = globals()["import_%s" % row[0].strip()]
|
||||
except KeyError:
|
||||
continue
|
||||
fct = events.raiseQueryEvent(
|
||||
'ImportObject', row[0].strip()
|
||||
)
|
||||
if not fct:
|
||||
continue
|
||||
fct = fct[0]
|
||||
try:
|
||||
fct(request.user, row, form.cleaned_data)
|
||||
except IntegrityError, e:
|
||||
if form.cleaned_data["continue_if_exists"]:
|
||||
continue
|
||||
raise ModoboaException(_("Object already exists: %s" % row))
|
||||
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", {
|
||||
@@ -142,7 +152,7 @@ def import_identities(request):
|
||||
|
||||
helptext = _("""Provide a CSV file where lines respect one of the following formats:
|
||||
<ul>
|
||||
<li><em>account; loginname; password; first name; last name; enabled; group; address[, domain, ...]</em></li>
|
||||
<li><em>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>
|
||||
|
||||
@@ -2,10 +2,9 @@ from django.contrib.auth.decorators import login_required
|
||||
from django.utils.translation import ugettext as _
|
||||
from modoboa.lib.exceptions import ModoboaException
|
||||
from modoboa.lib.webutils import (
|
||||
ajax_response, ajax_simple_response, _render_to_string
|
||||
render_to_json_response, _render_to_string
|
||||
)
|
||||
from modoboa.extensions.admin.lib import needs_mailbox
|
||||
from modoboa.extensions.admin.exceptions import BadDestination
|
||||
from modoboa.extensions.admin.models import Alias, Mailbox
|
||||
from modoboa.extensions.admin.forms import ForwardForm
|
||||
|
||||
@@ -13,12 +12,7 @@ from modoboa.extensions.admin.forms import ForwardForm
|
||||
@login_required
|
||||
@needs_mailbox()
|
||||
def forward(request, tplname='admin/forward.html'):
|
||||
try:
|
||||
mb = request.user.mailbox_set.all()[0]
|
||||
except IndexError:
|
||||
raise ModoboaException(
|
||||
_("You need a mailbox in order to define a forward")
|
||||
)
|
||||
mb = request.user.mailbox_set.all()[0]
|
||||
try:
|
||||
al = Alias.objects.get(address=mb.address,
|
||||
domain__name=mb.domain.name)
|
||||
@@ -28,28 +22,23 @@ def forward(request, tplname='admin/forward.html'):
|
||||
form = ForwardForm(request.POST)
|
||||
error = None
|
||||
if form.is_valid():
|
||||
try:
|
||||
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)
|
||||
if request.user.group != "SimpleUsers":
|
||||
al.post_create(request.user)
|
||||
return ajax_response(request, respmsg=_("Forward updated"))
|
||||
except BadDestination, e:
|
||||
error = str(e)
|
||||
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 ajax_simple_response(dict(
|
||||
status="ko",
|
||||
errors=form.errors,
|
||||
respmsg=error
|
||||
))
|
||||
return render_to_json_response(
|
||||
{'form_errors': form.errors}, status=400
|
||||
)
|
||||
|
||||
form = ForwardForm()
|
||||
if al is not None:
|
||||
@@ -60,8 +49,7 @@ def forward(request, tplname='admin/forward.html'):
|
||||
pass
|
||||
else:
|
||||
form.fields["keepcopies"].initial = True
|
||||
return ajax_simple_response({
|
||||
"status": "ok",
|
||||
return render_to_json_response({
|
||||
"content": _render_to_string(request, tplname, {
|
||||
"form": form
|
||||
})
|
||||
|
||||
@@ -8,9 +8,8 @@ Provides:
|
||||
* Per-domain settings
|
||||
|
||||
"""
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template import Template, Context
|
||||
import sys
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.core.extensions import ModoExtension, exts_pool
|
||||
|
||||
@@ -30,141 +29,33 @@ class Amavis(ModoExtension):
|
||||
for existing domains to let Amavis consider them local.
|
||||
"""
|
||||
from modoboa.extensions.admin.models import Domain
|
||||
from .models import Users, Policy
|
||||
from modoboa.extensions.amavis.models import Users
|
||||
from modoboa.extensions.amavis.lib import (
|
||||
create_user_and_policy, create_user_and_use_policy
|
||||
)
|
||||
|
||||
for dom in Domain.objects.all():
|
||||
try:
|
||||
Users.objects.get(email="@%s" % dom.name)
|
||||
except Users.DoesNotExist:
|
||||
p = Policy.objects.create(policy_name=dom.name)
|
||||
Users.objects.create(email="@%s" % dom.name, fullname=dom.name,
|
||||
priority=7, policy=p)
|
||||
create_user_and_policy(dom.name)
|
||||
for domalias in dom.domainalias_set.all():
|
||||
try:
|
||||
Users.objects.get(email='@%s' % domalias.name)
|
||||
except Users.DoesNotExist:
|
||||
create_user_and_use_policy(domalias.name, dom.name)
|
||||
|
||||
def load(self):
|
||||
from .app_settings import ParametersForm, UserSettings
|
||||
|
||||
parameters.register(ParametersForm, "Amavis")
|
||||
parameters.register(UserSettings, ugettext_lazy("Quarantine"))
|
||||
from modoboa.extensions.amavis import general_callbacks
|
||||
if 'modoboa.extensions.amavis.general_callbacks' in sys.modules:
|
||||
reload(general_callbacks)
|
||||
|
||||
def destroy(self):
|
||||
events.unregister("UserMenuDisplay", menu)
|
||||
events.unregister_extension()
|
||||
parameters.unregister()
|
||||
|
||||
exts_pool.register_extension(Amavis)
|
||||
|
||||
|
||||
@events.observe("UserMenuDisplay")
|
||||
def menu(target, user):
|
||||
if target == "top_menu":
|
||||
return [
|
||||
{"name": "quarantine",
|
||||
"label": _("Quarantine"),
|
||||
"url": reverse('modoboa.extensions.amavis.views.index')}
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
@events.observe("CreateDomain")
|
||||
def on_create_domain(user, domain):
|
||||
from .models import Users, Policy
|
||||
p = Policy.objects.create(policy_name=domain.name)
|
||||
Users.objects.create(email="@%s" % domain.name, fullname=domain.name,
|
||||
priority=7, policy=p)
|
||||
|
||||
|
||||
@events.observe("DomainModified")
|
||||
def on_domain_modified(domain):
|
||||
if domain.oldname != domain.name:
|
||||
from .models import Users
|
||||
u = Users.objects.get(email="@%s" % domain.oldname)
|
||||
u.email = "@%s" % domain.name
|
||||
u.fullname = domain.name
|
||||
u.policy.policy_name = domain.name
|
||||
u.policy.save()
|
||||
u.save()
|
||||
|
||||
|
||||
@events.observe("DeleteDomain")
|
||||
def on_delete_domain(domain):
|
||||
from .models import Users
|
||||
|
||||
try:
|
||||
u = Users.objects.get(email="@%s" % domain.name)
|
||||
except Users.DoesNotExist:
|
||||
return
|
||||
u.policy.delete()
|
||||
u.delete()
|
||||
|
||||
|
||||
@events.observe("GetStaticContent")
|
||||
def extra_static_content(user):
|
||||
if user.group == "SimpleUsers":
|
||||
return []
|
||||
|
||||
tpl = Template("""<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
{% if user_can_release == "no" %}var poller = new Poller("{{ url }}", {
|
||||
interval: {{ interval }},
|
||||
success_cb: function(data) {
|
||||
var $link = $("#nbrequests");
|
||||
if (data.requests > 0) {
|
||||
$link.html(data.requests + " " + "{{ text }}");
|
||||
$link.parent().removeClass('hidden');
|
||||
} else {
|
||||
$link.parent().addClass('hidden');
|
||||
}
|
||||
}
|
||||
});{% endif %}
|
||||
|
||||
$(document).bind('domform_init', function() {
|
||||
activate_widget.call($('#id_spam_subject_tag2_act'));
|
||||
});
|
||||
});
|
||||
</script>""")
|
||||
|
||||
return [tpl.render(
|
||||
Context(dict(
|
||||
url=reverse("modoboa.extensions.amavis.views.nbrequests"),
|
||||
interval=int(parameters.get_admin("CHECK_REQUESTS_INTERVAL")) * 1000,
|
||||
text=_("pending requests"),
|
||||
user_can_release=parameters.get_admin("USER_CAN_RELEASE")
|
||||
))
|
||||
)]
|
||||
|
||||
|
||||
@events.observe("TopNotifications")
|
||||
def display_requests(user):
|
||||
from .sql_listing import get_wrapper
|
||||
|
||||
if parameters.get_admin("USER_CAN_RELEASE") == "yes" \
|
||||
or user.group == "SimpleUsers":
|
||||
return []
|
||||
nbrequests = get_wrapper().get_pending_requests(user)
|
||||
|
||||
url = reverse("modoboa.extensions.amavis.views.index")
|
||||
url += "#listing/?viewrequests=1"
|
||||
tpl = Template('<div class="btn-group {{ css }}"><a id="nbrequests" href="{{ url }}" class="btn btn-danger">{{ label }}</a></div>')
|
||||
css = "hidden" if nbrequests == 0 else ""
|
||||
return [tpl.render(Context(dict(
|
||||
label=_("%d pending requests" % nbrequests), url=url, css=css
|
||||
)))]
|
||||
|
||||
|
||||
@events.observe("ExtraDomainForm")
|
||||
def extra_domain_form(user, domain):
|
||||
from .forms import DomainPolicyForm
|
||||
|
||||
if not user.has_perm("admin.view_domains"):
|
||||
return []
|
||||
return [
|
||||
dict(
|
||||
id="amavis", title=_("Content filter"), cls=DomainPolicyForm,
|
||||
formtpl="amavis/domain_content_filter.html"
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@events.observe("FillDomainInstances")
|
||||
def fill_domain_instances(user, domain, instances):
|
||||
if not user.has_perm("admin.view_domains"):
|
||||
return
|
||||
instances["amavis"] = domain
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# coding: utf-8
|
||||
from django import forms
|
||||
from models import Policy, Users
|
||||
from modoboa.lib.formutils import InlineRadioSelect
|
||||
from modoboa.extensions.amavis.models import Policy, Users
|
||||
|
||||
|
||||
class DomainPolicyForm(forms.ModelForm):
|
||||
@@ -34,20 +34,21 @@ class DomainPolicyForm(forms.ModelForm):
|
||||
self.fields[f].required = False
|
||||
|
||||
def save(self, user, commit=True):
|
||||
p = super(DomainPolicyForm, self).save(commit=False)
|
||||
for f in ['bypass_spam_checks', 'bypass_virus_checks', 'bypass_banned_checks']:
|
||||
if getattr(p, f) == '':
|
||||
setattr(p, f, None)
|
||||
policy = super(DomainPolicyForm, self).save(commit=False)
|
||||
for field in ['bypass_spam_checks', 'bypass_virus_checks',
|
||||
'bypass_banned_checks']:
|
||||
if getattr(policy, field) == '':
|
||||
setattr(policy, field, None)
|
||||
|
||||
if self.cleaned_data['spam_subject_tag2_act']:
|
||||
p.spam_subject_tag2 = None
|
||||
policy.spam_subject_tag2 = None
|
||||
|
||||
if commit:
|
||||
p.save()
|
||||
policy.save()
|
||||
try:
|
||||
u = Users.objects.get(policy=p)
|
||||
u = Users.objects.get(fullname=policy.policy_name)
|
||||
except Users.DoesNotExist:
|
||||
u = Users.objects.get(email="@%s" % self.domain.name)
|
||||
u.policy = p
|
||||
p.save()
|
||||
return p
|
||||
u.policy = policy
|
||||
policy.save()
|
||||
return policy
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.template import Template, Context
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.extensions.admin.models import DomainAlias
|
||||
from modoboa.extensions.amavis.lib import (
|
||||
create_user_and_policy, update_user_and_policy, delete_user_and_policy,
|
||||
create_user_and_use_policy, delete_user
|
||||
)
|
||||
|
||||
|
||||
@events.observe("UserMenuDisplay")
|
||||
def menu(target, user):
|
||||
if target == "top_menu":
|
||||
return [
|
||||
{"name": "quarantine",
|
||||
"label": _("Quarantine"),
|
||||
"url": reverse('modoboa.extensions.amavis.views.index')}
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
@events.observe("DomainCreated")
|
||||
def on_domain_created(user, domain):
|
||||
create_user_and_policy(domain.name)
|
||||
|
||||
|
||||
@events.observe("DomainModified")
|
||||
def on_domain_modified(domain):
|
||||
update_user_and_policy(domain.oldname, domain.name)
|
||||
|
||||
|
||||
@events.observe("DomainDeleted")
|
||||
def on_domain_deleted(domain):
|
||||
delete_user_and_policy(domain.name)
|
||||
|
||||
|
||||
@events.observe("DomainAliasCreated")
|
||||
def on_domain_alias_created(user, domainalias):
|
||||
create_user_and_use_policy(domainalias.name, domainalias.target.name)
|
||||
|
||||
|
||||
@events.observe("DomainAliasDeleted")
|
||||
def on_domain_alias_deleted(domainaliases):
|
||||
if isinstance(domainaliases, DomainAlias):
|
||||
domainaliases = [domainaliases]
|
||||
for domainalias in domainaliases:
|
||||
delete_user(domainalias.name)
|
||||
|
||||
|
||||
@events.observe("GetStaticContent")
|
||||
def extra_static_content(caller, user):
|
||||
if user.group == "SimpleUsers":
|
||||
return []
|
||||
|
||||
if caller == 'top' and parameters.get_admin("USER_CAN_RELEASE") == 'no':
|
||||
tpl = Template("""<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
var poller = new Poller("{{ url }}", {
|
||||
interval: {{ interval }},
|
||||
success_cb: function(data) {
|
||||
var $link = $("#nbrequests");
|
||||
if (data.requests > 0) {
|
||||
$link.html(data.requests + " " + "{{ text }}");
|
||||
$link.parent().removeClass('hidden');
|
||||
} else {
|
||||
$link.parent().addClass('hidden');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
""")
|
||||
return [tpl.render(
|
||||
Context({
|
||||
'url': reverse("modoboa.extensions.amavis.views.nbrequests"),
|
||||
'interval': int(parameters.get_admin("CHECK_REQUESTS_INTERVAL")) * 1000,
|
||||
'text': _("pending requests"),
|
||||
})
|
||||
)]
|
||||
|
||||
if caller == 'domains':
|
||||
tpl = Template("""<script type="text/javascript">
|
||||
$(document).bind('domform_init', function() {
|
||||
activate_widget.call($('#id_spam_subject_tag2_act'));
|
||||
});
|
||||
</script>
|
||||
""")
|
||||
|
||||
return [tpl.render(Context({}))]
|
||||
return []
|
||||
|
||||
|
||||
@events.observe("TopNotifications")
|
||||
def display_requests(user):
|
||||
from .sql_listing import get_wrapper
|
||||
|
||||
if parameters.get_admin("USER_CAN_RELEASE") == "yes" \
|
||||
or user.group == "SimpleUsers":
|
||||
return []
|
||||
nbrequests = get_wrapper().get_pending_requests(user)
|
||||
|
||||
url = reverse("modoboa.extensions.amavis.views.index")
|
||||
url += "#listing/?viewrequests=1"
|
||||
tpl = Template('<div class="btn-group {{ css }}"><a id="nbrequests" href="{{ url }}" class="btn btn-danger">{{ label }}</a></div>')
|
||||
css = "hidden" if nbrequests == 0 else ""
|
||||
return [tpl.render(Context(dict(
|
||||
label=_("%d pending requests" % nbrequests), url=url, css=css
|
||||
)))]
|
||||
|
||||
|
||||
def send_amavis_form():
|
||||
"""
|
||||
"""
|
||||
from .forms import DomainPolicyForm
|
||||
return [{
|
||||
'id': 'amavis', 'title': _("Content filter"), 'cls': DomainPolicyForm,
|
||||
'formtpl': 'amavis/domain_content_filter.html'
|
||||
}]
|
||||
|
||||
|
||||
@events.observe("ExtraDomainForm")
|
||||
def extra_domain_form(user, domain):
|
||||
if not user.has_perm("admin.view_domains"):
|
||||
return []
|
||||
return send_amavis_form()
|
||||
|
||||
|
||||
@events.observe('ExtraRelayDomainForm')
|
||||
def extra_relaydomain_form(user, rdomain):
|
||||
if not user.has_perm("postfix_relay_domains.add_relaydomain"):
|
||||
return []
|
||||
return send_amavis_form()
|
||||
|
||||
|
||||
@events.observe("FillDomainInstances")
|
||||
def fill_domain_instances(user, domain, instances):
|
||||
if not user.has_perm("admin.view_domains"):
|
||||
return
|
||||
instances["amavis"] = domain
|
||||
|
||||
|
||||
@events.observe("FillRelayDomainInstances")
|
||||
def fill_relaydomain_instances(user, rdomain, instances):
|
||||
if not user.has_perm("postfix_relay_domains.add_relaydomain"):
|
||||
return
|
||||
instances["amavis"] = rdomain
|
||||
@@ -7,7 +7,8 @@ from functools import wraps
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.exceptions import ModoboaException
|
||||
from modoboa.lib.exceptions import InternalError
|
||||
from modoboa.extensions.amavis.models import Users, Policy
|
||||
|
||||
|
||||
def selfservice(ssfunc=None):
|
||||
@@ -50,7 +51,7 @@ class AMrelease(object):
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.sock.connect(path)
|
||||
except socket.error, err:
|
||||
raise ModoboaException(
|
||||
raise InternalError(
|
||||
_("Connection to amavis failed: %s" % str(err))
|
||||
)
|
||||
|
||||
@@ -76,3 +77,71 @@ recipient=%s
|
||||
if re.search("250 [\d\.]+ Ok", answer):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def create_user_and_policy(name):
|
||||
"""Create records.
|
||||
|
||||
Create two records (a user and a policy) using :keyword:`name` as
|
||||
an identifier.
|
||||
|
||||
:param str name: name
|
||||
"""
|
||||
policy = Policy.objects.create(policy_name=name)
|
||||
Users.objects.create(
|
||||
email="@%s" % name, fullname=name,
|
||||
priority=7, policy=policy
|
||||
)
|
||||
|
||||
|
||||
def create_user_and_use_policy(name, policy_name):
|
||||
"""Create a *users* record and use an existing policy.
|
||||
|
||||
:param str name: user record name
|
||||
:param str policy_name: policy name
|
||||
"""
|
||||
policy = Policy.objects.get(policy_name=policy_name)
|
||||
Users.objects.create(
|
||||
email="@%s" % name, fullname=name,
|
||||
priority=7, policy=policy
|
||||
)
|
||||
|
||||
|
||||
def update_user_and_policy(oldname, newname):
|
||||
"""Update records.
|
||||
|
||||
:param str oldname: old name
|
||||
:param str newname: new name
|
||||
"""
|
||||
if oldname == newname:
|
||||
return
|
||||
u = Users.objects.get(email="@%s" % oldname)
|
||||
u.email = "@%s" % newname
|
||||
u.fullname = newname
|
||||
u.policy.policy_name = newname
|
||||
u.policy.save()
|
||||
u.save()
|
||||
|
||||
|
||||
def delete_user_and_policy(name):
|
||||
"""Delete records.
|
||||
|
||||
:param str name: identifier
|
||||
"""
|
||||
try:
|
||||
u = Users.objects.get(email="@%s" % name)
|
||||
except Users.DoesNotExist:
|
||||
return
|
||||
u.policy.delete()
|
||||
u.delete()
|
||||
|
||||
|
||||
def delete_user(name):
|
||||
"""Delete a *users* record.
|
||||
|
||||
:param str name: user record name
|
||||
"""
|
||||
try:
|
||||
Users.objects.get(email="@%s" % name).delete()
|
||||
except Users.DoesNotExist:
|
||||
pass
|
||||
|
||||
@@ -10,9 +10,12 @@ class Migration(DataMigration):
|
||||
def forwards(self, orm):
|
||||
"Write your forwards methods here."
|
||||
orm["amavis.Users"].objects.all().delete()
|
||||
orm["amavis.Policy"].objects.create(id=1, policy_name='default', bypass_virus_checks=None,
|
||||
bypass_spam_checks=None, bypass_banned_checks=None,
|
||||
spam_subject_tag2=None)
|
||||
if not orm['amavis.Policy'].objects.all():
|
||||
orm["amavis.Policy"].objects.create(
|
||||
id=1, policy_name='default', bypass_virus_checks=None,
|
||||
bypass_spam_checks=None, bypass_banned_checks=None,
|
||||
spam_subject_tag2=None
|
||||
)
|
||||
for dom in orm["admin.Domain"].objects.all():
|
||||
orm["amavis.Users"].objects.create(email="@%s" % dom.name, fullname=dom.name,
|
||||
priority=7, policy_id=1)
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import DataMigration
|
||||
from django.db import models
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
class Migration(DataMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
"Write your forwards methods here."
|
||||
try:
|
||||
ext = orm["core.Extension"].objects.get(name="amavis")
|
||||
except ObjectDoesNotExist:
|
||||
return
|
||||
if not ext.enabled:
|
||||
return
|
||||
for domalias in orm["admin.DomainAlias"].objects.all():
|
||||
u = orm["amavis.Users"](
|
||||
email="@%s" % domalias.name, fullname=domalias.name, priority=7
|
||||
)
|
||||
u.policy = orm["amavis.Policy"].objects.get(policy_name=domalias.target.name)
|
||||
u.save()
|
||||
|
||||
def backwards(self, orm):
|
||||
"Write your backwards methods here."
|
||||
raise RuntimeError("Cannot revert this migration")
|
||||
|
||||
models = {
|
||||
'admin.alias': {
|
||||
'Meta': {'ordering': "['domain__name', 'address']", 'unique_together': "(('address', 'domain'),)", 'object_name': 'Alias'},
|
||||
'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}),
|
||||
'aliases': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Alias']", 'symmetrical': 'False'}),
|
||||
'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}),
|
||||
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['admin.Mailbox']", 'symmetrical': 'False'})
|
||||
},
|
||||
'admin.domain': {
|
||||
'Meta': {'ordering': "['name']", 'object_name': 'Domain'},
|
||||
'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
|
||||
'quota': ('django.db.models.fields.IntegerField', [], {})
|
||||
},
|
||||
'admin.domainalias': {
|
||||
'Meta': {'object_name': 'DomainAlias'},
|
||||
'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
|
||||
'target': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"})
|
||||
},
|
||||
'admin.mailbox': {
|
||||
'Meta': {'object_name': 'Mailbox'},
|
||||
'address': ('django.db.models.fields.CharField', [], {'max_length': '252'}),
|
||||
'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}),
|
||||
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'quota': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"})
|
||||
},
|
||||
'admin.mailboxoperation': {
|
||||
'Meta': {'object_name': 'MailboxOperation'},
|
||||
'argument': ('django.db.models.fields.TextField', [], {}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Mailbox']", 'null': 'True', 'blank': 'True'}),
|
||||
'type': ('django.db.models.fields.CharField', [], {'max_length': '20'})
|
||||
},
|
||||
'admin.objectdates': {
|
||||
'Meta': {'object_name': 'ObjectDates'},
|
||||
'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
|
||||
},
|
||||
'admin.quota': {
|
||||
'Meta': {'object_name': 'Quota'},
|
||||
'bytes': ('django.db.models.fields.BigIntegerField', [], {'default': '0'}),
|
||||
'mbox': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'quota_value'", 'unique': 'True', 'null': 'True', 'to': "orm['admin.Mailbox']"}),
|
||||
'messages': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'username': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'primary_key': 'True'})
|
||||
},
|
||||
u'amavis.maddr': {
|
||||
'Meta': {'object_name': 'Maddr', 'db_table': "u'maddr'", 'managed': 'False'},
|
||||
'domain': ('django.db.models.fields.CharField', [], {'max_length': '765'}),
|
||||
'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.BigIntegerField', [], {'primary_key': 'True'}),
|
||||
'partition_tag': ('django.db.models.fields.IntegerField', [], {'unique': 'True', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'amavis.mailaddr': {
|
||||
'Meta': {'object_name': 'Mailaddr', 'db_table': "u'mailaddr'", 'managed': 'False'},
|
||||
'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
|
||||
'id': ('django.db.models.fields.IntegerField', [], {'primary_key': 'True'}),
|
||||
'priority': ('django.db.models.fields.IntegerField', [], {})
|
||||
},
|
||||
u'amavis.msgrcpt': {
|
||||
'Meta': {'unique_together': "(('partition_tag', 'mail', 'rseqnum'),)", 'object_name': 'Msgrcpt', 'db_table': "u'msgrcpt'", 'managed': 'False'},
|
||||
'bl': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}),
|
||||
'bspam_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'content': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
|
||||
'ds': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
|
||||
'is_local': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
|
||||
'mail': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['amavis.Msgs']", 'primary_key': 'True'}),
|
||||
'partition_tag': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'rid': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['amavis.Maddr']", 'primary_key': 'True', 'db_column': "'rid'"}),
|
||||
'rs': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
|
||||
'rseqnum': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
|
||||
'smtp_resp': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}),
|
||||
'wl': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'})
|
||||
},
|
||||
u'amavis.msgs': {
|
||||
'Meta': {'unique_together': "(('partition_tag', 'mail_id'),)", 'object_name': 'Msgs', 'db_table': "u'msgs'", 'managed': 'False'},
|
||||
'am_id': ('django.db.models.fields.CharField', [], {'max_length': '60'}),
|
||||
'client_addr': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}),
|
||||
'content': ('django.db.models.fields.CharField', [], {'max_length': '1', 'blank': 'True'}),
|
||||
'dsn_sent': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}),
|
||||
'from_addr': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}),
|
||||
'host': ('django.db.models.fields.CharField', [], {'max_length': '765'}),
|
||||
'mail_id': ('django.db.models.fields.CharField', [], {'max_length': '12', 'primary_key': 'True'}),
|
||||
'message_id': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}),
|
||||
'originating': ('django.db.models.fields.CharField', [], {'max_length': '3'}),
|
||||
'partition_tag': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'policy': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}),
|
||||
'quar_loc': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
|
||||
'quar_type': ('django.db.models.fields.CharField', [], {'max_length': '1', 'blank': 'True'}),
|
||||
'secret_id': ('django.db.models.fields.CharField', [], {'max_length': '12', 'blank': 'True'}),
|
||||
'sid': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['amavis.Maddr']", 'db_column': "'sid'"}),
|
||||
'size': ('django.db.models.fields.IntegerField', [], {}),
|
||||
'spam_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'subject': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}),
|
||||
'time_iso': ('django.db.models.fields.CharField', [], {'max_length': '48'}),
|
||||
'time_num': ('django.db.models.fields.IntegerField', [], {})
|
||||
},
|
||||
u'amavis.policy': {
|
||||
'Meta': {'object_name': 'Policy', 'db_table': "u'policy'", 'managed': 'False'},
|
||||
'addr_extension_bad_header': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'addr_extension_banned': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'addr_extension_spam': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'addr_extension_virus': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'archive_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'bad_header_admin': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'bad_header_lover': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}),
|
||||
'bad_header_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'banned_admin': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'banned_files_lover': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}),
|
||||
'banned_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'banned_rulenames': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'bypass_banned_checks': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '3', 'null': 'True'}),
|
||||
'bypass_header_checks': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}),
|
||||
'bypass_spam_checks': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '3', 'null': 'True'}),
|
||||
'bypass_virus_checks': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '3', 'null': 'True'}),
|
||||
'clean_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'disclaimer_options': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'forward_method': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'message_size_limit': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'newvirus_admin': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'policy_name': ('django.db.models.fields.CharField', [], {'max_length': '96', 'blank': 'True'}),
|
||||
'sa_userconf': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'sa_username': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'spam_admin': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'spam_dsn_cutoff_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'spam_kill_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'spam_lover': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}),
|
||||
'spam_quarantine_cutoff_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'spam_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'spam_subject_tag': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'spam_subject_tag2': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'spam_subject_tag3': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'spam_tag2_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'spam_tag3_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'spam_tag_level': ('django.db.models.fields.FloatField', [], {'null': 'True', 'blank': 'True'}),
|
||||
'unchecked_lover': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}),
|
||||
'unchecked_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'virus_admin': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'virus_lover': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}),
|
||||
'virus_quarantine_to': ('django.db.models.fields.CharField', [], {'max_length': '192', 'null': 'True', 'blank': 'True'}),
|
||||
'warnbadhrecip': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}),
|
||||
'warnbannedrecip': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'}),
|
||||
'warnvirusrecip': ('django.db.models.fields.CharField', [], {'max_length': '3', 'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'amavis.quarantine': {
|
||||
'Meta': {'ordering': "['-mail__time_num']", 'unique_together': "(('partition_tag', 'mail', 'chunk_ind'),)", 'object_name': 'Quarantine', 'db_table': "u'quarantine'", 'managed': 'False'},
|
||||
'chunk_ind': ('django.db.models.fields.IntegerField', [], {}),
|
||||
'mail': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['amavis.Msgs']", 'primary_key': 'True'}),
|
||||
'mail_text': ('django.db.models.fields.TextField', [], {}),
|
||||
'partition_tag': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'amavis.users': {
|
||||
'Meta': {'object_name': 'Users', 'db_table': "u'users'", 'managed': 'False'},
|
||||
'email': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
|
||||
'fullname': ('django.db.models.fields.CharField', [], {'max_length': '765', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'policy': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['amavis.Policy']"}),
|
||||
'priority': ('django.db.models.fields.IntegerField', [], {})
|
||||
},
|
||||
u'amavis.wblist': {
|
||||
'Meta': {'object_name': 'Wblist', 'db_table': "u'wblist'", 'managed': 'False'},
|
||||
'rid': ('django.db.models.fields.IntegerField', [], {'primary_key': 'True'}),
|
||||
'sid': ('django.db.models.fields.IntegerField', [], {'primary_key': 'True'}),
|
||||
'wb': ('django.db.models.fields.CharField', [], {'max_length': '30'})
|
||||
},
|
||||
u'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
u'auth.permission': {
|
||||
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
u'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
u'core.extension': {
|
||||
'Meta': {'object_name': 'Extension'},
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '150'})
|
||||
},
|
||||
u'core.log': {
|
||||
'Meta': {'object_name': 'Log'},
|
||||
'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'level': ('django.db.models.fields.CharField', [], {'max_length': '15'}),
|
||||
'logger': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
|
||||
'message': ('django.db.models.fields.CharField', [], {'max_length': '255'})
|
||||
},
|
||||
u'core.objectaccess': {
|
||||
'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"})
|
||||
},
|
||||
u'core.user': {
|
||||
'Meta': {'ordering': "['username']", 'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_local': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['core', 'admin', 'amavis']
|
||||
symmetrical = True
|
||||
@@ -117,9 +117,8 @@ Quarantine.prototype = {
|
||||
url: $link.attr("href"),
|
||||
data: "action=" + name + "&selection=" + selection.join(","),
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
success: $.proxy(this.action_cb, this)
|
||||
});
|
||||
dataType: 'json'
|
||||
}).done($.proxy(this.action_cb, this));
|
||||
},
|
||||
|
||||
release_selection: function(e) {
|
||||
@@ -139,9 +138,8 @@ Quarantine.prototype = {
|
||||
}
|
||||
$.ajax({
|
||||
url: $link.attr("href"),
|
||||
dataType: 'json',
|
||||
success: $.proxy(this.action_cb, this)
|
||||
});
|
||||
dataType: 'json'
|
||||
}).done($.proxy(this.action_cb, this));
|
||||
},
|
||||
|
||||
release: function(e) {
|
||||
@@ -175,11 +173,11 @@ Quarantine.prototype = {
|
||||
if (!$rawheaders.length) {
|
||||
$.ajax({
|
||||
url: $link.attr("href"),
|
||||
success: $.proxy(function(data) {
|
||||
$mailcontent.find("#table-container").append($(data));
|
||||
this.show_rawheaders($mailcontent);
|
||||
}, this)
|
||||
});
|
||||
global: false
|
||||
}).done($.proxy(function(data) {
|
||||
$mailcontent.find("#table-container").append($(data));
|
||||
this.show_rawheaders($mailcontent);
|
||||
}, this));
|
||||
return;
|
||||
}
|
||||
this.show_rawheaders($mailcontent);
|
||||
|
||||
@@ -7,14 +7,9 @@ function send_action(e, message) {
|
||||
}
|
||||
$.ajax({
|
||||
url: $link.attr("href"),
|
||||
dataType: 'json',
|
||||
success: function(data) {
|
||||
if (data.status == "ok") {
|
||||
$("body").notify("success", data.respmsg, 2000);
|
||||
} else {
|
||||
$("body").notify("error", data.respmsg);
|
||||
}
|
||||
}
|
||||
dataType: 'json'
|
||||
}).done(function(data) {
|
||||
$("body").notify("success", data, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
# coding: utf-8
|
||||
import email
|
||||
from django.shortcuts import render
|
||||
from django.http import HttpResponseRedirect, HttpResponse, Http404
|
||||
from django.http import HttpResponseRedirect, Http404
|
||||
from django.template import Template, Context
|
||||
from django.utils import simplejson
|
||||
from django.utils.translation import ugettext as _, ungettext
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.decorators \
|
||||
import login_required, user_passes_test
|
||||
from django.db.models import Q
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.exceptions import ModoboaException
|
||||
from modoboa.lib.exceptions import BadRequest
|
||||
from modoboa.lib.webutils import (
|
||||
getctx, ajax_response, ajax_simple_response
|
||||
getctx, ajax_response, render_to_json_response
|
||||
)
|
||||
from modoboa.lib.email_listing import parse_search_parameters
|
||||
from modoboa.extensions.admin.models import Mailbox, Domain
|
||||
from templatetags.amavis_tags import quar_menu, viewm_menu
|
||||
from modoboa.extensions.amavis.templatetags.amavis_tags import (
|
||||
quar_menu, viewm_menu
|
||||
)
|
||||
from .lib import selfservice, AMrelease
|
||||
from .sql_listing import SQLlisting, SQLemail, get_wrapper
|
||||
from .models import Msgrcpt
|
||||
@@ -47,7 +48,7 @@ def empty_quarantine(request):
|
||||
content = "<div class='alert alert-info'>%s</div>" % _("Empty quarantine")
|
||||
ctx = getctx("ok", level=2, listing=content, navbar="",
|
||||
menu=quar_menu(request.user))
|
||||
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
|
||||
return render_to_json_response(ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -79,7 +80,7 @@ def _listing(request):
|
||||
rcptfilter = request.session["pattern"]
|
||||
continue
|
||||
else:
|
||||
raise Exception("unsupported search criteria %s" % c)
|
||||
raise BadRequest("unsupported search criteria %s" % c)
|
||||
flt = nfilter if flt is None else flt | nfilter
|
||||
|
||||
msgs = get_wrapper().get_mails(request, rcptfilter)
|
||||
@@ -105,7 +106,7 @@ def _listing(request):
|
||||
navbar = lst.render_navbar(page, "listing/?")
|
||||
ctx = getctx("ok", listing=content, navbar=navbar,
|
||||
menu=quar_menu(request.user))
|
||||
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
|
||||
return render_to_json_response(ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -176,7 +177,7 @@ def viewmail(request, mail_id):
|
||||
""").render(Context({"url": reverse(getmailcontent, args=[mail_id])}))
|
||||
menu = viewm_menu(request.user, mail_id, rcpt)
|
||||
ctx = getctx("ok", menu=menu, listing=content)
|
||||
return HttpResponse(simplejson.dumps(ctx), mimetype="application/json")
|
||||
return render_to_json_response(ctx)
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -202,14 +203,14 @@ def check_mail_id(request, mail_id):
|
||||
def delete_selfservice(request, mail_id):
|
||||
rcpt = request.GET.get("rcpt", None)
|
||||
if rcpt is None:
|
||||
raise ModoboaException(_("Invalid request"))
|
||||
raise BadRequest(_("Invalid request"))
|
||||
try:
|
||||
msgrcpt = get_wrapper().get_recipient_message(rcpt, mail_id)
|
||||
msgrcpt.rs = 'D'
|
||||
msgrcpt.save()
|
||||
except Msgrcpt.DoesNotExist:
|
||||
raise ModoboaException(_("Invalid request"))
|
||||
return ajax_simple_response(dict(status="ok", respmsg=_("Message deleted")))
|
||||
raise BadRequest(_("Invalid request"))
|
||||
return render_to_json_response(_("Message deleted"))
|
||||
|
||||
|
||||
@selfservice(delete_selfservice)
|
||||
@@ -241,13 +242,13 @@ def release_selfservice(request, mail_id):
|
||||
rcpt = request.GET.get("rcpt", None)
|
||||
secret_id = request.GET.get("secret_id", None)
|
||||
if rcpt is None or secret_id is None:
|
||||
raise ModoboaException(_("Invalid request"))
|
||||
raise BadRequest(_("Invalid request"))
|
||||
try:
|
||||
msgrcpt = get_wrapper().get_recipient_message(rcpt, mail_id)
|
||||
except Msgrcpt.DoesNotExist:
|
||||
raise ModoboaException(_("Invalid request"))
|
||||
raise BadRequest(_("Invalid request"))
|
||||
if secret_id != msgrcpt.mail.secret_id:
|
||||
raise ModoboaException(_("Invalid request"))
|
||||
raise BadRequest(_("Invalid request"))
|
||||
if parameters.get_admin("USER_CAN_RELEASE") == "no":
|
||||
msgrcpt.rs = 'p'
|
||||
msg = _("Request sent")
|
||||
@@ -258,9 +259,9 @@ def release_selfservice(request, mail_id):
|
||||
rcpt.rs = 'R'
|
||||
msg = _("Message released")
|
||||
else:
|
||||
raise ModoboaException(result)
|
||||
raise BadRequest(result)
|
||||
msgrcpt.save()
|
||||
return ajax_simple_response(dict(status="ok", respmsg=msg))
|
||||
return render_to_json_response(msg)
|
||||
|
||||
|
||||
@selfservice(release_selfservice)
|
||||
@@ -329,4 +330,4 @@ def process(request):
|
||||
@user_passes_test(lambda u: u.group != 'SimpleUsers')
|
||||
def nbrequests(request):
|
||||
result = get_wrapper().get_pending_requests(request.user)
|
||||
return ajax_simple_response(dict(status="ok", requests=result))
|
||||
return render_to_json_response({'requests': result})
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
The *limits* extension
|
||||
----------------------
|
||||
"""
|
||||
import sys
|
||||
from django.contrib.auth.models import Permission, Group
|
||||
from django.db import IntegrityError
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.core.extensions import ModoExtension, exts_pool
|
||||
from .forms import ResourcePoolForm
|
||||
import controls
|
||||
|
||||
levents = [
|
||||
'GetExtraLimitTemplates'
|
||||
]
|
||||
|
||||
|
||||
class Limits(ModoExtension):
|
||||
@@ -19,7 +22,8 @@ class Limits(ModoExtension):
|
||||
label = "Limits"
|
||||
version = "1.0"
|
||||
description = ugettext_lazy(
|
||||
"Per administrator resources to limit the number of objects they can create"
|
||||
"Per administrator resources to limit the number of objects "
|
||||
"they can create"
|
||||
)
|
||||
|
||||
def init(self):
|
||||
@@ -47,94 +51,17 @@ class Limits(ModoExtension):
|
||||
|
||||
def load(self):
|
||||
from modoboa.extensions.limits.app_settings import ParametersForm
|
||||
from modoboa.extensions.limits import controls
|
||||
|
||||
parameters.register(ParametersForm, ugettext_lazy("Limits"))
|
||||
events.declare(levents)
|
||||
from modoboa.extensions.limits import general_callbacks
|
||||
if 'modoboa.extensions.limits.general_callbacks' in sys.modules:
|
||||
reload(general_callbacks)
|
||||
|
||||
def destroy(self):
|
||||
events.unregister_extension()
|
||||
parameters.unregister()
|
||||
Group.objects.get(name="Resellers").delete()
|
||||
|
||||
exts_pool.register_extension(Limits)
|
||||
|
||||
|
||||
@events.observe("GetExtraRoles")
|
||||
def get_extra_roles(user):
|
||||
if user.is_superuser:
|
||||
return [("Resellers", _("Reseller")), ]
|
||||
return []
|
||||
|
||||
|
||||
@events.observe("ExtraAdminContent")
|
||||
def display_pool_usage(user, target, currentpage):
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
if target != "leftcol" or user.is_superuser:
|
||||
return []
|
||||
if currentpage == "identities":
|
||||
names = ["mailboxes_limit", "mailbox_aliases_limit"]
|
||||
if user.has_perm("admin.add_domain"):
|
||||
names += ["domain_admins_limit"]
|
||||
else:
|
||||
names = ["domains_limit", "domain_aliases_limit"]
|
||||
limits = user.limitspool.limit_set.filter(name__in=names, maxvalue__gt=0)
|
||||
if len(limits) == 0:
|
||||
return []
|
||||
return [render_to_string("limits/poolusage.html", dict(limits=limits))]
|
||||
|
||||
|
||||
@events.observe("ExtraAccountForm")
|
||||
def extra_account_form(user, account=None):
|
||||
if not user.group in ["SuperAdmins", "Resellers"]:
|
||||
return []
|
||||
if account is not None and not account.group in ["Resellers", "DomainAdmins"]:
|
||||
return []
|
||||
|
||||
return [
|
||||
dict(
|
||||
id="resources", title=_("Resources"), cls=ResourcePoolForm
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@events.observe("CheckExtraAccountForm")
|
||||
def check_form_access(account, form):
|
||||
if form["id"] != "resources":
|
||||
return [True]
|
||||
if not account.belongs_to_group("Resellers") and \
|
||||
not account.belongs_to_group("DomainAdmins"):
|
||||
return [False]
|
||||
return [True]
|
||||
|
||||
|
||||
@events.observe("FillAccountInstances")
|
||||
def fill_account_instances(user, account, instances):
|
||||
if not user.is_superuser and not user.belongs_to_group("Resellers"):
|
||||
return
|
||||
if not account.belongs_to_group("Resellers") and \
|
||||
not account.belongs_to_group("DomainAdmins"):
|
||||
return
|
||||
instances["resources"] = account
|
||||
|
||||
|
||||
@events.observe("GetStaticContent")
|
||||
def get_static_content(user):
|
||||
if user.group == "SimpleUsers":
|
||||
return []
|
||||
return """<style>
|
||||
.resource {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.resource .progress {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.resource .progress .bar {
|
||||
color: #000000;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$(".progress").tooltip();
|
||||
});
|
||||
</script>
|
||||
"""
|
||||
|
||||
@@ -39,3 +39,7 @@ class ParametersForm(AdminParametersForm):
|
||||
help_text=_("Maximum number of allowed aliases for a new administrator"),
|
||||
widget=forms.widgets.TextInput(attrs={"class": "span1"})
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(AdminParametersForm, self).__init__(*args, **kwargs)
|
||||
self._load_extra_parameters('A')
|
||||
|
||||
@@ -6,43 +6,27 @@
|
||||
"""
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.permissions import get_object_owner
|
||||
from .lib import LimitReached
|
||||
from .models import limits_tpl, LimitsPool
|
||||
from .lib import LimitReached, inc_limit_usage, dec_limit_usage
|
||||
from .models import LimitTemplates, LimitsPool
|
||||
|
||||
|
||||
def check_limit(user, lname):
|
||||
def check_limit(user, lname, count=1):
|
||||
"""Check if a user has reached a defined limit
|
||||
|
||||
We check if the user will not reach the given limit after this
|
||||
creation.
|
||||
|
||||
Raise a ``LimitReached`` exception if the limit is reached.
|
||||
|
||||
:param user: a ``User`` object
|
||||
:param int count: the number of objects the user tries to create
|
||||
:raises: ``LimitReached``
|
||||
"""
|
||||
try:
|
||||
if user.limitspool.will_be_reached(lname):
|
||||
if user.limitspool.will_be_reached(lname, count):
|
||||
raise LimitReached(user.limitspool.get_limit(lname))
|
||||
except LimitsPool.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
def inc_limit(user, lname):
|
||||
try:
|
||||
user.limitspool.inc_curvalue(lname)
|
||||
except LimitsPool.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
def dec_limit(user, lname):
|
||||
if user is None:
|
||||
return
|
||||
try:
|
||||
user.limitspool.dec_curvalue(lname)
|
||||
except LimitsPool.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
def move_pool_resource(owner, user):
|
||||
"""Move resource from one pool to another
|
||||
|
||||
@@ -54,7 +38,7 @@ def move_pool_resource(owner, user):
|
||||
except LimitsPool.DoesNotExist:
|
||||
return
|
||||
if not owner.is_superuser:
|
||||
for ltpl in limits_tpl:
|
||||
for ltpl in LimitTemplates().templates:
|
||||
l = user.limitspool.get_limit(ltpl[0])
|
||||
if l.maxvalue < 0:
|
||||
continue
|
||||
@@ -66,15 +50,16 @@ def move_pool_resource(owner, user):
|
||||
user.limitspool.delete()
|
||||
|
||||
|
||||
@events.observe('CreateDomain')
|
||||
@events.observe('DomainCreated')
|
||||
def inc_nb_domains(user, domain):
|
||||
inc_limit(user, 'domains_limit')
|
||||
inc_limit_usage(user, 'domains_limit')
|
||||
|
||||
|
||||
@events.observe('DeleteDomain')
|
||||
def dec_nb_domains(domain):
|
||||
owner = get_object_owner(domain)
|
||||
dec_limit(owner, 'domains_limit')
|
||||
@events.observe('DomainDeleted')
|
||||
def dec_nb_domains(domain, owner=None):
|
||||
if owner is None:
|
||||
owner = get_object_owner(domain)
|
||||
dec_limit_usage(owner, 'domains_limit')
|
||||
for domalias in domain.domainalias_set.all():
|
||||
dec_nb_domaliases(domalias)
|
||||
for mailbox in domain.mailbox_set.all():
|
||||
@@ -85,7 +70,7 @@ def dec_nb_domains(domain):
|
||||
|
||||
@events.observe('DomainAliasCreated')
|
||||
def inc_nb_domaliases(user, domalias):
|
||||
inc_limit(user, 'domain_aliases_limit')
|
||||
inc_limit_usage(user, 'domain_aliases_limit')
|
||||
|
||||
|
||||
@events.observe('DomainAliasDeleted')
|
||||
@@ -96,15 +81,15 @@ def dec_nb_domaliases(domainaliases):
|
||||
domainaliases = [domainaliases]
|
||||
for domainalias in domainaliases:
|
||||
owner = get_object_owner(domainalias)
|
||||
dec_limit(owner, 'domain_aliases_limit')
|
||||
dec_limit_usage(owner, 'domain_aliases_limit')
|
||||
|
||||
|
||||
@events.observe('CreateMailbox')
|
||||
@events.observe('MailboxCreated')
|
||||
def inc_nb_mailboxes(user, mailbox):
|
||||
inc_limit(user, 'mailboxes_limit')
|
||||
inc_limit_usage(user, 'mailboxes_limit')
|
||||
|
||||
|
||||
@events.observe('DeleteMailbox')
|
||||
@events.observe('MailboxDeleted')
|
||||
def dec_nb_mailboxes(mailboxes):
|
||||
from modoboa.extensions.admin.models import Mailbox
|
||||
|
||||
@@ -112,40 +97,66 @@ def dec_nb_mailboxes(mailboxes):
|
||||
mailboxes = [mailboxes]
|
||||
for mailbox in mailboxes:
|
||||
owner = get_object_owner(mailbox)
|
||||
dec_limit(owner, 'mailboxes_limit')
|
||||
dec_limit_usage(owner, 'mailboxes_limit')
|
||||
|
||||
|
||||
@events.observe('MailboxAliasCreated')
|
||||
def inc_nb_mbaliases(user, mailboxalias):
|
||||
inc_limit(user, 'mailbox_aliases_limit')
|
||||
inc_limit_usage(user, 'mailbox_aliases_limit')
|
||||
|
||||
|
||||
@events.observe('MailboxAliasDeleted')
|
||||
def dec_nb_mbaliases(mailboxalias):
|
||||
owner = get_object_owner(mailboxalias)
|
||||
dec_limit(owner, 'mailbox_aliases_limit')
|
||||
def dec_nb_mbaliases(mailboxaliases):
|
||||
from modoboa.extensions.admin.models import Alias
|
||||
|
||||
if isinstance(mailboxaliases, Alias):
|
||||
mailboxaliases = [mailboxaliases]
|
||||
for alias in mailboxaliases:
|
||||
owner = get_object_owner(alias)
|
||||
dec_limit_usage(owner, 'mailbox_aliases_limit')
|
||||
|
||||
|
||||
@events.observe('CanCreate')
|
||||
def can_create_new_object(user, objtype):
|
||||
check_limit(user, '%s_limit' % objtype)
|
||||
def can_create_new_object(user, objtype, count=1):
|
||||
check_limit(user, '%s_limit' % objtype, count)
|
||||
|
||||
|
||||
@events.observe("AccountCreated")
|
||||
def create_pool(user):
|
||||
owner = get_object_owner(user)
|
||||
if not owner.is_superuser and \
|
||||
not owner.belongs_to_group("Resellers"):
|
||||
if not owner.group in ['SuperAdmins', 'Resellers']:
|
||||
return
|
||||
|
||||
if user.belongs_to_group("DomainAdmins"):
|
||||
if user.group == 'DomainAdmins':
|
||||
check_limit(owner, 'domain_admins_limit')
|
||||
inc_limit(owner, 'domain_admins_limit')
|
||||
inc_limit_usage(owner, 'domain_admins_limit')
|
||||
|
||||
if user.group in ["DomainAdmins", "Resellers"]:
|
||||
p = LimitsPool(user=user)
|
||||
p.save()
|
||||
p.create_limits()
|
||||
if user.group in ['DomainAdmins', 'Resellers']:
|
||||
p, created = LimitsPool.objects.get_or_create(user=user)
|
||||
p.create_limits(owner)
|
||||
|
||||
|
||||
@events.observe("UserCanSetRole")
|
||||
def user_can_set_role(user, role, account=None):
|
||||
"""Check if the user can still set this role.
|
||||
|
||||
The only interesting case concerns resellers defining new domain
|
||||
administrators. We want to check if they are allowed to do this
|
||||
operation before any modification is made to :keyword:`account`.
|
||||
|
||||
:param ``User`` user: connected user
|
||||
:param ``User`` account: account modified (None on creation)
|
||||
:param str newrole: role to check
|
||||
"""
|
||||
if role == 'DomainAdmins':
|
||||
lname = 'domain_admins_limit'
|
||||
else:
|
||||
return [True]
|
||||
if user.is_superuser or not user.limitspool.will_be_reached(lname):
|
||||
return [True]
|
||||
if account is not None and account.group == role:
|
||||
return [True]
|
||||
return [False]
|
||||
|
||||
|
||||
@events.observe("AccountModified")
|
||||
@@ -164,19 +175,19 @@ def on_account_modified(old, new):
|
||||
except LimitsPool.DoesNotExist:
|
||||
p = LimitsPool(user=new)
|
||||
p.save()
|
||||
p.create_limits()
|
||||
p.create_limits(owner)
|
||||
|
||||
if not new.group in ["DomainAdmins", "Resellers"]:
|
||||
move_pool_resource(owner, new)
|
||||
|
||||
if old.oldgroup == "DomainAdmins":
|
||||
if new.group != "DomainAdmins":
|
||||
dec_limit(owner, 'domain_admins_limit')
|
||||
dec_limit_usage(owner, 'domain_admins_limit')
|
||||
return
|
||||
|
||||
if new.group == "DomainAdmins":
|
||||
check_limit(owner, 'domain_admins_limit')
|
||||
inc_limit(owner, 'domain_admins_limit')
|
||||
inc_limit_usage(owner, 'domain_admins_limit')
|
||||
|
||||
|
||||
@events.observe("AccountDeleted")
|
||||
@@ -188,4 +199,21 @@ def on_account_deleted(account, byuser, **kwargs):
|
||||
move_pool_resource(owner, account)
|
||||
|
||||
if account.group == "DomainAdmins":
|
||||
dec_limit(owner, 'domain_admins_limit')
|
||||
dec_limit_usage(owner, 'domain_admins_limit')
|
||||
|
||||
|
||||
@events.observe('DomainOwnershipRemoved')
|
||||
def domain_ownership_removed(reseller, domain):
|
||||
"""DomainOwnershipRemoved listener.
|
||||
|
||||
The access :keyword:`reseller` had to :keyword:`domain` has been
|
||||
removed by a super adminstrator. We must decrement all limit
|
||||
usages.
|
||||
|
||||
:param ``User`` reseller: reseller that created :keyword:`domain`
|
||||
:param ``Domain`` domain: domain
|
||||
"""
|
||||
dec_nb_domains(domain, reseller)
|
||||
for dadmin in domain.admins:
|
||||
if reseller.is_owner(dadmin):
|
||||
dec_limit_usage(reseller, 'domain_admins_limit')
|
||||
|
||||
@@ -1,44 +1,26 @@
|
||||
# coding: utf-8
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from modoboa.lib import parameters
|
||||
from .models import limits_tpl
|
||||
from .models import LimitTemplates
|
||||
from .lib import BadLimitValue, UnsufficientResource
|
||||
|
||||
|
||||
class ResourcePoolForm(forms.Form):
|
||||
domain_admins_limit = forms.IntegerField(
|
||||
label=ugettext_lazy("Domain admins"),
|
||||
help_text=ugettext_lazy("Maximum number of domain administrators this user can create")
|
||||
)
|
||||
domains_limit = forms.IntegerField(
|
||||
label=ugettext_lazy("Domains"),
|
||||
help_text=ugettext_lazy("Maximum number of domains this user can create")
|
||||
)
|
||||
domain_aliases_limit = forms.IntegerField(
|
||||
label=ugettext_lazy("Domain aliases"),
|
||||
help_text=ugettext_lazy("Maximum number of domain aliases this user can create")
|
||||
)
|
||||
mailboxes_limit = forms.IntegerField(
|
||||
label=ugettext_lazy("Mailboxes"),
|
||||
help_text=ugettext_lazy("Maximum number of mailboxes this user can create")
|
||||
)
|
||||
mailbox_aliases_limit = forms.IntegerField(
|
||||
label=ugettext_lazy("Mailbox aliases"),
|
||||
help_text=ugettext_lazy("Maximum number of mailbox aliases this user can create")
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.account = None
|
||||
if "instance" in kwargs:
|
||||
self.account = kwargs["instance"]
|
||||
del kwargs["instance"]
|
||||
super(ResourcePoolForm, self).__init__(*args, **kwargs)
|
||||
for tpl in LimitTemplates().templates:
|
||||
if len(tpl) > 3 and self.account is not None and self.account.group != tpl[3]:
|
||||
continue
|
||||
self.fields[tpl[0]] = forms.IntegerField(
|
||||
label=tpl[1], help_text=tpl[2]
|
||||
)
|
||||
if hasattr(self, "account"):
|
||||
if self.account.group != "Resellers":
|
||||
del self.fields["domain_admins_limit"]
|
||||
del self.fields["domains_limit"]
|
||||
del self.fields["domain_aliases_limit"]
|
||||
self.load_from_user(self.account)
|
||||
|
||||
def check_limit_value(self, lname):
|
||||
@@ -46,29 +28,30 @@ class ResourcePoolForm(forms.Form):
|
||||
raise forms.ValidationError(_("Invalid limit"))
|
||||
return self.cleaned_data[lname]
|
||||
|
||||
def clean_domains_limit(self):
|
||||
return self.check_limit_value("domains_limit")
|
||||
|
||||
def clean_domain_aliases_limit(self):
|
||||
return self.check_limit_value("domain_aliases_limit")
|
||||
|
||||
def clean_mailboxes_limit(self):
|
||||
return self.check_limit_value("mailboxes_limit")
|
||||
|
||||
def clean_mailbox_aliases_limit(self):
|
||||
return self.check_limit_value("mailbox_aliases_limit")
|
||||
def clean(self):
|
||||
cleaned_data = super(ResourcePoolForm, self).clean()
|
||||
for lname in self.fields.keys():
|
||||
if not lname in self._errors and cleaned_data[lname] < -1:
|
||||
self._errors[lname] = self.error_class([_('Invalid limit')])
|
||||
del cleaned_data[lname]
|
||||
return cleaned_data
|
||||
|
||||
def load_from_user(self, user):
|
||||
for l in limits_tpl:
|
||||
if not l[0] in self.fields:
|
||||
continue
|
||||
self.fields[l[0]].initial = user.limitspool.getmaxvalue(l[0])
|
||||
for lname in self.fields.keys():
|
||||
self.fields[lname].initial = user.limitspool.getmaxvalue(lname)
|
||||
# The following lines will become useless in a near
|
||||
# future.
|
||||
if self.fields[l[0]].initial == -2:
|
||||
self.fields[l[0]].initial = parameters.get_admin("DEFLT_%s" % l[0].upper())
|
||||
if self.fields[lname].initial == -2:
|
||||
self.fields[lname].initial = parameters.get_admin("DEFLT_%s" % lname.upper())
|
||||
|
||||
def allocate_from_pool(self, limit, pool):
|
||||
"""Allocate resource using an existing pool.
|
||||
|
||||
When a reseller creates a domain administrator, he generally
|
||||
assigns him resource to create new objetcs. As a reseller may
|
||||
also be limited, the resource he gives is taken from its own
|
||||
pool.
|
||||
"""
|
||||
ol = pool.get_limit(limit.name)
|
||||
if ol.maxvalue == -2:
|
||||
raise BadLimitValue(_("Your resources are not initialized yet"))
|
||||
@@ -92,7 +75,7 @@ class ResourcePoolForm(forms.Form):
|
||||
from modoboa.lib.permissions import get_object_owner
|
||||
|
||||
owner = get_object_owner(self.account)
|
||||
for ltpl in limits_tpl:
|
||||
for ltpl in LimitTemplates().templates:
|
||||
if not ltpl[0] in self.cleaned_data:
|
||||
continue
|
||||
l = self.account.limitspool.limit_set.get(name=ltpl[0])
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
from django.utils.translation import ugettext as _
|
||||
from modoboa.lib import events
|
||||
from modoboa.extensions.limits.models import LimitTemplates
|
||||
from modoboa.extensions.limits.forms import ResourcePoolForm
|
||||
|
||||
|
||||
@events.observe("GetExtraRoles")
|
||||
def get_extra_roles(user):
|
||||
if user.is_superuser:
|
||||
return [("Resellers", _("Reseller")), ]
|
||||
return []
|
||||
|
||||
|
||||
@events.observe("ExtraAdminContent")
|
||||
def display_pool_usage(user, target, currentpage):
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
if target != "leftcol" or user.is_superuser:
|
||||
return []
|
||||
if currentpage == "identities":
|
||||
names = ["mailboxes_limit", "mailbox_aliases_limit"]
|
||||
if user.has_perm("admin.add_domain"):
|
||||
names += ["domain_admins_limit"]
|
||||
else:
|
||||
names = [
|
||||
tpl[0] for tpl in LimitTemplates().templates
|
||||
if tpl[0] not in ["domain_admins_limit", "mailboxes_limit",
|
||||
"mailbox_aliases_limit"]
|
||||
and (len(tpl) == 3 or tpl[3] == user.group)
|
||||
]
|
||||
|
||||
limits = user.limitspool.limit_set.filter(name__in=names, maxvalue__gt=0)
|
||||
if len(limits) == 0:
|
||||
return []
|
||||
return [render_to_string("limits/poolusage.html", dict(limits=limits))]
|
||||
|
||||
|
||||
@events.observe("ExtraAccountForm")
|
||||
def extra_account_form(user, account=None):
|
||||
if not user.group in ["SuperAdmins", "Resellers"]:
|
||||
return []
|
||||
if account is not None and \
|
||||
not account.group in ["Resellers", "DomainAdmins"]:
|
||||
return []
|
||||
|
||||
return [
|
||||
dict(
|
||||
id="resources", title=_("Resources"), cls=ResourcePoolForm
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@events.observe("CheckExtraAccountForm")
|
||||
def check_form_access(account, form):
|
||||
if form["id"] != "resources":
|
||||
return [True]
|
||||
if not account.belongs_to_group("Resellers") and \
|
||||
not account.belongs_to_group("DomainAdmins"):
|
||||
return [False]
|
||||
return [True]
|
||||
|
||||
|
||||
@events.observe("FillAccountInstances")
|
||||
def fill_account_instances(user, account, instances):
|
||||
if not user.is_superuser and not user.belongs_to_group("Resellers"):
|
||||
return
|
||||
if not account.belongs_to_group("Resellers") and \
|
||||
not account.belongs_to_group("DomainAdmins"):
|
||||
return
|
||||
instances["resources"] = account
|
||||
|
||||
|
||||
@events.observe("GetStaticContent")
|
||||
def get_static_content(caller, user):
|
||||
if caller not in ['domains', 'identities']:
|
||||
return []
|
||||
if user.group == "SimpleUsers":
|
||||
return []
|
||||
return ["""<style>
|
||||
.resource {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.resource .progress {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.resource .progress .bar {
|
||||
color: #000000;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
$(".progress").tooltip();
|
||||
});
|
||||
</script>
|
||||
"""]
|
||||
@@ -6,9 +6,12 @@
|
||||
"""
|
||||
from django.utils.translation import ugettext as _
|
||||
from modoboa.lib.exceptions import ModoboaException
|
||||
from modoboa.extensions.limits.models import LimitsPool
|
||||
|
||||
|
||||
class LimitReached(ModoboaException):
|
||||
http_code = 403
|
||||
|
||||
def __init__(self, limit):
|
||||
self.limit = limit
|
||||
|
||||
@@ -17,6 +20,8 @@ class LimitReached(ModoboaException):
|
||||
|
||||
|
||||
class UnsufficientResource(ModoboaException):
|
||||
http_code = 424
|
||||
|
||||
def __init__(self, limit):
|
||||
self.limit = limit
|
||||
|
||||
@@ -25,4 +30,23 @@ class UnsufficientResource(ModoboaException):
|
||||
|
||||
|
||||
class BadLimitValue(ModoboaException):
|
||||
pass
|
||||
http_code = 400
|
||||
|
||||
|
||||
def inc_limit_usage(user, lname):
|
||||
"""Increase a given limit usage.
|
||||
|
||||
"""
|
||||
try:
|
||||
user.limitspool.inc_curvalue(lname)
|
||||
except LimitsPool.DoesNotExist:
|
||||
pass
|
||||
|
||||
|
||||
def dec_limit_usage(user, lname):
|
||||
if user is None:
|
||||
return
|
||||
try:
|
||||
user.limitspool.dec_curvalue(lname)
|
||||
except LimitsPool.DoesNotExist:
|
||||
pass
|
||||
|
||||
@@ -2,27 +2,56 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.lib.singleton import Singleton
|
||||
from django.conf import settings
|
||||
from modoboa.lib import events
|
||||
|
||||
limits_tpl = [
|
||||
("domain_admins_limit", ugettext_lazy("Domain admins")),
|
||||
("domains_limit", ugettext_lazy("Domains")),
|
||||
("domain_aliases_limit", ugettext_lazy("Domain aliases")),
|
||||
("mailboxes_limit", ugettext_lazy("Mailboxes")),
|
||||
("mailbox_aliases_limit", ugettext_lazy("Mailbox aliases"))
|
||||
]
|
||||
|
||||
class LimitTemplates(Singleton):
|
||||
|
||||
def __init__(self):
|
||||
self.__templates = [
|
||||
("domain_admins_limit", ugettext_lazy("Domain admins"),
|
||||
ugettext_lazy("Maximum number of domain administrators this user can create"),
|
||||
'Resellers'),
|
||||
("domains_limit", ugettext_lazy("Domains"),
|
||||
ugettext_lazy("Maximum number of domains this user can create"),
|
||||
'Resellers'),
|
||||
("domain_aliases_limit", ugettext_lazy("Domain aliases"),
|
||||
ugettext_lazy("Maximum number of domain aliases this user can create"),
|
||||
'Resellers'),
|
||||
("mailboxes_limit", ugettext_lazy("Mailboxes"),
|
||||
ugettext_lazy("Maximum number of mailboxes this user can create")),
|
||||
("mailbox_aliases_limit", ugettext_lazy("Mailbox aliases"),
|
||||
ugettext_lazy("Maximum number of mailbox aliases this user can create"))
|
||||
]
|
||||
|
||||
@property
|
||||
def templates(self):
|
||||
return self.__templates + events.raiseQueryEvent('GetExtraLimitTemplates')
|
||||
|
||||
|
||||
class LimitsPool(models.Model):
|
||||
user = models.OneToOneField(settings.AUTH_USER_MODEL)
|
||||
|
||||
def create_limits(self):
|
||||
for ltpl in limits_tpl:
|
||||
l = Limit()
|
||||
l.name = ltpl[0]
|
||||
l.pool = self
|
||||
l.maxvalue = int(parameters.get_admin("DEFLT_%s" % ltpl[0].upper()))
|
||||
l.save()
|
||||
def create_limits(self, creator):
|
||||
"""Create limits for this pool.
|
||||
|
||||
All limits defined into ``LimitTemplates`` will be created. If
|
||||
the creator is a super administrator, the default maximum values
|
||||
will be used.
|
||||
|
||||
:param ``User`` creator: user creating this pool
|
||||
"""
|
||||
for ltpl in LimitTemplates().templates:
|
||||
try:
|
||||
Limit.objects.get(name=ltpl[0], pool=self)
|
||||
except Limit.DoesNotExist:
|
||||
maxvalue = int(parameters.get_admin("DEFLT_%s" % ltpl[0].upper())) \
|
||||
if creator.is_superuser else 0
|
||||
Limit.objects.create(
|
||||
name=ltpl[0], pool=self, maxvalue=maxvalue
|
||||
)
|
||||
|
||||
def getcurvalue(self, lname):
|
||||
l = self.limit_set.get(name=lname)
|
||||
@@ -32,6 +61,11 @@ class LimitsPool(models.Model):
|
||||
l = self.limit_set.get(name=lname)
|
||||
return l.maxvalue
|
||||
|
||||
def set_maxvalue(self, lname, value):
|
||||
l = self.limit_set.get(name=lname)
|
||||
l.maxvalue = value
|
||||
l.save()
|
||||
|
||||
def inc_curvalue(self, lname, nb=1):
|
||||
l = self.limit_set.get(name=lname)
|
||||
l.curvalue += nb
|
||||
@@ -92,7 +126,7 @@ class Limit(models.Model):
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
for l in limits_tpl:
|
||||
for l in LimitTemplates().templates:
|
||||
if l[0] == self.name:
|
||||
return l[1]
|
||||
return ""
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from modoboa.lib.tests import ExtTestCase
|
||||
from modoboa.core.factories import UserFactory
|
||||
from modoboa.extensions.admin.factories import DomainFactory, MailboxFactory
|
||||
from modoboa.lib import parameters
|
||||
from modoboa.core.models import User
|
||||
from modoboa.core.factories import UserFactory
|
||||
from modoboa.extensions.admin.factories import (
|
||||
DomainFactory, MailboxFactory, populate_database
|
||||
)
|
||||
from modoboa.extensions.admin.models import Alias, Domain, DomainAlias
|
||||
from modoboa.extensions.limits.models import LimitTemplates
|
||||
|
||||
|
||||
class PermissionsTestCase(ExtTestCase):
|
||||
fixtures = ["initial_users.json"]
|
||||
name = "limits"
|
||||
|
||||
def setUp(self):
|
||||
super(PermissionsTestCase, self).setUp()
|
||||
dom = DomainFactory(name='test.com')
|
||||
self.admin = UserFactory(
|
||||
username='admin@test.com', groups=('DomainAdmins',)
|
||||
)
|
||||
MailboxFactory(address='admin', domain=dom, user=self.admin)
|
||||
dom.add_admin(self.admin)
|
||||
self.activate_extensions('limits')
|
||||
populate_database()
|
||||
|
||||
def test_domainadmin_deletes_reseller(self):
|
||||
"""Check if a domain admin can delete a reseller.
|
||||
@@ -26,13 +26,378 @@ class PermissionsTestCase(ExtTestCase):
|
||||
values = dict(
|
||||
username="reseller@test.com", first_name="Reseller", last_name="",
|
||||
password1="toto", password2="toto", role="Resellers",
|
||||
is_active=True, email="reseller@test.com", stepid=2
|
||||
is_active=True, email="reseller@test.com", stepid='step2'
|
||||
)
|
||||
self.check_ajax_post(reverse("modoboa.extensions.admin.views.identity.newaccount"), values)
|
||||
self.ajax_post(reverse("modoboa.extensions.admin.views.identity.newaccount"), values)
|
||||
account = User.objects.get(username="reseller@test.com")
|
||||
self.clt.logout()
|
||||
self.clt.login(username="admin@test.com", password="toto")
|
||||
self.check_ajax_get(
|
||||
resp = self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.delaccount", args=[account.id]),
|
||||
{}, status="ko", respmsg="Permission denied"
|
||||
{}, status=403
|
||||
)
|
||||
self.assertEqual(resp, "Permission denied")
|
||||
|
||||
|
||||
class ResourceTestCase(ExtTestCase):
|
||||
fixtures = ["initial_users.json"]
|
||||
|
||||
def setUp(self):
|
||||
"""Custom setUp method.
|
||||
|
||||
The 'limits' is manually loaded to ensure extra parameters
|
||||
provided by 'postfix_relay_domains' are properly received.
|
||||
"""
|
||||
super(ResourceTestCase, self).setUp()
|
||||
self.activate_extensions('limits')
|
||||
for tpl in LimitTemplates().templates:
|
||||
parameters.save_admin('DEFLT_%s' % tpl[0].upper(), 2, app='limits')
|
||||
populate_database()
|
||||
|
||||
def _create_account(self, username, role='SimpleUsers', status=200):
|
||||
values = dict(
|
||||
username=username, first_name="Tester", last_name="Toto",
|
||||
password1="toto", password2="toto", role=role,
|
||||
quota_act=True,
|
||||
is_active=True, email=username, stepid='step2',
|
||||
)
|
||||
return self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.newaccount"),
|
||||
values, status
|
||||
)
|
||||
|
||||
def _create_alias(self, email, rcpt='user@test.com', status=200):
|
||||
values = dict(
|
||||
email=email, recipients=rcpt, enabled=True
|
||||
)
|
||||
return self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.alias.newalias"),
|
||||
values, status
|
||||
)
|
||||
|
||||
def _create_domain(self, name, status=200, withtpl=False):
|
||||
values = {
|
||||
"name": name, "quota": 100, "create_dom_admin": "no",
|
||||
"create_aliases": "no", "stepid": 'step2'
|
||||
}
|
||||
if withtpl:
|
||||
values['create_dom_admin'] = 'yes'
|
||||
values['dom_admin_username'] = 'admin'
|
||||
values['create_aliases'] = 'yes'
|
||||
return self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.domain.newdomain"),
|
||||
values, status
|
||||
)
|
||||
|
||||
def _domain_alias_operation(self, optype, domain, name, status=200):
|
||||
dom = Domain.objects.get(name=domain)
|
||||
values = {
|
||||
'name': dom.name, 'quota': dom.quota, 'enabled': dom.enabled,
|
||||
}
|
||||
aliases = [alias.name for alias in dom.domainalias_set.all()]
|
||||
if optype == 'add':
|
||||
aliases.append(name)
|
||||
else:
|
||||
aliases.remove(name)
|
||||
for cpt, alias in enumerate(aliases):
|
||||
fname = 'aliases' if not cpt else 'aliases_%d' % cpt
|
||||
values[fname] = alias
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.domain.editdomain",
|
||||
args=[dom.id]),
|
||||
values, status
|
||||
)
|
||||
|
||||
def _check_limit(self, name, curvalue, maxvalue):
|
||||
l = self.user.limitspool.get_limit('%s_limit' % name)
|
||||
self.assertEqual(l.curvalue, curvalue)
|
||||
self.assertEqual(l.maxvalue, maxvalue)
|
||||
|
||||
|
||||
class DomainAdminTestCase(ResourceTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(DomainAdminTestCase, self).setUp()
|
||||
self.user = User.objects.get(username='admin@test.com')
|
||||
self.user.limitspool.set_maxvalue('mailboxes_limit', 2)
|
||||
self.user.limitspool.set_maxvalue('mailbox_aliases_limit', 2)
|
||||
self.clt.logout()
|
||||
self.clt.login(username='admin@test.com', password='toto')
|
||||
|
||||
def test_mailboxes_limit(self):
|
||||
self._create_account('tester1@test.com')
|
||||
self._check_limit('mailboxes', 1, 2)
|
||||
self._create_account('tester2@test.com')
|
||||
self._check_limit('mailboxes', 2, 2)
|
||||
resp = self._create_account('tester3@test.com', status=403)
|
||||
self._check_limit('mailboxes', 2, 2)
|
||||
self.ajax_post(
|
||||
reverse('modoboa.extensions.admin.views.identity.delaccount',
|
||||
args=[User.objects.get(username='tester2@test.com').id]),
|
||||
{}
|
||||
)
|
||||
self._check_limit('mailboxes', 1, 2)
|
||||
|
||||
def test_aliases_limit(self):
|
||||
self._create_alias('alias1@test.com')
|
||||
self._check_limit('mailbox_aliases', 1, 2)
|
||||
self._create_alias('alias2@test.com')
|
||||
self._check_limit('mailbox_aliases', 2, 2)
|
||||
resp = self._create_alias('alias3@test.com', status=403)
|
||||
self._check_limit('mailbox_aliases', 2, 2)
|
||||
self.ajax_post(
|
||||
reverse('modoboa.extensions.admin.views.alias.delalias') + '?selection=%d' \
|
||||
% Alias.objects.get(address='alias2', domain__name='test.com').id,
|
||||
{}
|
||||
)
|
||||
self._check_limit('mailbox_aliases', 1, 2)
|
||||
|
||||
def test_aliases_limit_through_account_form(self):
|
||||
user = User.objects.get(username='user@test.com')
|
||||
values = dict(
|
||||
username=user.username, role=user.group,
|
||||
is_active=user.is_active, email=user.email, quota_act=True,
|
||||
aliases="alias1@test.com", aliases_1="alias2@test.com"
|
||||
)
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[user.id]),
|
||||
values
|
||||
)
|
||||
Alias.objects.get(address='alias1', domain__name='test.com')
|
||||
self._check_limit('mailbox_aliases', 2, 2)
|
||||
|
||||
|
||||
class ResellerTestCase(ResourceTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ResellerTestCase, self).setUp()
|
||||
self.user = UserFactory.create(
|
||||
username='reseller', groups=('Resellers',)
|
||||
)
|
||||
self.clt.logout()
|
||||
self.clt.login(username='reseller', password='toto')
|
||||
|
||||
def test_domains_limit(self):
|
||||
self._create_domain('domain1.tld')
|
||||
self._check_limit('domains', 1, 2)
|
||||
self._create_domain('domain2.tld')
|
||||
self._check_limit('domains', 2, 2)
|
||||
resp = self._create_domain('domain3.tld', 403)
|
||||
self._check_limit('domains', 2, 2)
|
||||
self.ajax_post(
|
||||
reverse('modoboa.extensions.admin.views.domain.deldomain',
|
||||
args=[Domain.objects.get(name='domain2.tld').id]),
|
||||
{}
|
||||
)
|
||||
self._check_limit('domains', 1, 2)
|
||||
|
||||
def test_domain_aliases_limit(self):
|
||||
self._create_domain('pouet.com')
|
||||
self._domain_alias_operation('add', 'pouet.com', 'domain_alias1.tld')
|
||||
self._check_limit('domain_aliases', 1, 2)
|
||||
self._domain_alias_operation('add', 'pouet.com', 'domain_alias2.tld')
|
||||
self._check_limit('domain_aliases', 2, 2)
|
||||
resp = self._domain_alias_operation(
|
||||
'add', 'pouet.com', 'domain_alias3.tld', 403
|
||||
)
|
||||
self._check_limit('domain_aliases', 2, 2)
|
||||
self._domain_alias_operation('delete', 'pouet.com', 'domain_alias2.tld')
|
||||
self._check_limit('domain_aliases', 1, 2)
|
||||
|
||||
def test_domain_admins_limit(self):
|
||||
self._create_domain('domain.tld')
|
||||
self._create_account('admin1@domain.tld', role='DomainAdmins')
|
||||
self._check_limit('domain_admins', 1, 2)
|
||||
self._create_account('admin2@domain.tld', role='DomainAdmins')
|
||||
self._check_limit('domain_admins', 2, 2)
|
||||
resp = self._create_account('admin3@domain.tld', role='DomainAdmins', status=400)
|
||||
self.assertEqual(
|
||||
resp['form_errors']['role'][0],
|
||||
'Select a valid choice. DomainAdmins is not one of the available choices.'
|
||||
)
|
||||
self._check_limit('domain_admins', 2, 2)
|
||||
|
||||
self.user.limitspool.set_maxvalue('mailboxes_limit', 3)
|
||||
self._create_account('user1@domain.tld')
|
||||
user = User.objects.get(username='user1@domain.tld')
|
||||
values = {
|
||||
'username': user.username, 'role': 'DomainAdmins', 'quota_act': True,
|
||||
'is_active': user.is_active, 'email': user.email
|
||||
}
|
||||
resp = self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[user.id]),
|
||||
values, status=400
|
||||
)
|
||||
self.assertEqual(
|
||||
resp['form_errors']['role'][0],
|
||||
'Select a valid choice. DomainAdmins is not one of the available choices.'
|
||||
)
|
||||
self._check_limit('domain_admins', 2, 2)
|
||||
|
||||
def test_domain_admin_resource_are_empty(self):
|
||||
self._create_domain('domain.tld')
|
||||
self._create_account('admin1@domain.tld', role='DomainAdmins')
|
||||
domadmin = User.objects.get(username='admin1@domain.tld')
|
||||
for l in ['mailboxes', 'mailbox_aliases']:
|
||||
self.assertEqual(
|
||||
domadmin.limitspool.get_limit('%s_limit' % l).maxvalue, 0
|
||||
)
|
||||
|
||||
def test_domain_admins_limit_from_domain_tpl(self):
|
||||
self.user.limitspool.set_maxvalue('domains_limit', 3)
|
||||
self._create_domain('domain1.tld', withtpl=True)
|
||||
self._create_domain('domain2.tld', withtpl=True)
|
||||
self._check_limit('domain_admins', 2, 2)
|
||||
self._check_limit('domains', 2, 3)
|
||||
resp = self._create_domain('domain3.tld', status=200, withtpl=True)
|
||||
self._check_limit('domain_admins', 2, 2)
|
||||
self._check_limit('domains', 3, 3)
|
||||
|
||||
def test_reseller_deletes_domain(self):
|
||||
"""Check if all resources are restored after the deletion.
|
||||
"""
|
||||
self._create_domain('domain.tld', withtpl=True)
|
||||
dom = Domain.objects.get(name="domain.tld")
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.domain.deldomain",
|
||||
args=[dom.id]),
|
||||
{}
|
||||
)
|
||||
self._check_limit('domains', 0, 2)
|
||||
self._check_limit('domain_admins', 1, 2)
|
||||
self._check_limit('mailboxes', 0, 2)
|
||||
self._check_limit('mailbox_aliases', 0, 2)
|
||||
|
||||
def test_sadmin_removes_ownership(self):
|
||||
self._create_domain('domain.tld', withtpl=True)
|
||||
dom = Domain.objects.get(name="domain.tld")
|
||||
self.clt.logout()
|
||||
self.clt.login(username='admin', password='password')
|
||||
self.ajax_get(
|
||||
reverse(
|
||||
'modoboa.extensions.admin.views.identity.remove_permission'
|
||||
) + '?domid=%d&daid=%d' % (dom.id, self.user.id)
|
||||
, {}
|
||||
)
|
||||
self._check_limit('domains', 0, 2)
|
||||
self._check_limit('domain_admins', 0, 2)
|
||||
self._check_limit('mailboxes', 0, 2)
|
||||
self._check_limit('mailbox_aliases', 0, 2)
|
||||
|
||||
def test_allocate_from_pool(self):
|
||||
self._create_domain('domain.tld')
|
||||
self._create_account('admin1@domain.tld', role='DomainAdmins')
|
||||
user = User.objects.get(username='admin1@domain.tld')
|
||||
|
||||
# Give 1 mailbox and 2 aliases to the admin -> should work
|
||||
values = {
|
||||
'username': user.username, 'role': user.group, 'quota_act': True,
|
||||
'is_active': user.is_active, 'email': user.email,
|
||||
'mailboxes_limit': 1, 'mailbox_aliases_limit': 2
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[user.id]),
|
||||
values
|
||||
)
|
||||
self._check_limit('mailboxes', 1, 1)
|
||||
self._check_limit('mailbox_aliases', 0, 0)
|
||||
|
||||
# Delete the admin -> resources should go back to the
|
||||
# reseller's pool
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.delaccount",
|
||||
args=[user.id]),
|
||||
{}
|
||||
)
|
||||
self._check_limit('mailboxes', 0, 2)
|
||||
self._check_limit('mailbox_aliases', 0, 2)
|
||||
|
||||
def test_restore_resources(self):
|
||||
self._create_domain('domain.tld')
|
||||
dom = Domain.objects.get(name='domain.tld')
|
||||
self._create_account('admin1@domain.tld', role='DomainAdmins')
|
||||
user = User.objects.get(username='admin1@domain.tld')
|
||||
values = {
|
||||
'username': user.username, 'role': user.group, 'quota_act': True,
|
||||
'is_active': user.is_active, 'email': user.email,
|
||||
'mailboxes_limit': 1, 'mailbox_aliases_limit': 2
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[user.id]),
|
||||
values
|
||||
)
|
||||
dom.add_admin(user)
|
||||
self.clt.logout()
|
||||
self.clt.login(username='admin1@domain.tld', password='toto')
|
||||
self._create_account('user1@domain.tld')
|
||||
self._create_alias('alias1@domain.tld', 'user1@domain.tld')
|
||||
self._create_alias('alias2@domain.tld', 'user1@domain.tld')
|
||||
self.clt.logout()
|
||||
self.clt.login(username='reseller', password='toto')
|
||||
# Delete the admin -> resources should go back to the
|
||||
# reseller's pool
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.delaccount",
|
||||
args=[user.id]),
|
||||
{}
|
||||
)
|
||||
self._check_limit('mailboxes', 1, 2)
|
||||
self._check_limit('mailbox_aliases', 2, 2)
|
||||
|
||||
def test_change_role(self):
|
||||
self._create_domain('domain.tld')
|
||||
self._create_account('admin1@domain.tld', role='DomainAdmins')
|
||||
user = User.objects.get(username='admin1@domain.tld')
|
||||
|
||||
# Give 1 mailbox and 2 aliases to the admin -> should work
|
||||
values = {
|
||||
'username': user.username, 'role': user.group, 'quota_act': True,
|
||||
'is_active': user.is_active, 'email': user.email,
|
||||
'mailboxes_limit': 1, 'mailbox_aliases_limit': 2
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[user.id]),
|
||||
values
|
||||
)
|
||||
self._check_limit('mailboxes', 1, 1)
|
||||
self._check_limit('mailbox_aliases', 0, 0)
|
||||
|
||||
# Change admin role to SimpleUser -> resources should go back
|
||||
# to the reseller.
|
||||
values = {
|
||||
'username': user.username, 'role': 'SimpleUsers', 'quota_act': True,
|
||||
'is_active': user.is_active, 'email': user.email,
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[user.id]),
|
||||
values
|
||||
)
|
||||
self._check_limit('mailboxes', 1, 2)
|
||||
self._check_limit('mailbox_aliases', 0, 2)
|
||||
|
||||
|
||||
def test_allocate_too_much(self):
|
||||
self._create_domain('domain.tld')
|
||||
self._create_account('admin1@domain.tld', role='DomainAdmins')
|
||||
user = User.objects.get(username='admin1@domain.tld')
|
||||
|
||||
# Give 2 mailboxes and 3 aliases to the admin -> should fail.
|
||||
values = {
|
||||
'username': user.username, 'role': user.group, 'quota_act': True,
|
||||
'is_active': user.is_active, 'email': user.email,
|
||||
'mailboxes_limit': 2, 'mailbox_aliases_limit': 3
|
||||
}
|
||||
resp = self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[user.id]),
|
||||
values, 424
|
||||
)
|
||||
self.assertEqual(resp, 'Not enough resources')
|
||||
self._check_limit('mailboxes', 1, 2)
|
||||
self._check_limit('mailbox_aliases', 0, 2)
|
||||
|
||||
@@ -6,13 +6,20 @@ This module provides a way to integrate Modoboa auto-reply
|
||||
functionality into Postfix.
|
||||
|
||||
"""
|
||||
import sys
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.core.extensions import ModoExtension, exts_pool
|
||||
from .models import Transport, Alias
|
||||
from modoboa.extensions.postfix_autoreply.models import (
|
||||
Transport, Alias
|
||||
)
|
||||
|
||||
|
||||
class PostfixAutoreply(ModoExtension):
|
||||
"""
|
||||
Auto-reply (vacation) functionality using Postfix.
|
||||
|
||||
"""
|
||||
name = "postfix_autoreply"
|
||||
label = "Postfix autoreply"
|
||||
version = "1.0"
|
||||
@@ -25,7 +32,7 @@ class PostfixAutoreply(ModoExtension):
|
||||
try:
|
||||
Transport.objects.get(domain="autoreply.%s" % dom.name)
|
||||
except Transport.DoesNotExist:
|
||||
onCreateDomain(None, dom)
|
||||
onDomainCreated(None, dom)
|
||||
else:
|
||||
continue
|
||||
|
||||
@@ -33,19 +40,17 @@ class PostfixAutoreply(ModoExtension):
|
||||
try:
|
||||
Alias.objects.get(full_address=mb.full_address)
|
||||
except Alias.DoesNotExist:
|
||||
onCreateMailbox(None, mb)
|
||||
onMailboxCreated(None, mb)
|
||||
|
||||
def load(self):
|
||||
from modoboa.extensions.postfix_autoreply.app_settings import ParametersForm
|
||||
parameters.register(ParametersForm, ugettext_lazy("Automatic replies"))
|
||||
from modoboa.extensions.postfix_autoreply import general_callbacks
|
||||
if 'modoboa.extensions.postfix_autoreply.general_callbacks' in sys.modules:
|
||||
reload(general_callbacks)
|
||||
|
||||
def destroy(self):
|
||||
events.unregister("CreateDomain", onCreateDomain)
|
||||
events.unregister("DeleteDomain", onDeleteDomain)
|
||||
events.unregister("CreateMailbox", onCreateMailbox)
|
||||
events.unregister("DeleteMailbox", onDeleteMailbox)
|
||||
events.unregister("ModifyMailbox", onModifyMailbox)
|
||||
events.unregister("UserMenuDisplay", menu)
|
||||
events.unregister_extension()
|
||||
parameters.unregister()
|
||||
|
||||
exts_pool.register_extension(PostfixAutoreply)
|
||||
@@ -55,87 +60,3 @@ exts_pool.register_extension(PostfixAutoreply)
|
||||
def extra_routes():
|
||||
return [(r'^user/autoreply/$',
|
||||
'modoboa.extensions.postfix_autoreply.views.autoreply'), ]
|
||||
|
||||
|
||||
@events.observe("ExtraUprefsJS")
|
||||
def extra_js(user):
|
||||
return ["""function autoreply_cb() {
|
||||
$('#id_untildate').datepicker({format: 'yyyy-mm-dd', language: '%s'});
|
||||
}
|
||||
""" % parameters.get_user(user, "LANG", app="core")
|
||||
]
|
||||
|
||||
|
||||
@events.observe("UserMenuDisplay")
|
||||
def menu(target, user):
|
||||
if target != "uprefs_menu":
|
||||
return []
|
||||
if not user.mailbox_set.count():
|
||||
return []
|
||||
return [
|
||||
{"name": "autoreply",
|
||||
"class": "ajaxlink",
|
||||
"url": "autoreply/",
|
||||
"label": ugettext_lazy("Auto-reply message")}
|
||||
]
|
||||
|
||||
|
||||
@events.observe("CreateDomain")
|
||||
def onCreateDomain(user, domain):
|
||||
transport = Transport()
|
||||
transport.domain = "autoreply.%s" % domain.name
|
||||
transport.method = "autoreply:"
|
||||
transport.save()
|
||||
|
||||
|
||||
@events.observe("DomainModified")
|
||||
def onDomainModified(domain):
|
||||
if domain.oldname == domain.name:
|
||||
return
|
||||
Transport.objects.filter(domain="autoreply.%s" % domain.oldname) \
|
||||
.update(domain="autoreply.%s" % domain.name)
|
||||
for al in Alias.objects.filter(full_address__contains="@%s" % domain.oldname):
|
||||
new_address = al.full_address.replace("@%s" % domain.oldname, "@%s" % domain.name)
|
||||
al.full_address = new_address
|
||||
al.autoreply_address = "%s@autoreply.%s" % (new_address, domain.name)
|
||||
al.save()
|
||||
|
||||
|
||||
@events.observe("DeleteDomain")
|
||||
def onDeleteDomain(domain):
|
||||
Transport.objects.filter(domain="autoreply.%s" % domain.name).delete()
|
||||
|
||||
|
||||
@events.observe("CreateMailbox")
|
||||
def onCreateMailbox(user, mailbox):
|
||||
alias = Alias()
|
||||
alias.full_address = mailbox.full_address
|
||||
alias.autoreply_address = \
|
||||
"%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name)
|
||||
alias.save()
|
||||
|
||||
|
||||
@events.observe("DeleteMailbox")
|
||||
def onDeleteMailbox(mailboxes):
|
||||
from modoboa.extensions.admin.models import Mailbox
|
||||
|
||||
if isinstance(mailboxes, Mailbox):
|
||||
mailboxes = [mailboxes]
|
||||
for mailbox in mailboxes:
|
||||
try:
|
||||
alias = Alias.objects.get(full_address=mailbox.full_address)
|
||||
except Alias.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
alias.delete()
|
||||
|
||||
|
||||
@events.observe("ModifyMailbox")
|
||||
def onModifyMailbox(mailbox, oldmailbox):
|
||||
if oldmailbox.full_address == mailbox.full_address:
|
||||
return
|
||||
alias = Alias.objects.get(full_address=oldmailbox.full_address)
|
||||
alias.full_address = mailbox.full_address
|
||||
alias.autoreply_address = \
|
||||
"%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name)
|
||||
alias.save()
|
||||
|
||||
@@ -16,3 +16,39 @@ class ParametersForm(AdminParametersForm):
|
||||
help_text=ugettext_lazy("Timeout in seconds between two auto-replies to the same recipient")
|
||||
)
|
||||
|
||||
default_subject = forms.CharField(
|
||||
label=ugettext_lazy("Default subject"),
|
||||
initial=ugettext_lazy("I'm off"),
|
||||
help_text=ugettext_lazy(
|
||||
"Default subject used when an auto-reply message is created automatically"
|
||||
)
|
||||
)
|
||||
|
||||
default_content = forms.CharField(
|
||||
label=ugettext_lazy("Default content"),
|
||||
initial=ugettext_lazy(
|
||||
"""I'm currently off. I'll answer as soon as I come back.
|
||||
|
||||
Best regards,
|
||||
%(name)s
|
||||
"""),
|
||||
help_text=ugettext_lazy(
|
||||
"Default content user when an auto-reply message is created "
|
||||
"automatically. The '%(name)s' macro will be replaced by the "
|
||||
"user's full name."
|
||||
),
|
||||
widget=forms.widgets.Textarea
|
||||
)
|
||||
|
||||
def clean_default_content(self):
|
||||
"""Check if the provided value is valid.
|
||||
|
||||
Must be a valid format string which will be used with the %
|
||||
operator.
|
||||
"""
|
||||
tpl = self.cleaned_data["default_content"]
|
||||
try:
|
||||
test = tpl % {"name": "Antoine Nguyen"}
|
||||
except (KeyError, ValueError):
|
||||
raise forms.ValidationError(ugettext_lazy("Invalid syntax"))
|
||||
return tpl
|
||||
|
||||
@@ -2,14 +2,26 @@
|
||||
import datetime
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from django.utils import timezone
|
||||
from .models import ARmessage
|
||||
|
||||
|
||||
class ARmessageForm(forms.ModelForm):
|
||||
untildate = forms.DateField(
|
||||
fromdate = forms.DateTimeField(
|
||||
label=ugettext_lazy('From'),
|
||||
required=False,
|
||||
help_text=ugettext_lazy("Activate your auto reply from this date"),
|
||||
widget=forms.TextInput(
|
||||
attrs={'class': 'datefield', 'readonly': 'readonly'}
|
||||
)
|
||||
)
|
||||
untildate = forms.DateTimeField(
|
||||
label=ugettext_lazy('Until'),
|
||||
required=False,
|
||||
help_text=ugettext_lazy("Activate your auto reply until this date")
|
||||
help_text=ugettext_lazy("Activate your auto reply until this date"),
|
||||
widget=forms.TextInput(
|
||||
attrs={'class': 'datefield', 'readonly': 'readonly'}
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -18,10 +30,32 @@ class ARmessageForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ARmessageForm, self).__init__(*args, **kwargs)
|
||||
self.fields.keyOrder = ['subject', 'content', 'untildate', 'enabled']
|
||||
self.fields.keyOrder = ['subject', 'content', 'fromdate', 'untildate', 'enabled']
|
||||
if 'instance' in kwargs and kwargs['instance'] is not None:
|
||||
self.fields['fromdate'].initial = kwargs['instance'].fromdate
|
||||
self.fields['untildate'].initial = kwargs['instance'].untildate
|
||||
|
||||
def clean_untildate(self):
|
||||
def clean(self):
|
||||
"""Custom fields validaton.
|
||||
|
||||
We want to be sure that fromdate < untildate and that they are
|
||||
both in the future.
|
||||
"""
|
||||
super(ARmessageForm, self).clean()
|
||||
if self._errors:
|
||||
raise forms.ValidationError(self._errors)
|
||||
if self.cleaned_data["fromdate"] is not None:
|
||||
if self.cleaned_data["fromdate"] < timezone.now():
|
||||
self._errrors["fromdate"] = self.error_class([_("This date is over")])
|
||||
del self.cleaned_data['fromdate']
|
||||
else:
|
||||
self.cleaned_data['fromdate'] = timezone.now()
|
||||
if self.cleaned_data["untildate"] is not None:
|
||||
if self.cleaned_data["untildate"] < datetime.date.today():
|
||||
raise forms.ValidationError(_("This date is over"))
|
||||
return self.cleaned_data['untildate']
|
||||
if self.cleaned_data["untildate"] < timezone.now():
|
||||
self._errors["untildate"] = self.error_class([_("This date is over")])
|
||||
del self.cleaned_data['untildate']
|
||||
elif self.cleaned_data['untildate'] < self.cleaned_data['fromdate']:
|
||||
self._errors["untildate"] = \
|
||||
self.error_class([_("Must be greater than start date")])
|
||||
del self.cleaned_data['untildate']
|
||||
return self.cleaned_data
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.extensions.postfix_autoreply.models import Transport, Alias
|
||||
from .models import ARmessage
|
||||
|
||||
|
||||
@events.observe("ExtraUprefsJS")
|
||||
def extra_js(user):
|
||||
return ["""function autoreply_cb() {
|
||||
$('.datefield').datetimepicker({
|
||||
format: 'yyyy-mm-dd hh:ii:ss',
|
||||
language: '%(lang)s',
|
||||
autoclose: true,
|
||||
todayHighlight: true,
|
||||
todayBtn: 'linked'
|
||||
});
|
||||
}
|
||||
""" % {'lang': parameters.get_user(user, "LANG", app="core")}
|
||||
]
|
||||
|
||||
|
||||
@events.observe("UserMenuDisplay")
|
||||
def menu(target, user):
|
||||
if target != "uprefs_menu":
|
||||
return []
|
||||
if not user.mailbox_set.count():
|
||||
return []
|
||||
return [
|
||||
{"name": "autoreply",
|
||||
"class": "ajaxlink",
|
||||
"url": "autoreply/",
|
||||
"label": ugettext_lazy("Auto-reply message")}
|
||||
]
|
||||
|
||||
|
||||
@events.observe("DomainCreated")
|
||||
def onDomainCreated(user, domain):
|
||||
transport = Transport()
|
||||
transport.domain = "autoreply.%s" % domain.name
|
||||
transport.method = "autoreply:"
|
||||
transport.save()
|
||||
|
||||
|
||||
@events.observe("DomainModified")
|
||||
def onDomainModified(domain):
|
||||
if domain.oldname == domain.name:
|
||||
return
|
||||
Transport.objects.filter(domain="autoreply.%s" % domain.oldname) \
|
||||
.update(domain="autoreply.%s" % domain.name)
|
||||
for al in Alias.objects.filter(full_address__contains="@%s" % domain.oldname):
|
||||
new_address = al.full_address.replace("@%s" % domain.oldname, "@%s" % domain.name)
|
||||
al.full_address = new_address
|
||||
al.autoreply_address = "%s@autoreply.%s" % (new_address, domain.name)
|
||||
al.save()
|
||||
|
||||
|
||||
@events.observe("DomainDeleted")
|
||||
def onDomainDeleted(domain):
|
||||
Transport.objects.filter(domain="autoreply.%s" % domain.name).delete()
|
||||
|
||||
|
||||
@events.observe("MailboxCreated")
|
||||
def onMailboxCreated(user, mailbox):
|
||||
alias = Alias()
|
||||
alias.full_address = mailbox.full_address
|
||||
alias.autoreply_address = \
|
||||
"%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name)
|
||||
alias.save()
|
||||
|
||||
|
||||
@events.observe("MailboxDeleted")
|
||||
def onMailboxDeleted(mailboxes):
|
||||
from modoboa.extensions.admin.models import Mailbox
|
||||
|
||||
if isinstance(mailboxes, Mailbox):
|
||||
mailboxes = [mailboxes]
|
||||
for mailbox in mailboxes:
|
||||
try:
|
||||
alias = Alias.objects.get(full_address=mailbox.full_address)
|
||||
except Alias.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
alias.delete()
|
||||
|
||||
|
||||
@events.observe("MailboxModified")
|
||||
def onMailboxModified(mailbox):
|
||||
if mailbox.full_address == mailbox.old_full_address:
|
||||
return
|
||||
alias = Alias.objects.get(full_address=mailbox.old_full_address)
|
||||
alias.full_address = mailbox.full_address
|
||||
alias.autoreply_address = \
|
||||
"%s@autoreply.%s" % (mailbox.full_address, mailbox.domain.name)
|
||||
alias.save()
|
||||
|
||||
|
||||
@events.observe("ExtraFormFields")
|
||||
def extra_mailform_fields(form_name, mailbox=None):
|
||||
"""Define extra fields to include in mail forms.
|
||||
|
||||
For now, only the auto-reply state can be modified.
|
||||
|
||||
:param str form_name: form name (must be 'mailform')
|
||||
:param Mailbox mailbox: mailbox
|
||||
"""
|
||||
from modoboa.lib.formutils import YesNoField
|
||||
|
||||
if form_name != "mailform":
|
||||
return []
|
||||
status = False
|
||||
if mailbox is not None and mailbox.armessage_set.count():
|
||||
status = mailbox.armessage_set.all()[0].enabled
|
||||
return [
|
||||
('autoreply', YesNoField(
|
||||
label=ugettext_lazy("Enable auto-reply"),
|
||||
initial="yes" if status else "no",
|
||||
help_text=ugettext_lazy("Enable or disable Postfix auto-reply")
|
||||
))
|
||||
]
|
||||
|
||||
|
||||
@events.observe("SaveExtraFormFields")
|
||||
def save_extra_mailform_fields(form_name, mailbox, values):
|
||||
"""Set the auto-reply status for a mailbox.
|
||||
|
||||
If a corresponding auto-reply message exists, we update its
|
||||
status. Otherwise, we create a message using default values.
|
||||
|
||||
:param str form_name: form name (must be 'mailform')
|
||||
:param Mailbox mailbox: mailbox
|
||||
:param dict values: form values
|
||||
"""
|
||||
if form_name != 'mailform':
|
||||
return
|
||||
if mailbox.armessage_set.count():
|
||||
arm = mailbox.armessage_set.all()[0]
|
||||
else:
|
||||
arm = ARmessage(mbox=mailbox)
|
||||
arm.subject = parameters.get_admin("DEFAULT_SUBJECT")
|
||||
arm.content = parameters.get_admin("DEFAULT_CONTENT") \
|
||||
% {'name': mailbox.user.fullname}
|
||||
arm.fromdate = timezone.now()
|
||||
arm.enabled = True if values['autoreply'] == 'yes' else False
|
||||
arm.save()
|
||||
@@ -12,8 +12,10 @@ from modoboa.extensions.postfix_autoreply.models import ARmessage, ARhistoric
|
||||
|
||||
|
||||
def send_autoreply(sender, mailbox, armessage):
|
||||
if armessage.fromdate < timezone.now():
|
||||
return
|
||||
if armessage.untildate is not None \
|
||||
and armessage.untildate < datetime.date.today():
|
||||
and armessage.untildate < timezone.now():
|
||||
armessage.enabled = False
|
||||
armessage.save()
|
||||
return
|
||||
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding field 'ARmessage.fromdate'
|
||||
db.add_column(u'postfix_autoreply_armessage', 'fromdate',
|
||||
self.gf('django.db.models.fields.DateTimeField')(default=timezone.now),
|
||||
keep_default=False)
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting field 'ARmessage.fromdate'
|
||||
db.delete_column(u'postfix_autoreply_armessage', 'fromdate')
|
||||
|
||||
|
||||
models = {
|
||||
'admin.domain': {
|
||||
'Meta': {'ordering': "['name']", 'object_name': 'Domain'},
|
||||
'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
|
||||
'quota': ('django.db.models.fields.IntegerField', [], {})
|
||||
},
|
||||
'admin.mailbox': {
|
||||
'Meta': {'object_name': 'Mailbox'},
|
||||
'address': ('django.db.models.fields.CharField', [], {'max_length': '252'}),
|
||||
'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}),
|
||||
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'quota': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"})
|
||||
},
|
||||
'admin.objectdates': {
|
||||
'Meta': {'object_name': 'ObjectDates'},
|
||||
'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
u'auth.permission': {
|
||||
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
u'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
u'core.objectaccess': {
|
||||
'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"})
|
||||
},
|
||||
u'core.user': {
|
||||
'Meta': {'ordering': "['username']", 'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_local': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'})
|
||||
},
|
||||
u'postfix_autoreply.alias': {
|
||||
'Meta': {'object_name': 'Alias'},
|
||||
'autoreply_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'full_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
u'postfix_autoreply.arhistoric': {
|
||||
'Meta': {'unique_together': "(('armessage', 'sender'),)", 'object_name': 'ARhistoric'},
|
||||
'armessage': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['postfix_autoreply.ARmessage']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'last_sent': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
|
||||
'sender': ('django.db.models.fields.CharField', [], {'max_length': '254'})
|
||||
},
|
||||
u'postfix_autoreply.armessage': {
|
||||
'Meta': {'object_name': 'ARmessage'},
|
||||
'content': ('django.db.models.fields.TextField', [], {}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'fromdate': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'mbox': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Mailbox']"}),
|
||||
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'untildate': ('django.db.models.fields.DateTimeField', [], {})
|
||||
},
|
||||
u'postfix_autoreply.transport': {
|
||||
'Meta': {'object_name': 'Transport'},
|
||||
'domain': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'method': ('django.db.models.fields.CharField', [], {'max_length': '255'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['postfix_autoreply']
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Changing field 'ARmessage.untildate'
|
||||
db.alter_column(u'postfix_autoreply_armessage', 'untildate', self.gf('django.db.models.fields.DateTimeField')(null=True))
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# User chose to not deal with backwards NULL issues for 'ARmessage.untildate'
|
||||
raise RuntimeError("Cannot reverse this migration. 'ARmessage.untildate' and its values cannot be restored.")
|
||||
|
||||
models = {
|
||||
'admin.domain': {
|
||||
'Meta': {'ordering': "['name']", 'object_name': 'Domain'},
|
||||
'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
|
||||
'quota': ('django.db.models.fields.IntegerField', [], {})
|
||||
},
|
||||
'admin.mailbox': {
|
||||
'Meta': {'object_name': 'Mailbox'},
|
||||
'address': ('django.db.models.fields.CharField', [], {'max_length': '252'}),
|
||||
'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.ObjectDates']"}),
|
||||
'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Domain']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'quota': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"})
|
||||
},
|
||||
'admin.objectdates': {
|
||||
'Meta': {'object_name': 'ObjectDates'},
|
||||
'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
u'auth.permission': {
|
||||
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
u'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
u'core.objectaccess': {
|
||||
'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'},
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['core.User']"})
|
||||
},
|
||||
u'core.user': {
|
||||
'Meta': {'ordering': "['username']", 'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_local': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'})
|
||||
},
|
||||
u'postfix_autoreply.alias': {
|
||||
'Meta': {'object_name': 'Alias'},
|
||||
'autoreply_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'full_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
},
|
||||
u'postfix_autoreply.arhistoric': {
|
||||
'Meta': {'unique_together': "(('armessage', 'sender'),)", 'object_name': 'ARhistoric'},
|
||||
'armessage': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['postfix_autoreply.ARmessage']"}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'last_sent': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
|
||||
'sender': ('django.db.models.fields.CharField', [], {'max_length': '254'})
|
||||
},
|
||||
u'postfix_autoreply.armessage': {
|
||||
'Meta': {'object_name': 'ARmessage'},
|
||||
'content': ('django.db.models.fields.TextField', [], {}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'fromdate': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'mbox': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['admin.Mailbox']"}),
|
||||
'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'untildate': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'})
|
||||
},
|
||||
u'postfix_autoreply.transport': {
|
||||
'Meta': {'object_name': 'Transport'},
|
||||
'domain': ('django.db.models.fields.CharField', [], {'max_length': '300'}),
|
||||
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'method': ('django.db.models.fields.CharField', [], {'max_length': '255'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['postfix_autoreply']
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils import timezone
|
||||
from modoboa.extensions.admin.models import Mailbox
|
||||
|
||||
|
||||
@@ -27,7 +28,8 @@ class ARmessage(models.Model):
|
||||
_('enabled'),
|
||||
help_text=_("Activate/Deactivate your auto reply")
|
||||
)
|
||||
untildate = models.DateTimeField()
|
||||
fromdate = models.DateTimeField(default=timezone.now)
|
||||
untildate = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
|
||||
class ARhistoric(models.Model):
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from modoboa.core.models import User
|
||||
from modoboa.lib.tests import ExtTestCase
|
||||
from modoboa.extensions.admin import factories
|
||||
from modoboa.extensions.admin.models import (
|
||||
Domain
|
||||
)
|
||||
from modoboa.extensions.postfix_autoreply.models import (
|
||||
Transport, Alias, ARmessage
|
||||
)
|
||||
|
||||
|
||||
class EventsTestCase(ExtTestCase):
|
||||
fixtures = ['initial_users.json']
|
||||
|
||||
def setUp(self):
|
||||
super(EventsTestCase, self).setUp()
|
||||
self.activate_extensions('postfix_autoreply')
|
||||
factories.populate_database()
|
||||
|
||||
def test_domain_created_event(self):
|
||||
values = {
|
||||
"name": "domain.tld", "quota": 100, "create_dom_admin": "no",
|
||||
"stepid": 'step2'
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.domain.newdomain"),
|
||||
values
|
||||
)
|
||||
trans = Transport.objects.get(domain='autoreply.domain.tld')
|
||||
|
||||
def test_domain_deleted_event(self):
|
||||
dom = Domain.objects.get(name="test.com")
|
||||
trans = Transport.objects.get(domain='autoreply.test.com')
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.domain.deldomain",
|
||||
args=[dom.id]),
|
||||
{}
|
||||
)
|
||||
with self.assertRaises(Transport.DoesNotExist):
|
||||
Transport.objects.get(domain='autoreply.test.com')
|
||||
|
||||
def test_domain_modified_event(self):
|
||||
values = {
|
||||
"name": "test.fr", "quota": 100, "enabled": True
|
||||
}
|
||||
dom = Domain.objects.get(name="test.com")
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.domain.editdomain",
|
||||
args=[dom.id]),
|
||||
values
|
||||
)
|
||||
trans = Transport.objects.get(domain='autoreply.test.fr')
|
||||
self.assertEqual(
|
||||
Alias.objects.filter(full_address__contains='@test.fr').count(), 2
|
||||
)
|
||||
for al in Alias.objects.filter(full_address__contains='@test.fr'):
|
||||
self.assertIn('autoreply.test.fr', al.autoreply_address)
|
||||
|
||||
def test_mailbox_created_event(self):
|
||||
values = {
|
||||
'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', 'autoreply': 'no'
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.newaccount"),
|
||||
values
|
||||
)
|
||||
al = Alias.objects.get(full_address='tester@test.com')
|
||||
self.assertEqual(al.autoreply_address, 'tester@test.com@autoreply.test.com')
|
||||
arm = ARmessage.objects.get(mbox__address='tester')
|
||||
|
||||
def test_mailbox_deleted_event(self):
|
||||
account = User.objects.get(username="user@test.com")
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.delaccount",
|
||||
args=[account.id]),
|
||||
{}
|
||||
)
|
||||
with self.assertRaises(Alias.DoesNotExist):
|
||||
Alias.objects.get(full_address='user@test.com')
|
||||
with self.assertRaises(ARmessage.DoesNotExist):
|
||||
ARmessage.objects.get(mbox__address='user', mbox__domain__name='test.com')
|
||||
|
||||
|
||||
def test_modify_mailbox_event(self):
|
||||
values = {
|
||||
'username': "leon@test.com", 'first_name': 'Tester', 'last_name': 'Toto',
|
||||
'role': 'SimpleUsers', 'quota_act': True, 'is_active': True,
|
||||
'email': 'leon@test.com', 'autoreply': 'no'
|
||||
}
|
||||
account = User.objects.get(username="user@test.com")
|
||||
self.ajax_post(
|
||||
reverse("modoboa.extensions.admin.views.identity.editaccount",
|
||||
args=[account.id]),
|
||||
values
|
||||
)
|
||||
with self.assertRaises(Alias.DoesNotExist):
|
||||
Alias.objects.get(full_address='user@test.com')
|
||||
al = Alias.objects.get(full_address='leon@test.com')
|
||||
|
||||
|
||||
class FormTestCase(ExtTestCase):
|
||||
fixtures = ['initial_users.json']
|
||||
|
||||
def setUp(self):
|
||||
super(FormTestCase, self).setUp()
|
||||
self.activate_extensions('postfix_autoreply')
|
||||
factories.populate_database()
|
||||
|
||||
# def test_set_autoreply(self):
|
||||
# self.clt.get(
|
||||
# reverse('modoboa.core.views.user.index')
|
||||
# )
|
||||
# values = {
|
||||
# 'subject': 'test', 'content': "I'm off", "enabled": True
|
||||
# }
|
||||
# self.ajax_post(
|
||||
# reverse('modoboa.extensions.postfix_autoreply.views.autoreply'),
|
||||
# values
|
||||
# )
|
||||
# account = User.objects.get(username="user@test.com")
|
||||
# arm = ARmessage.objects.get(mbox=account.mailbox_set.all()[0])
|
||||
# self.assertEqual(arm.subject, 'test')
|
||||
# self.assertTrue(arm.enabled)
|
||||
# self.assertFalse(arm.untildate)
|
||||
# self.assertTrue(arm.fromdate)
|
||||
@@ -3,7 +3,7 @@ from datetime import date
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.translation import ugettext as _
|
||||
from modoboa.lib.webutils import (
|
||||
ajax_simple_response, _render_to_string
|
||||
render_to_json_response, _render_to_string
|
||||
)
|
||||
from modoboa.extensions.admin.lib import needs_mailbox
|
||||
from modoboa.extensions.admin.models import Mailbox
|
||||
@@ -26,27 +26,20 @@ def autoreply(request, tplname="postfix_autoreply/autoreply.html"):
|
||||
form = ARmessageForm(request.POST)
|
||||
if form.is_valid():
|
||||
arm = form.save(commit=False)
|
||||
arm.fromdate = form.cleaned_data["fromdate"]
|
||||
arm.untildate = form.cleaned_data["untildate"]
|
||||
arm.mbox = mb
|
||||
arm.save()
|
||||
return ajax_simple_response(dict(
|
||||
status="ok",
|
||||
respmsg=_("Auto reply message updated successfully.")
|
||||
))
|
||||
return render_to_json_response(
|
||||
_("Auto reply message updated successfully.")
|
||||
)
|
||||
|
||||
return ajax_simple_response({
|
||||
"status": "ko",
|
||||
"errors": form.errors,
|
||||
"onload_cb": "autoreply_cb"
|
||||
})
|
||||
return render_to_json_response(
|
||||
{"form_errors": form.errors}, status=400
|
||||
)
|
||||
|
||||
form = ARmessageForm(instance=arm)
|
||||
if arm is not None:
|
||||
form.fields['untildate'].initial = arm.untildate
|
||||
else:
|
||||
form.fields['untildate'].initial = date.today()
|
||||
return ajax_simple_response({
|
||||
"status": "ok",
|
||||
return render_to_json_response({
|
||||
"content": _render_to_string(request, tplname, {"form": form}),
|
||||
"onload_cb": "autoreply_cb"
|
||||
})
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# coding: utf-8
|
||||
import sys
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from modoboa.lib import events, parameters
|
||||
from modoboa.core.extensions import ModoExtension, exts_pool
|
||||
from .models import RelayDomain, RelayDomainAlias, Service
|
||||
|
||||
extension_events = [
|
||||
"RelayDomainCreated",
|
||||
"RelayDomainDeleted",
|
||||
"RelayDomainModified",
|
||||
"RelayDomainAliasCreated",
|
||||
"RelayDomainAliasDeleted",
|
||||
"ExtraRelayDomainForm",
|
||||
"FillRelayDomainInstances"
|
||||
]
|
||||
|
||||
|
||||
def init_limits_dependant_features():
|
||||
"""Populate limits tables.
|
||||
|
||||
Update the pool of every defined user (to add new limits) and
|
||||
update the *Resellers* group with new permissions.
|
||||
"""
|
||||
from modoboa.core.models import User
|
||||
from modoboa.extensions.limits import controls
|
||||
|
||||
for u in User.objects.all():
|
||||
controls.create_pool(u)
|
||||
grp = Group.objects.get(name='Resellers')
|
||||
for model in [RelayDomain, RelayDomainAlias, Service]:
|
||||
ct = ContentType.objects.get_for_model(model)
|
||||
name = model.__name__.lower()
|
||||
for action in ['add', 'change', 'delete']:
|
||||
grp.permissions.add(
|
||||
Permission.objects.get(
|
||||
content_type=ct, codename='%s_%s' % (action, name)
|
||||
)
|
||||
)
|
||||
grp.save()
|
||||
|
||||
|
||||
def init_amavis_dependant_features():
|
||||
"""Populate amavis database.
|
||||
|
||||
We create records for *users* and *policy* tables for each defined
|
||||
relay domain or relay domain alias.
|
||||
"""
|
||||
from modoboa.extensions.amavis.models import Users
|
||||
from modoboa.extensions.amavis.lib import (
|
||||
create_user_and_policy, create_user_and_use_policy
|
||||
)
|
||||
|
||||
for rdom in RelayDomain.objects.all():
|
||||
try:
|
||||
Users.objects.get(email="@%s" % rdom.name)
|
||||
except Users.DoesNotExist:
|
||||
create_user_and_policy(rdom.name)
|
||||
for rdomalias in rdom.relaydomainalias_set.all():
|
||||
try:
|
||||
Users.objects.get(email='@%s' % rdomalias.name)
|
||||
except Users.DoesNotExist:
|
||||
create_user_and_use_policy(rdomalias.name, rdom.name)
|
||||
|
||||
|
||||
class PostfixRelayDomains(ModoExtension):
|
||||
name = "postfix_relay_domains"
|
||||
label = "Postfix relay domains"
|
||||
version = "1.0"
|
||||
description = ugettext_lazy("Relay domains support for Postfix")
|
||||
|
||||
def init(self):
|
||||
"""Initialisation method.
|
||||
|
||||
Only run once, when the extension is enabled. Populates the
|
||||
service table with default entries.
|
||||
"""
|
||||
for service_name in ['relay', 'smtp']:
|
||||
Service.objects.get_or_create(name=service_name)
|
||||
if exts_pool.is_extension_enabled('limits'):
|
||||
init_limits_dependant_features()
|
||||
|
||||
def load(self):
|
||||
from .app_settings import AdminParametersForm
|
||||
|
||||
parameters.register(
|
||||
AdminParametersForm, ugettext_lazy("Relay domains")
|
||||
)
|
||||
events.declare(extension_events)
|
||||
from modoboa.extensions.postfix_relay_domains import general_callbacks
|
||||
if 'modoboa.extensions.postfix_relay_domains.general_callbacks' in sys.modules:
|
||||
reload(general_callbacks)
|
||||
if exts_pool.is_extension_enabled('limits'):
|
||||
import limits_callbacks
|
||||
if 'modoboa.extensions.postfix_relay_domains.limits_callbacks' in sys.modules:
|
||||
reload(limits_callbacks)
|
||||
if exts_pool.is_extension_enabled('amavis'):
|
||||
import amavis_callbacks
|
||||
if 'modoboa.extensions.postfix_relay_domains.amavis_callbacks' in sys.modules:
|
||||
reload(amavis_callbacks)
|
||||
|
||||
def destroy(self):
|
||||
events.unregister_extension()
|
||||
parameters.unregister()
|
||||
|
||||
exts_pool.register_extension(PostfixRelayDomains)
|
||||
@@ -0,0 +1,30 @@
|
||||
from modoboa.lib import events
|
||||
from modoboa.extensions.amavis.lib import (
|
||||
create_user_and_policy, update_user_and_policy, delete_user_and_policy,
|
||||
create_user_and_use_policy, delete_user
|
||||
)
|
||||
|
||||
|
||||
@events.observe("RelayDomainCreated")
|
||||
def on_relay_domain_created(user, rdomain):
|
||||
create_user_and_policy(rdomain.name)
|
||||
|
||||
|
||||
@events.observe("RelayDomainModified")
|
||||
def on_relay_domain_modified(rdomain):
|
||||
update_user_and_policy(rdomain.oldname, rdomain.name)
|
||||
|
||||
|
||||
@events.observe("RelayDomainDeleted")
|
||||
def on_relay_domain_deleted(rdomain):
|
||||
delete_user_and_policy(rdomain.name)
|
||||
|
||||
|
||||
@events.observe("RelayDomainAliasCreated")
|
||||
def on_relay_domain_alias_created(user, rdomainalias):
|
||||
create_user_and_use_policy(rdomainalias.name, rdomainalias.target.name)
|
||||
|
||||
|
||||
@events.observe("RelayDomainAliasDeleted")
|
||||
def on_relay_domain_alias_deleted(rdomainalias):
|
||||
delete_user(rdomainalias.name)
|
||||
@@ -0,0 +1,13 @@
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy
|
||||
from modoboa.lib import parameters
|
||||
|
||||
|
||||
class AdminParametersForm(parameters.AdminParametersForm):
|
||||
app = "postfix_relay_domains"
|
||||
|
||||
master_cf_path = forms.CharField(
|
||||
label=ugettext_lazy("Postfix's master.cf path"),
|
||||
initial="/etc/postfix/master.cf",
|
||||
help_text=ugettext_lazy('Path to the master.cf configuration file')
|
||||
)
|
||||
@@ -0,0 +1,23 @@
|
||||
import factory
|
||||
from modoboa.core.factories import PermissionFactory
|
||||
from . import models
|
||||
|
||||
|
||||
class ServiceFactory(factory.DjangoModelFactory):
|
||||
FACTORY_FOR = models.Service
|
||||
FACTORY_DJANGO_GET_OR_CREATE = ('name', )
|
||||
|
||||
name = 'dummy'
|
||||
|
||||
|
||||
class RelayDomainFactory(PermissionFactory):
|
||||
FACTORY_FOR = models.RelayDomain
|
||||
|
||||
target_host = 'external.host.tld'
|
||||
service = factory.SubFactory(ServiceFactory)
|
||||
|
||||
|
||||
class RelayDomainAliasFactory(PermissionFactory):
|
||||
FACTORY_FOR = models.RelayDomainAlias
|
||||
|
||||
enabled = True
|
||||
@@ -0,0 +1,157 @@
|
||||
from django import forms
|
||||
from django.http import QueryDict
|
||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||
from modoboa.lib import events
|
||||
from modoboa.lib.formutils import DynamicForm, DomainNameField, TabForms
|
||||
from modoboa.extensions.admin.models import Domain, DomainAlias
|
||||
from .models import RelayDomain, RelayDomainAlias
|
||||
|
||||
|
||||
class RelayDomainFormGeneral(forms.ModelForm, DynamicForm):
|
||||
aliases = DomainNameField(
|
||||
label=ugettext_lazy("Alias(es)"),
|
||||
required=False,
|
||||
help_text=ugettext_lazy(
|
||||
"Alias(es) of this relay domain. Indicate only one name per input"
|
||||
", press ENTER to add a new input."
|
||||
)
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = RelayDomain
|
||||
exclude = ['dates']
|
||||
widgets = {
|
||||
'service': forms.Select(attrs={'class': 'span2'})
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RelayDomainFormGeneral, self).__init__(*args, **kwargs)
|
||||
if args and isinstance(args[0], QueryDict):
|
||||
self._load_from_qdict(args[0], "aliases", DomainNameField)
|
||||
elif 'instance' in kwargs:
|
||||
rd = kwargs['instance']
|
||||
for pos, rdalias in enumerate(rd.relaydomainalias_set.all()):
|
||||
name = "aliases_%d" % (pos + 1)
|
||||
self._create_field(forms.CharField, name, rdalias.name, 3)
|
||||
|
||||
def clean(self):
|
||||
"""Custom fields validaton.
|
||||
|
||||
We want to prevent duplicate names between domains, relay
|
||||
domains, domain aliases and relay domain aliases.
|
||||
|
||||
The validation way is not very smart...
|
||||
"""
|
||||
super(RelayDomainFormGeneral, self).clean()
|
||||
if self._errors:
|
||||
raise forms.ValidationError(self._errors)
|
||||
cleaned_data = self.cleaned_data
|
||||
for dtype, label in [(Domain, _('domain')),
|
||||
(DomainAlias, _('domain alias')),
|
||||
(RelayDomainAlias, _('relay domain alias'))]:
|
||||
try:
|
||||
dtype.objects.get(name=cleaned_data['name'])
|
||||
except dtype.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
self._errors["name"] = self.error_class(
|
||||
[_("A %s with this name already exists" % label)]
|
||||
)
|
||||
del cleaned_data["name"]
|
||||
break
|
||||
|
||||
for k in cleaned_data.keys():
|
||||
if not k.startswith("aliases"):
|
||||
continue
|
||||
if not cleaned_data[k]:
|
||||
del cleaned_data[k]
|
||||
continue
|
||||
for dtype, name in [(RelayDomain, _('relay domain')),
|
||||
(DomainAlias, _('domain alias')),
|
||||
(Domain, _('domain'))]:
|
||||
try:
|
||||
dtype.objects.get(name=cleaned_data[k])
|
||||
except dtype.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
self._errors[k] = self.error_class(
|
||||
[_("A %s with this name already exists" % name)]
|
||||
)
|
||||
del cleaned_data[k]
|
||||
break
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def save(self, user, commit=True, rdomalias_post_create=False):
|
||||
"""Custom save method.
|
||||
|
||||
As relay domain aliases are defined using the same form as
|
||||
relay domains, we need to save them manually.
|
||||
|
||||
:param ``User`` user: connected user
|
||||
"""
|
||||
rd = super(RelayDomainFormGeneral, self).save(commit=False)
|
||||
if commit:
|
||||
rd.save()
|
||||
aliases = []
|
||||
for k, v in self.cleaned_data.iteritems():
|
||||
if not k.startswith("aliases"):
|
||||
continue
|
||||
if v in ["", None]:
|
||||
continue
|
||||
aliases.append(v)
|
||||
for rdalias in rd.relaydomainalias_set.all():
|
||||
if not rdalias.name in aliases:
|
||||
rdalias.delete()
|
||||
else:
|
||||
aliases.remove(rdalias.name)
|
||||
if aliases:
|
||||
events.raiseEvent(
|
||||
"CanCreate", user, "relay_domain_aliases", len(aliases)
|
||||
)
|
||||
for alias in aliases:
|
||||
try:
|
||||
rd.relaydomainalias_set.get(name=alias)
|
||||
except RelayDomainAlias.DoesNotExist:
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
al = RelayDomainAlias(
|
||||
name=alias, target=rd, enabled=rd.enabled
|
||||
)
|
||||
al.save(creator=user) \
|
||||
if rdomalias_post_create else al.save()
|
||||
return rd
|
||||
|
||||
|
||||
class RelayDomainForm(TabForms):
|
||||
"""Specific edition form for relay domains.
|
||||
|
||||
We use a *tabs* compatible form because extensions can add their
|
||||
own tab. (ex: amavis)
|
||||
"""
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
self.user = user
|
||||
self.forms = []
|
||||
if user.has_perm("postfix_relay_domains.change_relaydomain"):
|
||||
self.forms.append({
|
||||
'id': 'general', 'title': _("General"),
|
||||
'formtpl': 'postfix_relay_domains/relaydomain_form.html',
|
||||
'cls': RelayDomainFormGeneral, 'mandatory': True
|
||||
})
|
||||
|
||||
cbargs = [user]
|
||||
if "instances" in kwargs:
|
||||
cbargs += [kwargs["instances"]["general"]]
|
||||
self.forms += events.raiseQueryEvent("ExtraRelayDomainForm", *cbargs)
|
||||
super(RelayDomainForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def save(self, user):
|
||||
"""Custom save method
|
||||
|
||||
As forms interact with each other, it is easier to make custom
|
||||
code to save them.
|
||||
"""
|
||||
self.forms[0]['instance'].save(user, rdomalias_post_create=True)
|
||||
for f in self.forms[1:]:
|
||||
f["instance"].save(user)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user