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