Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2f3634c7e | ||
|
|
b62a3cbc3e | ||
|
|
8edb75587e | ||
|
|
de48661d0d | ||
|
|
a905ba5f06 | ||
|
|
6ae90be3bf | ||
|
|
5e24817de6 | ||
|
|
732189482e | ||
|
|
2bbde15f53 | ||
|
|
37cf0776b5 | ||
|
|
3fbace871c | ||
|
|
648e9f7adf | ||
|
|
ab2bfdd00f | ||
|
|
0565978930 | ||
|
|
89d8944e60 | ||
|
|
4084771621 | ||
|
|
840496c48f | ||
|
|
9843b35f54 | ||
|
|
bfd66f5019 | ||
|
|
0bc31360b0 | ||
|
|
267d9bb93e | ||
|
|
2cc84d565c | ||
|
|
c96d180591 | ||
|
|
1303b0f2a9 | ||
|
|
9f535a0a90 | ||
|
|
70109785c6 | ||
|
|
7fd10f2775 | ||
|
|
f59b8c7a1b | ||
|
|
312ac13185 | ||
|
|
e6c582be9f | ||
|
|
483c429feb | ||
|
|
da5482e095 | ||
|
|
de4646876a | ||
|
|
bbc8a96811 | ||
|
|
9ac9cd46b0 | ||
|
|
c694b07380 | ||
|
|
672c4c7273 | ||
|
|
ee023ac2e9 | ||
|
|
cc77bdf36d | ||
|
|
dec309a0fd | ||
|
|
9488543e44 | ||
|
|
50326bcc98 | ||
|
|
272b624b9b | ||
|
|
e230801a1c | ||
|
|
07833d5ca9 | ||
|
|
101a561894 | ||
|
|
327962432a | ||
|
|
6051d49315 | ||
|
|
95f361743b | ||
|
|
c6afc5d425 | ||
|
|
466f53254b |
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
@@ -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)
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
dist/
|
||||
test/
|
||||
.vscode/
|
||||
|
||||
@@ -1,7 +1,57 @@
|
||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||
# CHANGE LOG
|
||||
|
||||
## main(v0.9.1)
|
||||
## main(v1.0.6)
|
||||
|
||||
- feat: |DB| update db schema add index
|
||||
- feat: |地址密码| 增加地址密码登录功能, 通过 `ENABLE_ADDRESS_PASSWORD` 配置启用
|
||||
- fix: |GitHub Actions| 修复 debug 模式配置,仅当 DEBUG_MODE 为 'true' 时才启用调试模式
|
||||
- feat: |Admin| 账户管理页面新增多选批量操作功能(批量删除、批量清空收件箱、批量清空发件箱)
|
||||
|
||||
## v1.0.5
|
||||
|
||||
- feat: 新增 `DISABLE_CUSTOM_ADDRESS_NAME` 配置: 禁用自定义邮箱地址名称功能
|
||||
- feat: 新增 `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` 配置: 创建地址时优先使用第一个域名
|
||||
- feat: |UI| 主页增加进入极简模式按钮
|
||||
- feat: |Webhook| 增加白名单开关功能,支持灵活控制访问权限
|
||||
|
||||
## v1.0.4
|
||||
|
||||
- feat: |UI| 优化极简模式主页, 增加全部邮件页面功能(删除/下载/附件/...), 可在 `外观` 中切换
|
||||
- feat: admin 账号设置页面增加 `邮件转发规则` 配置
|
||||
- feat: admin 账号设置页面增加 `禁止接收未知地址邮件` 配置
|
||||
- feat: 邮件页面增加 上一封/下一封 按钮
|
||||
|
||||
## v1.0.3
|
||||
|
||||
- fix: 修复 github actions 部署问题
|
||||
- feat: telegram /new 不指定域名时, 使用随机地址
|
||||
|
||||
## v1.0.2
|
||||
|
||||
- fix: 修复 oauth2 登录失败的问题
|
||||
|
||||
## v1.0.1
|
||||
|
||||
- feat: |UI| 增加极简模式主页, 可在 `外观` 中切换
|
||||
- fix: 修复 oauth2 登录时,default role 不生效的问题
|
||||
|
||||
## v1.0.0
|
||||
|
||||
- fix: |UI| 修复 User 查看收件箱,不选择地址时,关键词查询不生效
|
||||
- fix: 修复自动清理任务,时间为 0 时不生效的问题
|
||||
- feat: 清理功能增加 创建 n 天前地址清理,n 天前未活跃地址清理
|
||||
- fix: |IMAP Proxy| 修复 IMAP Proxy 服务器,无法查看新邮件的问题
|
||||
|
||||
## v0.10.0
|
||||
|
||||
- feat: 支持 User 查看收件箱,`/user_api/mails` 接口, 支持 `address` 和 `keyword` 过滤
|
||||
- fix: 修复 Oauth2 登录获取 Token 时,一些 Oauth2 需要 `redirect_uri` 参数的问题
|
||||
- feat: 用户访问网页时,如果 `user token` 在 7 天内过期,自动刷新
|
||||
- feat: admin portal 中增加初始化 db 的功能
|
||||
- feat: 增加 `ALWAYS_SHOW_ANNOUNCEMENT` 变量,用于配置是否总是显示公告
|
||||
|
||||
## v0.9.1
|
||||
|
||||
- feat: |UI| support google ads
|
||||
- feat: |UI| 使用 shadow DOM 防止样式污染
|
||||
|
||||
@@ -1,90 +1,188 @@
|
||||
# 使用 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] 支持发送邮件,支持 `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)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<!-- markdownlint-disable-file MD033 MD045 -->
|
||||
# Cloudflare Temp Email
|
||||
|
||||
<p align="center">
|
||||
<a href="README.md">🇨🇳 中文</a> |
|
||||
<a href="README_EN.md">🇺🇸 English</a>
|
||||
</p>
|
||||
|
||||
**A fully-featured temporary email service built on Cloudflare's free services.**
|
||||
|
||||
> This project is for learning and personal use only.
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
- [📖 Documentation](https://temp-mail-docs.awsl.uk/en/)
|
||||
- [🎯 Live Demo](https://mail.awsl.uk/)
|
||||
- [📝 CHANGELOG](CHANGELOG.md)
|
||||
|
||||
<p align="center">
|
||||
<a href="https://temp-mail-docs.awsl.uk/en/guide/actions/github-action.html">
|
||||
<img src="https://deploy.workers.cloudflare.com/button" alt="Deploy to Cloudflare Workers">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
- **� Email Processing**: Rust WASM parser, SMTP/IMAP support, attachments, auto-reply
|
||||
- **👥 User Management**: OAuth2 login, Passkey authentication, role management
|
||||
- **🌐 Admin Panel**: Complete admin console, user management, scheduled cleanup
|
||||
- **🤖 Integrations**: Telegram Bot, webhooks, CAPTCHA, rate limiting
|
||||
- **� Modern UI**: Multi-language, responsive design, JWT auto-login
|
||||
|
||||
## 🏗️ Tech Stack
|
||||
|
||||
- **Frontend**: Vue 3 + TypeScript + Vite
|
||||
- **Backend**: Cloudflare Workers + D1 Database
|
||||
- **Email**: Cloudflare Email Routing + Rust WASM Parser
|
||||
- **Storage**: Cloudflare KV + R2 (optional S3)
|
||||
|
||||
## 🌟 Community
|
||||
|
||||
- [Telegram](https://t.me/cloudflare_temp_email)
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License - see [LICENSE](LICENSE) for details.
|
||||
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE
|
||||
address
|
||||
ADD
|
||||
password TEXT;
|
||||
@@ -9,15 +9,22 @@ CREATE TABLE IF NOT EXISTS raw_mails (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_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 +57,8 @@ CREATE TABLE IF NOT EXISTS sendbox (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sendbox_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.9.1",
|
||||
"version": "1.0.6",
|
||||
"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",
|
||||
@@ -24,29 +25,30 @@
|
||||
"@vueuse/core": "^12.8.2",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.8.4",
|
||||
"axios": "^1.12.2",
|
||||
"jszip": "^3.10.1",
|
||||
"mail-parser-wasm": "^0.2.1",
|
||||
"naive-ui": "^2.41.0",
|
||||
"postal-mime": "^2.4.3",
|
||||
"naive-ui": "^2.43.1",
|
||||
"postal-mime": "^2.4.4",
|
||||
"vooks": "^0.2.12",
|
||||
"vue": "^3.5.13",
|
||||
"vue": "^3.5.22",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-i18n": "^11.1.3",
|
||||
"vue-router": "^4.5.0"
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vicons/fa": "^0.13.0",
|
||||
"@vicons/material": "^0.13.0",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"unplugin-auto-import": "^19.1.2",
|
||||
"unplugin-vue-components": "^28.4.1",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-pwa": "^1.0.0",
|
||||
"vite-plugin-top-level-await": "^1.5.0",
|
||||
"vite-plugin-wasm": "^3.4.1",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"unplugin-auto-import": "^19.3.0",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^6.3.6",
|
||||
"vite-plugin-pwa": "^1.0.3",
|
||||
"vite-plugin-top-level-await": "^1.6.0",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-window": "^7.3.0",
|
||||
"wrangler": "^4.10.0"
|
||||
}
|
||||
"wrangler": "^4.40.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const apiFetch = async (path, options = {}) => {
|
||||
data: options.body || null,
|
||||
headers: {
|
||||
'x-lang': i18n.global.locale.value,
|
||||
'x-user-token': userJwt.value,
|
||||
'x-user-token': options.userJwt || userJwt.value,
|
||||
'x-user-access-token': userSettings.value.access_token,
|
||||
'x-custom-auth': auth.value,
|
||||
'x-admin-auth': adminAuth.value,
|
||||
@@ -78,6 +78,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,11 +86,16 @@ 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;
|
||||
}
|
||||
if (openSettings.value.announcement && openSettings.value.announcement != announcement.value) {
|
||||
if (openSettings.value.announcement
|
||||
&& !openSettings.value.fetched
|
||||
&& (openSettings.value.announcement != announcement.value
|
||||
|| openSettings.value.alwaysShowAnnouncement)
|
||||
) {
|
||||
announcement.value = openSettings.value.announcement;
|
||||
notification.info({
|
||||
content: () => {
|
||||
@@ -139,6 +145,19 @@ const getUserSettings = async (message) => {
|
||||
if (!userJwt.value) return;
|
||||
const res = await api.fetch("/user_api/settings")
|
||||
Object.assign(userSettings.value, res)
|
||||
// auto refresh user jwt
|
||||
if (userSettings.value.new_user_token) {
|
||||
try {
|
||||
await api.fetch("/user_api/settings", {
|
||||
userJwt: userSettings.value.new_user_token,
|
||||
})
|
||||
userJwt.value = userSettings.value.new_user_token;
|
||||
console.log("User JWT updated successfully");
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Failed to update user JWT", error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
message?.error(error.message || "error");
|
||||
} finally {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<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";
|
||||
|
||||
const message = useMessage()
|
||||
const isMobile = useIsMobile()
|
||||
@@ -51,8 +51,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 +62,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 +133,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 +155,8 @@ const { t } = useI18n({
|
||||
cancelMultiAction: '取消多选',
|
||||
selectAll: '全选本页',
|
||||
unselectAll: '取消全选',
|
||||
prevMail: '上一封',
|
||||
nextMail: '下一封',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -185,10 +228,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 +276,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) => {
|
||||
@@ -413,59 +446,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')">
|
||||
@@ -517,89 +522,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 }}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CloudDownloadRound, ReplyFilled, ForwardFilled, FullscreenRound } from '@vicons/material'
|
||||
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
|
||||
import { getDownloadEmlUrl } from '../utils/email-parser';
|
||||
import { utcToLocalDate } from '../utils';
|
||||
import { useGlobalState } from '../store';
|
||||
|
||||
const { preferShowTextMail, useIframeShowMail, useUTCDate } = useGlobalState();
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
delete: 'Delete',
|
||||
deleteMailTip: 'Are you sure you want to delete mail?',
|
||||
attachments: 'View Attachments',
|
||||
downloadMail: 'Download Mail',
|
||||
reply: 'Reply',
|
||||
forward: 'Forward',
|
||||
showTextMail: 'Show Text Mail',
|
||||
showHtmlMail: 'Show HTML Mail',
|
||||
saveToS3: 'Save to S3',
|
||||
size: 'Size',
|
||||
fullscreen: 'Fullscreen',
|
||||
},
|
||||
zh: {
|
||||
delete: '删除',
|
||||
deleteMailTip: '确定要删除邮件吗?',
|
||||
attachments: '查看附件',
|
||||
downloadMail: '下载邮件',
|
||||
reply: '回复',
|
||||
forward: '转发',
|
||||
showTextMail: '显示纯文本邮件',
|
||||
showHtmlMail: '显示HTML邮件',
|
||||
saveToS3: '保存到S3',
|
||||
size: '大小',
|
||||
fullscreen: '全屏',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
mail: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showEMailTo: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
enableUserDeleteEmail: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showReply: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showSaveS3: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// 回调函数 props
|
||||
onDelete: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
},
|
||||
onReply: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
},
|
||||
onForward: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
},
|
||||
onSaveToS3: {
|
||||
type: Function,
|
||||
default: () => { }
|
||||
}
|
||||
});
|
||||
|
||||
const showTextMail = ref(preferShowTextMail.value);
|
||||
const showAttachments = ref(false);
|
||||
const curAttachments = ref([]);
|
||||
const attachmentLoding = ref(false);
|
||||
const showFullscreen = ref(false);
|
||||
|
||||
const handleDelete = () => {
|
||||
props.onDelete();
|
||||
};
|
||||
|
||||
const handleViewAttachments = () => {
|
||||
curAttachments.value = props.mail.attachments;
|
||||
showAttachments.value = true;
|
||||
};
|
||||
|
||||
const handleReply = () => {
|
||||
props.onReply();
|
||||
};
|
||||
|
||||
const handleForward = () => {
|
||||
props.onForward();
|
||||
};
|
||||
|
||||
|
||||
const handleSaveToS3 = async (filename, blob) => {
|
||||
attachmentLoding.value = true;
|
||||
try {
|
||||
await props.onSaveToS3(filename, blob);
|
||||
} finally {
|
||||
attachmentLoding.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mail-content-renderer">
|
||||
<!-- 邮件信息标签 -->
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
ID: {{ mail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ utcToLocalDate(mail.created_at, useUTCDate.value) }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ mail.source }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ mail.address }}
|
||||
</n-tag>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="handleDelete">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||
</template>
|
||||
{{ t('deleteMailTip') }}
|
||||
</n-popconfirm>
|
||||
|
||||
<n-button v-if="mail.attachments && mail.attachments.length > 0" size="small" tertiary type="info"
|
||||
@click="handleViewAttachments">
|
||||
{{ t('attachments') }}
|
||||
</n-button>
|
||||
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="mail.id + '.eml'"
|
||||
:href="getDownloadEmlUrl(mail.raw)">
|
||||
<template #icon>
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</template>
|
||||
{{ t('downloadMail') }}
|
||||
</n-button>
|
||||
|
||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="handleReply">
|
||||
<template #icon>
|
||||
<n-icon :component="ReplyFilled" />
|
||||
</template>
|
||||
{{ t('reply') }}
|
||||
</n-button>
|
||||
|
||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="handleForward">
|
||||
<template #icon>
|
||||
<n-icon :component="ForwardFilled" />
|
||||
</template>
|
||||
{{ t('forward') }}
|
||||
</n-button>
|
||||
|
||||
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
||||
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
||||
</n-button>
|
||||
|
||||
<n-button size="small" tertiary type="info" @click="showFullscreen = true">
|
||||
<template #icon>
|
||||
<n-icon :component="FullscreenRound" />
|
||||
</template>
|
||||
{{ t('fullscreen') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
|
||||
<!-- 邮件内容 -->
|
||||
<div class="mail-content">
|
||||
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="mail.message" class="mail-iframe">
|
||||
</iframe>
|
||||
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mail.message" class="mail-html" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<n-drawer v-model:show="showFullscreen" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
|
||||
style="height: 100vh;">
|
||||
<n-drawer-content :title="mail.subject" closable>
|
||||
<div class="fullscreen-mail-content">
|
||||
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="mail.message" class="mail-iframe">
|
||||
</iframe>
|
||||
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mail.message" class="mail-html" />
|
||||
</div>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
|
||||
<!-- 附件模态框 -->
|
||||
<n-modal v-model:show="showAttachments" preset="dialog" title="Dialog">
|
||||
<template #header>
|
||||
<div>{{ t('attachments') }}</div>
|
||||
</template>
|
||||
<n-spin v-model:show="attachmentLoding">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.filename">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
Size: {{ row.size }}
|
||||
</n-tag>
|
||||
<n-button v-if="showSaveS3" @click="handleSaveToS3(row.filename, row.blob)" ghost type="info"
|
||||
size="small">
|
||||
{{ t('saveToS3') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-thing>
|
||||
<template #suffix>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
|
||||
:href="row.url">
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</n-button>
|
||||
</template>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</n-spin>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mail-content-renderer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mail-content {
|
||||
margin-top: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mail-text {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.mail-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.mail-html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fullscreen-mail-content {
|
||||
height: calc(100vh - 120px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.fullscreen-mail-content .mail-iframe {
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
</style>
|
||||
@@ -10,16 +10,19 @@ 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: '',
|
||||
announcement: '',
|
||||
alwaysShowAnnouncement: false,
|
||||
prefix: '',
|
||||
addressRegex: '',
|
||||
needAuth: false,
|
||||
adminContact: '',
|
||||
enableUserCreateEmail: false,
|
||||
disableAnonymousUserCreateEmail: false,
|
||||
disableCustomAddressName: false,
|
||||
enableUserDeleteEmail: false,
|
||||
enableAutoReply: false,
|
||||
enableIndexAbout: false,
|
||||
@@ -33,6 +36,7 @@ export const useGlobalState = createGlobalState(
|
||||
isS3Enabled: false,
|
||||
showGithub: true,
|
||||
disableAdminPasswordCheck: false,
|
||||
enableAddressPassword: false,
|
||||
})
|
||||
const settings = ref({
|
||||
fetched: false,
|
||||
@@ -60,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("");
|
||||
@@ -92,6 +97,8 @@ export const useGlobalState = createGlobalState(
|
||||
is_admin: false,
|
||||
/** @type {string | null} */
|
||||
access_token: null,
|
||||
/** @type {string | null} */
|
||||
new_user_token: null,
|
||||
/** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */
|
||||
user_role: null,
|
||||
});
|
||||
@@ -139,6 +146,8 @@ export const useGlobalState = createGlobalState(
|
||||
showAdminPage,
|
||||
userOauth2SessionState,
|
||||
userOauth2SessionClientID,
|
||||
useSimpleIndex,
|
||||
addressPassword,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import Mails from './admin/Mails.vue';
|
||||
import MailsUnknow from './admin/MailsUnknow.vue';
|
||||
import About from './common/About.vue';
|
||||
import Maintenance from './admin/Maintenance.vue';
|
||||
import DatabaseManager from './admin/DatabaseManager.vue';
|
||||
import Appearance from './common/Appearance.vue';
|
||||
import Telegram from './admin/Telegram.vue';
|
||||
import Webhook from './admin/Webhook.vue';
|
||||
@@ -67,6 +68,7 @@ const { t } = useI18n({
|
||||
webhookSettings: 'Webhook Settings',
|
||||
statistics: 'Statistics',
|
||||
maintenance: 'Maintenance',
|
||||
database: 'Database',
|
||||
workerconfig: 'Worker Config',
|
||||
appearance: 'Appearance',
|
||||
about: 'About',
|
||||
@@ -93,6 +95,7 @@ const { t } = useI18n({
|
||||
webhookSettings: 'Webhook 设置',
|
||||
statistics: '统计',
|
||||
maintenance: '维护',
|
||||
database: '数据库',
|
||||
workerconfig: 'Worker 配置',
|
||||
appearance: '外观',
|
||||
about: '关于',
|
||||
@@ -112,7 +115,7 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="userSettings.fetched">
|
||||
<n-modal v-model:show="showAdminPasswordModal" :closable="false" :closeOnEsc="false" :maskClosable="false"
|
||||
preset="dialog" :title="t('accessHeader')">
|
||||
<p>{{ t('accessTip') }}</p>
|
||||
@@ -126,6 +129,9 @@ onMounted(async () => {
|
||||
<n-tabs v-if="showAdminPage" type="card" v-model:value="adminTab" :placement="globalTabplacement">
|
||||
<n-tab-pane name="qucickSetup" :tab="t('qucickSetup')">
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="database" :tab="t('database')">
|
||||
<DatabaseManager />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="account_settings" :tab="t('account_settings')">
|
||||
<AccountSettings />
|
||||
</n-tab-pane>
|
||||
@@ -196,6 +202,9 @@ onMounted(async () => {
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
||||
<n-tabs type="bar" justify-content="center" animated>
|
||||
<n-tab-pane name="database" :tab="t('database')">
|
||||
<DatabaseManager />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="workerconfig" :tab="t('workerconfig')">
|
||||
<WorkerConfig />
|
||||
</n-tab-pane>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,6 +7,7 @@ import AddressMangement from './user/AddressManagement.vue';
|
||||
import UserSettingsPage from './user/UserSettings.vue';
|
||||
import UserBar from './user/UserBar.vue';
|
||||
import BindAddress from './user/BindAddress.vue';
|
||||
import UserMailBox from './user/UserMailBox.vue';
|
||||
|
||||
const {
|
||||
userTab, globalTabplacement, userSettings
|
||||
@@ -16,11 +17,13 @@ const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
address_management: 'Address Management',
|
||||
user_mail_box_tab: 'Mail Box',
|
||||
user_settings: 'User Settings',
|
||||
bind_address: 'Bind Mail Address',
|
||||
},
|
||||
zh: {
|
||||
address_management: '地址管理',
|
||||
user_mail_box_tab: '收件箱',
|
||||
user_settings: '用户设置',
|
||||
bind_address: '绑定邮箱地址',
|
||||
}
|
||||
@@ -36,6 +39,9 @@ const { t } = useI18n({
|
||||
<n-tab-pane name="address_management" :tab="t('address_management')">
|
||||
<AddressMangement />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="user_mail_box_tab" :tab="t('user_mail_box_tab')">
|
||||
<UserMailBox />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="user_settings" :tab="t('user_settings')">
|
||||
<UserSettingsPage />
|
||||
</n-tab-pane>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CleaningServicesFilled } from '@vicons/material'
|
||||
|
||||
import { api } from '../../api'
|
||||
import { init } from 'vooks/lib/on-fonts-ready';
|
||||
|
||||
const message = useMessage()
|
||||
const dbVersionData = ref({
|
||||
need_initialization: false,
|
||||
need_migration: false,
|
||||
current_db_version: '',
|
||||
code_db_version: ''
|
||||
})
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
need_initialization_tip: 'Database initialization is required. Please initialize the database.',
|
||||
need_migration_tip: 'Database migration is required. Please migrate the database.',
|
||||
current_db_version: 'Current DB Version',
|
||||
code_db_version: 'Code Needed DB Version',
|
||||
init: 'Initialize Database',
|
||||
migration: 'Migrate Database',
|
||||
initializationSuccess: 'Database initialized successfully',
|
||||
migrationSuccess: 'Database migrated successfully',
|
||||
},
|
||||
zh: {
|
||||
need_initialization_tip: '需要初始化数据库,请初始化数据库',
|
||||
need_migration_tip: '需要迁移数据库,请迁移数据库',
|
||||
current_db_version: '当前数据库版本',
|
||||
code_db_version: '需要的数据库版本',
|
||||
init: '初始化数据库',
|
||||
migration: '升级数据库 Schema',
|
||||
initializationSuccess: '数据库初始化成功',
|
||||
migrationSuccess: '数据库升级成功',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await api.fetch('/admin/db_version');
|
||||
if (res) Object.assign(dbVersionData.value, res);
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const initialization = async () => {
|
||||
try {
|
||||
await api.fetch('/admin/db_initialize', {
|
||||
method: 'POST'
|
||||
});
|
||||
await fetchData();
|
||||
message.success(t('initializationSuccess'));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const migration = async () => {
|
||||
try {
|
||||
await api.fetch('/admin/db_migration', {
|
||||
method: 'POST'
|
||||
});
|
||||
await fetchData();
|
||||
message.success(t('migrationSuccess'));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData();
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-alert v-if="dbVersionData.need_initialization" type="warning" :show-icon="false" :bordered="false">
|
||||
<span>{{ t('need_initialization_tip') }}</span>
|
||||
<n-button @click="initialization" type="primary" secondary block :loading="loading">
|
||||
{{ t('init') }}
|
||||
</n-button>
|
||||
</n-alert>
|
||||
<n-alert v-if="dbVersionData.need_migration" type="warning" :show-icon="false" :bordered="false">
|
||||
<span>{{ t('need_migration_tip') }}</span>
|
||||
<n-button @click="migration" type="primary" secondary block :loading="loading">
|
||||
{{ t('migration') }}
|
||||
</n-button>
|
||||
</n-alert>
|
||||
<n-alert type="info" :show-icon="false" :bordered="false">
|
||||
<span>
|
||||
{{ t('current_db_version') }}: {{ dbVersionData.current_db_version || "unknown" }},
|
||||
{{ t('code_db_version') }}: {{ dbVersionData.code_db_version }}
|
||||
</span>
|
||||
</n-alert>
|
||||
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-card {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.n-alert {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -11,10 +11,12 @@ 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,
|
||||
})
|
||||
|
||||
const { t } = useI18n({
|
||||
@@ -24,22 +26,26 @@ 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",
|
||||
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 天前的未活跃地址",
|
||||
autoCleanup: "自动清理",
|
||||
cleanupSuccess: "清理成功",
|
||||
cleanupNow: "立即清理",
|
||||
save: "保存",
|
||||
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档",
|
||||
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档, 配置为 0 天表示全部清空",
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -86,9 +92,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 +137,30 @@ 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>
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
@@ -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">
|
||||
@@ -224,7 +226,7 @@ onMounted(async () => {
|
||||
<n-form-item-row label="Access Token URL" required>
|
||||
<n-input v-model:value="item.accessTokenURL" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="Access Token accessTokenFormat" required>
|
||||
<n-form-item-row label="Access Token Params Format" required>
|
||||
<n-select v-model:value="item.accessTokenFormat" :options="accessTokenFormatOptions" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row label="User Info URL" required>
|
||||
@@ -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")'>
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { api } from '../../api'
|
||||
import MailBox from '../../components/MailBox.vue';
|
||||
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
addressQueryTip: 'Leave blank to query all addresses',
|
||||
keywordQueryTip: 'Leave blank to not query by keyword',
|
||||
query: 'Query',
|
||||
},
|
||||
zh: {
|
||||
addressQueryTip: '留空查询所有地址',
|
||||
keywordQueryTip: '留空不按关键字查询',
|
||||
query: '查询',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mailBoxKey = ref("")
|
||||
const addressFilter = ref();
|
||||
const mailKeyword = ref("")
|
||||
const addressFilterOptions = ref([]);
|
||||
|
||||
const queryMail = () => {
|
||||
addressFilter.value = addressFilter.value ? addressFilter.value.trim() : addressFilter.value;
|
||||
mailKeyword.value = mailKeyword.value.trim();
|
||||
mailBoxKey.value = Date.now();
|
||||
}
|
||||
|
||||
const fetchMailData = async (limit, offset) => {
|
||||
return await api.fetch(
|
||||
`/user_api/mails`
|
||||
+ `?limit=${limit}`
|
||||
+ `&offset=${offset}`
|
||||
+ (addressFilter.value ? `&address=${addressFilter.value}` : '')
|
||||
+ (mailKeyword.value ? `&keyword=${mailKeyword.value}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
const fetchAddresData = async () => {
|
||||
try {
|
||||
const { results } = await api.fetch(
|
||||
`/user_api/bind_address`
|
||||
);
|
||||
addressFilterOptions.value = results.map((item) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.name
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMail = async (curMailId) => {
|
||||
await api.fetch(`/user_api/mails/${curMailId}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
watch(addressFilter, async (newValue) => {
|
||||
queryMail();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
fetchAddresData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="margin-top: 10px;">
|
||||
<n-input-group>
|
||||
<n-select v-model:value="addressFilter" :options="addressFilterOptions" clearable
|
||||
:placeholder="t('addressQueryTip')" />
|
||||
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" @keydown.enter="queryMail" />
|
||||
<n-button @click="queryMail" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<div style="margin-top: 10px;"></div>
|
||||
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="true" :fetchMailData="fetchMailData"
|
||||
:deleteMail="deleteMail" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temp-email-pages",
|
||||
"version": "0.9.1",
|
||||
"version": "1.0.6",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -11,6 +11,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"wrangler": "^4.10.0"
|
||||
}
|
||||
"wrangler": "^4.40.1"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
@@ -14,6 +14,6 @@ pnpm add -D wrangler@latest
|
||||
cd ..
|
||||
|
||||
cd vitepress-docs/
|
||||
pnpm up
|
||||
pnpm up --latest
|
||||
pnpm add -D wrangler@latest
|
||||
cd ..
|
||||
|
||||
@@ -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):
|
||||
"""边查边返回邮件"""
|
||||
def email_generator():
|
||||
for range_item in messages.ranges:
|
||||
start, end = range_item
|
||||
_logger.info(f"Fetching messages: {self.name}, range: {start}-{end}")
|
||||
|
||||
for email_data in self.fetchGenerator(start, end):
|
||||
yield email_data
|
||||
|
||||
# 返回生成器,让IMAP4服务器逐个处理
|
||||
return email_generator()
|
||||
|
||||
def fetchGenerator(self, start, end):
|
||||
"""通用的邮件获取生成器,边查边返回"""
|
||||
start = max(start, 1)
|
||||
|
||||
# 根据邮箱类型确定API端点
|
||||
if self.name == "INBOX":
|
||||
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
|
||||
|
||||
@@ -81,6 +81,8 @@ PREFIX = "tmp" # The mailbox name prefix to be processed
|
||||
# MIN_ADDRESS_LEN = 1
|
||||
# MAX_ADDRESS_LEN = 30
|
||||
# ANNOUNCEMENT = "Custom Announcement"
|
||||
# always show ANNOUNCEMENT even no changes
|
||||
# ALWAYS_SHOW_ANNOUNCEMENT = true
|
||||
# address check REGEX, if not set, will not check
|
||||
# ADDRESS_CHECK_REGEX = "^(?!.*admin).*"
|
||||
# address name replace REGEX, if not set, the default is [^a-z0-9]
|
||||
|
||||
|
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 |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 38 KiB |
@@ -1,3 +1,17 @@
|
||||
# 初始化/更新 D1 数据库
|
||||
|
||||
## 创建数据库
|
||||
|
||||
打开 cloudflare 控制台,选择 `Workers & Pages` -> `D1` -> `Create Database`,点击创建数据库
|
||||
|
||||

|
||||
|
||||
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库,并获取到数据库的 `名称` 和 `数据库 ID`
|
||||
|
||||
## 初始化数据库
|
||||
|
||||
在部署完成后,在 admin 页面的 `快速设置` -> `数据库` 中,点击 `初始化数据库` 按钮来初始化数据库
|
||||
|
||||
## 更新数据库 schema
|
||||
|
||||
参考 [命令行更新 d1](/zh/guide/cli/d1) 或者 [用户界面更新 d1](/zh/guide/ui/d1)
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
::: warning 注意
|
||||
目前只支持 worker 和 pages 的部署。
|
||||
有问题请通过 `Github Issues` 反馈,感谢。
|
||||
|
||||
`worker.dev` 域名在中国无法访问,请自定义域名
|
||||
:::
|
||||
|
||||
## 部署步骤
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Cloudflare Worker 后端
|
||||
|
||||
> [!warning] 注意
|
||||
> `worker.dev` 域名在中国无法访问,请自定义域名
|
||||
|
||||
## 初始化项目
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 查看邮件 API
|
||||
|
||||
## 通过 HTTP API 查看邮件
|
||||
## 通过 邮件 API 查看邮件
|
||||
|
||||
这是一个 `python` 的例子,使用 `requests` 库查看邮件。
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
limit = 10
|
||||
offset = 0
|
||||
res = requests.get(
|
||||
f"http://localhost:8787/api/mails?limit={limit}&offset={offset}",
|
||||
f"https://<你的worker地址>/api/mails?limit={limit}&offset={offset}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {你的JWT密码}",
|
||||
# "x-custom-auth": "<你的网站密码>", # 如果启用了自定义密码
|
||||
@@ -16,3 +16,51 @@ res = requests.get(
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## admin 邮件 API
|
||||
|
||||
支持 `address` filter 和 `keyword` filter
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
url = "https://<你的worker地址>/admin/mails"
|
||||
|
||||
querystring = {
|
||||
"limit":"20",
|
||||
"offset":"0",
|
||||
# adress 和 keyword 为可选参数
|
||||
"address":"xxxx@awsl.uk",
|
||||
"keyword":"xxxx"
|
||||
}
|
||||
|
||||
headers = {"x-admin-auth": "<你的Admin密码>"}
|
||||
|
||||
response = requests.get(url, headers=headers, params=querystring)
|
||||
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
## user 邮件 API
|
||||
|
||||
支持 `address` filter 和 `keyword` filter
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
url = "https://<你的worker地址>/user_api/mails"
|
||||
|
||||
querystring = {
|
||||
"limit":"20",
|
||||
"offset":"0",
|
||||
# adress 和 keyword 为可选参数
|
||||
"address":"xxxx@awsl.uk",
|
||||
"keyword":"xxxx"
|
||||
}
|
||||
|
||||
headers = {"x-admin-auth": "<你的Admin密码>"}
|
||||
|
||||
response = requests.get(url, headers=headers, params=querystring)
|
||||
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
@@ -24,9 +24,9 @@ res = requests.post(
|
||||
print(res.json())
|
||||
```
|
||||
|
||||
# 批量创建随机用户名邮箱地址 API 示例
|
||||
## 批量创建随机用户名邮箱地址 API 示例
|
||||
|
||||
## 通过 admin API 批量新建邮箱地址
|
||||
### 通过 admin API 批量新建邮箱地址
|
||||
|
||||
这是一个 `python` 的例子,使用 `requests` 库发送邮件。
|
||||
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
# 初始化/更新 D1 数据库
|
||||
|
||||
## 初始化数据库
|
||||
## 创建数据库
|
||||
|
||||
打开 cloudflare 控制台,选择 `Workers & Pages` -> `D1` -> `Create Database`,点击创建数据库
|
||||
打开 cloudflare 控制台,选择 `Storage & Databases` -> `D1 SQL Database` -> `Create Database`,点击创建数据库
|
||||
|
||||

|
||||
|
||||
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
|
||||
|
||||
## 初始化数据库
|
||||
|
||||
::: warning 注意
|
||||
下面输入的是 `db/schema.sql` 的内容
|
||||
你也可以跳过初始化数据库,在部署完成后,在 admin 页面的 `快速设置` -> `数据库` 中,点击 `初始化数据库` 按钮来初始化数据库
|
||||
:::
|
||||
|
||||
打开 `Console` 标签页,输入 `db/schema.sql` 的内容,点击 `Execute` 执行
|
||||
打开 `Console` 标签页,输入仓库中 [db/schema.sql](https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/db/schema.sql) 文件的内容,点击 `Execute` 执行
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -42,11 +42,11 @@ const generate = async () => {
|
||||
}
|
||||
</script>
|
||||
|
||||
1. 点击 `Workers & Pages` -> `Overview` -> `Create Application`
|
||||
1. 点击 `Compute (Workers)` -> `Workers & Pages` -> `Create`
|
||||
|
||||

|
||||
|
||||
2. 选择 `Pages`,选择 `Create using direct upload`
|
||||
2. 选择 `Pages`,选择 `Use direct upload`
|
||||
|
||||

|
||||
|
||||
@@ -54,6 +54,9 @@ const generate = async () => {
|
||||
- 此处 worker 域名为后端 api 的域名,比如我部署在 `https://temp-email-api.awsl.uk`,则填写 `https://temp-email-api.awsl.uk`
|
||||
- 如果你的域名是 `https://temp-email-api.xxx.workers.dev`,则填写 `https://temp-email-api.xxx.workers.dev`
|
||||
|
||||
> [!warning] 注意
|
||||
> `worker.dev` 域名在中国无法访问,请自定义域名
|
||||
|
||||
<div :class="$style.container">
|
||||
<input :class="$style.input" type="text" v-model="domain" placeholder="请输入地址"></input>
|
||||
<button :class="$style.button" @click="generate">生成</button>
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
# Cloudflare workers 后端
|
||||
|
||||
1. 点击 `Workers & Pages` -> `Overview` -> `Create Application`
|
||||
> [!warning] 注意
|
||||
> `worker.dev` 域名在中国无法访问,请自定义域名
|
||||
|
||||
1. 点击 `Compute (Workers)` -> `Workers & Pages` -> `Create`
|
||||
|
||||

|
||||
|
||||
2. 选择 `Worker`,点击 `Create Worker`, 修改名称然后点击 `Deploy`
|
||||
|
||||

|
||||

|
||||
|
||||
3. 回到 `Overview`,找到刚刚创建的 worker,点击 `Settings` -> `Runtime`, 修改 `Compatibility flags`, 增加 `nodejs_compat`, 兼容日期也需要大于图片中的日期。
|
||||
3. 回到 `Workers & Pages`,找到刚刚创建的 worker,点击 `Settings` -> `Runtime`, 修改 `Compatibility flags`, 手动输入增加 `nodejs_compat`, 兼容日期也需要大于图片中的日期。
|
||||
|
||||

|
||||
|
||||
@@ -23,10 +27,11 @@
|
||||
>
|
||||
> 参考: [issues156](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/156#issuecomment-2079453822)
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
6. 点击 `Settings` -> `Variables`, 如图所示添加变量
|
||||
6. 点击 `Settings` -> `Variables and Secrets`, 如图所示添加变量
|
||||
|
||||

|
||||
|
||||
@@ -48,12 +53,16 @@
|
||||
| `ENABLE_USER_CREATE_EMAIL` | 文本/JSON | 是否允许用户创建邮箱, 不配置则不允许 | `true` |
|
||||
| `ENABLE_USER_DELETE_EMAIL` | 文本/JSON | 是否允许用户删除邮件, 不配置则不允许 | `true` |
|
||||
|
||||
7. 点击 `Settings` -> `Variables`, 下拉找到 `D1 Database`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 D1 数据库,点击 `Deploy`
|
||||
7. 点击 `Settings` -> `Bindings`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 D1 数据库,点击 `Add Binding`
|
||||
|
||||
> [!NOTE] 重要
|
||||
> 注意此处 `D1 Database` 的绑定名称必须为 `DB`
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
8. 点击 `Settings` -> `Trggers`, 这里可以添加自己的域名,你也可以使用自动生成的 `*.workers.dev` 的域名。记录下这个域名,后面部署前端会用到。
|
||||
|
||||
@@ -64,24 +73,35 @@
|
||||
|
||||

|
||||
|
||||
9. 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤,点击 `Workers & Pages` -> `KV` -> `Create Namespace`, 如图,点击 `Create Namespace`,然后在 `Settings` -> `Variables`, 下拉找到 `KV`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 `KV` 缓存,点击 `Deploy`
|
||||
9. 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤
|
||||
|
||||
> [!NOTE] 重要
|
||||
> 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤
|
||||
>
|
||||
> 注意此处 `KV` 的绑定名称必须为 `KV`
|
||||
|
||||
点击 `Storage & Databases` -> `KV` -> `Create Namespace`, 如图,点击 `Create Namespace`
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
然后点击 `Settings` -> `Bindings`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 KV,点击 `Add Binding`
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
10. Telegram Bot 配置
|
||||
|
||||
> [!NOTE]
|
||||
> 如果不需要 Telegram Bot, 可跳过此步骤
|
||||
|
||||
请先创建一个 Telegram Bot,然后获取 `token`,然后执行下面的命令,将 `token` 添加到 `Variables` 中, Name: `TELEGRAM_BOT_TOKEN`
|
||||
请先创建一个 Telegram Bot,然后获取 `token`,将 `token` 添加到 `Variables and Secrets` 中, 变量名称: `TELEGRAM_BOT_TOKEN`
|
||||
|
||||
11. 如果你想要使用 admin 页面中的定时任务清理邮件,需要到 `Settings` -> `Triggers` -> `Cron Triggers` 中添加定时任务.
|
||||
11. 如果你想要使用 admin 页面中的定时任务清理邮件,需要到 `Settings` -> `Trigger Events` -> `Cron Triggers` 中添加定时任务.
|
||||
|
||||
> [!NOTE]
|
||||
> 选择 `cron` 表达式,输入 `0 0 * * *`(此表达式表示每天午夜运行),点击 `Add` 增加。请根据您的需求调整此表达式。
|
||||
|
||||
@@ -22,17 +22,20 @@
|
||||
|
||||
## 邮箱相关变量
|
||||
|
||||
| 变量名 | 类型 | 说明 | 示例 |
|
||||
| ---------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
|
||||
| `PREFIX` | 文本 | 新建 `邮箱名称` 的默认前缀,不需要前缀可不配置 | `tmp` |
|
||||
| `MIN_ADDRESS_LEN` | 数字 | `邮箱名称` 的最小长度 | `1` |
|
||||
| `MAX_ADDRESS_LEN` | 数字 | `邮箱名称` 的最大长度 | `30` |
|
||||
| `ADDRESS_CHECK_REGEX` | 文本 | `邮箱名称` 的正则表达式, 只用于检查 | `^(?!.*admin).*` |
|
||||
| `ADDRESS_REGEX` | 文本 | `邮箱名称` 替换非法符号的正则表达式, 不在其中的符号将被替换,如果不设置,默认为 `[^a-z0-9]`, 需谨慎使用, 有些符号可能导致无法收件 | `[^a-z0-9]` |
|
||||
| `DEFAULT_DOMAINS` | JSON | 默认用户可用的域名(未登录或未分配角色的用户) | `["awsl.uk", "dreamhunter2333.xyz"]` |
|
||||
| `DOMAIN_LABELS` | JSON | 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称 | `["中文.awsl.uk", "dreamhunter2333.xyz"]` |
|
||||
| `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件 | `true` |
|
||||
| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额,如果不设置,将为 0 | `1` |
|
||||
| 变量名 | 类型 | 说明 | 示例 |
|
||||
| ------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
|
||||
| `PREFIX` | 文本 | 新建 `邮箱名称` 的默认前缀,不需要前缀可不配置 | `tmp` |
|
||||
| `MIN_ADDRESS_LEN` | 数字 | `邮箱名称` 的最小长度 | `1` |
|
||||
| `MAX_ADDRESS_LEN` | 数字 | `邮箱名称` 的最大长度 | `30` |
|
||||
| `DISABLE_CUSTOM_ADDRESS_NAME` | 文本/JSON | 禁用自定义邮箱地址名称,如果设置为 true,则用户无法输入自定义邮箱名称,将由后台自动生成 | `true` |
|
||||
| `ADDRESS_CHECK_REGEX` | 文本 | `邮箱名称` 的正则表达式, 只用于检查 | `^(?!.*admin).*` |
|
||||
| `ADDRESS_REGEX` | 文本 | `邮箱名称` 替换非法符号的正则表达式, 不在其中的符号将被替换,如果不设置,默认为 `[^a-z0-9]`, 需谨慎使用, 有些符号可能导致无法收件 | `[^a-z0-9]` |
|
||||
| `DEFAULT_DOMAINS` | JSON | 默认用户可用的域名(未登录或未分配角色的用户) | `["awsl.uk", "dreamhunter2333.xyz"]` |
|
||||
| `CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST` | 文本/JSON | 创建新地址时是否优先使用默认域名,如果设置为 true,当未指定域名时将使用第一个域名, 主要用于 telegram bot 场景 | `false` |
|
||||
| `DOMAIN_LABELS` | JSON | 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称 | `["中文.awsl.uk", "dreamhunter2333.xyz"]` |
|
||||
| `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件 | `true` |
|
||||
| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额,如果不设置,将为 0 | `1` |
|
||||
| `ENABLE_ADDRESS_PASSWORD` | 文本/JSON | 启用邮箱地址密码功能,启用后创建新地址时会自动生成密码,并支持密码登录和修改 | `true` |
|
||||
|
||||
## 接受邮件相关变量
|
||||
|
||||
@@ -88,16 +91,17 @@
|
||||
|
||||
## 网页相关变量
|
||||
|
||||
| 变量名 | 类型 | 说明 | 示例 |
|
||||
| ------------------------- | ----------- | ------------------------------------------------ | --------------------- |
|
||||
| `DEFAULT_LANG` | 文本 | Worker 错误信息默认语言, zh/en | `zh` |
|
||||
| `TITLE` | 文本 | 自定义前端页面网站标题,支持 html | `Custom Title` |
|
||||
| `ANNOUNCEMENT` | 文本 | 自定义前端页面公告,支持 html | `Custom Announcement` |
|
||||
| `COPYRIGHT` | 文本 | 自定义前端界面页脚文本,支持 html | `Dream Hunter` |
|
||||
| `ADMIN_CONTACT` | 文本 | admin 联系方式,可配置任意字符串, 不配置则不显示 | `xxx@gmail.com` |
|
||||
| `DISABLE_SHOW_GITHUB` | 文本/JSON | 是否显示 GitHub 链接 | `true` |
|
||||
| `CF_TURNSTILE_SITE_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` |
|
||||
| `CF_TURNSTILE_SECRET_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` |
|
||||
| 变量名 | 类型 | 说明 | 示例 |
|
||||
| -------------------------- | ----------- | ------------------------------------------------ | --------------------- |
|
||||
| `DEFAULT_LANG` | 文本 | Worker 错误信息默认语言, zh/en | `zh` |
|
||||
| `TITLE` | 文本 | 自定义前端页面网站标题,支持 html | `Custom Title` |
|
||||
| `ANNOUNCEMENT` | 文本 | 自定义前端页面公告,支持 html | `Custom Announcement` |
|
||||
| `ALWAYS_SHOW_ANNOUNCEMENT` | 文本/JSON | 是否总是显示公告(即使无更改), 默认 `false` | `true` |
|
||||
| `COPYRIGHT` | 文本 | 自定义前端界面页脚文本,支持 html | `Dream Hunter` |
|
||||
| `ADMIN_CONTACT` | 文本 | admin 联系方式,可配置任意字符串, 不配置则不显示 | `xxx@gmail.com` |
|
||||
| `DISABLE_SHOW_GITHUB` | 文本/JSON | 是否显示 GitHub 链接 | `true` |
|
||||
| `CF_TURNSTILE_SITE_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` |
|
||||
| `CF_TURNSTILE_SECRET_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` |
|
||||
|
||||
## Telegram Bot 相关变量
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "temp-mail-docs",
|
||||
"private": true,
|
||||
"version": "0.9.1",
|
||||
"version": "1.0.6",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.14.1",
|
||||
"vitepress": "^1.6.3",
|
||||
"wrangler": "^4.10.0"
|
||||
"@types/node": "^24.5.2",
|
||||
"vitepress": "^1.6.4",
|
||||
"wrangler": "^4.40.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vitepress dev docs",
|
||||
@@ -16,5 +16,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"jszip": "^3.10.1"
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "0.9.1",
|
||||
"version": "1.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -11,29 +11,31 @@
|
||||
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250412.0",
|
||||
"@cloudflare/workers-types": "^4.20250926.0",
|
||||
"@eslint/js": "9.18.0",
|
||||
"@simplewebauthn/types": "10.0.0",
|
||||
"@types/node": "^22.18.6",
|
||||
"eslint": "9.18.0",
|
||||
"globals": "^15.15.0",
|
||||
"typescript-eslint": "^8.29.1",
|
||||
"wrangler": "^4.10.0"
|
||||
"typescript-eslint": "^8.44.1",
|
||||
"wrangler": "^4.40.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.787.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.787.0",
|
||||
"@aws-sdk/client-s3": "3.888.0",
|
||||
"@aws-sdk/s3-request-presigner": "3.888.0",
|
||||
"@simplewebauthn/server": "10.0.1",
|
||||
"hono": "^4.7.6",
|
||||
"hono": "^4.9.8",
|
||||
"jsonpath-plus": "^10.3.0",
|
||||
"mimetext": "^3.0.27",
|
||||
"postal-mime": "^2.4.3",
|
||||
"resend": "^4.2.0",
|
||||
"postal-mime": "^2.4.4",
|
||||
"resend": "^4.8.0",
|
||||
"telegraf": "4.16.3",
|
||||
"worker-mailer": "^1.1.1"
|
||||
"worker-mailer": "^1.1.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"telegraf@4.16.3": "patches/telegraf@4.16.3.patch"
|
||||
}
|
||||
}
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
diff --git a/lib/core/network/client.js b/lib/core/network/client.js
|
||||
index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d698de5ce 100644
|
||||
index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..ab82c5391596ee2ed8dbe4128f3ba57a8cc998ca 100644
|
||||
--- a/lib/core/network/client.js
|
||||
+++ b/lib/core/network/client.js
|
||||
@@ -1,18 +1,18 @@
|
||||
@@ -37,7 +37,17 @@ index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d
|
||||
const path = __importStar(require("path"));
|
||||
const node_fetch_1 = __importDefault(require("node-fetch"));
|
||||
const check_1 = require("../helpers/check");
|
||||
@@ -61,10 +61,10 @@ const DEFAULT_OPTIONS = {
|
||||
@@ -39,7 +39,8 @@ const multipart_stream_1 = __importDefault(require("./multipart-stream"));
|
||||
const error_1 = __importDefault(require("./error"));
|
||||
const url_1 = require("url");
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
-const debug = require('debug')('telegraf:client');
|
||||
+// const debug = require('debug')('telegraf:client');
|
||||
+const debug = (msg) => console.debug(`[telegraf:client] ${msg}`);
|
||||
const { isStream } = multipart_stream_1.default;
|
||||
const WEBHOOK_REPLY_METHOD_ALLOWLIST = new Set([
|
||||
'answerCallbackQuery',
|
||||
@@ -61,10 +62,10 @@ const DEFAULT_OPTIONS = {
|
||||
apiRoot: 'https://api.telegram.org',
|
||||
apiMode: 'bot',
|
||||
webhookReply: true,
|
||||
@@ -52,7 +62,7 @@ index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d
|
||||
attachmentAgent: undefined,
|
||||
testEnv: false,
|
||||
};
|
||||
@@ -112,9 +112,9 @@ async function buildFormDataConfig(payload, agent) {
|
||||
@@ -112,9 +113,9 @@ async function buildFormDataConfig(payload, agent) {
|
||||
}
|
||||
const boundary = crypto.randomBytes(32).toString('hex');
|
||||
const formData = new multipart_stream_1.default(boundary);
|
||||
@@ -65,7 +75,7 @@ index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d
|
||||
return {
|
||||
method: 'POST',
|
||||
compress: true,
|
||||
@@ -205,14 +205,15 @@ async function attachFormMedia(form, media, id, agent) {
|
||||
@@ -205,14 +206,15 @@ async function attachFormMedia(form, media, id, agent) {
|
||||
if ('source' in media && media.source) {
|
||||
let mediaSource = media.source;
|
||||
if (typeof media.source === 'string') {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Context } from "hono";
|
||||
import { handleListQuery } from "../common";
|
||||
|
||||
export default {
|
||||
getMails: async (c: Context<HonoCustomType>) => {
|
||||
const { address, limit, offset, keyword } = c.req.query();
|
||||
const addressQuery = address ? `address = ?` : "";
|
||||
const addressParams = address ? [address] : [];
|
||||
const keywordQuery = keyword ? `raw like ?` : "";
|
||||
const keywordParams = keyword ? [`%${keyword}%`] : [];
|
||||
const filterQuerys = [addressQuery, keywordQuery].filter((item) => item).join(" and ");
|
||||
const finalQuery = filterQuerys.length > 0 ? `where ${filterQuerys}` : "";
|
||||
const filterParams = [...addressParams, ...keywordParams]
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails ${finalQuery}`,
|
||||
`SELECT count(*) as count FROM raw_mails ${finalQuery}`,
|
||||
filterParams, limit, offset
|
||||
);
|
||||
},
|
||||
getUnknowMails: async (c: Context<HonoCustomType>) => {
|
||||
const { limit, offset } = c.req.query();
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails where address NOT IN (select name from address) `,
|
||||
`SELECT count(*) as count FROM raw_mails`
|
||||
+ ` where address NOT IN (select name from address) `,
|
||||
[], limit, offset
|
||||
);
|
||||
},
|
||||
deleteMail: async (c: Context<HonoCustomType>) => {
|
||||
const { id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE id = ? `
|
||||
).bind(id).run();
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils';
|
||||
import { UserSettings, GeoData, UserInfo } from "../models";
|
||||
import { handleListQuery } from '../common'
|
||||
import { HonoCustomType } from '../types';
|
||||
import UserBindAddressModule from '../user_api/bind_address';
|
||||
import i18n from '../i18n';
|
||||
|
||||
export default {
|
||||
getSetting: async (c: Context<HonoCustomType>) => {
|
||||
@@ -90,7 +90,8 @@ export default {
|
||||
},
|
||||
deleteUser: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id } = c.req.param();
|
||||
if (!user_id) return c.text("Invalid user_id", 400);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!user_id) return c.text(msgs.UserNotFoundMsg, 400);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM users WHERE id = ?`
|
||||
).bind(user_id).run();
|
||||
@@ -105,7 +106,8 @@ export default {
|
||||
resetPassword: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id } = c.req.param();
|
||||
const { password } = await c.req.json();
|
||||
if (!user_id) return c.text("Invalid user_id", 400);
|
||||
const msgs = i18n.getMessagesbyContext(c);
|
||||
if (!user_id) return c.text(msgs.UserNotFoundMsg, 400);
|
||||
try {
|
||||
checkUserPassword(password);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
@@ -146,11 +148,22 @@ export default {
|
||||
return c.json({ success: true })
|
||||
},
|
||||
bindAddress: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id, address_id } = await c.req.json();
|
||||
return await UserBindAddressModule.bindByID(c, user_id, address_id);
|
||||
const {
|
||||
user_email, address, user_id, address_id
|
||||
} = await c.req.json();
|
||||
const db_user_id = user_id ?? await c.env.DB.prepare(
|
||||
`SELECT id FROM users WHERE user_email = ?`
|
||||
).bind(user_email).first<number | undefined | null>("id");
|
||||
const db_address_id = address_id ?? await c.env.DB.prepare(
|
||||
`SELECT id FROM address WHERE name = ?`
|
||||
).bind(address).first<number | undefined | null>("id");
|
||||
return await UserBindAddressModule.bindByID(c, db_user_id, db_address_id);
|
||||
},
|
||||
getBindedAddresses: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id } = c.req.param();
|
||||
return await UserBindAddressModule.getBindedAddressesById(c, user_id);
|
||||
const results = await UserBindAddressModule.getBindedAddressesById(c, user_id);
|
||||
return c.json({
|
||||
results: results,
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import { cleanup } from '../common';
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting } from '../utils';
|
||||
import { CleanupSettings } from '../models';
|
||||
import { HonoCustomType } from '../types';
|
||||
|
||||
export default {
|
||||
cleanup: async (c: Context<HonoCustomType>) => {
|
||||
@@ -18,13 +17,11 @@ export default {
|
||||
return c.json({ success: true })
|
||||
},
|
||||
getCleanup: async (c: Context<HonoCustomType>) => {
|
||||
const value = await getJsonSetting(c, CONSTANTS.AUTO_CLEANUP_KEY);
|
||||
const cleanupSetting = new CleanupSettings(value);
|
||||
const cleanupSetting = await getJsonSetting<CleanupSettings>(c, CONSTANTS.AUTO_CLEANUP_KEY);
|
||||
return c.json(cleanupSetting)
|
||||
},
|
||||
saveCleanup: async (c: Context<HonoCustomType>) => {
|
||||
const value = await c.req.json();
|
||||
const cleanupSetting = new CleanupSettings(value);
|
||||
const cleanupSetting = await c.req.json<CleanupSettings>();
|
||||
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting));
|
||||
return c.json({ success: true })
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { Context } from "hono";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import utils from "../utils";
|
||||
|
||||
const DB_INIT_QUERIES = `
|
||||
CREATE TABLE IF NOT EXISTS raw_mails (
|
||||
id INTEGER PRIMARY KEY,
|
||||
message_id TEXT,
|
||||
source TEXT,
|
||||
address TEXT,
|
||||
raw TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
||||
|
||||
CREATE 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,
|
||||
name TEXT,
|
||||
address TEXT UNIQUE,
|
||||
subject TEXT,
|
||||
message TEXT,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_auto_reply_mails_address ON auto_reply_mails(address);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS address_sender (
|
||||
id INTEGER PRIMARY KEY,
|
||||
address TEXT UNIQUE,
|
||||
balance INTEGER DEFAULT 0,
|
||||
enabled INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_address_sender_address ON address_sender(address);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sendbox (
|
||||
id INTEGER PRIMARY KEY,
|
||||
address TEXT,
|
||||
raw TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
|
||||
CREATE INDEX IF NOT EXISTS idx_sendbox_created_at ON sendbox(created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_email TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
user_info TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_user_email ON users(user_email);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users_address (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER,
|
||||
address_id INTEGER UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER UNIQUE NOT NULL,
|
||||
role_text TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_passkeys (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL,
|
||||
passkey_name TEXT NOT NULL,
|
||||
passkey_id TEXT NOT NULL,
|
||||
passkey TEXT NOT NULL,
|
||||
counter INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_passkeys_user_id ON user_passkeys(user_id);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_passkeys_user_id_passkey_id ON user_passkeys(user_id, passkey_id);
|
||||
`
|
||||
|
||||
export default {
|
||||
initialize: async (c: Context<HonoCustomType>) => {
|
||||
// remove all \r and \n characters from the query string
|
||||
// split by ; and join with a ;\n
|
||||
const query = DB_INIT_QUERIES.replace(/[\r\n]/g, "")
|
||||
.split(";")
|
||||
.map((query) => query.trim())
|
||||
.join(";\n");
|
||||
await c.env.DB.exec(query);
|
||||
|
||||
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||
if (version) {
|
||||
return c.json({ message: "Database already initialized" });
|
||||
}
|
||||
await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
|
||||
return c.json({ message: "Database initialized" });
|
||||
},
|
||||
migrate: async (c: Context<HonoCustomType>) => {
|
||||
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||
if (version == "v0.0.2") {
|
||||
// example migration from v0.0.2 to v0.0.3
|
||||
const query = `ALTER TABLE address ADD password TEXT;`
|
||||
await c.env.DB.exec(query);
|
||||
}
|
||||
if (version != CONSTANTS.DB_VERSION) {
|
||||
// TODO: Perform migration logic here
|
||||
// remove all \r and \n characters from the query string
|
||||
// split by ; and join with a ;\n
|
||||
const query = DB_INIT_QUERIES.replace(/[\r\n]/g, "")
|
||||
.split(";")
|
||||
.map((query) => query.trim())
|
||||
.join(";\n");
|
||||
await c.env.DB.exec(query);
|
||||
// Update the version in the settings table
|
||||
await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
|
||||
return c.json({
|
||||
success: true,
|
||||
message: "Database migrated"
|
||||
});
|
||||
}
|
||||
return c.json({
|
||||
success: true,
|
||||
message: "Database does not need migration"
|
||||
});
|
||||
},
|
||||
getVersion: async (c: Context<HonoCustomType>) => {
|
||||
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||
return c.json({
|
||||
need_initialization: !version,
|
||||
need_migration: version && version != CONSTANTS.DB_VERSION,
|
||||
current_db_version: version,
|
||||
code_db_version: CONSTANTS.DB_VERSION
|
||||
});
|
||||
},
|
||||
}
|
||||
@@ -2,8 +2,7 @@ import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import i18n from '../i18n'
|
||||
import { HonoCustomType } from '../types'
|
||||
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles } from '../utils'
|
||||
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles, getBooleanValue, hashPassword } from '../utils'
|
||||
import { newAddress, handleListQuery } from '../common'
|
||||
import { CONSTANTS } from '../constants'
|
||||
import cleanup_api from './cleanup_api'
|
||||
@@ -12,7 +11,10 @@ import webhook_settings from './webhook_settings'
|
||||
import mail_webhook_settings from './mail_webhook_settings'
|
||||
import oauth2_settings from './oauth2_settings'
|
||||
import worker_config from './worker_config'
|
||||
import admin_mail_api from './admin_mail_api'
|
||||
import { sendMailbyAdmin } from './send_mail'
|
||||
import db_api from './db_api'
|
||||
import { EmailRuleSettings } from '../models'
|
||||
|
||||
export const api = new Hono<HonoCustomType>()
|
||||
|
||||
@@ -54,6 +56,7 @@ api.post('/admin/new_address', async (c) => {
|
||||
checkAllowDomains: false,
|
||||
enableCheckNameRegex: false,
|
||||
});
|
||||
|
||||
return c.json(res);
|
||||
} catch (e) {
|
||||
return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400)
|
||||
@@ -87,6 +90,34 @@ api.delete('/admin/delete_address/:id', async (c) => {
|
||||
})
|
||||
})
|
||||
|
||||
api.delete('/admin/clear_inbox/:id', async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE address IN`
|
||||
+ ` (select name from address where id = ?) `
|
||||
).bind(id).run();
|
||||
if (!mailSuccess) {
|
||||
return c.text("Failed to clear inbox", 500)
|
||||
}
|
||||
return c.json({
|
||||
success: mailSuccess
|
||||
})
|
||||
})
|
||||
|
||||
api.delete('/admin/clear_sent_items/:id', async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const { success: sendboxSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM sendbox WHERE address IN`
|
||||
+ ` (select name from address where id = ?) `
|
||||
).bind(id).run();
|
||||
if (!sendboxSuccess) {
|
||||
return c.text("Failed to clear sent items", 500)
|
||||
}
|
||||
return c.json({
|
||||
success: sendboxSuccess
|
||||
})
|
||||
})
|
||||
|
||||
api.get('/admin/show_password/:id', async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const name = await c.env.DB.prepare(
|
||||
@@ -101,55 +132,35 @@ api.get('/admin/show_password/:id', async (c) => {
|
||||
})
|
||||
})
|
||||
|
||||
api.get('/admin/mails', async (c) => {
|
||||
const { address, limit, offset, keyword } = c.req.query();
|
||||
if (address && keyword) {
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails where address = ? and raw like ? `,
|
||||
`SELECT count(*) as count FROM raw_mails where address = ? and raw like ? `,
|
||||
[address, `%${keyword}%`], limit, offset
|
||||
);
|
||||
} else if (keyword) {
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails where raw like ? `,
|
||||
`SELECT count(*) as count FROM raw_mails where raw like ? `,
|
||||
[`%${keyword}%`], limit, offset
|
||||
);
|
||||
} else if (address) {
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails where address = ? `,
|
||||
`SELECT count(*) as count FROM raw_mails where address = ? `,
|
||||
[address], limit, offset
|
||||
);
|
||||
} else {
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails `,
|
||||
`SELECT count(*) as count FROM raw_mails `,
|
||||
[], limit, offset
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/admin/mails_unknow', async (c) => {
|
||||
const { limit, offset } = c.req.query();
|
||||
return await handleListQuery(c,
|
||||
`SELECT * FROM raw_mails where address NOT IN (select name from address) `,
|
||||
`SELECT count(*) as count FROM raw_mails`
|
||||
+ ` where address NOT IN (select name from address) `,
|
||||
[], limit, offset
|
||||
);
|
||||
});
|
||||
|
||||
api.delete('/admin/mails/:id', async (c) => {
|
||||
api.post('/admin/address/:id/reset_password', async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const { password } = await c.req.json();
|
||||
// 检查功能是否启用
|
||||
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
||||
return c.text("Password management is disabled", 403);
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
return c.text("Password is required", 400);
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE id = ? `
|
||||
).bind(id).run();
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE id = ?`
|
||||
).bind(hashedPassword, id).run();
|
||||
|
||||
if (!success) {
|
||||
return c.text("Failed to reset password", 500);
|
||||
}
|
||||
|
||||
return c.json({ success: true });
|
||||
})
|
||||
|
||||
// mail api
|
||||
api.get('/admin/mails', admin_mail_api.getMails);
|
||||
api.get('/admin/mails_unknow', admin_mail_api.getUnknowMails);
|
||||
api.delete('/admin/mails/:id', admin_mail_api.deleteMail)
|
||||
|
||||
api.get('/admin/address_sender', async (c) => {
|
||||
const { address, limit, offset } = c.req.query();
|
||||
if (address) {
|
||||
@@ -260,13 +271,15 @@ api.get('/admin/account_settings', async (c) => {
|
||||
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
|
||||
const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY);
|
||||
const fromBlockList = c.env.KV ? await c.env.KV.get<string[]>(CONSTANTS.EMAIL_KV_BLACK_LIST, 'json') : [];
|
||||
const emailRuleSettings = await getJsonSetting<EmailRuleSettings>(c, CONSTANTS.EMAIL_RULE_SETTINGS_KEY);
|
||||
const noLimitSendAddressList = await getJsonSetting(c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY);
|
||||
return c.json({
|
||||
blockList: blockList || [],
|
||||
sendBlockList: sendBlockList || [],
|
||||
verifiedAddressList: verifiedAddressList || [],
|
||||
fromBlockList: fromBlockList || [],
|
||||
noLimitSendAddressList: noLimitSendAddressList || []
|
||||
noLimitSendAddressList: noLimitSendAddressList || [],
|
||||
emailRuleSettings: emailRuleSettings || {}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -278,7 +291,7 @@ api.post('/admin/account_settings', async (c) => {
|
||||
/** @type {{ blockList: Array<string>, sendBlockList: Array<string> }} */
|
||||
const {
|
||||
blockList, sendBlockList, noLimitSendAddressList,
|
||||
verifiedAddressList, fromBlockList
|
||||
verifiedAddressList, fromBlockList, emailRuleSettings
|
||||
} = await c.req.json();
|
||||
if (!blockList || !sendBlockList || !verifiedAddressList) {
|
||||
return c.text("Invalid blockList or sendBlockList", 400)
|
||||
@@ -308,6 +321,10 @@ api.post('/admin/account_settings', async (c) => {
|
||||
c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY,
|
||||
JSON.stringify(noLimitSendAddressList || [])
|
||||
)
|
||||
await saveSetting(
|
||||
c, CONSTANTS.EMAIL_RULE_SETTINGS_KEY,
|
||||
JSON.stringify(emailRuleSettings || {})
|
||||
)
|
||||
return c.json({
|
||||
success: true
|
||||
})
|
||||
@@ -348,3 +365,8 @@ api.get("/admin/worker/configs", worker_config.getConfig);
|
||||
|
||||
// send mail by admin
|
||||
api.post("/admin/send_mail", sendMailbyAdmin);
|
||||
|
||||
// db api
|
||||
api.get('admin/db_version', db_api.getVersion);
|
||||
api.post('admin/db_initialize', db_api.initialize);
|
||||
api.post('admin/db_migration', db_api.migrate);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Context } from "hono";
|
||||
import { HonoCustomType, ParsedEmailContext } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { WebhookSettings } from "../models";
|
||||
import { commonParseMail, sendWebhook } from "../common";
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Context } from 'hono';
|
||||
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { UserOauth2Settings } from "../models";
|
||||
import { HonoCustomType } from '../types';
|
||||
import { getJsonSetting, saveSetting } from '../utils';
|
||||
|
||||
async function getUserOauth2Settings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Context } from "hono";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { sendMail } from "../mails_api/send_mail_api";
|
||||
|
||||
export const sendMailbyAdmin = async (c: Context<HonoCustomType>) => {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Context } from "hono";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { AdminWebhookSettings } from "../models";
|
||||
|
||||
async function getWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const settings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
||||
return c.json(settings || new AdminWebhookSettings([]));
|
||||
return c.json(settings || new AdminWebhookSettings(false, []));
|
||||
}
|
||||
|
||||
async function saveWebhookSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Context } from 'hono';
|
||||
|
||||
import { HonoCustomType } from '../types';
|
||||
import { getAdminPasswords, getBooleanValue, getDefaultDomains, getDomains, getIntValue, getPasswords, getStringArray, getStringValue, getUserRoles, getAnotherWorkerList, getSplitStringListValue } from '../utils';
|
||||
import utils from '../utils';
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { isS3Enabled } from '../mails_api/s3_attachment';
|
||||
|
||||
@@ -10,48 +9,50 @@ export default {
|
||||
return c.json({
|
||||
"DEFAULT_LANG": c.env.DEFAULT_LANG,
|
||||
"TITLE": c.env.TITLE,
|
||||
"HAS_PASSWORD": getPasswords(c).length,
|
||||
"HAS_ADMIN_PASSWORDS": getAdminPasswords(c).length,
|
||||
"ANNOUNCEMENT": getStringValue(c.env.ANNOUNCEMENT),
|
||||
"HAS_PASSWORD": utils.getPasswords(c).length,
|
||||
"HAS_ADMIN_PASSWORDS": utils.getAdminPasswords(c).length,
|
||||
"ANNOUNCEMENT": utils.getStringValue(c.env.ANNOUNCEMENT),
|
||||
"ALWAYS_SHOW_ANNOUNCEMENT": utils.getBooleanValue(c.env.ALWAYS_SHOW_ANNOUNCEMENT),
|
||||
|
||||
"PREFIX": getStringValue(c.env.PREFIX),
|
||||
"ADDRESS_CHECK_REGEX": getStringValue(c.env.ADDRESS_CHECK_REGEX),
|
||||
"ADDRESS_REGEX": getStringValue(c.env.ADDRESS_REGEX),
|
||||
"MIN_ADDRESS_LEN": getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||
"MAX_ADDRESS_LEN": getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||
"PREFIX": utils.getStringValue(c.env.PREFIX),
|
||||
"ADDRESS_CHECK_REGEX": utils.getStringValue(c.env.ADDRESS_CHECK_REGEX),
|
||||
"ADDRESS_REGEX": utils.getStringValue(c.env.ADDRESS_REGEX),
|
||||
"MIN_ADDRESS_LEN": utils.getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||
"MAX_ADDRESS_LEN": utils.getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||
|
||||
"FORWARD_ADDRESS_LIST": getStringArray(c.env.FORWARD_ADDRESS_LIST),
|
||||
"DEFAULT_DOMAINS": getDefaultDomains(c),
|
||||
"DOMAINS": getDomains(c),
|
||||
"DOMAIN_LABELS": getStringArray(c.env.DOMAIN_LABELS),
|
||||
"FORWARD_ADDRESS_LIST": utils.getStringArray(c.env.FORWARD_ADDRESS_LIST),
|
||||
"SUBDOMAIN_FORWARD_ADDRESS_LIST": utils.getJsonObjectValue<SubdomainForwardAddressList[]>(c.env.SUBDOMAIN_FORWARD_ADDRESS_LIST),
|
||||
"DEFAULT_DOMAINS": utils.getDefaultDomains(c),
|
||||
"DOMAINS": utils.getDomains(c),
|
||||
"DOMAIN_LABELS": utils.getStringArray(c.env.DOMAIN_LABELS),
|
||||
|
||||
"HAS_JWT_SECRET": !!getStringValue(c.env.JWT_SECRET),
|
||||
"HAS_JWT_SECRET": !!utils.getStringValue(c.env.JWT_SECRET),
|
||||
|
||||
"ADMIN_USER_ROLE": getStringValue(c.env.ADMIN_USER_ROLE),
|
||||
"USER_DEFAULT_ROLE": getStringValue(c.env.USER_DEFAULT_ROLE),
|
||||
"USER_ROLES": getUserRoles(c),
|
||||
"NO_LIMIT_SEND_ROLE": getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE),
|
||||
"ADMIN_USER_ROLE": utils.getStringValue(c.env.ADMIN_USER_ROLE),
|
||||
"USER_DEFAULT_ROLE": utils.getStringValue(c.env.USER_DEFAULT_ROLE),
|
||||
"USER_ROLES": utils.getUserRoles(c),
|
||||
"NO_LIMIT_SEND_ROLE": utils.getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE),
|
||||
|
||||
"ADMIN_CONTACT": c.env.ADMIN_CONTACT,
|
||||
"ENABLE_USER_CREATE_EMAIL": getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
|
||||
"DISABLE_ANONYMOUS_USER_CREATE_EMAIL": getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
|
||||
"ENABLE_USER_DELETE_EMAIL": getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
||||
"ENABLE_AUTO_REPLY": getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
||||
"ENABLE_USER_CREATE_EMAIL": utils.getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
|
||||
"DISABLE_ANONYMOUS_USER_CREATE_EMAIL": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
|
||||
"ENABLE_USER_DELETE_EMAIL": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
||||
"ENABLE_AUTO_REPLY": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
||||
"COPYRIGHT": c.env.COPYRIGHT,
|
||||
"ENABLE_WEBHOOK": getBooleanValue(c.env.ENABLE_WEBHOOK),
|
||||
"ENABLE_WEBHOOK": utils.getBooleanValue(c.env.ENABLE_WEBHOOK),
|
||||
"S3_ENABLED": isS3Enabled(c),
|
||||
"VERSION": CONSTANTS.VERSION,
|
||||
"DISABLE_SHOW_GITHUB": !getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
||||
"DISABLE_ADMIN_PASSWORD_CHECK": getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK),
|
||||
"ENABLE_CHECK_JUNK_MAIL": getBooleanValue(c.env.ENABLE_CHECK_JUNK_MAIL),
|
||||
"JUNK_MAIL_CHECK_LIST": getStringArray(c.env.JUNK_MAIL_CHECK_LIST),
|
||||
"JUNK_MAIL_FORCE_PASS_LIST": getStringArray(c.env.JUNK_MAIL_FORCE_PASS_LIST),
|
||||
"DISABLE_SHOW_GITHUB": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
||||
"DISABLE_ADMIN_PASSWORD_CHECK": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK),
|
||||
"ENABLE_CHECK_JUNK_MAIL": utils.getBooleanValue(c.env.ENABLE_CHECK_JUNK_MAIL),
|
||||
"JUNK_MAIL_CHECK_LIST": utils.getStringArray(c.env.JUNK_MAIL_CHECK_LIST),
|
||||
"JUNK_MAIL_FORCE_PASS_LIST": utils.getStringArray(c.env.JUNK_MAIL_FORCE_PASS_LIST),
|
||||
|
||||
"REMOVE_EXCEED_SIZE_ATTACHMENT": getBooleanValue(c.env.REMOVE_EXCEED_SIZE_ATTACHMENT),
|
||||
"REMOVE_ALL_ATTACHMENT": getBooleanValue(c.env.REMOVE_ALL_ATTACHMENT),
|
||||
"REMOVE_EXCEED_SIZE_ATTACHMENT": utils.getBooleanValue(c.env.REMOVE_EXCEED_SIZE_ATTACHMENT),
|
||||
"REMOVE_ALL_ATTACHMENT": utils.getBooleanValue(c.env.REMOVE_ALL_ATTACHMENT),
|
||||
|
||||
"ENABLE_ANOTHER_WORKER": getBooleanValue(c.env.ENABLE_ANOTHER_WORKER),
|
||||
"ANOTHER_WORKER_LIST": getAnotherWorkerList(c),
|
||||
"ENABLE_ANOTHER_WORKER": utils.getBooleanValue(c.env.ENABLE_ANOTHER_WORKER),
|
||||
"ANOTHER_WORKER_LIST": utils.getAnotherWorkerList(c),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import { getDomains, getPasswords, getBooleanValue, getIntValue, getStringArray, getDefaultDomains, getStringValue } from './utils';
|
||||
import utils from './utils';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { HonoCustomType } from './types';
|
||||
import { isS3Enabled } from './mails_api/s3_attachment';
|
||||
|
||||
const api = new Hono<HonoCustomType>
|
||||
@@ -10,35 +9,39 @@ const api = new Hono<HonoCustomType>
|
||||
api.get('/open_api/settings', async (c) => {
|
||||
// check header x-custom-auth
|
||||
let needAuth = false;
|
||||
const passwords = getPasswords(c);
|
||||
const passwords = utils.getPasswords(c);
|
||||
if (passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
needAuth = !auth || !passwords.includes(auth);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
"title": c.env.TITLE,
|
||||
"announcement": getStringValue(c.env.ANNOUNCEMENT),
|
||||
"prefix": getStringValue(c.env.PREFIX),
|
||||
"addressRegex": getStringValue(c.env.ADDRESS_REGEX),
|
||||
"minAddressLen": getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||
"maxAddressLen": getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||
"defaultDomains": getDefaultDomains(c),
|
||||
"domains": getDomains(c),
|
||||
"domainLabels": getStringArray(c.env.DOMAIN_LABELS),
|
||||
"announcement": utils.getStringValue(c.env.ANNOUNCEMENT),
|
||||
"alwaysShowAnnouncement": utils.getBooleanValue(c.env.ALWAYS_SHOW_ANNOUNCEMENT),
|
||||
"prefix": utils.getStringValue(c.env.PREFIX),
|
||||
"addressRegex": utils.getStringValue(c.env.ADDRESS_REGEX),
|
||||
"minAddressLen": utils.getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||
"maxAddressLen": utils.getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||
"defaultDomains": utils.getDefaultDomains(c),
|
||||
"domains": utils.getDomains(c),
|
||||
"domainLabels": utils.getStringArray(c.env.DOMAIN_LABELS),
|
||||
"needAuth": needAuth,
|
||||
"adminContact": c.env.ADMIN_CONTACT,
|
||||
"enableUserCreateEmail": getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
|
||||
"disableAnonymousUserCreateEmail": getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
|
||||
"enableUserDeleteEmail": getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
||||
"enableAutoReply": getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
||||
"enableIndexAbout": getBooleanValue(c.env.ENABLE_INDEX_ABOUT),
|
||||
"enableUserCreateEmail": utils.getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
|
||||
"disableAnonymousUserCreateEmail": utils.getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
|
||||
"disableCustomAddressName": utils.getBooleanValue(c.env.DISABLE_CUSTOM_ADDRESS_NAME),
|
||||
"enableUserDeleteEmail": utils.getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
||||
"enableAutoReply": utils.getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
||||
"enableIndexAbout": utils.getBooleanValue(c.env.ENABLE_INDEX_ABOUT),
|
||||
"copyright": c.env.COPYRIGHT,
|
||||
"cfTurnstileSiteKey": c.env.CF_TURNSTILE_SITE_KEY,
|
||||
"enableWebhook": getBooleanValue(c.env.ENABLE_WEBHOOK),
|
||||
"enableWebhook": utils.getBooleanValue(c.env.ENABLE_WEBHOOK),
|
||||
"isS3Enabled": isS3Enabled(c),
|
||||
"version": CONSTANTS.VERSION,
|
||||
"showGithub": !getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
||||
"disableAdminPasswordCheck": getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK)
|
||||
"showGithub": !utils.getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
|
||||
"disableAdminPasswordCheck": utils.getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK),
|
||||
"enableAddressPassword": utils.getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
@@ -1,14 +1,38 @@
|
||||
import { Context } from 'hono';
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList } from './utils';
|
||||
import { HonoCustomType, UserRole, AnotherWorker, RPCEmailMessage, ParsedEmailContext } from './types';
|
||||
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList, hashPassword } from './utils';
|
||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
|
||||
|
||||
const DEFAULT_NAME_REGEX = /[^a-z0-9]/g;
|
||||
|
||||
export const generateRandomName = (c: Context<HonoCustomType>): string => {
|
||||
// name min length min 1
|
||||
const minLength = Math.max(
|
||||
getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||
1
|
||||
);
|
||||
// name max length min 1
|
||||
const maxLength = Math.max(
|
||||
getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||
1
|
||||
);
|
||||
|
||||
// Build full name recursively until minimum length is reached
|
||||
const buildName = (currentName: string = ""): string => {
|
||||
return currentName.length >= minLength
|
||||
? currentName
|
||||
: buildName(currentName + Math.random().toString(36).substring(2, 15));
|
||||
};
|
||||
|
||||
const fullName = buildName();
|
||||
|
||||
// Return truncated to max length
|
||||
return fullName.substring(0, Math.min(fullName.length, maxLength));
|
||||
};
|
||||
|
||||
const checkNameRegex = (c: Context<HonoCustomType>, name: string) => {
|
||||
let error = null;
|
||||
try {
|
||||
@@ -41,6 +65,54 @@ const getNameRegex = (c: Context<HonoCustomType>): RegExp => {
|
||||
return DEFAULT_NAME_REGEX;
|
||||
}
|
||||
|
||||
export async function updateAddressUpdatedAt(
|
||||
c: Context<HonoCustomType>,
|
||||
address: string | undefined | null
|
||||
): Promise<void> {
|
||||
if (!address) {
|
||||
return;
|
||||
}
|
||||
// update address updated_at
|
||||
try {
|
||||
await c.env.DB.prepare(
|
||||
`UPDATE address SET updated_at = datetime('now') where name = ?`
|
||||
).bind(address).run();
|
||||
} catch (e) {
|
||||
console.warn("Failed to update address updated_at", e);
|
||||
}
|
||||
}
|
||||
|
||||
export const generateRandomPassword = (): string => {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let password = "";
|
||||
for (let i = 0; i < 8; i++) {
|
||||
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||
}
|
||||
return password;
|
||||
}
|
||||
|
||||
const generatePasswordForAddress = async (
|
||||
c: Context<HonoCustomType>,
|
||||
address: string
|
||||
): Promise<string | null> => {
|
||||
if (!getBooleanValue(c.env.ENABLE_ADDRESS_PASSWORD)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const plainPassword = generateRandomPassword();
|
||||
const hashedPassword = await hashPassword(plainPassword);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`UPDATE address SET password = ?, updated_at = datetime('now') WHERE name = ?`
|
||||
).bind(hashedPassword, address).run();
|
||||
|
||||
if (!success) {
|
||||
console.warn("Failed to set generated password for address:", address);
|
||||
return null;
|
||||
}
|
||||
|
||||
return plainPassword;
|
||||
}
|
||||
|
||||
export const newAddress = async (
|
||||
c: Context<HonoCustomType>,
|
||||
{
|
||||
@@ -59,9 +131,9 @@ export const newAddress = async (
|
||||
checkAllowDomains?: boolean,
|
||||
enableCheckNameRegex?: boolean,
|
||||
}
|
||||
): Promise<{ address: string, jwt: string }> => {
|
||||
// remove special characters
|
||||
name = name.replace(getNameRegex(c), '')
|
||||
): Promise<{ address: string, jwt: string, password?: string | null }> => {
|
||||
// trim whitespace and remove special characters
|
||||
name = name.trim().replace(getNameRegex(c), '')
|
||||
// check name
|
||||
if (enableCheckNameRegex) {
|
||||
await checkNameBlockList(c, name);
|
||||
@@ -86,15 +158,20 @@ export const newAddress = async (
|
||||
}
|
||||
// create address with prefix
|
||||
if (typeof addressPrefix === "string") {
|
||||
name = addressPrefix + name;
|
||||
name = addressPrefix.trim() + name;
|
||||
} else if (enablePrefix) {
|
||||
name = getStringValue(c.env.PREFIX) + name;
|
||||
name = getStringValue(c.env.PREFIX).trim() + name;
|
||||
}
|
||||
// check domain
|
||||
const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c);
|
||||
// if domain is not set, use the first domain
|
||||
// if domain is not set, select domain based on environment configuration
|
||||
if (!domain && allowDomains.length > 0) {
|
||||
domain = allowDomains[0];
|
||||
const createAddressDefaultDomainFirst = getBooleanValue(c.env.CREATE_ADDRESS_DEFAULT_DOMAIN_FIRST);
|
||||
if (createAddressDefaultDomainFirst) {
|
||||
domain = allowDomains[0];
|
||||
} else {
|
||||
domain = allowDomains[Math.floor(Math.random() * allowDomains.length)];
|
||||
}
|
||||
}
|
||||
// check domain is valid
|
||||
if (!domain || !allowDomains.includes(domain)) {
|
||||
@@ -109,6 +186,7 @@ export const newAddress = async (
|
||||
if (!success) {
|
||||
throw new Error("Failed to create address")
|
||||
}
|
||||
await updateAddressUpdatedAt(c, name);
|
||||
} catch (e) {
|
||||
const message = (e as Error).message;
|
||||
if (message && message.includes("UNIQUE")) {
|
||||
@@ -119,6 +197,10 @@ export const newAddress = async (
|
||||
const address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where name = ?`
|
||||
).bind(name).first<number>("id");
|
||||
|
||||
// 如果启用地址密码功能,自动生成密码
|
||||
const generatedPassword = await generatePasswordForAddress(c, name);
|
||||
|
||||
// create jwt
|
||||
const jwt = await Jwt.sign({
|
||||
address: name,
|
||||
@@ -127,6 +209,7 @@ export const newAddress = async (
|
||||
return {
|
||||
jwt: jwt,
|
||||
address: name,
|
||||
password: generatedPassword,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +239,18 @@ export const cleanup = async (
|
||||
}
|
||||
console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
|
||||
switch (cleanType) {
|
||||
case "inactiveAddress":
|
||||
await batchDeleteAddressWithData(
|
||||
c,
|
||||
`updated_at < datetime('now', '-${cleanDays} day')`
|
||||
)
|
||||
break;
|
||||
case "addressCreated":
|
||||
await batchDeleteAddressWithData(
|
||||
c,
|
||||
`created_at < datetime('now', '-${cleanDays} day')`
|
||||
)
|
||||
break;
|
||||
case "mails":
|
||||
await c.env.DB.prepare(`
|
||||
DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')`
|
||||
@@ -178,9 +273,38 @@ export const cleanup = async (
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: need senbox delete?
|
||||
*/
|
||||
const batchDeleteAddressWithData = async (
|
||||
c: Context<HonoCustomType>,
|
||||
addressQueryCondition: string,
|
||||
): Promise<boolean> => {
|
||||
await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE address IN ( ` +
|
||||
`SELECT name FROM address WHERE ${addressQueryCondition})`
|
||||
).run();
|
||||
await c.env.DB.prepare(
|
||||
`DELETE FROM sendbox WHERE address IN ( ` +
|
||||
`SELECT name FROM address WHERE ${addressQueryCondition})`
|
||||
).run();
|
||||
await c.env.DB.prepare(
|
||||
`DELETE FROM auto_reply_mails WHERE address IN ( ` +
|
||||
`SELECT name FROM address WHERE ${addressQueryCondition})`
|
||||
).run();
|
||||
await c.env.DB.prepare(
|
||||
`DELETE FROM address_sender WHERE address IN ( ` +
|
||||
`SELECT name FROM address WHERE ${addressQueryCondition})`
|
||||
).run();
|
||||
await c.env.DB.prepare(
|
||||
`DELETE FROM users_address WHERE address_id IN ( ` +
|
||||
`SELECT id FROM address WHERE ${addressQueryCondition})`
|
||||
).run();
|
||||
// delete address
|
||||
await c.env.DB.prepare(`
|
||||
DELETE FROM address WHERE ${addressQueryCondition}`
|
||||
).run();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
export const deleteAddressWithData = async (
|
||||
c: Context<HonoCustomType>,
|
||||
address: string | undefined | null,
|
||||
@@ -215,13 +339,19 @@ export const deleteAddressWithData = async (
|
||||
const { success: sendAccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM address_sender WHERE address = ? `
|
||||
).bind(address).run();
|
||||
const { success: sendboxSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM sendbox WHERE address = ? `
|
||||
).bind(address).run();
|
||||
const { success: addressSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM users_address WHERE address_id = ? `
|
||||
).bind(address_id).run();
|
||||
const { success: autoReplySuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM auto_reply_mails WHERE address = ? `
|
||||
).bind(address).run();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM address WHERE name = ? `
|
||||
).bind(address).run();
|
||||
if (!success || !mailSuccess || !addressSuccess || !sendAccess) {
|
||||
if (!success || !mailSuccess || !sendboxSuccess || !addressSuccess || !sendAccess || !autoReplySuccess) {
|
||||
throw new Error("Failed to delete address")
|
||||
}
|
||||
return true;
|
||||
@@ -384,7 +514,7 @@ export async function triggerWebhook(
|
||||
|
||||
// user mail webhook
|
||||
const adminSettings = await c.env.KV.get<AdminWebhookSettings>(CONSTANTS.WEBHOOK_KV_SETTINGS_KEY, "json");
|
||||
if (adminSettings?.allowList.includes(address)) {
|
||||
if (!adminSettings?.enableAllowList || adminSettings?.allowList.includes(address)) {
|
||||
const settings = await c.env.KV.get<WebhookSettings>(
|
||||
`${CONSTANTS.WEBHOOK_KV_USER_SETTINGS_KEY}:${address}`, "json"
|
||||
);
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: 'v' + '0.9.1',
|
||||
VERSION: 'v' + '1.0.6',
|
||||
|
||||
// DB Version
|
||||
DB_VERSION_KEY: 'db_version',
|
||||
DB_VERSION: "v0.0.3",
|
||||
|
||||
// DB settings
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
@@ -9,6 +13,7 @@ export const CONSTANTS = {
|
||||
OAUTH2_SETTINGS_KEY: 'oauth2_settings',
|
||||
VERIFIED_ADDRESS_LIST_KEY: 'verified_address_list',
|
||||
NO_LIMIT_SEND_ADDRESS_LIST_KEY: 'no_limit_send_address_list',
|
||||
EMAIL_RULE_SETTINGS_KEY: 'email_rule_settings',
|
||||
|
||||
// KV
|
||||
TG_KV_PREFIX: "temp-mail-telegram",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createMimeMessage } from "mimetext";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import { Bindings } from "../types";
|
||||
|
||||
export const auto_reply = async (message: ForwardableEmailMessage, env: Bindings): Promise<void> => {
|
||||
const message_id = message.headers.get("Message-ID");
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { Bindings } from "../types";
|
||||
|
||||
export const isBlocked = async (from: string, env: Bindings): Promise<boolean> => {
|
||||
if (env.BLACK_LIST && env.BLACK_LIST.split(",").some(word => from.includes(word))) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Bindings, ParsedEmailContext } from "../types";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import { commonParseMail } from "../common";
|
||||
import { createMimeMessage } from "mimetext";
|
||||
|
||||