From 937e09c7235f64f041f595b17687193c02782493 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 14 Aug 2013 15:43:16 +0200 Subject: [PATCH 01/97] wip: big refactoring Moving files. --- modoboa/admin/models/__init__.py | 0 modoboa/core/__init__.py | 8 + modoboa/core/app_settings.py | 276 ++++++++++ .../{ => commands}/templates/settings.py.tpl | 0 .../core/{ => commands}/templates/urls.py.tpl | 0 modoboa/core/exceptions.py | 14 + modoboa/core/extensions.py | 92 ++++ modoboa/core/fixtures/initial_users.json | 1 + modoboa/core/forms.py | 71 +++ modoboa/core/loggers.py | 11 + modoboa/core/models.py | 510 ++++++++++++++++++ modoboa/core/tables.py | 12 + modoboa/core/templates/core/extensions.html | 6 + modoboa/core/templates/core/information.html | 17 + modoboa/core/templates/core/logs.html | 20 + modoboa/core/templates/core/parameters.html | 8 + .../core/templates/core/settings_header.html | 23 + modoboa/core/templates/core/user_index.html | 21 + .../core/templates/core/user_preferences.html | 9 + modoboa/core/templates/core/user_profile.html | 10 + modoboa/core/templatetags/__init__.py | 0 modoboa/core/templatetags/core_tags.py | 151 ++++++ modoboa/core/urls.py | 14 + modoboa/core/views/__init__.py | 0 modoboa/core/views/admin.py | 128 +++++ modoboa/core/views/auth.py | 71 +++ modoboa/core/views/user.py | 73 +++ modoboa/extensions/__init__.py | 91 ---- modoboa/lib/authbackends.py | 10 +- modoboa/lib/cryptutils.py | 41 ++ modoboa/lib/listing.py | 30 ++ modoboa/lib/middleware.py | 14 +- modoboa/lib/models.py | 7 - modoboa/lib/parameters.py | 4 +- modoboa/lib/templatetags/libextras.py | 2 +- modoboa/lib/webutils.py | 10 +- modoboa/templates/nlayout.html | 6 +- modoboa/templates/registration/login.html | 2 +- modoboa/urls.py | 22 +- 39 files changed, 1664 insertions(+), 121 deletions(-) create mode 100644 modoboa/admin/models/__init__.py create mode 100644 modoboa/core/app_settings.py rename modoboa/core/{ => commands}/templates/settings.py.tpl (100%) rename modoboa/core/{ => commands}/templates/urls.py.tpl (100%) create mode 100644 modoboa/core/exceptions.py create mode 100644 modoboa/core/extensions.py create mode 100644 modoboa/core/fixtures/initial_users.json create mode 100644 modoboa/core/forms.py create mode 100644 modoboa/core/loggers.py create mode 100644 modoboa/core/models.py create mode 100644 modoboa/core/tables.py create mode 100644 modoboa/core/templates/core/extensions.html create mode 100644 modoboa/core/templates/core/information.html create mode 100644 modoboa/core/templates/core/logs.html create mode 100644 modoboa/core/templates/core/parameters.html create mode 100644 modoboa/core/templates/core/settings_header.html create mode 100644 modoboa/core/templates/core/user_index.html create mode 100644 modoboa/core/templates/core/user_preferences.html create mode 100644 modoboa/core/templates/core/user_profile.html create mode 100644 modoboa/core/templatetags/__init__.py create mode 100644 modoboa/core/templatetags/core_tags.py create mode 100644 modoboa/core/urls.py create mode 100644 modoboa/core/views/__init__.py create mode 100644 modoboa/core/views/admin.py create mode 100644 modoboa/core/views/auth.py create mode 100644 modoboa/core/views/user.py create mode 100644 modoboa/lib/cryptutils.py create mode 100644 modoboa/lib/listing.py diff --git a/modoboa/admin/models/__init__.py b/modoboa/admin/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index e69de29bb..b85142032 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -0,0 +1,8 @@ +from django.utils.translation import ugettext_lazy +from modoboa.lib import parameters + + +def load_settings(): + from app_settings import GeneralParametersForm, UserSettings + parameters.register(GeneralParametersForm, ugettext_lazy("General")) + parameters.register(UserSettings, ugettext_lazy("General")) diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py new file mode 100644 index 000000000..fb49d4b1a --- /dev/null +++ b/modoboa/core/app_settings.py @@ -0,0 +1,276 @@ +# coding: utf-8 +from django import forms +from django.conf import settings +from django.utils.translation import ugettext as _, ugettext_lazy +from modoboa.lib.formutils import YesNoField, SeparatorField, InlineRadioSelect +from modoboa.lib.sysutils import exec_cmd +from modoboa.lib import parameters +from modoboa.auth.lib import random_key + + +def enabled_applications(): + """Return the list of currently enabled extensions + + We check if the table exists before trying to fetch activated + extensions because the admin module is always imported by Django, + even before the database exists (example: the first ``syncdb``). + + :return: a list + """ + from modoboa.core.models import Extension + from modoboa.lib.dbutils import db_table_exists + + result = [("core", "core")] + if db_table_exists("core_extension"): + exts = Extension.objects.filter(enabled=True) + result += [(ext.name, ext.name) for ext in exts] + return sorted(result, key=lambda e: e[0]) + + +class GeneralParametersForm(parameters.AdminParametersForm): + app = "core" + + sep1 = SeparatorField(label=ugettext_lazy("Authentication")) + + authentication_type = forms.ChoiceField( + label=ugettext_lazy("Authentication type"), + choices=[('local', ugettext_lazy("Local")), + ('ldap', "LDAP")], + initial="local", + help_text=ugettext_lazy("The backend used for authentication"), + widget=InlineRadioSelect + ) + + password_scheme = forms.ChoiceField( + label=ugettext_lazy("Default password scheme"), + choices=[("crypt", "crypt"), + ("md5", "md5"), + ("md5crypt", "md5crypt"), + ("sha256", "sha256"), + ("plain", "plain")], + initial="md5crypt", + help_text=ugettext_lazy("Scheme used to crypt mailbox passwords") + ) + + secret_key = forms.CharField( + label=ugettext_lazy("Secret key"), + initial=random_key(), + help_text=ugettext_lazy("Key used to encrypt/decrypt passwords") + ) + + # LDAP specific settings + ldap_sep = SeparatorField(label=ugettext_lazy("LDAP settings")) + + ldap_server_address = forms.CharField( + label=ugettext_lazy("Server address"), + initial="localhost", + help_text=ugettext_lazy("The IP address of the DNS name of the LDAP server") + ) + + ldap_server_port = forms.IntegerField( + label=ugettext_lazy("Server port"), + initial=389, + help_text=ugettext_lazy("The TCP port number used by the LDAP server") + ) + + ldap_secured = YesNoField( + label=ugettext_lazy("Use a secured connection"), + initial="no", + help_text=ugettext_lazy("Use an SSL/TLS connection to access the LDAP server") + ) + + ldap_auth_method = forms.ChoiceField( + label=ugettext_lazy("Authentication method"), + choices=[('searchbind', ugettext_lazy("Search and bind")), + ('directbind', ugettext_lazy("Direct bind"))], + initial='searchbind', + help_text=ugettext_lazy("Choose the authentication method to use"), + widget=InlineRadioSelect + ) + + ldap_bind_dn = forms.CharField( + label=ugettext_lazy("Bind DN"), + initial='', + help_text=ugettext_lazy("The distinguished name to use when binding to the LDAP server. Leave empty for an anonymous bind"), + required=False + ) + + ldap_bind_password = forms.CharField( + 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, + required=False + ) + + ldap_search_base = forms.CharField( + label=ugettext_lazy("Search base"), + initial="", + help_text=ugettext_lazy("The distinguished name of the search base"), + required=False + ) + + ldap_search_filter = forms.CharField( + label=ugettext_lazy("Search filter"), + initial="(mail=%(user)s)", + help_text=ugettext_lazy("An optional filter string (e.g. '(objectClass=person)'). In order to be valid, it must be enclosed in parentheses."), + required=False + ) + + ldap_user_dn_template = forms.CharField( + label=ugettext_lazy("User DN template"), + initial="", + help_text=ugettext_lazy("The template used to construct a user's DN. It should contain one placeholder (ie. %(user)s)"), + required=False + ) + + ldap_password_attribute = forms.CharField( + label=ugettext_lazy("Password attribute"), + initial="userPassword", + help_text=ugettext_lazy("The attribute used to store user passwords") + ) + + ldap_is_active_directory = YesNoField( + label=ugettext_lazy("Active Directory"), + initial="no", + help_text=ugettext_lazy("Tell if the LDAP server is an Active Directory one") + ) + + sep3 = SeparatorField(label=ugettext_lazy("Miscellaneous")) + + log_maximum_age = forms.IntegerField( + label=ugettext_lazy("Maximum log record age"), + initial=365, + help_text=ugettext_lazy("The maximum age in days of a log record") + ) + + items_per_page = forms.IntegerField( + label=ugettext_lazy("Items per page"), + initial=30, + help_text=ugettext_lazy("Number of displayed items per page") + ) + + default_top_redirection = forms.ChoiceField( + label=ugettext_lazy("Default top redirection"), + choices=[], + initial="admin", + help_text=ugettext_lazy("The default redirection used when no application is specified") + ) + + # Visibility rules + visibility_rules = { + "ldap_sep": "authentication_type=ldap", + "ldap_server_address": "authentication_type=ldap", + "ldap_server_port": "authentication_type=ldap", + "ldap_secured": "authentication_type=ldap", + "ldap_auth_method": "authentication_type=ldap", + "ldap_bind_dn": "ldap_auth_method=searchbind", + "ldap_bind_password": "ldap_auth_method=searchbind", + "ldap_search_base": "ldap_auth_method=searchbind", + "ldap_search_filter": "ldap_auth_method=searchbind", + "ldap_user_dn_template": "ldap_auth_method=directbind", + "ldap_password_attribute": "authentication_type=ldap", + "ldap_is_active_directory": "authentication_type=ldap" + } + + def __init__(self, *args, **kwargs): + super(GeneralParametersForm, self).__init__(*args, **kwargs) + self.fields["default_top_redirection"].choices = enabled_applications() + + def clean_ldap_user_dn_template(self): + tpl = self.cleaned_data["ldap_user_dn_template"] + try: + test = tpl % {"user": "toto"} + except ValueError: + raise forms.ValidationError(_("Invalid syntax")) + return tpl + + def clean(self): + """Custom validation method + + Depending on 'ldap_auth_method' value, we check for different + required parameters. + """ + super(GeneralParametersForm, self).clean() + if len(self._errors): + raise forms.ValidationError(self._errors) + cleaned_data = self.cleaned_data + if cleaned_data["authentication_type"] != "ldap": + return cleaned_data + + if cleaned_data["ldap_auth_method"] == "searchbind": + required_fields = ["ldap_search_base", "ldap_search_filter"] + else: + required_fields = ["ldap_user_dn_template"] + + for f in required_fields: + if not f in cleaned_data or cleaned_data[f] == u'': + self._errors[f] = self.error_class([_("This field is required")]) + + return cleaned_data + + def to_django_settings(self): + """Apply LDAP related parameters to Django settings + + Doing so, we can use the django_auth_ldap module. + """ + from django.conf import settings + try: + import ldap + from django_auth_ldap.config import LDAPSearch + ldap_available = True + except ImportError: + ldap_available = False + + values = self.get_current_values() + if not ldap_available or values["authentication_type"] != "ldap": + return + if not hasattr(settings, "AUTH_LDAP_USER_ATTR_MAP"): + setattr(settings, "AUTH_LDAP_USER_ATTR_MAP", { + "first_name": "givenName", + "email": "mail", + "last_name": "sn" + }) + ldap_uri = 'ldaps://' if values["ldap_secured"] == "yes" else "ldap://" + ldap_uri += "%s:%s" % (values["ldap_server_address"], values["ldap_server_port"]) + setattr(settings, "AUTH_LDAP_SERVER_URI", ldap_uri) + if values["ldap_auth_method"] == "searchbind": + setattr(settings, "AUTH_LDAP_BIND_DN", values["ldap_bind_dn"]) + setattr(settings, "AUTH_LDAP_BIND_PASSWORD", values["ldap_bind_password"]) + search = LDAPSearch( + values["ldap_search_base"], ldap.SCOPE_SUBTREE, + values["ldap_search_filter"] + ) + setattr(settings, "AUTH_LDAP_USER_SEARCH", search) + else: + setattr(settings, "AUTH_LDAP_USER_DN_TEMPLATE", values["ldap_user_dn_template"]) + + if values["ldap_is_active_directory"] == "yes": + if not hasattr(settings, "AUTH_LDAP_GLOBAL_OPTIONS"): + setattr(settings, "AUTH_LDAP_GLOBAL_OPTIONS", { + ldap.OPT_REFERRALS: False + }) + else: + settings.AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_REFERRALS] = False + + +def translate_language_code(value): + if "-" in value: + return value.split("-")[0] + return value + + +class UserSettings(parameters.UserParametersForm): + app = "core" + + sep = SeparatorField(label=ugettext_lazy("Display")) + + lang = forms.ChoiceField( + initial=translate_language_code(settings.LANGUAGE_CODE), + label=ugettext_lazy("Prefered language"), + choices=[("de", "deutsch"), ("en", "english"), + ("es", "español"), ("fr", "français"), + ("it", "italiano"), ("pt", "português"), + ("sv", "svenska"),], + help_text=ugettext_lazy("Prefered language to display pages") + ) diff --git a/modoboa/core/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl similarity index 100% rename from modoboa/core/templates/settings.py.tpl rename to modoboa/core/commands/templates/settings.py.tpl diff --git a/modoboa/core/templates/urls.py.tpl b/modoboa/core/commands/templates/urls.py.tpl similarity index 100% rename from modoboa/core/templates/urls.py.tpl rename to modoboa/core/commands/templates/urls.py.tpl diff --git a/modoboa/core/exceptions.py b/modoboa/core/exceptions.py new file mode 100644 index 000000000..063db7ad9 --- /dev/null +++ b/modoboa/core/exceptions.py @@ -0,0 +1,14 @@ +""" +: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 diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py new file mode 100644 index 000000000..95999dbd0 --- /dev/null +++ b/modoboa/core/extensions.py @@ -0,0 +1,92 @@ +import os +import sys +import re +from django.conf import settings +from django.conf.urls import include + + +class ModoExtension(object): + """Base extension class + + Each Modoboa extension must inherit from this class to be + considered as valid. + """ + name = None + version = None + description = "" + url = None + needs_media = False + always_active = False + + def infos(self): + return dict(name=self.name, label=self.label, version=self.version, + description=self.description, url=self.url) + + def init(self): + pass + + def destroy(self): + pass + + def load(self): + pass + + +class ExtensionsPool(object): + """The extensions manager""" + + def __init__(self): + self.extensions = dict() + + def register_extension(self, ext, show=True): + self.extensions[ext.name] = dict(cls=ext, show=show) + + def get_extension(self, name): + if not name in self.extensions: + return None + if not "instance" in self.extensions[name]: + self.extensions[name]["instance"] = self.extensions[name]["cls"]() + return self.extensions[name]["instance"] + + def get_extension_infos(self, name): + instance = self.get_extension(name) + if instance is None: + return None + return instance.infos() + + def load_all(self): + from modoboa.core.models import Extension + + for ext in settings.MODOBOA_APPS: + __import__(ext) + result = [] + for extname in self.extensions.keys(): + extinstance = self.get_extension(extname) + if not extinstance.always_active: + try: + ext = Extension.objects.get(name=extname) + if not ext.enabled: + continue + 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): + result = [] + for extname, extdef in self.extensions.iteritems(): + if not extdef["show"]: + continue + infos = extdef["instance"].infos() + infos["id"] = extname + result += [infos] + return sorted(result, key=lambda i: i["name"]) + +exts_pool = ExtensionsPool() diff --git a/modoboa/core/fixtures/initial_users.json b/modoboa/core/fixtures/initial_users.json new file mode 100644 index 000000000..bf60843ba --- /dev/null +++ b/modoboa/core/fixtures/initial_users.json @@ -0,0 +1 @@ +[{"pk": 1, "model": "core.user", "fields": {"username": "admin", "first_name": "", "last_name": "", "is_active": true, "is_superuser": true, "is_staff": false, "last_login": "2012-01-26 15:19:25", "groups": [], "user_permissions": [], "password": "{CRYPT}dTTsGDkA5ZHKg", "email": "", "date_joined": "2012-01-26 15:19:25"}},{"pk": 1, "model": "core.objectaccess", "fields": {"is_owner": true, "user": 1, "content_type": ["core", "user"], "object_id": 1}}] \ No newline at end of file diff --git a/modoboa/core/forms.py b/modoboa/core/forms.py new file mode 100644 index 000000000..00c2119a0 --- /dev/null +++ b/modoboa/core/forms.py @@ -0,0 +1,71 @@ +# coding: utf-8 +from django import forms +from django.utils.translation import ugettext as _, ugettext_lazy +from modoboa.core.models import User +from modoboa.lib import parameters + + +class LoginForm(forms.Form): + username = forms.CharField( + label=ugettext_lazy("Username"), + widget=forms.TextInput(attrs={"class": "input-block-level"}) + ) + password = forms.CharField( + label=ugettext_lazy("Password"), + widget=forms.PasswordInput(attrs={"class": "input-block-level"}) + ) + rememberme = forms.BooleanField( + initial=False, + required=False + ) + + +class ProfileForm(forms.ModelForm): + oldpassword = forms.CharField( + label=ugettext_lazy("Old password"), required=False, + widget=forms.PasswordInput + ) + newpassword = forms.CharField( + label=ugettext_lazy("New password"), required=False, + widget=forms.PasswordInput + ) + confirmation = forms.CharField( + label=ugettext_lazy("Confirmation"), required=False, + widget=forms.PasswordInput + ) + + class Meta: + model = User + fields = ("first_name", "last_name") + + def __init__(self, update_password, *args, **kwargs): + super(ProfileForm, self).__init__(*args, **kwargs) + if not update_password: + del self.fields["oldpassword"] + del self.fields["newpassword"] + del self.fields["confirmation"] + + def clean_oldpassword(self): + if self.cleaned_data["oldpassword"] == "": + return self.cleaned_data["oldpassword"] + + if parameters.get_admin("AUTHENTICATION_TYPE", app="admin") != "local": + return self.cleaned_data["oldpassword"] + + if not self.instance.check_password(self.cleaned_data["oldpassword"]): + raise forms.ValidationError(_("Old password mismatchs")) + return self.cleaned_data["oldpassword"] + + def clean_confirmation(self): + if self.cleaned_data["newpassword"] != self.cleaned_data["confirmation"]: + raise forms.ValidationError(_("Passwords mismatch")) + return self.cleaned_data["confirmation"] + + def save(self, commit=True): + user = super(ProfileForm, self).save(commit=False) + if commit: + if self.cleaned_data.has_key("confirmation") and \ + self.cleaned_data["confirmation"] != "": + user.set_password(self.cleaned_data["confirmation"], self.cleaned_data["oldpassword"]) + user.save() + return user diff --git a/modoboa/core/loggers.py b/modoboa/core/loggers.py new file mode 100644 index 000000000..210fd63eb --- /dev/null +++ b/modoboa/core/loggers.py @@ -0,0 +1,11 @@ +import logging +from models import Log + + +class SQLHandler(logging.Handler): + + def emit(self, record): + Log.objects.create( + message=record.getMessage(), level=record.levelname, + logger=record.name + ) diff --git a/modoboa/core/models.py b/modoboa/core/models.py new file mode 100644 index 000000000..c277af0f2 --- /dev/null +++ b/modoboa/core/models.py @@ -0,0 +1,510 @@ +import re +import hashlib +import crypt +import base64 +from random import Random +import reversion +from django.db import models +from django.db.models import Q +from django.dispatch import receiver +from django.utils import timezone +from django.utils.translation import ugettext as _, ugettext_lazy +from django.utils.crypto import constant_time_compare +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes import generic +from django.contrib.auth.models import ( + UserManager, Group, AbstractBaseUser, PermissionsMixin +) +from modoboa.lib import events, md5crypt, parameters +from modoboa.lib.exceptions import PermDeniedException +from modoboa.core.extensions import exts_pool +from modoboa.core.exceptions import AdminError + +try: + from modoboa.lib.ldaputils import * + ldap_available = True +except ImportError: + ldap_available = False + + +class User(AbstractBaseUser, PermissionsMixin): + """Custom User model. + + It overloads the way passwords are stored into the database. The + main reason to change this mechanism is to ensure the + compatibility with the way Dovecot stores passwords. + + It also adds new attributes and methods. + """ + username = models.CharField(max_length=254, unique=True) + first_name = models.CharField(max_length=30, blank=True) + last_name = models.CharField(max_length=30, blank=True) + email = models.EmailField(max_length=254, blank=True) + is_staff = models.BooleanField(default=False) + is_active = models.BooleanField(default=True) + date_joined = models.DateTimeField(default=timezone.now) + + objects = UserManager() + + USERNAME_FIELD = 'username' + REQUIRED_FIELDS = ['email'] + + class Meta: + ordering = ["username"] + + password_expr = re.compile(r'(\{(\w+)\}|(\$1\$))(.+)') + + def delete(self, fromuser, keep_mb_dir, *args, **kwargs): + """Custom delete method + + To check permissions properly, we need to make a distinction + between 2 cases: + + * If the user owns a mailbox, the check is made on that object + (useful for domain admins) + + * Otherwise, the check is made on the user + """ + from modoboa.lib.permissions import \ + get_object_owner, grant_access_to_object, ungrant_access_to_object + + if fromuser == self: + raise AdminError(_("You can't delete your own account")) + + if not fromuser.can_access(self): + raise PermDeniedException + + # if self.has_mailbox: + # mb = self.mailbox_set.all()[0] + # if not fromuser.can_access(mb): + # raise PermDeniedException + # mb.delete(keepdir=keep_mb_dir) + + owner = get_object_owner(self) + for ooentry in self.objectaccess_set.filter(is_owner=True): + if ooentry.content_object is not None: + grant_access_to_object(owner, ooentry.content_object, True) + + events.raiseEvent("AccountDeleted", self) + ungrant_access_to_object(self) + super(User, self).delete(*args, **kwargs) + + @staticmethod + def get_content_type(): + """An uggly hack to retrieve the appropriate content_type! + + The explanation is available here: + https://code.djangoproject.com/ticket/11154 + + Quickly, the content_types framework does not retrieve the + appropriate content type for proxy models, it retrieves the + one of the first parent that is not a proxy. + """ + if not hasattr(User, "ct"): + User.ct = ContentType.objects.get(app_label="admin", model="user") + return User.ct + + def _crypt_password(self, raw_value): + scheme = parameters.get_admin("PASSWORD_SCHEME") + if type(raw_value) is unicode: + raw_value = raw_value.encode("utf-8") + if scheme == "crypt": + salt = "".join(Random().sample(string.letters + string.digits, 2)) + result = crypt.crypt(raw_value, salt) + prefix = "{CRYPT}" + elif scheme == "md5": + obj = hashlib.md5(raw_value) + result = obj.hexdigest() + prefix = "{MD5}" + # The md5crypt scheme is the only supported method that has both: + # (a) a salt ("crypt" has this too), + # (b) supports passwords lengths of more than 8 characters (all except + # "crypt"). + elif scheme == "md5crypt": + # The salt may vary from 12 to 48 bits. (Using all six bytes here + # with a subset of characters means we get only 35 random bits.) + salt = "".join(Random().sample(string.letters + string.digits, 6)) + result = md5crypt(raw_value, salt) + prefix = "" # the result already has $1$ prepended to it + # to signify what this is + elif scheme == "sha256": + obj = hashlib.sha256(raw_value) + result = base64.b64encode(obj.digest()) + prefix = "{SHA256}" + else: + scheme = "plain" + result = raw_value + prefix = "{PLAIN}" + return "%s%s" % (prefix, result) + + def set_password(self, raw_value, curvalue=None): + """Password update + + Update the current mailbox's password with the given clear + value. This value is encrypted according to the defined method + before it is saved. + + :param raw_value: the new password's value + :param curvalue: the current password (for LDAP authentication) + """ + if parameters.get_admin("AUTHENTICATION_TYPE") == "local": + self.password = self._crypt_password(raw_value) + else: + if not ldap_available: + raise AdminError( + _("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))) + events.raiseEvent( + "PasswordUpdated", self, raw_value, self.pk is None + ) + + def check_password(self, raw_value): + m = self.password_expr.match(self.password) + if m is None: + return False + if type(raw_value) is unicode: + raw_value = raw_value.encode("utf-8") + scheme = (m.group(2) or m.group(3)).lower() + val2 = m.group(4) + if scheme == u"crypt": + val1 = crypt.crypt(raw_value, val2) + elif scheme == u"md5": + val1 = hashlib.md5(raw_value).hexdigest() + elif scheme == u"sha256": + val1 = base64.b64encode(hashlib.sha256(raw_value).digest()) + elif scheme == u"$1$": # md5crypt + salt, hashed = val2.split('$') + val1 = md5crypt(raw_value, str(salt)) + val2 = self.password # re-add scheme for comparison below + else: + val1 = raw_value + return constant_time_compare(val1, val2) + + @property + def tags(self): + return [{"name": "account", "label": _("account"), "type": "idt"}, + {"name": self.group, "label": self.group, "type": "grp", "color": "info"}] + + # @property + # def has_mailbox(self): + # return self.mailbox_set.count() != 0 + + @property + def fullname(self): + if self.first_name != u"": + return u"%s %s" % (self.first_name, self.last_name) + return self.username + + @property + def identity(self): + return self.username + + @property + def name_or_rcpt(self): + if self.first_name != "": + return "%s %s" % (self.first_name, self.last_name) + return "----" + + @property + def group(self): + if self.is_superuser: + return "SuperAdmins" + try: + return self.groups.all()[0].name + except IndexError: + return "SimpleUsers" + + @property + def enabled(self): + return self.is_active + + @property + def encoded_address(self): + from email.header import Header + if self.first_name != "" or self.last_name != "": + return "%s <%s>" % \ + (Header(self.fullname, 'utf8').encode(), self.email) + return self.email + + def belongs_to_group(self, name): + """Simple shortcut to check if this user is a member of a + specific group. + + :param name: the group's name + :return: a boolean + """ + try: + self.groups.get(name=name) + except Group.DoesNotExist: + return False + return True + + def is_owner(self, obj): + """Tell is the user is the unique owner of this object + + :param obj: an object inheriting from ``models.Model`` + :return: a boolean + """ + from modoboa.lib.permissions import get_content_type + + ct = get_content_type(obj) + try: + ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) + except ObjectAccess.DoesNotExist: + return False + return ooentry.is_owner + + def can_access(self, obj): + """Check if the user can access a specific object + + This function is recursive : if the given user hasn't got direct + access to this object and if he has got access other ``User`` + objects, we check if one of those users owns the object. + + :param obj: a admin object + :return: a boolean + """ + from modoboa.lib.permissions import get_content_type + + if self.is_superuser: + return True + + ct = get_content_type(obj) + try: + ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) + except ObjectAccess.DoesNotExist: + pass + else: + return True + if ct.model == "user": + return False + + ct = self.get_content_type() + qs = self.objectaccess_set.filter(content_type=ct) + for ooentry in qs.all(): + if ooentry.content_object.is_owner(obj): + return True + return False + + def grant_access_to_all_objects(self): + """Give access to all objects defined in the database + + Must be used when an account is promoted as a super user. + """ + from modoboa.lib.permissions import grant_access_to_objects, get_content_type + grant_access_to_objects(self, User.objects.all(), get_content_type(User)) + grant_access_to_objects(self, Domain.objects.all(), get_content_type(Domain)) + grant_access_to_objects(self, DomainAlias.objects.all(), get_content_type(DomainAlias)) + grant_access_to_objects(self, Mailbox.objects.all(), get_content_type(Mailbox)) + grant_access_to_objects(self, Alias.objects.all(), get_content_type(Alias)) + + def set_role(self, role): + """Set administrative role for this account + + :param string role: the role to set + """ + if role is None or self.group == role: + return + self.groups.clear() + if role == "SuperAdmins": + self.is_superuser = True + self.grant_access_to_all_objects() + else: + if self.is_superuser: + ObjectAccess.objects.filter(user=self).delete() + self.is_superuser = False + try: + self.groups.add(Group.objects.get(name=role)) + except Group.DoesNotExist: + self.groups.add(Group.objects.get(name="SimpleUsers")) + if self.group != "SimpleUsers" and not self.can_access(self): + from modoboa.lib.permissions import grant_access_to_object + grant_access_to_object(self, self) + self.save() + + 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("AccountCreated", self) + + def save(self, *args, **kwargs): + if "creator" in kwargs: + creator = kwargs["creator"] + del kwargs["creator"] + else: + creator = None + super(User, self).save(*args, **kwargs) + if creator is not None: + self.post_create(creator) + + def from_csv(self, user, row, crypt_password=True): + """Create a new account from a CSV file entry + + The expected order is the following:: + + loginname, password, first name, last name, enabled, group, address[, domain, ...] + + :param user: + :param row: a list containing the expected information + :param crypt_password: + """ + if len(row) < 6: + raise AdminError(_("Invalid line")) + role = row[6].strip() + if not user.is_superuser and not role in ["SimpleUsers", "DomainAdmins"]: + raise PermDeniedException( + _("You can't import an account with a role greater than yours") + ) + self.username = row[1].strip() + if crypt_password: + self.set_password(row[2].strip()) + else: + self.password = row[2].strip() + self.first_name = row[3].strip() + self.last_name = row[4].strip() + self.is_active = (row[5].strip() == 'True') + self.save(creator=user) + self.set_role(role) + + self.email = row[7].strip() + if self.email != "": + mailbox, domname = split_mailbox(self.email) + try: + domain = Domain.objects.get(name=domname) + except Domain.DoesNotExist: + raise AdminError( + _("Account import failed (%s): domain does not exist" % self.username) + ) + if not user.can_access(domain): + raise PermDeniedException + mb = Mailbox(address=mailbox, domain=domain, user=self, use_domain_quota=True) + mb.set_quota(override_rules=user.has_perm("admin.change_domain")) + mb.save(creator=user) + if self.group == "DomainAdmins": + for domname in row[8:]: + try: + dom = Domain.objects.get(name=domname.strip()) + except Domain.DoesNotExist: + continue + dom.add_admin(self) + + def to_csv(self, csvwriter): + row = ["account", self.username.encode("utf-8"), self.password, + self.first_name.encode("utf-8"), self.last_name.encode("utf-8"), + self.is_active, self.group, self.email] + if self.group == "DomainAdmins": + row += [dom.name for dom in self.get_domains()] + csvwriter.writerow(row) + +reversion.register(User) + + +class ObjectAccess(models.Model): + user = models.ForeignKey(User) + content_type = models.ForeignKey(ContentType) + object_id = models.PositiveIntegerField() + content_object = generic.GenericForeignKey('content_type', 'object_id') + is_owner = models.BooleanField(default=False) + + class Meta: + unique_together = (("user", "content_type", "object_id"),) + + def __unicode__(self): + return "%s => %s (%s)" % (self.user, self.content_object, self.content_type) + + +class Extension(models.Model): + name = models.CharField(max_length=150) + enabled = models.BooleanField( + ugettext_lazy('enabled'), + help_text=ugettext_lazy("Check to enable this extension") + ) + + def __init__(self, *args, **kwargs): + super(Extension, self).__init__(*args, **kwargs) + + def __unicode__(self): + return self.name + + def __get_ext_instance(self): + if not self.name: + return None + if hasattr(self, "instance") and self.instance: + return + self.instance = exts_pool.get_extension(self.name) + if self.instance: + self.__get_ext_dir() + + def __get_ext_dir(self): + modname = self.instance.__module__ + path = os.path.realpath(sys.modules[modname].__file__) + self.extdir = os.path.dirname(path) + + def on(self): + self.enabled = True + self.save() + + self.__get_ext_instance() + self.instance.load() + self.instance.init() + + if self.instance.needs_media: + path = os.path.join(settings.MEDIA_ROOT, self.name) + exec_cmd("mkdir %s" % path) + + events.raiseEvent("ExtEnabled", self) + + def off(self): + self.__get_ext_instance() + if self.instance is None: + return + self.instance.destroy() + + self.enabled = False + self.save() + + if self.instance.needs_media: + path = os.path.join(settings.MEDIA_ROOT, self.name) + exec_cmd("rm -r %s" % path) + + events.raiseEvent("ExtDisabled", self) + +reversion.register(Extension) + + +class Log(models.Model): + date_created = models.DateTimeField(auto_now_add=True) + message = models.CharField(max_length=255) + level = models.CharField(max_length=15) + logger = models.CharField(max_length=30) + + +@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: + action = _("added") + level = "info" + elif version.type == reversion.models.VERSION_CHANGE: + 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) diff --git a/modoboa/core/tables.py b/modoboa/core/tables.py new file mode 100644 index 000000000..9e1d79a53 --- /dev/null +++ b/modoboa/core/tables.py @@ -0,0 +1,12 @@ +from django.utils.translation import ugettext as _, ugettext_lazy +from modoboa.lib import tables + + +class ExtensionsTable(tables.Table): + idkey = "id" + selection = tables.SelectionColumn("selection", width="4%", header=False) + label = tables.Column("label", label=ugettext_lazy("Name"), width="15%") + version = tables.Column("version", label=ugettext_lazy("Version"), width="6%") + descr = tables.Column("description", label=ugettext_lazy("Description")) + + cols_order = ["selection", "label", "version", "descr"] diff --git a/modoboa/core/templates/core/extensions.html b/modoboa/core/templates/core/extensions.html new file mode 100644 index 000000000..276524756 --- /dev/null +++ b/modoboa/core/templates/core/extensions.html @@ -0,0 +1,6 @@ +{% load i18n %}{% load url from future %} +

{% trans "Extensions" %} {% trans "Choose the extension(s) to use" %}


+
{% csrf_token %} + {{ extensions|safe }} +
+{% trans 'Save' %} diff --git a/modoboa/core/templates/core/information.html b/modoboa/core/templates/core/information.html new file mode 100644 index 000000000..80f7500a3 --- /dev/null +++ b/modoboa/core/templates/core/information.html @@ -0,0 +1,17 @@ +{% load i18n libextras %} +

{% trans "Information" %} {% trans "Various information about Modoboa" %}


+
Modoboa {% get_version %} {% trans "is currently in use." %}
+ +

{% trans "Active users" %}

+ + + + + + + {% connected_users as users %}{% for u in users %} + + + + {% endfor %} +
{% trans "Name" %}{% trans "Group name" %}{% trans "Last login" %}
{{ u.username }}{{ u.group }}{{ u.last_login }}
diff --git a/modoboa/core/templates/core/logs.html b/modoboa/core/templates/core/logs.html new file mode 100644 index 000000000..741e9e6eb --- /dev/null +++ b/modoboa/core/templates/core/logs.html @@ -0,0 +1,20 @@ +{% load i18n core_tags %} +

{% trans "Logs" %} {% trans "Previously on Modoboa..." %}


+ + + + + + + + + + + {% for l in logs %} + + + + + {% endfor %} + +
{% trans "Date" %}{% trans "Level" %}{% trans "Logger" %}{% trans "Message" %}
{{ l.date_created }}{{ l.level|colorize_level|safe }}{{ l.logger }}{{ l.message|tohtml|safe }}
diff --git a/modoboa/core/templates/core/parameters.html b/modoboa/core/templates/core/parameters.html new file mode 100644 index 000000000..c425d0e60 --- /dev/null +++ b/modoboa/core/templates/core/parameters.html @@ -0,0 +1,8 @@ +{% load i18n libextras %}{% load url from future %} +{% url "modoboa.admin.views.saveparameters" as action %} +

{% trans "Parameters" %} {% trans "Configure Modoboa" %}


+
{% csrf_token %} + {% include "common/parameters.html" %} +
+
+{% trans 'Save' %} diff --git a/modoboa/core/templates/core/settings_header.html b/modoboa/core/templates/core/settings_header.html new file mode 100644 index 000000000..b9fc504ec --- /dev/null +++ b/modoboa/core/templates/core/settings_header.html @@ -0,0 +1,23 @@ +{% extends "twocols.html" %} +{% load i18n libextras core_tags %} +{% block pagetitle %}{% trans "Modoboa settings" %}{% endblock %} +{% block html_head %} + + + + + + + +{% endblock %} +{% block leftcol %}{% endblock %} +{% block apparea %}
{% endblock %} diff --git a/modoboa/core/templates/core/user_index.html b/modoboa/core/templates/core/user_index.html new file mode 100644 index 000000000..e721beabe --- /dev/null +++ b/modoboa/core/templates/core/user_index.html @@ -0,0 +1,21 @@ +{% extends "twocols.html" %}{% load i18n core_tags libextras %} +{% block pagetitle %}{% trans "Settings" %}{% endblock %} +{% block html_head %} + + + + + +{% endblock %} +{% block leftcol %}{% endblock %} +{% block apparea %}
{% endblock %} diff --git a/modoboa/core/templates/core/user_preferences.html b/modoboa/core/templates/core/user_preferences.html new file mode 100644 index 000000000..c63f285c0 --- /dev/null +++ b/modoboa/core/templates/core/user_preferences.html @@ -0,0 +1,9 @@ +{% load i18n %}{% load url from future %} +

{% trans "Preferences" %} {% trans "Customize Modoboa" %}

+
+
{% csrf_token %} + {% include "common/parameters.html" %} +
+ +
diff --git a/modoboa/core/templates/core/user_profile.html b/modoboa/core/templates/core/user_profile.html new file mode 100644 index 000000000..462892b52 --- /dev/null +++ b/modoboa/core/templates/core/user_profile.html @@ -0,0 +1,10 @@ +{% load i18n libextras %}{% load url from future %} +

{% trans "Profile" %} {% trans "Update your personal information" %}

+
+
{% csrf_token %} + {% render_form form %} +
+ +
+
diff --git a/modoboa/core/templatetags/__init__.py b/modoboa/core/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py new file mode 100644 index 000000000..3c7d720ac --- /dev/null +++ b/modoboa/core/templatetags/core_tags.py @@ -0,0 +1,151 @@ +import re +from django import template +from django.template.loader import render_to_string +from django.utils.translation import ugettext as _ +from django.core.urlresolvers import reverse +from modoboa.lib import events + + +register = template.Library() + + +@register.simple_tag +def core_menu(selection, user): + entries = [] + # if user.has_perm("admin.view_domains"): + # entries += [ + # {"name" : "domains", + # "url" : reverse("modoboa.admin.views.domains"), + # "label" : _("Domains")} + # ] + entries += \ + events.raiseQueryEvent("AdminMenuDisplay", "top_menu", user) + # if user.has_perm("admin.add_user") or user.has_perm("admin.add_alias"): + # entries += [ + # {"name" : "identities", + # "url" : reverse("modoboa.admin.views.identities"), + # "label" : _("Identities")}, + # ] + if user.is_superuser: + entries += [ + {"name": "settings", + "label": _("Modoboa"), + "url": reverse("modoboa.core.views.admin.viewsettings")} + ] + + if not len(entries): + return "" + return render_to_string("common/menulist.html", { + "entries": entries, + "selection": selection, + "user": user} + ) + + +@register.simple_tag +def extensions_menu(selection, user): + menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu", user) + return render_to_string('common/menulist.html', { + "selection": selection, "entries": menu, "user": user + }) + + +@register.simple_tag +def admin_menu(selection, user): + entries = [ + {"name": "extensions", + "class": "ajaxlink", + "url": "extensions/", + "label": _("Extensions"), + "img": ""}, + {"name": "info", + "class": "ajaxlink", + "url": "info/", + "label": _("Information")}, + {"name": "logs", + "class": "ajaxlink", + "url": "logs/", + "label": _("Logs")}, + {"name": "parameters", + "class": "ajaxlink", + "url": "parameters/", + "img": "", + "label": _("Parameters")}, + ] + return render_to_string('common/menu.html', { + "entries": entries, + "css": "nav nav-list", + "selection": selection, + "user": user + }) + + +@register.simple_tag +def user_menu(user, selection): + entries = [ + {"name": "user", + "img": "icon-user icon-white", + "label": user.fullname, + "menu": [ + {"name": "settings", + "img": "icon-list", + "label": _("Settings"), + "url": reverse("modoboa.core.views.user.index")} + ]} + ] + + entries[0]["menu"] += \ + events.raiseQueryEvent("UserMenuDisplay", "options_menu", user) \ + + [{"name": "logout", + "url": reverse("modoboa.core.views.auth.dologout"), + "label": _("Logout"), + "img": "icon-off"}] + + return render_to_string("common/menulist.html", { + "selection": selection, "entries": entries, "user": user + }) + + +@register.simple_tag +def uprefs_menu(selection, user): + entries = [ + {"name": "profile", + "class": "ajaxlink", + "url": "profile/", + "label": _("Profile")}, + {"name": "preferences", + "class": "ajaxlink", + "url": "preferences/", + "label": _("Preferences")}, + ] + # if user.has_mailbox: + # entries.insert(0, {"name": "forward", + # "class": "ajaxlink", + # "url": "forward/", + # "label": _("Forward")}) + + entries += events.raiseQueryEvent("UserMenuDisplay", "uprefs_menu", user) + entries = sorted(entries, key=lambda e: e["label"]) + return render_to_string('common/menu.html', { + "entries": entries, + "css": "nav nav-list", + "selection": selection, + "user": user + }) + + +@register.filter +def colorize_level(level): + classes = { + "INFO": "text-info", + "WARNING": "text-warning", + "CRITICAL": "text-error" + } + if not level in classes: + return level + return "

%s

" % (classes[level], level) + + +@register.filter +def tohtml(message): + return re.sub("'(.*?)'", "\g<1>", message) diff --git a/modoboa/core/urls.py b/modoboa/core/urls.py new file mode 100644 index 000000000..a4d1147f9 --- /dev/null +++ b/modoboa/core/urls.py @@ -0,0 +1,14 @@ +from django.conf.urls import patterns, url + + +urlpatterns = patterns( + 'modoboa.core.views', + + (r'^settings/$', 'viewsettings'), + (r'^settings/parameters/$', 'viewparameters'), + (r'^settings/parameters/save/$', 'saveparameters'), + (r'^settings/extensions/$', 'viewextensions'), + (r'^settings/extensions/save/$', 'saveextensions'), + (r'^settings/info/$', 'information'), + (r'^settings/logs/$', 'logs'), +) diff --git a/modoboa/core/views/__init__.py b/modoboa/core/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/core/views/admin.py b/modoboa/core/views/admin.py new file mode 100644 index 000000000..19a0009c3 --- /dev/null +++ b/modoboa/core/views/admin.py @@ -0,0 +1,128 @@ +from django.shortcuts import render +from django.template.loader import render_to_string +from django.utils.translation import ugettext as _ +from django.contrib.auth.decorators import ( + login_required, user_passes_test +) +from modoboa.lib import parameters +from modoboa.lib.webutils import ( + ajax_simple_response, _render_to_string +) +from modoboa.lib.listing import get_sort_order, get_listing_page +from modoboa.core.models import Extension, Log +from modoboa.core.tables import ExtensionsTable + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def viewsettings(request, tplname='core/settings_header.html'): + return render(request, tplname, { + "selection": "settings" + }) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def viewparameters(request, tplname='core/parameters.html'): + return ajax_simple_response({ + "status": "ok", + "left_selection": "parameters", + "content": _render_to_string(request, tplname, { + "forms": parameters.get_admin_forms + }) + }) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def saveparameters(request): + for formdef in parameters.get_admin_forms(request.POST): + form = formdef["form"] + if form.is_valid(): + 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") + )) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def viewextensions(request, tplname='core/extensions.html'): + from modoboa.core.extensions import exts_pool + + exts = exts_pool.list_all() + for ext in exts: + try: + dbext = Extension.objects.get(name=ext["id"]) + ext["selection"] = dbext.enabled + except Extension.DoesNotExist: + dbext = Extension() + dbext.name = ext["id"] + dbext.enabled = False + dbext.save() + ext["selection"] = False + + tbl = ExtensionsTable(request, exts) + return ajax_simple_response({ + "status": "ok", + "content": _render_to_string(request, tplname, {"extensions": tbl}) + }) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def saveextensions(request): + actived_exts = Extension.objects.filter(enabled=True) + found = [] + for k in request.POST.keys(): + if k.startswith("select_"): + parts = k.split("_", 1) + dbext = Extension.objects.get(name=parts[1]) + if not dbext in actived_exts: + dbext.on() + else: + found += [dbext] + for ext in actived_exts: + if not ext in found: + ext.off() + + return ajax_simple_response(dict( + status="ok", respmsg=_("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", + "content": render_to_string(tplname, {}) + }) + + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def logs(request, tplname="core/logs.html"): + from modoboa.lib.templatetags.libextras import pagination_bar + + sort_order, sort_dir = get_sort_order( + request.GET, "date_created", + allowed_values=['date_created', 'level', 'logger', 'message'] + ) + page = get_listing_page( + Log.objects.all().order_by("%s%s" % (sort_dir, sort_order)), + request.GET.get("page", 1) + ) + return ajax_simple_response({ + "status": "ok", + "content": render_to_string(tplname, { + "logs": page.object_list, + }), + "page": page.number, + "paginbar": pagination_bar(page), + }) diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py new file mode 100644 index 000000000..b892b0f58 --- /dev/null +++ b/modoboa/core/views/auth.py @@ -0,0 +1,71 @@ +# coding: utf-8 +import logging +from django.views.decorators.cache import never_cache +from django.contrib.auth import authenticate, login, logout +from django.http import HttpResponse, HttpResponseRedirect +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext as _ +from modoboa.lib import events, parameters +from modoboa.lib.webutils import _render_to_string +from modoboa.core.forms import LoginForm +from modoboa.auth.lib import encrypt + + +def dologin(request): + error = None + if request.method == "POST": + form = LoginForm(request.POST) + if form.is_valid(): + logger = logging.getLogger('modoboa.auth') + user = authenticate(username=form.cleaned_data["username"], + password=form.cleaned_data["password"]) + if user and user.is_active: + login(request, user) + if not form.cleaned_data["rememberme"]: + request.session.set_expiry(0) + # if request.user.has_mailbox: + # request.session["password"] = encrypt(form.cleaned_data["password"]) + + request.session["django_language"] = \ + parameters.get_user(request.user, "LANG") + + logger.info( + _("User '%s' successfully logged in" % user.username) + ) + events.raiseEvent("UserLogin", request, + form.cleaned_data["username"], + form.cleaned_data["password"]) + + nextlocation = request.POST.get("next", None) + if nextlocation is None or nextlocation == "None": + if user.group != "SimpleUsers": + nextlocation = reverse("modoboa.lib.webutils.topredirection") + else: + nextlocation = reverse("domains") + return HttpResponseRedirect(nextlocation) + error = _("Your username and password didn't match. Please try again.") + logger.warning("Failed connection attempt from '%(addr)s' as user '%(user)s'" \ + % {"addr": request.META["REMOTE_ADDR"], + "user": form.cleaned_data["username"]}) + + nextlocation = request.POST.get("next", None) + httpcode = 401 + else: + form = LoginForm() + nextlocation = request.GET.get("next", None) + httpcode = 200 + + return HttpResponse(_render_to_string(request, "registration/login.html", { + "form": form, "error": error, "next": nextlocation, + "annoucements": events.raiseQueryEvent("GetAnnouncement", "loginpage") + }), status=httpcode) + +dologin = never_cache(dologin) + + +def dologout(request): + events.raiseEvent("UserLogout", request) + logger = logging.getLogger("modoboa.auth") + logger.info(_("User '%s' logged out" % request.user.username)) + logout(request) + return HttpResponseRedirect(reverse(dologin)) diff --git a/modoboa/core/views/user.py b/modoboa/core/views/user.py new file mode 100644 index 000000000..3a9b077d0 --- /dev/null +++ b/modoboa/core/views/user.py @@ -0,0 +1,73 @@ +from django.shortcuts import render +from django.utils.translation import ugettext as _ +from django.contrib.auth.decorators import login_required, user_passes_test +from modoboa.lib import events, parameters +from modoboa.lib.webutils import ( + ajax_simple_response, _render_to_string +) +from modoboa.lib.cryptutils import encrypt +from modoboa.core.forms import ProfileForm + + +@login_required +def index(request, tplname="core/user_index.html"): + extrajs = events.raiseQueryEvent("ExtraUprefsJS", request.user) + return render(request, tplname, { + "selection": "user", + "extrajs": "".join(extrajs) + }) + + +@login_required +def profile(request, tplname='core/user_profile.html'): + update_password = True + if True in events.raiseQueryEvent("PasswordChange", request.user): + update_password = False + + if request.method == "POST": + form = ProfileForm( + update_password, request.POST, instance=request.user + ) + if form.is_valid(): + 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 + }) + + form = ProfileForm(update_password, instance=request.user) + return ajax_simple_response({ + "status": "ok", + "content": _render_to_string(request, tplname, { + "form": form + }) + }) + + +@login_required +def preferences(request): + if request.method == "POST": + for formdef in parameters.get_user_forms(request.user, request.POST)(): + form = formdef["form"] + if form.is_valid(): + form.save() + continue + return ajax_simple_response({ + "status": "ko", "prefix": form.app, "errors": form.errors + }) + + return ajax_simple_response({ + "status": "ok", "respmsg": _("Preferences saved") + }) + + return ajax_simple_response({ + "status": "ok", + "content": _render_to_string(request, "core/user_preferences.html", { + "forms": parameters.get_user_forms(request.user) + }) + }) diff --git a/modoboa/extensions/__init__.py b/modoboa/extensions/__init__.py index e8bddb244..e69de29bb 100644 --- a/modoboa/extensions/__init__.py +++ b/modoboa/extensions/__init__.py @@ -1,91 +0,0 @@ -# coding: utf-8 -import os -import sys -import re -from django.conf import settings -from django.conf.urls import include - - -class ModoExtension(object): - """Base extension class - - Each Modoboa extension must inherit from this class to be - considered as valid. - """ - name = None - version = None - description = "" - url = None - needs_media = False - always_active = False - - def infos(self): - return dict(name=self.name, label=self.label, version=self.version, - description=self.description, url=self.url) - - def init(self): - pass - - def destroy(self): - pass - - def load(self): - pass - - -class ExtensionsPool(object): - """The extensions manager""" - - def __init__(self): - self.extensions = dict() - - def register_extension(self, ext, show=True): - self.extensions[ext.name] = dict(cls=ext, show=show) - - def get_extension(self, name): - if not name in self.extensions: - return None - if not "instance" in self.extensions[name]: - self.extensions[name]["instance"] = self.extensions[name]["cls"]() - return self.extensions[name]["instance"] - - def get_extension_infos(self, name): - instance = self.get_extension(name) - return instance.infos() - - def load_all(self): - from modoboa.admin.models import Extension - - for ext in settings.MODOBOA_APPS: - __import__(ext) - result = [] - for extname in self.extensions.keys(): - extinstance = self.get_extension(extname) - if not extinstance.always_active: - try: - ext = Extension.objects.get(name=extname) - if not ext.enabled: - continue - 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): - result = [] - for extname, extdef in self.extensions.iteritems(): - if not extdef["show"]: - continue - infos = extdef["instance"].infos() - infos["id"] = extname - result += [infos] - return sorted(result, key=lambda i: i["name"]) - -exts_pool = ExtensionsPool() diff --git a/modoboa/lib/authbackends.py b/modoboa/lib/authbackends.py index 01752a81c..4edefd86d 100644 --- a/modoboa/lib/authbackends.py +++ b/modoboa/lib/authbackends.py @@ -1,10 +1,9 @@ from django.contrib.auth.backends import ModelBackend -from django.utils.translation import gettext as _ -from modoboa.admin.models import User -from modoboa.lib.exceptions import ModoboaException +from modoboa.core.models import User from modoboa.lib import parameters from modoboa.lib.emailutils import split_mailbox + class SimpleBackend(ModelBackend): def authenticate(self, username=None, password=None): @@ -23,10 +22,11 @@ class SimpleBackend(ModelBackend): return None try: - from django_auth_ldap.backend import LDAPBackend as orig_LDAPBackend, _LDAPUser + from django_auth_ldap.backend import ( + LDAPBackend as orig_LDAPBackend, _LDAPUser + ) from modoboa.admin.models import populate_callback - class LDAPBackend(orig_LDAPBackend): def get_or_create_user(self, username, ldap_user): diff --git a/modoboa/lib/cryptutils.py b/modoboa/lib/cryptutils.py new file mode 100644 index 000000000..8808cd240 --- /dev/null +++ b/modoboa/lib/cryptutils.py @@ -0,0 +1,41 @@ +# coding: utf-8 +from Crypto.Cipher import AES +import base64 +import random +import string +from modoboa.lib import parameters + + +def random_key(l=16): + """Generate a random key + + :param integer l: the key's length + :return: a string + """ + char_set = string.digits + string.letters + string.punctuation + return ''.join(random.sample(char_set * l, l)) + + +def encrypt(clear): + key = parameters.get_admin("SECRET_KEY", app="admin") + obj = AES.new(key, AES.MODE_ECB) + if type(clear) is unicode: + clear = clear.encode("utf-8") + if len(clear) % AES.block_size: + clear += " " * (AES.block_size - len(clear) % AES.block_size) + ciph = obj.encrypt(clear) + ciph = base64.b64encode(ciph) + return ciph + + +def decrypt(ciph): + obj = AES.new( + parameters.get_admin("SECRET_KEY", app="admin"), AES.MODE_ECB + ) + ciph = base64.b64decode(ciph) + clear = obj.decrypt(ciph) + return clear.rstrip(' ') + + +def get_password(request): + return decrypt(request.session["password"]) diff --git a/modoboa/lib/listing.py b/modoboa/lib/listing.py new file mode 100644 index 000000000..9d1b0a38e --- /dev/null +++ b/modoboa/lib/listing.py @@ -0,0 +1,30 @@ +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from modoboa.lib import parameters + + +def get_sort_order(qdict, default, allowed_values=None): + """Return a sort order from a querydict object + + :param QueryDict qdict: the object to analyse + :param string default: the default sort order if no one is found + :param list allowed_values: an optional list of allowed values + :return: a 2uple of strings + """ + sort_order = qdict.get("sort_order", default) + if sort_order.startswith("-"): + sort_dir = "-" + sort_order = sort_order[1:] + else: + sort_dir = "" + if allowed_values is not None and not sort_order in allowed_values: + return (default, "") + return (sort_order, sort_dir) + + +def get_listing_page(objects, pagenum): + paginator = Paginator(objects, int(parameters.get_admin("ITEMS_PER_PAGE", app="core"))) + try: + page = paginator.page(int(pagenum)) + except (EmptyPage, PageNotAnInteger, ValueError): + page = paginator.page(paginator.num_pages) + return page diff --git a/modoboa/lib/middleware.py b/modoboa/lib/middleware.py index eea67d1bf..bce9881fd 100644 --- a/modoboa/lib/middleware.py +++ b/modoboa/lib/middleware.py @@ -1,10 +1,11 @@ # coding: utf-8 import re from django.http import Http404, HttpResponseRedirect -from modoboa.admin.models import Extension +from modoboa.core.models import Extension from modoboa.lib.webutils import _render_error, ajax_response from modoboa.lib.exceptions import ModoboaException + class ExtControlMiddleware(object): def process_view(self, request, view, args, kwargs): m = re.match("modoboa\.extensions\.(\w+)", view.__module__) @@ -17,6 +18,7 @@ class ExtControlMiddleware(object): return None raise Http404 + class AjaxLoginRedirect(object): def process_response(self, request, response): if request.is_ajax(): @@ -24,12 +26,16 @@ class AjaxLoginRedirect(object): response.status_code = 278 return response + class CommonExceptionCatcher(object): def process_exception(self, request, exception): if not isinstance(exception, ModoboaException): return None if not request.is_ajax(): - return _render_error(request, user_context=dict(error=str(exception))) - - return ajax_response(request, status="ko", respmsg=unicode(exception), norefresh=True) + return _render_error( + request, user_context=dict(error=str(exception)) + ) + return ajax_response( + request, status="ko", respmsg=unicode(exception), norefresh=True + ) diff --git a/modoboa/lib/models.py b/modoboa/lib/models.py index 5bf2781de..f98bfdc34 100644 --- a/modoboa/lib/models.py +++ b/modoboa/lib/models.py @@ -25,10 +25,3 @@ class UserParameter(models.Model): @property def shortname(self): return self.name.split(".")[1].lower() - - -class Log(models.Model): - date_created = models.DateTimeField(auto_now_add=True) - message = models.CharField(max_length=255) - level = models.CharField(max_length=15) - logger = models.CharField(max_length=30) diff --git a/modoboa/lib/parameters.py b/modoboa/lib/parameters.py index 8e1671ea8..56d9a8590 100644 --- a/modoboa/lib/parameters.py +++ b/modoboa/lib/parameters.py @@ -285,7 +285,9 @@ def get_user(user, name, app=None, raise_error=True): def get_sorted_apps(level, first="admin"): - sorted_apps = [first] + sorted_apps = [] + if first in _params[level]: + sorted_apps.append(first) sorted_apps += sorted( [app for app in _params[level].keys() if app != first], key=lambda app: _params[level][app]["label"] diff --git a/modoboa/lib/templatetags/libextras.py b/modoboa/lib/templatetags/libextras.py index 2ed64725c..7c8f9cc81 100644 --- a/modoboa/lib/templatetags/libextras.py +++ b/modoboa/lib/templatetags/libextras.py @@ -194,7 +194,7 @@ class ConnectedUsers(template.Node): self.varname = varname def render(self, context): - from modoboa.admin.models import User + from modoboa.core.models import User sessions = Session.objects.filter(expire_date__gte=timezone.now()) uid_list = [] diff --git a/modoboa/lib/webutils.py b/modoboa/lib/webutils.py index 3e7ceec92..254af4938 100644 --- a/modoboa/lib/webutils.py +++ b/modoboa/lib/webutils.py @@ -149,16 +149,16 @@ def topredirection(request): :param request: a Request object """ from modoboa.lib import parameters - from modoboa.extensions import exts_pool + from modoboa.core.extensions import exts_pool - topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="admin") - if not topredir in ["admin", "userprefs"]: + topredir = parameters.get_admin("DEFAULT_TOP_REDIRECTION", app="core") + if not topredir in ["core"]: infos = exts_pool.get_extension_infos(topredir) path = infos["url"] if infos["url"] else infos["name"] else: - path = topredir + path = "" # topredir - if topredir in ["admin", "stats"] and \ + if topredir in ["core", "stats"] and \ request.user.belongs_to_group('SimpleUsers'): path = "userprefs" diff --git a/modoboa/templates/nlayout.html b/modoboa/templates/nlayout.html index 0ded1dbd3..a4a3a5a81 100644 --- a/modoboa/templates/nlayout.html +++ b/modoboa/templates/nlayout.html @@ -1,4 +1,4 @@ -{% load i18n admin_extras uprefs_extras libextras %}{% load url from future %} +{% load i18n core_tags libextras %}{% load url from future %} @@ -38,8 +38,8 @@ diff --git a/modoboa/templates/registration/login.html b/modoboa/templates/registration/login.html index 12dd1377b..ecedd40ca 100644 --- a/modoboa/templates/registration/login.html +++ b/modoboa/templates/registration/login.html @@ -23,7 +23,7 @@ -
{% csrf_token %} {% render_field form.username %} diff --git a/modoboa/urls.py b/modoboa/urls.py index 36c376ea5..8f65d9399 100644 --- a/modoboa/urls.py +++ b/modoboa/urls.py @@ -1,15 +1,31 @@ from django.conf.urls import patterns, include, url from django.conf import settings -from modoboa.extensions import * +from modoboa.core.extensions import exts_pool from modoboa.lib import parameters +from modoboa.core import load_settings +load_settings() urlpatterns = patterns('', (r'^$', 'modoboa.lib.webutils.topredirection'), - (r'^accounts/login/$', 'modoboa.auth.views.dologin'), - (r'^accounts/logout/$', 'modoboa.auth.views.dologout'), (r'^jsi18n/$', 'django.views.i18n.javascript_catalog', {'packages': ('modoboa', ), }), + + (r'^accounts/login/$', 'modoboa.core.views.auth.dologin'), + (r'^accounts/logout/$', 'modoboa.core.views.auth.dologout'), + + (r'^admin/$', 'modoboa.core.views.admin.viewsettings'), + (r'^admin/parameters/$', 'modoboa.core.views.admin.viewparameters'), + (r'^admin/parameters/save/$', 'modoboa.core.views.admin.saveparameters'), + (r'^admin/extensions/$', 'modoboa.core.views.admin.viewextensions'), + (r'^admin/extensions/save/$', 'modoboa.core.views.admin.saveextensions'), + (r'^admin/info/$', 'modoboa.core.views.admin.information'), + (r'^admin/logs/$', 'modoboa.core.views.admin.logs'), + + (r'^user/$', 'modoboa.core.views.user.index'), + (r'^user/preferences/$', 'modoboa.core.views.user.preferences'), + (r'^user/profile/$', 'modoboa.core.views.user.profile'), + *exts_pool.load_all() ) From 115553236509754ceea3435327b8cd1c07260d06 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 16 Aug 2013 16:34:15 +0200 Subject: [PATCH 02/97] wip: Big refacto The admin application is now a true extension. --- modoboa/admin/__init__.py | 32 - modoboa/admin/app_settings.py | 291 ---- modoboa/admin/models.py | 1322 ----------------- modoboa/admin/views.py | 889 ----------- modoboa/core/__init__.py | 13 +- modoboa/core/app_settings.py | 2 +- modoboa/core/extensions.py | 10 +- modoboa/core/models.py | 1 + modoboa/core/templatetags/core_tags.py | 30 + modoboa/extensions/admin/__init__.py | 21 + modoboa/extensions/admin/app_settings.py | 48 + modoboa/{ => extensions}/admin/exceptions.py | 0 .../{ => extensions}/admin/fixtures/demo.json | 0 .../admin/fixtures/initial_data.json | 0 .../admin/fixtures/initial_users.json | 0 .../admin/fixtures/test_content.json | 0 modoboa/{ => extensions}/admin/forms.py | 9 +- modoboa/{ => extensions}/admin/lib.py | 0 .../admin/migrations/0001_initial.py | 0 .../0002_auto__del_field_alias_mbox.py | 0 .../migrations/0003_auto__add_extension.py | 0 .../admin/migrations/0004_add_permissions.py | 0 .../0005_increase_columns_length.py | 0 .../migrations/0006_auto__add_domainalias.py | 0 ...ess__chg_field_alias_address__del_field.py | 0 .../migrations/0008_update_mailbox_path.py | 0 .../0009_auto__add_field_alias_extmboxes.py | 0 .../migrations/0010_fill_alias_domain.py | 0 ...d_domain_dates__add_field_domainalias_d.py | 0 .../migrations/0012_fill_object_dates.py | 0 .../migrations/0013_update_mailbox_path.py | 0 .../0014_auto__add_unique_domain_name.py | 0 .../admin/migrations/0015_move_passwords.py | 0 ...que_objectaccess_user_content_type_obje.py | 0 .../admin/migrations/0017_set_permissions.py | 0 ...8_auto__add_unique_alias_domain_address.py | 0 .../migrations/0019_set_default_group.py | 0 .../migrations/0020_rename_amavis_ext.py | 0 .../0021_default_superadmin_ownership.py | 0 ...l_field_mailbox_gid__del_field_mailbox_.py | 0 .../admin/migrations/0023_auto__add_quota.py | 0 .../admin/migrations/0024_create_quotas.py | 0 .../0025_auto__add_field_quota_mbox.py | 0 .../admin/migrations/0026_set_quota_mbox.py | 0 ...uto__add_field_mailbox_use_domain_quota.py | 0 .../0028_auto__chg_field_mailbox_quota.py | 0 ...ilbox_user__chg_field_objectaccess_user.py | 0 .../migrations/0030_update_permissions.py | 0 .../0031_auto__add_aliases_to_alias.py | 0 .../0032_auto__chg_field_quota_bytes.py | 0 ...ilbox_address__chg_field_quota_username.py | 0 .../0034_auto__add_field_user_is_local.py | 0 .../admin/migrations/0035_set_local_flag.py | 0 .../admin/migrations/__init__.py | 0 modoboa/extensions/admin/models/__init__.py | 8 + modoboa/extensions/admin/models/alias.py | 195 +++ modoboa/extensions/admin/models/base.py | 49 + modoboa/extensions/admin/models/domain.py | 133 ++ .../extensions/admin/models/domain_alias.py | 78 + modoboa/extensions/admin/models/mailbox.py | 213 +++ .../admin/static/admin/css/admin.css | 0 .../admin/static/admin/css/settings.css | 0 .../admin/static/admin/js/admin.js | 0 modoboa/{ => extensions}/admin/tables.py | 17 +- .../admin/templates/admin/dlistform.html | 0 .../templates/admin/domain_general_form.html | 0 .../templates/admin/domain_options_form.html | 0 .../admin/templates/admin/domains.html | 2 +- .../admin/templates/admin/editdomainform.html | 0 .../admin/templates/admin/extensions.html | 0 .../admin/templates/admin/header.html | 0 .../admin/templates/admin/identities.html | 8 +- .../templates/admin/identities_table.html | 0 .../admin/templates/admin/import_done.html | 0 .../admin/templates/admin/importform.html | 0 .../admin/templates/admin/information.html | 0 .../admin/templates/admin/listing.html | 2 +- .../admin/templates/admin/logs.html | 0 .../admin/templates/admin/mailboxes_raw.html | 0 .../admin/templates/admin/mailform.html | 0 .../admin/templates/admin/newdomainform.html | 0 .../admin/templates/admin/parameters.html | 0 .../admin/templates/admin/permsform.html | 0 .../admin/templates/admin/quotas.html | 0 .../admin/templates/admin/searchform.html | 0 .../templates/admin/settings_header.html | 0 .../admin/templatetags}/__init__.py | 0 .../admin/templatetags/admin_tags.py} | 209 +-- modoboa/{ => extensions}/admin/tests.py | 0 modoboa/{ => extensions}/admin/urls.py | 35 +- .../admin/views}/__init__.py | 0 modoboa/extensions/admin/views/alias.py | 163 ++ modoboa/extensions/admin/views/domain.py | 184 +++ modoboa/extensions/admin/views/export.py | 83 ++ modoboa/extensions/admin/views/identity.py | 244 +++ modoboa/extensions/admin/views/import.py | 165 ++ modoboa/lib/middleware.py | 11 +- modoboa/lib/permissions.py | 2 +- modoboa/lib/webutils.py | 43 +- modoboa/urls.py | 14 +- 100 files changed, 1795 insertions(+), 2731 deletions(-) delete mode 100644 modoboa/admin/__init__.py delete mode 100644 modoboa/admin/app_settings.py delete mode 100644 modoboa/admin/models.py delete mode 100644 modoboa/admin/views.py create mode 100644 modoboa/extensions/admin/__init__.py create mode 100644 modoboa/extensions/admin/app_settings.py rename modoboa/{ => extensions}/admin/exceptions.py (100%) rename modoboa/{ => extensions}/admin/fixtures/demo.json (100%) rename modoboa/{ => extensions}/admin/fixtures/initial_data.json (100%) rename modoboa/{ => extensions}/admin/fixtures/initial_users.json (100%) rename modoboa/{ => extensions}/admin/fixtures/test_content.json (100%) rename modoboa/{ => extensions}/admin/forms.py (99%) rename modoboa/{ => extensions}/admin/lib.py (100%) rename modoboa/{ => extensions}/admin/migrations/0001_initial.py (100%) rename modoboa/{ => extensions}/admin/migrations/0002_auto__del_field_alias_mbox.py (100%) rename modoboa/{ => extensions}/admin/migrations/0003_auto__add_extension.py (100%) rename modoboa/{ => extensions}/admin/migrations/0004_add_permissions.py (100%) rename modoboa/{ => extensions}/admin/migrations/0005_increase_columns_length.py (100%) rename modoboa/{ => extensions}/admin/migrations/0006_auto__add_domainalias.py (100%) rename modoboa/{ => extensions}/admin/migrations/0007_auto__del_field_alias_full_address__chg_field_alias_address__del_field.py (100%) rename modoboa/{ => extensions}/admin/migrations/0008_update_mailbox_path.py (100%) rename modoboa/{ => extensions}/admin/migrations/0009_auto__add_field_alias_extmboxes.py (100%) rename modoboa/{ => extensions}/admin/migrations/0010_fill_alias_domain.py (100%) rename modoboa/{ => extensions}/admin/migrations/0011_auto__add_objectdates__add_field_domain_dates__add_field_domainalias_d.py (100%) rename modoboa/{ => extensions}/admin/migrations/0012_fill_object_dates.py (100%) rename modoboa/{ => extensions}/admin/migrations/0013_update_mailbox_path.py (100%) rename modoboa/{ => extensions}/admin/migrations/0014_auto__add_unique_domain_name.py (100%) rename modoboa/{ => extensions}/admin/migrations/0015_move_passwords.py (100%) rename modoboa/{ => extensions}/admin/migrations/0016_auto__add_objectaccess__add_unique_objectaccess_user_content_type_obje.py (100%) rename modoboa/{ => extensions}/admin/migrations/0017_set_permissions.py (100%) rename modoboa/{ => extensions}/admin/migrations/0018_auto__add_unique_alias_domain_address.py (100%) rename modoboa/{ => extensions}/admin/migrations/0019_set_default_group.py (100%) rename modoboa/{ => extensions}/admin/migrations/0020_rename_amavis_ext.py (100%) rename modoboa/{ => extensions}/admin/migrations/0021_default_superadmin_ownership.py (100%) rename modoboa/{ => extensions}/admin/migrations/0022_auto__del_field_mailbox_uid__del_field_mailbox_gid__del_field_mailbox_.py (100%) rename modoboa/{ => extensions}/admin/migrations/0023_auto__add_quota.py (100%) rename modoboa/{ => extensions}/admin/migrations/0024_create_quotas.py (100%) rename modoboa/{ => extensions}/admin/migrations/0025_auto__add_field_quota_mbox.py (100%) rename modoboa/{ => extensions}/admin/migrations/0026_set_quota_mbox.py (100%) rename modoboa/{ => extensions}/admin/migrations/0027_auto__add_field_mailbox_use_domain_quota.py (100%) rename modoboa/{ => extensions}/admin/migrations/0028_auto__chg_field_mailbox_quota.py (100%) rename modoboa/{ => extensions}/admin/migrations/0029_auto__add_user__chg_field_mailbox_user__chg_field_objectaccess_user.py (100%) rename modoboa/{ => extensions}/admin/migrations/0030_update_permissions.py (100%) rename modoboa/{ => extensions}/admin/migrations/0031_auto__add_aliases_to_alias.py (100%) rename modoboa/{ => extensions}/admin/migrations/0032_auto__chg_field_quota_bytes.py (100%) rename modoboa/{ => extensions}/admin/migrations/0033_auto__chg_field_mailbox_address__chg_field_quota_username.py (100%) rename modoboa/{ => extensions}/admin/migrations/0034_auto__add_field_user_is_local.py (100%) rename modoboa/{ => extensions}/admin/migrations/0035_set_local_flag.py (100%) rename modoboa/{ => extensions}/admin/migrations/__init__.py (100%) create mode 100644 modoboa/extensions/admin/models/__init__.py create mode 100644 modoboa/extensions/admin/models/alias.py create mode 100644 modoboa/extensions/admin/models/base.py create mode 100644 modoboa/extensions/admin/models/domain.py create mode 100644 modoboa/extensions/admin/models/domain_alias.py create mode 100644 modoboa/extensions/admin/models/mailbox.py rename modoboa/{ => extensions}/admin/static/admin/css/admin.css (100%) rename modoboa/{ => extensions}/admin/static/admin/css/settings.css (100%) rename modoboa/{ => extensions}/admin/static/admin/js/admin.js (100%) rename modoboa/{ => extensions}/admin/tables.py (84%) rename modoboa/{ => extensions}/admin/templates/admin/dlistform.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/domain_general_form.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/domain_options_form.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/domains.html (79%) rename modoboa/{ => extensions}/admin/templates/admin/editdomainform.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/extensions.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/header.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/identities.html (60%) rename modoboa/{ => extensions}/admin/templates/admin/identities_table.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/import_done.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/importform.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/information.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/listing.html (92%) rename modoboa/{ => extensions}/admin/templates/admin/logs.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/mailboxes_raw.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/mailform.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/newdomainform.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/parameters.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/permsform.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/quotas.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/searchform.html (100%) rename modoboa/{ => extensions}/admin/templates/admin/settings_header.html (100%) rename modoboa/{admin/models => extensions/admin/templatetags}/__init__.py (100%) rename modoboa/{admin/templatetags/admin_extras.py => extensions/admin/templatetags/admin_tags.py} (61%) rename modoboa/{ => extensions}/admin/tests.py (100%) rename modoboa/{ => extensions}/admin/urls.py (76%) rename modoboa/{admin/templatetags => extensions/admin/views}/__init__.py (100%) create mode 100644 modoboa/extensions/admin/views/alias.py create mode 100644 modoboa/extensions/admin/views/domain.py create mode 100644 modoboa/extensions/admin/views/export.py create mode 100644 modoboa/extensions/admin/views/identity.py create mode 100644 modoboa/extensions/admin/views/import.py diff --git a/modoboa/admin/__init__.py b/modoboa/admin/__init__.py deleted file mode 100644 index 5ebe1de28..000000000 --- a/modoboa/admin/__init__.py +++ /dev/null @@ -1,32 +0,0 @@ -# coding: utf-8 -from django.utils.translation import ugettext as _, ugettext_lazy -from modoboa.extensions import ModoExtension, exts_pool -from modoboa.lib import parameters, events - - -class AdminConsole(ModoExtension): - name = "admin" - label = ugettext_lazy("Administration console") - version = "1.0" - description = ugettext_lazy("Web based console to manage domains, accounts and aliases") - always_active = True - - def load(self): - from app_settings import GeneralParametersForm - parameters.register(GeneralParametersForm, ugettext_lazy("General")) - - def destroy(self): - parameters.unregister() - -exts_pool.register_extension(AdminConsole, show=False) - - -@events.observe("ExtDisabled") -def unset_default_topredirection(extension): - """ - Simple callback to change the default redirection if the - corresponding extension is being disabled. - """ - topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION") - if topredirection == extension.name: - parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs") diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py deleted file mode 100644 index 1f02db899..000000000 --- a/modoboa/admin/app_settings.py +++ /dev/null @@ -1,291 +0,0 @@ -from django import forms -from django.utils.translation import ugettext as _, ugettext_lazy -from modoboa.lib.formutils import YesNoField, SeparatorField, InlineRadioSelect -from modoboa.lib.sysutils import exec_cmd -from modoboa.lib import parameters -from modoboa.auth.lib import random_key - - -def enabled_applications(): - """Return the list of currently enabled extensions - - We check if the table exists before trying to fetch activated - extensions because the admin module is always imported by Django, - even before the database exists (example: the first ``syncdb``). - - :return: a list - """ - from modoboa.admin.models import Extension - from modoboa.lib.dbutils import db_table_exists - - result = [("userprefs", "userprefs")] - if db_table_exists("admin_extension"): - exts = Extension.objects.filter(enabled=True) - result += [(ext.name, ext.name) for ext in exts] - return sorted(result, key=lambda e: e[0]) - - -class GeneralParametersForm(parameters.AdminParametersForm): - app = "admin" - - sep1 = SeparatorField(label=ugettext_lazy("Authentication")) - - authentication_type = forms.ChoiceField( - label=ugettext_lazy("Authentication type"), - choices=[('local', ugettext_lazy("Local")), - ('ldap', "LDAP")], - initial="local", - help_text=ugettext_lazy("The backend used for authentication"), - widget=InlineRadioSelect - ) - - password_scheme = forms.ChoiceField( - label=ugettext_lazy("Default password scheme"), - choices=[("crypt", "crypt"), - ("md5", "md5"), - ("md5crypt", "md5crypt"), - ("sha256", "sha256"), - ("plain", "plain")], - initial="md5crypt", - help_text=ugettext_lazy("Scheme used to crypt mailbox passwords") - ) - - secret_key = forms.CharField( - label=ugettext_lazy("Secret key"), - initial=random_key(), - help_text=ugettext_lazy("Key used to encrypt/decrypt passwords") - ) - - # LDAP specific settings - ldap_sep = SeparatorField(label=ugettext_lazy("LDAP settings")) - - ldap_server_address = forms.CharField( - label=ugettext_lazy("Server address"), - initial="localhost", - help_text=ugettext_lazy("The IP address of the DNS name of the LDAP server") - ) - - ldap_server_port = forms.IntegerField( - label=ugettext_lazy("Server port"), - initial=389, - help_text=ugettext_lazy("The TCP port number used by the LDAP server") - ) - - ldap_secured = YesNoField( - label=ugettext_lazy("Use a secured connection"), - initial="no", - help_text=ugettext_lazy("Use an SSL/TLS connection to access the LDAP server") - ) - - ldap_auth_method = forms.ChoiceField( - label=ugettext_lazy("Authentication method"), - choices=[('searchbind', ugettext_lazy("Search and bind")), - ('directbind', ugettext_lazy("Direct bind"))], - initial='searchbind', - help_text=ugettext_lazy("Choose the authentication method to use"), - widget=InlineRadioSelect - ) - - ldap_bind_dn = forms.CharField( - label=ugettext_lazy("Bind DN"), - initial='', - help_text=ugettext_lazy("The distinguished name to use when binding to the LDAP server. Leave empty for an anonymous bind"), - required=False - ) - - ldap_bind_password = forms.CharField( - 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(render_value=True), - required=False - ) - - ldap_search_base = forms.CharField( - label=ugettext_lazy("Search base"), - initial="", - help_text=ugettext_lazy("The distinguished name of the search base"), - required=False - ) - - ldap_search_filter = forms.CharField( - label=ugettext_lazy("Search filter"), - initial="(mail=%(user)s)", - help_text=ugettext_lazy("An optional filter string (e.g. '(objectClass=person)'). In order to be valid, it must be enclosed in parentheses."), - required=False - ) - - ldap_user_dn_template = forms.CharField( - label=ugettext_lazy("User DN template"), - initial="", - help_text=ugettext_lazy("The template used to construct a user's DN. It should contain one placeholder (ie. %(user)s)"), - required=False - ) - - ldap_password_attribute = forms.CharField( - label=ugettext_lazy("Password attribute"), - initial="userPassword", - help_text=ugettext_lazy("The attribute used to store user passwords") - ) - - ldap_is_active_directory = YesNoField( - label=ugettext_lazy("Active Directory"), - initial="no", - help_text=ugettext_lazy("Tell if the LDAP server is an Active Directory one") - ) - - sep2 = SeparatorField(label=ugettext_lazy("Mailboxes")) - - handle_mailboxes = YesNoField( - label=ugettext_lazy("Handle mailboxes on filesystem"), - initial="no", - help_text=ugettext_lazy("Rename or remove mailboxes on the filesystem when they get renamed or removed within Modoboa") - ) - - mailboxes_owner = forms.CharField( - label=ugettext_lazy("Mailboxes ower"), - initial="vmail", - help_text=ugettext_lazy("The UNIX account who owns mailboxes on the filesystem") - ) - - auto_account_removal = YesNoField( - label=ugettext_lazy("Automatic account removal"), - initial="no", - help_text=ugettext_lazy("When a mailbox is removed, also remove the associated account"), - ) - - sep3 = SeparatorField(label=ugettext_lazy("Miscellaneous")) - - log_maximum_age = forms.IntegerField( - label=ugettext_lazy("Maximum log record age"), - initial=365, - help_text=ugettext_lazy("The maximum age in days of a log record") - ) - - items_per_page = forms.IntegerField( - label=ugettext_lazy("Items per page"), - initial=30, - help_text=ugettext_lazy("Number of displayed items per page") - ) - - default_top_redirection = forms.ChoiceField( - label=ugettext_lazy("Default top redirection"), - choices=[], - initial="userprefs", - help_text=ugettext_lazy("The default redirection used when no application is specified") - ) - - # Visibility rules - visibility_rules = { - "mailboxes_owner": "handle_mailboxes=yes", - "ldap_sep": "authentication_type=ldap", - "ldap_server_address": "authentication_type=ldap", - "ldap_server_port": "authentication_type=ldap", - "ldap_secured": "authentication_type=ldap", - "ldap_auth_method": "authentication_type=ldap", - "ldap_bind_dn": "ldap_auth_method=searchbind", - "ldap_bind_password": "ldap_auth_method=searchbind", - "ldap_search_base": "ldap_auth_method=searchbind", - "ldap_search_filter": "ldap_auth_method=searchbind", - "ldap_user_dn_template": "ldap_auth_method=directbind", - "ldap_password_attribute": "authentication_type=ldap", - "ldap_is_active_directory": "authentication_type=ldap" - } - - def __init__(self, *args, **kwargs): - super(GeneralParametersForm, self).__init__(*args, **kwargs) - self.fields["default_top_redirection"].choices = enabled_applications() - hide_fields = False - try: - code, version = exec_cmd("dovecot --version") - except OSError, e: - hide_fields = True - else: - if code or not version.strip().startswith("2"): - hide_fields = True - if hide_fields: - del self.fields["handle_mailboxes"] - del self.fields["mailboxes_owner"] - - 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: - test = tpl % {"user": "toto"} - except ValueError: - raise forms.ValidationError(_("Invalid syntax")) - return tpl - - def clean(self): - """Custom validation method - - Depending on 'ldap_auth_method' value, we check for different - required parameters. - """ - super(GeneralParametersForm, self).clean() - if len(self._errors): - raise forms.ValidationError(self._errors) - cleaned_data = self.cleaned_data - if cleaned_data["authentication_type"] != "ldap": - return cleaned_data - - if cleaned_data["ldap_auth_method"] == "searchbind": - required_fields = ["ldap_search_base", "ldap_search_filter"] - else: - required_fields = ["ldap_user_dn_template"] - - for f in required_fields: - if not f in cleaned_data or cleaned_data[f] == u'': - self._errors[f] = self.error_class([_("This field is required")]) - - return cleaned_data - - def to_django_settings(self): - """Apply LDAP related parameters to Django settings - - Doing so, we can use the django_auth_ldap module. - """ - from django.conf import settings - try: - import ldap - from django_auth_ldap.config import LDAPSearch - ldap_available = True - except ImportError: - ldap_available = False - - values = self.get_current_values() - if not ldap_available or values["authentication_type"] != "ldap": - return - if not hasattr(settings, "AUTH_LDAP_USER_ATTR_MAP"): - setattr(settings, "AUTH_LDAP_USER_ATTR_MAP", { - "first_name": "givenName", - "email": "mail", - "last_name": "sn" - }) - ldap_uri = 'ldaps://' if values["ldap_secured"] == "yes" else "ldap://" - ldap_uri += "%s:%s" % (values["ldap_server_address"], values["ldap_server_port"]) - setattr(settings, "AUTH_LDAP_SERVER_URI", ldap_uri) - if values["ldap_auth_method"] == "searchbind": - setattr(settings, "AUTH_LDAP_BIND_DN", values["ldap_bind_dn"]) - setattr(settings, "AUTH_LDAP_BIND_PASSWORD", values["ldap_bind_password"]) - search = LDAPSearch( - values["ldap_search_base"], ldap.SCOPE_SUBTREE, - values["ldap_search_filter"] - ) - setattr(settings, "AUTH_LDAP_USER_SEARCH", search) - else: - setattr(settings, "AUTH_LDAP_USER_DN_TEMPLATE", values["ldap_user_dn_template"]) - - if values["ldap_is_active_directory"] == "yes": - if not hasattr(settings, "AUTH_LDAP_GLOBAL_OPTIONS"): - setattr(settings, "AUTH_LDAP_GLOBAL_OPTIONS", { - ldap.OPT_REFERRALS: False - }) - else: - settings.AUTH_LDAP_GLOBAL_OPTIONS[ldap.OPT_REFERRALS] = False diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py deleted file mode 100644 index 6a2134c29..000000000 --- a/modoboa/admin/models.py +++ /dev/null @@ -1,1322 +0,0 @@ -# coding: utf-8 -from django.db import models -from django.db.models import Q -from django.db.models.signals import pre_delete -from django.dispatch import receiver -from django.contrib.contenttypes.models import ContentType -from django.contrib.contenttypes import generic -from django.contrib.auth.models import ( - UserManager, Group, AbstractBaseUser, PermissionsMixin -) -from django.utils.translation import ugettext as _, ugettext_lazy -from django.utils.crypto import constant_time_compare -from django.utils import timezone -from django.conf import settings -from modoboa.lib import md5crypt, parameters, events -from modoboa.lib.exceptions import PermDeniedException -from modoboa.lib.sysutils import exec_cmd -from modoboa.lib.emailutils import split_mailbox -from modoboa.extensions import exts_pool -from exceptions import AdminError -import reversion -import os -import sys -import pwd -import re -import crypt, hashlib, string, base64 -from random import Random - -try: - from modoboa.lib.ldaputils import * - ldap_available = True -except ImportError: - ldap_available = False - - -class User(AbstractBaseUser, PermissionsMixin): - """Custom User model. - - It overloads the way passwords are stored into the database. The - main reason to change this mechanism is to ensure the - compatibility with the way Dovecot stores passwords. - - It also adds new attributes and methods. - """ - username = models.CharField(max_length=254, unique=True) - first_name = models.CharField(max_length=30, blank=True) - last_name = models.CharField(max_length=30, blank=True) - email = models.EmailField(max_length=254, blank=True) - is_staff = models.BooleanField(default=False) - is_active = models.BooleanField(default=True) - date_joined = models.DateTimeField(default=timezone.now) - is_local = models.BooleanField(default=True) - - objects = UserManager() - - USERNAME_FIELD = 'username' - REQUIRED_FIELDS = ['email'] - - class Meta: - ordering = ["username"] - - password_expr = re.compile(r'(\{(\w+)\}|(\$1\$))(.+)') - - def delete(self, fromuser, keep_mb_dir, *args, **kwargs): - """Custom delete method - - To check permissions properly, we need to make a distinction - between 2 cases: - - * If the user owns a mailbox, the check is made on that object - (useful for domain admins) - - * Otherwise, the check is made on the user - """ - from modoboa.lib.permissions import \ - get_object_owner, grant_access_to_object, ungrant_access_to_object - - if fromuser == self: - raise AdminError(_("You can't delete your own account")) - - if not fromuser.can_access(self): - raise PermDeniedException - - if self.has_mailbox: - mb = self.mailbox_set.all()[0] - if not fromuser.can_access(mb): - raise PermDeniedException - mb.delete(keepdir=keep_mb_dir) - - owner = get_object_owner(self) - for ooentry in self.objectaccess_set.filter(is_owner=True): - if ooentry.content_object is not None: - grant_access_to_object(owner, ooentry.content_object, True) - - events.raiseEvent("AccountDeleted", self) - ungrant_access_to_object(self) - super(User, self).delete(*args, **kwargs) - - @staticmethod - def get_content_type(): - """An uggly hack to retrieve the appropriate content_type! - - The explanation is available here: - https://code.djangoproject.com/ticket/11154 - - Quickly, the content_types framework does not retrieve the - appropriate content type for proxy models, it retrieves the - one of the first parent that is not a proxy. - """ - if not hasattr(User, "ct"): - User.ct = ContentType.objects.get(app_label="admin", model="user") - return User.ct - - def _crypt_password(self, raw_value): - scheme = parameters.get_admin("PASSWORD_SCHEME") - if type(raw_value) is unicode: - raw_value = raw_value.encode("utf-8") - if scheme == "crypt": - salt = "".join(Random().sample(string.letters + string.digits, 2)) - result = crypt.crypt(raw_value, salt) - prefix = "{CRYPT}" - elif scheme == "md5": - obj = hashlib.md5(raw_value) - result = obj.hexdigest() - prefix = "{MD5}" - # The md5crypt scheme is the only supported method that has both: - # (a) a salt ("crypt" has this too), - # (b) supports passwords lengths of more than 8 characters (all except - # "crypt"). - elif scheme == "md5crypt": - # The salt may vary from 12 to 48 bits. (Using all six bytes here - # with a subset of characters means we get only 35 random bits.) - salt = "".join(Random().sample(string.letters + string.digits, 6)) - result = md5crypt(raw_value, salt) - prefix = "" # the result already has $1$ prepended to it to signify what this is - elif scheme == "sha256": - obj = hashlib.sha256(raw_value) - result = base64.b64encode(obj.digest()) - prefix = "{SHA256}" - else: - scheme = "plain" - result = raw_value - prefix = "{PLAIN}" - return "%s%s" % (prefix, result) - - def set_password(self, raw_value, curvalue=None): - """Password update - - Update the current mailbox's password with the given clear - value. This value is encrypted according to the defined method - before it is saved. - - :param raw_value: the new password's value - :param curvalue: the current password (for LDAP authentication) - """ - if self.is_local: - self.password = self._crypt_password(raw_value) - else: - if not ldap_available: - raise AdminError(_("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))) - events.raiseEvent("PasswordUpdated", self, raw_value, self.pk is None) - - def check_password(self, raw_value): - m = self.password_expr.match(self.password) - if m is None: - return False - if type(raw_value) is unicode: - raw_value = raw_value.encode("utf-8") - scheme = (m.group(2) or m.group(3)).lower() - val2 = m.group(4) - if scheme == u"crypt": - val1 = crypt.crypt(raw_value, val2) - elif scheme == u"md5": - val1 = hashlib.md5(raw_value).hexdigest() - elif scheme == u"sha256": - val1 = base64.b64encode(hashlib.sha256(raw_value).digest()) - elif scheme == u"$1$": # md5crypt - salt, hashed = val2.split('$') - val1 = md5crypt(raw_value, str(salt)) - val2 = self.password # re-add scheme for comparison below - else: - val1 = raw_value - return constant_time_compare(val1, val2) - - @property - def tags(self): - return [{"name": "account", "label": _("account"), "type": "idt"}, - {"name": self.group, "label": self.group, "type": "grp", "color": "info"}] - - @property - def has_mailbox(self): - return self.mailbox_set.count() != 0 - - @property - def fullname(self): - if self.first_name != u"": - return u"%s %s" % (self.first_name, self.last_name) - return self.username - - @property - def identity(self): - return self.username - - @property - def name_or_rcpt(self): - if self.first_name != "": - return "%s %s" % (self.first_name, self.last_name) - return "----" - - @property - def group(self): - if self.is_superuser: - return "SuperAdmins" - try: - return self.groups.all()[0].name - except IndexError: - return "SimpleUsers" - - @property - def enabled(self): - return self.is_active - - @property - def encoded_address(self): - from email.header import Header - if self.first_name != "" or self.last_name != "": - return "%s <%s>" % \ - (Header(self.fullname, 'utf8').encode(), self.email) - return self.email - - def belongs_to_group(self, name): - """Simple shortcut to check if this user is a member of a - specific group. - - :param name: the group's name - :return: a boolean - """ - try: - self.groups.get(name=name) - except Group.DoesNotExist: - return False - return True - - def get_identities(self, querydict=None): - """Return all identities owned by this user - - :param querydict: a querydict object - :return: a queryset - """ - from modoboa.lib.permissions import get_content_type - from itertools import chain - - if querydict: - squery = querydict.get("searchquery", None) - idtfilter = querydict.getlist("idtfilter", None) - grpfilter = querydict.getlist("grpfilter", None) - else: - squery = None - idtfilter = None - grpfilter = None - - accounts = [] - if not idtfilter or "account" in idtfilter: - userct = get_content_type(self) - ids = self.objectaccess_set.filter(content_type=userct) \ - .values_list('object_id', flat=True) - q = Q(pk__in=ids) - if squery: - q &= Q(username__icontains=squery) | Q(email__icontains=squery) - if grpfilter and len(grpfilter): - if "SuperAdmins" in grpfilter: - q &= Q(is_superuser=True) - grpfilter.remove("SuperAdmins") - if len(grpfilter): - q |= Q(groups__name__in=grpfilter) - else: - q &= Q(groups__name__in=grpfilter) - accounts = User.objects.select_related().filter(q) - - aliases = [] - if not idtfilter or ("alias" in idtfilter - or "forward" in idtfilter - or "dlist" in idtfilter): - alct = get_content_type(Alias) - ids = self.objectaccess_set.filter(content_type=alct) \ - .values_list('object_id', flat=True) - q = Q(pk__in=ids) - if squery: - if '@' in squery: - local_part, domname = split_mailbox(squery) - if local_part: - q &= Q(address__icontains=local_part) - if domname: - q &= Q(domain__name__icontains=domname) - else: - q &= Q(address__icontains=squery) | Q(domain__name__icontains=squery) - aliases = Alias.objects.select_related().filter(q) - if idtfilter: - aliases = [al for al in aliases if al.type in idtfilter] - - return chain(accounts, aliases) - - def get_domains(self): - """Return the domains belonging to this user - - The result is a ``QuerySet`` object, so this function can be used - to fill ``ModelChoiceField`` objects. - """ - if self.is_superuser: - return Domain.objects.all() - return Domain.objects.filter(owners__user=self) - - def get_domaliases(self): - """Return the domain aliases that belong to this user - - The result will contain the domain aliases defined for each domain - that user can see. - - :return: a list of ``DomainAlias`` objects - """ - if self.is_superuser: - return DomainAlias.objects.all() - domains = self.get_domains() - domaliases = [] - for dom in domains: - domaliases += dom.domainalias_set.all() - return domaliases - - def get_mailboxes(self, squery=None): - """Return the mailboxes that belong to this user - - The result will contain the mailboxes defined for each domain that - user can see. - - :param string squery: a search query - :return: a list of ``Mailbox`` objects - """ - from modoboa.lib.permissions import get_content_type - - qf = None - if squery is not None: - if '@' in squery: - parts = squery.split('@') - addrfilter = '@'.join(parts[:-1]) - domfilter = parts[-1] - qf = Q(address__contains=addrfilter) & Q(domain__name__contains=domfilter) - else: - qf = Q(address__contains=squery) | Q(domain__name__contains=squery) - ids = self.objectaccess_set.filter(content_type=get_content_type(Mailbox)) \ - .values_list('object_id', flat=True) - if qf is not None: - qf = Q(pk__in=ids) & qf - else: - qf = Q(pk__in=ids) - return Mailbox.objects.filter(qf) - - def get_aliases(self): - from modoboa.lib.permissions import get_content_type - ids = self.objectaccess_set.filter(content_type=get_content_type(Alias)) \ - .values_list('object_id', flat=True) - return Alias.objects.filter(pk__in=ids) - - def get_mbaliases(self): - """Return the mailbox aliases that belong to this user - - The result will contain the mailbox aliases defined for each - domain that user can see. - - :return: a list of ``Alias`` objects - """ - if self.is_superuser: - return Alias.objects.all() - domains = self.get_domains() - mbaliases = [] - for dom in domains: - mbaliases += dom.alias_set.all() - return mbaliases - - def is_owner(self, obj): - """Tell is the user is the unique owner of this object - - :param obj: an object inheriting from ``models.Model`` - :return: a boolean - """ - from modoboa.lib.permissions import get_content_type - - ct = get_content_type(obj) - try: - ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) - except ObjectAccess.DoesNotExist: - return False - return ooentry.is_owner - - def can_access(self, obj): - """Check if the user can access a specific object - - This function is recursive : if the given user hasn't got direct - access to this object and if he has got access other ``User`` - objects, we check if one of those users owns the object. - - :param obj: a admin object - :return: a boolean - """ - from modoboa.lib.permissions import get_content_type - - if self.is_superuser: - return True - - ct = get_content_type(obj) - try: - ooentry = self.objectaccess_set.get(content_type=ct, object_id=obj.id) - except ObjectAccess.DoesNotExist: - pass - else: - return True - if ct.model == "user": - return False - - ct = self.get_content_type() - qs = self.objectaccess_set.filter(content_type=ct) - for ooentry in qs.all(): - if ooentry.content_object.is_owner(obj): - return True - return False - - def grant_access_to_all_objects(self): - """Give access to all objects defined in the database - - Must be used when an account is promoted as a super user. - """ - from modoboa.lib.permissions import grant_access_to_objects, get_content_type - grant_access_to_objects(self, User.objects.all(), get_content_type(User)) - grant_access_to_objects(self, Domain.objects.all(), get_content_type(Domain)) - grant_access_to_objects(self, DomainAlias.objects.all(), get_content_type(DomainAlias)) - grant_access_to_objects(self, Mailbox.objects.all(), get_content_type(Mailbox)) - grant_access_to_objects(self, Alias.objects.all(), get_content_type(Alias)) - - def set_role(self, role): - """Set administrative role for this account - - :param string role: the role to set - """ - if role is None or self.group == role: - return - self.groups.clear() - if role == "SuperAdmins": - self.is_superuser = True - self.grant_access_to_all_objects() - else: - if self.is_superuser: - ObjectAccess.objects.filter(user=self).delete() - self.is_superuser = False - try: - self.groups.add(Group.objects.get(name=role)) - except Group.DoesNotExist: - self.groups.add(Group.objects.get(name="SimpleUsers")) - if self.group != "SimpleUsers" and not self.can_access(self): - from modoboa.lib.permissions import grant_access_to_object - grant_access_to_object(self, self) - self.save() - - 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("AccountCreated", self) - - def save(self, *args, **kwargs): - if "creator" in kwargs: - creator = kwargs["creator"] - del kwargs["creator"] - else: - creator = None - super(User, self).save(*args, **kwargs) - if creator is not None: - self.post_create(creator) - - def from_csv(self, user, row, crypt_password=True): - """Create a new account from a CSV file entry - - The expected order is the following:: - - "account", loginname, password, first name, last name, enabled, group, address[, domain, ...] - - :param user: the user importing a new account - :param row: a list containing the expected information - :param crypt_password: - """ - if len(row) < 7: - raise AdminError(_("Invalid line")) - role = row[6].strip() - if not user.is_superuser and not role in ["SimpleUsers", "DomainAdmins"]: - raise PermDeniedException( - _("You can't import an account with a role greater than yours") - ) - self.username = row[1].strip() - if role == "SimpleUsers": - if (len(row) < 8 or not row[7].strip()): - raise AdminError( - _("The simple user '%s' must have a valid email address" % self.username) - ) - if self.username != row[7].strip(): - raise AdminError( - _("username and email fields must not differ for '%s'" % self.username) - ) - - if crypt_password: - self.set_password(row[2].strip()) - else: - self.password = row[2].strip() - self.first_name = row[3].strip() - self.last_name = row[4].strip() - self.is_active = (row[5].strip() == 'True') - self.save(creator=user) - self.set_role(role) - - if len(row) < 8: - return - - self.email = row[7].strip() - if self.email != "": - self.save() - mailbox, domname = split_mailbox(self.email) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise AdminError( - _("Account import failed (%s): domain does not exist" % self.username) - ) - if not user.can_access(domain): - raise PermDeniedException - mb = Mailbox(address=mailbox, domain=domain, user=self, use_domain_quota=True) - mb.set_quota(override_rules=user.has_perm("admin.change_domain")) - mb.save(creator=user) - - if self.group == "DomainAdmins": - for domname in row[8:]: - try: - dom = Domain.objects.get(name=domname.strip()) - except Domain.DoesNotExist: - continue - dom.add_admin(self) - - def to_csv(self, csvwriter): - row = ["account", self.username.encode("utf-8"), self.password, - self.first_name.encode("utf-8"), self.last_name.encode("utf-8"), - self.is_active, self.group, self.email] - if self.group == "DomainAdmins": - row += [dom.name for dom in self.get_domains()] - csvwriter.writerow(row) - -reversion.register(User) - - -class ObjectAccess(models.Model): - user = models.ForeignKey(User) - content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - content_object = generic.GenericForeignKey('content_type', 'object_id') - is_owner = models.BooleanField(default=False) - - class Meta: - unique_together = (("user", "content_type", "object_id"),) - - def __unicode__(self): - return "%s => %s (%s)" % (self.user, self.content_object, self.content_type) - - -class ObjectDates(models.Model): - """Dates recording for admin objects - - This table keeps creation and last modification dates for Domains, - domain aliases, mailboxes and aliases objects. - """ - creation = models.DateTimeField(auto_now_add=True) - last_modification = models.DateTimeField(auto_now=True) - - @staticmethod - def set_for_object(obj): - """Initialize or update dates for a given object. - - :param obj: an admin object (Domain, Mailbox, etc) - """ - try: - dates = getattr(obj, "dates") - except ObjectDates.DoesNotExist: - dates = ObjectDates() - dates.save() - obj.dates = dates - - -class DatesAware(models.Model): - """Abstract model to support dates - - Inherit from this model to automatically add the "dates" feature - to another model. It defines the appropriate field and handles - saves. - """ - dates = models.ForeignKey(ObjectDates) - - 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 - - @property - def last_modification(self): - return self.dates.last_modification - - -class Domain(DatesAware): - name = models.CharField(ugettext_lazy('name'), max_length=100, unique=True, - help_text=ugettext_lazy("The domain name")) - quota = models.IntegerField( - help_text=ugettext_lazy("Default quota in MB applied to mailboxes") - ) - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy("Check to activate this domain") - ) - owners = generic.GenericRelation(ObjectAccess) - - class Meta: - permissions = ( - ("view_domain", "View domain"), - ("view_domains", "View domains"), - ) - ordering = ["name"] - - @property - def domainalias_count(self): - return self.domainalias_set.count() - - @property - def mailbox_count(self): - return self.mailbox_set.count() - - @property - def mbalias_count(self): - return self.alias_set.count() - - @property - def admins(self): - """Return the domain administrators of this domain - - :return: a list of User objects - """ - return [oa.user for oa in self.owners.filter(user__is_superuser=False)] - - def add_admin(self, account): - """Add a new administrator for this domain - - :param User account: the administrotor to add - """ - from modoboa.lib.permissions import grant_access_to_object - grant_access_to_object(account, self) - for mb in self.mailbox_set.all(): - if mb.user.has_perm("admin.add_domain"): - continue - grant_access_to_object(account, mb) - grant_access_to_object(account, mb.user) - for al in self.alias_set.all(): - grant_access_to_object(account, al) - - def remove_admin(self, account): - """Remove an administrator for this domain - - :param User account: the administrotor to remove - """ - from modoboa.lib.permissions import ungrant_access_to_object - ungrant_access_to_object(self, account) - for mb in self.mailbox_set.all(): - if mb.user.has_perm("admin.add_domain"): - continue - ungrant_access_to_object(mb, account) - ungrant_access_to_object(mb.user, account) - for al in self.alias_set.all(): - ungrant_access_to_object(al, account) - - def delete(self, fromuser, keepdir=False): - from modoboa.lib.permissions import \ - ungrant_access_to_object, ungrant_access_to_objects - - 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()) - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm == "yes" and not keepdir: - for mb in self.mailbox_set.all(): - mb.delete_dir() - if self.alias_set.count(): - events.raiseEvent("MailboxAliasDelete", 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) - 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")) - self.name = row[1].strip() - self.quota = int(row[2].strip()) - self.enabled = (row[3].strip() == 'True') - self.save(creator=user) - - def to_csv(self, csvwriter): - csvwriter.writerow(["domain", self.name, self.quota, self.enabled]) - -reversion.register(Domain) - - -class DomainAlias(DatesAware): - name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True, - help_text=ugettext_lazy("The alias name")) - target = models.ForeignKey( - Domain, verbose_name=ugettext_lazy('target'), - help_text=ugettext_lazy("The domain this alias points to") - ) - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy("Check to activate this alias") - ) - - class Meta: - permissions = ( - ("view_domaliases", "View domain aliases"), - ) - - 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 - - Expected format: ["domainalias", domain alias name, targeted domain, enabled] - - :param user: a ``User`` object - :param row: a list containing the alias definition - """ - if len(row) < 4: - raise AdminError(_("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)) - self.enabled = row[3].strip() == 'True' - self.save(creator=user) - - def to_csv(self, csvwriter): - """Export a domain alias using CSV format - - :param csvwriter: a ``csv.writer`` object - """ - csvwriter.writerow(["domainalias", self.name, - self.target.name, self.enabled]) - -reversion.register(DomainAlias) - - -class Mailbox(DatesAware): - address = models.CharField( - ugettext_lazy('address'), max_length=252, - help_text=ugettext_lazy("Mailbox address (without the @domain.tld part)") - ) - quota = models.PositiveIntegerField() - use_domain_quota = models.BooleanField(default=False) - domain = models.ForeignKey(Domain) - user = models.ForeignKey(User) - - class Meta: - permissions = ( - ("view_mailboxes", "View mailboxes"), - ) - - def __init__(self, *args, **kwargs): - super(Mailbox, self).__init__(*args, **kwargs) - self.__mail_home = None - - def __str__(self): - return self.full_address - - def __full_address(self, localpart): - return "%s@%s" % (localpart, self.domain.name) - - @property - def full_address(self): - return self.__full_address(self.address) - - @property - def enabled(self): - return self.user.is_active - - @property - def alias_count(self): - return self.alias_set.count() - - @property - def mail_home(self): - """ - - """ - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm is None or hm == "no": - return None - if self.__mail_home is None: - code, output = exec_cmd("doveadm user %s -f home" % self.full_address, - sudo_user=parameters.get_admin("MAILBOXES_OWNER")) - if code: - raise AdminError(_("Failed to retrieve mailbox location (%s)" % output)) - self.__mail_home = output.strip() - return self.__mail_home - - def rename_dir(self, old_mail_home): - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm is None or hm == "no": - return - self.__mail_home = None - if not os.path.exists(old_mail_home): - return - code, output = exec_cmd( - "mv %s %s" % (old_mail_home, self.mail_home), - sudo_user=parameters.get_admin("MAILBOXES_OWNER") - ) - if code: - raise AdminError(_("Failed to rename mailbox: %s" % output)) - - def rename(self, address, domain): - """Rename the mailbox - - :param string address: the new mailbox's address (local part) - :param Domain domain: the new mailbox's domain - """ - old_mail_home = self.mail_home - qs = Quota.objects.filter(username=self.full_address) - self.address = address - self.domain = domain - self.rename_dir(old_mail_home) - qs.update(username=self.full_address) - - def delete_dir(self): - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm is None or hm == "no": - return - if not os.path.exists(self.mail_home): - return - code, output = exec_cmd( - "rm -r %s" % self.mail_home, - sudo_user=parameters.get_admin("MAILBOXES_OWNER") - ) - if code: - raise AdminError(_("Failed to remove mailbox: %s" % output)) - - def set_quota(self, value=None, override_rules=False): - """Set or update quota's value for this mailbox. - - :param integer value: the quota's value - :param bool override_rules: allow to override defined quota rules - """ - if value is None: - if self.use_domain_quota: - self.quota = self.domain.quota - else: - self.quota = 0 - elif int(value) > self.domain.quota and not override_rules: - raise AdminError( - _("Quota is greater than the allowed domain's limit (%dM)" % self.domain.quota) - ) - else: - self.quota = value - if not self.quota and not override_rules: - raise AdminError(_("A quota is required")) - - def get_quota(self): - q = Quota.objects.get(username=self.full_address) - return int(q.bytes / 1048576) - - def get_quota_in_percent(self): - if not self.quota: - return 0 - q = Quota.objects.get(username=self.full_address) - return int(q.bytes / float(self.quota * 1048576) * 100) - - 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) - if creator.is_superuser and not self.user.has_perm("admin.add_domain"): - # A super user is creating a new mailbox. Give - # access to that mailbox (and the associated - # account) to the appropriate domain admins, - # except if the new account has a more important - # role (SuperAdmin, Reseller) - for admin in self.domain.admins: - grant_access_to_object(admin, self) - grant_access_to_object(admin, self.user) - - def save(self, *args, **kwargs): - 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: - Quota.objects.create(mbox=self, username=self.full_address) - - def delete(self, keepdir=False): - """Custom delete method - - We try to delete the associated quota in the same time (it may - has already been removed if we're deleting a domain). - - :param bool keepdir: delete the mailbox home dir on the filesystem or not - """ - try: - q = Quota.objects.get(username=self.full_address) - except Quota.DoesNotExist: - pass - else: - q.delete() - if not keepdir: - self.delete_dir() - super(Mailbox, self).delete() - -reversion.register(Mailbox) - - -@receiver(pre_delete, sender=Mailbox) -def mailbox_deleted_handler(sender, **kwargs): - """``Mailbox`` pre_delete signal receiver - - In order to properly handle deletions (ie. we don't want to leave - orphan records into the db), we define this custom receiver. - - It manually removes the mailbox from the aliases it is linked to - and then remove all empty aliases. - """ - from modoboa.lib.permissions import ungrant_access_to_object - - mb = kwargs['instance'] - events.raiseEvent("DeleteMailbox", mb) - ungrant_access_to_object(mb) - for alias in mb.alias_set.all(): - alias.mboxes.remove(mb) - if alias.mboxes.count() == 0: - alias.delete() - - -class Quota(models.Model): - username = models.EmailField(primary_key=True, max_length=254) - bytes = models.BigIntegerField(default=0) - messages = models.IntegerField(default=0) - - mbox = models.OneToOneField(Mailbox, related_name="quota_value", null=True) - - -class Alias(DatesAware): - address = models.CharField( - ugettext_lazy('address'), max_length=254, - help_text=ugettext_lazy("The alias address (without the domain part). For a 'catch-all' address, just enter an * character.") - ) - domain = models.ForeignKey(Domain) - mboxes = models.ManyToManyField( - Mailbox, verbose_name=ugettext_lazy('mailboxes'), - help_text=ugettext_lazy("The mailboxes this alias points to") - ) - aliases = models.ManyToManyField( - 'Alias', - help_text=ugettext_lazy("The aliases this alias points to") - ) - extmboxes = models.TextField(blank=True) - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy("Check to activate this alias") - ) - - class Meta: - permissions = ( - ("view_aliases", "View aliases"), - ) - unique_together = (("address", "domain"),) - ordering = ["domain__name", "address"] - - def __unicode__(self): - return self.full_address - - @property - def full_address(self): - return "%s@%s" % (self.address, self.domain.name) - - @property - def identity(self): - return self.full_address - - @property - def name_or_rcpt(self): - rcpts_count = self.get_recipients_count() - if not rcpts_count: - return "---" - rcpts = self.get_recipients() - if rcpts_count > 1: - return "%s, ..." % rcpts[0] - return rcpts[0] - - @property - def type(self): - cpt = self.get_recipients_count() - if cpt > 1: - return "dlist" - if self.extmboxes != "": - return "forward" - return "alias" - - @property - def tags(self): - labels = { - "dlist": _("distribution list"), - "forward": _("forward"), - "alias": _("alias") - } - altype = self.type - return [{"name": altype, "label": labels[altype], "type": "idt"}] - - def post_create(self, creator): - from modoboa.lib.permissions import grant_access_to_object - grant_access_to_object(creator, self, is_owner=True) - events.raiseEvent("MailboxAliasCreated", creator, self) - if creator.is_superuser: - for admin in self.domain.admins: - grant_access_to_object(admin, self) - - def save(self, int_rcpts, ext_rcpts, *args, **kwargs): - if len(ext_rcpts): - self.extmboxes = ",".join(ext_rcpts) - else: - self.extmboxes = "" - 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: - if isinstance(t, Alias): - if not t in curaliases: - self.aliases.add(t) - continue - if not t in curmboxes: - self.mboxes.add(t) - for t in curaliases: - if not t in int_rcpts: - self.aliases.remove(t) - for t in curmboxes: - 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 - - Internal and external addresses are mixed into a single list. - """ - result = [al.full_address for al in self.aliases.all()] - result += [mb.full_address for mb in self.mboxes.all()] - if self.extmboxes != "": - result += self.extmboxes.split(',') - return result - - def get_recipients_count(self): - total = 0 - if self.extmboxes != "": - total += len(self.extmboxes.split(',')) - return total + self.aliases.count() + self.mboxes.count() - - def ui_disabled(self, user): - if user.is_superuser: - return False - for mb in self.mboxes.all(): - if not user.is_owner(mb.domain): - return True - return False - - def from_csv(self, user, row, expected_elements=5): - """Create a new alias from a CSV file entry - - """ - if len(row) < expected_elements: - raise AdminError(_("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)) - if not user.can_access(domain): - raise PermDeniedException - self.address = localpart - self.domain = domain - self.enabled = (row[2].strip() == 'True') - int_rcpts = [] - ext_rcpts = [] - for rcpt in row[3:]: - rcpt = rcpt.strip() - localpart, domname = split_mailbox(rcpt) - try: - Domain.objects.get(name=domname) - except Domain.DoesNotExist: - ext_rcpts += [rcpt] - continue - try: - target = Alias.objects.get(domain__name=domname, address=localpart) - if target.full_address == self.full_address: - target = None - except Alias.DoesNotExist: - target = None - if target is None: - try: - target = Mailbox.objects.get(address=localpart, - domain__name=domname) - except Mailbox.DoesNotExist: - raise AdminError(_("Local recipient %s not found" % rcpt)) - int_rcpts += [target] - self.save(int_rcpts, ext_rcpts, creator=user) - - def to_csv(self, csvwriter): - row = [self.type, self.full_address, self.enabled] - row += self.get_recipients() - csvwriter.writerow(row) - -reversion.register(Alias) - - -class Extension(models.Model): - name = models.CharField(max_length=150) - enabled = models.BooleanField( - ugettext_lazy('enabled'), - help_text=ugettext_lazy("Check to enable this extension") - ) - - def __init__(self, *args, **kwargs): - super(Extension, self).__init__(*args, **kwargs) - - def __unicode__(self): - return self.name - - def __get_ext_instance(self): - if not self.name: - return None - if hasattr(self, "instance") and self.instance: - return - self.instance = exts_pool.get_extension(self.name) - if self.instance: - self.__get_ext_dir() - - def __get_ext_dir(self): - modname = self.instance.__module__ - path = os.path.realpath(sys.modules[modname].__file__) - self.extdir = os.path.dirname(path) - - def on(self): - self.enabled = True - self.save() - - self.__get_ext_instance() - self.instance.load() - self.instance.init() - - if self.instance.needs_media: - path = os.path.join(settings.MEDIA_ROOT, self.name) - exec_cmd("mkdir %s" % path) - - events.raiseEvent("ExtEnabled", self) - - def off(self): - self.__get_ext_instance() - if self.instance is None: - return - self.instance.destroy() - - self.enabled = False - self.save() - - if self.instance.needs_media: - path = os.path.join(settings.MEDIA_ROOT, self.name) - exec_cmd("rm -r %s" % path) - - events.raiseEvent("ExtDisabled", self) - -reversion.register(Extension) - - -def populate_callback(user): - """Populate callback - - If the LDAP authentication backend is in use, this callback will - be called each time a new user authenticates succesfuly to - Modoboa. This function is in charge of creating the mailbox - associated to the provided ``User`` object. - - :param user: a ``User`` instance - """ - from modoboa.lib.permissions import grant_access_to_object - - sadmins = User.objects.filter(is_superuser=True) - user.set_role("SimpleUsers") - user.post_create(sadmins[0]) - for su in sadmins[1:]: - grant_access_to_object(su, user) - - localpart, domname = split_mailbox(user.username) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - domain = Domain(name=domname, enabled=True, quota=0) - domain.save(creator=sadmins[0]) - for su in sadmins[1:]: - grant_access_to_object(su, domain) - try: - mb = Mailbox.objects.get(domain=domain, address=localpart) - except Mailbox.DoesNotExist: - mb = Mailbox(address=localpart, domain=domain, user=user, use_domain_quota=True) - mb.set_quota(override_rules=True) - mb.save(creator=sadmins[0]) - for su in sadmins[1:]: - grant_access_to_object(su, mb) - - -@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: - action = _("added") - level = "info" - elif version.type == reversion.models.VERSION_CHANGE: - 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) diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py deleted file mode 100644 index b34136387..000000000 --- a/modoboa/admin/views.py +++ /dev/null @@ -1,889 +0,0 @@ -# coding: utf-8 -from django.http import HttpResponse, HttpResponseRedirect -from django.utils.translation import ugettext as _, ungettext -from django.core.urlresolvers import reverse -from django.contrib.auth.decorators \ - import login_required, permission_required, user_passes_test -from django.db.models import Q -from django.shortcuts import render -from django.template.loader import render_to_string -from django.db import transaction, IntegrityError -from django.views.decorators.csrf import ensure_csrf_cookie -import cStringIO -import csv - -from lib import get_sort_order, get_listing_page -from forms import * -from models import * -from modoboa.admin.tables import DomainsTable, ExtensionsTable -from modoboa.lib import events, parameters -from modoboa.lib.exceptions import ModoboaException, PermDeniedException -from modoboa.lib.webutils \ - import _render, ajax_response, ajax_simple_response, _render_to_string -from modoboa.lib.emailutils import split_mailbox -from modoboa.lib.models import Parameter, Log -from modoboa.lib.formutils import CreationWizard -from modoboa.lib.templatetags.libextras import pagination_bar - - -@login_required -def index(request): - return HttpResponseRedirect(reverse(domains)) - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.view_domains") or u.has_perm("admin.view_mailboxes")) -def _domains(request): - sort_order, sort_dir = get_sort_order(request.GET, "name") - domains = request.user.get_domains() - 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(), - "page": page.number, - "paginbar": pagination_bar(page), - "handle_mailboxes": parameters.get_admin("HANDLE_MAILBOXES", - raise_error=False), - "auto_account_removal": parameters.get_admin("AUTO_ACCOUNT_REMOVAL") - }) - - -@login_required -@ensure_csrf_cookie -def domains(request, tplname="admin/domains.html"): - if not request.user.has_perm("admin.view_domains"): - if request.user.has_perm("admin.view_mailboxes"): - return HttpResponseRedirect(reverse(identities)) - return HttpResponseRedirect(reverse("modoboa.userprefs.views.index")) - return render(request, tplname, {"selection": "domains"}) - - -@login_required -@permission_required("admin.add_user") -def domains_list(request): - doms = [dom.name for dom in request.user.get_domains()] - return ajax_simple_response(doms) - - -@login_required -@permission_required("admin.add_domain") -@transaction.commit_on_success -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.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") - - if request.method == "POST": - retcode, data = cwizard.validate_step(request) - if retcode == -1: - raise AdminError(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")) - ) - - from modoboa.lib.templatetags.libextras import render_form - return ajax_simple_response(dict( - status="ko", stepid=data, - form=render_form(cwizard.steps[data]["form"]) - )) - - cwizard.create_forms() - commonctx.update(steps=cwizard.steps) - commonctx.update(subtitle="1. %s" % cwizard.steps[0]['title']) - return render(request, tplname, commonctx) - - -@login_required -@permission_required("admin.view_domains") -def editdomain(request, dom_id, tplname="admin/editdomainform.html"): - domain = Domain.objects.get(pk=dom_id) - if not request.user.can_access(domain): - raise PermDeniedException - - domadmins = filter( - lambda u: request.user.can_access(u) and not u.is_superuser, - domain.admins - ) - if not request.user.is_superuser: - domadmins = filter(lambda u: u.group == "DomainAdmins", domadmins) - - 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 - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) - - commonctx["tabs"] = DomainForm(request.user, instances=instances) - commonctx["domadmins"] = domadmins - return render(request, tplname, commonctx) - - -@login_required -@permission_required("admin.delete_domain") -@transaction.commit_on_success -def deldomain(request, dom_id): - keepdir = True if request.POST.get("keepdir", "false") == "true" else False - try: - mb = Mailbox.objects.get(user__id=request.user.id) - except Mailbox.DoesNotExist: - mb = None - - dom = Domain.objects.get(pk=dom_id) - if not request.user.can_access(dom): - raise PermDeniedException - if mb and mb.domain == dom: - raise AdminError(_("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}) - - -def _validate_alias(request, form, successmsg, tplname, commonctx, 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")) - if callback: - callback(request.user, alias) - return ajax_simple_response({"status": "ok", "respmsg": 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) - - -def _new_alias(request, title, action, successmsg, - tplname="common/generic_modal_form.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 - ) - - form = AliasForm(request.user) - ctx["form"] = form - return render(request, tplname, ctx) - - -@login_required -@permission_required("admin.add_alias") -@transaction.commit_on_success -def newdlist(request): - return _new_alias( - request, _("New distribution list"), reverse(newdlist), - _("Distribution list created") - ) - - -@login_required -@permission_required("admin.add_alias") -@transaction.commit_on_success -def newalias(request): - return _new_alias( - request, _("New alias"), reverse(newalias), - _("Alias created") - ) - - -@login_required -@permission_required("admin.add_alias") -@transaction.commit_on_success -def newforward(request): - return _new_alias( - request, _("New forward"), reverse(newforward), - _("Forward created") - ) - - -@login_required -@permission_required("admin.change_alias") -def editalias(request, alid, tplname="common/generic_modal_form.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": - form = AliasForm(request.user, request.POST, instance=alias) - return _validate_alias(request, form, successmsg, tplname, ctx) - - form = AliasForm(request.user, instance=alias) - ctx["form"] = form - return render(request, tplname, ctx) - - -def _del_alias(request, msg, msgs): - selection = request.GET["selection"].split(",") - for alid in selection: - alias = Alias.objects.get(pk=alid) - if not request.user.can_access(alias): - raise PermDeniedException - alias.delete() - - msg = ungettext(msg, msgs, len(selection)) - return ajax_simple_response({"status": "ok", "respmsg": msg}) - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def delalias(request): - return _del_alias(request, "Alias deleted", "Aliases deleted") - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def deldlist(request): - return _del_alias(request, "Distribution list deleted", - "Distribution lists deleted") - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def delforward(request): - return _del_alias(request, "Forward deleted", "Forwards deleted") - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) -def _identities(request): - idents_list = request.user.get_identities(request.GET) - sort_order, sort_dir = get_sort_order(request.GET, "identity", - ["identity", "name_or_rcpt", "tags"]) - if sort_order in ["identity", "name_or_rcpt"]: - objects = sorted(idents_list, key=lambda o: getattr(o, sort_order), - reverse=sort_dir == '-') - else: - objects = sorted(idents_list, key=lambda o: o.tags[0], - reverse=sort_dir == '-') - page = get_listing_page(objects, request.GET.get("page", 1)) - return ajax_simple_response({ - "table": _render_to_string(request, "admin/identities_table.html", { - "identities": page.object_list, - "tableid": "objects_table" - }), - "handle_mailboxes": parameters.get_admin("HANDLE_MAILBOXES", - raise_error=False), - "page": page.number, - "paginbar": pagination_bar(page) - }) - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) -@ensure_csrf_cookie -def identities(request, tplname="admin/identities.html"): - return render(request, tplname, { - "selection": "identities", - "deflocation": "list/" - }) - - -@login_required -@permission_required("admin.add_user") -def accounts_list(request): - accs = User.objects.filter(is_superuser=False) \ - .exclude(groups__name='SimpleUsers') - res = [a.username for a in accs.all()] - return ajax_simple_response(res) - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") - or u.has_perm("admin.add_alias")) -def allowed_recipients_list(request): - ret = [mb.full_address for mb in request.user.get_mailboxes()] - ret += [al.full_address for al in request.user.get_aliases()] - return ajax_simple_response(ret) - - -@login_required -@permission_required("admin.add_mailbox") -def list_quotas(request, tplname="admin/quotas.html"): - from modoboa.lib.dbutils import db_type - - sort_order, sort_dir = get_sort_order(request.GET, "address") - mboxes = request.user.get_mailboxes(request.GET.get("searchquery", None)) - mboxes = mboxes.exclude(quota=0) - if sort_order in ["address", "quota", "quota_value__bytes"]: - mboxes = mboxes.order_by("%s%s" % (sort_dir, sort_order)) - elif sort_order == "quota_usage": - if db_type() == "postgres": - select = '(admin_quota.bytes::float / (CAST(admin_mailbox.quota AS BIGINT) * 1048576)) * 100' - else: - select = 'admin_quota.bytes / (admin_mailbox.quota * 1048576) * 100' - mboxes = mboxes.extra( - select={'quota_usage': select}, - where=["admin_quota.mbox_id=admin_mailbox.id"], - tables=["admin_quota"], - order_by=["%s%s" % (sort_dir, sort_order)] - ) - else: - raise AdminError(_("Invalid request")) - page = get_listing_page(mboxes, request.GET.get("page", 1)) - return ajax_simple_response({ - "status": "ok", - "page": page.number, - "paginbar": pagination_bar(page), - "table": _render_to_string(request, tplname, { - "mboxes": page - }) - }) - - -@login_required -@permission_required("admin.add_user") -@transaction.commit_on_success -def newaccount(request, tplname='common/wizard_forms.html'): - def create_account(steps): - """Account creation callback - - 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) - cwizard.add_step(AccountFormGeneral, _("General"), - [dict(classes="btn-inverse next", label=_("Next"))], - new_args=[request.user]) - cwizard.add_step(AccountFormMail, _("Mail"), - [dict(classes="btn-primary submit", label=_("Create")), - dict(classes="btn-inverse prev", label=_("Previous"))], - formtpl="admin/mailform.html") - - if request.method == "POST": - retcode, data = cwizard.validate_step(request) - if retcode == -1: - raise AdminError(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=_("Account created") - )) - - from modoboa.lib.templatetags.libextras import render_form - return ajax_simple_response(dict( - status="ko", stepid=data, - form=render_form(cwizard.steps[data]["form"]) - )) - - cwizard.create_forms() - ctx.update(steps=cwizard.steps) - ctx.update(subtitle="1. %s" % cwizard.steps[0]['title']) - return render(request, tplname, ctx) - - -@login_required -@permission_required("admin.change_user") -@transaction.commit_on_success -def editaccount(request, accountid, tplname="common/tabforms.html"): - account = User.objects.get(pk=accountid) - if not request.user.can_access(account): - raise PermDeniedException - mb = None - if account.has_mailbox: - mb = account.mailbox_set.all()[0] - - 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, - instances=instances, classes=classes) - account.oldgroup = account.group - if form.is_valid(mandatory_only=True): - form.save_general_form() - 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() - - ctx["tabs"] = form - return ajax_response(request, status="ko", template=tplname, **ctx) - - ctx["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 - return render(request, tplname, ctx) - - -@login_required -@permission_required("admin.delete_user") -@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, keepdir) - - msg = ungettext("Account deleted", "Accounts deleted", 1) - return ajax_simple_response({"status": "ok", "respmsg": msg}) - - -@login_required -@permission_required("admin.add_domain") -def remove_permission(request): - domid = request.GET.get("domid", None) - daid = request.GET.get("daid", None) - if domid is None or daid is None: - raise AdminError(_("Invalid request")) - try: - account = User.objects.get(pk=daid) - domain = Domain.objects.get(pk=domid) - except (User.DoesNotExist, Domain.DoesNotExist): - raise AdminError(_("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(dict(status="ok")) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def viewsettings(request, tplname='admin/settings_header.html'): - return render(request, tplname, { - "selection": "settings" - }) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def viewparameters(request, tplname='admin/parameters.html'): - return ajax_simple_response({ - "status": "ok", - "left_selection": "parameters", - "content": _render_to_string(request, tplname, { - "forms": parameters.get_admin_forms - }) - }) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def saveparameters(request): - for formdef in parameters.get_admin_forms(request.POST): - form = formdef["form"] - if form.is_valid(): - 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") - )) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def viewextensions(request, tplname='admin/extensions.html'): - from modoboa.extensions import exts_pool - - exts = exts_pool.list_all() - for ext in exts: - try: - dbext = Extension.objects.get(name=ext["id"]) - ext["selection"] = dbext.enabled - except Extension.DoesNotExist: - dbext = Extension() - dbext.name = ext["id"] - dbext.enabled = False - dbext.save() - ext["selection"] = False - - tbl = ExtensionsTable(request, exts) - return ajax_simple_response({ - "status": "ok", - "content": _render_to_string(request, tplname, {"extensions": tbl}) - }) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def saveextensions(request): - actived_exts = Extension.objects.filter(enabled=True) - found = [] - for k in request.POST.keys(): - if k.startswith("select_"): - parts = k.split("_", 1) - dbext = Extension.objects.get(name=parts[1]) - if not dbext in actived_exts: - dbext.on() - else: - found += [dbext] - for ext in actived_exts: - if not ext in found: - ext.off() - - return ajax_simple_response(dict( - status="ok", respmsg=_("Modifications applied.") - )) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def information(request, tplname="admin/information.html"): - return ajax_simple_response({ - "status": "ok", - "content": render_to_string(tplname, {}) - }) - - -@login_required -@user_passes_test(lambda u: u.is_superuser) -def logs(request, tplname="admin/logs.html"): - sort_order, sort_dir = get_sort_order( - request.GET, "date_created", - allowed_values=['date_created', 'level', 'logger', 'message'] - ) - page = get_listing_page( - Log.objects.all().order_by("%s%s" % (sort_dir, sort_order)), - request.GET.get("page", 1) - ) - return ajax_simple_response({ - "status": "ok", - "content": render_to_string(tplname, { - "logs": page.object_list, - }), - "page": page.number, - "paginbar": pagination_bar(page), - }) - - -@transaction.commit_on_success -def import_domain(user, row, formopts): - """Specific code for domains import""" - dom = Domain() - dom.from_csv(user, row) - - -@transaction.commit_on_success -def import_domainalias(user, row, formopts): - """Specific code for domain aliases import""" - domalias = DomainAlias() - domalias.from_csv(user, row) - - -@transaction.commit_on_success -def import_account(user, row, formopts): - """Specific code for accounts import""" - account = User() - account.from_csv(user, row, formopts["crypt_password"]) - - -@transaction.commit_on_success -def _import_alias(user, row, **kwargs): - """Specific code for aliases import""" - alias = Alias() - alias.from_csv(user, row, **kwargs) - - -def import_alias(user, row, formopts): - _import_alias(user, row, expected_elements=4) - - -def import_forward(user, row, formopts): - _import_alias(user, row, expected_elements=4) - - -def import_dlist(user, row, formopts): - _import_alias(user, row) - -def importdata(request, formclass=ImportDataForm): - """Generic import function - - As the process of importing data from a CSV file is the same - whatever the type, we do a maximum of the work here. - - :param request: a ``Request`` instance - :param typ: a string indicating the object type being imported - :return: a ``Response`` instance - """ - error = None - form = formclass(request.POST, request.FILES) - if form.is_valid(): - try: - reader = csv.reader(request.FILES['sourcefile'], - delimiter=form.cleaned_data['sepchar']) - except csv.Error, e: - error = str(e) - - if error is None: - try: - cpt = 0 - for row in reader: - if not row: - continue - try: - fct = globals()["import_%s" % row[0].strip()] - except KeyError: - continue - 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)) - cpt += 1 - msg = _("%d objects imported successfully" % cpt) - return render(request, "admin/import_done.html", { - "status": "ok", "msg": msg - }) - except (ModoboaException), e: - error = str(e) - - return render(request, "admin/import_done.html", { - "status": "ko", "msg": error - }) - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) -def import_identities(request): - if request.method == "POST": - return importdata(request, ImportIdentitiesForm) - - helptext = _("""Provide a CSV file where lines respect one of the following formats: -
    -
  • account; loginname; password; first name; last name; enabled; group; address[, domain, ...]
  • -
  • alias; address; enabled; internal recipient
  • -
  • forward; address; enabled; external recipient
  • -
  • dlist; address; enabled; recipient; recipient; ...
  • -
-

The first element of each line is mandatory and must be equal to one of the previous values.

- -

You can use a different character as separator.

-""") - ctx = dict( - title=_("Import identities"), - action_label=_("Import"), - action_classes="submit", - action=reverse(import_identities), - formid="importform", - enctype="multipart/form-data", - target="import_target", - form=ImportIdentitiesForm(), - helptext=helptext - ) - return render(request, "admin/importform.html", ctx) - - -def _export(content, filename): - """Export a csv file's content - - :param content: the content to export (string) - :param filename: the name that will appear into the response - :return: an ``HttpResponse`` object - """ - resp = HttpResponse(content) - resp["Content-Type"] = "text/csv" - resp["Content-Length"] = len(content) - resp["Content-Disposition"] = 'attachment; filename="%s"' % filename - return resp - - -@login_required -@user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) -def export_identities(request): - ctx = { - "title": _("Export identities"), - "action_label": _("Export"), - "action_classes": "submit", - "formid": "exportform", - "action": reverse(export_identities), - } - - if request.method == "POST": - form = ExportIdentitiesForm(request.POST) - form.is_valid() - fp = cStringIO.StringIO() - csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) - for ident in request.user.get_identities(): - ident.to_csv(csvwriter) - content = fp.getvalue() - fp.close() - return _export(content, form.cleaned_data["filename"]) - - ctx["form"] = ExportIdentitiesForm() - return render(request, "common/generic_modal_form.html", ctx) - - -@login_required -@permission_required("admin.add_domain") -@transaction.commit_on_success -def import_domains(request): - if request.method == "POST": - return importdata(request) - - helptext = _("""Provide a CSV file where lines respect one of the following formats: -
    -
  • domain; name; quota; enabled
  • -
  • domainalias; name; targeted domain; enabled
  • -
-

The first element of each line is mandatory and must be equal to one of the previous values.

-

You can use a different character as separator.

-""") - - ctx = dict( - title=_("Import domains"), - action_label=_("Import"), - action_classes="submit", - action=reverse(import_domains), - formid="importform", - enctype="multipart/form-data", - target="import_target", - helptext=helptext, - form=ImportDataForm() - ) - return render(request, "admin/importform.html", ctx) - - -@login_required -@permission_required("admin.add_domain") -def export_domains(request): - ctx = { - "title": _("Export domains"), - "action_label": _("Export"), - "action_classes": "submit", - "formid": "exportform", - "action": reverse(export_domains), - } - - if request.method == "POST": - form = ExportDomainsForm(request.POST) - form.is_valid() - fp = cStringIO.StringIO() - csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) - for dom in request.user.get_domains(): - 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"]) - - ctx["form"] = ExportDomainsForm() - return render(request, "common/generic_modal_form.html", ctx) diff --git a/modoboa/core/__init__.py b/modoboa/core/__init__.py index b85142032..cf4bc5c04 100644 --- a/modoboa/core/__init__.py +++ b/modoboa/core/__init__.py @@ -1,8 +1,19 @@ from django.utils.translation import ugettext_lazy -from modoboa.lib import parameters +from modoboa.lib import parameters, events def load_settings(): from app_settings import GeneralParametersForm, UserSettings parameters.register(GeneralParametersForm, ugettext_lazy("General")) parameters.register(UserSettings, ugettext_lazy("General")) + + +@events.observe("ExtDisabled") +def unset_default_topredirection(extension): + """ + Simple callback to change the default redirection if the + corresponding extension is being disabled. + """ + topredirection = parameters.get_admin("DEFAULT_TOP_REDIRECTION") + if topredirection == extension.name: + parameters.save_admin("DEFAULT_TOP_REDIRECTION", "userprefs") diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index fb49d4b1a..484d59de6 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -153,7 +153,7 @@ class GeneralParametersForm(parameters.AdminParametersForm): default_top_redirection = forms.ChoiceField( label=ugettext_lazy("Default top redirection"), choices=[], - initial="admin", + initial="core", help_text=ugettext_lazy("The default redirection used when no application is specified") ) diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py index 95999dbd0..2b7f91772 100644 --- a/modoboa/core/extensions.py +++ b/modoboa/core/extensions.py @@ -12,15 +12,19 @@ class ModoExtension(object): considered as valid. """ name = None - version = None + label = None + version = "NA" description = "" url = None needs_media = False always_active = False def infos(self): - return dict(name=self.name, label=self.label, version=self.version, - description=self.description, url=self.url) + return dict( + name=self.name, label=self.label, version=self.version, + description=self.description, url=self.url, + always_active=self.always_active + ) def init(self): pass diff --git a/modoboa/core/models.py b/modoboa/core/models.py index c277af0f2..2906089cc 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -50,6 +50,7 @@ class User(AbstractBaseUser, PermissionsMixin): REQUIRED_FIELDS = ['email'] class Meta: + db_table = "admin_user" ordering = ["username"] password_expr = re.compile(r'(\{(\w+)\}|(\$1\$))(.+)') diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py index 3c7d720ac..3fe55b895 100644 --- a/modoboa/core/templatetags/core_tags.py +++ b/modoboa/core/templatetags/core_tags.py @@ -42,6 +42,36 @@ def core_menu(selection, user): ) +@register.simple_tag +def settings_menu(selection, user): + entries = [ + {"name": "extensions", + "class": "ajaxlink", + "url": "extensions/", + "label": _("Extensions"), + "img": ""}, + {"name": "info", + "class": "ajaxlink", + "url": "info/", + "label": _("Information")}, + {"name": "logs", + "class": "ajaxlink", + "url": "logs/", + "label": _("Logs")}, + {"name": "parameters", + "class": "ajaxlink", + "url": "parameters/", + "img": "", + "label": _("Parameters")}, + ] + return render_to_string('common/menu.html', { + "entries": entries, + "css": "nav nav-list", + "selection": selection, + "user": user + }) + + @register.simple_tag def extensions_menu(selection, user): menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu", user) diff --git a/modoboa/extensions/admin/__init__.py b/modoboa/extensions/admin/__init__.py new file mode 100644 index 000000000..dfe89d830 --- /dev/null +++ b/modoboa/extensions/admin/__init__.py @@ -0,0 +1,21 @@ +# coding: utf-8 +from django.utils.translation import ugettext as _, ugettext_lazy +from modoboa.core.extensions import ModoExtension, exts_pool +from modoboa.lib import parameters, events + + +class AdminConsole(ModoExtension): + name = "admin" + label = ugettext_lazy("Administration console") + version = "1.0" + description = ugettext_lazy("Web based console to manage domains, accounts and aliases") + always_active = True + + def load(self): + from app_settings import AdminParametersForm + parameters.register(AdminParametersForm, ugettext_lazy("Administration")) + + def destroy(self): + parameters.unregister() + +exts_pool.register_extension(AdminConsole, show=False) diff --git a/modoboa/extensions/admin/app_settings.py b/modoboa/extensions/admin/app_settings.py new file mode 100644 index 000000000..c5fed0767 --- /dev/null +++ b/modoboa/extensions/admin/app_settings.py @@ -0,0 +1,48 @@ +from django import forms +from django.utils.translation import ugettext_lazy +from modoboa.lib.formutils import YesNoField, SeparatorField, InlineRadioSelect +from modoboa.lib.sysutils import exec_cmd +from modoboa.lib import parameters + + +class AdminParametersForm(parameters.AdminParametersForm): + app = "admin" + + mbsep = SeparatorField(label=ugettext_lazy("Mailboxes")) + + handle_mailboxes = YesNoField( + label=ugettext_lazy("Handle mailboxes on filesystem"), + initial="no", + help_text=ugettext_lazy("Rename or remove mailboxes on the filesystem when they get renamed or removed within Modoboa") + ) + + mailboxes_owner = forms.CharField( + label=ugettext_lazy("Mailboxes ower"), + initial="vmail", + help_text=ugettext_lazy("The UNIX account who owns mailboxes on the filesystem") + ) + + auto_account_removal = YesNoField( + label=ugettext_lazy("Automatic account removal"), + initial="no", + help_text=ugettext_lazy("When a mailbox is removed, also remove the associated account"), + ) + + # Visibility rules + visibility_rules = { + "mailboxes_owner": "handle_mailboxes=yes", + } + + def __init__(self, *args, **kwargs): + super(AdminParametersForm, self).__init__(*args, **kwargs) + hide_fields = False + try: + code, version = exec_cmd("dovecot --version") + except OSError: + hide_fields = True + else: + if code or not version.strip().startswith("2"): + hide_fields = True + if hide_fields: + del self.fields["handle_mailboxes"] + del self.fields["mailboxes_owner"] diff --git a/modoboa/admin/exceptions.py b/modoboa/extensions/admin/exceptions.py similarity index 100% rename from modoboa/admin/exceptions.py rename to modoboa/extensions/admin/exceptions.py diff --git a/modoboa/admin/fixtures/demo.json b/modoboa/extensions/admin/fixtures/demo.json similarity index 100% rename from modoboa/admin/fixtures/demo.json rename to modoboa/extensions/admin/fixtures/demo.json diff --git a/modoboa/admin/fixtures/initial_data.json b/modoboa/extensions/admin/fixtures/initial_data.json similarity index 100% rename from modoboa/admin/fixtures/initial_data.json rename to modoboa/extensions/admin/fixtures/initial_data.json diff --git a/modoboa/admin/fixtures/initial_users.json b/modoboa/extensions/admin/fixtures/initial_users.json similarity index 100% rename from modoboa/admin/fixtures/initial_users.json rename to modoboa/extensions/admin/fixtures/initial_users.json diff --git a/modoboa/admin/fixtures/test_content.json b/modoboa/extensions/admin/fixtures/test_content.json similarity index 100% rename from modoboa/admin/fixtures/test_content.json rename to modoboa/extensions/admin/fixtures/test_content.json diff --git a/modoboa/admin/forms.py b/modoboa/extensions/admin/forms.py similarity index 99% rename from modoboa/admin/forms.py rename to modoboa/extensions/admin/forms.py index 1459d4593..90f27dbe0 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/extensions/admin/forms.py @@ -3,13 +3,16 @@ from django import forms from django.contrib.auth.models import Group from django.utils.translation import ugettext as _, ugettext_lazy from django.http import QueryDict -from modoboa.admin.templatetags.admin_extras import gender -from modoboa.admin.exceptions import AdminError from modoboa.lib import events, parameters from modoboa.lib.emailutils import split_mailbox from modoboa.lib.permissions import get_account_roles from modoboa.lib.formutils import * -from modoboa.admin.models import User, Domain, DomainAlias, Mailbox, Alias +from modoboa.core.models import User +from modoboa.extensions.admin.templatetags.admin_tags import gender +from modoboa.extensions.admin.exceptions import AdminError +from modoboa.extensions.admin.models import ( + Domain, DomainAlias, Mailbox, Alias +) class DomainFormGeneral(forms.ModelForm, DynamicForm): diff --git a/modoboa/admin/lib.py b/modoboa/extensions/admin/lib.py similarity index 100% rename from modoboa/admin/lib.py rename to modoboa/extensions/admin/lib.py diff --git a/modoboa/admin/migrations/0001_initial.py b/modoboa/extensions/admin/migrations/0001_initial.py similarity index 100% rename from modoboa/admin/migrations/0001_initial.py rename to modoboa/extensions/admin/migrations/0001_initial.py diff --git a/modoboa/admin/migrations/0002_auto__del_field_alias_mbox.py b/modoboa/extensions/admin/migrations/0002_auto__del_field_alias_mbox.py similarity index 100% rename from modoboa/admin/migrations/0002_auto__del_field_alias_mbox.py rename to modoboa/extensions/admin/migrations/0002_auto__del_field_alias_mbox.py diff --git a/modoboa/admin/migrations/0003_auto__add_extension.py b/modoboa/extensions/admin/migrations/0003_auto__add_extension.py similarity index 100% rename from modoboa/admin/migrations/0003_auto__add_extension.py rename to modoboa/extensions/admin/migrations/0003_auto__add_extension.py diff --git a/modoboa/admin/migrations/0004_add_permissions.py b/modoboa/extensions/admin/migrations/0004_add_permissions.py similarity index 100% rename from modoboa/admin/migrations/0004_add_permissions.py rename to modoboa/extensions/admin/migrations/0004_add_permissions.py diff --git a/modoboa/admin/migrations/0005_increase_columns_length.py b/modoboa/extensions/admin/migrations/0005_increase_columns_length.py similarity index 100% rename from modoboa/admin/migrations/0005_increase_columns_length.py rename to modoboa/extensions/admin/migrations/0005_increase_columns_length.py diff --git a/modoboa/admin/migrations/0006_auto__add_domainalias.py b/modoboa/extensions/admin/migrations/0006_auto__add_domainalias.py similarity index 100% rename from modoboa/admin/migrations/0006_auto__add_domainalias.py rename to modoboa/extensions/admin/migrations/0006_auto__add_domainalias.py diff --git a/modoboa/admin/migrations/0007_auto__del_field_alias_full_address__chg_field_alias_address__del_field.py b/modoboa/extensions/admin/migrations/0007_auto__del_field_alias_full_address__chg_field_alias_address__del_field.py similarity index 100% rename from modoboa/admin/migrations/0007_auto__del_field_alias_full_address__chg_field_alias_address__del_field.py rename to modoboa/extensions/admin/migrations/0007_auto__del_field_alias_full_address__chg_field_alias_address__del_field.py diff --git a/modoboa/admin/migrations/0008_update_mailbox_path.py b/modoboa/extensions/admin/migrations/0008_update_mailbox_path.py similarity index 100% rename from modoboa/admin/migrations/0008_update_mailbox_path.py rename to modoboa/extensions/admin/migrations/0008_update_mailbox_path.py diff --git a/modoboa/admin/migrations/0009_auto__add_field_alias_extmboxes.py b/modoboa/extensions/admin/migrations/0009_auto__add_field_alias_extmboxes.py similarity index 100% rename from modoboa/admin/migrations/0009_auto__add_field_alias_extmboxes.py rename to modoboa/extensions/admin/migrations/0009_auto__add_field_alias_extmboxes.py diff --git a/modoboa/admin/migrations/0010_fill_alias_domain.py b/modoboa/extensions/admin/migrations/0010_fill_alias_domain.py similarity index 100% rename from modoboa/admin/migrations/0010_fill_alias_domain.py rename to modoboa/extensions/admin/migrations/0010_fill_alias_domain.py diff --git a/modoboa/admin/migrations/0011_auto__add_objectdates__add_field_domain_dates__add_field_domainalias_d.py b/modoboa/extensions/admin/migrations/0011_auto__add_objectdates__add_field_domain_dates__add_field_domainalias_d.py similarity index 100% rename from modoboa/admin/migrations/0011_auto__add_objectdates__add_field_domain_dates__add_field_domainalias_d.py rename to modoboa/extensions/admin/migrations/0011_auto__add_objectdates__add_field_domain_dates__add_field_domainalias_d.py diff --git a/modoboa/admin/migrations/0012_fill_object_dates.py b/modoboa/extensions/admin/migrations/0012_fill_object_dates.py similarity index 100% rename from modoboa/admin/migrations/0012_fill_object_dates.py rename to modoboa/extensions/admin/migrations/0012_fill_object_dates.py diff --git a/modoboa/admin/migrations/0013_update_mailbox_path.py b/modoboa/extensions/admin/migrations/0013_update_mailbox_path.py similarity index 100% rename from modoboa/admin/migrations/0013_update_mailbox_path.py rename to modoboa/extensions/admin/migrations/0013_update_mailbox_path.py diff --git a/modoboa/admin/migrations/0014_auto__add_unique_domain_name.py b/modoboa/extensions/admin/migrations/0014_auto__add_unique_domain_name.py similarity index 100% rename from modoboa/admin/migrations/0014_auto__add_unique_domain_name.py rename to modoboa/extensions/admin/migrations/0014_auto__add_unique_domain_name.py diff --git a/modoboa/admin/migrations/0015_move_passwords.py b/modoboa/extensions/admin/migrations/0015_move_passwords.py similarity index 100% rename from modoboa/admin/migrations/0015_move_passwords.py rename to modoboa/extensions/admin/migrations/0015_move_passwords.py diff --git a/modoboa/admin/migrations/0016_auto__add_objectaccess__add_unique_objectaccess_user_content_type_obje.py b/modoboa/extensions/admin/migrations/0016_auto__add_objectaccess__add_unique_objectaccess_user_content_type_obje.py similarity index 100% rename from modoboa/admin/migrations/0016_auto__add_objectaccess__add_unique_objectaccess_user_content_type_obje.py rename to modoboa/extensions/admin/migrations/0016_auto__add_objectaccess__add_unique_objectaccess_user_content_type_obje.py diff --git a/modoboa/admin/migrations/0017_set_permissions.py b/modoboa/extensions/admin/migrations/0017_set_permissions.py similarity index 100% rename from modoboa/admin/migrations/0017_set_permissions.py rename to modoboa/extensions/admin/migrations/0017_set_permissions.py diff --git a/modoboa/admin/migrations/0018_auto__add_unique_alias_domain_address.py b/modoboa/extensions/admin/migrations/0018_auto__add_unique_alias_domain_address.py similarity index 100% rename from modoboa/admin/migrations/0018_auto__add_unique_alias_domain_address.py rename to modoboa/extensions/admin/migrations/0018_auto__add_unique_alias_domain_address.py diff --git a/modoboa/admin/migrations/0019_set_default_group.py b/modoboa/extensions/admin/migrations/0019_set_default_group.py similarity index 100% rename from modoboa/admin/migrations/0019_set_default_group.py rename to modoboa/extensions/admin/migrations/0019_set_default_group.py diff --git a/modoboa/admin/migrations/0020_rename_amavis_ext.py b/modoboa/extensions/admin/migrations/0020_rename_amavis_ext.py similarity index 100% rename from modoboa/admin/migrations/0020_rename_amavis_ext.py rename to modoboa/extensions/admin/migrations/0020_rename_amavis_ext.py diff --git a/modoboa/admin/migrations/0021_default_superadmin_ownership.py b/modoboa/extensions/admin/migrations/0021_default_superadmin_ownership.py similarity index 100% rename from modoboa/admin/migrations/0021_default_superadmin_ownership.py rename to modoboa/extensions/admin/migrations/0021_default_superadmin_ownership.py diff --git a/modoboa/admin/migrations/0022_auto__del_field_mailbox_uid__del_field_mailbox_gid__del_field_mailbox_.py b/modoboa/extensions/admin/migrations/0022_auto__del_field_mailbox_uid__del_field_mailbox_gid__del_field_mailbox_.py similarity index 100% rename from modoboa/admin/migrations/0022_auto__del_field_mailbox_uid__del_field_mailbox_gid__del_field_mailbox_.py rename to modoboa/extensions/admin/migrations/0022_auto__del_field_mailbox_uid__del_field_mailbox_gid__del_field_mailbox_.py diff --git a/modoboa/admin/migrations/0023_auto__add_quota.py b/modoboa/extensions/admin/migrations/0023_auto__add_quota.py similarity index 100% rename from modoboa/admin/migrations/0023_auto__add_quota.py rename to modoboa/extensions/admin/migrations/0023_auto__add_quota.py diff --git a/modoboa/admin/migrations/0024_create_quotas.py b/modoboa/extensions/admin/migrations/0024_create_quotas.py similarity index 100% rename from modoboa/admin/migrations/0024_create_quotas.py rename to modoboa/extensions/admin/migrations/0024_create_quotas.py diff --git a/modoboa/admin/migrations/0025_auto__add_field_quota_mbox.py b/modoboa/extensions/admin/migrations/0025_auto__add_field_quota_mbox.py similarity index 100% rename from modoboa/admin/migrations/0025_auto__add_field_quota_mbox.py rename to modoboa/extensions/admin/migrations/0025_auto__add_field_quota_mbox.py diff --git a/modoboa/admin/migrations/0026_set_quota_mbox.py b/modoboa/extensions/admin/migrations/0026_set_quota_mbox.py similarity index 100% rename from modoboa/admin/migrations/0026_set_quota_mbox.py rename to modoboa/extensions/admin/migrations/0026_set_quota_mbox.py diff --git a/modoboa/admin/migrations/0027_auto__add_field_mailbox_use_domain_quota.py b/modoboa/extensions/admin/migrations/0027_auto__add_field_mailbox_use_domain_quota.py similarity index 100% rename from modoboa/admin/migrations/0027_auto__add_field_mailbox_use_domain_quota.py rename to modoboa/extensions/admin/migrations/0027_auto__add_field_mailbox_use_domain_quota.py diff --git a/modoboa/admin/migrations/0028_auto__chg_field_mailbox_quota.py b/modoboa/extensions/admin/migrations/0028_auto__chg_field_mailbox_quota.py similarity index 100% rename from modoboa/admin/migrations/0028_auto__chg_field_mailbox_quota.py rename to modoboa/extensions/admin/migrations/0028_auto__chg_field_mailbox_quota.py diff --git a/modoboa/admin/migrations/0029_auto__add_user__chg_field_mailbox_user__chg_field_objectaccess_user.py b/modoboa/extensions/admin/migrations/0029_auto__add_user__chg_field_mailbox_user__chg_field_objectaccess_user.py similarity index 100% rename from modoboa/admin/migrations/0029_auto__add_user__chg_field_mailbox_user__chg_field_objectaccess_user.py rename to modoboa/extensions/admin/migrations/0029_auto__add_user__chg_field_mailbox_user__chg_field_objectaccess_user.py diff --git a/modoboa/admin/migrations/0030_update_permissions.py b/modoboa/extensions/admin/migrations/0030_update_permissions.py similarity index 100% rename from modoboa/admin/migrations/0030_update_permissions.py rename to modoboa/extensions/admin/migrations/0030_update_permissions.py diff --git a/modoboa/admin/migrations/0031_auto__add_aliases_to_alias.py b/modoboa/extensions/admin/migrations/0031_auto__add_aliases_to_alias.py similarity index 100% rename from modoboa/admin/migrations/0031_auto__add_aliases_to_alias.py rename to modoboa/extensions/admin/migrations/0031_auto__add_aliases_to_alias.py diff --git a/modoboa/admin/migrations/0032_auto__chg_field_quota_bytes.py b/modoboa/extensions/admin/migrations/0032_auto__chg_field_quota_bytes.py similarity index 100% rename from modoboa/admin/migrations/0032_auto__chg_field_quota_bytes.py rename to modoboa/extensions/admin/migrations/0032_auto__chg_field_quota_bytes.py diff --git a/modoboa/admin/migrations/0033_auto__chg_field_mailbox_address__chg_field_quota_username.py b/modoboa/extensions/admin/migrations/0033_auto__chg_field_mailbox_address__chg_field_quota_username.py similarity index 100% rename from modoboa/admin/migrations/0033_auto__chg_field_mailbox_address__chg_field_quota_username.py rename to modoboa/extensions/admin/migrations/0033_auto__chg_field_mailbox_address__chg_field_quota_username.py diff --git a/modoboa/admin/migrations/0034_auto__add_field_user_is_local.py b/modoboa/extensions/admin/migrations/0034_auto__add_field_user_is_local.py similarity index 100% rename from modoboa/admin/migrations/0034_auto__add_field_user_is_local.py rename to modoboa/extensions/admin/migrations/0034_auto__add_field_user_is_local.py diff --git a/modoboa/admin/migrations/0035_set_local_flag.py b/modoboa/extensions/admin/migrations/0035_set_local_flag.py similarity index 100% rename from modoboa/admin/migrations/0035_set_local_flag.py rename to modoboa/extensions/admin/migrations/0035_set_local_flag.py diff --git a/modoboa/admin/migrations/__init__.py b/modoboa/extensions/admin/migrations/__init__.py similarity index 100% rename from modoboa/admin/migrations/__init__.py rename to modoboa/extensions/admin/migrations/__init__.py diff --git a/modoboa/extensions/admin/models/__init__.py b/modoboa/extensions/admin/models/__init__.py new file mode 100644 index 000000000..8ef79509f --- /dev/null +++ b/modoboa/extensions/admin/models/__init__.py @@ -0,0 +1,8 @@ +from .domain import Domain +from .domain_alias import DomainAlias +from .mailbox import Mailbox, Quota +from .alias import Alias + +__all__ = [ + 'Domain', 'DomainAlias', 'Mailbox', 'Quota', 'Alias' +] diff --git a/modoboa/extensions/admin/models/alias.py b/modoboa/extensions/admin/models/alias.py new file mode 100644 index 000000000..ee1607cd1 --- /dev/null +++ b/modoboa/extensions/admin/models/alias.py @@ -0,0 +1,195 @@ +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 .domain import Domain +from .mailbox import Mailbox + + +class Alias(DatesAware): + address = models.CharField( + ugettext_lazy('address'), max_length=254, + help_text=ugettext_lazy("The alias address (without the domain part). For a 'catch-all' address, just enter an * character.") + ) + domain = models.ForeignKey(Domain) + mboxes = models.ManyToManyField( + Mailbox, verbose_name=ugettext_lazy('mailboxes'), + help_text=ugettext_lazy("The mailboxes this alias points to") + ) + aliases = models.ManyToManyField( + 'Alias', + help_text=ugettext_lazy("The aliases this alias points to") + ) + extmboxes = models.TextField(blank=True) + enabled = models.BooleanField( + ugettext_lazy('enabled'), + help_text=ugettext_lazy("Check to activate this alias") + ) + + class Meta: + permissions = ( + ("view_aliases", "View aliases"), + ) + unique_together = (("address", "domain"),) + ordering = ["domain__name", "address"] + + def __unicode__(self): + return self.full_address + + @property + def full_address(self): + return "%s@%s" % (self.address, self.domain.name) + + @property + def identity(self): + return self.full_address + + @property + def name_or_rcpt(self): + rcpts_count = self.get_recipients_count() + if not rcpts_count: + return "---" + rcpts = self.get_recipients() + if rcpts_count > 1: + return "%s, ..." % rcpts[0] + return rcpts[0] + + @property + def type(self): + cpt = self.get_recipients_count() + if cpt > 1: + return "dlist" + if self.extmboxes != "": + return "forward" + return "alias" + + @property + def tags(self): + labels = { + "dlist": _("distribution list"), + "forward": _("forward"), + "alias": _("alias") + } + altype = self.type + return [{"name": altype, "label": labels[altype], "type": "idt"}] + + def post_create(self, creator): + from modoboa.lib.permissions import grant_access_to_object + grant_access_to_object(creator, self, is_owner=True) + events.raiseEvent("MailboxAliasCreated", creator, self) + if creator.is_superuser: + for admin in self.domain.admins: + grant_access_to_object(admin, self) + + def save(self, int_rcpts, ext_rcpts, *args, **kwargs): + if len(ext_rcpts): + self.extmboxes = ",".join(ext_rcpts) + else: + self.extmboxes = "" + 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: + if isinstance(t, Alias): + if not t in curaliases: + self.aliases.add(t) + continue + if not t in curmboxes: + self.mboxes.add(t) + for t in curaliases: + if not t in int_rcpts: + self.aliases.remove(t) + for t in curmboxes: + 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 + + Internal and external addresses are mixed into a single list. + """ + result = [al.full_address for al in self.aliases.all()] + result += [mb.full_address for mb in self.mboxes.all()] + if self.extmboxes != "": + result += self.extmboxes.split(',') + return result + + def get_recipients_count(self): + total = 0 + if self.extmboxes != "": + total += len(self.extmboxes.split(',')) + return total + self.aliases.count() + self.mboxes.count() + + def ui_disabled(self, user): + if user.is_superuser: + return False + for mb in self.mboxes.all(): + if not user.is_owner(mb.domain): + return True + return False + + def from_csv(self, user, row, expected_elements=5): + """Create a new alias from a CSV file entry + + """ + if len(row) < expected_elements: + raise AdminError(_("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)) + if not user.can_access(domain): + raise PermDeniedException + self.address = localpart + self.domain = domain + self.enabled = (row[2].strip() == 'True') + int_rcpts = [] + ext_rcpts = [] + for rcpt in row[3:]: + rcpt = rcpt.strip() + localpart, domname = split_mailbox(rcpt) + try: + Domain.objects.get(name=domname) + except Domain.DoesNotExist: + ext_rcpts += [rcpt] + continue + try: + target = Alias.objects.get(domain__name=domname, address=localpart) + if target.full_address == self.full_address: + target = None + except Alias.DoesNotExist: + target = None + if target is None: + try: + target = Mailbox.objects.get(address=localpart, + domain__name=domname) + except Mailbox.DoesNotExist: + raise AdminError(_("Local recipient %s not found" % rcpt)) + int_rcpts += [target] + self.save(int_rcpts, ext_rcpts, creator=user) + + def to_csv(self, csvwriter): + row = [self.type, self.full_address, self.enabled] + row += self.get_recipients() + csvwriter.writerow(row) + +reversion.register(Alias) diff --git a/modoboa/extensions/admin/models/base.py b/modoboa/extensions/admin/models/base.py new file mode 100644 index 000000000..1f6b0a152 --- /dev/null +++ b/modoboa/extensions/admin/models/base.py @@ -0,0 +1,49 @@ +from django.db import models + + +class ObjectDates(models.Model): + """Dates recording for admin objects + + This table keeps creation and last modification dates for Domains, + domain aliases, mailboxes and aliases objects. + """ + creation = models.DateTimeField(auto_now_add=True) + last_modification = models.DateTimeField(auto_now=True) + + @staticmethod + def set_for_object(obj): + """Initialize or update dates for a given object. + + :param obj: an admin object (Domain, Mailbox, etc) + """ + try: + dates = getattr(obj, "dates") + except ObjectDates.DoesNotExist: + dates = ObjectDates() + dates.save() + obj.dates = dates + + +class DatesAware(models.Model): + """Abstract model to support dates + + Inherit from this model to automatically add the "dates" feature + to another model. It defines the appropriate field and handles + saves. + """ + dates = models.ForeignKey(ObjectDates) + + 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 + + @property + def last_modification(self): + return self.dates.last_modification diff --git a/modoboa/extensions/admin/models/domain.py b/modoboa/extensions/admin/models/domain.py new file mode 100644 index 000000000..559fc7909 --- /dev/null +++ b/modoboa/extensions/admin/models/domain.py @@ -0,0 +1,133 @@ +import reversion +from django.db import models +from django.utils.translation import ugettext as _, ugettext_lazy +from django.contrib.contenttypes import generic +from modoboa.lib import events, parameters +from modoboa.core.models import ObjectAccess +from .base import DatesAware + + +class Domain(DatesAware): + name = models.CharField(ugettext_lazy('name'), max_length=100, unique=True, + help_text=ugettext_lazy("The domain name")) + quota = models.IntegerField( + help_text=ugettext_lazy("Default quota in MB applied to mailboxes") + ) + enabled = models.BooleanField( + ugettext_lazy('enabled'), + help_text=ugettext_lazy("Check to activate this domain") + ) + owners = generic.GenericRelation(ObjectAccess) + + class Meta: + permissions = ( + ("view_domain", "View domain"), + ("view_domains", "View domains"), + ) + ordering = ["name"] + + @property + def domainalias_count(self): + return self.domainalias_set.count() + + @property + def mailbox_count(self): + return self.mailbox_set.count() + + @property + def mbalias_count(self): + return self.alias_set.count() + + @property + def admins(self): + """Return the domain administrators of this domain + + :return: a list of User objects + """ + return [oa.user for oa in self.owners.filter(user__is_superuser=False)] + + def add_admin(self, account): + """Add a new administrator for this domain + + :param User account: the administrotor to add + """ + from modoboa.lib.permissions import grant_access_to_object + grant_access_to_object(account, self) + for mb in self.mailbox_set.all(): + if mb.user.has_perm("admin.add_domain"): + continue + grant_access_to_object(account, mb) + grant_access_to_object(account, mb.user) + for al in self.alias_set.all(): + grant_access_to_object(account, al) + + def remove_admin(self, account): + """Remove an administrator for this domain + + :param User account: the administrotor to remove + """ + from modoboa.lib.permissions import ungrant_access_to_object + ungrant_access_to_object(self, account) + for mb in self.mailbox_set.all(): + if mb.user.has_perm("admin.add_domain"): + continue + ungrant_access_to_object(mb, account) + ungrant_access_to_object(mb.user, account) + for al in self.alias_set.all(): + ungrant_access_to_object(al, account) + + def delete(self, fromuser, keepdir=False): + from modoboa.lib.permissions import \ + ungrant_access_to_object, ungrant_access_to_objects + + 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()) + hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) + if hm == "yes" and not keepdir: + for mb in self.mailbox_set.all(): + mb.delete_dir() + if self.alias_set.count(): + events.raiseEvent("MailboxAliasDelete", 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) + 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")) + self.name = row[1].strip() + self.quota = int(row[2].strip()) + self.enabled = (row[3].strip() == 'True') + self.save(creator=user) + + def to_csv(self, csvwriter): + csvwriter.writerow(["domain", self.name, self.quota, self.enabled]) + +reversion.register(Domain) diff --git a/modoboa/extensions/admin/models/domain_alias.py b/modoboa/extensions/admin/models/domain_alias.py new file mode 100644 index 000000000..0bdf49694 --- /dev/null +++ b/modoboa/extensions/admin/models/domain_alias.py @@ -0,0 +1,78 @@ +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 .domain import Domain + + +class DomainAlias(DatesAware): + name = models.CharField(ugettext_lazy("name"), max_length=100, unique=True, + help_text=ugettext_lazy("The alias name")) + target = models.ForeignKey( + Domain, verbose_name=ugettext_lazy('target'), + help_text=ugettext_lazy("The domain this alias points to") + ) + enabled = models.BooleanField( + ugettext_lazy('enabled'), + help_text=ugettext_lazy("Check to activate this alias") + ) + + class Meta: + permissions = ( + ("view_domaliases", "View domain aliases"), + ) + + 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 + + Expected format: ["domainalias", domain alias name, targeted domain, enabled] + + :param user: a ``User`` object + :param row: a list containing the alias definition + """ + if len(row) < 4: + raise AdminError(_("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)) + self.enabled = row[3].strip() == 'True' + self.save(creator=user) + + def to_csv(self, csvwriter): + """Export a domain alias using CSV format + + :param csvwriter: a ``csv.writer`` object + """ + csvwriter.writerow(["domainalias", self.name, + self.target.name, self.enabled]) + +reversion.register(DomainAlias) diff --git a/modoboa/extensions/admin/models/mailbox.py b/modoboa/extensions/admin/models/mailbox.py new file mode 100644 index 000000000..287034216 --- /dev/null +++ b/modoboa/extensions/admin/models/mailbox.py @@ -0,0 +1,213 @@ +import os +import reversion +from django.db import models +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.sysutils import exec_cmd +from modoboa.core.models import User +from modoboa.extensions.admin.exceptions import AdminError +from .base import DatesAware +from .domain import Domain + + +class Mailbox(DatesAware): + address = models.CharField( + ugettext_lazy('address'), max_length=252, + help_text=ugettext_lazy("Mailbox address (without the @domain.tld part)") + ) + quota = models.PositiveIntegerField() + use_domain_quota = models.BooleanField(default=False) + domain = models.ForeignKey(Domain) + user = models.ForeignKey(User) + + class Meta: + permissions = ( + ("view_mailboxes", "View mailboxes"), + ) + + def __init__(self, *args, **kwargs): + super(Mailbox, self).__init__(*args, **kwargs) + self.__mail_home = None + + def __str__(self): + return self.full_address + + def __full_address(self, localpart): + return "%s@%s" % (localpart, self.domain.name) + + @property + def full_address(self): + return self.__full_address(self.address) + + @property + def enabled(self): + return self.user.is_active + + @property + def alias_count(self): + return self.alias_set.count() + + @property + def mail_home(self): + """ + + """ + hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) + if hm is None or hm == "no": + return None + if self.__mail_home is None: + code, output = exec_cmd("doveadm user %s -f home" % self.full_address, + sudo_user=parameters.get_admin("MAILBOXES_OWNER")) + if code: + raise AdminError(_("Failed to retrieve mailbox location (%s)" % output)) + self.__mail_home = output.strip() + return self.__mail_home + + def rename_dir(self, old_mail_home): + hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) + if hm is None or hm == "no": + return + self.__mail_home = None + if not os.path.exists(old_mail_home): + return + code, output = exec_cmd( + "mv %s %s" % (old_mail_home, self.mail_home), + sudo_user=parameters.get_admin("MAILBOXES_OWNER") + ) + if code: + raise AdminError(_("Failed to rename mailbox: %s" % output)) + + def rename(self, address, domain): + """Rename the mailbox + + :param string address: the new mailbox's address (local part) + :param Domain domain: the new mailbox's domain + """ + old_mail_home = self.mail_home + qs = Quota.objects.filter(username=self.full_address) + self.address = address + self.domain = domain + self.rename_dir(old_mail_home) + qs.update(username=self.full_address) + + def delete_dir(self): + hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) + if hm is None or hm == "no": + return + if not os.path.exists(self.mail_home): + return + code, output = exec_cmd( + "rm -r %s" % self.mail_home, + sudo_user=parameters.get_admin("MAILBOXES_OWNER") + ) + if code: + raise AdminError(_("Failed to remove mailbox: %s" % output)) + + def set_quota(self, value=None, override_rules=False): + """Set or update quota's value for this mailbox. + + :param integer value: the quota's value + :param bool override_rules: allow to override defined quota rules + """ + if value is None: + if self.use_domain_quota: + self.quota = self.domain.quota + else: + self.quota = 0 + elif int(value) > self.domain.quota and not override_rules: + raise AdminError( + _("Quota is greater than the allowed domain's limit (%dM)" % self.domain.quota) + ) + else: + self.quota = value + if not self.quota and not override_rules: + raise AdminError(_("A quota is required")) + + def get_quota(self): + q = Quota.objects.get(username=self.full_address) + return int(q.bytes / 1048576) + + def get_quota_in_percent(self): + if not self.quota: + return 0 + q = Quota.objects.get(username=self.full_address) + return int(q.bytes / float(self.quota * 1048576) * 100) + + 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) + if creator.is_superuser and not self.user.has_perm("admin.add_domain"): + # A super user is creating a new mailbox. Give + # access to that mailbox (and the associated + # account) to the appropriate domain admins, + # except if the new account has a more important + # role (SuperAdmin, Reseller) + for admin in self.domain.admins: + grant_access_to_object(admin, self) + grant_access_to_object(admin, self.user) + + def save(self, *args, **kwargs): + 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: + Quota.objects.create(mbox=self, username=self.full_address) + + def delete(self, keepdir=False): + """Custom delete method + + We try to delete the associated quota in the same time (it may + has already been removed if we're deleting a domain). + + :param bool keepdir: delete the mailbox home dir on the filesystem or not + """ + try: + q = Quota.objects.get(username=self.full_address) + except Quota.DoesNotExist: + pass + else: + q.delete() + if not keepdir: + self.delete_dir() + super(Mailbox, self).delete() + +reversion.register(Mailbox) + + +@receiver(pre_delete, sender=Mailbox) +def mailbox_deleted_handler(sender, **kwargs): + """``Mailbox`` pre_delete signal receiver + + In order to properly handle deletions (ie. we don't want to leave + orphan records into the db), we define this custom receiver. + + It manually removes the mailbox from the aliases it is linked to + and then remove all empty aliases. + """ + from modoboa.lib.permissions import ungrant_access_to_object + + mb = kwargs['instance'] + events.raiseEvent("DeleteMailbox", mb) + ungrant_access_to_object(mb) + for alias in mb.alias_set.all(): + alias.mboxes.remove(mb) + if alias.mboxes.count() == 0: + alias.delete() + + +class Quota(models.Model): + username = models.EmailField(primary_key=True, max_length=254) + bytes = models.BigIntegerField(default=0) + messages = models.IntegerField(default=0) + + mbox = models.OneToOneField(Mailbox, related_name="quota_value", null=True) diff --git a/modoboa/admin/static/admin/css/admin.css b/modoboa/extensions/admin/static/admin/css/admin.css similarity index 100% rename from modoboa/admin/static/admin/css/admin.css rename to modoboa/extensions/admin/static/admin/css/admin.css diff --git a/modoboa/admin/static/admin/css/settings.css b/modoboa/extensions/admin/static/admin/css/settings.css similarity index 100% rename from modoboa/admin/static/admin/css/settings.css rename to modoboa/extensions/admin/static/admin/css/settings.css diff --git a/modoboa/admin/static/admin/js/admin.js b/modoboa/extensions/admin/static/admin/js/admin.js similarity index 100% rename from modoboa/admin/static/admin/js/admin.js rename to modoboa/extensions/admin/static/admin/js/admin.js diff --git a/modoboa/admin/tables.py b/modoboa/extensions/admin/tables.py similarity index 84% rename from modoboa/admin/tables.py rename to modoboa/extensions/admin/tables.py index 3be7b989a..6ab552161 100644 --- a/modoboa/admin/tables.py +++ b/modoboa/extensions/admin/tables.py @@ -1,7 +1,8 @@ # coding: utf-8 from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.lib import tables -from templatetags.admin_extras import * +from templatetags.admin_tags import domain_actions + class DomainsTable(tables.Table): tableid = "objects_table" @@ -9,8 +10,8 @@ class DomainsTable(tables.Table): styles = "table" name = tables.LinkColumn( - "name", label=ugettext_lazy("Name"), - urlpattern="modoboa.admin.views.editdomain", + "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" ) @@ -18,7 +19,7 @@ class DomainsTable(tables.Table): "domainalias_set", label=ugettext_lazy("Alias(es)"), safe=True, sortable=True, sort_order="domainalias__name" ) - actions = tables.ActionColumn("actions", label=ugettext_lazy("Actions"), + actions = tables.ActionColumn("actions", label=ugettext_lazy("Actions"), defvalue=domain_actions) cols_order = ["name", "domaliases", "actions"] @@ -27,7 +28,6 @@ class DomainsTable(tables.Table): super(DomainsTable, self).__init__(request) self.populate(self._rows_from_model(doms)) - def parse_domainalias_set(self, aliases): if not len(aliases.all()): return "---" @@ -41,11 +41,14 @@ class DomainsTable(tables.Table): return "muted" return "" + class ExtensionsTable(tables.Table): idkey = "id" selection = tables.SelectionColumn("selection", width="4%", header=False) label = tables.Column("label", label=ugettext_lazy("Name"), width="15%") - version = tables.Column("version", label=ugettext_lazy("Version"), width="6%") + version = tables.Column( + "version", label=ugettext_lazy("Version"), width="6%" + ) descr = tables.Column("description", label=ugettext_lazy("Description")) - + cols_order = ["selection", "label", "version", "descr"] diff --git a/modoboa/admin/templates/admin/dlistform.html b/modoboa/extensions/admin/templates/admin/dlistform.html similarity index 100% rename from modoboa/admin/templates/admin/dlistform.html rename to modoboa/extensions/admin/templates/admin/dlistform.html diff --git a/modoboa/admin/templates/admin/domain_general_form.html b/modoboa/extensions/admin/templates/admin/domain_general_form.html similarity index 100% rename from modoboa/admin/templates/admin/domain_general_form.html rename to modoboa/extensions/admin/templates/admin/domain_general_form.html diff --git a/modoboa/admin/templates/admin/domain_options_form.html b/modoboa/extensions/admin/templates/admin/domain_options_form.html similarity index 100% rename from modoboa/admin/templates/admin/domain_options_form.html rename to modoboa/extensions/admin/templates/admin/domain_options_form.html diff --git a/modoboa/admin/templates/admin/domains.html b/modoboa/extensions/admin/templates/admin/domains.html similarity index 79% rename from modoboa/admin/templates/admin/domains.html rename to modoboa/extensions/admin/templates/admin/domains.html index a4b05f1e1..c9c1cd418 100644 --- a/modoboa/admin/templates/admin/domains.html +++ b/modoboa/extensions/admin/templates/admin/domains.html @@ -1,4 +1,4 @@ -{% extends "admin/listing.html" %}{% load i18n admin_extras libextras %} +{% extends "admin/listing.html" %}{% load i18n admin_tags libextras %} {% block pagetitle %}{% trans "Domains" %}{% endblock %} {% block html_head %} {{ block.super }} diff --git a/modoboa/admin/templates/admin/editdomainform.html b/modoboa/extensions/admin/templates/admin/editdomainform.html similarity index 100% rename from modoboa/admin/templates/admin/editdomainform.html rename to modoboa/extensions/admin/templates/admin/editdomainform.html diff --git a/modoboa/admin/templates/admin/extensions.html b/modoboa/extensions/admin/templates/admin/extensions.html similarity index 100% rename from modoboa/admin/templates/admin/extensions.html rename to modoboa/extensions/admin/templates/admin/extensions.html diff --git a/modoboa/admin/templates/admin/header.html b/modoboa/extensions/admin/templates/admin/header.html similarity index 100% rename from modoboa/admin/templates/admin/header.html rename to modoboa/extensions/admin/templates/admin/header.html diff --git a/modoboa/admin/templates/admin/identities.html b/modoboa/extensions/admin/templates/admin/identities.html similarity index 60% rename from modoboa/admin/templates/admin/identities.html rename to modoboa/extensions/admin/templates/admin/identities.html index 3c120f874..e19b57331 100644 --- a/modoboa/admin/templates/admin/identities.html +++ b/modoboa/extensions/admin/templates/admin/identities.html @@ -1,23 +1,23 @@ -{% extends "admin/listing.html" %}{% load admin_extras libextras i18n %}{% load url from future %} +{% extends "admin/listing.html" %}{% load admin_tags libextras i18n %}{% load url from future %} {% block pagetitle %}{% trans "Identities" %}{% endblock %} {% block html_head %} {{ block.super }} - - - - -{% endblock %} -{% block leftcol %}{% endblock %} -{% block apparea %}
{% endblock %} diff --git a/modoboa/extensions/admin/templatetags/admin_tags.py b/modoboa/extensions/admin/templatetags/admin_tags.py index 3a13c4fe7..2cc789343 100644 --- a/modoboa/extensions/admin/templatetags/admin_tags.py +++ b/modoboa/extensions/admin/templatetags/admin_tags.py @@ -106,19 +106,19 @@ def identities_menu(user): @register.simple_tag def domain_actions(user, domid): - from modoboa.admin.models import Domain + from modoboa.extensions.admin.models import Domain domain = Domain.objects.get(pk=domid) actions = [ {"name": "listidentities", - "url": reverse("modoboa.admin.views.identities") + "#list/?searchquery=@%s" % domain.name, + "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.admin.views.deldomain", args=[domid]), + "url": reverse("modoboa.extensions.admin.views.domain.deldomain", args=[domid]), "title": _("Delete the domain"), "img": "icon-trash" }) @@ -132,72 +132,78 @@ def identity_actions(user, ident): if name == "User": actions = events.raiseQueryEvent("ExtraAccountActions", ident) actions += [ - {"name" : "delaccount", - "url" : reverse("modoboa.admin.views.delaccount", args=[objid]), - "img" : "icon-trash", - "title" : _("Delete this account")}, - ] + {"name": "delaccount", + "url": reverse("modoboa.extensions.admin.views.identity.delaccount", args=[objid]), + "img": "icon-trash", + "title": _("Delete this account")}, + ] else: if ident.get_recipients_count() >= 2: actions = [ - {"name" : "deldlist", - "url" : reverse("modoboa.admin.views.deldlist") + "?selection=%s" % objid, - "img" : "icon-trash", - "title" : _("Delete this distribution list")}, - ] + {"name": "deldlist", + "url": reverse("modoboa.extensions.admin.views.alias.deldlist") + "?selection=%s" % objid, + "img": "icon-trash", + "title": _("Delete this distribution list")}, + ] elif ident.extmboxes != "": actions = [ - {"name" : "delforward", - "url" : reverse("modoboa.admin.views.delforward") + "?selection=%s" % objid, - "img" : "icon-trash", - "title" : _("Delete this forward")}, - ] + {"name": "delforward", + "url": reverse("modoboa.extensions.admin.views.alias.delforward") + "?selection=%s" % objid, + "img": "icon-trash", + "title": _("Delete this forward")}, + ] else: actions = [ - {"name" : "delalias", - "url" : reverse("modoboa.admin.views.delalias") + "?selection=%s" % objid, - "img" : "icon-trash", - "title" : _("Delete this alias")}, - ] + {"name": "delalias", + "url": reverse("modoboa.extensions.admin.views.alias.delalias") + "?selection=%s" % objid, + "img": "icon-trash", + "title": _("Delete this alias")}, + ] return render_actions(actions) @register.simple_tag def identity_modify_link(identity, active_tab='default'): - linkdef = {"label" : identity.identity, "modal" : True} + linkdef = {"label": identity.identity, "modal": True} if identity.__class__.__name__ == "User": - linkdef["url"] = reverse("modoboa.admin.views.editaccount", args=[identity.id]) + linkdef["url"] = reverse( + "modoboa.extensions.admin.views.identity.editaccount", + args=[identity.id] + ) linkdef["url"] += "?active_tab=%s" % active_tab linkdef["modalcb"] = "admin.editaccount_cb" else: - linkdef["url"] = reverse("modoboa.admin.views.editalias", args=[identity.id]) + linkdef["url"] = reverse( + "modoboa.extensions.admin.views.alias.editalias", + args=[identity.id] + ) linkdef["modalcb"] = "admin.aliasform_cb" return render_link(linkdef) @register.simple_tag def domadmin_actions(daid, domid): - actions = [dict( - name="removeperm", - url=reverse("modoboa.admin.views.remove_permission") + "?domid=%s&daid=%s" % (domid, daid), - img="icon-trash", - title=_("Remove this permission") - )] + actions = [{ + "name": "removeperm", + "url": reverse("modoboa.extensions.admin.views.identity.remove_permission") + "?domid=%s&daid=%s" % (domid, daid), + "img": "icon-trash", + "title": _("Remove this permission") + }] return render_actions(actions) @register.simple_tag def loadadminextmenu(user): menu = events.raiseQueryEvent("AdminMenuDisplay", "admin_menu_box", user) - return render_to_string('common/menulist.html', - {"entries" : menu, "user" : user}) + return render_to_string('common/menulist.html', + {"entries": menu, "user": user}) @register.simple_tag def param(app, definition): name = "%s.%s" % (app, definition["name"]) - value = definition.has_key("value") \ - and definition["value"] or definition["deflt"] + value = definition["value"] \ + if "value" in definition else definition["deflt"] if definition["type"] == "separator": return "
%s
" % (unicode(definition["label"])) @@ -211,8 +217,7 @@ def param(app, definition): result = """
""" % (extratags, name, - definition.has_key("label") and _(definition["label"]) - or definition["name"]) + _(definition["label"]) if "label" in definition else definition["name"]) result += "
" if definition["type"] in ["string", "int"]: @@ -222,7 +227,7 @@ def param(app, definition): if definition["type"] == "text": result += "" \ % (name, name, value) - + if definition["type"] == "list_yesno": for idx, v in enumerate([("yes", _("Yes")), ("no", _("No"))]): checked = "checked" if value == v[0] else "" @@ -230,13 +235,14 @@ def param(app, definition): %(label)s -""" % dict(id="%s_%d" % (name, idx), name=name, value=v[0], label=v[1], checked=checked) +""" % dict(id="%s_%d" % (name, idx), name=name, value=v[0], + label=v[1], checked=checked) if definition["type"] == "list": result += """ """ - if definition.has_key("help"): + if "help" in definition: result += """ """ % (unicode(definition["help"]), _("Help")) @@ -264,7 +270,7 @@ def param(app, definition): @register.filter def gender(value, target): - if genders.has_key(value): + if value in genders: trans = target == "m" and genders[value][0] or genders[value][1] if trans.find("_") == -1: return trans @@ -273,5 +279,7 @@ def gender(value, target): @register.simple_tag def get_extra_admin_content(user, target, currentpage): - res = events.raiseQueryEvent("ExtraAdminContent", user, target, currentpage) + res = events.raiseQueryEvent( + "ExtraAdminContent", user, target, currentpage + ) return "".join(res) diff --git a/modoboa/extensions/admin/tests.py b/modoboa/extensions/admin/tests.py index 60d380a20..514a79f30 100644 --- a/modoboa/extensions/admin/tests.py +++ b/modoboa/extensions/admin/tests.py @@ -15,10 +15,10 @@ from django.test.client import Client from django.utils import simplejson from django.core.urlresolvers import reverse from django.core.files.base import ContentFile -from models import * -from modoboa.admin.lib import * from modoboa.lib import parameters from modoboa.lib.tests import ModoTestCase +from modoboa.extensions.admin.lib import * +from modoboa.extensions.admin.models import * class DomainTestCase(ModoTestCase): diff --git a/modoboa/extensions/admin/urls.py b/modoboa/extensions/admin/urls.py index ccb454700..799d270cf 100644 --- a/modoboa/extensions/admin/urls.py +++ b/modoboa/extensions/admin/urls.py @@ -3,6 +3,7 @@ from django.conf.urls import patterns, url urlpatterns = patterns( 'modoboa.extensions.admin.views.domain', + (r'^$', 'index'), url(r'^domains/$', 'domains', name="domains"), (r'^domains/list/$', '_domains'), (r'^domains/flatlist/$', 'domains_list'), diff --git a/modoboa/extensions/admin/views/domain.py b/modoboa/extensions/admin/views/domain.py index 65a4bd6b9..70e6824ee 100644 --- a/modoboa/extensions/admin/views/domain.py +++ b/modoboa/extensions/admin/views/domain.py @@ -22,13 +22,18 @@ from modoboa.extensions.admin.forms import ( ) +@login_required +def index(request): + return HttpResponseRedirect(reverse(domains)) + + @login_required @user_passes_test( lambda u: u.has_perm("admin.view_domains") or u.has_perm("admin.view_mailboxes") ) def _domains(request): sort_order, sort_dir = get_sort_order(request.GET, "name") - domains = request.user.get_domains() + domains = Domain.objects.get_for_admin(request.user) squery = request.GET.get("searchquery", None) if squery is not None: q = Q(name__contains=squery) @@ -60,7 +65,7 @@ def domains(request, tplname="admin/domains.html"): @login_required @permission_required("admin.add_user") def domains_list(request): - doms = [dom.name for dom in request.user.get_domains()] + doms = [dom.name for dom in Domain.objects.get_for_admin(request.user)] return ajax_simple_response(doms) diff --git a/modoboa/extensions/admin/views/export.py b/modoboa/extensions/admin/views/export.py index 6c238f78d..437e48173 100644 --- a/modoboa/extensions/admin/views/export.py +++ b/modoboa/extensions/admin/views/export.py @@ -8,6 +8,8 @@ from django.utils.translation import ugettext as _, ungettext 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.forms import ( ExportIdentitiesForm, ExportDomainsForm ) @@ -45,7 +47,7 @@ def export_identities(request): form.is_valid() fp = cStringIO.StringIO() csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) - for ident in request.user.get_identities(): + for ident in get_identities(request.user): ident.to_csv(csvwriter) content = fp.getvalue() fp.close() @@ -71,7 +73,7 @@ def export_domains(request): form.is_valid() fp = cStringIO.StringIO() csvwriter = csv.writer(fp, delimiter=form.cleaned_data["sepchar"]) - for dom in request.user.get_domains(): + for dom in Domain.objects.get_for_admin(request.user): dom.to_csv(csvwriter) for da in dom.domainalias_set.all(): da.to_csv(csvwriter) diff --git a/modoboa/extensions/admin/views/identity.py b/modoboa/extensions/admin/views/identity.py index edd18d570..8a12954f5 100644 --- a/modoboa/extensions/admin/views/identity.py +++ b/modoboa/extensions/admin/views/identity.py @@ -12,7 +12,10 @@ from modoboa.lib.webutils import ajax_simple_response, _render_to_string from modoboa.lib.formutils import CreationWizard from modoboa.lib.templatetags.libextras import pagination_bar from modoboa.core.models import User -from modoboa.extensions.admin.lib import get_sort_order, get_listing_page +from modoboa.extensions.admin.models import Mailbox +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 @@ -24,7 +27,7 @@ from modoboa.extensions.admin.forms import ( lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias") ) def _identities(request): - idents_list = request.user.get_identities(request.GET) + idents_list = get_identities(request.user, request.GET) sort_order, sort_dir = get_sort_order(request.GET, "identity", ["identity", "name_or_rcpt", "tags"]) if sort_order in ["identity", "name_or_rcpt"]: @@ -73,7 +76,9 @@ def list_quotas(request, tplname="admin/quotas.html"): from modoboa.lib.dbutils import db_type sort_order, sort_dir = get_sort_order(request.GET, "address") - mboxes = request.user.get_mailboxes(request.GET.get("searchquery", None)) + mboxes = Mailbox.objects.get_for_admin( + request.user, request.GET.get("searchquery", None) + ) mboxes = mboxes.exclude(quota=0) if sort_order in ["address", "quota", "quota_value__bytes"]: mboxes = mboxes.order_by("%s%s" % (sort_dir, sort_order)) @@ -175,7 +180,7 @@ def editaccount(request, accountid, tplname="common/tabforms.html"): if not request.user.can_access(account): raise PermDeniedException mb = None - if account.has_mailbox: + if account.mailbox_set.count(): mb = account.mailbox_set.all()[0] instances = dict(general=account, mail=mb, perms=account) diff --git a/modoboa/extensions/sievefilters/forms.py b/modoboa/extensions/sievefilters/forms.py index 5ee3724bf..41ef3b16a 100644 --- a/modoboa/extensions/sievefilters/forms.py +++ b/modoboa/extensions/sievefilters/forms.py @@ -5,7 +5,7 @@ from django.forms.widgets import RadioFieldRenderer, RadioSelect, RadioInput from django.utils.safestring import mark_safe from django.utils.html import conditional_escape from django.utils.encoding import force_unicode -from modoboa.admin.templatetags.admin_extras import gender +from modoboa.extensions.admin.templatetags.admin_tags import gender class FiltersSetForm(forms.Form): name = forms.CharField() diff --git a/modoboa/extensions/stats/__init__.py b/modoboa/extensions/stats/__init__.py index acaa695ce..61bb62101 100644 --- a/modoboa/extensions/stats/__init__.py +++ b/modoboa/extensions/stats/__init__.py @@ -22,7 +22,7 @@ class Stats(ModoExtension): def load(self): from app_settings import ParametersForm - events.registerEvent("GetGraphSets") + events.declare(["GetGraphSets"]) parameters.register(ParametersForm, ugettext_lazy("Graphical statistics")) def destroy(self): diff --git a/modoboa/lib/events.py b/modoboa/lib/events.py index ee3a81b8d..a296374cc 100644 --- a/modoboa/lib/events.py +++ b/modoboa/lib/events.py @@ -16,47 +16,27 @@ from django.conf import settings events = [ "CanCreate", - "CreateDomain", - "DomainModified", - "DeleteDomain", - - "DomainAliasCreated", - "DomainAliasDeleted", - - "CreateMailbox", - "DeleteMailbox", - "ModifyMailbox", - - "MailboxAliasCreated", - "MailboxAliasDeleted", - "AccountCreated", "AccountModified", "AccountDeleted", + "AccountExported", + "AccountImported", "PasswordUpdated", "ExtraAccountActions", - + "RoleChanged", + "GetExtraRoles", "PasswordChange", - + "UserMenuDisplay", "AdminMenuDisplay", "GetStaticContent", - "ExtraAccountForm", - "CheckExtraAccountForm", - "FillAccountInstances", - - "ExtraDomainForm", - "FillDomainInstances", - - "GetExtraRoles", - "ExtEnabled", "ExtDisabled", "UserLogin", "UserLogout", - + "GetAnnouncement", "TopNotifications", @@ -64,17 +44,20 @@ events = [ "ExtraUprefsRoutes", "ExtraUprefsJS" - ] +] callbacks = {} -def registerEvent(name): - """Register a new event - :param name: the event's name +def declare(nevents): + """Declare new events + + :param list nevents: a list of event names """ - if not name in events: - events.append(name) + for evt in nevents: + if not evt in events: + events.append(evt) + def register(event, callback): """Register a plugin callback for a specific event @@ -90,10 +73,11 @@ def register(event, callback): if not event in callbacks.keys(): callbacks[event] = {} fullname = "%s.%s" % (callback.__module__, callback.__name__) - if not callbacks[event].has_key(fullname): + if not fullname in callbacks[event]: callbacks[event][fullname] = callback return 1 + class observe(object): """Event observing decorator @@ -111,7 +95,7 @@ class observe(object): """ def __init__(self, *evtnames, **kwargs): self.evtnames = evtnames - if kwargs.has_key("extname"): + if "extname" in kwargs: self.extname = kwargs["extname"] def __guess_extension_name(self, modname): @@ -123,7 +107,9 @@ class observe(object): def __call__(self, f): modname = inspect.getmodule(inspect.stack()[1][0]).__name__ - extname = self.extname if hasattr(self, "extname") else self.__guess_extension_name(modname) + extname = self.extname if hasattr(self, "extname") \ + else self.__guess_extension_name(modname) + @wraps(f) def wrapped_f(*args): if extname: @@ -140,12 +126,12 @@ class observe(object): return [] elif not modname in settings.MODOBOA_APPS: return [] - return f(*args) - - map(lambda evt: register(evt, wrapped_f), self.evtnames) + for evt in self.evtnames: + register(evt, wrapped_f) return wrapped_f + def unregister(event, callback): """Unregister a callback for a specific event @@ -154,7 +140,7 @@ def unregister(event, callback): """ if not event in events: return False - if not callbacks.has_key(event): + if not event in callbacks: return False fullname = "%s.%s" % (callback.__module__, callback.__name__) try: @@ -162,6 +148,7 @@ def unregister(event, callback): except KeyError: pass + def raiseEvent(event, *args): """Raise a specific event @@ -172,10 +159,11 @@ def raiseEvent(event, *args): """ if not event in events or not event in callbacks.keys(): return 0 - for name, callback in callbacks[event].iteritems(): + for callback in callbacks[event].values(): callback(*args) return 1 + def raiseQueryEvent(event, *args): """Raise a specific event and wait for answers from callbacks @@ -187,10 +175,11 @@ def raiseQueryEvent(event, *args): result = [] if not event in events or not event in callbacks.keys(): return result - for name, callback in callbacks[event].iteritems(): + for callback in callbacks[event].values(): result += callback(*args) return result + def raiseDictEvent(event, *args): """Raise a specific event and return result as a dictionnary @@ -203,7 +192,7 @@ def raiseDictEvent(event, *args): result = {} if not event in events or not event in callbacks.keys(): return result - for name, callback in callbacks[event].iteritems(): + for callback in callbacks[event].values(): tmp = callback(*args) for k, v in tmp.iteritems(): result[k] = v diff --git a/modoboa/lib/permissions.py b/modoboa/lib/permissions.py index 0133eb49c..90c0eea9a 100644 --- a/modoboa/lib/permissions.py +++ b/modoboa/lib/permissions.py @@ -7,18 +7,10 @@ from modoboa.core.models import ObjectAccess import events from exceptions import ModoboaException -def get_content_type(obj): - """Simple function that use the right method to retrieve a content type - - :param obj: a django model - :return: a ``ContentType`` object - """ - return obj.get_content_type() if hasattr(obj, "get_content_type") \ - else ContentType.objects.get_for_model(obj) def get_account_roles(user): """Return the list of supported account roles - + This list can be extended by extensions which listen to the ``GetExtraRoles`` event. @@ -32,6 +24,7 @@ def get_account_roles(user): std_roles += events.raiseQueryEvent("GetExtraRoles", user) return sorted(std_roles, key=lambda role: role[1]) + def grant_access_to_object(user, obj, is_owner=False): """Grant access to an object for a given user @@ -48,7 +41,7 @@ def grant_access_to_object(user, obj, is_owner=False): :param obj: an admin. object (Domain, Mailbox, ...) :param is_owner: the user is the unique object's owner """ - ct = get_content_type(obj) + ct = ContentType.objects.get_for_model(obj) try: entry = user.objectaccess_set.get(content_type=ct, object_id=obj.id) entry.is_owner = is_owner @@ -62,7 +55,7 @@ def grant_access_to_object(user, obj, is_owner=False): ObjectAccess.objects.create(user=user, content_type=ct, object_id=obj.id, is_owner=is_owner) if is_owner: - from modoboa.admin.models import User + from modoboa.core.models import User for su in User.objects.filter(is_superuser=True): if su == user: continue @@ -74,6 +67,7 @@ def grant_access_to_object(user, obj, is_owner=False): except IntegrityError, e: raise ModoboaException(_("Failed to grant access (%s)" % str(e))) + def grant_access_to_objects(user, objects, ct): """Grant access to a collection of objects @@ -100,7 +94,7 @@ def ungrant_access_to_object(obj, user=None): :param obj: an object inheriting from ``models.Model`` :param user: a ``User`` object """ - ct = get_content_type(obj) + ct = ContentType.objects.get_for_model(obj) if user: try: ObjectAccess.objects.get(user=user, content_type=ct, object_id=obj.id).delete() @@ -109,22 +103,24 @@ def ungrant_access_to_object(obj, user=None): else: ObjectAccess.objects.filter(content_type=ct, object_id=obj.id).delete() + def ungrant_access_to_objects(objects): """Cancel all accesses for a given objects list :param objects: a list of objects inheriting from ``model.Model`` """ for obj in objects: - ct = get_content_type(obj) + ct = ContentType.objects.get_for_model(obj) ObjectAccess.objects.filter(content_type=ct, object_id=obj.id).delete() + def get_object_owner(obj): """Return the unique owner of this object :param obj: an object inheriting from ``model.Model`` :return: a ``User`` object """ - ct = get_content_type(obj) + ct = ContentType.objects.get_for_model(obj) try: entry = ObjectAccess.objects.get(content_type=ct, object_id=obj.id, is_owner=True) except ObjectAccess.DoesNotExist: From eabe53cea04d6a9a38e25fa84de4616aa216d16c Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 20 Aug 2013 16:32:39 +0200 Subject: [PATCH 05/97] wip: Big refactoring Limits extension OK. --- modoboa/extensions/admin/views/domain.py | 2 +- modoboa/extensions/admin/views/identity.py | 10 +++---- modoboa/extensions/limits/__init__.py | 33 ++++++++++++++-------- modoboa/extensions/limits/controls.py | 32 ++++++++++++++++----- modoboa/extensions/limits/forms.py | 6 ++-- modoboa/extensions/limits/lib.py | 12 ++------ modoboa/extensions/limits/models.py | 6 ++-- modoboa/extensions/limits/tests.py | 16 ++++++----- 8 files changed, 71 insertions(+), 46 deletions(-) diff --git a/modoboa/extensions/admin/views/domain.py b/modoboa/extensions/admin/views/domain.py index 70e6824ee..1fede9a73 100644 --- a/modoboa/extensions/admin/views/domain.py +++ b/modoboa/extensions/admin/views/domain.py @@ -63,7 +63,7 @@ def domains(request, tplname="admin/domains.html"): @login_required -@permission_required("admin.add_user") +@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) diff --git a/modoboa/extensions/admin/views/identity.py b/modoboa/extensions/admin/views/identity.py index 8a12954f5..6ca1410f9 100644 --- a/modoboa/extensions/admin/views/identity.py +++ b/modoboa/extensions/admin/views/identity.py @@ -24,7 +24,7 @@ from modoboa.extensions.admin.forms import ( @login_required @user_passes_test( - lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias") + lambda u: u.has_perm("core.add_user") or u.has_perm("admin.add_alias") ) def _identities(request): idents_list = get_identities(request.user, request.GET) @@ -62,7 +62,7 @@ def identities(request, tplname="admin/identities.html"): @login_required -@permission_required("admin.add_user") +@permission_required("core.add_user") def accounts_list(request): accs = User.objects.filter(is_superuser=False) \ .exclude(groups__name='SimpleUsers') @@ -107,7 +107,7 @@ def list_quotas(request, tplname="admin/quotas.html"): @login_required -@permission_required("admin.add_user") +@permission_required("core.add_user") @transaction.commit_on_success def newaccount(request, tplname='common/wizard_forms.html'): def create_account(steps): @@ -173,7 +173,7 @@ def newaccount(request, tplname='common/wizard_forms.html'): @login_required -@permission_required("admin.change_user") +@permission_required("core.change_user") @transaction.commit_on_success def editaccount(request, accountid, tplname="common/tabforms.html"): account = User.objects.get(pk=accountid) @@ -220,7 +220,7 @@ def editaccount(request, accountid, tplname="common/tabforms.html"): @login_required -@permission_required("admin.delete_user") +@permission_required("core.delete_user") @transaction.commit_on_success def delaccount(request, accountid): keepdir = True if request.POST.get("keepdir", "false") == "true" else False diff --git a/modoboa/extensions/limits/__init__.py b/modoboa/extensions/limits/__init__.py index 1b57fd5b7..657e9a43e 100644 --- a/modoboa/extensions/limits/__init__.py +++ b/modoboa/extensions/limits/__init__.py @@ -10,39 +10,42 @@ from django.contrib.contenttypes.models import ContentType from django.utils.translation import ugettext as _, ugettext_lazy from django.core.urlresolvers import reverse from modoboa.lib import events, parameters -from modoboa.extensions import ModoExtension, exts_pool -from models import * -from forms import ResourcePoolForm +from modoboa.core.extensions import ModoExtension, exts_pool +from .forms import ResourcePoolForm import controls import views + class Limits(ModoExtension): name = "limits" label = "Limits" version = "1.0" - description = ugettext_lazy("Per administrator resources to limit the number of objects they can create") + description = ugettext_lazy( + "Per administrator resources to limit the number of objects they can create" + ) def init(self): - from modoboa.admin.models import User, Domain - + from modoboa.core.models import User + from modoboa.extensions.admin.models import Domain + ct = ContentType.objects.get(app_label="admin", model="domain") dagrp = Group.objects.get(name="DomainAdmins") - + grp = Group(name="Resellers") grp.save() grp.permissions.add(*dagrp.permissions.all()) - + ct = ContentType.objects.get_for_model(Permission) for pname in ["view_permissions"]: perm = Permission.objects.get(content_type=ct, codename=pname) grp.permissions.add(perm) - + ct = ContentType.objects.get_for_model(Domain) for pname in ["view_domains", "add_domain", "change_domain", "delete_domain"]: perm = Permission.objects.get(content_type=ct, codename=pname) grp.permissions.add(perm) grp.save() - + for user in User.objects.filter(groups__name='DomainAdmins'): try: controls.create_pool(user) @@ -50,7 +53,7 @@ class Limits(ModoExtension): pass def load(self): - from app_settings import ParametersForm + from modoboa.extensions.limits.app_settings import ParametersForm parameters.register(ParametersForm, ugettext_lazy("Limits")) def destroy(self): @@ -59,12 +62,14 @@ class Limits(ModoExtension): exts_pool.register_extension(Limits) + @events.observe("GetExtraRoles") def get_extra_roles(user): if user.is_superuser: - return [("Resellers", _("Reseller")),] + return [("Resellers", _("Reseller")), ] return [] + @events.observe("ExtraAdminContent") def display_pool_usage(user, target, currentpage): from django.template.loader import render_to_string @@ -82,6 +87,7 @@ def display_pool_usage(user, target, currentpage): 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"]: @@ -95,6 +101,7 @@ def extra_account_form(user, account=None): ) ] + @events.observe("CheckExtraAccountForm") def check_form_access(account, form): if form["id"] != "resources": @@ -104,6 +111,7 @@ def check_form_access(account, form): 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"): @@ -113,6 +121,7 @@ def fill_account_instances(user, account, instances): return instances["resources"] = account + @events.observe("GetStaticContent") def get_static_content(user): if user.group == "SimpleUsers": diff --git a/modoboa/extensions/limits/controls.py b/modoboa/extensions/limits/controls.py index 264cb072e..27c624401 100644 --- a/modoboa/extensions/limits/controls.py +++ b/modoboa/extensions/limits/controls.py @@ -5,15 +5,16 @@ """ from modoboa.lib import events -from lib import * -from models import * from modoboa.lib.permissions import get_object_owner +from .lib import LimitReached +from .models import limits_tpl, LimitsPool + def check_limit(user, lname): """Check if a user has reached a defined limit We check if the user will not reach the given limit after this - creation. + creation. Raise a ``LimitReached`` exception if the limit is reached. @@ -25,12 +26,14 @@ def check_limit(user, 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 @@ -39,6 +42,7 @@ def dec_limit(user, lname): except LimitsPool.DoesNotExist: pass + def move_pool_resource(owner, user): """Move resource from one pool to another @@ -61,10 +65,12 @@ def move_pool_resource(owner, user): user.limitspool.delete() + @events.observe('CreateDomain') def inc_nb_domains(user, domain): inc_limit(user, 'domains_limit') + @events.observe('DeleteDomain') def dec_nb_domains(domain): owner = get_object_owner(domain) @@ -76,13 +82,15 @@ def dec_nb_domains(domain): for mbalias in domain.alias_set.all(): dec_nb_mbaliases(mbalias) + @events.observe('DomainAliasCreated') def inc_nb_domaliases(user, domalias): inc_limit(user, 'domain_aliases_limit') + @events.observe('DomainAliasDeleted') def dec_nb_domaliases(domainaliases): - from modoboa.admin.models import DomainAlias + from modoboa.extensions.admin.models import DomainAlias if isinstance(domainaliases, DomainAlias): domainaliases = [domainaliases] @@ -90,13 +98,15 @@ def dec_nb_domaliases(domainaliases): owner = get_object_owner(domainalias) dec_limit(owner, 'domain_aliases_limit') + @events.observe('CreateMailbox') def inc_nb_mailboxes(user, mailbox): inc_limit(user, 'mailboxes_limit') + @events.observe('DeleteMailbox') def dec_nb_mailboxes(mailboxes): - from modoboa.admin.models import Mailbox + from modoboa.extensions.admin.models import Mailbox if isinstance(mailboxes, Mailbox): mailboxes = [mailboxes] @@ -104,21 +114,26 @@ def dec_nb_mailboxes(mailboxes): owner = get_object_owner(mailbox) dec_limit(owner, 'mailboxes_limit') + @events.observe('MailboxAliasCreated') def inc_nb_mbaliases(user, mailboxalias): inc_limit(user, 'mailbox_aliases_limit') + @events.observe('MailboxAliasDeleted') def dec_nb_mbaliases(mailboxalias): owner = get_object_owner(mailboxalias) dec_limit(owner, 'mailbox_aliases_limit') + @events.observe('CanCreate') def can_create_new_object(user, objtype): check_limit(user, '%s_limit' % objtype) + @events.observe("AccountCreated") def create_pool(user): + print "pouet" owner = get_object_owner(user) if not owner.is_superuser and \ not owner.belongs_to_group("Resellers"): @@ -133,9 +148,10 @@ def create_pool(user): p.save() p.create_limits() + @events.observe("AccountModified") def on_account_modified(old, new): - """Update limits when roles are updated""" + """Update limits when roles are updated""" owner = get_object_owner(old) if owner.group not in ["SuperAdmins", "Resellers"]: # Domain admins can't change the role so nothing to check. @@ -158,13 +174,15 @@ def on_account_modified(old, new): if new.group != "DomainAdmins": dec_limit(owner, 'domain_admins_limit') return - + if new.group == "DomainAdmins": check_limit(owner, 'domain_admins_limit') inc_limit(owner, 'domain_admins_limit') + @events.observe("AccountDeleted") def on_account_deleted(account): + print "del" owner = get_object_owner(account) if not owner.group in ["SuperAdmins", "Resellers"]: return diff --git a/modoboa/extensions/limits/forms.py b/modoboa/extensions/limits/forms.py index 432a5c287..8a5ac4fcf 100644 --- a/modoboa/extensions/limits/forms.py +++ b/modoboa/extensions/limits/forms.py @@ -1,10 +1,10 @@ # coding: utf-8 from django import forms -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.lib import parameters -from models import * -from lib import * +from .models import limits_tpl +from .lib import BadLimitValue, UnsufficientResource class ResourcePoolForm(forms.Form): diff --git a/modoboa/extensions/limits/lib.py b/modoboa/extensions/limits/lib.py index fb71a5706..4fd916cf8 100644 --- a/modoboa/extensions/limits/lib.py +++ b/modoboa/extensions/limits/lib.py @@ -8,6 +8,7 @@ from django.contrib.auth.models import Group from django.utils.translation import ugettext as _ from modoboa.lib.exceptions import ModoboaException + class LimitReached(ModoboaException): def __init__(self, limit): self.limit = limit @@ -15,6 +16,7 @@ class LimitReached(ModoboaException): def __str__(self): return _("%s: limit reached") % self.limit.label + class UnsufficientResource(ModoboaException): def __init__(self, limit): self.limit = limit @@ -22,14 +24,6 @@ class UnsufficientResource(ModoboaException): def __str__(self): return _("Not enough resources") + class BadLimitValue(ModoboaException): pass - -def is_reseller(user): - """Tell if a user is a reseller or not - - :param user: a User object - :return: True if the user is a reseller, False otherwise. - """ - grp = Group.objects.get(name="Resellers") - return grp in user.groups.all() diff --git a/modoboa/extensions/limits/models.py b/modoboa/extensions/limits/models.py index 87b00f999..14b27ef74 100644 --- a/modoboa/extensions/limits/models.py +++ b/modoboa/extensions/limits/models.py @@ -4,7 +4,7 @@ from django.db import models from django.contrib.contenttypes import generic from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.lib import parameters -from modoboa.admin.models import User +from modoboa.core.models import User from django.conf import settings limits_tpl = [ @@ -13,7 +13,8 @@ limits_tpl = [ ("domain_aliases_limit", ugettext_lazy("Domain aliases")), ("mailboxes_limit", ugettext_lazy("Mailboxes")), ("mailbox_aliases_limit", ugettext_lazy("Mailbox aliases")) - ] +] + class LimitsPool(models.Model): user = models.OneToOneField(settings.AUTH_USER_MODEL) @@ -74,6 +75,7 @@ class LimitsPool(models.Model): except Limit.DoesNotExist: return None + class Limit(models.Model): name = models.CharField(max_length=255) curvalue = models.IntegerField(default=0) diff --git a/modoboa/extensions/limits/tests.py b/modoboa/extensions/limits/tests.py index 87fb27c32..0c599de10 100644 --- a/modoboa/extensions/limits/tests.py +++ b/modoboa/extensions/limits/tests.py @@ -1,6 +1,6 @@ from django.core.urlresolvers import reverse from modoboa.lib.tests import ExtTestCase -from modoboa.admin.models import User +from modoboa.core.models import User class PermissionsTestCase(ExtTestCase): @@ -21,9 +21,11 @@ class PermissionsTestCase(ExtTestCase): account = User.objects.get(username="reseller@test.com") self.clt.logout() self.clt.login(username="admin@test.com", password="toto") - self.check_ajax_get(reverse("modoboa.admin.views.delaccount", args=[account.id]), {}, - status="ko", respmsg="Permission denied") - self.check_ajax_get(reverse("modoboa.admin.views.delaccount", args=[4]), {}, - status="ko", respmsg="Permission denied") - - + self.check_ajax_get( + reverse("modoboa.extensions.admin.views.identity.delaccount", args=[account.id]), + {}, status="ko", respmsg="Permission denied" + ) + self.check_ajax_get( + reverse("modoboa.extensions.admin.views.identity.delaccount", args=[4]), + {}, status="ko", respmsg="Permission denied" + ) From 3b3a2017e6866a60761ebd5fe1f451bcf6838828 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 20 Aug 2013 16:53:40 +0200 Subject: [PATCH 06/97] wip: Big refactoring autoreply extension OK. --- modoboa/extensions/admin/lib.py | 18 ++++++++++++++ .../extensions/postfix_autoreply/__init__.py | 17 ++++++------- modoboa/extensions/postfix_autoreply/forms.py | 13 +++++----- .../management/commands/autoreply.py | 9 +++---- .../extensions/postfix_autoreply/models.py | 24 +++++++++++++------ modoboa/extensions/postfix_autoreply/views.py | 15 +++++------- modoboa/lib/decorators.py | 22 ----------------- modoboa/lib/exceptions.py | 6 ++--- modoboa/urls.py | 8 ++++++- 9 files changed, 71 insertions(+), 61 deletions(-) delete mode 100644 modoboa/lib/decorators.py diff --git a/modoboa/extensions/admin/lib.py b/modoboa/extensions/admin/lib.py index e6c338228..8fbdfd7ca 100644 --- a/modoboa/extensions/admin/lib.py +++ b/modoboa/extensions/admin/lib.py @@ -1,13 +1,31 @@ # coding: utf-8 +from functools import wraps from django.db.models import Q from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.contrib.contenttypes.models import ContentType from modoboa.core.models import User from modoboa.lib import parameters +from modoboa.lib.exceptions import ModoboaException from modoboa.lib.emailutils import split_mailbox from modoboa.extensions.admin.models import Alias +def needs_mailbox(): + """Check if the current user owns at least one mailbox + + Some applications (the webmail for example) need a mailbox to + work. + """ + def decorator(f): + @wraps(f) + def wrapped_f(request, *args, **kwargs): + if request.user.mailbox_set.count(): + return f(request, *args, **kwargs) + raise ModoboaException() + return wrapped_f + return decorator + + def get_sort_order(qdict, default, allowed_values=None): """Return a sort order from a querydict object diff --git a/modoboa/extensions/postfix_autoreply/__init__.py b/modoboa/extensions/postfix_autoreply/__init__.py index 8ef596627..ff8dd915c 100644 --- a/modoboa/extensions/postfix_autoreply/__init__.py +++ b/modoboa/extensions/postfix_autoreply/__init__.py @@ -9,8 +9,8 @@ functionality into Postfix. from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.lib import events, parameters -from modoboa.extensions import ModoExtension, exts_pool -from models import * +from modoboa.core.extensions import ModoExtension, exts_pool +from .models import Transport, Alias class PostfixAutoreply(ModoExtension): @@ -20,7 +20,7 @@ class PostfixAutoreply(ModoExtension): description = ugettext_lazy("Auto-reply (vacation) functionality using Postfix") def init(self): - from modoboa.admin.models import Domain + from modoboa.extensions.admin.models import Domain for dom in Domain.objects.all(): try: @@ -37,7 +37,7 @@ class PostfixAutoreply(ModoExtension): onCreateMailbox(None, mb) def load(self): - from app_settings import ParametersForm + from modoboa.extensions.postfix_autoreply.app_settings import ParametersForm parameters.register(ParametersForm, ugettext_lazy("Automatic replies")) def destroy(self): @@ -54,7 +54,8 @@ exts_pool.register_extension(PostfixAutoreply) @events.observe("ExtraUprefsRoutes") def extra_routes(): - return [(r'^autoreply/$', 'modoboa.extensions.postfix_autoreply.views.autoreply'),] + return [(r'^user/autoreply/$', + 'modoboa.extensions.postfix_autoreply.views.autoreply'), ] @events.observe("ExtraUprefsJS") @@ -62,7 +63,7 @@ def extra_js(user): return ["""function autoreply_cb() { $('#id_untildate').datepicker({format: 'yyyy-mm-dd', language: '%s'}); } -""" % parameters.get_user(user, "LANG", app="general") +""" % parameters.get_user(user, "LANG", app="core") ] @@ -70,7 +71,7 @@ def extra_js(user): def menu(target, user): if target != "uprefs_menu": return [] - if not user.has_mailbox: + if not user.mailbox_set.count(): return [] return [ {"name": "autoreply", @@ -105,7 +106,7 @@ def onCreateMailbox(user, mailbox): @events.observe("DeleteMailbox") def onDeleteMailbox(mailboxes): - from modoboa.admin.models import Mailbox + from modoboa.extensions.admin.models import Mailbox if isinstance(mailboxes, Mailbox): mailboxes = [mailboxes] diff --git a/modoboa/extensions/postfix_autoreply/forms.py b/modoboa/extensions/postfix_autoreply/forms.py index 4c4420a81..baeaa94b9 100644 --- a/modoboa/extensions/postfix_autoreply/forms.py +++ b/modoboa/extensions/postfix_autoreply/forms.py @@ -1,20 +1,21 @@ # coding: utf-8 -from django import forms -from models import ARmessage -from django.utils.translation import ugettext as _, ugettext_lazy import datetime +from django import forms +from django.utils.translation import ugettext as _, ugettext_lazy +from .models import ARmessage + class ARmessageForm(forms.ModelForm): untildate = forms.DateField( - label=ugettext_lazy('Until'), + label=ugettext_lazy('Until'), required=False, help_text=ugettext_lazy("Activate your auto reply until this date") - ) + ) class Meta: model = ARmessage fields = ('subject', 'content', 'enabled') - + def __init__(self, *args, **kwargs): super(ARmessageForm, self).__init__(*args, **kwargs) self.fields.keyOrder = ['subject', 'content', 'untildate', 'enabled'] diff --git a/modoboa/extensions/postfix_autoreply/management/commands/autoreply.py b/modoboa/extensions/postfix_autoreply/management/commands/autoreply.py index ed189fef4..0ef5d35b3 100644 --- a/modoboa/extensions/postfix_autoreply/management/commands/autoreply.py +++ b/modoboa/extensions/postfix_autoreply/management/commands/autoreply.py @@ -6,10 +6,11 @@ from django.core.management.base import BaseCommand, CommandError from django.utils import timezone from modoboa.lib import parameters from modoboa.lib.emailutils import split_mailbox, sendmail_simple -from modoboa.admin.models import Mailbox +from modoboa.extensions.admin.models import Mailbox from modoboa.extensions.postfix_autoreply import PostfixAutoreply from modoboa.extensions.postfix_autoreply.models import ARmessage, ARhistoric + def send_autoreply(sender, mailbox, armessage): if armessage.untildate is not None \ and armessage.untildate < datetime.date.today(): @@ -20,10 +21,10 @@ def send_autoreply(sender, mailbox, armessage): try: lastar = ARhistoric.objects.get(armessage=armessage.id, sender=sender) PostfixAutoreply().load() - timeout = parameters.get_admin("AUTOREPLIES_TIMEOUT", + timeout = parameters.get_admin("AUTOREPLIES_TIMEOUT", app="postfix_autoreply") delta = datetime.timedelta(seconds=int(timeout)) - now = timezone.make_aware(datetime.datetime.now(), + now = timezone.make_aware(datetime.datetime.now(), timezone.get_default_timezone()) if lastar.last_sent + delta > now: sys.exit(0) @@ -34,7 +35,7 @@ def send_autoreply(sender, mailbox, armessage): sendmail_simple(mailbox.user.encoded_address, sender, armessage.subject, armessage.content.encode('utf-8')) - + lastar.last_sent = datetime.datetime.now() lastar.save() diff --git a/modoboa/extensions/postfix_autoreply/models.py b/modoboa/extensions/postfix_autoreply/models.py index 5381f6a19..5eaaf9e31 100644 --- a/modoboa/extensions/postfix_autoreply/models.py +++ b/modoboa/extensions/postfix_autoreply/models.py @@ -1,25 +1,35 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ -from modoboa.admin.models import Domain, Mailbox +from modoboa.extensions.admin.models import Domain, Mailbox + class Transport(models.Model): domain = models.CharField(max_length=300) method = models.CharField(max_length=255) + class Alias(models.Model): full_address = models.CharField(max_length=255) autoreply_address = models.CharField(max_length=255) + class ARmessage(models.Model): mbox = models.ForeignKey(Mailbox) - subject = models.CharField(_('subject'), max_length=255, - help_text=_("The subject that will appear in sent emails")) - content = models.TextField(_('content'), - help_text=_("The content that will appear in sent emails")) - enabled = models.BooleanField(_('enabled'), - help_text=_("Activate/Deactivate your auto reply")) + subject = models.CharField( + _('subject'), max_length=255, + help_text=_("The subject that will appear in sent emails") + ) + content = models.TextField( + _('content'), + help_text=_("The content that will appear in sent emails") + ) + enabled = models.BooleanField( + _('enabled'), + help_text=_("Activate/Deactivate your auto reply") + ) untildate = models.DateField() + class ARhistoric(models.Model): armessage = models.ForeignKey(ARmessage) last_sent = models.DateTimeField(auto_now=True) diff --git a/modoboa/extensions/postfix_autoreply/views.py b/modoboa/extensions/postfix_autoreply/views.py index 8fa347d4b..2766d533e 100644 --- a/modoboa/extensions/postfix_autoreply/views.py +++ b/modoboa/extensions/postfix_autoreply/views.py @@ -1,17 +1,14 @@ # coding: utf-8 from datetime import date -from django.http import HttpResponse -from django.contrib.auth.decorators \ - import login_required -from django.contrib import messages -from django.core.urlresolvers import reverse +from django.contrib.auth.decorators import login_required from django.utils.translation import ugettext as _ from modoboa.lib.webutils import ( - ajax_simple_response, ajax_response, _render_to_string + ajax_simple_response, _render_to_string ) -from modoboa.lib.decorators import needs_mailbox -from forms import * -from models import * +from modoboa.extensions.admin.lib import needs_mailbox +from modoboa.extensions.admin.models import Mailbox +from .forms import ARmessageForm +from .models import ARmessage @login_required diff --git a/modoboa/lib/decorators.py b/modoboa/lib/decorators.py deleted file mode 100644 index 4dba8f235..000000000 --- a/modoboa/lib/decorators.py +++ /dev/null @@ -1,22 +0,0 @@ -# coding: utf-8 -""" -:mod:`decorators` --- common decorators ---------------------------------------- -""" -from functools import wraps -from exceptions import NeedsMailboxException - -def needs_mailbox(): - """Check if the current user owns at least one mailbox - - Some applications (the webmail for example) need a mailbox to - work. - """ - def decorator(f): - @wraps(f) - def wrapped_f(request, *args, **kwargs): - if request.user.has_mailbox: - return f(request, *args, **kwargs) - raise NeedsMailboxException() - return wrapped_f - return decorator diff --git a/modoboa/lib/exceptions.py b/modoboa/lib/exceptions.py index 39505abf0..026b72bd7 100644 --- a/modoboa/lib/exceptions.py +++ b/modoboa/lib/exceptions.py @@ -6,9 +6,11 @@ """ from django.utils.translation import ugettext as _ + class ModoboaException(Exception): pass + class PermDeniedException(ModoboaException): def __init__(self, msg=None): self.msg = msg @@ -17,7 +19,3 @@ class PermDeniedException(ModoboaException): if self.msg: return _("Permission denied: %s" % self.msg) return _("Permission denied") - -class NeedsMailboxException(ModoboaException): - pass - diff --git a/modoboa/urls.py b/modoboa/urls.py index d929f9195..8c85d6060 100644 --- a/modoboa/urls.py +++ b/modoboa/urls.py @@ -1,7 +1,7 @@ from django.conf.urls import patterns, include, url from django.conf import settings from modoboa.core.extensions import exts_pool -from modoboa.lib import parameters +from modoboa.lib import parameters, events from modoboa.core import load_settings load_settings() @@ -25,7 +25,13 @@ urlpatterns = patterns('', (r'^user/$', 'modoboa.core.views.user.index'), (r'^user/preferences/$', 'modoboa.core.views.user.preferences'), (r'^user/profile/$', 'modoboa.core.views.user.profile'), +) +urlpatterns += patterns('', + *events.raiseQueryEvent("ExtraUprefsRoutes") +) + +urlpatterns += patterns('', *exts_pool.load_all() ) From c28f635db2da23c8f9be25e3cd9a93750802915d Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 22 Aug 2013 16:48:07 +0200 Subject: [PATCH 07/97] wip: big refactoring stats extension OK. --- modoboa/core/models.py | 1 + modoboa/extensions/stats/__init__.py | 10 +-- modoboa/extensions/stats/graph_templates.py | 47 ++++++++------ modoboa/extensions/stats/grapher.py | 57 +++++++++-------- .../stats/management/commands/logparser.py | 35 +++++----- .../stats/templates/stats/index.html | 2 +- modoboa/extensions/stats/views.py | 64 +++++++++++-------- 7 files changed, 119 insertions(+), 97 deletions(-) diff --git a/modoboa/core/models.py b/modoboa/core/models.py index ccfc3624e..d4e4b277d 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -19,6 +19,7 @@ from django.contrib.auth.models import ( from django.contrib.contenttypes.models import ContentType from modoboa.lib import events, md5crypt, parameters from modoboa.lib.exceptions import PermDeniedException +from modoboa.lib.sysutils import exec_cmd from modoboa.core.extensions import exts_pool from modoboa.core.exceptions import AdminError diff --git a/modoboa/extensions/stats/__init__.py b/modoboa/extensions/stats/__init__.py index 61bb62101..7197cc01c 100644 --- a/modoboa/extensions/stats/__init__.py +++ b/modoboa/extensions/stats/__init__.py @@ -10,7 +10,7 @@ sent, received, bounced, rejected from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.lib import events, parameters -from modoboa.extensions import ModoExtension, exts_pool +from modoboa.core.extensions import ModoExtension, exts_pool class Stats(ModoExtension): @@ -21,9 +21,11 @@ class Stats(ModoExtension): needs_media = True def load(self): - from app_settings import ParametersForm + from modoboa.extensions.stats.app_settings import ParametersForm events.declare(["GetGraphSets"]) - parameters.register(ParametersForm, ugettext_lazy("Graphical statistics")) + parameters.register( + ParametersForm, ugettext_lazy("Graphical statistics") + ) def destroy(self): events.unregister("AdminMenuDisplay", menu) @@ -44,7 +46,7 @@ def menu(target, user): @events.observe("GetGraphSets") def get_default_graph_sets(): - from graph_templates import MailTraffic + from modoboa.extensions.stats.graph_templates import MailTraffic gset = MailTraffic() return {gset.html_id: gset} diff --git a/modoboa/extensions/stats/graph_templates.py b/modoboa/extensions/stats/graph_templates.py index 5350a4c0d..cbe8ab161 100644 --- a/modoboa/extensions/stats/graph_templates.py +++ b/modoboa/extensions/stats/graph_templates.py @@ -2,6 +2,7 @@ from django.utils.translation import ugettext_lazy + class Graph(object): width = 540 height = 120 @@ -16,42 +17,45 @@ class Graph(object): def display_name(self): return self.__class__.__name__.lower() + class AvgTraffic(Graph): title = ugettext_lazy('Average normal traffic') vertlabel = ugettext_lazy('msgs/min') - + def __init__(self): super(AvgTraffic, self).__init__( - sent={'type' : 'AREA', 'color' : '#00FF00', - "legend" : ugettext_lazy("sent messages")}, - recv={"type" : "AREA", "color" : "#0000FF", - "legend" : ugettext_lazy("received messages")} - ) + sent={'type': 'AREA', 'color': '#00FF00', + "legend": ugettext_lazy("sent messages")}, + recv={"type": "AREA", "color": "#0000FF", + "legend": ugettext_lazy("received messages")} + ) + class AvgBadTraffic(Graph): title = ugettext_lazy('Average bad traffic') vertlabel = ugettext_lazy('msgs/min') - + def __init__(self): super(AvgBadTraffic, self).__init__( - bounced={'type' : 'AREA', 'color' : '#FFFF00', - "legend" : ugettext_lazy("bounced messages")}, - reject={"type" : "AREA", "color" : "#FF0000", - "legend" : ugettext_lazy("rejected messages")} - ) + bounced={'type': 'AREA', 'color': '#FFFF00', + "legend": ugettext_lazy("bounced messages")}, + reject={"type": "AREA", "color": "#FF0000", + "legend": ugettext_lazy("rejected messages")} + ) + class AvgTrafficSize(Graph): title = ugettext_lazy('Average normal traffic size') vertlabel = ugettext_lazy('bytes/min') - + def __init__(self): super(AvgTrafficSize, self).__init__( - size_recv={'type' : 'AREA', 'color' : '#FF9900', - "legend" : ugettext_lazy("received size")}, - size_sent={"type" : "AREA", "color" : "#339999", - "legend" : ugettext_lazy("sent size")} - ) - + size_recv={'type': 'AREA', 'color': '#FF9900', + "legend": ugettext_lazy("received size")}, + size_sent={"type": "AREA", "color": "#339999", + "legend": ugettext_lazy("sent size")} + ) + class GraphSet(object): title = None @@ -63,11 +67,12 @@ class GraphSet(object): def get_graphs(self): if not hasattr(self, '__graph_instances'): - self.__graph_instances = map(lambda gc: gc(), self.graphs) + self.__graph_instances = [gc() for gc in self.graphs] return self.__graph_instances def get_graph_names(self): - return map(lambda g: g.display_name, self.get_graphs()) + return [g.display_name for g in self.get_graphs()] + class MailTraffic(GraphSet): title = ugettext_lazy('Mail traffic') diff --git a/modoboa/extensions/stats/grapher.py b/modoboa/extensions/stats/grapher.py index 02f14c67b..e19f42bf6 100644 --- a/modoboa/extensions/stats/grapher.py +++ b/modoboa/extensions/stats/grapher.py @@ -1,15 +1,17 @@ # coding: utf-8 -import sys, os +import sys +import os import time import datetime from django.utils.translation import ugettext as _, ugettext_lazy from modoboa.lib import parameters -periods = [{"name" : "day", "label" : ugettext_lazy("Day")}, - {"name" : "week", "label" : ugettext_lazy("Week")}, - {"name" : "month", "label" : ugettext_lazy("Month")}, - {"name" : "year", "label" : ugettext_lazy("Year")}] +periods = [{"name": "day", "label": ugettext_lazy("Day")}, + {"name": "week", "label": ugettext_lazy("Week")}, + {"name": "month", "label": ugettext_lazy("Month")}, + {"name": "year", "label": ugettext_lazy("Year")}] + def str2Time(y, M, d, h="00", m="00", s="00"): """Date conversion @@ -20,17 +22,18 @@ def str2Time(y, M, d, h="00", m="00", s="00"): """ try: if not M.isdigit(): - local = time.strptime("%s %s %s %s:%s:%s" % (y, M, d, h, m, s), \ - "%Y %b %d %H:%M:%S") + local = time.strptime("%s %s %s %s:%s:%s" % (y, M, d, h, m, s), + "%Y %b %d %H:%M:%S") else: - local = time.strptime("%s %s %s %s:%s:%s" % (y, M, d, h, m, s), \ - "%Y %m %d %H:%M:%S") + local = time.strptime("%s %s %s %s:%s:%s" % (y, M, d, h, m, s), + "%Y %m %d %H:%M:%S") except ValueError: - print >>sys.stderr, "Error: failed to convert date and time" + print >> sys.stderr, "Error: failed to convert date and time" return 0 return int(time.mktime(local)) + class Grapher(object): def __init__(self): self.rrd_rootdir = parameters.get_admin("RRD_ROOTDIR") @@ -53,7 +56,7 @@ class Grapher(object): defs += [str('DEF:%s=%s:%s:%s' % (v, rrdfile, v, graph_tpl.cf)), str('CDEF:%spm=%s,60,*' % (v, v)), str('VDEF:%s_total=%s,TOTAL' % (v, v))] - type = d.has_key("type") and d["type"] or "LINE" + type = d["type"] if "type" in d else "LINE" lines += [str("%s:%spm%s:%s:STACK" % (type, v, d["color"], (d["legend"].encode("utf8")).ljust(20)))] lines.append('GPRINT:%s_total:%s%%7.0lf%%s\\l' % (v, _("Total").encode('utf-8'))) @@ -64,23 +67,25 @@ class Grapher(object): defs.append('GPRINT:%s_first:%s %%c:strftime' % (first_ds, "From")) defs.append('GPRINT:%s_last:%s %%c\\c:strftime' % (first_ds, "to")) defs.append('COMMENT:\\s') - + params = defs + lines - rrdtool.graph(str(path), - "--imgformat", "PNG", - "--width", str(graph_tpl.width), - "--height", str(graph_tpl.height), - "--start", start, - "--end", end, - "--vertical-label", graph_tpl.vertlabel.encode("utf-8"), - "--lower-limit", "0", - "--slope-mode", - "--units-exponent", "0", - "--title", str("%s: %s" % (target, graph_tpl.title.encode("utf-8"))), - *params) + rrdtool.graph( + str(path), + "--imgformat", "PNG", + "--width", str(graph_tpl.width), + "--height", str(graph_tpl.height), + "--start", start, + "--end", end, + "--vertical-label", graph_tpl.vertlabel.encode("utf-8"), + "--lower-limit", "0", + "--slope-mode", + "--units-exponent", "0", + "--title", str("%s: %s" % (target, graph_tpl.title.encode("utf-8"))), + *params + ) if not os.path.exists(path): - print "[graph] Impossible to create %s graph" %path + print "[graph] Impossible to create %s graph" % path def make_defaults(self, target, graph_tpl): end = "%d" % int(time.mktime(time.localtime())) @@ -88,5 +93,3 @@ class Grapher(object): self.process(target, "week", "-1week", end, graph_tpl) self.process(target, "month", "-1month", end, graph_tpl) self.process(target, "year", "-1year", end, graph_tpl) - - diff --git a/modoboa/extensions/stats/management/commands/logparser.py b/modoboa/extensions/stats/management/commands/logparser.py index 49473ae03..cbc5c2151 100644 --- a/modoboa/extensions/stats/management/commands/logparser.py +++ b/modoboa/extensions/stats/management/commands/logparser.py @@ -28,7 +28,7 @@ from optparse import make_option from modoboa.lib import parameters from modoboa.admin.models import Domain from modoboa.extensions.stats import Stats -from modoboa.extensions.stats.grapher import * +from modoboa.extensions.stats.grapher import str2Time, Grapher from modoboa.extensions.stats.graph_templates import MailTraffic rrdstep = 60 @@ -37,6 +37,7 @@ points_per_sample = 3 variables = ["sent", "recv", "bounced", "reject", "spam", "virus", "size_sent", "size_recv"] + class LogParser(object): def __init__(self, options, workdir, year=None): self.logfile = options["logfile"] @@ -94,7 +95,6 @@ class LogParser(object): params += ['DS:%s:%s:%s:0:U' % (v, ds_type, rrdstep * 2)] # Set up RRD to archive data - rras = [] for cf in ['AVERAGE', 'MAX']: for step in [day_steps, week_steps, month_steps, year_steps]: params += ['RRA:%s:0.5:%s:%s' % (cf, step, realrows)] @@ -120,14 +120,14 @@ class LogParser(object): if not os.path.exists(fname): self.lupdates[fname] = self.init_rrd(fname, m) if self.debug: - print "[rrd] create new RRD file %s" %fname + print "[rrd] create new RRD file %s" % fname else: - if not self.lupdates.has_key(fname): + if not fname in self.lupdates: self.lupdates[fname] = rrdtool.last(str(fname)) if m <= self.lupdates[fname]: if self.verbose: - print "[rrd] VERBOSE events at %s already recorded in RRD" %m + print "[rrd] VERBOSE events at %s already recorded in RRD" % m return False tpl = "" @@ -140,7 +140,8 @@ class LogParser(object): if m > self.lupdates[fname] + rrdstep: values = "" for v in variables: - if values != "": values += ":" + if values != "": + values += ":" values += "0" for p in range(self.lupdates[fname] + rrdstep, m, rrdstep): if self.verbose: @@ -165,16 +166,17 @@ class LogParser(object): def initcounters(self, dom, cur_t): init = {} - for v in variables: init[v] = 0 + for v in variables: + init[v] = 0 self.data[dom][cur_t] = init def inc_counter(self, dom, cur_t, counter, val=1): if dom is not None and dom in self.domains: - if not self.data[dom].has_key(cur_t): + if not cur_t in self.data[dom]: self.initcounters(dom, cur_t) self.data[dom][cur_t][counter] += val - if not self.data["global"].has_key(cur_t): + if not cur_t in self.data["global"]: self.initcounters("global", cur_t) self.data["global"][cur_t][counter] += val @@ -211,7 +213,7 @@ class LogParser(object): if not m: continue (mo, da, ho, mi, se, host, prog, pid, log) = m.groups() - se = int(int(se) / rrdstep) # rrd step is one-minute => se = 0 + se = int(int(se) / rrdstep) # rrd step is one-minute => se = 0 if prev_se != se or prev_mi != mi or prev_ho != ho: cur_t = str2Time(self.year(mo), mo, da, ho, mi, se) @@ -225,18 +227,18 @@ class LogParser(object): m = re.search("message-id=<([^>]*)>", line_log) if m: - self.workdict[line_id] = {'from' : m.group(1), 'size' : 0} + self.workdict[line_id] = {'from': m.group(1), 'size': 0} continue m = re.search("from=<([^>]*)>, size=(\d+)", line_log) if m: - self.workdict[line_id] = {'from' : m.group(1), - 'size' : string.atoi(m.group(2))} + self.workdict[line_id] = {'from': m.group(1), + 'size': string.atoi(m.group(2))} continue m = re.search("to=<([^>]*)>.*status=(\S+)", line_log) if m: - if not self.workdict.has_key(line_id): + if not line_id in self.workdict: if self.debug: print "Inconsistent mail (%s: %s), skipping" % (line_id, m.group(1)) continue @@ -276,13 +278,14 @@ class LogParser(object): G = Grapher() for dom, data in self.data.iteritems(): if self.debug: - print "[rrd] dealing with domain %s" %dom + print "[rrd] dealing with domain %s" % dom for t in sorted(data.keys()): self.update_rrd(dom, t) for graph_tpl in MailTraffic().get_graphs(): G.make_defaults(dom, graph_tpl) + class Command(BaseCommand): help = 'Log file parser' @@ -293,7 +296,7 @@ class Command(BaseCommand): dest="verbose", help="Set verbose mode"), make_option("--debug", default=False, action="store_true", help="Set debug mode") - ) + ) def handle(self, *args, **options): Stats().load() diff --git a/modoboa/extensions/stats/templates/stats/index.html b/modoboa/extensions/stats/templates/stats/index.html index 325e7b9da..b70947d06 100644 --- a/modoboa/extensions/stats/templates/stats/index.html +++ b/modoboa/extensions/stats/templates/stats/index.html @@ -7,7 +7,7 @@ diff --git a/modoboa/core/templatetags/core_tags.py b/modoboa/core/templatetags/core_tags.py index 1c7933ed9..53ff25b7d 100644 --- a/modoboa/core/templatetags/core_tags.py +++ b/modoboa/core/templatetags/core_tags.py @@ -135,12 +135,6 @@ def uprefs_menu(selection, user): "url": "preferences/", "label": _("Preferences")}, ] - # if user.has_mailbox: - # entries.insert(0, {"name": "forward", - # "class": "ajaxlink", - # "url": "forward/", - # "label": _("Forward")}) - entries += events.raiseQueryEvent("UserMenuDisplay", "uprefs_menu", user) entries = sorted(entries, key=lambda e: e["label"]) return render_to_string('common/menu.html', { diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index 3e2b25561..cb84e6c36 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -23,6 +23,7 @@ def dologin(request): login(request, user) if not form.cleaned_data["rememberme"]: request.session.set_expiry(0) + events.raiseEvent("UserLoggedIn", request, form) # if request.user.has_mailbox: # request.session["password"] = encrypt(form.cleaned_data["password"]) diff --git a/modoboa/demo/__init__.py b/modoboa/demo/__init__.py index 739076d53..69ccfecc5 100755 --- a/modoboa/demo/__init__.py +++ b/modoboa/demo/__init__.py @@ -6,27 +6,29 @@ from django.template.loader import render_to_string from modoboa.lib import events from modoboa.lib.webutils import static_url + @events.observe("UserMenuDisplay") def menu(target, user): if target != "top_menu_middle": return [] - if not user.has_mailbox: + if not user.mailbox_set.count(): return [] return [ - {"name" : "demo", - "label" : _("Test messages"), - "img" : static_url("pics/demo.png"), - "class" : "topdropdown", - "menu" : [ - {"name" : "sendvirus", - "label" : _("Send virus"), - "url" : reverse("modoboa.demo.views.send_virus")}, - {"name" : "sendspam", - "label" : _("Send spam"), - "url" : reverse("modoboa.demo.views.send_spam")} - ] + {"name": "demo", + "label": _("Test messages"), + "img": static_url("pics/demo.png"), + "class": "topdropdown", + "menu": [ + {"name": "sendvirus", + "label": _("Send virus"), + "url": reverse("modoboa.demo.views.send_virus")}, + {"name": "sendspam", + "label": _("Send spam"), + "url": reverse("modoboa.demo.views.send_spam")} + ] } - ] + ] + @events.observe("GetAnnouncement") def announcement(target): @@ -35,13 +37,15 @@ def announcement(target): return [txt] return "" + @events.observe("PasswordChange") def password_change(user): return [user.id == 1] + @events.observe("GetStaticContent") def get_static_content(user): - if not user.has_mailbox: + if not user.mailbox_set.count(): return [] return """ - - - -{% endblock %} -{% block leftcol %}{% endblock %} -{% block apparea %}
{% endblock %} diff --git a/modoboa/userprefs/templates/userprefs/preferences.html b/modoboa/userprefs/templates/userprefs/preferences.html deleted file mode 100644 index 26a514dd7..000000000 --- a/modoboa/userprefs/templates/userprefs/preferences.html +++ /dev/null @@ -1,9 +0,0 @@ -{% load i18n %}{% load url from future %} -

{% trans "Preferences" %} {% trans "Customize Modoboa" %}

-
-{% csrf_token %} - {% include "common/parameters.html" %} -
- - diff --git a/modoboa/userprefs/templates/userprefs/profile.html b/modoboa/userprefs/templates/userprefs/profile.html deleted file mode 100644 index f5df0b64e..000000000 --- a/modoboa/userprefs/templates/userprefs/profile.html +++ /dev/null @@ -1,10 +0,0 @@ -{% load i18n libextras %}{% load url from future %} -

{% trans "Profile" %} {% trans "Update your personal information" %}

-
-
{% csrf_token %} - {% render_form form %} -
- -
-
diff --git a/modoboa/userprefs/templatetags/__init__.py b/modoboa/userprefs/templatetags/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/modoboa/userprefs/templatetags/uprefs_extras.py b/modoboa/userprefs/templatetags/uprefs_extras.py deleted file mode 100644 index 8a73ad118..000000000 --- a/modoboa/userprefs/templatetags/uprefs_extras.py +++ /dev/null @@ -1,70 +0,0 @@ -# coding: utf-8 -from django import template -from django.template.loader import render_to_string -from django.utils.translation import ugettext as _ -from django.core.urlresolvers import reverse -from modoboa.lib import events - -register = template.Library() - - -@register.simple_tag -def uprefs_menu(selection, user): - entries = [ - {"name": "profile", - "class": "ajaxlink", - "url": "profile/", - "label": _("Profile")}, - {"name": "preferences", - "class": "ajaxlink", - "url": "preferences/", - "label": _("Preferences")}, - ] - if user.has_mailbox: - entries.insert(0, {"name": "forward", - "class": "ajaxlink", - "url": "forward/", - "label": _("Forward")}) - - entries += events.raiseQueryEvent("UserMenuDisplay", "uprefs_menu", user) - entries = sorted(entries, key=lambda e: e["label"]) - return render_to_string('common/menu.html', { - "entries": entries, - "css": "nav nav-list", - "selection": selection, - "user": user - }) - - -@register.simple_tag -def user_menu(user, selection): - entries = [ - {"name": "user", - "img": "icon-user icon-white", - "label": user.fullname, - "menu": [ - {"name": "settings", - "img": "icon-list", - "label": _("Settings"), - "url": reverse("modoboa.userprefs.views.index")} - ]} - ] - - entries[0]["menu"] += \ - events.raiseQueryEvent("UserMenuDisplay", "options_menu", user) \ - + [{"name": "logout", - "url": reverse("modoboa.auth.views.dologout"), - "label": _("Logout"), - "img": "icon-off"}] - - return render_to_string("common/menulist.html", { - "selection": selection, "entries": entries, "user": user - }) - - -@register.simple_tag -def loadextmenu(selection, user): - menu = events.raiseQueryEvent("UserMenuDisplay", "top_menu", user) - return render_to_string('common/menulist.html', { - "selection": selection, "entries": menu, "user": user - }) diff --git a/modoboa/userprefs/urls.py b/modoboa/userprefs/urls.py deleted file mode 100644 index 05e214541..000000000 --- a/modoboa/userprefs/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -# coding: utf-8 -from django.conf.urls import patterns, url -from modoboa.lib import events - -urlpatterns = patterns('modoboa.userprefs.views', - url(r'^$', 'index'), - (r'^preferences/$', 'preferences'), - (r'^profile/$', 'profile'), - (r'^forward/$', 'forward'), -) - -urlpatterns += patterns('', - *events.raiseQueryEvent("ExtraUprefsRoutes") -) diff --git a/modoboa/userprefs/views.py b/modoboa/userprefs/views.py deleted file mode 100644 index be70d1958..000000000 --- a/modoboa/userprefs/views.py +++ /dev/null @@ -1,135 +0,0 @@ -# coding: utf-8 -from django.shortcuts import render -from django.contrib.auth.decorators import login_required, user_passes_test -from django.utils.translation import ugettext as _ -from modoboa.lib import parameters, events -from modoboa.lib.decorators import needs_mailbox -from modoboa.lib.exceptions import ModoboaException -from modoboa.lib.webutils import ( - ajax_response, ajax_simple_response, _render_to_string -) -from modoboa.admin.models import Mailbox, Alias -from modoboa.auth.lib import encrypt -from forms import ForwardForm, BadDestination, ProfileForm - - -@login_required -def index(request, tplname="userprefs/index.html"): - extrajs = events.raiseQueryEvent("ExtraUprefsJS", request.user) - return render(request, tplname, { - "selection": "user", - "extrajs": "".join(extrajs) - }) - - -@login_required -@needs_mailbox() -def forward(request, tplname='userprefs/forward.html'): - try: - mb = request.user.mailbox_set.all()[0] - except IndexError: - raise ModoboaException( - _("You need a mailbox in order to define a forward") - ) - try: - al = Alias.objects.get(address=mb.address, - domain__name=mb.domain.name) - except Alias.DoesNotExist: - al = None - if request.method == "POST": - form = ForwardForm(request.POST) - error = None - if form.is_valid(): - 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(intdests, 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) - - return ajax_simple_response(dict( - status="ko", - errors=form.errors, - respmsg=error - )) - - form = ForwardForm() - if al is not None: - form.fields["dest"].initial = al.extmboxes - try: - selfmb = al.mboxes.get(pk=mb.id) - except Mailbox.DoesNotExist: - pass - else: - form.fields["keepcopies"].initial = True - return ajax_simple_response({ - "status": "ok", - "content": _render_to_string(request, tplname, { - "form": form - }) - }) - - -@login_required -def profile(request, tplname='userprefs/profile.html'): - update_password = True - if True in events.raiseQueryEvent("PasswordChange", request.user): - update_password = False - - if request.method == "POST": - form = ProfileForm( - update_password, request.POST, instance=request.user - ) - if form.is_valid(): - 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 - }) - - form = ProfileForm(update_password, instance=request.user) - return ajax_simple_response({ - "status": "ok", - "content": _render_to_string(request, tplname, { - "form": form - }) - }) - - -@login_required -def preferences(request): - if request.method == "POST": - for formdef in parameters.get_user_forms(request.user, request.POST)(): - form = formdef["form"] - if form.is_valid(): - form.save() - continue - return ajax_simple_response({ - "status": "ko", "prefix": form.app, "errors": form.errors - }) - - return ajax_simple_response({ - "status": "ok", "respmsg": _("Preferences saved") - }) - - return ajax_simple_response({ - "status": "ok", - "content": _render_to_string(request, "userprefs/preferences.html", { - "forms": parameters.get_user_forms(request.user) - }) - }) From 271c2530e8b1f0204504ba2104a649756e7eebe5 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 23 Aug 2013 17:41:13 +0200 Subject: [PATCH 13/97] wip: big refactoring --- doc/extending/events.rst | 5 +++-- modoboa/core/models.py | 10 ++-------- modoboa/core/views/auth.py | 3 --- modoboa/extensions/admin/__init__.py | 19 +++++++++++++++++++ modoboa/extensions/admin/views/identity.py | 2 +- modoboa/extensions/limits/controls.py | 3 +-- 6 files changed, 26 insertions(+), 16 deletions(-) diff --git a/doc/extending/events.rst b/doc/extending/events.rst index 48b0570a7..76bcf0493 100644 --- a/doc/extending/events.rst +++ b/doc/extending/events.rst @@ -65,9 +65,10 @@ Raised when an existing account is deleted. *Callback prototype*:: - def callback(account): pass + def callback(account, byuser, **options): pass -* ``oldaccount`` is the account that is going to be deleted +* ``account`` is the account that is going to be deleted +* ``byuser`` is the adminstrator deleting ``account`` AccountModified =============== diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 2c90a4604..035a09b63 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -59,7 +59,7 @@ class User(AbstractBaseUser, PermissionsMixin): password_expr = re.compile(r'(\{(\w+)\}|(\$1\$))(.+)') - def delete(self, fromuser, keep_mb_dir, *args, **kwargs): + def delete(self, fromuser, *args, **kwargs): """Custom delete method To check permissions properly, we need to make a distinction @@ -79,18 +79,12 @@ class User(AbstractBaseUser, PermissionsMixin): if not fromuser.can_access(self): raise PermDeniedException - # if self.has_mailbox: - # mb = self.mailbox_set.all()[0] - # if not fromuser.can_access(mb): - # raise PermDeniedException - # mb.delete(keepdir=keep_mb_dir) - owner = get_object_owner(self) for ooentry in self.objectaccess_set.filter(is_owner=True): if ooentry.content_object is not None: grant_access_to_object(owner, ooentry.content_object, True) - events.raiseEvent("AccountDeleted", self) + events.raiseEvent("AccountDeleted", self, fromuser, **kwargs) ungrant_access_to_object(self) super(User, self).delete(*args, **kwargs) diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index cb84e6c36..c56b81a69 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -23,9 +23,6 @@ def dologin(request): login(request, user) if not form.cleaned_data["rememberme"]: request.session.set_expiry(0) - events.raiseEvent("UserLoggedIn", request, form) - # if request.user.has_mailbox: - # request.session["password"] = encrypt(form.cleaned_data["password"]) request.session["django_language"] = \ parameters.get_user(request.user, "LANG") diff --git a/modoboa/extensions/admin/__init__.py b/modoboa/extensions/admin/__init__.py index 793ea13ae..4677515d9 100644 --- a/modoboa/extensions/admin/__init__.py +++ b/modoboa/extensions/admin/__init__.py @@ -154,3 +154,22 @@ def import_account_mailbox(user, account, row): except Domain.DoesNotExist: continue dom.add_admin(account) + + +@events.observe("UserLogin") +def user_logged_in(request, username, password): + from modoboa.lib.cryptutils import encrypt + + if request.user.mailbox_set.count(): + request.session["password"] = encrypt(password) + + +@events.observe("AccountDeleted") +def account_deleted(account, byuser, **kwargs): + if not account.mailbox_set.count(): + return + mb = account.mailbox_set.all()[0] + if not byuser.can_access(mb): + raise PermDeniedException + keep_mb_dir = kwargs.get("keep_mb_dir", True) + mb.delete(keepdir=keep_mb_dir) diff --git a/modoboa/extensions/admin/views/identity.py b/modoboa/extensions/admin/views/identity.py index 6ca1410f9..3ca9a9a8a 100644 --- a/modoboa/extensions/admin/views/identity.py +++ b/modoboa/extensions/admin/views/identity.py @@ -225,7 +225,7 @@ def editaccount(request, accountid, tplname="common/tabforms.html"): def delaccount(request, accountid): keepdir = True if request.POST.get("keepdir", "false") == "true" else False - User.objects.get(pk=accountid).delete(request.user, keepdir) + 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}) diff --git a/modoboa/extensions/limits/controls.py b/modoboa/extensions/limits/controls.py index 27c624401..f8174e666 100644 --- a/modoboa/extensions/limits/controls.py +++ b/modoboa/extensions/limits/controls.py @@ -181,8 +181,7 @@ def on_account_modified(old, new): @events.observe("AccountDeleted") -def on_account_deleted(account): - print "del" +def on_account_deleted(account, byuser, **kwargs): owner = get_object_owner(account) if not owner.group in ["SuperAdmins", "Resellers"]: return From aea3f58bad35b8160f2ac1f4f056ec48ad6cc487 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 23 Aug 2013 17:44:06 +0200 Subject: [PATCH 14/97] Useless import removed. --- modoboa/core/views/auth.py | 1 - 1 file changed, 1 deletion(-) diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index c56b81a69..f4d132d9e 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -7,7 +7,6 @@ from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from modoboa.lib import events, parameters from modoboa.lib.webutils import _render_to_string -from modoboa.lib.cryptutils import encrypt from modoboa.core.forms import LoginForm From c509cb4edec50709a0dd8bf24b696a63488d6e7d Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 9 Sep 2013 09:29:19 +0200 Subject: [PATCH 15/97] Support only integers for this option. fixes #478 --- .../extensions/webmail/static/webmail/js/webmail.js | 2 +- modoboa/lib/emailutils.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/modoboa/extensions/webmail/static/webmail/js/webmail.js b/modoboa/extensions/webmail/static/webmail/js/webmail.js index 8cea472b2..c9c745bea 100644 --- a/modoboa/extensions/webmail/static/webmail/js/webmail.js +++ b/modoboa/extensions/webmail/static/webmail/js/webmail.js @@ -83,7 +83,7 @@ Webmail.prototype = { $(document).on("click", "a[name=forward]", $.proxy(this.reply_loader, this)); $(document).on("click", "a[name=delete]", $.proxy(this.delete_message, this)); $(document).on("click", "a[name=activate_links]", $.proxy(function(e) { this.display_mode(e, "1"); }, this)); - $(document).on("click", "a[name=disable_links]", $.proxy(function(e) { this.disable_mode(e, "0"); }, this)); + $(document).on("click", "a[name=disable_links]", $.proxy(function(e) { this.display_mode(e, "0"); }, this)); $(document).on("click", "a[name=sendmail]", $.proxy(this.sendmail, this)); diff --git a/modoboa/lib/emailutils.py b/modoboa/lib/emailutils.py index bc58ea1b3..66fe831c1 100644 --- a/modoboa/lib/emailutils.py +++ b/modoboa/lib/emailutils.py @@ -23,7 +23,7 @@ class EmailAddress(object): return self.fulladdress class Email(object): - def __init__(self, msg, mformat="plain", dformat="plain", links="0"): + def __init__(self, msg, mformat="plain", dformat="plain", links=0): self.attached_map = {} self.contents = {"html" : "", "plain" : ""} self.headers = [] @@ -78,7 +78,7 @@ class Email(object): sometimes, messages embark inline images, which means they must be displayed and not attached. """ - if self.dformat == "html" and self.links != "0" \ + if self.dformat == "html" and self.links \ and msg.has_key("Content-Disposition"): if msg["Content-Disposition"].startswith("inline"): cid = None @@ -167,9 +167,9 @@ class Email(object): def viewmail_html(self, content, **kwargs): if content is None or content == "": return "" - links = kwargs.has_key("links") and kwargs["links"] or "0" - html = lxml.html.fromstring(content) - if links == "0": + links = kwargs.get("links", 0) + html = lxml.html.fromstring(content) + if not links: html.rewrite_links(lambda x: None) else: html.rewrite_links(self.map_cid) @@ -179,7 +179,6 @@ class Email(object): else: body = lxml.html.tostring(body) body = re.sub("<(/?)body", lambda m: "<%sdiv" % m.group(1), body) - body = Template(body).render(Context({})) return body def split_mailbox(mailbox): From 1036aa31a21d798df70536fe7821a8d6464853f4 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 9 Sep 2013 21:33:17 +0200 Subject: [PATCH 16/97] Mailboxes manipulation fixed. fixes #479 --- .../extensions/webmail/static/webmail/js/webmail.js | 11 +++++------ .../extensions/webmail/templates/webmail/folder.html | 2 +- modoboa/extensions/webmail/templatetags/webextras.py | 6 +++--- modoboa/extensions/webmail/views.py | 10 ++++++---- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/modoboa/extensions/webmail/static/webmail/js/webmail.js b/modoboa/extensions/webmail/static/webmail/js/webmail.js index c9c745bea..d60e2cfec 100644 --- a/modoboa/extensions/webmail/static/webmail/js/webmail.js +++ b/modoboa/extensions/webmail/static/webmail/js/webmail.js @@ -536,7 +536,7 @@ Webmail.prototype = { return; } $parent = $ul; - mailbox = $parent.attr("name") + this.hdelimiter + mailbox; + mailbox = $parent.attr("name") + this.options.hdelimiter + mailbox; } else { $parent = $("#mboxes_container").children("ul"); } @@ -549,8 +549,8 @@ Webmail.prototype = { * If needed, the mailbox will be moved to its new location. */ rename_mailbox: function(oldname, newname, oldparent, newparent) { - var oldpattern = (oldparent) ? oldparent + this.hdelimiter + oldname : oldname; - var newpattern = (newparent) ? newparent + this.hdelimiter + newparent : newname; + var oldpattern = (oldparent) ? oldparent + this.options.hdelimiter + oldname : oldname; + var newpattern = (newparent) ? newparent + this.options.hdelimiter + newname : newname; var $link = $("#folders").find('a[href="' + oldpattern + '"]'); if (oldname != newname) { @@ -560,14 +560,13 @@ Webmail.prototype = { $link.parent("li").attr("name", newpattern); $link.prepend($i); $link.attr("href", newpattern); - this.navobject.setparam("mbox", newname).update(false, true); + this.navobject.setparam("mbox", newpattern).update(false, true); } if (oldparent != newparent) { - var newlocation = (newparent) ? newparent + this.hdelimiter + newname : newname; this.remove_mbox_from_tree($link.parent("li")); this.add_mailbox_to_tree(newparent, newname); if (this.navobject.getparam("action") == "listmailbox") { - this.navobject.setparam("mbox", newlocation).update(); + this.navobject.setparam("mbox", newpattern).update(); } else { this.select_mailbox("INBOX"); } diff --git a/modoboa/extensions/webmail/templates/webmail/folder.html b/modoboa/extensions/webmail/templates/webmail/folder.html index 717997f59..e0c03b57e 100644 --- a/modoboa/extensions/webmail/templates/webmail/folder.html +++ b/modoboa/extensions/webmail/templates/webmail/folder.html @@ -4,7 +4,7 @@
{% endblock %} diff --git a/modoboa/extensions/webmail/templatetags/webextras.py b/modoboa/extensions/webmail/templatetags/webextras.py index 7efffbb13..03a85cf2e 100644 --- a/modoboa/extensions/webmail/templatetags/webextras.py +++ b/modoboa/extensions/webmail/templatetags/webextras.py @@ -119,7 +119,7 @@ def listmailbox_menu(selection, folder, user): )) @register.simple_tag -def print_mailboxes(tree, selected=None, withunseen=False, selectonly=False): +def print_mailboxes(tree, selected=None, withunseen=False, selectonly=False, hdelimiter='.'): """Display a tree of mailboxes and sub-mailboxes :param tree: the mailboxes to display @@ -129,7 +129,7 @@ def print_mailboxes(tree, selected=None, withunseen=False, selectonly=False): for mbox in tree: cssclass = "" name = mbox["path"] if mbox.has_key("sub") else mbox["name"] - label = separate_mailbox(mbox["name"])[0] + label = separate_mailbox(mbox["name"], hdelimiter)[0] if selected == name: cssclass = "active" result += "
  • \n" % (name, cssclass) @@ -157,7 +157,7 @@ def print_mailboxes(tree, selected=None, withunseen=False, selectonly=False): 'selectfolder' if selectonly else 'loadfolder', extra_attrs, iclass, label) if mbox.has_key("sub") and len(mbox["sub"]): result += "\n" + + print_mailboxes(mbox["sub"], selected, withunseen, selectonly, hdelimiter) + "\n" result += "
  • \n" return result diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index 968ee4436..113b831da 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -134,7 +134,8 @@ def newfolder(request, tplname="webmail/folder.html"): "action_classes": "submit", "withunseen": False, "selectonly": True, - "mboxes": mbc.getmboxes(request.user)} + "mboxes": mbc.getmboxes(request.user), + "hdelimiter": mbc.hdelimiter} if request.method == "POST": form = FolderForm(request.POST) @@ -164,14 +165,15 @@ def editfolder(request, tplname="webmail/folder.html"): "action_label" : _("Update"), "action_classes" : "submit", "withunseen" : False, - "selectonly" : True} + "selectonly" : True, + "hdelimiter": mbc.hdelimiter} if request.method == "POST": form = FolderForm(request.POST) if form.is_valid(): pf = request.POST.get("parent_folder", None) ctx["selected"] = pf - oldname, oldparent = separate_mailbox(request.POST["oldname"]) + oldname, oldparent = separate_mailbox(request.POST["oldname"], sep=mbc.hdelimiter) res = dict(status="ok", respmsg=_("Mailbox updated")) if form.cleaned_data["name"] != oldname \ or (pf != oldparent): @@ -193,7 +195,7 @@ def editfolder(request, tplname="webmail/folder.html"): name = request.GET.get("name", None) if name is None: raise WebmailError(_("Invalid request")) - shortname, parent = separate_mailbox(name) + shortname, parent = separate_mailbox(name, sep=mbc.hdelimiter) ctx["mboxes"] = mbc.getmboxes(request.user, until_mailbox=parent) ctx["form"] = FolderForm() ctx["form"].fields["oldname"].initial = name From 23eb93b063dce17ffe963da94ca9e8776ce64cff Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 9 Sep 2013 21:35:31 +0200 Subject: [PATCH 17/97] The gunicorn command line was wrong. --- doc/integration/web_servers.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/integration/web_servers.rst b/doc/integration/web_servers.rst index ff5ca5712..fbe187256 100644 --- a/doc/integration/web_servers.rst +++ b/doc/integration/web_servers.rst @@ -113,7 +113,7 @@ file named *gunicorn.conf.py* inside Modoboa's root dir):: To start gunicorn, execute the following commands:: $ cd - $ gunicorn -c gunicorn.conf.py + $ gunicorn -c gunicorn.conf.py .wsgi:application Now the *nginx* part. Just create a new virtual host and use the following configuration:: From d86733911a11de90064995dc2ee88acf98bfafe3 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 10 Sep 2013 18:54:58 +0200 Subject: [PATCH 18/97] Missing import added. see #459 --- modoboa/admin/forms.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index 18e878c75..8c8cd0daf 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -10,7 +10,9 @@ from modoboa.lib.exceptions import PermDeniedException from modoboa.lib.emailutils import split_mailbox from modoboa.lib.permissions import get_account_roles from modoboa.lib.formutils import * -from modoboa.admin.models import User, Domain, DomainAlias, Mailbox, Alias +from modoboa.admin.models import ( + User, Domain, DomainAlias, Mailbox, Alias, Quota +) class DomainFormGeneral(forms.ModelForm, DynamicForm): From a5100880340546d44309923bccfec2a41c071663 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 10 Sep 2013 22:46:36 +0200 Subject: [PATCH 19/97] Don't forget aliases :p see #459 --- modoboa/admin/forms.py | 37 +++++++++++++++---- .../extensions/postfix_autoreply/__init__.py | 5 +++ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index 8c8cd0daf..db716e541 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -75,17 +75,40 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): return cleaned_data + def update_mailbox_quotas(self, domain): + """Update all quota records associated to this domain + + This methid must be called only when a domain gets renamed. As + the primary key used for a quota is an email address, rename a + domain will change all associated email addresses, so it will + change the primary keys used for quotas. The consequence is we + can't issue regular UPDATE queries using the .save() method of + a Quota instance (it will trigger an INSERT as the primary key + has changed). + + So, we use this ugly hack to bypass this behaviour. It is not + perfomant at all as it will generate one query per quota + record to update. + """ + for q in Quota.objects.filter(username__contains="@%s" % self.oldname).values('username'): + 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): + """Custom save method + + Updating a domain may have consequences on other objects + (domain alias, mailbox, quota). The most tricky part concerns + quotas update. + + + """ d = super(DomainFormGeneral, self).save(commit=False) if commit: old_mail_homes = None - hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) - if hm == "yes": - if self.oldname is not None and d.name != self.oldname: - for q in Quota.objects.filter(username__contains="@%s" % self.oldname): - q.username = q.username.replace('@%s' % self.oldname, '@%s' % d.name) - q.save() - old_mail_homes = dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all()) + if self.oldname is not None and d.name != self.oldname: + self.update_mailbox_quotas(d) + old_mail_homes = dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all()) d.save() Mailbox.objects.filter(domain=d, use_domain_quota=True).update(quota=d.quota) if old_mail_homes is not None: diff --git a/modoboa/extensions/postfix_autoreply/__init__.py b/modoboa/extensions/postfix_autoreply/__init__.py index 6434a339e..b86855897 100644 --- a/modoboa/extensions/postfix_autoreply/__init__.py +++ b/modoboa/extensions/postfix_autoreply/__init__.py @@ -94,6 +94,11 @@ def onDomainModified(domain): 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") From ec8aba2aaa56bc42c6d7d039d95bd41360bb890b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 08:36:44 +0200 Subject: [PATCH 20/97] Handle mailbox operations in background. see #482 --- modoboa/admin/forms.py | 21 ++- modoboa/admin/management/__init__.py | 0 modoboa/admin/management/commands/__init__.py | 0 .../commands/handle_mailbox_operations.py | 62 ++++++++ .../0036_auto__add_mailboxoperation.py | 137 ++++++++++++++++++ modoboa/admin/models.py | 33 ++--- modoboa/admin/views.py | 9 +- modoboa/core/templates/settings.py.tpl | 10 ++ 8 files changed, 243 insertions(+), 29 deletions(-) create mode 100644 modoboa/admin/management/__init__.py create mode 100644 modoboa/admin/management/commands/__init__.py create mode 100644 modoboa/admin/management/commands/handle_mailbox_operations.py create mode 100644 modoboa/admin/migrations/0036_auto__add_mailboxoperation.py diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index db716e541..f5e2a217d 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -90,6 +90,8 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): perfomant at all as it will generate one query per quota record to update. """ + Mailbox.objects.filter(domain=domain, use_domain_quota=True) \ + .update(quota=d.quota) for q in Quota.objects.filter(username__contains="@%s" % self.oldname).values('username'): username = q['username'].replace('@%s' % self.oldname, '@%s' % domain.name) Quota.objects.filter(username=q['username']).update(username=username) @@ -99,21 +101,16 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): Updating a domain may have consequences on other objects (domain alias, mailbox, quota). The most tricky part concerns - quotas update. - + quotas update. """ d = super(DomainFormGeneral, self).save(commit=False) if commit: old_mail_homes = None if self.oldname is not None and d.name != self.oldname: - self.update_mailbox_quotas(d) - old_mail_homes = dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all()) + old_mail_homes = \ + dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all()) d.save() - Mailbox.objects.filter(domain=d, use_domain_quota=True).update(quota=d.quota) - if old_mail_homes is not None: - for mb in d.mailbox_set.all(): - mb.rename_dir(old_mail_homes[mb.id]) for k, v in self.cleaned_data.iteritems(): if not k.startswith("aliases"): continue @@ -133,6 +130,14 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if not len(filter(lambda name: self.cleaned_data[name] == dalias.name, self.cleaned_data.keys())): dalias.delete() + + self.update_mailbox_quotas(d) + if old_mail_homes is not None: + for mb in d.mailbox_set.all(): + MailboxOperation.objects.create( + mailbox=mb, type='rename', argument=old_mail_homes[mb.id] + ) + return d diff --git a/modoboa/admin/management/__init__.py b/modoboa/admin/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/admin/management/commands/__init__.py b/modoboa/admin/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py new file mode 100644 index 000000000..419766023 --- /dev/null +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -0,0 +1,62 @@ +import os +import logging +from optparse import make_option +from django.core.management.base import BaseCommand +from modoboa.lib import parameters +from modoboa.admin.models import MailboxOperation + + +class OperationError(Exception): + pass + + +class Command(BaseCommand): + args = '' + help = 'Handles rename and delete operations on mailboxes' + + def __init__(self, *args, **kwargs): + super(Command, self).__init__(*args, **kwargs) + self.logger = logging.getLogger('modoboa.cron') + + def rename_mailbox(self, operation): + if not os.path.exists(operation.argument): + return + new_mail_home = operation.mailbox.mail_home.strip(os.sep) + self.logger.info("Renaming %s to %s", operation.argument, new_mail_home) + curpath = '/' + for part in new_mail_home.split(os.sep)[:-1]: + if not part: + continue + curpath = os.path.join(curpath, part) + if not os.path.exists(curpath): + os.mkdir(curpath) + code, output = exec_cmd( + "mv %s %s" % (old_mail_home, new_mail_home) + ) + if code: + raise OperationError(output) + + def delete_mailbox(self, operation): + if not os.path.exists(operation.argument): + return + self.logger.info("Deleting %s", operation.argument) + code, output = exec_cmd( + "rm -r %s" % operation.argument + ) + if code: + raise OperationError(output) + + def handle(self): + if parameters.get_admin("HANDLE_MAILBOXES") == 'no': + return + for ope in MailboxOperation.objects.all(): + try: + f = getattr(self, '%s_mailbox' % ope.optype) + except AttributeError: + continue + try: + f(ope) + except OperationError as e: + self.logger.critical('Operation failed: %s', str(e)) + else: + ope.delete() diff --git a/modoboa/admin/migrations/0036_auto__add_mailboxoperation.py b/modoboa/admin/migrations/0036_auto__add_mailboxoperation.py new file mode 100644 index 000000000..f59c90677 --- /dev/null +++ b/modoboa/admin/migrations/0036_auto__add_mailboxoperation.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'MailboxOperation' + db.create_table(u'admin_mailboxoperation', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('mailbox', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['admin.Mailbox'], null=True, blank=True)), + ('type', self.gf('django.db.models.fields.CharField')(max_length=20)), + ('argument', self.gf('django.db.models.fields.TextField')()), + )) + db.send_create_signal(u'admin', ['MailboxOperation']) + + + def backwards(self, orm): + # Deleting model 'MailboxOperation' + db.delete_table(u'admin_mailboxoperation') + + + models = { + u'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': u"orm['admin.Alias']", 'symmetrical': 'False'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"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': u"orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + u'admin.domain': { + 'Meta': {'ordering': "['name']", 'object_name': 'Domain'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': u"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', [], {}) + }, + u'admin.domainalias': { + 'Meta': {'object_name': 'DomainAlias'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': u"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': u"orm['admin.Domain']"}) + }, + u'admin.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'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '252'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"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['admin.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'}) + }, + u'admin.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['admin.User']"}) + }, + u'admin.objectdates': { + 'Meta': {'object_name': 'ObjectDates'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + u'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': u"orm['admin.Mailbox']"}), + 'messages': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'username': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'primary_key': 'True'}) + }, + u'admin.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'}) + }, + 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'}) + } + } + + complete_apps = ['admin'] \ No newline at end of file diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py index 8b395d800..8619781b4 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -702,7 +702,9 @@ class Domain(DatesAware): hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) if hm == "yes" and not keepdir: for mb in self.mailbox_set.all(): - mb.delete_dir() + MailboxOperation.objects.create( + type='delete', argument=mb.mail_home + ) if self.alias_set.count(): events.raiseEvent("MailboxAliasDelete", self.alias_set.all()) ungrant_access_to_objects(self.alias_set.all()) @@ -873,15 +875,9 @@ class Mailbox(DatesAware): hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) if hm is None or hm == "no": return - self.__mail_home = None - if not os.path.exists(old_mail_home): - return - code, output = exec_cmd( - "mv %s %s" % (old_mail_home, self.mail_home), - sudo_user=parameters.get_admin("MAILBOXES_OWNER") + MailboxOperation.objects.create( + mailbox=self, type='rename', argument=old_mail_home ) - if code: - raise AdminError(_("Failed to rename mailbox: %s" % output)) def rename(self, address, domain): """Rename the mailbox @@ -893,21 +889,14 @@ class Mailbox(DatesAware): qs = Quota.objects.filter(username=self.full_address) self.address = address self.domain = domain - self.rename_dir(old_mail_home) qs.update(username=self.full_address) + self.rename_dir(old_mail_home) def delete_dir(self): hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) if hm is None or hm == "no": return - if not os.path.exists(self.mail_home): - return - code, output = exec_cmd( - "rm -r %s" % self.mail_home, - sudo_user=parameters.get_admin("MAILBOXES_OWNER") - ) - if code: - raise AdminError(_("Failed to remove mailbox: %s" % output)) + MailboxOperation.objects.create(type='delete', argument=self.mail_home) def set_quota(self, value=None, override_rules=False): """Set or update quota's value for this mailbox. @@ -1017,6 +1006,14 @@ class Quota(models.Model): mbox = models.OneToOneField(Mailbox, related_name="quota_value", null=True) +class MailboxOperation(models.Model): + mailbox = models.ForeignKey(Mailbox, blank=True, null=True) + type = models.CharField( + max_length=20, choices=(('rename', 'rename'), ('delete', 'delete')) + ) + argument = models.TextField() + + class Alias(DatesAware): address = models.CharField( ugettext_lazy('address'), max_length=254, diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py index b34136387..3582e9b33 100644 --- a/modoboa/admin/views.py +++ b/modoboa/admin/views.py @@ -125,6 +125,7 @@ def newdomain(request, tplname="common/wizard_forms.html"): @login_required @permission_required("admin.view_domains") +@transaction.commit_on_success def editdomain(request, dom_id, tplname="admin/editdomainform.html"): domain = Domain.objects.get(pk=dom_id) if not request.user.can_access(domain): @@ -157,11 +158,13 @@ def editdomain(request, dom_id, tplname="admin/editdomainform.html"): error = str(e) else: events.raiseEvent("DomainModified", domain) - return ajax_simple_response({"status": "ok", "respmsg": _("Domain modified")}) + return ajax_simple_response({"status": "ok", "respmsg": _("Domain modified")}) commonctx["tabs"] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) + 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 diff --git a/modoboa/core/templates/settings.py.tpl b/modoboa/core/templates/settings.py.tpl index 4f926f712..67aa8c691 100644 --- a/modoboa/core/templates/settings.py.tpl +++ b/modoboa/core/templates/settings.py.tpl @@ -208,6 +208,11 @@ LOGGING = { 'facility': SysLogHandler.LOG_AUTH, 'formatter': 'syslog' }, + 'syslog-cron': { + 'class': 'logging.handlers.SysLogHandler', + 'facility': SysLogHandler.LOG_CRON, + 'formatter': 'syslog' + } 'modoboa': { 'class': 'modoboa.lib.logutils.SQLHandler', } @@ -227,6 +232,11 @@ LOGGING = { 'handlers': ['modoboa'], 'level': 'INFO', 'propagate': False + }, + 'modoboa.cron': { + 'handlers': ['syslog-cron'], + 'level': 'INFO', + 'propagate': False } } } From 08b0fd0e5c187eed525c04276a9fdb241b35e5cf Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 16:46:28 +0200 Subject: [PATCH 21/97] Log results into the database. see #482 --- modoboa/admin/app_settings.py | 4 +-- modoboa/admin/forms.py | 14 ++++----- .../commands/handle_mailbox_operations.py | 29 +++++++++---------- modoboa/admin/models.py | 5 ++++ 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/modoboa/admin/app_settings.py b/modoboa/admin/app_settings.py index 1f02db899..7b183b15c 100644 --- a/modoboa/admin/app_settings.py +++ b/modoboa/admin/app_settings.py @@ -197,11 +197,11 @@ class GeneralParametersForm(parameters.AdminParametersForm): self.fields["default_top_redirection"].choices = enabled_applications() hide_fields = False try: - code, version = exec_cmd("dovecot --version") + code, output = exec_cmd("which doveadm") except OSError, e: hide_fields = True else: - if code or not version.strip().startswith("2"): + if code: hide_fields = True if hide_fields: del self.fields["handle_mailboxes"] diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index f5e2a217d..75ec95f28 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -78,7 +78,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): def update_mailbox_quotas(self, domain): """Update all quota records associated to this domain - This methid must be called only when a domain gets renamed. As + This method must be called only when a domain gets renamed. As the primary key used for a quota is an email address, rename a domain will change all associated email addresses, so it will change the primary keys used for quotas. The consequence is we @@ -90,8 +90,6 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): perfomant at all as it will generate one query per quota record to update. """ - Mailbox.objects.filter(domain=domain, use_domain_quota=True) \ - .update(quota=d.quota) for q in Quota.objects.filter(username__contains="@%s" % self.oldname).values('username'): username = q['username'].replace('@%s' % self.oldname, '@%s' % domain.name) Quota.objects.filter(username=q['username']).update(username=username) @@ -108,9 +106,13 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if commit: old_mail_homes = None if self.oldname is not None and d.name != self.oldname: + d.name = self.oldname old_mail_homes = \ dict((mb.id, mb.mail_home) for mb in d.mailbox_set.all()) + d.name = self.cleaned_data['name'] d.save() + Mailbox.objects.filter(domain=d, use_domain_quota=True) \ + .update(quota=d.quota) for k, v in self.cleaned_data.iteritems(): if not k.startswith("aliases"): continue @@ -131,12 +133,10 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): self.cleaned_data.keys())): dalias.delete() - self.update_mailbox_quotas(d) if old_mail_homes is not None: + self.update_mailbox_quotas(d) for mb in d.mailbox_set.all(): - MailboxOperation.objects.create( - mailbox=mb, type='rename', argument=old_mail_homes[mb.id] - ) + mb.rename_dir() return d diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index 419766023..e606be97d 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -3,6 +3,8 @@ import logging 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.admin import AdminConsole from modoboa.admin.models import MailboxOperation @@ -16,22 +18,18 @@ class Command(BaseCommand): def __init__(self, *args, **kwargs): super(Command, self).__init__(*args, **kwargs) - self.logger = logging.getLogger('modoboa.cron') + self.logger = logging.getLogger('modoboa.admin') def rename_mailbox(self, operation): if not os.path.exists(operation.argument): return - new_mail_home = operation.mailbox.mail_home.strip(os.sep) - self.logger.info("Renaming %s to %s", operation.argument, new_mail_home) - curpath = '/' - for part in new_mail_home.split(os.sep)[:-1]: - if not part: - continue - curpath = os.path.join(curpath, part) - if not os.path.exists(curpath): - os.mkdir(curpath) + new_mail_home = operation.mailbox.mail_home + try: + os.makedirs(os.path.dirname(new_mail_home)) + except os.error as e: + raise OperationError(str(e)) code, output = exec_cmd( - "mv %s %s" % (old_mail_home, new_mail_home) + "mv %s %s" % (ope.argument, new_mail_home) ) if code: raise OperationError(output) @@ -39,24 +37,25 @@ class Command(BaseCommand): def delete_mailbox(self, operation): if not os.path.exists(operation.argument): return - self.logger.info("Deleting %s", operation.argument) code, output = exec_cmd( "rm -r %s" % operation.argument ) if code: raise OperationError(output) - def handle(self): + def handle(self, *args, **options): + AdminConsole().load() if parameters.get_admin("HANDLE_MAILBOXES") == 'no': return for ope in MailboxOperation.objects.all(): try: - f = getattr(self, '%s_mailbox' % ope.optype) + f = getattr(self, '%s_mailbox' % ope.type) except AttributeError: continue try: f(ope) except OperationError as e: - self.logger.critical('Operation failed: %s', str(e)) + self.logger.critical('Operation failed: %s. Reason: %s', ope, str(e)) else: + self.logger.info('Operation succeed: %s', ope) ope.delete() diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py index 8619781b4..8c03ee7e9 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -1013,6 +1013,11 @@ class MailboxOperation(models.Model): ) argument = models.TextField() + def __str__(self): + if self.type == 'rename': + return 'Rename %s -> %s' % (self.argument, self.mailbox.full_address) + return 'Delete %s' % self.argument + class Alias(DatesAware): address = models.CharField( From 40e37389ad511f1f37850fa0028ef58fbc5234cd Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 16:48:37 +0200 Subject: [PATCH 22/97] Fixed wrong call. see #482 --- modoboa/admin/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index 75ec95f28..d1da8c872 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -136,7 +136,7 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): if old_mail_homes is not None: self.update_mailbox_quotas(d) for mb in d.mailbox_set.all(): - mb.rename_dir() + mb.rename_dir(old_mail_homes[mb.id]) return d From 71856df2dabd09396a26a115982d25e0c48198f6 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 17:00:54 +0200 Subject: [PATCH 23/97] Check if sudo is needed before executing the command. see #482 --- .../commands/handle_mailbox_operations.py | 3 ++- modoboa/admin/models.py | 19 ++++++++++++++++--- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index e606be97d..b6332d9ac 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -5,6 +5,7 @@ from django.core.management.base import BaseCommand from modoboa.lib import parameters from modoboa.lib.sysutils import exec_cmd from modoboa.admin import AdminConsole +from modoboa.admin.exceptions import AdminError from modoboa.admin.models import MailboxOperation @@ -54,7 +55,7 @@ class Command(BaseCommand): continue try: f(ope) - except OperationError as e: + except (OperationError, AdminError) as e: self.logger.critical('Operation failed: %s. Reason: %s', ope, str(e)) else: self.logger.info('Operation succeed: %s', ope) diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py index 8c03ee7e9..5b6a111ef 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -24,6 +24,7 @@ import sys import pwd import re import crypt, hashlib, string, base64 +import pwd from random import Random try: @@ -857,15 +858,27 @@ class Mailbox(DatesAware): @property def mail_home(self): - """ + """Retrieve the home directory of this mailbox. + The home directory refers to the place on the file system + where the mailbox data is stored. + + We ask dovecot to give us this information because there are + several patterns to understand and we don't want to implement + them. """ hm = parameters.get_admin("HANDLE_MAILBOXES", raise_error=False) if hm is None or hm == "no": return None if self.__mail_home is None: - code, output = exec_cmd("doveadm user %s -f home" % self.full_address, - sudo_user=parameters.get_admin("MAILBOXES_OWNER")) + curuser = pwd.getpwuid(os.getuid()).pw_name + mbowner = parameters.get_admin("MAILBOXES_OWNER") + options = {} + if curuser != mbowner: + options['sudo_user'] = mbowner + code, output = exec_cmd( + "doveadm user %s -f home" % self.full_address, **options + ) if code: raise AdminError(_("Failed to retrieve mailbox location (%s)" % output)) self.__mail_home = output.strip() From f2f4e243aea69657cc9bb40cd2c4102d1cdd7298 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 17:05:35 +0200 Subject: [PATCH 24/97] Check if path exists before trying to create it. see #482 --- .../commands/handle_mailbox_operations.py | 13 +++++++------ modoboa/admin/models.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index b6332d9ac..e4da2cb36 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -25,10 +25,11 @@ class Command(BaseCommand): if not os.path.exists(operation.argument): return new_mail_home = operation.mailbox.mail_home - try: - os.makedirs(os.path.dirname(new_mail_home)) - except os.error as e: - raise OperationError(str(e)) + if not os.path.exists(new_mail_home): + try: + os.makedirs(os.path.dirname(new_mail_home)) + except os.error as e: + raise OperationError(str(e)) code, output = exec_cmd( "mv %s %s" % (ope.argument, new_mail_home) ) @@ -56,7 +57,7 @@ class Command(BaseCommand): try: f(ope) except (OperationError, AdminError) as e: - self.logger.critical('Operation failed: %s. Reason: %s', ope, str(e)) + self.logger.critical('%s failed (reason: %s)', ope, e) else: - self.logger.info('Operation succeed: %s', ope) + self.logger.info('%s succeed', ope) ope.delete() diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py index 5b6a111ef..2e2084857 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -1028,7 +1028,7 @@ class MailboxOperation(models.Model): def __str__(self): if self.type == 'rename': - return 'Rename %s -> %s' % (self.argument, self.mailbox.full_address) + return 'Rename %s -> %s' % (self.argument, self.mailbox.mail_home) return 'Delete %s' % self.argument From 8ea423173cba143f68a94d65ad5f3b5ce650d434 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 17:09:01 +0200 Subject: [PATCH 25/97] Check the container, not the leaf :p see #482 --- .../admin/management/commands/handle_mailbox_operations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index e4da2cb36..663bf3919 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -25,9 +25,10 @@ class Command(BaseCommand): if not os.path.exists(operation.argument): return new_mail_home = operation.mailbox.mail_home - if not os.path.exists(new_mail_home): + dirname = os.path.dirname(new_mail_home) + if not os.path.exists(dirname): try: - os.makedirs(os.path.dirname(new_mail_home)) + os.makedirs(dirname) except os.error as e: raise OperationError(str(e)) code, output = exec_cmd( From 21d6aaecd9657c50c03fb4d1f6666964eba5717e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 17:57:08 +0200 Subject: [PATCH 26/97] Prevent multiple executions. see #482 --- .../commands/handle_mailbox_operations.py | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/modoboa/admin/management/commands/handle_mailbox_operations.py b/modoboa/admin/management/commands/handle_mailbox_operations.py index 663bf3919..03b899e37 100644 --- a/modoboa/admin/management/commands/handle_mailbox_operations.py +++ b/modoboa/admin/management/commands/handle_mailbox_operations.py @@ -14,9 +14,15 @@ class OperationError(Exception): class Command(BaseCommand): - args = '' help = 'Handles rename and delete operations on mailboxes' + option_list = BaseCommand.option_list + ( + make_option( + '--pidfile', default='/tmp/handle_mailbox_operations.pid', + help='Path to the file that will contain the PID of this process' + ), + ) + def __init__(self, *args, **kwargs): super(Command, self).__init__(*args, **kwargs) self.logger = logging.getLogger('modoboa.admin') @@ -32,7 +38,7 @@ class Command(BaseCommand): except os.error as e: raise OperationError(str(e)) code, output = exec_cmd( - "mv %s %s" % (ope.argument, new_mail_home) + "mv %s %s" % (operation.argument, new_mail_home) ) if code: raise OperationError(output) @@ -46,10 +52,30 @@ class Command(BaseCommand): if code: raise OperationError(output) + def check_pidfile(self, path): + """Check if this command is already running + + :param str path: path to the file containing the PID + :return: a boolean, True means we can go further + """ + if os.path.exists(path): + with open(path) as fp: + pid = fp.read().strip() + code, output = exec_cmd( + "grep handle_mailbox_operations /proc/%s/cmdline" % pid + ) + if not code: + return False + with open(path, 'w') as fp: + print >> fp, os.getpid() + return True + def handle(self, *args, **options): AdminConsole().load() if parameters.get_admin("HANDLE_MAILBOXES") == 'no': return + if not self.check_pidfile(options['pidfile']): + return for ope in MailboxOperation.objects.all(): try: f = getattr(self, '%s_mailbox' % ope.type) @@ -58,7 +84,9 @@ class Command(BaseCommand): try: f(ope) except (OperationError, AdminError) as e: - self.logger.critical('%s failed (reason: %s)', ope, e) + self.logger.critical('%s failed (reason: %s)', + ope, str(e).encode('utf-8')) else: self.logger.info('%s succeed', ope) ope.delete() + os.unlink(options['pidfile']) From 524a585444ff8a69bad8e38b275b8968734d648d Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 18:27:57 +0200 Subject: [PATCH 27/97] New SQL map file to use with check_recipient_access. see #482 --- modoboa/core/commands/postfix_maps.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modoboa/core/commands/postfix_maps.py b/modoboa/core/commands/postfix_maps.py index f697db4e2..73358fab5 100644 --- a/modoboa/core/commands/postfix_maps.py +++ b/modoboa/core/commands/postfix_maps.py @@ -65,6 +65,12 @@ class CatchallAliasesMap(MapFile): postgres = "(SELECT mb.address || '@' || dom.name FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id WHERE mb.id IN (SELECT al_mb.mailbox_id FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id INNER JOIN admin_alias_mboxes al_mb ON al.id=al_mb.alias_id WHERE al.enabled AND al.address='*' AND dom.name='%d' AND dom.enabled)) UNION (SELECT al.extmboxes FROM admin_alias al INNER JOIN admin_domain dom ON al.domain_id=dom.id WHERE al.enabled AND al.extmboxes<>'' AND al.address='*' AND dom.name='%d' AND dom.enabled)" +class MaintainMap(MapFile): + filename = 'sql-maintain.cf' + mysql = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" + postgres = "SELECT '450 Requested mail action not taken: mailbox unavailable' FROM admin_mailbox mb INNER JOIN admin_domain dom ON mb.domain_id=dom.id INNER JOIN admin_mailboxoperation mbop ON mbop.mailbox_id=mb.id WHERE dom.name='%d' AND mb.address='%u' LIMIT 1" + + class TransportMap(MapFile): category = "autoreply" filename = 'sql-transport.cf' From 00fab59c94e0cecc220b853c64e441f30f65b494 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 19:07:21 +0200 Subject: [PATCH 28/97] Documentation updated. fixes #482 --- doc/integration/imap_and_smtp.rst | 49 +++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index bbcab775e..d035c6627 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -45,9 +45,46 @@ Then, edit the ``inbox`` namespace and add the following lines:: It ensures all the special mailboxes will be automaticaly created for new accounts. -.. note:: +Operations on the file system +----------------------------- + +Three operation types are considered: + +#. Mailbox creation +#. Mailbox renaming +#. Mailbox deletion + +The first one is managed by Dovecot. The last two ones may be managed +by Modoboa if it can access the file system where the mailboxes are +stored (see :ref:`admin-params` to activate this feature). + +Those operations are treated asynchronously by a cron script. For +example, when you rename an e-mail address through the web UI, the +associated mailbox on the file system is not modified +directly. Instead of that, a 'rename' order is created for this +mailbox. The mailbox will be considered unavailable until the order is +not executed (see :ref:`Postfix configuration `). + +Edit the crontab of the user who owns the mailboxes on the file system:: + + $ crontab -u -e + +And add the following line inside:: + + * * * * * python /var/www/mail_koalabs_org/manage.py handle_mailbox_operations + +.. warning:: + + The cron script must be executed by the system user owning the mailboxes. + +.. warning:: + + The user running the cron script must have access to the + *settings.py* file of the modoboa instance. + +The result of each order is recorded into Modoboa's log. Go to +*Modoboa > Logs* to consult them. - Modoboa is not responsible for mailboxes creation, Dovecot is. Authentication ============== @@ -369,6 +406,8 @@ directory, run the following command:: ``mapfiles`` is the directory where the files will be stored. Answer the few questions and you're done. +.. _postfix_config: + Configuration ============= @@ -393,6 +432,12 @@ Use the following configuration in the */etc/postfix/main.cf* file mysql:/etc/postfix/sql-email2email.cf, mysql:/etc/postfix/sql-catchall-aliases.cf + smtpd_recipient_restrictions = + ... + check_recipient_access mysql:/etc/postfix/maps/sql-maintain.cf + permit_mynetworks + ... + # Stuff after Then, edit the */etc/postfix/master.cf* file and add the following From db17f923fe36c1cf846c6cd29e131085d7a1b364 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 19:12:08 +0200 Subject: [PATCH 29/97] Release note for 1.0.1 added. --- doc/getting_started/upgrade.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/getting_started/upgrade.rst b/doc/getting_started/upgrade.rst index b9ac53826..fcf7fc452 100644 --- a/doc/getting_started/upgrade.rst +++ b/doc/getting_started/upgrade.rst @@ -43,6 +43,13 @@ Finally, follow the common procedure:: $ python manage.py syncdb --migrate $ python manage.py collectstatic +1.0.1: operations on mailboxes +============================== + +The way Modoboa handles **rename** and **delete** operations on mailboxes has +been improved. Make sure to consult :ref:`fs_operations`. + + 1.0.0: production ready, at last ================================ From 3e2bb6683a86d9c141796aab10762eb13b7f85f4 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Sat, 14 Sep 2013 19:13:10 +0200 Subject: [PATCH 30/97] Label added. --- doc/integration/imap_and_smtp.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index d035c6627..3280b5af2 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -45,6 +45,8 @@ Then, edit the ``inbox`` namespace and add the following lines:: It ensures all the special mailboxes will be automaticaly created for new accounts. +.. _fs_operations: + Operations on the file system ----------------------------- From 0faf4caa733008cfd26758ba01c9e1a69b320fb6 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 16 Sep 2013 17:38:02 +0200 Subject: [PATCH 31/97] Don't forget to load the delete handler when the list is reloaded. --- modoboa/extensions/sievefilters/static/sievefilters/js/lib.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js b/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js index e38092166..7854c0c13 100644 --- a/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js +++ b/modoboa/extensions/sievefilters/static/sievefilters/js/lib.js @@ -44,6 +44,7 @@ function changefs(evt) { function init_filters_list() { $("a[name=togglestate]").click(toggle_filter_state); $("a[name*=movefilter_]").click(move_filter); + $("a[name=removefilter]").click(removefilter); } /* @@ -63,7 +64,6 @@ function loadfs(response) { $("#fsetmenu").remove(); } $("#menu").after($(response.menu)); - $("a[name=removefilter]").click(removefilter); $("a[name=activatefs]").click(activatefs); $("a[name=savefs]").click(savefs); $("a[name=removefs]").click(removefs); From 5e87fc764c4599d24929b500c242619aa5a2a803 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 17 Sep 2013 08:43:23 +0200 Subject: [PATCH 32/97] Properly encode filenames using RFC6266. see #473 fixes #481 --- modoboa/admin/views.py | 2 +- modoboa/extensions/sievefilters/views.py | 3 ++- modoboa/extensions/webmail/lib.py | 19 ++++++++++++------- modoboa/extensions/webmail/views.py | 7 ++++--- modoboa/lib/emailutils.py | 6 +++--- requirements.txt | 1 + 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py index 3582e9b33..fac30eb03 100644 --- a/modoboa/admin/views.py +++ b/modoboa/admin/views.py @@ -804,7 +804,7 @@ def _export(content, filename): resp = HttpResponse(content) resp["Content-Type"] = "text/csv" resp["Content-Length"] = len(content) - resp["Content-Disposition"] = 'attachment; filename="%s"' % filename + resp["Content-Disposition"] = build_header(filename) return resp diff --git a/modoboa/extensions/sievefilters/views.py b/modoboa/extensions/sievefilters/views.py index d730eff2b..9cae88c11 100644 --- a/modoboa/extensions/sievefilters/views.py +++ b/modoboa/extensions/sievefilters/views.py @@ -1,5 +1,6 @@ # coding: utf-8 from sievelib.managesieve import Error +from rfc6266 import build_header from django.http import HttpResponse from django.template.loader import render_to_string from django.contrib.auth.decorators import login_required @@ -263,7 +264,7 @@ def download_filters_set(request, name): resp = HttpResponse(script) resp["Content-Type"] = "text/plain; charset=utf-8" resp["Content-Length"] = len(script) - resp["Content-Disposition"] = 'attachment; filename="%s.txt"' % name + resp["Content-Disposition"] = build_header('%s.txt' % name) return resp @login_required diff --git a/modoboa/extensions/webmail/lib.py b/modoboa/extensions/webmail/lib.py index 8d39667cb..b8011d852 100644 --- a/modoboa/extensions/webmail/lib.py +++ b/modoboa/extensions/webmail/lib.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import email import lxml import chardet +from rfc6266 import build_header, parse_headers from django.core.files.uploadhandler import FileUploadHandler, SkipFile from django.utils.translation import ugettext as _, ugettext_lazy @@ -277,20 +278,24 @@ class ImapEmail(Email): attname = "part_%s" % att["pnum"] params = None key = None - if att.has_key("params") and att["params"] != "NIL": + if "params" in att and att["params"] != "NIL": params = att["params"] key = "name" - if key is None and \ - att.has_key("disposition") and len(att["disposition"]) > 1: + if key is None and "disposition" in att and len(att["disposition"]) > 1: params = att["disposition"][1] key = "filename" if key and params: for pos, value in enumerate(params): - if value == key: + if not value.startswith(key): + continue + header = "%s; %s=%s" % (att['disposition'][0], value, + u2u_decode.u2u_decode(params[pos + 1]).strip("\r\t\n")) + attname = parse_headers(header).filename_unsafe + if attname is None: attname = u2u_decode.u2u_decode(params[pos + 1]).strip("\r\t\n") - break + break self.attachments[att["pnum"]] = attname def _fetch_inlines(self): @@ -302,7 +307,7 @@ class ImapEmail(Email): params["fname"] = os.path.join(settings.MEDIA_URL, fname) if os.path.exists(path): continue - + pdef, content = self.imapc.fetchpart(self.mailid, self.mbox, params["pnum"]) fp = open(path, "wb") fp.write(decode_payload(params["encoding"], content)) @@ -613,7 +618,7 @@ def create_mail_attachment(attdef): res.set_payload(fp.read()) fp.close() Encoders.encode_base64(res) - res.add_header("Content-Disposition", 'attachment', filename=attdef["fname"]) + res['Content-Disposition'] = build_header(attdef['fname'].decode('utf-8')) return res diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index 113b831da..06234f912 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -43,16 +43,17 @@ def getattachment(request): resp = HttpResponse(decode_payload(partdef["encoding"], payload)) resp["Content-Type"] = partdef["Content-Type"] resp["Content-Transfer-Encoding"] = partdef["encoding"] + print partdef if partdef["disposition"] != 'NIL': disp = partdef["disposition"] # FIXME : ugly hack, see fetch_parser.py for more explanation # :p if type(disp[1][0]) != dict: - cd = '%s; %s="%s"' % (disp[0], disp[1][0], disp[1][1]) + cd = '%s; %s=%s' % (disp[0], disp[1][0], disp[1][1]) else: - cd = '%s; %s="%s"' % (disp[0], disp[1][0]['struct'][0], disp[1][0]['struct'][1]) + cd = '%s; %s=%s' % (disp[0], disp[1][0]['struct'][0], disp[1][0]['struct'][1]) else: - cd = 'attachment; filename="%s"' % request.GET["fname"] + cd = build_header(request.GET["fname"]) resp["Content-Disposition"] = cd resp["Content-Length"] = partdef["size"] return resp diff --git a/modoboa/lib/emailutils.py b/modoboa/lib/emailutils.py index 66fe831c1..a10381ffe 100644 --- a/modoboa/lib/emailutils.py +++ b/modoboa/lib/emailutils.py @@ -79,15 +79,15 @@ class Email(object): must be displayed and not attached. """ if self.dformat == "html" and self.links \ - and msg.has_key("Content-Disposition"): + and "Content-Disposition" in msg: if msg["Content-Disposition"].startswith("inline"): cid = None - if msg.has_key("Content-ID"): + if "Content-ID" in msg: m = re.match("<(.+)>", msg["Content-ID"]) cid = m is not None and m.group(1) or msg["Content-ID"] fname = msg.get_filename() if fname is None: - if msg.has_key("Content-Location"): + if "Content-Location" in msg: fname = msg["Content-Location"] elif cid is not None: fname = cid diff --git a/requirements.txt b/requirements.txt index 195f44718..33a9fecab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ pycrypto sievelib chardet argparse +rfc6266 From 88d755431aa30f74e0c870ae1caea209adaa8780 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 17 Sep 2013 15:03:26 +0200 Subject: [PATCH 33/97] Compress attachments before the HTTP proxy does. fix #480 --- modoboa/extensions/webmail/views.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modoboa/extensions/webmail/views.py b/modoboa/extensions/webmail/views.py index 06234f912..57eebb635 100644 --- a/modoboa/extensions/webmail/views.py +++ b/modoboa/extensions/webmail/views.py @@ -1,12 +1,14 @@ # coding: utf-8 -from django.conf import settings import os +from rfc6266 import build_header +from django.conf import settings from django.http import HttpResponse from django.template import Template, Context from django.utils.translation import ugettext as _, ungettext from django.core.urlresolvers import reverse from django.contrib.auth.decorators import login_required from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.gzip import gzip_page from modoboa.lib import parameters from modoboa.lib.webutils import _render, _render_to_string, \ ajax_response, ajax_simple_response @@ -20,8 +22,10 @@ from lib import decode_payload, AttachmentUploadHandler, \ ImapEmail, ReplyModifier, ForwardModifier from templatetags import webextras + @login_required @needs_mailbox() +@gzip_page def getattachment(request): """Fetch a message attachment @@ -36,14 +40,11 @@ def getattachment(request): if not mbox or not mailid or not pnum: raise WebmailError(_("Invalid request")) - headers = {"Content-Type": "text/plain", - "Content-Transfer-Encoding": None} imapc = get_imapconnector(request) partdef, payload = imapc.fetchpart(mailid, mbox, pnum) resp = HttpResponse(decode_payload(partdef["encoding"], payload)) resp["Content-Type"] = partdef["Content-Type"] resp["Content-Transfer-Encoding"] = partdef["encoding"] - print partdef if partdef["disposition"] != 'NIL': disp = partdef["disposition"] # FIXME : ugly hack, see fetch_parser.py for more explanation @@ -58,6 +59,7 @@ def getattachment(request): resp["Content-Length"] = partdef["size"] return resp + @login_required @needs_mailbox() def move(request): From 69cf92647c158003cd7d99132521bfd966e88d17 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 17 Sep 2013 15:05:04 +0200 Subject: [PATCH 34/97] Few fixes. --- modoboa/extensions/webmail/static/webmail/js/webmail.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modoboa/extensions/webmail/static/webmail/js/webmail.js b/modoboa/extensions/webmail/static/webmail/js/webmail.js index d60e2cfec..8ddd4c5b8 100644 --- a/modoboa/extensions/webmail/static/webmail/js/webmail.js +++ b/modoboa/extensions/webmail/static/webmail/js/webmail.js @@ -709,14 +709,14 @@ Webmail.prototype = { * is clicked) */ compose_loader: function(e) { - var $link = $(e.target); + var $link = get_target(e, 'a'); e.preventDefault(); this.navobject.reset().setparam("action", $link.attr("href")).update(); }, reply_loader: function(e) { - var $link = $(e.target); + var $link = get_target(e, "a"); e.preventDefault(); this.navobject.reset().updateparams($link.attr("href")).update(); }, From b96706c98d9392b5bac668cd06630552c689b78c Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Tue, 17 Sep 2013 19:47:05 +0200 Subject: [PATCH 35/97] Few fixes. --- modoboa/admin/static/admin/css/admin.css | 4 ++++ modoboa/admin/views.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/modoboa/admin/static/admin/css/admin.css b/modoboa/admin/static/admin/css/admin.css index a28919dd9..2768bf843 100644 --- a/modoboa/admin/static/admin/css/admin.css +++ b/modoboa/admin/static/admin/css/admin.css @@ -22,6 +22,10 @@ table .progress { margin-bottom: 0 !important; } +table tbody tr:hover { + background-color: #F9F9F9; +} + .search { width: 85%; -webkit-border-radius:14px;-moz-border-radius:14px;border-radius:14px; diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py index fac30eb03..1dbcef8e1 100644 --- a/modoboa/admin/views.py +++ b/modoboa/admin/views.py @@ -1,4 +1,7 @@ # coding: utf-8 +import cStringIO +import csv +from rfc6266 import build_header from django.http import HttpResponse, HttpResponseRedirect from django.utils.translation import ugettext as _, ungettext from django.core.urlresolvers import reverse @@ -9,8 +12,6 @@ from django.shortcuts import render from django.template.loader import render_to_string from django.db import transaction, IntegrityError from django.views.decorators.csrf import ensure_csrf_cookie -import cStringIO -import csv from lib import get_sort_order, get_listing_page from forms import * From 4510897020474ba435ef75a1a91c8ebdd6eb6b6b Mon Sep 17 00:00:00 2001 From: Louis-Dominique Dubeau Date: Tue, 17 Sep 2013 18:44:20 -0400 Subject: [PATCH 36/97] Added documentation on uwsgi. --- doc/integration/web_servers.rst | 206 +++++++++++++++++++++++++++++--- 1 file changed, 192 insertions(+), 14 deletions(-) diff --git a/doc/integration/web_servers.rst b/doc/integration/web_servers.rst index fbe187256..639aba65b 100644 --- a/doc/integration/web_servers.rst +++ b/doc/integration/web_servers.rst @@ -10,6 +10,14 @@ Web servers Apache2 ******* +.. note:: The following instructions are meant to help you get your + site up and running quickly. However it is not possible for + the people contributing documentation to Modoboa to test + every single combination of web server, wsgi server, + distribution, etc. So it is possible that **your** + installation of uwsgi or nginx or Apache or what-have-you + works differently. Keep this in mind. + mod_wgsi ======== @@ -37,13 +45,13 @@ following content inside:: WSGIScriptAlias / /wsgi.py -This is just one possible configuration. +This is just one possible configuration. .. note:: *Django* 1.3 users, please consult this `page `_, it contains an example *wsgi.py* file. -.. note:: +.. note:: You will certainly need more configuration in order to launch Apache. @@ -77,9 +85,9 @@ following content inside:: -This is just one possible configuration. +This is just one possible configuration. -.. note:: +.. note:: You will certainly need more configuration in order to launch Apache. @@ -89,17 +97,27 @@ This is just one possible configuration. Nginx ***** +.. note:: The following instructions are meant to help you get your + site up and running quickly. However it is not possible for + the people contributing documentation to Modoboa to test + every single combination of web server, wsgi server, + distribution, etc. So it is possible that **your** + installation of uwsgi or nginx or Apache or what-have-you + works differently. Keep this in mind. + +Green Unicorn +============= + `Nginx `_ is a really fast HTTP server. Associated with `Green Unicorn `_, it gives you one of the -best setup to serve python/Django applications. Modoboa's -performances are really good with this configuration. +best setups to serve python/Django applications. Modoboa's +performance is really good with this configuration. -To use this setup, first download and install those softwares -(`Install gunicorn `_ and `install -nginx `_). - -Then, use the following sample *gunicorn* configuration (create a new -file named *gunicorn.conf.py* inside Modoboa's root dir):: +To use this setup, first download and install `nginx +`__ and `gunicorn +`_. Then, use the following sample +*gunicorn* configuration (create a new file named *gunicorn.conf.py* +inside Modoboa's root dir):: backlog = 2048 bind = "unix:/var/run/gunicorn/modoboa.sock" @@ -123,13 +141,19 @@ following configuration:: } server { - listen 443; + listen 443 ssl; + ssl on; + keepalive_timeout 70; + server_name ; root ; access_log /var/log/nginx/.access.log; error_log /var/log/nginx/.error.log; + ssl_certificate ; + ssl_certificate_key ; + location /sitestatic/ { autoindex on; } @@ -143,10 +167,164 @@ following configuration:: proxy_set_header Host $http_host; proxy_redirect off; proxy_set_header X-Forwarded-Protocol ssl; - proxy_pass http://modoboa; + proxy_pass http://modoboa; } } +If you do not plan to use SSL then change the listen directive to +``listen 80;`` and delete each of the following directives:: + + ssl on; + keepalive_timeout 70; + ssl_certificate ; + ssl_certificate_key ; + proxy_set_header X-Forwarded-Protocol ssl; + +If you do plan to use SSL, you'll have to generate a certificate and a +key. `This article +`__ +contains information about how to do it. + Paste this content to your configuration (replace values between ``<>`` with yours), restart *nginx* and enjoy a really fast application! + +uwsgi +===== + +The following setup is meant to get you started quickly. You should +read the documentation of both nginx and uwsgi to understand how to +optimize their configuration for your site. + +The Django documentation includes the following warning regarding +uwsgi: + +.. warning:: Some distributions, including Debian and Ubuntu, ship an + outdated version of uWSGI that does not conform to the + WSGI specification. Versions prior to 1.2.6 do not call + close on the response object after handling a request. In + those cases the request_finished signal isn’t sent. This + can result in idle connections to database and memcache + servers. + +Use uwsgi 1.2.6 or newer. If you do not, you *will* run into +problems. Modoboa will fail in obscure ways. + +To use this setup, first download and install `nginx +`__ and `uwsgi +`_. + +Here is a sample nginx configuration:: + + server { + listen 443 ssl; + ssl on; + keepalive_timeout 70; + + server_name ; + root ; + + ssl_certificate ; + ssl_certificate_key ; + + access_log /var/log/nginx/.access.log; + error_log /var/log/nginx/.error.log; + + location /sitestatic/ { + autoindex on; + alias ; + } + + # Whether or not Modoboa uses a media directory depends on how + # you configured Modoboa. It does not hurt to have this. + location /media/ { + autoindex on; + alias ; + } + + # This denies access to any file that begins with + # ".ht". Apache's .htaccess and .htpasswd are such files. A + # Modoboa installed from scratch would not contain any such + # files, but you never know what the future holds. + location ~ /\.ht { + deny all; + } + + location / { + include uwsgi_params; + uwsgi_pass ; + uwsgi_param UWSGI_SCRIPT modoboa_server.wsgi:application + uwsgi_param UWSGI_SCHEME https; + } + } + +If you do not plan to use SSL then change the listen directive to +``listen 80;`` and delete each of the following directives:: + + ssl on; + keepalive_timeout 70; + ssl_certificate ; + ssl_certificate_key ; + uwsgi_param UWSGI_SCHEME https; + +If you do plan to use SSL, you'll have to generate a certificate and a +key. `This article +`__ +contains information about how to do it. + +Make sure to replace the ``<...>`` in the sample configuration with +appropriate values. Here are some explanations for the cases that may +not be completely self-explanatory: + +```` + Where Modoboa's :file:`settings.py` resides. This is also where the + :file:`sitestatic` and :file:`media` directories reside. + +```` + This is the URL which will be the root of your Modoboa site at your + domain. For instance, if your Modoboa installation is reachable at + at ``https://foo/modoboa`` then ```` is + ``/modoboa``. In this case you probably also have to set the + ``alias`` directives to point to where Modoboa's sitestatic and + media directories are because otherwise nginx won't be able to find + them. + + If Modoboa is at the root of your domain, then ```` is an empty string and can be deleted from the configuration + above. In this case, you probably do not need the ``alias`` + directives. + +```` + The location where uwsig is listening. It could be a unix domain + socket or an address:port combination. Ubuntu configures uwsgi so + that the port is:: + + unix:/run/uwsgi/app//socket + + where ```` is the name of the application. + +Your uwsgi configuration should be:: + + [uwsgi] + # Not needed when using uwsgi from pip + # plugins = python + chdir = + module = .wsgi:application + master = true + harakiri = 30 + sharedarea = 4 + processes = 4 + vhost = true + no-default-app = true + +The plugins directive should be turned on if you use a uwsgi +installation that requires it. If uwsgi was installed from pip, it +does not require it. In the configuration above: + +```` + The directory where :file:`manage.py` resides. This directory is the + parent of ```` + +```` + The name that you passed to ``modoboa-admin.py deploy`` when you + created your Modoboa instance. From dc00f7d2fd27c8709f0ad07f8fa32d144a34bcbf Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 18 Sep 2013 09:46:28 +0200 Subject: [PATCH 37/97] Properly decode URI components before use them. see #481 --- modoboa/static/js/history.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modoboa/static/js/history.js b/modoboa/static/js/history.js index 72cd2c069..d582219a1 100644 --- a/modoboa/static/js/history.js +++ b/modoboa/static/js/history.js @@ -179,7 +179,7 @@ History.prototype = { }, getbaseurl: function() { - return this.base.substr(0, this.base.length - 1); + return decodeURIComponent(this.base.substr(0, this.base.length - 1)); }, delparam: function(name) { From 6738cecb4446ed2e306597e785eb7d3d11c59c89 Mon Sep 17 00:00:00 2001 From: ricardopadilha Date: Wed, 18 Sep 2013 16:04:47 +0200 Subject: [PATCH 38/97] Added missing comma in settings.py.tpl A missing comma in settings.py.tpl breaks new deployments. --- modoboa/core/templates/settings.py.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modoboa/core/templates/settings.py.tpl b/modoboa/core/templates/settings.py.tpl index 67aa8c691..a80b25769 100644 --- a/modoboa/core/templates/settings.py.tpl +++ b/modoboa/core/templates/settings.py.tpl @@ -212,7 +212,7 @@ LOGGING = { 'class': 'logging.handlers.SysLogHandler', 'facility': SysLogHandler.LOG_CRON, 'formatter': 'syslog' - } + }, 'modoboa': { 'class': 'modoboa.lib.logutils.SQLHandler', } From c207b96a7420785f1e984f08dbfef9ddf01f1e4b Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 18 Sep 2013 21:11:06 +0200 Subject: [PATCH 39/97] Useless configuration removed. --- modoboa/core/templates/settings.py.tpl | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/modoboa/core/templates/settings.py.tpl b/modoboa/core/templates/settings.py.tpl index a80b25769..4f926f712 100644 --- a/modoboa/core/templates/settings.py.tpl +++ b/modoboa/core/templates/settings.py.tpl @@ -208,11 +208,6 @@ LOGGING = { 'facility': SysLogHandler.LOG_AUTH, 'formatter': 'syslog' }, - 'syslog-cron': { - 'class': 'logging.handlers.SysLogHandler', - 'facility': SysLogHandler.LOG_CRON, - 'formatter': 'syslog' - }, 'modoboa': { 'class': 'modoboa.lib.logutils.SQLHandler', } @@ -232,11 +227,6 @@ LOGGING = { 'handlers': ['modoboa'], 'level': 'INFO', 'propagate': False - }, - 'modoboa.cron': { - 'handlers': ['syslog-cron'], - 'level': 'INFO', - 'propagate': False } } } From 98f3bbda0fa0ff16a5b39183917c9a5ec65dfc95 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 18 Sep 2013 22:14:36 +0200 Subject: [PATCH 40/97] Consistency. --- doc/additional/migrating.rst | 4 +- doc/additional/virtualmachine.rst | 11 ++--- doc/extending/plugin_api.rst | 6 +-- doc/getting_started/configuration.rst | 16 +++---- doc/getting_started/install.rst | 12 ++--- doc/getting_started/plugins.rst | 28 +++++------ doc/getting_started/upgrade.rst | 58 +++++++++++----------- doc/integration/imap_and_smtp.rst | 69 ++++++++++++++------------- doc/integration/web_servers.rst | 61 ++++++++++++----------- 9 files changed, 134 insertions(+), 131 deletions(-) diff --git a/doc/additional/migrating.rst b/doc/additional/migrating.rst index beaaa66c1..448e7fdd1 100644 --- a/doc/additional/migrating.rst +++ b/doc/additional/migrating.rst @@ -16,7 +16,7 @@ existing `PostfixAdmin (version 2.3.3+) First, you must follow the :ref:`installation` step to create a fresh Modoboa database. -Once done, edit the *setting.py* file. First, add a new database +Once done, edit the :file:`settings.py` file. First, add a new database connection named ``pfxadmin`` into the ``DATABASES`` variable corresponding to your PostfixAdmin setup:: @@ -56,7 +56,7 @@ used by PostfixAdmin, check its configuration file if you're not sure) Click on the *Save* button. -The procedure is over, edit the *settings.py* file and: +The procedure is over, edit the :file:`settings.py` file and: * remove the ``pfxadmin`` database connection from the ``DATABASES`` variable diff --git a/doc/additional/virtualmachine.rst b/doc/additional/virtualmachine.rst index 942da5a10..471c2c418 100644 --- a/doc/additional/virtualmachine.rst +++ b/doc/additional/virtualmachine.rst @@ -4,9 +4,9 @@ Using the virtual machine Introduction ============ -A virtual machine with a ready-to-use *Modoboa* setup is available -`here `_. It is composed -of the following components: +A virtual machine with a ready-to-use Modoboa setup is available `here +`_. It is composed of +the following components: * Debian 6.0 (squeeze) * Modoboa and its prerequisites @@ -32,7 +32,7 @@ If you can't use the vmdk format, you can use `qemu Then, just use your prefered virtualization software (qemu, kvm, virtualbox, etc.) to start the machine. You'll need to configure at least one bridged network interface if you want to be able to play -with *Modoboa*, ie. your machine must be visible from your network. +with Modoboa, ie. your machine must be visible from your network. The default network interface of the machine (``eth0``) is configured to use the DHCP protocol. @@ -49,7 +49,7 @@ root demo the root user demo demo an unpriviliged user ===== ======== ==================== -To connect to *Modoboa*, first connect to the system and retrieve its +To connect to Modoboa, first connect to the system and retrieve its current network address like this:: $ /sbin/ifconfig eth0 @@ -72,4 +72,3 @@ admin@demo.local admin Administrator of the domain *demo.local*. user@demo.local user Simple user. Can access to applications. ================ ======== ============================================ - diff --git a/doc/extending/plugin_api.rst b/doc/extending/plugin_api.rst index b6d2e5f27..a96381405 100644 --- a/doc/extending/plugin_api.rst +++ b/doc/extending/plugin_api.rst @@ -33,12 +33,12 @@ The required glue ***************** To create a new plugin, just start a new django application like -this (into modoboa's directory):: +this (into Modoboa's directory):: $ python manage.py startapp Then, you need to register this application using the provided -API. Just copy/paste the following example into the *__init__.py* file +API. Just copy/paste the following example into the :file:`__init__.py` file of the future extension:: from modoboa.extensions import ModoExtension, exts_pool @@ -72,7 +72,7 @@ of the future extension:: exts_pool.register_extension(MyExtension) Once done, simply add your plugin's module name to the -``INSTALLED_APPS`` variable located inside *settings.py*. Optionaly, +``INSTALLED_APPS`` variable located inside :file:`settings.py`. Optionaly, run ``python manage.py syncdb`` if your plugin provides custom tables and ``python manage.py collectstatic`` to update static files. diff --git a/doc/getting_started/configuration.rst b/doc/getting_started/configuration.rst index cad72e254..efcf39658 100644 --- a/doc/getting_started/configuration.rst +++ b/doc/getting_started/configuration.rst @@ -86,7 +86,7 @@ Custom logo You have the possibility to use a custom logo instead of the default one on the login page. -To do so, open the *settings.py* file and add a +To do so, open the :file:`settings.py` file and add a ``MODOBOA_CUSTOM_LOGO`` variable. This variable must contain the relative URL of your logo under ``MEDIA_URL``. For example:: @@ -107,7 +107,7 @@ Host configuration To manipulate mailboxes on the filesystem, you must allow the user who runs Modoboa to execute commands as the user who owns mailboxes. -To do so, edit the */etc/sudoers* file and add the following inside:: +To do so, edit the :file:`/etc/sudoers` file and add the following inside:: ALL=() NOPASSWD: ALL @@ -121,7 +121,7 @@ Time zone and language Modoboa is available in many languages. -To specify the default language to use, edit the *settings.py* file +To specify the default language to use, edit the :file:`settings.py` file and modify the ``LANGUAGE_CODE`` variable:: LANGUAGE_CODE = 'fr' # or 'en' for english, etc. @@ -143,7 +143,7 @@ Modoboa uses `Django's session framework `_ to store per-user information. -Few parameters need to be set in the *settings.py* configuration +Few parameters need to be set in the :file:`settings.py` configuration file to make Modoboa behave as expected:: SESSION_EXPIRE_AT_BROWSER_CLOSE = False # Default value @@ -151,7 +151,7 @@ file to make Modoboa behave as expected:: This parameter is optional but you must ensure it is set to ``False`` (the default value). -The default configuration file provided by the *modoboa-admin.py* +The default configuration file provided by the ``modoboa-admin.py`` command is properly configured. **** @@ -163,7 +163,7 @@ LDAP Authentication ============== -*Modoboa* supports external LDAP authentication using the following extra components: +Modoboa supports external LDAP authentication using the following extra components: * `Python LDAP client `_ * `Django LDAP authentication backend `_ @@ -172,7 +172,7 @@ If you want to use this feature, you must first install those components:: $ pip install python-ldap django-auth-ldap -Then, all you have to do is to modify the *settings.py* file. Add a +Then, all you have to do is to modify the :file:`settings.py` file. Add a new authentication backend to the `AUTHENTICATION_BACKENDS` variable, like this:: @@ -247,7 +247,7 @@ If you need additional parameters, you will find a detailled documentation `here `_. Once the authentication is properly configured, the users defined in -your LDAP directory will be able to connect to *Modoboa*, the associated +your LDAP directory will be able to connect to Modoboa, the associated domain and mailboxes will be automatically created if needed. .. note:: diff --git a/doc/getting_started/install.rst b/doc/getting_started/install.rst index 426f6b89d..1c4f21d6e 100644 --- a/doc/getting_started/install.rst +++ b/doc/getting_started/install.rst @@ -30,8 +30,8 @@ You can choose between two options: * Use the Python package available on the `PyPI `_ * Download the sources tarball -The easiest one is to install it from the *PyPI*. Just run the -following command and you're done:: +The easiest one is to install it from PyPI. Just run the following +command and you're done:: $ pip install modoboa @@ -56,8 +56,8 @@ package. On Debian/Ubuntu:: `virtualenv `_ users ========================================================== -When you deploy an application using *virtualenv*, you may have to -compile some dependencies. For example, *modoboa* relies on *lxml*, +When you deploy an application using virtualenv, you may have to +compile some dependencies. For example, modoboa relies on lxml, which is a C python module. In order to install it, you will need to install the following requirements: @@ -65,7 +65,7 @@ install the following requirements: * libxslt development files * libxml2 development files -On a *Debian* like system, just run the following command:: +On a Debian like system, just run the following command:: $ apt-get install python-dev libxml2-dev libxslt-dev @@ -75,7 +75,7 @@ On a *Debian* like system, just run the following command:: Database ******** -Thanks to *django*, Modoboa supports several databases. Depending on +Thanks to Django, Modoboa supports several databases. Depending on the one you will use, you must install the appropriate python package: * `mysqldb `_ for `MySQL `_ diff --git a/doc/getting_started/plugins.rst b/doc/getting_started/plugins.rst index 5fcfcffb6..cb47c879f 100644 --- a/doc/getting_started/plugins.rst +++ b/doc/getting_started/plugins.rst @@ -50,11 +50,11 @@ This plugin provides a simple management frontend for `amavisd-new The per-domain policies feature only works for new installations. Currently, you can't use modoboa with an existing - database (ie. with data in *users* and *policies* tables). + database (ie. with data in ``users`` and ``policies`` tables). .. note:: - This plugin requires *amavisd-new* version **2.7.0** or higher. If + This plugin requires amavisd-new version **2.7.0** or higher. If you're planning to use the :ref:`selfservice`, you'll need version **2.8.0**. @@ -62,7 +62,7 @@ Database ======== You must tell to Modoboa where it can find the amavis -database. Inside *settings.py*, add a new connection to the +database. Inside :file:`settings.py`, add a new connection to the ``DATABASES`` variable like this:: DATABASES = { @@ -81,7 +81,7 @@ Replace values between ``<>`` with yours. .. note:: - Modoboa doesn't create *amavis* tables. You need to install them + Modoboa doesn't create amavis tables. You need to install them following the `official documentation `_. @@ -106,7 +106,7 @@ Release messages To release messages, first take a look at `this page `_. It -explains how to configure *amavisd-new* to listen somewhere for the +explains how to configure amavisd-new to listen somewhere for the AM.PDP protocol. This protocol is used to send requests. Below is an example of a working configuration:: @@ -126,7 +126,7 @@ Below is an example of a working configuration:: Don't forget to update the ``inet_acl`` list if you plan to release from the network. -Once *amavisd-new* is configured, just tell Modoboa where it can find +Once amavisd-new is configured, just tell Modoboa where it can find the *release server* by modifying the following parameters in the online panel: @@ -212,7 +212,7 @@ the required identifiers. To activate this feature, go the administration panel and set the **Enable self-service mode** parameter to yes. -The last step is to customize the notification messages *amavis* +The last step is to customize the notification messages amavis sends. The most important is to embark a direct link. Take a look at the `README.customize `_ file to learn what you're allowed to do. @@ -229,7 +229,7 @@ Graphical statistics This plugin collects various statistics about emails traffic on your server. It parses a log file to collect information, store it into RRD -files (see `rrdtool `_)and then +files (see `rrdtool `_) and then generates graphics in PNG format. To use it, go to the online parameters panel and adapt the following @@ -273,15 +273,15 @@ Postifx auto-reply messages *************************** This plugin let users define an auto-reply message (*vacation*). It is -based on *postfix* capabilities. +based on Postfix capabilities. The user that executes the autoreply script needs to access -*settings.py*. You must apply proper permissions on this file. For -example, if *settings.py* belongs to *www-data:www-data*, you can add -the *vmail* user to the *www-data* group and set the read permission +:file:`settings.py`. You must apply proper permissions on this file. For +example, if :file:`settings.py` belongs to ``www-data:www-data``, you can add +the ``vmail`` user to the ``www-data`` group and set the read permission for the group. -To make *postfix* use this feature, you need to update your +To make Postfix use this feature, you need to update your configuration files as follows: ``/etc/postfix/main.cf``:: @@ -334,7 +334,7 @@ Two working modes are available: To use this plugin, your hosting setup must include a *ManageSieve* server and your local delivery agent must understand the *Sieve* -language. Don't panic, *Dovecot* supports both :-) (refer to +language. Don't panic, Dovecot supports both :-) (refer to :ref:`dovecot` to know how to enable those features). .. note:: diff --git a/doc/getting_started/upgrade.rst b/doc/getting_started/upgrade.rst index fcf7fc452..9015d9e0a 100644 --- a/doc/getting_started/upgrade.rst +++ b/doc/getting_started/upgrade.rst @@ -56,7 +56,7 @@ been improved. Make sure to consult :ref:`fs_operations`. Configuration file update ------------------------- -Several modifications need to be done into *settings.py*. +Several modifications need to be done into :file:`settings.py`. #. Add the following import statement:: @@ -190,7 +190,7 @@ It is necessary to update the queries used to retrieve users and mailboxes: #. Run ``modoboa-admin.py postfix_maps --dbtype `` and compare the files with those that postfix currently use. Make necessary updates in light of the differences -#. Into *dovecot-sql.conf*, update the ``user_query`` query, refer to +#. Into :file:`dovecot-sql.conf`, update the ``user_query`` query, refer to :ref:`dovecot_mysql_queries` or :ref:`dovecot_pg_queries` #. Update dovecot's configuration to activate the new :ref:`quota related features ` @@ -207,7 +207,7 @@ may encounter the following issues: #. South fails to migrate ``reversion`` - Due to the admin user model change, the script *0001_initial.py* + Due to the admin user model change, the script :file:`0001_initial.py` may fail. Just deactivate ``reversion`` from ``INSTALLED_APPS`` and run the command again. Once done, reactivate ``reversion`` and run the command one last time. @@ -216,7 +216,7 @@ may encounter the following issues: 0.9.4: administrative panel performance improved ================================================ -#. Edit the *settings.py* file and remove +#. Edit the :file:`settings.py` file and remove ``'django.contrib.auth.backends.ModelBackend'`` from the ``AUTHENTICATION_BACKENDS`` variable @@ -229,10 +229,10 @@ For this version, we recommend to install a new instance (see Then, copy the following content from the old installation to the new one: -* The ``media`` directory +* The :file:`media` directory * The directory containing RRD files if you use the :ref:`stats` plugin -Don't copy the old *settings.py* file, just keep the new one and +Don't copy the old :file:`settings.py` file, just keep the new one and modify it (see :ref:`database` and :ref:`timezone_lang`). Migrate your database (see :ref:`latestversion`). @@ -260,16 +260,16 @@ requires a migration. .. note:: Many files have been renamed/removed for this version. I recommend that you backup important files (*settings.py*, etc.) elsewhere - (ie. */tmp* for example). Then, remove the *modoboa* directory, + (ie. :file:`/tmp` for example). Then, remove the :file:`modoboa` directory, extract the new tarball at the same place, rename the new directory - to *modoboa* and copy the files you've just backup into it. + to :file:`modoboa` and copy the files you've just backup into it. .. note:: If the first super administrator you created is named ``admin``, its password will be changed to ``password`` at the end of this upgrade. Don't forget to modify it! -#. Edit the *settings.py* file and update the following variables +#. Edit the :file:`settings.py` file and update the following variables (just copy/paste their new content):: MIDDLEWARE_CLASSES = ( @@ -326,10 +326,10 @@ requires a migration. $ ./manage.py collectstatic #. If you are using the *stats* extension, please rename the - */static/stats* directory to */media/stats* + :file:`/static/stats` directory to :file:`/media/stats` and change the value of the ``IMG_ROOTDIR`` parameter (go to the adminstration panel) -#. Restart the python instance(s) that serve *Modoboa* +#. Restart the python instance(s) that serve Modoboa #. Log into Modoboa, go to *Modoboa > Extensions*, uncheck all extensions, save. Then, check the extensions you want to use and @@ -339,7 +339,7 @@ requires a migration. (see :ref:`webservers`) #. **For Dovecot users only**, you need to modify the - ``password_query`` (file */etc/dovecot/dovecot-sql.conf* by default + ``password_query`` (file :file:`/etc/dovecot/dovecot-sql.conf` by default on a Debian system) like this:: password_query = SELECT email AS user, password FROM auth_user WHERE email='%u' @@ -347,7 +347,7 @@ requires a migration. 0.8.8: CSV import feature and minor fixes ========================================= -#. Edit the *settings.py* file and add +#. Edit the :file:`settings.py` file and add ``'modoboa.lib.middleware.AjaxLoginRedirect'`` to the ``MIDDLEWARE_CLASSES`` variable like this:: @@ -362,7 +362,7 @@ requires a migration. 'modoboa.extensions.webmail.middleware.WebmailErrorMiddleware', ) -#. Still inside *settings.py*, modify the ``DATABASE_ROUTERS`` +#. Still inside :file:`settings.py`, modify the ``DATABASE_ROUTERS`` variable like this:: DATABASE_ROUTERS = ["modoboa.extensions.amavis_quarantine.dbrouter.AmavisRouter"] @@ -371,7 +371,7 @@ requires a migration. 0.8.7: per-user language selection ================================== -#. Edit the *settings.py* file and add the +#. Edit the :file:`settings.py` file and add the ``'django.middleware.locale.LocaleMiddleware'`` middleware to the ``MIDDLEWARE_CLASSES`` variable like this:: @@ -404,7 +404,7 @@ requires a migration. ========================================================= #. Just update your configuration if you are using the quarantine - plugin. Open *settings.py*, move the database configuration from + plugin. Open :file:`settings.py`, move the database configuration from the ``DB_CONNECTIONS`` variable to the ``DATABASES`` variable, like this:: @@ -426,7 +426,7 @@ requires a migration. DATABASE_ROUTERS = ["modoboa.extensions.amavis_quarantine.dbrouter.AmavisRouter"] -#. Remove the deprecated ``DB_CONNECTIONS`` variable from *settings.py*. +#. Remove the deprecated ``DB_CONNECTIONS`` variable from :file:`settings.py`. 0.8.5: new "Sieve filters" plugin, improved admin app ===================================================== @@ -437,7 +437,7 @@ requires a migration. $ python manage.py migrate admin #. Add ``modoboa.auth`` and ``modoboa.extensions.sievefilters`` to the - ``INSTALLED_APPS`` variable in *settings.py*. + ``INSTALLED_APPS`` variable in :file:`settings.py`. #. Go the *Settings/Extensions* panel, deactivate and activate your extensions, it will update all the symbolic links. @@ -445,7 +445,7 @@ requires a migration. 0.8.4: folders manipulation support (webmail) and bugfixes ========================================================== -#. Update the ``MIDDLEWARE_CLASSES`` variable in *settings.py*:: +#. Update the ``MIDDLEWARE_CLASSES`` variable in :file:`settings.py`:: MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', @@ -460,7 +460,7 @@ requires a migration. extensions, it will update all the symbolic links to the new format. #. Optional: update the ``DATABASES`` and ``TEMPLATE_LOADERS`` - variables in *settings.py* to remove warning messages (appearing with + variables in :file:`settings.py` to remove warning messages (appearing with Django 1.3):: DATABASES = { @@ -482,14 +482,14 @@ requires a migration. 0.8.3: admin application refactoring and more ============================================= -#. Migrate the *admin* application:: +#. Migrate the ``admin`` application:: $ python manage.py migrate admin #. Update SQL queries used in your environnement (see :ref:`postfix` or :ref:`dovecot`). -#. Update *postfix* configuration so that it can handle domain aliases +#. Update Postfix configuration so that it can handle domain aliases (see :ref:`postfix`). @@ -502,9 +502,9 @@ requires a migration. #. Update your config file and add all extensions to ``INSTALLED_APPS`` (even those you are not going to use). -#. Inside the */templates/* directory, remove all symbolic links. -#. Download the latest release of ckeditor and extract it into */static/js/*. It should create a new directory named *ckeditor*. -#. Update the following variables inside *settings.py*:: +#. Inside the :file:`/templates/` directory, remove all symbolic links. +#. Download the latest release of ckeditor and extract it into :file:`/static/js/`. It should create a new directory named ``ckeditor``. +#. Update the following variables inside :file:`settings.py`:: MEDIA_ROOT = os.path.join(MODOBOA_DIR, 'static') MEDIA_URL = '/static/' @@ -518,9 +518,9 @@ requires a migration. 0.8.1 : project renamed ======================= -#. First, rename the *mailng* directory to *modoboa* and copy all the - content from *modoboa-0.8.1* to *modoboa*. -#. Edit *settings.py* and replace all occurences of mailng by +#. First, rename the ``mailng`` directory to ``modoboa`` and copy all the + content from ``modoboa-0.8.1`` to ``modoboa``. +#. Edit :file:`settings.py` and replace all occurences of mailng by modoboa. Make sure you don't modify the ``DATABASE`` section as you're not going to rename your database. #. Rename the ``MAILNG_DIR`` variable to ``MODOBOA_DIR``. @@ -554,7 +554,7 @@ Before you start the migration, make sure you have updated your 'mailng.userprefs', ) -Starting with 0.8, mailng.main doesn't exist anymore. You must remove +Starting with 0.8, ``mailng.main`` doesn't exist anymore. You must remove it from your ``INSTALLED_APPS``. Finally, run the following commands:: diff --git a/doc/integration/imap_and_smtp.rst b/doc/integration/imap_and_smtp.rst index 3280b5af2..50d396112 100644 --- a/doc/integration/imap_and_smtp.rst +++ b/doc/integration/imap_and_smtp.rst @@ -12,12 +12,12 @@ Modoboa works better with Dovecot 2.0 so the following documentation is suitable for this combination. In this section, we assume dovecot's configuration resides in -*/etc/dovecot*, all pathes will be relative to this directory. +:file:`/etc/dovecot`, all pathes will be relative to this directory. Mailboxes ========= -First, edit the *conf.d/10-mail.conf* and set the ``mail_location`` +First, edit the :file:`conf.d/10-mail.conf` and set the ``mail_location`` variable:: # maildir @@ -63,7 +63,7 @@ stored (see :ref:`admin-params` to activate this feature). Those operations are treated asynchronously by a cron script. For example, when you rename an e-mail address through the web UI, the associated mailbox on the file system is not modified -directly. Instead of that, a 'rename' order is created for this +directly. Instead of that, a *rename* order is created for this mailbox. The mailbox will be considered unavailable until the order is not executed (see :ref:`Postfix configuration `). @@ -82,7 +82,7 @@ And add the following line inside:: .. warning:: The user running the cron script must have access to the - *settings.py* file of the modoboa instance. + :file:`settings.py` file of the modoboa instance. The result of each order is recorded into Modoboa's log. Go to *Modoboa > Logs* to consult them. @@ -91,7 +91,7 @@ The result of each order is recorded into Modoboa's log. Go to Authentication ============== -To make the authentication work, edit the *conf.d/10-auth.conf* and +To make the authentication work, edit the :file:`conf.d/10-auth.conf` and uncomment the following line at the end:: #!include auth-system.conf.ext @@ -103,7 +103,7 @@ uncomment the following line at the end:: #!include auth-static.conf.ext -Then, edit the *conf.d/auth-sql.conf.ext* file and add the following +Then, edit the :file:`conf.d/auth-sql.conf.ext` file and add the following content inside:: passdb sql { @@ -120,13 +120,13 @@ content inside:: Make sure to activate only one backend (per type) inside your configuration (just comment the other ones). -Edit the *dovecot-sql.conf.ext* and modify the configuration according +Edit the :file:`dovecot-sql.conf.ext` and modify the configuration according to your database engine. .. _dovecot_mysql_queries: -*MySQL* users -------------- +MySQL users +----------- :: @@ -144,8 +144,8 @@ to your database engine. .. _dovecot_pg_queries: -*PostgreSQL* users ------------------- +PostgreSQL users +---------------- :: @@ -169,7 +169,7 @@ LDA === The LDA is activated by default but you must define a *postmaster* -address. Open the *conf.d/15-lda.conf* file modify the following line:: +address. Open the :file:`conf.d/15-lda.conf` file modify the following line:: postmaster_address = postmaster@ @@ -182,12 +182,12 @@ Modoboa lets adminstrators define per-domain and/or per-account limits (quota). It also lists the current quota usage of each account. Those features require Dovecot to be configured in a specific way. -Inside *conf.d/10-mail.conf*, add the ``quota`` plugin to the default +Inside :file:`conf.d/10-mail.conf`, add the ``quota`` plugin to the default activated ones:: mail_plugins = quota -Inside *conf.d/10-master.conf*, update the *dict* service to set +Inside :file:`conf.d/10-master.conf`, update the ``dict`` service to set proper permissions:: service dict { @@ -200,7 +200,7 @@ proper permissions:: } } -Inside *conf.d/20-imap.conf*, activate the ``imap_quota`` plugin:: +Inside :file:`conf.d/20-imap.conf`, activate the ``imap_quota`` plugin:: protocol imap { # ... @@ -210,13 +210,13 @@ Inside *conf.d/20-imap.conf*, activate the ``imap_quota`` plugin:: # ... } -Inside *dovecot.conf*, activate the quota SQL dictionary backend:: +Inside :file:`dovecot.conf`, activate the quota SQL dictionary backend:: dict { quota = :/etc/dovecot/dovecot-dict-sql.conf.ext } -Inside *conf.d/90-quota.conf*, activate the *quota dictionary* backend:: +Inside :file:`conf.d/90-quota.conf`, activate the *quota dictionary* backend:: plugin { quota = dict:User quota::proxy::quota @@ -224,7 +224,8 @@ Inside *conf.d/90-quota.conf*, activate the *quota dictionary* backend:: It will tell Dovecot to keep quota usage in the SQL dictionary. -Finally, edit the *dovecot-dict-sql.conf.ext* file and put the following content inside:: +Finally, edit the :file:`dovecot-dict-sql.conf.ext` file and put the +following content inside:: connect = host= dbname= user= password= @@ -247,9 +248,9 @@ Finally, edit the *dovecot-dict-sql.conf.ext* file and put the following content Database schema update ^^^^^^^^^^^^^^^^^^^^^^ -The `admin_quota` table is created by *Django* but unfortunately it -doesn't support `DEFAULT` constraints (it only simulates them when the -ORM is used). As *PostgreSQL* is a bit strict about constraint +The ``admin_quota`` table is created by Django but unfortunately it +doesn't support ``DEFAULT`` constraints (it only simulates them when the +ORM is used). As PostgreSQL is a bit strict about constraint violations, you must execute the following query manually:: db=> ALTER TABLE admin_quota ALTER COLUMN bytes SET DEFAULT 0; @@ -318,8 +319,8 @@ work for Modoboa. You should use the following one instead: CREATE TRIGGER setmboxid AFTER INSERT ON admin_quota FOR EACH ROW EXECUTE PROCEDURE set_mboxid(); -Copy this example into a file (for example: *quota-trigger.sql*) on -server running *postgres* and execute the following commands:: +Copy this example into a file (for example: :file:`quota-trigger.sql`) on +server running postgres and execute the following commands:: $ su - postgres $ psql < /path/to/quota-trigger.sql @@ -332,7 +333,7 @@ For existing installations, *Dovecot* (> 2) offers a command to recalculate the current quota usages. For example, if you want to update all usages, run the following command:: - $ doveadmin quota recalc -A + $ doveadm quota recalc -A Be carefull, it can take a while to execute. @@ -342,7 +343,7 @@ ManageSieve/Sieve Modoboa lets users define filtering rules from the web interface. To do so, it requires *ManageSieve* to be activated on your server. -Inside *conf.d/20-managesieve.conf*, make sure the following lines are +Inside :file:`conf.d/20-managesieve.conf`, make sure the following lines are uncommented:: protocols = $protocols sieve @@ -361,7 +362,7 @@ uncommented:: Messages filtering using Sieve is done by the LDA. -Inside *conf.d/15-lda.conf*, activate the ``sieve`` plugin like this:: +Inside :file:`conf.d/15-lda.conf`, activate the ``sieve`` plugin like this:: protocol lda { # Space separated list of plugins to load (default is global mail_plugins). @@ -369,7 +370,7 @@ Inside *conf.d/15-lda.conf*, activate the ``sieve`` plugin like this:: } Finally, configure the ``sieve`` plugin by editing the -*conf.d/90-sieve.conf* file. Put the follwing content inside:: +:file:`conf.d/90-sieve.conf` file. Put the follwing caontent inside:: plugin { # Location of the active script. When ManageSieve is used this is actually @@ -382,7 +383,7 @@ Finally, configure the ``sieve`` plugin by editing the sieve_dir = ~/sieve } -Restart *Dovecot*. +Restart Dovecot. .. _postfix: @@ -398,22 +399,22 @@ Map files ========= You first need to create configuration files (or map files) that will -be used by *postfix* to lookup into Modoboa tables. +be used by Postfix to lookup into Modoboa tables. To automaticaly generate the requested map files and store them in a directory, run the following command:: $ modoboa-admin.py postfix_maps --dbtype mapfiles -``mapfiles`` is the directory where the files will be stored. Answer the -few questions and you're done. +:file:`mapfiles` is the directory where the files will be +stored. Answer the few questions and you're done. .. _postfix_config: Configuration ============= -Use the following configuration in the */etc/postfix/main.cf* file +Use the following configuration in the :file:`/etc/postfix/main.cf` file (this is just one possible configuration):: # Stuff before @@ -442,10 +443,10 @@ Use the following configuration in the */etc/postfix/main.cf* file # Stuff after -Then, edit the */etc/postfix/master.cf* file and add the following +Then, edit the :file:`/etc/postfix/master.cf` file and add the following definition at the end:: dovecot unix - n n - - pipe flags=DRhu user=vmail:vmail argv=/usr/lib/dovecot/deliver -f ${sender} -d ${recipient} -Restart *Postfix*. +Restart Postfix. diff --git a/doc/integration/web_servers.rst b/doc/integration/web_servers.rst index 639aba65b..2c2d11185 100644 --- a/doc/integration/web_servers.rst +++ b/doc/integration/web_servers.rst @@ -10,18 +10,19 @@ Web servers Apache2 ******* -.. note:: The following instructions are meant to help you get your - site up and running quickly. However it is not possible for - the people contributing documentation to Modoboa to test - every single combination of web server, wsgi server, - distribution, etc. So it is possible that **your** - installation of uwsgi or nginx or Apache or what-have-you - works differently. Keep this in mind. +.. note:: + + The following instructions are meant to help you get your site up + and running quickly. However it is not possible for the people + contributing documentation to Modoboa to test every single + combination of web server, wsgi server, distribution, etc. So it is + possible that **your** installation of uwsgi or nginx or Apache or + what-have-you works differently. Keep this in mind. mod_wgsi ======== -First, make sure that *mod_wsgi* is installed on your server. +First, make sure that mod_wsgi is installed on your server. Create a new virtualhost in your Apache configuration and put the following content inside:: @@ -48,8 +49,8 @@ following content inside:: This is just one possible configuration. .. note:: - *Django* 1.3 users, please consult this `page `_, - it contains an example *wsgi.py* file. + Django 1.3 users, please consult this `page `_, + it contains an example :file:`wsgi.py` file. .. note:: You will certainly need more configuration in order to launch @@ -58,7 +59,7 @@ This is just one possible configuration. mod_python ========== -First, make sure that *mod_python* is installed on your server. +First, make sure that mod_python is installed on your server. Create a new virtualhost in your Apache configuration and put the following content inside:: @@ -97,13 +98,14 @@ This is just one possible configuration. Nginx ***** -.. note:: The following instructions are meant to help you get your - site up and running quickly. However it is not possible for - the people contributing documentation to Modoboa to test - every single combination of web server, wsgi server, - distribution, etc. So it is possible that **your** - installation of uwsgi or nginx or Apache or what-have-you - works differently. Keep this in mind. +.. note:: + + The following instructions are meant to help you get your site up + and running quickly. However it is not possible for the people + contributing documentation to Modoboa to test every single + combination of web server, wsgi server, distribution, etc. So it is + possible that **your** installation of uwsgi or nginx or Apache or + what-have-you works differently. Keep this in mind. Green Unicorn ============= @@ -116,7 +118,7 @@ performance is really good with this configuration. To use this setup, first download and install `nginx `__ and `gunicorn `_. Then, use the following sample -*gunicorn* configuration (create a new file named *gunicorn.conf.py* +gunicorn configuration (create a new file named :file:`gunicorn.conf.py` inside Modoboa's root dir):: backlog = 2048 @@ -133,7 +135,7 @@ To start gunicorn, execute the following commands:: $ cd $ gunicorn -c gunicorn.conf.py .wsgi:application -Now the *nginx* part. Just create a new virtual host and use the +Now the nginx part. Just create a new virtual host and use the following configuration:: upstream modoboa { @@ -167,7 +169,7 @@ following configuration:: proxy_set_header Host $http_host; proxy_redirect off; proxy_set_header X-Forwarded-Protocol ssl; - proxy_pass http://modoboa; + proxy_pass http://modoboa; } } @@ -186,7 +188,7 @@ key. `This article contains information about how to do it. Paste this content to your configuration (replace values between -``<>`` with yours), restart *nginx* and enjoy a really fast +``<>`` with yours), restart nginx and enjoy a really fast application! uwsgi @@ -199,13 +201,14 @@ optimize their configuration for your site. The Django documentation includes the following warning regarding uwsgi: -.. warning:: Some distributions, including Debian and Ubuntu, ship an - outdated version of uWSGI that does not conform to the - WSGI specification. Versions prior to 1.2.6 do not call - close on the response object after handling a request. In - those cases the request_finished signal isn’t sent. This - can result in idle connections to database and memcache - servers. +.. warning:: + + Some distributions, including Debian and Ubuntu, ship an outdated + version of uWSGI that does not conform to the WSGI + specification. Versions prior to 1.2.6 do not call close on the + response object after handling a request. In those cases the + request_finished signal isn’t sent. This can result in idle + connections to database and memcache servers. Use uwsgi 1.2.6 or newer. If you do not, you *will* run into problems. Modoboa will fail in obscure ways. From f5626ecaa6fd4a5d2c46744be30a1fb9a995b217 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 19 Sep 2013 21:12:50 +0200 Subject: [PATCH 41/97] Display the name of the object about to be deleted. fixes #483 --- modoboa/admin/static/admin/js/admin.js | 16 ++-------- modoboa/admin/templatetags/admin_extras.py | 31 +++++-------------- modoboa/admin/tests.py | 4 +-- modoboa/admin/urls.py | 3 -- modoboa/admin/views.py | 36 ++++++++-------------- modoboa/static/js/confirmation.js | 4 ++- 6 files changed, 29 insertions(+), 65 deletions(-) diff --git a/modoboa/admin/static/admin/js/admin.js b/modoboa/admin/static/admin/js/admin.js index 825d645aa..835ea339d 100644 --- a/modoboa/admin/static/admin/js/admin.js +++ b/modoboa/admin/static/admin/js/admin.js @@ -114,7 +114,7 @@ Domains.prototype = { : gettext("This operation will remove all data associated to this domain, excepting accounts."); $("a[name=deldomain]").confirm({ - question: gettext("Delete this domain?"), + question: function() { return this.$element.attr('title'); }, method: "POST", warning: warnmsg, checkboxes: deloptions, @@ -240,23 +240,13 @@ Identities.prototype = { $("a.filter").click($.proxy(this.filter_by_tag, this)); $("a[name=delaccount]").confirm({ - question: gettext("Delete this account?"), + question: function() { return this.$element.attr('title'); }, method: "POST", checkboxes: deloptions, success_cb: $.proxy(this.reload_listing, this) }); - $("a[name=deldlist]").confirm({ - question: gettext("Delete this list?"), - method: "DELETE", - success_cb: $.proxy(this.reload_listing, this) - }); - $("a[name=delforward]").confirm({ - question: gettext("Delete this forward?"), - method: "DELETE", - success_cb: $.proxy(this.reload_listing, this) - }); $("a[name=delalias]").confirm({ - question: gettext("Delete this alias?"), + question: function() { return this.$element.attr('title'); }, method: "DELETE", success_cb: $.proxy(this.reload_listing, this) }); diff --git a/modoboa/admin/templatetags/admin_extras.py b/modoboa/admin/templatetags/admin_extras.py index 5cdf8b245..504a75295 100644 --- a/modoboa/admin/templatetags/admin_extras.py +++ b/modoboa/admin/templatetags/admin_extras.py @@ -175,7 +175,7 @@ def domain_actions(user, domid): actions.append({ "name": "deldomain", "url": reverse("modoboa.admin.views.deldomain", args=[domid]), - "title": _("Delete the domain"), + "title": _("Delete %s?" % domain.name), "img": "icon-trash" }) return render_actions(actions) @@ -191,30 +191,15 @@ def identity_actions(user, ident): {"name" : "delaccount", "url" : reverse("modoboa.admin.views.delaccount", args=[objid]), "img" : "icon-trash", - "title" : _("Delete this account")}, + "title" : _("Delete %s?" % ident.username)}, ] else: - if ident.get_recipients_count() >= 2: - actions = [ - {"name" : "deldlist", - "url" : reverse("modoboa.admin.views.deldlist") + "?selection=%s" % objid, - "img" : "icon-trash", - "title" : _("Delete this distribution list")}, - ] - elif ident.extmboxes != "": - actions = [ - {"name" : "delforward", - "url" : reverse("modoboa.admin.views.delforward") + "?selection=%s" % objid, - "img" : "icon-trash", - "title" : _("Delete this forward")}, - ] - else: - actions = [ - {"name" : "delalias", - "url" : reverse("modoboa.admin.views.delalias") + "?selection=%s" % objid, - "img" : "icon-trash", - "title" : _("Delete this alias")}, - ] + actions = [ + {"name" : "delalias", + "url" : reverse("modoboa.admin.views.delalias") + "?selection=%s" % objid, + "img" : "icon-trash", + "title" : _("Delete %s?" % ident.full_address)}, + ] return render_actions(actions) @register.simple_tag diff --git a/modoboa/admin/tests.py b/modoboa/admin/tests.py index 60d380a20..18e5a7f4e 100644 --- a/modoboa/admin/tests.py +++ b/modoboa/admin/tests.py @@ -208,7 +208,7 @@ class AliasTestCase(ModoTestCase): values) self.assertEqual(dlist.get_recipients_count(), 2) - self.check_ajax_get(reverse("modoboa.admin.views.deldlist") + "?selection=%d" \ + self.check_ajax_get(reverse("modoboa.admin.views.delalias") + "?selection=%d" \ % dlist.id, {}) self.assertRaises(Alias.DoesNotExist, Alias.objects.get, address="all", domain__name="test.com") @@ -224,7 +224,7 @@ class AliasTestCase(ModoTestCase): values) self.assertEqual(fwd.get_recipients_count(), 1) - self.check_ajax_get(reverse("modoboa.admin.views.delforward") + "?selection=%d" \ + self.check_ajax_get(reverse("modoboa.admin.views.delalias") + "?selection=%d" \ % fwd.id, {}) self.assertRaises(Alias.DoesNotExist, Alias.objects.get, address="forward", domain__name="test.com") diff --git a/modoboa/admin/urls.py b/modoboa/admin/urls.py index 048d1b859..edf49f22c 100644 --- a/modoboa/admin/urls.py +++ b/modoboa/admin/urls.py @@ -35,10 +35,7 @@ urlpatterns = patterns( (r'^accounts/delete/(?P\d+)/$', 'delaccount'), (r'^distriblists/new/$', 'newdlist'), - (r'^distriblists/delete/$', 'deldlist'), - (r'^forwards/new/$', 'newforward'), - (r'^forwards/delete/$', 'delforward'), (r'^aliases/new/$', 'newalias'), (r'^aliases/edit/(?P\d+)/$', 'editalias'), diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py index 1dbcef8e1..7d142c41c 100644 --- a/modoboa/admin/views.py +++ b/modoboa/admin/views.py @@ -300,40 +300,30 @@ def editalias(request, alid, tplname="common/generic_modal_form.html"): return render(request, tplname, ctx) -def _del_alias(request, msg, msgs): +@login_required +@permission_required("admin.delete_alias") +@transaction.commit_on_success +def delalias(request): selection = request.GET["selection"].split(",") for alid in selection: alias = Alias.objects.get(pk=alid) if not request.user.can_access(alias): raise PermDeniedException + if alias.type == 'dlist': + msg = "Distribution list deleted" + msgs = "Distribution lists deleted" + elif alias.type == 'forward': + msg = "Forward deleted" + msgs = "Forwards deleted" + else: + msg = "Alias deleted" + msgs = "Aliases deleted" alias.delete() msg = ungettext(msg, msgs, len(selection)) return ajax_simple_response({"status": "ok", "respmsg": msg}) -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def delalias(request): - return _del_alias(request, "Alias deleted", "Aliases deleted") - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def deldlist(request): - return _del_alias(request, "Distribution list deleted", - "Distribution lists deleted") - - -@login_required -@permission_required("admin.delete_alias") -@transaction.commit_on_success -def delforward(request): - return _del_alias(request, "Forward deleted", "Forwards deleted") - - @login_required @user_passes_test(lambda u: u.has_perm("admin.add_user") or u.has_perm("admin.add_alias")) def _identities(request): diff --git a/modoboa/static/js/confirmation.js b/modoboa/static/js/confirmation.js index e360eba04..59bd9314d 100644 --- a/modoboa/static/js/confirmation.js +++ b/modoboa/static/js/confirmation.js @@ -36,9 +36,11 @@ id: "confirmbox", 'class': "modal" }); + var question = (typeof this.options.question == 'function') + ? this.options.question.apply(this) : this.options.question; box.append($('