From b15e475475448e4e303de5989f0130a550a04a8e Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Mon, 6 May 2013 17:28:04 +0200 Subject: [PATCH] Improvements about aliases: * Referencing an existing alias as a recipient is now allowed * Alias type verification is less restrictive fixes #403 --- modoboa/admin/forms.py | 185 +++++------------- .../0031_auto__add_aliases_to_alias.py | 128 ++++++++++++ modoboa/admin/models.py | 80 +++++--- modoboa/admin/static/admin/js/admin.js | 8 +- modoboa/admin/templates/admin/identities.html | 4 +- modoboa/admin/templatetags/admin_extras.py | 2 +- modoboa/admin/tests.py | 10 +- modoboa/admin/urls.py | 5 +- modoboa/admin/views.py | 159 +++++++-------- 9 files changed, 317 insertions(+), 264 deletions(-) create mode 100644 modoboa/admin/migrations/0031_auto__add_aliases_to_alias.py diff --git a/modoboa/admin/forms.py b/modoboa/admin/forms.py index 0c114ec47..7fefb0af3 100644 --- a/modoboa/admin/forms.py +++ b/modoboa/admin/forms.py @@ -1,13 +1,14 @@ # coding: utf-8 from django import forms from django.contrib.auth.models import Group -from modoboa.admin.models import * from django.utils.translation import ugettext as _, ugettext_lazy from django.http import QueryDict from modoboa.admin.templatetags.admin_extras import gender +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 class DomainFormGeneral(forms.ModelForm, DynamicForm): @@ -54,7 +55,6 @@ class DomainFormGeneral(forms.ModelForm, DynamicForm): self._errors["name"] = self.error_class([_("An alias with this name already exists")]) del cleaned_data["name"] - errors = [] for k in cleaned_data.keys(): if not k.startswith("aliases"): continue @@ -198,7 +198,7 @@ class DomainForm(TabForms): f["instance"].save(user) -class DlistForm(forms.ModelForm, DynamicForm): +class AliasForm(forms.ModelForm, DynamicForm): email = forms.EmailField( label=ugettext_lazy("Email address"), help_text=ugettext_lazy("The distribution list address. Use the '*' character to create a 'catchall' address (ex: *@domain.tld).") @@ -214,7 +214,7 @@ class DlistForm(forms.ModelForm, DynamicForm): def __init__(self, user, *args, **kwargs): self.user = user - super(DlistForm, self).__init__(*args, **kwargs) + super(AliasForm, self).__init__(*args, **kwargs) self.fields.keyOrder = ['email', 'recipients', 'enabled'] if len(args) and isinstance(args[0], QueryDict): @@ -223,6 +223,10 @@ class DlistForm(forms.ModelForm, DynamicForm): dlist = kwargs["instance"] self.fields["email"].initial = dlist.full_address cpt = 1 + for al in dlist.aliases.all(): + name = "recipients_%d" % cpt + self._create_field(forms.EmailField, name, al.full_address, 2) + cpt += 1 for mb in dlist.mboxes.all(): name = "recipients_%d" % (cpt) self._create_field(forms.EmailField, name, mb.full_address, 2) @@ -254,7 +258,7 @@ class DlistForm(forms.ModelForm, DynamicForm): self.int_rcpts = [] total = 0 - for k, v in self.cleaned_data.iteritems(): + for k, v in self.cleaned_data.items(): if not k.startswith("recipients"): continue if v == "": @@ -266,16 +270,21 @@ class DlistForm(forms.ModelForm, DynamicForm): domain = Domain.objects.get(name=domname) except Domain.DoesNotExist: domain = None - if domain: + if domain is not None: if not self.user.can_access(domain): raise PermDeniedException(v) try: - mb = Mailbox.objects.get(domain=domain, address=local_part) - except Mailbox.DoesNotExist: - raise AdminError(_("Mailbox %s does not exist" % v)) - if mb in self.int_rcpts: + rcpt = Alias.objects.get(domain=domain, address=local_part) + except Alias.DoesNotExist: + rcpt = None + if rcpt is None: + try: + rcpt = Mailbox.objects.get(domain=domain, address=local_part) + except Mailbox.DoesNotExist: + raise AdminError(_("Local recipient %s not found" % v)) + if rcpt in self.int_rcpts: raise AdminError(_("Recipient %s already present" % v)) - self.int_rcpts += [mb] + self.int_rcpts += [rcpt] total += 1 continue @@ -287,122 +296,22 @@ class DlistForm(forms.ModelForm, DynamicForm): if total == 0: raise AdminError(_("No recipient defined")) - if total < 2: - raise AdminError(_("A distribution list must contain at least two recipients")) - def save(self, commit=True): - dlist = super(DlistForm, self).save(commit=False) - localpart, domname = split_mailbox(self.cleaned_data["email"]) - dlist.address = localpart - dlist.domain = Domain.objects.get(name=domname) - if commit: - dlist.save(self.int_rcpts, self.ext_rcpts) - self.save_m2m() - return dlist - - -class GenericAliasForm(forms.ModelForm): - email = forms.EmailField( - label=ugettext_lazy("Address"), - help_text=ugettext_lazy("A valid e-mail address. Use the '*' character to create a 'catchall' address (ex: *@domain.tld).") - ) - - class Meta: - model = Alias - fields = ("enabled", ) - - def __init__(self, user, *args, **kwargs): - self.user = user - super(GenericAliasForm, self).__init__(*args, **kwargs) - if "instance" in kwargs: - alias = kwargs["instance"] - self.fields["email"].initial = alias.full_address - - def clean_email(self): - localpart, domname = split_mailbox(self.cleaned_data["email"]) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - raise forms.ValidationError(_("Domain does not exist")) - if not self.user.can_access(domain): - raise forms.ValidationError(_("You don't have access to this domain")) - return self.cleaned_data["email"] - - def set_recipients(self): - pass - - def _save(self): - alias = super(GenericAliasForm, self).save(commit=False) + alias = super(AliasForm, self).save(commit=False) localpart, domname = split_mailbox(self.cleaned_data["email"]) alias.address = localpart alias.domain = Domain.objects.get(name=domname) - return alias - - -class AliasForm(GenericAliasForm): - int_recipient = forms.EmailField( - label=ugettext_lazy("Recipient"), - help_text=ugettext_lazy("A local recipient address") - ) - - def __init__(self, *args, **kwargs): - super(AliasForm, self).__init__(*args, **kwargs) - self.fields.keyOrder = ['email', 'int_recipient', 'enabled'] - if "instance" in kwargs: - alias = kwargs["instance"] - if len(alias.mboxes.all()): - self.fields["int_recipient"].initial = alias.mboxes.all()[0].full_address - - def clean_int_recipient(self): - localpart, domname = split_mailbox(self.cleaned_data["int_recipient"]) - try: - self.rcpt_mb = Mailbox.objects.get(address=localpart, domain__name=domname) - except Mailbox.DoesNotExist: - raise forms.ValidationError(_("Mailbox does not exist")) - if not self.user.can_access(self.rcpt_mb): - raise forms.ValidationError(_("You can't access this mailbox")) - return self.cleaned_data["int_recipient"] - - def save(self, commit=True): - alias = self._save() if commit: - alias.save([self.rcpt_mb], []) - return alias - - -class ForwardForm(GenericAliasForm): - ext_recipient = forms.EmailField( - label=ugettext_lazy("Recipient"), - help_text=ugettext_lazy("An external recipient address") - ) - - def __init__(self, *args, **kwargs): - super(ForwardForm, self).__init__(*args, **kwargs) - self.fields.keyOrder = ['email', 'ext_recipient', 'enabled'] - if "instance" in kwargs: - alias = kwargs["instance"] - self.fields["ext_recipient"].initial = alias.extmboxes - - def clean_ext_recipient(self): - local_part, domname = split_mailbox(self.cleaned_data["ext_recipient"]) - try: - domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: - pass - else: - raise forms.ValidationError(_("Local recipients are forbidden")) - return self.cleaned_data["ext_recipient"] - - def save(self, commit=True): - alias = self._save() - if commit: - alias.save([], [self.cleaned_data["ext_recipient"]]) + alias.save(self.int_rcpts, self.ext_rcpts) + self.save_m2m() return alias class ImportDataForm(forms.Form): sourcefile = forms.FileField(label=ugettext_lazy("Select a file")) - sepchar = forms.CharField(label=ugettext_lazy("Separator"), max_length=1, required=False) + sepchar = forms.CharField( + label=ugettext_lazy("Separator"), max_length=1, required=False + ) continue_if_exists = forms.BooleanField( label=ugettext_lazy("Continue on error"), required=False, help_text=ugettext_lazy("Don't treat duplicated objects as error") @@ -483,7 +392,11 @@ class AccountFormGeneral(forms.ModelForm): self.fields["is_active"].label = _("Enabled") self.user = user if user.group == "DomainAdmins": - del self.fields["role"] + #del self.fields["role"] + self.fields["role"] = forms.CharField( + label="", + widget=forms.HiddenInput, required=False + ) else: self.fields["role"].choices = \ [('', ugettext_lazy("Choose"))] + get_account_roles(user) @@ -494,16 +407,16 @@ class AccountFormGeneral(forms.ModelForm): and args[0].get("password2", "") == ""): self.fields["password1"].required = False self.fields["password2"].required = False - if user.group != "DomainAdmins": - u = kwargs["instance"] - if u.is_superuser: - role = "SuperAdmins" - else: - try: - role = u.groups.all()[0].name - except IndexError: - role = "SimpleUsers" - self.fields["role"].initial = role + + u = kwargs["instance"] + self.fields["role"].initial = u.group + + def clean_role(self): + if self.user.group == "DomainAdmins": + if self.instance == self.user: + return "DomainAdmins" + return "SimpleUsers" + return self.cleaned_data["role"] def clean_username(self): from django.core.validators import validate_email @@ -530,12 +443,12 @@ class AccountFormGeneral(forms.ModelForm): if self.cleaned_data["password1"] != "": account.set_password(self.cleaned_data["password1"]) account.save() - role = None - if "role" in self.cleaned_data: - role = self.cleaned_data["role"] - elif self.user.group == "DomainAdmins" and self.user != account: - role = "SimpleUsers" - account.set_role(role) + # role = None + # if "role" in self.cleaned_data: + # role = self.cleaned_data["role"] + # elif self.user.group == "DomainAdmins" and self.user != account: + # role = "SimpleUsers" + account.set_role(self.cleaned_data["role"]) return account @@ -683,8 +596,8 @@ class AccountPermissionsForm(forms.Form, DynamicForm): self._load_from_qdict(args[0], "domains", DomainNameField) def save(self): - current_domains = map(lambda dom: dom.name, self.account.get_domains()) - for name, value in self.cleaned_data.iteritems(): + current_domains = [dom.name for dom in self.account.get_domains()] + for name, value in self.cleaned_data.items(): if not name.startswith("domains"): continue if value in ["", None]: diff --git a/modoboa/admin/migrations/0031_auto__add_aliases_to_alias.py b/modoboa/admin/migrations/0031_auto__add_aliases_to_alias.py new file mode 100644 index 000000000..491f06122 --- /dev/null +++ b/modoboa/admin/migrations/0031_auto__add_aliases_to_alias.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding M2M table for field aliases on 'Alias' + db.create_table(u'admin_alias_aliases', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('from_alias', models.ForeignKey(orm[u'admin.alias'], null=False)), + ('to_alias', models.ForeignKey(orm[u'admin.alias'], null=False)) + )) + db.create_unique(u'admin_alias_aliases', ['from_alias_id', 'to_alias_id']) + + + def backwards(self, orm): + # Removing M2M table for field aliases on 'Alias' + db.delete_table('admin_alias_aliases') + + + models = { + u'admin.alias': { + 'Meta': {'ordering': "['domain__name', 'address']", 'unique_together': "(('address', 'domain'),)", 'object_name': 'Alias'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'aliases': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['admin.Alias']", 'symmetrical': 'False'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.Domain']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'extmboxes': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'mboxes': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['admin.Mailbox']", 'symmetrical': 'False'}) + }, + u'admin.domain': { + 'Meta': {'ordering': "['name']", 'object_name': 'Domain'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'quota': ('django.db.models.fields.IntegerField', [], {}) + }, + u'admin.domainalias': { + 'Meta': {'object_name': 'DomainAlias'}, + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.ObjectDates']"}), + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}), + 'target': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.Domain']"}) + }, + u'admin.extension': { + 'Meta': {'object_name': 'Extension'}, + 'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '150'}) + }, + u'admin.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'address': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'dates': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.ObjectDates']"}), + 'domain': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.Domain']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'quota': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'use_domain_quota': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.User']"}) + }, + u'admin.objectaccess': { + 'Meta': {'unique_together': "(('user', 'content_type', 'object_id'),)", 'object_name': 'ObjectAccess'}, + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_owner': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'object_id': ('django.db.models.fields.PositiveIntegerField', [], {}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['admin.User']"}) + }, + u'admin.objectdates': { + 'Meta': {'object_name': 'ObjectDates'}, + 'creation': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_modification': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + u'admin.quota': { + 'Meta': {'object_name': 'Quota'}, + 'bytes': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'mbox': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'quota_value'", 'unique': 'True', 'null': 'True', 'to': u"orm['admin.Mailbox']"}), + 'messages': ('django.db.models.fields.IntegerField', [], {'default': '0'}), + 'username': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'primary_key': 'True'}) + }, + u'admin.user': { + 'Meta': {'ordering': "['username']", 'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '254', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '254'}) + }, + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + } + } + + complete_apps = ['admin'] \ No newline at end of file diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py index e9dbf29a7..6ef1e59e6 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -6,7 +6,9 @@ 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.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 @@ -220,7 +222,7 @@ class User(AbstractBaseUser, PermissionsMixin): try: return self.groups.all()[0].name except IndexError: - return "?" + return "SimpleUsers" @property def enabled(self): @@ -276,8 +278,8 @@ class User(AbstractBaseUser, PermissionsMixin): accounts = User.objects.select_related().filter(q) aliases = [] - if not idtfilter or ("alias" in idtfilter - or "forward" in idtfilter + 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) \ @@ -294,7 +296,7 @@ class User(AbstractBaseUser, PermissionsMixin): q &= Q(address__icontains=squery) | Q(domain__name__icontains=squery) aliases = Alias.objects.select_related().filter(q) if idtfilter: - aliases = filter(lambda a: a.type in idtfilter, aliases) + aliases = [al for al in aliases if al.type in idtfilter] return chain(accounts, aliases) @@ -351,7 +353,13 @@ class User(AbstractBaseUser, PermissionsMixin): 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 @@ -446,6 +454,9 @@ class User(AbstractBaseUser, PermissionsMixin): 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): @@ -515,8 +526,8 @@ class User(AbstractBaseUser, PermissionsMixin): 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"), + 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()] @@ -559,6 +570,7 @@ class ObjectDates(models.Model): dates.save() obj.dates = dates + class DatesAware(models.Model): """Abstract model to support dates @@ -954,12 +966,12 @@ def mailbox_deleted_handler(sender, **kwargs): 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) @@ -967,7 +979,7 @@ def mailbox_deleted_handler(sender, **kwargs): alias.mboxes.remove(mb) if alias.mboxes.count() == 0: alias.delete() - + class Quota(models.Model): username = models.EmailField(primary_key=True) @@ -981,20 +993,26 @@ 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")) + 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"] @@ -1028,12 +1046,12 @@ class Alias(DatesAware): @property def tags(self): labels = { - "dlist" : _("distribution list"), - "forward" : _("forward"), - "alias" : _("alias") - } + "dlist": _("distribution list"), + "forward": _("forward"), + "alias": _("alias") + } altype = self.type - return [{"name" : altype, "label" : labels[altype], "type" : "idt"}] + return [{"name": altype, "label": labels[altype], "type": "idt"}] def post_create(self, creator): from modoboa.lib.permissions import grant_access_to_object @@ -1056,10 +1074,18 @@ class Alias(DatesAware): 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) @@ -1076,15 +1102,17 @@ class Alias(DatesAware): Internal and external addresses are mixed into a single list. """ - result = map(lambda mb: mb.full_address, self.mboxes.all()) + 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 != "": - return self.mboxes.count() + len(self.extmboxes.split(',')) - return self.mboxes.count() + total += len(self.extmboxes.split(',')) + return total + self.aliases.count() + self.mboxes.count() def ui_disabled(self, user): if user.is_superuser: @@ -1129,12 +1157,13 @@ class Alias(DatesAware): row += self.get_recipients() csvwriter.writerow(row) + 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) @@ -1185,6 +1214,7 @@ class Extension(models.Model): events.raiseEvent("ExtDisabled", self) + def populate_callback(user): """Populate callback diff --git a/modoboa/admin/static/admin/js/admin.js b/modoboa/admin/static/admin/js/admin.js index 71203bdd1..1b337dc0e 100644 --- a/modoboa/admin/static/admin/js/admin.js +++ b/modoboa/admin/static/admin/js/admin.js @@ -359,7 +359,8 @@ Identities.prototype = { generalform_init: function() { $("#id_role").change($.proxy(function(e) { var $this = $(e.target); - if ($this.attr("value") == "SimpleUsers") { + var value = $this.attr("value"); + if (value == "SimpleUsers" || value == "") { this.simpleuser_mode(); } else { this.normal_mode(); @@ -395,7 +396,8 @@ Identities.prototype = { }, mailform_prefill: function() { - if (!$("#id_role").length || $("#id_role").attr("value") == "SimpleUsers") { + var $role = $("#id_role"); + if (!$role.length || $role.attr("value") == "" || $role.attr("value") == "SimpleUsers") { $("#id_email").attr("value", $("#id_username").attr("value")); } }, @@ -434,7 +436,7 @@ Identities.prototype = { }); $("#id_recipients").dynamic_input(); $("#id_int_recipient").autocompleter({ - choices: get_mboxes_list + choices: get_allowed_recipients }); $(".submit").one('click', $.proxy(function(e) { simple_ajax_form_post(e, { diff --git a/modoboa/admin/templates/admin/identities.html b/modoboa/admin/templates/admin/identities.html index 6e664ec8a..3c120f874 100644 --- a/modoboa/admin/templates/admin/identities.html +++ b/modoboa/admin/templates/admin/identities.html @@ -16,8 +16,8 @@ function get_account_list(container) { }); } -function get_mboxes_list(container) { - $.getJSON("{% url 'modoboa.admin.views.mboxes_list' %}", function(data) { +function get_allowed_recipients(container) { + $.getJSON("{% url 'modoboa.admin.views.allowed_recipients_list' %}", function(data) { container.value = data; }); } diff --git a/modoboa/admin/templatetags/admin_extras.py b/modoboa/admin/templatetags/admin_extras.py index 78219cbb3..7908177f3 100644 --- a/modoboa/admin/templatetags/admin_extras.py +++ b/modoboa/admin/templatetags/admin_extras.py @@ -220,7 +220,7 @@ def identity_modify_link(identity, active_tab='default'): linkdef["url"] += "?active_tab=%s" % active_tab linkdef["modalcb"] = "admin.editaccount_cb" else: - linkdef["url"] = reverse("modoboa.admin.views.editalias_dispatcher", args=[identity.id]) + linkdef["url"] = reverse("modoboa.admin.views.editalias", args=[identity.id]) linkdef["modalcb"] = "admin.aliasform_cb" return render_link(linkdef) diff --git a/modoboa/admin/tests.py b/modoboa/admin/tests.py index 5346c83bf..34416b135 100644 --- a/modoboa/admin/tests.py +++ b/modoboa/admin/tests.py @@ -192,7 +192,7 @@ class AliasTestCase(ModoTestCase): dlist = Alias.objects.get(address="all", domain__name="test.com") self.assertEqual(len(dlist.get_recipients()), 3) del values["recipients_1"] - self.check_ajax_post(reverse("modoboa.admin.views.editalias_dispatcher", args=[dlist.id]), + self.check_ajax_post(reverse("modoboa.admin.views.editalias", args=[dlist.id]), values) self.assertEqual(dlist.get_recipients_count(), 2) @@ -202,13 +202,13 @@ class AliasTestCase(ModoTestCase): address="all", domain__name="test.com") def test_forward(self): - values = dict(email="forward@test.com", ext_recipient="rcpt@dest.com") + values = dict(email="forward@test.com", recipients="rcpt@dest.com") self.check_ajax_post(reverse("modoboa.admin.views.newforward"), values) fwd = Alias.objects.get(address="forward", domain__name="test.com") - self.assertEqual(len(fwd.get_recipients()), 1) + self.assertEqual(fwd.get_recipients_count(), 1) - values["recipient"] = "rcpt2@dest.com" - self.check_ajax_post(reverse("modoboa.admin.views.editalias_dispatcher", args=[fwd.id]), + values["recipients"] = "rcpt2@dest.com" + self.check_ajax_post(reverse("modoboa.admin.views.editalias", args=[fwd.id]), values) self.assertEqual(fwd.get_recipients_count(), 1) diff --git a/modoboa/admin/urls.py b/modoboa/admin/urls.py index 50f6582a2..f388203e3 100644 --- a/modoboa/admin/urls.py +++ b/modoboa/admin/urls.py @@ -32,8 +32,6 @@ urlpatterns = patterns( (r'^accounts/edit/(?P\d+)/$', 'editaccount'), (r'^accounts/delete/$', 'delaccount'), - (r'^mailboxes/list/$', 'mboxes_list'), - (r'^distriblists/new/$', 'newdlist'), (r'^distriblists/delete/$', 'deldlist'), @@ -41,6 +39,7 @@ urlpatterns = patterns( (r'^forwards/delete/$', 'delforward'), (r'^aliases/new/$', 'newalias'), - (r'^aliases/edit/(?P\d+)/$', 'editalias_dispatcher'), + (r'^aliases/edit/(?P\d+)/$', 'editalias'), (r'^aliases/delete/$', 'delalias'), + (r'^aliases/allowed_recipients/$', 'allowed_recipients_list'), ) diff --git a/modoboa/admin/views.py b/modoboa/admin/views.py index d3e46a72a..33bbc266a 100644 --- a/modoboa/admin/views.py +++ b/modoboa/admin/views.py @@ -69,28 +69,6 @@ def domains_list(request): return ajax_simple_response(doms) -def _validate_domain(request, form, successmsg, commonctx, tplname, - callback=None, tpl_form_name="form"): - """Domain form validation - - Common function shared between creation and modification actions. - """ - error = None - if form.is_valid(): - try: - domain = form.save(request.user) - except AdminError, e: - error = str(e) - else: - if callback is not None: - callback(request.user, domain) - return ajax_simple_response({"status" : "ok", "respmsg" : successmsg}) - - commonctx[tpl_form_name] = form - commonctx["error"] = error - return ajax_response(request, status="ko", template=tplname, **commonctx) - - @login_required @permission_required("admin.add_domain") @transaction.commit_on_success @@ -167,13 +145,21 @@ def editdomain(request, dom_id, tplname="admin/editdomainform.html"): "formid": "domform", "domain": domain} if request.method == "POST": - def editdomain_cb(user, newdomain): - events.raiseEvent("DomainModified", domain) - + error = None domain.oldname = domain.name form = DomainForm(request.user, request.POST, instances=instances) - return _validate_domain(request, form, _("Domain modified"), commonctx, tplname, - tpl_form_name="tabs", callback=editdomain_cb) + if form.is_valid(): + try: + domain = 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 @@ -227,17 +213,26 @@ def _validate_alias(request, form, successmsg, tplname, commonctx, callback=None return ajax_response(request, status="ko", template=tplname, **commonctx) -def _new_alias(request, tplname, formclass, ctx, successmsg): +def _new_alias(request, title, action, successmsg, + tplname="common/generic_modal_form.html"): events.raiseEvent("CanCreate", request.user, "mailbox_aliases") - ctx.update(action_label=_("Create"), action_classes="submit") + 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 = formclass(request.user, request.POST) - return _validate_alias(request, form, successmsg, tplname, ctx, callback) + form = AliasForm(request.user, request.POST) + return _validate_alias( + request, form, successmsg, tplname, ctx, callback + ) - form = formclass(request.user) + form = AliasForm(request.user) ctx["form"] = form return render(request, tplname, ctx) @@ -246,75 +241,58 @@ def _new_alias(request, tplname, formclass, ctx, successmsg): @permission_required("admin.add_alias") @transaction.commit_on_success def newdlist(request): - ctx = dict(title=_("New distribution list"), - action=reverse(newdlist), - formid="aliasform") - return _new_alias(request, "common/generic_modal_form.html", - DlistForm, ctx, _("Distribution list created")) + 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): - ctx = dict(title=_("New alias"), - action=reverse(newalias), - formid="aliasform") - return _new_alias(request, "common/generic_modal_form.html", - AliasForm, ctx, _("Alias created")) + return _new_alias( + request, _("New alias"), reverse(newalias), + _("Alias created") + ) @login_required @permission_required("admin.add_alias") @transaction.commit_on_success def newforward(request): - ctx = dict(title=_("New forward"), - action=reverse(newforward), - formid="aliasform") - return _new_alias(request, "common/generic_modal_form.html", - ForwardForm, ctx, _("Forward created")) - - -def _edit_alias(request, alias, tplname, formclass, ctx, successmsg): - if not request.user.can_access(alias): - raise PermDeniedException - ctx.update(title=alias.full_address, action_label=_("Update"), - action_classes="submit") - if request.method == "POST": - form = formclass(request.user, request.POST, instance=alias) - return _validate_alias(request, form, successmsg, tplname, ctx) - - form = formclass(request.user, instance=alias) - ctx["form"] = form - return render(request, tplname, ctx) - - -def editalias(request, alias, ctx): - return _edit_alias(request, alias, "common/generic_modal_form.html", - AliasForm, ctx, _("Alias modified")) - - -def editdlist(request, alias, ctx): - return _edit_alias(request, alias, "admin/dlistform.html", - DlistForm, ctx, _("Distribution list modified")) - - -def editforward(request, alias, ctx): - return _edit_alias(request, alias, "common/generic_modal_form.html", - ForwardForm, ctx, _("Forward modified")) + return _new_alias( + request, _("New forward"), reverse(newforward), + _("Forward created") + ) @login_required @permission_required("admin.change_alias") -def editalias_dispatcher(request, alid): +def editalias(request, alid, tplname="common/generic_modal_form.html"): alias = Alias.objects.get(pk=alid) - ctx = dict(action=reverse(editalias_dispatcher, - args=[alias.id]), formid="aliasform") + 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: - return editdlist(request, alias, ctx) - if alias.extmboxes != "": - return editforward(request, alias, ctx) - return editalias(request, alias, ctx) + 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): @@ -388,16 +366,19 @@ def identities(request, tplname="admin/identities.html"): @login_required @permission_required("admin.add_user") def accounts_list(request): - accs = User.objects.filter(is_superuser=False).exclude(groups__name='SimpleUsers') + 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 -@permission_required("admin.add_mailbox") -def mboxes_list(request): - mboxes = request.user.get_mailboxes() - return ajax_simple_response([mb.full_address for mb in mboxes]) +@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 @@ -452,7 +433,7 @@ def newaccount(request, tplname='common/wizard_forms.html'): 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"))],