mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-11 18:10:01 +08:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0ccc3ded1 | ||
|
|
163d9451f7 | ||
|
|
60dda7e3fe | ||
|
|
384eb9b041 | ||
|
|
38816cbf0f | ||
|
|
d7d1ba6b64 | ||
|
|
14725e9e9f | ||
|
|
2c1e63b8bc | ||
|
|
f3a1d980c5 | ||
|
|
75c48beb3b | ||
|
|
26ccfdd6e0 | ||
|
|
aa8f3b4d46 | ||
|
|
a749c829d2 | ||
|
|
4b2caf1a4b | ||
|
|
80a8848ed8 | ||
|
|
dcfc1b3721 | ||
|
|
b0a0a6a1ef | ||
|
|
00c671cf14 | ||
|
|
0b78d1ff4a | ||
|
|
d152a7ce9f | ||
|
|
21fed3fb00 | ||
|
|
9448b3c754 | ||
|
|
f1827f223a | ||
|
|
2a0a34869e | ||
|
|
881e66e484 | ||
|
|
de7c3d5176 | ||
|
|
720d097ed7 | ||
|
|
53a03dc6a0 | ||
|
|
72b99e0c5e | ||
|
|
c4d9fe1fb9 | ||
|
|
af9f46ba65 | ||
|
|
8bfd76bf71 | ||
|
|
dd477fe2c8 | ||
|
|
0db611bb3e | ||
|
|
6225f6521a | ||
|
|
da2e72e523 |
17
.github/workflows/frontend_deploy.yaml
vendored
17
.github/workflows/frontend_deploy.yaml
vendored
@@ -35,14 +35,27 @@ jobs:
|
||||
echo "${{ secrets.FRONTEND_ENV }}" > .env.prod
|
||||
export project_name=${{ secrets.FRONTEND_NAME }}
|
||||
pnpm install --no-frozen-lockfile
|
||||
pnpm run deploy --project-name=$project_name
|
||||
export frontend_branch=${{ secrets.FRONTEND_BRANCH }}
|
||||
if [ -n "$frontend_branch" ]; then
|
||||
echo "Deploying branch $frontend_branch"
|
||||
pnpm run deploy:actions --project-name=$project_name
|
||||
else
|
||||
echo "Deploying branch prodcution"
|
||||
pnpm run deploy --project-name=$project_name
|
||||
fi
|
||||
echo "Deploying prodcution for ${{ github.ref_name }}"
|
||||
echo "Deployed for tag ${{ github.ref_name }}"
|
||||
|
||||
export tg_mini_app_project_name=${{ secrets.TG_FRONTEND_NAME }}
|
||||
if [ -n "$tg_mini_app_project_name" ]; then
|
||||
echo "Deploying telegram mini app $tg_mini_app_project_name"
|
||||
pnpm run deploy:telegram --project-name=$tg_mini_app_project_name
|
||||
if [ -n "$frontend_branch" ]; then
|
||||
echo "Deploying telegram mini app branch $frontend_branch"
|
||||
pnpm run deploy:actions:telegram --project-name=$tg_mini_app_project_name
|
||||
else
|
||||
echo "Deploying telegram mini app branch prodcution"
|
||||
pnpm run deploy:telegram --project-name=$tg_mini_app_project_name
|
||||
fi
|
||||
echo "Deployed telegram mini app for ${{ github.ref_name }}"
|
||||
fi
|
||||
env:
|
||||
|
||||
39
.github/workflows/frontend_pagefunction_deploy.yaml
vendored
Normal file
39
.github/workflows/frontend_pagefunction_deploy.yaml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Deploy Frontend with page function
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
- uses: pnpm/action-setup@v3
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Deploy Frontend for ${{ github.ref_name }}
|
||||
run: |
|
||||
cd frontend/
|
||||
pnpm install --no-frozen-lockfile
|
||||
pnpm build:pages
|
||||
cd ../pages/
|
||||
echo '${{ secrets.PAGE_TOML }}' > wrangler.toml
|
||||
pnpm install --no-frozen-lockfile
|
||||
pnpm run deploy
|
||||
echo "Deploying prodcution for ${{ github.ref_name }}"
|
||||
env:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
135
.gitignore
vendored
135
.gitignore
vendored
@@ -1,3 +1,138 @@
|
||||
dist/
|
||||
test/
|
||||
.vscode/
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
.wrangler
|
||||
wrangler.toml
|
||||
.dev.vars
|
||||
pnpm-lock.yaml
|
||||
|
||||
45
CHANGELOG.md
45
CHANGELOG.md
@@ -1,6 +1,49 @@
|
||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||
# CHANGE LOG
|
||||
|
||||
## v0.6.1
|
||||
|
||||
- pages github actions && 修复清理邮件天数为 0 不生效 by @tqjason (#355)
|
||||
- fix: imap proxy server 不支持 密码 by @dreamhunter2333 (#356)
|
||||
- worker 新增 `ANNOUNCEMENT` 配置, 用于配置公告信息 by @dreamhunter2333 (#357)
|
||||
- fix: telegram bot 新建地址默认选择第一个域名 by @dreamhunter2333 (#358)
|
||||
|
||||
## v0.6.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
DB changes: 增加用户角色表, 需要执行 `db/2024-07-14-patch.sql` 更新 `D1` 数据库
|
||||
|
||||
### Changes
|
||||
|
||||
worker 配置文件新增 `DEFAULT_DOMAINS`, `USER_ROLES`, `USER_DEFAULT_ROLE`, 具体查看文档 [worker配置](https://temp-mail-docs.awsl.uk/zh/guide/cli/worker.html#%E4%BF%AE%E6%94%B9-wrangler-toml-%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6)
|
||||
|
||||
- 移除 `apiV1` 相关代码和相关的数据库表
|
||||
- 更新 `admin/statistics` api, 添加用户统计信息
|
||||
- 更新地址的规则,只允许小写+数字,对于历史的地址在查询邮件时会进行 `lowercase` 处理
|
||||
- 增加用户角色功能,`admin` 可以设置用户角色(目前可配置每个角色域名和前缀)
|
||||
- admin 页面搜索优化, 回车自动搜索, 输入内容自动 trim
|
||||
|
||||
## v0.5.4
|
||||
|
||||
- 点击 logo 5 次进入 admin 页面
|
||||
- 修复 401 时无法跳转登录页面(admin 和 网站认证)
|
||||
|
||||
## v0.5.3
|
||||
|
||||
- 修复 smtp imap proxy sever 的一些 bug
|
||||
- 完善用户/admin 删除收件箱/发件箱的功能
|
||||
- admin 可以删除 发件权限记录
|
||||
- 添加中文邮件别名配置 `DOMAIN_LABELS` [文档](https://temp-mail-docs.awsl.uk/zh/guide/cli/worker.html)
|
||||
- 移除 `mail channels` 相关代码
|
||||
- github actions 增加 `FRONTEND_BRANCH` 变量用于指定部署的分支 (#324)
|
||||
|
||||
## v0.5.1
|
||||
|
||||
- 添加 `mail-parser-wasm-worker` 用于 worker 解析邮件, [文档](https://temp-mail-docs.awsl.uk/zh/guide/feature/mail_parser_wasm_worker.html)
|
||||
- 添加校验用户邮箱长度配置 `MIN_ADDRESS_LEN` 和 `MAX_ADDRESS_LEN`
|
||||
- 修复 `pages function` 未转发 `telegram` api 问题
|
||||
|
||||
## v0.5.0
|
||||
|
||||
- UI: 增加本地缓存进行地址管理
|
||||
@@ -277,7 +320,7 @@ The `mails` table will be discarded, and the `raw` text of the new `mail` will b
|
||||
```bash
|
||||
git checkout v0.2.0
|
||||
cd worker
|
||||
wrangler d1 execute dev --file=../db/2024-04-09-patch.sql
|
||||
wrangler d1 execute dev --file=../db/2024-04-09-patch.sql --remote
|
||||
pnpm run deploy
|
||||
cd ../frontend
|
||||
pnpm run deploy
|
||||
|
||||
26
README.md
26
README.md
@@ -1,17 +1,29 @@
|
||||
# 使用 cloudflare 免费服务,搭建临时邮箱
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/LICENSE">
|
||||
<img alt="MIT License" src="https://img.shields.io/github/license/dreamhunter2333/cloudflare_temp_email">
|
||||
<a href="https://hellogithub.com/repository/2ccc64bb1ba346b480625f584aa19eb1" target="_blank">
|
||||
<img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=2ccc64bb1ba346b480625f584aa19eb1&claim_uid=FxNypXK7UQ9OECT" alt="Featured|HelloGitHub"/>
|
||||
</a>
|
||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/graphs/contributors">
|
||||
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/dreamhunter2333/cloudflare_temp_email">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://temp-mail-docs.awsl.uk" target="_blank">
|
||||
<img alt="docs" src="https://img.shields.io/badge/docs-grey?style=for-the-badge&logo=vitepress">
|
||||
</a>
|
||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest" target="_blank">
|
||||
<img src="https://img.shields.io/github/v/release/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
||||
</a>
|
||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/LICENSE" target="_blank">
|
||||
<img alt="MIT License" src="https://img.shields.io/github/license/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
||||
</a>
|
||||
<a href="https://github.com/dreamhunter2333/cloudflare_temp_email/graphs/contributors" target="_blank">
|
||||
<img alt="GitHub contributors" src="https://img.shields.io/github/contributors/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
||||
</a>
|
||||
<a href="">
|
||||
<img alt="GitHub top language" src="https://img.shields.io/github/languages/top/dreamhunter2333/cloudflare_temp_email">
|
||||
<img alt="GitHub top language" src="https://img.shields.io/github/languages/top/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
||||
</a>
|
||||
<a href="https://discord.gg/dQEwTWhA6Q">
|
||||
<img alt="Join Discord Chat" src="https://img.shields.io/discord/1238705663623036939.svg?label=discord&logo=discord">
|
||||
<a href="">
|
||||
<img src="https://img.shields.io/github/last-commit/dreamhunter2333/cloudflare_temp_email?style=for-the-badge">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
|
||||
9
db/2024-07-14-patch.sql
Normal file
9
db/2024-07-14-patch.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER UNIQUE NOT NULL,
|
||||
role_text TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
|
||||
@@ -1,15 +1,3 @@
|
||||
CREATE TABLE IF NOT EXISTS mails (
|
||||
id INTEGER PRIMARY KEY,
|
||||
message_id TEXT,
|
||||
source TEXT,
|
||||
address TEXT,
|
||||
subject TEXT,
|
||||
message TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_mails_address ON mails(address);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS raw_mails (
|
||||
id INTEGER PRIMARY KEY,
|
||||
message_id TEXT,
|
||||
@@ -43,15 +31,6 @@ CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_auto_reply_mails_address ON auto_reply_mails(address);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS attachments (
|
||||
id INTEGER PRIMARY KEY,
|
||||
source TEXT,
|
||||
address TEXT,
|
||||
message_id TEXT,
|
||||
data TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS address_sender (
|
||||
id INTEGER PRIMARY KEY,
|
||||
address TEXT UNIQUE,
|
||||
@@ -99,3 +78,13 @@ CREATE TABLE IF NOT EXISTS users_address (
|
||||
CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_roles (
|
||||
id INTEGER PRIMARY KEY,
|
||||
user_id INTEGER UNIQUE NOT NULL,
|
||||
role_text TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "0.5.0",
|
||||
"version": "0.6.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -11,36 +11,38 @@
|
||||
"build:telegram": "VITE_IS_TELEGRAM=true vite build -m prod --emptyOutDir",
|
||||
"preview": "vite preview",
|
||||
"deploy:telegram": "npm run build:telegram && wrangler pages deploy ./dist --branch production",
|
||||
"deploy:actions:telegram": "npm run build:telegram && wrangler pages deploy ./dist",
|
||||
"deploy:preview": "npm run build && wrangler pages deploy ./dist --branch preview",
|
||||
"deploy": "npm run build && wrangler pages deploy ./dist --branch production"
|
||||
"deploy": "npm run build && wrangler pages deploy ./dist --branch production",
|
||||
"deploy:actions": "npm run build && wrangler pages deploy ./dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@unhead/vue": "^1.9.11",
|
||||
"@unhead/vue": "^1.9.15",
|
||||
"@vicons/material": "^0.12.0",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.7.2",
|
||||
"jszip": "^3.10.1",
|
||||
"mail-parser-wasm": "^0.1.6",
|
||||
"mail-parser-wasm": "^0.1.8",
|
||||
"naive-ui": "^2.38.2",
|
||||
"postal-mime": "^2.2.5",
|
||||
"vooks": "^0.2.12",
|
||||
"vue": "^3.4.27",
|
||||
"vue": "^3.4.31",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.3.2"
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vicons/fa": "^0.12.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-vue-components": "^0.27.0",
|
||||
"vite": "^5.2.11",
|
||||
"unplugin-vue-components": "^0.27.2",
|
||||
"vite": "^5.3.3",
|
||||
"vite-plugin-pwa": "^0.19.8",
|
||||
"vite-plugin-top-level-await": "^1.4.1",
|
||||
"vite-plugin-wasm": "^3.3.0",
|
||||
"workbox-window": "^7.1.0",
|
||||
"wrangler": "^3.57.1"
|
||||
"wrangler": "^3.63.1"
|
||||
}
|
||||
}
|
||||
|
||||
2460
frontend/pnpm-lock.yaml
generated
2460
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -54,7 +54,7 @@ onMounted(async () => {
|
||||
<n-config-provider :locale="localeConfig" :theme="theme">
|
||||
<n-global-style />
|
||||
<n-spin description="loading..." :show="loading">
|
||||
<n-message-provider>
|
||||
<n-message-provider container-style="margin-top: 20px;">
|
||||
<n-grid x-gap="12" :cols="12">
|
||||
<n-gi v-if="showSideMargin" span="1"></n-gi>
|
||||
<n-gi :span="!showSideMargin ? 12 : 10">
|
||||
|
||||
@@ -4,13 +4,14 @@ import axios from 'axios'
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||
const {
|
||||
loading, auth, jwt, settings, openSettings,
|
||||
userOpenSettings, userSettings,
|
||||
userOpenSettings, userSettings, announcement,
|
||||
showAuth, adminAuth, showAdminAuth, userJwt
|
||||
} = useGlobalState();
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: API_BASE,
|
||||
timeout: 30000
|
||||
timeout: 30000,
|
||||
validateStatus: (status) => status >= 200 && status <= 500
|
||||
});
|
||||
|
||||
const apiFetch = async (path, options = {}) => {
|
||||
@@ -27,14 +28,14 @@ const apiFetch = async (path, options = {}) => {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
if (response.status === 401 && openSettings.value.auth) {
|
||||
showAuth.value = true;
|
||||
throw new Error("Unauthorized, you access password is wrong")
|
||||
}
|
||||
if (response.status === 401 && path.startsWith("/admin")) {
|
||||
showAdminAuth.value = true;
|
||||
throw new Error("Unauthorized, your admin password is wrong")
|
||||
}
|
||||
if (response.status === 401 && openSettings.value.auth) {
|
||||
showAuth.value = true;
|
||||
throw new Error("Unauthorized, you access password is wrong")
|
||||
}
|
||||
if (response.status >= 300) {
|
||||
throw new Error(`${response.status} ${response.data}` || "error");
|
||||
}
|
||||
@@ -53,13 +54,18 @@ const apiFetch = async (path, options = {}) => {
|
||||
const getOpenSettings = async (message) => {
|
||||
try {
|
||||
const res = await api.fetch("/open_api/settings");
|
||||
const domainLabels = res["domainLabels"] || [];
|
||||
Object.assign(openSettings.value, {
|
||||
...res,
|
||||
title: res["title"] || "",
|
||||
prefix: res["prefix"] || "",
|
||||
minAddressLen: res["minAddressLen"] || 1,
|
||||
maxAddressLen: res["maxAddressLen"] || 30,
|
||||
needAuth: res["needAuth"] || false,
|
||||
domains: res["domains"].map((domain) => {
|
||||
defaultDomains: res["defaultDomains"] || [],
|
||||
domains: res["domains"].map((domain, index) => {
|
||||
return {
|
||||
label: domain,
|
||||
label: domainLabels.length > index ? domainLabels[index] : domain,
|
||||
value: domain
|
||||
}
|
||||
}),
|
||||
@@ -76,6 +82,14 @@ const getOpenSettings = async (message) => {
|
||||
if (openSettings.value.needAuth) {
|
||||
showAuth.value = true;
|
||||
}
|
||||
if (openSettings.value.announcement && openSettings.value.announcement != announcement.value) {
|
||||
announcement.value = openSettings.value.announcement;
|
||||
message.info(announcement.value, {
|
||||
showIcon: false,
|
||||
duration: 0,
|
||||
closable: true
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ const { t } = useI18n({
|
||||
saveToS3: 'Save to S3',
|
||||
multiAction: 'Multi Action',
|
||||
cancelMultiAction: 'Cancel Multi Action',
|
||||
selectAll: 'Select All',
|
||||
selectAll: 'Select All of This Page',
|
||||
unselectAll: 'Unselect All',
|
||||
},
|
||||
zh: {
|
||||
@@ -109,7 +109,7 @@ const { t } = useI18n({
|
||||
saveToS3: '保存到S3',
|
||||
multiAction: '多选',
|
||||
cancelMultiAction: '取消多选',
|
||||
selectAll: '全选',
|
||||
selectAll: '全选本页',
|
||||
unselectAll: '取消全选',
|
||||
}
|
||||
}
|
||||
@@ -387,7 +387,8 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</template>
|
||||
<template #2>
|
||||
<n-card v-if="curMail" class="mail-item" :title="curMail.subject" style="overflow: auto; max-height: 100vh;">
|
||||
<n-card :bordered="false" embedded v-if="curMail" class="mail-item" :title="curMail.subject"
|
||||
style="overflow: auto; max-height: 100vh;">
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
ID: {{ curMail.id }}
|
||||
@@ -434,7 +435,7 @@ onBeforeUnmount(() => {
|
||||
</iframe>
|
||||
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
|
||||
</n-card>
|
||||
<n-card class="mail-item" v-else>
|
||||
<n-card :bordered="false" embedded class="mail-item" v-else>
|
||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||
</n-result>
|
||||
</n-card>
|
||||
@@ -483,7 +484,7 @@ onBeforeUnmount(() => {
|
||||
<n-drawer v-model:show="curMail" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
|
||||
style="height: 80vh;">
|
||||
<n-drawer-content :title="curMail ? curMail.subject : ''" closable>
|
||||
<n-card style="overflow: auto;">
|
||||
<n-card :bordered="false" embedded style="overflow: auto;">
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
ID: {{ curMail.id }}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { watch, onMounted, ref } from "vue";
|
||||
import { watch, onMounted, ref, computed } from "vue";
|
||||
import { useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../store'
|
||||
@@ -9,6 +9,11 @@ const message = useMessage()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const props = defineProps({
|
||||
enableUserDeleteEmail: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
requried: false
|
||||
},
|
||||
showEMailFrom: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@@ -18,9 +23,14 @@ const props = defineProps({
|
||||
default: () => { },
|
||||
requried: true
|
||||
},
|
||||
deleteMail: {
|
||||
type: Function,
|
||||
default: () => { },
|
||||
requried: false
|
||||
},
|
||||
})
|
||||
|
||||
const { isDark, mailboxSplitSize } = useGlobalState()
|
||||
const { isDark, mailboxSplitSize, loading } = useGlobalState()
|
||||
const data = ref([])
|
||||
|
||||
const count = ref(0)
|
||||
@@ -30,19 +40,35 @@ const pageSize = ref(20)
|
||||
const curMail = ref(null);
|
||||
const showCode = ref(false)
|
||||
|
||||
const multiActionMode = ref(false)
|
||||
const showMultiActionDelete = ref(false)
|
||||
const multiActionDeleteProgress = ref({ percentage: 0, tip: '0/0' })
|
||||
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
success: 'Success',
|
||||
refresh: 'Refresh',
|
||||
showCode: 'Change View Original Code',
|
||||
pleaseSelectMail: "Please select a mail to view."
|
||||
pleaseSelectMail: "Please select a mail to view.",
|
||||
delete: 'Delete',
|
||||
deleteMailTip: 'Are you sure you want to delete mail?',
|
||||
multiAction: 'Multi Action',
|
||||
cancelMultiAction: 'Cancel Multi Action',
|
||||
selectAll: 'Select All of This Page',
|
||||
unselectAll: 'Unselect All',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
refresh: '刷新',
|
||||
showCode: '切换查看元数据',
|
||||
pleaseSelectMail: "请选择一封邮件查看。",
|
||||
delete: '删除',
|
||||
deleteMailTip: '确定要删除邮件吗?',
|
||||
multiAction: '多选',
|
||||
cancelMultiAction: '取消多选',
|
||||
selectAll: '全选本页',
|
||||
unselectAll: '取消全选',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -105,6 +131,71 @@ const onSpiltSizeChange = (size) => {
|
||||
mailboxSplitSize.value = size;
|
||||
}
|
||||
|
||||
const deleteMail = async () => {
|
||||
try {
|
||||
await props.deleteMail(curMail.value.id);
|
||||
message.success(t("success"));
|
||||
curMail.value = null;
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
};
|
||||
|
||||
const showMultiActionMode = computed(() => {
|
||||
return props.enableUserDeleteEmail;
|
||||
});
|
||||
|
||||
const multiActionModeClick = (enableMulti) => {
|
||||
if (enableMulti) {
|
||||
data.value.forEach((item) => {
|
||||
item.checked = false;
|
||||
});
|
||||
multiActionMode.value = true;
|
||||
} else {
|
||||
multiActionMode.value = false;
|
||||
data.value.forEach((item) => {
|
||||
item.checked = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const multiActionSelectAll = (checked) => {
|
||||
data.value.forEach((item) => {
|
||||
item.checked = checked;
|
||||
});
|
||||
}
|
||||
|
||||
const multiActionDeleteMail = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const selectedMails = data.value.filter((item) => item.checked);
|
||||
if (selectedMails.length === 0) {
|
||||
message.error(t('pleaseSelectMail'));
|
||||
return;
|
||||
}
|
||||
multiActionDeleteProgress.value = {
|
||||
percentage: 0,
|
||||
tip: `0/${selectedMails.length}`
|
||||
};
|
||||
for (const [index, mail] of selectedMails.entries()) {
|
||||
await props.deleteMail(mail.id);
|
||||
showMultiActionDelete.value = true;
|
||||
multiActionDeleteProgress.value = {
|
||||
percentage: Math.floor((index + 1) / selectedMails.length * 100),
|
||||
tip: `${index + 1}/${selectedMails.length}`
|
||||
};
|
||||
}
|
||||
message.success(t("success"));
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
showMultiActionDelete.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refresh();
|
||||
});
|
||||
@@ -112,70 +203,105 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25"
|
||||
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange">
|
||||
<template #1>
|
||||
<div class="center">
|
||||
<div v-if="!isMobile" class="left">
|
||||
<div style="margin-bottom: 10px;">
|
||||
<n-space v-if="multiActionMode">
|
||||
<n-button @click="multiActionModeClick(false)" tertiary>
|
||||
{{ t('cancelMultiAction') }}
|
||||
</n-button>
|
||||
<n-button @click="multiActionSelectAll(true)" tertiary>
|
||||
{{ t('selectAll') }}
|
||||
</n-button>
|
||||
<n-button @click="multiActionSelectAll(false)" tertiary>
|
||||
{{ t('unselectAll') }}
|
||||
</n-button>
|
||||
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="multiActionDeleteMail">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error">{{ t('delete') }}</n-button>
|
||||
</template>
|
||||
{{ t('deleteMailTip') }}
|
||||
</n-popconfirm>
|
||||
</n-space>
|
||||
<n-space v-else>
|
||||
<n-button v-if="showMultiActionMode" @click="multiActionModeClick(true)" type="primary" tertiary>
|
||||
{{ t('multiAction') }}
|
||||
</n-button>
|
||||
<div style="display: inline-block; margin-right: 10px;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||
:page-sizes="[20, 50, 100]" show-size-picker />
|
||||
</div>
|
||||
<n-button @click="refresh" size="small" type="primary" tertiary>
|
||||
<n-button @click="refresh" type="primary" tertiary>
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div style="overflow: auto; height: 80vh;">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||
:class="mailItemClass(row)">
|
||||
<n-thing :title="row.subject">
|
||||
<template #description>
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${row.created_at} UTC` }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailFrom" type="info">
|
||||
FROM: {{ row.address }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
TO: {{ row.to_mail }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</div>
|
||||
<n-split direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
|
||||
:on-update:size="onSpiltSizeChange">
|
||||
<template #1>
|
||||
<div style="overflow: auto; height: 80vh;">
|
||||
<n-list hoverable clickable>
|
||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||
:class="mailItemClass(row)">
|
||||
<template #prefix v-if="multiActionMode">
|
||||
<n-checkbox v-model:checked="row.checked" />
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
</template>
|
||||
<template #2>
|
||||
<n-card v-if="curMail" class="mail-item" :title="curMail.subject" style="overflow: auto; max-height: 100vh;">
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${curMail.created_at} UTC` }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.address }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
TO: {{ curMail.to_mail }}
|
||||
</n-tag>
|
||||
<n-button size="small" tertiary type="info" @click="showCode = !showCode">
|
||||
{{ t('showCode') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<pre v-if="showCode" style="margin-top: 10px;">{{ curMail.raw }}</pre>
|
||||
<pre v-else-if="!curMail.is_html" style="margin-top: 10px;">{{ curMail.content }}</pre>
|
||||
<div v-else v-html="curMail.content" style="margin-top: 10px;"></div>
|
||||
</n-card>
|
||||
<n-card class="mail-item" v-else>
|
||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||
</n-result>
|
||||
</n-card>
|
||||
</template>
|
||||
</n-split>
|
||||
<n-thing :title="row.subject">
|
||||
<template #description>
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${row.created_at} UTC` }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailFrom" type="info">
|
||||
FROM: {{ row.address }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
TO: {{ row.to_mail }}
|
||||
</n-tag>
|
||||
</template>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
</template>
|
||||
<template #2>
|
||||
<n-card :bordered="false" embedded v-if="curMail" class="mail-item" :title="curMail.subject"
|
||||
style="overflow: auto; max-height: 100vh;">
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
{{ `${curMail.created_at} UTC` }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.address }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
TO: {{ curMail.to_mail }}
|
||||
</n-tag>
|
||||
<n-button size="small" tertiary type="info" @click="showCode = !showCode">
|
||||
{{ t('showCode') }}
|
||||
</n-button>
|
||||
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||
</template>
|
||||
{{ t('deleteMailTip') }}
|
||||
</n-popconfirm>
|
||||
</n-space>
|
||||
<pre v-if="showCode" style="margin-top: 10px;">{{ curMail.raw }}</pre>
|
||||
<pre v-else-if="!curMail.is_html" style="margin-top: 10px;">{{ curMail.content }}</pre>
|
||||
<div v-else v-html="curMail.content" style="margin-top: 10px;"></div>
|
||||
</n-card>
|
||||
<n-card :bordered="false" embedded class="mail-item" v-else>
|
||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||
</n-result>
|
||||
</n-card>
|
||||
</template>
|
||||
</n-split>
|
||||
</div>
|
||||
<div class="left" v-else>
|
||||
<div class="center">
|
||||
<div style="display: inline-block; margin-right: 10px;">
|
||||
@@ -210,7 +336,7 @@ onMounted(async () => {
|
||||
<n-drawer v-model:show="curMail" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
|
||||
style="height: 80vh;">
|
||||
<n-drawer-content :title="curMail ? curMail.subject : ''" closable>
|
||||
<n-card style="overflow: auto;">
|
||||
<n-card :bordered="false" embedded style="overflow: auto;">
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
ID: {{ curMail.id }}
|
||||
@@ -224,6 +350,15 @@ onMounted(async () => {
|
||||
<n-tag type="info">
|
||||
TO: {{ curMail.to_mail }}
|
||||
</n-tag>
|
||||
<n-button size="small" tertiary type="info" @click="showCode = !showCode">
|
||||
{{ t('showCode') }}
|
||||
</n-button>
|
||||
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||
</template>
|
||||
{{ t('deleteMailTip') }}
|
||||
</n-popconfirm>
|
||||
</n-space>
|
||||
<pre v-if="showCode" style="margin-top: 10px;">{{ curMail.raw }}</pre>
|
||||
<pre v-else-if="!curMail.is_html" style="margin-top: 10px;">{{ curMail.content }}</pre>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, watch, defineModel, onMounted } from "vue";
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../store'
|
||||
const { openSettings, isDark } = useGlobalState()
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ref } from "vue";
|
||||
import { createGlobalState, useStorage, useDark, useToggle } from '@vueuse/core'
|
||||
import { createGlobalState, useStorage, useDark, useToggle, useLocalStorage } from '@vueuse/core'
|
||||
|
||||
export const useGlobalState = createGlobalState(
|
||||
() => {
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
const loading = ref(false);
|
||||
const announcement = useLocalStorage('announcement', '');
|
||||
const openSettings = ref({
|
||||
title: '',
|
||||
announcement: '',
|
||||
prefix: '',
|
||||
needAuth: false,
|
||||
adminContact: '',
|
||||
@@ -15,6 +17,9 @@ export const useGlobalState = createGlobalState(
|
||||
enableUserDeleteEmail: false,
|
||||
enableAutoReply: false,
|
||||
enableIndexAbout: false,
|
||||
/** @type {string[]} */
|
||||
defaultDomains: [],
|
||||
/** @type {Array<{label: string, value: string}>} */
|
||||
domains: [],
|
||||
copyright: 'Dream Hunter',
|
||||
cfTurnstileSiteKey: '',
|
||||
@@ -69,6 +74,8 @@ export const useGlobalState = createGlobalState(
|
||||
user_email: '',
|
||||
/** @type {number} */
|
||||
user_id: 0,
|
||||
/** @type {null | {domains: string[] | undefined | null, role: string, prefix: string | undefined | null}} */
|
||||
user_role: null,
|
||||
});
|
||||
const telegramApp = ref(window.Telegram?.WebApp || {});
|
||||
const isTelegram = ref(!!window.Telegram?.WebApp?.initData);
|
||||
@@ -78,6 +85,7 @@ export const useGlobalState = createGlobalState(
|
||||
loading,
|
||||
settings,
|
||||
sendMailModel,
|
||||
announcement,
|
||||
openSettings,
|
||||
showAuth,
|
||||
showAddressCredential,
|
||||
|
||||
@@ -203,6 +203,24 @@ useHead({
|
||||
]
|
||||
});
|
||||
|
||||
const logoClickCount = ref(0);
|
||||
const logoClick = async () => {
|
||||
if (route.path.includes("admin")) {
|
||||
logoClickCount.value = 0;
|
||||
return;
|
||||
}
|
||||
if (logoClickCount.value >= 5) {
|
||||
logoClickCount.value = 0;
|
||||
message.info("Change to admin Page");
|
||||
await router.push(getRouterPathWithLang('/admin', locale.value));
|
||||
} else {
|
||||
logoClickCount.value++;
|
||||
}
|
||||
if (logoClickCount.value > 0) {
|
||||
message.info(`Click ${5 - logoClickCount.value + 1} times to enter the admin page`);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await api.getOpenSettings(message);
|
||||
});
|
||||
@@ -215,7 +233,9 @@ onMounted(async () => {
|
||||
<h3>{{ openSettings.title || t('title') }}</h3>
|
||||
</template>
|
||||
<template #avatar>
|
||||
<n-avatar style="margin-left: 10px;" src="/logo.png" />
|
||||
<div @click="logoClick">
|
||||
<n-avatar style="margin-left: 10px;" src="/logo.png" />
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<n-space>
|
||||
|
||||
@@ -51,6 +51,10 @@ const deleteMail = async (curMailId) => {
|
||||
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
const deleteSenboxMail = async (curMailId) => {
|
||||
await api.fetch(`/api/sendbox/${curMailId}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
const fetchSenboxData = async (limit, offset) => {
|
||||
return await api.fetch(`/api/sendbox?limit=${limit}&offset=${offset}`);
|
||||
};
|
||||
@@ -86,7 +90,8 @@ const saveToS3 = async (mail_id, filename, blob) => {
|
||||
:deleteMail="deleteMail" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
||||
<SendBox :fetchMailData="fetchSenboxData" />
|
||||
<SendBox :fetchMailData="fetchSenboxData" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||
:deleteMail="deleteSenboxMail" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendmail" :tab="t('sendmail')">
|
||||
<SendMail />
|
||||
|
||||
@@ -94,6 +94,7 @@ const deleteEmail = async () => {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
addressQuery.value = addressQuery.value.trim()
|
||||
const { results, count: addressCount } = await api.fetch(
|
||||
`/admin/address`
|
||||
+ `?limit=${pageSize.value}`
|
||||
@@ -268,7 +269,7 @@ onMounted(async () => {
|
||||
<span>
|
||||
<p>{{ t("addressCredentialTip") }}</p>
|
||||
</span>
|
||||
<n-card>
|
||||
<n-card :bordered="false" embedded>
|
||||
<b>{{ curEmailCredential }}</b>
|
||||
</n-card>
|
||||
<template #action>
|
||||
@@ -283,7 +284,8 @@ onMounted(async () => {
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')" />
|
||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')"
|
||||
@keydown.enter="fetchData" />
|
||||
<n-button @click="fetchData" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
@@ -296,7 +298,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card style="max-width: 600px;">
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<n-form-item-row :label="t('address_block_list')">
|
||||
<n-select v-model:value="addressBlockList" filterable multiple tag
|
||||
:placeholder="t('address_block_list_placeholder')" />
|
||||
|
||||
@@ -71,11 +71,11 @@ onMounted(async () => {
|
||||
<div class="center">
|
||||
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('addressCredential')">
|
||||
<p>{{ t('addressCredential') }}</p>
|
||||
<n-card>
|
||||
<n-card :bordered="false" embedded>
|
||||
<b>{{ result }}</b>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
<n-card style="max-width: 600px;">
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
|
||||
<n-checkbox v-model:checked="enablePrefix" />
|
||||
</n-form-item-row>
|
||||
|
||||
@@ -29,12 +29,9 @@ const { t } = useI18n({
|
||||
const mailBoxKey = ref("")
|
||||
const mailKeyword = ref("")
|
||||
|
||||
watch([adminMailTabAddress, mailKeyword], () => {
|
||||
const queryMail = () => {
|
||||
adminMailTabAddress.value = adminMailTabAddress.value.trim();
|
||||
mailKeyword.value = mailKeyword.value.trim();
|
||||
});
|
||||
|
||||
const queryMail = () => {
|
||||
mailBoxKey.value = Date.now();
|
||||
}
|
||||
|
||||
@@ -48,6 +45,10 @@ const fetchMailData = async (limit, offset) => {
|
||||
);
|
||||
}
|
||||
|
||||
const deleteMail = async (curMailId) => {
|
||||
await api.fetch(`/admin/mails/${curMailId}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
@@ -59,13 +60,15 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div style="margin-top: 10px;">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')" />
|
||||
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" />
|
||||
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')"
|
||||
@keydown.enter="queryMail" />
|
||||
<n-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" @keydown.enter="queryMail" />
|
||||
<n-button @click="queryMail" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<div style="margin-top: 10px;"></div>
|
||||
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="false" :fetchMailData="fetchMailData" />
|
||||
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="true" :fetchMailData="fetchMailData"
|
||||
:deleteMail="deleteMail" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -15,6 +15,10 @@ const fetchMailUnknowData = async (limit, offset) => {
|
||||
);
|
||||
}
|
||||
|
||||
const deleteMail = async (curMailId) => {
|
||||
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
@@ -25,6 +29,6 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div v-if="adminAuth" style="margin-top: 10px;">
|
||||
<MailBox :enableUserDeleteEmail="false" :fetchMailData="fetchMailUnknowData" />
|
||||
<MailBox :enableUserDeleteEmail="true" :fetchMailData="fetchMailUnknowData" :deleteMail="deleteMail" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -22,10 +22,10 @@ const cleanupModel = ref({
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
tip: 'Please input the cleanup days',
|
||||
mailBoxLabel: 'Clean up days for mailbox',
|
||||
mailUnknowLabel: "Clean up days for unknow receiver",
|
||||
sendBoxLabel: "Clean up days for sendbox",
|
||||
tip: 'Please input the days',
|
||||
mailBoxLabel: 'Cleanup the inbox before n days',
|
||||
mailUnknowLabel: "Cleanup the unknow mail before n days",
|
||||
sendBoxLabel: "Cleanup the sendbox before n days",
|
||||
cleanupNow: "Cleanup now",
|
||||
autoCleanup: "Auto cleanup",
|
||||
cleanupSuccess: "Cleanup success",
|
||||
@@ -33,10 +33,10 @@ const { t } = useI18n({
|
||||
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document",
|
||||
},
|
||||
zh: {
|
||||
tip: '请输入清理天数',
|
||||
mailBoxLabel: '收件箱清理天数',
|
||||
mailUnknowLabel: "无收件人邮件清理天数",
|
||||
sendBoxLabel: "发件箱清理天数",
|
||||
tip: '请输入天数',
|
||||
mailBoxLabel: '清理 n 天前的收件箱',
|
||||
mailUnknowLabel: "清理 n 天前的无收件人邮件",
|
||||
sendBoxLabel: "清理 n 天前的发件箱",
|
||||
autoCleanup: "自动清理",
|
||||
cleanupSuccess: "清理成功",
|
||||
cleanupNow: "立即清理",
|
||||
@@ -91,8 +91,8 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card>
|
||||
<n-alert :show-icon="false">
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-alert :show-icon="false" :bordered="false">
|
||||
<span>{{ t('cronTip') }}</span>
|
||||
</n-alert>
|
||||
<n-form :model="cleanupModel">
|
||||
|
||||
@@ -21,22 +21,28 @@ const { t } = useI18n({
|
||||
});
|
||||
|
||||
const fetchData = async (limit, offset) => {
|
||||
adminSendBoxTabAddress.value = adminSendBoxTabAddress.value.trim();
|
||||
return await api.fetch(
|
||||
`/admin/sendbox?limit=${limit}&offset=${offset}`
|
||||
+ (adminSendBoxTabAddress.value ? `&address=${adminSendBoxTabAddress.value}` : '')
|
||||
);
|
||||
}
|
||||
|
||||
const deleteSenboxMail = async (curMailId) => {
|
||||
await api.fetch(`/admin/sendbox/${curMailId}`, { method: 'DELETE' });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="adminSendBoxTabAddress" :placeholder="t('queryTip')" />
|
||||
<n-input v-model:value="adminSendBoxTabAddress" :placeholder="t('queryTip')" @keydown.enter="fetchData" />
|
||||
<n-button @click="fetchData" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<SendBox style="margin-top: 10px;" :fetchMailData="fetchData" :showEMailFrom="true" />
|
||||
<SendBox style="margin-top: 10px;" :enableUserDeleteEmail="true" :deleteMail="deleteSenboxMail"
|
||||
:fetchMailData="fetchData" :showEMailFrom="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ const { t } = useI18n({
|
||||
enable: 'Enable',
|
||||
disable: 'Disable',
|
||||
modify: 'Modify',
|
||||
delete: 'Delete',
|
||||
deleteTip: 'Are you sure to delete this?',
|
||||
created_at: 'Created At',
|
||||
action: 'Action',
|
||||
itemCount: 'itemCount',
|
||||
@@ -32,6 +34,8 @@ const { t } = useI18n({
|
||||
enable: '启用',
|
||||
disable: '禁用',
|
||||
modify: '修改',
|
||||
delete: '删除',
|
||||
deleteTip: '确定删除吗?',
|
||||
created_at: '创建时间',
|
||||
action: '操作',
|
||||
itemCount: '总数',
|
||||
@@ -75,6 +79,7 @@ const updateData = async () => {
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
addressQuery.value = addressQuery.value.trim();
|
||||
const { results, count: addressCount } = await api.fetch(
|
||||
`/admin/address_sender`
|
||||
+ `?limit=${pageSize.value}`
|
||||
@@ -134,7 +139,25 @@ const columns = [
|
||||
}
|
||||
},
|
||||
{ default: () => t('modify') }
|
||||
)
|
||||
),
|
||||
h(NPopconfirm,
|
||||
{
|
||||
onPositiveClick: async () => {
|
||||
await api.fetch(`/admin/address_sender/${row.id}`, { method: 'DELETE' });
|
||||
await fetchData();
|
||||
}
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
type: "error",
|
||||
},
|
||||
{ default: () => t('delete') }
|
||||
),
|
||||
default: () => t('deleteTip')
|
||||
}
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -170,7 +193,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="addressQuery" />
|
||||
<n-input v-model:value="addressQuery" @keydown.enter="fetchData" />
|
||||
<n-button @click="fetchData" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
@@ -183,7 +206,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -13,14 +13,18 @@ const message = useMessage()
|
||||
const { t } = useI18n({
|
||||
messages: {
|
||||
en: {
|
||||
userCount: 'Account Count',
|
||||
activeUser: '7 days Active Mail Account',
|
||||
userCount: 'User Count',
|
||||
addressCount: 'Address Count',
|
||||
activeAddressCount7days: '7 days Active Address Count',
|
||||
activeAddressCount30days: '30 days Active Address Count',
|
||||
mailCount: 'Mail Count',
|
||||
sendMailCount: 'Send Mail Count'
|
||||
},
|
||||
zh: {
|
||||
userCount: '地址总数',
|
||||
activeUser: '周活跃邮箱地址',
|
||||
userCount: '用户总数',
|
||||
addressCount: '邮箱地址总数',
|
||||
activeAddressCount7days: '7天活跃邮箱地址总数',
|
||||
activeAddressCount30days: '30天活跃邮箱地址总数',
|
||||
mailCount: '邮件总数',
|
||||
sendMailCount: '发送邮件总数'
|
||||
}
|
||||
@@ -28,21 +32,27 @@ const { t } = useI18n({
|
||||
});
|
||||
|
||||
const statistics = ref({
|
||||
addressCount: 0,
|
||||
userCount: 0,
|
||||
mailCount: 0,
|
||||
activeUserCount7days: 0,
|
||||
activeAddressCount7days: 0,
|
||||
activeAddressCount30days: 0,
|
||||
sendMailCount: 0,
|
||||
})
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
try {
|
||||
const {
|
||||
userCount, activeUserCount7days, mailCount, sendMailCount
|
||||
userCount, mailCount, sendMailCount,
|
||||
addressCount, activeAddressCount7days,
|
||||
activeAddressCount30days,
|
||||
} = await api.fetch(`/admin/statistics`);
|
||||
statistics.value.mailCount = mailCount || 0;
|
||||
statistics.value.userCount = userCount || 0;
|
||||
statistics.value.activeUserCount7days = activeUserCount7days || 0;
|
||||
statistics.value.sendMailCount = sendMailCount || 0;
|
||||
statistics.value.userCount = userCount || 0;
|
||||
statistics.value.addressCount = addressCount || 0;
|
||||
statistics.value.activeAddressCount7days = activeAddressCount7days || 0;
|
||||
statistics.value.activeAddressCount30days = activeAddressCount30days || 0;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
@@ -58,36 +68,63 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-card>
|
||||
<n-row>
|
||||
<n-col :span="6">
|
||||
<n-statistic :label="t('userCount')" :value="statistics.userCount">
|
||||
<template #prefix>
|
||||
<n-icon :component="User" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
<n-col :span="6">
|
||||
<n-statistic :label="t('activeUser')" :value="statistics.activeUserCount7days">
|
||||
<template #prefix>
|
||||
<n-icon :component="UserCheck" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
<n-col :span="6">
|
||||
<n-statistic :label="t('mailCount')" :value="statistics.mailCount">
|
||||
<template #prefix>
|
||||
<n-icon :component="MailBulk" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
<n-col :span="6">
|
||||
<n-statistic :label="t('sendMailCount')" :value="statistics.sendMailCount">
|
||||
<template #prefix>
|
||||
<n-icon :component="SendOutlined" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-card>
|
||||
<div>
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-row>
|
||||
|
||||
<n-col :span="8">
|
||||
<n-statistic :label="t('addressCount')" :value="statistics.addressCount">
|
||||
<template #prefix>
|
||||
<n-icon :component="User" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
<n-col :span="8">
|
||||
<n-statistic :label="t('activeAddressCount7days')" :value="statistics.activeAddressCount7days">
|
||||
<template #prefix>
|
||||
<n-icon :component="UserCheck" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
<n-col :span="8">
|
||||
<n-statistic :label="t('activeAddressCount30days')" :value="statistics.activeAddressCount30days">
|
||||
<template #prefix>
|
||||
<n-icon :component="UserCheck" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-card>
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-row>
|
||||
<n-col :span="8">
|
||||
<n-statistic :label="t('userCount')" :value="statistics.userCount">
|
||||
<template #prefix>
|
||||
<n-icon :component="User" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
<n-col :span="8">
|
||||
<n-statistic :label="t('mailCount')" :value="statistics.mailCount">
|
||||
<template #prefix>
|
||||
<n-icon :component="MailBulk" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
<n-col :span="8">
|
||||
<n-statistic :label="t('sendMailCount')" :value="statistics.sendMailCount">
|
||||
<template #prefix>
|
||||
<n-icon :component="SendOutlined" />
|
||||
</template>
|
||||
</n-statistic>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -112,8 +112,8 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card style="max-width: 800px; overflow: auto;">
|
||||
<n-card>
|
||||
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-form-item-row :label="t('enableTelegramAllowList')">
|
||||
<n-input-group>
|
||||
<n-checkbox v-model:checked="settings.enableAllowList" style="width: 20%;">
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { ref, h, onMounted, watch, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { NMenu, NButton, NBadge } from 'naive-ui';
|
||||
import { NMenu, NButton, NBadge, NTag } from 'naive-ui';
|
||||
import { MenuFilled } from '@vicons/material'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { hashPassword } from '../../utils';
|
||||
|
||||
const { loading } = useGlobalState()
|
||||
const { loading, openSettings } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
@@ -16,6 +16,7 @@ const { t } = useI18n({
|
||||
en: {
|
||||
success: 'Success',
|
||||
user_email: 'User Email',
|
||||
role: 'Role',
|
||||
address_count: 'Address Count',
|
||||
created_at: 'Created At',
|
||||
actions: 'Actions',
|
||||
@@ -29,10 +30,15 @@ const { t } = useI18n({
|
||||
createUser: 'Create User',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
changeRole: 'Change Role',
|
||||
prefix: 'Prefix',
|
||||
domains: 'Domains',
|
||||
roleDonotExist: 'Current Role does not exist',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
user_email: '用户邮箱',
|
||||
role: '角色',
|
||||
address_count: '地址数量',
|
||||
created_at: '创建时间',
|
||||
actions: '操作',
|
||||
@@ -46,6 +52,10 @@ const { t } = useI18n({
|
||||
createUser: '创建用户',
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
changeRole: '更改角色',
|
||||
prefix: '前缀',
|
||||
domains: '域名',
|
||||
roleDonotExist: '当前角色不存在',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -64,9 +74,31 @@ const user = ref({
|
||||
email: "",
|
||||
password: ""
|
||||
})
|
||||
const showChangeRole = ref(false)
|
||||
const userRoles = ref([])
|
||||
const curUserRole = ref('')
|
||||
const userRolesOptions = computed(() => {
|
||||
return userRoles.value.map(role => {
|
||||
return {
|
||||
label: role.role,
|
||||
value: role.role
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const fetchUserRoles = async () => {
|
||||
try {
|
||||
const results = await api.fetch(`/admin/user_roles`);
|
||||
userRoles.value = results;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
userQuery.value = userQuery.value.trim()
|
||||
const { results, count: userCount } = await api.fetch(
|
||||
`/admin/users`
|
||||
+ `?limit=${pageSize.value}`
|
||||
@@ -138,6 +170,24 @@ const deleteUser = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const changeRole = async () => {
|
||||
try {
|
||||
await api.fetch(`/admin/user_roles`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
user_id: curUserId.value,
|
||||
role_text: curUserRole.value
|
||||
})
|
||||
});
|
||||
message.success(t('success'));
|
||||
showChangeRole.value = false;
|
||||
await fetchData();
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "ID",
|
||||
@@ -147,6 +197,19 @@ const columns = [
|
||||
title: t('user_email'),
|
||||
key: "user_email"
|
||||
},
|
||||
{
|
||||
title: t('role'),
|
||||
key: "role_text",
|
||||
render(row) {
|
||||
if (!row.role_text) return null;
|
||||
return h(NTag, {
|
||||
bordered: false,
|
||||
type: "info"
|
||||
}, {
|
||||
default: () => row.role_text
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('address_count'),
|
||||
key: "address_count",
|
||||
@@ -176,6 +239,19 @@ const columns = [
|
||||
icon: () => h(MenuFilled),
|
||||
key: "action",
|
||||
children: [
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
text: true,
|
||||
onClick: () => {
|
||||
curUserId.value = row.id;
|
||||
curUserRole.value = row.role_text;
|
||||
showChangeRole.value = true;
|
||||
}
|
||||
},
|
||||
{ default: () => t('changeRole') }
|
||||
),
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
@@ -212,12 +288,29 @@ const columns = [
|
||||
}
|
||||
]
|
||||
|
||||
const getRolePrefix = (role) => {
|
||||
const res = userRoles.value.find(r => r.role === role)?.prefix;
|
||||
if (res === undefined || res === null) return openSettings.value.prefix;
|
||||
return res;
|
||||
}
|
||||
|
||||
const getRoleDomains = (role) => {
|
||||
const res = userRoles.value.find(r => r.role === role)?.domains;
|
||||
if (res === undefined || res === null || res.length == 0) return openSettings.value.defaultDomains;
|
||||
return res;
|
||||
}
|
||||
|
||||
const roleDonotExist = computed(() => {
|
||||
return curUserRole.value && !userRoles.value.some(r => r.role === curUserRole.value);
|
||||
})
|
||||
|
||||
watch([page, pageSize], async () => {
|
||||
await fetchData()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
await fetchUserRoles();
|
||||
await fetchData();
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -256,8 +349,21 @@ onMounted(async () => {
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showChangeRole" preset="dialog" :title="t('changeRole')">
|
||||
<n-alert type="error" :bordered="false" v-if="roleDonotExist">
|
||||
<span>{{ t('roleDonotExist') }}</span>
|
||||
</n-alert>
|
||||
<p>{{ t('prefix') + ": " + getRolePrefix(curUserRole) }}</p>
|
||||
<p>{{ t('domains') + ": " + JSON.stringify(getRoleDomains(curUserRole)) }}</p>
|
||||
<n-select clearable v-model:value="curUserRole" :options="userRolesOptions" />
|
||||
<template #action>
|
||||
<n-button :loading="loading" @click="changeRole" size="small" tertiary type="primary">
|
||||
{{ t('changeRole') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="userQuery" />
|
||||
<n-input v-model:value="userQuery" @keydown.enter="fetchData" />
|
||||
<n-button @click="fetchData" type="primary" tertiary>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
@@ -276,7 +382,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card style="max-width: 600px;">
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<n-form :model="userSettings">
|
||||
<n-form-item-row :label="t('enableUserRegister')">
|
||||
<n-checkbox v-model:checked="userSettings.enable" />
|
||||
|
||||
@@ -62,7 +62,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card style="max-width: 800px; overflow: auto;">
|
||||
<n-card :bordered="false" embedded style="max-width: 800px; overflow: auto;">
|
||||
<n-form-item-row :label="t('webhookAllowList')">
|
||||
<n-select v-model:value="webhookSettings.allowList" filterable multiple tag
|
||||
:placeholder="t('webhookAllowList')" />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { GithubAlt, Discord, Telegram } from '@vicons/fa'
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card>
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-button tag="a" target="_blank" href="https://github.com/dreamhunter2333/cloudflare_temp_email">
|
||||
<template #icon>
|
||||
<n-icon :component="GithubAlt" />
|
||||
|
||||
@@ -16,7 +16,7 @@ const { t } = useI18n({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-alert v-if="openSettings.adminContact" :show-icon="false">
|
||||
<n-alert v-if="openSettings.adminContact" :show-icon="false" :bordered="false">
|
||||
<span>{{ t('adminContact', { msg: openSettings.adminContact }) }}</span>
|
||||
</n-alert>
|
||||
</template>
|
||||
|
||||
@@ -40,7 +40,7 @@ const { t } = useI18n({
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card>
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-form-item-row v-if="!isMobile" :label="t('mailboxSplitSize')">
|
||||
<n-slider v-model:value="mailboxSplitSize" :min="0.25" :max="0.75" :step="0.01" :marks="{
|
||||
0.25: '0.25',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { NewLabelOutlined, EmailOutlined } from '@vicons/material'
|
||||
@@ -72,7 +72,7 @@ const { locale, t } = useI18n({
|
||||
login: 'Login',
|
||||
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
|
||||
getNewEmail: 'Create New Email',
|
||||
getNewEmailTip1: 'Please input the email you want to use. only allow ., a-z, A-Z and 0-9',
|
||||
getNewEmailTip1: 'Please input the email you want to use. only allow a-z and 0-9',
|
||||
getNewEmailTip2: 'Levaing it blank will generate a random email address.',
|
||||
getNewEmailTip3: 'You can choose a domain from the dropdown list.',
|
||||
credential: 'Email Address Credential',
|
||||
@@ -87,7 +87,7 @@ const { locale, t } = useI18n({
|
||||
login: '登录',
|
||||
pleaseGetNewEmail: '请"登录"或点击 "注册新邮箱" 按钮来获取一个新的邮箱地址',
|
||||
getNewEmail: '创建新邮箱',
|
||||
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 ., a-z, A-Z, 0-9',
|
||||
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 a-z, 0-9',
|
||||
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
||||
getNewEmailTip3: '你可以从下拉列表中选择一个域名。',
|
||||
credential: '邮箱地址凭据',
|
||||
@@ -110,7 +110,7 @@ const generateName = async () => {
|
||||
.split('@')[0]
|
||||
.replace(/\s+/g, '.')
|
||||
.replace(/\.{2,}/g, '.')
|
||||
.replace(/[^a-zA-Z0-9.]/g, '')
|
||||
.replace(/[^a-z0-9]/g, '')
|
||||
.toLowerCase();
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
@@ -140,17 +140,45 @@ const newEmail = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const addressPrefix = computed(() => {
|
||||
// if user has role, return role prefix
|
||||
if (userSettings.value?.user_role) {
|
||||
return userSettings.value.user_role.prefix || "";
|
||||
}
|
||||
// if user has no role, return default prefix
|
||||
return openSettings.value.prefix;
|
||||
});
|
||||
|
||||
const domainsOptions = computed(() => {
|
||||
// if user has role, return role domains
|
||||
if (userSettings.value.user_role) {
|
||||
const allDomains = userSettings.value.user_role.domains;
|
||||
if (!allDomains) return openSettings.value.domains;
|
||||
return openSettings.value.domains.filter((domain) => {
|
||||
return allDomains.includes(domain.value);
|
||||
});
|
||||
}
|
||||
// if user has no role, return default domains
|
||||
if (!openSettings.value.defaultDomains) {
|
||||
return openSettings.value.domains;
|
||||
}
|
||||
// if user has no role and no default domains, return all domains
|
||||
return openSettings.value.domains.filter((domain) => {
|
||||
return openSettings.value.defaultDomains.includes(domain.value);
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
if (!openSettings.value.domains || openSettings.value.domains.length === 0) {
|
||||
await api.getOpenSettings();
|
||||
}
|
||||
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0]?.value : "";
|
||||
emailDomain.value = domainsOptions.value ? domainsOptions.value[0]?.value : "";
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-alert v-if="userSettings.user_email" :show-icon="false" closable>
|
||||
<n-alert v-if="userSettings.user_email" :show-icon="false" :bordered="false" closable>
|
||||
<span>{{ t('bindUserInfo') }}</span>
|
||||
</n-alert>
|
||||
<n-tabs v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||
@@ -186,13 +214,14 @@ onMounted(async () => {
|
||||
{{ t('generateName') }}
|
||||
</n-button>
|
||||
<n-input-group>
|
||||
<n-input-group-label v-if="openSettings.prefix">
|
||||
{{ openSettings.prefix }}
|
||||
<n-input-group-label v-if="addressPrefix">
|
||||
{{ addressPrefix }}
|
||||
</n-input-group-label>
|
||||
<n-input v-model:value="emailName" />
|
||||
<n-input v-model:value="emailName" show-count :minlength="openSettings.minAddressLen"
|
||||
:maxlength="openSettings.maxAddressLen" />
|
||||
<n-input-group-label>@</n-input-group-label>
|
||||
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||
:options="openSettings.domains" />
|
||||
:options="domainsOptions" />
|
||||
</n-input-group>
|
||||
<Turnstile v-model:value="cfToken" />
|
||||
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">
|
||||
@@ -205,7 +234,7 @@ onMounted(async () => {
|
||||
</n-spin>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="help" :tab="t('help')">
|
||||
<n-alert :show-icon="false">
|
||||
<n-alert :show-icon="false" :bordered="false">
|
||||
<span>{{ t('pleaseGetNewEmail') }}</span>
|
||||
</n-alert>
|
||||
<AdminContact />
|
||||
|
||||
@@ -59,7 +59,7 @@ const deleteAccount = async () => {
|
||||
|
||||
<template>
|
||||
<div class="center" v-if="settings.address">
|
||||
<n-card>
|
||||
<n-card :bordered="false" embedded>
|
||||
<Appearance />
|
||||
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
||||
{{ t('showAddressCredential') }}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import useClipboard from 'vue-clipboard3'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Copy, User, ExchangeAlt } from '@vicons/fa'
|
||||
@@ -19,7 +19,7 @@ const router = useRouter()
|
||||
|
||||
const {
|
||||
jwt, settings, showAddressCredential, userJwt,
|
||||
isTelegram
|
||||
isTelegram, openSettings
|
||||
} = useGlobalState()
|
||||
|
||||
const { locale, t } = useI18n({
|
||||
@@ -52,6 +52,17 @@ const { locale, t } = useI18n({
|
||||
const showChangeAddress = ref(false)
|
||||
const showTelegramChangeAddress = ref(false)
|
||||
const showLocalAddress = ref(false)
|
||||
const addressLabel = computed(() => {
|
||||
if (settings.value.address) {
|
||||
const domain = settings.value.address.split('@')[1]
|
||||
const domainLabel = openSettings.value.domains.find(
|
||||
d => d.value === domain
|
||||
)?.label;
|
||||
if (!domainLabel) return settings.value.address;
|
||||
return settings.value.address.replace('@' + domain, `@${domainLabel}`);
|
||||
}
|
||||
return settings.value.address;
|
||||
})
|
||||
|
||||
const copy = async () => {
|
||||
try {
|
||||
@@ -73,13 +84,13 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-card v-if="!settings.fetched">
|
||||
<n-card :bordered="false" embedded v-if="!settings.fetched">
|
||||
<n-skeleton style="height: 50vh" />
|
||||
</n-card>
|
||||
<div v-else-if="settings.address">
|
||||
<n-alert type="info" :show-icon="false">
|
||||
<n-alert type="info" :show-icon="false" :bordered="false">
|
||||
<span>
|
||||
<b>{{ settings.address }}</b>
|
||||
<b>{{ addressLabel }}</b>
|
||||
<n-button v-if="isTelegram" style="margin-left: 10px" @click="showTelegramChangeAddress = true"
|
||||
size="small" tertiary type="primary">
|
||||
<n-icon :component="ExchangeAlt" /> {{ t('addressManage') }}
|
||||
@@ -102,8 +113,8 @@ onMounted(async () => {
|
||||
<TelegramAddress />
|
||||
</div>
|
||||
<div v-else class="center">
|
||||
<n-card style="max-width: 600px;">
|
||||
<n-alert v-if="jwt" type="warning" :show-icon="false" closable>
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<n-alert v-if="jwt" type="warning" :show-icon="false" :bordered="false" closable>
|
||||
<span>{{ t('fetchAddressError') }}</span>
|
||||
</n-alert>
|
||||
<Login />
|
||||
@@ -129,7 +140,7 @@ onMounted(async () => {
|
||||
<span>
|
||||
<p>{{ t("addressCredentialTip") }}</p>
|
||||
</span>
|
||||
<n-card>
|
||||
<n-card :bordered="false" embedded>
|
||||
<b>{{ jwt }}</b>
|
||||
</n-card>
|
||||
</n-modal>
|
||||
|
||||
@@ -86,6 +86,6 @@ onMounted(async () => {
|
||||
{{ t('download') }}
|
||||
</n-button>
|
||||
</n-modal>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -81,7 +81,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card v-if="settings.address" :title='t("settings")'>
|
||||
<n-card :bordered="false" embedded v-if="settings.address" :title='t("settings")'>
|
||||
<div class="right">
|
||||
<n-button type="primary" @click="saveData">{{ t('save') }}</n-button>
|
||||
</div>
|
||||
|
||||
@@ -144,12 +144,12 @@ const columns = [
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-alert type="warning" :show-icon="false">
|
||||
<n-alert type="warning" :show-icon="false" :bordered="false">
|
||||
<span>{{ t('tip') }}</span>
|
||||
</n-alert>
|
||||
<n-tabs type="segment" v-model:value="tabValue">
|
||||
<n-tab-pane name="address" :tab="t('address')">
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="bind" :tab="t('bind')">
|
||||
<Login :bindUserAddress="bindAddress" />
|
||||
|
||||
@@ -142,9 +142,9 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center" v-if="settings.address">
|
||||
<n-card>
|
||||
<n-card :bordered="false" embedded>
|
||||
<div v-if="!settings.send_balance || settings.send_balance <= 0">
|
||||
<n-alert type="warning" :show-icon="false">
|
||||
<n-alert type="warning" :show-icon="false" :bordered="false">
|
||||
{{ t('requestAccessTip') }}
|
||||
<n-button type="primary" tertiary @click="requestAccess" size="small">{{ t('requestAccess')
|
||||
}}</n-button>
|
||||
@@ -153,7 +153,7 @@ onMounted(async () => {
|
||||
<AdminContact />
|
||||
</div>
|
||||
<div v-else>
|
||||
<n-alert type="info" :show-icon="false">
|
||||
<n-alert type="info" :show-icon="false" :bordered="false">
|
||||
{{ t('send_balance') }}: {{ settings.send_balance }}
|
||||
</n-alert>
|
||||
<div class="right">
|
||||
@@ -187,7 +187,7 @@ onMounted(async () => {
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item :label="t('content')" label-placement="top">
|
||||
<n-card v-if="isPreview">
|
||||
<n-card :bordered="false" embedded v-if="isPreview">
|
||||
<div v-html="sendMailModel.content" />
|
||||
</n-card>
|
||||
<div v-else-if="sendMailModel.contentType == 'rich'" style="border: 1px solid #ccc">
|
||||
|
||||
@@ -148,7 +148,7 @@ onMounted(async () => {
|
||||
<div>
|
||||
<n-tabs type="segment">
|
||||
<n-tab-pane name="address" :tab="t('address')">
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="bind" :tab="t('bind')">
|
||||
<Login :newAddressPath="newAddressPath" :bindUserAddress="bindAddress" />
|
||||
|
||||
@@ -89,7 +89,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center" v-if="settings.address">
|
||||
<n-card v-if="enableWebhook" style="max-width: 800px; overflow: auto;">
|
||||
<n-card :bordered="false" embedded v-if="enableWebhook" style="max-width: 800px; overflow: auto;">
|
||||
<n-form-item-row label="URL">
|
||||
<n-input v-model:value="webhookSettings.url" />
|
||||
</n-form-item-row>
|
||||
|
||||
@@ -41,7 +41,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card v-if="curMail.message" style="max-width: 800px; overflow: auto;">
|
||||
<n-card :bordered="false" embedded v-if="curMail.message" style="max-width: 800px; overflow: auto;">
|
||||
<n-tag type="info">
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
|
||||
@@ -165,6 +165,6 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" embedded />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -29,7 +29,7 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center" v-if="userSettings.user_email">
|
||||
<n-card style="max-width: 600px;">
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<Login />
|
||||
</n-card>
|
||||
</div>
|
||||
|
||||
@@ -37,19 +37,19 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-card v-if="!userSettings.fetched">
|
||||
<n-card :bordered="false" embedded v-if="!userSettings.fetched">
|
||||
<n-skeleton style="height: 50vh" />
|
||||
</n-card>
|
||||
<div v-else-if="userSettings.user_email">
|
||||
<n-alert type="success" :show-icon="false">
|
||||
<n-alert type="success" :show-icon="false" :bordered="false">
|
||||
<span>
|
||||
<b>{{ t('currentUser') }} <b>{{ userSettings.user_email }}</b></b>
|
||||
</span>
|
||||
</n-alert>
|
||||
</div>
|
||||
<div v-else class="center">
|
||||
<n-card style="max-width: 600px;">
|
||||
<n-alert v-if="userJwt" type="warning" :show-icon="false" closable>
|
||||
<n-card :bordered="false" embedded style="max-width: 600px;">
|
||||
<n-alert v-if="userJwt" type="warning" :show-icon="false" :bordered="false" closable>
|
||||
<span>{{ t('fetchUserSettingsError') }}</span>
|
||||
</n-alert>
|
||||
<UserLogin />
|
||||
|
||||
@@ -10,7 +10,7 @@ import { hashPassword } from '../../utils';
|
||||
|
||||
import Turnstile from '../../components/Turnstile.vue';
|
||||
|
||||
const { userJwt, userTab, userOpenSettings } = useGlobalState()
|
||||
const { userJwt, userTab, userOpenSettings, openSettings } = useGlobalState()
|
||||
const message = useMessage();
|
||||
const router = useRouter();
|
||||
|
||||
@@ -98,7 +98,7 @@ const sendVerificationCode = async () => {
|
||||
message.error(t('pleaseInputEmail'));
|
||||
return;
|
||||
}
|
||||
if (!cfToken.value && userOpenSettings.value.enableMailVerify) {
|
||||
if (openSettings.value.cfTurnstileSiteKey && !cfToken.value && userOpenSettings.value.enableMailVerify) {
|
||||
message.error(t('pleaseCompleteTurnstile'));
|
||||
return;
|
||||
}
|
||||
@@ -228,7 +228,7 @@ onMounted(async () => {
|
||||
{{ t('resetPassword') }}
|
||||
</n-button>
|
||||
</n-form>
|
||||
<n-alert v-else :show-icon="false">
|
||||
<n-alert v-else :show-icon="false" :bordered="false">
|
||||
<span>
|
||||
{{ t('cannotForgotPassword') }}
|
||||
</span>
|
||||
|
||||
@@ -43,8 +43,8 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="center" v-if="userSettings.user_email">
|
||||
<n-card>
|
||||
<n-alert :show-icon="false">
|
||||
<n-card :bordered="false" embedded>
|
||||
<n-alert :show-icon="false" :bordered="false">
|
||||
<span>
|
||||
{{ t('passordTip') }}
|
||||
</span>
|
||||
|
||||
1
mail-parser-wasm/.gitignore
vendored
1
mail-parser-wasm/.gitignore
vendored
@@ -12,3 +12,4 @@ Cargo.lock
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
web/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mail-parser-wasm"
|
||||
version = "0.1.6"
|
||||
version = "0.1.8"
|
||||
edition = "2021"
|
||||
description = "A simple mail parser for wasm"
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,16 +1,45 @@
|
||||
# mail-parser-wasm
|
||||
# mail-parser-wasm web and cf worker
|
||||
|
||||
## usage
|
||||
## [mail-parser-wasm](https://www.npmjs.com/package/mail-parser-wasm)
|
||||
|
||||
### mail-parser-wasm usage
|
||||
|
||||
```bash
|
||||
pnpm add mail-parser-wasm
|
||||
```
|
||||
|
||||
```js
|
||||
import { parse_message } from 'mail-parser-wasm'
|
||||
|
||||
const parsedEmail = parse_message(item.raw);
|
||||
const parsedEmail = parse_message(rawEmail);
|
||||
```
|
||||
|
||||
## build
|
||||
### mail-parser-wasm build
|
||||
|
||||
```bash
|
||||
wasm-pack build --release
|
||||
wasm-pack publish
|
||||
```
|
||||
|
||||
## [mail-parser-wasm-worker](https://www.npmjs.com/package/mail-parser-wasm-worker)
|
||||
|
||||
### mail-parser-wasm-worker usage
|
||||
|
||||
```bash
|
||||
pnpm add mail-parser-wasm-worker
|
||||
```
|
||||
|
||||
```js
|
||||
import { parse_message_wrapper } from 'mail-parser-wasm-worker'
|
||||
|
||||
const parsedEmail = parse_message_wrapper(rawEmail);
|
||||
```
|
||||
|
||||
### mail-parser-wasm-worker build
|
||||
|
||||
```bash
|
||||
wasm-pack build --out-dir web --target web --release
|
||||
find web/ -type f ! -name '*.json' ! -name '.gitignore' -exec cp {} worker/ \;
|
||||
# modify worker/package.json version or whatever
|
||||
pnpm publish worker --no-git-checks
|
||||
```
|
||||
|
||||
9
mail-parser-wasm/worker/index.d.ts
vendored
Normal file
9
mail-parser-wasm/worker/index.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import initAsync, { MessageResult } from './mail_parser_wasm';
|
||||
import MODULE from './mail_parser_wasm_bg.wasm';
|
||||
export { initAsync, MODULE };
|
||||
export * from './mail_parser_wasm';
|
||||
/**
|
||||
* @param {string} raw_message
|
||||
* @returns {MessageResult}
|
||||
*/
|
||||
export function parse_message_wrapper(raw_message: string): MessageResult;
|
||||
12
mail-parser-wasm/worker/index.js
Normal file
12
mail-parser-wasm/worker/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import initAsync, { initSync, parse_message } from './mail_parser_wasm';
|
||||
import MODULE from './mail_parser_wasm_bg.wasm';
|
||||
|
||||
initSync(MODULE);
|
||||
|
||||
|
||||
export { initAsync, MODULE };
|
||||
export * from './mail_parser_wasm';
|
||||
export const parse_message_wrapper = (raw_message) => {
|
||||
initSync(MODULE);
|
||||
return parse_message(raw_message);
|
||||
}
|
||||
24
mail-parser-wasm/worker/package.json
Normal file
24
mail-parser-wasm/worker/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "mail-parser-wasm-worker",
|
||||
"description": "A simple mail parser for worker",
|
||||
"homepage": "https://github.com/dreamhunter2333/cloudflare_temp_email/tree/main/mail-parser-wasm",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dreamhunter2333/cloudflare_temp_email",
|
||||
"directory": "mail-parser-wasm"
|
||||
},
|
||||
"version": "0.1.8",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"mail_parser_wasm_bg.wasm",
|
||||
"mail_parser_wasm.js",
|
||||
"mail_parser_wasm.d.ts",
|
||||
"index.js",
|
||||
"index.d.ts"
|
||||
],
|
||||
"module": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"sideEffects": [
|
||||
"./snippets/*"
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
const API_PATHS = ["/api/", "/open_api/", "/user_api/", "/admin/"]
|
||||
const API_PATHS = [
|
||||
"/api/",
|
||||
"/open_api/",
|
||||
"/user_api/",
|
||||
"/admin/",
|
||||
"/telegram/"
|
||||
];
|
||||
|
||||
export async function onRequest(context) {
|
||||
const reqPath = new URL(context.request.url).pathname;
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"wrangler": "^3.55.0"
|
||||
"wrangler": "^3.62.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ class Settings(BaseSettings):
|
||||
proxy_url: str = "http://localhost:8787"
|
||||
port: int = 8025
|
||||
imap_port: int = 11143
|
||||
basic_password: str = ""
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
|
||||
@@ -121,6 +121,7 @@ class SimpleMailbox:
|
||||
f"{settings.proxy_url}/api/mails?limit={limit}&offset={start - 1}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.password}",
|
||||
"x-custom-auth": f"{settings.basic_password}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
@@ -147,6 +148,7 @@ class SimpleMailbox:
|
||||
f"{settings.proxy_url}/api/sendbox?limit={limit}&offset={start - 1}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.password}",
|
||||
"x-custom-auth": f"{settings.basic_password}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
@@ -47,16 +47,26 @@ def parse_email(raw: str) -> EmailModel:
|
||||
def generate_email_model(item: dict) -> EmailModel:
|
||||
email_json = json.loads(item["raw"])
|
||||
message = MIMEMultipart()
|
||||
message['From'] = f"{email_json["from"]['name']} <{
|
||||
email_json["from"]['email']}>"
|
||||
message['To'] = ", ".join(
|
||||
[f"{to['name']} <{to['email']}>" for to in email_json["personalizations"][0]["to"]])
|
||||
if email_json.get("version") == "v2":
|
||||
message['From'] = f"{email_json["from_name"]} <{item["address"]}>" if email_json.get(
|
||||
"from_name") else item["address"]
|
||||
message['To'] = f"{email_json["to_name"]} <{email_json["to_mail"]}>" if email_json.get(
|
||||
"to_name") else email_json["to_mail"]
|
||||
message.attach(MIMEText(
|
||||
email_json["content"],
|
||||
"html" if email_json.get("is_html") else "plain"
|
||||
))
|
||||
else:
|
||||
message['From'] = f"{email_json["from"]['name']} <{
|
||||
email_json["from"]['email']}>"
|
||||
message['To'] = ", ".join(
|
||||
[f"{to['name']} <{to['email']}>" for to in email_json["personalizations"][0]["to"]])
|
||||
message.attach(MIMEText(
|
||||
email_json["content"][0]["value"],
|
||||
"html" if "html" in email_json["content"][0]["type"] else "plain"
|
||||
))
|
||||
message['Subject'] = email_json["subject"]
|
||||
message["Date"] = datetime.datetime.strptime(
|
||||
item["created_at"], "%Y-%m-%d %H:%M:%S"
|
||||
).strftime("%a, %d %b %Y %H:%M:%S +0000")
|
||||
message.attach(MIMEText(
|
||||
email_json["content"][0]["value"],
|
||||
"html" if "html" in email_json["content"][0]["type"] else "plain"
|
||||
))
|
||||
return parse_email(message.as_string())
|
||||
|
||||
@@ -31,6 +31,16 @@ export default defineConfig({
|
||||
logo: { src: '/logo.png', width: 24, height: 24 },
|
||||
search: { provider: 'local' },
|
||||
socialLinks: [
|
||||
{
|
||||
icon: 'discord',
|
||||
link: 'https://discord.gg/dQEwTWhA6Q'
|
||||
},
|
||||
{
|
||||
icon: {
|
||||
svg: '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 448 512"><path d="M446.7 98.6l-67.6 318.8c-5.1 22.5-18.4 28.1-37.3 17.5l-103-75.9l-49.7 47.8c-5.5 5.5-10.1 10.1-20.7 10.1l7.4-104.9l190.9-172.5c8.3-7.4-1.8-11.5-12.9-4.1L117.8 284L16.2 252.2c-22.1-6.9-22.5-22.1 4.6-32.7L418.2 66.4c18.4-6.9 34.5 4.1 28.5 32.2z" fill="currentColor"></path></svg>'
|
||||
},
|
||||
link: 'https://t.me/cloudflare_temp_email'
|
||||
},
|
||||
{
|
||||
icon: 'github',
|
||||
link: 'https://github.com/dreamhunter2333/cloudflare_temp_email'
|
||||
|
||||
@@ -100,7 +100,6 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
items: [
|
||||
{ text: '命令行部署准备', link: 'cli/pre-requisite' },
|
||||
{ text: 'D1 数据库', link: 'cli/d1' },
|
||||
{ text: '配置 DKIM', link: 'dkim' },
|
||||
{ text: 'Cloudflare workers 后端', link: 'cli/worker' },
|
||||
{ text: '配置邮件转发', link: 'email-routing.md' },
|
||||
{ text: 'Cloudflare Pages 前端', link: 'cli/pages' },
|
||||
@@ -112,7 +111,6 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'D1 数据库', link: 'ui/d1' },
|
||||
{ text: '配置 DKIM', link: 'dkim' },
|
||||
{ text: 'Cloudflare workers 后端', link: 'ui/worker' },
|
||||
{ text: '配置邮件转发', link: 'email-routing.md' },
|
||||
{ text: 'Cloudflare Pages 前端', link: 'ui/pages' },
|
||||
@@ -136,6 +134,8 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
{ text: '配置子域名邮箱', link: 'feature/subdomain' },
|
||||
{ text: '配置 Telegram Bot', link: 'feature/telegram' },
|
||||
{ text: '配置 S3 附件', link: 'feature/s3-attachment' },
|
||||
{ text: '配置 worker 使用 wasm 解析邮件', link: 'feature/mail_parser_wasm_worker' },
|
||||
{ text: '新建邮箱地址 API', link: 'feature/new-address-api' },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -34,10 +34,10 @@ git clone https://github.com/dreamhunter2333/cloudflare_temp_email.git
|
||||
```bash
|
||||
# create a database, and copy the output to wrangler.toml in the next step
|
||||
wrangler d1 create dev
|
||||
wrangler d1 execute dev --file=db/schema.sql
|
||||
wrangler d1 execute dev --file=db/schema.sql --remote
|
||||
# schema update, if you have initialized the database before this date, you can execute this command to update
|
||||
# wrangler d1 execute dev --file=db/2024-01-13-patch.sql
|
||||
# wrangler d1 execute dev --file=db/2024-04-03-patch.sql
|
||||
# wrangler d1 execute dev --file=db/2024-01-13-patch.sql --remote
|
||||
# wrangler d1 execute dev --file=db/2024-04-03-patch.sql --remote
|
||||
# create a namespace, and copy the output to wrangler.toml in the next step
|
||||
wrangler kv:namespace create DEV
|
||||
```
|
||||
@@ -76,13 +76,26 @@ node_compat = true
|
||||
[vars]
|
||||
# TITLE = "Custom Title" # The title of the site
|
||||
PREFIX = "tmp" # The mailbox name prefix to be processed
|
||||
# (min, max) length of the adderss, if not set, the default is (1, 30)
|
||||
# ANNOUNCEMENT = "Custom Announcement"
|
||||
# MIN_ADDRESS_LEN = 1
|
||||
# MAX_ADDRESS_LEN = 30
|
||||
# If you want your site to be private, uncomment below and change your password
|
||||
# PASSWORDS = ["123", "456"]
|
||||
# admin console password, if not configured, access to the console is not allowed
|
||||
# ADMIN_PASSWORDS = ["123", "456"]
|
||||
# admin contact information. If not configured, it will not be displayed. Any string can be configured.
|
||||
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # your domain name
|
||||
DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all your domain name
|
||||
# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name
|
||||
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
||||
# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification)
|
||||
# User roles configuration, if domains is empty will use default_domains, if prefix is null will use default prefix, if prefix is empty string will not use prefix
|
||||
# USER_ROLES = [
|
||||
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
|
||||
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "admin", prefix = "" },
|
||||
# ]
|
||||
JWT_SECRET = "xxx" # Key used to generate jwt
|
||||
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
|
||||
# Allow users to create email addresses
|
||||
@@ -100,9 +113,6 @@ ENABLE_AUTO_REPLY = false
|
||||
# Turnstile verification configuration
|
||||
# CF_TURNSTILE_SITE_KEY = ""
|
||||
# CF_TURNSTILE_SECRET_KEY = ""
|
||||
# dkim config
|
||||
# DKIM_SELECTOR = "mailchannels" # Refer to the DKIM section mailchannels._domainkey for mailchannels
|
||||
# DKIM_PRIVATE_KEY = "" # Refer to the contents of priv_key.txt in the DKIM section
|
||||
# telegram bot
|
||||
# TG_MAX_ACCOUNTS = 5
|
||||
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||
@@ -154,39 +164,3 @@ pnpm run deploy
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Configure sending emails
|
||||
|
||||
Find the `SPF` record of `TXT` in the domain name `DNS` record, and add `include:relay.mailchannels.net`
|
||||
|
||||
```bash
|
||||
v=spf1 include:_spf.mx.cloudflare.net include:relay.mailchannels.net ~all
|
||||
```
|
||||
|
||||
Create a new `_mailchannels` record, the type is `TXT`, the content is `v=mc1 cfid=your worker domain name`
|
||||
|
||||
- The worker domain name here is the domain name of the back-end api. For example, if I deploy it at `https://temp-email-api.awsl.uk/`, fill in `v=mc1 cfid=awsl.uk`
|
||||
- If your domain name is `https://temp-email-api.xxx.workers.dev`, fill in `v=mc1 cfid=xxx.workers.dev`
|
||||
|
||||
## Configure DKIM
|
||||
|
||||
Ref: [Adding-a-DKIM-Signature](https://support.mailchannels.com/hc/en-us/articles/7122849237389-Adding-a-DKIM-Signature)
|
||||
|
||||
Creating a DKIM private and public key:
|
||||
Private key as PEM file and base64 encoded txt file:
|
||||
|
||||
```bash
|
||||
openssl genrsa 2048 | tee priv_key.pem | openssl rsa -outform der | openssl base64 -A > priv_key.txt
|
||||
```
|
||||
|
||||
Public key as DNS record:
|
||||
|
||||
```bash
|
||||
echo -n "v=DKIM1;p=" > pub_key_record.txt && \
|
||||
openssl rsa -in priv_key.pem -pubout -outform der | openssl base64 -A >> pub_key_record.txt
|
||||
```
|
||||
|
||||
Add `TXT` record in `Cloudflare` all your mail domain `DNS`
|
||||
|
||||
- `_dmarc`: `v=DMARC1; p=none; adkim=r; aspf=r;`
|
||||
- `mailchannels._domainkey`: `v=DKIM1; p=<content of the file pub_key_record.txt>`
|
||||
|
||||
@@ -8,7 +8,7 @@ hero:
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Try it now
|
||||
link: https://mail.awsl.uk/
|
||||
link: https://mail.awsl.uk/en
|
||||
- theme: alt
|
||||
text: command line deployment
|
||||
link: /en/cli
|
||||
|
||||
@@ -9,7 +9,7 @@ cd worker
|
||||
cp wrangler.toml.template wrangler.toml
|
||||
# 创建 D1 并执行 schema.sql
|
||||
wrangler d1 create dev
|
||||
wrangler d1 execute dev --file=../db/schema.sql
|
||||
wrangler d1 execute dev --file=../db/schema.sql --remote
|
||||
```
|
||||
|
||||
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
|
||||
@@ -25,6 +25,6 @@ wrangler d1 execute dev --file=../db/schema.sql
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
wrangler d1 execute dev --file=../db/2024-01-13-patch.sql
|
||||
wrangler d1 execute dev --file=../db/2024-04-03-patch.sql
|
||||
wrangler d1 execute dev --file=../db/2024-01-13-patch.sql --remote
|
||||
wrangler d1 execute dev --file=../db/2024-04-03-patch.sql --remote
|
||||
```
|
||||
|
||||
@@ -44,13 +44,28 @@ node_compat = true
|
||||
[vars]
|
||||
# TITLE = "Custom Title" # 自定义网站标题
|
||||
PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空字符串
|
||||
# (min, max) adderss的长度,如果不设置,默认为(1, 30)
|
||||
# ANNOUNCEMENT = "Custom Announcement" # 自定义公告
|
||||
# MIN_ADDRESS_LEN = 1
|
||||
# MAX_ADDRESS_LEN = 30
|
||||
# 如果你想要你的网站私有,取消下面的注释,并修改密码
|
||||
# PASSWORDS = ["123", "456"]
|
||||
# admin 控制台密码, 不配置则不允许访问控制台
|
||||
# ADMIN_PASSWORDS = ["123", "456"]
|
||||
# admin 联系方式,不配置则不显示,可配置任意字符串
|
||||
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||
# DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 默认用户可用的域名(未登录或未分配角色的用户)
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
|
||||
# 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称
|
||||
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
||||
# 新用户默认角色, 仅在启用邮件验证时有效
|
||||
# USER_DEFAULT_ROLE = "vip"
|
||||
# 用户角色配置, 如果 domains 为空将使用 default_domains
|
||||
# 如果 prefix 为 null 将使用默认前缀, 如果 prefix 为空字符串将不使用前缀
|
||||
# USER_ROLES = [
|
||||
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
|
||||
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "admin", prefix = "" },
|
||||
# ]
|
||||
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
|
||||
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
||||
# 是否允许用户创建邮件, 不配置则不允许
|
||||
@@ -68,9 +83,6 @@ ENABLE_AUTO_REPLY = false
|
||||
# Turnstile 人机验证配置
|
||||
# CF_TURNSTILE_SITE_KEY = ""
|
||||
# CF_TURNSTILE_SECRET_KEY = ""
|
||||
# dkim config
|
||||
# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels
|
||||
# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容
|
||||
# telegram bot 最多绑定邮箱数量
|
||||
# TG_MAX_ACCOUNTS = 5
|
||||
# 全局转发地址列表,如果不配置则不启用,启用后所有邮件都会转发到列表中的地址
|
||||
|
||||
@@ -23,19 +23,3 @@ wrangler secret put RESEND_TOKEN
|
||||
wrangler secret put RESEND_TOKEN_XXX_COM
|
||||
wrangler secret put RESEND_TOKEN_DREAMHUNTER2333_XYZ
|
||||
```
|
||||
|
||||
## 使用 mailchannels 发送邮件
|
||||
|
||||
::: warning
|
||||
[Mail Channels 免费电子邮件发送 API 将于2024年6月30日结束](https://support.mailchannels.com/hc/en-us/articles/26814255454093-End-of-Life-Notice-Cloudflare-Workers)
|
||||
:::
|
||||
|
||||
1. 找到域名 `DNS` 记录的 `TXT` 的 `SPF` 记录, 增加 `include:relay.mailchannels.net`
|
||||
|
||||
`v=spf1 include:_spf.mx.cloudflare.net include:relay.mailchannels.net ~all`
|
||||
|
||||
2. 新建 `_mailchannels` 记录, 类型为 `TXT`, 内容为 `v=mc1 cfid=你的worker域名`
|
||||
|
||||
- 此处 worker 域名为后端 api 的域名,比如我部署在 `https://temp-email-api.awsl.uk/`,则填写 `v=mc1 cfid=awsl.uk`
|
||||
|
||||
- 如果你的域名是 `https://temp-email-api.xxx.workers.dev`,则填写 `v=mc1 cfid=xxx.workers.dev`
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
# 配置 DKIM
|
||||
|
||||
如果你不想配置 DKIM,可以跳过这一节。
|
||||
|
||||
参考: [Adding-a-DKIM-Signature](https://support.mailchannels.com/hc/en-us/articles/7122849237389-Adding-a-DKIM-Signature)
|
||||
|
||||
Creating a DKIM private and public key:
|
||||
Private key as PEM file and base64 encoded txt file:
|
||||
|
||||
```bash
|
||||
openssl genrsa 2048 | tee priv_key.pem | openssl rsa -outform der | openssl base64 -A > priv_key.txt
|
||||
```
|
||||
|
||||
Public key as DNS record:
|
||||
|
||||
```bash
|
||||
echo -n "v=DKIM1;p=" > pub_key_record.txt && \
|
||||
openssl rsa -in priv_key.pem -pubout -outform der | openssl base64 -A >> pub_key_record.txt
|
||||
```
|
||||
|
||||
在 `Cloudflare` 的 `DNS` 记录中添加 `TXT` 记录
|
||||
|
||||
例如:
|
||||
|
||||
- `_dmarc`: `v=DMARC1; p=none; adkim=r; aspf=r;`
|
||||
- `mailchannels._domainkey`: `v=DKIM1; p=<content of the file pub_key_record.txt>`
|
||||
|
||||
那我在 `wrangler.toml` 中的配置应该是这样的:
|
||||
|
||||
```toml
|
||||
DKIM_SELECTOR = "mailchannels"
|
||||
DKIM_PRIVATE_KEY = "<priv_key.txt 的内容>"
|
||||
```
|
||||
@@ -0,0 +1,63 @@
|
||||
# mail-parser-wasm-worker
|
||||
|
||||
> [!NOTE]
|
||||
> 如果你使用了 webhook 转发,或者 telegram bot 接受邮件,但是邮件内容是乱码,或者无法解析,你对解析的需要更高的要求,可以使用这个功能。
|
||||
|
||||
## 修改代码
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
pnpm add mail-parser-wasm-worker
|
||||
```
|
||||
|
||||
编辑 `worker/src/common.ts`, 取消注释这段代码,使用 mail-parser-wasm-worker 来解析邮件
|
||||
|
||||
```ts
|
||||
export const commonParseMail = async (raw_mail: string | undefined | null): Promise<{
|
||||
sender: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string
|
||||
} | undefined> => {
|
||||
if (!raw_mail) {
|
||||
return undefined;
|
||||
}
|
||||
// 取消注释这段代码,使用 mail-parser-wasm-worker 来解析邮件 start
|
||||
// TODO: WASM parse email
|
||||
try {
|
||||
const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
|
||||
|
||||
const parsedEmail = parse_message_wrapper(raw_mail);
|
||||
return {
|
||||
sender: parsedEmail.sender || "",
|
||||
subject: parsedEmail.subject || "",
|
||||
text: parsedEmail.text || "",
|
||||
html: parsedEmail.body_html || "",
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Failed use mail-parser-wasm-worker to parse email", e);
|
||||
}
|
||||
// 取消注释这段代码,使用 mail-parser-wasm-worker 来解析邮件 end
|
||||
try {
|
||||
const { default: PostalMime } = await import('postal-mime');
|
||||
const parsedEmail = await PostalMime.parse(raw_mail);
|
||||
return {
|
||||
sender: parsedEmail.from ? `${parsedEmail.from.name} <${parsedEmail.from.address}>` : "",
|
||||
subject: parsedEmail.subject || "",
|
||||
text: parsedEmail.text || "",
|
||||
html: parsedEmail.html || "",
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Failed use PostalMime to parse email", e);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
pnpm run deploy
|
||||
```
|
||||
25
vitepress-docs/docs/zh/guide/feature/new-address-api.md
Normal file
25
vitepress-docs/docs/zh/guide/feature/new-address-api.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 新建邮箱地址 API
|
||||
|
||||
## 通过 admin API 新建邮箱地址
|
||||
|
||||
这是一个 `python` 的例子,使用 `requests` 库发送邮件。
|
||||
|
||||
```python
|
||||
res = requests.post(
|
||||
# 替换 xxxx.xxxx 为你的 worker 域名
|
||||
"https://xxxx.xxxx/admin/new_address",
|
||||
json={
|
||||
# 是否启用前缀 (True/False)
|
||||
"enablePrefix": True,
|
||||
"name": "<邮箱名称>",
|
||||
"domain": "<邮箱域名>",
|
||||
},
|
||||
headers={
|
||||
'x-admin-auth': "<你的网站admin密码>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
# 返回值 {"jwt": "<Jwt>"}
|
||||
print(res.json())
|
||||
```
|
||||
@@ -1,5 +1,9 @@
|
||||
# 配置子域名邮箱
|
||||
|
||||
::: warning
|
||||
子域名邮箱发送邮件可能无法发送邮件,建议使用主域名邮箱发送邮件,子域名邮箱仅用于接收邮件。
|
||||
:::
|
||||
|
||||
参考
|
||||
|
||||
- [配置子域名邮箱](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/164#issuecomment-2082612710)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
- `BACKEND_TOML`: 后端配置文件,[参考此处](/zh/guide/cli/worker.html#修改-wrangler-toml-配置文件)
|
||||
- `FRONTEND_ENV`: 前端配置文件,请复制 `frontend/.env.example` 的内容,[并参考此处修改](/zh/guide/cli/pages.html)
|
||||
- `FRONTEND_NAME`: 你在 Cloudflare Pages 创建的项目名称,可通过 [用户界面](https://temp-mail-docs.awsl.uk/zh/guide/ui/pages.html) 或者 [命令行](https://temp-mail-docs.awsl.uk/zh/guide/cli/pages.html) 创建
|
||||
- `FRONTEND_BRANCH`: (可选) pages 部署的分支,可不配置,默认 `production`
|
||||
- `TG_FRONTEND_NAME`: (可选) 你在 Cloudflare Pages 创建的项目名称,同 `FRONTEND_NAME`,如果需要 Telegram Mini App 功能,请填写
|
||||
|
||||
1. 打开仓库的 `Actions` 页面,找到 `Deploy Backend Production` 和 `Deploy Frontend`,点击 `Run workflow` 选择分支手动部署
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"version": "0.2.6",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.7",
|
||||
"vitepress": "^1.1.0",
|
||||
"wrangler": "^3.50.0"
|
||||
"@types/node": "^20.14.10",
|
||||
"vitepress": "^1.2.3",
|
||||
"wrangler": "^3.63.1"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vitepress dev docs",
|
||||
|
||||
2689
vitepress-docs/pnpm-lock.yaml
generated
2689
vitepress-docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -11,20 +11,20 @@
|
||||
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240512.0",
|
||||
"@cloudflare/workers-types": "^4.20240620.0",
|
||||
"@eslint/js": "8.56.0",
|
||||
"eslint": "8.56.0",
|
||||
"globals": "^15.3.0",
|
||||
"typescript-eslint": "^7.10.0",
|
||||
"wrangler": "^3.57.1"
|
||||
"globals": "^15.8.0",
|
||||
"typescript-eslint": "^7.15.0",
|
||||
"wrangler": "^3.63.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.588.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.588.0",
|
||||
"hono": "^4.3.9",
|
||||
"@aws-sdk/client-s3": "^3.609.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.609.0",
|
||||
"hono": "^4.4.12",
|
||||
"mimetext": "^3.0.24",
|
||||
"postal-mime": "^2.2.5",
|
||||
"resend": "^3.2.0",
|
||||
"resend": "^3.4.0",
|
||||
"telegraf": "4.16.3"
|
||||
},
|
||||
"pnpm": {
|
||||
|
||||
2043
worker/pnpm-lock.yaml
generated
2043
worker/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
import { Context } from 'hono';
|
||||
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { getJsonSetting, saveSetting, checkUserPassword, getDomains } from '../utils';
|
||||
import { getJsonSetting, saveSetting, checkUserPassword, getDomains, getUserRoles } from '../utils';
|
||||
import { UserSettings, GeoData, UserInfo } from "../models";
|
||||
import { handleListQuery } from '../common'
|
||||
import { HonoCustomType } from '../types';
|
||||
@@ -38,18 +38,22 @@ export default {
|
||||
const { limit, offset, query } = c.req.query();
|
||||
if (query) {
|
||||
return await handleListQuery(c,
|
||||
`SELECT u.id, u.user_email, u.created_at, u.updated_at,`
|
||||
`SELECT u.id as id, u.user_email, u.created_at, u.updated_at,`
|
||||
+ ` ur.role_text as role_text,`
|
||||
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
|
||||
+ ` FROM users u`
|
||||
+ ` LEFT JOIN user_roles ur ON u.id = ur.user_id`
|
||||
+ ` where u.user_email like ?`,
|
||||
`SELECT count(*) as count FROM users where user_email like ?`,
|
||||
[`%${query}%`], limit, offset
|
||||
);
|
||||
}
|
||||
return await handleListQuery(c,
|
||||
`SELECT u.id, u.user_email, u.created_at, u.updated_at,`
|
||||
`SELECT u.id as id, u.user_email, u.created_at, u.updated_at,`
|
||||
+ ` ur.role_text as role_text,`
|
||||
+ ` (SELECT COUNT(*) FROM users_address WHERE user_id = u.id) AS address_count`
|
||||
+ ` FROM users u`,
|
||||
+ ` FROM users u`
|
||||
+ ` LEFT JOIN user_roles ur ON u.id = ur.user_id`,
|
||||
`SELECT count(*) as count FROM users`,
|
||||
[], limit, offset
|
||||
);
|
||||
@@ -114,4 +118,30 @@ export default {
|
||||
}
|
||||
return c.json({ success: true });
|
||||
},
|
||||
updateUserRoles: async (c: Context<HonoCustomType>) => {
|
||||
const { user_id, role_text } = await c.req.json();
|
||||
if (!user_id) return c.text("Invalid user_id", 400);
|
||||
if (!role_text) {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM user_roles WHERE user_id = ?`
|
||||
).bind(user_id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to update user roles", 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
}
|
||||
const user_roles = getUserRoles(c);
|
||||
if (!user_roles.find((r) => r.role === role_text)) {
|
||||
return c.text("Invalid role_text", 400)
|
||||
}
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO user_roles (user_id, role_text)`
|
||||
+ ` VALUES (?, ?)`
|
||||
+ ` ON CONFLICT(user_id) DO UPDATE SET role_text = ?, updated_at = datetime('now')`
|
||||
).bind(user_id, role_text, role_text).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to update user roles", 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { HonoCustomType } from '../types'
|
||||
import { sendAdminInternalMail, getJsonSetting, saveSetting } from '../utils'
|
||||
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles } from '../utils'
|
||||
import { newAddress, handleListQuery } from '../common'
|
||||
import { CONSTANTS } from '../constants'
|
||||
import cleanup_api from './cleanup_api'
|
||||
@@ -40,7 +40,7 @@ api.post('/admin/new_address', async (c) => {
|
||||
return c.text("Please provide a name", 400)
|
||||
}
|
||||
try {
|
||||
const res = await newAddress(c, name, domain, enablePrefix);
|
||||
const res = await newAddress(c, name, domain, enablePrefix, false, null, false);
|
||||
return c.json(res);
|
||||
} catch (e) {
|
||||
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
||||
@@ -127,6 +127,16 @@ api.get('/admin/mails_unknow', async (c) => {
|
||||
);
|
||||
});
|
||||
|
||||
api.delete('/admin/mails/:id', async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE id = ? `
|
||||
).bind(id).run();
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
})
|
||||
|
||||
api.get('/admin/address_sender', async (c) => {
|
||||
const { address, limit, offset } = c.req.query();
|
||||
if (address) {
|
||||
@@ -166,6 +176,16 @@ api.post('/admin/address_sender', async (c) => {
|
||||
})
|
||||
})
|
||||
|
||||
api.delete('/admin/address_sender/:id', async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM address_sender WHERE id = ? `
|
||||
).bind(id).run();
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
})
|
||||
|
||||
api.get('/admin/sendbox', async (c) => {
|
||||
const { address, limit, offset } = c.req.query();
|
||||
if (address) {
|
||||
@@ -182,6 +202,16 @@ api.get('/admin/sendbox', async (c) => {
|
||||
);
|
||||
})
|
||||
|
||||
api.delete('/admin/sendbox/:id', async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM sendbox WHERE id = ? `
|
||||
).bind(id).run();
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
})
|
||||
|
||||
api.get('/admin/statistics', async (c) => {
|
||||
const { count: mailCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM raw_mails`
|
||||
@@ -189,16 +219,24 @@ api.get('/admin/statistics', async (c) => {
|
||||
const { count: addressCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM address`
|
||||
).first<{ count: number }>() || {};
|
||||
const { count: activeUserCount7days } = await c.env.DB.prepare(
|
||||
const { count: activeAddressCount7days } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')`
|
||||
).first<{ count: number }>() || {};
|
||||
const { count: activeAddressCount30days } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM address where updated_at > datetime('now', '-30 day')`
|
||||
).first<{ count: number }>() || {};
|
||||
const { count: sendMailCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM sendbox`
|
||||
).first<{ count: number }>() || {};
|
||||
const { count: userCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM users`
|
||||
).first<{ count: number }>() || {};
|
||||
return c.json({
|
||||
mailCount: mailCount,
|
||||
userCount: addressCount,
|
||||
activeUserCount7days: activeUserCount7days,
|
||||
addressCount: addressCount,
|
||||
activeAddressCount7days: activeAddressCount7days,
|
||||
activeAddressCount30days: activeAddressCount30days,
|
||||
userCount: userCount,
|
||||
sendMailCount: sendMailCount
|
||||
})
|
||||
});
|
||||
@@ -254,5 +292,7 @@ api.get('/admin/users', admin_user_api.getUsers)
|
||||
api.delete('/admin/users/:user_id', admin_user_api.deleteUser)
|
||||
api.post('/admin/users', admin_user_api.createUser)
|
||||
api.post('/admin/users/:user_id/reset_password', admin_user_api.resetPassword)
|
||||
api.get('/admin/user_roles', async (c) => c.json(getUserRoles(c)))
|
||||
api.post('/admin/user_roles', admin_user_api.updateUserRoles)
|
||||
api.get("/admin/webhook/settings", webhook_settings.getWebhookSettings);
|
||||
api.post("/admin/webhook/settings", webhook_settings.saveWebhookSettings);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import { getDomains, getPasswords, getBooleanValue } from './utils';
|
||||
import { getDomains, getPasswords, getBooleanValue, getIntValue, getStringArray, getDefaultDomains, getStringValue } from './utils';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { HonoCustomType } from './types';
|
||||
import { isS3Enabled } from './mails_api/s3_attachment';
|
||||
@@ -17,8 +17,13 @@ api.get('/open_api/settings', async (c) => {
|
||||
}
|
||||
return c.json({
|
||||
"title": c.env.TITLE,
|
||||
"announcement": getStringValue(c.env.ANNOUNCEMENT),
|
||||
"prefix": c.env.PREFIX,
|
||||
"minAddressLen": getIntValue(c.env.MIN_ADDRESS_LEN, 1),
|
||||
"maxAddressLen": getIntValue(c.env.MAX_ADDRESS_LEN, 30),
|
||||
"defaultDomains": getDefaultDomains(c),
|
||||
"domains": getDomains(c),
|
||||
"domainLabels": getStringArray(c.env.DOMAIN_LABELS),
|
||||
"needAuth": needAuth,
|
||||
"adminContact": c.env.ADMIN_CONTACT,
|
||||
"enableUserCreateEmail": getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
|
||||
|
||||
@@ -1,32 +1,52 @@
|
||||
import { Context } from 'hono';
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { getBooleanValue, getDomains, getStringValue } from './utils';
|
||||
import { HonoCustomType } from './types';
|
||||
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains } from './utils';
|
||||
import { HonoCustomType, UserRole } from './types';
|
||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||
|
||||
export const newAddress = async (
|
||||
c: Context<HonoCustomType>,
|
||||
name: string, domain: string | undefined | null,
|
||||
enablePrefix: boolean
|
||||
enablePrefix: boolean,
|
||||
checkLengthByConfig: boolean = true,
|
||||
addressPrefix: string | undefined | null = null,
|
||||
checkAllowDomains: boolean = true
|
||||
): Promise<{ address: string, jwt: string }> => {
|
||||
// remove special characters
|
||||
name = name.replace(/[^a-zA-Z0-9.]/g, '')
|
||||
name = name.replace(/[^a-z0-9]/g, '')
|
||||
// name min length min 1
|
||||
const minAddressLength = Math.max(
|
||||
checkLengthByConfig ? getIntValue(c.env.MIN_ADDRESS_LEN, 1) : 1,
|
||||
1
|
||||
);
|
||||
// name max length min 1
|
||||
const maxAddressLength = Math.max(
|
||||
checkLengthByConfig ? getIntValue(c.env.MAX_ADDRESS_LEN, 30) : 30,
|
||||
1
|
||||
);
|
||||
// check name length
|
||||
if (name.length <= 0) {
|
||||
throw new Error("Name too short")
|
||||
if (name.length < minAddressLength) {
|
||||
throw new Error(`Name too short (min ${minAddressLength})`);
|
||||
}
|
||||
// create address
|
||||
if (enablePrefix) {
|
||||
if (name.length > maxAddressLength) {
|
||||
throw new Error(`Name too long (max ${maxAddressLength})`);
|
||||
}
|
||||
// create address with prefix
|
||||
if (typeof addressPrefix === "string") {
|
||||
name = addressPrefix + name;
|
||||
} else if (enablePrefix) {
|
||||
name = getStringValue(c.env.PREFIX) + name;
|
||||
}
|
||||
if (name.length >= 30) {
|
||||
throw new Error("Name too long (max 30)")
|
||||
// check domain
|
||||
const allowDomains = checkAllowDomains ? await getAllowDomains(c) : getDomains(c);
|
||||
// if domain is not set, use the first domain
|
||||
if (!domain && allowDomains.length > 0) {
|
||||
domain = allowDomains[0];
|
||||
}
|
||||
// check domain, generate random domain
|
||||
const domains = getDomains(c);
|
||||
if (!domain || !domains.includes(domain)) {
|
||||
domain = domains[Math.floor(Math.random() * domains.length)];
|
||||
// check domain is valid
|
||||
if (!domain || !allowDomains.includes(domain)) {
|
||||
throw new Error("Invalid domain")
|
||||
}
|
||||
// create address
|
||||
name = name + "@" + domain;
|
||||
@@ -63,7 +83,7 @@ export const cleanup = async (
|
||||
cleanType: string | undefined | null,
|
||||
cleanDays: number | undefined | null
|
||||
): Promise<boolean> => {
|
||||
if (!cleanType || !cleanDays || cleanDays < 0 || cleanDays > 30) {
|
||||
if (!cleanType || typeof cleanDays !== 'number' || cleanDays < 0 || cleanDays > 30) {
|
||||
throw new Error("Invalid cleanType or cleanDays")
|
||||
}
|
||||
console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
|
||||
@@ -166,3 +186,74 @@ export const handleListQuery = async (
|
||||
).bind(...params).first("count") : 0;
|
||||
return c.json({ results, count });
|
||||
}
|
||||
|
||||
|
||||
export const commonParseMail = async (raw_mail: string | undefined | null): Promise<{
|
||||
sender: string,
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string
|
||||
} | undefined> => {
|
||||
if (!raw_mail) {
|
||||
return undefined;
|
||||
}
|
||||
// TODO: WASM parse email
|
||||
// try {
|
||||
// const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
|
||||
|
||||
// const parsedEmail = parse_message_wrapper(raw_mail);
|
||||
// return {
|
||||
// sender: parsedEmail.sender || "",
|
||||
// subject: parsedEmail.subject || "",
|
||||
// text: parsedEmail.text || "",
|
||||
// html: parsedEmail.body_html || "",
|
||||
// };
|
||||
// } catch (e) {
|
||||
// console.error("Failed use mail-parser-wasm-worker to parse email", e);
|
||||
// }
|
||||
try {
|
||||
const { default: PostalMime } = await import('postal-mime');
|
||||
const parsedEmail = await PostalMime.parse(raw_mail);
|
||||
return {
|
||||
sender: parsedEmail.from ? `${parsedEmail.from.name} <${parsedEmail.from.address}>` : "",
|
||||
subject: parsedEmail.subject || "",
|
||||
text: parsedEmail.text || "",
|
||||
html: parsedEmail.html || "",
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Failed use PostalMime to parse email", e);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const commonGetUserRole = async (
|
||||
c: Context<HonoCustomType>, user_id: number
|
||||
): Promise<UserRole | undefined | null> => {
|
||||
const user_roles = getUserRoles(c);
|
||||
const role_text = await c.env.DB.prepare(
|
||||
`SELECT role_text FROM user_roles where user_id = ?`
|
||||
).bind(user_id).first<string | undefined | null>("role_text");
|
||||
return role_text ? user_roles.find((r) => r.role === role_text) : null;
|
||||
}
|
||||
|
||||
export const getAddressPrefix = async (c: Context<HonoCustomType>): Promise<string | undefined> => {
|
||||
const user = c.get("userPayload");
|
||||
if (!user) {
|
||||
return c.env.PREFIX;
|
||||
}
|
||||
const user_role = await commonGetUserRole(c, user.user_id);
|
||||
if (typeof user_role?.prefix === "string") {
|
||||
return user_role.prefix;
|
||||
}
|
||||
return c.env.PREFIX;
|
||||
}
|
||||
|
||||
export const getAllowDomains = async (c: Context<HonoCustomType>): Promise<string[]> => {
|
||||
const user = c.get("userPayload");
|
||||
if (!user) {
|
||||
return getDefaultDomains(c);
|
||||
}
|
||||
const user_role = await commonGetUserRole(c, user.user_id);
|
||||
return user_role?.domains || getDefaultDomains(c);;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: 'v0.5.0',
|
||||
VERSION: 'v0.6.1',
|
||||
|
||||
// DB settings
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
// api v1 is deprecated
|
||||
const api = new Hono()
|
||||
|
||||
api.get('/admin/v1/mails', async (c) => {
|
||||
const { address, limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT id, source, subject, message FROM mails where address = ? order by id desc limit ? offset ? `
|
||||
).bind(address, limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: mailCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM mails where address = ? `
|
||||
).bind(address).first();
|
||||
count = mailCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
});
|
||||
|
||||
api.get('/admin/v1/mails_unknow', async (c) => {
|
||||
const { limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(`
|
||||
SELECT id, source, subject, message FROM mails
|
||||
where address NOT IN(select name from address)
|
||||
order by id desc limit ? offset ? `
|
||||
).bind(limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: mailCount } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM mails
|
||||
where address NOT IN (select name from address)`
|
||||
).first();
|
||||
count = mailCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
});
|
||||
|
||||
api.get('/api/v1/mails', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
if (!address) {
|
||||
return c.json({ "error": "No address" }, 400)
|
||||
}
|
||||
const { limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT id, source, subject, message, message_id, created_at FROM mails where address = ? order by id desc limit ? offset ?`
|
||||
).bind(address, limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: mailCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM mails where address = ?`
|
||||
).bind(address).first();
|
||||
count = mailCount;
|
||||
}
|
||||
// add attachments
|
||||
let attachmentResults = [];
|
||||
const message_ids = results.map((r) => r.message_id).filter((r) => r);
|
||||
if (message_ids && message_ids.length > 0) {
|
||||
const { results: innerAttachmentResults } = await c.env.DB.prepare(
|
||||
`SELECT id, message_id FROM attachments where message_id in (${message_ids.map((id) => `'${id}'`).join(",")})`
|
||||
).all();
|
||||
attachmentResults = innerAttachmentResults || [];
|
||||
}
|
||||
results.forEach((r) => {
|
||||
const attachment_id = attachmentResults.filter((ar) => ar.message_id == r.message_id).map((ar) => ar.id);
|
||||
if (attachment_id && attachment_id.length > 0) {
|
||||
r.attachment_id = attachment_id[0];
|
||||
}
|
||||
delete r.message_id;
|
||||
})
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
})
|
||||
|
||||
// attachments
|
||||
api.get("/api/v1/attachment/:attachment_id", async (c) => {
|
||||
const { attachment_id } = c.req.param();
|
||||
const { data } = await c.env.DB.prepare(
|
||||
`SELECT data FROM attachments where id = ? `
|
||||
).bind(attachment_id).first();
|
||||
if (!data) {
|
||||
return c.text("Not found", 404)
|
||||
}
|
||||
return c.json(JSON.parse(data))
|
||||
})
|
||||
|
||||
export { api }
|
||||
@@ -2,7 +2,7 @@ import { Hono } from 'hono'
|
||||
|
||||
import { HonoCustomType } from "../types";
|
||||
import { getBooleanValue, getJsonSetting, checkCfTurnstile } from '../utils';
|
||||
import { newAddress, handleListQuery, deleteAddressWithData } from '../common'
|
||||
import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains } from '../common'
|
||||
import { CONSTANTS } from '../constants'
|
||||
import auto_reply from './auto_reply'
|
||||
import webhook_settings from './webhook_settings';
|
||||
@@ -38,9 +38,10 @@ api.delete('/api/mails/:id', async (c) => {
|
||||
}
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { id } = c.req.param();
|
||||
// TODO: add toLowerCase() to handle old data
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE address = ? and id = ? `
|
||||
).bind(address, id).run();
|
||||
).bind(address.toLowerCase(), id).run();
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
@@ -117,7 +118,8 @@ api.post('/api/new_address', async (c) => {
|
||||
console.error(error);
|
||||
}
|
||||
try {
|
||||
const res = await newAddress(c, name, domain, true);
|
||||
const addressPrefix = await getAddressPrefix(c);
|
||||
const res = await newAddress(c, name, domain, true, true, addressPrefix);
|
||||
return c.json(res);
|
||||
} catch (e) {
|
||||
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createMimeMessage } from 'mimetext';
|
||||
import { Resend } from 'resend';
|
||||
|
||||
import { CONSTANTS } from '../constants'
|
||||
import { getJsonSetting, getDomains, getIntValue } from '../utils';
|
||||
import { getJsonSetting, getDomains, getIntValue, getBooleanValue } from '../utils';
|
||||
import { GeoData } from '../models'
|
||||
import { handleListQuery } from '../common'
|
||||
import { HonoCustomType } from '../types';
|
||||
@@ -89,64 +89,6 @@ const sendMailByResend = async (
|
||||
console.log(`Resend success: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
const sendMailByMailChannels = async (
|
||||
c: Context<HonoCustomType>, address: string,
|
||||
reqJson: {
|
||||
from_name: string, to_mail: string, to_name: string,
|
||||
subject: string, content: string, is_html: boolean
|
||||
}
|
||||
): Promise<void> => {
|
||||
/* eslint-disable prefer-const */
|
||||
let {
|
||||
from_name, to_mail, to_name,
|
||||
subject, content, is_html
|
||||
} = reqJson;
|
||||
/* eslint-enable prefer-const */
|
||||
from_name = from_name || address;
|
||||
to_name = to_name || to_mail;
|
||||
let dmikBody = {}
|
||||
if (c.env.DKIM_SELECTOR && c.env.DKIM_PRIVATE_KEY && address.includes("@")) {
|
||||
dmikBody = {
|
||||
"dkim_domain": address.split("@")[1],
|
||||
"dkim_selector": c.env.DKIM_SELECTOR,
|
||||
"dkim_private_key": c.env.DKIM_PRIVATE_KEY,
|
||||
}
|
||||
}
|
||||
const body = {
|
||||
"personalizations": [
|
||||
{
|
||||
"to": [{
|
||||
"email": to_mail,
|
||||
"name": to_name,
|
||||
}],
|
||||
...dmikBody,
|
||||
}
|
||||
],
|
||||
"from": {
|
||||
"email": address,
|
||||
"name": from_name,
|
||||
},
|
||||
"subject": subject,
|
||||
"content": [{
|
||||
"type": is_html ? "text/html" : "text/plain",
|
||||
"value": content,
|
||||
}],
|
||||
};
|
||||
const send_request = new Request("https://api.mailchannels.net/tx/v1/send", {
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
"body": JSON.stringify(body),
|
||||
});
|
||||
const resp = await fetch(send_request);
|
||||
const respText = await resp.text();
|
||||
console.log(resp.status + " " + resp.statusText + ": " + respText);
|
||||
if (resp.status >= 300) {
|
||||
throw new Error(`Mailchannels error: ${resp.status} ${respText}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const sendMail = async (
|
||||
c: Context<HonoCustomType>, address: string,
|
||||
reqJson: {
|
||||
@@ -208,9 +150,8 @@ export const sendMail = async (
|
||||
else if (resendEnabled) {
|
||||
await sendMailByResend(c, address, reqJson);
|
||||
}
|
||||
// send by mailchannels
|
||||
else {
|
||||
await sendMailByMailChannels(c, address, reqJson);
|
||||
throw new Error("Please enable resend or verified address list")
|
||||
}
|
||||
// update balance
|
||||
if (!sendByVerifiedAddressList) {
|
||||
@@ -292,3 +233,17 @@ api.get('/api/sendbox', async (c) => {
|
||||
const { limit, offset } = c.req.query();
|
||||
return getSendbox(c, address, limit, offset);
|
||||
})
|
||||
|
||||
api.delete('/api/sendbox/:id', async (c) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text("User delete email is disabled", 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM sendbox WHERE address = ? and id = ? `
|
||||
).bind(address, id).run();
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { HonoCustomType } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { AdminWebhookSettings, WebhookMail } from "../models";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import PostalMime from 'postal-mime';
|
||||
import { commonParseMail } from "../common";
|
||||
|
||||
|
||||
class WebhookSettings {
|
||||
@@ -15,10 +15,10 @@ class WebhookSettings {
|
||||
body: string = JSON.stringify({
|
||||
"from": "${from}",
|
||||
"to": "${to}",
|
||||
"headers": "${headers}",
|
||||
"subject": "${subject}",
|
||||
"raw": "${raw}",
|
||||
"parsedText": "${parsedText}",
|
||||
"parsedHtml": "${parsedHtml}",
|
||||
}, null, 2)
|
||||
}
|
||||
|
||||
@@ -98,14 +98,14 @@ export async function trigerWebhook(
|
||||
if (!settings) {
|
||||
return;
|
||||
}
|
||||
const parsedEmail = await PostalMime.parse(raw_mail);
|
||||
const parsedEmail = await commonParseMail(raw_mail);
|
||||
const res = await sendWebhook(settings, {
|
||||
from: parsedEmail.from.address || "",
|
||||
from: parsedEmail?.sender || "",
|
||||
to: address,
|
||||
headers: JSON.stringify(parsedEmail.headers),
|
||||
subject: parsedEmail.subject || "",
|
||||
subject: parsedEmail?.subject || "",
|
||||
raw: raw_mail,
|
||||
parsedText: parsedEmail.text || parsedEmail.html || ""
|
||||
parsedText: parsedEmail?.text || "",
|
||||
parsedHtml: parsedEmail?.html || ""
|
||||
});
|
||||
if (!res.success) {
|
||||
console.log(res.message);
|
||||
@@ -119,14 +119,15 @@ async function testWebhookSettings(c: Context<HonoCustomType>): Promise<Response
|
||||
const raw = await c.env.DB.prepare(
|
||||
`SELECT raw FROM raw_mails WHERE address = ? ORDER BY RANDOM() LIMIT 1`
|
||||
).bind(address).first<string>("raw");
|
||||
const parsedEmail = raw ? await PostalMime.parse(raw) : {} as any;
|
||||
|
||||
const parsedEmail = await commonParseMail(raw);
|
||||
const res = await sendWebhook(settings, {
|
||||
from: parsedEmail?.from?.address || "test@test.com",
|
||||
from: parsedEmail?.sender || "test@test.com",
|
||||
to: address,
|
||||
headers: JSON.stringify(parsedEmail?.headers || { "X-Test": "test" }),
|
||||
subject: parsedEmail?.subject || "test subject",
|
||||
raw: raw || "test raw email",
|
||||
parsedText: parsedEmail?.text || parsedEmail?.html || "test parsed text"
|
||||
parsedText: parsedEmail?.text || "test parsed text",
|
||||
parsedHtml: parsedEmail?.html || "test parsed html"
|
||||
});
|
||||
if (!res.success) {
|
||||
return c.text(res.message || "send webhook error", 400);
|
||||
|
||||
@@ -9,10 +9,10 @@ export class AdminWebhookSettings {
|
||||
export type WebhookMail = {
|
||||
from: string;
|
||||
to: string;
|
||||
headers: string;
|
||||
subject: string;
|
||||
raw: string;
|
||||
parsedText: string;
|
||||
parsedHtml: string;
|
||||
}
|
||||
|
||||
export class CleanupSettings {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
|
||||
import { Context } from "hono";
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { Telegraf, Context as TgContext, Markup } from "telegraf";
|
||||
import { callbackQuery } from "telegraf/filters";
|
||||
import PostalMime from 'postal-mime';
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getDomains, getStringValue } from '../utils';
|
||||
import { HonoCustomType } from "../types";
|
||||
import { TelegramSettings } from "./settings";
|
||||
import { bindTelegramAddress, deleteTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress, unbindTelegramByAddress } from "./common";
|
||||
import { commonParseMail } from "../common";
|
||||
|
||||
|
||||
const COMMANDS = [
|
||||
{
|
||||
@@ -18,7 +18,7 @@ const COMMANDS = [
|
||||
},
|
||||
{
|
||||
command: "new",
|
||||
description: "新建邮箱地址, 如果要自定义邮箱地址, 请输入 /new <name>@<domain>, name [a-zA-Z0-9.] 有效"
|
||||
description: "新建邮箱地址, 如果要自定义邮箱地址, 请输入 /new <name>@<domain>, name [a-z0-9] 有效"
|
||||
},
|
||||
{
|
||||
command: "address",
|
||||
@@ -195,13 +195,13 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
if (!db_address_id) {
|
||||
return await ctx.reply("无效地址");
|
||||
}
|
||||
const { raw, id: mailId } = await c.env.DB.prepare(
|
||||
const { raw, id: mailId, created_at } = await c.env.DB.prepare(
|
||||
`SELECT * FROM raw_mails where address = ? `
|
||||
+ ` order by id desc limit 1 offset ?`
|
||||
).bind(
|
||||
queryAddress, mailIndex
|
||||
).first<{ raw: string, id: string }>() || {};
|
||||
const { mail } = raw ? await parseMail(raw) : { mail: "已经没有邮件了" };
|
||||
).first<{ raw: string, id: string, created_at: string }>() || {};
|
||||
const { mail } = raw ? await parseMail(raw, queryAddress, created_at) : { mail: "已经没有邮件了" };
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
const miniAppButtons = []
|
||||
if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) {
|
||||
@@ -265,22 +265,26 @@ export async function initTelegramBotCommands(bot: Telegraf) {
|
||||
await bot.telegram.setMyCommands(COMMANDS);
|
||||
}
|
||||
|
||||
const parseMail = async (raw_mail: string | undefined | null) => {
|
||||
const parseMail = async (
|
||||
raw_mail: string | undefined | null,
|
||||
address: string, created_at: string | undefined | null
|
||||
) => {
|
||||
if (!raw_mail) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const parsedEmail = await PostalMime.parse(raw_mail);
|
||||
if (parsedEmail?.text?.length && parsedEmail?.text?.length > 1000) {
|
||||
parsedEmail.text = parsedEmail.text.substring(0, 1000) + "...消息过长请到miniapp查看";
|
||||
const parsedEmail = await commonParseMail(raw_mail);
|
||||
let parsedText = parsedEmail?.text || "";
|
||||
if (parsedText.length && parsedText.length > 1000) {
|
||||
parsedText = parsedEmail?.text.substring(0, 1000) + "\n\n...\n消息过长请到miniapp查看";
|
||||
}
|
||||
return {
|
||||
isHtml: false,
|
||||
mail: `From: ${parsedEmail.from ? `${parsedEmail.from.name}[${parsedEmail.from.address}]` : "无发件人"}\n`
|
||||
+ `To: ${parsedEmail.to?.map(t => `${t.name}[${t.address}]`).join(" ")}\n`
|
||||
+ `Subject: ${parsedEmail.subject}\n`
|
||||
+ `Date: ${parsedEmail.date}\n`
|
||||
+ `Content:\n${parsedEmail.text || "解析失败,请打开 mini app 查看"}`
|
||||
mail: `From: ${parsedEmail?.sender || "无发件人"}\n`
|
||||
+ `To: ${address}\n`
|
||||
+ (created_at ? `Date: ${created_at}\n` : "")
|
||||
+ `Subject: ${parsedEmail?.subject}\n`
|
||||
+ `Content:\n${parsedText || "解析失败,请打开 mini app 查看"}`
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
@@ -299,7 +303,7 @@ export async function sendMailToTelegram(
|
||||
return;
|
||||
}
|
||||
const userId = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${address}`);
|
||||
const { mail } = await parseMail(raw_mail);
|
||||
const { mail } = await parseMail(raw_mail, address, new Date().toUTCString());
|
||||
if (!mail) {
|
||||
return;
|
||||
}
|
||||
|
||||
17
worker/src/types.d.ts
vendored
17
worker/src/types.d.ts
vendored
@@ -1,3 +1,9 @@
|
||||
export type UserRole = {
|
||||
domains: string[] | undefined | null,
|
||||
role: string,
|
||||
prefix: string | undefined | null
|
||||
}
|
||||
|
||||
export type Bindings = {
|
||||
// bindings
|
||||
DB: D1Database
|
||||
@@ -7,8 +13,15 @@ export type Bindings = {
|
||||
|
||||
// config
|
||||
TITLE: string | undefined
|
||||
ANNOUNCEMENT: string | undefined | null
|
||||
PREFIX: string | undefined
|
||||
MIN_ADDRESS_LEN: string | number | undefined
|
||||
MAX_ADDRESS_LEN: string | number | undefined
|
||||
DEFAULT_DOMAINS: string | string[] | undefined
|
||||
DOMAINS: string | string[] | undefined
|
||||
USER_DEFAULT_ROLE: string | UserRole | undefined
|
||||
USER_ROLES: string | UserRole[] | undefined
|
||||
DOMAIN_LABELS: string | string[] | undefined
|
||||
PASSWORDS: string | string[] | undefined
|
||||
ADMIN_PASSWORDS: string | string[] | undefined
|
||||
JWT_SECRET: string
|
||||
@@ -30,10 +43,6 @@ export type Bindings = {
|
||||
S3_BUCKET: string | undefined
|
||||
S3_URL_EXPIRES: number | undefined
|
||||
|
||||
// dkim
|
||||
DKIM_SELECTOR: string | undefined
|
||||
DKIM_PRIVATE_KEY: string | undefined
|
||||
|
||||
// cf turnstile
|
||||
CF_TURNSTILE_SITE_KEY: string | undefined
|
||||
CF_TURNSTILE_SECRET_KEY: string | undefined
|
||||
|
||||
@@ -2,8 +2,9 @@ import { Context } from "hono";
|
||||
|
||||
import { HonoCustomType } from "../types";
|
||||
import { UserSettings } from "../models";
|
||||
import { getJsonSetting } from "../utils"
|
||||
import { getJsonSetting, getUserRoles } from "../utils"
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { commonGetUserRole } from "../common";
|
||||
|
||||
export default {
|
||||
openSettings: async (c: Context<HonoCustomType>) => {
|
||||
@@ -19,10 +20,14 @@ export default {
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user.user_id).first("id");
|
||||
).bind(user.user_id).first<number | undefined | null>("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400);
|
||||
}
|
||||
return c.json(user);
|
||||
const user_role = await commonGetUserRole(c, db_user_id);
|
||||
return c.json({
|
||||
...user,
|
||||
user_role: user_role
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Context } from 'hono';
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { HonoCustomType } from '../types';
|
||||
import { checkCfTurnstile, getJsonSetting, checkUserPassword } from "../utils"
|
||||
import { checkCfTurnstile, getJsonSetting, checkUserPassword, getUserRoles, getStringValue } from "../utils"
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { GeoData, UserInfo, UserSettings } from "../models";
|
||||
import { sendMail } from "../mails_api/send_mail_api";
|
||||
@@ -124,6 +124,28 @@ export default {
|
||||
if (!success) {
|
||||
return c.text("Failed to register", 500)
|
||||
}
|
||||
const defaultRole = getStringValue(c.env.USER_DEFAULT_ROLE);
|
||||
if (!defaultRole) return c.json({ success: true })
|
||||
const user_roles = getUserRoles(c);
|
||||
if (!user_roles.find((r) => r.role === defaultRole)) {
|
||||
return c.text("Invalid role_text", 400)
|
||||
}
|
||||
// find user_id
|
||||
const user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where user_email = ?`
|
||||
).bind(email).first<number | undefined | null>("id");
|
||||
if (!user_id) {
|
||||
return c.text("User not found", 400)
|
||||
}
|
||||
// update user roles
|
||||
const { success: success2 } = await c.env.DB.prepare(
|
||||
`INSERT INTO user_roles (user_id, role_text)`
|
||||
+ ` VALUES (?, ?)`
|
||||
+ ` ON CONFLICT(user_id) DO NOTHING`
|
||||
).bind(user_id, defaultRole).run();
|
||||
if (!success2) {
|
||||
return c.text("Failed to update user roles", 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
login: async (c: Context<HonoCustomType>) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Context } from "hono";
|
||||
import { createMimeMessage } from "mimetext";
|
||||
import { HonoCustomType } from "./types";
|
||||
import { HonoCustomType, UserRole } from "./types";
|
||||
import { User } from "telegraf/types";
|
||||
|
||||
export const getJsonSetting = async (
|
||||
c: Context<HonoCustomType>, key: string
|
||||
@@ -79,6 +80,30 @@ export const getIntValue = (
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export const getStringArray = (
|
||||
value: string | string[] | undefined | null
|
||||
): string[] => {
|
||||
if (!value) {
|
||||
return [];
|
||||
}
|
||||
// check if value is an array, if not use json.parse
|
||||
if (!Array.isArray(value)) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse value", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export const getDefaultDomains = (c: Context<HonoCustomType>): string[] => {
|
||||
const domains = getStringArray(c.env.DEFAULT_DOMAINS);
|
||||
if (domains && domains.length > 0) return domains;
|
||||
return getDomains(c);
|
||||
}
|
||||
|
||||
export const getDomains = (c: Context<HonoCustomType>): string[] => {
|
||||
if (!c.env.DOMAINS) {
|
||||
return [];
|
||||
@@ -95,6 +120,22 @@ export const getDomains = (c: Context<HonoCustomType>): string[] => {
|
||||
return c.env.DOMAINS;
|
||||
}
|
||||
|
||||
export const getUserRoles = (c: Context<HonoCustomType>): UserRole[] => {
|
||||
if (!c.env.USER_ROLES) {
|
||||
return [];
|
||||
}
|
||||
// check if USER_ROLES is an array, if not use json.parse
|
||||
if (!Array.isArray(c.env.USER_ROLES)) {
|
||||
try {
|
||||
return JSON.parse(c.env.USER_ROLES);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse USER_ROLES", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return c.env.USER_ROLES;
|
||||
}
|
||||
|
||||
export const getPasswords = (c: Context<HonoCustomType>): string[] => {
|
||||
if (!c.env.PASSWORDS) {
|
||||
return [];
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Context, Hono } from 'hono'
|
||||
import { cors } from 'hono/cors';
|
||||
import { jwt } from 'hono/jwt'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
// @ts-ignore
|
||||
import { api as apiV1 } from './deprecated';
|
||||
|
||||
import { api as commonApi } from './commom_api';
|
||||
import { api as mailsApi } from './mails_api'
|
||||
import { api as userApi } from './user_api';
|
||||
@@ -16,11 +13,16 @@ import { api as telegramApi } from './telegram_api'
|
||||
import { email } from './email';
|
||||
import { scheduled } from './scheduled';
|
||||
import { getAdminPasswords, getPasswords, getBooleanValue } from './utils';
|
||||
import { HonoCustomType } from './types';
|
||||
import { HonoCustomType, UserPayload } from './types';
|
||||
|
||||
const app = new Hono<HonoCustomType>()
|
||||
//cors
|
||||
app.use('/*', cors());
|
||||
// error handler
|
||||
app.onError((err, c) => {
|
||||
console.error(err)
|
||||
return c.text(`${err.name} ${err.message}`, 500)
|
||||
})
|
||||
// rate limit
|
||||
app.use('/*', async (c, next) => {
|
||||
if (
|
||||
@@ -53,6 +55,26 @@ app.use('/*', async (c, next) => {
|
||||
}
|
||||
await next()
|
||||
});
|
||||
|
||||
const checkUserPayload = async (
|
||||
c: Context<HonoCustomType>
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const token = c.req.raw.headers.get("x-user-token");
|
||||
if (!token) return;
|
||||
const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
|
||||
// check expired
|
||||
if (!payload.exp) return;
|
||||
// exp is in seconds
|
||||
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
||||
return;
|
||||
}
|
||||
c.set("userPayload", payload as UserPayload);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// api auth
|
||||
app.use('/api/*', async (c, next) => {
|
||||
// check header x-custom-auth
|
||||
@@ -64,6 +86,7 @@ app.use('/api/*', async (c, next) => {
|
||||
}
|
||||
}
|
||||
if (c.req.path.startsWith("/api/new_address")) {
|
||||
await checkUserPayload(c);
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
@@ -90,7 +113,7 @@ app.use('/user_api/*', async (c, next) => {
|
||||
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
||||
return c.text("Token Expired", 401)
|
||||
}
|
||||
c.set("userPayload", payload);
|
||||
c.set("userPayload", payload as UserPayload);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.text("Need User Token", 401)
|
||||
@@ -121,12 +144,17 @@ app.route('/', commonApi)
|
||||
app.route('/', mailsApi)
|
||||
app.route('/', userApi)
|
||||
app.route('/', adminApi)
|
||||
app.route('/', apiV1)
|
||||
app.route('/', apiSendMail)
|
||||
app.route('/', telegramApi)
|
||||
|
||||
app.get('/', async c => c.text("OK"))
|
||||
app.get('/health_check', async c => c.text("OK"))
|
||||
app.get('/', async c => {
|
||||
if (!c.env.DB) { return c.text("DB is not available", 400); }
|
||||
return c.text("OK");
|
||||
})
|
||||
app.get('/health_check', async c => {
|
||||
if (!c.env.DB) { return c.text("DB is not available", 400); }
|
||||
return c.text("OK");
|
||||
})
|
||||
app.all('/*', async c => c.text("Not Found", 404))
|
||||
|
||||
|
||||
|
||||
@@ -17,14 +17,27 @@ node_compat = true
|
||||
|
||||
[vars]
|
||||
# TITLE = "Custom Title" # custom title
|
||||
# ANNOUNCEMENT = "Custom Announcement"
|
||||
PREFIX = "tmp"
|
||||
# (min, max) length of the adderss, if not set, the default is (1, 30)
|
||||
# MIN_ADDRESS_LEN = 1
|
||||
# MAX_ADDRESS_LEN = 30
|
||||
# IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES
|
||||
# PASSWORDS = ["123", "456"]
|
||||
# For admin panel
|
||||
# ADMIN_PASSWORDS = ["123", "456"]
|
||||
# ADMIN CONTACT, CAN BE ANY STRING
|
||||
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
|
||||
DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # domain name for no role users
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # all domain names
|
||||
# For chinese domain name, you can use DOMAIN_LABELS to show chinese domain name
|
||||
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
|
||||
# USER_DEFAULT_ROLE = "vip" # default role for new users(only when enable mail verification)
|
||||
# User roles configuration, if domains is empty will use default_domains, if prefix is null will use default prefix, if prefix is empty string will not use prefix
|
||||
# USER_ROLES = [
|
||||
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
|
||||
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "admin", prefix = "" },
|
||||
# ]
|
||||
JWT_SECRET = "xxx"
|
||||
BLACK_LIST = ""
|
||||
# Allow users to create email addresses
|
||||
@@ -42,9 +55,6 @@ ENABLE_AUTO_REPLY = false
|
||||
# Turnstile verification
|
||||
# CF_TURNSTILE_SITE_KEY = ""
|
||||
# CF_TURNSTILE_SECRET_KEY = ""
|
||||
# dkim config
|
||||
# DKIM_SELECTOR = ""
|
||||
# DKIM_PRIVATE_KEY = ""
|
||||
# telegram bot
|
||||
# TG_MAX_ACCOUNTS = 5
|
||||
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||
|
||||
Reference in New Issue
Block a user