From 415aefc764c45fee827f9ef7d63df41fbcbc9d77 Mon Sep 17 00:00:00 2001 From: Spitap Date: Wed, 8 Mar 2023 14:35:53 +0100 Subject: [PATCH 01/12] Started import work --- docker/Dockerfile.dev | 4 +- frontend/Makefile | 4 +- frontend/src/api/imap_migration/migrations.js | 15 ++ frontend/src/api/imap_migration/providers.js | 27 +++ .../imap_migration/MigrationsList.vue | 118 ++++++++++ .../imap_migration/ProviderAssociatedForm.vue | 117 ++++++++++ .../imap_migration/ProviderCreationForm.vue | 118 ++++++++++ .../imap_migration/ProviderForm.vue | 138 ++++++++++++ .../imap_migration/ProviderGeneralForm.vue | 167 ++++++++++++++ .../imap_migration/ProvidersList.vue | 154 +++++++++++++ frontend/src/components/layout/Navbar.vue | 19 +- frontend/src/components/tools/DomainField.vue | 174 +++++++++++++++ frontend/src/plugins/vee-validate.js | 9 + frontend/src/router/index.js | 27 +++ frontend/src/store/index.js | 4 +- frontend/src/store/modules/providers.js | 73 +++++++ .../src/views/imap_migration/ProviderEdit.vue | 28 +++ .../src/views/imap_migration/Providers.vue | 56 +++++ .../core/commands/templates/settings.py.tpl | 2 + modoboa/imap_migration/__init__.py | 16 ++ modoboa/imap_migration/api/__init__.py | 0 modoboa/imap_migration/api/v1/__init__.py | 0 modoboa/imap_migration/api/v1/serializers.py | 93 ++++++++ modoboa/imap_migration/api/v1/urls.py | 12 ++ modoboa/imap_migration/api/v1/viewsets.py | 39 ++++ modoboa/imap_migration/api/v2/__init__.py | 0 modoboa/imap_migration/api/v2/serializers.py | 30 +++ modoboa/imap_migration/api/v2/urls.py | 13 ++ modoboa/imap_migration/api/v2/viewsets.py | 35 +++ modoboa/imap_migration/app_settings.py | 107 +++++++++ modoboa/imap_migration/apps.py | 35 +++ modoboa/imap_migration/auth_backends.py | 105 +++++++++ modoboa/imap_migration/checks.py | 29 +++ modoboa/imap_migration/factories.py | 37 ++++ modoboa/imap_migration/handlers.py | 19 ++ .../locale/cs_CZ/LC_MESSAGES/django.po | 134 ++++++++++++ .../locale/de/LC_MESSAGES/django.po | 133 ++++++++++++ .../locale/el_GR/LC_MESSAGES/django.po | 134 ++++++++++++ .../locale/en/LC_MESSAGES/django.po | 95 ++++++++ .../locale/fr/LC_MESSAGES/django.po | 101 +++++++++ .../locale/it/LC_MESSAGES/django.po | 134 ++++++++++++ .../locale/ja_JP/LC_MESSAGES/django.po | 134 ++++++++++++ .../locale/nl_NL/LC_MESSAGES/django.po | 134 ++++++++++++ .../locale/pt_BR/LC_MESSAGES/django.po | 134 ++++++++++++ .../locale/sl_SI/LC_MESSAGES/django.po | 135 ++++++++++++ .../locale/sv/LC_MESSAGES/django.po | 134 ++++++++++++ modoboa/imap_migration/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../commands/generate_offlineimap_config.py | 44 ++++ .../imap_migration/migrations/0001_initial.py | 25 +++ .../migrations/0002_password.py | 19 ++ .../migrations/0003_auto_20190207_0919.py | 81 +++++++ .../migrations/0004_auto_20190207_0939.py | 26 +++ .../migrations/0005_auto_20210401_0938.py | 24 +++ modoboa/imap_migration/migrations/__init__.py | 0 modoboa/imap_migration/models.py | 55 +++++ .../templates/imap_migration/index.html | 27 +++ .../templates/imap_migration/offlineimap.conf | 40 ++++ modoboa/imap_migration/tests.py | 204 ++++++++++++++++++ modoboa/imap_migration/urls.py | 11 + modoboa/imap_migration/views.py | 19 ++ modoboa/parameters/api/v2/tests.py | 2 +- modoboa/urls.py | 1 + modoboa/urls_api_v1.py | 1 + modoboa/urls_api_v2.py | 3 +- test_project/test_project/settings.py | 2 + 66 files changed, 3802 insertions(+), 8 deletions(-) create mode 100644 frontend/src/api/imap_migration/migrations.js create mode 100644 frontend/src/api/imap_migration/providers.js create mode 100644 frontend/src/components/imap_migration/MigrationsList.vue create mode 100644 frontend/src/components/imap_migration/ProviderAssociatedForm.vue create mode 100644 frontend/src/components/imap_migration/ProviderCreationForm.vue create mode 100644 frontend/src/components/imap_migration/ProviderForm.vue create mode 100644 frontend/src/components/imap_migration/ProviderGeneralForm.vue create mode 100644 frontend/src/components/imap_migration/ProvidersList.vue create mode 100644 frontend/src/components/tools/DomainField.vue create mode 100644 frontend/src/store/modules/providers.js create mode 100644 frontend/src/views/imap_migration/ProviderEdit.vue create mode 100644 frontend/src/views/imap_migration/Providers.vue create mode 100644 modoboa/imap_migration/__init__.py create mode 100644 modoboa/imap_migration/api/__init__.py create mode 100644 modoboa/imap_migration/api/v1/__init__.py create mode 100644 modoboa/imap_migration/api/v1/serializers.py create mode 100644 modoboa/imap_migration/api/v1/urls.py create mode 100644 modoboa/imap_migration/api/v1/viewsets.py create mode 100644 modoboa/imap_migration/api/v2/__init__.py create mode 100644 modoboa/imap_migration/api/v2/serializers.py create mode 100644 modoboa/imap_migration/api/v2/urls.py create mode 100644 modoboa/imap_migration/api/v2/viewsets.py create mode 100644 modoboa/imap_migration/app_settings.py create mode 100644 modoboa/imap_migration/apps.py create mode 100644 modoboa/imap_migration/auth_backends.py create mode 100644 modoboa/imap_migration/checks.py create mode 100644 modoboa/imap_migration/factories.py create mode 100644 modoboa/imap_migration/handlers.py create mode 100644 modoboa/imap_migration/locale/cs_CZ/LC_MESSAGES/django.po create mode 100644 modoboa/imap_migration/locale/de/LC_MESSAGES/django.po create mode 100644 modoboa/imap_migration/locale/el_GR/LC_MESSAGES/django.po create mode 100644 modoboa/imap_migration/locale/en/LC_MESSAGES/django.po create mode 100644 modoboa/imap_migration/locale/fr/LC_MESSAGES/django.po create mode 100644 modoboa/imap_migration/locale/it/LC_MESSAGES/django.po create mode 100644 modoboa/imap_migration/locale/ja_JP/LC_MESSAGES/django.po create mode 100644 modoboa/imap_migration/locale/nl_NL/LC_MESSAGES/django.po create mode 100644 modoboa/imap_migration/locale/pt_BR/LC_MESSAGES/django.po create mode 100644 modoboa/imap_migration/locale/sl_SI/LC_MESSAGES/django.po create mode 100644 modoboa/imap_migration/locale/sv/LC_MESSAGES/django.po create mode 100644 modoboa/imap_migration/management/__init__.py create mode 100644 modoboa/imap_migration/management/commands/__init__.py create mode 100644 modoboa/imap_migration/management/commands/generate_offlineimap_config.py create mode 100644 modoboa/imap_migration/migrations/0001_initial.py create mode 100644 modoboa/imap_migration/migrations/0002_password.py create mode 100644 modoboa/imap_migration/migrations/0003_auto_20190207_0919.py create mode 100644 modoboa/imap_migration/migrations/0004_auto_20190207_0939.py create mode 100644 modoboa/imap_migration/migrations/0005_auto_20210401_0938.py create mode 100644 modoboa/imap_migration/migrations/__init__.py create mode 100644 modoboa/imap_migration/models.py create mode 100644 modoboa/imap_migration/templates/imap_migration/index.html create mode 100644 modoboa/imap_migration/templates/imap_migration/offlineimap.conf create mode 100644 modoboa/imap_migration/tests.py create mode 100644 modoboa/imap_migration/urls.py create mode 100644 modoboa/imap_migration/views.py diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 618c4b87e..cb3db4ee8 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -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 diff --git a/frontend/Makefile b/frontend/Makefile index 2b35d21f4..1e98a11ff 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -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; diff --git a/frontend/src/api/imap_migration/migrations.js b/frontend/src/api/imap_migration/migrations.js new file mode 100644 index 000000000..82f8ace12 --- /dev/null +++ b/frontend/src/api/imap_migration/migrations.js @@ -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}/`) + } +} diff --git a/frontend/src/api/imap_migration/providers.js b/frontend/src/api/imap_migration/providers.js new file mode 100644 index 000000000..e191d6814 --- /dev/null +++ b/frontend/src/api/imap_migration/providers.js @@ -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) + } +} diff --git a/frontend/src/components/imap_migration/MigrationsList.vue b/frontend/src/components/imap_migration/MigrationsList.vue new file mode 100644 index 000000000..879680551 --- /dev/null +++ b/frontend/src/components/imap_migration/MigrationsList.vue @@ -0,0 +1,118 @@ + + + diff --git a/frontend/src/components/imap_migration/ProviderAssociatedForm.vue b/frontend/src/components/imap_migration/ProviderAssociatedForm.vue new file mode 100644 index 000000000..527e4a8e0 --- /dev/null +++ b/frontend/src/components/imap_migration/ProviderAssociatedForm.vue @@ -0,0 +1,117 @@ + + + diff --git a/frontend/src/components/imap_migration/ProviderCreationForm.vue b/frontend/src/components/imap_migration/ProviderCreationForm.vue new file mode 100644 index 000000000..d955d2f22 --- /dev/null +++ b/frontend/src/components/imap_migration/ProviderCreationForm.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/frontend/src/components/imap_migration/ProviderForm.vue b/frontend/src/components/imap_migration/ProviderForm.vue new file mode 100644 index 000000000..ac7ed36f0 --- /dev/null +++ b/frontend/src/components/imap_migration/ProviderForm.vue @@ -0,0 +1,138 @@ + + + diff --git a/frontend/src/components/imap_migration/ProviderGeneralForm.vue b/frontend/src/components/imap_migration/ProviderGeneralForm.vue new file mode 100644 index 000000000..56402d56f --- /dev/null +++ b/frontend/src/components/imap_migration/ProviderGeneralForm.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/frontend/src/components/imap_migration/ProvidersList.vue b/frontend/src/components/imap_migration/ProvidersList.vue new file mode 100644 index 000000000..57f2e7058 --- /dev/null +++ b/frontend/src/components/imap_migration/ProvidersList.vue @@ -0,0 +1,154 @@ + + + diff --git a/frontend/src/components/layout/Navbar.vue b/frontend/src/components/layout/Navbar.vue index 4c4ddc9a7..9702afb95 100644 --- a/frontend/src/components/layout/Navbar.vue +++ b/frontend/src/components/layout/Navbar.vue @@ -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 } } }) diff --git a/frontend/src/components/tools/DomainField.vue b/frontend/src/components/tools/DomainField.vue new file mode 100644 index 000000000..af8e294fa --- /dev/null +++ b/frontend/src/components/tools/DomainField.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/frontend/src/plugins/vee-validate.js b/frontend/src/plugins/vee-validate.js index df8416106..3d753f006 100644 --- a/frontend/src/plugins/vee-validate.js +++ b/frontend/src/plugins/vee-validate.js @@ -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) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 49e554976..2c42d97ef 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -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', diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index ae203cd8b..f4241e4b1 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -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' }) diff --git a/frontend/src/store/modules/providers.js b/frontend/src/store/modules/providers.js new file mode 100644 index 000000000..127c42bb5 --- /dev/null +++ b/frontend/src/store/modules/providers.js @@ -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 +} diff --git a/frontend/src/views/imap_migration/ProviderEdit.vue b/frontend/src/views/imap_migration/ProviderEdit.vue new file mode 100644 index 000000000..518b9b665 --- /dev/null +++ b/frontend/src/views/imap_migration/ProviderEdit.vue @@ -0,0 +1,28 @@ + + + diff --git a/frontend/src/views/imap_migration/Providers.vue b/frontend/src/views/imap_migration/Providers.vue new file mode 100644 index 000000000..f229388c2 --- /dev/null +++ b/frontend/src/views/imap_migration/Providers.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index cef5eb7a5..6107ff4bf 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -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 diff --git a/modoboa/imap_migration/__init__.py b/modoboa/imap_migration/__init__.py new file mode 100644 index 000000000..62d56a35c --- /dev/null +++ b/modoboa/imap_migration/__init__.py @@ -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" diff --git a/modoboa/imap_migration/api/__init__.py b/modoboa/imap_migration/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/imap_migration/api/v1/__init__.py b/modoboa/imap_migration/api/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/imap_migration/api/v1/serializers.py b/modoboa/imap_migration/api/v1/serializers.py new file mode 100644 index 000000000..bf0cc6a60 --- /dev/null +++ b/modoboa/imap_migration/api/v1/serializers.py @@ -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 diff --git a/modoboa/imap_migration/api/v1/urls.py b/modoboa/imap_migration/api/v1/urls.py new file mode 100644 index 000000000..c1b429749 --- /dev/null +++ b/modoboa/imap_migration/api/v1/urls.py @@ -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 diff --git a/modoboa/imap_migration/api/v1/viewsets.py b/modoboa/imap_migration/api/v1/viewsets.py new file mode 100644 index 000000000..2a1bfeaf1 --- /dev/null +++ b/modoboa/imap_migration/api/v1/viewsets.py @@ -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 diff --git a/modoboa/imap_migration/api/v2/__init__.py b/modoboa/imap_migration/api/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/imap_migration/api/v2/serializers.py b/modoboa/imap_migration/api/v2/serializers.py new file mode 100644 index 000000000..6cd56021a --- /dev/null +++ b/modoboa/imap_migration/api/v2/serializers.py @@ -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() diff --git a/modoboa/imap_migration/api/v2/urls.py b/modoboa/imap_migration/api/v2/urls.py new file mode 100644 index 000000000..8c7f57af2 --- /dev/null +++ b/modoboa/imap_migration/api/v2/urls.py @@ -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 diff --git a/modoboa/imap_migration/api/v2/viewsets.py b/modoboa/imap_migration/api/v2/viewsets.py new file mode 100644 index 000000000..1eea06b81 --- /dev/null +++ b/modoboa/imap_migration/api/v2/viewsets.py @@ -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) diff --git a/modoboa/imap_migration/app_settings.py b/modoboa/imap_migration/app_settings.py new file mode 100644 index 000000000..c4c3a85df --- /dev/null +++ b/modoboa/imap_migration/app_settings.py @@ -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 " + ) + ) diff --git a/modoboa/imap_migration/apps.py b/modoboa/imap_migration/apps.py new file mode 100644 index 000000000..55a125506 --- /dev/null +++ b/modoboa/imap_migration/apps.py @@ -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() diff --git a/modoboa/imap_migration/auth_backends.py b/modoboa/imap_migration/auth_backends.py new file mode 100644 index 000000000..d87276498 --- /dev/null +++ b/modoboa/imap_migration/auth_backends.py @@ -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 diff --git a/modoboa/imap_migration/checks.py b/modoboa/imap_migration/checks.py new file mode 100644 index 000000000..6f20e160b --- /dev/null +++ b/modoboa/imap_migration/checks.py @@ -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 diff --git a/modoboa/imap_migration/factories.py b/modoboa/imap_migration/factories.py new file mode 100644 index 000000000..f4c8f12f8 --- /dev/null +++ b/modoboa/imap_migration/factories.py @@ -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) diff --git a/modoboa/imap_migration/handlers.py b/modoboa/imap_migration/handlers.py new file mode 100644 index 000000000..7914af083 --- /dev/null +++ b/modoboa/imap_migration/handlers.py @@ -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")} + ] diff --git a/modoboa/imap_migration/locale/cs_CZ/LC_MESSAGES/django.po b/modoboa/imap_migration/locale/cs_CZ/LC_MESSAGES/django.po new file mode 100644 index 000000000..ab5cc4a1b --- /dev/null +++ b/modoboa/imap_migration/locale/cs_CZ/LC_MESSAGES/django.po @@ -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" diff --git a/modoboa/imap_migration/locale/de/LC_MESSAGES/django.po b/modoboa/imap_migration/locale/de/LC_MESSAGES/django.po new file mode 100644 index 000000000..a85c2ea73 --- /dev/null +++ b/modoboa/imap_migration/locale/de/LC_MESSAGES/django.po @@ -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" diff --git a/modoboa/imap_migration/locale/el_GR/LC_MESSAGES/django.po b/modoboa/imap_migration/locale/el_GR/LC_MESSAGES/django.po new file mode 100644 index 000000000..85a6c2676 --- /dev/null +++ b/modoboa/imap_migration/locale/el_GR/LC_MESSAGES/django.po @@ -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 , 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 \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 "Η μετάβαση ακυρώθηκε με επιτυχία" diff --git a/modoboa/imap_migration/locale/en/LC_MESSAGES/django.po b/modoboa/imap_migration/locale/en/LC_MESSAGES/django.po new file mode 100644 index 000000000..dea4ac49b --- /dev/null +++ b/modoboa/imap_migration/locale/en/LC_MESSAGES/django.po @@ -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 , 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 \n" +"Language-Team: LANGUAGE \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 "" diff --git a/modoboa/imap_migration/locale/fr/LC_MESSAGES/django.po b/modoboa/imap_migration/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 000000000..b2b4ac788 --- /dev/null +++ b/modoboa/imap_migration/locale/fr/LC_MESSAGES/django.po @@ -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 , 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 \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" diff --git a/modoboa/imap_migration/locale/it/LC_MESSAGES/django.po b/modoboa/imap_migration/locale/it/LC_MESSAGES/django.po new file mode 100644 index 000000000..87ff54baa --- /dev/null +++ b/modoboa/imap_migration/locale/it/LC_MESSAGES/django.po @@ -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 , 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 \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" diff --git a/modoboa/imap_migration/locale/ja_JP/LC_MESSAGES/django.po b/modoboa/imap_migration/locale/ja_JP/LC_MESSAGES/django.po new file mode 100644 index 000000000..9c0f26023 --- /dev/null +++ b/modoboa/imap_migration/locale/ja_JP/LC_MESSAGES/django.po @@ -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 , 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 \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 "マイグレーションのキャンセルに成功しました" diff --git a/modoboa/imap_migration/locale/nl_NL/LC_MESSAGES/django.po b/modoboa/imap_migration/locale/nl_NL/LC_MESSAGES/django.po new file mode 100644 index 000000000..14dc0ac9e --- /dev/null +++ b/modoboa/imap_migration/locale/nl_NL/LC_MESSAGES/django.po @@ -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. , 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. \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" diff --git a/modoboa/imap_migration/locale/pt_BR/LC_MESSAGES/django.po b/modoboa/imap_migration/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 000000000..b7e5b5581 --- /dev/null +++ b/modoboa/imap_migration/locale/pt_BR/LC_MESSAGES/django.po @@ -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 , 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 \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" diff --git a/modoboa/imap_migration/locale/sl_SI/LC_MESSAGES/django.po b/modoboa/imap_migration/locale/sl_SI/LC_MESSAGES/django.po new file mode 100644 index 000000000..657fc2707 --- /dev/null +++ b/modoboa/imap_migration/locale/sl_SI/LC_MESSAGES/django.po @@ -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 , 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 \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" diff --git a/modoboa/imap_migration/locale/sv/LC_MESSAGES/django.po b/modoboa/imap_migration/locale/sv/LC_MESSAGES/django.po new file mode 100644 index 000000000..3594d8f64 --- /dev/null +++ b/modoboa/imap_migration/locale/sv/LC_MESSAGES/django.po @@ -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 , 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 \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" diff --git a/modoboa/imap_migration/management/__init__.py b/modoboa/imap_migration/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/imap_migration/management/commands/__init__.py b/modoboa/imap_migration/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/imap_migration/management/commands/generate_offlineimap_config.py b/modoboa/imap_migration/management/commands/generate_offlineimap_config.py new file mode 100644 index 000000000..6e3cdc90a --- /dev/null +++ b/modoboa/imap_migration/management/commands/generate_offlineimap_config.py @@ -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) diff --git a/modoboa/imap_migration/migrations/0001_initial.py b/modoboa/imap_migration/migrations/0001_initial.py new file mode 100644 index 000000000..416ae4c91 --- /dev/null +++ b/modoboa/imap_migration/migrations/0001_initial.py @@ -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,), + ), + ] diff --git a/modoboa/imap_migration/migrations/0002_password.py b/modoboa/imap_migration/migrations/0002_password.py new file mode 100644 index 000000000..83c455926 --- /dev/null +++ b/modoboa/imap_migration/migrations/0002_password.py @@ -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), + ), + ] diff --git a/modoboa/imap_migration/migrations/0003_auto_20190207_0919.py b/modoboa/imap_migration/migrations/0003_auto_20190207_0919.py new file mode 100644 index 000000000..eb0867b77 --- /dev/null +++ b/modoboa/imap_migration/migrations/0003_auto_20190207_0919.py @@ -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) + ] diff --git a/modoboa/imap_migration/migrations/0004_auto_20190207_0939.py b/modoboa/imap_migration/migrations/0004_auto_20190207_0939.py new file mode 100644 index 000000000..4b1f5f59b --- /dev/null +++ b/modoboa/imap_migration/migrations/0004_auto_20190207_0939.py @@ -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), + ), + ] diff --git a/modoboa/imap_migration/migrations/0005_auto_20210401_0938.py b/modoboa/imap_migration/migrations/0005_auto_20210401_0938.py new file mode 100644 index 000000000..f49ac7383 --- /dev/null +++ b/modoboa/imap_migration/migrations/0005_auto_20210401_0938.py @@ -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), + ), + ] diff --git a/modoboa/imap_migration/migrations/__init__.py b/modoboa/imap_migration/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/imap_migration/models.py b/modoboa/imap_migration/models.py new file mode 100644 index 000000000..d3660acfa --- /dev/null +++ b/modoboa/imap_migration/models.py @@ -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) diff --git a/modoboa/imap_migration/templates/imap_migration/index.html b/modoboa/imap_migration/templates/imap_migration/index.html new file mode 100644 index 000000000..2ce37d2f9 --- /dev/null +++ b/modoboa/imap_migration/templates/imap_migration/index.html @@ -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 %} + +
+
+ +{% endblock %} diff --git a/modoboa/imap_migration/templates/imap_migration/offlineimap.conf b/modoboa/imap_migration/templates/imap_migration/offlineimap.conf new file mode 100644 index 000000000..e6db6e1a5 --- /dev/null +++ b/modoboa/imap_migration/templates/imap_migration/offlineimap.conf @@ -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 %} diff --git a/modoboa/imap_migration/tests.py b/modoboa/imap_migration/tests.py new file mode 100644 index 000000000..57128d4ad --- /dev/null +++ b/modoboa/imap_migration/tests.py @@ -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, '
') + + +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] + ) diff --git a/modoboa/imap_migration/urls.py b/modoboa/imap_migration/urls.py new file mode 100644 index 000000000..3c51b274d --- /dev/null +++ b/modoboa/imap_migration/urls.py @@ -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"), +] diff --git a/modoboa/imap_migration/views.py b/modoboa/imap_migration/views.py new file mode 100644 index 000000000..d50cf61d3 --- /dev/null +++ b/modoboa/imap_migration/views.py @@ -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) diff --git a/modoboa/parameters/api/v2/tests.py b/modoboa/parameters/api/v2/tests.py index 60d663fa2..a4ffc6e3c 100644 --- a/modoboa/parameters/api/v2/tests.py +++ b/modoboa/parameters/api/v2/tests.py @@ -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") diff --git a/modoboa/urls.py b/modoboa/urls.py index d30c04bc9..9eaee8196 100644 --- a/modoboa/urls.py +++ b/modoboa/urls.py @@ -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"), diff --git a/modoboa/urls_api_v1.py b/modoboa/urls_api_v1.py index 7697ba96b..2077cec23 100644 --- a/modoboa/urls_api_v1.py +++ b/modoboa/urls_api_v1.py @@ -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")) ] diff --git a/modoboa/urls_api_v2.py b/modoboa/urls_api_v2.py index 4e95c8350..91ab85b39 100644 --- a/modoboa/urls_api_v2.py +++ b/modoboa/urls_api_v2.py @@ -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") diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py index 1f7e5aa6c..e742d6931 100644 --- a/test_project/test_project/settings.py +++ b/test_project/test_project/settings.py @@ -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 From 76c99094fd7c87475867aeba2095ce4aad21bd6a Mon Sep 17 00:00:00 2001 From: Spitap Date: Mon, 13 Mar 2023 16:57:16 +0100 Subject: [PATCH 02/12] Updated favicon, edited settings logic, specify type of parameters --- frontend/public/favicon.ico | Bin 4286 -> 10127 bytes frontend/src/components/layout/BaseNavBar.vue | 149 ++++++++++++++ .../src/components/layout/ExtensionNavBar.vue | 154 ++++++++++++++ frontend/src/components/layout/Navbar.vue | 192 ++++-------------- .../src/components/layout/SettingsDrawer.vue | 86 ++++++++ .../components/parameters/ParametersForm.vue | 3 + modoboa/dmarc/forms.py | 3 +- modoboa/imap_migration/apps.py | 3 +- modoboa/parameters/api/v2/serializers.py | 1 + modoboa/parameters/tools.py | 9 +- modoboa/pdfcredentials/apps.py | 3 +- 11 files changed, 449 insertions(+), 154 deletions(-) create mode 100644 frontend/src/components/layout/BaseNavBar.vue create mode 100644 frontend/src/components/layout/ExtensionNavBar.vue create mode 100644 frontend/src/components/layout/SettingsDrawer.vue diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico index df36fcfb72584e00488330b560ebcf34a41c64c2..985be6b035613a73a23e7c8fbe7f5ded40f8d7fd 100644 GIT binary patch literal 10127 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 ZJBglrZg&ir=q+`ChKjCo-80*${{W0w6BGad literal 4286 zcmds*O-Phc6o&64GDVCEQHxsW(p4>LW*W<827=Unuo8sGpRux(DN@jWP-e29Wl%wj zY84_aq9}^Am9-cWTD5GGEo#+5Fi2wX_P*bo+xO!)p*7B;iKlbFd(U~_d(U?#hLj56 zPhFkj-|A6~Qk#@g^#D^U0XT1cu=c-vu1+SElX9NR;kzAUV(q0|dl0|%h|dI$%VICy zJnu2^L*Te9JrJMGh%-P79CL0}dq92RGU6gI{v2~|)p}sG5x0U*z<8U;Ij*hB9z?ei z@g6Xq-pDoPl=MANPiR7%172VA%r)kevtV-_5H*QJKFmd;8yA$98zCxBZYXTNZ#QFk2(TX0;Y2dt&WitL#$96|gJY=3xX zpCoi|YNzgO3R`f@IiEeSmKrPSf#h#Qd<$%Ej^RIeeYfsxhPMOG`S`Pz8q``=511zm zAm)MX5AV^5xIWPyEu7u>qYs?pn$I4nL9J!=K=SGlKLXpE<5x+2cDTXq?brj?n6sp= zphe9;_JHf40^9~}9i08r{XM$7HB!`{Ys~TK0kx<}ZQng`UPvH*11|q7&l9?@FQz;8 zx!=3<4seY*%=OlbCbcae?5^V_}*K>Uo6ZWV8mTyE^B=DKy7-sdLYkR5Z?paTgK-zyIkKjIcpyO z{+uIt&YSa_$QnN_@t~L014dyK(fOOo+W*MIxbA6Ndgr=Y!f#Tokqv}n<7-9qfHkc3 z=>a|HWqcX8fzQCT=dqVbogRq!-S>H%yA{1w#2Pn;=e>JiEj7Hl;zdt-2f+j2%DeVD zsW0Ab)ZK@0cIW%W7z}H{&~yGhn~D;aiP4=;m-HCo`BEI+Kd6 z={Xwx{TKxD#iCLfl2vQGDitKtN>z|-AdCN|$jTFDg0m3O`WLD4_s#$S diff --git a/frontend/src/components/layout/BaseNavBar.vue b/frontend/src/components/layout/BaseNavBar.vue new file mode 100644 index 000000000..4b5221654 --- /dev/null +++ b/frontend/src/components/layout/BaseNavBar.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/frontend/src/components/layout/ExtensionNavBar.vue b/frontend/src/components/layout/ExtensionNavBar.vue new file mode 100644 index 000000000..7ee554ef0 --- /dev/null +++ b/frontend/src/components/layout/ExtensionNavBar.vue @@ -0,0 +1,154 @@ + + + + + diff --git a/frontend/src/components/layout/Navbar.vue b/frontend/src/components/layout/Navbar.vue index 9702afb95..3fbd6637e 100644 --- a/frontend/src/components/layout/Navbar.vue +++ b/frontend/src/components/layout/Navbar.vue @@ -21,64 +21,33 @@ mdi-chevron-left
- - - - + + + Core + + + Extensions + + + +