Files
cloudron-box/dashboard/src/views/EmailEventlogView.vue
T

216 lines
8.7 KiB
Vue
Raw Normal View History

2025-03-10 16:16:04 +01:00
<script setup>
2026-01-16 10:29:51 +01:00
import { ref, reactive, onMounted, watch, useTemplateRef, nextTick } from 'vue';
2025-07-10 11:55:11 +02:00
import { Button, TextInput, MultiSelect } from '@cloudron/pankow';
import { useDebouncedRef, prettyEmailAddresses, prettyLongDate } from '@cloudron/pankow/utils';
2025-03-10 16:16:04 +01:00
import MailModel from '../models/MailModel.js';
const mailModel = MailModel.create();
const availableTypes = [
{ id: 'bounce', name: 'Bounce' },
{ id: 'deferred', name: 'Deferred' },
{ id: 'denied', name: 'Denied' },
{ id: 'queued', name: 'Queued' },
{ id: 'quota', name: 'Quota' },
{ id: 'saved', name: 'Saved' },
{ id: 'sent', name: 'Sent' },
2025-03-10 16:16:04 +01:00
{ id: 'spam', name: 'Spam' },
{ id: 'spam-learn', name: 'Spam Training' },
2025-03-10 16:16:04 +01:00
];
const refreshBusy = ref(false);
const eventlogs = ref([]);
const search = useDebouncedRef('');
const page = ref(1);
const perPage = ref(10);
const eventlogContainer = useTemplateRef('eventlogContainer');
2026-02-05 15:14:15 +01:00
// eslint-disable-next-line prefer-const
let types = reactive([]);
2025-03-10 16:16:04 +01:00
async function onRefresh() {
refreshBusy.value = true;
2025-12-08 16:51:26 +01:00
page.value = 1;
2025-03-10 16:16:04 +01:00
const [error, result] = await mailModel.eventlog(types.join(','), search.value, page.value, perPage.value);
if (error) return console.error(error);
2025-03-10 16:16:04 +01:00
eventlogs.value = result;
2026-01-16 10:29:51 +01:00
await nextTick();
while (eventlogContainer.value && eventlogContainer.value.scrollHeight <= eventlogContainer.value.offsetHeight) {
await fetchMore();
}
2025-03-10 16:16:04 +01:00
refreshBusy.value = false;
}
async function fetchMore() {
page.value++;
2025-03-10 16:16:04 +01:00
const [error, result] = await mailModel.eventlog(types.join(','), search.value, page.value, perPage.value);
if (error) return console.error(error);
eventlogs.value = eventlogs.value.concat(result);
}
async function onScroll(event) {
if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) await fetchMore();
2025-03-10 16:16:04 +01:00
}
watch(perPage, onRefresh);
watch(types, onRefresh);
watch(search, onRefresh);
onMounted(async () => {
await onRefresh();
while (eventlogContainer.value && eventlogContainer.value.scrollHeight <= eventlogContainer.value.offsetHeight) {
await fetchMore();
}
2025-03-10 16:16:04 +01:00
});
</script>
<template>
2025-09-22 11:09:41 +02:00
<div class="content-large" style="height: 100%; overflow: hidden; display: flex; flex-direction: column;">
<!-- cant use Section component as we need control over vertical scrolling -->
<div class="section" style="overflow: hidden; display: flex; flex-direction: column;">
<h2 class="section-header">
{{ $t('emails.eventlog.title') }}
<div>
<TextInput :placeholder="$t('main.searchPlaceholder')" style="flex-grow: 1;" v-model="search"/>
<MultiSelect v-model="types" :options="availableTypes" option-key="id" option-label="name" :selected-label="types.length ? $t('main.multiselect.selected', { n: types.length }) : $t('emails.typeFilterHeader')"/>
<Button tool secondary @click="onRefresh()" :loading="refreshBusy" icon="fa-solid fa-sync-alt" />
2025-09-22 11:09:41 +02:00
<Button tool secondary href="/logs.html?id=mail" target="_blank">{{ $t('main.action.logs') }}</Button>
</div>
</h2>
<div class="section-body" ref="eventlogContainer" style="margin-top: 16px; overflow: auto; padding-top: 0" @scroll="onScroll">
<table class="eventlog-table">
<thead>
<tr>
<th style="width: 25px"><!-- Icon --></th>
<th style="width:160px">{{ $t('emails.eventlog.time') }}</th>
<th style="width: 20%">{{ $t('emails.eventlog.mailFrom') }}</th>
<th style="width: 20%">{{ $t('emails.eventlog.rcptTo') }}</th>
<th>{{ $t('emails.eventlog.details') }}</th>
</tr>
</thead>
<tbody>
<template v-for="eventlog in eventlogs" :key="eventlog._id">
<tr @click="eventlog.isOpen = !eventlog.isOpen" :class="{ 'active': eventlog.isOpen }" >
<td>
<i class="fas fa-arrow-circle-left" v-if="eventlog.type === 'sent'" v-tooltip="$t('emails.eventlog.type.outgoing')"></i>
<i class="fas fa-history" v-if="eventlog.type === 'deferred'" v-tooltip="$t('emails.eventlog.type.deferred')"></i>
<i class="fas fa-arrow-circle-right" v-if="eventlog.type === 'saved'" v-tooltip="$t('emails.eventlog.type.incoming')"></i>
<i class="fas fa-align-justify" v-if="eventlog.type === 'queued' && !eventlog.spamStatus" v-tooltip="$t('emails.eventlog.type.queued')"></i>
<i class="fas fa-align-justify" v-if="eventlog.type === 'queued' && eventlog.spamStatus && eventlog.spamStatus.indexOf('Yes,') !== 0" v-tooltip="$t('emails.eventlog.type.queued')"></i>
<i class="fas fa-trash" v-if="eventlog.type === 'queued' && eventlog.spamStatus && eventlog.spamStatus.indexOf('Yes,') === 0" v-tooltip="$t('emails.eventlog.type.queued')"></i>
<i class="fas fa-minus-circle" v-if="eventlog.type === 'denied'" v-tooltip="$t('emails.eventlog.type.denied')"></i>
<i class="fas fa-hand-paper" v-if="eventlog.type === 'bounce'" v-tooltip="$t('emails.eventlog.type.bounce')"></i>
<i class="fas fa-filter" v-if="eventlog.type === 'spam-learn'" v-tooltip="$t('emails.eventlog.type.spamFilterTrained')"></i>
<i class="fas fa-fill-drip" v-if="eventlog.type === 'quota'" v-tooltip="$t('emails.eventlog.type.quota')"></i>
</td>
<td>{{ prettyLongDate(eventlog.ts) }}</td>
<td class="elide-table-cell">{{ prettyEmailAddresses(eventlog.mailFrom) || '-' }}</td>
<td class="elide-table-cell">{{ prettyEmailAddresses(eventlog.rcptTo) || eventlog.mailbox || '-' }}</td>
<td>
<span v-if="eventlog.type === 'bounce'">{{ $t('emails.eventlog.type.bounceInfo') }}. {{ eventlog.message || eventlog.reason }}</span>
2025-11-19 13:39:04 +01:00
<span v-if="eventlog.type === 'deferred'">{{ $t('emails.eventlog.type.deferredInfo', { delay:eventlog.delay }) }} {{ eventlog.message || eventlog.reason }} </span>
<span v-if="eventlog.type === 'queued'">
<span v-if="eventlog.direction === 'inbound'">{{ $t('emails.eventlog.type.inboundInfo') }}</span>
<span v-if="eventlog.direction === 'outbound'">{{ $t('emails.eventlog.type.outboundInfo') }}</span>
</span>
<span v-if="eventlog.type === 'saved'">{{ $t('emails.eventlog.type.savedInfo') }}</span>
<span v-if="eventlog.type === 'sent'">{{ $t('emails.eventlog.type.sentInfo') }}</span>
<span v-if="eventlog.type === 'denied'">{{ $t('emails.eventlog.type.deniedInfo') }}. {{ eventlog.message || eventlog.reason }} </span>
<span v-if="eventlog.type === 'spam-learn'">{{ $t('emails.eventlog.type.spamFilterTrainedInfo') }}</span>
<span v-if="eventlog.type === 'quota'">
<span v-if="eventlog.quotaPercent > 0">{{ $t('emails.eventlog.type.overQuotaInfo', { mailbox: eventlog.mailbox, quotaPercent: eventlog.quotaPercent }) }}</span>
<span v-if="eventlog.quotaPercent < 0">{{ $t('emails.eventlog.type.underQuotaInfo', { mailbox: eventlog.mailbox, quotaPercent: -eventlog.quotaPercent }) }}</span>
</span>
</td>
</tr>
2025-09-23 21:19:20 +02:00
<tr v-if="eventlog.isOpen">
2025-11-05 17:55:11 +01:00
<td colspan="5" class="eventlog-details">
<pre>{{ JSON.stringify(eventlog, null, 4) }}</pre>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
2025-03-10 16:16:04 +01:00
</div>
</template>
2026-02-17 14:48:25 +01:00
<style scoped>
.eventlog-table {
width: 100%;
overflow: auto;
border-spacing: 0px;
table-layout: fixed;
}
.elide-table-cell {
overflow: hidden;
text-overflow: ellipsis;
}
.eventlog-table thead {
background-color: var(--pankow-body-background-color);
top: 0;
position: sticky;
z-index: 1; /* avoids see-through table headers if items in the table have opacity set */
}
.eventlog-table th {
text-align: left;
}
.eventlog-table tbody tr {
cursor: pointer;
}
.eventlog-table tbody tr.active,
.eventlog-table tbody tr:hover {
background-color: var(--pankow-color-background-hover);
}
.eventlog-table th,
.eventlog-table td {
padding: 6px;
}
.eventlog-filter {
display: flex;
gap: 5px;
flex-wrap: wrap;
margin: 20px 0;
}
.eventlog-details {
background-color: color-mix(in oklab, var(--pankow-color-background-hover), black 5%);
cursor: auto;
position: relative;
}
.eventlog-source {
padding-left: 10px;
padding-bottom: 10px;
cursor: copy;
}
.eventlog-details pre {
white-space: pre-wrap;
color: var(--pankow-text-color);
font-size: 13px;
padding-left: 10px;
margin: 0;
border: none;
border-radius: var(--pankow-border-radius);
}
</style>