From d2ef09b7f2aca69a57f3f1335161c3e34b036cba Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Wed, 27 Mar 2024 14:52:58 +0100 Subject: [PATCH] WIP: sieve filters --- .pre-commit-config.yaml | 2 +- docker-compose.yml | 10 + docker/dovecot_config/auth-custom.conf | 12 + docker/dovecot_config/dovecot-oauth2.conf.ext | 6 + frontend/package.json | 1 + frontend/src/api/account.js | 21 + .../src/components/account/FilterForm.vue | 275 ++++++++++++ .../src/components/account/FilterSetForm.vue | 62 +++ frontend/src/layouts/dashboard/NavBar.vue | 5 + frontend/src/router/index.js | 26 +- frontend/src/stores/auth.store.js | 48 ++- frontend/src/views/account/FiltersView.vue | 165 ++++++++ frontend/yarn.lock | 12 + .../core/commands/templates/settings.py.tpl | 2 + modoboa/sievefilters/__init__.py | 16 + modoboa/sievefilters/api/__init__.py | 0 modoboa/sievefilters/api/v2/__init__.py | 0 modoboa/sievefilters/api/v2/serializers.py | 145 +++++++ modoboa/sievefilters/api/v2/urls.py | 10 + modoboa/sievefilters/api/v2/viewsets.py | 89 ++++ modoboa/sievefilters/apps.py | 27 ++ modoboa/sievefilters/constants.py | 72 ++++ modoboa/sievefilters/forms.py | 395 ++++++++++++++++++ modoboa/sievefilters/handlers.py | 39 ++ modoboa/sievefilters/imaputils.py | 246 +++++++++++ modoboa/sievefilters/lib.py | 115 +++++ .../locale/cs_CZ/LC_MESSAGES/django.po | 364 ++++++++++++++++ .../locale/cs_CZ/LC_MESSAGES/djangojs.po | 40 ++ .../locale/de/LC_MESSAGES/django.po | 368 ++++++++++++++++ .../locale/de/LC_MESSAGES/djangojs.po | 42 ++ .../locale/el_GR/LC_MESSAGES/django.po | 368 ++++++++++++++++ .../locale/el_GR/LC_MESSAGES/djangojs.po | 40 ++ .../locale/en/LC_MESSAGES/django.po | 362 ++++++++++++++++ .../locale/en/LC_MESSAGES/djangojs.po | 34 ++ .../locale/es/LC_MESSAGES/django.po | 364 ++++++++++++++++ .../locale/es/LC_MESSAGES/djangojs.po | 41 ++ .../locale/fr/LC_MESSAGES/django.po | 366 ++++++++++++++++ .../locale/fr/LC_MESSAGES/djangojs.po | 39 ++ .../locale/it/LC_MESSAGES/django.po | 365 ++++++++++++++++ .../locale/it/LC_MESSAGES/djangojs.po | 39 ++ .../locale/nl_NL/LC_MESSAGES/django.po | 380 +++++++++++++++++ .../locale/nl_NL/LC_MESSAGES/djangojs.po | 41 ++ .../locale/pl_PL/LC_MESSAGES/django.po | 365 ++++++++++++++++ .../locale/pl_PL/LC_MESSAGES/djangojs.po | 41 ++ .../locale/pt_BR/LC_MESSAGES/django.po | 367 ++++++++++++++++ .../locale/pt_BR/LC_MESSAGES/djangojs.po | 40 ++ .../locale/pt_PT/LC_MESSAGES/django.po | 381 +++++++++++++++++ .../locale/pt_PT/LC_MESSAGES/djangojs.po | 42 ++ .../locale/ru/LC_MESSAGES/django.po | 364 ++++++++++++++++ .../locale/ru/LC_MESSAGES/djangojs.po | 39 ++ .../locale/sv/LC_MESSAGES/django.po | 364 ++++++++++++++++ .../locale/sv/LC_MESSAGES/djangojs.po | 40 ++ modoboa/sievefilters/mocks.py | 91 ++++ modoboa/sievefilters/rfc6266.py | 108 +++++ .../static/sievefilters/css/style.css | 134 ++++++ .../sievefilters/js/jquery.dynfields.js | 362 ++++++++++++++++ .../static/sievefilters/js/lib.js | 247 +++++++++++ .../templates/sievefilters/filter.html | 26 ++ .../templates/sievefilters/guieditor.html | 25 ++ .../templates/sievefilters/index.html | 104 +++++ .../templates/sievefilters/rawfilter.html | 4 + modoboa/sievefilters/templatetags/__init__.py | 0 .../templatetags/sfilters_tags.py | 213 ++++++++++ modoboa/sievefilters/tests.py | 165 ++++++++ modoboa/sievefilters/urls.py | 42 ++ modoboa/sievefilters/views.py | 326 +++++++++++++++ modoboa/urls.py | 2 + modoboa/urls_api_v2.py | 1 + requirements.txt | 4 + test_project/test_project/settings.py | 15 +- 70 files changed, 8939 insertions(+), 27 deletions(-) create mode 100644 docker/dovecot_config/auth-custom.conf create mode 100644 docker/dovecot_config/dovecot-oauth2.conf.ext create mode 100644 frontend/src/components/account/FilterForm.vue create mode 100644 frontend/src/components/account/FilterSetForm.vue create mode 100644 frontend/src/views/account/FiltersView.vue create mode 100644 modoboa/sievefilters/__init__.py create mode 100644 modoboa/sievefilters/api/__init__.py create mode 100644 modoboa/sievefilters/api/v2/__init__.py create mode 100644 modoboa/sievefilters/api/v2/serializers.py create mode 100644 modoboa/sievefilters/api/v2/urls.py create mode 100644 modoboa/sievefilters/api/v2/viewsets.py create mode 100644 modoboa/sievefilters/apps.py create mode 100644 modoboa/sievefilters/constants.py create mode 100644 modoboa/sievefilters/forms.py create mode 100644 modoboa/sievefilters/handlers.py create mode 100644 modoboa/sievefilters/imaputils.py create mode 100644 modoboa/sievefilters/lib.py create mode 100644 modoboa/sievefilters/locale/cs_CZ/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/cs_CZ/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/locale/de/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/de/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/locale/el_GR/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/el_GR/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/locale/en/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/en/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/locale/es/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/es/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/locale/fr/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/fr/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/locale/it/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/it/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/locale/nl_NL/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/nl_NL/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/locale/pl_PL/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/pl_PL/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/locale/pt_BR/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/pt_BR/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/locale/pt_PT/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/pt_PT/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/locale/ru/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/ru/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/locale/sv/LC_MESSAGES/django.po create mode 100644 modoboa/sievefilters/locale/sv/LC_MESSAGES/djangojs.po create mode 100644 modoboa/sievefilters/mocks.py create mode 100644 modoboa/sievefilters/rfc6266.py create mode 100644 modoboa/sievefilters/static/sievefilters/css/style.css create mode 100644 modoboa/sievefilters/static/sievefilters/js/jquery.dynfields.js create mode 100644 modoboa/sievefilters/static/sievefilters/js/lib.js create mode 100644 modoboa/sievefilters/templates/sievefilters/filter.html create mode 100644 modoboa/sievefilters/templates/sievefilters/guieditor.html create mode 100644 modoboa/sievefilters/templates/sievefilters/index.html create mode 100644 modoboa/sievefilters/templates/sievefilters/rawfilter.html create mode 100644 modoboa/sievefilters/templatetags/__init__.py create mode 100644 modoboa/sievefilters/templatetags/sfilters_tags.py create mode 100644 modoboa/sievefilters/tests.py create mode 100644 modoboa/sievefilters/urls.py create mode 100644 modoboa/sievefilters/views.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8976ec645..cbc9bca12 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: name: eslint language: node types: [javascript] - entry: eslint --fix + entry: eslint -c frontend/.eslintrc.js --ignore-pattern modoboa/* --fix additional_dependencies: ["eslint-plugin-vue"] - repo: https://github.com/psf/black rev: "24.2.0" diff --git a/docker-compose.yml b/docker-compose.yml index 649b68a76..20c29f2f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,6 +41,7 @@ services: python3 manage.py runserver 0:8000" volumes: - .:/code + - ../sievelib/sievelib:/opt/venv/lib/python3.12/site-packages/sievelib front: container_name: modoboa_front @@ -57,3 +58,12 @@ services: DOCKER: "yes" depends_on: - api + + dovecot: + container_name: modoboa_dovecot + restart: unless-stopped + image: dovecot/dovecot:latest + depends_on: + - api + volumes: + - ./docker/dovecot_config:/etc/dovecot/conf.d diff --git a/docker/dovecot_config/auth-custom.conf b/docker/dovecot_config/auth-custom.conf new file mode 100644 index 000000000..923b0f36c --- /dev/null +++ b/docker/dovecot_config/auth-custom.conf @@ -0,0 +1,12 @@ +auth_mechanisms = oauthbearer xoauth2 + +# authentication debug logging +auth_debug = yes +auth_verbose = yes +auth_debug_passwords = yes + +passdb { + driver = oauth2 + mechanisms = xoauth2 oauthbearer + args = /etc/dovecot/conf.d/dovecot-oauth2.conf.ext +} diff --git a/docker/dovecot_config/dovecot-oauth2.conf.ext b/docker/dovecot_config/dovecot-oauth2.conf.ext new file mode 100644 index 000000000..2e5899f04 --- /dev/null +++ b/docker/dovecot_config/dovecot-oauth2.conf.ext @@ -0,0 +1,6 @@ +introspection_mode = post +introspection_url = http://b1r8hUce03fA3OK260MQBgvi96dsnXvdmSDCVoLJ:dX8ZxICUPCVSoOJiyNAWIozf58IjmlUNQ6PPbX8rqD90QAw0te8052PKI82IRzmeCxv1kvHjSFpCOPqwteiFDj3TBq1cxmrn0Do2AlyDWVD1p8lUeiloERNEcj1InXaR@api:8000/api/o/introspect/ +username_attribute = username +tls_ca_cert_file = /etc/ssl/certs/ca-certificates.crt +active_attribute = active +active_value = true diff --git a/frontend/package.json b/frontend/package.json index 284e351f3..ce4e699f9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "debounce": "^2.0.0", "js-cookie": "^3.0.5", "luxon": "^3.4.4", + "oidc-client-ts": "^3.0.1", "pinia": "^2.1.7", "qrcode.vue": "^3.4.1", "roboto-fontface": "*", diff --git a/frontend/src/api/account.js b/frontend/src/api/account.js index 962002c8a..c0e4dde1d 100644 --- a/frontend/src/api/account.js +++ b/frontend/src/api/account.js @@ -50,4 +50,25 @@ export default { setARMessage(data) { return repository.put(`${resource}/armessage/`, data) }, + getFilterSets() { + return repository.get(`${resource}/filtersets/`) + }, + createFilterSet(data) { + return repository.post(`${resource}/filtersets/`, data) + }, + getFilterConditionTemplates() { + return repository.get(`${resource}/filtersets/condition_templates/`) + }, + getFilterActionTemplates() { + return repository.get(`${resource}/filtersets/action_templates/`) + }, + getFilters(filterSetName) { + return repository.get(`${resource}/filtersets/${filterSetName}/filters/`) + }, + createFilter(filterSetName, data) { + return repository.post( + `${resource}/filtersets/${filterSetName}/filters/`, + data + ) + }, } diff --git a/frontend/src/components/account/FilterForm.vue b/frontend/src/components/account/FilterForm.vue new file mode 100644 index 000000000..a8e336007 --- /dev/null +++ b/frontend/src/components/account/FilterForm.vue @@ -0,0 +1,275 @@ + + + diff --git a/frontend/src/components/account/FilterSetForm.vue b/frontend/src/components/account/FilterSetForm.vue new file mode 100644 index 000000000..43c69e266 --- /dev/null +++ b/frontend/src/components/account/FilterSetForm.vue @@ -0,0 +1,62 @@ + + + diff --git a/frontend/src/layouts/dashboard/NavBar.vue b/frontend/src/layouts/dashboard/NavBar.vue index d7a2abc21..fec5599d7 100644 --- a/frontend/src/layouts/dashboard/NavBar.vue +++ b/frontend/src/layouts/dashboard/NavBar.vue @@ -257,6 +257,11 @@ const mainMenuItems = [ ] const userSettingsMenuItems = [ + { + text: $gettext('Filters'), + to: { name: 'AccountFilters' }, + icon: 'mdi-filter' + }, { text: $gettext('Settings'), to: { name: 'AccountSettings' }, diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 92d8e5c5f..9c0612c9e 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -256,6 +256,15 @@ const routes = [ requiresAuth: true, }, children: [ + { + path: 'filters', + name: 'AccountFilters', + component: () => import('@/views/account/FiltersView.vue'), + meta: { + layout: 'account', + requiresAuth: true, + }, + }, { path: ':tab?', name: 'AccountSettings', @@ -283,18 +292,15 @@ router.beforeEach((to, from, next) => { if (to.meta.requiresAuth !== undefined) { const authStore = useAuthStore() authStore.initialize().then(() => { - if (!authStore.isAuthenticated) { - next({ name: 'Login' }) - } else { - /* + authStore.validateAccess() + /* if (to.meta.allowedRoles !== undefined) { - if (to.meta.allowedRoles.indexOf(authStore.authUser.role) === -1) { - next({ name: 'Dashboard' }) - return - } + if (to.meta.allowedRoles.indexOf(authStore.authUser.role) === -1) { + next({ name: 'Dashboard' }) + return + } }*/ - next() - } + next() }) } else { next() diff --git a/frontend/src/stores/auth.store.js b/frontend/src/stores/auth.store.js index fd9942260..09f748451 100644 --- a/frontend/src/stores/auth.store.js +++ b/frontend/src/stores/auth.store.js @@ -3,15 +3,28 @@ import Cookies from 'js-cookie' import { defineStore } from 'pinia' import { computed, ref } from 'vue' import gettext from '@/plugins/gettext' +import { UserManager } from 'oidc-client-ts' import repository from '@/api/repository' import accountApi from '@/api/account' import accountsApi from '@/api/accounts' -import authApi from '@/api/auth' +// import authApi from '@/api/auth' export const useAuthStore = defineStore('auth', () => { const authUser = ref({}) const isAuthenticated = ref(false) + const manager = new UserManager({ + authority: '/api/o', + client_id: 'LVQbfIIX3khWR3nDvix1u9yEGHZUxcx53bhJ7FlD', + // Is client_secret necessary? + client_secret: + 'fXzG7tq23eXrM7RK41pXHRseuRWnxe4gV0p4A4YXJlF9jva7QfsDft1g7ESYU9AXR35QE0lo2Ti696OXsqb9JHLjtrPxaYmgMSRmRwLCce5hVr3FB2QhfSOThFYhcuuR', + metadata: { + authorization_endpoint: '/api/o/authorize/', + token_endpoint: '/api/o/token/', + }, + scope: 'read write', + }) const userHasMailbox = computed(() => { return authUser.value.mailbox !== null @@ -26,31 +39,35 @@ export const useAuthStore = defineStore('auth', () => { } async function initialize() { - const token = Cookies.get('token') - if (!token) { + if (isAuthenticated.value) { return } - repository.defaults.headers.common.Authorization = `Bearer ${token}` + const user = await manager.getUser() + if (!user) { + return + } + repository.defaults.headers.common.Authorization = `Bearer ${user.access_token}` repository.defaults.headers.post['Content-Type'] = 'application/json' return fetchUser() } - async function login(payload) { - const resp = await authApi.requestToken(payload) - const cookiesAttributes = { sameSite: 'strict' } - if (payload.rememberMe) { - cookiesAttributes.expires = 90 + async function validateAccess() { + const user = await manager.getUser() + if (!user || user.expired) { + manager.signinRedirect() } + } - const cookie = Cookies.withAttributes(cookiesAttributes) - cookie.set('token', resp.data.access) - cookie.set('refreshToken', resp.data.refresh) + async function login(payload) { + await manager.signinResourceOwnerCredentials(payload) return initialize() + // const cookiesAttributes = { sameSite: 'strict' } + // if (payload.rememberMe) { + // cookiesAttributes.expires = 90 + // } } async function $reset() { delete repository.defaults.headers.common.Authorization - Cookies.remove('token') - Cookies.remove('refreshToken') authUser.value = {} isAuthenticated.value = false } @@ -85,12 +102,13 @@ export const useAuthStore = defineStore('auth', () => { return { authUser, isAuthenticated, + userHasMailbox, + validateAccess, fetchUser, initialize, login, $reset, updateAccount, finalizeTFASetup, - userHasMailbox, } }) diff --git a/frontend/src/views/account/FiltersView.vue b/frontend/src/views/account/FiltersView.vue new file mode 100644 index 000000000..3fb0fd0c0 --- /dev/null +++ b/frontend/src/views/account/FiltersView.vue @@ -0,0 +1,165 @@ + + + + + diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 23ec7c162..0c6420e74 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1134,6 +1134,11 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== +jwt-decode@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" + integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== + keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" @@ -1254,6 +1259,13 @@ nth-check@^2.1.1: dependencies: boolbase "^1.0.0" +oidc-client-ts@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-3.0.1.tgz#be264fb87c89f74f73863646431c32cd06f5ceb7" + integrity sha512-xX8unZNtmtw3sOz4FPSqDhkLFnxCDsdo2qhFEH2opgWnF/iXMFoYdBQzkwCxAZVgt3FT3DnuBY3k80EZHT0RYg== + dependencies: + jwt-decode "^4.0.0" + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index 0961ce799..96a8b03a4 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -92,6 +92,8 @@ MODOBOA_APPS = ( 'modoboa.pdfcredentials', 'modoboa.dmarc', 'modoboa.imap_migration', + 'modoboa.postfix_autoreply', + 'modoboa.sievefilters', # Modoboa extensions here. {% for extension in extensions %} '{{ extension }}', {% endfor %} diff --git a/modoboa/sievefilters/__init__.py b/modoboa/sievefilters/__init__.py new file mode 100644 index 000000000..15612f649 --- /dev/null +++ b/modoboa/sievefilters/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +"""The sieve filters editor of Modoboa.""" + +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_sievefilters.apps.SieveFiltersConfig" diff --git a/modoboa/sievefilters/api/__init__.py b/modoboa/sievefilters/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/sievefilters/api/v2/__init__.py b/modoboa/sievefilters/api/v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/sievefilters/api/v2/serializers.py b/modoboa/sievefilters/api/v2/serializers.py new file mode 100644 index 000000000..386c0e12f --- /dev/null +++ b/modoboa/sievefilters/api/v2/serializers.py @@ -0,0 +1,145 @@ +"""Sievefilters serializers.""" + +from sievelib import commands + +from rest_framework import serializers + +from modoboa.sievefilters import constants, lib + + +class FilterSetSerializer(serializers.Serializer): + + name = serializers.CharField() + active = serializers.BooleanField(default=False) + + +class ConditionSerializer(serializers.Serializer): + + name = serializers.CharField() + operator = serializers.CharField() + value = serializers.CharField() + + +class ActionSerializer(serializers.Serializer): + + name = serializers.CharField() + args = serializers.DictField(child=serializers.CharField()) + + +class FilterSerializer(serializers.Serializer): + + name = serializers.CharField() + enabled = serializers.BooleanField(default=True) + match_type = serializers.ChoiceField(choices=constants.MATCH_TYPES) + conditions = ConditionSerializer(many=True) + actions = ActionSerializer(many=True) + + @staticmethod + def from_filters(filters) -> "FilterSerializer": + result = [] + for fobj in filters: + test = ( + fobj["content"].children[0] if not fobj["enabled"] else fobj["content"] + ) + item = { + "name": fobj["name"], + "enabled": fobj["enabled"], + "match_type": test["test"].name, + "conditions": [], + "actions": [], + } + for t in test["test"]["tests"]: + print(t) + if isinstance(t, commands.TrueCommand): + item["match_type"] = "all" + item["conditions"] += [ + {name: "Subject", "operator": "contains", value: ""} + ] + break + elif isinstance(t, commands.SizeCommand): + item["conditions"] += [ + { + name: "size", + "operator": t["comparator"][1:], + "value": t["limit"], + } + ] + else: + operator_prefix = "" + if isinstance(t, commands.NotCommand): + t = t["test"] + operator_prefix = "not" + item["conditions"] += [ + { + "name": t["header-names"].strip('"'), + "operator": "{}{}".format( + operator_prefix, t["match-type"][1:] + ), + "value": t["key-list"].strip('"'), + } + ] + for c in test.children: + action = {"name": c.name, "args": {}} + tpl = lib.find_action_template(c.name) + for argtpl in tpl.get("args", []): + if argtpl["name"] not in c: + continue + value = c[argtpl["name"]] + if argtpl["type"] == "boolean": + value = True if value else False + else: + value = value.strip('"') + action["args"][argtpl["name"]] = value + item["actions"] += [action] + result.append(item) + return FilterSerializer(result, many=True) + + def to_filter(self): + """Convert serializer data to filter.""" + conditions = [] + actions = [] + for condition in self.validated_data["conditions"]: + conditions.append( + [condition["name"], f":{condition['operator']}", condition["value"]] + ) + for input_action in self.validated_data["actions"]: + action = [input_action["name"]] + tpl = lib.find_action_template(input_action["name"]) + if "args_order" in tpl: + for name in tpl["args_order"]: + if name in input_action["args"]: + action.append(input_action["args"][name]) + else: + action += [arg for arg in input_action["args"].values()] + actions.append(action) + return (conditions, actions) + + +class OperatorSerializer(serializers.Serializer): + + name = serializers.CharField() + label = serializers.CharField() + type = serializers.CharField() + + +class ConditionTemplateSerializer(serializers.Serializer): + + name = serializers.CharField() + label = serializers.CharField() + operators = OperatorSerializer(many=True) + + +class ActionArgumentSerializer(serializers.Serializer): + + name = serializers.CharField() + type = serializers.CharField() + label = serializers.CharField(required=False) + value = serializers.CharField(required=False) + + +class ActionTemplateSerializer(serializers.Serializer): + + name = serializers.CharField() + label = serializers.CharField() + args = ActionArgumentSerializer(many=True, required=False) + args_order = serializers.ListField(child=serializers.CharField(), required=False) diff --git a/modoboa/sievefilters/api/v2/urls.py b/modoboa/sievefilters/api/v2/urls.py new file mode 100644 index 000000000..c332321c1 --- /dev/null +++ b/modoboa/sievefilters/api/v2/urls.py @@ -0,0 +1,10 @@ +"""App related API urls.""" + +from rest_framework import routers + +from modoboa.sievefilters.api.v2 import viewsets + +router = routers.SimpleRouter() +router.register(r"account/filtersets", viewsets.FilterSetViewSet, basename="filterset") + +urlpatterns = router.urls diff --git a/modoboa/sievefilters/api/v2/viewsets.py b/modoboa/sievefilters/api/v2/viewsets.py new file mode 100644 index 000000000..7d6aba501 --- /dev/null +++ b/modoboa/sievefilters/api/v2/viewsets.py @@ -0,0 +1,89 @@ +"""Sievefilters viewsets.""" + +import copy + +from rest_framework import response, viewsets +from rest_framework.decorators import action + +from modoboa.lib.connections import ConnectionError +from modoboa.sievefilters import constants +from modoboa.sievefilters.lib import SieveClient, SieveClientError +from modoboa.sievefilters.api.v2 import serializers + + +class FilterSetViewSet(viewsets.ViewSet): + + serializer_class = serializers.FilterSetSerializer + + def get_sieve_client(self, request): + # try: + return SieveClient(user=request.user.username, password=str(request.auth)) + # except ConnectionError as e: + # raise + + def list(self, request): + sclient = self.get_sieve_client(request) + active_script, scripts = sclient.listscripts() + scripts = [ + {"name": script, "active": active_script == script} for script in scripts + ] + return response.Response(scripts) + + def create(self, request): + serializer = serializers.FilterSetSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + sclient = self.get_sieve_client(request) + sclient.pushscript( + serializer.validated_data["name"], + "# Empty script", + serializer.validated_data["active"], + ) + return response.Response(serializer.validated_data, 201) + + @action(methods=["get"], detail=False) + def condition_templates(self, request): + templates = copy.deepcopy(constants.CONDITION_TEMPLATES) + for template in templates: + template["operators"] = [ + {"name": operator[0], "label": operator[1], "type": operator[2]} + for operator in template["operators"] + ] + serializer = serializers.ConditionTemplateSerializer(templates, many=True) + return response.Response(serializer.data) + + @action(methods=["get"], detail=False) + def action_templates(self, request): + serializer = serializers.ActionTemplateSerializer( + constants.ACTION_TEMPLATES, many=True + ) + return response.Response(serializer.data) + + @action(methods=["get", "post"], detail=True) + def filters(self, request, pk=None): + sclient = self.get_sieve_client(request) + try: + fset = sclient.getscript(pk, format="gui") + except SieveClientError: + return response.Response(status=404) + if request.method == "GET": + serializer = serializers.FilterSerializer.from_filters(fset.filters) + return response.Response(serializer.data) + serializer = serializers.FilterSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + conditions, actions = serializer.to_filter() + match_type = serializer.validated_data["match_type"] + if match_type == "all": + match_type = "anyof" + conditions = [("true",)] + fltname = serializer.validated_data["name"].encode("utf-8") + try: + fset.addfilter(fltname, conditions, actions, match_type) + except (BadArgument, BadValue) as inst: + return response.Response(str(inst), status=400) + sclient.pushscript(fset.name, "{}".format(fset)) + return response.Response(serializer.validated_data, status=201) + + # @action(methods=["delete"], detail=True, url_path="filters/{filter}") + # def delete_filter(self, request, pk=None): + # sclient = self.get_sieve_client(request) + # return response.Response(status=204) diff --git a/modoboa/sievefilters/apps.py b/modoboa/sievefilters/apps.py new file mode 100644 index 000000000..693ce6217 --- /dev/null +++ b/modoboa/sievefilters/apps.py @@ -0,0 +1,27 @@ +"""AppConfig for stats.""" + +from django.apps import AppConfig +from django.utils.translation import gettext_lazy + + +def load_settings(): + from modoboa.parameters import tools as param_tools + from modoboa.sievefilters import forms + + param_tools.registry.add( + "global", forms.ParametersForm, gettext_lazy("Sieve filters") + ) + param_tools.registry.add( + "user", forms.UserSettings, gettext_lazy("Message filters") + ) + + +class SieveFiltersConfig(AppConfig): + """App configuration.""" + + name = "modoboa.sievefilters" + verbose_name = "Sieve filters editor for Modoboa" + + def ready(self): + load_settings() + from . import handlers diff --git a/modoboa/sievefilters/constants.py b/modoboa/sievefilters/constants.py new file mode 100644 index 000000000..e7f160b23 --- /dev/null +++ b/modoboa/sievefilters/constants.py @@ -0,0 +1,72 @@ +"""Sieve filters constants.""" + +from django.utils.translation import gettext_lazy as _ + + +MATCH_TYPE_ALLOF = "allof" +MATCH_TYPE_ANYOF = "anyof" +MATCH_TYPE_ALL = "all" +MATCH_TYPES = [ + (MATCH_TYPE_ALLOF, _("All of the following")), + (MATCH_TYPE_ANYOF, _("Any of the following")), + (MATCH_TYPE_ALL, _("All messages")), +] + +HEADER_OPERATORS = [ + ("contains", _("contains"), "string"), + ("notcontains", _("does not contain"), "string"), + ("is", _("is"), "string"), + ("isnot", _("is not"), "string"), +] + +CONDITION_TEMPLATES = [ + {"name": "Subject", "label": _("Subject"), "operators": HEADER_OPERATORS}, + {"name": "From", "label": _("Sender"), "operators": HEADER_OPERATORS}, + {"name": "To", "label": _("Recipient"), "operators": HEADER_OPERATORS}, + {"name": "Cc", "label": _("Cc"), "operators": HEADER_OPERATORS}, + { + "name": "size", + "label": _("Size"), + "operators": [ + ("over", _("is greater than"), "number"), + ("under", _("is less than"), "number"), + ], + }, +] + +ACTION_TEMPLATES = [ + { + "name": "fileinto", + "label": _("Move message to"), + "args": [ + {"name": "mailbox", "type": "list", "vloader": "userfolders"}, + { + "name": "copy", + "type": "boolean", + "label": _("Keep local copy"), + "value": ":copy", + }, + ], + "args_order": ["copy", "mailbox"], + }, + { + "name": "redirect", + "label": _("Redirect message to"), + "args": [ + {"name": "address", "type": "string"}, + { + "name": "copy", + "type": "boolean", + "label": _("Keep local copy"), + "value": ":copy", + }, + ], + "args_order": ["copy", "address"], + }, + { + "name": "reject", + "label": _("Reject message"), + "args": [{"name": "text", "type": "string"}], + }, + {"name": "stop", "label": _("Stop processing")}, +] diff --git a/modoboa/sievefilters/forms.py b/modoboa/sievefilters/forms.py new file mode 100644 index 000000000..c33eebb23 --- /dev/null +++ b/modoboa/sievefilters/forms.py @@ -0,0 +1,395 @@ +"""Custom forms.""" + +from sievelib import commands +from sievelib.managesieve import SUPPORTED_AUTH_MECHS + +from django import forms +from django.http import QueryDict +from django.utils.encoding import smart_str +from django.utils.translation import gettext as _, gettext_lazy + +from modoboa.admin.templatetags.admin_tags import gender +from modoboa.lib import form_utils +from modoboa.parameters import forms as param_forms + +from . import constants +from . import imaputils +from . import lib + + +class FiltersSetForm(forms.Form): + name = forms.CharField(widget=forms.TextInput(attrs={"class": "form-control"})) + active = forms.BooleanField( + label=gender("Active", "m"), + required=False, + initial=False, + help_text=gettext_lazy("Check to activate this filters set"), + ) + + +class FilterForm(forms.Form): + """A dynamic form to edit a filter.""" + + def __init__(self, conditions, actions, request, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.field_widths = {"match_type": 8} + + self.fields["name"] = forms.CharField(label=_("Name")) + self.fields["match_type"] = forms.ChoiceField( + label=_("Match type"), + choices=[ + ("allof", _("All of the following")), + ("anyof", _("Any of the following")), + ("all", _("All messages")), + ], + initial="anyof", + widget=form_utils.HorizontalRadioSelect(), + ) + + self.conds_cnt = 0 + for c in conditions: + getattr(self, "_build_%s_field" % c[0])(c[1], c[2]) + self.actions_cnt = 0 + for a in actions: + getattr(self, "_build_%s_field" % a[0])(request, *a[1:]) + + def clean_name(self): + """Check that name does not contain strange chars.""" + if "#" in self.cleaned_data["name"]: + raise forms.ValidationError(_("Wrong filter name")) + return self.cleaned_data["name"] + + def _build_header_field(self, name, op, value): + """Add a new header field to form.""" + targets = [] + ops = [] + vfield = None + for tpl in constants.CONDITION_TEMPLATES: + targets += [ + (tpl["name"], tpl["label"]), + ] + if tpl["name"] != name: + continue + for opdef in tpl["operators"]: + ops += [opdef[:2]] + if op != opdef[0]: + continue + if opdef[2] in ["string", "number"]: + vfield = forms.CharField(max_length=255, initial=value) + + self.fields["cond_target_%d" % self.conds_cnt] = forms.ChoiceField( + initial=name, choices=targets + ) + self.fields["cond_operator_%d" % self.conds_cnt] = forms.ChoiceField( + initial=op, choices=ops + ) + self.fields["cond_value_%d" % self.conds_cnt] = vfield + self.conds_cnt += 1 + + def _build_Subject_field(self, op, value): + self._build_header_field("Subject", op, value) + + def _build_To_field(self, op, value): + self._build_header_field("To", op, value) + + def _build_From_field(self, op, value): + self._build_header_field("From", op, value) + + def _build_Cc_field(self, op, value): + self._build_header_field("Cc", op, value) + + def _build_size_field(self, op, value): + self._build_header_field("size", op, value) + + def _build_action_field(self, request, name, *values): + """Add a new action field to form.""" + actions = [] + args = None + for tpl in constants.ACTION_TEMPLATES: + actions += [ + (tpl["name"], tpl["label"]), + ] + if name == tpl["name"]: + args = tpl.get("args", []) + self.fields["action_name_%d" % self.actions_cnt] = forms.ChoiceField( + initial=name, choices=actions + ) + for cnt in range(len(args)): + arg = args[cnt] + value = values[cnt] if len(values) > cnt else None + aname = "action_arg_%d_%d" % (self.actions_cnt, cnt) + if arg["type"] == "string": + self.fields[aname] = forms.CharField(max_length=255, initial=value) + elif arg["type"] == "boolean": + self.fields[aname] = forms.BooleanField( + label=arg["label"], initial=value, required=False + ) + elif arg["type"] == "list": + choices = getattr(self, arg["vloader"])(request) + self.fields[aname] = forms.ChoiceField(initial=value, choices=choices) + self.actions_cnt += 1 + + def _build_redirect_field(self, request, *values): + self._build_action_field(request, "redirect", *values) + + def _build_reject_field(self, request, *values): + self._build_action_field(request, "reject", *values) + + def _build_fileinto_field(self, request, *values): + self._build_action_field(request, "fileinto", *values) + + def _build_stop_field(self, request): + self._build_action_field(request, "stop") + + def __build_folders_list(self, folders, user, imapc, parentmb=None): + ret = [] + for fd in folders: + value = fd["path"] if "path" in fd else fd["name"] + if parentmb: + ret += [ + ( + value, + fd["name"].replace("%s%s" % (parentmb, imapc.hdelimiter), ""), + ) + ] + else: + ret += [(value, fd["name"])] + if "sub" in fd: + submboxes = imapc.getmboxes(user, value) + ret += self.__build_folders_list(submboxes, user, imapc, value) + return ret + + def userfolders(self, request): + mbc = imaputils.get_imapconnector(request) + ret = mbc.getmboxes(request.user) + + folders = self.__build_folders_list(ret, request.user, mbc) + return folders + + def tofilter(self): + """Convert form values to filter values.""" + conditions = [] + actions = [] + for cpt in range(self.conds_cnt): + conditions += [ + ( + self.cleaned_data["cond_target_%d" % cpt], + ":" + self.cleaned_data["cond_operator_%d" % cpt], + self.cleaned_data["cond_value_%d" % cpt], + ) + ] + for cpt in range(self.actions_cnt): + action = self.cleaned_data["action_name_%d" % cpt] + tpl = lib.find_action_template(action) + naction = [action] + args = {} + for pos, argtpl in enumerate(tpl.get("args", [])): + fieldname = "action_arg_{}_{}".format(cpt, pos) + if fieldname not in self.cleaned_data: + continue + value = self.cleaned_data[fieldname] + if argtpl["type"] == "boolean" and isinstance(value, bool): + if not value: + continue + value = argtpl["value"] + args[argtpl["name"]] = value + if "args_order" in tpl: + # Corresponding command requires args to be in a special order + for name in tpl["args_order"]: + if name in args: + naction.append(args[name]) + else: + naction += [arg for arg in args.values()] + actions += [naction] + return (conditions, actions) + + +def build_filter_form_from_qdict(request): + conditions = [] + actions = [] + qdict = QueryDict("", mutable=True) + qdict["name"] = request.POST["name"] + qdict["match_type"] = request.POST["match_type"] + cpt = 0 + i = 0 + if qdict["match_type"] != "all": + while True: + if cpt == int(request.POST["conds"]): + break + if "cond_target_%d" % i in request.POST: + qdict["cond_target_%d" % cpt] = request.POST["cond_target_%d" % i] + qdict["cond_operator_%d" % cpt] = request.POST["cond_operator_%d" % i] + qdict["cond_value_%d" % cpt] = request.POST["cond_value_%d" % i] + condtarget = request.POST["cond_target_%d" % i] + condop = request.POST["cond_operator_%d" % i] + condvalue = request.POST["cond_value_%d" % i] + conditions += [(condtarget, condop, condvalue)] + cpt += 1 + i += 1 + cpt = 0 + i = 0 + while True: + if cpt == int(request.POST["actions"]): + break + if "action_name_%d" % i in request.POST: + qdict["action_name_%d" % cpt] = request.POST["action_name_%d" % i] + action = request.POST["action_name_%d" % i] + argcpt = 0 + args = [] + while True: + try: + qdict["action_arg_%d_%d" % (cpt, argcpt)] = request.POST[ + "action_arg_%d_%d" % (i, argcpt) + ] + args += [request.POST["action_arg_%d_%d" % (i, argcpt)]] + except KeyError: + break + argcpt += 1 + args = [action] + args + actions += [args] + cpt += 1 + i += 1 + + return FilterForm(conditions, actions, request, qdict) + + +def build_filter_form_from_filter(request, name, fobj): + match_type = fobj["test"].name + conditions = [] + for t in fobj["test"]["tests"]: + if isinstance(t, commands.TrueCommand): + match_type = "all" + conditions += [("Subject", "contains", "")] + break + elif isinstance(t, commands.SizeCommand): + conditions += [("size", t["comparator"][1:], t["limit"])] + else: + operator_prefix = "" + if isinstance(t, commands.NotCommand): + t = t["test"] + operator_prefix = "not" + conditions += [ + ( + smart_str(t["header-names"]).strip('"'), + "{}{}".format(operator_prefix, smart_str(t["match-type"])[1:]), + smart_str(t["key-list"]).strip('"'), + ) + ] + actions = [] + for c in fobj.children: + action = (c.name,) + tpl = lib.find_action_template(c.name) + for argtpl in tpl.get("args", []): + if argtpl["name"] not in c: + continue + value = c[argtpl["name"]] + if argtpl["type"] == "boolean": + value = True if value else False + else: + value = value.strip('"') + action += (value,) + actions += [action] + form = FilterForm(conditions, actions, request) + form.fields["name"].initial = smart_str(name) + form.fields["match_type"].initial = match_type + return form + + +def supported_auth_mechs(): + values = [("AUTO", "auto")] + for m in SUPPORTED_AUTH_MECHS: + values += [(m, m.lower())] + return values + + +class ParametersForm(param_forms.AdminParametersForm): + app = "sievefilters" + + sep1 = form_utils.SeparatorField(label=gettext_lazy("ManageSieve settings")) + + server = forms.CharField( + label=gettext_lazy("Server address"), + initial="127.0.0.1", + help_text=gettext_lazy("Address of your MANAGESIEVE server"), + widget=forms.TextInput(attrs={"class": "form-control"}), + ) + + port = forms.IntegerField( + label=gettext_lazy("Server port"), + initial=4190, + help_text=gettext_lazy("Listening port of your MANAGESIEVE server"), + widget=forms.TextInput(attrs={"class": "form-control"}), + ) + + starttls = form_utils.YesNoField( + label=gettext_lazy("Connect using STARTTLS"), + initial=False, + help_text=gettext_lazy("Use the STARTTLS extension"), + ) + + authentication_mech = forms.ChoiceField( + label=gettext_lazy("Authentication mechanism"), + choices=supported_auth_mechs(), + initial="auto", + help_text=gettext_lazy("Prefered authentication mechanism"), + widget=forms.Select(attrs={"class": "form-control"}), + ) + + sep2 = form_utils.SeparatorField(label=gettext_lazy("IMAP settings")) + + imap_server = forms.CharField( + label=gettext_lazy("Server address"), + initial="127.0.0.1", + help_text=gettext_lazy("Address of your IMAP server"), + ) + + imap_secured = form_utils.YesNoField( + label=gettext_lazy("Use a secured connection"), + initial=False, + help_text=gettext_lazy("Use a secured connection to access IMAP server"), + ) + + imap_port = forms.IntegerField( + label=gettext_lazy("Server port"), + initial=143, + help_text=gettext_lazy("Listening port of your IMAP server"), + ) + + +class UserSettings(param_forms.UserParametersForm): + app = "sievefilters" + + sep1 = form_utils.SeparatorField(label=gettext_lazy("General")) + + editor_mode = forms.ChoiceField( + initial="gui", + label=gettext_lazy("Editor mode"), + choices=[("raw", "raw"), ("gui", "simplified")], + help_text=gettext_lazy("Select the mode you want the editor to work in"), + widget=form_utils.HorizontalRadioSelect(), + ) + + sep2 = form_utils.SeparatorField(label=gettext_lazy("Mailboxes")) + + trash_folder = forms.CharField( + initial="Trash", + label=gettext_lazy("Trash folder"), + help_text=gettext_lazy("Folder where deleted messages go"), + ) + + sent_folder = forms.CharField( + initial="Sent", + label=gettext_lazy("Sent folder"), + help_text=gettext_lazy("Folder where copies of sent messages go"), + ) + + drafts_folder = forms.CharField( + initial="Drafts", + label=gettext_lazy("Drafts folder"), + help_text=gettext_lazy("Folder where drafts go"), + ) + + @staticmethod + def has_access(**kwargs): + return hasattr(kwargs.get("user"), "mailbox") diff --git a/modoboa/sievefilters/handlers.py b/modoboa/sievefilters/handlers.py new file mode 100644 index 000000000..da945f2df --- /dev/null +++ b/modoboa/sievefilters/handlers.py @@ -0,0 +1,39 @@ +"""Modoboa sievefilters handlers.""" + +from django.urls import reverse +from django.dispatch import receiver +from django.utils.translation import gettext as _ + +from modoboa.core import signals as core_signals + +from . import lib + + +@receiver(core_signals.extra_user_menu_entries) +def menu(sender, location, user, **kwargs): + """Return extra menu entry.""" + if location != "options_menu" or not hasattr(user, "mailbox"): + return [] + return [ + { + "name": "sievefilters", + "label": _("Message filters"), + "url": reverse("sievefilters:index"), + "img": "fa fa-filter", + } + ] + + +@receiver(core_signals.user_logout) +def userlogout(sender, request, **kwargs): + """Close managesieve connection.""" + if not hasattr(request.user, "mailbox"): + return + try: + sc = lib.SieveClient( + user=request.user.username, password=request.session["password"] + ) + except Exception: + pass + else: + sc.logout() diff --git a/modoboa/sievefilters/imaputils.py b/modoboa/sievefilters/imaputils.py new file mode 100644 index 000000000..a6d6b20ce --- /dev/null +++ b/modoboa/sievefilters/imaputils.py @@ -0,0 +1,246 @@ +# coding: utf-8 +""" +:mod:`imaputils` --- Extra IMAPv4 utilities +------------------------------------------- +""" + +import imaplib +import re +from operator import itemgetter +import socket +import ssl + +import six + +from django.utils.encoding import smart_bytes +from django.utils.translation import gettext as _ + +from modoboa.lib import imap_utf7 # NOQA +from modoboa.lib.connections import ConnectionsManager +from modoboa.lib.exceptions import ModoboaException, InternalError +from modoboa.parameters import tools as param_tools + + +class ImapError(ModoboaException): + + def __init__(self, reason): + self.reason = reason + + def __str__(self): + return str(self.reason) + + +@six.add_metaclass(ConnectionsManager) +class IMAPconnector(object): + """The IMAPv4 connector.""" + + list_base_pattern = r'\((?P.*?)\) "(?P.*)" "?(?P[^"]*)"?' + list_response_pattern_literal = re.compile( + r'\((?P.*?)\) "(?P.*)" \{(?P\d+)\}' + ) + list_response_pattern = re.compile(list_base_pattern) + listextended_response_pattern = re.compile( + list_base_pattern + r"\s*(?P.*)" + ) + + def __init__(self, user=None, password=None): + self.__hdelimiter = None + self.conf = dict(param_tools.get_global_parameters("sievefilters")) + self.address = self.conf["imap_server"] + self.port = self.conf["imap_port"] + self.login(user, password) + + def _cmd(self, name, *args, **kwargs): + """IMAP command wrapper + + To simplify errors handling, this wrapper calls the + appropriate method (``uid`` or FIXME) and then check the + return code. If an error has occured, an ``ImapError`` + exception is raised. + + For specific commands commands (FETCH, ...), the result is + parsed using the IMAPclient module before being returned. + + :param name: the command's name + :return: the command's result + """ + try: + typ, data = self.m._simple_command(name, *args) + except imaplib.IMAP4.error as e: + raise ImapError(e) + if typ == "NO": + raise ImapError(data) + if "responses" not in kwargs: + if name not in self.m.untagged_responses: + return None + return self.m.untagged_responses.pop(name) + res = [] + for r in kwargs["responses"]: + if r not in self.m.untagged_responses: + return None + res.append(self.m.untagged_responses.pop(r)) + return res + + @property + def hdelimiter(self): + """Return the default hierachy delimiter. + + This is a simple way to retrieve the default delimiter (see + http://www.imapwiki.org/ClientImplementation/MailboxList). + + :return: a string + """ + if self.__hdelimiter is None: + data = self._cmd("LIST", '""', '""') + m = self.list_response_pattern.match(data[0].decode()) + if m is None: + raise InternalError(_("Failed to retrieve hierarchy delimiter")) + self.__hdelimiter = m.group("delimiter") + return self.__hdelimiter + + def refresh(self, user, password): + """Check if current connection needs a refresh + + Is it really secure? + """ + if self.m is not None: + try: + self._cmd("NOOP") + except ImapError: + if hasattr(self, "current_mailbox"): + del self.current_mailbox + else: + return + + self.login(user, password) + + def login(self, user, passwd): + """Custom login method + + We connect to the server, issue a LOGIN command. If + successfull, we try to record a eventuel CAPABILITY untagged + response. Otherwise, we issue the command. + + :param user: username + :param passwd: password + """ + try: + if self.conf["imap_secured"]: + self.m = imaplib.IMAP4_SSL(self.address, self.port) + else: + self.m = imaplib.IMAP4(self.address, self.port) + except (socket.error, imaplib.IMAP4.error, ssl.SSLError) as error: + raise ImapError(_("Connection to IMAP server failed: %s" % error)) + + passwd = self.m._quote(passwd) + data = self._cmd("LOGIN", smart_bytes(user), smart_bytes(passwd)) + self.m.state = "AUTH" + if "CAPABILITY" in self.m.untagged_responses: + self.capabilities = ( + self.m.untagged_responses.pop("CAPABILITY")[0].decode().split() + ) + else: + data = self._cmd("CAPABILITY") + self.capabilities = data[0].decode().split() + + def logout(self): + """Logout from server.""" + try: + self._cmd("CHECK") + except ImapError: + pass + self._cmd("LOGOUT") + del self.m + self.m = None + if hasattr(self, "current_mailbox"): + del self.current_mailbox + + def _listmboxes(self, topmailbox, mailboxes): + """Retrieve mailboxes list.""" + pattern = ( + '"{0}{1}%"'.format( + topmailbox.encode("imap4-utf-7").decode(), self.hdelimiter + ) + if topmailbox + else "%" + ) + resp = self._cmd("LIST", '""', pattern, "RETURN", "(CHILDREN)") + newmboxes = [] + for mb in resp: + if not mb: + continue + if type(mb) in [list, tuple]: + flags, delimiter, namelen = self.list_response_pattern_literal.match( + mb[0].decode() + ).groups() + name = mb[1][0 : int(namelen)] + else: + flags, delimiter, name, childinfo = ( + self.listextended_response_pattern.match(mb.decode()).groups() + ) + flags = flags.split(" ") + name = bytearray(name, "utf-8") + name = name.decode("imap4-utf-7") + mdm_found = False + for idx, mdm in enumerate(mailboxes): + if mdm["name"] == name: + mdm_found = True + descr = mailboxes[idx] + break + if not mdm_found: + descr = {"name": name} + newmboxes += [descr] + + if "\\Marked" in flags or "\\UnMarked" not in flags: + descr["send_status"] = True + if "\\HasChildren" in flags: + if "\\NonExistent" in flags: + descr["removed"] = True + descr["path"] = name + descr["sub"] = [] + + mailboxes += sorted(newmboxes, key=itemgetter("name")) + + def getmboxes(self, user, topmailbox=""): + """Returns a list of mailboxes for a particular user + + By default, only the first level of mailboxes under + ``topmailbox`` is returned. + + :param user: a ``User`` instance + :param topmailbox: the mailbox where to start in the tree + :return: a list + """ + if topmailbox: + md_mailboxes = [] + else: + md_mailboxes = [ + {"name": "INBOX", "class": "fa fa-inbox"}, + { + "name": user.parameters.get_value("drafts_folder"), + "class": "fa fa-file", + }, + {"name": "Junk", "class": "fa fa-fire"}, + { + "name": user.parameters.get_value("sent_folder"), + "class": "fa fa-envelope", + }, + { + "name": user.parameters.get_value("trash_folder"), + "class": "fa fa-trash", + }, + ] + self._listmboxes(topmailbox, md_mailboxes) + + return md_mailboxes + + +def get_imapconnector(request): + """Simple shortcut to create a connector + + :param request: a ``Request`` object + """ + imapc = IMAPconnector( + user=request.user.username, password=request.session["password"] + ) + return imapc diff --git a/modoboa/sievefilters/lib.py b/modoboa/sievefilters/lib.py new file mode 100644 index 000000000..17c866bd3 --- /dev/null +++ b/modoboa/sievefilters/lib.py @@ -0,0 +1,115 @@ +# coding: utf-8 + +"""Internal tools.""" + +import ssl + +from sievelib.factory import FiltersSet +from sievelib import managesieve +from sievelib.parser import Parser +import six + +from django.utils.translation import gettext as _ + +from modoboa.lib.connections import ConnectionsManager, ConnectionError +from modoboa.lib.exceptions import ModoboaException +from modoboa.parameters import tools as param_tools + +from . import constants + + +class SieveClientError(ModoboaException): + http_code = 424 + + +# @six.add_metaclass(ConnectionsManager) +class SieveClient: + """Sieve client.""" + + def __init__(self, user=None, password=None): + try: + ret, msg = self.login(user, password) + except managesieve.Error as e: + raise ConnectionError(str(e)) + if not ret: + raise ConnectionError(msg) + + def login(self, user, password): + conf = dict(param_tools.get_global_parameters("sievefilters")) + self.msc = managesieve.Client(conf["server"], conf["port"], debug=True) + authmech = conf["authentication_mech"] + if authmech == "AUTO": + authmech = None + try: + ret = self.msc.connect( + user, password, starttls=conf["starttls"], authmech=authmech + ) + except managesieve.Error as err: + print(err) + ret = False + if not ret: + return False, _( + "Connection to MANAGESIEVE server failed, check your " "configuration" + ) + return True, None + + def logout(self): + self.msc.logout() + self.msc = None + + def refresh(self, user, password): + if self.msc is not None: + try: + self.msc.capability() + except managesieve.Error as e: + pass + else: + return + try: + ret, msg = self.login(user, password) + except (managesieve.Error, ssl.SSLError) as e: + raise ConnectionError(e) + if not ret: + raise ConnectionError(msg) + + def listscripts(self): + return self.msc.listscripts() + + def getscript(self, name: str, format: str = "raw") -> FiltersSet | str | None: + content = self.msc.getscript(name) + if content is None: + raise SieveClientError(self.msc.errmsg.decode()) + if format == "raw": + return content + p = Parser() + if not p.parse(content): + print("Parse error????") + return None + fs = FiltersSet(name) + fs.from_parser_result(p) + return fs + + def pushscript(self, name: str, content: str, active: bool = False) -> None: + if not self.msc.havespace(name, len(content)): + error = "%s (%s)" % (_("Not enough space on server"), self.msc.errmsg) + raise SieveClientError(error) + if not self.msc.putscript(name, content): + raise SieveClientError(self.msc.errmsg.decode()) + if active and not self.msc.setactive(name): + raise SieveClientError(self.msc.errmsg) + + def deletescript(self, name): + if not self.msc.deletescript(name): + raise SieveClientError(self.msc.errmsg.decode()) + + def activatescript(self, name): + if not self.msc.setactive(name): + raise SieveClientError(self.msc.errmsg.decode()) + + +def find_action_template(action): + """Find template corresponding to action.""" + for tpl in constants.ACTION_TEMPLATES: + if tpl["name"] == action: + return tpl + raise RuntimeError("action {} not defined".format(action)) diff --git a/modoboa/sievefilters/locale/cs_CZ/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/cs_CZ/LC_MESSAGES/django.po new file mode 100644 index 000000000..dd688c0be --- /dev/null +++ b/modoboa/sievefilters/locale/cs_CZ/LC_MESSAGES/django.po @@ -0,0 +1,364 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Peter Sveter, 2016-2017 +# Jarda Tesar , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 18:04+0100\n" +"PO-Revision-Date: 2018-08-10 11:55+0000\n" +"Last-Translator: Jarda Tesar \n" +"Language-Team: Czech (Czech Republic) (http://www.transifex.com/tonio/modoboa/language/cs_CZ/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: cs_CZ\n" +"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" + +#: constants.py:6 +msgid "contains" +msgstr "obsahuje" + +#: constants.py:7 +msgid "does not contain" +msgstr "neobsahuje" + +#: constants.py:8 +msgid "is" +msgstr "je" + +#: constants.py:9 +msgid "is not" +msgstr "není" + +#: constants.py:14 +msgid "Subject" +msgstr "Předmět" + +#: constants.py:17 +msgid "Sender" +msgstr "Odesílatel" + +#: constants.py:20 +msgid "Recipient" +msgstr "Příjemce" + +#: constants.py:23 +msgid "Cc" +msgstr "Kopie" + +#: constants.py:25 +msgid "Size" +msgstr "Velikost" + +#: constants.py:26 +msgid "is greater than" +msgstr "Je větší než" + +#: constants.py:27 +msgid "is less than" +msgstr "Je menší než" + +#: constants.py:31 +msgid "Move message to" +msgstr "Přesunout zprávu do" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "Ponechat místní kopii" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "Přesměrovat zprávu na" + +#: constants.py:48 +msgid "Reject message" +msgstr "Odmítnout zprávu" + +#: constants.py:50 +msgid "Stop processing" +msgstr "Ukončit zpracování" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "Zatrhněte pole pro aktivaci této sady filtrů" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Jméno" + +#: forms.py:89 +msgid "Match type" +msgstr "Musí souhlasit" + +#: forms.py:90 +msgid "All of the following" +msgstr "Všechna pravidla" + +#: forms.py:91 +msgid "Any of the following" +msgstr "Alespoň jedno z pravidel" + +#: forms.py:92 +msgid "All messages" +msgstr "Všechny zprávy" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "Chybný název filtru" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "Nastavení ManageSieve" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "Adresa serveru" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "Adresa vašeho serveru ManegeSieve" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "Port serveru" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "Port, na kterém naslouchá váš ManageSieve server" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "Připojit pomocí STARTTLS" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "Použít rozšíření STARTTLS" + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "Režim ověření" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "Preferovaný režim ověření" + +#: forms.py:381 +msgid "IMAP settings" +msgstr "Nastavení IMAP" + +#: forms.py:386 +msgid "Address of your IMAP server" +msgstr "Adresa vašeho IMAP serveru" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "Použít zabezpečené připojení" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "Použít zabezpečené připojení pro přístup k IMAP serveru" + +#: forms.py:398 +msgid "Listening port of your IMAP server" +msgstr "Port, na kterém naslouchá váš IMAP server" + +#: forms.py:405 +msgid "General" +msgstr "Obecné" + +#: forms.py:409 +msgid "Editor mode" +msgstr "Režim úpravy" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "Zvolte režim editoru textu, ve kterém si přejete pracovat" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "Schránky" + +#: forms.py:419 +msgid "Trash folder" +msgstr "Složka koše" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "Složka pro smazané zprávy" + +#: forms.py:425 +msgid "Sent folder" +msgstr "Složka odeslané pošty" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "Složka pro kopie odeslaných zpráv" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "Složka konceptů" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "Složka pro koncepty zpráv" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "Filtry zpráv" + +#: imaputils.py:99 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Nebylo možné zjistit hierarchický oddělovač" + +#: imaputils.py:135 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "Spojení se serverem IMAP selhalo: %s" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "Spojení se serverem ManageSieve selhalo, zkontrolujte nastavení" + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "Nedostatek prostoru na serveru" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "Doplněk pro jednoduchou tvorbu filtrů zpráv na serveru" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "Sieve filtry" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "Podmínky" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "Akce" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Aktivní" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Dosud nebyl vytvořen žádný filtr" + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "Sady filtrů" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "aktivní" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "Dosud nebyla vytvořena žádná sada filtrů. Aby bylo možné vytvořit filtry, je nejprve potřeba vytvořit sadu filtrů." + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "Nová sada filtrů" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "Nový filtr" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "Uložit sadu filtrů" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "Aktitovat sadu filtrů" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "Odstranit sadu filtrů" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "Stáhnout" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "Upravit filtr" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "Odstranit tento filtr" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "Posunout tento filtr nahoru" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "Posunout tento filtr dolů" + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "Nebylo možné získat sadu filtrů" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "Vytvořit" + +#: views.py:161 +msgid "Filter created" +msgstr "Filtr vytvořen" + +#: views.py:179 +msgid "Update" +msgstr "Aktualizovat" + +#: views.py:186 +msgid "Filter modified" +msgstr "Filtr upraven" + +#: views.py:209 +msgid "Filter removed" +msgstr "Filtr odstraněn" + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "Nezdařilo se odstranit filtr" + +#: views.py:225 +msgid "Filters set saved" +msgstr "Sada filtrů uložena" + +#: views.py:241 +msgid "Filters set created" +msgstr "Sada filtrů vytvořena" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "Vytvořit novou sadu filtrů" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "Sada filtrů vymazána" + +#: views.py:278 +msgid "Filters set activated" +msgstr "Sada filtrů aktivována" + +#: views.py:307 +msgid "yes" +msgstr "ano" + +#: views.py:311 +msgid "no" +msgstr "ne" diff --git a/modoboa/sievefilters/locale/cs_CZ/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/cs_CZ/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..a8a6b1837 --- /dev/null +++ b/modoboa/sievefilters/locale/cs_CZ/LC_MESSAGES/djangojs.po @@ -0,0 +1,40 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Miroslav Abrahám , 2013,2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-02-11 13:54+0100\n" +"PO-Revision-Date: 2017-09-22 09:57+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Czech (Czech Republic) (http://www.transifex.com/tonio/modoboa/language/cs_CZ/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: cs_CZ\n" +"Plural-Forms: nplurals=4; plural=(n == 1 && n % 1 == 0) ? 0 : (n >= 2 && n <= 4 && n % 1 == 0) ? 1: (n % 1 != 0 ) ? 2 : 3;\n" + +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "Odstranit tento filtr?" + +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "Uložit změny?" + +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "Smazat sadu filtrů?" + +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "Aktivovat tuto sadu filtrů?" + +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "aktivní" diff --git a/modoboa/sievefilters/locale/de/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/de/LC_MESSAGES/django.po new file mode 100644 index 000000000..124f3e4e1 --- /dev/null +++ b/modoboa/sievefilters/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,368 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Alexander Dube , 2017 +# Arledrian , 2017 +# Arvedui , 2014 +# Boris Andratzek , 2018 +# controlcde, 2015 +# Patrick Koetter , 2010 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 18:04+0100\n" +"PO-Revision-Date: 2018-04-25 14:02+0000\n" +"Last-Translator: Boris Andratzek \n" +"Language-Team: German (http://www.transifex.com/tonio/modoboa/language/de/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: constants.py:6 +msgid "contains" +msgstr "enthält" + +#: constants.py:7 +msgid "does not contain" +msgstr "enthält nicht" + +#: constants.py:8 +msgid "is" +msgstr "ist" + +#: constants.py:9 +msgid "is not" +msgstr "ist nicht" + +#: constants.py:14 +msgid "Subject" +msgstr "Betreff" + +#: constants.py:17 +msgid "Sender" +msgstr "Sender" + +#: constants.py:20 +msgid "Recipient" +msgstr "Empfänger" + +#: constants.py:23 +msgid "Cc" +msgstr "Cc" + +#: constants.py:25 +msgid "Size" +msgstr "Größe" + +#: constants.py:26 +msgid "is greater than" +msgstr "ist größer als" + +#: constants.py:27 +msgid "is less than" +msgstr "ist kleiner als" + +#: constants.py:31 +msgid "Move message to" +msgstr "Verschiebe Nachricht nach" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "Behalte lokale Kopie" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "Leite Nachricht um zu" + +#: constants.py:48 +msgid "Reject message" +msgstr "E-Mail ablehnen" + +#: constants.py:50 +msgid "Stop processing" +msgstr "Stoppe Bearbeitung" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "Häkchen setzen, um den Filter-Satz zu aktivieren" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Name" + +#: forms.py:89 +msgid "Match type" +msgstr "Vergleichte Typ" + +#: forms.py:90 +msgid "All of the following" +msgstr "Alle der folgenden" + +#: forms.py:91 +msgid "Any of the following" +msgstr "Irgendeines der folgenden" + +#: forms.py:92 +msgid "All messages" +msgstr "Alle Nachrichten" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "Falscher Filtername" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "ManageSieve-Einstellungen" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "Server-Adresse" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "Adresse Ihres MANAGESIEVE-Servers" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "Server-Port" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "TCP-Port Ihres MANAGESIEVE-Servers" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "STARTTLS für Verbindungsaufbau nutzen" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "STARTTLS benutzen" + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "Authentifizierungsmechanismus" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "Bevorzugte Authentifizierungsmethode" + +#: forms.py:381 +msgid "IMAP settings" +msgstr "IMAP-Einstellungen" + +#: forms.py:386 +msgid "Address of your IMAP server" +msgstr "Addresse Ihres IMAP-Servers" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "Benutze eine sichere Verbindung" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "Benutze eine gesicherte Verbindung zum Zugriff auf den IMAP Server" + +#: forms.py:398 +msgid "Listening port of your IMAP server" +msgstr "TCPIP Port Ihres IMAP Servers" + +#: forms.py:405 +msgid "General" +msgstr "Allgemein" + +#: forms.py:409 +msgid "Editor mode" +msgstr "Editor-Modus" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "Wählen Sie den Modus aus, in dem der Editor arbeiten soll" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "E-Mail-Postfächer" + +#: forms.py:419 +msgid "Trash folder" +msgstr "Papierkorb-Ordner" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "Ordner, in den gelöschte Nachrichten gehen" + +#: forms.py:425 +msgid "Sent folder" +msgstr "Gesendet-Ordner" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "Ordner, in den Kopien gesendeter Nachrichten gehen" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "Entwürfe-Ordner" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "Ordner für Entwürfe" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "Nachrichten-Filter" + +#: imaputils.py:99 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "" + +#: imaputils.py:135 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "Verbindung zum IMAP-Server gescheitert: %s" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "Verbindung zu MANAGESIEVE-Server fehlgeschlagen. Überprüfen Sie Ihre Konfiguration." + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "Kein weiterer Platz auf Server verfügbar" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "Plugin, um auf einfache Weise serverseitige Filter zu erstellen" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "SIEVE-Filter" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "Bedingungen" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "Aktionen" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Aktiv" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Noch keine Filter definiert" + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "Filter-Sätze" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "aktiv" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "Noch keine Filter-Sätze definiert. Sie müssen einen Satz erstellen, um diesem Filter hinzufügen zu können." + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "Neuer Filter-Satz" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "Neuer Filter" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "Filter-Satz speichern" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "Filter-Satz aktivieren" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "Filter-Satz entfernen" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "Herunterladen" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "Filter bearbeiten" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "Filter entfernen" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "Filter nach oben verschieben" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "Filter nach unten verschieben" + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "Konnte Filter-Satz nicht laden" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "Erstellen" + +#: views.py:161 +msgid "Filter created" +msgstr "Filter erstellt" + +#: views.py:179 +msgid "Update" +msgstr "Aktualisieren" + +#: views.py:186 +msgid "Filter modified" +msgstr "Filter verändert" + +#: views.py:209 +msgid "Filter removed" +msgstr "Filter entfernt" + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "Konnte Filter nicht entfernen" + +#: views.py:225 +msgid "Filters set saved" +msgstr "Filter-Satz gespeichert" + +#: views.py:241 +msgid "Filters set created" +msgstr "Filter-Satz erstellt" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "Neuen Filter-Satz erstellen" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "Filter-Satz entfernt" + +#: views.py:278 +msgid "Filters set activated" +msgstr "Filter-Satz aktiviert" + +#: views.py:307 +msgid "yes" +msgstr "Ja" + +#: views.py:311 +msgid "no" +msgstr "Nein" diff --git a/modoboa/sievefilters/locale/de/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/de/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..00a55759c --- /dev/null +++ b/modoboa/sievefilters/locale/de/LC_MESSAGES/djangojs.po @@ -0,0 +1,42 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Arvedui , 2014 +# controlcde, 2015 +# Patrick Koetter , 2010 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-02-11 13:54+0100\n" +"PO-Revision-Date: 2017-09-22 09:57+0000\n" +"Last-Translator: controlcde\n" +"Language-Team: German (http://www.transifex.com/tonio/modoboa/language/de/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "Diesen Filter entfernen?" + +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "Änderungen speichern?" + +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "Diesen Filter-Satz entfernen?" + +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "Diesen Filter-Satz aktivieren?" + +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "Aktiv" diff --git a/modoboa/sievefilters/locale/el_GR/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/el_GR/LC_MESSAGES/django.po new file mode 100644 index 000000000..c73ce79e4 --- /dev/null +++ b/modoboa/sievefilters/locale/el_GR/LC_MESSAGES/django.po @@ -0,0 +1,368 @@ +# 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 +# Kostas Moumouris , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 18:04+0100\n" +"PO-Revision-Date: 2017-10-12 13:12+0000\n" +"Last-Translator: Kostas Moumouris \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" + +#: constants.py:6 +msgid "contains" +msgstr "περιέχει" + +#: constants.py:7 +msgid "does not contain" +msgstr "δεν περιέχει" + +#: constants.py:8 +msgid "is" +msgstr "είναι" + +#: constants.py:9 +msgid "is not" +msgstr "δεν είναι" + +#: constants.py:14 +msgid "Subject" +msgstr "Τίτλος" + +#: constants.py:17 +msgid "Sender" +msgstr "Αποστολέας" + +#: constants.py:20 +msgid "Recipient" +msgstr "Παραλήπτης" + +#: constants.py:23 +msgid "Cc" +msgstr "Cc" + +#: constants.py:25 +msgid "Size" +msgstr "Μέγεθος" + +#: constants.py:26 +msgid "is greater than" +msgstr "είναι μεγαλύτερο από" + +#: constants.py:27 +msgid "is less than" +msgstr "είναι μικρότερο από" + +#: constants.py:31 +msgid "Move message to" +msgstr "Μετακίνηση μηνυμάτων προς" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "Ανακατεύθυνση μηνυμάτων προς" + +#: constants.py:48 +msgid "Reject message" +msgstr "Απόρριψη μηνύματος" + +#: constants.py:50 +msgid "Stop processing" +msgstr "Ακύρωση επεξεργασίας" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "Επιλέξτε για να ενεργοποιηθεί αυτό το σετ φίλτρων" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Όνομα" + +#: forms.py:89 +msgid "Match type" +msgstr "" + +#: forms.py:90 +msgid "All of the following" +msgstr "Όλα τα παρακάτω" + +#: forms.py:91 +msgid "Any of the following" +msgstr "Οποιοδήποτε από τα παρακάτων" + +#: forms.py:92 +msgid "All messages" +msgstr "Όλα τα μηνύματα" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "Λάθος όνομα φίλτρου" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "Ρυθμίσεις ManageSieve" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "Διεύθυνση εξυπηρετητή" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "Η διεύθυνση του εξυπηρετητή MANAGESIEVE" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "Πόρτα εξυπηρετητή" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "Η πόρτα λειτουργίας του MANAGESIEVE εξυπηρετητή σας" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "Σύνδεση με χρήση STARTTLS" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "Χρήση του extension STARTTLS" + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "Μηχανισμός πιστοποίησης" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "Επιλεγμένος μηχανισμός πιστοποίησης" + +#: forms.py:381 +msgid "IMAP settings" +msgstr "ρύθμισης IMAP" + +#: forms.py:386 +msgid "Address of your IMAP server" +msgstr "Διεύθυνση του διακομιστή IMAP" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "Χρησιμοποιήστε μια ασφαλή σύνδεση" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "Χρησιμοποιήστε μια ασφαλή σύνδεση για πρόσβαση στον διακομιστή IMAP" + +#: forms.py:398 +msgid "Listening port of your IMAP server" +msgstr "Η θύρα που ακούει ο IMAP server σας" + +#: forms.py:405 +msgid "General" +msgstr "Γενικά" + +#: forms.py:409 +msgid "Editor mode" +msgstr "Λειτουργία συντάκτη" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "Επιλέξτε τη λειτουργία στην οποία θέλετε να εργάζεται ο συντάκτης" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "Γραμματοκιβώτια" + +#: forms.py:419 +msgid "Trash folder" +msgstr "Φάκελος σκουπιδιών" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "Φάκελος που πηγαίνουν τα σβησμένα μηνύματα" + +#: forms.py:425 +msgid "Sent folder" +msgstr "Φάκελος απεσταλμένων" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "Φάκελος όπου πηγαίνουν αντίγραφα των απεσταλμένων μηνυμάτων" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "Φάκελος πρόχειρων" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "Ο φάκελος που θα πηγαίνουν τα πρόχειρα" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "Φίλτρα μηνυμάτων" + +#: imaputils.py:99 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Αποτυχία κατά την ανάκτηση της οροθετημένης ιεραρχίας" + +#: imaputils.py:135 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "Η σύνδεση στο διακομιστή IMAP απέτυχε:%s" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "" +"Η σύνδεση με τον εξυπηρετητή MANAGESIEVE απέτυχε, ελέγξτε τις ρυθμίσεις σας " + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "Δεν υπάρχει διαθέσιμος χώρος στον εξυπηρετητή" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "Πρόσθετο για εύκολη δημιουργία φίλτρων από την πλευρά του διακομιστή" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "Φίλτρα Sieve" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "Συνθήκες" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "Ενέργειες" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Ενεργό" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Δεν υπάρχει καθορισμένο φίλτρο" + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "Σετ φίλτρων" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "ενεργό" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "" +"Δεν έχουν οριστεί ακόμη φίλτρα. Πρέπει να δημιουργήσετε τουλάχιστον ένα για " +"να καθοριστούν τα φίλτρα" + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "Νέο σετ φίλτρων" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "Νέο φίλτρο" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "Αποθήκευση του σετ φίλτρων" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "Ενεργοποίηση του σετ φίλτρων" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "Αφαίρεση του σετ φίλτρων" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "Λήψη" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "Επεξεργασία φίλτρου" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "Αφαίρεση αυτού του φίλτρου" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "Μετακίνηση αυτού του φίλτρου πάνω" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "Μετακίνηση αυτού του φίλτρου κάτω" + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "Σφάλμα κατά την την λήψη του σετ φίλτρων" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "Δημιουργία" + +#: views.py:161 +msgid "Filter created" +msgstr "Το φίλτρο δημιουργήθηκε" + +#: views.py:179 +msgid "Update" +msgstr "Ενημέρωση" + +#: views.py:186 +msgid "Filter modified" +msgstr "Το φίλτρο άλλαξε" + +#: views.py:209 +msgid "Filter removed" +msgstr "Το φίλτρο αφαιρέθηκε " + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "Σφάλμα κατά την αφαίρεση του φίλτρου" + +#: views.py:225 +msgid "Filters set saved" +msgstr "Το σετ φίλτρων αποθηκεύτηκε" + +#: views.py:241 +msgid "Filters set created" +msgstr "Το σετ φίλτρων δημιουργήθηκε" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "Δημιουργία νέου σετ φίλτρων" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "Το σετ φίλτρων διαγράφηκε" + +#: views.py:278 +msgid "Filters set activated" +msgstr "Το σετ φίλτρων ενεργοποιήθηκε" + +#: views.py:307 +msgid "yes" +msgstr "ναι" + +#: views.py:311 +msgid "no" +msgstr "όχι" diff --git a/modoboa/sievefilters/locale/el_GR/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/el_GR/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..d32830a74 --- /dev/null +++ b/modoboa/sievefilters/locale/el_GR/LC_MESSAGES/djangojs.po @@ -0,0 +1,40 @@ +# 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: 2017-03-03 09:34+0100\n" +"PO-Revision-Date: 2017-09-22 22:21+0000\n" +"Last-Translator: Giannis Kapetanakis \n" +"Language-Team: Greek (Greece) (http://www.transifex.com/tonio/modoboa/language/el_GR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: el_GR\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "Αφαίρεση αυτού του φίλτρου;" + +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "Αποθήκευση αλλαγών;" + +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "Αφαίρεση αυτού του σετ φίλτρων;" + +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "Ενεργοποίηση αυτού του σετ φίλτρων;" + +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "ενεργό" diff --git a/modoboa/sievefilters/locale/en/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/en/LC_MESSAGES/django.po new file mode 100644 index 000000000..8d511c3b5 --- /dev/null +++ b/modoboa/sievefilters/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,362 @@ +# 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: 2017-12-01 18:04+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" + +#: constants.py:6 +msgid "contains" +msgstr "" + +#: constants.py:7 +msgid "does not contain" +msgstr "" + +#: constants.py:8 +msgid "is" +msgstr "" + +#: constants.py:9 +msgid "is not" +msgstr "" + +#: constants.py:14 +msgid "Subject" +msgstr "" + +#: constants.py:17 +msgid "Sender" +msgstr "" + +#: constants.py:20 +msgid "Recipient" +msgstr "" + +#: constants.py:23 +msgid "Cc" +msgstr "" + +#: constants.py:25 +msgid "Size" +msgstr "" + +#: constants.py:26 +msgid "is greater than" +msgstr "" + +#: constants.py:27 +msgid "is less than" +msgstr "" + +#: constants.py:31 +msgid "Move message to" +msgstr "" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "" + +#: constants.py:48 +msgid "Reject message" +msgstr "" + +#: constants.py:50 +msgid "Stop processing" +msgstr "" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "" + +#: forms.py:89 +msgid "Match type" +msgstr "" + +#: forms.py:90 +msgid "All of the following" +msgstr "" + +#: forms.py:91 +msgid "Any of the following" +msgstr "" + +#: forms.py:92 +msgid "All messages" +msgstr "" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "" + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "" + +#: forms.py:381 +msgid "IMAP settings" +msgstr "" + +#: forms.py:386 +msgid "Address of your IMAP server" +msgstr "" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "" + +#: forms.py:398 +msgid "Listening port of your IMAP server" +msgstr "" + +#: forms.py:405 +msgid "General" +msgstr "" + +#: forms.py:409 +msgid "Editor mode" +msgstr "" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "" + +#: forms.py:419 +msgid "Trash folder" +msgstr "" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "" + +#: forms.py:425 +msgid "Sent folder" +msgstr "" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "" + +#: imaputils.py:99 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "" + +#: imaputils.py:135 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "" + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "" + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "" + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "" + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "" + +#: views.py:161 +msgid "Filter created" +msgstr "" + +#: views.py:179 +msgid "Update" +msgstr "" + +#: views.py:186 +msgid "Filter modified" +msgstr "" + +#: views.py:209 +msgid "Filter removed" +msgstr "" + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "" + +#: views.py:225 +msgid "Filters set saved" +msgstr "" + +#: views.py:241 +msgid "Filters set created" +msgstr "" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "" + +#: views.py:278 +msgid "Filters set activated" +msgstr "" + +#: views.py:307 +msgid "yes" +msgstr "" + +#: views.py:311 +msgid "no" +msgstr "" diff --git a/modoboa/sievefilters/locale/en/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/en/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..a7c3f457c --- /dev/null +++ b/modoboa/sievefilters/locale/en/LC_MESSAGES/djangojs.po @@ -0,0 +1,34 @@ +# 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: 2015-02-11 13:54+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" +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "" +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "" +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "" +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "" +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "" diff --git a/modoboa/sievefilters/locale/es/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/es/LC_MESSAGES/django.po new file mode 100644 index 000000000..7a319918e --- /dev/null +++ b/modoboa/sievefilters/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,364 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Evilham , 2017 +# Pedro Gracia , 2016 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 18:04+0100\n" +"PO-Revision-Date: 2017-12-01 17:05+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Spanish (http://www.transifex.com/tonio/modoboa/language/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: constants.py:6 +msgid "contains" +msgstr "contiene" + +#: constants.py:7 +msgid "does not contain" +msgstr "no contiene" + +#: constants.py:8 +msgid "is" +msgstr "es" + +#: constants.py:9 +msgid "is not" +msgstr "no es" + +#: constants.py:14 +msgid "Subject" +msgstr "Tema" + +#: constants.py:17 +msgid "Sender" +msgstr "Remitente" + +#: constants.py:20 +msgid "Recipient" +msgstr "Destinatario" + +#: constants.py:23 +msgid "Cc" +msgstr "Cc" + +#: constants.py:25 +msgid "Size" +msgstr "Tamaño" + +#: constants.py:26 +msgid "is greater than" +msgstr "es mayor que" + +#: constants.py:27 +msgid "is less than" +msgstr "es menor que" + +#: constants.py:31 +msgid "Move message to" +msgstr "Mover el mensaje a" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "Redirigir el mensaje a" + +#: constants.py:48 +msgid "Reject message" +msgstr "Bloquear mensaje" + +#: constants.py:50 +msgid "Stop processing" +msgstr "Detener procesamiento" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "Marcar para activar este conjunto de filtros" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Nombre" + +#: forms.py:89 +msgid "Match type" +msgstr "" + +#: forms.py:90 +msgid "All of the following" +msgstr "Todos los siguientes" + +#: forms.py:91 +msgid "Any of the following" +msgstr "Cualquiera de los siguientes" + +#: forms.py:92 +msgid "All messages" +msgstr "Todos los mensajes" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "Nombre de filtro erróneo" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "Configuración de ManageSieve" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "Dirección del servidor" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "Dirección de su servidor MANAGESIEVE" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "Puerto del servidor" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "Puerto a la escucha de su servidor MANAGESIEVE" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "Conectar usando STARTTLS" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "Usar la extensión STARTTLS" + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "Mecanismos de autenticación" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "Mecanismo de autenticación preferido" + +#: forms.py:381 +msgid "IMAP settings" +msgstr "Configuración de IMAP" + +#: forms.py:386 +msgid "Address of your IMAP server" +msgstr "Dirección de su servidor IMAP" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "Usar una conexión segura" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "Usar una conexión segura para acceder al servidor IMAP" + +#: forms.py:398 +msgid "Listening port of your IMAP server" +msgstr "Puerto de su servidor IMAP" + +#: forms.py:405 +msgid "General" +msgstr "General" + +#: forms.py:409 +msgid "Editor mode" +msgstr "Modo editor" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "Seleccione el modo en el que desea que el editor trabaje" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "Mailboxes" + +#: forms.py:419 +msgid "Trash folder" +msgstr "Carpeta de basura" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "Carpeta dónde van los mensajes eliminados" + +#: forms.py:425 +msgid "Sent folder" +msgstr "Carpeta de enviados" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "Carpeta dónde van las copias de los correos enviados" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "Carpeta de borradores" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "Carpeta dónde van los borradores" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "Filtros de mensajes" + +#: imaputils.py:99 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Fallo al recuperar el delimitador jerárquico" + +#: imaputils.py:135 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "Falló la conexión con el servidor IMAP: %s" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "La conexión al servidor MANAGESIEVE falló, verifique su configuración." + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "No hay suficiente espacio en el servidor" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "Extensión para crear filtros en el servidor de forma sencilla" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "filtros Sieve" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "Condiciones" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "Acciones" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Activo" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "No hay un filtro definido aún" + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "Conjunto de filtros" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "activo" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "No hay un conjunto de filtros definido aún. Debe crear uno para poder definir filtros." + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "Nuevo conjunto de filtros" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "Nuevo filtro" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "Guardar conjunto de filtros" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "Activar conjunto de filtros" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "Eliminar conjunto de filtros" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "Descargar" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "Editar filtro" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "Eliminar este filtro" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "Subir este filtro" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "Bajar este filtro" + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "Fallo en la recuperación de un conjunto de filtros" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "Crear" + +#: views.py:161 +msgid "Filter created" +msgstr "Filtro creado" + +#: views.py:179 +msgid "Update" +msgstr "Actualizar" + +#: views.py:186 +msgid "Filter modified" +msgstr "Filtro modificado" + +#: views.py:209 +msgid "Filter removed" +msgstr "Filtro eliminado" + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "La eliminación del filtro falló" + +#: views.py:225 +msgid "Filters set saved" +msgstr "Filtros guardados" + +#: views.py:241 +msgid "Filters set created" +msgstr "Conjunto de filtros creado" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "Crear un nuevo conjunto de filtros" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "Conjunto de filtros eliminado" + +#: views.py:278 +msgid "Filters set activated" +msgstr "Conjunto de filtros activado" + +#: views.py:307 +msgid "yes" +msgstr "sí" + +#: views.py:311 +msgid "no" +msgstr "no" diff --git a/modoboa/sievefilters/locale/es/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/es/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..22af1f1f0 --- /dev/null +++ b/modoboa/sievefilters/locale/es/LC_MESSAGES/djangojs.po @@ -0,0 +1,41 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Pedro Gracia , 2011,2013 +# Pedro Gracia , 2010-2011 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-02-11 13:54+0100\n" +"PO-Revision-Date: 2017-09-22 09:57+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Spanish (http://www.transifex.com/tonio/modoboa/language/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "¿Eliminar este filtro?" + +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "¿Guardar cambios?" + +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "¿Eliminar este conjunto de filtros?" + +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "¿Activar este conjunto de filtros?" + +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "activo" diff --git a/modoboa/sievefilters/locale/fr/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 000000000..23a9600b2 --- /dev/null +++ b/modoboa/sievefilters/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,366 @@ +# 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: 2017-12-01 18:04+0100\n" +"PO-Revision-Date: 2017-12-01 18:06+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" + +#: constants.py:6 +msgid "contains" +msgstr "contient" + +#: constants.py:7 +msgid "does not contain" +msgstr "ne contient pas" + +#: constants.py:8 +msgid "is" +msgstr "est" + +#: constants.py:9 +msgid "is not" +msgstr "n'est pas" + +#: constants.py:14 +msgid "Subject" +msgstr "Sujet" + +#: constants.py:17 +msgid "Sender" +msgstr "Expéditeur" + +#: constants.py:20 +msgid "Recipient" +msgstr "Destinataire" + +#: constants.py:23 +msgid "Cc" +msgstr "Cc" + +#: constants.py:25 +msgid "Size" +msgstr "Taille" + +#: constants.py:26 +msgid "is greater than" +msgstr "est supérieure à" + +#: constants.py:27 +msgid "is less than" +msgstr "est inférieure à" + +#: constants.py:31 +msgid "Move message to" +msgstr "Déplacer le message vers" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "Conserver une copie locale" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "Rediriger le message vers" + +#: constants.py:48 +msgid "Reject message" +msgstr "Rejeter le message" + +#: constants.py:50 +msgid "Stop processing" +msgstr "Arrêter le traitement" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "Cocher pour activer ce jeu de filtres" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Nom" + +#: forms.py:89 +msgid "Match type" +msgstr "Correspondance" + +#: forms.py:90 +msgid "All of the following" +msgstr "Toutes les conditions suivantes" + +#: forms.py:91 +msgid "Any of the following" +msgstr "Au moins une des conditions suivantes" + +#: forms.py:92 +msgid "All messages" +msgstr "Tous les messages" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "Nom de filtre invalide" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "Paramètres ManageSieve" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "Adresse du serveur" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "Adresse de votre serveur ManageSieve" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "Port utilisé par le serveur" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "Port d'écoute de votre serveur ManageSieve" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "Se connecter en utilisant STARTTLS" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "Utiliser l'extension STARTTLS" + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "Méthode d'authentification" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "Méthode d'authentification préférée" + +#: forms.py:381 +msgid "IMAP settings" +msgstr "Paramètres IMAP" + +#: forms.py:386 +msgid "Address of your IMAP server" +msgstr "Adresse de votre serveur IMAP" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "Utiliser une connexion sécurisée" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "Utiliser une connexion sécurisée pour accéder au serveur IMAP" + +#: forms.py:398 +msgid "Listening port of your IMAP server" +msgstr "Port d'écoute de votre serveur IMAP" + +#: forms.py:405 +msgid "General" +msgstr "Général" + +#: forms.py:409 +msgid "Editor mode" +msgstr "Mode de l'editeur" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "Sélectionner le mode de fonctionnement de l'éditeur" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "Boîtes aux lettres" + +#: forms.py:419 +msgid "Trash folder" +msgstr "Dossier corbeille" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "Dossier où vont les messages supprimés" + +#: forms.py:425 +msgid "Sent folder" +msgstr "Dossier d'envoi" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "Dossier où sont copiés les messages envoyés" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "Dossier brouillon" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "Dossier où sont stockés les brouillons" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "Filtres de messages" + +#: imaputils.py:99 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Impossible de récupérer le délimiteur de hierarchie" + +#: imaputils.py:135 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "Échec de connexion au serveur IMAP : %s" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "" +"La connexion au serveur ManageSieve a échoué, vérifiez votre configuration" + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "Pas assez d'espace sur le serveur" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "Extension pour créer simplement des filtres côté serveur" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "Filtres Sieve" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "Conditions" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "Actions" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Actif" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Aucun filtre défini." + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "Jeux de filtres" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "actif" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "" +"Aucun jeu de filtres défini. Vous devez en créer un pour pouvoir définir des " +"filtres." + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "Nouveau jeu de filtres" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "Nouveau filtre" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "Sauvegarder le jeu de filtres" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "Activer le jeu de filtres" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "Supprimer le jeu de filtres" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "Télécharger" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "Editer le filtre" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "Supprimer ce filtre" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "Déplacer ce filtre vers le haut" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "Déplacer ce filtre vers le bas" + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "Impossible de récuperer le jeu de filtres" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "Créer" + +#: views.py:161 +msgid "Filter created" +msgstr "Filtre créé" + +#: views.py:179 +msgid "Update" +msgstr "Mettre à jour" + +#: views.py:186 +msgid "Filter modified" +msgstr "Filtre modifié" + +#: views.py:209 +msgid "Filter removed" +msgstr "Filtre supprimé" + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "Impossible de supprimer le filtre" + +#: views.py:225 +msgid "Filters set saved" +msgstr "Jeu de filtres sauvegardé" + +#: views.py:241 +msgid "Filters set created" +msgstr "Jeu de filtres créé" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "Créer un nouveau jeu de filtres" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "Jeu de filtres supprimé" + +#: views.py:278 +msgid "Filters set activated" +msgstr "Jeu de filtres activé" + +#: views.py:307 +msgid "yes" +msgstr "oui" + +#: views.py:311 +msgid "no" +msgstr "non" diff --git a/modoboa/sievefilters/locale/fr/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/fr/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..a5d1a80bb --- /dev/null +++ b/modoboa/sievefilters/locale/fr/LC_MESSAGES/djangojs.po @@ -0,0 +1,39 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-02-11 13:54+0100\n" +"PO-Revision-Date: 2017-09-22 09:57+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: French (http://www.transifex.com/tonio/modoboa/language/fr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "Supprimer ce filtre?" + +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "Enregistrer les changements?" + +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "Supprimer ce jeu de filtres?" + +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "Activer ce jeu de filtres?" + +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "actif" diff --git a/modoboa/sievefilters/locale/it/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/it/LC_MESSAGES/django.po new file mode 100644 index 000000000..b9c99de6c --- /dev/null +++ b/modoboa/sievefilters/locale/it/LC_MESSAGES/django.po @@ -0,0 +1,365 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Giancarlo Gaifas , 2013 +# marcotrevisan , 2018 +# Rocco , 2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 18:04+0100\n" +"PO-Revision-Date: 2018-11-01 22:22+0000\n" +"Last-Translator: marcotrevisan \n" +"Language-Team: Italian (http://www.transifex.com/tonio/modoboa/language/it/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: constants.py:6 +msgid "contains" +msgstr "contiene" + +#: constants.py:7 +msgid "does not contain" +msgstr "non contiene" + +#: constants.py:8 +msgid "is" +msgstr "è" + +#: constants.py:9 +msgid "is not" +msgstr "non è" + +#: constants.py:14 +msgid "Subject" +msgstr "Oggetto" + +#: constants.py:17 +msgid "Sender" +msgstr "Mittente" + +#: constants.py:20 +msgid "Recipient" +msgstr "Destinatario" + +#: constants.py:23 +msgid "Cc" +msgstr "Cc" + +#: constants.py:25 +msgid "Size" +msgstr "Dimensione" + +#: constants.py:26 +msgid "is greater than" +msgstr "è maggiore di" + +#: constants.py:27 +msgid "is less than" +msgstr "è minore di" + +#: constants.py:31 +msgid "Move message to" +msgstr "Sposta il messaggio in" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "Mantieni copia locale" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "Ridireziona il messaggio a" + +#: constants.py:48 +msgid "Reject message" +msgstr "Rifiuta il messaggio" + +#: constants.py:50 +msgid "Stop processing" +msgstr "Termina l'elaborazione" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "Seleziona per attivare questo insieme di filtri" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Nome" + +#: forms.py:89 +msgid "Match type" +msgstr "Tipo di confronto" + +#: forms.py:90 +msgid "All of the following" +msgstr "Tutti i seguenti" + +#: forms.py:91 +msgid "Any of the following" +msgstr "Alcuni dei seguenti" + +#: forms.py:92 +msgid "All messages" +msgstr "Tutti i messaggi" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "Nome del filtro errato" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "Impostazioni ManageSieve" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "Indirizzo del server" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "Indirizzo del tuo server MANAGESIEVE" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "Porta del server" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "Porta di ascolto del tuo servere MANAGESIEVE" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "Connetti utilizzando STARTTLS" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "Usa l'estensione STARTTLS" + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "Meccanismo di autenticazione" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "Meccanismo di autenticazione preferito" + +#: forms.py:381 +msgid "IMAP settings" +msgstr "Impostazioni IMAP" + +#: forms.py:386 +msgid "Address of your IMAP server" +msgstr "Indirizzo del server IMAP" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "Utilizza connessione sicura" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "Utilizza una connessione sicura per l'accesso al server IMAP" + +#: forms.py:398 +msgid "Listening port of your IMAP server" +msgstr "Porta in ascolto del server IMAP" + +#: forms.py:405 +msgid "General" +msgstr "Generale" + +#: forms.py:409 +msgid "Editor mode" +msgstr "Modo editor" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "Seleziona la modalità di lavoro che desideri per l'editor" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "Caselle di posta" + +#: forms.py:419 +msgid "Trash folder" +msgstr "Cestino" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "Cartella dove finiscono i messaggi eliminati" + +#: forms.py:425 +msgid "Sent folder" +msgstr "Inviati" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "Cartella dove finiscono le copie dei messaggi spediti" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "Bozze" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "Cartella dove finiscono le bozze" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "Filtri messaggi" + +#: imaputils.py:99 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Impossibile recuperare il delimitatore delle gerarchie" + +#: imaputils.py:135 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "Connessione al server IMAP fallita: %s" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "Connessione al server MANAGESIEVE fallita, controlla la tua configurazione" + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "Non c'è spazio sufficiente nel server" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "Plugin per creare facilmente filtri lato-server" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "Filtri Sieve" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "Condizioni" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "Azioni" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Attivo" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Non ci sono ancora filtri definiti" + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "Impostazioni dei filtri" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "attivo" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "Non è ancora stato definito alcun gruppo di filtri. Devi crearne uno per definire dei filtri" + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "Nuovo gruppo di filtri" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "Nuovo filtro" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "Salva il gruppo di filtri" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "Attiva gruppo di filtri" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "Elimina gruppo di filtri" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "Download" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "Modifica filtro" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "Elimina questo filtro" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "Sposta questo filtro in alto" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "Sposta questo filtro in basso " + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "Recupero delle impostazioni di filtro fallito" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "Crea" + +#: views.py:161 +msgid "Filter created" +msgstr "Filtro creato" + +#: views.py:179 +msgid "Update" +msgstr "Aggiorna" + +#: views.py:186 +msgid "Filter modified" +msgstr "Filtro modificato" + +#: views.py:209 +msgid "Filter removed" +msgstr "Filtro eliminato" + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "Rimozione del filtro fallita" + +#: views.py:225 +msgid "Filters set saved" +msgstr "Gruppo filtri salvato" + +#: views.py:241 +msgid "Filters set created" +msgstr "Gruppo filtri creato" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "Crea un nuovo gruppo di filtri" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "Gruppo filtri eliminato" + +#: views.py:278 +msgid "Filters set activated" +msgstr "Gruppo filtri attivato" + +#: views.py:307 +msgid "yes" +msgstr "si" + +#: views.py:311 +msgid "no" +msgstr "no" diff --git a/modoboa/sievefilters/locale/it/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/it/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..4544e1079 --- /dev/null +++ b/modoboa/sievefilters/locale/it/LC_MESSAGES/djangojs.po @@ -0,0 +1,39 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-02-11 13:54+0100\n" +"PO-Revision-Date: 2017-09-22 09:57+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Italian (http://www.transifex.com/tonio/modoboa/language/it/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "Rimuovere questo filtro?" + +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "Salvare le modifiche?" + +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "Rimuovere il gruppo di filtri?" + +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "Attivare il gruppo di filtri?" + +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "attivo" diff --git a/modoboa/sievefilters/locale/nl_NL/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/nl_NL/LC_MESSAGES/django.po new file mode 100644 index 000000000..bdf421920 --- /dev/null +++ b/modoboa/sievefilters/locale/nl_NL/LC_MESSAGES/django.po @@ -0,0 +1,380 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Andrew , 2014 +# TuxBrother , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 18:04+0100\n" +"PO-Revision-Date: 2014-12-31 11:37+0000\n" +"Last-Translator: TuxBrother \n" +"Language-Team: Dutch (Netherlands) (http://www.transifex.com/projects/p/" +"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" + +#: constants.py:6 +msgid "contains" +msgstr "bevat" + +#: constants.py:7 +msgid "does not contain" +msgstr "bevat niet" + +#: constants.py:8 +msgid "is" +msgstr "is" + +#: constants.py:9 +msgid "is not" +msgstr "is niet" + +#: constants.py:14 +msgid "Subject" +msgstr "Onderwerp" + +#: constants.py:17 +msgid "Sender" +msgstr "Afzender" + +#: constants.py:20 +msgid "Recipient" +msgstr "Ontvanger" + +#: constants.py:23 +msgid "Cc" +msgstr "CC" + +#: constants.py:25 +msgid "Size" +msgstr "Grootte" + +#: constants.py:26 +msgid "is greater than" +msgstr "is groter dan" + +#: constants.py:27 +msgid "is less than" +msgstr "is minder dan" + +#: constants.py:31 +msgid "Move message to" +msgstr "Verplaats bericht naar" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "Bericht doorsturen naar" + +#: constants.py:48 +#, fuzzy +#| msgid "Redirect message to" +msgid "Reject message" +msgstr "Bericht doorsturen naar" + +#: constants.py:50 +msgid "Stop processing" +msgstr "" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "Aanvinken om deze filterset te activeren" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Naam" + +#: forms.py:89 +msgid "Match type" +msgstr "" + +#: forms.py:90 +msgid "All of the following" +msgstr "Alles van de volgende" + +#: forms.py:91 +msgid "Any of the following" +msgstr "Elke van het volgende" + +#: forms.py:92 +msgid "All messages" +msgstr "Alle berichten" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "Verkeerde filter naam" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "ManageSieve instellingen" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "Server adres" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "Adres van uw MANAGESIEVE server" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "Server poort" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "Poort van uw MANAGESIEVE server" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "Verbind dmv. STARTTLS" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "Gebruik de STARTTLS extensie (aanbevolen)" + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "Authenticatie mechanisme" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "Geprefereerde authenticatie mechanisme" + +#: forms.py:381 +#, fuzzy +#| msgid "ManageSieve settings" +msgid "IMAP settings" +msgstr "ManageSieve instellingen" + +#: forms.py:386 +#, fuzzy +#| msgid "Address of your MANAGESIEVE server" +msgid "Address of your IMAP server" +msgstr "Adres van uw MANAGESIEVE server" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "" + +#: forms.py:398 +#, fuzzy +#| msgid "Listening port of your MANAGESIEVE server" +msgid "Listening port of your IMAP server" +msgstr "Poort van uw MANAGESIEVE server" + +#: forms.py:405 +msgid "General" +msgstr "Algemeen" + +#: forms.py:409 +msgid "Editor mode" +msgstr "Editor mode" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "Selecteer de mode waarin u de editor wilt hebben" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "" + +#: forms.py:419 +msgid "Trash folder" +msgstr "" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "" + +#: forms.py:425 +msgid "Sent folder" +msgstr "" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "Berichtfilters" + +#: imaputils.py:99 +#, fuzzy +#| msgid "Failed to retrieve filters set" +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Mislukt om de filterset te te laden" + +#: imaputils.py:135 +#, fuzzy, python-format +#| msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgid "Connection to IMAP server failed: %s" +msgstr "Verbinding naar MANAGESIEVE server mislukt, controleer uw configuratie" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "Verbinding naar MANAGESIEVE server mislukt, controleer uw configuratie" + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "Niet genoeg ruimte beschikbaar op de server" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "" +"Plugin om gemakkelijk filters op de server in te stellen (bijvoorbeeld om uw " +"mail te sorteren)" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "Sieve filters" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "Voorwaarden" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "Acties" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Actief" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Nog geen filter aangemaakt" + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "Filtersets" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "actief" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "" +"Nog geen filterset gedefinieerd. U moet een filterset aanmaken om filters in " +"te kunnen stellen." + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "Nieuwe filterset" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "Nieuw filter" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "Filterset opslaan" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "Filterset activeren" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "Filterset verwijderen" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "Download" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "Filter bewerken" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "Dit filter verwijderen" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "Dit filter omhoogplaatsen" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "Dit filter omlaagplaatsen" + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "Mislukt om de filterset te te laden" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "Aanmaken" + +#: views.py:161 +msgid "Filter created" +msgstr "Filter aangemaakt" + +#: views.py:179 +msgid "Update" +msgstr "Toepassen" + +#: views.py:186 +msgid "Filter modified" +msgstr "Filter aangepast" + +#: views.py:209 +msgid "Filter removed" +msgstr "Filter verwijderd" + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "Mislukt om het filter te verwijderen" + +#: views.py:225 +msgid "Filters set saved" +msgstr "Filterset opgeslagen" + +#: views.py:241 +msgid "Filters set created" +msgstr "Filterset aangemaakt" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "Maak een nieuwe filterset aan" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "Filterset verwijderd" + +#: views.py:278 +msgid "Filters set activated" +msgstr "Filterset geactiveerd" + +#: views.py:307 +msgid "yes" +msgstr "ja" + +#: views.py:311 +msgid "no" +msgstr "nee" diff --git a/modoboa/sievefilters/locale/nl_NL/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/nl_NL/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..54073170c --- /dev/null +++ b/modoboa/sievefilters/locale/nl_NL/LC_MESSAGES/djangojs.po @@ -0,0 +1,41 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Ronald Otto , 2017 +# TuxBrother , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-02-11 13:54+0100\n" +"PO-Revision-Date: 2017-09-22 09:57+0000\n" +"Last-Translator: Ronald Otto \n" +"Language-Team: Dutch (Netherlands) (http://www.transifex.com/tonio/modoboa/language/nl_NL/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: nl_NL\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "Dit filter verwijderen?" + +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "Wijzigingen opslaan?" + +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "Deze filterset verwijderen?" + +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "Wilt u deze filterset activeren?" + +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "actief" diff --git a/modoboa/sievefilters/locale/pl_PL/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/pl_PL/LC_MESSAGES/django.po new file mode 100644 index 000000000..5811fa9eb --- /dev/null +++ b/modoboa/sievefilters/locale/pl_PL/LC_MESSAGES/django.po @@ -0,0 +1,365 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Artur Slowik , 2017 +# Dariusz Koryto , 2016 +# Tomasz Proć , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 18:04+0100\n" +"PO-Revision-Date: 2017-12-02 10:16+0000\n" +"Last-Translator: Tomasz Proć \n" +"Language-Team: Polish (Poland) (http://www.transifex.com/tonio/modoboa/language/pl_PL/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pl_PL\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" + +#: constants.py:6 +msgid "contains" +msgstr "zawiera" + +#: constants.py:7 +msgid "does not contain" +msgstr "nie zawiera" + +#: constants.py:8 +msgid "is" +msgstr "jest" + +#: constants.py:9 +msgid "is not" +msgstr "nie jest" + +#: constants.py:14 +msgid "Subject" +msgstr "Temat" + +#: constants.py:17 +msgid "Sender" +msgstr "Nadawca" + +#: constants.py:20 +msgid "Recipient" +msgstr "Odbioraca" + +#: constants.py:23 +msgid "Cc" +msgstr "DW" + +#: constants.py:25 +msgid "Size" +msgstr "Rozmiar" + +#: constants.py:26 +msgid "is greater than" +msgstr "jest większe niż" + +#: constants.py:27 +msgid "is less than" +msgstr "jest mniejsze niż" + +#: constants.py:31 +msgid "Move message to" +msgstr "Przenieś wiadomość do" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "Zachowaj kopię" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "Przekieruj wiadomość do" + +#: constants.py:48 +msgid "Reject message" +msgstr "Odrzuć wiadomość" + +#: constants.py:50 +msgid "Stop processing" +msgstr "Zatrzymaj przetwarzanie" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "Zaznacz aby uruchomić ten zestaw filtrów" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Nazwa" + +#: forms.py:89 +msgid "Match type" +msgstr "Typ dopasowania" + +#: forms.py:90 +msgid "All of the following" +msgstr "Wszystkie poniższe" + +#: forms.py:91 +msgid "Any of the following" +msgstr "Żadne z poniższych" + +#: forms.py:92 +msgid "All messages" +msgstr "Wszystkie wiadomości" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "Zła nazwa filtru" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "Ustawienia ManageSieve" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "Adres serwera" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "Addres twojego serwera MANAGESIEVE" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "Port serwera" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "Port twojego serwera MANAGESIEVE" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "Połącz używając STARTTLS" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "Użyj rozszerzenia STARTTLS" + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "Mechanizm autentykacji" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "Preferowany mechanizm uwierzytelnienia" + +#: forms.py:381 +msgid "IMAP settings" +msgstr "Ustawienia IMAP" + +#: forms.py:386 +msgid "Address of your IMAP server" +msgstr "Adres serwera IMAP" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "Użyj bezpiecznego polączenia" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "Użyj bezpiecznego połączenia do serwera IMAP" + +#: forms.py:398 +msgid "Listening port of your IMAP server" +msgstr "Port, na którym nasłuchuje twój serwer IMAP" + +#: forms.py:405 +msgid "General" +msgstr "Ogólne" + +#: forms.py:409 +msgid "Editor mode" +msgstr "Tryb edycji" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "Wybierz tryb w jakim ma działać edytor" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "Skrzynki pocztowe" + +#: forms.py:419 +msgid "Trash folder" +msgstr "Folder kosza" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "Folder, do którego trafiają usunięte wiadomości" + +#: forms.py:425 +msgid "Sent folder" +msgstr "Folder wysłane" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "Folder, do którego trafiają kopie wysłanych wiadomości" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "Folder kopie robocze" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "Folder, do którego trafiają kopie robocze" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "Filtry wiadomości" + +#: imaputils.py:99 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Nie udało się rozpoznać hierarchii separatora" + +#: imaputils.py:135 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "Błąd połączenia do serwera IMAP: %s" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "Błąd połączenia do serwera MANAGESIEVE: %s, sprawdź konfigurację" + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "Brak miejsca na serwerze" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "Wtyczka do łatwego tworzenia filtrów po stronie serwera" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "Filtry Sieve" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "Warunki" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "Akcje" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Aktywne" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Brak zdefiniowanych filtrów" + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "Zestaw filtrów" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "aktywny" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "Nie zdefiniowano zestawu filtrów. Musisz utworzyć przynajmniej jeden." + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "Nowy zestaw filtrów" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "Nowy filter" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "Zapisz zestaw filtrów" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "Uruchom zestaw filtrów" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "Usuń zestaw filtrów" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "Pobierz" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "Edytuj filtr" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "Usuń ten filtr" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "Przenieś ten filtr wyżej" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "Przenieś ten filtr niżej" + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "Nie udało się pobrać zestawu filtrów" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "Utwórz" + +#: views.py:161 +msgid "Filter created" +msgstr "Filtr utworzony" + +#: views.py:179 +msgid "Update" +msgstr "Zaktualizuj" + +#: views.py:186 +msgid "Filter modified" +msgstr "Filtr zmodyfikowano" + +#: views.py:209 +msgid "Filter removed" +msgstr "Filtr usunięto" + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "Nie udało się usunąć filtru" + +#: views.py:225 +msgid "Filters set saved" +msgstr "Zestaw filtrów zapisano" + +#: views.py:241 +msgid "Filters set created" +msgstr "Zestaw filtrów utworzono" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "Utwórz nowy zestaw filtrów" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "Filtry skasowano" + +#: views.py:278 +msgid "Filters set activated" +msgstr "Filtry usunięto" + +#: views.py:307 +msgid "yes" +msgstr "tak" + +#: views.py:311 +msgid "no" +msgstr "nie" diff --git a/modoboa/sievefilters/locale/pl_PL/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/pl_PL/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..e14691c6f --- /dev/null +++ b/modoboa/sievefilters/locale/pl_PL/LC_MESSAGES/djangojs.po @@ -0,0 +1,41 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Dariusz Koryto , 2016 +# Tomasz Proć , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-02-11 13:54+0100\n" +"PO-Revision-Date: 2017-12-01 16:15+0000\n" +"Last-Translator: Tomasz Proć \n" +"Language-Team: Polish (Poland) (http://www.transifex.com/tonio/modoboa/language/pl_PL/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pl_PL\n" +"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n" + +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "Usunać ten filter?" + +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "Zapisać zmiany?" + +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "Usunąć zestaw filtrów?" + +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "Aktywować ten zestaw filtrów? " + +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "aktywne" diff --git a/modoboa/sievefilters/locale/pt_BR/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/pt_BR/LC_MESSAGES/django.po new file mode 100644 index 000000000..9e95ab8a5 --- /dev/null +++ b/modoboa/sievefilters/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,367 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# csmk, 2014 +# Felipe Morato , 2017 +# Paulino Michelazzo , 2014-2015 +# Rafael Barretto , 2013 +# Rafael Barretto , 2017-2018 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 18:04+0100\n" +"PO-Revision-Date: 2018-09-25 03:25+0000\n" +"Last-Translator: Rafael Barretto \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/tonio/modoboa/language/pt_BR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: constants.py:6 +msgid "contains" +msgstr "contém" + +#: constants.py:7 +msgid "does not contain" +msgstr "não contém" + +#: constants.py:8 +msgid "is" +msgstr "é" + +#: constants.py:9 +msgid "is not" +msgstr "não é" + +#: constants.py:14 +msgid "Subject" +msgstr "Assunto" + +#: constants.py:17 +msgid "Sender" +msgstr "Remetente" + +#: constants.py:20 +msgid "Recipient" +msgstr "Destinatário" + +#: constants.py:23 +msgid "Cc" +msgstr "Cc" + +#: constants.py:25 +msgid "Size" +msgstr "Tamanho" + +#: constants.py:26 +msgid "is greater than" +msgstr "é maior que" + +#: constants.py:27 +msgid "is less than" +msgstr "é menor que" + +#: constants.py:31 +msgid "Move message to" +msgstr "Mover mensagem para" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "Manter cópia local" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "Redirecionar mensagem para" + +#: constants.py:48 +msgid "Reject message" +msgstr "Rejeitar mensagem" + +#: constants.py:50 +msgid "Stop processing" +msgstr "Parar processo" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "Marque para ativar esse conjunto de filtros" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Nome" + +#: forms.py:89 +msgid "Match type" +msgstr "" + +#: forms.py:90 +msgid "All of the following" +msgstr "Todas as seguintes" + +#: forms.py:91 +msgid "Any of the following" +msgstr "Uma das seguintes" + +#: forms.py:92 +msgid "All messages" +msgstr "Todas as mensagens" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "Nome errado do filtro" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "Configurações do ManageSieve" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "Endereço do servidor" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "Endereço do servidor MANAGESIEVE" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "Porta do servidor" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "Porta de escuta do servidor MANAGESIEVE" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "Conectar usando STARTTLS" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "Usar a extensão STARTTLS " + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "Mecanismo de autenticação" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "Mecanismo de autenticação preferencial" + +#: forms.py:381 +msgid "IMAP settings" +msgstr "Configurações IMAP" + +#: forms.py:386 +msgid "Address of your IMAP server" +msgstr "Endereço do seu servidor IMAP" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "Usar uma conexão segura" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "Usar uma conexão segura para acessar o servidor IMAP" + +#: forms.py:398 +msgid "Listening port of your IMAP server" +msgstr "Porta de escuta do seu servidor IMAP" + +#: forms.py:405 +msgid "General" +msgstr "Geral" + +#: forms.py:409 +msgid "Editor mode" +msgstr "Modo editor" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "Selecione o modo que você deseja que o editor trabalhe" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "Caixas de correio" + +#: forms.py:419 +msgid "Trash folder" +msgstr "Pasta para Itens Deletados" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "Pasta que irão as mensagens deletadas" + +#: forms.py:425 +msgid "Sent folder" +msgstr "Pasta para Itens Enviados" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "Pasta onde irão as cópias de mensagens enviadas" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "Pasta para Rascunhos" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "Pasta onde irão os rascunhos" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "Filtros de mensagem" + +#: imaputils.py:99 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Falha ao recuperar o delimitador de hierarquia" + +#: imaputils.py:135 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "Falha ao conectar no servidor IMAP: %s" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "Conexão com o servidor MANAGESIEVE falhou, verifique sua configuração" + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "Espaço no servidor insuficiente" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "Plugin para criar facilmente filtros 'server-side'" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "Filtros do Sieve" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "Condições" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "Ações" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Ativo" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Sem filtros definidos" + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "Conjuntos de filtros" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "ativo" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "Nenhum conjunto de filtros ainda definido. Você deve criar um para poder definir filtros." + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "Novo conjunto de filtros" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "Novo filtro" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "Salvar conjunto de filtros" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "Ativar conjunto de filtros" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "Remover conjunto de filtros" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "Baixar" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "Editar filtro" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "Remover esse filtro" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "Mover esse filtro para cima" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "Mover esse filtro para baixo" + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "Falha ao recuperar conjunto de filtros" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "Criar" + +#: views.py:161 +msgid "Filter created" +msgstr "Filtro criado" + +#: views.py:179 +msgid "Update" +msgstr "Atualizar" + +#: views.py:186 +msgid "Filter modified" +msgstr "Filtro modificado" + +#: views.py:209 +msgid "Filter removed" +msgstr "Filtro removido" + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "Falha ao remover filtro" + +#: views.py:225 +msgid "Filters set saved" +msgstr "Conjunto de filtros salvo" + +#: views.py:241 +msgid "Filters set created" +msgstr "Conjunto de filtros criado" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "Criar um novo conjunto de filtros" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "Conjunto de filtros apagados" + +#: views.py:278 +msgid "Filters set activated" +msgstr "Conjuntos de filtros ativados" + +#: views.py:307 +msgid "yes" +msgstr "sim" + +#: views.py:311 +msgid "no" +msgstr "não" diff --git a/modoboa/sievefilters/locale/pt_BR/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/pt_BR/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..c47186dfe --- /dev/null +++ b/modoboa/sievefilters/locale/pt_BR/LC_MESSAGES/djangojs.po @@ -0,0 +1,40 @@ +# 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 , 2014 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-02-11 13:54+0100\n" +"PO-Revision-Date: 2017-09-23 21:21+0000\n" +"Last-Translator: Paulino Michelazzo \n" +"Language-Team: Portuguese (Brazil) (http://www.transifex.com/tonio/modoboa/language/pt_BR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "Remover este filtro?" + +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "Salvar alterações?" + +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "Remover conjunto de filtros?" + +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "Ativar este conjunto de filtros?" + +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "ativo" diff --git a/modoboa/sievefilters/locale/pt_PT/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/pt_PT/LC_MESSAGES/django.po new file mode 100644 index 000000000..a7ed3d719 --- /dev/null +++ b/modoboa/sievefilters/locale/pt_PT/LC_MESSAGES/django.po @@ -0,0 +1,381 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Damien Duponchelle , 2013 +# lusitan , 2013 +# Mike C. , 2012 +# Mike C. , 2013-2014 +# Sandra Ribeiro , 2012-2013 +# Sandra Ribeiro , 2012 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 18:04+0100\n" +"PO-Revision-Date: 2014-12-29 12:51+0000\n" +"Last-Translator: Mike C. \n" +"Language-Team: Portuguese (Portugal) (http://www.transifex.com/projects/p/" +"modoboa/language/pt_PT/)\n" +"Language: pt_PT\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" + +#: constants.py:6 +msgid "contains" +msgstr "contém" + +#: constants.py:7 +msgid "does not contain" +msgstr "não contém" + +#: constants.py:8 +msgid "is" +msgstr "é" + +#: constants.py:9 +msgid "is not" +msgstr "não é" + +#: constants.py:14 +msgid "Subject" +msgstr "Assunto" + +#: constants.py:17 +msgid "Sender" +msgstr "Remetente" + +#: constants.py:20 +msgid "Recipient" +msgstr "Destinatário" + +#: constants.py:23 +msgid "Cc" +msgstr "Cc" + +#: constants.py:25 +msgid "Size" +msgstr "Tamanho" + +#: constants.py:26 +msgid "is greater than" +msgstr "é maior do que" + +#: constants.py:27 +msgid "is less than" +msgstr "é menor do que" + +#: constants.py:31 +msgid "Move message to" +msgstr "Mover mensagens para" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "Reencaminhar mensagens para" + +#: constants.py:48 +#, fuzzy +#| msgid "Redirect message to" +msgid "Reject message" +msgstr "Reencaminhar mensagens para" + +#: constants.py:50 +msgid "Stop processing" +msgstr "" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "Marque para ativar estes filtros" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Nome" + +#: forms.py:89 +msgid "Match type" +msgstr "" + +#: forms.py:90 +msgid "All of the following" +msgstr "Todos estes" + +#: forms.py:91 +msgid "Any of the following" +msgstr "Algum destes" + +#: forms.py:92 +msgid "All messages" +msgstr "Todas as mensagens" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "Filtro inválido" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "Definições do MANAGESIEVE" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "Endereço do Servidor" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "Endereço do servidor MANAGESIEVE" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "Porta do Servidor" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "Porta atribuída ao servidor MANAGESIEVE" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "Ligar via STARTTLS" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "Usar a extensão STARTLS" + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "Mecanismo de autenticação" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "Método de autenticação preferencial" + +#: forms.py:381 +#, fuzzy +#| msgid "ManageSieve settings" +msgid "IMAP settings" +msgstr "Definições do MANAGESIEVE" + +#: forms.py:386 +#, fuzzy +#| msgid "Address of your MANAGESIEVE server" +msgid "Address of your IMAP server" +msgstr "Endereço do servidor MANAGESIEVE" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "" + +#: forms.py:398 +#, fuzzy +#| msgid "Listening port of your MANAGESIEVE server" +msgid "Listening port of your IMAP server" +msgstr "Porta atribuída ao servidor MANAGESIEVE" + +#: forms.py:405 +msgid "General" +msgstr "Geral" + +#: forms.py:409 +msgid "Editor mode" +msgstr "Modo de edição" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "Seleccione o modo de funcionamento do editor" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "" + +#: forms.py:419 +msgid "Trash folder" +msgstr "" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "" + +#: forms.py:425 +msgid "Sent folder" +msgstr "" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "Gerir filtros" + +#: imaputils.py:99 +#, fuzzy +#| msgid "Failed to retrieve filters set" +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Falha ao recuperar os filtros definidos" + +#: imaputils.py:135 +#, fuzzy, python-format +#| msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgid "Connection to IMAP server failed: %s" +msgstr "Ligação ao servidor MANAGESIEVE falhou, valide a configuração" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "Ligação ao servidor MANAGESIEVE falhou, valide a configuração" + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "Espaço insuficiente no servidor" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "Plugin para criar facilmente filtros 'server-side'" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "Filtros Sieve" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "Condições" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "Ações" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Ativo" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Ainda não existe nenhum filtro definido" + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "Grupos de filtros" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "ativo" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "" +"Não existem grupos de filtros. Terá de criar um para poder definir filtros." + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "Novo grupo de filtros" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "Novo filtro" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "Guardar grupo de filtros" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "Ativar grupo de filtros" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "Eliminar grupo de filtros" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "Descarregar" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "Editar filtro" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "Eliminar este filtro" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "Mover este filtro para cima" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "Mover este filtro para baixo" + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "Falha ao recuperar os filtros definidos" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "Criar" + +#: views.py:161 +msgid "Filter created" +msgstr "Filtro criado" + +#: views.py:179 +msgid "Update" +msgstr "Actualizar" + +#: views.py:186 +msgid "Filter modified" +msgstr "Filtro modificado" + +#: views.py:209 +msgid "Filter removed" +msgstr "Filtro removido" + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "Falha ao remover filtro" + +#: views.py:225 +msgid "Filters set saved" +msgstr "Grupo de filtros guardado" + +#: views.py:241 +msgid "Filters set created" +msgstr "Grupo de filtros criado" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "Criar novo grupo de filtros" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "Grupo de filtros eliminado" + +#: views.py:278 +msgid "Filters set activated" +msgstr "Grupo de filtros ativado" + +#: views.py:307 +msgid "yes" +msgstr "sim" + +#: views.py:311 +msgid "no" +msgstr "não" diff --git a/modoboa/sievefilters/locale/pt_PT/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/pt_PT/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..5958ad167 --- /dev/null +++ b/modoboa/sievefilters/locale/pt_PT/LC_MESSAGES/djangojs.po @@ -0,0 +1,42 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# lusitan , 2013 +# Mike C. , 2012,2014 +# Sandra Ribeiro , 2013 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-02-11 13:54+0100\n" +"PO-Revision-Date: 2017-09-22 09:57+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Portuguese (Portugal) (http://www.transifex.com/tonio/modoboa/language/pt_PT/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt_PT\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "Remover este filtro?" + +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "Guardar alterações?" + +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "Limpar filtros definidos?" + +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "Activar estes filtros?" + +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "activo" diff --git a/modoboa/sievefilters/locale/ru/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/ru/LC_MESSAGES/django.po new file mode 100644 index 000000000..827376069 --- /dev/null +++ b/modoboa/sievefilters/locale/ru/LC_MESSAGES/django.po @@ -0,0 +1,364 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Ravil Kadyrbayev, 2015 +# Yuriy Matskanyuk , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 18:04+0100\n" +"PO-Revision-Date: 2018-11-27 18:02+0000\n" +"Last-Translator: Yuriy Matskanyuk \n" +"Language-Team: Russian (http://www.transifex.com/tonio/modoboa/language/ru/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" + +#: constants.py:6 +msgid "contains" +msgstr "содержит" + +#: constants.py:7 +msgid "does not contain" +msgstr "не содержит" + +#: constants.py:8 +msgid "is" +msgstr "является" + +#: constants.py:9 +msgid "is not" +msgstr "не" + +#: constants.py:14 +msgid "Subject" +msgstr "Тема" + +#: constants.py:17 +msgid "Sender" +msgstr "Отправитель" + +#: constants.py:20 +msgid "Recipient" +msgstr "Получатель" + +#: constants.py:23 +msgid "Cc" +msgstr "Копия" + +#: constants.py:25 +msgid "Size" +msgstr "размер" + +#: constants.py:26 +msgid "is greater than" +msgstr "больше" + +#: constants.py:27 +msgid "is less than" +msgstr "меньше" + +#: constants.py:31 +msgid "Move message to" +msgstr "Переместить сообщение" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "Перенаправление сообщение для" + +#: constants.py:48 +msgid "Reject message" +msgstr "" + +#: constants.py:50 +msgid "Stop processing" +msgstr "Остановить обработку" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "Проверьте, чтобы активировать этот набор фильтров" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Логин" + +#: forms.py:89 +msgid "Match type" +msgstr "" + +#: forms.py:90 +msgid "All of the following" +msgstr "Любое из следующих" + +#: forms.py:91 +msgid "Any of the following" +msgstr "Любое из следующих" + +#: forms.py:92 +msgid "All messages" +msgstr "Все сообщения" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "Неверное имя фильтра" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "Настройки фильтров содержания" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "Адрес сервера" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "Адрес вашего MANAGESIEVE сервера" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "Порт сервера" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "Порт вашего MANAGESIEVE сервера" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "Подключение с помощью STARTTLS" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "Используйте расширение STARTTLS" + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "Механизм аутентификации" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "Предпочтительный механизм аутентификации" + +#: forms.py:381 +msgid "IMAP settings" +msgstr "" + +#: forms.py:386 +msgid "Address of your IMAP server" +msgstr "" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "Использовать защищенное соединение" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "" + +#: forms.py:398 +msgid "Listening port of your IMAP server" +msgstr "" + +#: forms.py:405 +msgid "General" +msgstr "Основные" + +#: forms.py:409 +msgid "Editor mode" +msgstr "Режим редактирования" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "Выберите нужный режим для работы редактора" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "Почтовые ящики" + +#: forms.py:419 +msgid "Trash folder" +msgstr "Корзина" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "" + +#: forms.py:425 +msgid "Sent folder" +msgstr "Отправленные" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "Черновики" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "Фильтры сообщений" + +#: imaputils.py:99 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "" + +#: imaputils.py:135 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "Соединение с сервером MANAGESIEVE не удалось, проверьте конфигурацию" + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "Нет свободного места на сервере" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "Плагин позволяет создавать фильтры на сервере" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "Фильтры содержания" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "Соответствие" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "Действие" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Активно" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Фильтры не определены" + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "Набор фильтров" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "Активно" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "Фильтры не определены. Вы должны определить и установить фильтры " + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "Новый набор фильтров" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "Новый фильтр" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "Сохранить набор фильтров" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "Активировать набор фильтров" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "Удалить набор фильтров" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "Загрузить" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "Редактировать фильтр" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "Удалить этот фильтр" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "Переместить этот фильтр выше" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "Переместить этот фильтр ниже" + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "Не удалось получить набор фильтров" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "Создать" + +#: views.py:161 +msgid "Filter created" +msgstr "Фильтр создан" + +#: views.py:179 +msgid "Update" +msgstr "Обновление" + +#: views.py:186 +msgid "Filter modified" +msgstr "Фильтр модифицирован" + +#: views.py:209 +msgid "Filter removed" +msgstr "Фильтр удален" + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "Ошибка удаления фильтра" + +#: views.py:225 +msgid "Filters set saved" +msgstr "Набор фильтров сохранен" + +#: views.py:241 +msgid "Filters set created" +msgstr "Набор фильтров создан" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "Создание нового набора фильтров" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "Набор фильтров удален" + +#: views.py:278 +msgid "Filters set activated" +msgstr "Набор фильтров активирован" + +#: views.py:307 +msgid "yes" +msgstr "Да" + +#: views.py:311 +msgid "no" +msgstr "Нет" diff --git a/modoboa/sievefilters/locale/ru/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/ru/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..2764f4fba --- /dev/null +++ b/modoboa/sievefilters/locale/ru/LC_MESSAGES/djangojs.po @@ -0,0 +1,39 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-02-11 13:54+0100\n" +"PO-Revision-Date: 2017-09-22 09:57+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Russian (http://www.transifex.com/tonio/modoboa/language/ru/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ru\n" +"Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" + +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "Удалить фильтр?" + +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "Сохранить изменения?" + +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "Удалить набор фильтров?" + +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "Активировать набор фильтров?" + +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "активно" diff --git a/modoboa/sievefilters/locale/sv/LC_MESSAGES/django.po b/modoboa/sievefilters/locale/sv/LC_MESSAGES/django.po new file mode 100644 index 000000000..662d03955 --- /dev/null +++ b/modoboa/sievefilters/locale/sv/LC_MESSAGES/django.po @@ -0,0 +1,364 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +# Martin Persson , 2014 +# Olle Gustafsson , 2013,2015,2017-2018 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-12-01 18:04+0100\n" +"PO-Revision-Date: 2018-08-14 08:14+0000\n" +"Last-Translator: Olle Gustafsson \n" +"Language-Team: Swedish (http://www.transifex.com/tonio/modoboa/language/sv/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: constants.py:6 +msgid "contains" +msgstr "innehåller" + +#: constants.py:7 +msgid "does not contain" +msgstr "innehåller inte" + +#: constants.py:8 +msgid "is" +msgstr "är" + +#: constants.py:9 +msgid "is not" +msgstr "är inte" + +#: constants.py:14 +msgid "Subject" +msgstr "Ämne" + +#: constants.py:17 +msgid "Sender" +msgstr "Avsändare" + +#: constants.py:20 +msgid "Recipient" +msgstr "Mottagare" + +#: constants.py:23 +msgid "Cc" +msgstr "Cc" + +#: constants.py:25 +msgid "Size" +msgstr "Storlek" + +#: constants.py:26 +msgid "is greater than" +msgstr "är större än" + +#: constants.py:27 +msgid "is less than" +msgstr "är mindre än" + +#: constants.py:31 +msgid "Move message to" +msgstr "Flytta meddelande till" + +#: constants.py:36 constants.py:44 +msgid "Keep local copy" +msgstr "Behåll lokal kopia" + +#: constants.py:40 +msgid "Redirect message to" +msgstr "Omdirigera meddelande till" + +#: constants.py:48 +msgid "Reject message" +msgstr "Avvisa meddelande" + +#: constants.py:50 +msgid "Stop processing" +msgstr "Sluta bearbeta" + +#: forms.py:33 +msgid "Check to activate this filters set" +msgstr "Markera för att aktivera denna filter regel" + +#: forms.py:87 templates/modoboa_sievefilters/guieditor.html:6 +msgid "Name" +msgstr "Namn" + +#: forms.py:89 +msgid "Match type" +msgstr "Matcha typ" + +#: forms.py:90 +msgid "All of the following" +msgstr "Alla av följande" + +#: forms.py:91 +msgid "Any of the following" +msgstr "Någon av följande" + +#: forms.py:92 +msgid "All messages" +msgstr "Alla meddelanden" + +#: forms.py:107 +msgid "Wrong filter name" +msgstr "Fel filter namn" + +#: forms.py:351 +msgid "ManageSieve settings" +msgstr "ManageSieve inställningar" + +#: forms.py:354 forms.py:384 +msgid "Server address" +msgstr "Server adress" + +#: forms.py:356 +msgid "Address of your MANAGESIEVE server" +msgstr "Adressen till din MANAGESIEVE server" + +#: forms.py:361 forms.py:396 +msgid "Server port" +msgstr "Server port" + +#: forms.py:363 +msgid "Listening port of your MANAGESIEVE server" +msgstr "MANAGESIEVE server lyssnar på port" + +#: forms.py:368 +msgid "Connect using STARTTLS" +msgstr "Anslut med STARTTLS" + +#: forms.py:370 +msgid "Use the STARTTLS extension" +msgstr "Använd STARTTLS" + +#: forms.py:374 +msgid "Authentication mechanism" +msgstr "Autentiseringsmekanism" + +#: forms.py:377 +msgid "Prefered authentication mechanism" +msgstr "Önskad autentiseringsmekanism" + +#: forms.py:381 +msgid "IMAP settings" +msgstr "IMAP inställningar" + +#: forms.py:386 +msgid "Address of your IMAP server" +msgstr "Adress för din IMAP server" + +#: forms.py:390 +msgid "Use a secured connection" +msgstr "Använd en säker anslutning" + +#: forms.py:392 +msgid "Use a secured connection to access IMAP server" +msgstr "Använd en säker anslutning för att ansluta till IMAP server" + +#: forms.py:398 +msgid "Listening port of your IMAP server" +msgstr "Lyssningsport för din IMAP server" + +#: forms.py:405 +msgid "General" +msgstr "Generellt" + +#: forms.py:409 +msgid "Editor mode" +msgstr "Redigeringsläge" + +#: forms.py:411 +msgid "Select the mode you want the editor to work in" +msgstr "Välj det läge du vill redigeraren att arbeta i" + +#: forms.py:415 +msgid "Mailboxes" +msgstr "Brevlådor" + +#: forms.py:419 +msgid "Trash folder" +msgstr "Skräpkatalog" + +#: forms.py:420 +msgid "Folder where deleted messages go" +msgstr "Katalog där raderade meddelanden hamnar" + +#: forms.py:425 +msgid "Sent folder" +msgstr "Skickat katalog" + +#: forms.py:426 +msgid "Folder where copies of sent messages go" +msgstr "Katalog där en kopia av skickade meddelanden hamnar" + +#: forms.py:431 +msgid "Drafts folder" +msgstr "Utkast katalog" + +#: forms.py:432 +msgid "Folder where drafts go" +msgstr "Katalog där utkast sparas" + +#: handlers.py:19 modo_extension.py:26 +#: templates/modoboa_sievefilters/index.html:4 +msgid "Message filters" +msgstr "Meddelandefilter" + +#: imaputils.py:99 +msgid "Failed to retrieve hierarchy delimiter" +msgstr "Misslyckades med att hämta hierarkinavgränsaren" + +#: imaputils.py:135 +#, python-format +msgid "Connection to IMAP server failed: %s" +msgstr "Anslutningen till IMAP servern misslyckades: %s" + +#: lib.py:54 +msgid "Connection to MANAGESIEVE server failed, check your configuration" +msgstr "Anslutning till MANAGESIEVE servern misslyckades, kontrollera din konfiguration" + +#: lib.py:98 +msgid "Not enough space on server" +msgstr "Inte tillräckligt med utrymme på servern" + +#: modo_extension.py:18 +msgid "Plugin to easily create server-side filters" +msgstr "Plugin för att enkelt skapa filter på serversidan" + +#: modo_extension.py:24 +msgid "Sieve filters" +msgstr "Sieve filter" + +#: templates/modoboa_sievefilters/filter.html:9 +msgid "Conditions" +msgstr "Villkor" + +#: templates/modoboa_sievefilters/filter.html:20 +#: templates/modoboa_sievefilters/guieditor.html:5 views.py:104 +msgid "Actions" +msgstr "Åtgärder" + +#: templates/modoboa_sievefilters/guieditor.html:7 +msgid "Active" +msgstr "Aktivera" + +#: templates/modoboa_sievefilters/guieditor.html:24 +msgid "No filter defined yet" +msgstr "Inga filter definierade än" + +#: templates/modoboa_sievefilters/index.html:82 +msgid "Filter sets" +msgstr "Filter uppsättning" + +#: templates/modoboa_sievefilters/index.html:86 +msgid "active" +msgstr "aktivera" + +#: templates/modoboa_sievefilters/index.html:101 +msgid "" +"No filters set defined yet. You must create one in order to define filters." +msgstr "Inga filter uppsättningar definierade. Du måste skapa en uppsättning för att kunna definiera filter." + +#: templatetags/sfilters_tags.py:21 +msgid "New filters set" +msgstr "Ny filter uppsättning" + +#: templatetags/sfilters_tags.py:39 views.py:153 +msgid "New filter" +msgstr "Nytt filter" + +#: templatetags/sfilters_tags.py:50 +msgid "Save filters set" +msgstr "Spara filter uppsättning" + +#: templatetags/sfilters_tags.py:58 +msgid "Activate filters set" +msgstr "Aktivera filter uppsättning" + +#: templatetags/sfilters_tags.py:63 +msgid "Remove filters set" +msgstr "Radera filter uppsättning" + +#: templatetags/sfilters_tags.py:67 +msgid "Download" +msgstr "Ladda ner" + +#: templatetags/sfilters_tags.py:82 views.py:175 +msgid "Edit filter" +msgstr "Redigera filter" + +#: templatetags/sfilters_tags.py:90 +msgid "Remove this filter" +msgstr "Radera detta filter" + +#: templatetags/sfilters_tags.py:99 +msgid "Move this filter up" +msgstr "Flytta upp detta filter" + +#: templatetags/sfilters_tags.py:108 +msgid "Move this filter down" +msgstr "Flytta ner detta filter" + +#: views.py:87 +msgid "Failed to retrieve filters set" +msgstr "Det gick inte att hämta filteruppsättning" + +#: views.py:156 views.py:248 +msgid "Create" +msgstr "Skapa" + +#: views.py:161 +msgid "Filter created" +msgstr "Filter krashade" + +#: views.py:179 +msgid "Update" +msgstr "Uppdatera" + +#: views.py:186 +msgid "Filter modified" +msgstr "Filter modifierat" + +#: views.py:209 +msgid "Filter removed" +msgstr "Filter raderat" + +#: views.py:210 +msgid "Failed to remove filter" +msgstr "Det gick inte att radera filtret" + +#: views.py:225 +msgid "Filters set saved" +msgstr "Filter uppsättning sparat" + +#: views.py:241 +msgid "Filters set created" +msgstr "Filter uppsättning skapat" + +#: views.py:246 +msgid "Create a new filters set" +msgstr "Skapa ny filteruppsättning" + +#: views.py:265 +msgid "Filters set deleted" +msgstr "Filter uppsättning raderad" + +#: views.py:278 +msgid "Filters set activated" +msgstr "Filter uppsättning aktiverad" + +#: views.py:307 +msgid "yes" +msgstr "ja" + +#: views.py:311 +msgid "no" +msgstr "nej" diff --git a/modoboa/sievefilters/locale/sv/LC_MESSAGES/djangojs.po b/modoboa/sievefilters/locale/sv/LC_MESSAGES/djangojs.po new file mode 100644 index 000000000..844f08999 --- /dev/null +++ b/modoboa/sievefilters/locale/sv/LC_MESSAGES/djangojs.po @@ -0,0 +1,40 @@ +# 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 , 2013,2015 +msgid "" +msgstr "" +"Project-Id-Version: Modoboa\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2015-02-11 13:54+0100\n" +"PO-Revision-Date: 2017-09-22 09:57+0000\n" +"Last-Translator: Antoine Nguyen \n" +"Language-Team: Swedish (http://www.transifex.com/tonio/modoboa/language/sv/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: sv\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: static/modoboa_sievefilters/js/lib.js:14 +msgid "Remove this filter?" +msgstr "Radera detta filter?" + +#: static/modoboa_sievefilters/js/lib.js:107 +msgid "Save changes?" +msgstr "Spara ändringar?" + +#: static/modoboa_sievefilters/js/lib.js:129 +msgid "Remove filters set?" +msgstr "Radera filteruppsättning?" + +#: static/modoboa_sievefilters/js/lib.js:146 +msgid "Activate this filters set?" +msgstr "Aktivera filteruppsättning?" + +#: static/modoboa_sievefilters/js/lib.js:154 +#: static/modoboa_sievefilters/js/lib.js:203 +msgid "active" +msgstr "aktivera" diff --git a/modoboa/sievefilters/mocks.py b/modoboa/sievefilters/mocks.py new file mode 100644 index 000000000..b2370a355 --- /dev/null +++ b/modoboa/sievefilters/mocks.py @@ -0,0 +1,91 @@ +# coding: utf-8 + +"""Mock objects.""" + + +SAMPLE_SIEVE_SCRIPT = """require ["fileinto", "copy"]; + +# Filter: test1 +if anyof (header :contains "To" "test1") { + fileinto :copy "Test1"; +} + +# Filter: test2 +if anyof (header :contains "To" "test2") { + fileinto "Test2"; +} +""" + +SAMPLE_SIEVE_SCRIPT2 = """require ["fileinto"]; + +# Filter: Tést +if anyof (header :contains "Subject" "toto", not header :contains "To" "peé") { + fileinto "Administratif.Contrats.CIC"; + stop; +} +""" + + +class ManagesieveClientMock(object): + """Fake managesieve client.""" + + def __init__(self, *args, **kwargs): + self.scripts = { + "main_script": SAMPLE_SIEVE_SCRIPT, + "second_script": SAMPLE_SIEVE_SCRIPT, + "third_script": SAMPLE_SIEVE_SCRIPT2, + } + + def connect(self, username, password, **kwargs): + return username == "user@test.com" and password == "toto" + + def logout(self): + return True + + def capability(self): + return """ +"IMPLEMENTATION" "Example1 ManageSieved v001" +"VERSION" "1.0" +"SASL" "PLAIN SCRAM-SHA-1 GSSAPI" +"SIEVE" "fileinto vacation" +"STARTTLS" +""" + + def havespace(self, name, size): + return True + + def listscripts(self): + return ("main_script", ["second_script"]) + + def getscript(self, name): + return self.scripts.get(name) + + def putscript(self, name, content): + self.scripts[name] = content + return True + + def setactive(self, name): + return True + + def deletescript(self, name): + return True + + +class IMAP4Mock(object): + """Fake IMAP4 client.""" + + def __init__(self, *args, **kwargs): + self.untagged_responses = {} + + def _quote(self, data): + return data + + def _simple_command(self, name, *args, **kwargs): + if name == "CAPABILITY": + self.untagged_responses["CAPABILITY"] = [b""] + elif name == "LIST": + self.untagged_responses["LIST"] = [b'() "." "INBOX"'] + return "OK", None + + def list(self): + return "OK", [b'() "." "INBOX"'] diff --git a/modoboa/sievefilters/rfc6266.py b/modoboa/sievefilters/rfc6266.py new file mode 100644 index 000000000..bbb973fd0 --- /dev/null +++ b/modoboa/sievefilters/rfc6266.py @@ -0,0 +1,108 @@ +""" +Copy of the original rfc6266 package, just the generation part. + +Orginal code is available at https://github.com/g2p/rfc6266. +""" + +from urllib.parse import quote + + +# RFC 2616 +separator_chars = '()<>@,;:\\"/[]?={} \t' +# RFC 5987 +attr_chars_nonalnum = "!#$&+-.^_`|~" + + +def percent_encode(string, safe, encoding): + return quote(string, safe, encoding, errors="strict") + + +def is_token_char(ch): + # Must be ascii, and neither a control char nor a separator char + asciicode = ord(ch) + # < 128 means ascii, exclude control chars at 0-31 and 127, + # exclude separator characters. + return 31 < asciicode < 127 and ch not in separator_chars + + +def is_token(candidate): + return all(is_token_char(ch) for ch in candidate) + + +def is_ascii(text): + return all(ord(ch) < 128 for ch in text) + + +def fits_inside_codec(text, codec): + try: + text.encode(codec) + except UnicodeEncodeError: + return False + else: + return True + + +def is_lws_safe(text): + return normalize_ws(text) == text + + +def normalize_ws(text): + return " ".join(text.split()) + + +def qd_quote(text): + return text.replace("\\", "\\\\").replace('"', '\\"') + + +def build_header(filename, disposition="attachment", filename_compat=None): + """Generate a Content-Disposition header for a given filename. + For legacy clients that don't understand the filename* parameter, + a filename_compat value may be given. + It should either be ascii-only (recommended) or iso-8859-1 only. + In the later case it should be a character string + (unicode in Python 2). + Options for generating filename_compat (only useful for legacy clients): + - ignore (will only send filename*); + - strip accents using unicode's decomposing normalisations, + which can be done from unicode data (stdlib), and keep only ascii; + - use the ascii transliteration tables from Unidecode (PyPI); + - use iso-8859-1 + Ignore is the safest, and can be used to trigger a fallback + to the document location (which can be percent-encoded utf-8 + if you control the URLs). + See https://tools.ietf.org/html/rfc6266#appendix-D + """ + + # While this method exists, it could also sanitize the filename + # by rejecting slashes or other weirdness that might upset a receiver. + + if disposition != "attachment": + assert is_token(disposition) + + rv = disposition + + if is_token(filename): + rv += "; filename=%s" % (filename,) + return rv + elif is_ascii(filename) and is_lws_safe(filename): + qd_filename = qd_quote(filename) + rv += '; filename="%s"' % (qd_filename,) + if qd_filename == filename: + # RFC 6266 claims some implementations are iffy on qdtext's + # backslash-escaping, we'll include filename* in that case. + return rv + elif filename_compat: + if is_token(filename_compat): + rv += "; filename=%s" % (filename_compat,) + else: + assert is_lws_safe(filename_compat) + rv += '; filename="%s"' % (qd_quote(filename_compat),) + + # alnum are already considered always-safe, but the rest isn't. + # Python encodes ~ when it shouldn't, for example. + rv += "; filename*=utf-8''%s" % ( + percent_encode(filename, safe=attr_chars_nonalnum, encoding="utf-8"), + ) + + # This will only encode filename_compat, if it used non-ascii iso-8859-1. + return rv.encode("iso-8859-1") diff --git a/modoboa/sievefilters/static/sievefilters/css/style.css b/modoboa/sievefilters/static/sievefilters/css/style.css new file mode 100644 index 000000000..6ff656c85 --- /dev/null +++ b/modoboa/sievefilters/static/sievefilters/css/style.css @@ -0,0 +1,134 @@ +.fset_container { + padding: 0 15px; +} + +#set_content textarea { + width: 100%; + height: 500px; +} + +#set_content label { + margin: 5px; +} + +#filters_list { + border-left: 1px solid #CCC; + position: absolute; + padding-left: 5px; + left: 190px; + right: 10px; + top: 0px; + bottom: 0px; +} + +#id_content { + width: 95% !important; + height: 270px; +} + +/* Filter form */ +#filterform { + margin: 7px; +} + +#filterform h2 { + margin: 0; + padding: 0; + font-size: 1em; + margin-top: 20px; +} + +#submit_container { + width: 100%; + position: absolute; + bottom: 5px; + text-align: center; +} + +#error { + white-space: pre-line; +} + +#addfilterlist { + background-color: #FAFAFA; + border: 1px solid #CCCCCC; + border-radius: 26px 26px 26px 26px; + padding: 14px 5px 5px; +} + +#matchtype { + font-size: 0.9em; +} + + +div.item { + margin-bottom: 10px; + margin-left: 5px; + overflow: auto; +} + +ul.errors { + list-style: none; + display: inline; + margin: 0; + padding: 0; +} + +.errors li { + color: red; + font-weight: bold; + font-size: 0.9em; + display: inline; +} + +.leftbox { + width: 155px; +} + +.leftbox h1 { + float: none !important; + padding: 0; + margin: 0; + font-size: 1.1em; +} + +.leftbox ul { + list-style: none; + padding: 5px 0 0 0; + margin: 0; +} + +.leftbox li { + margin: 5px 0; +} + +.leftbox a, .leftbox a:visited { + color: black; + text-decoration: none; +} + +.leftbox a:hover { + color: #6AB0C5; + text-decoration: none; +} + +.leftbox li.selected { + background-color: #FFFF77; +} + +.leftbox img { + float: left; + margin-right: 5px; +} + +a.green { + text-decoration: none; + font-weight: bold; + color: green; +} + +a.red { + text-decoration: none; + font-weight: bold; + color: red; +} \ No newline at end of file diff --git a/modoboa/sievefilters/static/sievefilters/js/jquery.dynfields.js b/modoboa/sievefilters/static/sievefilters/js/jquery.dynfields.js new file mode 100644 index 000000000..35065de3a --- /dev/null +++ b/modoboa/sievefilters/static/sievefilters/js/jquery.dynfields.js @@ -0,0 +1,362 @@ +;(function ($) { + var DynamicCondition = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.dyncondition.defaults, options) + this.fields_cnt = 1 + this.nextid = 1 + + var $addlink = $('', { + href: '#', + id: this.$element.attr('id') + '_rmbtn', + html: '', + }) + $('select[name*=cond_target_]').change($.proxy(this.target_change, this)) + $addlink.click($.proxy(this.addfields, this)) + this.$element.append($addlink) + + for (var cpt = 1; true; cpt++) { + var $div = $('#condition_' + cpt) + + if (!$div.length) { + break + } + $div.append(this.newrmlink()) + this.nextid++ + this.fields_cnt++ + } + } + + DynamicCondition.prototype = { + constructor: DynamicCondition, + + loadoperators: function ($container, tpl) { + $container.empty() + for (var ocpt = 0; ocpt < tpl.operators.length; ocpt++) { + var $opt = $('', { + href: '#', + id: 'condition_' + this.nextid + '_rmbtn', + html: '', + }).click($.proxy(this.removefields, this)) + }, + + addfields: function (e) { + e.preventDefault() + var $div = $('
', { + id: 'condition_' + this.nextid, + class: 'item', + }) + var $condtarget = $('', { + id: 'id_cond_operator_' + this.nextid, + name: 'cond_operator_' + this.nextid, + class: 'form-control element-inline-block', + }) + var $condvalue = $('', { + id: 'id_cond_value_' + this.nextid, + name: 'cond_value_' + this.nextid, + type: 'text', + class: 'form-control element-inline-block', + }) + var $rmlink = this.newrmlink() + + for (var cpt = 0; cpt < this.options.templates.length; cpt++) { + var tpl = this.options.templates[cpt] + var $opt = $('
') + $condoperator.wrap('
') + $condvalue.wrap('
') + this.$element.after($div) + this.nextid++ + this.fields_cnt++ + }, + + target_change: function (e) { + var $target = $(e.target) + var parts = $target.attr('name').split('_') + var $container = $('#id_cond_operator_' + parts[2]) + + for (var cpt = 0; cpt < this.options.templates.length; cpt++) { + var tpl = this.options.templates[cpt] + + if (tpl.name != $target.val()) { + continue + } + this.loadoperators($container, tpl) + break + } + }, + + counter: function () { + return this.fields_cnt + }, + } + + $.fn.dyncondition = function (method) { + return this.each(function () { + var $this = $(this), + data = $this.data('dyncondition'), + options = typeof method === 'object' && method + + if (!data) { + $this.data('dyncondition', new DynamicCondition(this, options)) + } + if (typeof method === 'string') { + data[method]() + } + }) + } + + $.fn.dyncondition.defaults = { + defaultcond: 'Subject', + templates: [], + } +})(jQuery) + +/* + * Dynamic actions class + */ +;(function ($) { + var DynamicAction = function (element, options) { + this.$element = $(element) + this.options = $.extend({}, $.fn.dynaction.defaults, options) + this.nextid = 1 + this.fields_cnt = 1 + + var $addlink = $('', { + href: '#', + id: this.$element.attr('id') + '_rmbtn', + html: '', + }) + $('select[name*=action_name_]').change($.proxy(this.name_change, this)) + $addlink.click($.proxy(this.addfields, this)) + this.$element.append($addlink) + + for (var cpt = 1; true; cpt++) { + var $div = $('#action_' + cpt) + + if (!$div.length) { + break + } + $div.append(this.newrmlink()) + this.nextid++ + this.fields_cnt++ + } + + $('#id_action_arg_0_0') + .find('option') + .each(function () { + var $this = $(this), + cpt = 1, + start = 0, + value = $this.val() + + while (true) { + var id = value.indexOf(options.hdelimiter, start) + if (id == -1) { + break + } + $this.css('margin-left', cpt * 20 + 'px') + cpt++ + start = id + 1 + } + }) + + this.$fdoptions = $('#id_action_arg_0_0').find('option') + } + + DynamicAction.prototype = { + constructor: DynamicAction, + + loadargs: function (id, tpl) { + var res = [] + + for (var acpt = 0; acpt < tpl.args.length; acpt++) { + var argname = 'action_arg_' + id + '_' + acpt + var $child = null + + if (tpl.args[acpt].type == 'list') { + $child = $('', { + type: 'checkbox', + id: 'id_' + argname, + name: argname, + }).prependTo($label) + } else if (tpl.args[acpt].type == 'string') { + $child = $('', { + type: 'text', + id: 'id_' + argname, + name: argname, + class: 'form-control', + }) + } + res.push($child) + } + return res + }, + + /** + * Remove current action argument(s) before a change. + */ + deletePrevArgs: function (id) { + for (var cpt = 0; true; cpt++) { + var $arg = $('#id_action_arg_' + id + '_' + cpt) + if (!$arg.length) { + break + } + $arg.parent().remove() + } + }, + + /** + * Method called when the action is changed. + */ + name_change: function (e) { + var $target = $(e.target) + var parts = $target.attr('name').split('_') + var $pos = $('#action_' + parts[2] + '_rmbtn') + + e.preventDefault() + this.deletePrevArgs(parts[2]) + for (var cpt = 0; cpt < this.options.templates.length; cpt++) { + var tpl = this.options.templates[cpt] + var $div = $('
') + if (tpl.name != $target.val()) { + continue + } + $.each(this.loadargs(parts[2], tpl), function (idx, arg) { + arg.appendTo($div) + }) + $pos.before($div) + } + }, + + newrmlink: function () { + return $('', { + href: '#', + id: 'action_' + this.nextid + '_rmbtn', + html: '', + }).click($.proxy(this.removefields, this)) + }, + + addfields: function (e) { + e.preventDefault() + var $div = $('
', { id: 'action_' + this.nextid, class: 'item' }) + var $actioname = $('{% endif %} + {% render_field form.name %} +
{% trans "Conditions" %}
+
+
+ {% render_field form.match_type %} +
+
+ {% for ec in conds_nb %} + {% display_condition form ec %} + {% endfor %} +
+
+
{% trans "Actions" %}
+
+ {% for ea in actions_nb %} + {% display_action form ea %} + {% endfor %} +
+{% endblock %} diff --git a/modoboa/sievefilters/templates/sievefilters/guieditor.html b/modoboa/sievefilters/templates/sievefilters/guieditor.html new file mode 100644 index 000000000..30bf5f117 --- /dev/null +++ b/modoboa/sievefilters/templates/sievefilters/guieditor.html @@ -0,0 +1,25 @@ +{% load i18n sfilters_tags %} +{% if fs.filters %} + + + + + + + {% for filter in fs.filters %} + + + + + + {% endfor %} +
{% trans "Actions" %}{% trans "Name" %}{% trans "Active" %}
{% filter_actions fs.name filter forloop.counter forloop.last %}{{ filter.name }} + + {{ filter.enabled|yesno }} + +
+{% else %} +
{% trans "No filter defined yet" %}
+{% endif %} diff --git a/modoboa/sievefilters/templates/sievefilters/index.html b/modoboa/sievefilters/templates/sievefilters/index.html new file mode 100644 index 000000000..9d7bebc73 --- /dev/null +++ b/modoboa/sievefilters/templates/sievefilters/index.html @@ -0,0 +1,104 @@ +{% extends "twocols.html" %} +{% load i18n static sfilters_tags %} + +{% block pagetitle %}{% trans "Message filters" %}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block extra_js %} + + + + + +{% endblock %} + +{% block leftcol %} + +{% endblock %} + +{% block apparea %} +
+ {% if not active_script and not scripts %} +
{% trans "No filters set defined yet. You must create one in order to define filters." %}
+ {% endif %} +
+{% endblock %} diff --git a/modoboa/sievefilters/templates/sievefilters/rawfilter.html b/modoboa/sievefilters/templates/sievefilters/rawfilter.html new file mode 100644 index 000000000..0194f98f5 --- /dev/null +++ b/modoboa/sievefilters/templates/sievefilters/rawfilter.html @@ -0,0 +1,4 @@ +
{% csrf_token %} + +
diff --git a/modoboa/sievefilters/templatetags/__init__.py b/modoboa/sievefilters/templatetags/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/modoboa/sievefilters/templatetags/sfilters_tags.py b/modoboa/sievefilters/templatetags/sfilters_tags.py new file mode 100644 index 000000000..c8892cdc3 --- /dev/null +++ b/modoboa/sievefilters/templatetags/sfilters_tags.py @@ -0,0 +1,213 @@ +"""Custom template tags.""" + +from django import template +from django.urls import reverse +from django.template.loader import render_to_string +from django.utils.translation import gettext as _ + +from modoboa.lib.web_utils import render_actions +from modoboa.lib.templatetags.form_tags import configure_field_classes + +from .. import lib + +register = template.Library() + + +@register.simple_tag +def sfilters_menu(user): + entries = [ + { + "name": "newfilterset", + "img": "fa fa-plus", + "label": _("New filters set"), + "url": reverse("sievefilters:fs_add"), + "modal": True, + "modalcb": "filtersetform_cb", + } + ] + + return render_to_string("common/menulist.html", {"entries": entries, "user": user}) + + +@register.simple_tag +def fset_menu(mode, setname): + entries = [] + if mode == "gui": + entries += [ + { + "name": "newfilter", + "img": "fa fa-plus", + "label": _("New filter"), + "url": reverse("sievefilters:filter_add", args=[setname]), + "modal": True, + "autowidth": True, + "modalcb": "filterform_cb", + } + ] + if mode == "raw": + entries += [ + { + "name": "savefs", + "img": "fa fa-download-alt", + "label": _("Save filters set"), + "url": reverse("sievefilters:fs_save", args=[setname]), + }, + ] + + entries += [ + { + "name": "activatefs", + "img": "fa fa-check", + "label": _("Activate filters set"), + "url": reverse("sievefilters:fs_activate", args=[setname]), + }, + { + "name": "removefs", + "img": "fa fa-trash", + "label": _("Remove filters set"), + "url": reverse("sievefilters:fs_delete", args=[setname]), + }, + { + "name": "downloadfs", + "img": "fa fa-download", + "label": _("Download"), + "url": reverse("sievefilters:fs_download", args=[setname]), + }, + ] + + return render_to_string("common/menulist.html", {"entries": entries}) + + +@register.simple_tag +def filter_actions(setname, f, position, islast): + actions = [ + { + "name": "editfilter", + "url": reverse("sievefilters:filter_change", args=[setname, f["name"]]), + "img": "fa fa-edit", + "title": _("Edit filter"), + "modal": True, + "autowidth": True, + "modalcb": "filterform_cb", + }, + { + "name": "removefilter", + "url": reverse("sievefilters:filter_delete", args=[setname, f["name"]]), + "img": "fa fa-trash", + "title": _("Remove this filter"), + }, + ] + if position != 1: + actions += [ + { + "name": "movefilter_up", + "url": reverse( + "sievefilters:filter_move_up", args=[setname, f["name"]] + ), + "img": "fa fa-chevron-up", + "title": _("Move this filter up"), + } + ] + if not islast: + actions += [ + { + "name": "movefilter_down", + "url": reverse( + "sievefilters:filter_move_down", args=[setname, f["name"]] + ), + "img": "fa fa-chevron-down", + "title": _("Move this filter down"), + }, + ] + return render_actions(actions) + + +@register.simple_tag +def display_errors(errors): + if not len(errors): + return "" + t = template.Template( + """ +
    + {% for error in errors %} +
  • {{ error|safe }}
  • + {% endfor %} +
+""" + ) + return t.render(template.Context({"errors": errors})) + + +@register.simple_tag +def display_condition(form, cnt): + target = form["cond_target_%d" % cnt] + configure_field_classes(target) + operator = form["cond_operator_%d" % cnt] + configure_field_classes(operator) + value = form["cond_value_%d" % cnt] + configure_field_classes(value) + t = template.Template( + """ +
+
{{ tfield }}
+
{{ofield}}
+
{{ vfield }}
+ {{ verrors }} +
+""" + ) + return t.render( + template.Context( + { + "idx": cnt, + "tfield": target, + "ofield": operator, + "vfield": value, + "verrors": display_errors(value.errors), + } + ) + ) + + +@register.simple_tag +def display_action(form, cnt): + action = form["action_name_%d" % cnt] + tpl = lib.find_action_template(action.initial) + configure_field_classes(action) + values = [] + verrors = [] + for pos, argtpl in enumerate(tpl.get("args", [])): + fieldname = "action_arg_{}_{}".format(cnt, pos) + if fieldname not in form.fields: + continue + field = form[fieldname] + configure_field_classes(field) + arg = {"field": field} + if field.errors: + verrors += field.errors + if argtpl["type"] == "boolean": + arg["checkbox"] = True + values += [arg] + t = template.Template( + """ +
+
+ {{ afield }} +
+
+ {% for v in values %}{% if v.checkbox %}
{% else %}{{ v.field }}{% endif %}{% endfor %} + {{ verrors }} +
+
+""" + ) + return t.render( + template.Context( + { + "idx": cnt, + "afield": action, + "values": values, + "verrors": display_errors(verrors), + } + ) + ) diff --git a/modoboa/sievefilters/tests.py b/modoboa/sievefilters/tests.py new file mode 100644 index 000000000..998142514 --- /dev/null +++ b/modoboa/sievefilters/tests.py @@ -0,0 +1,165 @@ +# coding: utf-8 + +"""Sievefilters tests.""" + +from unittest import mock + +from django.urls import reverse + +from modoboa.admin import factories as admin_factories +from modoboa.core import models as core_models +from modoboa.lib.tests import ModoTestCase + +from . import mocks + + +class SieveFiltersTestCase(ModoTestCase): + """Check sieve filters.""" + + @classmethod + def setUpTestData(cls): + """Create some users.""" + super().setUpTestData() + admin_factories.populate_database() + cls.user = core_models.User.objects.get(username="user@test.com") + + def setUp(self): + """Connect with a simpler user.""" + patcher = mock.patch("sievelib.managesieve.Client") + self.mock_client = patcher.start() + self.mock_client.return_value = mocks.ManagesieveClientMock() + self.addCleanup(patcher.stop) + + patcher = mock.patch("imaplib.IMAP4") + self.mock_imap4 = patcher.start() + self.mock_imap4.return_value = mocks.IMAP4Mock() + self.addCleanup(patcher.stop) + + url = reverse("core:login") + data = {"username": self.user.username, "password": "toto"} + self.client.post(url, data) + + def test_index(self): + """Test index view.""" + response = self.client.get(reverse("sievefilters:index")) + self.assertContains(response, "main_script (active)") + response = self.client.post(reverse("core:logout")) + self.assertEqual(response.status_code, 302) + + def test_getfs(self): + """Test getfs view.""" + response = self.ajax_get(reverse("sievefilters:fs_get", args=["main_script"])) + self.assertIn("/sfilters/main_script/editfilter/test1/", response["content"]) + + def test_toggle_filter_state(self): + """Test toggle_filter_state view.""" + url = reverse("sievefilters:filter_toggle_state", args=["main_script", "test1"]) + response = self.ajax_get(url) + self.assertEqual(response["color"], "red") + response = self.ajax_get(url) + self.assertEqual(response["color"], "green") + + def test_move_filter_up(self): + """Test move_filter_up view.""" + url = reverse("sievefilters:filter_move_up", args=["main_script", "test2"]) + response = self.ajax_get(url) + pos1 = response["content"].find("/sfilters/main_script/editfilter/test2/") + pos2 = response["content"].find("/sfilters/main_script/editfilter/test1/") + self.assertTrue(pos1 < pos2) + + def test_move_filter_down(self): + """Test move_filter_down view.""" + url = reverse("sievefilters:filter_move_down", args=["main_script", "test1"]) + response = self.ajax_get(url) + pos1 = response["content"].find("/sfilters/main_script/editfilter/test2/") + pos2 = response["content"].find("/sfilters/main_script/editfilter/test1/") + self.assertTrue(pos1 < pos2) + + def test_new_filter(self): + """Test new_filter view.""" + url = reverse("sievefilters:filter_add", args=["main_script"]) + response = self.client.get(url) + self.assertContains(response, "New filter") + + data = { + "name": "tést filter", + "match_type": "anyof", + "conds": 1, + "cond_target_0": "Subject", + "cond_operator_0": "contains", + "cond_value_0": "Test", + "actions": 1, + "action_name_0": "fileinto", + "action_arg_0_0": "Trash", + } + response = self.ajax_post(url, data) + self.assertEqual(response, "Filter created") + + def test_edit_filter(self): + """Test edit_filter view.""" + url = reverse("sievefilters:filter_change", args=["main_script", "test1"]) + response = self.client.get(url) + self.assertContains(response, "Edit filter") + + data = { + "oldname": "test1", + "name": "test1", + "match_type": "all", + "conds": 1, + "cond_target_0": "To", + "cond_operator_0": "contains", + "cond_value_0": "test1", + "actions": 1, + "action_name_0": "fileinto", + "action_arg_0_0": "Trash", + "action_arg_0_1": True, + } + response = self.ajax_post(url, data) + self.assertEqual(response, "Filter modified") + + def test_edit_filter_with_not(self): + """Try to edit a filter containing a not operator.""" + url = reverse("sievefilters:filter_change", args=["third_script", "Tést"]) + response = self.client.get(url) + self.assertContains(response, "Edit filter") + + def test_remove_filter(self): + """Test removefilter view.""" + url = reverse("sievefilters:filter_delete", args=["second_script", "test1"]) + response = self.ajax_get(url) + self.assertEqual(response, "Filter removed") + + def test_download_filters_set(self): + """Test download_filters_set view.""" + url = reverse("sievefilters:fs_download", args=["main_script"]) + response = self.client.get(url) + self.assertEqual(response.content.decode(), mocks.SAMPLE_SIEVE_SCRIPT) + + def test_activate_filters_set(self): + """Test activate_filters_set view.""" + url = reverse("sievefilters:fs_activate", args=["second_script"]) + response = self.ajax_get(url) + self.assertEqual(response["respmsg"], "Filters set activated") + + def test_remove_filters_set(self): + """Test remove_filters_set view.""" + url = reverse("sievefilters:fs_delete", args=["second_script"]) + response = self.ajax_get(url) + self.assertEqual(response["respmsg"], "Filters set deleted") + + def test_new_filters_set(self): + """Test new_filters_set view.""" + url = reverse("sievefilters:fs_add") + response = self.client.get(url) + self.assertContains(response, "Create a new filters set") + + data = {"name": "new_script"} + response = self.ajax_post(url, data) + self.assertEqual(response["respmsg"], "Filters set created") + + def test_savefs(self): + """Test savefs view.""" + url = reverse("sievefilters:fs_save", args=["new_script"]) + data = {"scriptcontent": mocks.SAMPLE_SIEVE_SCRIPT} + response = self.ajax_post(url, data) + self.assertEqual(response["respmsg"], "Filters set saved") diff --git a/modoboa/sievefilters/urls.py b/modoboa/sievefilters/urls.py new file mode 100644 index 000000000..d4a0ed0c6 --- /dev/null +++ b/modoboa/sievefilters/urls.py @@ -0,0 +1,42 @@ +"""Sieve urls.""" + +from django.urls import path + +from . import views + +app_name = "sievefilters" + +urlpatterns = [ + path("", views.index, name="index"), + path("savefs//", views.savefs, name="fs_save"), + path("newfs/", views.new_filters_set, name="fs_add"), + path("removefs//", views.remove_filters_set, name="fs_delete"), + path("activatefs//", views.activate_filters_set, name="fs_activate"), + path("downloadfs//", views.download_filters_set, name="fs_download"), + path("templates//", views.get_templates, name="templates_get"), + path("/newfilter/", views.newfilter, name="filter_add"), + path( + "/editfilter//", views.editfilter, name="filter_change" + ), + path( + "/removefilter//", + views.removefilter, + name="filter_delete", + ), + path( + "/togglestate//", + views.toggle_filter_state, + name="filter_toggle_state", + ), + path( + "/moveup//", + views.move_filter_up, + name="filter_move_up", + ), + path( + "/movedown//", + views.move_filter_down, + name="filter_move_down", + ), + path("/", views.getfs, name="fs_get"), +] diff --git a/modoboa/sievefilters/views.py b/modoboa/sievefilters/views.py new file mode 100644 index 000000000..cc586c16d --- /dev/null +++ b/modoboa/sievefilters/views.py @@ -0,0 +1,326 @@ +"""Custom views.""" + +from sievelib.commands import BadArgument, BadValue +from sievelib.managesieve import Error + +from django.contrib.auth.decorators import login_required +from django.urls import reverse +from django.http import HttpResponse, JsonResponse +from django.shortcuts import render +from django.template.loader import render_to_string +from django.utils.encoding import smart_bytes, smart_str +from django.utils.translation import gettext as _ + +from modoboa.admin.lib import needs_mailbox +from modoboa.lib.connections import ConnectionError +from modoboa.lib.exceptions import BadRequest +from modoboa.lib.web_utils import _render_error, ajax_response, render_to_json_response + +from . import constants +from .forms import ( + FilterForm, + build_filter_form_from_qdict, + build_filter_form_from_filter, + FiltersSetForm, +) +from .imaputils import get_imapconnector +from .lib import SieveClient, SieveClientError +from .rfc6266 import build_header +from .templatetags.sfilters_tags import fset_menu + + +@login_required +@needs_mailbox() +def index(request, tplname="sievefilters/index.html"): + try: + sc = SieveClient( + user=request.user.username, password=request.session["password"] + ) + except ConnectionError as e: + return _render_error(request, user_context={"error": e}) + + try: + active_script, scripts = sc.listscripts() + except Error as e: + return _render_error(request, user_context={"error": e}) + + if active_script is None: + active_script = "" + default_script = "%s/" % scripts[0] if len(scripts) else "" + else: + default_script = "%s/" % active_script + return render( + request, + tplname, + { + "selection": "user", + "active_script": active_script, + "default_script": default_script, + "scripts": sorted(scripts), + "hdelimiter": get_imapconnector(request).hdelimiter, + }, + ) + + +@login_required +@needs_mailbox() +def get_templates(request, ftype): + if ftype == "condition": + data = constants.CONDITION_TEMPLATES + else: + data = constants.ACTION_TEMPLATES + return JsonResponse(data, safe=False) + + +@login_required +@needs_mailbox() +def getfs(request, name): + sc = SieveClient(user=request.user.username, password=request.session["password"]) + editormode = request.user.parameters.get_value("editor_mode") + error = None + try: + content = sc.getscript(name, format=editormode) + except SieveClientError as e: + error = str(e) + else: + if content is None: + error = _("Failed to retrieve filters set") + + if error is not None: + return ajax_response(request, "ko", respmsg=error) + + if editormode == "raw": + htmlcontent = render_to_string( + "sievefilters/rawfilter.html", {"name": name, "scriptcontent": content} + ) + else: + htmlcontent = render_to_string("sievefilters/guieditor.html", {"fs": content}) + + menu = ( + "".format( + _("Actions"), fset_menu(editormode, name) + ) + ) + resp = {"menu": menu, "content": htmlcontent} + return render_to_json_response(resp) + + +def build_filter_ctx(ctx, form): + ctx["form"] = form + ctx["conds_nb"] = range(form.conds_cnt) + ctx["actions_nb"] = range(form.actions_cnt) + return ctx + + +def submitfilter(request, setname, okmsg, tplname, tplctx, update=False, sc=None): + form = build_filter_form_from_qdict(request) + if form.is_valid(): + if sc is None: + sc = SieveClient( + user=request.user.username, password=request.session["password"] + ) + fset = sc.getscript(setname, format="fset") + conditions, actions = form.tofilter() + match_type = form.cleaned_data["match_type"] + if match_type == "all": + match_type = "anyof" + conditions = [("true",)] + fltname = form.cleaned_data["name"].encode("utf-8") + try: + if not update: + fset.addfilter(fltname, conditions, actions, match_type) + else: + oldname = request.POST["oldname"].encode("utf-8") + fset.updatefilter(oldname, fltname, conditions, actions, match_type) + except (BadArgument, BadValue) as inst: + raise BadRequest(str(inst)) + sc.pushscript(fset.name, "{}".format(fset)) + return render_to_json_response(okmsg) + + return render_to_json_response({"form_errors": form.errors}, status=400) + + +@login_required +@needs_mailbox() +def newfilter(request, setname, tplname="sievefilters/filter.html"): + ctx = { + "title": _("New filter"), + "formid": "filterform", + "action": reverse("sievefilters:filter_add", args=[setname]), + "action_label": _("Create"), + "action_classes": "submit", + } + if request.method == "POST": + return submitfilter(request, setname, _("Filter created"), tplname, ctx) + + conds = [("Subject", "contains", "")] + actions = [("fileinto", "")] + form = FilterForm(conds, actions, request) + ctx = build_filter_ctx(ctx, form) + return render(request, tplname, ctx) + + +@login_required +@needs_mailbox() +def editfilter(request, setname, fname, tplname="sievefilters/filter.html"): + ctx = { + "title": _("Edit filter"), + "formid": "filterform", + "action": reverse("sievefilters:filter_change", args=[setname, fname]), + "action_label": _("Update"), + "action_classes": "submit", + } + sc = SieveClient(user=request.user.username, password=request.session["password"]) + if request.method == "POST": + return submitfilter( + request, setname, _("Filter modified"), tplname, ctx, update=True, sc=sc + ) + + fset = sc.getscript(setname, format="fset") + fname = smart_bytes(fname) + f = fset.getfilter(fname) + form = build_filter_form_from_filter(request, fname, f) + ctx = build_filter_ctx(ctx, form) + ctx["oldname"] = fname + ctx["hidestyle"] = "none" if form.fields["match_type"].initial == "all" else "block" + return render(request, tplname, ctx) + + +@login_required +@needs_mailbox() +def removefilter(request, setname, fname): + sc = SieveClient(user=request.user.username, password=request.session["password"]) + fset = sc.getscript(setname, format="fset") + if fset.removefilter(fname.encode("utf-8")): + sc.pushscript(fset.name, "{}".format(fset)) + return render_to_json_response(_("Filter removed")) + return render_to_json_response(_("Failed to remove filter"), status=500) + + +@login_required +@needs_mailbox() +def savefs(request, name): + if "scriptcontent" not in request.POST: + return + sc = SieveClient(user=request.user.username, password=request.session["password"]) + try: + sc.pushscript(name, request.POST["scriptcontent"]) + except SieveClientError as e: + error = str(e) + return ajax_response(request, "ko", respmsg=error) + return ajax_response(request, respmsg=_("Filters set saved")) + + +@login_required +@needs_mailbox() +def new_filters_set(request, tplname="common/generic_modal_form.html"): + if request.method == "POST": + form = FiltersSetForm(request.POST) + if form.is_valid(): + sc = SieveClient( + user=request.user.username, password=request.session["password"] + ) + sc.pushscript( + form.cleaned_data["name"], "# Empty script", form.cleaned_data["active"] + ) + return render_to_json_response( + { + "url": form.cleaned_data["name"], + "active": form.cleaned_data["active"], + "respmsg": _("Filters set created"), + } + ) + return render_to_json_response({"form_errors": form.errors}, status=400) + + ctx = { + "title": _("Create a new filters set"), + "formid": "newfiltersset", + "action_label": _("Create"), + "action_classes": "submit", + "action": reverse("sievefilters:fs_add"), + "withmenu": False, + "withunseen": False, + "form": FiltersSetForm(), + } + return render(request, tplname, ctx) + + +@login_required +@needs_mailbox() +def remove_filters_set(request, name): + sc = SieveClient(user=request.user.username, password=request.session["password"]) + sc.deletescript(name) + acs, scripts = sc.listscripts() + ctx = { + "respmsg": _("Filters set deleted"), + "newfs": acs, + } + return render_to_json_response(ctx) + + +@login_required +@needs_mailbox() +def activate_filters_set(request, name): + sc = SieveClient(user=request.user.username, password=request.session["password"]) + try: + sc.activatescript(name) + except SieveClientError as e: + return ajax_response(request, "ko", respmsg=str(e)) + return ajax_response(request, respmsg=_("Filters set activated")) + + +@login_required +@needs_mailbox() +def download_filters_set(request, name): + sc = SieveClient(user=request.user.username, password=request.session["password"]) + try: + script = sc.getscript(name) + except SieveClientError as e: + return ajax_response(request, "ko", respmsg=str(e)) + + resp = HttpResponse(script) + resp["Content-Type"] = "text/plain; charset=utf-8" + resp["Content-Length"] = len(script) + resp["Content-Disposition"] = build_header("%s.txt" % name) + return resp + + +@login_required +@needs_mailbox() +def toggle_filter_state(request, setname, fname): + sc = SieveClient(user=request.user.username, password=request.session["password"]) + fname = smart_str(fname) + fset = sc.getscript(setname, format="fset") + if fset.is_filter_disabled(fname): + ret = fset.enablefilter(fname) + newstate = _("yes") + color = "green" + else: + ret = fset.disablefilter(fname) + newstate = _("no") + color = "red" + if not ret: + pass + sc.pushscript(setname, "{}".format(fset)) + return render_to_json_response({"label": newstate, "color": color}) + + +def move_filter(request, setname, fname, direction): + sc = SieveClient(user=request.user.username, password=request.session["password"]) + fset = sc.getscript(setname, format="fset") + fset.movefilter(fname.encode("utf-8"), direction) + sc.pushscript(setname, str(fset)) + return ajax_response(request, template="sievefilters/guieditor.html", fs=fset) + + +@login_required +@needs_mailbox() +def move_filter_up(request, setname, fname): + return move_filter(request, setname, fname, "up") + + +@login_required +@needs_mailbox() +def move_filter_down(request, setname, fname): + return move_filter(request, setname, fname, "down") diff --git a/modoboa/urls.py b/modoboa/urls.py index 9f240ee04..bc6ad136c 100644 --- a/modoboa/urls.py +++ b/modoboa/urls.py @@ -29,6 +29,7 @@ urlpatterns = [ path("stats/", include("modoboa.maillog.urls")), path("dmarc/", include("modoboa.dmarc.urls")), path("imap_migration/", include("modoboa.imap_migration.urls")), + path("sfilters/", include("modoboa.sievefilters.urls")), path("user/forward/", user_views.forward, name="user_forward"), path( "accounts/password_reset/", @@ -100,6 +101,7 @@ urlpatterns += [ name="docs-index-v2", ), path("api/schema-v2/redoc/", SpectacularRedocView.as_view(url_name="schema-v2")), + path("api/o/", include("oauth2_provider.urls", namespace="oauth2_provider")), path("api/v1/", include("modoboa.urls_api_v1", namespace="v1")), path("api/v2/", include("modoboa.urls_api_v2", namespace="v2")), ] diff --git a/modoboa/urls_api_v2.py b/modoboa/urls_api_v2.py index ab42765ee..5de4b5e0f 100644 --- a/modoboa/urls_api_v2.py +++ b/modoboa/urls_api_v2.py @@ -19,6 +19,7 @@ urlpatterns = [ path("", include("modoboa.transport.api.v2.urls")), path("", include("modoboa.pdfcredentials.api.v2.urls")), path("", include("modoboa.postfix_autoreply.api.v2.urls")), + path("", include("modoboa.sievefilters.api.v2.urls")), ] urlpatterns += exts_pool.get_urls(category="api") diff --git a/requirements.txt b/requirements.txt index 13ed9a525..d02fc1a4b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,7 @@ djangorestframework>=3.14,<3.16 drf-spectacular uritemplate djangorestframework-simplejwt==5.3.1 +django-oauth-toolkit passlib~=1.7.4 bcrypt # Requires libffi-dev and python-dev @@ -48,3 +49,6 @@ python-magic>=0.4.24 # RQ rq>=1.13.0 django-rq + +# Sieve +sievelib>=1.1.1 diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py index 79889a953..aa9271543 100644 --- a/test_project/test_project/settings.py +++ b/test_project/test_project/settings.py @@ -66,6 +66,7 @@ INSTALLED_APPS = ( "reversion", "ckeditor", "ckeditor_uploader", + "oauth2_provider", "rest_framework", "rest_framework.authtoken", "drf_spectacular", @@ -95,6 +96,7 @@ MODOBOA_APPS = ( "modoboa.dmarc", "modoboa.imap_migration", "modoboa.postfix_autoreply", + "modoboa.sievefilters", # Modoboa extensions here. ) @@ -189,6 +191,17 @@ STATICFILES_DIRS = (os.path.join(BASE_DIR, "..", "modoboa", "bower_components"), MEDIA_URL = "/media/" MEDIA_ROOT = os.path.join(BASE_DIR, "media") +# oAuth2 settings + +OAUTH2_PROVIDER = { + "SCOPES": { + "read": "Read scope", + "write": "Write scope", + "introspection": "Introspect token scope", + }, + "DEFAULT_SCOPES": ["read", "write"], +} + # Rest framework settings REST_FRAMEWORK = { @@ -202,7 +215,7 @@ REST_FRAMEWORK = { "password_recovery_apply": "25/hour", }, "DEFAULT_AUTHENTICATION_CLASSES": ( - "modoboa.core.drf_authentication.JWTAuthenticationWith2FA", + "oauth2_provider.contrib.rest_framework.OAuth2Authentication", "rest_framework.authentication.TokenAuthentication", "rest_framework.authentication.SessionAuthentication", ),