diff --git a/doc/upgrade.rst b/doc/upgrade.rst index b711bc576..b14f2b8aa 100644 --- a/doc/upgrade.rst +++ b/doc/upgrade.rst @@ -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 diff --git a/frontend/src/api/webmail.js b/frontend/src/api/webmail.js index 249686b9a..067b42a2b 100644 --- a/frontend/src/api/webmail.js +++ b/frontend/src/api/webmail.js @@ -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}/`) diff --git a/frontend/src/components/contacts/ContactList.vue b/frontend/src/components/contacts/ContactList.vue index 9ec7f9150..544f3efa1 100644 --- a/frontend/src/components/contacts/ContactList.vue +++ b/frontend/src/components/contacts/ContactList.vue @@ -46,6 +46,12 @@ + diff --git a/frontend/src/components/webmail/AttachmentsDialog.vue b/frontend/src/components/webmail/AttachmentsDialog.vue index 01b033d0c..55457bbd0 100644 --- a/frontend/src/components/webmail/AttachmentsDialog.vue +++ b/frontend/src/components/webmail/AttachmentsDialog.vue @@ -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 }) diff --git a/frontend/src/components/webmail/ComposeEmailForm.vue b/frontend/src/components/webmail/ComposeEmailForm.vue index 73d74b29f..2324acca9 100644 --- a/frontend/src/components/webmail/ComposeEmailForm.vue +++ b/frontend/src/components/webmail/ComposeEmailForm.vue @@ -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) { diff --git a/frontend/src/layouts/admin/AdminLayout.vue b/frontend/src/layouts/admin/AdminLayout.vue index c03c9cd8f..1e7f1f922 100644 --- a/frontend/src/layouts/admin/AdminLayout.vue +++ b/frontend/src/layouts/admin/AdminLayout.vue @@ -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' } }) diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 7320bffea..2e84f77c0 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -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)), diff --git a/modoboa/admin/api/v1/serializers.py b/modoboa/admin/api/v1/serializers.py index a7b0932b3..9b2a22ed7 100644 --- a/modoboa/admin/api/v1/serializers.py +++ b/modoboa/admin/api/v1/serializers.py @@ -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 [] diff --git a/modoboa/admin/api/v2/serializers.py b/modoboa/admin/api/v2/serializers.py index b11fbda5d..f33c19e5f 100644 --- a/modoboa/admin/api/v2/serializers.py +++ b/modoboa/admin/api/v2/serializers.py @@ -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 = [] diff --git a/modoboa/admin/models/base.py b/modoboa/admin/models/base.py index 06b71029b..cd4d4ef5a 100644 --- a/modoboa/admin/models/base.py +++ b/modoboa/admin/models/base.py @@ -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 diff --git a/modoboa/autoreply/models.py b/modoboa/autoreply/models.py index 8a77cd753..10070bc7f 100644 --- a/modoboa/autoreply/models.py +++ b/modoboa/autoreply/models.py @@ -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: diff --git a/modoboa/contacts/importer/__init__.py b/modoboa/contacts/importer/__init__.py index 76b391812..6064ed4b7 100644 --- a/modoboa/contacts/importer/__init__.py +++ b/modoboa/contacts/importer/__init__.py @@ -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) diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py index 55551fc49..34f0baa7c 100644 --- a/modoboa/core/extensions.py +++ b/modoboa/core/extensions.py @@ -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 diff --git a/modoboa/core/models.py b/modoboa/core/models.py index 351fbb904..129e0f0d5 100644 --- a/modoboa/core/models.py +++ b/modoboa/core/models.py @@ -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 diff --git a/modoboa/imap_migration/__init__.py b/modoboa/imap_migration/__init__.py index 6b3d80114..e944787c5 100644 --- a/modoboa/imap_migration/__init__.py +++ b/modoboa/imap_migration/__init__.py @@ -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" diff --git a/modoboa/lib/exceptions.py b/modoboa/lib/exceptions.py index 1ddfb8ecd..2de2ae340 100644 --- a/modoboa/lib/exceptions.py +++ b/modoboa/lib/exceptions.py @@ -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: diff --git a/modoboa/limits/tests/test_user_limits.py b/modoboa/limits/tests/test_user_limits.py index 6d14df96c..ccd144059 100644 --- a/modoboa/limits/tests/test_user_limits.py +++ b/modoboa/limits/tests/test_user_limits.py @@ -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} diff --git a/modoboa/parameters/tools.py b/modoboa/parameters/tools.py index 44ebed708..6ad51bef1 100644 --- a/modoboa/parameters/tools.py +++ b/modoboa/parameters/tools.py @@ -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() diff --git a/modoboa/rspamd/admin.py b/modoboa/rspamd/admin.py index 13be29d96..846f6b406 100644 --- a/modoboa/rspamd/admin.py +++ b/modoboa/rspamd/admin.py @@ -1,6 +1 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.contrib import admin - # Register your models here. diff --git a/modoboa/sievefilters/__init__.py b/modoboa/sievefilters/__init__.py index a48e9b877..82d26c218 100644 --- a/modoboa/sievefilters/__init__.py +++ b/modoboa/sievefilters/__init__.py @@ -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" diff --git a/modoboa/sievefilters/api/v2/serializers.py b/modoboa/sievefilters/api/v2/serializers.py index b101b2749..2575fef81 100644 --- a/modoboa/sievefilters/api/v2/serializers.py +++ b/modoboa/sievefilters/api/v2/serializers.py @@ -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": diff --git a/modoboa/sievefilters/api/v2/vloaders.py b/modoboa/sievefilters/api/v2/vloaders.py index d8b9787b3..dffdb2aa4 100644 --- a/modoboa/sievefilters/api/v2/vloaders.py +++ b/modoboa/sievefilters/api/v2/vloaders.py @@ -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) diff --git a/modoboa/sievefilters/lib.py b/modoboa/sievefilters/lib.py index 3ee538cf8..fa12b776c 100644 --- a/modoboa/sievefilters/lib.py +++ b/modoboa/sievefilters/lib.py @@ -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()) diff --git a/modoboa/webmail/lib/attachments.py b/modoboa/webmail/lib/attachments.py index 53ca33e93..391139762 100644 --- a/modoboa/webmail/lib/attachments.py +++ b/modoboa/webmail/lib/attachments.py @@ -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"]: diff --git a/modoboa/webmail/lib/imaputils.py b/modoboa/webmail/lib/imaputils.py index 8b36a64b3..706e6ebc0 100644 --- a/modoboa/webmail/lib/imaputils.py +++ b/modoboa/webmail/lib/imaputils.py @@ -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 diff --git a/modoboa/webmail/lib/sendmail.py b/modoboa/webmail/lib/sendmail.py index a5b6b8cd9..bc178b38e 100644 --- a/modoboa/webmail/lib/sendmail.py +++ b/modoboa/webmail/lib/sendmail.py @@ -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.