mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-05 15:38:34 +08:00
feat: admin search mailbox && fix generateName multi dot && user jwt exp in 30 days && UI globalTabplacement && useSideMargin (#214)
* fix: generateName multi dot && user jwt exp in 30 days * feat: support admin search mailbox * fix: DELETE mail bug(should be raw_mails) * feat: UI add globalTabplacement * feat: UI add useSideMargin option
This commit is contained in:
@@ -20,6 +20,11 @@
|
|||||||
### function changs
|
### function changs
|
||||||
|
|
||||||
- 增加用户注册功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证
|
- 增加用户注册功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证
|
||||||
|
- 增加默认以文本显示邮件,文本和HTML邮箱显示方式切换按钮
|
||||||
|
- 修复 `BUG` 随机生成的邮箱名字不合法 #211
|
||||||
|
- `admin` 邮件页面支持邮件内容搜索 #210
|
||||||
|
- 修复删除地址时邮件未删除的BUG #213
|
||||||
|
- UI 增加全局标签页位置配置, 侧边距配置
|
||||||
|
|
||||||
## v0.3.3
|
## v0.3.3
|
||||||
|
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import Header from './views/Header.vue';
|
|||||||
import Footer from './views/Footer.vue';
|
import Footer from './views/Footer.vue';
|
||||||
|
|
||||||
|
|
||||||
const { localeCache, isDark, loading } = useGlobalState()
|
const { localeCache, isDark, loading, useSideMargin } = useGlobalState()
|
||||||
const theme = computed(() => isDark.value ? darkTheme : null)
|
const theme = computed(() => isDark.value ? darkTheme : null)
|
||||||
const localeConfig = computed(() => localeCache.value == 'zh' ? zhCN : null)
|
const localeConfig = computed(() => localeCache.value == 'zh' ? zhCN : null)
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
const showSideMargin = computed(() => !isMobile.value && !useSideMargin.value);
|
||||||
|
|
||||||
const { locale } = useI18n({
|
const { locale } = useI18n({
|
||||||
useScope: 'global',
|
useScope: 'global',
|
||||||
@@ -39,8 +40,8 @@ onMounted(async () => {
|
|||||||
<n-spin description="loading..." :show="loading">
|
<n-spin description="loading..." :show="loading">
|
||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
<n-grid x-gap="12" :cols="12">
|
<n-grid x-gap="12" :cols="12">
|
||||||
<n-gi v-if="!isMobile" span="1"></n-gi>
|
<n-gi v-if="!showSideMargin" span="1"></n-gi>
|
||||||
<n-gi :span="isMobile ? 12 : 10">
|
<n-gi :span="showSideMargin ? 12 : 10">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<n-space vertical>
|
<n-space vertical>
|
||||||
<n-layout style="min-height: 80vh;">
|
<n-layout style="min-height: 80vh;">
|
||||||
@@ -51,7 +52,7 @@ onMounted(async () => {
|
|||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<n-gi v-if="!isMobile" span="1"></n-gi>
|
<n-gi v-if="!showSideMargin" span="1"></n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
<n-back-top />
|
<n-back-top />
|
||||||
</n-message-provider>
|
</n-message-provider>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const {
|
|||||||
|
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL: API_BASE,
|
baseURL: API_BASE,
|
||||||
timeout: 10000
|
timeout: 30000
|
||||||
});
|
});
|
||||||
|
|
||||||
const apiFetch = async (path, options = {}) => {
|
const apiFetch = async (path, options = {}) => {
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ export const useGlobalState = createGlobalState(
|
|||||||
const userJwt = useStorage('userJwt', '');
|
const userJwt = useStorage('userJwt', '');
|
||||||
const userTab = useStorage('userTab', 'user_settings');
|
const userTab = useStorage('userTab', 'user_settings');
|
||||||
const indexTab = useStorage('indexTab', 'mailbox');
|
const indexTab = useStorage('indexTab', 'mailbox');
|
||||||
|
const globalTabplacement = useStorage('globalTabplacement', 'top');
|
||||||
|
const useSideMargin = useStorage('useSideMargin', true);
|
||||||
const userOpenSettings = ref({
|
const userOpenSettings = ref({
|
||||||
enable: false,
|
enable: false,
|
||||||
enableMailVerify: false,
|
enableMailVerify: false,
|
||||||
@@ -91,6 +93,8 @@ export const useGlobalState = createGlobalState(
|
|||||||
indexTab,
|
indexTab,
|
||||||
userOpenSettings,
|
userOpenSettings,
|
||||||
userSettings,
|
userSettings,
|
||||||
|
globalTabplacement,
|
||||||
|
useSideMargin,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ import UserSettings from './admin/UserSettings.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';
|
||||||
|
import Appearance from './common/Appearance.vue';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
localeCache, adminAuth, showAdminAuth, adminTab, loading
|
localeCache, adminAuth, showAdminAuth, adminTab, loading, globalTabplacement
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
@@ -44,7 +45,9 @@ const { t } = useI18n({
|
|||||||
unknow: 'Mails with unknow receiver',
|
unknow: 'Mails with unknow receiver',
|
||||||
senderAccess: 'Sender Access Control',
|
senderAccess: 'Sender Access Control',
|
||||||
sendBox: 'Send Box',
|
sendBox: 'Send Box',
|
||||||
|
statistics: 'Statistics',
|
||||||
maintenance: 'Maintenance',
|
maintenance: 'Maintenance',
|
||||||
|
appearance: 'Appearance',
|
||||||
ok: 'OK',
|
ok: 'OK',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
@@ -59,7 +62,9 @@ const { t } = useI18n({
|
|||||||
unknow: '无收件人邮件',
|
unknow: '无收件人邮件',
|
||||||
senderAccess: '发件权限控制',
|
senderAccess: '发件权限控制',
|
||||||
sendBox: '发件箱',
|
sendBox: '发件箱',
|
||||||
|
statistics: '统计',
|
||||||
maintenance: '维护',
|
maintenance: '维护',
|
||||||
|
appearance: '外观',
|
||||||
ok: '确定',
|
ok: '确定',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,8 +90,7 @@ onMounted(async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<Statistics />
|
<n-tabs type="card" v-model:value="adminTab" :placement="globalTabplacement">
|
||||||
<n-tabs type="card" v-model:value="adminTab">
|
|
||||||
<n-tab-pane name="account" :tab="t('account')">
|
<n-tab-pane name="account" :tab="t('account')">
|
||||||
<Account />
|
<Account />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
@@ -114,9 +118,15 @@ onMounted(async () => {
|
|||||||
<n-tab-pane name="sendBox" :tab="t('sendBox')">
|
<n-tab-pane name="sendBox" :tab="t('sendBox')">
|
||||||
<SendBox />
|
<SendBox />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="statistics" :tab="t('statistics')">
|
||||||
|
<Statistics />
|
||||||
|
</n-tab-pane>
|
||||||
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
||||||
<Maintenance />
|
<Maintenance />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="appearance" :tab="t('appearance')">
|
||||||
|
<Appearance />
|
||||||
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import SendBox from './index/SendBox.vue';
|
|||||||
import SendMail from './index/SendMail.vue';
|
import SendMail from './index/SendMail.vue';
|
||||||
import AccountSettings from './index/AccountSettings.vue';
|
import AccountSettings from './index/AccountSettings.vue';
|
||||||
|
|
||||||
const { localeCache, settings, openSettings, indexTab } = useGlobalState()
|
const { localeCache, settings, openSettings, indexTab, globalTabplacement } = useGlobalState()
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
locale: localeCache.value || 'zh',
|
locale: localeCache.value || 'zh',
|
||||||
@@ -45,7 +45,7 @@ const deleteMail = async (curMailId) => {
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<AddressBar />
|
<AddressBar />
|
||||||
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab">
|
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
|
||||||
<n-tab-pane name="mailbox" :tab="t('mailbox')">
|
<n-tab-pane name="mailbox" :tab="t('mailbox')">
|
||||||
<MailBox :showEMailTo="false" :showReply="true" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
<MailBox :showEMailTo="false" :showReply="true" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import UserBar from './user/UserBar.vue';
|
|||||||
import BindAddress from './user/BindAddress.vue';
|
import BindAddress from './user/BindAddress.vue';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
localeCache, userTab, userOpenSettings, userSettings
|
localeCache, userTab, globalTabplacement, userSettings
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
@@ -33,7 +33,7 @@ const { t } = useI18n({
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<UserBar />
|
<UserBar />
|
||||||
<n-tabs v-if="userSettings.user_email" type="card" v-model:value="userTab">
|
<n-tabs v-if="userSettings.user_email" type="card" v-model:value="userTab" :placement="globalTabplacement">
|
||||||
<n-tab-pane name="address_management" :tab="t('address_management')">
|
<n-tab-pane name="address_management" :tab="t('address_management')">
|
||||||
<AddressMangement />
|
<AddressMangement />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted, watch } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
@@ -16,19 +16,27 @@ const { t } = useI18n({
|
|||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
addressQueryTip: 'Leave blank to query all addresses',
|
addressQueryTip: 'Leave blank to query all addresses',
|
||||||
|
keywordQueryTip: 'Leave blank to not query by keyword',
|
||||||
query: 'Query',
|
query: 'Query',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
addressQueryTip: '留空查询所有地址',
|
addressQueryTip: '留空查询所有地址',
|
||||||
|
keywordQueryTip: '留空不按关键字查询',
|
||||||
query: '查询',
|
query: '查询',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const mailBoxKey = ref("")
|
const mailBoxKey = ref("")
|
||||||
|
const mailKeyword = ref("")
|
||||||
|
|
||||||
const queryAddress = () => {
|
watch([adminMailTabAddress, mailKeyword], () => {
|
||||||
mailBoxKey.value = adminMailTabAddress.value;
|
adminMailTabAddress.value = adminMailTabAddress.value.trim();
|
||||||
|
mailKeyword.value = mailKeyword.value.trim();
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryMail = () => {
|
||||||
|
mailBoxKey.value = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchMailData = async (limit, offset) => {
|
const fetchMailData = async (limit, offset) => {
|
||||||
@@ -37,6 +45,7 @@ const fetchMailData = async (limit, offset) => {
|
|||||||
+ `?limit=${limit}`
|
+ `?limit=${limit}`
|
||||||
+ `&offset=${offset}`
|
+ `&offset=${offset}`
|
||||||
+ (adminMailTabAddress.value ? `&address=${adminMailTabAddress.value}` : '')
|
+ (adminMailTabAddress.value ? `&address=${adminMailTabAddress.value}` : '')
|
||||||
|
+ (mailKeyword.value ? `&keyword=${mailKeyword.value}` : '')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +61,8 @@ onMounted(async () => {
|
|||||||
<div>
|
<div>
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')" />
|
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')" />
|
||||||
<n-button @click="queryAddress" type="primary" tertiary>
|
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" />
|
||||||
|
<n-button @click="queryMail" type="primary" tertiary>
|
||||||
{{ t('query') }}
|
{{ t('query') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const { t } = useI18n({
|
|||||||
autoCleanup: "Auto cleanup",
|
autoCleanup: "Auto cleanup",
|
||||||
cleanupSuccess: "Cleanup success",
|
cleanupSuccess: "Cleanup success",
|
||||||
save: "Save",
|
save: "Save",
|
||||||
|
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document",
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
tip: '请输入清理天数',
|
tip: '请输入清理天数',
|
||||||
@@ -43,6 +44,7 @@ const { t } = useI18n({
|
|||||||
cleanupSuccess: "清理成功",
|
cleanupSuccess: "清理成功",
|
||||||
cleanupNow: "立即清理",
|
cleanupNow: "立即清理",
|
||||||
save: "保存",
|
save: "保存",
|
||||||
|
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -93,6 +95,9 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card>
|
<n-card>
|
||||||
|
<n-alert :show-icon="false">
|
||||||
|
<span>{{ t('cronTip') }}</span>
|
||||||
|
</n-alert>
|
||||||
<n-form :model="cleanupModel">
|
<n-form :model="cleanupModel">
|
||||||
<n-form-item-row :label="t('mailBoxLabel')">
|
<n-form-item-row :label="t('mailBoxLabel')">
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
|
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
|
||||||
@@ -162,6 +167,10 @@ onMounted(async () => {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.n-alert {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
|
||||||
|
const {
|
||||||
|
localeCache, mailboxSplitSize, useIframeShowMail, preferShowTextMail,
|
||||||
|
globalTabplacement, useSideMargin
|
||||||
|
} = useGlobalState()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
mailboxSplitSize: 'Mailbox Split Size',
|
||||||
|
useIframeShowMail: 'Use iframe Show HTML Mail',
|
||||||
|
preferShowTextMail: 'Display text Mail by default',
|
||||||
|
useSideMargin: 'Turn on the side margins on the left and right sides of the page',
|
||||||
|
globalTabplacement: 'Global Tab Placement',
|
||||||
|
left: 'left',
|
||||||
|
top: 'top',
|
||||||
|
right: 'right',
|
||||||
|
bottom: 'bottom',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
mailboxSplitSize: '邮箱界面分栏大小',
|
||||||
|
preferShowTextMail: '默认以文本显示邮件',
|
||||||
|
useIframeShowMail: '使用iframe显示HTML邮件',
|
||||||
|
globalTabplacement: '全局选项卡位置',
|
||||||
|
useSideMargin: '开启页面左右两侧侧边距',
|
||||||
|
left: '左侧',
|
||||||
|
top: '顶部',
|
||||||
|
right: '右侧',
|
||||||
|
bottom: '底部',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="center">
|
||||||
|
<n-card>
|
||||||
|
<n-form-item-row :label="t('mailboxSplitSize')">
|
||||||
|
<n-slider v-model:value="mailboxSplitSize" :min="0.25" :max="0.75" :step="0.01" :marks="{
|
||||||
|
0.25: '0.25',
|
||||||
|
0.5: '0.5',
|
||||||
|
0.75: '0.75'
|
||||||
|
}" />
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('preferShowTextMail')">
|
||||||
|
<n-switch v-model:value="preferShowTextMail" :round="false" />
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('useIframeShowMail')">
|
||||||
|
<n-switch v-model:value="useIframeShowMail" :round="false" />
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('useSideMargin')">
|
||||||
|
<n-switch v-model:value="useSideMargin" :round="false" />
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('globalTabplacement')">
|
||||||
|
<n-radio-group v-model:value="globalTabplacement">
|
||||||
|
<n-radio-button value="top" :label="t('top')" />
|
||||||
|
<n-radio-button value="left" :label="t('left')" />
|
||||||
|
<n-radio-button value="right" :label="t('right')" />
|
||||||
|
<n-radio-button value="bottom" :label="t('bottom')" />
|
||||||
|
</n-radio-group>
|
||||||
|
</n-form-item-row>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.n-card {
|
||||||
|
max-width: 800px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,10 +5,10 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
|
import Appearance from '../common/Appearance.vue'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
jwt, localeCache, settings, showAddressCredential, loading,
|
jwt, localeCache, settings, showAddressCredential, loading
|
||||||
mailboxSplitSize, useIframeShowMail, preferShowTextMail
|
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@@ -19,9 +19,6 @@ const { t } = useI18n({
|
|||||||
locale: localeCache.value || 'zh',
|
locale: localeCache.value || 'zh',
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
mailboxSplitSize: 'Mailbox Split Size',
|
|
||||||
useIframeShowMail: 'Use iframe Show HTML Mail',
|
|
||||||
preferShowTextMail: 'Display text Mail by default',
|
|
||||||
logout: "Logout",
|
logout: "Logout",
|
||||||
delteAccount: "Delete Account",
|
delteAccount: "Delete Account",
|
||||||
showAddressCredential: 'Show Address Credential',
|
showAddressCredential: 'Show Address Credential',
|
||||||
@@ -30,9 +27,6 @@ const { t } = useI18n({
|
|||||||
delteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
delteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
mailboxSplitSize: '邮箱界面分栏大小',
|
|
||||||
preferShowTextMail: '默认以文本显示邮件',
|
|
||||||
useIframeShowMail: '使用iframe显示HTML邮件',
|
|
||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
delteAccount: "删除账户",
|
delteAccount: "删除账户",
|
||||||
showAddressCredential: '查看邮箱地址凭证',
|
showAddressCredential: '查看邮箱地址凭证',
|
||||||
@@ -66,21 +60,7 @@ const deleteAccount = async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center" v-if="settings.address">
|
<div class="center" v-if="settings.address">
|
||||||
<n-card>
|
<n-card>
|
||||||
<n-card>
|
<Appearance />
|
||||||
<n-form-item-row :label="t('mailboxSplitSize')">
|
|
||||||
<n-slider v-model:value="mailboxSplitSize" :min="0.25" :max="0.75" :step="0.01" :marks="{
|
|
||||||
0.25: '0.25',
|
|
||||||
0.5: '0.5',
|
|
||||||
0.75: '0.75'
|
|
||||||
}" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('preferShowTextMail')">
|
|
||||||
<n-switch v-model:value="preferShowTextMail" :round="false" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('useIframeShowMail')">
|
|
||||||
<n-switch v-model:value="useIframeShowMail" :round="false" />
|
|
||||||
</n-form-item-row>
|
|
||||||
</n-card>
|
|
||||||
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
||||||
{{ t('showAddressCredential') }}
|
{{ t('showAddressCredential') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
|||||||
@@ -83,9 +83,10 @@ const generateName = async () => {
|
|||||||
try {
|
try {
|
||||||
generateNameLoading.value = true;
|
generateNameLoading.value = true;
|
||||||
const { faker } = await import('https://esm.sh/@faker-js/faker');
|
const { faker } = await import('https://esm.sh/@faker-js/faker');
|
||||||
emailName.value = faker.person
|
emailName.value = faker.internet.email()
|
||||||
.fullName()
|
.split('@')[0]
|
||||||
.replace(/\s+/g, '.')
|
.replace(/\s+/g, '.')
|
||||||
|
.replace(/\.{2,}/g, '.')
|
||||||
.replace(/[^a-zA-Z0-9.]/g, '')
|
.replace(/[^a-zA-Z0-9.]/g, '')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
+44
-100
@@ -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 { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils'
|
import { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils'
|
||||||
import { newAddress } from '../common'
|
import { newAddress, handleListQuery } from '../common'
|
||||||
import { CONSTANTS } from '../constants'
|
import { CONSTANTS } from '../constants'
|
||||||
import cleanup_api from './cleanup_api'
|
import cleanup_api from './cleanup_api'
|
||||||
import admin_user_api from './admin_user_api'
|
import admin_user_api from './admin_user_api'
|
||||||
@@ -74,7 +74,7 @@ api.delete('/admin/delete_address/:id', async (c) => {
|
|||||||
return c.text("Failed to delete address", 500)
|
return c.text("Failed to delete address", 500)
|
||||||
}
|
}
|
||||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
const { success: mailSuccess } = await c.env.DB.prepare(
|
||||||
`DELETE FROM mails WHERE address IN`
|
`DELETE FROM raw_mails WHERE address IN`
|
||||||
+ ` (select name from address where id = ?) `
|
+ ` (select name from address where id = ?) `
|
||||||
).bind(id).run();
|
).bind(id).run();
|
||||||
if (!mailSuccess) {
|
if (!mailSuccess) {
|
||||||
@@ -107,111 +107,58 @@ api.get('/admin/show_password/:id', async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
api.get('/admin/mails', async (c) => {
|
api.get('/admin/mails', async (c) => {
|
||||||
const { address, limit, offset } = c.req.query();
|
const { address, limit, offset, keyword } = c.req.query();
|
||||||
if (!limit || limit < 0 || limit > 100) {
|
if (address && keyword) {
|
||||||
return c.text("Invalid limit", 400)
|
return await handleListQuery(c,
|
||||||
|
`SELECT * FROM raw_mails where address = ? and raw like ? `,
|
||||||
|
`SELECT count(*) as count FROM raw_mails where address = ? and raw like ? `,
|
||||||
|
[address, `%${keyword}%`], limit, offset
|
||||||
|
);
|
||||||
|
} else if (keyword) {
|
||||||
|
return await handleListQuery(c,
|
||||||
|
`SELECT * FROM raw_mails where raw like ? `,
|
||||||
|
`SELECT count(*) as count FROM raw_mails where raw like ? `,
|
||||||
|
[`%${keyword}%`], limit, offset
|
||||||
|
);
|
||||||
|
} else if (address) {
|
||||||
|
return await handleListQuery(c,
|
||||||
|
`SELECT * FROM raw_mails where address = ? `,
|
||||||
|
`SELECT count(*) as count FROM raw_mails where address = ? `,
|
||||||
|
[address], limit, offset
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return await handleListQuery(c,
|
||||||
|
`SELECT * FROM raw_mails `,
|
||||||
|
`SELECT count(*) as count FROM raw_mails `,
|
||||||
|
[], limit, offset
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!offset || offset < 0) {
|
|
||||||
return c.text("Invalid offset", 400)
|
|
||||||
}
|
|
||||||
if (!address) {
|
|
||||||
const { results } = await c.env.DB.prepare(
|
|
||||||
`SELECT * FROM raw_mails order by id desc limit ? offset ?`
|
|
||||||
).bind(limit, offset).all();
|
|
||||||
let count = 0;
|
|
||||||
if (offset == 0) {
|
|
||||||
const { count: mailCount } = await c.env.DB.prepare(
|
|
||||||
`SELECT count(*) as count FROM raw_mails`
|
|
||||||
).first();
|
|
||||||
count = mailCount;
|
|
||||||
}
|
|
||||||
return c.json({
|
|
||||||
results: results,
|
|
||||||
count: count
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const { results } = await c.env.DB.prepare(
|
|
||||||
`SELECT * FROM raw_mails where address = ? order by id desc limit ? offset ?`
|
|
||||||
).bind(address, limit, offset).all();
|
|
||||||
let count = 0;
|
|
||||||
if (offset == 0) {
|
|
||||||
const { count: mailCount } = await c.env.DB.prepare(
|
|
||||||
`SELECT count(*) as count FROM raw_mails where address = ? `
|
|
||||||
).bind(address).first();
|
|
||||||
count = mailCount;
|
|
||||||
}
|
|
||||||
return c.json({
|
|
||||||
results: results,
|
|
||||||
count: count
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
api.get('/admin/mails_unknow', async (c) => {
|
api.get('/admin/mails_unknow', async (c) => {
|
||||||
const { limit, offset } = c.req.query();
|
const { limit, offset } = c.req.query();
|
||||||
if (!limit || limit < 0 || limit > 100) {
|
return await handleListQuery(c,
|
||||||
return c.text("Invalid limit", 400)
|
`SELECT * FROM raw_mails where address NOT IN (select name from address) `,
|
||||||
}
|
`SELECT count(*) as count FROM raw_mails`
|
||||||
if (!offset || offset < 0) {
|
+ ` where address NOT IN (select name from address) `,
|
||||||
return c.text("Invalid offset", 400)
|
[], limit, offset
|
||||||
}
|
);
|
||||||
const { results } = await c.env.DB.prepare(`
|
|
||||||
SELECT * FROM raw_mails
|
|
||||||
where address NOT IN (select name from address)
|
|
||||||
order by id desc limit ? offset ? `
|
|
||||||
).bind(limit, offset).all();
|
|
||||||
let count = 0;
|
|
||||||
if (offset == 0) {
|
|
||||||
const { count: mailCount } = await c.env.DB.prepare(`
|
|
||||||
SELECT count(*) as count FROM raw_mails
|
|
||||||
where address NOT IN
|
|
||||||
(select name from address)`
|
|
||||||
).first();
|
|
||||||
count = mailCount;
|
|
||||||
}
|
|
||||||
return c.json({
|
|
||||||
results: results,
|
|
||||||
count: count
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 (!limit || limit < 0 || limit > 100) {
|
|
||||||
return c.text("Invalid limit", 400)
|
|
||||||
}
|
|
||||||
if (!offset || offset < 0) {
|
|
||||||
return c.text("Invalid offset", 400)
|
|
||||||
}
|
|
||||||
if (address) {
|
if (address) {
|
||||||
const { results } = await c.env.DB.prepare(
|
return await handleListQuery(c,
|
||||||
`SELECT * FROM address_sender where address = ? order by id desc limit ? offset ?`
|
`SELECT * FROM address_sender where address = ? `,
|
||||||
).bind(address, limit, offset).all();
|
`SELECT count(*) as count FROM address_sender where address = ? `,
|
||||||
let count = 0;
|
[address], limit, offset
|
||||||
if (offset == 0) {
|
);
|
||||||
const { count: addressCount } = await c.env.DB.prepare(
|
|
||||||
`SELECT count(*) as count FROM address_sender where address = ?`
|
|
||||||
).bind(address).first();
|
|
||||||
count = addressCount;
|
|
||||||
}
|
|
||||||
return c.json({
|
|
||||||
results: results,
|
|
||||||
count: count
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
const { results } = await c.env.DB.prepare(
|
return await handleListQuery(c,
|
||||||
`SELECT * FROM address_sender order by id desc limit ? offset ? `
|
`SELECT * FROM address_sender `,
|
||||||
).bind(limit, offset).all();
|
`SELECT count(*) as count FROM address_sender `,
|
||||||
let count = 0;
|
[], limit, offset
|
||||||
if (offset == 0) {
|
);
|
||||||
const { count: addressCount } = await c.env.DB.prepare(
|
|
||||||
`SELECT count(*) as count FROM address_sender`
|
|
||||||
).first();
|
|
||||||
count = addressCount;
|
|
||||||
}
|
|
||||||
return c.json({
|
|
||||||
results: results,
|
|
||||||
count: count
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
api.post('/admin/address_sender', async (c) => {
|
api.post('/admin/address_sender', async (c) => {
|
||||||
@@ -276,9 +223,6 @@ api.get('/admin/sendbox', async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
api.get('/admin/statistics', async (c) => {
|
api.get('/admin/statistics', async (c) => {
|
||||||
const { count: mailCountV1 } = await c.env.DB.prepare(`
|
|
||||||
SELECT count(*) as count FROM mails`
|
|
||||||
).first();
|
|
||||||
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`
|
||||||
).first();
|
).first();
|
||||||
@@ -292,7 +236,7 @@ api.get('/admin/statistics', async (c) => {
|
|||||||
SELECT count(*) as count FROM sendbox`
|
SELECT count(*) as count FROM sendbox`
|
||||||
).first();
|
).first();
|
||||||
return c.json({
|
return c.json({
|
||||||
mailCount: (mailCountV1 || 0) + (mailCount || 0),
|
mailCount: mailCount,
|
||||||
userCount: addressCount,
|
userCount: addressCount,
|
||||||
activeUserCount7days: activeUserCount7days,
|
activeUserCount7days: activeUserCount7days,
|
||||||
sendMailCount: sendMailCount
|
sendMailCount: sendMailCount
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export const cleanup = async (c, cleanType, cleanDays) => {
|
|||||||
case "address":
|
case "address":
|
||||||
await c.env.DB.prepare(`
|
await c.env.DB.prepare(`
|
||||||
DELETE FROM address WHERE updated_at < datetime('now', '-${cleanDays} day')`
|
DELETE FROM address WHERE updated_at < datetime('now', '-${cleanDays} day')`
|
||||||
|
+ ` AND id NOT IN (SELECT address_id FROM users_address)`
|
||||||
).run();
|
).run();
|
||||||
break;
|
break;
|
||||||
case "sendbox":
|
case "sendbox":
|
||||||
@@ -86,3 +87,31 @@ export const cleanup = async (c, cleanType, cleanDays) => {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} c context
|
||||||
|
* @param {*} query @type {string} query
|
||||||
|
* @param {*} countQuery @type {string} countQuery
|
||||||
|
* @param {*} limit @type {number} limit
|
||||||
|
* @param {*} offset @type {number} offset
|
||||||
|
* @returns {Promise} Promise
|
||||||
|
*/
|
||||||
|
export const handleListQuery = async (
|
||||||
|
c, query, countQuery, params, limit, offset
|
||||||
|
) => {
|
||||||
|
if (!limit || limit < 0 || limit > 100) {
|
||||||
|
return c.text("Invalid limit", 400)
|
||||||
|
}
|
||||||
|
if (!offset || offset < 0) {
|
||||||
|
return c.text("Invalid offset", 400)
|
||||||
|
}
|
||||||
|
const resultsQuery = `${query} order by id desc limit ? offset ?`;
|
||||||
|
const { results } = await c.env.DB.prepare(resultsQuery).bind(
|
||||||
|
...params, limit, offset
|
||||||
|
).all();
|
||||||
|
const count = offset == 0 ? await c.env.DB.prepare(
|
||||||
|
countQuery
|
||||||
|
).bind(...params).first("count") : 0;
|
||||||
|
return c.json({ results, count });
|
||||||
|
}
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ api.post('/api/send_mail', async (c) => {
|
|||||||
api.post('/external/api/send_mail', async (c) => {
|
api.post('/external/api/send_mail', async (c) => {
|
||||||
const { token } = await c.req.json();
|
const { token } = await c.req.json();
|
||||||
try {
|
try {
|
||||||
const { address } = await Jwt.verify(token, c.env.JWT_SECRET);
|
const { address } = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
|
||||||
if (!address) {
|
if (!address) {
|
||||||
return c.text("No address", 400)
|
return c.text("No address", 400)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ api.delete('/api/delete_address', async (c) => {
|
|||||||
return c.text("Failed to delete address", 500)
|
return c.text("Failed to delete address", 500)
|
||||||
}
|
}
|
||||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
const { success: mailSuccess } = await c.env.DB.prepare(
|
||||||
`DELETE FROM mails WHERE address = ? `
|
`DELETE FROM raw_mails WHERE address = ? `
|
||||||
).bind(address).run();
|
).bind(address).run();
|
||||||
if (!mailSuccess) {
|
if (!mailSuccess) {
|
||||||
return c.text("Failed to delete mails", 500)
|
return c.text("Failed to delete mails", 500)
|
||||||
|
|||||||
@@ -132,7 +132,10 @@ export default {
|
|||||||
// create jwt
|
// create jwt
|
||||||
const jwt = await Jwt.sign({
|
const jwt = await Jwt.sign({
|
||||||
user_email: email,
|
user_email: email,
|
||||||
user_id: user_id
|
user_id: user_id,
|
||||||
|
// 30 days expire in seconds
|
||||||
|
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
||||||
|
iat: Math.floor(Date.now() / 1000),
|
||||||
}, c.env.JWT_SECRET)
|
}, c.env.JWT_SECRET)
|
||||||
return c.json({
|
return c.json({
|
||||||
jwt: jwt
|
jwt: jwt
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ app.use('/api/*', async (c, next) => {
|
|||||||
await next();
|
await next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return jwt({ secret: c.env.JWT_SECRET })(c, next);
|
return jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
|
||||||
});
|
});
|
||||||
// user_api auth
|
// user_api auth
|
||||||
app.use('/user_api/*', async (c, next) => {
|
app.use('/user_api/*', async (c, next) => {
|
||||||
@@ -67,7 +67,13 @@ app.use('/user_api/*', async (c, next) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const token = c.req.raw.headers.get("x-user-token");
|
const token = c.req.raw.headers.get("x-user-token");
|
||||||
const payload = await Jwt.verify(token, c.env.JWT_SECRET);
|
const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
|
||||||
|
// check expired
|
||||||
|
if (!payload.exp) return c.text("Invalid Token", 401);
|
||||||
|
// exp is in seconds
|
||||||
|
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
||||||
|
return c.text("Token Expired", 401)
|
||||||
|
}
|
||||||
c.set("userPayload", payload);
|
c.set("userPayload", payload);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -76,7 +82,7 @@ app.use('/user_api/*', async (c, next) => {
|
|||||||
if (c.req.path.startsWith('/user_api/bind_address')
|
if (c.req.path.startsWith('/user_api/bind_address')
|
||||||
&& c.req.method === 'POST'
|
&& c.req.method === 'POST'
|
||||||
) {
|
) {
|
||||||
return jwt({ secret: c.env.JWT_SECRET })(c, next);
|
return jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
|
||||||
}
|
}
|
||||||
await next();
|
await next();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user