mirror of
https://github.com/requarks/wiki.git
synced 2026-10-12 14:01:58 -05:00
545 lines
18 KiB
Vue
545 lines
18 KiB
Vue
<template>
|
|
<w-page class="admin-mail">
|
|
<div class="flex flex-wrap p-4 items-center">
|
|
<div class="flex-none">
|
|
<img
|
|
class="admin-icon animated fadeInLeft"
|
|
src="/_assets/icons/fluent-message-settings-animated.svg" />
|
|
</div>
|
|
<div class="min-w-0 flex-1 pl-4">
|
|
<div class="text-h5 admin-page-title animated fadeInLeft">{{ t('admin.mail.title') }}</div>
|
|
<div class="text-subtitle1 text-grey animated fadeInLeft wait-p2s">
|
|
{{ t('admin.mail.subtitle') }}
|
|
</div>
|
|
</div>
|
|
<div class="flex-none">
|
|
<w-btn
|
|
class="mr-2 acrylic-btn"
|
|
icon="la:question-circle"
|
|
flat
|
|
color="grey"
|
|
:aria-label="t(`common.actions.viewDocs`)"
|
|
:href="siteStore.docsBase + `/admin/mail`"
|
|
target="_blank">
|
|
<w-tooltip>{{ t(`common.actions.viewDocs`) }}</w-tooltip>
|
|
</w-btn>
|
|
<w-btn
|
|
class="mr-2 acrylic-btn"
|
|
icon="la:redo-alt"
|
|
flat
|
|
color="secondary"
|
|
:loading="state.loading > 0"
|
|
:aria-label="t(`common.actions.refresh`)"
|
|
@click="load">
|
|
<w-tooltip>{{ t(`common.actions.refresh`) }}</w-tooltip>
|
|
</w-btn>
|
|
<w-btn
|
|
unelevated
|
|
icon="mdi:check"
|
|
:label="t(`common.actions.apply`)"
|
|
color="secondary"
|
|
@click="save"
|
|
:disabled="state.loading > 0" />
|
|
</div>
|
|
</div>
|
|
<w-separator inset />
|
|
<div class="grid grid-cols-12 p-4 gap-4">
|
|
<div class="col-span-12 lg:col-span-7">
|
|
<!-- ----------------------- -->
|
|
<!-- Configuration -->
|
|
<!-- ----------------------- -->
|
|
<w-card class="pb-2">
|
|
<w-card-header>{{ t('admin.mail.configuration') }}</w-card-header>
|
|
<w-item>
|
|
<blueprint-icon icon="contact" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.senderName`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.general.senderNameHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section>
|
|
<w-input
|
|
outlined
|
|
v-model="state.config.senderName"
|
|
dense
|
|
hide-bottom-space
|
|
:aria-label="t(`admin.mail.senderName`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<w-separator class="my-2" inset />
|
|
<w-item>
|
|
<blueprint-icon icon="envelope" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.senderEmail`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.general.senderEmailHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section>
|
|
<w-input
|
|
outlined
|
|
v-model="state.config.senderEmail"
|
|
dense
|
|
:aria-label="t(`admin.mail.senderEmail`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<w-separator class="my-2" inset />
|
|
<w-item>
|
|
<blueprint-icon icon="dns" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.defaultBaseURL`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.general.defaultBaseURLHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section>
|
|
<w-input
|
|
outlined
|
|
v-model="state.config.defaultBaseURL"
|
|
dense
|
|
placeholder="https://wiki.example.com"
|
|
:aria-label="t(`admin.mail.defaultBaseURL`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
</w-card>
|
|
<!-- ----------------------- -->
|
|
<!-- SMTP -->
|
|
<!-- ----------------------- -->
|
|
<w-card class="pb-2 mt-4">
|
|
<w-card-header>{{ t('admin.mail.smtp') }}</w-card-header>
|
|
<w-item>
|
|
<blueprint-icon icon="dns" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.smtpHost`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.mail.smtpHostHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section>
|
|
<w-input
|
|
outlined
|
|
v-model="state.config.host"
|
|
dense
|
|
no-autofill
|
|
hide-bottom-space
|
|
:aria-label="t(`admin.mail.smtpHost`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<w-separator class="my-2" inset />
|
|
<w-item>
|
|
<blueprint-icon icon="ethernet-off" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.smtpPort`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.mail.smtpPortHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section style="flex: 0 0 120px">
|
|
<w-input
|
|
outlined
|
|
v-model="state.config.port"
|
|
dense
|
|
no-autofill
|
|
:aria-label="t(`admin.mail.smtpPort`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<w-separator class="my-2" inset />
|
|
<w-item tag="label">
|
|
<blueprint-icon icon="secure" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.smtpTLS`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.mail.smtpTLSHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section avatar>
|
|
<w-toggle v-model="state.config.secure" :aria-label="t(`admin.mail.smtpTLS`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<w-separator class="my-2" inset />
|
|
<w-item tag="label">
|
|
<blueprint-icon icon="security-ssl" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.smtpVerifySSL`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.mail.smtpVerifySSLHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section avatar>
|
|
<w-toggle
|
|
v-model="state.config.verifySSL"
|
|
:aria-label="t(`admin.mail.smtpVerifySSL`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<w-separator class="my-2" inset />
|
|
<w-item>
|
|
<blueprint-icon icon="test-account" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.smtpUser`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.mail.smtpUserHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section>
|
|
<w-input
|
|
outlined
|
|
v-model="state.config.user"
|
|
dense
|
|
no-autofill
|
|
:aria-label="t(`admin.mail.smtpUser`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<w-separator class="my-2" inset />
|
|
<w-item>
|
|
<blueprint-icon icon="password" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.smtpPwd`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.mail.smtpPwdHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section>
|
|
<!-- -> Masked, with WInput's own reveal toggle: what arrives from the server is a mask
|
|
rather than the password, so the only value ever readable here is one the
|
|
operator is typing at that moment and may want to check -->
|
|
<w-input
|
|
outlined
|
|
v-model="state.config.pass"
|
|
dense
|
|
type="password"
|
|
revealable
|
|
no-autofill
|
|
:aria-label="t(`admin.mail.smtpPwd`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<w-separator class="my-2" inset />
|
|
<w-item>
|
|
<blueprint-icon icon="server" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.smtpName`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.mail.smtpNameHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section>
|
|
<w-input
|
|
outlined
|
|
v-model="state.config.name"
|
|
dense
|
|
no-autofill
|
|
hide-bottom-space
|
|
:aria-label="t(`admin.mail.smtpName`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
</w-card>
|
|
<!-- ----------------------- -->
|
|
<!-- DKIM -->
|
|
<!-- ----------------------- -->
|
|
<w-card class="pb-2 mt-4">
|
|
<w-card-header>{{ t('admin.mail.dkim') }}</w-card-header>
|
|
<w-item class="pt-0">
|
|
<w-item-section>
|
|
<w-card class="bg-info text-white rounded" flat>
|
|
<w-card-section class="items-center" horizontal>
|
|
<w-card-section class="flex-none pr-0">
|
|
<w-icon name="la:info-circle" size="lg" />
|
|
</w-card-section>
|
|
<w-card-section class="text-caption">{{
|
|
t('admin.mail.dkimHint')
|
|
}}</w-card-section>
|
|
</w-card-section>
|
|
</w-card>
|
|
</w-item-section>
|
|
</w-item>
|
|
<w-item tag="label">
|
|
<blueprint-icon icon="received" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.dkimUse`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.mail.dkimUseHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section avatar>
|
|
<w-toggle v-model="state.config.useDKIM" :aria-label="t(`admin.mail.dkimUse`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<template v-if="state.config.useDKIM">
|
|
<w-separator class="my-2" inset />
|
|
<w-item>
|
|
<blueprint-icon icon="dns" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.dkimDomainName`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.mail.dkimDomainNameHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section>
|
|
<w-input
|
|
outlined
|
|
v-model="state.config.dkimDomainName"
|
|
dense
|
|
:aria-label="t(`admin.mail.dkimDomainName`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<w-separator class="my-2" inset />
|
|
<w-item>
|
|
<blueprint-icon icon="access" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.dkimKeySelector`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.mail.dkimKeySelectorHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section>
|
|
<w-input
|
|
outlined
|
|
v-model="state.config.dkimKeySelector"
|
|
dense
|
|
:aria-label="t(`admin.mail.dkimKeySelector`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<w-separator class="my-2" inset />
|
|
<w-item>
|
|
<blueprint-icon icon="grand-master-key" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.dkimPrivateKey`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.mail.dkimPrivateKeyHint`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section>
|
|
<w-input
|
|
outlined
|
|
v-model="state.config.dkimPrivateKey"
|
|
dense
|
|
no-autofill
|
|
:aria-label="t(`admin.mail.dkimPrivateKey`)"
|
|
type="textarea" />
|
|
</w-item-section>
|
|
</w-item>
|
|
</template>
|
|
</w-card>
|
|
</div>
|
|
<div class="col-span-12 lg:col-span-5">
|
|
<!-- ----------------------- -->
|
|
<!-- MAIL TEMPLATES -->
|
|
<!-- ----------------------- -->
|
|
<w-card class="pb-2 mb-4" v-if="flagStore.experimental">
|
|
<w-card-header>{{ t('admin.mail.templates') }}</w-card-header>
|
|
<w-list>
|
|
<w-item>
|
|
<blueprint-icon icon="resume-template" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.templateWelcome`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section side>
|
|
<w-btn
|
|
outline
|
|
no-caps
|
|
icon="la:edit"
|
|
color="primary"
|
|
@click="editTemplate(`welcome`)"
|
|
:label="t(`common.actions.edit`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<w-separator inset />
|
|
<w-item>
|
|
<blueprint-icon icon="resume-template" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.templateResetPwd`) }}</w-item-label>
|
|
</w-item-section>
|
|
<w-item-section side>
|
|
<w-btn
|
|
outline
|
|
no-caps
|
|
icon="la:edit"
|
|
color="primary"
|
|
@click="editTemplate(`pwdreset`)"
|
|
:label="t(`common.actions.edit`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
</w-list>
|
|
</w-card>
|
|
<!-- ----------------------- -->
|
|
<!-- SMTP TEST -->
|
|
<!-- ----------------------- -->
|
|
<w-card class="pb-2">
|
|
<w-card-header>{{ t('admin.mail.test') }}</w-card-header>
|
|
<w-item>
|
|
<blueprint-icon class="self-start" icon="email" />
|
|
<w-item-section>
|
|
<w-item-label>{{ t(`admin.mail.testRecipient`) }}</w-item-label>
|
|
<w-item-label caption>{{ t(`admin.mail.testRecipientHint`) }}</w-item-label>
|
|
<w-input
|
|
class="mt-4"
|
|
outlined
|
|
v-model="state.testEmail"
|
|
dense
|
|
:aria-label="t(`admin.mail.testRecipient`)" />
|
|
</w-item-section>
|
|
</w-item>
|
|
<div class="flex justify-end pr-4 py-2">
|
|
<w-btn
|
|
unelevated
|
|
color="primary"
|
|
icon="la:paper-plane"
|
|
:label="t(`admin.mail.testSend`)"
|
|
@click="sendTest"
|
|
:loading="state.testLoading" />
|
|
</div>
|
|
</w-card>
|
|
</div>
|
|
</div>
|
|
</w-page>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { useI18n } from 'vue-i18n'
|
|
import { onMounted, reactive } from 'vue'
|
|
|
|
import { useMeta } from '@/composables/meta'
|
|
import { notify } from '@/composables/notify'
|
|
import { apiErrorMessage } from '@/helpers/apiError'
|
|
|
|
import { useAdminStore } from '@/stores/admin'
|
|
import { useFlagsStore } from '@/stores/flags'
|
|
import { useSiteStore } from '@/stores/site'
|
|
|
|
import { toMerged } from 'es-toolkit/object'
|
|
|
|
// STORES
|
|
|
|
const adminStore = useAdminStore()
|
|
const flagStore = useFlagsStore()
|
|
const siteStore = useSiteStore()
|
|
|
|
// I18N
|
|
|
|
const { t } = useI18n()
|
|
|
|
// META
|
|
|
|
useMeta(() => ({
|
|
title: t('admin.mail.title')
|
|
}))
|
|
|
|
// DATA
|
|
|
|
/**
|
|
* Fallbacks for config keys the API may not return yet, so that every control renders with a
|
|
* defined value. Must mirror the mail defaults seeded by the backend.
|
|
*/
|
|
function defaultConfig() {
|
|
return {
|
|
senderName: '',
|
|
senderEmail: '',
|
|
defaultBaseURL: '',
|
|
host: '',
|
|
port: 465,
|
|
name: '',
|
|
secure: true,
|
|
verifySSL: true,
|
|
user: '',
|
|
pass: '',
|
|
useDKIM: false,
|
|
dkimDomainName: '',
|
|
dkimKeySelector: '',
|
|
dkimPrivateKey: ''
|
|
}
|
|
}
|
|
|
|
const state = reactive({
|
|
config: defaultConfig(),
|
|
testEmail: '',
|
|
testLoading: false,
|
|
loading: 0
|
|
})
|
|
|
|
// METHODS
|
|
async function load() {
|
|
state.loading++
|
|
try {
|
|
const resp = await API_CLIENT.get('mail/config').json()
|
|
if (!resp) {
|
|
throw new Error('Failed to fetch mail config.')
|
|
}
|
|
state.config = toMerged(defaultConfig(), resp)
|
|
adminStore.info.isMailConfigured = state.config?.host?.length > 2
|
|
} catch (err) {
|
|
notify({
|
|
type: 'negative',
|
|
message: 'Failed to fetch mail config',
|
|
caption: err.message
|
|
})
|
|
}
|
|
state.loading--
|
|
}
|
|
|
|
async function save() {
|
|
if (state.loading > 0) {
|
|
return
|
|
}
|
|
|
|
state.loading++
|
|
try {
|
|
const resp = await API_CLIENT.put('mail/config', {
|
|
json: {
|
|
senderName: state.config.senderName || '',
|
|
senderEmail: state.config.senderEmail || '',
|
|
defaultBaseURL: state.config.defaultBaseURL || '',
|
|
host: state.config.host || '',
|
|
port: Number.parseInt(state.config.port, 10) || 465,
|
|
name: state.config.name || '',
|
|
secure: state.config.secure ?? false,
|
|
verifySSL: state.config.verifySSL ?? false,
|
|
user: state.config.user || '',
|
|
pass: state.config.pass || '',
|
|
useDKIM: state.config.useDKIM ?? false,
|
|
dkimDomainName: state.config.dkimDomainName || '',
|
|
dkimKeySelector: state.config.dkimKeySelector || '',
|
|
dkimPrivateKey: state.config.dkimPrivateKey || ''
|
|
}
|
|
}).json()
|
|
if (!resp?.ok) {
|
|
throw new Error(
|
|
t(`admin.mail.${resp?.error}`, resp?.message || 'An unexpected error occured.')
|
|
)
|
|
}
|
|
notify({
|
|
type: 'positive',
|
|
message: t('admin.mail.saveSuccess')
|
|
})
|
|
adminStore.info.isMailConfigured = state.config?.host?.length > 2
|
|
} catch (err) {
|
|
notify({
|
|
type: 'negative',
|
|
message: err.message
|
|
})
|
|
}
|
|
state.loading--
|
|
}
|
|
|
|
function editTemplate(tmplId) {
|
|
adminStore.$patch({
|
|
overlayOpts: { id: tmplId },
|
|
overlay: 'MailTemplateEditorOverlay'
|
|
})
|
|
}
|
|
|
|
async function sendTest() {
|
|
if (!state.testEmail) {
|
|
notify({
|
|
type: 'negative',
|
|
message: t('admin.mail.testRecipientMissing')
|
|
})
|
|
return
|
|
}
|
|
state.testLoading = true
|
|
try {
|
|
// -> The stored configuration is what it sends through, so anything typed above and not yet
|
|
// saved is not what is being tested
|
|
const resp = await API_CLIENT.post('mail/test', {
|
|
json: {
|
|
recipient: state.testEmail
|
|
}
|
|
}).json()
|
|
if (!resp?.ok) {
|
|
throw new Error(resp?.message || 'An unexpected error occured.')
|
|
}
|
|
notify({
|
|
type: 'positive',
|
|
message: t('admin.mail.sendTestSuccess')
|
|
})
|
|
} catch (err) {
|
|
notify({
|
|
type: 'negative',
|
|
message: t('admin.mail.sendTestFailed'),
|
|
// -> The mail server's own complaint, which is the whole value of the button
|
|
caption: apiErrorMessage(err)
|
|
})
|
|
}
|
|
state.testLoading = false
|
|
}
|
|
|
|
// MOUNTED
|
|
|
|
onMounted(() => {
|
|
load()
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss"></style>
|