mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Started import work
This commit is contained in:
@@ -7,7 +7,7 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||
FROM base as build
|
||||
|
||||
RUN apk add --update openssl librrd python3-dev libffi-dev gcc g++ musl-dev libxml2-dev libxslt-dev \
|
||||
libressl-dev jpeg-dev rrdtool-dev file make gettext \
|
||||
libressl-dev jpeg-dev rrdtool-dev file make gettext freetype-dev \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
RUN python3 -m venv $VIRTUAL_ENV
|
||||
WORKDIR /tmp
|
||||
@@ -18,7 +18,7 @@ RUN pip install -r requirements.txt -r test-requirements.txt
|
||||
|
||||
FROM base as run
|
||||
COPY --from=build $VIRTUAL_ENV $VIRTUAL_ENV
|
||||
RUN apk add --no-cache --update librrd libxslt libjpeg libressl git rrdtool \
|
||||
RUN apk add --no-cache --update librrd libxslt libjpeg libressl git rrdtool libmagic \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
RUN mkdir /code
|
||||
WORKDIR /code
|
||||
|
||||
+2
-2
@@ -47,9 +47,9 @@ translations: ./$(INPUT_DIR)/translations.json
|
||||
# Generate .po files for each available language.
|
||||
@for lang in $(LOCALES); do \
|
||||
export PO_FILE=$(OUTPUT_DIR)/locale/$$lang/LC_MESSAGES/app.po; \
|
||||
echo "msgmerge --update $$PO_FILE $@"; \
|
||||
echo "msgmerge --backup=off --update $$PO_FILE $@"; \
|
||||
mkdir -p $$(dirname $$PO_FILE); \
|
||||
[ -f $$PO_FILE ] && msgmerge --lang=$$lang --update $$PO_FILE $@ || msginit --no-translator --locale=$$lang --input=$@ --output-file=$$PO_FILE; \
|
||||
[ -f $$PO_FILE ] && msgmerge --backup=off --lang=$$lang --update $$PO_FILE $@ || msginit --no-translator --locale=$$lang --input=$@ --output-file=$$PO_FILE; \
|
||||
msgattrib --no-wrap --no-obsolete -o $$PO_FILE $$PO_FILE; \
|
||||
done;
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import repository from '@/api/repository'
|
||||
|
||||
const resource = 'migrations'
|
||||
|
||||
export default {
|
||||
getMigrations (query) {
|
||||
return repository.get(`/${resource}/`, query)
|
||||
},
|
||||
getMigration (slug, query) {
|
||||
return repository.get(`/${resource}/${slug}/`, query)
|
||||
},
|
||||
deleteMigration (slug) {
|
||||
return repository.delete(`/${resource}/${slug}/`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import repository from '@/api/repository'
|
||||
|
||||
const resource = 'email-providers'
|
||||
|
||||
export default {
|
||||
getProviders (params) {
|
||||
return repository.get(`/${resource}/`, { params })
|
||||
},
|
||||
getProvider (providerId) {
|
||||
return repository.get(`/${resource}/${providerId}/`)
|
||||
},
|
||||
createProvider (params) {
|
||||
return repository.post(`/${resource}/`, params)
|
||||
},
|
||||
updateProvider (slug, params) {
|
||||
return repository.put(`/${resource}/${slug}/`, params)
|
||||
},
|
||||
patchProvider (providerId, params) {
|
||||
return repository.patch(`/${resource}/${providerId}/`, params)
|
||||
},
|
||||
deleteProvider (slug) {
|
||||
return repository.delete(`/${resource}/${slug}/`)
|
||||
},
|
||||
checkProvider (params) {
|
||||
return repository.post(`/${resource}/check_connection/`, params)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<v-card class="mt-6">
|
||||
<v-toolbar flat>
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" small>
|
||||
Actions <v-icon right>mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
placeholder="Search"
|
||||
filled
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-toolbar>
|
||||
<v-data-table
|
||||
v-model="selected"
|
||||
:headers="headers"
|
||||
:items="migrations"
|
||||
:search="search"
|
||||
item-key="pk"
|
||||
:options.sync="options"
|
||||
:server-items-length="totalMigrations"
|
||||
:loading="loading"
|
||||
class="elevation-1"
|
||||
show-select
|
||||
>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on">
|
||||
<v-icon>mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<menu-items :items="getMenuItems(item)" :object="item" />
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Migration from '@/api/imap_migration/migrations'
|
||||
import MenuItems from '@/components/tools/MenuItems'
|
||||
import { bus } from '@/main'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuItems
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
migrations: [],
|
||||
headers: [
|
||||
{ text: this.$gettext('Provider'), value: 'provider' },
|
||||
{ text: this.$gettext('Old account'), value: 'old_account' },
|
||||
{ text: this.$gettext('New account'), value: 'new_account' },
|
||||
{ text: this.$gettext('Actions'), value: 'actions', sortable: false, align: 'right' }
|
||||
|
||||
],
|
||||
selected: [],
|
||||
loading: true,
|
||||
search: '',
|
||||
options: {},
|
||||
totalMigrations: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteMigration (migration) {
|
||||
this.$dialog.confirm(
|
||||
this.$gettext('Remove this migration?'), {
|
||||
cancelText: this.$gettext('Cancel'),
|
||||
okText: this.$gettext('Proceed')
|
||||
}
|
||||
).then(dialog => {
|
||||
Migration.deleteMigration(migration.id).then(response => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Provider deleted') })
|
||||
this.migrations = this.migrations.filter(item => {
|
||||
return item.id !== migration.id
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
getMigrations (filter) {
|
||||
this.loading = true
|
||||
let query = {}
|
||||
if (filter !== undefined) {
|
||||
query = { params: { search: filter } }
|
||||
}
|
||||
Migration.getMigrations(query).then(response => {
|
||||
this.migrations = response.data
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
getMenuItems (item) {
|
||||
const result = []
|
||||
result.push({
|
||||
label: this.$gettext('Delete'),
|
||||
icon: 'mdi-delete-outline',
|
||||
onClick: this.deleteMigration,
|
||||
color: 'red'
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getMigrations()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
vid="initialdomain"
|
||||
>
|
||||
<v-text-field
|
||||
ref="initialdomainField"
|
||||
v-model="domain.initialdomain"
|
||||
:error-messages="errors"
|
||||
:placeholder="'provider.domain' | translate"
|
||||
outlined
|
||||
dense
|
||||
@input="update"
|
||||
@keydown="onKeyDown"
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
vid="newd_omain"
|
||||
>
|
||||
<v-text-field
|
||||
ref="newdomainField"
|
||||
v-model="domain.new_domain"
|
||||
:error-messages="errors"
|
||||
:placeholder="'local.domain' | translate"
|
||||
outlined
|
||||
dense
|
||||
@input="update"
|
||||
@keydown="onKeyDown"
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="addDomain"
|
||||
>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-chip
|
||||
v-for="(domain, index) in domains"
|
||||
:key="index"
|
||||
class="mr-2 mt-2"
|
||||
close
|
||||
@click:close="removeDomain(index)"
|
||||
>
|
||||
<template v-if="domain.new_domain">
|
||||
{{domain.name}} --> {{domain.new_domain}}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{domain.name}} --> {{domain.name}}
|
||||
</template>
|
||||
</v-chip>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
data () {
|
||||
return {
|
||||
domains: [],
|
||||
domain: {
|
||||
initialdomain: '',
|
||||
new_domain: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async validateForm () {
|
||||
return await this.$refs.observer.validate()
|
||||
},
|
||||
update () {
|
||||
this.$emit('input', this.domains)
|
||||
},
|
||||
async addDomain () {
|
||||
if (await this.validateForm()) {
|
||||
this.domains.push({
|
||||
name: this.domain.initialdomain,
|
||||
new_domain: this.domain.new_domain
|
||||
})
|
||||
this.$emit('input', this.domains)
|
||||
this.$refs.initialdomainField.reset()
|
||||
this.$refs.newdomainField.reset()
|
||||
}
|
||||
},
|
||||
removeDomain (index) {
|
||||
this.domains.splice(index, 1)
|
||||
this.$emit('input', this.domains)
|
||||
},
|
||||
onKeyDown (e) {
|
||||
const keyCode = e.keyCode
|
||||
if (keyCode === 13 || keyCode === 9) {
|
||||
// on enter or tab
|
||||
this.addDomain()
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
handler: function (newValue) {
|
||||
if (newValue) {
|
||||
this.domains = [...newValue]
|
||||
} else {
|
||||
this.domains = []
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<creation-form
|
||||
:title="'New provider'|translate"
|
||||
:steps="steps"
|
||||
:form-observer-getter="getObserver"
|
||||
:validate-object="validateProvider"
|
||||
:summary-sections="summarySections"
|
||||
@close="close"
|
||||
@create="submit"
|
||||
>
|
||||
<template v-slot:form.general="{ step }">
|
||||
<provider-general-form :ref="`form_${step}`" v-model="provider" />
|
||||
</template>
|
||||
<template v-slot:form.associated="{ step }">
|
||||
<provider-associated-form :ref="`form_${step}`" v-model="provider.domains" />
|
||||
</template>
|
||||
</creation-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { bus } from '@/main'
|
||||
import CreationForm from '@/components/tools/CreationForm'
|
||||
import ProviderAssociatedForm from './ProviderAssociatedForm'
|
||||
import ProviderGeneralForm from './ProviderGeneralForm'
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
components: {
|
||||
CreationForm,
|
||||
'provider-associated-form': ProviderAssociatedForm,
|
||||
ProviderGeneralForm
|
||||
},
|
||||
computed: {
|
||||
formatAssociatedDomains () {
|
||||
let associatedDomains = ''
|
||||
for (const domain of this.provider.domains) {
|
||||
associatedDomains += `${domain.name} => ${domain.new_domain}, `
|
||||
}
|
||||
return associatedDomains
|
||||
},
|
||||
summarySections () {
|
||||
const result = [
|
||||
{
|
||||
title: this.$gettext('General'),
|
||||
items: [
|
||||
{ key: this.$gettext('Name'), value: this.provider.name },
|
||||
{ key: this.$gettext('Address'), value: this.provider.address },
|
||||
{ key: this.$gettext('Port'), value: this.provider.port },
|
||||
{ key: this.$gettext('Secured'), value: this.provider.secured, type: 'yesno' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: this.$gettext('Associated domains'),
|
||||
items: [
|
||||
{ key: this.$gettext('Perform a connection check'), value: this.formatAssociatedDomains }
|
||||
]
|
||||
}
|
||||
]
|
||||
return result
|
||||
},
|
||||
steps () {
|
||||
return [
|
||||
{ name: 'general', title: this.$gettext('General') },
|
||||
{ name: 'associated', title: this.$gettext('Associated domains') }
|
||||
]
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
provider: {
|
||||
domains: []
|
||||
},
|
||||
formErrors: {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.initProvider()
|
||||
},
|
||||
methods: {
|
||||
initProvider () {
|
||||
this.provider = {
|
||||
name: '',
|
||||
address: '',
|
||||
port: 993,
|
||||
secured: true,
|
||||
domains: []
|
||||
}
|
||||
},
|
||||
close () {
|
||||
this.initProvider()
|
||||
this.$emit('close')
|
||||
},
|
||||
getObserver (step) {
|
||||
return this.$refs[`form_${step}`].$refs.observer
|
||||
},
|
||||
validateProvider () {
|
||||
|
||||
},
|
||||
performConnectionCheck () {
|
||||
|
||||
},
|
||||
submit () {
|
||||
const data = JSON.parse(JSON.stringify(this.provider))
|
||||
console.log(this.provider)
|
||||
this.$store.dispatch('providers/createProvider', data).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Provider created') })
|
||||
this.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.highligth {
|
||||
background-color: #515D78;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,138 @@
|
||||
<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>Name: </translate> {{ editedProvider.name }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<provider-general-form ref="generalForm" v-model="editedProvider" />
|
||||
</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>Associated domains</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 v-if="editedProvider.domains.length > 0" cols="6">
|
||||
<translate>Number of associated domains: </translate> {{ editedProvider.domains.length }}
|
||||
</v-col>
|
||||
<v-col v-else cols="6">
|
||||
<translate>No associated domains</translate>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-fade-transition>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<provider-associated-form ref="associatedForm" v-model="editedProvider.domains" />
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
<div class="mt-4 d-flex justify-end">
|
||||
<v-btn @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 providers from '@/api/imap_migration/providers'
|
||||
import ProviderGeneralForm from './ProviderGeneralForm'
|
||||
import ProviderAssociatedForm from './ProviderAssociatedForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProviderGeneralForm,
|
||||
ProviderAssociatedForm
|
||||
},
|
||||
props: ['provider'],
|
||||
data () {
|
||||
return {
|
||||
editedProvider: {},
|
||||
panel: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async save () {
|
||||
if (this.$refs.generalForm !== undefined) {
|
||||
const valid = await this.$refs.generalForm.validateForm()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (this.$refs.associatedForm !== undefined) {
|
||||
const valid = await this.$refs.associatedForm.validateForm()
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(JSON.stringify(this.editedProvider))
|
||||
await providers.patch(this.editedProvider.id, data).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Provider updated') })
|
||||
})
|
||||
} catch (error) {
|
||||
if (this.$refs.generalForm) {
|
||||
this.$refs.generalForm.$refs.observer.setErrors(error.response.data)
|
||||
}
|
||||
if (this.$refs.associatedForm) {
|
||||
this.$refs.associatedForm.$refs.observer.setErrors(error.response.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
provider: {
|
||||
handler: function (val) {
|
||||
if (val) {
|
||||
this.editedProvider = { ...val }
|
||||
if (!this.editedProvider.domains) {
|
||||
this.editedProvider.domains = []
|
||||
}
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,167 @@
|
||||
<template>
|
||||
<validation-observer ref="observer">
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<v-text-field
|
||||
:label="'Provider name' | translate"
|
||||
v-model="form.name"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
@input="update"
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required"
|
||||
>
|
||||
<v-text-field
|
||||
:label="'Address (hostname or IP)' | translate"
|
||||
v-model="form.address"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
@input="update"
|
||||
/>
|
||||
</validation-provider>
|
||||
<validation-provider
|
||||
v-slot="{ errors }"
|
||||
rules="required|portNumber"
|
||||
>
|
||||
<v-text-field
|
||||
:label="'IMAP Port' | translate"
|
||||
v-model="form.port"
|
||||
type="number"
|
||||
:error-messages="errors"
|
||||
outlined
|
||||
@input="update"
|
||||
/>
|
||||
</validation-provider>
|
||||
<v-switch
|
||||
:label="'Secured' | translate"
|
||||
v-model="form.secured"
|
||||
:hint="'Control if the IMAP is secured through SSL/TLS or Start SSL' | translate"
|
||||
persistent-hint
|
||||
@change="update"
|
||||
/>
|
||||
<div class="my-7">
|
||||
<v-btn
|
||||
:loading="loading"
|
||||
color="primary"
|
||||
@click="checkConnection"
|
||||
>
|
||||
<translate>Check connection: </translate>
|
||||
<v-icon
|
||||
v-if="connectionStatus === 0"
|
||||
color="blue-grey-darken-2"
|
||||
>
|
||||
mdi-help-circle-outline
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else-if="connectionStatus === 1"
|
||||
color="success"
|
||||
>
|
||||
mdi-check-all
|
||||
</v-icon>
|
||||
<v-icon
|
||||
v-else
|
||||
color="warning"
|
||||
>
|
||||
mdi-alert-circle-outline
|
||||
</v-icon>
|
||||
<template v-slot:loader>
|
||||
<span class="custom-loader">
|
||||
<v-icon light>mdi-cached</v-icon>
|
||||
</span>
|
||||
</template>
|
||||
</v-btn>
|
||||
</div>
|
||||
</validation-observer>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Provider from '@/api/imap_migration/providers'
|
||||
|
||||
export default {
|
||||
props: ['value'],
|
||||
data () {
|
||||
return {
|
||||
form: {},
|
||||
connectionStatus: 0,
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async checkConnection () {
|
||||
if (await this.validateForm()) {
|
||||
this.loading = true
|
||||
const params = {
|
||||
address: this.form.address,
|
||||
port: this.form.port,
|
||||
secured: this.form.secured
|
||||
}
|
||||
console.error(params)
|
||||
Provider.checkProvider(params).then(response => {
|
||||
this.connectionStatus = 1
|
||||
this.loading = false
|
||||
}).catch(error => {
|
||||
console.log(error)
|
||||
this.loading = false
|
||||
this.connectionStatus = 2
|
||||
})
|
||||
}
|
||||
},
|
||||
async validateForm () {
|
||||
return await this.$refs.observer.validate()
|
||||
},
|
||||
update () {
|
||||
this.connectionStatus = 0
|
||||
console.log(this.form)
|
||||
this.$emit('input', this.form)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.form = { ...this.value }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.custom-loader {
|
||||
animation: loader 1s infinite;
|
||||
display: flex;
|
||||
}
|
||||
@-moz-keyframes loader {
|
||||
from {
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes loader {
|
||||
from {
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@-o-keyframes loader {
|
||||
from {
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes loader {
|
||||
from {
|
||||
transform: rotate(0);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<v-card class="mt-6">
|
||||
<v-toolbar flat>
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn v-bind="attrs" v-on="on" small>
|
||||
Actions <v-icon right>mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list dense>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
placeholder="Search"
|
||||
filled
|
||||
outlined
|
||||
dense
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-toolbar>
|
||||
<v-data-table
|
||||
v-model="selected"
|
||||
:headers="headers"
|
||||
:items="providers"
|
||||
:search="search"
|
||||
item-key="name"
|
||||
single-expand
|
||||
:expanded.sync="expanded"
|
||||
class="elevation-1"
|
||||
show-select
|
||||
@click:row="(item, slot) => slot.expand(!slot.isExpanded)"
|
||||
>
|
||||
<template v-slot:item.secured="{ item }">
|
||||
<v-icon
|
||||
v-if="item.secured === true"
|
||||
color="green"
|
||||
>
|
||||
mdi-lock
|
||||
</v-icon>
|
||||
<v-icon
|
||||
color="red"
|
||||
v-else
|
||||
>
|
||||
mdi-lock-off
|
||||
</v-icon>
|
||||
</template>
|
||||
<template v-slot:item.domains="{ item }">
|
||||
<v-chip
|
||||
v-if="item.domains.length > 0"
|
||||
>
|
||||
{{item.domains.length}} associated domain(s)
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-else
|
||||
>
|
||||
No associated domain
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:expanded-item="{ headers, item }">
|
||||
<td :colspan="headers.length">
|
||||
<v-chip
|
||||
v-for="(domain, index) in item.domains"
|
||||
:key="index"
|
||||
>
|
||||
<template v-if="domain.new_domain">
|
||||
{{domain.name}} --> {{domain.new_domain}}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{domain.name}} --> {{domain.name}}
|
||||
</template>
|
||||
</v-chip>
|
||||
</td>
|
||||
</template>
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-menu offset-y>
|
||||
<template v-slot:activator="{ on, attrs }">
|
||||
<v-btn icon v-bind="attrs" v-on="on">
|
||||
<v-icon>mdi-dots-horizontal</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<menu-items :items="getMenuItems(item)" :object="item" />
|
||||
</v-menu>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { mapGetters } from 'vuex'
|
||||
import { bus } from '@/main'
|
||||
import MenuItems from '@/components/tools/MenuItems'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuItems
|
||||
},
|
||||
computed: mapGetters({
|
||||
providers: 'providers/providers'
|
||||
}),
|
||||
data () {
|
||||
return {
|
||||
headers: [
|
||||
{ text: this.$gettext('Name'), value: 'name' },
|
||||
{ text: this.$gettext('Address'), value: 'address' },
|
||||
{ text: this.$gettext('Port'), value: 'port' },
|
||||
{ text: this.$gettext('Secured'), value: 'secured' },
|
||||
{ text: this.$gettext('Associated domains'), value: 'domains' },
|
||||
{ text: this.$gettext('Actions'), value: 'actions', sortable: false, align: 'right' }
|
||||
],
|
||||
selected: [],
|
||||
search: '',
|
||||
expanded: []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteProvider (provider) {
|
||||
this.$store.dispatch('providers/deleteProvider', { id: provider.id }).then(resp => {
|
||||
bus.$emit('notification', { msg: this.$gettext('Provider deleted') })
|
||||
})
|
||||
},
|
||||
editProvider (provider) {
|
||||
this.$router.push({ name: 'ProviderEdit', params: { id: provider.id } })
|
||||
},
|
||||
loadAssociatedDomains ({ item, value }) {
|
||||
if (!value) {
|
||||
return
|
||||
}
|
||||
this.$set(this.item.domains)
|
||||
},
|
||||
getMenuItems (item) {
|
||||
const result = []
|
||||
result.push({
|
||||
label: this.$gettext('Delete'),
|
||||
icon: 'mdi-delete-outline',
|
||||
onClick: this.deleteProvider,
|
||||
color: 'red'
|
||||
})
|
||||
result.push({
|
||||
label: this.$gettext('Edit'),
|
||||
icon: 'mdi-circle-edit-outline',
|
||||
onClick: this.editProvider
|
||||
})
|
||||
return result
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('providers/getProviders')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -168,6 +168,23 @@ export default {
|
||||
icon: 'mdi-domain',
|
||||
roles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
|
||||
},
|
||||
{
|
||||
icon: 'mdi-email-sync-outline',
|
||||
text: this.$gettext('Migrations'),
|
||||
roles: ['SuperAdmins', 'Resellers'],
|
||||
children: [
|
||||
{
|
||||
text: this.$gettext('Email providers'),
|
||||
to: { name: 'ProvdiersList' },
|
||||
roles: ['SuperAdmins', 'Resellers']
|
||||
},
|
||||
{
|
||||
text: this.$gettext('Migrations'),
|
||||
to: { name: 'MigrationsList' },
|
||||
roles: ['Resellers', 'SuperAdmins']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
text: this.$gettext('Identities'),
|
||||
to: { name: 'Identities' },
|
||||
@@ -261,7 +278,7 @@ export default {
|
||||
created () {
|
||||
parameters.getApplications().then(response => {
|
||||
response.data.forEach(item => {
|
||||
this.mainMenuItems[5].children.push({
|
||||
this.mainMenuItems[6].children.push({
|
||||
text: item.label,
|
||||
to: { name: 'ParametersEdit', params: { app: item.name } }
|
||||
})
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-text-field
|
||||
ref="input"
|
||||
v-model="input"
|
||||
outlined
|
||||
dense
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
@input="update"
|
||||
@keydown="onKeyDown"
|
||||
/>
|
||||
<div class="menu"
|
||||
v-if="showMenu"
|
||||
:style="`width: ${width}px;`"
|
||||
>
|
||||
<v-list class="list">
|
||||
<v-list-item
|
||||
v-for="(domain, index) in filteredDomains"
|
||||
:key="index"
|
||||
@click="selectDomain(domain)"
|
||||
:class="selectionIndex === index ? 'selected' : ''"
|
||||
>
|
||||
<v-list-item-title>{{ domain.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
value: String,
|
||||
allowAdd: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
domains: 'identities/domains'
|
||||
}),
|
||||
filteredDomains () {
|
||||
return this.domains.filter(domain => domain.name.startsWith(this.input))
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
input: this.value,
|
||||
selectionIndex: 0,
|
||||
showMenu: false,
|
||||
width: 500
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onKeyDown (e) {
|
||||
const keyCode = e.keyCode
|
||||
|
||||
if (keyCode === 40 || keyCode === 34) {
|
||||
// on arrow down or page down
|
||||
if (!this.showMenu) {
|
||||
this.showMenu = true
|
||||
} else {
|
||||
this.increaseSelectionIndex()
|
||||
}
|
||||
e.preventDefault()
|
||||
} else if (keyCode === 38 || keyCode === 33) {
|
||||
// on arrow up or page up
|
||||
if (!this.showMenu) {
|
||||
this.showMenu = true
|
||||
} else {
|
||||
this.decreaseSelectionIndex()
|
||||
}
|
||||
e.preventDefault()
|
||||
} else if (keyCode === 13 || keyCode === 9) {
|
||||
// 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) {
|
||||
// on escape
|
||||
if (this.filteredDomains.length > 0) {
|
||||
this.showMenu = false
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
},
|
||||
increaseSelectionIndex () {
|
||||
if (this.selectionIndex >= this.filteredDomains.length - 1) {
|
||||
this.selectionIndex = 0
|
||||
} else {
|
||||
this.selectionIndex += 1
|
||||
}
|
||||
},
|
||||
decreaseSelectionIndex () {
|
||||
if (this.selectionIndex <= 0) {
|
||||
this.selectionIndex = this.filteredDomains.length - 1
|
||||
} else {
|
||||
this.selectionIndex -= 1
|
||||
}
|
||||
},
|
||||
selectDomain (domain) {
|
||||
if (domain !== undefined) {
|
||||
this.input = domain.name
|
||||
}
|
||||
this.showMenu = false
|
||||
this.$emit('input', this.input)
|
||||
this.$emit('domain-selected')
|
||||
},
|
||||
update (value) {
|
||||
if (this.allowAdd) {
|
||||
this.showMenu = this.filteredDomains.length > 0
|
||||
} else {
|
||||
this.showMenu = true
|
||||
}
|
||||
this.$emit('input', this.input)
|
||||
},
|
||||
onResize () {
|
||||
this.updateMinWidthProperty()
|
||||
},
|
||||
updateMinWidthProperty () {
|
||||
this.width = this.$refs.input.$el.clientWidth
|
||||
},
|
||||
reset () {
|
||||
this.input = ''
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// window.addEventListener('resize', this.onResize)
|
||||
},
|
||||
updated () {
|
||||
this.$nextTick(() => {
|
||||
this.updateMinWidthProperty()
|
||||
})
|
||||
},
|
||||
watch: {
|
||||
value (newValue) {
|
||||
this.input = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu {
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
margin-top: -20px;
|
||||
padding: 5px 0;
|
||||
box-shadow: 0 1px 5px 1px rgba(0,0,0,0.3);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
contain: content;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.list {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 1.1em;
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.selected {
|
||||
background-color: cornsilk;
|
||||
}
|
||||
</style>
|
||||
@@ -36,6 +36,15 @@ extend('validPassword', {
|
||||
},
|
||||
message: $gettext('Invalid password')
|
||||
})
|
||||
extend('portNumber', {
|
||||
validate: async (value) => {
|
||||
if (value < 0 || value > 65535) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
message: $gettext('Specify a valid port number')
|
||||
})
|
||||
|
||||
// Register components globally
|
||||
Vue.component('ValidationObserver', ValidationObserver)
|
||||
|
||||
@@ -56,6 +56,33 @@ const routes = [
|
||||
allowedRoles: ['DomainAdmins', 'Resellers', 'SuperAdmins']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/imap_migration/migrations',
|
||||
name: 'MigrationsList',
|
||||
component: () => import('../components/imap_migration/MigrationsList.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
allowedRoles: ['Resellers', 'SuperAdmins']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/imap_migration/providers',
|
||||
name: 'ProvdiersList',
|
||||
component: () => import('../views/imap_migration/Providers.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
allowedRoles: ['Resellers', 'SuperAdmins']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/imap_migration/providers/:id/edit',
|
||||
name: 'ProviderEdit',
|
||||
component: () => import('../views/imap_migration/ProviderEdit.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
allowedRoles: ['Resellers', 'SuperAdmins']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/identities',
|
||||
name: 'Identities',
|
||||
|
||||
@@ -4,6 +4,7 @@ import Vuex from 'vuex'
|
||||
import auth from '@/store/modules/auth'
|
||||
import domains from '@/store/modules/domains'
|
||||
import identities from '@/store/modules/identities'
|
||||
import providers from '@/store/modules/providers'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
@@ -11,7 +12,8 @@ export default new Vuex.Store({
|
||||
modules: {
|
||||
auth,
|
||||
domains,
|
||||
identities
|
||||
identities,
|
||||
providers
|
||||
},
|
||||
strict: process.env.NODE_ENV !== 'production'
|
||||
})
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import Vue from 'vue'
|
||||
import providerApi from '@/api/imap_migration/providers'
|
||||
|
||||
// mutation types
|
||||
export const ADD_PROVIDER = 'ADD_PROVIDER'
|
||||
export const UPDATE_PROVIDER = 'SET_PROVIDER'
|
||||
export const SET_PROVIDERS = 'SET_PROVIDERS'
|
||||
export const DELETE_PROVIDER = 'DELETE_PROVIDER'
|
||||
|
||||
const state = {
|
||||
providersLoaded: false,
|
||||
providers: []
|
||||
}
|
||||
|
||||
const getters = {
|
||||
providers: state => state.providers
|
||||
}
|
||||
|
||||
const actions = {
|
||||
getProviders ({ commit }, params) {
|
||||
return providerApi.getProviders(params).then(response => {
|
||||
commit(SET_PROVIDERS, { providers: response.data })
|
||||
})
|
||||
},
|
||||
createProvider ({ commit }, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
providerApi.createProvider(data).then(response => {
|
||||
commit(ADD_PROVIDER, { provider: response.data })
|
||||
resolve(response)
|
||||
}).catch(error => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
updateProvider ({ commit }, data) {
|
||||
return providerApi.updateProvider(data.id, data).then(response => {
|
||||
commit(UPDATE_PROVIDER, { provider: response.data })
|
||||
})
|
||||
},
|
||||
deleteProvider ({ commit }, data) {
|
||||
return providerApi.deleteProvider(data.id).then(response => {
|
||||
commit(DELETE_PROVIDER, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
[SET_PROVIDERS] (state, { providers }) {
|
||||
state.providersLoaded = true
|
||||
state.providers = providers
|
||||
},
|
||||
[ADD_PROVIDER] (state, { provider }) {
|
||||
state.providers.push(provider)
|
||||
},
|
||||
[UPDATE_PROVIDER] (state, { provider }) {
|
||||
state.providers.forEach(function (item, pos) {
|
||||
if (item.pk === provider.pk) {
|
||||
Vue.set(state.providers, pos, provider)
|
||||
}
|
||||
})
|
||||
},
|
||||
[DELETE_PROVIDER] (state, data) {
|
||||
state.providers = state.providers.filter(item => item.id !== data.id)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="text-h4"><translate>Edit provider</translate></span>
|
||||
<div class="mt-4" />
|
||||
<provider-form :provider="provider" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import providers from '@/api/imap_migration/providers'
|
||||
import ProviderForm from '@/components/imap_migration/ProviderForm'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProviderForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
provider: {}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
providers.getProvider(this.$route.params.id).then(resp => {
|
||||
this.provider = resp.data
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title><translate>Providers</translate></v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
offset-y
|
||||
color="primary"
|
||||
@click="showProviderWizard = true"
|
||||
>
|
||||
<v-icon left>mdi-plus</v-icon>
|
||||
<translate>New</translate>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<providers-list />
|
||||
|
||||
<v-dialog
|
||||
v-model="showProviderWizard"
|
||||
fullscreen
|
||||
scrollable
|
||||
transition="dialog-bottom-transition"
|
||||
>
|
||||
<provider-creation-form @close="showProviderWizard = false" />
|
||||
<template v-slot:help>
|
||||
<ul>
|
||||
<li><em>domain; name; quota; default mailbox quota; enabled</em></li>
|
||||
<li><em>domainalias; name; targeted domain; enabled</em></li>
|
||||
</ul>
|
||||
</template>
|
||||
</v-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ProviderCreationForm from '@/components/imap_migration/ProviderCreationForm'
|
||||
import ProvidersList from '@/components/imap_migration/ProvidersList'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ProviderCreationForm,
|
||||
ProvidersList
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showProviderWizard: false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-toolbar {
|
||||
background-color: #f7f8fa !important;
|
||||
}
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@ MODOBOA_APPS = (
|
||||
'modoboa.maillog',
|
||||
'modoboa.pdfcredentials',
|
||||
'modoboa.dmarc',
|
||||
'modoboa.imap_migration',
|
||||
# Modoboa extensions here.
|
||||
{% for extension in extensions %} '{{ extension }}',
|
||||
{% endfor %}
|
||||
@@ -119,6 +120,7 @@ AUTHENTICATION_BACKENDS = (
|
||||
#'modoboa.lib.authbackends.LDAPBackend',
|
||||
#'modoboa.lib.authbackends.SMTPBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'modoboa.imap_migration.auth_backends.IMAPBackend',
|
||||
)
|
||||
|
||||
# SMTP authentication
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""A plugin to migrate mailboxes using IMAP."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from pkg_resources import get_distribution, DistributionNotFound
|
||||
|
||||
|
||||
try:
|
||||
__version__ = get_distribution(__name__).version
|
||||
except DistributionNotFound:
|
||||
# package is not installed
|
||||
pass
|
||||
|
||||
default_app_config = "modoboa.imap_migration.apps.IMAPMigrationConfig"
|
||||
@@ -0,0 +1,93 @@
|
||||
"""API serializers."""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from modoboa.admin import models as admin_models
|
||||
|
||||
from modoboa.imap_migration import models
|
||||
|
||||
|
||||
class EmailProviderDomainSerializer(serializers.ModelSerializer):
|
||||
"""Serializer class for EmailProviderDomain."""
|
||||
|
||||
id = serializers.IntegerField(required=False)
|
||||
|
||||
class Meta:
|
||||
extra_kwargs = {
|
||||
"name": {
|
||||
"validators": []
|
||||
}
|
||||
}
|
||||
fields = ("id", "name", "new_domain")
|
||||
model = models.EmailProviderDomain
|
||||
|
||||
|
||||
class EmailProviderSerializer(serializers.ModelSerializer):
|
||||
"""Serializer class for EmailProvider."""
|
||||
|
||||
domains = EmailProviderDomainSerializer(many=True, required=False)
|
||||
|
||||
class Meta:
|
||||
fields = "__all__"
|
||||
model = models.EmailProvider
|
||||
|
||||
def create(self, validated_data):
|
||||
"""Create provider and domains."""
|
||||
domains = validated_data.pop("domains", None)
|
||||
validated_data.pop("id", None)
|
||||
provider = models.EmailProvider.objects.create(**validated_data)
|
||||
if domains:
|
||||
to_create = []
|
||||
for domain in domains:
|
||||
to_create.append(models.EmailProviderDomain(
|
||||
provider=provider, **domain))
|
||||
models.EmailProviderDomain.objects.bulk_create(to_create)
|
||||
return provider
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
"""Update provider and domains."""
|
||||
domains = validated_data.pop("domains", [])
|
||||
domain_ids = [domain["id"] for domain in domains if "id" in domain]
|
||||
for key, value in validated_data.items():
|
||||
setattr(instance, key, value)
|
||||
instance.save()
|
||||
for domain in instance.domains.all():
|
||||
if domain.id not in domain_ids:
|
||||
domain.delete()
|
||||
else:
|
||||
for updated_domain in domains:
|
||||
if updated_domain.get("id") != domain.id:
|
||||
continue
|
||||
domain.name = updated_domain["name"]
|
||||
domain.new_domain = updated_domain.get("new_domain")
|
||||
domain.save()
|
||||
domains.remove(updated_domain)
|
||||
break
|
||||
to_create = []
|
||||
for new_domain in domains:
|
||||
to_create.append(models.EmailProviderDomain(
|
||||
name=new_domain["name"],
|
||||
new_domain=new_domain.get("new_domain"),
|
||||
provider=instance
|
||||
))
|
||||
models.EmailProviderDomain.objects.bulk_create(to_create)
|
||||
return instance
|
||||
|
||||
|
||||
class MailboxSerializer(serializers.ModelSerializer):
|
||||
"""Simple mailbox serializer."""
|
||||
|
||||
class Meta:
|
||||
fields = ("id", "full_address")
|
||||
model = admin_models.Mailbox
|
||||
|
||||
|
||||
class MigrationSerializer(serializers.ModelSerializer):
|
||||
"""Serializer class for Migration."""
|
||||
|
||||
mailbox = MailboxSerializer()
|
||||
|
||||
class Meta:
|
||||
depth = 1
|
||||
fields = ("id", "provider", "mailbox", "username")
|
||||
model = models.Migration
|
||||
@@ -0,0 +1,12 @@
|
||||
"""API urls."""
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from . import viewsets
|
||||
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(r"email-providers", viewsets.EmailProviderViewSet)
|
||||
router.register(r"migrations", viewsets.MigrationViewSet)
|
||||
|
||||
urlpatterns = router.urls
|
||||
@@ -0,0 +1,39 @@
|
||||
"""API viewsets."""
|
||||
|
||||
from rest_framework import filters, mixins, permissions, viewsets
|
||||
|
||||
from ... import models
|
||||
|
||||
from . import serializers
|
||||
|
||||
|
||||
class EmailProviderViewSet(viewsets.ModelViewSet):
|
||||
"""ViewSet class for EmailProvider."""
|
||||
|
||||
filter_backends = (filters.SearchFilter, )
|
||||
permission_classes = (
|
||||
permissions.IsAuthenticated,
|
||||
permissions.DjangoModelPermissions
|
||||
)
|
||||
queryset = models.EmailProvider.objects.all().prefetch_related("domains")
|
||||
search_fields = ("name", )
|
||||
serializer_class = serializers.EmailProviderSerializer
|
||||
|
||||
|
||||
class MigrationViewSet(mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
"""ViewSet class for Migration."""
|
||||
|
||||
filter_backends = (filters.SearchFilter, )
|
||||
permission_classes = (
|
||||
permissions.IsAuthenticated,
|
||||
permissions.DjangoModelPermissions
|
||||
)
|
||||
queryset = models.Migration.objects.all().select_related(
|
||||
"provider", "mailbox__domain")
|
||||
search_fields = (
|
||||
"username", "provider__name", "mailbox__address",
|
||||
"mailbox__domain__name")
|
||||
serializer_class = serializers.MigrationSerializer
|
||||
@@ -0,0 +1,30 @@
|
||||
"""APIv2 serializers."""
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class IMAPMigrationSettingsSerializer(serializers.Serializer):
|
||||
"""A serializer for global parameters."""
|
||||
|
||||
# General
|
||||
enabled_imapmigration = serializers.BooleanField(default=True)
|
||||
|
||||
# OfflineIMAP settings
|
||||
max_sync_accounts = serializers.IntegerField(default=1)
|
||||
|
||||
# OfflineIMAP Filter settings
|
||||
create_folders = serializers.BooleanField(default=True)
|
||||
folder_filter_exclude = serializers.CharField(required=False,
|
||||
allow_blank=True,
|
||||
default="")
|
||||
folder_filter_include = serializers.CharField(required=False,
|
||||
allow_blank=True,
|
||||
default="")
|
||||
|
||||
|
||||
class CheckProviderSerializer(serializers.Serializer):
|
||||
"""A serializer for checking connection to IMAP server."""
|
||||
|
||||
address = serializers.CharField()
|
||||
port = serializers.IntegerField(min_value=0, max_value=65535)
|
||||
secured = serializers.BooleanField()
|
||||
@@ -0,0 +1,13 @@
|
||||
"""API urls."""
|
||||
|
||||
from rest_framework import routers
|
||||
|
||||
from ..v1 import viewsets as v1_viewsets
|
||||
from .viewsets import EmailProviderViewSet
|
||||
|
||||
|
||||
router = routers.SimpleRouter()
|
||||
router.register(r"email-providers", EmailProviderViewSet)
|
||||
router.register(r"migrations", v1_viewsets.MigrationViewSet)
|
||||
|
||||
urlpatterns = router.urls
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Viewsets for API V2 for imap_migration."""
|
||||
|
||||
import imaplib
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
from rest_framework import serializers, response
|
||||
from rest_framework.decorators import action
|
||||
|
||||
from modoboa.imap_migration.api.v1 import viewsets as v1_viewsets
|
||||
from .serializers import CheckProviderSerializer
|
||||
|
||||
|
||||
class EmailProviderViewSet(v1_viewsets.EmailProviderViewSet):
|
||||
|
||||
@action(methods=["post"], detail=False)
|
||||
def check_connection(self, request, **kwargs):
|
||||
"""check that provided information allow connection to imap server."""
|
||||
serializer = CheckProviderSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
secured = serializer.data["secured"]
|
||||
address = serializer.data["address"]
|
||||
port = serializer.data["port"]
|
||||
try:
|
||||
if secured:
|
||||
print(address)
|
||||
print(port)
|
||||
imaplib.IMAP4_SSL(address, port)
|
||||
else:
|
||||
imaplib.IMAP4(address, port)
|
||||
except (socket.error, imaplib.IMAP4.error, ssl.SSLError) as error:
|
||||
print(type(error))
|
||||
print(error)
|
||||
return response.Response({'status': 'failed'}, 400)
|
||||
return response.Response(status=200)
|
||||
@@ -0,0 +1,107 @@
|
||||
"""IMAP migration forms."""
|
||||
|
||||
import collections
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from modoboa.lib import form_utils
|
||||
from modoboa.parameters import forms as param_forms
|
||||
|
||||
|
||||
IMAP_MIGRATION_PARAMETERS_STRUCT = collections.OrderedDict([
|
||||
("general", {
|
||||
"label": _("General"),
|
||||
"params": collections.OrderedDict([
|
||||
("enabled_imapmigration", {
|
||||
"label": _("Enable IMAP Migration"),
|
||||
"help_text": _(
|
||||
"Enable IMAP Migration."),
|
||||
})
|
||||
])
|
||||
}),
|
||||
("offlineimap", {
|
||||
"label": _("OfflineIMAP"),
|
||||
"params": collections.OrderedDict([
|
||||
("max_sync_accounts", {
|
||||
"label": _("Concurrent sync jobs"),
|
||||
"help_text": _(
|
||||
"The maximum number of concurrent synchronization jobs")
|
||||
})
|
||||
])
|
||||
}),
|
||||
("offlineimapfilter", {
|
||||
"label": _("OfflineIMAP Filter"),
|
||||
"params": collections.OrderedDict([
|
||||
("create_folders", {
|
||||
"label": _("Create Folders"),
|
||||
"help_text": _(
|
||||
"Allow Creation of missing folders during sync"),
|
||||
}),
|
||||
("folder_filter_exclude", {
|
||||
"label": _("Folder Filter Exclusions"),
|
||||
"help_text": _(
|
||||
"Use a regular expression to explicitly include folders in sync. "
|
||||
"Example: ^Trash$|Del")
|
||||
}),
|
||||
("folder_filter_include", {
|
||||
"label": _("Folder Filter Inclusions"),
|
||||
"help_text": _(
|
||||
"A comma seperated list of folders to explicitly include in sync "
|
||||
"even if filtered by the Folder Filter Exclusions. Example: "
|
||||
"debian.user, debian.personal ")
|
||||
})
|
||||
])
|
||||
})
|
||||
])
|
||||
|
||||
|
||||
class ParametersForm(param_forms.AdminParametersForm):
|
||||
"""IMAP migration settings."""
|
||||
|
||||
app = "imap_migration"
|
||||
|
||||
sep1 = form_utils.SeparatorField(label=_("General"))
|
||||
|
||||
enabled_imapmigration = form_utils.YesNoField(
|
||||
label=_("Enable IMAP Migration"),
|
||||
initial=True,
|
||||
help_text=_("Enable IMAP Migration.")
|
||||
)
|
||||
|
||||
sep2 = form_utils.SeparatorField(label=_("OfflineIMAP settings"))
|
||||
|
||||
max_sync_accounts = forms.IntegerField(
|
||||
label=_("Concurrent sync jobs"),
|
||||
initial=1,
|
||||
help_text=_("The maximum number of concurrent synchronization jobs")
|
||||
)
|
||||
|
||||
sep3 = form_utils.SeparatorField(label=_("OfflineIMAP Filter settings"))
|
||||
|
||||
create_folders = form_utils.YesNoField(
|
||||
label=_("Create Folders"),
|
||||
initial=True,
|
||||
help_text=_("Allow Creation of missing folders during sync")
|
||||
)
|
||||
|
||||
folder_filter_exclude = forms.CharField(
|
||||
required=False,
|
||||
label=_("Folder Filter Exclusions"),
|
||||
initial="",
|
||||
help_text=_(
|
||||
"Use a regular expression to explicitly include folders in sync. "
|
||||
"Example: ^Trash$|Del"
|
||||
)
|
||||
)
|
||||
|
||||
folder_filter_include = forms.CharField(
|
||||
required=False,
|
||||
label=_("Folder Filter Inclusions"),
|
||||
initial="",
|
||||
help_text=_(
|
||||
"A comma seperated list of folders to explicitly include in sync "
|
||||
"even if filtered by the Folder Filter Exclusions. Example: "
|
||||
"debian.user, debian.personal "
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""AppConfig for IMAP migration."""
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy
|
||||
|
||||
|
||||
def load_imapmigration_settings():
|
||||
"""Load core settings.
|
||||
|
||||
This function must be manually called (see :file:`urls.py`) in
|
||||
order to load base settings.
|
||||
"""
|
||||
from modoboa.parameters import tools as param_tools
|
||||
from . import app_settings
|
||||
from .api.v2 import serializers
|
||||
|
||||
param_tools.registry.add(
|
||||
"global", app_settings.ParametersForm, ugettext_lazy("IMAP Migration"))
|
||||
param_tools.registry.add2(
|
||||
"global", "imap_migration", ugettext_lazy("IMAP Migration"),
|
||||
app_settings.IMAP_MIGRATION_PARAMETERS_STRUCT,
|
||||
serializers.IMAPMigrationSettingsSerializer)
|
||||
|
||||
|
||||
class IMAPMigrationConfig(AppConfig):
|
||||
"""App configuration."""
|
||||
|
||||
name = "modoboa.imap_migration"
|
||||
verbose_name = "Migration through IMAP for Modoboa"
|
||||
|
||||
def ready(self):
|
||||
from . import checks # noqa
|
||||
from . import handlers # noqa
|
||||
|
||||
load_imapmigration_settings()
|
||||
@@ -0,0 +1,105 @@
|
||||
"""IMAP authentication backend for Django."""
|
||||
|
||||
import imaplib
|
||||
import socket
|
||||
import ssl
|
||||
|
||||
from django.utils.encoding import smart_bytes, smart_str
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.core import models as core_models
|
||||
from modoboa.lib import email_utils
|
||||
from modoboa.lib.exceptions import ModoboaException
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class IMAPBackend:
|
||||
"""IMAP authentication backend."""
|
||||
|
||||
def authenticate(self, request, username=None, password=None):
|
||||
"""Check the username/password and return a User."""
|
||||
if not param_tools.get_global_parameter("enabled_imapmigration"):
|
||||
return None
|
||||
condition = not param_tools.get_global_parameter(
|
||||
"auto_create_domain_and_mailbox", "admin")
|
||||
if condition:
|
||||
# Automatic domain/mailbox is disabled. Deny auth to
|
||||
# prevent further issues...
|
||||
return None
|
||||
self.address, domain = email_utils.split_mailbox(username)
|
||||
provider_domain = models.EmailProviderDomain.objects.filter(
|
||||
name=domain).select_related("provider").first()
|
||||
if not provider_domain:
|
||||
# Domain not allowed for migration: failure
|
||||
return None
|
||||
address = provider_domain.provider.address
|
||||
port = provider_domain.provider.port
|
||||
try:
|
||||
if provider_domain.provider.secured:
|
||||
conn = imaplib.IMAP4_SSL(address, port)
|
||||
else:
|
||||
conn = imaplib.IMAP4(address, port)
|
||||
except (socket.error, imaplib.IMAP4.error, ssl.SSLError) as error:
|
||||
raise ModoboaException(
|
||||
_("Connection to IMAP server failed: %s") % error)
|
||||
|
||||
try:
|
||||
typ, data = conn.login(
|
||||
smart_bytes(username), smart_str(password))
|
||||
except imaplib.IMAP4.error:
|
||||
typ = "NO"
|
||||
conn.logout()
|
||||
if typ != "OK":
|
||||
return None
|
||||
self.provider_domain = provider_domain
|
||||
return self.get_or_create_user(username, password)
|
||||
|
||||
def get_or_create_user(self, username, password):
|
||||
"""Get a user or create it the first time.
|
||||
|
||||
.. note::
|
||||
|
||||
We assume the username is a valid email address.
|
||||
"""
|
||||
orig_username = username
|
||||
# Check if old addresses must be converted
|
||||
if self.provider_domain.new_domain:
|
||||
username = u"{}@{}".format(
|
||||
self.address, self.provider_domain.new_domain.name)
|
||||
user, created = core_models.User.objects.get_or_create(
|
||||
username__iexact=username, defaults={
|
||||
"username": username.lower(), "email": username.lower()
|
||||
}
|
||||
)
|
||||
if created:
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
core_models.populate_callback(user)
|
||||
models.Migration.objects.create(
|
||||
provider=self.provider_domain.provider,
|
||||
mailbox=user.mailbox,
|
||||
username=orig_username,
|
||||
password=password
|
||||
)
|
||||
else:
|
||||
# What happens if an account already exists?
|
||||
if not hasattr(user, "mailbox"):
|
||||
# No mailbox => might be an admin account
|
||||
return None
|
||||
qset = models.Migration.objects.filter(mailbox=user.mailbox)
|
||||
if not qset.exists():
|
||||
# No migration => either someone else account, or
|
||||
# migration is done
|
||||
return None
|
||||
return user
|
||||
|
||||
def get_user(self, user_pk):
|
||||
"""Retrieve a User instance."""
|
||||
user = None
|
||||
try:
|
||||
user = core_models.User.objects.get(pk=user_pk)
|
||||
except core_models.User.DoesNotExist:
|
||||
pass
|
||||
return user
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Deployment checks."""
|
||||
|
||||
from django.core.checks import Warning, register
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.core import models as core_models
|
||||
|
||||
|
||||
W001 = Warning(
|
||||
_("Automatic domain/mailbox creation is disabled which means IMAP "
|
||||
"authentication won't work."),
|
||||
hint=_("Go to the online parameters panel (admin tab) and activate "
|
||||
"this feature."),
|
||||
id="modoboa-imap-migration.W001"
|
||||
)
|
||||
|
||||
|
||||
@register(deploy=True)
|
||||
def check_auto_creation_is_enabled(app_configs, **kwargs):
|
||||
"""Ensure automatic domain/mailbox creation is on."""
|
||||
lc = core_models.LocalConfig.objects.first()
|
||||
if not lc:
|
||||
return []
|
||||
errors = []
|
||||
if not lc.parameters.get_value(
|
||||
"auto_create_domain_and_mailbox", app="admin"):
|
||||
errors.append(W001)
|
||||
return errors
|
||||
@@ -0,0 +1,37 @@
|
||||
"""IMAP migration factories."""
|
||||
|
||||
import factory
|
||||
|
||||
from modoboa.admin import factories as admin_factories
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
class EmailProviderFactory(factory.django.DjangoModelFactory):
|
||||
"""Factory for EmailProvider."""
|
||||
|
||||
class Meta:
|
||||
model = models.EmailProvider
|
||||
|
||||
name = factory.Sequence(lambda n: "Provider {}".format(n))
|
||||
address = "127.0.0.1"
|
||||
port = 143
|
||||
|
||||
|
||||
class EmailProviderDomainFactory(factory.django.DjangoModelFactory):
|
||||
"""Factory for EmailProviderDomain."""
|
||||
|
||||
class Meta:
|
||||
model = models.EmailProviderDomain
|
||||
|
||||
provider = factory.SubFactory(EmailProviderFactory)
|
||||
|
||||
|
||||
class MigrationFactory(factory.django.DjangoModelFactory):
|
||||
"""Factory for Migration."""
|
||||
|
||||
class Meta:
|
||||
model = models.Migration
|
||||
|
||||
mailbox = factory.SubFactory(admin_factories.MailboxFactory)
|
||||
provider = factory.SubFactory(EmailProviderFactory)
|
||||
@@ -0,0 +1,19 @@
|
||||
"""IMAP migration handlers."""
|
||||
|
||||
from django.urls import reverse
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from modoboa.core import signals as core_signals
|
||||
|
||||
|
||||
@receiver(core_signals.extra_user_menu_entries)
|
||||
def menu(sender, location, user, **kwargs):
|
||||
"""Return extra menu entry."""
|
||||
if location != "top_menu" or not user.is_superuser:
|
||||
return []
|
||||
return [
|
||||
{"name": "imap_migrations",
|
||||
"label": _("IMAP migrations"),
|
||||
"url": reverse("imap_migration:index")}
|
||||
]
|
||||
@@ -0,0 +1,134 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Jan Laufik, 2016
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-03-01 17:42+0100\n"
|
||||
"PO-Revision-Date: 2016-11-28 11:43+0000\n"
|
||||
"Last-Translator: Jan Laufik\n"
|
||||
"Language-Team: Czech (Czech Republic) (http://www.transifex.com/tonio/"
|
||||
"modoboa/language/cs_CZ/)\n"
|
||||
"Language: cs_CZ\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
|
||||
|
||||
#: auth_backends.py:45
|
||||
#, python-format
|
||||
msgid "Connection to IMAP server failed: %s"
|
||||
msgstr "Spojení se serverem IMAP selhalo: %s"
|
||||
|
||||
#: checks.py:11
|
||||
msgid ""
|
||||
"Automatic domain/mailbox creation is disabled which means IMAP "
|
||||
"authentication won't work."
|
||||
msgstr ""
|
||||
|
||||
#: checks.py:13
|
||||
msgid ""
|
||||
"Go to the online parameters panel (admin tab) and activate this feature."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:15
|
||||
msgid "OfflineIMAP settings"
|
||||
msgstr "Nastavení OfflineIMAP"
|
||||
|
||||
#: forms.py:18
|
||||
msgid "Concurrent sync jobs"
|
||||
msgstr "Souběžné synchronizační úkoly"
|
||||
|
||||
#: forms.py:20
|
||||
msgid "The maximum number of concurrent synchronization jobs"
|
||||
msgstr "Maximální počet souběžných synchronizačních úkolů"
|
||||
|
||||
#: forms.py:23
|
||||
#, fuzzy
|
||||
#| msgid "OfflineIMAP settings"
|
||||
msgid "OfflineIMAP Filter settings"
|
||||
msgstr "Nastavení OfflineIMAP"
|
||||
|
||||
#: forms.py:26
|
||||
msgid "Create Folders"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:28
|
||||
msgid "Allow Creation of missing folders during sync"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:33
|
||||
msgid "Folder Filter Exclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:36
|
||||
msgid ""
|
||||
"Use a regular expression to explicitly include folders in sync. Example: "
|
||||
"^Trash$|Del"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:43
|
||||
msgid "Folder Filter Inclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:46
|
||||
msgid ""
|
||||
"A comma seperated list of folders to explicitly include in sync even if "
|
||||
"filtered by the Folder Filter Exclusions. Example: debian.user, debian."
|
||||
"personal "
|
||||
msgstr ""
|
||||
|
||||
#: handlers.py:17
|
||||
#, fuzzy
|
||||
#| msgid "IMAP migration"
|
||||
msgid "IMAP migrations"
|
||||
msgstr "IMAP migrace"
|
||||
|
||||
#: modo_extension.py:16
|
||||
msgid "IMAP migration using OfflineIMAP"
|
||||
msgstr "IMAP migrace pomocí OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:19
|
||||
msgid "Migrate existing mailboxes using IMAP and OfflineIMAP"
|
||||
msgstr "Migrace existujících schránek pomocí IMAP protokolu a OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:25 templates/modoboa.imap_migration/index.html:6
|
||||
msgid "IMAP migration"
|
||||
msgstr "IMAP migrace"
|
||||
|
||||
#~ msgid "Connection settings"
|
||||
#~ msgstr "Konfigurace připojení"
|
||||
|
||||
#~ msgid "Server address"
|
||||
#~ msgstr "Adresa serveru"
|
||||
|
||||
#~ msgid "Address of your IMAP server"
|
||||
#~ msgstr "Adresa vašeho IMAP serveru"
|
||||
|
||||
#~ msgid "Use a secured connection"
|
||||
#~ msgstr "Použít zabezpečené připojení"
|
||||
|
||||
#~ msgid "Use a secured connection to access IMAP server"
|
||||
#~ msgstr "Použít zabezpečené připojení pro přístup k IMAP serveru"
|
||||
|
||||
#~ msgid "Server port"
|
||||
#~ msgstr "Port serveru"
|
||||
|
||||
#~ msgid "Listening port of your IMAP server"
|
||||
#~ msgstr "Port, na kterém naslouchá váš IMAP server"
|
||||
|
||||
#~ msgid "Cancel this account's migration"
|
||||
#~ msgstr "Zrušit migraci tohoto účtu"
|
||||
|
||||
#~ msgid "Cancel the migration for %s?"
|
||||
#~ msgstr "Zrušit migraci pro %s?"
|
||||
|
||||
#~ msgid "Unknow migration"
|
||||
#~ msgstr "Nenznámá migrace"
|
||||
|
||||
#~ msgid "Migration cancelled successfully"
|
||||
#~ msgstr "Migrace úspěšně zrušena"
|
||||
@@ -0,0 +1,133 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# controlc.de, 2015
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-03-01 17:42+0100\n"
|
||||
"PO-Revision-Date: 2016-11-09 16:13+0000\n"
|
||||
"Last-Translator: controlc.de\n"
|
||||
"Language-Team: German (http://www.transifex.com/tonio/modoboa/language/de/)\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: auth_backends.py:45
|
||||
#, python-format
|
||||
msgid "Connection to IMAP server failed: %s"
|
||||
msgstr "Verbindung zu IMAP-Server fehlgeschlagen: %s"
|
||||
|
||||
#: checks.py:11
|
||||
msgid ""
|
||||
"Automatic domain/mailbox creation is disabled which means IMAP "
|
||||
"authentication won't work."
|
||||
msgstr ""
|
||||
|
||||
#: checks.py:13
|
||||
msgid ""
|
||||
"Go to the online parameters panel (admin tab) and activate this feature."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:15
|
||||
msgid "OfflineIMAP settings"
|
||||
msgstr "OfflineIMAP-Einstellungen"
|
||||
|
||||
#: forms.py:18
|
||||
msgid "Concurrent sync jobs"
|
||||
msgstr "Gleichzeitige Synchronisierungsaufgaben"
|
||||
|
||||
#: forms.py:20
|
||||
msgid "The maximum number of concurrent synchronization jobs"
|
||||
msgstr "Die maximale Anzahl von gleichzeitigen Synchronisierungsaufgaben"
|
||||
|
||||
#: forms.py:23
|
||||
#, fuzzy
|
||||
#| msgid "OfflineIMAP settings"
|
||||
msgid "OfflineIMAP Filter settings"
|
||||
msgstr "OfflineIMAP-Einstellungen"
|
||||
|
||||
#: forms.py:26
|
||||
msgid "Create Folders"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:28
|
||||
msgid "Allow Creation of missing folders during sync"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:33
|
||||
msgid "Folder Filter Exclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:36
|
||||
msgid ""
|
||||
"Use a regular expression to explicitly include folders in sync. Example: "
|
||||
"^Trash$|Del"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:43
|
||||
msgid "Folder Filter Inclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:46
|
||||
msgid ""
|
||||
"A comma seperated list of folders to explicitly include in sync even if "
|
||||
"filtered by the Folder Filter Exclusions. Example: debian.user, debian."
|
||||
"personal "
|
||||
msgstr ""
|
||||
|
||||
#: handlers.py:17
|
||||
#, fuzzy
|
||||
#| msgid "IMAP migration"
|
||||
msgid "IMAP migrations"
|
||||
msgstr "IMAP-Migration"
|
||||
|
||||
#: modo_extension.py:16
|
||||
msgid "IMAP migration using OfflineIMAP"
|
||||
msgstr "IMAP-Migration benutzt OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:19
|
||||
msgid "Migrate existing mailboxes using IMAP and OfflineIMAP"
|
||||
msgstr "Migration existierender Postfächer benutzt IMAP und OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:25 templates/modoboa_imap_migration/index.html:6
|
||||
msgid "IMAP migration"
|
||||
msgstr "IMAP-Migration"
|
||||
|
||||
#~ msgid "Connection settings"
|
||||
#~ msgstr "Verbindungseinstellungen"
|
||||
|
||||
#~ msgid "Server address"
|
||||
#~ msgstr "Server-Adresse"
|
||||
|
||||
#~ msgid "Address of your IMAP server"
|
||||
#~ msgstr "Addresse Ihres IMAP-Servers"
|
||||
|
||||
#~ msgid "Use a secured connection"
|
||||
#~ msgstr "Gesicherte Verbindung verwenden"
|
||||
|
||||
#~ msgid "Use a secured connection to access IMAP server"
|
||||
#~ msgstr "Gesicherte Verbindung für Zugriff auf IMAP-Server verwenden"
|
||||
|
||||
#~ msgid "Server port"
|
||||
#~ msgstr "Server-Port"
|
||||
|
||||
#~ msgid "Listening port of your IMAP server"
|
||||
#~ msgstr "Port auf dem der IMAP-Server lauscht"
|
||||
|
||||
#~ msgid "Cancel this account's migration"
|
||||
#~ msgstr "Diese Konten-Migration abbrechen"
|
||||
|
||||
#~ msgid "Cancel the migration for %s?"
|
||||
#~ msgstr "Migration für %s abbrechen?"
|
||||
|
||||
#~ msgid "Unknow migration"
|
||||
#~ msgstr "Unbekannte Migration"
|
||||
|
||||
#~ msgid "Migration cancelled successfully"
|
||||
#~ msgstr "Abbruch der Migration war erfolgreich"
|
||||
@@ -0,0 +1,134 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Giannis Kapetanakis <bilias@edu.physics.uoc.gr>, 2016
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-03-01 17:42+0100\n"
|
||||
"PO-Revision-Date: 2016-11-09 16:13+0000\n"
|
||||
"Last-Translator: Giannis Kapetanakis <bilias@edu.physics.uoc.gr>\n"
|
||||
"Language-Team: Greek (Greece) (http://www.transifex.com/tonio/modoboa/"
|
||||
"language/el_GR/)\n"
|
||||
"Language: el_GR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: auth_backends.py:45
|
||||
#, python-format
|
||||
msgid "Connection to IMAP server failed: %s"
|
||||
msgstr "Παρουσιάστηκε σφάλμα στη σύνδεση με τον εξυπηρετητή IMAP: %s"
|
||||
|
||||
#: checks.py:11
|
||||
msgid ""
|
||||
"Automatic domain/mailbox creation is disabled which means IMAP "
|
||||
"authentication won't work."
|
||||
msgstr ""
|
||||
|
||||
#: checks.py:13
|
||||
msgid ""
|
||||
"Go to the online parameters panel (admin tab) and activate this feature."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:15
|
||||
msgid "OfflineIMAP settings"
|
||||
msgstr "Ρυθμίσεις OfflineIMAP"
|
||||
|
||||
#: forms.py:18
|
||||
msgid "Concurrent sync jobs"
|
||||
msgstr "Ταυτόχρονες εργασίες συγχρονισμού"
|
||||
|
||||
#: forms.py:20
|
||||
msgid "The maximum number of concurrent synchronization jobs"
|
||||
msgstr "Ο μέγιστος αριθμός ταυτόχρονων εργασιών συγχρονισμού"
|
||||
|
||||
#: forms.py:23
|
||||
#, fuzzy
|
||||
#| msgid "OfflineIMAP settings"
|
||||
msgid "OfflineIMAP Filter settings"
|
||||
msgstr "Ρυθμίσεις OfflineIMAP"
|
||||
|
||||
#: forms.py:26
|
||||
msgid "Create Folders"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:28
|
||||
msgid "Allow Creation of missing folders during sync"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:33
|
||||
msgid "Folder Filter Exclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:36
|
||||
msgid ""
|
||||
"Use a regular expression to explicitly include folders in sync. Example: "
|
||||
"^Trash$|Del"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:43
|
||||
msgid "Folder Filter Inclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:46
|
||||
msgid ""
|
||||
"A comma seperated list of folders to explicitly include in sync even if "
|
||||
"filtered by the Folder Filter Exclusions. Example: debian.user, debian."
|
||||
"personal "
|
||||
msgstr ""
|
||||
|
||||
#: handlers.py:17
|
||||
#, fuzzy
|
||||
#| msgid "IMAP migration"
|
||||
msgid "IMAP migrations"
|
||||
msgstr "Μετάβαση IMAP"
|
||||
|
||||
#: modo_extension.py:16
|
||||
msgid "IMAP migration using OfflineIMAP"
|
||||
msgstr "Μετάβαση IMAP με χρήση του OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:19
|
||||
msgid "Migrate existing mailboxes using IMAP and OfflineIMAP"
|
||||
msgstr "Μετάβαση υπαρχόντων γραμματοκιβωτίων με χρήση IMAP και του OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:25 templates/modoboa_imap_migration/index.html:6
|
||||
msgid "IMAP migration"
|
||||
msgstr "Μετάβαση IMAP"
|
||||
|
||||
#~ msgid "Connection settings"
|
||||
#~ msgstr "Ρυθμίσεις σύνδεσης"
|
||||
|
||||
#~ msgid "Server address"
|
||||
#~ msgstr "Διεύθυνση εξυπηρετητή"
|
||||
|
||||
#~ msgid "Address of your IMAP server"
|
||||
#~ msgstr "Η διεύθυνση του εξυπηρετητή IMAP"
|
||||
|
||||
#~ msgid "Use a secured connection"
|
||||
#~ msgstr "Χρήση ασφαλής σύνδεσης"
|
||||
|
||||
#~ msgid "Use a secured connection to access IMAP server"
|
||||
#~ msgstr "Χρήση ασφαλής σύνδεσης με τον εξυπηρετητή IMAP"
|
||||
|
||||
#~ msgid "Server port"
|
||||
#~ msgstr "Πόρτα εξυπηρετητή"
|
||||
|
||||
#~ msgid "Listening port of your IMAP server"
|
||||
#~ msgstr "Η πόρτα λειτουργίας του εξυπηρετητή IMAP"
|
||||
|
||||
#~ msgid "Cancel this account's migration"
|
||||
#~ msgstr "Ακύρωση της μετάβασης αυτού του λογαριασμού"
|
||||
|
||||
#~ msgid "Cancel the migration for %s?"
|
||||
#~ msgstr "Ακύρωση της μετάβασης του %s;"
|
||||
|
||||
#~ msgid "Unknow migration"
|
||||
#~ msgstr "Άγνωστη μετάβαση"
|
||||
|
||||
#~ msgid "Migration cancelled successfully"
|
||||
#~ msgstr "Η μετάβαση ακυρώθηκε με επιτυχία"
|
||||
@@ -0,0 +1,95 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-03-01 17:42+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: auth_backends.py:45
|
||||
#, python-format
|
||||
msgid "Connection to IMAP server failed: %s"
|
||||
msgstr ""
|
||||
|
||||
#: checks.py:11
|
||||
msgid ""
|
||||
"Automatic domain/mailbox creation is disabled which means IMAP "
|
||||
"authentication won't work."
|
||||
msgstr ""
|
||||
|
||||
#: checks.py:13
|
||||
msgid ""
|
||||
"Go to the online parameters panel (admin tab) and activate this feature."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:15
|
||||
msgid "OfflineIMAP settings"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:18
|
||||
msgid "Concurrent sync jobs"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:20
|
||||
msgid "The maximum number of concurrent synchronization jobs"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:23
|
||||
msgid "OfflineIMAP Filter settings"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:26
|
||||
msgid "Create Folders"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:28
|
||||
msgid "Allow Creation of missing folders during sync"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:33
|
||||
msgid "Folder Filter Exclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:36
|
||||
msgid ""
|
||||
"Use a regular expression to explicitly include folders in sync. Example: "
|
||||
"^Trash$|Del"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:43
|
||||
msgid "Folder Filter Inclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:46
|
||||
msgid ""
|
||||
"A comma seperated list of folders to explicitly include in sync even if "
|
||||
"filtered by the Folder Filter Exclusions. Example: debian.user, debian."
|
||||
"personal "
|
||||
msgstr ""
|
||||
|
||||
#: handlers.py:17
|
||||
msgid "IMAP migrations"
|
||||
msgstr ""
|
||||
|
||||
#: modo_extension.py:16
|
||||
msgid "IMAP migration using OfflineIMAP"
|
||||
msgstr ""
|
||||
|
||||
#: modo_extension.py:19
|
||||
msgid "Migrate existing mailboxes using IMAP and OfflineIMAP"
|
||||
msgstr ""
|
||||
|
||||
#: modo_extension.py:25 templates/modoboa_imap_migration/index.html:6
|
||||
msgid "IMAP migration"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,101 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Antoine Nguyen <tonio@ngyn.org>, 2017
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-03-01 17:42+0100\n"
|
||||
"PO-Revision-Date: 2019-03-01 18:09+0100\n"
|
||||
"Last-Translator: Antoine Nguyen <tonio@ngyn.org>\n"
|
||||
"Language-Team: French (http://www.transifex.com/tonio/modoboa/language/fr/)\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: auth_backends.py:45
|
||||
#, python-format
|
||||
msgid "Connection to IMAP server failed: %s"
|
||||
msgstr "Echec lors de la connexion au serveur IMAP : %s"
|
||||
|
||||
#: checks.py:11
|
||||
msgid ""
|
||||
"Automatic domain/mailbox creation is disabled which means IMAP "
|
||||
"authentication won't work."
|
||||
msgstr "La création automatique de domaine/compte est désactivée, ce qui veut dire que l'authentification IMAP ne fonctionnera pas."
|
||||
|
||||
#: checks.py:13
|
||||
msgid ""
|
||||
"Go to the online parameters panel (admin tab) and activate this feature."
|
||||
msgstr "Rendez vous sur le panneau de configuration des paramètres (onglet Administration) et activez cette fonctionnalité."
|
||||
|
||||
#: forms.py:15
|
||||
msgid "OfflineIMAP settings"
|
||||
msgstr "Paramètres OfflineIMAP"
|
||||
|
||||
#: forms.py:18
|
||||
msgid "Concurrent sync jobs"
|
||||
msgstr "Tâche(s) de synchronisation concurrente(s)"
|
||||
|
||||
#: forms.py:20
|
||||
msgid "The maximum number of concurrent synchronization jobs"
|
||||
msgstr "Nombre maximal de tâche de synchronisation concurrente"
|
||||
|
||||
#: forms.py:23
|
||||
msgid "OfflineIMAP Filter settings"
|
||||
msgstr "Paramètres de filtragre OfflineIMAP"
|
||||
|
||||
#: forms.py:26
|
||||
msgid "Create Folders"
|
||||
msgstr "Création des dossiers"
|
||||
|
||||
#: forms.py:28
|
||||
msgid "Allow Creation of missing folders during sync"
|
||||
msgstr "Autoriser la création des dossiers manquant pendant la synchro"
|
||||
|
||||
#: forms.py:33
|
||||
msgid "Folder Filter Exclusions"
|
||||
msgstr "Filtre d'exclusion de dossier"
|
||||
|
||||
#: forms.py:36
|
||||
msgid ""
|
||||
"Use a regular expression to explicitly include folders in sync. Example: "
|
||||
"^Trash$|Del"
|
||||
msgstr ""
|
||||
"Utiliser une expression régulière pour explicitement inclure des dossiers à "
|
||||
"synchroniser. Exemple : ^Trash$|Del"
|
||||
|
||||
#: forms.py:43
|
||||
msgid "Folder Filter Inclusions"
|
||||
msgstr "Filtre d'inclusion de dossier"
|
||||
|
||||
#: forms.py:46
|
||||
msgid ""
|
||||
"A comma seperated list of folders to explicitly include in sync even if "
|
||||
"filtered by the Folder Filter Exclusions. Example: debian.user, debian."
|
||||
"personal "
|
||||
msgstr ""
|
||||
"Une liste de dossiers séparés par des virgules à explicitement inclure "
|
||||
"pendant la synchronisation même s'ils sont concernés par le filtre "
|
||||
"d'exclusion. Exemple debian.user, debian.personal"
|
||||
|
||||
#: handlers.py:17
|
||||
msgid "IMAP migrations"
|
||||
msgstr "Migrations IMAP"
|
||||
|
||||
#: modo_extension.py:16
|
||||
msgid "IMAP migration using OfflineIMAP"
|
||||
msgstr "Migration IMAP basée sur OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:19
|
||||
msgid "Migrate existing mailboxes using IMAP and OfflineIMAP"
|
||||
msgstr "Migrer des boites existantes en utilisant IMAP et OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:25 templates/modoboa_imap_migration/index.html:6
|
||||
msgid "IMAP migration"
|
||||
msgstr "Migration IMAP"
|
||||
@@ -0,0 +1,134 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Rocco <mors10@hotmail.com>, 2015
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-03-01 17:42+0100\n"
|
||||
"PO-Revision-Date: 2016-11-09 16:13+0000\n"
|
||||
"Last-Translator: Rocco <mors10@hotmail.com>\n"
|
||||
"Language-Team: Italian (http://www.transifex.com/tonio/modoboa/language/"
|
||||
"it/)\n"
|
||||
"Language: it\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: auth_backends.py:45
|
||||
#, python-format
|
||||
msgid "Connection to IMAP server failed: %s"
|
||||
msgstr "Conennessione al server IMAP fallito: %s"
|
||||
|
||||
#: checks.py:11
|
||||
msgid ""
|
||||
"Automatic domain/mailbox creation is disabled which means IMAP "
|
||||
"authentication won't work."
|
||||
msgstr ""
|
||||
|
||||
#: checks.py:13
|
||||
msgid ""
|
||||
"Go to the online parameters panel (admin tab) and activate this feature."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:15
|
||||
msgid "OfflineIMAP settings"
|
||||
msgstr "Impostazioni OfflineIMAP"
|
||||
|
||||
#: forms.py:18
|
||||
msgid "Concurrent sync jobs"
|
||||
msgstr "Sincronizzazioni contemporanee"
|
||||
|
||||
#: forms.py:20
|
||||
msgid "The maximum number of concurrent synchronization jobs"
|
||||
msgstr "Il numero massimo si sincronizzazioni contemporanee"
|
||||
|
||||
#: forms.py:23
|
||||
#, fuzzy
|
||||
#| msgid "OfflineIMAP settings"
|
||||
msgid "OfflineIMAP Filter settings"
|
||||
msgstr "Impostazioni OfflineIMAP"
|
||||
|
||||
#: forms.py:26
|
||||
msgid "Create Folders"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:28
|
||||
msgid "Allow Creation of missing folders during sync"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:33
|
||||
msgid "Folder Filter Exclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:36
|
||||
msgid ""
|
||||
"Use a regular expression to explicitly include folders in sync. Example: "
|
||||
"^Trash$|Del"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:43
|
||||
msgid "Folder Filter Inclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:46
|
||||
msgid ""
|
||||
"A comma seperated list of folders to explicitly include in sync even if "
|
||||
"filtered by the Folder Filter Exclusions. Example: debian.user, debian."
|
||||
"personal "
|
||||
msgstr ""
|
||||
|
||||
#: handlers.py:17
|
||||
#, fuzzy
|
||||
#| msgid "IMAP migration"
|
||||
msgid "IMAP migrations"
|
||||
msgstr "Migrazione IMAP"
|
||||
|
||||
#: modo_extension.py:16
|
||||
msgid "IMAP migration using OfflineIMAP"
|
||||
msgstr "Migrazione IMAP utilizzado OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:19
|
||||
msgid "Migrate existing mailboxes using IMAP and OfflineIMAP"
|
||||
msgstr "Migrare caselle di posta elettronica utilizzando IMAP e OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:25 templates/modoboa_imap_migration/index.html:6
|
||||
msgid "IMAP migration"
|
||||
msgstr "Migrazione IMAP"
|
||||
|
||||
#~ msgid "Connection settings"
|
||||
#~ msgstr "Impostazioni di connessione"
|
||||
|
||||
#~ msgid "Server address"
|
||||
#~ msgstr "Indirizzo del Server"
|
||||
|
||||
#~ msgid "Address of your IMAP server"
|
||||
#~ msgstr "Indirizzo del server IMAP"
|
||||
|
||||
#~ msgid "Use a secured connection"
|
||||
#~ msgstr "Utilizza connessione sicura"
|
||||
|
||||
#~ msgid "Use a secured connection to access IMAP server"
|
||||
#~ msgstr "Utilizza una connessione sicura per l'accesso al server IMAP"
|
||||
|
||||
#~ msgid "Server port"
|
||||
#~ msgstr "Porta del server"
|
||||
|
||||
#~ msgid "Listening port of your IMAP server"
|
||||
#~ msgstr "Porta in ascolto del server IMAP"
|
||||
|
||||
#~ msgid "Cancel this account's migration"
|
||||
#~ msgstr "Cancellare l'account di questa migrazione"
|
||||
|
||||
#~ msgid "Cancel the migration for %s?"
|
||||
#~ msgstr "Cancellare la migrazione %s?"
|
||||
|
||||
#~ msgid "Unknow migration"
|
||||
#~ msgstr "Migrazione sconosciuta"
|
||||
|
||||
#~ msgid "Migration cancelled successfully"
|
||||
#~ msgstr "Migrazione avvenuta con successo"
|
||||
@@ -0,0 +1,134 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# cwatanab <chihiro.watanabe@live.jp>, 2016
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-03-01 17:42+0100\n"
|
||||
"PO-Revision-Date: 2016-11-09 16:13+0000\n"
|
||||
"Last-Translator: cwatanab <chihiro.watanabe@live.jp>\n"
|
||||
"Language-Team: Japanese (Japan) (http://www.transifex.com/tonio/modoboa/"
|
||||
"language/ja_JP/)\n"
|
||||
"Language: ja_JP\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
#: auth_backends.py:45
|
||||
#, python-format
|
||||
msgid "Connection to IMAP server failed: %s"
|
||||
msgstr "IMAP サーバーへの接続に失敗しました: %s"
|
||||
|
||||
#: checks.py:11
|
||||
msgid ""
|
||||
"Automatic domain/mailbox creation is disabled which means IMAP "
|
||||
"authentication won't work."
|
||||
msgstr ""
|
||||
|
||||
#: checks.py:13
|
||||
msgid ""
|
||||
"Go to the online parameters panel (admin tab) and activate this feature."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:15
|
||||
msgid "OfflineIMAP settings"
|
||||
msgstr "オフライン IMAP 設定"
|
||||
|
||||
#: forms.py:18
|
||||
msgid "Concurrent sync jobs"
|
||||
msgstr "同期ジョブの並列度"
|
||||
|
||||
#: forms.py:20
|
||||
msgid "The maximum number of concurrent synchronization jobs"
|
||||
msgstr "同期ジョブを同時に行う最大数"
|
||||
|
||||
#: forms.py:23
|
||||
#, fuzzy
|
||||
#| msgid "OfflineIMAP settings"
|
||||
msgid "OfflineIMAP Filter settings"
|
||||
msgstr "オフライン IMAP 設定"
|
||||
|
||||
#: forms.py:26
|
||||
msgid "Create Folders"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:28
|
||||
msgid "Allow Creation of missing folders during sync"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:33
|
||||
msgid "Folder Filter Exclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:36
|
||||
msgid ""
|
||||
"Use a regular expression to explicitly include folders in sync. Example: "
|
||||
"^Trash$|Del"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:43
|
||||
msgid "Folder Filter Inclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:46
|
||||
msgid ""
|
||||
"A comma seperated list of folders to explicitly include in sync even if "
|
||||
"filtered by the Folder Filter Exclusions. Example: debian.user, debian."
|
||||
"personal "
|
||||
msgstr ""
|
||||
|
||||
#: handlers.py:17
|
||||
#, fuzzy
|
||||
#| msgid "IMAP migration"
|
||||
msgid "IMAP migrations"
|
||||
msgstr "IMAP マイグレーション"
|
||||
|
||||
#: modo_extension.py:16
|
||||
msgid "IMAP migration using OfflineIMAP"
|
||||
msgstr "オフライン IMAP を使用して IMAP マイグレーション"
|
||||
|
||||
#: modo_extension.py:19
|
||||
msgid "Migrate existing mailboxes using IMAP and OfflineIMAP"
|
||||
msgstr "IMAP と オフライン IMAP を使って既存のメールボックスを移行します"
|
||||
|
||||
#: modo_extension.py:25 templates/modoboa_imap_migration/index.html:6
|
||||
msgid "IMAP migration"
|
||||
msgstr "IMAP マイグレーション"
|
||||
|
||||
#~ msgid "Connection settings"
|
||||
#~ msgstr "接続設定"
|
||||
|
||||
#~ msgid "Server address"
|
||||
#~ msgstr "サーバーアドレス"
|
||||
|
||||
#~ msgid "Address of your IMAP server"
|
||||
#~ msgstr "IMAP サーバーのアドレス"
|
||||
|
||||
#~ msgid "Use a secured connection"
|
||||
#~ msgstr "セキュアな接続を使う"
|
||||
|
||||
#~ msgid "Use a secured connection to access IMAP server"
|
||||
#~ msgstr "IMAP サーバーへのアクセスにセキュアな接続を使う"
|
||||
|
||||
#~ msgid "Server port"
|
||||
#~ msgstr "サーバーポート"
|
||||
|
||||
#~ msgid "Listening port of your IMAP server"
|
||||
#~ msgstr "IMAP サーバーの待受ポート"
|
||||
|
||||
#~ msgid "Cancel this account's migration"
|
||||
#~ msgstr "このアカウントのマイグレーションをキャンセルする"
|
||||
|
||||
#~ msgid "Cancel the migration for %s?"
|
||||
#~ msgstr "%s のマイグレーションをキャンセルしますか?"
|
||||
|
||||
#~ msgid "Unknow migration"
|
||||
#~ msgstr "不明なマイグレーション"
|
||||
|
||||
#~ msgid "Migration cancelled successfully"
|
||||
#~ msgstr "マイグレーションのキャンセルに成功しました"
|
||||
@@ -0,0 +1,134 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Tuxis Internet Engineering V.O.F. <info@tuxis.nl>, 2016
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-03-01 17:42+0100\n"
|
||||
"PO-Revision-Date: 2016-12-20 14:22+0000\n"
|
||||
"Last-Translator: Tuxis Internet Engineering V.O.F. <info@tuxis.nl>\n"
|
||||
"Language-Team: Dutch (Netherlands) (http://www.transifex.com/tonio/modoboa/"
|
||||
"language/nl_NL/)\n"
|
||||
"Language: nl_NL\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: auth_backends.py:45
|
||||
#, python-format
|
||||
msgid "Connection to IMAP server failed: %s"
|
||||
msgstr "Verbinding met de IMAP server mislukt: %s"
|
||||
|
||||
#: checks.py:11
|
||||
msgid ""
|
||||
"Automatic domain/mailbox creation is disabled which means IMAP "
|
||||
"authentication won't work."
|
||||
msgstr ""
|
||||
|
||||
#: checks.py:13
|
||||
msgid ""
|
||||
"Go to the online parameters panel (admin tab) and activate this feature."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:15
|
||||
msgid "OfflineIMAP settings"
|
||||
msgstr "OfflineIMAP instellingen"
|
||||
|
||||
#: forms.py:18
|
||||
msgid "Concurrent sync jobs"
|
||||
msgstr "Gelijktijdige synchronisatietaken"
|
||||
|
||||
#: forms.py:20
|
||||
msgid "The maximum number of concurrent synchronization jobs"
|
||||
msgstr "Het maximale aantal gelijktijdige synchronisatietaken"
|
||||
|
||||
#: forms.py:23
|
||||
#, fuzzy
|
||||
#| msgid "OfflineIMAP settings"
|
||||
msgid "OfflineIMAP Filter settings"
|
||||
msgstr "OfflineIMAP instellingen"
|
||||
|
||||
#: forms.py:26
|
||||
msgid "Create Folders"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:28
|
||||
msgid "Allow Creation of missing folders during sync"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:33
|
||||
msgid "Folder Filter Exclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:36
|
||||
msgid ""
|
||||
"Use a regular expression to explicitly include folders in sync. Example: "
|
||||
"^Trash$|Del"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:43
|
||||
msgid "Folder Filter Inclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:46
|
||||
msgid ""
|
||||
"A comma seperated list of folders to explicitly include in sync even if "
|
||||
"filtered by the Folder Filter Exclusions. Example: debian.user, debian."
|
||||
"personal "
|
||||
msgstr ""
|
||||
|
||||
#: handlers.py:17
|
||||
#, fuzzy
|
||||
#| msgid "IMAP migration"
|
||||
msgid "IMAP migrations"
|
||||
msgstr "IMAP migratie"
|
||||
|
||||
#: modo_extension.py:16
|
||||
msgid "IMAP migration using OfflineIMAP"
|
||||
msgstr "IMAP migratie door middel van OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:19
|
||||
msgid "Migrate existing mailboxes using IMAP and OfflineIMAP"
|
||||
msgstr "Migreer bestaande mailboxen door middel van IMAP en OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:25 templates/modoboa_imap_migration/index.html:6
|
||||
msgid "IMAP migration"
|
||||
msgstr "IMAP migratie"
|
||||
|
||||
#~ msgid "Connection settings"
|
||||
#~ msgstr "Verbinding instellingen"
|
||||
|
||||
#~ msgid "Server address"
|
||||
#~ msgstr "Serveradres"
|
||||
|
||||
#~ msgid "Address of your IMAP server"
|
||||
#~ msgstr "Adres van uw IMAP-server"
|
||||
|
||||
#~ msgid "Use a secured connection"
|
||||
#~ msgstr "Gebruik een beveiligde verbinding"
|
||||
|
||||
#~ msgid "Use a secured connection to access IMAP server"
|
||||
#~ msgstr "Gebruik een beveiligde verbinding met de IMAP server"
|
||||
|
||||
#~ msgid "Server port"
|
||||
#~ msgstr "Serverpoort"
|
||||
|
||||
#~ msgid "Listening port of your IMAP server"
|
||||
#~ msgstr "Poort van uw IMAP server"
|
||||
|
||||
#~ msgid "Cancel this account's migration"
|
||||
#~ msgstr "Annuleer de migratie van dit account"
|
||||
|
||||
#~ msgid "Cancel the migration for %s?"
|
||||
#~ msgstr "Annuleer de migratie van %s?"
|
||||
|
||||
#~ msgid "Unknow migration"
|
||||
#~ msgstr "Onbekende migratie"
|
||||
|
||||
#~ msgid "Migration cancelled successfully"
|
||||
#~ msgstr "Migratie succesvol geannuleerd"
|
||||
@@ -0,0 +1,134 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Paulino Michelazzo <paulino@michelazzo.com.br>, 2015
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-03-01 17:42+0100\n"
|
||||
"PO-Revision-Date: 2016-11-09 16:13+0000\n"
|
||||
"Last-Translator: Paulino Michelazzo <paulino@michelazzo.com.br>\n"
|
||||
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/tonio/modoboa/"
|
||||
"language/pt_BR/)\n"
|
||||
"Language: pt_BR\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||
|
||||
#: auth_backends.py:45
|
||||
#, python-format
|
||||
msgid "Connection to IMAP server failed: %s"
|
||||
msgstr "Falha ao conectar no servidor IMAP: %s"
|
||||
|
||||
#: checks.py:11
|
||||
msgid ""
|
||||
"Automatic domain/mailbox creation is disabled which means IMAP "
|
||||
"authentication won't work."
|
||||
msgstr ""
|
||||
|
||||
#: checks.py:13
|
||||
msgid ""
|
||||
"Go to the online parameters panel (admin tab) and activate this feature."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:15
|
||||
msgid "OfflineIMAP settings"
|
||||
msgstr "Configurações OfflineIMAP"
|
||||
|
||||
#: forms.py:18
|
||||
msgid "Concurrent sync jobs"
|
||||
msgstr "Tarefas de sincronismo concorrentes"
|
||||
|
||||
#: forms.py:20
|
||||
msgid "The maximum number of concurrent synchronization jobs"
|
||||
msgstr "O número máximo de tarefas de sincronismo concorrentes"
|
||||
|
||||
#: forms.py:23
|
||||
#, fuzzy
|
||||
#| msgid "OfflineIMAP settings"
|
||||
msgid "OfflineIMAP Filter settings"
|
||||
msgstr "Configurações OfflineIMAP"
|
||||
|
||||
#: forms.py:26
|
||||
msgid "Create Folders"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:28
|
||||
msgid "Allow Creation of missing folders during sync"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:33
|
||||
msgid "Folder Filter Exclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:36
|
||||
msgid ""
|
||||
"Use a regular expression to explicitly include folders in sync. Example: "
|
||||
"^Trash$|Del"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:43
|
||||
msgid "Folder Filter Inclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:46
|
||||
msgid ""
|
||||
"A comma seperated list of folders to explicitly include in sync even if "
|
||||
"filtered by the Folder Filter Exclusions. Example: debian.user, debian."
|
||||
"personal "
|
||||
msgstr ""
|
||||
|
||||
#: handlers.py:17
|
||||
#, fuzzy
|
||||
#| msgid "IMAP migration"
|
||||
msgid "IMAP migrations"
|
||||
msgstr "Migração IMAP"
|
||||
|
||||
#: modo_extension.py:16
|
||||
msgid "IMAP migration using OfflineIMAP"
|
||||
msgstr "Migração IMAP usando OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:19
|
||||
msgid "Migrate existing mailboxes using IMAP and OfflineIMAP"
|
||||
msgstr "Migrar as caixas postais existentes usando IMAP e OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:25 templates/modoboa_imap_migration/index.html:6
|
||||
msgid "IMAP migration"
|
||||
msgstr "Migração IMAP"
|
||||
|
||||
#~ msgid "Connection settings"
|
||||
#~ msgstr "Configurações de conexão"
|
||||
|
||||
#~ msgid "Server address"
|
||||
#~ msgstr "Endereço do servidor"
|
||||
|
||||
#~ msgid "Address of your IMAP server"
|
||||
#~ msgstr "Endereço do seu servidor IMAP"
|
||||
|
||||
#~ msgid "Use a secured connection"
|
||||
#~ msgstr "Usar conexão segura"
|
||||
|
||||
#~ msgid "Use a secured connection to access IMAP server"
|
||||
#~ msgstr "Usar uma conexão segura para acessar o servidor IMAP"
|
||||
|
||||
#~ msgid "Server port"
|
||||
#~ msgstr "Porta do servidor"
|
||||
|
||||
#~ msgid "Listening port of your IMAP server"
|
||||
#~ msgstr "Porta de escuta do seu servidor IMAP"
|
||||
|
||||
#~ msgid "Cancel this account's migration"
|
||||
#~ msgstr "Cancelar esta migração de conta"
|
||||
|
||||
#~ msgid "Cancel the migration for %s?"
|
||||
#~ msgstr "Cancelar a migração para %s?"
|
||||
|
||||
#~ msgid "Unknow migration"
|
||||
#~ msgstr "Migração desconhecida"
|
||||
|
||||
#~ msgid "Migration cancelled successfully"
|
||||
#~ msgstr "Migração cancelada com sucesso"
|
||||
@@ -0,0 +1,135 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Gregor <info@gregorjev.net>, 2016
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-03-01 17:42+0100\n"
|
||||
"PO-Revision-Date: 2016-11-09 16:13+0000\n"
|
||||
"Last-Translator: Gregor <info@gregorjev.net>\n"
|
||||
"Language-Team: Slovenian (Slovenia) (http://www.transifex.com/tonio/modoboa/"
|
||||
"language/sl_SI/)\n"
|
||||
"Language: sl_SI\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n"
|
||||
"%100==4 ? 2 : 3);\n"
|
||||
|
||||
#: auth_backends.py:45
|
||||
#, python-format
|
||||
msgid "Connection to IMAP server failed: %s"
|
||||
msgstr "Povezava z IMAP strežnikom ni uspela: %s"
|
||||
|
||||
#: checks.py:11
|
||||
msgid ""
|
||||
"Automatic domain/mailbox creation is disabled which means IMAP "
|
||||
"authentication won't work."
|
||||
msgstr ""
|
||||
|
||||
#: checks.py:13
|
||||
msgid ""
|
||||
"Go to the online parameters panel (admin tab) and activate this feature."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:15
|
||||
msgid "OfflineIMAP settings"
|
||||
msgstr "IMAP nastavitve brez povezave"
|
||||
|
||||
#: forms.py:18
|
||||
msgid "Concurrent sync jobs"
|
||||
msgstr "Concurrent sync jobs"
|
||||
|
||||
#: forms.py:20
|
||||
msgid "The maximum number of concurrent synchronization jobs"
|
||||
msgstr "The maximum number of concurrent synchronization jobs"
|
||||
|
||||
#: forms.py:23
|
||||
#, fuzzy
|
||||
#| msgid "OfflineIMAP settings"
|
||||
msgid "OfflineIMAP Filter settings"
|
||||
msgstr "IMAP nastavitve brez povezave"
|
||||
|
||||
#: forms.py:26
|
||||
msgid "Create Folders"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:28
|
||||
msgid "Allow Creation of missing folders during sync"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:33
|
||||
msgid "Folder Filter Exclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:36
|
||||
msgid ""
|
||||
"Use a regular expression to explicitly include folders in sync. Example: "
|
||||
"^Trash$|Del"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:43
|
||||
msgid "Folder Filter Inclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:46
|
||||
msgid ""
|
||||
"A comma seperated list of folders to explicitly include in sync even if "
|
||||
"filtered by the Folder Filter Exclusions. Example: debian.user, debian."
|
||||
"personal "
|
||||
msgstr ""
|
||||
|
||||
#: handlers.py:17
|
||||
#, fuzzy
|
||||
#| msgid "IMAP migration"
|
||||
msgid "IMAP migrations"
|
||||
msgstr "Selitev IMAP"
|
||||
|
||||
#: modo_extension.py:16
|
||||
msgid "IMAP migration using OfflineIMAP"
|
||||
msgstr "IMAP selitev z uporabo IMAP brez povezave"
|
||||
|
||||
#: modo_extension.py:19
|
||||
msgid "Migrate existing mailboxes using IMAP and OfflineIMAP"
|
||||
msgstr "Preseli obstoječe poštne predale z uporabo IMAP in IMAP brez povezave"
|
||||
|
||||
#: modo_extension.py:25 templates/modoboa_imap_migration/index.html:6
|
||||
msgid "IMAP migration"
|
||||
msgstr "Selitev IMAP"
|
||||
|
||||
#~ msgid "Connection settings"
|
||||
#~ msgstr "Nastavitve povezave"
|
||||
|
||||
#~ msgid "Server address"
|
||||
#~ msgstr "Naslov strežnika"
|
||||
|
||||
#~ msgid "Address of your IMAP server"
|
||||
#~ msgstr "Naslov vašega IMAP strežnika"
|
||||
|
||||
#~ msgid "Use a secured connection"
|
||||
#~ msgstr "Uporabi zavarovano povezavo"
|
||||
|
||||
#~ msgid "Use a secured connection to access IMAP server"
|
||||
#~ msgstr "Za dostop do IMAP strežnika, uporabite zavarovano povezavo"
|
||||
|
||||
#~ msgid "Server port"
|
||||
#~ msgstr "Vrata strežnika"
|
||||
|
||||
#~ msgid "Listening port of your IMAP server"
|
||||
#~ msgstr "Vrata vašega IMAP strežnika"
|
||||
|
||||
#~ msgid "Cancel this account's migration"
|
||||
#~ msgstr "Prekliči selitev računa"
|
||||
|
||||
#~ msgid "Cancel the migration for %s?"
|
||||
#~ msgstr "Prekličem selitev za %s?"
|
||||
|
||||
#~ msgid "Unknow migration"
|
||||
#~ msgstr "Neznana selitev"
|
||||
|
||||
#~ msgid "Migration cancelled successfully"
|
||||
#~ msgstr "Selivet je uspešno preklicana"
|
||||
@@ -0,0 +1,134 @@
|
||||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
#
|
||||
# Translators:
|
||||
# Olle Gustafsson <olle@dalnix.se>, 2016
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Modoboa\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-03-01 17:42+0100\n"
|
||||
"PO-Revision-Date: 2016-11-09 16:13+0000\n"
|
||||
"Last-Translator: Olle Gustafsson <olle@dalnix.se>\n"
|
||||
"Language-Team: Swedish (http://www.transifex.com/tonio/modoboa/language/"
|
||||
"sv/)\n"
|
||||
"Language: sv\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#: auth_backends.py:45
|
||||
#, python-format
|
||||
msgid "Connection to IMAP server failed: %s"
|
||||
msgstr "Anslutning till IMAP server misslyckades: %s"
|
||||
|
||||
#: checks.py:11
|
||||
msgid ""
|
||||
"Automatic domain/mailbox creation is disabled which means IMAP "
|
||||
"authentication won't work."
|
||||
msgstr ""
|
||||
|
||||
#: checks.py:13
|
||||
msgid ""
|
||||
"Go to the online parameters panel (admin tab) and activate this feature."
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:15
|
||||
msgid "OfflineIMAP settings"
|
||||
msgstr "OfflineIMAP inställningar"
|
||||
|
||||
#: forms.py:18
|
||||
msgid "Concurrent sync jobs"
|
||||
msgstr "Samtidiga synkroniserings jobb"
|
||||
|
||||
#: forms.py:20
|
||||
msgid "The maximum number of concurrent synchronization jobs"
|
||||
msgstr "Det maximala antalet samtidiga synkroniserings jobb"
|
||||
|
||||
#: forms.py:23
|
||||
#, fuzzy
|
||||
#| msgid "OfflineIMAP settings"
|
||||
msgid "OfflineIMAP Filter settings"
|
||||
msgstr "OfflineIMAP inställningar"
|
||||
|
||||
#: forms.py:26
|
||||
msgid "Create Folders"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:28
|
||||
msgid "Allow Creation of missing folders during sync"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:33
|
||||
msgid "Folder Filter Exclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:36
|
||||
msgid ""
|
||||
"Use a regular expression to explicitly include folders in sync. Example: "
|
||||
"^Trash$|Del"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:43
|
||||
msgid "Folder Filter Inclusions"
|
||||
msgstr ""
|
||||
|
||||
#: forms.py:46
|
||||
msgid ""
|
||||
"A comma seperated list of folders to explicitly include in sync even if "
|
||||
"filtered by the Folder Filter Exclusions. Example: debian.user, debian."
|
||||
"personal "
|
||||
msgstr ""
|
||||
|
||||
#: handlers.py:17
|
||||
#, fuzzy
|
||||
#| msgid "IMAP migration"
|
||||
msgid "IMAP migrations"
|
||||
msgstr "IMAP migrering"
|
||||
|
||||
#: modo_extension.py:16
|
||||
msgid "IMAP migration using OfflineIMAP"
|
||||
msgstr "IMAP migrering med hjälp av OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:19
|
||||
msgid "Migrate existing mailboxes using IMAP and OfflineIMAP"
|
||||
msgstr "Migrera existerande brevlådor med hjälp av IMAP och OfflineIMAP"
|
||||
|
||||
#: modo_extension.py:25 templates/modoboa_imap_migration/index.html:6
|
||||
msgid "IMAP migration"
|
||||
msgstr "IMAP migrering"
|
||||
|
||||
#~ msgid "Connection settings"
|
||||
#~ msgstr "Anslutningsinställningar"
|
||||
|
||||
#~ msgid "Server address"
|
||||
#~ msgstr "Server adress"
|
||||
|
||||
#~ msgid "Address of your IMAP server"
|
||||
#~ msgstr "Adress till din IMAP server"
|
||||
|
||||
#~ msgid "Use a secured connection"
|
||||
#~ msgstr "Använd en säker anslutning"
|
||||
|
||||
#~ msgid "Use a secured connection to access IMAP server"
|
||||
#~ msgstr "Använd en säker anslutning för åtkomst till IMAP server"
|
||||
|
||||
#~ msgid "Server port"
|
||||
#~ msgstr "Server port"
|
||||
|
||||
#~ msgid "Listening port of your IMAP server"
|
||||
#~ msgstr "Lyssningsport för din IMAP server"
|
||||
|
||||
#~ msgid "Cancel this account's migration"
|
||||
#~ msgstr "Avbryt detta kontos migrering"
|
||||
|
||||
#~ msgid "Cancel the migration for %s?"
|
||||
#~ msgstr "Avbryt migreringen för %s?"
|
||||
|
||||
#~ msgid "Unknow migration"
|
||||
#~ msgstr "Okänd migrering"
|
||||
|
||||
#~ msgid "Migration cancelled successfully"
|
||||
#~ msgstr "Migrering avbröts framgångsrikt"
|
||||
@@ -0,0 +1,44 @@
|
||||
"""A management command to create an offlineimap configuration file."""
|
||||
|
||||
import os
|
||||
import stat
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from modoboa.admin.app_settings import load_admin_settings
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from ...models import Migration
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Command definition."""
|
||||
|
||||
help = "Generate an offlineimap configuration file."
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add extra arguments to command line."""
|
||||
parser.add_argument(
|
||||
"--output", default="/tmp/offlineimap.conf",
|
||||
help="Path of the generated file")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Entry point."""
|
||||
load_admin_settings()
|
||||
conf = dict(
|
||||
param_tools.get_global_parameters("imap_migration"))
|
||||
context = {
|
||||
"imap_create_folders": conf["create_folders"],
|
||||
"imap_folder_filter_exclude": conf["folder_filter_exclude"],
|
||||
"imap_folder_filter_include": ", ".join(
|
||||
"'{0}'".format(w)
|
||||
for w in conf["folder_filter_include"].split(",")),
|
||||
"migrations": Migration.objects.select_related(
|
||||
"provider", "mailbox__domain"),
|
||||
}
|
||||
with open(options["output"], "w") as fpo:
|
||||
content = render_to_string(
|
||||
"imap_migration/offlineimap.conf", context)
|
||||
fpo.write(content)
|
||||
os.chmod(options["output"], stat.S_IRUSR | stat.S_IWUSR)
|
||||
@@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '__first__'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Migration',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('_password', models.CharField(max_length=100)),
|
||||
('mailbox', models.ForeignKey(to='admin.Mailbox', on_delete=models.CASCADE)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('imap_migration', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='migration',
|
||||
name='_password',
|
||||
field=models.CharField(max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.18 on 2019-02-07 08:19
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def create_default_provider(apps, schema_editor):
|
||||
"""Create default provider based on old settings."""
|
||||
EmailProvider = apps.get_model("imap_migration", "EmailProvider")
|
||||
Migration = apps.get_model("imap_migration", "Migration")
|
||||
LocalConfig = apps.get_model("core", "LocalConfig")
|
||||
lc = LocalConfig.objects.first()
|
||||
if not lc:
|
||||
return
|
||||
params = lc._parameters.get("imap_migration")
|
||||
if params is None:
|
||||
return
|
||||
provider = EmailProvider.objects.create(
|
||||
name="Default",
|
||||
address=params["server_address"],
|
||||
port=params["server_port"],
|
||||
secured=params["secured"]
|
||||
)
|
||||
for migration in Migration.objects.select_related("mailbox__domain"):
|
||||
migration.provider = provider
|
||||
migration.username = (
|
||||
migration.mailbox.address + "@" + migration.mailbox.domain.name)
|
||||
migration.save(update_fields=["provider", "username"])
|
||||
|
||||
|
||||
def backwards(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('admin', '0014_auto_20181017_1628'),
|
||||
('imap_migration', '0002_password'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='EmailProvider',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=50)),
|
||||
('address', models.CharField(max_length=200)),
|
||||
('port', models.PositiveIntegerField(default=143)),
|
||||
('secured', models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EmailProviderDomain',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
('new_domain', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='admin.Domain')),
|
||||
('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='imap_migration.EmailProvider')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['provider', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='migration',
|
||||
name='username',
|
||||
field=models.CharField(max_length=254, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='migration',
|
||||
name='provider',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='imap_migration.EmailProvider'),
|
||||
),
|
||||
migrations.RunPython(create_default_provider, backwards)
|
||||
]
|
||||
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.18 on 2019-02-07 08:39
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('imap_migration', '0003_auto_20190207_0919'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='migration',
|
||||
name='provider',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='imap_migration.EmailProvider'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='migration',
|
||||
name='username',
|
||||
field=models.CharField(max_length=254),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 2.2.12 on 2021-04-01 07:38
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('imap_migration', '0004_auto_20190207_0939'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='emailproviderdomain',
|
||||
name='provider',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='domains', to='imap_migration.EmailProvider'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='migration',
|
||||
name='username',
|
||||
field=models.CharField(max_length=254, unique=True),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
IMAP migration models.
|
||||
"""
|
||||
|
||||
from django.db import models
|
||||
|
||||
from modoboa.admin.models import Mailbox
|
||||
from modoboa.lib.cryptutils import encrypt, decrypt
|
||||
|
||||
|
||||
class EmailProvider(models.Model):
|
||||
"""Email provider model."""
|
||||
|
||||
name = models.CharField(max_length=50)
|
||||
address = models.CharField(max_length=200)
|
||||
port = models.PositiveIntegerField(default=143)
|
||||
secured = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
ordering = ["name"]
|
||||
|
||||
|
||||
class EmailProviderDomain(models.Model):
|
||||
"""Email provider domain class."""
|
||||
|
||||
provider = models.ForeignKey(
|
||||
EmailProvider, on_delete=models.CASCADE, related_name="domains")
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
new_domain = models.ForeignKey(
|
||||
"admin.Domain", on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["provider", "name"]
|
||||
|
||||
|
||||
class Migration(models.Model):
|
||||
"""Represent mailboxes to migrate."""
|
||||
|
||||
provider = models.ForeignKey(EmailProvider, on_delete=models.CASCADE)
|
||||
mailbox = models.ForeignKey(Mailbox, on_delete=models.CASCADE)
|
||||
username = models.CharField(max_length=254, unique=True)
|
||||
_password = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return self.username
|
||||
|
||||
@property
|
||||
def password(self):
|
||||
"""Password getter."""
|
||||
return decrypt(self._password)
|
||||
|
||||
@password.setter
|
||||
def password(self, value):
|
||||
"""Password setter."""
|
||||
self._password = encrypt(value)
|
||||
@@ -0,0 +1,27 @@
|
||||
{% extends "fluid.html" %}
|
||||
|
||||
{% load i18n lib_tags static %}
|
||||
{% load render_bundle from webpack_loader %}
|
||||
|
||||
{% block pagetitle %}{% trans "IMAP migration" %}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
{% if not debug %}
|
||||
{% render_bundle 'chunk-vendors' 'css' 'IMAP_MIGRATION' %}
|
||||
{% render_bundle 'app' 'css' 'IMAP_MIGRATION' %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% if not debug %}
|
||||
{% render_bundle 'chunk-vendors' 'js' 'IMAP_MIGRATION' %}
|
||||
{% endif %}
|
||||
{% render_bundle 'app' 'js' 'IMAP_MIGRATION' %}
|
||||
{% endblock %}
|
||||
|
||||
{% block container_content %}
|
||||
|
||||
<div id="app">
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
[general]
|
||||
accounts = {{ migrations|join:", " }}
|
||||
|
||||
{% for migration in migrations %}
|
||||
[Account {{ migration }}]
|
||||
localrepository = Local_{{ migration }}
|
||||
remoterepository = Remote_{{ migration }}
|
||||
|
||||
[Repository Local_{{ migration }}]
|
||||
type = IMAP
|
||||
ssl = false
|
||||
remotehost = localhost
|
||||
remoteuser = {{ migration }}
|
||||
remotepass = {{ migration.password }}
|
||||
{% if imap_create_folders == False %}
|
||||
createfolders = False
|
||||
{% endif %}
|
||||
|
||||
[Repository Remote_{{ migration }}]
|
||||
type = IMAP
|
||||
remotehost = {{ migration.provider.address }}
|
||||
remoteport = {{ migration.provider.port }}
|
||||
{% if migration.provider.secured %}
|
||||
ssl = yes
|
||||
sslcacertfile = /etc/ssl/certs/ca-certificates.crt
|
||||
{% else %}
|
||||
ssl = no
|
||||
{% endif %}
|
||||
remoteuser = {{ migration }}
|
||||
remotepass = {{ migration.password }}
|
||||
readonly = true
|
||||
{% autoescape off %}
|
||||
{% if imap_folder_filter_exclude|length > 0 %}
|
||||
folderfilter = lambda folder: not re.search('({{ imap_folder_filter_exclude }})', folder)
|
||||
{% endif %}
|
||||
{% if imap_folder_filter_include|length > 2 %}
|
||||
folderincludes = [{{ imap_folder_filter_include }}]
|
||||
{% endif %}
|
||||
{% endautoescape %}
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,204 @@
|
||||
"""IMAP migration tests."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from unittest import mock
|
||||
|
||||
from six.moves import configparser
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.urls import reverse
|
||||
|
||||
from modoboa.admin import factories as admin_factories
|
||||
from modoboa.admin import models as admin_models
|
||||
from modoboa.core import factories as core_factories
|
||||
from modoboa.core import models as core_models
|
||||
from modoboa.lib.tests import ModoTestCase, ModoAPITestCase
|
||||
|
||||
from . import checks
|
||||
from . import factories
|
||||
from . import models
|
||||
|
||||
|
||||
class DataMixin(object):
|
||||
"""A mixin to provide test data."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super(DataMixin, cls).setUpTestData()
|
||||
admin_factories.populate_database()
|
||||
cls.mb = admin_models.Mailbox.objects.get(
|
||||
user__username="user@test.com")
|
||||
cls.migration = factories.MigrationFactory(
|
||||
password="Toto1234", mailbox=cls.mb, username="user@test.com")
|
||||
|
||||
|
||||
# class ViewsTestCase(DataMixin, ModoTestCase):
|
||||
# """Views test cases."""
|
||||
|
||||
# def test_index(self):
|
||||
# """Test index view."""
|
||||
# url = reverse("modoboa_imap_migration:index")
|
||||
# response = self.client.get(url)
|
||||
# self.assertContains(response, '<div id="app">')
|
||||
|
||||
|
||||
class AuthenticationTestCase(DataMixin, ModoTestCase):
|
||||
"""IMAP authentication test case."""
|
||||
|
||||
@mock.patch("imaplib.IMAP4")
|
||||
def test_authentication(self, mock_imap):
|
||||
"""Check IMAP authentication."""
|
||||
factories.EmailProviderDomainFactory(name="test.com")
|
||||
mock_imap.return_value.login.return_value = ["OK", b""]
|
||||
url = reverse("core:login")
|
||||
data = {"username": "new_user@test.com", "password": "Toto1234"}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
user = core_models.User.objects.get(username="new_user@test.com")
|
||||
self.assertEqual(user.mailbox.domain.name, "test.com")
|
||||
|
||||
mock_imap.return_value.login.return_value = ["NO", b"Error"]
|
||||
data = {"username": "new_user@test.com", "password": "Toto123"}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
mock_imap.return_value.login.return_value = ["OK", b""]
|
||||
data = {"username": "new_user@test2.com", "password": "Toto1234"}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
self.set_global_parameter(
|
||||
"auto_create_domain_and_mailbox", False, app="admin")
|
||||
data = {"username": "new_user2@test.com", "password": "Toto1234"}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
@mock.patch("imaplib.IMAP4")
|
||||
def test_authenticate_and_rename(self, mock_imap):
|
||||
"""Check address renaming."""
|
||||
domain = admin_models.Domain.objects.get(name="test.com")
|
||||
factories.EmailProviderDomainFactory(
|
||||
name="gmail.com", new_domain=domain)
|
||||
mock_imap.return_value.login.return_value = ["OK", b""]
|
||||
url = reverse("core:login")
|
||||
data = {"username": "new_user@gmail.com", "password": "Toto1234"}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
user = core_models.User.objects.get(username="new_user@test.com")
|
||||
self.assertEqual(user.mailbox.domain.name, "test.com")
|
||||
|
||||
@mock.patch("imaplib.IMAP4")
|
||||
def test_authenticate_conflicts(self, mock_imap):
|
||||
"""Check potential conflicts."""
|
||||
core_factories.UserFactory(
|
||||
username="admin2@test.com", groups=("DomainAdmins", ),
|
||||
password="{PLAIN}toto"
|
||||
)
|
||||
domain = admin_models.Domain.objects.get(name="test.com")
|
||||
factories.EmailProviderDomainFactory(
|
||||
name="gmail.com", new_domain=domain)
|
||||
mock_imap.return_value.login.return_value = ["OK", b""]
|
||||
url = reverse("core:login")
|
||||
data = {"username": "admin@gmail.com", "password": "Toto1234"}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
data = {"username": "admin2@gmail.com", "password": "Toto1234"}
|
||||
response = self.client.post(url, data)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
|
||||
class ManagementCommandTestCase(DataMixin, ModoTestCase):
|
||||
"""Management command test cases."""
|
||||
|
||||
def setUp(self):
|
||||
super(ManagementCommandTestCase, self).setUp()
|
||||
self.workdir = tempfile.mkdtemp()
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.workdir)
|
||||
|
||||
def test_generate_offlineimap_config(self):
|
||||
"""Test generate_offlineimap_config command."""
|
||||
path = os.path.join(self.workdir, "offlineimap.conf")
|
||||
call_command("generate_offlineimap_config", "--output", path)
|
||||
self.assertTrue(os.path.exists(path))
|
||||
conf = configparser.ConfigParser()
|
||||
conf.read(path)
|
||||
self.assertTrue(conf.has_section("Account user@test.com"))
|
||||
|
||||
|
||||
class ViewSetTestCase(DataMixin, ModoAPITestCase):
|
||||
"""ViewSet related tests."""
|
||||
|
||||
def test_create_provider(self):
|
||||
url = reverse("api:emailprovider-list")
|
||||
data = {
|
||||
"name": "Google",
|
||||
"address": "imap.google.com",
|
||||
"port": 143,
|
||||
"secured": True,
|
||||
"domains": [
|
||||
{"id": 1000,
|
||||
"name": "gmail.com"}
|
||||
]
|
||||
}
|
||||
response = self.client.post(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertTrue(
|
||||
models.EmailProviderDomain.objects.filter(name="gmail.com")
|
||||
.exists()
|
||||
)
|
||||
|
||||
def test_update_provider(self):
|
||||
provider_domain = factories.EmailProviderDomainFactory(
|
||||
name="outlook.com")
|
||||
url = reverse(
|
||||
"api:emailprovider-detail", args=[provider_domain.provider.pk])
|
||||
data = {
|
||||
"id": provider_domain.provider.id,
|
||||
"name": provider_domain.provider.name,
|
||||
"address": provider_domain.provider.address,
|
||||
"port": provider_domain.provider.port,
|
||||
"domains": [
|
||||
{"id": provider_domain.id,
|
||||
"name": "hotmail.com",
|
||||
"new_domain": None},
|
||||
{"name": "gmail.com",
|
||||
"new_domain": admin_models.Domain.objects.get(
|
||||
name="test.com").pk}
|
||||
]
|
||||
}
|
||||
response = self.client.put(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
provider_domain.refresh_from_db()
|
||||
self.assertEqual(provider_domain.name, data["domains"][0]["name"])
|
||||
self.assertTrue(
|
||||
models.EmailProviderDomain.objects.filter(name="gmail.com")
|
||||
.exists()
|
||||
)
|
||||
data["domains"].pop()
|
||||
response = self.client.put(url, data, format="json")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertFalse(
|
||||
models.EmailProviderDomain.objects.filter(name="gmail.com")
|
||||
.exists()
|
||||
)
|
||||
|
||||
|
||||
class ChecksTestCase(ModoTestCase):
|
||||
"""Deploy checks test case."""
|
||||
|
||||
def test_auto_creation_check(self):
|
||||
self.assertEqual(
|
||||
checks.check_auto_creation_is_enabled(None),
|
||||
[]
|
||||
)
|
||||
self.set_global_parameter(
|
||||
"auto_create_domain_and_mailbox", False, app="admin")
|
||||
self.assertEqual(
|
||||
checks.check_auto_creation_is_enabled(None),
|
||||
[checks.W001]
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Custom urls."""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
app_name = "imap_migration"
|
||||
|
||||
urlpatterns = [
|
||||
path("", views.IndexView.as_view(), name="index"),
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
"""Views for the IMAP migration extension."""
|
||||
|
||||
from django.contrib.auth import mixins as auth_mixins
|
||||
|
||||
from django.views import generic
|
||||
|
||||
|
||||
class IndexView(auth_mixins.LoginRequiredMixin,
|
||||
auth_mixins.PermissionRequiredMixin,
|
||||
generic.TemplateView):
|
||||
"""Index view."""
|
||||
|
||||
permission_required = "modoboa.imap_migration.add_migration"
|
||||
template_name = "modoboa.imap_migration/index.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Set menu selection."""
|
||||
kwargs.update({"selection": "imap_migrations"})
|
||||
return super(IndexView, self).get_context_data(**kwargs)
|
||||
@@ -12,7 +12,7 @@ class ParametersAPITestCase(ModoAPITestCase):
|
||||
resp = self.client.get(url)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
# core, admin, limits, pdf credentials
|
||||
self.assertEqual(len(resp.json()), 5)
|
||||
self.assertEqual(len(resp.json()), 6)
|
||||
|
||||
def test_get_structure(self):
|
||||
url = reverse("v2:parameter-structure")
|
||||
|
||||
@@ -30,6 +30,7 @@ urlpatterns = [
|
||||
path('pdfcredentials', include("modoboa.pdfcredentials.urls")),
|
||||
path('stats/', include("modoboa.maillog.urls")),
|
||||
path('dmarc/', include("modoboa.dmarc.urls")),
|
||||
path('imap_migration/', include("modoboa.imap_migration.urls")),
|
||||
|
||||
path('user/forward/', user_views.forward, name="user_forward"),
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ app_name = "api"
|
||||
urlpatterns = [
|
||||
path('', include("modoboa.core.api.v1.urls")),
|
||||
path('', include("modoboa.admin.api.v1.urls")),
|
||||
path('', include("modoboa.imap_migration.api.v1.urls")),
|
||||
path('', include("modoboa.limits.api.v1.urls")),
|
||||
path('', include("modoboa.relaydomains.api.v1.urls"))
|
||||
]
|
||||
|
||||
@@ -10,13 +10,14 @@ urlpatterns = [
|
||||
path('', include("modoboa.core.api.v2.urls")),
|
||||
path('', include("modoboa.admin.api.v2.urls")),
|
||||
path('', include("modoboa.parameters.api.v2.urls")),
|
||||
path('', include("modoboa.imap_migration.api.v2.urls")),
|
||||
path('', include("modoboa.limits.api.v1.urls")),
|
||||
path('', include("modoboa.relaydomains.api.v1.urls")),
|
||||
path('', include("modoboa.dmarc.api.v2.urls")),
|
||||
path('', include("modoboa.dnstools.api.v2.urls")),
|
||||
path('', include("modoboa.maillog.api.v2.urls")),
|
||||
path('', include("modoboa.transport.api.v2.urls")),
|
||||
path('', include("modoboa.pdfcredentials.api.v2.urls")),
|
||||
path('', include("modoboa.pdfcredentials.api.v2.urls"))
|
||||
]
|
||||
|
||||
urlpatterns += exts_pool.get_urls(category="api")
|
||||
|
||||
@@ -91,6 +91,7 @@ MODOBOA_APPS = (
|
||||
'modoboa.maillog',
|
||||
'modoboa.pdfcredentials',
|
||||
'modoboa.dmarc',
|
||||
'modoboa.imap_migration',
|
||||
# Modoboa extensions here.
|
||||
)
|
||||
|
||||
@@ -126,6 +127,7 @@ AUTHENTICATION_BACKENDS = (
|
||||
# 'modoboa.lib.authbackends.LDAPBackend',
|
||||
# 'modoboa.lib.authbackends.SMTPBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
'modoboa.imap_migration.auth_backends.IMAPBackend',
|
||||
)
|
||||
|
||||
# SMTP authentication
|
||||
|
||||
Reference in New Issue
Block a user