Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83b9bc9d5f | ||
|
|
e81142f5ef | ||
|
|
32ce446a27 | ||
|
|
64bcf7d019 | ||
|
|
c015b57d73 | ||
|
|
0366699fec | ||
|
|
9c9f4565b1 | ||
|
|
8a02509ffa | ||
|
|
8b210388ee | ||
|
|
a456bfda7c | ||
|
|
23d1709ca1 | ||
|
|
6ce7e2e7f6 | ||
|
|
c0e870ce54 | ||
|
|
90e80fee53 | ||
|
|
4fd7f776f6 | ||
|
|
c73c86e86c | ||
|
|
08a3d4ce0e | ||
|
|
1404079073 | ||
|
|
829782d0cb | ||
|
|
f624fe5b58 | ||
|
|
b058a1bd12 | ||
|
|
0a8f50f9e0 | ||
|
|
a3edb09305 | ||
|
|
58dcdc65f8 | ||
|
|
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 | ||
|
|
fb74504282 | ||
|
|
cb758ec012 | ||
|
|
02835c18e9 | ||
|
|
f83492e683 | ||
|
|
074a3b6f2a | ||
|
|
d738210cb5 | ||
|
|
cfeafb2d30 | ||
|
|
49d29ac7cc | ||
|
|
372b71b08b | ||
|
|
0c5365da1f | ||
|
|
58ad025e61 | ||
|
|
6c41288a7b | ||
|
|
b8f0fa49cf | ||
|
|
ee2fdab279 | ||
|
|
fbd2e0e844 | ||
|
|
165efa69cc | ||
|
|
2790f65a5f | ||
|
|
37a9b0557a | ||
|
|
42a828e98b | ||
|
|
a124e00766 |
23
.github/ISSUE_TEMPLATE/bug-反馈.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
name: Bug 反馈
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: "[BUG]"
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 复现步骤
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 预期行为
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 部署方式
|
||||||
|
|
||||||
|
- [ ] cli 部署
|
||||||
|
- [ ] 用户界面部署
|
||||||
|
|
||||||
|
## 浏览器环境
|
||||||
16
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
name: Feature Request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: "[Feature]"
|
||||||
|
labels: enhancement, good first issue
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 请描述您的 Feature
|
||||||
|
|
||||||
|
## 描述您想要的解决方案
|
||||||
|
|
||||||
|
## 描述您考虑过的替代方案
|
||||||
|
|
||||||
|
## 附加上下文
|
||||||
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 }}
|
||||||
48
.github/workflows/docs_deploy.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: Deploy Docs
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "vitepress-docs/**"
|
||||||
|
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 }}
|
||||||
48
.github/workflows/smtp_proxy_server.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
name: SMTP Proxy Server Docker Image CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "smtp_proxy_server/**"
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: ghcr.io
|
||||||
|
IMAGE_NAME: smtp_proxy_server
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push Docker images
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./smtp_proxy_server
|
||||||
|
file: ./smtp_proxy_server/dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
|
||||||
|
${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE_NAME }}:latest
|
||||||
46
.github/workflows/tag_build.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: Tag Build CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
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: Build Frontend
|
||||||
|
run: cd frontend && pnpm install --no-frozen-lockfile && pnpm build:release
|
||||||
|
|
||||||
|
- name: Zip Frontend dist
|
||||||
|
run: cd frontend/dist/ && zip -r frontend.zip *
|
||||||
|
|
||||||
|
- name: cp wrangler.toml
|
||||||
|
run: cd worker && cp wrangler.toml.template wrangler.toml
|
||||||
|
|
||||||
|
- name: Build Backend
|
||||||
|
run: cd worker && pnpm install --no-frozen-lockfile && pnpm build
|
||||||
|
|
||||||
|
- name: Upload to Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
frontend/dist/frontend.zip
|
||||||
|
worker/dist/worker.js
|
||||||
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
dist/
|
dist/
|
||||||
test/
|
test/
|
||||||
|
.vscode/
|
||||||
|
|||||||
34
CHANGELOG
@@ -1,34 +0,0 @@
|
|||||||
# CHANGE LOG
|
|
||||||
|
|
||||||
## 2024-04-10 v0.0.1
|
|
||||||
|
|
||||||
Breaking changes:
|
|
||||||
|
|
||||||
- remove `ENABLE_ATTACHMENT` config
|
|
||||||
- use rust wasm to parse email in frontend
|
|
||||||
- deprecated api moved to `/api/v1`
|
|
||||||
|
|
||||||
DB changes
|
|
||||||
|
|
||||||
- `db/2024-04-09-patch.sql`
|
|
||||||
|
|
||||||
## 2024-04-09 v0.0.0
|
|
||||||
|
|
||||||
release v0.0.0
|
|
||||||
|
|
||||||
## 2024-04-03
|
|
||||||
|
|
||||||
DB changes
|
|
||||||
|
|
||||||
- `db/2024-04-03-patch.sql`
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
- add delete account
|
|
||||||
- add admin panel search
|
|
||||||
|
|
||||||
## 2024-01-13
|
|
||||||
|
|
||||||
DB changes
|
|
||||||
|
|
||||||
- `db/2024-01-13-patch.sql`
|
|
||||||
166
CHANGELOG.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# CHANGE LOG
|
||||||
|
|
||||||
|
## main branch to be released
|
||||||
|
|
||||||
|
### DB Changes
|
||||||
|
|
||||||
|
新增 `settings` 表,用于存储通用配置信息
|
||||||
|
|
||||||
|
- `db/2024-05-01-patch.sql`
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- `ENABLE_USER_CREATE_EMAIL` 是否允许用户创建邮件
|
||||||
|
- 允许 admin 创建无前缀的邮件
|
||||||
|
- 添加 `SMTP proxy server`,支持 SMTP 发送邮件
|
||||||
|
- 修复某些情况浏览器无法加载 `wasm` 时使用 js 解析邮件
|
||||||
|
- 页脚添加 `COPYRIGHT`
|
||||||
|
- UI 允许用户切换邮件展示模式 `v-html` / `iframe`
|
||||||
|
- 添加 `admin` 账户配置页面,支持配置用户注册名称黑名单
|
||||||
|
|
||||||
|
## v0.3.0
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
`address` 表的前缀将从代码中迁移到 db 中,请将下面 sql 中的 `tmp` 替换为你的前缀,然后执行。
|
||||||
|
如果你的数据很重要,请先备份数据库。
|
||||||
|
|
||||||
|
**注意替换前缀**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
update
|
||||||
|
address
|
||||||
|
set
|
||||||
|
name = 'tmp' || name;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- `address` 表的前缀将从代码中迁移到 db 中
|
||||||
|
- `admin` 账户页面添加收发邮件数量
|
||||||
|
- `admin` 发件页面默认显示全部
|
||||||
|
- `admin` 发件权限页面支持搜索地址
|
||||||
|
- `admin` 邮件页面使用左右分栏 UI
|
||||||
|
|
||||||
|
* feat: remove PREFIX logic in db by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/171
|
||||||
|
* feat: admin page add account mail count && sendbox default all && sen… by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/172
|
||||||
|
* feat: all mail use MailBox Component by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/173
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/dreamhunter2333/cloudflare_temp_email/compare/0.2.10...v0.3.0
|
||||||
|
|
||||||
|
## v0.2.10
|
||||||
|
|
||||||
|
- `ENABLE_USER_DELETE_EMAIL` 是否允许用户删除账户和邮件
|
||||||
|
- `ENABLE_AUTO_REPLY` 是否启用自动回复
|
||||||
|
- fetchAddressError 提示改进
|
||||||
|
- 自动刷新显示倒计时
|
||||||
|
|
||||||
|
* feat: docs update by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/165
|
||||||
|
* feat: add ENABLE_USER_DELETE_EMAIL && ENABLE_AUTO_REPLY && modify fetchAddressError i18n && UI: show autoRefreshInterval by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/169
|
||||||
|
|
||||||
|
## v0.2.9
|
||||||
|
|
||||||
|
- 添加富文本编辑器
|
||||||
|
- admin 联系方式,不配置则不显示,可配置任意字符串 `ADMIN_CONTACT = "xx@xx.xxx"`
|
||||||
|
- 默认发送邮件余额,如果不设置,将为 0 `DEFAULT_SEND_BALANCE = 1`
|
||||||
|
|
||||||
|
## v0.2.8
|
||||||
|
|
||||||
|
- 允许用户删除邮件
|
||||||
|
- admin 修改发件权限时邮件通知用户
|
||||||
|
- 发件权限默认 1 条
|
||||||
|
- 添加 RATE_LIMITER 限流 发送邮件 和 新建地址
|
||||||
|
- 一些 bug 修复
|
||||||
|
|
||||||
|
---
|
||||||
|
- feat: allow user delete mail && notify when send access changed by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/132
|
||||||
|
- feat: requset_send_mail_access default 1 balance by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/143
|
||||||
|
- fix: RATE_LIMITER not call jwt by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/146
|
||||||
|
- fix: delete_address not delete address_sender by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/153
|
||||||
|
- fix: send_balance not update when click sendmail by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/155
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
DB changes:
|
||||||
|
|
||||||
|
- `db/2024-04-12-patch.sql`
|
||||||
|
|
||||||
|
## 2024-04-10 v0.2.0
|
||||||
|
|
||||||
|
### Breaking Changes
|
||||||
|
|
||||||
|
- remove `ENABLE_ATTACHMENT` config
|
||||||
|
- use rust wasm to parse email in frontend
|
||||||
|
- deprecated api moved to `/api/v1`
|
||||||
|
|
||||||
|
### Rust Mail Parser
|
||||||
|
|
||||||
|
由于 nodejs 解析 email 的库有些问题,此版本切换到使用 rust wasm 调用 rust 的mail 解析库
|
||||||
|
|
||||||
|
- 速度更快,附件支持好,可以显示邮件的附件图片
|
||||||
|
- 解析支持更多 rfc 规范
|
||||||
|
|
||||||
|
Due to some problems with nodejs' email parsing library, this version switches to using rust wasm to call rust's mail parsing library.
|
||||||
|
|
||||||
|
- Faster speed, good attachment support, can display attachment images of emails
|
||||||
|
- Parsing supports more rfc specifications
|
||||||
|
|
||||||
|
### DB changs
|
||||||
|
|
||||||
|
将 `mails` 表废弃,新的 `mail` 的 `raw` 文本将直接存入 `raw_mails` 表.
|
||||||
|
The `mails` table will be discarded, and the `raw` text of the new `mail` will be directly stored in the `raw_mails` table
|
||||||
|
|
||||||
|
## Upgrade Step
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout v0.2.0
|
||||||
|
cd worker
|
||||||
|
wrangler d1 execute dev --file=../db/2024-04-09-patch.sql
|
||||||
|
pnpm run deploy
|
||||||
|
cd ../frontend
|
||||||
|
pnpm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:对于历史邮件,请使用部署新网页查看旧数据。
|
||||||
|
Note: For historical messages, use the Deploy New web page to view old data.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout feature/backup
|
||||||
|
cd frontend
|
||||||
|
# 创建一个新的 pages, 用于访问旧数据
|
||||||
|
pnpm run deploy --project-name temp-email-v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2024-04-09 v0.0.0
|
||||||
|
|
||||||
|
release v0.0.0
|
||||||
|
|
||||||
|
## 2024-04-03
|
||||||
|
|
||||||
|
DB changes
|
||||||
|
|
||||||
|
- `db/2024-04-03-patch.sql`
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- add delete account
|
||||||
|
- add admin panel search
|
||||||
|
|
||||||
|
## 2024-01-13
|
||||||
|
|
||||||
|
DB changes
|
||||||
|
|
||||||
|
- `db/2024-01-13-patch.sql`
|
||||||
203
README.md
@@ -1,24 +1,17 @@
|
|||||||
# 使用 cloudflare 免费服务,搭建临时邮箱
|
# 使用 cloudflare 免费服务,搭建临时邮箱
|
||||||
|
|
||||||
## [English](README_EN.md)
|
## [查看部署文档](https://temp-mail-docs.awsl.uk)
|
||||||
|
|
||||||
[CHANGELOG](CHANGELOG)
|
## [English Docs](https://temp-mail-docs.awsl.uk/en/)
|
||||||
|
|
||||||
[Backend](https://temp-email-api.dreamhunter2333.xyz/)
|
## [CHANGELOG](CHANGELOG.md)
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
[Frontend](https://temp-email.dreamhunter2333.xyz/)
|
## [在线演示](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>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=dreamhunter2333/cloudflare_temp_email&type=Date&theme=dark" />
|
||||||
@@ -27,172 +20,30 @@
|
|||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
|
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
|
||||||
- [English](#english)
|
- [查看部署文档](#查看部署文档)
|
||||||
|
- [English Docs](#english-docs)
|
||||||
|
- [CHANGELOG](#changelog)
|
||||||
- [在线演示](#在线演示)
|
- [在线演示](#在线演示)
|
||||||
- [功能/TODO](#功能todo)
|
- [功能/TODO](#功能todo)
|
||||||
- [什么是临时邮箱](#什么是临时邮箱)
|
- [Reference](#reference)
|
||||||
- [Cloudflare 服务](#cloudflare-服务)
|
|
||||||
- [wrangler 的安装](#wrangler-的安装)
|
|
||||||
- [D1 数据库](#d1-数据库)
|
|
||||||
- [Cloudflare workers 后端](#cloudflare-workers-后端)
|
|
||||||
- [Cloudflare Workers 后端](#cloudflare-workers-后端-1)
|
|
||||||
- [Cloudflare Email Routing](#cloudflare-email-routing)
|
|
||||||
- [Cloudflare Pages 前端](#cloudflare-pages-前端)
|
|
||||||
- [参考资料](#参考资料)
|
|
||||||
|
|
||||||
## [在线演示](https://temp-email.dreamhunter2333.xyz/)
|
|
||||||
|
|
||||||
## 功能/TODO
|
## 功能/TODO
|
||||||
|
|
||||||
- [x] Cloudflare D1 作为数据库
|
- [x] 使用 `password` 重新登录之前的邮箱
|
||||||
- [x] 使用 Cloudflare Pages 部署前端
|
- [x] 获取自定义名字的邮箱,`admin` 可配置黑名单
|
||||||
- [x] 使用 Cloudflare Workers 部署后端
|
|
||||||
- [x] email 转发使用 Cloudflare Email Routing
|
|
||||||
- [x] 使用 password 重新登录之前的邮箱
|
|
||||||
- [x] 获取自定义名字的邮箱
|
|
||||||
- [x] 支持多语言
|
- [x] 支持多语言
|
||||||
- [x] 增加访问授权,可作为私人站点
|
- [x] 增加访问密码,可作为私人站点
|
||||||
- [x] 增加自动回复功能
|
- [x] 增加自动回复功能
|
||||||
- [x] 增加查看附件功能
|
- [x] 增加查看 `附件` 功能
|
||||||
- [x] 使用 rust wasm 解析邮件
|
- [x] 使用 `rust wasm` 解析邮件
|
||||||
|
- [x] 支持发送邮件
|
||||||
|
- [x] 支持 `DKIM`
|
||||||
|
- [x] `admin` 后台创建无前缀邮箱
|
||||||
|
- [x] 添加 `SMTP proxy server`,支持 SMTP 发送邮件
|
||||||
|
|
||||||
---
|
## Reference
|
||||||
|
|
||||||
## 什么是临时邮箱
|
- Cloudflare D1 作为数据库
|
||||||
|
- 使用 Cloudflare Pages 部署前端
|
||||||
临时邮箱,也被称为一次性邮箱或临时邮件地址,是一种用于临时接收邮件的虚拟邮箱。与常规邮箱不同,临时邮箱旨在提供一种匿名且临时的邮件接收解决方案。
|
- 使用 Cloudflare Workers 部署后端
|
||||||
|
- email 转发使用 Cloudflare Email Routing
|
||||||
临时邮箱往往由网站或在线服务提供商提供,用户可以在需要注册或接收验证邮件时使用临时邮箱地址,而无需暴露自己的真实邮箱地址。这样做的好处是可以保护个人隐私
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cloudflare 服务
|
|
||||||
|
|
||||||
- `D1` 是 `Cloudflare` 的原生无服务器数据库。
|
|
||||||
- `Pages` 是 `Cloudflare` 的静态网站托管服务, 速度超快,始终保持最新状态。
|
|
||||||
- `Workers` 是 `Cloudflare` 的 `serverless` 应用服务,可以在全球 300 个数据中心运行代码, 而无需配置或维护基础架构。
|
|
||||||
- `Cloudflare Email Routing` 可以处理域名的所有电子邮件流量,而无需管理电子邮件服务器。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## wrangler 的安装
|
|
||||||
|
|
||||||
安装 wrangler
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install wrangler -g
|
|
||||||
```
|
|
||||||
|
|
||||||
克隆项目
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/dreamhunter2333/cloudflare_temp_email.git
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## D1 数据库
|
|
||||||
|
|
||||||
第一次执行登录 wrangler 命令时,会提示登录, 按提示操作即可
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 创建 D1 并执行 schema.sql
|
|
||||||
wrangler d1 create dev
|
|
||||||
wrangler d1 execute dev --file=db/schema.sql
|
|
||||||
# schema 更新,如果你在此日期之前初始化过数据库,可以执行此命令更新
|
|
||||||
# wrangler d1 execute dev --file=db/2024-01-13-patch.sql
|
|
||||||
# wrangler d1 execute dev --file=db/2024-04-03-patch.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cloudflare workers 后端
|
|
||||||
|
|
||||||
初始化项目
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd worker
|
|
||||||
pnpm install
|
|
||||||
cp wrangler.toml.template wrangler.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
修改 `wrangler.toml` 文件
|
|
||||||
|
|
||||||
```bash
|
|
||||||
name = "cloudflare_temp_email"
|
|
||||||
main = "src/worker.js"
|
|
||||||
compatibility_date = "2023-08-14"
|
|
||||||
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 = "" # 黑名单,用于过滤发件人,逗号分隔
|
|
||||||
|
|
||||||
[[d1_databases]]
|
|
||||||
binding = "DB"
|
|
||||||
database_name = "xxx" # D1 数据库名称
|
|
||||||
database_id = "xxx" # D1 数据库 ID
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cloudflare Workers 后端
|
|
||||||
|
|
||||||
部署
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm run deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
部署成功之后再路由中可以看到 `worker` 的 `url`,控制台也会输出 `worker` 的 `url`
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cloudflare Email Routing
|
|
||||||
|
|
||||||
配置对应域名的 `电子邮件 DNS 记录`
|
|
||||||
|
|
||||||
配置 `Cloudflare Email Routing` catch-all 发送到 `worker`
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cloudflare Pages 前端
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
pnpm install
|
|
||||||
cp .env.example .env.local
|
|
||||||
```
|
|
||||||
|
|
||||||
修改 `.env.local` 文件, 将 `VITE_API_BASE` 修改为 `worker` 的 `url`, 不要在末尾加 `/`
|
|
||||||
|
|
||||||
例如: `VITE_API_BASE=https://xxx.xxx.workers.dev`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm build --emptyOutDir
|
|
||||||
# 根据提示创建 pages
|
|
||||||
pnpm run deploy
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 参考资料
|
|
||||||
|
|
||||||
- https://developers.cloudflare.com/d1/
|
|
||||||
- https://developers.cloudflare.com/pages/
|
|
||||||
- https://developers.cloudflare.com/workers/
|
|
||||||
- https://developers.cloudflare.com/email-routing/
|
|
||||||
|
|||||||
84
README_EN.md
@@ -1,84 +0,0 @@
|
|||||||
# cloudflare temp email
|
|
||||||
|
|
||||||
## [中文](README.md)
|
|
||||||
|
|
||||||
[CHANGELOG](CHANGELOG)
|
|
||||||
|
|
||||||
## [Live Demo](https://temp-email.dreamhunter2333.xyz/)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
- [x] Cloudflare D1 as a database
|
|
||||||
- [x] Deploy the front end with Cloudflare Pages
|
|
||||||
- [x] Deploy the backend with Cloudflare Workers
|
|
||||||
- [x] Email forwarding using Cloudflare Email Routing
|
|
||||||
- [x] Use password to login to the previous mailbox again.
|
|
||||||
- [x] Get Custom Name Email
|
|
||||||
- [x] Support multiple languages
|
|
||||||
- [x] Add access authorization, which can be used as a private site
|
|
||||||
- [x] Add auto reply feature
|
|
||||||
- [x] Add attachment viewing function
|
|
||||||
- [x] use rust wasm to parse email
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Deploy
|
|
||||||
|
|
||||||
[Install/Update Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/)
|
|
||||||
|
|
||||||
## DB - Cloudflare D1
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# create a database, and copy the output to wrangler.toml in the next step
|
|
||||||
wrangler d1 create dev
|
|
||||||
wrangler d1 execute dev --file=db/schema.sql
|
|
||||||
# schema update, if you have initialized the database before this date, you can execute this command to update
|
|
||||||
# wrangler d1 execute dev --file=db/2024-01-13-patch.sql
|
|
||||||
# wrangler d1 execute dev --file=db/2024-04-03-patch.sql
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Backend - Cloudflare workers
|
|
||||||
|
|
||||||
```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
|
|
||||||
```
|
|
||||||
|
|
||||||
you can find and test the worker's url in the workers dashboard
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
enable email route and config email forward catch-all to the worker
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Frontend - Cloudflare pages
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
pnpm install
|
|
||||||
# add .env.local and modify VITE_API_BASE to your worker's url
|
|
||||||
# VITE_API_BASE=https://xxx.xxx.workers.dev - don't put / in the end
|
|
||||||
cp .env.example .env.local
|
|
||||||
pnpm build --emptyOutDir
|
|
||||||
pnpm run deploy
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
14
db/2024-04-12-patch.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS address_sender (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
address TEXT UNIQUE,
|
||||||
|
balance INTEGER DEFAULT 0,
|
||||||
|
enabled INTEGER DEFAULT 1,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sendbox (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
address TEXT,
|
||||||
|
raw TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
6
db/2024-05-01-patch.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
@@ -8,6 +8,8 @@ CREATE TABLE IF NOT EXISTS mails (
|
|||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mails_address ON mails(address);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS raw_mails (
|
CREATE TABLE IF NOT EXISTS raw_mails (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
message_id TEXT,
|
message_id TEXT,
|
||||||
@@ -17,6 +19,8 @@ CREATE TABLE IF NOT EXISTS raw_mails (
|
|||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS address (
|
CREATE TABLE IF NOT EXISTS address (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
name TEXT UNIQUE,
|
name TEXT UNIQUE,
|
||||||
@@ -24,6 +28,8 @@ CREATE TABLE IF NOT EXISTS address (
|
|||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
source_prefix TEXT,
|
source_prefix TEXT,
|
||||||
@@ -35,6 +41,8 @@ CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
|||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_auto_reply_mails_address ON auto_reply_mails(address);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS attachments (
|
CREATE TABLE IF NOT EXISTS attachments (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
source TEXT,
|
source TEXT,
|
||||||
@@ -43,3 +51,29 @@ CREATE TABLE IF NOT EXISTS attachments (
|
|||||||
data TEXT,
|
data TEXT,
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS address_sender (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
address TEXT UNIQUE,
|
||||||
|
balance INTEGER DEFAULT 0,
|
||||||
|
enabled INTEGER DEFAULT 1,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_address_sender_address ON address_sender(address);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sendbox (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
address TEXT,
|
||||||
|
raw TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_sendbox_address ON sendbox(address);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|||||||
1
frontend/.gitignore
vendored
@@ -29,3 +29,4 @@ coverage
|
|||||||
|
|
||||||
.env.*
|
.env.*
|
||||||
*-dist/
|
*-dist/
|
||||||
|
components.d.ts
|
||||||
|
|||||||
@@ -6,31 +6,36 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build -m prod --emptyOutDir",
|
"build": "vite build -m prod --emptyOutDir",
|
||||||
|
"build:release": "vite build -m example --emptyOutDir",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"deploy": "npm run build && wrangler pages deploy ../dist --branch production"
|
"deploy:preview": "npm run build && wrangler pages deploy ./dist --branch preview",
|
||||||
|
"deploy": "npm run build && wrangler pages deploy ./dist --branch production"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vicons/material": "^0.12.0",
|
"@vicons/material": "^0.12.0",
|
||||||
"@vueuse/core": "^10.9.0",
|
"@vueuse/core": "^10.9.0",
|
||||||
|
"@wangeditor/editor": "^5.1.23",
|
||||||
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"mail-parser-wasm": "^0.1.6",
|
"mail-parser-wasm": "^0.1.6",
|
||||||
"naive-ui": "^2.38.1",
|
"naive-ui": "^2.38.1",
|
||||||
"postal-mime": "^2.2.1",
|
"postal-mime": "^2.2.5",
|
||||||
"vooks": "^0.2.12",
|
"vooks": "^0.2.12",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.25",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-i18n": "^9.10.2",
|
"vue-i18n": "^9.13.1",
|
||||||
"vue-router": "^4.3.0"
|
"vue-router": "^4.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vicons/fa": "^0.12.0",
|
"@vicons/fa": "^0.12.0",
|
||||||
"@vitejs/plugin-vue": "^4.6.2",
|
"@vitejs/plugin-vue": "^4.6.2",
|
||||||
"unplugin-auto-import": "^0.17.5",
|
"unplugin-auto-import": "^0.17.5",
|
||||||
"unplugin-vue-components": "^0.26.0",
|
"unplugin-vue-components": "^0.26.0",
|
||||||
"vite": "^5.2.6",
|
"vite": "^5.2.10",
|
||||||
"vite-plugin-pwa": "^0.19.7",
|
"vite-plugin-pwa": "^0.19.8",
|
||||||
"vite-plugin-top-level-await": "^1.4.1",
|
"vite-plugin-top-level-await": "^1.4.1",
|
||||||
"vite-plugin-wasm": "^3.3.0",
|
"vite-plugin-wasm": "^3.3.0",
|
||||||
"workbox-window": "^7.0.0"
|
"workbox-window": "^7.1.0",
|
||||||
|
"wrangler": "^3.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6972
frontend/pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 5.7 KiB |
@@ -1,14 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { darkTheme, NGlobalStyle } from 'naive-ui'
|
import { darkTheme, NGlobalStyle, zhCN } from 'naive-ui'
|
||||||
import { zhCN } from 'naive-ui'
|
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useGlobalState } from './store'
|
import { useGlobalState } from './store'
|
||||||
import { useIsMobile } from './utils/composables'
|
import { useIsMobile } from './utils/composables'
|
||||||
import Header from './views/Header.vue';
|
import Header from './views/Header.vue';
|
||||||
|
import Footer from './views/Footer.vue';
|
||||||
|
|
||||||
const { localeCache, themeSwitch, loading } = useGlobalState()
|
|
||||||
const theme = computed(() => themeSwitch.value ? darkTheme : null)
|
const { localeCache, isDark, loading } = useGlobalState()
|
||||||
|
const theme = computed(() => isDark.value ? darkTheme : null)
|
||||||
const localeConfig = computed(() => localeCache.value == 'zh' ? zhCN : null)
|
const localeConfig = computed(() => localeCache.value == 'zh' ? zhCN : null)
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
@@ -42,8 +43,11 @@ onMounted(async () => {
|
|||||||
<n-gi :span="isMobile ? 12 : 10">
|
<n-gi :span="isMobile ? 12 : 10">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<n-space vertical>
|
<n-space vertical>
|
||||||
<Header />
|
<n-layout style="min-height: 80vh;">
|
||||||
<router-view></router-view>
|
<Header />
|
||||||
|
<router-view></router-view>
|
||||||
|
</n-layout>
|
||||||
|
<Footer />
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const apiFetch = async (path, options = {}) => {
|
|||||||
const getOpenSettings = async (message) => {
|
const getOpenSettings = async (message) => {
|
||||||
try {
|
try {
|
||||||
const res = await api.fetch("/open_api/settings");
|
const res = await api.fetch("/open_api/settings");
|
||||||
openSettings.value = {
|
Object.assign(openSettings.value, {
|
||||||
prefix: res["prefix"] || "",
|
prefix: res["prefix"] || "",
|
||||||
needAuth: res["needAuth"] || false,
|
needAuth: res["needAuth"] || false,
|
||||||
domains: res["domains"].map((domain) => {
|
domains: res["domains"].map((domain) => {
|
||||||
@@ -57,8 +57,13 @@ const getOpenSettings = async (message) => {
|
|||||||
label: domain,
|
label: domain,
|
||||||
value: domain
|
value: domain
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
};
|
adminContact: res["adminContact"] || "",
|
||||||
|
enableUserCreateEmail: res["enableUserCreateEmail"] || false,
|
||||||
|
enableUserDeleteEmail: res["enableUserDeleteEmail"] || false,
|
||||||
|
enableAutoReply: res["enableAutoReply"] || false,
|
||||||
|
copyright: res["copyright"] || openSettings.value.copyright,
|
||||||
|
});
|
||||||
if (openSettings.value.needAuth) {
|
if (openSettings.value.needAuth) {
|
||||||
showAuth.value = true;
|
showAuth.value = true;
|
||||||
}
|
}
|
||||||
@@ -77,6 +82,7 @@ const getSettings = async () => {
|
|||||||
address: res["address"],
|
address: res["address"],
|
||||||
auto_reply: res["auto_reply"],
|
auto_reply: res["auto_reply"],
|
||||||
has_v1_mails: res["has_v1_mails"],
|
has_v1_mails: res["has_v1_mails"],
|
||||||
|
send_balance: res["send_balance"],
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
settings.value.fetched = true;
|
settings.value.fetched = true;
|
||||||
|
|||||||
378
frontend/src/components/MailBox.vue
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
<script setup>
|
||||||
|
import { watch, onMounted, ref, onBeforeUnmount } from "vue";
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useGlobalState } from '../store'
|
||||||
|
import { CloudDownloadRound } from '@vicons/material'
|
||||||
|
import { useIsMobile } from '../utils/composables'
|
||||||
|
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
enableUserDeleteEmail: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
requried: false
|
||||||
|
},
|
||||||
|
showEMailTo: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
requried: false
|
||||||
|
},
|
||||||
|
fetchMailData: {
|
||||||
|
type: Function,
|
||||||
|
default: () => { },
|
||||||
|
requried: true
|
||||||
|
},
|
||||||
|
deleteMail: {
|
||||||
|
type: Function,
|
||||||
|
default: () => { },
|
||||||
|
requried: false
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const { themeSwitch, mailboxSplitSize, useIframeShowMail } = useGlobalState()
|
||||||
|
const autoRefresh = ref(false)
|
||||||
|
const autoRefreshInterval = ref(30)
|
||||||
|
const data = ref([])
|
||||||
|
const timer = ref(null)
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
|
||||||
|
const showAttachments = ref(false)
|
||||||
|
const curAttachments = ref([])
|
||||||
|
const curMail = ref(null);
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
success: 'Success',
|
||||||
|
autoRefresh: 'Auto Refresh',
|
||||||
|
refreshAfter: 'Refresh After {msg} Seconds',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
attachments: 'Show Attachments',
|
||||||
|
downloadMail: 'Download Mail',
|
||||||
|
pleaseSelectMail: "Please select a mail to view.",
|
||||||
|
delete: 'Delete',
|
||||||
|
deleteMailTip: 'Are you sure you want to delete this mail?'
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
success: '成功',
|
||||||
|
autoRefresh: '自动刷新',
|
||||||
|
refreshAfter: '{msg}秒后刷新',
|
||||||
|
refresh: '刷新',
|
||||||
|
downloadMail: '下载邮件',
|
||||||
|
attachments: '查看附件',
|
||||||
|
pleaseSelectMail: "请选择一封邮件查看。",
|
||||||
|
delete: '删除',
|
||||||
|
deleteMailTip: '确定要删除这封邮件吗?'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupAutoRefresh = async (autoRefresh) => {
|
||||||
|
// auto refresh every 30 seconds
|
||||||
|
autoRefreshInterval.value = 30;
|
||||||
|
if (autoRefresh) {
|
||||||
|
timer.value = setInterval(async () => {
|
||||||
|
autoRefreshInterval.value--;
|
||||||
|
if (autoRefreshInterval.value <= 0) {
|
||||||
|
autoRefreshInterval.value = 30;
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(autoRefresh, async (autoRefresh, old) => {
|
||||||
|
setupAutoRefresh(autoRefresh)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
|
||||||
|
if (page !== oldPage || pageSize !== oldPageSize) {
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
try {
|
||||||
|
const { results, count: totalCount } = await props.fetchMailData(
|
||||||
|
pageSize.value, (page.value - 1) * pageSize.value
|
||||||
|
);
|
||||||
|
data.value = await Promise.all(results.map(async (item) => {
|
||||||
|
return await processItem(item);
|
||||||
|
}));
|
||||||
|
if (totalCount > 0) {
|
||||||
|
count.value = totalCount;
|
||||||
|
}
|
||||||
|
if (!isMobile.value && !curMail.value && data.value.length > 0) {
|
||||||
|
curMail.value = data.value[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clickRow = async (row) => {
|
||||||
|
curMail.value = row;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAttachments = (attachments) => {
|
||||||
|
curAttachments.value = attachments;
|
||||||
|
showAttachments.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 props.deleteMail(curMail.value.id);
|
||||||
|
message.success(t("success"));
|
||||||
|
curMail.value = null;
|
||||||
|
await refresh();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSpiltSizeChange = (size) => {
|
||||||
|
mailboxSplitSize.value = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25"
|
||||||
|
:default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange">
|
||||||
|
<template #1>
|
||||||
|
<div class="center">
|
||||||
|
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
||||||
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||||
|
</div>
|
||||||
|
<n-switch v-model:value="autoRefresh" size="small" :round="false">
|
||||||
|
<template #checked>
|
||||||
|
{{ t('refreshAfter', { msg: autoRefreshInterval }) }}
|
||||||
|
</template>
|
||||||
|
<template #unchecked>
|
||||||
|
{{ t('autoRefresh') }}
|
||||||
|
</template>
|
||||||
|
</n-switch>
|
||||||
|
<n-button @click="refresh" size="small" type="primary">
|
||||||
|
{{ t('refresh') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div style="overflow: auto; height: 80vh;">
|
||||||
|
<n-list hoverable clickable>
|
||||||
|
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||||
|
:class="mailItemClass(row)">
|
||||||
|
<n-thing :title="row.subject">
|
||||||
|
<template #description>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ row.id }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ row.created_at }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
FROM: {{ row.source }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag v-if="showEMailTo" type="info">
|
||||||
|
TO: {{ row.address }}
|
||||||
|
</n-tag>
|
||||||
|
</template>
|
||||||
|
</n-thing>
|
||||||
|
</n-list-item>
|
||||||
|
</n-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #2>
|
||||||
|
<n-card v-if="curMail" class="mail-item" :title="curMail.subject" style="overflow: auto; max-height: 100vh;">
|
||||||
|
<n-space>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ curMail.id }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ curMail.created_at }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
FROM: {{ curMail.source }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag v-if="showEMailTo" type="info">
|
||||||
|
TO: {{ curMail.address }}
|
||||||
|
</n-tag>
|
||||||
|
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t('deleteMailTip') }}
|
||||||
|
</n-popconfirm>
|
||||||
|
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
||||||
|
@click="getAttachments(curMail.attachments)">
|
||||||
|
{{ t('attachments') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
||||||
|
:href="getDownloadEmlUrl(curMail.raw)">
|
||||||
|
<n-icon :component="CloudDownloadRound" />
|
||||||
|
{{ t('downloadMail') }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
<iframe v-if="useIframeShowMail" :srcdoc="curMail.message"
|
||||||
|
style="margin-top: 10px;width: 100%; height: 100%;">
|
||||||
|
</iframe>
|
||||||
|
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
|
||||||
|
</n-card>
|
||||||
|
<n-card class="mail-item" v-else>
|
||||||
|
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||||
|
</n-result>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
</n-split>
|
||||||
|
<div class="left" v-else>
|
||||||
|
<div class="center">
|
||||||
|
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
||||||
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||||
|
</div>
|
||||||
|
<n-switch v-model:value="autoRefresh" size="small">
|
||||||
|
<template #checked>
|
||||||
|
{{ t('autoRefresh') }}
|
||||||
|
</template>
|
||||||
|
<template #unchecked>
|
||||||
|
{{ t('autoRefresh') }}
|
||||||
|
</template></n-switch>
|
||||||
|
<n-button @click="refresh" size="small" type="primary">
|
||||||
|
{{ t('refresh') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div style="overflow: auto; height: 80vh;">
|
||||||
|
<n-list hoverable clickable>
|
||||||
|
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)">
|
||||||
|
<n-thing :title="row.subject">
|
||||||
|
<template #description>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ row.id }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ row.created_at }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
FROM: {{ row.source }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag v-if="showEMailTo" type="info">
|
||||||
|
TO: {{ row.address }}
|
||||||
|
</n-tag>
|
||||||
|
</template>
|
||||||
|
</n-thing>
|
||||||
|
</n-list-item>
|
||||||
|
</n-list>
|
||||||
|
</div>
|
||||||
|
<n-drawer v-model:show="curMail" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
|
||||||
|
style="height: 80vh;">
|
||||||
|
<n-drawer-content :title="curMail ? curMail.subject : ''" closable>
|
||||||
|
<n-card style="overflow: auto;">
|
||||||
|
<n-space>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ curMail.id }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ curMail.created_at }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
FROM: {{ curMail.source }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag v-if="showEMailTo" type="info">
|
||||||
|
TO: {{ curMail.address }}
|
||||||
|
</n-tag>
|
||||||
|
<n-popconfirm v-if="enableUserDeleteEmail" @positive-click="deleteMail">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
|
||||||
|
</template>
|
||||||
|
{{ t('deleteMailTip') }}
|
||||||
|
</n-popconfirm>
|
||||||
|
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
||||||
|
@click="getAttachments(curMail.attachments)">
|
||||||
|
{{ t('attachments') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
||||||
|
:href="getDownloadEmlUrl(curMail)">
|
||||||
|
<n-icon :component="CloudDownloadRound" />
|
||||||
|
{{ t('downloadMail') }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
<div v-html="curMail.message" style="margin-top: 10px;"></div>
|
||||||
|
</n-card>
|
||||||
|
</n-drawer-content>
|
||||||
|
</n-drawer>
|
||||||
|
</div>
|
||||||
|
<n-modal v-model:show="showAttachments" preset="dialog" title="Dialog">
|
||||||
|
<template #header>
|
||||||
|
<div>{{ t("attachments") }}</div>
|
||||||
|
</template>
|
||||||
|
<n-list hoverable clickable>
|
||||||
|
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
|
||||||
|
<n-thing class="center" :title="row.filename">
|
||||||
|
<template #description>
|
||||||
|
<n-space>
|
||||||
|
<n-tag type="info">
|
||||||
|
Size: {{ row.size }}
|
||||||
|
</n-tag>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-thing>
|
||||||
|
<template #suffix>
|
||||||
|
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
|
||||||
|
:href="row.url">
|
||||||
|
<n-icon :component="CloudDownloadRound" />
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-list-item>
|
||||||
|
</n-list>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-dark-backgroud {
|
||||||
|
background-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-light-backgroud {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-item {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,7 +12,7 @@ const i18n = createI18n({
|
|||||||
'en': {
|
'en': {
|
||||||
messages: {}
|
messages: {}
|
||||||
},
|
},
|
||||||
'zhCN': {
|
'zh': {
|
||||||
messages: {}
|
messages: {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Index from '../views/Index.vue'
|
import Index from '../views/Index.vue'
|
||||||
import Settings from '../views/Settings.vue'
|
import User from '../views/User.vue'
|
||||||
|
import SendMail from '../views/send/SendMail.vue'
|
||||||
import Admin from '../views/Admin.vue'
|
import Admin from '../views/Admin.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -11,8 +12,12 @@ const router = createRouter({
|
|||||||
component: Index
|
component: Index
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/user',
|
||||||
component: Settings
|
component: User
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/send',
|
||||||
|
component: SendMail
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { createGlobalState, useStorage } from '@vueuse/core'
|
import { createGlobalState, useStorage } from '@vueuse/core'
|
||||||
|
import { useDark, useToggle } from '@vueuse/core'
|
||||||
|
|
||||||
export const useGlobalState = createGlobalState(
|
export const useGlobalState = createGlobalState(
|
||||||
() => {
|
() => {
|
||||||
|
const isDark = useDark()
|
||||||
|
const toggleDark = useToggle(isDark)
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const openSettings = ref({
|
const openSettings = ref({
|
||||||
prefix: '',
|
prefix: '',
|
||||||
needAuth: false,
|
needAuth: false,
|
||||||
domains: [{
|
adminContact: '',
|
||||||
label: 'test.com',
|
enableUserCreateEmail: false,
|
||||||
value: 'test.com'
|
enableUserDeleteEmail: false,
|
||||||
}]
|
enableAutoReply: false,
|
||||||
|
domains: [],
|
||||||
|
copyright: 'Dream Hunter',
|
||||||
})
|
})
|
||||||
const settings = ref({
|
const settings = ref({
|
||||||
fetched: false,
|
fetched: false,
|
||||||
has_v1_mails: false,
|
has_v1_mails: false,
|
||||||
|
send_balance: 0,
|
||||||
address: '',
|
address: '',
|
||||||
auto_reply: {
|
auto_reply: {
|
||||||
subject: '',
|
subject: '',
|
||||||
@@ -25,25 +31,37 @@ export const useGlobalState = createGlobalState(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
const showAuth = ref(false);
|
const showAuth = ref(false);
|
||||||
|
const showPassword = ref(false);
|
||||||
const showAdminAuth = ref(false);
|
const showAdminAuth = ref(false);
|
||||||
const auth = useStorage('auth', '');
|
const auth = useStorage('auth', '');
|
||||||
const adminAuth = useStorage('adminAuth', '');
|
const adminAuth = useStorage('adminAuth', '');
|
||||||
const jwt = useStorage('jwt', '');
|
const jwt = useStorage('jwt', '');
|
||||||
const localeCache = useStorage('locale', 'zhCN');
|
const localeCache = useStorage('locale', 'zh');
|
||||||
const themeSwitch = useStorage('themeSwitch', false);
|
const themeSwitch = useStorage('themeSwitch', false);
|
||||||
const showLogin = ref(false);
|
const adminTab = ref("account");
|
||||||
|
const adminMailTabAddress = ref("");
|
||||||
|
const adminSendBoxTabAddress = ref("");
|
||||||
|
const mailboxSplitSize = useStorage('mailboxSplitSize', 0.25);
|
||||||
|
const useIframeShowMail = useStorage('useIframeShowMail', false);
|
||||||
return {
|
return {
|
||||||
|
isDark,
|
||||||
|
toggleDark,
|
||||||
loading,
|
loading,
|
||||||
settings,
|
settings,
|
||||||
openSettings,
|
openSettings,
|
||||||
showAuth,
|
showAuth,
|
||||||
|
showPassword,
|
||||||
auth,
|
auth,
|
||||||
jwt,
|
jwt,
|
||||||
localeCache,
|
localeCache,
|
||||||
themeSwitch,
|
themeSwitch,
|
||||||
adminAuth,
|
adminAuth,
|
||||||
showAdminAuth,
|
showAdminAuth,
|
||||||
showLogin,
|
adminTab,
|
||||||
|
adminMailTabAddress,
|
||||||
|
adminSendBoxTabAddress,
|
||||||
|
mailboxSplitSize,
|
||||||
|
useIframeShowMail,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import PostalMime from 'postal-mime';
|
import PostalMime from 'postal-mime';
|
||||||
import { parse_message } from 'mail-parser-wasm'
|
|
||||||
|
function humanFileSize(size) {
|
||||||
|
const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
|
||||||
|
return parseFloat((size / Math.pow(1024, i)).toFixed(2)) + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i];
|
||||||
|
}
|
||||||
|
|
||||||
export async function processItem(item) {
|
export async function processItem(item) {
|
||||||
// Try to parse the email using mail-parser-wasm
|
// Try to parse the email using mail-parser-wasm
|
||||||
try {
|
try {
|
||||||
|
const { parse_message } = await import('mail-parser-wasm');
|
||||||
const parsedEmail = parse_message(item.raw);
|
const parsedEmail = parse_message(item.raw);
|
||||||
item.source = parsedEmail.sender || item.source;
|
item.source = parsedEmail.sender || item.source;
|
||||||
item.subject = parsedEmail.subject || '';
|
item.subject = parsedEmail.subject || '';
|
||||||
@@ -20,7 +25,7 @@ export async function processItem(item) {
|
|||||||
return {
|
return {
|
||||||
id: a_item.content_id || Math.random().toString(36).substring(2, 15),
|
id: a_item.content_id || Math.random().toString(36).substring(2, 15),
|
||||||
filename: a_item.filename || a_item.content_id || "",
|
filename: a_item.filename || a_item.content_id || "",
|
||||||
size: a_item.content?.length || 0,
|
size: humanFileSize(a_item.content?.length || 0),
|
||||||
url: blob_url
|
url: blob_url
|
||||||
}
|
}
|
||||||
}) || [];
|
}) || [];
|
||||||
@@ -52,7 +57,7 @@ export async function processItem(item) {
|
|||||||
return {
|
return {
|
||||||
id: a_item.contentId || Math.random().toString(36).substring(2, 15),
|
id: a_item.contentId || Math.random().toString(36).substring(2, 15),
|
||||||
filename: a_item.filename || a_item.contentId || "",
|
filename: a_item.filename || a_item.contentId || "",
|
||||||
size: a_item.content?.length || 0,
|
size: humanFileSize(a_item.content?.length || 0),
|
||||||
url: blob_url
|
url: blob_url
|
||||||
}
|
}
|
||||||
}) || [];
|
}) || [];
|
||||||
@@ -62,6 +67,7 @@ export async function processItem(item) {
|
|||||||
item.subject = 'No Subject';
|
item.subject = 'No Subject';
|
||||||
item.message = item.raw;
|
item.message = item.raw;
|
||||||
}
|
}
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDownloadEmlUrl(raw) {
|
export function getDownloadEmlUrl(raw) {
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, h, onMounted, watch } from 'vue';
|
import { onMounted } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { User, UserCheck, MailBulk } from '@vicons/fa'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
import { api } from '../api'
|
|
||||||
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
|
||||||
|
|
||||||
const { localeCache, adminAuth, showAdminAuth } = useGlobalState()
|
import SenderAccess from './admin/SenderAccess.vue'
|
||||||
const router = useRouter()
|
import Statistics from "./admin/Statistics.vue"
|
||||||
|
import SendBox from './admin/SendBox.vue';
|
||||||
|
import Account from './admin/Account.vue';
|
||||||
|
import CreateAccount from './admin/CreateAccount.vue';
|
||||||
|
import AccountSettings from './admin/AccountSettings.vue';
|
||||||
|
import Mails from './admin/Mails.vue';
|
||||||
|
import MailsUnknow from './admin/MailsUnknow.vue';
|
||||||
|
import Maintenance from './admin/Maintenance.vue';
|
||||||
|
|
||||||
|
const {
|
||||||
|
localeCache, adminAuth, showAdminAuth, adminTab, loading
|
||||||
|
} = useGlobalState()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const showEmailPassword = ref(false)
|
|
||||||
const curEmailPassword = ref("")
|
|
||||||
const addressQuery = ref("")
|
|
||||||
|
|
||||||
const authFunc = async () => {
|
const authFunc = async () => {
|
||||||
try {
|
try {
|
||||||
location.reload()
|
location.reload()
|
||||||
@@ -28,373 +31,79 @@ const { t } = useI18n({
|
|||||||
locale: localeCache.value || 'zh',
|
locale: localeCache.value || 'zh',
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
title: 'Temp Email Admin',
|
accessHeader: 'Admin Password',
|
||||||
auth: 'Admin Auth',
|
accessTip: 'Please enter the admin password',
|
||||||
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',
|
mails: 'Emails',
|
||||||
itemCount: 'itemCount',
|
|
||||||
query: 'Query',
|
|
||||||
userCount: 'User Count',
|
|
||||||
activeUser: '7 days Active User',
|
|
||||||
mailCount: 'Mail Count',
|
|
||||||
account: 'Account',
|
account: 'Account',
|
||||||
unknow: 'Unknow',
|
account_create: 'Create Account',
|
||||||
addressQueryTip: 'Leave blank to query all addresses',
|
account_settings: 'Account Settings',
|
||||||
|
unknow: 'Mails with unknow receiver',
|
||||||
|
senderAccess: 'Sender Access Control',
|
||||||
|
sendBox: 'Send Box',
|
||||||
|
maintenance: 'Maintenance',
|
||||||
|
ok: 'OK',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
title: '临时邮件 Admin',
|
accessHeader: 'Admin 密码',
|
||||||
auth: 'Admin 授权',
|
accessTip: '请输入 Admin 密码',
|
||||||
home: '首页',
|
|
||||||
authTip: '请输入正确的授权码',
|
|
||||||
name: '名称',
|
|
||||||
created_at: '创建时间',
|
|
||||||
showPass: '显示密码',
|
|
||||||
password: '密码',
|
|
||||||
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
|
||||||
delete: '删除',
|
|
||||||
deleteTip: '确定要删除这个邮箱吗?',
|
|
||||||
refresh: '刷新',
|
|
||||||
mails: '邮件',
|
mails: '邮件',
|
||||||
itemCount: '总数',
|
|
||||||
query: '查询',
|
|
||||||
userCount: '用户总数',
|
|
||||||
activeUser: '周活跃用户',
|
|
||||||
mailCount: '邮件总数',
|
|
||||||
account: '账号',
|
account: '账号',
|
||||||
unknow: '未知',
|
account_create: '创建账号',
|
||||||
addressQueryTip: '留空查询所有地址',
|
account_settings: '账号设置',
|
||||||
|
unknow: '无收件人邮件',
|
||||||
|
senderAccess: '发件权限控制',
|
||||||
|
sendBox: '发件箱',
|
||||||
|
maintenance: '维护',
|
||||||
|
ok: '确定',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
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 () => {
|
onMounted(async () => {
|
||||||
if (!adminAuth.value) {
|
if (!adminAuth.value) {
|
||||||
showAdminAuth.value = true
|
showAdminAuth.value = true;
|
||||||
} else {
|
return;
|
||||||
await fetchData()
|
|
||||||
await fetchStatistics()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-modal v-model:show="showAdminAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
<n-modal v-model:show="showAdminAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
||||||
title="Dialog">
|
:title="t('accessHeader')">
|
||||||
<template #header>
|
<p>{{ t('accessTip') }}</p>
|
||||||
<div>{{ t('auth') }}</div>
|
|
||||||
</template>
|
|
||||||
<p>{{ t('authTip') }}</p>
|
|
||||||
<n-input v-model:value="adminAuth" type="textarea" :autosize="{ minRows: 3 }" />
|
<n-input v-model:value="adminAuth" type="textarea" :autosize="{ minRows: 3 }" />
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="authFunc" size="small" tertiary round type="primary">
|
<n-button @click="authFunc" type="primary" :loading="loading">
|
||||||
{{ t('auth') }}
|
{{ t('ok') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-modal v-model:show="showEmailPassword" preset="dialog" title="Dialog">
|
<Statistics />
|
||||||
<template #header>
|
<n-tabs type="card" v-model:value="adminTab">
|
||||||
<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">
|
|
||||||
<n-tab-pane name="account" :tab="t('account')">
|
<n-tab-pane name="account" :tab="t('account')">
|
||||||
<n-input-group>
|
<Account />
|
||||||
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')" />
|
</n-tab-pane>
|
||||||
<n-button @click="fetchData" type="primary" ghost>
|
<n-tab-pane name="account_create" :tab="t('account_create')">
|
||||||
{{ t('query') }}
|
<CreateAccount />
|
||||||
</n-button>
|
</n-tab-pane>
|
||||||
</n-input-group>
|
<n-tab-pane name="account_settings" :tab="t('account_settings')">
|
||||||
<div style="display: inline-block;">
|
<AccountSettings />
|
||||||
<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" />
|
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="mails" :tab="t('mails')">
|
<n-tab-pane name="mails" :tab="t('mails')">
|
||||||
<n-input-group>
|
<Mails />
|
||||||
<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>
|
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="unknow" :tab="t('unknow')">
|
<n-tab-pane name="unknow" :tab="t('unknow')">
|
||||||
<n-button @click="fetchMailUnknowData" type="primary" ghost>
|
<MailsUnknow />
|
||||||
{{ t('query') }}
|
</n-tab-pane>
|
||||||
</n-button>
|
<n-tab-pane name="senderAccess" :tab="t('senderAccess')">
|
||||||
<n-list hoverable clickable>
|
<SenderAccess />
|
||||||
<div style="display: inline-block; margin-bottom: 10px;">
|
</n-tab-pane>
|
||||||
<n-pagination v-model:page="mailUnknowPage" v-model:page-size="mailUnknowPageSize"
|
<n-tab-pane name="sendBox" :tab="t('sendBox')">
|
||||||
:item-count="mailUnknowCount" simple>
|
<SendBox />
|
||||||
<template #prefix="{ itemCount }">
|
</n-tab-pane>
|
||||||
{{ t('itemCount') }}: {{ itemCount }}
|
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
||||||
</template>
|
<Maintenance />
|
||||||
</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>
|
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
38
frontend/src/views/Footer.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useGlobalState } from '../store'
|
||||||
|
const { localeCache, openSettings } = useGlobalState()
|
||||||
|
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
copyright: "Copyright"
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
copyright: "版权所有"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-divider class="footer-divider" />
|
||||||
|
<div style="text-align: center; padding: 20px">
|
||||||
|
<n-text depth="3">
|
||||||
|
{{ t('copyright') }} © 2023-{{ new Date().getFullYear() }} {{ openSettings.copyright }}
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer-divider {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 var(--x-padding);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -4,43 +4,29 @@ import { ref, h, computed, onMounted } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useIsMobile } from '../utils/composables'
|
import { useIsMobile } from '../utils/composables'
|
||||||
import { DarkModeFilled, LightModeFilled, MenuFilled, AdminPanelSettingsFilled } from '@vicons/material'
|
import {
|
||||||
|
DarkModeFilled, LightModeFilled, MenuFilled,
|
||||||
|
AdminPanelSettingsFilled, SendFilled
|
||||||
|
} from '@vicons/material'
|
||||||
import { GithubAlt, Language, User, Home, Copy } from '@vicons/fa'
|
import { GithubAlt, Language, User, Home, Copy } from '@vicons/fa'
|
||||||
|
|
||||||
|
import Login from './Login.vue'
|
||||||
|
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
const { toClipboard } = useClipboard()
|
const { toClipboard } = useClipboard()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const { jwt, localeCache, themeSwitch, showAuth, adminAuth, auth } = useGlobalState()
|
const {
|
||||||
const { showLogin, openSettings, settings } = useGlobalState()
|
jwt, localeCache, toggleDark, isDark, settings,
|
||||||
|
showAuth, adminAuth, auth, loading
|
||||||
|
} = useGlobalState()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
const isAdminRoute = computed(() => route.path.includes('admin'))
|
const isAdminRoute = computed(() => route.path.includes('admin'))
|
||||||
|
|
||||||
const showNewEmail = ref(false)
|
const showMobileMenu = ref(false)
|
||||||
const showLogout = ref(false)
|
|
||||||
const showDelteAccount = ref(false)
|
|
||||||
const password = ref('')
|
|
||||||
const showPassword = ref(false)
|
|
||||||
const emailName = ref("")
|
|
||||||
const emailDomain = ref("")
|
|
||||||
|
|
||||||
const login = async () => {
|
|
||||||
try {
|
|
||||||
jwt.value = password.value;
|
|
||||||
await api.getSettings()
|
|
||||||
location.reload()
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
jwt.value = '';
|
|
||||||
location.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
const authFunc = async () => {
|
const authFunc = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -62,59 +48,34 @@ const { t } = useI18n({
|
|||||||
title: 'Cloudflare Temp Email',
|
title: 'Cloudflare Temp Email',
|
||||||
dark: 'Dark',
|
dark: 'Dark',
|
||||||
light: 'Light',
|
light: 'Light',
|
||||||
login: 'Login',
|
accessHeader: 'Access Password',
|
||||||
logout: 'Logout',
|
accessTip: 'Please enter the correct password',
|
||||||
logoutConfirm: 'Are you sure to logout?',
|
|
||||||
delteAccount: "Delete Account",
|
|
||||||
delteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
|
||||||
auth: 'Auth',
|
|
||||||
authTip: 'Please enter the correct auth code',
|
|
||||||
settings: 'Settings',
|
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
menu: 'Menu',
|
menu: 'Menu',
|
||||||
user: 'User',
|
user: 'User',
|
||||||
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
|
sendMail: 'Send Mail',
|
||||||
getNewEmail: 'Get New Email',
|
|
||||||
getNewEmailTip1: 'Please input the email you want to use.',
|
|
||||||
getNewEmailTip2: 'Levaing it blank will generate a random email address.',
|
|
||||||
yourAddress: 'Your email address is',
|
yourAddress: 'Your email address is',
|
||||||
password: 'Password',
|
|
||||||
passwordTip: 'Please copy the password and you can use it to login to your email account.', cancel: 'Cancel',
|
|
||||||
ok: 'OK',
|
ok: 'OK',
|
||||||
copy: 'Copy',
|
copy: 'Copy',
|
||||||
copied: 'Copied',
|
copied: 'Copied',
|
||||||
showPassword: 'Show Password',
|
fetchAddressError: 'Login password is invalid or account not exist, it may be network connection issue, please try again later.',
|
||||||
fetchAddressError: 'Fetch address error, maybe your jwt is invalid or network error.',
|
|
||||||
mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.',
|
mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
title: 'Cloudflare 临时邮件',
|
title: 'Cloudflare 临时邮件',
|
||||||
dark: '暗色',
|
dark: '暗色',
|
||||||
light: '亮色',
|
light: '亮色',
|
||||||
login: '登录',
|
accessHeader: '访问密码',
|
||||||
logout: '登出',
|
accessTip: '请输入站点访问密码',
|
||||||
logoutConfirm: '确定要登出吗?',
|
|
||||||
delteAccount: "删除账户",
|
|
||||||
delteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
|
||||||
auth: '授权',
|
|
||||||
authTip: '请输入正确的授权码',
|
|
||||||
settings: '设置',
|
|
||||||
home: '主页',
|
home: '主页',
|
||||||
menu: '菜单',
|
menu: '菜单',
|
||||||
user: '用户',
|
user: '用户',
|
||||||
pleaseGetNewEmail: '请"登录"或点击 "获取新邮箱" 按钮来获取一个新的邮箱地址',
|
sendMail: '发送邮件',
|
||||||
getNewEmail: '获取新邮箱',
|
|
||||||
getNewEmailTip1: '请输入你想要使用的邮箱地址。',
|
|
||||||
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
|
||||||
yourAddress: '你的邮箱地址是',
|
yourAddress: '你的邮箱地址是',
|
||||||
password: '密码',
|
|
||||||
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
|
||||||
cancel: '取消',
|
|
||||||
ok: '确定',
|
ok: '确定',
|
||||||
copy: '复制',
|
copy: '复制',
|
||||||
copied: '已复制',
|
copied: '已复制',
|
||||||
showPassword: '查看密码',
|
fetchAddressError: '登录密码无效或账号不存在,也可能是网络连接异常,请稍后再尝试。',
|
||||||
fetchAddressError: '获取地址失败, 请检查你的 jwt 是否有效 或 网络是否正常。',
|
|
||||||
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
|
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,10 +88,10 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => router.push('/')
|
style: "width: 100%",
|
||||||
|
onClick: () => { router.push('/'); showMobileMenu.value = false; }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => t('home'),
|
default: () => t('home'),
|
||||||
@@ -143,10 +104,10 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => router.push('/admin')
|
style: "width: 100%",
|
||||||
|
onClick: () => { router.push('/admin'); showMobileMenu.value = false; }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => "Admin",
|
default: () => "Admin",
|
||||||
@@ -160,9 +121,10 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
|
style: "width: 100%",
|
||||||
|
onClick: () => { router.push("/user"); showMobileMenu.value = false; }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => t('user'),
|
default: () => t('user'),
|
||||||
@@ -171,74 +133,20 @@ const menuOptions = computed(() => [
|
|||||||
),
|
),
|
||||||
show: showUserMenu.value,
|
show: showUserMenu.value,
|
||||||
key: "user",
|
key: "user",
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: () => h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
tertiary: true,
|
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
|
||||||
onClick: () => { showPassword.value = true }
|
|
||||||
},
|
|
||||||
{ default: () => t('showPassword') }
|
|
||||||
),
|
|
||||||
key: "showPassword"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: () => h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
tertiary: true,
|
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
|
||||||
onClick: () => { router.push('/settings') }
|
|
||||||
},
|
|
||||||
{ default: () => t('settings') }
|
|
||||||
),
|
|
||||||
key: "settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: () => h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
tertiary: true,
|
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
|
||||||
onClick: () => { showLogout.value = true }
|
|
||||||
},
|
|
||||||
{ default: () => t('logout') }
|
|
||||||
),
|
|
||||||
key: "logout"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: () => h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
tertiary: true,
|
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
|
||||||
onClick: () => { showDelteAccount.value = true }
|
|
||||||
},
|
|
||||||
{ default: () => t('delteAccount') }
|
|
||||||
),
|
|
||||||
key: "delte_account"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => { themeSwitch.value = !themeSwitch.value }
|
style: "width: 100%",
|
||||||
|
onClick: () => { toggleDark(); showMobileMenu.value = false; }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => themeSwitch.value ? t('light') : t('dark'),
|
default: () => isDark.value ? t('light') : t('dark'),
|
||||||
icon: () => h(
|
icon: () => h(
|
||||||
NIcon, { component: themeSwitch.value ? LightModeFilled : DarkModeFilled }
|
NIcon, { component: isDark.value ? LightModeFilled : DarkModeFilled }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -248,10 +156,13 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: false,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
onClick: () => localeCache.value == 'zh' ? changeLocale('en') : changeLocale('zh')
|
style: "width: 100%",
|
||||||
|
onClick: () => {
|
||||||
|
localeCache.value == 'zh' ? changeLocale('en') : changeLocale('zh');
|
||||||
|
showMobileMenu.value = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => localeCache.value == 'zh' ? "English" : "中文",
|
default: () => localeCache.value == 'zh' ? "English" : "中文",
|
||||||
@@ -266,9 +177,9 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
bordered: !isMobile.value,
|
text: true,
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
size: "small",
|
||||||
|
style: "width: 100%",
|
||||||
tag: "a",
|
tag: "a",
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
href: "https://github.com/dreamhunter2333/cloudflare_temp_email",
|
href: "https://github.com/dreamhunter2333/cloudflare_temp_email",
|
||||||
@@ -282,21 +193,6 @@ const menuOptions = computed(() => [
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const menuOptionsMobile = computed(() => [
|
|
||||||
{
|
|
||||||
label: t('menu'),
|
|
||||||
icon: () => h(
|
|
||||||
NIcon,
|
|
||||||
{
|
|
||||||
component: MenuFilled
|
|
||||||
}
|
|
||||||
),
|
|
||||||
key: "menu",
|
|
||||||
children: menuOptions.value
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
|
|
||||||
const copy = async () => {
|
const copy = async () => {
|
||||||
try {
|
try {
|
||||||
await toClipboard(settings.value.address)
|
await toClipboard(settings.value.address)
|
||||||
@@ -306,49 +202,38 @@ const copy = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newEmail = async () => {
|
|
||||||
try {
|
|
||||||
const res = await api.fetch(
|
|
||||||
`/api/new_address`
|
|
||||||
+ `?name=${emailName.value || ''}`
|
|
||||||
+ `&domain=${emailDomain.value || ''}`
|
|
||||||
);
|
|
||||||
jwt.value = res["jwt"];
|
|
||||||
await api.getSettings();
|
|
||||||
showNewEmail.value = false;
|
|
||||||
showPassword.value = true;
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteAccount = async () => {
|
|
||||||
try {
|
|
||||||
await api.fetch(`/api/delete_address`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
jwt.value = '';
|
|
||||||
location.reload()
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await api.getOpenSettings(message);
|
await api.getOpenSettings(message);
|
||||||
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0].value : "";
|
await api.getSettings();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-layout-header>
|
<n-page-header>
|
||||||
<h2 style="display: inline-block; margin-left: 10px;">{{ t('title') }}</h2>
|
<template #title>
|
||||||
<div>
|
<h3>{{ t('title') }}</h3>
|
||||||
<n-menu v-if="!isMobile" mode="horizontal" :options="menuOptions" />
|
</template>
|
||||||
<n-menu v-else mode="horizontal" :options="menuOptionsMobile" />
|
<template #avatar>
|
||||||
</div>
|
<n-avatar style="margin-left: 10px;" src="/logo.png" />
|
||||||
</n-layout-header>
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<n-space>
|
||||||
|
<n-menu v-if="!isMobile" mode="horizontal" :options="menuOptions" />
|
||||||
|
<n-button v-else :text="true" @click="showMobileMenu = !showMobileMenu" style="margin-right: 10px;">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="MenuFilled" />
|
||||||
|
</template>
|
||||||
|
{{ t('menu') }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
</n-page-header>
|
||||||
|
<n-drawer v-model:show="showMobileMenu" placement="top" style="height: 100vh;">
|
||||||
|
<n-drawer-content :title="t('menu')" closable>
|
||||||
|
<n-menu :options="menuOptions" />
|
||||||
|
</n-drawer-content>
|
||||||
|
</n-drawer>
|
||||||
<div v-if="!isAdminRoute">
|
<div v-if="!isAdminRoute">
|
||||||
<n-card v-if="!settings.fetched">
|
<n-card v-if="!settings.fetched">
|
||||||
<n-skeleton style="height: 50vh" />
|
<n-skeleton style="height: 50vh" />
|
||||||
@@ -357,7 +242,7 @@ onMounted(async () => {
|
|||||||
<n-alert v-if="settings.has_v1_mails" type="warning" show-icon closable>
|
<n-alert v-if="settings.has_v1_mails" type="warning" show-icon closable>
|
||||||
<span>
|
<span>
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small"
|
<n-button tag="a" target="_blank" tertiary type="info" size="small"
|
||||||
href="https://temp-email-v1.dreamhunter2333.xyz/">
|
href="https://mail-v1.awsl.uk">
|
||||||
<b>{{ t('mailV1Alert') }} </b>
|
<b>{{ t('mailV1Alert') }} </b>
|
||||||
</n-button>
|
</n-button>
|
||||||
</span>
|
</span>
|
||||||
@@ -365,113 +250,32 @@ onMounted(async () => {
|
|||||||
<n-alert type="info" show-icon>
|
<n-alert type="info" show-icon>
|
||||||
<span>
|
<span>
|
||||||
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
||||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary round type="primary">
|
<n-button style="margin-left: 10px" @click="router.push('/send')" size="small" tertiary
|
||||||
|
type="primary">
|
||||||
|
<n-icon :component="SendFilled" /> {{ t('sendMail') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary type="primary">
|
||||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</span>
|
</span>
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</div>
|
</div>
|
||||||
<n-card v-else>
|
<div v-else class="center">
|
||||||
<n-result status="info" :description="t('pleaseGetNewEmail')">
|
<n-card style="max-width: 600px;">
|
||||||
<template #footer>
|
<n-alert v-if="jwt" type="warning" show-icon>
|
||||||
<n-alert v-if="jwt" type="warning" show-icon>
|
<span>{{ t('fetchAddressError') }}</span>
|
||||||
<span>{{ t('fetchAddressError') }}</span>
|
</n-alert>
|
||||||
</n-alert>
|
<Login />
|
||||||
<n-button @click="showLogin = true" tertiary round type="primary">
|
</n-card>
|
||||||
{{ t('login') }}
|
</div>
|
||||||
</n-button>
|
|
||||||
<n-button @click="showNewEmail = true" tertiary round type="primary">
|
|
||||||
{{ t('getNewEmail') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-result>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
</div>
|
||||||
<n-modal v-model:show="showNewEmail" preset="dialog" title="Dialog">
|
|
||||||
<template #header>
|
|
||||||
<div>{{ t('getNewEmail') }}</div>
|
|
||||||
</template>
|
|
||||||
<span>
|
|
||||||
<p>{{ t("getNewEmailTip1") }}</p>
|
|
||||||
<p>{{ t("getNewEmailTip2") }}</p>
|
|
||||||
</span>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-group-label v-if="openSettings.prefix">
|
|
||||||
{{ openSettings.prefix }}
|
|
||||||
</n-input-group-label>
|
|
||||||
<n-input v-model:value="emailName" />
|
|
||||||
<n-input-group-label>@</n-input-group-label>
|
|
||||||
<n-select v-model:value="emailDomain" :consistent-menu-width="false" :options="openSettings.domains" />
|
|
||||||
</n-input-group>
|
|
||||||
<template #action>
|
|
||||||
<n-button @click="showNewEmail = false">
|
|
||||||
{{ t('cancel') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="newEmail" type="primary">
|
|
||||||
{{ t('ok') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showPassword" preset="dialog" title="Dialog">
|
|
||||||
<template #header>
|
|
||||||
<div>{{ t("password") }}</div>
|
|
||||||
</template>
|
|
||||||
<span>
|
|
||||||
<p>{{ t("passwordTip") }}</p>
|
|
||||||
</span>
|
|
||||||
<n-card>
|
|
||||||
<b>{{ jwt }}</b>
|
|
||||||
</n-card>
|
|
||||||
<template #action>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showLogin" preset="dialog" title="Dialog">
|
|
||||||
<template #header>
|
|
||||||
<div>{{ t('login') }}</div>
|
|
||||||
</template>
|
|
||||||
<n-input v-model:value="password" type="textarea" :autosize="{
|
|
||||||
minRows: 3
|
|
||||||
}" />
|
|
||||||
<template #action>
|
|
||||||
<n-button @click="login" size="small" tertiary round type="primary">
|
|
||||||
{{ t('login') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showLogout" preset="dialog" title="Dialog">
|
|
||||||
<template #header>
|
|
||||||
<div>{{ t('logout') }}</div>
|
|
||||||
</template>
|
|
||||||
<p>{{ t('logoutConfirm') }}</p>
|
|
||||||
<template #action>
|
|
||||||
<n-button @click="logout" size="small" tertiary round type="primary">
|
|
||||||
{{ t('logout') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showDelteAccount" preset="dialog" title="Dialog">
|
|
||||||
<template #header>
|
|
||||||
<div>{{ t('delteAccount') }}</div>
|
|
||||||
</template>
|
|
||||||
<p>{{ t('delteAccountConfirm') }}</p>
|
|
||||||
<template #action>
|
|
||||||
<n-button @click="deleteAccount" size="small" tertiary round type="error">
|
|
||||||
{{ t('delteAccount') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
<n-modal v-model:show="showAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
||||||
title="Dialog">
|
:title="t('accessHeader')">
|
||||||
<template #header>
|
<p>{{ t('accessTip') }}</p>
|
||||||
<div>{{ t('auth') }}</div>
|
<n-input v-model:value="auth" type="textarea" :autosize="{ minRows: 3 }" />
|
||||||
</template>
|
|
||||||
<p>{{ t('authTip') }}</p>
|
|
||||||
<n-input v-model:value="auth" type="textarea" :autosize="{
|
|
||||||
minRows: 3
|
|
||||||
}" />
|
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="authFunc" size="small" tertiary round type="primary">
|
<n-button :loading="loading" @click="authFunc" type="primary">
|
||||||
{{ t('auth') }}
|
{{ t('ok') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
@@ -494,4 +298,16 @@ onMounted(async () => {
|
|||||||
.n-card {
|
.n-card {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
place-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-form .n-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,299 +1,22 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { watch, onMounted, ref } from "vue";
|
import MailBox from '../components/MailBox.vue';
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
import { CloudDownloadRound } from '@vicons/material'
|
|
||||||
import { useIsMobile } from '../utils/composables'
|
|
||||||
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
|
||||||
|
|
||||||
const message = useMessage()
|
const { settings, openSettings } = useGlobalState()
|
||||||
const isMobile = useIsMobile()
|
|
||||||
|
|
||||||
const { settings, themeSwitch } = useGlobalState()
|
const fetchMailData = async (limit, offset) => {
|
||||||
const autoRefresh = ref(false)
|
return await api.fetch(`/api/mails?limit=${limit}&offset=${offset}`);
|
||||||
const data = ref([])
|
|
||||||
const timer = ref(null)
|
|
||||||
|
|
||||||
const count = ref(0)
|
|
||||||
const page = ref(1)
|
|
||||||
const pageSize = ref(20)
|
|
||||||
|
|
||||||
const showAttachments = ref(false)
|
|
||||||
const curAttachments = ref([])
|
|
||||||
const curMail = ref(null);
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
autoRefresh: 'Auto Refresh',
|
|
||||||
refresh: 'Refresh',
|
|
||||||
attachments: 'Show Attachments',
|
|
||||||
downloadMail: 'Download Mail',
|
|
||||||
pleaseSelectMail: "Please select a mail to view."
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
autoRefresh: '自动刷新',
|
|
||||||
refresh: '刷新',
|
|
||||||
downloadMail: '下载邮件',
|
|
||||||
attachments: '查看附件',
|
|
||||||
pleaseSelectMail: "请选择一封邮件查看。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const setupAutoRefresh = async (autoRefresh) => {
|
|
||||||
if (autoRefresh) {
|
|
||||||
timer.value = setInterval(async () => {
|
|
||||||
await refresh();
|
|
||||||
}, 30000)
|
|
||||||
} else {
|
|
||||||
clearInterval(timer.value)
|
|
||||||
timer.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(autoRefresh, async (autoRefresh, old) => {
|
|
||||||
setupAutoRefresh(autoRefresh)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
|
|
||||||
if (page !== oldPage || pageSize !== oldPageSize) {
|
|
||||||
await refresh();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
if (typeof settings.value.address != 'string' || settings.value.address.trim() === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { results, count: totalCount } = await api.fetch(
|
|
||||||
`/api/mails`
|
|
||||||
+ `?limit=${pageSize.value}`
|
|
||||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
|
||||||
);
|
|
||||||
data.value = await Promise.all(results.map(async (item) => {
|
|
||||||
return await processItem(item);
|
|
||||||
}));
|
|
||||||
if (totalCount > 0) {
|
|
||||||
count.value = totalCount;
|
|
||||||
}
|
|
||||||
if (!isMobile.value && !curMail.value && data.value.length > 0) {
|
|
||||||
curMail.value = data.value[0];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const clickRow = async (row) => {
|
const deleteMail = async (curMailId) => {
|
||||||
curMail.value = row;
|
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAttachments = (attachments) => {
|
|
||||||
curAttachments.value = attachments;
|
|
||||||
showAttachments.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mailItemClass = (row) => {
|
|
||||||
return curMail.value && row.id == curMail.value.id ? (themeSwitch.value ? 'overlay overlay-dark-backgroud' : 'overlay overlay-light-backgroud') : '';
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await api.getSettings();
|
|
||||||
await refresh();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="settings.address">
|
||||||
<n-layout v-if="settings.address">
|
<MailBox :showEMailTo="false" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
||||||
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25" :default-size="0.25">
|
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
||||||
<template #1>
|
|
||||||
<div>
|
|
||||||
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
|
||||||
</div>
|
|
||||||
<n-switch v-model:value="autoRefresh" size="small">
|
|
||||||
<template #checked>
|
|
||||||
{{ t('autoRefresh') }}
|
|
||||||
</template>
|
|
||||||
<template #unchecked>
|
|
||||||
{{ t('autoRefresh') }}
|
|
||||||
</template></n-switch>
|
|
||||||
<n-button class="center" @click="refresh" size="small" type="primary">
|
|
||||||
{{ t('refresh') }}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
<div style="overflow: scroll; height: 80vh;">
|
|
||||||
<n-list hoverable clickable>
|
|
||||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
|
||||||
:class="mailItemClass(row)">
|
|
||||||
<n-thing class="center" :title="row.subject" style="overflow: scroll">
|
|
||||||
<template #description>
|
|
||||||
<n-tag type="info">
|
|
||||||
ID: {{ row.id }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
{{ row.created_at }}
|
|
||||||
</n-tag>
|
|
||||||
<div style="word-break: break-all; font-size: small;">
|
|
||||||
FROM: {{ row.source }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-thing>
|
|
||||||
</n-list-item>
|
|
||||||
</n-list>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #2>
|
|
||||||
<n-card v-if="curMail" class="mail-item" :title="curMail.subject" style="overflow: scroll;">
|
|
||||||
<n-space>
|
|
||||||
<n-tag type="info">
|
|
||||||
ID: {{ curMail.id }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
{{ curMail.created_at }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
FROM: {{ curMail.source }}
|
|
||||||
</n-tag>
|
|
||||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
|
||||||
@click="getAttachments(curMail.attachments)">
|
|
||||||
{{ t('attachments') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
|
||||||
:href="getDownloadEmlUrl(curMail.raw)">
|
|
||||||
<n-icon :component="CloudDownloadRound" />
|
|
||||||
{{ t('downloadMail') }}
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
<div v-html="curMail.message" style="margin-top: 10px;max-height: 100vh;"></div>
|
|
||||||
</n-card>
|
|
||||||
<n-card class="mail-item" v-else>
|
|
||||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
|
||||||
</n-result>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
</n-split>
|
|
||||||
<div class="left" v-else>
|
|
||||||
<div>
|
|
||||||
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
|
||||||
</div>
|
|
||||||
<n-switch v-model:value="autoRefresh" size="small">
|
|
||||||
<template #checked>
|
|
||||||
{{ t('autoRefresh') }}
|
|
||||||
</template>
|
|
||||||
<template #unchecked>
|
|
||||||
{{ t('autoRefresh') }}
|
|
||||||
</template></n-switch>
|
|
||||||
<n-button class="center" @click="refresh" size="small" type="primary">
|
|
||||||
{{ t('refresh') }}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
<div id="drawer-target" style="overflow: scroll; max-height: 80vh;">
|
|
||||||
<n-list hoverable clickable>
|
|
||||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)">
|
|
||||||
<n-thing class="center" :title="row.subject" style="overflow: scroll">
|
|
||||||
<template #description>
|
|
||||||
<n-tag type="info">
|
|
||||||
ID: {{ row.id }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
{{ row.created_at }}
|
|
||||||
</n-tag>
|
|
||||||
<div style="word-break: break-all; font-size: small;">
|
|
||||||
FROM: {{ row.source }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-thing>
|
|
||||||
</n-list-item>
|
|
||||||
</n-list>
|
|
||||||
</div>
|
|
||||||
<n-drawer v-model:show="curMail" width="100%" :trap-focus="false" :block-scroll="false" to="#drawer-target">
|
|
||||||
<n-drawer-content :title="curMail.subject" closable>
|
|
||||||
<n-card style="overflow: scroll;">
|
|
||||||
<n-space>
|
|
||||||
<n-tag type="info">
|
|
||||||
ID: {{ curMail.id }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
{{ curMail.created_at }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
FROM: {{ curMail.source }}
|
|
||||||
</n-tag>
|
|
||||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
|
||||||
@click="getAttachments(curMail.attachments)">
|
|
||||||
{{ t('attachments') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
|
||||||
:href="getDownloadEmlUrl(curMail)">
|
|
||||||
{{ t('downloadMail') }}
|
|
||||||
<n-icon :component="CloudDownloadRound" />
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
<div v-html="curMail.message" style="max-height: 100vh;"></div>
|
|
||||||
</n-card>
|
|
||||||
</n-drawer-content>
|
|
||||||
</n-drawer>
|
|
||||||
</div>
|
|
||||||
</n-layout>
|
|
||||||
<n-modal v-model:show="showAttachments" preset="dialog" title="Dialog">
|
|
||||||
<template #header>
|
|
||||||
<div>{{ t("attachments") }}</div>
|
|
||||||
</template>
|
|
||||||
<n-list hoverable clickable>
|
|
||||||
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
|
|
||||||
<n-thing class="center" :title="row.filename">
|
|
||||||
<template #description>
|
|
||||||
<n-space>
|
|
||||||
<n-tag type="info">
|
|
||||||
Size: {{ row.size }}
|
|
||||||
</n-tag>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
</n-thing>
|
|
||||||
<template #suffix>
|
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="row.filename"
|
|
||||||
:href="row.url">
|
|
||||||
<n-icon :component="CloudDownloadRound" />
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-list-item>
|
|
||||||
</n-list>
|
|
||||||
<template #action>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.left {
|
|
||||||
overflow: scroll;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-dark-backgroud {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-light-backgroud {
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-item {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
166
frontend/src/views/Login.vue
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import AdminContact from './admin/AdminContact.vue'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../store'
|
||||||
|
import { api } from '../api'
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const {
|
||||||
|
jwt, localeCache, loading, openSettings, showPassword
|
||||||
|
} = useGlobalState()
|
||||||
|
|
||||||
|
const tabValue = ref('signin')
|
||||||
|
const password = ref('')
|
||||||
|
const emailName = ref("")
|
||||||
|
const emailDomain = ref("")
|
||||||
|
|
||||||
|
const login = async () => {
|
||||||
|
if (!password.value) {
|
||||||
|
message.error(t('passwordInput'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
jwt.value = password.value;
|
||||||
|
await api.getSettings()
|
||||||
|
location.reload()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
login: 'Login',
|
||||||
|
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
|
||||||
|
getNewEmail: 'Get New Email',
|
||||||
|
getNewEmailTip1: 'Please input the email you want to use. only allow ., a-z, A-Z and 0-9',
|
||||||
|
getNewEmailTip2: 'Levaing it blank will generate a random email address.',
|
||||||
|
getNewEmailTip3: 'You can choose a domain from the dropdown list.',
|
||||||
|
password: 'Password',
|
||||||
|
ok: 'OK',
|
||||||
|
generateName: 'Generate Fake Name',
|
||||||
|
help: 'Help',
|
||||||
|
passwordInput: 'Please input the password',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
login: '登录',
|
||||||
|
pleaseGetNewEmail: '请"登录"或点击 "获取新邮箱" 按钮来获取一个新的邮箱地址',
|
||||||
|
getNewEmail: '注册新邮箱',
|
||||||
|
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 ., a-z, A-Z, 0-9',
|
||||||
|
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
||||||
|
getNewEmailTip3: '你可以从下拉列表中选择一个域名。',
|
||||||
|
password: '密码',
|
||||||
|
ok: '确定',
|
||||||
|
generateName: '生成随机名字',
|
||||||
|
help: '帮助',
|
||||||
|
passwordInput: '请输入密码',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateNameLoading = ref(false);
|
||||||
|
const generateName = async () => {
|
||||||
|
try {
|
||||||
|
generateNameLoading.value = true;
|
||||||
|
const { faker } = await import('https://esm.sh/@faker-js/faker');
|
||||||
|
emailName.value = faker.person
|
||||||
|
.fullName()
|
||||||
|
.replace(/\s+/g, '.')
|
||||||
|
.replace(/[^a-zA-Z0-9.]/g, '')
|
||||||
|
.toLowerCase();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
} finally {
|
||||||
|
generateNameLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const newEmail = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.fetch(
|
||||||
|
`/api/new_address`
|
||||||
|
+ `?name=${emailName.value || ''}`
|
||||||
|
+ `&domain=${emailDomain.value || ''}`
|
||||||
|
);
|
||||||
|
jwt.value = res["jwt"];
|
||||||
|
await api.getSettings();
|
||||||
|
showPassword.value = true;
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0].value : "";
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-tabs v-model:value="tabValue" size="large" justify-content="space-evenly">
|
||||||
|
<n-tab-pane name="signin" :tab="t('login')">
|
||||||
|
<n-form>
|
||||||
|
<n-form-item-row :label="t('password')" required>
|
||||||
|
<n-input v-model:value="password" type="textarea" :autosize="{ minRows: 3 }" />
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-button @click="login" :loading="loading" type="primary" block secondary strong>
|
||||||
|
{{ t('login') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button v-if="openSettings.enableUserCreateEmail" @click="tabValue = 'register'" block secondary
|
||||||
|
strong>
|
||||||
|
{{ t('getNewEmail') }}
|
||||||
|
</n-button>
|
||||||
|
</n-form>
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane v-if="openSettings.enableUserCreateEmail" name="register" :tab="t('getNewEmail')">
|
||||||
|
<n-spin :show="generateNameLoading">
|
||||||
|
<n-form>
|
||||||
|
<span>
|
||||||
|
<p>{{ t("getNewEmailTip1") }}</p>
|
||||||
|
<p>{{ t("getNewEmailTip2") }}</p>
|
||||||
|
<p>{{ t("getNewEmailTip3") }}</p>
|
||||||
|
</span>
|
||||||
|
<n-button @click="generateName" style="margin-bottom: 10px;">
|
||||||
|
{{ t('generateName') }}
|
||||||
|
</n-button>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input-group-label v-if="openSettings.prefix">
|
||||||
|
{{ openSettings.prefix }}
|
||||||
|
</n-input-group-label>
|
||||||
|
<n-input v-model:value="emailName" />
|
||||||
|
<n-input-group-label>@</n-input-group-label>
|
||||||
|
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||||
|
:options="openSettings.domains" />
|
||||||
|
</n-input-group>
|
||||||
|
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">
|
||||||
|
{{ t('ok') }}
|
||||||
|
</n-button>
|
||||||
|
</n-form>
|
||||||
|
</n-spin>
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="help" :tab="t('help')">
|
||||||
|
<n-alert type="info" show-icon>
|
||||||
|
<span>{{ t('pleaseGetNewEmail') }}</span>
|
||||||
|
</n-alert>
|
||||||
|
<AdminContact />
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.n-alert {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-form .n-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
46
frontend/src/views/User.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../store'
|
||||||
|
|
||||||
|
import AutoReply from './user/AutoReply.vue';
|
||||||
|
import SendBox from './send/SendBox.vue';
|
||||||
|
import Account from './user/Account.vue';
|
||||||
|
|
||||||
|
const { localeCache, settings, openSettings } = useGlobalState()
|
||||||
|
const userTab = useStorage('userTab', 'account')
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
sendbox: 'Send Box',
|
||||||
|
auto_reply: 'Auto Reply',
|
||||||
|
account: 'Account',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
sendbox: '发件箱',
|
||||||
|
auto_reply: '自动回复',
|
||||||
|
account: '账户',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="settings.address">
|
||||||
|
<n-tabs type="card" v-model:value="userTab">
|
||||||
|
<n-tab-pane name="account" :tab="t('account')">
|
||||||
|
<Account />
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
||||||
|
<SendBox />
|
||||||
|
</n-tab-pane>
|
||||||
|
<n-tab-pane v-if="openSettings.enableAutoReply" name="auto_reply" :tab="t('auto_reply')">
|
||||||
|
<AutoReply />
|
||||||
|
</n-tab-pane>
|
||||||
|
</n-tabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
308
frontend/src/views/admin/Account.vue
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, h, onMounted, watch } from 'vue';
|
||||||
|
import { NBadge } from 'naive-ui'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
import { api } from '../../api'
|
||||||
|
import { NButton, NMenu } from 'naive-ui';
|
||||||
|
import { MenuFilled } from '@vicons/material'
|
||||||
|
|
||||||
|
const {
|
||||||
|
localeCache, adminAuth, showAdminAuth, loading,
|
||||||
|
adminTab, adminMailTabAddress, adminSendBoxTabAddress
|
||||||
|
} = useGlobalState()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
name: 'Name',
|
||||||
|
created_at: 'Created At',
|
||||||
|
updated_at: 'Update At',
|
||||||
|
mail_count: 'Mail Count',
|
||||||
|
send_count: 'Send Count',
|
||||||
|
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: '创建时间',
|
||||||
|
updated_at: '更新时间',
|
||||||
|
mail_count: '邮件数量',
|
||||||
|
send_count: '发送数量',
|
||||||
|
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('updated_at'),
|
||||||
|
key: "updated_at"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('mail_count'),
|
||||||
|
key: "mail_count",
|
||||||
|
render(row) {
|
||||||
|
return h(NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
onClick: () => {
|
||||||
|
if (row.mail_count > 0) {
|
||||||
|
adminMailTabAddress.value = row.name;
|
||||||
|
adminTab.value = "mails";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: () => h(NBadge, {
|
||||||
|
value: row.mail_count,
|
||||||
|
'show-zero': true,
|
||||||
|
max: 99,
|
||||||
|
type: "success"
|
||||||
|
}),
|
||||||
|
default: () => row.mail_count > 0 ? t('viewMails') : ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('send_count'),
|
||||||
|
key: "send_count",
|
||||||
|
render(row) {
|
||||||
|
return h(NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
onClick: () => {
|
||||||
|
if (row.send_count > 0) {
|
||||||
|
adminSendBoxTabAddress.value = row.name;
|
||||||
|
adminTab.value = "sendBox";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: () => h(NBadge, {
|
||||||
|
value: row.send_count,
|
||||||
|
'show-zero': true,
|
||||||
|
max: 99,
|
||||||
|
type: "success"
|
||||||
|
}),
|
||||||
|
default: () => row.send_count > 0 ? t('viewSendBox') : ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
onClick: () => showPassword(row.id)
|
||||||
|
},
|
||||||
|
{ default: () => t('showPass') }
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h(NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
onClick: () => {
|
||||||
|
adminMailTabAddress.value = row.name;
|
||||||
|
adminTab.value = "mails";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => t('viewMails') }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h(NButton,
|
||||||
|
{
|
||||||
|
text: true,
|
||||||
|
onClick: () => {
|
||||||
|
adminSendBoxTabAddress.value = row.name;
|
||||||
|
adminTab.value = "sendBox";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => t('viewSendBox') }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => h(NButton,
|
||||||
|
{
|
||||||
|
text: 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 :loading="loading" @click="deleteEmail" size="small" tertiary 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>
|
||||||
84
frontend/src/views/admin/AccountSettings.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
import { api } from '../../api'
|
||||||
|
|
||||||
|
const {
|
||||||
|
localeCache, loading, openSettings,
|
||||||
|
} = useGlobalState()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
save: 'Save',
|
||||||
|
successTip: 'Save Success',
|
||||||
|
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
|
||||||
|
address_block_list_placeholder: 'Please enter the keywords you want to block',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
save: '保存',
|
||||||
|
successTip: '保存成功',
|
||||||
|
address_block_list: '用户地址屏蔽关键词(管理员可跳过检查)',
|
||||||
|
address_block_list_placeholder: '请输入您想要屏蔽的关键词',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const addressBlockList = ref([])
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await api.fetch(`/admin/account_settings`)
|
||||||
|
addressBlockList.value = res.blockList || []
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/admin/account_settings`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
blockList: addressBlockList.value || []
|
||||||
|
})
|
||||||
|
})
|
||||||
|
message.success(t('successTip'))
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="center">
|
||||||
|
<n-card style="max-width: 600px;">
|
||||||
|
<n-form-item-row :label="t('address_block_list')">
|
||||||
|
<n-select v-model:value="addressBlockList" filterable multiple tag
|
||||||
|
:placeholder="t('address_block_list_placeholder')" />
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-button @click="save" type="primary" block :loading="loading">
|
||||||
|
{{ t('save') }}
|
||||||
|
</n-button>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
place-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
23
frontend/src/views/admin/AdminContact.vue
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
const { localeCache, openSettings } = useGlobalState()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
adminContact: 'If you need help, please contact the administrator ({msg})',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
adminContact: '如果你需要帮助,请联系管理员 ({msg})',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-alert v-if="openSettings.adminContact" type="info" show-icon>
|
||||||
|
<span>{{ t('adminContact', { msg: openSettings.adminContact }) }}</span>
|
||||||
|
</n-alert>
|
||||||
|
</template>
|
||||||
109
frontend/src/views/admin/CreateAccount.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
import { api } from '../../api'
|
||||||
|
|
||||||
|
const {
|
||||||
|
localeCache, loading, openSettings,
|
||||||
|
} = useGlobalState()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
address: 'Address',
|
||||||
|
enablePrefix: 'If enable Prefix',
|
||||||
|
creatNewEmail: 'Get New Email',
|
||||||
|
fillInAllFields: 'Please fill in all fields',
|
||||||
|
successTip: 'Success Created',
|
||||||
|
password: 'Password',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
address: '地址',
|
||||||
|
enablePrefix: '是否启用前缀',
|
||||||
|
creatNewEmail: '创建新邮箱',
|
||||||
|
fillInAllFields: '请填写完整信息',
|
||||||
|
successTip: '创建成功',
|
||||||
|
password: '密码',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const enablePrefix = ref(true)
|
||||||
|
const emailName = ref("")
|
||||||
|
const emailDomain = ref("")
|
||||||
|
const showReultModal = ref(false)
|
||||||
|
const result = ref("")
|
||||||
|
|
||||||
|
const newEmail = async () => {
|
||||||
|
if (!emailName.value || !emailDomain.value) {
|
||||||
|
message.error(t('fillInAllFields'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await api.fetch(`/admin/new_address`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
enablePrefix: enablePrefix.value,
|
||||||
|
name: emailName.value,
|
||||||
|
domain: emailDomain.value,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
result.value = res["jwt"];
|
||||||
|
message.success(t('successTip'))
|
||||||
|
showReultModal.value = true
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (openSettings.prefix) {
|
||||||
|
enablePrefix.value = true
|
||||||
|
}
|
||||||
|
emailDomain.value = openSettings.value.domains?.[0]?.value || ""
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="center">
|
||||||
|
<n-modal v-model:show="showReultModal" preset="dialog" :title="t('password')">
|
||||||
|
<p>{{ t('password') }}</p>
|
||||||
|
<n-card>
|
||||||
|
<b>{{ result }}</b>
|
||||||
|
</n-card>
|
||||||
|
</n-modal>
|
||||||
|
<n-card style="max-width: 600px;">
|
||||||
|
<n-form-item-row v-if="openSettings.prefix" :label="t('enablePrefix')">
|
||||||
|
<n-checkbox v-model:checked="enablePrefix" />
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('address')">
|
||||||
|
<n-input-group>
|
||||||
|
<n-input-group-label v-if="enablePrefix && openSettings.prefix">
|
||||||
|
{{ openSettings.prefix }}
|
||||||
|
</n-input-group-label>
|
||||||
|
<n-input v-model:value="emailName" />
|
||||||
|
<n-input-group-label>@</n-input-group-label>
|
||||||
|
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||||
|
:options="openSettings.domains" />
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-button @click="newEmail" type="primary" block :loading="loading">
|
||||||
|
{{ t('creatNewEmail') }}
|
||||||
|
</n-button>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
place-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
frontend/src/views/admin/Mails.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
import { api } from '../../api'
|
||||||
|
import MailBox from '../../components/MailBox.vue';
|
||||||
|
|
||||||
|
const {
|
||||||
|
localeCache, adminAuth, showAdminAuth,
|
||||||
|
adminMailTabAddress
|
||||||
|
} = useGlobalState()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
addressQueryTip: 'Leave blank to query all addresses',
|
||||||
|
query: 'Query',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
addressQueryTip: '留空查询所有地址',
|
||||||
|
query: '查询',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mailBoxKey = ref("")
|
||||||
|
|
||||||
|
const queryAddress = () => {
|
||||||
|
mailBoxKey.value = adminMailTabAddress.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchMailData = async (limit, offset) => {
|
||||||
|
return await api.fetch(
|
||||||
|
`/admin/mails`
|
||||||
|
+ `?limit=${limit}`
|
||||||
|
+ `&offset=${offset}`
|
||||||
|
+ (adminMailTabAddress.value ? `&address=${adminMailTabAddress.value}` : '')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!adminAuth.value) {
|
||||||
|
showAdminAuth.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input v-model:value="adminMailTabAddress" :placeholder="t('addressQueryTip')" />
|
||||||
|
<n-button @click="queryAddress" type="primary" ghost>
|
||||||
|
{{ t('query') }}
|
||||||
|
</n-button>
|
||||||
|
</n-input-group>
|
||||||
|
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="false" :fetchMailData="fetchMailData" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
30
frontend/src/views/admin/MailsUnknow.vue
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted } from 'vue';
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
import { api } from '../../api'
|
||||||
|
import MailBox from '../../components/MailBox.vue';
|
||||||
|
|
||||||
|
const { adminAuth, showAdminAuth } = useGlobalState()
|
||||||
|
|
||||||
|
const fetchMailUnknowData = async (limit, offset) => {
|
||||||
|
return await api.fetch(
|
||||||
|
`/admin/mails_unknow`
|
||||||
|
+ `?limit=${limit}`
|
||||||
|
+ `&offset=${offset}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!adminAuth.value) {
|
||||||
|
showAdminAuth.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="adminAuth">
|
||||||
|
<MailBox :enableUserDeleteEmail="false" :fetchMailData="fetchMailUnknowData" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
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>
|
||||||
159
frontend/src/views/admin/SendBox.vue
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<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 () => {
|
||||||
|
try {
|
||||||
|
const { results, count: addressCount } = await api.fetch(
|
||||||
|
`/admin/sendbox`
|
||||||
|
+ `?limit=${pageSize.value}`
|
||||||
|
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||||
|
+ (adminSendBoxTabAddress.value ? `&address=${adminSendBoxTabAddress.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>
|
||||||
194
frontend/src/views/admin/SenderAccess.vue
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, h, onMounted, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
import { api } from '../../api'
|
||||||
|
|
||||||
|
const { localeCache, loading } = useGlobalState()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
address: 'Address',
|
||||||
|
success: 'Success',
|
||||||
|
enable: 'Enable',
|
||||||
|
disable: 'Disable',
|
||||||
|
modify: 'Modify',
|
||||||
|
created_at: 'Created At',
|
||||||
|
action: 'Action',
|
||||||
|
itemCount: 'itemCount',
|
||||||
|
modalTip: 'Please input the sender balance',
|
||||||
|
balance: 'Balance',
|
||||||
|
query: 'Query',
|
||||||
|
ok: 'OK'
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
address: '地址',
|
||||||
|
success: '成功',
|
||||||
|
enable: '启用',
|
||||||
|
disable: '禁用',
|
||||||
|
modify: '修改',
|
||||||
|
created_at: '创建时间',
|
||||||
|
action: '操作',
|
||||||
|
itemCount: '总数',
|
||||||
|
modalTip: '请输入发件额度',
|
||||||
|
balance: '余额',
|
||||||
|
query: '查询',
|
||||||
|
ok: '确定'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = ref([])
|
||||||
|
const count = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
|
||||||
|
const curRow = ref({})
|
||||||
|
const showModal = ref(false)
|
||||||
|
const senderBalance = ref(0)
|
||||||
|
const senderEnabled = ref(false)
|
||||||
|
|
||||||
|
const addressQuery = ref('')
|
||||||
|
|
||||||
|
const updateData = async () => {
|
||||||
|
try {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
});
|
||||||
|
showModal.value = false;
|
||||||
|
message.success(t("success"));
|
||||||
|
await fetchData()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const { results, count: addressCount } = await api.fetch(
|
||||||
|
`/admin/address_sender`
|
||||||
|
+ `?limit=${pageSize.value}`
|
||||||
|
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||||
|
+ (addressQuery.value ? `&address=${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('address'),
|
||||||
|
key: "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('created_at'),
|
||||||
|
key: "created_at"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('balance'),
|
||||||
|
key: "balance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Enabled",
|
||||||
|
key: "enabled",
|
||||||
|
render(row) {
|
||||||
|
return h('div', [
|
||||||
|
h('span', row.enabled ? t('enable') : t('disable'))
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('action'),
|
||||||
|
key: 'actions',
|
||||||
|
render(row) {
|
||||||
|
return h('div', [
|
||||||
|
h(NButton,
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
ghost: true,
|
||||||
|
onClick: () => {
|
||||||
|
showModal.value = true;
|
||||||
|
curRow.value = row;
|
||||||
|
senderEnabled.value = row.enabled ? true : false;
|
||||||
|
senderBalance.value = row.balance;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => t('modify') }
|
||||||
|
)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
watch([page, pageSize], async () => {
|
||||||
|
await fetchData()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<n-modal v-model:show="showModal" preset="dialog">
|
||||||
|
<p>{{ curRow.address }}</p>
|
||||||
|
<p>{{ t('modalTip') }}</p>
|
||||||
|
<n-form-item :show-label="false">
|
||||||
|
<n-checkbox v-model:checked="senderEnabled">
|
||||||
|
{{ t('enable') }}
|
||||||
|
</n-checkbox>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :show-label="false">
|
||||||
|
<n-input-number v-model:value="senderBalance" :min="0" :max="1000" />
|
||||||
|
</n-form-item>
|
||||||
|
<template #action>
|
||||||
|
<n-button :loading="loading" @click="updateData()" size="small" tertiary type="primary">
|
||||||
|
{{ t('ok') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
<n-input-group>
|
||||||
|
<n-input v-model:value="addressQuery" />
|
||||||
|
<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>
|
||||||
94
frontend/src/views/admin/Statistics.vue
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<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-card>
|
||||||
|
<n-row>
|
||||||
|
<n-col :span="6">
|
||||||
|
<n-statistic :label="t('userCount')" :value="statistics.userCount">
|
||||||
|
<template #prefix>
|
||||||
|
<n-icon :component="User" />
|
||||||
|
</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-col>
|
||||||
|
<n-col :span="6">
|
||||||
|
<n-statistic :label="t('activeUser')" :value="statistics.activeUserCount7days">
|
||||||
|
<template #prefix>
|
||||||
|
<n-icon :component="UserCheck" />
|
||||||
|
</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-col>
|
||||||
|
<n-col :span="6">
|
||||||
|
<n-statistic :label="t('mailCount')" :value="statistics.mailCount">
|
||||||
|
<template #prefix>
|
||||||
|
<n-icon :component="MailBulk" />
|
||||||
|
</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-col>
|
||||||
|
<n-col :span="6">
|
||||||
|
<n-statistic :label="t('sendMailCount')" :value="statistics.sendMailCount">
|
||||||
|
<template #prefix>
|
||||||
|
<n-icon :component="SendOutlined" />
|
||||||
|
</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-col>
|
||||||
|
</n-row>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
156
frontend/src/views/send/SendBox.vue
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<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 } = 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',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
itemCount: 'itemCount',
|
||||||
|
view: 'View',
|
||||||
|
ok: 'OK'
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
address: '地址',
|
||||||
|
success: '成功',
|
||||||
|
to_mail: '收件人邮箱',
|
||||||
|
subject: '主题',
|
||||||
|
created_at: '创建时间',
|
||||||
|
action: '操作',
|
||||||
|
refresh: '刷新',
|
||||||
|
itemCount: '总数',
|
||||||
|
view: '查看',
|
||||||
|
ok: '确定'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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 () => {
|
||||||
|
try {
|
||||||
|
const { results, count: addressCount } = await api.fetch(
|
||||||
|
`/api/sendbox`
|
||||||
|
+ `?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 () => {
|
||||||
|
await fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="settings.address">
|
||||||
|
<n-modal v-model:show="showModal" preset="dialog">
|
||||||
|
<pre>{{ curRow.raw }}</pre>
|
||||||
|
</n-modal>
|
||||||
|
<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>
|
||||||
|
<template #suffix>
|
||||||
|
<n-button @click="fetchData" type="primary" size="small" ghost>
|
||||||
|
{{ t('refresh') }}
|
||||||
|
</n-button>
|
||||||
|
</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>
|
||||||
248
frontend/src/views/send/SendMail.vue
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<script setup>
|
||||||
|
import '@wangeditor/editor/dist/css/style.css'
|
||||||
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||||
|
import { DomEditor } from '@wangeditor/editor'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { onMounted, onBeforeUnmount, ref, shallowRef } from 'vue'
|
||||||
|
import { useStorage } from '@vueuse/core'
|
||||||
|
import AdminContact from '../admin/AdminContact.vue'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
import { api } from '../../api'
|
||||||
|
import router from '../../router'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const isPreview = ref(false)
|
||||||
|
const editorRef = shallowRef()
|
||||||
|
|
||||||
|
const mailModel = useStorage('mailModelCache', {
|
||||||
|
fromName: "",
|
||||||
|
toName: "",
|
||||||
|
toMail: "",
|
||||||
|
subject: "",
|
||||||
|
contentType: 'text',
|
||||||
|
content: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
const { settings } = useGlobalState()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
successSend: 'Please check your sendbox. If failed, please check your balance or try again later.',
|
||||||
|
fromName: 'Your Name and Address, leave Name blank to use email address',
|
||||||
|
toName: 'Recipient Name and Address, leave Name blank to use email address',
|
||||||
|
subject: 'Subject',
|
||||||
|
options: 'Options',
|
||||||
|
edit: 'Edit',
|
||||||
|
preview: 'Preview',
|
||||||
|
content: 'Content',
|
||||||
|
send: 'Send',
|
||||||
|
requestAccess: 'Request Access',
|
||||||
|
requestAccessTip: 'You need to request access to send mail, if have request, please contact admin.',
|
||||||
|
send_balance: 'Send Mail Balance Left',
|
||||||
|
text: 'Text',
|
||||||
|
html: 'HTML',
|
||||||
|
'rich text': 'Rich Text',
|
||||||
|
tooLarge: 'Too large file, please upload file less than 1MB.',
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
successSend: '请查看您的发件箱, 如果失败, 请检查您的余额或稍后重试。',
|
||||||
|
fromName: '你的名称和地址,名称不填写则使用邮箱地址',
|
||||||
|
toName: '收件人名称和地址,名称不填写则使用邮箱地址',
|
||||||
|
subject: '主题',
|
||||||
|
options: '选项',
|
||||||
|
edit: '编辑',
|
||||||
|
preview: '预览',
|
||||||
|
content: '内容',
|
||||||
|
send: '发送',
|
||||||
|
requestAccess: '申请权限',
|
||||||
|
requestAccessTip: '您需要申请权限才能发送邮件, 如果已经申请过, 请联系管理员提升额度。',
|
||||||
|
send_balance: '剩余发送邮件额度',
|
||||||
|
text: '文本',
|
||||||
|
html: 'HTML',
|
||||||
|
'rich text': '富文本',
|
||||||
|
tooLarge: '文件过大, 请上传小于1MB的文件。',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const contentTypes = [
|
||||||
|
{ label: t('text'), value: 'text' },
|
||||||
|
{ label: t('html'), value: 'html' },
|
||||||
|
{ label: t('rich text'), value: 'rich' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const send = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/api/send_mail`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body:
|
||||||
|
JSON.stringify({
|
||||||
|
from_name: mailModel.value.fromName,
|
||||||
|
to_name: mailModel.value.toName,
|
||||||
|
to_mail: mailModel.value.toMail,
|
||||||
|
subject: mailModel.value.subject,
|
||||||
|
is_html: mailModel.value.contentType != 'text',
|
||||||
|
content: mailModel.value.content,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
mailModel.value = {
|
||||||
|
fromName: "",
|
||||||
|
toName: "",
|
||||||
|
toMail: "",
|
||||||
|
subject: "",
|
||||||
|
contentType: 'text',
|
||||||
|
content: "",
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
} finally {
|
||||||
|
message.success(t("successSend"));
|
||||||
|
router.push('/sendbox');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestAccess = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/api/requset_send_mail_access`,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({})
|
||||||
|
}
|
||||||
|
)
|
||||||
|
message.success(t("success"))
|
||||||
|
await api.getSettings();
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolbarConfig = {
|
||||||
|
excludeKeys: ["uploadVideo"]
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorConfig = {
|
||||||
|
MENU_CONF: {
|
||||||
|
'uploadImage': {
|
||||||
|
async customUpload() {
|
||||||
|
message.error(t('tooLarge'))
|
||||||
|
},
|
||||||
|
maxFileSize: 1 * 1024 * 1024,
|
||||||
|
base64LimitSize: 1 * 1024 * 1024,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
const editor = editorRef.value
|
||||||
|
if (editor == null) return
|
||||||
|
editor.destroy()
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleCreated = (editor) => {
|
||||||
|
editorRef.value = editor;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await api.getSettings();
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="center" v-if="settings.address">
|
||||||
|
<n-card>
|
||||||
|
<div v-if="!settings.send_balance || settings.send_balance <= 0">
|
||||||
|
<n-alert type="warning" show-icon>
|
||||||
|
{{ t('requestAccessTip') }}
|
||||||
|
<n-button type="primary" ghost @click="requestAccess">{{ t('requestAccess') }}</n-button>
|
||||||
|
</n-alert>
|
||||||
|
<br />
|
||||||
|
<AdminContact />
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<n-alert type="info" show-icon>
|
||||||
|
{{ t('send_balance') }}: {{ settings.send_balance }}
|
||||||
|
</n-alert>
|
||||||
|
<div class="right">
|
||||||
|
<n-button type="primary" @click="send">{{ t('send') }}</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="left">
|
||||||
|
<n-form :model="mailModel">
|
||||||
|
<n-form-item :label="t('fromName')" label-placement="top">
|
||||||
|
<n-input-group>
|
||||||
|
<n-input v-model:value="mailModel.fromName" />
|
||||||
|
<n-input :value="settings.address" disabled />
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="t('toName')" label-placement="top">
|
||||||
|
<n-input-group>
|
||||||
|
<n-input v-model:value="mailModel.toName" />
|
||||||
|
<n-input v-model:value="mailModel.toMail" />
|
||||||
|
</n-input-group>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="t('subject')" label-placement="top">
|
||||||
|
<n-input v-model:value="mailModel.subject" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="t('options')" label-placement="top">
|
||||||
|
<n-radio-group v-model:value="mailModel.contentType">
|
||||||
|
<n-radio-button v-for="option in contentTypes" :key="option.value" :value="option.value"
|
||||||
|
:label="option.label" />
|
||||||
|
</n-radio-group>
|
||||||
|
<n-button v-if="mailModel.contentType != 'text'" @click="isPreview = !isPreview"
|
||||||
|
style="margin-left: 10px;">
|
||||||
|
{{ isPreview ? t('edit') : t('preview') }}
|
||||||
|
</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item :label="t('content')" label-placement="top">
|
||||||
|
<n-card v-if="isPreview">
|
||||||
|
<div v-html="mailModel.content" />
|
||||||
|
</n-card>
|
||||||
|
<div v-else-if="mailModel.contentType == 'rich'" style="border: 1px solid #ccc">
|
||||||
|
<Toolbar style="border-bottom: 1px solid #ccc" :defaultConfig="toolbarConfig"
|
||||||
|
:editor="editorRef" mode="default" />
|
||||||
|
<Editor style="height: 500px; overflow-y: hidden;" v-model="mailModel.content"
|
||||||
|
:defaultConfig="editorConfig" mode="default" @onCreated="handleCreated" />
|
||||||
|
</div>
|
||||||
|
<n-input v-else type="textarea" v-model:value="mailModel.content" :autosize="{
|
||||||
|
minRows: 3
|
||||||
|
}" />
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.n-card {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
text-align: left;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
text-align: center;
|
||||||
|
place-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
text-align: left;
|
||||||
|
place-items: left;
|
||||||
|
justify-content: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
text-align: right;
|
||||||
|
place-items: right;
|
||||||
|
justify-content: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
132
frontend/src/views/user/Account.vue
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
|
||||||
|
const {
|
||||||
|
jwt, localeCache, settings, showPassword, mailboxSplitSize, useIframeShowMail
|
||||||
|
} = useGlobalState()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const showLogout = ref(false)
|
||||||
|
const showDelteAccount = ref(false)
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
mailboxSplitSize: 'Mailbox Split Size',
|
||||||
|
useIframeShowMail: 'Use iframe Show Mail',
|
||||||
|
logout: "Logout",
|
||||||
|
delteAccount: "Delete Account",
|
||||||
|
showPassword: 'Show Password',
|
||||||
|
password: 'Password',
|
||||||
|
passwordTip: 'Please copy the password and you can use it to login to your email account.',
|
||||||
|
logoutConfirm: 'Are you sure to logout?',
|
||||||
|
delteAccount: "Delete Account",
|
||||||
|
delteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
mailboxSplitSize: '邮箱界面分栏大小',
|
||||||
|
useIframeShowMail: '使用iframe显示邮件',
|
||||||
|
logout: '退出登录',
|
||||||
|
delteAccount: "删除账户",
|
||||||
|
showPassword: '查看密码',
|
||||||
|
password: '密码',
|
||||||
|
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
||||||
|
logoutConfirm: '确定要退出登录吗?',
|
||||||
|
delteAccount: "删除账户",
|
||||||
|
delteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
jwt.value = '';
|
||||||
|
await router.push('/')
|
||||||
|
location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAccount = async () => {
|
||||||
|
try {
|
||||||
|
await api.fetch(`/api/delete_address`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
jwt.value = '';
|
||||||
|
await router.push('/')
|
||||||
|
location.reload()
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="center" v-if="settings.address">
|
||||||
|
<n-card>
|
||||||
|
<n-card>
|
||||||
|
<n-form-item-row :label="t('mailboxSplitSize')">
|
||||||
|
<n-slider v-model:value="mailboxSplitSize" :min="0.25" :max="0.75" :step="0.01" :marks="{
|
||||||
|
0.25: '0.25',
|
||||||
|
0.5: '0.5',
|
||||||
|
0.75: '0.75'
|
||||||
|
}" />
|
||||||
|
</n-form-item-row>
|
||||||
|
<n-form-item-row :label="t('useIframeShowMail')">
|
||||||
|
<n-switch v-model:value="useIframeShowMail" :round="false" />
|
||||||
|
</n-form-item-row>
|
||||||
|
</n-card>
|
||||||
|
<n-button @click="showPassword = true" type="primary" secondary block strong>
|
||||||
|
{{ t('showPassword') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="showLogout = true" secondary block strong>
|
||||||
|
{{ t('logout') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="showDelteAccount = true" type="error" secondary block strong>
|
||||||
|
{{ t('delteAccount') }}
|
||||||
|
</n-button>
|
||||||
|
</n-card>
|
||||||
|
<n-modal v-model:show="showPassword" preset="dialog" :title="t('password')">
|
||||||
|
<span>
|
||||||
|
<p>{{ t("passwordTip") }}</p>
|
||||||
|
</span>
|
||||||
|
<n-card>
|
||||||
|
<b>{{ jwt }}</b>
|
||||||
|
</n-card>
|
||||||
|
</n-modal>
|
||||||
|
<n-modal v-model:show="showLogout" preset="dialog" :title="t('logout')">
|
||||||
|
<p>{{ t('logoutConfirm') }}</p>
|
||||||
|
<template #action>
|
||||||
|
<n-button :loading="loading" @click="logout" size="small" tertiary type="primary">
|
||||||
|
{{ t('logout') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
<n-modal v-model:show="showDelteAccount" preset="dialog" :title="t('delteAccount')">
|
||||||
|
<p>{{ t('delteAccountConfirm') }}</p>
|
||||||
|
<template #action>
|
||||||
|
<n-button :loading="loading" @click="deleteAccount" size="small" tertiary type="error">
|
||||||
|
{{ t('delteAccount') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.n-card {
|
||||||
|
max-width: 800px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.n-button {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,9 +2,8 @@
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import Header from './Header.vue'
|
import { useGlobalState } from '../../store'
|
||||||
import { useGlobalState } from '../store'
|
import { api } from '../../api'
|
||||||
import { api } from '../api'
|
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const sourcePrefix = ref("")
|
const sourcePrefix = ref("")
|
||||||
@@ -43,7 +42,6 @@ const { t } = useI18n({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getSettings = async () => {
|
const getSettings = async () => {
|
||||||
await api.getSettings()
|
|
||||||
sourcePrefix.value = settings.value.auto_reply.source_prefix || ""
|
sourcePrefix.value = settings.value.auto_reply.source_prefix || ""
|
||||||
enableAutoReply.value = settings.value.auto_reply.enabled || false
|
enableAutoReply.value = settings.value.auto_reply.enabled || false
|
||||||
name.value = settings.value.auto_reply.name || ""
|
name.value = settings.value.auto_reply.name || ""
|
||||||
@@ -3,7 +3,6 @@ import { fileURLToPath, URL } from 'node:url'
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
import { splitVendorChunkPlugin } from 'vite';
|
|
||||||
import AutoImport from 'unplugin-auto-import/vite'
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
import Components from 'unplugin-vue-components/vite'
|
import Components from 'unplugin-vue-components/vite'
|
||||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||||
@@ -13,13 +12,21 @@ import topLevelAwait from "vite-plugin-top-level-await";
|
|||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
outDir: '../dist',
|
outDir: './dist',
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks(id) {
|
||||||
|
if (id.includes('wangeditor')) {
|
||||||
|
return 'vendor-wangeditor';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
wasm(),
|
wasm(),
|
||||||
topLevelAwait(),
|
topLevelAwait(),
|
||||||
splitVendorChunkPlugin(),
|
|
||||||
AutoImport({
|
AutoImport({
|
||||||
imports: [
|
imports: [
|
||||||
'vue',
|
'vue',
|
||||||
|
|||||||
2
smtp_proxy_server/.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
proxy_url=https://temp-email-api.xxx.xxx
|
||||||
|
port=8025
|
||||||
161
smtp_proxy_server/.gitignore
vendored
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
test*
|
||||||
12
smtp_proxy_server/docker-compose.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
smtp_proxy_server:
|
||||||
|
image: ghcr.io/dreamhunter2333/cloudflare_temp_email/smtp_proxy_server:latest
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: dockerfile
|
||||||
|
container_name: "smtp_proxy_server"
|
||||||
|
ports:
|
||||||
|
- "8025:8025"
|
||||||
|
environment:
|
||||||
|
- proxy_url=https://temp-email-api.xxx.xxx
|
||||||
|
- port=8025
|
||||||
7
smtp_proxy_server/dockerfile
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt /requirements.txt
|
||||||
|
RUN python3 -m pip install -r /requirements.txt
|
||||||
|
COPY . /app
|
||||||
|
ENTRYPOINT [ "python3", "server.py" ]
|
||||||
3
smtp_proxy_server/requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
aiosmtpd==1.4.5
|
||||||
|
pydantic-settings==2.2.1
|
||||||
|
requests==2.31.0
|
||||||
147
smtp_proxy_server/server.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import email
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
from aiosmtpd.controller import Controller
|
||||||
|
from aiosmtpd.smtp import SMTP, Session, Envelope, AuthResult, LoginPassword
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
)
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
proxy_url: str = "http://localhost:8787"
|
||||||
|
port: int = 8025
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
env_file = ".env"
|
||||||
|
|
||||||
|
|
||||||
|
class CustomSMTPHandler:
|
||||||
|
|
||||||
|
def authenticator(self, server, session, envelope, mechanism, auth_data):
|
||||||
|
fail_nothandled = AuthResult(success=False, handled=False)
|
||||||
|
if mechanism not in ("LOGIN", "PLAIN"):
|
||||||
|
_logger.warning(f"Unsupported mechanism {mechanism}")
|
||||||
|
return fail_nothandled
|
||||||
|
if not isinstance(auth_data, LoginPassword):
|
||||||
|
_logger.warning(f"Invalid auth data {auth_data}")
|
||||||
|
return fail_nothandled
|
||||||
|
return AuthResult(success=True, auth_data=auth_data)
|
||||||
|
|
||||||
|
async def handle_DATA(self, server: SMTP, session: Session, envelope: Envelope) -> str:
|
||||||
|
_logger.info(
|
||||||
|
f"handle_DATA from {envelope.mail_from} to {envelope.rcpt_tos}"
|
||||||
|
)
|
||||||
|
if not isinstance(session.auth_data, LoginPassword):
|
||||||
|
return '530 Authentication required'
|
||||||
|
if len(envelope.rcpt_tos) != 1:
|
||||||
|
return '500 Only one recipient allowed'
|
||||||
|
# Only one recipient allowed
|
||||||
|
to_mail = envelope.rcpt_tos[0]
|
||||||
|
# Parse email
|
||||||
|
msg = email.message_from_string(envelope.content)
|
||||||
|
content_list = []
|
||||||
|
if msg.is_multipart():
|
||||||
|
for part in msg.walk():
|
||||||
|
content_type = part.get_content_type()
|
||||||
|
payload = part.get_payload(decode=True)
|
||||||
|
if content_type not in ["text/plain", "text/html"]:
|
||||||
|
_logger.warning(f"Skipping {content_type}")
|
||||||
|
continue
|
||||||
|
if not payload:
|
||||||
|
continue
|
||||||
|
content_list.append({
|
||||||
|
"type": content_type,
|
||||||
|
"value": payload.decode()
|
||||||
|
})
|
||||||
|
elif msg.get_content_type() in ["text/plain", "text/html"] and msg.get_payload(decode=True):
|
||||||
|
content_list.append({
|
||||||
|
"type": msg.get_content_type(),
|
||||||
|
"value": msg.get_payload(decode=True).decode()
|
||||||
|
})
|
||||||
|
|
||||||
|
if not content_list:
|
||||||
|
return '500 Invalid content'
|
||||||
|
body = max(
|
||||||
|
content_list,
|
||||||
|
key=lambda x: (x["type"] == "text/html", len(x["value"]))
|
||||||
|
)
|
||||||
|
from_name, _ = email.utils.parseaddr(
|
||||||
|
str(email.header.make_header(
|
||||||
|
email.header.decode_header(msg['From'])
|
||||||
|
))
|
||||||
|
)
|
||||||
|
to_mail_map = {}
|
||||||
|
for to in str(email.header.make_header(
|
||||||
|
email.header.decode_header(msg['To'])
|
||||||
|
)).split(","):
|
||||||
|
tmp_to_name, tmp_to_mail = email.utils.parseaddr(to)
|
||||||
|
to_mail_map[tmp_to_mail] = tmp_to_name
|
||||||
|
_logger.info(f"Parsed mail from {from_name} to {to_mail_map}")
|
||||||
|
# Send mail
|
||||||
|
send_body = {
|
||||||
|
"from_name": from_name,
|
||||||
|
"to_name": to_mail_map.get(to_mail),
|
||||||
|
"to_mail": to_mail,
|
||||||
|
"subject": str(email.header.make_header(
|
||||||
|
email.header.decode_header(msg['Subject'])
|
||||||
|
)),
|
||||||
|
"is_html": body["type"] == "text/html",
|
||||||
|
"content": body["value"],
|
||||||
|
}
|
||||||
|
_logger.info(f"Send mail {send_body}")
|
||||||
|
try:
|
||||||
|
res = requests.post(
|
||||||
|
f"{settings.proxy_url}/api/send_mail",
|
||||||
|
json=send_body, headers={
|
||||||
|
"Authorization": f"Bearer {session.auth_data.password.decode()}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if res.status_code != 200:
|
||||||
|
_logger.error(
|
||||||
|
"Failed to send mail "
|
||||||
|
f"code=[{res.status_code}] text=[{res.text}]"
|
||||||
|
)
|
||||||
|
return f'500 Internal server error code=[{res.status_code}] text=[{res.text}]'
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(e)
|
||||||
|
return '500 Internal server error'
|
||||||
|
|
||||||
|
return '250 OK'
|
||||||
|
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
|
handler = CustomSMTPHandler()
|
||||||
|
server = Controller(
|
||||||
|
handler,
|
||||||
|
hostname="",
|
||||||
|
port=settings.port,
|
||||||
|
auth_require_tls=False,
|
||||||
|
decode_data=True,
|
||||||
|
authenticator=handler.authenticator,
|
||||||
|
auth_exclude_mechanism=["DONT"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def start():
|
||||||
|
_logger.info(f"Starting server settings[{settings}]")
|
||||||
|
server.start()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
task = loop.create_task(start())
|
||||||
|
try:
|
||||||
|
loop.run_forever()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
_logger.info("Got KeyboardInterrupt, stopping")
|
||||||
|
server.stop()
|
||||||
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'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
147
vitepress-docs/docs/.vitepress/zh.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
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: '配置 SMTP 代理服务', link: 'feature/config-smtp-proxy' },
|
||||||
|
{ text: '发送邮件 API', link: 'feature/send-mail-api' },
|
||||||
|
{ text: '配置子域名邮箱', link: 'feature/subdomain' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '功能简介',
|
||||||
|
collapsed: false,
|
||||||
|
items: [
|
||||||
|
{ text: 'Admin 控制台', link: 'feature/admin' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ text: '参考', base: "/", link: 'reference' }
|
||||||
|
]
|
||||||
|
}
|
||||||
166
vitepress-docs/docs/en/cli.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# cloudflare temp email
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
- [x] Cloudflare D1 as a database
|
||||||
|
- [x] Deploy the front end with Cloudflare Pages
|
||||||
|
- [x] Deploy the backend with Cloudflare Workers
|
||||||
|
- [x] Email forwarding using Cloudflare Email Routing
|
||||||
|
- [x] Use password to login to the previous mailbox again.
|
||||||
|
- [x] Get Custom Name Email
|
||||||
|
- [x] Support multiple languages
|
||||||
|
- [x] Add access authorization, which can be used as a private site
|
||||||
|
- [x] Add auto reply feature
|
||||||
|
- [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
|
||||||
|
# create a database, and copy the output to wrangler.toml in the next step
|
||||||
|
wrangler d1 create dev
|
||||||
|
wrangler d1 execute dev --file=db/schema.sql
|
||||||
|
# schema update, if you have initialized the database before this date, you can execute this command to update
|
||||||
|
# wrangler d1 execute dev --file=db/2024-01-13-patch.sql
|
||||||
|
# wrangler d1 execute dev --file=db/2024-04-03-patch.sql
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Backend - Cloudflare workers
|
||||||
|
|
||||||
|
The first deployment will prompt you to create a project. Please fill in `production` for the `production` branch.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd worker
|
||||||
|
pnpm install
|
||||||
|
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"]
|
||||||
|
# admin contact information. If not configured, it will not be displayed. Any string can be configured.
|
||||||
|
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||||
|
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # your domain name
|
||||||
|
JWT_SECRET = "xxx" # Key used to generate jwt
|
||||||
|
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
|
||||||
|
# Allow users to create email addresses
|
||||||
|
ENABLE_USER_CREATE_EMAIL = true
|
||||||
|
# Allow users to delete messages
|
||||||
|
ENABLE_USER_DELETE_EMAIL = true
|
||||||
|
# Allow automatic replies to emails
|
||||||
|
ENABLE_AUTO_REPLY = false
|
||||||
|
# Footer text
|
||||||
|
# COPYRIGHT = "Dream Hunter"
|
||||||
|
# default send balance, if not set, it will be 0
|
||||||
|
# DEFAULT_SEND_BALANCE = 1
|
||||||
|
# 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
|
||||||
|
|
||||||
|
Before you can bind an email address to your Worker, you need to enable Email Routing and have at least one verified email address.
|
||||||
|
|
||||||
|
enable email route and config email forward catch-all to the worker
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Frontend - Cloudflare pages
|
||||||
|
|
||||||
|
The first deployment will prompt you to create a project. Please fill in `production` for the `production` branch.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
pnpm install
|
||||||
|
# add .env.local and modify VITE_API_BASE to your worker's url
|
||||||
|
# VITE_API_BASE=https://xxx.xxx.workers.dev - don't put / in the end
|
||||||
|
cp .env.example .env.local
|
||||||
|
pnpm build --emptyOutDir
|
||||||
|
pnpm run deploy
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Configure sending emails
|
||||||
|
|
||||||
|
Find the `SPF` record of `TXT` in the domain name `DNS` record, and add `include:relay.mailchannels.net`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
v=spf1 include:_spf.mx.cloudflare.net include:relay.mailchannels.net ~all
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a new `_mailchannels` record, the type is `TXT`, the content is `v=mc1 cfid=your worker domain name`
|
||||||
|
|
||||||
|
- The worker domain name here is the domain name of the back-end api. For example, if I deploy it at `https://temp-email-api.awsl.uk/`, fill in `v=mc1 cfid=awsl.uk`
|
||||||
|
- If your domain name is `https://temp-email-api.xxx.workers.dev`, fill in `v=mc1 cfid=xxx.workers.dev`
|
||||||
|
|
||||||
|
## Configure DKIM
|
||||||
|
|
||||||
|
Ref: [Adding-a-DKIM-Signature](https://support.mailchannels.com/hc/en-us/articles/7122849237389-Adding-a-DKIM-Signature)
|
||||||
|
|
||||||
|
Creating a DKIM private and public key:
|
||||||
|
Private key as PEM file and base64 encoded txt file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl genrsa 2048 | tee priv_key.pem | openssl rsa -outform der | openssl base64 -A > priv_key.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Public key as DNS record:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo -n "v=DKIM1;p=" > pub_key_record.txt && \
|
||||||
|
openssl rsa -in priv_key.pem -pubout -outform der | openssl base64 -A >> pub_key_record.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `TXT` record in `Cloudflare` all your mail domain `DNS`
|
||||||
|
|
||||||
|
- `_dmarc`: `v=DMARC1; p=none; adkim=r; aspf=r;`
|
||||||
|
- `mailchannels._domainkey`: `v=DKIM1; p=<content of the file pub_key_record.txt>`
|
||||||
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: 支持发送邮件(UI/API/SMTP)
|
||||||
|
details: 支持通过域名邮箱发送 txt 或者 html 邮件,支持 DKIM 签名, UI/API/SMTP 发送邮件
|
||||||
|
---
|
||||||
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-upload.png
Normal file
|
After Width: | Height: | Size: 21 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))
|
||||||
|
```
|
||||||
78
vitepress-docs/docs/zh/guide/cli/worker.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# 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"]
|
||||||
|
# admin 联系方式,不配置则不显示,可配置任意字符串
|
||||||
|
# ADMIN_CONTACT = "xx@xx.xxx"
|
||||||
|
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
|
||||||
|
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
|
||||||
|
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
|
||||||
|
# 是否允许用户创建邮件, 不配置则不允许
|
||||||
|
ENABLE_USER_CREATE_EMAIL = true
|
||||||
|
# 允许用户删除邮件, 不配置则不允许
|
||||||
|
ENABLE_USER_DELETE_EMAIL = true
|
||||||
|
# 允许自动回复邮件
|
||||||
|
ENABLE_AUTO_REPLY = false
|
||||||
|
# 前端界面页脚文本
|
||||||
|
# COPYRIGHT = "Dream Hunter"
|
||||||
|
# 默认发送邮件余额,如果不设置,将为 0
|
||||||
|
# DEFAULT_SEND_BALANCE = 1
|
||||||
|
# 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`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 打开 `worker` 的 `url`,如果显示 `OK` 说明部署成功
|
||||||
|
>
|
||||||
|
> 打开 `/health_check`,如果显示 `OK` 说明部署成功
|
||||||
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 控制台密码`, 不配置则不允许访问控制台。
|
||||||
|
|
||||||
|

|
||||||
41
vitepress-docs/docs/zh/guide/feature/config-smtp-proxy.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# 搭建 SMTP 代理服务
|
||||||
|
|
||||||
|
## 为什么需要 SMTP 代理服务
|
||||||
|
|
||||||
|
SMTP 的应用场景更加广泛
|
||||||
|
|
||||||
|
## 如何搭建 SMTP 代理服务
|
||||||
|
|
||||||
|
### Local Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd smtp_proxy_server/
|
||||||
|
# 复制配置文件, 并修改配置文件
|
||||||
|
# 你的 worker 地址,proxy_url=https://temp-email-api.xxx.xxx
|
||||||
|
# 你的 SMTP 服务端口,port=8025
|
||||||
|
cp .env.example .env
|
||||||
|
python3 -m venv venv
|
||||||
|
./venv/bin/python3 -m pip install -r requirements.txt
|
||||||
|
./venv/bin/python3 server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd smtp_proxy_server/
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
修改 docker-compose.yaml 中的环境变量, 注意选择合适的 `tag`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
smtp_proxy_server:
|
||||||
|
image: ghcr.io/dreamhunter2333/cloudflare_temp_email/smtp_proxy_server:latest
|
||||||
|
container_name: "smtp_proxy_server"
|
||||||
|
ports:
|
||||||
|
- "8025:8025"
|
||||||
|
environment:
|
||||||
|
- proxy_url=https://temp-email-api.xxx.xxx
|
||||||
|
- port=8025
|
||||||
|
```
|
||||||
49
vitepress-docs/docs/zh/guide/feature/send-mail-api.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# 发送邮件 API
|
||||||
|
|
||||||
|
## 通过 HTTP API 发送邮件
|
||||||
|
|
||||||
|
这是一个 `python` 的例子,使用 `requests` 库发送邮件。
|
||||||
|
|
||||||
|
```python
|
||||||
|
send_body = {
|
||||||
|
"from_name": "发件人名字",
|
||||||
|
"to_name": "收件人名字",
|
||||||
|
"to_mail": "收件人地址",
|
||||||
|
"subject": "邮件主题",
|
||||||
|
"is_html": False, # 根据内容设置是否为 HTML
|
||||||
|
"content": "<邮件内容:html 或者 文本>",
|
||||||
|
}
|
||||||
|
|
||||||
|
res = requests.post(
|
||||||
|
"http://localhost:8787/api/send_mail",
|
||||||
|
json=send_body, headers={
|
||||||
|
"Authorization": f"Bearer {session.auth_data.password.decode()}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 通过 SMTP 发送邮件
|
||||||
|
|
||||||
|
请先参考 [配置 SMTP 代理](/zh/guide/config-smtp-proxy.html)。
|
||||||
|
|
||||||
|
这是一个 `python` 的例子,使用 `smtplib` 库发送邮件。
|
||||||
|
|
||||||
|
`JWT令牌密码`: 即为邮箱登录密码,可以在 UI 界面中查看密码菜单中查看。
|
||||||
|
|
||||||
|
```python
|
||||||
|
import smtplib
|
||||||
|
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
|
||||||
|
|
||||||
|
with smtplib.SMTP('localhost', 8025) as smtp:
|
||||||
|
smtp.login("jwt", "此处填写你的JWT令牌密码")
|
||||||
|
message = MIMEMultipart()
|
||||||
|
message['From'] = "Me <me@awsl.uk>"
|
||||||
|
message['To'] = "Admin <admin@awsl.uk>"
|
||||||
|
message['Subject'] = "测试主题"
|
||||||
|
message.attach(MIMEText("测试内容", 'html'))
|
||||||
|
smtp.sendmail("me@awsl.uk", "admin@awsl.uk", message.as_string())
|
||||||
|
```
|
||||||
5
vitepress-docs/docs/zh/guide/feature/subdomain.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# 配置子域名邮箱
|
||||||
|
|
||||||
|
参考
|
||||||
|
|
||||||
|
- [配置子域名邮箱](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/164#issuecomment-2082612710)
|
||||||
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>
|
||||||
40
vitepress-docs/docs/zh/guide/ui/worker.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# 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`, 点击 `Deploy`
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 上传需要先点击左侧菜单的 Explorer,
|
||||||
|
> 在文件列表的窗口里点击鼠标右键,在右键菜单里找到 `Upload`,
|
||||||
|
> 请参考下面的截图
|
||||||
|
>
|
||||||
|
> 参考: [issues156](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/156#issuecomment-2079453822)
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
5. 点击 `Settings` -> `Trggers`, 这里可以添加自己的域名,你也可以使用自动生成的 `*.workers.dev` 的域名。记录下这个域名,后面部署前端会用到。
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> 打开 `worker` 的 `url`,如果显示 `OK` 说明部署成功
|
||||||
|
>
|
||||||
|
> 打开 `/health_check`,如果显示 `OK` 说明部署成功
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||