mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-17 05:41:25 +02:00
Improved listing and detail pages
This commit is contained in:
@@ -4,4 +4,19 @@ export default {
|
||||
getQuarantineContent(params) {
|
||||
return repository.get('/amavis/quarantine/', { params })
|
||||
},
|
||||
releaseSelection(data) {
|
||||
return repository.post('/amavis/quarantine/release_selection/', data)
|
||||
},
|
||||
deleteSelection(data) {
|
||||
return repository.post('/amavis/quarantine/delete_selection/', data)
|
||||
},
|
||||
markMessageSelection(data) {
|
||||
return repository.post('/amavis/quarantine/mark_selection/', data)
|
||||
},
|
||||
getMessageContent(mailid, rcpt) {
|
||||
return repository.get(`/amavis/quarantine/${mailid}/?rcpt=${rcpt}`)
|
||||
},
|
||||
getMessageHeaders(mailid) {
|
||||
return repository.get(`/amavis/quarantine/${mailid}/headers/`)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,27 +9,98 @@
|
||||
:items-length="totalMessages"
|
||||
:page="currentPage"
|
||||
:items-per-page="itemsPerPageR"
|
||||
item-value="pk"
|
||||
item-value="mailid"
|
||||
elevation="0"
|
||||
show-select
|
||||
return-object
|
||||
density="compact"
|
||||
:sort-by="sortByR"
|
||||
:row-props="getRowProps"
|
||||
:cell-props="getCellProps"
|
||||
@update:options="updatedOptions"
|
||||
@click:row="openMessage"
|
||||
>
|
||||
<template #top>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:placeholder="$gettext('Search in messages')"
|
||||
variant="outlined"
|
||||
single-line
|
||||
flat
|
||||
hide-details
|
||||
density="compact"
|
||||
class="flex-grow-0 w-33 mr-4"
|
||||
clearable
|
||||
@click:clear="fetchContent"
|
||||
@keyup.enter="submitSearch"
|
||||
></v-text-field>
|
||||
<div class="d-flex my-4">
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
:placeholder="$gettext('Search in messages')"
|
||||
variant="outlined"
|
||||
single-line
|
||||
flat
|
||||
hide-details
|
||||
density="compact"
|
||||
class="flex-grow-0 w-33 mr-4"
|
||||
clearable
|
||||
@click:clear="fetchContent"
|
||||
@keyup.enter="submitSearch"
|
||||
></v-text-field>
|
||||
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
color="success"
|
||||
variant="tonal"
|
||||
icon="mdi-check"
|
||||
size="small"
|
||||
:disabled="selected.length === 0"
|
||||
:title="$gettext('Release selection')"
|
||||
:loading="loading"
|
||||
@click="releaseSelection"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
icon="mdi-trash-can"
|
||||
size="small"
|
||||
:disabled="selected.length === 0"
|
||||
:title="$gettext('Delete selection')"
|
||||
:loading="loading"
|
||||
@click="deleteSelection"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
icon
|
||||
:disabled="selected.length === 0"
|
||||
>
|
||||
<v-icon icon="mdi-cog" />
|
||||
<v-menu activator="parent">
|
||||
<v-list>
|
||||
<v-list-item
|
||||
:title="$gettext('Mark as spam')"
|
||||
@click="markSelectionAsSpam"
|
||||
/>
|
||||
<v-list-item
|
||||
:title="$gettext('Mark as not spam')"
|
||||
@click="markSelectionAsHam"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
<template #[`item.type`]="{ item }">
|
||||
<v-chip :color="getTypeColor(item.type)" size="small" label>
|
||||
{{ item.type }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template #[`item.from_address`]="{ item }">
|
||||
<span :title="item.from_address">
|
||||
{{ $truncate(item.from_address, 30) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #[`item.subject`]="{ item }">
|
||||
<span :title="item.subject">
|
||||
{{ $truncate(item.subject, 30) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #[`item.datetime`]="{ item }">
|
||||
{{ $date(item.datetime) }}
|
||||
</template>
|
||||
</v-data-table-server>
|
||||
</v-card>
|
||||
@@ -37,13 +108,161 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useBusStore } from '@/stores'
|
||||
import api from '@/api/amavis'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const router = useRouter()
|
||||
const { displayNotification } = useBusStore()
|
||||
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPageR = ref(10)
|
||||
const loading = ref(false)
|
||||
const messages = ref([])
|
||||
const search = ref('')
|
||||
const selected = ref([])
|
||||
const sortByR = ref([])
|
||||
const totalMessages = ref(0)
|
||||
|
||||
const fetchContent = () => {}
|
||||
const headers = [
|
||||
{ title: '', key: 'type' },
|
||||
{ title: $gettext('Score'), key: 'score' },
|
||||
{ title: $gettext('To'), key: 'to_address' },
|
||||
{ title: $gettext('From'), key: 'from_address' },
|
||||
{ title: $gettext('Subject'), key: 'subject' },
|
||||
{ title: $gettext('Date'), key: 'datetime' },
|
||||
]
|
||||
|
||||
const getRowProps = ({ item }) => {
|
||||
return {
|
||||
class: item.style,
|
||||
}
|
||||
}
|
||||
|
||||
const getCellProps = () => {
|
||||
return { class: 'clickable' }
|
||||
}
|
||||
|
||||
const getTypeColor = (value) => {
|
||||
if (['S', 'V'].includes(value)) {
|
||||
return 'error'
|
||||
}
|
||||
if (['Y', 'H', 'M', 'B', 'O', 'T'].includes(value)) {
|
||||
return 'warning'
|
||||
}
|
||||
if (value === 'C') {
|
||||
return 'success'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const updatedOptions = async ({ page, itemsPerPage, sortBy }) => {
|
||||
currentPage.value = page
|
||||
itemsPerPageR.value = itemsPerPage
|
||||
sortByR.value = sortBy
|
||||
fetchContent()
|
||||
}
|
||||
|
||||
const fetchContent = async () => {
|
||||
loading.value = true
|
||||
const params = {
|
||||
page: currentPage.value || 1,
|
||||
page_size: itemsPerPageR.value || 10,
|
||||
}
|
||||
if (sortByR.value && sortByR.value.length) {
|
||||
params.ordering = sortByR.value
|
||||
.map((item) => (item.order !== 'asc' ? `-${item.key}` : item.key))
|
||||
.join(',')
|
||||
}
|
||||
if (search.value !== '') {
|
||||
params.search = search.value
|
||||
}
|
||||
try {
|
||||
const resp = await api.getQuarantineContent(params)
|
||||
messages.value = resp.data.results
|
||||
totalMessages.value = resp.data.count
|
||||
selected.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openMessage = (event, { item }) => {
|
||||
router.push({
|
||||
name: 'QuarantineMessageView',
|
||||
params: { mailid: item.mailid, rcpt: item.to_address },
|
||||
})
|
||||
}
|
||||
|
||||
const prepareSelectionPayload = () => {
|
||||
const data = {
|
||||
selection: [],
|
||||
}
|
||||
for (const row of selected.value) {
|
||||
data.selection.push({ rcpt: row.to_address, mailid: row.mailid })
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
const deleteSelection = async () => {
|
||||
loading.value = true
|
||||
const data = prepareSelectionPayload()
|
||||
try {
|
||||
await api.deleteSelection(data)
|
||||
fetchContent()
|
||||
displayNotification({ msg: $gettext('Selection deleted') })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const releaseSelection = async () => {
|
||||
loading.value = true
|
||||
const data = prepareSelectionPayload()
|
||||
try {
|
||||
const resp = await api.releaseSelection(data)
|
||||
fetchContent()
|
||||
if (resp.data.status === 'pending') {
|
||||
displayNotification({ msg: $gettext('Release request sent') })
|
||||
} else {
|
||||
displayNotification({ msg: $gettext('Selection released') })
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const markSelection = async (mtype) => {
|
||||
loading.value = true
|
||||
const data = prepareSelectionPayload()
|
||||
data.type = mtype
|
||||
try {
|
||||
await api.markMessageSelection(data)
|
||||
displayNotification({ msg: $gettext('Selection is being processed...') })
|
||||
selected.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const markSelectionAsSpam = async () => {
|
||||
await markSelection('spam')
|
||||
}
|
||||
|
||||
const markSelectionAsHam = async () => {
|
||||
await markSelection('ham')
|
||||
}
|
||||
|
||||
const submitSearch = () => {}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.unseen {
|
||||
font-weight: bold;
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -441,6 +441,14 @@ const routes = [
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':mailid/:rcpt',
|
||||
name: 'QuarantineMessageView',
|
||||
component: () => import('@/views/quarantine/MessageView.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="bg-white rounded-lg pa-4 position-relative h-100">
|
||||
<v-toolbar color="white">
|
||||
<v-btn icon="mdi-arrow-left" size="small" variant="flat" @click="close" />
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
color="success"
|
||||
variant="tonal"
|
||||
icon="mdi-check"
|
||||
size="small"
|
||||
:title="$gettext('Release message')"
|
||||
:loading="loading"
|
||||
@click="releaseMessage"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
icon="mdi-trash-can"
|
||||
size="small"
|
||||
:title="$gettext('Delete message')"
|
||||
:loading="loading"
|
||||
@click="deleteMessage"
|
||||
>
|
||||
</v-btn>
|
||||
<v-btn class="ml-2" variant="tonal" icon size="small">
|
||||
<v-icon icon="mdi-cog" />
|
||||
<v-menu activator="parent">
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
:title="$gettext('Mark as spam')"
|
||||
@click="markAsSpam"
|
||||
/>
|
||||
<v-list-item
|
||||
:title="$gettext('Mark as non-spam')"
|
||||
@click="markAsHam"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
class="ml-2"
|
||||
variant="tonal"
|
||||
size="small"
|
||||
:loading="loading"
|
||||
@click="toggleFullHeaders"
|
||||
>
|
||||
<span v-if="!fullHeaders">
|
||||
{{ $gettext('View full headers') }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $gettext('Hide full headers') }}
|
||||
</span>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<div class="pa-4 bg-grey-lighten-3 rounded">
|
||||
<v-row v-for="header in headers" :key="header.name" no-gutters>
|
||||
<v-col cols="2" class="font-weight-bold">
|
||||
{{ header.name }}
|
||||
</v-col>
|
||||
<v-col cols="10">{{ header.value }}</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
<div class="mt-4" v-html="message.body" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useBusStore } from '@/stores'
|
||||
import api from '@/api/amavis'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { $gettext } = useGettext()
|
||||
const { displayNotification } = useBusStore()
|
||||
|
||||
const fullHeaders = ref(null)
|
||||
const loading = ref(false)
|
||||
const message = ref(null)
|
||||
|
||||
const headers = computed(() => {
|
||||
if (!fullHeaders.value) {
|
||||
return message.value.headers
|
||||
}
|
||||
return fullHeaders.value
|
||||
})
|
||||
|
||||
const close = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
const toggleFullHeaders = async () => {
|
||||
if (fullHeaders.value) {
|
||||
fullHeaders.value = null
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
const resp = await api.getMessageHeaders(route.params.mailid)
|
||||
fullHeaders.value = resp.data.headers
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const releaseMessage = async () => {
|
||||
loading.value = true
|
||||
const data = {
|
||||
selection: [{ rcpt: route.rcpt, mailid: route.mailid }],
|
||||
}
|
||||
try {
|
||||
const resp = await api.releaseSelection(data)
|
||||
if (resp.data.status === 'pending') {
|
||||
displayNotification({ msg: $gettext('Release request sent') })
|
||||
} else {
|
||||
displayNotification({ msg: $gettext('Message released') })
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMessage = async () => {
|
||||
loading.value = true
|
||||
const data = {
|
||||
selection: [{ rcpt: route.params.rcpt, mailid: route.params.mailid }],
|
||||
}
|
||||
try {
|
||||
await api.deleteSelection(data)
|
||||
displayNotification({ msg: $gettext('Message deleted') })
|
||||
router.push({ name: 'QuarantineView' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const mark = async (mtype, msg) => {
|
||||
loading.value = true
|
||||
const data = {
|
||||
type: mtype,
|
||||
selection: [{ rcpt: route.params.rcpt, mailid: route.params.mailid }],
|
||||
}
|
||||
try {
|
||||
await api.markMessageSelection(data)
|
||||
displayNotification({ msg })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const markAsSpam = async () => {
|
||||
await mark('spam', $gettext('Message marked as spam'))
|
||||
}
|
||||
|
||||
const markAsHam = async () => {
|
||||
await mark('ham', $gettext('Message marked as non-spam'))
|
||||
}
|
||||
|
||||
const resp = await api.getMessageContent(route.params.mailid)
|
||||
message.value = resp.data
|
||||
</script>
|
||||
@@ -1,9 +1,11 @@
|
||||
<template>
|
||||
<div class="text-h5 ml-4">
|
||||
{{ $gettext('Quarantine') }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-h5 ml-4">
|
||||
{{ $gettext('Quarantine') }}
|
||||
</div>
|
||||
|
||||
<QuarantineList />
|
||||
<QuarantineList />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
@@ -96,7 +96,7 @@ class Email:
|
||||
if self._headers is None:
|
||||
self._headers = []
|
||||
|
||||
# don't break modoboa_webmail.lib.imapemail.ImapEmail, it
|
||||
# don't break modoboa.webmail.lib.imapemail.ImapEmail, it
|
||||
# doesn't have self._msg it loads e-mails (and their headers)
|
||||
# from the IMAP server.
|
||||
if self._msg is not None:
|
||||
|
||||
+4
-1
@@ -77,7 +77,10 @@ dependencies = [
|
||||
# User
|
||||
"drf-nested-routers",
|
||||
"vobject",
|
||||
"caldav==2.0.1"
|
||||
"caldav==2.0.1",
|
||||
# Amavis
|
||||
"html2text",
|
||||
"idna",
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
|
||||
Reference in New Issue
Block a user