mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Added new API methods and updated frontend
This commit is contained in:
@@ -41,7 +41,6 @@ services:
|
||||
python3 manage.py runserver 0:8000"
|
||||
volumes:
|
||||
- .:/code
|
||||
- ../sievelib/sievelib:/opt/venv/lib/python3.12/site-packages/sievelib
|
||||
|
||||
front:
|
||||
container_name: modoboa_front
|
||||
|
||||
@@ -71,4 +71,15 @@ export default {
|
||||
data
|
||||
)
|
||||
},
|
||||
updateFilter(filterSetName, filter, data) {
|
||||
return repository.put(
|
||||
`${resource}/filtersets/${filterSetName}/filters/${filter}/`,
|
||||
data
|
||||
)
|
||||
},
|
||||
deleteFilter(filterSetName, filter) {
|
||||
return repository.delete(
|
||||
`${resource}/filtersets/${filterSetName}/filters/${filter}/`
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="[rules.required]"
|
||||
@update:model-value=""
|
||||
/>
|
||||
<v-select
|
||||
v-model="condition.operator"
|
||||
@@ -149,6 +150,7 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useBusStore } from '@/stores'
|
||||
import accountApi from '@/api/account'
|
||||
import rules from '@/plugins/rules'
|
||||
|
||||
@@ -164,6 +166,7 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['close'])
|
||||
const busStore = useBusStore()
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const conditionTemplates = ref([])
|
||||
@@ -178,9 +181,12 @@ const submitLabel = computed(() => {
|
||||
return (props.filter) ? $gettext('Update') : $gettext('Create')
|
||||
})
|
||||
|
||||
let originalFilterName
|
||||
|
||||
watch(() => props.filter, (value) => {
|
||||
if (value) {
|
||||
form.value = JSON.parse(JSON.stringify(props.filter))
|
||||
form.value = JSON.parse(JSON.stringify(value))
|
||||
originalFilterName = value.name
|
||||
} else {
|
||||
form.value = getInitialFormContent()
|
||||
}
|
||||
@@ -234,7 +240,7 @@ function getActionArguments(action) {
|
||||
function addCondition() {
|
||||
form.value.conditions.push({
|
||||
name: conditionTemplates.value[0].name,
|
||||
operator: conditionTemplates.value[0].operators[0]
|
||||
operator: conditionTemplates.value[0].operators[0].name
|
||||
})
|
||||
}
|
||||
|
||||
@@ -258,18 +264,30 @@ async function submit() {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
accountApi.createFilter(props.filterSet, form.value).then(() => {
|
||||
|
||||
})
|
||||
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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
accountApi.getFilterConditionTemplates().then(resp => {
|
||||
conditionTemplates.value = resp.data
|
||||
form.value.conditions[0].name = conditionTemplates.value[0].name
|
||||
form.value.conditions[0].operator = conditionTemplates.value[0].operators[0]
|
||||
if (!props.filter) {
|
||||
form.value.conditions[0].name = conditionTemplates.value[0].name
|
||||
form.value.conditions[0].operator = conditionTemplates.value[0].operators[0].name
|
||||
}
|
||||
})
|
||||
accountApi.getFilterActionTemplates().then(resp => {
|
||||
actionTemplates.value = resp.data
|
||||
form.value.actions[0].name = actionTemplates.value[0].name
|
||||
if (!props.filter) {
|
||||
form.value.actions[0].name = actionTemplates.value[0].name
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -88,11 +88,13 @@
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useBusStore } from '@/stores'
|
||||
import accountApi from '@/api/account'
|
||||
import FilterSetForm from '@/components/account/FilterSetForm.vue'
|
||||
import FilterForm from '@/components/account/FilterForm.vue'
|
||||
import MenuItems from '@/components/tools/MenuItems.vue'
|
||||
|
||||
const busStore = useBusStore()
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const filterSets = ref([])
|
||||
@@ -109,9 +111,7 @@ const headers = [
|
||||
]
|
||||
|
||||
watch(currentFilterSet, (value) => {
|
||||
accountApi.getFilters(value.name).then(resp => {
|
||||
filters.value = resp.data
|
||||
})
|
||||
fetchFilters(value.name)
|
||||
})
|
||||
|
||||
function closeFilterSetForm() {
|
||||
@@ -122,6 +122,7 @@ function closeFilterSetForm() {
|
||||
function closeFilterForm() {
|
||||
showFilterForm.value = false
|
||||
selectedFilter.value = null
|
||||
fetchFilters(currentFilterSet.value.name)
|
||||
}
|
||||
|
||||
function fetchFilterSets() {
|
||||
@@ -130,12 +131,22 @@ function fetchFilterSets() {
|
||||
})
|
||||
}
|
||||
|
||||
function fetchFilters(filterSetName) {
|
||||
accountApi.getFilters(filterSetName).then(resp => {
|
||||
filters.value = resp.data
|
||||
})
|
||||
}
|
||||
|
||||
function editFilter(filter) {
|
||||
selectedFilter.value = filter
|
||||
showFilterForm.value = true
|
||||
}
|
||||
|
||||
function deleteFilter(filter) {
|
||||
accountApi.deleteFilter(currentFilterSet.value.name, filter.name).then(() => {
|
||||
busStore.displayNotification({ msg: $gettext('Filter removed') })
|
||||
fetchFilters(currentFilterSet.value.name)
|
||||
})
|
||||
}
|
||||
|
||||
const filterActions = [
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Sievefilters serializers."""
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
from sievelib import commands
|
||||
|
||||
from rest_framework import serializers
|
||||
@@ -49,17 +51,16 @@ class FilterSerializer(serializers.Serializer):
|
||||
"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: ""}
|
||||
{"name": "Subject", "operator": "contains", "value": ""}
|
||||
]
|
||||
break
|
||||
elif isinstance(t, commands.SizeCommand):
|
||||
item["conditions"] += [
|
||||
{
|
||||
name: "size",
|
||||
"name": "size",
|
||||
"operator": t["comparator"][1:],
|
||||
"value": t["limit"],
|
||||
}
|
||||
@@ -94,14 +95,19 @@ class FilterSerializer(serializers.Serializer):
|
||||
result.append(item)
|
||||
return FilterSerializer(result, many=True)
|
||||
|
||||
def to_filter(self):
|
||||
"""Convert serializer data to filter."""
|
||||
conditions = []
|
||||
def to_filter(self) -> Tuple[str, List, List]:
|
||||
"""Convert serializer data to filter representation."""
|
||||
conditions: List[Tuple] = []
|
||||
actions = []
|
||||
for condition in self.validated_data["conditions"]:
|
||||
conditions.append(
|
||||
[condition["name"], f":{condition['operator']}", condition["value"]]
|
||||
)
|
||||
match_type = self.validated_data["match_type"]
|
||||
if match_type == "all":
|
||||
match_type = "anyof"
|
||||
conditions = [("true",)]
|
||||
else:
|
||||
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"])
|
||||
@@ -112,7 +118,7 @@ class FilterSerializer(serializers.Serializer):
|
||||
else:
|
||||
action += [arg for arg in input_action["args"].values()]
|
||||
actions.append(action)
|
||||
return (conditions, actions)
|
||||
return (match_type, conditions, actions)
|
||||
|
||||
|
||||
class OperatorSerializer(serializers.Serializer):
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"""Sievefilters viewsets."""
|
||||
|
||||
import copy
|
||||
from typing import Optional
|
||||
|
||||
from sievelib.commands import BadArgument, BadValue
|
||||
|
||||
from drf_spectacular.types import OpenApiTypes
|
||||
from drf_spectacular.utils import extend_schema, OpenApiParameter
|
||||
from rest_framework import response, viewsets
|
||||
from rest_framework.decorators import action
|
||||
|
||||
@@ -22,6 +27,7 @@ class FilterSetViewSet(viewsets.ViewSet):
|
||||
# raise
|
||||
|
||||
def list(self, request):
|
||||
"""Retrieve list of available filter sets."""
|
||||
sclient = self.get_sieve_client(request)
|
||||
active_script, scripts = sclient.listscripts()
|
||||
scripts = [
|
||||
@@ -30,6 +36,7 @@ class FilterSetViewSet(viewsets.ViewSet):
|
||||
return response.Response(scripts)
|
||||
|
||||
def create(self, request):
|
||||
"""Create a new filter set."""
|
||||
serializer = serializers.FilterSetSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
sclient = self.get_sieve_client(request)
|
||||
@@ -40,6 +47,10 @@ class FilterSetViewSet(viewsets.ViewSet):
|
||||
)
|
||||
return response.Response(serializer.validated_data, 201)
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve the list of available filter condition templates",
|
||||
responses=serializers.ConditionTemplateSerializer,
|
||||
)
|
||||
@action(methods=["get"], detail=False)
|
||||
def condition_templates(self, request):
|
||||
templates = copy.deepcopy(constants.CONDITION_TEMPLATES)
|
||||
@@ -51,6 +62,10 @@ class FilterSetViewSet(viewsets.ViewSet):
|
||||
serializer = serializers.ConditionTemplateSerializer(templates, many=True)
|
||||
return response.Response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
description="Retrieve the list of available filter action templates",
|
||||
responses=serializers.ActionTemplateSerializer,
|
||||
)
|
||||
@action(methods=["get"], detail=False)
|
||||
def action_templates(self, request):
|
||||
serializer = serializers.ActionTemplateSerializer(
|
||||
@@ -58,11 +73,18 @@ class FilterSetViewSet(viewsets.ViewSet):
|
||||
)
|
||||
return response.Response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter("id", OpenApiTypes.STR),
|
||||
],
|
||||
request=serializers.FilterSerializer,
|
||||
responses={201: serializers.FilterSerializer},
|
||||
)
|
||||
@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")
|
||||
fset = sclient.getscript(pk, format="fset")
|
||||
except SieveClientError:
|
||||
return response.Response(status=404)
|
||||
if request.method == "GET":
|
||||
@@ -70,20 +92,61 @@ class FilterSetViewSet(viewsets.ViewSet):
|
||||
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")
|
||||
match_type, conditions, actions = serializer.to_filter()
|
||||
fltname = serializer.validated_data["name"]
|
||||
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))
|
||||
sclient.pushscript(fset.name, str(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)
|
||||
@extend_schema(
|
||||
description="Update an existing filter",
|
||||
parameters=[
|
||||
OpenApiParameter("id", OpenApiTypes.STR, location=OpenApiParameter.PATH),
|
||||
OpenApiParameter(
|
||||
"filter", OpenApiTypes.STR, location=OpenApiParameter.PATH
|
||||
),
|
||||
],
|
||||
request=serializers.FilterSerializer,
|
||||
responses=serializers.FilterSerializer,
|
||||
)
|
||||
@action(methods=["PUT"], detail=True, url_path="filters/(?P<filter>[^/.]+)")
|
||||
def update_filter(self, request, pk, filter: str):
|
||||
sclient = self.get_sieve_client(request)
|
||||
try:
|
||||
fset = sclient.getscript(pk, format="fset")
|
||||
except SieveClientError:
|
||||
return response.Response(status=404)
|
||||
serializer = serializers.FilterSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
match_type, conditions, actions = serializer.to_filter()
|
||||
name = serializer.validated_data["name"]
|
||||
try:
|
||||
fset.updatefilter(filter, name, conditions, actions, match_type)
|
||||
except (BadArgument, BadValue) as inst:
|
||||
return response.Response(str(inst), status=400)
|
||||
sclient.pushscript(fset.name, str(fset))
|
||||
return response.Response(serializer.validated_data, status=200)
|
||||
|
||||
@extend_schema(
|
||||
description="Delete an existing filter",
|
||||
parameters=[
|
||||
OpenApiParameter("id", OpenApiTypes.STR, location=OpenApiParameter.PATH),
|
||||
OpenApiParameter(
|
||||
"filter", OpenApiTypes.STR, location=OpenApiParameter.PATH
|
||||
),
|
||||
],
|
||||
)
|
||||
@update_filter.mapping.delete
|
||||
def delete_filter(self, request, pk, filter: str):
|
||||
sclient = self.get_sieve_client(request)
|
||||
try:
|
||||
fset = sclient.getscript(pk, format="fset")
|
||||
except SieveClientError:
|
||||
return response.Response(status=404)
|
||||
if fset.removefilter(filter):
|
||||
sclient.pushscript(fset.name, str(fset))
|
||||
return response.Response(status=204)
|
||||
return response.Response(status=404)
|
||||
|
||||
+16
-16
@@ -1,8 +1,7 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""Internal tools."""
|
||||
|
||||
import ssl
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
from sievelib.factory import FiltersSet
|
||||
from sievelib import managesieve
|
||||
@@ -26,15 +25,16 @@ class SieveClientError(ModoboaException):
|
||||
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 __init__(self, user: Optional[str] = None, password: Optional[str] = None):
|
||||
if user and password:
|
||||
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):
|
||||
def login(self, user: str, password: str) -> Tuple[bool, Union[str, None]]:
|
||||
conf = dict(param_tools.get_global_parameters("sievefilters"))
|
||||
self.msc = managesieve.Client(conf["server"], conf["port"], debug=True)
|
||||
authmech = conf["authentication_mech"]
|
||||
@@ -57,7 +57,7 @@ class SieveClient:
|
||||
self.msc.logout()
|
||||
self.msc = None
|
||||
|
||||
def refresh(self, user, password):
|
||||
def refresh(self, user: str, password: str):
|
||||
if self.msc is not None:
|
||||
try:
|
||||
self.msc.capability()
|
||||
@@ -75,7 +75,7 @@ class SieveClient:
|
||||
def listscripts(self):
|
||||
return self.msc.listscripts()
|
||||
|
||||
def getscript(self, name: str, format: str = "raw") -> FiltersSet | str | None:
|
||||
def getscript(self, name: str, format: str = "raw") -> Union[FiltersSet, str, None]:
|
||||
content = self.msc.getscript(name)
|
||||
if content is None:
|
||||
raise SieveClientError(self.msc.errmsg.decode())
|
||||
@@ -98,18 +98,18 @@ class SieveClient:
|
||||
if active and not self.msc.setactive(name):
|
||||
raise SieveClientError(self.msc.errmsg)
|
||||
|
||||
def deletescript(self, name):
|
||||
def deletescript(self, name: str):
|
||||
if not self.msc.deletescript(name):
|
||||
raise SieveClientError(self.msc.errmsg.decode())
|
||||
|
||||
def activatescript(self, name):
|
||||
def activatescript(self, name: str):
|
||||
if not self.msc.setactive(name):
|
||||
raise SieveClientError(self.msc.errmsg.decode())
|
||||
|
||||
|
||||
def find_action_template(action):
|
||||
def find_action_template(action: str):
|
||||
"""Find template corresponding to action."""
|
||||
for tpl in constants.ACTION_TEMPLATES:
|
||||
if tpl["name"] == action:
|
||||
return tpl
|
||||
raise RuntimeError("action {} not defined".format(action))
|
||||
raise RuntimeError(f"action {action} not defined")
|
||||
|
||||
+1
-1
@@ -51,4 +51,4 @@ rq>=1.13.0
|
||||
django-rq
|
||||
|
||||
# Sieve
|
||||
sievelib>=1.1.1
|
||||
sievelib>=1.3.0
|
||||
|
||||
Reference in New Issue
Block a user