mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Fetch user mailboxes using IMAP
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user