Started import work

This commit is contained in:
Spitap
2023-04-18 15:44:08 +02:00
committed by Antoine Nguyen
parent 844c1e482e
commit 415aefc764
66 changed files with 3802 additions and 8 deletions
+2 -2
View File
@@ -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
View File
@@ -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>
+18 -1
View File
@@ -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>
+9
View File
@@ -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)
+27
View File
@@ -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',
+3 -1
View File
@@ -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'
})
+73
View File
@@ -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
+16
View File
@@ -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
+12
View File
@@ -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
+39
View File
@@ -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()
+13
View File
@@ -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
+35
View File
@@ -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)
+107
View File
@@ -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 "
)
)
+35
View File
@@ -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()
+105
View File
@@ -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
+29
View File
@@ -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
+37
View File
@@ -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)
+19
View File
@@ -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),
),
]
+55
View File
@@ -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 %}
+204
View File
@@ -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]
)
+11
View File
@@ -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"),
]
+19
View File
@@ -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)
+1 -1
View File
@@ -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")
+1
View File
@@ -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"),
+1
View File
@@ -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"))
]
+2 -1
View File
@@ -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")
+2
View File
@@ -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