mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
@@ -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:
|
||||
|
||||
@@ -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')]),
|
||||
),
|
||||
]
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
])
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<th>{% trans "Full name" %}</th><td>{{ object.fullname }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Role" %}</th><td>{{ object.role }}</td>
|
||||
<th>{% trans "Role" %}</th><td>{{ object.get_role_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{% trans "Created" %}</th><td>{{ object.date_joined|date:"SHORT_DATETIME_FORMAT" }}</td>
|
||||
@@ -82,6 +82,18 @@
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel panel-info">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Additional sender addresses" %}</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
{% for saddress in object.mailbox.senderaddress_set.all %}
|
||||
<tr><td>{{ saddress }}</td></tr>
|
||||
{% empty %}
|
||||
<tr><td>{% trans "No sender address defined" %}</td></tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for template in templates.right %}
|
||||
{% include template %}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{% load form_tags %}
|
||||
|
||||
{% render_field form.email %}
|
||||
{% include "common/generic_field.html" with field=form.quota help_display_mode="tooltip" label_width="col-sm-4" appended_text=_("MB") activator=True activator_value=form.quota_act.value deactivate_if_empty=False activator_label=_("Use domain default value") activator_size="col-sm-5" %}
|
||||
{% render_extra_fields form %}
|
||||
{% render_fields_group form "aliases" %}
|
||||
{% render_fields_group form "senderaddress" %}
|
||||
|
||||
@@ -70,6 +70,29 @@ class AccountTestCase(ModoTestCase):
|
||||
alias__internal=True).exists()
|
||||
)
|
||||
|
||||
def test_sender_address(self):
|
||||
"""Check if sender addresses are saved."""
|
||||
account = User.objects.get(username="user@test.com")
|
||||
values = {
|
||||
"username": "user@test.com", "first_name": "Tester",
|
||||
"last_name": "Toto", "role": "SimpleUsers",
|
||||
"quota_act": True, "is_active": True, "email": "user@test.com",
|
||||
"senderaddress": "test@titi.com", "senderaddress_1": "toto@go.com"
|
||||
}
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", args=[account.pk]), values)
|
||||
self.assertEqual(
|
||||
models.SenderAddress.objects.filter(
|
||||
mailbox__address="user").count(),
|
||||
2)
|
||||
del values["senderaddress_1"]
|
||||
self.ajax_post(
|
||||
reverse("admin:account_change", args=[account.pk]), values)
|
||||
self.assertEqual(
|
||||
models.SenderAddress.objects.filter(
|
||||
mailbox__address="user").count(),
|
||||
1)
|
||||
|
||||
def test_conflicts(self):
|
||||
"""Check if unicity constraints are respected."""
|
||||
values = {
|
||||
|
||||
@@ -11,5 +11,6 @@ class MapFilesTestCase(MapFilesTestCaseMixin, TestCase):
|
||||
|
||||
MAP_FILES = [
|
||||
"sql-domains.cf", "sql-domain-aliases.cf", "sql-aliases.cf",
|
||||
"sql-maintain.cf"
|
||||
"sql-maintain.cf", "sql-sender-login-mailboxes.cf",
|
||||
"sql-sender-login-mailboxes-extra.cf", "sql-sender-login-aliases.cf"
|
||||
]
|
||||
|
||||
@@ -174,7 +174,7 @@ def newaccount(request):
|
||||
@login_required
|
||||
@permission_required("core.change_user")
|
||||
@reversion.create_revision()
|
||||
def editaccount(request, pk, tplname="common/tabforms.html"):
|
||||
def editaccount(request, pk):
|
||||
account = User.objects.get(pk=pk)
|
||||
if not request.user.can_access(account):
|
||||
raise PermDeniedException
|
||||
|
||||
@@ -2,6 +2,20 @@
|
||||
|
||||
"""Core application constants."""
|
||||
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
SIMPLEUSERS_ROLE = ("SimpleUsers", ugettext_lazy("Simple user"))
|
||||
DOMAINADMINS_ROLE = ("DomainAdmins", ugettext_lazy("Domain administrator"))
|
||||
RESELLERS_ROLE = ("Resellers", ugettext_lazy("Reseller"))
|
||||
SUPERADMINS_ROLE = ("SuperAdmins", ugettext_lazy("Super administrator"))
|
||||
|
||||
ROLES = (
|
||||
SIMPLEUSERS_ROLE,
|
||||
DOMAINADMINS_ROLE,
|
||||
RESELLERS_ROLE,
|
||||
SUPERADMINS_ROLE,
|
||||
)
|
||||
|
||||
ADMIN_GROUPS = [
|
||||
"SuperAdmins",
|
||||
"Resellers",
|
||||
|
||||
@@ -357,6 +357,13 @@ class User(PermissionsMixin):
|
||||
self.save()
|
||||
self._role = role
|
||||
|
||||
def get_role_display(self):
|
||||
"""Return the display name of this role."""
|
||||
for role in constants.ROLES:
|
||||
if role[0] == self.role:
|
||||
return role[1]
|
||||
return _("Unknown")
|
||||
|
||||
@cached_property
|
||||
def is_admin(self):
|
||||
"""Shortcut to check if user is administrator."""
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
# coding: utf-8
|
||||
"""Object level permissions."""
|
||||
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
from modoboa.core import constants as core_constants
|
||||
from modoboa.core.models import ObjectAccess, User
|
||||
from modoboa.lib import events
|
||||
|
||||
SIMPLEUSERS_ROLE = ("SimpleUsers", ugettext_lazy("Simple user"))
|
||||
DOMAINADMINS_ROLE = ("DomainAdmins", ugettext_lazy("Domain administrator"))
|
||||
RESELLERS_ROLE = ("Resellers", ugettext_lazy("Reseller"))
|
||||
SUPERADMINS_ROLE = ("SuperAdmins", ugettext_lazy("Super administrator"))
|
||||
|
||||
|
||||
def get_account_roles(user, account=None):
|
||||
"""Return the list of available account roles.
|
||||
@@ -23,15 +19,16 @@ def get_account_roles(user, account=None):
|
||||
:param ``User`` account: account beeing modified (None on creation)
|
||||
:return: list of strings
|
||||
"""
|
||||
result = [SIMPLEUSERS_ROLE]
|
||||
result = [core_constants.SIMPLEUSERS_ROLE]
|
||||
filters = events.raiseQueryEvent(
|
||||
"UserCanSetRole", user, "DomainAdmins", account
|
||||
)
|
||||
if user.has_perm("admin.add_domain") and \
|
||||
(not filters or True in filters):
|
||||
result += [DOMAINADMINS_ROLE]
|
||||
result += [core_constants.DOMAINADMINS_ROLE]
|
||||
if user.is_superuser:
|
||||
result += [RESELLERS_ROLE, SUPERADMINS_ROLE]
|
||||
result += [
|
||||
core_constants.RESELLERS_ROLE, core_constants.SUPERADMINS_ROLE]
|
||||
result += events.raiseQueryEvent("GetExtraRoles", user, account)
|
||||
return sorted(result, key=lambda role: role[1])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user