Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2f3634c7e | ||
|
|
b62a3cbc3e | ||
|
|
8edb75587e | ||
|
|
de48661d0d | ||
|
|
a905ba5f06 | ||
|
|
6ae90be3bf | ||
|
|
5e24817de6 | ||
|
|
732189482e | ||
|
|
2bbde15f53 | ||
|
|
37cf0776b5 | ||
|
|
3fbace871c | ||
|
|
648e9f7adf | ||
|
|
ab2bfdd00f | ||
|
|
0565978930 | ||
|
|
89d8944e60 | ||
|
|
4084771621 | ||
|
|
840496c48f | ||
|
|
9843b35f54 | ||
|
|
bfd66f5019 | ||
|
|
0bc31360b0 | ||
|
|
267d9bb93e | ||
|
|
2cc84d565c | ||
|
|
c96d180591 | ||
|
|
1303b0f2a9 | ||
|
|
9f535a0a90 | ||
|
|
70109785c6 | ||
|
|
7fd10f2775 | ||
|
|
f59b8c7a1b | ||
|
|
312ac13185 | ||
|
|
e6c582be9f | ||
|
|
483c429feb | ||
|
|
da5482e095 | ||
|
|
de4646876a | ||
|
|
bbc8a96811 | ||
|
|
9ac9cd46b0 | ||
|
|
c694b07380 | ||
|
|
672c4c7273 | ||
|
|
ee023ac2e9 | ||
|
|
cc77bdf36d | ||
|
|
dec309a0fd | ||
|
|
9488543e44 | ||
|
|
50326bcc98 | ||
|
|
272b624b9b | ||
|
|
e230801a1c | ||
|
|
07833d5ca9 | ||
|
|
101a561894 | ||
|
|
327962432a | ||
|
|
6051d49315 | ||
|
|
95f361743b | ||
|
|
c6afc5d425 | ||
|
|
466f53254b | ||
|
|
ce0a10e6de | ||
|
|
26995982af | ||
|
|
0894ac0dc9 | ||
|
|
47e2cb56b4 | ||
|
|
32767176f0 | ||
|
|
31eb6c23d1 | ||
|
|
91a859bbcf | ||
|
|
525f5e2dce | ||
|
|
908fc0cc86 | ||
|
|
97d24b2087 | ||
|
|
983300acf4 | ||
|
|
144a792cb2 | ||
|
|
278f0112d0 | ||
|
|
764faebf9f | ||
|
|
d4f0c82e42 | ||
|
|
cf680e6349 | ||
|
|
c3987d364c | ||
|
|
3a542a8391 | ||
|
|
241e0b7b28 | ||
|
|
b43353ea47 | ||
|
|
6c334d32f6 | ||
|
|
7889d2edea | ||
|
|
2426e0b51a | ||
|
|
61434ab6f7 | ||
|
|
7f6a02ca38 | ||
|
|
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 |
@@ -0,0 +1,3 @@
|
|||||||
|
[flake8]
|
||||||
|
max-line-length = 180
|
||||||
|
exclude = .git,__pycache__,build,dist
|
||||||
@@ -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);
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
name: Deploy Backend Production
|
name: Deploy Backend
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [Upstream Sync]
|
||||||
|
types: [completed]
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
@@ -18,7 +21,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v3
|
- uses: pnpm/action-setup@v3
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -29,14 +32,43 @@ jobs:
|
|||||||
|
|
||||||
- name: Deploy Backend for ${{ github.ref_name }}
|
- name: Deploy Backend for ${{ github.ref_name }}
|
||||||
run: |
|
run: |
|
||||||
|
export use_worker_assets=${{ secrets.USE_WORKER_ASSETS }}
|
||||||
|
export use_worker_assets_with_telegram=${{ secrets.USE_WORKER_ASSETS_WITH_TELEGRAM }}
|
||||||
|
if [ -n "$use_worker_assets" ]; then
|
||||||
|
cd frontend/
|
||||||
|
pnpm install --no-frozen-lockfile
|
||||||
|
if [ -n "$use_worker_assets_with_telegram" ]; then
|
||||||
|
echo "Building with telegram pages"
|
||||||
|
pnpm build:telegram:pages
|
||||||
|
else
|
||||||
|
echo "Building with normal pages"
|
||||||
|
pnpm build:pages
|
||||||
|
fi
|
||||||
|
cd ..
|
||||||
|
fi
|
||||||
|
|
||||||
|
export debug_mode=${{ secrets.DEBUG_MODE }}
|
||||||
|
export use_mail_wasm_parser=${{ secrets.BACKEND_USE_MAIL_WASM_PARSER }}
|
||||||
cd worker/
|
cd worker/
|
||||||
echo '${{ secrets.BACKEND_TOML }}' > wrangler.toml
|
echo '${{ secrets.BACKEND_TOML }}' > wrangler.toml
|
||||||
pnpm install --no-frozen-lockfile
|
pnpm install --no-frozen-lockfile
|
||||||
output=$(pnpm run deploy 2>&1)
|
|
||||||
if [ $? -ne 0 ]; then
|
if [ -n "$use_mail_wasm_parser" ]; then
|
||||||
code=$?
|
echo "Using mail-parser-wasm-worker"
|
||||||
echo "Command failed with exit code $code"
|
pnpm add mail-parser-wasm-worker
|
||||||
exit $code
|
git apply ../.github/config/mail-parser-wasm-worker.patch
|
||||||
|
echo "Applied mail-parser-wasm-worker patch"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$debug_mode" = "true" ]; 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
|
fi
|
||||||
echo "Deployed for tag ${{ github.ref_name }}"
|
echo "Deployed for tag ${{ github.ref_name }}"
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v3
|
- uses: pnpm/action-setup@v3
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
name: Deploy Frontend
|
name: Deploy Frontend
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: [Upstream Sync]
|
||||||
|
types: [completed]
|
||||||
push:
|
push:
|
||||||
paths:
|
|
||||||
- "frontend/**"
|
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -20,7 +21,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v3
|
- uses: pnpm/action-setup@v3
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -38,12 +39,12 @@ jobs:
|
|||||||
export frontend_branch=${{ secrets.FRONTEND_BRANCH }}
|
export frontend_branch=${{ secrets.FRONTEND_BRANCH }}
|
||||||
if [ -n "$frontend_branch" ]; then
|
if [ -n "$frontend_branch" ]; then
|
||||||
echo "Deploying branch $frontend_branch"
|
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
|
else
|
||||||
echo "Deploying branch prodcution"
|
echo "Deploying branch production"
|
||||||
pnpm run deploy --project-name=$project_name
|
pnpm run deploy --project-name=$project_name
|
||||||
fi
|
fi
|
||||||
echo "Deploying prodcution for ${{ github.ref_name }}"
|
echo "Deploying production for ${{ github.ref_name }}"
|
||||||
echo "Deployed for tag ${{ github.ref_name }}"
|
echo "Deployed for tag ${{ github.ref_name }}"
|
||||||
|
|
||||||
export tg_mini_app_project_name=${{ secrets.TG_FRONTEND_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"
|
echo "Deploying telegram mini app $tg_mini_app_project_name"
|
||||||
if [ -n "$frontend_branch" ]; then
|
if [ -n "$frontend_branch" ]; then
|
||||||
echo "Deploying telegram mini app branch $frontend_branch"
|
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
|
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
|
pnpm run deploy:telegram --project-name=$tg_mini_app_project_name
|
||||||
fi
|
fi
|
||||||
echo "Deployed telegram mini app for ${{ github.ref_name }}"
|
echo "Deployed telegram mini app for ${{ github.ref_name }}"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v3
|
- uses: pnpm/action-setup@v3
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: PR Agent action step
|
- name: PR Agent action step
|
||||||
id: pragent
|
id: pragent
|
||||||
uses: Codium-ai/pr-agent@main
|
uses: docker://codiumai/pr-agent:0.29-github_action
|
||||||
env:
|
env:
|
||||||
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false"
|
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false"
|
||||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Install Node.js
|
- name: Install Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
|
|
||||||
- uses: pnpm/action-setup@v3
|
- uses: pnpm/action-setup@v3
|
||||||
name: Install pnpm
|
name: Install pnpm
|
||||||
@@ -30,7 +30,13 @@ jobs:
|
|||||||
run: cd frontend && pnpm install --no-frozen-lockfile && pnpm build:release
|
run: cd frontend && pnpm install --no-frozen-lockfile && pnpm build:release
|
||||||
|
|
||||||
- name: Zip Frontend dist
|
- 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
|
- name: cp wrangler.toml
|
||||||
run: cd worker && cp wrangler.toml.template wrangler.toml
|
run: cd worker && cp wrangler.toml.template wrangler.toml
|
||||||
@@ -38,9 +44,24 @@ jobs:
|
|||||||
- name: Build Backend
|
- name: Build Backend
|
||||||
run: cd worker && pnpm install --no-frozen-lockfile && pnpm build
|
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
|
- name: Upload to Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
frontend/dist/frontend.zip
|
frontend/frontend.zip
|
||||||
worker/dist/worker.js
|
frontend/telegram-frontend.zip
|
||||||
|
worker/worker.js
|
||||||
|
worker/worker-with-wasm-mail-parser.zip
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
.DS_Store
|
||||||
dist/
|
dist/
|
||||||
test/
|
test/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -1,6 +1,145 @@
|
|||||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||||
# CHANGE LOG
|
# CHANGE LOG
|
||||||
|
|
||||||
|
## main(v1.0.6)
|
||||||
|
|
||||||
|
- feat: |DB| update db schema add index
|
||||||
|
- feat: |地址密码| 增加地址密码登录功能, 通过 `ENABLE_ADDRESS_PASSWORD` 配置启用
|
||||||
|
- fix: |GitHub Actions| 修复 debug 模式配置,仅当 DEBUG_MODE 为 'true' 时才启用调试模式
|
||||||
|
- feat: |Admin| 账户管理页面新增多选批量操作功能(批量删除、批量清空收件箱、批量清空发件箱)
|
||||||
|
|
||||||
|
## v1.0.5
|
||||||
|
|
||||||
|
- feat: 新增 `DISABLE_CUSTOM_ADDRESS_NAME` 配置: 禁用自定义邮箱地址名称功能
|
||||||
|
- feat: 新增 `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` 配置: 创建地址时优先使用第一个域名
|
||||||
|
- feat: |UI| 主页增加进入极简模式按钮
|
||||||
|
- feat: |Webhook| 增加白名单开关功能,支持灵活控制访问权限
|
||||||
|
|
||||||
|
## v1.0.4
|
||||||
|
|
||||||
|
- feat: |UI| 优化极简模式主页, 增加全部邮件页面功能(删除/下载/附件/...), 可在 `外观` 中切换
|
||||||
|
- feat: admin 账号设置页面增加 `邮件转发规则` 配置
|
||||||
|
- feat: admin 账号设置页面增加 `禁止接收未知地址邮件` 配置
|
||||||
|
- feat: 邮件页面增加 上一封/下一封 按钮
|
||||||
|
|
||||||
|
## v1.0.3
|
||||||
|
|
||||||
|
- fix: 修复 github actions 部署问题
|
||||||
|
- feat: telegram /new 不指定域名时, 使用随机地址
|
||||||
|
|
||||||
|
## v1.0.2
|
||||||
|
|
||||||
|
- fix: 修复 oauth2 登录失败的问题
|
||||||
|
|
||||||
|
## v1.0.1
|
||||||
|
|
||||||
|
- feat: |UI| 增加极简模式主页, 可在 `外观` 中切换
|
||||||
|
- fix: 修复 oauth2 登录时,default role 不生效的问题
|
||||||
|
|
||||||
|
## v1.0.0
|
||||||
|
|
||||||
|
- fix: |UI| 修复 User 查看收件箱,不选择地址时,关键词查询不生效
|
||||||
|
- fix: 修复自动清理任务,时间为 0 时不生效的问题
|
||||||
|
- feat: 清理功能增加 创建 n 天前地址清理,n 天前未活跃地址清理
|
||||||
|
- fix: |IMAP Proxy| 修复 IMAP Proxy 服务器,无法查看新邮件的问题
|
||||||
|
|
||||||
|
## v0.10.0
|
||||||
|
|
||||||
|
- feat: 支持 User 查看收件箱,`/user_api/mails` 接口, 支持 `address` 和 `keyword` 过滤
|
||||||
|
- fix: 修复 Oauth2 登录获取 Token 时,一些 Oauth2 需要 `redirect_uri` 参数的问题
|
||||||
|
- feat: 用户访问网页时,如果 `user token` 在 7 天内过期,自动刷新
|
||||||
|
- feat: admin portal 中增加初始化 db 的功能
|
||||||
|
- feat: 增加 `ALWAYS_SHOW_ANNOUNCEMENT` 变量,用于配置是否总是显示公告
|
||||||
|
|
||||||
|
## v0.9.1
|
||||||
|
|
||||||
|
- feat: |UI| support google ads
|
||||||
|
- feat: |UI| 使用 shadow DOM 防止样式污染
|
||||||
|
- feat: |UI| 支持 URL jwt 参数自动登录邮箱,jwt 参数会覆盖浏览器中的 jwt
|
||||||
|
- fix: |CleanUP| 修复清理邮件时,清理时间超过 30 天报错的 bug
|
||||||
|
- feat: admin 用户管理页面: 增加 用户地址查看功能
|
||||||
|
- feat: | S3 附件| 增加 S3 附件删除功能
|
||||||
|
- feat: | Admin API| 增加 admin 绑定用户和地址的 api
|
||||||
|
- feat: | Oauth2 | Oatuh2 获取用户信息时,支持 `JSONPATH` 表达式
|
||||||
|
|
||||||
|
## v0.9.0
|
||||||
|
|
||||||
|
- feat: | Worker | 支持多语言
|
||||||
|
- feat: | Worker | `NO_LIMIT_SEND_ROLE` 配置支持多角色, 逗号分割
|
||||||
|
- feat: | Actions | build 里增加 `worker-with-wasm-mail-parser.zip` 支持 UI 部署带 `wasm` 的 worker
|
||||||
|
|
||||||
|
## v0.8.7
|
||||||
|
|
||||||
|
- fix: |UI| 修复移动设备日期显示问题
|
||||||
|
- feat: |Worker| 支持通过 `SMTP` 发送邮件, 使用 [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md)
|
||||||
|
|
||||||
|
## v0.8.6
|
||||||
|
|
||||||
|
- feat: |UI| 公告支持 html 格式
|
||||||
|
- feat: |UI| `COPYRIGHT` 支持 html 格式
|
||||||
|
- 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
|
## v0.7.4
|
||||||
|
|
||||||
- feat: UI 列表页面增加最小宽度
|
- feat: UI 列表页面增加最小宽度
|
||||||
|
|||||||
@@ -1,89 +1,188 @@
|
|||||||
# 使用 cloudflare 免费服务,搭建临时邮箱
|
<!-- markdownlint-disable-file MD033 MD045 -->
|
||||||
|
# Cloudflare 临时邮箱 - 免费搭建临时邮件服务
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://hellogithub.com/repository/2ccc64bb1ba346b480625f584aa19eb1" target="_blank">
|
<a href="https://temp-mail-docs.awsl.uk" target="_blank">
|
||||||
<img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=2ccc64bb1ba346b480625f584aa19eb1&claim_uid=FxNypXK7UQ9OECT" alt="Featured|HelloGitHub"/>
|
<img alt="docs" src="https://img.shields.io/badge/docs-grey?logo=vitepress">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest" target="_blank">
|
||||||
|
<img src="https://img.shields.io/github/v/release/dreamhunter2333/cloudflare_temp_email">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/LICENSE" target="_blank">
|
||||||
|
<img alt="MIT License" src="https://img.shields.io/github/license/dreamhunter2333/cloudflare_temp_email">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/graphs/contributors" target="_blank">
|
||||||
|
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/dreamhunter2333/cloudflare_temp_email">
|
||||||
|
</a>
|
||||||
|
<a href="">
|
||||||
|
<img alt="GitHub top language" src="https://img.shields.io/github/languages/top/dreamhunter2333/cloudflare_temp_email">
|
||||||
|
</a>
|
||||||
|
<a href="">
|
||||||
|
<img src="https://img.shields.io/github/last-commit/dreamhunter2333/cloudflare_temp_email">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://temp-mail-docs.awsl.uk" target="_blank">
|
<a href="https://hellogithub.com/repository/2ccc64bb1ba346b480625f584aa19eb1" target="_blank">
|
||||||
<img alt="docs" src="https://img.shields.io/badge/docs-grey?style=for-the-badge&logo=vitepress">
|
<img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=2ccc64bb1ba346b480625f584aa19eb1&claim_uid=FxNypXK7UQ9OECT" alt="Featured|HelloGitHub" height="30"/>
|
||||||
</a>
|
|
||||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest" target="_blank">
|
|
||||||
<img src="https://img.shields.io/github/v/release/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/LICENSE" target="_blank">
|
|
||||||
<img alt="MIT License" src="https://img.shields.io/github/license/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/graphs/contributors" target="_blank">
|
|
||||||
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="">
|
|
||||||
<img alt="GitHub top language" src="https://img.shields.io/github/languages/top/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="">
|
|
||||||
<img src="https://img.shields.io/github/last-commit/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="README.md">🇨🇳 中文文档</a> |
|
||||||
|
<a href="README_EN.md">🇺🇸 English Document</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
> 本项目仅供学习和个人用途,请勿将其用于任何违法行为,否则后果自负。
|
> 本项目仅供学习和个人用途,请勿将其用于任何违法行为,否则后果自负。
|
||||||
|
|
||||||
## [查看部署文档](https://temp-mail-docs.awsl.uk)
|
**🎉 一个功能完整的临时邮箱服务!**
|
||||||
|
|
||||||
[](https://deploy.workers.cloudflare.com/?url=https://github.com/dreamhunter2333/cloudflare_temp_email)
|
- 🆓 **完全免费** - 基于 Cloudflare 免费服务构建,零成本运行
|
||||||
|
- ⚡ **高性能** - Rust WASM 邮件解析,响应速度极快
|
||||||
|
- 🎨 **现代化界面** - 响应式设计,支持多语言,操作简便
|
||||||
|
- 🔐 **地址密码** - 支持为邮箱地址设置独立密码,增强安全性 (通过 `ENABLE_ADDRESS_PASSWORD` 启用)
|
||||||
|
|
||||||
[Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/github-action.html)
|
## 📚 部署文档 - 快速开始
|
||||||
|
|
||||||
[English Docs](https://temp-mail-docs.awsl.uk/en/)
|
[📖 部署文档](https://temp-mail-docs.awsl.uk) | [🚀 Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html)
|
||||||
|
|
||||||
## [CHANGELOG](CHANGELOG.md)
|
<a href="https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html">
|
||||||
|
<img src="https://deploy.workers.cloudflare.com/button" alt="Deploy to Cloudflare Workers" height="32">
|
||||||
|
</a>
|
||||||
|
|
||||||
## [在线演示](https://mail.awsl.uk/)
|
## 📝 更新日志
|
||||||
|
|
||||||
|
查看 [CHANGELOG](CHANGELOG.md) 了解最新更新内容。
|
||||||
|
|
||||||
|
## 🎯 在线体验
|
||||||
|
|
||||||
|
立即体验 → [https://mail.awsl.uk/](https://mail.awsl.uk/)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>📊 服务状态监控(点击收缩/展开)</summary>
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| [Backend](https://temp-email-api.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml)       |
|
| [Backend](https://temp-email-api.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml)       |
|
||||||
| [Frontend](https://mail.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml)       |
|
| [Frontend](https://mail.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml)       |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>⭐ Star History(点击收缩/展开)</summary>
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
|
</details>
|
||||||
- [查看部署文档](#查看部署文档)
|
|
||||||
- [CHANGELOG](#changelog)
|
|
||||||
- [在线演示](#在线演示)
|
|
||||||
- [功能/TODO](#功能todo)
|
|
||||||
- [Reference](#reference)
|
|
||||||
- [Join Community](#join-community)
|
|
||||||
|
|
||||||
## 功能/TODO
|
<details open>
|
||||||
|
<summary>📖 目录(点击收缩/展开)</summary>
|
||||||
|
|
||||||
- [x] 使用 `password` 重新登录之前的邮箱
|
- [Cloudflare 临时邮箱 - 免费搭建临时邮件服务](#cloudflare-临时邮箱---免费搭建临时邮件服务)
|
||||||
- [x] 获取自定义名字的邮箱,`admin` 可配置黑名单
|
- [📚 部署文档 - 快速开始](#-部署文档---快速开始)
|
||||||
- [x] 支持多语言
|
- [📝 更新日志](#-更新日志)
|
||||||
- [x] 增加访问密码,可作为私人站点
|
- [🎯 在线体验](#-在线体验)
|
||||||
- [x] 增加自动回复功能
|
- [✨ 核心功能](#-核心功能)
|
||||||
- [x] 增加查看 `附件` 功能
|
- [📧 邮件处理](#-邮件处理)
|
||||||
- [x] 使用 `rust wasm` 解析邮件
|
- [👥 用户管理](#-用户管理)
|
||||||
- [x] 支持发送邮件
|
- [🔧 管理功能](#-管理功能)
|
||||||
- [x] 支持 `DKIM`
|
- [🌐 多语言与界面](#-多语言与界面)
|
||||||
- [x] `admin` 后台创建无前缀邮箱
|
- [🤖 集成与扩展](#-集成与扩展)
|
||||||
- [x] 添加 `SMTP proxy server`,支持 `SMTP` 发送邮件, `IMAP` 查看邮件
|
- [🏗️ 技术架构](#️-技术架构)
|
||||||
|
- [🏛️ 系统架构](#️-系统架构)
|
||||||
|
- [🛠️ 技术栈](#️-技术栈)
|
||||||
|
- [📦 主要组件](#-主要组件)
|
||||||
|
- [🌟 加入社区](#-加入社区)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## ✨ 核心功能
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary>✨ 核心功能详情(点击收缩/展开)</summary>
|
||||||
|
|
||||||
|
### 📧 邮件处理
|
||||||
|
|
||||||
|
- [x] 使用 `rust wasm` 解析邮件,解析速度快,几乎所有邮件都能解析,node 的解析模块解析邮件失败的邮件,rust wasm 也能解析成功
|
||||||
|
- [x] 支持发送邮件,支持 `DKIM` 验证
|
||||||
|
- [x] 支持 `SMTP` 和 `Resend` 等多种发送方式
|
||||||
|
- [x] 增加查看 `附件` 功能,支持附件图片显示
|
||||||
|
- [x] 支持 S3 附件存储和删除功能
|
||||||
|
- [x] 垃圾邮件检测和黑白名单配置
|
||||||
|
- [x] 邮件转发功能,支持全局转发地址
|
||||||
|
|
||||||
|
### 👥 用户管理
|
||||||
|
|
||||||
|
- [x] 使用 `凭证` 重新登录之前的邮箱
|
||||||
- [x] 添加完整的用户注册登录功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证切换不同邮箱
|
- [x] 添加完整的用户注册登录功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证切换不同邮箱
|
||||||
- [x] `Telegram Bot` 使用,以及 `Telegram` 推送
|
- [x] 支持 `OAuth2` 第三方登录(Github、Authentik 等)
|
||||||
|
- [x] 支持 `Passkey` 无密码登录
|
||||||
|
- [x] 用户角色管理,支持多角色域名和前缀配置
|
||||||
|
- [x] 用户收件箱查看,支持地址和关键词过滤
|
||||||
|
|
||||||
## Reference
|
### 🔧 管理功能
|
||||||
|
|
||||||
- Cloudflare D1 作为数据库
|
- [x] 完整的 admin 控制台
|
||||||
- 使用 Cloudflare Pages 部署前端
|
- [x] `admin` 后台创建无前缀邮箱
|
||||||
- 使用 Cloudflare Workers 部署后端
|
- [x] admin 用户管理页面,增加用户地址查看功能
|
||||||
- email 转发使用 Cloudflare Email Routing
|
- [x] 定时清理功能,支持多种清理策略
|
||||||
|
- [x] 获取自定义名字的邮箱,`admin` 可配置黑名单
|
||||||
|
- [x] 增加访问密码,可作为私人站点
|
||||||
|
|
||||||
## Join Community
|
### 🌐 多语言与界面
|
||||||
|
|
||||||
|
- [x] 前后台均支持多语言
|
||||||
|
- [x] 现代化 UI 设计,支持响应式布局
|
||||||
|
- [x] 支持 Google Ads 集成
|
||||||
|
- [x] 使用 shadow DOM 防止样式污染
|
||||||
|
- [x] 支持 URL JWT 参数自动登录
|
||||||
|
|
||||||
|
### 🤖 集成与扩展
|
||||||
|
|
||||||
|
- [x] 完整的 `Telegram Bot` 支持,以及 `Telegram` 推送,Telegram Bot 小程序
|
||||||
|
- [x] 添加 `SMTP proxy server`,支持 `SMTP` 发送邮件,`IMAP` 查看邮件
|
||||||
|
- [x] Webhook 支持,消息推送集成
|
||||||
|
- [x] 支持 `CF Turnstile` 人机验证
|
||||||
|
- [x] 限流配置,防止滥用
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 🏗️ 技术架构
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>🏗️ 技术架构详情(点击收缩/展开)</summary>
|
||||||
|
|
||||||
|
### 🏛️ 系统架构
|
||||||
|
|
||||||
|
- **数据库**: Cloudflare D1 作为主数据库
|
||||||
|
- **前端部署**: 使用 Cloudflare Pages 部署前端
|
||||||
|
- **后端部署**: 使用 Cloudflare Workers 部署后端
|
||||||
|
- **邮件转发**: 使用 Cloudflare Email Routing
|
||||||
|
|
||||||
|
### 🛠️ 技术栈
|
||||||
|
|
||||||
|
- **前端**: Vue 3 + Vite + TypeScript
|
||||||
|
- **后端**: TypeScript + Cloudflare Workers
|
||||||
|
- **邮件解析**: Rust WASM (mail-parser-wasm)
|
||||||
|
- **数据库**: Cloudflare D1 (SQLite)
|
||||||
|
- **存储**: Cloudflare KV + R2 (可选 S3)
|
||||||
|
- **代理服务**: Python SMTP/IMAP Proxy Server
|
||||||
|
|
||||||
|
### 📦 主要组件
|
||||||
|
|
||||||
|
- **Worker**: 核心后端服务
|
||||||
|
- **Frontend**: Vue 3 用户界面
|
||||||
|
- **Mail Parser WASM**: Rust 邮件解析模块
|
||||||
|
- **SMTP Proxy Server**: Python 邮件代理服务
|
||||||
|
- **Pages Functions**: Cloudflare Pages 中间件
|
||||||
|
- **Documentation**: VitePress 文档站点
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## 🌟 加入社区
|
||||||
|
|
||||||
- [Discord](https://discord.gg/dQEwTWhA6Q)
|
|
||||||
- [Telegram](https://t.me/cloudflare_temp_email)
|
- [Telegram](https://t.me/cloudflare_temp_email)
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<!-- markdownlint-disable-file MD033 MD045 -->
|
||||||
|
# Cloudflare Temp Email
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="README.md">🇨🇳 中文</a> |
|
||||||
|
<a href="README_EN.md">🇺🇸 English</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
**A fully-featured temporary email service built on Cloudflare's free services.**
|
||||||
|
|
||||||
|
> This project is for learning and personal use only.
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
- [📖 Documentation](https://temp-mail-docs.awsl.uk/en/)
|
||||||
|
- [🎯 Live Demo](https://mail.awsl.uk/)
|
||||||
|
- [📝 CHANGELOG](CHANGELOG.md)
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://temp-mail-docs.awsl.uk/en/guide/actions/github-action.html">
|
||||||
|
<img src="https://deploy.workers.cloudflare.com/button" alt="Deploy to Cloudflare Workers">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
## ✨ Key Features
|
||||||
|
|
||||||
|
- **� Email Processing**: Rust WASM parser, SMTP/IMAP support, attachments, auto-reply
|
||||||
|
- **👥 User Management**: OAuth2 login, Passkey authentication, role management
|
||||||
|
- **🌐 Admin Panel**: Complete admin console, user management, scheduled cleanup
|
||||||
|
- **🤖 Integrations**: Telegram Bot, webhooks, CAPTCHA, rate limiting
|
||||||
|
- **� Modern UI**: Multi-language, responsive design, JWT auto-login
|
||||||
|
|
||||||
|
## 🏗️ Tech Stack
|
||||||
|
|
||||||
|
- **Frontend**: Vue 3 + TypeScript + Vite
|
||||||
|
- **Backend**: Cloudflare Workers + D1 Database
|
||||||
|
- **Email**: Cloudflare Email Routing + Rust WASM Parser
|
||||||
|
- **Storage**: Cloudflare KV + R2 (optional S3)
|
||||||
|
|
||||||
|
## 🌟 Community
|
||||||
|
|
||||||
|
- [Telegram](https://t.me/cloudflare_temp_email)
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT License - see [LICENSE](LICENSE) for details.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE
|
||||||
|
address
|
||||||
|
ADD
|
||||||
|
password TEXT;
|
||||||
@@ -9,15 +9,22 @@ CREATE TABLE IF NOT EXISTS raw_mails (
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_raw_mails_created_at ON raw_mails(created_at);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS address (
|
CREATE TABLE IF NOT EXISTS address (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT UNIQUE,
|
name TEXT UNIQUE,
|
||||||
|
password TEXT,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
|
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_address_created_at ON address(created_at);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_address_updated_at ON address(updated_at);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
source_prefix TEXT,
|
source_prefix TEXT,
|
||||||
@@ -50,6 +57,8 @@ CREATE TABLE IF NOT EXISTS sendbox (
|
|||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
|
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sendbox_created_at ON sendbox(created_at);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT,
|
value TEXT,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudflare_temp_email",
|
"name": "cloudflare_temp_email",
|
||||||
"version": "0.7.4",
|
"version": "1.0.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -8,7 +8,10 @@
|
|||||||
"build": "vite build -m prod --emptyOutDir",
|
"build": "vite build -m prod --emptyOutDir",
|
||||||
"build:release": "vite build -m example --emptyOutDir",
|
"build:release": "vite build -m example --emptyOutDir",
|
||||||
"build:pages": "vite build -m pages --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": "VITE_IS_TELEGRAM=true vite build -m prod --emptyOutDir",
|
||||||
|
"build:telegram:pages": "VITE_IS_TELEGRAM=true vite build -m pages --emptyOutDir",
|
||||||
|
"build:telegram:release": "VITE_IS_TELEGRAM=true vite build -m example --emptyOutDir",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"deploy:telegram": "npm run build:telegram && wrangler pages deploy ./dist --branch production",
|
"deploy:telegram": "npm run build:telegram && wrangler pages deploy ./dist --branch production",
|
||||||
"deploy:actions:telegram": "npm run build:telegram && wrangler pages deploy ./dist",
|
"deploy:actions:telegram": "npm run build:telegram && wrangler pages deploy ./dist",
|
||||||
@@ -17,33 +20,35 @@
|
|||||||
"deploy:actions": "npm run build && wrangler pages deploy ./dist"
|
"deploy:actions": "npm run build && wrangler pages deploy ./dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "^10.0.0",
|
"@simplewebauthn/browser": "10.0.0",
|
||||||
"@unhead/vue": "^1.9.16",
|
"@unhead/vue": "^1.11.20",
|
||||||
"@vicons/material": "^0.12.0",
|
"@vueuse/core": "^12.8.2",
|
||||||
"@vueuse/core": "^10.11.1",
|
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.12.2",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"mail-parser-wasm": "^0.1.8",
|
"mail-parser-wasm": "^0.2.1",
|
||||||
"naive-ui": "^2.39.0",
|
"naive-ui": "^2.43.1",
|
||||||
"postal-mime": "^2.2.7",
|
"postal-mime": "^2.4.4",
|
||||||
"vooks": "^0.2.12",
|
"vooks": "^0.2.12",
|
||||||
"vue": "^3.4.37",
|
"vue": "^3.5.22",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-i18n": "^9.13.1",
|
"vue-i18n": "^11.1.12",
|
||||||
"vue-router": "^4.4.3"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vicons/fa": "^0.12.0",
|
"@vicons/fa": "^0.13.0",
|
||||||
"@vitejs/plugin-vue": "^5.1.2",
|
"@vicons/material": "^0.13.0",
|
||||||
"unplugin-auto-import": "^0.18.2",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"unplugin-vue-components": "^0.27.4",
|
"unplugin-auto-import": "^19.3.0",
|
||||||
"vite": "^5.4.0",
|
"unplugin-vue-components": "^28.8.0",
|
||||||
"vite-plugin-pwa": "^0.19.8",
|
"vite": "^6.3.6",
|
||||||
"vite-plugin-top-level-await": "^1.4.4",
|
"vite-plugin-pwa": "^1.0.3",
|
||||||
"vite-plugin-wasm": "^3.3.0",
|
"vite-plugin-top-level-await": "^1.6.0",
|
||||||
"workbox-window": "^7.1.0",
|
"vite-plugin-wasm": "^3.5.0",
|
||||||
"wrangler": "^3.70.0"
|
"workbox-build": "^7.3.0",
|
||||||
}
|
"workbox-window": "^7.3.0",
|
||||||
|
"wrangler": "^4.40.1"
|
||||||
|
},
|
||||||
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { darkTheme, NGlobalStyle, zhCN } from 'naive-ui'
|
import { darkTheme, NGlobalStyle, zhCN } from 'naive-ui'
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
|
import { useScript } from '@unhead/vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useGlobalState } from './store'
|
import { useGlobalState } from './store'
|
||||||
import { useIsMobile } from './utils/composables'
|
import { useIsMobile } from './utils/composables'
|
||||||
@@ -11,12 +12,15 @@ import { api } from './api'
|
|||||||
const {
|
const {
|
||||||
isDark, loading, useSideMargin, telegramApp, isTelegram
|
isDark, loading, useSideMargin, telegramApp, isTelegram
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
|
const adClient = import.meta.env.VITE_GOOGLE_AD_CLIENT;
|
||||||
|
const adSlot = import.meta.env.VITE_GOOGLE_AD_SLOT;
|
||||||
const { locale } = useI18n({});
|
const { locale } = useI18n({});
|
||||||
const theme = computed(() => isDark.value ? darkTheme : null)
|
const theme = computed(() => isDark.value ? darkTheme : null)
|
||||||
const localeConfig = computed(() => locale.value == 'zh' ? zhCN : null)
|
const localeConfig = computed(() => locale.value == 'zh' ? zhCN : null)
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const showSideMargin = computed(() => !isMobile.value && useSideMargin.value);
|
const showSideMargin = computed(() => !isMobile.value && useSideMargin.value);
|
||||||
|
const showAd = computed(() => !isMobile.value && adClient && adSlot);
|
||||||
|
const gridMaxCols = computed(() => showAd.value ? 8 : 12);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|
||||||
@@ -37,6 +41,18 @@ onMounted(async () => {
|
|||||||
document.body.appendChild(script);
|
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
|
// check if telegram is enabled
|
||||||
const enableTelegram = import.meta.env.VITE_IS_TELEGRAM;
|
const enableTelegram = import.meta.env.VITE_IS_TELEGRAM;
|
||||||
if (
|
if (
|
||||||
@@ -61,24 +77,36 @@ onMounted(async () => {
|
|||||||
<n-config-provider :locale="localeConfig" :theme="theme">
|
<n-config-provider :locale="localeConfig" :theme="theme">
|
||||||
<n-global-style />
|
<n-global-style />
|
||||||
<n-spin description="loading..." :show="loading">
|
<n-spin description="loading..." :show="loading">
|
||||||
<n-message-provider container-style="margin-top: 20px;">
|
<n-notification-provider container-style="margin-top: 60px;">
|
||||||
<n-grid x-gap="12" :cols="12">
|
<n-message-provider container-style="margin-top: 20px;">
|
||||||
<n-gi v-if="showSideMargin" span="1"></n-gi>
|
<n-grid x-gap="12" :cols="gridMaxCols">
|
||||||
<n-gi :span="!showSideMargin ? 12 : 10">
|
<n-gi v-if="showSideMargin" span="1">
|
||||||
<div class="main">
|
<div class="side" v-if="showAd">
|
||||||
<n-space vertical>
|
<ins class="adsbygoogle" style="display:block" :data-ad-client="adClient" :data-ad-slot="adSlot"
|
||||||
<n-layout style="min-height: 80vh;">
|
data-ad-format="auto" data-full-width-responsive="true"></ins>
|
||||||
<Header />
|
</div>
|
||||||
<router-view></router-view>
|
</n-gi>
|
||||||
</n-layout>
|
<n-gi :span="!showSideMargin ? gridMaxCols : (gridMaxCols - 2)">
|
||||||
<Footer />
|
<div class="main">
|
||||||
</n-space>
|
<n-space vertical>
|
||||||
</div>
|
<n-layout style="min-height: 80vh;">
|
||||||
</n-gi>
|
<Header />
|
||||||
<n-gi v-if="showSideMargin" span="1"></n-gi>
|
<router-view></router-view>
|
||||||
</n-grid>
|
</n-layout>
|
||||||
<n-back-top />
|
<Footer />
|
||||||
</n-message-provider>
|
</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-spin>
|
||||||
</n-config-provider>
|
</n-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
|
import { h } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import i18n from '../i18n'
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||||
const {
|
const {
|
||||||
loading, auth, jwt, settings, openSettings,
|
loading, auth, jwt, settings, openSettings,
|
||||||
@@ -21,7 +24,8 @@ const apiFetch = async (path, options = {}) => {
|
|||||||
method: options.method || 'GET',
|
method: options.method || 'GET',
|
||||||
data: options.body || null,
|
data: options.body || null,
|
||||||
headers: {
|
headers: {
|
||||||
'x-user-token': userJwt.value,
|
'x-lang': i18n.global.locale.value,
|
||||||
|
'x-user-token': options.userJwt || userJwt.value,
|
||||||
'x-user-access-token': userSettings.value.access_token,
|
'x-user-access-token': userSettings.value.access_token,
|
||||||
'x-custom-auth': auth.value,
|
'x-custom-auth': auth.value,
|
||||||
'x-admin-auth': adminAuth.value,
|
'x-admin-auth': adminAuth.value,
|
||||||
@@ -31,14 +35,12 @@ const apiFetch = async (path, options = {}) => {
|
|||||||
});
|
});
|
||||||
if (response.status === 401 && path.startsWith("/admin")) {
|
if (response.status === 401 && path.startsWith("/admin")) {
|
||||||
showAdminAuth.value = true;
|
showAdminAuth.value = true;
|
||||||
throw new Error("Unauthorized, your admin password is wrong")
|
|
||||||
}
|
}
|
||||||
if (response.status === 401 && openSettings.value.auth) {
|
if (response.status === 401 && openSettings.value.auth) {
|
||||||
showAuth.value = true;
|
showAuth.value = true;
|
||||||
throw new Error("Unauthorized, you access password is wrong")
|
|
||||||
}
|
}
|
||||||
if (response.status >= 300) {
|
if (response.status >= 300) {
|
||||||
throw new Error(`${response.status} ${response.data}` || "error");
|
throw new Error(`[${response.status}]: ${response.data}` || "error");
|
||||||
}
|
}
|
||||||
const data = response.data;
|
const data = response.data;
|
||||||
return data;
|
return data;
|
||||||
@@ -52,7 +54,7 @@ const apiFetch = async (path, options = {}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOpenSettings = async (message) => {
|
const getOpenSettings = async (message, notification) => {
|
||||||
try {
|
try {
|
||||||
const res = await api.fetch("/open_api/settings");
|
const res = await api.fetch("/open_api/settings");
|
||||||
const domainLabels = res["domainLabels"] || [];
|
const domainLabels = res["domainLabels"] || [];
|
||||||
@@ -75,6 +77,8 @@ const getOpenSettings = async (message) => {
|
|||||||
}),
|
}),
|
||||||
adminContact: res["adminContact"] || "",
|
adminContact: res["adminContact"] || "",
|
||||||
enableUserCreateEmail: res["enableUserCreateEmail"] || false,
|
enableUserCreateEmail: res["enableUserCreateEmail"] || false,
|
||||||
|
disableAnonymousUserCreateEmail: res["disableAnonymousUserCreateEmail"] || false,
|
||||||
|
disableCustomAddressName: res["disableCustomAddressName"] || false,
|
||||||
enableUserDeleteEmail: res["enableUserDeleteEmail"] || false,
|
enableUserDeleteEmail: res["enableUserDeleteEmail"] || false,
|
||||||
enableAutoReply: res["enableAutoReply"] || false,
|
enableAutoReply: res["enableAutoReply"] || false,
|
||||||
enableIndexAbout: res["enableIndexAbout"] || false,
|
enableIndexAbout: res["enableIndexAbout"] || false,
|
||||||
@@ -82,16 +86,23 @@ const getOpenSettings = async (message) => {
|
|||||||
cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "",
|
cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "",
|
||||||
enableWebhook: res["enableWebhook"] || false,
|
enableWebhook: res["enableWebhook"] || false,
|
||||||
isS3Enabled: res["isS3Enabled"] || false,
|
isS3Enabled: res["isS3Enabled"] || false,
|
||||||
|
enableAddressPassword: res["enableAddressPassword"] || false,
|
||||||
});
|
});
|
||||||
if (openSettings.value.needAuth) {
|
if (openSettings.value.needAuth) {
|
||||||
showAuth.value = true;
|
showAuth.value = true;
|
||||||
}
|
}
|
||||||
if (openSettings.value.announcement && openSettings.value.announcement != announcement.value) {
|
if (openSettings.value.announcement
|
||||||
|
&& !openSettings.value.fetched
|
||||||
|
&& (openSettings.value.announcement != announcement.value
|
||||||
|
|| openSettings.value.alwaysShowAnnouncement)
|
||||||
|
) {
|
||||||
announcement.value = openSettings.value.announcement;
|
announcement.value = openSettings.value.announcement;
|
||||||
message.info(announcement.value, {
|
notification.info({
|
||||||
showIcon: false,
|
content: () => {
|
||||||
duration: 0,
|
return h("div", {
|
||||||
closable: true
|
innerHTML: announcement.value
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -134,6 +145,19 @@ const getUserSettings = async (message) => {
|
|||||||
if (!userJwt.value) return;
|
if (!userJwt.value) return;
|
||||||
const res = await api.fetch("/user_api/settings")
|
const res = await api.fetch("/user_api/settings")
|
||||||
Object.assign(userSettings.value, res)
|
Object.assign(userSettings.value, res)
|
||||||
|
// auto refresh user jwt
|
||||||
|
if (userSettings.value.new_user_token) {
|
||||||
|
try {
|
||||||
|
await api.fetch("/user_api/settings", {
|
||||||
|
userJwt: userSettings.value.new_user_token,
|
||||||
|
})
|
||||||
|
userJwt.value = userSettings.value.new_user_token;
|
||||||
|
console.log("User JWT updated successfully");
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.error("Failed to update user JWT", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message?.error(error.message || "error");
|
message?.error(error.message || "error");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { watch, onMounted, ref, onBeforeUnmount } from "vue";
|
import { watch, onMounted, ref, onBeforeUnmount, computed } from "vue";
|
||||||
import { useMessage } from 'naive-ui'
|
import { useMessage } from 'naive-ui'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
import { CloudDownloadRound, ReplyFilled } from '@vicons/material'
|
import { CloudDownloadRound, ArrowBackIosNewFilled, ArrowForwardIosFilled } from '@vicons/material'
|
||||||
import { useIsMobile } from '../utils/composables'
|
import { useIsMobile } from '../utils/composables'
|
||||||
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
import { processItem } from '../utils/email-parser'
|
||||||
|
import { utcToLocalDate } from '../utils';
|
||||||
|
import MailContentRenderer from "./MailContentRenderer.vue";
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
@@ -49,11 +51,10 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isDark, mailboxSplitSize, indexTab, loading,
|
isDark, mailboxSplitSize, indexTab, loading, useUTCDate,
|
||||||
useIframeShowMail, sendMailModel, preferShowTextMail
|
autoRefresh, configAutoRefreshInterval, sendMailModel
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const autoRefresh = ref(false)
|
const autoRefreshInterval = ref(configAutoRefreshInterval.value)
|
||||||
const autoRefreshInterval = ref(30)
|
|
||||||
const data = ref([])
|
const data = ref([])
|
||||||
const timer = ref(null)
|
const timer = ref(null)
|
||||||
|
|
||||||
@@ -61,10 +62,49 @@ const count = ref(0)
|
|||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
|
|
||||||
const showAttachments = ref(false)
|
const canGoPrevMail = computed(() => {
|
||||||
const curAttachments = ref([])
|
if (!curMail.value) return false
|
||||||
|
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
|
||||||
|
return currentIndex > 0 || page.value > 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const canGoNextMail = computed(() => {
|
||||||
|
if (!curMail.value) return false
|
||||||
|
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
|
||||||
|
return currentIndex < data.value.length - 1 || count.value > page.value * pageSize.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const prevMail = async () => {
|
||||||
|
if (!canGoPrevMail.value) return
|
||||||
|
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
|
||||||
|
|
||||||
|
if (currentIndex > 0) {
|
||||||
|
curMail.value = data.value[currentIndex - 1]
|
||||||
|
} else if (page.value > 1) {
|
||||||
|
page.value--
|
||||||
|
await refresh()
|
||||||
|
if (data.value.length > 0) {
|
||||||
|
curMail.value = data.value[data.value.length - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextMail = async () => {
|
||||||
|
if (!canGoNextMail.value) return
|
||||||
|
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
|
||||||
|
|
||||||
|
if (currentIndex < data.value.length - 1) {
|
||||||
|
curMail.value = data.value[currentIndex + 1]
|
||||||
|
} else if (count.value > page.value * pageSize.value) {
|
||||||
|
page.value++
|
||||||
|
await refresh()
|
||||||
|
if (data.value.length > 0) {
|
||||||
|
curMail.value = data.value[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const curMail = ref(null);
|
const curMail = ref(null);
|
||||||
const showTextMail = ref(preferShowTextMail.value)
|
|
||||||
|
|
||||||
const multiActionMode = ref(false)
|
const multiActionMode = ref(false)
|
||||||
const showMultiActionDownload = ref(false)
|
const showMultiActionDownload = ref(false)
|
||||||
@@ -85,6 +125,7 @@ const { t } = useI18n({
|
|||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
deleteMailTip: 'Are you sure you want to delete mail?',
|
deleteMailTip: 'Are you sure you want to delete mail?',
|
||||||
reply: 'Reply',
|
reply: 'Reply',
|
||||||
|
forwardMail: 'Forward',
|
||||||
showTextMail: 'Show Text Mail',
|
showTextMail: 'Show Text Mail',
|
||||||
showHtmlMail: 'Show Html Mail',
|
showHtmlMail: 'Show Html Mail',
|
||||||
saveToS3: 'Save to S3',
|
saveToS3: 'Save to S3',
|
||||||
@@ -92,6 +133,8 @@ const { t } = useI18n({
|
|||||||
cancelMultiAction: 'Cancel Multi Action',
|
cancelMultiAction: 'Cancel Multi Action',
|
||||||
selectAll: 'Select All of This Page',
|
selectAll: 'Select All of This Page',
|
||||||
unselectAll: 'Unselect All',
|
unselectAll: 'Unselect All',
|
||||||
|
prevMail: 'Previous',
|
||||||
|
nextMail: 'Next',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
success: '成功',
|
success: '成功',
|
||||||
@@ -104,6 +147,7 @@ const { t } = useI18n({
|
|||||||
delete: '删除',
|
delete: '删除',
|
||||||
deleteMailTip: '确定要删除邮件吗?',
|
deleteMailTip: '确定要删除邮件吗?',
|
||||||
reply: '回复',
|
reply: '回复',
|
||||||
|
forwardMail: '转发',
|
||||||
showTextMail: '显示纯文本邮件',
|
showTextMail: '显示纯文本邮件',
|
||||||
showHtmlMail: '显示HTML邮件',
|
showHtmlMail: '显示HTML邮件',
|
||||||
saveToS3: '保存到S3',
|
saveToS3: '保存到S3',
|
||||||
@@ -111,19 +155,23 @@ const { t } = useI18n({
|
|||||||
cancelMultiAction: '取消多选',
|
cancelMultiAction: '取消多选',
|
||||||
selectAll: '全选本页',
|
selectAll: '全选本页',
|
||||||
unselectAll: '取消全选',
|
unselectAll: '取消全选',
|
||||||
|
prevMail: '上一封',
|
||||||
|
nextMail: '下一封',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const setupAutoRefresh = async (autoRefresh) => {
|
const setupAutoRefresh = async (autoRefresh) => {
|
||||||
// auto refresh every 30 seconds
|
// auto refresh every configAutoRefreshInterval seconds
|
||||||
autoRefreshInterval.value = 30;
|
autoRefreshInterval.value = configAutoRefreshInterval.value;
|
||||||
if (autoRefresh) {
|
if (autoRefresh) {
|
||||||
|
clearInterval(timer.value);
|
||||||
timer.value = setInterval(async () => {
|
timer.value = setInterval(async () => {
|
||||||
|
if (loading.value) return;
|
||||||
autoRefreshInterval.value--;
|
autoRefreshInterval.value--;
|
||||||
if (autoRefreshInterval.value <= 0) {
|
if (autoRefreshInterval.value <= 0) {
|
||||||
autoRefreshInterval.value = 30;
|
autoRefreshInterval.value = configAutoRefreshInterval.value;
|
||||||
await refresh();
|
await backFirstPageAndRefresh();
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
} else {
|
} else {
|
||||||
@@ -134,7 +182,7 @@ const setupAutoRefresh = async (autoRefresh) => {
|
|||||||
|
|
||||||
watch(autoRefresh, async (autoRefresh, old) => {
|
watch(autoRefresh, async (autoRefresh, old) => {
|
||||||
setupAutoRefresh(autoRefresh)
|
setupAutoRefresh(autoRefresh)
|
||||||
})
|
}, { immediate: true })
|
||||||
|
|
||||||
watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
|
watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
|
||||||
if (page !== oldPage || pageSize !== oldPageSize) {
|
if (page !== oldPage || pageSize !== oldPageSize) {
|
||||||
@@ -167,6 +215,11 @@ const refresh = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const backFirstPageAndRefresh = async () => {
|
||||||
|
page.value = 1;
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
|
||||||
const clickRow = async (row) => {
|
const clickRow = async (row) => {
|
||||||
if (multiActionMode.value) {
|
if (multiActionMode.value) {
|
||||||
row.checked = !row.checked;
|
row.checked = !row.checked;
|
||||||
@@ -175,10 +228,6 @@ const clickRow = async (row) => {
|
|||||||
curMail.value = row;
|
curMail.value = row;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAttachments = (attachments) => {
|
|
||||||
curAttachments.value = attachments;
|
|
||||||
showAttachments.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mailItemClass = (row) => {
|
const mailItemClass = (row) => {
|
||||||
return curMail.value && row.id == curMail.value.id ? (isDark.value ? 'overlay overlay-dark-backgroud' : 'overlay overlay-light-backgroud') : '';
|
return curMail.value && row.id == curMail.value.id ? (isDark.value ? 'overlay overlay-dark-backgroud' : 'overlay overlay-light-backgroud') : '';
|
||||||
@@ -214,18 +263,21 @@ const replyMail = async () => {
|
|||||||
indexTab.value = 'sendmail';
|
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) => {
|
const onSpiltSizeChange = (size) => {
|
||||||
mailboxSplitSize.value = size;
|
mailboxSplitSize.value = size;
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachmentLoding = ref(false)
|
|
||||||
const saveToS3Proxy = async (filename, blob) => {
|
const saveToS3Proxy = async (filename, blob) => {
|
||||||
attachmentLoding.value = true
|
await props.saveToS3(curMail.value.id, filename, blob);
|
||||||
try {
|
|
||||||
await props.saveToS3(curMail.value.id, filename, blob);
|
|
||||||
} finally {
|
|
||||||
attachmentLoding.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const multiActionModeClick = (enableMulti) => {
|
const multiActionModeClick = (enableMulti) => {
|
||||||
@@ -354,7 +406,7 @@ onBeforeUnmount(() => {
|
|||||||
{{ t('autoRefresh') }}
|
{{ t('autoRefresh') }}
|
||||||
</template>
|
</template>
|
||||||
</n-switch>
|
</n-switch>
|
||||||
<n-button @click="refresh" type="primary" tertiary>
|
<n-button @click="backFirstPageAndRefresh" type="primary" tertiary>
|
||||||
{{ t('refresh') }}
|
{{ t('refresh') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -362,7 +414,7 @@ onBeforeUnmount(() => {
|
|||||||
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
|
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
|
||||||
:on-update:size="onSpiltSizeChange">
|
:on-update:size="onSpiltSizeChange">
|
||||||
<template #1>
|
<template #1>
|
||||||
<div style="overflow: auto; height: 80vh;">
|
<div style="overflow: auto; min-height: 50vh; max-height: 100vh;">
|
||||||
<n-list hoverable clickable>
|
<n-list hoverable clickable>
|
||||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||||
:class="mailItemClass(row)">
|
:class="mailItemClass(row)">
|
||||||
@@ -375,13 +427,17 @@ onBeforeUnmount(() => {
|
|||||||
ID: {{ row.id }}
|
ID: {{ row.id }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
{{ `${row.created_at} UTC` }}
|
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<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>
|
||||||
<n-tag v-if="showEMailTo" type="info">
|
<n-tag v-if="showEMailTo" type="info">
|
||||||
TO: {{ row.address }}
|
<n-ellipsis style="max-width: 240px;">
|
||||||
|
TO: {{ row.address }}
|
||||||
|
</n-ellipsis>
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</template>
|
</template>
|
||||||
</n-thing>
|
</n-thing>
|
||||||
@@ -390,53 +446,31 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #2>
|
<template #2>
|
||||||
|
<div v-if="curMail" style="margin: 8px;">
|
||||||
|
<n-flex justify="space-between">
|
||||||
|
<n-button @click="prevMail" :disabled="!canGoPrevMail" text size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ArrowBackIosNewFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('prevMail') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="nextMail" :disabled="!canGoNextMail" text size="small" icon-placement="right">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ArrowForwardIosFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('nextMail') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</div>
|
||||||
<n-card :bordered="false" embedded v-if="curMail" class="mail-item" :title="curMail.subject"
|
<n-card :bordered="false" embedded v-if="curMail" class="mail-item" :title="curMail.subject"
|
||||||
style="overflow: auto; max-height: 100vh;">
|
style="overflow: auto; max-height: 100vh;">
|
||||||
<n-space>
|
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
|
||||||
<n-tag type="info">
|
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
|
||||||
ID: {{ curMail.id }}
|
:onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail" :onSaveToS3="saveToS3Proxy" />
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
{{ `${curMail.created_at} UTC` }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
FROM: {{ curMail.source }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag v-if="showEMailTo" type="info">
|
|
||||||
TO: {{ curMail.address }}
|
|
||||||
</n-tag>
|
|
||||||
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
|
||||||
</template>
|
|
||||||
{{ t('deleteMailTip') }}
|
|
||||||
</n-popconfirm>
|
|
||||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
|
||||||
@click="getAttachments(curMail.attachments)">
|
|
||||||
{{ t('attachments') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
|
||||||
:href="getDownloadEmlUrl(curMail.raw)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="CloudDownloadRound" />
|
|
||||||
</template>
|
|
||||||
{{ t('downloadMail') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="replyMail">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="ReplyFilled" />
|
|
||||||
</template>
|
|
||||||
{{ t('reply') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
|
||||||
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
<pre v-if="showTextMail" style="margin-top: 10px;">{{ curMail.text }}</pre>
|
|
||||||
<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>
|
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-card :bordered="false" embedded class="mail-item" v-else>
|
<n-card :bordered="false" embedded class="mail-item" v-else>
|
||||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||||
@@ -458,7 +492,7 @@ onBeforeUnmount(() => {
|
|||||||
{{ t('autoRefresh') }}
|
{{ t('autoRefresh') }}
|
||||||
</template>
|
</template>
|
||||||
</n-switch>
|
</n-switch>
|
||||||
<n-button @click="refresh" tertiary size="small" type="primary">
|
<n-button @click="backFirstPageAndRefresh" tertiary size="small" type="primary">
|
||||||
{{ t('refresh') }}
|
{{ t('refresh') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -471,10 +505,10 @@ onBeforeUnmount(() => {
|
|||||||
ID: {{ row.id }}
|
ID: {{ row.id }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
{{ `${row.created_at} UTC` }}
|
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
FROM: {{ row.source }}
|
{{ showEMailTo ? "FROM: " + row.source : row.source }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag v-if="showEMailTo" type="info">
|
<n-tag v-if="showEMailTo" type="info">
|
||||||
TO: {{ row.address }}
|
TO: {{ row.address }}
|
||||||
@@ -488,83 +522,14 @@ onBeforeUnmount(() => {
|
|||||||
style="height: 80vh;">
|
style="height: 80vh;">
|
||||||
<n-drawer-content :title="curMail ? curMail.subject : ''" closable>
|
<n-drawer-content :title="curMail ? curMail.subject : ''" closable>
|
||||||
<n-card :bordered="false" embedded style="overflow: auto;">
|
<n-card :bordered="false" embedded style="overflow: auto;">
|
||||||
<n-space>
|
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
|
||||||
<n-tag type="info">
|
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
|
||||||
ID: {{ curMail.id }}
|
:useUTCDate="useUTCDate" :onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail"
|
||||||
</n-tag>
|
:onSaveToS3="saveToS3Proxy" />
|
||||||
<n-tag type="info">
|
|
||||||
{{ `${curMail.created_at} UTC` }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
FROM: {{ curMail.source }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag v-if="showEMailTo" type="info">
|
|
||||||
TO: {{ curMail.address }}
|
|
||||||
</n-tag>
|
|
||||||
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
|
|
||||||
<template #trigger>
|
|
||||||
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
|
||||||
</template>
|
|
||||||
{{ t('deleteMailTip') }}
|
|
||||||
</n-popconfirm>
|
|
||||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
|
||||||
@click="getAttachments(curMail.attachments)">
|
|
||||||
{{ t('attachments') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
|
||||||
:href="getDownloadEmlUrl(curMail)">
|
|
||||||
<n-icon :component="CloudDownloadRound" />
|
|
||||||
{{ t('downloadMail') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="replyMail">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="ReplyFilled" />
|
|
||||||
</template>
|
|
||||||
{{ t('reply') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
|
||||||
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
<pre v-if="showTextMail" style="margin-top: 10px;">{{ curMail.text }}</pre>
|
|
||||||
<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>
|
|
||||||
</n-card>
|
</n-card>
|
||||||
</n-drawer-content>
|
</n-drawer-content>
|
||||||
</n-drawer>
|
</n-drawer>
|
||||||
</div>
|
</div>
|
||||||
<n-modal v-model:show="showAttachments" preset="dialog" title="Dialog">
|
|
||||||
<template #header>
|
|
||||||
<div>{{ t("attachments") }}</div>
|
|
||||||
</template>
|
|
||||||
<n-spin v-model:show="attachmentLoding">
|
|
||||||
<n-list hoverable clickable>
|
|
||||||
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
|
|
||||||
<n-thing class="center" :title="row.filename">
|
|
||||||
<template #description>
|
|
||||||
<n-space>
|
|
||||||
<n-tag type="info">
|
|
||||||
Size: {{ row.size }}
|
|
||||||
</n-tag>
|
|
||||||
<n-button v-if="showSaveS3" @click="saveToS3Proxy(row.filename, row.blob)" ghost type="info"
|
|
||||||
size="small">
|
|
||||||
{{ t('saveToS3') }}
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
</n-thing>
|
|
||||||
<template #suffix>
|
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
|
|
||||||
:href="row.url">
|
|
||||||
<n-icon :component="CloudDownloadRound" />
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-list-item>
|
|
||||||
</n-list>
|
|
||||||
</n-spin>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showMultiActionDownload" preset="dialog" :title="t('downloadMail')">
|
<n-modal v-model:show="showMultiActionDownload" preset="dialog" :title="t('downloadMail')">
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
{{ multiActionDownloadZip.filename }}
|
{{ multiActionDownloadZip.filename }}
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { CloudDownloadRound, ReplyFilled, ForwardFilled, FullscreenRound } from '@vicons/material'
|
||||||
|
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
|
||||||
|
import { getDownloadEmlUrl } from '../utils/email-parser';
|
||||||
|
import { utcToLocalDate } from '../utils';
|
||||||
|
import { useGlobalState } from '../store';
|
||||||
|
|
||||||
|
const { preferShowTextMail, useIframeShowMail, useUTCDate } = useGlobalState();
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
delete: 'Delete',
|
||||||
|
deleteMailTip: 'Are you sure you want to delete mail?',
|
||||||
|
attachments: 'View Attachments',
|
||||||
|
downloadMail: 'Download Mail',
|
||||||
|
reply: 'Reply',
|
||||||
|
forward: 'Forward',
|
||||||
|
showTextMail: 'Show Text Mail',
|
||||||
|
showHtmlMail: 'Show HTML Mail',
|
||||||
|
saveToS3: 'Save to S3',
|
||||||
|
size: 'Size',
|
||||||
|
fullscreen: 'Fullscreen',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
delete: '删除',
|
||||||
|
deleteMailTip: '确定要删除邮件吗?',
|
||||||
|
attachments: '查看附件',
|
||||||
|
downloadMail: '下载邮件',
|
||||||
|
reply: '回复',
|
||||||
|
forward: '转发',
|
||||||
|
showTextMail: '显示纯文本邮件',
|
||||||
|
showHtmlMail: '显示HTML邮件',
|
||||||
|
saveToS3: '保存到S3',
|
||||||
|
size: '大小',
|
||||||
|
fullscreen: '全屏',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
mail: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
showEMailTo: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
enableUserDeleteEmail: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
showReply: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
showSaveS3: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
// 回调函数 props
|
||||||
|
onDelete: {
|
||||||
|
type: Function,
|
||||||
|
default: () => { }
|
||||||
|
},
|
||||||
|
onReply: {
|
||||||
|
type: Function,
|
||||||
|
default: () => { }
|
||||||
|
},
|
||||||
|
onForward: {
|
||||||
|
type: Function,
|
||||||
|
default: () => { }
|
||||||
|
},
|
||||||
|
onSaveToS3: {
|
||||||
|
type: Function,
|
||||||
|
default: () => { }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const showTextMail = ref(preferShowTextMail.value);
|
||||||
|
const showAttachments = ref(false);
|
||||||
|
const curAttachments = ref([]);
|
||||||
|
const attachmentLoding = ref(false);
|
||||||
|
const showFullscreen = ref(false);
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
props.onDelete();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewAttachments = () => {
|
||||||
|
curAttachments.value = props.mail.attachments;
|
||||||
|
showAttachments.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReply = () => {
|
||||||
|
props.onReply();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleForward = () => {
|
||||||
|
props.onForward();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handleSaveToS3 = async (filename, blob) => {
|
||||||
|
attachmentLoding.value = true;
|
||||||
|
try {
|
||||||
|
await props.onSaveToS3(filename, blob);
|
||||||
|
} finally {
|
||||||
|
attachmentLoding.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="mail-content-renderer">
|
||||||
|
<!-- 邮件信息标签 -->
|
||||||
|
<n-space>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ mail.id }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ utcToLocalDate(mail.created_at, useUTCDate.value) }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
FROM: {{ mail.source }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag v-if="showEMailTo" type="info">
|
||||||
|
TO: {{ mail.address }}
|
||||||
|
</n-tag>
|
||||||
|
|
||||||
|
<!-- 操作按钮 -->
|
||||||
|
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="handleDelete">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t('deleteMailTip') }}
|
||||||
|
</n-popconfirm>
|
||||||
|
|
||||||
|
<n-button v-if="mail.attachments && mail.attachments.length > 0" size="small" tertiary type="info"
|
||||||
|
@click="handleViewAttachments">
|
||||||
|
{{ t('attachments') }}
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="mail.id + '.eml'"
|
||||||
|
:href="getDownloadEmlUrl(mail.raw)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="CloudDownloadRound" />
|
||||||
|
</template>
|
||||||
|
{{ t('downloadMail') }}
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button v-if="showReply" size="small" tertiary type="info" @click="handleReply">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="ReplyFilled" />
|
||||||
|
</template>
|
||||||
|
{{ t('reply') }}
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button v-if="showReply" size="small" tertiary type="info" @click="handleForward">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="ForwardFilled" />
|
||||||
|
</template>
|
||||||
|
{{ t('forward') }}
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
||||||
|
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
||||||
|
</n-button>
|
||||||
|
|
||||||
|
<n-button size="small" tertiary type="info" @click="showFullscreen = true">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="FullscreenRound" />
|
||||||
|
</template>
|
||||||
|
{{ t('fullscreen') }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
|
||||||
|
<!-- 邮件内容 -->
|
||||||
|
<div class="mail-content">
|
||||||
|
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
|
||||||
|
<iframe v-else-if="useIframeShowMail" :srcdoc="mail.message" class="mail-iframe">
|
||||||
|
</iframe>
|
||||||
|
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mail.message" class="mail-html" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-drawer v-model:show="showFullscreen" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
|
||||||
|
style="height: 100vh;">
|
||||||
|
<n-drawer-content :title="mail.subject" closable>
|
||||||
|
<div class="fullscreen-mail-content">
|
||||||
|
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
|
||||||
|
<iframe v-else-if="useIframeShowMail" :srcdoc="mail.message" class="mail-iframe">
|
||||||
|
</iframe>
|
||||||
|
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mail.message" class="mail-html" />
|
||||||
|
</div>
|
||||||
|
</n-drawer-content>
|
||||||
|
</n-drawer>
|
||||||
|
|
||||||
|
<!-- 附件模态框 -->
|
||||||
|
<n-modal v-model:show="showAttachments" preset="dialog" title="Dialog">
|
||||||
|
<template #header>
|
||||||
|
<div>{{ t('attachments') }}</div>
|
||||||
|
</template>
|
||||||
|
<n-spin v-model:show="attachmentLoding">
|
||||||
|
<n-list hoverable clickable>
|
||||||
|
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
|
||||||
|
<n-thing class="center" :title="row.filename">
|
||||||
|
<template #description>
|
||||||
|
<n-space>
|
||||||
|
<n-tag type="info">
|
||||||
|
Size: {{ row.size }}
|
||||||
|
</n-tag>
|
||||||
|
<n-button v-if="showSaveS3" @click="handleSaveToS3(row.filename, row.blob)" ghost type="info"
|
||||||
|
size="small">
|
||||||
|
{{ t('saveToS3') }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-thing>
|
||||||
|
<template #suffix>
|
||||||
|
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
|
||||||
|
:href="row.url">
|
||||||
|
<n-icon :component="CloudDownloadRound" />
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-list-item>
|
||||||
|
</n-list>
|
||||||
|
</n-spin>
|
||||||
|
</n-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mail-content-renderer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-content {
|
||||||
|
margin-top: 10px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-text {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-html {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-mail-content {
|
||||||
|
height: calc(100vh - 120px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullscreen-mail-content .mail-iframe {
|
||||||
|
min-height: calc(100vh - 120px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,6 +4,7 @@ import { useMessage } from 'naive-ui'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
import { useIsMobile } from '../utils/composables'
|
import { useIsMobile } from '../utils/composables'
|
||||||
|
import { utcToLocalDate } from '../utils';
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
@@ -30,7 +31,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const { isDark, mailboxSplitSize, loading } = useGlobalState()
|
const { isDark, mailboxSplitSize, loading, useUTCDate } = useGlobalState()
|
||||||
const data = ref([])
|
const data = ref([])
|
||||||
|
|
||||||
const count = ref(0)
|
const count = ref(0)
|
||||||
@@ -251,7 +252,7 @@ onMounted(async () => {
|
|||||||
ID: {{ row.id }}
|
ID: {{ row.id }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
{{ `${row.created_at} UTC` }}
|
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag v-if="showEMailFrom" type="info">
|
<n-tag v-if="showEMailFrom" type="info">
|
||||||
FROM: {{ row.address }}
|
FROM: {{ row.address }}
|
||||||
@@ -273,7 +274,7 @@ onMounted(async () => {
|
|||||||
ID: {{ curMail.id }}
|
ID: {{ curMail.id }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
{{ `${curMail.created_at} UTC` }}
|
{{ utcToLocalDate(curMail.created_at, useUTCDate) }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
FROM: {{ curMail.address }}
|
FROM: {{ curMail.address }}
|
||||||
@@ -320,7 +321,7 @@ onMounted(async () => {
|
|||||||
ID: {{ row.id }}
|
ID: {{ row.id }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
{{ `${row.created_at} UTC` }}
|
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag v-if="showEMailFrom" type="info">
|
<n-tag v-if="showEMailFrom" type="info">
|
||||||
FROM: {{ row.address }}
|
FROM: {{ row.address }}
|
||||||
@@ -342,7 +343,7 @@ onMounted(async () => {
|
|||||||
ID: {{ curMail.id }}
|
ID: {{ curMail.id }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
{{ `${curMail.created_at} UTC` }}
|
{{ utcToLocalDate(curMail.created_at, useUTCDate) }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
FROM: {{ curMail.address }}
|
FROM: {{ curMail.address }}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 { 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'
|
import { createHead } from '@unhead/vue'
|
||||||
|
|
||||||
registerSW({ immediate: true })
|
import App from './App.vue'
|
||||||
const i18n = createI18n({
|
import router from './router'
|
||||||
legacy: false, // you must set `false`, to use Composition API
|
import i18n from './i18n'
|
||||||
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'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const head = createHead()
|
const head = createHead()
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
import Index from '../views/Index.vue'
|
import Index from '../views/Index.vue'
|
||||||
import User from '../views/User.vue'
|
import User from '../views/User.vue'
|
||||||
import UserOauth2Callback from '../views/user/UserOauth2Callback.vue'
|
import UserOauth2Callback from '../views/user/UserOauth2Callback.vue'
|
||||||
|
import i18n from '../i18n'
|
||||||
|
import { useGlobalState } from '../store'
|
||||||
|
|
||||||
|
const { jwt } = useGlobalState()
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
@@ -37,6 +41,20 @@ const router = createRouter({
|
|||||||
redirect: '/'
|
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
|
export default router
|
||||||
|
|||||||
@@ -10,15 +10,19 @@ export const useGlobalState = createGlobalState(
|
|||||||
const toggleDark = useToggle(isDark)
|
const toggleDark = useToggle(isDark)
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const announcement = useLocalStorage('announcement', '');
|
const announcement = useLocalStorage('announcement', '');
|
||||||
|
const useSimpleIndex = useLocalStorage('useSimpleIndex', false);
|
||||||
const openSettings = ref({
|
const openSettings = ref({
|
||||||
fetched: false,
|
fetched: false,
|
||||||
title: '',
|
title: '',
|
||||||
announcement: '',
|
announcement: '',
|
||||||
|
alwaysShowAnnouncement: false,
|
||||||
prefix: '',
|
prefix: '',
|
||||||
addressRegex: '',
|
addressRegex: '',
|
||||||
needAuth: false,
|
needAuth: false,
|
||||||
adminContact: '',
|
adminContact: '',
|
||||||
enableUserCreateEmail: false,
|
enableUserCreateEmail: false,
|
||||||
|
disableAnonymousUserCreateEmail: false,
|
||||||
|
disableCustomAddressName: false,
|
||||||
enableUserDeleteEmail: false,
|
enableUserDeleteEmail: false,
|
||||||
enableAutoReply: false,
|
enableAutoReply: false,
|
||||||
enableIndexAbout: false,
|
enableIndexAbout: false,
|
||||||
@@ -32,6 +36,7 @@ export const useGlobalState = createGlobalState(
|
|||||||
isS3Enabled: false,
|
isS3Enabled: false,
|
||||||
showGithub: true,
|
showGithub: true,
|
||||||
disableAdminPasswordCheck: false,
|
disableAdminPasswordCheck: false,
|
||||||
|
enableAddressPassword: false,
|
||||||
})
|
})
|
||||||
const settings = ref({
|
const settings = ref({
|
||||||
fetched: false,
|
fetched: false,
|
||||||
@@ -59,6 +64,7 @@ export const useGlobalState = createGlobalState(
|
|||||||
const auth = useStorage('auth', '');
|
const auth = useStorage('auth', '');
|
||||||
const adminAuth = useStorage('adminAuth', '');
|
const adminAuth = useStorage('adminAuth', '');
|
||||||
const jwt = useStorage('jwt', '');
|
const jwt = useStorage('jwt', '');
|
||||||
|
const addressPassword = useSessionStorage('addressPassword', '');
|
||||||
const adminTab = useSessionStorage('adminTab', "account");
|
const adminTab = useSessionStorage('adminTab', "account");
|
||||||
const adminMailTabAddress = ref("");
|
const adminMailTabAddress = ref("");
|
||||||
const adminSendBoxTabAddress = ref("");
|
const adminSendBoxTabAddress = ref("");
|
||||||
@@ -66,10 +72,13 @@ export const useGlobalState = createGlobalState(
|
|||||||
const useIframeShowMail = useStorage('useIframeShowMail', false);
|
const useIframeShowMail = useStorage('useIframeShowMail', false);
|
||||||
const preferShowTextMail = useStorage('preferShowTextMail', false);
|
const preferShowTextMail = useStorage('preferShowTextMail', false);
|
||||||
const userJwt = useStorage('userJwt', '');
|
const userJwt = useStorage('userJwt', '');
|
||||||
const userTab = useSessionStorage('userTab', 'user_settings');
|
const userTab = useSessionStorage('userTab', 'address_management');
|
||||||
const indexTab = useSessionStorage('indexTab', 'mailbox');
|
const indexTab = useSessionStorage('indexTab', 'mailbox');
|
||||||
const globalTabplacement = useStorage('globalTabplacement', 'top');
|
const globalTabplacement = useStorage('globalTabplacement', 'top');
|
||||||
const useSideMargin = useStorage('useSideMargin', true);
|
const useSideMargin = useStorage('useSideMargin', true);
|
||||||
|
const useUTCDate = useStorage('useUTCDate', false);
|
||||||
|
const autoRefresh = useStorage('autoRefresh', false);
|
||||||
|
const configAutoRefreshInterval = useStorage("configAutoRefreshInterval", 60);
|
||||||
const userOpenSettings = ref({
|
const userOpenSettings = ref({
|
||||||
fetched: false,
|
fetched: false,
|
||||||
enable: false,
|
enable: false,
|
||||||
@@ -88,6 +97,8 @@ export const useGlobalState = createGlobalState(
|
|||||||
is_admin: false,
|
is_admin: false,
|
||||||
/** @type {string | null} */
|
/** @type {string | null} */
|
||||||
access_token: null,
|
access_token: null,
|
||||||
|
/** @type {string | null} */
|
||||||
|
new_user_token: null,
|
||||||
/** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */
|
/** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */
|
||||||
user_role: null,
|
user_role: null,
|
||||||
});
|
});
|
||||||
@@ -127,11 +138,16 @@ export const useGlobalState = createGlobalState(
|
|||||||
userSettings,
|
userSettings,
|
||||||
globalTabplacement,
|
globalTabplacement,
|
||||||
useSideMargin,
|
useSideMargin,
|
||||||
|
useUTCDate,
|
||||||
|
autoRefresh,
|
||||||
|
configAutoRefreshInterval,
|
||||||
telegramApp,
|
telegramApp,
|
||||||
isTelegram,
|
isTelegram,
|
||||||
showAdminPage,
|
showAdminPage,
|
||||||
userOauth2SessionState,
|
userOauth2SessionState,
|
||||||
userOauth2SessionClientID,
|
userOauth2SessionClientID,
|
||||||
|
useSimpleIndex,
|
||||||
|
addressPassword,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,3 +11,20 @@ export const getRouterPathWithLang = (path: string, lang: string) => {
|
|||||||
}
|
}
|
||||||
return `/${lang}${path}`;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import Mails from './admin/Mails.vue';
|
|||||||
import MailsUnknow from './admin/MailsUnknow.vue';
|
import MailsUnknow from './admin/MailsUnknow.vue';
|
||||||
import About from './common/About.vue';
|
import About from './common/About.vue';
|
||||||
import Maintenance from './admin/Maintenance.vue';
|
import Maintenance from './admin/Maintenance.vue';
|
||||||
|
import DatabaseManager from './admin/DatabaseManager.vue';
|
||||||
import Appearance from './common/Appearance.vue';
|
import Appearance from './common/Appearance.vue';
|
||||||
import Telegram from './admin/Telegram.vue';
|
import Telegram from './admin/Telegram.vue';
|
||||||
import Webhook from './admin/Webhook.vue';
|
import Webhook from './admin/Webhook.vue';
|
||||||
@@ -30,6 +31,12 @@ const {
|
|||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
|
const SendMail = defineAsyncComponent(() => {
|
||||||
|
loading.value = true;
|
||||||
|
return import('./admin/SendMail.vue')
|
||||||
|
.finally(() => loading.value = false);
|
||||||
|
});
|
||||||
|
|
||||||
const authFunc = async () => {
|
const authFunc = async () => {
|
||||||
try {
|
try {
|
||||||
adminAuth.value = tmpAdminAuth.value;
|
adminAuth.value = tmpAdminAuth.value;
|
||||||
@@ -45,6 +52,7 @@ const { t } = useI18n({
|
|||||||
accessHeader: 'Admin Password',
|
accessHeader: 'Admin Password',
|
||||||
accessTip: 'Please enter the admin password',
|
accessTip: 'Please enter the admin password',
|
||||||
mails: 'Emails',
|
mails: 'Emails',
|
||||||
|
sendMail: 'Send Mail',
|
||||||
qucickSetup: 'Quick Setup',
|
qucickSetup: 'Quick Setup',
|
||||||
account: 'Account',
|
account: 'Account',
|
||||||
account_create: 'Create Account',
|
account_create: 'Create Account',
|
||||||
@@ -60,6 +68,7 @@ const { t } = useI18n({
|
|||||||
webhookSettings: 'Webhook Settings',
|
webhookSettings: 'Webhook Settings',
|
||||||
statistics: 'Statistics',
|
statistics: 'Statistics',
|
||||||
maintenance: 'Maintenance',
|
maintenance: 'Maintenance',
|
||||||
|
database: 'Database',
|
||||||
workerconfig: 'Worker Config',
|
workerconfig: 'Worker Config',
|
||||||
appearance: 'Appearance',
|
appearance: 'Appearance',
|
||||||
about: 'About',
|
about: 'About',
|
||||||
@@ -70,6 +79,7 @@ const { t } = useI18n({
|
|||||||
accessHeader: 'Admin 密码',
|
accessHeader: 'Admin 密码',
|
||||||
accessTip: '请输入 Admin 密码',
|
accessTip: '请输入 Admin 密码',
|
||||||
mails: '邮件',
|
mails: '邮件',
|
||||||
|
sendMail: '发送邮件',
|
||||||
qucickSetup: '快速设置',
|
qucickSetup: '快速设置',
|
||||||
account: '账号',
|
account: '账号',
|
||||||
account_create: '创建账号',
|
account_create: '创建账号',
|
||||||
@@ -85,6 +95,7 @@ const { t } = useI18n({
|
|||||||
webhookSettings: 'Webhook 设置',
|
webhookSettings: 'Webhook 设置',
|
||||||
statistics: '统计',
|
statistics: '统计',
|
||||||
maintenance: '维护',
|
maintenance: '维护',
|
||||||
|
database: '数据库',
|
||||||
workerconfig: 'Worker 配置',
|
workerconfig: 'Worker 配置',
|
||||||
appearance: '外观',
|
appearance: '外观',
|
||||||
about: '关于',
|
about: '关于',
|
||||||
@@ -104,7 +115,7 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="userSettings.fetched">
|
||||||
<n-modal v-model:show="showAdminPasswordModal" :closable="false" :closeOnEsc="false" :maskClosable="false"
|
<n-modal v-model:show="showAdminPasswordModal" :closable="false" :closeOnEsc="false" :maskClosable="false"
|
||||||
preset="dialog" :title="t('accessHeader')">
|
preset="dialog" :title="t('accessHeader')">
|
||||||
<p>{{ t('accessTip') }}</p>
|
<p>{{ t('accessTip') }}</p>
|
||||||
@@ -118,6 +129,9 @@ onMounted(async () => {
|
|||||||
<n-tabs v-if="showAdminPage" type="card" v-model:value="adminTab" :placement="globalTabplacement">
|
<n-tabs v-if="showAdminPage" type="card" v-model:value="adminTab" :placement="globalTabplacement">
|
||||||
<n-tab-pane name="qucickSetup" :tab="t('qucickSetup')">
|
<n-tab-pane name="qucickSetup" :tab="t('qucickSetup')">
|
||||||
<n-tabs type="bar" justify-content="center" animated>
|
<n-tabs type="bar" justify-content="center" animated>
|
||||||
|
<n-tab-pane name="database" :tab="t('database')">
|
||||||
|
<DatabaseManager />
|
||||||
|
</n-tab-pane>
|
||||||
<n-tab-pane name="account_settings" :tab="t('account_settings')">
|
<n-tab-pane name="account_settings" :tab="t('account_settings')">
|
||||||
<AccountSettings />
|
<AccountSettings />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
@@ -172,6 +186,9 @@ onMounted(async () => {
|
|||||||
<n-tab-pane name="sendBox" :tab="t('sendBox')">
|
<n-tab-pane name="sendBox" :tab="t('sendBox')">
|
||||||
<SendBox />
|
<SendBox />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="sendMail" :tab="t('sendMail')">
|
||||||
|
<SendMail />
|
||||||
|
</n-tab-pane>
|
||||||
<n-tab-pane name="mailWebhook" :tab="t('mailWebhook')">
|
<n-tab-pane name="mailWebhook" :tab="t('mailWebhook')">
|
||||||
<MailWebhook />
|
<MailWebhook />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
@@ -185,6 +202,9 @@ onMounted(async () => {
|
|||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
||||||
<n-tabs type="bar" justify-content="center" animated>
|
<n-tabs type="bar" justify-content="center" animated>
|
||||||
|
<n-tab-pane name="database" :tab="t('database')">
|
||||||
|
<DatabaseManager />
|
||||||
|
</n-tab-pane>
|
||||||
<n-tab-pane name="workerconfig" :tab="t('workerconfig')">
|
<n-tab-pane name="workerconfig" :tab="t('workerconfig')">
|
||||||
<WorkerConfig />
|
<WorkerConfig />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|||||||
@@ -21,9 +21,14 @@ const { t } = useI18n({
|
|||||||
<div>
|
<div>
|
||||||
<n-divider class="footer-divider" />
|
<n-divider class="footer-divider" />
|
||||||
<div style="text-align: center; padding: 20px">
|
<div style="text-align: center; padding: 20px">
|
||||||
<n-text depth="3">
|
<n-space justify="center">
|
||||||
{{ t('copyright') }} © 2023-{{ new Date().getFullYear() }} {{ openSettings.copyright }}
|
<n-text depth="3">
|
||||||
</n-text>
|
{{ t('copyright') }} © 2023-{{ new Date().getFullYear() }}
|
||||||
|
</n-text>
|
||||||
|
<n-text depth="3">
|
||||||
|
<div v-html="openSettings.copyright"></div>
|
||||||
|
</n-text>
|
||||||
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { api } from '../api'
|
|||||||
import { getRouterPathWithLang } from '../utils'
|
import { getRouterPathWithLang } from '../utils'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const notification = useNotification()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
toggleDark, isDark, isTelegram, showAdminPage,
|
toggleDark, isDark, isTelegram, showAdminPage,
|
||||||
@@ -125,7 +126,9 @@ const menuOptions = computed(() => [
|
|||||||
type: menuValue.value == "admin" ? "primary" : "default",
|
type: menuValue.value == "admin" ? "primary" : "default",
|
||||||
style: "width: 100%",
|
style: "width: 100%",
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
|
loading.value = true;
|
||||||
await router.push(getRouterPathWithLang('/admin', locale.value));
|
await router.push(getRouterPathWithLang('/admin', locale.value));
|
||||||
|
loading.value = false;
|
||||||
showMobileMenu.value = false;
|
showMobileMenu.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -213,7 +216,9 @@ const logoClick = async () => {
|
|||||||
if (logoClickCount.value >= 5) {
|
if (logoClickCount.value >= 5) {
|
||||||
logoClickCount.value = 0;
|
logoClickCount.value = 0;
|
||||||
message.info("Change to admin Page");
|
message.info("Change to admin Page");
|
||||||
|
loading.value = true;
|
||||||
await router.push(getRouterPathWithLang('/admin', locale.value));
|
await router.push(getRouterPathWithLang('/admin', locale.value));
|
||||||
|
loading.value = false;
|
||||||
} else {
|
} else {
|
||||||
logoClickCount.value++;
|
logoClickCount.value++;
|
||||||
}
|
}
|
||||||
@@ -223,7 +228,7 @@ const logoClick = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await api.getOpenSettings(message);
|
await api.getOpenSettings(message, notification);
|
||||||
// make sure user_id is fetched
|
// make sure user_id is fetched
|
||||||
if (!userSettings.value.user_id) await api.getUserSettings(message);
|
if (!userSettings.value.user_id) await api.getUserSettings(message);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,34 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { defineAsyncComponent } from 'vue'
|
import { defineAsyncComponent, onMounted, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
|
import { useIsMobile } from '../utils/composables'
|
||||||
|
import { FullscreenExitOutlined } from '@vicons/material'
|
||||||
|
|
||||||
import AddressBar from './index/AddressBar.vue';
|
import AddressBar from './index/AddressBar.vue';
|
||||||
import MailBox from '../components/MailBox.vue';
|
import MailBox from '../components/MailBox.vue';
|
||||||
import SendBox from '../components/SendBox.vue';
|
import SendBox from '../components/SendBox.vue';
|
||||||
import AutoReply from './index/AutoReply.vue';
|
import AutoReply from './index/AutoReply.vue';
|
||||||
import AccountSettings from './index/AccountSettings.vue';
|
import AccountSettings from './index/AccountSettings.vue';
|
||||||
|
import Appearance from './common/Appearance.vue';
|
||||||
import Webhook from './index/Webhook.vue';
|
import Webhook from './index/Webhook.vue';
|
||||||
import Attachment from './index/Attachment.vue';
|
import Attachment from './index/Attachment.vue';
|
||||||
import About from './common/About.vue';
|
import About from './common/About.vue';
|
||||||
|
import SimpleIndex from './index/SimpleIndex.vue';
|
||||||
|
|
||||||
const SendMail = defineAsyncComponent(() => import('./index/SendMail.vue'));
|
const { loading, settings, openSettings, indexTab, globalTabplacement, useSimpleIndex } = useGlobalState()
|
||||||
const { settings, openSettings, indexTab, globalTabplacement } = useGlobalState()
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const route = useRoute()
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
|
const SendMail = defineAsyncComponent(() => {
|
||||||
|
loading.value = true;
|
||||||
|
return import('./index/SendMail.vue')
|
||||||
|
.finally(() => loading.value = false);
|
||||||
|
});
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
@@ -26,26 +38,37 @@ const { t } = useI18n({
|
|||||||
sendmail: 'Send Mail',
|
sendmail: 'Send Mail',
|
||||||
auto_reply: 'Auto Reply',
|
auto_reply: 'Auto Reply',
|
||||||
accountSettings: 'Account Settings',
|
accountSettings: 'Account Settings',
|
||||||
|
appearance: 'Appearance',
|
||||||
about: 'About',
|
about: 'About',
|
||||||
s3Attachment: 'S3 Attachment',
|
s3Attachment: 'S3 Attachment',
|
||||||
saveToS3Success: 'save to s3 success',
|
saveToS3Success: 'save to s3 success',
|
||||||
webhookSettings: 'Webhook Settings',
|
webhookSettings: 'Webhook Settings',
|
||||||
|
query: 'Query',
|
||||||
|
enterSimpleMode: 'Simple Mode',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
mailbox: '收件箱',
|
mailbox: '收件箱',
|
||||||
sendbox: '发件箱',
|
sendbox: '发件箱',
|
||||||
sendmail: '发送邮件',
|
sendmail: '发送邮件',
|
||||||
auto_reply: '自动回复',
|
auto_reply: '自动回复',
|
||||||
accountSettings: '账户设置',
|
accountSettings: '账户',
|
||||||
|
appearance: '外观',
|
||||||
about: '关于',
|
about: '关于',
|
||||||
s3Attachment: 'S3附件',
|
s3Attachment: 'S3附件',
|
||||||
saveToS3Success: '保存到s3成功',
|
saveToS3Success: '保存到s3成功',
|
||||||
webhookSettings: 'Webhook 设置',
|
webhookSettings: 'Webhook 设置',
|
||||||
|
query: '查询',
|
||||||
|
enterSimpleMode: '极简模式',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchMailData = async (limit, offset) => {
|
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}`);
|
return await api.fetch(`/api/mails?limit=${limit}&offset=${offset}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -80,39 +103,89 @@ const saveToS3 = async (mail_id, filename, blob) => {
|
|||||||
message.error(error.message || "save to s3 error");
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<AddressBar />
|
<div v-if="useSimpleIndex">
|
||||||
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
|
<SimpleIndex />
|
||||||
<n-tab-pane name="mailbox" :tab="t('mailbox')">
|
</div>
|
||||||
<MailBox :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled" :saveToS3="saveToS3"
|
<div v-else>
|
||||||
:enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :fetchMailData="fetchMailData"
|
<AddressBar />
|
||||||
:deleteMail="deleteMail" />
|
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
|
||||||
</n-tab-pane>
|
<template #prefix v-if="!isMobile">
|
||||||
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
<n-button @click="useSimpleIndex = true" tertiary size="small">
|
||||||
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
<template #icon>
|
||||||
:deleteMail="deleteSenboxMail" />
|
<n-icon>
|
||||||
</n-tab-pane>
|
<FullscreenExitOutlined />
|
||||||
<n-tab-pane name="sendmail" :tab="t('sendmail')">
|
</n-icon>
|
||||||
<SendMail />
|
</template>
|
||||||
</n-tab-pane>
|
{{ t('enterSimpleMode') }}
|
||||||
<n-tab-pane name="accountSettings" :tab="t('accountSettings')">
|
</n-button>
|
||||||
<AccountSettings />
|
</template>
|
||||||
</n-tab-pane>
|
<n-tab-pane name="mailbox" :tab="t('mailbox')">
|
||||||
<n-tab-pane v-if="openSettings.enableAutoReply" name="auto_reply" :tab="t('auto_reply')">
|
<div v-if="showMailIdQuery" style="margin-bottom: 10px;">
|
||||||
<AutoReply />
|
<n-input-group>
|
||||||
</n-tab-pane>
|
<n-input v-model:value="mailIdQuery" />
|
||||||
<n-tab-pane v-if="openSettings.enableWebhook" name="webhook" :tab="t('webhookSettings')">
|
<n-button @click="queryMail" type="primary" tertiary>
|
||||||
<Webhook />
|
{{ t('query') }}
|
||||||
</n-tab-pane>
|
</n-button>
|
||||||
<n-tab-pane v-if="openSettings.isS3Enabled" name="s3_attachment" :tab="t('s3Attachment')">
|
</n-input-group>
|
||||||
<Attachment />
|
</div>
|
||||||
</n-tab-pane>
|
<MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled"
|
||||||
<n-tab-pane v-if="openSettings.enableIndexAbout" name="about" :tab="t('about')">
|
:saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||||
<About />
|
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
||||||
|
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||||
|
:deleteMail="deleteSenboxMail" />
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="sendmail" :tab="t('sendmail')">
|
||||||
|
<SendMail />
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="accountSettings" :tab="t('accountSettings')">
|
||||||
|
<AccountSettings />
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="appearance" :tab="t('appearance')">
|
||||||
|
<Appearance :showUseSimpleIndex="true" />
|
||||||
|
</n-tab-pane>
|
||||||
|
<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('webhookSettings')">
|
||||||
|
<Webhook />
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane v-if="openSettings.isS3Enabled" name="s3_attachment" :tab="t('s3Attachment')">
|
||||||
|
<Attachment />
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane v-if="openSettings.enableIndexAbout" name="about" :tab="t('about')">
|
||||||
|
<About />
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import AddressMangement from './user/AddressManagement.vue';
|
|||||||
import UserSettingsPage from './user/UserSettings.vue';
|
import UserSettingsPage from './user/UserSettings.vue';
|
||||||
import UserBar from './user/UserBar.vue';
|
import UserBar from './user/UserBar.vue';
|
||||||
import BindAddress from './user/BindAddress.vue';
|
import BindAddress from './user/BindAddress.vue';
|
||||||
|
import UserMailBox from './user/UserMailBox.vue';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
userTab, globalTabplacement, userSettings
|
userTab, globalTabplacement, userSettings
|
||||||
@@ -16,11 +17,13 @@ const { t } = useI18n({
|
|||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
address_management: 'Address Management',
|
address_management: 'Address Management',
|
||||||
|
user_mail_box_tab: 'Mail Box',
|
||||||
user_settings: 'User Settings',
|
user_settings: 'User Settings',
|
||||||
bind_address: 'Bind Mail Address',
|
bind_address: 'Bind Mail Address',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
address_management: '地址管理',
|
address_management: '地址管理',
|
||||||
|
user_mail_box_tab: '收件箱',
|
||||||
user_settings: '用户设置',
|
user_settings: '用户设置',
|
||||||
bind_address: '绑定邮箱地址',
|
bind_address: '绑定邮箱地址',
|
||||||
}
|
}
|
||||||
@@ -36,6 +39,9 @@ const { t } = useI18n({
|
|||||||
<n-tab-pane name="address_management" :tab="t('address_management')">
|
<n-tab-pane name="address_management" :tab="t('address_management')">
|
||||||
<AddressMangement />
|
<AddressMangement />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="user_mail_box_tab" :tab="t('user_mail_box_tab')">
|
||||||
|
<UserMailBox />
|
||||||
|
</n-tab-pane>
|
||||||
<n-tab-pane name="user_settings" :tab="t('user_settings')">
|
<n-tab-pane name="user_settings" :tab="t('user_settings')">
|
||||||
<UserSettingsPage />
|
<UserSettingsPage />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, h, onMounted, watch } from 'vue';
|
import { ref, h, onMounted, watch, computed } from 'vue';
|
||||||
import { NBadge } from 'naive-ui'
|
import { NBadge, useMessage } from 'naive-ui'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
@@ -9,7 +9,7 @@ import { NButton, NMenu } from 'naive-ui';
|
|||||||
import { MenuFilled } from '@vicons/material'
|
import { MenuFilled } from '@vicons/material'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading, adminTab,
|
loading, adminTab, openSettings,
|
||||||
adminMailTabAddress, adminSendBoxTabAddress
|
adminMailTabAddress, adminSendBoxTabAddress
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
@@ -27,13 +27,31 @@ const { t } = useI18n({
|
|||||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
deleteTip: 'Are you sure to delete this email?',
|
deleteTip: 'Are you sure to delete this email?',
|
||||||
delteAccount: 'Delete Account',
|
deleteAccount: 'Delete Account',
|
||||||
viewMails: 'View Mails',
|
viewMails: 'View Mails',
|
||||||
viewSendBox: 'View SendBox',
|
viewSendBox: 'View SendBox',
|
||||||
itemCount: 'itemCount',
|
itemCount: 'itemCount',
|
||||||
query: 'Query',
|
query: 'Query',
|
||||||
addressQueryTip: 'Leave blank to query all addresses',
|
addressQueryTip: 'Leave blank to query all addresses',
|
||||||
actions: 'Actions'
|
clearInbox: 'Clear Inbox',
|
||||||
|
clearSentItems: 'Clear Sent Items',
|
||||||
|
clearInboxTip: 'Are you sure to clear inbox for this email?',
|
||||||
|
clearSentItemsTip: 'Are you sure to clear sent items for this email?',
|
||||||
|
actions: 'Actions',
|
||||||
|
success: 'Success',
|
||||||
|
resetPassword: 'Reset Password',
|
||||||
|
newPassword: 'New Password',
|
||||||
|
passwordResetSuccess: 'Password reset successfully',
|
||||||
|
selectAll: 'Select All of This Page',
|
||||||
|
unselectAll: 'Unselect All',
|
||||||
|
pleaseSelectAddress: 'Please select address',
|
||||||
|
selectedItems: 'Selected',
|
||||||
|
multiDelete: 'Multi Delete',
|
||||||
|
multiDeleteTip: 'Are you sure to delete selected addresses?',
|
||||||
|
multiClearInbox: 'Multi Clear Inbox',
|
||||||
|
multiClearInboxTip: 'Are you sure to clear inbox for selected addresses?',
|
||||||
|
multiClearSentItems: 'Multi Clear Sent Items',
|
||||||
|
multiClearSentItemsTip: 'Are you sure to clear sent items for selected addresses?',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
name: '名称',
|
name: '名称',
|
||||||
@@ -46,13 +64,31 @@ const { t } = useI18n({
|
|||||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
deleteTip: '确定要删除这个邮箱吗?',
|
deleteTip: '确定要删除这个邮箱吗?',
|
||||||
delteAccount: '删除邮箱',
|
deleteAccount: '删除邮箱',
|
||||||
viewMails: '查看邮件',
|
viewMails: '查看邮件',
|
||||||
viewSendBox: '查看发件箱',
|
viewSendBox: '查看发件箱',
|
||||||
itemCount: '总数',
|
itemCount: '总数',
|
||||||
query: '查询',
|
query: '查询',
|
||||||
addressQueryTip: '留空查询所有地址',
|
addressQueryTip: '留空查询所有地址',
|
||||||
|
clearInbox: '清空收件箱',
|
||||||
|
clearSentItems: '清空发件箱',
|
||||||
|
clearInboxTip: '确定要清空这个邮箱的收件箱吗?',
|
||||||
|
clearSentItemsTip: '确定要清空这个邮箱的发件箱吗?',
|
||||||
actions: '操作',
|
actions: '操作',
|
||||||
|
success: '成功',
|
||||||
|
resetPassword: '重置密码',
|
||||||
|
newPassword: '新密码',
|
||||||
|
passwordResetSuccess: '密码重置成功',
|
||||||
|
selectAll: '全选本页',
|
||||||
|
unselectAll: '取消全选',
|
||||||
|
pleaseSelectAddress: '请选择地址',
|
||||||
|
selectedItems: '已选择',
|
||||||
|
multiDelete: '批量删除',
|
||||||
|
multiDeleteTip: '确定要删除选中的邮箱吗?',
|
||||||
|
multiClearInbox: '批量清空收件箱',
|
||||||
|
multiClearInboxTip: '确定要清空选中邮箱的收件箱吗?',
|
||||||
|
multiClearSentItems: '批量清空发件箱',
|
||||||
|
multiClearSentItemsTip: '确定要清空选中邮箱的发件箱吗?',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -60,6 +96,20 @@ const { t } = useI18n({
|
|||||||
const showEmailCredential = ref(false)
|
const showEmailCredential = ref(false)
|
||||||
const curEmailCredential = ref("")
|
const curEmailCredential = ref("")
|
||||||
const curDeleteAddressId = ref(0);
|
const curDeleteAddressId = ref(0);
|
||||||
|
const curClearInboxAddressId = ref(0);
|
||||||
|
const curClearSentItemsAddressId = ref(0);
|
||||||
|
const showResetPassword = ref(false);
|
||||||
|
const curResetPasswordAddressId = ref(0);
|
||||||
|
const newPassword = ref('');
|
||||||
|
|
||||||
|
// Multi-action mode state
|
||||||
|
const checkedRowKeys = ref([]);
|
||||||
|
const showMultiActionModal = ref(false);
|
||||||
|
const multiActionProgress = ref({ percentage: 0, tip: '0/0' });
|
||||||
|
const multiActionTitle = ref('');
|
||||||
|
|
||||||
|
const selectedCount = computed(() => checkedRowKeys.value.length);
|
||||||
|
const showMultiActionBar = computed(() => checkedRowKeys.value.length > 0);
|
||||||
|
|
||||||
const addressQuery = ref("")
|
const addressQuery = ref("")
|
||||||
|
|
||||||
@@ -68,6 +118,8 @@ const count = ref(0)
|
|||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = ref(20)
|
const pageSize = ref(20)
|
||||||
const showDeleteAccount = ref(false)
|
const showDeleteAccount = ref(false)
|
||||||
|
const showClearInbox = ref(false)
|
||||||
|
const showClearSentItems = ref(false)
|
||||||
|
|
||||||
const showCredential = async (id) => {
|
const showCredential = async (id) => {
|
||||||
try {
|
try {
|
||||||
@@ -83,7 +135,7 @@ const showCredential = async (id) => {
|
|||||||
const deleteEmail = async () => {
|
const deleteEmail = async () => {
|
||||||
try {
|
try {
|
||||||
await api.adminDeleteAddress(curDeleteAddressId.value)
|
await api.adminDeleteAddress(curDeleteAddressId.value)
|
||||||
message.success("success");
|
message.success(t("success"));
|
||||||
await fetchData()
|
await fetchData()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
@@ -92,6 +144,142 @@ const deleteEmail = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearInbox = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/admin/clear_inbox/${curClearInboxAddressId.value}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
message.success(t("success"));
|
||||||
|
await fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
} finally {
|
||||||
|
showClearInbox.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearSentItems = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/admin/clear_sent_items/${curClearSentItemsAddressId.value}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
message.success(t("success"));
|
||||||
|
await fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
} finally {
|
||||||
|
showClearSentItems.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetPassword = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/admin/address/${curResetPasswordAddressId.value}/reset_password`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
password: newPassword.value
|
||||||
|
})
|
||||||
|
});
|
||||||
|
message.success(t("passwordResetSuccess"));
|
||||||
|
newPassword.value = '';
|
||||||
|
showResetPassword.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-action mode functions
|
||||||
|
const multiActionSelectAll = () => {
|
||||||
|
checkedRowKeys.value = data.value.map(item => item.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiActionUnselectAll = () => {
|
||||||
|
checkedRowKeys.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用批量操作函数
|
||||||
|
const executeBatchOperation = async ({
|
||||||
|
shouldSkip = () => false,
|
||||||
|
apiCall,
|
||||||
|
title,
|
||||||
|
operationName = 'operation'
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const selectedAddresses = data.value.filter((item) =>
|
||||||
|
checkedRowKeys.value.includes(item.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedAddresses.length === 0) {
|
||||||
|
message.error(t('pleaseSelectAddress'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const failedIds = [];
|
||||||
|
const totalCount = selectedAddresses.length;
|
||||||
|
|
||||||
|
multiActionProgress.value = {
|
||||||
|
percentage: 0,
|
||||||
|
tip: `0/${totalCount}`
|
||||||
|
};
|
||||||
|
multiActionTitle.value = title;
|
||||||
|
showMultiActionModal.value = true;
|
||||||
|
|
||||||
|
for (const [index, address] of selectedAddresses.entries()) {
|
||||||
|
try {
|
||||||
|
if (!shouldSkip(address)) {
|
||||||
|
await apiCall(address.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`${operationName} failed for address ${address.id}:`, error);
|
||||||
|
failedIds.push(address.id);
|
||||||
|
}
|
||||||
|
multiActionProgress.value = {
|
||||||
|
percentage: Math.floor((index + 1) / totalCount * 100),
|
||||||
|
tip: `${index + 1}/${totalCount}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchData();
|
||||||
|
checkedRowKeys.value = failedIds;
|
||||||
|
message.success(t("success"));
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiActionDeleteAccounts = async () => {
|
||||||
|
await executeBatchOperation({
|
||||||
|
apiCall: (id) => api.adminDeleteAddress(id),
|
||||||
|
title: t('multiDelete') + ' ' + t('success'),
|
||||||
|
operationName: 'Delete'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiActionClearInbox = async () => {
|
||||||
|
await executeBatchOperation({
|
||||||
|
shouldSkip: (address) => address.mail_count <= 0,
|
||||||
|
apiCall: (id) => api.fetch(`/admin/clear_inbox/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
}),
|
||||||
|
title: t('multiClearInbox') + ' ' + t('success'),
|
||||||
|
operationName: 'ClearInbox'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiActionClearSentItems = async () => {
|
||||||
|
await executeBatchOperation({
|
||||||
|
shouldSkip: (address) => address.send_count <= 0,
|
||||||
|
apiCall: (id) => api.fetch(`/admin/clear_sent_items/${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
}),
|
||||||
|
title: t('multiClearSentItems') + ' ' + t('success'),
|
||||||
|
operationName: 'ClearSentItems'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
addressQuery.value = addressQuery.value.trim()
|
addressQuery.value = addressQuery.value.trim()
|
||||||
@@ -106,12 +294,15 @@ const fetchData = async () => {
|
|||||||
count.value = addressCount;
|
count.value = addressCount;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.error(error);
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
{
|
||||||
|
type: 'selection'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "ID",
|
title: "ID",
|
||||||
key: "id"
|
key: "id"
|
||||||
@@ -212,7 +403,8 @@ const columns = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ default: () => t('viewMails') }
|
{ default: () => t('viewMails') }
|
||||||
)
|
),
|
||||||
|
show: row.mail_count > 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () => h(NButton,
|
label: () => h(NButton,
|
||||||
@@ -224,7 +416,47 @@ const columns = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ default: () => t('viewSendBox') }
|
{ default: () => t('viewSendBox') }
|
||||||
)
|
),
|
||||||
|
show: row.send_count > 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h(NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
onClick: () => {
|
||||||
|
curClearInboxAddressId.value = row.id;
|
||||||
|
showClearInbox.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => t('clearInbox') }
|
||||||
|
),
|
||||||
|
show: row.mail_count > 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h(NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
onClick: () => {
|
||||||
|
curClearSentItemsAddressId.value = row.id;
|
||||||
|
showClearSentItems.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => t('clearSentItems') }
|
||||||
|
),
|
||||||
|
show: row.send_count > 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h(NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
onClick: () => {
|
||||||
|
curResetPasswordAddressId.value = row.id;
|
||||||
|
showResetPassword.value = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => t('resetPassword') }
|
||||||
|
),
|
||||||
|
show: openSettings.value?.enableAddressPassword
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () => h(NButton,
|
label: () => h(NButton,
|
||||||
@@ -271,21 +503,78 @@ onMounted(async () => {
|
|||||||
<template #action>
|
<template #action>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal v-model:show="showDeleteAccount" preset="dialog" :title="t('delteAccount')">
|
<n-modal v-model:show="showDeleteAccount" preset="dialog" :title="t('deleteAccount')">
|
||||||
<p>{{ t('deleteTip') }}</p>
|
<p>{{ t('deleteTip') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="deleteEmail" size="small" tertiary type="error">
|
<n-button :loading="loading" @click="deleteEmail" size="small" tertiary type="error">
|
||||||
{{ t('delteAccount') }}
|
{{ t('deleteAccount') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-input-group>
|
<n-modal v-model:show="showClearInbox" preset="dialog" :title="t('clearInbox')">
|
||||||
|
<p>{{ t('clearInboxTip') }}</p>
|
||||||
|
<template #action>
|
||||||
|
<n-button :loading="loading" @click="clearInbox" size="small" tertiary type="error">
|
||||||
|
{{ t('clearInbox') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
<n-modal v-model:show="showClearSentItems" preset="dialog" :title="t('clearSentItems')">
|
||||||
|
<p>{{ t('clearSentItemsTip') }}</p>
|
||||||
|
<template #action>
|
||||||
|
<n-button :loading="loading" @click="clearSentItems" size="small" tertiary type="error">
|
||||||
|
{{ t('clearSentItems') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
|
||||||
|
<n-modal v-model:show="showResetPassword" preset="dialog" :title="t('resetPassword')">
|
||||||
|
<n-form-item :label="t('newPassword')">
|
||||||
|
<n-input v-model:value="newPassword" type="password" placeholder="" show-password-on="click" />
|
||||||
|
</n-form-item>
|
||||||
|
<template #action>
|
||||||
|
<n-button :loading="loading" @click="resetPassword" size="small" tertiary type="info">
|
||||||
|
{{ t('resetPassword') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
<n-input-group style="margin-bottom: 10px;">
|
||||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')"
|
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')"
|
||||||
@keydown.enter="fetchData" />
|
@keydown.enter="fetchData" />
|
||||||
<n-button @click="fetchData" type="primary" tertiary>
|
<n-button @click="fetchData" type="primary" tertiary>
|
||||||
{{ t('query') }}
|
{{ t('query') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
|
|
||||||
|
<n-space v-if="showMultiActionBar" style="margin-bottom: 10px;">
|
||||||
|
<n-button @click="multiActionSelectAll" tertiary>
|
||||||
|
{{ t('selectAll') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="multiActionUnselectAll" tertiary>
|
||||||
|
{{ t('unselectAll') }}
|
||||||
|
</n-button>
|
||||||
|
<n-popconfirm @positive-click="multiActionDeleteAccounts">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button tertiary type="error">{{ t('multiDelete') }}</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t('multiDeleteTip') }}
|
||||||
|
</n-popconfirm>
|
||||||
|
<n-popconfirm @positive-click="multiActionClearInbox">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button tertiary type="warning">{{ t('multiClearInbox') }}</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t('multiClearInboxTip') }}
|
||||||
|
</n-popconfirm>
|
||||||
|
<n-popconfirm @positive-click="multiActionClearSentItems">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button tertiary type="warning">{{ t('multiClearSentItems') }}</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t('multiClearSentItemsTip') }}
|
||||||
|
</n-popconfirm>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ t('selectedItems') }}: {{ selectedCount }}
|
||||||
|
</n-tag>
|
||||||
|
</n-space>
|
||||||
<div style="overflow: auto;">
|
<div style="overflow: auto;">
|
||||||
<div style="display: inline-block;">
|
<div style="display: inline-block;">
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||||
@@ -295,8 +584,21 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</n-pagination>
|
</n-pagination>
|
||||||
</div>
|
</div>
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
<n-data-table v-model:checked-row-keys="checkedRowKeys" :columns="columns" :data="data" :bordered="false"
|
||||||
|
:row-key="row => row.id" embedded />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi-action progress modal -->
|
||||||
|
<n-modal v-model:show="showMultiActionModal" preset="dialog" :title="multiActionTitle" negative-text="OK">
|
||||||
|
<n-space justify="center">
|
||||||
|
<n-progress type="circle" status="info" :percentage="multiActionProgress.percentage">
|
||||||
|
<span style="text-align: center">
|
||||||
|
{{ multiActionProgress.tip }}
|
||||||
|
</span>
|
||||||
|
</n-progress>
|
||||||
|
</n-space>
|
||||||
|
</n-modal>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,160 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref } from 'vue';
|
import { onMounted, ref, h } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { NButton, NPopconfirm, NInput, NSelect } from 'naive-ui'
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
|
|
||||||
const { loading } = useGlobalState()
|
const { loading, openSettings } = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
tip: 'You can manually input the following multiple select input',
|
tip: 'You can manually input the following multiple select input and enter',
|
||||||
|
manualInputPrompt: 'Type and press Enter to add',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
successTip: 'Save Success',
|
successTip: 'Save Success',
|
||||||
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
|
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
|
||||||
address_block_list_placeholder: 'Please enter the keywords you want to block',
|
address_block_list_placeholder: 'Please enter the keywords you want to block',
|
||||||
send_address_block_list: 'Address Block Keywords for send email',
|
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)',
|
verified_address_list: 'Verified Address List(Can send email by cf internal api)',
|
||||||
fromBlockList: 'Block Keywords for receive email',
|
fromBlockList: 'Block Keywords for receive email',
|
||||||
|
block_receive_unknow_address_email: 'Block receive unknow address email',
|
||||||
|
email_forwarding_config: 'Email Forwarding Configuration',
|
||||||
|
domain_list: 'Domain List',
|
||||||
|
forward_address: 'Forward Address',
|
||||||
|
actions: 'Actions',
|
||||||
|
select_domain: 'Select Domain',
|
||||||
|
forward_placeholder: 'forward@example.com',
|
||||||
|
delete_rule: 'Delete',
|
||||||
|
delete_rule_confirm: 'Are you sure you want to delete this rule?',
|
||||||
|
delete_success: 'Delete Success',
|
||||||
|
forwarding_rule_warning: 'Each rule will run, if domains is empty, all emails will be forwarded, forward address needs to be a verified address',
|
||||||
|
add: 'Add',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
config: 'Config',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
tip: '您可以手动输入以下多选输入框',
|
tip: '您可以手动输入以下多选输入框, 回车增加',
|
||||||
|
manualInputPrompt: '输入后按回车键添加',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
successTip: '保存成功',
|
successTip: '保存成功',
|
||||||
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
|
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
|
||||||
address_block_list_placeholder: '请输入您想要屏蔽的关键词',
|
address_block_list_placeholder: '请输入您想要屏蔽的关键词',
|
||||||
send_address_block_list: '发送邮件地址屏蔽关键词',
|
send_address_block_list: '发送邮件地址屏蔽关键词',
|
||||||
|
noLimitSendAddressList: '无余额限制发送地址列表',
|
||||||
verified_address_list: '已验证地址列表(可通过 cf 内部 api 发送邮件)',
|
verified_address_list: '已验证地址列表(可通过 cf 内部 api 发送邮件)',
|
||||||
fromBlockList: '接收邮件地址屏蔽关键词',
|
fromBlockList: '接收邮件地址屏蔽关键词',
|
||||||
|
block_receive_unknow_address_email: '禁止接收未知地址邮件',
|
||||||
|
email_forwarding_config: '邮件转发配置',
|
||||||
|
domain_list: '域名列表',
|
||||||
|
forward_address: '转发地址',
|
||||||
|
actions: '操作',
|
||||||
|
select_domain: '选择域名',
|
||||||
|
forward_placeholder: 'forward@example.com',
|
||||||
|
delete_rule: '删除',
|
||||||
|
delete_rule_confirm: '确定要删除这条规则吗?',
|
||||||
|
delete_success: '删除成功',
|
||||||
|
forwarding_rule_warning: '每条规则都会运行,如果 domains 为空,则转发所有邮件,转发地址需要为已验证的地址',
|
||||||
|
add: '添加',
|
||||||
|
cancel: '取消',
|
||||||
|
config: '配置',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const addressBlockList = ref([])
|
const addressBlockList = ref([])
|
||||||
const sendAddressBlockList = ref([])
|
const sendAddressBlockList = ref([])
|
||||||
|
const noLimitSendAddressList = ref([])
|
||||||
const verifiedAddressList = ref([])
|
const verifiedAddressList = ref([])
|
||||||
const fromBlockList = ref([])
|
const fromBlockList = ref([])
|
||||||
|
const emailRuleSettings = ref({
|
||||||
|
blockReceiveUnknowAddressEmail: false,
|
||||||
|
emailForwardingList: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const showEmailForwardingModal = ref(false)
|
||||||
|
const emailForwardingList = ref([])
|
||||||
|
|
||||||
|
|
||||||
|
const emailForwardingColumns = [
|
||||||
|
{
|
||||||
|
title: t('domain_list'),
|
||||||
|
key: 'domains',
|
||||||
|
render: (row, index) => {
|
||||||
|
return h(NSelect, {
|
||||||
|
value: Array.isArray(row.domains) ? row.domains : [],
|
||||||
|
onUpdateValue: (val) => {
|
||||||
|
emailForwardingList.value[index].domains = val
|
||||||
|
},
|
||||||
|
options: openSettings.value?.domains || [],
|
||||||
|
multiple: true,
|
||||||
|
filterable: true,
|
||||||
|
tag: true,
|
||||||
|
placeholder: t('select_domain')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('forward_address'),
|
||||||
|
key: 'forward',
|
||||||
|
render: (row, index) => {
|
||||||
|
return h(NInput, {
|
||||||
|
value: row.forward,
|
||||||
|
onUpdateValue: (val) => {
|
||||||
|
emailForwardingList.value[index].forward = val
|
||||||
|
},
|
||||||
|
placeholder: 'forward@example.com'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('actions'),
|
||||||
|
key: 'actions',
|
||||||
|
render: (row, index) => {
|
||||||
|
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||||
|
h(NPopconfirm, {
|
||||||
|
onPositiveClick: () => {
|
||||||
|
emailForwardingList.value = emailForwardingList.value.filter((_, i) => i !== index)
|
||||||
|
message.success(t('delete_success'))
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
default: () => t('delete_rule_confirm'),
|
||||||
|
trigger: () => h(NButton, {
|
||||||
|
size: 'small',
|
||||||
|
type: 'error'
|
||||||
|
}, { default: () => t('delete_rule') })
|
||||||
|
})
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const openEmailForwardingModal = () => {
|
||||||
|
// 从 emailRuleSettings 转换出列表数据
|
||||||
|
emailForwardingList.value = emailRuleSettings.value.emailForwardingList ?
|
||||||
|
[...emailRuleSettings.value.emailForwardingList] : []
|
||||||
|
showEmailForwardingModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNewEmailForwardingItem = () => {
|
||||||
|
emailForwardingList.value = [
|
||||||
|
...emailForwardingList.value,
|
||||||
|
{
|
||||||
|
domains: [],
|
||||||
|
forward: ''
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveEmailForwardingConfig = () => {
|
||||||
|
emailRuleSettings.value.emailForwardingList = [...emailForwardingList.value]
|
||||||
|
showEmailForwardingModal.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -45,6 +163,11 @@ const fetchData = async () => {
|
|||||||
sendAddressBlockList.value = res.sendBlockList || []
|
sendAddressBlockList.value = res.sendBlockList || []
|
||||||
verifiedAddressList.value = res.verifiedAddressList || []
|
verifiedAddressList.value = res.verifiedAddressList || []
|
||||||
fromBlockList.value = res.fromBlockList || []
|
fromBlockList.value = res.fromBlockList || []
|
||||||
|
noLimitSendAddressList.value = res.noLimitSendAddressList || []
|
||||||
|
emailRuleSettings.value = res.emailRuleSettings || {
|
||||||
|
blockReceiveUnknowAddressEmail: false,
|
||||||
|
emailForwardingList: []
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
}
|
}
|
||||||
@@ -59,6 +182,8 @@ const save = async () => {
|
|||||||
sendBlockList: sendAddressBlockList.value || [],
|
sendBlockList: sendAddressBlockList.value || [],
|
||||||
verifiedAddressList: verifiedAddressList.value || [],
|
verifiedAddressList: verifiedAddressList.value || [],
|
||||||
fromBlockList: fromBlockList.value || [],
|
fromBlockList: fromBlockList.value || [],
|
||||||
|
noLimitSendAddressList: noLimitSendAddressList.value || [],
|
||||||
|
emailRuleSettings: emailRuleSettings.value,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
message.success(t('successTip'))
|
message.success(t('successTip'))
|
||||||
@@ -76,29 +201,88 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<n-alert :show-icon="false" style="margin-bottom: 10px;">
|
<n-alert :show-icon="false" :bordered="false" type="warning" style="margin-bottom: 10px;">
|
||||||
{{ t("tip") }}
|
<span>{{ t("tip") }}</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button @click="save" type="primary" :loading="loading">
|
||||||
|
{{ t('save') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
<n-form-item-row :label="t('address_block_list')">
|
<n-form-item-row :label="t('address_block_list')">
|
||||||
<n-select v-model:value="addressBlockList" filterable multiple tag
|
<n-select v-model:value="addressBlockList" filterable multiple tag
|
||||||
:placeholder="t('address_block_list_placeholder')" />
|
:placeholder="t('address_block_list_placeholder')">
|
||||||
|
<template #empty>
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('manualInputPrompt') }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
</n-select>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('send_address_block_list')">
|
<n-form-item-row :label="t('send_address_block_list')">
|
||||||
<n-select v-model:value="sendAddressBlockList" filterable multiple tag
|
<n-select v-model:value="sendAddressBlockList" filterable multiple tag
|
||||||
:placeholder="t('address_block_list_placeholder')" />
|
:placeholder="t('address_block_list_placeholder')">
|
||||||
|
<template #empty>
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('manualInputPrompt') }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
</n-select>
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('noLimitSendAddressList')">
|
||||||
|
<n-select v-model:value="noLimitSendAddressList" filterable multiple tag
|
||||||
|
:placeholder="t('noLimitSendAddressList')">
|
||||||
|
<template #empty>
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('manualInputPrompt') }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
</n-select>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('verified_address_list')">
|
<n-form-item-row :label="t('verified_address_list')">
|
||||||
<n-select v-model:value="verifiedAddressList" filterable multiple tag
|
<n-select v-model:value="verifiedAddressList" filterable multiple tag
|
||||||
:placeholder="t('verified_address_list')" />
|
:placeholder="t('verified_address_list')">
|
||||||
|
<template #empty>
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('manualInputPrompt') }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
</n-select>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('fromBlockList')">
|
<n-form-item-row :label="t('fromBlockList')">
|
||||||
<n-select v-model:value="fromBlockList" filterable multiple tag :placeholder="t('fromBlockList')" />
|
<n-select v-model:value="fromBlockList" filterable multiple tag :placeholder="t('fromBlockList')">
|
||||||
|
<template #empty>
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('manualInputPrompt') }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
</n-select>
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('block_receive_unknow_address_email')">
|
||||||
|
<n-switch v-model:value="emailRuleSettings.blockReceiveUnknowAddressEmail" :round="false" />
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('email_forwarding_config')">
|
||||||
|
<n-button @click="openEmailForwardingModal">{{ t('config') }}</n-button>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-button @click="save" type="primary" block :loading="loading">
|
|
||||||
{{ t('save') }}
|
|
||||||
</n-button>
|
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 邮件转发配置弹窗 -->
|
||||||
|
<n-modal v-model:show="showEmailForwardingModal" preset="card" :title="t('email_forwarding_config')"
|
||||||
|
style="max-width: 800px;">
|
||||||
|
<n-space vertical>
|
||||||
|
<n-alert :show-icon="false" :bordered="false" type="warning">
|
||||||
|
<span>{{ t('forwarding_rule_warning') }}</span>
|
||||||
|
</n-alert>
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-button @click="addNewEmailForwardingItem">{{ t('add') }}</n-button>
|
||||||
|
</n-space>
|
||||||
|
<n-data-table :columns="emailForwardingColumns" :data="emailForwardingList" :bordered="false" striped />
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-button @click="saveEmailForwardingConfig" type="primary">{{ t('save') }}</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-space>
|
||||||
|
</n-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ const { t } = useI18n({
|
|||||||
en: {
|
en: {
|
||||||
address: 'Address',
|
address: 'Address',
|
||||||
enablePrefix: 'If enable Prefix',
|
enablePrefix: 'If enable Prefix',
|
||||||
creatNewEmail: 'Get New Email',
|
creatNewEmail: 'Create New Email',
|
||||||
fillInAllFields: 'Please fill in all fields',
|
fillInAllFields: 'Please fill in all fields',
|
||||||
successTip: 'Success Created',
|
successTip: 'Success Created',
|
||||||
addressCredential: 'Mail Address Credential',
|
addressCredential: 'Mail Address Credential',
|
||||||
|
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||||
|
addressPassword: 'Address Password',
|
||||||
|
linkWithAddressCredential: 'Open to auto login email link',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
address: '地址',
|
address: '地址',
|
||||||
@@ -27,6 +30,9 @@ const { t } = useI18n({
|
|||||||
fillInAllFields: '请填写完整信息',
|
fillInAllFields: '请填写完整信息',
|
||||||
successTip: '创建成功',
|
successTip: '创建成功',
|
||||||
addressCredential: '邮箱地址凭证',
|
addressCredential: '邮箱地址凭证',
|
||||||
|
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||||
|
addressPassword: '地址密码',
|
||||||
|
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -36,6 +42,8 @@ const emailName = ref("")
|
|||||||
const emailDomain = ref("")
|
const emailDomain = ref("")
|
||||||
const showReultModal = ref(false)
|
const showReultModal = ref(false)
|
||||||
const result = ref("")
|
const result = ref("")
|
||||||
|
const addressPassword = ref("")
|
||||||
|
const createdAddress = ref("")
|
||||||
|
|
||||||
const newEmail = async () => {
|
const newEmail = async () => {
|
||||||
if (!emailName.value || !emailDomain.value) {
|
if (!emailName.value || !emailDomain.value) {
|
||||||
@@ -52,6 +60,8 @@ const newEmail = async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
result.value = res["jwt"];
|
result.value = res["jwt"];
|
||||||
|
addressPassword.value = res["password"] || '';
|
||||||
|
createdAddress.value = res["address"] || '';
|
||||||
message.success(t('successTip'))
|
message.success(t('successTip'))
|
||||||
showReultModal.value = true
|
showReultModal.value = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -59,6 +69,10 @@ const newEmail = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUrlWithJwt = () => {
|
||||||
|
return `${window.location.origin}/?jwt=${result.value}`
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (openSettings.prefix) {
|
if (openSettings.prefix) {
|
||||||
enablePrefix.value = true
|
enablePrefix.value = true
|
||||||
@@ -70,14 +84,29 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('addressCredential')">
|
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('addressCredential')">
|
||||||
<p>{{ t('addressCredential') }}</p>
|
<span>
|
||||||
<n-card :bordered="false" embedded>
|
<p>{{ t("addressCredentialTip") }}</p>
|
||||||
|
</span>
|
||||||
|
<n-card embedded>
|
||||||
<b>{{ result }}</b>
|
<b>{{ result }}</b>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
<n-card embedded v-if="addressPassword">
|
||||||
|
<p><b>{{ createdAddress }}</b></p>
|
||||||
|
<p>{{ t('addressPassword') }}: <b>{{ addressPassword }}</b></p>
|
||||||
|
</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>
|
</n-modal>
|
||||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
|
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
|
||||||
<n-checkbox v-model:checked="enablePrefix" />
|
<n-switch v-model:value="enablePrefix" :round="false" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('address')">
|
<n-form-item-row :label="t('address')">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { CleaningServicesFilled } from '@vicons/material'
|
||||||
|
|
||||||
|
import { api } from '../../api'
|
||||||
|
import { init } from 'vooks/lib/on-fonts-ready';
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const dbVersionData = ref({
|
||||||
|
need_initialization: false,
|
||||||
|
need_migration: false,
|
||||||
|
current_db_version: '',
|
||||||
|
code_db_version: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
need_initialization_tip: 'Database initialization is required. Please initialize the database.',
|
||||||
|
need_migration_tip: 'Database migration is required. Please migrate the database.',
|
||||||
|
current_db_version: 'Current DB Version',
|
||||||
|
code_db_version: 'Code Needed DB Version',
|
||||||
|
init: 'Initialize Database',
|
||||||
|
migration: 'Migrate Database',
|
||||||
|
initializationSuccess: 'Database initialized successfully',
|
||||||
|
migrationSuccess: 'Database migrated successfully',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
need_initialization_tip: '需要初始化数据库,请初始化数据库',
|
||||||
|
need_migration_tip: '需要迁移数据库,请迁移数据库',
|
||||||
|
current_db_version: '当前数据库版本',
|
||||||
|
code_db_version: '需要的数据库版本',
|
||||||
|
init: '初始化数据库',
|
||||||
|
migration: '升级数据库 Schema',
|
||||||
|
initializationSuccess: '数据库初始化成功',
|
||||||
|
migrationSuccess: '数据库升级成功',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.fetch('/admin/db_version');
|
||||||
|
if (res) Object.assign(dbVersionData.value, res);
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialization = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch('/admin/db_initialize', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
await fetchData();
|
||||||
|
message.success(t('initializationSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const migration = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch('/admin/db_migration', {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
await fetchData();
|
||||||
|
message.success(t('migrationSuccess'));
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="center">
|
||||||
|
<n-card :bordered="false" embedded>
|
||||||
|
<n-alert v-if="dbVersionData.need_initialization" type="warning" :show-icon="false" :bordered="false">
|
||||||
|
<span>{{ t('need_initialization_tip') }}</span>
|
||||||
|
<n-button @click="initialization" type="primary" secondary block :loading="loading">
|
||||||
|
{{ t('init') }}
|
||||||
|
</n-button>
|
||||||
|
</n-alert>
|
||||||
|
<n-alert v-if="dbVersionData.need_migration" type="warning" :show-icon="false" :bordered="false">
|
||||||
|
<span>{{ t('need_migration_tip') }}</span>
|
||||||
|
<n-button @click="migration" type="primary" secondary block :loading="loading">
|
||||||
|
{{ t('migration') }}
|
||||||
|
</n-button>
|
||||||
|
</n-alert>
|
||||||
|
<n-alert type="info" :show-icon="false" :bordered="false">
|
||||||
|
<span>
|
||||||
|
{{ t('current_db_version') }}: {{ dbVersionData.current_db_version || "unknown" }},
|
||||||
|
{{ t('code_db_version') }}: {{ dbVersionData.code_db_version }}
|
||||||
|
</span>
|
||||||
|
</n-alert>
|
||||||
|
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.n-card {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-alert {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
place-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -11,7 +11,7 @@ const fetchMailUnknowData = async (limit, offset) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deleteMail = async (curMailId) => {
|
const deleteMail = async (curMailId) => {
|
||||||
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
await api.fetch(`/admin/mails/${curMailId}`, { method: 'DELETE' });
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ const cleanupModel = ref({
|
|||||||
cleanMailsDays: 30,
|
cleanMailsDays: 30,
|
||||||
enableUnknowMailsAutoCleanup: false,
|
enableUnknowMailsAutoCleanup: false,
|
||||||
cleanUnknowMailsDays: 30,
|
cleanUnknowMailsDays: 30,
|
||||||
enableAddressAutoCleanup: false,
|
|
||||||
cleanAddressDays: 30,
|
|
||||||
enableSendBoxAutoCleanup: false,
|
enableSendBoxAutoCleanup: false,
|
||||||
cleanSendBoxDays: 30,
|
cleanSendBoxDays: 30,
|
||||||
|
enableAddressAutoCleanup: false,
|
||||||
|
cleanAddressDays: 30,
|
||||||
|
enableInactiveAddressAutoCleanup: false,
|
||||||
|
cleanInactiveAddressDays: 30,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
@@ -24,22 +26,26 @@ const { t } = useI18n({
|
|||||||
mailBoxLabel: 'Cleanup the inbox before n days',
|
mailBoxLabel: 'Cleanup the inbox before n days',
|
||||||
mailUnknowLabel: "Cleanup the unknow mail before n days",
|
mailUnknowLabel: "Cleanup the unknow mail before n days",
|
||||||
sendBoxLabel: "Cleanup the sendbox before n days",
|
sendBoxLabel: "Cleanup the sendbox before n days",
|
||||||
|
addressCreateLabel: "Cleanup the address created before n days",
|
||||||
|
inactiveAddressLabel: "Cleanup the inactive address before n days",
|
||||||
cleanupNow: "Cleanup now",
|
cleanupNow: "Cleanup now",
|
||||||
autoCleanup: "Auto cleanup",
|
autoCleanup: "Auto cleanup",
|
||||||
cleanupSuccess: "Cleanup success",
|
cleanupSuccess: "Cleanup success",
|
||||||
save: "Save",
|
save: "Save",
|
||||||
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document",
|
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document, setting 0 days means clear all",
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
tip: '请输入天数',
|
tip: '请输入天数',
|
||||||
mailBoxLabel: '清理 n 天前的收件箱',
|
mailBoxLabel: '清理 n 天前的收件箱',
|
||||||
mailUnknowLabel: "清理 n 天前的无收件人邮件",
|
mailUnknowLabel: "清理 n 天前的无收件人邮件",
|
||||||
sendBoxLabel: "清理 n 天前的发件箱",
|
sendBoxLabel: "清理 n 天前的发件箱",
|
||||||
|
addressCreateLabel: "清理 n 天前创建的地址",
|
||||||
|
inactiveAddressLabel: "清理 n 天前的未活跃地址",
|
||||||
autoCleanup: "自动清理",
|
autoCleanup: "自动清理",
|
||||||
cleanupSuccess: "清理成功",
|
cleanupSuccess: "清理成功",
|
||||||
cleanupNow: "立即清理",
|
cleanupNow: "立即清理",
|
||||||
save: "保存",
|
save: "保存",
|
||||||
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档",
|
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档, 配置为 0 天表示全部清空",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -86,9 +92,14 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card :bordered="false" embedded>
|
<n-card :bordered="false" embedded>
|
||||||
<n-alert :show-icon="false" :bordered="false">
|
<n-alert :show-icon="false" :bordered="false" type="warning">
|
||||||
<span>{{ t('cronTip') }}</span>
|
<span>{{ t('cronTip') }}</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button @click="save" type="primary" :loading="loading">
|
||||||
|
{{ t('save') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
<n-form :model="cleanupModel">
|
<n-form :model="cleanupModel">
|
||||||
<n-form-item-row :label="t('mailBoxLabel')">
|
<n-form-item-row :label="t('mailBoxLabel')">
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
|
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
|
||||||
@@ -126,9 +137,30 @@ onMounted(async () => {
|
|||||||
{{ t('cleanupNow') }}
|
{{ t('cleanupNow') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-button @click="save" type="primary" block :loading="loading">
|
<n-form-item-row :label="t('addressCreateLabel')">
|
||||||
{{ t('save') }}
|
<n-checkbox v-model:checked="cleanupModel.enableAddressAutoCleanup">
|
||||||
</n-button>
|
{{ t('autoCleanup') }}
|
||||||
|
</n-checkbox>
|
||||||
|
<n-input-number v-model:value="cleanupModel.cleanAddressDays" :placeholder="t('tip')" />
|
||||||
|
<n-button @click="cleanup('addressCreated', cleanupModel.cleanAddressDays)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="CleaningServicesFilled" />
|
||||||
|
</template>
|
||||||
|
{{ t('cleanupNow') }}
|
||||||
|
</n-button>
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('inactiveAddressLabel')">
|
||||||
|
<n-checkbox v-model:checked="cleanupModel.enableInactiveAddressAutoCleanup">
|
||||||
|
{{ t('autoCleanup') }}
|
||||||
|
</n-checkbox>
|
||||||
|
<n-input-number v-model:value="cleanupModel.cleanInactiveAddressDays" :placeholder="t('tip')" />
|
||||||
|
<n-button @click="cleanup('inactiveAddress', cleanupModel.cleanInactiveAddressDays)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="CleaningServicesFilled" />
|
||||||
|
</template>
|
||||||
|
{{ t('cleanupNow') }}
|
||||||
|
</n-button>
|
||||||
|
</n-form-item-row>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -15,25 +15,29 @@ const { t } = useI18n({
|
|||||||
init: 'Init',
|
init: 'Init',
|
||||||
successTip: 'Success',
|
successTip: 'Success',
|
||||||
status: 'Check Status',
|
status: 'Check Status',
|
||||||
enableTelegramAllowList: 'Enable Telegram Allow List(Manually input user ID)',
|
enableTelegramAllowList: 'Enable Telegram Allow List(Manually input Chat ID)',
|
||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
telegramAllowList: 'Telegram Allow List',
|
telegramAllowList: 'Telegram Allow List(Manually input telegram Chat ID)',
|
||||||
|
manualInputPrompt: 'Type and press Enter to add',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
miniAppUrl: 'Telegram Mini App URL',
|
miniAppUrl: 'Telegram Mini App URL',
|
||||||
enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram user ID)',
|
enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram Chat ID)',
|
||||||
globalMailPushList: 'Global Mail Push List',
|
globalMailPushList: 'Global Mail Push Chat ID List',
|
||||||
|
globalMailPushListTip: 'Support chat_id of private chat/group/channel. You can send a message to your bot, then visit this link to see chat_id, https://api.telegram.org/bot<Replace with your BOT TOKEN>/getUpdates',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
init: '初始化',
|
init: '初始化',
|
||||||
successTip: '成功',
|
successTip: '成功',
|
||||||
status: '查看状态',
|
status: '查看状态',
|
||||||
enableTelegramAllowList: '启用 Telegram 白名单(手动输入用户 ID)',
|
enableTelegramAllowList: '启用 Telegram 白名单(手动输入 Chat ID, 回车增加)',
|
||||||
enable: '启用',
|
enable: '启用',
|
||||||
telegramAllowList: 'Telegram 白名单',
|
telegramAllowList: 'Telegram 白名单(手动输入 Chat ID, 回车增加)',
|
||||||
|
manualInputPrompt: '输入后按回车键添加',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)',
|
miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)',
|
||||||
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram 用户 ID)',
|
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram Chat ID, 回车增加)',
|
||||||
globalMailPushList: '全局邮件推送用户列表',
|
globalMailPushList: '全局邮件推送 Chat ID 列表',
|
||||||
|
globalMailPushListTip: '支持对话/群组/频道的 Chat ID, 您可以发送一条消息给您的机器人,然后访问此链接来查看 chat_id, https://api.telegram.org/bot<这里替换成您的 BOT TOKEN>/getUpdates',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -113,6 +117,17 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button @click="fetchStatus" secondary>
|
||||||
|
{{ t('status') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="init" type="primary">
|
||||||
|
{{ t('init') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="saveSettings" type="primary">
|
||||||
|
{{ t('save') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
<n-card :bordered="false" embedded>
|
<n-card :bordered="false" embedded>
|
||||||
<n-form-item-row :label="t('enableTelegramAllowList')">
|
<n-form-item-row :label="t('enableTelegramAllowList')">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
@@ -120,31 +135,41 @@ onMounted(async () => {
|
|||||||
{{ t('enable') }}
|
{{ t('enable') }}
|
||||||
</n-checkbox>
|
</n-checkbox>
|
||||||
<n-select v-model:value="settings.allowList" filterable multiple tag style="width: 80%;"
|
<n-select v-model:value="settings.allowList" filterable multiple tag style="width: 80%;"
|
||||||
:placeholder="t('telegramAllowList')" />
|
:placeholder="t('telegramAllowList')">
|
||||||
|
<template #empty>
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('manualInputPrompt') }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
</n-select>
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
|
<br />
|
||||||
<n-form-item-row :label="t('enableGlobalMailPush')">
|
<n-form-item-row :label="t('enableGlobalMailPush')">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-checkbox v-model:checked="settings.enableGlobalMailPush" style="width: 20%;">
|
<n-checkbox v-model:checked="settings.enableGlobalMailPush" style="width: 20%;">
|
||||||
{{ t('enable') }}
|
{{ t('enable') }}
|
||||||
</n-checkbox>
|
</n-checkbox>
|
||||||
<n-select v-model:value="settings.globalMailPushList" filterable multiple tag
|
<n-select v-model:value="settings.globalMailPushList" filterable multiple tag
|
||||||
style="width: 80%;" :placeholder="t('globalMailPushList')" />
|
style="width: 80%;" :placeholder="t('globalMailPushList')">
|
||||||
|
<template #empty>
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('manualInputPrompt') }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
</n-select>
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
|
<template #feedback>
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('globalMailPushListTip') }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
|
<br />
|
||||||
<n-form-item-row :label="t('miniAppUrl')">
|
<n-form-item-row :label="t('miniAppUrl')">
|
||||||
<n-input v-model:value="settings.miniAppUrl"></n-input>
|
<n-input v-model:value="settings.miniAppUrl"></n-input>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-button @click="saveSettings" type="primary" block>
|
|
||||||
{{ t('save') }}
|
|
||||||
</n-button>
|
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-button @click="init" type="primary" block>
|
|
||||||
{{ t('init') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="fetchStatus" secondary block>
|
|
||||||
{{ t('status') }}
|
|
||||||
</n-button>
|
|
||||||
<pre v-if="status.fetched">{{ JSON.stringify(status, null, 2) }}</pre>
|
<pre v-if="status.fetched">{{ JSON.stringify(status, null, 2) }}</pre>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,8 +182,4 @@ onMounted(async () => {
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.n-button {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -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 { api } from '../../api'
|
||||||
import { hashPassword } from '../../utils';
|
import { hashPassword } from '../../utils';
|
||||||
|
|
||||||
|
import UserAddressManagement from './UserAddressManagement.vue'
|
||||||
|
|
||||||
const { loading, openSettings } = useGlobalState()
|
const { loading, openSettings } = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
@@ -34,6 +36,7 @@ const { t } = useI18n({
|
|||||||
prefix: 'Prefix',
|
prefix: 'Prefix',
|
||||||
domains: 'Domains',
|
domains: 'Domains',
|
||||||
roleDonotExist: 'Current Role does not exist',
|
roleDonotExist: 'Current Role does not exist',
|
||||||
|
userAddressManagement: 'Address Management',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
success: '成功',
|
success: '成功',
|
||||||
@@ -56,6 +59,7 @@ const { t } = useI18n({
|
|||||||
prefix: '前缀',
|
prefix: '前缀',
|
||||||
domains: '域名',
|
domains: '域名',
|
||||||
roleDonotExist: '当前角色不存在',
|
roleDonotExist: '当前角色不存在',
|
||||||
|
userAddressManagement: '地址管理',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -75,6 +79,7 @@ const user = ref({
|
|||||||
password: ""
|
password: ""
|
||||||
})
|
})
|
||||||
const showChangeRole = ref(false)
|
const showChangeRole = ref(false)
|
||||||
|
const showUserAddressManagement = ref(false)
|
||||||
const userRoles = ref([])
|
const userRoles = ref([])
|
||||||
const curUserRole = ref('')
|
const curUserRole = ref('')
|
||||||
const userRolesOptions = computed(() => {
|
const userRolesOptions = computed(() => {
|
||||||
@@ -214,12 +219,25 @@ const columns = [
|
|||||||
title: t('address_count'),
|
title: t('address_count'),
|
||||||
key: "address_count",
|
key: "address_count",
|
||||||
render(row) {
|
render(row) {
|
||||||
return h(NBadge, {
|
return h(NButton,
|
||||||
value: row.address_count,
|
{
|
||||||
'show-zero': true,
|
text: true,
|
||||||
max: 99,
|
onClick: () => {
|
||||||
type: "success"
|
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),
|
icon: () => h(MenuFilled),
|
||||||
key: "action",
|
key: "action",
|
||||||
children: [
|
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,
|
label: () => h(NButton,
|
||||||
{
|
{
|
||||||
@@ -362,6 +393,9 @@ onMounted(async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
<n-modal v-model:show="showUserAddressManagement" preset="card" :title="t('userAddressManagement')">
|
||||||
|
<UserAddressManagement :user_id="curUserId" />
|
||||||
|
</n-modal>
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input v-model:value="userQuery" @keydown.enter="fetchData" />
|
<n-input v-model:value="userQuery" @keydown.enter="fetchData" />
|
||||||
<n-button @click="fetchData" type="primary" tertiary>
|
<n-button @click="fetchData" type="primary" tertiary>
|
||||||
|
|||||||
@@ -17,9 +17,11 @@ const { t } = useI18n({
|
|||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
|
delete: 'Delete',
|
||||||
successTip: 'Save Success',
|
successTip: 'Save Success',
|
||||||
enable: 'Enable',
|
enable: 'Enable',
|
||||||
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
||||||
|
manualInputPrompt: 'Type and press Enter to add',
|
||||||
mailAllowList: 'Mail Address Allow List',
|
mailAllowList: 'Mail Address Allow List',
|
||||||
addOauth2: 'Add Oauth2',
|
addOauth2: 'Add Oauth2',
|
||||||
name: 'Name',
|
name: 'Name',
|
||||||
@@ -28,9 +30,11 @@ const { t } = useI18n({
|
|||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
save: '保存',
|
save: '保存',
|
||||||
|
delete: '删除',
|
||||||
successTip: '保存成功',
|
successTip: '保存成功',
|
||||||
enable: '启用',
|
enable: '启用',
|
||||||
enableMailAllowList: '启用邮件地址白名单(可手动输入)',
|
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
|
||||||
|
manualInputPrompt: '输入后按回车键添加',
|
||||||
mailAllowList: '邮件地址白名单',
|
mailAllowList: '邮件地址白名单',
|
||||||
addOauth2: '添加 Oauth2',
|
addOauth2: '添加 Oauth2',
|
||||||
name: '名称',
|
name: '名称',
|
||||||
@@ -182,7 +186,7 @@ onMounted(async () => {
|
|||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
<n-alert :show-icon="false" type="warning" closable style="margin-bottom: 10px;">
|
<n-alert :show-icon="false" :bordered="false" type="warning" closable style="margin-bottom: 10px;">
|
||||||
{{ t("tip") }}
|
{{ t("tip") }}
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<n-flex justify="end">
|
<n-flex justify="end">
|
||||||
@@ -193,8 +197,19 @@ onMounted(async () => {
|
|||||||
{{ t('save') }}
|
{{ t('save') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
<n-collapse default-expanded-names="1" accordion>
|
<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">
|
<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 :model="item">
|
||||||
<n-form-item-row :label="t('name')" required>
|
<n-form-item-row :label="t('name')" required>
|
||||||
<n-input v-model:value="item.name" />
|
<n-input v-model:value="item.name" />
|
||||||
@@ -211,13 +226,13 @@ onMounted(async () => {
|
|||||||
<n-form-item-row label="Access Token URL" required>
|
<n-form-item-row label="Access Token URL" required>
|
||||||
<n-input v-model:value="item.accessTokenURL" />
|
<n-input v-model:value="item.accessTokenURL" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row label="Access Token accessTokenFormat" required>
|
<n-form-item-row label="Access Token Params Format" required>
|
||||||
<n-select v-model:value="item.accessTokenFormat" :options="accessTokenFormatOptions" />
|
<n-select v-model:value="item.accessTokenFormat" :options="accessTokenFormatOptions" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row label="User Info URL" required>
|
<n-form-item-row label="User Info URL" required>
|
||||||
<n-input v-model:value="item.userInfoURL" />
|
<n-input v-model:value="item.userInfoURL" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row label="User Email Key" required>
|
<n-form-item-row label="User Email Key (Support JSONPATH like $[0].email)" required>
|
||||||
<n-input v-model:value="item.userEmailKey" />
|
<n-input v-model:value="item.userEmailKey" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row label="Redirect URL" required>
|
<n-form-item-row label="Redirect URL" required>
|
||||||
@@ -233,7 +248,13 @@ onMounted(async () => {
|
|||||||
</n-checkbox>
|
</n-checkbox>
|
||||||
<n-select v-model:value="item.mailAllowList" v-if="item.enableMailAllowList" filterable
|
<n-select v-model:value="item.mailAllowList" v-if="item.enableMailAllowList" filterable
|
||||||
multiple tag style="width: 80%;" :options="mailAllowOptions"
|
multiple tag style="width: 80%;" :options="mailAllowOptions"
|
||||||
:placeholder="t('mailAllowList')" />
|
:placeholder="t('mailAllowList')">
|
||||||
|
<template #empty>
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('manualInputPrompt') }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
</n-select>
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
</n-form>
|
</n-form>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const { t } = useI18n({
|
|||||||
enableMailVerify: 'Enable Mail Verify (Send address must be an address in the system with a balance and can send mail normally)',
|
enableMailVerify: 'Enable Mail Verify (Send address must be an address in the system with a balance and can send mail normally)',
|
||||||
verifyMailSender: 'Verify Mail Sender',
|
verifyMailSender: 'Verify Mail Sender',
|
||||||
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
||||||
|
manualInputPrompt: 'Type and press Enter to add',
|
||||||
mailAllowList: 'Mail Address Allow List',
|
mailAllowList: 'Mail Address Allow List',
|
||||||
maxAddressCount: 'Maximum number of email addresses that can be binded',
|
maxAddressCount: 'Maximum number of email addresses that can be binded',
|
||||||
},
|
},
|
||||||
@@ -28,7 +29,8 @@ const { t } = useI18n({
|
|||||||
enableUserRegister: "允许用户注册",
|
enableUserRegister: "允许用户注册",
|
||||||
enableMailVerify: '启用邮件验证(发送地址必须是系统中能有余额且能正常发送邮件的地址)',
|
enableMailVerify: '启用邮件验证(发送地址必须是系统中能有余额且能正常发送邮件的地址)',
|
||||||
verifyMailSender: '验证邮件发送地址',
|
verifyMailSender: '验证邮件发送地址',
|
||||||
enableMailAllowList: '启用邮件地址白名单(可手动输入)',
|
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
|
||||||
|
manualInputPrompt: '输入后按回车键添加',
|
||||||
mailAllowList: '邮件地址白名单',
|
mailAllowList: '邮件地址白名单',
|
||||||
maxAddressCount: '可绑定最大邮箱地址数量',
|
maxAddressCount: '可绑定最大邮箱地址数量',
|
||||||
}
|
}
|
||||||
@@ -83,9 +85,14 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button @click="save" type="primary" :loading="loading">
|
||||||
|
{{ t('save') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
<n-form :model="userSettings">
|
<n-form :model="userSettings">
|
||||||
<n-form-item-row :label="t('enableUserRegister')">
|
<n-form-item-row :label="t('enableUserRegister')">
|
||||||
<n-checkbox v-model:checked="userSettings.enable" />
|
<n-switch v-model:value="userSettings.enable" :round="false" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('enableMailVerify')">
|
<n-form-item-row :label="t('enableMailVerify')">
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
@@ -103,7 +110,13 @@ onMounted(async () => {
|
|||||||
</n-checkbox>
|
</n-checkbox>
|
||||||
<n-select v-model:value="userSettings.mailAllowList" v-if="userSettings.enableMailAllowList"
|
<n-select v-model:value="userSettings.mailAllowList" v-if="userSettings.enableMailAllowList"
|
||||||
filterable multiple tag style="width: 80%;" :options="mailAllowOptions"
|
filterable multiple tag style="width: 80%;" :options="mailAllowOptions"
|
||||||
:placeholder="t('mailAllowList')" />
|
:placeholder="t('mailAllowList')">
|
||||||
|
<template #empty>
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('manualInputPrompt') }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
</n-select>
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('maxAddressCount')">
|
<n-form-item-row :label="t('maxAddressCount')">
|
||||||
@@ -112,9 +125,6 @@ onMounted(async () => {
|
|||||||
:placeholder="t('maxAddressCount')" />
|
:placeholder="t('maxAddressCount')" />
|
||||||
</n-input-group>
|
</n-input-group>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-button @click="save" type="primary" block :loading="loading">
|
|
||||||
{{ t('save') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,13 +13,17 @@ const { t } = useI18n({
|
|||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
successTip: 'Success',
|
successTip: 'Success',
|
||||||
webhookAllowList: 'Webhook Allow List(Enter the address that is allowed to use webhook)',
|
enableAllowList: 'Enable Allow List (Restrict webhook access to specific users)',
|
||||||
|
webhookAllowList: 'Webhook Allow List(Enter the mail address that is allowed to use webhook and enter)',
|
||||||
|
manualInputPrompt: 'Type and press Enter to add',
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
notEnabled: 'Webhook is not enabled',
|
notEnabled: 'Webhook is not enabled',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
successTip: '成功',
|
successTip: '成功',
|
||||||
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的地址)',
|
enableAllowList: '启用白名单 (限制 webhook 访问权限,只有白名单中的用户可以使用)',
|
||||||
|
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的邮箱地址, 回车增加)',
|
||||||
|
manualInputPrompt: '输入后按回车键添加',
|
||||||
save: '保存',
|
save: '保存',
|
||||||
notEnabled: 'Webhook 未开启',
|
notEnabled: 'Webhook 未开启',
|
||||||
}
|
}
|
||||||
@@ -27,14 +31,16 @@ const { t } = useI18n({
|
|||||||
});
|
});
|
||||||
|
|
||||||
class WebhookSettings {
|
class WebhookSettings {
|
||||||
|
enableAllowList: boolean;
|
||||||
allowList: string[];
|
allowList: string[];
|
||||||
|
|
||||||
constructor(allowList: string[]) {
|
constructor(enableAllowList: boolean, allowList: string[]) {
|
||||||
|
this.enableAllowList = enableAllowList;
|
||||||
this.allowList = allowList;
|
this.allowList = allowList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const webhookSettings = ref(new WebhookSettings([]))
|
const webhookSettings = ref(new WebhookSettings(false, []))
|
||||||
const webhookEnabled = ref(false)
|
const webhookEnabled = ref(false)
|
||||||
const errorInfo = ref('')
|
const errorInfo = ref('')
|
||||||
|
|
||||||
@@ -68,13 +74,24 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card v-if="webhookEnabled" :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
<n-card v-if="webhookEnabled" :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button @click="saveSettings" type="primary">
|
||||||
|
{{ t('save') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
<n-form-item-row :label="t('enableAllowList')">
|
||||||
|
<n-switch v-model:value="webhookSettings.enableAllowList" :round="false" />
|
||||||
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('webhookAllowList')">
|
<n-form-item-row :label="t('webhookAllowList')">
|
||||||
<n-select v-model:value="webhookSettings.allowList" filterable multiple tag
|
<n-select v-model:value="webhookSettings.allowList" filterable multiple tag
|
||||||
:placeholder="t('webhookAllowList')" />
|
:placeholder="t('webhookAllowList')">
|
||||||
|
<template #empty>
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('manualInputPrompt') }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
</n-select>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-button @click="saveSettings" type="primary" block>
|
|
||||||
{{ t('save') }}
|
|
||||||
</n-button>
|
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-result v-else status="404" :title="t('notEnabled')" :description="errorInfo" />
|
<n-result v-else status="404" :title="t('notEnabled')" :description="errorInfo" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||||
<pre>{{ JSON.stringify(settings, null, 2) }}</pre>
|
<pre>{{ JSON.stringify(settings, null, 2) }}</pre>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { GithubAlt, Discord, Telegram } from '@vicons/fa'
|
import { GithubAlt, Discord, Telegram } from '@vicons/fa'
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
const { announcement } = useGlobalState()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card :bordered="false" embedded>
|
<n-card :bordered="false" embedded>
|
||||||
|
<div v-html="announcement"></div>
|
||||||
<n-button tag="a" target="_blank" href="https://github.com/dreamhunter2333/cloudflare_temp_email">
|
<n-button tag="a" target="_blank" href="https://github.com/dreamhunter2333/cloudflare_temp_email">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon :component="GithubAlt" />
|
<n-icon :component="GithubAlt" />
|
||||||
|
|||||||
@@ -3,16 +3,23 @@ import { useI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
import { useIsMobile } from '../../utils/composables'
|
import { useIsMobile } from '../../utils/composables'
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
|
const props = defineProps({
|
||||||
|
showUseSimpleIndex: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mailboxSplitSize, useIframeShowMail, preferShowTextMail,
|
mailboxSplitSize, useIframeShowMail, preferShowTextMail, configAutoRefreshInterval,
|
||||||
globalTabplacement, useSideMargin
|
globalTabplacement, useSideMargin, useUTCDate, useSimpleIndex
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
|
useSimpleIndex: 'Use Simple Index',
|
||||||
mailboxSplitSize: 'Mailbox Split Size',
|
mailboxSplitSize: 'Mailbox Split Size',
|
||||||
useIframeShowMail: 'Use iframe Show HTML Mail',
|
useIframeShowMail: 'Use iframe Show HTML Mail',
|
||||||
preferShowTextMail: 'Display text Mail by default',
|
preferShowTextMail: 'Display text Mail by default',
|
||||||
@@ -22,8 +29,11 @@ const { t } = useI18n({
|
|||||||
top: 'top',
|
top: 'top',
|
||||||
right: 'right',
|
right: 'right',
|
||||||
bottom: 'bottom',
|
bottom: 'bottom',
|
||||||
|
useUTCDate: 'Use UTC Date',
|
||||||
|
autoRefreshInterval: 'Auto Refresh Interval(Sec)',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
|
useSimpleIndex: '使用极简主页',
|
||||||
mailboxSplitSize: '邮箱界面分栏大小',
|
mailboxSplitSize: '邮箱界面分栏大小',
|
||||||
preferShowTextMail: '默认以文本显示邮件',
|
preferShowTextMail: '默认以文本显示邮件',
|
||||||
useIframeShowMail: '使用iframe显示HTML邮件',
|
useIframeShowMail: '使用iframe显示HTML邮件',
|
||||||
@@ -33,6 +43,8 @@ const { t } = useI18n({
|
|||||||
top: '顶部',
|
top: '顶部',
|
||||||
right: '右侧',
|
right: '右侧',
|
||||||
bottom: '底部',
|
bottom: '底部',
|
||||||
|
useUTCDate: '使用 UTC 时间',
|
||||||
|
autoRefreshInterval: '自动刷新间隔(秒)',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -48,12 +60,23 @@ const { t } = useI18n({
|
|||||||
0.75: '0.75'
|
0.75: '0.75'
|
||||||
}" />
|
}" />
|
||||||
</n-form-item-row>
|
</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 v-if="props.showUseSimpleIndex" :label="t('useSimpleIndex')">
|
||||||
|
<n-switch v-model:value="useSimpleIndex" :round="false" />
|
||||||
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('preferShowTextMail')">
|
<n-form-item-row :label="t('preferShowTextMail')">
|
||||||
<n-switch v-model:value="preferShowTextMail" :round="false" />
|
<n-switch v-model:value="preferShowTextMail" :round="false" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('useIframeShowMail')">
|
<n-form-item-row :label="t('useIframeShowMail')">
|
||||||
<n-switch v-model:value="useIframeShowMail" :round="false" />
|
<n-switch v-model:value="useIframeShowMail" :round="false" />
|
||||||
</n-form-item-row>
|
</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-form-item-row v-if="!isMobile" :label="t('useSideMargin')">
|
||||||
<n-switch v-model:value="useSideMargin" :round="false" />
|
<n-switch v-model:value="useSideMargin" :round="false" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Turnstile from '../../components/Turnstile.vue'
|
|||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import { getRouterPathWithLang } from '../../utils'
|
import { getRouterPathWithLang, hashPassword } from '../../utils'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
bindUserAddress: {
|
bindUserAddress: {
|
||||||
@@ -34,11 +34,12 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
const notification = useNotification()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
jwt, loading, openSettings,
|
jwt, loading, openSettings,
|
||||||
showAddressCredential, userSettings
|
showAddressCredential, userSettings, addressPassword
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
|
|
||||||
const tabValue = ref('signin')
|
const tabValue = ref('signin')
|
||||||
@@ -46,8 +47,47 @@ const credential = ref('')
|
|||||||
const emailName = ref("")
|
const emailName = ref("")
|
||||||
const emailDomain = ref("")
|
const emailDomain = ref("")
|
||||||
const cfToken = ref("")
|
const cfToken = ref("")
|
||||||
|
const loginMethod = ref('credential') // 'credential' or 'password'
|
||||||
|
const loginAddress = ref('')
|
||||||
|
const loginPassword = ref('')
|
||||||
|
|
||||||
|
// 根据 openSettings 初始化登录方式
|
||||||
|
const initLoginMethod = () => {
|
||||||
|
if (openSettings.value?.enableAddressPassword) {
|
||||||
|
loginMethod.value = 'password';
|
||||||
|
} else {
|
||||||
|
loginMethod.value = 'credential';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
|
if (loginMethod.value === 'password') {
|
||||||
|
// Password login
|
||||||
|
if (!loginAddress.value || !loginPassword.value) {
|
||||||
|
message.error(t('emailPasswordRequired'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await api.fetch('/api/address_login', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: loginAddress.value,
|
||||||
|
password: await hashPassword(loginPassword.value)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
jwt.value = res.jwt;
|
||||||
|
await api.getSettings();
|
||||||
|
try {
|
||||||
|
await props.bindUserAddress();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(`${t('bindUserAddressError')}: ${error.message}`);
|
||||||
|
}
|
||||||
|
await router.push(getRouterPathWithLang("/", locale.value));
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!credential.value) {
|
if (!credential.value) {
|
||||||
message.error(t('credentialInput'));
|
message.error(t('credentialInput'));
|
||||||
return;
|
return;
|
||||||
@@ -70,6 +110,7 @@ const { locale, t } = useI18n({
|
|||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
login: 'Login',
|
login: 'Login',
|
||||||
|
loginAndBind: 'Login and Bind',
|
||||||
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
|
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
|
||||||
getNewEmail: 'Create New Email',
|
getNewEmail: 'Create New Email',
|
||||||
getNewEmailTip1: 'Please input the email you want to use. only allow: ',
|
getNewEmailTip1: 'Please input the email you want to use. only allow: ',
|
||||||
@@ -82,9 +123,16 @@ const { locale, t } = useI18n({
|
|||||||
credentialInput: 'Please input the Mail Address Credential',
|
credentialInput: 'Please input the Mail Address Credential',
|
||||||
bindUserInfo: 'Logged in user, login without binding email or create new email address will bind to current user',
|
bindUserInfo: 'Logged in user, login without binding email or create new email address will bind to current user',
|
||||||
bindUserAddressError: 'Error when bind email address to user',
|
bindUserAddressError: 'Error when bind email address to user',
|
||||||
|
autoGeneratedName: 'Auto-generated name',
|
||||||
|
passwordLogin: 'Password Login',
|
||||||
|
credentialLogin: 'Credential Login',
|
||||||
|
email: 'Email',
|
||||||
|
password: 'Password',
|
||||||
|
emailPasswordRequired: 'Email and password are required',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
login: '登录',
|
login: '登录',
|
||||||
|
loginAndBind: '登录并绑定',
|
||||||
pleaseGetNewEmail: '请"登录"或点击 "注册新邮箱" 按钮来获取一个新的邮箱地址',
|
pleaseGetNewEmail: '请"登录"或点击 "注册新邮箱" 按钮来获取一个新的邮箱地址',
|
||||||
getNewEmail: '创建新邮箱',
|
getNewEmail: '创建新邮箱',
|
||||||
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许: ',
|
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许: ',
|
||||||
@@ -97,10 +145,23 @@ const { locale, t } = useI18n({
|
|||||||
credentialInput: '请输入邮箱地址凭据',
|
credentialInput: '请输入邮箱地址凭据',
|
||||||
bindUserInfo: '已登录用户, 登录未绑定邮箱或创建新邮箱地址将绑定到当前用户',
|
bindUserInfo: '已登录用户, 登录未绑定邮箱或创建新邮箱地址将绑定到当前用户',
|
||||||
bindUserAddressError: '绑定邮箱地址到用户时错误',
|
bindUserAddressError: '绑定邮箱地址到用户时错误',
|
||||||
|
autoGeneratedName: '自动生成名称',
|
||||||
|
passwordLogin: '密码登录',
|
||||||
|
credentialLogin: '凭据登录',
|
||||||
|
email: '邮箱',
|
||||||
|
password: '密码',
|
||||||
|
emailPasswordRequired: '邮箱和密码不能为空',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const loginAndBindTag = computed(() => {
|
||||||
|
if (userSettings.value.user_email) {
|
||||||
|
return t('loginAndBind')
|
||||||
|
}
|
||||||
|
return t('login')
|
||||||
|
})
|
||||||
|
|
||||||
const addressRegex = computed(() => {
|
const addressRegex = computed(() => {
|
||||||
try {
|
try {
|
||||||
if (openSettings.value.addressRegex) {
|
if (openSettings.value.addressRegex) {
|
||||||
@@ -124,6 +185,10 @@ const generateName = async () => {
|
|||||||
.replace(/\.{2,}/g, '.')
|
.replace(/\.{2,}/g, '.')
|
||||||
.replace(addressRegex.value, '')
|
.replace(addressRegex.value, '')
|
||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
|
// support maxAddressLen
|
||||||
|
if (emailName.value.length > openSettings.value.maxAddressLen) {
|
||||||
|
emailName.value = emailName.value.slice(0, openSettings.value.maxAddressLen);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -133,12 +198,15 @@ const generateName = async () => {
|
|||||||
|
|
||||||
const newEmail = async () => {
|
const newEmail = async () => {
|
||||||
try {
|
try {
|
||||||
|
// If custom names are disabled, send empty name to trigger backend auto-generation
|
||||||
|
const nameToSend = openSettings.value.disableCustomAddressName ? "" : emailName.value;
|
||||||
const res = await props.newAddressPath(
|
const res = await props.newAddressPath(
|
||||||
emailName.value,
|
nameToSend,
|
||||||
emailDomain.value,
|
emailDomain.value,
|
||||||
cfToken.value
|
cfToken.value
|
||||||
);
|
);
|
||||||
jwt.value = res["jwt"];
|
jwt.value = res["jwt"];
|
||||||
|
addressPassword.value = res["password"] || '';
|
||||||
await api.getSettings();
|
await api.getSettings();
|
||||||
await router.push(getRouterPathWithLang("/", locale.value));
|
await router.push(getRouterPathWithLang("/", locale.value));
|
||||||
showAddressCredential.value = true;
|
showAddressCredential.value = true;
|
||||||
@@ -180,11 +248,21 @@ const domainsOptions = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const showNewAddressTab = computed(() => {
|
||||||
|
if (openSettings.value.disableAnonymousUserCreateEmail
|
||||||
|
&& !userSettings.value.user_email
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return openSettings.value.enableUserCreateEmail;
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (!openSettings.value.domains || openSettings.value.domains.length === 0) {
|
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 : "";
|
emailDomain.value = domainsOptions.value ? domainsOptions.value[0]?.value : "";
|
||||||
|
initLoginMethod();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -194,19 +272,38 @@ onMounted(async () => {
|
|||||||
<span>{{ t('bindUserInfo') }}</span>
|
<span>{{ t('bindUserInfo') }}</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
<n-tabs v-if="openSettings.fetched" v-model:value="tabValue" size="large" justify-content="space-evenly">
|
<n-tabs v-if="openSettings.fetched" v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||||
<n-tab-pane name="signin" :tab="t('login')">
|
<n-tab-pane name="signin" :tab="loginAndBindTag">
|
||||||
<n-form>
|
<n-form>
|
||||||
<n-form-item-row :label="t('credential')" required>
|
<div v-if="loginMethod === 'password'">
|
||||||
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
<n-form-item-row :label="t('email')" required>
|
||||||
</n-form-item-row>
|
<n-input v-model:value="loginAddress" />
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('password')" required>
|
||||||
|
<n-input v-model:value="loginPassword" type="password" show-password-on="click" />
|
||||||
|
</n-form-item-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<n-form-item-row :label="t('credential')" required>
|
||||||
|
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
||||||
|
</n-form-item-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="switch-login-button">
|
||||||
|
<n-button v-if="openSettings?.enableAddressPassword"
|
||||||
|
@click="loginMethod === 'password' ? loginMethod = 'credential' : loginMethod = 'password'"
|
||||||
|
type="info" quaternary size="tiny">
|
||||||
|
{{ loginMethod === 'password' ? t('credentialLogin') : t('passwordLogin') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<n-button @click="login" :loading="loading" type="primary" block secondary strong>
|
<n-button @click="login" :loading="loading" type="primary" block secondary strong>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon :component="EmailOutlined" />
|
<n-icon :component="EmailOutlined" />
|
||||||
</template>
|
</template>
|
||||||
{{ t('login') }}
|
{{ loginAndBindTag }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button v-if="openSettings.enableUserCreateEmail" @click="tabValue = 'register'" block secondary
|
<n-button v-if="showNewAddressTab" @click="tabValue = 'register'" block secondary strong>
|
||||||
strong>
|
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon :component="NewLabelOutlined" />
|
<n-icon :component="NewLabelOutlined" />
|
||||||
</template>
|
</template>
|
||||||
@@ -214,23 +311,26 @@ onMounted(async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-tab-pane>
|
</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-spin :show="generateNameLoading">
|
||||||
<n-form>
|
<n-form>
|
||||||
<span>
|
<span>
|
||||||
<p>{{ t("getNewEmailTip1") + addressRegex.source }}</p>
|
<p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip1") +
|
||||||
<p>{{ t("getNewEmailTip2") }}</p>
|
addressRegex.source }}</p>
|
||||||
|
<p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip2") }}</p>
|
||||||
<p>{{ t("getNewEmailTip3") }}</p>
|
<p>{{ t("getNewEmailTip3") }}</p>
|
||||||
</span>
|
</span>
|
||||||
<n-button @click="generateName" style="margin-bottom: 10px;">
|
<n-button v-if="!openSettings.disableCustomAddressName" @click="generateName"
|
||||||
|
style="margin-bottom: 10px;">
|
||||||
{{ t('generateName') }}
|
{{ t('generateName') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-input-group>
|
<n-input-group>
|
||||||
<n-input-group-label v-if="addressPrefix">
|
<n-input-group-label v-if="addressPrefix">
|
||||||
{{ addressPrefix }}
|
{{ addressPrefix }}
|
||||||
</n-input-group-label>
|
</n-input-group-label>
|
||||||
<n-input v-model:value="emailName" show-count :minlength="openSettings.minAddressLen"
|
<n-input v-if="!openSettings.disableCustomAddressName" v-model:value="emailName" show-count
|
||||||
:maxlength="openSettings.maxAddressLen" />
|
:minlength="openSettings.minAddressLen" :maxlength="openSettings.maxAddressLen" />
|
||||||
|
<n-input v-else :value="t('autoGeneratedName')" disabled />
|
||||||
<n-input-group-label>@</n-input-group-label>
|
<n-input-group-label>@</n-input-group-label>
|
||||||
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||||
:options="domainsOptions" />
|
:options="domainsOptions" />
|
||||||
@@ -267,6 +367,12 @@ onMounted(async () => {
|
|||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.switch-login-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.n-form {
|
.n-form {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,34 +5,59 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import Appearance from '../common/Appearance.vue'
|
import { hashPassword } from '../../utils'
|
||||||
import { getRouterPathWithLang } from '../../utils'
|
import { getRouterPathWithLang } from '../../utils'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
jwt, settings, showAddressCredential, loading
|
jwt, settings, showAddressCredential, loading, openSettings
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const showLogout = ref(false)
|
const showLogout = ref(false)
|
||||||
const showDelteAccount = ref(false)
|
const showDeleteAccount = ref(false)
|
||||||
|
const showClearInbox = ref(false)
|
||||||
|
const showClearSentItems = ref(false)
|
||||||
|
const showChangePassword = ref(false)
|
||||||
|
const newPassword = ref('')
|
||||||
|
const confirmPassword = ref('')
|
||||||
const { locale, t } = useI18n({
|
const { locale, t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
logout: "Logout",
|
logout: "Logout",
|
||||||
delteAccount: "Delete Account",
|
deleteAccount: "Delete Account",
|
||||||
showAddressCredential: 'Show Address Credential',
|
showAddressCredential: 'Show Address Credential',
|
||||||
logoutConfirm: 'Are you sure to logout?',
|
logoutConfirm: 'Are you sure to logout?',
|
||||||
delteAccount: "Delete Account",
|
deleteAccount: "Delete Account",
|
||||||
delteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
deleteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
||||||
|
clearInbox: "Clear Inbox",
|
||||||
|
clearSentItems: "Clear Sent Items",
|
||||||
|
clearInboxConfirm: "Are you sure to clear all emails in your inbox?",
|
||||||
|
clearSentItemsConfirm: "Are you sure to clear all emails in your sent items?",
|
||||||
|
success: "Success",
|
||||||
|
changePassword: "Change Password",
|
||||||
|
newPassword: "New Password",
|
||||||
|
confirmPassword: "Confirm Password",
|
||||||
|
passwordMismatch: "Passwords do not match",
|
||||||
|
passwordChanged: "Password changed successfully",
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
delteAccount: "删除账户",
|
deleteAccount: "删除账户",
|
||||||
showAddressCredential: '查看邮箱地址凭证',
|
showAddressCredential: '查看邮箱地址凭证',
|
||||||
logoutConfirm: '确定要退出登录吗?',
|
logoutConfirm: '确定要退出登录吗?',
|
||||||
delteAccount: "删除账户",
|
deleteAccount: "删除账户",
|
||||||
delteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
deleteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
||||||
|
clearInbox: "清空收件箱",
|
||||||
|
clearSentItems: "清空发件箱",
|
||||||
|
clearInboxConfirm: "确定要清空你收件箱中的所有邮件吗?",
|
||||||
|
clearSentItemsConfirm: "确定要清空你发件箱中的所有邮件吗?",
|
||||||
|
success: "成功",
|
||||||
|
changePassword: "修改密码",
|
||||||
|
newPassword: "新密码",
|
||||||
|
confirmPassword: "确认密码",
|
||||||
|
passwordMismatch: "密码不匹配",
|
||||||
|
passwordChanged: "密码修改成功",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -55,35 +80,125 @@ const deleteAccount = async () => {
|
|||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearInbox = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/api/clear_inbox`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
message.success(t("success"));
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
} finally {
|
||||||
|
showClearInbox.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSentItems = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/api/clear_sent_items`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
message.success(t("success"));
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
} finally {
|
||||||
|
showClearSentItems.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePassword = async () => {
|
||||||
|
if (newPassword.value !== confirmPassword.value) {
|
||||||
|
message.error(t("passwordMismatch"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api.fetch(`/api/address_change_password`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
new_password: await hashPassword(newPassword.value)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
message.success(t("passwordChanged"));
|
||||||
|
newPassword.value = '';
|
||||||
|
confirmPassword.value = '';
|
||||||
|
showChangePassword.value = false;
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center" v-if="settings.address">
|
<div class="center" v-if="settings.address">
|
||||||
<n-card :bordered="false" embedded>
|
<n-card :bordered="false" embedded>
|
||||||
<Appearance />
|
|
||||||
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
||||||
{{ t('showAddressCredential') }}
|
{{ t('showAddressCredential') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
|
<n-button v-if="openSettings?.enableAddressPassword" @click="showChangePassword = true" type="info" secondary block strong>
|
||||||
|
{{ t('changePassword') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showClearInbox = true" type="warning" secondary
|
||||||
|
block strong>
|
||||||
|
{{ t('clearInbox') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showClearSentItems = true" type="warning"
|
||||||
|
secondary block strong>
|
||||||
|
{{ t('clearSentItems') }}
|
||||||
|
</n-button>
|
||||||
<n-button @click="showLogout = true" secondary block strong>
|
<n-button @click="showLogout = true" secondary block strong>
|
||||||
{{ t('logout') }}
|
{{ t('logout') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button @click="showDelteAccount = true" type="error" secondary block strong>
|
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showDeleteAccount = true" type="error" secondary
|
||||||
{{ t('delteAccount') }}
|
block strong>
|
||||||
|
{{ t('deleteAccount') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
||||||
<p>{{ t('logoutConfirm') }}</p>
|
<p>{{ t('logoutConfirm') }}</p>
|
||||||
<template #action>
|
<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') }}
|
{{ t('logout') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
|
<n-modal v-model:show="showDeleteAccount" preset="dialog" :title="t('deleteAccount')">
|
||||||
<p>{{ t('delteAccountConfirm') }}</p>
|
<p>{{ t('deleteAccountConfirm') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="deleteAccount" size="small" tertiary type="error">
|
<n-button :loading="loading" @click="deleteAccount" size="small" tertiary type="error">
|
||||||
{{ t('delteAccount') }}
|
{{ t('deleteAccount') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
<n-modal v-model:show="showClearInbox" preset="dialog" :title="t('clearInbox')">
|
||||||
|
<p>{{ t('clearInboxConfirm') }}</p>
|
||||||
|
<template #action>
|
||||||
|
<n-button :loading="loading" @click="clearInbox" size="small" tertiary type="warning">
|
||||||
|
{{ t('clearInbox') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
<n-modal v-model:show="showClearSentItems" preset="dialog" :title="t('clearSentItems')">
|
||||||
|
<p>{{ t('clearSentItemsConfirm') }}</p>
|
||||||
|
<template #action>
|
||||||
|
<n-button :loading="loading" @click="clearSentItems" size="small" tertiary type="warning">
|
||||||
|
{{ t('clearSentItems') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
|
||||||
|
<n-modal v-model:show="showChangePassword" preset="dialog" :title="t('changePassword')">
|
||||||
|
<n-form :model="{ newPassword, confirmPassword }">
|
||||||
|
<n-form-item :label="t('newPassword')">
|
||||||
|
<n-input v-model:value="newPassword" type="password" placeholder="" show-password-on="click" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="t('confirmPassword')">
|
||||||
|
<n-input v-model:value="confirmPassword" type="password" placeholder="" show-password-on="click" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
<template #action>
|
||||||
|
<n-button :loading="loading" @click="changePassword" size="small" tertiary type="info">
|
||||||
|
{{ t('changePassword') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const router = useRouter()
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
jwt, settings, showAddressCredential, userJwt,
|
jwt, settings, showAddressCredential, userJwt,
|
||||||
isTelegram, openSettings
|
isTelegram, openSettings, addressPassword
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
|
|
||||||
const { locale, t } = useI18n({
|
const { locale, t } = useI18n({
|
||||||
@@ -32,7 +32,9 @@ const { locale, t } = useI18n({
|
|||||||
copied: 'Copied',
|
copied: 'Copied',
|
||||||
fetchAddressError: 'Mail address credential is invalid or account not exist, it may be network connection issue, please try again later.',
|
fetchAddressError: 'Mail address credential is invalid or account not exist, it may be network connection issue, please try again later.',
|
||||||
addressCredential: 'Mail Address Credential',
|
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.',
|
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||||
|
addressPassword: 'Address Password',
|
||||||
userLogin: 'User Login',
|
userLogin: 'User Login',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
@@ -43,7 +45,9 @@ const { locale, t } = useI18n({
|
|||||||
copied: '已复制',
|
copied: '已复制',
|
||||||
fetchAddressError: '邮箱地址凭证无效或邮箱地址不存在,也可能是网络连接异常,请稍后再尝试。',
|
fetchAddressError: '邮箱地址凭证无效或邮箱地址不存在,也可能是网络连接异常,请稍后再尝试。',
|
||||||
addressCredential: '邮箱地址凭证',
|
addressCredential: '邮箱地址凭证',
|
||||||
|
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
||||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||||
|
addressPassword: '地址密码',
|
||||||
userLogin: '用户登录',
|
userLogin: '用户登录',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,6 +77,10 @@ const copy = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getUrlWithJwt = () => {
|
||||||
|
return `${window.location.origin}/?jwt=${jwt.value}`
|
||||||
|
}
|
||||||
|
|
||||||
const onUserLogin = async () => {
|
const onUserLogin = async () => {
|
||||||
await router.push(getRouterPathWithLang("/user", locale.value))
|
await router.push(getRouterPathWithLang("/user", locale.value))
|
||||||
}
|
}
|
||||||
@@ -140,9 +148,22 @@ onMounted(async () => {
|
|||||||
<span>
|
<span>
|
||||||
<p>{{ t("addressCredentialTip") }}</p>
|
<p>{{ t("addressCredentialTip") }}</p>
|
||||||
</span>
|
</span>
|
||||||
<n-card :bordered="false" embedded>
|
<n-card embedded>
|
||||||
<b>{{ jwt }}</b>
|
<b>{{ jwt }}</b>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
<n-card embedded v-if="addressPassword">
|
||||||
|
<p><b>{{ settings.address }}</b></p>
|
||||||
|
<p>{{ t('addressPassword') }}: <b>{{ addressPassword }}</b></p>
|
||||||
|
</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>
|
</n-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { ref, h, onMounted } from 'vue';
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
|
import { NPopconfirm } from 'naive-ui';
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
@@ -11,10 +12,16 @@ const { t } = useI18n({
|
|||||||
en: {
|
en: {
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
action: 'Action',
|
action: 'Action',
|
||||||
|
delete: 'Delete',
|
||||||
|
deleteConfirm: 'Are you sure to delete this attachment?',
|
||||||
|
deleteSuccess: 'Deleted successfully',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
download: '下载',
|
download: '下载',
|
||||||
action: '操作',
|
action: '操作',
|
||||||
|
delete: '删除',
|
||||||
|
deleteConfirm: '确定要删除此附件吗?',
|
||||||
|
deleteSuccess: '删除成功',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -66,6 +73,34 @@ const columns = [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ default: () => t('download') }
|
{ 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',
|
actions: 'Actions',
|
||||||
changeMailAddress: 'Change Mail Address',
|
changeMailAddress: 'Change Mail Address',
|
||||||
unbindMailAddress: 'Unbind Mail Address credential',
|
unbindMailAddress: 'Unbind Mail Address credential',
|
||||||
bind: 'Bind',
|
create_or_bind: 'Create or Bind',
|
||||||
bindAddressSuccess: 'Bind Address Success',
|
bindAddressSuccess: 'Bind Address Success',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
@@ -32,7 +32,7 @@ const { t } = useI18n({
|
|||||||
actions: '操作',
|
actions: '操作',
|
||||||
changeMailAddress: '切换邮箱地址',
|
changeMailAddress: '切换邮箱地址',
|
||||||
unbindMailAddress: '解绑邮箱地址',
|
unbindMailAddress: '解绑邮箱地址',
|
||||||
bind: '绑定',
|
create_or_bind: '创建或绑定',
|
||||||
bindAddressSuccess: '绑定地址成功',
|
bindAddressSuccess: '绑定地址成功',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ const columns = [
|
|||||||
<n-tab-pane name="address" :tab="t('address')">
|
<n-tab-pane name="address" :tab="t('address')">
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||||
</n-tab-pane>
|
</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" />
|
<Login :bindUserAddress="bindAddress" />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed, watch, onBeforeUnmount } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import {
|
||||||
|
ExitToAppFilled,
|
||||||
|
ContentCopyFilled,
|
||||||
|
RefreshFilled,
|
||||||
|
ArrowBackIosNewFilled,
|
||||||
|
ArrowForwardIosFilled,
|
||||||
|
SettingsFilled
|
||||||
|
} from '@vicons/material'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
import { api } from '../../api'
|
||||||
|
import Login from '../common/Login.vue'
|
||||||
|
import AccountSettings from './AccountSettings.vue'
|
||||||
|
import { processItem } from '../../utils/email-parser'
|
||||||
|
import MailContentRenderer from '../../components/MailContentRenderer.vue'
|
||||||
|
|
||||||
|
const { jwt, settings, useSimpleIndex, showAddressCredential, openSettings, loading } = useGlobalState()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 邮件数据
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const totalCount = ref(0)
|
||||||
|
const currentMail = ref(null)
|
||||||
|
const showAccountSettingsCard = ref(false)
|
||||||
|
const currentAutoRefreshInterval = ref(60)
|
||||||
|
const timer = ref(null)
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
exitSimpleIndex: 'Exit Simple',
|
||||||
|
copyAddress: 'Copy',
|
||||||
|
addressCopied: 'Address copied successfully',
|
||||||
|
refreshMails: 'Refresh',
|
||||||
|
noMails: 'No mails found',
|
||||||
|
prevPage: 'Previous',
|
||||||
|
nextPage: 'Next',
|
||||||
|
refreshSuccess: 'Mails refreshed successfully',
|
||||||
|
mailCount: '{current} / {total} emails',
|
||||||
|
accountSettings: "Account Settings",
|
||||||
|
addressCredential: 'Mail Address Credential',
|
||||||
|
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login',
|
||||||
|
deleteSuccess: 'Mail deleted successfully',
|
||||||
|
refreshAfter: 'Refresh After {msg} Seconds',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
exitSimpleIndex: '退出极简',
|
||||||
|
copyAddress: '复制',
|
||||||
|
addressCopied: '地址复制成功',
|
||||||
|
refreshMails: '刷新',
|
||||||
|
noMails: '暂无邮件',
|
||||||
|
prevPage: '上一页',
|
||||||
|
nextPage: '下一页',
|
||||||
|
refreshSuccess: '邮件刷新成功',
|
||||||
|
mailCount: '{current} / {total} 封邮件',
|
||||||
|
accountSettings: "账户设置",
|
||||||
|
addressCredential: '邮箱地址凭证',
|
||||||
|
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||||
|
deleteSuccess: '邮件删除成功',
|
||||||
|
refreshAfter: '{msg}秒后刷新',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 复制地址
|
||||||
|
const copyAddress = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(settings.value.address)
|
||||||
|
message.success(t('addressCopied'))
|
||||||
|
} catch (error) {
|
||||||
|
message.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取邮件数据
|
||||||
|
const fetchMails = async () => {
|
||||||
|
if (!settings.value.address) return
|
||||||
|
try {
|
||||||
|
const { results, count } = await api.fetch(`/api/mails?limit=1&offset=${currentPage.value - 1}`)
|
||||||
|
totalCount.value = count > 0 ? count : totalCount.value;
|
||||||
|
const rawMail = results && results.length > 0 ? results[0] : null
|
||||||
|
currentMail.value = rawMail ? await processItem(rawMail) : null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch mails:', error)
|
||||||
|
message.error('获取邮件失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除邮件
|
||||||
|
const deleteMail = async () => {
|
||||||
|
if (!currentMail.value) return;
|
||||||
|
try {
|
||||||
|
await api.fetch(`/api/mails/${currentMail.value.id}`, { method: 'DELETE' });
|
||||||
|
message.success(t('deleteSuccess'));
|
||||||
|
currentMail.value = null;
|
||||||
|
await refreshMails();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete mail:', error);
|
||||||
|
message.error('删除邮件失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新邮件
|
||||||
|
const refreshMails = async () => {
|
||||||
|
if (loading.value) return
|
||||||
|
currentPage.value = 1
|
||||||
|
showAccountSettingsCard.value = false
|
||||||
|
currentAutoRefreshInterval.value = 60
|
||||||
|
await fetchMails()
|
||||||
|
message.success(t('refreshSuccess'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页控制
|
||||||
|
const currentPageDisplay = computed(() => currentPage.value)
|
||||||
|
const totalPages = computed(() => Math.max(1, totalCount.value))
|
||||||
|
const canGoPrev = computed(() => currentPage.value > 1)
|
||||||
|
const canGoNext = computed(() => currentPage.value < totalPages.value)
|
||||||
|
const isFirstPage = computed(() => currentPage.value === 1)
|
||||||
|
|
||||||
|
const prevPage = async () => {
|
||||||
|
if (canGoPrev.value) {
|
||||||
|
currentPage.value--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPage = async () => {
|
||||||
|
if (canGoNext.value) {
|
||||||
|
currentPage.value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听页面变化
|
||||||
|
watch(currentPage, () => {
|
||||||
|
fetchMails()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await api.getSettings()
|
||||||
|
await fetchMails()
|
||||||
|
|
||||||
|
// 启动自动刷新
|
||||||
|
timer.value = setInterval(async () => {
|
||||||
|
if (!isFirstPage.value) {
|
||||||
|
currentAutoRefreshInterval.value = 60
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (--currentAutoRefreshInterval.value <= 0) {
|
||||||
|
await refreshMails()
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="center">
|
||||||
|
<div v-if="!settings.address">
|
||||||
|
<n-card :bordered="false" embedded>
|
||||||
|
<Login />
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<n-card :bordered="false" embedded>
|
||||||
|
<div style="text-align: center; margin-bottom: 16px; font-size: 18px;">
|
||||||
|
<n-text strong size="large">{{ settings.address }}</n-text>
|
||||||
|
</div>
|
||||||
|
<n-flex justify="center">
|
||||||
|
<n-button @click="refreshMails" :loading="loading" type="primary" tertiary size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<RefreshFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('refreshMails') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="copyAddress" tertiary size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ContentCopyFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('copyAddress') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="useSimpleIndex = false" tertiary size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ExitToAppFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('exitSimpleIndex') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="showAccountSettingsCard = true" tertiary size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<SettingsFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('accountSettings') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
<div v-if="isFirstPage" style="text-align: center; margin-top: 12px;">
|
||||||
|
<n-text depth="3" size="12">
|
||||||
|
{{ t('refreshAfter', { msg: Math.max(0, currentAutoRefreshInterval) }) }}
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 账户设置卡片 -->
|
||||||
|
<n-card v-if="showAccountSettingsCard" :bordered="false" embedded closable
|
||||||
|
@close="showAccountSettingsCard = false" :title="t('accountSettings')">
|
||||||
|
<AccountSettings />
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<n-card v-else :bordered="false" embedded style="text-align: left;">
|
||||||
|
|
||||||
|
<div v-if="totalCount > 1">
|
||||||
|
<n-flex justify="space-between">
|
||||||
|
<n-button @click="prevPage" :disabled="!canGoPrev" text size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ArrowBackIosNewFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('prevPage') }}
|
||||||
|
</n-button>
|
||||||
|
<n-text size="small">
|
||||||
|
{{ t('mailCount', { current: currentPageDisplay, total: totalCount }) }}
|
||||||
|
</n-text>
|
||||||
|
<n-button @click="nextPage" :disabled="!canGoNext" text size="small" icon-placement="right">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ArrowForwardIosFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('nextPage') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!currentMail" class="no-mail">
|
||||||
|
<n-empty :description="t('noMails')" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<h3 v-if="currentMail.subject">{{ currentMail.subject }}</h3>
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<MailContentRenderer :mail="currentMail" :showEMailTo="false" :showReply="false"
|
||||||
|
:enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :showSaveS3="false"
|
||||||
|
:onDelete="deleteMail" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
<n-modal v-model:show="showAddressCredential" preset="dialog" :title="t('addressCredential')">
|
||||||
|
<span>
|
||||||
|
<p>{{ t("addressCredentialTip") }}</p>
|
||||||
|
</span>
|
||||||
|
<n-card embedded>
|
||||||
|
<b>{{ jwt }}</b>
|
||||||
|
</n-card>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.center {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -5,8 +5,9 @@ import { useGlobalState } from '../../store'
|
|||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import { onMounted, watch } from 'vue';
|
import { onMounted, watch } from 'vue';
|
||||||
import { processItem } from '../../utils/email-parser'
|
import { processItem } from '../../utils/email-parser'
|
||||||
|
import { utcToLocalDate } from '../../utils';
|
||||||
|
|
||||||
const { telegramApp, loading } = useGlobalState()
|
const { telegramApp, loading, useUTCDate } = useGlobalState()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const curMail = ref({});
|
const curMail = ref({});
|
||||||
@@ -45,12 +46,12 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<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">
|
<n-tag type="info">
|
||||||
ID: {{ curMail.id }}
|
ID: {{ curMail.id }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
Date: {{ curMail.created_at }}
|
Date: {{ utcToLocalDate(curMail.created_at, useUTCDate) }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
FROM: {{ curMail.source }}
|
FROM: {{ curMail.source }}
|
||||||
@@ -58,7 +59,8 @@ onMounted(async () => {
|
|||||||
<n-tag v-if="showEMailTo" type="info">
|
<n-tag v-if="showEMailTo" type="info">
|
||||||
TO: {{ curMail.address }}
|
TO: {{ curMail.address }}
|
||||||
</n-tag>
|
</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>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -70,5 +72,6 @@ onMounted(async () => {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
height: 80vh;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { useGlobalState } from '../../store'
|
|||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import { getRouterPathWithLang } from '../../utils'
|
import { getRouterPathWithLang } from '../../utils'
|
||||||
|
|
||||||
|
import Login from '../common/Login.vue';
|
||||||
|
|
||||||
const { jwt } = useGlobalState()
|
const { jwt } = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -20,9 +22,14 @@ const { locale, t } = useI18n({
|
|||||||
mail_count: 'Mail Count',
|
mail_count: 'Mail Count',
|
||||||
send_count: 'Send Count',
|
send_count: 'Send Count',
|
||||||
actions: 'Actions',
|
actions: 'Actions',
|
||||||
changeMailAddress: 'Change Mail Address',
|
changeMailAddress: 'Change Address',
|
||||||
unbindAddress: 'Unbind Address',
|
unbindAddress: 'Unbind Address',
|
||||||
unbindAddressTip: 'Before unbinding, please switch to this email address and save the email address credential.',
|
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: {
|
zh: {
|
||||||
success: '成功',
|
success: '成功',
|
||||||
@@ -30,14 +37,23 @@ const { locale, t } = useI18n({
|
|||||||
mail_count: '邮件数量',
|
mail_count: '邮件数量',
|
||||||
send_count: '发送数量',
|
send_count: '发送数量',
|
||||||
actions: '操作',
|
actions: '操作',
|
||||||
changeMailAddress: '切换邮箱地址',
|
changeMailAddress: '切换地址',
|
||||||
unbindAddress: '解绑地址',
|
unbindAddress: '解绑地址',
|
||||||
unbindAddressTip: '解绑前请切换到此邮箱地址并保存邮箱地址凭证。',
|
unbindAddressTip: '解绑前请切换到此邮箱地址并保存邮箱地址凭证。',
|
||||||
|
transferAddress: '转移地址',
|
||||||
|
targetUserEmail: '目标用户邮箱',
|
||||||
|
transferAddressTip: '转移地址到其他用户将会从你的账户中移除此地址并转移给其他用户。确定要转移地址吗?',
|
||||||
|
address: '地址',
|
||||||
|
create_or_bind: '创建或绑定',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = ref([])
|
const data = ref([])
|
||||||
|
const showTranferAddress = ref(false)
|
||||||
|
const currentAddress = ref("")
|
||||||
|
const currentAddressId = ref(0)
|
||||||
|
const targetUserEmail = ref('')
|
||||||
|
|
||||||
const changeMailAddress = async (address_id) => {
|
const changeMailAddress = async (address_id) => {
|
||||||
try {
|
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 () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const { results, count: addressCount } = await api.fetch(
|
const { results } = await api.fetch(
|
||||||
`/user_api/bind_address`
|
`/user_api/bind_address`
|
||||||
);
|
);
|
||||||
data.value = results;
|
data.value = results;
|
||||||
if (addressCount > 0) {
|
|
||||||
count.value = addressCount;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
message.error(error.message || "error");
|
message.error(error.message || "error");
|
||||||
@@ -86,10 +128,6 @@ const fetchData = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
|
||||||
title: "ID",
|
|
||||||
key: "id"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t('name'),
|
title: t('name'),
|
||||||
key: "name"
|
key: "name"
|
||||||
@@ -138,6 +176,18 @@ const columns = [
|
|||||||
default: () => `${t('changeMailAddress')}?`
|
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,
|
h(NPopconfirm,
|
||||||
{
|
{
|
||||||
onPositiveClick: () => unbindAddress(row.id)
|
onPositiveClick: () => unbindAddress(row.id)
|
||||||
@@ -164,8 +214,29 @@ onMounted(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="overflow: auto;">
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { api } from '../../api'
|
||||||
|
import MailBox from '../../components/MailBox.vue';
|
||||||
|
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
addressQueryTip: 'Leave blank to query all addresses',
|
||||||
|
keywordQueryTip: 'Leave blank to not query by keyword',
|
||||||
|
query: 'Query',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
addressQueryTip: '留空查询所有地址',
|
||||||
|
keywordQueryTip: '留空不按关键字查询',
|
||||||
|
query: '查询',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mailBoxKey = ref("")
|
||||||
|
const addressFilter = ref();
|
||||||
|
const mailKeyword = ref("")
|
||||||
|
const addressFilterOptions = ref([]);
|
||||||
|
|
||||||
|
const queryMail = () => {
|
||||||
|
addressFilter.value = addressFilter.value ? addressFilter.value.trim() : addressFilter.value;
|
||||||
|
mailKeyword.value = mailKeyword.value.trim();
|
||||||
|
mailBoxKey.value = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchMailData = async (limit, offset) => {
|
||||||
|
return await api.fetch(
|
||||||
|
`/user_api/mails`
|
||||||
|
+ `?limit=${limit}`
|
||||||
|
+ `&offset=${offset}`
|
||||||
|
+ (addressFilter.value ? `&address=${addressFilter.value}` : '')
|
||||||
|
+ (mailKeyword.value ? `&keyword=${mailKeyword.value}` : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchAddresData = async () => {
|
||||||
|
try {
|
||||||
|
const { results } = await api.fetch(
|
||||||
|
`/user_api/bind_address`
|
||||||
|
);
|
||||||
|
addressFilterOptions.value = results.map((item) => {
|
||||||
|
return {
|
||||||
|
label: item.name,
|
||||||
|
value: item.name
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteMail = async (curMailId) => {
|
||||||
|
await api.fetch(`/user_api/mails/${curMailId}`, { method: 'DELETE' });
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(addressFilter, async (newValue) => {
|
||||||
|
queryMail();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchAddresData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<n-input-group>
|
||||||
|
<n-select v-model:value="addressFilter" :options="addressFilterOptions" clearable
|
||||||
|
:placeholder="t('addressQueryTip')" />
|
||||||
|
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" @keydown.enter="queryMail" />
|
||||||
|
<n-button @click="queryMail" type="primary" tertiary>
|
||||||
|
{{ t('query') }}
|
||||||
|
</n-button>
|
||||||
|
</n-input-group>
|
||||||
|
<div style="margin-top: 10px;"></div>
|
||||||
|
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="true" :fetchMailData="fetchMailData"
|
||||||
|
:deleteMail="deleteMail" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -232,7 +232,7 @@ const renamePasskey = async () => {
|
|||||||
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
||||||
<p>{{ t('logoutConfirm') }}</p>
|
<p>{{ t('logoutConfirm') }}</p>
|
||||||
<template #action>
|
<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') }}
|
{{ t('logout') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default defineConfig({
|
|||||||
{
|
{
|
||||||
'naive-ui': [
|
'naive-ui': [
|
||||||
'useMessage',
|
'useMessage',
|
||||||
|
'useNotification',
|
||||||
'NButton',
|
'NButton',
|
||||||
'NPopconfirm',
|
'NPopconfirm',
|
||||||
'NIcon',
|
'NIcon',
|
||||||
@@ -35,13 +36,16 @@ export default defineConfig({
|
|||||||
resolvers: [NaiveUiResolver()]
|
resolvers: [NaiveUiResolver()]
|
||||||
}),
|
}),
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: 'autoUpdate',
|
registerType: null,
|
||||||
devOptions: {
|
devOptions: {
|
||||||
enabled: true
|
enabled: false
|
||||||
},
|
},
|
||||||
workbox: {
|
workbox: {
|
||||||
disableDevLogs: true,
|
disableDevLogs: true,
|
||||||
globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
|
globPatterns: [],
|
||||||
|
runtimeCaching: [],
|
||||||
|
navigateFallback: null,
|
||||||
|
cleanupOutdatedCaches: true,
|
||||||
},
|
},
|
||||||
manifest: {
|
manifest: {
|
||||||
name: 'Temp Email',
|
name: 'Temp Email',
|
||||||
@@ -65,5 +69,10 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version),
|
'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version),
|
||||||
|
},
|
||||||
|
esbuild: {
|
||||||
|
supported: {
|
||||||
|
'top-level-await': true
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mail-parser-wasm"
|
name = "mail-parser-wasm"
|
||||||
version = "0.1.8"
|
version = "0.2.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "A simple mail parser for wasm"
|
description = "A simple mail parser for wasm"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -9,5 +9,5 @@ license = "MIT"
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
mail-parser = "0.9.3"
|
mail-parser = "0.9.4"
|
||||||
wasm-bindgen = "0.2.92"
|
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]
|
#[wasm_bindgen]
|
||||||
pub struct MessageResult {
|
pub struct MessageResult {
|
||||||
sender: String,
|
sender: String,
|
||||||
subject: String,
|
subject: String,
|
||||||
|
headers: Vec<MessageHeader>,
|
||||||
body_html: String,
|
body_html: String,
|
||||||
text: String,
|
text: String,
|
||||||
attachments: Vec<AttachmentResult>,
|
attachments: Vec<AttachmentResult>,
|
||||||
@@ -56,6 +77,11 @@ impl MessageResult {
|
|||||||
self.subject.clone()
|
self.subject.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(getter)]
|
||||||
|
pub fn headers(&self) -> Vec<MessageHeader> {
|
||||||
|
self.headers.clone()
|
||||||
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(getter)]
|
#[wasm_bindgen(getter)]
|
||||||
pub fn body_html(&self) -> String {
|
pub fn body_html(&self) -> String {
|
||||||
self.body_html.clone()
|
self.body_html.clone()
|
||||||
@@ -119,6 +145,7 @@ pub fn parse_message(raw_message: &str) -> MessageResult {
|
|||||||
return MessageResult {
|
return MessageResult {
|
||||||
sender: String::new(),
|
sender: String::new(),
|
||||||
subject: String::new(),
|
subject: String::new(),
|
||||||
|
headers: Vec::new(),
|
||||||
body_html: String::new(),
|
body_html: String::new(),
|
||||||
text: String::new(),
|
text: String::new(),
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(),
|
||||||
@@ -146,6 +173,14 @@ pub fn parse_message(raw_message: &str) -> MessageResult {
|
|||||||
.subject()
|
.subject()
|
||||||
.map(|subject| subject.to_owned())
|
.map(|subject| subject.to_owned())
|
||||||
.unwrap_or(String::new()),
|
.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: message
|
||||||
.body_html(0)
|
.body_html(0)
|
||||||
.map(|html| html.into_owned())
|
.map(|html| html.into_owned())
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import initAsync, { initSync, parse_message } from './mail_parser_wasm';
|
import initAsync, { initSync, parse_message } from './mail_parser_wasm';
|
||||||
import MODULE from './mail_parser_wasm_bg.wasm';
|
import MODULE from './mail_parser_wasm_bg.wasm';
|
||||||
|
|
||||||
initSync(MODULE);
|
initSync({ module: MODULE });
|
||||||
|
|
||||||
|
|
||||||
export { initAsync, MODULE };
|
export { initAsync, MODULE };
|
||||||
export * from './mail_parser_wasm';
|
export * from './mail_parser_wasm';
|
||||||
export const parse_message_wrapper = (raw_message) => {
|
export const parse_message_wrapper = (raw_message) => {
|
||||||
initSync(MODULE);
|
initSync({ module: MODULE });
|
||||||
return parse_message(raw_message);
|
return parse_message(raw_message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"url": "https://github.com/dreamhunter2333/cloudflare_temp_email",
|
"url": "https://github.com/dreamhunter2333/cloudflare_temp_email",
|
||||||
"directory": "mail-parser-wasm"
|
"directory": "mail-parser-wasm"
|
||||||
},
|
},
|
||||||
"version": "0.1.8",
|
"version": "0.2.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"files": [
|
||||||
"mail_parser_wasm_bg.wasm",
|
"mail_parser_wasm_bg.wasm",
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ const API_PATHS = [
|
|||||||
"/open_api/",
|
"/open_api/",
|
||||||
"/user_api/",
|
"/user_api/",
|
||||||
"/admin/",
|
"/admin/",
|
||||||
"/telegram/"
|
"/telegram/",
|
||||||
|
"/external/",
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function onRequest(context) {
|
export async function onRequest(context) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "temp-email-pages",
|
"name": "temp-email-pages",
|
||||||
"version": "0.7.4",
|
"version": "1.0.6",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"wrangler": "^3.71.0"
|
"wrangler": "^4.40.1"
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 --latest
|
||||||
|
pnpm add -D wrangler@latest
|
||||||
|
cd ..
|
||||||
@@ -65,6 +65,31 @@ class SimpleMailbox:
|
|||||||
self.addListener = self.listeners.append
|
self.addListener = self.listeners.append
|
||||||
self.removeListener = self.listeners.remove
|
self.removeListener = self.listeners.remove
|
||||||
self.message_count = 0
|
self.message_count = 0
|
||||||
|
self._update_message_count()
|
||||||
|
|
||||||
|
def _update_message_count(self):
|
||||||
|
"""主动获取邮件总数"""
|
||||||
|
try:
|
||||||
|
if self.name == "INBOX":
|
||||||
|
endpoint = "/api/mails"
|
||||||
|
elif self.name == "SENT":
|
||||||
|
endpoint = "/api/sendbox"
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
res = httpx.get(
|
||||||
|
f"{settings.proxy_url}{endpoint}?limit=1&offset=0",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {self.password}",
|
||||||
|
"x-custom-auth": f"{settings.basic_password}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if res.status_code == 200:
|
||||||
|
self.message_count = res.json()["count"]
|
||||||
|
# _logger.info(f"Updated {self.name} message count: {self.message_count}")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Failed to update message count for {self.name}: {e}")
|
||||||
|
|
||||||
def getFlags(self):
|
def getFlags(self):
|
||||||
return ["\\Seen"]
|
return ["\\Seen"]
|
||||||
@@ -73,7 +98,9 @@ class SimpleMailbox:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
def getMessageCount(self):
|
def getMessageCount(self):
|
||||||
return self.message_count or 1000
|
# 每次请求时更新邮件总数
|
||||||
|
self._update_message_count()
|
||||||
|
return self.message_count
|
||||||
|
|
||||||
def getRecentCount(self):
|
def getRecentCount(self):
|
||||||
return 0
|
return 0
|
||||||
@@ -91,6 +118,8 @@ class SimpleMailbox:
|
|||||||
return "/"
|
return "/"
|
||||||
|
|
||||||
def requestStatus(self, names):
|
def requestStatus(self, names):
|
||||||
|
# 在状态请求时也更新邮件总数
|
||||||
|
self._update_message_count()
|
||||||
r = {}
|
r = {}
|
||||||
if "MESSAGES" in names:
|
if "MESSAGES" in names:
|
||||||
r["MESSAGES"] = self.getMessageCount()
|
r["MESSAGES"] = self.getMessageCount()
|
||||||
@@ -105,65 +134,99 @@ class SimpleMailbox:
|
|||||||
return defer.succeed(r)
|
return defer.succeed(r)
|
||||||
|
|
||||||
def fetch(self, messages, uid):
|
def fetch(self, messages, uid):
|
||||||
|
"""边查边返回邮件"""
|
||||||
|
def email_generator():
|
||||||
|
for range_item in messages.ranges:
|
||||||
|
start, end = range_item
|
||||||
|
_logger.info(f"Fetching messages: {self.name}, range: {start}-{end}")
|
||||||
|
|
||||||
|
for email_data in self.fetchGenerator(start, end):
|
||||||
|
yield email_data
|
||||||
|
|
||||||
|
# 返回生成器,让IMAP4服务器逐个处理
|
||||||
|
return email_generator()
|
||||||
|
|
||||||
|
def fetchGenerator(self, start, end):
|
||||||
|
"""通用的邮件获取生成器,边查边返回"""
|
||||||
|
start = max(start, 1)
|
||||||
|
|
||||||
|
# 根据邮箱类型确定API端点
|
||||||
if self.name == "INBOX":
|
if self.name == "INBOX":
|
||||||
return self.fetchINBOX(messages)
|
endpoint = "/api/mails"
|
||||||
if self.name == "SENT":
|
elif self.name == "SENT":
|
||||||
return self.fetchSENT(messages)
|
endpoint = "/api/sendbox"
|
||||||
return []
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
def fetchINBOX(self, messages):
|
# 首先获取服务端邮件总数
|
||||||
start, end = messages.ranges[0]
|
count_res = httpx.get(
|
||||||
start = max(start, 1)
|
f"{settings.proxy_url}{endpoint}?limit=1&offset=0",
|
||||||
limit = min(20, end - start + 1) if end and end >= start else 20
|
|
||||||
if self.message_count > 0 and start > self.message_count:
|
|
||||||
return []
|
|
||||||
res = httpx.get(
|
|
||||||
f"{settings.proxy_url}/api/mails?limit={limit}&offset={start - 1}",
|
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {self.password}",
|
"Authorization": f"Bearer {self.password}",
|
||||||
"x-custom-auth": f"{settings.basic_password}",
|
"x-custom-auth": f"{settings.basic_password}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if res.status_code != 200:
|
if count_res.status_code != 200:
|
||||||
_logger.error(
|
_logger.error(
|
||||||
"Failed: "
|
f"Failed to get {self.name} email count: "
|
||||||
f"code=[{res.status_code}] text=[{res.text}]"
|
f"code=[{count_res.status_code}] text=[{count_res.text}]"
|
||||||
)
|
)
|
||||||
raise Exception("Failed to fetch emails")
|
return
|
||||||
if res.json()["count"] > 0:
|
|
||||||
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"]))
|
|
||||||
]
|
|
||||||
|
|
||||||
def fetchSENT(self, messages):
|
total_count = count_res.json()["count"]
|
||||||
start, end = messages.ranges[0]
|
self.message_count = total_count
|
||||||
start = max(start, 1)
|
|
||||||
limit = min(20, end - start + 1) if end and end >= start else 20
|
if total_count == 0 or start > total_count:
|
||||||
if self.message_count > 0 and start > self.message_count:
|
return
|
||||||
return []
|
|
||||||
res = httpx.get(
|
# 分批处理,每次获取一小批就立即返回
|
||||||
f"{settings.proxy_url}/api/sendbox?limit={limit}&offset={start - 1}",
|
batch_size = 20
|
||||||
headers={
|
current_start = start
|
||||||
"Authorization": f"Bearer {self.password}",
|
current_end = min(end or total_count, total_count)
|
||||||
"x-custom-auth": f"{settings.basic_password}",
|
|
||||||
"Content-Type": "application/json"
|
while current_start <= current_end:
|
||||||
}
|
batch_end = min(current_start + batch_size - 1, current_end)
|
||||||
)
|
|
||||||
if res.status_code != 200:
|
# 计算这一批的参数
|
||||||
_logger.error(
|
limit = batch_end - current_start + 1
|
||||||
"Failed: "
|
server_offset = total_count - batch_end
|
||||||
f"code=[{res.status_code}] text=[{res.text}]"
|
server_offset = max(0, server_offset)
|
||||||
|
|
||||||
|
_logger.info(
|
||||||
|
f"Fetching batch: start={current_start}, end={batch_end}, "
|
||||||
|
f"total_count={total_count}, limit={limit}, "
|
||||||
|
f"server_offset={server_offset}"
|
||||||
)
|
)
|
||||||
raise Exception("Failed to fetch emails")
|
|
||||||
if res.json()["count"] > 0:
|
res = httpx.get(
|
||||||
self.message_count = res.json()["count"]
|
f"{settings.proxy_url}{endpoint}?limit={limit}&offset={server_offset}",
|
||||||
return [
|
headers={
|
||||||
(start + uid, SimpleMessage(start + uid, generate_email_model(item)))
|
"Authorization": f"Bearer {self.password}",
|
||||||
for uid, item in enumerate(reversed(res.json()["results"]))
|
"x-custom-auth": f"{settings.basic_password}",
|
||||||
]
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if res.status_code != 200:
|
||||||
|
_logger.error(
|
||||||
|
f"Failed to fetch {self.name} emails: "
|
||||||
|
f"code=[{res.status_code}] text=[{res.text}]"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
emails = res.json()["results"]
|
||||||
|
for i, item in enumerate(reversed(emails)):
|
||||||
|
uid = total_count - server_offset - len(emails) + i + 1
|
||||||
|
if current_start <= uid <= batch_end:
|
||||||
|
if self.name == "INBOX":
|
||||||
|
email_model = parse_email(item["raw"])
|
||||||
|
elif self.name == "SENT":
|
||||||
|
email_model = generate_email_model(item)
|
||||||
|
|
||||||
|
# 立即返回这封邮件
|
||||||
|
yield (uid, SimpleMessage(uid, email_model))
|
||||||
|
|
||||||
|
current_start = batch_end + 1
|
||||||
|
|
||||||
def getUID(self, message):
|
def getUID(self, message):
|
||||||
return message.uid
|
return message.uid
|
||||||
|
|||||||
@@ -48,17 +48,14 @@ def generate_email_model(item: dict) -> EmailModel:
|
|||||||
email_json = json.loads(item["raw"])
|
email_json = json.loads(item["raw"])
|
||||||
message = MIMEMultipart()
|
message = MIMEMultipart()
|
||||||
if email_json.get("version") == "v2":
|
if email_json.get("version") == "v2":
|
||||||
message['From'] = f"{email_json["from_name"]} <{item["address"]}>" if email_json.get(
|
message['From'] = f'{email_json["from_name"]} <{item["address"]}>' if email_json.get("from_name") else item["address"]
|
||||||
"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['To'] = f"{email_json["to_name"]} <{email_json["to_mail"]}>" if email_json.get(
|
|
||||||
"to_name") else email_json["to_mail"]
|
|
||||||
message.attach(MIMEText(
|
message.attach(MIMEText(
|
||||||
email_json["content"],
|
email_json["content"],
|
||||||
"html" if email_json.get("is_html") else "plain"
|
"html" if email_json.get("is_html") else "plain"
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
message['From'] = f"{email_json["from"]['name']} <{
|
message['From'] = f'{email_json["from"]["name"]} <{email_json["from"]["email"]}>'
|
||||||
email_json["from"]['email']}>"
|
|
||||||
message['To'] = ", ".join(
|
message['To'] = ", ".join(
|
||||||
[f"{to['name']} <{to['email']}>" for to in email_json["personalizations"][0]["to"]])
|
[f"{to['name']} <{to['email']}>" for to in email_json["personalizations"][0]["to"]])
|
||||||
message.attach(MIMEText(
|
message.attach(MIMEText(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
aiosmtpd==1.4.6
|
aiosmtpd==1.4.6
|
||||||
pydantic-settings==2.2.1
|
pydantic-settings==2.9.1
|
||||||
requests==2.32.0
|
requests==2.32.4
|
||||||
twisted==24.3.0
|
Twisted==25.5.0
|
||||||
httpx==0.27.0
|
httpx==0.28.1
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
|
|
||||||
logo: { src: '/logo.png', width: 24, height: 24 },
|
logo: { src: '/logo.png', width: 24, height: 24 },
|
||||||
search: { provider: 'local' },
|
search: { provider: 'local' },
|
||||||
socialLinks: [
|
socialLinks: [
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const en = defineConfig({
|
|||||||
description: 'CloudFlare Free sending and receiving of temporary domain name mailboxes',
|
description: 'CloudFlare Free sending and receiving of temporary domain name mailboxes',
|
||||||
|
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
|
outline: 'deep',
|
||||||
nav: nav(),
|
nav: nav(),
|
||||||
|
|
||||||
editLink: {
|
editLink: {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const zh = defineConfig({
|
|||||||
},
|
},
|
||||||
|
|
||||||
outline: {
|
outline: {
|
||||||
|
level: 'deep',
|
||||||
label: '页面导航'
|
label: '页面导航'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -119,9 +120,22 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: '通过 Github Actions 部署',
|
text: '通过 Github Actions 部署',
|
||||||
|
collapsed: true,
|
||||||
|
items: [
|
||||||
|
{ text: 'Github Actions 部署准备', link: 'actions/pre-requisite' },
|
||||||
|
{ text: 'D1 数据库', link: 'actions/d1' },
|
||||||
|
{ text: 'Github Actions 配置', link: 'actions/github-action' },
|
||||||
|
{ text: '配置邮件转发', link: 'email-routing.md' },
|
||||||
|
{ text: '配置发送邮件', link: 'config-send-mail' },
|
||||||
|
{ text: '自动更新配置', link: 'actions/auto-update' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '通用',
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
items: [
|
items: [
|
||||||
{ text: '通过 Github Actions 部署', link: 'github-action' },
|
{ text: 'worker变量说明', link: 'worker-vars' },
|
||||||
|
{ text: '常见问题', link: 'common-issues' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -138,6 +152,8 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
|||||||
{ text: '配置 webhook', link: 'feature/webhook' },
|
{ text: '配置 webhook', link: 'feature/webhook' },
|
||||||
{ text: '新建邮箱地址 API', link: 'feature/new-address-api' },
|
{ text: '新建邮箱地址 API', link: 'feature/new-address-api' },
|
||||||
{ text: 'Oauth2 第三方登录', link: 'feature/user-oauth2' },
|
{ text: 'Oauth2 第三方登录', link: 'feature/user-oauth2' },
|
||||||
|
{ text: '配置其他worker增强', link: 'feature/another-worker-enhanced' },
|
||||||
|
{ text: '给网页增加 Google Ads', link: 'feature/google-ads.md' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ pnpm run deploy
|
|||||||
```toml
|
```toml
|
||||||
name = "cloudflare_temp_email"
|
name = "cloudflare_temp_email"
|
||||||
main = "src/worker.ts"
|
main = "src/worker.ts"
|
||||||
compatibility_date = "2023-08-14"
|
compatibility_date = "2024-09-23"
|
||||||
node_compat = true
|
compatibility_flags = [ "nodejs_compat" ]
|
||||||
|
|
||||||
# enable cron if you want set auto clean up
|
# enable cron if you want set auto clean up
|
||||||
# [triggers]
|
# [triggers]
|
||||||
@@ -74,16 +74,19 @@ node_compat = true
|
|||||||
# ]
|
# ]
|
||||||
|
|
||||||
[vars]
|
[vars]
|
||||||
|
# DEFAULT_LANG = "zh"
|
||||||
# TITLE = "Custom Title" # The title of the site
|
# TITLE = "Custom Title" # The title of the site
|
||||||
PREFIX = "tmp" # The mailbox name prefix to be processed
|
PREFIX = "tmp" # The mailbox name prefix to be processed
|
||||||
# (min, max) length of the adderss, if not set, the default is (1, 30)
|
# (min, max) length of the adderss, if not set, the default is (1, 30)
|
||||||
|
# MIN_ADDRESS_LEN = 1
|
||||||
|
# MAX_ADDRESS_LEN = 30
|
||||||
# ANNOUNCEMENT = "Custom Announcement"
|
# ANNOUNCEMENT = "Custom Announcement"
|
||||||
|
# always show ANNOUNCEMENT even no changes
|
||||||
|
# ALWAYS_SHOW_ANNOUNCEMENT = true
|
||||||
# address check REGEX, if not set, will not check
|
# address check REGEX, if not set, will not check
|
||||||
# ADDRESS_CHECK_REGEX = "^(?!.*admin).*"
|
# ADDRESS_CHECK_REGEX = "^(?!.*admin).*"
|
||||||
# address name replace REGEX, if not set, the default is [^a-z0-9]
|
# address name replace REGEX, if not set, the default is [^a-z0-9]
|
||||||
# ADDRESS_REGEX = "[^a-z0-9]"
|
# ADDRESS_REGEX = "[^a-z0-9]"
|
||||||
# MIN_ADDRESS_LEN = 1
|
|
||||||
# MAX_ADDRESS_LEN = 30
|
|
||||||
# If you want your site to be private, uncomment below and change your password
|
# If you want your site to be private, uncomment below and change your password
|
||||||
# PASSWORDS = ["123", "456"]
|
# PASSWORDS = ["123", "456"]
|
||||||
# admin console password, if not configured, access to the console is not allowed
|
# admin console password, if not configured, access to the console is not allowed
|
||||||
@@ -107,6 +110,8 @@ JWT_SECRET = "xxx" # Key used to generate jwt
|
|||||||
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
|
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
|
||||||
# Allow users to create email addresses
|
# Allow users to create email addresses
|
||||||
ENABLE_USER_CREATE_EMAIL = true
|
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
|
# Allow users to delete messages
|
||||||
ENABLE_USER_DELETE_EMAIL = true
|
ENABLE_USER_DELETE_EMAIL = true
|
||||||
# Allow automatic replies to emails
|
# Allow automatic replies to emails
|
||||||
@@ -118,14 +123,29 @@ ENABLE_AUTO_REPLY = false
|
|||||||
# DISABLE_SHOW_GITHUB = true # Disable Show GitHub link
|
# DISABLE_SHOW_GITHUB = true # Disable Show GitHub link
|
||||||
# default send balance, if not set, it will be 0
|
# default send balance, if not set, it will be 0
|
||||||
# DEFAULT_SEND_BALANCE = 1
|
# 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
|
# Turnstile verification configuration
|
||||||
# CF_TURNSTILE_SITE_KEY = ""
|
# CF_TURNSTILE_SITE_KEY = ""
|
||||||
# CF_TURNSTILE_SECRET_KEY = ""
|
# CF_TURNSTILE_SECRET_KEY = ""
|
||||||
# telegram bot
|
# telegram bot
|
||||||
# TG_MAX_ADDRESS = 5
|
# 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
|
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||||
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
# 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]]
|
[[d1_databases]]
|
||||||
binding = "DB"
|
binding = "DB"
|
||||||
|
|||||||
@@ -15,14 +15,17 @@ hero:
|
|||||||
- theme: alt
|
- theme: alt
|
||||||
text: 通过用户界面部署
|
text: 通过用户界面部署
|
||||||
link: /zh/guide/quick-start
|
link: /zh/guide/quick-start
|
||||||
|
- theme: alt
|
||||||
|
text: 通过 Github Actions 部署
|
||||||
|
link: /zh/guide/quick-start
|
||||||
|
|
||||||
features:
|
features:
|
||||||
- title: 免费托管在 CloudFlare,无需服务器
|
- title: 仅需域名即可私有部署, 免费托管在 CloudFlare,无需服务器
|
||||||
details: Cloudflare D1 数据库,Cloudflare Pages 前端,Cloudflare Workers 后端, Cloudflare Email Routing
|
details: 支持 password 登录邮箱, 用户注册,使用访问密码可作为私人站点,支持附件功能。
|
||||||
- title: 仅需域名即可私有部署
|
|
||||||
details: 支持 password 登录邮箱,使用访问密码可作为私人站点,支持附件功能
|
|
||||||
- title: 使用 rust wasm 解析邮件
|
- title: 使用 rust wasm 解析邮件
|
||||||
details: 使用 rust wasm 解析邮件,支持邮件各种RFC标准,支持附件, 速度极快
|
details: 使用 rust wasm 解析邮件,支持邮件各种RFC标准,支持附件, 速度极快
|
||||||
|
- title: 支持 Telegram Bot 和 Webhook
|
||||||
|
details: 邮件可转发到 Telegram 或者 webhook, Telegram Bot 支持绑定邮箱,查看邮件, Telegram 小程序
|
||||||
- title: 支持发送邮件(UI/API/SMTP)
|
- title: 支持发送邮件(UI/API/SMTP)
|
||||||
details: 支持通过域名邮箱发送 txt 或者 html 邮件,支持 DKIM 签名, UI/API/SMTP 发送邮件
|
details: 支持通过域名邮箱发送 txt 或者 html 邮件,支持 DKIM 签名, UI/API/SMTP 发送邮件
|
||||||
---
|
---
|
||||||
|
|||||||
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 30 KiB |