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')"
/>
+
+
+
+ {{ $gettext('Actions') }}
+
+
+
+
-
+
@@ -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: