Compare commits
58 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d485a7d0d | ||
|
|
abe812666f | ||
|
|
dbb55d948f | ||
|
|
a2a9f9e25f | ||
|
|
793901d349 | ||
|
|
113f9ad66b | ||
|
|
088bf3eefe | ||
|
|
024f9ba430 | ||
|
|
b337a44e62 | ||
|
|
eaeac8ebec | ||
|
|
7393519ba4 | ||
|
|
4ddc8e5c96 | ||
|
|
8b7ddae4f6 | ||
|
|
be36967b80 | ||
|
|
fac249ed31 | ||
|
|
bfd7d6811e | ||
|
|
b5c229b6c4 | ||
|
|
2728e9667b | ||
|
|
6109ab9e82 | ||
|
|
09a6cac8fe | ||
|
|
5f752c94f9 | ||
|
|
a2f3634c7e | ||
|
|
b62a3cbc3e | ||
|
|
8edb75587e | ||
|
|
de48661d0d | ||
|
|
a905ba5f06 | ||
|
|
6ae90be3bf | ||
|
|
5e24817de6 | ||
|
|
732189482e | ||
|
|
2bbde15f53 | ||
|
|
37cf0776b5 | ||
|
|
3fbace871c | ||
|
|
648e9f7adf | ||
|
|
ab2bfdd00f | ||
|
|
0565978930 | ||
|
|
89d8944e60 | ||
|
|
4084771621 | ||
|
|
840496c48f | ||
|
|
9843b35f54 | ||
|
|
bfd66f5019 | ||
|
|
0bc31360b0 | ||
|
|
267d9bb93e | ||
|
|
2cc84d565c | ||
|
|
c96d180591 | ||
|
|
1303b0f2a9 | ||
|
|
9f535a0a90 | ||
|
|
70109785c6 | ||
|
|
7fd10f2775 | ||
|
|
f59b8c7a1b | ||
|
|
312ac13185 | ||
|
|
e6c582be9f | ||
|
|
483c429feb | ||
|
|
da5482e095 | ||
|
|
de4646876a | ||
|
|
bbc8a96811 | ||
|
|
9ac9cd46b0 | ||
|
|
c694b07380 | ||
|
|
672c4c7273 |
11
.github/workflows/backend_deploy.yaml
vendored
@@ -33,10 +33,17 @@ jobs:
|
||||
- name: Deploy Backend for ${{ github.ref_name }}
|
||||
run: |
|
||||
export use_worker_assets=${{ secrets.USE_WORKER_ASSETS }}
|
||||
export use_worker_assets_with_telegram=${{ secrets.USE_WORKER_ASSETS_WITH_TELEGRAM }}
|
||||
if [ -n "$use_worker_assets" ]; then
|
||||
cd frontend/
|
||||
pnpm install --no-frozen-lockfile
|
||||
pnpm build:pages
|
||||
if [ -n "$use_worker_assets_with_telegram" ]; then
|
||||
echo "Building with telegram pages"
|
||||
pnpm build:telegram:pages
|
||||
else
|
||||
echo "Building with normal pages"
|
||||
pnpm build:pages
|
||||
fi
|
||||
cd ..
|
||||
fi
|
||||
|
||||
@@ -53,7 +60,7 @@ jobs:
|
||||
echo "Applied mail-parser-wasm-worker patch"
|
||||
fi
|
||||
|
||||
if [ -n "$debug_mode" ]; then
|
||||
if [ "$debug_mode" = "true" ]; then
|
||||
pnpm run deploy
|
||||
else
|
||||
output=$(pnpm run deploy 2>&1)
|
||||
|
||||
2
.github/workflows/pr_agent.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
steps:
|
||||
- name: PR Agent action step
|
||||
id: pragent
|
||||
uses: Codium-ai/pr-agent@main
|
||||
uses: docker://codiumai/pr-agent:0.29-github_action
|
||||
env:
|
||||
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false"
|
||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||
|
||||
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
dist/
|
||||
test/
|
||||
.vscode/
|
||||
|
||||
61
CHANGELOG.md
@@ -1,6 +1,67 @@
|
||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||
# CHANGE LOG
|
||||
|
||||
## v1.1.0(main)
|
||||
|
||||
- feat: |AI 提取| 增加 AI 邮件识别功能,使用 Cloudflare Workers AI 自动提取邮件中的验证码、认证链接、服务链接等重要信息
|
||||
- 支持优先级提取:验证码 > 认证链接 > 服务链接 > 订阅链接 > 其他链接
|
||||
- 管理员可配置地址白名单(支持通配符,如 `*@example.com`)
|
||||
- 前端列表和详情页展示提取结果
|
||||
- 需要配置 `ENABLE_AI_EMAIL_EXTRACT` 环境变量和 AI 绑定
|
||||
- 需要执行 `db/2025-12-06-metadata.sql` 文件中的 SQL 更新 `D1` 数据库 或者到 admin维护页面点击数据库更新按钮
|
||||
- feat: |Admin| 维护页面增加清理 n 天前空邮件的邮箱地址功能
|
||||
- fix: 修复自定义认证密码功能异常的问题 (前端属性名错误 & /open_api 接口被拦截)
|
||||
|
||||
## v1.0.7
|
||||
|
||||
- feat: |Admin| 新增 IP 黑名单功能,用于限制访问频率较高的 API
|
||||
- feat: |Admin| 新增 ASN 组织黑名单功能,支持基于 ASN 组织名称过滤请求(支持文本匹配和正则表达式)
|
||||
- feat: |Admin| 新增浏览器指纹黑名单功能,支持基于浏览器指纹过滤请求(支持精确匹配和正则表达式)
|
||||
|
||||
## v1.0.6
|
||||
|
||||
- feat: |DB| update db schema add index
|
||||
- feat: |地址密码| 增加地址密码登录功能, 通过 `ENABLE_ADDRESS_PASSWORD` 配置启用, 需要执行 `db/2025-09-23-patch.sql` 文件中的 SQL 更新 `D1` 数据库
|
||||
- fix: |GitHub Actions| 修复 debug 模式配置,仅当 DEBUG_MODE 为 'true' 时才启用调试模式
|
||||
- feat: |Admin| 账户管理页面新增多选批量操作功能(批量删除、批量清空收件箱、批量清空发件箱)
|
||||
- feat: |Admin| 维护页面增加清理未绑定用户地址的功能
|
||||
- feat: 支持针对角色配置不同的绑定地址数量上限, 可在 admin 页面配置
|
||||
|
||||
## v1.0.5
|
||||
|
||||
- feat: 新增 `DISABLE_CUSTOM_ADDRESS_NAME` 配置: 禁用自定义邮箱地址名称功能
|
||||
- feat: 新增 `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` 配置: 创建地址时优先使用第一个域名
|
||||
- feat: |UI| 主页增加进入极简模式按钮
|
||||
- feat: |Webhook| 增加白名单开关功能,支持灵活控制访问权限
|
||||
|
||||
## v1.0.4
|
||||
|
||||
- feat: |UI| 优化极简模式主页, 增加全部邮件页面功能(删除/下载/附件/...), 可在 `外观` 中切换
|
||||
- feat: admin 账号设置页面增加 `邮件转发规则` 配置
|
||||
- feat: admin 账号设置页面增加 `禁止接收未知地址邮件` 配置
|
||||
- feat: 邮件页面增加 上一封/下一封 按钮
|
||||
|
||||
## v1.0.3
|
||||
|
||||
- fix: 修复 github actions 部署问题
|
||||
- feat: telegram /new 不指定域名时, 使用随机地址
|
||||
|
||||
## v1.0.2
|
||||
|
||||
- fix: 修复 oauth2 登录失败的问题
|
||||
|
||||
## v1.0.1
|
||||
|
||||
- feat: |UI| 增加极简模式主页, 可在 `外观` 中切换
|
||||
- fix: 修复 oauth2 登录时,default role 不生效的问题
|
||||
|
||||
## v1.0.0
|
||||
|
||||
- fix: |UI| 修复 User 查看收件箱,不选择地址时,关键词查询不生效
|
||||
- fix: 修复自动清理任务,时间为 0 时不生效的问题
|
||||
- feat: 清理功能增加 创建 n 天前地址清理,n 天前未活跃地址清理
|
||||
- fix: |IMAP Proxy| 修复 IMAP Proxy 服务器,无法查看新邮件的问题
|
||||
|
||||
## v0.10.0
|
||||
|
||||
- feat: 支持 User 查看收件箱,`/user_api/mails` 接口, 支持 `address` 和 `keyword` 过滤
|
||||
|
||||
201
README.md
@@ -1,90 +1,189 @@
|
||||
# 使用 cloudflare 免费服务,搭建临时邮箱
|
||||
<!-- markdownlint-disable-file MD033 MD045 -->
|
||||
# Cloudflare 临时邮箱 - 免费搭建临时邮件服务
|
||||
|
||||
<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"/>
|
||||
<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>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://temp-mail-docs.awsl.uk" target="_blank">
|
||||
<img alt="docs" src="https://img.shields.io/badge/docs-grey?style=for-the-badge&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?style=for-the-badge">
|
||||
</a>
|
||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/LICENSE" target="_blank">
|
||||
<img alt="MIT License" src="https://img.shields.io/github/license/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
||||
</a>
|
||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/graphs/contributors" target="_blank">
|
||||
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
||||
</a>
|
||||
<a href="">
|
||||
<img alt="GitHub top language" src="https://img.shields.io/github/languages/top/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
||||
</a>
|
||||
<a href="">
|
||||
<img src="https://img.shields.io/github/last-commit/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
||||
<a 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>
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">🇨🇳 中文文档</a> |
|
||||
<a href="README_EN.md">🇺🇸 English Document</a>
|
||||
</p>
|
||||
|
||||
> 本项目仅供学习和个人用途,请勿将其用于任何违法行为,否则后果自负。
|
||||
|
||||
## [查看部署文档](https://temp-mail-docs.awsl.uk)
|
||||
**🎉 一个功能完整的临时邮箱服务!**
|
||||
|
||||
[](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html)
|
||||
- 🆓 **完全免费** - 基于 Cloudflare 免费服务构建,零成本运行
|
||||
- ⚡ **高性能** - Rust WASM 邮件解析,响应速度极快
|
||||
- 🎨 **现代化界面** - 响应式设计,支持多语言,操作简便
|
||||
- 🔐 **地址密码** - 支持为邮箱地址设置独立密码,增强安全性 (通过 `ENABLE_ADDRESS_PASSWORD` 启用)
|
||||
|
||||
[Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html)
|
||||
## 📚 部署文档 - 快速开始
|
||||
|
||||
[English Docs](https://temp-mail-docs.awsl.uk/en/)
|
||||
[📖 部署文档](https://temp-mail-docs.awsl.uk) | [🚀 Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html)
|
||||
|
||||
## [CHANGELOG](CHANGELOG.md)
|
||||
<a href="https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html">
|
||||
<img src="https://deploy.workers.cloudflare.com/button" alt="Deploy to Cloudflare Workers" height="32">
|
||||
</a>
|
||||
|
||||
## [在线演示](https://mail.awsl.uk/)
|
||||
## 📝 更新日志
|
||||
|
||||
查看 [CHANGELOG](CHANGELOG.md) 了解最新更新内容。
|
||||
|
||||
## 🎯 在线体验
|
||||
|
||||
立即体验 → [https://mail.awsl.uk/](https://mail.awsl.uk/)
|
||||
|
||||
<details>
|
||||
<summary>📊 服务状态监控(点击收缩/展开)</summary>
|
||||
|
||||
| | |
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Backend](https://temp-email-api.awsl.uk/) | [](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(点击收缩/展开)</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>
|
||||
|
||||
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
|
||||
- [查看部署文档](#查看部署文档)
|
||||
- [CHANGELOG](#changelog)
|
||||
- [在线演示](#在线演示)
|
||||
- [功能](#功能)
|
||||
- [Reference](#reference)
|
||||
- [Join Community](#join-community)
|
||||
</details>
|
||||
|
||||
## 功能
|
||||
<details open>
|
||||
<summary>📖 目录(点击收缩/展开)</summary>
|
||||
|
||||
- [Cloudflare 临时邮箱 - 免费搭建临时邮件服务](#cloudflare-临时邮箱---免费搭建临时邮件服务)
|
||||
- [📚 部署文档 - 快速开始](#-部署文档---快速开始)
|
||||
- [📝 更新日志](#-更新日志)
|
||||
- [🎯 在线体验](#-在线体验)
|
||||
- [✨ 核心功能](#-核心功能)
|
||||
- [📧 邮件处理](#-邮件处理)
|
||||
- [👥 用户管理](#-用户管理)
|
||||
- [🔧 管理功能](#-管理功能)
|
||||
- [🌐 多语言与界面](#-多语言与界面)
|
||||
- [🤖 集成与扩展](#-集成与扩展)
|
||||
- [🏗️ 技术架构](#️-技术架构)
|
||||
- [🏛️ 系统架构](#️-系统架构)
|
||||
- [🛠️ 技术栈](#️-技术栈)
|
||||
- [📦 主要组件](#-主要组件)
|
||||
- [🌟 加入社区](#-加入社区)
|
||||
|
||||
</details>
|
||||
|
||||
## ✨ 核心功能
|
||||
|
||||
<details open>
|
||||
<summary>✨ 核心功能详情(点击收缩/展开)</summary>
|
||||
|
||||
### 📧 邮件处理
|
||||
|
||||
- [x] 使用 `rust wasm` 解析邮件,解析速度快,几乎所有邮件都能解析,node 的解析模块解析邮件失败的邮件,rust wasm 也能解析成功
|
||||
- [x] **AI 邮件识别** - 使用 Cloudflare Workers AI 自动提取邮件中的验证码、认证链接、服务链接等重要信息
|
||||
- [x] 支持发送邮件,支持 `DKIM` 验证
|
||||
- [x] 支持 `SMTP` 和 `Resend` 等多种发送方式
|
||||
- [x] 增加查看 `附件` 功能,支持附件图片显示
|
||||
- [x] 支持 S3 附件存储和删除功能
|
||||
- [x] 垃圾邮件检测和黑白名单配置
|
||||
- [x] 邮件转发功能,支持全局转发地址
|
||||
|
||||
### 👥 用户管理
|
||||
|
||||
- [x] 使用 `rust wasm` 解析邮件, 解析速度快, 几乎所有邮件都能解析, node 的解析模块解析邮件失败的邮件, rust wasm 也能解析成功
|
||||
- [x] 使用 `凭证` 重新登录之前的邮箱
|
||||
- [x] 添加完整的用户注册登录功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证切换不同邮箱
|
||||
- [x] 前后台均支持多语言
|
||||
- [x] 支持 `OAuth2` 第三方登录(Github、Authentik 等)
|
||||
- [x] 支持 `Passkey` 无密码登录
|
||||
- [x] 用户角色管理,支持多角色域名和前缀配置
|
||||
- [x] 用户收件箱查看,支持地址和关键词过滤
|
||||
|
||||
### 🔧 管理功能
|
||||
|
||||
- [x] 完整的 admin 控制台
|
||||
- [x] `admin` 后台创建无前缀邮箱
|
||||
- [x] admin 用户管理页面,增加用户地址查看功能
|
||||
- [x] 定时清理功能,支持多种清理策略
|
||||
- [x] 获取自定义名字的邮箱,`admin` 可配置黑名单
|
||||
- [x] 增加访问密码,可作为私人站点
|
||||
- [x] admin 控制台
|
||||
- [x] 增加自动回复功能
|
||||
- [x] 增加查看 `附件` 功能
|
||||
- [x] 支持发送邮件
|
||||
- [x] 支持 `DKIM`
|
||||
- [x] `admin` 后台创建无前缀邮箱
|
||||
- [x] 添加 `SMTP proxy server`,支持 `SMTP` 发送邮件, `IMAP` 查看邮件
|
||||
- [x] 完整的 `Telegram Bot` 支持,以及 `Telegram` 推送, Telegram Bot 小程序
|
||||
|
||||
## Reference
|
||||
### 🌐 多语言与界面
|
||||
|
||||
- Cloudflare D1 作为数据库
|
||||
- 使用 Cloudflare Pages 部署前端
|
||||
- 使用 Cloudflare Workers 部署后端
|
||||
- email 转发使用 Cloudflare Email Routing
|
||||
- [x] 前后台均支持多语言
|
||||
- [x] 现代化 UI 设计,支持响应式布局
|
||||
- [x] 支持 Google Ads 集成
|
||||
- [x] 使用 shadow DOM 防止样式污染
|
||||
- [x] 支持 URL JWT 参数自动登录
|
||||
|
||||
## Join Community
|
||||
### 🤖 集成与扩展
|
||||
|
||||
- [x] 完整的 `Telegram Bot` 支持,以及 `Telegram` 推送,Telegram Bot 小程序
|
||||
- [x] 添加 `SMTP proxy server`,支持 `SMTP` 发送邮件,`IMAP` 查看邮件
|
||||
- [x] Webhook 支持,消息推送集成
|
||||
- [x] 支持 `CF Turnstile` 人机验证
|
||||
- [x] 限流配置,防止滥用
|
||||
|
||||
</details>
|
||||
|
||||
## 🏗️ 技术架构
|
||||
|
||||
<details>
|
||||
<summary>🏗️ 技术架构详情(点击收缩/展开)</summary>
|
||||
|
||||
### 🏛️ 系统架构
|
||||
|
||||
- **数据库**: Cloudflare D1 作为主数据库
|
||||
- **前端部署**: 使用 Cloudflare Pages 部署前端
|
||||
- **后端部署**: 使用 Cloudflare Workers 部署后端
|
||||
- **邮件转发**: 使用 Cloudflare Email Routing
|
||||
|
||||
### 🛠️ 技术栈
|
||||
|
||||
- **前端**: Vue 3 + Vite + TypeScript
|
||||
- **后端**: TypeScript + Cloudflare Workers
|
||||
- **邮件解析**: Rust WASM (mail-parser-wasm)
|
||||
- **数据库**: Cloudflare D1 (SQLite)
|
||||
- **存储**: Cloudflare KV + R2 (可选 S3)
|
||||
- **代理服务**: Python SMTP/IMAP Proxy Server
|
||||
|
||||
### 📦 主要组件
|
||||
|
||||
- **Worker**: 核心后端服务
|
||||
- **Frontend**: Vue 3 用户界面
|
||||
- **Mail Parser WASM**: Rust 邮件解析模块
|
||||
- **SMTP Proxy Server**: Python 邮件代理服务
|
||||
- **Pages Functions**: Cloudflare Pages 中间件
|
||||
- **Documentation**: VitePress 文档站点
|
||||
|
||||
</details>
|
||||
|
||||
## 🌟 加入社区
|
||||
|
||||
- [Discord](https://discord.gg/dQEwTWhA6Q)
|
||||
- [Telegram](https://t.me/cloudflare_temp_email)
|
||||
|
||||
46
README_EN.md
Normal file
@@ -0,0 +1,46 @@
|
||||
<!-- markdownlint-disable-file MD033 MD045 -->
|
||||
# Cloudflare Temp Email
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">🇨🇳 中文</a> |
|
||||
<a href="README_EN.md">🇺🇸 English</a>
|
||||
</p>
|
||||
|
||||
**A fully-featured temporary email service built on Cloudflare's free services.**
|
||||
|
||||
> This project is for learning and personal use only.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
- [📖 Documentation](https://temp-mail-docs.awsl.uk/en/)
|
||||
- [🎯 Live Demo](https://mail.awsl.uk/)
|
||||
- [📝 CHANGELOG](CHANGELOG.md)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://temp-mail-docs.awsl.uk/en/guide/actions/github-action.html">
|
||||
<img src="https://deploy.workers.cloudflare.com/button" alt="Deploy to Cloudflare Workers">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
- **📧 Email Processing**: Rust WASM parser, **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
|
||||
|
||||
## 🏗️ Tech Stack
|
||||
|
||||
- **Frontend**: Vue 3 + TypeScript + Vite
|
||||
- **Backend**: Cloudflare Workers + D1 Database
|
||||
- **Email**: Cloudflare Email Routing + Rust WASM Parser
|
||||
- **Storage**: Cloudflare KV + R2 (optional S3)
|
||||
|
||||
## 🌟 Community
|
||||
|
||||
- [Telegram](https://t.me/cloudflare_temp_email)
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
4
db/2025-09-23-patch.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE
|
||||
address
|
||||
ADD
|
||||
password TEXT;
|
||||
4
db/2025-12-06-metadata.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- Add metadata column to raw_mails table for storing AI extraction results and other metadata
|
||||
-- This column stores JSON data with flexible schema for various analysis results
|
||||
|
||||
ALTER TABLE raw_mails ADD COLUMN metadata TEXT;
|
||||
@@ -4,20 +4,28 @@ CREATE TABLE IF NOT EXISTS raw_mails (
|
||||
source TEXT,
|
||||
address TEXT,
|
||||
raw TEXT,
|
||||
metadata TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_created_at ON raw_mails(created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS address (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE,
|
||||
password TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_address_created_at ON address(created_at);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_address_updated_at ON address(updated_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
||||
id INTEGER PRIMARY KEY,
|
||||
source_prefix TEXT,
|
||||
@@ -50,6 +58,8 @@ CREATE TABLE IF NOT EXISTS sendbox (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sendbox_created_at ON sendbox(created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "0.10.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -10,6 +10,7 @@
|
||||
"build:pages": "vite build -m pages --emptyOutDir",
|
||||
"build:pages:nopwa": "VITE_PWA_DISABLED=true vite build -m pages --emptyOutDir",
|
||||
"build:telegram": "VITE_IS_TELEGRAM=true vite build -m prod --emptyOutDir",
|
||||
"build:telegram:pages": "VITE_IS_TELEGRAM=true vite build -m pages --emptyOutDir",
|
||||
"build:telegram:release": "VITE_IS_TELEGRAM=true vite build -m example --emptyOutDir",
|
||||
"preview": "vite preview",
|
||||
"deploy:telegram": "npm run build:telegram && wrangler pages deploy ./dist --branch production",
|
||||
@@ -19,35 +20,36 @@
|
||||
"deploy:actions": "npm run build && wrangler pages deploy ./dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fingerprintjs/fingerprintjs": "^5.0.1",
|
||||
"@simplewebauthn/browser": "10.0.0",
|
||||
"@unhead/vue": "^1.11.20",
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.9.0",
|
||||
"axios": "^1.13.2",
|
||||
"jszip": "^3.10.1",
|
||||
"mail-parser-wasm": "^0.2.1",
|
||||
"naive-ui": "^2.41.1",
|
||||
"postal-mime": "^2.4.3",
|
||||
"naive-ui": "^2.43.2",
|
||||
"postal-mime": "^2.6.1",
|
||||
"vooks": "^0.2.12",
|
||||
"vue": "^3.5.16",
|
||||
"vue": "^3.5.25",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-i18n": "^11.1.5",
|
||||
"vue-router": "^4.5.1"
|
||||
"vue-i18n": "^11.2.2",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"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.7.0",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vite-plugin-top-level-await": "^1.5.0",
|
||||
"vite-plugin-wasm": "^3.4.1",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-window": "^7.3.0",
|
||||
"wrangler": "^4.19.1"
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^6.4.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"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
3178
frontend/pnpm-lock.yaml
generated
@@ -23,7 +23,6 @@ const showAd = computed(() => !isMobile.value && adClient && adSlot);
|
||||
const gridMaxCols = computed(() => showAd.value ? 8 : 12);
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
try {
|
||||
await api.getUserSettings();
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { h } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
import i18n from '../i18n'
|
||||
import { getFingerprint } from '../utils/fingerprint'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||
const {
|
||||
@@ -20,6 +21,9 @@ const instance = axios.create({
|
||||
const apiFetch = async (path, options = {}) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// Get browser fingerprint for request tracking
|
||||
const fingerprint = await getFingerprint();
|
||||
|
||||
const response = await instance.request(path, {
|
||||
method: options.method || 'GET',
|
||||
data: options.body || null,
|
||||
@@ -29,6 +33,7 @@ const apiFetch = async (path, options = {}) => {
|
||||
'x-user-access-token': userSettings.value.access_token,
|
||||
'x-custom-auth': auth.value,
|
||||
'x-admin-auth': adminAuth.value,
|
||||
'x-fingerprint': fingerprint,
|
||||
'Authorization': `Bearer ${jwt.value}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -36,7 +41,7 @@ const apiFetch = async (path, options = {}) => {
|
||||
if (response.status === 401 && path.startsWith("/admin")) {
|
||||
showAdminAuth.value = true;
|
||||
}
|
||||
if (response.status === 401 && openSettings.value.auth) {
|
||||
if (response.status === 401 && openSettings.value.needAuth) {
|
||||
showAuth.value = true;
|
||||
}
|
||||
if (response.status >= 300) {
|
||||
@@ -78,6 +83,7 @@ const getOpenSettings = async (message, notification) => {
|
||||
adminContact: res["adminContact"] || "",
|
||||
enableUserCreateEmail: res["enableUserCreateEmail"] || false,
|
||||
disableAnonymousUserCreateEmail: res["disableAnonymousUserCreateEmail"] || false,
|
||||
disableCustomAddressName: res["disableCustomAddressName"] || false,
|
||||
enableUserDeleteEmail: res["enableUserDeleteEmail"] || false,
|
||||
enableAutoReply: res["enableAutoReply"] || false,
|
||||
enableIndexAbout: res["enableIndexAbout"] || false,
|
||||
@@ -85,6 +91,7 @@ const getOpenSettings = async (message, notification) => {
|
||||
cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "",
|
||||
enableWebhook: res["enableWebhook"] || false,
|
||||
isS3Enabled: res["isS3Enabled"] || false,
|
||||
enableAddressPassword: res["enableAddressPassword"] || false,
|
||||
});
|
||||
if (openSettings.value.needAuth) {
|
||||
showAuth.value = true;
|
||||
|
||||
150
frontend/src/components/AiExtractInfo.vue
Normal file
@@ -0,0 +1,150 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { ContentCopyOutlined, LinkRound, CodeRound } from '@vicons/material';
|
||||
import { useMessage } from 'naive-ui';
|
||||
|
||||
const message = useMessage();
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
authCode: 'Verification Code',
|
||||
authLink: 'Authentication Link',
|
||||
serviceLink: 'Service Link',
|
||||
subscriptionLink: 'Subscription Link',
|
||||
otherLink: 'Other Link',
|
||||
copySuccess: 'Copied successfully',
|
||||
copyFailed: 'Copy failed',
|
||||
open: 'Open',
|
||||
},
|
||||
zh: {
|
||||
authCode: '验证码',
|
||||
authLink: '认证链接',
|
||||
serviceLink: '服务链接',
|
||||
subscriptionLink: '订阅链接',
|
||||
otherLink: '其他链接',
|
||||
copySuccess: '复制成功',
|
||||
copyFailed: '复制失败',
|
||||
open: '打开',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
metadata: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
compact: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const aiExtract = computed(() => {
|
||||
if (!props.metadata) return null;
|
||||
try {
|
||||
const data = JSON.parse(props.metadata);
|
||||
return data.ai_extract || null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const typeLabel = computed(() => {
|
||||
if (!aiExtract.value) return '';
|
||||
const typeMap = {
|
||||
auth_code: t('authCode'),
|
||||
auth_link: t('authLink'),
|
||||
service_link: t('serviceLink'),
|
||||
subscription_link: t('subscriptionLink'),
|
||||
other_link: t('otherLink'),
|
||||
};
|
||||
return typeMap[aiExtract.value.type] || '';
|
||||
});
|
||||
|
||||
const typeIcon = computed(() => {
|
||||
if (!aiExtract.value) return null;
|
||||
const iconMap = {
|
||||
auth_code: CodeRound,
|
||||
auth_link: LinkRound,
|
||||
service_link: LinkRound,
|
||||
subscription_link: LinkRound,
|
||||
other_link: LinkRound,
|
||||
};
|
||||
return iconMap[aiExtract.value.type] || null;
|
||||
});
|
||||
|
||||
const isLink = computed(() => {
|
||||
return aiExtract.value && aiExtract.value.type !== 'auth_code';
|
||||
});
|
||||
|
||||
const displayText = computed(() => {
|
||||
if (!aiExtract.value) return '';
|
||||
// For auth_code, always show the raw result (verification code)
|
||||
if (aiExtract.value.type === 'auth_code') {
|
||||
return aiExtract.value.result;
|
||||
}
|
||||
// For links, prefer result_text as display label
|
||||
return aiExtract.value.result_text || aiExtract.value.result;
|
||||
});
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(aiExtract.value.result);
|
||||
message.success(t('copySuccess'));
|
||||
} catch (e) {
|
||||
message.error(t('copyFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const openLink = () => {
|
||||
if (isLink.value && aiExtract.value.result) {
|
||||
window.open(aiExtract.value.result, '_blank');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="aiExtract && aiExtract.result" class="ai-extract-info">
|
||||
<n-alert v-if="!compact" type="success" closable>
|
||||
<template #icon>
|
||||
<n-icon :component="typeIcon" />
|
||||
</template>
|
||||
<template #header>
|
||||
{{ typeLabel }}
|
||||
</template>
|
||||
<n-space align="center">
|
||||
<n-text v-if="aiExtract.type === 'auth_code'" strong style="font-size: 18px; font-family: monospace;">
|
||||
{{ aiExtract.result }}
|
||||
</n-text>
|
||||
<n-ellipsis v-else style="max-width: 400px;">
|
||||
{{ displayText }}
|
||||
</n-ellipsis>
|
||||
<n-button size="small" @click="copyToClipboard" tertiary>
|
||||
<template #icon>
|
||||
<n-icon :component="ContentCopyOutlined" />
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button v-if="isLink" size="small" @click="openLink" tertiary type="primary">
|
||||
{{ t('open') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-alert>
|
||||
<n-tag v-else type="success" @click="copyToClipboard" style="cursor: pointer;" size="small">
|
||||
<template #icon>
|
||||
<n-icon :component="typeIcon" />
|
||||
</template>
|
||||
<n-ellipsis style="max-width: 150px;">
|
||||
{{ typeLabel }}: {{ displayText }}
|
||||
</n-ellipsis>
|
||||
</n-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ai-extract-info {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,14 @@
|
||||
<script setup>
|
||||
import { watch, onMounted, ref, onBeforeUnmount } from "vue";
|
||||
import { watch, onMounted, ref, onBeforeUnmount, computed } from "vue";
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../store'
|
||||
import { CloudDownloadRound, ReplyFilled, ForwardFilled } from '@vicons/material'
|
||||
import { CloudDownloadRound, ArrowBackIosNewFilled, ArrowForwardIosFilled } from '@vicons/material'
|
||||
import { useIsMobile } from '../utils/composables'
|
||||
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
||||
import { processItem } from '../utils/email-parser'
|
||||
import { utcToLocalDate } from '../utils';
|
||||
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
|
||||
import MailContentRenderer from "./MailContentRenderer.vue";
|
||||
import AiExtractInfo from "./AiExtractInfo.vue";
|
||||
|
||||
const message = useMessage()
|
||||
const isMobile = useIsMobile()
|
||||
@@ -51,8 +52,8 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const {
|
||||
isDark, mailboxSplitSize, indexTab, loading, useUTCDate, autoRefresh, configAutoRefreshInterval,
|
||||
useIframeShowMail, sendMailModel, preferShowTextMail
|
||||
isDark, mailboxSplitSize, indexTab, loading, useUTCDate,
|
||||
autoRefresh, configAutoRefreshInterval, sendMailModel
|
||||
} = useGlobalState()
|
||||
const autoRefreshInterval = ref(configAutoRefreshInterval.value)
|
||||
const data = ref([])
|
||||
@@ -62,10 +63,49 @@ const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const showAttachments = ref(false)
|
||||
const curAttachments = ref([])
|
||||
const canGoPrevMail = computed(() => {
|
||||
if (!curMail.value) return false
|
||||
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
|
||||
return currentIndex > 0 || page.value > 1
|
||||
})
|
||||
|
||||
const canGoNextMail = computed(() => {
|
||||
if (!curMail.value) return false
|
||||
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
|
||||
return currentIndex < data.value.length - 1 || count.value > page.value * pageSize.value
|
||||
})
|
||||
|
||||
const prevMail = async () => {
|
||||
if (!canGoPrevMail.value) return
|
||||
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
|
||||
|
||||
if (currentIndex > 0) {
|
||||
curMail.value = data.value[currentIndex - 1]
|
||||
} else if (page.value > 1) {
|
||||
page.value--
|
||||
await refresh()
|
||||
if (data.value.length > 0) {
|
||||
curMail.value = data.value[data.value.length - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const nextMail = async () => {
|
||||
if (!canGoNextMail.value) return
|
||||
const currentIndex = data.value.findIndex(mail => mail.id === curMail.value.id)
|
||||
|
||||
if (currentIndex < data.value.length - 1) {
|
||||
curMail.value = data.value[currentIndex + 1]
|
||||
} else if (count.value > page.value * pageSize.value) {
|
||||
page.value++
|
||||
await refresh()
|
||||
if (data.value.length > 0) {
|
||||
curMail.value = data.value[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const curMail = ref(null);
|
||||
const showTextMail = ref(preferShowTextMail.value)
|
||||
|
||||
const multiActionMode = ref(false)
|
||||
const showMultiActionDownload = ref(false)
|
||||
@@ -94,6 +134,8 @@ const { t } = useI18n({
|
||||
cancelMultiAction: 'Cancel Multi Action',
|
||||
selectAll: 'Select All of This Page',
|
||||
unselectAll: 'Unselect All',
|
||||
prevMail: 'Previous',
|
||||
nextMail: 'Next',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
@@ -114,6 +156,8 @@ const { t } = useI18n({
|
||||
cancelMultiAction: '取消多选',
|
||||
selectAll: '全选本页',
|
||||
unselectAll: '取消全选',
|
||||
prevMail: '上一封',
|
||||
nextMail: '下一封',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -185,10 +229,6 @@ const clickRow = async (row) => {
|
||||
curMail.value = row;
|
||||
};
|
||||
|
||||
const getAttachments = (attachments) => {
|
||||
curAttachments.value = attachments;
|
||||
showAttachments.value = true;
|
||||
};
|
||||
|
||||
const mailItemClass = (row) => {
|
||||
return curMail.value && row.id == curMail.value.id ? (isDark.value ? 'overlay overlay-dark-backgroud' : 'overlay overlay-light-backgroud') : '';
|
||||
@@ -237,14 +277,8 @@ const onSpiltSizeChange = (size) => {
|
||||
mailboxSplitSize.value = size;
|
||||
}
|
||||
|
||||
const attachmentLoding = ref(false)
|
||||
const saveToS3Proxy = async (filename, blob) => {
|
||||
attachmentLoding.value = true
|
||||
try {
|
||||
await props.saveToS3(curMail.value.id, filename, blob);
|
||||
} finally {
|
||||
attachmentLoding.value = false
|
||||
}
|
||||
await props.saveToS3(curMail.value.id, filename, blob);
|
||||
}
|
||||
|
||||
const multiActionModeClick = (enableMulti) => {
|
||||
@@ -406,6 +440,7 @@ onBeforeUnmount(() => {
|
||||
TO: {{ row.address }}
|
||||
</n-ellipsis>
|
||||
</n-tag>
|
||||
<AiExtractInfo :metadata="row.metadata" compact />
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
@@ -413,59 +448,31 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</template>
|
||||
<template #2>
|
||||
<div v-if="curMail" style="margin: 8px;">
|
||||
<n-flex justify="space-between">
|
||||
<n-button @click="prevMail" :disabled="!canGoPrevMail" text size="small">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackIosNewFilled />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('prevMail') }}
|
||||
</n-button>
|
||||
<n-button @click="nextMail" :disabled="!canGoNextMail" text size="small" icon-placement="right">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowForwardIosFilled />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('nextMail') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
<n-card :bordered="false" embedded v-if="curMail" class="mail-item" :title="curMail.subject"
|
||||
style="overflow: auto; max-height: 100vh;">
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ utcToLocalDate(curMail.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ curMail.address }}
|
||||
</n-tag>
|
||||
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||
</template>
|
||||
{{ t('deleteMailTip') }}
|
||||
</n-popconfirm>
|
||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachments)">
|
||||
{{ t('attachments') }}
|
||||
</n-button>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
||||
:href="getDownloadEmlUrl(curMail.raw)">
|
||||
<template #icon>
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</template>
|
||||
{{ t('downloadMail') }}
|
||||
</n-button>
|
||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="replyMail">
|
||||
<template #icon>
|
||||
<n-icon :component="ReplyFilled" />
|
||||
</template>
|
||||
{{ t('reply') }}
|
||||
</n-button>
|
||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="forwardMail">
|
||||
<template #icon>
|
||||
<n-icon :component="ForwardFilled" />
|
||||
</template>
|
||||
{{ t('forwardMail') }}
|
||||
</n-button>
|
||||
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
||||
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<pre v-if="showTextMail" style="margin-top: 10px;">{{ curMail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
|
||||
style="margin-top: 10px;width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
<ShadowHtmlComponent v-else :htmlContent="curMail.message" style="margin-top: 10px;" />
|
||||
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
|
||||
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
|
||||
:onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail" :onSaveToS3="saveToS3Proxy" />
|
||||
</n-card>
|
||||
<n-card :bordered="false" embedded class="mail-item" v-else>
|
||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||
@@ -508,6 +515,7 @@ onBeforeUnmount(() => {
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ row.address }}
|
||||
</n-tag>
|
||||
<AiExtractInfo :metadata="row.metadata" compact />
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
@@ -517,89 +525,14 @@ onBeforeUnmount(() => {
|
||||
style="height: 80vh;">
|
||||
<n-drawer-content :title="curMail ? curMail.subject : ''" closable>
|
||||
<n-card :bordered="false" embedded style="overflow: auto;">
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ utcToLocalDate(curMail.created_at, useUTCDate) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ curMail.address }}
|
||||
</n-tag>
|
||||
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||
</template>
|
||||
{{ t('deleteMailTip') }}
|
||||
</n-popconfirm>
|
||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachments)">
|
||||
{{ t('attachments') }}
|
||||
</n-button>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
||||
:href="getDownloadEmlUrl(curMail)">
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
{{ t('downloadMail') }}
|
||||
</n-button>
|
||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="replyMail">
|
||||
<template #icon>
|
||||
<n-icon :component="ReplyFilled" />
|
||||
</template>
|
||||
{{ t('reply') }}
|
||||
</n-button>
|
||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="forwardMail">
|
||||
<template #icon>
|
||||
<n-icon :component="ForwardFilled" />
|
||||
</template>
|
||||
{{ t('forwardMail') }}
|
||||
</n-button>
|
||||
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
||||
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<pre v-if="showTextMail" style="margin-top: 10px;">{{ curMail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
|
||||
style="margin-top: 10px;width: 100%; height: 100%;">
|
||||
</iframe>
|
||||
<ShadowHtmlComponent :key="curMail.id" v-else :htmlContent="curMail.message" style="margin-top: 10px;" />
|
||||
<MailContentRenderer :mail="curMail" :showEMailTo="showEMailTo"
|
||||
:enableUserDeleteEmail="enableUserDeleteEmail" :showReply="showReply" :showSaveS3="showSaveS3"
|
||||
:useUTCDate="useUTCDate" :onDelete="deleteMail" :onReply="replyMail" :onForward="forwardMail"
|
||||
:onSaveToS3="saveToS3Proxy" />
|
||||
</n-card>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
</div>
|
||||
<n-modal v-model:show="showAttachments" preset="dialog" title="Dialog">
|
||||
<template #header>
|
||||
<div>{{ t("attachments") }}</div>
|
||||
</template>
|
||||
<n-spin v-model:show="attachmentLoding">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.filename">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
Size: {{ row.size }}
|
||||
</n-tag>
|
||||
<n-button v-if="showSaveS3" @click="saveToS3Proxy(row.filename, row.blob)" ghost type="info"
|
||||
size="small">
|
||||
{{ t('saveToS3') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-thing>
|
||||
<template #suffix>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
|
||||
:href="row.url">
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</n-button>
|
||||
</template>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-spin>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showMultiActionDownload" preset="dialog" :title="t('downloadMail')">
|
||||
<n-tag type="info">
|
||||
{{ multiActionDownloadZip.filename }}
|
||||
|
||||
286
frontend/src/components/MailContentRenderer.vue
Normal file
@@ -0,0 +1,286 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CloudDownloadRound, ReplyFilled, ForwardFilled, FullscreenRound } from '@vicons/material'
|
||||
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
|
||||
import AiExtractInfo from "./AiExtractInfo.vue";
|
||||
import { getDownloadEmlUrl } from '../utils/email-parser';
|
||||
import { utcToLocalDate } from '../utils';
|
||||
import { useGlobalState } from '../store';
|
||||
|
||||
const { preferShowTextMail, useIframeShowMail, useUTCDate } = useGlobalState();
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
delete: 'Delete',
|
||||
deleteMailTip: 'Are you sure you want to delete mail?',
|
||||
attachments: 'View Attachments',
|
||||
downloadMail: 'Download Mail',
|
||||
reply: 'Reply',
|
||||
forward: 'Forward',
|
||||
showTextMail: 'Show Text Mail',
|
||||
showHtmlMail: 'Show HTML Mail',
|
||||
saveToS3: 'Save to S3',
|
||||
size: 'Size',
|
||||
fullscreen: 'Fullscreen',
|
||||
},
|
||||
zh: {
|
||||
delete: '删除',
|
||||
deleteMailTip: '确定要删除邮件吗?',
|
||||
attachments: '查看附件',
|
||||
downloadMail: '下载邮件',
|
||||
reply: '回复',
|
||||
forward: '转发',
|
||||
showTextMail: '显示纯文本邮件',
|
||||
showHtmlMail: '显示HTML邮件',
|
||||
saveToS3: '保存到S3',
|
||||
size: '大小',
|
||||
fullscreen: '全屏',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
mail: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showEMailTo: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
enableUserDeleteEmail: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showReply: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showSaveS3: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 回调函数 props
|
||||
onDelete: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
},
|
||||
onReply: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
},
|
||||
onForward: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
},
|
||||
onSaveToS3: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
}
|
||||
});
|
||||
|
||||
const showTextMail = ref(preferShowTextMail.value);
|
||||
const showAttachments = ref(false);
|
||||
const curAttachments = ref([]);
|
||||
const attachmentLoding = ref(false);
|
||||
const showFullscreen = ref(false);
|
||||
|
||||
const handleDelete = () => {
|
||||
props.onDelete();
|
||||
};
|
||||
|
||||
const handleViewAttachments = () => {
|
||||
curAttachments.value = props.mail.attachments;
|
||||
showAttachments.value = true;
|
||||
};
|
||||
|
||||
const handleReply = () => {
|
||||
props.onReply();
|
||||
};
|
||||
|
||||
const handleForward = () => {
|
||||
props.onForward();
|
||||
};
|
||||
|
||||
|
||||
const handleSaveToS3 = async (filename, blob) => {
|
||||
attachmentLoding.value = true;
|
||||
try {
|
||||
await props.onSaveToS3(filename, blob);
|
||||
} finally {
|
||||
attachmentLoding.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mail-content-renderer">
|
||||
<!-- 邮件信息标签 -->
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
ID: {{ mail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ utcToLocalDate(mail.created_at, useUTCDate.value) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ mail.source }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ mail.address }}
|
||||
</n-tag>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="handleDelete">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||
</template>
|
||||
{{ t('deleteMailTip') }}
|
||||
</n-popconfirm>
|
||||
|
||||
<n-button v-if="mail.attachments && mail.attachments.length > 0" size="small" tertiary type="info"
|
||||
@click="handleViewAttachments">
|
||||
{{ t('attachments') }}
|
||||
</n-button>
|
||||
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="mail.id + '.eml'"
|
||||
:href="getDownloadEmlUrl(mail.raw)">
|
||||
<template #icon>
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</template>
|
||||
{{ t('downloadMail') }}
|
||||
</n-button>
|
||||
|
||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="handleReply">
|
||||
<template #icon>
|
||||
<n-icon :component="ReplyFilled" />
|
||||
</template>
|
||||
{{ t('reply') }}
|
||||
</n-button>
|
||||
|
||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="handleForward">
|
||||
<template #icon>
|
||||
<n-icon :component="ForwardFilled" />
|
||||
</template>
|
||||
{{ t('forward') }}
|
||||
</n-button>
|
||||
|
||||
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
||||
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
||||
</n-button>
|
||||
|
||||
<n-button size="small" tertiary type="info" @click="showFullscreen = true">
|
||||
<template #icon>
|
||||
<n-icon :component="FullscreenRound" />
|
||||
</template>
|
||||
{{ t('fullscreen') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
|
||||
<!-- AI 提取信息 -->
|
||||
<AiExtractInfo :metadata="mail.metadata" />
|
||||
|
||||
<!-- 邮件内容 -->
|
||||
<div class="mail-content">
|
||||
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="mail.message" class="mail-iframe">
|
||||
</iframe>
|
||||
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mail.message" class="mail-html" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-drawer v-model:show="showFullscreen" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
|
||||
style="height: 100vh;">
|
||||
<n-drawer-content :title="mail.subject" closable>
|
||||
<div class="fullscreen-mail-content">
|
||||
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="mail.message" class="mail-iframe">
|
||||
</iframe>
|
||||
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mail.message" class="mail-html" />
|
||||
</div>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
|
||||
<!-- 附件模态框 -->
|
||||
<n-modal v-model:show="showAttachments" preset="dialog" title="Dialog">
|
||||
<template #header>
|
||||
<div>{{ t('attachments') }}</div>
|
||||
</template>
|
||||
<n-spin v-model:show="attachmentLoding">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.filename">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
Size: {{ row.size }}
|
||||
</n-tag>
|
||||
<n-button v-if="showSaveS3" @click="handleSaveToS3(row.filename, row.blob)" ghost type="info"
|
||||
size="small">
|
||||
{{ t('saveToS3') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-thing>
|
||||
<template #suffix>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
|
||||
:href="row.url">
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</n-button>
|
||||
</template>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-spin>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mail-content-renderer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mail-content {
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mail-text {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.mail-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.mail-html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fullscreen-mail-content {
|
||||
height: calc(100vh - 120px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.fullscreen-mail-content .mail-iframe {
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,7 @@ export const useGlobalState = createGlobalState(
|
||||
const toggleDark = useToggle(isDark)
|
||||
const loading = ref(false);
|
||||
const announcement = useLocalStorage('announcement', '');
|
||||
const useSimpleIndex = useLocalStorage('useSimpleIndex', false);
|
||||
const openSettings = ref({
|
||||
fetched: false,
|
||||
title: '',
|
||||
@@ -21,6 +22,7 @@ export const useGlobalState = createGlobalState(
|
||||
adminContact: '',
|
||||
enableUserCreateEmail: false,
|
||||
disableAnonymousUserCreateEmail: false,
|
||||
disableCustomAddressName: false,
|
||||
enableUserDeleteEmail: false,
|
||||
enableAutoReply: false,
|
||||
enableIndexAbout: false,
|
||||
@@ -34,6 +36,7 @@ export const useGlobalState = createGlobalState(
|
||||
isS3Enabled: false,
|
||||
showGithub: true,
|
||||
disableAdminPasswordCheck: false,
|
||||
enableAddressPassword: false,
|
||||
})
|
||||
const settings = ref({
|
||||
fetched: false,
|
||||
@@ -61,6 +64,7 @@ export const useGlobalState = createGlobalState(
|
||||
const auth = useStorage('auth', '');
|
||||
const adminAuth = useStorage('adminAuth', '');
|
||||
const jwt = useStorage('jwt', '');
|
||||
const addressPassword = useSessionStorage('addressPassword', '');
|
||||
const adminTab = useSessionStorage('adminTab', "account");
|
||||
const adminMailTabAddress = ref("");
|
||||
const adminSendBoxTabAddress = ref("");
|
||||
@@ -107,6 +111,7 @@ export const useGlobalState = createGlobalState(
|
||||
const isTelegram = ref(!!window.Telegram?.WebApp?.initData);
|
||||
const userOauth2SessionState = useSessionStorage('userOauth2SessionState', '');
|
||||
const userOauth2SessionClientID = useSessionStorage('userOauth2SessionClientID', '');
|
||||
const browserFingerprint = ref('');
|
||||
return {
|
||||
isDark,
|
||||
toggleDark,
|
||||
@@ -142,6 +147,9 @@ export const useGlobalState = createGlobalState(
|
||||
showAdminPage,
|
||||
userOauth2SessionState,
|
||||
userOauth2SessionClientID,
|
||||
useSimpleIndex,
|
||||
addressPassword,
|
||||
browserFingerprint,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
30
frontend/src/utils/fingerprint.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import FingerprintJS from '@fingerprintjs/fingerprintjs';
|
||||
import { useGlobalState } from '../store';
|
||||
|
||||
const { browserFingerprint } = useGlobalState();
|
||||
|
||||
/**
|
||||
* Get browser fingerprint
|
||||
* Uses cached value from global state if available to avoid unnecessary computation
|
||||
* @returns Fingerprint visitor ID, or 'ERROR' if failed
|
||||
*/
|
||||
export const getFingerprint = async (): Promise<string> => {
|
||||
// Return cached fingerprint if available
|
||||
if (browserFingerprint.value) {
|
||||
return browserFingerprint.value;
|
||||
}
|
||||
|
||||
try {
|
||||
const fp = await FingerprintJS.load();
|
||||
const result = await fp.get();
|
||||
browserFingerprint.value = result.visitorId;
|
||||
return browserFingerprint.value;
|
||||
} catch (error) {
|
||||
console.error('Failed to get fingerprint:', error);
|
||||
// Return special error value to prevent blocking requests
|
||||
const errorValue = 'ERROR';
|
||||
browserFingerprint.value = errorValue;
|
||||
return errorValue;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,7 @@ import AccountSettings from './admin/AccountSettings.vue';
|
||||
import UserManagement from './admin/UserManagement.vue';
|
||||
import UserSettings from './admin/UserSettings.vue';
|
||||
import UserOauth2Settings from './admin/UserOauth2Settings.vue';
|
||||
import RoleAddressConfig from './admin/RoleAddressConfig.vue';
|
||||
import Mails from './admin/Mails.vue';
|
||||
import MailsUnknow from './admin/MailsUnknow.vue';
|
||||
import About from './common/About.vue';
|
||||
@@ -24,6 +25,8 @@ import Telegram from './admin/Telegram.vue';
|
||||
import Webhook from './admin/Webhook.vue';
|
||||
import MailWebhook from './admin/MailWebhook.vue';
|
||||
import WorkerConfig from './admin/WorkerConfig.vue';
|
||||
import IpBlacklistSettings from './admin/IpBlacklistSettings.vue';
|
||||
import AiExtractSettings from './admin/AiExtractSettings.vue';
|
||||
|
||||
const {
|
||||
adminAuth, showAdminAuth, adminTab, loading,
|
||||
@@ -61,6 +64,7 @@ const { t } = useI18n({
|
||||
user_management: 'User Management',
|
||||
user_settings: 'User Settings',
|
||||
userOauth2Settings: 'Oauth2 Settings',
|
||||
roleAddressConfig: 'Role Address Config',
|
||||
unknow: 'Mails with unknow receiver',
|
||||
senderAccess: 'Sender Access Control',
|
||||
sendBox: 'Send Box',
|
||||
@@ -70,6 +74,8 @@ const { t } = useI18n({
|
||||
maintenance: 'Maintenance',
|
||||
database: 'Database',
|
||||
workerconfig: 'Worker Config',
|
||||
ipBlacklistSettings: 'IP Blacklist',
|
||||
aiExtractSettings: 'AI Extract Settings',
|
||||
appearance: 'Appearance',
|
||||
about: 'About',
|
||||
ok: 'OK',
|
||||
@@ -88,6 +94,7 @@ const { t } = useI18n({
|
||||
user_management: '用户管理',
|
||||
user_settings: '用户设置',
|
||||
userOauth2Settings: 'Oauth2 设置',
|
||||
roleAddressConfig: '角色地址配置',
|
||||
unknow: '无收件人邮件',
|
||||
senderAccess: '发件权限控制',
|
||||
sendBox: '发件箱',
|
||||
@@ -97,6 +104,8 @@ const { t } = useI18n({
|
||||
maintenance: '维护',
|
||||
database: '数据库',
|
||||
workerconfig: 'Worker 配置',
|
||||
ipBlacklistSettings: 'IP 黑名单',
|
||||
aiExtractSettings: 'AI 提取设置',
|
||||
appearance: '外观',
|
||||
about: '关于',
|
||||
ok: '确定',
|
||||
@@ -157,6 +166,12 @@ onMounted(async () => {
|
||||
<n-tab-pane name="senderAccess" :tab="t('senderAccess')">
|
||||
<SenderAccess />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="ipBlacklistSettings" :tab="t('ipBlacklistSettings')">
|
||||
<IpBlacklistSettings />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="aiExtractSettings" :tab="t('aiExtractSettings')">
|
||||
<AiExtractSettings />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="webhook" :tab="t('webhookSettings')">
|
||||
<Webhook />
|
||||
</n-tab-pane>
|
||||
@@ -173,6 +188,9 @@ onMounted(async () => {
|
||||
<n-tab-pane name="userOauth2Settings" :tab="t('userOauth2Settings')">
|
||||
<UserOauth2Settings />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="roleAddressConfig" :tab="t('roleAddressConfig')">
|
||||
<RoleAddressConfig />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="mails" :tab="t('mails')">
|
||||
|
||||
@@ -5,19 +5,24 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
import { useIsMobile } from '../utils/composables'
|
||||
import { FullscreenExitOutlined } from '@vicons/material'
|
||||
|
||||
import AddressBar from './index/AddressBar.vue';
|
||||
import MailBox from '../components/MailBox.vue';
|
||||
import SendBox from '../components/SendBox.vue';
|
||||
import AutoReply from './index/AutoReply.vue';
|
||||
import AccountSettings from './index/AccountSettings.vue';
|
||||
import Appearance from './common/Appearance.vue';
|
||||
import Webhook from './index/Webhook.vue';
|
||||
import Attachment from './index/Attachment.vue';
|
||||
import About from './common/About.vue';
|
||||
import SimpleIndex from './index/SimpleIndex.vue';
|
||||
|
||||
const { loading, settings, openSettings, indexTab, globalTabplacement } = useGlobalState()
|
||||
const { loading, settings, openSettings, indexTab, globalTabplacement, useSimpleIndex } = useGlobalState()
|
||||
const message = useMessage()
|
||||
const route = useRoute()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const SendMail = defineAsyncComponent(() => {
|
||||
loading.value = true;
|
||||
@@ -33,23 +38,27 @@ const { t } = useI18n({
|
||||
sendmail: 'Send Mail',
|
||||
auto_reply: 'Auto Reply',
|
||||
accountSettings: 'Account Settings',
|
||||
appearance: 'Appearance',
|
||||
about: 'About',
|
||||
s3Attachment: 'S3 Attachment',
|
||||
saveToS3Success: 'save to s3 success',
|
||||
webhookSettings: 'Webhook Settings',
|
||||
query: 'Query',
|
||||
enterSimpleMode: 'Simple Mode',
|
||||
},
|
||||
zh: {
|
||||
mailbox: '收件箱',
|
||||
sendbox: '发件箱',
|
||||
sendmail: '发送邮件',
|
||||
auto_reply: '自动回复',
|
||||
accountSettings: '账户设置',
|
||||
accountSettings: '账户',
|
||||
appearance: '外观',
|
||||
about: '关于',
|
||||
s3Attachment: 'S3附件',
|
||||
saveToS3Success: '保存到s3成功',
|
||||
webhookSettings: 'Webhook 设置',
|
||||
query: '查询',
|
||||
enterSimpleMode: '极简模式',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -122,43 +131,61 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<AddressBar />
|
||||
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
|
||||
<n-tab-pane name="mailbox" :tab="t('mailbox')">
|
||||
<div v-if="showMailIdQuery" style="margin-bottom: 10px;">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="mailIdQuery" />
|
||||
<n-button @click="queryMail" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</div>
|
||||
<MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled"
|
||||
:saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
||||
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
:deleteMail="deleteSenboxMail" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendmail" :tab="t('sendmail')">
|
||||
<SendMail />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="accountSettings" :tab="t('accountSettings')">
|
||||
<AccountSettings />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableAutoReply" name="auto_reply" :tab="t('auto_reply')">
|
||||
<AutoReply />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableWebhook" name="webhook" :tab="t('webhookSettings')">
|
||||
<Webhook />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.isS3Enabled" name="s3_attachment" :tab="t('s3Attachment')">
|
||||
<Attachment />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableIndexAbout" name="about" :tab="t('about')">
|
||||
<About />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
<div v-if="useSimpleIndex">
|
||||
<SimpleIndex />
|
||||
</div>
|
||||
<div v-else>
|
||||
<AddressBar />
|
||||
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
|
||||
<template #prefix v-if="!isMobile">
|
||||
<n-button @click="useSimpleIndex = true" tertiary size="small">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<FullscreenExitOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('enterSimpleMode') }}
|
||||
</n-button>
|
||||
</template>
|
||||
<n-tab-pane name="mailbox" :tab="t('mailbox')">
|
||||
<div v-if="showMailIdQuery" style="margin-bottom: 10px;">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="mailIdQuery" />
|
||||
<n-button @click="queryMail" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</div>
|
||||
<MailBox :key="mailBoxKey" :showEMailTo="false" :showReply="true" :showSaveS3="openSettings.isS3Enabled"
|
||||
:saveToS3="saveToS3" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
||||
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
:deleteMail="deleteSenboxMail" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendmail" :tab="t('sendmail')">
|
||||
<SendMail />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="accountSettings" :tab="t('accountSettings')">
|
||||
<AccountSettings />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="appearance" :tab="t('appearance')">
|
||||
<Appearance :showUseSimpleIndex="true" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableAutoReply" name="auto_reply" :tab="t('auto_reply')">
|
||||
<AutoReply />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableWebhook" name="webhook" :tab="t('webhookSettings')">
|
||||
<Webhook />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.isS3Enabled" name="s3_attachment" :tab="t('s3Attachment')">
|
||||
<Attachment />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="openSettings.enableIndexAbout" name="about" :tab="t('about')">
|
||||
<About />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { NBadge } from 'naive-ui'
|
||||
import { ref, h, onMounted, watch, computed } from 'vue';
|
||||
import { NBadge, useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
@@ -9,7 +9,7 @@ import { NButton, NMenu } from 'naive-ui';
|
||||
import { MenuFilled } from '@vicons/material'
|
||||
|
||||
const {
|
||||
loading, adminTab,
|
||||
loading, adminTab, openSettings,
|
||||
adminMailTabAddress, adminSendBoxTabAddress
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
@@ -27,13 +27,31 @@ const { t } = useI18n({
|
||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||
delete: 'Delete',
|
||||
deleteTip: 'Are you sure to delete this email?',
|
||||
delteAccount: 'Delete Account',
|
||||
deleteAccount: 'Delete Account',
|
||||
viewMails: 'View Mails',
|
||||
viewSendBox: 'View SendBox',
|
||||
itemCount: 'itemCount',
|
||||
query: 'Query',
|
||||
addressQueryTip: 'Leave blank to query all addresses',
|
||||
actions: 'Actions'
|
||||
clearInbox: 'Clear Inbox',
|
||||
clearSentItems: 'Clear Sent Items',
|
||||
clearInboxTip: 'Are you sure to clear inbox for this email?',
|
||||
clearSentItemsTip: 'Are you sure to clear sent items for this email?',
|
||||
actions: 'Actions',
|
||||
success: 'Success',
|
||||
resetPassword: 'Reset Password',
|
||||
newPassword: 'New Password',
|
||||
passwordResetSuccess: 'Password reset successfully',
|
||||
selectAll: 'Select All of This Page',
|
||||
unselectAll: 'Unselect All',
|
||||
pleaseSelectAddress: 'Please select address',
|
||||
selectedItems: 'Selected',
|
||||
multiDelete: 'Multi Delete',
|
||||
multiDeleteTip: 'Are you sure to delete selected addresses?',
|
||||
multiClearInbox: 'Multi Clear Inbox',
|
||||
multiClearInboxTip: 'Are you sure to clear inbox for selected addresses?',
|
||||
multiClearSentItems: 'Multi Clear Sent Items',
|
||||
multiClearSentItemsTip: 'Are you sure to clear sent items for selected addresses?',
|
||||
},
|
||||
zh: {
|
||||
name: '名称',
|
||||
@@ -46,13 +64,31 @@ const { t } = useI18n({
|
||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||
delete: '删除',
|
||||
deleteTip: '确定要删除这个邮箱吗?',
|
||||
delteAccount: '删除邮箱',
|
||||
deleteAccount: '删除邮箱',
|
||||
viewMails: '查看邮件',
|
||||
viewSendBox: '查看发件箱',
|
||||
itemCount: '总数',
|
||||
query: '查询',
|
||||
addressQueryTip: '留空查询所有地址',
|
||||
clearInbox: '清空收件箱',
|
||||
clearSentItems: '清空发件箱',
|
||||
clearInboxTip: '确定要清空这个邮箱的收件箱吗?',
|
||||
clearSentItemsTip: '确定要清空这个邮箱的发件箱吗?',
|
||||
actions: '操作',
|
||||
success: '成功',
|
||||
resetPassword: '重置密码',
|
||||
newPassword: '新密码',
|
||||
passwordResetSuccess: '密码重置成功',
|
||||
selectAll: '全选本页',
|
||||
unselectAll: '取消全选',
|
||||
pleaseSelectAddress: '请选择地址',
|
||||
selectedItems: '已选择',
|
||||
multiDelete: '批量删除',
|
||||
multiDeleteTip: '确定要删除选中的邮箱吗?',
|
||||
multiClearInbox: '批量清空收件箱',
|
||||
multiClearInboxTip: '确定要清空选中邮箱的收件箱吗?',
|
||||
multiClearSentItems: '批量清空发件箱',
|
||||
multiClearSentItemsTip: '确定要清空选中邮箱的发件箱吗?',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -60,6 +96,20 @@ const { t } = useI18n({
|
||||
const showEmailCredential = ref(false)
|
||||
const curEmailCredential = ref("")
|
||||
const curDeleteAddressId = ref(0);
|
||||
const curClearInboxAddressId = ref(0);
|
||||
const curClearSentItemsAddressId = ref(0);
|
||||
const showResetPassword = ref(false);
|
||||
const curResetPasswordAddressId = ref(0);
|
||||
const newPassword = ref('');
|
||||
|
||||
// Multi-action mode state
|
||||
const checkedRowKeys = ref([]);
|
||||
const showMultiActionModal = ref(false);
|
||||
const multiActionProgress = ref({ percentage: 0, tip: '0/0' });
|
||||
const multiActionTitle = ref('');
|
||||
|
||||
const selectedCount = computed(() => checkedRowKeys.value.length);
|
||||
const showMultiActionBar = computed(() => checkedRowKeys.value.length > 0);
|
||||
|
||||
const addressQuery = ref("")
|
||||
|
||||
@@ -68,6 +118,8 @@ const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const showDeleteAccount = ref(false)
|
||||
const showClearInbox = ref(false)
|
||||
const showClearSentItems = ref(false)
|
||||
|
||||
const showCredential = async (id) => {
|
||||
try {
|
||||
@@ -83,7 +135,7 @@ const showCredential = async (id) => {
|
||||
const deleteEmail = async () => {
|
||||
try {
|
||||
await api.adminDeleteAddress(curDeleteAddressId.value)
|
||||
message.success("success");
|
||||
message.success(t("success"));
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
@@ -92,6 +144,142 @@ const deleteEmail = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const clearInbox = async () => {
|
||||
try {
|
||||
await api.fetch(`/admin/clear_inbox/${curClearInboxAddressId.value}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
message.success(t("success"));
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
showClearInbox.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const clearSentItems = async () => {
|
||||
try {
|
||||
await api.fetch(`/admin/clear_sent_items/${curClearSentItemsAddressId.value}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
message.success(t("success"));
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
showClearSentItems.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetPassword = async () => {
|
||||
try {
|
||||
await api.fetch(`/admin/address/${curResetPasswordAddressId.value}/reset_password`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
password: newPassword.value
|
||||
})
|
||||
});
|
||||
message.success(t("passwordResetSuccess"));
|
||||
newPassword.value = '';
|
||||
showResetPassword.value = false;
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-action mode functions
|
||||
const multiActionSelectAll = () => {
|
||||
checkedRowKeys.value = data.value.map(item => item.id);
|
||||
}
|
||||
|
||||
const multiActionUnselectAll = () => {
|
||||
checkedRowKeys.value = [];
|
||||
}
|
||||
|
||||
// 通用批量操作函数
|
||||
const executeBatchOperation = async ({
|
||||
shouldSkip = () => false,
|
||||
apiCall,
|
||||
title,
|
||||
operationName = 'operation'
|
||||
}) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const selectedAddresses = data.value.filter((item) =>
|
||||
checkedRowKeys.value.includes(item.id)
|
||||
);
|
||||
|
||||
if (selectedAddresses.length === 0) {
|
||||
message.error(t('pleaseSelectAddress'));
|
||||
return;
|
||||
}
|
||||
|
||||
const failedIds = [];
|
||||
const totalCount = selectedAddresses.length;
|
||||
|
||||
multiActionProgress.value = {
|
||||
percentage: 0,
|
||||
tip: `0/${totalCount}`
|
||||
};
|
||||
multiActionTitle.value = title;
|
||||
showMultiActionModal.value = true;
|
||||
|
||||
for (const [index, address] of selectedAddresses.entries()) {
|
||||
try {
|
||||
if (!shouldSkip(address)) {
|
||||
await apiCall(address.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${operationName} failed for address ${address.id}:`, error);
|
||||
failedIds.push(address.id);
|
||||
}
|
||||
multiActionProgress.value = {
|
||||
percentage: Math.floor((index + 1) / totalCount * 100),
|
||||
tip: `${index + 1}/${totalCount}`
|
||||
};
|
||||
}
|
||||
|
||||
await fetchData();
|
||||
checkedRowKeys.value = failedIds;
|
||||
message.success(t("success"));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const multiActionDeleteAccounts = async () => {
|
||||
await executeBatchOperation({
|
||||
apiCall: (id) => api.adminDeleteAddress(id),
|
||||
title: t('multiDelete') + ' ' + t('success'),
|
||||
operationName: 'Delete'
|
||||
});
|
||||
}
|
||||
|
||||
const multiActionClearInbox = async () => {
|
||||
await executeBatchOperation({
|
||||
shouldSkip: (address) => address.mail_count <= 0,
|
||||
apiCall: (id) => api.fetch(`/admin/clear_inbox/${id}`, {
|
||||
method: 'DELETE'
|
||||
}),
|
||||
title: t('multiClearInbox') + ' ' + t('success'),
|
||||
operationName: 'ClearInbox'
|
||||
});
|
||||
}
|
||||
|
||||
const multiActionClearSentItems = async () => {
|
||||
await executeBatchOperation({
|
||||
shouldSkip: (address) => address.send_count <= 0,
|
||||
apiCall: (id) => api.fetch(`/admin/clear_sent_items/${id}`, {
|
||||
method: 'DELETE'
|
||||
}),
|
||||
title: t('multiClearSentItems') + ' ' + t('success'),
|
||||
operationName: 'ClearSentItems'
|
||||
});
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
addressQuery.value = addressQuery.value.trim()
|
||||
@@ -106,12 +294,15 @@ const fetchData = async () => {
|
||||
count.value = addressCount;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
console.error(error);
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
type: 'selection'
|
||||
},
|
||||
{
|
||||
title: "ID",
|
||||
key: "id"
|
||||
@@ -228,6 +419,45 @@ const columns = [
|
||||
),
|
||||
show: row.send_count > 0
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => {
|
||||
curClearInboxAddressId.value = row.id;
|
||||
showClearInbox.value = true;
|
||||
}
|
||||
},
|
||||
{ default: () => t('clearInbox') }
|
||||
),
|
||||
show: row.mail_count > 0
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => {
|
||||
curClearSentItemsAddressId.value = row.id;
|
||||
showClearSentItems.value = true;
|
||||
}
|
||||
},
|
||||
{ default: () => t('clearSentItems') }
|
||||
),
|
||||
show: row.send_count > 0
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => {
|
||||
curResetPasswordAddressId.value = row.id;
|
||||
showResetPassword.value = true;
|
||||
}
|
||||
},
|
||||
{ default: () => t('resetPassword') }
|
||||
),
|
||||
show: openSettings.value?.enableAddressPassword
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
@@ -273,21 +503,78 @@ onMounted(async () => {
|
||||
<template #action>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showDeleteAccount" preset="dialog" :title="t('delteAccount')">
|
||||
<n-modal v-model:show="showDeleteAccount" preset="dialog" :title="t('deleteAccount')">
|
||||
<p>{{ t('deleteTip') }}</p>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="deleteEmail" size="small" tertiary type="error">
|
||||
{{ t('delteAccount') }}
|
||||
{{ t('deleteAccount') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-modal v-model:show="showClearInbox" preset="dialog" :title="t('clearInbox')">
|
||||
<p>{{ t('clearInboxTip') }}</p>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="clearInbox" size="small" tertiary type="error">
|
||||
{{ t('clearInbox') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showClearSentItems" preset="dialog" :title="t('clearSentItems')">
|
||||
<p>{{ t('clearSentItemsTip') }}</p>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="clearSentItems" size="small" tertiary type="error">
|
||||
{{ t('clearSentItems') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showResetPassword" preset="dialog" :title="t('resetPassword')">
|
||||
<n-form-item :label="t('newPassword')">
|
||||
<n-input v-model:value="newPassword" type="password" placeholder="" show-password-on="click" />
|
||||
</n-form-item>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="resetPassword" size="small" tertiary type="info">
|
||||
{{ t('resetPassword') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-input-group style="margin-bottom: 10px;">
|
||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')"
|
||||
@keydown.enter="fetchData" />
|
||||
<n-button @click="fetchData" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
|
||||
<n-space v-if="showMultiActionBar" style="margin-bottom: 10px;">
|
||||
<n-button @click="multiActionSelectAll" tertiary>
|
||||
{{ t('selectAll') }}
|
||||
</n-button>
|
||||
<n-button @click="multiActionUnselectAll" tertiary>
|
||||
{{ t('unselectAll') }}
|
||||
</n-button>
|
||||
<n-popconfirm @positive-click="multiActionDeleteAccounts">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error">{{ t('multiDelete') }}</n-button>
|
||||
</template>
|
||||
{{ t('multiDeleteTip') }}
|
||||
</n-popconfirm>
|
||||
<n-popconfirm @positive-click="multiActionClearInbox">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="warning">{{ t('multiClearInbox') }}</n-button>
|
||||
</template>
|
||||
{{ t('multiClearInboxTip') }}
|
||||
</n-popconfirm>
|
||||
<n-popconfirm @positive-click="multiActionClearSentItems">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="warning">{{ t('multiClearSentItems') }}</n-button>
|
||||
</template>
|
||||
{{ t('multiClearSentItemsTip') }}
|
||||
</n-popconfirm>
|
||||
<n-tag type="info">
|
||||
{{ t('selectedItems') }}: {{ selectedCount }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
<div style="overflow: auto;">
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||
@@ -297,8 +584,21 @@ onMounted(async () => {
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
<n-data-table v-model:checked-row-keys="checkedRowKeys" :columns="columns" :data="data" :bordered="false"
|
||||
:row-key="row => row.id" embedded />
|
||||
</div>
|
||||
|
||||
<!-- Multi-action progress modal -->
|
||||
<n-modal v-model:show="showMultiActionModal" preset="dialog" :title="multiActionTitle" negative-text="OK">
|
||||
<n-space justify="center">
|
||||
<n-progress type="circle" status="info" :percentage="multiActionProgress.percentage">
|
||||
<span style="text-align: center">
|
||||
{{ multiActionProgress.tip }}
|
||||
</span>
|
||||
</n-progress>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, ref, h } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { NButton, NPopconfirm, NInput, NSelect } from 'naive-ui'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { loading } = useGlobalState()
|
||||
const { loading, openSettings } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
tip: 'You can manually input the following multiple select input and enter',
|
||||
manualInputPrompt: 'Type and press Enter to add',
|
||||
save: 'Save',
|
||||
successTip: 'Save Success',
|
||||
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
|
||||
@@ -20,9 +22,24 @@ const { t } = useI18n({
|
||||
noLimitSendAddressList: 'No Balance Limit Send Address List',
|
||||
verified_address_list: 'Verified Address List(Can send email by cf internal api)',
|
||||
fromBlockList: 'Block Keywords for receive email',
|
||||
block_receive_unknow_address_email: 'Block receive unknow address email',
|
||||
email_forwarding_config: 'Email Forwarding Configuration',
|
||||
domain_list: 'Domain List',
|
||||
forward_address: 'Forward Address',
|
||||
actions: 'Actions',
|
||||
select_domain: 'Select Domain',
|
||||
forward_placeholder: 'forward@example.com',
|
||||
delete_rule: 'Delete',
|
||||
delete_rule_confirm: 'Are you sure you want to delete this rule?',
|
||||
delete_success: 'Delete Success',
|
||||
forwarding_rule_warning: 'Each rule will run, if domains is empty, all emails will be forwarded, forward address needs to be a verified address',
|
||||
add: 'Add',
|
||||
cancel: 'Cancel',
|
||||
config: 'Config',
|
||||
},
|
||||
zh: {
|
||||
tip: '您可以手动输入以下多选输入框, 回车增加',
|
||||
manualInputPrompt: '输入后按回车键添加',
|
||||
save: '保存',
|
||||
successTip: '保存成功',
|
||||
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
|
||||
@@ -31,6 +48,20 @@ const { t } = useI18n({
|
||||
noLimitSendAddressList: '无余额限制发送地址列表',
|
||||
verified_address_list: '已验证地址列表(可通过 cf 内部 api 发送邮件)',
|
||||
fromBlockList: '接收邮件地址屏蔽关键词',
|
||||
block_receive_unknow_address_email: '禁止接收未知地址邮件',
|
||||
email_forwarding_config: '邮件转发配置',
|
||||
domain_list: '域名列表',
|
||||
forward_address: '转发地址',
|
||||
actions: '操作',
|
||||
select_domain: '选择域名',
|
||||
forward_placeholder: 'forward@example.com',
|
||||
delete_rule: '删除',
|
||||
delete_rule_confirm: '确定要删除这条规则吗?',
|
||||
delete_success: '删除成功',
|
||||
forwarding_rule_warning: '每条规则都会运行,如果 domains 为空,则转发所有邮件,转发地址需要为已验证的地址',
|
||||
add: '添加',
|
||||
cancel: '取消',
|
||||
config: '配置',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -40,6 +71,90 @@ const sendAddressBlockList = ref([])
|
||||
const noLimitSendAddressList = ref([])
|
||||
const verifiedAddressList = ref([])
|
||||
const fromBlockList = ref([])
|
||||
const emailRuleSettings = ref({
|
||||
blockReceiveUnknowAddressEmail: false,
|
||||
emailForwardingList: []
|
||||
})
|
||||
|
||||
const showEmailForwardingModal = ref(false)
|
||||
const emailForwardingList = ref([])
|
||||
|
||||
|
||||
const emailForwardingColumns = [
|
||||
{
|
||||
title: t('domain_list'),
|
||||
key: 'domains',
|
||||
render: (row, index) => {
|
||||
return h(NSelect, {
|
||||
value: Array.isArray(row.domains) ? row.domains : [],
|
||||
onUpdateValue: (val) => {
|
||||
emailForwardingList.value[index].domains = val
|
||||
},
|
||||
options: openSettings.value?.domains || [],
|
||||
multiple: true,
|
||||
filterable: true,
|
||||
tag: true,
|
||||
placeholder: t('select_domain')
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('forward_address'),
|
||||
key: 'forward',
|
||||
render: (row, index) => {
|
||||
return h(NInput, {
|
||||
value: row.forward,
|
||||
onUpdateValue: (val) => {
|
||||
emailForwardingList.value[index].forward = val
|
||||
},
|
||||
placeholder: 'forward@example.com'
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('actions'),
|
||||
key: 'actions',
|
||||
render: (row, index) => {
|
||||
return h('div', { style: 'display: flex; gap: 8px;' }, [
|
||||
h(NPopconfirm, {
|
||||
onPositiveClick: () => {
|
||||
emailForwardingList.value = emailForwardingList.value.filter((_, i) => i !== index)
|
||||
message.success(t('delete_success'))
|
||||
}
|
||||
}, {
|
||||
default: () => t('delete_rule_confirm'),
|
||||
trigger: () => h(NButton, {
|
||||
size: 'small',
|
||||
type: 'error'
|
||||
}, { default: () => t('delete_rule') })
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const openEmailForwardingModal = () => {
|
||||
// 从 emailRuleSettings 转换出列表数据
|
||||
emailForwardingList.value = emailRuleSettings.value.emailForwardingList ?
|
||||
[...emailRuleSettings.value.emailForwardingList] : []
|
||||
showEmailForwardingModal.value = true
|
||||
}
|
||||
|
||||
const addNewEmailForwardingItem = () => {
|
||||
emailForwardingList.value = [
|
||||
...emailForwardingList.value,
|
||||
{
|
||||
domains: [],
|
||||
forward: ''
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const saveEmailForwardingConfig = () => {
|
||||
emailRuleSettings.value.emailForwardingList = [...emailForwardingList.value]
|
||||
showEmailForwardingModal.value = false
|
||||
}
|
||||
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
@@ -49,6 +164,10 @@ const fetchData = async () => {
|
||||
verifiedAddressList.value = res.verifiedAddressList || []
|
||||
fromBlockList.value = res.fromBlockList || []
|
||||
noLimitSendAddressList.value = res.noLimitSendAddressList || []
|
||||
emailRuleSettings.value = res.emailRuleSettings || {
|
||||
blockReceiveUnknowAddressEmail: false,
|
||||
emailForwardingList: []
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
@@ -64,6 +183,7 @@ const save = async () => {
|
||||
verifiedAddressList: verifiedAddressList.value || [],
|
||||
fromBlockList: fromBlockList.value || [],
|
||||
noLimitSendAddressList: noLimitSendAddressList.value || [],
|
||||
emailRuleSettings: emailRuleSettings.value,
|
||||
})
|
||||
})
|
||||
message.success(t('successTip'))
|
||||
@@ -81,33 +201,88 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<n-alert :show-icon="false" type="warning" style="margin-bottom: 10px;">
|
||||
{{ t("tip") }}
|
||||
<n-alert :show-icon="false" :bordered="false" type="warning" style="margin-bottom: 10px;">
|
||||
<span>{{ t("tip") }}</span>
|
||||
</n-alert>
|
||||
<n-flex justify="end">
|
||||
<n-button @click="save" type="primary" :loading="loading">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-form-item-row :label="t('address_block_list')">
|
||||
<n-select v-model:value="addressBlockList" filterable multiple tag
|
||||
:placeholder="t('address_block_list_placeholder')" />
|
||||
:placeholder="t('address_block_list_placeholder')">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('send_address_block_list')">
|
||||
<n-select v-model:value="sendAddressBlockList" filterable multiple tag
|
||||
:placeholder="t('address_block_list_placeholder')" />
|
||||
:placeholder="t('address_block_list_placeholder')">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('noLimitSendAddressList')">
|
||||
<n-select v-model:value="noLimitSendAddressList" filterable multiple tag
|
||||
:placeholder="t('noLimitSendAddressList')" />
|
||||
:placeholder="t('noLimitSendAddressList')">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('verified_address_list')">
|
||||
<n-select v-model:value="verifiedAddressList" filterable multiple tag
|
||||
:placeholder="t('verified_address_list')" />
|
||||
:placeholder="t('verified_address_list')">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('fromBlockList')">
|
||||
<n-select v-model:value="fromBlockList" filterable multiple tag :placeholder="t('fromBlockList')" />
|
||||
<n-select v-model:value="fromBlockList" filterable multiple tag :placeholder="t('fromBlockList')">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('block_receive_unknow_address_email')">
|
||||
<n-switch v-model:value="emailRuleSettings.blockReceiveUnknowAddressEmail" :round="false" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('email_forwarding_config')">
|
||||
<n-button @click="openEmailForwardingModal">{{ t('config') }}</n-button>
|
||||
</n-form-item-row>
|
||||
<n-button @click="save" type="primary" block :loading="loading">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<!-- 邮件转发配置弹窗 -->
|
||||
<n-modal v-model:show="showEmailForwardingModal" preset="card" :title="t('email_forwarding_config')"
|
||||
style="max-width: 800px;">
|
||||
<n-space vertical>
|
||||
<n-alert :show-icon="false" :bordered="false" type="warning">
|
||||
<span>{{ t('forwarding_rule_warning') }}</span>
|
||||
</n-alert>
|
||||
<n-space justify="end">
|
||||
<n-button @click="addNewEmailForwardingItem">{{ t('add') }}</n-button>
|
||||
</n-space>
|
||||
<n-data-table :columns="emailForwardingColumns" :data="emailForwardingList" :bordered="false" striped />
|
||||
<n-space justify="end">
|
||||
<n-button @click="saveEmailForwardingConfig" type="primary">{{ t('save') }}</n-button>
|
||||
</n-space>
|
||||
</n-space>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
125
frontend/src/views/admin/AiExtractSettings.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMessage } from 'naive-ui'
|
||||
// @ts-ignore
|
||||
import { api } from '../../api'
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
title: 'AI Email Extraction Settings',
|
||||
successTip: 'Success',
|
||||
save: 'Save',
|
||||
enableAllowList: 'Enable Address Allowlist',
|
||||
enableAllowListTip: 'When enabled, AI extraction will only process emails sent to addresses in the allowlist',
|
||||
allowList: 'Address Allowlist (Enter address and press Enter, wildcards supported)',
|
||||
allowListTip: "Wildcard * matches any characters, e.g. *{'@'}example.com matches all addresses under example.com domain",
|
||||
manualInputPrompt: 'Type and press Enter to add',
|
||||
disabledTip: 'When disabled, AI extraction will process all email addresses',
|
||||
},
|
||||
zh: {
|
||||
title: 'AI 邮件提取设置',
|
||||
successTip: '成功',
|
||||
save: '保存',
|
||||
enableAllowList: '启用地址白名单',
|
||||
enableAllowListTip: '启用后,AI 提取功能仅对白名单中的邮箱地址生效',
|
||||
allowList: '地址白名单 (请输入地址并回车,支持通配符)',
|
||||
allowListTip: "通配符 * 可匹配任意字符,如 *{'@'}example.com 可匹配 example.com 域名下的所有地址",
|
||||
manualInputPrompt: '输入后按回车键添加',
|
||||
disabledTip: '未启用时,所有邮箱地址都可使用 AI 提取功能',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
type AiExtractSettings = {
|
||||
enableAllowList: boolean
|
||||
allowList: string[]
|
||||
}
|
||||
|
||||
const settings = ref<AiExtractSettings>({
|
||||
enableAllowList: false,
|
||||
allowList: []
|
||||
})
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await api.fetch(`/admin/ai_extract/settings`) as AiExtractSettings
|
||||
Object.assign(settings.value, res)
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const saveSettings = async () => {
|
||||
try {
|
||||
await api.fetch(`/admin/ai_extract/settings`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(settings.value),
|
||||
})
|
||||
message.success(t('successTip'))
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :title="t('title')" :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||
<n-flex justify="end">
|
||||
<n-button @click="saveSettings" type="primary">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
|
||||
<n-form-item-row :label="t('enableAllowList')">
|
||||
<n-switch v-model:value="settings.enableAllowList" :round="false" />
|
||||
</n-form-item-row>
|
||||
|
||||
<n-alert v-if="!settings.enableAllowList" type="info" style="margin-bottom: 16px;">
|
||||
{{ t('disabledTip') }}
|
||||
</n-alert>
|
||||
|
||||
<div v-if="settings.enableAllowList">
|
||||
<n-alert type="warning" style="margin-bottom: 16px;">
|
||||
{{ t('enableAllowListTip') }}
|
||||
</n-alert>
|
||||
|
||||
<n-form-item-row :label="t('allowList')">
|
||||
<n-select v-model:value="settings.allowList" filterable multiple tag
|
||||
:placeholder="t('allowListTip')">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-form-item-row>
|
||||
|
||||
<n-text depth="3" style="font-size: 12px;">
|
||||
{{ t('allowListTip') }}
|
||||
</n-text>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -15,10 +15,13 @@ const { t } = useI18n({
|
||||
en: {
|
||||
address: 'Address',
|
||||
enablePrefix: 'If enable Prefix',
|
||||
creatNewEmail: 'Get New Email',
|
||||
creatNewEmail: 'Create New Email',
|
||||
fillInAllFields: 'Please fill in all fields',
|
||||
successTip: 'Success Created',
|
||||
addressCredential: 'Mail Address Credential',
|
||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||
addressPassword: 'Address Password',
|
||||
linkWithAddressCredential: 'Open to auto login email link',
|
||||
},
|
||||
zh: {
|
||||
address: '地址',
|
||||
@@ -27,6 +30,9 @@ const { t } = useI18n({
|
||||
fillInAllFields: '请填写完整信息',
|
||||
successTip: '创建成功',
|
||||
addressCredential: '邮箱地址凭证',
|
||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||
addressPassword: '地址密码',
|
||||
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -36,6 +42,8 @@ const emailName = ref("")
|
||||
const emailDomain = ref("")
|
||||
const showReultModal = ref(false)
|
||||
const result = ref("")
|
||||
const addressPassword = ref("")
|
||||
const createdAddress = ref("")
|
||||
|
||||
const newEmail = async () => {
|
||||
if (!emailName.value || !emailDomain.value) {
|
||||
@@ -52,6 +60,8 @@ const newEmail = async () => {
|
||||
})
|
||||
})
|
||||
result.value = res["jwt"];
|
||||
addressPassword.value = res["password"] || '';
|
||||
createdAddress.value = res["address"] || '';
|
||||
message.success(t('successTip'))
|
||||
showReultModal.value = true
|
||||
} catch (error) {
|
||||
@@ -59,6 +69,10 @@ const newEmail = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getUrlWithJwt = () => {
|
||||
return `${window.location.origin}/?jwt=${result.value}`
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (openSettings.prefix) {
|
||||
enablePrefix.value = true
|
||||
@@ -70,14 +84,29 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('addressCredential')">
|
||||
<p>{{ t('addressCredential') }}</p>
|
||||
<n-card :bordered="false" embedded>
|
||||
<span>
|
||||
<p>{{ t("addressCredentialTip") }}</p>
|
||||
</span>
|
||||
<n-card embedded>
|
||||
<b>{{ result }}</b>
|
||||
</n-card>
|
||||
<n-card embedded v-if="addressPassword">
|
||||
<p><b>{{ createdAddress }}</b></p>
|
||||
<p>{{ t('addressPassword') }}: <b>{{ addressPassword }}</b></p>
|
||||
</n-card>
|
||||
<n-card embedded>
|
||||
<n-collapse>
|
||||
<n-collapse-item :title='t("linkWithAddressCredential")'>
|
||||
<n-card embedded>
|
||||
<b>{{ getUrlWithJwt() }}</b>
|
||||
</n-card>
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
|
||||
<n-checkbox v-model:checked="enablePrefix" />
|
||||
<n-switch v-model:value="enablePrefix" :round="false" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('address')">
|
||||
<n-input-group>
|
||||
|
||||
220
frontend/src/views/admin/IpBlacklistSettings.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { loading } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
title: 'IP Blacklist Settings',
|
||||
manualInputPrompt: 'Type pattern and press Enter to add',
|
||||
save: 'Save',
|
||||
successTip: 'Save Success',
|
||||
enable_ip_blacklist: 'Enable IP Blacklist',
|
||||
enable_tip: 'Block IPs matching blacklist patterns from accessing rate-limited APIs',
|
||||
ip_blacklist: 'IP Blacklist Patterns',
|
||||
ip_blacklist_placeholder: 'Enter pattern (e.g., 192.168.1 or ^10\\.0\\.0\\.5$)',
|
||||
asn_blacklist: 'ASN Organization Blacklist',
|
||||
asn_blacklist_placeholder: 'Enter ASN organization (e.g., Google, Amazon)',
|
||||
fingerprint_blacklist: 'Browser Fingerprint Blacklist',
|
||||
fingerprint_blacklist_placeholder: 'Enter fingerprint ID (e.g., a1b2c3d4e5f6g7h8)',
|
||||
tip_ip: 'IP Blacklist: Supports text matching (e.g., "192.168.1") or regex (e.g., "^10\\.0\\.0\\.5$").',
|
||||
tip_asn: 'ASN Organization: Block by ISP/provider. Case-insensitive text matching or regex.',
|
||||
tip_fingerprint: 'Browser Fingerprint: Block by browser fingerprint. Supports exact matching or regex patterns.',
|
||||
tip_daily_limit: 'Daily Limit: Restrict the maximum number of requests per IP address per day (1-1000000).',
|
||||
tip_scope: 'Applies to: Create Address, Send Mail, External Send Mail API, User Registration, Verify Code',
|
||||
enable_daily_limit: 'Enable Daily Request Limit',
|
||||
enable_daily_limit_tip: 'Limit the number of API requests per IP address per day',
|
||||
daily_request_limit: 'Daily Request Limit',
|
||||
daily_request_limit_placeholder: 'Enter limit (e.g., 1000)',
|
||||
},
|
||||
zh: {
|
||||
title: 'IP 黑名单设置',
|
||||
manualInputPrompt: '输入匹配模式后按回车键添加',
|
||||
save: '保存',
|
||||
successTip: '保存成功',
|
||||
enable_ip_blacklist: '启用 IP 黑名单',
|
||||
enable_tip: '阻止匹配黑名单的 IP 访问限流 API',
|
||||
ip_blacklist: 'IP 黑名单匹配模式',
|
||||
ip_blacklist_placeholder: '输入匹配模式(例如:192.168.1 或 ^10\\.0\\.0\\.5$)',
|
||||
asn_blacklist: 'ASN 组织(运营商)黑名单',
|
||||
asn_blacklist_placeholder: '输入 ASN 组织名称(例如:Google, Amazon)',
|
||||
fingerprint_blacklist: '浏览器指纹黑名单',
|
||||
fingerprint_blacklist_placeholder: '输入指纹 ID(例如:a1b2c3d4e5f6g7h8)',
|
||||
tip_ip: 'IP 黑名单:支持文本匹配(如 "192.168.1")或正则表达式(如 "^10\\.0\\.0\\.5$")。',
|
||||
tip_asn: 'ASN 组织:根据运营商/ISP 拉黑。支持不区分大小写的文本匹配或正则表达式。',
|
||||
tip_fingerprint: '浏览器指纹:根据浏览器指纹拉黑。支持完全匹配或正则表达式。',
|
||||
tip_daily_limit: '每日限流:限制单个 IP 地址每天最多请求次数(1-1000000)。',
|
||||
tip_scope: '作用范围:创建邮箱地址、发送邮件、外部发送邮件 API、用户注册、验证码验证',
|
||||
enable_daily_limit: '启用每日请求限流',
|
||||
enable_daily_limit_tip: '限制每个 IP 地址每天的 API 请求次数',
|
||||
daily_request_limit: '每日请求次数上限',
|
||||
daily_request_limit_placeholder: '输入限制次数(例如:1000)',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const enabled = ref(false)
|
||||
const ipBlacklist = ref([])
|
||||
const asnBlacklist = ref([])
|
||||
const fingerprintBlacklist = ref([])
|
||||
const enableDailyLimit = ref(false)
|
||||
const dailyRequestLimit = ref(1000)
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await api.fetch(`/admin/ip_blacklist/settings`)
|
||||
enabled.value = res.enabled || false
|
||||
ipBlacklist.value = res.blacklist || []
|
||||
asnBlacklist.value = res.asnBlacklist || []
|
||||
fingerprintBlacklist.value = res.fingerprintBlacklist || []
|
||||
enableDailyLimit.value = res.enableDailyLimit || false
|
||||
dailyRequestLimit.value = res.dailyRequestLimit || 1000
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await api.fetch(`/admin/ip_blacklist/settings`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
enabled: enabled.value,
|
||||
blacklist: ipBlacklist.value || [],
|
||||
asnBlacklist: asnBlacklist.value || [],
|
||||
fingerprintBlacklist: fingerprintBlacklist.value || [],
|
||||
enableDailyLimit: enableDailyLimit.value,
|
||||
dailyRequestLimit: dailyRequestLimit.value
|
||||
})
|
||||
})
|
||||
message.success(t('successTip'))
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :title="t('title')" :bordered="false" embedded style="max-width: 800px;">
|
||||
<template #header-extra>
|
||||
<n-button @click="save" type="primary" :loading="loading">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</template>
|
||||
|
||||
<n-space vertical :size="20">
|
||||
<n-alert :show-icon="false" :bordered="false" type="info">
|
||||
<div style="line-height: 1.8;">
|
||||
<div><strong>{{ t("tip_scope") }}</strong></div>
|
||||
<div>• {{ t("tip_ip") }}</div>
|
||||
<div>• {{ t("tip_asn") }}</div>
|
||||
<div>• {{ t("tip_fingerprint") }}</div>
|
||||
<div>• {{ t("tip_daily_limit") }}</div>
|
||||
</div>
|
||||
</n-alert>
|
||||
|
||||
<n-form-item-row :label="t('enable_ip_blacklist')">
|
||||
<n-switch v-model:value="enabled" :round="false" />
|
||||
<n-text depth="3" style="margin-left: 10px; font-size: 12px;">
|
||||
{{ t('enable_tip') }}
|
||||
</n-text>
|
||||
</n-form-item-row>
|
||||
|
||||
<n-form-item-row :label="t('ip_blacklist')">
|
||||
<n-select
|
||||
v-model:value="ipBlacklist"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
:placeholder="t('ip_blacklist_placeholder')"
|
||||
:disabled="!enabled">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-form-item-row>
|
||||
|
||||
<n-form-item-row :label="t('asn_blacklist')">
|
||||
<n-select
|
||||
v-model:value="asnBlacklist"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
:placeholder="t('asn_blacklist_placeholder')"
|
||||
:disabled="!enabled">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-form-item-row>
|
||||
|
||||
<n-form-item-row :label="t('fingerprint_blacklist')">
|
||||
<n-select
|
||||
v-model:value="fingerprintBlacklist"
|
||||
filterable
|
||||
multiple
|
||||
tag
|
||||
:placeholder="t('fingerprint_blacklist_placeholder')"
|
||||
:disabled="!enabled">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-form-item-row>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<n-form-item-row :label="t('enable_daily_limit')">
|
||||
<n-switch v-model:value="enableDailyLimit" :round="false" />
|
||||
<n-text depth="3" style="margin-left: 10px; font-size: 12px;">
|
||||
{{ t('enable_daily_limit_tip') }}
|
||||
</n-text>
|
||||
</n-form-item-row>
|
||||
|
||||
<n-form-item-row :label="t('daily_request_limit')">
|
||||
<n-input-number
|
||||
v-model:value="dailyRequestLimit"
|
||||
:min="1"
|
||||
:max="1000000"
|
||||
:placeholder="t('daily_request_limit_placeholder')"
|
||||
:disabled="!enableDailyLimit"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</n-form-item-row>
|
||||
</n-space>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
margin: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -11,10 +11,16 @@ const cleanupModel = ref({
|
||||
cleanMailsDays: 30,
|
||||
enableUnknowMailsAutoCleanup: false,
|
||||
cleanUnknowMailsDays: 30,
|
||||
enableAddressAutoCleanup: false,
|
||||
cleanAddressDays: 30,
|
||||
enableSendBoxAutoCleanup: false,
|
||||
cleanSendBoxDays: 30,
|
||||
enableAddressAutoCleanup: false,
|
||||
cleanAddressDays: 30,
|
||||
enableInactiveAddressAutoCleanup: false,
|
||||
cleanInactiveAddressDays: 30,
|
||||
enableUnboundAddressAutoCleanup: false,
|
||||
cleanUnboundAddressDays: 30,
|
||||
enableEmptyAddressAutoCleanup: false,
|
||||
cleanEmptyAddressDays: 30,
|
||||
})
|
||||
|
||||
const { t } = useI18n({
|
||||
@@ -24,22 +30,30 @@ const { t } = useI18n({
|
||||
mailBoxLabel: 'Cleanup the inbox before n days',
|
||||
mailUnknowLabel: "Cleanup the unknow mail before n days",
|
||||
sendBoxLabel: "Cleanup the sendbox before n days",
|
||||
addressCreateLabel: "Cleanup the address created before n days",
|
||||
inactiveAddressLabel: "Cleanup the inactive address before n days",
|
||||
unboundAddressLabel: "Cleanup the unbound address before n days",
|
||||
emptyAddressLabel: "Cleanup the empty address before n days",
|
||||
cleanupNow: "Cleanup now",
|
||||
autoCleanup: "Auto cleanup",
|
||||
cleanupSuccess: "Cleanup success",
|
||||
save: "Save",
|
||||
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document",
|
||||
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document, setting 0 days means clear all",
|
||||
},
|
||||
zh: {
|
||||
tip: '请输入天数',
|
||||
mailBoxLabel: '清理 n 天前的收件箱',
|
||||
mailUnknowLabel: "清理 n 天前的无收件人邮件",
|
||||
sendBoxLabel: "清理 n 天前的发件箱",
|
||||
addressCreateLabel: "清理 n 天前创建的地址",
|
||||
inactiveAddressLabel: "清理 n 天前的未活跃地址",
|
||||
unboundAddressLabel: "清理 n 天前的未绑定用户地址",
|
||||
emptyAddressLabel: "清理 n 天前空邮件的邮箱地址",
|
||||
autoCleanup: "自动清理",
|
||||
cleanupSuccess: "清理成功",
|
||||
cleanupNow: "立即清理",
|
||||
save: "保存",
|
||||
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档",
|
||||
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档, 配置为 0 天表示全部清空",
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -86,9 +100,14 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-alert :show-icon="false" :bordered="false">
|
||||
<n-alert :show-icon="false" :bordered="false" type="warning">
|
||||
<span>{{ t('cronTip') }}</span>
|
||||
</n-alert>
|
||||
<n-flex justify="end">
|
||||
<n-button @click="save" type="primary" :loading="loading">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-form :model="cleanupModel">
|
||||
<n-form-item-row :label="t('mailBoxLabel')">
|
||||
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
|
||||
@@ -126,9 +145,54 @@ onMounted(async () => {
|
||||
{{ t('cleanupNow') }}
|
||||
</n-button>
|
||||
</n-form-item-row>
|
||||
<n-button @click="save" type="primary" block :loading="loading">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
<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-card>
|
||||
</div>
|
||||
|
||||
153
frontend/src/views/admin/RoleAddressConfig.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, h } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { NInputNumber, NTag, NSpace, NButton } from 'naive-ui';
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { loading } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
role: 'Role',
|
||||
maxAddressCount: 'Max Address Count',
|
||||
save: 'Save',
|
||||
successTip: 'Success',
|
||||
noRolesAvailable: 'No roles available in system config',
|
||||
roleConfigDesc: 'Configure maximum address count for each user role. Role-based limits take priority over global settings.',
|
||||
notConfigured: 'Not Configured (Use Global Settings)',
|
||||
},
|
||||
zh: {
|
||||
role: '角色',
|
||||
maxAddressCount: '最大地址数量',
|
||||
save: '保存',
|
||||
successTip: '成功',
|
||||
noRolesAvailable: '系统配置中没有可用的角色',
|
||||
roleConfigDesc: '为每个用户角色配置最大地址数量。角色配置优先于全局设置。',
|
||||
notConfigured: '未配置(使用全局设置)',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const systemRoles = ref([])
|
||||
const tableData = ref([])
|
||||
|
||||
const fetchUserRoles = async () => {
|
||||
try {
|
||||
const results = await api.fetch(`/admin/user_roles`);
|
||||
systemRoles.value = results;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const fetchRoleConfigs = async () => {
|
||||
try {
|
||||
const { configs } = await api.fetch(`/admin/role_address_config`);
|
||||
tableData.value = systemRoles.value.map(roleObj => ({
|
||||
role: roleObj.role,
|
||||
max_address_count: configs[roleObj.role]?.maxAddressCount ?? null,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const saveConfig = async () => {
|
||||
try {
|
||||
// convert tableData to object with nested structure
|
||||
const configs = {};
|
||||
tableData.value.forEach(row => {
|
||||
if (row.max_address_count !== null && row.max_address_count !== undefined) {
|
||||
configs[row.role] = { maxAddressCount: row.max_address_count };
|
||||
}
|
||||
});
|
||||
|
||||
await api.fetch(`/admin/role_address_config`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ configs })
|
||||
});
|
||||
message.success(t('successTip'));
|
||||
await fetchRoleConfigs();
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('role'),
|
||||
key: 'role',
|
||||
width: 200,
|
||||
render(row) {
|
||||
return h(NTag, {
|
||||
type: 'info',
|
||||
bordered: false
|
||||
}, {
|
||||
default: () => row.role
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('maxAddressCount'),
|
||||
key: 'max_address_count',
|
||||
render(row) {
|
||||
return h(NInputNumber, {
|
||||
value: row.max_address_count,
|
||||
min: 0,
|
||||
max: 999,
|
||||
clearable: true,
|
||||
placeholder: t('notConfigured'),
|
||||
style: 'width: 200px;',
|
||||
onUpdateValue: (value) => {
|
||||
row.max_address_count = value;
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchUserRoles();
|
||||
await fetchRoleConfigs();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="margin-top: 10px;">
|
||||
<n-alert type="info" :bordered="false" style="margin-bottom: 20px;">
|
||||
{{ t('roleConfigDesc') }}
|
||||
</n-alert>
|
||||
|
||||
<n-alert v-if="systemRoles.length === 0" type="warning" :bordered="false">
|
||||
{{ t('noRolesAvailable') }}
|
||||
</n-alert>
|
||||
|
||||
<div v-else>
|
||||
<n-space justify="end" style="margin-bottom: 12px;">
|
||||
<n-button :loading="loading" @click="saveConfig" type="primary">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
|
||||
<n-data-table
|
||||
:columns="columns"
|
||||
:data="tableData"
|
||||
:bordered="false"
|
||||
embedded
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-data-table {
|
||||
min-width: 600px;
|
||||
}
|
||||
</style>
|
||||
@@ -15,25 +15,29 @@ const { t } = useI18n({
|
||||
init: 'Init',
|
||||
successTip: 'Success',
|
||||
status: 'Check Status',
|
||||
enableTelegramAllowList: 'Enable Telegram Allow List(Manually input user ID)',
|
||||
enableTelegramAllowList: 'Enable Telegram Allow List(Manually input Chat ID)',
|
||||
enable: 'Enable',
|
||||
telegramAllowList: 'Telegram Allow List(Manually input telegram user ID)',
|
||||
telegramAllowList: 'Telegram Allow List(Manually input telegram Chat ID)',
|
||||
manualInputPrompt: 'Type and press Enter to add',
|
||||
save: 'Save',
|
||||
miniAppUrl: 'Telegram Mini App URL',
|
||||
enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram user ID)',
|
||||
globalMailPushList: 'Global Mail Push List',
|
||||
enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram Chat ID)',
|
||||
globalMailPushList: 'Global Mail Push Chat ID List',
|
||||
globalMailPushListTip: 'Support chat_id of private chat/group/channel. You can send a message to your bot, then visit this link to see chat_id, https://api.telegram.org/bot<Replace with your BOT TOKEN>/getUpdates',
|
||||
},
|
||||
zh: {
|
||||
init: '初始化',
|
||||
successTip: '成功',
|
||||
status: '查看状态',
|
||||
enableTelegramAllowList: '启用 Telegram 白名单(手动输入用户 ID, 回车增加)',
|
||||
enableTelegramAllowList: '启用 Telegram 白名单(手动输入 Chat ID, 回车增加)',
|
||||
enable: '启用',
|
||||
telegramAllowList: 'Telegram 白名单(手动输入用户 ID, 回车增加)',
|
||||
telegramAllowList: 'Telegram 白名单(手动输入 Chat ID, 回车增加)',
|
||||
manualInputPrompt: '输入后按回车键添加',
|
||||
save: '保存',
|
||||
miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)',
|
||||
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram 用户 ID, 回车增加)',
|
||||
globalMailPushList: '全局邮件推送用户列表',
|
||||
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram Chat ID, 回车增加)',
|
||||
globalMailPushList: '全局邮件推送 Chat ID 列表',
|
||||
globalMailPushListTip: '支持对话/群组/频道的 Chat ID, 您可以发送一条消息给您的机器人,然后访问此链接来查看 chat_id, https://api.telegram.org/bot<这里替换成您的 BOT TOKEN>/getUpdates',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -113,6 +117,17 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||
<n-flex justify="end">
|
||||
<n-button @click="fetchStatus" secondary>
|
||||
{{ t('status') }}
|
||||
</n-button>
|
||||
<n-button @click="init" type="primary">
|
||||
{{ t('init') }}
|
||||
</n-button>
|
||||
<n-button @click="saveSettings" type="primary">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-form-item-row :label="t('enableTelegramAllowList')">
|
||||
<n-input-group>
|
||||
@@ -120,31 +135,41 @@ onMounted(async () => {
|
||||
{{ t('enable') }}
|
||||
</n-checkbox>
|
||||
<n-select v-model:value="settings.allowList" filterable multiple tag style="width: 80%;"
|
||||
:placeholder="t('telegramAllowList')" />
|
||||
:placeholder="t('telegramAllowList')">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
<br />
|
||||
<n-form-item-row :label="t('enableGlobalMailPush')">
|
||||
<n-input-group>
|
||||
<n-checkbox v-model:checked="settings.enableGlobalMailPush" style="width: 20%;">
|
||||
{{ t('enable') }}
|
||||
</n-checkbox>
|
||||
<n-select v-model:value="settings.globalMailPushList" filterable multiple tag
|
||||
style="width: 80%;" :placeholder="t('globalMailPushList')" />
|
||||
style="width: 80%;" :placeholder="t('globalMailPushList')">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-input-group>
|
||||
<template #feedback>
|
||||
<n-text depth="3">
|
||||
{{ t('globalMailPushListTip') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-form-item-row>
|
||||
<br />
|
||||
<n-form-item-row :label="t('miniAppUrl')">
|
||||
<n-input v-model:value="settings.miniAppUrl"></n-input>
|
||||
</n-form-item-row>
|
||||
<n-button @click="saveSettings" type="primary" block>
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-card>
|
||||
<n-button @click="init" type="primary" block>
|
||||
{{ t('init') }}
|
||||
</n-button>
|
||||
<n-button @click="fetchStatus" secondary block>
|
||||
{{ t('status') }}
|
||||
</n-button>
|
||||
<pre v-if="status.fetched">{{ JSON.stringify(status, null, 2) }}</pre>
|
||||
</n-card>
|
||||
</div>
|
||||
@@ -157,8 +182,4 @@ onMounted(async () => {
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -21,6 +21,7 @@ const { t } = useI18n({
|
||||
successTip: 'Save Success',
|
||||
enable: 'Enable',
|
||||
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
||||
manualInputPrompt: 'Type and press Enter to add',
|
||||
mailAllowList: 'Mail Address Allow List',
|
||||
addOauth2: 'Add Oauth2',
|
||||
name: 'Name',
|
||||
@@ -33,6 +34,7 @@ const { t } = useI18n({
|
||||
successTip: '保存成功',
|
||||
enable: '启用',
|
||||
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
|
||||
manualInputPrompt: '输入后按回车键添加',
|
||||
mailAllowList: '邮件地址白名单',
|
||||
addOauth2: '添加 Oauth2',
|
||||
name: '名称',
|
||||
@@ -184,7 +186,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<n-alert :show-icon="false" type="warning" closable style="margin-bottom: 10px;">
|
||||
<n-alert :show-icon="false" :bordered="false" type="warning" closable style="margin-bottom: 10px;">
|
||||
{{ t("tip") }}
|
||||
</n-alert>
|
||||
<n-flex justify="end">
|
||||
@@ -246,7 +248,13 @@ onMounted(async () => {
|
||||
</n-checkbox>
|
||||
<n-select v-model:value="item.mailAllowList" v-if="item.enableMailAllowList" filterable
|
||||
multiple tag style="width: 80%;" :options="mailAllowOptions"
|
||||
:placeholder="t('mailAllowList')" />
|
||||
:placeholder="t('mailAllowList')">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
</n-form>
|
||||
|
||||
@@ -18,6 +18,7 @@ const { t } = useI18n({
|
||||
enableMailVerify: 'Enable Mail Verify (Send address must be an address in the system with a balance and can send mail normally)',
|
||||
verifyMailSender: 'Verify Mail Sender',
|
||||
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
||||
manualInputPrompt: 'Type and press Enter to add',
|
||||
mailAllowList: 'Mail Address Allow List',
|
||||
maxAddressCount: 'Maximum number of email addresses that can be binded',
|
||||
},
|
||||
@@ -29,6 +30,7 @@ const { t } = useI18n({
|
||||
enableMailVerify: '启用邮件验证(发送地址必须是系统中能有余额且能正常发送邮件的地址)',
|
||||
verifyMailSender: '验证邮件发送地址',
|
||||
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
|
||||
manualInputPrompt: '输入后按回车键添加',
|
||||
mailAllowList: '邮件地址白名单',
|
||||
maxAddressCount: '可绑定最大邮箱地址数量',
|
||||
}
|
||||
@@ -83,9 +85,14 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<n-flex justify="end">
|
||||
<n-button @click="save" type="primary" :loading="loading">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-form :model="userSettings">
|
||||
<n-form-item-row :label="t('enableUserRegister')">
|
||||
<n-checkbox v-model:checked="userSettings.enable" />
|
||||
<n-switch v-model:value="userSettings.enable" :round="false" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('enableMailVerify')">
|
||||
<n-input-group>
|
||||
@@ -103,7 +110,13 @@ onMounted(async () => {
|
||||
</n-checkbox>
|
||||
<n-select v-model:value="userSettings.mailAllowList" v-if="userSettings.enableMailAllowList"
|
||||
filterable multiple tag style="width: 80%;" :options="mailAllowOptions"
|
||||
:placeholder="t('mailAllowList')" />
|
||||
:placeholder="t('mailAllowList')">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('maxAddressCount')">
|
||||
@@ -112,9 +125,6 @@ onMounted(async () => {
|
||||
:placeholder="t('maxAddressCount')" />
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
<n-button @click="save" type="primary" block :loading="loading">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
@@ -13,13 +13,17 @@ const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
successTip: 'Success',
|
||||
webhookAllowList: 'Webhook Allow List(Enter the address that is allowed to use webhook and enter)',
|
||||
enableAllowList: 'Enable Allow List (Restrict webhook access to specific users)',
|
||||
webhookAllowList: 'Webhook Allow List(Enter the mail address that is allowed to use webhook and enter)',
|
||||
manualInputPrompt: 'Type and press Enter to add',
|
||||
save: 'Save',
|
||||
notEnabled: 'Webhook is not enabled',
|
||||
},
|
||||
zh: {
|
||||
successTip: '成功',
|
||||
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的地址, 回车增加)',
|
||||
enableAllowList: '启用白名单 (限制 webhook 访问权限,只有白名单中的用户可以使用)',
|
||||
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的邮箱地址, 回车增加)',
|
||||
manualInputPrompt: '输入后按回车键添加',
|
||||
save: '保存',
|
||||
notEnabled: 'Webhook 未开启',
|
||||
}
|
||||
@@ -27,14 +31,16 @@ const { t } = useI18n({
|
||||
});
|
||||
|
||||
class WebhookSettings {
|
||||
enableAllowList: boolean;
|
||||
allowList: string[];
|
||||
|
||||
constructor(allowList: string[]) {
|
||||
constructor(enableAllowList: boolean, allowList: string[]) {
|
||||
this.enableAllowList = enableAllowList;
|
||||
this.allowList = allowList;
|
||||
}
|
||||
}
|
||||
|
||||
const webhookSettings = ref(new WebhookSettings([]))
|
||||
const webhookSettings = ref(new WebhookSettings(false, []))
|
||||
const webhookEnabled = ref(false)
|
||||
const errorInfo = ref('')
|
||||
|
||||
@@ -68,13 +74,24 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card v-if="webhookEnabled" :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||
<n-flex justify="end">
|
||||
<n-button @click="saveSettings" type="primary">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-form-item-row :label="t('enableAllowList')">
|
||||
<n-switch v-model:value="webhookSettings.enableAllowList" :round="false" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('webhookAllowList')">
|
||||
<n-select v-model:value="webhookSettings.allowList" filterable multiple tag
|
||||
:placeholder="t('webhookAllowList')" />
|
||||
:placeholder="t('webhookAllowList')">
|
||||
<template #empty>
|
||||
<n-text depth="3">
|
||||
{{ t('manualInputPrompt') }}
|
||||
</n-text>
|
||||
</template>
|
||||
</n-select>
|
||||
</n-form-item-row>
|
||||
<n-button @click="saveSettings" type="primary" block>
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
</n-card>
|
||||
<n-result v-else status="404" :title="t('notEnabled')" :description="errorInfo" />
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded style="max-width: 600px; overflow: auto;">
|
||||
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||
<pre>{{ JSON.stringify(settings, null, 2) }}</pre>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
@@ -3,16 +3,23 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useIsMobile } from '../../utils/composables'
|
||||
import { useGlobalState } from '../../store'
|
||||
const props = defineProps({
|
||||
showUseSimpleIndex: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const {
|
||||
mailboxSplitSize, useIframeShowMail, preferShowTextMail, configAutoRefreshInterval,
|
||||
globalTabplacement, useSideMargin, useUTCDate
|
||||
globalTabplacement, useSideMargin, useUTCDate, useSimpleIndex
|
||||
} = useGlobalState()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
useSimpleIndex: 'Use Simple Index',
|
||||
mailboxSplitSize: 'Mailbox Split Size',
|
||||
useIframeShowMail: 'Use iframe Show HTML Mail',
|
||||
preferShowTextMail: 'Display text Mail by default',
|
||||
@@ -26,6 +33,7 @@ const { t } = useI18n({
|
||||
autoRefreshInterval: 'Auto Refresh Interval(Sec)',
|
||||
},
|
||||
zh: {
|
||||
useSimpleIndex: '使用极简主页',
|
||||
mailboxSplitSize: '邮箱界面分栏大小',
|
||||
preferShowTextMail: '默认以文本显示邮件',
|
||||
useIframeShowMail: '使用iframe显示HTML邮件',
|
||||
@@ -57,6 +65,9 @@ const { t } = useI18n({
|
||||
60: '60', 120: '120', 180: '180', 240: '240'
|
||||
}" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row v-if="props.showUseSimpleIndex" :label="t('useSimpleIndex')">
|
||||
<n-switch v-model:value="useSimpleIndex" :round="false" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('preferShowTextMail')">
|
||||
<n-switch v-model:value="preferShowTextMail" :round="false" />
|
||||
</n-form-item-row>
|
||||
|
||||
@@ -9,7 +9,7 @@ import Turnstile from '../../components/Turnstile.vue'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { getRouterPathWithLang } from '../../utils'
|
||||
import { getRouterPathWithLang, hashPassword } from '../../utils'
|
||||
|
||||
const props = defineProps({
|
||||
bindUserAddress: {
|
||||
@@ -39,7 +39,7 @@ const router = useRouter()
|
||||
|
||||
const {
|
||||
jwt, loading, openSettings,
|
||||
showAddressCredential, userSettings
|
||||
showAddressCredential, userSettings, addressPassword
|
||||
} = useGlobalState()
|
||||
|
||||
const tabValue = ref('signin')
|
||||
@@ -47,8 +47,47 @@ const credential = ref('')
|
||||
const emailName = ref("")
|
||||
const emailDomain = ref("")
|
||||
const cfToken = ref("")
|
||||
const loginMethod = ref('credential') // 'credential' or 'password'
|
||||
const loginAddress = ref('')
|
||||
const loginPassword = ref('')
|
||||
|
||||
// 根据 openSettings 初始化登录方式
|
||||
const initLoginMethod = () => {
|
||||
if (openSettings.value?.enableAddressPassword) {
|
||||
loginMethod.value = 'password';
|
||||
} else {
|
||||
loginMethod.value = 'credential';
|
||||
}
|
||||
}
|
||||
|
||||
const login = async () => {
|
||||
if (loginMethod.value === 'password') {
|
||||
// Password login
|
||||
if (!loginAddress.value || !loginPassword.value) {
|
||||
message.error(t('emailPasswordRequired'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await api.fetch('/api/address_login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email: loginAddress.value,
|
||||
password: await hashPassword(loginPassword.value)
|
||||
})
|
||||
});
|
||||
jwt.value = res.jwt;
|
||||
await api.getSettings();
|
||||
try {
|
||||
await props.bindUserAddress();
|
||||
} catch (error) {
|
||||
message.error(`${t('bindUserAddressError')}: ${error.message}`);
|
||||
}
|
||||
await router.push(getRouterPathWithLang("/", locale.value));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!credential.value) {
|
||||
message.error(t('credentialInput'));
|
||||
return;
|
||||
@@ -84,6 +123,12 @@ const { locale, t } = useI18n({
|
||||
credentialInput: 'Please input the Mail Address Credential',
|
||||
bindUserInfo: 'Logged in user, login without binding email or create new email address will bind to current user',
|
||||
bindUserAddressError: 'Error when bind email address to user',
|
||||
autoGeneratedName: 'Auto-generated name',
|
||||
passwordLogin: 'Password Login',
|
||||
credentialLogin: 'Credential Login',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
emailPasswordRequired: 'Email and password are required',
|
||||
},
|
||||
zh: {
|
||||
login: '登录',
|
||||
@@ -100,6 +145,12 @@ const { locale, t } = useI18n({
|
||||
credentialInput: '请输入邮箱地址凭据',
|
||||
bindUserInfo: '已登录用户, 登录未绑定邮箱或创建新邮箱地址将绑定到当前用户',
|
||||
bindUserAddressError: '绑定邮箱地址到用户时错误',
|
||||
autoGeneratedName: '自动生成名称',
|
||||
passwordLogin: '密码登录',
|
||||
credentialLogin: '凭据登录',
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
emailPasswordRequired: '邮箱和密码不能为空',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -147,12 +198,15 @@ const generateName = async () => {
|
||||
|
||||
const newEmail = async () => {
|
||||
try {
|
||||
// If custom names are disabled, send empty name to trigger backend auto-generation
|
||||
const nameToSend = openSettings.value.disableCustomAddressName ? "" : emailName.value;
|
||||
const res = await props.newAddressPath(
|
||||
emailName.value,
|
||||
nameToSend,
|
||||
emailDomain.value,
|
||||
cfToken.value
|
||||
);
|
||||
jwt.value = res["jwt"];
|
||||
addressPassword.value = res["password"] || '';
|
||||
await api.getSettings();
|
||||
await router.push(getRouterPathWithLang("/", locale.value));
|
||||
showAddressCredential.value = true;
|
||||
@@ -208,6 +262,7 @@ onMounted(async () => {
|
||||
await api.getOpenSettings(message, notification);
|
||||
}
|
||||
emailDomain.value = domainsOptions.value ? domainsOptions.value[0]?.value : "";
|
||||
initLoginMethod();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -219,9 +274,29 @@ onMounted(async () => {
|
||||
<n-tabs v-if="openSettings.fetched" v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||
<n-tab-pane name="signin" :tab="loginAndBindTag">
|
||||
<n-form>
|
||||
<n-form-item-row :label="t('credential')" required>
|
||||
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
</n-form-item-row>
|
||||
<div v-if="loginMethod === 'password'">
|
||||
<n-form-item-row :label="t('email')" required>
|
||||
<n-input v-model:value="loginAddress" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('password')" required>
|
||||
<n-input v-model:value="loginPassword" type="password" show-password-on="click" />
|
||||
</n-form-item-row>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<n-form-item-row :label="t('credential')" required>
|
||||
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
</n-form-item-row>
|
||||
</div>
|
||||
|
||||
<div class="switch-login-button">
|
||||
<n-button v-if="openSettings?.enableAddressPassword"
|
||||
@click="loginMethod === 'password' ? loginMethod = 'credential' : loginMethod = 'password'"
|
||||
type="info" quaternary size="tiny">
|
||||
{{ loginMethod === 'password' ? t('credentialLogin') : t('passwordLogin') }}
|
||||
</n-button>
|
||||
</div>
|
||||
|
||||
<n-button @click="login" :loading="loading" type="primary" block secondary strong>
|
||||
<template #icon>
|
||||
<n-icon :component="EmailOutlined" />
|
||||
@@ -240,19 +315,22 @@ onMounted(async () => {
|
||||
<n-spin :show="generateNameLoading">
|
||||
<n-form>
|
||||
<span>
|
||||
<p>{{ t("getNewEmailTip1") + addressRegex.source }}</p>
|
||||
<p>{{ t("getNewEmailTip2") }}</p>
|
||||
<p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip1") +
|
||||
addressRegex.source }}</p>
|
||||
<p v-if="!openSettings.disableCustomAddressName">{{ t("getNewEmailTip2") }}</p>
|
||||
<p>{{ t("getNewEmailTip3") }}</p>
|
||||
</span>
|
||||
<n-button @click="generateName" style="margin-bottom: 10px;">
|
||||
<n-button v-if="!openSettings.disableCustomAddressName" @click="generateName"
|
||||
style="margin-bottom: 10px;">
|
||||
{{ t('generateName') }}
|
||||
</n-button>
|
||||
<n-input-group>
|
||||
<n-input-group-label v-if="addressPrefix">
|
||||
{{ addressPrefix }}
|
||||
</n-input-group-label>
|
||||
<n-input v-model:value="emailName" show-count :minlength="openSettings.minAddressLen"
|
||||
:maxlength="openSettings.maxAddressLen" />
|
||||
<n-input v-if="!openSettings.disableCustomAddressName" v-model:value="emailName" show-count
|
||||
:minlength="openSettings.minAddressLen" :maxlength="openSettings.maxAddressLen" />
|
||||
<n-input v-else :value="t('autoGeneratedName')" disabled />
|
||||
<n-input-group-label>@</n-input-group-label>
|
||||
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||
:options="domainsOptions" />
|
||||
@@ -289,6 +367,12 @@ onMounted(async () => {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.switch-login-button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.n-form {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -5,34 +5,59 @@ import { useRouter } from 'vue-router'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import Appearance from '../common/Appearance.vue'
|
||||
import { hashPassword } from '../../utils'
|
||||
import { getRouterPathWithLang } from '../../utils'
|
||||
|
||||
const {
|
||||
jwt, settings, showAddressCredential, loading
|
||||
jwt, settings, showAddressCredential, loading, openSettings
|
||||
} = useGlobalState()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const showLogout = ref(false)
|
||||
const showDelteAccount = ref(false)
|
||||
const showDeleteAccount = ref(false)
|
||||
const showClearInbox = ref(false)
|
||||
const showClearSentItems = ref(false)
|
||||
const showChangePassword = ref(false)
|
||||
const newPassword = ref('')
|
||||
const confirmPassword = ref('')
|
||||
const { locale, t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
logout: "Logout",
|
||||
delteAccount: "Delete Account",
|
||||
deleteAccount: "Delete Account",
|
||||
showAddressCredential: 'Show Address Credential',
|
||||
logoutConfirm: 'Are you sure to logout?',
|
||||
delteAccount: "Delete Account",
|
||||
delteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
||||
deleteAccount: "Delete Account",
|
||||
deleteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
||||
clearInbox: "Clear Inbox",
|
||||
clearSentItems: "Clear Sent Items",
|
||||
clearInboxConfirm: "Are you sure to clear all emails in your inbox?",
|
||||
clearSentItemsConfirm: "Are you sure to clear all emails in your sent items?",
|
||||
success: "Success",
|
||||
changePassword: "Change Password",
|
||||
newPassword: "New Password",
|
||||
confirmPassword: "Confirm Password",
|
||||
passwordMismatch: "Passwords do not match",
|
||||
passwordChanged: "Password changed successfully",
|
||||
},
|
||||
zh: {
|
||||
logout: '退出登录',
|
||||
delteAccount: "删除账户",
|
||||
deleteAccount: "删除账户",
|
||||
showAddressCredential: '查看邮箱地址凭证',
|
||||
logoutConfirm: '确定要退出登录吗?',
|
||||
delteAccount: "删除账户",
|
||||
delteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
||||
deleteAccount: "删除账户",
|
||||
deleteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
||||
clearInbox: "清空收件箱",
|
||||
clearSentItems: "清空发件箱",
|
||||
clearInboxConfirm: "确定要清空你收件箱中的所有邮件吗?",
|
||||
clearSentItemsConfirm: "确定要清空你发件箱中的所有邮件吗?",
|
||||
success: "成功",
|
||||
changePassword: "修改密码",
|
||||
newPassword: "新密码",
|
||||
confirmPassword: "确认密码",
|
||||
passwordMismatch: "密码不匹配",
|
||||
passwordChanged: "密码修改成功",
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -55,20 +80,78 @@ const deleteAccount = async () => {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
};
|
||||
|
||||
const clearInbox = async () => {
|
||||
try {
|
||||
await api.fetch(`/api/clear_inbox`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
message.success(t("success"));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
showClearInbox.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearSentItems = async () => {
|
||||
try {
|
||||
await api.fetch(`/api/clear_sent_items`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
message.success(t("success"));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
showClearSentItems.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const changePassword = async () => {
|
||||
if (newPassword.value !== confirmPassword.value) {
|
||||
message.error(t("passwordMismatch"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await api.fetch(`/api/address_change_password`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
new_password: await hashPassword(newPassword.value)
|
||||
})
|
||||
});
|
||||
message.success(t("passwordChanged"));
|
||||
newPassword.value = '';
|
||||
confirmPassword.value = '';
|
||||
showChangePassword.value = false;
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center" v-if="settings.address">
|
||||
<n-card :bordered="false" embedded>
|
||||
<Appearance />
|
||||
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
||||
{{ t('showAddressCredential') }}
|
||||
</n-button>
|
||||
<n-button v-if="openSettings?.enableAddressPassword" @click="showChangePassword = true" type="info" secondary block strong>
|
||||
{{ t('changePassword') }}
|
||||
</n-button>
|
||||
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showClearInbox = true" type="warning" secondary
|
||||
block strong>
|
||||
{{ t('clearInbox') }}
|
||||
</n-button>
|
||||
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showClearSentItems = true" type="warning"
|
||||
secondary block strong>
|
||||
{{ t('clearSentItems') }}
|
||||
</n-button>
|
||||
<n-button @click="showLogout = true" secondary block strong>
|
||||
{{ t('logout') }}
|
||||
</n-button>
|
||||
<n-button @click="showDelteAccount = true" type="error" secondary block strong>
|
||||
{{ t('delteAccount') }}
|
||||
<n-button v-if="openSettings.enableUserDeleteEmail" @click="showDeleteAccount = true" type="error" secondary
|
||||
block strong>
|
||||
{{ t('deleteAccount') }}
|
||||
</n-button>
|
||||
</n-card>
|
||||
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
||||
@@ -79,11 +162,43 @@ const deleteAccount = async () => {
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
|
||||
<p>{{ t('delteAccountConfirm') }}</p>
|
||||
<n-modal v-model:show="showDeleteAccount" preset="dialog" :title="t('deleteAccount')">
|
||||
<p>{{ t('deleteAccountConfirm') }}</p>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="deleteAccount" size="small" tertiary type="error">
|
||||
{{ t('delteAccount') }}
|
||||
{{ t('deleteAccount') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showClearInbox" preset="dialog" :title="t('clearInbox')">
|
||||
<p>{{ t('clearInboxConfirm') }}</p>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="clearInbox" size="small" tertiary type="warning">
|
||||
{{ t('clearInbox') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showClearSentItems" preset="dialog" :title="t('clearSentItems')">
|
||||
<p>{{ t('clearSentItemsConfirm') }}</p>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="clearSentItems" size="small" tertiary type="warning">
|
||||
{{ t('clearSentItems') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showChangePassword" preset="dialog" :title="t('changePassword')">
|
||||
<n-form :model="{ newPassword, confirmPassword }">
|
||||
<n-form-item :label="t('newPassword')">
|
||||
<n-input v-model:value="newPassword" type="password" placeholder="" show-password-on="click" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('confirmPassword')">
|
||||
<n-input v-model:value="confirmPassword" type="password" placeholder="" show-password-on="click" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="changePassword" size="small" tertiary type="info">
|
||||
{{ t('changePassword') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
@@ -19,7 +19,7 @@ const router = useRouter()
|
||||
|
||||
const {
|
||||
jwt, settings, showAddressCredential, userJwt,
|
||||
isTelegram, openSettings
|
||||
isTelegram, openSettings, addressPassword
|
||||
} = useGlobalState()
|
||||
|
||||
const { locale, t } = useI18n({
|
||||
@@ -34,6 +34,7 @@ const { locale, t } = useI18n({
|
||||
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',
|
||||
},
|
||||
zh: {
|
||||
@@ -46,6 +47,7 @@ const { locale, t } = useI18n({
|
||||
addressCredential: '邮箱地址凭证',
|
||||
linkWithAddressCredential: '打开即可自动登录邮箱的链接',
|
||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||
addressPassword: '地址密码',
|
||||
userLogin: '用户登录',
|
||||
}
|
||||
}
|
||||
@@ -149,6 +151,10 @@ onMounted(async () => {
|
||||
<n-card embedded>
|
||||
<b>{{ jwt }}</b>
|
||||
</n-card>
|
||||
<n-card embedded v-if="addressPassword">
|
||||
<p><b>{{ settings.address }}</b></p>
|
||||
<p>{{ t('addressPassword') }}: <b>{{ addressPassword }}</b></p>
|
||||
</n-card>
|
||||
<n-card embedded>
|
||||
<n-collapse>
|
||||
<n-collapse-item :title='t("linkWithAddressCredential")'>
|
||||
|
||||
283
frontend/src/views/index/SimpleIndex.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, computed, watch, onBeforeUnmount } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMessage } from 'naive-ui'
|
||||
import {
|
||||
ExitToAppFilled,
|
||||
ContentCopyFilled,
|
||||
RefreshFilled,
|
||||
ArrowBackIosNewFilled,
|
||||
ArrowForwardIosFilled,
|
||||
SettingsFilled
|
||||
} from '@vicons/material'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import Login from '../common/Login.vue'
|
||||
import AccountSettings from './AccountSettings.vue'
|
||||
import { processItem } from '../../utils/email-parser'
|
||||
import MailContentRenderer from '../../components/MailContentRenderer.vue'
|
||||
|
||||
const { jwt, settings, useSimpleIndex, showAddressCredential, openSettings, loading } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
// 邮件数据
|
||||
const currentPage = ref(1)
|
||||
const totalCount = ref(0)
|
||||
const currentMail = ref(null)
|
||||
const showAccountSettingsCard = ref(false)
|
||||
const currentAutoRefreshInterval = ref(60)
|
||||
const timer = ref(null)
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
exitSimpleIndex: 'Exit Simple',
|
||||
copyAddress: 'Copy',
|
||||
addressCopied: 'Address copied successfully',
|
||||
refreshMails: 'Refresh',
|
||||
noMails: 'No mails found',
|
||||
prevPage: 'Previous',
|
||||
nextPage: 'Next',
|
||||
refreshSuccess: 'Mails refreshed successfully',
|
||||
mailCount: '{current} / {total} emails',
|
||||
accountSettings: "Account Settings",
|
||||
addressCredential: 'Mail Address Credential',
|
||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login',
|
||||
deleteSuccess: 'Mail deleted successfully',
|
||||
refreshAfter: 'Refresh After {msg} Seconds',
|
||||
},
|
||||
zh: {
|
||||
exitSimpleIndex: '退出极简',
|
||||
copyAddress: '复制',
|
||||
addressCopied: '地址复制成功',
|
||||
refreshMails: '刷新',
|
||||
noMails: '暂无邮件',
|
||||
prevPage: '上一页',
|
||||
nextPage: '下一页',
|
||||
refreshSuccess: '邮件刷新成功',
|
||||
mailCount: '{current} / {total} 封邮件',
|
||||
accountSettings: "账户设置",
|
||||
addressCredential: '邮箱地址凭证',
|
||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||
deleteSuccess: '邮件删除成功',
|
||||
refreshAfter: '{msg}秒后刷新',
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 复制地址
|
||||
const copyAddress = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(settings.value.address)
|
||||
message.success(t('addressCopied'))
|
||||
} catch (error) {
|
||||
message.error('复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取邮件数据
|
||||
const fetchMails = async () => {
|
||||
if (!settings.value.address) return
|
||||
try {
|
||||
const { results, count } = await api.fetch(`/api/mails?limit=1&offset=${currentPage.value - 1}`)
|
||||
totalCount.value = count > 0 ? count : totalCount.value;
|
||||
const rawMail = results && results.length > 0 ? results[0] : null
|
||||
currentMail.value = rawMail ? await processItem(rawMail) : null
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch mails:', error)
|
||||
message.error('获取邮件失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除邮件
|
||||
const deleteMail = async () => {
|
||||
if (!currentMail.value) return;
|
||||
try {
|
||||
await api.fetch(`/api/mails/${currentMail.value.id}`, { method: 'DELETE' });
|
||||
message.success(t('deleteSuccess'));
|
||||
currentMail.value = null;
|
||||
await refreshMails();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete mail:', error);
|
||||
message.error('删除邮件失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新邮件
|
||||
const refreshMails = async () => {
|
||||
if (loading.value) return
|
||||
currentPage.value = 1
|
||||
showAccountSettingsCard.value = false
|
||||
currentAutoRefreshInterval.value = 60
|
||||
await fetchMails()
|
||||
message.success(t('refreshSuccess'))
|
||||
}
|
||||
|
||||
// 分页控制
|
||||
const currentPageDisplay = computed(() => currentPage.value)
|
||||
const totalPages = computed(() => Math.max(1, totalCount.value))
|
||||
const canGoPrev = computed(() => currentPage.value > 1)
|
||||
const canGoNext = computed(() => currentPage.value < totalPages.value)
|
||||
const isFirstPage = computed(() => currentPage.value === 1)
|
||||
|
||||
const prevPage = async () => {
|
||||
if (canGoPrev.value) {
|
||||
currentPage.value--
|
||||
}
|
||||
}
|
||||
|
||||
const nextPage = async () => {
|
||||
if (canGoNext.value) {
|
||||
currentPage.value++
|
||||
}
|
||||
}
|
||||
|
||||
// 监听页面变化
|
||||
watch(currentPage, () => {
|
||||
fetchMails()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await api.getSettings()
|
||||
await fetchMails()
|
||||
|
||||
// 启动自动刷新
|
||||
timer.value = setInterval(async () => {
|
||||
if (!isFirstPage.value) {
|
||||
currentAutoRefreshInterval.value = 60
|
||||
return
|
||||
}
|
||||
|
||||
if (--currentAutoRefreshInterval.value <= 0) {
|
||||
await refreshMails()
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(timer.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<div v-if="!settings.address">
|
||||
<n-card :bordered="false" embedded>
|
||||
<Login />
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<n-card :bordered="false" embedded>
|
||||
<div style="text-align: center; margin-bottom: 16px; font-size: 18px;">
|
||||
<n-text strong size="large">{{ settings.address }}</n-text>
|
||||
</div>
|
||||
<n-flex justify="center">
|
||||
<n-button @click="refreshMails" :loading="loading" type="primary" tertiary size="small">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<RefreshFilled />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('refreshMails') }}
|
||||
</n-button>
|
||||
<n-button @click="copyAddress" tertiary size="small">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ContentCopyFilled />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('copyAddress') }}
|
||||
</n-button>
|
||||
<n-button @click="useSimpleIndex = false" tertiary size="small">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ExitToAppFilled />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('exitSimpleIndex') }}
|
||||
</n-button>
|
||||
<n-button @click="showAccountSettingsCard = true" tertiary size="small">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<SettingsFilled />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('accountSettings') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<div v-if="isFirstPage" style="text-align: center; margin-top: 12px;">
|
||||
<n-text depth="3" size="12">
|
||||
{{ t('refreshAfter', { msg: Math.max(0, currentAutoRefreshInterval) }) }}
|
||||
</n-text>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<!-- 账户设置卡片 -->
|
||||
<n-card v-if="showAccountSettingsCard" :bordered="false" embedded closable
|
||||
@close="showAccountSettingsCard = false" :title="t('accountSettings')">
|
||||
<AccountSettings />
|
||||
</n-card>
|
||||
|
||||
<n-card v-else :bordered="false" embedded style="text-align: left;">
|
||||
|
||||
<div v-if="totalCount > 1">
|
||||
<n-flex justify="space-between">
|
||||
<n-button @click="prevPage" :disabled="!canGoPrev" text size="small">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowBackIosNewFilled />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('prevPage') }}
|
||||
</n-button>
|
||||
<n-text size="small">
|
||||
{{ t('mailCount', { current: currentPageDisplay, total: totalCount }) }}
|
||||
</n-text>
|
||||
<n-button @click="nextPage" :disabled="!canGoNext" text size="small" icon-placement="right">
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<ArrowForwardIosFilled />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ t('nextPage') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<div v-if="!currentMail" class="no-mail">
|
||||
<n-empty :description="t('noMails')" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<h3 v-if="currentMail.subject">{{ currentMail.subject }}</h3>
|
||||
<div style="margin-top: 16px;">
|
||||
<MailContentRenderer :mail="currentMail" :showEMailTo="false" :showReply="false"
|
||||
:enableUserDeleteEmail="openSettings.enableUserDeleteEmail" :showSaveS3="false"
|
||||
:onDelete="deleteMail" />
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
<n-modal v-model:show="showAddressCredential" preset="dialog" :title="t('addressCredential')">
|
||||
<span>
|
||||
<p>{{ t("addressCredentialTip") }}</p>
|
||||
</span>
|
||||
<n-card embedded>
|
||||
<b>{{ jwt }}</b>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.n-card {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -27,7 +27,7 @@ const mailKeyword = ref("")
|
||||
const addressFilterOptions = ref([]);
|
||||
|
||||
const queryMail = () => {
|
||||
addressFilter.value = addressFilter.value.trim();
|
||||
addressFilter.value = addressFilter.value ? addressFilter.value.trim() : addressFilter.value;
|
||||
mailKeyword.value = mailKeyword.value.trim();
|
||||
mailBoxKey.value = Date.now();
|
||||
}
|
||||
@@ -64,7 +64,6 @@ const deleteMail = async (curMailId) => {
|
||||
};
|
||||
|
||||
watch(addressFilter, async (newValue) => {
|
||||
console.log("addressFilter", newValue);
|
||||
queryMail();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temp-email-pages",
|
||||
"version": "0.10.0",
|
||||
"version": "1.1.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -11,7 +11,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"wrangler": "^4.19.1"
|
||||
"wrangler": "^4.53.0"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
@@ -65,6 +65,31 @@ class SimpleMailbox:
|
||||
self.addListener = self.listeners.append
|
||||
self.removeListener = self.listeners.remove
|
||||
self.message_count = 0
|
||||
self._update_message_count()
|
||||
|
||||
def _update_message_count(self):
|
||||
"""主动获取邮件总数"""
|
||||
try:
|
||||
if self.name == "INBOX":
|
||||
endpoint = "/api/mails"
|
||||
elif self.name == "SENT":
|
||||
endpoint = "/api/sendbox"
|
||||
else:
|
||||
return
|
||||
|
||||
res = httpx.get(
|
||||
f"{settings.proxy_url}{endpoint}?limit=1&offset=0",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.password}",
|
||||
"x-custom-auth": f"{settings.basic_password}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
if res.status_code == 200:
|
||||
self.message_count = res.json()["count"]
|
||||
# _logger.info(f"Updated {self.name} message count: {self.message_count}")
|
||||
except Exception as e:
|
||||
_logger.error(f"Failed to update message count for {self.name}: {e}")
|
||||
|
||||
def getFlags(self):
|
||||
return ["\\Seen"]
|
||||
@@ -73,7 +98,9 @@ class SimpleMailbox:
|
||||
return 0
|
||||
|
||||
def getMessageCount(self):
|
||||
return self.message_count or 1000
|
||||
# 每次请求时更新邮件总数
|
||||
self._update_message_count()
|
||||
return self.message_count
|
||||
|
||||
def getRecentCount(self):
|
||||
return 0
|
||||
@@ -91,6 +118,8 @@ class SimpleMailbox:
|
||||
return "/"
|
||||
|
||||
def requestStatus(self, names):
|
||||
# 在状态请求时也更新邮件总数
|
||||
self._update_message_count()
|
||||
r = {}
|
||||
if "MESSAGES" in names:
|
||||
r["MESSAGES"] = self.getMessageCount()
|
||||
@@ -105,65 +134,99 @@ class SimpleMailbox:
|
||||
return defer.succeed(r)
|
||||
|
||||
def fetch(self, messages, uid):
|
||||
"""边查边返回邮件"""
|
||||
result = []
|
||||
for range_item in messages.ranges:
|
||||
start, end = range_item
|
||||
_logger.info(f"Fetching messages: {self.name}, range: {start}-{end}")
|
||||
|
||||
for email_data in self.fetchGenerator(start, end):
|
||||
result.append(email_data)
|
||||
|
||||
# 返回列表而不是生成器,以支持 IMAP SEARCH 等需要索引访问的操作
|
||||
return result
|
||||
|
||||
def fetchGenerator(self, start, end):
|
||||
"""通用的邮件获取生成器,边查边返回"""
|
||||
start = max(start, 1)
|
||||
|
||||
# 根据邮箱类型确定API端点
|
||||
if self.name == "INBOX":
|
||||
return self.fetchINBOX(messages)
|
||||
if self.name == "SENT":
|
||||
return self.fetchSENT(messages)
|
||||
return []
|
||||
endpoint = "/api/mails"
|
||||
elif self.name == "SENT":
|
||||
endpoint = "/api/sendbox"
|
||||
else:
|
||||
return
|
||||
|
||||
def fetchINBOX(self, messages):
|
||||
start, end = messages.ranges[0]
|
||||
start = max(start, 1)
|
||||
limit = min(20, end - start + 1) if end and end >= start else 20
|
||||
if self.message_count > 0 and start > self.message_count:
|
||||
return []
|
||||
res = httpx.get(
|
||||
f"{settings.proxy_url}/api/mails?limit={limit}&offset={start - 1}",
|
||||
# 首先获取服务端邮件总数
|
||||
count_res = httpx.get(
|
||||
f"{settings.proxy_url}{endpoint}?limit=1&offset=0",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.password}",
|
||||
"x-custom-auth": f"{settings.basic_password}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
if res.status_code != 200:
|
||||
if count_res.status_code != 200:
|
||||
_logger.error(
|
||||
"Failed: "
|
||||
f"code=[{res.status_code}] text=[{res.text}]"
|
||||
f"Failed to get {self.name} email count: "
|
||||
f"code=[{count_res.status_code}] text=[{count_res.text}]"
|
||||
)
|
||||
raise Exception("Failed to fetch emails")
|
||||
if res.json()["count"] > 0:
|
||||
self.message_count = res.json()["count"]
|
||||
return [
|
||||
(start + uid, SimpleMessage(start + uid, parse_email(item["raw"])))
|
||||
for uid, item in enumerate(res.json()["results"])
|
||||
]
|
||||
return
|
||||
|
||||
def fetchSENT(self, messages):
|
||||
start, end = messages.ranges[0]
|
||||
start = max(start, 1)
|
||||
limit = min(20, end - start + 1) if end and end >= start else 20
|
||||
if self.message_count > 0 and start > self.message_count:
|
||||
return []
|
||||
res = httpx.get(
|
||||
f"{settings.proxy_url}/api/sendbox?limit={limit}&offset={start - 1}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.password}",
|
||||
"x-custom-auth": f"{settings.basic_password}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
if res.status_code != 200:
|
||||
_logger.error(
|
||||
"Failed: "
|
||||
f"code=[{res.status_code}] text=[{res.text}]"
|
||||
total_count = count_res.json()["count"]
|
||||
self.message_count = total_count
|
||||
|
||||
if total_count == 0 or start > total_count:
|
||||
return
|
||||
|
||||
# 分批处理,每次获取一小批就立即返回
|
||||
batch_size = 20
|
||||
current_start = start
|
||||
current_end = min(end or total_count, total_count)
|
||||
|
||||
while current_start <= current_end:
|
||||
batch_end = min(current_start + batch_size - 1, current_end)
|
||||
|
||||
# 计算这一批的参数
|
||||
limit = batch_end - current_start + 1
|
||||
server_offset = total_count - batch_end
|
||||
server_offset = max(0, server_offset)
|
||||
|
||||
_logger.info(
|
||||
f"Fetching batch: start={current_start}, end={batch_end}, "
|
||||
f"total_count={total_count}, limit={limit}, "
|
||||
f"server_offset={server_offset}"
|
||||
)
|
||||
raise Exception("Failed to fetch emails")
|
||||
if res.json()["count"] > 0:
|
||||
self.message_count = res.json()["count"]
|
||||
return [
|
||||
(start + uid, SimpleMessage(start + uid, generate_email_model(item)))
|
||||
for uid, item in enumerate(reversed(res.json()["results"]))
|
||||
]
|
||||
|
||||
res = httpx.get(
|
||||
f"{settings.proxy_url}{endpoint}?limit={limit}&offset={server_offset}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.password}",
|
||||
"x-custom-auth": f"{settings.basic_password}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
if res.status_code != 200:
|
||||
_logger.error(
|
||||
f"Failed to fetch {self.name} emails: "
|
||||
f"code=[{res.status_code}] text=[{res.text}]"
|
||||
)
|
||||
break
|
||||
|
||||
emails = res.json()["results"]
|
||||
for i, item in enumerate(reversed(emails)):
|
||||
uid = total_count - server_offset - len(emails) + i + 1
|
||||
if current_start <= uid <= batch_end:
|
||||
if self.name == "INBOX":
|
||||
email_model = parse_email(item["raw"])
|
||||
elif self.name == "SENT":
|
||||
email_model = generate_email_model(item)
|
||||
|
||||
# 立即返回这封邮件
|
||||
yield (uid, SimpleMessage(uid, email_model))
|
||||
|
||||
current_start = batch_end + 1
|
||||
|
||||
def getUID(self, message):
|
||||
return message.uid
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
aiosmtpd==1.4.6
|
||||
pydantic-settings==2.2.1
|
||||
requests==2.32.0
|
||||
twisted==24.7.0
|
||||
httpx==0.27.0
|
||||
pydantic-settings==2.9.1
|
||||
requests==2.32.4
|
||||
Twisted==25.5.0
|
||||
httpx==0.28.1
|
||||
|
||||
@@ -2,13 +2,16 @@ import { defineConfig, type DefaultTheme } from 'vitepress'
|
||||
|
||||
export const en = defineConfig({
|
||||
title: "Temp Mail Doc",
|
||||
lang: 'zh-Hans',
|
||||
lang: 'en-US',
|
||||
description: 'CloudFlare Free sending and receiving of temporary domain name mailboxes',
|
||||
|
||||
themeConfig: {
|
||||
outline: 'deep',
|
||||
nav: nav(),
|
||||
|
||||
sidebar: {
|
||||
'/en/guide/': { base: '/en/guide/', items: sidebarGuide() },
|
||||
},
|
||||
|
||||
editLink: {
|
||||
pattern: 'https://github.com/dreamhunter2333/cloudflare_temp_email/edit/main/vitepress-docs/docs/:path',
|
||||
text: 'Edit this page on GitHub'
|
||||
@@ -18,6 +21,31 @@ export const en = defineConfig({
|
||||
message: 'Based on MIT license',
|
||||
copyright: `Copyright © 2023-${new Date().getFullYear()} Dream Hunter`
|
||||
},
|
||||
|
||||
docFooter: {
|
||||
prev: 'Previous',
|
||||
next: 'Next'
|
||||
},
|
||||
|
||||
outline: {
|
||||
level: 'deep',
|
||||
label: 'On this page'
|
||||
},
|
||||
|
||||
lastUpdated: {
|
||||
text: 'Last updated',
|
||||
formatOptions: {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'medium'
|
||||
}
|
||||
},
|
||||
|
||||
langMenuLabel: 'Language',
|
||||
returnToTopLabel: 'Return to top',
|
||||
sidebarMenuLabel: 'Menu',
|
||||
darkModeSwitchLabel: 'Theme',
|
||||
lightModeSwitchTitle: 'Switch to light mode',
|
||||
darkModeSwitchTitle: 'Switch to dark mode'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -29,15 +57,16 @@ function nav(): DefaultTheme.NavItem[] {
|
||||
},
|
||||
{
|
||||
text: 'Guide',
|
||||
link: '/en/cli',
|
||||
link: '/en/guide/quick-start',
|
||||
activeMatch: '/en/guide/'
|
||||
},
|
||||
{
|
||||
text: 'Service Status',
|
||||
link: '/status',
|
||||
link: '/en/status',
|
||||
},
|
||||
{
|
||||
text: 'Reference',
|
||||
link: '/reference',
|
||||
link: '/en/reference',
|
||||
},
|
||||
{
|
||||
text: process.env.TAG_NAME || 'v0.2.2',
|
||||
@@ -54,3 +83,88 @@ function nav(): DefaultTheme.NavItem[] {
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
return [
|
||||
{
|
||||
text: 'Introduction',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'What is Temporary Email', link: 'what-is-temp-mail' },
|
||||
{ text: 'Star History', link: 'star-history' },
|
||||
{ text: 'Quick Start', link: 'quick-start' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Deploy via Command Line',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'CLI Prerequisites', link: 'cli/pre-requisite' },
|
||||
{ text: 'D1 Database', link: 'cli/d1' },
|
||||
{ text: 'Cloudflare Workers Backend', link: 'cli/worker' },
|
||||
{ text: 'Configure Email Routing', link: 'email-routing.md' },
|
||||
{ text: 'Cloudflare Pages Frontend', link: 'cli/pages' },
|
||||
{ text: 'Configure Email Sending', link: 'config-send-mail' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Deploy via User Interface',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'D1 Database', link: 'ui/d1' },
|
||||
{ text: 'Cloudflare Workers Backend', link: 'ui/worker' },
|
||||
{ text: 'Configure Email Routing', link: 'email-routing.md' },
|
||||
{ text: 'Cloudflare Pages Frontend', link: 'ui/pages' },
|
||||
{ text: 'Configure Email Sending', link: 'config-send-mail' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Deploy via Github Actions',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Github Actions Prerequisites', link: 'actions/pre-requisite' },
|
||||
{ text: 'D1 Database', link: 'actions/d1' },
|
||||
{ text: 'Github Actions Configuration', link: 'actions/github-action' },
|
||||
{ text: 'Configure Email Routing', link: 'email-routing.md' },
|
||||
{ text: 'Configure Email Sending', link: 'config-send-mail' },
|
||||
{ text: 'Auto-Update Configuration', link: 'actions/auto-update' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'General',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Worker Variables', link: 'worker-vars' },
|
||||
{ text: 'Common Issues', link: 'common-issues' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Additional Features',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'AI Email Recognition', link: 'feature/ai-extract' },
|
||||
{ text: 'Configure SMTP IMAP Proxy', link: 'feature/config-smtp-proxy' },
|
||||
{ text: 'Send Email API', link: 'feature/send-mail-api' },
|
||||
{ text: 'View Email API', link: 'feature/mail-api' },
|
||||
{ text: 'Configure Subdomain Email', link: 'feature/subdomain' },
|
||||
{ text: 'Configure Telegram Bot', link: 'feature/telegram' },
|
||||
{ text: 'Configure S3 Attachments', link: 'feature/s3-attachment' },
|
||||
{ text: 'Configure WASM Email Parser', link: 'feature/mail_parser_wasm_worker' },
|
||||
{ text: 'Configure Webhook', link: 'feature/webhook' },
|
||||
{ text: 'New Address API', link: 'feature/new-address-api' },
|
||||
{ text: 'OAuth2 Third-party Login', link: 'feature/user-oauth2' },
|
||||
{ text: 'Enhance with Other Workers', link: 'feature/another-worker-enhanced' },
|
||||
{ text: 'Add Google Ads', link: 'feature/google-ads.md' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: 'Feature Overview',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Admin Console', link: 'feature/admin' },
|
||||
{ text: 'Admin User Management', link: 'feature/admin-user-management' },
|
||||
]
|
||||
},
|
||||
{ text: 'Reference', base: "/en/", link: 'reference' }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -142,6 +142,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
text: '附加功能',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'AI 邮件识别', link: 'feature/ai-extract' },
|
||||
{ text: '配置 SMTP IMAP 代理服务', link: 'feature/config-smtp-proxy' },
|
||||
{ text: '发送邮件 API', link: 'feature/send-mail-api' },
|
||||
{ text: '查看邮件 API', link: 'feature/mail-api' },
|
||||
|
||||
10
vitepress-docs/docs/en/guide/actions/auto-update.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# How to Configure Auto-Update for GitHub Actions Deployment
|
||||
|
||||
::: warning Notice
|
||||
If you encounter any issues, please report them via `GitHub Issues`. Thank you.
|
||||
Auto-update will not execute SQL files for the D1 database. When the database schema changes, you need to execute them manually.
|
||||
:::
|
||||
|
||||
1. Open the `Actions` page of the repository, find `Upstream Sync`, and click `enable workflow` to enable the `workflow`
|
||||
2. If `Upstream Sync` fails, go to the repository homepage and click `Sync` to synchronize manually
|
||||
3. You can customize the update interval by modifying the `schedule` configuration in `Upstream Sync`, refer to [cron expressions](https://crontab.guru/)
|
||||
17
vitepress-docs/docs/en/guide/actions/d1.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Initialize/Update D1 Database
|
||||
|
||||
## Create Database
|
||||
|
||||
Open the Cloudflare console, select `Workers & Pages` -> `D1` -> `Create Database`, and click to create the database
|
||||
|
||||

|
||||
|
||||
After creation, you can see the D1 database in the Cloudflare console and obtain the database `name` and `database ID`
|
||||
|
||||
## Initialize Database
|
||||
|
||||
After deployment is complete, go to the admin page's `Quick Setup` -> `Database` section and click the `Initialize Database` button to initialize the database
|
||||
|
||||
## Update Database Schema
|
||||
|
||||
Refer to [Update D1 via Command Line](/en/guide/cli/d1) or [Update D1 via UI](/en/guide/ui/d1)
|
||||
61
vitepress-docs/docs/en/guide/actions/github-action.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Deploy via GitHub Actions
|
||||
|
||||
::: warning Notice
|
||||
Currently only supports Worker and Pages deployment.
|
||||
If you encounter any issues, please report them via `GitHub Issues`. Thank you.
|
||||
|
||||
The `worker.dev` domain is inaccessible in China, please use a custom domain
|
||||
:::
|
||||
|
||||
## Deployment Steps
|
||||
|
||||
### Fork Repository and Enable Actions
|
||||
|
||||
- Fork this repository on GitHub
|
||||
- Open the `Actions` page of the repository
|
||||
- Find `Deploy Backend` and click `enable workflow` to enable the `workflow`
|
||||
- If you need separate frontend and backend deployment, find `Deploy Frontend` and click `enable workflow` to enable the `workflow`
|
||||
|
||||
### Configure Secrets
|
||||
|
||||
Then go to the repository page `Settings` -> `Secrets and variables` -> `Actions` -> `Repository secrets`, and add the following `secrets`:
|
||||
|
||||
- Common `secrets`
|
||||
|
||||
| Name | Description |
|
||||
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare Account ID, [Reference Documentation](https://developers.cloudflare.com/workers/wrangler/ci-cd/#cloudflare-account-id) |
|
||||
| `CLOUDFLARE_API_TOKEN` | Cloudflare API Token, [Reference Documentation](https://developers.cloudflare.com/workers/wrangler/ci-cd/#api-token) |
|
||||
|
||||
- Worker backend `secrets`
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `BACKEND_TOML` | Backend configuration file, [see here](/en/guide/cli/worker.html#modify-wrangler-toml-configuration-file) |
|
||||
| `DEBUG_MODE` | (Optional) Whether to enable debug mode, set to `true` to enable. By default, worker deployment logs are not output to GitHub Actions page, enabling this will output them |
|
||||
| `BACKEND_USE_MAIL_WASM_PARSER` | (Optional) Whether to use WASM to parse emails, set to `true` to enable. For features, refer to [Configure Worker to use WASM Email Parser](/en/guide/feature/mail_parser_wasm_worker) |
|
||||
| `USE_WORKER_ASSETS` | (Optional) Deploy Worker with frontend assets, set to `true` to enable |
|
||||
|
||||
- Pages frontend `secrets`
|
||||
|
||||
> [!warning] Notice
|
||||
> If you choose to deploy Worker with frontend assets, these `secrets` are not required
|
||||
|
||||
| Name | Description |
|
||||
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `FRONTEND_ENV` | Frontend configuration file, please copy the content from `frontend/.env.example`, [and modify according to this guide](/en/guide/cli/pages.html) |
|
||||
| `FRONTEND_NAME` | The project name you created in Cloudflare Pages, can be created via [UI](https://temp-mail-docs.awsl.uk/en/guide/ui/pages.html) or [Command Line](https://temp-mail-docs.awsl.uk/en/guide/cli/pages.html) |
|
||||
| `FRONTEND_BRANCH` | (Optional) Branch for pages deployment, can be left unconfigured, defaults to `production` |
|
||||
| `PAGE_TOML` | (Optional) Required when using page functions to forward backend requests. Please copy the content from `pages/wrangler.toml` and modify the `service` field to your worker backend name according to actual situation |
|
||||
| `TG_FRONTEND_NAME` | (Optional) The project name you created in Cloudflare Pages, same as `FRONTEND_NAME`. Fill this in if you need Telegram Mini App functionality |
|
||||
|
||||
### Deploy
|
||||
|
||||
- Open the `Actions` page of the repository
|
||||
- Find `Deploy Backend` and click `Run workflow` to select a branch and deploy manually
|
||||
- If you need separate frontend and backend deployment, find `Deploy Frontend` and click `Run workflow` to select a branch and deploy manually
|
||||
|
||||
## How to Configure Auto-Update
|
||||
|
||||
1. Open the `Actions` page of the repository, find `Upstream Sync`, and click `enable workflow` to enable the `workflow`
|
||||
2. If `Upstream Sync` fails, go to the repository homepage and click `Sync` to synchronize manually
|
||||
10
vitepress-docs/docs/en/guide/actions/pre-requisite.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# GitHub Actions Deployment Prerequisites
|
||||
|
||||
## GitHub Account
|
||||
|
||||
- A GitHub account is required
|
||||
- A stable network connection
|
||||
|
||||
## Fork Repository
|
||||
|
||||
- Fork [this repository](https://github.com/dreamhunter2333/cloudflare_temp_email.git) on GitHub
|
||||
30
vitepress-docs/docs/en/guide/cli/d1.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Initialize/Update D1 Database
|
||||
|
||||
When executing the wrangler login command for the first time, you will be prompted to log in. Follow the prompts to complete the login process.
|
||||
|
||||
## Initialize Database
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
cp wrangler.toml.template wrangler.toml
|
||||
# Create D1 and execute schema.sql
|
||||
wrangler d1 create dev
|
||||
wrangler d1 execute dev --file=../db/schema.sql --remote
|
||||
```
|
||||
|
||||
After creation, you can see the D1 database in the Cloudflare console.
|
||||
|
||||

|
||||
|
||||
## Update Database Schema
|
||||
|
||||
For `schema` updates, please confirm your previously deployed version.
|
||||
Check the [Changelog](https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/CHANGELOG.md)
|
||||
|
||||
Find the `patch` file you need to execute and run it, for example:
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
wrangler d1 execute dev --file=../db/2024-01-13-patch.sql --remote
|
||||
wrangler d1 execute dev --file=../db/2024-04-03-patch.sql --remote
|
||||
```
|
||||
51
vitepress-docs/docs/en/guide/cli/pages.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Cloudflare Pages Frontend
|
||||
|
||||
> [!warning] Notice
|
||||
> Choose one of the following methods
|
||||
|
||||
## Deploy Worker with Frontend Assets
|
||||
|
||||
Refer to [Deploy Worker](/en/guide/cli/worker#deploy-worker-with-frontend-optional)
|
||||
|
||||
## Separate Frontend and Backend Deployment
|
||||
|
||||
The first deployment will prompt you to create a project. For the `production` branch, enter `production`.
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install
|
||||
cp .env.example .env.prod
|
||||
```
|
||||
|
||||
Modify the `.env.prod` file.
|
||||
|
||||
Change `VITE_API_BASE` to the `worker` `url` created in the previous step. Do not add `/` at the end.
|
||||
|
||||
For example: `VITE_API_BASE=https://xxx.xxx.workers.dev`
|
||||
|
||||
```bash
|
||||
pnpm build --emptyOutDir
|
||||
# The first deployment will prompt you to create a project, for production branch enter production
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
After deployment, you can see your project in the Cloudflare console. You can configure a custom domain for `pages`.
|
||||
|
||||

|
||||
|
||||
## Forward Backend Requests Through Page Functions
|
||||
|
||||
Forwarding requests from page functions to the worker backend can achieve faster response times.
|
||||
|
||||
The first deployment will prompt you to create a project. For the `production` branch, enter `production`.
|
||||
|
||||
If your worker backend name is not `cloudflare_temp_email`, please modify `pages/wrangler.toml`.
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install
|
||||
# If you want to enable Cloudflare Zero Trust, you need to use pnpm build:pages:nopwa to disable caching
|
||||
pnpm build:pages
|
||||
cd ../pages
|
||||
pnpm run deploy
|
||||
```
|
||||
17
vitepress-docs/docs/en/guide/cli/pre-requisite.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Prerequisites
|
||||
|
||||
## Installing wrangler
|
||||
|
||||
Install wrangler
|
||||
|
||||
```bash
|
||||
npm install wrangler -g
|
||||
```
|
||||
|
||||
## Clone the Project
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dreamhunter2333/cloudflare_temp_email.git
|
||||
# Switch to the latest tag or the branch you want to deploy, you can also use the main branch directly
|
||||
# git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
```
|
||||
147
vitepress-docs/docs/en/guide/cli/worker.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Cloudflare Worker Backend
|
||||
|
||||
> [!warning] Notice
|
||||
> The `worker.dev` domain is not accessible in China, please use a custom domain
|
||||
|
||||
## Initialize Project
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
pnpm install
|
||||
cp wrangler.toml.template wrangler.toml
|
||||
```
|
||||
|
||||
## Create KV Cache
|
||||
|
||||
> [!NOTE]
|
||||
> If you want to enable user registration and need to send email verification, you need to create a `KV` cache. You can skip this step if not needed.
|
||||
> If you need Telegram Bot, you need to create a `KV` cache. You can skip this step if not needed.
|
||||
|
||||
Create KV cache through command line, or create it in the Cloudflare console, then copy the corresponding configuration to the `wrangler.toml` file.
|
||||
|
||||
```bash
|
||||
wrangler kv:namespace create DEV
|
||||
```
|
||||
|
||||
## Modify `wrangler.toml` Configuration File
|
||||
|
||||
> [!NOTE] Note
|
||||
> For more variable configurations, please check [Worker Variables Documentation](/en/guide/worker-vars)
|
||||
|
||||
```toml
|
||||
name = "cloudflare_temp_email"
|
||||
main = "src/worker.ts"
|
||||
compatibility_date = "2024-09-23"
|
||||
compatibility_flags = [ "nodejs_compat" ]
|
||||
|
||||
# If you want to use a custom domain, you need to add routes configuration
|
||||
# routes = [
|
||||
# { pattern = "temp-email-api.xxxxx.xyz", custom_domain = true },
|
||||
# ]
|
||||
|
||||
# If you want to deploy a worker with frontend assets, you need to add assets configuration
|
||||
# [assets]
|
||||
# directory = "../frontend/dist/"
|
||||
# binding = "ASSETS"
|
||||
# run_worker_first = true
|
||||
|
||||
# If you want to use scheduled tasks to clean up emails, uncomment the following and modify the cron expression
|
||||
# [triggers]
|
||||
# crons = [ "0 0 * * *" ]
|
||||
|
||||
# Send emails through Cloudflare
|
||||
# send_email = [
|
||||
# { name = "SEND_MAIL" },
|
||||
# ]
|
||||
|
||||
[vars]
|
||||
# Email name prefix, can be configured as an empty string or not configured if no prefix is needed
|
||||
PREFIX = "tmp"
|
||||
# All domains used for temporary email, supports multiple domains
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
|
||||
# Secret key for generating JWT, JWT is used for user login and authentication
|
||||
JWT_SECRET = "xxx"
|
||||
|
||||
# Admin console password, if not configured, console access is not allowed
|
||||
# ADMIN_PASSWORDS = ["123", "456"]
|
||||
|
||||
# Whether to allow users to create emails, not allowed if not configured
|
||||
ENABLE_USER_CREATE_EMAIL = true
|
||||
# Allow users to delete emails, not allowed if not configured
|
||||
ENABLE_USER_DELETE_EMAIL = true
|
||||
|
||||
# D1 database name and ID can be viewed in the Cloudflare console
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "xxx" # D1 database name
|
||||
database_id = "xxx" # D1 database ID
|
||||
|
||||
# KV config for user registration email verification, can be skipped if user registration is not enabled or registration verification is not enabled
|
||||
# [[kv_namespaces]]
|
||||
# binding = "KV"
|
||||
# id = "xxxx"
|
||||
|
||||
# Rate limit configuration for new address /api/new_address
|
||||
# [[unsafe.bindings]]
|
||||
# name = "RATE_LIMITER"
|
||||
# type = "ratelimit"
|
||||
# namespace_id = "1001"
|
||||
# # 10 requests per minute
|
||||
# simple = { limit = 10, period = 60 }
|
||||
|
||||
# Bind other workers to process emails, for example, using auth-inbox AI capabilities to parse verification codes or activation links
|
||||
# [[services]]
|
||||
# binding = "AUTH_INBOX"
|
||||
# service = "auth-inbox"
|
||||
```
|
||||
|
||||
## Deploy Worker with Frontend (Optional)
|
||||
|
||||
> [!NOTE]
|
||||
> If you don't need a [worker with frontend], you can skip this step.
|
||||
> Refer to the frontend deployment documentation later for separate frontend and backend deployment.
|
||||
|
||||
Ensure the frontend assets are built to the `frontend/dist` directory.
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install --no-frozen-lockfile
|
||||
pnpm build:pages
|
||||
```
|
||||
|
||||
Add the following configuration to the `wrangler.toml` file in the `worker` directory.
|
||||
|
||||
```toml
|
||||
[assets]
|
||||
directory = "../frontend/dist/"
|
||||
binding = "ASSETS"
|
||||
run_worker_first = true
|
||||
```
|
||||
|
||||
## Telegram Bot Configuration
|
||||
|
||||
> [!NOTE]
|
||||
> If you don't need Telegram Bot, you can skip this step.
|
||||
|
||||
Please create a Telegram Bot first, then get the `token`, and execute the following command to add the `token` to secrets.
|
||||
|
||||
```bash
|
||||
pnpm wrangler secret put TELEGRAM_BOT_TOKEN
|
||||
```
|
||||
|
||||
## Deploy
|
||||
|
||||
The first deployment will prompt you to create a project. For the `production` branch, enter `production`.
|
||||
|
||||
```bash
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
After successful deployment, you can see the `worker` `url` in the routes, and the console will also output the `worker` `url`.
|
||||
|
||||

|
||||
|
||||
> [!NOTE]
|
||||
> Open the `worker` `url`, if it displays `OK`, the deployment is successful.
|
||||
>
|
||||
> Open `/health_check`, if it displays `OK`, the deployment is successful.
|
||||
41
vitepress-docs/docs/en/guide/common-issues.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Common Issues
|
||||
|
||||
> [!NOTE] Note
|
||||
> If you don't find a solution here, please search or ask in `Github Issues`, or ask in the Telegram group.
|
||||
|
||||
## General
|
||||
|
||||
| Issue | Solution |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
|
||||
| Sending emails to authenticated forwarding addresses using Cloudflare Workers | Use CF's API for sending, only supports recipient addresses bound to CF, i.e., CF EMAIL forwarding destination addresses |
|
||||
| Binding multiple domains | Each domain needs to configure email forwarding to worker |
|
||||
|
||||
## Worker Related
|
||||
|
||||
| Issue | Solution |
|
||||
| ------------------------------------------------------------------ | --------------------------------------------------------------------------- |
|
||||
| `Uncaught Error: No such module "path". imported from "worker.js"` | [Reference](/en/guide/ui/worker) |
|
||||
| `No such module "node:stream". imported from "worker.js"` | [Reference](/en/guide/ui/worker) |
|
||||
| `Subdomain cannot send emails` | [Reference](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/515) |
|
||||
| `Failed to send verify code: No balance` | Set unlimited emails in admin console or increase quota on the sending permission page |
|
||||
| `Github OAuth unable to get email 400 Failed to get user email` | GitHub user needs to set email to public |
|
||||
| `Cannot read properties of undefined (reading 'map')` | Worker variables not set successfully |
|
||||
|
||||
## Pages Related
|
||||
|
||||
| Issue | Solution |
|
||||
| --------------- | --------------------------------------------------------- |
|
||||
| `network error` | Use incognito mode or clear browser cache and DNS cache |
|
||||
|
||||
## Telegram Bot
|
||||
|
||||
| Issue | Solution |
|
||||
| -------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| `Telgram Bot failed to get email: 400: Bad Request:BUTTON_URL_INVALID` | tg mini app URL is incorrect, should be the pages URL |
|
||||
| `Telegram bot bind error: bind adress count reach the limit` | Need to set worker variable `TG_MAX_ADDRESS` |
|
||||
|
||||
## Github Actions
|
||||
|
||||
| Issue | Solution |
|
||||
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| After Github Action deployment, CF always shows preview branch | Go to CF pages settings to confirm that the frontend branch matches the Github Action frontend deployment branch |
|
||||
84
vitepress-docs/docs/en/guide/config-send-mail.md
Normal file
@@ -0,0 +1,84 @@
|
||||
|
||||
# Configure Email Sending
|
||||
|
||||
::: warning Note
|
||||
All three methods can be configured simultaneously. When sending emails, it will prioritize using `resend`, if `resend` is not configured, it will use `smtp`.
|
||||
|
||||
If a Cloudflare authenticated forwarding email address is configured, CF's internal API will be prioritized for sending emails
|
||||
:::
|
||||
|
||||
## Send Emails Using Resend
|
||||
|
||||
Register at `https://resend.com/domains` and add DNS records according to the instructions.
|
||||
|
||||
Create an `api key` on the `API KEYS` page.
|
||||
|
||||
Then execute the following command to add `RESEND_TOKEN` to secrets:
|
||||
|
||||
> [!NOTE]
|
||||
> If you find this troublesome, you can also put it directly in plain text under `[vars]` in `wrangler.toml`, but this is not recommended
|
||||
|
||||
If you deployed through the UI, you can add it under `Variables and Secrets` in the Cloudflare UI interface.
|
||||
|
||||
```bash
|
||||
# Switch to worker directory
|
||||
cd worker
|
||||
wrangler secret put RESEND_TOKEN
|
||||
```
|
||||
|
||||
If you have multiple domains with different `api keys`, you can add multiple secrets in `wrangler.toml`, named `RESEND_TOKEN_` + `<UPPERCASE DOMAIN WITH . REPLACED BY _>`, for example:
|
||||
|
||||
```bash
|
||||
wrangler secret put RESEND_TOKEN_XXX_COM
|
||||
wrangler secret put RESEND_TOKEN_DREAMHUNTER2333_XYZ
|
||||
```
|
||||
|
||||
## Send Emails Using SMTP
|
||||
|
||||
The format of `SMTP_CONFIG` is as follows, where key is the domain name and value is the SMTP configuration. For SMTP configuration format details, refer to [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md)
|
||||
|
||||
```json
|
||||
{
|
||||
"awsl.uk": {
|
||||
"host": "smtp.xxx.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"authType": [
|
||||
"plain",
|
||||
"login"
|
||||
],
|
||||
"credentials": {
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then execute the following command to add `SMTP_CONFIG` to secrets:
|
||||
|
||||
> [!NOTE]
|
||||
> If you find this troublesome, you can also put it directly in plain text under `[vars]` in `wrangler.toml`, but this is not recommended
|
||||
|
||||
If you deployed through the UI, you can add it under `Variables and Secrets` in the Cloudflare UI interface.
|
||||
|
||||
```bash
|
||||
# Switch to worker directory
|
||||
cd worker
|
||||
wrangler secret put SMTP_CONFIG
|
||||
```
|
||||
|
||||
## Send Emails to Authenticated Forwarding Addresses on Cloudflare
|
||||
|
||||
Only supported for CLI deployment, add `send_email` configuration in `wrangler.toml`.
|
||||
|
||||
The destination email address must be an authenticated email address on Cloudflare, which has significant limitations. If you need to send emails to other addresses, you can use `resend` or `smtp` to send emails.
|
||||
|
||||
```toml
|
||||
# Send emails through Cloudflare
|
||||
send_email = [
|
||||
{ name = "SEND_MAIL" },
|
||||
]
|
||||
```
|
||||
|
||||
Admin console account configuration `Verified address list (can send emails through CF internal API)`
|
||||
9
vitepress-docs/docs/en/guide/email-routing.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Cloudflare Email Routing
|
||||
|
||||
1. In the CF console for the corresponding domain under `Email Routing`, configure the `Email DNS records`. If there are multiple domains, you need to configure `Email DNS records` for each domain.
|
||||
|
||||
2. Before binding an email address to your Worker, you need to enable email routing and have at least one verified email address (destination address).
|
||||
|
||||
3. Configure the `Catch-all address` in the routing rules of each domain's `Email Routing` to send to `worker`.
|
||||
|
||||

|
||||
@@ -0,0 +1,11 @@
|
||||
# Admin User Management
|
||||
|
||||
## User Management Page
|
||||
|
||||

|
||||
|
||||
## User Settings
|
||||
|
||||
Configure user login and authentication settings here
|
||||
|
||||

|
||||
15
vitepress-docs/docs/en/guide/feature/admin.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Admin Console
|
||||
|
||||
> [!NOTE]
|
||||
> You need to configure `ADMIN_PASSWORDS` or `ADMIN_USER_ROLE` to access the admin console
|
||||
> Admin role configuration: if the user role equals ADMIN_USER_ROLE, they can access the admin console
|
||||
|
||||
After deploying the frontend application, click the upper-left logo 5 times or visit the `/admin` path to enter the management console.
|
||||
|
||||
You need to configure `ADMIN_PASSWORDS` in the backend or ensure the current user role is `ADMIN_USER_ROLE`, otherwise access to the console will be denied.
|
||||
|
||||

|
||||
|
||||
## If your website is for private access only, you can disable this check
|
||||
|
||||
`DISABLE_ADMIN_PASSWORD_CHECK = true`
|
||||
70
vitepress-docs/docs/en/guide/feature/ai-extract.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# AI Email Recognition
|
||||
|
||||
> [!NOTE]
|
||||
> This feature is supported from version v1.1.0
|
||||
>
|
||||
> This feature is inspired by the [Alle project](https://github.com/bestruirui/Alle/blob/62e74629ded0c7966c12d4e1c54f0bcc2e54f12c/src/lib/email/extract.ts#L54)
|
||||
|
||||
## Features
|
||||
|
||||
The AI email recognition feature uses Cloudflare Workers AI to automatically analyze incoming email content and intelligently extract important information, including:
|
||||
|
||||
- **Verification Code** (auth_code) - OTP, security code, confirmation code, etc.
|
||||
- **Authentication Link** (auth_link) - Login, verify, activate, password reset links
|
||||
- **Service Link** (service_link) - GitHub, GitLab, deployment notifications and other service-related links
|
||||
- **Subscription Link** (subscription_link) - Unsubscribe, manage subscription links
|
||||
- **Other Link** (other_link) - Other valuable links
|
||||
|
||||
Extraction results are automatically saved to the `metadata` field in the database, and the frontend can directly display extracted verification codes or links.
|
||||
|
||||
## Configuration Variables
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
| -------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- |
|
||||
| `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` |
|
||||
|
||||
## Workers AI Binding
|
||||
|
||||
Configure Workers AI binding in `wrangler.toml`:
|
||||
|
||||
```toml
|
||||
[ai]
|
||||
binding = "AI"
|
||||
```
|
||||
|
||||
Or add in Cloudflare Dashboard Worker settings:
|
||||
- **Variable name**: `AI`
|
||||
- **Type**: Workers AI
|
||||
|
||||
## Address Allowlist (Optional)
|
||||
|
||||
To control costs and resource usage, you can configure an address allowlist in the Admin console's **AI Extract Settings** page:
|
||||
|
||||
### Configuration
|
||||
|
||||
- **Allowlist Disabled**: AI extraction will process all email addresses
|
||||
- **Allowlist Enabled**: AI extraction will only process addresses in the allowlist
|
||||
|
||||
### Allowlist Format
|
||||
|
||||
One address per line, supporting wildcard `*` to match any characters:
|
||||
|
||||
- **Exact Match**: `user@example.com` - Only matches this specific email
|
||||
- **Domain Wildcard**: `*@example.com` - Matches all emails under example.com domain
|
||||
- **User Wildcard**: `admin*@example.com` - Matches emails starting with admin
|
||||
- **Wildcard Anywhere**: `*test*@example.com` - Matches emails containing test
|
||||
- **Multiple Wildcards**: `admin*@*.com` - Matches emails starting with admin under any .com domain
|
||||
|
||||
### Configuration Example
|
||||
|
||||
```text
|
||||
user@example.com
|
||||
*@mydomain.com
|
||||
admin*@company.com
|
||||
```
|
||||
|
||||
This configuration will only perform AI extraction for:
|
||||
- `user@example.com` (exact match)
|
||||
- All emails under `@mydomain.com` (e.g., `test@mydomain.com`, `admin@mydomain.com`)
|
||||
- All emails starting with `admin` under `@company.com` (e.g., `admin@company.com`, `admin123@company.com`)
|
||||
144
vitepress-docs/docs/en/guide/feature/another-worker-enhanced.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Enhancement via Another Worker
|
||||
|
||||
> The core capability of temporary email is email management. Other workers can enhance temporary email functionality, for example, auth-inbox AI can parse verification codes or activation links
|
||||
> This feature only triggers other workers and executes after webhook
|
||||
> [!NOTE]
|
||||
> If you want to use worker enhancement, please create a worker that can be called via RPC in advance, details below
|
||||
> References:
|
||||
> - https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/
|
||||
> - https://developers.cloudflare.com/workers/runtime-apis/rpc/
|
||||
> - auth-inbox project: https://github.com/TooonyChen/AuthInbox
|
||||
|
||||
## Create Another Worker (using auth-inbox AI verification code parsing as an example)
|
||||
|
||||
### Transform Worker to Extend WorkerEntrypoint
|
||||
|
||||
A simple worker code that acts as a callee providing RPC method calls is as follows (the rpcEmail method is an example)
|
||||
(Using the already modified project https://github.com/oneisall8955/AuthInbox-fork)
|
||||
|
||||
src/index.ts file
|
||||
```js
|
||||
import { WorkerEntrypoint } from "cloudflare:workers";
|
||||
|
||||
interface Env {
|
||||
DB: D1Database;
|
||||
// ...
|
||||
}
|
||||
|
||||
export default class extends WorkerEntrypoint<Env> {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
console.log("Original fetch interface parameter is request,env,ctx");
|
||||
console.log("After modifying to WorkerEntrypoint style, there's only one parameter request, getting environment variables and context has slight changes");
|
||||
// Environment variable and context changes see:
|
||||
// https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#bindings-env
|
||||
// https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#lifecycle-methods-ctx
|
||||
const env: Env = this.env;
|
||||
const ctx: ExecutionContext = this.ctx;
|
||||
console.log("Subsequent logic remains unchanged");
|
||||
return new Response('ok', { status: 200 });
|
||||
}
|
||||
|
||||
// Main functionality
|
||||
async email(message: ForwardableEmailMessage): Promise<void> {
|
||||
console.log("Original fetch interface parameter is message,env,ctx");
|
||||
console.log("After modifying to WorkerEntrypoint style, there's only one parameter message, getting environment variables and context is the same as fetch method");
|
||||
const env: Env = this.env;
|
||||
const ctx: ExecutionContext = this.ctx;
|
||||
console.log("After receiving email routing request, subsequent logic remains unchanged");
|
||||
}
|
||||
|
||||
// Expose RPC interface to handle email requests from other workers
|
||||
async rpcEmail(requestBody: string): Promise<void> {
|
||||
console.log(`Received request from another worker (temporary email service cloudflare_temp_email), request body: ${requestBody}`);
|
||||
// requestBody is in JSON format, sent by temporary email service, format as follows
|
||||
// type RPCEmailMessage = {
|
||||
// from: string | undefined | null,
|
||||
// to: string | undefined | null,
|
||||
// rawEmail: string | undefined | null,
|
||||
// headers: Map<string, string>,
|
||||
// }
|
||||
// ... todo ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Deploy Another Worker
|
||||
|
||||
After modification, or using auth-inbox as an example, deploy to Cloudflare Worker. See https://github.com/TooonyChen/AuthInbox, or use the already modified project https://github.com/oneisall8955/AuthInbox-fork
|
||||
|
||||
## Configure Temporary Email Service to Use Specified Worker Enhancement
|
||||
|
||||
## Bind Service
|
||||
|
||||
### Configure via wrangler.toml
|
||||
|
||||
```toml
|
||||
[[services]]
|
||||
binding = "AUTH_INBOX"
|
||||
service = "auth-inbox"
|
||||
```
|
||||
|
||||
Here `binding = "AUTH_INBOX"` can be customized to any string, `service = "auth-inbox"` is the name of the deployed worker that provides RPC interface calls.
|
||||
|
||||
### User Interface Configuration
|
||||
|
||||
In Settings - Bindings, add binding, select binding service.
|
||||
Fill in the variable name with a custom name, can be any string, for example `AUTH_INBOX`.
|
||||
Select the service created in the previous step for service binding, for example `auth-inbox`.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Environment Variable Configuration
|
||||
|
||||
### Configure via wrangler.toml
|
||||
|
||||
```toml
|
||||
ENABLE_ANOTHER_WORKER = true
|
||||
ANOTHER_WORKER_LIST ="""
|
||||
[
|
||||
{
|
||||
"binding":"AUTH_INBOX",
|
||||
"method":"rpcEmail",
|
||||
"keywords":[
|
||||
"验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认",
|
||||
"account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm"
|
||||
]
|
||||
}
|
||||
]
|
||||
"""
|
||||
```
|
||||
|
||||
Environment variable explanation:
|
||||
- ENABLE_ANOTHER_WORKER = true: Default is false, set to true to enable other workers to process emails
|
||||
- ANOTHER_WORKER_LIST is a JSON array, each object contains 3 fields
|
||||
- binding: *Required, must match the binding = "XXX" specified in the services section*, in the example it's AUTH_INBOX
|
||||
- method: Optional, default is rpcEmail, refers to which RPC method of this worker to call for processing
|
||||
- keywords: Keyword array, case-insensitive. Used for filtering, if the *parsed email text* matches these keywords, this worker is triggered and the worker's `method` method is called
|
||||
|
||||
### User Interface Configuration
|
||||
|
||||
In Settings - Environment Variables, add environment variables
|
||||
- ENABLE_ANOTHER_WORKER = true
|
||||
- ANOTHER_WORKER_LIST is the JSON array string mentioned above, no further explanation needed, see above for detailed description
|
||||
```json
|
||||
[
|
||||
{
|
||||
"binding":"AUTH_INBOX",
|
||||
"method":"rpcEmail",
|
||||
"keywords":[
|
||||
"验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认",
|
||||
"account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm"
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Testing
|
||||
|
||||
Send an email to the temporary mailbox, observe the worker logs, or check the verification code on the panel provided by auth-inbox
|
||||
|
||||

|
||||
60
vitepress-docs/docs/en/guide/feature/config-smtp-proxy.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Setting Up SMTP IMAP Proxy Service
|
||||
|
||||
::: warning Notice
|
||||
If you are using `resend`, you can directly use `resend`'s `SMTP` service without needing this service
|
||||
:::
|
||||
|
||||
## Why Do You Need SMTP IMAP Proxy Service
|
||||
|
||||
`SMTP` and `IMAP` have a wider range of application scenarios
|
||||
|
||||
## How to Set Up SMTP IMAP Proxy Service
|
||||
|
||||
### Local Run
|
||||
|
||||
```bash
|
||||
cd smtp_proxy_server/
|
||||
# Copy configuration file and modify it
|
||||
# Your worker address, proxy_url=https://temp-email-api.xxx.xxx
|
||||
# Your SMTP service port, port=8025
|
||||
cp .env.example .env
|
||||
python3 -m venv venv
|
||||
./venv/bin/python3 -m pip install -r requirements.txt
|
||||
./venv/bin/python3 main.py
|
||||
```
|
||||
|
||||
### Docker Run
|
||||
|
||||
```bash
|
||||
cd smtp_proxy_server/
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Modify the environment variables in docker-compose.yaml, note to choose the appropriate `tag`
|
||||
|
||||
`proxy_url` is the URL address of the `worker`
|
||||
|
||||
```yaml
|
||||
services:
|
||||
smtp_proxy_server:
|
||||
image: ghcr.io/dreamhunter2333/cloudflare_temp_email/smtp_proxy_server:latest
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: dockerfile
|
||||
container_name: "smtp_proxy_server"
|
||||
ports:
|
||||
- "8025:8025"
|
||||
- "11143:11143"
|
||||
environment:
|
||||
- proxy_url=https://temp-email-api.xxx.xxx
|
||||
- port=8025
|
||||
- imap_port=11143
|
||||
```
|
||||
|
||||
## Using Thunderbird to Login
|
||||
|
||||
Download [Thunderbird](https://www.thunderbird.net/en-US/)
|
||||
|
||||
For password, enter the `email address credential`
|
||||
|
||||

|
||||
29
vitepress-docs/docs/en/guide/feature/google-ads.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Adding Google Ads to Your Website
|
||||
|
||||
## Command Line Deployment
|
||||
|
||||
Modify the `.env.prod` file
|
||||
|
||||
Add the following two variables, refer to [Google AdSense](https://www.google.com/adsense/start/) for specific values
|
||||
|
||||
```txt
|
||||
VITE_GOOGLE_AD_CLIENT=ca-pub-123456
|
||||
VITE_GOOGLE_AD_SLOT=123456
|
||||
```
|
||||
|
||||
Then execute the following commands to redeploy pages.
|
||||
|
||||
```bash
|
||||
pnpm build --emptyOutDir
|
||||
# For first deployment, you'll be prompted to create a project, fill in production for the production branch
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
## GitHub Action Deployment
|
||||
|
||||
Modify `FRONTEND_ENV`, add the following two variables, refer to [Google AdSense](https://www.google.com/adsense/start/) for specific values, then redeploy pages.
|
||||
|
||||
```txt
|
||||
VITE_GOOGLE_AD_CLIENT=ca-pub-123456
|
||||
VITE_GOOGLE_AD_SLOT=123456
|
||||
```
|
||||
66
vitepress-docs/docs/en/guide/feature/mail-api.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Mail API
|
||||
|
||||
## Viewing Emails via Mail API
|
||||
|
||||
This is a `python` example using the `requests` library to view emails.
|
||||
|
||||
```python
|
||||
limit = 10
|
||||
offset = 0
|
||||
res = requests.get(
|
||||
f"https://<your-worker-address>/api/mails?limit={limit}&offset={offset}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {your-JWT-password}",
|
||||
# "x-custom-auth": "<your-website-password>", # If custom password is enabled
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Admin Mail API
|
||||
|
||||
Supports `address` filter and `keyword` filter
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
url = "https://<your-worker-address>/admin/mails"
|
||||
|
||||
querystring = {
|
||||
"limit":"20",
|
||||
"offset":"0",
|
||||
# address and keyword are optional parameters
|
||||
"address":"xxxx@awsl.uk",
|
||||
"keyword":"xxxx"
|
||||
}
|
||||
|
||||
headers = {"x-admin-auth": "<your-Admin-password>"}
|
||||
|
||||
response = requests.get(url, headers=headers, params=querystring)
|
||||
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
## User Mail API
|
||||
|
||||
Supports `address` filter and `keyword` filter
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
headers = {"x-admin-auth": "<your-Admin-password>"}
|
||||
|
||||
response = requests.get(url, headers=headers, params=querystring)
|
||||
|
||||
print(response.json())
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
# mail-parser-wasm-worker
|
||||
|
||||
> [!NOTE]
|
||||
> If you are using webhook forwarding or telegram bot to receive emails, but the email content is garbled or cannot be parsed, and you have higher requirements for parsing, you can use this feature.
|
||||
|
||||
## UI Deployment
|
||||
|
||||
1. Download [worker-with-wasm-mail-parser.zip](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/worker-with-wasm-mail-parser.zip)
|
||||
|
||||
2. Go back to `Overview`, find the worker you just created, click `Edit Code`, delete the original files, upload `worker.js` and files with `wasm` extension, click `Deploy`
|
||||
|
||||
> [!NOTE]
|
||||
> To upload, first click Explorer in the left menu,
|
||||
> Right-click in the file list window and find `Upload` in the context menu,
|
||||
> Please refer to the screenshot below
|
||||
>
|
||||
> Reference: [issues156](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/156#issuecomment-2079453822)
|
||||
|
||||

|
||||

|
||||
|
||||
## CLI Deployment
|
||||
|
||||
### Modify Code
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
pnpm add mail-parser-wasm-worker
|
||||
```
|
||||
|
||||
Edit `worker/src/common.ts`, uncomment this code to use mail-parser-wasm-worker to parse emails
|
||||
|
||||
```ts
|
||||
export const commonParseMail = async (raw_mail: string | undefined | null): Promise<{
|
||||
sender: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string
|
||||
} | undefined> => {
|
||||
if (!raw_mail) {
|
||||
return undefined;
|
||||
}
|
||||
// Uncomment this code to use mail-parser-wasm-worker to parse emails start
|
||||
// TODO: WASM parse email
|
||||
try {
|
||||
const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
|
||||
|
||||
const parsedEmail = parse_message_wrapper(raw_mail);
|
||||
return {
|
||||
sender: parsedEmail.sender || "",
|
||||
subject: parsedEmail.subject || "",
|
||||
text: parsedEmail.text || "",
|
||||
headers: parsedEmail.headers || [],
|
||||
html: parsedEmail.body_html || "",
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Failed use mail-parser-wasm-worker to parse email", e);
|
||||
}
|
||||
// Uncomment this code to use mail-parser-wasm-worker to parse emails end
|
||||
try {
|
||||
const { default: PostalMime } = await import('postal-mime');
|
||||
const parsedEmail = await PostalMime.parse(raw_mail);
|
||||
return {
|
||||
sender: parsedEmail.from ? `${parsedEmail.from.name} <${parsedEmail.from.address}>` : "",
|
||||
subject: parsedEmail.subject || "",
|
||||
text: parsedEmail.text || "",
|
||||
html: parsedEmail.html || "",
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Failed use PostalMime to parse email", e);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
```
|
||||
|
||||
### Deploy
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
pnpm run deploy
|
||||
```
|
||||
92
vitepress-docs/docs/en/guide/feature/new-address-api.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Create New Email Address API
|
||||
|
||||
## Create Email Address via Admin API
|
||||
|
||||
This is a `python` example using the `requests` library to send emails.
|
||||
|
||||
```python
|
||||
res = requests.post(
|
||||
# Replace xxxx.xxxx with your worker domain
|
||||
"https://xxxx.xxxx/admin/new_address",
|
||||
json={
|
||||
# Enable prefix (True/False)
|
||||
"enablePrefix": True,
|
||||
"name": "<email_name>",
|
||||
"domain": "<email_domain>",
|
||||
},
|
||||
headers={
|
||||
'x-admin-auth': "<your_website_admin_password>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
# Returns {"jwt": "<Jwt>"}
|
||||
print(res.json())
|
||||
```
|
||||
|
||||
## Batch Create Random Username Email Addresses API Example
|
||||
|
||||
### Batch Create Email Addresses via Admin API
|
||||
|
||||
This is a `python` example using the `requests` library to send emails.
|
||||
|
||||
```python
|
||||
import requests
|
||||
import random
|
||||
import string
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
|
||||
def generate_random_name():
|
||||
# Generate 5 lowercase letters
|
||||
letters1 = ''.join(random.choices(string.ascii_lowercase, k=5))
|
||||
# Generate 1-3 digits
|
||||
numbers = ''.join(random.choices(string.digits, k=random.randint(1, 3)))
|
||||
# Generate 1-3 lowercase letters
|
||||
letters2 = ''.join(random.choices(string.ascii_lowercase, k=random.randint(1, 3)))
|
||||
# Combine into final name
|
||||
return letters1 + numbers + letters2
|
||||
|
||||
|
||||
def fetch_email_data(name):
|
||||
try:
|
||||
res = requests.post(
|
||||
"https://<worker_domain>/admin/new_address",
|
||||
json={
|
||||
"enablePrefix": True,
|
||||
"name": name,
|
||||
"domain": "<email_domain>",
|
||||
},
|
||||
headers={
|
||||
'x-admin-auth': "<your_website_admin_password>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
if res.status_code == 200:
|
||||
response_data = res.json()
|
||||
email = response_data.get("address", "no address")
|
||||
jwt = response_data.get("jwt", "no jwt")
|
||||
return f"{email}----{jwt}\n"
|
||||
else:
|
||||
print(f"Request failed, status code: {res.status_code}")
|
||||
return None
|
||||
except requests.RequestException as e:
|
||||
print(f"Request error: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def generate_and_save_emails(num_emails):
|
||||
with ThreadPoolExecutor(max_workers=30) as executor, open('email.txt', 'a') as file:
|
||||
futures = [executor.submit(fetch_email_data, generate_random_name()) for _ in range(num_emails)]
|
||||
|
||||
for future in as_completed(futures):
|
||||
result = future.result()
|
||||
if result:
|
||||
file.write(result)
|
||||
|
||||
|
||||
# Generate 10 emails and append to existing file
|
||||
generate_and_save_emails(10)
|
||||
|
||||
```
|
||||
34
vitepress-docs/docs/en/guide/feature/s3-attachment.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Configure S3 Attachments
|
||||
|
||||
## Configuration
|
||||
|
||||
> [!NOTE]
|
||||
> If you don't need S3 attachments, you can skip this step
|
||||
|
||||
Create an R2 bucket in Cloudflare. You can also use other S3 services (please submit an issue if you encounter bugs).
|
||||
|
||||
Reference: [Configure Cloudflare R2 cors](https://developers.cloudflare.com/r2/buckets/cors/#add-cors-policies-from-the-dashboard)
|
||||
|
||||
Reference: [Cloudflare R2 s3 token](https://developers.cloudflare.com/r2/api/s3/tokens/) to create a token, obtain `ENDPOINT`, `Access Key ID` and `Secret Access Key`, then execute the following commands to add them to secrets
|
||||
|
||||
> [!NOTE]
|
||||
> You can also add `secrets` in the Cloudflare worker UI interface
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
pnpm wrangler secret put S3_ENDPOINT
|
||||
pnpm wrangler secret put S3_ACCESS_KEY_ID
|
||||
pnpm wrangler secret put S3_SECRET_ACCESS_KEY
|
||||
# Note: Replace bucket with your bucket name
|
||||
pnpm wrangler secret put S3_BUCKET
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Save attachment
|
||||
|
||||

|
||||
|
||||
Download attachment
|
||||
|
||||

|
||||
67
vitepress-docs/docs/en/guide/feature/send-mail-api.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Send Email API
|
||||
|
||||
## Send Email via HTTP API
|
||||
|
||||
This is a `python` example using the `requests` library to send emails.
|
||||
|
||||
```python
|
||||
send_body = {
|
||||
"from_name": "Sender Name",
|
||||
"to_name": "Recipient Name",
|
||||
"to_mail": "Recipient Address",
|
||||
"subject": "Email Subject",
|
||||
"is_html": False, # Set whether it's HTML based on content
|
||||
"content": "<Email content: html or text>",
|
||||
}
|
||||
|
||||
res = requests.post(
|
||||
"http://localhost:8787/api/send_mail",
|
||||
json=send_body, headers={
|
||||
"Authorization": f"Bearer {your_JWT_password}",
|
||||
# "x-custom-auth": "<your_website_password>", # If custom password is enabled
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
# Using body authentication
|
||||
send_body = {
|
||||
"token": "<your_JWT_password>",
|
||||
"from_name": "Sender Name",
|
||||
"to_name": "Recipient Name",
|
||||
"to_mail": "Recipient Address",
|
||||
"subject": "Email Subject",
|
||||
"is_html": False, # Set whether it's HTML based on content
|
||||
"content": "<Email content: html or text>",
|
||||
}
|
||||
res = requests.post(
|
||||
"http://localhost:8787/external/api/send_mail",
|
||||
json=send_body, headers={
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Send Email via SMTP
|
||||
|
||||
Please first refer to [Configure SMTP Proxy](/en/guide/feature/config-smtp-proxy.html).
|
||||
|
||||
This is a `python` example using the `smtplib` library to send emails.
|
||||
|
||||
`JWT Token Password`: This is the email login password, which can be viewed in the password menu in the UI interface.
|
||||
|
||||
```python
|
||||
import smtplib
|
||||
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
|
||||
|
||||
with smtplib.SMTP('localhost', 8025) as smtp:
|
||||
smtp.login("jwt", "Enter your JWT token password here")
|
||||
message = MIMEMultipart()
|
||||
message['From'] = "Me <me@awsl.uk>"
|
||||
message['To'] = "Admin <admin@awsl.uk>"
|
||||
message['Subject'] = "Test Subject"
|
||||
message.attach(MIMEText("Test Content", 'html'))
|
||||
smtp.sendmail("me@awsl.uk", "admin@awsl.uk", message.as_string())
|
||||
```
|
||||
11
vitepress-docs/docs/en/guide/feature/subdomain.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Configure Subdomain Email
|
||||
|
||||
::: warning Note
|
||||
Subdomain emails may not be able to send emails. It is recommended to use main domain emails for sending and subdomain emails only for receiving.
|
||||
|
||||
Mail channel is no longer supported. The reference below is limited to the receiving part only.
|
||||
:::
|
||||
|
||||
Reference
|
||||
|
||||
- [Configure Subdomain Email](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/164#issuecomment-2082612710)
|
||||
66
vitepress-docs/docs/en/guide/feature/telegram.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Configure Telegram Bot
|
||||
|
||||
Try it here: [@cf_temp_mail_bot](https://t.me/cf_temp_mail_bot)
|
||||
|
||||
::: warning Note
|
||||
The default `worker.dev` domain certificate for worker is not supported by Telegram. Please use a custom domain when configuring Telegram Bot.
|
||||
:::
|
||||
|
||||
> [!NOTE]
|
||||
> If you want to use Telegram Bot, please bind `KV` first
|
||||
>
|
||||
> If you don't need Telegram Bot, you can skip this step
|
||||
>
|
||||
> If you want Telegram to have stronger email parsing capabilities, refer to [Configure worker to use wasm for email parsing](/en/guide/feature/mail_parser_wasm_worker)
|
||||
|
||||
## Telegram Bot Configuration
|
||||
|
||||
Please first create a Telegram Bot, obtain the `token`, then execute the following command to add the `token` to secrets
|
||||
|
||||
> [!NOTE]
|
||||
> If you find it troublesome, you can also put it in plain text under `[vars]` in `wrangler.toml`, but this is not recommended
|
||||
|
||||
If you deployed via UI, you can add it under `Variables and Secrets` in the Cloudflare UI interface
|
||||
|
||||
```bash
|
||||
# Switch to worker directory
|
||||
cd worker
|
||||
pnpm wrangler secret put TELEGRAM_BOT_TOKEN
|
||||
```
|
||||
|
||||
## Bot
|
||||
|
||||
- Can set whitelist users
|
||||
- Click `Initialize` to complete the configuration.
|
||||
- Click `View Status` to check the current configuration status.
|
||||
|
||||

|
||||
|
||||
## Mini App
|
||||
|
||||
Can be deployed via command line or UI interface
|
||||
|
||||
### UI Deployment
|
||||
|
||||
For other steps, refer to `Frontend and Backend Separation Deployment` in [UI Deployment](/en/guide/cli/pages)
|
||||
|
||||
> [!NOTE]
|
||||
> Download the zip from here, [telegram-frontend.zip](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/telegram-frontend.zip)
|
||||
>
|
||||
> Modify the index-xxx.js file in the zip, where xx is a random string
|
||||
>
|
||||
> Search for `https://temp-email-api.xxx.xxx`, replace it with your worker domain, then deploy the new zip file
|
||||
|
||||
### Command Line Deployment
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install
|
||||
cp .env.example .env.prod
|
||||
# --project-name can create a separate pages for mini app, you can also share one pages, but may encounter js loading issues
|
||||
pnpm run deploy:telegram --project-name=<your_project_name>
|
||||
```
|
||||
|
||||
- After deployment, please fill in the web URL in the `Settings` -> `Telegram Mini App` page `Telegram Mini App URL` in the admin backend.
|
||||
- Please execute `/setmenubutton` in `@BotFather`, then enter your web address to set the `Open App` button in the lower left corner.
|
||||
- Please execute `/newapp` in `@BotFather` to create a new app and register the mini app.
|
||||
26
vitepress-docs/docs/en/guide/feature/user-oauth2.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# OAuth2 Third-Party Login
|
||||
|
||||
> [!WARNING] Note
|
||||
> Third-party login will automatically register an account using the user's email (emails with the same address will be considered the same account)
|
||||
>
|
||||
> This account is the same as a registered account and can also set a password through the forgot password feature
|
||||
|
||||
## Register OAuth2 on Third-Party Platforms
|
||||
|
||||
### GitHub
|
||||
|
||||
- Please first create an OAuth App, then obtain the `Client ID` and `Client Secret`
|
||||
|
||||
Reference: [Creating an OAuth App](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)
|
||||
|
||||
### Authentik
|
||||
|
||||
- [Authentik OAuth2 Provider](https://docs.goauthentik.io/docs/providers/oauth2/)
|
||||
|
||||
## Configure OAuth2 in Admin Backend
|
||||
|
||||

|
||||
|
||||
## Test User Login Page
|
||||
|
||||

|
||||
44
vitepress-docs/docs/en/guide/feature/webhook.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Configure Webhook
|
||||
|
||||
> [!NOTE]
|
||||
> If you want to use webhook, please bind `KV` first and configure the `worker` variable `ENABLE_WEBHOOK = true`
|
||||
>
|
||||
> If you want webhook to have stronger email parsing capabilities, refer to [Configure worker to use wasm for email parsing](/en/guide/feature/mail_parser_wasm_worker)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You need to set up your own `webhook service` or use a `third-party platform`. This service needs to be able to receive `POST` requests and parse `json` data.
|
||||
|
||||
This project uses [songquanpeng/message-pusher](https://github.com/songquanpeng/message-pusher) as an example webhook service.
|
||||
|
||||
- You can use the service provided by [msgpusher.com](https://msgpusher.com)
|
||||
- You can also self-host the `message-pusher` service, refer to [songquanpeng/message-pusher](https://github.com/songquanpeng/message-pusher)
|
||||
|
||||
## Admin Configure Global Webhook
|
||||
|
||||

|
||||
|
||||
## Admin Allow Email to Use Webhook
|
||||
|
||||

|
||||
|
||||
## Configure Webhook for a Specific Email
|
||||
|
||||

|
||||
|
||||
## Webhook Data Format
|
||||
|
||||
To get the url, you need to configure the worker's `FRONTEND_URL` to your frontend address, or you can construct the url yourself using `id` = `${FRONTEND_URL}?mail_id=${id}`
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "${id}",
|
||||
"url": "${url}",
|
||||
"from": "${from}",
|
||||
"to": "${to}",
|
||||
"subject": "${subject}",
|
||||
"raw": "${raw}",
|
||||
"parsedText": "${parsedText}",
|
||||
"parsedHtml": "${parsedHtml}",
|
||||
}
|
||||
```
|
||||
42
vitepress-docs/docs/en/guide/quick-start.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Quick Start
|
||||
|
||||
## Before You Begin
|
||||
|
||||
You need a `good network environment` and a `Cloudflare account`. Open the [Cloudflare Dashboard](https://dash.cloudflare.com/)
|
||||
|
||||
Please choose one of the three deployment methods below:
|
||||
|
||||
- [Deploy via Command Line](/en/guide/cli/pre-requisite)
|
||||
- [Deploy via User Interface](/en/guide/ui/d1)
|
||||
- [Deploy via Github Actions](/en/guide/actions/pre-requisite)
|
||||
|
||||
### You can also refer to detailed tutorials provided by the community
|
||||
|
||||
- [【Tutorial】Beginner-Friendly Guide to Building Your Own Cloudflare Temporary Email (Domain Email)](https://linux.do/t/topic/316819/1)
|
||||
|
||||
## Upgrade Process
|
||||
|
||||
First, confirm your current version, then visit the [Release page](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/) and [CHANGELOG page](https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/CHANGELOG.md) to find your current version.
|
||||
|
||||
> [!WARNING] Warning
|
||||
> Pay attention to `Breaking Changes` which require `database SQL execution` or `configuration changes`.
|
||||
|
||||
Then review all changes from your current version onwards. Note that `Breaking Changes` require `database SQL execution` or `configuration changes`, while other feature updates can be configured as needed.
|
||||
|
||||
Then refer to the documentation below to use `CLI` or `UI` to redeploy the `worker` and `pages` over the previous deployment.
|
||||
|
||||
### CLI Deployment
|
||||
|
||||
- [Update D1 via Command Line](/en/guide/cli/d1)
|
||||
- [Deploy Worker via Command Line](/en/guide/cli/worker)
|
||||
- [Deploy Pages via Command Line](/en/guide/cli/pages)
|
||||
|
||||
### UI Deployment
|
||||
|
||||
- [Update D1 via User Interface](/en/guide/ui/d1)
|
||||
- [Deploy Worker via User Interface](/en/guide/ui/worker)
|
||||
- [Deploy Pages via User Interface](/en/guide/ui/pages)
|
||||
|
||||
### Github Actions Deployment
|
||||
|
||||
- [How to Configure Auto-Update with Github Actions](/en/guide/actions/auto-update)
|
||||
7
vitepress-docs/docs/en/guide/star-history.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Star History
|
||||
|
||||
<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>
|
||||
31
vitepress-docs/docs/en/guide/ui/d1.md
Normal file
@@ -0,0 +1,31 @@
|
||||
# Initialize/Update D1 Database
|
||||
|
||||
## Create Database
|
||||
|
||||
Open the Cloudflare console, select `Storage & Databases` -> `D1 SQL Database` -> `Create Database`, and click to create the database.
|
||||
|
||||

|
||||
|
||||
After creation, we can see the D1 database in the Cloudflare console.
|
||||
|
||||
## Initialize Database
|
||||
|
||||
::: warning Note
|
||||
You can also skip initializing the database and after deployment is complete, go to the admin page's `Quick Setup` -> `Database` section and click the `Initialize Database` button to initialize the database.
|
||||
:::
|
||||
|
||||
Open the `Console` tab, enter the content from the [db/schema.sql](https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/db/schema.sql) file in the repository, and click `Execute` to run it.
|
||||
|
||||

|
||||
|
||||
## Update Database Schema
|
||||
|
||||
For `schema` updates, please confirm the version you previously deployed.
|
||||
|
||||
Check the [Changelog](https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/CHANGELOG.md)
|
||||
|
||||
Find the `patch` file that needs to be executed, for example: `db/2024-01-13-patch.sql`
|
||||
|
||||
Open the `Console` tab, enter the content of the `patch` file, and click `Execute` to run it.
|
||||
|
||||

|
||||
105
vitepress-docs/docs/en/guide/ui/pages.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Cloudflare Pages Frontend
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const domain = ref("")
|
||||
const downloadUrl = ref("")
|
||||
const tip = ref("Download")
|
||||
|
||||
const generate = async () => {
|
||||
try {
|
||||
const response = await fetch("/ui_install/frontend.zip");
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
var zip = new JSZip();
|
||||
await zip.loadAsync(arrayBuffer);
|
||||
let target_content = ""
|
||||
let target_path = ""
|
||||
const directory = zip.folder("assets");
|
||||
if (directory) {
|
||||
for (const [relativePath, zipEntry] of Object.entries(directory.files)) {
|
||||
console.log(relativePath);
|
||||
if (relativePath.startsWith("assets/index-") && relativePath.endsWith(".js")){
|
||||
let content = await zipEntry.async("string");
|
||||
content = content.replace("https://temp-email-api.xxx.xxx", domain.value);
|
||||
target_path = relativePath;
|
||||
zip.file(relativePath, content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!target_path) {
|
||||
tip.value = "Generation failed";
|
||||
downloadUrl.value = '';
|
||||
}
|
||||
const blob = await zip.generateAsync({ type: "blob" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
downloadUrl.value = url;
|
||||
} catch (error) {
|
||||
console.error("Error: ", error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
1. Click `Compute (Workers)` -> `Workers & Pages` -> `Create`
|
||||
|
||||

|
||||
|
||||
2. Select `Pages`, choose `Use direct upload`
|
||||
|
||||

|
||||
|
||||
3. Enter the address of the deployed worker. The address should not include a trailing `/`. Click generate, and if successful, a download button will appear. You will get a zip package.
|
||||
- The worker domain here is the backend API domain. For example, if I deployed at `https://temp-email-api.awsl.uk`, then fill in `https://temp-email-api.awsl.uk`
|
||||
- If your domain is `https://temp-email-api.xxx.workers.dev`, then fill in `https://temp-email-api.xxx.workers.dev`
|
||||
|
||||
> [!warning] Note
|
||||
> The `worker.dev` domain is not accessible in China, please use a custom domain.
|
||||
|
||||
<div :class="$style.container">
|
||||
<input :class="$style.input" type="text" v-model="domain" placeholder="Please enter address"></input>
|
||||
<button :class="$style.button" @click="generate">Generate</button>
|
||||
<a v-if="downloadUrl" :href="downloadUrl" download="frontend.zip">{{ tip }}</a>
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> You can also deploy manually. Download the zip from here: [frontend.zip](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/frontend.zip)
|
||||
>
|
||||
> Modify the index-xxx.js file in the archive, where xx is a random string
|
||||
>
|
||||
> Search for `https://temp-email-api.xxx.xxx` and replace it with your worker's domain, then deploy the new zip file
|
||||
|
||||
4. Select `Pages`, click `Create Pages`, modify the name, upload the downloaded zip package, and then click `Deploy`
|
||||
|
||||

|
||||
|
||||
5. Open the `Pages` you just deployed, click `Custom Domain`. Here you can add your own domain, or you can use the automatically generated `*.pages.dev` domain. If you can open the domain, the deployment is successful.
|
||||
|
||||

|
||||
|
||||
<style module>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.input {
|
||||
border: 2px solid deepskyblue;
|
||||
margin-right: 10px;
|
||||
width: 75%;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: deepskyblue;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: green;
|
||||
}
|
||||
</style>
|
||||
107
vitepress-docs/docs/en/guide/ui/worker.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Cloudflare Workers Backend
|
||||
|
||||
> [!warning] Note
|
||||
> The `worker.dev` domain is not accessible in China, please use a custom domain.
|
||||
|
||||
1. Click `Compute (Workers)` -> `Workers & Pages` -> `Create`
|
||||
|
||||

|
||||
|
||||
2. Select `Worker`, click `Create Worker`, modify the name and then click `Deploy`
|
||||
|
||||

|
||||

|
||||
|
||||
3. Go back to `Workers & Pages`, find the worker you just created, click `Settings` -> `Runtime`, modify `Compatibility flags`, manually add `nodejs_compat`, and the compatibility date also needs to be later than the date shown in the image.
|
||||
|
||||

|
||||
|
||||
4. Download [worker.js](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/worker.js)
|
||||
|
||||
5. Go back to `Overview`, find the worker you just created, click `Edit Code`, delete the original file, upload `worker.js`, and click `Deploy`
|
||||
|
||||
> [!NOTE]
|
||||
> To upload, first click Explorer in the left menu,
|
||||
> then right-click in the file list window and find `Upload` in the context menu,
|
||||
> please refer to the screenshots below
|
||||
>
|
||||
> Reference: [issues156](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/156#issuecomment-2079453822)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
6. Click `Settings` -> `Variables and Secrets`, add variables as shown in the image
|
||||
|
||||

|
||||
|
||||
> [!NOTE] Note
|
||||
> For more variable configuration, please see [Worker Variables Documentation](/en/guide/worker-vars)
|
||||
>
|
||||
> Note that the outermost quotes are not needed for string format variables
|
||||
>
|
||||
> For `USER_ROLES`, please configure in this format: `[{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"vip","prefix":"vip"},{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"admin","prefix":""}]`
|
||||
|
||||
Recommended variable list
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
| -------------------------- | ----------- | ---------------------------------------------------------------------- | ------------------------------------ |
|
||||
| `PREFIX` | Text | Default prefix for new email names, can be omitted if no prefix needed | `tmp` |
|
||||
| `DOMAINS` | JSON | All domains for temporary email, supports multiple domains | `["awsl.uk", "dreamhunter2333.xyz"]` |
|
||||
| `JWT_SECRET` | Text/Secret | Secret for generating JWT, JWT is used for login and authentication | `xxx` |
|
||||
| `ADMIN_PASSWORDS` | JSON | Admin console password, console access not allowed if not configured | `["123", "456"]` |
|
||||
| `ENABLE_USER_CREATE_EMAIL` | Text/JSON | Whether to allow users to create emails, not allowed if not configured | `true` |
|
||||
| `ENABLE_USER_DELETE_EMAIL` | Text/JSON | Whether to allow users to delete emails, not allowed if not configured | `true` |
|
||||
|
||||
7. Click `Settings` -> `Bindings`, click `Add Binding`, enter the name as shown, select the D1 database you just created, and click `Add Binding`
|
||||
|
||||
> [!NOTE] Important
|
||||
> Note that the binding name for `D1 Database` here must be `DB`
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
8. Click `Settings` -> `Triggers`, here you can add your own domain, or you can use the automatically generated `*.workers.dev` domain. Record this domain, as it will be needed when deploying the frontend later.
|
||||
|
||||
> [!NOTE]
|
||||
> Open the `worker` `url`, if it displays `OK`, the deployment is successful
|
||||
>
|
||||
> Open `/health_check`, if it displays `OK`, the deployment is successful
|
||||
|
||||

|
||||
|
||||
9. If you want to enable the user registration feature and need to send email verification, you need to create a `KV` cache. You can skip this step if not needed.
|
||||
|
||||
> [!NOTE] Important
|
||||
> If you want to enable the user registration feature and need to send email verification, you need to create a `KV` cache. You can skip this step if not needed.
|
||||
>
|
||||
> Note that the binding name for `KV` here must be `KV`
|
||||
|
||||
Click `Storage & Databases` -> `KV` -> `Create Namespace`, as shown in the image, click `Create Namespace`
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Then click `Settings` -> `Bindings`, click `Add Binding`, enter the name as shown, select the KV you just created, and click `Add Binding`
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
10. Telegram Bot Configuration
|
||||
|
||||
> [!NOTE]
|
||||
> If you don't need Telegram Bot, you can skip this step
|
||||
|
||||
Please first create a Telegram Bot, then get the `token`, add the `token` to `Variables and Secrets`, variable name: `TELEGRAM_BOT_TOKEN`
|
||||
|
||||
11. If you want to use the scheduled task to clean emails in the admin page, you need to add a scheduled task in `Settings` -> `Trigger Events` -> `Cron Triggers`.
|
||||
|
||||
> [!NOTE]
|
||||
> Select `cron` expression, enter `0 0 * * *` (this expression means run daily at midnight), click `Add` to add. Please adjust this expression according to your needs.
|
||||
7
vitepress-docs/docs/en/guide/what-is-temp-mail.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Introduction to Temporary Email
|
||||
|
||||
## What is Temporary Email
|
||||
|
||||
A temporary email, also known as disposable email or temporary email address, is a virtual mailbox used for temporarily receiving emails. Unlike regular mailboxes, temporary emails are designed to provide an anonymous and temporary email receiving solution.
|
||||
|
||||
Temporary emails are often provided by websites or online service providers. Users can use temporary email addresses when they need to register or receive verification emails, without exposing their real email address. The benefit of this is to protect personal privacy.
|
||||
143
vitepress-docs/docs/en/guide/worker-vars.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Worker Variables
|
||||
|
||||
> [!NOTE] Note
|
||||
> For CLI deployment syntax, please refer to `worker/wrangler.toml.template`
|
||||
|
||||
## Required Variables
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
| -------------------------- | ----------- | ---------------------------------------------------------------------- | ------------------------------------ |
|
||||
| `DOMAINS` | JSON | All domains for temporary email, supports multiple domains | `["awsl.uk", "dreamhunter2333.xyz"]` |
|
||||
| `JWT_SECRET` | Text/Secret | Secret key for generating JWT, used for login and authentication | `xxx` |
|
||||
| `ADMIN_PASSWORDS` | JSON | Admin console passwords, console access disabled if not configured | `["123", "456"]` |
|
||||
| `ENABLE_USER_CREATE_EMAIL` | Text/JSON | Whether to allow users to create mailboxes, disabled if not configured | `true` |
|
||||
| `ENABLE_USER_DELETE_EMAIL` | Text/JSON | Whether to allow users to delete emails, disabled if not configured | `true` |
|
||||
|
||||
## Console Related Variables
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
| ------------------------------ | --------- | ------------------------------------------------------- | ---------------- |
|
||||
| `PASSWORDS` | JSON | Website private passwords, required after configuration | `["123", "456"]` |
|
||||
| `DISABLE_ADMIN_PASSWORD_CHECK` | Text/JSON | Warning: Admin console without password or user check | `false` |
|
||||
|
||||
## Email Related Variables
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
| ------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
|
||||
| `PREFIX` | Text | Default prefix for new `email address` names, can be left unconfigured if no prefix needed | `tmp` |
|
||||
| `MIN_ADDRESS_LEN` | Number | Minimum length of `email address` name | `1` |
|
||||
| `MAX_ADDRESS_LEN` | Number | Maximum length of `email address` name | `30` |
|
||||
| `DISABLE_CUSTOM_ADDRESS_NAME` | Text/JSON | Disable custom email address names, if set to true, users cannot enter custom names and they will be auto-generated | `true` |
|
||||
| `ADDRESS_CHECK_REGEX` | Text | Regular expression for `email address` name, used for validation only | `^(?!.*admin).*` |
|
||||
| `ADDRESS_REGEX` | Text | Regular expression to replace illegal symbols in `email address` name, symbols not in the regex will be replaced. Default is `[^a-z0-9]` if not set. Use with caution as some symbols may prevent email reception | `[^a-z0-9]` |
|
||||
| `DEFAULT_DOMAINS` | JSON | Default domains available to users (not logged in or users without assigned roles) | `["awsl.uk", "dreamhunter2333.xyz"]` |
|
||||
| `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` | Text/JSON | Whether to prioritize default domain when creating new addresses, if set to true, will use the first domain when no domain is specified, mainly for telegram bot scenarios | `false` |
|
||||
| `DOMAIN_LABELS` | JSON | For Chinese domains, you can use DOMAIN_LABELS to display Chinese names | `["中文.awsl.uk", "dreamhunter2333.xyz"]` |
|
||||
| `ENABLE_AUTO_REPLY` | Text/JSON | Allow automatic email replies | `true` |
|
||||
| `DEFAULT_SEND_BALANCE` | Text/JSON | Default email sending balance, will be 0 if not set | `1` |
|
||||
| `ENABLE_ADDRESS_PASSWORD` | Text/JSON | Enable address password feature, when enabled, passwords will be auto-generated for new addresses, supports password login and modification | `true` |
|
||||
|
||||
## Email Reception Related Variables
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
| ------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------- |
|
||||
| `BLACK_LIST` | Text | Blacklist for filtering senders, comma separated | `gov.cn,edu.cn` |
|
||||
| `ENABLE_CHECK_JUNK_MAIL` | Text/JSON | Whether to enable junk mail checking, used with the following two lists | `false` |
|
||||
| `JUNK_MAIL_CHECK_LIST` | JSON | Junk mail check configuration, marked as junk if any item `exists` and `fails` | `["spf", "dkim", "dmarc"]` |
|
||||
| `JUNK_MAIL_FORCE_PASS_LIST` | JSON | Junk mail check configuration, marked as junk if any item `does not exist` or `fails` | `["spf", "dkim", "dmarc"]` |
|
||||
| `FORWARD_ADDRESS_LIST` | JSON | Global forward address list, disabled if not configured, all emails will be forwarded to listed addresses when enabled | `["xxx@xxx.com"]` |
|
||||
| `REMOVE_EXCEED_SIZE_ATTACHMENT` | Text/JSON | If attachment exceeds 2MB, remove it, email may lose some information due to parsing | `true` |
|
||||
| `REMOVE_ALL_ATTACHMENT` | Text/JSON | Remove all attachments, email may lose some information due to parsing | `true` |
|
||||
|
||||
> [!NOTE]
|
||||
> `Junk mail checking` and `attachment removal` require email parsing, free tier CPU is limited, may cause large email parsing timeout
|
||||
>
|
||||
> If you want stronger email parsing capabilities
|
||||
>
|
||||
> Refer to [Configure worker to use wasm for email parsing](/en/guide/feature/mail_parser_wasm_worker)
|
||||
|
||||
## Webhook Related Variables
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
| ---------------- | --------- | ------------------------------------------------- | ------------------ |
|
||||
| `ENABLE_WEBHOOK` | Text/JSON | Whether to enable webhook | `true` |
|
||||
| `FRONTEND_URL` | Text | Frontend URL, used for sending webhook email URLs | `https://xxxx.xxx` |
|
||||
|
||||
> [!NOTE]
|
||||
> Webhook functionality requires email parsing, free tier CPU is limited, may cause large email parsing timeout
|
||||
>
|
||||
> If you want stronger email parsing capabilities
|
||||
>
|
||||
> Refer to [Configure worker to use wasm for email parsing](/en/guide/feature/mail_parser_wasm_worker)
|
||||
|
||||
## User Related Variables
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
| ------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------- | --------- |
|
||||
| `USER_DEFAULT_ROLE` | Text | Default role for new users, only effective when email verification is enabled | `vip` |
|
||||
| `ADMIN_USER_ROLE` | Text | Admin role configuration, if user role equals ADMIN_USER_ROLE, user can access admin console | `admin` |
|
||||
| `USER_ROLES` | JSON | - | See below |
|
||||
| `DISABLE_ANONYMOUS_USER_CREATE_EMAIL` | Text/JSON | Disable anonymous user mailbox creation, if set to true, users can only create addresses after login | `true` |
|
||||
| `NO_LIMIT_SEND_ROLE` | Text | Roles that can send unlimited emails, multiple roles separated by comma `vip,admin` | `vip` |
|
||||
|
||||
> [!NOTE] USER_ROLES User Role Configuration
|
||||
>
|
||||
> - If `domains` is empty, `DEFAULT_DOMAINS` will be used
|
||||
> - If prefix is null, the default prefix will be used, if prefix is an empty string, no prefix will be used
|
||||
>
|
||||
> When deploying through UI, configure `USER_ROLES` in this format: `[{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"vip","prefix":"vip"},{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"admin","prefix":""}]`
|
||||
>
|
||||
> When deploying via CLI, refer to `worker/wrangler.toml.template` and configure `USER_ROLES` in this format: `[{ domains = ["awsl.uk", "dreamhunter2333.xyz"], role = "vip", prefix = "vip" }, { domains = ["awsl.uk", "dreamhunter2333.xyz"], role = "admin", prefix = "" }]`
|
||||
|
||||
## Web Related Variables
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
| -------------------------- | ----------- | ------------------------------------------------------------------------ | --------------------- |
|
||||
| `DEFAULT_LANG` | Text | Worker error message default language, zh/en | `zh` |
|
||||
| `TITLE` | Text | Custom frontend page website title, supports html | `Custom Title` |
|
||||
| `ANNOUNCEMENT` | Text | Custom frontend page announcement, supports html | `Custom Announcement` |
|
||||
| `ALWAYS_SHOW_ANNOUNCEMENT` | Text/JSON | Whether to always show announcement (even if unchanged), default `false` | `true` |
|
||||
| `COPYRIGHT` | Text | Custom frontend footer text, supports html | `Dream Hunter` |
|
||||
| `ADMIN_CONTACT` | Text | Admin contact information, can be any string, hidden if not configured | `xxx@gmail.com` |
|
||||
| `DISABLE_SHOW_GITHUB` | Text/JSON | Whether to show GitHub link | `true` |
|
||||
| `CF_TURNSTILE_SITE_KEY` | Text/Secret | Turnstile CAPTCHA configuration | `xxx` |
|
||||
| `CF_TURNSTILE_SECRET_KEY` | Text/Secret | Turnstile CAPTCHA configuration | `xxx` |
|
||||
|
||||
## 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 | `{}` |
|
||||
|
||||
> [!NOTE]
|
||||
> Telegram functionality requires email parsing, free tier CPU is limited, may cause large email parsing timeout
|
||||
>
|
||||
> If you want stronger email parsing capabilities
|
||||
>
|
||||
> Refer to [Configure worker to use wasm for email parsing](/en/guide/feature/mail_parser_wasm_worker)
|
||||
|
||||
## Other Variables
|
||||
|
||||
| Variable Name | Type | Description | Example |
|
||||
| ----------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
|
||||
| `ENABLE_ANOTHER_WORKER` | Text/JSON | Whether to enable other workers to process emails | `false` |
|
||||
| `ANOTHER_WORKER_LIST` | JSON | - Configuration for other workers to process emails, multiple workers can be configured <br/> - Filter by keywords, call the bound worker's method (default method name is rpcEmail)<br/> - keywords are required, otherwise the worker will not be triggered | See below |
|
||||
|
||||
> [!NOTE]
|
||||
> `ANOTHER_WORKER_LIST` configuration example
|
||||
>
|
||||
> ```toml
|
||||
> #ANOTHER_WORKER_LIST ="""
|
||||
> #[
|
||||
> # {
|
||||
> # "binding":"AUTH_INBOX",
|
||||
> # "method":"rpcEmail",
|
||||
> # "keywords":[
|
||||
> # "验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认",
|
||||
> # "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm"
|
||||
> # ]
|
||||
> # }
|
||||
> #]
|
||||
> #
|
||||
> ```
|
||||
@@ -3,22 +3,29 @@
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "Temporary mailbox document"
|
||||
tagline: "Build CloudFlare to send and receive free temporary domain name mailboxes"
|
||||
name: "Temporary Email Docs"
|
||||
tagline: "Build Free CloudFlare Temporary Domain Email with Send & Receive"
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Try it now
|
||||
link: https://mail.awsl.uk/en
|
||||
- theme: alt
|
||||
text: command line deployment
|
||||
link: /en/cli
|
||||
- theme: brand
|
||||
text: Try it now
|
||||
link: https://mail.awsl.uk/
|
||||
- theme: alt
|
||||
text: CLI Deployment
|
||||
link: /en/guide/quick-start
|
||||
- theme: alt
|
||||
text: Deploy via UI
|
||||
link: /en/guide/quick-start
|
||||
- theme: alt
|
||||
text: Deploy via Github Actions
|
||||
link: /en/guide/quick-start
|
||||
|
||||
features:
|
||||
- title: Free hosting on CloudFlare, no server required
|
||||
details: Cloudflare D1 database, Cloudflare Pages frontend, Cloudflare Workers backend, Cloudflare Email Routing
|
||||
- title: Only domain name required for private deployment
|
||||
details: Support password login email, access authorization can be used as a private site, support attachment function
|
||||
- title: Use rust wasm to parse emails
|
||||
details: Use rust wasm to parse emails, support various RFC standards for emails, support attachments, extremely fast
|
||||
- title: Support sending emails
|
||||
details: Support sending txt or html emails through domain name mailboxes,Support DKIM signature
|
||||
- title: Private deployment with only a domain name, free hosting on CloudFlare, no server required
|
||||
details: Support password login for mailboxes, user registration, access password for private sites, attachment support.
|
||||
- title: Email parsing using Rust WASM
|
||||
details: Parse emails with Rust WASM, support various RFC email standards, support attachments, extremely fast
|
||||
- title: Telegram Bot and Webhook support
|
||||
details: Forward emails to Telegram or webhook, Telegram Bot supports mailbox binding, view emails, Telegram Mini App
|
||||
- title: Send emails (UI/API/SMTP)
|
||||
details: Send txt or html emails via domain mailboxes, DKIM signature support, send via UI/API/SMTP
|
||||
---
|
||||
|
||||
6
vitepress-docs/docs/en/reference.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Reference
|
||||
|
||||
- https://developers.cloudflare.com/d1/
|
||||
- https://developers.cloudflare.com/pages/
|
||||
- https://developers.cloudflare.com/workers/
|
||||
- https://developers.cloudflare.com/email-routing/
|
||||
8
vitepress-docs/docs/en/status.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Service Status
|
||||
|
||||
[Status Link](https://uptime.aks.awsl.icu/status/temp-email)
|
||||
|
||||
| Service | Status |
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Backend](https://temp-email-api.awsl.uk/) |       |
|
||||
| [Frontend](https://mail.awsl.uk/) |       |
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 49 KiB |
BIN
vitepress-docs/docs/public/ui_install/worker-bindings.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
vitepress-docs/docs/public/ui_install/worker-d1-1.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
vitepress-docs/docs/public/ui_install/worker-d1-2.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
BIN
vitepress-docs/docs/public/ui_install/worker-kv-0.png
Normal file
|
After Width: | Height: | Size: 29 KiB |