Various improvements

This commit is contained in:
Antoine Nguyen
2025-05-08 16:40:31 +02:00
parent 2dff99d6a0
commit dd362462ce
22 changed files with 340 additions and 165 deletions
+7 -5
View File
@@ -12,6 +12,10 @@ export default {
const params = { mailbox }
return repository.get('/webmail/mailboxes/quota/', { params })
},
getUserMailboxUnseen(mailbox) {
const params = { mailbox }
return repository.get('webmail/mailboxes/unseen/', { params })
},
createUserMailbox(body) {
return repository.post('/webmail/mailboxes/', body)
},
@@ -93,17 +97,15 @@ export default {
sendEmail(body) {
return repository.post('/webmail/emails/send/', body)
},
getComposeSession(uid) {
return repository.get(`/webmail/compose-sessions/${uid}/`)
},
createComposeSession() {
return repository.post('/webmail/compose-sessions/')
},
getAllowedSenders() {
return repository.get('/webmail/compose-sessions/allowed_senders/')
},
getUploadedAttachments(sessionUid) {
return repository.get(
`/webmail/compose-sessions/${sessionUid}/attachments/`
)
},
uploadAttachment(sessionUid, body) {
return repository.post(
`/webmail/compose-sessions/${sessionUid}/attachments/`,
@@ -50,6 +50,24 @@
density="compact"
variant="outlined"
/>
<v-textarea
v-else-if="param.widget === 'TextareaField'"
v-model="parameters[param.name]"
:hint="param.help_text"
persistent-hint
:error="formErrors[param.name] !== undefined"
:error-messages="formErrors[param.name]"
:type="
param.widget === 'PasswordField' ? 'password' : 'text'
"
density="compact"
variant="outlined"
/>
<HtmlEditor
v-else-if="param.widget === 'HTMLField'"
v-model="parameters[param.name]"
>
</HtmlEditor>
<v-text-field
v-else
v-model="parameters[param.name]"
@@ -86,6 +104,7 @@
import { computed, ref, watch } from 'vue'
import { useGettext } from 'vue3-gettext'
import { useBusStore } from '@/stores'
import HtmlEditor from '@/components/tools/HtmlEditor'
const props = defineProps({
app: {
+15 -1
View File
@@ -31,9 +31,16 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, watch } from 'vue'
import HtmlEditor from '@/components/tools/HtmlEditor'
const props = defineProps({
editorMode: {
type: String,
default: null,
},
})
const model = defineModel()
const emit = defineEmits(['onToggleHtmlMode'])
@@ -44,4 +51,11 @@ const toggleHtmlMode = () => {
htmlMode.value = !htmlMode.value
emit('onToggleHtmlMode', htmlMode.value)
}
watch(
() => props.editorMode,
(value) => {
htmlMode.value = value === 'html'
}
)
</script>
@@ -148,6 +148,7 @@
</div>
<BodyEditor
v-model="form.body"
:editor-mode="editorMode"
@on-toggle-html-mode="(value) => emit('onToggleHtmlMode', value)"
/>
</v-form>
@@ -161,7 +162,7 @@
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useGettext } from 'vue3-gettext'
import { useAuthStore, useBusStore } from '@/stores'
@@ -193,6 +194,7 @@ const authStore = useAuthStore()
const allowedSenders = ref([])
const attachmentCount = ref(0)
const contacts = ref([])
const editorMode = ref('plain')
const form = ref({})
const formRef = ref()
const showAttachmentsDialog = ref(false)
@@ -293,6 +295,17 @@ const lookForContacts = debounce(async (search) => {
}
}, 500)
const initialize = (body) => {
if (body.signature) {
if (form.value.body) {
form.value.body += body.signature
} else {
form.value.body = body.signature
}
}
editorMode.value = body.editor_format
}
watch(
() => props.originalEmail,
() => {
@@ -305,10 +318,12 @@ if (!route.query.uid) {
api.createComposeSession().then((resp) => {
const query = { ...route.query, uid: resp.data.uid }
router.push({ name: route.name, query })
initialize(resp.data)
})
} else {
api.getUploadedAttachments(route.query.uid).then((resp) => {
attachmentCount.value = resp.data.length
api.getComposeSession(route.query.uid).then((resp) => {
attachmentCount.value = resp.data.attachments.length
initialize(resp.data)
})
}
+23 -4
View File
@@ -170,7 +170,7 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useGettext } from 'vue3-gettext'
import { useBusStore } from '@/stores'
@@ -184,7 +184,7 @@ const props = defineProps({
})
const { $gettext } = useGettext()
const { displayNotification } = useBusStore()
const { displayNotification, reloadMailboxCounters } = useBusStore()
const router = useRouter()
const route = useRoute()
@@ -196,12 +196,15 @@ const selectAll = ref(false)
const selection = ref([])
const working = ref(false)
let intervalId = null
const openEmail = (emailid) => {
router.push({
name: 'EmailView',
query: { mailbox: props.mailbox, mailid: emailid },
})
}
const emit = defineEmits(['reloadContent'])
const fetchEmails = () => {
emails.value = {}
@@ -217,6 +220,11 @@ const fetchEmails = () => {
})
}
const autoRefreshContent = () => {
fetchEmails()
emit('refreshContent')
}
const submitSearch = () => {
if (search.value) {
fetchEmails()
@@ -240,6 +248,7 @@ const deleteSelection = () => {
working.value = false
displayNotification({ msg: $gettext('Message(s) deleted') })
fetchEmails()
reloadMailboxCounters()
})
}
@@ -251,7 +260,7 @@ const markSelectionAsJunk = () => {
api.markSelectionAsJunk(route.query.mailbox, selection.value).then(() => {
working.value = false
displayNotification({ msg: $gettext('Message(s) marked as junk') })
fetchEmails()
autoRefreshContent()
})
}
@@ -263,7 +272,7 @@ const markSelectionAsNotJunk = () => {
api.markSelectionAsNotJunk(route.query.mailbox, selection.value).then(() => {
working.value = false
displayNotification({ msg: $gettext('Message(s) marked as not junk') })
fetchEmails()
autoRefreshContent()
})
}
@@ -277,6 +286,7 @@ const flagSelection = (status) => {
selection.value = []
displayNotification({ msg: $gettext('Message(s) flagged') })
fetchEmails()
reloadMailboxCounters()
})
}
@@ -285,6 +295,7 @@ const emptyMailbox = () => {
api.emptyUserMailbox(route.query.mailbox).then(() => {
emails.value = {}
loading.value = false
reloadMailboxCounters()
})
}
@@ -294,6 +305,14 @@ const toggleFollowState = async (email) => {
email.flagged = flag === 'flagged'
}
onMounted(() => {
intervalId = setInterval(autoRefreshContent, 300 * 1000)
})
onUnmounted(() => {
clearInterval(intervalId)
})
watch(
() => props.mailbox,
() => {
@@ -15,7 +15,15 @@
>
<v-icon :icon="iconByMailboxType[mailbox.type]" class="mr-4" />
<template v-if="props.unseenCounters && mailbox.unseen > 0">
<span class="font-weight-bold">
<span
v-if="mailbox.name === route.query.mailbox"
class="font-weight-bold"
>
{{ getMailboxLabel(mailbox) }} ({{
getCurrentMailboxUnseen(mailbox)
}})
</span>
<span v-else class="font-weight-bold">
{{ getMailboxLabel(mailbox) }} ({{ mailbox.unseen }})
</span>
</template>
@@ -45,7 +53,9 @@
</template>
<script setup>
import { ref } from 'vue'
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import { useBusStore } from '@/stores'
import api from '@/api/webmail'
const props = defineProps({
@@ -73,6 +83,9 @@ const props = defineProps({
const model = defineModel()
const emit = defineEmits(['update:modelValue'])
const route = useRoute()
const busStore = useBusStore()
const iconByMailboxType = {
inbox: 'mdi-inbox',
draft: 'mdi-file',
@@ -82,6 +95,7 @@ const iconByMailboxType = {
normal: 'mdi-folder-outline',
}
const currentMailboxUnseen = ref(null)
const hoverStates = ref({})
const mailboxStates = ref({})
@@ -89,6 +103,13 @@ function getMailboxLabel(mailbox) {
return mailbox.label.split('/').pop()
}
function getCurrentMailboxUnseen(mailbox) {
if (currentMailboxUnseen.value === null) {
currentMailboxUnseen.value = mailbox.unseen
}
return currentMailboxUnseen.value
}
function setHover(mailbox, value) {
hoverStates.value[mailbox.name] = value
}
@@ -128,6 +149,15 @@ function updateSelection(value) {
emit('update:modelValue', value)
}
}
watch(
() => busStore.mbCounterKey,
() => {
api.getUserMailboxUnseen(route.query.mailbox).then((resp) => {
currentMailboxUnseen.value = resp.data.counter
})
}
)
</script>
<style scoped lang="scss">
@@ -20,6 +20,7 @@ const userSettingsMenuItems = computed(() => {
// Temporary solution...
const appIcons = {
contacts: 'mdi-contacts-outline',
webmail: 'mdi-at',
}
result.push({
+19 -11
View File
@@ -95,7 +95,7 @@
</template>
<script setup>
import { computed, ref } from 'vue'
import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useGettext } from 'vue3-gettext'
import { useBusStore } from '@/stores'
@@ -108,7 +108,7 @@ import api from '@/api/webmail'
const { $gettext } = useGettext()
const route = useRoute()
const router = useRouter()
const { displayNotification } = useBusStore()
const busStore = useBusStore()
const confirm = ref()
const drawer = ref(true)
@@ -149,10 +149,12 @@ function openMailbox(mailbox) {
})
}
api.getUserMailboxes().then((resp) => {
userMailboxes.value = resp.data.mailboxes
hdelimiter.value = resp.data.hdelimiter
})
const fetchUserMailboxes = () => {
api.getUserMailboxes().then((resp) => {
userMailboxes.value = resp.data.mailboxes
hdelimiter.value = resp.data.hdelimiter
})
}
const openComposeForm = () => {
router.push({ name: 'ComposeEmailView' })
@@ -161,9 +163,7 @@ const openComposeForm = () => {
const closeMailboxForm = () => {
showMailboxForm.value = false
editedMailbox.value = null
api.getUserMailboxes().then((resp) => {
userMailboxes.value = resp.data.mailboxes
})
fetchUserMailboxes()
}
const openMailboxForm = () => {
@@ -185,7 +185,7 @@ const navigateToMailbox = (mailbox) => {
const compressMailbox = async () => {
await api.compressUserMailbox({ name: selectedMailbox.value })
displayNotification({ msg: $gettext('Mailbox compressed') })
busStore.displayNotification({ msg: $gettext('Mailbox compressed') })
}
const deleteMailbox = async () => {
@@ -202,10 +202,18 @@ const deleteMailbox = async () => {
return
}
await api.deleteUserMailbox({ name: selectedMailbox.value })
displayNotification({ msg: $gettext('Mailbox deleted') })
busStore.displayNotification({ msg: $gettext('Mailbox deleted') })
navigateToMailbox('INBOX')
}
watch(
() => busStore.dataKey,
() => {
fetchUserMailboxes()
}
)
fetchUserMailboxes()
api.getUserMailboxQuota(route.query.mailbox || 'INBOX').then((resp) => {
mailboxQuota.value = resp.data
})
+16
View File
@@ -5,20 +5,36 @@ import { ref } from 'vue'
export const useBusStore = defineStore('bus', () => {
const notification = ref({})
const notificationColor = ref('')
const dataKey = ref(1)
const mbCounterKey = ref(1)
async function $reset() {
notification.value = {}
notificationColor.value = ''
dataKey.value = 1
mbCounterKey.value = 1
}
function displayNotification(options) {
notification.value = options.msg
notificationColor.value = options.type ? options.type : 'success'
}
function reloadData() {
dataKey.value++
}
function reloadMailboxCounters() {
mbCounterKey.value++
}
return {
notification,
notificationColor,
displayNotification,
reloadMailboxCounters,
dataKey,
mbCounterKey,
$reset,
}
})
+2 -18
View File
@@ -167,7 +167,7 @@ import api from '@/api/webmail'
import ContactCard from '@/components/webmail/ContactCard.vue'
const { $gettext } = useGettext()
const { displayNotification } = useBusStore()
const { displayNotification, reloadMailboxCounters } = useBusStore()
const route = useRoute()
const router = useRouter()
@@ -215,6 +215,7 @@ const fetchMailContent = () => {
api
.getEmailContent(route.query.mailbox, route.query.mailid, options)
.then((resp) => {
reloadMailboxCounters()
email.value = resp.data
nextTick(() => {
const iframe = document.createElement('iframe')
@@ -293,23 +294,6 @@ const openEmailSourceDialog = async () => {
showEmailSource.value = true
}
const storeContactId = (address, contact) => {
console.log(address, contact)
for (const rcpt of email.value.to) {
if (rcpt.address === address.address) {
rcpt.contact_id = contact.id
return
}
}
if (!email.value.cc) return
for (const rcpt of email.value.cc) {
if (rcpt.address === address.address) {
rcpt.contact_id = contact.id
return
}
}
}
const replyToEmail = (all) => {
const query = { ...route.query }
if (all) {
+5 -1
View File
@@ -71,6 +71,8 @@ class Registry:
for name, field in list(serializer.fields.items()):
if field.default is not empty:
data["defaults"][name] = field.default
else:
data["defaults"][name] = None
def get_applications(self, level):
"""Return all applications registered for level."""
@@ -144,7 +146,9 @@ class Registry:
if config.get("password"):
data["widget"] = "PasswordField"
else:
data["widget"] = field.__class__.__name__
data["widget"] = config.get(
"widget", field.__class__.__name__
)
if data["widget"] == "ChoiceField":
data["choices"] = [
{"text": value, "value": key}
+3 -3
View File
@@ -35,7 +35,7 @@ def __build_folders_list(
def user_mailboxes(request) -> List[UserMailbox]:
"""Retrieve list of available mailboxes for given user."""
mbc = imaputils.get_imapconnector(request, load_namespaces=False)
ret = mbc.getmboxes(request.user)
folders = __build_folders_list(ret, request.user, mbc)
with imaputils.get_imapconnector(request, load_namespaces=False) as mbc:
ret = mbc.getmboxes(request.user)
folders = __build_folders_list(ret, request.user, mbc)
return folders
+4 -4
View File
@@ -131,6 +131,7 @@ USER_PREFERENCES_STRUCT = collections.OrderedDict(
"enable_links",
{
"label": gettext_lazy("Enable HTML links display"),
"display": "displaymode=html",
"help_text": gettext_lazy(
"Enable/Disable HTML links display"
),
@@ -150,11 +151,9 @@ USER_PREFERENCES_STRUCT = collections.OrderedDict(
(
"refresh_interval",
{
"label": gettext_lazy(
"Number of displayed emails per page"
),
"label": gettext_lazy("Listing refresh rate"),
"help_text": gettext_lazy(
"Sets the maximum number of messages displayed in a page"
"Automatic listing refresh rate (in secconds)"
),
},
),
@@ -237,6 +236,7 @@ USER_PREFERENCES_STRUCT = collections.OrderedDict(
"help_text": gettext_lazy(
"User defined email signature"
),
"widget": "HTMLField",
},
),
]
+4
View File
@@ -42,9 +42,13 @@ class ImapEmail(Email):
super().__init__(*args, **kwargs)
self.request = request
self.imapc = get_imapconnector(request)
self.imapc = self.imapc.__enter__()
self.mbox, self.mailid = self.mailid.split(":")
self.attachments: dict[str, str] = {}
def __del__(self):
self.imapc.__exit__()
def fetch_headers(self, raw_addresses: bool = False) -> None:
"""Fetch message headers from server."""
msg = self.imapc.fetchmail(
+13 -4
View File
@@ -18,7 +18,7 @@ from modoboa.parameters import tools as param_tools
from ..exceptions import ImapError, WebmailInternalError
from .fetch_parser import FetchResponseParser
imaplib.Debug = 4
# imaplib.Debug = 4
# workaround for the "got more than 10000 bytes" exception. MAXLINE
# value set to 1M, as on latest python versions.
@@ -147,7 +147,7 @@ class IMAPconnector:
)
unseen_pattern = re.compile(r"[^\(]+\(UNSEEN (\d+)\)")
def __init__(self, user=None, password=None, load_namespaces: bool = True) -> None:
def __init__(self, user: str, password: str, with_namespaces: bool = True) -> None:
self.__hdelimiter: Optional[str] = None
self.__ns_prefixes: dict = {}
self.quota_usage: int = -1
@@ -157,9 +157,18 @@ class IMAPconnector:
self.conf = dict(param_tools.get_global_parameters("webmail"))
self.address = self.conf["imap_server"]
self.port = self.conf["imap_port"]
self.login(user, password)
if load_namespaces:
self.user = user
self.password = password
self.with_namespaces = with_namespaces
def __enter__(self):
self.login(self.user, self.password)
if self.load_namespaces:
self.load_namespaces()
return self
def __exit__(self, *args):
self.logout()
def _cmd(self, name: str, *args, **kwargs) -> Optional[list]:
"""IMAP command wrapper.
+3 -22
View File
@@ -15,31 +15,11 @@ from django.core.mail import EmailMessage, EmailMultiAlternatives
from modoboa.core import models as core_models
from modoboa.parameters import tools as param_tools
from modoboa.webmail.lib.attachments import create_mail_attachment
from modoboa.webmail.lib.utils import html2plaintext
from . import get_imapconnector
def html2plaintext(content: str) -> str:
"""HTML to plain text translation.
:param content: some HTML content
"""
if not content:
return ""
html = lxml.html.fromstring(content)
plaintext = ""
for ch in html.iter():
p = None
if ch.text is not None:
p = ch.text.strip("\r\t\n")
if ch.tag == "img":
p = ch.get("alt")
if p is None:
continue
plaintext += p + "\n"
return plaintext
def make_body_images_inline(body: str) -> Tuple[str, list]:
"""Look for images inside the body and make them inline.
@@ -183,5 +163,6 @@ def send_mail(
# Copy message to sent folder
sentfolder = request.user.parameters.get_value("sent_folder")
get_imapconnector(request).push_mail(sentfolder, msg.message())
with get_imapconnector(request) as imapc:
imapc.push_mail(sentfolder, msg.message())
return True, None
+4 -2
View File
@@ -1,5 +1,7 @@
"""Tools to deal with message signatures."""
from modoboa.webmail.lib.utils import html2plaintext
class EmailSignature:
"""User signature.
@@ -11,13 +13,13 @@ class EmailSignature:
self._sig = ""
dformat = user.parameters.get_value("editor")
content = user.parameters.get_value("signature")
if len(content):
if content and len(content):
getattr(self, f"_format_sig_{dformat}")(content)
def _format_sig_plain(self, content):
self._sig = f"""
---
{content}"""
{html2plaintext(content)}"""
def _format_sig_html(self, content):
content = f"---<br>{content}"
+23
View File
@@ -1,5 +1,28 @@
"""Misc. utilities."""
import lxml
def html2plaintext(content: str) -> str:
"""HTML to plain text translation.
:param content: some HTML content
"""
if not content:
return ""
html = lxml.html.fromstring(content)
plaintext = ""
for ch in html.iter():
p = None
if ch.text is not None:
p = ch.text.strip("\r\t\n")
if ch.tag == "img":
p = ch.get("alt")
if p is None:
continue
plaintext += p + "\n"
return plaintext
def decode_payload(encoding, payload):
"""Decode the payload according to the given encoding
+1 -1
View File
@@ -14,7 +14,7 @@ class IMAP4Mock:
def _simple_command(self, name, *args, **kwargs):
if name == "CAPABILITY":
self.untagged_responses["CAPABILITY"] = [b""]
self.untagged_responses["CAPABILITY"] = [b"QUOTA"]
elif name == "LIST":
self.untagged_responses["LIST"] = [b'() "." "INBOX"']
elif name == "NAMESPACE":
+16 -1
View File
@@ -4,7 +4,7 @@ from rest_framework import serializers
from modoboa.lib import email_utils
from modoboa.webmail import constants
from modoboa.webmail.lib import imapheader
from modoboa.webmail.lib import imapheader, signature
class GlobalParametersSerializer(serializers.Serializer):
@@ -41,6 +41,7 @@ class UserPreferencesSerializer(serializers.Serializer):
default=constants.DisplayMode.PLAIN.value, choices=constants.DISPLAY_MODES
)
signature = serializers.CharField(required=False)
signature = serializers.CharField(required=False)
class UserMailboxSerializer(serializers.Serializer):
@@ -66,6 +67,11 @@ class UserMailboxQuotaSerializer(serializers.Serializer):
limit = serializers.IntegerField(source="quota_limit")
class UserMailboxUnseenSerializer(serializers.Serializer):
counter = serializers.IntegerField()
class UserMailboxesSerializer(serializers.Serializer):
mailboxes = UserMailboxSerializer(many=True)
@@ -211,7 +217,16 @@ class SendEmailSerializer(serializers.Serializer):
class ComposeSessionSerializer(serializers.Serializer):
attachments = UploadedAttachmentSerializer(many=True, required=False)
uid = serializers.CharField()
signature = serializers.SerializerMethodField()
editor_format = serializers.SerializerMethodField()
def get_editor_format(self, obj):
return self.context["request"].user.parameters.get_value("editor")
def get_signature(self, obj):
return str(signature.EmailSignature(self.context["request"].user))
class AllowedSenderSerializer(serializers.Serializer):
+17 -3
View File
@@ -90,6 +90,15 @@ class UserMailboxViewSetTestCase(WebmailTestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["usage"], -1)
def test_unseen(self):
self.authenticate()
url = reverse("v2:webmail-mailbox-unseen")
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
response = self.client.get(f"{url}?mailbox=INBOX")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["counter"], 10)
def test_create(self):
self.authenticate()
url = reverse("v2:webmail-mailbox-list")
@@ -247,6 +256,13 @@ class ComposeSessionViewSetTestCase(WebmailTestCase):
self.authenticate()
self._create_compose_session()
def test_get(self):
self.authenticate()
uid = self._create_compose_session()
url = reverse("v2:webmail-compose-session-detail", args=[uid])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_get_allowed_senders(self):
self.authenticate()
url = reverse("v2:webmail-compose-session-allowed-senders")
@@ -257,12 +273,10 @@ class ComposeSessionViewSetTestCase(WebmailTestCase):
def test_attachments(self):
self.authenticate()
uid = self._create_compose_session()
url = reverse("v2:webmail-compose-session-attachments", args=[uid])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
manager = ComposeSessionManager(self.user.username)
self.set_global_parameters({"max_attachment_size": "10"})
url = reverse("v2:webmail-compose-session-attachments", args=[uid])
with self.settings(MEDIA_ROOT=self.workdir):
response = self.client.post(url, {"attachment": get_gif()})
self.assertEqual(response.status_code, 400)
+95 -80
View File
@@ -22,6 +22,8 @@ class UserMailboxViewSet(viewsets.GenericViewSet):
def get_serializer_class(self):
if self.action == "quota":
return serializers.UserMailboxQuotaSerializer
if self.action == "unseen":
return serializers.UserMailboxUnseenSerializer
if self.action in ["create", "compress", "empty", "delete"]:
return serializers.UserMailboxInputSerializer
if self.action == "rename":
@@ -30,8 +32,8 @@ class UserMailboxViewSet(viewsets.GenericViewSet):
def list(self, request):
parent_mailbox = request.GET.get("mailbox")
imapc = lib.get_imapconnector(request)
mboxes = imapc.getmboxes(request.user, parent_mailbox)
with lib.get_imapconnector(request) as imapc:
mboxes = imapc.getmboxes(request.user, parent_mailbox)
serializer = self.get_serializer(
{"mailboxes": mboxes, "hdelimiter": imapc.hdelimiter}
)
@@ -42,42 +44,56 @@ class UserMailboxViewSet(viewsets.GenericViewSet):
mailbox = request.GET.get("mailbox")
if mailbox is None:
raise Http404
imapc = lib.get_imapconnector(request)
imapc.getquota(mailbox)
serializer = self.get_serializer(imapc)
with lib.get_imapconnector(request) as imapc:
imapc.getquota(mailbox)
serializer = self.get_serializer(imapc)
return response.Response(serializer.data)
@action(methods=["get"], detail=False)
def unseen(self, request):
"""Get unseen messages counter for given mailbox."""
mailbox = request.GET.get("mailbox")
if mailbox is None:
raise Http404
with lib.get_imapconnector(request) as imapc:
serializer = self.get_serializer(
{"counter": imapc.unseen_messages(mailbox)}
)
return response.Response(serializer.data)
def create(self, request):
serializer = serializers.UserMailboxInputSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
imapc = lib.get_imapconnector(request)
imapc.create_folder(
serializer.validated_data["name"],
serializer.validated_data.get("parent_mailbox"),
)
with lib.get_imapconnector(request) as imapc:
imapc.create_folder(
serializer.validated_data["name"],
serializer.validated_data.get("parent_mailbox"),
)
return response.Response(serializer.validated_data, status=201)
@action(methods=["post"], detail=False)
def rename(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
imapc = lib.get_imapconnector(request)
oldname, oldparent = lib.separate_mailbox(
serializer.validated_data["oldname"], sep=imapc.hdelimiter
)
name = serializer.validated_data["name"]
parent = serializer.validated_data.get("parent_mailbox")
if name != oldname or parent != oldparent:
newname = name if parent is None else imapc.hdelimiter.join([parent, name])
imapc.rename_folder(oldname, newname)
with lib.get_imapconnector(request) as imapc:
oldname, oldparent = lib.separate_mailbox(
serializer.validated_data["oldname"], sep=imapc.hdelimiter
)
name = serializer.validated_data["name"]
parent = serializer.validated_data.get("parent_mailbox")
if name != oldname or parent != oldparent:
newname = (
name if parent is None else imapc.hdelimiter.join([parent, name])
)
imapc.rename_folder(oldname, newname)
return response.Response(serializer.validated_data)
@action(methods=["post"], detail=False)
def compress(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
imapc = lib.get_imapconnector(request)
imapc.compact(serializer.validated_data["name"])
with lib.get_imapconnector(request) as imapc:
imapc.compact(serializer.validated_data["name"])
return response.Response(status=204)
@action(methods=["post"], detail=False)
@@ -87,16 +103,16 @@ class UserMailboxViewSet(viewsets.GenericViewSet):
mailbox = serializer.validated_data["name"]
if mailbox != request.user.parameters.get_value("trash_folder"):
raise Http404
imapc = lib.get_imapconnector(request)
imapc.empty(mailbox)
with lib.get_imapconnector(request) as imapc:
imapc.empty(mailbox)
return response.Response(status=204)
@action(methods=["post"], detail=False)
def delete(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
imapc = lib.get_imapconnector(request)
imapc.delete_folder(serializer.validated_data["name"])
with lib.get_imapconnector(request) as imapc:
imapc.delete_folder(serializer.validated_data["name"])
return response.Response(status=204)
@@ -115,29 +131,29 @@ class UserEmailViewSet(viewsets.GenericViewSet):
def list(self, request):
mailbox = request.GET.get("mailbox", "INBOX")
imapc = lib.get_imapconnector(request)
search = request.GET.get("search")
if search:
imapc.parse_search_parameters("both", search)
total = imapc.messages_count(mbox=mailbox)
messages_per_page = request.user.parameters.get_value("messages_per_page")
paginator = Paginator(total, messages_per_page)
page_num = int(request.GET.get("page", 1))
page = paginator.getpage(int(request.GET.get("page", 1)))
if not page:
serializer = self.get_serializer(
{
"count": 0,
"first_index": 0,
"last_index": 0,
"prev_page": None,
"next_page": None,
"results": [],
}
)
return response.Response(serializer.data)
with lib.get_imapconnector(request) as imapc:
if search:
imapc.parse_search_parameters("both", search)
total = imapc.messages_count(mbox=mailbox)
messages_per_page = request.user.parameters.get_value("messages_per_page")
paginator = Paginator(total, messages_per_page)
page_num = int(request.GET.get("page", 1))
page = paginator.getpage(int(request.GET.get("page", 1)))
if not page:
serializer = self.get_serializer(
{
"count": 0,
"first_index": 0,
"last_index": 0,
"prev_page": None,
"next_page": None,
"results": [],
}
)
return response.Response(serializer.data)
content = imapc.fetch(page.id_start, page.id_stop, mbox=mailbox)
content = imapc.fetch(page.id_start, page.id_stop, mbox=mailbox)
content = [dict(msg) for msg in content]
serializer = self.get_serializer(
{
@@ -155,12 +171,12 @@ class UserEmailViewSet(viewsets.GenericViewSet):
"""Move selected messages to the given mailbox."""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
mbc = lib.get_imapconnector(request)
mbc.move(
",".join(serializer.validated_data["selection"]),
serializer.validated_data["mailbox"],
destination,
)
with lib.get_imapconnector(request) as mbc:
mbc.move(
",".join(serializer.validated_data["selection"]),
serializer.validated_data["mailbox"],
destination,
)
return len(serializer.validated_data["selection"])
@action(
@@ -202,10 +218,11 @@ class UserEmailViewSet(viewsets.GenericViewSet):
def flag(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
imapc = lib.get_imapconnector(request)
getattr(imapc, f"mark_messages_{serializer.validated_data['status']}")(
serializer.validated_data["mailbox"], serializer.validated_data["selection"]
)
with lib.get_imapconnector(request) as imapc:
getattr(imapc, f"mark_messages_{serializer.validated_data['status']}")(
serializer.validated_data["mailbox"],
serializer.validated_data["selection"],
)
return response.Response({"count": len(serializer.validated_data["selection"])})
@action(methods=["get"], detail=False)
@@ -256,8 +273,8 @@ class UserEmailViewSet(viewsets.GenericViewSet):
partnum = request.GET.get("partnum")
if not mailbox or not mailbox or not partnum:
raise Http404
imapc = lib.get_imapconnector(request)
partdef, payload = imapc.fetchpart(mailid, mailbox, partnum)
with lib.get_imapconnector(request) as imapc:
partdef, payload = imapc.fetchpart(mailid, mailbox, partnum)
resp = HttpResponse(lib.decode_payload(partdef["encoding"], payload))
resp["Content-Type"] = partdef["Content-Type"]
resp["Content-Transfer-Encoding"] = partdef["encoding"]
@@ -285,6 +302,14 @@ class ComposeSessionViewSet(viewsets.GenericViewSet):
return serializers.AllowedSenderSerializer
return serializers.ComposeSessionSerializer
def retrieve(self, request, pk=None):
manager = attachments.ComposeSessionManager(request.user.username)
session = manager.get_content(pk)
serializer = self.get_serializer(
{"uid": pk, "attachments": session["attachments"]}
)
return response.Response(serializer.data)
def create(self, request):
uid = attachments.ComposeSessionManager(request.user.username).create()
serializer = self.get_serializer({"uid": uid})
@@ -308,31 +333,21 @@ class ComposeSessionViewSet(viewsets.GenericViewSet):
serializer = self.get_serializer(addresses, many=True)
return response.Response(serializer.data)
@action(
methods=["get", "post"], detail=True, parser_classes=(parsers.MultiPartParser,)
)
@action(methods=["post"], detail=True, parser_classes=(parsers.MultiPartParser,))
def attachments(self, request, pk):
if request.method == "POST":
manager = attachments.ComposeSessionManager(request.user.username)
serializer = serializers.AttachmentUploadSerializer(data=request.FILES)
if not serializer.is_valid():
errors = serializer.errors
if request.upload_handlers[0].toobig:
errors["attachment"] = [
_("Attachment is too big (limit: %s)")
% request.upload_handlers[0].maxsize
]
serializer = serializers.AttachmentUploadSerializer(data=request.FILES)
if not serializer.is_valid():
errors = serializer.errors
if request.upload_handlers[0].toobig:
errors["attachment"] = [
_("Attachment is too big (limit: %s)")
% request.upload_handlers[0].maxsize
]
return response.Response(errors, status=400)
result = attachments.save_attachment(
request, pk, serializer.validated_data["attachment"]
)
return response.Response(result)
manager = attachments.ComposeSessionManager(request.user.username)
session = manager.get_content(pk)
serializer = serializers.UploadedAttachmentSerializer(
session["attachments"], many=True
result = attachments.save_attachment(
request, pk, serializer.validated_data["attachment"]
)
return response.Response(serializer.data)
return response.Response(result)
@action(methods=["delete"], detail=True, url_path="attachments/(?P<name>[^/.]+)")
def delete_attachment(self, request, pk, name):