2025-04-16 10:45:17 +02:00
|
|
|
|
2025-03-07 19:47:58 +01:00
|
|
|
<script setup>
|
|
|
|
|
|
|
|
|
|
import { ref, onMounted, useTemplateRef } from 'vue';
|
2025-04-16 17:59:03 +02:00
|
|
|
import { Button, ProgressBar, InputDialog, Dialog, FormGroup, TextInput, InputGroup, Switch, SingleSelect } from 'pankow';
|
2025-03-07 19:47:58 +01:00
|
|
|
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';
|
2025-03-10 11:18:40 +01:00
|
|
|
import TasksModel from '../models/TasksModel.js';
|
2025-03-07 19:47:58 +01:00
|
|
|
|
|
|
|
|
const domainsModel = DomainsModel.create();
|
|
|
|
|
const mailModel = MailModel.create();
|
2025-03-10 11:18:40 +01:00
|
|
|
const tasksModel = TasksModel.create();
|
2025-03-07 19:47:58 +01:00
|
|
|
|
2025-03-09 20:27:41 +01:00
|
|
|
const inputDialog = useTemplateRef('inputDialog');
|
2025-03-07 19:47:58 +01:00
|
|
|
const domains = ref([]);
|
2025-03-10 11:18:40 +01:00
|
|
|
|
2025-03-10 11:37:50 +01:00
|
|
|
const currentLocationSubdomain = ref('');
|
|
|
|
|
const currentLocationDomain = ref('');
|
2025-03-10 11:18:40 +01:00
|
|
|
const locationSubdomain = ref('');
|
|
|
|
|
const locationDomain = ref('');
|
|
|
|
|
const locationChangeBusy = ref(false);
|
|
|
|
|
const locationChangeMessage = ref('');
|
|
|
|
|
const locationChangeProgress = ref(0);
|
|
|
|
|
|
|
|
|
|
async function onChangeMailDomain() {
|
|
|
|
|
locationChangeBusy.value = true;
|
|
|
|
|
locationChangeMessage.value = '';
|
|
|
|
|
locationChangeProgress.value = 0;
|
|
|
|
|
|
|
|
|
|
const [error, result] = await mailModel.setLocation(locationSubdomain.value, locationDomain.value);
|
|
|
|
|
if (error) return console.error(error);
|
|
|
|
|
|
|
|
|
|
await tasksModel.wait(result, (result) => {
|
|
|
|
|
locationChangeMessage.value = result.message;
|
|
|
|
|
locationChangeProgress.value = result.percent;
|
|
|
|
|
});
|
|
|
|
|
|
2025-03-10 12:44:31 +01:00
|
|
|
currentLocationDomain.value = locationDomain.value;
|
|
|
|
|
currentLocationSubdomain.value = locationSubdomain.value;
|
2025-03-10 11:18:40 +01:00
|
|
|
locationChangeBusy.value = false;
|
|
|
|
|
locationChangeMessage.value = '';
|
|
|
|
|
locationChangeProgress.value = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-03-10 11:37:50 +01:00
|
|
|
const maxEmailSizeBusy = ref(false);
|
|
|
|
|
const maxEmailSize = ref(0);
|
|
|
|
|
const currentMaxEmailSize = ref(0);
|
|
|
|
|
|
|
|
|
|
async function onChangeMaxEmailSize() {
|
|
|
|
|
maxEmailSizeBusy.value = true;
|
|
|
|
|
|
|
|
|
|
const [error] = await mailModel.setMaxEmailSize(parseInt(maxEmailSize.value));
|
|
|
|
|
if (error) return console.error(error);
|
|
|
|
|
|
|
|
|
|
currentMaxEmailSize.value = maxEmailSize.value;
|
|
|
|
|
maxEmailSizeBusy.value = false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-10 12:44:31 +01:00
|
|
|
|
|
|
|
|
const aclDialog = useTemplateRef('aclDialog');
|
|
|
|
|
const dnsblZonesError = ref('');
|
|
|
|
|
const dnsblZonesBusy = ref(false);
|
|
|
|
|
const dnsblZones = ref([]);
|
|
|
|
|
const dnsblZonesString = ref('');
|
|
|
|
|
|
|
|
|
|
function onShowAclDialog() {
|
|
|
|
|
dnsblZonesError.value = '';
|
|
|
|
|
dnsblZonesBusy.value = false;
|
|
|
|
|
aclDialog.value.open();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onSubmitAcl() {
|
|
|
|
|
dnsblZonesError.value = '';
|
|
|
|
|
dnsblZonesBusy.value = true;
|
|
|
|
|
|
|
|
|
|
const zones = dnsblZonesString.value.split('\n').filter((l) => { return l !== ''; });
|
|
|
|
|
|
|
|
|
|
const [error] = await mailModel.setDnsblConfig(zones);
|
|
|
|
|
if (error) {
|
|
|
|
|
dnsblZonesError.value = error.body ? error.body.message : 'Internal error';
|
|
|
|
|
dnsblZonesBusy.value = false;
|
|
|
|
|
return console.error(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
aclDialog.value.close();
|
|
|
|
|
|
|
|
|
|
dnsblZones.value = zones;
|
|
|
|
|
dnsblZonesBusy.value = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const mailboxSharingEnabled = ref(false);
|
|
|
|
|
|
2025-03-09 20:55:58 +01:00
|
|
|
async function onChangeMailboxSharing(value) {
|
|
|
|
|
const [error] = await mailModel.setMailboxSharing(value);
|
|
|
|
|
if (error) {
|
|
|
|
|
mailboxSharingEnabled.value = !value;
|
|
|
|
|
return console.error(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-10 12:44:31 +01:00
|
|
|
|
|
|
|
|
const virtualAllMailEnabled = ref(false);
|
|
|
|
|
|
2025-03-09 20:55:58 +01:00
|
|
|
async function onChangeVirtualAllMail(value) {
|
|
|
|
|
const [error] = await mailModel.setVirtualAllMail(value);
|
|
|
|
|
if (error) {
|
|
|
|
|
virtualAllMailEnabled.value = !value;
|
|
|
|
|
return console.error(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-10 12:44:31 +01:00
|
|
|
|
|
|
|
|
const ftsEnabled = ref(false);
|
|
|
|
|
|
2025-03-09 20:55:58 +01:00
|
|
|
async function onChangeFts(value) {
|
|
|
|
|
const [error] = await mailModel.setFtsConfig(value);
|
|
|
|
|
if (error) {
|
|
|
|
|
ftsEnabled.value = !value;
|
|
|
|
|
return console.error(error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-10 12:44:31 +01:00
|
|
|
|
|
|
|
|
const spamFilterDialog = useTemplateRef('spamFilterDialog');
|
|
|
|
|
const spamFilterBusy = ref(false);
|
|
|
|
|
const spamFilterError = ref('');
|
|
|
|
|
const blocklist = ref([]);
|
|
|
|
|
const allowlist = ref([]);
|
|
|
|
|
const spamCustomConfig = ref('');
|
|
|
|
|
|
|
|
|
|
function onShowSpamFilterDialog() {
|
|
|
|
|
spamFilterError.value = '';
|
|
|
|
|
spamFilterBusy.value = false;
|
|
|
|
|
|
|
|
|
|
spamFilterDialog.value.open();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onSubmitSpamFilter() {
|
|
|
|
|
spamFilterError.value = '';
|
|
|
|
|
spamFilterBusy.value = true;
|
|
|
|
|
|
|
|
|
|
const block = blocklist.value.split('\n').filter((l) => { return l !== ''; });
|
|
|
|
|
const allow = allowlist.value.split('\n').filter((l) => { return l !== ''; });
|
|
|
|
|
|
|
|
|
|
let [error] = await mailModel.setSpamAcl(allow, block);
|
|
|
|
|
if (error) {
|
|
|
|
|
spamFilterError.value = error.body ? error.body.message : 'Internal error';
|
|
|
|
|
spamFilterBusy.value = false;
|
|
|
|
|
return console.error(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[error] = await mailModel.setSpamCustomConfig(spamCustomConfig.value);
|
|
|
|
|
if (error) {
|
|
|
|
|
spamFilterError.value = error.body ? error.body.message : 'Internal error';
|
|
|
|
|
spamFilterBusy.value = false;
|
|
|
|
|
return console.error(error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
spamFilterDialog.value.close();
|
|
|
|
|
|
|
|
|
|
blocklist.value = block.join('\n');
|
|
|
|
|
allowlist.value = allow.join('\n');
|
|
|
|
|
spamFilterBusy.value = false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-07 19:47:58 +01:00
|
|
|
onMounted(async () => {
|
2025-04-16 17:59:03 +02:00
|
|
|
let [error, result] = await domainsModel.list();
|
2025-03-07 19:47:58 +01:00
|
|
|
if (error) return console.error(error);
|
|
|
|
|
|
2025-04-16 17:59:03 +02:00
|
|
|
domains.value = result;
|
2025-03-07 19:47:58 +01:00
|
|
|
|
|
|
|
|
[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);
|
2025-03-10 12:44:31 +01:00
|
|
|
dnsblZones.value = result;
|
|
|
|
|
dnsblZonesString.value = result.join('\n');
|
2025-03-07 19:47:58 +01:00
|
|
|
|
|
|
|
|
[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);
|
2025-03-10 12:44:31 +01:00
|
|
|
allowlist.value = result.allowlist.join('\n');
|
|
|
|
|
blocklist.value = result.blocklist.join('\n');
|
2025-03-07 19:47:58 +01:00
|
|
|
|
|
|
|
|
[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">
|
2025-03-09 20:27:41 +01:00
|
|
|
<InputDialog ref="inputDialog"/>
|
|
|
|
|
|
2025-03-10 12:44:31 +01:00
|
|
|
<Dialog ref="aclDialog"
|
|
|
|
|
:title="$t('emails.aclDialog.title')"
|
|
|
|
|
:reject-label="dnsblZonesBusy ? '' : $t('main.dialog.cancel')"
|
|
|
|
|
reject-style="secondary"
|
|
|
|
|
:confirm-label="$t('main.dialog.save')"
|
|
|
|
|
:confirm-busy="dnsblZonesBusy"
|
|
|
|
|
@confirm="onSubmitAcl()"
|
|
|
|
|
>
|
|
|
|
|
<form @submit.prevent="onSubmitAcl()" autocomplete="off">
|
|
|
|
|
<fieldset :disabled="dnsblZonesBusy">
|
|
|
|
|
<input type="submit" style="display: none"/>
|
|
|
|
|
|
|
|
|
|
<FormGroup>
|
|
|
|
|
<label for="dnsblZonesInput">{{ $t('emails.aclDialog.dnsblZones') }} <sup><a href="https://docs.cloudron.io/email/#dnsbl" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
|
|
|
|
<p class="small">{{ $t('emails.aclDialog.dnsblZonesInfo') }}</p>
|
|
|
|
|
<textarea id="dnsblZonesInput" v-model="dnsblZonesString" :placeholder="$t('emails.aclDialog.dnsblZonesPlaceholder')" rows="4"></textarea>
|
|
|
|
|
</FormGroup>
|
|
|
|
|
|
|
|
|
|
<div class="has-error" v-if="dnsblZonesError">{{ dnsblZonesError }}</div>
|
|
|
|
|
</fieldset>
|
|
|
|
|
</form>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
<Dialog ref="spamFilterDialog"
|
|
|
|
|
:title="$t('emails.spamFilterDialog.title')"
|
|
|
|
|
:reject-label="spamFilterBusy ? '' : $t('main.dialog.cancel')"
|
|
|
|
|
reject-style="secondary"
|
|
|
|
|
:confirm-label="$t('main.dialog.save')"
|
|
|
|
|
:confirm-busy="spamFilterBusy"
|
|
|
|
|
@confirm="onSubmitSpamFilter()"
|
|
|
|
|
>
|
|
|
|
|
<form @submit.prevent="onSubmitSpamFilter()" autocomplete="off">
|
|
|
|
|
<fieldset :disabled="spamFilterBusy">
|
|
|
|
|
<input type="submit" style="display: none"/>
|
|
|
|
|
|
|
|
|
|
<FormGroup>
|
|
|
|
|
<label for="blocklistInput">{{ $t('emails.spamFilterDialog.blacklisteAddresses') }}</label>
|
|
|
|
|
<p class="small">{{ $t('emails.spamFilterDialog.blacklisteAddressesInfo') }}</p>
|
|
|
|
|
<textarea id="blocklistInput" v-model="blocklist" :placeholder="$t('emails.spamFilterDialog.blacklisteAddressesPlaceholder')" rows="4"></textarea>
|
|
|
|
|
</FormGroup>
|
|
|
|
|
|
|
|
|
|
<FormGroup>
|
|
|
|
|
<label for="customConfigInput">{{ $t('emails.spamFilterDialog.customRules') }} <sup><a href="https://docs.cloudron.io/email/#custom-spam-filtering-rules" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
|
|
|
|
<textarea id="customConfigInput" v-model="spamCustomConfig" :placeholder="$t('emails.spamFilterDialog.customRulesPlaceholder')" rows="4"></textarea>
|
|
|
|
|
</FormGroup>
|
|
|
|
|
|
|
|
|
|
<div class="has-error" v-if="spamFilterError">{{ spamFilterError }}</div>
|
|
|
|
|
</fieldset>
|
|
|
|
|
</form>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
2025-03-17 22:17:30 +01:00
|
|
|
<Section :title="$t('emails.settings.title')" :padding="false">
|
2025-03-26 16:04:58 +01:00
|
|
|
<SettingsItem wrap>
|
2025-03-10 11:18:40 +01:00
|
|
|
<FormGroup style="flex-grow: 1;">
|
2025-03-07 19:47:58 +01:00
|
|
|
<label>{{ $t('emails.settings.location') }}</label>
|
|
|
|
|
<div v-html="$t('emails.changeDomainDialog.description')"></div>
|
|
|
|
|
</FormGroup>
|
2025-03-26 16:04:58 +01:00
|
|
|
<div style="display: flex; gap: 6px; align-items: center; flex-wrap: wrap;">
|
2025-03-07 19:47:58 +01:00
|
|
|
<InputGroup>
|
2025-03-10 11:18:40 +01:00
|
|
|
<TextInput v-model="locationSubdomain" :disabled="locationChangeBusy"/>
|
|
|
|
|
<SingleSelect v-model="locationDomain" :options="domains" option-key="domain" option-label="domain" :disabled="locationChangeBusy"/>
|
2025-03-07 19:47:58 +01:00
|
|
|
</InputGroup>
|
2025-03-10 11:18:40 +01:00
|
|
|
<Button tool @click="onChangeMailDomain()" :plain="(currentLocationSubdomain !== locationSubdomain || currentLocationDomain !== locationDomain) ? null : true" :disabled="locationChangeBusy || (currentLocationSubdomain === locationSubdomain && currentLocationDomain === locationDomain)">{{ $t('main.dialog.save') }}</Button>
|
2025-03-07 19:47:58 +01:00
|
|
|
</div>
|
2025-03-10 11:18:40 +01:00
|
|
|
<template #bottom v-if="locationChangeProgress">
|
|
|
|
|
<ProgressBar :value="locationChangeProgress"/>
|
2025-03-10 12:44:31 +01:00
|
|
|
<div style="padding-top: 6px">{{ locationChangeMessage }}</div>
|
2025-03-10 11:18:40 +01:00
|
|
|
</template>
|
2025-03-07 19:47:58 +01:00
|
|
|
</SettingsItem>
|
|
|
|
|
|
|
|
|
|
<SettingsItem>
|
|
|
|
|
<FormGroup>
|
|
|
|
|
<label>{{ $t('emails.mailboxSharing.title') }}</label>
|
|
|
|
|
<div>{{ $t('emails.mailboxSharing.description') }}</div>
|
|
|
|
|
</FormGroup>
|
2025-03-09 20:55:58 +01:00
|
|
|
<Switch v-model="mailboxSharingEnabled" @change="onChangeMailboxSharing"/>
|
2025-03-07 19:47:58 +01:00
|
|
|
</SettingsItem>
|
|
|
|
|
|
|
|
|
|
<SettingsItem>
|
|
|
|
|
<FormGroup>
|
|
|
|
|
<label>{{ $t('emails.settings.virtualAllMail') }}</label>
|
|
|
|
|
<div v-html="$t('emails.changeVirtualAllMailDialog.description')"></div>
|
|
|
|
|
</FormGroup>
|
2025-03-09 20:55:58 +01:00
|
|
|
<Switch v-model="virtualAllMailEnabled" @change="onChangeVirtualAllMail"/>
|
2025-03-07 19:47:58 +01:00
|
|
|
</SettingsItem>
|
|
|
|
|
|
2025-03-26 16:04:58 +01:00
|
|
|
<SettingsItem wrap>
|
2025-03-07 19:47:58 +01:00
|
|
|
<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) }}
|
2025-03-10 11:37:50 +01:00
|
|
|
<input style="width: 200px" type="range" id="maxEmailSizeInput" v-model="maxEmailSize" step="1000000" min="1000000" max="1000000000" :disabled="maxEmailSizeBusy" />
|
|
|
|
|
<Button @click="onChangeMaxEmailSize()" tool :plain="currentMaxEmailSize !== maxEmailSize ? null : true" :loading="maxEmailSizeBusy" :disabled="maxEmailSizeBusy || currentMaxEmailSize === maxEmailSize">{{ $t('main.dialog.save') }}</Button>
|
2025-03-07 19:47:58 +01:00
|
|
|
</div>
|
|
|
|
|
</SettingsItem>
|
|
|
|
|
|
|
|
|
|
<SettingsItem>
|
|
|
|
|
<FormGroup>
|
|
|
|
|
<label>{{ $t('emails.settings.solrFts') }}</label>
|
|
|
|
|
<div v-html="$t('emails.solrConfig.description')"></div>
|
|
|
|
|
</FormGroup>
|
2025-03-09 20:55:58 +01:00
|
|
|
<Switch v-model="ftsEnabled" @change="onChangeFts"/>
|
2025-03-07 19:47:58 +01:00
|
|
|
</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">
|
2025-03-28 17:51:39 +01:00
|
|
|
<Button tool plain @click="onShowAclDialog()">{{ $t('main.dialog.edit') }}</Button>
|
2025-03-07 19:47:58 +01:00
|
|
|
</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">
|
2025-03-28 17:51:39 +01:00
|
|
|
<Button tool plain @click="onShowSpamFilterDialog()">{{ $t('main.dialog.edit') }}</Button>
|
2025-03-07 19:47:58 +01:00
|
|
|
</div>
|
|
|
|
|
</SettingsItem>
|
|
|
|
|
|
|
|
|
|
</Section>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|