mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Imported Rspamd to main repo
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import repository from './repository'
|
||||
|
||||
export default {
|
||||
getCapabilities() {
|
||||
return repository.get(`/capabilities/`)
|
||||
},
|
||||
}
|
||||
@@ -3,16 +3,20 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useAuthStore, useParametersStore } from '@/stores'
|
||||
import parametersApi from '@/api/parameters'
|
||||
import capabilitiesApi from '@/api/capabilities'
|
||||
import ConnectedLayout from '@/layouts/connected/ConnectedLayout.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const authStore = useAuthStore()
|
||||
const parametersStore = useParametersStore()
|
||||
|
||||
const isRspamdVisible = ref(false)
|
||||
const rspamd_url = ref('/rspamd')
|
||||
|
||||
const authUser = computed(() => authStore.authUser)
|
||||
const imapMigrationEnabled = computed(
|
||||
() => parametersStore.imapMigrationEnabled
|
||||
@@ -89,6 +93,13 @@ const adminMenuItems = [
|
||||
children: settings,
|
||||
roles: ['SuperAdmins'],
|
||||
},
|
||||
{
|
||||
icon: 'mdi-email-check-outline',
|
||||
text: 'Rspamd',
|
||||
to: rspamd_url.value,
|
||||
roles: ['SuperAdmins'],
|
||||
condition: () => isRspamdVisible.value,
|
||||
},
|
||||
{
|
||||
icon: 'mdi-information',
|
||||
text: $gettext('Information'),
|
||||
@@ -114,6 +125,12 @@ onMounted(() => {
|
||||
response.data.params.enabled_imapmigration
|
||||
})
|
||||
}
|
||||
capabilitiesApi.getCapabilities().then((response) => {
|
||||
isRspamdVisible.value = 'rspamd' in response.data.capabilities
|
||||
if (isRspamdVisible.value) {
|
||||
rspamd_url.value = response.data.capabilities.rspamd.location || '/rspamd'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -37,4 +37,5 @@ urlpatterns += [
|
||||
views.NotificationsAPIView.as_view(),
|
||||
name="notifications",
|
||||
),
|
||||
path("capabilities/", views.CapabilitiesAPIView.as_view(), name="capabilities"),
|
||||
]
|
||||
|
||||
@@ -11,7 +11,7 @@ from rest_framework import permissions, response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from modoboa.core import signals
|
||||
from modoboa.core.utils import check_for_updates
|
||||
from modoboa.core.utils import check_for_updates, get_capabilities
|
||||
from modoboa.lib.permissions import IsSuperUser
|
||||
from modoboa.lib.throttle import (
|
||||
UserLesserDdosUser,
|
||||
@@ -161,3 +161,13 @@ class NotificationsAPIView(APIView):
|
||||
)
|
||||
serializer = serializers.NotificationSerializer(notifications, many=True)
|
||||
return response.Response(serializer.data)
|
||||
|
||||
|
||||
class CapabilitiesAPIView(APIView):
|
||||
"""Return the capability of this Modoboa instance."""
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
throttle_classes = [UserLesserDdosUser]
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
return response.Response({"capabilities": get_capabilities()})
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from django.utils.translation import gettext as _
|
||||
from modoboa.core.extensions import exts_pool
|
||||
from modoboa.core.password_hashers import get_configured_password_hasher, PasswordHasher
|
||||
from modoboa.lib.api_client import ModoAPIClient
|
||||
from modoboa.rspamd.utils import check_rspamd_installed
|
||||
|
||||
|
||||
def parse_map_file(path):
|
||||
@@ -101,3 +102,13 @@ def check_for_deprecated_password_schemes() -> Optional[type[PasswordHasher]]:
|
||||
if models.User.objects.is_password_scheme_in_use(dhasher):
|
||||
return dhasher
|
||||
return None
|
||||
|
||||
|
||||
def get_capabilities():
|
||||
"""Return the list of capabilities of this modoboa instance."""
|
||||
capabilities = {}
|
||||
# Rspamd
|
||||
rspamd_options = check_rspamd_installed()
|
||||
if rspamd_options is not None:
|
||||
capabilities.update({"rspamd": rspamd_options})
|
||||
return capabilities
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""The rspamd frontend of Modoboa."""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from pkg_resources import DistributionNotFound, get_distribution
|
||||
|
||||
try:
|
||||
__version__ = get_distribution(__name__).version
|
||||
except DistributionNotFound:
|
||||
# package is not installed
|
||||
pass
|
||||
|
||||
default_app_config = "modoboa.rspamd.apps.ModoboaRspamdConfig"
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Modoboa rspamd serializer for api v2."""
|
||||
|
||||
import os
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
def validate_file_path(value):
|
||||
if value:
|
||||
if os.path.isfile(value):
|
||||
if not os.access(value, os.W_OK):
|
||||
# File exists and is writable by modoboa
|
||||
raise serializers.ValidationError(_("Provided path is not writable."))
|
||||
elif not os.path.isdir(os.path.dirname(value)):
|
||||
raise serializers.ValidationError(
|
||||
_("Please create the parent directory for the files")
|
||||
)
|
||||
elif not os.access(os.path.dirname(value), os.W_OK):
|
||||
# File doesn't exist and modoboa won't be able to create it...
|
||||
raise serializers.ValidationError(_("Unable to write to the directory."))
|
||||
|
||||
|
||||
class RspamdSettingsSerializer(serializers.Serializer):
|
||||
"""A serializer for global parameters."""
|
||||
|
||||
# dkim_settings_sep
|
||||
key_map_path = serializers.CharField(
|
||||
default="/var/lib/dkim/keys.path.map", allow_blank=True
|
||||
)
|
||||
selector_map_path = serializers.CharField(
|
||||
default="/var/lib/dkim/selectors.path.map", allow_blank=True
|
||||
)
|
||||
|
||||
# miscellaneous_sep
|
||||
rspamd_dashboard_location = serializers.CharField(
|
||||
default="/rspamd", allow_blank=True
|
||||
)
|
||||
|
||||
def validate_key_map_path(self, value):
|
||||
validate_file_path(value)
|
||||
return value
|
||||
|
||||
def validate_selector_map_path(self, value):
|
||||
validate_file_path(value)
|
||||
return value
|
||||
@@ -0,0 +1,111 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Modoboa rspamd forms."""
|
||||
|
||||
import collections
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from modoboa.lib import form_utils
|
||||
from modoboa.parameters import forms as param_forms
|
||||
|
||||
RSPAMD_PARAMETERS_STRUCT = collections.OrderedDict(
|
||||
[
|
||||
(
|
||||
"dkim_settings_sep",
|
||||
{
|
||||
"label": _("DKIM signing settings"),
|
||||
"params": collections.OrderedDict(
|
||||
[
|
||||
(
|
||||
"key_map_path",
|
||||
{
|
||||
"label": _("Key map path"),
|
||||
"help_text": _(
|
||||
"Absolute path of the file which contains "
|
||||
"paths to DKIM private keys. "
|
||||
"Must be readable by _rspamd group or user."
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"selector_map_path",
|
||||
{
|
||||
"label": _("Selector map path"),
|
||||
"help_text": _(
|
||||
"Absolute path of the file which contains "
|
||||
"names of DKIM selectors. "
|
||||
"Must be readable by _rspamd group or user."
|
||||
),
|
||||
},
|
||||
),
|
||||
]
|
||||
),
|
||||
},
|
||||
),
|
||||
(
|
||||
"miscellaneous_sep",
|
||||
{
|
||||
"label": _("Miscellaneous"),
|
||||
"params": collections.OrderedDict(
|
||||
[
|
||||
(
|
||||
"rspamd_dashboard_location",
|
||||
{
|
||||
"label": _("Rspamd dashboard location"),
|
||||
"help_text": _(
|
||||
"Location of the rspamd dashboard. "
|
||||
"Can either be a relative URL (eg. '/rspamd') "
|
||||
"or a full URL (eg. 'https://rspamd.domain.tld/')."
|
||||
"Only use for guidance on the Super-Admin dashboard."
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
},
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ParametersForm(param_forms.AdminParametersForm):
|
||||
"""Extension settings."""
|
||||
|
||||
app = "modoboa.rspamd"
|
||||
|
||||
dkim_settings_sep = form_utils.SeparatorField(label=_("DKIM signing settings"))
|
||||
key_map_path = forms.CharField(
|
||||
label=_("Key map path"),
|
||||
initial="",
|
||||
help_text=_(
|
||||
"Absolute path of the file which contains paths to DKIM "
|
||||
"private keys. "
|
||||
"Must be readable by _rspamd group or user."
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
selector_map_path = forms.CharField(
|
||||
label=_("Selector map path"),
|
||||
initial="",
|
||||
help_text=_(
|
||||
"Absolute path of the file which contains names of "
|
||||
"DKIM selectors. "
|
||||
"Must be readable by _rspamd group or user."
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
miscellaneous_sep = form_utils.SeparatorField(label=_("Miscellaneous"))
|
||||
rspamd_dashboard_location = forms.CharField(
|
||||
label=_("Rspamd dashboard location"),
|
||||
initial="/rspamd",
|
||||
help_text=_(
|
||||
"Location of the rspamd dashboard. "
|
||||
"Can either be a relative URL (eg. '/rspamd') "
|
||||
"or a full URL (eg. 'https://rspamd.domain.tld/')."
|
||||
"Only use for guidance on the Super-Admin dashboard."
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from .utils import check_rspamd_installed
|
||||
|
||||
|
||||
def load_rspamd_settings():
|
||||
"""Load rspamd settings.
|
||||
|
||||
This function must be manually called (see :file:`urls.py`) in
|
||||
order to load base settings.
|
||||
"""
|
||||
from modoboa.parameters import tools as param_tools
|
||||
from . import app_settings
|
||||
from .api.v2 import serializers
|
||||
|
||||
param_tools.registry.add(
|
||||
"global", app_settings.ParametersForm, gettext_lazy("Rspamd")
|
||||
)
|
||||
param_tools.registry.add2(
|
||||
"global",
|
||||
"rspamd",
|
||||
gettext_lazy("Rspamd"),
|
||||
app_settings.RSPAMD_PARAMETERS_STRUCT,
|
||||
serializers.RspamdSettingsSerializer,
|
||||
)
|
||||
|
||||
|
||||
class ModoboaRspamdConfig(AppConfig):
|
||||
name = "modoboa.rspamd"
|
||||
verbose_name = "Modoboa connector for Rspamd."
|
||||
|
||||
def ready(self):
|
||||
if check_rspamd_installed():
|
||||
from . import handlers # NOQA:F401
|
||||
|
||||
load_rspamd_settings()
|
||||
@@ -0,0 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""Modoboa rspamd handlers."""
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
|
||||
import django_rq
|
||||
|
||||
from modoboa.admin import models as admin_models
|
||||
|
||||
|
||||
@receiver(post_save, sender=admin_models.Domain)
|
||||
def update_rspamd_dkim_maps(sender, instance, created, **kwargs):
|
||||
"""Update config maps."""
|
||||
# Modify or create
|
||||
condition = (
|
||||
instance._loaded_values.get("dkim_private_key_path")
|
||||
!= instance.dkim_private_key_path
|
||||
or instance._loaded_values.get("dkim_key_selector")
|
||||
!= instance.dkim_key_selector
|
||||
or instance._loaded_values.get("enable_dkim") != instance.enable_dkim
|
||||
)
|
||||
if condition:
|
||||
# TODO : check how to handle it
|
||||
# Either with modoboa user or _rspamd...
|
||||
queue = django_rq.get_queue("modoboa")
|
||||
queue.enqueue(call_command, "manage_rspamd_maps", f"--domain={instance.name}")
|
||||
@@ -0,0 +1,100 @@
|
||||
"""Management command to create DKIM keys."""
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from modoboa.parameters import tools as param_tools
|
||||
from modoboa.admin import models
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Command class."""
|
||||
|
||||
def load_files(self):
|
||||
config = dict(param_tools.get_global_parameters("modoboa.rspamd"))
|
||||
if not config["key_map_path"] or not config["selector_map_path"]:
|
||||
raise CommandError(
|
||||
"path map path and/or selector map path "
|
||||
"not set in modoboa rspamd settings."
|
||||
)
|
||||
|
||||
self.dkim_path_map = {}
|
||||
try:
|
||||
with open(config["key_map_path"], "r") as f:
|
||||
for line in f:
|
||||
domain_name, path = line.split()
|
||||
self.dkim_path_map[domain_name] = path.replace("\n", "")
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
self.selector_map = {}
|
||||
try:
|
||||
with open(config["selector_map_path"], "r") as f:
|
||||
for line in f:
|
||||
domain_name, selector = line.split()
|
||||
self.selector_map[domain_name] = selector
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
def manage_domain(self, domain_instance):
|
||||
domain_name = domain_instance.name
|
||||
selector_entry = self.selector_map.get(domain_name)
|
||||
dkim_path_entry = self.dkim_path_map.get(domain_name)
|
||||
if not domain_instance.enable_dkim:
|
||||
if selector_entry is not None:
|
||||
self.selector_map.pop(domain_name)
|
||||
self.modified_selector_file = True
|
||||
if dkim_path_entry is not None:
|
||||
self.dkim_path_map.pop(domain_name)
|
||||
self.modified_key_path_file = True
|
||||
return
|
||||
|
||||
# modify selector map
|
||||
condition = (
|
||||
selector_entry is None
|
||||
or selector_entry != domain_instance.dkim_key_selector
|
||||
)
|
||||
if condition:
|
||||
self.selector_map[domain_name] = domain_instance.dkim_key_selector
|
||||
self.modified_selector_file = True
|
||||
|
||||
# modify dkim path map
|
||||
condition = (
|
||||
dkim_path_entry is None
|
||||
or dkim_path_entry != domain_instance.dkim_private_key_path
|
||||
)
|
||||
if condition:
|
||||
self.dkim_path_map[domain_name] = domain_instance.dkim_private_key_path
|
||||
self.modified_key_path_file = True
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add arguments to command."""
|
||||
parser.add_argument(
|
||||
"--domain",
|
||||
type=str,
|
||||
dest="domain",
|
||||
default="",
|
||||
help="Domain target for keys generation.",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Entry point."""
|
||||
self.modified_selector_file = False
|
||||
self.modified_key_path_file = False
|
||||
self.load_files()
|
||||
|
||||
if options["domain"] != "":
|
||||
domain = models.Domain.objects.filter(name=options["domain"])
|
||||
if domain.exists():
|
||||
self.manage_domain(domain[0])
|
||||
else:
|
||||
qset = models.Domain.objects.all()
|
||||
for domain in qset:
|
||||
self.manage_domain(domain)
|
||||
config = dict(param_tools.get_global_parameters("modoboa.rspamd"))
|
||||
|
||||
if self.modified_selector_file:
|
||||
with open(config["selector_map_path"], "w") as f:
|
||||
for domain_name, selector in self.selector_map.items():
|
||||
f.write(f"{domain_name} {selector}\n")
|
||||
if self.modified_key_path_file:
|
||||
with open(config["key_map_path"], "w") as f:
|
||||
for domain_name, key_path in self.dkim_path_map.items():
|
||||
f.write(f"{domain_name} {key_path}\n")
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Utilies for modoboa.rspamd plugin."""
|
||||
|
||||
import shutil
|
||||
|
||||
from django.core.exceptions import AppRegistryNotReady
|
||||
|
||||
from modoboa.lib import signals as lib_signals
|
||||
|
||||
|
||||
def check_rspamd_installed():
|
||||
try:
|
||||
is_rspamd_installed = shutil.which("rspamd") is not None
|
||||
rspamd_options = {} if is_rspamd_installed else None
|
||||
request = lib_signals.get_request()
|
||||
if is_rspamd_installed:
|
||||
if request is not None:
|
||||
rspamd_options["location"] = request.localconfig.parameters.get_value(
|
||||
"rspamd_dashboard_location"
|
||||
)
|
||||
else:
|
||||
rspamd_options["location"] = None
|
||||
except AppRegistryNotReady:
|
||||
return None
|
||||
return rspamd_options
|
||||
@@ -0,0 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -99,6 +99,7 @@ MODOBOA_APPS = (
|
||||
"modoboa.imap_migration",
|
||||
"modoboa.autoreply",
|
||||
"modoboa.sievefilters",
|
||||
"modoboa.rspamd",
|
||||
# Modoboa extensions here.
|
||||
"modoboa.contacts",
|
||||
"modoboa.calendars",
|
||||
|
||||
Reference in New Issue
Block a user