mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 15:38:34 +08:00
feat: support admin create address && add ENABLE_USER_CREATE_EMAIL config (#175)
This commit is contained in:
@@ -4,8 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
- "vitepress-docs/**"
|
- "vitepress-docs/**"
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|||||||
+6
-1
@@ -1,8 +1,13 @@
|
|||||||
# CHANGE LOG
|
# CHANGE LOG
|
||||||
|
|
||||||
|
## main branch to be released
|
||||||
|
|
||||||
|
- `ENABLE_USER_CREATE_EMAIL` 是否允许用户创建邮件
|
||||||
|
- 允许 admin 创建无前缀的邮件
|
||||||
|
|
||||||
## v0.3.0
|
## v0.3.0
|
||||||
|
|
||||||
### Breaking Changes:
|
### Breaking Changes
|
||||||
|
|
||||||
`address` 表的前缀将从代码中迁移到 db 中,请将下面 sql 中的 `tmp` 替换为你的前缀,然后执行。
|
`address` 表的前缀将从代码中迁移到 db 中,请将下面 sql 中的 `tmp` 替换为你的前缀,然后执行。
|
||||||
如果你的数据很重要,请先备份数据库。
|
如果你的数据很重要,请先备份数据库。
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ const getOpenSettings = async (message) => {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
adminContact: res["adminContact"] || "",
|
adminContact: res["adminContact"] || "",
|
||||||
|
enableUserCreateEmail: res["enableUserCreateEmail"] || false,
|
||||||
enableUserDeleteEmail: res["enableUserDeleteEmail"] || false,
|
enableUserDeleteEmail: res["enableUserDeleteEmail"] || false,
|
||||||
enableAutoReply: res["enableAutoReply"] || false,
|
enableAutoReply: res["enableAutoReply"] || false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { watch, onMounted, ref } from "vue";
|
import { watch, onMounted, ref, onBeforeUnmount } 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'
|
||||||
@@ -149,6 +149,10 @@ const deleteMail = async () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await refresh();
|
await refresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ export const useGlobalState = createGlobalState(
|
|||||||
prefix: '',
|
prefix: '',
|
||||||
needAuth: false,
|
needAuth: false,
|
||||||
adminContact: '',
|
adminContact: '',
|
||||||
|
enableUserCreateEmail: false,
|
||||||
enableUserDeleteEmail: false,
|
enableUserDeleteEmail: false,
|
||||||
enableAutoReply: false,
|
enableAutoReply: false,
|
||||||
domains: [{
|
domains: []
|
||||||
label: 'test.com',
|
|
||||||
value: 'test.com'
|
|
||||||
}]
|
|
||||||
})
|
})
|
||||||
const settings = ref({
|
const settings = ref({
|
||||||
fetched: false,
|
fetched: false,
|
||||||
@@ -38,7 +36,6 @@ export const useGlobalState = createGlobalState(
|
|||||||
const jwt = useStorage('jwt', '');
|
const jwt = useStorage('jwt', '');
|
||||||
const localeCache = useStorage('locale', 'zh');
|
const localeCache = useStorage('locale', 'zh');
|
||||||
const themeSwitch = useStorage('themeSwitch', false);
|
const themeSwitch = useStorage('themeSwitch', false);
|
||||||
const showLogin = ref(false);
|
|
||||||
const adminTab = ref("account");
|
const adminTab = ref("account");
|
||||||
const adminMailTabAddress = ref("");
|
const adminMailTabAddress = ref("");
|
||||||
const adminSendBoxTabAddress = ref("");
|
const adminSendBoxTabAddress = ref("");
|
||||||
@@ -55,7 +52,6 @@ export const useGlobalState = createGlobalState(
|
|||||||
themeSwitch,
|
themeSwitch,
|
||||||
adminAuth,
|
adminAuth,
|
||||||
showAdminAuth,
|
showAdminAuth,
|
||||||
showLogin,
|
|
||||||
adminTab,
|
adminTab,
|
||||||
adminMailTabAddress,
|
adminMailTabAddress,
|
||||||
adminSendBoxTabAddress,
|
adminSendBoxTabAddress,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import SenderAccess from './admin/SenderAccess.vue'
|
|||||||
import Statistics from "./admin/Statistics.vue"
|
import Statistics from "./admin/Statistics.vue"
|
||||||
import SendBox from './admin/SendBox.vue';
|
import SendBox from './admin/SendBox.vue';
|
||||||
import Account from './admin/Account.vue';
|
import Account from './admin/Account.vue';
|
||||||
|
import CreateAccount from './admin/CreateAccount.vue';
|
||||||
import Mails from './admin/Mails.vue';
|
import Mails from './admin/Mails.vue';
|
||||||
import MailsUnknow from './admin/MailsUnknow.vue';
|
import MailsUnknow from './admin/MailsUnknow.vue';
|
||||||
import Maintenance from './admin/Maintenance.vue';
|
import Maintenance from './admin/Maintenance.vue';
|
||||||
@@ -33,6 +34,7 @@ const { t } = useI18n({
|
|||||||
accessTip: 'Please enter the admin password',
|
accessTip: 'Please enter the admin password',
|
||||||
mails: 'Emails',
|
mails: 'Emails',
|
||||||
account: 'Account',
|
account: 'Account',
|
||||||
|
account_create: 'Create Account',
|
||||||
unknow: 'Mails with unknow receiver',
|
unknow: 'Mails with unknow receiver',
|
||||||
senderAccess: 'Sender Access Control',
|
senderAccess: 'Sender Access Control',
|
||||||
sendBox: 'Send Box',
|
sendBox: 'Send Box',
|
||||||
@@ -44,6 +46,7 @@ const { t } = useI18n({
|
|||||||
accessTip: '请输入 Admin 密码',
|
accessTip: '请输入 Admin 密码',
|
||||||
mails: '邮件',
|
mails: '邮件',
|
||||||
account: '账号',
|
account: '账号',
|
||||||
|
account_create: '创建账号',
|
||||||
unknow: '无收件人邮件',
|
unknow: '无收件人邮件',
|
||||||
senderAccess: '发件权限控制',
|
senderAccess: '发件权限控制',
|
||||||
sendBox: '发件箱',
|
sendBox: '发件箱',
|
||||||
@@ -64,10 +67,7 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-modal v-model:show="showAdminAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
<n-modal v-model:show="showAdminAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
||||||
title="Dialog">
|
:title="t('accessHeader')">
|
||||||
<template #header>
|
|
||||||
<div>{{ t('accessHeader') }}</div>
|
|
||||||
</template>
|
|
||||||
<p>{{ t('accessTip') }}</p>
|
<p>{{ t('accessTip') }}</p>
|
||||||
<n-input v-model:value="adminAuth" type="textarea" :autosize="{ minRows: 3 }" />
|
<n-input v-model:value="adminAuth" type="textarea" :autosize="{ minRows: 3 }" />
|
||||||
<template #action>
|
<template #action>
|
||||||
@@ -81,6 +81,9 @@ onMounted(async () => {
|
|||||||
<n-tab-pane name="account" :tab="t('account')">
|
<n-tab-pane name="account" :tab="t('account')">
|
||||||
<Account />
|
<Account />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="account_create" :tab="t('account_create')">
|
||||||
|
<CreateAccount />
|
||||||
|
</n-tab-pane>
|
||||||
<n-tab-pane name="mails" :tab="t('mails')">
|
<n-tab-pane name="mails" :tab="t('mails')">
|
||||||
<Mails />
|
<Mails />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ const {
|
|||||||
jwt, localeCache, toggleDark, isDark,
|
jwt, localeCache, toggleDark, isDark,
|
||||||
showAuth, adminAuth, auth, loading
|
showAuth, adminAuth, auth, loading
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const { showLogin, openSettings, settings } = useGlobalState()
|
const { openSettings, settings } = useGlobalState()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const isAdminRoute = computed(() => route.path.includes('admin'))
|
const isAdminRoute = computed(() => route.path.includes('admin'))
|
||||||
|
|
||||||
const showMobileMenu = ref(false)
|
const showMobileMenu = ref(false)
|
||||||
const showNewEmail = ref(false)
|
const tabValue = ref('signin')
|
||||||
const showLogout = ref(false)
|
const showLogout = ref(false)
|
||||||
const showDelteAccount = ref(false)
|
const showDelteAccount = ref(false)
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
@@ -33,6 +33,10 @@ const emailName = ref("")
|
|||||||
const emailDomain = ref("")
|
const emailDomain = ref("")
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
|
if (!password.value) {
|
||||||
|
message.error(t('passwordInput'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
jwt.value = password.value;
|
jwt.value = password.value;
|
||||||
await api.getSettings()
|
await api.getSettings()
|
||||||
@@ -96,14 +100,16 @@ const { t } = useI18n({
|
|||||||
fetchAddressError: 'Login password is invalid or account not exist, it may be network connection issue, please try again later.',
|
fetchAddressError: 'Login password is invalid or account not exist, it may be network connection issue, please try again later.',
|
||||||
mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.',
|
mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.',
|
||||||
generateName: 'Generate Fake Name',
|
generateName: 'Generate Fake Name',
|
||||||
|
help: 'Help',
|
||||||
|
passwordInput: 'Please input the password',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
title: 'Cloudflare 临时邮件',
|
title: 'Cloudflare 临时邮件',
|
||||||
dark: '暗色',
|
dark: '暗色',
|
||||||
light: '亮色',
|
light: '亮色',
|
||||||
login: '登录',
|
login: '登录',
|
||||||
logout: '登出',
|
logout: '退出登录',
|
||||||
logoutConfirm: '确定要登出吗?',
|
logoutConfirm: '确定要退出登录吗?',
|
||||||
delteAccount: "删除账户",
|
delteAccount: "删除账户",
|
||||||
delteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
delteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
||||||
accessHeader: '访问密码',
|
accessHeader: '访问密码',
|
||||||
@@ -115,7 +121,7 @@ const { t } = useI18n({
|
|||||||
sendbox: '发件箱',
|
sendbox: '发件箱',
|
||||||
sendMail: '发送邮件',
|
sendMail: '发送邮件',
|
||||||
pleaseGetNewEmail: '请"登录"或点击 "获取新邮箱" 按钮来获取一个新的邮箱地址',
|
pleaseGetNewEmail: '请"登录"或点击 "获取新邮箱" 按钮来获取一个新的邮箱地址',
|
||||||
getNewEmail: '获取新邮箱',
|
getNewEmail: '注册新邮箱',
|
||||||
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 ., a-z, A-Z, 0-9',
|
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 ., a-z, A-Z, 0-9',
|
||||||
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
||||||
getNewEmailTip3: '你可以从下拉列表中选择一个域名。',
|
getNewEmailTip3: '你可以从下拉列表中选择一个域名。',
|
||||||
@@ -130,6 +136,8 @@ const { t } = useI18n({
|
|||||||
fetchAddressError: '登录密码无效或账号不存在,也可能是网络连接异常,请稍后再尝试。',
|
fetchAddressError: '登录密码无效或账号不存在,也可能是网络连接异常,请稍后再尝试。',
|
||||||
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
|
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
|
||||||
generateName: '生成随机名字',
|
generateName: '生成随机名字',
|
||||||
|
help: '帮助',
|
||||||
|
passwordInput: '请输入密码',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -349,7 +357,6 @@ const newEmail = async () => {
|
|||||||
);
|
);
|
||||||
jwt.value = res["jwt"];
|
jwt.value = res["jwt"];
|
||||||
await api.getSettings();
|
await api.getSettings();
|
||||||
showNewEmail.value = false;
|
|
||||||
showPassword.value = true;
|
showPassword.value = true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
@@ -417,68 +424,74 @@ onMounted(async () => {
|
|||||||
<n-alert type="info" show-icon>
|
<n-alert type="info" show-icon>
|
||||||
<span>
|
<span>
|
||||||
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
||||||
<n-button style="margin-left: 10px" @click="router.push('/send')" size="small" tertiary round
|
<n-button style="margin-left: 10px" @click="router.push('/send')" size="small" tertiary
|
||||||
type="primary">
|
type="primary">
|
||||||
<n-icon :component="SendFilled" /> {{ t('sendMail') }}
|
<n-icon :component="SendFilled" /> {{ t('sendMail') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary round type="primary">
|
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary type="primary">
|
||||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</span>
|
</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</div>
|
</div>
|
||||||
<n-card v-else>
|
<div v-else class="center">
|
||||||
<n-result status="info" :description="t('pleaseGetNewEmail')">
|
<n-card style="max-width: 600px;">
|
||||||
<template #footer>
|
<n-alert v-if="jwt" type="warning" show-icon>
|
||||||
<n-alert v-if="jwt" type="warning" show-icon>
|
<span>{{ t('fetchAddressError') }}</span>
|
||||||
<span>{{ t('fetchAddressError') }}</span>
|
</n-alert>
|
||||||
</n-alert>
|
<n-tabs v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||||
<n-button @click="showLogin = true" tertiary round type="primary">
|
<n-tab-pane name="signin" :tab="t('login')">
|
||||||
{{ t('login') }}
|
<n-form>
|
||||||
</n-button>
|
<n-form-item-row :label="t('password')" required>
|
||||||
<n-button @click="showNewEmail = true" tertiary round type="primary">
|
<n-input v-model:value="password" type="textarea" :autosize="{ minRows: 3 }" />
|
||||||
{{ t('getNewEmail') }}
|
</n-form-item-row>
|
||||||
</n-button>
|
<n-button @click="login" :loading="loading" type="primary" block secondary strong>
|
||||||
</template>
|
{{ t('login') }}
|
||||||
</n-result>
|
</n-button>
|
||||||
</n-card>
|
<n-button v-if="openSettings.enableUserCreateEmail" @click="tabValue = 'register'" block
|
||||||
|
secondary strong>
|
||||||
|
{{ t('getNewEmail') }}
|
||||||
|
</n-button>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane v-if="openSettings.enableUserCreateEmail" name="register" :tab="t('getNewEmail')">
|
||||||
|
<n-spin :show="generateNameLoading">
|
||||||
|
<n-form>
|
||||||
|
<span>
|
||||||
|
<p>{{ t("getNewEmailTip1") }}</p>
|
||||||
|
<p>{{ t("getNewEmailTip2") }}</p>
|
||||||
|
<p>{{ t("getNewEmailTip3") }}</p>
|
||||||
|
</span>
|
||||||
|
<n-button @click="generateName" style="margin-bottom: 10px;">
|
||||||
|
{{ t('generateName') }}
|
||||||
|
</n-button>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input-group-label v-if="openSettings.prefix">
|
||||||
|
{{ openSettings.prefix }}
|
||||||
|
</n-input-group-label>
|
||||||
|
<n-input v-model:value="emailName" />
|
||||||
|
<n-input-group-label>@</n-input-group-label>
|
||||||
|
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||||
|
:options="openSettings.domains" />
|
||||||
|
</n-input-group>
|
||||||
|
<n-button type="primary" block secondary strong @click="newEmail"
|
||||||
|
:loading="loading">
|
||||||
|
{{ t('ok') }}
|
||||||
|
</n-button>
|
||||||
|
</n-form>
|
||||||
|
</n-spin>
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="help" :tab="t('help')">
|
||||||
|
<n-alert type="info" show-icon>
|
||||||
|
<span>{{ t('pleaseGetNewEmail') }}</span>
|
||||||
|
</n-alert>
|
||||||
|
<AdminContact />
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<n-modal v-model:show="showNewEmail" preset="dialog" title="Dialog">
|
<n-modal v-model:show="showPassword" preset="dialog" :title="t('password')">
|
||||||
<template #header>
|
|
||||||
<div>{{ t('getNewEmail') }}</div>
|
|
||||||
</template>
|
|
||||||
<n-spin :show="generateNameLoading">
|
|
||||||
<span>
|
|
||||||
<p>{{ t("getNewEmailTip1") }}</p>
|
|
||||||
<p>{{ t("getNewEmailTip2") }}</p>
|
|
||||||
<p>{{ t("getNewEmailTip3") }}</p>
|
|
||||||
</span>
|
|
||||||
<n-button @click="generateName" style="margin-bottom: 10px;">
|
|
||||||
{{ t('generateName') }}
|
|
||||||
</n-button>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-group-label v-if="openSettings.prefix">
|
|
||||||
{{ openSettings.prefix }}
|
|
||||||
</n-input-group-label>
|
|
||||||
<n-input v-model:value="emailName" />
|
|
||||||
<n-input-group-label>@</n-input-group-label>
|
|
||||||
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
|
||||||
:options="openSettings.domains" />
|
|
||||||
</n-input-group>
|
|
||||||
</n-spin>
|
|
||||||
<template #action>
|
|
||||||
<n-button @click="showNewEmail = false">
|
|
||||||
{{ t('cancel') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="newEmail" type="primary" :loading="loading">
|
|
||||||
{{ t('ok') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showPassword" preset="dialog" title="Dialog">
|
|
||||||
<template #header>
|
|
||||||
<div>{{ t("password") }}</div>
|
|
||||||
</template>
|
|
||||||
<span>
|
<span>
|
||||||
<p>{{ t("passwordTip") }}</p>
|
<p>{{ t("passwordTip") }}</p>
|
||||||
</span>
|
</span>
|
||||||
@@ -488,51 +501,26 @@ onMounted(async () => {
|
|||||||
<template #action>
|
<template #action>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal v-model:show="showLogin" preset="dialog" title="Dialog">
|
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
||||||
<template #header>
|
|
||||||
<div>{{ t('login') }}</div>
|
|
||||||
</template>
|
|
||||||
<AdminContact />
|
|
||||||
<n-input v-model:value="password" type="textarea" :autosize="{
|
|
||||||
minRows: 3
|
|
||||||
}" />
|
|
||||||
<template #action>
|
|
||||||
<n-button @click="login" :loading="loading" size="small" tertiary round type="primary">
|
|
||||||
{{ t('login') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showLogout" preset="dialog" title="Dialog">
|
|
||||||
<template #header>
|
|
||||||
<div>{{ t('logout') }}</div>
|
|
||||||
</template>
|
|
||||||
<p>{{ t('logoutConfirm') }}</p>
|
<p>{{ t('logoutConfirm') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="logout" size="small" tertiary round type="primary">
|
<n-button :loading="loading" @click="logout" size="small" tertiary type="primary">
|
||||||
{{ t('logout') }}
|
{{ t('logout') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal v-model:show="showDelteAccount" preset="dialog" title="Dialog">
|
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
|
||||||
<template #header>
|
|
||||||
<div>{{ t('delteAccount') }}</div>
|
|
||||||
</template>
|
|
||||||
<p>{{ t('delteAccountConfirm') }}</p>
|
<p>{{ t('delteAccountConfirm') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="deleteAccount" size="small" tertiary round type="error">
|
<n-button :loading="loading" @click="deleteAccount" size="small" tertiary type="error">
|
||||||
{{ t('delteAccount') }}
|
{{ t('delteAccount') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal v-model:show="showAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
<n-modal v-model:show="showAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
||||||
title="Dialog">
|
:title="t('accessHeader')">
|
||||||
<template #header>
|
|
||||||
<div>{{ t('accessHeader') }}</div>
|
|
||||||
</template>
|
|
||||||
<p>{{ t('accessTip') }}</p>
|
<p>{{ t('accessTip') }}</p>
|
||||||
<n-input v-model:value="auth" type="textarea" :autosize="{
|
<n-input v-model:value="auth" type="textarea" :autosize="{ minRows: 3 }" />
|
||||||
minRows: 3
|
|
||||||
}" />
|
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="authFunc" type="primary">
|
<n-button :loading="loading" @click="authFunc" type="primary">
|
||||||
{{ t('ok') }}
|
{{ t('ok') }}
|
||||||
@@ -563,4 +551,16 @@ onMounted(async () => {
|
|||||||
.n-card {
|
.n-card {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
place-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-form .n-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ onMounted(async () => {
|
|||||||
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
|
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
|
||||||
<p>{{ t('deleteTip') }}</p>
|
<p>{{ t('deleteTip') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="deleteEmail" size="small" tertiary round type="error">
|
<n-button :loading="loading" @click="deleteEmail" size="small" tertiary type="error">
|
||||||
{{ t('delteAccount') }}
|
{{ t('delteAccount') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
import { api } from '../../api'
|
||||||
|
|
||||||
|
const {
|
||||||
|
localeCache, loading, openSettings,
|
||||||
|
} = useGlobalState()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
address: 'Address',
|
||||||
|
enablePrefix: 'If enable Prefix',
|
||||||
|
creatNewEmail: 'Get New Email',
|
||||||
|
fillInAllFields: 'Please fill in all fields',
|
||||||
|
successTip: 'Success Created',
|
||||||
|
password: 'Password',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
address: '地址',
|
||||||
|
enablePrefix: '是否启用前缀',
|
||||||
|
creatNewEmail: '创建新邮箱',
|
||||||
|
fillInAllFields: '请填写完整信息',
|
||||||
|
successTip: '创建成功',
|
||||||
|
password: '密码',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const enablePrefix = ref(true)
|
||||||
|
const emailName = ref("")
|
||||||
|
const emailDomain = ref("")
|
||||||
|
const showReultModal = ref(false)
|
||||||
|
const result = ref("")
|
||||||
|
|
||||||
|
const newEmail = async () => {
|
||||||
|
if (!emailName.value || !emailDomain.value) {
|
||||||
|
message.error(t('fillInAllFields'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await api.fetch(`/admin/new_address`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
enablePrefix: enablePrefix.value,
|
||||||
|
name: emailName.value,
|
||||||
|
domain: emailDomain.value,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
result.value = res["jwt"];
|
||||||
|
message.success(t('successTip'))
|
||||||
|
showReultModal.value = true
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (openSettings.prefix) {
|
||||||
|
enablePrefix.value = true
|
||||||
|
}
|
||||||
|
emailDomain.value = openSettings.value.domains?.[0]?.value || ""
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="center">
|
||||||
|
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('password')">
|
||||||
|
<p>{{ t('password') }}</p>
|
||||||
|
<n-card>
|
||||||
|
<b>{{ result }}</b>
|
||||||
|
</n-card>
|
||||||
|
</n-modal>
|
||||||
|
<n-card style="max-width: 600px;">
|
||||||
|
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
|
||||||
|
<n-checkbox v-model:checked="enablePrefix" />
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('address')">
|
||||||
|
<n-input-group>
|
||||||
|
<n-input-group-label v-if="enablePrefix && openSettings.prefix">
|
||||||
|
{{ openSettings.prefix }}
|
||||||
|
</n-input-group-label>
|
||||||
|
<n-input v-model:value="emailName" />
|
||||||
|
<n-input-group-label>@</n-input-group-label>
|
||||||
|
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||||
|
:options="openSettings.domains" />
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item-row>
|
||||||
|
<div class="right">
|
||||||
|
<n-button @click="newEmail" type="primary" :loading="loading">
|
||||||
|
{{ t('creatNewEmail') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
place-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
text-align: right;
|
||||||
|
place-items: right;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -163,7 +163,7 @@ onMounted(async () => {
|
|||||||
<n-input-number v-model:value="senderBalance" :min="0" :max="1000" />
|
<n-input-number v-model:value="senderBalance" :min="0" :max="1000" />
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="updateData()" size="small" tertiary round type="primary">
|
<n-button :loading="loading" @click="updateData()" size="small" tertiary type="primary">
|
||||||
{{ t('ok') }}
|
{{ t('ok') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ PREFIX = "tmp" # The mailbox name prefix to be processed
|
|||||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # your domain name
|
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # your domain name
|
||||||
JWT_SECRET = "xxx" # Key used to generate jwt
|
JWT_SECRET = "xxx" # Key used to generate jwt
|
||||||
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
|
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
|
||||||
|
# Allow users to create email addresses
|
||||||
|
ENABLE_USER_CREATE_EMAIL = true
|
||||||
# Allow users to delete messages
|
# Allow users to delete messages
|
||||||
ENABLE_USER_DELETE_EMAIL = true
|
ENABLE_USER_DELETE_EMAIL = true
|
||||||
# Allow automatic replies to emails
|
# Allow automatic replies to emails
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空
|
|||||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
|
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
|
||||||
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
|
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
|
||||||
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
||||||
|
# 是否允许用户创建邮件, 不配置则不允许
|
||||||
|
ENABLE_USER_CREATE_EMAIL = true
|
||||||
# 允许用户删除邮件, 不配置则不允许
|
# 允许用户删除邮件, 不配置则不允许
|
||||||
ENABLE_USER_DELETE_EMAIL = true
|
ENABLE_USER_DELETE_EMAIL = true
|
||||||
# 允许自动回复邮件
|
# 允许自动回复邮件
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
import { getSendbox } from './send_mail_api'
|
|
||||||
import { sendAdminInternalMail } from './utils'
|
import { sendAdminInternalMail } from './utils'
|
||||||
|
import { newAddress } from './common'
|
||||||
|
|
||||||
const api = new Hono()
|
const api = new Hono()
|
||||||
|
|
||||||
@@ -54,6 +54,14 @@ api.get('/admin/address', async (c) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
api.post('/admin/new_address', async (c) => {
|
||||||
|
let { name, domain, enablePrefix } = await c.req.json();
|
||||||
|
if (!name) {
|
||||||
|
return c.text("Please provide a name", 400)
|
||||||
|
}
|
||||||
|
return newAddress(c, name, domain, enablePrefix);
|
||||||
|
})
|
||||||
|
|
||||||
api.delete('/admin/delete_address/:id', async (c) => {
|
api.delete('/admin/delete_address/:id', async (c) => {
|
||||||
const { id } = c.req.param();
|
const { id } = c.req.param();
|
||||||
const { success } = await c.env.DB.prepare(
|
const { success } = await c.env.DB.prepare(
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
|
|
||||||
|
import { getDomains } from './utils';
|
||||||
|
|
||||||
|
export const newAddress = async (c, name, domain, enablePrefix) => {
|
||||||
|
// remove special characters
|
||||||
|
name = name.replace(/[^a-zA-Z0-9.]/g, '')
|
||||||
|
// check name length
|
||||||
|
if (name.length < 0) {
|
||||||
|
return c.text("Name too short", 400)
|
||||||
|
}
|
||||||
|
if (name.length > 100) {
|
||||||
|
return c.text("Name too long (max 100)", 400)
|
||||||
|
}
|
||||||
|
// check domain, generate random domain
|
||||||
|
const domains = getDomains(c);
|
||||||
|
if (!domain || !domains.includes(domain)) {
|
||||||
|
domain = domains[Math.floor(Math.random() * domains.length)];
|
||||||
|
}
|
||||||
|
// create address
|
||||||
|
if (enablePrefix) {
|
||||||
|
name = c.env.PREFIX + name + "@" + domain;
|
||||||
|
} else {
|
||||||
|
name = name + "@" + domain;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { success } = await c.env.DB.prepare(
|
||||||
|
`INSERT INTO address(name) VALUES(?)`
|
||||||
|
).bind(name).run();
|
||||||
|
if (!success) {
|
||||||
|
return c.text("Failed to create address", 500)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message && e.message.includes("UNIQUE")) {
|
||||||
|
return c.text("Address already exists, please retry a new address", 400)
|
||||||
|
}
|
||||||
|
return c.text("Failed to create address", 500)
|
||||||
|
}
|
||||||
|
let address_id = 0;
|
||||||
|
try {
|
||||||
|
address_id = await c.env.DB.prepare(
|
||||||
|
`SELECT id FROM address where name = ?`
|
||||||
|
).bind(name).first("id");
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
// create jwt
|
||||||
|
const jwt = await Jwt.sign({
|
||||||
|
address: name,
|
||||||
|
address_id: address_id
|
||||||
|
}, c.env.JWT_SECRET)
|
||||||
|
return c.json({
|
||||||
|
jwt: jwt
|
||||||
|
})
|
||||||
|
}
|
||||||
+6
-46
@@ -1,7 +1,7 @@
|
|||||||
import { Hono } from 'hono'
|
import { Hono } from 'hono'
|
||||||
import { Jwt } from 'hono/utils/jwt'
|
|
||||||
|
|
||||||
import { getDomains, getPasswords } from './utils';
|
import { getDomains, getPasswords } from './utils';
|
||||||
|
import { newAddress } from './common'
|
||||||
|
|
||||||
const api = new Hono()
|
const api = new Hono()
|
||||||
|
|
||||||
@@ -153,62 +153,22 @@ api.get('/open_api/settings', async (c) => {
|
|||||||
"domains": getDomains(c),
|
"domains": getDomains(c),
|
||||||
"needAuth": needAuth,
|
"needAuth": needAuth,
|
||||||
"adminContact": c.env.ADMIN_CONTACT,
|
"adminContact": c.env.ADMIN_CONTACT,
|
||||||
|
"enableUserCreateEmail": c.env.ENABLE_USER_CREATE_EMAIL,
|
||||||
"enableUserDeleteEmail": c.env.ENABLE_USER_DELETE_EMAIL,
|
"enableUserDeleteEmail": c.env.ENABLE_USER_DELETE_EMAIL,
|
||||||
"enableAutoReply": c.env.ENABLE_AUTO_REPLY,
|
"enableAutoReply": c.env.ENABLE_AUTO_REPLY,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
api.get('/api/new_address', async (c) => {
|
api.get('/api/new_address', async (c) => {
|
||||||
|
if (!c.env.ENABLE_USER_CREATE_EMAIL) {
|
||||||
|
return c.text("New address is disabled", 403)
|
||||||
|
}
|
||||||
let { name, domain } = c.req.query();
|
let { name, domain } = c.req.query();
|
||||||
// if no name, generate random name
|
// if no name, generate random name
|
||||||
if (!name) {
|
if (!name) {
|
||||||
name = Math.random().toString(36).substring(2, 15);
|
name = Math.random().toString(36).substring(2, 15);
|
||||||
}
|
}
|
||||||
// remove special characters
|
return newAddress(c, name, domain, true);
|
||||||
name = name.replace(/[^a-zA-Z0-9.]/g, '')
|
|
||||||
// check name length
|
|
||||||
if (name.length < 0) {
|
|
||||||
return c.text("Name too short", 400)
|
|
||||||
}
|
|
||||||
if (name.length > 100) {
|
|
||||||
return c.text("Name too long (max 100)", 400)
|
|
||||||
}
|
|
||||||
// check domain, generate random domain
|
|
||||||
const domains = getDomains(c);
|
|
||||||
if (!domain || !domains.includes(domain)) {
|
|
||||||
domain = domains[Math.floor(Math.random() * domains.length)];
|
|
||||||
}
|
|
||||||
// create address
|
|
||||||
name = c.env.PREFIX + name + "@" + domain
|
|
||||||
try {
|
|
||||||
const { success } = await c.env.DB.prepare(
|
|
||||||
`INSERT INTO address(name) VALUES(?)`
|
|
||||||
).bind(name).run();
|
|
||||||
if (!success) {
|
|
||||||
return c.text("Failed to create address", 500)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message && e.message.includes("UNIQUE")) {
|
|
||||||
return c.text("Please retry a new address", 400)
|
|
||||||
}
|
|
||||||
return c.text("Failed to create address", 500)
|
|
||||||
}
|
|
||||||
let address_id = 0;
|
|
||||||
try {
|
|
||||||
address_id = await c.env.DB.prepare(
|
|
||||||
`SELECT id FROM address where name = ?`
|
|
||||||
).bind(name).first("id");
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
// create jwt
|
|
||||||
const jwt = await Jwt.sign({
|
|
||||||
address: name,
|
|
||||||
address_id: address_id
|
|
||||||
}, c.env.JWT_SECRET)
|
|
||||||
return c.json({
|
|
||||||
jwt: jwt
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
api.delete('/api/delete_address', async (c) => {
|
api.delete('/api/delete_address', async (c) => {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ PREFIX = "tmp"
|
|||||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
|
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
|
||||||
JWT_SECRET = "xxx"
|
JWT_SECRET = "xxx"
|
||||||
BLACK_LIST = ""
|
BLACK_LIST = ""
|
||||||
|
# Allow users to create email addresses
|
||||||
|
ENABLE_USER_CREATE_EMAIL = true
|
||||||
# Allow users to delete messages
|
# Allow users to delete messages
|
||||||
ENABLE_USER_DELETE_EMAIL = true
|
ENABLE_USER_DELETE_EMAIL = true
|
||||||
# Allow automatic replies to emails
|
# Allow automatic replies to emails
|
||||||
|
|||||||
Reference in New Issue
Block a user