diff --git a/modoboa/admin/forms/account.py b/modoboa/admin/forms/account.py index 5cc8c4394..cbcbbf587 100644 --- a/modoboa/admin/forms/account.py +++ b/modoboa/admin/forms/account.py @@ -22,7 +22,7 @@ from modoboa.lib.permissions import get_account_roles from modoboa.lib.validators import validate_utf8_email from modoboa.lib.web_utils import render_to_json_response -from ..models import Domain, Mailbox, Alias +from .. import models class AccountFormGeneral(forms.ModelForm): @@ -197,6 +197,16 @@ class AccountFormMail(forms.Form, DynamicForm): "a 'catchall' alias (ex: *@domain.tld)." ) ) + senderaddress = lib_fields.UTF8AndEmptyUserEmailField( + label=ugettext_lazy("Sender addresses"), + required=False, + help_text=ugettext_lazy( + "Additional sender address(es) for this account. The user will be " + "allowed to send emails using this address, even if it " + "does not exist locally. Indicate one address per input. Press " + "ENTER to add a new input." + ) + ) def __init__(self, *args, **kwargs): self.mb = kwargs.pop("instance", None) @@ -211,14 +221,17 @@ class AccountFormMail(forms.Form, DynamicForm): self.extra_fields.append(fname) if self.mb is not None: self.fields["email"].required = True - cpt = 1 qset = self.mb.aliasrecipient_set.filter(alias__internal=False) - for ralias in qset: - name = "aliases_%d" % cpt + for cpt, ralias in enumerate(qset): + name = "aliases_{}".format(cpt + 1) self._create_field( lib_fields.UTF8AndEmptyUserEmailField, name, ralias.alias.address) - cpt += 1 + for cpt, saddress in enumerate(self.mb.senderaddress_set.all()): + name = "senderaddress_{}".format(cpt + 1) + self._create_field( + lib_fields.UTF8AndEmptyUserEmailField, name, + saddress.address) self.fields["email"].initial = self.mb.full_address self.fields["quota_act"].initial = self.mb.use_domain_quota if not self.mb.use_domain_quota and self.mb.quota: @@ -229,6 +242,9 @@ class AccountFormMail(forms.Form, DynamicForm): if len(args) and isinstance(args[0], QueryDict): self._load_from_qdict( args[0], "aliases", lib_fields.UTF8AndEmptyUserEmailField) + self._load_from_qdict( + args[0], "senderaddress", + lib_fields.UTF8AndEmptyUserEmailField) def clean_email(self): """Ensure lower case emails""" @@ -237,8 +253,8 @@ class AccountFormMail(forms.Form, DynamicForm): if not domname: return email try: - self.domain = Domain.objects.get(name=domname) - except Domain.DoesNotExist: + self.domain = models.Domain.objects.get(name=domname) + except models.Domain.DoesNotExist: raise forms.ValidationError(_("Domain does not exist")) if not self.mb: try: @@ -254,23 +270,27 @@ class AccountFormMail(forms.Form, DynamicForm): Check if quota is >= 0 only when the domain value is not used. """ - super(AccountFormMail, self).clean() - if not self.cleaned_data["quota_act"] \ - and self.cleaned_data['quota'] is not None: - if self.cleaned_data["quota"] < 0: - self.add_error("quota", _("Must be a positive integer")) + cleaned_data = super(AccountFormMail, self).clean() + condition = ( + not cleaned_data["quota_act"] and + cleaned_data["quota"] is not None and + cleaned_data["quota"] < 0) + if condition: + self.add_error("quota", _("Must be a positive integer")) self.aliases = [] - for name, value in self.cleaned_data.iteritems(): - if not name.startswith("aliases"): - continue + self.sender_addresses = [] + for name, value in cleaned_data.iteritems(): if value == "": continue - local_part, domname = split_mailbox(value) - if not Domain.objects.filter(name=domname).exists(): - self.add_error(name, _("Local domain does not exist")) - break - self.aliases.append(value.lower()) - return self.cleaned_data + if name.startswith("aliases"): + local_part, domname = split_mailbox(value) + if not models.Domain.objects.filter(name=domname).exists(): + self.add_error(name, _("Local domain does not exist")) + continue + self.aliases.append(value.lower()) + elif name.startswith("senderaddress"): + self.sender_addresses.append(value.lower()) + return cleaned_data def create_mailbox(self, user, account): """Create a mailbox associated to :kw:`account`.""" @@ -278,7 +298,7 @@ class AccountFormMail(forms.Form, DynamicForm): raise lib_exceptions.PermDeniedException core_signals.can_create_object.send( self.__class__, context=user, object_type="mailboxes") - self.mb = Mailbox( + self.mb = models.Mailbox( address=self.locpart, domain=self.domain, user=account, use_domain_quota=self.cleaned_data["quota_act"]) self.mb.set_quota(self.cleaned_data["quota"], @@ -311,12 +331,27 @@ class AccountFormMail(forms.Form, DynamicForm): alias__address=alias).exists(): continue local_part, domname = split_mailbox(alias) - al = Alias(address=alias, enabled=account.is_active) - al.domain = Domain.objects.get(name=domname) + al = models.Alias(address=alias, enabled=account.is_active) + al.domain = models.Domain.objects.get(name=domname) al.save() al.set_recipients([self.mb.full_address]) al.post_create(user) + def _update_sender_addresses(self): + """Update mailbox sender addresses.""" + for saddress in self.mb.senderaddress_set.all(): + if saddress.address not in self.sender_addresses: + saddress.delete() + else: + self.sender_addresses.remove(saddress.address) + if not len(self.sender_addresses): + return + to_create = [] + for saddress in self.sender_addresses: + to_create.append( + models.SenderAddress(address=saddress, mailbox=self.mb)) + models.SenderAddress.objects.bulk_create(to_create) + def save(self, user, account): """Save or update account mailbox.""" if self.cleaned_data["email"] == "": @@ -339,6 +374,7 @@ class AccountFormMail(forms.Form, DynamicForm): account.save() self._update_aliases(user, account) + self._update_sender_addresses() return self.mb @@ -361,7 +397,8 @@ class AccountPermissionsForm(forms.Form, DynamicForm): if not hasattr(self, "account") or self.account is None: return - for pos, dom in enumerate(Domain.objects.get_for_admin(self.account)): + qset = models.Domain.objects.get_for_admin(self.account) + for pos, dom in enumerate(qset): name = "domains_%d" % (pos + 1) self._create_field(lib_fields.DomainNameField, name, dom.name) if len(args) and isinstance(args[0], QueryDict): @@ -370,7 +407,8 @@ class AccountPermissionsForm(forms.Form, DynamicForm): def save(self): current_domains = [ - dom.name for dom in Domain.objects.get_for_admin(self.account) + dom.name for dom in + models.Domain.objects.get_for_admin(self.account) ] for name, value in self.cleaned_data.items(): if not name.startswith("domains"): @@ -378,34 +416,31 @@ class AccountPermissionsForm(forms.Form, DynamicForm): if value in ["", None]: continue if value not in current_domains: - domain = Domain.objects.get(name=value) + domain = models.Domain.objects.get(name=value) domain.add_admin(self.account) - for domain in Domain.objects.get_for_admin(self.account): + for domain in models.Domain.objects.get_for_admin(self.account): if not filter(lambda name: self.cleaned_data[name] == domain.name, self.cleaned_data.keys()): domain.remove_admin(self.account) class AccountForm(TabForms): - """Account edition form.""" def __init__(self, request, *args, **kwargs): self.user = request.user self.forms = [ - dict(id="general", title=_("General"), - formtpl="admin/account_general_form.html", - cls=AccountFormGeneral, - new_args=[self.user], mandatory=True), - dict(id="mail", - title=_("Mail"), formtpl="admin/mailform.html", - cls=AccountFormMail), - dict( - id="perms", title=_("Permissions"), - formtpl="admin/permsform.html", - cls=AccountPermissionsForm - ) + {"id": "general", "title": _("General"), + "formtpl": "admin/account_general_form.html", + "cls": AccountFormGeneral, + "new_args": [self.user], "mandatory": True}, + {"id": "mail", + "title": _("Mail"), "formtpl": "admin/mailform.html", + "cls": AccountFormMail}, + {"id": "perms", "title": _("Permissions"), + "formtpl": "admin/permsform.html", + "cls": AccountPermissionsForm} ] cbargs = [self.user] if "instances" in kwargs: diff --git a/modoboa/admin/migrations/0006_auto_20160910_1030.py b/modoboa/admin/migrations/0006_auto_20160910_1030.py new file mode 100644 index 000000000..75c5bd8f5 --- /dev/null +++ b/modoboa/admin/migrations/0006_auto_20160910_1030.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.5 on 2016-09-10 08:30 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('admin', '0005_auto_20160719_0903'), + ] + + operations = [ + migrations.CreateModel( + name='SenderAddress', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('address', models.EmailField(max_length=254)), + ('mailbox', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='admin.Mailbox')), + ], + ), + migrations.AlterUniqueTogether( + name='senderaddress', + unique_together=set([('address', 'mailbox')]), + ), + ] diff --git a/modoboa/admin/models/__init__.py b/modoboa/admin/models/__init__.py index 0fdc800d3..29205eec3 100644 --- a/modoboa/admin/models/__init__.py +++ b/modoboa/admin/models/__init__.py @@ -1,10 +1,20 @@ +"""Admin models.""" + from .base import AdminObject from .domain import Domain, DNSBLResult from .domain_alias import DomainAlias -from .mailbox import Mailbox, Quota, MailboxOperation +from .mailbox import Mailbox, Quota, MailboxOperation, SenderAddress from .alias import Alias, AliasRecipient __all__ = [ - 'Domain', 'DNSBLResult', 'DomainAlias', 'Mailbox', 'Quota', 'Alias', - 'MailboxOperation', 'AdminObject', 'AliasRecipient' + "AdminObject", + "Alias", + "AliasRecipient", + "DNSBLResult", + "Domain", + "DomainAlias", + "Mailbox", + "MailboxOperation", + "Quota", + "SenderAddress", ] diff --git a/modoboa/admin/models/mailbox.py b/modoboa/admin/models/mailbox.py index 107e8f939..3c44842cb 100644 --- a/modoboa/admin/models/mailbox.py +++ b/modoboa/admin/models/mailbox.py @@ -340,6 +340,26 @@ class Mailbox(AdminObject): reversion.register(Mailbox) +@python_2_unicode_compatible +class SenderAddress(models.Model): + """Extra sender address for Mailbox.""" + + address = models.EmailField() + mailbox = models.ForeignKey(Mailbox) + + class Meta: + app_label = "admin" + unique_together = [ + ("address", "mailbox"), + ] + + def __str__(self): + """Return address.""" + return smart_text(self.address) + +reversion.register(SenderAddress) + + class MailboxOperation(models.Model): """An operation on a mailbox.""" diff --git a/modoboa/admin/postfix_maps.py b/modoboa/admin/postfix_maps.py index a0640a131..b256bd966 100644 --- a/modoboa/admin/postfix_maps.py +++ b/modoboa/admin/postfix_maps.py @@ -110,6 +110,33 @@ class SenderLoginMailboxMap(object): ) +class SenderLoginMailboxExtraMap(object): + """Map file to list per-mailbox extra addresses.""" + + filename = "sql-sender-login-mailboxes-extra.cf" + + # FIXME: is it necessary to filter against user status? + + mysql = ( + "SELECT concat(mb.address, '@', dom.name) FROM admin_mailbox mb " + "INNER JOIN admin_senderaddress sad ON sad.mailbox_id=mb.id " + "INNER JOIN admin_domain dom ON dom.id=mb.domain_id " + "WHERE sad.address='%s'" + ) + postgres = ( + "SELECT mb.address || '@' || dom.name) FROM admin_mailbox mb " + "INNER JOIN admin_senderaddress sad ON sad.mailbox_id=mb.id " + "INNER JOIN admin_domain dom ON dom.id=mb.domain_id " + "WHERE sad.address='%s'" + ) + sqlite = ( + "SELECT mb.address || '@' || dom.name) FROM admin_mailbox mb " + "INNER JOIN admin_senderaddress sad ON sad.mailbox_id=mb.id " + "INNER JOIN admin_domain dom ON dom.id=mb.domain_id " + "WHERE sad.address='%s'" + ) + + class SenderLoginAliasMap(object): """Map file to list authorized sender addresses (from aliases).""" @@ -139,5 +166,5 @@ class SenderLoginAliasMap(object): registry.add_files([ DomainsMap, DomainsAliasesMap, AliasesMap, MaintainMap, - SenderLoginAliasMap, SenderLoginMailboxMap + SenderLoginAliasMap, SenderLoginMailboxMap, SenderLoginMailboxExtraMap ]) diff --git a/modoboa/admin/static/admin/js/admin.js b/modoboa/admin/static/admin/js/admin.js index bb3a64c51..191860f40 100644 --- a/modoboa/admin/static/admin/js/admin.js +++ b/modoboa/admin/static/admin/js/admin.js @@ -493,6 +493,15 @@ Identities.prototype = { return true; } }); + $("#original_senderaddress").dynamic_input({ + input_added: function($row) { + $row.find("label").html(""); + }, + input_removed: function($input) { + $input.parents(".form-group").remove(); + return true; + } + }); $("#id_email").autocompleter({ from_character: "@", choices: $.proxy(this.get_domain_list, this) diff --git a/modoboa/admin/templates/admin/account_detail.html b/modoboa/admin/templates/admin/account_detail.html index fa85b5339..6e9c17b9f 100644 --- a/modoboa/admin/templates/admin/account_detail.html +++ b/modoboa/admin/templates/admin/account_detail.html @@ -26,7 +26,7 @@
| {{ saddress }} |
| {% trans "No sender address defined" %} |