mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-08 21:03:14 +08:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee023ac2e9 | ||
|
|
cc77bdf36d | ||
|
|
dec309a0fd | ||
|
|
9488543e44 | ||
|
|
50326bcc98 | ||
|
|
272b624b9b | ||
|
|
e230801a1c | ||
|
|
07833d5ca9 | ||
|
|
101a561894 | ||
|
|
327962432a | ||
|
|
6051d49315 | ||
|
|
95f361743b | ||
|
|
c6afc5d425 | ||
|
|
466f53254b | ||
|
|
ce0a10e6de | ||
|
|
26995982af | ||
|
|
0894ac0dc9 | ||
|
|
47e2cb56b4 | ||
|
|
32767176f0 | ||
|
|
31eb6c23d1 | ||
|
|
91a859bbcf | ||
|
|
525f5e2dce | ||
|
|
908fc0cc86 | ||
|
|
97d24b2087 | ||
|
|
983300acf4 | ||
|
|
144a792cb2 | ||
|
|
278f0112d0 | ||
|
|
764faebf9f | ||
|
|
d4f0c82e42 | ||
|
|
cf680e6349 | ||
|
|
c3987d364c | ||
|
|
3a542a8391 | ||
|
|
241e0b7b28 | ||
|
|
b43353ea47 | ||
|
|
6c334d32f6 | ||
|
|
7889d2edea | ||
|
|
2426e0b51a | ||
|
|
61434ab6f7 | ||
|
|
7f6a02ca38 |
3
.flake8
Normal file
3
.flake8
Normal file
@@ -0,0 +1,3 @@
|
||||
[flake8]
|
||||
max-line-length = 180
|
||||
exclude = .git,__pycache__,build,dist
|
||||
10
.github/workflows/backend_deploy.yaml
vendored
10
.github/workflows/backend_deploy.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
@@ -32,6 +32,14 @@ jobs:
|
||||
|
||||
- name: Deploy Backend for ${{ github.ref_name }}
|
||||
run: |
|
||||
export use_worker_assets=${{ secrets.USE_WORKER_ASSETS }}
|
||||
if [ -n "$use_worker_assets" ]; then
|
||||
cd frontend/
|
||||
pnpm install --no-frozen-lockfile
|
||||
pnpm build:pages
|
||||
cd ..
|
||||
fi
|
||||
|
||||
export debug_mode=${{ secrets.DEBUG_MODE }}
|
||||
export use_mail_wasm_parser=${{ secrets.BACKEND_USE_MAIL_WASM_PARSER }}
|
||||
cd worker/
|
||||
|
||||
2
.github/workflows/docs_deploy.yml
vendored
2
.github/workflows/docs_deploy.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
|
||||
2
.github/workflows/frontend_deploy.yaml
vendored
2
.github/workflows/frontend_deploy.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
|
||||
18
.github/workflows/tag_build.yml
vendored
18
.github/workflows/tag_build.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
@@ -44,10 +44,24 @@ jobs:
|
||||
- name: Build Backend
|
||||
run: cd worker && pnpm install --no-frozen-lockfile && pnpm build
|
||||
|
||||
- name: Move worker.js
|
||||
run: cd worker/dist && mv worker.js ../
|
||||
|
||||
- name: Build Worker with wasm mail parser
|
||||
run: |
|
||||
cd worker
|
||||
echo "Using mail-parser-wasm-worker"
|
||||
pnpm add mail-parser-wasm-worker
|
||||
git apply ../.github/config/mail-parser-wasm-worker.patch
|
||||
echo "Applied mail-parser-wasm-worker patch"
|
||||
pnpm build
|
||||
zip -r worker-with-wasm-mail-parser.zip dist/worker.js dist/*.wasm
|
||||
|
||||
- name: Upload to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
frontend/frontend.zip
|
||||
frontend/telegram-frontend.zip
|
||||
worker/dist/worker.js
|
||||
worker/worker.js
|
||||
worker/worker-with-wasm-mail-parser.zip
|
||||
|
||||
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,7 +1,37 @@
|
||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||
# CHANGE LOG
|
||||
|
||||
## main(v0.8.6)
|
||||
## v0.10.0
|
||||
|
||||
- feat: 支持 User 查看收件箱,`/user_api/mails` 接口, 支持 `address` 和 `keyword` 过滤
|
||||
- fix: 修复 Oauth2 登录获取 Token 时,一些 Oauth2 需要 `redirect_uri` 参数的问题
|
||||
- feat: 用户访问网页时,如果 `user token` 在 7 天内过期,自动刷新
|
||||
- feat: admin portal 中增加初始化 db 的功能
|
||||
- feat: 增加 `ALWAYS_SHOW_ANNOUNCEMENT` 变量,用于配置是否总是显示公告
|
||||
|
||||
## v0.9.1
|
||||
|
||||
- feat: |UI| support google ads
|
||||
- feat: |UI| 使用 shadow DOM 防止样式污染
|
||||
- feat: |UI| 支持 URL jwt 参数自动登录邮箱,jwt 参数会覆盖浏览器中的 jwt
|
||||
- fix: |CleanUP| 修复清理邮件时,清理时间超过 30 天报错的 bug
|
||||
- feat: admin 用户管理页面: 增加 用户地址查看功能
|
||||
- feat: | S3 附件| 增加 S3 附件删除功能
|
||||
- feat: | Admin API| 增加 admin 绑定用户和地址的 api
|
||||
- feat: | Oauth2 | Oatuh2 获取用户信息时,支持 `JSONPATH` 表达式
|
||||
|
||||
## v0.9.0
|
||||
|
||||
- feat: | Worker | 支持多语言
|
||||
- feat: | Worker | `NO_LIMIT_SEND_ROLE` 配置支持多角色, 逗号分割
|
||||
- feat: | Actions | build 里增加 `worker-with-wasm-mail-parser.zip` 支持 UI 部署带 `wasm` 的 worker
|
||||
|
||||
## v0.8.7
|
||||
|
||||
- fix: |UI| 修复移动设备日期显示问题
|
||||
- feat: |Worker| 支持通过 `SMTP` 发送邮件, 使用 [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md)
|
||||
|
||||
## v0.8.6
|
||||
|
||||
- feat: |UI| 公告支持 html 格式
|
||||
- feat: |UI| `COPYRIGHT` 支持 html 格式
|
||||
|
||||
15
README.md
15
README.md
@@ -56,25 +56,26 @@
|
||||
- [查看部署文档](#查看部署文档)
|
||||
- [CHANGELOG](#changelog)
|
||||
- [在线演示](#在线演示)
|
||||
- [功能/TODO](#功能todo)
|
||||
- [功能](#功能)
|
||||
- [Reference](#reference)
|
||||
- [Join Community](#join-community)
|
||||
|
||||
## 功能/TODO
|
||||
## 功能
|
||||
|
||||
- [x] 使用 `password` 重新登录之前的邮箱
|
||||
- [x] 使用 `rust wasm` 解析邮件, 解析速度快, 几乎所有邮件都能解析, node 的解析模块解析邮件失败的邮件, rust wasm 也能解析成功
|
||||
- [x] 使用 `凭证` 重新登录之前的邮箱
|
||||
- [x] 添加完整的用户注册登录功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证切换不同邮箱
|
||||
- [x] 前后台均支持多语言
|
||||
- [x] 获取自定义名字的邮箱,`admin` 可配置黑名单
|
||||
- [x] 支持多语言
|
||||
- [x] 增加访问密码,可作为私人站点
|
||||
- [x] admin 控制台
|
||||
- [x] 增加自动回复功能
|
||||
- [x] 增加查看 `附件` 功能
|
||||
- [x] 使用 `rust wasm` 解析邮件
|
||||
- [x] 支持发送邮件
|
||||
- [x] 支持 `DKIM`
|
||||
- [x] `admin` 后台创建无前缀邮箱
|
||||
- [x] 添加 `SMTP proxy server`,支持 `SMTP` 发送邮件, `IMAP` 查看邮件
|
||||
- [x] 添加完整的用户注册登录功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证切换不同邮箱
|
||||
- [x] `Telegram Bot` 使用,以及 `Telegram` 推送
|
||||
- [x] 完整的 `Telegram Bot` 支持,以及 `Telegram` 推送, Telegram Bot 小程序
|
||||
|
||||
## Reference
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "0.8.6",
|
||||
"version": "0.10.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -20,33 +20,34 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@simplewebauthn/browser": "10.0.0",
|
||||
"@unhead/vue": "^1.11.18",
|
||||
"@vueuse/core": "^12.5.0",
|
||||
"@unhead/vue": "^1.11.20",
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.7.9",
|
||||
"axios": "^1.9.0",
|
||||
"jszip": "^3.10.1",
|
||||
"mail-parser-wasm": "^0.2.1",
|
||||
"naive-ui": "^2.41.0",
|
||||
"postal-mime": "^2.4.1",
|
||||
"naive-ui": "^2.41.1",
|
||||
"postal-mime": "^2.4.3",
|
||||
"vooks": "^0.2.12",
|
||||
"vue": "^3.5.13",
|
||||
"vue": "^3.5.16",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-i18n": "^11.0.1",
|
||||
"vue-router": "^4.5.0"
|
||||
"vue-i18n": "^11.1.5",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vicons/fa": "^0.13.0",
|
||||
"@vicons/material": "^0.13.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"unplugin-auto-import": "^19.0.0",
|
||||
"unplugin-vue-components": "^28.0.0",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vite-plugin-top-level-await": "^1.4.4",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"unplugin-auto-import": "^19.3.0",
|
||||
"unplugin-vue-components": "^28.7.0",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vite-plugin-top-level-await": "^1.5.0",
|
||||
"vite-plugin-wasm": "^3.4.1",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-window": "^7.3.0",
|
||||
"wrangler": "^3.104.0"
|
||||
}
|
||||
"wrangler": "^4.19.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
3560
frontend/pnpm-lock.yaml
generated
3560
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { darkTheme, NGlobalStyle, zhCN } from 'naive-ui'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useScript } from '@unhead/vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from './store'
|
||||
import { useIsMobile } from './utils/composables'
|
||||
@@ -11,12 +12,15 @@ import { api } from './api'
|
||||
const {
|
||||
isDark, loading, useSideMargin, telegramApp, isTelegram
|
||||
} = useGlobalState()
|
||||
const adClient = import.meta.env.VITE_GOOGLE_AD_CLIENT;
|
||||
const adSlot = import.meta.env.VITE_GOOGLE_AD_SLOT;
|
||||
const { locale } = useI18n({});
|
||||
const theme = computed(() => isDark.value ? darkTheme : null)
|
||||
const localeConfig = computed(() => locale.value == 'zh' ? zhCN : null)
|
||||
const isMobile = useIsMobile()
|
||||
const showSideMargin = computed(() => !isMobile.value && useSideMargin.value);
|
||||
|
||||
const showAd = computed(() => !isMobile.value && adClient && adSlot);
|
||||
const gridMaxCols = computed(() => showAd.value ? 8 : 12);
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -37,6 +41,18 @@ onMounted(async () => {
|
||||
document.body.appendChild(script);
|
||||
}
|
||||
|
||||
// check if google ad is enabled
|
||||
if (showAd.value) {
|
||||
useScript({
|
||||
src: `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${adClient}`,
|
||||
async: true,
|
||||
crossorigin: "anonymous",
|
||||
});
|
||||
(window.adsbygoogle = window.adsbygoogle || []).push({});
|
||||
(window.adsbygoogle = window.adsbygoogle || []).push({});
|
||||
}
|
||||
|
||||
|
||||
// check if telegram is enabled
|
||||
const enableTelegram = import.meta.env.VITE_IS_TELEGRAM;
|
||||
if (
|
||||
@@ -63,9 +79,14 @@ onMounted(async () => {
|
||||
<n-spin description="loading..." :show="loading">
|
||||
<n-notification-provider container-style="margin-top: 60px;">
|
||||
<n-message-provider container-style="margin-top: 20px;">
|
||||
<n-grid x-gap="12" :cols="12">
|
||||
<n-gi v-if="showSideMargin" span="1"></n-gi>
|
||||
<n-gi :span="!showSideMargin ? 12 : 10">
|
||||
<n-grid x-gap="12" :cols="gridMaxCols">
|
||||
<n-gi v-if="showSideMargin" span="1">
|
||||
<div class="side" v-if="showAd">
|
||||
<ins class="adsbygoogle" style="display:block" :data-ad-client="adClient" :data-ad-slot="adSlot"
|
||||
data-ad-format="auto" data-full-width-responsive="true"></ins>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi :span="!showSideMargin ? gridMaxCols : (gridMaxCols - 2)">
|
||||
<div class="main">
|
||||
<n-space vertical>
|
||||
<n-layout style="min-height: 80vh;">
|
||||
@@ -76,7 +97,12 @@ onMounted(async () => {
|
||||
</n-space>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi v-if="showSideMargin" span="1"></n-gi>
|
||||
<n-gi v-if="showSideMargin" span="1">
|
||||
<div class="side" v-if="showAd">
|
||||
<ins class="adsbygoogle" style="display:block" :data-ad-client="adClient" :data-ad-slot="adSlot"
|
||||
data-ad-format="auto" data-full-width-responsive="true"></ins>
|
||||
</div>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<n-back-top />
|
||||
</n-message-provider>
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useGlobalState } from '../store'
|
||||
import { h } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
import i18n from '../i18n'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||
const {
|
||||
loading, auth, jwt, settings, openSettings,
|
||||
@@ -22,7 +24,8 @@ const apiFetch = async (path, options = {}) => {
|
||||
method: options.method || 'GET',
|
||||
data: options.body || null,
|
||||
headers: {
|
||||
'x-user-token': userJwt.value,
|
||||
'x-lang': i18n.global.locale.value,
|
||||
'x-user-token': options.userJwt || userJwt.value,
|
||||
'x-user-access-token': userSettings.value.access_token,
|
||||
'x-custom-auth': auth.value,
|
||||
'x-admin-auth': adminAuth.value,
|
||||
@@ -32,14 +35,12 @@ const apiFetch = async (path, options = {}) => {
|
||||
});
|
||||
if (response.status === 401 && path.startsWith("/admin")) {
|
||||
showAdminAuth.value = true;
|
||||
throw new Error("Unauthorized, your admin password is wrong")
|
||||
}
|
||||
if (response.status === 401 && openSettings.value.auth) {
|
||||
showAuth.value = true;
|
||||
throw new Error("Unauthorized, you access password is wrong")
|
||||
}
|
||||
if (response.status >= 300) {
|
||||
throw new Error(`${response.status} ${response.data}` || "error");
|
||||
throw new Error(`[${response.status}]: ${response.data}` || "error");
|
||||
}
|
||||
const data = response.data;
|
||||
return data;
|
||||
@@ -88,7 +89,11 @@ const getOpenSettings = async (message, notification) => {
|
||||
if (openSettings.value.needAuth) {
|
||||
showAuth.value = true;
|
||||
}
|
||||
if (openSettings.value.announcement && openSettings.value.announcement != announcement.value) {
|
||||
if (openSettings.value.announcement
|
||||
&& !openSettings.value.fetched
|
||||
&& (openSettings.value.announcement != announcement.value
|
||||
|| openSettings.value.alwaysShowAnnouncement)
|
||||
) {
|
||||
announcement.value = openSettings.value.announcement;
|
||||
notification.info({
|
||||
content: () => {
|
||||
@@ -138,6 +143,19 @@ const getUserSettings = async (message) => {
|
||||
if (!userJwt.value) return;
|
||||
const res = await api.fetch("/user_api/settings")
|
||||
Object.assign(userSettings.value, res)
|
||||
// auto refresh user jwt
|
||||
if (userSettings.value.new_user_token) {
|
||||
try {
|
||||
await api.fetch("/user_api/settings", {
|
||||
userJwt: userSettings.value.new_user_token,
|
||||
})
|
||||
userJwt.value = userSettings.value.new_user_token;
|
||||
console.log("User JWT updated successfully");
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Failed to update user JWT", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
message?.error(error.message || "error");
|
||||
} finally {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CloudDownloadRound, ReplyFilled, ForwardFilled } from '@vicons/material
|
||||
import { useIsMobile } from '../utils/composables'
|
||||
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
||||
import { utcToLocalDate } from '../utils';
|
||||
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
|
||||
|
||||
const message = useMessage()
|
||||
const isMobile = useIsMobile()
|
||||
@@ -171,7 +172,7 @@ const refresh = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const backFirstPageAndRefresh = async () =>{
|
||||
const backFirstPageAndRefresh = async () => {
|
||||
page.value = 1;
|
||||
await refresh();
|
||||
}
|
||||
@@ -380,7 +381,7 @@ onBeforeUnmount(() => {
|
||||
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
|
||||
:on-update:size="onSpiltSizeChange">
|
||||
<template #1>
|
||||
<div style="overflow: auto; height: 80vh;">
|
||||
<div style="overflow: auto; min-height: 50vh; max-height: 100vh;">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||
:class="mailItemClass(row)">
|
||||
@@ -396,10 +397,14 @@ onBeforeUnmount(() => {
|
||||
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
<n-ellipsis style="max-width: 240px;">
|
||||
{{ showEMailTo ? "FROM: " + row.source : row.source }}
|
||||
</n-ellipsis>
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ row.address }}
|
||||
<n-ellipsis style="max-width: 240px;">
|
||||
TO: {{ row.address }}
|
||||
</n-ellipsis>
|
||||
</n-tag>
|
||||
</template>
|
||||
</n-thing>
|
||||
@@ -460,7 +465,7 @@ onBeforeUnmount(() => {
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
|
||||
style="margin-top: 10px;width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
|
||||
<ShadowHtmlComponent v-else :htmlContent="curMail.message" style="margin-top: 10px;" />
|
||||
</n-card>
|
||||
<n-card :bordered="false" embedded class="mail-item" v-else>
|
||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||
@@ -498,7 +503,7 @@ onBeforeUnmount(() => {
|
||||
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
{{ showEMailTo ? "FROM: " + row.source : row.source }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ row.address }}
|
||||
@@ -560,7 +565,7 @@ onBeforeUnmount(() => {
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
|
||||
style="margin-top: 10px;width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
|
||||
<ShadowHtmlComponent :key="curMail.id" v-else :htmlContent="curMail.message" style="margin-top: 10px;" />
|
||||
</n-card>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
|
||||
75
frontend/src/components/ShadowHtmlComponent.vue
Normal file
75
frontend/src/components/ShadowHtmlComponent.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div v-if="useFallback" v-html="htmlContent"></div>
|
||||
<div v-else ref="shadowHost"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
htmlContent: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const shadowHost = ref(null);
|
||||
let shadowRoot = null;
|
||||
const useFallback = ref(false);
|
||||
|
||||
/**
|
||||
* Renders content into Shadow DOM with fallback to v-html
|
||||
*/
|
||||
const renderShadowDom = () => {
|
||||
if (!shadowHost.value && !useFallback.value) return;
|
||||
|
||||
try {
|
||||
// Don't attempt to use Shadow DOM if already in fallback mode
|
||||
if (useFallback.value) return;
|
||||
|
||||
// Initialize Shadow DOM if not already created
|
||||
if (!shadowRoot && shadowHost.value) {
|
||||
try {
|
||||
shadowRoot = shadowHost.value.attachShadow({ mode: 'open' });
|
||||
} catch (error) {
|
||||
console.warn('Shadow DOM not supported, falling back to v-html:', error);
|
||||
useFallback.value = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update content if Shadow DOM exists
|
||||
if (shadowRoot) {
|
||||
shadowRoot.innerHTML = props.htmlContent;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to render Shadow DOM, falling back to v-html:', error);
|
||||
useFallback.value = true;
|
||||
}
|
||||
};
|
||||
|
||||
// Initial render when component is mounted
|
||||
onMounted(() => {
|
||||
// Check if Shadow DOM is supported in this browser
|
||||
if (typeof Element.prototype.attachShadow !== 'function') {
|
||||
console.warn('Shadow DOM is not supported in this browser, using v-html fallback');
|
||||
useFallback.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
renderShadowDom();
|
||||
});
|
||||
|
||||
// Clean up resources when component is unmounted
|
||||
onBeforeUnmount(() => {
|
||||
if (shadowRoot) {
|
||||
shadowRoot.innerHTML = '';
|
||||
}
|
||||
shadowRoot = null;
|
||||
});
|
||||
|
||||
// Update Shadow DOM when htmlContent changes
|
||||
watch(() => props.htmlContent, () => {
|
||||
renderShadowDom();
|
||||
}, { flush: 'post' });
|
||||
</script>
|
||||
15
frontend/src/i18n.ts
Normal file
15
frontend/src/i18n.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // you must set `false`, to use Composition API
|
||||
locale: 'zh', // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
'en': {
|
||||
messages: {}
|
||||
},
|
||||
'zh': {
|
||||
messages: {}
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n;
|
||||
@@ -1,29 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import router from './router'
|
||||
import { createHead } from '@unhead/vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // you must set `false`, to use Composition API
|
||||
locale: 'zh', // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
'en': {
|
||||
messages: {}
|
||||
},
|
||||
'zh': {
|
||||
messages: {}
|
||||
}
|
||||
})
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.params.lang && ['en', 'zh'].includes(to.params.lang)) {
|
||||
i18n.global.locale.value = to.params.lang
|
||||
} else {
|
||||
i18n.global.locale.value = 'zh'
|
||||
}
|
||||
});
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import i18n from './i18n'
|
||||
|
||||
const head = createHead()
|
||||
const app = createApp(App)
|
||||
|
||||
@@ -2,6 +2,10 @@ import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Index from '../views/Index.vue'
|
||||
import User from '../views/User.vue'
|
||||
import UserOauth2Callback from '../views/user/UserOauth2Callback.vue'
|
||||
import i18n from '../i18n'
|
||||
import { useGlobalState } from '../store'
|
||||
|
||||
const { jwt } = useGlobalState()
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -37,6 +41,20 @@ const router = createRouter({
|
||||
redirect: '/'
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.params.lang && ['en', 'zh'].includes(to.params.lang)) {
|
||||
i18n.global.locale.value = to.params.lang
|
||||
} else {
|
||||
i18n.global.locale.value = 'zh'
|
||||
}
|
||||
// check if query parameter has jwt, set it to store
|
||||
if (to.query.jwt) {
|
||||
jwt.value = to.query.jwt;
|
||||
}
|
||||
next()
|
||||
});
|
||||
|
||||
export default router
|
||||
|
||||
@@ -14,6 +14,7 @@ export const useGlobalState = createGlobalState(
|
||||
fetched: false,
|
||||
title: '',
|
||||
announcement: '',
|
||||
alwaysShowAnnouncement: false,
|
||||
prefix: '',
|
||||
addressRegex: '',
|
||||
needAuth: false,
|
||||
@@ -67,7 +68,7 @@ export const useGlobalState = createGlobalState(
|
||||
const useIframeShowMail = useStorage('useIframeShowMail', false);
|
||||
const preferShowTextMail = useStorage('preferShowTextMail', false);
|
||||
const userJwt = useStorage('userJwt', '');
|
||||
const userTab = useSessionStorage('userTab', 'user_settings');
|
||||
const userTab = useSessionStorage('userTab', 'address_management');
|
||||
const indexTab = useSessionStorage('indexTab', 'mailbox');
|
||||
const globalTabplacement = useStorage('globalTabplacement', 'top');
|
||||
const useSideMargin = useStorage('useSideMargin', true);
|
||||
@@ -92,6 +93,8 @@ export const useGlobalState = createGlobalState(
|
||||
is_admin: false,
|
||||
/** @type {string | null} */
|
||||
access_token: null,
|
||||
/** @type {string | null} */
|
||||
new_user_token: null,
|
||||
/** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */
|
||||
user_role: null,
|
||||
});
|
||||
|
||||
@@ -19,6 +19,9 @@ export const utcToLocalDate = (utcDate: string, useUTCDate: boolean) => {
|
||||
}
|
||||
try {
|
||||
const date = new Date(utcDateString);
|
||||
// if invalid date string
|
||||
if (isNaN(date.getTime())) return utcDateString;
|
||||
|
||||
return date.toLocaleString();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -18,12 +18,12 @@ import Mails from './admin/Mails.vue';
|
||||
import MailsUnknow from './admin/MailsUnknow.vue';
|
||||
import About from './common/About.vue';
|
||||
import Maintenance from './admin/Maintenance.vue';
|
||||
import DatabaseManager from './admin/DatabaseManager.vue';
|
||||
import Appearance from './common/Appearance.vue';
|
||||
import Telegram from './admin/Telegram.vue';
|
||||
import Webhook from './admin/Webhook.vue';
|
||||
import MailWebhook from './admin/MailWebhook.vue';
|
||||
import WorkerConfig from './admin/WorkerConfig.vue';
|
||||
import SendMail from './admin/SendMail.vue';
|
||||
|
||||
const {
|
||||
adminAuth, showAdminAuth, adminTab, loading,
|
||||
@@ -31,6 +31,12 @@ const {
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const SendMail = defineAsyncComponent(() => {
|
||||
loading.value = true;
|
||||
return import('./admin/SendMail.vue')
|
||||
.finally(() => loading.value = false);
|
||||
});
|
||||
|
||||
const authFunc = async () => {
|
||||
try {
|
||||
adminAuth.value = tmpAdminAuth.value;
|
||||
@@ -62,6 +68,7 @@ const { t } = useI18n({
|
||||
webhookSettings: 'Webhook Settings',
|
||||
statistics: 'Statistics',
|
||||
maintenance: 'Maintenance',
|
||||
database: 'Database',
|
||||
workerconfig: 'Worker Config',
|
||||
appearance: 'Appearance',
|
||||
about: 'About',
|
||||
@@ -88,6 +95,7 @@ const { t } = useI18n({
|
||||
webhookSettings: 'Webhook 设置',
|
||||
statistics: '统计',
|
||||
maintenance: '维护',
|
||||
database: '数据库',
|
||||
workerconfig: 'Worker 配置',
|
||||
appearance: '外观',
|
||||
about: '关于',
|
||||
@@ -107,7 +115,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="userSettings.fetched">
|
||||
<n-modal v-model:show="showAdminPasswordModal" :closable="false" :closeOnEsc="false" :maskClosable="false"
|
||||
preset="dialog" :title="t('accessHeader')">
|
||||
<p>{{ t('accessTip') }}</p>
|
||||
@@ -121,6 +129,9 @@ onMounted(async () => {
|
||||
<n-tabs v-if="showAdminPage" type="card" v-model:value="adminTab" :placement="globalTabplacement">
|
||||
<n-tab-pane name="qucickSetup" :tab="t('qucickSetup')">
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="database" :tab="t('database')">
|
||||
<DatabaseManager />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="account_settings" :tab="t('account_settings')">
|
||||
<AccountSettings />
|
||||
</n-tab-pane>
|
||||
@@ -191,6 +202,9 @@ onMounted(async () => {
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="database" :tab="t('database')">
|
||||
<DatabaseManager />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="workerconfig" :tab="t('workerconfig')">
|
||||
<WorkerConfig />
|
||||
</n-tab-pane>
|
||||
|
||||
@@ -126,7 +126,9 @@ const menuOptions = computed(() => [
|
||||
type: menuValue.value == "admin" ? "primary" : "default",
|
||||
style: "width: 100%",
|
||||
onClick: async () => {
|
||||
loading.value = true;
|
||||
await router.push(getRouterPathWithLang('/admin', locale.value));
|
||||
loading.value = false;
|
||||
showMobileMenu.value = false;
|
||||
}
|
||||
},
|
||||
@@ -214,7 +216,9 @@ const logoClick = async () => {
|
||||
if (logoClickCount.value >= 5) {
|
||||
logoClickCount.value = 0;
|
||||
message.info("Change to admin Page");
|
||||
loading.value = true;
|
||||
await router.push(getRouterPathWithLang('/admin', locale.value));
|
||||
loading.value = false;
|
||||
} else {
|
||||
logoClickCount.value++;
|
||||
}
|
||||
|
||||
@@ -15,11 +15,16 @@ import Webhook from './index/Webhook.vue';
|
||||
import Attachment from './index/Attachment.vue';
|
||||
import About from './common/About.vue';
|
||||
|
||||
const SendMail = defineAsyncComponent(() => import('./index/SendMail.vue'));
|
||||
const { settings, openSettings, indexTab, globalTabplacement } = useGlobalState()
|
||||
const { loading, settings, openSettings, indexTab, globalTabplacement } = useGlobalState()
|
||||
const message = useMessage()
|
||||
const route = useRoute()
|
||||
|
||||
const SendMail = defineAsyncComponent(() => {
|
||||
loading.value = true;
|
||||
return import('./index/SendMail.vue')
|
||||
.finally(() => loading.value = false);
|
||||
});
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import AddressMangement from './user/AddressManagement.vue';
|
||||
import UserSettingsPage from './user/UserSettings.vue';
|
||||
import UserBar from './user/UserBar.vue';
|
||||
import BindAddress from './user/BindAddress.vue';
|
||||
import UserMailBox from './user/UserMailBox.vue';
|
||||
|
||||
const {
|
||||
userTab, globalTabplacement, userSettings
|
||||
@@ -16,11 +17,13 @@ const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
address_management: 'Address Management',
|
||||
user_mail_box_tab: 'Mail Box',
|
||||
user_settings: 'User Settings',
|
||||
bind_address: 'Bind Mail Address',
|
||||
},
|
||||
zh: {
|
||||
address_management: '地址管理',
|
||||
user_mail_box_tab: '收件箱',
|
||||
user_settings: '用户设置',
|
||||
bind_address: '绑定邮箱地址',
|
||||
}
|
||||
@@ -36,6 +39,9 @@ const { t } = useI18n({
|
||||
<n-tab-pane name="address_management" :tab="t('address_management')">
|
||||
<AddressMangement />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="user_mail_box_tab" :tab="t('user_mail_box_tab')">
|
||||
<UserMailBox />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="user_settings" :tab="t('user_settings')">
|
||||
<UserSettingsPage />
|
||||
</n-tab-pane>
|
||||
|
||||
@@ -212,7 +212,8 @@ const columns = [
|
||||
}
|
||||
},
|
||||
{ default: () => t('viewMails') }
|
||||
)
|
||||
),
|
||||
show: row.mail_count > 0
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
@@ -224,7 +225,8 @@ const columns = [
|
||||
}
|
||||
},
|
||||
{ default: () => t('viewSendBox') }
|
||||
)
|
||||
),
|
||||
show: row.send_count > 0
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
|
||||
126
frontend/src/views/admin/DatabaseManager.vue
Normal file
126
frontend/src/views/admin/DatabaseManager.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CleaningServicesFilled } from '@vicons/material'
|
||||
|
||||
import { api } from '../../api'
|
||||
import { init } from 'vooks/lib/on-fonts-ready';
|
||||
|
||||
const message = useMessage()
|
||||
const dbVersionData = ref({
|
||||
need_initialization: false,
|
||||
need_migration: false,
|
||||
current_db_version: '',
|
||||
code_db_version: ''
|
||||
})
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
need_initialization_tip: 'Database initialization is required. Please initialize the database.',
|
||||
need_migration_tip: 'Database migration is required. Please migrate the database.',
|
||||
current_db_version: 'Current DB Version',
|
||||
code_db_version: 'Code Needed DB Version',
|
||||
init: 'Initialize Database',
|
||||
migration: 'Migrate Database',
|
||||
initializationSuccess: 'Database initialized successfully',
|
||||
migrationSuccess: 'Database migrated successfully',
|
||||
},
|
||||
zh: {
|
||||
need_initialization_tip: '需要初始化数据库,请初始化数据库',
|
||||
need_migration_tip: '需要迁移数据库,请迁移数据库',
|
||||
current_db_version: '当前数据库版本',
|
||||
code_db_version: '需要的数据库版本',
|
||||
init: '初始化数据库',
|
||||
migration: '升级数据库 Schema',
|
||||
initializationSuccess: '数据库初始化成功',
|
||||
migrationSuccess: '数据库升级成功',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await api.fetch('/admin/db_version');
|
||||
if (res) Object.assign(dbVersionData.value, res);
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const initialization = async () => {
|
||||
try {
|
||||
await api.fetch('/admin/db_initialize', {
|
||||
method: 'POST'
|
||||
});
|
||||
await fetchData();
|
||||
message.success(t('initializationSuccess'));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const migration = async () => {
|
||||
try {
|
||||
await api.fetch('/admin/db_migration', {
|
||||
method: 'POST'
|
||||
});
|
||||
await fetchData();
|
||||
message.success(t('migrationSuccess'));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData();
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-alert v-if="dbVersionData.need_initialization" type="warning" :show-icon="false" :bordered="false">
|
||||
<span>{{ t('need_initialization_tip') }}</span>
|
||||
<n-button @click="initialization" type="primary" secondary block :loading="loading">
|
||||
{{ t('init') }}
|
||||
</n-button>
|
||||
</n-alert>
|
||||
<n-alert v-if="dbVersionData.need_migration" type="warning" :show-icon="false" :bordered="false">
|
||||
<span>{{ t('need_migration_tip') }}</span>
|
||||
<n-button @click="migration" type="primary" secondary block :loading="loading">
|
||||
{{ t('migration') }}
|
||||
</n-button>
|
||||
</n-alert>
|
||||
<n-alert type="info" :show-icon="false" :bordered="false">
|
||||
<span>
|
||||
{{ t('current_db_version') }}: {{ dbVersionData.current_db_version || "unknown" }},
|
||||
{{ t('code_db_version') }}: {{ dbVersionData.code_db_version }}
|
||||
</span>
|
||||
</n-alert>
|
||||
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-card {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.n-alert {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
94
frontend/src/views/admin/UserAddressManagement.vue
Normal file
94
frontend/src/views/admin/UserAddressManagement.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { NBadge } from 'naive-ui'
|
||||
|
||||
import { api } from '../../api'
|
||||
|
||||
const props = defineProps({
|
||||
user_id: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const { locale, t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
success: 'success',
|
||||
name: 'Name',
|
||||
mail_count: 'Mail Count',
|
||||
send_count: 'Send Count',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
name: '名称',
|
||||
mail_count: '邮件数量',
|
||||
send_count: '发送数量',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const data = ref([])
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const { results } = await api.fetch(
|
||||
`/admin/users/bind_address/${props.user_id}`,
|
||||
);
|
||||
data.value = results;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('name'),
|
||||
key: "name"
|
||||
},
|
||||
{
|
||||
title: t('mail_count'),
|
||||
key: "mail_count",
|
||||
render(row) {
|
||||
return h(NBadge, {
|
||||
value: row.mail_count,
|
||||
'show-zero': true,
|
||||
max: 99,
|
||||
type: "success"
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('send_count'),
|
||||
key: "send_count",
|
||||
render(row) {
|
||||
return h(NBadge, {
|
||||
value: row.send_count,
|
||||
'show-zero': true,
|
||||
max: 99,
|
||||
type: "success"
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="overflow: auto;">
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-data-table {
|
||||
min-width: 700px;
|
||||
}
|
||||
</style>
|
||||
@@ -8,6 +8,8 @@ import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { hashPassword } from '../../utils';
|
||||
|
||||
import UserAddressManagement from './UserAddressManagement.vue'
|
||||
|
||||
const { loading, openSettings } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
@@ -34,6 +36,7 @@ const { t } = useI18n({
|
||||
prefix: 'Prefix',
|
||||
domains: 'Domains',
|
||||
roleDonotExist: 'Current Role does not exist',
|
||||
userAddressManagement: 'Address Management',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
@@ -56,6 +59,7 @@ const { t } = useI18n({
|
||||
prefix: '前缀',
|
||||
domains: '域名',
|
||||
roleDonotExist: '当前角色不存在',
|
||||
userAddressManagement: '地址管理',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -75,6 +79,7 @@ const user = ref({
|
||||
password: ""
|
||||
})
|
||||
const showChangeRole = ref(false)
|
||||
const showUserAddressManagement = ref(false)
|
||||
const userRoles = ref([])
|
||||
const curUserRole = ref('')
|
||||
const userRolesOptions = computed(() => {
|
||||
@@ -214,12 +219,25 @@ const columns = [
|
||||
title: t('address_count'),
|
||||
key: "address_count",
|
||||
render(row) {
|
||||
return h(NBadge, {
|
||||
value: row.address_count,
|
||||
'show-zero': true,
|
||||
max: 99,
|
||||
type: "success"
|
||||
})
|
||||
return h(NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => {
|
||||
if (row.address_count <= 0) return;
|
||||
curUserId.value = row.id;
|
||||
showUserAddressManagement.value = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: () => h(NBadge, {
|
||||
value: row.address_count,
|
||||
'show-zero': true,
|
||||
max: 99,
|
||||
type: "success"
|
||||
}),
|
||||
default: () => row.address_count > 0 ? t('userAddressManagement') : ""
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -239,6 +257,19 @@ const columns = [
|
||||
icon: () => h(MenuFilled),
|
||||
key: "action",
|
||||
children: [
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => {
|
||||
curUserId.value = row.id;
|
||||
showUserAddressManagement.value = true;
|
||||
}
|
||||
},
|
||||
{ default: () => t('userAddressManagement') }
|
||||
),
|
||||
show: row.address_count > 0
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
@@ -362,6 +393,9 @@ onMounted(async () => {
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showUserAddressManagement" preset="card" :title="t('userAddressManagement')">
|
||||
<UserAddressManagement :user_id="curUserId" />
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="userQuery" @keydown.enter="fetchData" />
|
||||
<n-button @click="fetchData" type="primary" tertiary>
|
||||
|
||||
@@ -224,13 +224,13 @@ onMounted(async () => {
|
||||
<n-form-item-row label="Access Token URL" required>
|
||||
<n-input v-model:value="item.accessTokenURL" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="Access Token accessTokenFormat" required>
|
||||
<n-form-item-row label="Access Token Params Format" required>
|
||||
<n-select v-model:value="item.accessTokenFormat" :options="accessTokenFormatOptions" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="User Info URL" required>
|
||||
<n-input v-model:value="item.userInfoURL" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="User Email Key" required>
|
||||
<n-form-item-row label="User Email Key (Support JSONPATH like $[0].email)" required>
|
||||
<n-input v-model:value="item.userEmailKey" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="Redirect URL" required>
|
||||
|
||||
@@ -71,6 +71,7 @@ const { locale, t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
login: 'Login',
|
||||
loginAndBind: 'Login and Bind',
|
||||
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
|
||||
getNewEmail: 'Create New Email',
|
||||
getNewEmailTip1: 'Please input the email you want to use. only allow: ',
|
||||
@@ -86,6 +87,7 @@ const { locale, t } = useI18n({
|
||||
},
|
||||
zh: {
|
||||
login: '登录',
|
||||
loginAndBind: '登录并绑定',
|
||||
pleaseGetNewEmail: '请"登录"或点击 "注册新邮箱" 按钮来获取一个新的邮箱地址',
|
||||
getNewEmail: '创建新邮箱',
|
||||
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许: ',
|
||||
@@ -102,6 +104,13 @@ const { locale, t } = useI18n({
|
||||
}
|
||||
});
|
||||
|
||||
const loginAndBindTag = computed(() => {
|
||||
if (userSettings.value.user_email) {
|
||||
return t('loginAndBind')
|
||||
}
|
||||
return t('login')
|
||||
})
|
||||
|
||||
const addressRegex = computed(() => {
|
||||
try {
|
||||
if (openSettings.value.addressRegex) {
|
||||
@@ -208,7 +217,7 @@ onMounted(async () => {
|
||||
<span>{{ t('bindUserInfo') }}</span>
|
||||
</n-alert>
|
||||
<n-tabs v-if="openSettings.fetched" v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||
<n-tab-pane name="signin" :tab="t('login')">
|
||||
<n-tab-pane name="signin" :tab="loginAndBindTag">
|
||||
<n-form>
|
||||
<n-form-item-row :label="t('credential')" required>
|
||||
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
@@ -217,7 +226,7 @@ onMounted(async () => {
|
||||
<template #icon>
|
||||
<n-icon :component="EmailOutlined" />
|
||||
</template>
|
||||
{{ t('login') }}
|
||||
{{ loginAndBindTag }}
|
||||
</n-button>
|
||||
<n-button v-if="showNewAddressTab" @click="tabValue = 'register'" block secondary strong>
|
||||
<template #icon>
|
||||
|
||||
@@ -74,7 +74,7 @@ const deleteAccount = async () => {
|
||||
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
||||
<p>{{ t('logoutConfirm') }}</p>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="logout" size="small" tertiary type="primary">
|
||||
<n-button :loading="loading" @click="logout" size="small" tertiary type="warning">
|
||||
{{ t('logout') }}
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
@@ -32,6 +32,7 @@ const { locale, t } = useI18n({
|
||||
copied: 'Copied',
|
||||
fetchAddressError: 'Mail address credential is invalid or account not exist, it may be network connection issue, please try again later.',
|
||||
addressCredential: 'Mail Address Credential',
|
||||
linkWithAddressCredential: 'Open to auto login email link',
|
||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||
userLogin: 'User Login',
|
||||
},
|
||||
@@ -43,6 +44,7 @@ const { locale, t } = useI18n({
|
||||
copied: '已复制',
|
||||
fetchAddressError: '邮箱地址凭证无效或邮箱地址不存在,也可能是网络连接异常,请稍后再尝试。',
|
||||
addressCredential: '邮箱地址凭证',
|
||||
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||
userLogin: '用户登录',
|
||||
}
|
||||
@@ -73,6 +75,10 @@ const copy = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getUrlWithJwt = () => {
|
||||
return `${window.location.origin}/?jwt=${jwt.value}`
|
||||
}
|
||||
|
||||
const onUserLogin = async () => {
|
||||
await router.push(getRouterPathWithLang("/user", locale.value))
|
||||
}
|
||||
@@ -140,9 +146,18 @@ onMounted(async () => {
|
||||
<span>
|
||||
<p>{{ t("addressCredentialTip") }}</p>
|
||||
</span>
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-card embedded>
|
||||
<b>{{ jwt }}</b>
|
||||
</n-card>
|
||||
<n-card embedded>
|
||||
<n-collapse>
|
||||
<n-collapse-item :title='t("linkWithAddressCredential")'>
|
||||
<n-card embedded>
|
||||
<b>{{ getUrlWithJwt() }}</b>
|
||||
</n-card>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, h, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { api } from '../../api'
|
||||
import { NPopconfirm } from 'naive-ui';
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
@@ -11,10 +12,16 @@ const { t } = useI18n({
|
||||
en: {
|
||||
download: 'Download',
|
||||
action: 'Action',
|
||||
delete: 'Delete',
|
||||
deleteConfirm: 'Are you sure to delete this attachment?',
|
||||
deleteSuccess: 'Deleted successfully',
|
||||
},
|
||||
zh: {
|
||||
download: '下载',
|
||||
action: '操作',
|
||||
delete: '删除',
|
||||
deleteConfirm: '确定要删除此附件吗?',
|
||||
deleteSuccess: '删除成功',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -66,6 +73,34 @@ const columns = [
|
||||
}
|
||||
},
|
||||
{ default: () => t('download') }
|
||||
),
|
||||
h(NPopconfirm,
|
||||
{
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await api.fetch(`/api/attachment/delete`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key: row.key })
|
||||
});
|
||||
message.success(t('deleteSuccess'));
|
||||
await fetchData();
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
type: "error",
|
||||
},
|
||||
{ default: () => t('delete') }
|
||||
),
|
||||
default: () => t('deleteConfirm')
|
||||
}
|
||||
)
|
||||
])
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ const { t } = useI18n({
|
||||
actions: 'Actions',
|
||||
changeMailAddress: 'Change Mail Address',
|
||||
unbindMailAddress: 'Unbind Mail Address credential',
|
||||
bind: 'Bind',
|
||||
create_or_bind: 'Create or Bind',
|
||||
bindAddressSuccess: 'Bind Address Success',
|
||||
},
|
||||
zh: {
|
||||
@@ -32,7 +32,7 @@ const { t } = useI18n({
|
||||
actions: '操作',
|
||||
changeMailAddress: '切换邮箱地址',
|
||||
unbindMailAddress: '解绑邮箱地址',
|
||||
bind: '绑定',
|
||||
create_or_bind: '创建或绑定',
|
||||
bindAddressSuccess: '绑定地址成功',
|
||||
}
|
||||
}
|
||||
@@ -151,7 +151,7 @@ const columns = [
|
||||
<n-tab-pane name="address" :tab="t('address')">
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="bind" :tab="t('bind')">
|
||||
<n-tab-pane name="create_or_bind" :tab="t('create_or_bind')">
|
||||
<Login :bindUserAddress="bindAddress" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
|
||||
@@ -8,6 +8,8 @@ import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { getRouterPathWithLang } from '../../utils'
|
||||
|
||||
import Login from '../common/Login.vue';
|
||||
|
||||
const { jwt } = useGlobalState()
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
@@ -25,7 +27,9 @@ const { locale, t } = useI18n({
|
||||
unbindAddressTip: 'Before unbinding, please switch to this email address and save the email address credential.',
|
||||
transferAddress: 'Transfer Address',
|
||||
targetUserEmail: 'Target User Email',
|
||||
transferAddressTip: 'Transfer address to another user will remove the address from your account and transfer it to another user. Are you sure to transfer the address?'
|
||||
transferAddressTip: 'Transfer address to another user will remove the address from your account and transfer it to another user. Are you sure to transfer the address?',
|
||||
address: 'Address',
|
||||
create_or_bind: 'Create or Bind',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
@@ -38,7 +42,9 @@ const { locale, t } = useI18n({
|
||||
unbindAddressTip: '解绑前请切换到此邮箱地址并保存邮箱地址凭证。',
|
||||
transferAddress: '转移地址',
|
||||
targetUserEmail: '目标用户邮箱',
|
||||
transferAddressTip: '转移地址到其他用户将会从你的账户中移除此地址并转移给其他用户。确定要转移地址吗?'
|
||||
transferAddressTip: '转移地址到其他用户将会从你的账户中移除此地址并转移给其他用户。确定要转移地址吗?',
|
||||
address: '地址',
|
||||
create_or_bind: '创建或绑定',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -111,13 +117,10 @@ const transferAddress = async () => {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const { results, count: addressCount } = await api.fetch(
|
||||
const { results } = await api.fetch(
|
||||
`/user_api/bind_address`
|
||||
);
|
||||
data.value = results;
|
||||
if (addressCount > 0) {
|
||||
count.value = addressCount;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
@@ -211,20 +214,29 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal v-model:show="showTranferAddress" preset="dialog" :title="t('transferAddress')">
|
||||
<span>
|
||||
<p>{{ t("transferAddressTip") }}</p>
|
||||
<p>{{ t('transferAddress') + ": " + currentAddress }}</p>
|
||||
<n-input v-model:value="targetUserEmail" :placeholder="t('targetUserEmail')" />
|
||||
</span>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="transferAddress" size="small" tertiary type="error">
|
||||
{{ t('transferAddress') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<div style="overflow: auto;">
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
<div>
|
||||
<n-modal v-model:show="showTranferAddress" preset="dialog" :title="t('transferAddress')">
|
||||
<span>
|
||||
<p>{{ t("transferAddressTip") }}</p>
|
||||
<p>{{ t('transferAddress') + ": " + currentAddress }}</p>
|
||||
<n-input v-model:value="targetUserEmail" :placeholder="t('targetUserEmail')" />
|
||||
</span>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="transferAddress" size="small" tertiary type="error">
|
||||
{{ t('transferAddress') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-tabs type="segment">
|
||||
<n-tab-pane name="address" :tab="t('address')">
|
||||
<div style="overflow: auto;">
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="create_or_bind" :tab="t('create_or_bind')">
|
||||
<Login />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
90
frontend/src/views/user/UserMailBox.vue
Normal file
90
frontend/src/views/user/UserMailBox.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { api } from '../../api'
|
||||
import MailBox from '../../components/MailBox.vue';
|
||||
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
addressQueryTip: 'Leave blank to query all addresses',
|
||||
keywordQueryTip: 'Leave blank to not query by keyword',
|
||||
query: 'Query',
|
||||
},
|
||||
zh: {
|
||||
addressQueryTip: '留空查询所有地址',
|
||||
keywordQueryTip: '留空不按关键字查询',
|
||||
query: '查询',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mailBoxKey = ref("")
|
||||
const addressFilter = ref();
|
||||
const mailKeyword = ref("")
|
||||
const addressFilterOptions = ref([]);
|
||||
|
||||
const queryMail = () => {
|
||||
addressFilter.value = addressFilter.value.trim();
|
||||
mailKeyword.value = mailKeyword.value.trim();
|
||||
mailBoxKey.value = Date.now();
|
||||
}
|
||||
|
||||
const fetchMailData = async (limit, offset) => {
|
||||
return await api.fetch(
|
||||
`/user_api/mails`
|
||||
+ `?limit=${limit}`
|
||||
+ `&offset=${offset}`
|
||||
+ (addressFilter.value ? `&address=${addressFilter.value}` : '')
|
||||
+ (mailKeyword.value ? `&keyword=${mailKeyword.value}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
const fetchAddresData = async () => {
|
||||
try {
|
||||
const { results } = await api.fetch(
|
||||
`/user_api/bind_address`
|
||||
);
|
||||
addressFilterOptions.value = results.map((item) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.name
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMail = async (curMailId) => {
|
||||
await api.fetch(`/user_api/mails/${curMailId}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
watch(addressFilter, async (newValue) => {
|
||||
console.log("addressFilter", newValue);
|
||||
queryMail();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchAddresData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="margin-top: 10px;">
|
||||
<n-input-group>
|
||||
<n-select v-model:value="addressFilter" :options="addressFilterOptions" clearable
|
||||
:placeholder="t('addressQueryTip')" />
|
||||
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" @keydown.enter="queryMail" />
|
||||
<n-button @click="queryMail" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<div style="margin-top: 10px;"></div>
|
||||
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="true" :fetchMailData="fetchMailData"
|
||||
:deleteMail="deleteMail" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -232,7 +232,7 @@ const renamePasskey = async () => {
|
||||
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
||||
<p>{{ t('logoutConfirm') }}</p>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="logout" size="small" tertiary type="primary">
|
||||
<n-button :loading="loading" @click="logout" size="small" tertiary type="warning">
|
||||
{{ t('logout') }}
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,8 @@ const API_PATHS = [
|
||||
"/open_api/",
|
||||
"/user_api/",
|
||||
"/admin/",
|
||||
"/telegram/"
|
||||
"/telegram/",
|
||||
"/external/",
|
||||
];
|
||||
|
||||
export async function onRequest(context) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temp-email-pages",
|
||||
"version": "0.8.6",
|
||||
"version": "0.10.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -11,6 +11,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"wrangler": "^3.104.0"
|
||||
}
|
||||
"wrangler": "^4.19.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
cd frontend/
|
||||
pnpm up
|
||||
pnpm add -D wrangler@latest
|
||||
cd ..
|
||||
|
||||
cd worker/
|
||||
pnpm up
|
||||
pnpm add -D wrangler@latest
|
||||
cd ..
|
||||
|
||||
cd pages/
|
||||
pnpm up
|
||||
pnpm add -D wrangler@latest
|
||||
cd ..
|
||||
|
||||
cd vitepress-docs/
|
||||
pnpm up
|
||||
pnpm up --latest
|
||||
pnpm add -D wrangler@latest
|
||||
cd ..
|
||||
|
||||
@@ -135,7 +135,7 @@ class SimpleMailbox:
|
||||
self.message_count = res.json()["count"]
|
||||
return [
|
||||
(start + uid, SimpleMessage(start + uid, parse_email(item["raw"])))
|
||||
for uid, item in enumerate(reversed(res.json()["results"]))
|
||||
for uid, item in enumerate(res.json()["results"])
|
||||
]
|
||||
|
||||
def fetchSENT(self, messages):
|
||||
|
||||
@@ -48,17 +48,14 @@ def generate_email_model(item: dict) -> EmailModel:
|
||||
email_json = json.loads(item["raw"])
|
||||
message = MIMEMultipart()
|
||||
if email_json.get("version") == "v2":
|
||||
message['From'] = f"{email_json["from_name"]} <{item["address"]}>" if email_json.get(
|
||||
"from_name") else item["address"]
|
||||
message['To'] = f"{email_json["to_name"]} <{email_json["to_mail"]}>" if email_json.get(
|
||||
"to_name") else email_json["to_mail"]
|
||||
message['From'] = f'{email_json["from_name"]} <{item["address"]}>' if email_json.get("from_name") else item["address"]
|
||||
message['To'] = f'{email_json["to_name"]} <{email_json["to_mail"]}>' if email_json.get("to_name") else email_json["to_mail"]
|
||||
message.attach(MIMEText(
|
||||
email_json["content"],
|
||||
"html" if email_json.get("is_html") else "plain"
|
||||
))
|
||||
else:
|
||||
message['From'] = f"{email_json["from"]['name']} <{
|
||||
email_json["from"]['email']}>"
|
||||
message['From'] = f'{email_json["from"]["name"]} <{email_json["from"]["email"]}>'
|
||||
message['To'] = ", ".join(
|
||||
[f"{to['name']} <{to['email']}>" for to in email_json["personalizations"][0]["to"]])
|
||||
message.attach(MIMEText(
|
||||
|
||||
@@ -27,7 +27,6 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
themeConfig: {
|
||||
|
||||
logo: { src: '/logo.png', width: 24, height: 24 },
|
||||
search: { provider: 'local' },
|
||||
socialLinks: [
|
||||
|
||||
@@ -6,6 +6,7 @@ export const en = defineConfig({
|
||||
description: 'CloudFlare Free sending and receiving of temporary domain name mailboxes',
|
||||
|
||||
themeConfig: {
|
||||
outline: 'deep',
|
||||
nav: nav(),
|
||||
|
||||
editLink: {
|
||||
|
||||
@@ -28,6 +28,7 @@ export const zh = defineConfig({
|
||||
},
|
||||
|
||||
outline: {
|
||||
level: 'deep',
|
||||
label: '页面导航'
|
||||
},
|
||||
|
||||
@@ -121,6 +122,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
text: '通过 Github Actions 部署',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Github Actions 部署准备', link: 'actions/pre-requisite' },
|
||||
{ text: 'D1 数据库', link: 'actions/d1' },
|
||||
{ text: 'Github Actions 配置', link: 'actions/github-action' },
|
||||
{ text: '配置邮件转发', link: 'email-routing.md' },
|
||||
@@ -151,6 +153,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
{ text: '新建邮箱地址 API', link: 'feature/new-address-api' },
|
||||
{ text: 'Oauth2 第三方登录', link: 'feature/user-oauth2' },
|
||||
{ text: '配置其他worker增强', link: 'feature/another-worker-enhanced' },
|
||||
{ text: '给网页增加 Google Ads', link: 'feature/google-ads.md' },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -74,12 +74,15 @@ compatibility_flags = [ "nodejs_compat" ]
|
||||
# ]
|
||||
|
||||
[vars]
|
||||
# DEFAULT_LANG = "zh"
|
||||
# TITLE = "Custom Title" # The title of the site
|
||||
PREFIX = "tmp" # The mailbox name prefix to be processed
|
||||
# (min, max) length of the adderss, if not set, the default is (1, 30)
|
||||
# MIN_ADDRESS_LEN = 1
|
||||
# MAX_ADDRESS_LEN = 30
|
||||
# ANNOUNCEMENT = "Custom Announcement"
|
||||
# always show ANNOUNCEMENT even no changes
|
||||
# ALWAYS_SHOW_ANNOUNCEMENT = true
|
||||
# address check REGEX, if not set, will not check
|
||||
# ADDRESS_CHECK_REGEX = "^(?!.*admin).*"
|
||||
# address name replace REGEX, if not set, the default is [^a-z0-9]
|
||||
@@ -120,7 +123,8 @@ ENABLE_AUTO_REPLY = false
|
||||
# DISABLE_SHOW_GITHUB = true # Disable Show GitHub link
|
||||
# default send balance, if not set, it will be 0
|
||||
# DEFAULT_SEND_BALANCE = 1
|
||||
# NO_LIMIT_SEND_ROLE = "vip" # the role which can send emails without limit
|
||||
# the role which can send emails without limit, multiple roles can be separated by ,
|
||||
# NO_LIMIT_SEND_ROLE = "vip"
|
||||
# Turnstile verification configuration
|
||||
# CF_TURNSTILE_SITE_KEY = ""
|
||||
# CF_TURNSTILE_SECRET_KEY = ""
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
# 初始化/更新 D1 数据库
|
||||
|
||||
## 创建数据库
|
||||
|
||||
打开 cloudflare 控制台,选择 `Workers & Pages` -> `D1` -> `Create Database`,点击创建数据库
|
||||
|
||||

|
||||
|
||||
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库,并获取到数据库的 `名称` 和 `数据库 ID`
|
||||
|
||||
## 初始化数据库
|
||||
|
||||
在部署完成后,在 admin 页面的 `快速设置` -> `数据库` 中,点击 `初始化数据库` 按钮来初始化数据库
|
||||
|
||||
## 更新数据库 schema
|
||||
|
||||
参考 [命令行更新 d1](/zh/guide/cli/d1) 或者 [用户界面更新 d1](/zh/guide/ui/d1)
|
||||
|
||||
@@ -3,27 +3,56 @@
|
||||
::: warning 注意
|
||||
目前只支持 worker 和 pages 的部署。
|
||||
有问题请通过 `Github Issues` 反馈,感谢。
|
||||
|
||||
`worker.dev` 域名在中国无法访问,请自定义域名
|
||||
:::
|
||||
|
||||
## 部署步骤
|
||||
|
||||
1. 在 GitHub fork 本仓库
|
||||
### Fork 仓库并启用 Actions
|
||||
|
||||
2. 打开仓库的 `Actions` 页面,找到 `Deploy Backend Production` 和 `Deploy Frontend`,点击 `enable workflow` 启用 `workflow`
|
||||
- 在 GitHub fork 本仓库
|
||||
- 打开仓库的 `Actions` 页面
|
||||
- 找到 `Deploy Backend` 点击 `enable workflow` 启用 `workflow`
|
||||
- 如果需要前后端分离部署, 找到`Deploy Frontend` 点击 `enable workflow` 启用 `workflow`
|
||||
|
||||
3. 然后在仓库页面 `Settings` -> `Secrets and variables` -> `Actions` -> `Repository secrets`, 添加以下 `secrets`:
|
||||
### 配置 Secrets
|
||||
|
||||
- `CLOUDFLARE_ACCOUNT_ID`: Cloudflare 账户 ID, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#cloudflare-account-id)
|
||||
- `CLOUDFLARE_API_TOKEN`: Cloudflare API Token, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#api-token)
|
||||
- `BACKEND_TOML`: 后端配置文件,[参考此处](/zh/guide/cli/worker.html#修改-wrangler-toml-配置文件)
|
||||
- `FRONTEND_ENV`: 前端配置文件,请复制 `frontend/.env.example` 的内容,[并参考此处修改](/zh/guide/cli/pages.html)
|
||||
- `FRONTEND_NAME`: 你在 Cloudflare Pages 创建的项目名称,可通过 [用户界面](https://temp-mail-docs.awsl.uk/zh/guide/ui/pages.html) 或者 [命令行](https://temp-mail-docs.awsl.uk/zh/guide/cli/pages.html) 创建
|
||||
- `FRONTEND_BRANCH`: (可选) pages 部署的分支,可不配置,默认 `production`
|
||||
- `TG_FRONTEND_NAME`: (可选) 你在 Cloudflare Pages 创建的项目名称,同 `FRONTEND_NAME`,如果需要 Telegram Mini App 功能,请填写
|
||||
- `DEBUG_MODE`: (可选) 是否开启调试模式,配置为 `true` 开启, 默认 worker 部署日志不会输出到 Github Actions 页面,开启后会输出
|
||||
- `BACKEND_USE_MAIL_WASM_PARSER`: (可选) 是否使用 wasm 解析邮件,配置为 `true` 开启, 功能参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker)
|
||||
然后在仓库页面 `Settings` -> `Secrets and variables` -> `Actions` -> `Repository secrets`, 添加以下 `secrets`:
|
||||
|
||||
4. 打开仓库的 `Actions` 页面,找到 `Deploy Backend Production` 和 `Deploy Frontend`,点击 `Run workflow` 选择分支手动部署
|
||||
- 公共 `secrets`
|
||||
|
||||
| 名称 | 说明 |
|
||||
| ----------------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare 账户 ID, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#cloudflare-account-id) |
|
||||
| `CLOUDFLARE_API_TOKEN` | Cloudflare API Token, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#api-token) |
|
||||
|
||||
- worker 后端 `secrets`
|
||||
|
||||
| 名称 | 说明 |
|
||||
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `BACKEND_TOML` | 后端配置文件,[参考此处](/zh/guide/cli/worker.html#修改-wrangler-toml-配置文件) |
|
||||
| `DEBUG_MODE` | (可选) 是否开启调试模式,配置为 `true` 开启, 默认 worker 部署日志不会输出到 Github Actions 页面,开启后会输出 |
|
||||
| `BACKEND_USE_MAIL_WASM_PARSER` | (可选) 是否使用 wasm 解析邮件,配置为 `true` 开启, 功能参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker) |
|
||||
| `USE_WORKER_ASSETS` | (可选) 部署带有前端资源的 Worker, 配置为 `true` 开启 |
|
||||
|
||||
- pages 前端 `secrets`
|
||||
|
||||
> [!warning] 注意
|
||||
> 如果选择部署带有前端资源的 Worker, 则无须配置这些 `secrets`
|
||||
|
||||
| 名称 | 说明 |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `FRONTEND_ENV` | 前端配置文件,请复制 `frontend/.env.example` 的内容,[并参考此处修改](/zh/guide/cli/pages.html) |
|
||||
| `FRONTEND_NAME` | 你在 Cloudflare Pages 创建的项目名称,可通过 [用户界面](https://temp-mail-docs.awsl.uk/zh/guide/ui/pages.html) 或者 [命令行](https://temp-mail-docs.awsl.uk/zh/guide/cli/pages.html) 创建 |
|
||||
| `FRONTEND_BRANCH` | (可选) pages 部署的分支,可不配置,默认 `production` |
|
||||
| `TG_FRONTEND_NAME` | (可选) 你在 Cloudflare Pages 创建的项目名称,同 `FRONTEND_NAME`,如果需要 Telegram Mini App 功能,请填写 |
|
||||
|
||||
### 部署
|
||||
|
||||
- 打开仓库的 `Actions` 页面
|
||||
- 找到 `Deploy Backend` 点击 `Run workflow` 选择分支手动部署
|
||||
- 如果需要前后端分离部署, 找到 `Deploy Frontend`, 点击 `Run workflow` 选择分支手动部署
|
||||
|
||||
## 如何配置自动更新
|
||||
|
||||
|
||||
10
vitepress-docs/docs/zh/guide/actions/pre-requisite.md
Normal file
10
vitepress-docs/docs/zh/guide/actions/pre-requisite.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Gihub Actions 部署准备
|
||||
|
||||
## GitHub 账户
|
||||
|
||||
- 需要一个 GitHub 账户
|
||||
- 良好的网络环境
|
||||
|
||||
## Fork 仓库
|
||||
|
||||
- 在 GitHub fork [本仓库](https://github.com/dreamhunter2333/cloudflare_temp_email.git)
|
||||
@@ -1,8 +1,11 @@
|
||||
# Cloudflare Pages 前端
|
||||
|
||||
::: warning
|
||||
下面两种方式选择一种即可
|
||||
:::
|
||||
> [!warning] 注意
|
||||
> 下面几种方式选择一种即可
|
||||
|
||||
## 部署带有前端资源的 Worker
|
||||
|
||||
参考 [部署 Worker](/zh/guide/cli/worker#部署带有前端页面的-worker-可选)
|
||||
|
||||
## 前后端分离部署
|
||||
|
||||
@@ -22,7 +25,7 @@ cp .env.example .env.prod
|
||||
|
||||
```bash
|
||||
pnpm build --emptyOutDir
|
||||
# 根据提示创建 pages
|
||||
# 第一次部署会提示创建项目, production 分支请填写 production
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
# Cloudflare workers 后端
|
||||
# Cloudflare Worker 后端
|
||||
|
||||
> [!warning] 注意
|
||||
> `worker.dev` 域名在中国无法访问,请自定义域名
|
||||
|
||||
## 初始化项目
|
||||
|
||||
@@ -36,6 +39,12 @@ compatibility_flags = [ "nodejs_compat" ]
|
||||
# { pattern = "temp-email-api.xxxxx.xyz", custom_domain = true },
|
||||
# ]
|
||||
|
||||
# 如果你想要部署带有前端资源的 worker, 你需要添加 assets 配置
|
||||
# [assets]
|
||||
# directory = "../frontend/dist/"
|
||||
# binding = "ASSETS"
|
||||
# run_worker_first = true
|
||||
|
||||
# 如果你想要使用定时任务清理邮件,取消下面的注释,并修改 cron 表达式
|
||||
# [triggers]
|
||||
# crons = [ "0 0 * * *" ]
|
||||
@@ -86,6 +95,29 @@ database_id = "xxx" # D1 数据库 ID
|
||||
# service = "auth-inbox"
|
||||
```
|
||||
|
||||
## 部署带有前端页面的 worker(可选)
|
||||
|
||||
> [!NOTE]
|
||||
> 如果不需要 [带有前端页面的 worker],可以跳过此步骤
|
||||
> 参考之后部署前端文档,可以进行前后端分离部署
|
||||
|
||||
确认已构建前端资源到 `frontend/dist` 目录
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install --no-frozen-lockfile
|
||||
pnpm build:pages
|
||||
```
|
||||
|
||||
`worker` 目录下的 `wrangler.toml` 文件中添加下面的配置
|
||||
|
||||
```toml
|
||||
[assets]
|
||||
directory = "../frontend/dist/"
|
||||
binding = "ASSETS"
|
||||
run_worker_first = true
|
||||
```
|
||||
|
||||
## Telegram Bot 配置
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
## 通用
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
| ---------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| 使用 Cloudflare Workers 给已认证的邮箱发送邮件 | 使用 cf 的 API 进行发送,只支持绑定到 CF 上的收件地址,即 CF EMAIL 转发目的地址 |
|
||||
| 绑定多个域名 | 每个域名都需要设置 email 转发到 worker |
|
||||
| 问题 | 解决方案 |
|
||||
| -------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| 使用 Cloudflare Workers 给已认证的转发邮箱发送邮件 | 使用 cf 的 API 进行发送,只支持绑定到 CF 上的收件地址,即 CF EMAIL 转发目的地址 |
|
||||
| 绑定多个域名 | 每个域名都需要设置 email 转发到 worker |
|
||||
|
||||
## worker 相关
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
|
||||
# 配置发送邮件
|
||||
|
||||
## 使用 Cloudflare Workers 给已认证的邮箱发送邮件
|
||||
::: warning 注意
|
||||
三种方式可以同时配置,发送邮件时会优先使用 `resend`,如果没有配置 `resend`,则会使用 `smtp`.
|
||||
|
||||
admin 后台 账号配置 `已验证地址列表(可通过 cf 内部 api 发送邮件)`
|
||||
如果配置了 Cloudflare 已认证的转发邮箱地址,会优先使用 cf 内部 API 发送邮件
|
||||
:::
|
||||
|
||||
## 使用 resend 发送邮件
|
||||
|
||||
@@ -30,3 +32,53 @@ wrangler secret put RESEND_TOKEN
|
||||
wrangler secret put RESEND_TOKEN_XXX_COM
|
||||
wrangler secret put RESEND_TOKEN_DREAMHUNTER2333_XYZ
|
||||
```
|
||||
|
||||
## 使用 SMTP 发送邮件
|
||||
|
||||
`SMTP_CONFIG` 的格式如下,key 为域名,value 为 SMTP 配置,SMTP 配置格式详情可以参考 [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md)
|
||||
|
||||
```json
|
||||
{
|
||||
"awsl.uk": {
|
||||
"host": "smtp.xxx.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"authType": [
|
||||
"plain",
|
||||
"login"
|
||||
],
|
||||
"credentials": {
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后执行下面的命令,将 `SMTP_CONFIG` 添加到 secrets 中
|
||||
|
||||
> [!NOTE]
|
||||
> 如果你觉得麻烦,也可以直接明文放在 `wrangler.toml` 中 `[vars]` 下面,但是不推荐这样做
|
||||
|
||||
如果你是通过 UI 部署的,可以在 Cloudflare 的 UI 界面中添加到 `Variables and Secrets` 下面
|
||||
|
||||
```bash
|
||||
# 切换到 worker 目录
|
||||
cd worker
|
||||
wrangler secret put SMTP_CONFIG
|
||||
```
|
||||
|
||||
## 给 Cloudflare 上已认证的转发邮箱发送邮件
|
||||
|
||||
仅支持 CLI 部署时使用,在 `wrangler.toml` 中添加 `send_email` 配置
|
||||
|
||||
发送的目的邮箱地址必须是 Cloudflare 上已认证的邮箱地址,局限性较大,如果需要发送邮件给其他邮箱,可以使用 `resend` 或者 `smtp` 发送邮件
|
||||
|
||||
```toml
|
||||
# 通过 Cloudflare 发送邮件
|
||||
send_email = [
|
||||
{ name = "SEND_MAIL" },
|
||||
]
|
||||
```
|
||||
|
||||
admin 后台 账号配置 `已验证地址列表(可通过 cf 内部 api 发送邮件)`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 搭建 SMTP IMAP 代理服务
|
||||
|
||||
::: warning
|
||||
::: warning 注意
|
||||
如果你使用了 `resend`, 可直接使用 `resend` 的 `SMTP` 服务,不需要使用此服务
|
||||
:::
|
||||
|
||||
|
||||
29
vitepress-docs/docs/zh/guide/feature/google-ads.md
Normal file
29
vitepress-docs/docs/zh/guide/feature/google-ads.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 给网页增加 Google Ads
|
||||
|
||||
## 命令行部署
|
||||
|
||||
修改 `.env.prod` 文件
|
||||
|
||||
增加下列两个变量, 具体的值请参考 [Google AdSense](https://www.google.com/adsense/start/) 的说明
|
||||
|
||||
```txt
|
||||
VITE_GOOGLE_AD_CLIENT=ca-pub-123456
|
||||
VITE_GOOGLE_AD_SLOT=123456
|
||||
```
|
||||
|
||||
然后执行下列命令, 重新部署 pages 即可.
|
||||
|
||||
```bash
|
||||
pnpm build --emptyOutDir
|
||||
# 第一次部署会提示创建项目, production 分支请填写 production
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
## GitHub Action 部署
|
||||
|
||||
修改 `FRONTEND_ENV`, 增加下列两个变量, 具体的值请参考 [Google AdSense](https://www.google.com/adsense/start/) 的说明, 重新部署 pages 即可.
|
||||
|
||||
```txt
|
||||
VITE_GOOGLE_AD_CLIENT=ca-pub-123456
|
||||
VITE_GOOGLE_AD_SLOT=123456
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
# 查看邮件 API
|
||||
|
||||
## 通过 HTTP API 查看邮件
|
||||
## 通过 邮件 API 查看邮件
|
||||
|
||||
这是一个 `python` 的例子,使用 `requests` 库查看邮件。
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
limit = 10
|
||||
offset = 0
|
||||
res = requests.get(
|
||||
f"http://localhost:8787/api/mails?limit={limit}&offset={offset}",
|
||||
f"https://<你的worker地址>/api/mails?limit={limit}&offset={offset}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {你的JWT密码}",
|
||||
# "x-custom-auth": "<你的网站密码>", # 如果启用了自定义密码
|
||||
@@ -16,3 +16,51 @@ res = requests.get(
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## admin 邮件 API
|
||||
|
||||
支持 `address` filter 和 `keyword` filter
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
url = "https://<你的worker地址>/admin/mails"
|
||||
|
||||
querystring = {
|
||||
"limit":"20",
|
||||
"offset":"0",
|
||||
# adress 和 keyword 为可选参数
|
||||
"address":"xxxx@awsl.uk",
|
||||
"keyword":"xxxx"
|
||||
}
|
||||
|
||||
headers = {"x-admin-auth": "<你的Admin密码>"}
|
||||
|
||||
response = requests.get(url, headers=headers, params=querystring)
|
||||
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
## user 邮件 API
|
||||
|
||||
支持 `address` filter 和 `keyword` filter
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
url = "https://<你的worker地址>/user_api/mails"
|
||||
|
||||
querystring = {
|
||||
"limit":"20",
|
||||
"offset":"0",
|
||||
# adress 和 keyword 为可选参数
|
||||
"address":"xxxx@awsl.uk",
|
||||
"keyword":"xxxx"
|
||||
}
|
||||
|
||||
headers = {"x-admin-auth": "<你的Admin密码>"}
|
||||
|
||||
response = requests.get(url, headers=headers, params=querystring)
|
||||
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
@@ -3,7 +3,25 @@
|
||||
> [!NOTE]
|
||||
> 如果你使用了 webhook 转发,或者 telegram bot 接受邮件,但是邮件内容是乱码,或者无法解析,你对解析的需要更高的要求,可以使用这个功能。
|
||||
|
||||
## 修改代码
|
||||
## UI 部署
|
||||
|
||||
1. 下载 [worker-with-wasm-mail-parser.zip](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/worker-with-wasm-mail-parser.zip)
|
||||
|
||||
2. 回到 `Overview`,找到刚刚创建的 worker,点击 `Edit Code`, 删除原来的文件,上传 `worker.js` 和 `wasm` 后缀的文件, 点击 `Deploy`
|
||||
|
||||
> [!NOTE]
|
||||
> 上传需要先点击左侧菜单的 Explorer,
|
||||
> 在文件列表的窗口里点击鼠标右键,在右键菜单里找到 `Upload`,
|
||||
> 请参考下面的截图
|
||||
>
|
||||
> 参考: [issues156](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/156#issuecomment-2079453822)
|
||||
|
||||

|
||||

|
||||
|
||||
## CLI 部署
|
||||
|
||||
### 修改代码
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
@@ -56,7 +74,7 @@ export const commonParseMail = async (raw_mail: string | undefined | null): Prom
|
||||
}
|
||||
```
|
||||
|
||||
## 部署
|
||||
### 部署
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OAuth2 第三方登录
|
||||
|
||||
> [!WARNING]
|
||||
> [!WARNING] 注意
|
||||
> 第三方登录会自动使用用户邮箱注册账号(邮箱相同将视为同一账号)
|
||||
>
|
||||
> 此账号和注册的账号相同, 也可以通过忘记密码设置密码
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
- [通过命令行部署](/zh/guide/cli/pre-requisite)
|
||||
- [通过用户界面部署](/zh/guide/ui/d1)
|
||||
- [通过Github Actions 部署](/zh/guide/actions/github-action)
|
||||
- [通过Github Actions 部署](/zh/guide/actions/pre-requisite)
|
||||
|
||||
### 也可以参考网友提供的详细的小白教程
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 初始化/更新 D1 数据库
|
||||
|
||||
## 初始化数据库
|
||||
## 创建数据库
|
||||
|
||||
打开 cloudflare 控制台,选择 `Workers & Pages` -> `D1` -> `Create Database`,点击创建数据库
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
|
||||
|
||||
## 初始化数据库
|
||||
|
||||
你也可以跳过初始化数据库,在部署完成后,在 admin 页面的 `快速设置` -> `数据库` 中,点击 `初始化数据库` 按钮来初始化数据库
|
||||
|
||||
::: warning 注意
|
||||
下面输入的是 `db/schema.sql` 的内容
|
||||
:::
|
||||
|
||||
@@ -54,6 +54,9 @@ const generate = async () => {
|
||||
- 此处 worker 域名为后端 api 的域名,比如我部署在 `https://temp-email-api.awsl.uk`,则填写 `https://temp-email-api.awsl.uk`
|
||||
- 如果你的域名是 `https://temp-email-api.xxx.workers.dev`,则填写 `https://temp-email-api.xxx.workers.dev`
|
||||
|
||||
> [!warning] 注意
|
||||
> `worker.dev` 域名在中国无法访问,请自定义域名
|
||||
|
||||
<div :class="$style.container">
|
||||
<input :class="$style.input" type="text" v-model="domain" placeholder="请输入地址"></input>
|
||||
<button :class="$style.button" @click="generate">生成</button>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Cloudflare workers 后端
|
||||
|
||||
> [!warning] 注意
|
||||
> `worker.dev` 域名在中国无法访问,请自定义域名
|
||||
|
||||
1. 点击 `Workers & Pages` -> `Overview` -> `Create Application`
|
||||
|
||||

|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
| `ADMIN_USER_ROLE` | 文本 | admin 角色配置, 如果用户角色等于 ADMIN_USER_ROLE 则可以访问 admin 控制台 | `admin` |
|
||||
| `USER_ROLES` | JSON | - | 见下方 |
|
||||
| `DISABLE_ANONYMOUS_USER_CREATE_EMAIL` | 文本/JSON | 禁用匿名用户创建邮箱,如果设置为 true,则用户只能在登录后创建邮箱地址 | `true` |
|
||||
| `NO_LIMIT_SEND_ROLE` | 文本 | 可以无限发送邮件的角色 | `vip` |
|
||||
| `NO_LIMIT_SEND_ROLE` | 文本 | 可以无限发送邮件的角色, 多个角色使用逗号分割 `vip,admin` | `vip` |
|
||||
|
||||
> [!NOTE] USER_ROLES 用户角色配置说明
|
||||
>
|
||||
@@ -88,15 +88,17 @@
|
||||
|
||||
## 网页相关变量
|
||||
|
||||
| 变量名 | 类型 | 说明 | 示例 |
|
||||
| ------------------------- | ----------- | ------------------------------------------------ | --------------------- |
|
||||
| `TITLE` | 文本 | 自定义前端页面网站标题,支持 html | `Custom Title` |
|
||||
| `ANNOUNCEMENT` | 文本 | 自定义前端页面公告,支持 html | `Custom Announcement` |
|
||||
| `COPYRIGHT` | 文本 | 自定义前端界面页脚文本,支持 html | `Dream Hunter` |
|
||||
| `ADMIN_CONTACT` | 文本 | admin 联系方式,可配置任意字符串, 不配置则不显示 | `xxx@gmail.com` |
|
||||
| `DISABLE_SHOW_GITHUB` | 文本/JSON | 是否显示 GitHub 链接 | `true` |
|
||||
| `CF_TURNSTILE_SITE_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` |
|
||||
| `CF_TURNSTILE_SECRET_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` |
|
||||
| 变量名 | 类型 | 说明 | 示例 |
|
||||
| -------------------------- | ----------- | ------------------------------------------------ | --------------------- |
|
||||
| `DEFAULT_LANG` | 文本 | Worker 错误信息默认语言, zh/en | `zh` |
|
||||
| `TITLE` | 文本 | 自定义前端页面网站标题,支持 html | `Custom Title` |
|
||||
| `ANNOUNCEMENT` | 文本 | 自定义前端页面公告,支持 html | `Custom Announcement` |
|
||||
| `ALWAYS_SHOW_ANNOUNCEMENT` | 文本/JSON | 是否总是显示公告(即使无更改), 默认 `false` | `true` |
|
||||
| `COPYRIGHT` | 文本 | 自定义前端界面页脚文本,支持 html | `Dream Hunter` |
|
||||
| `ADMIN_CONTACT` | 文本 | admin 联系方式,可配置任意字符串, 不配置则不显示 | `xxx@gmail.com` |
|
||||
| `DISABLE_SHOW_GITHUB` | 文本/JSON | 是否显示 GitHub 链接 | `true` |
|
||||
| `CF_TURNSTILE_SITE_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` |
|
||||
| `CF_TURNSTILE_SECRET_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` |
|
||||
|
||||
## Telegram Bot 相关变量
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "temp-mail-docs",
|
||||
"private": true,
|
||||
"version": "0.8.6",
|
||||
"version": "0.10.0",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.7",
|
||||
"vitepress": "^1.6.2",
|
||||
"wrangler": "^3.104.0"
|
||||
"@types/node": "^22.15.30",
|
||||
"vitepress": "^1.6.3",
|
||||
"wrangler": "^4.19.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vitepress dev docs",
|
||||
@@ -16,5 +16,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"jszip": "^3.10.1"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
1808
vitepress-docs/pnpm-lock.yaml
generated
1808
vitepress-docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "0.8.6",
|
||||
"version": "0.10.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -11,27 +11,31 @@
|
||||
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250121.0",
|
||||
"@cloudflare/workers-types": "^4.20250607.0",
|
||||
"@eslint/js": "9.18.0",
|
||||
"@simplewebauthn/types": "10.0.0",
|
||||
"@types/node": "^22.15.30",
|
||||
"eslint": "9.18.0",
|
||||
"globals": "^15.14.0",
|
||||
"typescript-eslint": "^8.21.0",
|
||||
"wrangler": "^3.104.0"
|
||||
"globals": "^15.15.0",
|
||||
"typescript-eslint": "^8.33.1",
|
||||
"wrangler": "^4.19.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.732.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.732.0",
|
||||
"@aws-sdk/client-s3": "^3.826.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.826.0",
|
||||
"@simplewebauthn/server": "10.0.1",
|
||||
"hono": "^4.6.17",
|
||||
"hono": "^4.7.11",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"mimetext": "^3.0.27",
|
||||
"postal-mime": "^2.4.1",
|
||||
"resend": "^4.1.1",
|
||||
"telegraf": "4.16.3"
|
||||
"postal-mime": "^2.4.3",
|
||||
"resend": "^4.5.2",
|
||||
"telegraf": "4.16.3",
|
||||
"worker-mailer": "^1.1.4"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"telegraf@4.16.3": "patches/telegraf@4.16.3.patch"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
2700
worker/pnpm-lock.yaml
generated
2700
worker/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
38
worker/src/admin_api/admin_mail_api.ts
Normal file
38
worker/src/admin_api/admin_mail_api.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Context } from "hono";
|
||||
import { handleListQuery } from "../common";
|
||||
|
||||
export default {
|
||||
getMails: async (c: Context<HonoCustomType>) => {
|
||||
const { address, limit, offset, keyword } = c.req.query();
|
||||
const addressQuery = address ? `address = ?` : "";
|
||||
const addressParams = address ? [address] : [];
|
||||
const keywordQuery = keyword ? `raw like ?` : "";
|
||||
const keywordParams = keyword ? [`%${keyword}%`] : [];
|
||||
const filterQuerys = [addressQuery, keywordQuery].filter((item) => item).join(" and ");
|
||||
const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : "";
|
||||
const filterParams = [...addressParams, ...keywordParams]
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails ${finalQuery}`,
|
||||
`SELECT count(*) as count FROM raw_mails ${finalQuery}`,
|
||||
filterParams, limit, offset
|
||||
);
|
||||
},
|
||||
getUnknowMails: async (c: Context<HonoCustomType>) => {
|
||||
const { limit, offset } = c.req.query();
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails where address NOT IN (select name from address) `,
|
||||
`SELECT count(*) as count FROM raw_mails`
|
||||
+ ` where address NOT IN (select name from address) `,
|
||||
[], limit, offset
|
||||
);
|
||||
},
|
||||
deleteMail: async (c: Context<HonoCustomType>) => {
|
||||
const { id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE id = ? `
|
||||
).bind(id).run();
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils';
|
||||
import { UserSettings, GeoData, UserInfo } from "../models";
|
||||
import { handleListQuery } from '../common'
|
||||
import { HonoCustomType } from '../types';
|
||||
import UserBindAddressModule from '../user_api/bind_address';
|
||||
import i18n from '../i18n';
|
||||
|
||||
export default {
|
||||
getSetting: async (c: Context<HonoCustomType>) => {
|
||||
@@ -89,7 +90,8 @@ export default {
|
||||
},
|
||||
deleteUser: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id } = c.req.param();
|
||||
if (!user_id) return c.text("Invalid user_id", 400);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!user_id) return c.text(msgs.UserNotFoundMsg, 400);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM users WHERE id = ?`
|
||||
).bind(user_id).run();
|
||||
@@ -104,7 +106,8 @@ export default {
|
||||
resetPassword: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id } = c.req.param();
|
||||
const { password } = await c.req.json();
|
||||
if (!user_id) return c.text("Invalid user_id", 400);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!user_id) return c.text(msgs.UserNotFoundMsg, 400);
|
||||
try {
|
||||
checkUserPassword(password);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
@@ -143,5 +146,24 @@ export default {
|
||||
return c.text("Failed to update user roles", 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
}
|
||||
},
|
||||
bindAddress: async (c: Context<HonoCustomType>) => {
|
||||
const {
|
||||
user_email, address, user_id, address_id
|
||||
} = await c.req.json();
|
||||
const db_user_id = user_id ?? await c.env.DB.prepare(
|
||||
`SELECT id FROM users WHERE user_email = ?`
|
||||
).bind(user_email).first<number | undefined | null>("id");
|
||||
const db_address_id = address_id ?? await c.env.DB.prepare(
|
||||
`SELECT id FROM address WHERE name = ?`
|
||||
).bind(address).first<number | undefined | null>("id");
|
||||
return await UserBindAddressModule.bindByID(c, db_user_id, db_address_id);
|
||||
},
|
||||
getBindedAddresses: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id } = c.req.param();
|
||||
const results = await UserBindAddressModule.getBindedAddressesById(c, user_id);
|
||||
return c.json({
|
||||
results: results,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { cleanup } from '../common';
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting } from '../utils';
|
||||
import { CleanupSettings } from '../models';
|
||||
import { HonoCustomType } from '../types';
|
||||
|
||||
export default {
|
||||
cleanup: async (c: Context<HonoCustomType>) => {
|
||||
|
||||
156
worker/src/admin_api/db_api.ts
Normal file
156
worker/src/admin_api/db_api.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { Context } from "hono";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import utils from "../utils";
|
||||
|
||||
const DB_INIT_QUERIES = `
|
||||
CREATE TABLE IF NOT EXISTS raw_mails (
|
||||
id INTEGER PRIMARY KEY,
|
||||
message_id TEXT,
|
||||
source TEXT,
|
||||
address TEXT,
|
||||
raw TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS address (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
||||
id INTEGER PRIMARY KEY,
|
||||
source_prefix TEXT,
|
||||
name TEXT,
|
||||
address TEXT UNIQUE,
|
||||
subject TEXT,
|
||||
message TEXT,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_auto_reply_mails_address ON auto_reply_mails(address);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS address_sender (
|
||||
id INTEGER PRIMARY KEY,
|
||||
address TEXT UNIQUE,
|
||||
balance INTEGER DEFAULT 0,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_address_sender_address ON address_sender(address);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sendbox (
|
||||
id INTEGER PRIMARY KEY,
|
||||
address TEXT,
|
||||
raw TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
user_info TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_user_email ON users(user_email);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users_address (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER,
|
||||
address_id INTEGER UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER UNIQUE NOT NULL,
|
||||
role_text TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_passkeys (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
passkey_name TEXT NOT NULL,
|
||||
passkey_id TEXT NOT NULL,
|
||||
passkey TEXT NOT NULL,
|
||||
counter INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_passkeys_user_id ON user_passkeys(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_passkeys_user_id_passkey_id ON user_passkeys(user_id, passkey_id);
|
||||
`
|
||||
|
||||
export default {
|
||||
initialize: async (c: Context<HonoCustomType>) => {
|
||||
// remove all \r and \n characters from the query string
|
||||
// split by ; and join with a ;\n
|
||||
const query = DB_INIT_QUERIES.replace(/[\r\n]/g, "")
|
||||
.split(";")
|
||||
.map((query) => query.trim())
|
||||
.join(";\n");
|
||||
await c.env.DB.exec(query);
|
||||
|
||||
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||
if (version) {
|
||||
return c.json({ message: "Database already initialized" });
|
||||
}
|
||||
await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
|
||||
return c.json({ message: "Database initialized" });
|
||||
},
|
||||
migrate: async (c: Context<HonoCustomType>) => {
|
||||
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||
if (version != CONSTANTS.DB_VERSION) {
|
||||
// TODO: Perform migration logic here
|
||||
|
||||
// Update the version in the settings table
|
||||
await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
|
||||
return c.json({
|
||||
success: true,
|
||||
message: "Database migrated"
|
||||
});
|
||||
}
|
||||
return c.json({
|
||||
success: true,
|
||||
message: "Database does not need migration"
|
||||
});
|
||||
},
|
||||
getVersion: async (c: Context<HonoCustomType>) => {
|
||||
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||
return c.json({
|
||||
need_initialization: !version,
|
||||
need_migration: version && version != CONSTANTS.DB_VERSION,
|
||||
current_db_version: version,
|
||||
code_db_version: CONSTANTS.DB_VERSION
|
||||
});
|
||||
},
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { HonoCustomType } from '../types'
|
||||
import i18n from '../i18n'
|
||||
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles } from '../utils'
|
||||
import { newAddress, handleListQuery } from '../common'
|
||||
import { CONSTANTS } from '../constants'
|
||||
@@ -11,7 +11,9 @@ import webhook_settings from './webhook_settings'
|
||||
import mail_webhook_settings from './mail_webhook_settings'
|
||||
import oauth2_settings from './oauth2_settings'
|
||||
import worker_config from './worker_config'
|
||||
import admin_mail_api from './admin_mail_api'
|
||||
import { sendMailbyAdmin } from './send_mail'
|
||||
import db_api from './db_api'
|
||||
|
||||
export const api = new Hono<HonoCustomType>()
|
||||
|
||||
@@ -40,6 +42,8 @@ api.get('/admin/address', async (c) => {
|
||||
|
||||
api.post('/admin/new_address', async (c) => {
|
||||
const { name, domain, enablePrefix } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (!name) {
|
||||
return c.text("Please provide a name", 400)
|
||||
}
|
||||
@@ -53,7 +57,7 @@ api.post('/admin/new_address', async (c) => {
|
||||
});
|
||||
return c.json(res);
|
||||
} catch (e) {
|
||||
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
||||
return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -98,54 +102,10 @@ api.get('/admin/show_password/:id', async (c) => {
|
||||
})
|
||||
})
|
||||
|
||||
api.get('/admin/mails', async (c) => {
|
||||
const { address, limit, offset, keyword } = c.req.query();
|
||||
if (address && keyword) {
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails where address = ? and raw like ? `,
|
||||
`SELECT count(*) as count FROM raw_mails where address = ? and raw like ? `,
|
||||
[address, `%${keyword}%`], limit, offset
|
||||
);
|
||||
} else if (keyword) {
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails where raw like ? `,
|
||||
`SELECT count(*) as count FROM raw_mails where raw like ? `,
|
||||
[`%${keyword}%`], limit, offset
|
||||
);
|
||||
} else if (address) {
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails where address = ? `,
|
||||
`SELECT count(*) as count FROM raw_mails where address = ? `,
|
||||
[address], limit, offset
|
||||
);
|
||||
} else {
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails `,
|
||||
`SELECT count(*) as count FROM raw_mails `,
|
||||
[], limit, offset
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/admin/mails_unknow', async (c) => {
|
||||
const { limit, offset } = c.req.query();
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails where address NOT IN (select name from address) `,
|
||||
`SELECT count(*) as count FROM raw_mails`
|
||||
+ ` where address NOT IN (select name from address) `,
|
||||
[], limit, offset
|
||||
);
|
||||
});
|
||||
|
||||
api.delete('/admin/mails/:id', async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE id = ? `
|
||||
).bind(id).run();
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
})
|
||||
// mail api
|
||||
api.get('/admin/mails', admin_mail_api.getMails);
|
||||
api.get('/admin/mails_unknow', admin_mail_api.getUnknowMails);
|
||||
api.delete('/admin/mails/:id', admin_mail_api.deleteMail)
|
||||
|
||||
api.get('/admin/address_sender', async (c) => {
|
||||
const { address, limit, offset } = c.req.query();
|
||||
@@ -324,6 +284,8 @@ api.post('/admin/users', admin_user_api.createUser)
|
||||
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
|
||||
api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c)))
|
||||
api.post('/admin/user_roles', admin_user_api.updateUserRoles)
|
||||
api.get('/admin/users/bind_address/:user_id', admin_user_api.getBindedAddresses)
|
||||
api.post('/admin/users/bind_address', admin_user_api.bindAddress)
|
||||
|
||||
// user oauth2 settings
|
||||
api.get('/admin/user_oauth2_settings', oauth2_settings.getUserOauth2Settings)
|
||||
@@ -343,3 +305,8 @@ api.get("/admin/worker/configs", worker_config.getConfig);
|
||||
|
||||
// send mail by admin
|
||||
api.post("/admin/send_mail", sendMailbyAdmin);
|
||||
|
||||
// db api
|
||||
api.get('admin/db_version', db_api.getVersion);
|
||||
api.post('admin/db_initialize', db_api.initialize);
|
||||
api.post('admin/db_migration', db_api.migrate);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Context } from "hono";
|
||||
import { HonoCustomType, ParsedEmailContext } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { WebhookSettings } from "../models";
|
||||
import { commonParseMail, sendWebhook } from "../common";
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Context } from 'hono';
|
||||
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { UserOauth2Settings } from "../models";
|
||||
import { HonoCustomType } from '../types';
|
||||
import { getJsonSetting, saveSetting } from '../utils';
|
||||
|
||||
async function getUserOauth2Settings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Context } from "hono";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { sendMail } from "../mails_api/send_mail_api";
|
||||
|
||||
export const sendMailbyAdmin = async (c: Context<HonoCustomType>) => {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Context } from "hono";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { AdminWebhookSettings } from "../models";
|
||||
|
||||
|
||||
@@ -1,56 +1,58 @@
|
||||
import { Context } from 'hono';
|
||||
|
||||
import { HonoCustomType } from '../types';
|
||||
import { getAdminPasswords, getBooleanValue, getDefaultDomains, getDomains, getIntValue, getPasswords, getStringArray, getStringValue, getUserRoles, getAnotherWorkerList } from '../utils';
|
||||
import utils from '../utils';
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { isS3Enabled } from '../mails_api/s3_attachment';
|
||||
|
||||
export default {
|
||||
getConfig: async (c: Context<HonoCustomType>) => {
|
||||
return c.json({
|
||||
"DEFAULT_LANG": c.env.DEFAULT_LANG,
|
||||
"TITLE": c.env.TITLE,
|
||||
"HAS_PASSWORD": getPasswords(c).length,
|
||||
"HAS_ADMIN_PASSWORDS": getAdminPasswords(c).length,
|
||||
"ANNOUNCEMENT": getStringValue(c.env.ANNOUNCEMENT),
|
||||
"HAS_PASSWORD": utils.getPasswords(c).length,
|
||||
"HAS_ADMIN_PASSWORDS": utils.getAdminPasswords(c).length,
|
||||
"ANNOUNCEMENT": utils.getStringValue(c.env.ANNOUNCEMENT),
|
||||
"ALWAYS_SHOW_ANNOUNCEMENT": utils.getBooleanValue(c.env.ALWAYS_SHOW_ANNOUNCEMENT),
|
||||
|
||||
"PREFIX": getStringValue(c.env.PREFIX),
|
||||
"ADDRESS_CHECK_REGEX": getStringValue(c.env.ADDRESS_CHECK_REGEX),
|
||||
"ADDRESS_REGEX": getStringValue(c.env.ADDRESS_REGEX),
|
||||
"MIN_ADDRESS_LEN": getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||
"MAX_ADDRESS_LEN": getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||
"PREFIX": utils.getStringValue(c.env.PREFIX),
|
||||
"ADDRESS_CHECK_REGEX": utils.getStringValue(c.env.ADDRESS_CHECK_REGEX),
|
||||
"ADDRESS_REGEX": utils.getStringValue(c.env.ADDRESS_REGEX),
|
||||
"MIN_ADDRESS_LEN": utils.getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||
"MAX_ADDRESS_LEN": utils.getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||
|
||||
"FORWARD_ADDRESS_LIST": getStringArray(c.env.FORWARD_ADDRESS_LIST),
|
||||
"DEFAULT_DOMAINS": getDefaultDomains(c),
|
||||
"DOMAINS": getDomains(c),
|
||||
"DOMAIN_LABELS": getStringArray(c.env.DOMAIN_LABELS),
|
||||
"FORWARD_ADDRESS_LIST": utils.getStringArray(c.env.FORWARD_ADDRESS_LIST),
|
||||
"SUBDOMAIN_FORWARD_ADDRESS_LIST": utils.getJsonObjectValue<SubdomainForwardAddressList[]>(c.env.SUBDOMAIN_FORWARD_ADDRESS_LIST),
|
||||
"DEFAULT_DOMAINS": utils.getDefaultDomains(c),
|
||||
"DOMAINS": utils.getDomains(c),
|
||||
"DOMAIN_LABELS": utils.getStringArray(c.env.DOMAIN_LABELS),
|
||||
|
||||
"HAS_JWT_SECRET": !!getStringValue(c.env.JWT_SECRET),
|
||||
"HAS_JWT_SECRET": !!utils.getStringValue(c.env.JWT_SECRET),
|
||||
|
||||
"ADMIN_USER_ROLE": getStringValue(c.env.ADMIN_USER_ROLE),
|
||||
"USER_DEFAULT_ROLE": getStringValue(c.env.USER_DEFAULT_ROLE),
|
||||
"USER_ROLES": getUserRoles(c),
|
||||
"NO_LIMIT_SEND_ROLE": getStringValue(c.env.NO_LIMIT_SEND_ROLE),
|
||||
"ADMIN_USER_ROLE": utils.getStringValue(c.env.ADMIN_USER_ROLE),
|
||||
"USER_DEFAULT_ROLE": utils.getStringValue(c.env.USER_DEFAULT_ROLE),
|
||||
"USER_ROLES": utils.getUserRoles(c),
|
||||
"NO_LIMIT_SEND_ROLE": utils.getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE),
|
||||
|
||||
"ADMIN_CONTACT": c.env.ADMIN_CONTACT,
|
||||
"ENABLE_USER_CREATE_EMAIL": getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
|
||||
"DISABLE_ANONYMOUS_USER_CREATE_EMAIL": getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
|
||||
"ENABLE_USER_DELETE_EMAIL": getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
||||
"ENABLE_AUTO_REPLY": getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
||||
"ENABLE_USER_CREATE_EMAIL": utils.getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
|
||||
"DISABLE_ANONYMOUS_USER_CREATE_EMAIL": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
|
||||
"ENABLE_USER_DELETE_EMAIL": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
||||
"ENABLE_AUTO_REPLY": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
||||
"COPYRIGHT": c.env.COPYRIGHT,
|
||||
"ENABLE_WEBHOOK": getBooleanValue(c.env.ENABLE_WEBHOOK),
|
||||
"ENABLE_WEBHOOK": utils.getBooleanValue(c.env.ENABLE_WEBHOOK),
|
||||
"S3_ENABLED": isS3Enabled(c),
|
||||
"VERSION": CONSTANTS.VERSION,
|
||||
"DISABLE_SHOW_GITHUB": !getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
||||
"DISABLE_ADMIN_PASSWORD_CHECK": getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK),
|
||||
"ENABLE_CHECK_JUNK_MAIL": getBooleanValue(c.env.ENABLE_CHECK_JUNK_MAIL),
|
||||
"JUNK_MAIL_CHECK_LIST": getStringArray(c.env.JUNK_MAIL_CHECK_LIST),
|
||||
"JUNK_MAIL_FORCE_PASS_LIST": getStringArray(c.env.JUNK_MAIL_FORCE_PASS_LIST),
|
||||
"DISABLE_SHOW_GITHUB": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
||||
"DISABLE_ADMIN_PASSWORD_CHECK": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK),
|
||||
"ENABLE_CHECK_JUNK_MAIL": utils.getBooleanValue(c.env.ENABLE_CHECK_JUNK_MAIL),
|
||||
"JUNK_MAIL_CHECK_LIST": utils.getStringArray(c.env.JUNK_MAIL_CHECK_LIST),
|
||||
"JUNK_MAIL_FORCE_PASS_LIST": utils.getStringArray(c.env.JUNK_MAIL_FORCE_PASS_LIST),
|
||||
|
||||
"REMOVE_EXCEED_SIZE_ATTACHMENT": getBooleanValue(c.env.REMOVE_EXCEED_SIZE_ATTACHMENT),
|
||||
"REMOVE_ALL_ATTACHMENT": getBooleanValue(c.env.REMOVE_ALL_ATTACHMENT),
|
||||
"REMOVE_EXCEED_SIZE_ATTACHMENT": utils.getBooleanValue(c.env.REMOVE_EXCEED_SIZE_ATTACHMENT),
|
||||
"REMOVE_ALL_ATTACHMENT": utils.getBooleanValue(c.env.REMOVE_ALL_ATTACHMENT),
|
||||
|
||||
"ENABLE_ANOTHER_WORKER": getBooleanValue(c.env.ENABLE_ANOTHER_WORKER),
|
||||
"ANOTHER_WORKER_LIST": getAnotherWorkerList(c),
|
||||
"ENABLE_ANOTHER_WORKER": utils.getBooleanValue(c.env.ENABLE_ANOTHER_WORKER),
|
||||
"ANOTHER_WORKER_LIST": utils.getAnotherWorkerList(c),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import { getDomains, getPasswords, getBooleanValue, getIntValue, getStringArray, getDefaultDomains, getStringValue } from './utils';
|
||||
import utils from './utils';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { HonoCustomType } from './types';
|
||||
import { isS3Enabled } from './mails_api/s3_attachment';
|
||||
|
||||
const api = new Hono<HonoCustomType>
|
||||
@@ -10,35 +9,36 @@ const api = new Hono<HonoCustomType>
|
||||
api.get('/open_api/settings', async (c) => {
|
||||
// check header x-custom-auth
|
||||
let needAuth = false;
|
||||
const passwords = getPasswords(c);
|
||||
const passwords = utils.getPasswords(c);
|
||||
if (passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
needAuth = !auth || !passwords.includes(auth);
|
||||
}
|
||||
return c.json({
|
||||
"title": c.env.TITLE,
|
||||
"announcement": getStringValue(c.env.ANNOUNCEMENT),
|
||||
"prefix": getStringValue(c.env.PREFIX),
|
||||
"addressRegex": getStringValue(c.env.ADDRESS_REGEX),
|
||||
"minAddressLen": getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||
"maxAddressLen": getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||
"defaultDomains": getDefaultDomains(c),
|
||||
"domains": getDomains(c),
|
||||
"domainLabels": getStringArray(c.env.DOMAIN_LABELS),
|
||||
"announcement": utils.getStringValue(c.env.ANNOUNCEMENT),
|
||||
"alwaysShowAnnouncement": utils.getBooleanValue(c.env.ALWAYS_SHOW_ANNOUNCEMENT),
|
||||
"prefix": utils.getStringValue(c.env.PREFIX),
|
||||
"addressRegex": utils.getStringValue(c.env.ADDRESS_REGEX),
|
||||
"minAddressLen": utils.getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||
"maxAddressLen": utils.getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||
"defaultDomains": utils.getDefaultDomains(c),
|
||||
"domains": utils.getDomains(c),
|
||||
"domainLabels": utils.getStringArray(c.env.DOMAIN_LABELS),
|
||||
"needAuth": needAuth,
|
||||
"adminContact": c.env.ADMIN_CONTACT,
|
||||
"enableUserCreateEmail": getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
|
||||
"disableAnonymousUserCreateEmail": getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
|
||||
"enableUserDeleteEmail": getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
||||
"enableAutoReply": getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
||||
"enableIndexAbout": getBooleanValue(c.env.ENABLE_INDEX_ABOUT),
|
||||
"enableUserCreateEmail": utils.getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
|
||||
"disableAnonymousUserCreateEmail": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
|
||||
"enableUserDeleteEmail": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
||||
"enableAutoReply": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
||||
"enableIndexAbout": utils.getBooleanValue(c.env.ENABLE_INDEX_ABOUT),
|
||||
"copyright": c.env.COPYRIGHT,
|
||||
"cfTurnstileSiteKey": c.env.CF_TURNSTILE_SITE_KEY,
|
||||
"enableWebhook": getBooleanValue(c.env.ENABLE_WEBHOOK),
|
||||
"enableWebhook": utils.getBooleanValue(c.env.ENABLE_WEBHOOK),
|
||||
"isS3Enabled": isS3Enabled(c),
|
||||
"version": CONSTANTS.VERSION,
|
||||
"showGithub": !getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
||||
"disableAdminPasswordCheck": getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK)
|
||||
"showGithub": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
||||
"disableAdminPasswordCheck": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK)
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Context } from 'hono';
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList } from './utils';
|
||||
import { HonoCustomType, UserRole, AnotherWorker, RPCEmailMessage, ParsedEmailContext } from './types';
|
||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
|
||||
@@ -151,7 +150,7 @@ export const cleanup = async (
|
||||
cleanType: string | undefined | null,
|
||||
cleanDays: number | undefined | null
|
||||
): Promise<boolean> => {
|
||||
if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 30) {
|
||||
if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 1000) {
|
||||
throw new Error("Invalid cleanType or cleanDays")
|
||||
}
|
||||
console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
|
||||
@@ -339,26 +338,26 @@ export const getAllowDomains = async (c: Context<HonoCustomType>): Promise<strin
|
||||
return user_role?.domains || getDefaultDomains(c);;
|
||||
}
|
||||
|
||||
export async function sendWebhook(settings: WebhookSettings, formatMap: WebhookMail): Promise<{ success: boolean, message?: string }> {
|
||||
export async function sendWebhook(
|
||||
settings: WebhookSettings, formatMap: WebhookMail
|
||||
): Promise<{ success: boolean, message?: string }> {
|
||||
// send webhook
|
||||
let body = settings.body;
|
||||
for (const key of Object.keys(formatMap)) {
|
||||
/* eslint-disable no-useless-escape */
|
||||
body = body.replace(
|
||||
new RegExp(`\\$\\{${key}\\}`, "g"),
|
||||
JSON.stringify(
|
||||
formatMap[key as keyof WebhookMail]
|
||||
).replace(/^"(.*)"$/, '\$1')
|
||||
).replace(/^"(.*)"$/, '$1')
|
||||
);
|
||||
/* eslint-enable no-useless-escape */
|
||||
}
|
||||
console.log("send webhook", settings.url, settings.method, settings.headers, body);
|
||||
const response = await fetch(settings.url, {
|
||||
method: settings.method,
|
||||
headers: JSON.parse(settings.headers),
|
||||
body: body
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.log("send webhook error", settings.url, settings.method, settings.headers, body);
|
||||
console.log("send webhook error", response.status, response.statusText);
|
||||
return { success: false, message: `send webhook error: ${response.status} ${response.statusText}` };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: 'v0.8.6',
|
||||
VERSION: 'v' + '0.10.0',
|
||||
|
||||
// DB Version
|
||||
DB_VERSION_KEY: 'db_version',
|
||||
DB_VERSION: "v0.0.1",
|
||||
|
||||
// DB settings
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createMimeMessage } from "mimetext";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import { Bindings } from "../types";
|
||||
|
||||
export const auto_reply = async (message: ForwardableEmailMessage, env: Bindings): Promise<void> => {
|
||||
const message_id = message.headers.get("Message-ID");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { Bindings } from "../types";
|
||||
|
||||
export const isBlocked = async (from: string, env: Bindings): Promise<boolean> => {
|
||||
if (env.BLACK_LIST && env.BLACK_LIST.split(",").some(word => from.includes(word))) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Bindings, ParsedEmailContext } from "../types";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import { commonParseMail } from "../common";
|
||||
import { createMimeMessage } from "mimetext";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Bindings, ParsedEmailContext } from "../types";
|
||||
import { getBooleanValue, getStringArray } from "../utils";
|
||||
import { commonParseMail } from "../common";
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Context } from "hono";
|
||||
|
||||
import { getEnvStringList } from "../utils";
|
||||
import { getEnvStringList, getJsonObjectValue } from "../utils";
|
||||
import { sendMailToTelegram } from "../telegram_api";
|
||||
import { Bindings, HonoCustomType, RPCEmailMessage, ParsedEmailContext } from "../types";
|
||||
import { auto_reply } from "./auto_reply";
|
||||
import { isBlocked } from "./black_list";
|
||||
import { triggerWebhook, triggerAnotherWorker, commonParseMail } from "../common";
|
||||
@@ -67,6 +66,30 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
|
||||
console.error("forward email error", error);
|
||||
}
|
||||
|
||||
// forward subdomain email
|
||||
try {
|
||||
// 遍历 FORWARD_ADDRESS_LIST
|
||||
const subdomainForwardAddressList = getJsonObjectValue<SubdomainForwardAddressList[]>(env.SUBDOMAIN_FORWARD_ADDRESS_LIST) || [];
|
||||
for (const subdomainForwardAddress of subdomainForwardAddressList) {
|
||||
// 检查邮件是否匹配 domains
|
||||
if (subdomainForwardAddress.domains && subdomainForwardAddress.domains.length > 0) {
|
||||
for (const domain of subdomainForwardAddress.domains) {
|
||||
if (message.to.endsWith(domain)) {
|
||||
// 转发邮件
|
||||
await message.forward(subdomainForwardAddress.forward);
|
||||
// 支持多邮箱转发收件,不进行截止
|
||||
// break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果 domains 为空,则转发所有邮件
|
||||
await message.forward(subdomainForwardAddress.forward);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("subdomain forward email error", error);
|
||||
}
|
||||
|
||||
// send email to telegram
|
||||
try {
|
||||
await sendMailToTelegram(
|
||||
|
||||
43
worker/src/i18n/en.ts
Normal file
43
worker/src/i18n/en.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { LocaleMessages } from "./type";
|
||||
|
||||
const messages: LocaleMessages = {
|
||||
CustomAuthPasswordMsg: "You have enabled the private site password, please provide the password",
|
||||
UserTokenExpiredMsg: "Your token has expired, please login again",
|
||||
UserAcceesTokenExpiredMsg: "Your access token has expired, please refresh the page",
|
||||
UserRoleIsNotAdminMsg: "Your user role is not admin, no access to visit this page",
|
||||
NeedAdminPasswordMsg: "You need to provide the admin password to access this page",
|
||||
|
||||
KVNotAvailableMsg: "KV is not available, please contact the administrator",
|
||||
DBNotAvailableMsg: "DB is not available, please contact the administrator",
|
||||
JWTSecretNotSetMsg: "JWT_SECRET is not set, please contact the administrator",
|
||||
WebhookNotEnabledMsg: "Webhook is not enabled, please contact the administrator",
|
||||
DomainsNotSetMsg: "Domains are not set, please contact the administrator",
|
||||
|
||||
TurnstileCheckFailedMsg: "Human verification check failed",
|
||||
NewAddressDisabledMsg: "New address is disabled, please contact the administrator",
|
||||
NewAddressAnonymousDisabledMsg: "New address for anonymous user is disabled, please contact the administrator",
|
||||
FailedCreateAddressMsg: "Failed to create address",
|
||||
InvalidAddressMsg: "Invalid address",
|
||||
InvalidAddressCredentialMsg: "Invalid address credential",
|
||||
UserDeleteEmailDisabledMsg: "User delete address/email is disabled, please contact the administrator",
|
||||
|
||||
UserNotFoundMsg: "User not found",
|
||||
UserAlreadyExistsMsg: "User already exists, please login",
|
||||
FailedToRegisterMsg: "Failed to register",
|
||||
UserRegistrationDisabledMsg: "User registration is disabled, please contact the administrator",
|
||||
UserMailDomainMustInMsg: "User mail domain must be in this list",
|
||||
InvalidVerifyCodeMsg: "Invalid verify code",
|
||||
InvalidEmailOrPasswordMsg: "Invalid email or password",
|
||||
VerifyMailSenderNotSetMsg: "Verify mail sender address is not set, please contact the administrator",
|
||||
CodeAlreadySentMsg: "Code already sent, please wait",
|
||||
InvalidUserDefaultRoleMsg: "Invalid user default role, please contact the administrator",
|
||||
FailedUpdateUserDefaultRoleMsg: "Failed to update user default role, please contact the administrator",
|
||||
|
||||
Oauth2ClientIDNotFoundMsg: "Oauth2 client ID is not set, please contact the administrator",
|
||||
Oauth2CliendIDOrCodeMissingMsg: "Oauth2 client ID or code is missing",
|
||||
Oauth2FailedGetUserInfoMsg: "Failed to get user info from Oauth2 provider",
|
||||
Oauth2FailedGetAccessTokenMsg: "Failed to get access token from Oauth2 provider",
|
||||
Oauth2FailedGetUserEmailMsg: "Failed to get user email from Oauth2 provider",
|
||||
}
|
||||
|
||||
export default messages;
|
||||
28
worker/src/i18n/index.ts
Normal file
28
worker/src/i18n/index.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { LocaleMessages } from "./type";
|
||||
import zh from "./zh";
|
||||
import en from "./en";
|
||||
import { Context } from "hono";
|
||||
|
||||
export default {
|
||||
getMessages: (
|
||||
locale: string | null | undefined
|
||||
): LocaleMessages => {
|
||||
// multi-language support
|
||||
if (locale === "en") return en;
|
||||
if (locale === "zh") return zh;
|
||||
|
||||
// fallback language
|
||||
return en;
|
||||
},
|
||||
getMessagesbyContext: (
|
||||
c: Context<HonoCustomType>
|
||||
): LocaleMessages => {
|
||||
const locale = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
// multi-language support
|
||||
if (locale === "en") return en;
|
||||
if (locale === "zh") return zh;
|
||||
|
||||
// fallback language
|
||||
return en;
|
||||
}
|
||||
}
|
||||
39
worker/src/i18n/type.ts
Normal file
39
worker/src/i18n/type.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export type LocaleMessages = {
|
||||
CustomAuthPasswordMsg: string
|
||||
UserTokenExpiredMsg: string
|
||||
UserAcceesTokenExpiredMsg: string
|
||||
UserRoleIsNotAdminMsg: string
|
||||
NeedAdminPasswordMsg: string
|
||||
|
||||
KVNotAvailableMsg: string
|
||||
DBNotAvailableMsg: string
|
||||
JWTSecretNotSetMsg: string
|
||||
WebhookNotEnabledMsg: string
|
||||
DomainsNotSetMsg: string
|
||||
|
||||
TurnstileCheckFailedMsg: string
|
||||
NewAddressDisabledMsg: string
|
||||
NewAddressAnonymousDisabledMsg: string
|
||||
FailedCreateAddressMsg: string
|
||||
InvalidAddressMsg: string
|
||||
InvalidAddressCredentialMsg: string
|
||||
UserDeleteEmailDisabledMsg: string
|
||||
|
||||
UserNotFoundMsg: string
|
||||
UserAlreadyExistsMsg: string
|
||||
FailedToRegisterMsg: string
|
||||
UserRegistrationDisabledMsg: string
|
||||
UserMailDomainMustInMsg: string
|
||||
InvalidVerifyCodeMsg: string
|
||||
InvalidEmailOrPasswordMsg: string
|
||||
VerifyMailSenderNotSetMsg: string
|
||||
CodeAlreadySentMsg: string
|
||||
InvalidUserDefaultRoleMsg: string
|
||||
FailedUpdateUserDefaultRoleMsg: string
|
||||
|
||||
Oauth2ClientIDNotFoundMsg: string
|
||||
Oauth2CliendIDOrCodeMissingMsg: string
|
||||
Oauth2FailedGetUserInfoMsg: string
|
||||
Oauth2FailedGetAccessTokenMsg: string
|
||||
Oauth2FailedGetUserEmailMsg: string
|
||||
}
|
||||
43
worker/src/i18n/zh.ts
Normal file
43
worker/src/i18n/zh.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { LocaleMessages } from "./type";
|
||||
|
||||
const messages: LocaleMessages = {
|
||||
CustomAuthPasswordMsg: "你已启用私有站点密码,请提供密码",
|
||||
UserTokenExpiredMsg: "您的令牌已过期, 请重新登录",
|
||||
UserAcceesTokenExpiredMsg: "您的访问令牌已过期, 请刷新页面",
|
||||
UserRoleIsNotAdminMsg: "您的用户角色不是管理员, 无权访问",
|
||||
NeedAdminPasswordMsg: "您需要提供管理员密码才能访问此页面",
|
||||
|
||||
KVNotAvailableMsg: "KV 不可用, 请联系管理员",
|
||||
DBNotAvailableMsg: "DB 不可用, 请联系管理员",
|
||||
JWTSecretNotSetMsg: "JWT_SECRET 未设置, 请联系管理员",
|
||||
WebhookNotEnabledMsg: "Webhook 未启用, 请联系管理员",
|
||||
DomainsNotSetMsg: "域名列表未设置, 请联系管理员",
|
||||
|
||||
TurnstileCheckFailedMsg: "人机验证检查失败",
|
||||
NewAddressDisabledMsg: "新建邮箱地址已禁用, 请联系管理员",
|
||||
NewAddressAnonymousDisabledMsg: "匿名用户新建邮箱地址已禁用, 请联系管理员",
|
||||
FailedCreateAddressMsg: "创建邮箱地址失败",
|
||||
InvalidAddressMsg: "无效的邮箱地址",
|
||||
InvalidAddressCredentialMsg: "无效的邮箱地址凭据",
|
||||
UserDeleteEmailDisabledMsg: "用户删除邮箱/邮件已禁用, 请联系管理员",
|
||||
|
||||
UserNotFoundMsg: "用户不存在",
|
||||
UserAlreadyExistsMsg: "用户已存在, 请登录",
|
||||
FailedToRegisterMsg: "注册失败",
|
||||
UserRegistrationDisabledMsg: "用户注册已禁用, 请联系管理员",
|
||||
UserMailDomainMustInMsg: "用户邮箱域必须在此列表中",
|
||||
InvalidVerifyCodeMsg: "无效的验证码",
|
||||
InvalidEmailOrPasswordMsg: "无效的邮箱或密码",
|
||||
VerifyMailSenderNotSetMsg: "验证邮件发送邮箱未设置, 请联系管理员",
|
||||
CodeAlreadySentMsg: "验证码已发送, 请稍等",
|
||||
InvalidUserDefaultRoleMsg: "无效的用户默认角色, 请联系管理员",
|
||||
FailedUpdateUserDefaultRoleMsg: "更新用户默认角色失败, 请联系管理员",
|
||||
|
||||
Oauth2ClientIDNotFoundMsg: "Oauth2 客户端 ID 未设置, 请联系管理员",
|
||||
Oauth2CliendIDOrCodeMissingMsg: "Oauth2 客户端 ID 或 code 缺失",
|
||||
Oauth2FailedGetUserInfoMsg: "从 Oauth2 提供商获取用户信息失败",
|
||||
Oauth2FailedGetAccessTokenMsg: "从 Oauth2 提供商获取访问令牌失败",
|
||||
Oauth2FailedGetUserEmailMsg: "从 Oauth2 提供商获取用户邮箱失败",
|
||||
}
|
||||
|
||||
export default messages;
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Context } from "hono";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import { HonoCustomType } from "../types";
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import { HonoCustomType } from "../types";
|
||||
import { getBooleanValue, getJsonSetting, checkCfTurnstile, getStringValue } from '../utils';
|
||||
import i18n from '../i18n';
|
||||
import { getBooleanValue, getJsonSetting, checkCfTurnstile, getStringValue, getSplitStringListValue } from '../utils';
|
||||
import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains } from '../common'
|
||||
import { CONSTANTS } from '../constants'
|
||||
import auto_reply from './auto_reply'
|
||||
@@ -16,6 +16,7 @@ api.get('/api/webhook/settings', webhook_settings.getWebhookSettings)
|
||||
api.post('/api/webhook/settings', webhook_settings.saveWebhookSettings)
|
||||
api.post('/api/webhook/test', webhook_settings.testWebhookSettings)
|
||||
api.get('/api/attachment/list', s3_attachment.list)
|
||||
api.post('/api/attachment/delete', s3_attachment.deleteKey)
|
||||
api.post('/api/attachment/put_url', s3_attachment.getSignedPutUrl)
|
||||
api.post('/api/attachment/get_url', s3_attachment.getSignedGetUrl)
|
||||
|
||||
@@ -42,8 +43,10 @@ api.get('/api/mail/:mail_id', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/api/mails/:id', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text("User delete email is disabled", 403)
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { id } = c.req.param();
|
||||
@@ -59,16 +62,18 @@ api.delete('/api/mails/:id', async (c) => {
|
||||
api.get('/api/settings', async (c) => {
|
||||
const { address, address_id } = c.get("jwtPayload")
|
||||
const user_role = c.get("userRolePayload")
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (address_id && address_id > 0) {
|
||||
try {
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ? `
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("Invalid address", 400)
|
||||
return c.text(msgs.InvalidAddressMsg, 400)
|
||||
}
|
||||
} catch (error) {
|
||||
return c.text("Invalid address", 400)
|
||||
return c.text(msgs.InvalidAddressMsg, 400)
|
||||
}
|
||||
}
|
||||
// check address id
|
||||
@@ -78,11 +83,11 @@ api.get('/api/settings', async (c) => {
|
||||
`SELECT id FROM address where name = ? `
|
||||
).bind(address).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("Invalid address", 400)
|
||||
return c.text(msgs.InvalidAddressMsg, 400)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return c.text("Invalid address", 400)
|
||||
return c.text(msgs.InvalidAddressMsg, 400)
|
||||
}
|
||||
// update address updated_at
|
||||
try {
|
||||
@@ -92,7 +97,8 @@ api.get('/api/settings', async (c) => {
|
||||
} catch (e) {
|
||||
console.warn("Failed to update address")
|
||||
}
|
||||
const is_no_limit_send_balance = user_role && user_role === getStringValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
const is_no_limit_send_balance = user_role && no_limit_roles.includes(user_role);
|
||||
const balance = is_no_limit_send_balance ? 99999 : await c.env.DB.prepare(
|
||||
`SELECT balance FROM address_sender where address = ? and enabled = 1`
|
||||
).bind(address).first("balance");
|
||||
@@ -103,13 +109,15 @@ api.get('/api/settings', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/api/new_address', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL)
|
||||
&& !c.get("userPayload")
|
||||
) {
|
||||
return c.text("New address for anonymous user is disabled", 403)
|
||||
return c.text(msgs.NewAddressAnonymousDisabledMsg, 403)
|
||||
}
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL)) {
|
||||
return c.text("New address is disabled", 403)
|
||||
return c.text(msgs.NewAddressDisabledMsg, 403)
|
||||
}
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { name, domain, cf_token } = await c.req.json();
|
||||
@@ -117,7 +125,7 @@ api.post('/api/new_address', async (c) => {
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
} catch (error) {
|
||||
return c.text("Failed to check cf turnstile", 500)
|
||||
return c.text(msgs.TurnstileCheckFailedMsg, 500)
|
||||
}
|
||||
// if no name, generate random name
|
||||
if (!name) {
|
||||
@@ -143,7 +151,7 @@ api.post('/api/new_address', async (c) => {
|
||||
});
|
||||
return c.json(res);
|
||||
} catch (e) {
|
||||
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
||||
return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { HonoCustomType } from "../types";
|
||||
import { Context } from "hono";
|
||||
import {
|
||||
S3Client,
|
||||
ListObjectsV2Command,
|
||||
GetObjectCommand,
|
||||
PutObjectCommand
|
||||
PutObjectCommand,
|
||||
DeleteObjectCommand
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
|
||||
@@ -81,4 +81,16 @@ export default {
|
||||
}
|
||||
);
|
||||
},
|
||||
deleteKey: async (c: Context<HonoCustomType>) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { key } = await c.req.json()
|
||||
const client = getS3Client(c);
|
||||
await client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: c.env.S3_BUCKET,
|
||||
Key: `${address}/${key}`
|
||||
})
|
||||
);
|
||||
return c.json({ success: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ import { Context, Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { createMimeMessage } from 'mimetext';
|
||||
import { Resend } from 'resend';
|
||||
import { WorkerMailer, WorkerMailerOptions } from 'worker-mailer';
|
||||
|
||||
import i18n from '../i18n';
|
||||
import { CONSTANTS } from '../constants'
|
||||
import { getJsonSetting, getDomains, getIntValue, getBooleanValue, getStringValue } from '../utils';
|
||||
import { getJsonSetting, getDomains, getIntValue, getBooleanValue, getStringValue, getJsonObjectValue, getSplitStringListValue } from '../utils';
|
||||
import { GeoData } from '../models'
|
||||
import { handleListQuery } from '../common'
|
||||
import { HonoCustomType } from '../types';
|
||||
|
||||
|
||||
export const api = new Hono<HonoCustomType>()
|
||||
@@ -89,6 +90,32 @@ const sendMailByResend = async (
|
||||
console.log(`Resend success: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
const sendMailBySmtp = async (
|
||||
c: Context<HonoCustomType>, address: string,
|
||||
reqJson: {
|
||||
from_name: string, to_mail: string, to_name: string,
|
||||
subject: string, content: string, is_html: boolean
|
||||
},
|
||||
smtpOptions: WorkerMailerOptions
|
||||
): Promise<void> => {
|
||||
await WorkerMailer.send(
|
||||
smtpOptions,
|
||||
{
|
||||
from: {
|
||||
name: reqJson.from_name,
|
||||
email: address
|
||||
},
|
||||
to: {
|
||||
name: reqJson.to_name,
|
||||
email: reqJson.to_mail
|
||||
},
|
||||
subject: reqJson.subject,
|
||||
text: reqJson.is_html ? undefined : reqJson.content,
|
||||
html: reqJson.is_html ? reqJson.content : undefined
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const sendMail = async (
|
||||
c: Context<HonoCustomType>, address: string,
|
||||
reqJson: {
|
||||
@@ -109,7 +136,8 @@ export const sendMail = async (
|
||||
throw new Error("Invalid domain")
|
||||
}
|
||||
const user_role = c.get("userRolePayload");
|
||||
const is_no_limit_send_balance = user_role && user_role === getStringValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
const is_no_limit_send_balance = user_role && no_limit_roles.includes(user_role);
|
||||
// no need find noLimitSendAddressList if is_no_limit_send_balance
|
||||
const noLimitSendAddressList = is_no_limit_send_balance ?
|
||||
[] : await getJsonSetting(c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY) || [];
|
||||
@@ -138,15 +166,20 @@ export const sendMail = async (
|
||||
throw new Error("to_mail address is blocked")
|
||||
}
|
||||
if (!subject) {
|
||||
throw new Error("Invalid subject")
|
||||
throw new Error("Subject is empty")
|
||||
}
|
||||
if (!content) {
|
||||
throw new Error("Invalid content")
|
||||
throw new Error("Content is empty")
|
||||
}
|
||||
|
||||
// send to verified address list, do not update balance
|
||||
const resendEnabled = c.env.RESEND_TOKEN || c.env[
|
||||
`RESEND_TOKEN_${mailDomain.replace(/\./g, "_").toUpperCase()}`
|
||||
];
|
||||
// send by smtp
|
||||
const smtpConfigMap = getJsonObjectValue<Record<string, WorkerMailerOptions>>(c.env.SMTP_CONFIG);
|
||||
const smtpConfig = smtpConfigMap ? smtpConfigMap[mailDomain] : null;
|
||||
// send by verified address list
|
||||
let sendByVerifiedAddressList = false;
|
||||
if (c.env.SEND_MAIL) {
|
||||
const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY) || [];
|
||||
@@ -155,6 +188,8 @@ export const sendMail = async (
|
||||
sendByVerifiedAddressList = true;
|
||||
}
|
||||
}
|
||||
|
||||
// send mail workflow
|
||||
if (sendByVerifiedAddressList) {
|
||||
// do not update balance
|
||||
}
|
||||
@@ -162,9 +197,16 @@ export const sendMail = async (
|
||||
else if (resendEnabled) {
|
||||
await sendMailByResend(c, address, reqJson);
|
||||
}
|
||||
else {
|
||||
throw new Error("Please enable resend or verified address list")
|
||||
else if (smtpConfig) {
|
||||
await sendMailBySmtp(c, address, reqJson, smtpConfig);
|
||||
}
|
||||
else {
|
||||
if (c.env.SEND_MAIL) {
|
||||
throw new Error(`Please enable resend or smtp for domain ${mailDomain}. Or add ${to_mail} to verified address list`);
|
||||
}
|
||||
throw new Error(`Please enable resend or smtp for domain ${mailDomain}`);
|
||||
}
|
||||
|
||||
// update balance
|
||||
if (!sendByVerifiedAddressList && needCheckBalance) {
|
||||
try {
|
||||
@@ -247,8 +289,10 @@ api.get('/api/sendbox', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/api/sendbox/:id', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text("User delete email is disabled", 403)
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { id } = c.req.param();
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Context } from "hono";
|
||||
import { HonoCustomType, ParsedEmailContext } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { AdminWebhookSettings, WebhookSettings } from "../models";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import { commonParseMail, sendWebhook } from "../common";
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import { cleanup } from './common'
|
||||
import { CONSTANTS } from './constants'
|
||||
import { getJsonSetting } from './utils';
|
||||
import { CleanupSettings } from './models';
|
||||
import { Bindings, HonoCustomType } from './types';
|
||||
|
||||
export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) {
|
||||
console.log("Scheduled event: ", event);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Context } from "hono";
|
||||
import { Jwt } from "hono/utils/jwt";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { getIntValue, getJsonSetting } from "../utils";
|
||||
import { deleteAddressWithData, newAddress } from "../common";
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Hono } from 'hono'
|
||||
import { ServerResponse } from 'node:http'
|
||||
import { Writable } from 'node:stream'
|
||||
|
||||
import { HonoCustomType } from '../types'
|
||||
import { newTelegramBot, initTelegramBotCommands, sendMailToTelegram } from './telegram'
|
||||
import settings from './settings'
|
||||
import miniapp from './miniapp'
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Context } from "hono";
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { HonoCustomType } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { bindTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress } from "./common";
|
||||
import { checkCfTurnstile } from "../utils";
|
||||
import { TelegramSettings } from "./settings";
|
||||
import i18n from "../i18n";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const TG_AUTH_TIMEOUT = 300;
|
||||
@@ -84,11 +84,13 @@ async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Respo
|
||||
|
||||
async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, address, cf_token } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
// check cf turnstile
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
} catch (error) {
|
||||
return c.text("Failed to check cf turnstile", 500)
|
||||
return c.text(msgs.TurnstileCheckFailedMsg, 500)
|
||||
}
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Context } from "hono";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
export class TelegramSettings {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { callbackQuery } from "telegraf/filters";
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getDomains, getJsonObjectValue, getStringValue } from '../utils';
|
||||
import { HonoCustomType, ParsedEmailContext } from "../types";
|
||||
import { TelegramSettings } from "./settings";
|
||||
import { bindTelegramAddress, deleteTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress, unbindTelegramByAddress } from "./common";
|
||||
import { commonParseMail } from "../common";
|
||||
@@ -102,7 +101,10 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
const res = await tgUserNewAddress(c, userId.toString(), address);
|
||||
return await ctx.reply(`创建地址成功:\n`
|
||||
+ `地址: ${res.address}\n`
|
||||
+ `凭证: ${res.jwt}\n`
|
||||
+ `凭证: \`${res.jwt}\`\n`,
|
||||
{
|
||||
parse_mode: "Markdown"
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`创建地址失败: ${(e as Error).message}`);
|
||||
|
||||
20
worker/src/types.d.ts
vendored
20
worker/src/types.d.ts
vendored
@@ -1,19 +1,22 @@
|
||||
export type UserRole = {
|
||||
type UserRole = {
|
||||
domains: string[] | undefined | null,
|
||||
role: string,
|
||||
prefix: string | undefined | null
|
||||
}
|
||||
|
||||
export type Bindings = {
|
||||
type Bindings = {
|
||||
// bindings
|
||||
DB: D1Database
|
||||
KV: KVNamespace
|
||||
RATE_LIMITER: any
|
||||
SEND_MAIL: any
|
||||
ASSETS: Fetcher
|
||||
|
||||
// config
|
||||
DEFAULT_LANG: string | undefined
|
||||
TITLE: string | undefined
|
||||
ANNOUNCEMENT: string | undefined | null
|
||||
ALWAYS_SHOW_ANNOUNCEMENT: string | boolean | undefined
|
||||
PREFIX: string | undefined
|
||||
ADDRESS_CHECK_REGEX: string | undefined
|
||||
ADDRESS_REGEX: string | undefined
|
||||
@@ -50,6 +53,8 @@ export type Bindings = {
|
||||
ENABLE_ANOTHER_WORKER: string | boolean | undefined
|
||||
ANOTHER_WORKER_LIST: string | AnotherWorker[] | undefined
|
||||
|
||||
SUBDOMAIN_FORWARD_ADDRESS_LIST: string | SubdomainForwardAddressList[] | undefined
|
||||
|
||||
REMOVE_ALL_ATTACHMENT: string | boolean | undefined
|
||||
REMOVE_EXCEED_SIZE_ATTACHMENT: string | boolean | undefined
|
||||
|
||||
@@ -66,7 +71,10 @@ export type Bindings = {
|
||||
|
||||
// resend
|
||||
RESEND_TOKEN: string | undefined
|
||||
[key: `RESEND_TOKEN_${string}`]: string | undefined;
|
||||
[key: `RESEND_TOKEN_${string}`]: string | undefined
|
||||
|
||||
// SMTP config
|
||||
SMTP_CONFIG: string | object | undefined
|
||||
|
||||
// telegram config
|
||||
TELEGRAM_BOT_TOKEN: string
|
||||
@@ -93,6 +101,7 @@ type Variables = {
|
||||
userPayload: UserPayload,
|
||||
userRolePayload: string | undefined | null,
|
||||
jwtPayload: JwtPayload,
|
||||
lang: string | undefined | null
|
||||
}
|
||||
|
||||
type HonoCustomType = {
|
||||
@@ -123,3 +132,8 @@ type ParsedEmailContext = {
|
||||
headers?: Record<string, string>[]
|
||||
} | undefined
|
||||
}
|
||||
|
||||
type SubdomainForwardAddressList = {
|
||||
domains: string[] | undefined | null,
|
||||
forward: string,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user