Merge branch 'master' into feature/score-and-sorting-in-quarantine

Conflicts:
	modoboa/extensions/amavis/lib.py
	modoboa/extensions/amavis/views.py
	modoboa/extensions/webmail/lib.py
	modoboa/extensions/webmail/views.py
This commit is contained in:
Antoine Nguyen
2013-11-13 15:29:45 +01:00
130 changed files with 4717 additions and 1995 deletions
+19
View File
@@ -0,0 +1,19 @@
language: python
python:
- "2.6"
- "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
+3 -3
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
@@ -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
+403 -52
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
===============
@@ -127,6 +173,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 +205,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 +228,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 +261,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
===========
@@ -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,59 @@ 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>"}
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 +479,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 +517,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 +617,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 +661,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 +770,90 @@ 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)
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:
********************
+3
View File
@@ -53,6 +53,9 @@ 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;
+12 -4
View File
@@ -15,6 +15,7 @@ base_events = [
"RoleChanged",
"GetExtraRoles",
"PasswordChange",
"UserCanSetRole",
"UserMenuDisplay",
"AdminMenuDisplay",
@@ -32,12 +33,19 @@ base_events = [
"ExtraAdminContent",
"ExtraUprefsRoutes",
"ExtraUprefsJS"
"ExtraUprefsJS",
"GetExtraParameters"
]
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"))
@@ -52,4 +60,4 @@ def unset_default_topredirection(extension):
"""
topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION")
if topredirection == extension.name:
parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs")
parameters.save_admin("DEFAULT_TOP_REDIRECTION", "core")
+15 -4
View File
@@ -17,12 +17,16 @@ def enabled_applications():
:return: a list
"""
from modoboa.core.models import Extension
from modoboa.core.extensions import exts_pool
from modoboa.lib.dbutils import db_table_exists
result = [("core", "core")]
result = [("user", "user")]
if db_table_exists("core_extension"):
exts = Extension.objects.filter(enabled=True)
result += [(ext.name, ext.name) for ext in exts]
for ext in exts:
extclass = exts_pool.get_extension(ext.name)
if extclass.available_for_topredirection:
result.append((ext.name, ext.name))
return sorted(result, key=lambda e: e[0])
@@ -98,7 +102,7 @@ class GeneralParametersForm(parameters.AdminParametersForm):
label=ugettext_lazy("Bind password"),
initial='',
help_text=ugettext_lazy("The password to use when binding to the LDAP server (with 'Bind DN')"),
widget=forms.PasswordInput,
widget=forms.PasswordInput(render_value=True),
required=False
)
@@ -152,7 +156,7 @@ class GeneralParametersForm(parameters.AdminParametersForm):
default_top_redirection = forms.ChoiceField(
label=ugettext_lazy("Default top redirection"),
choices=[],
initial="core",
initial="user",
help_text=ugettext_lazy("The default redirection used when no application is specified")
)
@@ -176,6 +180,13 @@ class GeneralParametersForm(parameters.AdminParametersForm):
super(GeneralParametersForm, self).__init__(*args, **kwargs)
self.fields["default_top_redirection"].choices = enabled_applications()
def clean_secret_key(self):
if len(self.cleaned_data["secret_key"]) not in [16, 24, 32]:
raise forms.ValidationError(
_("Key must be either 16, 24, or 32 bytes long")
)
return self.cleaned_data["secret_key"]
def clean_ldap_user_dn_template(self):
tpl = self.cleaned_data["ldap_user_dn_template"]
try:
@@ -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
+34 -11
View File
@@ -15,6 +15,7 @@ class ModoExtension(object):
url = None
needs_media = False
always_active = False
available_for_topredirection = False
def infos(self):
return dict(
@@ -37,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)
@@ -49,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:
@@ -56,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)
@@ -71,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"])
+45 -22
View File
@@ -4,9 +4,11 @@ import re
import hashlib
import crypt
import base64
import logging
from random import Random
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
@@ -17,13 +19,14 @@ from django.contrib.auth.models import (
UserManager, Group, AbstractBaseUser, PermissionsMixin
)
from modoboa.lib import events, md5crypt, parameters
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.exceptions import (
PermDeniedException, InternalError, BadRequest
)
from modoboa.lib.sysutils import exec_cmd
from modoboa.core.extensions import exts_pool
from modoboa.core.exceptions import AdminError
try:
from modoboa.lib.ldaputils import *
from modoboa.lib.ldaputils import LDAPAuthBackend
ldap_available = True
except ImportError:
ldap_available = False
@@ -72,7 +75,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
@@ -105,6 +110,7 @@ class User(AbstractBaseUser, PermissionsMixin):
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.)
import string
salt = "".join(Random().sample(string.letters + string.digits, 6))
result = md5crypt(raw_value, salt)
prefix = "" # the result already has $1$ prepended to it
@@ -133,15 +139,12 @@ class User(AbstractBaseUser, PermissionsMixin):
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
)
@@ -197,7 +200,7 @@ class User(AbstractBaseUser, PermissionsMixin):
try:
return self.groups.all()[0].name
except IndexError:
return "SimpleUsers"
return "---"
@property
def enabled(self):
@@ -318,7 +321,7 @@ class User(AbstractBaseUser, PermissionsMixin):
: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 +330,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)
)
@@ -460,24 +463,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)
+1 -13
View File
@@ -45,7 +45,7 @@ def settings_menu(selection, user):
"label": _("Information")},
{"name": "logs",
"class": "ajaxlink",
"url": "logs/",
"url": "logs/&sort_order=-date_created",
"label": _("Logs")},
{"name": "parameters",
"class": "ajaxlink",
@@ -230,18 +230,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)
+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,
}),
+1 -1
View File
@@ -35,7 +35,7 @@ def dologin(request):
nextlocation = request.POST.get("next", None)
if nextlocation is None or nextlocation == "None":
if user.group != "SimpleUsers":
if user.group == "SimpleUsers":
nextlocation = reverse("modoboa.lib.webutils.topredirection")
else:
nextlocation = reverse("domains")
+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)
+22 -9
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"
]
@@ -139,7 +147,7 @@ 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):
@@ -193,6 +201,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
+92 -69
View File
@@ -2,14 +2,13 @@ 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
)
from modoboa.core.models import User
from modoboa.extensions.admin.exceptions import AdminError
from modoboa.extensions.admin.models import (
Domain, Mailbox, Alias
)
@@ -22,7 +21,9 @@ class AccountFormGeneral(forms.ModelForm):
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,20 +46,20 @@ 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 \
account = kwargs["instance"]
self.fields["role"].initial = account.group
if not account.is_local \
and parameters.get_admin("LDAP_AUTH_METHOD") == "directbind":
del self.fields["password1"]
del self.fields["password2"]
@@ -90,7 +91,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,14 +106,21 @@ 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):
@@ -143,6 +151,52 @@ 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:
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()
def save(self, user, account):
if self.cleaned_data["email"] == "":
return None
@@ -151,74 +205,43 @@ 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)
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)
@@ -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
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,13 @@ 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()
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" />
@@ -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(
+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
+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({})
+16 -6
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", {
+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)
if request.user.group != "SimpleUsers":
al.post_create(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
})
+18 -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
@@ -21,6 +20,7 @@ class Amavis(ModoExtension):
version = "1.0"
description = ugettext_lazy("Simple amavis management frontend")
url = "quarantine"
available_for_topredirection = True
def init(self):
"""Init function
@@ -29,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
+72 -2
View File
@@ -7,8 +7,10 @@ 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.lib.webutils import NavigationParameters
from modoboa.extensions.amavis.models import Users, Policy
def selfservice(ssfunc=None):
@@ -51,7 +53,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))
)
@@ -107,3 +109,71 @@ class QuarantineNavigationParameters(NavigationParameters):
if params:
url += "?%s" % ("&".join(params))
return url
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
@@ -146,9 +146,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) {
@@ -168,9 +167,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) {
@@ -204,11 +202,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);
});
}
+12 -12
View File
@@ -10,9 +10,9 @@ 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 ModoboaException, BadRequest
from modoboa.lib.webutils import (
getctx, ajax_response, ajax_simple_response
getctx, ajax_response, render_to_json_response
)
from modoboa.lib.templatetags.lib_tags import pagination_bar
from modoboa.extensions.admin.models import Mailbox, Domain
@@ -56,7 +56,7 @@ def _listing(request):
rcptfilter = 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
msgtype = navparams.get('msgtype', None)
@@ -180,14 +180,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)
@@ -221,13 +221,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")
@@ -238,9 +238,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)
@@ -313,4 +313,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()
@@ -0,0 +1,87 @@
from django.utils.translation import ugettext_lazy
from modoboa.lib import events, parameters
from modoboa.extensions.postfix_autoreply.models import Transport, Alias
@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("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 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()
+8 -12
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
@@ -29,24 +29,20 @@ def autoreply(request, tplname="postfix_autoreply/autoreply.html"):
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)
@@ -0,0 +1,143 @@
from django.core.urlresolvers import reverse
from django.template import Template, Context
from django.conf import settings
from django.utils.translation import ugettext_lazy
from django.db.models import Q
from modoboa.lib import events
from .models import RelayDomain, RelayDomainAlias
@events.observe('GetStaticContent')
def static_content(caller, user):
if caller != 'domains':
return []
t = Template("""<script src="{{ STATIC_URL }}postfix_relay_domains/js/relay_domains.js" type="text/javascript"></script>
<script type="text/javascript">
var rdomain;
$(document).ready(function() {
rdomain = new RelayDomains({});
});
</script>
""")
return [t.render(Context({'STATIC_URL': settings.STATIC_URL}))]
@events.observe('ExtraDomainFilters')
def extra_domain_filters():
return ['srvfilter']
@events.observe('ExtraDomainMenuEntries')
def extra_domain_menu_entries(user):
return [
{"name": "newrelaydomain",
"label": ugettext_lazy("Add relay domain"),
"img": "icon-plus",
"modal": True,
"modalcb": "rdomain.domainform_cb",
"url": reverse(
"modoboa.extensions.postfix_relay_domains.views.create"
)},
]
@events.observe('ExtraDomainEntries')
def extra_domain_entries(user, domfilter, searchquery, **extrafilters):
if domfilter is not None and domfilter and domfilter != 'relaydomain':
return []
relay_domains = RelayDomain.objects.get_for_admin(user)
if searchquery is not None:
q = Q(name__contains=searchquery)
q |= Q(relaydomainalias__name__contains=searchquery)
relay_domains = relay_domains.filter(q).distinct()
if 'srvfilter' in extrafilters and extrafilters['srvfilter']:
relay_domains = relay_domains.filter(
Q(service__name=extrafilters['srvfilter'])
)
return relay_domains
@events.observe('GetDomainModifyLink')
def rdomain_modify_link(domain):
if not isinstance(domain, RelayDomain):
return {}
return {
'url': reverse(
'modoboa.extensions.postfix_relay_domains.views.edit',
args=[domain.id]
),
'modalcb': 'rdomain.domainform_cb'
}
@events.observe('GetDomainActions')
def rdomain_actions(user, domain):
if not isinstance(domain, RelayDomain):
return []
if not user.has_perm("postfix_relay_domains.delete_relaydomain"):
return []
return [{
"name": "delrelaydomain",
"url": reverse("modoboa.extensions.postfix_relay_domains.views.delete",
args=[domain.id]),
"title": ugettext_lazy("Delete %s?" % domain.name),
"img": "icon-trash"
}]
@events.observe('CheckDomainName')
def check_domain_name():
return [
(RelayDomain, ugettext_lazy('relay domain')),
(RelayDomainAlias, ugettext_lazy('relay domain alias'))
]
@events.observe('GetExtraLimitTemplates')
def extra_limit_templates():
return [
('relay_domains_limit', ugettext_lazy('Relay domains'),
ugettext_lazy('Maximum number of relay domains this user can create'),
'Resellers'),
("relay_domain_aliases_limit", ugettext_lazy("Relay domain aliases"),
ugettext_lazy('Maximum number of relay domain aliases this user can create'),
'Resellers'),
]
@events.observe('ExtraDomainImportHelp')
def extra_domain_import_help():
return [ugettext_lazy("""
<li><em>relaydomain; name; target host; service; enabled; verify recipients</em></li>
<li><em>relaydomainalias; name; target; enabled</em></li>
""")]
@events.observe('ImportObject')
def get_import_func(objtype):
from .lib import import_relaydomain, import_relaydomainalias
if objtype == 'relaydomain':
return [import_relaydomain]
if objtype == 'relaydomainalias':
return [import_relaydomainalias]
return []
@events.observe('ExtEnabled')
def extension_enabled(extension):
"""ExtEnabled event listener.
Usefull when *limits* or *amavis* extensions are activated after
*postfix_relay_domains*.
:param extension: enabled extension
"""
from modoboa.extensions.postfix_relay_domains import (
init_limits_dependant_features, init_amavis_dependant_features
)
if extension.name == 'limits':
init_limits_dependant_features()
if extension.name == 'amavis':
init_amavis_dependant_features()
@@ -0,0 +1,14 @@
from django.db import transaction
from .models import RelayDomain, RelayDomainAlias
@transaction.commit_on_success
def import_relaydomain(user, row, formopts):
"""Specific code for relay domains import"""
RelayDomain().from_csv(user, row)
@transaction.commit_on_success
def import_relaydomainalias(user, row, formopts):
"""Specific code for relay domain aliases import"""
RelayDomainAlias().from_csv(user, row)
@@ -0,0 +1,58 @@
from django.utils.translation import ugettext_lazy
from modoboa.lib import events
from modoboa.lib.permissions import get_object_owner
from modoboa.extensions.limits.lib import inc_limit_usage, dec_limit_usage
from .models import RelayDomainAlias
@events.observe('GetExtraParameters')
def extra_parameters(app, level):
from django import forms
if app != 'limits' or level != 'A':
return {}
return {
'deflt_relay_domains_limit': forms.IntegerField(
label=ugettext_lazy("Relay domains"),
initial=0,
help_text=ugettext_lazy(
"Maximum number of allowed relay domains for a new administrator"
),
widget=forms.widgets.TextInput(attrs={"class": "span1"})
),
'deflt_relay_domain_aliases_limit': forms.IntegerField(
label=ugettext_lazy("Relay domain aliases"),
initial=0,
help_text=ugettext_lazy(
"Maximum number of allowed relay domain aliases for a new administrator"
),
widget=forms.widgets.TextInput(attrs={"class": "span1"})
)
}
@events.observe('RelayDomainCreated')
def inc_relaydomains_count(user, rdomain):
inc_limit_usage(user, 'relay_domains_limit')
@events.observe('RelayDomainDeleted')
def dec_relaydomains_count(rdomain):
owner = get_object_owner(rdomain)
dec_limit_usage(owner, 'relay_domains_limit')
for rdomalias in rdomain.relaydomainalias_set.all():
dec_rdomaliases_count(rdomalias)
@events.observe('RelayDomainAliasCreated')
def inc_rdomaliases_count(user, rdomalias):
inc_limit_usage(user, 'relay_domain_aliases_limit')
@events.observe('RelayDomainAliasDeleted')
def dec_rdomaliases_count(rdomainaliases):
if isinstance(rdomainaliases, RelayDomainAlias):
rdomainaliases = [rdomainaliases]
for rdomainalias in rdomainaliases:
owner = get_object_owner(rdomainalias)
dec_limit_usage(owner, 'relay_domain_aliases_limit')
@@ -0,0 +1,199 @@
import reversion
from django.db import models
from django.db.models.manager import Manager
from django.contrib.contenttypes import generic
from django.utils.translation import ugettext as _, ugettext_lazy
from modoboa.core.models import ObjectAccess
from modoboa.lib import parameters
from modoboa.lib.exceptions import BadRequest, NotFound
from modoboa.extensions.admin.models import AdminObject
class RelayDomainManager(Manager):
def get_for_admin(self, admin):
"""Return the relay domains belonging to this admin.
The result is a ``QuerySet`` object, so this function can be used
to fill ``ModelChoiceField`` objects.
"""
if admin.is_superuser:
return self.get_query_set()
return self.get_query_set().filter(owners__user=admin)
class ServiceManager(Manager):
def load_from_master_cf(self):
"""Load services from the master.cf file.
Parse the configuration file to update the service table. New
entries are saved and outdated ones (ie. present in the
database but not in the file) are removed.
"""
with open(parameters.get_admin('MASTER_CF_PATH')) as fp:
content = fp.read()
services = []
for line in content.split('\n'):
if not line or line.startswith('#'):
continue
parts = line.strip().split()
if len(parts) != 8:
continue
if parts[7] != 'smtp':
continue
srv, created = self.get_or_create(name=parts[0])
services.append(parts[0])
to_delete = []
for service in self.all():
if not service.name in services:
to_delete.append(service.name)
Service.objects.filter(name__in=to_delete).delete()
class Service(models.Model):
"""Postfix service.
"""
name = models.CharField(
ugettext_lazy('name'), max_length=100, unique=True,
help_text=ugettext_lazy('The service name')
)
objects = ServiceManager()
def __str__(self):
return self.name
class RelayDomain(AdminObject):
"""Relay domain.
A relay domain differs from a usual domaine because its final
destination is not reached yet. It must be accepted by the MTA but
it will then be transfered to another one.
"""
name = models.CharField(
ugettext_lazy('name'), max_length=100, unique=True,
help_text=ugettext_lazy('The domain name')
)
target_host = models.CharField(
ugettext_lazy('target host'), max_length=255,
help_text=ugettext_lazy('Remote destination of this domain')
)
service = models.ForeignKey(Service, default='relay')
enabled = models.BooleanField(
ugettext_lazy('enabled'),
help_text=ugettext_lazy('Check to activate this domain')
)
verify_recipients = models.BooleanField(
ugettext_lazy('verify recipients'),
help_text=ugettext_lazy('Check for valid recipients')
)
owners = generic.GenericRelation(ObjectAccess)
objects = RelayDomainManager()
class Meta:
ordering = ['name']
@property
def tags(self):
return [
{"name": "relaydomain", "label": _("Relay Domain"), "type": "dom"},
{"name": self.service.name, "label": "%s:" % self.service.name,
"type": "srv", "color": "info"}
]
@property
def aliases(self):
return self.relaydomainalias_set
def __str__(self):
return self.name
def to_csv(self, csvwriter):
"""Export this relay domain to CSV.
:param csvwriter:
"""
csvwriter.writerow(
["relaydomain", self.name, self.target_host,
self.service.name, self.enabled, self.verify_recipients]
)
for rdalias in self.relaydomainalias_set.all():
rdalias.to_csv(csvwriter)
def from_csv(self, user, row):
"""Import a relay domain from CSV.
:param user: user importing the relay domain
:param str row: relay domain definition
"""
if len(row) != 6:
raise BadRequest(_("Invalid line"))
self.name = row[1].strip()
self.target_host = row[2].strip()
self.service, created = Service.objects.get_or_create(name=row[3].strip())
self.enabled = (row[4].strip() == 'True')
self.verify_recipients = (row[5].strip() == 'True')
self.save(creator=user)
def post_create(self, creator):
"""Post creation actions.
:param ``User`` creator: user whos created this relay domain
"""
super(RelayDomain, self).post_create(creator)
for rdomalias in self.relaydomainalias_set.all():
rdomalias.post_create(creator)
reversion.register(RelayDomain)
class RelayDomainAlias(AdminObject):
"""Relay domain alias.
"""
name = models.CharField(
ugettext_lazy("name"), max_length=100, unique=True,
help_text=ugettext_lazy("The alias name")
)
target = models.ForeignKey(
RelayDomain, verbose_name=ugettext_lazy('target'),
help_text=ugettext_lazy("The relay domain this alias points to")
)
enabled = models.BooleanField(
ugettext_lazy('enabled'),
help_text=ugettext_lazy("Check to activate this alias")
)
def __str__(self):
return self.name
def to_csv(self, csvwriter):
"""Export this relay domain alias to CSV.
:param csvwriter:
"""
csvwriter.writerow(
['relaydomainalias', self.name, self.target.name, self.enabled]
)
def from_csv(self, user, row):
"""Import a relay domain alias from CSV.
:param user: user importing the relay domain alias
:param str row: relay domain alias definition
"""
if len(row) != 4:
raise BadRequest(_("Invalid line"))
self.name = row[1].strip()
try:
self.target = RelayDomain.objects.get(name=row[2].strip())
except RelayDomain.DoesNotExist:
raise NotFound(_("Relay domain %s does not exist" % row[2].strip()))
self.enabled = (row[3].strip() == 'True')
self.save(creator=user)
reversion.register(RelayDomainAlias)
@@ -0,0 +1,88 @@
/**
* @module relay_domains
* @desc Utility class to manage relay domains.
*/
var RelayDomains = function(options) {
this.initialize(options);
};
RelayDomains.prototype = {
constructor: RelayDomains,
defaults: {
},
initialize: function(options) {
this.options = $.extend({}, this.defaults, options);
admin.register_tag_handler("srv", this.srv_tag_handler);
$(document).bind('domain_listing_refresh', this.listing_refresh_cb);
},
listing_refresh_cb: function() {
$("a[name=delrelaydomain]").confirm({
question: function() { return this.$element.attr('title'); },
method: "DELETE",
success_cb: $.proxy(admin.reload_listing, admin)
});
},
launch_scan: function(e) {
var $link = get_target(e);
e.preventDefault();
$.ajax({
type: "POST",
url: $link.attr("href"),
global: false
}).done(function(data) {
var $widget = $('#id_service');
$widget.empty();
$.each(data, function(label, id) {
$widget.append($('<option />', {
value: id,
html: label
}));
});
$('.modal-body').prepend(
build_success_alert(gettext('Services updated'))
);
}).fail(function(jqxhr) {
var result = $.parseJSON(jqxhr.responseText);
$('.modal-body').prepend(
build_error_alert(result[0])
);
});
},
domainform_cb: function() {
$('#btnscan').click(this.launch_scan);
$('input:text:visible:first').focus();
$("#id_aliases").dynamic_input();
$(".submit").on('click', $.proxy(function(e) {
simple_ajax_form_post(e, {
formid: "rdomform",
reload_on_success: false,
success_cb: $.proxy(admin.reload_listing, admin)
});
}, this));
$(document).trigger('domform_init');
},
srv_tag_handler: function(tag, $link) {
if (this.navobj.getparam(tag + "filter") == undefined && $link.hasClass(tag)) {
var text = $link.attr("name");
this.navobj
.setparam("domfilter", "relaydomain")
.setparam(tag + "filter", text)
.update();
if ($("a[name=dom]").length == 0) {
$("#searchform").parent().after(this.make_tag("relaydomain", "dom"));
}
$("#searchform").parent().after(this.make_tag(text, tag));
return true;
}
return false;
}
};
@@ -0,0 +1,3 @@
{% extends "common/generic_modal_form.html" %}{% block formbody %}
{% include "postfix_relay_domains/relaydomain_form.html" %}
{% endblock %}
@@ -0,0 +1,7 @@
{% load lib_tags %}
{% render_field form.name %}
{% render_field form.target_host %}
{% include "postfix_relay_domains/service_field.html" with field=form.service %}
{% render_fields_group form "aliases" %}
{% render_field form.enabled %}
{% render_field form.verify_recipients %}
@@ -0,0 +1,2 @@
{% extends "common/generic_field.html" %}{% load i18n %}
{% block extra_controls %}<a id="btnscan" class="btn btn-success" style="margin-bottom: 8px" href="{% url 'modoboa.extensions.postfix_relay_domains.views.scan_for_services' %}">{% trans "Scan" %}</a>{% endblock %}
@@ -0,0 +1,212 @@
from django.core.urlresolvers import reverse
from modoboa.core.factories import UserFactory
from modoboa.lib import parameters
from modoboa.lib.tests import ExtTestCase
from modoboa.extensions.admin import factories
from modoboa.extensions.limits.tests import ResourceTestCase
from .models import RelayDomain, RelayDomainAlias, Service
from .factories import RelayDomainFactory, RelayDomainAliasFactory
class Operations(object):
def _create_relay_domain(self, name, status=200):
srv, created = Service.objects.get_or_create(name='dummy')
values = {
'name': name, 'target_host': 'external.host.tld',
'service': srv.id, 'enabled': True
}
return self.ajax_post(
reverse("modoboa.extensions.postfix_relay_domains.views.create"),
values, status
)
def _relay_domain_alias_operation(self, optype, rdomain, name, status=200):
rdom = RelayDomain.objects.get(name=rdomain)
values = {
'name': rdom.name, 'target_host': rdom.target_host,
'service': rdom.service.id
}
aliases = [alias.name for alias in rdom.relaydomainalias_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
return self.ajax_post(
reverse("modoboa.extensions.postfix_relay_domains.views.edit",
args=[rdom.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 RelayDomainsTestCase(ExtTestCase, Operations):
fixtures = ['initial_users.json']
def setUp(self):
super(RelayDomainsTestCase, self).setUp()
self.activate_extensions('postfix_relay_domains')
factories.populate_database()
self.rdom = RelayDomainFactory(name='relaydomain.tld')
RelayDomainAliasFactory(name='relaydomainalias.tld', target=self.rdom)
def test_create_relaydomain(self):
"""Test the creation of a relay domain.
We also check that unique constraints are respected: domain,
relay domain alias.
FIXME: add a check for domain alias.
"""
self._create_relay_domain('relaydomain1.tld')
rdom = RelayDomain.objects.get(name='relaydomain1.tld')
self.assertEqual(rdom.target_host, 'external.host.tld')
self.assertEqual(rdom.service.name, 'dummy')
self.assertEqual(rdom.enabled, True)
self.assertEqual(rdom.verify_recipients, False)
resp = self._create_relay_domain('test.com', 400)
self.assertEqual(resp['form_errors']['name'][0],
'A domain with this name already exists')
resp = self._create_relay_domain('relaydomainalias.tld', 400)
self.assertEqual(
resp['form_errors']['name'][0],
'A relay domain alias with this name already exists'
)
def test_create_relaydomainalias(self):
"""Test the creation of a relay domain alias.
We also check that unique constraints are respected: domain,
relay domain.
FIXME: add a check for domain alias.
"""
self._relay_domain_alias_operation(
'add', self.rdom.name, 'relaydomainalias1.tld'
)
resp = self._relay_domain_alias_operation(
'add', self.rdom.name, 'test.com', 400
)
self.assertEqual(
resp['form_errors']['aliases_2'][0],
'A domain with this name already exists'
)
resp = self._relay_domain_alias_operation(
'add', self.rdom.name, self.rdom.name, 400
)
self.assertEqual(
resp['form_errors']['aliases_2'][0],
'A relay domain with this name already exists'
)
def test_edit_relaydomain(self):
"""Test the modification of a relay domain.
Rename 'relaydomain.tld' domain to 'relaydomain.org'
"""
values = {
'name': 'relaydomain.org', 'target_host': self.rdom.target_host,
'service': self.rdom.service.id
}
self.ajax_post(
reverse('modoboa.extensions.postfix_relay_domains.views.edit',
args=[self.rdom.id]),
values
)
RelayDomain.objects.get(name='relaydomain.org')
def test_edit_relaydomainalias(self):
"""Test the modification of a relay domain alias.
Rename 'relaydomainalias.tld' domain to 'relaydomainalias.net'
"""
values = {
'name': 'relaydomain.org', 'target_host': self.rdom.target_host,
'service': self.rdom.service.id, 'aliases': 'relaydomainalias.net'
}
self.ajax_post(
reverse('modoboa.extensions.postfix_relay_domains.views.edit',
args=[self.rdom.id]),
values
)
RelayDomainAlias.objects.get(name='relaydomainalias.net')
with self.assertRaises(RelayDomainAlias.DoesNotExist):
RelayDomainAlias.objects.get(name='relaydomainalias.tld')
def test_delete_relaydomain(self):
"""Test the removal of a relay domain
"""
self.ajax_post(
reverse("modoboa.extensions.postfix_relay_domains.views.delete",
args=[self.rdom.id]),
{}
)
with self.assertRaises(RelayDomain.DoesNotExist):
RelayDomain.objects.get(name='relaydomain.tld')
def test_delete_relaydomainalias(self):
"""Test the remove of a relay domain alias
"""
self._relay_domain_alias_operation(
'del', self.rdom.name, 'relaydomainalias.tld'
)
with self.assertRaises(RelayDomain.DoesNotExist):
RelayDomain.objects.get(name='relaydomainalias.tld')
class LimitsTestCase(ExtTestCase, Operations):
fixtures = ['initial_users.json']
def setUp(self):
super(LimitsTestCase, self).setUp()
from modoboa.extensions.limits.models import LimitTemplates
self.activate_extensions('limits', 'postfix_relay_domains')
for tpl in LimitTemplates().templates:
parameters.save_admin('DEFLT_%s' % tpl[0].upper(), 2, app='limits')
self.user = UserFactory.create(
username='reseller', groups=('Resellers',)
)
self.clt.logout()
self.clt.login(username='reseller', password='toto')
def test_relay_domains_limit(self):
self._create_relay_domain('relaydomain1.tld')
self._check_limit('relay_domains', 1, 2)
self._create_relay_domain('relaydomain2.tld')
self._check_limit('relay_domains', 2, 2)
resp = self._create_relay_domain('relaydomain3.tld', 403)
self._check_limit('relay_domains', 2, 2)
self.ajax_post(
reverse('modoboa.extensions.postfix_relay_domains.views.delete',
args=[RelayDomain.objects.get(name='relaydomain2.tld').id]),
{}
)
self._check_limit('relay_domains', 1, 2)
def test_relay_domain_aliases_limit(self):
self._create_relay_domain('relaydomain1.tld')
self._relay_domain_alias_operation(
'add', 'relaydomain1.tld', 'relay_domain_alias1.tld'
)
self._check_limit('relay_domain_aliases', 1, 2)
self._relay_domain_alias_operation(
'add', 'relaydomain1.tld', 'relay_domain_alias2.tld'
)
self._check_limit('relay_domain_aliases', 2, 2)
self._relay_domain_alias_operation(
'add', 'relaydomain1.tld', 'relay_domain_alias3.tld', 403
)
self._check_limit('relay_domain_aliases', 2, 2)
self._relay_domain_alias_operation(
'delete', 'relaydomain1.tld', 'relay_domain_alias2.tld'
)
self._check_limit('relay_domain_aliases', 1, 2)
@@ -0,0 +1,9 @@
from django.conf.urls import patterns
urlpatterns = patterns(
'modoboa.extensions.postfix_relay_domains.views',
(r'^relaydomains/new/', 'create'),
(r'^relaydomains/(?P<rdom_id>\d+)/edit/$', 'edit'),
(r'^relaydomains/(?P<rdom_id>\d+)/delete/$', 'delete'),
(r'^relaydomains/scan_services/$', 'scan_for_services'),
)
@@ -0,0 +1,91 @@
from django.shortcuts import render
from django.db import transaction
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import (
login_required, permission_required
)
from modoboa.lib import events
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.webutils import render_to_json_response
from .models import RelayDomain, Service
from .forms import RelayDomainForm, RelayDomainFormGeneral
@login_required
@permission_required("postfix_relay_domains.add_relaydomain")
@transaction.commit_on_success
def create(request, tplname="postfix_relay_domains/new_relaydomain_form.html"):
events.raiseEvent("CanCreate", request.user, "relay_domains")
if request.method == 'POST':
form = RelayDomainFormGeneral(request.POST)
if form.is_valid():
rdom = form.save(request.user)
rdom.post_create(request.user)
return render_to_json_response(_("Relay domain created"))
return render_to_json_response(
{'form_errors': form.errors}, status=400
)
ctx = {"title": _("New relay domain"),
"action_label": _("Create"),
"action_classes": "submit",
"action": reverse(create),
"formid": "rdomform",
"form": RelayDomainFormGeneral()}
return render(request, tplname, ctx)
@login_required
@permission_required("postfix_relay_domains.change_relaydomain")
def edit(request, rdom_id, tplname='common/tabforms.html'):
rdom = RelayDomain.objects.get(pk=rdom_id)
if not request.user.can_access(rdom):
raise PermDeniedException
instances = {'general': rdom}
events.raiseEvent("FillRelayDomainInstances", request.user, rdom, instances)
if request.method == 'POST':
rdom.oldname = rdom.name
form = RelayDomainForm(request.user, request.POST, instances=instances)
if form.is_valid():
form.save(request.user)
events.raiseEvent('RelayDomainModified', rdom)
return render_to_json_response(_('Relay domain modified'))
return render_to_json_response(
{'form_errors': form.errors}, status=400
)
ctx = {
'action': reverse(edit, args=[rdom.id]),
'formid': 'rdomform',
'title': rdom.name,
'action_label': _("Update"),
'action_classes': "submit",
'tabs': RelayDomainForm(request.user, instances=instances)
}
return render(request, tplname, ctx)
@login_required
@permission_required("postfix_relay_domains.delete_relaydomain")
def delete(request, rdom_id):
rdom = RelayDomain.objects.get(pk=rdom_id)
if not request.user.can_access(rdom):
raise PermDeniedException
rdom.delete()
return render_to_json_response(_('Relay domain deleted'))
@login_required
@permission_required("postfix_relay_domains.add_service")
@require_http_methods(["POST"])
def scan_for_services(request):
try:
Service.objects.load_from_master_cf()
except IOError as e:
return render_to_json_response([str(e)], status=500)
return render_to_json_response(
dict((srv.name, srv.id) for srv in Service.objects.all())
)
+7 -32
View File
@@ -1,6 +1,6 @@
# coding: utf-8
from django.utils.translation import ugettext as _, ugettext_lazy
from django.core.urlresolvers import reverse
import sys
from django.utils.translation import ugettext_lazy
from modoboa.lib import events, parameters
from modoboa.core.extensions import ModoExtension, exts_pool
@@ -11,43 +11,18 @@ class SieveFilters(ModoExtension):
version = "1.0"
description = ugettext_lazy("Plugin to easily create server-side filters")
url = "sfilters"
available_for_topredirection = True
def load(self):
from .app_settings import ParametersForm, UserSettings
parameters.register(ParametersForm, ugettext_lazy("Sieve filters"))
parameters.register(UserSettings, ugettext_lazy("Message filters"))
from modoboa.extensions.sievefilters import general_callbacks
if 'modoboa.extensions.sievefilters.general_callbacks' in sys.modules:
reload(general_callbacks)
def destroy(self):
events.unregister("UserMenuDisplay", menu)
events.unregister_extension()
parameters.unregister()
exts_pool.register_extension(SieveFilters)
@events.observe("UserMenuDisplay")
def menu(target, user):
if target != "options_menu":
return []
if not user.mailbox_set.count():
return []
return [
{"name": "sievefilters",
"label": _("Message filters"),
"url": reverse("modoboa.extensions.sievefilters.views.index"),
"img": "icon-check"}
]
@events.observe("UserLogout")
def userlogout(request):
from .lib import SieveClient
if not request.user.mailbox_set.count():
return
try:
sc = SieveClient(user=request.user.username,
password=request.session["password"])
except Exception:
pass
else:
sc.logout()
@@ -0,0 +1,32 @@
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from modoboa.lib import events
@events.observe("UserMenuDisplay")
def menu(target, user):
if target != "options_menu":
return []
if not user.mailbox_set.count():
return []
return [
{"name": "sievefilters",
"label": _("Message filters"),
"url": reverse("modoboa.extensions.sievefilters.views.index"),
"img": "icon-check"}
]
@events.observe("UserLogout")
def userlogout(request):
from .lib import SieveClient
if not request.user.mailbox_set.count():
return
try:
sc = SieveClient(user=request.user.username,
password=request.session["password"])
except Exception:
pass
else:
sc.logout()
+3 -2
View File
@@ -5,11 +5,12 @@ from sievelib.managesieve import Client, Error
from sievelib.parser import Parser
from sievelib.factory import FiltersSet
from modoboa.lib import parameters
from modoboa.lib.exceptions import ModoboaException
from modoboa.lib.connections import ConnectionsManager, ConnectionError
class SieveClientError(Exception):
pass
class SieveClientError(ModoboaException):
http_code = 424
class SieveClient(object):
@@ -18,15 +18,10 @@ function removefilter(evt) {
$.ajax({
url: $this.attr("href"),
dataType: 'json',
success: function(response) {
if (response.status == "ok") {
history.update(true);
$("body").notify('success', response.respmsg, 2000);
} else {
$("body").notify('error', response.respmsg);
}
}
dataType: 'json'
}).done(function(response) {
history.update(true);
$("body").notify('success', response, 2000);
});
}
@@ -77,20 +72,15 @@ function loadfs(response) {
function send_command(cmd, extracb) {
$.ajax({
url: cmd,
dataType: 'json',
success: function(response) {
if (response.status == "ko") {
$("body").notify('error', response.respmsg);
} else {
if (extracb != undefined) {
extracb(response);
}
if (!response.norefresh) {
history.update(true);
}
$("body").notify('success', response.respmsg, 2000);
}
dataType: 'json'
}).done(function(response) {
if (extracb != undefined) {
extracb(response);
}
if (!response.norefresh) {
history.update(true);
}
$("body").notify('success', response.respmsg, 2000);
});
}
@@ -123,14 +113,9 @@ function savefs(evt) {
url: $editor.attr("action"),
type: $editor.attr("method"),
dataType: 'json',
data: $editor.serialize(),
success: function(response) {
if (response.status == "ko") {
$("body").notify('error', response.respmsg);
} else {
$("body").notify('success', response.respmsg, 2000);
}
}
data: $editor.serialize()
}).done(function(response) {
$("body").notify('success', response.respmsg, 2000);
});
}
@@ -181,16 +166,11 @@ function toggle_filter_state(event) {
event.preventDefault();
$.ajax({
url: $this.attr("href"),
dataType: 'json',
success: function(response) {
if (response.status == "ok") {
$this.html(response.label);
$this.attr("class", "");
$this.addClass(response.color);
} else {
$("body").notify('error', response.respmsg);
}
}
dataType: 'json'
}).done(function(response) {
$this.html(response.label);
$this.attr("class", "");
$this.addClass(response.color);
});
}
@@ -200,11 +180,10 @@ function move_filter(event) {
event.preventDefault();
$.ajax({
url: $this.attr('href'),
dataType: 'json',
success: function(response) {
$("#set_content").html(response.content);
init_filters_list();
}
dataType: 'json'
}).done(function(response) {
$("#set_content").html(response.content);
init_filters_list();
});
}
@@ -234,11 +213,10 @@ function filtersetform_cb() {
$("#newfiltersset").find("input").keypress(function(e) {
if (e.which == 13) e.preventDefault();
});
$(".submit").one('click', function(e) {
$(".submit").on('click', function(e) {
simple_ajax_form_post(e, {
formid: "newfiltersset",
reload_on_success: false,
error_cb: filtersetform_cb,
success_cb: filterset_created
});
});
@@ -257,8 +235,9 @@ function fetch_templates(url, container) {
$.ajax({
url: url,
async: false,
dataType: 'json',
success: function(data) { result = data; }
dataType: 'json'
}).done(function(data) {
result = data;
});
return result;
}
@@ -14,6 +14,25 @@ var history = new History({
var cond_templates = null;
var action_templates = null;
function display_filter_errors(data) {
$("ul.errors").remove();
$.each(data.form_errors, function(id, value) {
if (id.indexOf('cond') && id.indexOf('action')) {
return;
}
var fullid = "id_" + (data.prefix ? data.prefix + "-" : "") + id;
var $widget = $("#" + fullid);
var ulid = fullid + "-error";
var $ul = $("<ul />", {
"class": "errors",
"id": ulid
}).append($("<li />", {
html: value[0]
}));
$widget.after($ul);
});
}
function filterform_cb() {
cond_templates = fetch_templates("{% url 'modoboa.extensions.sievefilters.views.get_templates' 'condition' %}", cond_templates);
action_templates = fetch_templates("{% url 'modoboa.extensions.sievefilters.views.get_templates' 'action' %}", action_templates);
@@ -34,13 +53,14 @@ function filterform_cb() {
}
});
$(".submit").one('click', function(e) {
$(".submit").on('click', function(e) {
var counters = "conds=" + $dc.fields_cnt + "&actions=" + $da.fields_cnt;
simple_ajax_form_post(e, {
error_cb: filterform_cb,
formid: 'filterform',
reload_mode: 'history',
extradata: counters
extradata: counters,
error_cb: display_filter_errors
});
});
}
+52 -69
View File
@@ -9,7 +9,7 @@ from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from modoboa.lib import parameters
from modoboa.lib.webutils import _render_error, \
ajax_response, ajax_simple_response
ajax_response, render_to_json_response
from modoboa.lib.connections import ConnectionError
from modoboa.extensions.admin.lib import needs_mailbox
from .lib import SieveClient, SieveClientError
@@ -54,8 +54,12 @@ def index(request, tplname="sievefilters/index.html"):
@needs_mailbox()
def get_templates(request, ftype):
if ftype == "condition":
return ajax_simple_response(FilterForm([], [], request).cond_templates)
return ajax_simple_response(FilterForm([], [], request).action_templates)
return render_to_json_response(
FilterForm([], [], request).cond_templates
)
return render_to_json_response(
FilterForm([], [], request).action_templates
)
@login_required
@@ -87,8 +91,8 @@ def getfs(request, name):
menu = '<ul id="fsetmenu" class="nav nav-list"><li class="nav-header">%s</li>%s</ul>' % \
(_("Actions"), fset_menu(editormode, name))
resp = dict(status="ok", menu=menu, content=htmlcontent)
return ajax_simple_response(resp)
resp = dict(menu=menu, content=htmlcontent)
return render_to_json_response(resp)
def build_filter_ctx(ctx, form):
@@ -119,13 +123,12 @@ def submitfilter(request, setname, okmsg, tplname, tplctx, update=False, sc=None
fset.updatefilter(
oldname, fltname, conditions, actions, match_type
)
try:
sc.pushscript(fset.name, str(fset))
except SieveClientError as e:
return ajax_response(request, "ko", respmsg=str(e))
return ajax_response(request, respmsg=okmsg, ajaxnav=True)
tplctx = build_filter_ctx(tplctx, form)
return ajax_response(request, status="ko", template=tplname, **tplctx)
sc.pushscript(fset.name, str(fset))
return render_to_json_response(okmsg)
#tplctx = build_filter_ctx(tplctx, form)
#return ajax_response(request, status="ko", template=tplname, **tplctx)
return render_to_json_response({'form_errors': form.errors}, status=400)
@login_required
@@ -186,8 +189,8 @@ def removefilter(request, setname, fname):
fset = sc.getscript(setname, format="fset")
if fset.removefilter(fname.encode("utf-8")):
sc.pushscript(fset.name, str(fset))
return ajax_response(request, respmsg=_("Filter removed"))
return ajax_response(request, "ko", respmsg=_("Failed to remove filter"))
return render_to_json_response(_("Filter removed"))
return render_to_json_response(_("Failed to remove filter"), status=500)
@login_required
@@ -208,37 +211,28 @@ def savefs(request, name):
@login_required
@needs_mailbox()
def new_filters_set(request, tplname="common/generic_modal_form.html"):
if request.method == "POST":
form = FiltersSetForm(request.POST)
if form.is_valid():
sc = SieveClient(user=request.user.username,
password=request.session["password"])
sc.pushscript(form.cleaned_data["name"], "# Empty script",
form.cleaned_data["active"])
return render_to_json_response({
"url": form.cleaned_data["name"],
"active": form.cleaned_data["active"],
"respmsg": _("Filters set created")
})
return render_to_json_response({'form_errors': form.errors}, status=400)
ctx = {"title": _("Create a new filters set"),
"formid": "newfiltersset",
"action_label": _("Create"),
"action_classes": "submit",
"action": reverse(new_filters_set),
"withmenu": False,
"withunseen": False}
if request.method == "POST":
form = FiltersSetForm(request.POST)
error = None
if form.is_valid():
sc = SieveClient(user=request.user.username,
password=request.session["password"])
try:
sc.pushscript(form.cleaned_data["name"], "# Empty script",
form.cleaned_data["active"])
except SieveClientError, e:
error = str(e)
else:
return ajax_simple_response({
"status": "ok",
"url": form.cleaned_data["name"],
"active": form.cleaned_data["active"],
"respmsg": _("Filters set created")
})
ctx["form"] = form
ctx["error"] = error
return ajax_response(request, status="ko", template=tplname, **ctx)
ctx["form"] = FiltersSetForm()
"withunseen": False,
"form": FiltersSetForm()}
return render(request, tplname, ctx)
@@ -247,13 +241,10 @@ def new_filters_set(request, tplname="common/generic_modal_form.html"):
def remove_filters_set(request, name):
sc = SieveClient(user=request.user.username,
password=request.session["password"])
try:
sc.deletescript(name)
except SieveClientError, e:
return ajax_simple_response(dict(status="ko", respmsg=str(e)))
sc.deletescript(name)
acs, scripts = sc.listscripts()
return ajax_simple_response(dict(
status="ok", respmsg=_("Filters set deleted"), newfs=acs
return render_to_json_response(dict(
respmsg=_("Filters set deleted"), newfs=acs
))
@@ -293,24 +284,19 @@ def toggle_filter_state(request, setname, fname):
password=request.session["password"])
if type(fname) is unicode:
fname = fname.encode("utf-8")
try:
fset = sc.getscript(setname, format="fset")
if fset.is_filter_disabled(fname):
ret = fset.enablefilter(fname)
newstate = _("yes")
color = "green"
else:
ret = fset.disablefilter(fname)
newstate = _("no")
color = "red"
if not ret:
pass
sc.pushscript(setname, str(fset))
except SieveClientError, e:
return ajax_response(request, "ko", respmsg=str(e))
return ajax_simple_response({
"status": "ok",
fset = sc.getscript(setname, format="fset")
if fset.is_filter_disabled(fname):
ret = fset.enablefilter(fname)
newstate = _("yes")
color = "green"
else:
ret = fset.disablefilter(fname)
newstate = _("no")
color = "red"
if not ret:
pass
sc.pushscript(setname, str(fset))
return render_to_json_response({
"label": newstate,
"color": color
})
@@ -319,12 +305,9 @@ def toggle_filter_state(request, setname, fname):
def move_filter(request, setname, fname, direction):
sc = SieveClient(user=request.user.username,
password=request.session["password"])
try:
fset = sc.getscript(setname, format="fset")
fset.movefilter(fname.encode("utf-8"), direction)
sc.pushscript(setname, str(fset))
except (SieveClientError), e:
return ajax_response(request, "ko", respmsg=str(e))
fset = sc.getscript(setname, format="fset")
fset.movefilter(fname.encode("utf-8"), direction)
sc.pushscript(setname, str(fset))
return ajax_response(
request, template="sievefilters/guieditor.html", fs=fset
)
+10 -23
View File
@@ -7,8 +7,8 @@ This module provides support to retrieve statistics from postfix log :
sent, received, bounced, rejected
"""
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _, ugettext_lazy
import sys
from django.utils.translation import ugettext_lazy
from modoboa.lib import events, parameters
from modoboa.core.extensions import ModoExtension, exts_pool
@@ -17,7 +17,9 @@ class Stats(ModoExtension):
name = "stats"
label = "Statistics"
version = "1.0"
description = ugettext_lazy("Graphical statistics about emails traffic using RRDtool")
description = ugettext_lazy(
"Graphical statistics about emails traffic using RRDtool"
)
needs_media = True
def load(self):
@@ -26,27 +28,12 @@ class Stats(ModoExtension):
parameters.register(
ParametersForm, ugettext_lazy("Graphical statistics")
)
from modoboa.extensions.stats import general_callbacks
if 'modoboa.extensions.stats.general_callbacks' in sys.modules:
reload(general_callbacks)
def destroy(self):
events.unregister("AdminMenuDisplay", menu)
events.unregister_extension()
parameters.unregister()
exts_pool.register_extension(Stats)
@events.observe("UserMenuDisplay")
def menu(target, user):
if target != "top_menu" or user.group == "SimpleUsers":
return []
return [
{"name": "stats",
"label": _("Statistics"),
"url": reverse('fullindex')}
]
@events.observe("GetGraphSets")
def get_default_graph_sets():
from modoboa.extensions.stats.graph_templates import MailTraffic
gset = MailTraffic()
return {gset.html_id: gset}
@@ -0,0 +1,22 @@
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext as _
from modoboa.lib import events
@events.observe("UserMenuDisplay")
def menu(target, user):
if target != "top_menu" or user.group == "SimpleUsers":
return []
return [
{"name": "stats",
"label": _("Statistics"),
"url": reverse('fullindex')}
]
@events.observe("GetGraphSets")
def get_default_graph_sets():
from modoboa.extensions.stats.graph_templates import MailTraffic
gset = MailTraffic()
return {gset.html_id: gset}
@@ -79,10 +79,6 @@ Stats.prototype = {
},
graphs_cb: function(data) {
if (data.status == "ko") {
$("body").notify("error", data.respmsg);
return;
}
if (data.content) {
$(".tab-pane.active").html(data.content);
}
+10 -11
View File
@@ -5,9 +5,9 @@ from django.contrib.auth.decorators import (
login_required, user_passes_test, permission_required
)
from modoboa.lib import events
from modoboa.lib.exceptions import ModoboaException, PermDeniedException
from modoboa.lib.exceptions import BadRequest, PermDeniedException, NotFound
from modoboa.lib.webutils import (
_render_to_string, ajax_simple_response
_render_to_string, render_to_json_response
)
from modoboa.extensions.admin.models import (
Domain
@@ -24,7 +24,7 @@ def index(request):
deflocation = "graphs/?gset=mailtraffic"
if not request.user.is_superuser:
if not Domain.objects.get_for_admin(request.user).count():
raise ModoboaException(_("No statistics available"))
raise NotFound(_("No statistics available"))
period = request.GET.get("period", "day")
graph_sets = events.raiseDictEvent('GetGraphSets')
@@ -43,14 +43,14 @@ def graphs(request):
gset = request.GET.get("gset", None)
gsets = events.raiseDictEvent("GetGraphSets")
if not gset in gsets:
raise ModoboaException(_("Unknown graphic set"))
raise NotFound(_("Unknown graphic set"))
searchq = request.GET.get("searchquery", None)
period = request.GET.get("period", "day")
tplvars = dict(graphs=[], period=period)
if searchq in [None, "global"]:
if not request.user.is_superuser:
if not Domain.objects.get_for_admin(request.user).count():
return ajax_simple_response({"status": "ok"})
return render_to_json_response({})
tplvars.update(
domain=Domain.objects.get_for_admin(request.user)[0].name
)
@@ -59,14 +59,14 @@ def graphs(request):
else:
domain = Domain.objects.filter(name__contains=searchq)
if domain.count() != 1:
return ajax_simple_response({"status": "ok"})
return render_to_json_response({})
if not request.user.can_access(domain[0]):
raise PermDeniedException
tplvars.update(domain=domain[0].name)
if period == "custom":
if not "start" in request.GET or not "end" in request.GET:
raise ModoboaException(_("Bad custom period"))
raise BadRequest(_("Bad custom period"))
start = request.GET["start"]
end = request.GET["end"]
G = Grapher()
@@ -83,7 +83,6 @@ def graphs(request):
else:
tplvars['graphs'] = gsets[gset].get_graph_names()
return ajax_simple_response(dict(
status="ok",
content=_render_to_string(request, "stats/graphs.html", tplvars)
))
return render_to_json_response({
'content': _render_to_string(request, "stats/graphs.html", tplvars)
})
+6 -35
View File
@@ -1,4 +1,5 @@
# coding: utf-8
import sys
from django.utils.translation import ugettext as _, ugettext_lazy
from django.core.urlresolvers import reverse
from modoboa.lib import events, parameters
@@ -11,49 +12,19 @@ class Webmail(ModoExtension):
version = "1.0"
description = ugettext_lazy("Simple IMAP webmail")
needs_media = True
available_for_topredirection = True
def load(self):
from .app_settings import ParametersForm, UserSettings
parameters.register(ParametersForm, "Webmail")
parameters.register(UserSettings, "Webmail")
from modoboa.extensions.webmail import general_callbacks
if 'modoboa.extensions.webmail.general_callbacks' in sys.modules:
reload(general_callbacks)
def destroy(self):
events.unregister("UserMenuDisplay", menu)
events.unregister_extension()
parameters.unregister()
exts_pool.register_extension(Webmail)
@events.observe("UserMenuDisplay")
def menu(target, user):
if target != "top_menu":
return []
if not user.mailbox_set.count():
return []
return [
{"name": "webmail",
"label": _("Webmail"),
"url": reverse("modoboa.extensions.webmail.views.index")},
]
@events.observe("UserLogout")
def userlogout(request):
from .lib import IMAPconnector
from .exceptions import ImapError
if not request.user.mailbox_set.count():
return
try:
m = IMAPconnector(user=request.user.username,
password=request.session["password"])
except Exception:
# TODO silent exception are bad : we should at least log it
return
# The following statement may fail under Python 2.6...
try:
m.logout()
except ImapError:
pass
+14 -4
View File
@@ -6,14 +6,14 @@
"""
import re
from django.utils.translation import ugettext as _
from modoboa.lib.exceptions import ModoboaException
from modoboa.lib.exceptions import ModoboaException, InternalError
class WebmailError(ModoboaException):
errorexpr = re.compile("\[([^\]]+)\]\s*([^\.]+)")
class WebmailInternalError(InternalError):
errorexpr = re.compile(r'\[([^\]]+)\]\s*([^\.]+)')
def __init__(self, reason, ajax=False):
match = WebmailError.errorexpr.match(reason)
match = WebmailInternalError.errorexpr.match(reason)
if not match:
self.reason = reason
else:
@@ -24,6 +24,16 @@ class WebmailError(ModoboaException):
return self.reason
class UnknownAction(ModoboaException):
"""
Use this exception when the webmail encounter an unknown action.
"""
http_code = 404
def __init__(self):
super(UnknownAction, self).__init__(_("Unknown action"))
class ImapError(ModoboaException):
def __init__(self, reason):
self.reason = reason

Some files were not shown because too many files have changed in this diff Show More