Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1be94a81e0 | ||
|
|
27138dc6a0 | ||
|
|
90c8eff2e5 | ||
|
|
c92d7dee64 | ||
|
|
6854ddb508 | ||
|
|
e524f82674 | ||
|
|
3a35a10523 | ||
|
|
91203d7f66 | ||
|
|
74cf49a5e2 | ||
|
|
99e1bdc30d | ||
|
|
7a5ac89889 | ||
|
|
81ce099299 | ||
|
|
357ec97a5f | ||
|
|
d36bdd7a69 | ||
|
|
267947a82e | ||
|
|
dcfe17dac2 | ||
|
|
b619b11a92 | ||
|
|
f43ec672f3 | ||
|
|
d1e06b40cb | ||
|
|
2604a76ae5 | ||
|
|
00231e7ade | ||
|
|
50f04b2456 | ||
|
|
2ec1a61608 | ||
|
|
eafcf00e5e | ||
|
|
d73fee2c97 | ||
|
|
4bb1016887 | ||
|
|
aea8b964bb | ||
|
|
63cf97f5e2 | ||
|
|
209693673d |
44
.github/workflows/backend_deploy.yaml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Deploy Backend Production
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
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 Backend for ${{ github.ref_name }}
|
||||
run: |
|
||||
cd worker/
|
||||
echo '${{ secrets.BACKEND_TOML }}' > wrangler.toml
|
||||
pnpm install --no-frozen-lockfile
|
||||
output=$(pnpm run deploy 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
code=$?
|
||||
echo "Command failed with exit code $code"
|
||||
exit $code
|
||||
fi
|
||||
echo "Deployed for tag ${{ github.ref_name }}"
|
||||
env:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
50
.github/workflows/docs_deploy.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Deploy Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "vitepress-docs/**"
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- 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 Docs for ${{github.ref_name}}
|
||||
run: |
|
||||
cd vitepress-docs/
|
||||
wget https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/frontend.zip -O docs/public/ui_install/frontend.zip
|
||||
pnpm install --no-frozen-lockfile
|
||||
if [[ ${{github.ref}} == refs/tags/* ]]; then
|
||||
export TAG_NAME=${{github.ref_name}}
|
||||
else
|
||||
export TAG_NAME=$(git describe --tags --abbrev=0)
|
||||
fi
|
||||
echo "Deploying docs for tag $TAG_NAME"
|
||||
pnpm run deploy
|
||||
env:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
43
.github/workflows/frontend_deploy.yaml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Deploy Frontend
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "frontend/**"
|
||||
tags:
|
||||
- "*"
|
||||
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/
|
||||
echo "${{ secrets.FRONTEND_ENV }}" > .env.prod
|
||||
export project_name=${{ secrets.FRONTEND_NAME }}
|
||||
pnpm install --no-frozen-lockfile
|
||||
pnpm run deploy --project-name=$project_name
|
||||
echo "Deploying prodcution for ${{ github.ref_name }}"
|
||||
echo "Deployed for tag ${{ github.ref_name }}"
|
||||
env:
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
13
CHANGELOG.md
@@ -1,6 +1,17 @@
|
||||
# CHANGE LOG
|
||||
|
||||
## 2024-04-12 v0.2.1
|
||||
## v0.2.7
|
||||
|
||||
- Added user interface installation documentation
|
||||
- Support email DKIM
|
||||
- Rate limiting configuration for `/api/new_address`
|
||||
|
||||
## v0.2.6
|
||||
|
||||
- Added admin query outbox page
|
||||
- Add admin data cleaning page
|
||||
|
||||
## 2024-04-12 v0.2.5
|
||||
|
||||
- support send email
|
||||
|
||||
|
||||
79
README.md
@@ -1,31 +1,17 @@
|
||||
# 使用 cloudflare 免费服务,搭建临时邮箱
|
||||
|
||||
## [English](README_EN.md)
|
||||
## [查看部署文档](https://temp-mail-docs.awsl.uk)
|
||||
|
||||
## [新版部署文档](https://temp-mail-docs.awsl.uk)
|
||||
## [English](https://temp-mail-docs.awsl.uk/en/)
|
||||
|
||||
## [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
## [在线演示](https://mail.awsl.uk/)
|
||||
|
||||
[https://mail.awsl.uk](https://mail.awsl.uk/)
|
||||
或者 [https://temp-email.dreamhunter2333.xyz](https://temp-email.dreamhunter2333.xyz/)
|
||||
|
||||
[Backend](https://temp-email-api.awsl.uk/)
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
[Frontend](https://mail.awsl.uk/)
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
| | |
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| [Backend](https://temp-email-api.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/backend_deploy.yaml)       |
|
||||
| [Frontend](https://mail.awsl.uk/) | [](https://github.com/dreamhunter2333/cloudflare_temp_email/actions/workflows/frontend_deploy.yaml)       |
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
||||
@@ -34,6 +20,7 @@
|
||||
</picture>
|
||||
|
||||
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
|
||||
- [查看部署文档](#查看部署文档)
|
||||
- [English](#english)
|
||||
- [CHANGELOG](#changelog)
|
||||
- [在线演示](#在线演示)
|
||||
@@ -43,13 +30,12 @@
|
||||
- [wrangler 的安装](#wrangler-的安装)
|
||||
- [D1 数据库](#d1-数据库)
|
||||
- [Cloudflare workers 后端](#cloudflare-workers-后端)
|
||||
- [Cloudflare Workers 后端](#cloudflare-workers-后端-1)
|
||||
- [Cloudflare Email Routing](#cloudflare-email-routing)
|
||||
- [Cloudflare Pages 前端](#cloudflare-pages-前端)
|
||||
- [配置发送邮件](#配置发送邮件)
|
||||
- [配置 DKIM](#配置-dkim)
|
||||
- [参考资料](#参考资料)
|
||||
|
||||
|
||||
## 功能/TODO
|
||||
|
||||
- [x] Cloudflare D1 作为数据库
|
||||
@@ -64,6 +50,7 @@
|
||||
- [x] 增加查看附件功能
|
||||
- [x] 使用 rust wasm 解析邮件
|
||||
- [x] 支持发送邮件
|
||||
- [x] 支持 DKIM
|
||||
|
||||
---
|
||||
|
||||
@@ -96,6 +83,8 @@ npm install wrangler -g
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dreamhunter2333/cloudflare_temp_email.git
|
||||
# 切换到最新 tag 或者你想部署的分支,你也可以直接使用 main 分支
|
||||
# git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
```
|
||||
|
||||
---
|
||||
@@ -115,7 +104,7 @@ wrangler d1 execute dev --file=db/schema.sql
|
||||
|
||||
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -146,17 +135,24 @@ PREFIX = "tmp" # 要处理的邮箱名称前缀
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名
|
||||
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥
|
||||
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
||||
# dkim config
|
||||
# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels
|
||||
# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "xxx" # D1 数据库名称
|
||||
database_id = "xxx" # D1 数据库 ID
|
||||
|
||||
# 新建地址限流配置
|
||||
# [[unsafe.bindings]]
|
||||
# name = "RATE_LIMITER"
|
||||
# type = "ratelimit"
|
||||
# namespace_id = "1001"
|
||||
# # 10 requests per minute
|
||||
# simple = { limit = 10, period = 60 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cloudflare Workers 后端
|
||||
|
||||
部署
|
||||
|
||||
第一次部署会提示创建项目, `production` 分支请填写 `production`
|
||||
@@ -167,7 +163,7 @@ pnpm run deploy
|
||||
|
||||
部署成功之后再路由中可以看到 `worker` 的 `url`,控制台也会输出 `worker` 的 `url`
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -179,7 +175,7 @@ pnpm run deploy
|
||||
|
||||
配置 `Cloudflare Email Routing` catch-all 发送到 `worker`
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
@@ -203,7 +199,7 @@ pnpm build --emptyOutDir
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
## 配置发送邮件
|
||||
|
||||
@@ -218,6 +214,29 @@ v=spf1 include:_spf.mx.cloudflare.net include:relay.mailchannels.net ~all
|
||||
- 此处 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`
|
||||
|
||||
## 配置 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>`
|
||||
|
||||
## 参考资料
|
||||
|
||||
- https://developers.cloudflare.com/d1/
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"build": "vite build -m prod --emptyOutDir",
|
||||
"build:release": "vite build -m example --emptyOutDir",
|
||||
"preview": "vite preview",
|
||||
"deploy:preview": "npm run build && wrangler pages deploy ./dist --branch preview",
|
||||
"deploy": "npm run build && wrangler pages deploy ./dist --branch production"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -32,6 +33,7 @@
|
||||
"vite-plugin-pwa": "^0.19.7",
|
||||
"vite-plugin-top-level-await": "^1.4.1",
|
||||
"vite-plugin-wasm": "^3.3.0",
|
||||
"workbox-window": "^7.0.0"
|
||||
"workbox-window": "^7.0.0",
|
||||
"wrangler": "^3.50.0"
|
||||
}
|
||||
}
|
||||
|
||||
607
frontend/pnpm-lock.yaml
generated
@@ -67,6 +67,9 @@ devDependencies:
|
||||
workbox-window:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
wrangler:
|
||||
specifier: ^3.50.0
|
||||
version: 3.50.0
|
||||
|
||||
packages:
|
||||
|
||||
@@ -1254,6 +1257,64 @@ packages:
|
||||
'@babel/helper-validator-identifier': 7.22.20
|
||||
to-fast-properties: 2.0.0
|
||||
|
||||
/@cloudflare/kv-asset-handler@0.3.1:
|
||||
resolution: {integrity: sha512-lKN2XCfKCmpKb86a1tl4GIwsJYDy9TGuwjhDELLmpKygQhw8X2xR4dusgpC5Tg7q1pB96Eb0rBo81kxSILQMwA==}
|
||||
dependencies:
|
||||
mime: 3.0.0
|
||||
dev: true
|
||||
|
||||
/@cloudflare/workerd-darwin-64@1.20240405.0:
|
||||
resolution: {integrity: sha512-ut8kwpHmlz9dNSjoov6v1b6jS50J46Mj9QcMA0t1Hne36InaQk/qqPSd12fN5p2GesZ9OOBJvBdDsTblVdyJ1w==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cloudflare/workerd-darwin-arm64@1.20240405.0:
|
||||
resolution: {integrity: sha512-x3A3Ym+J2DH1uYnw0aedeKOTnUebEo312+Aladv7bFri97pjRJcqVbYhMtOHLkHjwYn7bpKSY2eL5iM+0XT29A==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cloudflare/workerd-linux-64@1.20240405.0:
|
||||
resolution: {integrity: sha512-3tYpfjtxEQ0R30Pna7OF3Bz0CTx30hc0QNtH61KnkvXtaeYMkWutSKQKXIuVlPa/7v1MHp+8ViBXMflmS7HquA==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cloudflare/workerd-linux-arm64@1.20240405.0:
|
||||
resolution: {integrity: sha512-NpKZlvmdgcX/m4tP5zM91AfJpZrue2/GRA+Sl3szxAivu2uE5jDVf5SS9dzqzCVfPrdhylqH7yeL4U/cafFNOg==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cloudflare/workerd-windows-64@1.20240405.0:
|
||||
resolution: {integrity: sha512-REBeJMxvUCjwuEVzSSIBtzAyM69QjToab8qBst0S9vdih+9DObym4dw8CevdBQhDbFrHiyL9E6pAZpLPNHVgCw==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@cspotcode/source-map-support@0.8.1:
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
dev: true
|
||||
|
||||
/@css-render/plugin-bem@0.15.12(css-render@0.15.12):
|
||||
resolution: {integrity: sha512-Lq2jSOZn+wYQtsyaFj6QRz2EzAnd3iW5fZeHO1WSXQdVYwvwGX0ZiH3X2JQgtgYLT1yeGtrwrqJdNdMEUD2xTw==}
|
||||
peerDependencies:
|
||||
@@ -1274,6 +1335,24 @@ packages:
|
||||
resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==}
|
||||
dev: false
|
||||
|
||||
/@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.17.19):
|
||||
resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==}
|
||||
peerDependencies:
|
||||
esbuild: '*'
|
||||
dependencies:
|
||||
esbuild: 0.17.19
|
||||
dev: true
|
||||
|
||||
/@esbuild-plugins/node-modules-polyfill@0.2.2(esbuild@0.17.19):
|
||||
resolution: {integrity: sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==}
|
||||
peerDependencies:
|
||||
esbuild: '*'
|
||||
dependencies:
|
||||
esbuild: 0.17.19
|
||||
escape-string-regexp: 4.0.0
|
||||
rollup-plugin-node-polyfills: 0.2.1
|
||||
dev: true
|
||||
|
||||
/@esbuild/aix-ppc64@0.20.2:
|
||||
resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1283,6 +1362,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm64@0.17.19:
|
||||
resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm64@0.20.2:
|
||||
resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1292,6 +1380,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm@0.17.19:
|
||||
resolution: {integrity: sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-arm@0.20.2:
|
||||
resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1301,6 +1398,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-x64@0.17.19:
|
||||
resolution: {integrity: sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/android-x64@0.20.2:
|
||||
resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1310,6 +1416,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-arm64@0.17.19:
|
||||
resolution: {integrity: sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-arm64@0.20.2:
|
||||
resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1319,6 +1434,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-x64@0.17.19:
|
||||
resolution: {integrity: sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/darwin-x64@0.20.2:
|
||||
resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1328,6 +1452,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-arm64@0.17.19:
|
||||
resolution: {integrity: sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-arm64@0.20.2:
|
||||
resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1337,6 +1470,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-x64@0.17.19:
|
||||
resolution: {integrity: sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/freebsd-x64@0.20.2:
|
||||
resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1346,6 +1488,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm64@0.17.19:
|
||||
resolution: {integrity: sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm64@0.20.2:
|
||||
resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1355,6 +1506,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm@0.17.19:
|
||||
resolution: {integrity: sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-arm@0.20.2:
|
||||
resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1364,6 +1524,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ia32@0.17.19:
|
||||
resolution: {integrity: sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ia32@0.20.2:
|
||||
resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1373,6 +1542,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-loong64@0.17.19:
|
||||
resolution: {integrity: sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-loong64@0.20.2:
|
||||
resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1382,6 +1560,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-mips64el@0.17.19:
|
||||
resolution: {integrity: sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-mips64el@0.20.2:
|
||||
resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1391,6 +1578,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ppc64@0.17.19:
|
||||
resolution: {integrity: sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-ppc64@0.20.2:
|
||||
resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1400,6 +1596,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-riscv64@0.17.19:
|
||||
resolution: {integrity: sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-riscv64@0.20.2:
|
||||
resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1409,6 +1614,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-s390x@0.17.19:
|
||||
resolution: {integrity: sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-s390x@0.20.2:
|
||||
resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1418,6 +1632,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-x64@0.17.19:
|
||||
resolution: {integrity: sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/linux-x64@0.20.2:
|
||||
resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1427,6 +1650,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/netbsd-x64@0.17.19:
|
||||
resolution: {integrity: sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/netbsd-x64@0.20.2:
|
||||
resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1436,6 +1668,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-x64@0.17.19:
|
||||
resolution: {integrity: sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/openbsd-x64@0.20.2:
|
||||
resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1445,6 +1686,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/sunos-x64@0.17.19:
|
||||
resolution: {integrity: sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/sunos-x64@0.20.2:
|
||||
resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1454,6 +1704,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-arm64@0.17.19:
|
||||
resolution: {integrity: sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-arm64@0.20.2:
|
||||
resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1463,6 +1722,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-ia32@0.17.19:
|
||||
resolution: {integrity: sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-ia32@0.20.2:
|
||||
resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1472,6 +1740,15 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-x64@0.17.19:
|
||||
resolution: {integrity: sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@esbuild/win32-x64@0.20.2:
|
||||
resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1481,6 +1758,11 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/@fastify/busboy@2.1.1:
|
||||
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||
engines: {node: '>=14'}
|
||||
dev: true
|
||||
|
||||
/@intlify/core-base@9.10.2:
|
||||
resolution: {integrity: sha512-HGStVnKobsJL0DoYIyRCGXBH63DMQqEZxDUGrkNI05FuTcruYUtOAxyL3zoAZu/uDGO6mcUvm3VXBaHG2GdZCg==}
|
||||
engines: {node: '>= 16'}
|
||||
@@ -1538,6 +1820,13 @@ packages:
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: true
|
||||
|
||||
/@jridgewell/trace-mapping@0.3.9:
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: true
|
||||
|
||||
/@juggle/resize-observer@3.4.0:
|
||||
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
|
||||
dev: false
|
||||
@@ -1898,6 +2187,10 @@ packages:
|
||||
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
|
||||
dev: true
|
||||
|
||||
/@types/json-schema@7.0.15:
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
dev: true
|
||||
|
||||
/@types/katex@0.16.7:
|
||||
resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==}
|
||||
dev: false
|
||||
@@ -1912,6 +2205,12 @@ packages:
|
||||
resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==}
|
||||
dev: false
|
||||
|
||||
/@types/node-forge@1.3.11:
|
||||
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
|
||||
dependencies:
|
||||
'@types/node': 20.11.30
|
||||
dev: true
|
||||
|
||||
/@types/node@20.11.30:
|
||||
resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==}
|
||||
dependencies:
|
||||
@@ -2040,6 +2339,11 @@ packages:
|
||||
- '@vue/composition-api'
|
||||
- vue
|
||||
|
||||
/acorn-walk@8.3.2:
|
||||
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
dev: true
|
||||
|
||||
/acorn@8.11.3:
|
||||
resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@@ -2099,6 +2403,12 @@ packages:
|
||||
is-shared-array-buffer: 1.0.3
|
||||
dev: true
|
||||
|
||||
/as-table@1.0.55:
|
||||
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
||||
dependencies:
|
||||
printable-characters: 1.0.42
|
||||
dev: true
|
||||
|
||||
/async-validator@4.2.5:
|
||||
resolution: {integrity: sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==}
|
||||
dev: false
|
||||
@@ -2178,6 +2488,10 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/blake3-wasm@2.1.5:
|
||||
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
|
||||
dev: true
|
||||
|
||||
/brace-expansion@1.1.11:
|
||||
resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
|
||||
dependencies:
|
||||
@@ -2233,6 +2547,15 @@ packages:
|
||||
resolution: {integrity: sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==}
|
||||
dev: true
|
||||
|
||||
/capnp-ts@0.7.0:
|
||||
resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==}
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
tslib: 2.6.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/chalk@2.4.2:
|
||||
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -2301,6 +2624,11 @@ packages:
|
||||
delayed-stream: 1.0.0
|
||||
dev: false
|
||||
|
||||
/commander@12.0.0:
|
||||
resolution: {integrity: sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==}
|
||||
engines: {node: '>=18'}
|
||||
dev: true
|
||||
|
||||
/commander@2.20.3:
|
||||
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
|
||||
dev: true
|
||||
@@ -2318,6 +2646,11 @@ packages:
|
||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||
dev: true
|
||||
|
||||
/cookie@0.5.0:
|
||||
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: true
|
||||
|
||||
/core-js-compat@3.36.1:
|
||||
resolution: {integrity: sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==}
|
||||
dependencies:
|
||||
@@ -2343,6 +2676,10 @@ packages:
|
||||
/csstype@3.1.3:
|
||||
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
|
||||
|
||||
/data-uri-to-buffer@2.0.2:
|
||||
resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
|
||||
dev: true
|
||||
|
||||
/data-view-buffer@1.0.1:
|
||||
resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2534,6 +2871,36 @@ packages:
|
||||
is-symbol: 1.0.4
|
||||
dev: true
|
||||
|
||||
/esbuild@0.17.19:
|
||||
resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@esbuild/android-arm': 0.17.19
|
||||
'@esbuild/android-arm64': 0.17.19
|
||||
'@esbuild/android-x64': 0.17.19
|
||||
'@esbuild/darwin-arm64': 0.17.19
|
||||
'@esbuild/darwin-x64': 0.17.19
|
||||
'@esbuild/freebsd-arm64': 0.17.19
|
||||
'@esbuild/freebsd-x64': 0.17.19
|
||||
'@esbuild/linux-arm': 0.17.19
|
||||
'@esbuild/linux-arm64': 0.17.19
|
||||
'@esbuild/linux-ia32': 0.17.19
|
||||
'@esbuild/linux-loong64': 0.17.19
|
||||
'@esbuild/linux-mips64el': 0.17.19
|
||||
'@esbuild/linux-ppc64': 0.17.19
|
||||
'@esbuild/linux-riscv64': 0.17.19
|
||||
'@esbuild/linux-s390x': 0.17.19
|
||||
'@esbuild/linux-x64': 0.17.19
|
||||
'@esbuild/netbsd-x64': 0.17.19
|
||||
'@esbuild/openbsd-x64': 0.17.19
|
||||
'@esbuild/sunos-x64': 0.17.19
|
||||
'@esbuild/win32-arm64': 0.17.19
|
||||
'@esbuild/win32-ia32': 0.17.19
|
||||
'@esbuild/win32-x64': 0.17.19
|
||||
dev: true
|
||||
|
||||
/esbuild@0.20.2:
|
||||
resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2575,11 +2942,20 @@ packages:
|
||||
engines: {node: '>=0.8.0'}
|
||||
dev: true
|
||||
|
||||
/escape-string-regexp@4.0.0:
|
||||
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/escape-string-regexp@5.0.0:
|
||||
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
||||
engines: {node: '>=12'}
|
||||
dev: true
|
||||
|
||||
/estree-walker@0.6.1:
|
||||
resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==}
|
||||
dev: true
|
||||
|
||||
/estree-walker@1.0.1:
|
||||
resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==}
|
||||
dev: true
|
||||
@@ -2602,6 +2978,11 @@ packages:
|
||||
resolution: {integrity: sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==}
|
||||
dev: false
|
||||
|
||||
/exit-hook@2.2.1:
|
||||
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
|
||||
engines: {node: '>=6'}
|
||||
dev: true
|
||||
|
||||
/fast-deep-equal@3.1.3:
|
||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||
dev: true
|
||||
@@ -2725,6 +3106,13 @@ packages:
|
||||
resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==}
|
||||
dev: true
|
||||
|
||||
/get-source@2.0.12:
|
||||
resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==}
|
||||
dependencies:
|
||||
data-uri-to-buffer: 2.0.2
|
||||
source-map: 0.6.1
|
||||
dev: true
|
||||
|
||||
/get-symbol-description@1.0.2:
|
||||
resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2741,6 +3129,10 @@ packages:
|
||||
is-glob: 4.0.3
|
||||
dev: true
|
||||
|
||||
/glob-to-regexp@0.4.1:
|
||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||
dev: true
|
||||
|
||||
/glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
dependencies:
|
||||
@@ -2752,6 +3144,17 @@ packages:
|
||||
path-is-absolute: 1.0.1
|
||||
dev: true
|
||||
|
||||
/glob@8.1.0:
|
||||
resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
inflight: 1.0.6
|
||||
inherits: 2.0.4
|
||||
minimatch: 5.1.6
|
||||
once: 1.4.0
|
||||
dev: true
|
||||
|
||||
/globals@11.12.0:
|
||||
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -3151,6 +3554,35 @@ packages:
|
||||
mime-db: 1.52.0
|
||||
dev: false
|
||||
|
||||
/mime@3.0.0:
|
||||
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/miniflare@3.20240405.1:
|
||||
resolution: {integrity: sha512-oShOR/ckr9JTO1bkPQH0nXvuSgJjoE+E5+M1tvP01Q8Z+Q0GJnzU2+FDYUH8yIK/atHv7snU8yy0X6KWVn1YdQ==}
|
||||
engines: {node: '>=16.13'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
acorn: 8.11.3
|
||||
acorn-walk: 8.3.2
|
||||
capnp-ts: 0.7.0
|
||||
exit-hook: 2.2.1
|
||||
glob-to-regexp: 0.4.1
|
||||
stoppable: 1.1.0
|
||||
undici: 5.28.4
|
||||
workerd: 1.20240405.0
|
||||
ws: 8.16.0
|
||||
youch: 3.3.3
|
||||
zod: 3.22.4
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
dev: true
|
||||
|
||||
/minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
dependencies:
|
||||
@@ -3184,6 +3616,11 @@ packages:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
dev: true
|
||||
|
||||
/mustache@4.2.0:
|
||||
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/naive-ui@2.38.1(vue@3.4.21):
|
||||
resolution: {integrity: sha512-AnU1FQ7K/CbhguAX++V4kCFjk7h7RvWt4nvZPRjORMpq+fUIlzD+EcQ5Cv1VqDloNF8+eMv4Akc2Ogacc9S+5A==}
|
||||
peerDependencies:
|
||||
@@ -3216,6 +3653,11 @@ packages:
|
||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||
hasBin: true
|
||||
|
||||
/node-forge@1.3.1:
|
||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||
engines: {node: '>= 6.13.0'}
|
||||
dev: true
|
||||
|
||||
/node-releases@2.0.14:
|
||||
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
|
||||
dev: true
|
||||
@@ -3259,6 +3701,10 @@ packages:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
dev: true
|
||||
|
||||
/path-to-regexp@6.2.2:
|
||||
resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==}
|
||||
dev: true
|
||||
|
||||
/pathe@1.1.2:
|
||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||
dev: true
|
||||
@@ -3306,6 +3752,10 @@ packages:
|
||||
engines: {node: ^14.13.1 || >=16.0.0}
|
||||
dev: true
|
||||
|
||||
/printable-characters@1.0.42:
|
||||
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
|
||||
dev: true
|
||||
|
||||
/proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
dev: false
|
||||
@@ -3386,6 +3836,11 @@ packages:
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/resolve.exports@2.0.2:
|
||||
resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/resolve@1.22.8:
|
||||
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
|
||||
hasBin: true
|
||||
@@ -3400,6 +3855,21 @@ packages:
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/rollup-plugin-inject@3.0.2:
|
||||
resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==}
|
||||
deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.
|
||||
dependencies:
|
||||
estree-walker: 0.6.1
|
||||
magic-string: 0.25.9
|
||||
rollup-pluginutils: 2.8.2
|
||||
dev: true
|
||||
|
||||
/rollup-plugin-node-polyfills@0.2.1:
|
||||
resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==}
|
||||
dependencies:
|
||||
rollup-plugin-inject: 3.0.2
|
||||
dev: true
|
||||
|
||||
/rollup-plugin-terser@7.0.2(rollup@2.79.1):
|
||||
resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==}
|
||||
deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-terser
|
||||
@@ -3413,6 +3883,12 @@ packages:
|
||||
terser: 5.30.0
|
||||
dev: true
|
||||
|
||||
/rollup-pluginutils@2.8.2:
|
||||
resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==}
|
||||
dependencies:
|
||||
estree-walker: 0.6.1
|
||||
dev: true
|
||||
|
||||
/rollup@2.79.1:
|
||||
resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
@@ -3474,6 +3950,11 @@ packages:
|
||||
is-regex: 1.1.4
|
||||
dev: true
|
||||
|
||||
/safe-stable-stringify@2.4.3:
|
||||
resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/scule@1.3.0:
|
||||
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
||||
dev: true
|
||||
@@ -3486,6 +3967,14 @@ packages:
|
||||
resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
|
||||
dev: false
|
||||
|
||||
/selfsigned@2.4.1:
|
||||
resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
'@types/node-forge': 1.3.11
|
||||
node-forge: 1.3.1
|
||||
dev: true
|
||||
|
||||
/semver@6.3.1:
|
||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
@@ -3557,6 +4046,18 @@ packages:
|
||||
deprecated: Please use @jridgewell/sourcemap-codec instead
|
||||
dev: true
|
||||
|
||||
/stacktracey@2.1.8:
|
||||
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
|
||||
dependencies:
|
||||
as-table: 1.0.55
|
||||
get-source: 2.0.12
|
||||
dev: true
|
||||
|
||||
/stoppable@1.1.0:
|
||||
resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==}
|
||||
engines: {node: '>=4', npm: '>=6'}
|
||||
dev: true
|
||||
|
||||
/string.prototype.matchall@4.0.11:
|
||||
resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -3692,6 +4193,24 @@ packages:
|
||||
resolution: {integrity: sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==}
|
||||
dev: false
|
||||
|
||||
/ts-json-schema-generator@1.5.1:
|
||||
resolution: {integrity: sha512-apX5qG2+NA66j7b4AJm8q/DpdTeOsjfh7A3LpKsUiil0FepkNwtN28zYgjrsiiya2/OPhsr/PSjX5FUYg79rCg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@types/json-schema': 7.0.15
|
||||
commander: 12.0.0
|
||||
glob: 8.1.0
|
||||
json5: 2.2.3
|
||||
normalize-path: 3.0.0
|
||||
safe-stable-stringify: 2.4.3
|
||||
typescript: 5.4.5
|
||||
dev: true
|
||||
|
||||
/tslib@2.6.2:
|
||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||
dev: true
|
||||
|
||||
/type-fest@0.16.0:
|
||||
resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3741,6 +4260,12 @@ packages:
|
||||
possible-typed-array-names: 1.0.0
|
||||
dev: true
|
||||
|
||||
/typescript@5.4.5:
|
||||
resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ufo@1.5.3:
|
||||
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
|
||||
dev: true
|
||||
@@ -3758,6 +4283,13 @@ packages:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
dev: true
|
||||
|
||||
/undici@5.28.4:
|
||||
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
|
||||
engines: {node: '>=14.0'}
|
||||
dependencies:
|
||||
'@fastify/busboy': 2.1.1
|
||||
dev: true
|
||||
|
||||
/unicode-canonical-property-names-ecmascript@2.0.0:
|
||||
resolution: {integrity: sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -4260,10 +4792,85 @@ packages:
|
||||
workbox-core: 7.0.0
|
||||
dev: true
|
||||
|
||||
/workerd@1.20240405.0:
|
||||
resolution: {integrity: sha512-AWrOSBh4Ll7sBWHuh0aywm8hDkKqsZmcwnDB0PVGszWZM5mndNBI5iJ/8haXVpdoyqkJQEVdhET9JDi4yU8tRg==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
optionalDependencies:
|
||||
'@cloudflare/workerd-darwin-64': 1.20240405.0
|
||||
'@cloudflare/workerd-darwin-arm64': 1.20240405.0
|
||||
'@cloudflare/workerd-linux-64': 1.20240405.0
|
||||
'@cloudflare/workerd-linux-arm64': 1.20240405.0
|
||||
'@cloudflare/workerd-windows-64': 1.20240405.0
|
||||
dev: true
|
||||
|
||||
/wrangler@3.50.0:
|
||||
resolution: {integrity: sha512-JlLuch+6DtaC5HGp8YD9Au++XvMv34g3ySdlB5SyPbaObELi8P9ZID5vgyf9AA75djzxL7cuNOk1YdKCJEuq0w==}
|
||||
engines: {node: '>=16.17.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@cloudflare/workers-types': ^4.20240405.0
|
||||
peerDependenciesMeta:
|
||||
'@cloudflare/workers-types':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@cloudflare/kv-asset-handler': 0.3.1
|
||||
'@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19)
|
||||
'@esbuild-plugins/node-modules-polyfill': 0.2.2(esbuild@0.17.19)
|
||||
blake3-wasm: 2.1.5
|
||||
chokidar: 3.6.0
|
||||
esbuild: 0.17.19
|
||||
miniflare: 3.20240405.1
|
||||
nanoid: 3.3.7
|
||||
path-to-regexp: 6.2.2
|
||||
resolve: 1.22.8
|
||||
resolve.exports: 2.0.2
|
||||
selfsigned: 2.4.1
|
||||
source-map: 0.6.1
|
||||
ts-json-schema-generator: 1.5.1
|
||||
xxhash-wasm: 1.0.2
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
dev: true
|
||||
|
||||
/wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
dev: true
|
||||
|
||||
/ws@8.16.0:
|
||||
resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
peerDependencies:
|
||||
bufferutil: ^4.0.1
|
||||
utf-8-validate: '>=5.0.2'
|
||||
peerDependenciesMeta:
|
||||
bufferutil:
|
||||
optional: true
|
||||
utf-8-validate:
|
||||
optional: true
|
||||
dev: true
|
||||
|
||||
/xxhash-wasm@1.0.2:
|
||||
resolution: {integrity: sha512-ibF0Or+FivM9lNrg+HGJfVX8WJqgo+kCLDc4vx6xMeTce7Aj+DLttKbxxRR/gNLSAelRc1omAPlJ77N/Jem07A==}
|
||||
dev: true
|
||||
|
||||
/yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
dev: true
|
||||
|
||||
/youch@3.3.3:
|
||||
resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==}
|
||||
dependencies:
|
||||
cookie: 0.5.0
|
||||
mustache: 4.2.0
|
||||
stacktracey: 2.1.8
|
||||
dev: true
|
||||
|
||||
/zod@3.22.4:
|
||||
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
|
||||
dev: true
|
||||
|
||||
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 5.7 KiB |
@@ -30,9 +30,12 @@ export const useGlobalState = createGlobalState(
|
||||
const auth = useStorage('auth', '');
|
||||
const adminAuth = useStorage('adminAuth', '');
|
||||
const jwt = useStorage('jwt', '');
|
||||
const localeCache = useStorage('locale', 'zhCN');
|
||||
const localeCache = useStorage('locale', 'zh');
|
||||
const themeSwitch = useStorage('themeSwitch', false);
|
||||
const showLogin = ref(false);
|
||||
const adminTab = ref("account");
|
||||
const adminMailTabAddress = ref("");
|
||||
const adminSendBoxTabAddress = ref("");
|
||||
return {
|
||||
loading,
|
||||
settings,
|
||||
@@ -45,6 +48,9 @@ export const useGlobalState = createGlobalState(
|
||||
adminAuth,
|
||||
showAdminAuth,
|
||||
showLogin,
|
||||
adminTab,
|
||||
adminMailTabAddress,
|
||||
adminSendBoxTabAddress,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { User, UserCheck, MailBulk } from '@vicons/fa'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
import { processItem } from '../utils/email-parser'
|
||||
|
||||
import SenderAccess from './admin/SenderAccess.vue'
|
||||
import Statistics from "./admin/Statistics.vue"
|
||||
import SendBox from './admin/SendBox.vue';
|
||||
import Account from './admin/Account.vue';
|
||||
import Mails from './admin/Mails.vue';
|
||||
import MailsUnknow from './admin/MailsUnknow.vue';
|
||||
import Maintenance from './admin/Maintenance.vue';
|
||||
|
||||
const { localeCache, adminAuth, showAdminAuth } = useGlobalState()
|
||||
const router = useRouter()
|
||||
const {
|
||||
localeCache, adminAuth, showAdminAuth, adminTab
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const showEmailPassword = ref(false)
|
||||
const curEmailPassword = ref("")
|
||||
const addressQuery = ref("")
|
||||
|
||||
const authFunc = async () => {
|
||||
try {
|
||||
location.reload()
|
||||
@@ -29,243 +29,34 @@ const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
title: 'Temp Email Admin',
|
||||
auth: 'Admin Auth',
|
||||
home: 'Home',
|
||||
authTip: 'Please enter the correct auth code',
|
||||
name: 'Name',
|
||||
created_at: 'Created At',
|
||||
showPass: 'Show Passwrod',
|
||||
password: 'Password',
|
||||
passwordTip: 'Please copy the password and you can use it to login to your email account.',
|
||||
delete: 'Delete',
|
||||
deleteTip: 'Are you sure to delete this email?',
|
||||
refresh: 'Refresh',
|
||||
mails: 'Emails',
|
||||
itemCount: 'itemCount',
|
||||
query: 'Query',
|
||||
userCount: 'User Count',
|
||||
activeUser: '7 days Active User',
|
||||
mailCount: 'Mail Count',
|
||||
account: 'Account',
|
||||
unknow: 'Unknow',
|
||||
addressQueryTip: 'Leave blank to query all addresses',
|
||||
unknow: 'Mails with unknow receiver',
|
||||
senderAccess: 'Sender Access Control',
|
||||
sendBox: 'Send Box',
|
||||
maintenance: 'Maintenance',
|
||||
},
|
||||
zh: {
|
||||
title: '临时邮件 Admin',
|
||||
auth: 'Admin 授权',
|
||||
home: '首页',
|
||||
authTip: '请输入正确的授权码',
|
||||
name: '名称',
|
||||
created_at: '创建时间',
|
||||
showPass: '显示密码',
|
||||
password: '密码',
|
||||
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
||||
delete: '删除',
|
||||
deleteTip: '确定要删除这个邮箱吗?',
|
||||
refresh: '刷新',
|
||||
mails: '邮件',
|
||||
itemCount: '总数',
|
||||
query: '查询',
|
||||
userCount: '用户总数',
|
||||
activeUser: '周活跃用户',
|
||||
mailCount: '邮件总数',
|
||||
account: '账号',
|
||||
unknow: '未知',
|
||||
addressQueryTip: '留空查询所有地址',
|
||||
unknow: '无收件人邮件',
|
||||
senderAccess: '发件权限控制',
|
||||
sendBox: '发件箱',
|
||||
maintenance: '维护',
|
||||
}
|
||||
}
|
||||
});
|
||||
const data = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const showPassword = async (id) => {
|
||||
try {
|
||||
curEmailPassword.value = await api.adminShowPassword(id)
|
||||
showEmailPassword.value = true
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
showEmailPassword.value = false
|
||||
curEmailPassword.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const deleteEmail = async (id) => {
|
||||
try {
|
||||
await api.adminDeleteAddress(id)
|
||||
message.success("success");
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const { results, count: addressCount } = await api.fetch(
|
||||
`/admin/address`
|
||||
+ `?limit=${pageSize.value}`
|
||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||
+ (addressQuery.value ? `&query=${addressQuery.value}` : "")
|
||||
);
|
||||
data.value = results;
|
||||
if (addressCount > 0) {
|
||||
count.value = addressCount;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "ID",
|
||||
key: "id"
|
||||
},
|
||||
{
|
||||
title: t('name'),
|
||||
key: "name"
|
||||
},
|
||||
{
|
||||
title: t('created_at'),
|
||||
key: "created_at"
|
||||
},
|
||||
{
|
||||
title: 'Action',
|
||||
key: 'actions',
|
||||
render(row) {
|
||||
return h('div', [
|
||||
h(NButton,
|
||||
{
|
||||
type: 'success',
|
||||
ghost: true,
|
||||
onClick: () => showPassword(row.id)
|
||||
},
|
||||
{ default: () => t('showPass') }
|
||||
),
|
||||
h(NButton,
|
||||
{
|
||||
type: 'success',
|
||||
ghost: true,
|
||||
onClick: () => {
|
||||
mailAddress.value = row.name
|
||||
tab.value = "mails"
|
||||
}
|
||||
},
|
||||
{ default: () => t('mails') }
|
||||
),
|
||||
h(NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => deleteEmail(row.id)
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton, { type: "error" }, () => t('delete')),
|
||||
default: () => t('deleteTip')
|
||||
}
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
watch([page, pageSize], async () => {
|
||||
await fetchData()
|
||||
})
|
||||
|
||||
const statistics = ref({
|
||||
userCount: 0,
|
||||
mailCount: 0,
|
||||
activeUserCount7days: 0,
|
||||
})
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
try {
|
||||
const { userCount, activeUserCount7days, mailCount } = await api.fetch(`/admin/statistics`);
|
||||
statistics.value.mailCount = mailCount || 0;
|
||||
statistics.value.userCount = userCount || 0;
|
||||
statistics.value.activeUserCount7days = activeUserCount7days || 0;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true
|
||||
} else {
|
||||
await fetchData()
|
||||
await fetchStatistics()
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
})
|
||||
|
||||
const tab = ref("account")
|
||||
const mailAddress = ref("")
|
||||
const mailData = ref([])
|
||||
const mailCount = ref(0)
|
||||
const mailPage = ref(1)
|
||||
const mailPageSize = ref(20)
|
||||
|
||||
watch([mailPage, mailPageSize, mailAddress], async () => {
|
||||
await fetchMailData()
|
||||
})
|
||||
|
||||
const fetchMailData = async () => {
|
||||
if (!mailAddress.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { results, count } = await api.fetch(
|
||||
`/admin/mails`
|
||||
+ `?address=${mailAddress.value}`
|
||||
+ `&limit=${mailPageSize.value}`
|
||||
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
||||
);
|
||||
mailData.value = await Promise.all(results.map(async (item) => {
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (count > 0) {
|
||||
mailCount.value = count;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const mailUnknowData = ref([])
|
||||
const mailUnknowCount = ref(0)
|
||||
const mailUnknowPage = ref(1)
|
||||
const mailUnknowPageSize = ref(20)
|
||||
|
||||
watch([mailUnknowPage, mailUnknowPageSize], async () => {
|
||||
await fetchMailUnknowData()
|
||||
})
|
||||
|
||||
const fetchMailUnknowData = async () => {
|
||||
try {
|
||||
const { results, count } = await api.fetch(
|
||||
`/admin/mails_unknow`
|
||||
+ `?limit=${mailPageSize.value}`
|
||||
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
||||
);
|
||||
mailUnknowData.value = await Promise.all(results.map(async (item) => {
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (count > 0) {
|
||||
mailUnknowCount.value = count;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -283,125 +74,26 @@ const fetchMailUnknowData = async () => {
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showEmailPassword" preset="dialog" title="Dialog">
|
||||
<template #header>
|
||||
<div>{{ t("password") }}</div>
|
||||
</template>
|
||||
<span>
|
||||
<p>{{ t("passwordTip") }}</p>
|
||||
</span>
|
||||
<n-card>
|
||||
<b>{{ curEmailPassword }}</b>
|
||||
</n-card>
|
||||
<template #action>
|
||||
</template>
|
||||
</n-modal>
|
||||
<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('activeUser')" :value="statistics.activeUserCount7days">
|
||||
<template #prefix>
|
||||
<n-icon :component="UserCheck" />
|
||||
</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-row>
|
||||
<n-tabs type="segment" v-model:value="tab">
|
||||
<Statistics />
|
||||
<n-tabs type="card" v-model:value="adminTab">
|
||||
<n-tab-pane name="account" :tab="t('account')">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')" />
|
||||
<n-button @click="fetchData" type="primary" ghost>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" :page-sizes="[20, 50, 100]"
|
||||
show-size-picker>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
<Account />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="mails" :tab="t('mails')">
|
||||
<n-input-group>
|
||||
<n-input v-model:value="mailAddress" />
|
||||
<n-button @click="fetchMailData" type="primary" ghost>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<n-list hoverable clickable>
|
||||
<div style="display: inline-block; margin-bottom: 10px;">
|
||||
<n-pagination v-model:page="mailPage" v-model:page-size="mailPageSize" :item-count="mailCount" simple>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-list-item v-for="row in mailData" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.subject">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</template>
|
||||
<div v-html="row.message"></div>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
<Mails />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="unknow" :tab="t('unknow')">
|
||||
<n-button @click="fetchMailUnknowData" type="primary" ghost>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
<n-list hoverable clickable>
|
||||
<div style="display: inline-block; margin-bottom: 10px;">
|
||||
<n-pagination v-model:page="mailUnknowPage" v-model:page-size="mailUnknowPageSize"
|
||||
:item-count="mailUnknowCount" simple>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-list-item v-for="row in mailUnknowData" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.subject">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</template>
|
||||
<div v-html="row.message"></div>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
<MailsUnknow />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="senderAccess" :tab="t('senderAccess')">
|
||||
<SenderAccess />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="sendBox" :tab="t('sendBox')">
|
||||
<SendBox />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
||||
<Maintenance />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -184,7 +184,7 @@ const menuOptions = computed(() => [
|
||||
label: () => h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
size: "small",
|
||||
onClick: () => router.push('/sendbox')
|
||||
@@ -197,7 +197,7 @@ const menuOptions = computed(() => [
|
||||
label: () => h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
size: "small",
|
||||
onClick: () => { showPassword.value = true }
|
||||
@@ -210,7 +210,7 @@ const menuOptions = computed(() => [
|
||||
label: () => h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
size: "small",
|
||||
onClick: () => { router.push('/settings') }
|
||||
@@ -223,7 +223,7 @@ const menuOptions = computed(() => [
|
||||
label: () => h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
size: "small",
|
||||
onClick: () => { showLogout.value = true }
|
||||
@@ -236,7 +236,7 @@ const menuOptions = computed(() => [
|
||||
label: () => h(
|
||||
NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
size: "small",
|
||||
onClick: () => { showDelteAccount.value = true }
|
||||
|
||||
@@ -32,14 +32,18 @@ const { t } = useI18n({
|
||||
refresh: 'Refresh',
|
||||
attachments: 'Show Attachments',
|
||||
downloadMail: 'Download Mail',
|
||||
pleaseSelectMail: "Please select a mail to view."
|
||||
pleaseSelectMail: "Please select a mail to view.",
|
||||
delete: 'Delete',
|
||||
deleteMailTip: 'Are you sure you want to delete this mail?'
|
||||
},
|
||||
zh: {
|
||||
autoRefresh: '自动刷新',
|
||||
refresh: '刷新',
|
||||
downloadMail: '下载邮件',
|
||||
attachments: '查看附件',
|
||||
pleaseSelectMail: "请选择一封邮件查看。"
|
||||
pleaseSelectMail: "请选择一封邮件查看。",
|
||||
delete: '删除',
|
||||
deleteMailTip: '确定要删除这封邮件吗?'
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -100,6 +104,19 @@ const mailItemClass = (row) => {
|
||||
return curMail.value && row.id == curMail.value.id ? (themeSwitch.value ? 'overlay overlay-dark-backgroud' : 'overlay overlay-light-backgroud') : '';
|
||||
};
|
||||
|
||||
const deleteMail = async () => {
|
||||
try {
|
||||
await api.fetch(`/api/mails/${curMail.value.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
message.success(t("success"));
|
||||
curMail.value = null;
|
||||
await refresh();
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await refresh();
|
||||
});
|
||||
@@ -158,6 +175,12 @@ onMounted(async () => {
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
</n-tag>
|
||||
<n-popconfirm @positive-click="deleteMail">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||
</template>
|
||||
{{ t('deleteMailTip') }}
|
||||
</n-popconfirm>
|
||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachments)">
|
||||
{{ t('attachments') }}
|
||||
@@ -224,6 +247,12 @@ onMounted(async () => {
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
</n-tag>
|
||||
<n-popconfirm @positive-click="deleteMail">
|
||||
<template #trigger>
|
||||
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||
</template>
|
||||
{{ t('deleteMailTip') }}
|
||||
</n-popconfirm>
|
||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachments)">
|
||||
{{ t('attachments') }}
|
||||
|
||||
249
frontend/src/views/admin/Account.vue
Normal file
@@ -0,0 +1,249 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { NMenu } from 'naive-ui';
|
||||
import { MenuFilled } from '@vicons/material'
|
||||
|
||||
const {
|
||||
localeCache, adminAuth, showAdminAuth,
|
||||
adminTab, adminMailTabAddress, adminSendBoxTabAddress
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
name: 'Name',
|
||||
created_at: 'Created At',
|
||||
showPass: 'Show Passwrod',
|
||||
password: 'Password',
|
||||
passwordTip: 'Please copy the password and you can use it to login to your email account.',
|
||||
delete: 'Delete',
|
||||
deleteTip: 'Are you sure to delete this email?',
|
||||
delteAccount: 'Delete Account',
|
||||
viewMails: 'View Mails',
|
||||
viewSendBox: 'View SendBox',
|
||||
itemCount: 'itemCount',
|
||||
query: 'Query',
|
||||
addressQueryTip: 'Leave blank to query all addresses',
|
||||
actions: 'Actions'
|
||||
},
|
||||
zh: {
|
||||
name: '名称',
|
||||
created_at: '创建时间',
|
||||
showPass: '显示密码',
|
||||
password: '密码',
|
||||
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
||||
delete: '删除',
|
||||
deleteTip: '确定要删除这个邮箱吗?',
|
||||
delteAccount: '删除邮箱',
|
||||
viewMails: '查看邮件',
|
||||
viewSendBox: '查看发件箱',
|
||||
itemCount: '总数',
|
||||
query: '查询',
|
||||
addressQueryTip: '留空查询所有地址',
|
||||
actions: '操作',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const showEmailPassword = ref(false)
|
||||
const curEmailPassword = ref("")
|
||||
const curDeleteAddressId = ref(0);
|
||||
|
||||
const addressQuery = ref("")
|
||||
|
||||
const data = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const showDelteAccount = ref(false)
|
||||
|
||||
const showPassword = async (id) => {
|
||||
try {
|
||||
curEmailPassword.value = await api.adminShowPassword(id)
|
||||
showEmailPassword.value = true
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
showEmailPassword.value = false
|
||||
curEmailPassword.value = ""
|
||||
}
|
||||
}
|
||||
|
||||
const deleteEmail = async () => {
|
||||
try {
|
||||
await api.adminDeleteAddress(curDeleteAddressId.value)
|
||||
message.success("success");
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
showDelteAccount.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const { results, count: addressCount } = await api.fetch(
|
||||
`/admin/address`
|
||||
+ `?limit=${pageSize.value}`
|
||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||
+ (addressQuery.value ? `&query=${addressQuery.value}` : "")
|
||||
);
|
||||
data.value = results;
|
||||
if (addressCount > 0) {
|
||||
count.value = addressCount;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "ID",
|
||||
key: "id"
|
||||
},
|
||||
{
|
||||
title: t('name'),
|
||||
key: "name"
|
||||
},
|
||||
{
|
||||
title: t('created_at'),
|
||||
key: "created_at"
|
||||
},
|
||||
{
|
||||
title: t('actions'),
|
||||
key: 'actions',
|
||||
render(row) {
|
||||
return h('div', [
|
||||
h(NMenu, {
|
||||
mode: "horizontal",
|
||||
options: [
|
||||
{
|
||||
label: t('actions'),
|
||||
icon: () => h(MenuFilled),
|
||||
key: "action",
|
||||
children: [
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
onClick: () => showPassword(row.id)
|
||||
},
|
||||
{ default: () => t('showPass') }
|
||||
),
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
onClick: () => {
|
||||
adminMailTabAddress.value = row.name;
|
||||
adminTab.value = "mails";
|
||||
}
|
||||
},
|
||||
{ default: () => t('viewMails') }
|
||||
)
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
onClick: () => {
|
||||
adminSendBoxTabAddress.value = row.name;
|
||||
adminTab.value = "sendBox";
|
||||
}
|
||||
},
|
||||
{ default: () => t('viewSendBox') }
|
||||
)
|
||||
},
|
||||
{
|
||||
label: () => h(NButton,
|
||||
{
|
||||
bordered: false,
|
||||
ghost: true,
|
||||
onClick: () => {
|
||||
curDeleteAddressId.value = row.id;
|
||||
showDelteAccount.value = true;
|
||||
}
|
||||
},
|
||||
{ default: () => t('delete') }
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
watch([page, pageSize], async () => {
|
||||
await fetchData()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
await fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-modal v-model:show="showEmailPassword" preset="dialog" title="Dialog">
|
||||
<template #header>
|
||||
<div>{{ t("password") }}</div>
|
||||
</template>
|
||||
<span>
|
||||
<p>{{ t("passwordTip") }}</p>
|
||||
</span>
|
||||
<n-card>
|
||||
<b>{{ curEmailPassword }}</b>
|
||||
</n-card>
|
||||
<template #action>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
|
||||
<p>{{ t('deleteTip') }}</p>
|
||||
<template #action>
|
||||
<n-button @click="deleteEmail" size="small" tertiary round type="error">
|
||||
{{ t('delteAccount') }}
|
||||
</n-button>
|
||||
</template>
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')" />
|
||||
<n-button @click="fetchData" type="primary" ghost>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||
:page-sizes="[20, 50, 100]" show-size-picker>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-pagination {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
112
frontend/src/views/admin/Mails.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { processItem } from '../../utils/email-parser'
|
||||
|
||||
const {
|
||||
localeCache, adminAuth, showAdminAuth,
|
||||
adminMailTabAddress
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
mails: 'Emails',
|
||||
itemCount: 'itemCount',
|
||||
query: 'Query',
|
||||
},
|
||||
zh: {
|
||||
mails: '邮件',
|
||||
itemCount: '总数',
|
||||
query: '查询',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mailData = ref([])
|
||||
const mailCount = ref(0)
|
||||
const mailPage = ref(1)
|
||||
const mailPageSize = ref(20)
|
||||
|
||||
watch([mailPage, mailPageSize, adminMailTabAddress], async () => {
|
||||
await fetchMailData()
|
||||
})
|
||||
|
||||
const fetchMailData = async () => {
|
||||
if (!adminMailTabAddress.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { results, count } = await api.fetch(
|
||||
`/admin/mails`
|
||||
+ `?address=${adminMailTabAddress.value}`
|
||||
+ `&limit=${mailPageSize.value}`
|
||||
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
||||
);
|
||||
mailData.value = await Promise.all(results.map(async (item) => {
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (count > 0) {
|
||||
mailCount.value = count;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
await fetchMailData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="adminMailTabAddress" />
|
||||
<n-button @click="fetchMailData" type="primary" ghost>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<n-list hoverable clickable>
|
||||
<div style="display: inline-block; margin-bottom: 10px;">
|
||||
<n-pagination v-model:page="mailPage" v-model:page-size="mailPageSize" :item-count="mailCount" simple>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-list-item v-for="row in mailData" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.subject">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</template>
|
||||
<div v-html="row.message"></div>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-pagination {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
106
frontend/src/views/admin/MailsUnknow.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { processItem } from '../../utils/email-parser'
|
||||
|
||||
const {
|
||||
localeCache, adminAuth, showAdminAuth
|
||||
} = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
itemCount: 'itemCount',
|
||||
refresh: 'Refresh'
|
||||
},
|
||||
zh: {
|
||||
itemCount: '总数',
|
||||
refresh: '刷新'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const mailUnknowData = ref([])
|
||||
const mailUnknowCount = ref(0)
|
||||
const mailUnknowPage = ref(1)
|
||||
const mailUnknowPageSize = ref(20)
|
||||
|
||||
watch([mailUnknowPage, mailUnknowPageSize], async () => {
|
||||
await fetchMailUnknowData()
|
||||
})
|
||||
|
||||
const fetchMailUnknowData = async () => {
|
||||
try {
|
||||
const { results, count } = await api.fetch(
|
||||
`/admin/mails_unknow`
|
||||
+ `?limit=${mailUnknowPageSize.value}`
|
||||
+ `&offset=${(mailUnknowPage.value - 1) * mailUnknowPage.value}`
|
||||
);
|
||||
mailUnknowData.value = await Promise.all(results.map(async (item) => {
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (count > 0) {
|
||||
mailUnknowCount.value = count;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
await fetchMailUnknowData();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-button @click="fetchMailUnknowData" type="primary" ghost>
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
<n-list hoverable clickable>
|
||||
<div style="display: inline-block; margin-bottom: 10px;">
|
||||
<n-pagination v-model:page="mailUnknowPage" v-model:page-size="mailUnknowPageSize"
|
||||
:item-count="mailUnknowCount" simple>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-list-item v-for="row in mailUnknowData" v-bind:key="row.id">
|
||||
<n-thing class="center" :title="row.subject">
|
||||
<template #description>
|
||||
<n-space>
|
||||
<n-tag type="info">
|
||||
FROM: {{ row.source }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
ID: {{ row.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
TO: {{ row.address }}
|
||||
</n-tag>
|
||||
</n-space>
|
||||
</template>
|
||||
<div v-html="row.message"></div>
|
||||
</n-thing>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-pagination {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
118
frontend/src/views/admin/Maintenance.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { CleaningServicesFilled } from '@vicons/material'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, adminAuth, showAdminAuth } = useGlobalState()
|
||||
const message = useMessage()
|
||||
const cleanMailsDays = ref(30)
|
||||
const cleanUnknowMailsDays = ref(30)
|
||||
const cleanAddressDays = ref(30)
|
||||
const cleanSendBoxDays = ref(30)
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
tip: 'Please input the cleanup days',
|
||||
mailBoxTip: "Clean up {day} days ago mailbox",
|
||||
mailUnknowTip: "Clean up {day} days ago mails with unknow receiver",
|
||||
addressUnActiveTip: "Clean up {day} days ago unactive address",
|
||||
sendBoxTip: "Clean up {day} days ago sendbox",
|
||||
cleanupSuccess: "Cleanup success",
|
||||
},
|
||||
zh: {
|
||||
tip: '请输入清理天数',
|
||||
mailBoxTip: "清理{day}天前的收件箱",
|
||||
mailUnknowTip: "清理{day}天前的无收件人邮件",
|
||||
addressUnActiveTip: "清理{day}天前的未活动地址",
|
||||
sendBoxTip: "清理{day}天前的发件箱",
|
||||
cleanupSuccess: "清理成功",
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const cleanup = async (cleanType, cleanDays) => {
|
||||
try {
|
||||
await api.fetch('/admin/cleanup', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ cleanType, cleanDays })
|
||||
});
|
||||
message.success(t('cleanupSuccess'));
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card>
|
||||
<div class="item">
|
||||
<n-input-number v-model:value="cleanMailsDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('mails', cleanMailsDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('mailBoxTip', { day: cleanMailsDays }) }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="item">
|
||||
<n-input-number v-model:value="cleanUnknowMailsDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('mails_unknow', cleanUnknowMailsDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('mailUnknowTip', { day: cleanUnknowMailsDays }) }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="item">
|
||||
<n-input-number v-model:value="cleanAddressDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('address', cleanAddressDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('addressUnActiveTip', { day: cleanAddressDays }) }}
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="item">
|
||||
<n-input-number v-model:value="cleanSendBoxDays" :placeholder="t('tip')" />
|
||||
<n-button @click="cleanup('sendbox', cleanSendBoxDays)">
|
||||
<template #icon>
|
||||
<n-icon :component="CleaningServicesFilled" />
|
||||
</template>
|
||||
{{ t('sendBoxTip', { day: cleanSendBoxDays }) }}
|
||||
</n-button>
|
||||
</div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-card {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
margin: 10px;
|
||||
}
|
||||
</style>
|
||||
162
frontend/src/views/admin/SendBox.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, settings, adminAuth, adminSendBoxTabAddress } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
address: 'Address',
|
||||
success: 'Success',
|
||||
to_mail: 'To Mail',
|
||||
subject: 'Subject',
|
||||
created_at: 'Created At',
|
||||
action: 'Action',
|
||||
query: 'Query',
|
||||
itemCount: 'itemCount',
|
||||
view: 'View',
|
||||
},
|
||||
zh: {
|
||||
address: '地址',
|
||||
success: '成功',
|
||||
to_mail: '收件人邮箱',
|
||||
subject: '主题',
|
||||
created_at: '创建时间',
|
||||
action: '操作',
|
||||
query: '查询',
|
||||
itemCount: '总数',
|
||||
view: '查看',
|
||||
}
|
||||
}
|
||||
});
|
||||
const data = ref([])
|
||||
const count = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = ref(20)
|
||||
|
||||
const curRow = ref({})
|
||||
const showModal = ref(false)
|
||||
|
||||
const fetchData = async () => {
|
||||
if (!adminSendBoxTabAddress.value) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { results, count: addressCount } = await api.fetch(
|
||||
`/admin/sendbox`
|
||||
+ `?address=${adminSendBoxTabAddress.value}`
|
||||
+ `&limit=${pageSize.value}`
|
||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||
);
|
||||
data.value = results.map((item) => {
|
||||
try {
|
||||
const data = JSON.parse(item.raw);
|
||||
item.to_mail = data?.personalizations?.map(
|
||||
(p) => p.to?.map((t) => t.email).join(',')
|
||||
).join(';');
|
||||
item.subject = data.subject;
|
||||
item.raw = JSON.stringify(data, null, 2);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
if (addressCount > 0) {
|
||||
count.value = addressCount;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: "ID",
|
||||
key: "id"
|
||||
},
|
||||
{
|
||||
title: t('address'),
|
||||
key: "address"
|
||||
},
|
||||
{
|
||||
title: t('to_mail'),
|
||||
key: "to_mail"
|
||||
},
|
||||
{
|
||||
title: t('subject'),
|
||||
key: "subject"
|
||||
},
|
||||
{
|
||||
title: t('created_at'),
|
||||
key: "created_at"
|
||||
},
|
||||
{
|
||||
title: t('action'),
|
||||
key: 'actions',
|
||||
render(row) {
|
||||
return h('div', [
|
||||
h(NButton,
|
||||
{
|
||||
type: 'success',
|
||||
ghost: true,
|
||||
onClick: () => {
|
||||
showModal.value = true;
|
||||
curRow.value = row;
|
||||
}
|
||||
},
|
||||
{ default: () => t('view') }
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
watch([page, pageSize], async () => {
|
||||
await fetchData()
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
showAdminAuth.value = true;
|
||||
return;
|
||||
}
|
||||
await fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="settings.address">
|
||||
<n-modal v-model:show="showModal" preset="dialog">
|
||||
<pre>{{ curRow.raw }}</pre>
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="adminSendBoxTabAddress" />
|
||||
<n-button @click="fetchData" type="primary" ghost>
|
||||
{{ t('query') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||
:page-sizes="[20, 50, 100]" show-size-picker>
|
||||
<template #prefix="{ itemCount }">
|
||||
{{ t('itemCount') }}: {{ itemCount }}
|
||||
</template>
|
||||
</n-pagination>
|
||||
</div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.n-pagination {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -57,6 +57,7 @@ const updateData = async () => {
|
||||
await api.fetch(`/admin/address_sender`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
address: curRow.value.address,
|
||||
address_id: curRow.value.id,
|
||||
balance: senderBalance.value,
|
||||
enabled: senderEnabled.value ? 1 : 0
|
||||
|
||||
92
frontend/src/views/admin/Statistics.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<script setup>
|
||||
import { ref, h, onMounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { User, UserCheck, MailBulk } from '@vicons/fa'
|
||||
import { SendOutlined } from '@vicons/material'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, adminAuth } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
userCount: 'User Count',
|
||||
activeUser: '7 days Active User',
|
||||
mailCount: 'Mail Count',
|
||||
sendMailCount: 'Send Mail Count'
|
||||
},
|
||||
zh: {
|
||||
userCount: '用户总数',
|
||||
activeUser: '周活跃用户',
|
||||
mailCount: '邮件总数',
|
||||
sendMailCount: '发送邮件总数'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const statistics = ref({
|
||||
userCount: 0,
|
||||
mailCount: 0,
|
||||
activeUserCount7days: 0,
|
||||
sendMailCount: 0,
|
||||
})
|
||||
|
||||
const fetchStatistics = async () => {
|
||||
try {
|
||||
const {
|
||||
userCount, activeUserCount7days, mailCount, sendMailCount
|
||||
} = 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;
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (!adminAuth.value) {
|
||||
return;
|
||||
}
|
||||
await fetchStatistics()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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>
|
||||
</template>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
@@ -36,7 +36,7 @@ const { t } = useI18n({
|
||||
content: 'Content',
|
||||
send: 'Send',
|
||||
requestAccess: 'Request Access',
|
||||
requestAccessTip: 'You need to request access to send mail',
|
||||
requestAccessTip: 'You need to request access to send mail, if have request, please contact admin.',
|
||||
send_balance: 'Send Mail Balance Left',
|
||||
},
|
||||
zh: {
|
||||
@@ -51,7 +51,7 @@ const { t } = useI18n({
|
||||
content: '内容',
|
||||
send: '发送',
|
||||
requestAccess: '申请权限',
|
||||
requestAccessTip: '您需要申请权限才能发送邮件',
|
||||
requestAccessTip: '您需要申请权限才能发送邮件, 如果已经申请过, 请联系管理员提升额度。',
|
||||
send_balance: '剩余发送邮件额度',
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,7 @@ const send = async () => {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
message.success(t("successSend"));
|
||||
router.push('/');
|
||||
router.push('/sendbox');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,10 +97,15 @@ const requestAccess = async () => {
|
||||
}
|
||||
)
|
||||
message.success(t("success"))
|
||||
await api.getSettings();
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await api.getSettings();
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
307
vitepress-docs/.gitignore
vendored
Normal file
@@ -0,0 +1,307 @@
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||
|
||||
# Custom
|
||||
dist/
|
||||
|
||||
# User-specific files
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUNIT
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
#**/Properties/launchSettings.json
|
||||
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_i.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# JustCode is a .NET coding add-in
|
||||
.JustCode
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# TODO: Comment the next line if you want to checkin your web deploy settings
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/packages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/packages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/packages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# CodeRush
|
||||
.cr/
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
/coverage
|
||||
/src/client/shared.ts
|
||||
/src/node/shared.ts
|
||||
*.log
|
||||
.DS_Store
|
||||
.vite_opt_cache
|
||||
dist
|
||||
node_modules
|
||||
TODOs.md
|
||||
.vscode
|
||||
docs/.vitepress/cache/
|
||||
docs/.vitepress/dist/
|
||||
.idea/
|
||||
|
||||
*.zip
|
||||
40
vitepress-docs/docs/.vitepress/config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { defineConfig } from 'vitepress'
|
||||
import { zh } from './zh'
|
||||
import { en } from './en'
|
||||
|
||||
export default defineConfig({
|
||||
title: "Temp Mail Doc",
|
||||
lang: 'zh-CN',
|
||||
lastUpdated: true,
|
||||
locales: {
|
||||
root: { label: '简体中文', ...zh },
|
||||
en: { label: 'English', ...en }
|
||||
},
|
||||
head: [
|
||||
['link', { rel: 'icon', type: 'image/png', href: '/logo.png' }],
|
||||
['meta', { name: 'theme-color', content: '#5f67ee' }],
|
||||
['meta', { property: 'og:type', content: 'website' }],
|
||||
['meta', { property: 'og:locale', content: 'Temp Mail Doc' }],
|
||||
['meta', { property: 'og:title', content: 'Temp Mail Doc' }],
|
||||
['meta', { property: 'og:site_name', content: 'Temp Mail' }],
|
||||
['meta', { property: 'og:image', content: 'https://temp-mail-docs.awsl.uk/logo.png' }],
|
||||
['meta', { property: 'og:url', content: 'https://temp-mail-docs.awsl.uk' }],
|
||||
],
|
||||
sitemap: {
|
||||
hostname: 'https://temp-mail-docs.awsl.uk',
|
||||
transformItems(items) {
|
||||
return items.filter((item) => !item.url.includes('migration'))
|
||||
}
|
||||
},
|
||||
themeConfig: {
|
||||
|
||||
logo: { src: '/logo.png', width: 24, height: 24 },
|
||||
search: { provider: 'local' },
|
||||
socialLinks: [
|
||||
{
|
||||
icon: 'github',
|
||||
link: 'https://github.com/dreamhunter2333/cloudflare_temp_email'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
55
vitepress-docs/docs/.vitepress/en.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { defineConfig, type DefaultTheme } from 'vitepress'
|
||||
|
||||
export const en = defineConfig({
|
||||
title: "Temp Mail Doc",
|
||||
lang: 'zh-Hans',
|
||||
description: 'CloudFlare Free sending and receiving of temporary domain name mailboxes',
|
||||
|
||||
themeConfig: {
|
||||
nav: nav(),
|
||||
|
||||
editLink: {
|
||||
pattern: 'https://github.com/dreamhunter2333/cloudflare_temp_email/edit/main/vitepress-docs/docs/:path',
|
||||
text: 'Edit this page on GitHub'
|
||||
},
|
||||
|
||||
footer: {
|
||||
message: 'Based on MIT license',
|
||||
copyright: `Copyright © 2023-${new Date().getFullYear()} Dream Hunter`
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
function nav(): DefaultTheme.NavItem[] {
|
||||
return [
|
||||
{
|
||||
text: 'Home',
|
||||
link: '/en/',
|
||||
},
|
||||
{
|
||||
text: 'Guide',
|
||||
link: '/en/cli',
|
||||
},
|
||||
{
|
||||
text: 'Service Status',
|
||||
link: '/status',
|
||||
},
|
||||
{
|
||||
text: 'Reference',
|
||||
link: '/reference',
|
||||
},
|
||||
{
|
||||
text: process.env.TAG_NAME || 'v0.2.2',
|
||||
items: [
|
||||
{
|
||||
text: 'CHANGELOG',
|
||||
link: 'https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/CHANGELOG.md'
|
||||
},
|
||||
{
|
||||
text: 'Contribute',
|
||||
link: 'https://github.com/dreamhunter2333/cloudflare_temp_email'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
138
vitepress-docs/docs/.vitepress/zh.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { defineConfig, type DefaultTheme } from 'vitepress'
|
||||
|
||||
export const zh = defineConfig({
|
||||
title: "临时邮箱文档",
|
||||
lang: 'zh-Hans',
|
||||
description: 'CloudFlare 免费收发 临时域名邮箱',
|
||||
|
||||
themeConfig: {
|
||||
nav: nav(),
|
||||
|
||||
sidebar: {
|
||||
'/zh/guide/': { base: '/zh/guide/', items: sidebarGuide() },
|
||||
},
|
||||
|
||||
editLink: {
|
||||
pattern: 'https://github.com/dreamhunter2333/cloudflare_temp_email/edit/main/vitepress-docs/docs/:path',
|
||||
text: '在 GitHub 上编辑此页面'
|
||||
},
|
||||
|
||||
footer: {
|
||||
message: '基于 MIT 许可发布',
|
||||
copyright: `版权所有 © 2023-${new Date().getFullYear()} Dream Hunter`
|
||||
},
|
||||
|
||||
docFooter: {
|
||||
prev: '上一页',
|
||||
next: '下一页'
|
||||
},
|
||||
|
||||
outline: {
|
||||
label: '页面导航'
|
||||
},
|
||||
|
||||
lastUpdated: {
|
||||
text: '最后更新于',
|
||||
formatOptions: {
|
||||
dateStyle: 'short',
|
||||
timeStyle: 'medium'
|
||||
}
|
||||
},
|
||||
|
||||
langMenuLabel: '多语言',
|
||||
returnToTopLabel: '回到顶部',
|
||||
sidebarMenuLabel: '菜单',
|
||||
darkModeSwitchLabel: '主题',
|
||||
lightModeSwitchTitle: '切换到浅色模式',
|
||||
darkModeSwitchTitle: '切换到深色模式'
|
||||
}
|
||||
})
|
||||
|
||||
function nav(): DefaultTheme.NavItem[] {
|
||||
return [
|
||||
{
|
||||
text: '主页',
|
||||
link: '/',
|
||||
},
|
||||
{
|
||||
text: '指南',
|
||||
link: '/zh/guide/quick-start',
|
||||
activeMatch: '/zh/guide/'
|
||||
},
|
||||
{
|
||||
text: '服务状态',
|
||||
link: '/status',
|
||||
},
|
||||
{
|
||||
text: '参考',
|
||||
link: '/reference',
|
||||
},
|
||||
{
|
||||
text: process.env.TAG_NAME || 'v0.2.2',
|
||||
items: [
|
||||
{
|
||||
text: '更新日志',
|
||||
link: 'https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/CHANGELOG.md'
|
||||
},
|
||||
{
|
||||
text: '参与贡献',
|
||||
link: 'https://github.com/dreamhunter2333/cloudflare_temp_email'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function sidebarGuide(): DefaultTheme.SidebarItem[] {
|
||||
return [
|
||||
{
|
||||
text: '简介',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '什么是临时邮箱', link: 'what-is-temp-mail' },
|
||||
{ text: 'Star History', link: 'star-history' },
|
||||
{ text: '快速开始部署', link: 'quick-start' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '通过命令行部署',
|
||||
collapsed: false,
|
||||
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' },
|
||||
{ text: '配置发送邮件', link: 'config-send-mail' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '通过用户界面部署',
|
||||
collapsed: false,
|
||||
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' },
|
||||
{ text: '配置发送邮件', link: 'config-send-mail' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '通过 Github Actions 部署',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: '开发中', link: 'github-action' },
|
||||
]
|
||||
},
|
||||
{
|
||||
text: '功能简介',
|
||||
collapsed: false,
|
||||
items: [
|
||||
{ text: 'Admin 控制台', link: 'feature/admin' },
|
||||
]
|
||||
},
|
||||
{ text: '参考', base: "/", link: 'reference' }
|
||||
]
|
||||
}
|
||||
@@ -1,13 +1,5 @@
|
||||
# cloudflare temp email
|
||||
|
||||
## [中文](README.md)
|
||||
|
||||
[CHANGELOG](CHANGELOG.md)
|
||||
|
||||
## [Live Demo](https://mail.awsl.uk/)
|
||||
|
||||
[https://mail.awsl.uk](https://mail.awsl.uk/)
|
||||
|
||||
This is a temporary email service that uses Cloudflare Workers to create a temporary email address and view the received email in web browser.
|
||||
|
||||
## Features
|
||||
@@ -24,13 +16,19 @@ This is a temporary email service that uses Cloudflare Workers to create a tempo
|
||||
- [x] Add attachment viewing function
|
||||
- [x] use rust wasm to parse email
|
||||
- [x] support send email
|
||||
|
||||

|
||||
- [x] support DKIM
|
||||
|
||||
## Deploy
|
||||
|
||||
[Install/Update Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/)
|
||||
|
||||
```bash
|
||||
npm install wrangler -g
|
||||
git clone https://github.com/dreamhunter2333/cloudflare_temp_email.git
|
||||
# Switch to the latest tag or the branch you want to deploy. You can also use the main branch directly.
|
||||
# git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
```
|
||||
|
||||
## DB - Cloudflare D1
|
||||
|
||||
```bash
|
||||
@@ -42,7 +40,7 @@ wrangler d1 execute dev --file=db/schema.sql
|
||||
# wrangler d1 execute dev --file=db/2024-04-03-patch.sql
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
### Backend - Cloudflare workers
|
||||
|
||||
@@ -51,24 +49,49 @@ The first deployment will prompt you to create a project. Please fill in `produc
|
||||
```bash
|
||||
cd worker
|
||||
pnpm install
|
||||
# copy wrangler.toml.template to wrangler.toml
|
||||
# and add your d1 config and these config
|
||||
# PREFIX = "tmp" - the email create will be like tmp<xxxxx>@DOMAIN
|
||||
# IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES
|
||||
# PASSWORDS = ["123", "456"]
|
||||
# For admin panel, if not set will no allow to access the admin panel
|
||||
# ADMIN_PASSWORDS = ["123", "456"]
|
||||
# DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] you domain name
|
||||
# JWT_SECRET = "xxx"
|
||||
# BLACK_LIST = ""
|
||||
cp wrangler.toml.template wrangler.toml
|
||||
# deploy
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
`wrangler.toml`
|
||||
|
||||
```bash
|
||||
name = "cloudflare_temp_email"
|
||||
main = "src/worker.js"
|
||||
compatibility_date = "2023-08-14"
|
||||
node_compat = true
|
||||
|
||||
[vars]
|
||||
PREFIX = "tmp" # The mailbox name prefix to be processed
|
||||
# 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"]
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # your domain name
|
||||
JWT_SECRET = "xxx" # Key used to generate jwt
|
||||
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
|
||||
# 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
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "xxx" # D1 database name
|
||||
database_id = "xxx" # D1 database ID
|
||||
|
||||
# Create a new address current limiting configuration
|
||||
# [[unsafe.bindings]]
|
||||
# name = "RATE_LIMITER"
|
||||
# type = "ratelimit"
|
||||
# namespace_id = "1001"
|
||||
# # 10 requests per minute
|
||||
# simple = { limit = 10, period = 60 }
|
||||
```
|
||||
|
||||
you can find and test the worker's url in the workers dashboard
|
||||
|
||||

|
||||

|
||||
|
||||
## Cloudflare Email Routing
|
||||
|
||||
@@ -76,7 +99,7 @@ Before you can bind an email address to your Worker, you need to enable Email Ro
|
||||
|
||||
enable email route and config email forward catch-all to the worker
|
||||
|
||||

|
||||

|
||||
|
||||
### Frontend - Cloudflare pages
|
||||
|
||||
@@ -92,7 +115,7 @@ pnpm build --emptyOutDir
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
## Configure sending emails
|
||||
|
||||
@@ -106,3 +129,26 @@ Create a new `_mailchannels` record, the type is `TXT`, the content is `v=mc1 cf
|
||||
|
||||
- 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>`
|
||||
24
vitepress-docs/docs/en/index.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "Temporary mailbox document"
|
||||
tagline: "Build CloudFlare to send and receive free temporary domain name mailboxes"
|
||||
actions:
|
||||
- theme: brand
|
||||
text: Try it now
|
||||
link: https://mail.awsl.uk/
|
||||
- theme: alt
|
||||
text: command line deployment
|
||||
link: /en/cli
|
||||
features:
|
||||
- title: Free hosting on CloudFlare, no server required
|
||||
details: Cloudflare D1 database, Cloudflare Pages frontend, Cloudflare Workers backend, Cloudflare Email Routing
|
||||
- title: Only domain name required for private deployment
|
||||
details: Support password login email, access authorization can be used as a private site, support attachment function
|
||||
- title: Use rust wasm to parse emails
|
||||
details: Use rust wasm to parse emails, support various RFC standards for emails, support attachments, extremely fast
|
||||
- title: Support sending emails
|
||||
details: Support sending txt or html emails through domain name mailboxes,Support DKIM signature
|
||||
---
|
||||
28
vitepress-docs/docs/index.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
# https://vitepress.dev/reference/default-theme-home-page
|
||||
layout: home
|
||||
|
||||
hero:
|
||||
name: "临时邮箱文档"
|
||||
tagline: "搭建 CloudFlare 免费收发 临时域名邮箱"
|
||||
actions:
|
||||
- theme: brand
|
||||
text: 立即试用
|
||||
link: https://mail.awsl.uk/
|
||||
- theme: alt
|
||||
text: 命令行部署
|
||||
link: /zh/guide/quick-start
|
||||
- theme: alt
|
||||
text: 通过用户界面部署
|
||||
link: /zh/guide/quick-start
|
||||
|
||||
features:
|
||||
- title: 免费托管在 CloudFlare,无需服务器
|
||||
details: Cloudflare D1 数据库,Cloudflare Pages 前端,Cloudflare Workers 后端, Cloudflare Email Routing
|
||||
- title: 仅需域名即可私有部署
|
||||
details: 支持 password 登录邮箱,访问授权可作为私人站点,支持附件功能
|
||||
- title: 使用 rust wasm 解析邮件
|
||||
details: 使用 rust wasm 解析邮件,支持邮件各种RFC标准,支持附件, 速度极快
|
||||
- title: 支持发送邮件
|
||||
details: 支持通过域名邮箱发送 txt 或者 html 邮件,支持 DKIM 签名
|
||||
---
|
||||
BIN
vitepress-docs/docs/public/feature/admin.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
vitepress-docs/docs/public/logo.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
BIN
vitepress-docs/docs/public/ui_install/d1-exec.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
vitepress-docs/docs/public/ui_install/d1.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
vitepress-docs/docs/public/ui_install/pages-1.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
vitepress-docs/docs/public/ui_install/pages-domain.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
vitepress-docs/docs/public/ui_install/pages.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
vitepress-docs/docs/public/ui_install/worker-1.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
vitepress-docs/docs/public/ui_install/worker-2.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
vitepress-docs/docs/public/ui_install/worker-3.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
vitepress-docs/docs/public/ui_install/worker-d1.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
vitepress-docs/docs/public/ui_install/worker-var.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
vitepress-docs/docs/public/ui_install/worker_home.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
6
vitepress-docs/docs/reference.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# Reference
|
||||
|
||||
- https://developers.cloudflare.com/d1/
|
||||
- https://developers.cloudflare.com/pages/
|
||||
- https://developers.cloudflare.com/workers/
|
||||
- https://developers.cloudflare.com/email-routing/
|
||||
8
vitepress-docs/docs/status.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Status Page
|
||||
|
||||
[Status Link](https://uptime.aks.awsl.icu/status/temp-email)
|
||||
|
||||
| Service | Status |
|
||||
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Backend](https://temp-email-api.awsl.uk/) |       |
|
||||
| [Frontend](https://mail.awsl.uk/) |       |
|
||||
27
vitepress-docs/docs/zh/guide/cli/d1.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# 初始化/更新 D1 数据库
|
||||
|
||||
第一次执行登录 wrangler 命令时,会提示登录, 按提示操作即可
|
||||
|
||||
## 初始化数据库
|
||||
|
||||
```bash
|
||||
# 创建 D1 并执行 schema.sql
|
||||
wrangler d1 create dev
|
||||
wrangler d1 execute dev --file=db/schema.sql
|
||||
```
|
||||
|
||||
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
|
||||
|
||||

|
||||
|
||||
## 更新数据库 schema
|
||||
|
||||
`schema` 更新,请确认你之前部署的版本,
|
||||
查看 [更新日志](https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/CHANGELOG.md)
|
||||
|
||||
找到需要执行的 `patch` 文件, 执行, 例如:
|
||||
|
||||
```bash
|
||||
wrangler d1 execute dev --file=db/2024-01-13-patch.sql
|
||||
wrangler d1 execute dev --file=db/2024-04-03-patch.sql
|
||||
```
|
||||
25
vitepress-docs/docs/zh/guide/cli/pages.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Cloudflare Pages 前端
|
||||
|
||||
第一次部署会提示创建项目, `production` 分支请填写 `production`
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install
|
||||
cp .env.example .env.prod
|
||||
```
|
||||
|
||||
修改 `.env.prod` 文件
|
||||
|
||||
将 `VITE_API_BASE` 修改为上一步创建的 `worker` 的 `url`, 不要在末尾加 `/`
|
||||
|
||||
例如: `VITE_API_BASE=https://xxx.xxx.workers.dev`
|
||||
|
||||
```bash
|
||||
pnpm build --emptyOutDir
|
||||
# 根据提示创建 pages
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
部署完成之后你可以在 Cloudflare 控制台看到你的项目, 可以为 `pages` 配置自定义域名
|
||||
|
||||

|
||||
17
vitepress-docs/docs/zh/guide/cli/pre-requisite.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# 先决条件
|
||||
|
||||
## wrangler 的安装
|
||||
|
||||
安装 wrangler
|
||||
|
||||
```bash
|
||||
npm install wrangler -g
|
||||
```
|
||||
|
||||
## 克隆项目
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dreamhunter2333/cloudflare_temp_email.git
|
||||
# 切换到最新 tag 或者你想部署的分支,你也可以直接使用 main 分支
|
||||
# git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
|
||||
```
|
||||
61
vitepress-docs/docs/zh/guide/cli/worker.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Cloudflare workers 后端
|
||||
|
||||
## 初始化项目
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
pnpm install
|
||||
cp wrangler.toml.template wrangler.toml
|
||||
```
|
||||
|
||||
## 修改 `wrangler.toml` 配置文件
|
||||
|
||||
```toml
|
||||
name = "cloudflare_temp_email"
|
||||
main = "src/worker.js"
|
||||
compatibility_date = "2023-12-01"
|
||||
# 如果你想使用自定义域名,你需要添加 routes 配置
|
||||
# routes = [
|
||||
# { pattern = "temp-email-api.xxxxx.xyz", custom_domain = true },
|
||||
# ]
|
||||
node_compat = true
|
||||
|
||||
[vars]
|
||||
PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空字符串
|
||||
# 如果你想要你的网站私有,取消下面的注释,并修改密码
|
||||
# PASSWORDS = ["123", "456"]
|
||||
# admin 控制台密码, 不配置则不允许访问控制台
|
||||
# ADMIN_PASSWORDS = ["123", "456"]
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
|
||||
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥
|
||||
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
||||
# dkim config
|
||||
# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels
|
||||
# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容
|
||||
|
||||
# D1 数据库的名称和 ID 可以在 cloudflare 控制台查看
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "xxx" # D1 数据库名称
|
||||
database_id = "xxx" # D1 数据库 ID
|
||||
|
||||
# 新建地址限流配置 /api/new_address
|
||||
# [[unsafe.bindings]]
|
||||
# name = "RATE_LIMITER"
|
||||
# type = "ratelimit"
|
||||
# namespace_id = "1001"
|
||||
# # 10 requests per minute
|
||||
# simple = { limit = 10, period = 60 }
|
||||
```
|
||||
|
||||
## 部署
|
||||
|
||||
第一次部署会提示创建项目, `production` 分支请填写 `production`
|
||||
|
||||
```bash
|
||||
pnpm run deploy
|
||||
```
|
||||
|
||||
部署成功之后再路由中可以看到 `worker` 的 `url`,控制台也会输出 `worker` 的 `url`
|
||||
|
||||

|
||||
12
vitepress-docs/docs/zh/guide/config-send-mail.md
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
# 配置发送邮件
|
||||
|
||||
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`
|
||||
33
vitepress-docs/docs/zh/guide/dkim.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# 配置 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 的内容>"
|
||||
```
|
||||
9
vitepress-docs/docs/zh/guide/email-routing.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Cloudflare Email Routing
|
||||
|
||||
1. 配置对应域名的 `电子邮件 DNS 记录`, 如果是多个域名,需要配置多个域名的 `电子邮件 DNS 记录`
|
||||
|
||||
2. 在将电子邮件地址绑定到您的 Worker 之前,您需要启用电子邮件路由并拥有至少一个经过验证的电子邮件地址。
|
||||
|
||||
3. 配置每个域名的 `Cloudflare Email Routing` catch-all 发送到 `worker`
|
||||
|
||||

|
||||
7
vitepress-docs/docs/zh/guide/feature/admin.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Admin 控制台
|
||||
|
||||
部署前端应用之后,访问 `/admin` 路径即可进入管理控制台。
|
||||
|
||||
需要在后端配置 `admin 控制台密码`, 不配置则不允许访问控制台。
|
||||
|
||||

|
||||
5
vitepress-docs/docs/zh/guide/github-action.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# 通过 Github Actions 部署
|
||||
|
||||
::: warning
|
||||
开发中...
|
||||
:::
|
||||
8
vitepress-docs/docs/zh/guide/quick-start.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# 快速开始
|
||||
|
||||
- 良好的网络环境
|
||||
- cloudflare 账号
|
||||
|
||||
打开 [cloudflare控制台](https://dash.cloudflare.com/)
|
||||
|
||||
请查看通过 [命令行部署](/zh/guide/cli/pre-requisite) 或者 [用户界面部署](/zh/guide/ui/d1)
|
||||
7
vitepress-docs/docs/zh/guide/star-history.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Star History
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date" />
|
||||
</picture>
|
||||
25
vitepress-docs/docs/zh/guide/ui/d1.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# 初始化/更新 D1 数据库
|
||||
|
||||
## 初始化数据库
|
||||
|
||||
打开 cloudflare 控制台,选择 `Workers & Pages` -> `D1` -> `Create Database`,点击创建数据库
|
||||
|
||||

|
||||
|
||||
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
|
||||
|
||||
打开 `Console` 标签页,输入 `db/schema.sql` 的内容,点击 `Execute` 执行
|
||||
|
||||

|
||||
|
||||
## 更新数据库 schema
|
||||
|
||||
`schema` 更新,请确认你之前部署的版本,
|
||||
|
||||
查看 [更新日志](https://github.com/dreamhunter2333/cloudflare_temp_email/blob/main/CHANGELOG.md)
|
||||
|
||||
找到需要执行的 `patch` 文件, 执行, 例如: `db/2024-01-13-patch.sql`
|
||||
|
||||
打开 `Console` 标签页,输入 `patch` 文件的内容,点击 `Execute` 执行
|
||||
|
||||

|
||||
102
vitepress-docs/docs/zh/guide/ui/pages.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Cloudflare Pages 前端
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import JSZip from 'jszip';
|
||||
|
||||
const domain = ref("")
|
||||
const downloadUrl = ref("")
|
||||
const tip = ref("下载")
|
||||
|
||||
const generate = async () => {
|
||||
try {
|
||||
const response = await fetch("/ui_install/frontend.zip");
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
var zip = new JSZip();
|
||||
await zip.loadAsync(arrayBuffer);
|
||||
let target_content = ""
|
||||
let target_path = ""
|
||||
const directory = zip.folder("assets");
|
||||
if (directory) {
|
||||
for (const [relativePath, zipEntry] of Object.entries(directory.files)) {
|
||||
console.log(relativePath);
|
||||
if (relativePath.startsWith("assets/index-") && relativePath.endsWith(".js")){
|
||||
let content = await zipEntry.async("string");
|
||||
content = content.replace("https://temp-email-api.xxx.xxx", domain.value);
|
||||
target_path = relativePath;
|
||||
zip.file(relativePath, content);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!target_path) {
|
||||
tip.value = "生成失败";
|
||||
downloadUrl.value = '';
|
||||
}
|
||||
const blob = await zip.generateAsync({ type: "blob" });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
downloadUrl.value = url;
|
||||
} catch (error) {
|
||||
console.error("Error: ", error);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
1. 点击 `Workers & Pages` -> `Overview` -> `Create Application`
|
||||
|
||||

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

|
||||
|
||||
3. 输入部署的 worker 的地址, 地址不要带 `/`,点击生成,成功会出现下载按钮,你会得到一个 zip 包
|
||||
- 此处 worker 域名为后端 api 的域名,比如我部署在 `https://temp-email-api.awsl.uk`,则填写 `https://temp-email-api.awsl.uk`
|
||||
- 如果你的域名是 `https://temp-email-api.xxx.workers.dev`,则填写 `https://temp-email-api.xxx.workers.dev`
|
||||
|
||||
<div :class="$style.container">
|
||||
<input :class="$style.input" type="text" v-model="domain" placeholder="请输入地址"></input>
|
||||
<button :class="$style.button" @click="generate">生成</button>
|
||||
<a v-if="downloadUrl" :href="downloadUrl" download="frontend.zip">{{ tip }}</a>
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> 你也可以手动部署,从这里下载 zip, [frontend.zip](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/frontend.zip)
|
||||
>
|
||||
> 修改压缩包里面的 index-xxx.js 文件 ,xx 是随机的字符串
|
||||
>
|
||||
> 搜索 `https://temp-email-api.xxx.xxx` ,替换成你worker 的域名,然后部署新的zip文件
|
||||
|
||||
4. 选择 `Pages`,点击 `Create Pages`, 修改名称,上传下载的 zip 包,然后点击 `Deploy`
|
||||
|
||||

|
||||
|
||||
5. 打开 刚刚部署的 `Pages`,点击 `Custom Domain` 这里可以添加自己的域名,你也可以使用自动生成的 `*.pages.dev` 的域名。能打开域名说明部署成功。
|
||||
|
||||

|
||||
|
||||
<style module>
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.input {
|
||||
border: 2px solid deepskyblue;
|
||||
margin-right: 10px;
|
||||
width: 75%;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: deepskyblue;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: green;
|
||||
}
|
||||
</style>
|
||||
27
vitepress-docs/docs/zh/guide/ui/worker.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Cloudflare workers 后端
|
||||
|
||||
1. 点击 `Workers & Pages` -> `Overview` -> `Create Application`
|
||||
|
||||

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

|
||||
|
||||
3. 下载 [worker.js](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/worker.js)
|
||||
|
||||
4. 回到 `Overview`,找到刚刚创建的 worker,点击 `Edit Code`, 上传 `worker.js`, 删除 `index.js`,然后重命名 `worker.js` 为 `index.js`, 点击 `Deploy`
|
||||
|
||||

|
||||
|
||||
5. 点击 `Settings` -> `Trggers`, 这里可以添加自己的域名,你也可以使用自动生成的 `*.workers.dev` 的域名。能打开域名说明部署成功,记录下这个域名,后面部署前端会用到。
|
||||
|
||||

|
||||
|
||||
6. 点击 `Settings` -> `Variables`, 如图所示添加变量,参考 [修改 wrangler.toml 配置文件](/zh/guide/cli/worker.html#修改-wrangler-toml-配置文件) 中的 `vars` 部分
|
||||
|
||||

|
||||
|
||||
7. 点击 `Settings` -> `Variables`, 下拉找到 `D1 Database`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 D1 数据库,点击 `Deploy`
|
||||
|
||||

|
||||
7
vitepress-docs/docs/zh/guide/what-is-temp-mail.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# 临时邮箱简介
|
||||
|
||||
## 什么是临时邮箱
|
||||
|
||||
临时邮箱,也被称为一次性邮箱或临时邮件地址,是一种用于临时接收邮件的虚拟邮箱。与常规邮箱不同,临时邮箱旨在提供一种匿名且临时的邮件接收解决方案。
|
||||
|
||||
临时邮箱往往由网站或在线服务提供商提供,用户可以在需要注册或接收验证邮件时使用临时邮箱地址,而无需暴露自己的真实邮箱地址。这样做的好处是可以保护个人隐私
|
||||
20
vitepress-docs/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "temp-mail-docs",
|
||||
"private": true,
|
||||
"version": "0.2.6",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.7",
|
||||
"vitepress": "^1.1.0",
|
||||
"wrangler": "^3.50.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vitepress dev docs",
|
||||
"build": "vitepress build docs",
|
||||
"preview": "vitepress preview docs",
|
||||
"deploy": "npm run build && wrangler pages deploy ./docs/.vitepress/dist --project-name=temp-mail-docs --branch production"
|
||||
},
|
||||
"dependencies": {
|
||||
"jszip": "^3.10.1"
|
||||
}
|
||||
}
|
||||
2001
vitepress-docs/pnpm-lock.yaml
generated
Normal file
@@ -1,5 +1,7 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { getSendbox } from './send_mail_api'
|
||||
import { sendAdminInternalMail } from './utils'
|
||||
|
||||
const api = new Hono()
|
||||
|
||||
@@ -64,8 +66,11 @@ api.delete('/admin/delete_address/:id', async (c) => {
|
||||
if (!mailSuccess) {
|
||||
return c.text("Failed to delete mails", 500)
|
||||
}
|
||||
const { success: sendAccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM address_sender WHERE address_id = ? `
|
||||
).bind(id).run();
|
||||
return c.json({
|
||||
success: success
|
||||
success: success && mailSuccess && sendAccess
|
||||
})
|
||||
})
|
||||
|
||||
@@ -95,7 +100,7 @@ api.get('/admin/mails', async (c) => {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT id, source, raw, created_at FROM raw_mails where address = ? order by id desc limit ? offset ?`
|
||||
`SELECT * FROM raw_mails where address = ? order by id desc limit ? offset ?`
|
||||
).bind(address, limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
@@ -119,7 +124,7 @@ api.get('/admin/mails_unknow', async (c) => {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(`
|
||||
SELECT id, source, raw, created_at FROM raw_mails
|
||||
SELECT * FROM raw_mails
|
||||
where address NOT IN(select concat('${c.env.PREFIX}', name) from address)
|
||||
order by id desc limit ? offset ? `
|
||||
).bind(limit, offset).all();
|
||||
@@ -163,7 +168,7 @@ api.get('/admin/address_sender', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/admin/address_sender', async (c) => {
|
||||
let { address_id, balance, enabled } = await c.req.json();
|
||||
let { address, address_id, balance, enabled } = await c.req.json();
|
||||
if (!address_id) {
|
||||
return c.text("Invalid address_id", 400)
|
||||
}
|
||||
@@ -174,11 +179,20 @@ api.post('/admin/address_sender', async (c) => {
|
||||
if (!success) {
|
||||
return c.text("Failed to update address sender", 500)
|
||||
}
|
||||
await sendAdminInternalMail(
|
||||
c, address, "Account Send Access Updated",
|
||||
`You send access has been ${enabled ? "enabled" : "disabled"}, balance: ${balance}`
|
||||
);
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
})
|
||||
|
||||
api.get('/admin/sendbox', async (c) => {
|
||||
const { address, limit, offset } = c.req.query();
|
||||
return getSendbox(c, address, limit, offset);
|
||||
})
|
||||
|
||||
api.get('/admin/statistics', async (c) => {
|
||||
const { count: mailCountV1 } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM mails`
|
||||
@@ -192,11 +206,51 @@ api.get('/admin/statistics', async (c) => {
|
||||
const { count: activeUserCount7days } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')`
|
||||
).first();
|
||||
const { count: sendMailCount } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM sendbox`
|
||||
).first();
|
||||
return c.json({
|
||||
mailCount: (mailCountV1 || 0) + (mailCount || 0),
|
||||
userCount: addressCount,
|
||||
activeUserCount7days: activeUserCount7days
|
||||
activeUserCount7days: activeUserCount7days,
|
||||
sendMailCount: sendMailCount
|
||||
})
|
||||
});
|
||||
|
||||
api.post('/admin/cleanup', async (c) => {
|
||||
const { cleanType, cleanDays } = await c.req.json();
|
||||
if (!cleanType || !cleanDays || cleanDays < 0 || cleanDays > 30) {
|
||||
return c.text("Invalid cleanType or cleanDays", 400)
|
||||
}
|
||||
console.log(`Cleanup ${cleanType} before ${cleanDays} days`);
|
||||
switch (cleanType) {
|
||||
case "mails":
|
||||
await c.env.DB.prepare(`
|
||||
DELETE FROM raw_mails WHERE created_at < datetime('now', '-${cleanDays} day')`
|
||||
).run();
|
||||
break;
|
||||
case "mails_unknow":
|
||||
await c.env.DB.prepare(`
|
||||
DELETE FROM raw_mails WHERE address NOT IN
|
||||
(select concat('${c.env.PREFIX}', name) from address) AND created_at < datetime('now', '-${cleanDays} day')`
|
||||
).run();
|
||||
break;
|
||||
case "address":
|
||||
await c.env.DB.prepare(`
|
||||
DELETE FROM address WHERE updated_at < datetime('now', '-${cleanDays} day')`
|
||||
).run();
|
||||
break;
|
||||
case "sendbox":
|
||||
await c.env.DB.prepare(`
|
||||
DELETE FROM sendbox WHERE created_at < datetime('now', '-${cleanDays} day')`
|
||||
).run();
|
||||
break;
|
||||
default:
|
||||
return c.text("Invalid cleanType", 400)
|
||||
}
|
||||
return c.json({
|
||||
success: true
|
||||
})
|
||||
})
|
||||
|
||||
export { api }
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { getDomains, getPasswords } from './utils';
|
||||
|
||||
const api = new Hono()
|
||||
|
||||
api.get('/api/mails', async (c) => {
|
||||
@@ -31,6 +33,17 @@ api.get('/api/mails', async (c) => {
|
||||
})
|
||||
})
|
||||
|
||||
api.delete('/api/mails/:id', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE address = ? and id = ?`
|
||||
).bind(address, id).run();
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
})
|
||||
|
||||
api.get('/api/settings', async (c) => {
|
||||
const { address, address_id } = c.get("jwtPayload")
|
||||
if (address_id && address_id > 0) {
|
||||
@@ -124,13 +137,14 @@ api.post('/api/settings', async (c) => {
|
||||
api.get('/open_api/settings', async (c) => {
|
||||
// check header x-custom-auth
|
||||
let needAuth = false;
|
||||
if (c.env.PASSWORDS && c.env.PASSWORDS.length > 0) {
|
||||
const passwords = getPasswords(c);
|
||||
if (passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
needAuth = !c.env.PASSWORDS.includes(auth);
|
||||
needAuth = !passwords.includes(auth);
|
||||
}
|
||||
return c.json({
|
||||
"prefix": c.env.PREFIX,
|
||||
"domains": c.env.DOMAINS,
|
||||
"domains": getDomains(c),
|
||||
"needAuth": needAuth,
|
||||
});
|
||||
})
|
||||
@@ -151,8 +165,9 @@ api.get('/api/new_address', async (c) => {
|
||||
return c.text("Name too long (max 100)", 400)
|
||||
}
|
||||
// check domain, generate random domain
|
||||
if (!domain || !c.env.DOMAINS.includes(domain)) {
|
||||
domain = c.env.DOMAINS[Math.floor(Math.random() * c.env.DOMAINS.length)];
|
||||
const domains = getDomains(c);
|
||||
if (!domain || !domains.includes(domain)) {
|
||||
domain = domains[Math.floor(Math.random() * domains.length)];
|
||||
}
|
||||
// create address
|
||||
const emailAddress = c.env.PREFIX + name + "@" + domain
|
||||
@@ -205,8 +220,11 @@ api.delete('/api/delete_address', async (c) => {
|
||||
if (!mailSuccess) {
|
||||
return c.text("Failed to delete mails", 500)
|
||||
}
|
||||
const { success: sendAccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM address_sender WHERE address = ? `
|
||||
).bind(address).run();
|
||||
return c.json({
|
||||
success: success
|
||||
success: success && mailSuccess && sendAccess
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ api.post('/api/requset_send_mail_access', async (c) => {
|
||||
}
|
||||
try {
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO address_sender (address, enabled) VALUES (?, 0)`
|
||||
`INSERT INTO address_sender (address, balance, enabled) VALUES (?, 1, 1)`
|
||||
).bind(address).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to request send mail access", 500)
|
||||
@@ -52,13 +52,22 @@ api.post('/api/send_mail', async (c) => {
|
||||
if (!content) {
|
||||
return c.text("Invalid content", 400)
|
||||
}
|
||||
const body = JSON.stringify({
|
||||
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": {
|
||||
@@ -70,13 +79,13 @@ api.post('/api/send_mail', async (c) => {
|
||||
"type": is_html ? "text/html" : "text/plain",
|
||||
"value": content,
|
||||
}],
|
||||
});
|
||||
};
|
||||
let send_request = new Request("https://api.mailchannels.net/tx/v1/send", {
|
||||
"method": "POST",
|
||||
"headers": {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
"body": body,
|
||||
"body": JSON.stringify(body),
|
||||
});
|
||||
const resp = await fetch(send_request);
|
||||
const respText = await resp.text();
|
||||
@@ -98,9 +107,12 @@ api.post('/api/send_mail', async (c) => {
|
||||
}
|
||||
// save to sendbox
|
||||
try {
|
||||
if (body?.personalizations?.[0]?.dkim_private_key) {
|
||||
delete body.personalizations[0].dkim_private_key;
|
||||
}
|
||||
const { success: success2 } = await c.env.DB.prepare(
|
||||
`INSERT INTO sendbox (address, raw) VALUES (?, ?)`
|
||||
).bind(address, body).run();
|
||||
).bind(address, JSON.stringify(body)).run();
|
||||
if (!success2) {
|
||||
console.warn(`Failed to save to sendbox for ${address}`);
|
||||
}
|
||||
@@ -110,12 +122,10 @@ api.post('/api/send_mail', async (c) => {
|
||||
return c.json({ status: "ok" });
|
||||
})
|
||||
|
||||
api.get('/api/sendbox', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
const getSendbox = async (c, address, limit, offset) => {
|
||||
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)
|
||||
}
|
||||
@@ -137,6 +147,12 @@ api.get('/api/sendbox', async (c) => {
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
}
|
||||
|
||||
api.get('/api/sendbox', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { limit, offset } = c.req.query();
|
||||
return getSendbox(c, address, limit, offset);
|
||||
})
|
||||
|
||||
export { api }
|
||||
export { api, getSendbox }
|
||||
|
||||
79
worker/src/utils.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import { createMimeMessage } from "mimetext";
|
||||
|
||||
export const getDomains = (c) => {
|
||||
if (!c.env.DOMAINS) {
|
||||
return [];
|
||||
}
|
||||
// check if DOMAINS is an array, if not use json.parse
|
||||
if (!Array.isArray(c.env.DOMAINS)) {
|
||||
try {
|
||||
return JSON.parse(c.env.DOMAINS);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse DOMAINS", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return c.env.DOMAINS;
|
||||
}
|
||||
|
||||
export const getPasswords = (c) => {
|
||||
if (!c.env.PASSWORDS) {
|
||||
return [];
|
||||
}
|
||||
// check if PASSWORDS is an array, if not use json.parse
|
||||
if (!Array.isArray(c.env.PASSWORDS)) {
|
||||
try {
|
||||
return JSON.parse(c.env.PASSWORDS);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse PASSWORDS", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return c.env.PASSWORDS;
|
||||
}
|
||||
|
||||
export const getAdminPasswords = (c) => {
|
||||
if (!c.env.ADMIN_PASSWORDS) {
|
||||
return [];
|
||||
}
|
||||
// check if ADMIN_PASSWORDS is an array, if not use json.parse
|
||||
if (!Array.isArray(c.env.ADMIN_PASSWORDS)) {
|
||||
try {
|
||||
return JSON.parse(c.env.ADMIN_PASSWORDS);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse ADMIN_PASSWORDS", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
return c.env.ADMIN_PASSWORDS;
|
||||
}
|
||||
|
||||
export const sendAdminInternalMail = async (c, toMail, subject, text) => {
|
||||
try {
|
||||
|
||||
const msg = createMimeMessage();
|
||||
msg.setSender({
|
||||
name: "Admin",
|
||||
addr: "admin@internal"
|
||||
});
|
||||
msg.setRecipient(toMail);
|
||||
msg.setSubject(subject);
|
||||
msg.addMessage({
|
||||
contentType: 'text/plain',
|
||||
data: text
|
||||
});
|
||||
const message_id = Math.random().toString(36).substring(2, 15);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
|
||||
).bind(
|
||||
"admin@internal", toMail, msg.asRaw(), message_id
|
||||
).run();
|
||||
if (!success) {
|
||||
console.log(`Failed save message from admin@internal to ${toMail}`);
|
||||
}
|
||||
return success;
|
||||
} catch (error) {
|
||||
console.log("sendAdminInternalMail error", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -7,29 +7,43 @@ import { api as adminApi } from './admin_api';
|
||||
import { api as apiV1 } from './api_v1';
|
||||
import { api as apiSendMail } from './send_mail_api'
|
||||
import { email } from './email';
|
||||
import { getAdminPasswords, getPasswords } from './utils';
|
||||
|
||||
const app = new Hono()
|
||||
app.use('/*', cors());
|
||||
app.use('/api/*', async (c, next) => {
|
||||
// check header x-custom-auth
|
||||
if (c.env.PASSWORDS && c.env.PASSWORDS.length > 0) {
|
||||
const passwords = getPasswords(c);
|
||||
if (passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
if (!auth || !c.env.PASSWORDS.includes(auth)) {
|
||||
if (!auth || !passwords.includes(auth)) {
|
||||
return c.text("Need Password", 401)
|
||||
}
|
||||
}
|
||||
if (c.req.path.startsWith("/api/new_address") || c.req.path.startsWith("/api/send_mail")) {
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
if (reqIp && c.env.RATE_LIMITER) {
|
||||
const { success } = await c.env.RATE_LIMITER.limit(
|
||||
{ key: `${c.req.path}|${reqIp}` }
|
||||
)
|
||||
if (!success) {
|
||||
return c.text(`IP=${reqIp} Rate limit exceeded for ${c.req.path}`, 429)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (c.req.path.startsWith("/api/new_address")) {
|
||||
await next();
|
||||
return;
|
||||
};
|
||||
}
|
||||
return jwt({ secret: c.env.JWT_SECRET })(c, next);
|
||||
});
|
||||
|
||||
app.use('/admin/*', async (c, next) => {
|
||||
// check header x-admin-auth
|
||||
if (c.env.ADMIN_PASSWORDS && c.env.ADMIN_PASSWORDS.length > 0) {
|
||||
const adminPasswords = getAdminPasswords(c);
|
||||
if (adminPasswords && adminPasswords.length > 0) {
|
||||
const adminAuth = c.req.raw.headers.get("x-admin-auth");
|
||||
if (adminAuth && c.env.ADMIN_PASSWORDS.includes(adminAuth)) {
|
||||
if (adminAuth && adminPasswords.includes(adminAuth)) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,19 @@ PREFIX = "tmp"
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
|
||||
JWT_SECRET = "xxx"
|
||||
BLACK_LIST = ""
|
||||
# dkim config
|
||||
# DKIM_SELECTOR = ""
|
||||
# DKIM_PRIVATE_KEY = ""
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "xxx"
|
||||
database_id = "xxx"
|
||||
|
||||
# ratelimit config for /api/new_address
|
||||
# [[unsafe.bindings]]
|
||||
# name = "RATE_LIMITER"
|
||||
# type = "ratelimit"
|
||||
# namespace_id = "1001"
|
||||
# # 10 requests per minute
|
||||
# simple = { limit = 10, period = 60 }
|
||||
|
||||