Split up Email views
This commit is contained in:
@@ -5,12 +5,12 @@ const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, onMounted, useTemplateRef } from 'vue';
|
||||
import { Button, Checkbox, InputDialog, Dialog, FormGroup, Switch } from 'pankow';
|
||||
import { Button, Checkbox, InputDialog, Dialog, FormGroup, Switch, SingleSelect } from 'pankow';
|
||||
import { prettyDecimalSize, sleep } from 'pankow/utils';
|
||||
import Section from '../components/Section.vue';
|
||||
import SettingsItem from '../components/SettingsItem.vue';
|
||||
import CatchAllSettingsItem from '../components/CatchAllSettingsItem.vue';
|
||||
import MailRelaySettingsItem from '../components/MailRelaySettingsItem.vue';
|
||||
import MailDomainStatus from '../components/MailDomainStatus.vue';
|
||||
import DashboardModel from '../models/DashboardModel.js';
|
||||
import DomainsModel from '../models/DomainsModel.js';
|
||||
import MailModel from '../models/MailModel.js';
|
||||
@@ -22,15 +22,43 @@ const mailModel = MailModel.create();
|
||||
const inputDialog = useTemplateRef('inputDialog');
|
||||
const enableIncomingDialog = useTemplateRef('enableIncomingDialog');
|
||||
const connectionDialog = useTemplateRef('connectionDialog');
|
||||
const domains = ref([]);
|
||||
const domain = ref('');
|
||||
const domainProvider = ref('');
|
||||
const mailConfig = ref({});
|
||||
const mailFqdn = ref('');
|
||||
const refreshBusy = ref(false);
|
||||
const incomingEnabled = ref(false);
|
||||
const enableIncomeBusy = ref(false);
|
||||
const enableIncomingSetupDns = ref(false);
|
||||
const infoMenu = ref([]);
|
||||
const busyRefresh = ref(true);
|
||||
const mailboxCount = ref('');
|
||||
const inboundEnabled = ref(false);
|
||||
const outboundEnabled = ref(false);
|
||||
const usage = ref(0);
|
||||
|
||||
// TODO
|
||||
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' });
|
||||
}
|
||||
|
||||
|
||||
function onShowConnectionDialog() {
|
||||
connectionDialog.value.open();
|
||||
@@ -57,7 +85,7 @@ async function onAskIncomingToggle(value) {
|
||||
const [error] = await mailModel.setEnabled(domain.value, false);
|
||||
if (error) return console.error(error);
|
||||
|
||||
await refreshMailConfig();
|
||||
await onDomainChanged();
|
||||
}
|
||||
|
||||
async function onEnableIncoming() {
|
||||
@@ -73,7 +101,7 @@ async function onEnableIncoming() {
|
||||
if (error) console.error(error);
|
||||
}
|
||||
|
||||
await refreshMailConfig();
|
||||
await onDomainChanged();
|
||||
|
||||
enableIncomingDialog.value.close();
|
||||
enableIncomeBusy.value = false;
|
||||
@@ -117,14 +145,20 @@ async function onSubmitSignature() {
|
||||
signatureBusy.value = false;
|
||||
}
|
||||
|
||||
async function onDomainChanged() {
|
||||
busyRefresh.value = true;
|
||||
|
||||
async function refreshMailConfig() {
|
||||
refreshBusy.value = true;
|
||||
let [error, result] = await domainsModel.get(domain.value);
|
||||
if (error) return console.error(error);
|
||||
|
||||
const [error, result] = await mailModel.config(domain.value);
|
||||
domainProvider.value = result.provider;
|
||||
|
||||
[error, result] = await mailModel.config(domain.value);
|
||||
if (error) return console.error(error);
|
||||
|
||||
mailConfig.value = result;
|
||||
inboundEnabled.value = result.enabled;
|
||||
outboundEnabled.value = result.relay?.provider !== 'noop';
|
||||
incomingEnabled.value = result.enabled;
|
||||
mailFromValidation.value = result.mailFromValidation;
|
||||
signatureText.value = mailConfig.value.banner.text || '';
|
||||
@@ -140,24 +174,44 @@ async function refreshMailConfig() {
|
||||
action: onShowConnectionDialog
|
||||
}];
|
||||
|
||||
refreshBusy.value = false;
|
||||
// do this even if no outbound since people forget to remove mailboxes
|
||||
[error, result] = await mailModel.mailboxCount(domain.value);
|
||||
if (error) console.error(error);
|
||||
|
||||
mailboxCount.value = result;
|
||||
|
||||
// this may temporarily fail while the mail server is restarting
|
||||
while (true) {
|
||||
const [error, result] = await mailModel.usage(domain.value);
|
||||
if (error && error.status === 424) {
|
||||
await sleep(1000);
|
||||
continue;
|
||||
} else if (error) return console.error(error);
|
||||
|
||||
usage.value = 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) { usage.value += result[m].diskSize; });
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
busyRefresh.value = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
domain.value = window.location.hash.slice('#/email/'.length);
|
||||
if (!domain.value) return;
|
||||
|
||||
let [error, result] = await domainsModel.get(domain.value);
|
||||
if (error) return console.error(error);
|
||||
|
||||
domainProvider.value = result.provider;
|
||||
|
||||
[error, result] = await dashboardModel.config();
|
||||
let [error, result] = await dashboardModel.config();
|
||||
if (error) return console.error(error);
|
||||
|
||||
mailFqdn.value = result.mailFqdn;
|
||||
|
||||
await refreshMailConfig();
|
||||
[error, result] = await domainsModel.list();
|
||||
if (error) return console.error(error);
|
||||
|
||||
domains.value = result;
|
||||
domain.value = domains.value[0].domain;
|
||||
|
||||
await onDomainChanged();
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -231,22 +285,24 @@ onMounted(async () => {
|
||||
|
||||
<Section :title="$t('email.config.title', { domain: domain })">
|
||||
<template #header-buttons>
|
||||
<SingleSelect v-if="domains.length" v-model="domain" :options="domains" option-key="domain" option-label="domain" @select="onDomainChanged"/>
|
||||
<Button secondary tool icon="fa-solid fa-book" v-tooltip="$t('app.docsActionTooltip')" :menu="infoMenu" />
|
||||
</template>
|
||||
|
||||
<!-- v-if here ensures the prop is already set when passed down to MailDomainStatus -->
|
||||
<MailDomainStatus v-if="domain" :domain="domain"/>
|
||||
<div v-if="busyRefresh">{{ $t('main.loadingPlaceholder') }} ...</div>
|
||||
<div v-else>
|
||||
<div v-if="inboundEnabled">
|
||||
{{ $t('emails.domains.stats', { mailboxCount: mailboxCount, usage: prettyDecimalSize(usage) }) }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<span v-if="outboundEnabled">{{ $t('emails.domains.outbound') }}</span>
|
||||
<span v-else>{{ $t('emails.domains.disabled') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</Section>
|
||||
|
||||
<Section :title="$t('emails.settings.title')" :padding="false">
|
||||
<SettingsItem>
|
||||
<FormGroup>
|
||||
<label>{{ $t('email.incoming.title') }}</label>
|
||||
<div>{{ $t(mailConfig.enabled ? 'email.incoming.enabled' : 'email.incoming.disabled') }}</div>
|
||||
</FormGroup>
|
||||
<Switch v-model="incomingEnabled" :disabled="refreshBusy" @change="onAskIncomingToggle"/>
|
||||
</SettingsItem>
|
||||
|
||||
<Section title="Sending" :padding="false" v-if="!busyRefresh">
|
||||
<MailRelaySettingsItem v-if="domain" :domain="domain"/>
|
||||
|
||||
<SettingsItem>
|
||||
@@ -256,6 +312,16 @@ onMounted(async () => {
|
||||
</FormGroup>
|
||||
<Switch v-model="mailFromValidation" @change="onToggleMailFromValidation" :disabled="mailFromValidationBusy"/>
|
||||
</SettingsItem>
|
||||
</Section>
|
||||
|
||||
<Section title="Receiving" :padding="false" v-if="!busyRefresh">
|
||||
<SettingsItem>
|
||||
<FormGroup>
|
||||
<label>{{ $t('email.incoming.title') }}</label>
|
||||
<div>{{ $t(mailConfig.enabled ? 'email.incoming.enabled' : 'email.incoming.disabled') }}</div>
|
||||
</FormGroup>
|
||||
<Switch v-model="incomingEnabled" :disabled="busyRefresh" @change="onAskIncomingToggle"/>
|
||||
</SettingsItem>
|
||||
|
||||
<CatchAllSettingsItem :domain-config="mailConfig" />
|
||||
|
||||
|
||||
@@ -1,133 +1,21 @@
|
||||
|
||||
<script setup>
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
const i18n = useI18n();
|
||||
const t = i18n.t;
|
||||
|
||||
import { ref, onMounted, useTemplateRef } from 'vue';
|
||||
import { Button, TableView, ProgressBar, InputDialog, Dialog, FormGroup, TextInput, InputGroup, Switch, ButtonGroup, SingleSelect } from 'pankow';
|
||||
import { Button, ProgressBar, InputDialog, Dialog, FormGroup, TextInput, InputGroup, Switch, SingleSelect } from 'pankow';
|
||||
import { prettyDecimalSize } from 'pankow/utils';
|
||||
import Section from '../components/Section.vue';
|
||||
import StateLED from '../components/StateLED.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';
|
||||
import TasksModel from '../models/TasksModel.js';
|
||||
|
||||
const domainsModel = DomainsModel.create();
|
||||
const mailModel = MailModel.create();
|
||||
const profileModel = ProfileModel.create();
|
||||
const tasksModel = TasksModel.create();
|
||||
|
||||
const inputDialog = useTemplateRef('inputDialog');
|
||||
const columns = {
|
||||
status: {
|
||||
width: '32px',
|
||||
sort: true
|
||||
},
|
||||
domain: {
|
||||
label: t('emails.domains.domain'),
|
||||
sort: true
|
||||
},
|
||||
config: {
|
||||
label: t('emails.domains.config'),
|
||||
hideMobile: true,
|
||||
},
|
||||
actions: {}
|
||||
};
|
||||
const domains = ref([]);
|
||||
const profile = ref({});
|
||||
const busy = ref(true);
|
||||
|
||||
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
|
||||
for (const domain of domains.value) refreshStatus(domain);
|
||||
}
|
||||
|
||||
async function refreshStatus(domain) {
|
||||
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' });
|
||||
}
|
||||
|
||||
|
||||
const currentLocationSubdomain = ref('');
|
||||
const currentLocationDomain = ref('');
|
||||
@@ -281,11 +169,10 @@ async function onSubmitSpamFilter() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
let [error, result] = await profileModel.get();
|
||||
let [error, result] = await domainsModel.list();
|
||||
if (error) return console.error(error);
|
||||
profile.value = result;
|
||||
|
||||
await refresh();
|
||||
domains.value = result;
|
||||
|
||||
[error, result] = await mailModel.location();
|
||||
if (error) return console.error(error);
|
||||
@@ -385,45 +272,6 @@ onMounted(async () => {
|
||||
</form>
|
||||
</Dialog>
|
||||
|
||||
<!-- <Section :title="$t('emails.domains.title')"> -->
|
||||
<Section :title="$t('emails.title')">
|
||||
<template #header-buttons>
|
||||
<Button href="#/emails-eventlog">{{ $t('eventlog.title') }}</Button>
|
||||
</template>
|
||||
|
||||
<!-- TODO new section for test mail sending -->
|
||||
|
||||
<TableView :columns="columns" :model="domains" :busy="busy">
|
||||
<template #status="domain">
|
||||
<StateLED :busy="!domain.statusCheckDone" :state="domain.status ? 'success' : 'danger'"/>
|
||||
</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')" :padding="false">
|
||||
<SettingsItem wrap>
|
||||
<FormGroup style="flex-grow: 1;">
|
||||
@@ -0,0 +1,134 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { eachLimit } from 'async';
|
||||
import { Button } from 'pankow';
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
<div @click="onToggleExpanded(domain.domain)" style="display: flex; justify-content: space-between;">
|
||||
<div>
|
||||
<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;
|
||||
padding: 10px 0;
|
||||
border-radius: var(--pankow-border-radius);
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.domain-list-item.active,
|
||||
.domain-list-item:hover {
|
||||
background-color: var(--pankow-color-background-hover);
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user