Files
cloudron-box/dashboard/src/views/EmailDomainsView.vue
T
2025-12-10 18:04:07 +01:00

172 lines
5.3 KiB
Vue

<script setup>
import { ref, onMounted, computed } from 'vue';
import { prettyDecimalSize, sleep } from '@cloudron/pankow/utils';
import { prettyRelayProviderName } from '../utils.js';
import { TextInput } from '@cloudron/pankow';
import Section from '../components/Section.vue';
import StateLED from '../components/StateLED.vue';
import DomainsModel from '../models/DomainsModel.js';
import MailModel from '../models/MailModel.js';
const domainsModel = DomainsModel.create();
const mailModel = MailModel.create();
const domains = ref([]);
const searchFilter = ref('');
const filteredDomains = computed(() => {
if (!searchFilter.value) return domains.value;
return domains.value.filter(d => {
const search = searchFilter.value.toLowerCase();
if (d.domain.toLowerCase().indexOf(search) !== -1) return true;
if (d.provider.toLowerCase().indexOf(search) !== -1) return true;
if (d.inboundEnabled && 'inbound'.includes(search)) return true;
if (d.outboundEnabled && 'outbound'.includes(search)) return true;
return false;
});
});
async function refreshStatus() {
for (const domain of domains.value) {
const [error, result] = await mailModel.status(domain.domain);
if (error) {
console.error(error);
} else {
domain.status = Object.keys(result).every((k) => {
return result[k].status === 'passed' || result[k].status === 'skipped';
});
domain.loadingStatus = false;
}
}
}
async function refreshUsage() {
for (const domain of domains.value) {
let [error, result] = await mailModel.config(domain.domain);
if (error) return console.error(error);
domain.inboundEnabled = result.enabled;
domain.outboundEnabled = result.relay?.provider !== 'noop';
domain.relayProvider = result.relay ? result.relay.provider : 'unset';
// do this even if no outbound since people forget to remove mailboxes
[error, result] = await mailModel.stats(domain.domain);
if (error) console.error(error);
domain.mailboxCount = result.mailboxCount;
// this may temporarily fail while the mail server is restarting
while (true) {
const [error, result] = await mailModel.usage(domain.domain);
if (error && error.status === 424) {
await sleep(1000);
continue;
} else if (error) return console.error(error);
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; });
break;
}
domain.loadingUsage = false;
}
}
onMounted(async () => {
const [error, result] = await domainsModel.list();
if (error) return console.error(error);
result.forEach(d => {
d.loadingStatus = true;
d.status = {
state: ''
};
d.loadingUsage = true;
d.mailboxCount = 0;
d.usage = 0;
d.inboundEnabled = true;
d.outboundEnabled = true;
});
domains.value = result;
refreshStatus();
refreshUsage();
});
</script>
<template>
<div class="content">
<Section :title="$t('emails.domains.title')">
<template #header-title-extra>
<span style="font-weight: normal; font-size: 14px">({{ domains.length === 0 ? '-' : filteredDomains.length }})</span>
</template>
<template #header-buttons>
<TextInput :placeholder="$t('main.searchPlaceholder')" style="flex-grow: 1;" v-model="searchFilter"/>
</template>
<div>
<div v-if="domains.length !== 0 && filteredDomains.length === 0" class="email-placeholder">{{ $t('domains.noMatchesPlaceholder') }}</div>
<a v-for="domain in filteredDomains" :key="domain.domain" :href="`#/email-domain/${domain.domain}`" class="email-domain">
<div style="display: flex; align-items: center;">
<StateLED :busy="domain.loadingStatus" :state="domain.status ? 'success' : 'danger'"/>
</div>
<div class="email-domain-details">
<div><b style="font-size: 16px">{{ domain.domain }}</b></div>
<div v-if="domain.loadingUsage">
{{ $t('main.loadingPlaceholder') }} ...
</div>
<div v-else>
<div v-if="domain.inboundEnabled">
{{ $t('emails.domains.inbound') }}. Relayed via {{ prettyRelayProviderName(domain.relayProvider) }}. {{ $t('emails.domains.stats', { mailboxCount: domain.mailboxCount, usage: prettyDecimalSize(domain.usage) }) }}
</div>
<div v-else>
<span v-if="domain.outboundEnabled">{{ $t('emails.domains.outbound') }}. Relayed via {{ prettyRelayProviderName(domain.relayProvider) }}</span>
<span v-else>{{ $t('emails.domains.disabled') }}</span>
</div>
</div>
</div>
</a>
</div>
</Section>
</div>
</template>
<style scoped>
.email-placeholder {
text-align: center;
margin-top: 150px;
}
.email-domain {
display: flex;
border-radius: var(--pankow-border-radius);
padding: 10px;
gap: 10px;
margin-bottom: 10px;
color: var(--pankow-text-color);
}
.email-domain:hover {
background-color: var(--pankow-color-background-hover);
}
.email-domain-details {
flex-grow: 1;
}
</style>