mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-26 06:08:38 +01:00
Merge pull request #3039 from modoboa/improved-csv-import
Improved alias management
This commit is contained in:
@@ -446,7 +446,7 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
|
||||
aliases = []
|
||||
for alias in value:
|
||||
localpart, domain = models.validate_alias_address(
|
||||
alias, self.context["request"].user)
|
||||
alias, self.context["request"].user, ignore_existing=True)
|
||||
aliases.append({"localpart": localpart, "domain": domain})
|
||||
return aliases
|
||||
|
||||
@@ -543,12 +543,12 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
|
||||
self.set_permissions(user, domains)
|
||||
if aliases:
|
||||
for alias in aliases:
|
||||
models.Alias.objects.create(
|
||||
creator=creator,
|
||||
domain=alias["domain"],
|
||||
models.Alias.objects.modify_or_create(
|
||||
address="{}@{}".format(alias["localpart"], alias["domain"]),
|
||||
recipients=[user.username]
|
||||
)
|
||||
recipients=[user.username],
|
||||
creator=creator,
|
||||
domain=alias["domain"]
|
||||
)
|
||||
return user
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
@@ -556,6 +556,7 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
|
||||
mailbox_data = validated_data.pop("mailbox", None)
|
||||
password = validated_data.pop("password", None)
|
||||
domains = validated_data.pop("domains", [])
|
||||
aliases = validated_data.pop("aliases", None)
|
||||
for key, value in validated_data.items():
|
||||
setattr(instance, key, value)
|
||||
if password:
|
||||
@@ -571,6 +572,28 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
|
||||
mailbox_data["full_address"] = validated_data["username"]
|
||||
instance.email = validated_data["username"]
|
||||
self._create_mailbox(creator, instance, mailbox_data)
|
||||
if aliases is not None and "username" in validated_data:
|
||||
# We create a list to keep track of alias we need to delete
|
||||
alias_recipients = instance.mailbox.alias_addresses
|
||||
for alias in aliases:
|
||||
address = "{}@{}".format(alias["localpart"], alias["domain"])
|
||||
models.Alias.objects.modify_or_create(
|
||||
address=address,
|
||||
recipients=[validated_data["username"]],
|
||||
creator=creator,
|
||||
domain=alias["domain"]
|
||||
)
|
||||
try:
|
||||
alias_recipients.remove(address)
|
||||
except ValueError:
|
||||
continue
|
||||
for alias_address in alias_recipients:
|
||||
alias = models.Alias.objects.filter(
|
||||
address=alias_address, internal=False)
|
||||
if alias.exists():
|
||||
alias.first().remove_recipient_or_delete(
|
||||
validated_data["username"]
|
||||
)
|
||||
instance.save()
|
||||
resources = validated_data.get("resources")
|
||||
if resources:
|
||||
|
||||
@@ -253,7 +253,10 @@ class AccountViewSetTestCase(ModoAPITestCase):
|
||||
"password": "Toto12345",
|
||||
"mailbox": {
|
||||
"quota": 10
|
||||
}
|
||||
},
|
||||
"aliases": [
|
||||
"aliasupdate1@test.com"
|
||||
]
|
||||
}
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
@@ -304,6 +307,80 @@ class AccountViewSetTestCase(ModoAPITestCase):
|
||||
self.assertEqual(account.mailbox.message_limit, None)
|
||||
self.assertEqual(account.mailbox.is_send_only, False)
|
||||
|
||||
def test_update_aliases(self):
|
||||
account = core_models.User.objects.get(username="user@test.com")
|
||||
url = reverse("v2:account-detail", args=[account.pk])
|
||||
data = {
|
||||
"username": "user@test.com",
|
||||
"role": "SimpleUsers",
|
||||
"password": "Toto12345",
|
||||
"mailbox": {
|
||||
"quota": 10
|
||||
},
|
||||
"aliases": [
|
||||
"aliasupdate1@test.com"
|
||||
]
|
||||
}
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(
|
||||
models.Alias.objects.filter(address="aliasupdate1@test.com").exists())
|
||||
|
||||
# Create an alias for another user
|
||||
url = reverse("v2:account-list")
|
||||
data = {
|
||||
"username": "toto@test.com",
|
||||
"role": "SimpleUsers",
|
||||
"mailbox": {
|
||||
"use_domain_quota": True
|
||||
},
|
||||
"password": "Toto12345",
|
||||
"language": "fr",
|
||||
"aliases": ["totoalias@test.com"]
|
||||
}
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 201)
|
||||
|
||||
# Try updating existing account with this newly created alias
|
||||
url = reverse("v2:account-detail", args=[account.pk])
|
||||
data = {
|
||||
"username": "user@test.com",
|
||||
"role": "SimpleUsers",
|
||||
"password": "Toto12345",
|
||||
"mailbox": {
|
||||
"quota": 10
|
||||
},
|
||||
"aliases": [
|
||||
"totoalias@test.com",
|
||||
"aliasupdate1@test.com",
|
||||
]
|
||||
}
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
alias = models.Alias.objects.filter(address="totoalias@test.com")
|
||||
self.assertTrue(alias.exists())
|
||||
alias_recipients = list(alias.first().recipients)
|
||||
self.assertIn("toto@test.com", alias_recipients)
|
||||
self.assertIn("user@test.com", alias_recipients)
|
||||
|
||||
# Try deleting the aliases
|
||||
data = {
|
||||
"username": "user@test.com",
|
||||
"role": "SimpleUsers",
|
||||
"mailbox": {
|
||||
"quota": 10
|
||||
},
|
||||
"aliases": []
|
||||
}
|
||||
resp = self.client.patch(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(
|
||||
models.Alias.objects.filter(address="aliasupdate1@test.com").exists())
|
||||
alias_recipients = list(models.Alias.objects.filter(
|
||||
address="totoalias@test.com").first().recipients
|
||||
)
|
||||
self.assertEqual(alias_recipients, ["toto@test.com"])
|
||||
|
||||
def test_update_admin(self):
|
||||
account = core_models.User.objects.get(username="admin")
|
||||
url = reverse("v2:account-detail", args=[account.pk])
|
||||
@@ -458,7 +535,10 @@ class AliasViewSetTestCase(ModoAPITestCase):
|
||||
|
||||
data = {"address": "alias@test.com"}
|
||||
resp = self.client.post(url, data, format="json")
|
||||
self.assertEqual(resp.status_code, 400)
|
||||
self.assertEqual(resp.status_code, 409)
|
||||
|
||||
al_id = models.Alias.objects.get(address="alias@test.com").pk
|
||||
self.assertEqual(resp.json()["id"], al_id)
|
||||
|
||||
data = {"address": "alias2@test.com"}
|
||||
resp = self.client.post(url, data, format="json")
|
||||
|
||||
@@ -18,6 +18,7 @@ from modoboa.lib import pagination
|
||||
from modoboa.lib import renderers as lib_renderers
|
||||
from modoboa.lib import viewsets as lib_viewsets
|
||||
from modoboa.lib.throttle import GetThrottleViewsetMixin
|
||||
from modoboa.lib.exceptions import AliasExists
|
||||
|
||||
from ... import lib
|
||||
from ... import models
|
||||
@@ -163,7 +164,6 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
|
||||
filter_backends = (filters.SearchFilter, dj_filters.DjangoFilterBackend)
|
||||
filterset_class = AccountFilterSet
|
||||
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action in ["create", "validate", "update", "partial_update"]:
|
||||
return serializers.WritableAccountSerializer
|
||||
@@ -264,7 +264,13 @@ class AliasViewSet(v1_viewsets.AliasViewSet):
|
||||
data=request.data,
|
||||
context=self.get_serializer_context(),
|
||||
partial=True)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
serializer.is_valid(raise_exception=True)
|
||||
except AliasExists as e:
|
||||
return response.Response(
|
||||
data={"id": e.alias_id, "status": _("This alias already exists")},
|
||||
status=409
|
||||
)
|
||||
return response.Response(status=204)
|
||||
|
||||
@action(methods=["get"], detail=False)
|
||||
|
||||
@@ -166,15 +166,15 @@ def _import_alias(user, row, **kwargs):
|
||||
|
||||
|
||||
def import_alias(user, row, formopts):
|
||||
_import_alias(user, row, expected_elements=4)
|
||||
_import_alias(user, row, expected_elements=4, formopts=formopts)
|
||||
|
||||
|
||||
def import_forward(user, row, formopts):
|
||||
_import_alias(user, row, expected_elements=4)
|
||||
_import_alias(user, row, expected_elements=4, formopts=formopts)
|
||||
|
||||
|
||||
def import_dlist(user, row, formopts):
|
||||
_import_alias(user, row)
|
||||
_import_alias(user, row, formopts=formopts)
|
||||
|
||||
|
||||
def get_dns_resolver():
|
||||
|
||||
@@ -7,6 +7,7 @@ import os
|
||||
import progressbar
|
||||
from chardet.universaldetector import UniversalDetector
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext as _
|
||||
@@ -72,8 +73,8 @@ class ImportCommand(BaseCommand):
|
||||
if options["continue_if_exists"]:
|
||||
continue
|
||||
raise CommandError(
|
||||
"Object already exists: {}".format(
|
||||
options["sepchar"].join(row[:2])))
|
||||
"Object already exists at line {}: {}".format(
|
||||
i+1, options["sepchar"].join(row[:2])))
|
||||
i += 1
|
||||
pbar.update(i)
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Admin models."""
|
||||
|
||||
from .alarm import Alarm
|
||||
from .alias import Alias, AliasRecipient, validate_alias_address
|
||||
from .alias import (
|
||||
Alias,
|
||||
AliasRecipient,
|
||||
validate_alias_address)
|
||||
from .base import AdminObject
|
||||
from .domain import Domain
|
||||
from .domain_alias import DomainAlias
|
||||
|
||||
@@ -16,7 +16,7 @@ from reversion import revisions as reversion
|
||||
from modoboa.core import signals as core_signals
|
||||
from modoboa.lib.email_utils import split_mailbox
|
||||
from modoboa.lib.exceptions import (
|
||||
BadRequest, Conflict, NotFound, ModoboaException
|
||||
BadRequest, Conflict, NotFound, ModoboaException, AliasExists
|
||||
)
|
||||
from .. import signals
|
||||
from .base import AdminObject
|
||||
@@ -24,7 +24,9 @@ from .domain import Domain
|
||||
from .mailbox import Mailbox
|
||||
|
||||
|
||||
def validate_alias_address(address, creator, internal=False, instance=None):
|
||||
def validate_alias_address(
|
||||
address, creator, internal=False, instance=None, ignore_existing=False
|
||||
):
|
||||
"""Check if the given alias address can be created by creator."""
|
||||
local_part, domain = split_mailbox(address)
|
||||
domain = Domain.objects.filter(name=domain).first()
|
||||
@@ -33,8 +35,13 @@ def validate_alias_address(address, creator, internal=False, instance=None):
|
||||
if not creator.can_access(domain):
|
||||
raise ValidationError(_("Permission denied."))
|
||||
if not instance or instance.address != address:
|
||||
if Alias.objects.filter(address=address, internal=internal).exists():
|
||||
raise ValidationError(_("An alias with this name already exists."))
|
||||
alias = Alias.objects.filter(address=address, internal=internal)
|
||||
condition = (
|
||||
alias.exists()
|
||||
and not ignore_existing
|
||||
)
|
||||
if condition:
|
||||
raise AliasExists(alias.first().pk)
|
||||
if instance is None:
|
||||
try:
|
||||
# Check creator limits
|
||||
@@ -63,6 +70,20 @@ class AliasManager(models.Manager):
|
||||
alias.set_recipients(recipients)
|
||||
return alias
|
||||
|
||||
def modify_or_create(self, address, recipients, creator, domain):
|
||||
"""Add recipient if the alias already exists or create it."""
|
||||
|
||||
alias = self.get_queryset().filter(address=address, internal=False)
|
||||
if alias.exists():
|
||||
alias.first().add_recipients(recipients)
|
||||
return
|
||||
self.create(
|
||||
creator=creator,
|
||||
domain=domain,
|
||||
address=address,
|
||||
recipients=recipients
|
||||
)
|
||||
|
||||
|
||||
class Alias(AdminObject):
|
||||
"""Mailbox alias."""
|
||||
@@ -187,6 +208,21 @@ class Alias(AdminObject):
|
||||
kwargs["r_mailbox"] = rcpt
|
||||
AliasRecipient(**kwargs).save()
|
||||
|
||||
def remove_recipient_or_delete(self, recipient_to_delete: str):
|
||||
"""
|
||||
Delete the selected recipient from this alias
|
||||
or delete the whole alias if only one is left.
|
||||
"""
|
||||
alias_recipients = list(self.recipients)
|
||||
if recipient_to_delete in alias_recipients:
|
||||
if len(alias_recipients) == 1:
|
||||
# Only recipient, we delete the Alias
|
||||
self.delete()
|
||||
else:
|
||||
alias_recipients.remove(recipient_to_delete)
|
||||
self.set_recipients(alias_recipients)
|
||||
self.save()
|
||||
|
||||
def set_recipients(self, address_list):
|
||||
"""Set recipients for this alias."""
|
||||
|
||||
@@ -209,12 +245,33 @@ class Alias(AdminObject):
|
||||
"""Return the number of recipients of this alias."""
|
||||
return self.aliasrecipient_set.count()
|
||||
|
||||
def from_csv(self, user, row, expected_elements=5):
|
||||
def from_csv(self,
|
||||
user,
|
||||
row,
|
||||
expected_elements=5,
|
||||
formopts=False,
|
||||
**kwargs):
|
||||
"""Create a new alias from a CSV file entry."""
|
||||
if len(row) < expected_elements:
|
||||
raise BadRequest(_("Invalid line: {}").format(row))
|
||||
self.address = row[1].strip().lower()
|
||||
local_part, self.domain = validate_alias_address(self.address, user)
|
||||
try:
|
||||
local_part, self.domain = validate_alias_address(self.address, user)
|
||||
except AliasExists as e:
|
||||
# If continue_if_exists is true, we simply update the alias
|
||||
# Else we throw the conflict issue
|
||||
continue_if_exists = False
|
||||
if formopts:
|
||||
continue_if_exists = formopts.get("continue_if_exists", False)
|
||||
if continue_if_exists:
|
||||
alias = Alias.objects.filter(address=self.address,
|
||||
internal=False).first()
|
||||
alias.add_recipients([raddress.strip() for raddress in row[3:]])
|
||||
alias.save()
|
||||
return
|
||||
else:
|
||||
raise e
|
||||
|
||||
self.enabled = (row[2].strip().lower() in ["true", "1", "yes", "y"])
|
||||
self.save()
|
||||
self.set_recipients([raddress.strip() for raddress in row[3:]])
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
alias; alias1@test.com; True; truc@test.com
|
||||
|
@@ -418,3 +418,26 @@ dómain; dómªin2.com; 1000; 100; True
|
||||
self.assertEqual(dom.default_mailbox_quota, 200)
|
||||
self.assertTrue(dom.enabled)
|
||||
self.assertTrue(admin.is_owner(dom))
|
||||
|
||||
def test_import_aliases(self):
|
||||
f = ContentFile("""
|
||||
account; user1@test.com; toto; User; One; True; SimpleUsers; user1@test.com; 0
|
||||
account; Truc@test.com; toto; René; Truc; True; DomainAdmins; truc@test.com; 5; test.com
|
||||
alias; alias1@test.com; True; user1@test.com
|
||||
""", name="identities.csv") # NOQA:E501
|
||||
resp = self.client.post(
|
||||
reverse("admin:identity_import"),
|
||||
{"sourcefile": f, "crypt_password": True}
|
||||
)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
test_file = os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
"test_data/import_aliases.csv"
|
||||
)
|
||||
with self.assertRaises(CommandError) as cm:
|
||||
call_command("modo", "import", test_file)
|
||||
ex_message = cm.exception.messages
|
||||
self.assertTrue(ex_message.startswith("Object already exists"))
|
||||
|
||||
call_command("modo", "import", "--continue-if-exists", test_file)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.conf.urls import include, path
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
path(r'', include('modoboa.urls')),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Extension management."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include
|
||||
from django.urls import include
|
||||
from django.urls import re_path
|
||||
from django.utils.encoding import smart_str
|
||||
|
||||
|
||||
@@ -52,6 +52,16 @@ class Conflict(ModoboaException):
|
||||
http_code = 409
|
||||
|
||||
|
||||
class AliasExists(Conflict):
|
||||
"""
|
||||
Use this exception to indicate that the requested alias already exists
|
||||
and that it should be updated instead of created.
|
||||
"""
|
||||
|
||||
def __init__(self, alias_id):
|
||||
self.alias_id = alias_id
|
||||
|
||||
|
||||
class PermDeniedException(ModoboaException):
|
||||
"""
|
||||
Use this exception when a user tries to do something he is not
|
||||
|
||||
Reference in New Issue
Block a user