mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-10 09:57:01 +08:00
Compare commits
21
Commits
v0.10.0
...
840496c48f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
840496c48f | ||
|
|
9843b35f54 | ||
|
|
bfd66f5019 | ||
|
|
0bc31360b0 | ||
|
|
267d9bb93e | ||
|
|
2cc84d565c | ||
|
|
c96d180591 | ||
|
|
1303b0f2a9 | ||
|
|
9f535a0a90 | ||
|
|
70109785c6 | ||
|
|
7fd10f2775 | ||
|
|
f59b8c7a1b | ||
|
|
312ac13185 | ||
|
|
e6c582be9f | ||
|
|
483c429feb | ||
|
|
da5482e095 | ||
|
|
de4646876a | ||
|
|
bbc8a96811 | ||
|
|
9ac9cd46b0 | ||
|
|
c694b07380 | ||
|
|
672c4c7273 |
@@ -14,7 +14,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: PR Agent action step
|
- name: PR Agent action step
|
||||||
id: pragent
|
id: pragent
|
||||||
uses: Codium-ai/pr-agent@main
|
uses: docker://codiumai/pr-agent:0.29-github_action
|
||||||
env:
|
env:
|
||||||
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false"
|
PR_REVIEWER.REQUIRE_TESTS_REVIEW: "false"
|
||||||
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
OPENAI_KEY: ${{ secrets.OPENAI_KEY }}
|
||||||
|
|||||||
@@ -1,6 +1,27 @@
|
|||||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||||
# CHANGE LOG
|
# CHANGE LOG
|
||||||
|
|
||||||
|
## 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
|
## v0.10.0
|
||||||
|
|
||||||
- feat: 支持 User 查看收件箱,`/user_api/mails` 接口, 支持 `address` 和 `keyword` 过滤
|
- feat: 支持 User 查看收件箱,`/user_api/mails` 接口, 支持 `address` 和 `keyword` 过滤
|
||||||
|
|||||||
@@ -1,90 +1,187 @@
|
|||||||
# 使用 cloudflare 免费服务,搭建临时邮箱
|
<!-- markdownlint-disable-file MD033 MD045 -->
|
||||||
|
# 🚀 Cloudflare 临时邮箱 - 免费搭建临时邮件服务
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://hellogithub.com/repository/2ccc64bb1ba346b480625f584aa19eb1" target="_blank">
|
<a href="https://temp-mail-docs.awsl.uk" target="_blank">
|
||||||
<img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=2ccc64bb1ba346b480625f584aa19eb1&claim_uid=FxNypXK7UQ9OECT" alt="Featured|HelloGitHub"/>
|
<img alt="docs" src="https://img.shields.io/badge/docs-grey?logo=vitepress">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest" target="_blank">
|
||||||
|
<img src="https://img.shields.io/github/v/release/dreamhunter2333/cloudflare_temp_email">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/LICENSE" target="_blank">
|
||||||
|
<img alt="MIT License" src="https://img.shields.io/github/license/dreamhunter2333/cloudflare_temp_email">
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/graphs/contributors" target="_blank">
|
||||||
|
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/dreamhunter2333/cloudflare_temp_email">
|
||||||
|
</a>
|
||||||
|
<a href="">
|
||||||
|
<img alt="GitHub top language" src="https://img.shields.io/github/languages/top/dreamhunter2333/cloudflare_temp_email">
|
||||||
|
</a>
|
||||||
|
<a href="">
|
||||||
|
<img src="https://img.shields.io/github/last-commit/dreamhunter2333/cloudflare_temp_email">
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://temp-mail-docs.awsl.uk" target="_blank">
|
<a href="https://hellogithub.com/repository/2ccc64bb1ba346b480625f584aa19eb1" target="_blank">
|
||||||
<img alt="docs" src="https://img.shields.io/badge/docs-grey?style=for-the-badge&logo=vitepress">
|
<img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=2ccc64bb1ba346b480625f584aa19eb1&claim_uid=FxNypXK7UQ9OECT" alt="Featured|HelloGitHub" height="30"/>
|
||||||
</a>
|
|
||||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest" target="_blank">
|
|
||||||
<img src="https://img.shields.io/github/v/release/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/LICENSE" target="_blank">
|
|
||||||
<img alt="MIT License" src="https://img.shields.io/github/license/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/graphs/contributors" target="_blank">
|
|
||||||
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="">
|
|
||||||
<img alt="GitHub top language" src="https://img.shields.io/github/languages/top/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
|
||||||
</a>
|
|
||||||
<a href="">
|
|
||||||
<img src="https://img.shields.io/github/last-commit/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="README.md">🇨🇳 中文文档</a> |
|
||||||
|
<a href="README_EN.md">🇺🇸 English Document</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
> 本项目仅供学习和个人用途,请勿将其用于任何违法行为,否则后果自负。
|
> 本项目仅供学习和个人用途,请勿将其用于任何违法行为,否则后果自负。
|
||||||
|
|
||||||
## [查看部署文档](https://temp-mail-docs.awsl.uk)
|
**🎉 一个功能完整的临时邮箱服务!**
|
||||||
|
|
||||||
[](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html)
|
- 🆓 **完全免费** - 基于 Cloudflare 免费服务构建,零成本运行
|
||||||
|
- ⚡ **高性能** - Rust WASM 邮件解析,响应速度极快
|
||||||
|
- 🎨 **现代化界面** - 响应式设计,支持多语言,操作简便
|
||||||
|
|
||||||
[Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html)
|
## 📚 部署文档 - 快速开始
|
||||||
|
|
||||||
[English Docs](https://temp-mail-docs.awsl.uk/en/)
|
[📖 部署文档](https://temp-mail-docs.awsl.uk) | [🚀 Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html)
|
||||||
|
|
||||||
## [CHANGELOG](CHANGELOG.md)
|
<a href="https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html">
|
||||||
|
<img src="https://deploy.workers.cloudflare.com/button" alt="Deploy to Cloudflare Workers" height="32">
|
||||||
|
</a>
|
||||||
|
|
||||||
## [在线演示](https://mail.awsl.uk/)
|
## 📝 更新日志
|
||||||
|
|
||||||
|
查看 [CHANGELOG](CHANGELOG.md) 了解最新更新内容。
|
||||||
|
|
||||||
|
## 🎯 在线体验
|
||||||
|
|
||||||
|
立即体验 → [https://mail.awsl.uk/](https://mail.awsl.uk/)
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>📊 服务状态监控(点击收缩/展开)</summary>
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| [Backend](https://temp-email-api.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml)       |
|
| [Backend](https://temp-email-api.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml)       |
|
||||||
| [Frontend](https://mail.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml)       |
|
| [Frontend](https://mail.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml)       |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>⭐ Star History(点击收缩/展开)</summary>
|
||||||
|
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
||||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
|
</details>
|
||||||
- [查看部署文档](#查看部署文档)
|
|
||||||
- [CHANGELOG](#changelog)
|
|
||||||
- [在线演示](#在线演示)
|
|
||||||
- [功能](#功能)
|
|
||||||
- [Reference](#reference)
|
|
||||||
- [Join Community](#join-community)
|
|
||||||
|
|
||||||
## 功能
|
<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] 使用 `凭证` 重新登录之前的邮箱
|
||||||
- [x] 添加完整的用户注册登录功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证切换不同邮箱
|
- [x] 添加完整的用户注册登录功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证切换不同邮箱
|
||||||
- [x] 前后台均支持多语言
|
- [x] 支持 `OAuth2` 第三方登录(Github、Authentik 等)
|
||||||
|
- [x] 支持 `Passkey` 无密码登录
|
||||||
|
- [x] 用户角色管理,支持多角色域名和前缀配置
|
||||||
|
- [x] 用户收件箱查看,支持地址和关键词过滤
|
||||||
|
|
||||||
|
### 🔧 管理功能
|
||||||
|
|
||||||
|
- [x] 完整的 admin 控制台
|
||||||
|
- [x] `admin` 后台创建无前缀邮箱
|
||||||
|
- [x] admin 用户管理页面,增加用户地址查看功能
|
||||||
|
- [x] 定时清理功能,支持多种清理策略
|
||||||
- [x] 获取自定义名字的邮箱,`admin` 可配置黑名单
|
- [x] 获取自定义名字的邮箱,`admin` 可配置黑名单
|
||||||
- [x] 增加访问密码,可作为私人站点
|
- [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 作为数据库
|
- [x] 前后台均支持多语言
|
||||||
- 使用 Cloudflare Pages 部署前端
|
- [x] 现代化 UI 设计,支持响应式布局
|
||||||
- 使用 Cloudflare Workers 部署后端
|
- [x] 支持 Google Ads 集成
|
||||||
- email 转发使用 Cloudflare Email Routing
|
- [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)
|
- [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.
|
||||||
+11
-11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudflare_temp_email",
|
"name": "cloudflare_temp_email",
|
||||||
"version": "0.10.0",
|
"version": "1.0.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -24,15 +24,15 @@
|
|||||||
"@vueuse/core": "^12.8.2",
|
"@vueuse/core": "^12.8.2",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.11.0",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"mail-parser-wasm": "^0.2.1",
|
"mail-parser-wasm": "^0.2.1",
|
||||||
"naive-ui": "^2.41.1",
|
"naive-ui": "^2.42.0",
|
||||||
"postal-mime": "^2.4.3",
|
"postal-mime": "^2.4.4",
|
||||||
"vooks": "^0.2.12",
|
"vooks": "^0.2.12",
|
||||||
"vue": "^3.5.16",
|
"vue": "^3.5.18",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-i18n": "^11.1.5",
|
"vue-i18n": "^11.1.11",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -40,14 +40,14 @@
|
|||||||
"@vicons/material": "^0.13.0",
|
"@vicons/material": "^0.13.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.4",
|
"@vitejs/plugin-vue": "^5.2.4",
|
||||||
"unplugin-auto-import": "^19.3.0",
|
"unplugin-auto-import": "^19.3.0",
|
||||||
"unplugin-vue-components": "^28.7.0",
|
"unplugin-vue-components": "^28.8.0",
|
||||||
"vite": "^6.3.5",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-pwa": "^1.0.0",
|
"vite-plugin-pwa": "^1.0.2",
|
||||||
"vite-plugin-top-level-await": "^1.5.0",
|
"vite-plugin-top-level-await": "^1.6.0",
|
||||||
"vite-plugin-wasm": "^3.4.1",
|
"vite-plugin-wasm": "^3.5.0",
|
||||||
"workbox-build": "^7.3.0",
|
"workbox-build": "^7.3.0",
|
||||||
"workbox-window": "^7.3.0",
|
"workbox-window": "^7.3.0",
|
||||||
"wrangler": "^4.19.1"
|
"wrangler": "^4.28.1"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1013
-1008
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ export const useGlobalState = createGlobalState(
|
|||||||
const toggleDark = useToggle(isDark)
|
const toggleDark = useToggle(isDark)
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const announcement = useLocalStorage('announcement', '');
|
const announcement = useLocalStorage('announcement', '');
|
||||||
|
const useSimpleIndex = useLocalStorage('useSimpleIndex', false);
|
||||||
const openSettings = ref({
|
const openSettings = ref({
|
||||||
fetched: false,
|
fetched: false,
|
||||||
title: '',
|
title: '',
|
||||||
@@ -142,6 +143,7 @@ export const useGlobalState = createGlobalState(
|
|||||||
showAdminPage,
|
showAdminPage,
|
||||||
userOauth2SessionState,
|
userOauth2SessionState,
|
||||||
userOauth2SessionClientID,
|
userOauth2SessionClientID,
|
||||||
|
useSimpleIndex,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,11 +11,14 @@ import MailBox from '../components/MailBox.vue';
|
|||||||
import SendBox from '../components/SendBox.vue';
|
import SendBox from '../components/SendBox.vue';
|
||||||
import AutoReply from './index/AutoReply.vue';
|
import AutoReply from './index/AutoReply.vue';
|
||||||
import AccountSettings from './index/AccountSettings.vue';
|
import AccountSettings from './index/AccountSettings.vue';
|
||||||
|
import Appearance from './common/Appearance.vue';
|
||||||
import Webhook from './index/Webhook.vue';
|
import Webhook from './index/Webhook.vue';
|
||||||
import Attachment from './index/Attachment.vue';
|
import Attachment from './index/Attachment.vue';
|
||||||
import About from './common/About.vue';
|
import About from './common/About.vue';
|
||||||
|
|
||||||
const { loading, settings, openSettings, indexTab, globalTabplacement } = useGlobalState()
|
import SimpleIndex from './index/SimpleIndex.vue';
|
||||||
|
|
||||||
|
const { loading, settings, openSettings, indexTab, globalTabplacement, useSimpleIndex } = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
@@ -33,6 +36,7 @@ const { t } = useI18n({
|
|||||||
sendmail: 'Send Mail',
|
sendmail: 'Send Mail',
|
||||||
auto_reply: 'Auto Reply',
|
auto_reply: 'Auto Reply',
|
||||||
accountSettings: 'Account Settings',
|
accountSettings: 'Account Settings',
|
||||||
|
appearance: 'Appearance',
|
||||||
about: 'About',
|
about: 'About',
|
||||||
s3Attachment: 'S3 Attachment',
|
s3Attachment: 'S3 Attachment',
|
||||||
saveToS3Success: 'save to s3 success',
|
saveToS3Success: 'save to s3 success',
|
||||||
@@ -44,7 +48,8 @@ const { t } = useI18n({
|
|||||||
sendbox: '发件箱',
|
sendbox: '发件箱',
|
||||||
sendmail: '发送邮件',
|
sendmail: '发送邮件',
|
||||||
auto_reply: '自动回复',
|
auto_reply: '自动回复',
|
||||||
accountSettings: '账户设置',
|
accountSettings: '账户',
|
||||||
|
appearance: '外观',
|
||||||
about: '关于',
|
about: '关于',
|
||||||
s3Attachment: 'S3附件',
|
s3Attachment: 'S3附件',
|
||||||
saveToS3Success: '保存到s3成功',
|
saveToS3Success: '保存到s3成功',
|
||||||
@@ -122,6 +127,10 @@ onMounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<div v-if="useSimpleIndex">
|
||||||
|
<SimpleIndex />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
<AddressBar />
|
<AddressBar />
|
||||||
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
|
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
|
||||||
<n-tab-pane name="mailbox" :tab="t('mailbox')">
|
<n-tab-pane name="mailbox" :tab="t('mailbox')">
|
||||||
@@ -147,6 +156,9 @@ onMounted(() => {
|
|||||||
<n-tab-pane name="accountSettings" :tab="t('accountSettings')">
|
<n-tab-pane name="accountSettings" :tab="t('accountSettings')">
|
||||||
<AccountSettings />
|
<AccountSettings />
|
||||||
</n-tab-pane>
|
</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')">
|
<n-tab-pane v-if="openSettings.enableAutoReply" name="auto_reply" :tab="t('auto_reply')">
|
||||||
<AutoReply />
|
<AutoReply />
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
@@ -161,4 +173,5 @@ onMounted(() => {
|
|||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const { t } = useI18n({
|
|||||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
||||||
delete: 'Delete',
|
delete: 'Delete',
|
||||||
deleteTip: 'Are you sure to delete this email?',
|
deleteTip: 'Are you sure to delete this email?',
|
||||||
delteAccount: 'Delete Account',
|
deleteAccount: 'Delete Account',
|
||||||
viewMails: 'View Mails',
|
viewMails: 'View Mails',
|
||||||
viewSendBox: 'View SendBox',
|
viewSendBox: 'View SendBox',
|
||||||
itemCount: 'itemCount',
|
itemCount: 'itemCount',
|
||||||
@@ -46,7 +46,7 @@ const { t } = useI18n({
|
|||||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
||||||
delete: '删除',
|
delete: '删除',
|
||||||
deleteTip: '确定要删除这个邮箱吗?',
|
deleteTip: '确定要删除这个邮箱吗?',
|
||||||
delteAccount: '删除邮箱',
|
deleteAccount: '删除邮箱',
|
||||||
viewMails: '查看邮件',
|
viewMails: '查看邮件',
|
||||||
viewSendBox: '查看发件箱',
|
viewSendBox: '查看发件箱',
|
||||||
itemCount: '总数',
|
itemCount: '总数',
|
||||||
@@ -273,11 +273,11 @@ onMounted(async () => {
|
|||||||
<template #action>
|
<template #action>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal v-model:show="showDeleteAccount" preset="dialog" :title="t('delteAccount')">
|
<n-modal v-model:show="showDeleteAccount" preset="dialog" :title="t('deleteAccount')">
|
||||||
<p>{{ t('deleteTip') }}</p>
|
<p>{{ t('deleteTip') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="deleteEmail" size="small" tertiary type="error">
|
<n-button :loading="loading" @click="deleteEmail" size="small" tertiary type="error">
|
||||||
{{ t('delteAccount') }}
|
{{ t('deleteAccount') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ const cleanupModel = ref({
|
|||||||
cleanMailsDays: 30,
|
cleanMailsDays: 30,
|
||||||
enableUnknowMailsAutoCleanup: false,
|
enableUnknowMailsAutoCleanup: false,
|
||||||
cleanUnknowMailsDays: 30,
|
cleanUnknowMailsDays: 30,
|
||||||
enableAddressAutoCleanup: false,
|
|
||||||
cleanAddressDays: 30,
|
|
||||||
enableSendBoxAutoCleanup: false,
|
enableSendBoxAutoCleanup: false,
|
||||||
cleanSendBoxDays: 30,
|
cleanSendBoxDays: 30,
|
||||||
|
enableAddressAutoCleanup: false,
|
||||||
|
cleanAddressDays: 30,
|
||||||
|
enableInactiveAddressAutoCleanup: false,
|
||||||
|
cleanInactiveAddressDays: 30,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
@@ -24,22 +26,26 @@ const { t } = useI18n({
|
|||||||
mailBoxLabel: 'Cleanup the inbox before n days',
|
mailBoxLabel: 'Cleanup the inbox before n days',
|
||||||
mailUnknowLabel: "Cleanup the unknow mail before n days",
|
mailUnknowLabel: "Cleanup the unknow mail before n days",
|
||||||
sendBoxLabel: "Cleanup the sendbox before n days",
|
sendBoxLabel: "Cleanup the sendbox before n days",
|
||||||
|
addressCreateLabel: "Cleanup the address created before n days",
|
||||||
|
inactiveAddressLabel: "Cleanup the inactive address before n days",
|
||||||
cleanupNow: "Cleanup now",
|
cleanupNow: "Cleanup now",
|
||||||
autoCleanup: "Auto cleanup",
|
autoCleanup: "Auto cleanup",
|
||||||
cleanupSuccess: "Cleanup success",
|
cleanupSuccess: "Cleanup success",
|
||||||
save: "Save",
|
save: "Save",
|
||||||
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document",
|
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document, setting 0 days means clear all",
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
tip: '请输入天数',
|
tip: '请输入天数',
|
||||||
mailBoxLabel: '清理 n 天前的收件箱',
|
mailBoxLabel: '清理 n 天前的收件箱',
|
||||||
mailUnknowLabel: "清理 n 天前的无收件人邮件",
|
mailUnknowLabel: "清理 n 天前的无收件人邮件",
|
||||||
sendBoxLabel: "清理 n 天前的发件箱",
|
sendBoxLabel: "清理 n 天前的发件箱",
|
||||||
|
addressCreateLabel: "清理 n 天前创建的地址",
|
||||||
|
inactiveAddressLabel: "清理 n 天前的未活跃地址",
|
||||||
autoCleanup: "自动清理",
|
autoCleanup: "自动清理",
|
||||||
cleanupSuccess: "清理成功",
|
cleanupSuccess: "清理成功",
|
||||||
cleanupNow: "立即清理",
|
cleanupNow: "立即清理",
|
||||||
save: "保存",
|
save: "保存",
|
||||||
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档",
|
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档, 配置为 0 天表示全部清空",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -86,9 +92,14 @@ onMounted(async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card :bordered="false" embedded>
|
<n-card :bordered="false" embedded>
|
||||||
<n-alert :show-icon="false" :bordered="false">
|
<n-alert :show-icon="false" :bordered="false" type="warning">
|
||||||
<span>{{ t('cronTip') }}</span>
|
<span>{{ t('cronTip') }}</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
|
<n-flex justify="end">
|
||||||
|
<n-button @click="save" type="primary" :loading="loading">
|
||||||
|
{{ t('save') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
<n-form :model="cleanupModel">
|
<n-form :model="cleanupModel">
|
||||||
<n-form-item-row :label="t('mailBoxLabel')">
|
<n-form-item-row :label="t('mailBoxLabel')">
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
|
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
|
||||||
@@ -126,9 +137,30 @@ onMounted(async () => {
|
|||||||
{{ t('cleanupNow') }}
|
{{ t('cleanupNow') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
<n-button @click="save" type="primary" block :loading="loading">
|
<n-form-item-row :label="t('addressCreateLabel')">
|
||||||
{{ t('save') }}
|
<n-checkbox v-model:checked="cleanupModel.enableAddressAutoCleanup">
|
||||||
|
{{ 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-button>
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('inactiveAddressLabel')">
|
||||||
|
<n-checkbox v-model:checked="cleanupModel.enableInactiveAddressAutoCleanup">
|
||||||
|
{{ t('autoCleanup') }}
|
||||||
|
</n-checkbox>
|
||||||
|
<n-input-number v-model:value="cleanupModel.cleanInactiveAddressDays" :placeholder="t('tip')" />
|
||||||
|
<n-button @click="cleanup('inactiveAddress', cleanupModel.cleanInactiveAddressDays)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="CleaningServicesFilled" />
|
||||||
|
</template>
|
||||||
|
{{ t('cleanupNow') }}
|
||||||
|
</n-button>
|
||||||
|
</n-form-item-row>
|
||||||
</n-form>
|
</n-form>
|
||||||
</n-card>
|
</n-card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,16 +3,23 @@ import { useI18n } from 'vue-i18n'
|
|||||||
|
|
||||||
import { useIsMobile } from '../../utils/composables'
|
import { useIsMobile } from '../../utils/composables'
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
|
const props = defineProps({
|
||||||
|
showUseSimpleIndex: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mailboxSplitSize, useIframeShowMail, preferShowTextMail, configAutoRefreshInterval,
|
mailboxSplitSize, useIframeShowMail, preferShowTextMail, configAutoRefreshInterval,
|
||||||
globalTabplacement, useSideMargin, useUTCDate
|
globalTabplacement, useSideMargin, useUTCDate, useSimpleIndex
|
||||||
} = useGlobalState()
|
} = useGlobalState()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
const { t } = useI18n({
|
const { t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
|
useSimpleIndex: 'Use Simple Index',
|
||||||
mailboxSplitSize: 'Mailbox Split Size',
|
mailboxSplitSize: 'Mailbox Split Size',
|
||||||
useIframeShowMail: 'Use iframe Show HTML Mail',
|
useIframeShowMail: 'Use iframe Show HTML Mail',
|
||||||
preferShowTextMail: 'Display text Mail by default',
|
preferShowTextMail: 'Display text Mail by default',
|
||||||
@@ -26,6 +33,7 @@ const { t } = useI18n({
|
|||||||
autoRefreshInterval: 'Auto Refresh Interval(Sec)',
|
autoRefreshInterval: 'Auto Refresh Interval(Sec)',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
|
useSimpleIndex: '使用极简主页',
|
||||||
mailboxSplitSize: '邮箱界面分栏大小',
|
mailboxSplitSize: '邮箱界面分栏大小',
|
||||||
preferShowTextMail: '默认以文本显示邮件',
|
preferShowTextMail: '默认以文本显示邮件',
|
||||||
useIframeShowMail: '使用iframe显示HTML邮件',
|
useIframeShowMail: '使用iframe显示HTML邮件',
|
||||||
@@ -57,6 +65,9 @@ const { t } = useI18n({
|
|||||||
60: '60', 120: '120', 180: '180', 240: '240'
|
60: '60', 120: '120', 180: '180', 240: '240'
|
||||||
}" />
|
}" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
|
<n-form-item-row v-if="props.showUseSimpleIndex" :label="t('useSimpleIndex')">
|
||||||
|
<n-switch v-model:value="useSimpleIndex" :round="false" />
|
||||||
|
</n-form-item-row>
|
||||||
<n-form-item-row :label="t('preferShowTextMail')">
|
<n-form-item-row :label="t('preferShowTextMail')">
|
||||||
<n-switch v-model:value="preferShowTextMail" :round="false" />
|
<n-switch v-model:value="preferShowTextMail" :round="false" />
|
||||||
</n-form-item-row>
|
</n-form-item-row>
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import { useGlobalState } from '../../store'
|
||||||
import { api } from '../../api'
|
import { api } from '../../api'
|
||||||
import Appearance from '../common/Appearance.vue'
|
|
||||||
import { getRouterPathWithLang } from '../../utils'
|
import { getRouterPathWithLang } from '../../utils'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -15,24 +14,24 @@ const router = useRouter()
|
|||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const showLogout = ref(false)
|
const showLogout = ref(false)
|
||||||
const showDelteAccount = ref(false)
|
const showDeleteAccount = ref(false)
|
||||||
const { locale, t } = useI18n({
|
const { locale, t } = useI18n({
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
logout: "Logout",
|
logout: "Logout",
|
||||||
delteAccount: "Delete Account",
|
deleteAccount: "Delete Account",
|
||||||
showAddressCredential: 'Show Address Credential',
|
showAddressCredential: 'Show Address Credential',
|
||||||
logoutConfirm: 'Are you sure to logout?',
|
logoutConfirm: 'Are you sure to logout?',
|
||||||
delteAccount: "Delete Account",
|
deleteAccount: "Delete Account",
|
||||||
delteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
deleteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
logout: '退出登录',
|
logout: '退出登录',
|
||||||
delteAccount: "删除账户",
|
deleteAccount: "删除账户",
|
||||||
showAddressCredential: '查看邮箱地址凭证',
|
showAddressCredential: '查看邮箱地址凭证',
|
||||||
logoutConfirm: '确定要退出登录吗?',
|
logoutConfirm: '确定要退出登录吗?',
|
||||||
delteAccount: "删除账户",
|
deleteAccount: "删除账户",
|
||||||
delteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
deleteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -60,15 +59,14 @@ const deleteAccount = async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="center" v-if="settings.address">
|
<div class="center" v-if="settings.address">
|
||||||
<n-card :bordered="false" embedded>
|
<n-card :bordered="false" embedded>
|
||||||
<Appearance />
|
|
||||||
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
||||||
{{ t('showAddressCredential') }}
|
{{ t('showAddressCredential') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button @click="showLogout = true" secondary block strong>
|
<n-button @click="showLogout = true" secondary block strong>
|
||||||
{{ t('logout') }}
|
{{ t('logout') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
<n-button @click="showDelteAccount = true" type="error" secondary block strong>
|
<n-button @click="showDeleteAccount = true" type="error" secondary block strong>
|
||||||
{{ t('delteAccount') }}
|
{{ t('deleteAccount') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-card>
|
</n-card>
|
||||||
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
||||||
@@ -79,11 +77,11 @@ const deleteAccount = async () => {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
|
<n-modal v-model:show="showDeleteAccount" preset="dialog" :title="t('deleteAccount')">
|
||||||
<p>{{ t('delteAccountConfirm') }}</p>
|
<p>{{ t('deleteAccountConfirm') }}</p>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="deleteAccount" size="small" tertiary type="error">
|
<n-button :loading="loading" @click="deleteAccount" size="small" tertiary type="error">
|
||||||
{{ t('delteAccount') }}
|
{{ t('deleteAccount') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import {
|
||||||
|
ExitToAppFilled,
|
||||||
|
ContentCopyFilled,
|
||||||
|
RefreshFilled,
|
||||||
|
ArrowBackIosNewFilled,
|
||||||
|
ArrowForwardIosFilled,
|
||||||
|
SettingsFilled
|
||||||
|
} from '@vicons/material'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
import { api } from '../../api'
|
||||||
|
import Login from '../common/Login.vue'
|
||||||
|
import AccountSettings from './AccountSettings.vue'
|
||||||
|
import { processItem } from '../../utils/email-parser'
|
||||||
|
import { utcToLocalDate } from '../../utils'
|
||||||
|
import ShadowHtmlComponent from '../../components/ShadowHtmlComponent.vue'
|
||||||
|
|
||||||
|
const { jwt, settings, useSimpleIndex, useUTCDate, showAddressCredential } = useGlobalState()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 邮件数据
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const totalCount = ref(0)
|
||||||
|
const loading = ref(false)
|
||||||
|
const currentMail = ref(null)
|
||||||
|
const showAccountSettingsCard = ref(false)
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
exitSimpleIndex: 'Exit Simple',
|
||||||
|
copyAddress: 'Copy',
|
||||||
|
addressCopied: 'Address copied successfully',
|
||||||
|
refreshMails: 'Refresh',
|
||||||
|
noMails: 'No mails found',
|
||||||
|
prevPage: 'Previous',
|
||||||
|
nextPage: 'Next',
|
||||||
|
refreshSuccess: 'Mails refreshed successfully',
|
||||||
|
mailCount: '{current} / {total} emails',
|
||||||
|
accountSettings: "Account Settings",
|
||||||
|
addressCredential: 'Mail Address Credential',
|
||||||
|
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
exitSimpleIndex: '退出极简',
|
||||||
|
copyAddress: '复制',
|
||||||
|
addressCopied: '地址复制成功',
|
||||||
|
refreshMails: '刷新',
|
||||||
|
noMails: '暂无邮件',
|
||||||
|
prevPage: '上一页',
|
||||||
|
nextPage: '下一页',
|
||||||
|
refreshSuccess: '邮件刷新成功',
|
||||||
|
mailCount: '{current} / {total} 封邮件',
|
||||||
|
accountSettings: "账户设置",
|
||||||
|
addressCredential: '邮箱地址凭证',
|
||||||
|
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 复制地址
|
||||||
|
const copyAddress = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(settings.value.address)
|
||||||
|
message.success(t('addressCopied'))
|
||||||
|
} catch (error) {
|
||||||
|
message.error('复制失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取邮件数据
|
||||||
|
const fetchMails = async () => {
|
||||||
|
if (!settings.value.address) return
|
||||||
|
try {
|
||||||
|
const { results, count } = await api.fetch(`/api/mails?limit=1&offset=${currentPage.value - 1}`)
|
||||||
|
totalCount.value = count > 0 ? count : totalCount.value;
|
||||||
|
const rawMail = results && results.length > 0 ? results[0] : null
|
||||||
|
currentMail.value = rawMail ? await processItem(rawMail) : null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch mails:', error)
|
||||||
|
message.error('获取邮件失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新邮件
|
||||||
|
const refreshMails = async () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
await fetchMails()
|
||||||
|
message.success(t('refreshSuccess'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页控制
|
||||||
|
const currentPageDisplay = computed(() => currentPage.value)
|
||||||
|
const totalPages = computed(() => Math.max(1, totalCount.value))
|
||||||
|
const canGoPrev = computed(() => currentPage.value > 1)
|
||||||
|
const canGoNext = computed(() => currentPage.value < totalPages.value)
|
||||||
|
|
||||||
|
const prevPage = async () => {
|
||||||
|
if (canGoPrev.value) {
|
||||||
|
currentPage.value--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextPage = async () => {
|
||||||
|
if (canGoNext.value) {
|
||||||
|
currentPage.value++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听页面变化
|
||||||
|
watch(currentPage, () => {
|
||||||
|
fetchMails()
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await api.getSettings()
|
||||||
|
await fetchMails()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="center">
|
||||||
|
<div v-if="!settings.address">
|
||||||
|
<n-card :bordered="false" embedded>
|
||||||
|
<Login />
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<n-card :bordered="false" embedded>
|
||||||
|
<div style="text-align: center; margin-bottom: 16px; font-size: 18px;">
|
||||||
|
<n-text strong size="large">{{ settings.address }}</n-text>
|
||||||
|
</div>
|
||||||
|
<n-flex justify="center">
|
||||||
|
<n-button @click="refreshMails" :loading="loading" type="primary" tertiary size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<RefreshFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('refreshMails') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="copyAddress" tertiary size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ContentCopyFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('copyAddress') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="useSimpleIndex = false" tertiary size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ExitToAppFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('exitSimpleIndex') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="showAccountSettingsCard = true" tertiary size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<SettingsFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('accountSettings') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<!-- 账户设置卡片 -->
|
||||||
|
<n-card v-if="showAccountSettingsCard" :bordered="false" embedded closable
|
||||||
|
@close="showAccountSettingsCard = false" :title="t('accountSettings')">
|
||||||
|
<AccountSettings />
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<n-card :bordered="false" embedded style="text-align: left;">
|
||||||
|
|
||||||
|
<div v-if="totalCount > 1">
|
||||||
|
<n-flex justify="space-between">
|
||||||
|
<n-button @click="prevPage" :disabled="!canGoPrev" text size="small">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ArrowBackIosNewFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('prevPage') }}
|
||||||
|
</n-button>
|
||||||
|
<n-text size="small">
|
||||||
|
{{ t('mailCount', { current: currentPageDisplay, total: totalCount }) }}
|
||||||
|
</n-text>
|
||||||
|
<n-button @click="nextPage" :disabled="!canGoNext" text size="small" icon-placement="right">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon>
|
||||||
|
<ArrowForwardIosFilled />
|
||||||
|
</n-icon>
|
||||||
|
</template>
|
||||||
|
{{ t('nextPage') }}
|
||||||
|
</n-button>
|
||||||
|
</n-flex>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!currentMail" class="no-mail">
|
||||||
|
<n-empty :description="t('noMails')" />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<h3 v-if="currentMail.subject">{{ currentMail.subject }}</h3>
|
||||||
|
|
||||||
|
<n-space>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ currentMail.id }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ utcToLocalDate(currentMail.created_at, useUTCDate.value) }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
FROM: {{ currentMail.source }}
|
||||||
|
</n-tag>
|
||||||
|
</n-space>
|
||||||
|
|
||||||
|
<div style="margin-top: 16px;">
|
||||||
|
<ShadowHtmlComponent v-if="currentMail.message" :htmlContent="currentMail.message" />
|
||||||
|
<pre v-else>{{ currentMail.text }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
<n-modal v-model:show="showAddressCredential" preset="dialog" :title="t('addressCredential')">
|
||||||
|
<span>
|
||||||
|
<p>{{ t("addressCredentialTip") }}</p>
|
||||||
|
</span>
|
||||||
|
<n-card embedded>
|
||||||
|
<b>{{ jwt }}</b>
|
||||||
|
</n-card>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.center {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-card {
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -27,7 +27,7 @@ const mailKeyword = ref("")
|
|||||||
const addressFilterOptions = ref([]);
|
const addressFilterOptions = ref([]);
|
||||||
|
|
||||||
const queryMail = () => {
|
const queryMail = () => {
|
||||||
addressFilter.value = addressFilter.value.trim();
|
addressFilter.value = addressFilter.value ? addressFilter.value.trim() : addressFilter.value;
|
||||||
mailKeyword.value = mailKeyword.value.trim();
|
mailKeyword.value = mailKeyword.value.trim();
|
||||||
mailBoxKey.value = Date.now();
|
mailBoxKey.value = Date.now();
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,6 @@ const deleteMail = async (curMailId) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
watch(addressFilter, async (newValue) => {
|
watch(addressFilter, async (newValue) => {
|
||||||
console.log("addressFilter", newValue);
|
|
||||||
queryMail();
|
queryMail();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "temp-email-pages",
|
"name": "temp-email-pages",
|
||||||
"version": "0.10.0",
|
"version": "1.0.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"wrangler": "^4.19.1"
|
"wrangler": "^4.28.1"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,31 @@ class SimpleMailbox:
|
|||||||
self.addListener = self.listeners.append
|
self.addListener = self.listeners.append
|
||||||
self.removeListener = self.listeners.remove
|
self.removeListener = self.listeners.remove
|
||||||
self.message_count = 0
|
self.message_count = 0
|
||||||
|
self._update_message_count()
|
||||||
|
|
||||||
|
def _update_message_count(self):
|
||||||
|
"""主动获取邮件总数"""
|
||||||
|
try:
|
||||||
|
if self.name == "INBOX":
|
||||||
|
endpoint = "/api/mails"
|
||||||
|
elif self.name == "SENT":
|
||||||
|
endpoint = "/api/sendbox"
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
res = httpx.get(
|
||||||
|
f"{settings.proxy_url}{endpoint}?limit=1&offset=0",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {self.password}",
|
||||||
|
"x-custom-auth": f"{settings.basic_password}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if res.status_code == 200:
|
||||||
|
self.message_count = res.json()["count"]
|
||||||
|
# _logger.info(f"Updated {self.name} message count: {self.message_count}")
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Failed to update message count for {self.name}: {e}")
|
||||||
|
|
||||||
def getFlags(self):
|
def getFlags(self):
|
||||||
return ["\\Seen"]
|
return ["\\Seen"]
|
||||||
@@ -73,7 +98,9 @@ class SimpleMailbox:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
def getMessageCount(self):
|
def getMessageCount(self):
|
||||||
return self.message_count or 1000
|
# 每次请求时更新邮件总数
|
||||||
|
self._update_message_count()
|
||||||
|
return self.message_count
|
||||||
|
|
||||||
def getRecentCount(self):
|
def getRecentCount(self):
|
||||||
return 0
|
return 0
|
||||||
@@ -91,6 +118,8 @@ class SimpleMailbox:
|
|||||||
return "/"
|
return "/"
|
||||||
|
|
||||||
def requestStatus(self, names):
|
def requestStatus(self, names):
|
||||||
|
# 在状态请求时也更新邮件总数
|
||||||
|
self._update_message_count()
|
||||||
r = {}
|
r = {}
|
||||||
if "MESSAGES" in names:
|
if "MESSAGES" in names:
|
||||||
r["MESSAGES"] = self.getMessageCount()
|
r["MESSAGES"] = self.getMessageCount()
|
||||||
@@ -105,20 +134,73 @@ class SimpleMailbox:
|
|||||||
return defer.succeed(r)
|
return defer.succeed(r)
|
||||||
|
|
||||||
def fetch(self, messages, uid):
|
def fetch(self, messages, uid):
|
||||||
|
"""边查边返回邮件"""
|
||||||
|
def email_generator():
|
||||||
|
for range_item in messages.ranges:
|
||||||
|
start, end = range_item
|
||||||
|
_logger.info(f"Fetching messages: {self.name}, range: {start}-{end}")
|
||||||
|
|
||||||
|
for email_data in self.fetchGenerator(start, end):
|
||||||
|
yield email_data
|
||||||
|
|
||||||
|
# 返回生成器,让IMAP4服务器逐个处理
|
||||||
|
return email_generator()
|
||||||
|
|
||||||
|
def fetchGenerator(self, start, end):
|
||||||
|
"""通用的邮件获取生成器,边查边返回"""
|
||||||
|
start = max(start, 1)
|
||||||
|
|
||||||
|
# 根据邮箱类型确定API端点
|
||||||
if self.name == "INBOX":
|
if self.name == "INBOX":
|
||||||
return self.fetchINBOX(messages)
|
endpoint = "/api/mails"
|
||||||
if self.name == "SENT":
|
elif self.name == "SENT":
|
||||||
return self.fetchSENT(messages)
|
endpoint = "/api/sendbox"
|
||||||
return []
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 首先获取服务端邮件总数
|
||||||
|
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 count_res.status_code != 200:
|
||||||
|
_logger.error(
|
||||||
|
f"Failed to get {self.name} email count: "
|
||||||
|
f"code=[{count_res.status_code}] text=[{count_res.text}]"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
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}"
|
||||||
|
)
|
||||||
|
|
||||||
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(
|
res = httpx.get(
|
||||||
f"{settings.proxy_url}/api/mails?limit={limit}&offset={start - 1}",
|
f"{settings.proxy_url}{endpoint}?limit={limit}&offset={server_offset}",
|
||||||
headers={
|
headers={
|
||||||
"Authorization": f"Bearer {self.password}",
|
"Authorization": f"Bearer {self.password}",
|
||||||
"x-custom-auth": f"{settings.basic_password}",
|
"x-custom-auth": f"{settings.basic_password}",
|
||||||
@@ -127,43 +209,24 @@ class SimpleMailbox:
|
|||||||
)
|
)
|
||||||
if res.status_code != 200:
|
if res.status_code != 200:
|
||||||
_logger.error(
|
_logger.error(
|
||||||
"Failed: "
|
f"Failed to fetch {self.name} emails: "
|
||||||
f"code=[{res.status_code}] text=[{res.text}]"
|
f"code=[{res.status_code}] text=[{res.text}]"
|
||||||
)
|
)
|
||||||
raise Exception("Failed to fetch emails")
|
break
|
||||||
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"])
|
|
||||||
]
|
|
||||||
|
|
||||||
def fetchSENT(self, messages):
|
emails = res.json()["results"]
|
||||||
start, end = messages.ranges[0]
|
for i, item in enumerate(reversed(emails)):
|
||||||
start = max(start, 1)
|
uid = total_count - server_offset - len(emails) + i + 1
|
||||||
limit = min(20, end - start + 1) if end and end >= start else 20
|
if current_start <= uid <= batch_end:
|
||||||
if self.message_count > 0 and start > self.message_count:
|
if self.name == "INBOX":
|
||||||
return []
|
email_model = parse_email(item["raw"])
|
||||||
res = httpx.get(
|
elif self.name == "SENT":
|
||||||
f"{settings.proxy_url}/api/sendbox?limit={limit}&offset={start - 1}",
|
email_model = generate_email_model(item)
|
||||||
headers={
|
|
||||||
"Authorization": f"Bearer {self.password}",
|
# 立即返回这封邮件
|
||||||
"x-custom-auth": f"{settings.basic_password}",
|
yield (uid, SimpleMessage(uid, email_model))
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
current_start = batch_end + 1
|
||||||
)
|
|
||||||
if res.status_code != 200:
|
|
||||||
_logger.error(
|
|
||||||
"Failed: "
|
|
||||||
f"code=[{res.status_code}] text=[{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, generate_email_model(item)))
|
|
||||||
for uid, item in enumerate(reversed(res.json()["results"]))
|
|
||||||
]
|
|
||||||
|
|
||||||
def getUID(self, message):
|
def getUID(self, message):
|
||||||
return message.uid
|
return message.uid
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
aiosmtpd==1.4.6
|
aiosmtpd==1.4.6
|
||||||
pydantic-settings==2.2.1
|
pydantic-settings==2.9.1
|
||||||
requests==2.32.0
|
requests==2.32.4
|
||||||
twisted==24.7.0
|
Twisted==25.5.0
|
||||||
httpx==0.27.0
|
httpx==0.28.1
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "temp-mail-docs",
|
"name": "temp-mail-docs",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.10.0",
|
"version": "1.0.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.15.30",
|
"@types/node": "^24.2.0",
|
||||||
"vitepress": "^1.6.3",
|
"vitepress": "^1.6.4",
|
||||||
"wrangler": "^4.19.1"
|
"wrangler": "^4.28.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vitepress dev docs",
|
"dev": "vitepress dev docs",
|
||||||
|
|||||||
Generated
+471
-453
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloudflare_temp_email",
|
"name": "cloudflare_temp_email",
|
||||||
"version": "0.10.0",
|
"version": "1.0.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -11,26 +11,26 @@
|
|||||||
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20250607.0",
|
"@cloudflare/workers-types": "^4.20250807.0",
|
||||||
"@eslint/js": "9.18.0",
|
"@eslint/js": "9.18.0",
|
||||||
"@simplewebauthn/types": "10.0.0",
|
"@simplewebauthn/types": "10.0.0",
|
||||||
"@types/node": "^22.15.30",
|
"@types/node": "^22.17.0",
|
||||||
"eslint": "9.18.0",
|
"eslint": "9.18.0",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"typescript-eslint": "^8.33.1",
|
"typescript-eslint": "^8.39.0",
|
||||||
"wrangler": "^4.19.1"
|
"wrangler": "^4.28.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.826.0",
|
"@aws-sdk/client-s3": "^3.863.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.826.0",
|
"@aws-sdk/s3-request-presigner": "^3.863.0",
|
||||||
"@simplewebauthn/server": "10.0.1",
|
"@simplewebauthn/server": "10.0.1",
|
||||||
"hono": "^4.7.11",
|
"hono": "^4.9.0",
|
||||||
"jsonpath-plus": "^10.3.0",
|
"jsonpath-plus": "^10.3.0",
|
||||||
"mimetext": "^3.0.27",
|
"mimetext": "^3.0.27",
|
||||||
"postal-mime": "^2.4.3",
|
"postal-mime": "^2.4.4",
|
||||||
"resend": "^4.5.2",
|
"resend": "^4.8.0",
|
||||||
"telegraf": "4.16.3",
|
"telegraf": "4.16.3",
|
||||||
"worker-mailer": "^1.1.4"
|
"worker-mailer": "^1.1.5"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"patchedDependencies": {
|
"patchedDependencies": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
diff --git a/lib/core/network/client.js b/lib/core/network/client.js
|
diff --git a/lib/core/network/client.js b/lib/core/network/client.js
|
||||||
index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d698de5ce 100644
|
index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..ab82c5391596ee2ed8dbe4128f3ba57a8cc998ca 100644
|
||||||
--- a/lib/core/network/client.js
|
--- a/lib/core/network/client.js
|
||||||
+++ b/lib/core/network/client.js
|
+++ b/lib/core/network/client.js
|
||||||
@@ -1,18 +1,18 @@
|
@@ -1,18 +1,18 @@
|
||||||
@@ -37,7 +37,17 @@ index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d
|
|||||||
const path = __importStar(require("path"));
|
const path = __importStar(require("path"));
|
||||||
const node_fetch_1 = __importDefault(require("node-fetch"));
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
||||||
const check_1 = require("../helpers/check");
|
const check_1 = require("../helpers/check");
|
||||||
@@ -61,10 +61,10 @@ const DEFAULT_OPTIONS = {
|
@@ -39,7 +39,8 @@ const multipart_stream_1 = __importDefault(require("./multipart-stream"));
|
||||||
|
const error_1 = __importDefault(require("./error"));
|
||||||
|
const url_1 = require("url");
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
-const debug = require('debug')('telegraf:client');
|
||||||
|
+// const debug = require('debug')('telegraf:client');
|
||||||
|
+const debug = (msg) => console.debug(`[telegraf:client] ${msg}`);
|
||||||
|
const { isStream } = multipart_stream_1.default;
|
||||||
|
const WEBHOOK_REPLY_METHOD_ALLOWLIST = new Set([
|
||||||
|
'answerCallbackQuery',
|
||||||
|
@@ -61,10 +62,10 @@ const DEFAULT_OPTIONS = {
|
||||||
apiRoot: 'https://api.telegram.org',
|
apiRoot: 'https://api.telegram.org',
|
||||||
apiMode: 'bot',
|
apiMode: 'bot',
|
||||||
webhookReply: true,
|
webhookReply: true,
|
||||||
@@ -52,7 +62,7 @@ index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d
|
|||||||
attachmentAgent: undefined,
|
attachmentAgent: undefined,
|
||||||
testEnv: false,
|
testEnv: false,
|
||||||
};
|
};
|
||||||
@@ -112,9 +112,9 @@ async function buildFormDataConfig(payload, agent) {
|
@@ -112,9 +113,9 @@ async function buildFormDataConfig(payload, agent) {
|
||||||
}
|
}
|
||||||
const boundary = crypto.randomBytes(32).toString('hex');
|
const boundary = crypto.randomBytes(32).toString('hex');
|
||||||
const formData = new multipart_stream_1.default(boundary);
|
const formData = new multipart_stream_1.default(boundary);
|
||||||
@@ -65,7 +75,7 @@ index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d
|
|||||||
return {
|
return {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
compress: true,
|
compress: true,
|
||||||
@@ -205,14 +205,15 @@ async function attachFormMedia(form, media, id, agent) {
|
@@ -205,14 +206,15 @@ async function attachFormMedia(form, media, id, agent) {
|
||||||
if ('source' in media && media.source) {
|
if ('source' in media && media.source) {
|
||||||
let mediaSource = media.source;
|
let mediaSource = media.source;
|
||||||
if (typeof media.source === 'string') {
|
if (typeof media.source === 'string') {
|
||||||
|
|||||||
Generated
+886
-867
File diff suppressed because it is too large
Load Diff
@@ -17,13 +17,11 @@ export default {
|
|||||||
return c.json({ success: true })
|
return c.json({ success: true })
|
||||||
},
|
},
|
||||||
getCleanup: async (c: Context<HonoCustomType>) => {
|
getCleanup: async (c: Context<HonoCustomType>) => {
|
||||||
const value = await getJsonSetting(c, CONSTANTS.AUTO_CLEANUP_KEY);
|
const cleanupSetting = await getJsonSetting<CleanupSettings>(c, CONSTANTS.AUTO_CLEANUP_KEY);
|
||||||
const cleanupSetting = new CleanupSettings(value);
|
|
||||||
return c.json(cleanupSetting)
|
return c.json(cleanupSetting)
|
||||||
},
|
},
|
||||||
saveCleanup: async (c: Context<HonoCustomType>) => {
|
saveCleanup: async (c: Context<HonoCustomType>) => {
|
||||||
const value = await c.req.json();
|
const cleanupSetting = await c.req.json<CleanupSettings>();
|
||||||
const cleanupSetting = new CleanupSettings(value);
|
|
||||||
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting));
|
await saveSetting(c, CONSTANTS.AUTO_CLEANUP_KEY, JSON.stringify(cleanupSetting));
|
||||||
return c.json({ success: true })
|
return c.json({ success: true })
|
||||||
}
|
}
|
||||||
|
|||||||
+70
-3
@@ -40,6 +40,23 @@ const getNameRegex = (c: Context<HonoCustomType>): RegExp => {
|
|||||||
return DEFAULT_NAME_REGEX;
|
return DEFAULT_NAME_REGEX;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateAddressUpdatedAt(
|
||||||
|
c: Context<HonoCustomType>,
|
||||||
|
address: string | undefined | null
|
||||||
|
): Promise<void> {
|
||||||
|
if (!address) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// update address updated_at
|
||||||
|
try {
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`UPDATE address SET updated_at = datetime('now') where name = ?`
|
||||||
|
).bind(address).run();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to update address updated_at", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const newAddress = async (
|
export const newAddress = async (
|
||||||
c: Context<HonoCustomType>,
|
c: Context<HonoCustomType>,
|
||||||
{
|
{
|
||||||
@@ -91,9 +108,9 @@ export const newAddress = async (
|
|||||||
}
|
}
|
||||||
// check domain
|
// check domain
|
||||||
const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c);
|
const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c);
|
||||||
// if domain is not set, use the first domain
|
// if domain is not set, use the random domain
|
||||||
if (!domain && allowDomains.length > 0) {
|
if (!domain && allowDomains.length > 0) {
|
||||||
domain = allowDomains[0];
|
domain = allowDomains[Math.floor(Math.random() * allowDomains.length)];
|
||||||
}
|
}
|
||||||
// check domain is valid
|
// check domain is valid
|
||||||
if (!domain || !allowDomains.includes(domain)) {
|
if (!domain || !allowDomains.includes(domain)) {
|
||||||
@@ -108,6 +125,7 @@ export const newAddress = async (
|
|||||||
if (!success) {
|
if (!success) {
|
||||||
throw new Error("Failed to create address")
|
throw new Error("Failed to create address")
|
||||||
}
|
}
|
||||||
|
await updateAddressUpdatedAt(c, name);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = (e as Error).message;
|
const message = (e as Error).message;
|
||||||
if (message && message.includes("UNIQUE")) {
|
if (message && message.includes("UNIQUE")) {
|
||||||
@@ -155,6 +173,18 @@ export const cleanup = async (
|
|||||||
}
|
}
|
||||||
console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
|
console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
|
||||||
switch (cleanType) {
|
switch (cleanType) {
|
||||||
|
case "inactiveAddress":
|
||||||
|
await batchDeleteAddressWithData(
|
||||||
|
c,
|
||||||
|
`updated_at < datetime('now', '-${cleanDays} day')`
|
||||||
|
)
|
||||||
|
break;
|
||||||
|
case "addressCreated":
|
||||||
|
await batchDeleteAddressWithData(
|
||||||
|
c,
|
||||||
|
`created_at < datetime('now', '-${cleanDays} day')`
|
||||||
|
)
|
||||||
|
break;
|
||||||
case "mails":
|
case "mails":
|
||||||
await c.env.DB.prepare(`
|
await c.env.DB.prepare(`
|
||||||
DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')`
|
DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')`
|
||||||
@@ -177,6 +207,37 @@ export const cleanup = async (
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const batchDeleteAddressWithData = async (
|
||||||
|
c: Context<HonoCustomType>,
|
||||||
|
addressQueryCondition: string,
|
||||||
|
): Promise<boolean> => {
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`DELETE FROM raw_mails WHERE address IN ( ` +
|
||||||
|
`SELECT name FROM address WHERE ${addressQueryCondition})`
|
||||||
|
).run();
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`DELETE FROM sendbox WHERE address IN ( ` +
|
||||||
|
`SELECT name FROM address WHERE ${addressQueryCondition})`
|
||||||
|
).run();
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`DELETE FROM auto_reply_mails WHERE address IN ( ` +
|
||||||
|
`SELECT name FROM address WHERE ${addressQueryCondition})`
|
||||||
|
).run();
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`DELETE FROM address_sender WHERE address IN ( ` +
|
||||||
|
`SELECT name FROM address WHERE ${addressQueryCondition})`
|
||||||
|
).run();
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`DELETE FROM users_address WHERE address_id IN ( ` +
|
||||||
|
`SELECT id FROM address WHERE ${addressQueryCondition})`
|
||||||
|
).run();
|
||||||
|
// delete address
|
||||||
|
await c.env.DB.prepare(`
|
||||||
|
DELETE FROM address WHERE ${addressQueryCondition}`
|
||||||
|
).run();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO: need senbox delete?
|
* TODO: need senbox delete?
|
||||||
*/
|
*/
|
||||||
@@ -214,13 +275,19 @@ export const deleteAddressWithData = async (
|
|||||||
const { success: sendAccess } = await c.env.DB.prepare(
|
const { success: sendAccess } = await c.env.DB.prepare(
|
||||||
`DELETE FROM address_sender WHERE address = ? `
|
`DELETE FROM address_sender WHERE address = ? `
|
||||||
).bind(address).run();
|
).bind(address).run();
|
||||||
|
const { success: sendboxSuccess } = await c.env.DB.prepare(
|
||||||
|
`DELETE FROM sendbox WHERE address = ? `
|
||||||
|
).bind(address).run();
|
||||||
const { success: addressSuccess } = await c.env.DB.prepare(
|
const { success: addressSuccess } = await c.env.DB.prepare(
|
||||||
`DELETE FROM users_address WHERE address_id = ? `
|
`DELETE FROM users_address WHERE address_id = ? `
|
||||||
).bind(address_id).run();
|
).bind(address_id).run();
|
||||||
|
const { success: autoReplySuccess } = await c.env.DB.prepare(
|
||||||
|
`DELETE FROM auto_reply_mails WHERE address = ? `
|
||||||
|
).bind(address).run();
|
||||||
const { success } = await c.env.DB.prepare(
|
const { success } = await c.env.DB.prepare(
|
||||||
`DELETE FROM address WHERE name = ? `
|
`DELETE FROM address WHERE name = ? `
|
||||||
).bind(address).run();
|
).bind(address).run();
|
||||||
if (!success || !mailSuccess || !addressSuccess || !sendAccess) {
|
if (!success || !mailSuccess || !sendboxSuccess || !addressSuccess || !sendAccess || !autoReplySuccess) {
|
||||||
throw new Error("Failed to delete address")
|
throw new Error("Failed to delete address")
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const CONSTANTS = {
|
export const CONSTANTS = {
|
||||||
VERSION: 'v' + '0.10.0',
|
VERSION: 'v' + '1.0.3',
|
||||||
|
|
||||||
// DB Version
|
// DB Version
|
||||||
DB_VERSION_KEY: 'db_version',
|
DB_VERSION_KEY: 'db_version',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Hono } from 'hono'
|
import { Context, Hono } from 'hono'
|
||||||
|
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
import { getBooleanValue, getJsonSetting, checkCfTurnstile, getStringValue, getSplitStringListValue } from '../utils';
|
import { getBooleanValue, getJsonSetting, checkCfTurnstile, getStringValue, getSplitStringListValue } from '../utils';
|
||||||
import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains } from '../common'
|
import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains, updateAddressUpdatedAt } from '../common'
|
||||||
import { CONSTANTS } from '../constants'
|
import { CONSTANTS } from '../constants'
|
||||||
import auto_reply from './auto_reply'
|
import auto_reply from './auto_reply'
|
||||||
import webhook_settings from './webhook_settings';
|
import webhook_settings from './webhook_settings';
|
||||||
@@ -26,6 +26,7 @@ api.get('/api/mails', async (c) => {
|
|||||||
return c.json({ "error": "No address" }, 400)
|
return c.json({ "error": "No address" }, 400)
|
||||||
}
|
}
|
||||||
const { limit, offset } = c.req.query();
|
const { limit, offset } = c.req.query();
|
||||||
|
if (Number.parseInt(offset) <= 0) await updateAddressUpdatedAt(c, address);
|
||||||
return await handleListQuery(c,
|
return await handleListQuery(c,
|
||||||
`SELECT * FROM raw_mails where address = ?`,
|
`SELECT * FROM raw_mails where address = ?`,
|
||||||
`SELECT count(*) as count FROM raw_mails where address = ?`,
|
`SELECT count(*) as count FROM raw_mails where address = ?`,
|
||||||
@@ -89,14 +90,9 @@ api.get('/api/settings', async (c) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return c.text(msgs.InvalidAddressMsg, 400)
|
return c.text(msgs.InvalidAddressMsg, 400)
|
||||||
}
|
}
|
||||||
// update address updated_at
|
|
||||||
try {
|
await updateAddressUpdatedAt(c, address);
|
||||||
c.env.DB.prepare(
|
|
||||||
`UPDATE address SET updated_at = datetime('now') where name = ?`
|
|
||||||
).bind(address).run();
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Failed to update address")
|
|
||||||
}
|
|
||||||
const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
|
const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||||
const is_no_limit_send_balance = user_role && no_limit_roles.includes(user_role);
|
const is_no_limit_send_balance = user_role && no_limit_roles.includes(user_role);
|
||||||
const balance = is_no_limit_send_balance ? 99999 : await c.env.DB.prepare(
|
const balance = is_no_limit_send_balance ? 99999 : await c.env.DB.prepare(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export type WebhookMail = {
|
|||||||
parsedHtml: string;
|
parsedHtml: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CleanupSettings {
|
export type CleanupSettings = {
|
||||||
|
|
||||||
enableMailsAutoCleanup: boolean | undefined;
|
enableMailsAutoCleanup: boolean | undefined;
|
||||||
cleanMailsDays: number;
|
cleanMailsDays: number;
|
||||||
@@ -40,22 +40,11 @@ export class CleanupSettings {
|
|||||||
cleanUnknowMailsDays: number;
|
cleanUnknowMailsDays: number;
|
||||||
enableSendBoxAutoCleanup: boolean | undefined;
|
enableSendBoxAutoCleanup: boolean | undefined;
|
||||||
cleanSendBoxDays: number;
|
cleanSendBoxDays: number;
|
||||||
|
enableAddressAutoCleanup: boolean | undefined;
|
||||||
constructor(data: CleanupSettings | undefined | null) {
|
cleanAddressDays: number;
|
||||||
const {
|
enableInactiveAddressAutoCleanup: boolean | undefined;
|
||||||
enableMailsAutoCleanup, cleanMailsDays,
|
cleanInactiveAddressDays: number;
|
||||||
enableUnknowMailsAutoCleanup, cleanUnknowMailsDays,
|
|
||||||
enableSendBoxAutoCleanup, cleanSendBoxDays
|
|
||||||
} = data || {};
|
|
||||||
this.enableMailsAutoCleanup = enableMailsAutoCleanup;
|
|
||||||
this.cleanMailsDays = cleanMailsDays || 0;
|
|
||||||
this.enableUnknowMailsAutoCleanup = enableUnknowMailsAutoCleanup;
|
|
||||||
this.cleanUnknowMailsDays = cleanUnknowMailsDays || 0;
|
|
||||||
this.enableSendBoxAutoCleanup = enableSendBoxAutoCleanup;
|
|
||||||
this.cleanSendBoxDays = cleanSendBoxDays || 0;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class GeoData {
|
export class GeoData {
|
||||||
|
|
||||||
|
|||||||
+22
-5
@@ -6,31 +6,48 @@ import { CleanupSettings } from './models';
|
|||||||
|
|
||||||
export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) {
|
export async function scheduled(event: ScheduledEvent, env: Bindings, ctx: any) {
|
||||||
console.log("Scheduled event: ", event);
|
console.log("Scheduled event: ", event);
|
||||||
const value = await getJsonSetting(
|
const autoCleanupSetting = await getJsonSetting<CleanupSettings>(
|
||||||
{ env: env, } as Context<HonoCustomType>,
|
{ env: env, } as Context<HonoCustomType>,
|
||||||
CONSTANTS.AUTO_CLEANUP_KEY
|
CONSTANTS.AUTO_CLEANUP_KEY
|
||||||
);
|
);
|
||||||
const autoCleanupSetting = new CleanupSettings(value);
|
if (!autoCleanupSetting) {
|
||||||
|
console.log("No auto cleanup settings found, skipping cleanup.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.log("autoCleanupSetting:", JSON.stringify(autoCleanupSetting));
|
console.log("autoCleanupSetting:", JSON.stringify(autoCleanupSetting));
|
||||||
if (autoCleanupSetting.enableMailsAutoCleanup && autoCleanupSetting.cleanMailsDays > 0) {
|
if (autoCleanupSetting.enableMailsAutoCleanup) {
|
||||||
await cleanup(
|
await cleanup(
|
||||||
{ env: env, } as Context<HonoCustomType>,
|
{ env: env, } as Context<HonoCustomType>,
|
||||||
"mails",
|
"mails",
|
||||||
autoCleanupSetting.cleanMailsDays
|
autoCleanupSetting.cleanMailsDays
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (autoCleanupSetting.enableUnknowMailsAutoCleanup && autoCleanupSetting.cleanUnknowMailsDays > 0) {
|
if (autoCleanupSetting.enableUnknowMailsAutoCleanup) {
|
||||||
await cleanup(
|
await cleanup(
|
||||||
{ env: env, } as Context<HonoCustomType>,
|
{ env: env, } as Context<HonoCustomType>,
|
||||||
"mails_unknow",
|
"mails_unknow",
|
||||||
autoCleanupSetting.cleanUnknowMailsDays
|
autoCleanupSetting.cleanUnknowMailsDays
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (autoCleanupSetting.enableSendBoxAutoCleanup && autoCleanupSetting.cleanSendBoxDays > 0) {
|
if (autoCleanupSetting.enableSendBoxAutoCleanup) {
|
||||||
await cleanup(
|
await cleanup(
|
||||||
{ env: env, } as Context<HonoCustomType>,
|
{ env: env, } as Context<HonoCustomType>,
|
||||||
"sendbox",
|
"sendbox",
|
||||||
autoCleanupSetting.cleanSendBoxDays
|
autoCleanupSetting.cleanSendBoxDays
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (autoCleanupSetting.enableInactiveAddressAutoCleanup) {
|
||||||
|
await cleanup(
|
||||||
|
{ env: env, } as Context<HonoCustomType>,
|
||||||
|
"inactiveAddress",
|
||||||
|
autoCleanupSetting.cleanInactiveAddressDays
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (autoCleanupSetting.enableAddressAutoCleanup) {
|
||||||
|
await cleanup(
|
||||||
|
{ env: env, } as Context<HonoCustomType>,
|
||||||
|
"addressCreated",
|
||||||
|
autoCleanupSetting.cleanAddressDays
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,14 @@ export const jwtListToAddressData = async (
|
|||||||
for (const jwt of jwtList) {
|
for (const jwt of jwtList) {
|
||||||
try {
|
try {
|
||||||
const { address, address_id } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
const { address, address_id } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||||
|
const name = await c.env.DB.prepare(
|
||||||
|
`SELECT name FROM address WHERE id = ? `
|
||||||
|
).bind(address_id).first("name");
|
||||||
|
if (!name) {
|
||||||
|
addressList.push("无效地址");
|
||||||
|
invalidJwtList.push(jwt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
addressList.push(address as string);
|
addressList.push(address as string);
|
||||||
addressIdMap[address as string] = address_id as number;
|
addressIdMap[address as string] = address_id as number;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -75,7 +83,7 @@ export const bindTelegramAddress = async (
|
|||||||
return address as string;
|
return address as string;
|
||||||
}
|
}
|
||||||
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
||||||
throw Error("绑定地址数量已达上限");
|
throw Error("绑定地址数量已达上限, 请先 /cleaninvalidaddress");
|
||||||
}
|
}
|
||||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, jwt]));
|
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, jwt]));
|
||||||
// for mail push to telegram
|
// for mail push to telegram
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ const COMMANDS = [
|
|||||||
description: "查看邮件, 请输入 /mails <邮箱地址>, 不输入地址默认查看第一个地址"
|
description: "查看邮件, 请输入 /mails <邮箱地址>, 不输入地址默认查看第一个地址"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
command: "cleaninvalidadress",
|
command: "cleaninvalidaddress",
|
||||||
description: "清理无效地址, 请输入 /cleaninvalidadress"
|
description: "清理无效地址, 请输入 /cleaninvalidaddress"
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const address = ctx?.message?.text.slice("/unbind".length).trim();
|
const address = ctx?.message?.text.slice("/delete".length).trim();
|
||||||
if (!address) {
|
if (!address) {
|
||||||
return await ctx.reply("请输入地址");
|
return await ctx.reply("请输入地址");
|
||||||
}
|
}
|
||||||
@@ -186,7 +186,7 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command("cleaninvalidadress", async (ctx: TgContext) => {
|
bot.command("cleaninvalidaddress", async (ctx: TgContext) => {
|
||||||
const userId = ctx?.message?.from?.id;
|
const userId = ctx?.message?.from?.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
return await ctx.reply("无法获取用户信息");
|
return await ctx.reply("无法获取用户信息");
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getJsonSetting } from "../utils"
|
|||||||
import { CONSTANTS } from "../constants";
|
import { CONSTANTS } from "../constants";
|
||||||
import { unbindTelegramByAddress } from '../telegram_api/common';
|
import { unbindTelegramByAddress } from '../telegram_api/common';
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
|
import { updateAddressUpdatedAt } from '../common';
|
||||||
|
|
||||||
const UserBindAddressModule = {
|
const UserBindAddressModule = {
|
||||||
bind: async (c: Context<HonoCustomType>) => {
|
bind: async (c: Context<HonoCustomType>) => {
|
||||||
@@ -237,6 +238,7 @@ const UserBindAddressModule = {
|
|||||||
if (!newAddressSuccess) {
|
if (!newAddressSuccess) {
|
||||||
throw new Error("Failed to create address")
|
throw new Error("Failed to create address")
|
||||||
}
|
}
|
||||||
|
await updateAddressUpdatedAt(c, address);
|
||||||
// find new address id
|
// find new address id
|
||||||
const new_address_id = await c.env.DB.prepare(
|
const new_address_id = await c.env.DB.prepare(
|
||||||
`SELECT id FROM address WHERE name = ?`
|
`SELECT id FROM address WHERE name = ?`
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Context } from 'hono';
|
|||||||
import { Jwt } from 'hono/utils/jwt'
|
import { Jwt } from 'hono/utils/jwt'
|
||||||
|
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
import { getJsonSetting } from '../utils';
|
import { getJsonSetting, getStringValue, getUserRoles } from '../utils';
|
||||||
import { UserOauth2Settings } from '../models';
|
import { UserOauth2Settings } from '../models';
|
||||||
import { CONSTANTS } from '../constants';
|
import { CONSTANTS } from '../constants';
|
||||||
|
|
||||||
@@ -110,6 +110,23 @@ export default {
|
|||||||
if (!user_id) {
|
if (!user_id) {
|
||||||
return c.text(msgs.UserNotFoundMsg, 400)
|
return c.text(msgs.UserNotFoundMsg, 400)
|
||||||
}
|
}
|
||||||
|
// process user roles
|
||||||
|
const defaultRole = getStringValue(c.env.USER_DEFAULT_ROLE);
|
||||||
|
if (defaultRole) {
|
||||||
|
const user_roles = getUserRoles(c);
|
||||||
|
if (!user_roles.find((r) => r.role === defaultRole)) {
|
||||||
|
return c.text(msgs.InvalidUserDefaultRoleMsg, 500);
|
||||||
|
}
|
||||||
|
// update user roles
|
||||||
|
const { success: success2 } = await c.env.DB.prepare(
|
||||||
|
`INSERT INTO user_roles (user_id, role_text)`
|
||||||
|
+ ` VALUES (?, ?)`
|
||||||
|
+ ` ON CONFLICT(user_id) DO NOTHING`
|
||||||
|
).bind(user_id, defaultRole).run();
|
||||||
|
if (!success2) {
|
||||||
|
return c.text(msgs.FailedUpdateUserDefaultRoleMsg, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
// create jwt
|
// create jwt
|
||||||
const jwt = await Jwt.sign({
|
const jwt = await Jwt.sign({
|
||||||
user_email: email,
|
user_email: email,
|
||||||
|
|||||||
@@ -64,6 +64,16 @@ export default {
|
|||||||
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
|
||||||
iat: Math.floor(Date.now() / 1000),
|
iat: Math.floor(Date.now() / 1000),
|
||||||
}, c.env.JWT_SECRET, "HS256");
|
}, c.env.JWT_SECRET, "HS256");
|
||||||
|
// update address updated_at
|
||||||
|
try {
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`UPDATE address SET updated_at = datetime('now') where id IN `
|
||||||
|
+ `(SELECT address_id FROM users_address WHERE user_id = ?)`
|
||||||
|
).bind(user.user_id).run();
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Failed to update address updated_at")
|
||||||
|
}
|
||||||
return c.json({
|
return c.json({
|
||||||
...user,
|
...user,
|
||||||
is_admin: is_admin,
|
is_admin: is_admin,
|
||||||
|
|||||||
Reference in New Issue
Block a user