Added more actions on identities:

* create/edit/delete alias
* edit/delete account
This commit is contained in:
Antoine Nguyen
2021-05-14 15:12:46 +02:00
parent 8fa43f75cf
commit 4e7d660b44
26 changed files with 1099 additions and 459 deletions
+3
View File
@@ -20,5 +20,8 @@ export default {
},
patch (accountId, data) {
return repository.patch(`${resource}/${accountId}/`, data)
},
delete (accountId, data) {
return repository.post(`${resource}/${accountId}/delete/`, data)
}
}
+24
View File
@@ -0,0 +1,24 @@
import repository from './repository'
const resource = 'aliases'
export default {
get (aliasId) {
return repository.get(`${resource}/${aliasId}/`)
},
getRandomAddress () {
return repository.get(`${resource}/random_address/`)
},
validate (data) {
return repository.post(`${resource}/validate/`, data)
},
create (data) {
return repository.post(`${resource}/`, data)
},
patch (aliasId, data) {
return repository.patch(`${resource}/${aliasId}/`, data)
},
delete (aliasId) {
return repository.delete(`${resource}/${aliasId}/`)
}
}
+2 -2
View File
@@ -6,13 +6,13 @@ export default {
getApplications () {
return repository.get(`/${resource}/applications/`)
},
getAppliction (app) {
getApplication (app) {
return repository.get(`/${resource}/${app}/`)
},
getApplicationStructure (app) {
return repository.get(`/${resource}/structure/?app=${app}`)
},
saveApplication (app, data) {
return repository.post(`/${resource}/${app}/`, data)
return repository.put(`/${resource}/${app}/`, data)
}
}
@@ -10,9 +10,11 @@
name="name"
rules="required"
>
<label class="m-label">{{ $gettext('Name') }}</label>
<v-text-field
v-model="form.name"
:label="'Name' | translate"
dense
outlined
:error-messages="errors"
/>
</validation-provider>
@@ -21,14 +23,16 @@
name="target"
rules="required"
>
<label class="m-label">{{ $gettext('Choose a domain') }}</label>
<v-select
v-model="form.target"
:items="domains"
item-text="name"
item-value="pk"
:label="'Choose a domain' | translate"
:error-messages="errors"
single-line
dense
outlined
/>
</validation-provider>
<v-switch
@@ -1,130 +1,31 @@
<template>
<div class="d-flex justify-center inner">
<v-stepper v-model="step">
<v-stepper-header class="align-center px-10">
<v-img
src="../../assets/Modoboa_RVB-BLEU-SANS.png"
max-width="190"
/>
<v-stepper-step :complete="step > 1" step="1">
<translate>General</translate>
</v-stepper-step>
<v-stepper-step :complete="step > 2" step="2">
<translate>DNS</translate>
</v-stepper-step>
<v-stepper-step :complete="step > 3" step="3">
<translate>Limitations</translate>
</v-stepper-step>
<v-stepper-step :complete="step > 4" step="4">
<translate>Options</translate>
</v-stepper-step>
<v-stepper-step step="5">
<translate>Summary</translate>
</v-stepper-step>
<v-btn icon @click="close">
<v-icon color="primary" x-large>mdi-close</v-icon>
</v-btn>
</v-stepper-header>
<v-stepper-items class="mt-4 d-flex justify-center">
<v-stepper-content step="1" class="flex-grow-0">
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">New domain</translate> /
<translate>General</translate>
</div>
<domain-general-form ref="form_1" :domain="domain" />
<div class="d-flex justify-end">
<v-btn
color="primary"
@click="goToNextStep(1, 2)"
large
>
<translate>Next</translate>
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content step="2" class="flex-grow-0">
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">New domain</translate> /
<translate>DNS</translate>
</div>
<domain-dns-form ref="form_2" :domain="domain" />
<div class="d-flex justify-end mt-8">
<v-btn @click="step = 1" class="mr-10" text>
<translate>Back</translate>
</v-btn>
<v-btn
color="primary"
@click="step = 3"
large
>
<translate>Next</translate>
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content step="3" class="flex-grow-0">
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">New domain</translate> /
<translate>Limitations</translate>
</div>
<domain-limitations-form ref="form_3" :domain="domain" />
<div class="d-flex justify-end mt-8">
<v-btn @click="step = 2" class="mr-10" text>
<translate>Back</translate>
</v-btn>
<v-btn
color="primary"
@click="goToNextStep(3, 4)"
large
>
<translate>Next</translate>
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content step="4" class="flex-grow-0">
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">New domain</translate> /
<translate>Options</translate>
</div>
<domain-options-form ref="form_4" :domain="domain" @createAdmin="updateCreateAdmin" />
<div class="d-flex justify-end mt-8">
<v-btn @click="step = 3" class="mr-10" text>
<translate>Back</translate>
</v-btn>
<v-btn
color="primary"
@click="goToNextStep(4, 5)"
large
>
<translate>Next</translate>
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content step="5" class="flex-grow-0">
<div class="text-center text-h3"><translate>Summary</translate></div>
<creation-summary
:sections="summarySections"
@modify-step="val => step = val"
/>
<div class="d-flex justify-center mt-8">
<v-btn
color="primary"
@click="submit"
large
>
<translate>Confirm and create</translate>
</v-btn>
</div>
</v-stepper-content>
</v-stepper-items>
</v-stepper>
<confirm-dialog ref="confirm" />
</div>
<creation-form
:title="'New domain'|translate"
:steps="steps"
:form-observer-getter="getObserver"
:validate-object="validateDomain"
:summary-sections="summarySections"
@close="close"
@create="submit"
>
<template v-slot:form.general="{ step }">
<domain-general-form :ref="`form_${step}`" :domain="domain" />
</template>
<template v-slot:form.dns="{ step }">
<domain-dns-form :ref="`form_${step}`" :domain="domain" />
</template>
<template v-slot:form.limitations="{ step }">
<domain-limitations-form :ref="`form_${step}`" :domain="domain" />
</template>
<template v-slot:form.options="{ step }">
<domain-options-form :ref="`form_${step}`" :domain="domain" @createAdmin="updateCreateAdmin" />
</template>
</creation-form>
</template>
<script>
import { bus } from '@/main'
import ConfirmDialog from '@/components/layout/ConfirmDialog'
import CreationSummary from '@/components/tools/CreationSummary'
import CreationForm from '@/components/tools/CreationForm'
import DomainDNSForm from './DomainDNSForm'
import DomainGeneralForm from './DomainGeneralForm'
import DomainLimitationsForm from './DomainLimitationsForm'
@@ -133,8 +34,7 @@ import DomainOptionsForm from './DomainOptionsForm'
export default {
props: ['value'],
components: {
ConfirmDialog,
CreationSummary,
CreationForm,
'domain-dns-form': DomainDNSForm,
DomainGeneralForm,
DomainLimitationsForm,
@@ -205,7 +105,12 @@ export default {
domain_admin: {}
},
formErrors: {},
step: 1
steps: [
{ name: 'general', title: this.$gettext('General') },
{ name: 'dns', title: this.$gettext('DNS') },
{ name: 'limitations', title: this.$gettext('Limitations') },
{ name: 'options', title: this.$gettext('Options') }
]
}
},
mounted () {
@@ -230,32 +135,15 @@ export default {
}
}
},
async close (withConfirm) {
if (withConfirm) {
const confirm = await this.$refs.confirm.open(
this.$gettext('Warning'),
this.$gettext('If you close this form now, your modifications won\'t be saved. Do you confirm?'),
{
color: 'error'
}
)
if (!confirm) {
return
}
}
this.$emit('close')
close () {
this.initDomain()
this.step = 1
this.$refs.observer_1.reset()
this.$refs.observer_3.reset()
this.$refs.observer_4.reset()
this.$emit('close')
},
async goToNextStep (current, next) {
const valid = await this.$refs[`form_${current}`].$refs.observer.validate()
if (!valid) {
return
}
this.step = next
getObserver (step) {
return this.$refs[`form_${step}`].$refs.observer
},
validateDomain () {
},
updateCreateAdmin (value) {
this.createAdmin = value
@@ -273,33 +161,3 @@ export default {
}
}
</script>
<style lang="scss">
.inner {
background-color: #fff;
}
.v-stepper {
width: 100%;
overflow: auto;
&__content {
width: 60%;
}
&__items {
overflow-y: auto;
}
&__wrapper {
padding: 0 10px;
}
}
.subtitle {
color: #000;
border-bottom: 1px solid #DBDDDF;
}
.edit-link {
text-decoration: none;
}
</style>
@@ -1,143 +1,44 @@
<template>
<div class="d-flex justify-center inner">
<v-stepper v-model="step">
<v-stepper-header class="align-center px-10">
<v-img
src="../../assets/Modoboa_RVB-BLEU-SANS.png"
max-width="190"
/>
<v-stepper-step :complete="step > 1" step="1">
<translate>Role</translate>
</v-stepper-step>
<v-stepper-step :complete="step > 2" step="2">
<translate>Identification</translate>
</v-stepper-step>
<v-stepper-step :complete="step > 3" step="3">
<translate>Mailbox</translate>
</v-stepper-step>
<v-stepper-step :complete="step > 4" step="4">
<translate>Aliases</translate>
</v-stepper-step>
<v-stepper-step step="5">
<translate>Summary</translate>
</v-stepper-step>
<v-btn icon @click="close">
<v-icon color="primary" x-large>mdi-close</v-icon>
</v-btn>
</v-stepper-header>
<v-stepper-items class="mt-4 d-flex justify-center">
<v-stepper-content step="1" class="flex-grow-0">
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">New account</translate> /
<translate>Role</translate>
</div>
<account-role-form ref="form_1" :account="account" />
<div class="d-flex justify-end mt-4">
<v-btn
color="primary"
@click="goToNextStep(1, 2)"
large
>
<translate>Next</translate>
<creation-form
:title="'New account'|translate"
:steps="steps"
:form-observer-getter="getObserver"
:validate-object="validateAccount"
:summary-sections="summarySections"
@close="close"
@create="submit"
>
<template v-slot:form.role="{ step }">
<account-role-form :ref="`form_${step}`" :account="account" />
</template>
<template v-slot:form.identification="{ step }">
<account-general-form v-if="step >= 2" :ref="`form_${step}`" :account="account" />
</template>
<template v-slot:form.mailbox="{ step }">
<account-mailbox-form :ref="`form_${step}`" :account="account" />
</template>
<template v-slot:form.aliases="{ step }">
<account-alias-form v-if="step >= 4" :ref="`form_${step}`" :account="account" />
</template>
<template v-slot:item.random_password="{ item }">
<v-col cols="12" class="highligth white--text">
<v-row>
<v-col><span>{{ item.key }}</span></v-col>
<v-col class="text-right" v-if="item.type === 'yesno'">
{{ item.value|yesno }}
</v-col>
</v-row>
<v-row>
<v-col class="text-right py-1">
{{ account.password }}
<v-btn icon color="white" :title="'Copy to clipboard'|translate" @click="copyPassword">
<v-icon>mdi-clipboard-multiple-outline</v-icon>
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content step="2" class="flex-grow-0">
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">New account</translate> /
<translate>Identification</translate>
</div>
<account-general-form v-if="step >= 2" ref="form_2" :account="account" />
<div class="d-flex justify-end">
<v-btn @click="step = 1" class="mr-10" text>
<translate>Back</translate>
</v-btn>
<v-btn
color="primary"
@click="goToNextStep(2, 3)"
large
>
<translate>Next</translate>
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content step="3" class="flex-grow-0">
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">New account</translate> /
<translate>Mailbox</translate>
</div>
<account-mailbox-form ref="form_3" :account="account" />
<div class="d-flex justify-end">
<v-btn @click="step = 2" class="mr-10" text>
<translate>Back</translate>
</v-btn>
<v-btn
color="primary"
@click="goToNextStep(3, 4)"
large
>
<translate>Next</translate>
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content step="4" class="flex-grow-0">
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">New account</translate> /
<translate>Aliases</translate>
</div>
<account-alias-form v-if="step >= 4" ref="form_4" :account="account" />
<div class="d-flex justify-end">
<v-btn @click="step = 3" class="mr-10" text>
<translate>Back</translate>
</v-btn>
<v-btn
color="primary"
@click="goToNextStep(4, 5)"
large
>
<translate>Next</translate>
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content step="5" class="flex-grow-0">
<div class="text-center text-h3"><translate>Summary</translate></div>
<creation-summary
:sections="summarySections"
@modify-step="val => step = val"
>
<template v-slot:item.random_password="{ item }">
<v-col cols="12" class="highligth white--text">
<v-row>
<v-col><span>{{ item.key }}</span></v-col>
<v-col class="text-right" v-if="item.type === 'yesno'">
{{ item.value|yesno }}
</v-col>
</v-row>
<v-row>
<v-col class="text-right py-1">
{{ account.password }}
<v-btn icon color="white" :title="'Copy to clipboard'|translate" @click="copyPassword">
<v-icon>mdi-clipboard-multiple-outline</v-icon>
</v-btn>
</v-col>
</v-row>
</v-col>
</template>
</creation-summary>
<div class="d-flex justify-center mt-8">
<v-btn
color="primary"
@click="submit"
large
>
<translate>Confirm and create</translate>
</v-btn>
</div>
</v-stepper-content>
</v-stepper-items>
</v-stepper>
<confirm-dialog ref="confirm" />
</div>
</v-col>
</v-row>
</v-col>
</template>
</creation-form>
</template>
<script>
@@ -147,8 +48,7 @@ import AccountAliasForm from './AccountAliasForm'
import AccountGeneralForm from './AccountGeneralForm'
import AccountMailboxForm from './AccountMailboxForm'
import AccountRoleForm from './AccountRoleForm'
import ConfirmDialog from '@/components/layout/ConfirmDialog'
import CreationSummary from '@/components/tools/CreationSummary'
import CreationForm from '@/components/tools/CreationForm'
export default {
components: {
@@ -156,8 +56,7 @@ export default {
AccountGeneralForm,
AccountMailboxForm,
AccountRoleForm,
ConfirmDialog,
CreationSummary
CreationForm
},
computed: {
summarySections () {
@@ -220,7 +119,24 @@ export default {
data () {
return {
account: this.getInitialForm(),
step: 1
steps: [
{
name: 'role',
title: this.$gettext('Role')
},
{
name: 'identification',
title: this.$gettext('Identification')
},
{
name: 'mailbox',
title: this.$gettext('Mailbox')
},
{
name: 'aliases',
title: this.$gettext('Aliases')
}
]
}
},
methods: {
@@ -240,40 +156,15 @@ export default {
random_password: true
}
},
async close (withConfirm) {
if (withConfirm) {
const confirm = await this.$refs.confirm.open(
this.$gettext('Warning'),
this.$gettext('If you close this form now, your modifications won\'t be saved. Do you confirm?'),
{
color: 'error'
}
)
if (!confirm) {
return
}
}
getObserver (step) {
return this.$refs[`form_${step}`].$refs.observer
},
validateAccount () {
return accounts.validate(this.account)
},
close () {
this.account = this.getInitialForm()
this.$emit('close')
this.step = 1
},
async goToNextStep (current, next) {
const valid = await this.$refs[`form_${current}`].$refs.observer.validate()
if (!valid) {
return
}
try {
await accounts.validate(this.account)
} catch (error) {
if (error.response.data.mailbox && error.response.data.mailbox.full_address) {
this.$refs[`form_${current}`].$refs.observer.setErrors({
username: error.response.data.mailbox.full_address
})
}
this.$refs[`form_${current}`].$refs.observer.setErrors(error.response.data)
return
}
this.step = next
},
submit () {
accounts.create(this.account).then(resp => {
@@ -290,33 +181,6 @@ export default {
</script>
<style lang="scss">
.inner {
background-color: #fff;
}
.v-stepper {
width: 100%;
overflow: auto;
&__content {
width: 60%;
}
&__items {
overflow-y: auto;
}
&__wrapper {
padding: 0 10px;
}
}
.subtitle {
color: #000;
border-bottom: 1px solid #DBDDDF;
}
.edit-link {
text-decoration: none;
}
.highligth {
background-color: #515D78;
}
@@ -0,0 +1,112 @@
<template>
<creation-form
:title="'New alias'|translate"
:steps="steps"
:form-observer-getter="getObserver"
:validate-object="validateAlias"
:summary-sections="summarySections"
@close="close"
@create="submit"
>
<template v-slot:form.general="{ step }">
<alias-general-form :ref="`form_${step}`" :alias="alias" />
</template>
<template v-slot:form.recipients="{ step }">
<alias-recipient-form :ref="`form_${step}`" :alias="alias" />
</template>
</creation-form>
</template>
<script>
import { bus } from '@/main'
import aliases from '@/api/aliases'
import AliasGeneralForm from '@/components/identities/AliasGeneralForm'
import AliasRecipientForm from './AliasRecipientForm'
import CreationForm from '@/components/tools/CreationForm'
export default {
components: {
AliasGeneralForm,
AliasRecipientForm,
CreationForm
},
computed: {
summarySections () {
const result = [
{
title: this.$gettext('General'),
items: [
{ key: this.$gettext('Random address'), value: this.alias.random, type: 'yesno' },
{ key: this.$gettext('Address'), value: this.alias.address }
]
}
]
if (this.alias.expire_at !== undefined) {
result[0].items.push({
key: this.$gettext('Expire at'), value: this.alias.expire_at
})
}
if (this.alias.description !== undefined) {
result[0].items.push({
key: this.$gettext('Description'), value: this.alias.description
})
}
result.push({
title: this.$gettext('Recipients'),
items: [
{ key: this.$gettext('Recipients'), value: this.alias.recipients.join(', ') }
]
})
return result
}
},
data () {
return {
alias: this.getInitialForm(),
steps: [
{
name: 'general',
title: this.$gettext('General')
},
{
name: 'recipients',
title: this.$gettext('Recipients')
}
]
}
},
methods: {
close () {
this.alias = this.getInitialForm()
this.$emit('close')
},
getInitialForm () {
return {
random: false,
enabled: true,
recipients: []
}
},
getObserver (step) {
return this.$refs[`form_${step}`].$refs.observer
},
validateAlias () {
const data = { ...this.alias }
if (data.recipients.length === 0) {
delete data.recipients
}
return aliases.validate(data)
},
submit () {
aliases.create(this.alias).then(resp => {
bus.$emit('notification', { msg: this.$gettext('Alias created') })
this.$emit('created')
this.close()
})
}
},
mounted () {
this.$store.dispatch('identities/fetchDomains')
}
}
</script>
@@ -0,0 +1,131 @@
<template>
<div>
<v-expansion-panels v-model="panel">
<v-expansion-panel>
<v-expansion-panel-header v-slot="{ open }">
<v-row no-gutters>
<v-col cols="4">
<translate>General</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%"
>
<v-col cols="6">
<translate>Address: </translate> {{ editedAlias.address }}
</v-col>
<v-col cols="6">
<translate class="mr-2">Enabled</translate>
<v-icon color="success" v-if="editedAlias.enabled">mdi-check-circle-outline</v-icon>
<v-icon v-else>mdi-close-circle-outline</v-icon>
</v-col>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-header>
<v-expansion-panel-content>
<alias-general-form ref="generalForm" :alias="editedAlias" />
</v-expansion-panel-content>
</v-expansion-panel>
<v-expansion-panel>
<v-expansion-panel-header v-slot="{ open }">
<v-row no-gutters>
<v-col cols="4">
<translate>Recipients</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%"
>
<v-chip
v-for="(rcpt, index) in editedAlias.recipients"
:key="index"
small
>
{{ rcpt }}
</v-chip>
</v-row>
</v-fade-transition>
</v-col>
</v-row>
</v-expansion-panel-header>
<v-expansion-panel-content>
<alias-recipient-form ref="recipientForm" :alias="editedAlias" />
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
<div class="mt-4 d-flex justify-end">
<v-btn color="grey lighten-1" @click="$router.go(-1)">
<translate>Cancel</translate>
</v-btn>
<v-btn class="ml-4" color="primary darken-1" @click="save">
<translate>Save</translate>
</v-btn>
</div>
</div>
</template>
<script>
import { bus } from '@/main'
import aliases from '@/api/aliases'
import AliasGeneralForm from './AliasGeneralForm'
import AliasRecipientForm from './AliasRecipientForm'
export default {
components: {
AliasGeneralForm,
AliasRecipientForm
},
props: ['alias'],
data () {
return {
editedAlias: {},
panel: 0
}
},
methods: {
async save () {
if (this.$refs.generalForm !== undefined) {
const valid = await this.$refs.generalForm.$refs.observer.validate()
if (!valid) {
return
}
}
try {
await aliases.patch(this.editedAlias.pk, this.editedAlias).then(resp => {
bus.$emit('notification', { msg: this.$gettext('Alias updated') })
})
} catch (error) {
if (this.$refs.generalForm) {
this.$refs.generalForm.$refs.observer.setErrors(error.response.data)
}
}
}
},
watch: {
alias: {
handler: function (val) {
if (val) {
this.editedAlias = { ...val }
}
},
immediate: true
}
}
}
</script>
@@ -0,0 +1,90 @@
<template>
<validation-observer ref="observer">
<v-switch
v-if="!alias.pk"
v-model="alias.random"
:label="'Random address'|translate"
/>
<validation-provider
v-if="!alias.random"
v-slot="{ errors }"
name="address"
rules="required"
>
<label class="m-label">{{ $gettext('Email address') }}</label>
<email-field
ref="address"
v-model="alias.address"
:error-messages="errors"
type="email"
outlined
dense
/>
</validation-provider>
<validation-provider
v-if="alias.random"
v-slot="{ errors }"
rules="required"
>
<label class="m-label">{{ $gettext('Domain') }}</label>
<v-select
v-model="alias.domain"
:items="domains"
:error-messages="errors"
item-text="name"
return-object
outlined
dense
@change="updateAddress"
/>
</validation-provider>
<v-switch
v-model="alias.enabled"
:label="'Enabled'|translate"
/>
<label class="m-label">{{ $gettext('Expire at') }}</label>
<v-text-field
v-model="alias.expire"
outlined
dense
/>
<label class="m-label">{{ $gettext('Description') }}</label>
<v-textarea
v-model="alias.description"
rows="2"
outlined
dense
/>
</validation-observer>
</template>
<script>
import { mapGetters } from 'vuex'
import aliases from '@/api/aliases'
import EmailField from '@/components/tools/EmailField'
export default {
components: {
EmailField
},
props: ['alias'],
computed: {
...mapGetters({
domains: 'identities/domains'
})
},
methods: {
updateAddress (value) {
aliases.getRandomAddress().then(resp => {
this.$set(this.alias, 'address', `${resp.data.address}@${value.name}`)
})
}
},
watch: {
alias (val) {
console.log(val)
}
}
}
</script>
@@ -0,0 +1,50 @@
<template>
<validation-observer ref="observer">
<validation-provider
vid="recipients"
v-slot="{ errors }"
>
<email-field
ref="recipientField"
v-model="recipient"
:placeholder="'Start typing a name here...'|translate"
@domain-selected="addRecipient"
:hint="'Alias(es) of this mailbox. To create a catchall alias, just enter the domain name (@domain.tld).'|translate"
persistent-hint
allow-add
:error-messages="errors"
/>
</validation-provider>
<v-chip
v-for="(recipient, index) in alias.recipients"
:key="index"
class="mr-2 mt-2"
close
@click:close="alias.recipients.splice(index, 1)"
>
{{ recipient }}
</v-chip>
</validation-observer>
</template>
<script>
import EmailField from '@/components/tools/EmailField'
export default {
props: ['alias'],
components: {
EmailField
},
data () {
return {
recipient: ''
}
},
methods: {
async addRecipient () {
this.alias.recipients.push(this.recipient)
this.$refs.recipientField.reset()
}
}
}
</script>
@@ -54,16 +54,27 @@
</div>
</template>
</v-data-table>
<confirm-dialog ref="confirmAlias" />
<confirm-dialog ref="confirmAccount">
<v-checkbox v-model="keepAccountFolder"
:label="'Do not delete account folder'|translate"
hide-details
/>
</confirm-dialog>
</v-card>
</template>
<script>
import { bus } from '@/main'
import identities from '@/api/identities'
import accounts from '@/api/accounts'
import aliases from '@/api/aliases'
import ConfirmDialog from '@/components/layout/ConfirmDialog'
import MenuItems from '@/components/tools/MenuItems'
export default {
components: {
ConfirmDialog,
MenuItems
},
data () {
@@ -75,23 +86,73 @@ export default {
{ text: this.$gettext('Actions'), value: 'actions', sortable: false, align: 'right' }
],
identities: [],
keepAccountFolder: false,
search: '',
selected: []
}
},
methods: {
async deleteAlias (alias) {
const confirm = await this.$refs.confirmAlias.open(
this.$gettext('Warning'),
this.$gettext(`Do you really want to delete the alias ${alias.identity}?`),
{
color: 'error',
cancelLabel: this.$gettext('No'),
agreeLabel: this.$gettext('Yes')
}
)
if (!confirm) {
return
}
aliases.delete(alias.pk).then(() => {
bus.$emit('notification', { msg: this.$gettext('Alias deleted') })
this.fetchIdentities()
})
},
async deleteAccount (account) {
const confirm = await this.$refs.confirmAccount.open(
this.$gettext('Warning'),
this.$gettext(`Do you really want to delete the account ${account.identity}?`),
{
color: 'error',
cancelLabel: this.$gettext('No'),
agreeLabel: this.$gettext('Yes')
}
)
if (!confirm) {
return
}
const data = { keepdir: this.keepAccountFolder }
accounts.delete(account.pk, data).then(() => {
bus.$emit('notification', { msg: this.$gettext('Account deleted') })
this.keepAccountFolder = false
this.fetchIdentities()
}).catch(error => {
bus.$emit('notification', { msg: error.response.data, type: 'error' })
})
},
editAccount (account) {
this.$router.push({ name: 'AccountEdit', params: { id: account.pk } })
},
editAlias (alias) {
this.$router.push({ name: 'AliasEdit', params: { id: alias.pk } })
},
fetchIdentities () {
identities.getAll().then(resp => {
this.identities = resp.data
})
},
getMenuItems (item) {
return [
{ label: this.$gettext('Edit'), icon: 'mdi-circle-edit-outline', onClick: this.editAccount }
]
const result = []
if (item.type === 'account') {
result.push({ label: this.$gettext('Edit'), icon: 'mdi-circle-edit-outline', onClick: this.editAccount })
result.push({ label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteAccount, color: 'red' })
} else if (item.type === 'alias') {
result.push({ label: this.$gettext('Edit'), icon: 'mdi-circle-edit-outline', onClick: this.editAlias })
result.push({ label: this.$gettext('Delete'), icon: 'mdi-delete-outline', onClick: this.deleteAlias, color: 'red' })
}
return result
}
},
mounted () {
@@ -15,34 +15,39 @@
<v-card>
<v-card-text>
<template v-for="(param, index) in displayableParams(element.parameters)">
<v-switch :key="index"
:label="param.label"
v-model="parameters[param.name]"
:hint="param.help_text"
persistent-hint
class="my-2"
v-if="param.widget === 'BooleanField'"
/>
<v-select :key="index"
:label="param.label"
v-model="parameters[param.name]"
:items="param.choices"
:hint="param.help_text"
persistent-hint
class="my-2"
v-else-if="param.widget === 'ChoiceField'"
/>
<v-text-field
:key="index"
:label="param.label"
:hint="param.help_text"
persistent-hint
v-model="parameters[param.name]"
:error="formErrors[param.name] !== undefined"
:error-messages="formErrors[param.name]"
class="my-2"
v-else
/>
<div class="my-4" :key="index">
<template v-if="param.widget === 'BooleanField'">
<v-switch
v-model="parameters[param.name]"
:label="param.label"
:hint="param.help_text"
persistent-hint
v-if="param.widget === 'BooleanField'"
/>
</template>
<template v-else>
<label class="m-label">{{ param.label }}</label>
<v-select
v-model="parameters[param.name]"
:items="param.choices"
:hint="param.help_text"
persistent-hint
v-if="param.widget === 'ChoiceField'"
dense
outlined
/>
<v-text-field
:hint="param.help_text"
persistent-hint
v-model="parameters[param.name]"
:error="formErrors[param.name] !== undefined"
:error-messages="formErrors[param.name]"
v-else
dense
outlined
/>
</template>
</div>
</template>
</v-card-text>
</v-card>
@@ -64,6 +69,7 @@
</template>
<script>
import { bus } from '@/main'
import parameters from '@/api/parameters'
export default {
@@ -112,9 +118,10 @@ export default {
return params.filter(param => this.display(param))
},
save () {
parameters.saveApplication(this.$route.params.app, this.parameters).then(response => {
}, response => {
this.formErrors = response.data
parameters.saveApplication(this.$route.params.app, this.parameters).then(() => {
bus.$emit('notification', { msg: this.$gettext('Parameters updated') })
}).catch(error => {
this.formErrors = error.response.data
})
}
}
@@ -0,0 +1,158 @@
<template>
<div class="d-flex justify-center inner">
<v-stepper v-model="currentStep">
<v-stepper-header class="align-center px-10">
<v-img
src="../../assets/Modoboa_RVB-BLEU-SANS.png"
max-width="190"
/>
<v-stepper-step
v-for="(step, index) in steps"
:key="index"
:complete="currentStep > index + 1" :step="index + 1">
{{ step.title }}
</v-stepper-step>
<v-stepper-step :step="steps.length + 1">
<translate>Summary</translate>
</v-stepper-step>
<v-btn icon @click="close">
<v-icon color="primary" x-large>mdi-close</v-icon>
</v-btn>
</v-stepper-header>
<v-stepper-items class="mt-4 d-flex justify-center">
<v-stepper-content
v-for="(step, index) in steps"
:key="index"
:step="index + 1"
class="flex-grow-0"
>
<div class="mb-6 text-h5">
<translate class="grey--text text--darken-1">{{ title }}</translate> /
<translate>{{ step.title }}</translate>
</div>
<slot :name="`form.${step.name}`" v-bind:step="index + 1" />
<div class="d-flex justify-end mt-4">
<v-btn v-if="index + 1 > 1" @click="currentStep = index" class="mr-10" text>
<translate>Back</translate>
</v-btn>
<v-btn
color="primary"
@click="goToNextStep(index + 1, index + 2)"
large
>
<translate>Next</translate>
</v-btn>
</div>
</v-stepper-content>
<v-stepper-content :step="steps.length + 1" class="flex-grow-0">
<div class="text-center text-h3"><translate>Summary</translate></div>
<creation-summary
:sections="summarySections"
@modify-step="val => currentStep = val"
>
<template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope">
<slot :name="slot" v-bind="scope" />
</template>
</creation-summary>
<div class="d-flex justify-center mt-8">
<v-btn
color="primary"
@click="$emit('create')"
large
>
<translate>Confirm and create</translate>
</v-btn>
</div>
</v-stepper-content>
</v-stepper-items>
</v-stepper>
<confirm-dialog ref="confirm" />
</div>
</template>
<script>
import ConfirmDialog from '@/components/layout/ConfirmDialog'
import CreationSummary from '@/components/tools/CreationSummary'
export default {
components: {
ConfirmDialog,
CreationSummary
},
props: {
title: String,
steps: Array,
summarySections: Array,
formObserverGetter: Function,
validateObject: Function
},
data () {
return {
currentStep: 1
}
},
methods: {
async close (withConfirm) {
if (withConfirm) {
const confirm = await this.$refs.confirm.open(
this.$gettext('Warning'),
this.$gettext('If you close this form now, your modifications won\'t be saved. Do you confirm?'),
{
color: 'error'
}
)
if (!confirm) {
return
}
}
this.$emit('close')
this.currentStep = 1
this.steps.forEach((item, index) => {
const observer = this.formObserverGetter(index + 1)
if (observer !== undefined) {
observer.reset()
}
})
},
async goToNextStep (current, next) {
const observer = this.formObserverGetter(current)
if (observer !== undefined) {
const valid = await observer.validate()
if (!valid) {
return
}
try {
await this.validateObject()
} catch (error) {
observer.setErrors(error.response.data)
return
}
}
this.currentStep = next
}
}
}
</script>
<style lang="scss">
.inner {
background-color: #fff;
}
.v-stepper {
width: 100%;
overflow: auto;
&__content {
width: 60%;
}
&__items {
overflow-y: auto;
}
&__wrapper {
padding: 0 10px;
}
}
</style>
+23 -3
View File
@@ -4,6 +4,7 @@
ref="input"
v-model="input"
outlined
dense
v-bind="$attrs"
v-on="$listeners"
@input="update"
@@ -31,7 +32,13 @@
import { mapGetters } from 'vuex'
export default {
props: ['value'],
props: {
value: String,
allowAdd: {
type: Boolean,
default: false
}
},
computed: {
...mapGetters({
domains: 'identities/domains'
@@ -75,6 +82,8 @@ export default {
// on enter or tab
if (this.filteredDomains.length > 0) {
this.selectDomain(this.filteredDomains[this.selectionIndex])
} else if (this.allowAdd) {
this.selectDomain()
}
e.preventDefault()
} else if (keyCode === 27) {
@@ -100,7 +109,9 @@ export default {
}
},
selectDomain (domain) {
this.input = this.input.split('@')[0] + '@' + domain.name
if (domain !== undefined) {
this.input = this.input.split('@')[0] + '@' + domain.name
}
this.showMenu = false
this.$emit('input', this.input)
this.$emit('domain-selected')
@@ -108,7 +119,11 @@ export default {
update (value) {
if (this.input.indexOf('@') !== -1) {
this.domainSearch = this.input.split('@')[1]
this.showMenu = true
if (this.allowAdd) {
this.showMenu = this.filteredDomains.length > 0
} else {
this.showMenu = true
}
} else {
this.showMenu = false
}
@@ -132,6 +147,11 @@ export default {
this.$nextTick(() => {
this.updateMinWidthProperty()
})
},
watch: {
value (newValue) {
this.input = newValue
}
}
}
</script>
+8 -1
View File
@@ -57,7 +57,14 @@ const routes = [
requiresAuth: true
}
},
{
path: '/identities/aliases/:id/edit',
name: 'AliasEdit',
component: () => import('../views/identities/AliasEdit.vue'),
meta: {
requiresAuth: true
}
},
{
path: '/parameters/:app',
name: 'ParametersEdit',
@@ -0,0 +1,28 @@
<template>
<div>
<span class="text-h4"><translate>Edit alias</translate></span>
<div class="mt-4" />
<alias-form :alias="alias" />
</div>
</template>
<script>
import aliases from '@/api/aliases'
import AliasForm from '@/components/identities/AliasForm'
export default {
components: {
AliasForm
},
data () {
return {
alias: {}
}
},
mounted () {
aliases.get(this.$route.params.id).then(resp => {
this.alias = resp.data
})
}
}
</script>
+16 -1
View File
@@ -20,7 +20,7 @@
<v-list-item @click="showCreationWizard = true">
<v-list-item-title><translate>Account</translate></v-list-item-title>
</v-list-item>
<v-list-item @click="showAliasForm = true">
<v-list-item @click="showAliasCreationWizard = true">
<v-list-item-title><translate>Alias</translate></v-list-item-title>
</v-list-item>
</v-list>
@@ -37,24 +37,39 @@
>
<account-creation-form @close="closeCreationWizard" @created="updateIdentities" />
</v-dialog>
<v-dialog
v-model="showAliasCreationWizard"
fullscreen
scrollable
transition="dialog-bottom-transition"
persistent
>
<alias-creation-form @close="closeAliasWizard" @created="updateIdentities" />
</v-dialog>
</div>
</template>
<script>
import AccountCreationForm from '@/components/identities/AccountCreationForm'
import AliasCreationForm from '@/components/identities/AliasCreationForm'
import IdentityList from '@/components/identities/IdentityList'
export default {
components: {
AccountCreationForm,
AliasCreationForm,
IdentityList
},
data () {
return {
showAliasCreationWizard: false,
showCreationWizard: false
}
},
methods: {
closeAliasWizard () {
this.showAliasCreationWizard = false
},
closeCreationWizard () {
this.showCreationWizard = false
},
+1 -1
View File
@@ -454,7 +454,7 @@ class AliasSerializer(serializers.ModelSerializer):
def validate_address(self, value):
"""Check domain."""
local_part, self.domain = admin_models.validate_alias_address(
value, self.context["request"].user)
value, self.context["request"].user, instance=self.instance)
return value
def create(self, validated_data):
-1
View File
@@ -184,7 +184,6 @@ class AliasViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
serializer_class = serializers.AliasSerializer
http_method_names = ["get", "post", "put", "delete"]
def get_queryset(self):
"""Filter queryset based on current user."""
+130 -7
View File
@@ -1,13 +1,18 @@
"""Admin API v2 serializers."""
from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _
from django.contrib.auth import password_validation
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.lib import validators, web_utils
from modoboa.parameters import tools as param_tools
from ... import constants, lib, models
@@ -105,15 +110,24 @@ class AdminGlobalParametersSerializer(serializers.Serializer):
enable_mx_checks = serializers.BooleanField(default=True)
valid_mxs = serializers.CharField(allow_blank=True)
domains_must_have_authorized_mx = serializers.BooleanField(default=False)
enable_spf_checks = serializers.BooleanField(default=True)
enable_dkim_checks = serializers.BooleanField(default=True)
enable_dmarc_checks = serializers.BooleanField(default=True)
enable_autoconfig_checks = serializers.BooleanField(default=True)
custom_dns_server = serializers.IPAddressField(allow_blank=True)
enable_dnsbl_checks = serializers.BooleanField(default=True)
dkim_keys_storage_dir = serializers.CharField(allow_blank=True)
dkim_default_key_length = serializers.ChoiceField(
default=2048, choices=constants.DKIM_KEY_LENGTHS)
default_domain_quota = serializers.IntegerField(default=0)
default_domain_message_limit = serializers.IntegerField(
required=False, allow_null=True)
# Mailboxes settings
handle_mailboxes = serializers.BooleanField(default=False)
default_domain_quota = serializers.IntegerField(default=0)
default_mailbox_quota = serializers.IntegerField(default=0)
default_mailbox_message_limit = serializers.IntegerField(
required=False, allow_null=True)
auto_account_removal = serializers.BooleanField(default=False)
auto_create_domain_and_mailbox = serializers.BooleanField(default=True)
create_alias_on_mbox_rename = serializers.BooleanField(default=False)
@@ -147,11 +161,37 @@ class IdentitySerializer(serializers.Serializer):
"""Serializer used for identities."""
pk = serializers.IntegerField()
type = serializers.CharField()
identity = serializers.CharField()
name_or_rcpt = serializers.CharField()
tags = TagSerializer(many=True)
class MailboxSerializer(serializers.ModelSerializer):
"""Base mailbox serializer."""
quota = serializers.CharField(required=False)
class Meta:
model = models.Mailbox
fields = (
"pk", "use_domain_quota", "quota", "message_limit"
)
def validate_quota(self, value):
"""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 WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
"""Add support for aliases and sender addresses."""
@@ -159,6 +199,7 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
child=lib_fields.DRFEmailFieldUTF8(),
required=False
)
mailbox = MailboxSerializer(required=False)
class Meta(v1_serializers.WritableAccountSerializer.Meta):
fields = tuple(
@@ -176,25 +217,107 @@ class WritableAccountSerializer(v1_serializers.WritableAccountSerializer):
aliases.append({"localpart": localpart, "domain": domain})
return aliases
def validate(self, data):
"""Check constraints."""
master_user = data.get("master_user", False)
role = data.get("role")
if master_user and role != "SuperAdmins":
raise serializers.ValidationError({
"master_user": _("Not allowed for this role.")
})
if role == "SimpleUsers":
username = data.get("username")
if username:
try:
validators.UTF8EmailValidator()(username)
except ValidationError as err:
raise ValidationError({"username": err.message})
mailbox = data.get("mailbox")
if mailbox is None:
if not self.instance:
data["mailbox"] = {
"use_domain_quota": True
}
condition = (
not data.get("random_password") and (
data.get("password") or
not self.partial
)
)
if condition:
password = data.get("password")
if password:
try:
password_validation.validate_password(
data["password"], self.instance)
except ValidationError as exc:
raise serializers.ValidationError({
"password": exc.messages[0]})
elif not self.instance:
raise serializers.ValidationError({
"password": _("This field is required.")
})
domain_names = data.get("domains")
if not domain_names:
return data
domains = []
for name in domain_names:
domain = models.Domain.objects.filter(name=name).first()
if domain:
domains.append(domain)
continue
raise serializers.ValidationError({
"domains": _("Local domain {} does not exist").format(name)
})
data["domains"] = domains
return data
def create(self, validated_data):
"""Handle aliases."""
user = self.context["request"].user
"""Create account, mailbox and aliases."""
creator = self.context["request"].user
mailbox_data = validated_data.pop("mailbox", None)
role = validated_data.pop("role")
domains = validated_data.pop("domains", [])
random_password = validated_data.pop("random_password", False)
aliases = validated_data.pop("aliases", None)
user = super().create(validated_data)
user = core_models.User(**validated_data)
if random_password:
password = lib.make_password()
else:
password = validated_data.pop("password")
user.set_password(password)
if "language" not in validated_data:
user.language = settings.LANGUAGE_CODE
user.save(creator=creator)
if mailbox_data:
mailbox_data["full_address"] = user.username
self._create_mailbox(creator, user, mailbox_data)
user.role = role
self.set_permissions(user, domains)
if aliases:
for alias in aliases:
models.Alias.objects.create(
creator=user,
creator=creator,
domain=alias.domain,
address=alias.localpart,
recipients=[validated_data["mailbox"]["full_address"]]
recipients=[user.username]
)
return user
class DeleteAccountSerializer(serializers.Serializer):
"""Serializer used with delete operation."""
keepdir = serializers.BooleanField(default=False)
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")
fields = tuple(
field
for field in v1_serializers.AliasSerializer.Meta.fields
if field != "internal"
) + ("expire_at", "description")
+1 -1
View File
@@ -11,7 +11,7 @@ router.register(r"domains", viewsets.DomainViewSet, basename="domain")
router.register(
r"domainaliases", v1_viewsets.DomainAliasViewSet, basename="domain_alias")
router.register(r"accounts", viewsets.AccountViewSet, basename="account")
router.register(r"aliases", v1_viewsets.AliasViewSet, basename="alias")
router.register(r"aliases", viewsets.AliasViewSet, basename="alias")
router.register(
r"senderaddresses", v1_viewsets.SenderAddressViewSet,
basename="sender_address")
+34 -1
View File
@@ -139,6 +139,8 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
def get_serializer_class(self):
if self.action in ["create", "validate"]:
return serializers.WritableAccountSerializer
if self.action == "delete":
return serializers.DeleteAccountSerializer
return super().get_serializer_class()
def get_queryset(self):
@@ -153,7 +155,7 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
@action(methods=["post"], detail=False)
def validate(self, request, **kwargs):
"""Validate given alias without creating it."""
"""Validate given account without creating it."""
serializer = self.get_serializer(
data=request.data,
context=self.get_serializer_context(),
@@ -167,6 +169,15 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
password = lib.make_password()
return response.Response({"password": password})
@action(methods=["post"], detail=True)
def delete(self, request, **kwargs):
"""Custom delete method that accepts body arguments."""
account = self.get_object()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
account.delete()
return response.Response(status=status.HTTP_204_NO_CONTENT)
class IdentityViewSet(viewsets.ViewSet):
"""Viewset for identities."""
@@ -178,3 +189,25 @@ class IdentityViewSet(viewsets.ViewSet):
serializer = serializers.IdentitySerializer(
lib.get_identities(request.user), many=True)
return response.Response(serializer.data)
class AliasViewSet(v1_viewsets.AliasViewSet):
"""Viewset for Alias."""
serializer_class = serializers.AliasSerializer
@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)
@action(methods=["get"], detail=False)
def random_address(self, request, **kwargs):
return response.Response({
"address": models.Alias.generate_random_address()
})
+54 -7
View File
@@ -302,12 +302,45 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([
"does not use one of the defined addresses."
)
}),
("enable_spf_checks", {
"label": ugettext_lazy("Enable SPF checks"),
"help_text": ugettext_lazy(
"Check if every domain has a valid SPF record"
)
}),
("enable_dkim_checks", {
"label": ugettext_lazy("Enable DKIM checks"),
"help_text": ugettext_lazy(
"Check if every domain with DKIM signin enabled "
"has a valid DNS record"
)
}),
("enable_dmarc_checks", {
"label": ugettext_lazy("Enable DMARC checks"),
"help_text": ugettext_lazy(
"Check if every domain has a valid DMARC record"
)
}),
("enable_autoconfig_checks", {
"label": ugettext_lazy("Enable SPF checks"),
"help_text": ugettext_lazy(
"Check if every domain has a valid records for "
"autoconfiguration"
)
}),
("enable_dnsbl_checks", {
"label": ugettext_lazy("Enable DNSBL checks"),
"help_text": ugettext_lazy(
"Check every domain against major DNSBL providers"
)
}),
("custom_dns_server", {
"label": ugettext_lazy("Custom DNS server"),
"help_text": ugettext_lazy(
"Use a custom DNS server instead of local server "
"configuration"
)
}),
("dkim_keys_storage_dir", {
"label": ugettext_lazy("DKIM keys storage directory"),
"help_text": ugettext_lazy(
@@ -322,6 +355,20 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([
"Default length in bits for newly generated DKIM keys."
)
}),
("default_domain_quota", {
"label": ugettext_lazy("Default domain quota"),
"help_text": ugettext_lazy(
"Default quota (in MB) applied to freshly created domains "
"with no value specified. A value of 0 means no quota."
)
}),
("default_domain_message_limit", {
"label": ugettext_lazy("Default domain sending limit"),
"help_text": ugettext_lazy(
"Number of messages freshly created domains will be "
"allowed to send per day. Leave empty for no limit."
)
}),
])
}),
("mailboxes", {
@@ -334,13 +381,6 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([
" renamed or removed within Modoboa"
)
}),
("default_domain_quota", {
"label": ugettext_lazy("Default domain quota"),
"help_text": ugettext_lazy(
"Default quota (in MB) applied to freshly created domains "
"with no value specified. A value of 0 means no quota."
)
}),
("default_mailbox_quota", {
"label": ugettext_lazy("Default mailbox quota"),
"help_text": ugettext_lazy(
@@ -349,6 +389,13 @@ GLOBAL_PARAMETERS_STRUCT = collections.OrderedDict([
"quota."
)
}),
("default_mailbox_message_limit", {
"label": ugettext_lazy("Default mailbox sending limit"),
"help_text": ugettext_lazy(
"Number of messages freshly created mailboxes will be "
"allowed to send per day. Leave empty for no limit."
)
}),
("auto_account_removal", {
"label": ugettext_lazy("Automatic account removal"),
"help_text": ugettext_lazy(
+2
View File
@@ -111,11 +111,13 @@ def mailbox_deleted_handler(sender, **kwargs):
alias.delete()
models.Quota.objects.filter(username=mb.full_address).delete()
request = lib_signals.get_request()
print(request)
if request:
if not request.localconfig.parameters.get_value(
"handle_mailboxes", raise_exception=False):
return
keepdir = request.POST.get("keepdir", "false") == "true"
print(keepdir)
if keepdir:
return
mb.delete_dir()
+16 -16
View File
@@ -7,7 +7,7 @@ from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.encoding import (
force_bytes, force_str, python_2_unicode_compatible, smart_text
force_bytes, force_str, smart_text
)
from django.utils.translation import ugettext as _, ugettext_lazy
@@ -24,7 +24,7 @@ from .domain import Domain
from .mailbox import Mailbox
def validate_alias_address(address, creator, internal=False):
def validate_alias_address(address, creator, internal=False, instance=None):
"""Check if the given alias address can be created by creator."""
local_part, domain = split_mailbox(address)
domain = Domain.objects.filter(name=domain).first()
@@ -32,18 +32,20 @@ def validate_alias_address(address, creator, internal=False):
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))
if instance and instance.address != address:
if Alias.objects.filter(address=address, internal=internal).exists():
raise ValidationError(_("An alias with this name already exists."))
if instance is None:
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
@@ -116,7 +118,6 @@ class Alias(AdminObject):
@property
def type(self):
"""FIXME: deprecated."""
return "alias"
@property
@@ -223,7 +224,6 @@ class Alias(AdminObject):
reversion.register(Alias)
@python_2_unicode_compatible
class AliasRecipient(models.Model):
"""An alias recipient."""
+4
View File
@@ -150,6 +150,10 @@ class User(AbstractUser):
"""Return detail url for this user."""
return reverse("admin:account_detail", args=[self.pk])
@property
def type(self):
return "account"
@property
def tags(self):
return [{"name": "account", "label": _("account"), "type": "idt"},