mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-20 06:08:35 +02:00
Improved alias management
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-chip
|
||||
v-for="(alias, index) in aliases"
|
||||
v-for="(alias, index) in form.aliases"
|
||||
:key="index"
|
||||
class="mr-2 mt-2"
|
||||
close
|
||||
@@ -37,19 +37,25 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
currentAlias: '',
|
||||
aliases: []
|
||||
form: {
|
||||
mailbox: {},
|
||||
aliases: []
|
||||
},
|
||||
currentAlias: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reset () {
|
||||
this.currentAlias = ''
|
||||
this.aliases = []
|
||||
this.form.aliases = []
|
||||
},
|
||||
update () {
|
||||
this.$emit('input', this.form)
|
||||
},
|
||||
async addAlias () {
|
||||
try {
|
||||
await accounts.validate({ aliases: [this.currentAlias] })
|
||||
this.aliases.push(this.currentAlias)
|
||||
await accounts.validate({ aliases: [this.currentAlias], mailbox: this.form.mailbox })
|
||||
this.form.aliases.push(this.currentAlias)
|
||||
this.$refs.aliasField.reset()
|
||||
} catch (error) {
|
||||
let errorMsg = null
|
||||
@@ -62,15 +68,20 @@ export default {
|
||||
}
|
||||
},
|
||||
removeAlias (index) {
|
||||
this.aliases.splice(index, 1)
|
||||
this.form.aliases.splice(index, 1)
|
||||
this.update()
|
||||
},
|
||||
update () {
|
||||
this.$emit('input', this.aliases)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.aliases = [...this.value]
|
||||
this.form = { ...this.value }
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler (newValue) {
|
||||
this.form = { ...newValue }
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<account-mailbox-form :ref="`form_${step}`" v-model="account" />
|
||||
</template>
|
||||
<template v-slot:form.aliases="{ step }">
|
||||
<account-alias-form v-if="step >= 4" :ref="`form_${step}`" v-model="account.aliases" />
|
||||
<account-alias-form v-if="step >= 4" :ref="`form_${step}`" v-model="account" />
|
||||
</template>
|
||||
<template v-slot:item.random_password="{ item }">
|
||||
<template v-if="item.value">
|
||||
|
||||
@@ -103,6 +103,37 @@
|
||||
<account-mailbox-form ref="mailboxForm" v-model="editedAccount" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel v-if="usernameIsEmail">
|
||||
<v-expansion-panel-header v-slot="{ open }">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="4">
|
||||
<translate>Alias</translate>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="8"
|
||||
class="text--secondary"
|
||||
>
|
||||
<v-fade-transition leave-absolute>
|
||||
<span v-if="open"></span>
|
||||
<v-row
|
||||
v-else
|
||||
no-gutters
|
||||
style="width: 100%"
|
||||
>
|
||||
<template v-if="account.aliases">
|
||||
<v-col cols="6">
|
||||
<translate class="mr-2">Aliases: </translate> <translate>Number of associated alias: </translate> {{ account.aliases.length }}
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<account-alias-form ref="aliasForm" v-model="editedAccount" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
<v-expansion-panel
|
||||
v-if="limitsConfig.params && limitsConfig.params.enable_admin_limits && account.role !== 'SimpleUsers' && account.role !== 'SuperAdmins'"
|
||||
>
|
||||
@@ -135,6 +166,7 @@ import { bus } from '@/main'
|
||||
import accounts from '@/api/accounts'
|
||||
import AccountGeneralForm from './AccountGeneralForm'
|
||||
import AccountMailboxForm from './AccountMailboxForm'
|
||||
import AccountAliasForm from './AccountAliasForm'
|
||||
import AccountRoleForm from './AccountRoleForm'
|
||||
import ResourcesForm from '@/components/tools/ResourcesForm'
|
||||
import parameters from '@/api/parameters'
|
||||
@@ -143,6 +175,7 @@ export default {
|
||||
components: {
|
||||
AccountGeneralForm,
|
||||
AccountMailboxForm,
|
||||
AccountAliasForm,
|
||||
AccountRoleForm,
|
||||
ResourcesForm
|
||||
},
|
||||
|
||||
@@ -101,7 +101,14 @@ export default {
|
||||
if (data.recipients.length === 0) {
|
||||
delete data.recipients
|
||||
}
|
||||
return aliases.validate(data)
|
||||
const validation = aliases.validate(data)
|
||||
validation.catch(error => {
|
||||
if (error.response.status === 409 && error.response.data.id !== undefined) {
|
||||
bus.$emit('notification', { msg: this.$gettext('Alias already exists, redirecting to edit page'), type: 'warning' })
|
||||
this.$router.push({ name: 'AliasEdit', params: { id: error.response.data.id } })
|
||||
}
|
||||
})
|
||||
return validation
|
||||
},
|
||||
submit () {
|
||||
aliases.create(this.alias).then(resp => {
|
||||
|
||||
@@ -29,13 +29,19 @@
|
||||
dense
|
||||
/>
|
||||
</validation-provider>
|
||||
<label class="m-label"><translate>Separator</translate></label>
|
||||
<v-text-field
|
||||
v-model="form.sepchar"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
name="name"
|
||||
rules="required"
|
||||
>
|
||||
<label class="m-label"><translate>Separator</translate></label>
|
||||
<v-text-field
|
||||
v-model="form.sepchar"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
dense
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-switch
|
||||
v-model="form.continue_if_exists"
|
||||
:label="'Continue on error'|translate"
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
:title="'Import identities'|translate"
|
||||
@beforeSubmit="prepareData"
|
||||
@submit="importIdentities"
|
||||
@close="showImportForm = false"
|
||||
@close="closeImportForm"
|
||||
>
|
||||
<template v-slot:help>
|
||||
<ul>
|
||||
@@ -124,10 +124,16 @@ export default {
|
||||
})
|
||||
},
|
||||
prepareData (data) {
|
||||
data.append('crypt_passwords', this.form.crypt_passwords)
|
||||
if (this.form !== undefined) {
|
||||
data.append('crypt_passwords', this.form.crypt_passwords)
|
||||
}
|
||||
},
|
||||
importIdentities (data) {
|
||||
this.importContent(identities, data)
|
||||
},
|
||||
closeImportForm () {
|
||||
this.showImportForm = false
|
||||
this.updateIdentities()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
address="{}@{}".format(alias["localpart"], alias["domain"]),
|
||||
recipients=[user.username]
|
||||
)
|
||||
models.modify_or_create_alias(
|
||||
"{}@{}".format(alias["localpart"], alias["domain"]),
|
||||
[user.username],
|
||||
creator,
|
||||
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,14 @@ 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 and "username" in validated_data:
|
||||
for alias in aliases:
|
||||
models.modify_or_create_alias(
|
||||
"{}@{}".format(alias["localpart"], alias["domain"]),
|
||||
[validated_data["username"]],
|
||||
creator,
|
||||
alias["domain"]
|
||||
)
|
||||
instance.save()
|
||||
resources = validated_data.get("resources")
|
||||
if resources:
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -79,8 +79,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,8 @@
|
||||
"""Admin models."""
|
||||
|
||||
from .alarm import Alarm
|
||||
from .alias import Alias, AliasRecipient, validate_alias_address
|
||||
from .alias import (
|
||||
Alias, AliasRecipient, validate_alias_address, modify_or_create_alias)
|
||||
from .base import AdminObject
|
||||
from .domain import Domain
|
||||
from .domain_alias import DomainAlias
|
||||
@@ -22,4 +23,5 @@ __all__ = [
|
||||
"Quota",
|
||||
"SenderAddress",
|
||||
"validate_alias_address",
|
||||
"modify_or_create_alias",
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -49,6 +56,21 @@ def validate_alias_address(address, creator, internal=False, instance=None):
|
||||
return local_part, domain
|
||||
|
||||
|
||||
def modify_or_create_alias(address, recipients, creator=None, domain=None):
|
||||
"""Add recipient if the alias already exists or create it."""
|
||||
|
||||
alias = Alias.objects.filter(address=address, internal=False)
|
||||
if alias.exists():
|
||||
alias.first().add_recipients(recipients)
|
||||
elif creator is not None and domain is not None:
|
||||
Alias.objects.create(
|
||||
creator=creator,
|
||||
domain=domain,
|
||||
address=address,
|
||||
recipients=recipients
|
||||
)
|
||||
|
||||
|
||||
class AliasManager(models.Manager):
|
||||
"""Custom manager for Alias."""
|
||||
|
||||
@@ -209,12 +231,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:]])
|
||||
|
||||
@@ -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