mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-12 11:19:56 +08:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4c96c9868 | ||
|
|
2318e0f7e2 | ||
|
|
2cce3df213 | ||
|
|
a38a31a407 | ||
|
|
276093f113 | ||
|
|
36f8c4b3de | ||
|
|
8964d4461d | ||
|
|
a771446b9b | ||
|
|
50ab6756bd | ||
|
|
aee1f1942b | ||
|
|
3ebe22115a | ||
|
|
5e227d2b2d | ||
|
|
3b3968f3b4 | ||
|
|
499f65078b | ||
|
|
0d63142bd7 | ||
|
|
16ce1bf4e0 | ||
|
|
288eb38302 | ||
|
|
24366e2bff | ||
|
|
e5f62d4713 | ||
|
|
1836f931ee | ||
|
|
0f2836eebb | ||
|
|
b933aef7d9 | ||
|
|
fa72d7187f | ||
|
|
d15a4904a5 | ||
|
|
a25199eb34 |
4
.github/workflows/pr_agent.yml
vendored
4
.github/workflows/pr_agent.yml
vendored
@@ -3,6 +3,7 @@ name: Codium PR Agent
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, ready_for_review]
|
||||
issue_comment:
|
||||
jobs:
|
||||
pr_agent_job:
|
||||
if: ${{ github.event.sender.type != 'Bot' }}
|
||||
@@ -10,11 +11,12 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
name: Run pr agent on every pull request, respond to user comments
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
id: pragent
|
||||
uses: docker://codiumai/pr-agent:0.29-github_action
|
||||
uses: qodo-ai/pr-agent@main
|
||||
env:
|
||||
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false"
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
|
||||
7
.github/workflows/smtp_proxy_server.yml
vendored
7
.github/workflows/smtp_proxy_server.yml
vendored
@@ -36,6 +36,9 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Set lowercase repository name
|
||||
run: echo "REPO_LOWER=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
@@ -44,5 +47,5 @@ jobs:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
|
||||
${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE_NAME }}:latest
|
||||
${{ env.REGISTRY }}/${{ env.REPO_LOWER }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
|
||||
${{ env.REGISTRY }}/${{ env.REPO_LOWER }}/${{ env.IMAGE_NAME }}:latest
|
||||
|
||||
35
AGENTS.md
Normal file
35
AGENTS.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure
|
||||
- Backend: `worker/` (Workers API; entry `worker/src/worker.ts`, APIs under `worker/src/*_api/`).
|
||||
- Frontend: `frontend/` (Vue 3 app; routes in `frontend/src/router/`).
|
||||
- Pages middleware: `pages/functions/_middleware.js`.
|
||||
- Mail parser: `mail-parser-wasm/` (Rust WASM).
|
||||
- SMTP/IMAP proxy: `smtp_proxy_server/`.
|
||||
- DB schema/migrations: `db/`.
|
||||
- Docs: `vitepress-docs/`.
|
||||
|
||||
## Build & Dev Commands
|
||||
Run inside each folder:
|
||||
- Frontend: `pnpm dev`, `pnpm build`.
|
||||
- Worker: `pnpm dev`, `pnpm lint`, `pnpm build`.
|
||||
- Pages: `pnpm dev`.
|
||||
- Docs: `pnpm dev` in `vitepress-docs/`.
|
||||
- WASM: `wasm-pack build --release` in `mail-parser-wasm/`.
|
||||
- SMTP proxy: `pip install -r smtp_proxy_server/requirements.txt` then `python smtp_proxy_server/main.py`.
|
||||
|
||||
## Coding Style
|
||||
- Follow existing module style. `worker/` uses TypeScript + ESLint; `frontend/` uses Vue SFCs.
|
||||
- Keep current naming patterns: `*_api/`, `utils/`, `models/`.
|
||||
- ESM imports only (`type: module`).
|
||||
|
||||
## Testing
|
||||
- No formal test runner. Validate with local dev servers and key flows (login, inbox, send/receive).
|
||||
|
||||
## Commits & PRs
|
||||
- Use Conventional Commits (`feat:`, `fix:`, `docs:`). Recent history includes PR numbers like `(#123)`.
|
||||
- PRs should explain scope and add screenshots for UI changes.
|
||||
|
||||
## Config Tips
|
||||
- Worker settings in `worker/wrangler.toml` (see template for bindings).
|
||||
- Frontend uses `VITE_*` env vars. Don’t commit secrets.
|
||||
38
CHANGELOG.md
38
CHANGELOG.md
@@ -1,7 +1,41 @@
|
||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||
<!-- markdownlint-disable-file MD004 MD024 MD033 MD034 MD036 -->
|
||||
# CHANGE LOG
|
||||
|
||||
## v1.1.0(main)
|
||||
<p align="center">
|
||||
<a href="CHANGELOG.md">🇨🇳 中文</a> |
|
||||
<a href="CHANGELOG_EN.md">🇺🇸 English</a>
|
||||
</p>
|
||||
|
||||
## v1.2.1(main)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: |定时任务| 修复定时任务清理报错 `e.get is not a function`,使用可选链安全访问 Context 方法
|
||||
|
||||
### Improvements
|
||||
|
||||
- style: |AI 提取| 暗色模式下 AI 提取信息使用更柔和的蓝色 (#A8C7FA),减少视觉疲劳
|
||||
|
||||
## v1.2.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- |数据库| 新增 `source_meta` 字段,需执行 `db/2025-12-27-source-meta.sql` 更新数据库或到 admin 维护页面点击数据库更新按钮
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |Admin| 新增管理员账号页面,显示当前登录方式并支持退出登录(仅限密码登录方式)
|
||||
- fix: |GitHub Actions| 修复容器镜像名需要全部小写的问题
|
||||
- feat: |邮件转发| 新增来源地址正则转发功能,支持按发件人地址过滤转发,完全向后兼容
|
||||
- feat: |地址来源| 新增地址来源追踪功能,记录地址创建来源(Web 记录 IP,Telegram 记录用户 ID,Admin 后台标记)
|
||||
- feat: |邮件过滤| 移除后端 keyword 参数,改为前端过滤当前页邮件,优化查询性能
|
||||
- feat: |前端| 地址切换统一为下拉组件,极简模式支持切换,主页提供地址管理入口
|
||||
- feat: |数据库| 为 `message_id` 字段添加索引,优化邮件更新操作性能,需执行 `db/2025-12-15-message-id-index.sql` 更新数据库
|
||||
- feat: |Admin| 维护页面增加自定义 SQL 清理功能,支持定时任务执行自定义清理语句
|
||||
- feat: |国际化| 后端 API 错误消息全面支持中英文国际化
|
||||
- feat: |Telegram| 机器人支持中英文切换,新增 `/lang` 命令设置语言偏好
|
||||
|
||||
## v1.1.0
|
||||
|
||||
- feat: |AI 提取| 增加 AI 邮件识别功能,使用 Cloudflare Workers AI 自动提取邮件中的验证码、认证链接、服务链接等重要信息
|
||||
- 支持优先级提取:验证码 > 认证链接 > 服务链接 > 订阅链接 > 其他链接
|
||||
|
||||
588
CHANGELOG_EN.md
Normal file
588
CHANGELOG_EN.md
Normal file
@@ -0,0 +1,588 @@
|
||||
<!-- markdownlint-disable-file MD004 MD024 MD033 MD034 MD036 -->
|
||||
# CHANGE LOG
|
||||
|
||||
<p align="center">
|
||||
<a href="CHANGELOG.md">🇨🇳 中文</a> |
|
||||
<a href="CHANGELOG_EN.md">🇺🇸 English</a>
|
||||
</p>
|
||||
|
||||
## v1.2.1(main)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: |Scheduled Tasks| Fix scheduled task cleanup error `e.get is not a function`, use optional chaining for safe access to Context methods
|
||||
|
||||
### Improvements
|
||||
|
||||
- style: |AI Extraction| Use softer blue color (#A8C7FA) for AI extraction info in dark mode to reduce eye strain
|
||||
|
||||
## v1.2.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- |Database| Add `source_meta` field, need to execute `db/2025-12-27-source-meta.sql` to update database or click database update button on admin maintenance page
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |Admin| Add admin account page, display current login method and support logout (password login only)
|
||||
- fix: |GitHub Actions| Fix container image name must be lowercase
|
||||
- feat: |Email Forwarding| Add source address regex forwarding, filter by sender address, fully backward compatible
|
||||
- feat: |Address Source| Add address source tracking feature, record address creation source (Web records IP, Telegram records user ID, Admin panel marked)
|
||||
- feat: |Email Filtering| Remove backend keyword parameter, switch to frontend filtering of current page emails, optimize query performance
|
||||
- feat: |Frontend| Unify address switching into a dropdown component, support switching in simple mode, add address management entry on the homepage
|
||||
- feat: |Database| Add index for `message_id` field to optimize email update operations, need to execute `db/2025-12-15-message-id-index.sql` to update database
|
||||
- feat: |Admin| Add custom SQL cleanup feature to maintenance page, support scheduled task execution of custom cleanup statements
|
||||
- feat: |i18n| Backend API error messages now fully support Chinese and English internationalization
|
||||
- feat: |Telegram| Bot supports Chinese/English switching, add `/lang` command to set language preference
|
||||
|
||||
## v1.1.0
|
||||
|
||||
- feat: |AI Extraction| Add AI email recognition feature, use Cloudflare Workers AI to automatically extract verification codes, authentication links, service links and other important information from emails
|
||||
- Support priority extraction: verification codes > authentication links > service links > subscription links > other links
|
||||
- Admin can configure address whitelist (supports wildcards, e.g. `*@example.com`)
|
||||
- Frontend list and detail pages display extraction results
|
||||
- Need to configure `ENABLE_AI_EMAIL_EXTRACT` environment variable and AI binding
|
||||
- Need to execute SQL in `db/2025-12-06-metadata.sql` file to update `D1` database or click database update button on admin maintenance page
|
||||
- feat: |Admin| Add feature to cleanup addresses with empty mailboxes older than n days on maintenance page
|
||||
- fix: Fix custom authentication password function issue (frontend property name error & /open_api interface blocked)
|
||||
|
||||
## v1.0.7
|
||||
|
||||
- feat: |Admin| Add IP blacklist feature for limiting high-frequency API access
|
||||
- feat: |Admin| Add ASN organization blacklist feature, support filtering requests based on ASN organization name (supports text matching and regex)
|
||||
- feat: |Admin| Add browser fingerprint blacklist feature, support filtering requests based on browser fingerprint (supports exact matching and regex)
|
||||
|
||||
## v1.0.6
|
||||
|
||||
- feat: |DB| Update db schema add index
|
||||
- feat: |Address Password| Add address password login feature, enabled via `ENABLE_ADDRESS_PASSWORD` configuration, need to execute SQL in `db/2025-09-23-patch.sql` file to update `D1` database
|
||||
- fix: |GitHub Actions| Fix debug mode configuration, only enable debug mode when DEBUG_MODE is 'true'
|
||||
- feat: |Admin| Account management page adds multi-select batch operations (batch delete, batch clear inbox, batch clear outbox)
|
||||
- feat: |Admin| Maintenance page adds feature to cleanup unbound user addresses
|
||||
- feat: Support configuring different bound address quantity limits for different roles, configurable in admin page
|
||||
|
||||
## v1.0.5
|
||||
|
||||
- feat: Add `DISABLE_CUSTOM_ADDRESS_NAME` configuration: disable custom email address name feature
|
||||
- feat: Add `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` configuration: prioritize first domain when creating addresses
|
||||
- feat: |UI| Add button to enter minimalist mode on homepage
|
||||
- feat: |Webhook| Add whitelist switch feature, support flexible access control
|
||||
|
||||
## v1.0.4
|
||||
|
||||
- feat: |UI| Optimize minimalist mode homepage, add all emails page functionality (delete/download/attachments/...), switchable in `Appearance`
|
||||
- feat: Admin account settings page adds `Email Forwarding Rules` configuration
|
||||
- feat: Admin account settings page adds `Reject Unknown Address Emails` configuration
|
||||
- feat: Email page adds Previous/Next buttons
|
||||
|
||||
## v1.0.3
|
||||
|
||||
- fix: Fix github actions deployment issue
|
||||
- feat: telegram /new when domain not specified, use random address
|
||||
|
||||
## v1.0.2
|
||||
|
||||
- fix: Fix oauth2 login failure issue
|
||||
|
||||
## v1.0.1
|
||||
|
||||
- feat: |UI| Add minimalist mode homepage, switchable in `Appearance`
|
||||
- fix: Fix oauth2 login default role not taking effect issue
|
||||
|
||||
## v1.0.0
|
||||
|
||||
- fix: |UI| Fix User inbox viewing, when address not selected, keyword query not working
|
||||
- fix: Fix auto cleanup task, time 0 not taking effect issue
|
||||
- feat: Cleanup feature adds cleanup of addresses created n days ago, cleanup of addresses inactive for n days
|
||||
- fix: |IMAP Proxy| Fix IMAP Proxy server unable to view new emails issue
|
||||
|
||||
## v0.10.0
|
||||
|
||||
- feat: Support User inbox viewing, `/user_api/mails` interface, support `address` and `keyword` filtering
|
||||
- fix: Fix Oauth2 login token retrieval, some Oauth2 require `redirect_uri` parameter issue
|
||||
- feat: When user accesses webpage, if `user token` expires within 7 days, auto refresh
|
||||
- feat: Add db initialization feature to admin portal
|
||||
- feat: Add `ALWAYS_SHOW_ANNOUNCEMENT` variable to configure whether to always show announcements
|
||||
|
||||
## v0.9.1
|
||||
|
||||
- feat: |UI| Support google ads
|
||||
- feat: |UI| Use shadow DOM to prevent style pollution
|
||||
- feat: |UI| Support URL jwt parameter auto-login to mailbox, jwt parameter overrides browser jwt
|
||||
- fix: |CleanUP| Fix cleanup emails when cleanup time exceeds 30 days error bug
|
||||
- feat: Admin user management page: add user address viewing feature
|
||||
- feat: | S3 Attachments| Add S3 attachment deletion feature
|
||||
- feat: | Admin API| Add admin bind user and address api
|
||||
- feat: | Oauth2 | When Oauth2 gets user info, support `JSONPATH` expressions
|
||||
|
||||
## v0.9.0
|
||||
|
||||
- feat: | Worker | Support multi-language
|
||||
- feat: | Worker | `NO_LIMIT_SEND_ROLE` configuration supports multiple roles, comma separated
|
||||
- feat: | Actions | Add `worker-with-wasm-mail-parser.zip` in build to support UI deployment with `wasm` worker
|
||||
|
||||
## v0.8.7
|
||||
|
||||
- fix: |UI| Fix mobile device date display issue
|
||||
- feat: |Worker| Support sending emails via `SMTP`, using [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md)
|
||||
|
||||
## v0.8.6
|
||||
|
||||
- feat: |UI| Announcements support html format
|
||||
- feat: |UI| `COPYRIGHT` supports html format
|
||||
- feat: |Doc| Optimize deployment documentation, supplement `Github Actions Deployment Documentation`, add `Worker Variable Description`
|
||||
|
||||
## v0.8.5
|
||||
|
||||
- feat: |mail-parser-wasm-worker| Fix `deprecated` parameter warning when calling `initSync` function
|
||||
- feat: rpc headers convert & typo (#559)
|
||||
- fix: telegram mail page use iframe show email (#561)
|
||||
- feat: |Worker| Add `REMOVE_ALL_ATTACHMENT` and `REMOVE_EXCEED_SIZE_ATTACHMENT` for removing email attachments, due to parsing emails some information will be lost, such as images.
|
||||
|
||||
## v0.8.4
|
||||
|
||||
- fix: |UI| Fix admin portal delete call api error when no recipient email
|
||||
- feat: |Telegram Bot| Add telegram bot cleanup invalid address credentials command
|
||||
- feat: Add worker configuration `DISABLE_ANONYMOUS_USER_CREATE_EMAIL` to disable anonymous user email creation, only allow logged-in users to create email addresses
|
||||
- feat: Add worker configuration `ENABLE_ANOTHER_WORKER` and `ANOTHER_WORKER_LIST`, for calling other worker rpc interfaces (#547)
|
||||
- feat: |UI| Auto refresh configuration saved to browser, configurable refresh interval
|
||||
- feat: Spam detection adds check-when-exists list `JUNK_MAIL_CHECK_LIST` configuration
|
||||
- feat: | Worker | Add `ParsedEmailContext` class for caching parsed email content, reduce parsing times
|
||||
- feat: |Github Action| Worker deployment adds `DEBUG_MODE` output logging, `BACKEND_USE_MAIL_WASM_PARSER` configuration for whether to use wasm to parse emails
|
||||
|
||||
## v0.8.3
|
||||
|
||||
- feat: |Github Action| Add auto update and deploy feature
|
||||
- feat: |UI| Admin user settings, support oauth2 configuration deletion
|
||||
- feat: Add spam detection must-pass list `JUNK_MAIL_FORCE_PASS_LIST` configuration
|
||||
|
||||
## v0.8.2
|
||||
|
||||
- fix: |Doc| Fix some documentation errors
|
||||
- fix: |Github Action| Fix frontend deployment branch error issue
|
||||
- feat: Admin send email feature
|
||||
- feat: Admin backend, account configuration page adds unlimited send email address list
|
||||
|
||||
## v0.8.1
|
||||
|
||||
- feat: |Doc| Update UI installation documentation
|
||||
- feat: |UI| Hide mailbox account ID from users
|
||||
- feat: |UI| Add `Forward` button to email detail page
|
||||
|
||||
## v0.8.0
|
||||
|
||||
- feat: |UI| Random address generation doesn't exceed max length
|
||||
- feat: |UI| Email time display in browser timezone, can switch to display UTC time in settings
|
||||
- feat: Support transferring emails to other users
|
||||
|
||||
## v0.7.6
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
UI deployment worker needs to click Settings -> Runtime, modify Compatibility flags, add `nodejs_compat`
|
||||
|
||||

|
||||
|
||||
### Changes
|
||||
|
||||
- feat: Support pre-setting bot info to reduce telegram callback latency (#441)
|
||||
- feat: Add telegram mini app build archive
|
||||
- feat: Add whether to enable spam check `ENABLE_CHECK_JUNK_MAIL` configuration
|
||||
|
||||
## v0.7.5
|
||||
|
||||
- fix: Fix `name` validation check
|
||||
|
||||
## v0.7.4
|
||||
|
||||
- feat: UI list page adds minimum width
|
||||
- fix: Fix `name` validation check
|
||||
- fix: Fix `DEFAULT_DOMAINS` configuration empty not taking effect issue
|
||||
|
||||
## v0.7.3
|
||||
|
||||
- feat: Worker adds `ADDRESS_CHECK_REGEX`, address name regex, only for checking, matching will pass check
|
||||
- fix: UI fix login page tab active icon misalignment
|
||||
- fix: UI fix admin page refresh popup password input issue
|
||||
- feat: Support `OAuth2` login, can login via `Github` `Authentik` and other third parties, see details [OAuth2 Third-party Login](https://temp-mail-docs.awsl.uk/en/guide/feature/user-oauth2.html)
|
||||
|
||||
## v0.7.2
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
`webhook` structure adds `enabled` field, existing configurations need to be re-enabled and saved on the page.
|
||||
|
||||
### Changes
|
||||
|
||||
- fix: Worker adds `NO_LIMIT_SEND_ROLE` configuration, loading failure issue
|
||||
- feat: Worker adds `# ADDRESS_REGEX = "[^a-z.0-9]"` configuration, regex for replacing illegal symbols, if not set, defaults to [^a-z0-9], use with caution, some symbols may cause receiving issues
|
||||
- feat: Worker optimizes webhook logic, supports admin configuring global webhook, adds `message pusher` integration example
|
||||
|
||||
## v0.7.1
|
||||
|
||||
- fix: Fix user role loading failure issue
|
||||
- feat: Admin account settings adds source email address blacklist configuration
|
||||
|
||||
## v0.7.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
DB changes: Add user `passkey` table, need to execute `db/2024-08-10-patch.sql` to update `D1` database
|
||||
|
||||
### Changes
|
||||
|
||||
- Docs: Update new-address-api.md (#360)
|
||||
- feat: Worker adds `ADMIN_USER_ROLE` configuration, for configuring admin user role, users with this role can access admin management page (#363)
|
||||
- feat: Worker adds `DISABLE_SHOW_GITHUB` configuration, for configuring whether to show github link
|
||||
- feat: Worker adds `NO_LIMIT_SEND_ROLE` configuration, for configuring roles that can send unlimited emails
|
||||
- feat: User adds `passkey` login method, for user login, no password required
|
||||
- feat: Worker adds `DISABLE_ADMIN_PASSWORD_CHECK` configuration, for configuring whether to disable admin console password check, if your site is only privately accessible, you can disable the check
|
||||
|
||||
## v0.6.1
|
||||
|
||||
- pages github actions && fix cleanup emails days 0 not taking effect by @tqjason (#355)
|
||||
- fix: imap proxy server doesn't support password by @dreamhunter2333 (#356)
|
||||
- worker adds `ANNOUNCEMENT` configuration, for configuring announcement info by @dreamhunter2333 (#357)
|
||||
- fix: telegram bot create new address defaults to first domain by @dreamhunter2333 (#358)
|
||||
|
||||
## v0.6.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
DB changes: Add user role table, need to execute `db/2024-07-14-patch.sql` to update `D1` database
|
||||
|
||||
### Changes
|
||||
|
||||
Worker configuration file adds `DEFAULT_DOMAINS`, `USER_ROLES`, `USER_DEFAULT_ROLE`, see documentation [worker configuration](https://temp-mail-docs.awsl.uk/en/guide/cli/worker.html)
|
||||
|
||||
- Remove `apiV1` related code and related database tables
|
||||
- Update `admin/statistics` api, add user statistics info
|
||||
- Update address rules, only allow lowercase+numbers, for historical addresses `lowercase` processing will be performed when querying emails
|
||||
- Add user role feature, `admin` can set user roles (currently can configure domain and prefix for each role)
|
||||
- Admin page search optimization, enter key auto search, input content auto trim
|
||||
|
||||
## v0.5.4
|
||||
|
||||
- Click logo 5 times to enter admin page
|
||||
- Fix 401 cannot redirect to login page (admin and site authentication)
|
||||
|
||||
## v0.5.3
|
||||
|
||||
- Fix some bugs in smtp imap proxy server
|
||||
- Improve user/admin delete inbox/outbox functionality
|
||||
- Admin can delete send permission records
|
||||
- Add Chinese email alias configuration `DOMAIN_LABELS` [documentation](https://temp-mail-docs.awsl.uk/en/guide/cli/worker.html)
|
||||
- Remove `mail channels` related code
|
||||
- github actions adds `FRONTEND_BRANCH` variable to specify deployment branch (#324)
|
||||
|
||||
## v0.5.1
|
||||
|
||||
- Add `mail-parser-wasm-worker` for worker email parsing, [documentation](https://temp-mail-docs.awsl.uk/en/guide/feature/mail_parser_wasm_worker.html)
|
||||
- Add user email length validation configuration `MIN_ADDRESS_LEN` and `MAX_ADDRESS_LEN`
|
||||
- Fix `pages function` not forwarding `telegram` api issue
|
||||
|
||||
## v0.5.0
|
||||
|
||||
- UI: Add local cache for address management
|
||||
- worker: Add `FORWARD_ADDRESS_LIST` global email forwarding address (equivalent to `catch all`)
|
||||
- UI: Multi-language uses routing for switching
|
||||
- Add save attachments to S3 feature
|
||||
- UI: Add received email list `batch delete` and `batch download`
|
||||
|
||||
## v0.4.6
|
||||
|
||||
- Worker configuration file adds `TITLE = "Custom Title"`, can customize website title
|
||||
- Fix KV not bound unable to delete address issue
|
||||
|
||||
## v0.4.5
|
||||
|
||||
- UI lazy load
|
||||
- telegram bot adds user global push feature (admin users)
|
||||
- Add support for cloudflare verified user sending emails
|
||||
- Add using `resend` to send emails, `resend` provides http and smtp api, easier to use, documentation: https://temp-mail-docs.awsl.uk/en/guide/config-send-mail.html
|
||||
|
||||
## v0.4.4
|
||||
|
||||
- Add telegram mini app
|
||||
- telegram bot adds `unbind`, `delete` commands
|
||||
- Fix webhook multiline text issue
|
||||
|
||||
## v0.4.3
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
Configuration file `main = "src/worker.js"` changed to `main = "src/worker.ts"`
|
||||
|
||||
### Changes
|
||||
|
||||
- `telegram bot` whitelist configuration
|
||||
- `ENABLE_WEBHOOK` add webhook
|
||||
- UI: admin page uses two-level tabs
|
||||
- UI: can directly switch addresses on homepage after login
|
||||
- UI: outbox also uses split view display (similar to inbox)
|
||||
- `SMTP IMAP Proxy` add outbox viewing
|
||||
|
||||
* feat: telegram bot TelegramSettings && webhook by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/244
|
||||
* fix build by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/245
|
||||
* feat: UI changes by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/247
|
||||
* feat: SMTP IMAP Proxy: add sendbox && UI: sendbox use split view by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/248
|
||||
|
||||
## v0.4.2
|
||||
|
||||
- Fix some bugs in smtp imap proxy server
|
||||
- Fix UI interface text errors, interface adds version number
|
||||
- Add telegram bot documentation https://temp-mail-docs.awsl.uk/en/guide/feature/telegram.html
|
||||
|
||||
* fix: imap server by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/227
|
||||
* fix: Maintenance wrong label by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/229
|
||||
* feat: add version for frontend && backend by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/230
|
||||
* feat: add page functions proxy to make response faster by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/234
|
||||
* feat: add about page by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/235
|
||||
* feat: remove mailV1Alert && fix mobile showSideMargin by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/236
|
||||
* feat: telegram bot by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/238
|
||||
* fix: remove cleanup address due to many table need to be clean by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/240
|
||||
* feat: docs: Telegram Bot by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/241
|
||||
* fix: smtp_proxy: cannot decode 8bit && tg bot new random address by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/242
|
||||
* fix: smtp_proxy: update raise imap4.NoSuchMailbox by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/243
|
||||
|
||||
### v0.4.1
|
||||
|
||||
- Username limited to max 30 characters
|
||||
- Fix `/external/api/send_mail` not returning bug (#222)
|
||||
- Add `IMAP proxy` service, support `IMAP` viewing emails
|
||||
- UI interface adds version number display
|
||||
|
||||
* feat: use common function handleListQuery when query by page by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/220
|
||||
* fix: typos by @lwd-temp in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/221
|
||||
* fix: name max 30 && /external/api/send_mail not return result by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/222
|
||||
* fix: smtp_proxy_server support decode from mail charset by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/223
|
||||
* feat: add imap proxy server by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/225
|
||||
* feat: UI show version by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/226
|
||||
|
||||
### New Contributors
|
||||
|
||||
* @lwd-temp made their first contribution in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/221
|
||||
|
||||
## v0.4.0
|
||||
|
||||
### DB Changes/Breaking changes
|
||||
|
||||
Added user related tables for storing user information
|
||||
|
||||
- `db/2024-05-08-patch.sql`
|
||||
|
||||
### config changes
|
||||
|
||||
Enable user registration email verification requires `KV`
|
||||
|
||||
```toml
|
||||
# kv config for send email verification code
|
||||
# [[kv_namespaces]]
|
||||
# binding = "KV"
|
||||
# id = "xxxx"
|
||||
```
|
||||
|
||||
### function changes
|
||||
|
||||
- Add user registration feature, can bind email addresses, automatically obtain email JWT credentials after binding
|
||||
- Add default text display for emails, text and HTML email display mode switch button
|
||||
- Fix `BUG` randomly generated email names are invalid #211
|
||||
- `admin` email page supports email content search #210
|
||||
- Fix bug where emails weren't deleted when deleting addresses #213
|
||||
- UI adds global tab position configuration, side margin configuration
|
||||
|
||||
* feat: update docs by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/204
|
||||
* feat: add Deploy to Cloudflare Workers button by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/205
|
||||
* feat: add Deploy to Cloudflare Workers docs by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/206
|
||||
* feat: add UserLogin by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/209
|
||||
* feat: admin search mailbox && fix generateName multi dot && user jwt exp in 30 days && UI globalTabplacement && useSideMargin by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/214
|
||||
* feat: UI check openSettings in Login page by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/215
|
||||
* feat: UI move AdminContact to common by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/217
|
||||
* feat: docs by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/218
|
||||
|
||||
## v0.3.3
|
||||
|
||||
- Fix Admin delete email error
|
||||
- UI: Reply email button, quote original email text #186
|
||||
- Add send email address blacklist
|
||||
- Add `CF Turnstile` CAPTCHA configuration
|
||||
- Add `/external/api/send_mail` send email api, use body verification #194
|
||||
|
||||
## v0.3.2
|
||||
|
||||
## What's Changed
|
||||
|
||||
- UI: Add reply email button
|
||||
- Add scheduled cleanup feature, configurable in admin page (need to enable scheduled task in config file)
|
||||
- Fix delete account no response issue
|
||||
|
||||
* feat: UI: MailBox add reply button by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/187
|
||||
* feat: add cron auto clean up by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/189
|
||||
* fix: delete account by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/190
|
||||
|
||||
## v0.3.1
|
||||
|
||||
### DB Changes
|
||||
|
||||
Added `settings` table for storing general configuration information
|
||||
|
||||
- `db/2024-05-01-patch.sql`
|
||||
|
||||
### Changes
|
||||
|
||||
- `ENABLE_USER_CREATE_EMAIL` whether to allow users to create emails
|
||||
- Allow admin to create emails without prefix
|
||||
- Add `SMTP proxy server`, support SMTP sending emails
|
||||
- Fix some cases where browsers can't load `wasm` use js to parse emails
|
||||
- Footer adds `COPYRIGHT`
|
||||
- UI allows users to switch email display mode `v-html` / `iframe`
|
||||
- Add `admin` account configuration page, support configuring user registration name blacklist
|
||||
|
||||
* feat: support admin create address && add ENABLE_USER_CREATE_EMAIL co… by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/175
|
||||
* feat: add SMTP proxy server by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/177
|
||||
* fix: cf ui var is string by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/178
|
||||
* fix: UI mailbox 100vh to 80vh by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/179
|
||||
* fix: smtp_proxy_server hostname && add docker image for linux/arm64 by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/180
|
||||
* fix: some browser do not support wasm by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/182
|
||||
* feat: add COPYRIGHT by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/183
|
||||
* feat: UI: add user page: useIframeShowMail && mailboxSplitSize by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/184
|
||||
* feat: add address_block_list for new address by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/185
|
||||
|
||||
## v0.3.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
The prefix of the `address` table will migrate from code to db, please replace `tmp` in the sql below with your prefix, then execute.
|
||||
If your data is important, please backup your database first.
|
||||
|
||||
**Note: Replace prefix**
|
||||
|
||||
```sql
|
||||
update
|
||||
address
|
||||
set
|
||||
name = 'tmp' || name;
|
||||
```
|
||||
|
||||
### Changes
|
||||
|
||||
- Migrate the prefix of the `address` table from code to db
|
||||
- `admin` account page adds send/receive email counts
|
||||
- `admin` outbox page defaults to show all
|
||||
- `admin` send permission page supports search by address
|
||||
- `admin` email page uses split view UI
|
||||
|
||||
* feat: remove PREFIX logic in db by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/171
|
||||
* feat: admin page add account mail count && sendbox default all && sen… by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/172
|
||||
* feat: all mail use MailBox Component by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/173
|
||||
|
||||
**Full Changelog**: https://github.com/dreamhunter2333/cloudflare_temp_email/compare/0.2.10...v0.3.0
|
||||
|
||||
## v0.2.10
|
||||
|
||||
- `ENABLE_USER_DELETE_EMAIL` whether to allow users to delete account and emails
|
||||
- `ENABLE_AUTO_REPLY` whether to enable auto reply
|
||||
- fetchAddressError prompt improvement
|
||||
- Auto refresh shows countdown
|
||||
|
||||
* feat: docs update by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/165
|
||||
* feat: add ENABLE_USER_DELETE_EMAIL && ENABLE_AUTO_REPLY && modify fetchAddressError i18n && UI: show autoRefreshInterval by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/169
|
||||
|
||||
## v0.2.9
|
||||
|
||||
- Add rich text editor
|
||||
- Admin contact info, won't show if not configured, can configure any string `ADMIN_CONTACT = "xx@xx.xxx"`
|
||||
- Default send email balance, if not set, will be 0 `DEFAULT_SEND_BALANCE = 1`
|
||||
|
||||
## v0.2.8
|
||||
|
||||
- Allow users to delete emails
|
||||
- Admin notifies user by email when modifying send permissions
|
||||
- Send permission defaults to 1
|
||||
- Add RATE_LIMITER rate limiting for sending emails and creating new addresses
|
||||
- Some bug fixes
|
||||
|
||||
- feat: allow user delete mail && notify when send access changed by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/132
|
||||
- feat: request_send_mail_access default 1 balance by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/143
|
||||
- fix: RATE_LIMITER not call jwt by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/146
|
||||
- fix: delete_address not delete address_sender by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/153
|
||||
- fix: send_balance not update when click sendmail by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/155
|
||||
|
||||
## v0.2.7
|
||||
|
||||
- Added user interface installation documentation
|
||||
- Support email DKIM
|
||||
- Rate limiting configuration for `/api/new_address`
|
||||
|
||||
## v0.2.6
|
||||
|
||||
- Added admin query outbox page
|
||||
- Add admin data cleaning page
|
||||
|
||||
## 2024-04-12 v0.2.5
|
||||
|
||||
- Support send email
|
||||
|
||||
DB changes:
|
||||
|
||||
- `db/2024-04-12-patch.sql`
|
||||
|
||||
## 2024-04-10 v0.2.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- remove `ENABLE_ATTACHMENT` config
|
||||
- use rust wasm to parse email in frontend
|
||||
- deprecated api moved to `/api/v1`
|
||||
|
||||
### Rust Mail Parser
|
||||
|
||||
Due to some problems with nodejs' email parsing library, this version switches to using rust wasm to call rust's mail parsing library.
|
||||
|
||||
- Faster speed, good attachment support, can display attachment images of emails
|
||||
- Parsing supports more rfc specifications
|
||||
|
||||
### DB changes
|
||||
|
||||
The `mails` table will be discarded, and the `raw` text of the new `mail` will be directly stored in the `raw_mails` table
|
||||
|
||||
## Upgrade Step
|
||||
|
||||
```bash
|
||||
git checkout v0.2.0
|
||||
cd worker
|
||||
wrangler d1 execute dev --file=../db/2024-04-09-patch.sql --remote
|
||||
pnpm run deploy
|
||||
cd ../frontend
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
Note: For historical messages, use the Deploy New web page to view old data.
|
||||
|
||||
```bash
|
||||
git checkout feature/backup
|
||||
cd frontend
|
||||
# Create a new pages for accessing old data
|
||||
pnpm run deploy --project-name temp-email-v1
|
||||
```
|
||||
|
||||
## 2024-04-09 v0.0.0
|
||||
|
||||
release v0.0.0
|
||||
|
||||
## 2024-04-03
|
||||
|
||||
DB changes
|
||||
|
||||
- `db/2024-04-03-patch.sql`
|
||||
|
||||
Changes:
|
||||
|
||||
- add delete account
|
||||
- add admin panel search
|
||||
|
||||
## 2024-01-13
|
||||
|
||||
DB changes
|
||||
|
||||
- `db/2024-01-13-patch.sql`
|
||||
10
README.md
10
README.md
@@ -110,7 +110,7 @@
|
||||
- [x] 使用 `rust wasm` 解析邮件,解析速度快,几乎所有邮件都能解析,node 的解析模块解析邮件失败的邮件,rust wasm 也能解析成功
|
||||
- [x] **AI 邮件识别** - 使用 Cloudflare Workers AI 自动提取邮件中的验证码、认证链接、服务链接等重要信息
|
||||
- [x] 支持发送邮件,支持 `DKIM` 验证
|
||||
- [x] 支持 `SMTP` 和 `Resend` 等多种发送方式
|
||||
- [x] 支持 `SMTP` 和 `Resend` 等多种发送方式
|
||||
- [x] 增加查看 `附件` 功能,支持附件图片显示
|
||||
- [x] 支持 S3 附件存储和删除功能
|
||||
- [x] 垃圾邮件检测和黑白名单配置
|
||||
@@ -184,6 +184,14 @@
|
||||
|
||||
</details>
|
||||
|
||||
### 提醒
|
||||
|
||||
- 在Resend添加域名记录时,如果您域名解析服务商正在托管您的3级域名a.b.com,请删除Resend生成的默认name中二级域名前缀b,否则将会添加a.b.b.com,导致验证失败。添加记录后,可通过
|
||||
```bash
|
||||
nslookup -qt="mx" a.b.com 1.1.1.1
|
||||
```
|
||||
进行验证。
|
||||
|
||||
## 🌟 加入社区
|
||||
|
||||
- [Telegram](https://t.me/cloudflare_temp_email)
|
||||
|
||||
216
README_EN.md
216
README_EN.md
@@ -1,46 +1,196 @@
|
||||
<!-- markdownlint-disable-file MD033 MD045 -->
|
||||
# Cloudflare Temp Email
|
||||
# Cloudflare Temp Email - Free Temporary Email Service
|
||||
|
||||
<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 href="https://temp-mail-docs.awsl.uk" target="_blank">
|
||||
<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>
|
||||
</p>
|
||||
|
||||
## ✨ Key Features
|
||||
<p align="center">
|
||||
<a href="https://hellogithub.com/repository/2ccc64bb1ba346b480625f584aa19eb1" target="_blank">
|
||||
<img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=2ccc64bb1ba346b480625f584aa19eb1&claim_uid=FxNypXK7UQ9OECT" alt="Featured|HelloGitHub" height="30"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
- **📧 Email Processing**: Rust WASM parser, **AI email extraction** (verification codes, auth links), 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
|
||||
- **<2A> Modern UI**: Multi-language, responsive design, JWT auto-login
|
||||
<p align="center">
|
||||
<a href="README.md">🇨🇳 中文文档</a> |
|
||||
<a href="README_EN.md">🇺🇸 English Document</a>
|
||||
</p>
|
||||
|
||||
## 🏗️ Tech Stack
|
||||
> This project is for learning and personal use only. Please do not use it for any illegal activities, or you will be responsible for the consequences.
|
||||
|
||||
- **Frontend**: Vue 3 + TypeScript + Vite
|
||||
- **Backend**: Cloudflare Workers + D1 Database
|
||||
- **Email**: Cloudflare Email Routing + Rust WASM Parser
|
||||
**🎉 A fully-featured temporary email service!**
|
||||
|
||||
- 🆓 **Completely Free** - Built on Cloudflare's free services with zero cost
|
||||
- ⚡ **High Performance** - Rust WASM email parsing for extremely fast response
|
||||
- 🎨 **Modern UI** - Responsive design with multi-language support and easy operation
|
||||
- 🔐 **Address Password** - Support setting individual passwords for email addresses to enhance security (enabled via `ENABLE_ADDRESS_PASSWORD`)
|
||||
|
||||
## 📚 Deployment Documentation - Quick Start
|
||||
|
||||
[📖 Documentation](https://temp-mail-docs.awsl.uk) | [🚀 Github Action Deployment Guide](https://temp-mail-docs.awsl.uk/en/guide/actions/github-action.html)
|
||||
|
||||
<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" height="32">
|
||||
</a>
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
See [CHANGELOG](CHANGELOG.md) for the latest updates.
|
||||
|
||||
## 🎯 Live Demo
|
||||
|
||||
Try it now → [https://mail.awsl.uk/](https://mail.awsl.uk/)
|
||||
|
||||
<details>
|
||||
<summary>📊 Service Status Monitoring (Click to expand/collapse)</summary>
|
||||
|
||||
| | |
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Backend](https://temp-email-api.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml)       |
|
||||
| [Frontend](https://mail.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml)       |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>⭐ Star History (Click to expand/collapse)</summary>
|
||||
|
||||
<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: 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" />
|
||||
</picture>
|
||||
|
||||
</details>
|
||||
|
||||
<details open>
|
||||
<summary>📖 Table of Contents (Click to expand/collapse)</summary>
|
||||
|
||||
- [Cloudflare Temp Email - Free Temporary Email Service](#cloudflare-temp-email---free-temporary-email-service)
|
||||
- [📚 Deployment Documentation - Quick Start](#-deployment-documentation---quick-start)
|
||||
- [📝 Changelog](#-changelog)
|
||||
- [🎯 Live Demo](#-live-demo)
|
||||
- [✨ Core Features](#-core-features)
|
||||
- [📧 Email Processing](#-email-processing)
|
||||
- [👥 User Management](#-user-management)
|
||||
- [🔧 Admin Features](#-admin-features)
|
||||
- [🌐 Multi-language \& Interface](#-multi-language--interface)
|
||||
- [🤖 Integration \& Extensions](#-integration--extensions)
|
||||
- [🏗️ Technical Architecture](#️-technical-architecture)
|
||||
- [🏛️ System Architecture](#️-system-architecture)
|
||||
- [🛠️ Tech Stack](#️-tech-stack)
|
||||
- [📦 Main Components](#-main-components)
|
||||
- [🌟 Join the Community](#-join-the-community)
|
||||
|
||||
</details>
|
||||
|
||||
## ✨ Core Features
|
||||
|
||||
<details open>
|
||||
<summary>✨ Core Features Details (Click to expand/collapse)</summary>
|
||||
|
||||
### 📧 Email Processing
|
||||
|
||||
- [x] Use `rust wasm` to parse emails, with fast parsing speed. Almost all emails can be parsed. Even emails that Node.js parsing modules fail to parse can be successfully parsed by rust wasm
|
||||
- [x] **AI Email Recognition** - Use Cloudflare Workers AI to automatically extract verification codes, authentication links, service links and other important information from emails
|
||||
- [x] Support sending emails with `DKIM` verification
|
||||
- [x] Support multiple sending methods such as `SMTP` and `Resend`
|
||||
- [x] Add attachment viewing feature with support for displaying attachment images
|
||||
- [x] Support S3 attachment storage and deletion
|
||||
- [x] Spam detection and blacklist/whitelist configuration
|
||||
- [x] Email forwarding feature with global forwarding address support
|
||||
|
||||
### 👥 User Management
|
||||
|
||||
- [x] Use `credentials` to log in to previously used mailboxes
|
||||
- [x] Add complete user registration and login functionality. Users can bind email addresses and automatically obtain email JWT credentials to switch between different mailboxes after binding
|
||||
- [x] Support `OAuth2` third-party login (Github, Authentik, etc.)
|
||||
- [x] Support `Passkey` passwordless login
|
||||
- [x] User role management with support for multi-role domain and prefix configuration
|
||||
- [x] User inbox viewing with address and keyword filtering support
|
||||
|
||||
### 🔧 Admin Features
|
||||
|
||||
- [x] Complete admin console
|
||||
- [x] Create mailboxes without prefix in `admin` backend
|
||||
- [x] Admin user management page with user address viewing feature
|
||||
- [x] Scheduled cleanup function with support for multiple cleanup strategies
|
||||
- [x] Get mailboxes with custom names, `admin` can configure blacklist
|
||||
- [x] Add access password for use as a private site
|
||||
|
||||
### 🌐 Multi-language & Interface
|
||||
|
||||
- [x] Both frontend and backend support multi-language
|
||||
- [x] Modern UI design with responsive layout
|
||||
- [x] Google Ads integration support
|
||||
- [x] Use shadow DOM to prevent style pollution
|
||||
- [x] Support URL JWT parameter auto-login
|
||||
|
||||
### 🤖 Integration & Extensions
|
||||
|
||||
- [x] Complete `Telegram Bot` support, `Telegram` push notifications, and Telegram Bot mini app
|
||||
- [x] Add `SMTP proxy server` supporting `SMTP` for sending emails and `IMAP` for viewing emails
|
||||
- [x] Webhook support and message push integration
|
||||
- [x] Support `CF Turnstile` CAPTCHA verification
|
||||
- [x] Rate limiting configuration to prevent abuse
|
||||
|
||||
</details>
|
||||
|
||||
## 🏗️ Technical Architecture
|
||||
|
||||
<details>
|
||||
<summary>🏗️ Technical Architecture Details (Click to expand/collapse)</summary>
|
||||
|
||||
### 🏛️ System Architecture
|
||||
|
||||
- **Database**: Cloudflare D1 as the main database
|
||||
- **Frontend Deployment**: Deploy frontend using Cloudflare Pages
|
||||
- **Backend Deployment**: Deploy backend using Cloudflare Workers
|
||||
- **Email Routing**: Use Cloudflare Email Routing
|
||||
|
||||
### 🛠️ Tech Stack
|
||||
|
||||
- **Frontend**: Vue 3 + Vite + TypeScript
|
||||
- **Backend**: TypeScript + Cloudflare Workers
|
||||
- **Email Parsing**: Rust WASM (mail-parser-wasm)
|
||||
- **Database**: Cloudflare D1 (SQLite)
|
||||
- **Storage**: Cloudflare KV + R2 (optional S3)
|
||||
- **Proxy Service**: Python SMTP/IMAP Proxy Server
|
||||
|
||||
## 🌟 Community
|
||||
### 📦 Main Components
|
||||
|
||||
- **Worker**: Core backend service
|
||||
- **Frontend**: Vue 3 user interface
|
||||
- **Mail Parser WASM**: Rust email parsing module
|
||||
- **SMTP Proxy Server**: Python email proxy service
|
||||
- **Pages Functions**: Cloudflare Pages middleware
|
||||
- **Documentation**: VitePress documentation site
|
||||
|
||||
</details>
|
||||
|
||||
### Important Notes
|
||||
|
||||
- When adding domain records in Resend, if your DNS provider is hosting your 3rd level domain a.b.com, please remove the 2nd level domain prefix b from the default name generated by Resend, otherwise it will add a.b.b.com, causing verification to fail. After adding the record, you can verify it using:
|
||||
```bash
|
||||
nslookup -qt="mx" a.b.com 1.1.1.1
|
||||
```
|
||||
|
||||
## 🌟 Join the Community
|
||||
|
||||
- [Telegram](https://t.me/cloudflare_temp_email)
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
||||
4
db/2025-12-15-message-id-index.sql
Normal file
4
db/2025-12-15-message-id-index.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- Add index on message_id column in raw_mails table
|
||||
-- This index improves performance for queries filtering/updating by message_id
|
||||
-- Example: UPDATE raw_mails SET metadata = ? WHERE message_id = ?
|
||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_message_id ON raw_mails(message_id);
|
||||
8
db/2025-12-27-source-meta.sql
Normal file
8
db/2025-12-27-source-meta.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- Add source_meta column to address table for tracking address creation source
|
||||
-- For web: stores IP address (e.g., "192.168.1.1") or "web:unknown" as fallback
|
||||
-- For telegram: stores "tg:{userId}" (e.g., "tg:123456789")
|
||||
-- For admin: stores "admin"
|
||||
|
||||
ALTER TABLE address ADD COLUMN source_meta TEXT;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_address_source_meta ON address(source_meta);
|
||||
@@ -12,10 +12,13 @@ CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_created_at ON raw_mails(created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_message_id ON raw_mails(message_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS address (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE,
|
||||
password TEXT,
|
||||
source_meta TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -26,6 +29,8 @@ CREATE INDEX IF NOT EXISTS idx_address_created_at ON address(created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_address_updated_at ON address(updated_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_address_source_meta ON address(source_meta);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
||||
id INTEGER PRIMARY KEY,
|
||||
source_prefix TEXT,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -22,34 +22,34 @@
|
||||
"dependencies": {
|
||||
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
||||
"@simplewebauthn/browser": "10.0.0",
|
||||
"@unhead/vue": "^1.11.20",
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"@unhead/vue": "^2.1.2",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.13.2",
|
||||
"jszip": "^3.10.1",
|
||||
"mail-parser-wasm": "^0.2.1",
|
||||
"naive-ui": "^2.43.2",
|
||||
"postal-mime": "^2.6.1",
|
||||
"postal-mime": "^2.7.3",
|
||||
"vooks": "^0.2.12",
|
||||
"vue": "^3.5.25",
|
||||
"vue": "^3.5.27",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-router": "^4.6.3"
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vicons/fa": "^0.13.0",
|
||||
"@vicons/material": "^0.13.0",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"unplugin-auto-import": "^19.3.0",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^6.4.1",
|
||||
"@vitejs/plugin-vue": "^6.0.3",
|
||||
"unplugin-auto-import": "^20.3.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
"workbox-build": "^7.4.0",
|
||||
"workbox-window": "^7.4.0",
|
||||
"wrangler": "^4.53.0"
|
||||
"wrangler": "^4.59.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
2691
frontend/pnpm-lock.yaml
generated
2691
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,15 @@ const showSideMargin = computed(() => !isMobile.value && useSideMargin.value);
|
||||
const showAd = computed(() => !isMobile.value && adClient && adSlot);
|
||||
const gridMaxCols = computed(() => showAd.value ? 8 : 12);
|
||||
|
||||
// Load Google Ad script at top level (not inside onMounted)
|
||||
if (showAd.value) {
|
||||
useScript({
|
||||
src: `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${adClient}`,
|
||||
async: true,
|
||||
crossorigin: "anonymous",
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await api.getUserSettings();
|
||||
@@ -42,11 +51,6 @@ onMounted(async () => {
|
||||
|
||||
// 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({});
|
||||
}
|
||||
|
||||
260
frontend/src/components/AddressSelect.vue
Normal file
260
frontend/src/components/AddressSelect.vue
Normal file
@@ -0,0 +1,260 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
import { useLocalStorage } from '@vueuse/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import useClipboard from 'vue-clipboard3'
|
||||
import { Copy } from '@vicons/fa'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
|
||||
const props = defineProps({
|
||||
showCopy: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
},
|
||||
})
|
||||
|
||||
const message = useMessage()
|
||||
const { toClipboard } = useClipboard()
|
||||
|
||||
const {
|
||||
jwt, settings, userJwt, isTelegram, openSettings, telegramApp
|
||||
} = useGlobalState()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
userAddresses: 'User Addresses',
|
||||
localAddresses: 'Local Addresses',
|
||||
address: 'Address',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
},
|
||||
zh: {
|
||||
userAddresses: '用户地址',
|
||||
localAddresses: '本地地址',
|
||||
address: '地址',
|
||||
copy: '复制',
|
||||
copied: '已复制',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const addressOptions = ref([])
|
||||
const addressValue = ref(null)
|
||||
const addressLoading = ref(false)
|
||||
const localAddressCache = useLocalStorage("LocalAddressCache", [])
|
||||
const optionValueMap = new Map()
|
||||
|
||||
const formatAddressLabel = (address) => {
|
||||
if (!address) return address;
|
||||
const domain = address.split('@')[1]
|
||||
const domainLabel = openSettings.value.domains.find(
|
||||
d => d.value === domain
|
||||
)?.label;
|
||||
if (!domainLabel) return address;
|
||||
return address.replace('@' + domain, `@${domainLabel}`);
|
||||
}
|
||||
|
||||
const parseJwtAddress = (curJwt) => {
|
||||
try {
|
||||
const payload = JSON.parse(
|
||||
decodeURIComponent(
|
||||
atob(curJwt.split(".")[1]
|
||||
.replace(/-/g, "+").replace(/_/g, "/")
|
||||
)
|
||||
)
|
||||
);
|
||||
return payload.address;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const getOptionValue = (key, scope, payload, address) => {
|
||||
if (optionValueMap.has(key)) {
|
||||
const cached = optionValueMap.get(key)
|
||||
cached.scope = scope
|
||||
cached.payload = payload
|
||||
cached.address = address
|
||||
return cached
|
||||
}
|
||||
const value = { key, scope, payload, address }
|
||||
optionValueMap.set(key, value)
|
||||
return value
|
||||
}
|
||||
|
||||
const buildLocalOptions = (excludeAddresses = new Set()) => {
|
||||
if (typeof jwt.value === 'string' && jwt.value && !localAddressCache.value.includes(jwt.value)) {
|
||||
localAddressCache.value.push(jwt.value)
|
||||
}
|
||||
const children = localAddressCache.value
|
||||
.map((curJwt) => {
|
||||
const address = parseJwtAddress(curJwt);
|
||||
if (!address) return null;
|
||||
if (excludeAddresses.has(address)) return null;
|
||||
const label = formatAddressLabel(address);
|
||||
const key = `local:${curJwt}`;
|
||||
const option = { label, value: getOptionValue(key, 'local', curJwt, address), address };
|
||||
if (settings.value.address && address === settings.value.address) {
|
||||
addressValue.value = option.value;
|
||||
}
|
||||
return option;
|
||||
})
|
||||
.filter(Boolean);
|
||||
return children;
|
||||
}
|
||||
|
||||
const buildUserOptions = async () => {
|
||||
const children = [];
|
||||
try {
|
||||
const { results } = await api.fetch(`/user_api/bind_address`);
|
||||
for (const row of results || []) {
|
||||
const address = row.address || row.name;
|
||||
if (!address) continue;
|
||||
const label = formatAddressLabel(address);
|
||||
const key = `user:${row.id}`;
|
||||
const option = { label, value: getOptionValue(key, 'user', String(row.id), address), address };
|
||||
if (settings.value.address && address === settings.value.address) {
|
||||
addressValue.value = option.value;
|
||||
}
|
||||
children.push(option);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
const buildTelegramOptions = async () => {
|
||||
const children = [];
|
||||
try {
|
||||
const data = await api.fetch(`/telegram/get_bind_address`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
initData: telegramApp.value.initData
|
||||
})
|
||||
});
|
||||
for (const row of data || []) {
|
||||
if (!row?.address || !row?.jwt) continue;
|
||||
const label = formatAddressLabel(row.address);
|
||||
const key = `tg:${row.jwt}`;
|
||||
const option = { label, value: getOptionValue(key, 'tg', row.jwt, row.address), address: row.address };
|
||||
if (settings.value.address && row.address === settings.value.address) {
|
||||
addressValue.value = option.value;
|
||||
}
|
||||
children.push(option);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
const refreshAddressOptions = async () => {
|
||||
addressLoading.value = true;
|
||||
addressValue.value = null;
|
||||
try {
|
||||
if (isTelegram.value) {
|
||||
const telegramChildren = await buildTelegramOptions();
|
||||
addressOptions.value = telegramChildren;
|
||||
return;
|
||||
}
|
||||
const groups = [];
|
||||
if (userJwt.value) {
|
||||
const userChildren = await buildUserOptions();
|
||||
if (userChildren.length > 0) {
|
||||
groups.push({ type: 'group', label: t('userAddresses'), children: userChildren });
|
||||
}
|
||||
const userAddressSet = new Set(userChildren.map((item) => item.address));
|
||||
const localChildren = buildLocalOptions(userAddressSet);
|
||||
if (localChildren.length > 0) {
|
||||
groups.push({ type: 'group', label: t('localAddresses'), children: localChildren });
|
||||
}
|
||||
} else {
|
||||
const localChildren = buildLocalOptions();
|
||||
if (localChildren.length > 0) {
|
||||
groups.push({ type: 'group', label: t('localAddresses'), children: localChildren });
|
||||
}
|
||||
}
|
||||
addressOptions.value = groups;
|
||||
} finally {
|
||||
addressLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const onAddressChange = async (value) => {
|
||||
if (!value) return;
|
||||
if (value.scope === 'local' || value.scope === 'tg') {
|
||||
jwt.value = value.payload;
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
if (value.scope === 'user') {
|
||||
try {
|
||||
const res = await api.fetch(`/user_api/bind_address_jwt/${value.payload}`);
|
||||
if (!res?.jwt) {
|
||||
message.error("jwt not found");
|
||||
return;
|
||||
}
|
||||
jwt.value = res.jwt;
|
||||
location.reload();
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
await toClipboard(settings.value.address)
|
||||
message.success(t('copied'));
|
||||
} catch (e) {
|
||||
message.error(e.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshAddressOptions();
|
||||
});
|
||||
|
||||
watch([userJwt, isTelegram, () => settings.value.address], async () => {
|
||||
await refreshAddressOptions();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex class="address-row" align="center" justify="center" :wrap="true">
|
||||
<n-select v-model:value="addressValue" :options="addressOptions" :size="size" filterable
|
||||
:loading="addressLoading" :placeholder="t('address')" @update:value="onAddressChange"
|
||||
class="address-select" />
|
||||
<slot name="actions" />
|
||||
<n-button v-if="showCopy" class="address-copy" @click="copy" :size="size" tertiary type="primary">
|
||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.address-row {
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.address-select {
|
||||
min-width: 220px;
|
||||
max-width: 420px;
|
||||
flex: 1 1 220px;
|
||||
}
|
||||
|
||||
.address-copy {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -3,8 +3,34 @@ import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ContentCopyOutlined, LinkRound, CodeRound } from '@vicons/material';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { useGlobalState } from '../store';
|
||||
|
||||
const message = useMessage();
|
||||
const { isDark } = useGlobalState();
|
||||
|
||||
// Dark mode: use Gmail's softer blue (#A8C7FA) for better readability
|
||||
const alertThemeOverrides = computed(() => {
|
||||
if (isDark.value) {
|
||||
return {
|
||||
colorSuccess: 'rgba(168, 199, 250, 0.15)',
|
||||
borderSuccess: '1px solid rgba(168, 199, 250, 0.3)',
|
||||
iconColorSuccess: '#A8C7FA',
|
||||
titleTextColorSuccess: '#A8C7FA',
|
||||
}
|
||||
}
|
||||
return {}
|
||||
});
|
||||
|
||||
const tagThemeOverrides = computed(() => {
|
||||
if (isDark.value) {
|
||||
return {
|
||||
colorSuccess: 'rgba(168, 199, 250, 0.15)',
|
||||
borderSuccess: '1px solid rgba(168, 199, 250, 0.3)',
|
||||
textColorSuccess: '#A8C7FA',
|
||||
}
|
||||
}
|
||||
return {}
|
||||
});
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
@@ -108,7 +134,7 @@ const openLink = () => {
|
||||
|
||||
<template>
|
||||
<div v-if="aiExtract && aiExtract.result" class="ai-extract-info">
|
||||
<n-alert v-if="!compact" type="success" closable>
|
||||
<n-alert v-if="!compact" type="success" closable :theme-overrides="alertThemeOverrides">
|
||||
<template #icon>
|
||||
<n-icon :component="typeIcon" />
|
||||
</template>
|
||||
@@ -132,7 +158,7 @@ const openLink = () => {
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-alert>
|
||||
<n-tag v-else type="success" @click="copyToClipboard" style="cursor: pointer;" size="small">
|
||||
<n-tag v-else type="success" @click="copyToClipboard" style="cursor: pointer;" size="small" :theme-overrides="tagThemeOverrides">
|
||||
<template #icon>
|
||||
<n-icon :component="typeIcon" />
|
||||
</template>
|
||||
|
||||
@@ -49,20 +49,44 @@ const props = defineProps({
|
||||
default: (mail_id, filename, blob) => { },
|
||||
required: false
|
||||
},
|
||||
showFilterInput: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: false
|
||||
},
|
||||
})
|
||||
|
||||
const localFilterKeyword = ref('')
|
||||
|
||||
const {
|
||||
isDark, mailboxSplitSize, indexTab, loading, useUTCDate,
|
||||
autoRefresh, configAutoRefreshInterval, sendMailModel
|
||||
} = useGlobalState()
|
||||
const autoRefreshInterval = ref(configAutoRefreshInterval.value)
|
||||
const data = ref([])
|
||||
const rawData = ref([])
|
||||
const timer = ref(null)
|
||||
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
// Computed property for filtered data (only filter current page)
|
||||
const data = computed(() => {
|
||||
if (!localFilterKeyword.value || localFilterKeyword.value.trim() === '') {
|
||||
return rawData.value;
|
||||
}
|
||||
const keyword = localFilterKeyword.value.toLowerCase();
|
||||
return rawData.value.filter(mail => {
|
||||
// Search in subject, text, message fields
|
||||
const searchFields = [
|
||||
mail.subject || '',
|
||||
mail.text || '',
|
||||
mail.message || ''
|
||||
].map(field => field.toLowerCase());
|
||||
return searchFields.some(field => field.includes(keyword));
|
||||
});
|
||||
})
|
||||
|
||||
const canGoPrevMail = computed(() => {
|
||||
if (!curMail.value) return false
|
||||
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
|
||||
@@ -136,6 +160,8 @@ const { t } = useI18n({
|
||||
unselectAll: 'Unselect All',
|
||||
prevMail: 'Previous',
|
||||
nextMail: 'Next',
|
||||
keywordQueryTip: 'Filter current page',
|
||||
query: 'Query',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
@@ -158,6 +184,8 @@ const { t } = useI18n({
|
||||
unselectAll: '取消全选',
|
||||
prevMail: '上一封',
|
||||
nextMail: '下一封',
|
||||
keywordQueryTip: '过滤当前页',
|
||||
query: '查询',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -197,7 +225,7 @@ const refresh = async () => {
|
||||
pageSize.value, (page.value - 1) * pageSize.value
|
||||
);
|
||||
loading.value = true;
|
||||
data.value = await Promise.all(results.map(async (item) => {
|
||||
rawData.value = await Promise.all(results.map(async (item) => {
|
||||
item.checked = false;
|
||||
return await processItem(item);
|
||||
}));
|
||||
@@ -370,7 +398,7 @@ onBeforeUnmount(() => {
|
||||
<div>
|
||||
<div v-if="!isMobile" class="left">
|
||||
<div style="margin-bottom: 10px;">
|
||||
<n-space v-if="multiActionMode">
|
||||
<n-space v-if="multiActionMode" align="center">
|
||||
<n-button @click="multiActionModeClick(false)" tertiary>
|
||||
{{ t('cancelMultiAction') }}
|
||||
</n-button>
|
||||
@@ -393,7 +421,7 @@ onBeforeUnmount(() => {
|
||||
{{ t('downloadMail') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<n-space v-else>
|
||||
<n-space v-else align="center">
|
||||
<n-button @click="multiActionModeClick(true)" type="primary" tertiary>
|
||||
{{ t('multiAction') }}
|
||||
</n-button>
|
||||
@@ -410,6 +438,9 @@ onBeforeUnmount(() => {
|
||||
<n-button @click="backFirstPageAndRefresh" type="primary" tertiary>
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
<n-input v-if="showFilterInput" v-model:value="localFilterKeyword"
|
||||
:placeholder="t('keywordQueryTip')" style="width: 200px; display: flex; align-items: center;"
|
||||
clearable />
|
||||
</n-space>
|
||||
</div>
|
||||
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
|
||||
@@ -482,10 +513,8 @@ onBeforeUnmount(() => {
|
||||
</n-split>
|
||||
</div>
|
||||
<div class="left" v-else>
|
||||
<n-space justify="center">
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||
</div>
|
||||
<n-space justify="space-around" align="center" :wrap="false" style="display: flex; align-items: center;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||
<n-switch v-model:value="autoRefresh" size="small" :round="false">
|
||||
<template #checked>
|
||||
{{ t('refreshAfter', { msg: autoRefreshInterval }) }}
|
||||
@@ -498,6 +527,10 @@ onBeforeUnmount(() => {
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<div v-if="showFilterInput" style="padding: 0 10px; margin-top: 8px; margin-bottom: 10px;">
|
||||
<n-input v-model:value="localFilterKeyword"
|
||||
:placeholder="t('keywordQueryTip')" size="small" clearable />
|
||||
</div>
|
||||
<div style="overflow: auto; height: 80vh;">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)">
|
||||
@@ -510,10 +543,14 @@ onBeforeUnmount(() => {
|
||||
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ showEMailTo ? "FROM: " + row.source : row.source }}
|
||||
<n-ellipsis style="max-width: 240px;">
|
||||
{{ showEMailTo ? "FROM: " + row.source : row.source }}
|
||||
</n-ellipsis>
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ row.address }}
|
||||
<n-ellipsis style="max-width: 240px;">
|
||||
TO: {{ row.address }}
|
||||
</n-ellipsis>
|
||||
</n-tag>
|
||||
<AiExtractInfo :metadata="row.metadata" compact />
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createHead } from '@unhead/vue'
|
||||
import { createHead } from '@unhead/vue/client'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
import { getRouterPathWithLang } from '../utils'
|
||||
|
||||
import SenderAccess from './admin/SenderAccess.vue'
|
||||
import Statistics from "./admin/Statistics.vue"
|
||||
@@ -30,9 +32,11 @@ import AiExtractSettings from './admin/AiExtractSettings.vue';
|
||||
|
||||
const {
|
||||
adminAuth, showAdminAuth, adminTab, loading,
|
||||
globalTabplacement, showAdminPage, userSettings
|
||||
globalTabplacement, showAdminPage, userSettings,
|
||||
openSettings
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const SendMail = defineAsyncComponent(() => {
|
||||
loading.value = true;
|
||||
@@ -49,7 +53,20 @@ const authFunc = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const { t } = useI18n({
|
||||
const showLogoutModal = ref(false)
|
||||
|
||||
const handleLogout = async () => {
|
||||
// 清空管理员认证
|
||||
adminAuth.value = '';
|
||||
// 重置管理员相关状态
|
||||
showAdminAuth.value = false;
|
||||
adminTab.value = 'account';
|
||||
// 显示成功提示并跳转
|
||||
message.success(t('logoutSuccess'));
|
||||
await router.push(getRouterPathWithLang('/', locale.value));
|
||||
}
|
||||
|
||||
const { t, locale } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
accessHeader: 'Admin Password',
|
||||
@@ -80,6 +97,16 @@ const { t } = useI18n({
|
||||
about: 'About',
|
||||
ok: 'OK',
|
||||
mailWebhook: 'Mail Webhook',
|
||||
adminAccount: 'Admin',
|
||||
loginMethod: 'Login Method',
|
||||
loginViaPassword: 'Admin Password Login',
|
||||
loginViaUserAdmin: 'User Admin Permission',
|
||||
loginViaDisabledCheck: 'Disabled Password Check',
|
||||
logout: 'Logout',
|
||||
logoutConfirmTitle: 'Confirm Logout',
|
||||
logoutConfirmContent: 'Are you sure you want to logout from admin panel?',
|
||||
confirm: 'Confirm',
|
||||
logoutSuccess: 'Logout successful',
|
||||
},
|
||||
zh: {
|
||||
accessHeader: 'Admin 密码',
|
||||
@@ -110,12 +137,36 @@ const { t } = useI18n({
|
||||
about: '关于',
|
||||
ok: '确定',
|
||||
mailWebhook: '邮件 Webhook',
|
||||
adminAccount: '管理员',
|
||||
loginMethod: '登录方式',
|
||||
loginViaPassword: 'Admin 密码登录',
|
||||
loginViaUserAdmin: '用户管理员权限',
|
||||
loginViaDisabledCheck: '已禁用密码检查',
|
||||
logout: '退出登录',
|
||||
logoutConfirmTitle: '确认退出',
|
||||
logoutConfirmContent: '确定要退出管理员面板吗?',
|
||||
confirm: '确认',
|
||||
logoutSuccess: '退出成功',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const showAdminPasswordModal = computed(() => !showAdminPage.value || showAdminAuth.value)
|
||||
const tmpAdminAuth = ref('')
|
||||
// 判断是否通过 admin password 登录(而非用户管理员权限)
|
||||
const isAdminPasswordLogin = computed(() => !!adminAuth.value)
|
||||
|
||||
// 获取当前登录方式
|
||||
const currentLoginMethod = computed(() => {
|
||||
if (adminAuth.value) {
|
||||
return t('loginViaPassword');
|
||||
} else if (userSettings.value.is_admin) {
|
||||
return t('loginViaUserAdmin');
|
||||
} else if (openSettings.value.disableAdminPasswordCheck) {
|
||||
return t('loginViaDisabledCheck');
|
||||
}
|
||||
return '';
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// make sure user_id is fetched
|
||||
@@ -234,10 +285,32 @@ onMounted(async () => {
|
||||
<n-tab-pane name="appearance" :tab="t('appearance')">
|
||||
<Appearance />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="adminAccount" :tab="t('adminAccount')">
|
||||
<div style="display: flex; justify-content: center; padding: 20px;">
|
||||
<n-card style="width: 600px;">
|
||||
<n-space vertical>
|
||||
<n-text strong>{{ t('loginMethod') }}</n-text>
|
||||
<n-text>{{ currentLoginMethod }}</n-text>
|
||||
<n-divider v-if="isAdminPasswordLogin" />
|
||||
<n-button v-if="isAdminPasswordLogin" type="warning" @click="showLogoutModal = true" block>
|
||||
{{ t('logout') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="about" :tab="t('about')">
|
||||
<About />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
<n-modal v-model:show="showLogoutModal" preset="dialog" :title="t('logoutConfirmTitle')">
|
||||
<p>{{ t('logoutConfirmContent') }}</p>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="handleLogout" size="small" tertiary type="warning">
|
||||
{{ t('confirm') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ onMounted(() => {
|
||||
</div>
|
||||
<MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled"
|
||||
:saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" :showFilterInput="true" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
||||
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
|
||||
@@ -22,6 +22,7 @@ const { t } = useI18n({
|
||||
updated_at: 'Update At',
|
||||
mail_count: 'Mail Count',
|
||||
send_count: 'Send Count',
|
||||
source_meta: 'Source',
|
||||
showCredential: 'Show Mail Address Credential',
|
||||
addressCredential: 'Mail Address Credential',
|
||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||
@@ -59,6 +60,7 @@ const { t } = useI18n({
|
||||
updated_at: '更新时间',
|
||||
mail_count: '邮件数量',
|
||||
send_count: '发送数量',
|
||||
source_meta: '来源',
|
||||
showCredential: '查看邮箱地址凭证',
|
||||
addressCredential: '邮箱地址凭证',
|
||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||
@@ -319,6 +321,10 @@ const columns = [
|
||||
title: t('updated_at'),
|
||||
key: "updated_at"
|
||||
},
|
||||
{
|
||||
title: t('source_meta'),
|
||||
key: "source_meta"
|
||||
},
|
||||
{
|
||||
title: t('mail_count'),
|
||||
key: "mail_count",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, h } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { NButton, NPopconfirm, NInput, NSelect } from 'naive-ui'
|
||||
import { NButton, NPopconfirm, NInput, NSelect, NRadioGroup, NRadio } from 'naive-ui'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
@@ -24,7 +24,7 @@ const { t } = useI18n({
|
||||
fromBlockList: 'Block Keywords for receive email',
|
||||
block_receive_unknow_address_email: 'Block receive unknow address email',
|
||||
email_forwarding_config: 'Email Forwarding Configuration',
|
||||
domain_list: 'Domain List',
|
||||
domain_list: 'Domain List (Optional)',
|
||||
forward_address: 'Forward Address',
|
||||
actions: 'Actions',
|
||||
select_domain: 'Select Domain',
|
||||
@@ -32,10 +32,20 @@ const { t } = useI18n({
|
||||
delete_rule: 'Delete',
|
||||
delete_rule_confirm: 'Are you sure you want to delete this rule?',
|
||||
delete_success: 'Delete Success',
|
||||
forwarding_rule_warning: 'Each rule will run, if domains is empty, all emails will be forwarded, forward address needs to be a verified address',
|
||||
forwarding_rule_warning: 'Each rule will run independently. Forward address needs to be a verified address.',
|
||||
add: 'Add',
|
||||
cancel: 'Cancel',
|
||||
config: 'Config',
|
||||
source_patterns: 'Source Address Regex (Optional)',
|
||||
source_patterns_placeholder: 'e.g. gmail.com',
|
||||
source_match_mode: 'Match Mode',
|
||||
match_any: 'Any',
|
||||
match_all: 'All',
|
||||
source_patterns_tip: 'Domain list filters by recipient address, source regex filters by sender address. Both conditions must match for forwarding (AND logic). Leave either empty to skip that filter.',
|
||||
regex_too_long: 'Regex pattern too long (max 200 characters)',
|
||||
regex_invalid: 'Invalid regex pattern',
|
||||
forward_address_required: 'Forward address is required',
|
||||
rule_index: 'Rule',
|
||||
},
|
||||
zh: {
|
||||
tip: '您可以手动输入以下多选输入框, 回车增加',
|
||||
@@ -50,7 +60,7 @@ const { t } = useI18n({
|
||||
fromBlockList: '接收邮件地址屏蔽关键词',
|
||||
block_receive_unknow_address_email: '禁止接收未知地址邮件',
|
||||
email_forwarding_config: '邮件转发配置',
|
||||
domain_list: '域名列表',
|
||||
domain_list: '域名列表(可选)',
|
||||
forward_address: '转发地址',
|
||||
actions: '操作',
|
||||
select_domain: '选择域名',
|
||||
@@ -58,10 +68,20 @@ const { t } = useI18n({
|
||||
delete_rule: '删除',
|
||||
delete_rule_confirm: '确定要删除这条规则吗?',
|
||||
delete_success: '删除成功',
|
||||
forwarding_rule_warning: '每条规则都会运行,如果 domains 为空,则转发所有邮件,转发地址需要为已验证的地址',
|
||||
forwarding_rule_warning: '每条规则独立运行,转发地址需要为已验证的地址。',
|
||||
add: '添加',
|
||||
cancel: '取消',
|
||||
config: '配置',
|
||||
source_patterns: '来源地址正则(可选)',
|
||||
source_patterns_placeholder: '例如: gmail.com',
|
||||
source_match_mode: '匹配模式',
|
||||
match_any: '任一',
|
||||
match_all: '全部',
|
||||
source_patterns_tip: '域名列表按收件地址过滤,来源正则按发件地址过滤,两者均为可选。同时配置时需同时满足(AND 逻辑),留空则跳过该条件。',
|
||||
regex_too_long: '正则表达式过长(最大200字符)',
|
||||
regex_invalid: '无效的正则表达式',
|
||||
forward_address_required: '转发地址不能为空',
|
||||
rule_index: '规则',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -98,6 +118,39 @@ const emailForwardingColumns = [
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('source_patterns'),
|
||||
key: 'sourcePatterns',
|
||||
render: (row, index) => {
|
||||
return h('div', { style: 'display: flex; flex-direction: column; gap: 4px;' }, [
|
||||
h(NSelect, {
|
||||
value: Array.isArray(row.sourcePatterns) ? row.sourcePatterns : [],
|
||||
onUpdateValue: (val) => {
|
||||
emailForwardingList.value[index].sourcePatterns = val
|
||||
},
|
||||
multiple: true,
|
||||
filterable: true,
|
||||
tag: true,
|
||||
placeholder: t('source_patterns_placeholder')
|
||||
}, {
|
||||
empty: () => h('span', { style: 'color: #999; font-size: 12px;' }, t('manualInputPrompt'))
|
||||
}),
|
||||
h(NRadioGroup, {
|
||||
value: row.sourceMatchMode || 'any',
|
||||
onUpdateValue: (val) => {
|
||||
emailForwardingList.value[index].sourceMatchMode = val
|
||||
},
|
||||
size: 'small',
|
||||
style: 'margin-top: 4px;'
|
||||
}, {
|
||||
default: () => [
|
||||
h(NRadio, { value: 'any' }, { default: () => t('match_any') }),
|
||||
h(NRadio, { value: 'all' }, { default: () => t('match_all') })
|
||||
]
|
||||
})
|
||||
])
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('forward_address'),
|
||||
key: 'forward',
|
||||
@@ -145,12 +198,50 @@ const addNewEmailForwardingItem = () => {
|
||||
...emailForwardingList.value,
|
||||
{
|
||||
domains: [],
|
||||
forward: ''
|
||||
forward: '',
|
||||
sourcePatterns: [],
|
||||
sourceMatchMode: 'any'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const MAX_REGEX_LENGTH = 200
|
||||
|
||||
const validateForwardingRules = () => {
|
||||
for (let i = 0; i < emailForwardingList.value.length; i++) {
|
||||
const rule = emailForwardingList.value[i]
|
||||
|
||||
// 验证转发地址
|
||||
if (!rule.forward || rule.forward.trim() === '') {
|
||||
message.error(`${t('forward_address_required')} (${t('rule_index')} ${i + 1})`)
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证正则表达式
|
||||
if (rule.sourcePatterns && rule.sourcePatterns.length > 0) {
|
||||
for (const pattern of rule.sourcePatterns) {
|
||||
// 检查长度
|
||||
if (pattern.length > MAX_REGEX_LENGTH) {
|
||||
message.error(`${t('regex_too_long')}: ${pattern.substring(0, 30)}...`)
|
||||
return false
|
||||
}
|
||||
// 检查正则有效性
|
||||
try {
|
||||
new RegExp(pattern, 'i')
|
||||
} catch (e) {
|
||||
message.error(`${t('regex_invalid')}: ${pattern}`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const saveEmailForwardingConfig = () => {
|
||||
if (!validateForwardingRules()) {
|
||||
return
|
||||
}
|
||||
emailRuleSettings.value.emailForwardingList = [...emailForwardingList.value]
|
||||
showEmailForwardingModal.value = false
|
||||
}
|
||||
@@ -164,9 +255,9 @@ const fetchData = async () => {
|
||||
verifiedAddressList.value = res.verifiedAddressList || []
|
||||
fromBlockList.value = res.fromBlockList || []
|
||||
noLimitSendAddressList.value = res.noLimitSendAddressList || []
|
||||
emailRuleSettings.value = res.emailRuleSettings || {
|
||||
blockReceiveUnknowAddressEmail: false,
|
||||
emailForwardingList: []
|
||||
emailRuleSettings.value = {
|
||||
blockReceiveUnknowAddressEmail: res.emailRuleSettings?.blockReceiveUnknowAddressEmail || false,
|
||||
emailForwardingList: res.emailRuleSettings?.emailForwardingList || []
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
@@ -269,10 +360,12 @@ onMounted(async () => {
|
||||
|
||||
<!-- 邮件转发配置弹窗 -->
|
||||
<n-modal v-model:show="showEmailForwardingModal" preset="card" :title="t('email_forwarding_config')"
|
||||
style="max-width: 800px;">
|
||||
style="max-width: 1000px;">
|
||||
<n-space vertical>
|
||||
<n-alert :show-icon="false" :bordered="false" type="warning">
|
||||
<span>{{ t('forwarding_rule_warning') }}</span>
|
||||
<br />
|
||||
<span>{{ t('source_patterns_tip') }}</span>
|
||||
</n-alert>
|
||||
<n-space justify="end">
|
||||
<n-button @click="addNewEmailForwardingItem">{{ t('add') }}</n-button>
|
||||
|
||||
@@ -12,23 +12,19 @@ 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 mailKeyword = ref("")
|
||||
|
||||
const queryMail = () => {
|
||||
adminMailTabAddress.value = adminMailTabAddress.value.trim();
|
||||
mailKeyword.value = mailKeyword.value.trim();
|
||||
mailBoxKey.value = Date.now();
|
||||
}
|
||||
|
||||
@@ -38,7 +34,6 @@ const fetchMailData = async (limit, offset) => {
|
||||
+ `?limit=${limit}`
|
||||
+ `&offset=${offset}`
|
||||
+ (adminMailTabAddress.value ? `&address=${adminMailTabAddress.value}` : '')
|
||||
+ (mailKeyword.value ? `&keyword=${mailKeyword.value}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,14 +46,13 @@ const deleteMail = async (curMailId) => {
|
||||
<div style="margin-top: 10px;">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')"
|
||||
@keydown.enter="queryMail" />
|
||||
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" @keydown.enter="queryMail" />
|
||||
@keydown.enter="queryMail" clearable />
|
||||
<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" />
|
||||
:deleteMail="deleteMail" :showFilterInput="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CleaningServicesFilled } from '@vicons/material'
|
||||
import { CleaningServicesFilled, AddFilled, DeleteFilled } from '@vicons/material'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { loading } = useGlobalState()
|
||||
const message = useMessage()
|
||||
const cleanupModel = ref({
|
||||
enableMailsAutoCleanup: false,
|
||||
@@ -21,6 +23,7 @@ const cleanupModel = ref({
|
||||
cleanUnboundAddressDays: 30,
|
||||
enableEmptyAddressAutoCleanup: false,
|
||||
cleanEmptyAddressDays: 30,
|
||||
customSqlCleanupList: []
|
||||
})
|
||||
|
||||
const { t } = useI18n({
|
||||
@@ -37,8 +40,18 @@ const { t } = useI18n({
|
||||
cleanupNow: "Cleanup now",
|
||||
autoCleanup: "Auto cleanup",
|
||||
cleanupSuccess: "Cleanup success",
|
||||
saveSuccess: "Save success",
|
||||
save: "Save",
|
||||
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document, setting 0 days means clear all",
|
||||
basicCleanup: "Basic Cleanup",
|
||||
customSqlCleanup: "Custom SQL Cleanup",
|
||||
customSqlTip: "Add custom DELETE SQL statements for scheduled cleanup. Only single DELETE statement is allowed per entry.",
|
||||
addCustomSql: "Add Custom SQL",
|
||||
sqlName: "Name",
|
||||
sqlStatement: "SQL Statement (DELETE only)",
|
||||
sqlNamePlaceholder: "e.g. Clean old logs",
|
||||
sqlPlaceholder: "e.g. DELETE FROM raw_mails WHERE source GLOB '*{'@'}example.com' AND created_at < datetime('now', '-3 day')",
|
||||
deleteCustomSql: "Delete",
|
||||
},
|
||||
zh: {
|
||||
tip: '请输入天数',
|
||||
@@ -51,9 +64,19 @@ const { t } = useI18n({
|
||||
emptyAddressLabel: "清理 n 天前空邮件的邮箱地址",
|
||||
autoCleanup: "自动清理",
|
||||
cleanupSuccess: "清理成功",
|
||||
saveSuccess: "保存成功",
|
||||
cleanupNow: "立即清理",
|
||||
save: "保存",
|
||||
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档, 配置为 0 天表示全部清空",
|
||||
basicCleanup: "基础清理",
|
||||
customSqlCleanup: "自定义 SQL 清理",
|
||||
customSqlTip: "添加自定义 DELETE SQL 语句进行定时清理。每条记录仅允许单条 DELETE 语句。",
|
||||
addCustomSql: "添加自定义 SQL",
|
||||
sqlName: "名称",
|
||||
sqlStatement: "SQL 语句 (仅限 DELETE)",
|
||||
sqlNamePlaceholder: "例如: 清理旧日志",
|
||||
sqlPlaceholder: "例如: DELETE FROM raw_mails WHERE source GLOB '*{'@'}example.com' AND created_at < datetime('now', '-3 day')",
|
||||
deleteCustomSql: "删除",
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -70,10 +93,29 @@ const cleanup = async (cleanType, cleanDays) => {
|
||||
}
|
||||
}
|
||||
|
||||
const addCustomSql = () => {
|
||||
if (!cleanupModel.value.customSqlCleanupList) {
|
||||
cleanupModel.value.customSqlCleanupList = [];
|
||||
}
|
||||
cleanupModel.value.customSqlCleanupList.push({
|
||||
id: Date.now().toString(),
|
||||
name: '',
|
||||
sql: '',
|
||||
enabled: false
|
||||
});
|
||||
}
|
||||
|
||||
const removeCustomSql = (index) => {
|
||||
cleanupModel.value.customSqlCleanupList.splice(index, 1);
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await api.fetch('/admin/auto_cleanup');
|
||||
if (res) Object.assign(cleanupModel.value, res);
|
||||
if (!cleanupModel.value.customSqlCleanupList) {
|
||||
cleanupModel.value.customSqlCleanupList = [];
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
@@ -85,7 +127,7 @@ const save = async () => {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(cleanupModel.value)
|
||||
});
|
||||
message.success(t('cleanupSuccess'));
|
||||
message.success(t('saveSuccess'));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
@@ -108,92 +150,132 @@ onMounted(async () => {
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-form :model="cleanupModel">
|
||||
<n-form-item-row :label="t('mailBoxLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
|
||||
{{ t('autoCleanup') }}
|
||||
</n-checkbox>
|
||||
<n-input-number v-model:value="cleanupModel.cleanMailsDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('mails', cleanupModel.cleanMailsDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('cleanupNow') }}
|
||||
</n-button>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('mailUnknowLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableUnknowMailsAutoCleanup">
|
||||
{{ t('autoCleanup') }}
|
||||
</n-checkbox>
|
||||
<n-input-number v-model:value="cleanupModel.cleanUnknowMailsDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('mails_unknow', cleanupModel.cleanUnknowMailsDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('cleanupNow') }}
|
||||
</n-button>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('sendBoxLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableSendBoxAutoCleanup">
|
||||
{{ t('autoCleanup') }}
|
||||
</n-checkbox>
|
||||
<n-input-number v-model:value="cleanupModel.cleanSendBoxDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('sendbox', cleanupModel.cleanSendBoxDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('cleanupNow') }}
|
||||
</n-button>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('addressCreateLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableAddressAutoCleanup">
|
||||
{{ 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-item-row :label="t('unboundAddressLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableUnboundAddressAutoCleanup">
|
||||
{{ t('autoCleanup') }}
|
||||
</n-checkbox>
|
||||
<n-input-number v-model:value="cleanupModel.cleanUnboundAddressDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('unboundAddress', cleanupModel.cleanUnboundAddressDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('cleanupNow') }}
|
||||
</n-button>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('emptyAddressLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableEmptyAddressAutoCleanup">
|
||||
{{ t('autoCleanup') }}
|
||||
</n-checkbox>
|
||||
<n-input-number v-model:value="cleanupModel.cleanEmptyAddressDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('emptyAddress', cleanupModel.cleanEmptyAddressDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('cleanupNow') }}
|
||||
</n-button>
|
||||
</n-form-item-row>
|
||||
</n-form>
|
||||
<n-tabs type="segment" style="margin-top: 16px;">
|
||||
<n-tab-pane name="basic" :tab="t('basicCleanup')">
|
||||
<n-form :model="cleanupModel">
|
||||
<n-form-item-row :label="t('mailBoxLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
|
||||
{{ t('autoCleanup') }}
|
||||
</n-checkbox>
|
||||
<n-input-number v-model:value="cleanupModel.cleanMailsDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('mails', cleanupModel.cleanMailsDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('cleanupNow') }}
|
||||
</n-button>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('mailUnknowLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableUnknowMailsAutoCleanup">
|
||||
{{ t('autoCleanup') }}
|
||||
</n-checkbox>
|
||||
<n-input-number v-model:value="cleanupModel.cleanUnknowMailsDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('mails_unknow', cleanupModel.cleanUnknowMailsDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('cleanupNow') }}
|
||||
</n-button>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('sendBoxLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableSendBoxAutoCleanup">
|
||||
{{ t('autoCleanup') }}
|
||||
</n-checkbox>
|
||||
<n-input-number v-model:value="cleanupModel.cleanSendBoxDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('sendbox', cleanupModel.cleanSendBoxDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('cleanupNow') }}
|
||||
</n-button>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('addressCreateLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableAddressAutoCleanup">
|
||||
{{ 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-item-row :label="t('unboundAddressLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableUnboundAddressAutoCleanup">
|
||||
{{ t('autoCleanup') }}
|
||||
</n-checkbox>
|
||||
<n-input-number v-model:value="cleanupModel.cleanUnboundAddressDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('unboundAddress', cleanupModel.cleanUnboundAddressDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('cleanupNow') }}
|
||||
</n-button>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('emptyAddressLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableEmptyAddressAutoCleanup">
|
||||
{{ t('autoCleanup') }}
|
||||
</n-checkbox>
|
||||
<n-input-number v-model:value="cleanupModel.cleanEmptyAddressDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('emptyAddress', cleanupModel.cleanEmptyAddressDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('cleanupNow') }}
|
||||
</n-button>
|
||||
</n-form-item-row>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="custom_sql" :tab="t('customSqlCleanup')">
|
||||
<n-alert :show-icon="false" :bordered="false" type="info" style="margin-bottom: 16px;">
|
||||
<span>{{ t('customSqlTip') }}</span>
|
||||
</n-alert>
|
||||
<n-space vertical>
|
||||
<n-card v-for="(item, index) in cleanupModel.customSqlCleanupList" :key="item.id" size="small">
|
||||
<n-space vertical>
|
||||
<n-space align="center">
|
||||
<n-checkbox v-model:checked="item.enabled">
|
||||
{{ t('autoCleanup') }}
|
||||
</n-checkbox>
|
||||
<n-input v-model:value="item.name" :placeholder="t('sqlNamePlaceholder')" style="width: 200px;" />
|
||||
<n-button @click="removeCustomSql(index)" type="error" quaternary>
|
||||
<template #icon>
|
||||
<n-icon :component="DeleteFilled" />
|
||||
</template>
|
||||
{{ t('deleteCustomSql') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<n-input
|
||||
v-model:value="item.sql"
|
||||
type="textarea"
|
||||
:placeholder="t('sqlPlaceholder')"
|
||||
:autosize="{ minRows: 2 }"
|
||||
class="sql-input"
|
||||
/>
|
||||
</n-space>
|
||||
</n-card>
|
||||
<n-button @click="addCustomSql">
|
||||
<template #icon>
|
||||
<n-icon :component="AddFilled" />
|
||||
</template>
|
||||
{{ t('addCustomSql') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
@@ -214,8 +296,7 @@ onMounted(async () => {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
margin: 10px;
|
||||
.sql-input {
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,81 +1,51 @@
|
||||
<script setup>
|
||||
import useClipboard from 'vue-clipboard3'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Copy, User, ExchangeAlt } from '@vicons/fa'
|
||||
import { User, ExchangeAlt } from '@vicons/fa'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import Login from '../common/Login.vue'
|
||||
import AddressManagement from '../user/AddressManagement.vue'
|
||||
import TelegramAddress from './TelegramAddress.vue'
|
||||
import LocalAddress from './LocalAddress.vue'
|
||||
import AddressManagement from '../user/AddressManagement.vue'
|
||||
import { getRouterPathWithLang } from '../../utils'
|
||||
import AddressSelect from '../../components/AddressSelect.vue'
|
||||
|
||||
const { toClipboard } = useClipboard()
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
jwt, settings, showAddressCredential, userJwt,
|
||||
isTelegram, openSettings, addressPassword
|
||||
isTelegram, addressPassword
|
||||
} = useGlobalState()
|
||||
|
||||
const { locale, t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
addressManage: 'Address Manage',
|
||||
changeAddress: 'Change Address',
|
||||
ok: 'OK',
|
||||
copy: 'Copy',
|
||||
copied: 'Copied',
|
||||
fetchAddressError: 'Mail address credential is invalid or account not exist, it may be network connection issue, please try again later.',
|
||||
addressCredential: 'Mail Address Credential',
|
||||
linkWithAddressCredential: 'Open to auto login email link',
|
||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||
addressPassword: 'Address Password',
|
||||
userLogin: 'User Login',
|
||||
addressManage: 'Manage',
|
||||
},
|
||||
zh: {
|
||||
addressManage: '地址管理',
|
||||
changeAddress: '更换地址',
|
||||
ok: '确定',
|
||||
copy: '复制',
|
||||
copied: '已复制',
|
||||
fetchAddressError: '邮箱地址凭证无效或邮箱地址不存在,也可能是网络连接异常,请稍后再尝试。',
|
||||
addressCredential: '邮箱地址凭证',
|
||||
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||
addressPassword: '地址密码',
|
||||
userLogin: '用户登录',
|
||||
addressManage: '地址管理',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const showChangeAddress = ref(false)
|
||||
const showTelegramChangeAddress = ref(false)
|
||||
const showLocalAddress = ref(false)
|
||||
const addressLabel = computed(() => {
|
||||
if (settings.value.address) {
|
||||
const domain = settings.value.address.split('@')[1]
|
||||
const domainLabel = openSettings.value.domains.find(
|
||||
d => d.value === domain
|
||||
)?.label;
|
||||
if (!domainLabel) return settings.value.address;
|
||||
return settings.value.address.replace('@' + domain, `@${domainLabel}`);
|
||||
}
|
||||
return settings.value.address;
|
||||
})
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
await toClipboard(settings.value.address)
|
||||
message.success(t('copied'));
|
||||
} catch (e) {
|
||||
message.error(e.message || "error");
|
||||
}
|
||||
}
|
||||
const showAddressManage = ref(false)
|
||||
|
||||
const getUrlWithJwt = () => {
|
||||
return `${window.location.origin}/?jwt=${jwt.value}`
|
||||
@@ -97,29 +67,25 @@ onMounted(async () => {
|
||||
</n-card>
|
||||
<div v-else-if="settings.address">
|
||||
<n-alert type="info" :show-icon="false" :bordered="false">
|
||||
<span>
|
||||
<b>{{ addressLabel }}</b>
|
||||
<n-button v-if="isTelegram" style="margin-left: 10px" @click="showTelegramChangeAddress = true"
|
||||
size="small" tertiary type="primary">
|
||||
<n-icon :component="ExchangeAlt" /> {{ t('addressManage') }}
|
||||
</n-button>
|
||||
<n-button v-else-if="userJwt" style="margin-left: 10px" @click="showChangeAddress = true"
|
||||
size="small" tertiary type="primary">
|
||||
<n-icon :component="ExchangeAlt" /> {{ t('changeAddress') }}
|
||||
</n-button>
|
||||
<n-button v-else style="margin-left: 10px" @click="showLocalAddress = true" size="small" tertiary
|
||||
type="primary">
|
||||
<n-icon :component="ExchangeAlt" /> {{ t('addressManage') }}
|
||||
</n-button>
|
||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary type="primary">
|
||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||
</n-button>
|
||||
</span>
|
||||
<AddressSelect>
|
||||
<template #actions>
|
||||
<n-button class="address-manage" size="small" tertiary type="primary"
|
||||
@click="showAddressManage = true">
|
||||
<n-icon :component="ExchangeAlt" />
|
||||
{{ t('addressManage') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</AddressSelect>
|
||||
</n-alert>
|
||||
</div>
|
||||
<div v-else-if="isTelegram">
|
||||
<TelegramAddress />
|
||||
</div>
|
||||
<div v-else-if="userJwt" class="center">
|
||||
<n-card :bordered="false" embedded style="max-width: 900px; width: 100%;">
|
||||
<AddressManagement />
|
||||
</n-card>
|
||||
</div>
|
||||
<div v-else class="center">
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<n-alert v-if="jwt" type="warning" :show-icon="false" :bordered="false" closable>
|
||||
@@ -135,15 +101,6 @@ onMounted(async () => {
|
||||
</n-button>
|
||||
</n-card>
|
||||
</div>
|
||||
<n-modal v-model:show="showTelegramChangeAddress" preset="card" :title="t('changeAddress')">
|
||||
<TelegramAddress />
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showChangeAddress" preset="card" :title="t('changeAddress')">
|
||||
<AddressManagement />
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showLocalAddress" preset="card" :title="t('changeAddress')">
|
||||
<LocalAddress />
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showAddressCredential" preset="dialog" :title="t('addressCredential')">
|
||||
<span>
|
||||
<p>{{ t("addressCredentialTip") }}</p>
|
||||
@@ -165,6 +122,11 @@ onMounted(async () => {
|
||||
</n-collapse>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showAddressManage" preset="card" :title="t('addressManage')">
|
||||
<TelegramAddress v-if="isTelegram" />
|
||||
<AddressManagement v-else-if="userJwt" />
|
||||
<LocalAddress v-else />
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -186,4 +148,9 @@ onMounted(async () => {
|
||||
justify-content: center;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.address-manage {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,7 +48,7 @@ const data = computed(() => {
|
||||
}
|
||||
return localAddressCache.value.map((curJwt: string) => {
|
||||
try {
|
||||
var payload = JSON.parse(
|
||||
const payload = JSON.parse(
|
||||
decodeURIComponent(
|
||||
atob(curJwt.split(".")[1]
|
||||
.replace(/-/g, "+").replace(/_/g, "/")
|
||||
|
||||
@@ -17,6 +17,7 @@ import Login from '../common/Login.vue'
|
||||
import AccountSettings from './AccountSettings.vue'
|
||||
import { processItem } from '../../utils/email-parser'
|
||||
import MailContentRenderer from '../../components/MailContentRenderer.vue'
|
||||
import AddressSelect from '../../components/AddressSelect.vue'
|
||||
|
||||
const { jwt, settings, useSimpleIndex, showAddressCredential, openSettings, loading } = useGlobalState()
|
||||
const message = useMessage()
|
||||
@@ -171,7 +172,7 @@ onBeforeUnmount(() => {
|
||||
<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>
|
||||
<AddressSelect :showCopy="false" size="small" />
|
||||
</div>
|
||||
<n-flex justify="center">
|
||||
<n-button @click="refreshMails" :loading="loading" type="primary" tertiary size="small">
|
||||
|
||||
@@ -5,17 +5,16 @@ import { useI18n } from 'vue-i18n'
|
||||
import { api } from '../../api'
|
||||
import MailBox from '../../components/MailBox.vue';
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
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: '查询',
|
||||
}
|
||||
}
|
||||
@@ -23,12 +22,10 @@ const { t } = useI18n({
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -38,7 +35,6 @@ const fetchMailData = async (limit, offset) => {
|
||||
+ `?limit=${limit}`
|
||||
+ `&offset=${offset}`
|
||||
+ (addressFilter.value ? `&address=${addressFilter.value}` : '')
|
||||
+ (mailKeyword.value ? `&keyword=${mailKeyword.value}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,13 +73,12 @@ onMounted(() => {
|
||||
<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" />
|
||||
:deleteMail="deleteMail" :showFilterInput="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temp-email-pages",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -11,7 +11,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"wrangler": "^4.53.0"
|
||||
"wrangler": "^4.59.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ Extraction results are automatically saved to the `metadata` field in the databa
|
||||
| `ENABLE_AI_EMAIL_EXTRACT` | Text/JSON | Whether to enable AI email recognition feature | `true` |
|
||||
| `AI_EXTRACT_MODEL` | Text | AI model name, choose from [models supporting JSON mode](https://developers.cloudflare.com/workers-ai/features/json-mode/#supported-models) | `@cf/meta/llama-3.1-8b-instruct` |
|
||||
|
||||
## Content Length Limit
|
||||
|
||||
To avoid AI model token limits, the maximum email content length for processing is **4000 characters**. Email content exceeding this limit will be truncated before AI analysis.
|
||||
|
||||
## Workers AI Binding
|
||||
|
||||
Configure Workers AI binding in `wrangler.toml`:
|
||||
|
||||
@@ -19,7 +19,7 @@ res = requests.get(
|
||||
|
||||
## Admin Mail API
|
||||
|
||||
Supports `address` filter and `keyword` filter
|
||||
Supports `address` filter
|
||||
|
||||
```python
|
||||
import requests
|
||||
@@ -29,9 +29,8 @@ url = "https://<your-worker-address>/admin/mails"
|
||||
querystring = {
|
||||
"limit":"20",
|
||||
"offset":"0",
|
||||
# address and keyword are optional parameters
|
||||
"address":"xxxx@awsl.uk",
|
||||
"keyword":"xxxx"
|
||||
# address is optional parameter
|
||||
"address":"xxxx@awsl.uk"
|
||||
}
|
||||
|
||||
headers = {"x-admin-auth": "<your-Admin-password>"}
|
||||
@@ -41,9 +40,11 @@ response = requests.get(url, headers=headers, params=querystring)
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
**Note**: Keyword filtering has been removed from the backend API. If you need to filter emails by content, please use the frontend filter input in the UI, which filters the currently displayed page.
|
||||
|
||||
## User Mail API
|
||||
|
||||
Supports `address` filter and `keyword` filter
|
||||
Supports `address` filter
|
||||
|
||||
```python
|
||||
import requests
|
||||
@@ -53,9 +54,8 @@ url = "https://<your-worker-address>/user_api/mails"
|
||||
querystring = {
|
||||
"limit":"20",
|
||||
"offset":"0",
|
||||
# address and keyword are optional parameters
|
||||
"address":"xxxx@awsl.uk",
|
||||
"keyword":"xxxx"
|
||||
# address is optional parameter
|
||||
"address":"xxxx@awsl.uk"
|
||||
}
|
||||
|
||||
headers = {"x-admin-auth": "<your-Admin-password>"}
|
||||
@@ -64,3 +64,5 @@ response = requests.get(url, headers=headers, params=querystring)
|
||||
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
**Note**: Keyword filtering has been removed from the backend API. If you need to filter emails by content, please use the frontend filter input in the UI, which filters the currently displayed page.
|
||||
|
||||
@@ -36,6 +36,25 @@ pnpm wrangler secret put TELEGRAM_BOT_TOKEN
|
||||
|
||||

|
||||
|
||||
## Language Switching
|
||||
|
||||
> [!NOTE]
|
||||
> This feature is available since v1.2.0
|
||||
|
||||
Telegram Bot supports Chinese and English switching. Users can set their language preference via the `/lang` command.
|
||||
|
||||
### Enable Language Switching
|
||||
|
||||
You need to configure `TG_ALLOW_USER_LANG = true` in worker variables to enable this feature.
|
||||
|
||||
### Usage
|
||||
|
||||
- `/lang zh` - Switch to Chinese
|
||||
- `/lang en` - Switch to English
|
||||
- `/lang` - View current language setting
|
||||
|
||||
Language preferences are saved to KV, and each user can set their preference independently.
|
||||
|
||||
## Mini App
|
||||
|
||||
Can be deployed via command line or UI interface
|
||||
|
||||
@@ -105,10 +105,11 @@
|
||||
|
||||
## Telegram Bot Related Variables
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
| ---------------- | ------ | --------------------------------------------------------------------------- | ------- |
|
||||
| `TG_MAX_ADDRESS` | Number | Maximum number of mailboxes that can be bound to telegram bot | `5` |
|
||||
| `TG_BOT_INFO` | Text | Optional, telegram BOT_INFO, predefined BOT_INFO can reduce webhook latency | `{}` |
|
||||
| Variable Name | Type | Description | Example |
|
||||
| -------------------- | --------- | --------------------------------------------------------------------------- | ------- |
|
||||
| `TG_MAX_ADDRESS` | Number | Maximum number of mailboxes that can be bound to telegram bot | `5` |
|
||||
| `TG_BOT_INFO` | Text | Optional, telegram BOT_INFO, predefined BOT_INFO can reduce webhook latency | `{}` |
|
||||
| `TG_ALLOW_USER_LANG` | Text/JSON | Allow users to switch language via `/lang` command, default `false` | `true` |
|
||||
|
||||
> [!NOTE]
|
||||
> Telegram functionality requires email parsing, free tier CPU is limited, may cause large email parsing timeout
|
||||
@@ -117,6 +118,33 @@
|
||||
>
|
||||
> Refer to [Configure worker to use wasm for email parsing](/en/guide/feature/mail_parser_wasm_worker)
|
||||
|
||||
## Email Forwarding Related Variables
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
| --------------------------------- | ---- | ---------------------------------------------------------------------------------------- | --------- |
|
||||
| `SUBDOMAIN_FORWARD_ADDRESS_LIST` | JSON | Subdomain/rule forwarding configuration, supports filtering by domain and source regex | See below |
|
||||
|
||||
> [!NOTE] SUBDOMAIN_FORWARD_ADDRESS_LIST Configuration
|
||||
>
|
||||
> v1.2.0 added `sourcePatterns` and `sourceMatchMode` fields for filtering by sender address regex:
|
||||
>
|
||||
> - `domains`: Target domain list, matches all domains if empty
|
||||
> - `forward`: Forward destination address
|
||||
> - `sourcePatterns`: Source address regex list (optional)
|
||||
> - `sourceMatchMode`: Match mode, `any` (match any, default) or `all` (match all)
|
||||
>
|
||||
> Regex pattern max length is 200 characters to prevent ReDoS attacks
|
||||
>
|
||||
> ```toml
|
||||
> SUBDOMAIN_FORWARD_ADDRESS_LIST = """
|
||||
> [
|
||||
> {"domains":[""],"forward":"xxx1@xxx.com"},
|
||||
> {"domains":["subdomain-1.domain.com","subdomain-2.domain.com"],"forward":"xxx2@xxx.com"},
|
||||
> {"domains":["example.com"],"forward":"admin@xxx.com","sourcePatterns":[".*@github.com",".*@gitlab.com"],"sourceMatchMode":"any"}
|
||||
> ]
|
||||
> """
|
||||
> ```
|
||||
|
||||
## Other Variables
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
|
||||
@@ -24,6 +24,10 @@ AI 邮件识别功能使用 Cloudflare Workers AI 自动分析收到的邮件内
|
||||
| `ENABLE_AI_EMAIL_EXTRACT` | 文本/JSON | 是否启用 AI 邮件识别功能 | `true` |
|
||||
| `AI_EXTRACT_MODEL` | 文本 | AI 模型名称,从[支持 JSON 模式的模型](https://developers.cloudflare.com/workers-ai/features/json-mode/#supported-models)中选择 | `@cf/meta/llama-3.1-8b-instruct` |
|
||||
|
||||
## 内容长度限制
|
||||
|
||||
为避免 AI 模型 token 限制,邮件内容最大处理长度为 **4000 字符**。超过此长度的邮件内容将被截断后再进行 AI 分析。
|
||||
|
||||
## Workers AI 绑定
|
||||
|
||||
需要在 `wrangler.toml` 中配置 Workers AI 绑定:
|
||||
|
||||
@@ -19,7 +19,7 @@ res = requests.get(
|
||||
|
||||
## admin 邮件 API
|
||||
|
||||
支持 `address` filter 和 `keyword` filter
|
||||
支持 `address` 过滤
|
||||
|
||||
```python
|
||||
import requests
|
||||
@@ -29,9 +29,8 @@ url = "https://<你的worker地址>/admin/mails"
|
||||
querystring = {
|
||||
"limit":"20",
|
||||
"offset":"0",
|
||||
# adress 和 keyword 为可选参数
|
||||
"address":"xxxx@awsl.uk",
|
||||
"keyword":"xxxx"
|
||||
# address 为可选参数
|
||||
"address":"xxxx@awsl.uk"
|
||||
}
|
||||
|
||||
headers = {"x-admin-auth": "<你的Admin密码>"}
|
||||
@@ -41,9 +40,11 @@ response = requests.get(url, headers=headers, params=querystring)
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
**注意**:后端 API 已移除关键词过滤功能。如需按内容过滤邮件,请使用前端界面的过滤输入框,该功能可过滤当前显示的页面。
|
||||
|
||||
## user 邮件 API
|
||||
|
||||
支持 `address` filter 和 `keyword` filter
|
||||
支持 `address` 过滤
|
||||
|
||||
```python
|
||||
import requests
|
||||
@@ -53,9 +54,8 @@ url = "https://<你的worker地址>/user_api/mails"
|
||||
querystring = {
|
||||
"limit":"20",
|
||||
"offset":"0",
|
||||
# adress 和 keyword 为可选参数
|
||||
"address":"xxxx@awsl.uk",
|
||||
"keyword":"xxxx"
|
||||
# address 为可选参数
|
||||
"address":"xxxx@awsl.uk"
|
||||
}
|
||||
|
||||
headers = {"x-admin-auth": "<你的Admin密码>"}
|
||||
@@ -64,3 +64,5 @@ response = requests.get(url, headers=headers, params=querystring)
|
||||
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
**注意**:后端 API 已移除关键词过滤功能。如需按内容过滤邮件,请使用前端界面的过滤输入框,该功能可过滤当前显示的页面。
|
||||
|
||||
@@ -36,6 +36,25 @@ pnpm wrangler secret put TELEGRAM_BOT_TOKEN
|
||||
|
||||

|
||||
|
||||
## 语言切换功能
|
||||
|
||||
> [!NOTE]
|
||||
> 此功能从 v1.2.0 版本开始支持
|
||||
|
||||
Telegram Bot 支持中英文切换,用户可以通过 `/lang` 命令设置语言偏好。
|
||||
|
||||
### 启用语言切换
|
||||
|
||||
需要在 worker 变量中配置 `TG_ALLOW_USER_LANG = true` 才能启用此功能。
|
||||
|
||||
### 使用方法
|
||||
|
||||
- `/lang zh` - 切换为中文
|
||||
- `/lang en` - 切换为英文
|
||||
- `/lang` - 查看当前语言设置
|
||||
|
||||
语言偏好会保存到 KV 中,每个用户可以独立设置。
|
||||
|
||||
## Mini App
|
||||
|
||||
可以通过命令行部署,或者 UI 界面部署
|
||||
|
||||
@@ -105,10 +105,11 @@
|
||||
|
||||
## Telegram Bot 相关变量
|
||||
|
||||
| 变量名 | 类型 | 说明 | 示例 |
|
||||
| ---------------- | ---- | ---------------------------------------------------------------------- | ---- |
|
||||
| `TG_MAX_ADDRESS` | 数字 | telegram bot 最多绑定邮箱数量 | `5` |
|
||||
| `TG_BOT_INFO` | 文本 | 可不配置,telegram BOT_INFO,预定义的 BOT_INFO 可以降低 webhook 的延迟 | `{}` |
|
||||
| 变量名 | 类型 | 说明 | 示例 |
|
||||
| ------------------- | --------- | ---------------------------------------------------------------------- | ----- |
|
||||
| `TG_MAX_ADDRESS` | 数字 | telegram bot 最多绑定邮箱数量 | `5` |
|
||||
| `TG_BOT_INFO` | 文本 | 可不配置,telegram BOT_INFO,预定义的 BOT_INFO 可以降低 webhook 的延迟 | `{}` |
|
||||
| `TG_ALLOW_USER_LANG`| 文本/JSON | 是否允许用户通过 `/lang` 命令切换语言,默认 `false` | `true`|
|
||||
|
||||
> [!NOTE]
|
||||
> Telegram 功能需要解析邮件,免费版 CPU 有限,可能会导致大邮件解析超时
|
||||
@@ -117,6 +118,33 @@
|
||||
>
|
||||
> 参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker)
|
||||
|
||||
## 邮件转发相关变量
|
||||
|
||||
| 变量名 | 类型 | 说明 | 示例 |
|
||||
| --------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------- | ------ |
|
||||
| `SUBDOMAIN_FORWARD_ADDRESS_LIST` | JSON | 子域名/规则转发配置,支持按域名和来源地址正则过滤转发 | 见下方 |
|
||||
|
||||
> [!NOTE] SUBDOMAIN_FORWARD_ADDRESS_LIST 配置说明
|
||||
>
|
||||
> v1.2.0 新增 `sourcePatterns` 和 `sourceMatchMode` 字段,支持按发件人地址正则过滤转发:
|
||||
>
|
||||
> - `domains`: 目标域名列表,为空则匹配所有域名
|
||||
> - `forward`: 转发目标地址
|
||||
> - `sourcePatterns`: 来源地址正则表达式列表(可选)
|
||||
> - `sourceMatchMode`: 匹配模式,`any`(任一匹配,默认) 或 `all`(全部匹配)
|
||||
>
|
||||
> 正则表达式最大长度 200 字符,防止 ReDoS 攻击
|
||||
>
|
||||
> ```toml
|
||||
> SUBDOMAIN_FORWARD_ADDRESS_LIST = """
|
||||
> [
|
||||
> {"domains":[""],"forward":"xxx1@xxx.com"},
|
||||
> {"domains":["subdomain-1.domain.com","subdomain-2.domain.com"],"forward":"xxx2@xxx.com"},
|
||||
> {"domains":["example.com"],"forward":"admin@xxx.com","sourcePatterns":[".*@github.com",".*@gitlab.com"],"sourceMatchMode":"any"}
|
||||
> ]
|
||||
> """
|
||||
> ```
|
||||
|
||||
## 其他变量
|
||||
|
||||
| 变量名 | 类型 | 说明 | 示例 |
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "temp-mail-docs",
|
||||
"private": true,
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.1",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/node": "^25.0.9",
|
||||
"vitepress": "^1.6.4",
|
||||
"wrangler": "^4.53.0"
|
||||
"wrangler": "^4.59.2"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vitepress dev docs",
|
||||
|
||||
1133
vitepress-docs/pnpm-lock.yaml
generated
1133
vitepress-docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -11,24 +11,24 @@
|
||||
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20251205.0",
|
||||
"@eslint/js": "9.18.0",
|
||||
"@cloudflare/workers-types": "^4.20260118.0",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@simplewebauthn/types": "10.0.0",
|
||||
"@types/node": "^22.19.1",
|
||||
"eslint": "9.18.0",
|
||||
"globals": "^15.15.0",
|
||||
"typescript-eslint": "^8.48.1",
|
||||
"wrangler": "^4.53.0"
|
||||
"@types/node": "^25.0.9",
|
||||
"eslint": "9.39.1",
|
||||
"globals": "^16.5.0",
|
||||
"typescript-eslint": "^8.53.0",
|
||||
"wrangler": "^4.59.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.888.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.888.0",
|
||||
"@simplewebauthn/server": "10.0.1",
|
||||
"hono": "^4.10.7",
|
||||
"hono": "^4.11.4",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"mimetext": "^3.0.27",
|
||||
"postal-mime": "^2.6.1",
|
||||
"resend": "^4.8.0",
|
||||
"postal-mime": "^2.7.3",
|
||||
"resend": "^6.7.0",
|
||||
"telegraf": "4.16.3",
|
||||
"worker-mailer": "^1.2.1"
|
||||
},
|
||||
|
||||
1615
worker/pnpm-lock.yaml
generated
1615
worker/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,14 +3,12 @@ import { handleListQuery } from "../common";
|
||||
|
||||
export default {
|
||||
getMails: async (c: Context<HonoCustomType>) => {
|
||||
const { address, limit, offset, keyword } = c.req.query();
|
||||
const { address, limit, offset } = 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 filterQuerys = [addressQuery].filter((item) => item).join(" and ");
|
||||
const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : "";
|
||||
const filterParams = [...addressParams, ...keywordParams]
|
||||
const filterParams = [...addressParams]
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails ${finalQuery}`,
|
||||
`SELECT count(*) as count FROM raw_mails ${finalQuery}`,
|
||||
|
||||
@@ -14,23 +14,24 @@ export default {
|
||||
return c.json(settings)
|
||||
},
|
||||
saveSetting: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const value = await c.req.json();
|
||||
const settings = new UserSettings(value);
|
||||
if (settings.enableMailVerify && !c.env.KV) {
|
||||
return c.text("Please enable KV first if you want to enable mail verify", 403)
|
||||
return c.text(msgs.EnableKVForMailVerifyMsg, 403)
|
||||
}
|
||||
if (settings.enableMailVerify && !settings.verifyMailSender) {
|
||||
return c.text("Please provide verifyMailSender", 400)
|
||||
return c.text(msgs.VerifyMailSenderNotSetMsg, 400)
|
||||
}
|
||||
if (settings.enableMailVerify && settings.verifyMailSender) {
|
||||
const mailDomain = settings.verifyMailSender.split("@")[1];
|
||||
const domains = getDomains(c);
|
||||
if (!domains.includes(mailDomain)) {
|
||||
return c.text(`VerifyMailSender(${settings.verifyMailSender}) domain must in ${JSON.stringify(domains, null, 2)}`, 400)
|
||||
return c.text(`${msgs.VerifyMailDomainInvalidMsg} ${JSON.stringify(domains, null, 2)}`, 400)
|
||||
}
|
||||
}
|
||||
if (settings.maxAddressCount < 0) {
|
||||
return c.text("Invalid maxAddressCount", 400)
|
||||
return c.text(msgs.InvalidMaxAddressCountMsg, 400)
|
||||
}
|
||||
await saveSetting(c, CONSTANTS.USER_SETTINGS_KEY, JSON.stringify(settings));
|
||||
return c.json({ success: true })
|
||||
@@ -60,9 +61,10 @@ export default {
|
||||
);
|
||||
},
|
||||
createUser: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { email, password } = await c.req.json();
|
||||
if (!email || !password) {
|
||||
return c.text("Invalid email or password", 400)
|
||||
return c.text(msgs.InvalidEmailOrPasswordMsg, 400)
|
||||
}
|
||||
// geo data
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
@@ -77,14 +79,14 @@ export default {
|
||||
email, password, JSON.stringify(userInfo)
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to register", 500)
|
||||
return c.text(msgs.FailedToRegisterMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
const errorMsg = (e as Error).message;
|
||||
if (errorMsg && errorMsg.includes("UNIQUE")) {
|
||||
return c.text("User already exists", 400)
|
||||
return c.text(msgs.UserAlreadyExistsMsg, 400)
|
||||
}
|
||||
return c.text(`Failed to register: ${errorMsg}`, 500)
|
||||
return c.text(`${msgs.FailedToRegisterMsg}: ${errorMsg}`, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
@@ -99,7 +101,7 @@ export default {
|
||||
`DELETE FROM users_address WHERE user_id = ?`
|
||||
).bind(user_id).run();
|
||||
if (!success || !addressSuccess) {
|
||||
return c.text("Failed to delete user", 500)
|
||||
return c.text(msgs.FailedDeleteUserMsg, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
@@ -114,28 +116,29 @@ export default {
|
||||
`UPDATE users SET password = ? WHERE id = ?`
|
||||
).bind(password, user_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to reset password", 500)
|
||||
return c.text(msgs.FailedUpdatePasswordMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
return c.text(`Failed to reset password: ${(e as Error).message}`, 500)
|
||||
return c.text(`${msgs.FailedUpdatePasswordMsg}: ${(e as Error).message}`, 500)
|
||||
}
|
||||
return c.json({ success: true });
|
||||
},
|
||||
updateUserRoles: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { user_id, role_text } = await c.req.json();
|
||||
if (!user_id) return c.text("Invalid user_id", 400);
|
||||
if (!user_id) return c.text(msgs.InvalidUserIdMsg, 400);
|
||||
if (!role_text) {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM user_roles WHERE user_id = ?`
|
||||
).bind(user_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to update user roles", 500)
|
||||
return c.text(msgs.FailedUpdateUserDefaultRoleMsg, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
}
|
||||
const user_roles = getUserRoles(c);
|
||||
if (!user_roles.find((r) => r.role === role_text)) {
|
||||
return c.text("Invalid role_text", 400)
|
||||
return c.text(msgs.InvalidRoleTextMsg, 400)
|
||||
}
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO user_roles (user_id, role_text)`
|
||||
@@ -143,7 +146,7 @@ export default {
|
||||
+ ` ON CONFLICT(user_id) DO UPDATE SET role_text = ?, updated_at = datetime('now')`
|
||||
).bind(user_id, role_text, role_text).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to update user roles", 500)
|
||||
return c.text(msgs.FailedUpdateUserDefaultRoleMsg, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
|
||||
@@ -3,16 +3,105 @@ import { Context } from 'hono';
|
||||
import { cleanup } from '../common';
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting } from '../utils';
|
||||
import { CleanupSettings } from '../models';
|
||||
import { CleanupSettings, CustomSqlCleanup } from '../models';
|
||||
import i18n from '../i18n';
|
||||
import { LocaleMessages } from '../i18n/type';
|
||||
|
||||
// SQL validation error types
|
||||
type SqlValidationError = 'empty' | 'too_long' | 'not_delete' | 'multiple_statements' | 'has_comments';
|
||||
|
||||
// Normalize SQL: trim and remove trailing semicolon
|
||||
const normalizeSql = (sql: string): string => {
|
||||
let normalized = sql.trim();
|
||||
if (normalized.endsWith(';')) {
|
||||
normalized = normalized.slice(0, -1).trim();
|
||||
}
|
||||
return normalized;
|
||||
};
|
||||
|
||||
// Get error message from error type
|
||||
const getValidationErrorMsg = (errorType: SqlValidationError, msgs: LocaleMessages): string => {
|
||||
switch (errorType) {
|
||||
case 'empty': return msgs.SqlEmptyMsg;
|
||||
case 'too_long': return msgs.SqlTooLongMsg;
|
||||
case 'not_delete': return msgs.SqlOnlyDeleteMsg;
|
||||
case 'multiple_statements': return msgs.SqlSingleStatementMsg;
|
||||
case 'has_comments': return msgs.SqlNoCommentsMsg;
|
||||
}
|
||||
};
|
||||
|
||||
// Validate custom SQL cleanup statement
|
||||
export const validateCustomSql = (sql: string): { valid: boolean; errorType?: SqlValidationError } => {
|
||||
if (!sql || !sql.trim()) {
|
||||
return { valid: false, errorType: 'empty' };
|
||||
}
|
||||
|
||||
const trimmedSql = normalizeSql(sql);
|
||||
|
||||
// Check SQL length (max 1000 characters)
|
||||
if (trimmedSql.length > 1000) {
|
||||
return { valid: false, errorType: 'too_long' };
|
||||
}
|
||||
|
||||
const sqlUpper = trimmedSql.toUpperCase();
|
||||
|
||||
// Only allow DELETE statements
|
||||
if (!sqlUpper.startsWith('DELETE ')) {
|
||||
return { valid: false, errorType: 'not_delete' };
|
||||
}
|
||||
|
||||
// Only allow single statement (no semicolons after trimming)
|
||||
if (trimmedSql.includes(';')) {
|
||||
return { valid: false, errorType: 'multiple_statements' };
|
||||
}
|
||||
|
||||
// Forbid SQL comments
|
||||
if (/--/.test(trimmedSql) || /\/\*/.test(trimmedSql)) {
|
||||
return { valid: false, errorType: 'has_comments' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
};
|
||||
|
||||
// Execute custom SQL cleanup
|
||||
export const executeCustomSqlCleanup = async (
|
||||
c: Context<HonoCustomType>,
|
||||
customSql: CustomSqlCleanup
|
||||
): Promise<{ success: boolean; rowsAffected?: number; error?: string }> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!customSql || !customSql.sql) {
|
||||
return { success: false, error: msgs.InvalidCleanupConfigMsg };
|
||||
}
|
||||
|
||||
const validation = validateCustomSql(customSql.sql);
|
||||
if (!validation.valid) {
|
||||
return { success: false, error: getValidationErrorMsg(validation.errorType!, msgs) };
|
||||
}
|
||||
|
||||
const sql = normalizeSql(customSql.sql);
|
||||
|
||||
try {
|
||||
console.log(`Executing custom SQL cleanup [${customSql.name}]: ${sql}`);
|
||||
const result = await c.env.DB.prepare(sql).run();
|
||||
const rowsAffected = result.meta?.changes ?? 0;
|
||||
console.log(`Custom SQL cleanup [${customSql.name}] completed, rows affected: ${rowsAffected}`);
|
||||
return { success: true, rowsAffected };
|
||||
} catch (error) {
|
||||
const errorMessage = (error as Error).message || "Unknown error";
|
||||
console.error(`Custom SQL cleanup [${customSql.name}] failed:`, errorMessage);
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
cleanup: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { cleanType, cleanDays } = await c.req.json();
|
||||
try {
|
||||
await cleanup(c, cleanType, cleanDays);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return c.text(`Failed to cleanup ${(error as Error).message}`, 500)
|
||||
return c.text(`${msgs.OperationFailedMsg}: ${(error as Error).message}`, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
@@ -21,7 +110,22 @@ export default {
|
||||
return c.json(cleanupSetting)
|
||||
},
|
||||
saveCleanup: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const cleanupSetting = await c.req.json<CleanupSettings>();
|
||||
|
||||
// Validate custom SQL cleanup list
|
||||
if (cleanupSetting.customSqlCleanupList && cleanupSetting.customSqlCleanupList.length > 0) {
|
||||
for (const customSql of cleanupSetting.customSqlCleanupList) {
|
||||
if (customSql.sql) {
|
||||
const validation = validateCustomSql(customSql.sql);
|
||||
if (!validation.valid) {
|
||||
const errorMsg = getValidationErrorMsg(validation.errorType!, msgs);
|
||||
return c.text(`[${customSql.name || 'unnamed'}]: ${errorMsg}`, 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting));
|
||||
return c.json({ success: true })
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS address (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE,
|
||||
password TEXT,
|
||||
source_meta TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -31,6 +32,8 @@ CREATE INDEX IF NOT EXISTS idx_address_created_at ON address(created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_address_updated_at ON address(updated_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_address_source_meta ON address(source_meta);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
||||
id INTEGER PRIMARY KEY,
|
||||
source_prefix TEXT,
|
||||
@@ -138,14 +141,42 @@ export default {
|
||||
},
|
||||
migrate: async (c: Context<HonoCustomType>) => {
|
||||
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||
if (version == "v0.0.2") {
|
||||
// example migration from v0.0.2 to v0.0.3
|
||||
const query = `ALTER TABLE address ADD password TEXT;`
|
||||
await c.env.DB.exec(query);
|
||||
if (version && version <= "v0.0.2") {
|
||||
// migration to v0.0.3: add password column
|
||||
const tableInfo = await c.env.DB.prepare(
|
||||
`PRAGMA table_info(address)`
|
||||
).all();
|
||||
const hasPassword = tableInfo.results?.some(
|
||||
(col: any) => col.name === 'password'
|
||||
);
|
||||
if (!hasPassword) {
|
||||
await c.env.DB.exec(`ALTER TABLE address ADD COLUMN password TEXT;`);
|
||||
}
|
||||
}
|
||||
if (version == "v0.0.3") {
|
||||
// migration from v0.0.3 to v0.0.4
|
||||
await c.env.DB.exec(`ALTER TABLE raw_mails ADD COLUMN metadata TEXT;`);
|
||||
if (version && version <= "v0.0.3") {
|
||||
// migration to v0.0.4: add metadata column
|
||||
const tableInfo = await c.env.DB.prepare(
|
||||
`PRAGMA table_info(raw_mails)`
|
||||
).all();
|
||||
const hasMetadata = tableInfo.results?.some(
|
||||
(col: any) => col.name === 'metadata'
|
||||
);
|
||||
if (!hasMetadata) {
|
||||
await c.env.DB.exec(`ALTER TABLE raw_mails ADD COLUMN metadata TEXT;`);
|
||||
}
|
||||
}
|
||||
if (version && version <= "v0.0.4") {
|
||||
// migration to v0.0.5: add source_meta column
|
||||
const tableInfo = await c.env.DB.prepare(
|
||||
`PRAGMA table_info(address)`
|
||||
).all();
|
||||
const hasSourceMeta = tableInfo.results?.some(
|
||||
(col: any) => col.name === 'source_meta'
|
||||
);
|
||||
if (!hasSourceMeta) {
|
||||
await c.env.DB.exec(`ALTER TABLE address ADD COLUMN source_meta TEXT;`);
|
||||
await c.env.DB.exec(`CREATE INDEX IF NOT EXISTS idx_address_source_meta ON address(source_meta);`);
|
||||
}
|
||||
}
|
||||
if (version != CONSTANTS.DB_VERSION) {
|
||||
// remove all \r and \n characters from the query string
|
||||
|
||||
@@ -45,10 +45,9 @@ api.get('/admin/address', async (c) => {
|
||||
|
||||
api.post('/admin/new_address', async (c) => {
|
||||
const { name, domain, enablePrefix } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!name) {
|
||||
return c.text("Please provide a name", 400)
|
||||
return c.text(msgs.RequiredFieldMsg, 400)
|
||||
}
|
||||
try {
|
||||
const res = await newAddress(c, {
|
||||
@@ -57,6 +56,7 @@ api.post('/admin/new_address', async (c) => {
|
||||
addressPrefix: null,
|
||||
checkAllowDomains: false,
|
||||
enableCheckNameRegex: false,
|
||||
sourceMeta: 'admin'
|
||||
});
|
||||
|
||||
return c.json(res);
|
||||
@@ -66,19 +66,20 @@ api.post('/admin/new_address', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/admin/delete_address/:id', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM address WHERE id = ? `
|
||||
).bind(id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to delete address", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE address IN`
|
||||
+ ` (select name from address where id = ?) `
|
||||
).bind(id).run();
|
||||
if (!mailSuccess) {
|
||||
return c.text("Failed to delete mails", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
const { success: sendAccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM address_sender WHERE address IN`
|
||||
@@ -93,13 +94,14 @@ api.delete('/admin/delete_address/:id', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/admin/clear_inbox/:id', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { id } = c.req.param();
|
||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE address IN`
|
||||
+ ` (select name from address where id = ?) `
|
||||
).bind(id).run();
|
||||
if (!mailSuccess) {
|
||||
return c.text("Failed to clear inbox", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({
|
||||
success: mailSuccess
|
||||
@@ -107,13 +109,14 @@ api.delete('/admin/clear_inbox/:id', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/admin/clear_sent_items/:id', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { id } = c.req.param();
|
||||
const { success: sendboxSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM sendbox WHERE address IN`
|
||||
+ ` (select name from address where id = ?) `
|
||||
).bind(id).run();
|
||||
if (!sendboxSuccess) {
|
||||
return c.text("Failed to clear sent items", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({
|
||||
success: sendboxSuccess
|
||||
@@ -135,15 +138,16 @@ api.get('/admin/show_password/:id', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/admin/address/:id/reset_password', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { id } = c.req.param();
|
||||
const { password } = await c.req.json();
|
||||
// 检查功能是否启用
|
||||
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
||||
return c.text("Password management is disabled", 403);
|
||||
return c.text(msgs.PasswordChangeDisabledMsg, 403);
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
return c.text("Password is required", 400);
|
||||
return c.text(msgs.NewPasswordRequiredMsg, 400);
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
@@ -152,7 +156,7 @@ api.post('/admin/address/:id/reset_password', async (c) => {
|
||||
).bind(hashedPassword, id).run();
|
||||
|
||||
if (!success) {
|
||||
return c.text("Failed to reset password", 500);
|
||||
return c.text(msgs.FailedUpdatePasswordMsg, 500);
|
||||
}
|
||||
|
||||
return c.json({ success: true });
|
||||
@@ -180,18 +184,19 @@ api.get('/admin/address_sender', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/admin/address_sender', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
/* eslint-disable prefer-const */
|
||||
let { address, address_id, balance, enabled } = await c.req.json();
|
||||
/* eslint-enable prefer-const */
|
||||
if (!address_id) {
|
||||
return c.text("Invalid address_id", 400)
|
||||
return c.text(msgs.InvalidAddressIdMsg, 400)
|
||||
}
|
||||
enabled = enabled ? 1 : 0;
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`UPDATE address_sender SET enabled = ?, balance = ? WHERE id = ? `
|
||||
).bind(enabled, balance, address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to update address sender", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
await sendAdminInternalMail(
|
||||
c, address, "Account Send Access Updated",
|
||||
@@ -290,16 +295,17 @@ api.get('/admin/account_settings', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/admin/account_settings', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
/** @type {{ blockList: Array<string>, sendBlockList: Array<string> }} */
|
||||
const {
|
||||
blockList, sendBlockList, noLimitSendAddressList,
|
||||
verifiedAddressList, fromBlockList, emailRuleSettings
|
||||
} = await c.req.json();
|
||||
if (!blockList || !sendBlockList || !verifiedAddressList) {
|
||||
return c.text("Invalid blockList or sendBlockList", 400)
|
||||
return c.text(msgs.InvalidInputMsg, 400)
|
||||
}
|
||||
if (!c.env.SEND_MAIL && verifiedAddressList.length > 0) {
|
||||
return c.text("Please enable SEND_MAIL to use verifiedAddressList", 400)
|
||||
return c.text(msgs.EnableSendMailMsg, 400)
|
||||
}
|
||||
await saveSetting(
|
||||
c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY,
|
||||
@@ -314,7 +320,7 @@ api.post('/admin/account_settings', async (c) => {
|
||||
JSON.stringify(verifiedAddressList)
|
||||
)
|
||||
if (fromBlockList?.length > 0 && !c.env.KV) {
|
||||
return c.text("Please enable KV to use fromBlockList", 400)
|
||||
return c.text(msgs.EnableKVMsg, 400)
|
||||
}
|
||||
if (fromBlockList) {
|
||||
await c.env.KV.put(CONSTANTS.EMAIL_KV_BLACK_LIST, JSON.stringify(fromBlockList || []))
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Context } from "hono";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getJsonSetting, saveSetting } from "../utils";
|
||||
import { IpBlacklistSettings } from "../ip_blacklist";
|
||||
import i18n from "../i18n";
|
||||
|
||||
/**
|
||||
* Get IP blacklist settings from database
|
||||
@@ -26,55 +27,47 @@ async function getIpBlacklistSettings(c: Context<HonoCustomType>): Promise<Respo
|
||||
* Save IP blacklist settings to database
|
||||
*/
|
||||
async function saveIpBlacklistSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const settings = await c.req.json<IpBlacklistSettings>();
|
||||
|
||||
// Validate settings
|
||||
if (typeof settings.enabled !== 'boolean') {
|
||||
return c.text("Invalid enabled value", 400);
|
||||
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: enabled`, 400);
|
||||
}
|
||||
|
||||
if (!Array.isArray(settings.blacklist)) {
|
||||
return c.text("Invalid blacklist value", 400);
|
||||
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: blacklist`, 400);
|
||||
}
|
||||
|
||||
if (!Array.isArray(settings.asnBlacklist)) {
|
||||
return c.text("Invalid asnBlacklist value", 400);
|
||||
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: asnBlacklist`, 400);
|
||||
}
|
||||
|
||||
if (!Array.isArray(settings.fingerprintBlacklist)) {
|
||||
return c.text("Invalid fingerprintBlacklist value", 400);
|
||||
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: fingerprintBlacklist`, 400);
|
||||
}
|
||||
|
||||
if (typeof settings.enableDailyLimit !== 'boolean') {
|
||||
return c.text("Invalid enableDailyLimit value", 400);
|
||||
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: enableDailyLimit`, 400);
|
||||
}
|
||||
|
||||
const limit = Number(settings.dailyRequestLimit);
|
||||
if (isNaN(limit) || limit < 1 || limit > 1000000) {
|
||||
return c.text("Invalid dailyRequestLimit value (must be between 1 and 1000000)", 400);
|
||||
return c.text(`${msgs.InvalidIpBlacklistSettingMsg}: dailyRequestLimit (1-1000000)`, 400);
|
||||
}
|
||||
|
||||
// Add size limit
|
||||
const MAX_BLACKLIST_SIZE = 1000;
|
||||
if (settings.blacklist.length > MAX_BLACKLIST_SIZE) {
|
||||
return c.text(
|
||||
`Blacklist exceeds maximum size (${MAX_BLACKLIST_SIZE} entries)`,
|
||||
400
|
||||
);
|
||||
return c.text(`${msgs.BlacklistExceedsMaxSizeMsg}: blacklist (${MAX_BLACKLIST_SIZE})`, 400);
|
||||
}
|
||||
|
||||
if (settings.asnBlacklist.length > MAX_BLACKLIST_SIZE) {
|
||||
return c.text(
|
||||
`ASN blacklist exceeds maximum size (${MAX_BLACKLIST_SIZE} entries)`,
|
||||
400
|
||||
);
|
||||
return c.text(`${msgs.BlacklistExceedsMaxSizeMsg}: asnBlacklist (${MAX_BLACKLIST_SIZE})`, 400);
|
||||
}
|
||||
|
||||
if (settings.fingerprintBlacklist.length > MAX_BLACKLIST_SIZE) {
|
||||
return c.text(
|
||||
`Fingerprint blacklist exceeds maximum size (${MAX_BLACKLIST_SIZE} entries)`,
|
||||
400
|
||||
);
|
||||
return c.text(`${msgs.BlacklistExceedsMaxSizeMsg}: fingerprintBlacklist (${MAX_BLACKLIST_SIZE})`, 400);
|
||||
}
|
||||
|
||||
// Sanitize patterns (trim and remove empty strings)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles,
|
||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
|
||||
import i18n from './i18n';
|
||||
|
||||
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
|
||||
|
||||
@@ -123,6 +124,7 @@ export const newAddress = async (
|
||||
addressPrefix = null,
|
||||
checkAllowDomains = true,
|
||||
enableCheckNameRegex = true,
|
||||
sourceMeta = null,
|
||||
}: {
|
||||
name: string, domain: string | undefined | null,
|
||||
enablePrefix: boolean,
|
||||
@@ -130,8 +132,10 @@ export const newAddress = async (
|
||||
addressPrefix?: string | undefined | null,
|
||||
checkAllowDomains?: boolean,
|
||||
enableCheckNameRegex?: boolean,
|
||||
sourceMeta?: string | undefined | null,
|
||||
}
|
||||
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
// trim whitespace and remove special characters
|
||||
name = name.trim().replace(getNameRegex(c), '')
|
||||
// check name
|
||||
@@ -151,10 +155,10 @@ export const newAddress = async (
|
||||
);
|
||||
// check name length
|
||||
if (name.length < minAddressLength) {
|
||||
throw new Error(`Name too short (min ${minAddressLength})`);
|
||||
throw new Error(`${msgs.NameTooShortMsg} (min ${minAddressLength})`);
|
||||
}
|
||||
if (name.length > maxAddressLength) {
|
||||
throw new Error(`Name too long (max ${maxAddressLength})`);
|
||||
throw new Error(`${msgs.NameTooLongMsg} (max ${maxAddressLength})`);
|
||||
}
|
||||
// create address with prefix
|
||||
if (typeof addressPrefix === "string") {
|
||||
@@ -175,24 +179,35 @@ export const newAddress = async (
|
||||
}
|
||||
// check domain is valid
|
||||
if (!domain || !allowDomains.includes(domain)) {
|
||||
throw new Error("Invalid domain")
|
||||
throw new Error(msgs.InvalidDomainMsg)
|
||||
}
|
||||
// create address
|
||||
name = name + "@" + domain;
|
||||
try {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO address(name) VALUES(?)`
|
||||
).bind(name).run();
|
||||
if (!success) {
|
||||
throw new Error("Failed to create address")
|
||||
// Try insert with source_meta field first
|
||||
const result = await c.env.DB.prepare(
|
||||
`INSERT INTO address(name, source_meta) VALUES(?, ?)`
|
||||
).bind(name, sourceMeta).run();
|
||||
if (!result.success) {
|
||||
throw new Error(msgs.FailedCreateAddressMsg)
|
||||
}
|
||||
await updateAddressUpdatedAt(c, name);
|
||||
} catch (e) {
|
||||
const message = (e as Error).message;
|
||||
if (message && message.includes("UNIQUE")) {
|
||||
throw new Error("Address already exists")
|
||||
// Fallback: source_meta field may not exist, try without it
|
||||
if (message && message.includes("source_meta")) {
|
||||
const result = await c.env.DB.prepare(
|
||||
`INSERT INTO address(name) VALUES(?)`
|
||||
).bind(name).run();
|
||||
if (!result.success) {
|
||||
throw new Error(msgs.FailedCreateAddressMsg)
|
||||
}
|
||||
await updateAddressUpdatedAt(c, name);
|
||||
} else if (message && message.includes("UNIQUE")) {
|
||||
throw new Error(msgs.AddressAlreadyExistsMsg)
|
||||
} else {
|
||||
throw new Error(msgs.FailedCreateAddressMsg)
|
||||
}
|
||||
throw new Error("Failed to create address")
|
||||
}
|
||||
const address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where name = ?`
|
||||
@@ -234,8 +249,9 @@ export const cleanup = async (
|
||||
cleanType: string | undefined | null,
|
||||
cleanDays: number | undefined | null
|
||||
): Promise<boolean> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 1000) {
|
||||
throw new Error("Invalid cleanType or cleanDays")
|
||||
throw new Error(msgs.InvalidCleanupConfigMsg)
|
||||
}
|
||||
console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
|
||||
switch (cleanType) {
|
||||
@@ -281,7 +297,7 @@ export const cleanup = async (
|
||||
)
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid cleanType")
|
||||
throw new Error(msgs.InvalidCleanTypeMsg)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -323,11 +339,12 @@ export const deleteAddressWithData = async (
|
||||
address: string | undefined | null,
|
||||
address_id: number | undefined | null
|
||||
): Promise<boolean> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
throw new Error("Delete email is disabled")
|
||||
throw new Error(msgs.UserDeleteEmailDisabledMsg)
|
||||
}
|
||||
if (!address && !address_id) {
|
||||
throw new Error("Address or address_id required")
|
||||
throw new Error(msgs.RequiredFieldMsg)
|
||||
}
|
||||
// get address_id or address
|
||||
if (!address_id) {
|
||||
@@ -341,7 +358,7 @@ export const deleteAddressWithData = async (
|
||||
}
|
||||
// check address again
|
||||
if (!address || !address_id) {
|
||||
throw new Error("Can't find address");
|
||||
throw new Error(msgs.AddressNotFoundMsg);
|
||||
}
|
||||
// unbind telegram
|
||||
await unbindTelegramByAddress(c, address);
|
||||
@@ -365,7 +382,7 @@ export const deleteAddressWithData = async (
|
||||
`DELETE FROM address WHERE name = ? `
|
||||
).bind(address).run();
|
||||
if (!success || !mailSuccess || !sendboxSuccess || !addressSuccess || !sendAccess || !autoReplySuccess) {
|
||||
throw new Error("Failed to delete address")
|
||||
throw new Error(msgs.OperationFailedMsg)
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -376,6 +393,7 @@ export const handleListQuery = async (
|
||||
limit: string | number | undefined | null,
|
||||
offset: string | number | undefined | null
|
||||
): Promise<Response> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (typeof limit === "string") {
|
||||
limit = parseInt(limit);
|
||||
}
|
||||
@@ -383,10 +401,10 @@ export const handleListQuery = async (
|
||||
offset = parseInt(offset);
|
||||
}
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
return c.text(msgs.InvalidLimitMsg, 400)
|
||||
}
|
||||
if (offset == null || offset == undefined || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
return c.text(msgs.InvalidOffsetMsg, 400)
|
||||
}
|
||||
const resultsQuery = `${query} order by id desc limit ? offset ?`;
|
||||
const { results } = await c.env.DB.prepare(resultsQuery).bind(
|
||||
|
||||
@@ -3,7 +3,7 @@ export const CONSTANTS = {
|
||||
|
||||
// DB Version
|
||||
DB_VERSION_KEY: 'db_version',
|
||||
DB_VERSION: "v0.0.4",
|
||||
DB_VERSION: "v0.0.5",
|
||||
|
||||
// DB settings
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
|
||||
139
worker/src/email/forward.ts
Normal file
139
worker/src/email/forward.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { Context } from "hono";
|
||||
|
||||
import { getEnvStringList, getJsonObjectValue, getJsonSetting } from "../utils";
|
||||
import { EmailRuleSettings } from "../models";
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
// 正则表达式最大长度限制,防止 ReDoS 攻击
|
||||
const MAX_REGEX_PATTERN_LENGTH = 200;
|
||||
|
||||
/**
|
||||
* 安全地测试单个正则表达式
|
||||
*/
|
||||
function safeRegexTest(pattern: string, input: string): boolean {
|
||||
try {
|
||||
// 限制正则复杂度:最大长度限制
|
||||
if (pattern.length > MAX_REGEX_PATTERN_LENGTH) {
|
||||
console.warn("source pattern too long, skipped:", pattern.substring(0, 50) + "...");
|
||||
return false;
|
||||
}
|
||||
const regex = new RegExp(pattern, 'i');
|
||||
return regex.test(input);
|
||||
} catch (regexError) {
|
||||
console.error("regex test error for pattern:", pattern, regexError);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查来源地址是否匹配正则规则
|
||||
*/
|
||||
function matchSourcePatterns(
|
||||
from: string,
|
||||
sourcePatterns: string[] | undefined | null,
|
||||
sourceMatchMode: 'any' | 'all' | undefined
|
||||
): boolean {
|
||||
if (!sourcePatterns || sourcePatterns.length === 0) {
|
||||
// 未配置来源正则,默认匹配
|
||||
return true;
|
||||
}
|
||||
|
||||
const matchMode = sourceMatchMode || 'any';
|
||||
|
||||
if (matchMode === 'all') {
|
||||
// 全部匹配模式:所有正则都必须匹配
|
||||
return sourcePatterns.every(pattern => safeRegexTest(pattern, from));
|
||||
} else {
|
||||
// 任一匹配模式(默认):任一正则匹配即可
|
||||
return sourcePatterns.some(pattern => safeRegexTest(pattern, from));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全局转发:转发到 FORWARD_ADDRESS_LIST 中的所有地址
|
||||
*/
|
||||
async function forwardToGlobalAddresses(
|
||||
message: ForwardableEmailMessage,
|
||||
env: Bindings
|
||||
): Promise<void> {
|
||||
try {
|
||||
const forwardAddressList = getEnvStringList(env.FORWARD_ADDRESS_LIST);
|
||||
for (const forwardAddress of forwardAddressList) {
|
||||
await message.forward(forwardAddress);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("forward email error", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规则转发:根据域名和来源地址正则规则转发
|
||||
*/
|
||||
async function forwardByRules(
|
||||
message: ForwardableEmailMessage,
|
||||
env: Bindings
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 获取环境变量配置
|
||||
const subdomainForwardAddressList = getJsonObjectValue<SubdomainForwardAddressList[]>(
|
||||
env.SUBDOMAIN_FORWARD_ADDRESS_LIST
|
||||
) || [];
|
||||
|
||||
// 获取数据库配置
|
||||
const emailRuleSettings = await getJsonSetting<EmailRuleSettings>(
|
||||
{ env: env } as Context<HonoCustomType>,
|
||||
CONSTANTS.EMAIL_RULE_SETTINGS_KEY
|
||||
);
|
||||
|
||||
// 合并两个配置,env 里的配置优先级更高
|
||||
const allRules = [
|
||||
...(subdomainForwardAddressList || []),
|
||||
...(emailRuleSettings?.emailForwardingList || []),
|
||||
];
|
||||
|
||||
for (const rule of allRules) {
|
||||
// 检查来源地址是否匹配正则
|
||||
if (!matchSourcePatterns(message.from, rule.sourcePatterns, rule.sourceMatchMode)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查目标地址是否匹配域名,并转发
|
||||
// 保持原始逻辑:每个匹配的 domain 都会触发一次转发
|
||||
if (rule.domains && rule.domains.length > 0) {
|
||||
for (const domain of rule.domains) {
|
||||
if (message.to.endsWith(domain) && rule.forward) {
|
||||
await message.forward(rule.forward);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 域名为空,转发所有邮件
|
||||
if (rule.forward) {
|
||||
await message.forward(rule.forward);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("forward by rules error", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行所有转发逻辑
|
||||
*/
|
||||
async function forwardEmail(
|
||||
message: ForwardableEmailMessage,
|
||||
env: Bindings
|
||||
): Promise<void> {
|
||||
// 全局转发
|
||||
await forwardToGlobalAddresses(message, env);
|
||||
|
||||
// 规则转发
|
||||
await forwardByRules(message, env);
|
||||
}
|
||||
|
||||
export {
|
||||
forwardEmail,
|
||||
forwardToGlobalAddresses,
|
||||
forwardByRules,
|
||||
matchSourcePatterns,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Context } from "hono";
|
||||
|
||||
import { getEnvStringList, getJsonObjectValue, getJsonSetting } from "../utils";
|
||||
import { getJsonSetting } from "../utils";
|
||||
import { sendMailToTelegram } from "../telegram_api";
|
||||
import { auto_reply } from "./auto_reply";
|
||||
import { isBlocked } from "./black_list";
|
||||
@@ -8,6 +8,7 @@ import { triggerWebhook, triggerAnotherWorker, commonParseMail } from "../common
|
||||
import { check_if_junk_mail } from "./check_junk";
|
||||
import { remove_attachment_if_need } from "./check_attachment";
|
||||
import { extractEmailInfo } from "./ai_extract";
|
||||
import { forwardEmail } from "./forward";
|
||||
import { EmailRuleSettings } from "../models";
|
||||
import { CONSTANTS } from "../constants";
|
||||
|
||||
@@ -79,46 +80,7 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
|
||||
}
|
||||
|
||||
// forward email
|
||||
try {
|
||||
const forwardAddressList = getEnvStringList(env.FORWARD_ADDRESS_LIST)
|
||||
for (const forwardAddress of forwardAddressList) {
|
||||
await message.forward(forwardAddress);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("forward email error", error);
|
||||
}
|
||||
|
||||
// forward subdomain email
|
||||
try {
|
||||
// 遍历 FORWARD_ADDRESS_LIST
|
||||
const subdomainForwardAddressList = getJsonObjectValue<SubdomainForwardAddressList[]>(env.SUBDOMAIN_FORWARD_ADDRESS_LIST) || [];
|
||||
const emailRuleSettings = await getJsonSetting<EmailRuleSettings>(
|
||||
{ env: env } as Context<HonoCustomType>, CONSTANTS.EMAIL_RULE_SETTINGS_KEY
|
||||
);
|
||||
// 合并两个配置, env 里的配置优先级更高
|
||||
const allSubdomainForwardAddressList = [
|
||||
...(subdomainForwardAddressList || []),
|
||||
...(emailRuleSettings?.emailForwardingList || []),
|
||||
];
|
||||
for (const subdomainForwardAddress of allSubdomainForwardAddressList) {
|
||||
// 检查邮件是否匹配 domains
|
||||
if (subdomainForwardAddress.domains && subdomainForwardAddress.domains.length > 0) {
|
||||
for (const domain of subdomainForwardAddress.domains) {
|
||||
if (message.to.endsWith(domain) && subdomainForwardAddress.forward) {
|
||||
// 转发邮件
|
||||
await message.forward(subdomainForwardAddress.forward);
|
||||
// 支持多邮箱转发收件,不进行截止
|
||||
// break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果 domains 为空,则转发所有邮件
|
||||
await message.forward(subdomainForwardAddress.forward);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("subdomain forward email error", error);
|
||||
}
|
||||
await forwardEmail(message, env);
|
||||
|
||||
// send email to telegram
|
||||
try {
|
||||
|
||||
@@ -46,6 +46,129 @@ const messages: LocaleMessages = {
|
||||
PasswordLoginDisabledMsg: "Password login is disabled",
|
||||
EmailPasswordRequiredMsg: "Email and password are required",
|
||||
AddressNotFoundMsg: "Address not found",
|
||||
|
||||
// Common messages (merged similar ones)
|
||||
OperationFailedMsg: "Operation failed",
|
||||
RequiredFieldMsg: "Required field is missing",
|
||||
InvalidInputMsg: "Invalid input",
|
||||
|
||||
// Address related
|
||||
NameTooShortMsg: "Name is too short",
|
||||
NameTooLongMsg: "Name is too long",
|
||||
InvalidDomainMsg: "Invalid domain",
|
||||
AddressAlreadyExistsMsg: "Address already exists",
|
||||
MaxAddressCountReachedMsg: "Max address count reached",
|
||||
AddressNotBindedMsg: "Address is not binded",
|
||||
AddressAlreadyBindedMsg: "Address is already binded, please unbind first",
|
||||
TargetUserNotFoundMsg: "Target user not found",
|
||||
|
||||
// Send mail related
|
||||
NoBalanceMsg: "No balance",
|
||||
AddressBlockedMsg: "Address is blocked",
|
||||
SubjectEmptyMsg: "Subject is empty",
|
||||
ContentEmptyMsg: "Content is empty",
|
||||
AlreadyRequestedMsg: "Already requested",
|
||||
EnableResendOrSmtpMsg: "Please enable resend or smtp for this domain",
|
||||
EnableResendOrSmtpWithVerifiedMsg: "Please enable resend or smtp for this domain, or add recipient to verified address list",
|
||||
InvalidToMailMsg: "Invalid recipient address",
|
||||
|
||||
// Admin related
|
||||
InvalidAddressIdMsg: "Invalid address_id",
|
||||
EnableKVMsg: "Please enable KV first",
|
||||
EnableSendMailMsg: "Please enable SEND_MAIL first",
|
||||
InvalidCleanupConfigMsg: "Invalid cleanType or cleanDays",
|
||||
InvalidCleanTypeMsg: "Invalid cleanType",
|
||||
EnableKVForMailVerifyMsg: "Please enable KV first if you want to enable mail verify",
|
||||
VerifyMailDomainInvalidMsg: "VerifyMailSender domain must be in",
|
||||
InvalidMaxAddressCountMsg: "Invalid maxAddressCount",
|
||||
FailedDeleteUserMsg: "Failed to delete user",
|
||||
InvalidUserIdMsg: "Invalid user_id",
|
||||
InvalidRoleTextMsg: "Invalid role_text",
|
||||
|
||||
// SQL validation
|
||||
SqlEmptyMsg: "SQL statement is empty",
|
||||
SqlTooLongMsg: "SQL statement is too long (max 1000 characters)",
|
||||
SqlOnlyDeleteMsg: "Only DELETE statements are allowed",
|
||||
SqlSingleStatementMsg: "Only single SQL statement is allowed",
|
||||
SqlNoCommentsMsg: "SQL comments are not allowed",
|
||||
|
||||
// Passkey related
|
||||
InvalidPasskeyNameMsg: "Invalid passkey name",
|
||||
PasskeyNotFoundMsg: "Passkey not found",
|
||||
AuthenticationFailedMsg: "Authentication failed",
|
||||
RegistrationFailedMsg: "Registration failed",
|
||||
|
||||
// Auto reply related
|
||||
AutoReplyDisabledMsg: "Auto reply is disabled",
|
||||
InvalidAutoReplyMsg: "Invalid subject or message",
|
||||
SubjectOrMessageTooLongMsg: "Subject or message is too long",
|
||||
|
||||
// Bind address related
|
||||
NoAddressOrUserTokenMsg: "No address or user token",
|
||||
InvalidAddressOrUserTokenMsg: "Invalid address or user token",
|
||||
|
||||
// Pagination related
|
||||
InvalidLimitMsg: "Invalid limit",
|
||||
InvalidOffsetMsg: "Invalid offset",
|
||||
|
||||
// Clear inbox/sent items related
|
||||
FailedClearInboxMsg: "Failed to clear inbox",
|
||||
FailedClearSentItemsMsg: "Failed to clear sent items",
|
||||
|
||||
// Webhook related
|
||||
WebhookNotAllowedForUserMsg: "Webhook settings is not allowed for this user",
|
||||
|
||||
// IP blacklist related
|
||||
InvalidIpBlacklistSettingMsg: "Invalid IP blacklist setting",
|
||||
BlacklistExceedsMaxSizeMsg: "Blacklist exceeds maximum size",
|
||||
|
||||
// Telegram bot messages
|
||||
TgUnableGetUserInfoMsg: "Unable to get user info",
|
||||
TgNoPermissionMsg: "You don't have permission to use this bot",
|
||||
TgWelcomeMsg: "Welcome! You can open the mini app",
|
||||
TgCurrentPrefixMsg: "Current prefix enabled:",
|
||||
TgCurrentDomainsMsg: "Available domains:",
|
||||
TgAvailableCommandsMsg: "Available commands:",
|
||||
TgCreateSuccessMsg: "Address created successfully:",
|
||||
TgCreateFailedMsg: "Failed to create address:",
|
||||
TgBindSuccessMsg: "Binding successful:",
|
||||
TgBindFailedMsg: "Binding failed:",
|
||||
TgUnbindSuccessMsg: "Unbinding successful:",
|
||||
TgUnbindFailedMsg: "Unbinding failed:",
|
||||
TgDeleteSuccessMsg: "Deleted successfully:",
|
||||
TgDeleteFailedMsg: "Delete failed:",
|
||||
TgAddressListMsg: "Address list:",
|
||||
TgGetAddressFailedMsg: "Failed to get address list:",
|
||||
TgCleanSuccessMsg: "Invalid addresses cleaned:",
|
||||
TgCurrentAddressListMsg: "Current address list:",
|
||||
TgCleanFailedMsg: "Failed to clean invalid addresses:",
|
||||
TgNotBoundAddressMsg: "This address is not bound:",
|
||||
TgInvalidAddressMsg: "Invalid address",
|
||||
TgNoMoreMailsMsg: "No more mails",
|
||||
TgNoMailMsg: "No mail",
|
||||
TgGetMailFailedMsg: "Failed to get mail:",
|
||||
TgParseMailFailedMsg: "Failed to parse mail:",
|
||||
TgViewMailBtnMsg: "View Mail",
|
||||
TgPrevBtnMsg: "Prev",
|
||||
TgNextBtnMsg: "Next",
|
||||
TgPleaseInputCredentialMsg: "Please enter credential",
|
||||
TgPleaseInputAddressMsg: "Please enter address",
|
||||
TgAddressMsg: "Address:",
|
||||
TgPasswordMsg: "Password:",
|
||||
TgCredentialMsg: "Credential:",
|
||||
TgNoSenderMsg: "No sender",
|
||||
TgMsgTooLongMsg: "Message too long, please view in mini app",
|
||||
TgParseFailedViewInAppMsg: "Parse failed, please view in mini app",
|
||||
TgMaxAddressReachedMsg: "Maximum address limit reached",
|
||||
TgMaxAddressReachedCleanMsg: "Maximum address limit reached, please /cleaninvalidaddress first",
|
||||
TgInvalidCredentialMsg: "Invalid credential",
|
||||
TgAddressNotYoursMsg: "This address does not belong to you",
|
||||
TgLangSetSuccessMsg: "Language set successfully:",
|
||||
TgCurrentLangMsg: "Current language:",
|
||||
TgSelectLangMsg: "Please select language:",
|
||||
TgNoPermissionViewMailMsg: "No permission to view this mail",
|
||||
TgBotTokenRequiredMsg: "TELEGRAM_BOT_TOKEN is required",
|
||||
TgLangFeatureDisabledMsg: "Language setting feature is disabled. System default language is used.",
|
||||
}
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -17,7 +17,7 @@ export default {
|
||||
getMessagesbyContext: (
|
||||
c: Context<HonoCustomType>
|
||||
): LocaleMessages => {
|
||||
const locale = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const locale = c?.get?.("lang") || c.env?.DEFAULT_LANG;
|
||||
// multi-language support
|
||||
if (locale === "en") return en;
|
||||
if (locale === "zh") return zh;
|
||||
|
||||
@@ -44,4 +44,127 @@ export type LocaleMessages = {
|
||||
PasswordLoginDisabledMsg: string
|
||||
EmailPasswordRequiredMsg: string
|
||||
AddressNotFoundMsg: string
|
||||
|
||||
// Common messages (merged similar ones)
|
||||
OperationFailedMsg: string
|
||||
RequiredFieldMsg: string
|
||||
InvalidInputMsg: string
|
||||
|
||||
// Address related
|
||||
NameTooShortMsg: string
|
||||
NameTooLongMsg: string
|
||||
InvalidDomainMsg: string
|
||||
AddressAlreadyExistsMsg: string
|
||||
MaxAddressCountReachedMsg: string
|
||||
AddressNotBindedMsg: string
|
||||
AddressAlreadyBindedMsg: string
|
||||
TargetUserNotFoundMsg: string
|
||||
|
||||
// Send mail related
|
||||
NoBalanceMsg: string
|
||||
AddressBlockedMsg: string
|
||||
SubjectEmptyMsg: string
|
||||
ContentEmptyMsg: string
|
||||
AlreadyRequestedMsg: string
|
||||
EnableResendOrSmtpMsg: string
|
||||
EnableResendOrSmtpWithVerifiedMsg: string
|
||||
InvalidToMailMsg: string
|
||||
|
||||
// Admin related
|
||||
InvalidAddressIdMsg: string
|
||||
EnableKVMsg: string
|
||||
EnableSendMailMsg: string
|
||||
InvalidCleanupConfigMsg: string
|
||||
InvalidCleanTypeMsg: string
|
||||
EnableKVForMailVerifyMsg: string
|
||||
VerifyMailDomainInvalidMsg: string
|
||||
InvalidMaxAddressCountMsg: string
|
||||
FailedDeleteUserMsg: string
|
||||
InvalidUserIdMsg: string
|
||||
InvalidRoleTextMsg: string
|
||||
|
||||
// SQL validation
|
||||
SqlEmptyMsg: string
|
||||
SqlTooLongMsg: string
|
||||
SqlOnlyDeleteMsg: string
|
||||
SqlSingleStatementMsg: string
|
||||
SqlNoCommentsMsg: string
|
||||
|
||||
// Passkey related
|
||||
InvalidPasskeyNameMsg: string
|
||||
PasskeyNotFoundMsg: string
|
||||
AuthenticationFailedMsg: string
|
||||
RegistrationFailedMsg: string
|
||||
|
||||
// Auto reply related
|
||||
AutoReplyDisabledMsg: string
|
||||
InvalidAutoReplyMsg: string
|
||||
SubjectOrMessageTooLongMsg: string
|
||||
|
||||
// Bind address related
|
||||
NoAddressOrUserTokenMsg: string
|
||||
InvalidAddressOrUserTokenMsg: string
|
||||
|
||||
// Pagination related
|
||||
InvalidLimitMsg: string
|
||||
InvalidOffsetMsg: string
|
||||
|
||||
// Clear inbox/sent items related
|
||||
FailedClearInboxMsg: string
|
||||
FailedClearSentItemsMsg: string
|
||||
|
||||
// Webhook related
|
||||
WebhookNotAllowedForUserMsg: string
|
||||
|
||||
// IP blacklist related
|
||||
InvalidIpBlacklistSettingMsg: string
|
||||
BlacklistExceedsMaxSizeMsg: string
|
||||
|
||||
// Telegram bot messages
|
||||
TgUnableGetUserInfoMsg: string
|
||||
TgNoPermissionMsg: string
|
||||
TgWelcomeMsg: string
|
||||
TgCurrentPrefixMsg: string
|
||||
TgCurrentDomainsMsg: string
|
||||
TgAvailableCommandsMsg: string
|
||||
TgCreateSuccessMsg: string
|
||||
TgCreateFailedMsg: string
|
||||
TgBindSuccessMsg: string
|
||||
TgBindFailedMsg: string
|
||||
TgUnbindSuccessMsg: string
|
||||
TgUnbindFailedMsg: string
|
||||
TgDeleteSuccessMsg: string
|
||||
TgDeleteFailedMsg: string
|
||||
TgAddressListMsg: string
|
||||
TgGetAddressFailedMsg: string
|
||||
TgCleanSuccessMsg: string
|
||||
TgCurrentAddressListMsg: string
|
||||
TgCleanFailedMsg: string
|
||||
TgNotBoundAddressMsg: string
|
||||
TgInvalidAddressMsg: string
|
||||
TgNoMoreMailsMsg: string
|
||||
TgNoMailMsg: string
|
||||
TgGetMailFailedMsg: string
|
||||
TgParseMailFailedMsg: string
|
||||
TgViewMailBtnMsg: string
|
||||
TgPrevBtnMsg: string
|
||||
TgNextBtnMsg: string
|
||||
TgPleaseInputCredentialMsg: string
|
||||
TgPleaseInputAddressMsg: string
|
||||
TgAddressMsg: string
|
||||
TgPasswordMsg: string
|
||||
TgCredentialMsg: string
|
||||
TgNoSenderMsg: string
|
||||
TgMsgTooLongMsg: string
|
||||
TgParseFailedViewInAppMsg: string
|
||||
TgMaxAddressReachedMsg: string
|
||||
TgMaxAddressReachedCleanMsg: string
|
||||
TgInvalidCredentialMsg: string
|
||||
TgAddressNotYoursMsg: string
|
||||
TgLangSetSuccessMsg: string
|
||||
TgCurrentLangMsg: string
|
||||
TgSelectLangMsg: string
|
||||
TgNoPermissionViewMailMsg: string
|
||||
TgBotTokenRequiredMsg: string
|
||||
TgLangFeatureDisabledMsg: string
|
||||
}
|
||||
|
||||
@@ -46,6 +46,129 @@ const messages: LocaleMessages = {
|
||||
PasswordLoginDisabledMsg: "密码登录已禁用",
|
||||
EmailPasswordRequiredMsg: "邮箱和密码不能为空",
|
||||
AddressNotFoundMsg: "邮箱地址不存在",
|
||||
|
||||
// Common messages (merged similar ones)
|
||||
OperationFailedMsg: "操作失败",
|
||||
RequiredFieldMsg: "缺少必填字段",
|
||||
InvalidInputMsg: "输入无效",
|
||||
|
||||
// Address related
|
||||
NameTooShortMsg: "名称太短",
|
||||
NameTooLongMsg: "名称太长",
|
||||
InvalidDomainMsg: "无效的域名",
|
||||
AddressAlreadyExistsMsg: "邮箱地址已存在",
|
||||
MaxAddressCountReachedMsg: "已达到最大地址数量限制",
|
||||
AddressNotBindedMsg: "邮箱地址未绑定",
|
||||
AddressAlreadyBindedMsg: "邮箱地址已绑定, 请先解绑",
|
||||
TargetUserNotFoundMsg: "目标用户不存在",
|
||||
|
||||
// Send mail related
|
||||
NoBalanceMsg: "余额不足",
|
||||
AddressBlockedMsg: "地址已被屏蔽",
|
||||
SubjectEmptyMsg: "主题不能为空",
|
||||
ContentEmptyMsg: "内容不能为空",
|
||||
AlreadyRequestedMsg: "已经申请过了",
|
||||
EnableResendOrSmtpMsg: "请先为此域名启用 resend 或 smtp",
|
||||
EnableResendOrSmtpWithVerifiedMsg: "请先为此域名启用 resend 或 smtp,或将收件人添加到已验证地址列表",
|
||||
InvalidToMailMsg: "收件人地址无效",
|
||||
|
||||
// Admin related
|
||||
InvalidAddressIdMsg: "无效的 address_id",
|
||||
EnableKVMsg: "请先启用 KV",
|
||||
EnableSendMailMsg: "请先启用 SEND_MAIL",
|
||||
InvalidCleanupConfigMsg: "无效的 cleanType 或 cleanDays",
|
||||
InvalidCleanTypeMsg: "无效的 cleanType",
|
||||
EnableKVForMailVerifyMsg: "如果要启用邮件验证,请先启用 KV",
|
||||
VerifyMailDomainInvalidMsg: "验证邮件发送者域名必须在",
|
||||
InvalidMaxAddressCountMsg: "无效的 maxAddressCount",
|
||||
FailedDeleteUserMsg: "删除用户失败",
|
||||
InvalidUserIdMsg: "无效的 user_id",
|
||||
InvalidRoleTextMsg: "无效的 role_text",
|
||||
|
||||
// SQL validation
|
||||
SqlEmptyMsg: "SQL 语句为空",
|
||||
SqlTooLongMsg: "SQL 语句过长 (最大 1000 字符)",
|
||||
SqlOnlyDeleteMsg: "只允许 DELETE 语句",
|
||||
SqlSingleStatementMsg: "只允许单条 SQL 语句",
|
||||
SqlNoCommentsMsg: "不允许 SQL 注释",
|
||||
|
||||
// Passkey related
|
||||
InvalidPasskeyNameMsg: "无效的 passkey 名称",
|
||||
PasskeyNotFoundMsg: "Passkey 不存在",
|
||||
AuthenticationFailedMsg: "认证失败",
|
||||
RegistrationFailedMsg: "注册失败",
|
||||
|
||||
// Auto reply related
|
||||
AutoReplyDisabledMsg: "自动回复已禁用",
|
||||
InvalidAutoReplyMsg: "无效的主题或消息",
|
||||
SubjectOrMessageTooLongMsg: "主题或消息太长",
|
||||
|
||||
// Bind address related
|
||||
NoAddressOrUserTokenMsg: "缺少地址或用户令牌",
|
||||
InvalidAddressOrUserTokenMsg: "无效的地址或用户令牌",
|
||||
|
||||
// Pagination related
|
||||
InvalidLimitMsg: "无效的 limit 参数",
|
||||
InvalidOffsetMsg: "无效的 offset 参数",
|
||||
|
||||
// Clear inbox/sent items related
|
||||
FailedClearInboxMsg: "清空收件箱失败",
|
||||
FailedClearSentItemsMsg: "清空已发送邮件失败",
|
||||
|
||||
// Webhook related
|
||||
WebhookNotAllowedForUserMsg: "此用户不允许使用 Webhook 设置",
|
||||
|
||||
// IP blacklist related
|
||||
InvalidIpBlacklistSettingMsg: "无效的 IP 黑名单设置",
|
||||
BlacklistExceedsMaxSizeMsg: "黑名单超出最大条目限制",
|
||||
|
||||
// Telegram bot messages
|
||||
TgUnableGetUserInfoMsg: "无法获取用户信息",
|
||||
TgNoPermissionMsg: "您没有权限使用此机器人",
|
||||
TgWelcomeMsg: "欢迎使用本机器人, 您可以打开 mini app",
|
||||
TgCurrentPrefixMsg: "当前已启用前缀:",
|
||||
TgCurrentDomainsMsg: "当前可用域名:",
|
||||
TgAvailableCommandsMsg: "请使用以下命令:",
|
||||
TgCreateSuccessMsg: "创建地址成功:",
|
||||
TgCreateFailedMsg: "创建地址失败:",
|
||||
TgBindSuccessMsg: "绑定成功:",
|
||||
TgBindFailedMsg: "绑定失败:",
|
||||
TgUnbindSuccessMsg: "解绑成功:",
|
||||
TgUnbindFailedMsg: "解绑失败:",
|
||||
TgDeleteSuccessMsg: "删除成功:",
|
||||
TgDeleteFailedMsg: "删除失败:",
|
||||
TgAddressListMsg: "地址列表:",
|
||||
TgGetAddressFailedMsg: "获取地址列表失败:",
|
||||
TgCleanSuccessMsg: "清理无效地址成功:",
|
||||
TgCurrentAddressListMsg: "当前地址列表:",
|
||||
TgCleanFailedMsg: "清理无效地址失败:",
|
||||
TgNotBoundAddressMsg: "未绑定此地址:",
|
||||
TgInvalidAddressMsg: "无效地址",
|
||||
TgNoMoreMailsMsg: "已经没有邮件了",
|
||||
TgNoMailMsg: "无邮件",
|
||||
TgGetMailFailedMsg: "获取邮件失败:",
|
||||
TgParseMailFailedMsg: "解析邮件失败:",
|
||||
TgViewMailBtnMsg: "查看邮件",
|
||||
TgPrevBtnMsg: "上一条",
|
||||
TgNextBtnMsg: "下一条",
|
||||
TgPleaseInputCredentialMsg: "请输入凭证",
|
||||
TgPleaseInputAddressMsg: "请输入地址",
|
||||
TgAddressMsg: "地址:",
|
||||
TgPasswordMsg: "密码:",
|
||||
TgCredentialMsg: "凭证:",
|
||||
TgNoSenderMsg: "无发件人",
|
||||
TgMsgTooLongMsg: "消息过长请到 mini app 查看",
|
||||
TgParseFailedViewInAppMsg: "解析失败,请打开 mini app 查看",
|
||||
TgMaxAddressReachedMsg: "绑定地址数量已达上限",
|
||||
TgMaxAddressReachedCleanMsg: "绑定地址数量已达上限, 请先 /cleaninvalidaddress",
|
||||
TgInvalidCredentialMsg: "无效凭证",
|
||||
TgAddressNotYoursMsg: "此地址不属于您",
|
||||
TgLangSetSuccessMsg: "语言设置成功:",
|
||||
TgCurrentLangMsg: "当前语言:",
|
||||
TgSelectLangMsg: "请选择语言:",
|
||||
TgNoPermissionViewMailMsg: "无权查看此邮件",
|
||||
TgBotTokenRequiredMsg: "需要设置 TELEGRAM_BOT_TOKEN",
|
||||
TgLangFeatureDisabledMsg: "语言设置功能已禁用,使用系统默认语言",
|
||||
}
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -7,8 +7,7 @@ export default {
|
||||
// 修改地址密码
|
||||
changePassword: async (c: Context<HonoCustomType>) => {
|
||||
const { new_password } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { address, address_id } = c.get("jwtPayload");
|
||||
|
||||
// 检查功能是否启用
|
||||
@@ -39,8 +38,7 @@ export default {
|
||||
// 地址密码登录
|
||||
login: async (c: Context<HonoCustomType>) => {
|
||||
const { email, password, cf_token } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
|
||||
// 检查功能是否启用
|
||||
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Context } from "hono";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import i18n from "../i18n";
|
||||
|
||||
|
||||
export default {
|
||||
getAutoReply: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) {
|
||||
return c.text("Auto reply is disabled", 403)
|
||||
return c.text(msgs.AutoReplyDisabledMsg, 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload")
|
||||
const results = await c.env.DB.prepare(
|
||||
@@ -23,17 +25,18 @@ export default {
|
||||
})
|
||||
},
|
||||
saveAutoReply: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) {
|
||||
return c.text("Auto reply is disabled", 403)
|
||||
return c.text(msgs.AutoReplyDisabledMsg, 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload");
|
||||
const { auto_reply } = await c.req.json();
|
||||
const { name, subject, source_prefix, message, enabled } = auto_reply;
|
||||
if ((!subject || !message) && enabled) {
|
||||
return c.text("Invalid subject or message", 400)
|
||||
return c.text(msgs.InvalidAutoReplyMsg, 400)
|
||||
}
|
||||
else if (subject.length > 255 || message.length > 255) {
|
||||
return c.text("Subject or message too long", 400)
|
||||
return c.text(msgs.SubjectOrMessageTooLongMsg, 400)
|
||||
}
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT OR REPLACE INTO auto_reply_mails`
|
||||
@@ -44,7 +47,7 @@ export default {
|
||||
subject || '', message || '', enabled ? 1 : 0
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to auto_reply settings", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({
|
||||
success: success
|
||||
|
||||
@@ -45,8 +45,7 @@ api.get('/api/mail/:mail_id', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/api/mails/:id', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||
}
|
||||
@@ -64,8 +63,7 @@ api.delete('/api/mails/:id', async (c) => {
|
||||
api.get('/api/settings', async (c) => {
|
||||
const { address, address_id } = c.get("jwtPayload")
|
||||
const user_role = c.get("userRolePayload")
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (address_id && address_id > 0) {
|
||||
try {
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
@@ -106,8 +104,7 @@ api.get('/api/settings', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/api/new_address', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL)
|
||||
&& !c.get("userPayload")
|
||||
) {
|
||||
@@ -144,11 +141,17 @@ api.post('/api/new_address', async (c) => {
|
||||
}
|
||||
try {
|
||||
const addressPrefix = await getAddressPrefix(c);
|
||||
// Get client IP for source tracking
|
||||
const sourceMeta = c.req.header('CF-Connecting-IP')
|
||||
|| c.req.header('X-Forwarded-For')?.split(',')[0]?.trim()
|
||||
|| c.req.header('X-Real-IP')
|
||||
|| 'web:unknown';
|
||||
const res = await newAddress(c, {
|
||||
name, domain,
|
||||
enablePrefix: true,
|
||||
checkLengthByConfig: true,
|
||||
addressPrefix
|
||||
addressPrefix,
|
||||
sourceMeta
|
||||
});
|
||||
return c.json(res);
|
||||
} catch (e) {
|
||||
@@ -165,8 +168,7 @@ api.delete('/api/delete_address', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/api/clear_inbox', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||
}
|
||||
@@ -175,7 +177,7 @@ api.delete('/api/clear_inbox', async (c) => {
|
||||
`DELETE FROM raw_mails WHERE address = ?`
|
||||
).bind(address).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to clear inbox", 500)
|
||||
return c.text(msgs.FailedClearInboxMsg, 500)
|
||||
}
|
||||
return c.json({
|
||||
success: success
|
||||
@@ -183,8 +185,7 @@ api.delete('/api/clear_inbox', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/api/clear_sent_items', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||
}
|
||||
@@ -193,7 +194,7 @@ api.delete('/api/clear_sent_items', async (c) => {
|
||||
`DELETE FROM sendbox WHERE address = ?`
|
||||
).bind(address).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to clear sent items", 500)
|
||||
return c.text(msgs.FailedClearSentItemsMsg, 500)
|
||||
}
|
||||
return c.json({
|
||||
success: success
|
||||
|
||||
@@ -14,9 +14,10 @@ import { handleListQuery } from '../common'
|
||||
export const api = new Hono<HonoCustomType>()
|
||||
|
||||
api.post('/api/requset_send_mail_access', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { address } = c.get("jwtPayload")
|
||||
if (!address) {
|
||||
return c.text("No address", 400)
|
||||
return c.text(msgs.AddressNotFoundMsg, 400)
|
||||
}
|
||||
try {
|
||||
const default_balance = getIntValue(c.env.DEFAULT_SEND_BALANCE, 0);
|
||||
@@ -26,14 +27,14 @@ api.post('/api/requset_send_mail_access', async (c) => {
|
||||
address, default_balance, default_balance > 0 ? 1 : 0
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to request send mail access", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
const message = (e as Error).message;
|
||||
if (message && message.includes("UNIQUE")) {
|
||||
return c.text("Already requested", 400)
|
||||
return c.text(msgs.AlreadyRequestedMsg, 400)
|
||||
}
|
||||
return c.text("Failed to request send mail access", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({ status: "ok" })
|
||||
})
|
||||
@@ -126,14 +127,15 @@ export const sendMail = async (
|
||||
isAdmin?: boolean
|
||||
}
|
||||
): Promise<void> => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!address) {
|
||||
throw new Error("No address")
|
||||
throw new Error(msgs.AddressNotFoundMsg)
|
||||
}
|
||||
// check domain
|
||||
const mailDomain = address.split("@")[1];
|
||||
const domains = getDomains(c);
|
||||
if (!domains.includes(mailDomain)) {
|
||||
throw new Error("Invalid domain")
|
||||
throw new Error(msgs.InvalidDomainMsg)
|
||||
}
|
||||
const user_role = c.get("userRolePayload");
|
||||
const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
@@ -150,7 +152,7 @@ export const sendMail = async (
|
||||
where address = ? and enabled = 1`
|
||||
).bind(address).first<number>("balance");
|
||||
if (!balance || balance <= 0) {
|
||||
throw new Error("No balance")
|
||||
throw new Error(msgs.NoBalanceMsg)
|
||||
}
|
||||
}
|
||||
const {
|
||||
@@ -158,18 +160,18 @@ export const sendMail = async (
|
||||
subject, content, is_html
|
||||
} = reqJson;
|
||||
if (!to_mail) {
|
||||
throw new Error("Invalid to mail")
|
||||
throw new Error(msgs.InvalidToMailMsg)
|
||||
}
|
||||
// check SEND_BLOCK_LIST_KEY
|
||||
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY) as string[];
|
||||
if (sendBlockList && sendBlockList.some((item) => to_mail.includes(item))) {
|
||||
throw new Error("to_mail address is blocked")
|
||||
throw new Error(msgs.AddressBlockedMsg)
|
||||
}
|
||||
if (!subject) {
|
||||
throw new Error("Subject is empty")
|
||||
throw new Error(msgs.SubjectEmptyMsg)
|
||||
}
|
||||
if (!content) {
|
||||
throw new Error("Content is empty")
|
||||
throw new Error(msgs.ContentEmptyMsg)
|
||||
}
|
||||
|
||||
// send to verified address list, do not update balance
|
||||
@@ -202,9 +204,9 @@ export const sendMail = async (
|
||||
}
|
||||
else {
|
||||
if (c.env.SEND_MAIL) {
|
||||
throw new Error(`Please enable resend or smtp for domain ${mailDomain}. Or add ${to_mail} to verified address list`);
|
||||
throw new Error(`${msgs.EnableResendOrSmtpWithVerifiedMsg} (${mailDomain})`);
|
||||
}
|
||||
throw new Error(`Please enable resend or smtp for domain ${mailDomain}`);
|
||||
throw new Error(`${msgs.EnableResendOrSmtpMsg} (${mailDomain})`);
|
||||
}
|
||||
|
||||
// update balance
|
||||
@@ -253,11 +255,12 @@ api.post('/api/send_mail', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/external/api/send_mail', async (c) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { token } = await c.req.json();
|
||||
try {
|
||||
const { address } = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
|
||||
if (!address) {
|
||||
return c.text("No address", 400)
|
||||
return c.text(msgs.AddressNotFoundMsg, 400)
|
||||
}
|
||||
const reqJson = await c.req.json();
|
||||
await sendMail(c, address as string, reqJson);
|
||||
@@ -289,8 +292,7 @@ api.get('/api/sendbox', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/api/sendbox/:id', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ import { Context } from "hono";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { AdminWebhookSettings, WebhookSettings } from "../models";
|
||||
import { commonParseMail, sendWebhook } from "../common";
|
||||
import i18n from "../i18n";
|
||||
|
||||
|
||||
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { address } = c.get("jwtPayload")
|
||||
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
||||
if (adminSettings?.enableAllowList && !adminSettings?.allowList.includes(address)) {
|
||||
return c.text("Webhook settings is not allowed for this user", 403);
|
||||
return c.text(msgs.WebhookNotAllowedForUserMsg, 403);
|
||||
}
|
||||
const settings = await c.env.KV.get<WebhookSettings>(
|
||||
`${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json"
|
||||
@@ -18,10 +20,11 @@ async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response>
|
||||
|
||||
|
||||
async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { address } = c.get("jwtPayload")
|
||||
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
||||
if (adminSettings?.enableAllowList && !adminSettings?.allowList.includes(address)) {
|
||||
return c.text("Webhook settings is not allowed for this user", 403);
|
||||
return c.text(msgs.WebhookNotAllowedForUserMsg, 403);
|
||||
}
|
||||
const settings = await c.req.json<WebhookSettings>();
|
||||
await c.env.KV.put(
|
||||
|
||||
@@ -34,6 +34,13 @@ export type WebhookMail = {
|
||||
parsedHtml: string;
|
||||
}
|
||||
|
||||
export type CustomSqlCleanup = {
|
||||
id: string; // Unique identifier
|
||||
name: string; // Cleanup task name
|
||||
sql: string; // Custom SQL statement (DELETE only)
|
||||
enabled: boolean; // Whether to enable auto cleanup
|
||||
}
|
||||
|
||||
export type CleanupSettings = {
|
||||
|
||||
enableMailsAutoCleanup: boolean | undefined;
|
||||
@@ -50,6 +57,7 @@ export type CleanupSettings = {
|
||||
cleanUnboundAddressDays: number;
|
||||
enableEmptyAddressAutoCleanup: boolean | undefined;
|
||||
cleanEmptyAddressDays: number;
|
||||
customSqlCleanupList: CustomSqlCleanup[] | undefined;
|
||||
}
|
||||
|
||||
export class GeoData {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { cleanup } from './common'
|
||||
import { CONSTANTS } from './constants'
|
||||
import { getJsonSetting } from './utils';
|
||||
import { CleanupSettings } from './models';
|
||||
import { executeCustomSqlCleanup } from './admin_api/cleanup_api';
|
||||
|
||||
export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) {
|
||||
console.log("Scheduled event: ", event);
|
||||
@@ -64,4 +65,18 @@ export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any)
|
||||
autoCleanupSetting.cleanEmptyAddressDays
|
||||
);
|
||||
}
|
||||
// Execute custom SQL cleanup tasks
|
||||
if (autoCleanupSetting.customSqlCleanupList && autoCleanupSetting.customSqlCleanupList.length > 0) {
|
||||
for (const customSql of autoCleanupSetting.customSqlCleanupList) {
|
||||
if (customSql.enabled && customSql.sql) {
|
||||
const result = await executeCustomSqlCleanup(
|
||||
{ env: env, } as Context<HonoCustomType>,
|
||||
customSql
|
||||
);
|
||||
if (!result.success) {
|
||||
console.error(`Custom SQL cleanup [${customSql.name}] failed: ${result.error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ import { Jwt } from "hono/utils/jwt";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getBooleanValue, getIntValue, getJsonSetting } from "../utils";
|
||||
import { deleteAddressWithData, newAddress, generateRandomName } from "../common";
|
||||
import { LocaleMessages } from "../i18n/type";
|
||||
|
||||
export const tgUserNewAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, address: string
|
||||
c: Context<HonoCustomType>, userId: string, address: string,
|
||||
msgs: LocaleMessages
|
||||
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
||||
if (c.env.RATE_LIMITER) {
|
||||
const { success } = await c.env.RATE_LIMITER.limit(
|
||||
@@ -23,7 +25,7 @@ export const tgUserNewAddress = async (
|
||||
const [name, domain] = trimmedAddress.includes("@") ? trimmedAddress.split("@") : [trimmedAddress, null];
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
||||
throw Error("绑定地址数量已达上限");
|
||||
throw Error(msgs.TgMaxAddressReachedMsg);
|
||||
}
|
||||
// Generate name if disabled or not provided
|
||||
const finalName = (!name || disableCustomAddressName) ? generateRandomName(c) : name;
|
||||
@@ -38,7 +40,8 @@ export const tgUserNewAddress = async (
|
||||
const res = await newAddress(c, {
|
||||
name: finalName,
|
||||
domain,
|
||||
enablePrefix: true
|
||||
enablePrefix: true,
|
||||
sourceMeta: `tg:${userId}`
|
||||
});
|
||||
// for mail push to telegram
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, res.jwt]));
|
||||
@@ -47,7 +50,8 @@ export const tgUserNewAddress = async (
|
||||
}
|
||||
|
||||
export const jwtListToAddressData = async (
|
||||
c: Context<HonoCustomType>, jwtList: string[]
|
||||
c: Context<HonoCustomType>, jwtList: string[],
|
||||
msgs: LocaleMessages
|
||||
): Promise<{
|
||||
addressList: string[], addressIdMap: Record<string, number>,
|
||||
invalidJwtList: string[]
|
||||
@@ -62,35 +66,36 @@ export const jwtListToAddressData = async (
|
||||
`SELECT name FROM address WHERE id = ? `
|
||||
).bind(address_id).first("name");
|
||||
if (!name) {
|
||||
addressList.push("无效地址");
|
||||
addressList.push(msgs.TgInvalidAddressMsg);
|
||||
invalidJwtList.push(jwt);
|
||||
continue;
|
||||
}
|
||||
addressList.push(address as string);
|
||||
addressIdMap[address as string] = address_id as number;
|
||||
} catch (e) {
|
||||
addressList.push("无效凭证");
|
||||
addressList.push(msgs.TgInvalidCredentialMsg);
|
||||
invalidJwtList.push(jwt);
|
||||
console.log(`获取地址列表失败: ${(e as Error).message}`);
|
||||
console.log(`Failed to get address list: ${(e as Error).message}`);
|
||||
}
|
||||
}
|
||||
return { addressList, addressIdMap, invalidJwtList };
|
||||
}
|
||||
|
||||
export const bindTelegramAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, jwt: string
|
||||
c: Context<HonoCustomType>, userId: string, jwt: string,
|
||||
msgs: LocaleMessages
|
||||
): Promise<string> => {
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
if (!address) {
|
||||
throw Error("无效凭证");
|
||||
throw Error(msgs.TgInvalidCredentialMsg);
|
||||
}
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressIdMap } = await jwtListToAddressData(c, jwtList);
|
||||
const { addressIdMap } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
if (address as string in addressIdMap) {
|
||||
return address as string;
|
||||
}
|
||||
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
||||
throw Error("绑定地址数量已达上限, 请先 /cleaninvalidaddress");
|
||||
throw Error(msgs.TgMaxAddressReachedCleanMsg);
|
||||
}
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, jwt]));
|
||||
// for mail push to telegram
|
||||
@@ -132,12 +137,13 @@ export const unbindTelegramByAddress = async (
|
||||
|
||||
|
||||
export const deleteTelegramAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, address: string
|
||||
c: Context<HonoCustomType>, userId: string, address: string,
|
||||
msgs: LocaleMessages
|
||||
): Promise<boolean> => {
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressIdMap } = await jwtListToAddressData(c, jwtList);
|
||||
const { addressIdMap } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
if (!(address in addressIdMap)) {
|
||||
throw Error("此地址不属于您");
|
||||
throw Error(msgs.TgAddressNotYoursMsg);
|
||||
}
|
||||
await deleteAddressWithData(c, null, addressIdMap[address])
|
||||
return true;
|
||||
|
||||
@@ -5,26 +5,29 @@ import { Writable } from 'node:stream'
|
||||
import { newTelegramBot, initTelegramBotCommands, sendMailToTelegram } from './telegram'
|
||||
import settings from './settings'
|
||||
import miniapp from './miniapp'
|
||||
import i18n from '../i18n'
|
||||
|
||||
export const api = new Hono<HonoCustomType>();
|
||||
export { sendMailToTelegram }
|
||||
|
||||
api.use("/telegram/*", async (c, next) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!c.env.TELEGRAM_BOT_TOKEN) {
|
||||
return c.text("TELEGRAM_BOT_TOKEN is required", 400);
|
||||
return c.text(msgs.TgBotTokenRequiredMsg, 400);
|
||||
}
|
||||
if (!c.env.KV) {
|
||||
return c.text("KV is required", 400);
|
||||
return c.text(msgs.KVNotAvailableMsg, 400);
|
||||
}
|
||||
return await next();
|
||||
});
|
||||
|
||||
api.use("/admin/telegram/*", async (c, next) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!c.env.TELEGRAM_BOT_TOKEN) {
|
||||
return c.text("TELEGRAM_BOT_TOKEN is required", 400);
|
||||
return c.text(msgs.TgBotTokenRequiredMsg, 400);
|
||||
}
|
||||
if (!c.env.KV) {
|
||||
return c.text("KV is required", 400);
|
||||
return c.text(msgs.KVNotAvailableMsg, 400);
|
||||
}
|
||||
return await next();
|
||||
});
|
||||
@@ -51,7 +54,7 @@ api.post("/admin/telegram/init", async (c) => {
|
||||
console.log(`setting webhook to ${webhookUrl}`);
|
||||
const bot = newTelegramBot(c, token);
|
||||
await bot.telegram.setWebhook(webhookUrl)
|
||||
await initTelegramBotCommands(bot);
|
||||
await initTelegramBotCommands(c, bot);
|
||||
return c.json({
|
||||
message: "webhook set successfully",
|
||||
});
|
||||
|
||||
@@ -84,8 +84,7 @@ async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Respo
|
||||
|
||||
async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, address, cf_token } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
// check cf turnstile
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
@@ -95,7 +94,7 @@ async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response>
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
// get the address list from the KV
|
||||
const res = await tgUserNewAddress(c, userId, address)
|
||||
const res = await tgUserNewAddress(c, userId, address, msgs)
|
||||
return c.json(res);
|
||||
}
|
||||
catch (e) {
|
||||
@@ -105,9 +104,10 @@ async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response>
|
||||
|
||||
async function bindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, jwt } = await c.req.json();
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
await bindTelegramAddress(c, userId, jwt);
|
||||
await bindTelegramAddress(c, userId, jwt, msgs);
|
||||
return c.json({ success: true });
|
||||
}
|
||||
catch (e) {
|
||||
@@ -129,10 +129,11 @@ async function unbindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
|
||||
async function getMail(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, mailId } = await c.req.json();
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList);
|
||||
const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
const result = await c.env.DB.prepare(
|
||||
`SELECT * FROM raw_mails where id = ?`
|
||||
).bind(mailId).first();
|
||||
@@ -140,14 +141,14 @@ async function getMail(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const superUser = settings?.enableGlobalMailPush && settings?.globalMailPushList.includes(userId);
|
||||
if (!superUser) {
|
||||
if (result?.address && !(result.address as string in addressIdMap)) {
|
||||
return c.text("无权查看此邮件", 403);
|
||||
return c.text(msgs.TgNoPermissionViewMailMsg, 403);
|
||||
}
|
||||
const address_id = addressIdMap[result?.address as string];
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ? `
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("无权查看此邮件", 403);
|
||||
return c.text(msgs.TgNoPermissionViewMailMsg, 403);
|
||||
}
|
||||
}
|
||||
return c.json(result);
|
||||
|
||||
@@ -4,48 +4,79 @@ import { Telegraf, Context as TgContext, Markup } from "telegraf";
|
||||
import { callbackQuery } from "telegraf/filters";
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getDomains, getJsonObjectValue, getStringValue } from '../utils';
|
||||
import { getBooleanValue, getDomains, getJsonObjectValue, getStringValue } from '../utils';
|
||||
import { TelegramSettings } from "./settings";
|
||||
import { bindTelegramAddress, deleteTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress, unbindTelegramByAddress } from "./common";
|
||||
import { commonParseMail } from "../common";
|
||||
import { UserFromGetMe } from "telegraf/types";
|
||||
import i18n from "../i18n";
|
||||
import { LocaleMessages } from "../i18n/type";
|
||||
|
||||
// Helper to get messages by userId
|
||||
const getTgMessages = async (
|
||||
c: Context<HonoCustomType>,
|
||||
ctx?: TgContext,
|
||||
userId?: string | null
|
||||
): Promise<LocaleMessages> => {
|
||||
// Check if user language config is enabled (default false)
|
||||
if (!getBooleanValue(c.env.TG_ALLOW_USER_LANG)) {
|
||||
return i18n.getMessages(c.env.DEFAULT_LANG || 'zh');
|
||||
}
|
||||
|
||||
const uid = userId || ctx?.message?.from?.id?.toString() || ctx?.callbackQuery?.from?.id?.toString();
|
||||
if (uid) {
|
||||
const savedLang = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:lang:${uid}`);
|
||||
if (savedLang) { return i18n.getMessages(savedLang); }
|
||||
}
|
||||
return i18n.getMessages(c.env.DEFAULT_LANG || 'zh');
|
||||
};
|
||||
|
||||
// Bilingual command descriptions with full usage instructions
|
||||
const COMMANDS = [
|
||||
{
|
||||
command: "start",
|
||||
description: "开始使用"
|
||||
description: "开始使用 | Get started"
|
||||
},
|
||||
{
|
||||
command: "new",
|
||||
description: "新建邮箱地址, 如果要自定义邮箱地址, 请输入 /new, 通过 /new <name>@<domain> 可以指定, name [a-z0-9] 有效, name 为空则随机生成, @<domain> 可选"
|
||||
description: "新建邮箱, /new <name>@<domain>, name[a-z0-9]有效, 为空随机生成, @domain可选 | Create address, /new <name>@<domain>, name[a-z0-9] valid, empty=random, @domain optional"
|
||||
},
|
||||
{
|
||||
command: "address",
|
||||
description: "查看邮箱地址列表"
|
||||
description: "查看邮箱地址列表 | View address list"
|
||||
},
|
||||
{
|
||||
command: "bind",
|
||||
description: "绑定邮箱地址, 请输入 /bind <邮箱地址凭证>"
|
||||
description: "绑定邮箱, /bind <邮箱地址凭证> | Bind address, /bind <credential>"
|
||||
},
|
||||
{
|
||||
command: "unbind",
|
||||
description: "解绑邮箱地址, 请输入 /unbind <邮箱地址>"
|
||||
description: "解绑邮箱, /unbind <邮箱地址> | Unbind address, /unbind <address>"
|
||||
},
|
||||
{
|
||||
command: "delete",
|
||||
description: "删除邮箱地址, 请输入 /delete <邮箱地址>"
|
||||
description: "删除邮箱, /delete <邮箱地址> | Delete address, /delete <address>"
|
||||
},
|
||||
{
|
||||
command: "mails",
|
||||
description: "查看邮件, 请输入 /mails <邮箱地址>, 不输入地址默认查看第一个地址"
|
||||
description: "查看邮件, /mails <邮箱地址>, 不输入地址默认第一个 | View mails, /mails <address>, default first if empty"
|
||||
},
|
||||
{
|
||||
command: "cleaninvalidaddress",
|
||||
description: "清理无效地址, 请输入 /cleaninvalidaddress"
|
||||
description: "清理无效地址 | Clean invalid addresses"
|
||||
},
|
||||
{
|
||||
command: "lang",
|
||||
description: "设置语言 /lang <zh|en> | Set language /lang <zh|en>"
|
||||
},
|
||||
]
|
||||
|
||||
export const getTelegramCommands = (c: Context<HonoCustomType>) => {
|
||||
return getBooleanValue(c.env.TG_ALLOW_USER_LANG)
|
||||
? COMMANDS
|
||||
: COMMANDS.filter(cmd => cmd.command !== "lang");
|
||||
}
|
||||
|
||||
export function newTelegramBot(c: Context<HonoCustomType>, token: string): Telegraf {
|
||||
const bot = new Telegraf(token);
|
||||
const botInfo = getJsonObjectValue<UserFromGetMe>(c.env.TG_BOT_INFO);
|
||||
@@ -61,14 +92,16 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
|
||||
const userId = ctx?.message?.from?.id || ctx.callbackQuery?.message?.chat?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
if (settings?.enableAllowList && settings?.enableAllowList
|
||||
if (settings?.enableAllowList
|
||||
&& !settings.allowList.includes(userId.toString())
|
||||
) {
|
||||
return await ctx.reply("您没有权限使用此机器人");
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
return await ctx.reply(msgs.TgNoPermissionMsg);
|
||||
}
|
||||
try {
|
||||
await next();
|
||||
@@ -79,153 +112,192 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
})
|
||||
|
||||
bot.command("start", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const prefix = getStringValue(c.env.PREFIX)
|
||||
const domains = getDomains(c);
|
||||
const commands = getTelegramCommands(c);
|
||||
return await ctx.reply(
|
||||
"欢迎使用本机器人, 您可以打开 mini app \n\n"
|
||||
+ (prefix ? `当前已启用前缀: ${prefix}\n` : '')
|
||||
+ `当前可用域名: ${JSON.stringify(domains)}\n`
|
||||
+ "请使用以下命令:\n"
|
||||
+ COMMANDS.map(c => `/${c.command}: ${c.description}`).join("\n")
|
||||
`${msgs.TgWelcomeMsg}\n\n`
|
||||
+ (prefix ? `${msgs.TgCurrentPrefixMsg} ${prefix}\n` : '')
|
||||
+ `${msgs.TgCurrentDomainsMsg} ${JSON.stringify(domains)}\n`
|
||||
+ `${msgs.TgAvailableCommandsMsg}\n`
|
||||
+ commands.map(cmd => `/${cmd.command}: ${cmd.description}`).join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
bot.command("new", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/new".length).trim();
|
||||
const res = await tgUserNewAddress(c, userId.toString(), address);
|
||||
return await ctx.reply(`创建地址成功:\n`
|
||||
+ `地址: ${res.address}\n`
|
||||
+ (res.password ? `密码: \`${res.password}\`\n` : '')
|
||||
+ `凭证: \`${res.jwt}\`\n`,
|
||||
const res = await tgUserNewAddress(c, userId.toString(), address, msgs);
|
||||
return await ctx.reply(`${msgs.TgCreateSuccessMsg}\n`
|
||||
+ `${msgs.TgAddressMsg} ${res.address}\n`
|
||||
+ (res.password ? `${msgs.TgPasswordMsg} \`${res.password}\`\n` : '')
|
||||
+ `${msgs.TgCredentialMsg} \`${res.jwt}\`\n`,
|
||||
{
|
||||
parse_mode: "Markdown"
|
||||
}
|
||||
);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`创建地址失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgCreateFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("bind", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const jwt = ctx?.message?.text.slice("/bind".length).trim();
|
||||
if (!jwt) {
|
||||
return await ctx.reply("请输入凭证");
|
||||
return await ctx.reply(msgs.TgPleaseInputCredentialMsg);
|
||||
}
|
||||
const address = await bindTelegramAddress(c, userId.toString(), jwt);
|
||||
return await ctx.reply(`绑定成功:\n`
|
||||
+ `地址: ${address}`
|
||||
const address = await bindTelegramAddress(c, userId.toString(), jwt, msgs);
|
||||
return await ctx.reply(`${msgs.TgBindSuccessMsg}\n`
|
||||
+ `${msgs.TgAddressMsg} ${address}`
|
||||
);
|
||||
}
|
||||
catch (e) {
|
||||
return await ctx.reply(`绑定失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgBindFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("unbind", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/unbind".length).trim();
|
||||
if (!address) {
|
||||
return await ctx.reply("请输入地址");
|
||||
return await ctx.reply(msgs.TgPleaseInputAddressMsg);
|
||||
}
|
||||
await unbindTelegramAddress(c, userId.toString(), address);
|
||||
return await ctx.reply(`解绑成功:\n地址: ${address}`
|
||||
return await ctx.reply(`${msgs.TgUnbindSuccessMsg}\n${msgs.TgAddressMsg} ${address}`
|
||||
);
|
||||
}
|
||||
catch (e) {
|
||||
return await ctx.reply(`解绑失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgUnbindFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
})
|
||||
|
||||
bot.command("delete", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/delete".length).trim();
|
||||
if (!address) {
|
||||
return await ctx.reply("请输入地址");
|
||||
return await ctx.reply(msgs.TgPleaseInputAddressMsg);
|
||||
}
|
||||
await deleteTelegramAddress(c, userId.toString(), address);
|
||||
return await ctx.reply(`删除成功: ${address}`);
|
||||
await deleteTelegramAddress(c, userId.toString(), address, msgs);
|
||||
return await ctx.reply(`${msgs.TgDeleteSuccessMsg} ${address}`);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`删除失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgDeleteFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("address", async (ctx) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressList } = await jwtListToAddressData(c, jwtList);
|
||||
return await ctx.reply(`地址列表:\n\n`
|
||||
+ addressList.map(a => `地址: ${a}`).join("\n")
|
||||
const { addressList } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
return await ctx.reply(`${msgs.TgAddressListMsg}\n\n`
|
||||
+ addressList.map(a => `${msgs.TgAddressMsg} ${a}`).join("\n")
|
||||
);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`获取地址列表失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgGetAddressFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("cleaninvalidaddress", async (ctx: TgContext) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
try {
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { invalidJwtList } = await jwtListToAddressData(c, jwtList);
|
||||
const { invalidJwtList } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
const newJwtList = jwtList.filter(jwt => !invalidJwtList.includes(jwt));
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify(newJwtList));
|
||||
const { addressList } = await jwtListToAddressData(c, newJwtList);
|
||||
return await ctx.reply(`清理无效地址成功:\n\n`
|
||||
+ `当前地址列表:\n\n`
|
||||
+ addressList.map(a => `地址: ${a}`).join("\n")
|
||||
const { addressList } = await jwtListToAddressData(c, newJwtList, msgs);
|
||||
return await ctx.reply(`${msgs.TgCleanSuccessMsg}\n\n`
|
||||
+ `${msgs.TgCurrentAddressListMsg}\n\n`
|
||||
+ addressList.map(a => `${msgs.TgAddressMsg} ${a}`).join("\n")
|
||||
);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`清理无效地址失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgCleanFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.command("lang", async (ctx: TgContext) => {
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
|
||||
// Check if user language config is enabled
|
||||
if (!getBooleanValue(c.env.TG_ALLOW_USER_LANG)) {
|
||||
return await ctx.reply(msgs.TgLangFeatureDisabledMsg);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const lang = ctx?.message?.text.slice("/lang".length).trim().toLowerCase();
|
||||
if (lang === 'zh' || lang === 'en') {
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:lang:${userId}`, lang);
|
||||
return await ctx.reply(`${msgs.TgLangSetSuccessMsg} ${lang === 'zh' ? '中文' : 'English'}`);
|
||||
}
|
||||
|
||||
const currentLang = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:lang:${userId}`);
|
||||
return await ctx.reply(
|
||||
`${msgs.TgCurrentLangMsg} ${currentLang || 'auto'}\n`
|
||||
+ `${msgs.TgSelectLangMsg}\n`
|
||||
+ `/lang zh - 中文\n`
|
||||
+ `/lang en - English`
|
||||
);
|
||||
});
|
||||
|
||||
const queryMail = async (ctx: TgContext, queryAddress: string, mailIndex: number, edit: boolean) => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
const userId = ctx?.message?.from?.id || ctx.callbackQuery?.message?.chat?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
return await ctx.reply(msgs.TgUnableGetUserInfoMsg);
|
||||
}
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList);
|
||||
const { addressList, addressIdMap } = await jwtListToAddressData(c, jwtList, msgs);
|
||||
if (!queryAddress && addressList.length > 0) {
|
||||
queryAddress = addressList[0];
|
||||
}
|
||||
if (!(queryAddress in addressIdMap)) {
|
||||
return await ctx.reply(`未绑定此地址 ${queryAddress}`);
|
||||
return await ctx.reply(`${msgs.TgNotBoundAddressMsg} ${queryAddress}`);
|
||||
}
|
||||
const address_id = addressIdMap[queryAddress];
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ? `
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return await ctx.reply("无效地址");
|
||||
return await ctx.reply(msgs.TgInvalidAddressMsg);
|
||||
}
|
||||
const { raw, id: mailId, created_at } = await c.env.DB.prepare(
|
||||
`SELECT * FROM raw_mails where address = ? `
|
||||
@@ -233,47 +305,49 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
).bind(
|
||||
queryAddress, mailIndex
|
||||
).first<{ raw: string, id: string, created_at: string }>() || {};
|
||||
const { mail } = raw ? await parseMail({ rawEmail: raw }, queryAddress, created_at) : { mail: "已经没有邮件了" };
|
||||
const { mail } = raw ? await parseMail(msgs, { rawEmail: raw }, queryAddress, created_at) : { mail: msgs.TgNoMoreMailsMsg };
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
const miniAppButtons = []
|
||||
if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) {
|
||||
const url = new URL(settings.miniAppUrl);
|
||||
url.pathname = "/telegram_mail"
|
||||
url.searchParams.set("mail_id", mailId);
|
||||
miniAppButtons.push(Markup.button.webApp("查看邮件", url.toString()));
|
||||
miniAppButtons.push(Markup.button.webApp(msgs.TgViewMailBtnMsg, url.toString()));
|
||||
}
|
||||
if (edit) {
|
||||
return await ctx.editMessageText(mail || "无邮件",
|
||||
return await ctx.editMessageText(mail || msgs.TgNoMailMsg,
|
||||
{
|
||||
...Markup.inlineKeyboard([
|
||||
Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
Markup.button.callback(msgs.TgPrevBtnMsg, `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
...miniAppButtons,
|
||||
Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
Markup.button.callback(msgs.TgNextBtnMsg, `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
])
|
||||
},
|
||||
);
|
||||
}
|
||||
return await ctx.reply(mail || "无邮件",
|
||||
return await ctx.reply(mail || msgs.TgNoMailMsg,
|
||||
{
|
||||
...Markup.inlineKeyboard([
|
||||
Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
Markup.button.callback(msgs.TgPrevBtnMsg, `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
...miniAppButtons,
|
||||
Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
Markup.button.callback(msgs.TgNextBtnMsg, `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
])
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bot.command("mails", async ctx => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
try {
|
||||
const queryAddress = ctx?.message?.text.slice("/mails".length).trim();
|
||||
return await queryMail(ctx, queryAddress, 0, false);
|
||||
} catch (e) {
|
||||
return await ctx.reply(`获取邮件失败: ${(e as Error).message}`);
|
||||
return await ctx.reply(`${msgs.TgGetMailFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
});
|
||||
|
||||
bot.on(callbackQuery("data"), async ctx => {
|
||||
const msgs = await getTgMessages(c, ctx);
|
||||
// Use ctx.callbackQuery.data
|
||||
try {
|
||||
const data = ctx.callbackQuery.data;
|
||||
@@ -283,8 +357,8 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log(`获取邮件失败: ${(e as Error).message}`, e);
|
||||
return await ctx.answerCbQuery(`获取邮件失败: ${(e as Error).message}`);
|
||||
console.log(`${msgs.TgGetMailFailedMsg} ${(e as Error).message}`, e);
|
||||
return await ctx.answerCbQuery(`${msgs.TgGetMailFailedMsg} ${(e as Error).message}`);
|
||||
}
|
||||
await ctx.answerCbQuery();
|
||||
});
|
||||
@@ -293,11 +367,12 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
}
|
||||
|
||||
|
||||
export async function initTelegramBotCommands(bot: Telegraf) {
|
||||
await bot.telegram.setMyCommands(COMMANDS);
|
||||
export async function initTelegramBotCommands(c: Context<HonoCustomType>, bot: Telegraf) {
|
||||
await bot.telegram.setMyCommands(getTelegramCommands(c));
|
||||
}
|
||||
|
||||
const parseMail = async (
|
||||
msgs: LocaleMessages,
|
||||
parsedEmailContext: ParsedEmailContext,
|
||||
address: string, created_at: string | undefined | null
|
||||
) => {
|
||||
@@ -308,20 +383,20 @@ const parseMail = async (
|
||||
const parsedEmail = await commonParseMail(parsedEmailContext);
|
||||
let parsedText = parsedEmail?.text || "";
|
||||
if (parsedText.length && parsedText.length > 1000) {
|
||||
parsedText = parsedEmail?.text.substring(0, 1000) + "\n\n...\n消息过长请到miniapp查看";
|
||||
parsedText = parsedEmail?.text.substring(0, 1000) + `\n\n...\n${msgs.TgMsgTooLongMsg}`;
|
||||
}
|
||||
return {
|
||||
isHtml: false,
|
||||
mail: `From: ${parsedEmail?.sender || "无发件人"}\n`
|
||||
mail: `From: ${parsedEmail?.sender || msgs.TgNoSenderMsg}\n`
|
||||
+ `To: ${address}\n`
|
||||
+ (created_at ? `Date: ${created_at}\n` : "")
|
||||
+ `Subject: ${parsedEmail?.subject}\n`
|
||||
+ `Content:\n${parsedText || "解析失败,请打开 mini app 查看"}`
|
||||
+ `Content:\n${parsedText || msgs.TgParseFailedViewInAppMsg}`
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
isHtml: false,
|
||||
mail: `解析邮件失败: ${(e as Error).message}`
|
||||
mail: `${msgs.TgParseMailFailedMsg} ${(e as Error).message}`
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -336,10 +411,6 @@ export async function sendMailToTelegram(
|
||||
return;
|
||||
}
|
||||
const userId = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${address}`);
|
||||
const { mail } = await parseMail(parsedEmailContext, address, new Date().toUTCString());
|
||||
if (!mail) {
|
||||
return;
|
||||
}
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
const globalPush = settings?.enableGlobalMailPush && settings?.globalMailPushList;
|
||||
if (!userId && !globalPush) {
|
||||
@@ -349,28 +420,31 @@ export async function sendMailToTelegram(
|
||||
`SELECT id FROM raw_mails where address = ? and message_id = ?`
|
||||
).bind(address, message_id).first<string>("id");
|
||||
const bot = newTelegramBot(c, c.env.TELEGRAM_BOT_TOKEN);
|
||||
const miniAppButtons = []
|
||||
if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) {
|
||||
const url = new URL(settings.miniAppUrl);
|
||||
url.pathname = "/telegram_mail"
|
||||
url.searchParams.set("mail_id", mailId);
|
||||
miniAppButtons.push(Markup.button.webApp("查看邮件", url.toString()));
|
||||
}
|
||||
|
||||
const buildAndSend = async (targetUserId: string, msgs: LocaleMessages) => {
|
||||
const { mail } = await parseMail(msgs, parsedEmailContext, address, new Date().toUTCString());
|
||||
if (!mail) return;
|
||||
const buttons = [];
|
||||
if (settings?.miniAppUrl && mailId) {
|
||||
const url = new URL(settings.miniAppUrl);
|
||||
url.pathname = "/telegram_mail"
|
||||
url.searchParams.set("mail_id", mailId);
|
||||
buttons.push(Markup.button.webApp(msgs.TgViewMailBtnMsg, url.toString()));
|
||||
}
|
||||
await bot.telegram.sendMessage(targetUserId, mail, {
|
||||
...Markup.inlineKeyboard([...buttons])
|
||||
});
|
||||
};
|
||||
|
||||
if (globalPush) {
|
||||
const globalMsgs = i18n.getMessages(c.env.DEFAULT_LANG || 'zh');
|
||||
for (const pushId of settings.globalMailPushList) {
|
||||
await bot.telegram.sendMessage(pushId, mail, {
|
||||
...Markup.inlineKeyboard([
|
||||
...miniAppButtons,
|
||||
])
|
||||
});
|
||||
await buildAndSend(pushId, globalMsgs);
|
||||
}
|
||||
}
|
||||
if (!userId) {
|
||||
return;
|
||||
|
||||
if (userId) {
|
||||
const userMsgs = await getTgMessages(c, undefined, userId);
|
||||
await buildAndSend(userId, userMsgs);
|
||||
}
|
||||
await bot.telegram.sendMessage(userId, mail, {
|
||||
...Markup.inlineKeyboard([
|
||||
...miniAppButtons,
|
||||
])
|
||||
});
|
||||
}
|
||||
|
||||
4
worker/src/types.d.ts
vendored
4
worker/src/types.d.ts
vendored
@@ -84,6 +84,7 @@ type Bindings = {
|
||||
TELEGRAM_BOT_TOKEN: string
|
||||
TG_MAX_ADDRESS: number | undefined
|
||||
TG_BOT_INFO: string | object | undefined
|
||||
TG_ALLOW_USER_LANG: string | boolean | undefined
|
||||
|
||||
// webhook config
|
||||
FRONTEND_URL: string | undefined
|
||||
@@ -144,4 +145,7 @@ type ParsedEmailContext = {
|
||||
type SubdomainForwardAddressList = {
|
||||
domains: string[] | undefined | null,
|
||||
forward: string,
|
||||
// 来源地址正则匹配 (可选,兼容原配置)
|
||||
sourcePatterns?: string[] | undefined | null, // 来源地址正则表达式列表
|
||||
sourceMatchMode?: 'any' | 'all' | undefined, // 匹配模式: any-任一匹配, all-全部匹配
|
||||
}
|
||||
|
||||
@@ -32,22 +32,23 @@ const UserBindAddressModule = {
|
||||
c: Context<HonoCustomType>,
|
||||
user_id: number | string, address_id: number | string
|
||||
) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!address_id || !user_id) {
|
||||
return c.text("No address or user token", 400)
|
||||
return c.text(msgs.NoAddressOrUserTokenMsg, 400)
|
||||
}
|
||||
// check if address exists
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ?`
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("Address not found", 400)
|
||||
return c.text(msgs.AddressNotFoundMsg, 400)
|
||||
}
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user_id).first("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400)
|
||||
return c.text(msgs.UserNotFoundMsg, 400)
|
||||
}
|
||||
// check if binded
|
||||
const db_user_address_id = await c.env.DB.prepare(
|
||||
@@ -66,7 +67,7 @@ const UserBindAddressModule = {
|
||||
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
|
||||
).bind(user_id).first<{ count: number }>() || { count: 0 };
|
||||
if (count >= maxAddressCount) {
|
||||
return c.text("Max address count reached", 400)
|
||||
return c.text(msgs.MaxAddressCountReachedMsg, 400)
|
||||
}
|
||||
}
|
||||
// bind
|
||||
@@ -75,36 +76,37 @@ const UserBindAddressModule = {
|
||||
`INSERT INTO users_address (user_id, address_id) VALUES (?, ?)`
|
||||
).bind(user_id, address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to bind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
if (error.message && error.message.includes("UNIQUE")) {
|
||||
return c.text("Address already binded, please unbind first", 400)
|
||||
return c.text(msgs.AddressAlreadyBindedMsg, 400)
|
||||
}
|
||||
return c.text("Failed to bind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
unbind: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address_id } = await c.req.json();
|
||||
if (!address_id || !user_id) {
|
||||
return c.text("Invalid address or user token", 400)
|
||||
return c.text(msgs.InvalidAddressOrUserTokenMsg, 400)
|
||||
}
|
||||
// check if address exists
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ?`
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("Address not found", 400)
|
||||
return c.text(msgs.AddressNotFoundMsg, 400)
|
||||
}
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user_id).first("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400)
|
||||
return c.text(msgs.UserNotFoundMsg, 400)
|
||||
}
|
||||
// unbind
|
||||
try {
|
||||
@@ -112,10 +114,10 @@ const UserBindAddressModule = {
|
||||
`DELETE FROM users_address where user_id = ? and address_id = ?`
|
||||
).bind(user_id, address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to unbind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
return c.text("Failed to unbind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
@@ -167,18 +169,19 @@ const UserBindAddressModule = {
|
||||
return results || [];
|
||||
},
|
||||
getBindedAddressJwt: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { address_id } = c.req.param();
|
||||
// check binded
|
||||
const { user_id } = c.get("userPayload");
|
||||
if (!address_id || !user_id) {
|
||||
return c.text("Invalid address or user token", 400)
|
||||
return c.text(msgs.InvalidAddressOrUserTokenMsg, 400)
|
||||
}
|
||||
// check users_address if address binded
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT user_id FROM users_address WHERE address_id = ? and user_id = ?`
|
||||
).bind(address_id, user_id).first("user_id");
|
||||
if (!db_user_id) {
|
||||
return c.text("Address not binded", 400)
|
||||
return c.text(msgs.AddressNotBindedMsg, 400)
|
||||
}
|
||||
// generate jwt
|
||||
const name = await c.env.DB.prepare(
|
||||
@@ -193,6 +196,7 @@ const UserBindAddressModule = {
|
||||
})
|
||||
},
|
||||
transferAddress: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address_id, target_user_email } = await c.req.json();
|
||||
// check if address exists
|
||||
@@ -200,21 +204,21 @@ const UserBindAddressModule = {
|
||||
`SELECT name FROM address where id = ?`
|
||||
).bind(address_id).first<string>("name");
|
||||
if (!address) {
|
||||
return c.text("Address not found", 400)
|
||||
return c.text(msgs.AddressNotFoundMsg, 400)
|
||||
}
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user_id).first("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400)
|
||||
return c.text(msgs.UserNotFoundMsg, 400)
|
||||
}
|
||||
// check if target user exists
|
||||
const target_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where user_email = ?`
|
||||
).bind(target_user_email).first<number>("id");
|
||||
if (!target_user_id) {
|
||||
return c.text("Target user not found", 400)
|
||||
return c.text(msgs.TargetUserNotFoundMsg, 400)
|
||||
}
|
||||
// check target user binded address count
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
@@ -228,14 +232,14 @@ const UserBindAddressModule = {
|
||||
`SELECT COUNT(*) as count FROM users_address where user_id = ?`
|
||||
).bind(target_user_id).first<{ count: number }>() || { count: 0 };
|
||||
if (count >= maxAddressCount) {
|
||||
return c.text("Target User Max address count reached", 400)
|
||||
return c.text(msgs.MaxAddressCountReachedMsg, 400)
|
||||
}
|
||||
}
|
||||
// check if binded
|
||||
const db_user_address_id = await c.env.DB.prepare(
|
||||
`SELECT user_id FROM users_address where user_id = ? and address_id = ?`
|
||||
).bind(user_id, address_id).first("user_id");
|
||||
if (!db_user_address_id) return c.text("Address not binded", 400)
|
||||
if (!db_user_address_id) return c.text(msgs.AddressNotBindedMsg, 400)
|
||||
// unbind telegram address
|
||||
await unbindTelegramByAddress(c, address);
|
||||
// unbind user address
|
||||
@@ -244,10 +248,10 @@ const UserBindAddressModule = {
|
||||
`DELETE FROM users_address where user_id = ? and address_id = ?`
|
||||
).bind(user_id, address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to unbind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
return c.text("Failed to unbind user", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
// delete address
|
||||
await c.env.DB.prepare(
|
||||
@@ -258,7 +262,7 @@ const UserBindAddressModule = {
|
||||
`INSERT INTO address(name) VALUES(?)`
|
||||
).bind(address).run();
|
||||
if (!newAddressSuccess) {
|
||||
throw new Error("Failed to create address")
|
||||
throw new Error(msgs.FailedCreateAddressMsg)
|
||||
}
|
||||
await updateAddressUpdatedAt(c, address);
|
||||
// find new address id
|
||||
@@ -266,7 +270,7 @@ const UserBindAddressModule = {
|
||||
`SELECT id FROM address WHERE name = ?`
|
||||
).bind(address).first<number | null | undefined>("id");
|
||||
if (!new_address_id) {
|
||||
throw new Error("Failed to find new address id")
|
||||
throw new Error(msgs.OperationFailedMsg)
|
||||
}
|
||||
// bind
|
||||
try {
|
||||
@@ -274,14 +278,14 @@ const UserBindAddressModule = {
|
||||
`INSERT INTO users_address (user_id, address_id) VALUES (?, ?)`
|
||||
).bind(target_user_id, new_address_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to bind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
if (error.message && error.message.includes("UNIQUE")) {
|
||||
return c.text("Address already binded, please unbind first", 400)
|
||||
return c.text(msgs.AddressAlreadyBindedMsg, 400)
|
||||
}
|
||||
return c.text("Failed to bind", 500)
|
||||
return c.text(msgs.OperationFailedMsg, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ export default {
|
||||
getOauth2LoginUrl: async (c: Context<HonoCustomType>) => {
|
||||
const settings = await getJsonSetting<UserOauth2Settings[]>(c, CONSTANTS.OAUTH2_SETTINGS_KEY);
|
||||
const { clientID, state } = c.req.query();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const setting = settings?.find(s => s.clientID === clientID);
|
||||
if (!setting) {
|
||||
return c.text(msgs.Oauth2ClientIDNotFoundMsg, 400);
|
||||
@@ -22,8 +21,7 @@ export default {
|
||||
},
|
||||
oauth2Login: async (c: Context<HonoCustomType>) => {
|
||||
const { clientID, code } = await c.req.json<{ clientID?: string, code?: string }>();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!clientID || !code) {
|
||||
return c.text(msgs.Oauth2CliendIDOrCodeMissingMsg, 400);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { Passkey } from '../models';
|
||||
import { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
|
||||
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
||||
import i18n from '../i18n';
|
||||
|
||||
export default {
|
||||
getPassKeys: async (c: Context<HonoCustomType>) => {
|
||||
@@ -20,10 +21,11 @@ export default {
|
||||
return c.json(results);
|
||||
},
|
||||
renamePassKey: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const user = c.get("userPayload");
|
||||
const { passkey_id, passkey_name } = await c.req.json();
|
||||
if (!passkey_name || passkey_name.length > 255) {
|
||||
return c.text("Invalid passkey name", 400);
|
||||
return c.text(msgs.InvalidPasskeyNameMsg, 400);
|
||||
}
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`UPDATE user_passkeys SET passkey_name = ? WHERE user_id = ? AND passkey_id = ?`
|
||||
@@ -71,6 +73,7 @@ export default {
|
||||
return c.json(options);
|
||||
},
|
||||
registerResponse: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const user = c.get("userPayload");
|
||||
const { credential, origin, passkey_name } = await c.req.json();
|
||||
// Verify the registration response
|
||||
@@ -90,7 +93,7 @@ export default {
|
||||
const { verified, registrationInfo } = verification;
|
||||
|
||||
if (!verified || !registrationInfo) {
|
||||
return c.text("Registration failed", 400);
|
||||
return c.text(msgs.RegistrationFailedMsg, 400);
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -131,10 +134,11 @@ export default {
|
||||
return c.json(options);
|
||||
},
|
||||
authenticateResponse: async (c: Context<HonoCustomType>) => {
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
const { domain, credential, origin } = await c.req.json();
|
||||
const passkey_id = credential?.id;
|
||||
if (!passkey_id) {
|
||||
return c.text("Invalid request", 400);
|
||||
return c.text(msgs.InvalidInputMsg, 400);
|
||||
}
|
||||
const { user_id, counter, passkey } = await c.env.DB.prepare(
|
||||
`SELECT user_id, counter, passkey FROM user_passkeys WHERE passkey_id = ?`
|
||||
@@ -142,7 +146,7 @@ export default {
|
||||
counter: number; passkey: string; user_id: number;
|
||||
}>() || {};
|
||||
if (!passkey) {
|
||||
return c.text("Passkey not found", 404);
|
||||
return c.text(msgs.PasskeyNotFoundMsg, 404);
|
||||
}
|
||||
const passkeyData = JSON.parse(passkey) as Passkey;
|
||||
// Verify the registration response
|
||||
@@ -166,7 +170,7 @@ export default {
|
||||
});
|
||||
const { verified, authenticationInfo } = verification;
|
||||
if (!verified) {
|
||||
return c.text("Authentication failed", 400);
|
||||
return c.text(msgs.AuthenticationFailedMsg, 400);
|
||||
}
|
||||
|
||||
if (authenticationInfo) {
|
||||
@@ -186,7 +190,7 @@ export default {
|
||||
`SELECT user_email FROM users WHERE id = ?`
|
||||
).bind(user_id).first<{ user_email: string }>() || {};
|
||||
if (!user_email) {
|
||||
return c.text("User not found", 404);
|
||||
return c.text(msgs.UserNotFoundMsg, 404);
|
||||
}
|
||||
// create jwt
|
||||
const jwt = await Jwt.sign({
|
||||
|
||||
@@ -31,8 +31,7 @@ export default {
|
||||
},
|
||||
settings: async (c: Context<HonoCustomType>) => {
|
||||
const user = c.get("userPayload");
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
|
||||
@@ -10,8 +10,7 @@ import { sendMail } from "../mails_api/send_mail_api";
|
||||
export default {
|
||||
verifyCode: async (c: Context<HonoCustomType>) => {
|
||||
const { email, cf_token } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
// check cf turnstile
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
@@ -61,8 +60,7 @@ export default {
|
||||
register: async (c: Context<HonoCustomType>) => {
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value)
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
// check enable
|
||||
if (!settings.enable) {
|
||||
return c.text(msgs.UserRegistrationDisabledMsg, 403);
|
||||
@@ -154,8 +152,7 @@ export default {
|
||||
},
|
||||
login: async (c: Context<HonoCustomType>) => {
|
||||
const { email, password } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!email || !password) return c.text(msgs.InvalidEmailOrPasswordMsg, 400);
|
||||
const { id: user_id, password: dbPassword } = await c.env.DB.prepare(
|
||||
`SELECT id, password FROM users where user_email = ?`
|
||||
|
||||
@@ -5,22 +5,20 @@ import UserBindAddressModule from "./bind_address";
|
||||
export default {
|
||||
getMails: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id } = c.get("userPayload");
|
||||
const { address, limit, offset, keyword } = c.req.query();
|
||||
const { address, limit, offset } = c.req.query();
|
||||
const bindedAddressList = await UserBindAddressModule.getBindedAddressListById(c, user_id);
|
||||
const addressList = address ? bindedAddressList.filter((item) => item == address) : bindedAddressList;
|
||||
const addressQuery = `address IN (${addressList.map(() => "?").join(",")})`;
|
||||
const addressParams = addressList;
|
||||
const keywordQuery = keyword ? `raw like ?` : "";
|
||||
const keywordParams = keyword ? [`%${keyword}%`] : [];
|
||||
|
||||
// user must have at least one binded address to query mails
|
||||
if (addressList.length <= 0) {
|
||||
return c.json({ results: [], count: 0 });
|
||||
}
|
||||
|
||||
const filterQuerys = [addressQuery, keywordQuery].filter((item) => item).join(" and ");
|
||||
const filterQuerys = [addressQuery].filter((item) => item).join(" and ");
|
||||
const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : "";
|
||||
const filterParams = [...addressParams, ...keywordParams]
|
||||
const filterParams = [...addressParams]
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails ${finalQuery}`,
|
||||
`SELECT count(*) as count FROM raw_mails ${finalQuery}`,
|
||||
|
||||
@@ -52,7 +52,7 @@ app.use('/*', async (c, next) => {
|
||||
|
||||
// check header x-custom-auth
|
||||
const passwords = getPasswords(c);
|
||||
if (!c.req.path.startsWith("/open_api") && passwords && passwords.length > 0) {
|
||||
if (!c.req.path.startsWith("/open_api") && !c.req.path.startsWith("/telegram/") && passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
if (!auth || !passwords.includes(auth)) {
|
||||
return c.text(msgs.CustomAuthPasswordMsg, 401)
|
||||
|
||||
@@ -2,6 +2,7 @@ name = "cloudflare_temp_email"
|
||||
main = "src/worker.ts"
|
||||
compatibility_date = "2025-04-01"
|
||||
compatibility_flags = [ "nodejs_compat" ]
|
||||
keep_vars = true
|
||||
# if you want use custom_domain, you need to add routes
|
||||
# routes = [
|
||||
# { pattern = "temp-email-api.xxxxx.xyz", custom_domain = true },
|
||||
@@ -86,6 +87,8 @@ ENABLE_AUTO_REPLY = false
|
||||
# TG_MAX_ADDRESS = 5
|
||||
# telegram bot info, predefined bot info can reduce latency of the webhook
|
||||
# TG_BOT_INFO = "{}"
|
||||
# allow user to switch language via /lang command
|
||||
# TG_ALLOW_USER_LANG = true
|
||||
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
||||
# subdomain forward address list, if set, subdomain emails will be forwarded to these addresses
|
||||
|
||||
Reference in New Issue
Block a user