From 66a340466997044dcaf5e4b3576ad772a04bcff9 Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 20 Jul 2016 19:19:38 +0200 Subject: [PATCH] Random aliases. see #522 --- modoboa/admin/forms/alias.py | 34 ++++++++++++++++--- .../migrations/0005_auto_20160719_0903.py | 25 ++++++++++++++ modoboa/admin/models/alias.py | 13 +++++++ modoboa/admin/postfix_maps.py | 6 ++-- modoboa/admin/static/admin/js/admin.js | 12 +++++++ .../admin/templates/admin/alias_detail.html | 6 ++++ modoboa/admin/templates/admin/aliasform.html | 23 ++++++++----- modoboa/admin/tests/test_alias.py | 26 ++++++++++---- 8 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 modoboa/admin/migrations/0005_auto_20160719_0903.py diff --git a/modoboa/admin/forms/alias.py b/modoboa/admin/forms/alias.py index 8195d5613..75f9d2745 100644 --- a/modoboa/admin/forms/alias.py +++ b/modoboa/admin/forms/alias.py @@ -20,6 +20,7 @@ from ..models import Domain, Alias class AliasForm(forms.ModelForm, DynamicForm): """A form to create/modify an alias.""" + random_address = forms.BooleanField(required=False) address = lib_fields.UTF8AndEmptyUserEmailField( label=ugettext_lazy("Email address"), help_text=ugettext_lazy( @@ -39,16 +40,27 @@ class AliasForm(forms.ModelForm, DynamicForm): class Meta: model = Alias - fields = ("address", "enabled", ) + fields = ("address", "domain", "enabled", "expire_at", "description") + widgets = { + "domain": forms.widgets.Select( + attrs={"class": "selectpicker", "data-live-search": "true"}) + } def __init__(self, user, *args, **kwargs): self.user = user super(AliasForm, self).__init__(*args, **kwargs) self.fields = OrderedDict( (key, self.fields[key]) for key in - ['address', 'recipients', 'enabled'] + ["random_address", "address", "domain", "recipients", "enabled", + "expire_at", "description"] ) - + if self.instance.pk: + del self.fields["random_address"] + del self.fields["domain"] + else: + self.fields["address"].required = False + self.fields["domain"].required = False + self.fields["domain"].queryset = Domain.objects.get_for_admin(user) if len(args) and isinstance(args[0], QueryDict): if "instance" in kwargs: if not kwargs["instance"].domain.enabled: @@ -66,6 +78,8 @@ class AliasForm(forms.ModelForm, DynamicForm): def clean_address(self): """Check if address points to a local domain.""" + if not self.cleaned_data["address"]: + return self.cleaned_data["address"] localpart, domname = split_mailbox(self.cleaned_data["address"]) try: domain = Domain.objects.get(name=domname) @@ -87,6 +101,12 @@ class AliasForm(forms.ModelForm, DynamicForm): def clean(self): """Check it there is at least one recipient.""" super(AliasForm, self).clean() + random_address = self.cleaned_data.get("random_address") + if not random_address: + if not self.cleaned_data.get("address"): + self.add_error("address", _("This field is required")) + elif not self.cleaned_data.get("domain"): + self.add_error("domain", _("This field is required")) for field, value in self.cleaned_data.items(): if field.startswith("recipients") and value: return self.cleaned_data @@ -96,8 +116,12 @@ class AliasForm(forms.ModelForm, DynamicForm): def save(self, commit=True): """Custom save method.""" alias = super(AliasForm, self).save(commit=False) - local_part, domname = split_mailbox(self.cleaned_data["address"]) - alias.domain = Domain.objects.get(name=domname) + if self.cleaned_data.get("random_address"): + alias.address = "{}@{}".format( + Alias.generate_random_address(), alias.domain) + else: + local_part, domname = split_mailbox(self.cleaned_data["address"]) + alias.domain = Domain.objects.get(name=domname) if commit: alias.save() address_list = [ diff --git a/modoboa/admin/migrations/0005_auto_20160719_0903.py b/modoboa/admin/migrations/0005_auto_20160719_0903.py new file mode 100644 index 000000000..343ac82b1 --- /dev/null +++ b/modoboa/admin/migrations/0005_auto_20160719_0903.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2016-07-19 07:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0004_auto_20160608_1423'), + ] + + operations = [ + migrations.AddField( + model_name='alias', + name='description', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='alias', + name='expire_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/modoboa/admin/models/alias.py b/modoboa/admin/models/alias.py index c31147e56..c277f97a8 100644 --- a/modoboa/admin/models/alias.py +++ b/modoboa/admin/models/alias.py @@ -1,5 +1,8 @@ """Models related to aliases management.""" +import hashlib +import random + from django.core.urlresolvers import reverse from django.db import models from django.utils.encoding import python_2_unicode_compatible, smart_text @@ -37,6 +40,8 @@ class Alias(AdminObject): default=True ) internal = models.BooleanField(default=False) + description = models.TextField(blank=True) + expire_at = models.DateTimeField(blank=True, null=True) _objectname = 'MailboxAlias' class Meta: @@ -50,6 +55,14 @@ class Alias(AdminObject): def __str__(self): return smart_text(self.address) + @classmethod + def generate_random_address(self): + """Generate a random address (local part).""" + m = hashlib.md5() + for x in random.sample(xrange(10000000), 60): + m.update(str(x)) + return m.hexdigest()[:20] + @property def identity(self): return self.address diff --git a/modoboa/admin/postfix_maps.py b/modoboa/admin/postfix_maps.py index 72cf8df24..a0640a131 100644 --- a/modoboa/admin/postfix_maps.py +++ b/modoboa/admin/postfix_maps.py @@ -52,17 +52,17 @@ class AliasesMap(object): mysql = ( "SELECT alr.address FROM modoboa_admin_aliasrecipient AS alr " "INNER JOIN admin_alias AS al ON alr.alias_id=al.id " - "WHERE al.enabled=1 AND al.address='%s'" + "WHERE al.enabled=1 AND al.address='%s' AND al.expire_at>now()" ) postgres = ( "SELECT alr.address FROM modoboa_admin_aliasrecipient AS alr " "INNER JOIN admin_alias AS al ON alr.alias_id=al.id " - "WHERE al.enabled AND al.address='%s'" + "WHERE al.enabled AND al.address='%s' AND al.expire_at>now()" ) sqlite = ( "SELECT alr.address FROM modoboa_admin_aliasrecipient AS alr " "INNER JOIN admin_alias AS al ON alr.alias_id=al.id " - "WHERE al.enabled=1 AND al.address='%s'" + "WHERE al.enabled=1 AND al.address='%s' AND al.expire_at>now()" ) diff --git a/modoboa/admin/static/admin/js/admin.js b/modoboa/admin/static/admin/js/admin.js index ca52954da..bb3a64c51 100644 --- a/modoboa/admin/static/admin/js/admin.js +++ b/modoboa/admin/static/admin/js/admin.js @@ -552,6 +552,15 @@ Identities.prototype = { }, aliasform_cb: function() { + $("#id_random_address").click(function(evt) { + if ($(this).prop("checked")) { + $("#id_address").parents(".form-group").hide(); + $("#id_domain").parents(".form-group").show(); + } else { + $("#id_address").parents(".form-group").show(); + $("#id_domain").parents(".form-group").hide(); + } + }); $("#id_address").autocompleter({ from_character: "@", choices: $.proxy(this.get_domain_list, this) @@ -565,6 +574,9 @@ Identities.prototype = { return true; } }); + $("#id_expire_at").datetimepicker({ + format: 'YYYY-MM-DD HH:mm:ss' + }); $(".submit").on('click', $.proxy(function(e) { simple_ajax_form_post(e, { formid: "aliasform", diff --git a/modoboa/admin/templates/admin/alias_detail.html b/modoboa/admin/templates/admin/alias_detail.html index 01d49a6dc..6e00125ec 100644 --- a/modoboa/admin/templates/admin/alias_detail.html +++ b/modoboa/admin/templates/admin/alias_detail.html @@ -25,6 +25,12 @@
{% trans "Created" %}
{{ alias.creation|date:"SHORT_DATETIME_FORMAT" }}
{% trans "Modified" %}
{{ alias.last_modification|date:"SHORT_DATETIME_FORMAT" }}
+ {% if alias.expire_at %} +
{% trans "Expire at" %}
{{ alias.expire_at|date:"SHORT_DATETIME_FORMAT" }}
+ {% endif %} + {% if alias.description %} +
{% trans "Description" %}
{{ alias.description }}
+ {% endif %}
diff --git a/modoboa/admin/templates/admin/aliasform.html b/modoboa/admin/templates/admin/aliasform.html index af9b43954..934028565 100644 --- a/modoboa/admin/templates/admin/aliasform.html +++ b/modoboa/admin/templates/admin/aliasform.html @@ -1,10 +1,17 @@ -{% extends "common/generic_modal_form.html" %}{% load form_tags %} +{% extends "common/generic_modal_form.html" %} + +{% load form_tags %} + {% block formbody %} -{% render_field form.address %} -{% render_fields_group form "recipients" %} -{% if not form.instance.pk or form.instance.domain.enabled %} -{% render_field form.enabled %} -{% else %} -{% include "common/generic_field.html" with field=form.enabled label_width="col-sm-4" inline_message=_("Domain is disabled") extra_classes="info" %} -{% endif %} + {% if form.random_address %}{% render_field form.random_address %}{% endif %} + {% render_field form.address %} + {% if form.domain %}{% render_field form.domain hidden=True %}{% endif %} + {% render_fields_group form "recipients" %} + {% if not form.instance.pk or form.instance.domain.enabled %} + {% render_field form.enabled %} + {% else %} + {% include "common/generic_field.html" with field=form.enabled label_width="col-sm-4" inline_message=_("Domain is disabled") extra_classes="info" %} + {% endif %} + {% render_field form.expire_at %} + {% render_field form.description %} {% endblock %} diff --git a/modoboa/admin/tests/test_alias.py b/modoboa/admin/tests/test_alias.py index 3a7a4b162..926b2eabf 100644 --- a/modoboa/admin/tests/test_alias.py +++ b/modoboa/admin/tests/test_alias.py @@ -8,7 +8,7 @@ from modoboa.core.models import User from modoboa.lib.tests import ModoTestCase from .. import factories -from ..models import Alias, AliasRecipient +from ..models import Alias, AliasRecipient, Domain class AliasTestCase(ModoTestCase): @@ -126,11 +126,13 @@ class AliasTestCase(ModoTestCase): ) def test_dlist(self): - values = dict(address="all@test.com", - recipients="user@test.com", - recipients_1="admin@test.com", - recipients_2="ext@titi.com", - enabled=True) + values = { + "address": "all@test.com", + "recipients": "user@test.com", + "recipients_1": "admin@test.com", + "recipients_2": "ext@titi.com", + "enabled": True + } self.ajax_post( reverse("admin:alias_add"), values ) @@ -214,6 +216,18 @@ class AliasTestCase(ModoTestCase): reverse("admin:alias_add"), values ) + def test_random_alias(self): + """Test creation of a random alias.""" + alias_count = Alias.objects.count() + values = { + "random_address": True, + "domain": Domain.objects.get(name="test.com").pk, + "recipients": "user@test.com", + "enabled": True + } + self.ajax_post(reverse("admin:alias_add"), values) + self.assertEqual(Alias.objects.count(), alias_count + 1) + def test_distribution_list_deletion_on_user_update_bug(self): """This test demonstrates an issue with distribution list being deleted when one of the users which belong to that list is