From 115553236509754ceea3435327b8cd1c07260d06 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Fri, 16 Aug 2013 16:34:15 +0200 Subject: [PATCH] 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: - -

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: - -

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 }}