-
+
+ @click="selectChoice(choice.value)"
+ >
{{ choice.icon }}
{{ choice.label }}
@@ -25,14 +26,28 @@ export default {
disabled: {
type: Boolean,
default: false
- }
+ },
+ choicesPerLine: Number
},
data () {
return {
- currentChoice: null
+ currentChoice: null,
+ formatedChoices: []
}
},
methods: {
+ formatChoices () {
+ if (this.choicesPerLine) {
+ let sliceIndex = 0
+ while (sliceIndex < this.choices.length) {
+ const result = this.choices.slice(sliceIndex, sliceIndex + this.choicesPerLine)
+ this.formatedChoices.push(result)
+ sliceIndex += this.choicesPerLine
+ }
+ } else {
+ this.formatedChoices.push(this.choices)
+ }
+ },
iconColor (value) {
return (!this.disabled && value === this.currentChoice) ? 'primary' : ''
},
@@ -44,6 +59,9 @@ export default {
this.$emit('input', value)
}
},
+ mounted () {
+ this.formatChoices()
+ },
watch: {
value: {
handler (val) {
@@ -57,7 +75,8 @@ export default {
diff --git a/frontend/src/components/tools/EmailField.vue b/frontend/src/components/tools/EmailField.vue
new file mode 100644
index 000000000..1661719d6
--- /dev/null
+++ b/frontend/src/components/tools/EmailField.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js
index 91c21a61e..ae203cd8b 100644
--- a/frontend/src/store/index.js
+++ b/frontend/src/store/index.js
@@ -3,13 +3,15 @@ import Vuex from 'vuex'
import auth from '@/store/modules/auth'
import domains from '@/store/modules/domains'
+import identities from '@/store/modules/identities'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
auth,
- domains
+ domains,
+ identities
},
strict: process.env.NODE_ENV !== 'production'
})
diff --git a/frontend/src/store/modules/identities.js b/frontend/src/store/modules/identities.js
new file mode 100644
index 000000000..1cfb25b34
--- /dev/null
+++ b/frontend/src/store/modules/identities.js
@@ -0,0 +1,34 @@
+import domainApi from '@/api/domains'
+
+const state = {
+ domains: []
+}
+
+const getters = {
+ domains: state => state.domains,
+ getDomainByName: state => name => {
+ return state.domains.find(domain => domain.name === name)
+ }
+}
+
+const mutations = {
+ SET_DOMAINS (state, domains) {
+ state.domains = domains
+ }
+}
+
+const actions = {
+ fetchDomains ({ commit }) {
+ domainApi.getDomains().then(resp => {
+ commit('SET_DOMAINS', resp.data)
+ })
+ }
+}
+
+export default {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations
+}
diff --git a/frontend/src/views/identities/Identities.vue b/frontend/src/views/identities/Identities.vue
index 9b3fde372..0a6890cb3 100644
--- a/frontend/src/views/identities/Identities.vue
+++ b/frontend/src/views/identities/Identities.vue
@@ -26,15 +26,16 @@
-
+
-
+
@@ -56,6 +57,9 @@ export default {
methods: {
closeCreationWizard () {
this.showCreationWizard = false
+ },
+ updateIdentities () {
+ this.$refs.identities.fetchIdentities()
}
}
}
diff --git a/modoboa/admin/api/v1/serializers.py b/modoboa/admin/api/v1/serializers.py
index e93115678..6e2042af5 100644
--- a/modoboa/admin/api/v1/serializers.py
+++ b/modoboa/admin/api/v1/serializers.py
@@ -153,7 +153,7 @@ class MailboxSerializer(serializers.ModelSerializer):
"""Base mailbox serializer."""
full_address = lib_fields.DRFEmailFieldUTF8()
- quota = serializers.CharField()
+ quota = serializers.CharField(required=False)
class Meta:
model = models.Mailbox
@@ -169,6 +169,15 @@ class MailboxSerializer(serializers.ModelSerializer):
"""Convert quota to MB."""
return web_utils.size2integer(value, output_unit="MB")
+ def validate(self, data):
+ """Check if quota is required."""
+ if not data.get("use_domain_quota", False):
+ if "quota" not in data:
+ raise serializers.ValidationError({
+ "quota": _("This field is required")
+ })
+ return data
+
class AccountSerializer(serializers.ModelSerializer):
"""Base account serializer."""
@@ -262,7 +271,7 @@ class WritableAccountSerializer(AccountSerializer):
def __init__(self, *args, **kwargs):
"""Adapt fields to current user."""
- super(WritableAccountSerializer, self).__init__(*args, **kwargs)
+ super().__init__(*args, **kwargs)
request = self.context.get("request")
if not request:
return
@@ -292,11 +301,11 @@ class WritableAccountSerializer(AccountSerializer):
"full_address": data["username"],
"use_domain_quota": True
}
- elif mailbox["full_address"] != data["username"]:
+ elif "full_address" in mailbox and mailbox["full_address"] != data["username"]:
raise serializers.ValidationError({
"username": _("Must be equal to mailbox full_address")
})
- if not data.get("random_password"):
+ if not self.partial and not data.get("random_password"):
password = data.get("password")
if password:
try:
@@ -415,45 +424,32 @@ class WritableAccountSerializer(AccountSerializer):
class AliasSerializer(serializers.ModelSerializer):
- """Base Alias serializer."""
+ """Alias serializer."""
address = lib_fields.DRFEmailFieldUTF8AndEmptyUser()
recipients = serializers.ListField(
child=lib_fields.DRFEmailFieldUTF8AndEmptyUser(),
allow_empty=False,
- help_text=ugettext_lazy("A list of recipient"))
+ help_text=ugettext_lazy("A list of recipient")
+ )
class Meta:
model = admin_models.Alias
- fields = ("pk", "address", "enabled", "internal", "recipients")
+ fields = (
+ "pk", "address", "enabled", "internal", "recipients"
+ )
def validate_address(self, value):
"""Check domain."""
- local_part, domain = email_utils.split_mailbox(value)
- self.domain = admin_models.Domain.objects.filter(name=domain).first()
- if self.domain is None:
- raise serializers.ValidationError(_("Domain not found."))
- if not self.context["request"].user.can_access(self.domain):
- raise serializers.ValidationError(_("Permission denied."))
+ local_part, self.domain = admin_models.validate_alias_address(
+ value, self.context["request"].user)
return value
def create(self, validated_data):
"""Create appropriate objects."""
creator = self.context["request"].user
- try:
- core_signals.can_create_object.send(
- sender=self.__class__, context=creator,
- klass=admin_models.Alias)
- core_signals.can_create_object.send(
- sender=self.__class__, context=self.domain,
- object_type="mailbox_aliases")
- except lib_exceptions.ModoboaException as inst:
- raise serializers.ValidationError(force_text(inst))
- recipients = validated_data.pop("recipients", None)
- alias = admin_models.Alias(domain=self.domain, **validated_data)
- alias.save(creator=creator)
- alias.set_recipients(recipients)
- return alias
+ return admin_models.Alias.objects.create(
+ creator=creator, domain=self.domain, **validated_data)
def update(self, instance, validated_data):
"""Update objects."""
diff --git a/modoboa/admin/api/v2/serializers.py b/modoboa/admin/api/v2/serializers.py
index 99198f194..058d2b091 100644
--- a/modoboa/admin/api/v2/serializers.py
+++ b/modoboa/admin/api/v2/serializers.py
@@ -7,6 +7,7 @@ from rest_framework import serializers
from modoboa.admin.api.v1 import serializers as v1_serializers
from modoboa.core import models as core_models, signals as core_signals
from modoboa.lib import exceptions as lib_exceptions
+from modoboa.lib import fields as lib_fields
from modoboa.parameters import tools as param_tools
from ... import constants, lib, models
@@ -148,3 +149,49 @@ class IdentitySerializer(serializers.Serializer):
identity = serializers.CharField()
name_or_rcpt = serializers.CharField()
tags = TagSerializer(many=True)
+
+
+class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
+ """Add support for aliases and sender addresses."""
+
+ aliases = serializers.ListField(
+ child=lib_fields.DRFEmailFieldUTF8(),
+ required=False
+ )
+
+ class Meta(v1_serializers.WritableAccountSerializer.Meta):
+ fields = v1_serializers.WritableAccountSerializer.Meta.fields + (
+ "aliases",
+ )
+
+ def validate_aliases(self, value):
+ """Check if required domains are locals and user can access them."""
+ aliases = []
+ for alias in value:
+ localpart, domain = models.validate_alias_address(
+ alias, self.context["request"].user)
+ aliases.append({"localpart": localpart, "domain": domain})
+ return aliases
+
+ def create(self, validated_data):
+ """Handle aliases."""
+ user = self.context["request"].user
+ aliases = validated_data.pop("aliases", None)
+ user = super().create(validated_data)
+ if aliases:
+ for alias in aliases:
+ models.Alias.objects.create(
+ creator=user,
+ domain=alias.domain,
+ address=alias.localpart,
+ recipients=[validated_data["mailbox"]["full_address"]]
+ )
+ return user
+
+
+class AliasSerializer(v1_serializers.AliasSerializer):
+ """Alias serializer for v2 API."""
+
+ class Meta(v1_serializers.AliasSerializer.Meta):
+ # We remove 'internal' field
+ fields = ("pk", "address", "enabled", "internal", "recipients")
diff --git a/modoboa/admin/api/v2/viewsets.py b/modoboa/admin/api/v2/viewsets.py
index d0a942aa1..43ca7b5a9 100644
--- a/modoboa/admin/api/v2/viewsets.py
+++ b/modoboa/admin/api/v2/viewsets.py
@@ -136,6 +136,11 @@ 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"]:
+ return serializers.WritableAccountSerializer
+ return super().get_serializer_class()
+
def get_queryset(self):
"""Filter queryset based on current user."""
user = self.request.user
@@ -146,6 +151,16 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
)
return core_models.User.objects.filter(pk__in=ids)
+ @action(methods=["post"], detail=False)
+ def validate(self, request, **kwargs):
+ """Validate given alias without creating it."""
+ serializer = self.get_serializer(
+ data=request.data,
+ context=self.get_serializer_context(),
+ partial=True)
+ serializer.is_valid(raise_exception=True)
+ return response.Response(status=204)
+
class IdentityViewSet(viewsets.ViewSet):
"""Viewset for identities."""
diff --git a/modoboa/admin/models/__init__.py b/modoboa/admin/models/__init__.py
index 67e40e208..e5a6c1d69 100644
--- a/modoboa/admin/models/__init__.py
+++ b/modoboa/admin/models/__init__.py
@@ -1,7 +1,7 @@
"""Admin models."""
from .alarm import Alarm
-from .alias import Alias, AliasRecipient
+from .alias import Alias, AliasRecipient, validate_alias_address
from .base import AdminObject
from .domain import Domain
from .domain_alias import DomainAlias
@@ -21,4 +21,5 @@ __all__ = [
"MXRecord",
"Quota",
"SenderAddress",
+ "validate_alias_address",
]
diff --git a/modoboa/admin/models/alias.py b/modoboa/admin/models/alias.py
index 468b335df..f31db2af2 100644
--- a/modoboa/admin/models/alias.py
+++ b/modoboa/admin/models/alias.py
@@ -3,8 +3,7 @@
import hashlib
import random
-from reversion import revisions as reversion
-
+from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.encoding import (
@@ -12,10 +11,12 @@ from django.utils.encoding import (
)
from django.utils.translation import ugettext as _, ugettext_lazy
+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, PermDeniedException
+ BadRequest, Conflict, NotFound, ModoboaException
)
from .. import signals
from .base import AdminObject
@@ -23,9 +24,43 @@ from .domain import Domain
from .mailbox import Mailbox
-@python_2_unicode_compatible
-class Alias(AdminObject):
+def validate_alias_address(address, creator, internal=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()
+ if domain is None:
+ raise ValidationError(_("Domain not found."))
+ if not creator.can_access(domain):
+ raise ValidationError(_("Permission denied."))
+ if Alias.objects.filter(address=address, internal=internal).exists():
+ raise ValidationError(_("An alias with this name already exists."))
+ try:
+ # Check creator limits
+ core_signals.can_create_object.send(
+ sender="validate_alias_address", context=creator, klass=Alias)
+ # Check domain limits
+ core_signals.can_create_object.send(
+ sender="validate_alias_address", context=domain,
+ object_type="mailbox_aliases")
+ except ModoboaException as inst:
+ raise ValidationError(str(inst))
+ return local_part, domain
+
+class AliasManager(models.Manager):
+ """Custom manager for Alias."""
+
+ def create(self, *args, **kwargs):
+ """Custom create method."""
+ creator = kwargs.pop("creator")
+ recipients = kwargs.pop("recipients")
+ alias = super().create(*args, **kwargs)
+ alias.post_create(creator)
+ alias.set_recipients(recipients)
+ return alias
+
+
+class Alias(AdminObject):
"""Mailbox alias."""
address = models.CharField(
@@ -47,13 +82,15 @@ class Alias(AdminObject):
ugettext_lazy("Expire at"), blank=True, null=True)
_objectname = "MailboxAlias"
+ objects = AliasManager()
+
class Meta:
ordering = ["address"]
unique_together = (("address", "internal"), )
app_label = "admin"
def __str__(self):
- return smart_text(self.address)
+ return self.address
@classmethod
def generate_random_address(cls):
@@ -167,23 +204,11 @@ class Alias(AdminObject):
def from_csv(self, user, row, expected_elements=5):
"""Create a new alias from a CSV file entry."""
if len(row) < expected_elements:
- raise BadRequest(_("Invalid line: %s" % row))
- address = row[1].strip().lower()
- localpart, domname = split_mailbox(address)
- try:
- domain = Domain.objects.get(name=domname)
- except Domain.DoesNotExist:
- raise BadRequest(_("Domain '%s' does not exist" % domname))
- if not user.can_access(domain):
- raise PermDeniedException
- core_signals.can_create_object.send(
- sender="import", context=user, klass=Alias)
- core_signals.can_create_object.send(
- sender="import", context=domain, object_type="mailbox_aliases")
- if Alias.objects.filter(address=address, internal=False).exists():
+ raise BadRequest(_("Invalid line: {}").format(row))
+ self.address = row[1].strip().lower()
+ local_part, self.domain = validate_alias_address(self.address, user)
+ if Alias.objects.filter(address=self.address, internal=False).exists():
raise Conflict
- self.address = address
- self.domain = domain
self.enabled = (row[2].strip().lower() in ["true", "1", "yes", "y"])
self.save()
self.set_recipients([raddress.strip() for raddress in row[3:]])