Added new actions

This commit is contained in:
Antoine Nguyen
2025-05-01 18:00:00 +02:00
parent af196f7333
commit 927bc00194
6 changed files with 336 additions and 33 deletions
+34 -1
View File
@@ -15,11 +15,44 @@ export default {
}
return repository.get(`/webmail/emails/`, { params })
},
getEmailContent(mailbox, mailid) {
getEmailContent(mailbox, mailid, options) {
const params = {
mailbox,
mailid,
...options,
}
return repository.get(`/webmail/emails/content/`, { params })
},
getEmailAttachment(mailbox, mailid, partnum) {
const params = {
mailbox,
mailid,
partnum,
}
return repository.get(`/webmail/emails/attachment/`, {
params,
responseType: 'blob',
})
},
deleteSelection(mailbox, selection) {
const body = {
mailbox,
selection,
}
return repository.post('/webmail/emails/delete/', body)
},
markSelectionAsJunk(mailbox, selection) {
const body = {
mailbox,
selection,
}
return repository.post('/webmail/emails/mark_as_junk/', body)
},
markSelectionAsNotJunk(mailbox, selection) {
const body = {
mailbox,
selection,
}
return repository.post('/webmail/emails/mark_as_not_junk/', body)
},
}
+205 -26
View File
@@ -1,43 +1,221 @@
<template>
<v-toolbar color="white" class="mt-12">
<v-btn variant="tonal" @click="close">
<v-icon icon="mdi-arrow-left" />
{{ $gettext('Back') }}
</v-btn>
<v-btn color="primary">
{{ $gettext('Reply') }}
</v-btn>
</v-toolbar>
<div v-show="loaded" class="bg-white rounded-lg pa-4 mt-12">
<v-toolbar color="white">
<v-btn icon="mdi-arrow-left" size="small" variant="flat" @click="close" />
<div v-if="email" class="mt-4">
<h3>{{ email.subject }}</h3>
<div>{{ email.from_address.name }}</div>
<iframe class="email-frame" />
<v-btn
color="primary"
variant="tonal"
prepend-icon="mdi-reply"
append-icon="mdi-chevron-down"
>
{{ $gettext('Reply') }}
<v-menu activator="parent">
<v-list>
<v-list-item :title="$gettext('Reply all')" />
<v-list-item :title="$gettext('Forward')" />
</v-list>
</v-menu>
</v-btn>
<v-btn
class="ml-2"
color="error"
variant="tonal"
icon="mdi-trash-can"
size="small"
:loading="working"
@click="deleteEmail"
>
</v-btn>
<v-btn
v-if="$route.params.mailbox === 'Junk'"
class="ml-2"
color="warning"
variant="tonal"
icon="mdi-fire"
size="small"
:loading="working"
@click="markEmailAsJunk"
>
</v-btn>
<v-btn
v-else
class="ml-2"
color="success"
variant="tonal"
icon="mdi-thumb-up"
size="small"
:loading="working"
@click="markEmailAsNotJunk"
>
</v-btn>
<v-btn class="ml-2" variant="tonal" icon size="small">
<v-icon icon="mdi-cog" />
<v-menu activator="parent">
<v-list>
<v-list-item
v-if="!enableLinks"
:title="$gettext('Enable links')"
@click="enableLinks = true"
/>
<v-list-item
v-else
:title="$gettext('Disable links')"
@click="enableLinks = false"
/>
</v-list>
</v-menu>
</v-btn>
</v-toolbar>
<div v-if="email" class="bg-white pa-4">
<h2>{{ email.subject }}</h2>
<div class="d-flex mt-2">
<h3 v-if="email.from_address.fulladdress">
{{ email.from_address.fulladdress }}
</h3>
<h3 v-else>{{ email.from_address.address }}</h3>
<v-spacer />
<span>{{ email.date }}</span>
</div>
<div class="mt-2">
{{ $gettext('To') }}
{{ recipients }}
</div>
<div v-if="email.attachments.length" class="mt-2">
<v-icon icon="mdi-paperclip" />
<template
v-for="(attachment, index) in email.attachments"
:key="attachment.name"
>
<template v-if="index > 0">, </template>
<a
href="#"
@click="downloadAttachment(attachment.name, attachment.partnum)"
>
{{ attachment.name }}
</a>
</template>
</div>
<iframe class="email-frame" />
</div>
</div>
</template>
<script setup>
import { nextTick, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useGettext } from 'vue3-gettext'
import { useBusStore } from '@/stores'
import api from '@/api/webmail'
const { $gettext } = useGettext()
const { displayNotification } = useBusStore()
const route = useRoute()
const router = useRouter()
const enableLinks = ref(false)
const email = ref(null)
const loaded = ref(false)
const working = ref(false)
const recipients = computed(() => {
if (!email.value) {
return ''
}
return email.value.to
.map((rcpt) => (rcpt.name ? rcpt.name : rcpt.address))
.join(', ')
})
onMounted(() => {
api.getEmailContent(route.query.mailbox, route.query.mailid).then((resp) => {
email.value = resp.data
nextTick(() => {
const iframe = document.createElement('iframe')
iframe.classList.add('email-frame')
document.querySelector('iframe').replaceWith(iframe)
const iframeDoc = iframe.contentDocument
iframeDoc.write(email.value.body)
iframeDoc.close()
})
})
fetchMailContent()
})
watch(enableLinks, () => {
fetchMailContent()
})
const close = () => {
router.push({
name: 'MailboxView',
query: { mailbox: route.query.mailbox },
})
}
const fetchMailContent = () => {
const options = {
dformat: 'html',
links: enableLinks.value ? '1' : '0',
}
api
.getEmailContent(route.query.mailbox, route.query.mailid, options)
.then((resp) => {
email.value = resp.data
nextTick(() => {
const iframe = document.createElement('iframe')
iframe.classList.add('email-frame')
document.querySelector('iframe').replaceWith(iframe)
const iframeDoc = iframe.contentDocument
iframeDoc.write(email.value.body)
iframeDoc.close()
loaded.value = true
})
})
}
const downloadAttachment = (name, part) => {
api
.getEmailAttachment(route.query.mailbox, route.query.mailid, part)
.then((resp) => {
const blob = new Blob([resp.data], { type: resp.headers['Content-Type'] })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = name
link.click()
URL.revokeObjectURL(link.href)
})
}
const deleteEmail = () => {
working.value = true
api.deleteSelection(route.query.mailbox, [route.query.mailid]).then(() => {
working.value = false
router.push({
name: 'MailboxView',
query: { mailbox: route.query.mailbox },
})
displayNotification({ msg: $gettext('Message deleted') })
})
}
const markEmailAsJunk = () => {
working.value = true
api
.markSelectionAsJunk(route.query.mailbox, [route.query.mailid])
.then(() => {
working.value = false
router.push({
name: 'MailboxView',
query: { mailbox: route.query.mailbox },
})
displayNotification({ msg: $gettext('Message marked as junk') })
})
}
const markEmailAsNotJunk = () => {
working.value = true
api
.markSelectionAsNotJunk(route.query.mailbox, [route.query.mailid])
.then(() => {
working.value = false
router.push({
name: 'MailboxView',
query: { mailbox: route.query.mailbox },
})
displayNotification({ msg: $gettext('Message marked as not junk') })
})
}
</script>
<style>
@@ -46,5 +224,6 @@ onMounted(() => {
min-height: 1000px;
border: none;
margin-top: 10px;
background-color: #fff;
}
</style>
+1 -2
View File
@@ -8,7 +8,7 @@ from .attachments import (
from .imapemail import ImapEmail, ReplyModifier, ForwardModifier
from .imaputils import BodyStructure, IMAPconnector, get_imapconnector, separate_mailbox
from .signature import EmailSignature
from .utils import decode_payload, WebmailNavigationParameters
from .utils import decode_payload
from .sendmail import send_mail
@@ -20,7 +20,6 @@ __all__ = [
"IMAPconnector",
"ImapEmail",
"ReplyModifier",
"WebmailNavigationParameters",
"clean_attachments",
"create_mail_attachment",
"decode_payload",
+2 -1
View File
@@ -44,6 +44,7 @@ class ImapEmail(Email):
self.request = request
self.imapc = get_imapconnector(request)
self.mbox, self.mailid = self.mailid.split(":")
self.attachments: dict[str, str] = {}
def _insert_contact_links(self, addresses):
"""Insert 'add to address book' links."""
@@ -167,7 +168,7 @@ class ImapEmail(Email):
return part["params"][pos + 1]
return None
def _find_attachments(self):
def _find_attachments(self) -> None:
"""Retrieve attachments from the parsed body structure.
We try to find and decode a file name for each attachment. If
+26 -1
View File
@@ -64,9 +64,14 @@ class EmailAddressSerializer(serializers.Serializer):
name = serializers.CharField(required=False)
class AttachmentSerializer(serializers.Serializer):
name = serializers.CharField()
partnum = serializers.CharField()
class EmailHeadersSerializer(serializers.Serializer):
mailid = serializers.CharField()
imapid = serializers.CharField()
subject = serializers.SerializerMethodField()
from_address = serializers.SerializerMethodField()
date = serializers.SerializerMethodField()
@@ -91,4 +96,24 @@ class EmailSerializer(serializers.Serializer):
subject = serializers.CharField(source="Subject")
from_address = EmailAddressSerializer(source="From")
to = EmailAddressSerializer(source="To", many=True)
body = serializers.CharField()
date = serializers.CharField(source="Date")
attachments = serializers.SerializerMethodField()
def get_attachments(self, email):
result = []
if email.attachments:
for partnum, name in email.attachments.items():
data = {"name": name, "partnum": partnum}
result.append(data)
return result
class MoveSelectionSerializer(serializers.Serializer):
mailbox = serializers.CharField()
selection = serializers.ListField(child=serializers.CharField())
def validate_selection(self, value):
return [item for item in value if item.isdigit()]
+68 -2
View File
@@ -1,6 +1,6 @@
"""Webmail viewsets."""
from django.http import Http404
from django.http import Http404, HttpResponse
from rest_framework import response, viewsets
from rest_framework.decorators import action
@@ -37,13 +37,79 @@ class UserEmailViewSet(viewsets.GenericViewSet):
serializer = serializers.EmailHeadersSerializer(content, many=True)
return response.Response(serializer.data)
def move_selection(self, request, destination: str) -> int:
"""Move selected messages to the given mailbox."""
serializer = serializers.MoveSelectionSerializer(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,
)
return len(serializer.validated_data["selection"])
@action(
methods=["post"],
detail=False,
serializer_class=serializers.MoveSelectionSerializer,
)
def delete(self, request):
count = self.move_selection(
request, request.user.parameters.get_value("trash_folder")
)
return response.Response({"count": count})
@action(
methods=["post"],
detail=False,
serializer_class=serializers.MoveSelectionSerializer,
)
def mark_as_junk(self, request):
count = self.move_selection(
request, request.user.parameters.get_value("trash_folder")
)
return response.Response({"count": count})
@action(
methods=["post"],
detail=False,
serializer_class=serializers.MoveSelectionSerializer,
)
def mark_as_not_junk(self, request):
count = self.move_selection(request, "INBOX")
return response.Response({"count": count})
@action(methods=["get"], detail=False)
def content(self, request):
mailbox = request.GET.get("mailbox", "INBOX")
mailid = request.GET.get("mailid")
if not mailbox or not mailid:
raise Http404
email = lib.ImapEmail(request, f"{mailbox}:{mailid}")
dformat = self.request.GET.get("dformat", "plain")
email = lib.ImapEmail(
request,
f"{mailbox}:{mailid}",
dformat=dformat,
links=request.GET.get("links", "0") == "1",
)
email.fetch_headers()
serializer = serializers.EmailSerializer(email)
return response.Response(serializer.data)
@action(methods=["get"], detail=False)
def attachment(self, request):
mailbox = request.GET.get("mailbox", "INBOX")
mailid = request.GET.get("mailid")
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)
resp = HttpResponse(lib.decode_payload(partdef["encoding"], payload))
resp["Content-Type"] = partdef["Content-Type"]
resp["Content-Transfer-Encoding"] = partdef["encoding"]
resp["Content-Disposition"] = lib.rfc6266.build_header("attachment")
if int(partdef["size"]) < 200:
resp["Content-Length"] = partdef["size"]
return resp