新增tg和第三方邮件转发

This commit is contained in:
eoao
2025-06-28 13:35:37 +08:00
parent 010e0451ab
commit 136c0496b8
25 changed files with 948 additions and 457 deletions

View File

@@ -22,7 +22,6 @@
"path": "^0.12.7", "path": "^0.12.7",
"pinia": "^3.0.2", "pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0", "pinia-plugin-persistedstate": "^4.2.0",
"postal-mime": "^2.4.3",
"screenfull": "^6.0.2", "screenfull": "^6.0.2",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-cropper": "^1.1.4", "vue-cropper": "^1.1.4",

View File

@@ -125,7 +125,7 @@
<span> <span>
<Icon icon="mdi-light:email" width="20" height="20"/> <Icon icon="mdi-light:email" width="20" height="20"/>
</span> </span>
<span>{{ item.status === 7 ? formateReceive(item.recipient) : item.accountEmail }}</span> <span>{{ item.toEmail }}</span>
</div> </div>
<div class="del-status" v-if="item.isDel"> <div class="del-status" v-if="item.isDel">
<el-tag type="info" size="small">已删除</el-tag> <el-tag type="info" size="small">已删除</el-tag>

View File

@@ -39,6 +39,8 @@ function updateContent() {
const bodyStyleMatch = props.html.match(bodyStyleRegex); const bodyStyleMatch = props.html.match(bodyStyleRegex);
const bodyStyle = bodyStyleMatch ? bodyStyleMatch[1] : ''; const bodyStyle = bodyStyleMatch ? bodyStyleMatch[1] : '';
console.log(bodyStyle)
// 2. 移除 <body> 标签(保留内容) // 2. 移除 <body> 标签(保留内容)
const cleanedHtml = props.html.replace(/<\/?body[^>]*>/gi, ''); const cleanedHtml = props.html.replace(/<\/?body[^>]*>/gi, '');
@@ -63,19 +65,11 @@ function updateContent() {
${bodyStyle ? bodyStyle : ''} /* 注入 body 的 style */ ${bodyStyle ? bodyStyle : ''} /* 注入 body 的 style */
} }
img { img:not(table img) {
max-width: 100%; max-width: 100%;
height: auto !important; height: auto !important;
} }
*:not(p) {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: inherit;
-webkit-tap-highlight-color: transparent;
}
</style> </style>
<div class="shadow-content"> <div class="shadow-content">
${cleanedHtml} ${cleanedHtml}

View File

@@ -4,7 +4,7 @@ import { useAccountStore } from "@/store/account.js";
import { loginUserInfo } from "@/request/my.js"; import { loginUserInfo } from "@/request/my.js";
import { permsToRouter } from "@/utils/perm.js"; import { permsToRouter } from "@/utils/perm.js";
import router from "@/router"; import router from "@/router";
import { settingQuery } from "@/request/setting.js"; import { websiteConfig } from "@/request/setting.js";
import {cvtR2Url} from "@/utils/convert.js"; import {cvtR2Url} from "@/utils/convert.js";
export async function init() { export async function init() {
@@ -24,7 +24,7 @@ export async function init() {
return null; return null;
}); });
const [s, user] = await Promise.all([settingQuery(), userPromise]); const [s, user] = await Promise.all([websiteConfig(), userPromise]);
setting = s; setting = s;
settingStore.settings = setting; settingStore.settings = setting;
settingStore.domainList = setting.domainList; settingStore.domainList = setting.domainList;
@@ -41,7 +41,7 @@ export async function init() {
} }
} else { } else {
setting = await settingQuery(); setting = await websiteConfig();
settingStore.settings = setting; settingStore.settings = setting;
settingStore.domainList = setting.domainList; settingStore.domainList = setting.domainList;
document.title = setting.title; document.title = setting.title;

View File

@@ -7,7 +7,7 @@
<el-scrollbar class="scrollbar"> <el-scrollbar class="scrollbar">
<div v-infinite-scroll="getAccountList" :infinite-scroll-distance="600" :infinite-scroll-immediate="false"> <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)"> <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 }} {{ item.email }}
</div> </div>
<div class="opt"> <div class="opt">

View File

@@ -1,6 +1,6 @@
<template> <template>
<div class="send" v-show="show"> <div class="send" v-show="show" @click="close">
<div class="write-box"> <div class="write-box" @click.stop>
<div class="title"> <div class="title">
<div class="title-left"> <div class="title-left">
<span class="title-text"> <span class="title-text">

View File

@@ -4,7 +4,7 @@ import router from './router';
import './style.css'; import './style.css';
import VueCropper from 'vue-cropper'; import VueCropper from 'vue-cropper';
import 'vue-cropper/dist/index.css' import 'vue-cropper/dist/index.css'
import { init } from '@/utils/init.js'; import { init } from '@/init/init.js';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import piniaPersistedState from 'pinia-plugin-persistedstate'; import piniaPersistedState from 'pinia-plugin-persistedstate';
import perm from "@/directives/perm.js"; import perm from "@/directives/perm.js";

View File

@@ -8,6 +8,10 @@ export function settingQuery() {
return http.get('/setting/query') return http.get('/setting/query')
} }
export function websiteConfig() {
return http.get('/setting/websiteConfig')
}
export function setBackground(background) { export function setBackground(background) {
return http.put('/setting/setBackground',{background}) return http.put('/setting/setBackground',{background})
} }

View File

@@ -78,8 +78,10 @@
>注册 >注册
</el-button> </el-button>
</div> </div>
<div class="switch" @click="show = 'register'" v-if="show === 'login'">还有没有账号? <span>创建账号</span></div> <template v-if="settingStore.settings.register === 0">
<div class="switch" @click="show = 'login'" v-else>有账号? <span>去登录</span></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>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -74,7 +74,7 @@ defineOptions({
const emailStore = useEmailStore(); const emailStore = useEmailStore();
const sysEmailScroll = ref({}) const sysEmailScroll = ref({})
const searchType = ref('user') const searchType = ref('name')
const searchValue = ref('') const searchValue = ref('')
const mySelect = ref() const mySelect = ref()

View File

@@ -1,240 +1,277 @@
<template> <template>
<div class="settings-container"> <div class="settings-container">
<el-scrollbar class="scroll"> <div v-if="firstLoading" class="loading">
<div class="card-grid"> <loading />
<!-- Website Settings Card --> </div>
<div class="settings-card"> <el-scrollbar class="scroll" v-else >
<div class="card-title">网站设置</div> <div class="scroll-body">
<div class="card-content"> <div class="card-grid">
<div class="setting-item"> <!-- Website Settings Card -->
<div><span>用户注册</span></div> <div class="settings-card">
<div> <div class="card-title">网站设置</div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1" <div class="card-content">
v-model="setting.register"/> <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> <div class="setting-item">
<div class="setting-item"> <div><span>添加邮箱</span></div>
<div><span>添加邮箱</span></div> <div>
<div> <el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1" v-model="setting.addEmail"/>
v-model="setting.addEmail"/> </div>
</div> </div>
</div> <div class="setting-item">
<div class="setting-item"> <div>
<div> <span>多号模式</span>
<span>多号模式</span> <el-tooltip effect="dark" content="开启后账号栏出现一个用户可以添加多个邮箱">
<el-tooltip effect="dark" content="开启后账号栏出现一个用户可以添加多个邮箱"> <Icon class="warning" icon="fe:warning" width="18" height="18"/>
<Icon class="warning" icon="fe:warning" width="20" height="20"/> </el-tooltip>
</el-tooltip> </div>
<div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.manyEmail"/>
</div>
</div> </div>
<div> <div class="setting-item">
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1" <div>
v-model="setting.manyEmail"/> <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> <div class="setting-item">
<div class="setting-item"> <div>
<div> <span>物理清空数据</span>
<span>轮询刷新</span> <el-tooltip effect="dark" content="该操作会物理清空所有已被删除的数据">
<el-tooltip effect="dark" content="轮询请求服务器获取最新邮件"> <Icon class="warning" icon="fe:warning" width="18" height="18"/>
<Icon class="warning" icon="fe:warning" width="20" height="20"/> </el-tooltip>
</el-tooltip> </div>
</div> <div>
<div> <el-button class="opt-button" style="margin-top: 0" @click="physicsDeleteAllData" size="small"
<el-select type="primary">
@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">
<Icon icon="material-symbols:delete-outline-rounded" width="16" height="16"/> <Icon icon="material-symbols:delete-outline-rounded" width="16" height="16"/>
</el-button> </el-button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Email Sending Settings Card --> <!-- Personalization Settings Card -->
<div class="settings-card"> <div class="settings-card">
<div class="card-title">邮件设置</div> <div class="card-title">个性化设置</div>
<div class="card-content"> <div class="card-content">
<div class="setting-item"> <div class="setting-item">
<div><span>邮件接收</span></div> <div class="title-item"><span>网站标题</span></div>
<div> <div class="email-title">
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1" <span>{{ setting.title }}</span>
v-model="setting.receive"/> <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> <div class="setting-item">
<div class="setting-item"> <div class="title-item"><span>登录透明</span></div>
<div><span>邮件发送</span></div> <div>
<div> <el-input-number size="small" v-model="loginOpacity" @change="opacityChange" :precision="2" :step="0.01" :max="1" :min="0" />
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1" </div>
v-model="setting.send"/>
</div> </div>
</div> <div class="setting-item personalized">
<div class="setting-item"> <div><span>登录背景</span></div>
<div><span>添加resend令牌</span></div> <div>
<div> <el-image
<el-button class="opt-button" style="margin-top: 0" @click="openResendForm" size="small" type="primary"> class="background"
<Icon icon="material-symbols:add-rounded" width="16" height="16"/> :src="cvtR2Url(setting.background)"
</el-button> :preview-src-list="[cvtR2Url(setting.background)]"
</div> show-progress
</div> fit="cover"
<div class="setting-item token-item" v-for="(value, key, index) in setting.resendTokens" :key="index"> >
<div><span>{{ key }}</span></div> <template #error>
<div><span>{{ value }}</span></div> <div class="error-image" @click="openCut">
</div> <Icon icon="ph:image" width="24" height="24"/>
</div> </div>
</div> </template>
</el-image>
<!-- R2 Object Storage Card --> <div class="background-btn">
<div class="settings-card"> <el-button class="opt-button" size="small" type="primary" @click="openCut">
<div class="card-title">R2对象存储</div> <Icon icon="lsicon:edit-outline" width="16" height="16"/>
<div class="card-content"> </el-button>
<div class="setting-item"> <el-button class="opt-button" size="small" type="primary" @click="delBackground">
<div><span>访问域名</span></div> <Icon icon="material-symbols:delete-outline-rounded" width="16" height="16"/>
<div class="r2domain"> </el-button>
<span>{{ setting.r2Domain || '空' }}</span> </div>
<el-button class="opt-button" size="small" type="primary" @click="r2DomainShow = true"> </div>
<Icon icon="lsicon:edit-outline" width="16" height="16"/>
</el-button>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Turnstile Verification Card --> <!-- Email Sending Settings Card -->
<div class="settings-card"> <div class="settings-card">
<div class="card-title">Turnstile 人机验证</div> <div class="card-title">邮件设置</div>
<div class="card-content"> <div class="card-content">
<div class="setting-item"> <div class="setting-item">
<div><span>注册验证</span></div> <div><span>邮件接收</span></div>
<div> <div>
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1" <el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
v-model="setting.registerVerify"/> v-model="setting.receive"/>
</div>
</div> </div>
</div> <div class="setting-item">
<div class="setting-item"> <div><span>邮件发送</span></div>
<div><span>添加验证</span></div> <div>
<div> <el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1"
<el-switch @change="change" :before-change="beforeChange" :active-value="0" :inactive-value="1" v-model="setting.send"/>
v-model="setting.addEmailVerify"/> </div>
</div> </div>
</div> <div class="setting-item">
<div class="setting-item"> <div><span>添加 Resend Token</span></div>
<div><span>siteKey</span></div> <div>
<div class="bot-verify"> <el-button class="opt-button" style="margin-top: 0" @click="openResendForm" size="small" type="primary">
<span>{{ setting.siteKey || '空' }}</span> <Icon icon="material-symbols:add-rounded" width="16" height="16"/>
<el-button class="opt-button" size="small" type="primary" @click="turnstileShow = true"> </el-button>
<Icon icon="lsicon:edit-outline" width="16" height="16"/> </div>
</el-button>
</div> </div>
</div> <div class="setting-item token-item" v-for="(value, key, index) in setting.resendTokens" :key="index">
<div class="setting-item"> <div><span>{{ key }}</span></div>
<div><span>secretKey</span></div> <div><span>{{ value }}</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> </div>
</div>
<div class="settings-card"> <!-- R2 Object Storage Card -->
<div class="card-title">关于</div> <div class="settings-card">
<div class="card-content"> <div class="card-title">R2对象存储</div>
<div class="concerning-item"> <div class="card-content">
<span>版本:</span> <div class="setting-item">
<span>v1.2.1</span> <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>
<div class="concerning-item"> </div>
<span>交流:</span>
<el-button @click="jump('https://t.me/cloud_mail_tg')"> <div class="settings-card">
telegram <div class="card-title">邮件转发通知</div>
<template #icon> <div class="card-content">
<Icon icon="logos:telegram" width="30" height="30"/> <div class="setting-item">
</template> <div><span>Telegram 机器人</span></div>
</el-button> <div class="forward">
<el-button @click="jump('https://github.com/LaziestRen/cloud-mail')"> <span>{{ setting.tgBotStatus === 0 ? '已开启' : '已关闭' }}</span>
github <el-button class="opt-button" size="small" type="primary" @click="openTgSetting">
<template #icon> <Icon icon="fluent:settings-48-regular" width="18" height="18"/>
<Icon icon="codicon:github-inverted" width="22" height="22" /> </el-button>
</template> </div>
</el-button> </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>
</div> </div>
</div> </div>
<!-- Dialogs remain the same --> <!-- Dialogs remain the same -->
<el-dialog v-model="editTitleShow" title="修改标题" width="340" @closed="editTitle = ''"> <el-dialog v-model="editTitleShow" title="修改标题" width="340" @closed="editTitle = ''">
<form> <form>
@@ -242,7 +279,7 @@
<el-button type="primary" :loading="settingLoading" @click="saveTitle">保存</el-button> <el-button type="primary" :loading="settingLoading" @click="saveTitle">保存</el-button>
</form> </form>
</el-dialog> </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> <form>
<el-select style="margin-bottom: 15px" v-model="resendTokenForm.domain" placeholder="Select"> <el-select style="margin-bottom: 15px" v-model="resendTokenForm.domain" placeholder="Select">
<el-option <el-option
@@ -292,6 +329,83 @@
<el-button type="primary" :loading="settingLoading" @click="saveBackground">保存</el-button> <el-button type="primary" :loading="settingLoading" @click="saveBackground">保存</el-button>
</div> </div>
</el-dialog> </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> </el-scrollbar>
</div> </div>
</template> </template>
@@ -306,11 +420,14 @@ import {Icon} from "@iconify/vue";
import {cvtR2Url} from "@/utils/convert.js"; import {cvtR2Url} from "@/utils/convert.js";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import { debounce } from 'lodash-es' import { debounce } from 'lodash-es'
import {isEmail} from "@/utils/verify-utils.js";
import loading from "@/components/loading/index.vue";
defineOptions({ defineOptions({
name: 'sys-setting' name: 'sys-setting'
}) })
const firstLoading = ref(true)
const cropper = ref() const cropper = ref()
const cutImage = ref('') const cutImage = ref('')
const cutShow = ref(false) const cutShow = ref(false)
@@ -320,6 +437,9 @@ const editTitleShow = ref(false)
const resendTokenFormShow = ref(false) const resendTokenFormShow = ref(false)
const r2DomainShow = ref(false) const r2DomainShow = ref(false)
const turnstileShow = ref(false) const turnstileShow = ref(false)
const tgSettingShow = ref(false)
const thirdEmailShow = ref(false)
const forwardRulesShow = ref(false)
const settingStore = useSettingStore(); const settingStore = useSettingStore();
const {settings: setting} = storeToRefs(settingStore); const {settings: setting} = storeToRefs(settingStore);
const editTitle = ref('') const editTitle = ref('')
@@ -345,11 +465,123 @@ const options = [
{label: '20s', value: 20} {label: '20s', value: 20}
] ]
onMounted(() => { const tgChatId = ref([])
resendTokenForm.domain = settingStore.domainList[0]; const tgBotStatus = ref(0)
loginOpacity.value = settingStore.settings.loginOpacity 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() { function doOpacityChange() {
const form = {} const form = {}
form.loginOpacity = loginOpacity.value form.loginOpacity = loginOpacity.value
@@ -500,6 +732,7 @@ function jump(href) {
function editSetting(settingForm, refreshStatus = true) { function editSetting(settingForm, refreshStatus = true) {
if (settingLoading.value) return if (settingLoading.value) return
settingLoading.value = true settingLoading.value = true
settingSet(settingForm).then(() => { settingSet(settingForm).then(() => {
settingLoading.value = false settingLoading.value = false
ElMessage({ ElMessage({
@@ -517,7 +750,11 @@ function editSetting(settingForm, refreshStatus = true) {
r2DomainShow.value = false r2DomainShow.value = false
resendTokenFormShow.value = false resendTokenFormShow.value = false
turnstileShow.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 loginOpacity.value = setting.value.loginOpacity
setting.value = {...setting.value, ...JSON.parse(backup)} setting.value = {...setting.value, ...JSON.parse(backup)}
}).finally(() => { }).finally(() => {
@@ -530,11 +767,27 @@ function editSetting(settingForm, refreshStatus = true) {
.settings-container { .settings-container {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
background: #FAFCFF; background: #FAFCFF !important;
.loading {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
} }
.scroll { .scroll {
width: 100%; 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 { .card-grid {
@@ -577,6 +830,12 @@ function editSetting(settingForm, refreshStatus = true) {
overflow: hidden; overflow: hidden;
} }
@media (min-width: 885px) {
.about {
height: 210px;
}
}
.card-title { .card-title {
font-size: 15px; font-size: 15px;
font-weight: bold; font-weight: bold;
@@ -596,7 +855,6 @@ function editSetting(settingForm, refreshStatus = true) {
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
gap: 10px; gap: 10px;
font-weight: bold; font-weight: bold;
> div:first-child { > div:first-child {
display: flex; display: flex;
align-items: center; 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) { :deep(.cut-dialog.el-dialog) {
width: fit-content !important; width: fit-content !important;
height: 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 { .error-image {
background: #f5f7fa; background: #f5f7fa;
height: 100%; height: 100%;
@@ -654,7 +949,34 @@ function editSetting(settingForm, refreshStatus = true) {
.bot-verify { .bot-verify {
display: grid; display: grid;
grid-template-columns: 1fr auto; 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 { span {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -763,4 +1085,10 @@ form .el-button {
:deep(.el-select__wrapper) { :deep(.el-select__wrapper) {
min-height: 28px; min-height: 28px;
} }
</style>
<style>
.el-popper.is-dark {
}
</style> </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

View File

@@ -6,8 +6,8 @@
<title></title> <title></title>
<link rel="icon" href="/assets/favicon-C5dAZutX.svg" type="image/svg+xml"> <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 src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module" crossorigin src="/assets/index-BVIJB-AL.js"></script> <script type="module" crossorigin src="/assets/index-Cgh0xJS2.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DKdj6vty.css"> <link rel="stylesheet" crossorigin href="/assets/index-Cobuvpco.css">
</head> </head>
<body> <body>
<div id="loading-first"> <div id="loading-first">

View File

@@ -12,6 +12,11 @@ app.get('/setting/query', async (c) => {
return c.json(result.ok(setting)); 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) => { app.put('/setting/setBackground', async (c) => {
const key = await settingService.setBackground(c, await c.req.json()); const key = await settingService.setBackground(c, await c.req.json());
return c.json(result.ok(key)); return c.json(result.ok(key));

View File

@@ -78,6 +78,18 @@ export const settingConst = {
addEmailVerify: { addEmailVerify: {
OPEN: 0, OPEN: 0,
CLOSE: 1, CLOSE: 1,
},
forwardStatus: {
OPEN: 0,
CLOSE: 1,
},
tgBotStatus: {
OPEN: 0,
CLOSE: 1,
},
ruleType: {
ALL: 0,
RULE: 1
} }
} }

View File

@@ -5,13 +5,30 @@ import settingService from '../service/setting-service';
import attService from '../service/att-service'; import attService from '../service/att-service';
import constant from '../const/constant'; import constant from '../const/constant';
import fileUtils from '../utils/file-utils'; 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) { export async function email(message, env, ctx) {
try { 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; return;
} }
@@ -28,14 +45,18 @@ export async function email(message, env, ctx) {
const email = await PostalMime.parse(content); const email = await PostalMime.parse(content);
const toName = email.to.find(item => item.address === message.to)?.name || '';
const params = { const params = {
toEmail: message.to,
toName: toName,
sendEmail: email.from.address, sendEmail: email.from.address,
name: email.from.name, name: email.from.name,
subject: email.subject, subject: email.subject,
content: email.html, content: email.html,
text: email.text, text: email.text,
cc: email.cc ? JSON.stringify(email.cc) : '[]', 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), recipient: JSON.stringify(email.to),
inReplyTo: email.inReplyTo, inReplyTo: email.inReplyTo,
relation: email.references, relation: email.references,
@@ -49,32 +70,96 @@ export async function email(message, env, ctx) {
const attachments = []; const attachments = [];
const cidAttachments = []; const cidAttachments = [];
for(let item of email.attachments) { for (let item of email.attachments) {
let attachment = { ...item }; let attachment = { ...item };
attachment.key = constant.ATTACHMENT_PREFIX + await fileUtils.getBuffHash(attachment.content) + fileUtils.getExtFileName(item.filename); attachment.key = constant.ATTACHMENT_PREFIX + await fileUtils.getBuffHash(attachment.content) + fileUtils.getExtFileName(item.filename);
attachment.size = item.content.length ?? item.content.byteLength; attachment.size = item.content.length ?? item.content.byteLength;
attachments.push(attachment); attachments.push(attachment);
if (attachment.contentId) { 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 => { attachments.forEach(attachment => {
attachment.emailId = emailRow.emailId; attachment.emailId = emailRow.emailId;
attachment.userId = emailRow.userId; attachment.userId = emailRow.userId;
attachment.accountId = emailRow.accountId; 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) { if (attachments.length > 0) {
await attService.addAtt({ env }, attachments); 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) { } catch (e) {
console.error('邮件接收异常: ', e); console.error('邮件接收异常: ', e);
} }
} }

View File

@@ -12,6 +12,8 @@ export const email = sqliteTable('email', {
cc: text('cc').default('[]'), cc: text('cc').default('[]'),
bcc: text('bcc').default('[]'), bcc: text('bcc').default('[]'),
recipient: text('recipient'), recipient: text('recipient'),
toEmail: text('to_email').default('').notNull(),
toName: text('to_name').default('').notNull(),
inReplyTo: text('in_reply_to').default(''), inReplyTo: text('in_reply_to').default(''),
relation: text('relation').default(''), relation: text('relation').default(''),
messageId: text('message_id').default(''), messageId: text('message_id').default(''),

View File

@@ -13,7 +13,14 @@ export const setting = sqliteTable('setting', {
secretKey: text('secret_key'), secretKey: text('secret_key'),
siteKey: text('site_key'), siteKey: text('site_key'),
background: text('background'), 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(), resendTokens: text('resend_tokens').default("{}").notNull(),
}); });
export default setting export default setting

View File

@@ -12,10 +12,47 @@ const init = {
await this.intDB(c); await this.intDB(c);
await this.v1_1DB(c); await this.v1_1DB(c);
await this.v1_2DB(c); await this.v1_2DB(c);
await this.v1_3DB(c);
await settingService.refresh(c); await settingService.refresh(c);
return c.text('初始化成功'); 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){ async v1_2DB(c){
const ADD_COLUMN_SQL_LIST = [ const ADD_COLUMN_SQL_LIST = [

View File

@@ -10,8 +10,8 @@ import app from '../hono/hono';
const exclude = [ const exclude = [
'/login', '/login',
'/register', '/register',
'/setting/query',
'/file', '/file',
'/setting/websiteConfig',
'/webhooks', '/webhooks',
'/init' '/init'
]; ];

View File

@@ -17,7 +17,6 @@ import account from '../entity/account';
import starService from './star-service'; import starService from './star-service';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import kvConst from '../const/kv-const'; import kvConst from '../const/kv-const';
import constant from '../const/constant';
const emailService = { const emailService = {
@@ -514,7 +513,7 @@ const emailService = {
} }
if (accountEmail) { if (accountEmail) {
conditions.push(like(account.email, `${accountEmail}%`)); conditions.push(like(email.toEmail, `${accountEmail}%`));
} }
if (name) { if (name) {
@@ -535,16 +534,14 @@ const emailService = {
conditions.push(lt(email.emailId, emailId)); 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) .from(email)
.leftJoin(user, eq(email.userId, user.userId)) .leftJoin(user, eq(email.userId, user.userId))
.leftJoin(account, eq(email.accountId, account.accountId))
.where(and(...conditions)); .where(and(...conditions));
const queryCount = orm(c).select({ total: count() }) const queryCount = orm(c).select({ total: count() })
.from(email) .from(email)
.leftJoin(user, eq(email.userId, user.userId)) .leftJoin(user, eq(email.userId, user.userId))
.leftJoin(account, eq(email.accountId, account.accountId))
.where(and(...countConditions)); .where(and(...countConditions));
if (timeSort) { if (timeSort) {
@@ -574,10 +571,10 @@ const emailService = {
}, },
async completeReceive(c, status, emailId) { async completeReceive(c, status, emailId) {
await orm(c).update(email).set({ return await orm(c).update(email).set({
isDel: isDel.NORMAL, isDel: isDel.NORMAL,
status: status status: status
}).where(eq(email.emailId, emailId)).run(); }).where(eq(email.emailId, emailId)).returning().get();
} }
}; };

View File

@@ -7,7 +7,6 @@ import r2Service from './r2-service';
import emailService from './email-service'; import emailService from './email-service';
import accountService from './account-service'; import accountService from './account-service';
import userService from './user-service'; import userService from './user-service';
import starService from './star-service';
import constant from '../const/constant'; import constant from '../const/constant';
import BizError from '../error/biz-error'; import BizError from '../error/biz-error';
@@ -32,20 +31,20 @@ const settingService = {
async get(c) { async get(c) {
const settingRow = await this.query(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 => { Object.keys(settingRow.resendTokens).forEach(key => {
settingRow.resendTokens[key] = `${settingRow.resendTokens[key].slice(0, 12)}******`; settingRow.resendTokens[key] = `${settingRow.resendTokens[key].slice(0, 12)}******`;
}); });
return settingRow return settingRow;
}, },
async set(c, params) { async set(c, params) {
const settingData = await this.query(c) const settingData = await this.query(c);
let resendTokens = {...settingData.resendTokens,...params.resendTokens} let resendTokens = { ...settingData.resendTokens, ...params.resendTokens };
Object.keys(resendTokens).forEach(domain => { Object.keys(resendTokens).forEach(domain => {
if(!resendTokens[domain]) delete resendTokens[domain] if (!resendTokens[domain]) delete resendTokens[domain];
}) });
params.resendTokens = JSON.stringify(resendTokens) params.resendTokens = JSON.stringify(resendTokens);
await orm(c).update(setting).set({ ...params }).returning().get(); await orm(c).update(setting).set({ ...params }).returning().get();
await this.refresh(c); await this.refresh(c);
}, },
@@ -109,6 +108,25 @@ const settingService = {
await emailService.physicsDeleteAll(c); await emailService.physicsDeleteAll(c);
await accountService.physicsDeleteAll(c); await accountService.physicsDeleteAll(c);
await userService.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
};
} }
}; };

View File

@@ -1,15 +1,24 @@
import { parseHTML } from 'linkedom';
const emailUtils = { const emailUtils = {
getDomain(email) { getDomain(email) {
if (typeof email !== 'string') return '' if (typeof email !== 'string') return '';
const parts = email.split('@') const parts = email.split('@');
return parts.length === 2 ? parts[1] : '' return parts.length === 2 ? parts[1] : '';
}, },
getName(email) { getName(email) {
if (typeof email !== 'string') return '' if (typeof email !== 'string') return '';
const parts = email.trim().split('@') const parts = email.trim().split('@');
return parts.length === 2 ? parts[0] : '' 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;