Various fixes

This commit is contained in:
Antoine Nguyen
2025-06-24 18:15:40 +02:00
parent cd73a19596
commit 205bc4d057
26 changed files with 58 additions and 95 deletions
+1 -1
View File
@@ -209,7 +209,7 @@ Required changes to :file:`settings.py`
- Remove ``NEW_ADMIN_URL`` variable
- Remove ``new_admin_url`` context processor
- Remove ``new_admin_url`` and ``top_notifications`` context processors
- Remove ``ckeditor`` related apps from ``INSTALLED_APPS`` variable
+2 -2
View File
@@ -94,8 +94,8 @@ export default {
}
return repository.post('/webmail/emails/flag/', body)
},
sendEmail(body) {
return repository.post('/webmail/emails/send/', body)
sendEmailFromComposeSession(uid, body) {
return repository.post(`/webmail/compose-sessions/${uid}/send/`, body)
},
getComposeSession(uid) {
return repository.get(`/webmail/compose-sessions/${uid}/`)
@@ -46,6 +46,12 @@
<v-spacer></v-spacer>
</v-toolbar>
</template>
<template #[`item.display_name`]="{ item }">
<template v-if="item.display_name">
{{ item.display_name }}
</template>
<template v-else> {{ item.fist_name }} {{ item.last_name }} </template>
</template>
<template #[`item.emails`]="{ item }">
{{ displayEmails(item.emails) }}
</template>
@@ -90,7 +90,7 @@ const removeAttachment = async (attachment) => {
attachments.value.splice(index, 1)
}
api.getUploadedAttachments(props.sessionUid).then((resp) => {
attachments.value = resp.data
api.getComposeSession(props.sessionUid).then((resp) => {
attachments.value = resp.data.attachments
})
</script>
@@ -263,7 +263,7 @@ const submit = async () => {
body.bcc = bcc
}
try {
await api.sendEmail(body)
await api.sendEmailFromComposeSession(route.query.uid, body)
router.push({ name: 'MailboxView' })
displayNotification({ msg: $gettext('Email sent') })
} catch (error) {
+1 -1
View File
@@ -134,7 +134,7 @@ onMounted(() => {
capabilitiesApi.getCapabilities().then((response) => {
isRspamdVisible.value = 'rspamd' in response.data.capabilities
if (isRspamdVisible.value) {
rspamd_url.value =
rspamdUrl.value =
response.data.capabilities.rspamd.location || '/rspamd'
}
})
+1 -1
View File
@@ -34,7 +34,7 @@ export default defineConfig({
}),
],
base: '/',
define: { 'process.env': {}, __VUE_OPTIONS_API__: false },
define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
+1 -3
View File
@@ -1,7 +1,5 @@
"""Admin serializers."""
from typing import List
from django.contrib.auth import password_validation
from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404
@@ -277,7 +275,7 @@ class AccountSerializer(serializers.ModelSerializer):
"""Return role."""
return account.role
def get_domains(self, account) -> List[str]:
def get_domains(self, account) -> list[str]:
"""Return domains administered by this account."""
if account.role not in ["DomainAdmins", "Resellers"]:
return []
+2 -3
View File
@@ -2,7 +2,6 @@
import ipaddress
import os
from typing import List
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -388,7 +387,7 @@ class AccountMeSerializer(v1_serializers.AccountSerializer):
class Meta(v1_serializers.AccountSerializer.Meta):
fields = v1_serializers.AccountSerializer.Meta.fields + ("permissions",)
def get_permissions(self, user) -> List[str]:
def get_permissions(self, user) -> list[str]:
"""Return available permissions for given user."""
permissions = (
user.user_permissions.all() | Permission.objects.filter(group__user=user)
@@ -412,7 +411,7 @@ class AccountSerializer(v1_serializers.AccountSerializer):
return
self.fields["resources"] = serializers.SerializerMethodField()
def get_resources(self, account) -> List[AccountResourceSerializer]:
def get_resources(self, account) -> list[AccountResourceSerializer]:
if account.role == "SimpleUsers":
return []
resources = []
+1 -1
View File
@@ -48,7 +48,7 @@ class AdminObject(models.Model):
def from_db(cls, db, field_names, values):
"""Store loaded values."""
instance = super().from_db(db, field_names, values)
instance._loaded_values = dict(zip(field_names, values))
instance._loaded_values = dict(zip(field_names, values, strict=False))
return instance
@property
+1 -1
View File
@@ -56,7 +56,7 @@ class ARmessage(models.Model):
)
content = self.content % context
name = "autoreply"
action = [("vacation", ":subject", self.subject, ":days", days, content)]
action = [("vacation", ":subject", self.subject, ":days", str(days), content)]
if not fset.getfilter(name):
fset.addfilter(name, condition, action)
else:
+1 -2
View File
@@ -1,5 +1,4 @@
import csv
from typing import Optional
from modoboa.contacts.importer.backends.outlook import OutlookBackend
@@ -29,7 +28,7 @@ def import_csv_file(
backend_name: str,
csv_filename: str,
delimiter: str,
carddav_password: Optional[str] = None,
carddav_password: str | None = None,
):
with open(csv_filename) as fp:
backend, rows = get_import_backend(fp, delimiter, backend_name)
+4 -6
View File
@@ -1,7 +1,5 @@
"""Extension management."""
from typing import Dict, Optional
from django.conf import settings
from django.urls import include
from django.urls import re_path
@@ -22,8 +20,8 @@ class ModoExtension:
description: str = ""
needs_media: bool = False
always_active: bool = False
url: Optional[str] = None
topredirection_url: Optional[str] = None
url: str | None = None
topredirection_url: str | None = None
def get_available_apps(self) -> list:
return []
@@ -59,7 +57,7 @@ class ExtensionsPool:
"""The extensions manager"""
def __init__(self) -> None:
self.extensions: Dict[str, Dict] = {}
self.extensions: dict[str, dict] = {}
def register_extension(self, ext: ModoExtension, show: bool = True) -> None:
"""Register an extension.
@@ -69,7 +67,7 @@ class ExtensionsPool:
"""
self.extensions[ext.name] = {"cls": ext, "show": show}
def get_extension(self, name: str) -> Optional[ModoExtension]:
def get_extension(self, name: str) -> ModoExtension | None:
"""Retrieve the current instance of an extension."""
if name not in self.extensions:
return None
+1 -2
View File
@@ -2,7 +2,6 @@
import re
from email.header import Header
from typing import Optional
from django.core.serializers.json import DjangoJSONEncoder
from django.conf import settings
@@ -132,7 +131,7 @@ class User(AbstractUser):
raw_value = smart_bytes(raw_value)
return get_password_hasher(scheme.upper())().encrypt(raw_value)
def set_password(self, raw_value: str, curvalue: Optional[str] = None) -> None:
def set_password(self, raw_value: str, curvalue: str | None = None) -> None:
"""Password update.
Update the current mailbox's password with the given clear
-9
View File
@@ -1,12 +1,3 @@
"""A plugin to migrate mailboxes using IMAP."""
from pkg_resources import get_distribution, DistributionNotFound
try:
__version__ = get_distribution(__name__).version
except DistributionNotFound:
# package is not installed
pass
default_app_config = "modoboa.imap_migration.apps.IMAPMigrationConfig"
+1 -3
View File
@@ -4,8 +4,6 @@
"""
from typing import Optional
from django.utils.translation import gettext as _
@@ -14,7 +12,7 @@ class ModoboaException(Exception):
Base class for Modoboa custom exceptions.
"""
http_code: Optional[int] = None
http_code: int | None = None
def __init__(self, *args, **kwargs):
if "http_code" in kwargs:
+1 -3
View File
@@ -1,7 +1,5 @@
"""Test cases for the limits extension."""
from typing import Optional
from django.urls import reverse
from modoboa.admin.factories import populate_database
@@ -76,7 +74,7 @@ class ResourceTestCase(lib_tests.ModoAPITestCase):
self.assertEqual(response.status_code, status)
return response
def _create_alias(self, email, rcpt: Optional[list] = None, status=201):
def _create_alias(self, email, rcpt: list | None = None, status=201):
if rcpt is None:
rcpt = ["user@test.com"]
values = {"address": email, "recipients": rcpt, "enabled": True}
+4 -5
View File
@@ -1,7 +1,6 @@
"""Parameters management."""
import copy
from typing import Optional
from rest_framework.fields import empty
@@ -102,7 +101,7 @@ class Registry:
raise NotDefined(app)
return self._registry2[level][app]["serializer_class"]
def get_structure(self, level: str, for_app: Optional[str] = None) -> list:
def get_structure(self, level: str, for_app: str | None = None) -> list:
"""Return fields structure."""
result = []
for app, fields in list(self._registry2[level].items()):
@@ -141,7 +140,7 @@ class Registry:
result.append(item)
return result
def exists(self, level: str, app: str, parameter: Optional[str] = None) -> bool:
def exists(self, level: str, app: str, parameter: str | None = None) -> bool:
"""Check if parameter exists."""
parameters = self._registry2[level]
result = app in parameters
@@ -193,7 +192,7 @@ class Manager:
return self._parameters[app].get(parameter, default)
return default
def get_values(self, app: Optional[str] = None):
def get_values(self, app: str | None = None):
"""Return all values for the given level/app."""
if app is None:
app = guess_extension_name()
@@ -225,7 +224,7 @@ class Manager:
)
self._parameters[app][parameter] = value
def set_values(self, values: dict, app: Optional[str] = None) -> None:
def set_values(self, values: dict, app: str | None = None) -> None:
"""Set/update values for the given app."""
if app is None:
app = guess_extension_name()
-5
View File
@@ -1,6 +1 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib import admin
# Register your models here.
+1 -10
View File
@@ -1,12 +1,3 @@
"""The sieve filters editor of Modoboa."""
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"
default_app_config = "modoboa.sievefilters.apps.SieveFiltersConfig"
+3 -5
View File
@@ -1,7 +1,5 @@
"""Sievefilters serializers."""
from typing import List, Tuple
from sievelib import commands
from sievelib.factory import Filter
@@ -61,7 +59,7 @@ class FilterSerializer(serializers.Serializer):
actions = ActionSerializer(many=True)
@staticmethod
def from_filters(filters: List[Filter]) -> "FilterSerializer":
def from_filters(filters: list[Filter]) -> "FilterSerializer":
result = []
for fobj in filters:
if fobj["name"] == "autoreply":
@@ -117,9 +115,9 @@ class FilterSerializer(serializers.Serializer):
result.append(item)
return FilterSerializer(result, many=True)
def to_filter(self) -> Tuple[str, List, List]:
def to_filter(self) -> tuple[str, list, list]:
"""Convert serializer data to filter representation."""
conditions: List[Tuple] = []
conditions: list[tuple] = []
actions = []
match_type = self.validated_data["match_type"]
if match_type == "all":
+5 -5
View File
@@ -1,6 +1,6 @@
"""Value loading functions."""
from typing import List, Optional, TypedDict
from typing import TypedDict
from modoboa.webmail.lib import imaputils
@@ -13,9 +13,9 @@ class UserMailbox(TypedDict):
def __build_folders_list(
folders: list, user, imapc: imaputils.IMAPconnector, parentmb: Optional[str] = None
) -> List[UserMailbox]:
ret: List[UserMailbox] = []
folders: list, user, imapc: imaputils.IMAPconnector, parentmb: str | None = None
) -> list[UserMailbox]:
ret: list[UserMailbox] = []
for fd in folders:
value = fd["path"] if "path" in fd else fd["name"]
if parentmb:
@@ -33,7 +33,7 @@ def __build_folders_list(
return ret
def user_mailboxes(request) -> List[UserMailbox]:
def user_mailboxes(request) -> list[UserMailbox]:
"""Retrieve list of available mailboxes for given user."""
with imaputils.get_imapconnector(request, with_namespaces=False) as mbc:
ret = mbc.getmboxes(request.user)
+4 -6
View File
@@ -1,7 +1,5 @@
"""Internal tools."""
from typing import Optional, Tuple, Union
from sievelib.factory import FiltersSet
from sievelib import managesieve
from sievelib.parser import Parser
@@ -27,7 +25,7 @@ class SieveClient:
msc: managesieve.Client
def __init__(self, user: Optional[str] = None, password: Optional[str] = None):
def __init__(self, user: str | None = None, password: str | None = None):
if user and password:
try:
ret, msg = self.login(user, password)
@@ -36,9 +34,9 @@ class SieveClient:
if not ret:
raise SieveClientError(msg)
def login(self, user: str, password: str) -> Tuple[bool, Union[str, None]]:
def login(self, user: str, password: str) -> tuple[bool, str | None]:
conf = dict(param_tools.get_global_parameters("sievefilters"))
self.msc = managesieve.Client(conf["server"], conf["port"], debug=True)
self.msc = managesieve.Client(conf["server"], conf["port"], debug=False)
try:
ret = self.msc.connect(
user, password, starttls=conf["starttls"], authmech="OAUTHBEARER"
@@ -59,7 +57,7 @@ class SieveClient:
def listscripts(self):
return self.msc.listscripts()
def getscript(self, name: str, format: str = "raw") -> Union[FiltersSet, str, None]:
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())
+2 -3
View File
@@ -3,7 +3,6 @@ from email.mime.base import MIMEBase
import json
import os
from tempfile import NamedTemporaryFile
from typing import Optional
import uuid
import six
@@ -81,7 +80,7 @@ def save_attachment(request, session_uid: str, f) -> dict:
fp = NamedTemporaryFile(dir=get_storage_path(""), delete=False)
except Exception as e:
raise InternalError(str(e)) from None
if isinstance(f, (six.binary_type, six.text_type)):
if isinstance(f, six.binary_type | six.text_type):
fp.write(smart_bytes(f))
else:
for chunk in f.chunks():
@@ -100,7 +99,7 @@ def save_attachment(request, session_uid: str, f) -> dict:
return attachment
def remove_attachment(request, session_uid: str, name: str) -> Optional[str]:
def remove_attachment(request, session_uid: str, name: str) -> str | None:
manager = ComposeSessionManager(request.user.username)
session = manager.get_content(session_uid)
for att in session["attachments"]:
+10 -10
View File
@@ -5,7 +5,6 @@ import imaplib
import re
import ssl
import time
from typing import Optional
from django.conf import settings
from django.utils.translation import gettext as _
@@ -108,7 +107,7 @@ class BodyStructure:
self.attachments += [params]
def load_from_definition(
self, definition: list, multisubtype: Optional[str] = None
self, definition: list, multisubtype: str | None = None
) -> None:
for mp in definition:
if isinstance(mp, list):
@@ -148,11 +147,11 @@ class IMAPconnector:
unseen_pattern = re.compile(r"[^\(]+\(UNSEEN (\d+)\)")
def __init__(self, user: str, password: str, with_namespaces: bool = True) -> None:
self.__hdelimiter: Optional[str] = None
self.__hdelimiter: str | None = None
self.__ns_prefixes: dict = {}
self.quota_usage: int = -1
self.quota_limit: Optional[int] = None
self.quota_current: Optional[int] = None
self.quota_limit: int | None = None
self.quota_current: int | None = None
self.criterions: list = []
self.conf = dict(param_tools.get_global_parameters("webmail"))
self.address = self.conf["imap_server"]
@@ -170,7 +169,7 @@ class IMAPconnector:
def __exit__(self, *args):
self.logout()
def _cmd(self, name: str, *args, **kwargs) -> Optional[list]:
def _cmd(self, name: str, *args, **kwargs) -> list | None:
"""IMAP command wrapper.
To simplify errors handling, this wrapper calls the
@@ -240,7 +239,8 @@ class IMAPconnector:
except (OSError, imaplib.IMAP4.error, ssl.SSLError) as error:
raise ImapError(_(f"Connection to IMAP server failed: {error}")) from None
if not settings.WEBMAIL_DEV_MODE:
dev_mode = getattr(settings, "WEBMAIL_DEV_MODE", False)
if not dev_mode:
token = oauth2.build_oauthbearer_string(user, rawtoken)
data = self._cmd("AUTHENTICATE", b"OAUTHBEARER", token)
else:
@@ -600,7 +600,7 @@ class IMAPconnector:
self.select_mailbox(mbox, False)
self._cmd("EXPUNGE")
def create_folder(self, name: str, parent: Optional[str] = None) -> bool:
def create_folder(self, name: str, parent: str | None = None) -> bool:
if parent is not None:
name = f"{parent}{self.hdelimiter}{name}"
typ, data = self.m.create(self._encode_mbox_name(name))
@@ -675,7 +675,7 @@ class IMAPconnector:
return attdef, data[int(uid)][f"BODY[{partnum}]"]
def fetch(
self, start: int, stop: Optional[int] = None, mbox: Optional[str] = None
self, start: int, stop: int | None = None, mbox: str | None = None
) -> list:
"""Retrieve information about messages from the server.
@@ -748,7 +748,7 @@ class IMAPconnector:
return data[int(mailid)]
def separate_mailbox(fullname: str, sep: str = ".") -> tuple[str, Optional[str]]:
def separate_mailbox(fullname: str, sep: str = ".") -> tuple[str, str | None]:
"""Split a mailbox name.
If a separator is found in ``fullname``, this function returns the
+2 -5
View File
@@ -3,7 +3,6 @@ from email.mime.image import MIMEImage
import os
import pkg_resources
import smtplib
from typing import Optional, Tuple
from urllib.parse import unquote, urlparse
import lxml
@@ -20,7 +19,7 @@ from modoboa.webmail.lib.utils import html2plaintext
from . import get_imapconnector
def make_body_images_inline(body: str) -> Tuple[str, list]:
def make_body_images_inline(body: str) -> tuple[str, list]:
"""Look for images inside the body and make them inline.
Before sending a message in HTML format, it is necessary to find
@@ -117,9 +116,7 @@ def create_message(user: core_models.User, attributes: dict, attachments: list):
return msg
def send_mail(
request, attributes: dict, attachments: list
) -> Tuple[bool, Optional[str]]:
def send_mail(request, attributes: dict, attachments: list) -> tuple[bool, str | None]:
"""
Send a new email.