├── .gitignore
├── LICENSE
├── README.md
├── demo
├── demo1.png
├── demo2.png
├── demo3.png
├── demo4.png
└── logo.png
├── mail-vue
├── .env.dev
├── .env.release
├── .env.remote
├── index.html
├── package-lock.json
├── package.json
├── public
│ └── vite.svg
├── src
│ ├── App.vue
│ ├── assets
│ │ ├── favicon.svg
│ │ └── fonts
│ │ │ └── HarmonyOS_Sans_SC_Regular.woff2
│ ├── axios
│ │ └── index.js
│ ├── components
│ │ ├── email-scroll
│ │ │ └── index.vue
│ │ ├── hamburger
│ │ │ └── index.vue
│ │ └── loading
│ │ │ └── index.vue
│ ├── day
│ │ └── day.js
│ ├── layout
│ │ ├── account
│ │ │ └── index.vue
│ │ ├── aside
│ │ │ └── index.vue
│ │ ├── header
│ │ │ └── index.vue
│ │ ├── index.vue
│ │ └── main
│ │ │ └── index.vue
│ ├── main.js
│ ├── request
│ │ ├── account.js
│ │ ├── email.js
│ │ ├── login.js
│ │ ├── send.js
│ │ ├── setting.js
│ │ ├── star.js
│ │ └── user.js
│ ├── router
│ │ └── index.js
│ ├── store
│ │ ├── account.js
│ │ ├── email.js
│ │ ├── send.js
│ │ ├── setting.js
│ │ ├── ui.js
│ │ ├── user.js
│ │ └── writer.js
│ ├── style.css
│ ├── utils
│ │ ├── convert-utils.js
│ │ ├── file-utils.js
│ │ ├── time-utils.js
│ │ └── verify-utils.js
│ └── views
│ │ ├── content
│ │ └── index.vue
│ │ ├── email
│ │ └── index.vue
│ │ ├── login
│ │ └── index.vue
│ │ ├── setting
│ │ └── index.vue
│ │ ├── star
│ │ └── index.vue
│ │ ├── sys-setting
│ │ └── index.vue
│ │ └── test
│ │ └── index.vue
└── vite.config.js
├── mail-worker
├── .editorconfig
├── .prettierrc
├── dist
│ ├── assets
│ │ ├── HarmonyOS_Sans_SC_Regular-D3EGA0gC.woff2
│ │ ├── favicon-C5dAZutX.svg
│ │ ├── index-BRburrSt.js
│ │ └── index-CGZvsPhs.css
│ ├── index.html
│ └── vite.svg
├── package-lock.json
├── package.json
├── src
│ ├── api
│ │ ├── account-api.js
│ │ ├── email-api.js
│ │ ├── login-api.js
│ │ ├── setting-api.js
│ │ ├── star-api.js
│ │ ├── test-api.js
│ │ └── user-api.js
│ ├── const
│ │ ├── constant.js
│ │ ├── entity-const.js
│ │ └── kv-const.js
│ ├── email
│ │ └── email.js
│ ├── entity
│ │ ├── account.js
│ │ ├── att.js
│ │ ├── email.js
│ │ ├── orm.js
│ │ ├── setting.js
│ │ ├── star.js
│ │ └── user.js
│ ├── error
│ │ └── biz-error.js
│ ├── hono
│ │ ├── hono.js
│ │ └── webs.js
│ ├── index.js
│ ├── init
│ │ ├── init-cache.js
│ │ └── init-db.js
│ ├── model
│ │ └── result.js
│ ├── security
│ │ ├── security.js
│ │ └── user-context.js
│ ├── service
│ │ ├── account-service.js
│ │ ├── att-service.js
│ │ ├── email-service.js
│ │ ├── login-service.js
│ │ ├── r2-service.js
│ │ ├── setting-service.js
│ │ ├── star-service.js
│ │ ├── turnstile-service.js
│ │ └── user-service.js
│ └── utils
│ │ ├── crypto-utils.js
│ │ ├── email-utils.js
│ │ ├── file-utils.js
│ │ ├── jwt-utils.js
│ │ └── verify-utils.js
├── test
│ └── index.spec.js
├── vitest.config.js
├── wrangler-dev.toml
├── wrangler-test.toml
└── wrangler.toml
└── package-lock.json
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | *.log
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 | pnpm-debug.log*
7 | lerna-debug.log*
8 |
9 | node_modules
10 | dist-ssr
11 | *.local
12 |
13 | .vscode/*
14 | !.vscode/extensions.json
15 | .idea
16 | .DS_Store
17 | *.suo
18 | *.ntvs*
19 | *.njsproj
20 | *.sln
21 | *.sw?
22 |
23 | mail-vue/node_modules
24 | mail-vue/dist
25 | .wrangler
26 | .venv
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2025 LaziestRen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
Cloud Mail
7 |
8 |
9 |
一个使用Vue3开发的响应式简约邮箱服务, 可以部署到Cloudflare云平台实现免费白嫖🎉
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ## 在线演示
18 |
19 | 👉 https://skymail.ink
20 |
21 | |  |  |
22 | |--------------------------------------------------------|---------------------|
23 | |  |  |
24 |
25 |
26 |
27 |
28 |
29 | ## 功能介绍
30 |
31 | - **💰免费白嫖**:无需服务器,部署到Cloudflare Workers 免费使用,不要钱
32 |
33 | - **💻响应式设计**:响应式布局自动适配PC和大部分手机端浏览器
34 |
35 | - **🔀多号模式**:开启后一个用户可以添加多个邮箱,默认一用户一邮箱,类似各大邮箱平台
36 |
37 | - **📦附件接收**:支持接收附件,使用R2对象存储保存和下载文件
38 |
39 | - **⭐星标邮件**:标记重要邮件,以便快速查阅
40 |
41 | - **🎨个性化标题**:可以自定义网站标题
42 |
43 | - **⏱️轮询刷新**:轮询请求服务器自动获取最新邮件,可自定义间隔
44 |
45 | - **⚙️功能开关**:可以对注册,添加等功能关闭和开启,设为私人站点
46 |
47 | - **🤖人机验证**:集成Turnstile人机验证,防止人机批量注册
48 |
49 | - **📜更多功能**:正在开发中...
50 |
51 |
52 |
53 | ## 技术栈
54 |
55 | - **框架**:[Vue3](https://vuejs.org/) + [Element Plus](https://element-plus.org/)
56 |
57 | - **Web框架**:[Hono](https://hono.dev/)
58 |
59 | - **ORM:**[Drizzle](https://orm.drizzle.team/)
60 |
61 | - **平台:** [Cloudflare workers](https://developers.cloudflare.com/workers/)
62 |
63 | - **缓存**:[Cloudflare KV](https://developers.cloudflare.com/kv/)
64 |
65 | - **数据库**:[Cloudflare D1](https://developers.cloudflare.com/d1/)
66 |
67 | - **文件存储**:[Cloudflare R2](https://developers.cloudflare.com/r2/)
68 |
69 |
70 |
71 |
72 |
73 | ## 使用教程
74 |
75 | [**👉小白保姆教程-界面部署**](https://doc.skymail.ink)
76 |
77 | ### 环境要求
78 |
79 |
80 |
81 | Nodejs v18.20 +
82 |
83 | Cloudflare 账号
84 |
85 |
86 | **克隆项目到本地**
87 | ``` shell
88 | git clone https://github.com/LaziestRen/cloud-mail #拉取代码
89 | cd cloud-mail/mail-worker #进入worker目录
90 | ```
91 |
92 | **安装依赖**
93 | ```shell
94 | npm i
95 | ```
96 |
97 | **项目配置**
98 |
99 | ```toml
100 | [[d1_databases]]
101 | binding = "db" #d1数据库绑定名默认不可修改
102 | database_name = "" #d1数据库名字
103 | database_id = "" #d1数据库id
104 |
105 | [[kv_namespaces]]
106 | binding = "kv" #kv绑定名默认不可修改
107 | id = "" #kv数据库id
108 |
109 | #(可选)
110 | [[r2_buckets]]
111 | binding = "r2" #r2对象存储绑定名默认不可修改
112 | bucket_name = "" #r2对象存储桶的名字
113 |
114 |
115 | [assets]
116 | binding = "assets" #静态资源绑定名默认不可修改
117 | directory = "./dist" #前端vue项目打包的静态资源存放位置,默认dist
118 |
119 | [vars]
120 | domain = [] #邮件域名可以配置多个 示例: ["example1.com","example2.com"]
121 | admin = "" #管理员的邮箱 示例: admin@example.com
122 | jwt_secret = "" #jwt令牌的密钥,随便填一串字符串
123 | r2_domain = "" #r2对象存储桶的访问域名(可选)
124 | site_key = "" #Turnstile人机验证的站点密钥(可选)
125 | secret_key = "" #Turnstile人机验证的后端密钥(可选)
126 |
127 | ```
128 |
129 | **本地运行**
130 |
131 | 本地开发环境,数据库会自动安装,无需创建
132 |
133 | ```shell
134 | npm run dev
135 | ```
136 |
137 |
138 | **远程部署**
139 |
140 | 在Cloudflare控制台创建KV和D1数据库,可选:R2对象存储,Turnstile人机验证 (这两个不配置附件和人机验证不可用)
141 |
142 | 在 wrangler.toml 中配置对应环境变量
143 |
144 | ```shell
145 | npm run deploy
146 | ```
147 |
148 | 然后进入域名管理->电子邮件->路由规则->Catch-all 地址. 这里选择发送到 worker, 然后选择创建的worker
149 |
150 |
151 |
152 |
153 | ## 目录结构
154 |
155 | ```
156 | cloud-mail
157 | ├── mail-worker #worker后端项目
158 | │ ├── src
159 | │ │ ├── api #接口层
160 | │ │ ├── const #常量
161 | │ │ ├── email #邮件接收
162 | │ │ ├── entity #数据库实体层
163 | │ │ ├── error #自定义异常
164 | │ │ ├── hono #web框架配置 拦截器等
165 | │ │ ├── init #项目启动表创建 缓存初始化等
166 | │ │ ├── model #响应体数据封装
167 | │ │ ├── security #身份认证层
168 | │ │ ├── service #服务层
169 | │ │ ├── utils #工具类
170 | │ │ └── index.js #入口文件
171 | │ ├── pageckge.json #项目依赖
172 | │ └── wrangler.toml #项目配置
173 | └── mail-vue #vue前端项目
174 | ├── src
175 | │ ├── assets #静态资源字体等
176 | │ ├── axios #axios配置
177 | │ ├── components #自定义组件
178 | │ ├── day #dayjs配置
179 | │ ├── layout #主体布局组件
180 | │ ├── request #api接口
181 | │ ├── router #路由配置
182 | │ ├── store #全局状态管理
183 | │ ├── utils #工具类
184 | │ ├── views #页面组件
185 | │ ├── app.vue #根组件
186 | │ ├── main.js #入口js
187 | │ └── style.css #全局css
188 | ├── package.json #项目依赖
189 | └── env.dev #项目配置
190 | ```
191 |
192 |
193 |
194 | ## 许可证
195 |
196 | 本项目采用 [MIT](LICENSE) 许可证
197 |
198 |
199 |
200 |
201 |
202 |
--------------------------------------------------------------------------------
/demo/demo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaziestRen/cloud-mail/d4a5b297b34c72cdebe05eb589e30dea858189ec/demo/demo1.png
--------------------------------------------------------------------------------
/demo/demo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaziestRen/cloud-mail/d4a5b297b34c72cdebe05eb589e30dea858189ec/demo/demo2.png
--------------------------------------------------------------------------------
/demo/demo3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaziestRen/cloud-mail/d4a5b297b34c72cdebe05eb589e30dea858189ec/demo/demo3.png
--------------------------------------------------------------------------------
/demo/demo4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaziestRen/cloud-mail/d4a5b297b34c72cdebe05eb589e30dea858189ec/demo/demo4.png
--------------------------------------------------------------------------------
/demo/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaziestRen/cloud-mail/d4a5b297b34c72cdebe05eb589e30dea858189ec/demo/logo.png
--------------------------------------------------------------------------------
/mail-vue/.env.dev:
--------------------------------------------------------------------------------
1 | NODE_ENV = 'dev'
2 | VITE_APP_TITLE = '开发环境'
3 | VITE_BASE_URL = 'http://127.0.0.1:8787/api'
--------------------------------------------------------------------------------
/mail-vue/.env.release:
--------------------------------------------------------------------------------
1 | NODE_ENV = 'release'
2 | VITE_APP_TITLE = '发布环境'
3 | VITE_BASE_URL = '/api'
--------------------------------------------------------------------------------
/mail-vue/.env.remote:
--------------------------------------------------------------------------------
1 | NODE_ENV = 'remote'
2 | VITE_APP_TITLE = '远程环境'
3 | VITE_BASE_URL = 'xxxxxx'
--------------------------------------------------------------------------------
/mail-vue/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/mail-vue/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "video-admin-vue",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite --mode dev",
8 | "remote": "vite --mode remote",
9 | "build": "vite build --mode release",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@iconify/vue": "^4.3.0",
14 | "@vueuse/core": "^12.0.0",
15 | "@wangeditor/editor": "^5.1.23",
16 | "@wangeditor/editor-for-vue": "^5.1.12",
17 | "axios": "^1.7.8",
18 | "date-time-format-timezone": "^1.0.22",
19 | "dayjs": "^1.11.13",
20 | "element-plus": "^2.9.5",
21 | "path": "^0.12.7",
22 | "pinia": "^3.0.2",
23 | "pinia-plugin-persistedstate": "^4.2.0",
24 | "postal-mime": "^2.4.3",
25 | "vue": "^3.5.13",
26 | "vue-router": "^4.5.0"
27 | },
28 | "devDependencies": {
29 | "@vitejs/plugin-vue": "^5.2.1",
30 | "less": "^4.2.2",
31 | "sass": "^1.82.0",
32 | "terser": "^5.39.0",
33 | "vite": "6.2.6"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/mail-vue/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mail-vue/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
21 |
22 |
--------------------------------------------------------------------------------
/mail-vue/src/assets/favicon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mail-vue/src/assets/fonts/HarmonyOS_Sans_SC_Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaziestRen/cloud-mail/d4a5b297b34c72cdebe05eb589e30dea858189ec/mail-vue/src/assets/fonts/HarmonyOS_Sans_SC_Regular.woff2
--------------------------------------------------------------------------------
/mail-vue/src/axios/index.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 | import router from "@/router";
3 | import {ElMessage} from 'element-plus';
4 |
5 | let http = axios.create({
6 | baseURL: import.meta.env.VITE_BASE_URL
7 | });
8 |
9 | http.interceptors.request.use(config => {
10 | config.headers.Authorization = `${localStorage.getItem('token')}`
11 | return config
12 | })
13 |
14 | http.interceptors.response.use((res) => {
15 | return new Promise((resolve, reject) => {
16 | const data = res.data
17 | if (data.code === 401) {
18 | ElMessage({
19 | message: data.message,
20 | type: 'error',
21 | plain: true,
22 | })
23 | localStorage.removeItem('token')
24 | router.push('/login')
25 | reject(data)
26 | } else if (data.code === 403) {
27 | ElMessage({
28 | message: data.message,
29 | type: 'error',
30 | plain: true,
31 | })
32 | reject(data)
33 | } else if (data.code !== 200) {
34 | ElMessage({
35 | message: data.message,
36 | type: 'error',
37 | plain: true,
38 | })
39 | setTimeout(() => {
40 | reject(data)
41 | }, 1)
42 | }
43 | setTimeout(() => {
44 | resolve(data.data)
45 | }, 1)
46 | })
47 | },
48 | (error) => {
49 | if (error.message.includes('Network Error')) {
50 | ElMessage({
51 | message: '网络错误,请检查网络连接',
52 | type: 'error',
53 | plain: true,
54 | })
55 | } else if (error.code === 'ECONNABORTED') {
56 | ElMessage({
57 | message: '请求超时,请稍后重试',
58 | type: 'error',
59 | plain: true,
60 | })
61 | ElMessage.error('')
62 | } else if (error.response) {
63 | ElMessage({
64 | message: `服务器繁忙`,
65 | type: 'error',
66 | plain: true,
67 | })
68 | } else {
69 | ElMessage({
70 | message: '请求失败,请稍后再试',
71 | type: 'error',
72 | plain: true,
73 | })
74 | }
75 | return Promise.reject(error)
76 | })
77 |
78 | export default http
79 |
80 |
81 |
--------------------------------------------------------------------------------
/mail-vue/src/components/email-scroll/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
61 |
62 |
63 |
311 |
312 |
495 |
--------------------------------------------------------------------------------
/mail-vue/src/components/hamburger/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
30 |
31 |
43 |
--------------------------------------------------------------------------------
/mail-vue/src/components/loading/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
18 |
28 |
29 |
--------------------------------------------------------------------------------
/mail-vue/src/day/day.js:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 | import 'dayjs/locale/zh-cn'
3 | import utc from 'dayjs/plugin/utc'
4 | import timezone from 'dayjs/plugin/timezone'
5 | import relativeTime from 'dayjs/plugin/relativeTime'
6 |
7 | dayjs.extend(relativeTime)
8 | dayjs.extend(utc)
9 | dayjs.extend(timezone)
10 | dayjs.locale('zh-cn')
11 | dayjs.tz.setDefault('Asia/Shanghai')
12 |
13 | function day(time) {
14 | return dayjs(time).add(8, 'hours')
15 | }
16 |
17 | export const fromNow = (time) => {
18 | return time ? day(time).fromNow() : ''
19 | }
20 |
21 | export default day
--------------------------------------------------------------------------------
/mail-vue/src/layout/account/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{ item.email }}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 删除
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
101 |
107 |
108 |
109 |
110 |
283 |
--------------------------------------------------------------------------------
/mail-vue/src/layout/aside/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
{{settingStore.settings.title}}
7 |
8 |
9 |
11 |
12 |
13 |
14 |
16 |
17 |
18 |
19 |
21 |
22 |
23 |
24 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
37 |
38 |
39 |
50 |
51 |
149 |
--------------------------------------------------------------------------------
/mail-vue/src/layout/header/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
29 |
30 |
31 |
59 |
60 |
132 |
--------------------------------------------------------------------------------
/mail-vue/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
49 |
50 |
122 |
--------------------------------------------------------------------------------
/mail-vue/src/layout/main/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
51 |
52 |
130 |
--------------------------------------------------------------------------------
/mail-vue/src/main.js:
--------------------------------------------------------------------------------
1 | import {createApp} from 'vue';
2 | import App from './App.vue';
3 | import router from './router';
4 | import 'element-plus/dist/index.css';
5 | import './style.css';
6 | import ElementPlus from 'element-plus';
7 | import { createPinia } from 'pinia';
8 | import zhCn from 'element-plus/es/locale/lang/zh-cn';
9 | import piniaPersistedState from 'pinia-plugin-persistedstate';
10 | const pinia = createPinia().use(piniaPersistedState)
11 | const app = createApp(App).use(pinia)
12 |
13 | app.use(ElementPlus, {
14 | locale: zhCn,
15 | });
16 |
17 | app.use(router)
18 | app.config.devtools = true;
19 |
20 | app.mount('#app');
21 |
--------------------------------------------------------------------------------
/mail-vue/src/request/account.js:
--------------------------------------------------------------------------------
1 | import http from '@/axios/index.js'
2 |
3 | export function accountList(accountId, size) {
4 | return http.get('/account/list', {params: {accountId, size}});
5 | }
6 |
7 | export function accountAdd(email,token) {
8 | return http.post('/account/add', {email,token})
9 | }
10 |
11 | export function accountDelete(accountId) {
12 | return http.delete('/account/delete', {params: {accountId}})
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/mail-vue/src/request/email.js:
--------------------------------------------------------------------------------
1 | import http from '@/axios/index.js';
2 |
3 | export function emailList(accountId, emailId, size) {
4 | return http.get('/email/list', {params: {accountId, emailId, size}})
5 | }
6 |
7 | export function emailDelete(emailIds) {
8 | return http.delete('/email/delete?emailIds=' + emailIds)
9 | }
10 |
11 | export function attList(emailId) {
12 | return http.get('/email/attList', {params: {emailId}})
13 | }
14 |
15 | export function emailLatest(emailId,accountId) {
16 | return http.get('/email/latest',{params: {emailId,accountId}})
17 | }
--------------------------------------------------------------------------------
/mail-vue/src/request/login.js:
--------------------------------------------------------------------------------
1 | import http from '@/axios/index.js';
2 |
3 | export function login(email, password) {
4 | return http.post('/login', {email: email, password: password})
5 | }
6 |
7 | export function logout() {
8 | return http.delete('/logout')
9 | }
10 |
11 | export function register(form) {
12 | return http.post('/register', form)
13 | }
--------------------------------------------------------------------------------
/mail-vue/src/request/send.js:
--------------------------------------------------------------------------------
1 | import http from "@/axios/index.js";
2 |
3 | export function sendEmail(form) {
4 | return http.post('/send/sendEmail', form)
5 | }
6 |
7 | export function sendList(accountId, sendId, size) {
8 | return http.get('/send/list',{ params: {accountId, sendId, size}})
9 | }
10 |
11 | export function sendDelete(sendIds) {
12 | return http.delete('/send/delete?sendIds=' + sendIds);
13 | }
--------------------------------------------------------------------------------
/mail-vue/src/request/setting.js:
--------------------------------------------------------------------------------
1 | import http from '@/axios/index.js';
2 |
3 | export function settingSet(setting) {
4 | return http.put('/setting/set',setting)
5 | }
6 |
7 | export function settingQuery() {
8 | return http.get('/setting/query')
9 | }
--------------------------------------------------------------------------------
/mail-vue/src/request/star.js:
--------------------------------------------------------------------------------
1 | import http from "@/axios/index.js";
2 |
3 | export function starAdd(emailId) {
4 | return http.post('/star/add', {emailId})
5 | }
6 |
7 | export function starCancel(emailId) {
8 | return http.delete('/star/cancel', {params: {emailId}})
9 | }
10 |
11 | export function starList(emailId,size) {
12 | return http.get('/star/list', {params: {emailId,size}})
13 | }
--------------------------------------------------------------------------------
/mail-vue/src/request/user.js:
--------------------------------------------------------------------------------
1 | import http from '@/axios/index.js';
2 |
3 | export function loginUserInfo() {
4 | return http.get('/user/loginUserInfo')
5 | }
6 |
7 | export function resetPassword(password) {
8 | return http.put('/user/resetPassword', {password})
9 | }
10 |
11 | export function userDelete() {
12 | return http.delete('/user/delete')
13 | }
--------------------------------------------------------------------------------
/mail-vue/src/router/index.js:
--------------------------------------------------------------------------------
1 | import {createRouter, createWebHashHistory} from 'vue-router'
2 | import {loginUserInfo} from "@/request/user.js";
3 | import {useUiStore} from "@/store/ui.js";
4 | import {useAccountStore} from "@/store/account.js";
5 | import {useUserStore} from "@/store/user.js";
6 | import {useSettingStore} from "@/store/setting.js";
7 |
8 | const routes = [
9 | {
10 | path: '/',
11 | name: 'layout',
12 | redirect: '/email',
13 | component: () => import('@/layout/index.vue'),
14 | children: [
15 | {
16 | path: '/email',
17 | name: 'email',
18 | component: () => import('@/views/email/index.vue'),
19 | meta: {
20 | title: '收件箱',
21 | name: 'email',
22 | menu: true
23 | }
24 | },
25 | {
26 | path: '/content',
27 | name: 'content',
28 | component: () => import('@/views/content/index.vue'),
29 | meta: {
30 | title: '邮件详情',
31 | name: 'content',
32 | menu: false
33 | }
34 | },
35 | {
36 | path: '/setting',
37 | name: 'setting',
38 | component: () => import('@/views/setting/index.vue'),
39 | meta: {
40 | title: '个人设置',
41 | name: 'setting',
42 | menu: true
43 | }
44 | },
45 | {
46 | path: '/sys-setting',
47 | name: 'sys-setting',
48 | component: () => import('@/views/sys-setting/index.vue'),
49 | meta: {
50 | title: '系统设置',
51 | name: 'sys-setting',
52 | menu: true
53 | }
54 | },
55 | {
56 | path: '/star',
57 | name: 'star',
58 | component: () => import('@/views/star/index.vue'),
59 | meta: {
60 | title: '星标邮件',
61 | name: 'star',
62 | menu: true
63 | }
64 | },
65 | ]
66 |
67 | },
68 | {
69 | path: '/login',
70 | name: 'login',
71 | component: () => import('@/views/login/index.vue')
72 | },
73 | {
74 | path: '/test',
75 | name: 'test',
76 | component: () => import('@/views/test/index.vue')
77 | }
78 | ]
79 |
80 |
81 | const router = createRouter({
82 | history: createWebHashHistory(import.meta.env.BASE_URL),
83 | routes
84 | })
85 |
86 | let firstLogin = true
87 |
88 | router.beforeEach(async (to, from, next) => {
89 |
90 | const uiStore = useUiStore()
91 |
92 | if (uiStore.init && to.name === 'login') {
93 | await initSetting()
94 | }
95 |
96 | const token = localStorage.getItem('token')
97 |
98 | if (!token && to.name !== 'login') {
99 | return next({
100 | name: 'login'
101 | })
102 | }
103 |
104 | if (!token && to.name === 'login') {
105 | return next()
106 | }
107 |
108 | if (uiStore.init && to.name !== 'login') {
109 | await initSettingAndUserInfo()
110 | firstLogin = false
111 | return next()
112 | }
113 |
114 | if (firstLogin) {
115 | await initUserInfo()
116 | firstLogin = false
117 | return next()
118 | }
119 |
120 | next()
121 |
122 | })
123 |
124 | router.afterEach((to) => {
125 |
126 | const uiStore = useUiStore()
127 | if (to.meta.menu) {
128 | if (['content', 'email'].includes(to.meta.name)) {
129 | uiStore.accountShow = window.innerWidth > 767;
130 | } else {
131 | uiStore.accountShow = false
132 | }
133 | }
134 |
135 | if (to.name === 'login') {
136 | firstLogin = true
137 | }
138 |
139 | if (window.innerWidth < 768) {
140 | uiStore.asideShow = false
141 | }
142 | })
143 |
144 |
145 | async function initSettingAndUserInfo() {
146 | const uiStore = useUiStore()
147 | const userStore = useUserStore()
148 | const settingStore = useSettingStore()
149 | const accountStore = useAccountStore()
150 | const [setting,user] = await Promise.all([settingStore.initSetting(),loginUserInfo()])
151 | uiStore.init = false
152 | accountStore.currentAccountId = user.accountId
153 | userStore.user = user
154 | }
155 |
156 |
157 | async function initUserInfo() {
158 | const uiStore = useUiStore()
159 | const userStore = useUserStore()
160 | try {
161 | const user = await loginUserInfo()
162 | const accountStore = useAccountStore()
163 | accountStore.currentAccountId = user.accountId
164 | userStore.user = user
165 | uiStore.loginLoading = false
166 | } catch (e) {
167 | uiStore.loginLoading = false
168 | throw e
169 | }
170 | }
171 |
172 |
173 | async function initSetting() {
174 | const settingStore = useSettingStore()
175 | const uiStore = useUiStore()
176 | try {
177 | await settingStore.initSetting();
178 | uiStore.init = false
179 | } catch (e) {
180 | throw e
181 | }
182 | }
183 |
184 | export default router
185 |
--------------------------------------------------------------------------------
/mail-vue/src/store/account.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | export const useAccountStore = defineStore('account', {
4 | state: () => ({
5 | currentAccountId: 0,
6 | })
7 | })
--------------------------------------------------------------------------------
/mail-vue/src/store/email.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | export const useEmailStore = defineStore('email', {
4 | state: () => ({
5 | deleteIds: 0,
6 | starScroll: null,
7 | emailScroll: null,
8 | readEmail: {},
9 | }),
10 | persist: {
11 | pick: ['readEmail'],
12 | },
13 | })
14 |
--------------------------------------------------------------------------------
/mail-vue/src/store/send.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | export const useSendStore = defineStore('send', {
4 | state: () => ({
5 | deleteId: 0
6 | })
7 | })
8 |
--------------------------------------------------------------------------------
/mail-vue/src/store/setting.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 | import { settingQuery} from "@/request/setting.js";
3 |
4 | export const useSettingStore = defineStore('setting', {
5 | state: () => ({
6 | domainList: [],
7 | settings: {
8 | title: '-'
9 | }
10 | }),
11 | actions: {
12 | async initSetting() {
13 | if (this.domainList.length === 0) {
14 | const data = await settingQuery()
15 | this.domainList.push(...data.domainList)
16 | delete data.domainList
17 | this.settings = data
18 | }
19 | }
20 | }
21 | })
22 |
--------------------------------------------------------------------------------
/mail-vue/src/store/ui.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | export const useUiStore = defineStore('ui', {
4 | state: () => ({
5 | asideShow: window.innerWidth > 991,
6 | loginLoading: false,
7 | accountShow: false,
8 | init: true,
9 | }),
10 | persist: {
11 | pick: ['accountShow'],
12 | },
13 | })
--------------------------------------------------------------------------------
/mail-vue/src/store/user.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | export const useUserStore = defineStore('user', {
4 | state: () => ({
5 | user: {},
6 | })
7 | })
--------------------------------------------------------------------------------
/mail-vue/src/store/writer.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | export const useUserStore = defineStore('user', {
4 | state: () => ({
5 |
6 | })
7 | })
--------------------------------------------------------------------------------
/mail-vue/src/style.css:
--------------------------------------------------------------------------------
1 |
2 | * {
3 | margin: 0;
4 | padding: 0;
5 | box-sizing: border-box;
6 | }
7 |
8 | html, body {
9 | width: 100%;
10 | height: 100%;
11 | }
12 |
13 | #app {
14 | width: 100%;
15 | height: 100%;
16 | }
17 |
18 | @font-face {
19 | font-family: 'HarmonyOS';
20 | src: url('@/assets/fonts/HarmonyOS_Sans_SC_Regular.woff2') format('woff2');
21 | font-weight: normal;
22 | font-style: normal;
23 | font-display: swap;
24 | }
25 |
26 | :deep(.el-input__inner:focus) {
27 | background-color: transparent !important;
28 | border-color: #dcdfe6 !important;
29 | }
30 |
31 | body {
32 | font-family: 'HarmonyOS', -apple-system, BlinkMacSystemFont,
33 | 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
34 | 'Noto Sans', sans-serif;
35 | line-height: 1.5;
36 | color: #333;
37 | background-color: #fff;
38 | font-size: 14px;
39 | }
40 |
41 | * {
42 | -webkit-tap-highlight-color: transparent;
43 | }
44 |
45 | ul, ol {
46 | list-style: none;
47 | }
48 |
49 | img {
50 | max-width: 100%;
51 | height: auto;
52 | display: block;
53 | }
54 |
55 | button, input, select, textarea {
56 | font-family: inherit;
57 | font-size: inherit;
58 | outline: none;
59 | border: none;
60 | background: none;
61 | }
62 |
63 | *:focus {
64 | outline: none;
65 | }
66 |
67 | :root {
68 | --el-color-primary: #1890ff;
69 | --el-color-primary-dark-2: #1064c0;
70 | --el-color-primary-light-3: #4dabff;
71 | --el-color-primary-light-5: #69c0ff;
72 | --el-color-primary-light-7: #91d5ff;
73 | --el-color-primary-light-9: #e6f7ff;
74 | --el-text-color-regular: #333;
75 | }
76 |
--------------------------------------------------------------------------------
/mail-vue/src/utils/convert-utils.js:
--------------------------------------------------------------------------------
1 | import {useSettingStore} from "@/store/setting.js";
2 |
3 | export function cvtR2Url(key) {
4 | const settingStore = useSettingStore();
5 | return 'https://' + settingStore.settings.r2Domain + '/' + key
6 | }
--------------------------------------------------------------------------------
/mail-vue/src/utils/file-utils.js:
--------------------------------------------------------------------------------
1 | export function getExtName(fileName) {
2 | const index = fileName.lastIndexOf('.')
3 | return index !== -1 ? fileName.slice(index + 1).toLowerCase() : ''
4 | }
5 |
6 | export function formatBytes(bytes) {
7 | if (bytes === 0) return '0 B';
8 | const k = 1024;
9 | const units = ['B', 'KB', 'MB', 'GB', 'TB'];
10 | const i = Math.floor(Math.log(bytes) / Math.log(k));
11 | const size = (bytes / Math.pow(k, i)).toFixed(2);
12 | return `${size} ${units[i]}`;
13 | }
--------------------------------------------------------------------------------
/mail-vue/src/utils/time-utils.js:
--------------------------------------------------------------------------------
1 | export function sleep(ms) {
2 | return new Promise(resolve => setTimeout(resolve, ms))
3 | }
4 |
--------------------------------------------------------------------------------
/mail-vue/src/utils/verify-utils.js:
--------------------------------------------------------------------------------
1 | export function isEmail(email) {
2 | const reg = /^[a-zA-Z0-9]+@([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]+$/;
3 | return reg.test(email);
4 | }
--------------------------------------------------------------------------------
/mail-vue/src/views/content/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 | {{ email.subject }}
14 |
15 |
16 |
17 |
18 |
发件人
19 |
20 | {{ email.name }}
21 | <{{ email.sendEmail }}>
22 |
23 |
24 |
收件人{{ email.receiveEmail }}
25 |
26 |
{{ formatDetailDate(email.createTime) }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
附件列表
38 |
39 |
![]()
40 |
41 |
42 |
43 |
44 | {{ att.filename }}
45 |
46 |
{{ formatBytes(att.size) }}
47 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
158 |
--------------------------------------------------------------------------------
/mail-vue/src/views/email/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
72 |
73 |
--------------------------------------------------------------------------------
/mail-vue/src/views/login/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
97 |
103 |
108 |
109 |
110 |
111 |
112 |
292 |
293 |
452 |
--------------------------------------------------------------------------------
/mail-vue/src/views/setting/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
账户与密码
5 |
6 |
邮箱
7 |
{{ userStore.user.email }}
8 |
9 |
10 |
密码
11 |
12 | 修改密码
13 |
14 |
15 |
16 |
17 |
删除账户
18 |
19 | 此操作将永久删除您的账户及其所有数据,无法恢复
20 |
21 |
22 | 删除账户
23 |
24 |
25 |
26 |
33 |
34 |
35 |
36 |
122 |
--------------------------------------------------------------------------------
/mail-vue/src/views/star/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
35 |
--------------------------------------------------------------------------------
/mail-vue/src/views/sys-setting/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
网站设置
5 |
6 |
用户注册
7 |
8 |
10 |
11 |
12 |
13 |
添加邮箱
14 |
15 |
17 |
18 |
19 |
20 |
21 |
邮件接收
22 |
23 |
25 |
26 |
27 |
28 |
注册验证
29 |
30 |
32 |
33 |
34 |
35 |
添加验证
36 |
37 |
39 |
40 |
41 |
42 |
43 | 多号模式
44 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
55 |
56 |
57 |
58 |
59 | 轮询刷新
60 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
76 |
82 |
83 |
84 |
85 |
86 |
网站标题
87 |
88 |
{{ setting.title }}
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
100 |
101 |
102 |
103 |
191 |
--------------------------------------------------------------------------------
/mail-vue/src/views/test/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ item }}
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/mail-vue/vite.config.js:
--------------------------------------------------------------------------------
1 | import {defineConfig,loadEnv} from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import path from 'path'
4 | export default defineConfig(({mode}) => {
5 | const env = loadEnv(mode, process.cwd(), 'VITE')
6 | return {
7 | server: {
8 | host: true,
9 | port: 3001,
10 | hmr: true,
11 | },
12 | base: env.VITE_STATIC_URL || '/',
13 | plugins: [vue()
14 | ],
15 | resolve: {
16 | alias: {
17 | '@': path.resolve(__dirname, 'src')
18 | }
19 | },
20 | build: {
21 | outDir: '../mail-worker/dist',
22 | emptyOutDir: true,
23 | rollupOptions: {
24 | output: {
25 | manualChunks: () => 'all-in-one'
26 | }
27 | },
28 | assetsInclude: ['**/*.json'],
29 | }
30 | }
31 | })
32 |
--------------------------------------------------------------------------------
/mail-worker/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = tab
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [*.yml]
12 | indent_style = space
13 |
--------------------------------------------------------------------------------
/mail-worker/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 140,
3 | "singleQuote": true,
4 | "semi": true,
5 | "useTabs": true
6 | }
7 |
--------------------------------------------------------------------------------
/mail-worker/dist/assets/HarmonyOS_Sans_SC_Regular-D3EGA0gC.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaziestRen/cloud-mail/d4a5b297b34c72cdebe05eb589e30dea858189ec/mail-worker/dist/assets/HarmonyOS_Sans_SC_Regular-D3EGA0gC.woff2
--------------------------------------------------------------------------------
/mail-worker/dist/assets/favicon-C5dAZutX.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mail-worker/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/mail-worker/dist/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/mail-worker/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mail-cf",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "wrangler dev --config wrangler-dev.toml",
7 | "test": "wrangler deploy --config wrangler-test.toml",
8 | "deploy": "wrangler deploy",
9 | "start": "wrangler dev"
10 | },
11 | "devDependencies": {
12 | "@cloudflare/vitest-pool-workers": "^0.7.5",
13 | "vitest": "~3.0.7",
14 | "wrangler": "^4.7.0"
15 | },
16 | "dependencies": {
17 | "@cloudflare/vite-plugin": "^1.0.5",
18 | "dayjs": "^1.11.13",
19 | "drizzle-orm": "^0.42.0",
20 | "hono": "^4.7.5",
21 | "postal-mime": "^2.4.3",
22 | "uuid": "^11.1.0"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/mail-worker/src/api/account-api.js:
--------------------------------------------------------------------------------
1 | import app from '../hono/hono';
2 | import accountService from '../service/account-service';
3 | import result from '../model/result';
4 | import userContext from '../security/user-context';
5 |
6 | app.get('/account/list', async (c) => {
7 | const list = await accountService.list(c, c.req.query(), await userContext.getUserId(c));
8 | return c.json(result.ok(list));
9 | });
10 |
11 | app.delete('/account/delete', async (c) => {
12 | await accountService.delete(c, c.req.query(), await userContext.getUserId(c));
13 | return c.json(result.ok());
14 | });
15 |
16 | app.post('/account/add', async (c) => {
17 | const account = await accountService.add(c, await c.req.json(), await userContext.getUserId(c));
18 | return c.json(result.ok(account));
19 | });
20 |
21 |
--------------------------------------------------------------------------------
/mail-worker/src/api/email-api.js:
--------------------------------------------------------------------------------
1 | import app from '../hono/hono';
2 | import emailService from '../service/email-service';
3 | import result from '../model/result';
4 | import userContext from '../security/user-context';
5 | import attService from '../service/att-service';
6 |
7 | app.get('/email/list', async (c) => {
8 | const data = await emailService.list(c, c.req.query(), await userContext.getUserId(c));
9 | return c.json(result.ok(data));
10 | });
11 |
12 | app.get('/email/latest',async (c) => {
13 | const list = await emailService.latest(c, c.req.query(), await userContext.getUserId(c));
14 | return c.json(result.ok(list));
15 | })
16 |
17 | app.delete('/email/delete', async (c) => {
18 | await emailService.delete(c, c.req.query(), await userContext.getUserId(c));
19 | return c.json(result.ok());
20 | });
21 |
22 | app.get('/email/attList', async (c) => {
23 | const attList = await attService.list(c, c.req.query(), await userContext.getUserId(c));
24 | return c.json(result.ok(attList));
25 | })
26 |
27 |
--------------------------------------------------------------------------------
/mail-worker/src/api/login-api.js:
--------------------------------------------------------------------------------
1 | import app from '../hono/hono';
2 | import loginService from '../service/login-service';
3 | import result from '../model/result';
4 | import userContext from '../security/user-context';
5 |
6 | app.post('/login', async (c) => {
7 | const token = await loginService.login(c, await c.req.json());
8 | return c.json(result.ok({ token: token }));
9 | });
10 |
11 | app.post('/register', async (c) => {
12 | const jwt = await loginService.register(c, await c.req.json());
13 | return c.json(result.ok(jwt));
14 | });
15 |
16 | app.delete('/logout', async (c) => {
17 | await loginService.logout(c, await userContext.getUserId(c));
18 | return c.json(result.ok());
19 | });
20 |
--------------------------------------------------------------------------------
/mail-worker/src/api/setting-api.js:
--------------------------------------------------------------------------------
1 | import app from '../hono/hono';
2 | import result from '../model/result';
3 | import settingService from '../service/setting-service';
4 |
5 | app.put('/setting/set', async (c) => {
6 | await settingService.set(c, await c.req.json());
7 | return c.json(result.ok());
8 | })
9 |
10 | app.get('/setting/query', async (c) => {
11 | const setting = await settingService.query(c);
12 | return c.json(result.ok(setting));
13 | })
14 |
--------------------------------------------------------------------------------
/mail-worker/src/api/star-api.js:
--------------------------------------------------------------------------------
1 | import app from '../hono/hono';
2 | import startService from '../service/star-service';
3 | import userContext from '../security/user-context';
4 | import result from '../model/result';
5 |
6 | app.post('/star/add', async (c) => {
7 | await startService.add(c, await c.req.json(), await userContext.getUserId(c));
8 | return c.json(result.ok());
9 | });
10 |
11 | app.get('/star/list', async (c) => {
12 | const data = await startService.list(c, c.req.query(), await userContext.getUserId(c));
13 | return c.json(result.ok(data));
14 | });
15 |
16 | app.delete('/star/cancel', async (c) => {
17 | await startService.cancel(c, await c.req.query(), await userContext.getUserId(c));
18 | return c.json(result.ok());
19 | });
20 |
--------------------------------------------------------------------------------
/mail-worker/src/api/test-api.js:
--------------------------------------------------------------------------------
1 | import app from '../hono/hono';
2 |
--------------------------------------------------------------------------------
/mail-worker/src/api/user-api.js:
--------------------------------------------------------------------------------
1 | import app from '../hono/hono';
2 | import userService from '../service/user-service';
3 | import result from '../model/result';
4 | import userContext from '../security/user-context';
5 |
6 | app.get('/user/loginUserInfo', async (c) => {
7 | const user = await userService.loginUserInfo(c, await userContext.getUserId(c));
8 | return c.json(result.ok(user));
9 | });
10 |
11 | app.put('/user/resetPassword', async (c) => {
12 | await userService.resetPassword(c, await c.req.json(), await userContext.getUserId(c));
13 | return c.json(result.ok());
14 | });
15 |
16 | app.delete('/user/delete', async (c) => {
17 | await userService.delete(c, await userContext.getUserId(c));
18 | return c.json(result.ok());
19 | })
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/mail-worker/src/const/constant.js:
--------------------------------------------------------------------------------
1 | const constant = {
2 |
3 | TOKEN_HEADER: 'Authorization',
4 | JWT_UID: 'user_id:',
5 | JWT_TOKEN: 'token:',
6 | TOKEN_EXPIRE: 60 * 60 * 24 * 30,
7 | ATTACHMENT_PREFIX: 'attachments/'
8 | }
9 |
10 | export default constant
11 |
--------------------------------------------------------------------------------
/mail-worker/src/const/entity-const.js:
--------------------------------------------------------------------------------
1 | export const userConst = {
2 | type: {
3 | COMMON: 1,
4 | ADMIN: 0,
5 | },
6 | }
7 |
8 | export const settingConst = {
9 | register: {
10 | OPEN: 0,
11 | CLOSE: 1,
12 | },
13 | receive: {
14 | OPEN: 0,
15 | CLOSE: 1,
16 | },
17 | send: {
18 | OPEN: 0,
19 | CLOSE: 1,
20 | },
21 | addEmail: {
22 | OPEN: 0,
23 | CLOSE: 1,
24 | },
25 | manyEmail: {
26 | OPEN: 0,
27 | CLOSE: 1,
28 | },
29 | registerVerify: {
30 | OPEN: 0,
31 | CLOSE: 1,
32 | },
33 | addEmailVerify: {
34 | OPEN: 0,
35 | CLOSE: 1,
36 | }
37 | }
38 |
39 | export const isDel = {
40 | DELETE: 1,
41 | NORMAL: 0
42 | }
43 |
--------------------------------------------------------------------------------
/mail-worker/src/const/kv-const.js:
--------------------------------------------------------------------------------
1 | const KvConst = {
2 | AUTH_INFO: 'auth-uid:',
3 | SETTING: 'setting:',
4 | }
5 |
6 | export default KvConst;
7 |
--------------------------------------------------------------------------------
/mail-worker/src/email/email.js:
--------------------------------------------------------------------------------
1 | import PostalMime from 'postal-mime';
2 | import emailService from '../service/email-service';
3 | import accountService from '../service/account-service';
4 | import settingService from '../service/setting-service';
5 | import attService from '../service/att-service';
6 | import r2Service from '../service/r2-service';
7 | import constant from '../const/constant';
8 | import { v4 as uuidv4 } from 'uuid';
9 | import fileUtils from '../utils/file-utils';
10 |
11 | export async function email(message, env, ctx) {
12 |
13 | try {
14 |
15 | if (!await settingService.isReceive({ env })) {
16 | return;
17 | }
18 |
19 | const account = await accountService.selectByEmailIncludeDel({ env: env }, message.to);
20 |
21 | const reader = message.raw.getReader();
22 | let content = '';
23 |
24 | while (true) {
25 | const { done, value } = await reader.read();
26 | if (done) break;
27 | content += new TextDecoder().decode(value);
28 | }
29 |
30 | const email = await PostalMime.parse(content);
31 |
32 | const params = {
33 | sendEmail: email.from.address,
34 | name: email.from.name,
35 | receiveEmail: message.to,
36 | subject: email.subject,
37 | content: email.html,
38 | text: email.text,
39 | userId: account.userId,
40 | accountId: account.accountId
41 | };
42 |
43 | const emailRow = await emailService.receive({ env }, params);
44 |
45 | if (!env.r2) {
46 | console.warn('r2对象存储未配置, 附件取消保存');
47 | return;
48 | }
49 |
50 | if (email.attachments.length > 0) {
51 |
52 | let attachments = email.attachments.map(item => {
53 | let attachment = { ...item };
54 | attachment.emailId = emailRow.emailId;
55 | attachment.userId = emailRow.userId;
56 | attachment.accountId = emailRow.accountId;
57 | attachment.key = constant.ATTACHMENT_PREFIX + uuidv4() + fileUtils.getExtFileName(item.filename);
58 | attachment.size = item.content.length ?? item.content.byteLength;
59 | return attachment;
60 | });
61 |
62 | await attService.addAtt({ env }, attachments);
63 |
64 | for (let attachment of attachments) {
65 | await r2Service.putObj({ env }, attachment.key, attachment.content, {
66 | contentType: attachment.mimeType,
67 | contentDisposition: `attachment; filename="${attachment.filename}"`
68 | });
69 | }
70 |
71 | }
72 | } catch (e) {
73 | console.error('邮件接收异常: ', e);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/mail-worker/src/entity/account.js:
--------------------------------------------------------------------------------
1 | import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
2 | import { sql } from 'drizzle-orm';
3 | export const account = sqliteTable('account', {
4 | accountId: integer('account_id').primaryKey({ autoIncrement: true }),
5 | email: text('email').notNull(),
6 | status: integer('status').default(0).notNull(),
7 | latestEmailTime: text('latest_email_time'),
8 | createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`),
9 | userId: integer('user_id').notNull(),
10 | isDel: integer('is_del').default(0).notNull(),
11 | });
12 | export default account
13 |
--------------------------------------------------------------------------------
/mail-worker/src/entity/att.js:
--------------------------------------------------------------------------------
1 | import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
2 | import { sql } from 'drizzle-orm';
3 |
4 | export const att = sqliteTable('attachments', {
5 | attId: integer('att_id').primaryKey({ autoIncrement: true }),
6 | userId: integer('user_id').notNull(),
7 | emailId: integer('email_id').notNull(),
8 | accountId: integer('account_id').notNull(),
9 | key: text('key').notNull(),
10 | filename: text('filename'),
11 | mimeType: text('mime_type'),
12 | size: integer('size'),
13 | disposition: text('disposition'),
14 | related: text('related'),
15 | contentId: text('content_id'),
16 | encoding: text('encoding'),
17 | createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
18 | });
19 |
20 |
--------------------------------------------------------------------------------
/mail-worker/src/entity/email.js:
--------------------------------------------------------------------------------
1 | import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
2 | import { sql } from 'drizzle-orm';
3 | export const email = sqliteTable('email', {
4 | emailId: integer('email_id').primaryKey({ autoIncrement: true }),
5 | sendEmail: text('send_email'),
6 | name: text('name'),
7 | receiveEmail: text('receive_email').notNull(),
8 | accountId: integer('account_id').notNull(),
9 | userId: integer('user_id').notNull(),
10 | subject: text('subject'),
11 | text: text('text'),
12 | content: text('content'),
13 | createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
14 | isDel: integer('is_del').default(0).notNull()
15 | });
16 | export default email
17 |
--------------------------------------------------------------------------------
/mail-worker/src/entity/orm.js:
--------------------------------------------------------------------------------
1 | import { drizzle } from 'drizzle-orm/d1';
2 |
3 | export default function orm(c) {
4 | return drizzle(c.env.db,{logger: false})
5 | }
6 |
--------------------------------------------------------------------------------
/mail-worker/src/entity/setting.js:
--------------------------------------------------------------------------------
1 | import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
2 | export const setting = sqliteTable('setting', {
3 | register: integer('register').default(0).notNull(),
4 | receive: integer('receive').default(0).notNull(),
5 | title: text('title').default('').notNull(),
6 | manyEmail: integer('many_email').default(1).notNull(),
7 | addEmail: integer('add_email').default(0).notNull(),
8 | autoRefreshTime: integer('auto_refresh_time').default(0).notNull(),
9 | registerVerify: integer('register_verify').default(1).notNull(),
10 | addEmailVerify: integer('add_email_verify').default(1).notNull()
11 | });
12 | export default setting
13 |
--------------------------------------------------------------------------------
/mail-worker/src/entity/star.js:
--------------------------------------------------------------------------------
1 | import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
2 | import { sql } from 'drizzle-orm';
3 |
4 | export const star = sqliteTable('star', {
5 | starId: integer('star_id').primaryKey({ autoIncrement: true }),
6 | userId: integer('user_id').notNull(),
7 | emailId: integer('email_id').notNull(),
8 | createTime: text('create_time')
9 | .notNull()
10 | .default(sql`CURRENT_TIMESTAMP`),
11 | });
12 |
--------------------------------------------------------------------------------
/mail-worker/src/entity/user.js:
--------------------------------------------------------------------------------
1 | import { sqliteTable, text, integer} from 'drizzle-orm/sqlite-core';
2 | import { sql } from 'drizzle-orm';
3 | const user = sqliteTable('user', {
4 | userId: integer('user_id').primaryKey({ autoIncrement: true }),
5 | email: text('email').notNull(),
6 | type: integer('type').default(1).notNull(),
7 | password: text('password').notNull(),
8 | salt: text('salt').notNull(),
9 | status: integer('status').default(0).notNull(),
10 | createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`),
11 | activeTime: text('active_time'),
12 | isDel: integer('is_del').default(0).notNull(),
13 | });
14 | export default user
15 |
--------------------------------------------------------------------------------
/mail-worker/src/error/biz-error.js:
--------------------------------------------------------------------------------
1 | class BizError extends Error {
2 | constructor(message, code) {
3 | super(message);
4 | this.code = code ? code : 501;
5 | this.name = 'BizError';
6 | }
7 | }
8 |
9 | export default BizError;
10 |
--------------------------------------------------------------------------------
/mail-worker/src/hono/hono.js:
--------------------------------------------------------------------------------
1 | import { Hono } from 'hono';
2 |
3 | const app = new Hono();
4 | import initDB from '../init/init-db';
5 | import initCache from '../init/init-cache';
6 | import verify from '../security/security';
7 | import result from '../model/result';
8 | import { cors } from 'hono/cors';
9 |
10 | app.use('*', cors());
11 |
12 | let initStatus = true;
13 | app.use('*', async (c, next) => {
14 | if (initStatus) {
15 | await initDB(c);
16 | initStatus = false;
17 | }
18 | await next();
19 | });
20 |
21 |
22 | let initCacheStatus = true;
23 | app.use('*', async (c, next) => {
24 | if (initCacheStatus) {
25 | await initCache(c);
26 | initCacheStatus = false;
27 | }
28 | await next();
29 | });
30 |
31 |
32 | app.use('*', async (c, next) => {
33 | if(await verify(c)) {
34 | await next();
35 | }
36 | });
37 |
38 |
39 | app.onError((err, c) => {
40 | if (err.name === 'BizError') {
41 | console.log(err.message);
42 | }else {
43 | console.error(err);
44 | }
45 | return c.json(result.fail(err.message, err.code));
46 | });
47 |
48 | export default app;
49 |
50 |
51 |
--------------------------------------------------------------------------------
/mail-worker/src/hono/webs.js:
--------------------------------------------------------------------------------
1 | import app from './hono';
2 | import '../api/email-api';
3 | import '../api/user-api';
4 | import '../api/login-api';
5 | import '../api/setting-api';
6 | import '../api/account-api';
7 | import '../api/star-api';
8 | import '../api/test-api';
9 | export default app;
10 |
--------------------------------------------------------------------------------
/mail-worker/src/index.js:
--------------------------------------------------------------------------------
1 | import app from './hono/webs';
2 | import { email } from './email/email';
3 |
4 | export default {
5 | fetch(req, env, ctx) {
6 | const url = new URL(req.url)
7 | console.log(url.pathname)
8 | if (url.pathname.startsWith('/api/')) {
9 | url.pathname = url.pathname.replace('/api', '')
10 | req = new Request(url.toString(), req)
11 | return app.fetch(req, env, ctx);
12 | }
13 |
14 | return env.assets.fetch(req);
15 | },
16 | email: email
17 | };
18 |
--------------------------------------------------------------------------------
/mail-worker/src/init/init-cache.js:
--------------------------------------------------------------------------------
1 | import settingService from '../service/setting-service';
2 | export default async function initCache(c) {
3 | await settingService.refresh(c)
4 | }
5 |
--------------------------------------------------------------------------------
/mail-worker/src/init/init-db.js:
--------------------------------------------------------------------------------
1 | export default async function initDB(c) {
2 | await c.env.db.prepare(`
3 | CREATE TABLE IF NOT EXISTS email (
4 | email_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
5 | send_email TEXT,
6 | name TEXT,
7 | receive_email TEXT NOT NULL,
8 | account_id INTEGER NOT NULL,
9 | user_id INTEGER NOT NULL,
10 | subject TEXT,
11 | content TEXT,
12 | text TEXT,
13 | create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
14 | is_del INTEGER DEFAULT 0 NOT NULL
15 | )
16 | `).run();
17 |
18 | await c.env.db.prepare(`
19 | CREATE TABLE IF NOT EXISTS star (
20 | star_id INTEGER PRIMARY KEY AUTOINCREMENT,
21 | user_id INTEGER NOT NULL,
22 | email_id INTEGER NOT NULL,
23 | create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
24 | )
25 | `).run();
26 |
27 | await c.env.db.prepare(`
28 | CREATE TABLE IF NOT EXISTS attachments (
29 | att_id INTEGER PRIMARY KEY AUTOINCREMENT,
30 | user_id INTEGER NOT NULL,
31 | email_id INTEGER NOT NULL,
32 | account_id INTEGER NOT NULL,
33 | key TEXT NOT NULL,
34 | filename TEXT,
35 | mime_type TEXT,
36 | size INTEGER,
37 | disposition TEXT,
38 | related TEXT,
39 | content_id TEXT,
40 | encoding TEXT,
41 | create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
42 | )
43 | `).run();
44 |
45 | await c.env.db.prepare(`
46 | CREATE TABLE IF NOT EXISTS user (
47 | user_id INTEGER PRIMARY KEY AUTOINCREMENT,
48 | email TEXT NOT NULL,
49 | type INTEGER DEFAULT 1 NOT NULL,
50 | password TEXT NOT NULL,
51 | salt TEXT NOT NULL,
52 | status INTEGER DEFAULT 0 NOT NULL,
53 | create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
54 | active_time DATETIME,
55 | is_del INTEGER DEFAULT 0 NOT NULL
56 | )
57 | `).run();
58 |
59 | await c.env.db.prepare(`
60 | CREATE TABLE IF NOT EXISTS account (
61 | account_id INTEGER PRIMARY KEY AUTOINCREMENT,
62 | email TEXT NOT NULL,
63 | status INTEGER DEFAULT 0 NOT NULL,
64 | latest_email_time DATETIME,
65 | create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
66 | user_id INTEGER NOT NULL,
67 | is_del INTEGER DEFAULT 0 NOT NULL
68 | )
69 | `).run();
70 |
71 | await c.env.db.prepare(`
72 | CREATE TABLE IF NOT EXISTS setting (
73 | register INTEGER NOT NULL,
74 | receive INTEGER NOT NULL,
75 | add_email INTEGER NOT NULL,
76 | many_email INTEGER NOT NULL,
77 | title TEXT NOT NULL,
78 | auto_refresh_time INTEGER NOT NULL,
79 | register_verify INTEGER NOT NULL,
80 | add_email_verify INTEGER NOT NULL
81 | )
82 | `).run();
83 |
84 | await c.env.db.prepare(`
85 | INSERT INTO setting (register, receive,add_email,many_email,title,auto_refresh_time,register_verify,add_email_verify)
86 | SELECT 0, 0, 0, 1, 'Cloud 邮箱', 0, 1, 1
87 | WHERE NOT EXISTS (SELECT 1 FROM setting)
88 | `).run();
89 |
90 | }
91 |
92 |
93 |
94 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/mail-worker/src/model/result.js:
--------------------------------------------------------------------------------
1 | const result = {
2 | ok(data) {
3 | return { code: 200, message: 'success', data: data ? data : null };
4 | },
5 | fail(message, code = 500) {
6 | return { code, message };
7 | }
8 | };
9 | export default result;
10 |
--------------------------------------------------------------------------------
/mail-worker/src/security/security.js:
--------------------------------------------------------------------------------
1 | import BizError from '../error/biz-error';
2 | import constant from '../const/constant';
3 | import jwtUtils from '../utils/jwt-utils';
4 | import KvConst from '../const/kv-const';
5 | import { userConst } from '../const/entity-const';
6 | import dayjs from 'dayjs';
7 |
8 | const exclude = [
9 | '/login',
10 | '/register',
11 | '/setting/query'
12 | ];
13 |
14 | const adminPerm = [
15 | '/setting/set'
16 | ]
17 |
18 | const verify = async (c) => {
19 |
20 | if (c.req.path.startsWith('/test')) {
21 | return true
22 | }
23 |
24 | if (exclude.includes(c.req.path)) {
25 | return true;
26 | }
27 |
28 | const jwt = c.req.header(constant.TOKEN_HEADER);
29 |
30 | const result = await jwtUtils.verifyToken(c, jwt)
31 |
32 | if (!result) {
33 | throw new BizError('身份认证失效,请重新登录',401)
34 | }
35 |
36 | const {userId, token} = result
37 | const authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userId,{ type: 'json' });
38 |
39 | if (!authInfo) {
40 | throw new BizError('身份认证失效,请重新登录',401)
41 | }
42 |
43 | if(!authInfo.tokens.includes(token)) {
44 | throw new BizError('身份认证失效,请重新登录',401)
45 | }
46 |
47 | if (adminPerm.includes(c.req.path) && c.env.admin !== authInfo.user.email) {
48 | throw new BizError('权限不足',403)
49 | }
50 |
51 | if (dayjs().diff(dayjs(authInfo.createTime), 'day') >= 1) {
52 | authInfo.refreshTime = new Date().toISOString()
53 | await c.env.kv.put(KvConst.AUTH_INFO + userId, JSON.stringify(authInfo), { expirationTtl: constant.TOKEN_EXPIRE });
54 | }
55 |
56 | return true
57 |
58 | };
59 |
60 | export default verify;
61 |
--------------------------------------------------------------------------------
/mail-worker/src/security/user-context.js:
--------------------------------------------------------------------------------
1 | import JwtUtils from '../utils/jwt-utils';
2 | import constant from '../const/constant';
3 |
4 | const userContext = {
5 | async getUserId(c) {
6 | const jwt = c.req.header(constant.TOKEN_HEADER);
7 | const { userId } = await JwtUtils.verifyToken(c, jwt);
8 | return Number(userId);
9 | },
10 | async getToken(c) {
11 | const jwt = c.req.header(constant.TOKEN_HEADER);
12 | const { token } = JwtUtils.verifyToken(c,jwt);
13 | return token;
14 | },
15 | };
16 | export default userContext;
17 |
--------------------------------------------------------------------------------
/mail-worker/src/service/account-service.js:
--------------------------------------------------------------------------------
1 | import BizError from '../error/biz-error';
2 | import verifyUtils from '../utils/verify-utils';
3 | import emailUtils from '../utils/email-utils';
4 | import userService from './user-service';
5 | import emailService from './email-service';
6 | import orm from '../entity/orm';
7 | import account from '../entity/account';
8 | import { and, asc, eq, gt } from 'drizzle-orm';
9 | import { isDel } from '../const/entity-const';
10 | import settingService from './setting-service';
11 | import turnstileService from './turnstile-service';
12 |
13 | const accountService = {
14 |
15 | async add(c, params, userId) {
16 |
17 |
18 | if (!await settingService.isAddEmail(c)) {
19 | throw new BizError('添加邮箱功能已关闭');
20 | }
21 |
22 | let { email, token } = params;
23 |
24 | if (!email) {
25 | throw new BizError('邮箱不能为空');
26 | }
27 |
28 | if (!verifyUtils.isEmail(email)) {
29 | throw new BizError('非法邮箱');
30 | }
31 |
32 | if (!c.env.domain.includes(emailUtils.getDomain(email))) {
33 | throw new BizError('未配置改邮箱域名');
34 | }
35 |
36 | const accountRow = await this.selectByEmailIncludeDel(c, email);
37 |
38 | if (accountRow && accountRow.isDel === isDel.DELETE) {
39 | throw new BizError('该邮箱已被注销');
40 | }
41 |
42 | if (accountRow) {
43 | throw new BizError('该邮箱已被注册');
44 | }
45 |
46 | if (await settingService.isAddEmailVerify(c)) {
47 | await turnstileService.verify(c, token);
48 | }
49 |
50 | return orm(c).insert(account).values({ email: email, userId: userId }).returning().get();
51 | },
52 |
53 | selectByEmailIncludeDel(c, email) {
54 | return orm(c).select().from(account).where(eq(account.email, email)).get();
55 | },
56 |
57 | selectByEmail(c, email) {
58 | return orm(c).select().from(account).where(
59 | and(
60 | eq(account.email, email),
61 | eq(account.isDel, isDel.NORMAL)))
62 | .get();
63 | },
64 |
65 | list(c, params, userId) {
66 |
67 | let { accountId, size } = params;
68 |
69 | accountId = Number(accountId);
70 | size = Number(size);
71 |
72 | if (size > 30) {
73 | size = 30;
74 | }
75 |
76 | if (!accountId) {
77 | accountId = 0;
78 | }
79 | return orm(c).select().from(account).where(
80 | and(
81 | eq(account.userId, userId),
82 | eq(account.isDel, isDel.NORMAL),
83 | gt(account.accountId, accountId)))
84 | .orderBy(asc(account.accountId))
85 | .limit(size)
86 | .all();
87 | },
88 |
89 | async delete(c, params, userId) {
90 |
91 | let { accountId } = params;
92 |
93 | const user = await userService.selectById(c, userId);
94 | const accountRow = await this.selectById(c, accountId);
95 |
96 | if (accountRow.email === user.email) {
97 | throw new BizError('不可以删除自己的邮箱');
98 | }
99 |
100 | if (accountRow.userId !== user.userId) {
101 | throw new BizError('该邮箱不属于当前用户');
102 | }
103 |
104 | await orm(c).update(account).set({ isDel: isDel.DELETE }).where(
105 | and(eq(account.userId, userId),
106 | eq(account.accountId, accountId)))
107 | .run();
108 | await emailService.removeByAccountId(c, accountId);
109 | },
110 |
111 | selectById(c, accountId) {
112 | return orm(c).select().from(account).where(
113 | and(eq(account.accountId, accountId),
114 | eq(account.isDel, isDel.NORMAL)))
115 | .get();
116 | },
117 |
118 | async insert(c, params) {
119 | await orm(c).insert(account).values({ ...params }).returning();
120 | },
121 |
122 | async removeByUserId(c, userId) {
123 | await orm(c).update(account).set({ isDel: isDel.DELETE }).where(eq(account.userId, userId)).run();
124 | }
125 | };
126 |
127 | export default accountService;
128 |
--------------------------------------------------------------------------------
/mail-worker/src/service/att-service.js:
--------------------------------------------------------------------------------
1 | import orm from '../entity/orm';
2 | import { att } from '../entity/att';
3 | import { and, eq } from 'drizzle-orm';
4 |
5 | const attService = {
6 | async addAtt(c, params) {
7 | await orm(c).insert(att).values(params).run();
8 | },
9 | async list(c, params, userId) {
10 | const { emailId } = params;
11 |
12 | const list = await orm(c).select().from(att).where(
13 | and(
14 | eq(att.emailId, emailId),
15 | eq(att.userId, userId)))
16 | .all();
17 |
18 | return list;
19 | }
20 | };
21 |
22 | export default attService;
23 |
--------------------------------------------------------------------------------
/mail-worker/src/service/email-service.js:
--------------------------------------------------------------------------------
1 | import orm from '../entity/orm';
2 | import email from '../entity/email';
3 | import { isDel } from '../const/entity-const';
4 | import { and, desc, eq, gt, inArray, lt, sql } from 'drizzle-orm';
5 | import { star } from '../entity/star';
6 |
7 | const emailService = {
8 |
9 | async list(c, params, userId) {
10 |
11 | let { emailId, accountId, size } = params;
12 | size = Number(size);
13 | emailId = Number(emailId);
14 |
15 | if (size > 30) {
16 | size = 30;
17 | }
18 |
19 | if (!emailId) {
20 | emailId = 9999999999;
21 | }
22 |
23 | const list = await orm(c)
24 | .select({
25 | ...email,
26 | starId: star.starId
27 | })
28 | .from(email)
29 | .leftJoin(
30 | star,
31 | and(
32 | eq(star.emailId, email.emailId),
33 | eq(star.userId, userId)
34 | )
35 | )
36 | .where(
37 | and(
38 | lt(email.emailId, emailId),
39 | eq(email.accountId, accountId),
40 | eq(email.userId, userId),
41 | eq(email.isDel, isDel.NORMAL)
42 | )
43 | )
44 | .orderBy(desc(email.emailId))
45 | .limit(size)
46 | .all();
47 |
48 | const resultList = list.map(item => ({
49 | ...item,
50 | isStar: item.starId != null ? 1 : 0
51 | }));
52 |
53 | return { list: resultList };
54 | },
55 |
56 | async delete(c, params, userId) {
57 | const { emailIds } = params;
58 | const emailIdList = emailIds.split(',').map(Number);
59 | await orm(c).update(email).set({ isDel: isDel.DELETE }).where(
60 | and(
61 | eq(email.userId, userId),
62 | inArray(email.emailId, emailIdList)))
63 | .run();
64 | },
65 |
66 | receive(c, params) {
67 | return orm(c).insert(email).values({ ...params }).returning().get();
68 | },
69 |
70 | async removeByAccountId(c, accountId) {
71 | await orm(c).update(email)
72 | .set({ isDel: isDel.DELETE })
73 | .where(eq(email.accountId, accountId))
74 | .run();
75 | },
76 |
77 | async removeByUserId(c, userId) {
78 | await orm(c).update(email).set({ isDel: isDel.DELETE }).where(eq(email.userId, userId)).run();
79 | },
80 | selectById(c, emailId) {
81 | return orm(c).select().from(email).where(
82 | and(eq(email.emailId, emailId),
83 | eq(email.isDel, isDel.NORMAL)))
84 | .get();
85 | },
86 |
87 | latest(c, params, userId) {
88 | let { emailId,accountId } = params;
89 | return orm(c).select().from(email).where(
90 | and(
91 | eq(email.userId, userId),
92 | eq(email.isDel, isDel.NORMAL),
93 | eq(email.accountId, accountId),
94 | gt(email.emailId, emailId)
95 | ))
96 | .orderBy(desc(email.emailId))
97 | .limit(20);
98 | }
99 |
100 | };
101 |
102 | export default emailService;
103 |
--------------------------------------------------------------------------------
/mail-worker/src/service/login-service.js:
--------------------------------------------------------------------------------
1 | import BizError from '../error/biz-error';
2 | import userService from './user-service';
3 | import emailUtils from '../utils/email-utils';
4 | import { isDel, userConst } from '../const/entity-const';
5 | import JwtUtils from '../utils/jwt-utils';
6 | import { v4 as uuidv4 } from 'uuid';
7 | import KvConst from '../const/kv-const';
8 | import constant from '../const/constant';
9 | import userContext from '../security/user-context';
10 | import verifyUtils from '../utils/verify-utils';
11 | import accountService from './account-service';
12 | import settingService from './setting-service';
13 | import saltHashUtils from '../utils/crypto-utils';
14 | import cryptoUtils from '../utils/crypto-utils';
15 | import turnstileService from './turnstile-service';
16 |
17 |
18 | const loginService = {
19 |
20 | async register(c, params) {
21 |
22 | const { email, password, token } = params;
23 |
24 | if (!await settingService.isRegister(c)) {
25 | throw new BizError('注册功能已关闭');
26 | }
27 |
28 | if (!verifyUtils.isEmail(email)) {
29 | throw new BizError('非法邮箱');
30 | }
31 |
32 | if (password.length < 6) {
33 | throw new BizError('密码必须大于6位');
34 | }
35 |
36 | if (!c.env.domain.includes(emailUtils.getDomain(email))) {
37 | throw new BizError('非法邮箱域名');
38 | }
39 |
40 | const userRow = await userService.selectByEmailIncludeDel(c, email);
41 |
42 | if (userRow && userRow.isDel === isDel.DELETE) {
43 | throw new BizError('该邮箱已被注销');
44 | }
45 |
46 | if (userRow) {
47 | throw new BizError('该邮箱已被注册');
48 | }
49 |
50 | if (await settingService.isRegisterVerify(c)) {
51 | await turnstileService.verify(c,token)
52 | }
53 |
54 | const { salt, hash } = await saltHashUtils.hashPassword(password);
55 |
56 | const userId = await userService.insert(c, { email, password: hash, salt, type: userConst.type.COMMON });
57 | await accountService.insert(c, { userId: userId, email });
58 | },
59 |
60 | async login(c, params) {
61 |
62 | const { email, password } = params;
63 |
64 | if (!email || !password) {
65 | throw new BizError('邮箱和密码不能为空');
66 | }
67 |
68 | const userRow = await userService.selectByEmail(c, email);
69 |
70 | if (!userRow) {
71 | throw new BizError('该邮箱不存在');
72 | }
73 |
74 | if (!await cryptoUtils.verifyPassword(password, userRow.salt, userRow.password)) {
75 | throw new BizError('密码输入错误');
76 | }
77 |
78 | const uuid = uuidv4();
79 | const jwt = await JwtUtils.generateToken(c,{ userId: userRow.userId, token: uuid });
80 |
81 | let authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userRow.userId, { type: 'json' });
82 |
83 | if (authInfo) {
84 |
85 | authInfo.tokens.push(uuid);
86 |
87 | } else {
88 |
89 | authInfo = {
90 | tokens: [],
91 | user: userRow,
92 | refreshTime: new Date().toISOString()
93 | };
94 |
95 | authInfo.tokens.push(uuid);
96 |
97 | }
98 |
99 | await c.env.kv.put(KvConst.AUTH_INFO + userRow.userId, JSON.stringify(authInfo), { expirationTtl: constant.TOKEN_EXPIRE });
100 | return jwt;
101 | },
102 |
103 | async logout(c, userId) {
104 | const token = await userContext.getToken(c);
105 | const authInfo = await c.env.kv.get(KvConst.AUTH_INFO + userId, { type: 'json' });
106 | const index = authInfo.tokens.findIndex(item => item === token);
107 | authInfo.tokens.splice(index, 1);
108 | await c.env.kv.put(KvConst.AUTH_INFO + userId, JSON.stringify(authInfo));
109 | }
110 |
111 | };
112 |
113 | export default loginService;
114 |
--------------------------------------------------------------------------------
/mail-worker/src/service/r2-service.js:
--------------------------------------------------------------------------------
1 | const r2Service = {
2 | async putObj(c, key, content, metadata) {
3 | const body = typeof content === 'string'
4 | ? new TextEncoder().encode(content)
5 | : content;
6 | await c.env.r2.put(key, body, {
7 | httpMetadata: {...metadata}
8 | });
9 | }
10 | };
11 | export default r2Service;
12 |
--------------------------------------------------------------------------------
/mail-worker/src/service/setting-service.js:
--------------------------------------------------------------------------------
1 | import KvConst from '../const/kv-const';
2 | import setting from '../entity/setting';
3 | import orm from '../entity/orm';
4 | import { settingConst } from '../const/entity-const';
5 | import BizError from "../error/biz-error";
6 |
7 | const settingService = {
8 |
9 | async refresh(c) {
10 | const settingRow = await orm(c).select().from(setting).get();
11 | await c.env.kv.put(KvConst.SETTING, JSON.stringify(settingRow));
12 | },
13 |
14 | async query(c) {
15 | const setting = await c.env.kv.get(KvConst.SETTING, { type: 'json' });
16 | let domainList = c.env.domain;
17 | domainList = domainList.map(item => '@' + item);
18 | setting.domainList = domainList;
19 | setting.siteKey = c.env.site_key;
20 | setting.r2Domain = c.env.r2_domain;
21 | return setting;
22 | },
23 |
24 | async set(c, params) {
25 | if (params.registerVerify === 0 || params.addEmailVerify === 0) {
26 | if (!c.env.site_key || !c.env.secret_key) {
27 | throw new BizError('Turnstile密钥未配置,不能开启人机验证')
28 | }
29 | }
30 | await orm(c).update(setting).set({ ...params }).returning().get();
31 | await this.refresh(c);
32 | },
33 |
34 | async isRegister(c) {
35 | const { register } = await this.query(c);
36 | return register === settingConst.register.OPEN;
37 | },
38 |
39 | async isReceive(c) {
40 | const { receive } = await this.query(c);
41 | return receive === settingConst.receive.OPEN;
42 | },
43 |
44 | async isAddEmail(c) {
45 | const { addEmail, manyEmail } = await this.query(c);
46 | return addEmail === settingConst.addEmail.OPEN && manyEmail === settingConst.manyEmail.OPEN;
47 | },
48 |
49 | async isRegisterVerify(c) {
50 | const { registerVerify } = await this.query(c);
51 | return registerVerify === settingConst.registerVerify.OPEN;
52 | },
53 |
54 | async isAddEmailVerify(c) {
55 | const { addEmailVerify } = await this.query(c);
56 | return addEmailVerify === settingConst.addEmailVerify.OPEN;
57 | }
58 | };
59 |
60 | export default settingService;
61 |
--------------------------------------------------------------------------------
/mail-worker/src/service/star-service.js:
--------------------------------------------------------------------------------
1 | import orm from '../entity/orm';
2 | import { star } from '../entity/star';
3 | import emailService from './email-service';
4 | import BizError from '../error/biz-error';
5 | import { and, desc, eq, lt, sql } from 'drizzle-orm';
6 | import email from '../entity/email';
7 | import { isDel } from '../const/entity-const';
8 |
9 | const startService = {
10 |
11 | async add(c, params, userId) {
12 | const { emailId } = params;
13 | const email = await emailService.selectById(c, emailId);
14 | if (!email) {
15 | throw new BizError('星标的邮件不存在');
16 | }
17 | if (!email.userId === userId) {
18 | throw new BizError('星标的邮件非当前用户所有');
19 | }
20 | const exist = await orm(c).select().from(star).where(
21 | and(
22 | eq(star.userId, userId),
23 | eq(star.emailId, emailId)))
24 | .get()
25 |
26 | if (exist) {
27 | return
28 | }
29 |
30 | await orm(c).insert(star).values({ userId, emailId }).run();
31 | },
32 |
33 | async cancel(c, params, userId) {
34 | const { emailId } = params;
35 | await orm(c).delete(star).where(
36 | and(
37 | eq(star.userId, userId),
38 | eq(star.emailId, emailId)))
39 | .run();
40 | },
41 |
42 | async list(c, params, userId) {
43 | let { emailId, size } = params;
44 | emailId = Number(emailId);
45 | size = Number(size);
46 |
47 | if (!emailId) {
48 | emailId = 9999999999;
49 | }
50 |
51 | const list = await orm(c).select({
52 | isStar: sql`1`.as('isStar'),
53 | starId: star.starId
54 | , ...email
55 | }).from(star)
56 | .leftJoin(email, eq(email.emailId, star.emailId))
57 | .where(
58 | and(
59 | eq(star.userId, userId),
60 | eq(email.isDel, isDel.NORMAL),
61 | lt(star.emailId, emailId)))
62 | .orderBy(desc(star.emailId))
63 | .limit(size)
64 | .all();
65 | return { list };
66 | }
67 | };
68 |
69 | export default startService;
70 |
--------------------------------------------------------------------------------
/mail-worker/src/service/turnstile-service.js:
--------------------------------------------------------------------------------
1 | import BizError from '../error/biz-error';
2 |
3 | const turnstileService = {
4 |
5 | async verify(c, token) {
6 |
7 | if (!token) {
8 | throw new BizError('验证token不能为空');
9 | }
10 | const res = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
11 | method: 'POST',
12 | headers: {
13 | 'Content-Type': 'application/x-www-form-urlencoded'
14 | },
15 | body: new URLSearchParams({
16 | secret: c.env.secret_key,
17 | response: token,
18 | remoteip: c.req.header('cf-connecting-ip')
19 | })
20 | });
21 |
22 | const result = await res.json();
23 | console.log(result)
24 | if (!result.success) {
25 | throw new BizError('人机验证失败,请重试',400)
26 | }
27 | }
28 | };
29 |
30 | export default turnstileService;
31 |
--------------------------------------------------------------------------------
/mail-worker/src/service/user-service.js:
--------------------------------------------------------------------------------
1 | import BizError from '../error/biz-error';
2 | import accountService from './account-service';
3 | import orm from '../entity/orm';
4 | import user from '../entity/user';
5 | import { eq, and } from 'drizzle-orm';
6 | import { isDel } from '../const/entity-const';
7 | import kvConst from '../const/kv-const';
8 | import cryptoUtils from '../utils/crypto-utils';
9 |
10 | const userService = {
11 |
12 | async loginUserInfo(c, userId) {
13 | let user = await userService.selectById(c, userId);
14 | let account = await accountService.selectByEmailIncludeDel(c, user.email);
15 | delete user.password;
16 | delete user.salt;
17 | user.accountId = account.accountId;
18 | user.type = c.env.admin === user.email ? 0 : 1;
19 | return user;
20 | },
21 |
22 | async resetPassword(c, params, userId) {
23 |
24 | const { password } = params;
25 |
26 | if (password < 6) {
27 | throw new BizError('密码不能小于6位');
28 | }
29 | const { salt, hash } = await cryptoUtils.hashPassword(password);
30 | await orm(c).update(user).set({ password: hash, salt: salt }).where(eq(user.userId, userId)).run();
31 | },
32 |
33 | selectByEmail(c, email) {
34 | return orm(c).select().from(user).where(
35 | and(
36 | eq(user.email, email),
37 | eq(user.isDel, isDel.NORMAL)))
38 | .get();
39 | },
40 |
41 | async insert(c, params) {
42 | const { userId } = await orm(c).insert(user).values({ ...params }).returning().get();
43 | return userId;
44 | },
45 |
46 | selectByEmailIncludeDel(c, email) {
47 | return orm(c).select().from(user).where(eq(user.email, email)).get();
48 | },
49 |
50 | selectById(c, userId) {
51 | return orm(c).select().from(user).where(
52 | and(
53 | eq(user.userId, userId),
54 | eq(user.isDel, isDel.NORMAL)))
55 | .get();
56 | },
57 |
58 | async delete(c, userId) {
59 | await orm(c).update(user).set({ isDel: isDel.DELETE }).where(eq(user.userId, userId)).run();
60 | await Promise.all([
61 | c.env.kv.delete(kvConst.AUTH_INFO + userId),
62 | accountService.removeByUserId(c, userId)
63 | ]);
64 | }
65 | };
66 |
67 | export default userService;
68 |
--------------------------------------------------------------------------------
/mail-worker/src/utils/crypto-utils.js:
--------------------------------------------------------------------------------
1 | const encoder = new TextEncoder();
2 |
3 | const saltHashUtils = {
4 |
5 | generateSalt(length = 16) {
6 | const array = new Uint8Array(length);
7 | crypto.getRandomValues(array);
8 | return btoa(String.fromCharCode(...array));
9 | },
10 |
11 |
12 | async hashPassword(password) {
13 | const salt = this.generateSalt();
14 | const hash = await this.genHashPassword(password, salt);
15 | return { salt, hash };
16 | },
17 |
18 | async genHashPassword(password, salt) {
19 | const data = encoder.encode(salt + password);
20 | const hashBuffer = await crypto.subtle.digest('SHA-256', data);
21 | const hashArray = Array.from(new Uint8Array(hashBuffer));
22 | return btoa(String.fromCharCode(...hashArray));
23 | },
24 |
25 | async verifyPassword(inputPassword, salt, storedHash) {
26 | const hash = await this.genHashPassword(inputPassword, salt);
27 | return hash === storedHash;
28 | }
29 | };
30 |
31 | export default saltHashUtils;
32 |
--------------------------------------------------------------------------------
/mail-worker/src/utils/email-utils.js:
--------------------------------------------------------------------------------
1 | const emailUtils = {
2 | getDomain(email) {
3 | if (typeof email !== 'string') return ''
4 | const parts = email.split('@')
5 | return parts.length === 2 ? parts[1] : ''
6 | }
7 | }
8 |
9 | export default emailUtils
10 |
--------------------------------------------------------------------------------
/mail-worker/src/utils/file-utils.js:
--------------------------------------------------------------------------------
1 | const fileUtils = {
2 | getExtFileName(filename) {
3 | const index = filename.lastIndexOf('.');
4 | return index !== -1 ? filename.slice(index) : '';
5 | }
6 | };
7 |
8 | export default fileUtils
9 |
10 |
--------------------------------------------------------------------------------
/mail-worker/src/utils/jwt-utils.js:
--------------------------------------------------------------------------------
1 | const encoder = new TextEncoder();
2 | const decoder = new TextDecoder();
3 |
4 | const base64url = (input) => {
5 | const str = btoa(String.fromCharCode(...new Uint8Array(input)));
6 | return str.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
7 | };
8 |
9 | const base64urlDecode = (str) => {
10 | str = str.replace(/-/g, '+').replace(/_/g, '/');
11 | while (str.length % 4) str += '=';
12 | return Uint8Array.from(atob(str), c => c.charCodeAt(0));
13 | };
14 |
15 | const jwtUtils = {
16 | async generateToken(c, payload, expiresInSeconds) {
17 | const header = {
18 | alg: 'HS256',
19 | typ: 'JWT'
20 | };
21 |
22 | const now = Math.floor(Date.now() / 1000);
23 | const exp = expiresInSeconds ? now + expiresInSeconds : undefined;
24 |
25 | const fullPayload = {
26 | ...payload,
27 | iat: now,
28 | ...(exp ? { exp } : {})
29 | };
30 |
31 | const headerStr = base64url(encoder.encode(JSON.stringify(header)));
32 | const payloadStr = base64url(encoder.encode(JSON.stringify(fullPayload)));
33 | const data = `${headerStr}.${payloadStr}`;
34 |
35 | const key = await crypto.subtle.importKey(
36 | 'raw',
37 | encoder.encode(c.env.jwt_secret),
38 | { name: 'HMAC', hash: 'SHA-256' },
39 | false,
40 | ['sign']
41 | );
42 |
43 | const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
44 | const signatureStr = base64url(signature);
45 |
46 | return `${data}.${signatureStr}`;
47 | },
48 |
49 | async verifyToken(c, token) {
50 | try {
51 | const [headerB64, payloadB64, signatureB64] = token.split('.');
52 |
53 | if (!headerB64 || !payloadB64 || !signatureB64) return null;
54 |
55 | const data = `${headerB64}.${payloadB64}`;
56 | const key = await crypto.subtle.importKey(
57 | 'raw',
58 | encoder.encode(c.env.jwt_secret),
59 | { name: 'HMAC', hash: 'SHA-256' },
60 | false,
61 | ['verify']
62 | );
63 |
64 | const valid = await crypto.subtle.verify(
65 | 'HMAC',
66 | key,
67 | base64urlDecode(signatureB64),
68 | encoder.encode(data)
69 | );
70 |
71 | if (!valid) return null;
72 |
73 | const payloadJson = decoder.decode(base64urlDecode(payloadB64));
74 | const payload = JSON.parse(payloadJson);
75 |
76 | const now = Math.floor(Date.now() / 1000);
77 | if (payload.exp && payload.exp < now) return null;
78 |
79 | return payload;
80 |
81 | } catch (err) {
82 | return null;
83 | }
84 | }
85 | };
86 |
87 | export default jwtUtils;
88 |
--------------------------------------------------------------------------------
/mail-worker/src/utils/verify-utils.js:
--------------------------------------------------------------------------------
1 | const verifyUtils = {
2 | isEmail(str) {
3 | return /^[a-zA-Z0-9]+@([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]+$/.test(str);
4 | }
5 | }
6 |
7 | export default verifyUtils
8 |
--------------------------------------------------------------------------------
/mail-worker/test/index.spec.js:
--------------------------------------------------------------------------------
1 | import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
2 | import { describe, it, expect } from 'vitest';
3 | import worker from '../src';
4 |
5 | describe('Hello World worker', () => {
6 | it('responds with Hello World! (unit style)', async () => {
7 | const request = new Request('http://example.com');
8 | // Create an empty context to pass to `worker.fetch()`.
9 | const ctx = createExecutionContext();
10 | const response = await worker.fetch(request, env, ctx);
11 | // Wait for all `Promise`s passed to `ctx.waitUntil()` to settle before running test assertions
12 | await waitOnExecutionContext(ctx);
13 | expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
14 | });
15 |
16 | it('responds with Hello World! (integration style)', async () => {
17 | const response = await SELF.fetch('http://example.com');
18 | expect(await response.text()).toMatchInlineSnapshot(`"Hello World!"`);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/mail-worker/vitest.config.js:
--------------------------------------------------------------------------------
1 | import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config';
2 |
3 | export default defineWorkersConfig({
4 | test: {
5 | poolOptions: {
6 | workers: {
7 | wrangler: { configPath: './wrangler.jsonc' },
8 | },
9 | },
10 | },
11 | });
12 |
--------------------------------------------------------------------------------
/mail-worker/wrangler-dev.toml:
--------------------------------------------------------------------------------
1 | name = "cloud-mail-dev"
2 | main = "src/index.js"
3 | compatibility_date = "2025-04-09"
4 |
5 | [observability]
6 | enabled = true
7 |
8 | [dev]
9 | ip = "0.0.0.0"
10 |
11 | [[d1_databases]]
12 | binding = "db"
13 | database_name = "email"
14 | database_id = "a1c1a63a-6ef5-4e6d-8e8c-b6d9e8feb810"
15 |
16 | [[kv_namespaces]]
17 | binding = "kv"
18 | id = "2io01d4b299e481b9de060ece9e7785c"
19 |
20 | #[[r2_buckets]]
21 | #binding = ""
22 | #bucket_name = ""
23 |
24 | [assets]
25 | binding = "assets"
26 | directory = "./dist"
27 |
28 | [vars]
29 | domain = ["example.com", "example2.com"]
30 | admin = "admin@example.com"
31 | jwt_secret = "K8x@r!3XqZ7#bLm$9pV&yTfC1W*sD2Nv%Q@4Zh^gRb6&UsP!m"
32 | r2_domain = ""
33 | site_key = ""
34 | secret_key = ""
35 |
--------------------------------------------------------------------------------
/mail-worker/wrangler-test.toml:
--------------------------------------------------------------------------------
1 | name = "cloud-mail-test"
2 | main = "src/index.js"
3 | compatibility_date = "2025-04-09"
4 |
5 | [observability]
6 | enabled = true
7 | head_sampling_rate = 1
8 |
9 | [[d1_databases]]
10 | binding = "db"
11 | database_name = "email-test"
12 | database_id = ""
13 |
14 | [[kv_namespaces]]
15 | binding = "kv"
16 | id = ""
17 |
18 | [[r2_buckets]]
19 | binding = "r2"
20 | bucket_name = "email-test"
21 |
22 | [assets]
23 | binding = "assets"
24 | directory = "./dist"
25 |
26 | [vars]
27 | domain = ["", ""]
28 | admin = ""
29 | jwt_secret = ""
30 | r2_domain = ""
31 | site_key = ""
32 | secret_key = ""
33 |
34 |
--------------------------------------------------------------------------------
/mail-worker/wrangler.toml:
--------------------------------------------------------------------------------
1 | name = "cloud-mail"
2 | main = "src/index.js"
3 | compatibility_date = "2025-04-09"
4 |
5 | [observability]
6 | enabled = true
7 |
8 | #[[d1_databases]]
9 | #binding = "db" #d1数据库绑定名默认不可修改
10 | #database_name = "" #d1数据库名字
11 | #database_id = "" #d1数据库id
12 |
13 | #[[kv_namespaces]]
14 | #binding = "kv" #kv绑定名默认不可修改
15 | #id = "" #kv数据库id
16 |
17 | #(可选)
18 | #[[r2_buckets]]
19 | #binding = "r2" #r2对象存储绑定名默认不可修改
20 | #bucket_name = "" #r2对象存储桶的名字
21 |
22 | [assets]
23 | binding = "assets" #静态资源绑定名默认不可修改
24 | directory = "./dist" #前端vue项目打包的静态资源存放位置,默认dist
25 |
26 | #[vars]
27 | #domain = [] #邮件域名可可配置多个 示例: ["example1.com","example2.com"]
28 | #admin = "" #管理员的邮箱 示例: admin@example.com
29 | #jwt_secret = "" #jwt令牌的密钥,随便填一串字符串
30 | #r2_domain = "" #r2对象存储桶的访问域名(可选)
31 | #site_key = "" #Turnstile人机验证的站点密钥(可选)
32 | #secret_key = "" #Turnstile人机验证的密钥(可选)
33 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cloud-mail",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {}
6 | }
7 |
--------------------------------------------------------------------------------