Merge branch 'master' into feature/resizable_mailbox_list

This commit is contained in:
Antoine Nguyen
2013-11-26 15:42:20 +01:00
207 changed files with 6450 additions and 3522 deletions
+18
View File
@@ -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
View File
@@ -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
View File
@@ -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``.
+14
View File
@@ -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:
********************
+2
View File
@@ -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`:
+7 -7
View File
@@ -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;
+13 -3
View File
@@ -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"))
+54 -12
View File
@@ -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", {
+8
View File
@@ -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',
-14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+26
View File
@@ -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
+72
View File
@@ -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)
+125
View File
@@ -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)
+4 -44
View File
@@ -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)
+6 -6
View File
@@ -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
View File
@@ -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,
}),
+9 -18
View File
@@ -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)
})
+5 -4
View File
@@ -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>"""]
+7 -5
View File
@@ -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)
+44 -15
View File
@@ -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]
-19
View File
@@ -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
+2
View File
@@ -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}}]
+112 -73
View File
@@ -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
+25 -9
View File
@@ -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)
+67 -36
View File
@@ -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)
+7 -3
View File
@@ -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.")
)
+39 -6
View File
@@ -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'}),
+3 -1
View File
@@ -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'
]
+15 -25
View File
@@ -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)
+32 -5
View File
@@ -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()
+42 -33
View File
@@ -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)
+11 -27
View File
@@ -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)
+8 -16
View File
@@ -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 -40
View File
@@ -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,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(
+3 -1
View File
@@ -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'
]
+22 -25
View File
@@ -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
)
+10 -10
View File
@@ -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
)
+8 -8
View File
@@ -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
+7 -7
View File
@@ -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"
)
+37 -8
View File
@@ -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}')
+26
View File
@@ -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))
+34 -43
View File
@@ -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)
+85 -86
View File
@@ -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)
+3 -5
View File
@@ -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"])
+51 -72
View File
@@ -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({})
+17 -7
View File
@@ -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>
+19 -31
View File
@@ -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
})
+17 -126
View File
@@ -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
+12 -11
View File
@@ -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
+71 -2
View File
@@ -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);
});
}
+19 -18
View File
@@ -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})
+15 -88
View File
@@ -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')
+81 -53
View File
@@ -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')
+27 -44
View File
@@ -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>
"""]
+25 -1
View File
@@ -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
+49 -15
View File
@@ -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 ""
+378 -13
View File
@@ -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
+41 -7
View File
@@ -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
@@ -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']
@@ -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)
+9 -16
View File
@@ -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