327 lines
12 KiB
Vue
327 lines
12 KiB
Vue
<script setup>
|
|
|
|
import { useI18n } from 'vue-i18n';
|
|
const i18n = useI18n();
|
|
const t = i18n.t;
|
|
|
|
import { ref, onMounted, useTemplateRef } from 'vue';
|
|
import { Button, TableView, InputDialog, FormGroup, TextInput, InputGroup, Switch, ButtonGroup, SingleSelect } from 'pankow';
|
|
import { prettyDecimalSize } from 'pankow/utils';
|
|
import Section from '../components/Section.vue';
|
|
import SettingsItem from '../components/SettingsItem.vue';
|
|
import DomainsModel from '../models/DomainsModel.js';
|
|
import MailModel from '../models/MailModel.js';
|
|
import ProfileModel from '../models/ProfileModel.js';
|
|
|
|
const domainsModel = DomainsModel.create();
|
|
const mailModel = MailModel.create();
|
|
const profileModel = ProfileModel.create();
|
|
|
|
const inputDialog = useTemplateRef('inputDialog');
|
|
const columns = {
|
|
status: { sort: true },
|
|
domain: { label: t('emails.domains.domain'), sort: true },
|
|
config: { label: t('emails.domains.config') },
|
|
actions: {}
|
|
};
|
|
const domains = ref([]);
|
|
const profile = ref({});
|
|
const busy = ref(true);
|
|
const mailboxSharingEnabled = ref(false);
|
|
const virtualAllMailEnabled = ref(false);
|
|
const maxEmailSize = ref(0);
|
|
const currentMaxEmailSize = ref(0);
|
|
const ftsEnabled = ref(false);
|
|
const blocklist = ref([]);
|
|
const allowlist = ref([]);
|
|
const dnsblZones = ref([]);
|
|
const dnsblZonesString = ref('');
|
|
const locationSubdomain = ref('');
|
|
const locationDomain = ref('');
|
|
const currentLocationSubdomain = ref('');
|
|
const currentLocationDomain = ref('');
|
|
const spamCustomConfig = ref('');
|
|
|
|
async function refresh() {
|
|
busy.value = true;
|
|
|
|
const [error, result] = await domainsModel.list();
|
|
if (error) return console.error(error);
|
|
|
|
domains.value = result.map(domain => {
|
|
// used by ui to show 'loading'
|
|
domain.loading = true;
|
|
domain.loadingUsage = true;
|
|
domain.statusCheckDone = false;
|
|
|
|
return domain;
|
|
});
|
|
|
|
busy.value = false;
|
|
|
|
// individual status is fetched in background
|
|
refreshStatus();
|
|
}
|
|
|
|
async function refreshStatus() {
|
|
for (const domain of domains.value) {
|
|
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;
|
|
|
|
// TODO
|
|
// mail usage is loaded separately with a cancellation check. when there are a lot of domains, it runs a long time in background and slows down loading of new views
|
|
[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;
|
|
}
|
|
}
|
|
}
|
|
|
|
async function onSendTestMail(domain) {
|
|
const address = await inputDialog.value.prompt({
|
|
value: profile.value.email,
|
|
title: t('emails.testMailDialog.title', { domain: domain.domain }),
|
|
message: t('emails.testMailDialog.description', { domain: domain.domain }),
|
|
confirmLabel: t('emails.testMailDialog.sendAction'),
|
|
rejectLabel: t('main.dialog.cancel'),
|
|
rejectStyle: 'secondary'
|
|
});
|
|
|
|
if (!address) return;
|
|
|
|
const [error] = await mailModel.sendTestMail(domain.domain, address);
|
|
if (error) {
|
|
window.pankow.notify({ text: error.body ? error.body.message : 'Failed to send mail', type: 'danger' });
|
|
return console.error(error);
|
|
}
|
|
|
|
window.pankow.notify({ text: 'Mail sent', type: 'success' });
|
|
}
|
|
|
|
async function onChangeMailboxSharing(value) {
|
|
const [error] = await mailModel.setMailboxSharing(value);
|
|
if (error) {
|
|
mailboxSharingEnabled.value = !value;
|
|
return console.error(error);
|
|
}
|
|
}
|
|
|
|
async function onChangeVirtualAllMail(value) {
|
|
const [error] = await mailModel.setVirtualAllMail(value);
|
|
if (error) {
|
|
virtualAllMailEnabled.value = !value;
|
|
return console.error(error);
|
|
}
|
|
}
|
|
|
|
async function onChangeFts(value) {
|
|
const [error] = await mailModel.setFtsConfig(value);
|
|
if (error) {
|
|
ftsEnabled.value = !value;
|
|
return console.error(error);
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
let [error, result] = await profileModel.get();
|
|
if (error) return console.error(error);
|
|
profile.value = result;
|
|
|
|
await refresh();
|
|
|
|
[error, result] = await mailModel.location();
|
|
if (error) return console.error(error);
|
|
locationDomain.value = result.domain;
|
|
locationSubdomain.value = result.subdomain;
|
|
currentLocationDomain.value = result.domain;
|
|
currentLocationSubdomain.value = result.subdomain;
|
|
|
|
[error, result] = await mailModel.mailboxSharing();
|
|
if (error) return console.error(error);
|
|
mailboxSharingEnabled.value = result;
|
|
|
|
[error, result] = await mailModel.virtualAllMail();
|
|
if (error) return console.error(error);
|
|
virtualAllMailEnabled.value = result;
|
|
|
|
[error, result] = await mailModel.maxEmailSize();
|
|
if (error) return console.error(error);
|
|
maxEmailSize.value = result;
|
|
currentMaxEmailSize.value = result;
|
|
|
|
[error, result] = await mailModel.dnsblConfig();
|
|
if (error) return console.error(error);
|
|
dnsblZones.value = result.join('\n');
|
|
dnsblZonesString.value = result;
|
|
|
|
[error, result] = await mailModel.spamCustomConfig();
|
|
if (error) return console.error(error);
|
|
spamCustomConfig.value = result;
|
|
|
|
[error, result] = await mailModel.spamAcl();
|
|
if (error) return console.error(error);
|
|
allowlist.value = result.allowlist;
|
|
blocklist.value = result.blocklist;
|
|
|
|
[error, result] = await mailModel.ftsConfig();
|
|
if (error) return console.error(error);
|
|
ftsEnabled.value = result;
|
|
|
|
// TODO check mail service config for memory allocation for fts
|
|
});
|
|
|
|
</script>
|
|
|
|
<template>
|
|
<div class="content">
|
|
<InputDialog ref="inputDialog"/>
|
|
|
|
<h1 class="section-header">
|
|
{{ $t('emails.title') }}
|
|
<div style="display: flex; gap: 4px; flex-wrap: wrap; margin-top: 10px;">
|
|
<Button href="#/emails-queue">{{ $t('emails.action.queue') }}</Button>
|
|
<Button href="#/emails-eventlog">{{ $t('eventlog.title') }}</Button>
|
|
</div>
|
|
</h1>
|
|
|
|
<Section :title="$t('emails.domains.title')">
|
|
<TableView :columns="columns" :model="domains" :busy="busy">
|
|
<template #status="domain">
|
|
<i class="fa fa-circle" :class="{ 'status-active': domain.status, 'status-error': !domain.status }" v-if="domain.statusCheckDone"></i>
|
|
<i class="fa fa-circle-notch fa-spin" v-if="!domain.statusCheckDone"></i>
|
|
</template>
|
|
<template #domain="domain">
|
|
<a :href="`/#/email/${domain.domain}`">{{ domain.domain }}</a>
|
|
</template>
|
|
<template #config="domain">
|
|
<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>
|
|
</template>
|
|
<template #actions="domain">
|
|
<div class="table-actions">
|
|
<ButtonGroup>
|
|
<Button tool small secondary @click="onSendTestMail(domain)" v-tooltip="$t('emails.domains.testEmailTooltip')" icon="fa-solid fa-paper-plane"/>
|
|
<Button tool small secondary :href="`/#/email/${domain.domain}`" icon="fa-solid fa-pencil-alt"/>
|
|
</ButtonGroup>
|
|
</div>
|
|
</template>
|
|
</TableView>
|
|
</Section>
|
|
|
|
<Section :title="$t('emails.settings.title')">
|
|
<SettingsItem>
|
|
<FormGroup>
|
|
<label>{{ $t('emails.settings.location') }}</label>
|
|
<div v-html="$t('emails.changeDomainDialog.description')"></div>
|
|
</FormGroup>
|
|
<div style="display: flex; gap: 6px; align-items: center;">
|
|
<InputGroup>
|
|
<TextInput v-model="locationSubdomain" />
|
|
<SingleSelect v-model="locationDomain" :options="domains" option-key="domain" option-label="domain"/>
|
|
</InputGroup>
|
|
<Button tool :plain="(currentLocationSubdomain !== locationSubdomain || currentLocationDomain !== locationDomain) ? null : true" :disabled="currentLocationSubdomain === locationSubdomain && currentLocationDomain === locationDomain">{{ $t('main.dialog.save') }}</Button>
|
|
</div>
|
|
</SettingsItem>
|
|
|
|
<SettingsItem>
|
|
<FormGroup>
|
|
<label>{{ $t('emails.mailboxSharing.title') }}</label>
|
|
<div>{{ $t('emails.mailboxSharing.description') }}</div>
|
|
</FormGroup>
|
|
<Switch v-model="mailboxSharingEnabled" @change="onChangeMailboxSharing"/>
|
|
</SettingsItem>
|
|
|
|
<SettingsItem>
|
|
<FormGroup>
|
|
<label>{{ $t('emails.settings.virtualAllMail') }}</label>
|
|
<div v-html="$t('emails.changeVirtualAllMailDialog.description')"></div>
|
|
</FormGroup>
|
|
<Switch v-model="virtualAllMailEnabled" @change="onChangeVirtualAllMail"/>
|
|
</SettingsItem>
|
|
|
|
<SettingsItem>
|
|
<FormGroup>
|
|
<label for="maxEmailSizeInput">{{ $t('emails.settings.maxMailSize') }}</label>
|
|
<div v-html="$t('emails.changeMailSizeDialog.description')"></div>
|
|
</FormGroup>
|
|
<div style="display: flex; gap: 6px; align-items: center;">
|
|
{{ prettyDecimalSize(maxEmailSize) }}
|
|
<input style="width: 200px" type="range" id="maxEmailSizeInput" v-model="maxEmailSize" step="1000000" min="1000000" max="1000000000" />
|
|
<Button tool :plain="currentMaxEmailSize !== maxEmailSize ? null : true" :disabled="currentMaxEmailSize === maxEmailSize">{{ $t('main.dialog.save') }}</Button>
|
|
</div>
|
|
</SettingsItem>
|
|
|
|
<SettingsItem>
|
|
<FormGroup>
|
|
<label>{{ $t('emails.settings.solrFts') }}</label>
|
|
<div v-html="$t('emails.solrConfig.description')"></div>
|
|
</FormGroup>
|
|
<Switch v-model="ftsEnabled" @change="onChangeFts"/>
|
|
</SettingsItem>
|
|
|
|
<SettingsItem>
|
|
<FormGroup>
|
|
<label>{{ $t('emails.settings.acl') }}</label>
|
|
<div>{{ $t('emails.settings.aclOverview', { dnsblZonesCount: dnsblZones.length }) }}</div>
|
|
</FormGroup>
|
|
<div style="display: flex; align-items: center">
|
|
<Button tool plain>Edit</Button>
|
|
</div>
|
|
</SettingsItem>
|
|
|
|
<SettingsItem>
|
|
<FormGroup>
|
|
<label>{{ $t('emails.settings.spamFilter') }}</label>
|
|
<div>{{ $t('emails.settings.spamFilterOverview', { blacklistCount: blocklist.length }) }}</div>
|
|
</FormGroup>
|
|
<div style="display: flex; align-items: center">
|
|
<Button tool plain>Edit</Button>
|
|
</div>
|
|
</SettingsItem>
|
|
|
|
</Section>
|
|
</div>
|
|
</template>
|