新增tg和第三方邮件转发

This commit is contained in:
eoao
2025-06-28 13:35:37 +08:00
parent c9271a17dc
commit 7820d7eef5
25 changed files with 948 additions and 457 deletions
-1
View File
@@ -22,7 +22,6 @@
"path": "^0.12.7",
"pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0",
"postal-mime": "^2.4.3",
"screenfull": "^6.0.2",
"vue": "^3.5.13",
"vue-cropper": "^1.1.4",
@@ -125,7 +125,7 @@
<span>
<Icon icon="mdi-light:email" width="20" height="20"/>
</span>
<span>{{ item.status === 7 ? formateReceive(item.recipient) : item.accountEmail }}</span>
<span>{{ item.toEmail }}</span>
</div>
<div class="del-status" v-if="item.isDel">
<el-tag type="info" size="small">已删除</el-tag>
@@ -39,6 +39,8 @@ function updateContent() {
const bodyStyleMatch = props.html.match(bodyStyleRegex);
const bodyStyle = bodyStyleMatch ? bodyStyleMatch[1] : '';
console.log(bodyStyle)
// 2. 移除 <body> 标签(保留内容)
const cleanedHtml = props.html.replace(/<\/?body[^>]*>/gi, '');
@@ -63,19 +65,11 @@ function updateContent() {
${bodyStyle ? bodyStyle : ''} /* 注入 body 的 style */
}
img {
img:not(table img) {
max-width: 100%;
height: auto !important;
}
*:not(p) {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: inherit;
-webkit-tap-highlight-color: transparent;
}
</style>
<div class="shadow-content">
${cleanedHtml}
@@ -4,7 +4,7 @@ import { useAccountStore } from "@/store/account.js";
import { loginUserInfo } from "@/request/my.js";
import { permsToRouter } from "@/utils/perm.js";
import router from "@/router";
import { settingQuery } from "@/request/setting.js";
import { websiteConfig } from "@/request/setting.js";
import {cvtR2Url} from "@/utils/convert.js";
export async function init() {
@@ -24,7 +24,7 @@ export async function init() {
return null;
});
const [s, user] = await Promise.all([settingQuery(), userPromise]);
const [s, user] = await Promise.all([websiteConfig(), userPromise]);
setting = s;
settingStore.settings = setting;
settingStore.domainList = setting.domainList;
@@ -41,7 +41,7 @@ export async function init() {
}
} else {
setting = await settingQuery();
setting = await websiteConfig();
settingStore.settings = setting;
settingStore.domainList = setting.domainList;
document.title = setting.title;
+1 -1
View File
@@ -7,7 +7,7 @@
<el-scrollbar class="scrollbar">
<div v-infinite-scroll="getAccountList" :infinite-scroll-distance="600" :infinite-scroll-immediate="false">
<el-card class="item" :class="itemBg(item.accountId)" v-for="item in accounts" :key="item.accountId" @click="changeAccount(item)">
<div class="account">
<div class="account" @click.stop>
{{ item.email }}
</div>
<div class="opt">
+2 -2
View File
@@ -1,6 +1,6 @@
<template>
<div class="send" v-show="show">
<div class="write-box">
<div class="send" v-show="show" @click="close">
<div class="write-box" @click.stop>
<div class="title">
<div class="title-left">
<span class="title-text">
+1 -1
View File
@@ -4,7 +4,7 @@ import router from './router';
import './style.css';
import VueCropper from 'vue-cropper';
import 'vue-cropper/dist/index.css'
import { init } from '@/utils/init.js';
import { init } from '@/init/init.js';
import { createPinia } from 'pinia';
import piniaPersistedState from 'pinia-plugin-persistedstate';
import perm from "@/directives/perm.js";
+4
View File
@@ -8,6 +8,10 @@ export function settingQuery() {
return http.get('/setting/query')
}
export function websiteConfig() {
return http.get('/setting/websiteConfig')
}
export function setBackground(background) {
return http.put('/setting/setBackground',{background})
}
+4 -2
View File
@@ -78,8 +78,10 @@
>注册
</el-button>
</div>
<div class="switch" @click="show = 'register'" v-if="show === 'login'">还有没有账号? <span>创建账号</span></div>
<div class="switch" @click="show = 'login'" v-else>有账号? <span>去登录</span></div>
<template v-if="settingStore.settings.register === 0">
<div class="switch" @click="show = 'register'" v-if="show === 'login'">还有没有账号? <span>创建账号</span></div>
<div class="switch" @click="show = 'login'" v-else>已有账号? <span>去登录</span></div>
</template>
</div>
</div>
</div>
+1 -1
View File
@@ -74,7 +74,7 @@ defineOptions({
const emailStore = useEmailStore();
const sysEmailScroll = ref({})
const searchType = ref('user')
const searchType = ref('name')
const searchValue = ref('')
const mySelect = ref()
+540 -212
View File
@@ -1,240 +1,277 @@
<template>
<div class="settings-container">
<el-scrollbar class="scroll">
<div class="card-grid">
<!-- Website Settings Card -->
<div class="settings-card">
<div class="card-title">网站设置</div>
<div class="card-content">
<div class="setting-item">
<div><span>用户注册</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.register"/>
<div v-if="firstLoading" class="loading">
<loading />
</div>
<el-scrollbar class="scroll" v-else >
<div class="scroll-body">
<div class="card-grid">
<!-- Website Settings Card -->
<div class="settings-card">
<div class="card-title">网站设置</div>
<div class="card-content">
<div class="setting-item">
<div><span>用户注册</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.register"/>
</div>
</div>
</div>
<div class="setting-item">
<div><span>添加邮箱</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.addEmail"/>
<div class="setting-item">
<div><span>添加邮箱</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.addEmail"/>
</div>
</div>
</div>
<div class="setting-item">
<div>
<span>多号模式</span>
<el-tooltip effect="dark" content="开启后账号栏出现一个用户可以添加多个邮箱">
<Icon class="warning" icon="fe:warning" width="20" height="20"/>
</el-tooltip>
<div class="setting-item">
<div>
<span>多号模式</span>
<el-tooltip effect="dark" content="开启后账号栏出现一个用户可以添加多个邮箱">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.manyEmail"/>
</div>
</div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.manyEmail"/>
<div class="setting-item">
<div>
<span>轮询刷新</span>
<el-tooltip effect="dark" content="轮询请求服务器获取最新邮件">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
<div>
<el-select
@change="change"
style="width: 80px;"
v-model="setting.autoRefreshTime"
placeholder="Select"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
</div>
<div class="setting-item">
<div>
<span>轮询刷新</span>
<el-tooltip effect="dark" content="轮询请求服务器获取最新邮件">
<Icon class="warning" icon="fe:warning" width="20" height="20"/>
</el-tooltip>
</div>
<div>
<el-select
@change="change"
style="width: 80px;"
v-model="setting.autoRefreshTime"
placeholder="Select"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
</div>
<div class="setting-item">
<div>
<span>物理清空数据</span>
<el-tooltip effect="dark" content="该操作会物理清空所有已被删除的数据">
<Icon class="warning" icon="fe:warning" width="20" height="20"/>
</el-tooltip>
</div>
<div>
<el-button class="opt-button" style="margin-top: 0" @click="physicsDeleteAllData" size="small"
type="primary">
<Icon icon="material-symbols:delete-outline-rounded" width="16" height="16"/>
</el-button>
</div>
</div>
</div>
</div>
<!-- Personalization Settings Card -->
<div class="settings-card">
<div class="card-title">个性化设置</div>
<div class="card-content">
<div class="setting-item">
<div class="title-item"><span>网站标题</span></div>
<div class="email-title">
<span>{{ setting.title }}</span>
<el-button class="opt-button" size="small" type="primary" @click="editTitleShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
</div>
</div>
<div class="setting-item">
<div class="title-item"><span>登录透明</span></div>
<div>
<el-input-number size="small" v-model="loginOpacity" @change="opacityChange" :precision="2" :step="0.01" :max="1" :min="0" />
</div>
</div>
<div class="setting-item personalized">
<div><span>登录背景</span></div>
<div>
<el-image
class="background"
:src="cvtR2Url(setting.background)"
:preview-src-list="[cvtR2Url(setting.background)]"
show-progress
fit="cover"
>
<template #error>
<div class="error-image" @click="openCut">
<Icon icon="ph:image" width="24" height="24"/>
</div>
</template>
</el-image>
<div class="background-btn">
<el-button class="opt-button" size="small" type="primary" @click="openCut">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
<el-button class="opt-button" size="small" type="primary" @click="delBackground">
<div class="setting-item">
<div>
<span>物理清空数据</span>
<el-tooltip effect="dark" content="该操作会物理清空所有已被删除的数据">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
<div>
<el-button class="opt-button" style="margin-top: 0" @click="physicsDeleteAllData" size="small"
type="primary">
<Icon icon="material-symbols:delete-outline-rounded" width="16" height="16"/>
</el-button>
</div>
</div>
</div>
</div>
</div>
<!-- Email Sending Settings Card -->
<div class="settings-card">
<div class="card-title">邮件设置</div>
<div class="card-content">
<div class="setting-item">
<div><span>邮件接收</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.receive"/>
<!-- Personalization Settings Card -->
<div class="settings-card">
<div class="card-title">个性化设置</div>
<div class="card-content">
<div class="setting-item">
<div class="title-item"><span>网站标题</span></div>
<div class="email-title">
<span>{{ setting.title }}</span>
<el-button class="opt-button" size="small" type="primary" @click="editTitleShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
</div>
</div>
</div>
<div class="setting-item">
<div><span>邮件发送</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.send"/>
<div class="setting-item">
<div class="title-item"><span>登录透明</span></div>
<div>
<el-input-number size="small" v-model="loginOpacity" @change="opacityChange" :precision="2" :step="0.01" :max="1" :min="0" />
</div>
</div>
</div>
<div class="setting-item">
<div><span>添加resend令牌</span></div>
<div>
<el-button class="opt-button" style="margin-top: 0" @click="openResendForm" size="small" type="primary">
<Icon icon="material-symbols:add-rounded" width="16" height="16"/>
</el-button>
</div>
</div>
<div class="setting-item token-item" v-for="(value, key, index) in setting.resendTokens" :key="index">
<div><span>{{ key }}</span></div>
<div><span>{{ value }}</span></div>
</div>
</div>
</div>
<!-- R2 Object Storage Card -->
<div class="settings-card">
<div class="card-title">R2对象存储</div>
<div class="card-content">
<div class="setting-item">
<div><span>访问域名</span></div>
<div class="r2domain">
<span>{{ setting.r2Domain || '空' }}</span>
<el-button class="opt-button" size="small" type="primary" @click="r2DomainShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
<div class="setting-item personalized">
<div><span>登录背景</span></div>
<div>
<el-image
class="background"
:src="cvtR2Url(setting.background)"
:preview-src-list="[cvtR2Url(setting.background)]"
show-progress
fit="cover"
>
<template #error>
<div class="error-image" @click="openCut">
<Icon icon="ph:image" width="24" height="24"/>
</div>
</template>
</el-image>
<div class="background-btn">
<el-button class="opt-button" size="small" type="primary" @click="openCut">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
<el-button class="opt-button" size="small" type="primary" @click="delBackground">
<Icon icon="material-symbols:delete-outline-rounded" width="16" height="16"/>
</el-button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Turnstile Verification Card -->
<div class="settings-card">
<div class="card-title">Turnstile 人机验证</div>
<div class="card-content">
<div class="setting-item">
<div><span>注册验证</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.registerVerify"/>
<!-- Email Sending Settings Card -->
<div class="settings-card">
<div class="card-title">邮件设置</div>
<div class="card-content">
<div class="setting-item">
<div><span>邮件接收</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.receive"/>
</div>
</div>
</div>
<div class="setting-item">
<div><span>添加验证</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.addEmailVerify"/>
<div class="setting-item">
<div><span>邮件发送</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.send"/>
</div>
</div>
</div>
<div class="setting-item">
<div><span>siteKey</span></div>
<div class="bot-verify">
<span>{{ setting.siteKey || '空' }}</span>
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
<div class="setting-item">
<div><span>添加 Resend Token</span></div>
<div>
<el-button class="opt-button" style="margin-top: 0" @click="openResendForm" size="small" type="primary">
<Icon icon="material-symbols:add-rounded" width="16" height="16"/>
</el-button>
</div>
</div>
</div>
<div class="setting-item">
<div><span>secretKey</span></div>
<div class="bot-verify">
<span> {{ setting.secretKey || '空' }} </span>
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
<div class="setting-item token-item" v-for="(value, key, index) in setting.resendTokens" :key="index">
<div><span>{{ key }}</span></div>
<div><span>{{ value }}</span></div>
</div>
</div>
</div>
</div>
<div class="settings-card">
<div class="card-title">关于</div>
<div class="card-content">
<div class="concerning-item">
<span>版本:</span>
<span>v1.2.1</span>
<!-- R2 Object Storage Card -->
<div class="settings-card">
<div class="card-title">R2对象存储</div>
<div class="card-content">
<div class="setting-item">
<div><span>访问域名</span></div>
<div class="r2domain">
<span>{{ setting.r2Domain || '空' }}</span>
<el-button class="opt-button" size="small" type="primary" @click="r2DomainShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
</div>
</div>
</div>
<div class="concerning-item">
<span>交流:</span>
<el-button @click="jump('https://t.me/cloud_mail_tg')">
telegram
<template #icon>
<Icon icon="logos:telegram" width="30" height="30"/>
</template>
</el-button>
<el-button @click="jump('https://github.com/LaziestRen/cloud-mail')">
github
<template #icon>
<Icon icon="codicon:github-inverted" width="22" height="22" />
</template>
</el-button>
</div>
<div class="settings-card">
<div class="card-title">邮件转发通知</div>
<div class="card-content">
<div class="setting-item">
<div><span>Telegram 机器人</span></div>
<div class="forward">
<span>{{ setting.tgBotStatus === 0 ? '已开启' : '已关闭' }}</span>
<el-button class="opt-button" size="small" type="primary" @click="openTgSetting">
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
</el-button>
</div>
</div>
<div class="setting-item">
<div><span>第三方邮箱</span></div>
<div class="forward">
<span>{{ setting.forwardStatus === 0 ? '已开启' : '已关闭' }}</span>
<el-button class="opt-button" size="small" type="primary" @click="openThirdEmailSetting">
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
</el-button>
</div>
</div>
<div class="setting-item">
<div><span>转发规则</span></div>
<div class="forward">
<span>{{ setting.ruleType === 0 ? '全部转发' : '规则转发' }}</span>
<el-button class="opt-button" size="small" type="primary" @click="openForwardRules">
<Icon icon="fluent:settings-48-regular" width="18" height="18"/>
</el-button>
</div>
</div>
</div>
</div>
<!-- Turnstile Verification Card -->
<div class="settings-card">
<div class="card-title">Turnstile 人机验证</div>
<div class="card-content">
<div class="setting-item">
<div><span>注册验证</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.registerVerify"/>
</div>
</div>
<div class="setting-item">
<div><span>添加验证</span></div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.addEmailVerify"/>
</div>
</div>
<div class="setting-item">
<div><span>Site Key</span></div>
<div class="bot-verify">
<span>{{ setting.siteKey || '空' }}</span>
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
</div>
</div>
<div class="setting-item">
<div><span>Secret Key</span></div>
<div class="bot-verify">
<span> {{ setting.secretKey || '空' }} </span>
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true">
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
</div>
</div>
</div>
</div>
<div class="settings-card about">
<div class="card-title">关于</div>
<div class="card-content">
<div class="concerning-item">
<span>版本:</span>
<span>v1.2.1</span>
</div>
<div class="concerning-item">
<span>交流:</span>
<el-button @click="jump('https://t.me/cloud_mail_tg')">
telegram
<template #icon>
<Icon icon="logos:telegram" width="30" height="30"/>
</template>
</el-button>
<el-button @click="jump('https://github.com/LaziestRen/cloud-mail')">
github
<template #icon>
<Icon icon="codicon:github-inverted" width="22" height="22" />
</template>
</el-button>
</div>
</div>
</div>
</div>
</div>
<!-- Dialogs remain the same -->
<el-dialog v-model="editTitleShow" title="修改标题" width="340" @closed="editTitle = ''">
<form>
@@ -242,7 +279,7 @@
<el-button type="primary" :loading="settingLoading" @click="saveTitle">保存</el-button>
</form>
</el-dialog>
<el-dialog v-model="resendTokenFormShow" title="添加resend令牌" width="340" @closed="cleanResendTokenForm">
<el-dialog v-model="resendTokenFormShow" title="添加resend token" width="340" @closed="cleanResendTokenForm">
<form>
<el-select style="margin-bottom: 15px" v-model="resendTokenForm.domain" placeholder="Select">
<el-option
@@ -292,6 +329,83 @@
<el-button type="primary" :loading="settingLoading" @click="saveBackground">保存</el-button>
</div>
</el-dialog>
<el-dialog
v-model="tgSettingShow"
title="Telegram 机器人"
class="forward-dialog"
>
<template #header>
<div class="forward-head">
<span class="forward-set-title">Telegram 机器人</span>
<el-tooltip effect="dark" content="可以将接收的邮件转发到Tg机器人">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
</template>
<div class="forward-set-body">
<el-input placeholder="机器人 token" v-model="tgBotToken"></el-input>
<el-input-tag tag-type="warning" placeholder="用户 chat_id 多个用,分开 12345,54321" v-model="tgChatId" @add-tag="addChatTag" ></el-input-tag>
</div>
<template #footer>
<div class="dialog-footer">
<el-switch v-model="tgBotStatus" :active-value="0" :inactive-value="1" active-text="开启" inactive-text="关闭" />
<el-button :loading="settingLoading" type="primary" @click="tgBotSave">
保存
</el-button>
</div>
</template>
</el-dialog>
<el-dialog
v-model="thirdEmailShow"
class="forward-dialog"
>
<template #header>
<div class="forward-head">
<span class="forward-set-title">第三方邮箱</span>
<el-tooltip effect="dark" trigger="click" content="可以将邮件转到其他服务商邮箱,需要在cloudflare验证邮箱">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
</template>
<div class="forward-set-body">
<el-input-tag tag-type="warning" placeholder="多邮个箱用, 分开 example1.com,example2.com" v-model="forwardEmail" @add-tag="emailAddTag"></el-input-tag>
</div>
<template #footer>
<div class="dialog-footer">
<el-switch v-model="forwardStatus" :active-value="0" :inactive-value="1" active-text="开启" inactive-text="关闭" />
<el-button :loading="settingLoading" type="primary" @click="forwardEmailSave">
保存
</el-button>
</div>
</template>
</el-dialog>
<el-dialog
v-model="forwardRulesShow"
class="forward-dialog"
>
<template #header>
<div class="forward-head">
<span class="forward-set-title">转发规则</span>
<el-tooltip effect="dark" content="规则转发只会转发设置邮箱所接收的邮件">
<Icon class="warning" icon="fe:warning" width="18" height="18"/>
</el-tooltip>
</div>
</template>
<div class="forward-set-body">
<el-input-tag placeholder="多邮个箱用, 分开 example1.com,example2.com" tag-type="success" v-model="ruleEmail" @add-tag="ruleEmailAddTag" />
</div>
<template #footer>
<div class="dialog-footer">
<el-radio-group v-model="ruleType">
<el-radio :value="0" >全部转发</el-radio>
<el-radio :value="1" >规则转发</el-radio>
</el-radio-group>
<el-button :loading="settingLoading" type="primary" @click="ruleEmailSave">
保存
</el-button>
</div>
</template>
</el-dialog>
</el-scrollbar>
</div>
</template>
@@ -306,11 +420,14 @@ import {Icon} from "@iconify/vue";
import {cvtR2Url} from "@/utils/convert.js";
import {storeToRefs} from "pinia";
import { debounce } from 'lodash-es'
import {isEmail} from "@/utils/verify-utils.js";
import loading from "@/components/loading/index.vue";
defineOptions({
name: 'sys-setting'
})
const firstLoading = ref(true)
const cropper = ref()
const cutImage = ref('')
const cutShow = ref(false)
@@ -320,6 +437,9 @@ const editTitleShow = ref(false)
const resendTokenFormShow = ref(false)
const r2DomainShow = ref(false)
const turnstileShow = ref(false)
const tgSettingShow = ref(false)
const thirdEmailShow = ref(false)
const forwardRulesShow = ref(false)
const settingStore = useSettingStore();
const {settings: setting} = storeToRefs(settingStore);
const editTitle = ref('')
@@ -345,11 +465,123 @@ const options = [
{label: '20s', value: 20}
]
onMounted(() => {
resendTokenForm.domain = settingStore.domainList[0];
loginOpacity.value = settingStore.settings.loginOpacity
const tgChatId = ref([])
const tgBotStatus = ref(0)
const tgBotToken = ref('')
const forwardEmail = ref([])
const forwardStatus = ref(0)
const ruleType = ref(0)
const ruleEmail = ref([])
settingQuery().then(settingData => {
setting.value = settingData
resendTokenForm.domain = setting.value.domainList[0]
loginOpacity.value = setting.value.loginOpacity
firstLoading.value = false
})
function openTgSetting() {
tgBotStatus.value = setting.value.tgBotStatus
tgBotToken.value = setting.value.tgBotToken
tgChatId.value = []
if (setting.value.tgChatId) {
const list = setting.value.tgChatId.split(',')
tgChatId.value.push(...list)
}
tgSettingShow.value = true
}
function openThirdEmailSetting() {
forwardEmail.value = []
forwardStatus.value = setting.value.forwardStatus
if (setting.value.forwardEmail) {
const list = setting.value.forwardEmail.split(',')
forwardEmail.value.push(...list)
}
thirdEmailShow.value = true
}
function openForwardRules() {
ruleType.value = setting.value.ruleType
ruleEmail.value = []
if (setting.value.ruleEmail) {
const list = setting.value.ruleEmail.split(',')
ruleEmail.value.push(...list)
}
forwardRulesShow.value = true
}
function emailAddTag(val) {
const emails = Array.from(new Set(
val.split(/[,]/).map(item => item.trim()).filter(item => item)
));
forwardEmail.value.splice(forwardEmail.value.length - 1, 1)
emails.forEach(email => {
if (isEmail(email) && !forwardEmail.value.includes(email)) {
forwardEmail.value.push(email)
}
})
}
function ruleEmailAddTag(val) {
const emails = Array.from(new Set(
val.split(/[,]/).map(item => item.trim()).filter(item => item)
));
ruleEmail.value.splice(ruleEmail.value.length - 1, 1)
emails.forEach(email => {
if (isEmail(email) && !ruleEmail.value.includes(email)) {
ruleEmail.value.push(email)
}
})
}
function addChatTag(val) {
const chatIds = Array.from(new Set(
val.split(/[,]/).map(item => item.trim()).filter(item => item)
));
tgChatId.value.splice(tgChatId.value.length - 1, 1)
chatIds.forEach(id => {
if (!isNaN(Number(id))) {
tgChatId.value.push(id)
}
})
}
function tgBotSave() {
const form = {
tgBotToken: tgBotToken.value,
tgBotStatus: tgBotStatus.value,
tgChatId: tgChatId.value + ''
}
editSetting(form)
}
function forwardEmailSave() {
const form = {
forwardStatus: forwardStatus.value,
forwardEmail: forwardEmail.value + ''
}
editSetting(form)
}
function ruleEmailSave() {
const form = {
ruleEmail: ruleEmail.value + '',
ruleType: ruleType.value
}
editSetting(form)
}
function doOpacityChange() {
const form = {}
form.loginOpacity = loginOpacity.value
@@ -500,6 +732,7 @@ function jump(href) {
function editSetting(settingForm, refreshStatus = true) {
if (settingLoading.value) return
settingLoading.value = true
settingSet(settingForm).then(() => {
settingLoading.value = false
ElMessage({
@@ -517,7 +750,11 @@ function editSetting(settingForm, refreshStatus = true) {
r2DomainShow.value = false
resendTokenFormShow.value = false
turnstileShow.value = false
}).catch(() => {
tgSettingShow.value = false
thirdEmailShow.value = false
forwardRulesShow.value = false
}).catch((e) => {
console.log(e)
loginOpacity.value = setting.value.loginOpacity
setting.value = {...setting.value, ...JSON.parse(backup)}
}).finally(() => {
@@ -530,11 +767,27 @@ function editSetting(settingForm, refreshStatus = true) {
.settings-container {
height: 100%;
overflow: hidden;
background: #FAFCFF;
background: #FAFCFF !important;
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
}
.scroll {
width: 100%;
min-height: 100%;
:deep(.el-scrollbar__view) {
height: 100%;
}
.scroll-body {
min-height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
}
.card-grid {
@@ -577,6 +830,12 @@ function editSetting(settingForm, refreshStatus = true) {
overflow: hidden;
}
@media (min-width: 885px) {
.about {
height: 210px;
}
}
.card-title {
font-size: 15px;
font-weight: bold;
@@ -596,7 +855,6 @@ function editSetting(settingForm, refreshStatus = true) {
grid-template-columns: auto 1fr;
gap: 10px;
font-weight: bold;
> div:first-child {
display: flex;
align-items: center;
@@ -626,11 +884,48 @@ function editSetting(settingForm, refreshStatus = true) {
}
}
.dialog-footer {
display: flex;
justify-content: space-between;
}
:deep(.el-dialog) {
width: 400px !important;
@media (max-width: 440px) {
width: calc(100% - 40px) !important;
margin-right: 20px !important;
margin-left: 20px !important;
}
}
:deep(.cut-dialog.el-dialog) {
width: fit-content !important;
height: fit-content !important;
}
:deep(.forward-dialog.el-dialog) {
width: 500px !important;
@media (max-width: 540px) {
width: calc(100% - 40px) !important;
margin-right: 20px !important;
margin-left: 20px !important;
}
}
.forward-dialog {
.forward-head {
display: flex;
align-items: center;
.forward-set-title {
top: 1px;
position: relative;
font-size: 16px;
font-weight: bold;
}
}
}
.error-image {
background: #f5f7fa;
height: 100%;
@@ -654,7 +949,34 @@ function editSetting(settingForm, refreshStatus = true) {
.bot-verify {
display: grid;
grid-template-columns: 1fr auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
span {
display: flex;
align-items: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
min-width: 0;
}
.el-button {
width: 48px;
margin: 0 0 0 10px;
}
}
.forward-set-body {
display: flex;
flex-direction: column;
gap: 15px;
.el-switch {
align-self: end;
}
}
.forward {
span {
display: flex;
align-items: center;
@@ -763,4 +1085,10 @@ form .el-button {
:deep(.el-select__wrapper) {
min-height: 28px;
}
</style>
<style>
.el-popper.is-dark {
}
</style>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -6,8 +6,8 @@
<title></title>
<link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module" crossorigin src="/assets/index-BVIJB-AL.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DKdj6vty.css">
<script type="module" crossorigin src="/assets/index-Cgh0xJS2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cobuvpco.css">
</head>
<body>
<div id="loading-first">
+5
View File
@@ -12,6 +12,11 @@ app.get('/setting/query', async (c) => {
return c.json(result.ok(setting));
});
app.get('/setting/websiteConfig', async (c) => {
const setting = await settingService.websiteConfig(c);
return c.json(result.ok(setting));
})
app.put('/setting/setBackground', async (c) => {
const key = await settingService.setBackground(c, await c.req.json());
return c.json(result.ok(key));
+12
View File
@@ -78,6 +78,18 @@ export const settingConst = {
addEmailVerify: {
OPEN: 0,
CLOSE: 1,
},
forwardStatus: {
OPEN: 0,
CLOSE: 1,
},
tgBotStatus: {
OPEN: 0,
CLOSE: 1,
},
ruleType: {
ALL: 0,
RULE: 1
}
}
+94 -9
View File
@@ -5,13 +5,30 @@ import settingService from '../service/setting-service';
import attService from '../service/att-service';
import constant from '../const/constant';
import fileUtils from '../utils/file-utils';
import {attConst, emailConst, isDel} from '../const/entity-const';
import { attConst, emailConst, isDel, settingConst } from '../const/entity-const';
import emailUtils from '../utils/email-utils';
import dayjs from 'dayjs';
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
dayjs.extend(utc)
dayjs.extend(timezone)
export async function email(message, env, ctx) {
try {
if (!await settingService.isReceive({ env })) {
const {
receive,
tgBotToken,
tgChatId,
tgBotStatus,
forwardStatus,
forwardEmail,
ruleEmail,
ruleType
} = await settingService.query({ env });
if (receive === settingConst.receive.CLOSE) {
return;
}
@@ -28,14 +45,18 @@ export async function email(message, env, ctx) {
const email = await PostalMime.parse(content);
const toName = email.to.find(item => item.address === message.to)?.name || '';
const params = {
toEmail: message.to,
toName: toName,
sendEmail: email.from.address,
name: email.from.name,
subject: email.subject,
content: email.html,
text: email.text,
cc: email.cc ? JSON.stringify(email.cc) : '[]',
bcc:email.bcc ? JSON.stringify(email.bcc) : '[]',
bcc: email.bcc ? JSON.stringify(email.bcc) : '[]',
recipient: JSON.stringify(email.to),
inReplyTo: email.inReplyTo,
relation: email.references,
@@ -49,32 +70,96 @@ export async function email(message, env, ctx) {
const attachments = [];
const cidAttachments = [];
for(let item of email.attachments) {
for (let item of email.attachments) {
let attachment = { ...item };
attachment.key = constant.ATTACHMENT_PREFIX + await fileUtils.getBuffHash(attachment.content) + fileUtils.getExtFileName(item.filename);
attachment.size = item.content.length ?? item.content.byteLength;
attachments.push(attachment);
if (attachment.contentId) {
cidAttachments.push(attachment)
cidAttachments.push(attachment);
}
}
const emailRow = await emailService.receive({ env }, params, cidAttachments);
let emailRow = await emailService.receive({ env }, params, cidAttachments);
attachments.forEach(attachment => {
attachment.emailId = emailRow.emailId;
attachment.userId = emailRow.userId;
attachment.accountId = emailRow.accountId;
attachment.type = attachment.contentId ? attConst.type.EMBED : attConst.type.ATT
})
attachment.type = attachment.contentId ? attConst.type.EMBED : attConst.type.ATT;
});
if (attachments.length > 0) {
await attService.addAtt({ env }, attachments);
}
await emailService.completeReceive({ env },account ? emailConst.status.RECEIVE : emailConst.status.NOONE, emailRow.emailId);
emailRow = await emailService.completeReceive({ env }, account ? emailConst.status.RECEIVE : emailConst.status.NOONE, emailRow.emailId);
if (ruleType === settingConst.ruleType.RULE) {
const emails = ruleEmail.split(',');
if (!emails.includes(message.to)) {
return;
}
}
if (tgBotStatus === settingConst.tgBotStatus.OPEN && tgChatId) {
const tgMessage = `<b>${params.subject}</b>
<b>发件人</b>${params.name} &lt;${params.sendEmail}&gt;
<b>收件人\u200B</b>${message.to}
<b>时间</b>${dayjs.utc(emailRow.createTime).tz('Asia/Shanghai').format('YYYY-MM-DD HH:mm')}
${emailUtils.htmlToText(params.content) || ''}
`;
const tgChatIds = tgChatId.split(',');
await Promise.all(tgChatIds.map(async chatId => {
try {
const res = await fetch(`https://api.telegram.org/bot${tgBotToken}/sendMessage`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
chat_id: chatId,
parse_mode: 'HTML',
text: tgMessage
})
});
if (!res.ok) {
console.error(`转发 Telegram 失败: chatId=${chatId}, 状态码=${res.status}`);
}
} catch (e) {
console.error(`转发 Telegram 失败: chatId=${chatId}`, e);
}
}));
}
if (forwardStatus === settingConst.forwardStatus.OPEN && forwardEmail) {
const emails = forwardEmail.split(',');
await Promise.all(emails.map(async email => {
try {
await message.forward(email);
} catch (e) {
console.error(`转发邮箱 ${email} 失败:`, e);
}
}));
}
} catch (e) {
console.error('邮件接收异常: ', e);
}
}
+2
View File
@@ -12,6 +12,8 @@ export const email = sqliteTable('email', {
cc: text('cc').default('[]'),
bcc: text('bcc').default('[]'),
recipient: text('recipient'),
toEmail: text('to_email').default('').notNull(),
toName: text('to_name').default('').notNull(),
inReplyTo: text('in_reply_to').default(''),
relation: text('relation').default(''),
messageId: text('message_id').default(''),
+8 -1
View File
@@ -13,7 +13,14 @@ export const setting = sqliteTable('setting', {
secretKey: text('secret_key'),
siteKey: text('site_key'),
background: text('background'),
loginOpacity: integer('login_opacity').default(0.9),
tgBotToken: text('tg_bot_token').default('').notNull(),
tgChatId: text('tg_chat_id').default('').notNull(),
tgBotStatus: integer('tg_bot_status').default(1).notNull(),
forwardEmail: text('forward_email').default('').notNull(),
forwardStatus: integer('forward_status').default(1).notNull(),
ruleEmail: text('rule_email').default('').notNull(),
ruleType: integer('rule_type').default(0).notNull(),
loginOpacity: integer('login_opacity').default(0.88),
resendTokens: text('resend_tokens').default("{}").notNull(),
});
export default setting
+37
View File
@@ -12,10 +12,47 @@ const init = {
await this.intDB(c);
await this.v1_1DB(c);
await this.v1_2DB(c);
await this.v1_3DB(c);
await settingService.refresh(c);
return c.text('初始化成功');
},
async v1_3DB(c) {
const ADD_COLUMN_SQL_LIST = [
`ALTER TABLE setting ADD COLUMN tg_bot_token TEXT NOT NULL DEFAULT '';`,
`ALTER TABLE setting ADD COLUMN tg_chat_id TEXT NOT NULL DEFAULT '';`,
`ALTER TABLE setting ADD COLUMN tg_bot_status INTEGER NOT NULL DEFAULT 1;`,
`ALTER TABLE setting ADD COLUMN forward_email TEXT NOT NULL DEFAULT '';`,
`ALTER TABLE setting ADD COLUMN forward_status INTEGER TIME NOT NULL DEFAULT 1;`,
`ALTER TABLE setting ADD COLUMN rule_email TEXT NOT NULL DEFAULT '';`,
`ALTER TABLE setting ADD COLUMN rule_type INTEGER NOT NULL DEFAULT 0;`
];
for (let sql of ADD_COLUMN_SQL_LIST) {
try {
await c.env.db.prepare(sql).run();
} catch (e) {
console.warn(`跳过字段添加,原因:${e.message}`);
}
}
const nameColumn = await c.env.db.prepare(`SELECT * FROM pragma_table_info('email') WHERE name = 'to_email' limit 1`).first();
if (nameColumn) {
return
}
const queryList = []
queryList.push(c.env.db.prepare(`ALTER TABLE email ADD COLUMN to_email TEXT NOT NULL DEFAULT ''`));
queryList.push(c.env.db.prepare(`ALTER TABLE email ADD COLUMN to_name TEXT NOT NULL DEFAULT ''`));
queryList.push(c.env.db.prepare(`UPDATE email SET to_email = json_extract(recipient, '$[0].address'), to_name = json_extract(recipient, '$[0].name')`));
await c.env.db.batch(queryList);
},
async v1_2DB(c){
const ADD_COLUMN_SQL_LIST = [
+1 -1
View File
@@ -10,8 +10,8 @@ import app from '../hono/hono';
const exclude = [
'/login',
'/register',
'/setting/query',
'/file',
'/setting/websiteConfig',
'/webhooks',
'/init'
];
+4 -7
View File
@@ -17,7 +17,6 @@ import account from '../entity/account';
import starService from './star-service';
import dayjs from 'dayjs';
import kvConst from '../const/kv-const';
import constant from '../const/constant';
const emailService = {
@@ -514,7 +513,7 @@ const emailService = {
}
if (accountEmail) {
conditions.push(like(account.email, `${accountEmail}%`));
conditions.push(like(email.toEmail, `${accountEmail}%`));
}
if (name) {
@@ -535,16 +534,14 @@ const emailService = {
conditions.push(lt(email.emailId, emailId));
}
const query = orm(c).select({ ...email, userEmail: user.email, accountEmail: account.email })
const query = orm(c).select({ ...email, userEmail: user.email })
.from(email)
.leftJoin(user, eq(email.userId, user.userId))
.leftJoin(account, eq(email.accountId, account.accountId))
.where(and(...conditions));
const queryCount = orm(c).select({ total: count() })
.from(email)
.leftJoin(user, eq(email.userId, user.userId))
.leftJoin(account, eq(email.accountId, account.accountId))
.where(and(...countConditions));
if (timeSort) {
@@ -574,10 +571,10 @@ const emailService = {
},
async completeReceive(c, status, emailId) {
await orm(c).update(email).set({
return await orm(c).update(email).set({
isDel: isDel.NORMAL,
status: status
}).where(eq(email.emailId, emailId)).run();
}).where(eq(email.emailId, emailId)).returning().get();
}
};
+26 -8
View File
@@ -7,7 +7,6 @@ import r2Service from './r2-service';
import emailService from './email-service';
import accountService from './account-service';
import userService from './user-service';
import starService from './star-service';
import constant from '../const/constant';
import BizError from '../error/biz-error';
@@ -32,20 +31,20 @@ const settingService = {
async get(c) {
const settingRow = await this.query(c);
settingRow.secretKey = settingRow.secretKey ? `${settingRow.secretKey.slice(0, 12)}******`: null ;
settingRow.secretKey = settingRow.secretKey ? `${settingRow.secretKey.slice(0, 12)}******` : null;
Object.keys(settingRow.resendTokens).forEach(key => {
settingRow.resendTokens[key] = `${settingRow.resendTokens[key].slice(0, 12)}******`;
});
return settingRow
return settingRow;
},
async set(c, params) {
const settingData = await this.query(c)
let resendTokens = {...settingData.resendTokens,...params.resendTokens}
const settingData = await this.query(c);
let resendTokens = { ...settingData.resendTokens, ...params.resendTokens };
Object.keys(resendTokens).forEach(domain => {
if(!resendTokens[domain]) delete resendTokens[domain]
})
params.resendTokens = JSON.stringify(resendTokens)
if (!resendTokens[domain]) delete resendTokens[domain];
});
params.resendTokens = JSON.stringify(resendTokens);
await orm(c).update(setting).set({ ...params }).returning().get();
await this.refresh(c);
},
@@ -109,6 +108,25 @@ const settingService = {
await emailService.physicsDeleteAll(c);
await accountService.physicsDeleteAll(c);
await userService.physicsDeleteAll(c);
},
async websiteConfig(c) {
const settingRow = await this.get(c);
return {
register: settingRow.register,
title: settingRow.title,
manyEmail: settingRow.manyEmail,
addEmail: settingRow.addEmail,
autoRefreshTime: settingRow.autoRefreshTime,
addEmailVerify: settingRow.addEmailVerify,
registerVerify: settingRow.registerVerify,
send: settingRow.send,
r2Domain: settingRow.r2Domain,
siteKey: settingRow.siteKey,
background: settingRow.background,
loginOpacity: settingRow.loginOpacity,
domainList:settingRow.domainList
};
}
};
+18 -9
View File
@@ -1,15 +1,24 @@
import { parseHTML } from 'linkedom';
const emailUtils = {
getDomain(email) {
if (typeof email !== 'string') return ''
const parts = email.split('@')
return parts.length === 2 ? parts[1] : ''
if (typeof email !== 'string') return '';
const parts = email.split('@');
return parts.length === 2 ? parts[1] : '';
},
getName(email) {
if (typeof email !== 'string') return ''
const parts = email.trim().split('@')
return parts.length === 2 ? parts[0] : ''
}
}
if (typeof email !== 'string') return '';
const parts = email.trim().split('@');
return parts.length === 2 ? parts[0] : '';
},
export default emailUtils
htmlToText(content) {
const { document } = parseHTML(content);
document.querySelectorAll('style, script, title').forEach(el => el.remove());
return document.documentElement.innerText;
}
};
export default emailUtils;