From 5cb0f561f56cc9cef7871b9d92727d8f8874832a Mon Sep 17 00:00:00 2001 From: Antoine Nguyen Date: Thu, 4 Apr 2024 14:58:42 +0200 Subject: [PATCH] Added features --- frontend/src/api/account.js | 19 ++++ .../src/components/account/FilterForm.vue | 26 +++-- frontend/src/views/account/FiltersView.vue | 102 +++++++++++++++++- modoboa/lib/exceptions.py | 2 +- modoboa/sievefilters/api/v2/viewsets.py | 78 +++++++++++++- modoboa/sievefilters/lib.py | 2 + 6 files changed, 212 insertions(+), 17 deletions(-) diff --git a/frontend/src/api/account.js b/frontend/src/api/account.js index 2c97738f6..25a41e95b 100644 --- a/frontend/src/api/account.js +++ b/frontend/src/api/account.js @@ -53,9 +53,18 @@ export default { getFilterSets() { return repository.get(`${resource}/filtersets/`) }, + downloadFilterSet(filterSetName) { + return repository.get(`${resource}/filtersets/${filterSetName}/download/`) + }, createFilterSet(data) { return repository.post(`${resource}/filtersets/`, data) }, + activateFilterSet(filterSetName) { + return repository.post(`${resource}/filtersets/${filterSetName}/activate/`) + }, + deleteFilterSet(filterSetName) { + return repository.delete(`${resource}/filtersets/${filterSetName}/`) + }, getFilterConditionTemplates() { return repository.get(`${resource}/filtersets/condition_templates/`) }, @@ -77,6 +86,16 @@ export default { data ) }, + disableFilter(filterSetName, filter) { + return repository.post( + `${resource}/filtersets/${filterSetName}/filters/${filter}/disable/` + ) + }, + enableFilter(filterSetName, filter) { + return repository.post( + `${resource}/filtersets/${filterSetName}/filters/${filter}/enable/` + ) + }, deleteFilter(filterSetName, filter) { return repository.delete( `${resource}/filtersets/${filterSetName}/filters/${filter}/` diff --git a/frontend/src/components/account/FilterForm.vue b/frontend/src/components/account/FilterForm.vue index 90cbb589f..a60b24b82 100644 --- a/frontend/src/components/account/FilterForm.vue +++ b/frontend/src/components/account/FilterForm.vue @@ -264,17 +264,23 @@ async function submit() { if (!valid) { return } - if (!props.filter) { - accountApi.createFilter(props.filterSet, form.value).then(() => { - busStore.displayNotification({ msg: $gettext('Filter added') }) - close() - }) - } else { - accountApi.updateFilter(props.filterSet, originalFilterName, form.value).then(() => { - busStore.displayNotification({ msg: $gettext('Filter updated') }) - close() - }) + let msg + try { + if (!props.filter) { + await accountApi.createFilter(props.filterSet, form.value) + msg = $gettext('Filter added') + } else { + await accountApi.updateFilter(props.filterSet, originalFilterName, form.value) + msg = $gettext('Filter updated') + } + } catch (err) { + if (err.response.data && err.response.data.error) { + busStore.displayNotification({ msg: err.response.data.error, type: 'error' }) + } + return } + busStore.displayNotification({ msg }) + close() } accountApi.getFilterConditionTemplates().then(resp => { diff --git a/frontend/src/views/account/FiltersView.vue b/frontend/src/views/account/FiltersView.vue index 933902dd2..ed9e4ca94 100644 --- a/frontend/src/views/account/FiltersView.vue +++ b/frontend/src/views/account/FiltersView.vue @@ -19,10 +19,26 @@ color="primary" icon="mdi-plus" size="small" - class="ml-4" - @click="showCreationForm = true" + class="mx-4" + @click="showFilterSetForm = true" :title="$gettext('Add filter set')" /> + + + +
- + @@ -111,7 +127,9 @@ const headers = [ ] watch(currentFilterSet, (value) => { - fetchFilters(value.name) + if (value) { + fetchFilters(value.name) + } }) function closeFilterSetForm() { @@ -142,6 +160,20 @@ function editFilter(filter) { showFilterForm.value = true } +function enableFilter(filter) { + accountApi.enableFilter(currentFilterSet.value.name, filter.name).then(() => { + filter.enabled = true + busStore.displayNotification({ msg: $gettext('Filter enabled') }) + }) +} + +function disableFilter(filter) { + accountApi.disableFilter(currentFilterSet.value.name, filter.name).then(() => { + filter.enabled = false + busStore.displayNotification({ msg: $gettext('Filter disabled') }) + }) +} + function deleteFilter(filter) { accountApi.deleteFilter(currentFilterSet.value.name, filter.name).then(() => { busStore.displayNotification({ msg: $gettext('Filter removed') }) @@ -149,6 +181,28 @@ function deleteFilter(filter) { }) } +async function activateFilterSet(filterSet) { + await accountApi.activateFilterSet(filterSet.name) + busStore.displayNotification({ msg: $gettext('Filter set activated') }) +} + +async function deleteFilterSet(filterSet) { + await accountApi.deleteFilterSet(filterSet.name) + busStore.displayNotification({ msg: $gettext('Filter set removed') }) + currentFilterSet.value = null + fetchFilterSets() +} + +async function downloadFilterSet(filterSet) { + const resp = await accountApi.downloadFilterSet(filterSet.name) + const blob = new Blob([resp.data], { type: 'text/plain' }) + const link = document.createElement('a') + link.href = URL.createObjectURL(blob) + link.download = `${filterSet.name}.txt` + link.click() + URL.revokeObjectURL(link.href) +} + const filterActions = [ { label: $gettext('Edit'), @@ -163,6 +217,46 @@ const filterActions = [ } ] +function getFilterActions(filter) { + const result = [...filterActions] + if (filter.enabled) { + result.push({ + label: $gettext('Disable'), + icon: 'mdi-stop', + color: 'red', + onClick: disableFilter + }) + } else { + result.push({ + label: $gettext('Enable'), + icon: 'mdi-check', + color: 'success', + onClick: enableFilter + }) + } + return result +} + +const filterSetActions = ref([ + { + label: $gettext('Activate filter set'), + color: 'success', + icon: 'mdi-check', + onClick: activateFilterSet + }, + { + label: $gettext('Download filter set'), + icon: 'mdi-download', + onClick: downloadFilterSet + }, + { + label: $gettext('Delete filter set'), + color: 'error', + icon: 'mdi-trash-can', + onClick: deleteFilterSet + } +]) + fetchFilterSets() diff --git a/modoboa/lib/exceptions.py b/modoboa/lib/exceptions.py index 25eae6697..b1c603a44 100644 --- a/modoboa/lib/exceptions.py +++ b/modoboa/lib/exceptions.py @@ -18,7 +18,7 @@ class ModoboaException(Exception): if "http_code" in kwargs: self.http_code = kwargs["http_code"] del kwargs["http_code"] - super(ModoboaException, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) class InternalError(ModoboaException): diff --git a/modoboa/sievefilters/api/v2/viewsets.py b/modoboa/sievefilters/api/v2/viewsets.py index 141db27c6..f34b4e05b 100644 --- a/modoboa/sievefilters/api/v2/viewsets.py +++ b/modoboa/sievefilters/api/v2/viewsets.py @@ -4,6 +4,9 @@ import copy from typing import Optional from sievelib.commands import BadArgument, BadValue +from sievelib.factory import FilterAlreadyExists + +from django.http import HttpResponse from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema, OpenApiParameter @@ -13,6 +16,7 @@ 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.rfc6266 import build_header from modoboa.sievefilters.api.v2 import serializers @@ -31,8 +35,8 @@ class FilterSetViewSet(viewsets.ViewSet): sclient = self.get_sieve_client(request) active_script, scripts = sclient.listscripts() scripts = [ - {"name": script, "active": active_script == script} for script in scripts - ] + {"name": active_script, "active": True}, + ] + [{"name": script, "active": False} for script in scripts] return response.Response(scripts) def create(self, request): @@ -47,6 +51,44 @@ class FilterSetViewSet(viewsets.ViewSet): ) return response.Response(serializer.validated_data, 201) + @action(methods=["post"], detail=True) + def activate(self, request, pk): + """Activate an existing filter set.""" + sclient = self.get_sieve_client(request) + try: + fset = sclient.getscript(pk, format="fset") + except SieveClientError: + return response.Response(status=404) + try: + sclient.activatescript(pk) + except SieveClientError as err: + return response.Response({"error": str(err)}, status=400) + return response.Response() + + @action(methods=["get"], detail=True) + def download(self, request, pk): + """Download the content of an existing filter set.""" + sclient = self.get_sieve_client(request) + try: + script = sclient.getscript(pk) + except SieveClientError: + return response.Response(status=404) + resp = HttpResponse(script) + resp["Content-Type"] = "text/plain; charset=utf-8" + resp["Content-Length"] = len(script) + resp["Content-Disposition"] = build_header(f"{pk}.txt") + return resp + + def destroy(self, request, pk): + """Delete an existing filter set.""" + sclient = self.get_sieve_client(request) + try: + fset = sclient.getscript(pk, format="fset") + except SieveClientError: + return response.Response(status=404) + sclient.deletescript(pk) + return response.Response(status=204) + @extend_schema( description="Retrieve the list of available filter condition templates", responses=serializers.ConditionTemplateSerializer, @@ -98,6 +140,10 @@ class FilterSetViewSet(viewsets.ViewSet): fset.addfilter(fltname, conditions, actions, match_type) except (BadArgument, BadValue) as inst: return response.Response(str(inst), status=400) + except FilterAlreadyExists: + return response.Response( + {"error": f"Filter {fset.name} already exists"}, status=409 + ) sclient.pushscript(fset.name, str(fset)) return response.Response(serializer.validated_data, status=201) @@ -130,6 +176,34 @@ class FilterSetViewSet(viewsets.ViewSet): sclient.pushscript(fset.name, str(fset)) return response.Response(serializer.validated_data, status=200) + @action( + methods=["post"], detail=True, url_path="filters/(?P[^/.]+)/disable" + ) + def disable_filter(self, request, pk, filter: str): + """Disable an existing filter.""" + sclient = self.get_sieve_client(request) + try: + fset = sclient.getscript(pk, format="fset") + except SieveClientError: + return response.Response(status=404) + if not fset.disablefilter(filter): + return response.Response(status=404) + sclient.pushscript(pk, str(fset)) + return response.Response(status=204) + + @action(methods=["post"], detail=True, url_path="filters/(?P[^/.]+)/enable") + def enable_filter(self, request, pk, filter: str): + """Enable an existing filter.""" + sclient = self.get_sieve_client(request) + try: + fset = sclient.getscript(pk, format="fset") + except SieveClientError: + return response.Response(status=404) + if not fset.enablefilter(filter): + return response.Response(status=404) + sclient.pushscript(pk, str(fset)) + return response.Response(status=204) + @extend_schema( description="Delete an existing filter", parameters=[ diff --git a/modoboa/sievefilters/lib.py b/modoboa/sievefilters/lib.py index fe4c33ac2..61a6bfcf7 100644 --- a/modoboa/sievefilters/lib.py +++ b/modoboa/sievefilters/lib.py @@ -25,6 +25,8 @@ class SieveClientError(ModoboaException): class SieveClient: """Sieve client.""" + msc: managesieve.Client + def __init__(self, user: Optional[str] = None, password: Optional[str] = None): if user and password: try: