2025-04-16 17:59:03 +02:00
|
|
|
<script setup>
|
|
|
|
|
|
|
|
|
|
import { ref, onMounted } from 'vue';
|
|
|
|
|
import { eachLimit } from 'async';
|
2025-04-19 11:53:42 +02:00
|
|
|
import { Button, Icon } from 'pankow';
|
2025-04-16 17:59:03 +02:00
|
|
|
import { prettyDecimalSize } from 'pankow/utils';
|
|
|
|
|
import Section from '../components/Section.vue';
|
|
|
|
|
import StateLED from '../components/StateLED.vue';
|
|
|
|
|
import MailDomainStatus from '../components/MailDomainStatus.vue';
|
|
|
|
|
import DomainsModel from '../models/DomainsModel.js';
|
|
|
|
|
import MailModel from '../models/MailModel.js';
|
|
|
|
|
|
|
|
|
|
const domainsModel = DomainsModel.create();
|
|
|
|
|
const mailModel = MailModel.create();
|
|
|
|
|
|
|
|
|
|
const refreshBusy = ref(true);
|
|
|
|
|
const domains = ref([]);
|
|
|
|
|
const expandedDomain = ref('');
|
|
|
|
|
|
|
|
|
|
async function onRefresh() {
|
|
|
|
|
refreshBusy.value = true;
|
|
|
|
|
await eachLimit(domains.value, 10, refreshStatus);
|
|
|
|
|
refreshBusy.value = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function refreshStatus(domain) {
|
2025-04-19 11:53:42 +02:00
|
|
|
domain.loading = true;
|
|
|
|
|
|
2025-04-16 17:59:03 +02:00
|
|
|
let [error, result] = await mailModel.status(domain.domain);
|
|
|
|
|
if (error) {
|
|
|
|
|
console.error(error);
|
|
|
|
|
} else {
|
|
|
|
|
domain.status = Object.keys(result).every((k) => {
|
|
|
|
|
if (k === 'dns') return Object.keys(result.dns).every(function (k) { return result.dns[k].status; });
|
|
|
|
|
if (!('status' in result[k])) return true; // if status is not present, the test was not run
|
|
|
|
|
return result[k].status;
|
|
|
|
|
});
|
|
|
|
|
domain.statusCheckDone = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[error, result] = await mailModel.config(domain.domain);
|
|
|
|
|
if (error) {
|
|
|
|
|
console.error(error);
|
|
|
|
|
} else {
|
|
|
|
|
domain.inbound = result.enabled;
|
|
|
|
|
domain.outbound = result.relay?.provider !== 'noop';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// do this even if no outbound since people forget to remove mailboxes
|
|
|
|
|
[error, result] = await mailModel.mailboxCount(domain.domain);
|
|
|
|
|
if (error) {
|
|
|
|
|
console.error(error);
|
|
|
|
|
} else {
|
|
|
|
|
domain.mailboxCount = result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
domain.loading = false;
|
|
|
|
|
|
|
|
|
|
[error, result] = await mailModel.usage(domain.domain);
|
|
|
|
|
if (error) {
|
|
|
|
|
console.error(error);
|
|
|
|
|
} else {
|
|
|
|
|
domain.usage = 0;
|
|
|
|
|
// we used to use quotaValue here but it's quite different wrt diskSize. so choose diskSize consistently
|
|
|
|
|
// also, quotaValue can be missing for deleted mailboxes that are on disk but removed from dovecot/ldap itself
|
|
|
|
|
Object.keys(result).forEach(function (m) { domain.usage += result[m].diskSize; });
|
|
|
|
|
domain.loadingUsage = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onToggleExpanded(domain) {
|
|
|
|
|
expandedDomain.value = expandedDomain.value === domain ? '' : domain;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
const [error, result] = await domainsModel.list();
|
|
|
|
|
if (error) return console.error(error);
|
|
|
|
|
|
|
|
|
|
domains.value = result;
|
|
|
|
|
|
|
|
|
|
onRefresh();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="content">
|
|
|
|
|
<Section title="Mail domain status">
|
|
|
|
|
<template #header-buttons>
|
|
|
|
|
<Button @click="onRefresh" :disabled="refreshBusy" :loading="refreshBusy">Refresh</Button>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<div class="domain-list-item" :class="{ active: domain.domain === expandedDomain }" v-for="domain in domains" :key="domain.domain">
|
2025-04-19 11:53:42 +02:00
|
|
|
<div @click="onToggleExpanded(domain.domain)" style="display: flex; justify-content: space-between; padding: 10px 0;">
|
2025-04-16 17:59:03 +02:00
|
|
|
<div>
|
2025-04-19 11:53:42 +02:00
|
|
|
<i class="collapse fa-solid fa-angle-right" :class="{ expanded: domain.domain === expandedDomain }" style="margin-right: 6px;"></i>
|
2025-04-16 17:59:03 +02:00
|
|
|
<StateLED :busy="!domain.statusCheckDone" :state="domain.status ? 'success' : 'danger'" style="margin-right: 6px"/>
|
|
|
|
|
{{ domain.domain }}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div v-if="domain.loading">{{ $t('main.loadingPlaceholder') }} ...</div>
|
|
|
|
|
<div v-else>
|
|
|
|
|
<div v-if="domain.inbound">
|
|
|
|
|
<span v-if="domain.loadingUsage">{{ $t('emails.domains.stats', { mailboxCount: domain.mailboxCount }) }} {{ $t('main.loadingPlaceholder') }} ... </span>
|
|
|
|
|
<span v-else>{{ $t('emails.domains.stats', { mailboxCount: domain.mailboxCount, usage: prettyDecimalSize(domain.usage) }) }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div v-else>
|
|
|
|
|
<span v-if="domain.outbound">{{ $t('emails.domains.outbound') }}</span>
|
|
|
|
|
<span v-else>{{ $t('emails.domains.disabled') }}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<MailDomainStatus :domain="domain.domain" v-if="expandedDomain === domain.domain"/>
|
|
|
|
|
</div>
|
|
|
|
|
</Section>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
|
|
|
|
.domain-list-item {
|
|
|
|
|
cursor: pointer;
|
2025-04-19 11:53:42 +02:00
|
|
|
padding: 0 10px;
|
2025-04-16 17:59:03 +02:00
|
|
|
margin-bottom: 10px;
|
2025-04-19 11:53:42 +02:00
|
|
|
border-radius: var(--pankow-border-radius);
|
2025-04-16 17:59:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.domain-list-item.active,
|
|
|
|
|
.domain-list-item:hover {
|
|
|
|
|
background-color: var(--pankow-color-background-hover);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|