Fetch user mailboxes using IMAP

This commit is contained in:
Antoine Nguyen
2024-06-22 19:44:49 +02:00
committed by Spitfireap
parent f527051ca2
commit 56833e2804
9 changed files with 128 additions and 76 deletions
+10 -1
View File
@@ -98,8 +98,17 @@
/>
<div class="mx-2 d-flex flex-column flex-grow-1">
<template v-for="arg in getActionArguments(action.name)">
<v-select
v-if="arg.type === 'list'"
v-model="action.args[arg.name]"
:items="arg.choices"
item-title="label"
density="compact"
variant="outlined"
:rules="[rules.required]"
/>
<v-text-field
v-if="arg.type === 'string' || arg.type === 'list'"
v-if="arg.type === 'string'"
v-model="action.args[arg.name]"
density="compact"
variant="outlined"
+19 -14
View File
@@ -155,7 +155,7 @@ const displayName = computed(() => {
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 =
+7 -2
View File
@@ -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()
+10
View File
@@ -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")
@@ -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):
+3 -1
View File
@@ -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)
+41
View File
@@ -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
+1 -1
View File
@@ -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",
+28 -57
View File
@@ -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<flags>.*?)\) "(?P<delimiter>.*)" "?(?P<name>[^"]*)"?'
@@ -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