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