mirror of
https://github.com/schroinerxy/cloud-mail.git
synced 2026-06-21 19:35:50 +08:00
新增tg和第三方邮件转发
This commit is contained in:
@@ -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;
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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>
|
||||
-186
File diff suppressed because one or more lines are too long
+178
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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">
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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} <${params.sendEmail}>
|
||||
<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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(''),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -10,8 +10,8 @@ import app from '../hono/hono';
|
||||
const exclude = [
|
||||
'/login',
|
||||
'/register',
|
||||
'/setting/query',
|
||||
'/file',
|
||||
'/setting/websiteConfig',
|
||||
'/webhooks',
|
||||
'/init'
|
||||
];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user