mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-08-08 15:03:42 +08:00
feat: allow admin and user delete mail, sendbox, send access(only admin) (#329)
This commit is contained in:
@@ -90,7 +90,7 @@ const { t } = useI18n({
|
|||||||
saveToS3: 'Save to S3',
|
saveToS3: 'Save to S3',
|
||||||
multiAction: 'Multi Action',
|
multiAction: 'Multi Action',
|
||||||
cancelMultiAction: 'Cancel Multi Action',
|
cancelMultiAction: 'Cancel Multi Action',
|
||||||
selectAll: 'Select All',
|
selectAll: 'Select All of This Page',
|
||||||
unselectAll: 'Unselect All',
|
unselectAll: 'Unselect All',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
@@ -109,7 +109,7 @@ const { t } = useI18n({
|
|||||||
saveToS3: '保存到S3',
|
saveToS3: '保存到S3',
|
||||||
multiAction: '多选',
|
multiAction: '多选',
|
||||||
cancelMultiAction: '取消多选',
|
cancelMultiAction: '取消多选',
|
||||||
selectAll: '全选',
|
selectAll: '全选本页',
|
||||||
unselectAll: '取消全选',
|
unselectAll: '取消全选',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -387,7 +387,8 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #2>
|
<template #2>
|
||||||
<n-card v-if="curMail" class="mail-item" :title="curMail.subject" style="overflow: auto; max-height: 100vh;">
|
<n-card :bordered="false" embedded v-if="curMail" class="mail-item" :title="curMail.subject"
|
||||||
|
style="overflow: auto; max-height: 100vh;">
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
ID: {{ curMail.id }}
|
ID: {{ curMail.id }}
|
||||||
@@ -434,7 +435,7 @@ onBeforeUnmount(() => {
|
|||||||
</iframe>
|
</iframe>
|
||||||
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
|
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-card class="mail-item" v-else>
|
<n-card :bordered="false" embedded class="mail-item" v-else>
|
||||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||||
</n-result>
|
</n-result>
|
||||||
</n-card>
|
</n-card>
|
||||||
@@ -483,7 +484,7 @@ onBeforeUnmount(() => {
|
|||||||
<n-drawer v-model:show="curMail" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
|
<n-drawer v-model:show="curMail" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
|
||||||
style="height: 80vh;">
|
style="height: 80vh;">
|
||||||
<n-drawer-content :title="curMail ? curMail.subject : ''" closable>
|
<n-drawer-content :title="curMail ? curMail.subject : ''" closable>
|
||||||
<n-card style="overflow: auto;">
|
<n-card :bordered="false" embedded style="overflow: auto;">
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
ID: {{ curMail.id }}
|
ID: {{ curMail.id }}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { watch, onMounted, ref } from "vue";
|
import { watch, onMounted, ref, computed } from "vue";
|
||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
@@ -9,6 +9,11 @@ const message = useMessage()
|
|||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
enableUserDeleteEmail: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
requried: false
|
||||||
|
},
|
||||||
showEMailFrom: {
|
showEMailFrom: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false
|
default: false
|
||||||
@@ -18,6 +23,11 @@ const props = defineProps({
|
|||||||
default: () => { },
|
default: () => { },
|
||||||
requried: true
|
requried: true
|
||||||
},
|
},
|
||||||
|
deleteMail: {
|
||||||
|
type: Function,
|
||||||
|
default: () => { },
|
||||||
|
requried: false
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { isDark, mailboxSplitSize } = useGlobalState()
|
const { isDark, mailboxSplitSize } = useGlobalState()
|
||||||
@@ -30,19 +40,35 @@ const pageSize = ref(20)
|
|||||||
const curMail = ref(null);
|
const curMail = ref(null);
|
||||||
const showCode = ref(false)
|
const showCode = ref(false)
|
||||||
|
|
||||||
|
const multiActionMode = ref(false)
|
||||||
|
const showMultiActionDelete = ref(false)
|
||||||
|
const multiActionDeleteProgress = ref({ percentage: 0, tip: '0/0' })
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
success: 'Success',
|
success: 'Success',
|
||||||
refresh: 'Refresh',
|
refresh: 'Refresh',
|
||||||
showCode: 'Change View Original Code',
|
showCode: 'Change View Original Code',
|
||||||
pleaseSelectMail: "Please select a mail to view."
|
pleaseSelectMail: "Please select a mail to view.",
|
||||||
|
delete: 'Delete',
|
||||||
|
deleteMailTip: 'Are you sure you want to delete mail?',
|
||||||
|
multiAction: 'Multi Action',
|
||||||
|
cancelMultiAction: 'Cancel Multi Action',
|
||||||
|
selectAll: 'Select All of This Page',
|
||||||
|
unselectAll: 'Unselect All',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
success: '成功',
|
success: '成功',
|
||||||
refresh: '刷新',
|
refresh: '刷新',
|
||||||
showCode: '切换查看元数据',
|
showCode: '切换查看元数据',
|
||||||
pleaseSelectMail: "请选择一封邮件查看。",
|
pleaseSelectMail: "请选择一封邮件查看。",
|
||||||
|
delete: '删除',
|
||||||
|
deleteMailTip: '确定要删除邮件吗?',
|
||||||
|
multiAction: '多选',
|
||||||
|
cancelMultiAction: '取消多选',
|
||||||
|
selectAll: '全选本页',
|
||||||
|
unselectAll: '取消全选',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -105,6 +131,71 @@ const onSpiltSizeChange = (size) => {
|
|||||||
mailboxSplitSize.value = size;
|
mailboxSplitSize.value = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteMail = async () => {
|
||||||
|
try {
|
||||||
|
await props.deleteMail(curMail.value.id);
|
||||||
|
message.success(t("success"));
|
||||||
|
curMail.value = null;
|
||||||
|
await refresh();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showMultiActionMode = computed(() => {
|
||||||
|
return props.enableUserDeleteEmail;
|
||||||
|
});
|
||||||
|
|
||||||
|
const multiActionModeClick = (enableMulti) => {
|
||||||
|
if (enableMulti) {
|
||||||
|
data.value.forEach((item) => {
|
||||||
|
item.checked = false;
|
||||||
|
});
|
||||||
|
multiActionMode.value = true;
|
||||||
|
} else {
|
||||||
|
multiActionMode.value = false;
|
||||||
|
data.value.forEach((item) => {
|
||||||
|
item.checked = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiActionSelectAll = (checked) => {
|
||||||
|
data.value.forEach((item) => {
|
||||||
|
item.checked = checked;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiActionDeleteMail = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const selectedMails = data.value.filter((item) => item.checked);
|
||||||
|
if (selectedMails.length === 0) {
|
||||||
|
message.error(t('pleaseSelectMail'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
multiActionDeleteProgress.value = {
|
||||||
|
percentage: 0,
|
||||||
|
tip: `0/${selectedMails.length}`
|
||||||
|
};
|
||||||
|
for (const [index, mail] of selectedMails.entries()) {
|
||||||
|
await props.deleteMail(mail.id);
|
||||||
|
showMultiActionDelete.value = true;
|
||||||
|
multiActionDeleteProgress.value = {
|
||||||
|
percentage: Math.floor((index + 1) / selectedMails.length * 100),
|
||||||
|
tip: `${index + 1}/${selectedMails.length}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
message.success(t("success"));
|
||||||
|
await refresh();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
showMultiActionDelete.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await refresh();
|
await refresh();
|
||||||
});
|
});
|
||||||
@@ -112,70 +203,105 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25"
|
<div v-if="!isMobile" class="left">
|
||||||
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange">
|
<div style="margin-bottom: 10px;">
|
||||||
<template #1>
|
<n-space v-if="multiActionMode">
|
||||||
<div class="center">
|
<n-button @click="multiActionModeClick(false)" tertiary>
|
||||||
|
{{ t('cancelMultiAction') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="multiActionSelectAll(true)" tertiary>
|
||||||
|
{{ t('selectAll') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="multiActionSelectAll(false)" tertiary>
|
||||||
|
{{ t('unselectAll') }}
|
||||||
|
</n-button>
|
||||||
|
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="multiActionDeleteMail">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button tertiary type="error">{{ t('delete') }}</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t('deleteMailTip') }}
|
||||||
|
</n-popconfirm>
|
||||||
|
</n-space>
|
||||||
|
<n-space v-else>
|
||||||
|
<n-button v-if="showMultiActionMode" @click="multiActionModeClick(true)" type="primary" tertiary>
|
||||||
|
{{ t('multiAction') }}
|
||||||
|
</n-button>
|
||||||
<div style="display: inline-block; margin-right: 10px;">
|
<div style="display: inline-block; margin-right: 10px;">
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||||
|
:page-sizes="[20, 50, 100]" show-size-picker />
|
||||||
</div>
|
</div>
|
||||||
<n-button @click="refresh" size="small" type="primary" tertiary>
|
<n-button @click="refresh" type="primary" tertiary>
|
||||||
{{ t('refresh') }}
|
{{ t('refresh') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</n-space>
|
||||||
<div style="overflow: auto; height: 80vh;">
|
</div>
|
||||||
<n-list hoverable clickable>
|
<n-split direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
|
||||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
:on-update:size="onSpiltSizeChange">
|
||||||
:class="mailItemClass(row)">
|
<template #1>
|
||||||
<n-thing :title="row.subject">
|
<div style="overflow: auto; height: 80vh;">
|
||||||
<template #description>
|
<n-list hoverable clickable>
|
||||||
<n-tag type="info">
|
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||||
ID: {{ row.id }}
|
:class="mailItemClass(row)">
|
||||||
</n-tag>
|
<template #prefix v-if="multiActionMode">
|
||||||
<n-tag type="info">
|
<n-checkbox v-model:checked="row.checked" />
|
||||||
{{ `${row.created_at} UTC` }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag v-if="showEMailFrom" type="info">
|
|
||||||
FROM: {{ row.address }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
TO: {{ row.to_mail }}
|
|
||||||
</n-tag>
|
|
||||||
</template>
|
</template>
|
||||||
</n-thing>
|
<n-thing :title="row.subject">
|
||||||
</n-list-item>
|
<template #description>
|
||||||
</n-list>
|
<n-tag type="info">
|
||||||
</div>
|
ID: {{ row.id }}
|
||||||
</template>
|
</n-tag>
|
||||||
<template #2>
|
<n-tag type="info">
|
||||||
<n-card v-if="curMail" class="mail-item" :title="curMail.subject" style="overflow: auto; max-height: 100vh;">
|
{{ `${row.created_at} UTC` }}
|
||||||
<n-space>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag v-if="showEMailFrom" type="info">
|
||||||
ID: {{ curMail.id }}
|
FROM: {{ row.address }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
{{ `${curMail.created_at} UTC` }}
|
TO: {{ row.to_mail }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
</template>
|
||||||
FROM: {{ curMail.address }}
|
</n-thing>
|
||||||
</n-tag>
|
</n-list-item>
|
||||||
<n-tag type="info">
|
</n-list>
|
||||||
TO: {{ curMail.to_mail }}
|
</div>
|
||||||
</n-tag>
|
</template>
|
||||||
<n-button size="small" tertiary type="info" @click="showCode = !showCode">
|
<template #2>
|
||||||
{{ t('showCode') }}
|
<n-card :bordered="false" embedded v-if="curMail" class="mail-item" :title="curMail.subject"
|
||||||
</n-button>
|
style="overflow: auto; max-height: 100vh;">
|
||||||
</n-space>
|
<n-space>
|
||||||
<pre v-if="showCode" style="margin-top: 10px;">{{ curMail.raw }}</pre>
|
<n-tag type="info">
|
||||||
<pre v-else-if="!curMail.is_html" style="margin-top: 10px;">{{ curMail.content }}</pre>
|
ID: {{ curMail.id }}
|
||||||
<div v-else v-html="curMail.content" style="margin-top: 10px;"></div>
|
</n-tag>
|
||||||
</n-card>
|
<n-tag type="info">
|
||||||
<n-card class="mail-item" v-else>
|
{{ `${curMail.created_at} UTC` }}
|
||||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
</n-tag>
|
||||||
</n-result>
|
<n-tag type="info">
|
||||||
</n-card>
|
FROM: {{ curMail.address }}
|
||||||
</template>
|
</n-tag>
|
||||||
</n-split>
|
<n-tag type="info">
|
||||||
|
TO: {{ curMail.to_mail }}
|
||||||
|
</n-tag>
|
||||||
|
<n-button size="small" tertiary type="info" @click="showCode = !showCode">
|
||||||
|
{{ t('showCode') }}
|
||||||
|
</n-button>
|
||||||
|
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t('deleteMailTip') }}
|
||||||
|
</n-popconfirm>
|
||||||
|
</n-space>
|
||||||
|
<pre v-if="showCode" style="margin-top: 10px;">{{ curMail.raw }}</pre>
|
||||||
|
<pre v-else-if="!curMail.is_html" style="margin-top: 10px;">{{ curMail.content }}</pre>
|
||||||
|
<div v-else v-html="curMail.content" style="margin-top: 10px;"></div>
|
||||||
|
</n-card>
|
||||||
|
<n-card :bordered="false" embedded class="mail-item" v-else>
|
||||||
|
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||||
|
</n-result>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
</n-split>
|
||||||
|
</div>
|
||||||
<div class="left" v-else>
|
<div class="left" v-else>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<div style="display: inline-block; margin-right: 10px;">
|
<div style="display: inline-block; margin-right: 10px;">
|
||||||
@@ -210,7 +336,7 @@ onMounted(async () => {
|
|||||||
<n-drawer v-model:show="curMail" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
|
<n-drawer v-model:show="curMail" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
|
||||||
style="height: 80vh;">
|
style="height: 80vh;">
|
||||||
<n-drawer-content :title="curMail ? curMail.subject : ''" closable>
|
<n-drawer-content :title="curMail ? curMail.subject : ''" closable>
|
||||||
<n-card style="overflow: auto;">
|
<n-card :bordered="false" embedded style="overflow: auto;">
|
||||||
<n-space>
|
<n-space>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
ID: {{ curMail.id }}
|
ID: {{ curMail.id }}
|
||||||
@@ -224,6 +350,15 @@ onMounted(async () => {
|
|||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
TO: {{ curMail.to_mail }}
|
TO: {{ curMail.to_mail }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
|
<n-button size="small" tertiary type="info" @click="showCode = !showCode">
|
||||||
|
{{ t('showCode') }}
|
||||||
|
</n-button>
|
||||||
|
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t('deleteMailTip') }}
|
||||||
|
</n-popconfirm>
|
||||||
</n-space>
|
</n-space>
|
||||||
<pre v-if="showCode" style="margin-top: 10px;">{{ curMail.raw }}</pre>
|
<pre v-if="showCode" style="margin-top: 10px;">{{ curMail.raw }}</pre>
|
||||||
<pre v-else-if="!curMail.is_html" style="margin-top: 10px;">{{ curMail.content }}</pre>
|
<pre v-else-if="!curMail.is_html" style="margin-top: 10px;">{{ curMail.content }}</pre>
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ const deleteMail = async (curMailId) => {
|
|||||||
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const deleteSenboxMail = async (curMailId) => {
|
||||||
|
await api.fetch(`/api/sendbox/${curMailId}`, { method: 'DELETE' });
|
||||||
|
};
|
||||||
|
|
||||||
const fetchSenboxData = async (limit, offset) => {
|
const fetchSenboxData = async (limit, offset) => {
|
||||||
return await api.fetch(`/api/sendbox?limit=${limit}&offset=${offset}`);
|
return await api.fetch(`/api/sendbox?limit=${limit}&offset=${offset}`);
|
||||||
};
|
};
|
||||||
@@ -86,7 +90,8 @@ const saveToS3 = async (mail_id, filename, blob) => {
|
|||||||
:deleteMail="deleteMail" />
|
:deleteMail="deleteMail" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
||||||
<SendBox :fetchMailData="fetchSenboxData" />
|
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||||
|
:deleteMail="deleteSenboxMail" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="sendmail" :tab="t('sendmail')">
|
<n-tab-pane name="sendmail" :tab="t('sendmail')">
|
||||||
<SendMail />
|
<SendMail />
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ onMounted(async () => {
|
|||||||
<span>
|
<span>
|
||||||
<p>{{ t("addressCredentialTip") }}</p>
|
<p>{{ t("addressCredentialTip") }}</p>
|
||||||
</span>
|
</span>
|
||||||
<n-card>
|
<n-card :bordered="false" embedded>
|
||||||
<b>{{ curEmailCredential }}</b>
|
<b>{{ curEmailCredential }}</b>
|
||||||
</n-card>
|
</n-card>
|
||||||
<template #action>
|
<template #action>
|
||||||
@@ -296,7 +296,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</n-pagination>
|
</n-pagination>
|
||||||
</div>
|
</div>
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<n-form-item-row :label="t('address_block_list')">
|
<n-form-item-row :label="t('address_block_list')">
|
||||||
<n-select v-model:value="addressBlockList" filterable multiple tag
|
<n-select v-model:value="addressBlockList" filterable multiple tag
|
||||||
:placeholder="t('address_block_list_placeholder')" />
|
:placeholder="t('address_block_list_placeholder')" />
|
||||||
|
|||||||
@@ -71,11 +71,11 @@ onMounted(async () => {
|
|||||||
<div class="center">
|
<div class="center">
|
||||||
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('addressCredential')">
|
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('addressCredential')">
|
||||||
<p>{{ t('addressCredential') }}</p>
|
<p>{{ t('addressCredential') }}</p>
|
||||||
<n-card>
|
<n-card :bordered="false" embedded>
|
||||||
<b>{{ result }}</b>
|
<b>{{ result }}</b>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-card style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
|
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
|
||||||
<n-checkbox v-model:checked="enablePrefix" />
|
<n-checkbox v-model:checked="enablePrefix" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ const fetchMailData = async (limit, offset) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteMail = async (curMailId) => {
|
||||||
|
await api.fetch(`/admin/mails/${curMailId}`, { method: 'DELETE' });
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!adminAuth.value) {
|
if (!adminAuth.value) {
|
||||||
showAdminAuth.value = true;
|
showAdminAuth.value = true;
|
||||||
@@ -66,6 +70,7 @@ onMounted(async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
<div style="margin-top: 10px;"></div>
|
<div style="margin-top: 10px;"></div>
|
||||||
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="false" :fetchMailData="fetchMailData" />
|
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="true" :fetchMailData="fetchMailData"
|
||||||
|
:deleteMail="deleteMail" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ const fetchMailUnknowData = async (limit, offset) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteMail = async (curMailId) => {
|
||||||
|
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!adminAuth.value) {
|
if (!adminAuth.value) {
|
||||||
showAdminAuth.value = true;
|
showAdminAuth.value = true;
|
||||||
@@ -25,6 +29,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="adminAuth" style="margin-top: 10px;">
|
<div v-if="adminAuth" style="margin-top: 10px;">
|
||||||
<MailBox :enableUserDeleteEmail="false" :fetchMailData="fetchMailUnknowData" />
|
<MailBox :enableUserDeleteEmail="true" :fetchMailData="fetchMailUnknowData" :deleteMail="deleteMail" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -91,8 +91,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card>
|
<n-card :bordered="false" embedded>
|
||||||
<n-alert :show-icon="false">
|
<n-alert :show-icon="false" :bordered="false">
|
||||||
<span>{{ t('cronTip') }}</span>
|
<span>{{ t('cronTip') }}</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<n-form :model="cleanupModel">
|
<n-form :model="cleanupModel">
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ const fetchData = async (limit, offset) => {
|
|||||||
+ (adminSendBoxTabAddress.value ? `&address=${adminSendBoxTabAddress.value}` : '')
|
+ (adminSendBoxTabAddress.value ? `&address=${adminSendBoxTabAddress.value}` : '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteSenboxMail = async (curMailId) => {
|
||||||
|
await api.fetch(`/admin/sendbox/${curMailId}`, { method: 'DELETE' });
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -36,7 +40,8 @@ const fetchData = async (limit, offset) => {
|
|||||||
{{ t('query') }}
|
{{ t('query') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
<SendBox style="margin-top: 10px;" :fetchMailData="fetchData" :showEMailFrom="true" />
|
<SendBox style="margin-top: 10px;" :enableUserDeleteEmail="true" :deleteMail="deleteSenboxMail"
|
||||||
|
:fetchMailData="fetchData" :showEMailFrom="true" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ const { t } = useI18n({
|
|||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
disable: 'Disable',
|
disable: 'Disable',
|
||||||
modify: 'Modify',
|
modify: 'Modify',
|
||||||
|
delete: 'Delete',
|
||||||
|
deleteTip: 'Are you sure to delete this?',
|
||||||
created_at: 'Created At',
|
created_at: 'Created At',
|
||||||
action: 'Action',
|
action: 'Action',
|
||||||
itemCount: 'itemCount',
|
itemCount: 'itemCount',
|
||||||
@@ -32,6 +34,8 @@ const { t } = useI18n({
|
|||||||
enable: '启用',
|
enable: '启用',
|
||||||
disable: '禁用',
|
disable: '禁用',
|
||||||
modify: '修改',
|
modify: '修改',
|
||||||
|
delete: '删除',
|
||||||
|
deleteTip: '确定删除吗?',
|
||||||
created_at: '创建时间',
|
created_at: '创建时间',
|
||||||
action: '操作',
|
action: '操作',
|
||||||
itemCount: '总数',
|
itemCount: '总数',
|
||||||
@@ -134,7 +138,25 @@ const columns = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ default: () => t('modify') }
|
{ default: () => t('modify') }
|
||||||
)
|
),
|
||||||
|
h(NPopconfirm,
|
||||||
|
{
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
await api.fetch(`/admin/address_sender/${row.id}`, { method: 'DELETE' });
|
||||||
|
await fetchData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
trigger: () => h(NButton,
|
||||||
|
{
|
||||||
|
tertiary: true,
|
||||||
|
type: "error",
|
||||||
|
},
|
||||||
|
{ default: () => t('delete') }
|
||||||
|
),
|
||||||
|
default: () => t('deleteTip')
|
||||||
|
}
|
||||||
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,7 +205,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</n-pagination>
|
</n-pagination>
|
||||||
</div>
|
</div>
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-card>
|
<n-card :bordered="false" embedded>
|
||||||
<n-row>
|
<n-row>
|
||||||
<n-col :span="6">
|
<n-col :span="6">
|
||||||
<n-statistic :label="t('userCount')" :value="statistics.userCount">
|
<n-statistic :label="t('userCount')" :value="statistics.userCount">
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card style="max-width: 800px; overflow: auto;">
|
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||||
<n-card>
|
<n-card :bordered="false" embedded>
|
||||||
<n-form-item-row :label="t('enableTelegramAllowList')">
|
<n-form-item-row :label="t('enableTelegramAllowList')">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-checkbox v-model:checked="settings.enableAllowList" style="width: 20%;">
|
<n-checkbox v-model:checked="settings.enableAllowList" style="width: 20%;">
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</n-pagination>
|
</n-pagination>
|
||||||
</div>
|
</div>
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<n-form :model="userSettings">
|
<n-form :model="userSettings">
|
||||||
<n-form-item-row :label="t('enableUserRegister')">
|
<n-form-item-row :label="t('enableUserRegister')">
|
||||||
<n-checkbox v-model:checked="userSettings.enable" />
|
<n-checkbox v-model:checked="userSettings.enable" />
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card style="max-width: 800px; overflow: auto;">
|
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||||
<n-form-item-row :label="t('webhookAllowList')">
|
<n-form-item-row :label="t('webhookAllowList')">
|
||||||
<n-select v-model:value="webhookSettings.allowList" filterable multiple tag
|
<n-select v-model:value="webhookSettings.allowList" filterable multiple tag
|
||||||
:placeholder="t('webhookAllowList')" />
|
:placeholder="t('webhookAllowList')" />
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { GithubAlt, Discord, Telegram } from '@vicons/fa'
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card>
|
<n-card :bordered="false" embedded>
|
||||||
<n-button tag="a" target="_blank" href="https://github.com/dreamhunter2333/cloudflare_temp_email">
|
<n-button tag="a" target="_blank" href="https://github.com/dreamhunter2333/cloudflare_temp_email">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon :component="GithubAlt" />
|
<n-icon :component="GithubAlt" />
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const { t } = useI18n({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<n-alert v-if="openSettings.adminContact" :show-icon="false">
|
<n-alert v-if="openSettings.adminContact" :show-icon="false" :bordered="false">
|
||||||
<span>{{ t('adminContact', { msg: openSettings.adminContact }) }}</span>
|
<span>{{ t('adminContact', { msg: openSettings.adminContact }) }}</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const { t } = useI18n({
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card>
|
<n-card :bordered="false" embedded>
|
||||||
<n-form-item-row v-if="!isMobile" :label="t('mailboxSplitSize')">
|
<n-form-item-row v-if="!isMobile" :label="t('mailboxSplitSize')">
|
||||||
<n-slider v-model:value="mailboxSplitSize" :min="0.25" :max="0.75" :step="0.01" :marks="{
|
<n-slider v-model:value="mailboxSplitSize" :min="0.25" :max="0.75" :step="0.01" :marks="{
|
||||||
0.25: '0.25',
|
0.25: '0.25',
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-alert v-if="userSettings.user_email" :show-icon="false" closable>
|
<n-alert v-if="userSettings.user_email" :show-icon="false" :bordered="false" closable>
|
||||||
<span>{{ t('bindUserInfo') }}</span>
|
<span>{{ t('bindUserInfo') }}</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<n-tabs v-model:value="tabValue" size="large" justify-content="space-evenly">
|
<n-tabs v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||||
@@ -206,7 +206,7 @@ onMounted(async () => {
|
|||||||
</n-spin>
|
</n-spin>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="help" :tab="t('help')">
|
<n-tab-pane name="help" :tab="t('help')">
|
||||||
<n-alert :show-icon="false">
|
<n-alert :show-icon="false" :bordered="false">
|
||||||
<span>{{ t('pleaseGetNewEmail') }}</span>
|
<span>{{ t('pleaseGetNewEmail') }}</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<AdminContact />
|
<AdminContact />
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const deleteAccount = async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center" v-if="settings.address">
|
<div class="center" v-if="settings.address">
|
||||||
<n-card>
|
<n-card :bordered="false" embedded>
|
||||||
<Appearance />
|
<Appearance />
|
||||||
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
||||||
{{ t('showAddressCredential') }}
|
{{ t('showAddressCredential') }}
|
||||||
|
|||||||
@@ -84,11 +84,11 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-card v-if="!settings.fetched">
|
<n-card :bordered="false" embedded v-if="!settings.fetched">
|
||||||
<n-skeleton style="height: 50vh" />
|
<n-skeleton style="height: 50vh" />
|
||||||
</n-card>
|
</n-card>
|
||||||
<div v-else-if="settings.address">
|
<div v-else-if="settings.address">
|
||||||
<n-alert type="info" :show-icon="false">
|
<n-alert type="info" :show-icon="false" :bordered="false">
|
||||||
<span>
|
<span>
|
||||||
<b>{{ addressLabel }}</b>
|
<b>{{ addressLabel }}</b>
|
||||||
<n-button v-if="isTelegram" style="margin-left: 10px" @click="showTelegramChangeAddress = true"
|
<n-button v-if="isTelegram" style="margin-left: 10px" @click="showTelegramChangeAddress = true"
|
||||||
@@ -113,8 +113,8 @@ onMounted(async () => {
|
|||||||
<TelegramAddress />
|
<TelegramAddress />
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="center">
|
<div v-else class="center">
|
||||||
<n-card style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<n-alert v-if="jwt" type="warning" :show-icon="false" closable>
|
<n-alert v-if="jwt" type="warning" :show-icon="false" :bordered="false" closable>
|
||||||
<span>{{ t('fetchAddressError') }}</span>
|
<span>{{ t('fetchAddressError') }}</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<Login />
|
<Login />
|
||||||
@@ -140,7 +140,7 @@ onMounted(async () => {
|
|||||||
<span>
|
<span>
|
||||||
<p>{{ t("addressCredentialTip") }}</p>
|
<p>{{ t("addressCredentialTip") }}</p>
|
||||||
</span>
|
</span>
|
||||||
<n-card>
|
<n-card :bordered="false" embedded>
|
||||||
<b>{{ jwt }}</b>
|
<b>{{ jwt }}</b>
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|||||||
@@ -86,6 +86,6 @@ onMounted(async () => {
|
|||||||
{{ t('download') }}
|
{{ t('download') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card v-if="settings.address" :title='t("settings")'>
|
<n-card :bordered="false" embedded v-if="settings.address" :title='t("settings")'>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<n-button type="primary" @click="saveData">{{ t('save') }}</n-button>
|
<n-button type="primary" @click="saveData">{{ t('save') }}</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -144,12 +144,12 @@ const columns = [
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-alert type="warning" :show-icon="false">
|
<n-alert type="warning" :show-icon="false" :bordered="false">
|
||||||
<span>{{ t('tip') }}</span>
|
<span>{{ t('tip') }}</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<n-tabs type="segment" v-model:value="tabValue">
|
<n-tabs type="segment" v-model:value="tabValue">
|
||||||
<n-tab-pane name="address" :tab="t('address')">
|
<n-tab-pane name="address" :tab="t('address')">
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="bind" :tab="t('bind')">
|
<n-tab-pane name="bind" :tab="t('bind')">
|
||||||
<Login :bindUserAddress="bindAddress" />
|
<Login :bindUserAddress="bindAddress" />
|
||||||
|
|||||||
@@ -142,9 +142,9 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center" v-if="settings.address">
|
<div class="center" v-if="settings.address">
|
||||||
<n-card>
|
<n-card :bordered="false" embedded>
|
||||||
<div v-if="!settings.send_balance || settings.send_balance <= 0">
|
<div v-if="!settings.send_balance || settings.send_balance <= 0">
|
||||||
<n-alert type="warning" :show-icon="false">
|
<n-alert type="warning" :show-icon="false" :bordered="false">
|
||||||
{{ t('requestAccessTip') }}
|
{{ t('requestAccessTip') }}
|
||||||
<n-button type="primary" tertiary @click="requestAccess" size="small">{{ t('requestAccess')
|
<n-button type="primary" tertiary @click="requestAccess" size="small">{{ t('requestAccess')
|
||||||
}}</n-button>
|
}}</n-button>
|
||||||
@@ -153,7 +153,7 @@ onMounted(async () => {
|
|||||||
<AdminContact />
|
<AdminContact />
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<n-alert type="info" :show-icon="false">
|
<n-alert type="info" :show-icon="false" :bordered="false">
|
||||||
{{ t('send_balance') }}: {{ settings.send_balance }}
|
{{ t('send_balance') }}: {{ settings.send_balance }}
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
@@ -187,7 +187,7 @@ onMounted(async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<n-form-item :label="t('content')" label-placement="top">
|
<n-form-item :label="t('content')" label-placement="top">
|
||||||
<n-card v-if="isPreview">
|
<n-card :bordered="false" embedded v-if="isPreview">
|
||||||
<div v-html="sendMailModel.content" />
|
<div v-html="sendMailModel.content" />
|
||||||
</n-card>
|
</n-card>
|
||||||
<div v-else-if="sendMailModel.contentType == 'rich'" style="border: 1px solid #ccc">
|
<div v-else-if="sendMailModel.contentType == 'rich'" style="border: 1px solid #ccc">
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ onMounted(async () => {
|
|||||||
<div>
|
<div>
|
||||||
<n-tabs type="segment">
|
<n-tabs type="segment">
|
||||||
<n-tab-pane name="address" :tab="t('address')">
|
<n-tab-pane name="address" :tab="t('address')">
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="bind" :tab="t('bind')">
|
<n-tab-pane name="bind" :tab="t('bind')">
|
||||||
<Login :newAddressPath="newAddressPath" :bindUserAddress="bindAddress" />
|
<Login :newAddressPath="newAddressPath" :bindUserAddress="bindAddress" />
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center" v-if="settings.address">
|
<div class="center" v-if="settings.address">
|
||||||
<n-card v-if="enableWebhook" style="max-width: 800px; overflow: auto;">
|
<n-card :bordered="false" embedded v-if="enableWebhook" style="max-width: 800px; overflow: auto;">
|
||||||
<n-form-item-row label="URL">
|
<n-form-item-row label="URL">
|
||||||
<n-input v-model:value="webhookSettings.url" />
|
<n-input v-model:value="webhookSettings.url" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card v-if="curMail.message" style="max-width: 800px; overflow: auto;">
|
<n-card :bordered="false" embedded v-if="curMail.message" style="max-width: 800px; overflow: auto;">
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
ID: {{ curMail.id }}
|
ID: {{ curMail.id }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
|
|||||||
@@ -165,6 +165,6 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center" v-if="userSettings.user_email">
|
<div class="center" v-if="userSettings.user_email">
|
||||||
<n-card style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<Login />
|
<Login />
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,19 +37,19 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-card v-if="!userSettings.fetched">
|
<n-card :bordered="false" embedded v-if="!userSettings.fetched">
|
||||||
<n-skeleton style="height: 50vh" />
|
<n-skeleton style="height: 50vh" />
|
||||||
</n-card>
|
</n-card>
|
||||||
<div v-else-if="userSettings.user_email">
|
<div v-else-if="userSettings.user_email">
|
||||||
<n-alert type="success" :show-icon="false">
|
<n-alert type="success" :show-icon="false" :bordered="false">
|
||||||
<span>
|
<span>
|
||||||
<b>{{ t('currentUser') }} <b>{{ userSettings.user_email }}</b></b>
|
<b>{{ t('currentUser') }} <b>{{ userSettings.user_email }}</b></b>
|
||||||
</span>
|
</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="center">
|
<div v-else class="center">
|
||||||
<n-card style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<n-alert v-if="userJwt" type="warning" :show-icon="false" closable>
|
<n-alert v-if="userJwt" type="warning" :show-icon="false" :bordered="false" closable>
|
||||||
<span>{{ t('fetchUserSettingsError') }}</span>
|
<span>{{ t('fetchUserSettingsError') }}</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<UserLogin />
|
<UserLogin />
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ onMounted(async () => {
|
|||||||
{{ t('resetPassword') }}
|
{{ t('resetPassword') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-form>
|
</n-form>
|
||||||
<n-alert v-else :show-icon="false">
|
<n-alert v-else :show-icon="false" :bordered="false">
|
||||||
<span>
|
<span>
|
||||||
{{ t('cannotForgotPassword') }}
|
{{ t('cannotForgotPassword') }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center" v-if="userSettings.user_email">
|
<div class="center" v-if="userSettings.user_email">
|
||||||
<n-card>
|
<n-card :bordered="false" embedded>
|
||||||
<n-alert :show-icon="false">
|
<n-alert :show-icon="false" :bordered="false">
|
||||||
<span>
|
<span>
|
||||||
{{ t('passordTip') }}
|
{{ t('passordTip') }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
|||||||
items: [
|
items: [
|
||||||
{ text: '命令行部署准备', link: 'cli/pre-requisite' },
|
{ text: '命令行部署准备', link: 'cli/pre-requisite' },
|
||||||
{ text: 'D1 数据库', link: 'cli/d1' },
|
{ text: 'D1 数据库', link: 'cli/d1' },
|
||||||
{ text: '配置 DKIM', link: 'dkim' },
|
|
||||||
{ text: 'Cloudflare workers 后端', link: 'cli/worker' },
|
{ text: 'Cloudflare workers 后端', link: 'cli/worker' },
|
||||||
{ text: '配置邮件转发', link: 'email-routing.md' },
|
{ text: '配置邮件转发', link: 'email-routing.md' },
|
||||||
{ text: 'Cloudflare Pages 前端', link: 'cli/pages' },
|
{ text: 'Cloudflare Pages 前端', link: 'cli/pages' },
|
||||||
@@ -112,7 +111,6 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
|||||||
collapsed: true,
|
collapsed: true,
|
||||||
items: [
|
items: [
|
||||||
{ text: 'D1 数据库', link: 'ui/d1' },
|
{ text: 'D1 数据库', link: 'ui/d1' },
|
||||||
{ text: '配置 DKIM', link: 'dkim' },
|
|
||||||
{ text: 'Cloudflare workers 后端', link: 'ui/worker' },
|
{ text: 'Cloudflare workers 后端', link: 'ui/worker' },
|
||||||
{ text: '配置邮件转发', link: 'email-routing.md' },
|
{ text: '配置邮件转发', link: 'email-routing.md' },
|
||||||
{ text: 'Cloudflare Pages 前端', link: 'ui/pages' },
|
{ text: 'Cloudflare Pages 前端', link: 'ui/pages' },
|
||||||
|
|||||||
@@ -105,9 +105,6 @@ ENABLE_AUTO_REPLY = false
|
|||||||
# Turnstile verification configuration
|
# Turnstile verification configuration
|
||||||
# CF_TURNSTILE_SITE_KEY = ""
|
# CF_TURNSTILE_SITE_KEY = ""
|
||||||
# CF_TURNSTILE_SECRET_KEY = ""
|
# CF_TURNSTILE_SECRET_KEY = ""
|
||||||
# dkim config
|
|
||||||
# DKIM_SELECTOR = "mailchannels" # Refer to the DKIM section mailchannels._domainkey for mailchannels
|
|
||||||
# DKIM_PRIVATE_KEY = "" # Refer to the contents of priv_key.txt in the DKIM section
|
|
||||||
# telegram bot
|
# telegram bot
|
||||||
# TG_MAX_ACCOUNTS = 5
|
# TG_MAX_ACCOUNTS = 5
|
||||||
# global forward address list, if set, all emails will be forwarded to these addresses
|
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||||
@@ -159,39 +156,3 @@ pnpm run deploy
|
|||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Configure sending emails
|
|
||||||
|
|
||||||
Find the `SPF` record of `TXT` in the domain name `DNS` record, and add `include:relay.mailchannels.net`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
v=spf1 include:_spf.mx.cloudflare.net include:relay.mailchannels.net ~all
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a new `_mailchannels` record, the type is `TXT`, the content is `v=mc1 cfid=your worker domain name`
|
|
||||||
|
|
||||||
- The worker domain name here is the domain name of the back-end api. For example, if I deploy it at `https://temp-email-api.awsl.uk/`, fill in `v=mc1 cfid=awsl.uk`
|
|
||||||
- If your domain name is `https://temp-email-api.xxx.workers.dev`, fill in `v=mc1 cfid=xxx.workers.dev`
|
|
||||||
|
|
||||||
## Configure DKIM
|
|
||||||
|
|
||||||
Ref: [Adding-a-DKIM-Signature](https://support.mailchannels.com/hc/en-us/articles/7122849237389-Adding-a-DKIM-Signature)
|
|
||||||
|
|
||||||
Creating a DKIM private and public key:
|
|
||||||
Private key as PEM file and base64 encoded txt file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl genrsa 2048 | tee priv_key.pem | openssl rsa -outform der | openssl base64 -A > priv_key.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
Public key as DNS record:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo -n "v=DKIM1;p=" > pub_key_record.txt && \
|
|
||||||
openssl rsa -in priv_key.pem -pubout -outform der | openssl base64 -A >> pub_key_record.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
Add `TXT` record in `Cloudflare` all your mail domain `DNS`
|
|
||||||
|
|
||||||
- `_dmarc`: `v=DMARC1; p=none; adkim=r; aspf=r;`
|
|
||||||
- `mailchannels._domainkey`: `v=DKIM1; p=<content of the file pub_key_record.txt>`
|
|
||||||
|
|||||||
@@ -73,9 +73,6 @@ ENABLE_AUTO_REPLY = false
|
|||||||
# Turnstile 人机验证配置
|
# Turnstile 人机验证配置
|
||||||
# CF_TURNSTILE_SITE_KEY = ""
|
# CF_TURNSTILE_SITE_KEY = ""
|
||||||
# CF_TURNSTILE_SECRET_KEY = ""
|
# CF_TURNSTILE_SECRET_KEY = ""
|
||||||
# dkim config
|
|
||||||
# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels
|
|
||||||
# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容
|
|
||||||
# telegram bot 最多绑定邮箱数量
|
# telegram bot 最多绑定邮箱数量
|
||||||
# TG_MAX_ACCOUNTS = 5
|
# TG_MAX_ACCOUNTS = 5
|
||||||
# 全局转发地址列表,如果不配置则不启用,启用后所有邮件都会转发到列表中的地址
|
# 全局转发地址列表,如果不配置则不启用,启用后所有邮件都会转发到列表中的地址
|
||||||
|
|||||||
@@ -23,19 +23,3 @@ wrangler secret put RESEND_TOKEN
|
|||||||
wrangler secret put RESEND_TOKEN_XXX_COM
|
wrangler secret put RESEND_TOKEN_XXX_COM
|
||||||
wrangler secret put RESEND_TOKEN_DREAMHUNTER2333_XYZ
|
wrangler secret put RESEND_TOKEN_DREAMHUNTER2333_XYZ
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用 mailchannels 发送邮件
|
|
||||||
|
|
||||||
::: warning
|
|
||||||
[Mail Channels 免费电子邮件发送 API 将于2024年6月30日结束](https://support.mailchannels.com/hc/en-us/articles/26814255454093-End-of-Life-Notice-Cloudflare-Workers)
|
|
||||||
:::
|
|
||||||
|
|
||||||
1. 找到域名 `DNS` 记录的 `TXT` 的 `SPF` 记录, 增加 `include:relay.mailchannels.net`
|
|
||||||
|
|
||||||
`v=spf1 include:_spf.mx.cloudflare.net include:relay.mailchannels.net ~all`
|
|
||||||
|
|
||||||
2. 新建 `_mailchannels` 记录, 类型为 `TXT`, 内容为 `v=mc1 cfid=你的worker域名`
|
|
||||||
|
|
||||||
- 此处 worker 域名为后端 api 的域名,比如我部署在 `https://temp-email-api.awsl.uk/`,则填写 `v=mc1 cfid=awsl.uk`
|
|
||||||
|
|
||||||
- 如果你的域名是 `https://temp-email-api.xxx.workers.dev`,则填写 `v=mc1 cfid=xxx.workers.dev`
|
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
# 配置 DKIM
|
|
||||||
|
|
||||||
如果你不想配置 DKIM,可以跳过这一节。
|
|
||||||
|
|
||||||
参考: [Adding-a-DKIM-Signature](https://support.mailchannels.com/hc/en-us/articles/7122849237389-Adding-a-DKIM-Signature)
|
|
||||||
|
|
||||||
Creating a DKIM private and public key:
|
|
||||||
Private key as PEM file and base64 encoded txt file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
openssl genrsa 2048 | tee priv_key.pem | openssl rsa -outform der | openssl base64 -A > priv_key.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
Public key as DNS record:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo -n "v=DKIM1;p=" > pub_key_record.txt && \
|
|
||||||
openssl rsa -in priv_key.pem -pubout -outform der | openssl base64 -A >> pub_key_record.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
在 `Cloudflare` 的 `DNS` 记录中添加 `TXT` 记录
|
|
||||||
|
|
||||||
例如:
|
|
||||||
|
|
||||||
- `_dmarc`: `v=DMARC1; p=none; adkim=r; aspf=r;`
|
|
||||||
- `mailchannels._domainkey`: `v=DKIM1; p=<content of the file pub_key_record.txt>`
|
|
||||||
|
|
||||||
那我在 `wrangler.toml` 中的配置应该是这样的:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
DKIM_SELECTOR = "mailchannels"
|
|
||||||
DKIM_PRIVATE_KEY = "<priv_key.txt 的内容>"
|
|
||||||
```
|
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
# 配置子域名邮箱
|
# 配置子域名邮箱
|
||||||
|
|
||||||
|
::: warning
|
||||||
|
子域名邮箱发送邮件可能无法发送邮件,建议使用主域名邮箱发送邮件,子域名邮箱仅用于接收邮件。
|
||||||
|
:::
|
||||||
|
|
||||||
参考
|
参考
|
||||||
|
|
||||||
- [配置子域名邮箱](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/164#issuecomment-2082612710)
|
- [配置子域名邮箱](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/164#issuecomment-2082612710)
|
||||||
|
|||||||
@@ -127,6 +127,16 @@ api.get('/admin/mails_unknow', async (c) => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
api.delete('/admin/mails/:id', async (c) => {
|
||||||
|
const { id } = c.req.param();
|
||||||
|
const { success } = await c.env.DB.prepare(
|
||||||
|
`DELETE FROM raw_mails WHERE id = ? `
|
||||||
|
).bind(id).run();
|
||||||
|
return c.json({
|
||||||
|
success: success
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
api.get('/admin/address_sender', async (c) => {
|
api.get('/admin/address_sender', async (c) => {
|
||||||
const { address, limit, offset } = c.req.query();
|
const { address, limit, offset } = c.req.query();
|
||||||
if (address) {
|
if (address) {
|
||||||
@@ -166,6 +176,16 @@ api.post('/admin/address_sender', async (c) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
api.delete('/admin/address_sender/:id', async (c) => {
|
||||||
|
const { id } = c.req.param();
|
||||||
|
const { success } = await c.env.DB.prepare(
|
||||||
|
`DELETE FROM address_sender WHERE id = ? `
|
||||||
|
).bind(id).run();
|
||||||
|
return c.json({
|
||||||
|
success: success
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
api.get('/admin/sendbox', async (c) => {
|
api.get('/admin/sendbox', async (c) => {
|
||||||
const { address, limit, offset } = c.req.query();
|
const { address, limit, offset } = c.req.query();
|
||||||
if (address) {
|
if (address) {
|
||||||
@@ -182,6 +202,16 @@ api.get('/admin/sendbox', async (c) => {
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
api.delete('/admin/sendbox/:id', async (c) => {
|
||||||
|
const { id } = c.req.param();
|
||||||
|
const { success } = await c.env.DB.prepare(
|
||||||
|
`DELETE FROM sendbox WHERE id = ? `
|
||||||
|
).bind(id).run();
|
||||||
|
return c.json({
|
||||||
|
success: success
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
api.get('/admin/statistics', async (c) => {
|
api.get('/admin/statistics', async (c) => {
|
||||||
const { count: mailCount } = await c.env.DB.prepare(
|
const { count: mailCount } = await c.env.DB.prepare(
|
||||||
`SELECT count(*) as count FROM raw_mails`
|
`SELECT count(*) as count FROM raw_mails`
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createMimeMessage } from 'mimetext';
|
|||||||
import { Resend } from 'resend';
|
import { Resend } from 'resend';
|
||||||
|
|
||||||
import { CONSTANTS } from '../constants'
|
import { CONSTANTS } from '../constants'
|
||||||
import { getJsonSetting, getDomains, getIntValue } from '../utils';
|
import { getJsonSetting, getDomains, getIntValue, getBooleanValue } from '../utils';
|
||||||
import { GeoData } from '../models'
|
import { GeoData } from '../models'
|
||||||
import { handleListQuery } from '../common'
|
import { handleListQuery } from '../common'
|
||||||
import { HonoCustomType } from '../types';
|
import { HonoCustomType } from '../types';
|
||||||
@@ -89,64 +89,6 @@ const sendMailByResend = async (
|
|||||||
console.log(`Resend success: ${JSON.stringify(data)}`);
|
console.log(`Resend success: ${JSON.stringify(data)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendMailByMailChannels = async (
|
|
||||||
c: Context<HonoCustomType>, address: string,
|
|
||||||
reqJson: {
|
|
||||||
from_name: string, to_mail: string, to_name: string,
|
|
||||||
subject: string, content: string, is_html: boolean
|
|
||||||
}
|
|
||||||
): Promise<void> => {
|
|
||||||
/* eslint-disable prefer-const */
|
|
||||||
let {
|
|
||||||
from_name, to_mail, to_name,
|
|
||||||
subject, content, is_html
|
|
||||||
} = reqJson;
|
|
||||||
/* eslint-enable prefer-const */
|
|
||||||
from_name = from_name || address;
|
|
||||||
to_name = to_name || to_mail;
|
|
||||||
let dmikBody = {}
|
|
||||||
if (c.env.DKIM_SELECTOR && c.env.DKIM_PRIVATE_KEY && address.includes("@")) {
|
|
||||||
dmikBody = {
|
|
||||||
"dkim_domain": address.split("@")[1],
|
|
||||||
"dkim_selector": c.env.DKIM_SELECTOR,
|
|
||||||
"dkim_private_key": c.env.DKIM_PRIVATE_KEY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const body = {
|
|
||||||
"personalizations": [
|
|
||||||
{
|
|
||||||
"to": [{
|
|
||||||
"email": to_mail,
|
|
||||||
"name": to_name,
|
|
||||||
}],
|
|
||||||
...dmikBody,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"from": {
|
|
||||||
"email": address,
|
|
||||||
"name": from_name,
|
|
||||||
},
|
|
||||||
"subject": subject,
|
|
||||||
"content": [{
|
|
||||||
"type": is_html ? "text/html" : "text/plain",
|
|
||||||
"value": content,
|
|
||||||
}],
|
|
||||||
};
|
|
||||||
const send_request = new Request("https://api.mailchannels.net/tx/v1/send", {
|
|
||||||
"method": "POST",
|
|
||||||
"headers": {
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
"body": JSON.stringify(body),
|
|
||||||
});
|
|
||||||
const resp = await fetch(send_request);
|
|
||||||
const respText = await resp.text();
|
|
||||||
console.log(resp.status + " " + resp.statusText + ": " + respText);
|
|
||||||
if (resp.status >= 300) {
|
|
||||||
throw new Error(`Mailchannels error: ${resp.status} ${respText}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sendMail = async (
|
export const sendMail = async (
|
||||||
c: Context<HonoCustomType>, address: string,
|
c: Context<HonoCustomType>, address: string,
|
||||||
reqJson: {
|
reqJson: {
|
||||||
@@ -208,9 +150,8 @@ export const sendMail = async (
|
|||||||
else if (resendEnabled) {
|
else if (resendEnabled) {
|
||||||
await sendMailByResend(c, address, reqJson);
|
await sendMailByResend(c, address, reqJson);
|
||||||
}
|
}
|
||||||
// send by mailchannels
|
|
||||||
else {
|
else {
|
||||||
await sendMailByMailChannels(c, address, reqJson);
|
throw new Error("Please enable resend or verified address list")
|
||||||
}
|
}
|
||||||
// update balance
|
// update balance
|
||||||
if (!sendByVerifiedAddressList) {
|
if (!sendByVerifiedAddressList) {
|
||||||
@@ -292,3 +233,17 @@ api.get('/api/sendbox', async (c) => {
|
|||||||
const { limit, offset } = c.req.query();
|
const { limit, offset } = c.req.query();
|
||||||
return getSendbox(c, address, limit, offset);
|
return getSendbox(c, address, limit, offset);
|
||||||
})
|
})
|
||||||
|
|
||||||
|
api.delete('/api/sendbox/:id', async (c) => {
|
||||||
|
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||||
|
return c.text("User delete email is disabled", 403)
|
||||||
|
}
|
||||||
|
const { address } = c.get("jwtPayload")
|
||||||
|
const { id } = c.req.param();
|
||||||
|
const { success } = await c.env.DB.prepare(
|
||||||
|
`DELETE FROM sendbox WHERE address = ? and id = ? `
|
||||||
|
).bind(address, id).run();
|
||||||
|
return c.json({
|
||||||
|
success: success
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
4
worker/src/types.d.ts
vendored
4
worker/src/types.d.ts
vendored
@@ -33,10 +33,6 @@ export type Bindings = {
|
|||||||
S3_BUCKET: string | undefined
|
S3_BUCKET: string | undefined
|
||||||
S3_URL_EXPIRES: number | undefined
|
S3_URL_EXPIRES: number | undefined
|
||||||
|
|
||||||
// dkim
|
|
||||||
DKIM_SELECTOR: string | undefined
|
|
||||||
DKIM_PRIVATE_KEY: string | undefined
|
|
||||||
|
|
||||||
// cf turnstile
|
// cf turnstile
|
||||||
CF_TURNSTILE_SITE_KEY: string | undefined
|
CF_TURNSTILE_SITE_KEY: string | undefined
|
||||||
CF_TURNSTILE_SECRET_KEY: string | undefined
|
CF_TURNSTILE_SECRET_KEY: string | undefined
|
||||||
|
|||||||
@@ -47,9 +47,6 @@ ENABLE_AUTO_REPLY = false
|
|||||||
# Turnstile verification
|
# Turnstile verification
|
||||||
# CF_TURNSTILE_SITE_KEY = ""
|
# CF_TURNSTILE_SITE_KEY = ""
|
||||||
# CF_TURNSTILE_SECRET_KEY = ""
|
# CF_TURNSTILE_SECRET_KEY = ""
|
||||||
# dkim config
|
|
||||||
# DKIM_SELECTOR = ""
|
|
||||||
# DKIM_PRIVATE_KEY = ""
|
|
||||||
# telegram bot
|
# telegram bot
|
||||||
# TG_MAX_ACCOUNTS = 5
|
# TG_MAX_ACCOUNTS = 5
|
||||||
# global forward address list, if set, all emails will be forwarded to these addresses
|
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||||
|
|||||||
Reference in New Issue
Block a user