Compare commits

..
57 Commits
Author SHA1 Message Date
Dream Hunter 840496c48f feat: telegram check addrss exists (#705) 2025-08-08 13:26:45 +08:00
Dream Hunter 9843b35f54 feat: telegram use the random domain when not set (#704) 2025-08-08 13:07:00 +08:00
Dream Hunter bfd66f5019 fix: worker node compat (#699) 2025-07-28 13:18:47 +08:00
Dream Hunter 0bc31360b0 feat: upgrade dependencies (#698) 2025-07-28 12:58:47 +08:00
Dream Hunter 267d9bb93e fix: oauth2 callback failed (#691) 2025-07-20 17:09:23 +08:00
Dream Hunter 2cc84d565c feat: upgrade dependencies (#690) 2025-07-19 13:26:10 +08:00
Dream Hunter c96d180591 feat(oauth2): add default role assignment for new OAuth2 users (#688)
- Add default role assignment logic in OAuth2 login flow
- Import getStringValue and getUserRoles utilities
- Validate default role exists in system before assignment
- Use ON CONFLICT DO NOTHING to preserve existing user roles
- Add proper error handling for role assignment failures
2025-07-14 23:55:36 +08:00
Dream Hunter 1303b0f2a9 feat: |UI| add simple index (#684) 2025-06-28 15:52:19 +08:00
Dream Hunter 9f535a0a90 feature: update dependencies (#682) 2025-06-24 18:27:45 +08:00
Dream Hunter 70109785c6 feature: update readme (#680) 2025-06-22 20:41:21 +08:00
Dream Hunter 7fd10f2775 feature: update readme (#679) 2025-06-22 20:38:49 +08:00
Dream Hunter f59b8c7a1b feature: update readme (#678) 2025-06-22 20:21:04 +08:00
Dream Hunter 312ac13185 feature: update readme (#677) 2025-06-22 20:08:07 +08:00
Dream Hunter e6c582be9f feature: update address updated_at in multi api (#676) 2025-06-21 01:48:35 +08:00
Dream Hunter 483c429feb feature: update address updated_at in multi api (#675) 2025-06-21 01:41:28 +08:00
Dream Hunter da5482e095 feature: update dependencies (#674) 2025-06-21 01:06:37 +08:00
Dream Hunter de4646876a fix: imap cannot update message (#673) 2025-06-21 01:00:46 +08:00
Dream Hunter bbc8a96811 fix: imap cannot update message (#672) 2025-06-21 00:57:44 +08:00
Dream Hunter 9ac9cd46b0 feat: cleanup support address and inactive address (#671) 2025-06-18 17:31:15 +08:00
Dream Hunter c694b07380 fix: cron job not run when clean days is 0 (#670) 2025-06-18 13:15:32 +08:00
Dream Hunter 672c4c7273 fix: |UI| user mail page query word bug (#665) 2025-06-09 19:26:18 +08:00
Dream Hunter ee023ac2e9 feat: update changelog (#664) 2025-06-09 19:09:28 +08:00
Dream Hunter cc77bdf36d feat: add ALWAYS_SHOW_ANNOUNCEMENT option (#663) 2025-06-09 19:06:49 +08:00
Dream Hunter dec309a0fd fix: github actions node version (#660) 2025-06-02 11:28:41 +08:00
Dream Hunter 9488543e44 fix: ui admin portal show after fetch user data (#659) 2025-05-20 17:55:33 +08:00
Dream Hunter 50326bcc98 feature: support init db in admin portal (#658) 2025-05-20 17:45:55 +08:00
Dream Hunter 272b624b9b feature: utils import (#652) 2025-05-07 00:54:47 +08:00
Dream Hunter e230801a1c feature: update dependencies (#651) 2025-05-07 00:13:26 +08:00
Zyx-A 07833d5ca9 feature: 基于子域名转发到不同的邮箱中去 (#645) (#647) 2025-04-30 10:41:09 +08:00
Dream Hunter 101a561894 feature: auto refresh user token when token exp in 7 days (#644) 2025-04-26 21:22:26 +08:00
Dream Hunter 327962432a fix: some oauth2 need redirect_uri when get token (#643) 2025-04-26 20:56:47 +08:00
Dream Hunter 6051d49315 feature: version 0.10.0 (#640) 2025-04-24 02:04:40 +08:00
Dream Hunter 95f361743b feature: add /user_api/mails with filter params address and keyword (#639) 2025-04-24 02:01:21 +08:00
Dream Hunter c6afc5d425 feat: support admin api bind address to user (#635) 2025-04-16 13:36:41 +08:00
Dream Hunter 466f53254b feat: docs: update worker doc (#633) 2025-04-16 00:07:12 +08:00
Dream Hunter ce0a10e6de feat: |Admin Portal| optimized UI (#632) 2025-04-12 20:24:11 +08:00
Dream Hunter 26995982af feat: oatuh2 email key support jsonpath (#631) 2025-04-12 19:57:03 +08:00
Dream Hunter 0894ac0dc9 feat: support admin api bind address to user (#630) 2025-04-12 19:49:59 +08:00
Dream Hunter 47e2cb56b4 feat: support deploy worker with UI assets (#627) 2025-04-12 15:37:34 +08:00
Dream Hunter 32767176f0 feat: s3 attachment add delete (#625) 2025-04-07 20:17:56 +08:00
Dream Hunter 31eb6c23d1 feat: admin portal user page add user address manangement (#623) 2025-04-07 19:47:44 +08:00
Dream Hunter 91a859bbcf feat: support cleanDays max 1000 (#622) 2025-04-07 19:24:21 +08:00
Dream Hunter 525f5e2dce feat: support auto login with url query parameter (#606) 2025-03-16 14:20:24 +08:00
Dream Hunter 908fc0cc86 feat: |Doc| use shadow DOM render mail html (#604) 2025-03-08 10:53:45 +08:00
Dream Hunter 97d24b2087 feat: |Doc| add Google ads doc (#598) 2025-02-27 00:58:56 +08:00
Dream Hunter 983300acf4 feat: |UI| add loading for lazy load component (#597) 2025-02-27 00:36:13 +08:00
Dream Hunter 144a792cb2 feat: |UI| change SideMargin size base on gridMaxCols (#596) 2025-02-27 00:14:04 +08:00
Dream Hunter 278f0112d0 feat: |UI| change SideMargin size (#595) 2025-02-27 00:08:06 +08:00
Dream Hunter 764faebf9f feat: update dependencies && version to 0.9.1 (#594) 2025-02-26 23:58:37 +08:00
Dream Hunter d4f0c82e42 feat: update dependencies && version to 0.9.1 (#593) 2025-02-26 23:36:08 +08:00
Dream Hunter cf680e6349 feat: |UI| support google ads (#592) 2025-02-26 23:01:57 +08:00
Dream Hunter c3987d364c feat: |Actions| Tag build add worker-with-wasm-mail-parser.zip (#590) 2025-02-22 18:51:44 +08:00
Dream Hunter 3a542a8391 feat: |Worker| NO_LIMIT_SEND_ROLE support multi role splited by ',' (#588) 2025-02-22 16:58:48 +08:00
Dream Hunter 241e0b7b28 feat: |Worker| multi language add messages (#587) 2025-02-20 01:41:34 +08:00
Dream Hunter b43353ea47 feat: |Worker| multi language add messages (#586) 2025-02-20 01:05:02 +08:00
Dream Hunter 6c334d32f6 feat: |Worker| add var DEFAULT_LANG, zh/en (#585) 2025-02-20 00:42:48 +08:00
Dream Hunter 7889d2edea feat: |Worker| support multi language (#584) 2025-02-20 00:37:39 +08:00
115 changed files with 6523 additions and 4681 deletions
+3
View File
@@ -0,0 +1,3 @@
[flake8]
max-line-length = 180
exclude = .git,__pycache__,build,dist
+9 -1
View File
@@ -21,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
@@ -32,6 +32,14 @@ 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 }}
if [ -n "$use_worker_assets" ]; then
cd frontend/
pnpm install --no-frozen-lockfile
pnpm build:pages
cd ..
fi
export debug_mode=${{ secrets.DEBUG_MODE }} export debug_mode=${{ secrets.DEBUG_MODE }}
export use_mail_wasm_parser=${{ secrets.BACKEND_USE_MAIL_WASM_PARSER }} export use_mail_wasm_parser=${{ secrets.BACKEND_USE_MAIL_WASM_PARSER }}
cd worker/ cd worker/
+1 -1
View File
@@ -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 -1
View File
@@ -21,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
@@ -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
+1 -1
View File
@@ -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 }}
+16 -2
View File
@@ -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
@@ -44,10 +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/frontend.zip frontend/frontend.zip
frontend/telegram-frontend.zip frontend/telegram-frontend.zip
worker/dist/worker.js worker/worker.js
worker/worker-with-wasm-mail-parser.zip
+47 -1
View File
@@ -1,7 +1,53 @@
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 --> <!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
# CHANGE LOG # CHANGE LOG
## main(v0.8.7) ## 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| 修复移动设备日期显示问题 - fix: |UI| 修复移动设备日期显示问题
- feat: |Worker| 支持通过 `SMTP` 发送邮件, 使用 [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md) - feat: |Worker| 支持通过 `SMTP` 发送邮件, 使用 [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md)
+151 -53
View File
@@ -1,89 +1,187 @@
# 使用 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="FeaturedHelloGitHub"/> <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="FeaturedHelloGitHub" 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) **🎉 一个功能完整的临时邮箱服务!**
[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html) - 🆓 **完全免费** - 基于 Cloudflare 免费服务构建,零成本运行
-**高性能** - Rust WASM 邮件解析,响应速度极快
- 🎨 **现代化界面** - 响应式设计,支持多语言,操作简便
[Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/actions/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/) | [![Deploy Backend Production](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml/badge.svg)](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml) ![](https://uptime.aks.awsl.icu/api/badge/10/status) ![](https://uptime.aks.awsl.icu/api/badge/10/uptime) ![](https://uptime.aks.awsl.icu/api/badge/10/ping) ![](https://uptime.aks.awsl.icu/api/badge/10/avg-response) ![](https://uptime.aks.awsl.icu/api/badge/10/cert-exp) ![](https://uptime.aks.awsl.icu/api/badge/10/response) | | [Backend](https://temp-email-api.awsl.uk/) | [![Deploy Backend Production](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml/badge.svg)](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml) ![](https://uptime.aks.awsl.icu/api/badge/10/status) ![](https://uptime.aks.awsl.icu/api/badge/10/uptime) ![](https://uptime.aks.awsl.icu/api/badge/10/ping) ![](https://uptime.aks.awsl.icu/api/badge/10/avg-response) ![](https://uptime.aks.awsl.icu/api/badge/10/cert-exp) ![](https://uptime.aks.awsl.icu/api/badge/10/response) |
| [Frontend](https://mail.awsl.uk/) | [![Deploy Frontend](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml/badge.svg)](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml) ![](https://uptime.aks.awsl.icu/api/badge/12/status) ![](https://uptime.aks.awsl.icu/api/badge/12/uptime) ![](https://uptime.aks.awsl.icu/api/badge/12/ping) ![](https://uptime.aks.awsl.icu/api/badge/12/avg-response) ![](https://uptime.aks.awsl.icu/api/badge/12/cert-exp) ![](https://uptime.aks.awsl.icu/api/badge/12/response) | | [Frontend](https://mail.awsl.uk/) | [![Deploy Frontend](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml/badge.svg)](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml) ![](https://uptime.aks.awsl.icu/api/badge/12/status) ![](https://uptime.aks.awsl.icu/api/badge/12/uptime) ![](https://uptime.aks.awsl.icu/api/badge/12/ping) ![](https://uptime.aks.awsl.icu/api/badge/12/avg-response) ![](https://uptime.aks.awsl.icu/api/badge/12/cert-exp) ![](https://uptime.aks.awsl.icu/api/badge/12/response) |
</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)
+46
View File
@@ -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.
+19 -18
View File
@@ -1,6 +1,6 @@
{ {
"name": "cloudflare_temp_email", "name": "cloudflare_temp_email",
"version": "0.8.7", "version": "1.0.3",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -20,33 +20,34 @@
}, },
"dependencies": { "dependencies": {
"@simplewebauthn/browser": "10.0.0", "@simplewebauthn/browser": "10.0.0",
"@unhead/vue": "^1.11.19", "@unhead/vue": "^1.11.20",
"@vueuse/core": "^12.7.0", "@vueuse/core": "^12.8.2",
"@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.9", "axios": "^1.11.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mail-parser-wasm": "^0.2.1", "mail-parser-wasm": "^0.2.1",
"naive-ui": "^2.41.0", "naive-ui": "^2.42.0",
"postal-mime": "^2.4.3", "postal-mime": "^2.4.4",
"vooks": "^0.2.12", "vooks": "^0.2.12",
"vue": "^3.5.13", "vue": "^3.5.18",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-i18n": "^11.1.1", "vue-i18n": "^11.1.11",
"vue-router": "^4.5.0" "vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@vicons/fa": "^0.13.0", "@vicons/fa": "^0.13.0",
"@vicons/material": "^0.13.0", "@vicons/material": "^0.13.0",
"@vitejs/plugin-vue": "^5.2.1", "@vitejs/plugin-vue": "^5.2.4",
"unplugin-auto-import": "^19.1.0", "unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.1.0", "unplugin-vue-components": "^28.8.0",
"vite": "^6.1.0", "vite": "^6.3.5",
"vite-plugin-pwa": "^0.21.1", "vite-plugin-pwa": "^1.0.2",
"vite-plugin-top-level-await": "^1.5.0", "vite-plugin-top-level-await": "^1.6.0",
"vite-plugin-wasm": "^3.4.1", "vite-plugin-wasm": "^3.5.0",
"workbox-build": "^7.3.0", "workbox-build": "^7.3.0",
"workbox-window": "^7.3.0", "workbox-window": "^7.3.0",
"wrangler": "^3.109.1" "wrangler": "^4.28.1"
} },
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }
+1741 -1779
View File
File diff suppressed because it is too large Load Diff
+31 -5
View File
@@ -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 (
@@ -63,9 +79,14 @@ onMounted(async () => {
<n-spin description="loading..." :show="loading"> <n-spin description="loading..." :show="loading">
<n-notification-provider container-style="margin-top: 60px;"> <n-notification-provider container-style="margin-top: 60px;">
<n-message-provider container-style="margin-top: 20px;"> <n-message-provider container-style="margin-top: 20px;">
<n-grid x-gap="12" :cols="12"> <n-grid x-gap="12" :cols="gridMaxCols">
<n-gi v-if="showSideMargin" span="1"></n-gi> <n-gi v-if="showSideMargin" span="1">
<n-gi :span="!showSideMargin ? 12 : 10"> <div class="side" v-if="showAd">
<ins class="adsbygoogle" style="display:block" :data-ad-client="adClient" :data-ad-slot="adSlot"
data-ad-format="auto" data-full-width-responsive="true"></ins>
</div>
</n-gi>
<n-gi :span="!showSideMargin ? gridMaxCols : (gridMaxCols - 2)">
<div class="main"> <div class="main">
<n-space vertical> <n-space vertical>
<n-layout style="min-height: 80vh;"> <n-layout style="min-height: 80vh;">
@@ -76,7 +97,12 @@ onMounted(async () => {
</n-space> </n-space>
</div> </div>
</n-gi> </n-gi>
<n-gi v-if="showSideMargin" span="1"></n-gi> <n-gi v-if="showSideMargin" span="1">
<div class="side" v-if="showAd">
<ins class="adsbygoogle" style="display:block" :data-ad-client="adClient" :data-ad-slot="adSlot"
data-ad-format="auto" data-full-width-responsive="true"></ins>
</div>
</n-gi>
</n-grid> </n-grid>
<n-back-top /> <n-back-top />
</n-message-provider> </n-message-provider>
+23 -5
View File
@@ -2,6 +2,8 @@ import { useGlobalState } from '../store'
import { h } from 'vue' 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,
@@ -22,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,
@@ -32,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;
@@ -88,7 +89,11 @@ const getOpenSettings = async (message, notification) => {
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;
notification.info({ notification.info({
content: () => { content: () => {
@@ -138,6 +143,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 {
+12 -7
View File
@@ -7,6 +7,7 @@ import { CloudDownloadRound, ReplyFilled, ForwardFilled } from '@vicons/material
import { useIsMobile } from '../utils/composables' import { useIsMobile } from '../utils/composables'
import { processItem, getDownloadEmlUrl } from '../utils/email-parser' import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
import { utcToLocalDate } from '../utils'; import { utcToLocalDate } from '../utils';
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
const message = useMessage() const message = useMessage()
const isMobile = useIsMobile() const isMobile = useIsMobile()
@@ -171,7 +172,7 @@ const refresh = async () => {
} }
}; };
const backFirstPageAndRefresh = async () =>{ const backFirstPageAndRefresh = async () => {
page.value = 1; page.value = 1;
await refresh(); await refresh();
} }
@@ -380,7 +381,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)">
@@ -396,10 +397,14 @@ onBeforeUnmount(() => {
{{ utcToLocalDate(row.created_at, useUTCDate) }} {{ 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>
@@ -460,7 +465,7 @@ onBeforeUnmount(() => {
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message" <iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
style="margin-top: 10px;width: 100%; height: 100%;"> style="margin-top: 10px;width: 100%; height: 100%;">
</iframe> </iframe>
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div> <ShadowHtmlComponent v-else :htmlContent="curMail.message" style="margin-top: 10px;" />
</n-card> </n-card>
<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')">
@@ -498,7 +503,7 @@ onBeforeUnmount(() => {
{{ utcToLocalDate(row.created_at, useUTCDate) }} {{ 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 }}
@@ -560,7 +565,7 @@ onBeforeUnmount(() => {
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message" <iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
style="margin-top: 10px;width: 100%; height: 100%;"> style="margin-top: 10px;width: 100%; height: 100%;">
</iframe> </iframe>
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div> <ShadowHtmlComponent :key="curMail.id" v-else :htmlContent="curMail.message" style="margin-top: 10px;" />
</n-card> </n-card>
</n-drawer-content> </n-drawer-content>
</n-drawer> </n-drawer>
@@ -0,0 +1,75 @@
<template>
<div v-if="useFallback" v-html="htmlContent"></div>
<div v-else ref="shadowHost"></div>
</template>
<script setup>
import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
const props = defineProps({
htmlContent: {
type: String,
required: true,
},
});
const shadowHost = ref(null);
let shadowRoot = null;
const useFallback = ref(false);
/**
* Renders content into Shadow DOM with fallback to v-html
*/
const renderShadowDom = () => {
if (!shadowHost.value && !useFallback.value) return;
try {
// Don't attempt to use Shadow DOM if already in fallback mode
if (useFallback.value) return;
// Initialize Shadow DOM if not already created
if (!shadowRoot && shadowHost.value) {
try {
shadowRoot = shadowHost.value.attachShadow({ mode: 'open' });
} catch (error) {
console.warn('Shadow DOM not supported, falling back to v-html:', error);
useFallback.value = true;
return;
}
}
// Update content if Shadow DOM exists
if (shadowRoot) {
shadowRoot.innerHTML = props.htmlContent;
}
} catch (error) {
console.error('Failed to render Shadow DOM, falling back to v-html:', error);
useFallback.value = true;
}
};
// Initial render when component is mounted
onMounted(() => {
// Check if Shadow DOM is supported in this browser
if (typeof Element.prototype.attachShadow !== 'function') {
console.warn('Shadow DOM is not supported in this browser, using v-html fallback');
useFallback.value = true;
return;
}
renderShadowDom();
});
// Clean up resources when component is unmounted
onBeforeUnmount(() => {
if (shadowRoot) {
shadowRoot.innerHTML = '';
}
shadowRoot = null;
});
// Update Shadow DOM when htmlContent changes
watch(() => props.htmlContent, () => {
renderShadowDom();
}, { flush: 'post' });
</script>
+15
View File
@@ -0,0 +1,15 @@
import { createI18n } from 'vue-i18n'
const i18n = createI18n({
legacy: false, // you must set `false`, to use Composition API
locale: 'zh', // set locale
fallbackLocale: 'en', // set fallback locale
'en': {
messages: {}
},
'zh': {
messages: {}
}
})
export default i18n;
+3 -23
View File
@@ -1,29 +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 { createHead } from '@unhead/vue' import { createHead } from '@unhead/vue'
const i18n = createI18n({ import App from './App.vue'
legacy: false, // you must set `false`, to use Composition API import router from './router'
locale: 'zh', // set locale import i18n from './i18n'
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)
+19 -1
View File
@@ -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
+6 -1
View File
@@ -10,10 +10,12 @@ 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,
@@ -67,7 +69,7 @@ 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);
@@ -92,6 +94,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,
}); });
@@ -139,6 +143,7 @@ export const useGlobalState = createGlobalState(
showAdminPage, showAdminPage,
userOauth2SessionState, userOauth2SessionState,
userOauth2SessionClientID, userOauth2SessionClientID,
useSimpleIndex,
} }
}, },
) )
+16 -2
View File
@@ -18,12 +18,12 @@ 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';
import MailWebhook from './admin/MailWebhook.vue'; import MailWebhook from './admin/MailWebhook.vue';
import WorkerConfig from './admin/WorkerConfig.vue'; import WorkerConfig from './admin/WorkerConfig.vue';
import SendMail from './admin/SendMail.vue';
const { const {
adminAuth, showAdminAuth, adminTab, loading, adminAuth, showAdminAuth, adminTab, loading,
@@ -31,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;
@@ -62,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',
@@ -88,6 +95,7 @@ const { t } = useI18n({
webhookSettings: 'Webhook 设置', webhookSettings: 'Webhook 设置',
statistics: '统计', statistics: '统计',
maintenance: '维护', maintenance: '维护',
database: '数据库',
workerconfig: 'Worker 配置', workerconfig: 'Worker 配置',
appearance: '外观', appearance: '外观',
about: '关于', about: '关于',
@@ -107,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>
@@ -121,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>
@@ -191,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>
+4
View File
@@ -126,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;
} }
}, },
@@ -214,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++;
} }
+59 -41
View File
@@ -11,15 +11,23 @@ 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';
const SendMail = defineAsyncComponent(() => import('./index/SendMail.vue')); import SimpleIndex from './index/SimpleIndex.vue';
const { settings, openSettings, indexTab, globalTabplacement } = useGlobalState()
const { loading, settings, openSettings, indexTab, globalTabplacement, useSimpleIndex } = useGlobalState()
const message = useMessage() const message = useMessage()
const route = useRoute() const route = useRoute()
const SendMail = defineAsyncComponent(() => {
loading.value = true;
return import('./index/SendMail.vue')
.finally(() => loading.value = false);
});
const { t } = useI18n({ const { t } = useI18n({
messages: { messages: {
en: { en: {
@@ -28,6 +36,7 @@ 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',
@@ -39,7 +48,8 @@ const { t } = useI18n({
sendbox: '发件箱', sendbox: '发件箱',
sendmail: '发送邮件', sendmail: '发送邮件',
auto_reply: '自动回复', auto_reply: '自动回复',
accountSettings: '账户设置', accountSettings: '账户',
appearance: '外观',
about: '关于', about: '关于',
s3Attachment: 'S3附件', s3Attachment: 'S3附件',
saveToS3Success: '保存到s3成功', saveToS3Success: '保存到s3成功',
@@ -117,43 +127,51 @@ onMounted(() => {
<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>
<div v-if="showMailIdQuery" style="margin-bottom: 10px;"> <div v-else>
<n-input-group> <AddressBar />
<n-input v-model:value="mailIdQuery" /> <n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
<n-button @click="queryMail" type="primary" tertiary> <n-tab-pane name="mailbox" :tab="t('mailbox')">
{{ t('query') }} <div v-if="showMailIdQuery" style="margin-bottom: 10px;">
</n-button> <n-input-group>
</n-input-group> <n-input v-model:value="mailIdQuery" />
</div> <n-button @click="queryMail" type="primary" tertiary>
<MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled" {{ t('query') }}
:saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail" </n-button>
:fetchMailData="fetchMailData" :deleteMail="deleteMail" /> </n-input-group>
</n-tab-pane> </div>
<n-tab-pane name="sendbox" :tab="t('sendbox')"> <MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled"
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
:deleteMail="deleteSenboxMail" /> :fetchMailData="fetchMailData" :deleteMail="deleteMail" />
</n-tab-pane> </n-tab-pane>
<n-tab-pane name="sendmail" :tab="t('sendmail')"> <n-tab-pane name="sendbox" :tab="t('sendbox')">
<SendMail /> <SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
</n-tab-pane> :deleteMail="deleteSenboxMail" />
<n-tab-pane name="accountSettings" :tab="t('accountSettings')"> </n-tab-pane>
<AccountSettings /> <n-tab-pane name="sendmail" :tab="t('sendmail')">
</n-tab-pane> <SendMail />
<n-tab-pane v-if="openSettings.enableAutoReply" name="auto_reply" :tab="t('auto_reply')"> </n-tab-pane>
<AutoReply /> <n-tab-pane name="accountSettings" :tab="t('accountSettings')">
</n-tab-pane> <AccountSettings />
<n-tab-pane v-if="openSettings.enableWebhook" name="webhook" :tab="t('webhookSettings')"> </n-tab-pane>
<Webhook /> <n-tab-pane name="appearance" :tab="t('appearance')">
</n-tab-pane> <Appearance :showUseSimpleIndex="true" />
<n-tab-pane v-if="openSettings.isS3Enabled" name="s3_attachment" :tab="t('s3Attachment')"> </n-tab-pane>
<Attachment /> <n-tab-pane v-if="openSettings.enableAutoReply" name="auto_reply" :tab="t('auto_reply')">
</n-tab-pane> <AutoReply />
<n-tab-pane v-if="openSettings.enableIndexAbout" name="about" :tab="t('about')"> </n-tab-pane>
<About /> <n-tab-pane v-if="openSettings.enableWebhook" name="webhook" :tab="t('webhookSettings')">
</n-tab-pane> <Webhook />
</n-tabs> </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>
+6
View File
@@ -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>
+8 -6
View File
@@ -27,7 +27,7 @@ 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',
@@ -46,7 +46,7 @@ const { t } = useI18n({
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。', addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
delete: '删除', delete: '删除',
deleteTip: '确定要删除这个邮箱吗?', deleteTip: '确定要删除这个邮箱吗?',
delteAccount: '删除邮箱', deleteAccount: '删除邮箱',
viewMails: '查看邮件', viewMails: '查看邮件',
viewSendBox: '查看发件箱', viewSendBox: '查看发件箱',
itemCount: '总数', itemCount: '总数',
@@ -212,7 +212,8 @@ const columns = [
} }
}, },
{ default: () => t('viewMails') } { default: () => t('viewMails') }
) ),
show: row.mail_count > 0
}, },
{ {
label: () => h(NButton, label: () => h(NButton,
@@ -224,7 +225,8 @@ const columns = [
} }
}, },
{ default: () => t('viewSendBox') } { default: () => t('viewSendBox') }
) ),
show: row.send_count > 0
}, },
{ {
label: () => h(NButton, label: () => h(NButton,
@@ -271,11 +273,11 @@ 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>
@@ -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>
+40 -8
View File
@@ -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,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>
+40 -6
View File
@@ -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>
@@ -224,13 +224,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>
+12 -1
View File
@@ -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, configAutoRefreshInterval, mailboxSplitSize, useIframeShowMail, preferShowTextMail, configAutoRefreshInterval,
globalTabplacement, useSideMargin, useUTCDate 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',
@@ -26,6 +33,7 @@ const { t } = useI18n({
autoRefreshInterval: 'Auto Refresh Interval(Sec)', autoRefreshInterval: 'Auto Refresh Interval(Sec)',
}, },
zh: { zh: {
useSimpleIndex: '使用极简主页',
mailboxSplitSize: '邮箱界面分栏大小', mailboxSplitSize: '邮箱界面分栏大小',
preferShowTextMail: '默认以文本显示邮件', preferShowTextMail: '默认以文本显示邮件',
useIframeShowMail: '使用iframe显示HTML邮件', useIframeShowMail: '使用iframe显示HTML邮件',
@@ -57,6 +65,9 @@ const { t } = useI18n({
60: '60', 120: '120', 180: '180', 240: '240' 60: '60', 120: '120', 180: '180', 240: '240'
}" /> }" />
</n-form-item-row> </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>
+11 -2
View File
@@ -71,6 +71,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: ',
@@ -86,6 +87,7 @@ const { locale, t } = useI18n({
}, },
zh: { zh: {
login: '登录', login: '登录',
loginAndBind: '登录并绑定',
pleaseGetNewEmail: '请"登录"或点击 "注册新邮箱" 按钮来获取一个新的邮箱地址', pleaseGetNewEmail: '请"登录"或点击 "注册新邮箱" 按钮来获取一个新的邮箱地址',
getNewEmail: '创建新邮箱', getNewEmail: '创建新邮箱',
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许: ', getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许: ',
@@ -102,6 +104,13 @@ const { locale, t } = useI18n({
} }
}); });
const loginAndBindTag = computed(() => {
if (userSettings.value.user_email) {
return t('loginAndBind')
}
return t('login')
})
const addressRegex = computed(() => { const addressRegex = computed(() => {
try { try {
if (openSettings.value.addressRegex) { if (openSettings.value.addressRegex) {
@@ -208,7 +217,7 @@ 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> <n-form-item-row :label="t('credential')" required>
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" /> <n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
@@ -217,7 +226,7 @@ onMounted(async () => {
<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="showNewAddressTab" @click="tabValue = 'register'" block secondary strong> <n-button v-if="showNewAddressTab" @click="tabValue = 'register'" block secondary strong>
<template #icon> <template #icon>
+13 -15
View File
@@ -5,7 +5,6 @@ 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 { getRouterPathWithLang } from '../../utils' import { getRouterPathWithLang } from '../../utils'
const { const {
@@ -15,24 +14,24 @@ 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 { 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?",
}, },
zh: { zh: {
logout: '退出登录', logout: '退出登录',
delteAccount: "删除账户", deleteAccount: "删除账户",
showAddressCredential: '查看邮箱地址凭证', showAddressCredential: '查看邮箱地址凭证',
logoutConfirm: '确定要退出登录吗?', logoutConfirm: '确定要退出登录吗?',
delteAccount: "删除账户", deleteAccount: "删除账户",
delteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?", deleteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
} }
} }
}); });
@@ -60,30 +59,29 @@ const deleteAccount = async () => {
<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 @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 @click="showDeleteAccount = true" type="error" secondary block strong>
{{ t('delteAccount') }} {{ 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> </n-button>
</template> </template>
</n-modal> </n-modal>
+16 -1
View File
@@ -32,6 +32,7 @@ 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.',
userLogin: 'User Login', userLogin: 'User Login',
}, },
@@ -43,6 +44,7 @@ const { locale, t } = useI18n({
copied: '已复制', copied: '已复制',
fetchAddressError: '邮箱地址凭证无效或邮箱地址不存在,也可能是网络连接异常,请稍后再尝试。', fetchAddressError: '邮箱地址凭证无效或邮箱地址不存在,也可能是网络连接异常,请稍后再尝试。',
addressCredential: '邮箱地址凭证', addressCredential: '邮箱地址凭证',
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。', addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
userLogin: '用户登录', userLogin: '用户登录',
} }
@@ -73,6 +75,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 +146,18 @@ 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>
<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>
+35
View File
@@ -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')
}
) )
]) ])
} }
+3 -3
View File
@@ -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>
+252
View File
@@ -0,0 +1,252 @@
<script setup>
import { ref, onMounted, computed, watch } 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 { utcToLocalDate } from '../../utils'
import ShadowHtmlComponent from '../../components/ShadowHtmlComponent.vue'
const { jwt, settings, useSimpleIndex, useUTCDate, showAddressCredential } = useGlobalState()
const message = useMessage()
// 邮件数据
const currentPage = ref(1)
const totalCount = ref(0)
const loading = ref(false)
const currentMail = ref(null)
const showAccountSettingsCard = ref(false)
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',
},
zh: {
exitSimpleIndex: '退出极简',
copyAddress: '复制',
addressCopied: '地址复制成功',
refreshMails: '刷新',
noMails: '暂无邮件',
prevPage: '上一页',
nextPage: '下一页',
refreshSuccess: '邮件刷新成功',
mailCount: '{current} / {total} 封邮件',
accountSettings: "账户设置",
addressCredential: '邮箱地址凭证',
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。'
}
}
})
// 复制地址
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 refreshMails = async () => {
currentPage.value = 1
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 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()
})
</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>
</n-card>
<!-- 账户设置卡片 -->
<n-card v-if="showAccountSettingsCard" :bordered="false" embedded closable
@close="showAccountSettingsCard = false" :title="t('accountSettings')">
<AccountSettings />
</n-card>
<n-card :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>
<n-space>
<n-tag type="info">
ID: {{ currentMail.id }}
</n-tag>
<n-tag type="info">
{{ utcToLocalDate(currentMail.created_at, useUTCDate.value) }}
</n-tag>
<n-tag type="info">
FROM: {{ currentMail.source }}
</n-tag>
</n-space>
<div style="margin-top: 16px;">
<ShadowHtmlComponent v-if="currentMail.message" :htmlContent="currentMail.message" />
<pre v-else>{{ currentMail.text }}</pre>
</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>
+32 -20
View File
@@ -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()
@@ -25,7 +27,9 @@ const { locale, t } = useI18n({
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', transferAddress: 'Transfer Address',
targetUserEmail: 'Target User Email', targetUserEmail: 'Target User Email',
transferAddressTip: 'Transfer address to another user will remove the address from your account and transfer it to another user. Are you sure to transfer the address?' transferAddressTip: 'Transfer address to another user will remove the address from your account and transfer it to another user. Are you sure to transfer the address?',
address: 'Address',
create_or_bind: 'Create or Bind',
}, },
zh: { zh: {
success: '成功', success: '成功',
@@ -38,7 +42,9 @@ const { locale, t } = useI18n({
unbindAddressTip: '解绑前请切换到此邮箱地址并保存邮箱地址凭证。', unbindAddressTip: '解绑前请切换到此邮箱地址并保存邮箱地址凭证。',
transferAddress: '转移地址', transferAddress: '转移地址',
targetUserEmail: '目标用户邮箱', targetUserEmail: '目标用户邮箱',
transferAddressTip: '转移地址到其他用户将会从你的账户中移除此地址并转移给其他用户。确定要转移地址吗?' transferAddressTip: '转移地址到其他用户将会从你的账户中移除此地址并转移给其他用户。确定要转移地址吗?',
address: '地址',
create_or_bind: '创建或绑定',
} }
} }
}); });
@@ -111,13 +117,10 @@ const transferAddress = async () => {
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");
@@ -211,20 +214,29 @@ onMounted(async () => {
</script> </script>
<template> <template>
<n-modal v-model:show="showTranferAddress" preset="dialog" :title="t('transferAddress')"> <div>
<span> <n-modal v-model:show="showTranferAddress" preset="dialog" :title="t('transferAddress')">
<p>{{ t("transferAddressTip") }}</p> <span>
<p>{{ t('transferAddress') + ": " + currentAddress }}</p> <p>{{ t("transferAddressTip") }}</p>
<n-input v-model:value="targetUserEmail" :placeholder="t('targetUserEmail')" /> <p>{{ t('transferAddress') + ": " + currentAddress }}</p>
</span> <n-input v-model:value="targetUserEmail" :placeholder="t('targetUserEmail')" />
<template #action> </span>
<n-button :loading="loading" @click="transferAddress" size="small" tertiary type="error"> <template #action>
{{ t('transferAddress') }} <n-button :loading="loading" @click="transferAddress" size="small" tertiary type="error">
</n-button> {{ t('transferAddress') }}
</template> </n-button>
</n-modal> </template>
<div style="overflow: auto;"> </n-modal>
<n-data-table :columns="columns" :data="data" :bordered="false" embedded /> <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>
+89
View File
@@ -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>
+1 -1
View File
@@ -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>
+2 -1
View File
@@ -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) {
+4 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "temp-email-pages", "name": "temp-email-pages",
"version": "0.8.7", "version": "1.0.3",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -11,6 +11,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"wrangler": "^3.109.1" "wrangler": "^4.28.1"
} },
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }
+5 -1
View File
@@ -1,15 +1,19 @@
cd frontend/ cd frontend/
pnpm up pnpm up
pnpm add -D wrangler@latest
cd .. cd ..
cd worker/ cd worker/
pnpm up pnpm up
pnpm add -D wrangler@latest
cd .. cd ..
cd pages/ cd pages/
pnpm up pnpm up
pnpm add -D wrangler@latest
cd .. cd ..
cd vitepress-docs/ cd vitepress-docs/
pnpm up pnpm up --latest
pnpm add -D wrangler@latest
cd .. cd ..
+111 -48
View File
@@ -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
+3 -6
View File
@@ -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(
+4 -4
View File
@@ -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.7.0 Twisted==25.5.0
httpx==0.27.0 httpx==0.28.1
-1
View File
@@ -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: [
+1
View File
@@ -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: {
+3
View File
@@ -28,6 +28,7 @@ export const zh = defineConfig({
}, },
outline: { outline: {
level: 'deep',
label: '页面导航' label: '页面导航'
}, },
@@ -121,6 +122,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
text: '通过 Github Actions 部署', text: '通过 Github Actions 部署',
collapsed: true, collapsed: true,
items: [ items: [
{ text: 'Github Actions 部署准备', link: 'actions/pre-requisite' },
{ text: 'D1 数据库', link: 'actions/d1' }, { text: 'D1 数据库', link: 'actions/d1' },
{ text: 'Github Actions 配置', link: 'actions/github-action' }, { text: 'Github Actions 配置', link: 'actions/github-action' },
{ text: '配置邮件转发', link: 'email-routing.md' }, { text: '配置邮件转发', link: 'email-routing.md' },
@@ -151,6 +153,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
{ 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: '配置其他worker增强', link: 'feature/another-worker-enhanced' },
{ text: '给网页增加 Google Ads', link: 'feature/google-ads.md' },
] ]
}, },
{ {
+5 -1
View File
@@ -74,12 +74,15 @@ compatibility_flags = [ "nodejs_compat" ]
# ] # ]
[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 # MIN_ADDRESS_LEN = 1
# MAX_ADDRESS_LEN = 30 # 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]
@@ -120,7 +123,8 @@ 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 = ""
@@ -1,3 +1,17 @@
# 初始化/更新 D1 数据库 # 初始化/更新 D1 数据库
## 创建数据库
打开 cloudflare 控制台,选择 `Workers & Pages` -> `D1` -> `Create Database`,点击创建数据库
![d1](/ui_install/d1.png)
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库,并获取到数据库的 `名称``数据库 ID`
## 初始化数据库
在部署完成后,在 admin 页面的 `快速设置` -> `数据库` 中,点击 `初始化数据库` 按钮来初始化数据库
## 更新数据库 schema
参考 [命令行更新 d1](/zh/guide/cli/d1) 或者 [用户界面更新 d1](/zh/guide/ui/d1) 参考 [命令行更新 d1](/zh/guide/cli/d1) 或者 [用户界面更新 d1](/zh/guide/ui/d1)
@@ -3,27 +3,56 @@
::: warning 注意 ::: warning 注意
目前只支持 worker 和 pages 的部署。 目前只支持 worker 和 pages 的部署。
有问题请通过 `Github Issues` 反馈,感谢。 有问题请通过 `Github Issues` 反馈,感谢。
`worker.dev` 域名在中国无法访问,请自定义域名
::: :::
## 部署步骤 ## 部署步骤
1. 在 GitHub fork 仓库 ### Fork 仓库并启用 Actions
2. 打开仓库的 `Actions` 页面,找到 `Deploy Backend Production``Deploy Frontend`,点击 `enable workflow` 启用 `workflow` - 在 GitHub fork 本仓库
- 打开仓库的 `Actions` 页面
- 找到 `Deploy Backend` 点击 `enable workflow` 启用 `workflow`
- 如果需要前后端分离部署, 找到`Deploy Frontend` 点击 `enable workflow` 启用 `workflow`
3. 然后在仓库页面 `Settings` -> `Secrets and variables` -> `Actions` -> `Repository secrets`, 添加以下 `secrets`: ### 配置 Secrets
- `CLOUDFLARE_ACCOUNT_ID`: Cloudflare 账户 ID, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#cloudflare-account-id) 然后在仓库页面 `Settings` -> `Secrets and variables` -> `Actions` -> `Repository secrets`, 添加以下 `secrets`:
- `CLOUDFLARE_API_TOKEN`: Cloudflare API Token, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#api-token)
- `BACKEND_TOML`: 后端配置文件,[参考此处](/zh/guide/cli/worker.html#修改-wrangler-toml-配置文件)
- `FRONTEND_ENV`: 前端配置文件,请复制 `frontend/.env.example` 的内容,[并参考此处修改](/zh/guide/cli/pages.html)
- `FRONTEND_NAME`: 你在 Cloudflare Pages 创建的项目名称,可通过 [用户界面](https://temp-mail-docs.awsl.uk/zh/guide/ui/pages.html) 或者 [命令行](https://temp-mail-docs.awsl.uk/zh/guide/cli/pages.html) 创建
- `FRONTEND_BRANCH`: (可选) pages 部署的分支,可不配置,默认 `production`
- `TG_FRONTEND_NAME`: (可选) 你在 Cloudflare Pages 创建的项目名称,同 `FRONTEND_NAME`,如果需要 Telegram Mini App 功能,请填写
- `DEBUG_MODE`: (可选) 是否开启调试模式,配置为 `true` 开启, 默认 worker 部署日志不会输出到 Github Actions 页面,开启后会输出
- `BACKEND_USE_MAIL_WASM_PARSER`: (可选) 是否使用 wasm 解析邮件,配置为 `true` 开启, 功能参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker)
4. 打开仓库的 `Actions` 页面,找到 `Deploy Backend Production``Deploy Frontend`,点击 `Run workflow` 选择分支手动部署 - 公共 `secrets`
| 名称 | 说明 |
| ----------------------- | --------------------------------------------------------------------------------------------------------------- |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare 账户 ID, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#cloudflare-account-id) |
| `CLOUDFLARE_API_TOKEN` | Cloudflare API Token, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#api-token) |
- worker 后端 `secrets`
| 名称 | 说明 |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `BACKEND_TOML` | 后端配置文件,[参考此处](/zh/guide/cli/worker.html#修改-wrangler-toml-配置文件) |
| `DEBUG_MODE` | (可选) 是否开启调试模式,配置为 `true` 开启, 默认 worker 部署日志不会输出到 Github Actions 页面,开启后会输出 |
| `BACKEND_USE_MAIL_WASM_PARSER` | (可选) 是否使用 wasm 解析邮件,配置为 `true` 开启, 功能参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker) |
| `USE_WORKER_ASSETS` | (可选) 部署带有前端资源的 Worker, 配置为 `true` 开启 |
- pages 前端 `secrets`
> [!warning] 注意
> 如果选择部署带有前端资源的 Worker, 则无须配置这些 `secrets`
| 名称 | 说明 |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FRONTEND_ENV` | 前端配置文件,请复制 `frontend/.env.example` 的内容,[并参考此处修改](/zh/guide/cli/pages.html) |
| `FRONTEND_NAME` | 你在 Cloudflare Pages 创建的项目名称,可通过 [用户界面](https://temp-mail-docs.awsl.uk/zh/guide/ui/pages.html) 或者 [命令行](https://temp-mail-docs.awsl.uk/zh/guide/cli/pages.html) 创建 |
| `FRONTEND_BRANCH` | (可选) pages 部署的分支,可不配置,默认 `production` |
| `TG_FRONTEND_NAME` | (可选) 你在 Cloudflare Pages 创建的项目名称,同 `FRONTEND_NAME`,如果需要 Telegram Mini App 功能,请填写 |
### 部署
- 打开仓库的 `Actions` 页面
- 找到 `Deploy Backend` 点击 `Run workflow` 选择分支手动部署
- 如果需要前后端分离部署, 找到 `Deploy Frontend`, 点击 `Run workflow` 选择分支手动部署
## 如何配置自动更新 ## 如何配置自动更新
@@ -0,0 +1,10 @@
# Gihub Actions 部署准备
## GitHub 账户
- 需要一个 GitHub 账户
- 良好的网络环境
## Fork 仓库
- 在 GitHub fork [本仓库](https://github.com/dreamhunter2333/cloudflare_temp_email.git)
+7 -4
View File
@@ -1,8 +1,11 @@
# Cloudflare Pages 前端 # Cloudflare Pages 前端
::: warning > [!warning] 注意
下面种方式选择一种即可 > 下面种方式选择一种即可
:::
## 部署带有前端资源的 Worker
参考 [部署 Worker](/zh/guide/cli/worker#部署带有前端页面的-worker-可选)
## 前后端分离部署 ## 前后端分离部署
@@ -22,7 +25,7 @@ cp .env.example .env.prod
```bash ```bash
pnpm build --emptyOutDir pnpm build --emptyOutDir
# 根据提示创建 pages # 第一次部署会提示创建项目, production 分支请填写 production
pnpm run deploy pnpm run deploy
``` ```
+33 -1
View File
@@ -1,4 +1,7 @@
# Cloudflare workers 后端 # Cloudflare Worker 后端
> [!warning] 注意
> `worker.dev` 域名在中国无法访问,请自定义域名
## 初始化项目 ## 初始化项目
@@ -36,6 +39,12 @@ compatibility_flags = [ "nodejs_compat" ]
# { pattern = "temp-email-api.xxxxx.xyz", custom_domain = true }, # { pattern = "temp-email-api.xxxxx.xyz", custom_domain = true },
# ] # ]
# 如果你想要部署带有前端资源的 worker, 你需要添加 assets 配置
# [assets]
# directory = "../frontend/dist/"
# binding = "ASSETS"
# run_worker_first = true
# 如果你想要使用定时任务清理邮件,取消下面的注释,并修改 cron 表达式 # 如果你想要使用定时任务清理邮件,取消下面的注释,并修改 cron 表达式
# [triggers] # [triggers]
# crons = [ "0 0 * * *" ] # crons = [ "0 0 * * *" ]
@@ -86,6 +95,29 @@ database_id = "xxx" # D1 数据库 ID
# service = "auth-inbox" # service = "auth-inbox"
``` ```
## 部署带有前端页面的 worker(可选)
> [!NOTE]
> 如果不需要 [带有前端页面的 worker],可以跳过此步骤
> 参考之后部署前端文档,可以进行前后端分离部署
确认已构建前端资源到 `frontend/dist` 目录
```bash
cd frontend
pnpm install --no-frozen-lockfile
pnpm build:pages
```
`worker` 目录下的 `wrangler.toml` 文件中添加下面的配置
```toml
[assets]
directory = "../frontend/dist/"
binding = "ASSETS"
run_worker_first = true
```
## Telegram Bot 配置 ## Telegram Bot 配置
> [!NOTE] > [!NOTE]
@@ -1,6 +1,6 @@
# 搭建 SMTP IMAP 代理服务 # 搭建 SMTP IMAP 代理服务
::: warning ::: warning 注意
如果你使用了 `resend`, 可直接使用 `resend``SMTP` 服务,不需要使用此服务 如果你使用了 `resend`, 可直接使用 `resend``SMTP` 服务,不需要使用此服务
::: :::
@@ -0,0 +1,29 @@
# 给网页增加 Google Ads
## 命令行部署
修改 `.env.prod` 文件
增加下列两个变量, 具体的值请参考 [Google AdSense](https://www.google.com/adsense/start/) 的说明
```txt
VITE_GOOGLE_AD_CLIENT=ca-pub-123456
VITE_GOOGLE_AD_SLOT=123456
```
然后执行下列命令, 重新部署 pages 即可.
```bash
pnpm build --emptyOutDir
# 第一次部署会提示创建项目, production 分支请填写 production
pnpm run deploy
```
## GitHub Action 部署
修改 `FRONTEND_ENV`, 增加下列两个变量, 具体的值请参考 [Google AdSense](https://www.google.com/adsense/start/) 的说明, 重新部署 pages 即可.
```txt
VITE_GOOGLE_AD_CLIENT=ca-pub-123456
VITE_GOOGLE_AD_SLOT=123456
```
@@ -1,6 +1,6 @@
# 查看邮件 API # 查看邮件 API
## 通过 HTTP API 查看邮件 ## 通过 邮件 API 查看邮件
这是一个 `python` 的例子,使用 `requests` 库查看邮件。 这是一个 `python` 的例子,使用 `requests` 库查看邮件。
@@ -8,7 +8,7 @@
limit = 10 limit = 10
offset = 0 offset = 0
res = requests.get( res = requests.get(
f"http://localhost:8787/api/mails?limit={limit}&offset={offset}", f"https://<你的worker地址>/api/mails?limit={limit}&offset={offset}",
headers={ headers={
"Authorization": f"Bearer {你的JWT密码}", "Authorization": f"Bearer {你的JWT密码}",
# "x-custom-auth": "<你的网站密码>", # 如果启用了自定义密码 # "x-custom-auth": "<你的网站密码>", # 如果启用了自定义密码
@@ -16,3 +16,51 @@ res = requests.get(
} }
) )
``` ```
## admin 邮件 API
支持 `address` filter 和 `keyword` filter
```python
import requests
url = "https://<你的worker地址>/admin/mails"
querystring = {
"limit":"20",
"offset":"0",
# adress 和 keyword 为可选参数
"address":"xxxx@awsl.uk",
"keyword":"xxxx"
}
headers = {"x-admin-auth": "<你的Admin密码>"}
response = requests.get(url, headers=headers, params=querystring)
print(response.json())
```
## user 邮件 API
支持 `address` filter 和 `keyword` filter
```python
import requests
url = "https://<你的worker地址>/user_api/mails"
querystring = {
"limit":"20",
"offset":"0",
# adress 和 keyword 为可选参数
"address":"xxxx@awsl.uk",
"keyword":"xxxx"
}
headers = {"x-admin-auth": "<你的Admin密码>"}
response = requests.get(url, headers=headers, params=querystring)
print(response.json())
```
@@ -3,7 +3,25 @@
> [!NOTE] > [!NOTE]
> 如果你使用了 webhook 转发,或者 telegram bot 接受邮件,但是邮件内容是乱码,或者无法解析,你对解析的需要更高的要求,可以使用这个功能。 > 如果你使用了 webhook 转发,或者 telegram bot 接受邮件,但是邮件内容是乱码,或者无法解析,你对解析的需要更高的要求,可以使用这个功能。
## 修改代码 ## UI 部署
1. 下载 [worker-with-wasm-mail-parser.zip](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/worker-with-wasm-mail-parser.zip)
2. 回到 `Overview`,找到刚刚创建的 worker,点击 `Edit Code`, 删除原来的文件,上传 `worker.js``wasm` 后缀的文件, 点击 `Deploy`
> [!NOTE]
> 上传需要先点击左侧菜单的 Explorer,
> 在文件列表的窗口里点击鼠标右键,在右键菜单里找到 `Upload`,
> 请参考下面的截图
>
> 参考: [issues156](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/156#issuecomment-2079453822)
![worker2](/ui_install/worker-2.png)
![worker-upload](/ui_install/worker-upload.png)
## CLI 部署
### 修改代码
```bash ```bash
cd worker cd worker
@@ -56,7 +74,7 @@ export const commonParseMail = async (raw_mail: string | undefined | null): Prom
} }
``` ```
## 部署 ### 部署
```bash ```bash
cd worker cd worker
@@ -1,6 +1,6 @@
# OAuth2 第三方登录 # OAuth2 第三方登录
> [!WARNING] > [!WARNING] 注意
> 第三方登录会自动使用用户邮箱注册账号(邮箱相同将视为同一账号) > 第三方登录会自动使用用户邮箱注册账号(邮箱相同将视为同一账号)
> >
> 此账号和注册的账号相同, 也可以通过忘记密码设置密码 > 此账号和注册的账号相同, 也可以通过忘记密码设置密码
+1 -1
View File
@@ -8,7 +8,7 @@
- [通过命令行部署](/zh/guide/cli/pre-requisite) - [通过命令行部署](/zh/guide/cli/pre-requisite)
- [通过用户界面部署](/zh/guide/ui/d1) - [通过用户界面部署](/zh/guide/ui/d1)
- [通过Github Actions 部署](/zh/guide/actions/github-action) - [通过Github Actions 部署](/zh/guide/actions/pre-requisite)
### 也可以参考网友提供的详细的小白教程 ### 也可以参考网友提供的详细的小白教程
+5 -1
View File
@@ -1,6 +1,6 @@
# 初始化/更新 D1 数据库 # 初始化/更新 D1 数据库
## 初始化数据库 ## 创建数据库
打开 cloudflare 控制台,选择 `Workers & Pages` -> `D1` -> `Create Database`,点击创建数据库 打开 cloudflare 控制台,选择 `Workers & Pages` -> `D1` -> `Create Database`,点击创建数据库
@@ -8,6 +8,10 @@
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库 创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
## 初始化数据库
你也可以跳过初始化数据库,在部署完成后,在 admin 页面的 `快速设置` -> `数据库` 中,点击 `初始化数据库` 按钮来初始化数据库
::: warning 注意 ::: warning 注意
下面输入的是 `db/schema.sql` 的内容 下面输入的是 `db/schema.sql` 的内容
::: :::
+3
View File
@@ -54,6 +54,9 @@ const generate = async () => {
- 此处 worker 域名为后端 api 的域名,比如我部署在 `https://temp-email-api.awsl.uk`,则填写 `https://temp-email-api.awsl.uk` - 此处 worker 域名为后端 api 的域名,比如我部署在 `https://temp-email-api.awsl.uk`,则填写 `https://temp-email-api.awsl.uk`
- 如果你的域名是 `https://temp-email-api.xxx.workers.dev`,则填写 `https://temp-email-api.xxx.workers.dev` - 如果你的域名是 `https://temp-email-api.xxx.workers.dev`,则填写 `https://temp-email-api.xxx.workers.dev`
> [!warning] 注意
> `worker.dev` 域名在中国无法访问,请自定义域名
<div :class="$style.container"> <div :class="$style.container">
<input :class="$style.input" type="text" v-model="domain" placeholder="请输入地址"></input> <input :class="$style.input" type="text" v-model="domain" placeholder="请输入地址"></input>
<button :class="$style.button" @click="generate">生成</button> <button :class="$style.button" @click="generate">生成</button>
@@ -1,5 +1,8 @@
# Cloudflare workers 后端 # Cloudflare workers 后端
> [!warning] 注意
> `worker.dev` 域名在中国无法访问,请自定义域名
1. 点击 `Workers & Pages` -> `Overview` -> `Create Application` 1. 点击 `Workers & Pages` -> `Overview` -> `Create Application`
![create worker](/ui_install/worker_home.png) ![create worker](/ui_install/worker_home.png)
+12 -10
View File
@@ -75,7 +75,7 @@
| `ADMIN_USER_ROLE` | 文本 | admin 角色配置, 如果用户角色等于 ADMIN_USER_ROLE 则可以访问 admin 控制台 | `admin` | | `ADMIN_USER_ROLE` | 文本 | admin 角色配置, 如果用户角色等于 ADMIN_USER_ROLE 则可以访问 admin 控制台 | `admin` |
| `USER_ROLES` | JSON | - | 见下方 | | `USER_ROLES` | JSON | - | 见下方 |
| `DISABLE_ANONYMOUS_USER_CREATE_EMAIL` | 文本/JSON | 禁用匿名用户创建邮箱,如果设置为 true,则用户只能在登录后创建邮箱地址 | `true` | | `DISABLE_ANONYMOUS_USER_CREATE_EMAIL` | 文本/JSON | 禁用匿名用户创建邮箱,如果设置为 true,则用户只能在登录后创建邮箱地址 | `true` |
| `NO_LIMIT_SEND_ROLE` | 文本 | 可以无限发送邮件的角色 | `vip` | | `NO_LIMIT_SEND_ROLE` | 文本 | 可以无限发送邮件的角色, 多个角色使用逗号分割 `vip,admin` | `vip` |
> [!NOTE] USER_ROLES 用户角色配置说明 > [!NOTE] USER_ROLES 用户角色配置说明
> >
@@ -88,15 +88,17 @@
## 网页相关变量 ## 网页相关变量
| 变量名 | 类型 | 说明 | 示例 | | 变量名 | 类型 | 说明 | 示例 |
| ------------------------- | ----------- | ------------------------------------------------ | --------------------- | | -------------------------- | ----------- | ------------------------------------------------ | --------------------- |
| `TITLE` | 文本 | 自定义前端页面网站标题,支持 html | `Custom Title` | | `DEFAULT_LANG` | 文本 | Worker 错误信息默认语言, zh/en | `zh` |
| `ANNOUNCEMENT` | 文本 | 自定义前端页面公告,支持 html | `Custom Announcement` | | `TITLE` | 文本 | 自定义前端页面网站标题,支持 html | `Custom Title` |
| `COPYRIGHT` | 文本 | 自定义前端界面页脚文本,支持 html | `Dream Hunter` | | `ANNOUNCEMENT` | 文本 | 自定义前端页面公告,支持 html | `Custom Announcement` |
| `ADMIN_CONTACT` | 文本 | admin 联系方式,可配置任意字符串, 不配置则不显示 | `xxx@gmail.com` | | `ALWAYS_SHOW_ANNOUNCEMENT` | 文本/JSON | 是否总是显示公告(即使无更改), 默认 `false` | `true` |
| `DISABLE_SHOW_GITHUB` | 文本/JSON | 是否显示 GitHub 链接 | `true` | | `COPYRIGHT` | 文本 | 自定义前端界面页脚文本,支持 html | `Dream Hunter` |
| `CF_TURNSTILE_SITE_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` | | `ADMIN_CONTACT` | 文本 | admin 联系方式,可配置任意字符串, 不配置则不显示 | `xxx@gmail.com` |
| `CF_TURNSTILE_SECRET_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` | | `DISABLE_SHOW_GITHUB` | 文本/JSON | 是否显示 GitHub 链接 | `true` |
| `CF_TURNSTILE_SITE_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` |
| `CF_TURNSTILE_SECRET_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` |
## Telegram Bot 相关变量 ## Telegram Bot 相关变量
+6 -5
View File
@@ -1,12 +1,12 @@
{ {
"name": "temp-mail-docs", "name": "temp-mail-docs",
"private": true, "private": true,
"version": "0.8.7", "version": "1.0.3",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@types/node": "^22.13.4", "@types/node": "^24.2.0",
"vitepress": "^1.6.3", "vitepress": "^1.6.4",
"wrangler": "^3.109.1" "wrangler": "^4.28.1"
}, },
"scripts": { "scripts": {
"dev": "vitepress dev docs", "dev": "vitepress dev docs",
@@ -16,5 +16,6 @@
}, },
"dependencies": { "dependencies": {
"jszip": "^3.10.1" "jszip": "^3.10.1"
} },
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }
+833 -863
View File
File diff suppressed because it is too large Load Diff
+14 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "cloudflare_temp_email", "name": "cloudflare_temp_email",
"version": "0.8.7", "version": "1.0.3",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -11,28 +11,31 @@
"build": "wrangler deploy --dry-run --outdir dist --minify" "build": "wrangler deploy --dry-run --outdir dist --minify"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20250214.0", "@cloudflare/workers-types": "^4.20250807.0",
"@eslint/js": "9.18.0", "@eslint/js": "9.18.0",
"@simplewebauthn/types": "10.0.0", "@simplewebauthn/types": "10.0.0",
"@types/node": "^22.17.0",
"eslint": "9.18.0", "eslint": "9.18.0",
"globals": "^15.15.0", "globals": "^15.15.0",
"typescript-eslint": "^8.24.0", "typescript-eslint": "^8.39.0",
"wrangler": "^3.109.1" "wrangler": "^4.28.1"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.749.0", "@aws-sdk/client-s3": "^3.863.0",
"@aws-sdk/s3-request-presigner": "^3.749.0", "@aws-sdk/s3-request-presigner": "^3.863.0",
"@simplewebauthn/server": "10.0.1", "@simplewebauthn/server": "10.0.1",
"hono": "^4.7.1", "hono": "^4.9.0",
"jsonpath-plus": "^10.3.0",
"mimetext": "^3.0.27", "mimetext": "^3.0.27",
"postal-mime": "^2.4.3", "postal-mime": "^2.4.4",
"resend": "^4.1.2", "resend": "^4.8.0",
"telegraf": "4.16.3", "telegraf": "4.16.3",
"worker-mailer": "^1.0.1" "worker-mailer": "^1.1.5"
}, },
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
"telegraf@4.16.3": "patches/telegraf@4.16.3.patch" "telegraf@4.16.3": "patches/telegraf@4.16.3.patch"
} }
} },
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }
+14 -4
View File
@@ -1,5 +1,5 @@
diff --git a/lib/core/network/client.js b/lib/core/network/client.js diff --git a/lib/core/network/client.js b/lib/core/network/client.js
index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d698de5ce 100644 index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..ab82c5391596ee2ed8dbe4128f3ba57a8cc998ca 100644
--- a/lib/core/network/client.js --- a/lib/core/network/client.js
+++ b/lib/core/network/client.js +++ b/lib/core/network/client.js
@@ -1,18 +1,18 @@ @@ -1,18 +1,18 @@
@@ -37,7 +37,17 @@ index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d
const path = __importStar(require("path")); const path = __importStar(require("path"));
const node_fetch_1 = __importDefault(require("node-fetch")); const node_fetch_1 = __importDefault(require("node-fetch"));
const check_1 = require("../helpers/check"); const check_1 = require("../helpers/check");
@@ -61,10 +61,10 @@ const DEFAULT_OPTIONS = { @@ -39,7 +39,8 @@ const multipart_stream_1 = __importDefault(require("./multipart-stream"));
const error_1 = __importDefault(require("./error"));
const url_1 = require("url");
// eslint-disable-next-line @typescript-eslint/no-var-requires
-const debug = require('debug')('telegraf:client');
+// const debug = require('debug')('telegraf:client');
+const debug = (msg) => console.debug(`[telegraf:client] ${msg}`);
const { isStream } = multipart_stream_1.default;
const WEBHOOK_REPLY_METHOD_ALLOWLIST = new Set([
'answerCallbackQuery',
@@ -61,10 +62,10 @@ const DEFAULT_OPTIONS = {
apiRoot: 'https://api.telegram.org', apiRoot: 'https://api.telegram.org',
apiMode: 'bot', apiMode: 'bot',
webhookReply: true, webhookReply: true,
@@ -52,7 +62,7 @@ index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d
attachmentAgent: undefined, attachmentAgent: undefined,
testEnv: false, testEnv: false,
}; };
@@ -112,9 +112,9 @@ async function buildFormDataConfig(payload, agent) { @@ -112,9 +113,9 @@ async function buildFormDataConfig(payload, agent) {
} }
const boundary = crypto.randomBytes(32).toString('hex'); const boundary = crypto.randomBytes(32).toString('hex');
const formData = new multipart_stream_1.default(boundary); const formData = new multipart_stream_1.default(boundary);
@@ -65,7 +75,7 @@ index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d
return { return {
method: 'POST', method: 'POST',
compress: true, compress: true,
@@ -205,14 +205,15 @@ async function attachFormMedia(form, media, id, agent) { @@ -205,14 +206,15 @@ async function attachFormMedia(form, media, id, agent) {
if ('source' in media && media.source) { if ('source' in media && media.source) {
let mediaSource = media.source; let mediaSource = media.source;
if (typeof media.source === 'string') { if (typeof media.source === 'string') {
+1237 -1417
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
import { Context } from "hono";
import { handleListQuery } from "../common";
export default {
getMails: async (c: Context<HonoCustomType>) => {
const { address, limit, offset, keyword } = c.req.query();
const addressQuery = address ? `address = ?` : "";
const addressParams = address ? [address] : [];
const keywordQuery = keyword ? `raw like ?` : "";
const keywordParams = keyword ? [`%${keyword}%`] : [];
const filterQuerys = [addressQuery, keywordQuery].filter((item) => item).join(" and ");
const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : "";
const filterParams = [...addressParams, ...keywordParams]
return await handleListQuery(c,
`SELECT * FROM raw_mails ${finalQuery}`,
`SELECT count(*) as count FROM raw_mails ${finalQuery}`,
filterParams, limit, offset
);
},
getUnknowMails: async (c: Context<HonoCustomType>) => {
const { limit, offset } = c.req.query();
return await handleListQuery(c,
`SELECT * FROM raw_mails where address NOT IN (select name from address) `,
`SELECT count(*) as count FROM raw_mails`
+ ` where address NOT IN (select name from address) `,
[], limit, offset
);
},
deleteMail: async (c: Context<HonoCustomType>) => {
const { id } = c.req.param();
const { success } = await c.env.DB.prepare(
`DELETE FROM raw_mails WHERE id = ? `
).bind(id).run();
return c.json({
success: success
})
}
}
+26 -4
View File
@@ -4,7 +4,8 @@ import { CONSTANTS } from '../constants';
import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils'; import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils';
import { UserSettings, GeoData, UserInfo } from "../models"; import { UserSettings, GeoData, UserInfo } from "../models";
import { handleListQuery } from '../common' import { handleListQuery } from '../common'
import { HonoCustomType } from '../types'; import UserBindAddressModule from '../user_api/bind_address';
import i18n from '../i18n';
export default { export default {
getSetting: async (c: Context<HonoCustomType>) => { getSetting: async (c: Context<HonoCustomType>) => {
@@ -89,7 +90,8 @@ export default {
}, },
deleteUser: async (c: Context<HonoCustomType>) => { deleteUser: async (c: Context<HonoCustomType>) => {
const { user_id } = c.req.param(); const { user_id } = c.req.param();
if (!user_id) return c.text("Invalid user_id", 400); const msgs = i18n.getMessagesbyContext(c);
if (!user_id) return c.text(msgs.UserNotFoundMsg, 400);
const { success } = await c.env.DB.prepare( const { success } = await c.env.DB.prepare(
`DELETE FROM users WHERE id = ?` `DELETE FROM users WHERE id = ?`
).bind(user_id).run(); ).bind(user_id).run();
@@ -104,7 +106,8 @@ export default {
resetPassword: async (c: Context<HonoCustomType>) => { resetPassword: async (c: Context<HonoCustomType>) => {
const { user_id } = c.req.param(); const { user_id } = c.req.param();
const { password } = await c.req.json(); const { password } = await c.req.json();
if (!user_id) return c.text("Invalid user_id", 400); const msgs = i18n.getMessagesbyContext(c);
if (!user_id) return c.text(msgs.UserNotFoundMsg, 400);
try { try {
checkUserPassword(password); checkUserPassword(password);
const { success } = await c.env.DB.prepare( const { success } = await c.env.DB.prepare(
@@ -143,5 +146,24 @@ export default {
return c.text("Failed to update user roles", 500) return c.text("Failed to update user roles", 500)
} }
return c.json({ success: true }) return c.json({ success: true })
} },
bindAddress: async (c: Context<HonoCustomType>) => {
const {
user_email, address, user_id, address_id
} = await c.req.json();
const db_user_id = user_id ?? await c.env.DB.prepare(
`SELECT id FROM users WHERE user_email = ?`
).bind(user_email).first<number | undefined | null>("id");
const db_address_id = address_id ?? await c.env.DB.prepare(
`SELECT id FROM address WHERE name = ?`
).bind(address).first<number | undefined | null>("id");
return await UserBindAddressModule.bindByID(c, db_user_id, db_address_id);
},
getBindedAddresses: async (c: Context<HonoCustomType>) => {
const { user_id } = c.req.param();
const results = await UserBindAddressModule.getBindedAddressesById(c, user_id);
return c.json({
results: results,
});
},
} }
+2 -5
View File
@@ -4,7 +4,6 @@ import { cleanup } from '../common';
import { CONSTANTS } from '../constants'; import { CONSTANTS } from '../constants';
import { getJsonSetting, saveSetting } from '../utils'; import { getJsonSetting, saveSetting } from '../utils';
import { CleanupSettings } from '../models'; import { CleanupSettings } from '../models';
import { HonoCustomType } from '../types';
export default { export default {
cleanup: async (c: Context<HonoCustomType>) => { cleanup: async (c: Context<HonoCustomType>) => {
@@ -18,13 +17,11 @@ export default {
return c.json({ success: true }) return c.json({ success: true })
}, },
getCleanup: async (c: Context<HonoCustomType>) => { getCleanup: async (c: Context<HonoCustomType>) => {
const value = await getJsonSetting(c, CONSTANTS.AUTO_CLEANUP_KEY); const cleanupSetting = await getJsonSetting<CleanupSettings>(c, CONSTANTS.AUTO_CLEANUP_KEY);
const cleanupSetting = new CleanupSettings(value);
return c.json(cleanupSetting) return c.json(cleanupSetting)
}, },
saveCleanup: async (c: Context<HonoCustomType>) => { saveCleanup: async (c: Context<HonoCustomType>) => {
const value = await c.req.json(); const cleanupSetting = await c.req.json<CleanupSettings>();
const cleanupSetting = new CleanupSettings(value);
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting)); await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting));
return c.json({ success: true }) return c.json({ success: true })
} }
+156
View File
@@ -0,0 +1,156 @@
import { Context } from "hono";
import { CONSTANTS } from "../constants";
import utils from "../utils";
const DB_INIT_QUERIES = `
CREATE TABLE IF NOT EXISTS raw_mails (
id INTEGER PRIMARY KEY,
message_id TEXT,
source TEXT,
address TEXT,
raw TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
CREATE TABLE IF NOT EXISTS address (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
CREATE TABLE IF NOT EXISTS auto_reply_mails (
id INTEGER PRIMARY KEY,
source_prefix TEXT,
name TEXT,
address TEXT UNIQUE,
subject TEXT,
message TEXT,
enabled INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_auto_reply_mails_address ON auto_reply_mails(address);
CREATE TABLE IF NOT EXISTS address_sender (
id INTEGER PRIMARY KEY,
address TEXT UNIQUE,
balance INTEGER DEFAULT 0,
enabled INTEGER DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_address_sender_address ON address_sender(address);
CREATE TABLE IF NOT EXISTS sendbox (
id INTEGER PRIMARY KEY,
address TEXT,
raw TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
user_email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
user_info TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_user_email ON users(user_email);
CREATE TABLE IF NOT EXISTS users_address (
id INTEGER PRIMARY KEY,
user_id INTEGER,
address_id INTEGER UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id);
CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id);
CREATE TABLE IF NOT EXISTS user_roles (
id INTEGER PRIMARY KEY,
user_id INTEGER UNIQUE NOT NULL,
role_text TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
CREATE TABLE IF NOT EXISTS user_passkeys (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
passkey_name TEXT NOT NULL,
passkey_id TEXT NOT NULL,
passkey TEXT NOT NULL,
counter INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_user_passkeys_user_id ON user_passkeys(user_id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_passkeys_user_id_passkey_id ON user_passkeys(user_id, passkey_id);
`
export default {
initialize: async (c: Context<HonoCustomType>) => {
// remove all \r and \n characters from the query string
// split by ; and join with a ;\n
const query = DB_INIT_QUERIES.replace(/[\r\n]/g, "")
.split(";")
.map((query) => query.trim())
.join(";\n");
await c.env.DB.exec(query);
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
if (version) {
return c.json({ message: "Database already initialized" });
}
await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
return c.json({ message: "Database initialized" });
},
migrate: async (c: Context<HonoCustomType>) => {
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
if (version != CONSTANTS.DB_VERSION) {
// TODO: Perform migration logic here
// Update the version in the settings table
await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
return c.json({
success: true,
message: "Database migrated"
});
}
return c.json({
success: true,
message: "Database does not need migration"
});
},
getVersion: async (c: Context<HonoCustomType>) => {
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
return c.json({
need_initialization: !version,
need_migration: version && version != CONSTANTS.DB_VERSION,
current_db_version: version,
code_db_version: CONSTANTS.DB_VERSION
});
},
}
+17 -50
View File
@@ -1,7 +1,7 @@
import { Hono } from 'hono' import { Hono } from 'hono'
import { Jwt } from 'hono/utils/jwt' import { Jwt } from 'hono/utils/jwt'
import { HonoCustomType } from '../types' import i18n from '../i18n'
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles } from '../utils' import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles } from '../utils'
import { newAddress, handleListQuery } from '../common' import { newAddress, handleListQuery } from '../common'
import { CONSTANTS } from '../constants' import { CONSTANTS } from '../constants'
@@ -11,7 +11,9 @@ import webhook_settings from './webhook_settings'
import mail_webhook_settings from './mail_webhook_settings' import mail_webhook_settings from './mail_webhook_settings'
import oauth2_settings from './oauth2_settings' import oauth2_settings from './oauth2_settings'
import worker_config from './worker_config' import worker_config from './worker_config'
import admin_mail_api from './admin_mail_api'
import { sendMailbyAdmin } from './send_mail' import { sendMailbyAdmin } from './send_mail'
import db_api from './db_api'
export const api = new Hono<HonoCustomType>() export const api = new Hono<HonoCustomType>()
@@ -40,6 +42,8 @@ api.get('/admin/address', async (c) => {
api.post('/admin/new_address', async (c) => { api.post('/admin/new_address', async (c) => {
const { name, domain, enablePrefix } = await c.req.json(); const { name, domain, enablePrefix } = await c.req.json();
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
if (!name) { if (!name) {
return c.text("Please provide a name", 400) return c.text("Please provide a name", 400)
} }
@@ -53,7 +57,7 @@ api.post('/admin/new_address', async (c) => {
}); });
return c.json(res); return c.json(res);
} catch (e) { } catch (e) {
return c.text(`Failed create address: ${(e as Error).message}`, 400) return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400)
} }
}) })
@@ -98,54 +102,10 @@ api.get('/admin/show_password/:id', async (c) => {
}) })
}) })
api.get('/admin/mails', async (c) => { // mail api
const { address, limit, offset, keyword } = c.req.query(); api.get('/admin/mails', admin_mail_api.getMails);
if (address && keyword) { api.get('/admin/mails_unknow', admin_mail_api.getUnknowMails);
return await handleListQuery(c, api.delete('/admin/mails/:id', admin_mail_api.deleteMail)
`SELECT * FROM raw_mails where address = ? and raw like ? `,
`SELECT count(*) as count FROM raw_mails where address = ? and raw like ? `,
[address, `%${keyword}%`], limit, offset
);
} else if (keyword) {
return await handleListQuery(c,
`SELECT * FROM raw_mails where raw like ? `,
`SELECT count(*) as count FROM raw_mails where raw like ? `,
[`%${keyword}%`], limit, offset
);
} else if (address) {
return await handleListQuery(c,
`SELECT * FROM raw_mails where address = ? `,
`SELECT count(*) as count FROM raw_mails where address = ? `,
[address], limit, offset
);
} else {
return await handleListQuery(c,
`SELECT * FROM raw_mails `,
`SELECT count(*) as count FROM raw_mails `,
[], limit, offset
);
}
});
api.get('/admin/mails_unknow', async (c) => {
const { limit, offset } = c.req.query();
return await handleListQuery(c,
`SELECT * FROM raw_mails where address NOT IN (select name from address) `,
`SELECT count(*) as count FROM raw_mails`
+ ` where address NOT IN (select name from address) `,
[], limit, offset
);
});
api.delete('/admin/mails/:id', async (c) => {
const { id } = c.req.param();
const { success } = await c.env.DB.prepare(
`DELETE FROM raw_mails WHERE id = ? `
).bind(id).run();
return c.json({
success: success
})
})
api.get('/admin/address_sender', async (c) => { api.get('/admin/address_sender', async (c) => {
const { address, limit, offset } = c.req.query(); const { address, limit, offset } = c.req.query();
@@ -324,6 +284,8 @@ api.post('/admin/users', admin_user_api.createUser)
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword) api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c))) api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c)))
api.post('/admin/user_roles', admin_user_api.updateUserRoles) api.post('/admin/user_roles', admin_user_api.updateUserRoles)
api.get('/admin/users/bind_address/:user_id', admin_user_api.getBindedAddresses)
api.post('/admin/users/bind_address', admin_user_api.bindAddress)
// user oauth2 settings // user oauth2 settings
api.get('/admin/user_oauth2_settings', oauth2_settings.getUserOauth2Settings) api.get('/admin/user_oauth2_settings', oauth2_settings.getUserOauth2Settings)
@@ -343,3 +305,8 @@ api.get("/admin/worker/configs", worker_config.getConfig);
// send mail by admin // send mail by admin
api.post("/admin/send_mail", sendMailbyAdmin); api.post("/admin/send_mail", sendMailbyAdmin);
// db api
api.get('admin/db_version', db_api.getVersion);
api.post('admin/db_initialize', db_api.initialize);
api.post('admin/db_migration', db_api.migrate);
@@ -1,5 +1,4 @@
import { Context } from "hono"; import { Context } from "hono";
import { HonoCustomType, ParsedEmailContext } from "../types";
import { CONSTANTS } from "../constants"; import { CONSTANTS } from "../constants";
import { WebhookSettings } from "../models"; import { WebhookSettings } from "../models";
import { commonParseMail, sendWebhook } from "../common"; import { commonParseMail, sendWebhook } from "../common";
-1
View File
@@ -2,7 +2,6 @@ import { Context } from 'hono';
import { CONSTANTS } from '../constants'; import { CONSTANTS } from '../constants';
import { UserOauth2Settings } from "../models"; import { UserOauth2Settings } from "../models";
import { HonoCustomType } from '../types';
import { getJsonSetting, saveSetting } from '../utils'; import { getJsonSetting, saveSetting } from '../utils';
async function getUserOauth2Settings(c: Context<HonoCustomType>): Promise<Response> { async function getUserOauth2Settings(c: Context<HonoCustomType>): Promise<Response> {
-1
View File
@@ -1,5 +1,4 @@
import { Context } from "hono"; import { Context } from "hono";
import { HonoCustomType } from "../types";
import { sendMail } from "../mails_api/send_mail_api"; import { sendMail } from "../mails_api/send_mail_api";
export const sendMailbyAdmin = async (c: Context<HonoCustomType>) => { export const sendMailbyAdmin = async (c: Context<HonoCustomType>) => {
-1
View File
@@ -1,5 +1,4 @@
import { Context } from "hono"; import { Context } from "hono";
import { HonoCustomType } from "../types";
import { CONSTANTS } from "../constants"; import { CONSTANTS } from "../constants";
import { AdminWebhookSettings } from "../models"; import { AdminWebhookSettings } from "../models";
+35 -33
View File
@@ -1,56 +1,58 @@
import { Context } from 'hono'; import { Context } from 'hono';
import { HonoCustomType } from '../types'; import utils from '../utils';
import { getAdminPasswords, getBooleanValue, getDefaultDomains, getDomains, getIntValue, getPasswords, getStringArray, getStringValue, getUserRoles, getAnotherWorkerList } from '../utils';
import { CONSTANTS } from '../constants'; import { CONSTANTS } from '../constants';
import { isS3Enabled } from '../mails_api/s3_attachment'; import { isS3Enabled } from '../mails_api/s3_attachment';
export default { export default {
getConfig: async (c: Context<HonoCustomType>) => { getConfig: async (c: Context<HonoCustomType>) => {
return c.json({ return c.json({
"DEFAULT_LANG": c.env.DEFAULT_LANG,
"TITLE": c.env.TITLE, "TITLE": c.env.TITLE,
"HAS_PASSWORD": getPasswords(c).length, "HAS_PASSWORD": utils.getPasswords(c).length,
"HAS_ADMIN_PASSWORDS": getAdminPasswords(c).length, "HAS_ADMIN_PASSWORDS": utils.getAdminPasswords(c).length,
"ANNOUNCEMENT": getStringValue(c.env.ANNOUNCEMENT), "ANNOUNCEMENT": utils.getStringValue(c.env.ANNOUNCEMENT),
"ALWAYS_SHOW_ANNOUNCEMENT": utils.getBooleanValue(c.env.ALWAYS_SHOW_ANNOUNCEMENT),
"PREFIX": getStringValue(c.env.PREFIX), "PREFIX": utils.getStringValue(c.env.PREFIX),
"ADDRESS_CHECK_REGEX": getStringValue(c.env.ADDRESS_CHECK_REGEX), "ADDRESS_CHECK_REGEX": utils.getStringValue(c.env.ADDRESS_CHECK_REGEX),
"ADDRESS_REGEX": getStringValue(c.env.ADDRESS_REGEX), "ADDRESS_REGEX": utils.getStringValue(c.env.ADDRESS_REGEX),
"MIN_ADDRESS_LEN": getIntValue(c.env.MIN_ADDRESS_LEN, 1), "MIN_ADDRESS_LEN": utils.getIntValue(c.env.MIN_ADDRESS_LEN, 1),
"MAX_ADDRESS_LEN": getIntValue(c.env.MAX_ADDRESS_LEN, 30), "MAX_ADDRESS_LEN": utils.getIntValue(c.env.MAX_ADDRESS_LEN, 30),
"FORWARD_ADDRESS_LIST": getStringArray(c.env.FORWARD_ADDRESS_LIST), "FORWARD_ADDRESS_LIST": utils.getStringArray(c.env.FORWARD_ADDRESS_LIST),
"DEFAULT_DOMAINS": getDefaultDomains(c), "SUBDOMAIN_FORWARD_ADDRESS_LIST": utils.getJsonObjectValue<SubdomainForwardAddressList[]>(c.env.SUBDOMAIN_FORWARD_ADDRESS_LIST),
"DOMAINS": getDomains(c), "DEFAULT_DOMAINS": utils.getDefaultDomains(c),
"DOMAIN_LABELS": getStringArray(c.env.DOMAIN_LABELS), "DOMAINS": utils.getDomains(c),
"DOMAIN_LABELS": utils.getStringArray(c.env.DOMAIN_LABELS),
"HAS_JWT_SECRET": !!getStringValue(c.env.JWT_SECRET), "HAS_JWT_SECRET": !!utils.getStringValue(c.env.JWT_SECRET),
"ADMIN_USER_ROLE": getStringValue(c.env.ADMIN_USER_ROLE), "ADMIN_USER_ROLE": utils.getStringValue(c.env.ADMIN_USER_ROLE),
"USER_DEFAULT_ROLE": getStringValue(c.env.USER_DEFAULT_ROLE), "USER_DEFAULT_ROLE": utils.getStringValue(c.env.USER_DEFAULT_ROLE),
"USER_ROLES": getUserRoles(c), "USER_ROLES": utils.getUserRoles(c),
"NO_LIMIT_SEND_ROLE": getStringValue(c.env.NO_LIMIT_SEND_ROLE), "NO_LIMIT_SEND_ROLE": utils.getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE),
"ADMIN_CONTACT": c.env.ADMIN_CONTACT, "ADMIN_CONTACT": c.env.ADMIN_CONTACT,
"ENABLE_USER_CREATE_EMAIL": getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL), "ENABLE_USER_CREATE_EMAIL": utils.getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
"DISABLE_ANONYMOUS_USER_CREATE_EMAIL": getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL), "DISABLE_ANONYMOUS_USER_CREATE_EMAIL": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
"ENABLE_USER_DELETE_EMAIL": getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL), "ENABLE_USER_DELETE_EMAIL": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
"ENABLE_AUTO_REPLY": getBooleanValue(c.env.ENABLE_AUTO_REPLY), "ENABLE_AUTO_REPLY": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY),
"COPYRIGHT": c.env.COPYRIGHT, "COPYRIGHT": c.env.COPYRIGHT,
"ENABLE_WEBHOOK": getBooleanValue(c.env.ENABLE_WEBHOOK), "ENABLE_WEBHOOK": utils.getBooleanValue(c.env.ENABLE_WEBHOOK),
"S3_ENABLED": isS3Enabled(c), "S3_ENABLED": isS3Enabled(c),
"VERSION": CONSTANTS.VERSION, "VERSION": CONSTANTS.VERSION,
"DISABLE_SHOW_GITHUB": !getBooleanValue(c.env.DISABLE_SHOW_GITHUB), "DISABLE_SHOW_GITHUB": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
"DISABLE_ADMIN_PASSWORD_CHECK": getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK), "DISABLE_ADMIN_PASSWORD_CHECK": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK),
"ENABLE_CHECK_JUNK_MAIL": getBooleanValue(c.env.ENABLE_CHECK_JUNK_MAIL), "ENABLE_CHECK_JUNK_MAIL": utils.getBooleanValue(c.env.ENABLE_CHECK_JUNK_MAIL),
"JUNK_MAIL_CHECK_LIST": getStringArray(c.env.JUNK_MAIL_CHECK_LIST), "JUNK_MAIL_CHECK_LIST": utils.getStringArray(c.env.JUNK_MAIL_CHECK_LIST),
"JUNK_MAIL_FORCE_PASS_LIST": getStringArray(c.env.JUNK_MAIL_FORCE_PASS_LIST), "JUNK_MAIL_FORCE_PASS_LIST": utils.getStringArray(c.env.JUNK_MAIL_FORCE_PASS_LIST),
"REMOVE_EXCEED_SIZE_ATTACHMENT": getBooleanValue(c.env.REMOVE_EXCEED_SIZE_ATTACHMENT), "REMOVE_EXCEED_SIZE_ATTACHMENT": utils.getBooleanValue(c.env.REMOVE_EXCEED_SIZE_ATTACHMENT),
"REMOVE_ALL_ATTACHMENT": getBooleanValue(c.env.REMOVE_ALL_ATTACHMENT), "REMOVE_ALL_ATTACHMENT": utils.getBooleanValue(c.env.REMOVE_ALL_ATTACHMENT),
"ENABLE_ANOTHER_WORKER": getBooleanValue(c.env.ENABLE_ANOTHER_WORKER), "ENABLE_ANOTHER_WORKER": utils.getBooleanValue(c.env.ENABLE_ANOTHER_WORKER),
"ANOTHER_WORKER_LIST": getAnotherWorkerList(c), "ANOTHER_WORKER_LIST": utils.getAnotherWorkerList(c),
}) })
} }
} }
+19 -19
View File
@@ -1,8 +1,7 @@
import { Hono } from 'hono' import { Hono } from 'hono'
import { getDomains, getPasswords, getBooleanValue, getIntValue, getStringArray, getDefaultDomains, getStringValue } from './utils'; import utils from './utils';
import { CONSTANTS } from './constants'; import { CONSTANTS } from './constants';
import { HonoCustomType } from './types';
import { isS3Enabled } from './mails_api/s3_attachment'; import { isS3Enabled } from './mails_api/s3_attachment';
const api = new Hono<HonoCustomType> const api = new Hono<HonoCustomType>
@@ -10,35 +9,36 @@ const api = new Hono<HonoCustomType>
api.get('/open_api/settings', async (c) => { api.get('/open_api/settings', async (c) => {
// check header x-custom-auth // check header x-custom-auth
let needAuth = false; let needAuth = false;
const passwords = getPasswords(c); const passwords = utils.getPasswords(c);
if (passwords && passwords.length > 0) { if (passwords && passwords.length > 0) {
const auth = c.req.raw.headers.get("x-custom-auth"); const auth = c.req.raw.headers.get("x-custom-auth");
needAuth = !auth || !passwords.includes(auth); needAuth = !auth || !passwords.includes(auth);
} }
return c.json({ return c.json({
"title": c.env.TITLE, "title": c.env.TITLE,
"announcement": getStringValue(c.env.ANNOUNCEMENT), "announcement": utils.getStringValue(c.env.ANNOUNCEMENT),
"prefix": getStringValue(c.env.PREFIX), "alwaysShowAnnouncement": utils.getBooleanValue(c.env.ALWAYS_SHOW_ANNOUNCEMENT),
"addressRegex": getStringValue(c.env.ADDRESS_REGEX), "prefix": utils.getStringValue(c.env.PREFIX),
"minAddressLen": getIntValue(c.env.MIN_ADDRESS_LEN, 1), "addressRegex": utils.getStringValue(c.env.ADDRESS_REGEX),
"maxAddressLen": getIntValue(c.env.MAX_ADDRESS_LEN, 30), "minAddressLen": utils.getIntValue(c.env.MIN_ADDRESS_LEN, 1),
"defaultDomains": getDefaultDomains(c), "maxAddressLen": utils.getIntValue(c.env.MAX_ADDRESS_LEN, 30),
"domains": getDomains(c), "defaultDomains": utils.getDefaultDomains(c),
"domainLabels": getStringArray(c.env.DOMAIN_LABELS), "domains": utils.getDomains(c),
"domainLabels": utils.getStringArray(c.env.DOMAIN_LABELS),
"needAuth": needAuth, "needAuth": needAuth,
"adminContact": c.env.ADMIN_CONTACT, "adminContact": c.env.ADMIN_CONTACT,
"enableUserCreateEmail": getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL), "enableUserCreateEmail": utils.getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
"disableAnonymousUserCreateEmail": getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL), "disableAnonymousUserCreateEmail": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
"enableUserDeleteEmail": getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL), "enableUserDeleteEmail": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
"enableAutoReply": getBooleanValue(c.env.ENABLE_AUTO_REPLY), "enableAutoReply": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY),
"enableIndexAbout": getBooleanValue(c.env.ENABLE_INDEX_ABOUT), "enableIndexAbout": utils.getBooleanValue(c.env.ENABLE_INDEX_ABOUT),
"copyright": c.env.COPYRIGHT, "copyright": c.env.COPYRIGHT,
"cfTurnstileSiteKey": c.env.CF_TURNSTILE_SITE_KEY, "cfTurnstileSiteKey": c.env.CF_TURNSTILE_SITE_KEY,
"enableWebhook": getBooleanValue(c.env.ENABLE_WEBHOOK), "enableWebhook": utils.getBooleanValue(c.env.ENABLE_WEBHOOK),
"isS3Enabled": isS3Enabled(c), "isS3Enabled": isS3Enabled(c),
"version": CONSTANTS.VERSION, "version": CONSTANTS.VERSION,
"showGithub": !getBooleanValue(c.env.DISABLE_SHOW_GITHUB), "showGithub": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
"disableAdminPasswordCheck": getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK) "disableAdminPasswordCheck": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK)
}); });
}) })
+76 -10
View File
@@ -2,7 +2,6 @@ import { Context } from 'hono';
import { Jwt } from 'hono/utils/jwt' import { Jwt } from 'hono/utils/jwt'
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList } from './utils'; import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList } from './utils';
import { HonoCustomType, UserRole, AnotherWorker, RPCEmailMessage, ParsedEmailContext } from './types';
import { unbindTelegramByAddress } from './telegram_api/common'; import { unbindTelegramByAddress } from './telegram_api/common';
import { CONSTANTS } from './constants'; import { CONSTANTS } from './constants';
import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models'; import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
@@ -41,6 +40,23 @@ const getNameRegex = (c: Context<HonoCustomType>): RegExp => {
return DEFAULT_NAME_REGEX; return DEFAULT_NAME_REGEX;
} }
export async function updateAddressUpdatedAt(
c: Context<HonoCustomType>,
address: string | undefined | null
): Promise<void> {
if (!address) {
return;
}
// update address updated_at
try {
await c.env.DB.prepare(
`UPDATE address SET updated_at = datetime('now') where name = ?`
).bind(address).run();
} catch (e) {
console.warn("Failed to update address updated_at", e);
}
}
export const newAddress = async ( export const newAddress = async (
c: Context<HonoCustomType>, c: Context<HonoCustomType>,
{ {
@@ -92,9 +108,9 @@ export const newAddress = async (
} }
// check domain // check domain
const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c); const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c);
// if domain is not set, use the first domain // if domain is not set, use the random domain
if (!domain && allowDomains.length > 0) { if (!domain && allowDomains.length > 0) {
domain = allowDomains[0]; domain = allowDomains[Math.floor(Math.random() * allowDomains.length)];
} }
// check domain is valid // check domain is valid
if (!domain || !allowDomains.includes(domain)) { if (!domain || !allowDomains.includes(domain)) {
@@ -109,6 +125,7 @@ export const newAddress = async (
if (!success) { if (!success) {
throw new Error("Failed to create address") throw new Error("Failed to create address")
} }
await updateAddressUpdatedAt(c, name);
} catch (e) { } catch (e) {
const message = (e as Error).message; const message = (e as Error).message;
if (message && message.includes("UNIQUE")) { if (message && message.includes("UNIQUE")) {
@@ -151,11 +168,23 @@ export const cleanup = async (
cleanType: string | undefined | null, cleanType: string | undefined | null,
cleanDays: number | undefined | null cleanDays: number | undefined | null
): Promise<boolean> => { ): Promise<boolean> => {
if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 30) { if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 1000) {
throw new Error("Invalid cleanType or cleanDays") throw new Error("Invalid cleanType or cleanDays")
} }
console.log(`Cleanup ${cleanType} before ${cleanDays} days`); console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
switch (cleanType) { switch (cleanType) {
case "inactiveAddress":
await batchDeleteAddressWithData(
c,
`updated_at < datetime('now', '-${cleanDays} day')`
)
break;
case "addressCreated":
await batchDeleteAddressWithData(
c,
`created_at < datetime('now', '-${cleanDays} day')`
)
break;
case "mails": case "mails":
await c.env.DB.prepare(` await c.env.DB.prepare(`
DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')` DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')`
@@ -178,6 +207,37 @@ export const cleanup = async (
return true; return true;
} }
const batchDeleteAddressWithData = async (
c: Context<HonoCustomType>,
addressQueryCondition: string,
): Promise<boolean> => {
await c.env.DB.prepare(
`DELETE FROM raw_mails WHERE address IN ( ` +
`SELECT name FROM address WHERE ${addressQueryCondition})`
).run();
await c.env.DB.prepare(
`DELETE FROM sendbox WHERE address IN ( ` +
`SELECT name FROM address WHERE ${addressQueryCondition})`
).run();
await c.env.DB.prepare(
`DELETE FROM auto_reply_mails WHERE address IN ( ` +
`SELECT name FROM address WHERE ${addressQueryCondition})`
).run();
await c.env.DB.prepare(
`DELETE FROM address_sender WHERE address IN ( ` +
`SELECT name FROM address WHERE ${addressQueryCondition})`
).run();
await c.env.DB.prepare(
`DELETE FROM users_address WHERE address_id IN ( ` +
`SELECT id FROM address WHERE ${addressQueryCondition})`
).run();
// delete address
await c.env.DB.prepare(`
DELETE FROM address WHERE ${addressQueryCondition}`
).run();
return true;
}
/** /**
* TODO: need senbox delete? * TODO: need senbox delete?
*/ */
@@ -215,13 +275,19 @@ export const deleteAddressWithData = async (
const { success: sendAccess } = await c.env.DB.prepare( const { success: sendAccess } = await c.env.DB.prepare(
`DELETE FROM address_sender WHERE address = ? ` `DELETE FROM address_sender WHERE address = ? `
).bind(address).run(); ).bind(address).run();
const { success: sendboxSuccess } = await c.env.DB.prepare(
`DELETE FROM sendbox WHERE address = ? `
).bind(address).run();
const { success: addressSuccess } = await c.env.DB.prepare( const { success: addressSuccess } = await c.env.DB.prepare(
`DELETE FROM users_address WHERE address_id = ? ` `DELETE FROM users_address WHERE address_id = ? `
).bind(address_id).run(); ).bind(address_id).run();
const { success: autoReplySuccess } = await c.env.DB.prepare(
`DELETE FROM auto_reply_mails WHERE address = ? `
).bind(address).run();
const { success } = await c.env.DB.prepare( const { success } = await c.env.DB.prepare(
`DELETE FROM address WHERE name = ? ` `DELETE FROM address WHERE name = ? `
).bind(address).run(); ).bind(address).run();
if (!success || !mailSuccess || !addressSuccess || !sendAccess) { if (!success || !mailSuccess || !sendboxSuccess || !addressSuccess || !sendAccess || !autoReplySuccess) {
throw new Error("Failed to delete address") throw new Error("Failed to delete address")
} }
return true; return true;
@@ -339,26 +405,26 @@ export const getAllowDomains = async (c: Context<HonoCustomType>): Promise<strin
return user_role?.domains || getDefaultDomains(c);; return user_role?.domains || getDefaultDomains(c);;
} }
export async function sendWebhook(settings: WebhookSettings, formatMap: WebhookMail): Promise<{ success: boolean, message?: string }> { export async function sendWebhook(
settings: WebhookSettings, formatMap: WebhookMail
): Promise<{ success: boolean, message?: string }> {
// send webhook // send webhook
let body = settings.body; let body = settings.body;
for (const key of Object.keys(formatMap)) { for (const key of Object.keys(formatMap)) {
/* eslint-disable no-useless-escape */
body = body.replace( body = body.replace(
new RegExp(`\\$\\{${key}\\}`, "g"), new RegExp(`\\$\\{${key}\\}`, "g"),
JSON.stringify( JSON.stringify(
formatMap[key as keyof WebhookMail] formatMap[key as keyof WebhookMail]
).replace(/^"(.*)"$/, '\$1') ).replace(/^"(.*)"$/, '$1')
); );
/* eslint-enable no-useless-escape */
} }
console.log("send webhook", settings.url, settings.method, settings.headers, body);
const response = await fetch(settings.url, { const response = await fetch(settings.url, {
method: settings.method, method: settings.method,
headers: JSON.parse(settings.headers), headers: JSON.parse(settings.headers),
body: body body: body
}); });
if (!response.ok) { if (!response.ok) {
console.log("send webhook error", settings.url, settings.method, settings.headers, body);
console.log("send webhook error", response.status, response.statusText); console.log("send webhook error", response.status, response.statusText);
return { success: false, message: `send webhook error: ${response.status} ${response.statusText}` }; return { success: false, message: `send webhook error: ${response.status} ${response.statusText}` };
} }
+5 -1
View File
@@ -1,5 +1,9 @@
export const CONSTANTS = { export const CONSTANTS = {
VERSION: 'v0.8.7', VERSION: 'v' + '1.0.3',
// DB Version
DB_VERSION_KEY: 'db_version',
DB_VERSION: "v0.0.1",
// DB settings // DB settings
ADDRESS_BLOCK_LIST_KEY: 'address_block_list', ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
-1
View File
@@ -1,6 +1,5 @@
import { createMimeMessage } from "mimetext"; import { createMimeMessage } from "mimetext";
import { getBooleanValue } from "../utils"; import { getBooleanValue } from "../utils";
import { Bindings } from "../types";
export const auto_reply = async (message: ForwardableEmailMessage, env: Bindings): Promise<void> => { export const auto_reply = async (message: ForwardableEmailMessage, env: Bindings): Promise<void> => {
const message_id = message.headers.get("Message-ID"); const message_id = message.headers.get("Message-ID");
-1
View File
@@ -1,5 +1,4 @@
import { CONSTANTS } from "../constants"; import { CONSTANTS } from "../constants";
import { Bindings } from "../types";
export const isBlocked = async (from: string, env: Bindings): Promise<boolean> => { export const isBlocked = async (from: string, env: Bindings): Promise<boolean> => {
if (env.BLACK_LIST && env.BLACK_LIST.split(",").some(word => from.includes(word))) { if (env.BLACK_LIST && env.BLACK_LIST.split(",").some(word => from.includes(word))) {
-1
View File
@@ -1,4 +1,3 @@
import { Bindings, ParsedEmailContext } from "../types";
import { getBooleanValue } from "../utils"; import { getBooleanValue } from "../utils";
import { commonParseMail } from "../common"; import { commonParseMail } from "../common";
import { createMimeMessage } from "mimetext"; import { createMimeMessage } from "mimetext";
-1
View File
@@ -1,4 +1,3 @@
import { Bindings, ParsedEmailContext } from "../types";
import { getBooleanValue, getStringArray } from "../utils"; import { getBooleanValue, getStringArray } from "../utils";
import { commonParseMail } from "../common"; import { commonParseMail } from "../common";
+25 -2
View File
@@ -1,8 +1,7 @@
import { Context } from "hono"; import { Context } from "hono";
import { getEnvStringList } from "../utils"; import { getEnvStringList, getJsonObjectValue } from "../utils";
import { sendMailToTelegram } from "../telegram_api"; import { sendMailToTelegram } from "../telegram_api";
import { Bindings, HonoCustomType, RPCEmailMessage, ParsedEmailContext } from "../types";
import { auto_reply } from "./auto_reply"; import { auto_reply } from "./auto_reply";
import { isBlocked } from "./black_list"; import { isBlocked } from "./black_list";
import { triggerWebhook, triggerAnotherWorker, commonParseMail } from "../common"; import { triggerWebhook, triggerAnotherWorker, commonParseMail } from "../common";
@@ -67,6 +66,30 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
console.error("forward email error", error); console.error("forward email error", error);
} }
// forward subdomain email
try {
// 遍历 FORWARD_ADDRESS_LIST
const subdomainForwardAddressList = getJsonObjectValue<SubdomainForwardAddressList[]>(env.SUBDOMAIN_FORWARD_ADDRESS_LIST) || [];
for (const subdomainForwardAddress of subdomainForwardAddressList) {
// 检查邮件是否匹配 domains
if (subdomainForwardAddress.domains && subdomainForwardAddress.domains.length > 0) {
for (const domain of subdomainForwardAddress.domains) {
if (message.to.endsWith(domain)) {
// 转发邮件
await message.forward(subdomainForwardAddress.forward);
// 支持多邮箱转发收件,不进行截止
// break;
}
}
} else {
// 如果 domains 为空,则转发所有邮件
await message.forward(subdomainForwardAddress.forward);
}
}
} catch (error) {
console.error("subdomain forward email error", error);
}
// send email to telegram // send email to telegram
try { try {
await sendMailToTelegram( await sendMailToTelegram(
+43
View File
@@ -0,0 +1,43 @@
import { LocaleMessages } from "./type";
const messages: LocaleMessages = {
CustomAuthPasswordMsg: "You have enabled the private site password, please provide the password",
UserTokenExpiredMsg: "Your token has expired, please login again",
UserAcceesTokenExpiredMsg: "Your access token has expired, please refresh the page",
UserRoleIsNotAdminMsg: "Your user role is not admin, no access to visit this page",
NeedAdminPasswordMsg: "You need to provide the admin password to access this page",
KVNotAvailableMsg: "KV is not available, please contact the administrator",
DBNotAvailableMsg: "DB is not available, please contact the administrator",
JWTSecretNotSetMsg: "JWT_SECRET is not set, please contact the administrator",
WebhookNotEnabledMsg: "Webhook is not enabled, please contact the administrator",
DomainsNotSetMsg: "Domains are not set, please contact the administrator",
TurnstileCheckFailedMsg: "Human verification check failed",
NewAddressDisabledMsg: "New address is disabled, please contact the administrator",
NewAddressAnonymousDisabledMsg: "New address for anonymous user is disabled, please contact the administrator",
FailedCreateAddressMsg: "Failed to create address",
InvalidAddressMsg: "Invalid address",
InvalidAddressCredentialMsg: "Invalid address credential",
UserDeleteEmailDisabledMsg: "User delete address/email is disabled, please contact the administrator",
UserNotFoundMsg: "User not found",
UserAlreadyExistsMsg: "User already exists, please login",
FailedToRegisterMsg: "Failed to register",
UserRegistrationDisabledMsg: "User registration is disabled, please contact the administrator",
UserMailDomainMustInMsg: "User mail domain must be in this list",
InvalidVerifyCodeMsg: "Invalid verify code",
InvalidEmailOrPasswordMsg: "Invalid email or password",
VerifyMailSenderNotSetMsg: "Verify mail sender address is not set, please contact the administrator",
CodeAlreadySentMsg: "Code already sent, please wait",
InvalidUserDefaultRoleMsg: "Invalid user default role, please contact the administrator",
FailedUpdateUserDefaultRoleMsg: "Failed to update user default role, please contact the administrator",
Oauth2ClientIDNotFoundMsg: "Oauth2 client ID is not set, please contact the administrator",
Oauth2CliendIDOrCodeMissingMsg: "Oauth2 client ID or code is missing",
Oauth2FailedGetUserInfoMsg: "Failed to get user info from Oauth2 provider",
Oauth2FailedGetAccessTokenMsg: "Failed to get access token from Oauth2 provider",
Oauth2FailedGetUserEmailMsg: "Failed to get user email from Oauth2 provider",
}
export default messages;
+28
View File
@@ -0,0 +1,28 @@
import { LocaleMessages } from "./type";
import zh from "./zh";
import en from "./en";
import { Context } from "hono";
export default {
getMessages: (
locale: string | null | undefined
): LocaleMessages => {
// multi-language support
if (locale === "en") return en;
if (locale === "zh") return zh;
// fallback language
return en;
},
getMessagesbyContext: (
c: Context<HonoCustomType>
): LocaleMessages => {
const locale = c.get("lang") || c.env.DEFAULT_LANG;
// multi-language support
if (locale === "en") return en;
if (locale === "zh") return zh;
// fallback language
return en;
}
}
+39
View File
@@ -0,0 +1,39 @@
export type LocaleMessages = {
CustomAuthPasswordMsg: string
UserTokenExpiredMsg: string
UserAcceesTokenExpiredMsg: string
UserRoleIsNotAdminMsg: string
NeedAdminPasswordMsg: string
KVNotAvailableMsg: string
DBNotAvailableMsg: string
JWTSecretNotSetMsg: string
WebhookNotEnabledMsg: string
DomainsNotSetMsg: string
TurnstileCheckFailedMsg: string
NewAddressDisabledMsg: string
NewAddressAnonymousDisabledMsg: string
FailedCreateAddressMsg: string
InvalidAddressMsg: string
InvalidAddressCredentialMsg: string
UserDeleteEmailDisabledMsg: string
UserNotFoundMsg: string
UserAlreadyExistsMsg: string
FailedToRegisterMsg: string
UserRegistrationDisabledMsg: string
UserMailDomainMustInMsg: string
InvalidVerifyCodeMsg: string
InvalidEmailOrPasswordMsg: string
VerifyMailSenderNotSetMsg: string
CodeAlreadySentMsg: string
InvalidUserDefaultRoleMsg: string
FailedUpdateUserDefaultRoleMsg: string
Oauth2ClientIDNotFoundMsg: string
Oauth2CliendIDOrCodeMissingMsg: string
Oauth2FailedGetUserInfoMsg: string
Oauth2FailedGetAccessTokenMsg: string
Oauth2FailedGetUserEmailMsg: string
}
+43
View File
@@ -0,0 +1,43 @@
import { LocaleMessages } from "./type";
const messages: LocaleMessages = {
CustomAuthPasswordMsg: "你已启用私有站点密码,请提供密码",
UserTokenExpiredMsg: "您的令牌已过期, 请重新登录",
UserAcceesTokenExpiredMsg: "您的访问令牌已过期, 请刷新页面",
UserRoleIsNotAdminMsg: "您的用户角色不是管理员, 无权访问",
NeedAdminPasswordMsg: "您需要提供管理员密码才能访问此页面",
KVNotAvailableMsg: "KV 不可用, 请联系管理员",
DBNotAvailableMsg: "DB 不可用, 请联系管理员",
JWTSecretNotSetMsg: "JWT_SECRET 未设置, 请联系管理员",
WebhookNotEnabledMsg: "Webhook 未启用, 请联系管理员",
DomainsNotSetMsg: "域名列表未设置, 请联系管理员",
TurnstileCheckFailedMsg: "人机验证检查失败",
NewAddressDisabledMsg: "新建邮箱地址已禁用, 请联系管理员",
NewAddressAnonymousDisabledMsg: "匿名用户新建邮箱地址已禁用, 请联系管理员",
FailedCreateAddressMsg: "创建邮箱地址失败",
InvalidAddressMsg: "无效的邮箱地址",
InvalidAddressCredentialMsg: "无效的邮箱地址凭据",
UserDeleteEmailDisabledMsg: "用户删除邮箱/邮件已禁用, 请联系管理员",
UserNotFoundMsg: "用户不存在",
UserAlreadyExistsMsg: "用户已存在, 请登录",
FailedToRegisterMsg: "注册失败",
UserRegistrationDisabledMsg: "用户注册已禁用, 请联系管理员",
UserMailDomainMustInMsg: "用户邮箱域必须在此列表中",
InvalidVerifyCodeMsg: "无效的验证码",
InvalidEmailOrPasswordMsg: "无效的邮箱或密码",
VerifyMailSenderNotSetMsg: "验证邮件发送邮箱未设置, 请联系管理员",
CodeAlreadySentMsg: "验证码已发送, 请稍等",
InvalidUserDefaultRoleMsg: "无效的用户默认角色, 请联系管理员",
FailedUpdateUserDefaultRoleMsg: "更新用户默认角色失败, 请联系管理员",
Oauth2ClientIDNotFoundMsg: "Oauth2 客户端 ID 未设置, 请联系管理员",
Oauth2CliendIDOrCodeMissingMsg: "Oauth2 客户端 ID 或 code 缺失",
Oauth2FailedGetUserInfoMsg: "从 Oauth2 提供商获取用户信息失败",
Oauth2FailedGetAccessTokenMsg: "从 Oauth2 提供商获取访问令牌失败",
Oauth2FailedGetUserEmailMsg: "从 Oauth2 提供商获取用户邮箱失败",
}
export default messages;
-1
View File
@@ -1,6 +1,5 @@
import { Context } from "hono"; import { Context } from "hono";
import { getBooleanValue } from "../utils"; import { getBooleanValue } from "../utils";
import { HonoCustomType } from "../types";
export default { export default {
+26 -22
View File
@@ -1,8 +1,8 @@
import { Hono } from 'hono' import { Context, Hono } from 'hono'
import { HonoCustomType } from "../types"; import i18n from '../i18n';
import { getBooleanValue, getJsonSetting, checkCfTurnstile, getStringValue } from '../utils'; import { getBooleanValue, getJsonSetting, checkCfTurnstile, getStringValue, getSplitStringListValue } from '../utils';
import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains } from '../common' import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains, updateAddressUpdatedAt } from '../common'
import { CONSTANTS } from '../constants' import { CONSTANTS } from '../constants'
import auto_reply from './auto_reply' import auto_reply from './auto_reply'
import webhook_settings from './webhook_settings'; import webhook_settings from './webhook_settings';
@@ -16,6 +16,7 @@ api.get('/api/webhook/settings', webhook_settings.getWebhookSettings)
api.post('/api/webhook/settings', webhook_settings.saveWebhookSettings) api.post('/api/webhook/settings', webhook_settings.saveWebhookSettings)
api.post('/api/webhook/test', webhook_settings.testWebhookSettings) api.post('/api/webhook/test', webhook_settings.testWebhookSettings)
api.get('/api/attachment/list', s3_attachment.list) api.get('/api/attachment/list', s3_attachment.list)
api.post('/api/attachment/delete', s3_attachment.deleteKey)
api.post('/api/attachment/put_url', s3_attachment.getSignedPutUrl) api.post('/api/attachment/put_url', s3_attachment.getSignedPutUrl)
api.post('/api/attachment/get_url', s3_attachment.getSignedGetUrl) api.post('/api/attachment/get_url', s3_attachment.getSignedGetUrl)
@@ -25,6 +26,7 @@ api.get('/api/mails', async (c) => {
return c.json({ "error": "No address" }, 400) return c.json({ "error": "No address" }, 400)
} }
const { limit, offset } = c.req.query(); const { limit, offset } = c.req.query();
if (Number.parseInt(offset) <= 0) await updateAddressUpdatedAt(c, address);
return await handleListQuery(c, return await handleListQuery(c,
`SELECT * FROM raw_mails where address = ?`, `SELECT * FROM raw_mails where address = ?`,
`SELECT count(*) as count FROM raw_mails where address = ?`, `SELECT count(*) as count FROM raw_mails where address = ?`,
@@ -42,8 +44,10 @@ api.get('/api/mail/:mail_id', async (c) => {
}) })
api.delete('/api/mails/:id', async (c) => { api.delete('/api/mails/:id', async (c) => {
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) { if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
return c.text("User delete email is disabled", 403) return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
} }
const { address } = c.get("jwtPayload") const { address } = c.get("jwtPayload")
const { id } = c.req.param(); const { id } = c.req.param();
@@ -59,16 +63,18 @@ api.delete('/api/mails/:id', async (c) => {
api.get('/api/settings', async (c) => { api.get('/api/settings', async (c) => {
const { address, address_id } = c.get("jwtPayload") const { address, address_id } = c.get("jwtPayload")
const user_role = c.get("userRolePayload") const user_role = c.get("userRolePayload")
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
if (address_id && address_id > 0) { if (address_id && address_id > 0) {
try { try {
const db_address_id = await c.env.DB.prepare( const db_address_id = await c.env.DB.prepare(
`SELECT id FROM address where id = ? ` `SELECT id FROM address where id = ? `
).bind(address_id).first("id"); ).bind(address_id).first("id");
if (!db_address_id) { if (!db_address_id) {
return c.text("Invalid address", 400) return c.text(msgs.InvalidAddressMsg, 400)
} }
} catch (error) { } catch (error) {
return c.text("Invalid address", 400) return c.text(msgs.InvalidAddressMsg, 400)
} }
} }
// check address id // check address id
@@ -78,21 +84,17 @@ api.get('/api/settings', async (c) => {
`SELECT id FROM address where name = ? ` `SELECT id FROM address where name = ? `
).bind(address).first("id"); ).bind(address).first("id");
if (!db_address_id) { if (!db_address_id) {
return c.text("Invalid address", 400) return c.text(msgs.InvalidAddressMsg, 400)
} }
} }
} catch (error) { } catch (error) {
return c.text("Invalid address", 400) return c.text(msgs.InvalidAddressMsg, 400)
} }
// update address updated_at
try { await updateAddressUpdatedAt(c, address);
c.env.DB.prepare(
`UPDATE address SET updated_at = datetime('now') where name = ?` const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
).bind(address).run(); const is_no_limit_send_balance = user_role && no_limit_roles.includes(user_role);
} catch (e) {
console.warn("Failed to update address")
}
const is_no_limit_send_balance = user_role && user_role === getStringValue(c.env.NO_LIMIT_SEND_ROLE);
const balance = is_no_limit_send_balance ? 99999 : await c.env.DB.prepare( const balance = is_no_limit_send_balance ? 99999 : await c.env.DB.prepare(
`SELECT balance FROM address_sender where address = ? and enabled = 1` `SELECT balance FROM address_sender where address = ? and enabled = 1`
).bind(address).first("balance"); ).bind(address).first("balance");
@@ -103,13 +105,15 @@ api.get('/api/settings', async (c) => {
}) })
api.post('/api/new_address', async (c) => { api.post('/api/new_address', async (c) => {
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
if (getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL) if (getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL)
&& !c.get("userPayload") && !c.get("userPayload")
) { ) {
return c.text("New address for anonymous user is disabled", 403) return c.text(msgs.NewAddressAnonymousDisabledMsg, 403)
} }
if (!getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL)) { if (!getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL)) {
return c.text("New address is disabled", 403) return c.text(msgs.NewAddressDisabledMsg, 403)
} }
// eslint-disable-next-line prefer-const // eslint-disable-next-line prefer-const
let { name, domain, cf_token } = await c.req.json(); let { name, domain, cf_token } = await c.req.json();
@@ -117,7 +121,7 @@ api.post('/api/new_address', async (c) => {
try { try {
await checkCfTurnstile(c, cf_token); await checkCfTurnstile(c, cf_token);
} catch (error) { } catch (error) {
return c.text("Failed to check cf turnstile", 500) return c.text(msgs.TurnstileCheckFailedMsg, 500)
} }
// if no name, generate random name // if no name, generate random name
if (!name) { if (!name) {
@@ -143,7 +147,7 @@ api.post('/api/new_address', async (c) => {
}); });
return c.json(res); return c.json(res);
} catch (e) { } catch (e) {
return c.text(`Failed create address: ${(e as Error).message}`, 400) return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400)
} }
}) })
+14 -2
View File
@@ -1,10 +1,10 @@
import { HonoCustomType } from "../types";
import { Context } from "hono"; import { Context } from "hono";
import { import {
S3Client, S3Client,
ListObjectsV2Command, ListObjectsV2Command,
GetObjectCommand, GetObjectCommand,
PutObjectCommand PutObjectCommand,
DeleteObjectCommand
} from "@aws-sdk/client-s3"; } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
@@ -81,4 +81,16 @@ export default {
} }
); );
}, },
deleteKey: async (c: Context<HonoCustomType>) => {
const { address } = c.get("jwtPayload")
const { key } = await c.req.json()
const client = getS3Client(c);
await client.send(
new DeleteObjectCommand({
Bucket: c.env.S3_BUCKET,
Key: `${address}/${key}`
})
);
return c.json({ success: true });
}
} }
+7 -4
View File
@@ -4,11 +4,11 @@ import { createMimeMessage } from 'mimetext';
import { Resend } from 'resend'; import { Resend } from 'resend';
import { WorkerMailer, WorkerMailerOptions } from 'worker-mailer'; import { WorkerMailer, WorkerMailerOptions } from 'worker-mailer';
import i18n from '../i18n';
import { CONSTANTS } from '../constants' import { CONSTANTS } from '../constants'
import { getJsonSetting, getDomains, getIntValue, getBooleanValue, getStringValue, getJsonObjectValue } from '../utils'; import { getJsonSetting, getDomains, getIntValue, getBooleanValue, getStringValue, getJsonObjectValue, getSplitStringListValue } from '../utils';
import { GeoData } from '../models' import { GeoData } from '../models'
import { handleListQuery } from '../common' import { handleListQuery } from '../common'
import { HonoCustomType } from '../types';
export const api = new Hono<HonoCustomType>() export const api = new Hono<HonoCustomType>()
@@ -136,7 +136,8 @@ export const sendMail = async (
throw new Error("Invalid domain") throw new Error("Invalid domain")
} }
const user_role = c.get("userRolePayload"); const user_role = c.get("userRolePayload");
const is_no_limit_send_balance = user_role && user_role === getStringValue(c.env.NO_LIMIT_SEND_ROLE); const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
const is_no_limit_send_balance = user_role && no_limit_roles.includes(user_role);
// no need find noLimitSendAddressList if is_no_limit_send_balance // no need find noLimitSendAddressList if is_no_limit_send_balance
const noLimitSendAddressList = is_no_limit_send_balance ? const noLimitSendAddressList = is_no_limit_send_balance ?
[] : await getJsonSetting(c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY) || []; [] : await getJsonSetting(c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY) || [];
@@ -288,8 +289,10 @@ api.get('/api/sendbox', async (c) => {
}) })
api.delete('/api/sendbox/:id', async (c) => { api.delete('/api/sendbox/:id', async (c) => {
const lang = c.get("lang") || c.env.DEFAULT_LANG;
const msgs = i18n.getMessages(lang);
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) { if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
return c.text("User delete email is disabled", 403) return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
} }
const { address } = c.get("jwtPayload") const { address } = c.get("jwtPayload")
const { id } = c.req.param(); const { id } = c.req.param();
-2
View File
@@ -1,8 +1,6 @@
import { Context } from "hono"; import { Context } from "hono";
import { HonoCustomType, ParsedEmailContext } from "../types";
import { CONSTANTS } from "../constants"; import { CONSTANTS } from "../constants";
import { AdminWebhookSettings, WebhookSettings } from "../models"; import { AdminWebhookSettings, WebhookSettings } from "../models";
import { getBooleanValue } from "../utils";
import { commonParseMail, sendWebhook } from "../common"; import { commonParseMail, sendWebhook } from "../common";
+5 -16
View File
@@ -32,7 +32,7 @@ export type WebhookMail = {
parsedHtml: string; parsedHtml: string;
} }
export class CleanupSettings { export type CleanupSettings = {
enableMailsAutoCleanup: boolean | undefined; enableMailsAutoCleanup: boolean | undefined;
cleanMailsDays: number; cleanMailsDays: number;
@@ -40,23 +40,12 @@ export class CleanupSettings {
cleanUnknowMailsDays: number; cleanUnknowMailsDays: number;
enableSendBoxAutoCleanup: boolean | undefined; enableSendBoxAutoCleanup: boolean | undefined;
cleanSendBoxDays: number; cleanSendBoxDays: number;
enableAddressAutoCleanup: boolean | undefined;
constructor(data: CleanupSettings | undefined | null) { cleanAddressDays: number;
const { enableInactiveAddressAutoCleanup: boolean | undefined;
enableMailsAutoCleanup, cleanMailsDays, cleanInactiveAddressDays: number;
enableUnknowMailsAutoCleanup, cleanUnknowMailsDays,
enableSendBoxAutoCleanup, cleanSendBoxDays
} = data || {};
this.enableMailsAutoCleanup = enableMailsAutoCleanup;
this.cleanMailsDays = cleanMailsDays || 0;
this.enableUnknowMailsAutoCleanup = enableUnknowMailsAutoCleanup;
this.cleanUnknowMailsDays = cleanUnknowMailsDays || 0;
this.enableSendBoxAutoCleanup = enableSendBoxAutoCleanup;
this.cleanSendBoxDays = cleanSendBoxDays || 0;
}
} }
export class GeoData { export class GeoData {
ip: string; ip: string;
+22 -6
View File
@@ -3,35 +3,51 @@ import { cleanup } from './common'
import { CONSTANTS } from './constants' import { CONSTANTS } from './constants'
import { getJsonSetting } from './utils'; import { getJsonSetting } from './utils';
import { CleanupSettings } from './models'; import { CleanupSettings } from './models';
import { Bindings, HonoCustomType } from './types';
export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) { export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) {
console.log("Scheduled event: ", event); console.log("Scheduled event: ", event);
const value = await getJsonSetting( const autoCleanupSetting = await getJsonSetting<CleanupSettings>(
{ env: env, } as Context<HonoCustomType>, { env: env, } as Context<HonoCustomType>,
CONSTANTS.AUTO_CLEANUP_KEY CONSTANTS.AUTO_CLEANUP_KEY
); );
const autoCleanupSetting = new CleanupSettings(value); if (!autoCleanupSetting) {
console.log("No auto cleanup settings found, skipping cleanup.");
return;
}
console.log("autoCleanupSetting:", JSON.stringify(autoCleanupSetting)); console.log("autoCleanupSetting:", JSON.stringify(autoCleanupSetting));
if (autoCleanupSetting.enableMailsAutoCleanup && autoCleanupSetting.cleanMailsDays > 0) { if (autoCleanupSetting.enableMailsAutoCleanup) {
await cleanup( await cleanup(
{ env: env, } as Context<HonoCustomType>, { env: env, } as Context<HonoCustomType>,
"mails", "mails",
autoCleanupSetting.cleanMailsDays autoCleanupSetting.cleanMailsDays
); );
} }
if (autoCleanupSetting.enableUnknowMailsAutoCleanup && autoCleanupSetting.cleanUnknowMailsDays > 0) { if (autoCleanupSetting.enableUnknowMailsAutoCleanup) {
await cleanup( await cleanup(
{ env: env, } as Context<HonoCustomType>, { env: env, } as Context<HonoCustomType>,
"mails_unknow", "mails_unknow",
autoCleanupSetting.cleanUnknowMailsDays autoCleanupSetting.cleanUnknowMailsDays
); );
} }
if (autoCleanupSetting.enableSendBoxAutoCleanup && autoCleanupSetting.cleanSendBoxDays > 0) { if (autoCleanupSetting.enableSendBoxAutoCleanup) {
await cleanup( await cleanup(
{ env: env, } as Context<HonoCustomType>, { env: env, } as Context<HonoCustomType>,
"sendbox", "sendbox",
autoCleanupSetting.cleanSendBoxDays autoCleanupSetting.cleanSendBoxDays
); );
} }
if (autoCleanupSetting.enableInactiveAddressAutoCleanup) {
await cleanup(
{ env: env, } as Context<HonoCustomType>,
"inactiveAddress",
autoCleanupSetting.cleanInactiveAddressDays
);
}
if (autoCleanupSetting.enableAddressAutoCleanup) {
await cleanup(
{ env: env, } as Context<HonoCustomType>,
"addressCreated",
autoCleanupSetting.cleanAddressDays
);
}
} }
+9 -2
View File
@@ -1,7 +1,6 @@
import { Context } from "hono"; import { Context } from "hono";
import { Jwt } from "hono/utils/jwt"; import { Jwt } from "hono/utils/jwt";
import { CONSTANTS } from "../constants"; import { CONSTANTS } from "../constants";
import { HonoCustomType } from "../types";
import { getIntValue, getJsonSetting } from "../utils"; import { getIntValue, getJsonSetting } from "../utils";
import { deleteAddressWithData, newAddress } from "../common"; import { deleteAddressWithData, newAddress } from "../common";
@@ -52,6 +51,14 @@ export const jwtListToAddressData = async (
for (const jwt of jwtList) { for (const jwt of jwtList) {
try { try {
const { address, address_id } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256"); const { address, address_id } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
const name = await c.env.DB.prepare(
`SELECT name FROM address WHERE id = ? `
).bind(address_id).first("name");
if (!name) {
addressList.push("无效地址");
invalidJwtList.push(jwt);
continue;
}
addressList.push(address as string); addressList.push(address as string);
addressIdMap[address as string] = address_id as number; addressIdMap[address as string] = address_id as number;
} catch (e) { } catch (e) {
@@ -76,7 +83,7 @@ export const bindTelegramAddress = async (
return address as string; return address as string;
} }
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) { if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
throw Error("绑定地址数量已达上限"); throw Error("绑定地址数量已达上限, 请先 /cleaninvalidaddress");
} }
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, jwt])); await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, jwt]));
// for mail push to telegram // for mail push to telegram

Some files were not shown because too many files have changed in this diff Show More