Compare commits
10 Commits
421a963a92
...
498ffe1b18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
498ffe1b18 | ||
|
|
80a0cb65a5 | ||
|
|
a84b2ae469 | ||
|
|
22c35119c0 | ||
|
|
edba6b80d3 | ||
|
|
6ae3d43f9e | ||
|
|
40fc68e56c | ||
|
|
e68099edb3 | ||
|
|
fdf4259701 | ||
|
|
f6eaf6d0fd |
4
.github/workflows/deploy-cloudflare.yml
vendored
4
.github/workflows/deploy-cloudflare.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
R2_BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME || vars.R2_BUCKET_NAME }}
|
||||
PROJECT_LINK: ${{ secrets.PROJECT_LINK || vars.PROJECT_LINK }}
|
||||
AI_MODEL: ${{ secrets.AI_MODEL || vars.AI_MODEL || '@cf/meta/llama-3.1-8b-instruct' }}
|
||||
CF_EMAIL: ${{ secrets.CF_EMAIL || vars.CF_EMAIL || false }}
|
||||
CLOUDFLARE_EMAIL: ${{ secrets.CF_EMAIL || vars.CF_EMAIL || false }}
|
||||
ANALYSIS_CACHE: ${{ secrets.ANALYSIS_CACHE || vars.ANALYSIS_CACHE || false }}
|
||||
LINUXDO_CLIENT_ID: ${{ secrets.LINUXDO_CLIENT_ID || vars.LINUXDO_CLIENT_ID }}
|
||||
LINUXDO_CLIENT_SECRET: ${{ secrets.LINUXDO_CLIENT_SECRET || vars.LINUXDO_CLIENT_SECRET }}
|
||||
@@ -115,7 +115,7 @@ jobs:
|
||||
sed -i '/\[\[routes\]\]/,/^$/d' "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
if [ "$(printf '%s' "$CF_EMAIL" | tr '[:upper:]' '[:lower:]')" = "true" ]; then
|
||||
if [ "$(printf '%s' "$CLOUDFLARE_EMAIL" | tr '[:upper:]' '[:lower:]')" = "true" ]; then
|
||||
cat >> "$CONFIG_FILE" <<'EOF'
|
||||
|
||||
[[send_email]]
|
||||
|
||||
364
README.md
364
README.md
@@ -3,156 +3,324 @@
|
||||
<h1 align="center">Cloud Mail</h1>
|
||||
<p align="center">基于 Cloudflare 的简约响应式邮箱服务,支持邮件发送、附件收发 🎉</p>
|
||||
<p align="center">
|
||||
简体中文 | <a href="/README-en.md" style="margin-left: 5px">English </a>
|
||||
简体中文 | <a href="/README-en.md">English</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://github.com/maillab/cloud-mail/tree/main?tab=MIT-1-ov-file" target="_blank" >
|
||||
<img src="https://img.shields.io/badge/license-MIT-green" />
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/releases" target="_blank" >
|
||||
<a href="https://github.com/maillab/cloud-mail/blob/main/LICENSE" target="_blank">
|
||||
<img src="https://img.shields.io/badge/license-MIT-green" alt="license" />
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/releases" target="_blank">
|
||||
<img src="https://img.shields.io/github/v/release/maillab/cloud-mail" alt="releases" />
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/issues" >
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/issues" target="_blank">
|
||||
<img src="https://img.shields.io/github/issues/maillab/cloud-mail" alt="issues" />
|
||||
</a>
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/stargazers" target="_blank">
|
||||
<img src="https://img.shields.io/github/stars/maillab/cloud-mail" alt="stargazers" />
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/forks" target="_blank" >
|
||||
</a>
|
||||
<a href="https://github.com/maillab/cloud-mail/network/members" target="_blank">
|
||||
<img src="https://img.shields.io/github/forks/maillab/cloud-mail" alt="forks" />
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/20459" target="_blank" >
|
||||
<img src="https://trendshift.io/api/badge/repositories/20459" alt="trendshift" >
|
||||
<a href="https://trendshift.io/repositories/20459" target="_blank">
|
||||
<img src="https://trendshift.io/api/badge/repositories/20459" alt="trendshift" />
|
||||
</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 项目简介
|
||||
## 📖 项目简介
|
||||
|
||||
只需要一个域名,就可以创建多个不同的邮箱,类似各大邮箱平台,本项目支持署到 Cloudflare Workers ,降低服务器成本,搭建自己的邮箱服务
|
||||
Cloud Mail 是一个基于 Cloudflare 生态构建的轻量级邮箱服务。只需一个域名,即可创建多个邮箱账号,实现类似 Gmail、QQ 邮箱等主流平台的收发邮件功能。
|
||||
|
||||
## 项目展示
|
||||
本项目采用前后端分离架构,后端部署在 Cloudflare Workers,前端为 Vue3 单页应用,无需传统服务器,大幅降低运维成本。
|
||||
|
||||
- [在线演示](https://skymail.ink)<br>
|
||||
- [部署文档](https://doc.skymail.ink)<br>
|
||||
- 🌐 [在线演示](https://skymail.ink)
|
||||
- 📚 [部署文档](https://doc.skymail.ink)
|
||||
- 💬 [Telegram 交流群](https://t.me/cloud_mail_tg)
|
||||
|
||||
|  |  |
|
||||
|-----------------------|-----------------------|
|
||||
|  |  |
|
||||
## 📸 项目展示
|
||||
|
||||
|  |  |
|
||||
|:--:|:--:|
|
||||
|  |  |
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
- **💰 零成本部署** - 基于 Cloudflare 免费套餐,无需购买服务器
|
||||
- **💻 响应式设计** - 完美适配 PC、平板和移动设备
|
||||
- **📧 邮件收发** - 集成 Resend 实现邮件发送,支持群发、附件、内嵌图片
|
||||
- **📦 附件管理** - 使用 Cloudflare R2 存储附件,支持大文件上传下载
|
||||
- **🛡️ 权限管理** - RBAC 权限控制,支持用户分级和资源限制
|
||||
- **🔔 消息推送** - 邮件到达后自动推送到 Telegram 机器人
|
||||
- **📡 开放 API** - 提供 RESTful API,支持批量操作用户和邮件
|
||||
- **🔢 验证码识别** - 集成 Workers AI 自动识别邮件中的验证码
|
||||
- **📊 数据可视化** - 使用 ECharts 展示系统数据和使用统计
|
||||
- **🎨 个性化** - 支持自定义网站标题、登录页背景、透明度等
|
||||
- **🤖 人机验证** - 集成 Cloudflare Turnstile 防止恶意注册
|
||||
|
||||
## 功能介绍
|
||||
## 🚀 快速开始
|
||||
|
||||
- **💰 低成本使用**: 可部署到 Cloudflare Workers 降低服务器成本
|
||||
### 环境要求
|
||||
|
||||
- **💻 响应式设计**:响应式布局自动适配PC和大部分手机端浏览器
|
||||
- [Node.js](https://nodejs.org/) 18+
|
||||
- [npm](https://www.npmjs.com/) 或 [pnpm](https://pnpm.io/)
|
||||
- [Cloudflare 账号](https://dash.cloudflare.com/)
|
||||
- [Resend 账号](https://resend.com/)(用于发送邮件)
|
||||
|
||||
- **📧 邮件发送**:集成Resend发送邮件,支持群发,内嵌图片和附件发送,发送状态查看
|
||||
### 安装依赖
|
||||
|
||||
- **🛡️ 管理员功能**:可以对用户,邮件进行管理,RABC权限控制对功能及使用资源限制
|
||||
```bash
|
||||
# 克隆项目
|
||||
git clone https://github.com/maillab/cloud-mail.git
|
||||
cd cloud-mail
|
||||
|
||||
- **📦 附件收发**:支持收发附件,使用R2对象存储保存和下载文件
|
||||
# 安装后端依赖
|
||||
cd mail-worker
|
||||
npm install
|
||||
|
||||
- **🔔 邮件推送**:接收邮件后可以转发到TG机器人或其他服务商邮箱
|
||||
# 安装前端依赖
|
||||
cd ../mail-vue
|
||||
npm install
|
||||
```
|
||||
|
||||
- **📡 开放API**:支持使用API批量生成用户,多条件查询邮件
|
||||
### 本地开发
|
||||
|
||||
- **🔢 验证码识别**:使用Workers AI,自动识别邮件验证码
|
||||
**1. 配置后端**
|
||||
|
||||
- **📈 数据可视化**:使用ECharts对系统数据详情,用户邮件增长可视化显示
|
||||
```bash
|
||||
cd mail-worker
|
||||
|
||||
- **🎨 个性化设置**:可以自定义网站标题,登录背景,透明度
|
||||
# 创建开发环境配置
|
||||
cp wrangler.toml.example wrangler-dev.toml
|
||||
|
||||
- **🤖 人机验证**:集成Turnstile人机验证,防止人机批量注册
|
||||
# 编辑配置(填入你的 Cloudflare 账号信息)
|
||||
# 参见下方「配置说明」章节
|
||||
```
|
||||
|
||||
- **📜 更多功能**:正在开发中...
|
||||
**2. 启动后端开发服务器**
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# 或使用 wrangler 命令
|
||||
wrangler dev --config wrangler-dev.toml
|
||||
```
|
||||
|
||||
**3. 启动前端开发服务器**
|
||||
|
||||
## 技术栈
|
||||
```bash
|
||||
cd mail-vue
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- **平台**:[Cloudflare Workers](https://developers.cloudflare.com/workers/)
|
||||
前端默认运行在 `http://localhost:5173`,后端在 `http://localhost:8787`。
|
||||
|
||||
- **Web框架**:[Hono](https://hono.dev/)
|
||||
## 🛠️ 技术栈
|
||||
|
||||
- **ORM:**[Drizzle](https://orm.drizzle.team/)
|
||||
| 层级 | 技术 | 说明 |
|
||||
|:---|:---|:---|
|
||||
| **运行平台** | [Cloudflare Workers](https://workers.cloudflare.com/) | 边缘计算平台 |
|
||||
| **Web 框架** | [Hono](https://hono.dev/) | 轻量级 Web 框架 |
|
||||
| **ORM** | [Drizzle ORM](https://orm.drizzle.team/) | 类型安全的 ORM |
|
||||
| **数据库** | [Cloudflare D1](https://developers.cloudflare.com/d1/) | SQLite 边缘数据库 |
|
||||
| **缓存** | [Cloudflare KV](https://developers.cloudflare.com/kv/) | 键值存储 |
|
||||
| **文件存储** | [Cloudflare R2](https://developers.cloudflare.com/r2/) | 对象存储 |
|
||||
| **前端框架** | [Vue 3](https://vuejs.org/) | 渐进式 JS 框架 |
|
||||
| **UI 组件** | [Element Plus](https://element-plus.org/) | Vue 3 组件库 |
|
||||
| **状态管理** | [Pinia](https://pinia.vuejs.org/) | Vue 官方状态管理 |
|
||||
| **图表** | [ECharts](https://echarts.apache.org/) | 数据可视化 |
|
||||
| **邮件服务** | [Resend](https://resend.com/) | 现代化邮件 API |
|
||||
|
||||
- **前端框架**:[Vue3](https://vuejs.org/)
|
||||
|
||||
- **UI框架**:[Element Plus](https://element-plus.org/)
|
||||
|
||||
- **邮件推送:** [Resend](https://resend.com/)
|
||||
|
||||
- **缓存**:[Cloudflare KV](https://developers.cloudflare.com/kv/)
|
||||
|
||||
- **数据库**:[Cloudflare D1](https://developers.cloudflare.com/d1/)
|
||||
|
||||
- **文件存储**:[Cloudflare R2](https://developers.cloudflare.com/r2/)
|
||||
|
||||
## 目录结构
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
cloud-mail
|
||||
├── mail-worker # worker后端项目
|
||||
│ ├── src
|
||||
│ │ ├── api # api接口层
|
||||
│ │ ├── const # 项目常量
|
||||
│ │ ├── dao # 数据访问层
|
||||
│ │ ├── email # 邮件处理接收
|
||||
│ │ ├── entity # 数据库实体
|
||||
│ │ ├── error # 自定义异常
|
||||
│ │ ├── hono # web框架配置、拦截器、全局异常等
|
||||
│ │ ├── i18n # 语言国际化
|
||||
│ │ ├── init # 数据库缓存初始化
|
||||
│ │ ├── model # 响应体数据封装
|
||||
│ │ ├── security # 身份权限认证
|
||||
│ │ ├── service # 业务服务层
|
||||
│ │ ├── template # 消息模板
|
||||
│ │ ├── utils # 工具类
|
||||
│ │ └── index.js # 入口文件
|
||||
│ ├── pageckge.json # 项目依赖
|
||||
│ └── wrangler.toml # 项目配置
|
||||
cloud-mail/
|
||||
├── mail-worker/ # 后端 Worker 项目
|
||||
│ ├── src/
|
||||
│ │ ├── api/ # API 接口层
|
||||
│ │ ├── const/ # 常量定义
|
||||
│ │ ├── dao/ # 数据访问层
|
||||
│ │ ├── email/ # 邮件收发处理
|
||||
│ │ ├── entity/ # 数据库实体
|
||||
│ │ ├── error/ # 异常定义
|
||||
│ │ ├── hono/ # Web 框架配置
|
||||
│ │ ├── i18n/ # 国际化
|
||||
│ │ ├── init/ # 初始化逻辑
|
||||
│ │ ├── model/ # 数据模型
|
||||
│ │ ├── security/ # 权限认证
|
||||
│ │ ├── service/ # 业务逻辑层
|
||||
│ │ ├── template/ # 邮件模板
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ └── index.js # 入口文件
|
||||
│ ├── package.json
|
||||
│ └── wrangler.toml # 部署配置
|
||||
│
|
||||
├── mail-vue # vue前端项目
|
||||
│ ├── src
|
||||
│ │ ├── axios # axios配置
|
||||
│ │ ├── components # 自定义组件
|
||||
│ │ ├── echarts # echarts组件导入
|
||||
│ │ ├── i18n # 语言国际化
|
||||
│ │ ├── init # 入站初始化
|
||||
│ │ ├── layout # 主体布局组件
|
||||
│ │ ├── perm # 权限认证
|
||||
│ │ ├── request # api接口
|
||||
│ │ ├── router # 路由配置
|
||||
│ │ ├── store # 全局状态管理
|
||||
│ │ ├── utils # 工具类
|
||||
│ │ ├── views # 页面组件
|
||||
│ │ ├── app.vue # 入口组件
|
||||
│ │ ├── main.js # 入口js
|
||||
│ │ └── style.css # 全局css
|
||||
│ ├── package.json # 项目依赖
|
||||
└── └── env.release # 项目配置
|
||||
├── mail-vue/ # 前端 Vue 项目
|
||||
│ ├── src/
|
||||
│ │ ├── axios/ # Axios 配置
|
||||
│ │ ├── components/ # 公共组件
|
||||
│ │ ├── echarts/ # 图表组件
|
||||
│ │ ├── i18n/ # 国际化
|
||||
│ │ ├── init/ # 初始化
|
||||
│ │ ├── layout/ # 布局组件
|
||||
│ │ ├── perm/ # 权限管理
|
||||
│ │ ├── request/ # API 请求封装
|
||||
│ │ ├── router/ # 路由配置
|
||||
│ │ ├── store/ # 状态管理
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ ├── views/ # 页面视图
|
||||
│ │ ├── app.vue # 根组件
|
||||
│ │ ├── main.js # 入口文件
|
||||
│ │ └── style.css # 全局样式
|
||||
│ ├── package.json
|
||||
│ └── vite.config.js # Vite 配置
|
||||
│
|
||||
├── doc/ # 文档和演示图片
|
||||
├── LICENSE # MIT 许可证
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
## 赞助
|
||||
## ⚙️ 配置说明
|
||||
|
||||
<a href="https://doc.skymail.ink/support.html" >
|
||||
<img width="170px" src="./doc/images/support.png" alt="">
|
||||
### 后端配置 (mail-worker/wrangler.toml)
|
||||
|
||||
```toml
|
||||
name = "cloud-mail"
|
||||
main = "src/index.js"
|
||||
compatibility_date = "2024-01-01"
|
||||
|
||||
# D1 数据库绑定
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "cloud-mail-db"
|
||||
database_id = "your-database-id"
|
||||
|
||||
# KV 缓存绑定
|
||||
[[kv_namespaces]]
|
||||
binding = "CACHE"
|
||||
id = "your-kv-id"
|
||||
|
||||
# R2 存储绑定
|
||||
[[r2_buckets]]
|
||||
binding = "STORAGE"
|
||||
bucket_name = "cloud-mail-storage"
|
||||
|
||||
# 环境变量
|
||||
[vars]
|
||||
RESEND_API_KEY = "your-resend-api-key"
|
||||
JWT_SECRET = "your-jwt-secret"
|
||||
ADMIN_EMAIL = "admin@yourdomain.com"
|
||||
```
|
||||
|
||||
### 前端配置 (mail-vue/.env)
|
||||
|
||||
```env
|
||||
# 开发环境
|
||||
VITE_API_BASE_URL=http://localhost:8787
|
||||
|
||||
# 生产环境
|
||||
VITE_API_BASE_URL=https://your-worker.your-subdomain.workers.dev
|
||||
```
|
||||
|
||||
## 🚢 部署指南
|
||||
|
||||
### 部署后端
|
||||
|
||||
```bash
|
||||
cd mail-worker
|
||||
|
||||
# 测试环境部署
|
||||
npm run test
|
||||
|
||||
# 生产环境部署
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
### 部署前端
|
||||
|
||||
前端需要构建后部署到任意静态托管服务(如 Cloudflare Pages、Vercel、Netlify 等):
|
||||
|
||||
```bash
|
||||
cd mail-vue
|
||||
|
||||
# 构建生产版本
|
||||
npm run build
|
||||
|
||||
# 构建结果在 dist/ 目录,可直接部署
|
||||
```
|
||||
|
||||
### 推荐使用 Cloudflare Pages 部署前端
|
||||
|
||||
1. 在 Cloudflare Dashboard 创建 Pages 项目
|
||||
2. 连接 GitHub 仓库
|
||||
3. 构建设置:
|
||||
- 构建命令:`cd mail-vue && npm run build`
|
||||
- 构建输出目录:`mail-vue/dist`
|
||||
4. 添加环境变量(参考上方配置说明)
|
||||
|
||||
## 📝 开发脚本
|
||||
|
||||
### 后端 (mail-worker)
|
||||
|
||||
| 命令 | 说明 |
|
||||
|:---|:---|
|
||||
| `npm run dev` | 本地开发模式 |
|
||||
| `npm run start` | 启动开发服务器(同 dev) |
|
||||
| `npm run test` | 部署到测试环境 |
|
||||
| `npm run deploy` | 部署到生产环境 |
|
||||
|
||||
### 前端 (mail-vue)
|
||||
|
||||
| 命令 | 说明 |
|
||||
|:---|:---|
|
||||
| `npm run dev` | 本地开发模式 |
|
||||
| `npm run remote` | 使用远程 API 开发 |
|
||||
| `npm run build` | 构建生产版本 |
|
||||
| `npm run eo` | 构建 EO 环境 |
|
||||
| `npm run preview` | 预览构建结果 |
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
1. Fork 本仓库
|
||||
2. 创建特性分支:`git checkout -b feature/amazing-feature`
|
||||
3. 提交更改:`git commit -m 'Add amazing feature'`
|
||||
4. 推送分支:`git push origin feature/amazing-feature`
|
||||
5. 创建 Pull Request
|
||||
|
||||
## ❓ 常见问题
|
||||
|
||||
**Q: 项目完全免费吗?**
|
||||
A: 基于 Cloudflare 免费套餐可以运行基本功能,但如果邮件量大或附件多,可能需要升级到付费套餐。
|
||||
|
||||
**Q: 支持自定义域名吗?**
|
||||
A: 支持,可以在 Cloudflare 中为 Workers 和 Pages 绑定自定义域名。
|
||||
|
||||
**Q: 如何接收邮件?**
|
||||
A: 需要在域名 DNS 中添加 MX 记录指向 Cloudflare Email Routing,具体参见部署文档。
|
||||
|
||||
## 💖 赞助支持
|
||||
|
||||
如果本项目对你有帮助,欢迎赞助支持开发者:
|
||||
|
||||
<a href="https://doc.skymail.ink/support.html">
|
||||
<img width="170px" src="./doc/images/support.png" alt="赞助" />
|
||||
</a>
|
||||
|
||||
## 许可证
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 [MIT](LICENSE) 许可证
|
||||
本项目基于 [MIT](LICENSE) 许可证开源。
|
||||
|
||||
## 💬 社区交流
|
||||
|
||||
## 交流
|
||||
|
||||
[Telegram](https://t.me/cloud_mail_tg)
|
||||
|
||||
- [Telegram 群组](https://t.me/cloud_mail_tg)
|
||||
- [GitHub Issues](https://github.com/maillab/cloud-mail/issues)
|
||||
|
||||
---
|
||||
|
||||
<p align="center">
|
||||
Made with ❤️ by Cloud Mail Team
|
||||
</p>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="theme-color" content="#F1F1F1" id="theme-color-meta">
|
||||
<meta name="description" content="Cloud Mail 是一个 Serverless 响应式邮箱服务,支持邮件发送,可部署到 Cloudflare 平台,降低服务器成本。Cloud Mail is a serverless responsive email service that supports email sending and can be deployed on Cloudflare to reduce server costs.">
|
||||
<title>Cloud Mail</title>
|
||||
<link rel="icon" href="/public/mail.png" type="image/png">
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<script>
|
||||
@@ -129,7 +129,7 @@
|
||||
<body>
|
||||
<div id="loading-first" >
|
||||
<div class="loading-icon">
|
||||
<img class="loading-image" src="/mail-pwa.png" alt="Cloud Mail">
|
||||
<img class="loading-image" src="/logo.png" alt="Cloud Mail">
|
||||
<div class="loading-progress" aria-hidden="true"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
8095
mail-vue/package-lock.json
generated
Normal file
8095
mail-vue/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
mail-vue/public/favicon.ico
Normal file
BIN
mail-vue/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 88 KiB |
BIN
mail-vue/public/logo.png
Normal file
BIN
mail-vue/public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
@@ -143,6 +143,7 @@ const en = {
|
||||
websiteTitle: 'Title',
|
||||
loginBoxOpacity: 'Login Box Opacity',
|
||||
loginBackground: 'Background',
|
||||
backgroundDarken: 'Background Dim',
|
||||
emailSetting: 'Email',
|
||||
receiveEmail: 'Receive Email',
|
||||
autoRefresh: 'Auto Refresh',
|
||||
|
||||
@@ -143,6 +143,7 @@ const zh = {
|
||||
websiteTitle: '网站标题',
|
||||
loginBoxOpacity: '登录透明',
|
||||
loginBackground: '登录背景',
|
||||
backgroundDarken: '背景压暗',
|
||||
emailSetting: '邮件设置',
|
||||
receiveEmail: '邮件接收',
|
||||
autoRefresh: '自动刷新',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-scrollbar class="scroll">
|
||||
<div>
|
||||
<div class="title" >
|
||||
<Icon icon="mdi:email-outline" width="24" height="24" />
|
||||
<img class="logo" src="/logo.png" alt="" width="24" height="24" />
|
||||
<div>{{settingStore.settings.title}}</div>
|
||||
</div>
|
||||
<el-menu :collapse="false" text-color="#fff" active-text-color="#fff" style="margin-top: 10px">
|
||||
@@ -105,6 +105,11 @@ const route = useRoute();
|
||||
max-width: calc(240px - 20px - 30px);
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
:deep(.el-icon) {
|
||||
flex-shrink: 0;
|
||||
font-size: 20px;
|
||||
|
||||
@@ -6,6 +6,7 @@ export const useSettingStore = defineStore('setting', {
|
||||
settings: {
|
||||
r2Domain: '',
|
||||
loginOpacity: 1.00,
|
||||
loginDarkenFactor: 0,
|
||||
},
|
||||
lang: '',
|
||||
}),
|
||||
|
||||
@@ -237,16 +237,27 @@ const loginOpacity = computed(() => {
|
||||
return uiStore.dark ? `rgba(0, 0, 0, ${opacity})` : `rgba(255, 255, 255, ${opacity})`
|
||||
})
|
||||
|
||||
const loginDarkenFactor = computed(() => {
|
||||
const factor = Number(settingStore.settings.loginDarkenFactor ?? 0)
|
||||
if (Number.isNaN(factor)) return 0
|
||||
return Math.min(1, Math.max(0, factor))
|
||||
})
|
||||
|
||||
const hideLoginDomain = computed(() => settingStore.settings.loginDomain === 1)
|
||||
|
||||
const background = computed(() => {
|
||||
|
||||
return settingStore.settings.background ? {
|
||||
'background-image': `url(${cvtR2Url(settingStore.settings.background)})`,
|
||||
'background-repeat': 'no-repeat',
|
||||
'background-size': 'cover',
|
||||
'background-position': 'center'
|
||||
} : ''
|
||||
const bg = settingStore.settings.background
|
||||
if (!bg) return ''
|
||||
const bgUrl = cvtR2Url(bg)
|
||||
return {
|
||||
'background-image': `
|
||||
linear-gradient(rgba(0, 0, 0, ${loginDarkenFactor.value}), rgba(0, 0, 0, ${loginDarkenFactor.value})),
|
||||
url(${bgUrl})
|
||||
`,
|
||||
'background-repeat': 'no-repeat, no-repeat',
|
||||
'background-size': 'cover, cover',
|
||||
'background-position': 'center, center'
|
||||
}
|
||||
})
|
||||
|
||||
const openSelect = () => {
|
||||
|
||||
@@ -94,6 +94,13 @@
|
||||
:step="0.01" :max="1" :min="0"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="title-item"><span>{{ $t('backgroundDarken') }}</span></div>
|
||||
<div>
|
||||
<el-input-number size="small" v-model="loginDarkenFactor" @change="darkenChange" :precision="2"
|
||||
:step="0.01" :max="1" :min="0"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-item personalized">
|
||||
<div><span>{{ $t('loginBackground') }}</span></div>
|
||||
<div>
|
||||
@@ -855,6 +862,7 @@ const settingLoading = ref(false)
|
||||
const clearS3Loading = ref(false)
|
||||
const r2DomainInput = ref('')
|
||||
const loginOpacity = ref(0)
|
||||
const loginDarkenFactor = ref(0)
|
||||
const minEmailPrefix = ref(0)
|
||||
const emailPrefixFilter = ref([])
|
||||
const backgroundUrl = ref('')
|
||||
@@ -946,6 +954,7 @@ function getSettings() {
|
||||
settingStore.domainList = settingData.domainList;
|
||||
resendTokenForm.domain = setting.value.domainList[0]
|
||||
loginOpacity.value = setting.value.loginOpacity
|
||||
loginDarkenFactor.value = normalizeFactor(setting.value.loginDarkenFactor)
|
||||
minEmailPrefix.value = setting.value.minEmailPrefix
|
||||
firstLoading.value = false
|
||||
backgroundUrl.value = setting.value.background?.startsWith('http') ? setting.value.background : ''
|
||||
@@ -1231,6 +1240,19 @@ function doOpacityChange() {
|
||||
editSetting(form, true)
|
||||
}
|
||||
|
||||
function normalizeFactor(value) {
|
||||
const factor = Number(value ?? 0)
|
||||
if (Number.isNaN(factor)) return 0
|
||||
return Math.min(1, Math.max(0, factor))
|
||||
}
|
||||
|
||||
function doDarkenChange() {
|
||||
if (!settingReady.value) return
|
||||
const form = {}
|
||||
form.loginDarkenFactor = normalizeFactor(loginDarkenFactor.value)
|
||||
editSetting(form, true)
|
||||
}
|
||||
|
||||
function resetEmailPrefix() {
|
||||
minEmailPrefix.value = setting.value.minEmailPrefix
|
||||
emailPrefixFilter.value = setting.value.emailPrefixFilter
|
||||
@@ -1262,6 +1284,11 @@ const opacityChange = debounce(doOpacityChange, 1000, {
|
||||
trailing: true
|
||||
})
|
||||
|
||||
const darkenChange = debounce(doDarkenChange, 1000, {
|
||||
leading: false,
|
||||
trailing: true
|
||||
})
|
||||
|
||||
function saveBlackList() {
|
||||
|
||||
let form = {
|
||||
@@ -1494,6 +1521,7 @@ function editSetting(settingForm, refreshStatus = true) {
|
||||
aiCodeFilterShow.value = false
|
||||
}).catch((e) => {
|
||||
loginOpacity.value = setting.value.loginOpacity
|
||||
loginDarkenFactor.value = normalizeFactor(setting.value.loginDarkenFactor)
|
||||
setting.value = {...setting.value, ...JSON.parse(backup)}
|
||||
}).finally(() => {
|
||||
settingLoading.value = false
|
||||
|
||||
@@ -25,7 +25,7 @@ export default defineConfig(({mode}) => {
|
||||
theme_color: '#FFFFFF',
|
||||
icons: [
|
||||
{
|
||||
src: 'mail-pwa.png',
|
||||
src: 'logo.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
}
|
||||
|
||||
6501
mail-worker/package-lock.json
generated
Normal file
6501
mail-worker/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@ export const setting = sqliteTable('setting', {
|
||||
ruleEmail: text('rule_email').default('').notNull(),
|
||||
ruleType: integer('rule_type').default(0).notNull(),
|
||||
loginOpacity: integer('login_opacity').default(0.88),
|
||||
loginDarkenFactor: integer('login_darken_factor').default(0),
|
||||
resendTokens: text('resend_tokens').default("{}").notNull(),
|
||||
noticeTitle: text('notice_title').default('').notNull(),
|
||||
noticeContent: text('notice_content').default('').notNull(),
|
||||
|
||||
@@ -29,10 +29,19 @@ const dbInit = {
|
||||
await this.v2_8DB(c);
|
||||
await this.v2_9DB(c);
|
||||
await this.v3_0DB(c);
|
||||
await this.v3_1DB(c);
|
||||
await settingService.refresh(c);
|
||||
return c.text('success');
|
||||
},
|
||||
|
||||
async v3_1DB(c) {
|
||||
try {
|
||||
await c.env.db.prepare(`ALTER TABLE setting ADD COLUMN login_darken_factor INTEGER NOT NULL DEFAULT 0;`).run();
|
||||
} catch (e) {
|
||||
console.warn(`跳过字段:${e.message}`);
|
||||
}
|
||||
},
|
||||
|
||||
async v3_0DB(c) {
|
||||
try {
|
||||
await c.env.db.batch([
|
||||
@@ -597,9 +606,7 @@ const dbInit = {
|
||||
title TEXT NOT NULL,
|
||||
auto_refresh INTEGER NOT NULL,
|
||||
register_verify INTEGER NOT NULL,
|
||||
add_email_verify INTEGER NOT NULL,
|
||||
ai_code INTEGER NOT NULL DEFAULT 1,
|
||||
ai_code_filter TEXT NOT NULL DEFAULT ''
|
||||
add_email_verify INTEGER NOT NULL
|
||||
)
|
||||
`).run();
|
||||
|
||||
|
||||
@@ -91,13 +91,10 @@ const attService = {
|
||||
|
||||
if (src.startsWith(domainUtils.toOssDomain(r2Domain))) {
|
||||
attData.key = src.replace(domainUtils.toOssDomain(r2Domain) + '/','');
|
||||
attData.path = src;
|
||||
}
|
||||
|
||||
if (src.startsWith('attachments/')) {
|
||||
const origin = new URL(c.req.url).origin;
|
||||
attData.key = src;
|
||||
attData.path = origin + '/' + src;
|
||||
}
|
||||
|
||||
attData.contentId = cid;
|
||||
@@ -117,22 +114,34 @@ const attService = {
|
||||
}
|
||||
|
||||
//查询已有内嵌url图片信息
|
||||
const keys = [...new Set(imageDataList.filter(item => item.path).map(item => item.key))];
|
||||
const keys = [...new Set(imageDataList.filter(item => !item.content).map(item => item.key))];
|
||||
const dbImageList = await this.selectOneByKeys(c, keys);
|
||||
|
||||
//设置给当前附件
|
||||
imageDataList.forEach(image => {
|
||||
dbImageList.forEach(dbImage => {
|
||||
if (image.path && (image.key === dbImage.key)) {
|
||||
image.size = dbImage.size;
|
||||
image.filename = dbImage.filename;
|
||||
image.mimeType = dbImage.mimeType;
|
||||
image.contentType = dbImage.mimeType;
|
||||
}
|
||||
})
|
||||
})
|
||||
await Promise.all(imageDataList.map(async image => {
|
||||
if (image.content) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageDataList = imageDataList.filter(image => !image.path || image.size);
|
||||
const dbImage = dbImageList.find(dbImage => image.key === dbImage.key);
|
||||
if (!dbImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
image.size = dbImage.size;
|
||||
image.filename = dbImage.filename;
|
||||
image.mimeType = dbImage.mimeType;
|
||||
image.contentType = dbImage.mimeType;
|
||||
|
||||
const obj = await r2Service.getObj(c, image.key);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
|
||||
image.content = obj instanceof ArrayBuffer ? obj : await obj.arrayBuffer();
|
||||
}))
|
||||
|
||||
imageDataList = imageDataList.filter(image => image.content);
|
||||
|
||||
return { imageDataList, html: document.toString() };
|
||||
},
|
||||
|
||||
@@ -311,7 +311,7 @@ const emailService = {
|
||||
emailData.content = html;
|
||||
emailData.text = text;
|
||||
emailData.accountId = accountId;
|
||||
emailData.status = emailConst.status.SENT;
|
||||
emailData.status = useCloudflareEmail ? emailConst.status.DELIVERED : emailConst.status.SENT;
|
||||
emailData.type = emailConst.type.SEND;
|
||||
emailData.userId = userId;
|
||||
emailData.resendEmailId = data?.id;
|
||||
@@ -402,8 +402,6 @@ const emailService = {
|
||||
};
|
||||
}
|
||||
|
||||
console.log(sendForm)
|
||||
|
||||
const result = await c.env.email.send(sendForm);
|
||||
|
||||
return {
|
||||
@@ -422,7 +420,7 @@ const emailService = {
|
||||
subject: params.subject,
|
||||
text: params.text,
|
||||
html: params.html,
|
||||
attachments: params.attachments
|
||||
attachments: await this.toResendAttachments(params.attachments)
|
||||
};
|
||||
|
||||
if (params.sendType === 'reply') {
|
||||
@@ -436,34 +434,11 @@ const emailService = {
|
||||
},
|
||||
|
||||
async toCloudflareAttachments(attachments) {
|
||||
const arrayBufferAttachments = await this.toArrayBufferAttachments(attachments);
|
||||
|
||||
const result = [];
|
||||
|
||||
for (const attachment of attachments) {
|
||||
let content = attachment.content;
|
||||
|
||||
if (!content && attachment.path) {
|
||||
const response = await fetch(attachment.path);
|
||||
if (!response.ok) {
|
||||
throw new BizError(`Attachment fetch failed: ${attachment.filename}`);
|
||||
}
|
||||
content = await response.arrayBuffer();
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof content === 'string' && content.startsWith('data:')) {
|
||||
content = content.split(',')[1] || content;
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
content = fileUtils.base64ToUint8Array(content.replace(/\s+/g, '')).buffer;
|
||||
}
|
||||
|
||||
return arrayBufferAttachments.map(attachment => {
|
||||
const item = {
|
||||
content,
|
||||
content: attachment.content,
|
||||
filename: attachment.filename,
|
||||
type: attachment.mimeType || attachment.contentType || attachment.type || 'application/octet-stream',
|
||||
disposition: attachment.contentId ? 'inline' : 'attachment'
|
||||
@@ -473,12 +448,98 @@ const emailService = {
|
||||
item.contentId = attachment.contentId.replace(/^<|>$/g, '');
|
||||
}
|
||||
|
||||
result.push(item);
|
||||
return item;
|
||||
});
|
||||
},
|
||||
|
||||
async toResendAttachments(attachments = []) {
|
||||
const result = [];
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const content = await this.toAttachmentBase64(attachment);
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({
|
||||
...attachment,
|
||||
content,
|
||||
contentType: attachment.contentType || attachment.mimeType || attachment.type || 'application/octet-stream'
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
async toArrayBufferAttachments(attachments = []) {
|
||||
const result = [];
|
||||
|
||||
for (const attachment of attachments) {
|
||||
const content = await this.toAttachmentArrayBuffer(attachment);
|
||||
if (!content) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({ ...attachment, content });
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
async toAttachmentBase64(attachment) {
|
||||
let content = attachment.content;
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
if (content.startsWith('data:')) {
|
||||
content = content.split(',')[1] || content;
|
||||
}
|
||||
return content.replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
const arrayBuffer = await this.toAttachmentArrayBuffer(attachment);
|
||||
if (!arrayBuffer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
let binary = '';
|
||||
|
||||
for (let i = 0; i < bytes.length; i += 0x8000) {
|
||||
binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
||||
}
|
||||
|
||||
return btoa(binary);
|
||||
},
|
||||
|
||||
async toAttachmentArrayBuffer(attachment) {
|
||||
let content = attachment.content;
|
||||
|
||||
if (!content) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (content instanceof ArrayBuffer) {
|
||||
return content;
|
||||
}
|
||||
|
||||
if (content instanceof Uint8Array) {
|
||||
return content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
|
||||
}
|
||||
|
||||
if (typeof content === 'string') {
|
||||
if (content.startsWith('data:')) {
|
||||
content = content.split(',')[1] || content;
|
||||
}
|
||||
return fileUtils.base64ToUint8Array(content.replace(/\s+/g, '')).buffer;
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
//处理站内邮件发送
|
||||
async HandleOnSiteEmail(c, receiveEmail, sendEmailData, attList) {
|
||||
|
||||
|
||||
@@ -17,9 +17,11 @@ const kvObjService = {
|
||||
await Promise.all(keys.map( key => c.env.kv.delete(key)));
|
||||
},
|
||||
|
||||
async toObjResp(c, key) {
|
||||
|
||||
async getObj(c, key) {
|
||||
const obj = await c.env.kv.getWithMetadata(key, { type: "arrayBuffer"});
|
||||
if (!obj.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Response(obj.value, {
|
||||
headers: {
|
||||
@@ -28,6 +30,11 @@ const kvObjService = {
|
||||
'Cache-Control': obj.metadata?.cacheControl || null
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
async toObjResp(c, key) {
|
||||
|
||||
return await this.getObj(c, key);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,19 @@ const r2Service = {
|
||||
},
|
||||
|
||||
async getObj(c, key) {
|
||||
return await c.env.r2.get(key);
|
||||
const storageType = await this.storageType(c);
|
||||
|
||||
if (storageType === 'KV') {
|
||||
return await kvObjService.getObj(c, key);
|
||||
}
|
||||
|
||||
if (storageType === 'R2') {
|
||||
return await c.env.r2.get(key);
|
||||
}
|
||||
|
||||
if (storageType === 'S3') {
|
||||
return await s3Service.getObj(c, key);
|
||||
}
|
||||
},
|
||||
|
||||
async delete(c, key) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { S3Client, PutObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
||||
import { S3Client, PutObjectCommand, DeleteObjectsCommand, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import settingService from './setting-service';
|
||||
import domainUtils from '../utils/domain-uitls';
|
||||
import { settingConst } from '../const/entity-const';
|
||||
@@ -75,6 +75,23 @@ const s3Service = {
|
||||
);
|
||||
},
|
||||
|
||||
async getObj(c, key) {
|
||||
const client = await this.client(c);
|
||||
const { bucket } = await settingService.query(c);
|
||||
const result = await client.send(new GetObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key
|
||||
}));
|
||||
|
||||
return new Response(result.Body, {
|
||||
headers: {
|
||||
'Content-Type': result.ContentType || 'application/octet-stream',
|
||||
'Content-Disposition': result.ContentDisposition || null,
|
||||
'Cache-Control': result.CacheControl || null
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
async client(c) {
|
||||
const { region, endpoint, s3AccessKey, s3SecretKey, forcePathStyle } = await settingService.query(c);
|
||||
|
||||
@@ -138,6 +138,11 @@ const settingService = {
|
||||
params.aiCodeFilter = params.aiCodeFilter + '';
|
||||
}
|
||||
|
||||
if (params.loginDarkenFactor !== undefined) {
|
||||
const factor = Number(params.loginDarkenFactor);
|
||||
params.loginDarkenFactor = Number.isNaN(factor) ? 0 : Math.min(1, Math.max(0, factor));
|
||||
}
|
||||
|
||||
params.resendTokens = JSON.stringify(resendTokens);
|
||||
await orm(c).update(setting).set({ ...params }).returning().get();
|
||||
await this.refresh(c);
|
||||
@@ -214,6 +219,7 @@ const settingService = {
|
||||
siteKey: settingRow.siteKey,
|
||||
background: settingRow.background,
|
||||
loginOpacity: settingRow.loginOpacity,
|
||||
loginDarkenFactor: settingRow.loginDarkenFactor,
|
||||
domainList: settingRow.loginDomain === 1 && !token ? [] : settingRow.domainList,
|
||||
regKey: settingRow.regKey,
|
||||
regVerifyOpen: settingRow.regVerifyOpen,
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
name = "${NAME}"
|
||||
main = "src/index.js"
|
||||
compatibility_date = "2025-06-04"
|
||||
|
||||
[observability]
|
||||
enabled = true
|
||||
|
||||
[[routes]]
|
||||
pattern = "${CUSTOM_DOMAIN}"
|
||||
custom_domain = true
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "db"
|
||||
database_name = "cloud-mail"
|
||||
database_id = "${D1_DATABASE_ID}"
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "kv"
|
||||
id = "${KV_NAMESPACE_ID}"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "r2"
|
||||
bucket_name = "${R2_BUCKET_NAME}"
|
||||
|
||||
[ai]
|
||||
binding = "ai"
|
||||
|
||||
[assets]
|
||||
binding = "assets"
|
||||
directory = "./dist"
|
||||
not_found_handling = "single-page-application"
|
||||
run_worker_first = true
|
||||
|
||||
[triggers]
|
||||
crons = ["*/30 * * * *", "0 16 * * *"]
|
||||
|
||||
|
||||
[vars]
|
||||
ai_model = "${AI_MODEL}"
|
||||
analysis_cache = "${ANALYSIS_CACHE}"
|
||||
#orm_log = false
|
||||
domain = "${DOMAIN}"
|
||||
admin = "${ADMIN}"
|
||||
jwt_secret = "${JWT_SECRET}"
|
||||
project_link = "${PROJECT_LINK}"
|
||||
|
||||
linuxdo_client_id = "${LINUXDO_CLIENT_ID}"
|
||||
linuxdo_client_secret = "${LINUXDO_CLIENT_SECRET}"
|
||||
linuxdo_callback_url = "${LINUXDO_CALLBACK_URL}"
|
||||
linuxdo_switch = "${LINUXDO_SWITCH}"
|
||||
|
||||
[build]
|
||||
command = "pnpm --prefix ../mail-vue install && pnpm --prefix ../mail-vue run build"
|
||||
@@ -1,44 +0,0 @@
|
||||
name = "cloud-mail-dev"
|
||||
main = "src/index.js"
|
||||
compatibility_date = "2025-06-04"
|
||||
keep_vars = true
|
||||
|
||||
[observability]
|
||||
enabled = true
|
||||
|
||||
[dev]
|
||||
ip = "0.0.0.0"
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "db"
|
||||
database_name = "email"
|
||||
database_id = "a4c1a63a-6ef5-4e6d-8e8c-b6d9e8feb810"
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "kv"
|
||||
id = "2io01d4b299e481b9de060ece9e7785c"
|
||||
|
||||
#[[r2_buckets]]
|
||||
#binding = "r2"
|
||||
#bucket_name = "email"
|
||||
|
||||
#[ai]
|
||||
#binding = "ai"
|
||||
|
||||
[assets]
|
||||
binding = "assets"
|
||||
directory = "./dist"
|
||||
not_found_handling = "single-page-application"
|
||||
run_worker_first = true
|
||||
|
||||
[[send_email]]
|
||||
name = "email"
|
||||
|
||||
|
||||
[vars]
|
||||
ai_model = ""
|
||||
analysis_cache = false
|
||||
orm_log = false
|
||||
domain = ["example.com", "example2.com", "example3.com", "example4.com"]
|
||||
admin = "admin@example.com"
|
||||
jwt_secret = "b7f29a1d-18e2-4d3b-941f-f6b2c97c02fd"
|
||||
@@ -1,45 +0,0 @@
|
||||
name = "test-mail"
|
||||
main = "src/index.js"
|
||||
compatibility_date = "2025-06-04"
|
||||
keep_vars = true
|
||||
|
||||
[observability]
|
||||
enabled = true
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "db" #d1数据库绑定名默认不可修改
|
||||
database_name = "test-email" #d1数据库名字
|
||||
database_id = "e65f099d-796d-4eaa-8dff-529b368b20db" #d1数据库id
|
||||
|
||||
[[kv_namespaces]]
|
||||
binding = "kv" #kv绑定名默认不可修改
|
||||
id = "9be10cd058e04c55b526f8c0c116f50c" #kv数据库id
|
||||
|
||||
#(可选)
|
||||
#[[r2_buckets]]
|
||||
#binding = "r2" #r2对象存储绑定名默认不可修改
|
||||
#bucket_name = "test-email" #r2对象存储桶的名字
|
||||
|
||||
[ai]
|
||||
binding = "ai"
|
||||
|
||||
[assets]
|
||||
binding = "assets" #静态资源绑定名默认不可修改
|
||||
directory = "./dist" #前端vue项目打包的静态资源存放位置,默认dist
|
||||
not_found_handling = "single-page-application"
|
||||
run_worker_first = true
|
||||
|
||||
[triggers]
|
||||
crons = ["*/30 * * * *", "0 16 * * *"] #每30分钟刷新分析缓存,每天晚上12点执行每日任务
|
||||
|
||||
|
||||
[vars]
|
||||
ai_model = "" #ai模型,不填默认使用@cf/meta/llama-3.1-8b-instruct
|
||||
analysis_cache = false #是否开启分析数据缓存
|
||||
orm_log = false #是否sql日志
|
||||
domain = ["example.com"] #邮件域名可可配置多个 示例: ["example1.com","example2.com"]
|
||||
admin = "admin@example.com" #管理员的邮箱 示例: admin@example.com
|
||||
jwt_secret = "b7f29a1d-18e2-4d3b-941f-f6b2c97c02fd" #jwt令牌的密钥,随便填一串字符串
|
||||
|
||||
[build]
|
||||
command = "pnpm --prefix ../mail-vue install && pnpm --prefix ../mail-vue run build"
|
||||
Reference in New Issue
Block a user