Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
6ae3b0d85e | ||
|
|
01e6cb1075 | ||
|
|
814f6fada2 | ||
|
|
31901aacc5 | ||
|
|
fb9b9f6ae4 | ||
|
|
095951ab45 | ||
|
|
37614ce6fa | ||
|
|
3f81fbee6d | ||
|
|
cf13236e7b | ||
|
|
36e9c611e6 | ||
|
|
047200c1c2 | ||
|
|
a22add0e14 | ||
|
|
7b1c4cc72a | ||
|
|
3870727a08 | ||
|
|
2bb033964c | ||
|
|
9db5a00b35 | ||
|
|
e161eb5d14 | ||
|
|
b604f56d56 | ||
|
|
52caf811f5 | ||
|
|
ee3884914b | ||
|
|
844fc52bbc | ||
|
|
b87b49f09d | ||
|
|
5bfa588f70 | ||
|
|
92620cdedb | ||
|
|
e9748be9fe | ||
|
|
479322c430 | ||
|
|
934e58e23b | ||
|
|
c964d77a59 | ||
|
|
8a03d3e57f | ||
|
|
6caba7c863 | ||
|
|
43e5bdc764 | ||
|
|
7bec0daba4 | ||
|
|
13e5adef17 | ||
|
|
440238133e | ||
|
|
4a881e2d2b | ||
|
|
b0bf7a5f13 | ||
|
|
a9bb8785ba | ||
|
|
0b48baff6d | ||
|
|
e0b5e80efd | ||
|
|
b0e36ac2aa | ||
|
|
51db19c85b | ||
|
|
e52b010aa4 | ||
|
|
8f6793402c | ||
|
|
e86c530116 | ||
|
|
0308f518da | ||
|
|
3c2a8ed056 | ||
|
|
5f45ec7c14 | ||
|
|
1b7ebc98c5 | ||
|
|
c102004f4d | ||
|
|
3c81e05a2f | ||
|
|
5ff2ceb5e8 | ||
|
|
6c82efb738 | ||
|
|
e99acdcc6e | ||
|
|
8f30505706 | ||
|
|
ddfa2c5d03 | ||
|
|
49b3f10838 | ||
|
|
cc9ac67319 | ||
|
|
7cc2a2b576 | ||
|
|
393c5902c3 | ||
|
|
5ece49a576 | ||
|
|
de80857e2c | ||
|
|
a57a42b2a1 | ||
|
|
a24cc1f642 | ||
|
|
4c6fd3c2af | ||
|
|
1cf38c1768 | ||
|
|
b5b59acdb3 | ||
|
|
6d4783e1cd | ||
|
|
34e3e1b439 | ||
|
|
56104cd23a | ||
|
|
3664028e06 | ||
|
|
9888f98d74 | ||
|
|
ac5605f17f | ||
|
|
a9719cb3ec | ||
|
|
5f4978645b | ||
|
|
621476cb79 | ||
|
|
c969c4b082 | ||
|
|
d90f54345d | ||
|
|
797b8bb019 | ||
|
|
7e5d142924 |
3
.flake8
Normal file
@@ -0,0 +1,3 @@
|
||||
[flake8]
|
||||
max-line-length = 180
|
||||
exclude = .git,__pycache__,build,dist
|
||||
44
.github/config/mail-parser-wasm-worker.patch
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
diff --git a/worker/src/common.ts b/worker/src/common.ts
|
||||
index bd9bcc9..e7e2748 100644
|
||||
--- a/worker/src/common.ts
|
||||
+++ b/worker/src/common.ts
|
||||
@@ -273,23 +273,23 @@ export const commonParseMail = async (parsedEmailContext: ParsedEmailContext): P
|
||||
}
|
||||
const raw_mail = parsedEmailContext.rawEmail;
|
||||
// TODO: WASM parse email
|
||||
- // try {
|
||||
- // const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
|
||||
+ try {
|
||||
+ const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
|
||||
|
||||
- // const parsedEmail = parse_message_wrapper(raw_mail);
|
||||
- // parsedEmailContext.parsedEmail = {
|
||||
- // sender: parsedEmail.sender || "",
|
||||
- // subject: parsedEmail.subject || "",
|
||||
- // text: parsedEmail.text || "",
|
||||
- // headers: parsedEmail.headers?.map(
|
||||
- // (header) => ({ key: header.key, value: header.value })
|
||||
- // ) || [],
|
||||
- // html: parsedEmail.body_html || "",
|
||||
- // };
|
||||
- // return parsedEmailContext.parsedEmail;
|
||||
- // } catch (e) {
|
||||
- // console.error("Failed use mail-parser-wasm-worker to parse email", e);
|
||||
- // }
|
||||
+ const parsedEmail = parse_message_wrapper(raw_mail);
|
||||
+ parsedEmailContext.parsedEmail = {
|
||||
+ sender: parsedEmail.sender || "",
|
||||
+ subject: parsedEmail.subject || "",
|
||||
+ text: parsedEmail.text || "",
|
||||
+ headers: parsedEmail.headers?.map(
|
||||
+ (header) => ({ key: header.key, value: header.value })
|
||||
+ ) || [],
|
||||
+ html: parsedEmail.body_html || "",
|
||||
+ };
|
||||
+ return parsedEmailContext.parsedEmail;
|
||||
+ } catch (e) {
|
||||
+ console.error("Failed use mail-parser-wasm-worker to parse email", e);
|
||||
+ }
|
||||
try {
|
||||
const { default: PostalMime } = await import('postal-mime');
|
||||
const parsedEmail = await PostalMime.parse(raw_mail);
|
||||
37
.github/workflows/backend_deploy.yaml
vendored
@@ -1,6 +1,9 @@
|
||||
name: Deploy Backend Production
|
||||
name: Deploy Backend
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Upstream Sync]
|
||||
types: [completed]
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
@@ -29,14 +32,36 @@ 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/
|
||||
echo '${{ secrets.BACKEND_TOML }}' > wrangler.toml
|
||||
pnpm install --no-frozen-lockfile
|
||||
output=$(pnpm run deploy 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
code=$?
|
||||
echo "Command failed with exit code $code"
|
||||
exit $code
|
||||
|
||||
if [ -n "$use_mail_wasm_parser" ]; then
|
||||
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"
|
||||
fi
|
||||
|
||||
if [ -n "$debug_mode" ]; then
|
||||
pnpm run deploy
|
||||
else
|
||||
output=$(pnpm run deploy 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
code=$?
|
||||
echo "Command failed with exit code $code"
|
||||
exit $code
|
||||
fi
|
||||
fi
|
||||
echo "Deployed for tag ${{ github.ref_name }}"
|
||||
env:
|
||||
|
||||
15
.github/workflows/frontend_deploy.yaml
vendored
@@ -1,9 +1,10 @@
|
||||
name: Deploy Frontend
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Upstream Sync]
|
||||
types: [completed]
|
||||
push:
|
||||
paths:
|
||||
- "frontend/**"
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
@@ -38,12 +39,12 @@ jobs:
|
||||
export frontend_branch=${{ secrets.FRONTEND_BRANCH }}
|
||||
if [ -n "$frontend_branch" ]; then
|
||||
echo "Deploying branch $frontend_branch"
|
||||
pnpm run deploy:actions --project-name=$project_name
|
||||
pnpm run deploy:actions --project-name=$project_name --branch $frontend_branch
|
||||
else
|
||||
echo "Deploying branch prodcution"
|
||||
echo "Deploying branch production"
|
||||
pnpm run deploy --project-name=$project_name
|
||||
fi
|
||||
echo "Deploying prodcution for ${{ github.ref_name }}"
|
||||
echo "Deploying production for ${{ github.ref_name }}"
|
||||
echo "Deployed for tag ${{ github.ref_name }}"
|
||||
|
||||
export tg_mini_app_project_name=${{ secrets.TG_FRONTEND_NAME }}
|
||||
@@ -51,9 +52,9 @@ jobs:
|
||||
echo "Deploying telegram mini app $tg_mini_app_project_name"
|
||||
if [ -n "$frontend_branch" ]; then
|
||||
echo "Deploying telegram mini app branch $frontend_branch"
|
||||
pnpm run deploy:actions:telegram --project-name=$tg_mini_app_project_name
|
||||
pnpm run deploy:actions:telegram --project-name=$tg_mini_app_project_name --branch $frontend_branch
|
||||
else
|
||||
echo "Deploying telegram mini app branch prodcution"
|
||||
echo "Deploying telegram mini app branch production"
|
||||
pnpm run deploy:telegram --project-name=$tg_mini_app_project_name
|
||||
fi
|
||||
echo "Deployed telegram mini app for ${{ github.ref_name }}"
|
||||
|
||||
25
.github/workflows/sync.yaml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Upstream Sync
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync_latest_from_upstream:
|
||||
name: Sync latest commits from upstream repo
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.repository.fork }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Sync upstream changes
|
||||
id: sync
|
||||
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
|
||||
with:
|
||||
upstream_sync_repo: dreamhunter2333/cloudflare_temp_email
|
||||
upstream_sync_branch: main
|
||||
target_sync_branch: main
|
||||
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
|
||||
test_mode: false
|
||||
27
.github/workflows/tag_build.yml
vendored
@@ -30,7 +30,13 @@ jobs:
|
||||
run: cd frontend && pnpm install --no-frozen-lockfile && pnpm build:release
|
||||
|
||||
- name: Zip Frontend dist
|
||||
run: cd frontend/dist/ && zip -r frontend.zip *
|
||||
run: cd frontend/dist/ && zip -r frontend.zip * && mv frontend.zip ../
|
||||
|
||||
- name: Build Telegram Frontend
|
||||
run: cd frontend && pnpm install --no-frozen-lockfile && pnpm build:telegram:release
|
||||
|
||||
- name: Zip Telegram Frontend dist
|
||||
run: cd frontend/dist/ && zip -r telegram-frontend.zip * && mv telegram-frontend.zip ../
|
||||
|
||||
- name: cp wrangler.toml
|
||||
run: cd worker && cp wrangler.toml.template wrangler.toml
|
||||
@@ -38,9 +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/dist/frontend.zip
|
||||
worker/dist/worker.js
|
||||
frontend/frontend.zip
|
||||
frontend/telegram-frontend.zip
|
||||
worker/worker.js
|
||||
worker/worker-with-wasm-mail-parser.zip
|
||||
|
||||
8
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-python.vscode-pylance",
|
||||
"1yib.rust-bundle",
|
||||
"rust-lang.rust-analyzer",
|
||||
"vue.volar"
|
||||
]
|
||||
}
|
||||
114
CHANGELOG.md
@@ -1,6 +1,120 @@
|
||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||
# CHANGE LOG
|
||||
|
||||
## main(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 格式
|
||||
- feat: |Doc| 优化部署文档,补充了 `Github Actions 部署文档`,增加了 `Worker 变量说明`
|
||||
|
||||
## v0.8.5
|
||||
|
||||
- feat: |mail-parser-wasm-worker| 修复 `initSync` 函数调用时的 `deprecated` 参数警告
|
||||
- feat: rpc headers covert & typo (#559)
|
||||
- fix: telegram mail page use iframe show email (#561)
|
||||
- feat: |Worker| 增加 `REMOVE_ALL_ATTACHMENT` 和 `REMOVE_EXCEED_SIZE_ATTACHMENT` 用于移除邮件附件,由于是解析邮件的一些信息会丢失,比如图片等.
|
||||
|
||||
## v0.8.4
|
||||
|
||||
- fix: |UI| 修复 admin portal 无收件人邮箱删除调用api 错误
|
||||
- feat: |Telegram Bot| 增加 telegram bot 清理无效地址凭证命令
|
||||
- feat: 增加 worker 配置 `DISABLE_ANONYMOUS_USER_CREATE_EMAIL` 禁用匿名用户创建邮箱地址,只允许登录用户创建邮箱地址
|
||||
- feat: 增加 worker 配置 `ENABLE_ANOTHER_WORKER` 及 `ANOTHER_WORKER_LIST` ,用于调用其他 worker 的 rpc 接口 (#547)
|
||||
- feat: |UI| 自动刷新配置保存到浏览器,可配置刷新间隔
|
||||
- feat: 垃圾邮件检测增加存在时才检查的列表 `JUNK_MAIL_CHECK_LIST` 配置
|
||||
- feat: | Worker | 增加 `ParsedEmailContext` 类用于缓存解析后的邮件内容,减少解析次数
|
||||
- feat: |Github Action| Worker 部署增加 `DEBUG_MODE` 输出日志, `BACKEND_USE_MAIL_WASM_PARSER` 配置是否使用 wasm 解析邮件
|
||||
|
||||
## v0.8.3
|
||||
|
||||
- feat: |Github Action| 增加自动更新并部署功能
|
||||
- feat: |UI| admin 用户设置,支持 oauth2 配置的删除
|
||||
- feat: 增加垃圾邮件检测必须通过的列表 `JUNK_MAIL_FORCE_PASS_LIST` 配置
|
||||
|
||||
## v0.8.2
|
||||
|
||||
- fix: |Doc| 修复文档中的一些错误
|
||||
- fix: |Github Action| 修复 frontend 部署分支错误的问题
|
||||
- feat: admin 发送邮件功能
|
||||
- feat: admin 后台,账号配置页面添加无限发送邮件的地址列表
|
||||
|
||||
## v0.8.1
|
||||
|
||||
- feat: |Doc| 更新 UI 安装的文档
|
||||
- feat: |UI| 对用户隐藏邮箱账号的 ID
|
||||
- feat: |UI| 增加邮件详情页的 `转发` 按钮
|
||||
|
||||
## v0.8.0
|
||||
|
||||
- feat: |UI| 随机生成地址时不超过最大长度
|
||||
- feat: |UI| 邮件时间显示浏览器时区,可在设置中切换显示为 UTC 时间
|
||||
- feat: 支持转移邮件到其他用户
|
||||
|
||||
## v0.7.6
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
UI 部署 worker 需要点击 Settings -> Runtime, 修改 Compatibility flags, 增加 `nodejs_compat`
|
||||
|
||||

|
||||
|
||||
### Changes
|
||||
|
||||
- feat: 支持提前设置 bot info, 降低 telegram 回调延迟 (#441)
|
||||
- feat: 增加 telegram mini app 的 build 压缩包
|
||||
- feat: 增加是否启用垃圾邮件检查 `ENABLE_CHECK_JUNK_MAIL` 配置
|
||||
|
||||
## v0.7.5
|
||||
|
||||
- fix: 修复 `name` 的校验检查
|
||||
|
||||
## v0.7.4
|
||||
|
||||
- feat: UI 列表页面增加最小宽度
|
||||
- fix: 修复 `name` 的校验检查
|
||||
- fix: 修复 `DEFAULT_DOMAINS` 配置为空不生效的问题
|
||||
|
||||
## v0.7.3
|
||||
|
||||
- feat: worker 增加 `ADDRESS_CHECK_REGEX`, address name 的正则表达式, 只用于检查,符合条件将通过检查
|
||||
- fix: UI 修复登录页面 tab 激活图标错位
|
||||
- fix: UI 修复 admin 页面刷新弹框输入密码的问题
|
||||
- feat: support `Oath2` 登录, 可以通过 `Github` `Authentik` 等第三方登录, 详情查看 [OAuth2 第三方登录](https://temp-mail-docs.awsl.uk/zh/guide/feature/user-oauth2.html)
|
||||
|
||||
## v0.7.2
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
`webhook` 的结构增加了 `enabled` 字段,已经配置了的需要重新在页面开启并保存。
|
||||
|
||||
### Changes
|
||||
|
||||
- fix: worker 增加 `NO_LIMIT_SEND_ROLE` 配置, 加载失败的问题
|
||||
- feat: worker 增加 `# ADDRESS_REGEX = "[^a-z.0-9]"` 配置, 替换非法符号的正则表达式,如果不设置,默认为 [^a-z0-9], 需谨慎使用, 有些符号可能导致无法收件
|
||||
- feat: worker 优化 webhook 逻辑, 支持 admin 配置全局 webhook, 添加 `message pusher` 集成示例
|
||||
|
||||
## v0.7.1
|
||||
|
||||
- fix: 修复用户角色加载失败的问题
|
||||
|
||||
19
README.md
@@ -31,9 +31,9 @@
|
||||
|
||||
## [查看部署文档](https://temp-mail-docs.awsl.uk)
|
||||
|
||||
[](https://deploy.workers.cloudflare.com/?url=https://github.com/dreamhunter2333/cloudflare_temp_email)
|
||||
[](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html)
|
||||
|
||||
[Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/github-action.html)
|
||||
[Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html)
|
||||
|
||||
[English Docs](https://temp-mail-docs.awsl.uk/en/)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ CREATE TABLE IF NOT EXISTS raw_mails (
|
||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS address (
|
||||
id INTEGER PRIMARY KEY,
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
|
||||
3
frontend/.vscode/extensions.json
vendored
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "0.7.1",
|
||||
"version": "0.9.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -8,7 +8,9 @@
|
||||
"build": "vite build -m prod --emptyOutDir",
|
||||
"build:release": "vite build -m example --emptyOutDir",
|
||||
"build:pages": "vite build -m pages --emptyOutDir",
|
||||
"build:pages:nopwa": "VITE_PWA_DISABLED=true vite build -m pages --emptyOutDir",
|
||||
"build:telegram": "VITE_IS_TELEGRAM=true vite build -m prod --emptyOutDir",
|
||||
"build:telegram:release": "VITE_IS_TELEGRAM=true vite build -m example --emptyOutDir",
|
||||
"preview": "vite preview",
|
||||
"deploy:telegram": "npm run build:telegram && wrangler pages deploy ./dist --branch production",
|
||||
"deploy:actions:telegram": "npm run build:telegram && wrangler pages deploy ./dist",
|
||||
@@ -17,33 +19,34 @@
|
||||
"deploy:actions": "npm run build && wrangler pages deploy ./dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@simplewebauthn/browser": "^10.0.0",
|
||||
"@unhead/vue": "^1.9.15",
|
||||
"@vicons/material": "^0.12.0",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"@simplewebauthn/browser": "10.0.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.2",
|
||||
"axios": "^1.8.4",
|
||||
"jszip": "^3.10.1",
|
||||
"mail-parser-wasm": "^0.1.8",
|
||||
"naive-ui": "^2.38.2",
|
||||
"postal-mime": "^2.2.5",
|
||||
"mail-parser-wasm": "^0.2.1",
|
||||
"naive-ui": "^2.41.0",
|
||||
"postal-mime": "^2.4.3",
|
||||
"vooks": "^0.2.12",
|
||||
"vue": "^3.4.31",
|
||||
"vue": "^3.5.13",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.4.0"
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vicons/fa": "^0.12.0",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-vue-components": "^0.27.2",
|
||||
"vite": "^5.3.3",
|
||||
"vite-plugin-pwa": "^0.19.8",
|
||||
"vite-plugin-top-level-await": "^1.4.1",
|
||||
"vite-plugin-wasm": "^3.3.0",
|
||||
"workbox-window": "^7.1.0",
|
||||
"wrangler": "^3.63.1"
|
||||
"@vicons/fa": "^0.13.0",
|
||||
"@vicons/material": "^0.13.0",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"unplugin-auto-import": "^19.1.2",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"vite": "^6.2.6",
|
||||
"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": "^4.10.0"
|
||||
}
|
||||
}
|
||||
|
||||
5513
frontend/pnpm-lock.yaml
generated
@@ -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 (
|
||||
@@ -61,24 +77,36 @@ onMounted(async () => {
|
||||
<n-config-provider :locale="localeConfig" :theme="theme">
|
||||
<n-global-style />
|
||||
<n-spin description="loading..." :show="loading">
|
||||
<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">
|
||||
<div class="main">
|
||||
<n-space vertical>
|
||||
<n-layout style="min-height: 80vh;">
|
||||
<Header />
|
||||
<router-view></router-view>
|
||||
</n-layout>
|
||||
<Footer />
|
||||
</n-space>
|
||||
</div>
|
||||
</n-gi>
|
||||
<n-gi v-if="showSideMargin" span="1"></n-gi>
|
||||
</n-grid>
|
||||
<n-back-top />
|
||||
</n-message-provider>
|
||||
<n-notification-provider container-style="margin-top: 60px;">
|
||||
<n-message-provider container-style="margin-top: 20px;">
|
||||
<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;">
|
||||
<Header />
|
||||
<router-view></router-view>
|
||||
</n-layout>
|
||||
<Footer />
|
||||
</n-space>
|
||||
</div>
|
||||
</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>
|
||||
</n-notification-provider>
|
||||
</n-spin>
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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,
|
||||
@@ -21,6 +24,7 @@ const apiFetch = async (path, options = {}) => {
|
||||
method: options.method || 'GET',
|
||||
data: options.body || null,
|
||||
headers: {
|
||||
'x-lang': i18n.global.locale.value,
|
||||
'x-user-token': userJwt.value,
|
||||
'x-user-access-token': userSettings.value.access_token,
|
||||
'x-custom-auth': auth.value,
|
||||
@@ -31,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;
|
||||
@@ -52,10 +54,13 @@ const apiFetch = async (path, options = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getOpenSettings = async (message) => {
|
||||
const getOpenSettings = async (message, notification) => {
|
||||
try {
|
||||
const res = await api.fetch("/open_api/settings");
|
||||
const domainLabels = res["domainLabels"] || [];
|
||||
if (res["domains"]?.length < 1) {
|
||||
message.error("No domains found, please check your worker settings");
|
||||
}
|
||||
Object.assign(openSettings.value, {
|
||||
...res,
|
||||
title: res["title"] || "",
|
||||
@@ -72,6 +77,7 @@ const getOpenSettings = async (message) => {
|
||||
}),
|
||||
adminContact: res["adminContact"] || "",
|
||||
enableUserCreateEmail: res["enableUserCreateEmail"] || false,
|
||||
disableAnonymousUserCreateEmail: res["disableAnonymousUserCreateEmail"] || false,
|
||||
enableUserDeleteEmail: res["enableUserDeleteEmail"] || false,
|
||||
enableAutoReply: res["enableAutoReply"] || false,
|
||||
enableIndexAbout: res["enableIndexAbout"] || false,
|
||||
@@ -85,14 +91,18 @@ const getOpenSettings = async (message) => {
|
||||
}
|
||||
if (openSettings.value.announcement && openSettings.value.announcement != announcement.value) {
|
||||
announcement.value = openSettings.value.announcement;
|
||||
message.info(announcement.value, {
|
||||
showIcon: false,
|
||||
duration: 0,
|
||||
closable: true
|
||||
notification.info({
|
||||
content: () => {
|
||||
return h("div", {
|
||||
innerHTML: announcement.value
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
openSettings.value.fetched = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +129,8 @@ const getUserOpenSettings = async (message) => {
|
||||
Object.assign(userOpenSettings.value, res);
|
||||
} catch (error) {
|
||||
message.error(error.message || "fetch settings failed");
|
||||
} finally {
|
||||
userOpenSettings.value.fetched = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,11 @@ import { watch, onMounted, ref, onBeforeUnmount } from "vue";
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../store'
|
||||
import { CloudDownloadRound, ReplyFilled } from '@vicons/material'
|
||||
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()
|
||||
@@ -14,46 +16,45 @@ const props = defineProps({
|
||||
enableUserDeleteEmail: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
requried: false
|
||||
required: false
|
||||
},
|
||||
showEMailTo: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
requried: false
|
||||
required: false
|
||||
},
|
||||
fetchMailData: {
|
||||
type: Function,
|
||||
default: () => { },
|
||||
requried: true
|
||||
required: true
|
||||
},
|
||||
deleteMail: {
|
||||
type: Function,
|
||||
default: () => { },
|
||||
requried: false
|
||||
required: false
|
||||
},
|
||||
showReply: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
requried: false
|
||||
required: false
|
||||
},
|
||||
showSaveS3: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
requried: false
|
||||
required: false
|
||||
},
|
||||
saveToS3: {
|
||||
type: Function,
|
||||
default: (mail_id, filename, blob) => { },
|
||||
requried: false
|
||||
required: false
|
||||
},
|
||||
})
|
||||
|
||||
const {
|
||||
isDark, mailboxSplitSize, indexTab, loading,
|
||||
isDark, mailboxSplitSize, indexTab, loading, useUTCDate, autoRefresh, configAutoRefreshInterval,
|
||||
useIframeShowMail, sendMailModel, preferShowTextMail
|
||||
} = useGlobalState()
|
||||
const autoRefresh = ref(false)
|
||||
const autoRefreshInterval = ref(30)
|
||||
const autoRefreshInterval = ref(configAutoRefreshInterval.value)
|
||||
const data = ref([])
|
||||
const timer = ref(null)
|
||||
|
||||
@@ -85,6 +86,7 @@ const { t } = useI18n({
|
||||
delete: 'Delete',
|
||||
deleteMailTip: 'Are you sure you want to delete mail?',
|
||||
reply: 'Reply',
|
||||
forwardMail: 'Forward',
|
||||
showTextMail: 'Show Text Mail',
|
||||
showHtmlMail: 'Show Html Mail',
|
||||
saveToS3: 'Save to S3',
|
||||
@@ -104,6 +106,7 @@ const { t } = useI18n({
|
||||
delete: '删除',
|
||||
deleteMailTip: '确定要删除邮件吗?',
|
||||
reply: '回复',
|
||||
forwardMail: '转发',
|
||||
showTextMail: '显示纯文本邮件',
|
||||
showHtmlMail: '显示HTML邮件',
|
||||
saveToS3: '保存到S3',
|
||||
@@ -116,14 +119,16 @@ const { t } = useI18n({
|
||||
});
|
||||
|
||||
const setupAutoRefresh = async (autoRefresh) => {
|
||||
// auto refresh every 30 seconds
|
||||
autoRefreshInterval.value = 30;
|
||||
// auto refresh every configAutoRefreshInterval seconds
|
||||
autoRefreshInterval.value = configAutoRefreshInterval.value;
|
||||
if (autoRefresh) {
|
||||
clearInterval(timer.value);
|
||||
timer.value = setInterval(async () => {
|
||||
if (loading.value) return;
|
||||
autoRefreshInterval.value--;
|
||||
if (autoRefreshInterval.value <= 0) {
|
||||
autoRefreshInterval.value = 30;
|
||||
await refresh();
|
||||
autoRefreshInterval.value = configAutoRefreshInterval.value;
|
||||
await backFirstPageAndRefresh();
|
||||
}
|
||||
}, 1000)
|
||||
} else {
|
||||
@@ -134,7 +139,7 @@ const setupAutoRefresh = async (autoRefresh) => {
|
||||
|
||||
watch(autoRefresh, async (autoRefresh, old) => {
|
||||
setupAutoRefresh(autoRefresh)
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
|
||||
if (page !== oldPage || pageSize !== oldPageSize) {
|
||||
@@ -167,6 +172,11 @@ const refresh = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const backFirstPageAndRefresh = async () => {
|
||||
page.value = 1;
|
||||
await refresh();
|
||||
}
|
||||
|
||||
const clickRow = async (row) => {
|
||||
if (multiActionMode.value) {
|
||||
row.checked = !row.checked;
|
||||
@@ -214,6 +224,15 @@ const replyMail = async () => {
|
||||
indexTab.value = 'sendmail';
|
||||
};
|
||||
|
||||
const forwardMail = async () => {
|
||||
Object.assign(sendMailModel.value, {
|
||||
subject: `${t('forwardMail')}: ${curMail.value.subject}`,
|
||||
contentType: curMail.value.message ? 'html' : 'text',
|
||||
content: curMail.value.message || curMail.value.text,
|
||||
});
|
||||
indexTab.value = 'sendmail';
|
||||
};
|
||||
|
||||
const onSpiltSizeChange = (size) => {
|
||||
mailboxSplitSize.value = size;
|
||||
}
|
||||
@@ -354,7 +373,7 @@ onBeforeUnmount(() => {
|
||||
{{ t('autoRefresh') }}
|
||||
</template>
|
||||
</n-switch>
|
||||
<n-button @click="refresh" type="primary" tertiary>
|
||||
<n-button @click="backFirstPageAndRefresh" type="primary" tertiary>
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
@@ -362,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)">
|
||||
@@ -375,13 +394,17 @@ onBeforeUnmount(() => {
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${row.created_at} UTC` }}
|
||||
{{ 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>
|
||||
@@ -397,7 +420,7 @@ onBeforeUnmount(() => {
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${curMail.created_at} UTC` }}
|
||||
{{ utcToLocalDate(curMail.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
@@ -428,6 +451,12 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
{{ t('reply') }}
|
||||
</n-button>
|
||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="forwardMail">
|
||||
<template #icon>
|
||||
<n-icon :component="ForwardFilled" />
|
||||
</template>
|
||||
{{ t('forwardMail') }}
|
||||
</n-button>
|
||||
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
||||
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
||||
</n-button>
|
||||
@@ -436,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')">
|
||||
@@ -458,7 +487,7 @@ onBeforeUnmount(() => {
|
||||
{{ t('autoRefresh') }}
|
||||
</template>
|
||||
</n-switch>
|
||||
<n-button @click="refresh" tertiary size="small" type="primary">
|
||||
<n-button @click="backFirstPageAndRefresh" tertiary size="small" type="primary">
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
@@ -471,10 +500,10 @@ onBeforeUnmount(() => {
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${row.created_at} UTC` }}
|
||||
{{ 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 }}
|
||||
@@ -493,7 +522,7 @@ onBeforeUnmount(() => {
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${curMail.created_at} UTC` }}
|
||||
{{ utcToLocalDate(curMail.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
@@ -522,6 +551,12 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
{{ t('reply') }}
|
||||
</n-button>
|
||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="forwardMail">
|
||||
<template #icon>
|
||||
<n-icon :component="ForwardFilled" />
|
||||
</template>
|
||||
{{ t('forwardMail') }}
|
||||
</n-button>
|
||||
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
||||
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
||||
</n-button>
|
||||
@@ -530,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>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../store'
|
||||
import { useIsMobile } from '../utils/composables'
|
||||
import { utcToLocalDate } from '../utils';
|
||||
|
||||
const message = useMessage()
|
||||
const isMobile = useIsMobile()
|
||||
@@ -12,7 +13,7 @@ const props = defineProps({
|
||||
enableUserDeleteEmail: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
requried: false
|
||||
required: false
|
||||
},
|
||||
showEMailFrom: {
|
||||
type: Boolean,
|
||||
@@ -21,16 +22,16 @@ const props = defineProps({
|
||||
fetchMailData: {
|
||||
type: Function,
|
||||
default: () => { },
|
||||
requried: true
|
||||
required: true
|
||||
},
|
||||
deleteMail: {
|
||||
type: Function,
|
||||
default: () => { },
|
||||
requried: false
|
||||
required: false
|
||||
},
|
||||
})
|
||||
|
||||
const { isDark, mailboxSplitSize, loading } = useGlobalState()
|
||||
const { isDark, mailboxSplitSize, loading, useUTCDate } = useGlobalState()
|
||||
const data = ref([])
|
||||
|
||||
const count = ref(0)
|
||||
@@ -251,7 +252,7 @@ onMounted(async () => {
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${row.created_at} UTC` }}
|
||||
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailFrom" type="info">
|
||||
FROM: {{ row.address }}
|
||||
@@ -273,7 +274,7 @@ onMounted(async () => {
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${curMail.created_at} UTC` }}
|
||||
{{ utcToLocalDate(curMail.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.address }}
|
||||
@@ -320,7 +321,7 @@ onMounted(async () => {
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${row.created_at} UTC` }}
|
||||
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailFrom" type="info">
|
||||
FROM: {{ row.address }}
|
||||
@@ -342,7 +343,7 @@ onMounted(async () => {
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${curMail.created_at} UTC` }}
|
||||
{{ utcToLocalDate(curMail.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.address }}
|
||||
|
||||
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>
|
||||
179
frontend/src/components/WebhookComponent.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps({
|
||||
fetchData: {
|
||||
type: Function,
|
||||
default: () => { },
|
||||
required: true
|
||||
},
|
||||
saveSettings: {
|
||||
type: Function,
|
||||
default: (webhookSettings: WebhookSettings) => { },
|
||||
required: true
|
||||
},
|
||||
testSettings: {
|
||||
type: Function,
|
||||
default: (webhookSettings: WebhookSettings) => { },
|
||||
required: true
|
||||
},
|
||||
})
|
||||
|
||||
// @ts-ignore
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
successTip: 'Success',
|
||||
test: 'Test',
|
||||
save: 'Save',
|
||||
notEnabled: 'Webhook is not enabled for you',
|
||||
urlMissing: 'URL is required',
|
||||
enable: 'Enable',
|
||||
messagePusherDemo: 'Fill with Message Pusher Demo',
|
||||
messagePusherDoc: 'Message Pusher Doc',
|
||||
fillInDemoTip: 'Please modify the URL and other settings to your own',
|
||||
},
|
||||
zh: {
|
||||
successTip: '成功',
|
||||
test: '测试',
|
||||
save: '保存',
|
||||
notEnabled: 'Webhook 未开启,请联系管理员开启',
|
||||
urlMissing: 'URL 不能为空',
|
||||
enable: '启用',
|
||||
messagePusherDemo: '填入MessagePusher示例',
|
||||
messagePusherDoc: 'MessagePusher文档',
|
||||
fillInDemoTip: '请修改URL和其他设置为您自己的配置',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class WebhookSettings {
|
||||
enabled: boolean = false
|
||||
url: string = ''
|
||||
method: string = 'POST'
|
||||
headers: string = JSON.stringify({}, null, 2)
|
||||
body: string = JSON.stringify({}, null, 2)
|
||||
}
|
||||
|
||||
const messagePusherDocLink = "https://github.com/songquanpeng/message-pusher";
|
||||
|
||||
const messagePusherDemo = {
|
||||
enabled: true,
|
||||
url: 'https://msgpusher.com/push/username',
|
||||
method: 'POST',
|
||||
headers: JSON.stringify({
|
||||
'Content-Type': 'application/json',
|
||||
}, null, 2),
|
||||
body: JSON.stringify({
|
||||
"token": "token",
|
||||
"title": "${subject}",
|
||||
"description": "${subject}",
|
||||
"content": "*${subject}*\n\nFrom: ${from}\nTo: ${to}\n\n${parsedText}\n"
|
||||
}, null, 2),
|
||||
} as WebhookSettings;
|
||||
|
||||
const fillMessagePuhserDemo = () => {
|
||||
Object.assign(webhookSettings.value, messagePusherDemo)
|
||||
message.success(t('fillInDemoTip'))
|
||||
}
|
||||
|
||||
const webhookSettings = ref<WebhookSettings>(new WebhookSettings())
|
||||
const enableWebhook = ref(false)
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await props.fetchData()
|
||||
Object.assign(webhookSettings.value, res)
|
||||
enableWebhook.value = true
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const saveSettings = async () => {
|
||||
if (!webhookSettings.value.url) {
|
||||
message.error(t('urlMissing'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
await props.saveSettings(webhookSettings.value)
|
||||
message.success(t('successTip'))
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const testSettings = async () => {
|
||||
if (!webhookSettings.value.url) {
|
||||
message.error(t('urlMissing'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
await props.testSettings(webhookSettings.value)
|
||||
message.success(t('successTip'))
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded v-if="enableWebhook" style="max-width: 800px; overflow: auto;">
|
||||
<n-flex justify="end">
|
||||
<n-button tag="a" :href="messagePusherDocLink" target="_blank" secondary>
|
||||
{{ t('messagePusherDoc') }}
|
||||
</n-button>
|
||||
<n-button @click="fillMessagePuhserDemo" secondary>
|
||||
{{ t('messagePusherDemo') }}
|
||||
</n-button>
|
||||
<n-button v-if="webhookSettings.enabled" @click="testSettings" secondary>
|
||||
{{ t('test') }}
|
||||
</n-button>
|
||||
<n-button @click="saveSettings" type="primary">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-form-item-row :label="t('enable')">
|
||||
<n-switch v-model:value="webhookSettings.enabled" :round="false" />
|
||||
</n-form-item-row>
|
||||
<div v-if="webhookSettings.enabled">
|
||||
<n-form-item-row label="URL">
|
||||
<n-input v-model:value="webhookSettings.url" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="METHOD">
|
||||
<n-select v-model:value="webhookSettings.method" tag :options='[
|
||||
{ label: "POST", value: "POST" }
|
||||
]' />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="HEADERS">
|
||||
<n-input v-model:value="webhookSettings.headers" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="BODY">
|
||||
<n-input v-model:value="webhookSettings.body" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
</n-form-item-row>
|
||||
</div>
|
||||
</n-card>
|
||||
<n-result v-else status="404" :title="t('notEnabled')" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
8
frontend/src/constant/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
const COMMOM_MAIL = [
|
||||
"gmail.com", "163.com", "126.com", "qq.com", "outlook.com", "hotmail.com",
|
||||
"icloud.com", "yahoo.com", "foxmail.com"
|
||||
]
|
||||
|
||||
export default {
|
||||
COMMOM_MAIL
|
||||
}
|
||||
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,31 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import router from './router'
|
||||
import { registerSW } from 'virtual:pwa-register'
|
||||
import { createHead } from '@unhead/vue'
|
||||
|
||||
registerSW({ immediate: true })
|
||||
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)
|
||||
|
||||
15
frontend/src/models/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type UserOauth2Settings = {
|
||||
name: string;
|
||||
clientID: string;
|
||||
clientSecret: string;
|
||||
authorizationURL: string;
|
||||
accessTokenURL: string;
|
||||
accessTokenFormat?: string;
|
||||
userInfoURL: string;
|
||||
redirectURL: string;
|
||||
logoutURL?: string;
|
||||
userEmailKey: string;
|
||||
scope: string;
|
||||
enableMailAllowList?: boolean | undefined;
|
||||
mailAllowList?: string[] | undefined;
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
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(),
|
||||
routes: [
|
||||
@@ -16,6 +20,11 @@ const router = createRouter({
|
||||
alias: "/:lang/user",
|
||||
component: User
|
||||
},
|
||||
{
|
||||
path: '/user/oauth2/callback',
|
||||
alias: "/:lang/user/oauth2/callback",
|
||||
component: UserOauth2Callback
|
||||
},
|
||||
{
|
||||
path: '/admin',
|
||||
alias: "/:lang/admin",
|
||||
@@ -32,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
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { createGlobalState, useStorage, useDark, useToggle, useLocalStorage } from '@vueuse/core'
|
||||
import {
|
||||
createGlobalState, useStorage, useDark, useToggle,
|
||||
useLocalStorage, useSessionStorage
|
||||
} from '@vueuse/core'
|
||||
|
||||
export const useGlobalState = createGlobalState(
|
||||
() => {
|
||||
@@ -8,12 +11,15 @@ export const useGlobalState = createGlobalState(
|
||||
const loading = ref(false);
|
||||
const announcement = useLocalStorage('announcement', '');
|
||||
const openSettings = ref({
|
||||
fetched: false,
|
||||
title: '',
|
||||
announcement: '',
|
||||
prefix: '',
|
||||
addressRegex: '',
|
||||
needAuth: false,
|
||||
adminContact: '',
|
||||
enableUserCreateEmail: false,
|
||||
disableAnonymousUserCreateEmail: false,
|
||||
enableUserDeleteEmail: false,
|
||||
enableAutoReply: false,
|
||||
enableIndexAbout: false,
|
||||
@@ -26,6 +32,7 @@ export const useGlobalState = createGlobalState(
|
||||
enableWebhook: false,
|
||||
isS3Enabled: false,
|
||||
showGithub: true,
|
||||
disableAdminPasswordCheck: false,
|
||||
})
|
||||
const settings = ref({
|
||||
fetched: false,
|
||||
@@ -39,7 +46,7 @@ export const useGlobalState = createGlobalState(
|
||||
name: '',
|
||||
}
|
||||
});
|
||||
const sendMailModel = useStorage('sendMailModel', {
|
||||
const sendMailModel = useSessionStorage('sendMailModel', {
|
||||
fromName: "",
|
||||
toName: "",
|
||||
toMail: "",
|
||||
@@ -53,20 +60,26 @@ export const useGlobalState = createGlobalState(
|
||||
const auth = useStorage('auth', '');
|
||||
const adminAuth = useStorage('adminAuth', '');
|
||||
const jwt = useStorage('jwt', '');
|
||||
const adminTab = ref("account");
|
||||
const adminTab = useSessionStorage('adminTab', "account");
|
||||
const adminMailTabAddress = ref("");
|
||||
const adminSendBoxTabAddress = ref("");
|
||||
const mailboxSplitSize = useStorage('mailboxSplitSize', 0.25);
|
||||
const useIframeShowMail = useStorage('useIframeShowMail', false);
|
||||
const preferShowTextMail = useStorage('preferShowTextMail', false);
|
||||
const userJwt = useStorage('userJwt', '');
|
||||
const userTab = useStorage('userTab', 'user_settings');
|
||||
const indexTab = useStorage('indexTab', 'mailbox');
|
||||
const userTab = useSessionStorage('userTab', 'address_management');
|
||||
const indexTab = useSessionStorage('indexTab', 'mailbox');
|
||||
const globalTabplacement = useStorage('globalTabplacement', 'top');
|
||||
const useSideMargin = useStorage('useSideMargin', true);
|
||||
const useUTCDate = useStorage('useUTCDate', false);
|
||||
const autoRefresh = useStorage('autoRefresh', false);
|
||||
const configAutoRefreshInterval = useStorage("configAutoRefreshInterval", 60);
|
||||
const userOpenSettings = ref({
|
||||
fetched: false,
|
||||
enable: false,
|
||||
enableMailVerify: false,
|
||||
/** @type {{ clientID: string, name: string }[]} */
|
||||
oauth2ClientIDs: [],
|
||||
});
|
||||
const userSettings = ref({
|
||||
/** @type {boolean} */
|
||||
@@ -82,9 +95,15 @@ export const useGlobalState = createGlobalState(
|
||||
/** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */
|
||||
user_role: null,
|
||||
});
|
||||
const showAdminPage = computed(() => !!adminAuth.value || userSettings.value.is_admin);
|
||||
const showAdminPage = computed(() =>
|
||||
!!adminAuth.value
|
||||
|| userSettings.value.is_admin
|
||||
|| openSettings.value.disableAdminPasswordCheck
|
||||
);
|
||||
const telegramApp = ref(window.Telegram?.WebApp || {});
|
||||
const isTelegram = ref(!!window.Telegram?.WebApp?.initData);
|
||||
const userOauth2SessionState = useSessionStorage('userOauth2SessionState', '');
|
||||
const userOauth2SessionClientID = useSessionStorage('userOauth2SessionClientID', '');
|
||||
return {
|
||||
isDark,
|
||||
toggleDark,
|
||||
@@ -112,9 +131,14 @@ export const useGlobalState = createGlobalState(
|
||||
userSettings,
|
||||
globalTabplacement,
|
||||
useSideMargin,
|
||||
useUTCDate,
|
||||
autoRefresh,
|
||||
configAutoRefreshInterval,
|
||||
telegramApp,
|
||||
isTelegram,
|
||||
showAdminPage,
|
||||
userOauth2SessionState,
|
||||
userOauth2SessionClientID,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -11,3 +11,20 @@ export const getRouterPathWithLang = (path: string, lang: string) => {
|
||||
}
|
||||
return `/${lang}${path}`;
|
||||
}
|
||||
|
||||
export const utcToLocalDate = (utcDate: string, useUTCDate: boolean) => {
|
||||
const utcDateString = `${utcDate} UTC`;
|
||||
if (useUTCDate) {
|
||||
return utcDateString;
|
||||
}
|
||||
try {
|
||||
const date = new Date(utcDateString);
|
||||
// if invalid date string
|
||||
if (isNaN(date.getTime())) return utcDateString;
|
||||
|
||||
return date.toLocaleString();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
return utcDateString;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
|
||||
import SenderAccess from './admin/SenderAccess.vue'
|
||||
import Statistics from "./admin/Statistics.vue"
|
||||
@@ -12,6 +13,7 @@ import CreateAccount from './admin/CreateAccount.vue';
|
||||
import AccountSettings from './admin/AccountSettings.vue';
|
||||
import UserManagement from './admin/UserManagement.vue';
|
||||
import UserSettings from './admin/UserSettings.vue';
|
||||
import UserOauth2Settings from './admin/UserOauth2Settings.vue';
|
||||
import Mails from './admin/Mails.vue';
|
||||
import MailsUnknow from './admin/MailsUnknow.vue';
|
||||
import About from './common/About.vue';
|
||||
@@ -19,15 +21,24 @@ import Maintenance from './admin/Maintenance.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';
|
||||
|
||||
const {
|
||||
adminAuth, showAdminAuth, adminTab, loading,
|
||||
globalTabplacement, showAdminPage
|
||||
globalTabplacement, showAdminPage, userSettings
|
||||
} = 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;
|
||||
location.reload()
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
@@ -40,61 +51,72 @@ const { t } = useI18n({
|
||||
accessHeader: 'Admin Password',
|
||||
accessTip: 'Please enter the admin password',
|
||||
mails: 'Emails',
|
||||
sendMail: 'Send Mail',
|
||||
qucickSetup: 'Quick Setup',
|
||||
account: 'Account',
|
||||
account_create: 'Create Account',
|
||||
account_settings: 'Account Settings',
|
||||
user: 'User',
|
||||
user_management: 'User Management',
|
||||
user_settings: 'User Settings',
|
||||
userOauth2Settings: 'Oauth2 Settings',
|
||||
unknow: 'Mails with unknow receiver',
|
||||
senderAccess: 'Sender Access Control',
|
||||
sendBox: 'Send Box',
|
||||
telegram: 'Telegram Bot',
|
||||
webhook: 'Webhook',
|
||||
webhookSettings: 'Webhook Settings',
|
||||
statistics: 'Statistics',
|
||||
maintenance: 'Maintenance',
|
||||
workerconfig: 'Worker Config',
|
||||
appearance: 'Appearance',
|
||||
about: 'About',
|
||||
ok: 'OK',
|
||||
mailWebhook: 'Mail Webhook',
|
||||
},
|
||||
zh: {
|
||||
accessHeader: 'Admin 密码',
|
||||
accessTip: '请输入 Admin 密码',
|
||||
mails: '邮件',
|
||||
sendMail: '发送邮件',
|
||||
qucickSetup: '快速设置',
|
||||
account: '账号',
|
||||
account_create: '创建账号',
|
||||
account_settings: '账号设置',
|
||||
user: '用户',
|
||||
user_management: '用户管理',
|
||||
user_settings: '用户设置',
|
||||
userOauth2Settings: 'Oauth2 设置',
|
||||
unknow: '无收件人邮件',
|
||||
senderAccess: '发件权限控制',
|
||||
sendBox: '发件箱',
|
||||
telegram: '电报机器人',
|
||||
webhook: 'Webhook',
|
||||
webhookSettings: 'Webhook 设置',
|
||||
statistics: '统计',
|
||||
maintenance: '维护',
|
||||
workerconfig: 'Worker 配置',
|
||||
appearance: '外观',
|
||||
about: '关于',
|
||||
ok: '确定',
|
||||
mailWebhook: '邮件 Webhook',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const showAdminPasswordModal = computed(() => !showAdminPage.value || showAdminAuth.value)
|
||||
const tmpAdminAuth = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
if (!showAdminPage.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
// make sure user_id is fetched
|
||||
if (!userSettings.value.user_id) await api.getUserSettings(message);
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-modal v-model:show="showAdminAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
||||
:title="t('accessHeader')">
|
||||
<n-modal v-model:show="showAdminPasswordModal" :closable="false" :closeOnEsc="false" :maskClosable="false"
|
||||
preset="dialog" :title="t('accessHeader')">
|
||||
<p>{{ t('accessTip') }}</p>
|
||||
<n-input v-model:value="adminAuth" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
<n-input v-model:value="tmpAdminAuth" type="password" show-password-on="click" />
|
||||
<template #action>
|
||||
<n-button @click="authFunc" type="primary" :loading="loading">
|
||||
{{ t('ok') }}
|
||||
@@ -102,8 +124,21 @@ onMounted(async () => {
|
||||
</template>
|
||||
</n-modal>
|
||||
<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="account_settings" :tab="t('account_settings')">
|
||||
<AccountSettings />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="user_settings" :tab="t('user_settings')">
|
||||
<UserSettings />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="workerconfig" :tab="t('workerconfig')">
|
||||
<WorkerConfig />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="account" :tab="t('account')">
|
||||
<n-tabs type="bar" animated>
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="account" :tab="t('account')">
|
||||
<Account />
|
||||
</n-tab-pane>
|
||||
@@ -116,34 +151,43 @@ onMounted(async () => {
|
||||
<n-tab-pane name="senderAccess" :tab="t('senderAccess')">
|
||||
<SenderAccess />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="webhook" :tab="t('webhook')">
|
||||
<n-tab-pane name="webhook" :tab="t('webhookSettings')">
|
||||
<Webhook />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="user" :tab="t('user')">
|
||||
<n-tabs type="bar" animated>
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="user_management" :tab="t('user_management')">
|
||||
<UserManagement />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="user_settings" :tab="t('user_settings')">
|
||||
<UserSettings />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="userOauth2Settings" :tab="t('userOauth2Settings')">
|
||||
<UserOauth2Settings />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="mails" :tab="t('mails')">
|
||||
<n-tabs type="bar" animated>
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="mails" :tab="t('mails')">
|
||||
<Mails />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="unknow" :tab="t('unknow')">
|
||||
<MailsUnknow />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendBox" :tab="t('sendBox')">
|
||||
<SendBox />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendMail" :tab="t('sendMail')">
|
||||
<SendMail />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="mailWebhook" :tab="t('mailWebhook')">
|
||||
<MailWebhook />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendBox" :tab="t('sendBox')">
|
||||
<SendBox />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="telegram" :tab="t('telegram')">
|
||||
<Telegram />
|
||||
</n-tab-pane>
|
||||
@@ -151,7 +195,14 @@ onMounted(async () => {
|
||||
<Statistics />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
||||
<Maintenance />
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="workerconfig" :tab="t('workerconfig')">
|
||||
<WorkerConfig />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
||||
<Maintenance />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="appearance" :tab="t('appearance')">
|
||||
<Appearance />
|
||||
|
||||
@@ -21,9 +21,14 @@ const { t } = useI18n({
|
||||
<div>
|
||||
<n-divider class="footer-divider" />
|
||||
<div style="text-align: center; padding: 20px">
|
||||
<n-text depth="3">
|
||||
{{ t('copyright') }} © 2023-{{ new Date().getFullYear() }} {{ openSettings.copyright }}
|
||||
</n-text>
|
||||
<n-space justify="center">
|
||||
<n-text depth="3">
|
||||
{{ t('copyright') }} © 2023-{{ new Date().getFullYear() }}
|
||||
</n-text>
|
||||
<n-text depth="3">
|
||||
<div v-html="openSettings.copyright"></div>
|
||||
</n-text>
|
||||
</n-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,6 +15,7 @@ import { api } from '../api'
|
||||
import { getRouterPathWithLang } from '../utils'
|
||||
|
||||
const message = useMessage()
|
||||
const notification = useNotification()
|
||||
|
||||
const {
|
||||
toggleDark, isDark, isTelegram, showAdminPage,
|
||||
@@ -125,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;
|
||||
}
|
||||
},
|
||||
@@ -213,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++;
|
||||
}
|
||||
@@ -223,7 +228,7 @@ const logoClick = async () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await api.getOpenSettings(message);
|
||||
await api.getOpenSettings(message, notification);
|
||||
// make sure user_id is fetched
|
||||
if (!userSettings.value.user_id) await api.getUserSettings(message);
|
||||
});
|
||||
@@ -260,7 +265,7 @@ onMounted(async () => {
|
||||
<n-modal v-model:show="showAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
||||
:title="t('accessHeader')">
|
||||
<p>{{ t('accessTip') }}</p>
|
||||
<n-input v-model:value="auth" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
<n-input v-model:value="auth" type="password" show-password-on="click" />
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="authFunc" type="primary">
|
||||
{{ t('ok') }}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { defineAsyncComponent, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
@@ -14,9 +15,15 @@ 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: {
|
||||
@@ -29,6 +36,8 @@ const { t } = useI18n({
|
||||
about: 'About',
|
||||
s3Attachment: 'S3 Attachment',
|
||||
saveToS3Success: 'save to s3 success',
|
||||
webhookSettings: 'Webhook Settings',
|
||||
query: 'Query',
|
||||
},
|
||||
zh: {
|
||||
mailbox: '收件箱',
|
||||
@@ -39,11 +48,18 @@ const { t } = useI18n({
|
||||
about: '关于',
|
||||
s3Attachment: 'S3附件',
|
||||
saveToS3Success: '保存到s3成功',
|
||||
webhookSettings: 'Webhook 设置',
|
||||
query: '查询',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fetchMailData = async (limit, offset) => {
|
||||
if (mailIdQuery.value > 0) {
|
||||
const singleMail = await api.fetch(`/api/mail/${mailIdQuery.value}`);
|
||||
if (singleMail) return { results: [singleMail], count: 1 };
|
||||
return { results: [], count: 0 };
|
||||
}
|
||||
return await api.fetch(`/api/mails?limit=${limit}&offset=${offset}`);
|
||||
};
|
||||
|
||||
@@ -78,6 +94,30 @@ const saveToS3 = async (mail_id, filename, blob) => {
|
||||
message.error(error.message || "save to s3 error");
|
||||
}
|
||||
}
|
||||
|
||||
const mailBoxKey = ref("")
|
||||
const mailIdQuery = ref("")
|
||||
const showMailIdQuery = ref(false)
|
||||
|
||||
const queryMail = () => {
|
||||
mailBoxKey.value = Date.now();
|
||||
}
|
||||
|
||||
watch(route, () => {
|
||||
if (!route.query.mail_id) {
|
||||
showMailIdQuery.value = false;
|
||||
mailIdQuery.value = "";
|
||||
queryMail();
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (route.query.mail_id) {
|
||||
showMailIdQuery.value = true;
|
||||
mailIdQuery.value = route.query.mail_id;
|
||||
queryMail();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -85,9 +125,17 @@ const saveToS3 = async (mail_id, filename, blob) => {
|
||||
<AddressBar />
|
||||
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
|
||||
<n-tab-pane name="mailbox" :tab="t('mailbox')">
|
||||
<MailBox :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled" :saveToS3="saveToS3"
|
||||
:enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :fetchMailData="fetchMailData"
|
||||
:deleteMail="deleteMail" />
|
||||
<div v-if="showMailIdQuery" style="margin-bottom: 10px;">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="mailIdQuery" />
|
||||
<n-button @click="queryMail" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</div>
|
||||
<MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled"
|
||||
:saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
||||
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
@@ -102,7 +150,7 @@ const saveToS3 = async (mail_id, filename, blob) => {
|
||||
<n-tab-pane v-if="openSettings.enableAutoReply" name="auto_reply" :tab="t('auto_reply')">
|
||||
<AutoReply />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableWebhook" name="webhook" :tab="t('webhook')">
|
||||
<n-tab-pane v-if="openSettings.enableWebhook" name="webhook" :tab="t('webhookSettings')">
|
||||
<Webhook />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.isS3Enabled" name="s3_attachment" :tab="t('s3Attachment')">
|
||||
|
||||
@@ -9,7 +9,7 @@ import { NButton, NMenu } from 'naive-ui';
|
||||
import { MenuFilled } from '@vicons/material'
|
||||
|
||||
const {
|
||||
showAdminAuth, loading, adminTab,
|
||||
loading, adminTab,
|
||||
adminMailTabAddress, adminSendBoxTabAddress
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
@@ -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,
|
||||
@@ -286,15 +288,17 @@ onMounted(async () => {
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||
:page-sizes="[20, 50, 100]" show-size-picker>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
<div style="overflow: auto;">
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||
:page-sizes="[20, 50, 100]" show-size-picker>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -303,4 +307,8 @@ onMounted(async () => {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.n-data-table {
|
||||
min-width: 1000px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -11,22 +11,24 @@ const message = useMessage()
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
tip: 'You can manually input the following multiple select input',
|
||||
tip: 'You can manually input the following multiple select input and enter',
|
||||
save: 'Save',
|
||||
successTip: 'Save Success',
|
||||
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
|
||||
address_block_list_placeholder: 'Please enter the keywords you want to block',
|
||||
send_address_block_list: 'Address Block Keywords for send email',
|
||||
noLimitSendAddressList: 'No Balance Limit Send Address List',
|
||||
verified_address_list: 'Verified Address List(Can send email by cf internal api)',
|
||||
fromBlockList: 'Block Keywords for receive email',
|
||||
},
|
||||
zh: {
|
||||
tip: '您可以手动输入以下多选输入框',
|
||||
tip: '您可以手动输入以下多选输入框, 回车增加',
|
||||
save: '保存',
|
||||
successTip: '保存成功',
|
||||
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
|
||||
address_block_list_placeholder: '请输入您想要屏蔽的关键词',
|
||||
send_address_block_list: '发送邮件地址屏蔽关键词',
|
||||
noLimitSendAddressList: '无余额限制发送地址列表',
|
||||
verified_address_list: '已验证地址列表(可通过 cf 内部 api 发送邮件)',
|
||||
fromBlockList: '接收邮件地址屏蔽关键词',
|
||||
}
|
||||
@@ -35,6 +37,7 @@ const { t } = useI18n({
|
||||
|
||||
const addressBlockList = ref([])
|
||||
const sendAddressBlockList = ref([])
|
||||
const noLimitSendAddressList = ref([])
|
||||
const verifiedAddressList = ref([])
|
||||
const fromBlockList = ref([])
|
||||
|
||||
@@ -45,6 +48,7 @@ const fetchData = async () => {
|
||||
sendAddressBlockList.value = res.sendBlockList || []
|
||||
verifiedAddressList.value = res.verifiedAddressList || []
|
||||
fromBlockList.value = res.fromBlockList || []
|
||||
noLimitSendAddressList.value = res.noLimitSendAddressList || []
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
@@ -59,6 +63,7 @@ const save = async () => {
|
||||
sendBlockList: sendAddressBlockList.value || [],
|
||||
verifiedAddressList: verifiedAddressList.value || [],
|
||||
fromBlockList: fromBlockList.value || [],
|
||||
noLimitSendAddressList: noLimitSendAddressList.value || [],
|
||||
})
|
||||
})
|
||||
message.success(t('successTip'))
|
||||
@@ -76,7 +81,7 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<n-alert :show-icon="false" style="margin-bottom: 10px;">
|
||||
<n-alert :show-icon="false" type="warning" style="margin-bottom: 10px;">
|
||||
{{ t("tip") }}
|
||||
</n-alert>
|
||||
<n-form-item-row :label="t('address_block_list')">
|
||||
@@ -87,6 +92,10 @@ onMounted(async () => {
|
||||
<n-select v-model:value="sendAddressBlockList" filterable multiple tag
|
||||
:placeholder="t('address_block_list_placeholder')" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('noLimitSendAddressList')">
|
||||
<n-select v-model:value="noLimitSendAddressList" filterable multiple tag
|
||||
:placeholder="t('noLimitSendAddressList')" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('verified_address_list')">
|
||||
<n-select v-model:value="verifiedAddressList" filterable multiple tag
|
||||
:placeholder="t('verified_address_list')" />
|
||||
|
||||
30
frontend/src/views/admin/MailWebhook.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
// @ts-ignore
|
||||
import { api } from '../../api'
|
||||
|
||||
// @ts-ignore
|
||||
import WebhookComponent from '../../components/WebhookComponent.vue'
|
||||
|
||||
const fetchData = async () => {
|
||||
return await api.fetch(`/admin/mail_webhook/settings`)
|
||||
}
|
||||
|
||||
const saveSettings = async (webhookSettings: any) => {
|
||||
await api.fetch(`/admin/mail_webhook/settings`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(webhookSettings),
|
||||
})
|
||||
}
|
||||
|
||||
const testSettings = async (webhookSettings: any) => {
|
||||
await api.fetch(`/admin/mail_webhook/test`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(webhookSettings),
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WebhookComponent :fetchData="fetchData" :saveSettings="saveSettings" :testSettings="testSettings" />
|
||||
</template>
|
||||
@@ -11,7 +11,7 @@ const fetchMailUnknowData = async (limit, offset) => {
|
||||
}
|
||||
|
||||
const deleteMail = async (curMailId) => {
|
||||
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
||||
await api.fetch(`/admin/mails/${curMailId}`, { method: 'DELETE' });
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
199
frontend/src/views/admin/SendMail.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<script setup>
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { onBeforeUnmount, ref, shallowRef } from 'vue'
|
||||
import { useSessionStorage } from '@vueuse/core'
|
||||
import { api } from '../../api'
|
||||
|
||||
const message = useMessage()
|
||||
const isPreview = ref(false)
|
||||
const editorRef = shallowRef()
|
||||
|
||||
const sendMailModel = useSessionStorage('sendMailByAdminModel', {
|
||||
fromName: "",
|
||||
fromMail: "",
|
||||
toName: "",
|
||||
toMail: "",
|
||||
subject: "",
|
||||
contentType: 'text',
|
||||
content: "",
|
||||
});
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
successSend: 'Please check your sendbox. If failed, please try again later.',
|
||||
fromName: 'Your Name and Address, leave Name blank to use email address',
|
||||
toName: 'Recipient Name and Address, leave Name blank to use email address',
|
||||
subject: 'Subject',
|
||||
options: 'Options',
|
||||
edit: 'Edit',
|
||||
preview: 'Preview',
|
||||
content: 'Content',
|
||||
send: 'Send',
|
||||
text: 'Text',
|
||||
html: 'HTML',
|
||||
'rich text': 'Rich Text',
|
||||
tooLarge: 'Too large file, please upload file less than 1MB.',
|
||||
},
|
||||
zh: {
|
||||
successSend: '请查看您的发件箱, 如果失败, 请检查稍后重试。',
|
||||
fromName: '你的名称和地址,名称不填写则使用邮箱地址',
|
||||
toName: '收件人名称和地址,名称不填写则使用邮箱地址',
|
||||
subject: '主题',
|
||||
options: '选项',
|
||||
edit: '编辑',
|
||||
preview: '预览',
|
||||
content: '内容',
|
||||
send: '发送',
|
||||
text: '文本',
|
||||
html: 'HTML',
|
||||
'rich text': '富文本',
|
||||
tooLarge: '文件过大, 请上传小于1MB的文件。',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const contentTypes = [
|
||||
{ label: t('text'), value: 'text' },
|
||||
{ label: t('html'), value: 'html' },
|
||||
{ label: t('rich text'), value: 'rich' },
|
||||
]
|
||||
|
||||
const send = async () => {
|
||||
try {
|
||||
await api.fetch(`/admin/send_mail`,
|
||||
{
|
||||
method: 'POST',
|
||||
body:
|
||||
JSON.stringify({
|
||||
from_name: sendMailModel.value.fromName,
|
||||
from_mail: sendMailModel.value.fromMail,
|
||||
to_name: sendMailModel.value.toName,
|
||||
to_mail: sendMailModel.value.toMail,
|
||||
subject: sendMailModel.value.subject,
|
||||
is_html: sendMailModel.value.contentType != 'text',
|
||||
content: sendMailModel.value.content,
|
||||
})
|
||||
})
|
||||
sendMailModel.value = {
|
||||
fromName: "",
|
||||
fromMail: "",
|
||||
toName: "",
|
||||
toMail: "",
|
||||
subject: "",
|
||||
contentType: 'text',
|
||||
content: "",
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
message.success(t("successSend"));
|
||||
}
|
||||
}
|
||||
|
||||
const toolbarConfig = {
|
||||
excludeKeys: ["uploadVideo"]
|
||||
}
|
||||
|
||||
const editorConfig = {
|
||||
MENU_CONF: {
|
||||
'uploadImage': {
|
||||
async customUpload() {
|
||||
message.error(t('tooLarge'))
|
||||
},
|
||||
maxFileSize: 1 * 1024 * 1024,
|
||||
base64LimitSize: 1 * 1024 * 1024,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value
|
||||
if (editor == null) return
|
||||
editor.destroy()
|
||||
})
|
||||
|
||||
const handleCreated = (editor) => {
|
||||
editorRef.value = editor;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-flex justify="end">
|
||||
<n-button type="primary" @click="send">{{ t('send') }}</n-button>
|
||||
</n-flex>
|
||||
<div class="left">
|
||||
<n-form :model="sendMailModel">
|
||||
<n-form-item :label="t('fromName')" label-placement="top">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="sendMailModel.fromName" />
|
||||
<n-input v-model:value="sendMailModel.fromMail" />
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('toName')" label-placement="top">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="sendMailModel.toName" />
|
||||
<n-input v-model:value="sendMailModel.toMail" />
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('subject')" label-placement="top">
|
||||
<n-input v-model:value="sendMailModel.subject" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('options')" label-placement="top">
|
||||
<n-radio-group v-model:value="sendMailModel.contentType">
|
||||
<n-radio-button v-for="option in contentTypes" :key="option.value" :value="option.value"
|
||||
:label="option.label" />
|
||||
</n-radio-group>
|
||||
<n-button v-if="sendMailModel.contentType != 'text'" @click="isPreview = !isPreview"
|
||||
style="margin-left: 10px;">
|
||||
{{ isPreview ? t('edit') : t('preview') }}
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('content')" label-placement="top">
|
||||
<n-card :bordered="false" embedded v-if="isPreview">
|
||||
<div v-html="sendMailModel.content" />
|
||||
</n-card>
|
||||
<div v-else-if="sendMailModel.contentType == 'rich'" style="border: 1px solid #ccc">
|
||||
<Toolbar style="border-bottom: 1px solid #ccc" :defaultConfig="toolbarConfig"
|
||||
:editor="editorRef" mode="default" />
|
||||
<Editor style="height: 500px; overflow-y: hidden;" v-model="sendMailModel.content"
|
||||
:defaultConfig="editorConfig" mode="default" @onCreated="handleCreated" />
|
||||
</div>
|
||||
<n-input v-else type="textarea" v-model:value="sendMailModel.content" :autosize="{
|
||||
minRows: 3
|
||||
}" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-card {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
text-align: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.left {
|
||||
text-align: left;
|
||||
place-items: left;
|
||||
justify-content: left;
|
||||
}
|
||||
</style>
|
||||
@@ -198,15 +198,17 @@ onMounted(async () => {
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" :page-sizes="[20, 50, 100]"
|
||||
show-size-picker>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
<div style="overflow: auto;">
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" :page-sizes="[20, 50, 100]"
|
||||
show-size-picker>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -215,4 +217,8 @@ onMounted(async () => {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.n-data-table {
|
||||
min-width: 700px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -17,7 +17,7 @@ const { t } = useI18n({
|
||||
status: 'Check Status',
|
||||
enableTelegramAllowList: 'Enable Telegram Allow List(Manually input user ID)',
|
||||
enable: 'Enable',
|
||||
telegramAllowList: 'Telegram Allow List',
|
||||
telegramAllowList: 'Telegram Allow List(Manually input telegram user ID)',
|
||||
save: 'Save',
|
||||
miniAppUrl: 'Telegram Mini App URL',
|
||||
enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram user ID)',
|
||||
@@ -27,12 +27,12 @@ const { t } = useI18n({
|
||||
init: '初始化',
|
||||
successTip: '成功',
|
||||
status: '查看状态',
|
||||
enableTelegramAllowList: '启用 Telegram 白名单(手动输入用户 ID)',
|
||||
enableTelegramAllowList: '启用 Telegram 白名单(手动输入用户 ID, 回车增加)',
|
||||
enable: '启用',
|
||||
telegramAllowList: 'Telegram 白名单',
|
||||
telegramAllowList: 'Telegram 白名单(手动输入用户 ID, 回车增加)',
|
||||
save: '保存',
|
||||
miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)',
|
||||
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram 用户 ID)',
|
||||
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram 用户 ID, 回车增加)',
|
||||
globalMailPushList: '全局邮件推送用户列表',
|
||||
}
|
||||
}
|
||||
|
||||
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,27 +393,32 @@ 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>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||
:page-sizes="[20, 50, 100]" show-size-picker>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
<template #suffix>
|
||||
<n-button @click="showCreateUser = true" size="small" tertiary type="primary"
|
||||
style="margin-left: 10px">
|
||||
{{ t('createUser') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-pagination>
|
||||
<div style="overflow: auto;">
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||
:page-sizes="[20, 50, 100]" show-size-picker>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
<template #suffix>
|
||||
<n-button @click="showCreateUser = true" size="small" tertiary type="primary"
|
||||
style="margin-left: 10px">
|
||||
{{ t('createUser') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -391,4 +427,8 @@ onMounted(async () => {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.n-data-table {
|
||||
min-width: 800px;
|
||||
}
|
||||
</style>
|
||||
|
||||
266
frontend/src/views/admin/UserOauth2Settings.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// @ts-ignore
|
||||
import { useGlobalState } from '../../store'
|
||||
// @ts-ignore
|
||||
import { api } from '../../api'
|
||||
import constant from '../../constant'
|
||||
import { UserOauth2Settings } from '../../models';
|
||||
|
||||
const { loading } = useGlobalState()
|
||||
// @ts-ignore
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
save: 'Save',
|
||||
delete: 'Delete',
|
||||
successTip: 'Save Success',
|
||||
enable: 'Enable',
|
||||
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
||||
mailAllowList: 'Mail Address Allow List',
|
||||
addOauth2: 'Add Oauth2',
|
||||
name: 'Name',
|
||||
oauth2Type: 'Oauth2 Type',
|
||||
tip: 'Third-party login will automatically use the user\'s email to register an account (the same email will be regarded as the same account), this account is the same as the registered account, and you can also set the password through the forget password',
|
||||
},
|
||||
zh: {
|
||||
save: '保存',
|
||||
delete: '删除',
|
||||
successTip: '保存成功',
|
||||
enable: '启用',
|
||||
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
|
||||
mailAllowList: '邮件地址白名单',
|
||||
addOauth2: '添加 Oauth2',
|
||||
name: '名称',
|
||||
oauth2Type: 'Oauth2 类型',
|
||||
tip: '第三方登录会自动使用用户邮箱注册账号(邮箱相同将视为同一账号), 此账号和注册的账号相同, 也可以通过忘记密码设置密码',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mailAllowOptions = constant.COMMOM_MAIL.map((item) => {
|
||||
return { label: item, value: item }
|
||||
})
|
||||
|
||||
const userOauth2Settings = ref([] as UserOauth2Settings[])
|
||||
const showAddOauth2 = ref(false)
|
||||
const newOauth2Name = ref('')
|
||||
const newOauth2Type = ref('custom')
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await api.fetch(`/admin/user_oauth2_settings`)
|
||||
Object.assign(userOauth2Settings.value, res)
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
await api.fetch(`/admin/user_oauth2_settings`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(userOauth2Settings.value)
|
||||
})
|
||||
message.success(t('successTip'))
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const addNewOauth2 = () => {
|
||||
const authorizationURL = () => {
|
||||
switch (newOauth2Type.value) {
|
||||
case 'github':
|
||||
return 'https://github.com/login/oauth/authorize'
|
||||
case 'authentik':
|
||||
return 'https://youdomain/application/o/authorize/'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
const accessTokenURL = () => {
|
||||
switch (newOauth2Type.value) {
|
||||
case 'github':
|
||||
return 'https://github.com/login/oauth/access_token'
|
||||
case 'authentik':
|
||||
return 'https://youdomain/application/o/token/'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
const accessTokenFormat = () => {
|
||||
switch (newOauth2Type.value) {
|
||||
case 'github':
|
||||
return 'json'
|
||||
case 'authentik':
|
||||
return 'urlencoded'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
const userInfoURL = () => {
|
||||
switch (newOauth2Type.value) {
|
||||
case 'github':
|
||||
return 'https://api.github.com/user'
|
||||
case 'authentik':
|
||||
return 'https://youdomain/application/o/userinfo/'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
const userEmailKey = () => {
|
||||
switch (newOauth2Type.value) {
|
||||
case 'github':
|
||||
return 'email'
|
||||
case 'authentik':
|
||||
return 'email'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
const scope = () => {
|
||||
switch (newOauth2Type.value) {
|
||||
case 'github':
|
||||
return 'user:email'
|
||||
case 'authentik':
|
||||
return 'email openid'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
userOauth2Settings.value.push({
|
||||
name: newOauth2Name.value,
|
||||
clientID: '',
|
||||
clientSecret: '',
|
||||
authorizationURL: authorizationURL(),
|
||||
accessTokenURL: accessTokenURL(),
|
||||
accessTokenFormat: accessTokenFormat(),
|
||||
userInfoURL: userInfoURL(),
|
||||
userEmailKey: userEmailKey(),
|
||||
redirectURL: `${window.location.origin}/user/oauth2/callback`,
|
||||
logoutURL: '',
|
||||
scope: scope(),
|
||||
enableMailAllowList: false,
|
||||
mailAllowList: constant.COMMOM_MAIL
|
||||
} as UserOauth2Settings)
|
||||
newOauth2Name.value = ''
|
||||
showAddOauth2.value = false
|
||||
}
|
||||
|
||||
const accessTokenFormatOptions = [
|
||||
{ label: 'json', value: 'json' },
|
||||
{ label: 'urlencoded', value: 'urlencoded' },
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-modal v-model:show="showAddOauth2" preset="dialog" :title="t('addOauth2')">
|
||||
<n-form>
|
||||
<n-form-item-row :label="t('name')" required>
|
||||
<n-input v-model:value="newOauth2Name" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('oauth2Type')" required>
|
||||
<n-radio-group v-model:value="newOauth2Type">
|
||||
<n-radio-button value="github" label="Github" />
|
||||
<n-radio-button value="authentik" label="Authentik" />
|
||||
<n-radio-button value="custom" label="Custom" />
|
||||
</n-radio-group>
|
||||
</n-form-item-row>
|
||||
</n-form>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="addNewOauth2" size="small" tertiary type="primary">
|
||||
{{ t('addOauth2') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<n-alert :show-icon="false" type="warning" closable style="margin-bottom: 10px;">
|
||||
{{ t("tip") }}
|
||||
</n-alert>
|
||||
<n-flex justify="end">
|
||||
<n-button @click="showAddOauth2 = true" secondary :loading="loading">
|
||||
{{ t('addOauth2') }}
|
||||
</n-button>
|
||||
<n-button @click="save" type="primary" :loading="loading">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-divider />
|
||||
<n-collapse default-expanded-names="1" accordion :trigger-areas="['main', 'arrow']">
|
||||
<n-collapse-item v-for="(item, index) in userOauth2Settings" :key="index" :title="item.name">
|
||||
<template #header-extra>
|
||||
<n-popconfirm @positive-click="userOauth2Settings.splice(index, 1)">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error">
|
||||
{{ t('delete') }}
|
||||
</n-button>
|
||||
</template>
|
||||
{{ t('delete') }}
|
||||
</n-popconfirm>
|
||||
</template>
|
||||
<n-form :model="item">
|
||||
<n-form-item-row :label="t('name')" required>
|
||||
<n-input v-model:value="item.name" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="Client ID" required>
|
||||
<n-input v-model:value="item.clientID" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="Client Secret" required>
|
||||
<n-input v-model:value="item.clientSecret" type="password" show-password-on="click" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="Authorization URL" required>
|
||||
<n-input v-model:value="item.authorizationURL" />
|
||||
</n-form-item-row>
|
||||
<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-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 (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>
|
||||
<n-input v-model:value="item.redirectURL" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="Scope" required>
|
||||
<n-input v-model:value="item.scope" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('enableMailAllowList')">
|
||||
<n-input-group>
|
||||
<n-checkbox v-model:checked="item.enableMailAllowList" style="width: 20%;">
|
||||
{{ t('enable') }}
|
||||
</n-checkbox>
|
||||
<n-select v-model:value="item.mailAllowList" v-if="item.enableMailAllowList" filterable
|
||||
multiple tag style="width: 80%;" :options="mailAllowOptions"
|
||||
:placeholder="t('mailAllowList')" />
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
</n-form>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -28,7 +28,7 @@ const { t } = useI18n({
|
||||
enableUserRegister: "允许用户注册",
|
||||
enableMailVerify: '启用邮件验证(发送地址必须是系统中能有余额且能正常发送邮件的地址)',
|
||||
verifyMailSender: '验证邮件发送地址',
|
||||
enableMailAllowList: '启用邮件地址白名单(可手动输入)',
|
||||
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
|
||||
mailAllowList: '邮件地址白名单',
|
||||
maxAddressCount: '可绑定最大邮箱地址数量',
|
||||
}
|
||||
@@ -92,8 +92,8 @@ onMounted(async () => {
|
||||
<n-checkbox v-model:checked="userSettings.enableMailVerify" style="width: 20%;">
|
||||
{{ t('enable') }}
|
||||
</n-checkbox>
|
||||
<n-input v-model:value="userSettings.verifyMailSender" style="width: 80%;"
|
||||
:placeholder="t('verifyMailSender')" />
|
||||
<n-input v-model:value="userSettings.verifyMailSender" v-if="userSettings.enableMailVerify"
|
||||
style="width: 80%;" :placeholder="t('verifyMailSender')" />
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('enableMailAllowList')">
|
||||
@@ -101,8 +101,9 @@ onMounted(async () => {
|
||||
<n-checkbox v-model:checked="userSettings.enableMailAllowList" style="width: 20%;">
|
||||
{{ t('enable') }}
|
||||
</n-checkbox>
|
||||
<n-select v-model:value="userSettings.mailAllowList" filterable multiple tag style="width: 80%;"
|
||||
:options="mailAllowOptions" :placeholder="t('mailAllowList')" />
|
||||
<n-select v-model:value="userSettings.mailAllowList" v-if="userSettings.enableMailAllowList"
|
||||
filterable multiple tag style="width: 80%;" :options="mailAllowOptions"
|
||||
:placeholder="t('mailAllowList')" />
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('maxAddressCount')">
|
||||
|
||||
@@ -13,13 +13,15 @@ const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
successTip: 'Success',
|
||||
webhookAllowList: 'Webhook Allow List(Enter the address that is allowed to use webhook)',
|
||||
webhookAllowList: 'Webhook Allow List(Enter the address that is allowed to use webhook and enter)',
|
||||
save: 'Save',
|
||||
notEnabled: 'Webhook is not enabled',
|
||||
},
|
||||
zh: {
|
||||
successTip: '成功',
|
||||
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的地址)',
|
||||
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的地址, 回车增加)',
|
||||
save: '保存',
|
||||
notEnabled: 'Webhook 未开启',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -33,13 +35,16 @@ class WebhookSettings {
|
||||
}
|
||||
|
||||
const webhookSettings = ref(new WebhookSettings([]))
|
||||
const webhookEnabled = ref(false)
|
||||
const errorInfo = ref('')
|
||||
|
||||
const getSettings = async () => {
|
||||
try {
|
||||
const res = await api.fetch(`/admin/webhook/settings`)
|
||||
Object.assign(webhookSettings.value, res)
|
||||
webhookEnabled.value = true
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
errorInfo.value = (error as Error).message || "error";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +67,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||
<n-card v-if="webhookEnabled" :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||
<n-form-item-row :label="t('webhookAllowList')">
|
||||
<n-select v-model:value="webhookSettings.allowList" filterable multiple tag
|
||||
:placeholder="t('webhookAllowList')" />
|
||||
@@ -71,6 +76,7 @@ onMounted(async () => {
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-card>
|
||||
<n-result v-else status="404" :title="t('notEnabled')" :description="errorInfo" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
42
frontend/src/views/admin/WorkerConfig.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { loading } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const settings = ref({})
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await api.fetch(`/admin/worker/configs`)
|
||||
Object.assign(settings.value, res)
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded style="max-width: 600px; overflow: auto;">
|
||||
<pre>{{ JSON.stringify(settings, null, 2) }}</pre>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup>
|
||||
import { GithubAlt, Discord, Telegram } from '@vicons/fa'
|
||||
import { useGlobalState } from '../../store'
|
||||
const { announcement } = useGlobalState()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded>
|
||||
<div v-html="announcement"></div>
|
||||
<n-button tag="a" target="_blank" href="https://github.com/dreamhunter2333/cloudflare_temp_email">
|
||||
<template #icon>
|
||||
<n-icon :component="GithubAlt" />
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useIsMobile } from '../../utils/composables'
|
||||
import { useGlobalState } from '../../store'
|
||||
|
||||
const {
|
||||
mailboxSplitSize, useIframeShowMail, preferShowTextMail,
|
||||
globalTabplacement, useSideMargin
|
||||
mailboxSplitSize, useIframeShowMail, preferShowTextMail, configAutoRefreshInterval,
|
||||
globalTabplacement, useSideMargin, useUTCDate
|
||||
} = useGlobalState()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
@@ -22,6 +22,8 @@ const { t } = useI18n({
|
||||
top: 'top',
|
||||
right: 'right',
|
||||
bottom: 'bottom',
|
||||
useUTCDate: 'Use UTC Date',
|
||||
autoRefreshInterval: 'Auto Refresh Interval(Sec)',
|
||||
},
|
||||
zh: {
|
||||
mailboxSplitSize: '邮箱界面分栏大小',
|
||||
@@ -33,6 +35,8 @@ const { t } = useI18n({
|
||||
top: '顶部',
|
||||
right: '右侧',
|
||||
bottom: '底部',
|
||||
useUTCDate: '使用 UTC 时间',
|
||||
autoRefreshInterval: '自动刷新间隔(秒)',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -48,12 +52,20 @@ const { t } = useI18n({
|
||||
0.75: '0.75'
|
||||
}" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('autoRefreshInterval')">
|
||||
<n-slider v-model:value="configAutoRefreshInterval" :min="30" :max="300" :step="1" :marks="{
|
||||
60: '60', 120: '120', 180: '180', 240: '240'
|
||||
}" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('preferShowTextMail')">
|
||||
<n-switch v-model:value="preferShowTextMail" :round="false" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('useIframeShowMail')">
|
||||
<n-switch v-model:value="useIframeShowMail" :round="false" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('useUTCDate')">
|
||||
<n-switch v-model:value="useUTCDate" :round="false" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row v-if="!isMobile" :label="t('useSideMargin')">
|
||||
<n-switch v-model:value="useSideMargin" :round="false" />
|
||||
</n-form-item-row>
|
||||
|
||||
@@ -15,7 +15,7 @@ const props = defineProps({
|
||||
bindUserAddress: {
|
||||
type: Function,
|
||||
default: async () => { await api.bindUserAddress(); },
|
||||
requried: true
|
||||
required: true
|
||||
},
|
||||
newAddressPath: {
|
||||
type: Function,
|
||||
@@ -29,11 +29,12 @@ const props = defineProps({
|
||||
}),
|
||||
});
|
||||
},
|
||||
requried: true
|
||||
required: true
|
||||
},
|
||||
})
|
||||
|
||||
const message = useMessage()
|
||||
const notification = useNotification()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
@@ -70,9 +71,10 @@ 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 a-z and 0-9',
|
||||
getNewEmailTip1: 'Please input the email you want to use. only allow: ',
|
||||
getNewEmailTip2: 'Levaing it blank will generate a random email address.',
|
||||
getNewEmailTip3: 'You can choose a domain from the dropdown list.',
|
||||
credential: 'Email Address Credential',
|
||||
@@ -85,9 +87,10 @@ const { locale, t } = useI18n({
|
||||
},
|
||||
zh: {
|
||||
login: '登录',
|
||||
loginAndBind: '登录并绑定',
|
||||
pleaseGetNewEmail: '请"登录"或点击 "注册新邮箱" 按钮来获取一个新的邮箱地址',
|
||||
getNewEmail: '创建新邮箱',
|
||||
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 a-z, 0-9',
|
||||
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许: ',
|
||||
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
||||
getNewEmailTip3: '你可以从下拉列表中选择一个域名。',
|
||||
credential: '邮箱地址凭据',
|
||||
@@ -101,6 +104,25 @@ 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) {
|
||||
return new RegExp(openSettings.value.addressRegex, 'g');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error(`Invalid addressRegex: ${openSettings.value.addressRegex}`);
|
||||
}
|
||||
return /[^a-z0-9]/g;
|
||||
});
|
||||
|
||||
const generateNameLoading = ref(false);
|
||||
const generateName = async () => {
|
||||
try {
|
||||
@@ -110,8 +132,12 @@ const generateName = async () => {
|
||||
.split('@')[0]
|
||||
.replace(/\s+/g, '.')
|
||||
.replace(/\.{2,}/g, '.')
|
||||
.replace(/[^a-z0-9]/g, '')
|
||||
.replace(addressRegex.value, '')
|
||||
.toLowerCase();
|
||||
// support maxAddressLen
|
||||
if (emailName.value.length > openSettings.value.maxAddressLen) {
|
||||
emailName.value = emailName.value.slice(0, openSettings.value.maxAddressLen);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
@@ -168,9 +194,18 @@ const domainsOptions = computed(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const showNewAddressTab = computed(() => {
|
||||
if (openSettings.value.disableAnonymousUserCreateEmail
|
||||
&& !userSettings.value.user_email
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return openSettings.value.enableUserCreateEmail;
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (!openSettings.value.domains || openSettings.value.domains.length === 0) {
|
||||
await api.getOpenSettings();
|
||||
await api.getOpenSettings(message, notification);
|
||||
}
|
||||
emailDomain.value = domainsOptions.value ? domainsOptions.value[0]?.value : "";
|
||||
});
|
||||
@@ -181,8 +216,8 @@ onMounted(async () => {
|
||||
<n-alert v-if="userSettings.user_email" :show-icon="false" :bordered="false" closable>
|
||||
<span>{{ t('bindUserInfo') }}</span>
|
||||
</n-alert>
|
||||
<n-tabs v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||
<n-tab-pane name="signin" :tab="t('login')">
|
||||
<n-tabs v-if="openSettings.fetched" v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||
<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 }" />
|
||||
@@ -191,10 +226,9 @@ onMounted(async () => {
|
||||
<template #icon>
|
||||
<n-icon :component="EmailOutlined" />
|
||||
</template>
|
||||
{{ t('login') }}
|
||||
{{ loginAndBindTag }}
|
||||
</n-button>
|
||||
<n-button v-if="openSettings.enableUserCreateEmail" @click="tabValue = 'register'" block secondary
|
||||
strong>
|
||||
<n-button v-if="showNewAddressTab" @click="tabValue = 'register'" block secondary strong>
|
||||
<template #icon>
|
||||
<n-icon :component="NewLabelOutlined" />
|
||||
</template>
|
||||
@@ -202,11 +236,11 @@ onMounted(async () => {
|
||||
</n-button>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableUserCreateEmail" name="register" :tab="t('getNewEmail')">
|
||||
<n-tab-pane v-if="showNewAddressTab" name="register" :tab="t('getNewEmail')">
|
||||
<n-spin :show="generateNameLoading">
|
||||
<n-form>
|
||||
<span>
|
||||
<p>{{ t("getNewEmailTip1") }}</p>
|
||||
<p>{{ t("getNewEmailTip1") + addressRegex.source }}</p>
|
||||
<p>{{ t("getNewEmailTip2") }}</p>
|
||||
<p>{{ t("getNewEmailTip3") }}</p>
|
||||
</span>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,7 +13,7 @@ const isPreview = ref(false)
|
||||
const editorRef = shallowRef()
|
||||
|
||||
|
||||
const { settings, sendMailModel, indexTab } = useGlobalState()
|
||||
const { settings, sendMailModel, indexTab, userSettings } = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: 'zh',
|
||||
@@ -136,6 +136,8 @@ const handleCreated = (editor) => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// make sure user_id is fetched
|
||||
if (!userSettings.value.user_id) await api.getUserSettings(message);
|
||||
await api.getSettings();
|
||||
})
|
||||
</script>
|
||||
@@ -149,16 +151,15 @@ onMounted(async () => {
|
||||
<n-button type="primary" tertiary @click="requestAccess" size="small">{{ t('requestAccess')
|
||||
}}</n-button>
|
||||
</n-alert>
|
||||
<br />
|
||||
<AdminContact />
|
||||
</div>
|
||||
<div v-else>
|
||||
<n-alert type="info" :show-icon="false" :bordered="false">
|
||||
<n-alert type="info" :show-icon="false" :bordered="false" closable>
|
||||
{{ t('send_balance') }}: {{ settings.send_balance }}
|
||||
</n-alert>
|
||||
<div class="right">
|
||||
<n-flex justify="end">
|
||||
<n-button type="primary" @click="send">{{ t('send') }}</n-button>
|
||||
</div>
|
||||
</n-flex>
|
||||
<div class="left">
|
||||
<n-form :model="sendMailModel">
|
||||
<n-form-item :label="t('fromName')" label-placement="top">
|
||||
@@ -230,9 +231,7 @@ onMounted(async () => {
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.right {
|
||||
text-align: right;
|
||||
place-items: right;
|
||||
justify-content: right;
|
||||
.n-alert {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,129 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
// @ts-ignore
|
||||
import { useGlobalState } from '../../store'
|
||||
// @ts-ignore
|
||||
import { api } from '../../api'
|
||||
|
||||
const { settings } = useGlobalState()
|
||||
// @ts-ignore
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
successTip: 'Success',
|
||||
test: 'Test',
|
||||
save: 'Save',
|
||||
notEnabled: 'Webhook is not enabled for you',
|
||||
urlMissing: 'URL is required',
|
||||
},
|
||||
zh: {
|
||||
successTip: '成功',
|
||||
test: '测试',
|
||||
save: '保存',
|
||||
notEnabled: 'Webhook 未开启,请联系管理员开启',
|
||||
urlMissing: 'URL 不能为空',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
class WebhookSettings {
|
||||
url: string = ''
|
||||
method: string = 'POST'
|
||||
headers: string = JSON.stringify({}, null, 2)
|
||||
body: string = JSON.stringify({}, null, 2)
|
||||
}
|
||||
|
||||
const webhookSettings = ref<WebhookSettings>(new WebhookSettings())
|
||||
const enableWebhook = ref(false)
|
||||
import WebhookComponent from '../../components/WebhookComponent.vue'
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await api.fetch(`/api/webhook/settings`)
|
||||
Object.assign(webhookSettings.value, res)
|
||||
enableWebhook.value = true
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
}
|
||||
return await api.fetch(`/api/webhook/settings`)
|
||||
}
|
||||
|
||||
const saveSettings = async () => {
|
||||
if (!webhookSettings.value.url) {
|
||||
message.error(t('urlMissing'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
await api.fetch(`/api/webhook/settings`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(webhookSettings.value),
|
||||
})
|
||||
message.success(t('successTip'))
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
}
|
||||
const saveSettings = async (webhookSettings: any) => {
|
||||
await api.fetch(`/api/webhook/settings`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(webhookSettings),
|
||||
})
|
||||
}
|
||||
|
||||
const testSettings = async () => {
|
||||
if (!webhookSettings.value.url) {
|
||||
message.error(t('urlMissing'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
await api.fetch(`/api/webhook/test`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(webhookSettings.value),
|
||||
})
|
||||
message.success(t('successTip'))
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
}
|
||||
const testSettings = async (webhookSettings: any) => {
|
||||
await api.fetch(`/api/webhook/test`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(webhookSettings),
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center" v-if="settings.address">
|
||||
<n-card :bordered="false" embedded v-if="enableWebhook" style="max-width: 800px; overflow: auto;">
|
||||
<n-form-item-row label="URL">
|
||||
<n-input v-model:value="webhookSettings.url" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="METHOD">
|
||||
<n-select v-model:value="webhookSettings.method" tag :options='[
|
||||
{ label: "POST", value: "POST" }
|
||||
]' />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="HEADERS">
|
||||
<n-input v-model:value="webhookSettings.headers" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="BODY">
|
||||
<n-input v-model:value="webhookSettings.body" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
</n-form-item-row>
|
||||
<n-button @click="testSettings" secondary block strong>
|
||||
{{ t('test') }}
|
||||
</n-button>
|
||||
<n-button @click="saveSettings" type="primary" block>
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-card>
|
||||
<n-result v-else status="404" :title="t('notEnabled')" />
|
||||
</div>
|
||||
<WebhookComponent :fetchData="fetchData" :saveSettings="saveSettings" :testSettings="testSettings" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,8 +5,9 @@ import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { processItem } from '../../utils/email-parser'
|
||||
import { utcToLocalDate } from '../../utils';
|
||||
|
||||
const { telegramApp, loading } = useGlobalState()
|
||||
const { telegramApp, loading, useUTCDate } = useGlobalState()
|
||||
const route = useRoute()
|
||||
|
||||
const curMail = ref({});
|
||||
@@ -45,12 +46,12 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded v-if="curMail.message" style="max-width: 800px; overflow: auto;">
|
||||
<n-card :bordered="false" embedded v-if="curMail.message" style="max-width: 800px; height: 100%;">
|
||||
<n-tag type="info">
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
Date: {{ curMail.created_at }}
|
||||
Date: {{ utcToLocalDate(curMail.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
@@ -58,7 +59,8 @@ onMounted(async () => {
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ curMail.address }}
|
||||
</n-tag>
|
||||
<div v-html="curMail.message" style="margin-top: 10px;"></div>
|
||||
<iframe :srcdoc="curMail.message" style="margin-top: 10px;width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -70,5 +72,6 @@ onMounted(async () => {
|
||||
text-align: left;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
height: 80vh;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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()
|
||||
@@ -20,9 +22,14 @@ const { locale, t } = useI18n({
|
||||
mail_count: 'Mail Count',
|
||||
send_count: 'Send Count',
|
||||
actions: 'Actions',
|
||||
changeMailAddress: 'Change Mail Address',
|
||||
changeMailAddress: 'Change Address',
|
||||
unbindAddress: 'Unbind Address',
|
||||
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?',
|
||||
address: 'Address',
|
||||
create_or_bind: 'Create or Bind',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
@@ -30,14 +37,23 @@ const { locale, t } = useI18n({
|
||||
mail_count: '邮件数量',
|
||||
send_count: '发送数量',
|
||||
actions: '操作',
|
||||
changeMailAddress: '切换邮箱地址',
|
||||
changeMailAddress: '切换地址',
|
||||
unbindAddress: '解绑地址',
|
||||
unbindAddressTip: '解绑前请切换到此邮箱地址并保存邮箱地址凭证。',
|
||||
transferAddress: '转移地址',
|
||||
targetUserEmail: '目标用户邮箱',
|
||||
transferAddressTip: '转移地址到其他用户将会从你的账户中移除此地址并转移给其他用户。确定要转移地址吗?',
|
||||
address: '地址',
|
||||
create_or_bind: '创建或绑定',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const data = ref([])
|
||||
const showTranferAddress = ref(false)
|
||||
const currentAddress = ref("")
|
||||
const currentAddressId = ref(0)
|
||||
const targetUserEmail = ref('')
|
||||
|
||||
const changeMailAddress = async (address_id) => {
|
||||
try {
|
||||
@@ -70,15 +86,41 @@ const unbindAddress = async (address_id) => {
|
||||
}
|
||||
}
|
||||
|
||||
const transferAddress = async () => {
|
||||
if (!targetUserEmail.value) {
|
||||
message.error("targetUserEmail is required");
|
||||
return;
|
||||
}
|
||||
if (!currentAddressId.value) {
|
||||
message.error("currentAddressId is required");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api.fetch(`/user_api/transfer_address`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
address_id: currentAddressId.value,
|
||||
target_user_email: targetUserEmail.value
|
||||
})
|
||||
});
|
||||
message.success(t('transferAddress') + " " + t('success'));
|
||||
await fetchData();
|
||||
showTranferAddress.value = false;
|
||||
currentAddressId.value = 0;
|
||||
currentAddress.value = "";
|
||||
targetUserEmail.value = "";
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -86,10 +128,6 @@ const fetchData = async () => {
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "ID",
|
||||
key: "id"
|
||||
},
|
||||
{
|
||||
title: t('name'),
|
||||
key: "name"
|
||||
@@ -138,6 +176,18 @@ const columns = [
|
||||
default: () => `${t('changeMailAddress')}?`
|
||||
}
|
||||
),
|
||||
h(NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
type: "primary",
|
||||
onClick: () => {
|
||||
currentAddressId.value = row.id;
|
||||
currentAddress.value = row.name;
|
||||
showTranferAddress.value = true;
|
||||
}
|
||||
},
|
||||
{ default: () => t('transferAddress') }
|
||||
),
|
||||
h(NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => unbindAddress(row.id)
|
||||
@@ -165,6 +215,33 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
<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>
|
||||
|
||||
<style scoped>
|
||||
.n-data-table {
|
||||
min-width: 700px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { KeyFilled } from '@vicons/material'
|
||||
|
||||
import { api } from '../../api';
|
||||
import { useGlobalState } from '../../store'
|
||||
@@ -10,7 +11,10 @@ import { startAuthentication } from '@simplewebauthn/browser';
|
||||
|
||||
import Turnstile from '../../components/Turnstile.vue';
|
||||
|
||||
const { userJwt, userOpenSettings, openSettings } = useGlobalState()
|
||||
const {
|
||||
userJwt, userOpenSettings, openSettings,
|
||||
userOauth2SessionState, userOauth2SessionClientID
|
||||
} = useGlobalState()
|
||||
const message = useMessage();
|
||||
|
||||
const { t } = useI18n({
|
||||
@@ -33,6 +37,7 @@ const { t } = useI18n({
|
||||
pleaseCompleteTurnstile: 'Please complete turnstile',
|
||||
pleaseLogin: 'Please login',
|
||||
loginWithPasskey: 'Login with Passkey',
|
||||
loginWith: 'Login with {provider}',
|
||||
},
|
||||
zh: {
|
||||
login: '登录',
|
||||
@@ -52,6 +57,7 @@ const { t } = useI18n({
|
||||
pleaseCompleteTurnstile: '请完成人机验证',
|
||||
pleaseLogin: '请登录',
|
||||
loginWithPasskey: '使用 Passkey 登录',
|
||||
loginWith: '使用 {provider} 登录',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -184,6 +190,18 @@ const passkeyLogin = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const oauth2Login = async (clientID) => {
|
||||
try {
|
||||
userOauth2SessionClientID.value = clientID;
|
||||
userOauth2SessionState.value = Math.random().toString(36).substring(2);
|
||||
const res = await api.fetch(`/user_api/oauth2/login_url?clientID=${clientID}&state=${userOauth2SessionState.value}`);
|
||||
// redirect to oauth2 login page
|
||||
location.href = res.url;
|
||||
} catch (error) {
|
||||
message.error(error.message || "login failed");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
});
|
||||
@@ -191,7 +209,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-tabs v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||
<n-tabs v-model:value="tabValue" size="large" v-if="userOpenSettings.fetched" justify-content="space-evenly">
|
||||
<n-tab-pane name="signin" :tab="t('login')">
|
||||
<n-form>
|
||||
<n-form-item-row :label="t('email')" required>
|
||||
@@ -208,8 +226,15 @@ onMounted(async () => {
|
||||
</n-button>
|
||||
<n-divider />
|
||||
<n-button @click="passkeyLogin" type="primary" block secondary strong>
|
||||
<template #icon>
|
||||
<n-icon :component="KeyFilled" />
|
||||
</template>
|
||||
{{ t('loginWithPasskey') }}
|
||||
</n-button>
|
||||
<n-button @click="oauth2Login(item.clientID)" v-for="item in userOpenSettings.oauth2ClientIDs"
|
||||
:key="item.clientID" block secondary strong>
|
||||
{{ t('loginWith', { provider: item.name }) }}
|
||||
</n-button>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="userOpenSettings.enable" name="signup" :tab="t('register')">
|
||||
@@ -276,4 +301,8 @@ onMounted(async () => {
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
65
frontend/src/views/user/UserOauth2Callback.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api';
|
||||
|
||||
const {
|
||||
userJwt, userOauth2SessionState, userOauth2SessionClientID
|
||||
} = useGlobalState()
|
||||
|
||||
const message = useMessage();
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const errorInfo = ref('')
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
logging: 'Logging in...',
|
||||
stateNotMatch: 'state not match',
|
||||
},
|
||||
zh: {
|
||||
logging: '登录中...',
|
||||
stateNotMatch: 'state 不匹配',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
const state = route.query.state;
|
||||
if (state != userOauth2SessionState.value) {
|
||||
console.error('state not match');
|
||||
message.error(t('stateNotMatch'));
|
||||
return;
|
||||
}
|
||||
const code = route.query.code;
|
||||
if (!code) {
|
||||
console.error('code not found');
|
||||
message.error('code not found');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api.fetch(`/user_api/oauth2/callback`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
code: code,
|
||||
clientID: userOauth2SessionClientID.value
|
||||
})
|
||||
});
|
||||
userJwt.value = res.jwt;
|
||||
router.push('/user');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error(error.message || 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-result status="info" :title="t('logging')" :description="errorInfo">
|
||||
</n-result>
|
||||
</n-card>
|
||||
</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>
|
||||
|
||||
@@ -24,6 +24,7 @@ export default defineConfig({
|
||||
{
|
||||
'naive-ui': [
|
||||
'useMessage',
|
||||
'useNotification',
|
||||
'NButton',
|
||||
'NPopconfirm',
|
||||
'NIcon',
|
||||
@@ -35,13 +36,16 @@ export default defineConfig({
|
||||
resolvers: [NaiveUiResolver()]
|
||||
}),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
registerType: null,
|
||||
devOptions: {
|
||||
enabled: true
|
||||
enabled: false
|
||||
},
|
||||
workbox: {
|
||||
disableDevLogs: true,
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
|
||||
globPatterns: [],
|
||||
runtimeCaching: [],
|
||||
navigateFallback: null,
|
||||
cleanupOutdatedCaches: true,
|
||||
},
|
||||
manifest: {
|
||||
name: 'Temp Email',
|
||||
@@ -65,5 +69,10 @@ export default defineConfig({
|
||||
},
|
||||
define: {
|
||||
'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version),
|
||||
},
|
||||
esbuild: {
|
||||
supported: {
|
||||
'top-level-await': true
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mail-parser-wasm"
|
||||
version = "0.1.8"
|
||||
version = "0.2.1"
|
||||
edition = "2021"
|
||||
description = "A simple mail parser for wasm"
|
||||
license = "MIT"
|
||||
@@ -9,5 +9,5 @@ license = "MIT"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
mail-parser = "0.9.3"
|
||||
wasm-bindgen = "0.2.92"
|
||||
mail-parser = "0.9.4"
|
||||
wasm-bindgen = "0.2.99"
|
||||
|
||||
@@ -35,10 +35,31 @@ impl AttachmentResult {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[wasm_bindgen]
|
||||
pub struct MessageHeader {
|
||||
key: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl MessageHeader {
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn key(&self) -> String {
|
||||
self.key.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn value(&self) -> String {
|
||||
self.value.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct MessageResult {
|
||||
sender: String,
|
||||
subject: String,
|
||||
headers: Vec<MessageHeader>,
|
||||
body_html: String,
|
||||
text: String,
|
||||
attachments: Vec<AttachmentResult>,
|
||||
@@ -56,6 +77,11 @@ impl MessageResult {
|
||||
self.subject.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn headers(&self) -> Vec<MessageHeader> {
|
||||
self.headers.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn body_html(&self) -> String {
|
||||
self.body_html.clone()
|
||||
@@ -119,6 +145,7 @@ pub fn parse_message(raw_message: &str) -> MessageResult {
|
||||
return MessageResult {
|
||||
sender: String::new(),
|
||||
subject: String::new(),
|
||||
headers: Vec::new(),
|
||||
body_html: String::new(),
|
||||
text: String::new(),
|
||||
attachments: Vec::new(),
|
||||
@@ -146,6 +173,14 @@ pub fn parse_message(raw_message: &str) -> MessageResult {
|
||||
.subject()
|
||||
.map(|subject| subject.to_owned())
|
||||
.unwrap_or(String::new()),
|
||||
headers: message
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|header| MessageHeader {
|
||||
key: header.name().to_owned(),
|
||||
value: header.value().as_text().unwrap_or("").to_owned(),
|
||||
})
|
||||
.collect(),
|
||||
body_html: message
|
||||
.body_html(0)
|
||||
.map(|html| html.into_owned())
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import initAsync, { initSync, parse_message } from './mail_parser_wasm';
|
||||
import MODULE from './mail_parser_wasm_bg.wasm';
|
||||
|
||||
initSync(MODULE);
|
||||
initSync({ module: MODULE });
|
||||
|
||||
|
||||
export { initAsync, MODULE };
|
||||
export * from './mail_parser_wasm';
|
||||
export const parse_message_wrapper = (raw_message) => {
|
||||
initSync(MODULE);
|
||||
initSync({ module: MODULE });
|
||||
return parse_message(raw_message);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"url": "https://github.com/dreamhunter2333/cloudflare_temp_email",
|
||||
"directory": "mail-parser-wasm"
|
||||
},
|
||||
"version": "0.1.8",
|
||||
"version": "0.2.1",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"mail_parser_wasm_bg.wasm",
|
||||
|
||||
@@ -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": "1.0.0",
|
||||
"version": "0.9.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -11,6 +11,6 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"wrangler": "^3.62.0"
|
||||
"wrangler": "^4.10.0"
|
||||
}
|
||||
}
|
||||
|
||||
19
scripts/update-dependencies.sh
Executable file
@@ -0,0 +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 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(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
aiosmtpd==1.4.6
|
||||
pydantic-settings==2.2.1
|
||||
requests==2.32.0
|
||||
twisted==24.3.0
|
||||
twisted==24.7.0
|
||||
httpx==0.27.0
|
||||
|
||||
@@ -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: '页面导航'
|
||||
},
|
||||
|
||||
@@ -96,7 +97,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
},
|
||||
{
|
||||
text: '通过命令行部署',
|
||||
collapsed: true,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '命令行部署准备', link: 'cli/pre-requisite' },
|
||||
{ text: 'D1 数据库', link: 'cli/d1' },
|
||||
@@ -108,7 +109,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
},
|
||||
{
|
||||
text: '通过用户界面部署',
|
||||
collapsed: true,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'D1 数据库', link: 'ui/d1' },
|
||||
{ text: 'Cloudflare workers 后端', link: 'ui/worker' },
|
||||
@@ -121,12 +122,25 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
text: '通过 Github Actions 部署',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: '通过 Github Actions 部署', link: 'github-action' },
|
||||
{ text: 'Github Actions 部署准备', link: 'actions/pre-requisite' },
|
||||
{ text: 'D1 数据库', link: 'actions/d1' },
|
||||
{ text: 'Github Actions 配置', link: 'actions/github-action' },
|
||||
{ text: '配置邮件转发', link: 'email-routing.md' },
|
||||
{ text: '配置发送邮件', link: 'config-send-mail' },
|
||||
{ text: '自动更新配置', link: 'actions/auto-update' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '通用',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'worker变量说明', link: 'worker-vars' },
|
||||
{ text: '常见问题', link: 'common-issues' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '附加功能',
|
||||
collapsed: true,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '配置 SMTP IMAP 代理服务', link: 'feature/config-smtp-proxy' },
|
||||
{ text: '发送邮件 API', link: 'feature/send-mail-api' },
|
||||
@@ -135,12 +149,16 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
{ text: '配置 Telegram Bot', link: 'feature/telegram' },
|
||||
{ text: '配置 S3 附件', link: 'feature/s3-attachment' },
|
||||
{ text: '配置 worker 使用 wasm 解析邮件', link: 'feature/mail_parser_wasm_worker' },
|
||||
{ text: '配置 webhook', link: 'feature/webhook' },
|
||||
{ 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' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '功能简介',
|
||||
collapsed: true,
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Admin 控制台', link: 'feature/admin' },
|
||||
{ text: 'Admin 用户管理', link: 'feature/admin-user-management' },
|
||||
|
||||
@@ -61,8 +61,8 @@ pnpm run deploy
|
||||
```toml
|
||||
name = "cloudflare_temp_email"
|
||||
main = "src/worker.ts"
|
||||
compatibility_date = "2023-08-14"
|
||||
node_compat = true
|
||||
compatibility_date = "2024-09-23"
|
||||
compatibility_flags = [ "nodejs_compat" ]
|
||||
|
||||
# enable cron if you want set auto clean up
|
||||
# [triggers]
|
||||
@@ -74,12 +74,17 @@ node_compat = true
|
||||
# ]
|
||||
|
||||
[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)
|
||||
# ANNOUNCEMENT = "Custom Announcement"
|
||||
# MIN_ADDRESS_LEN = 1
|
||||
# MAX_ADDRESS_LEN = 30
|
||||
# ANNOUNCEMENT = "Custom Announcement"
|
||||
# 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]
|
||||
# ADDRESS_REGEX = "[^a-z0-9]"
|
||||
# If you want your site to be private, uncomment below and change your password
|
||||
# PASSWORDS = ["123", "456"]
|
||||
# admin console password, if not configured, access to the console is not allowed
|
||||
@@ -103,6 +108,8 @@ JWT_SECRET = "xxx" # Key used to generate jwt
|
||||
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
|
||||
# Allow users to create email addresses
|
||||
ENABLE_USER_CREATE_EMAIL = true
|
||||
# Disable anonymous user create email, if set true, users can only create email addresses after logging in
|
||||
# DISABLE_ANONYMOUS_USER_CREATE_EMAIL = true
|
||||
# Allow users to delete messages
|
||||
ENABLE_USER_DELETE_EMAIL = true
|
||||
# Allow automatic replies to emails
|
||||
@@ -114,14 +121,29 @@ 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 = ""
|
||||
# telegram bot
|
||||
# TG_MAX_ADDRESS = 5
|
||||
# telegram bot info, predefined bot info can reduce latency of the webhook
|
||||
# TG_BOT_INFO = "{}"
|
||||
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
||||
# Frontend URL
|
||||
# FRONTEND_URL = "https://xxxx.xxx"
|
||||
# Enable check junk mail
|
||||
# ENABLE_CHECK_JUNK_MAIL = false
|
||||
# junk mail check list, if status exists and status is not pass, will be marked as junk mail
|
||||
# JUNK_MAIL_CHECK_LIST = = ["spf", "dkim", "dmarc"]
|
||||
# junk mail force check pass list, if no status or status is not pass, will be marked as junk mail
|
||||
# JUNK_MAIL_FORCE_PASS_LIST = ["spf", "dkim", "dmarc"]
|
||||
# remove attachment if size exceed 2MB, mail maybe mising some information due to parsing
|
||||
# REMOVE_EXCEED_SIZE_ATTACHMENT = true
|
||||
# remove all attachment, mail maybe mising some information due to parsing
|
||||
# REMOVE_ALL_ATTACHMENT = true
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
|
||||
@@ -15,14 +15,17 @@ hero:
|
||||
- theme: alt
|
||||
text: 通过用户界面部署
|
||||
link: /zh/guide/quick-start
|
||||
- theme: alt
|
||||
text: 通过 Github Actions 部署
|
||||
link: /zh/guide/quick-start
|
||||
|
||||
features:
|
||||
- title: 免费托管在 CloudFlare,无需服务器
|
||||
details: Cloudflare D1 数据库,Cloudflare Pages 前端,Cloudflare Workers 后端, Cloudflare Email Routing
|
||||
- title: 仅需域名即可私有部署
|
||||
details: 支持 password 登录邮箱,使用访问密码可作为私人站点,支持附件功能
|
||||
- title: 仅需域名即可私有部署, 免费托管在 CloudFlare,无需服务器
|
||||
details: 支持 password 登录邮箱, 用户注册,使用访问密码可作为私人站点,支持附件功能。
|
||||
- title: 使用 rust wasm 解析邮件
|
||||
details: 使用 rust wasm 解析邮件,支持邮件各种RFC标准,支持附件, 速度极快
|
||||
- title: 支持 Telegram Bot 和 Webhook
|
||||
details: 邮件可转发到 Telegram 或者 webhook, Telegram Bot 支持绑定邮箱,查看邮件, Telegram 小程序
|
||||
- title: 支持发送邮件(UI/API/SMTP)
|
||||
details: 支持通过域名邮箱发送 txt 或者 html 邮件,支持 DKIM 签名, UI/API/SMTP 发送邮件
|
||||
---
|
||||
|
||||
BIN
vitepress-docs/docs/public/feature/address-webhook.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
vitepress-docs/docs/public/feature/admin-mail-webhook.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
BIN
vitepress-docs/docs/public/feature/admin-webhook-settings.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 231 KiB |
BIN
vitepress-docs/docs/public/feature/oauth2-login.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
vitepress-docs/docs/public/feature/oauth2.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
vitepress-docs/docs/public/ui_install/worker-runtime.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
10
vitepress-docs/docs/zh/guide/actions/auto-update.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Github Actions 部署如何配置自动更新
|
||||
|
||||
::: warning 注意
|
||||
有问题请通过 `Github Issues` 反馈,感谢。
|
||||
自动更新不会执行 D1 数据库的 sql 文件,当数据库 schema 变动时,需要手动执行。
|
||||
:::
|
||||
|
||||
1. 打开仓库的 `Actions` 页面,找到 `Upstream Sync`,点击 `enable workflow` 启用 `workflow`
|
||||
2. 如果 `Upstream Sync` 运行失败,到仓库主页点击 `Sync` 手动同步即可
|
||||
3. 修改 `Upstream Sync` 的 `schedule` 配置可自定义更新间隔,参考 [cron 表达式](https://crontab.guru/)
|
||||
3
vitepress-docs/docs/zh/guide/actions/d1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 初始化/更新 D1 数据库
|
||||
|
||||
参考 [命令行更新 d1](/zh/guide/cli/d1) 或者 [用户界面更新 d1](/zh/guide/ui/d1)
|
||||
58
vitepress-docs/docs/zh/guide/actions/github-action.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# 通过 Github Actions 部署
|
||||
|
||||
::: warning 注意
|
||||
目前只支持 worker 和 pages 的部署。
|
||||
有问题请通过 `Github Issues` 反馈,感谢。
|
||||
:::
|
||||
|
||||
## 部署步骤
|
||||
|
||||
### Fork 仓库并启用 Actions
|
||||
|
||||
- 在 GitHub fork 本仓库
|
||||
- 打开仓库的 `Actions` 页面
|
||||
- 找到 `Deploy Backend` 点击 `enable workflow` 启用 `workflow`
|
||||
- 如果需要前后端分离部署, 找到`Deploy Frontend` 点击 `enable workflow` 启用 `workflow`
|
||||
|
||||
### 配置 Secrets
|
||||
|
||||
然后在仓库页面 `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) |
|
||||
|
||||
- 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` 选择分支手动部署
|
||||
|
||||
## 如何配置自动更新
|
||||
|
||||
1. 打开仓库的 `Actions` 页面,找到 `Upstream Sync`,点击 `enable workflow` 启用 `workflow`
|
||||
2. 如果 `Upstream Sync` 运行失败,到仓库主页点击 `Sync` 手动同步即可
|
||||
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
|
||||
```
|
||||
|
||||
@@ -41,6 +44,7 @@ pnpm run deploy
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install
|
||||
# 如果你要启用 Cloudflare Zero Trust, 需要使用 pnpm build:pages:nopwa 来禁用缓存
|
||||
pnpm build:pages
|
||||
cd ../pages
|
||||
pnpm run deploy
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Cloudflare workers 后端
|
||||
# Cloudflare Worker 后端
|
||||
|
||||
## 初始化项目
|
||||
|
||||
@@ -22,15 +22,25 @@ wrangler kv:namespace create DEV
|
||||
|
||||
## 修改 `wrangler.toml` 配置文件
|
||||
|
||||
> [!NOTE] 注意
|
||||
> 更多变量的配置请查看 [worker变量说明](/zh/guide/worker-vars)
|
||||
|
||||
```toml
|
||||
name = "cloudflare_temp_email"
|
||||
main = "src/worker.ts"
|
||||
compatibility_date = "2023-12-01"
|
||||
compatibility_date = "2024-09-23"
|
||||
compatibility_flags = [ "nodejs_compat" ]
|
||||
|
||||
# 如果你想使用自定义域名,你需要添加 routes 配置
|
||||
# routes = [
|
||||
# { pattern = "temp-email-api.xxxxx.xyz", custom_domain = true },
|
||||
# ]
|
||||
node_compat = true
|
||||
|
||||
# 如果你想要部署带有前端资源的 worker, 你需要添加 assets 配置
|
||||
# [assets]
|
||||
# directory = "../frontend/dist/"
|
||||
# binding = "ASSETS"
|
||||
# run_worker_first = true
|
||||
|
||||
# 如果你想要使用定时任务清理邮件,取消下面的注释,并修改 cron 表达式
|
||||
# [triggers]
|
||||
@@ -42,57 +52,20 @@ node_compat = true
|
||||
# ]
|
||||
|
||||
[vars]
|
||||
# TITLE = "Custom Title" # 自定义网站标题
|
||||
PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空字符串
|
||||
# (min, max) adderss的长度,如果不设置,默认为(1, 30)
|
||||
# ANNOUNCEMENT = "Custom Announcement" # 自定义公告
|
||||
# MIN_ADDRESS_LEN = 1
|
||||
# MAX_ADDRESS_LEN = 30
|
||||
# 如果你想要你的网站私有,取消下面的注释,并修改密码
|
||||
# PASSWORDS = ["123", "456"]
|
||||
# 邮箱名称前缀,不需要后缀可配置为空字符串或者不配置
|
||||
PREFIX = "tmp"
|
||||
# 用于临时邮箱的所有域名, 支持多个域名
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
|
||||
# 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
|
||||
JWT_SECRET = "xxx"
|
||||
|
||||
# admin 控制台密码, 不配置则不允许访问控制台
|
||||
# ADMIN_PASSWORDS = ["123", "456"]
|
||||
# 警告: 管理员控制台没有密码或用户检查
|
||||
# DISABLE_ADMIN_PASSWORD_CHECK = false
|
||||
# admin 联系方式,不配置则不显示,可配置任意字符串
|
||||
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||
# DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 默认用户可用的域名(未登录或未分配角色的用户)
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
|
||||
# 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称
|
||||
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
||||
# 新用户默认角色, 仅在启用邮件验证时有效
|
||||
# USER_DEFAULT_ROLE = "vip"
|
||||
# admin 角色配置, 如果用户角色等于 ADMIN_USER_ROLE 则可以访问 admin 控制台
|
||||
# ADMIN_USER_ROLE = "admin" # the role which can access admin panel
|
||||
# 用户角色配置, 如果 domains 为空将使用 default_domains
|
||||
# 如果 prefix 为 null 将使用默认前缀, 如果 prefix 为空字符串将不使用前缀
|
||||
# USER_ROLES = [
|
||||
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
|
||||
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "admin", prefix = "" },
|
||||
# ]
|
||||
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
|
||||
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
||||
|
||||
# 是否允许用户创建邮件, 不配置则不允许
|
||||
ENABLE_USER_CREATE_EMAIL = true
|
||||
# 允许用户删除邮件, 不配置则不允许
|
||||
ENABLE_USER_DELETE_EMAIL = true
|
||||
# 允许自动回复邮件
|
||||
ENABLE_AUTO_REPLY = false
|
||||
# 是否启用 webhook
|
||||
# ENABLE_WEBHOOK = true
|
||||
# 前端界面页脚文本
|
||||
# COPYRIGHT = "Dream Hunter"
|
||||
# DISABLE_SHOW_GITHUB = true # 是否显示 GitHub 链接
|
||||
# 默认发送邮件余额,如果不设置,将为 0
|
||||
# DEFAULT_SEND_BALANCE = 1
|
||||
# NO_LIMIT_SEND_ROLE = "vip" # 可以无限发送邮件的角色
|
||||
# Turnstile 人机验证配置
|
||||
# CF_TURNSTILE_SITE_KEY = ""
|
||||
# CF_TURNSTILE_SECRET_KEY = ""
|
||||
# telegram bot 最多绑定邮箱数量
|
||||
# TG_MAX_ADDRESS = 5
|
||||
# 全局转发地址列表,如果不配置则不启用,启用后所有邮件都会转发到列表中的地址
|
||||
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
||||
|
||||
# D1 数据库的名称和 ID 可以在 cloudflare 控制台查看
|
||||
[[d1_databases]]
|
||||
@@ -112,6 +85,34 @@ database_id = "xxx" # D1 数据库 ID
|
||||
# namespace_id = "1001"
|
||||
# # 10 requests per minute
|
||||
# simple = { limit = 10, period = 60 }
|
||||
|
||||
# 绑定其他 worker 处理邮件,例如通过 auth-inbox ai 能力解析验证码或激活链接
|
||||
# [[services]]
|
||||
# binding = "AUTH_INBOX"
|
||||
# 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 配置
|
||||
|
||||
41
vitepress-docs/docs/zh/guide/common-issues.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# 常见问题
|
||||
|
||||
> [!NOTE] 注意
|
||||
> 如果你的问题没有在这里找到解决方案,请到 `Github Issues` 中搜索或者提问, 或者到 Telegram 群组中提问。
|
||||
|
||||
## 通用
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
| -------------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| 使用 Cloudflare Workers 给已认证的转发邮箱发送邮件 | 使用 cf 的 API 进行发送,只支持绑定到 CF 上的收件地址,即 CF EMAIL 转发目的地址 |
|
||||
| 绑定多个域名 | 每个域名都需要设置 email 转发到 worker |
|
||||
|
||||
## worker 相关
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
| ------------------------------------------------------------------ | --------------------------------------------------------------------------- |
|
||||
| `Uncaught Error: No such module "path". imported from "worker.js"` | [参考](/zh/guide/ui/worker) |
|
||||
| `No such module "node:stream". imported from "worker.js"` | [参考](/zh/guide/ui/worker) |
|
||||
| `二级域名无法发送邮件` | [参考](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/515) |
|
||||
| `Failed to send verify code: No balance` | admin 后台设置无限制邮件或者发件权限页面增加额度 |
|
||||
| `Github OAuth无法获取到邮箱 400 Failed to get user email` | 需要 github 用户设置公开邮箱 |
|
||||
| `Cannot read properties of undefined (reading 'map')` | worker 变量没有设置成功 |
|
||||
|
||||
## pages 相关
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
| --------------- | ---------------------------------------- |
|
||||
| `network error` | 使用无痕模式或者清空浏览器缓存,DNS 缓存 |
|
||||
|
||||
## telegram bot
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
| -------------------------------------------------------------- | -------------------------------------------------- |
|
||||
| `Telgram Bot获取邮件失败:400:Bad Request:BUTTON_URL_INVALID` | tg mini app 的 URL 填写错误,需要填写 pages 的 URL |
|
||||
| `Telegram bot bind error: bind adress count reach the limit` | 需要设置 worker 变量 `TG_MAX_ADDRESS` |
|
||||
|
||||
## Github Actions
|
||||
|
||||
| 问题 | 解决方案 |
|
||||
| ------------------------------------------ | --------------------------------------------------------------------------------- |
|
||||
| Github Action部署后,cf里始终是preview分支 | 到 cf pages 页面的设置中确认 前端的分支 和 Github Action 的 前端部署分支 是否相同 |
|
||||
@@ -1,9 +1,11 @@
|
||||
|
||||
# 配置发送邮件
|
||||
|
||||
## 使用 Cloudflare Workers 给已认证的邮箱发送邮件
|
||||
::: warning 注意
|
||||
三种方式可以同时配置,发送邮件时会优先使用 `resend`,如果没有配置 `resend`,则会使用 `smtp`.
|
||||
|
||||
admin 后台 账号配置 `已验证地址列表(可通过 cf 内部 api 发送邮件)`
|
||||
如果配置了 Cloudflare 已认证的转发邮箱地址,会优先使用 cf 内部 API 发送邮件
|
||||
:::
|
||||
|
||||
## 使用 resend 发送邮件
|
||||
|
||||
@@ -11,9 +13,16 @@ admin 后台 账号配置 `已验证地址列表(可通过 cf 内部 api 发送
|
||||
|
||||
`API KEYS` 页面创建 `api key`
|
||||
|
||||
使用 cli 或者直接添加到 `wrangler.toml` 的 `vars`,或者在 cloudflare worker 页面的变量中添加 `RESEND_TOKEN`
|
||||
然后执行下面的命令,将 `RESEND_TOKEN` 添加到 secrets 中
|
||||
|
||||
> [!NOTE]
|
||||
> 如果你觉得麻烦,也可以直接明文放在 `wrangler.toml` 中 `[vars]` 下面,但是不推荐这样做
|
||||
|
||||
如果你是通过 UI 部署的,可以在 Cloudflare 的 UI 界面中添加到 `Variables and Secrets` 下面
|
||||
|
||||
```bash
|
||||
# 切换到 worker 目录
|
||||
cd worker
|
||||
wrangler secret put RESEND_TOKEN
|
||||
```
|
||||
|
||||
@@ -23,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,9 +1,9 @@
|
||||
# Cloudflare Email Routing
|
||||
|
||||
1. 配置对应域名的 `电子邮件 DNS 记录`, 如果是多个域名,需要配置多个域名的 `电子邮件 DNS 记录`
|
||||
1. 在 CF 控制台网页的对应域名的 `Email Routing` 下,配置 `电子邮件 DNS 记录`, 如果是多个域名,需要配置多个域名的 `电子邮件 DNS 记录`
|
||||
|
||||
2. 在将电子邮件地址绑定到您的 Worker 之前,您需要启用电子邮件路由并拥有至少一个经过验证的电子邮件地址。
|
||||
2. 在将电子邮件地址绑定到您的 Worker 之前,您需要启用电子邮件路由并拥有至少一个经过验证的电子邮件地址(目标地址)。
|
||||
|
||||
3. 配置每个域名的 `Cloudflare Email Routing` catch-all 发送到 `worker`
|
||||
3. 配置每个域名的 `Email Routing` 的路由规则中的 `Catch-all 地址` 发送到 `worker`
|
||||
|
||||

|
||||
|
||||
144
vitepress-docs/docs/zh/guide/feature/another-worker-enhanced.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 通过其他 worker 增强
|
||||
|
||||
> 临时邮箱的核心能力在邮件的管理,通过其他 worker 可以增强临时邮箱的功能,例如通过 auth-inbox ai 能力解析验证码或激活链接
|
||||
> 该功能仅触发其他 worker ,在 webhook 后执行
|
||||
> [!NOTE]
|
||||
> 如果要使用 worker 增强,请提前创建可以 rpc 调用的 worker,具体下文详述
|
||||
> 参考:
|
||||
> - https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/
|
||||
> - https://developers.cloudflare.com/workers/runtime-apis/rpc/
|
||||
> - auth-inbox 项目:https://github.com/TooonyChen/AuthInbox
|
||||
|
||||
## 创建其他 worker(以 auth-inbox 项目ai解析验证码为例子)
|
||||
|
||||
### worker 改造为继承 WorkerEntrypoint
|
||||
|
||||
一个简单,作为被调用方,提供 rpc 方法调用的worker代码如下(rpcEmail 方法为样例)
|
||||
(使用已经修改好的项目 https://github.com/oneisall8955/AuthInbox-fork)
|
||||
|
||||
src/index.ts 文件
|
||||
```js
|
||||
import { WorkerEntrypoint } from "cloudflare:workers";
|
||||
|
||||
interface Env {
|
||||
DB: D1Database;
|
||||
// ...
|
||||
}
|
||||
|
||||
export default class extends WorkerEntrypoint<Env> {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
console.log("原本fetch接口入参是request,env,ctx");
|
||||
console.log("修改为WorkerEntrypoint风格后,只有一个入参request,获取环境变量和上下文有小改动");
|
||||
// 环境变量及上下文改动详见:
|
||||
// https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#bindings-env
|
||||
// https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#lifecycle-methods-ctx
|
||||
const env: Env = this.env;
|
||||
const ctx: ExecutionContext = this.ctx;
|
||||
console.log("后续逻辑不变");
|
||||
return new Response('ok', { status: 200 });
|
||||
}
|
||||
|
||||
// 主要功能
|
||||
async email(message: ForwardableEmailMessage): Promise<void> {
|
||||
console.log("原本fetch接口入参是message,env,ctx");
|
||||
console.log("修改为WorkerEntrypoint风格后,只有一个入参message,获取环境变量和上下文和fetch方法一样");
|
||||
const env: Env = this.env;
|
||||
const ctx: ExecutionContext = this.ctx;
|
||||
console.log("接受email routing请求后,后续逻辑不变");
|
||||
}
|
||||
|
||||
// 暴露rpc接口,处理来自其他worker的邮件请求
|
||||
async rpcEmail(requestBody: string): Promise<void> {
|
||||
console.log(`接受其他worker(临时邮件服务cloudflare_temp_email)的请求,request body: ${requestBody}`);
|
||||
// requestBody json 格式,由临时邮件服务发送,格式如下
|
||||
// type RPCEmailMessage = {
|
||||
// from: string | undefined | null,
|
||||
// to: string | undefined | null,
|
||||
// rawEmail: string | undefined | null,
|
||||
// headers: Map<string, string>,
|
||||
// }
|
||||
// ... todo ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 部署其他 worker
|
||||
|
||||
修改好或者使用 以auth-inbox 为例,部署到 cloudflare worker 上,详见 https://github.com/TooonyChen/AuthInbox ,或者使用已经修改好的项目 https://github.com/oneisall8955/AuthInbox-fork
|
||||
|
||||
## 配置临时邮件服务,使用指定其他 worker 增强
|
||||
|
||||
## 绑定服务
|
||||
|
||||
### 通过 wrangler.toml 配置
|
||||
|
||||
```toml
|
||||
[[services]]
|
||||
binding = "AUTH_INBOX"
|
||||
service = "auth-inbox"
|
||||
```
|
||||
|
||||
这里的 `binding = "AUTH_INBOX"` 可以自定义,可以是任何字符串,`service = "auth-inbox"` 是部署好的提供rpc接口调用的worker名称。
|
||||
|
||||
### 用户界面配置
|
||||
|
||||
在设置-绑定,添加绑定,选择绑定服务。
|
||||
变量名称填写自定义的名称,可以任意字符串 ,例如 `AUTH_INBOX`。
|
||||
服务绑定选择上一步创建好的服务,例如 `auth-inbox`。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 环境变量配置
|
||||
|
||||
### 通过 wrangler.toml 配置
|
||||
|
||||
```toml
|
||||
ENABLE_ANOTHER_WORKER = true
|
||||
ANOTHER_WORKER_LIST ="""
|
||||
[
|
||||
{
|
||||
"binding":"AUTH_INBOX",
|
||||
"method":"rpcEmail",
|
||||
"keywords":[
|
||||
"验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认",
|
||||
"account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm"
|
||||
]
|
||||
}
|
||||
]
|
||||
"""
|
||||
```
|
||||
|
||||
环境变量解释:
|
||||
- ENABLE_ANOTHER_WORKER = true:默认为false,true则开启其他 worker 处理邮件
|
||||
- ANOTHER_WORKER_LIST 是一个JOSN数组,每个对象包3个字段
|
||||
- binding: *必填,必须与services部分指定的 binding = "XXX" 保持一致*,例子中为 AUTH_INBOX
|
||||
- method: 可选,默认 rpcEmail,指的是调用这个 worker 的哪一个 rpc 方法处理
|
||||
- keywords: 关键词数组,忽略大小写。用于过滤,如果*解析后邮件文本*匹配到这些关键词,触发这个 worker,并且调用这个 worker 的 `method` 方法
|
||||
|
||||
### 用户界面配置
|
||||
|
||||
在设置-环境变量,添加环境变量
|
||||
- ENABLE_ANOTHER_WORKER = true
|
||||
- ANOTHER_WORKER_LIST 为上面提及的JSON数组字符串,不再复述,详细介绍看上文
|
||||
```json
|
||||
[
|
||||
{
|
||||
"binding":"AUTH_INBOX",
|
||||
"method":"rpcEmail",
|
||||
"keywords":[
|
||||
"验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认",
|
||||
"account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm"
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 测试
|
||||
|
||||
发送一个邮件到临时邮箱,观察worker日志到,或者到 auth-inbox 提供的面板上查看验证码
|
||||
|
||||

|
||||
@@ -1,6 +1,6 @@
|
||||
# 搭建 SMTP IMAP 代理服务
|
||||
|
||||
::: warning
|
||||
::: warning 注意
|
||||
如果你使用了 `resend`, 可直接使用 `resend` 的 `SMTP` 服务,不需要使用此服务
|
||||
:::
|
||||
|
||||
|
||||
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
|
||||
```
|
||||
@@ -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
|
||||
@@ -32,6 +50,7 @@ export const commonParseMail = async (raw_mail: string | undefined | null): Prom
|
||||
sender: parsedEmail.sender || "",
|
||||
subject: parsedEmail.subject || "",
|
||||
text: parsedEmail.text || "",
|
||||
headers: parsedEmail.headers || [],
|
||||
html: parsedEmail.body_html || "",
|
||||
};
|
||||
} catch (e) {
|
||||
@@ -55,7 +74,7 @@ export const commonParseMail = async (raw_mail: string | undefined | null): Prom
|
||||
}
|
||||
```
|
||||
|
||||
## 部署
|
||||
### 部署
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# 配置子域名邮箱
|
||||
|
||||
::: warning
|
||||
::: warning 注意
|
||||
子域名邮箱发送邮件可能无法发送邮件,建议使用主域名邮箱发送邮件,子域名邮箱仅用于接收邮件。
|
||||
|
||||
mail channel 已不被支持,下面参考中仅限收件部分。
|
||||
:::
|
||||
|
||||
参考
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
# 配置 Telegram Bot
|
||||
|
||||
试用地址:[@cf_temp_mail_bot](https://t.me/cf_temp_mail_bot)
|
||||
|
||||
::: warning 注意
|
||||
worker 默认的 `worker.dev` 域名的证书是不被 telegram 支持的,配置 Telegram Bot 请使用自定义域名
|
||||
:::
|
||||
|
||||
> [!NOTE]
|
||||
> 如果要使用 Telegram Bot, 请先绑定 `KV`
|
||||
>
|
||||
> 如果不需要 Telegram Bot, 可跳过此步骤
|
||||
>
|
||||
> 如果你想 Telegram 的解析邮件能力更强,参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker)
|
||||
|
||||
## Telegram Bot 配置
|
||||
|
||||
> [!NOTE]
|
||||
> 如果不需要 Telegram Bot, 可跳过此步骤
|
||||
|
||||
请先创建一个 Telegram Bot,然后获取 `token`,然后执行下面的命令,将 `token` 添加到 secrets 中
|
||||
|
||||
你也可以在 Cloudflare 的 UI 界面中添加 `secrets`
|
||||
> [!NOTE]
|
||||
> 如果你觉得麻烦,也可以直接明文放在 `wrangler.toml` 中 `[vars]` 下面,但是不推荐这样做
|
||||
|
||||
如果你是通过 UI 部署的,可以在 Cloudflare 的 UI 界面中添加到 `Variables and Secrets` 下面
|
||||
|
||||
```bash
|
||||
# 切换到 worker 目录
|
||||
cd worker
|
||||
pnpm wrangler secret put TELEGRAM_BOT_TOKEN
|
||||
```
|
||||
|
||||
@@ -23,6 +38,21 @@ pnpm wrangler secret put TELEGRAM_BOT_TOKEN
|
||||
|
||||
## Mini App
|
||||
|
||||
可以通过命令行部署,或者 UI 界面部署
|
||||
|
||||
### UI 部署
|
||||
|
||||
其他步骤参考 [UI 部署](/zh/guide/cli/pages) 中的 `前后端分离部署`
|
||||
|
||||
> [!NOTE]
|
||||
> 从这里下载 zip, [telegram-frontend.zip](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/telegram-frontend.zip)
|
||||
>
|
||||
> 修改压缩包里面的 index-xxx.js 文件 ,xx 是随机的字符串
|
||||
>
|
||||
> 搜索 `https://temp-email-api.xxx.xxx` ,替换成你worker 的域名,然后部署新的zip文件
|
||||
|
||||
### 命令行部署
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install
|
||||
@@ -31,8 +61,6 @@ cp .env.example .env.prod
|
||||
pnpm run deploy:telegram --project-name=<你的项目名称>
|
||||
```
|
||||
|
||||
部署完成后,请在 admin 后台的 `设置` -> `电报小程序` 页面 `电报小程序 URL`。
|
||||
|
||||
请在 `@BotFather` 处执行 `/setmenubutton`,然后输入你的网页地址,设置左下角的 `Open App` 按钮。
|
||||
|
||||
你也可以在 `@BotFather` 处执行 `/newapp` 新建 app 来获得 mini app 的链接
|
||||
- 部署完成后,请在 admin 后台的 `设置` -> `电报小程序` 页面 `电报小程序 URL` 中填写网页 URL。
|
||||
- 请在 `@BotFather` 处执行 `/setmenubutton`,然后输入你的网页地址,设置左下角的 `Open App` 按钮。
|
||||
- 请在 `@BotFather` 处执行 `/newapp` 新建 app 来注册 mini app。
|
||||
|
||||