diff --git a/frontend/src/components/account/FilterForm.vue b/frontend/src/components/account/FilterForm.vue
index 6a226e6b8..df7e05fa9 100644
--- a/frontend/src/components/account/FilterForm.vue
+++ b/frontend/src/components/account/FilterForm.vue
@@ -98,8 +98,17 @@
/>
+
{
const menuItems = computed(() => {
if (route.meta.layout === 'account') {
- return userSettingsMenuItems
+ return getUserSettingsMenuItems()
}
return mainMenuItems
})
@@ -256,19 +256,6 @@ const mainMenuItems = [
}
]
-const userSettingsMenuItems = [
- {
- text: $gettext('Filters'),
- to: { name: 'AccountFilters' },
- icon: 'mdi-filter'
- },
- {
- text: $gettext('Settings'),
- to: { name: 'AccountSettings' },
- icon: 'mdi-cog'
- }
-]
-
const userMenuItems = [
{
text: $gettext('Account'),
@@ -291,6 +278,24 @@ if (authUser.value.role !== 'SimpleUsers') {
})
}
+function getUserSettingsMenuItems() {
+ const result = []
+
+ if (authUser.value.mailbox) {
+ result.push({
+ text: $gettext('Filters'),
+ to: { name: 'AccountFilters' },
+ icon: 'mdi-filter'
+ })
+ }
+ result.push({
+ text: $gettext('Settings'),
+ to: { name: 'AccountSettings' },
+ icon: 'mdi-cog'
+ })
+ return result
+}
+
function displayMenuItem(item) {
if (isAuthenticated.value) {
const condition =
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
index a322c73c8..5b6a263f2 100644
--- a/frontend/src/router/index.js
+++ b/frontend/src/router/index.js
@@ -263,6 +263,7 @@ const routes = [
meta: {
layout: 'account',
requiresAuth: true,
+ requiresMailbox: true,
},
},
{
@@ -293,14 +294,18 @@ router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
authStore.initialize().then(() => {
authStore.validateAccess()
- /*
+ if (to.meta.requiresMailbox && !authStore.authUser.mailbox) {
+ next(false)
+ } else {
+ /*
if (to.meta.allowedRoles !== undefined) {
if (to.meta.allowedRoles.indexOf(authStore.authUser.role) === -1) {
next({ name: 'Dashboard' })
return
}
}*/
- next()
+ next()
+ }
})
} else {
next()
diff --git a/modoboa/lib/viewsets.py b/modoboa/lib/viewsets.py
index b97fd9a1d..94b3731bc 100644
--- a/modoboa/lib/viewsets.py
+++ b/modoboa/lib/viewsets.py
@@ -1,5 +1,6 @@
"""ViewSet related mixins and tools."""
+from rest_framework import permissions
from reversion.views import RevisionMixin
@@ -9,3 +10,12 @@ class RevisionModelMixin(RevisionMixin):
"""
pass
+
+
+class HasMailbox(permissions.BasePermission):
+ """Require user to has a mailbox."""
+
+ message = "User has no mailbox."
+
+ def has_permission(self, request, view):
+ return hasattr(request.user, "mailbox")
diff --git a/modoboa/sievefilters/api/v2/serializers.py b/modoboa/sievefilters/api/v2/serializers.py
index f492959d1..8091da1d6 100644
--- a/modoboa/sievefilters/api/v2/serializers.py
+++ b/modoboa/sievefilters/api/v2/serializers.py
@@ -10,6 +10,7 @@ from django.utils.translation import gettext as _
from rest_framework import serializers
from modoboa.sievefilters import constants, lib
+from modoboa.sievefilters.api.v2 import vloaders
class FilterSetSerializer(serializers.Serializer):
@@ -152,6 +153,14 @@ class ActionArgumentSerializer(serializers.Serializer):
type = serializers.CharField()
label = serializers.CharField(required=False)
value = serializers.CharField(required=False)
+ choices = serializers.SerializerMethodField(required=False)
+
+ def get_choices(self, obj):
+ print(obj)
+ if "vloader" not in obj:
+ return None
+ loader = getattr(vloaders, obj["vloader"])
+ return loader(self.context["request"])
class ActionTemplateSerializer(serializers.Serializer):
diff --git a/modoboa/sievefilters/api/v2/viewsets.py b/modoboa/sievefilters/api/v2/viewsets.py
index e3cdc60ae..db084ab6d 100644
--- a/modoboa/sievefilters/api/v2/viewsets.py
+++ b/modoboa/sievefilters/api/v2/viewsets.py
@@ -15,6 +15,7 @@ from rest_framework import response, viewsets
from rest_framework.decorators import action
from modoboa.lib.connections import ConnectionError
+from modoboa.lib.viewsets import HasMailbox
from modoboa.sievefilters import constants
from modoboa.sievefilters.lib import SieveClient, SieveClientError
from modoboa.sievefilters.rfc6266 import build_header
@@ -31,6 +32,7 @@ FILTER_SET_ID = OpenApiParameter(
class FilterSetViewSet(viewsets.ViewSet):
+ permission_classes = (HasMailbox,)
serializer_class = serializers.FilterSetSerializer
def get_sieve_client(self, request):
@@ -142,7 +144,7 @@ class FilterSetViewSet(viewsets.ViewSet):
@action(methods=["get"], detail=False)
def action_templates(self, request):
serializer = serializers.ActionTemplateSerializer(
- constants.ACTION_TEMPLATES, many=True
+ constants.ACTION_TEMPLATES, many=True, context={"request": request}
)
return response.Response(serializer.data)
diff --git a/modoboa/sievefilters/api/v2/vloaders.py b/modoboa/sievefilters/api/v2/vloaders.py
new file mode 100644
index 000000000..72914a5f3
--- /dev/null
+++ b/modoboa/sievefilters/api/v2/vloaders.py
@@ -0,0 +1,41 @@
+"""Value loading functions."""
+
+from typing import List, Optional, TypedDict
+
+from modoboa.sievefilters import imaputils
+
+
+class UserMailbox(TypedDict):
+ """Type definition for user mailbox."""
+
+ name: str
+ label: str
+
+
+def __build_folders_list(
+ folders: dict, user, imapc: imaputils.IMAPconnector, parentmb: Optional[str] = None
+) -> List[UserMailbox]:
+ ret: List[UserMailbox] = []
+ for fd in folders:
+ value = fd["path"] if "path" in fd else fd["name"]
+ if parentmb:
+ ret += [
+ {
+ "name": value,
+ "label": fd["name"].replace(f"{parentmb}{imapc.hdelimiter}", ""),
+ }
+ ]
+ else:
+ ret += [{"name": value, "label": fd["name"]}]
+ if "sub" in fd:
+ submboxes = imapc.getmboxes(user, value)
+ ret += __build_folders_list(submboxes, user, imapc, value)
+ return ret
+
+
+def user_mailboxes(request) -> List[UserMailbox]:
+ """Retrieve list of available mailboxes for given user."""
+ mbc = imaputils.get_imapconnector(request)
+ ret = mbc.getmboxes(request.user)
+ folders = __build_folders_list(ret, request.user, mbc)
+ return folders
diff --git a/modoboa/sievefilters/constants.py b/modoboa/sievefilters/constants.py
index e7f160b23..86e5533ae 100644
--- a/modoboa/sievefilters/constants.py
+++ b/modoboa/sievefilters/constants.py
@@ -39,7 +39,7 @@ ACTION_TEMPLATES = [
"name": "fileinto",
"label": _("Move message to"),
"args": [
- {"name": "mailbox", "type": "list", "vloader": "userfolders"},
+ {"name": "mailbox", "type": "list", "vloader": "user_mailboxes"},
{
"name": "copy",
"type": "boolean",
diff --git a/modoboa/sievefilters/imaputils.py b/modoboa/sievefilters/imaputils.py
index a6d6b20ce..a8ebf8964 100644
--- a/modoboa/sievefilters/imaputils.py
+++ b/modoboa/sievefilters/imaputils.py
@@ -1,17 +1,12 @@
-# coding: utf-8
-"""
-:mod:`imaputils` --- Extra IMAPv4 utilities
--------------------------------------------
-"""
+"""IMAPv4 utilities."""
+import base64
import imaplib
import re
from operator import itemgetter
import socket
import ssl
-import six
-
from django.utils.encoding import smart_bytes
from django.utils.translation import gettext as _
@@ -30,8 +25,7 @@ class ImapError(ModoboaException):
return str(self.reason)
-@six.add_metaclass(ConnectionsManager)
-class IMAPconnector(object):
+class IMAPconnector:
"""The IMAPv4 connector."""
list_base_pattern = r'\((?P.*?)\) "(?P.*)" "?(?P[^"]*)"?'
@@ -51,7 +45,8 @@ class IMAPconnector(object):
self.login(user, password)
def _cmd(self, name, *args, **kwargs):
- """IMAP command wrapper
+ """
+ IMAP command wrapper.
To simplify errors handling, this wrapper calls the
appropriate method (``uid`` or FIXME) and then check the
@@ -98,31 +93,12 @@ class IMAPconnector(object):
self.__hdelimiter = m.group("delimiter")
return self.__hdelimiter
- def refresh(self, user, password):
- """Check if current connection needs a refresh
-
- Is it really secure?
+ def login(self, user: str, passwd: str):
"""
- if self.m is not None:
- try:
- self._cmd("NOOP")
- except ImapError:
- if hasattr(self, "current_mailbox"):
- del self.current_mailbox
- else:
- return
+ Custom login method.
- self.login(user, password)
-
- def login(self, user, passwd):
- """Custom login method
-
- We connect to the server, issue a LOGIN command. If
- successfull, we try to record a eventuel CAPABILITY untagged
- response. Otherwise, we issue the command.
-
- :param user: username
- :param passwd: password
+ Only OAUTHBEARER mechanism is supported here. passwd parameter is actually a
+ Bearer token.
"""
try:
if self.conf["imap_secured"]:
@@ -132,8 +108,15 @@ class IMAPconnector(object):
except (socket.error, imaplib.IMAP4.error, ssl.SSLError) as error:
raise ImapError(_("Connection to IMAP server failed: %s" % error))
- passwd = self.m._quote(passwd)
- data = self._cmd("LOGIN", smart_bytes(user), smart_bytes(passwd))
+ token = (
+ b"n,a="
+ + user.encode("utf-8")
+ + b",\001auth=Bearer "
+ + passwd.encode("utf-8")
+ + b"\001\001"
+ )
+ token = base64.b64encode(token)
+ data = self._cmd("AUTHENTICATE", b"OAUTHBEARER", token)
self.m.state = "AUTH"
if "CAPABILITY" in self.m.untagged_responses:
self.capabilities = (
@@ -191,8 +174,6 @@ class IMAPconnector(object):
descr = {"name": name}
newmboxes += [descr]
- if "\\Marked" in flags or "\\UnMarked" not in flags:
- descr["send_status"] = True
if "\\HasChildren" in flags:
if "\\NonExistent" in flags:
descr["removed"] = True
@@ -201,7 +182,7 @@ class IMAPconnector(object):
mailboxes += sorted(newmboxes, key=itemgetter("name"))
- def getmboxes(self, user, topmailbox=""):
+ def getmboxes(self, user, topmailbox: str = ""):
"""Returns a list of mailboxes for a particular user
By default, only the first level of mailboxes under
@@ -215,32 +196,22 @@ class IMAPconnector(object):
md_mailboxes = []
else:
md_mailboxes = [
- {"name": "INBOX", "class": "fa fa-inbox"},
- {
- "name": user.parameters.get_value("drafts_folder"),
- "class": "fa fa-file",
- },
- {"name": "Junk", "class": "fa fa-fire"},
- {
- "name": user.parameters.get_value("sent_folder"),
- "class": "fa fa-envelope",
- },
- {
- "name": user.parameters.get_value("trash_folder"),
- "class": "fa fa-trash",
- },
+ {"name": "INBOX"},
+ {"name": user.parameters.get_value("drafts_folder")},
+ {"name": "Junk"},
+ {"name": user.parameters.get_value("sent_folder")},
+ {"name": user.parameters.get_value("trash_folder")},
]
self._listmboxes(topmailbox, md_mailboxes)
return md_mailboxes
-def get_imapconnector(request):
- """Simple shortcut to create a connector
+def get_imapconnector(request) -> IMAPconnector:
+ """
+ Simple shortcut to create a connector
:param request: a ``Request`` object
"""
- imapc = IMAPconnector(
- user=request.user.username, password=request.session["password"]
- )
+ imapc = IMAPconnector(user=request.user.username, password=str(request.auth))
return imapc