Compare commits
1 Commits
v0.4.0
...
feature/ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09e0d0b7d7 |
23
.github/ISSUE_TEMPLATE/bug-反馈.md
vendored
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug 反馈
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: "[BUG]"
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 复现步骤
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 预期行为
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 部署方式
|
|
||||||
|
|
||||||
- [ ] cli 部署
|
|
||||||
- [ ] 用户界面部署
|
|
||||||
|
|
||||||
## 浏览器环境
|
|
||||||
16
.github/ISSUE_TEMPLATE/feature-request.md
vendored
@@ -1,16 +0,0 @@
|
|||||||
---
|
|
||||||
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
@@ -1,44 +0,0 @@
|
|||||||
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
@@ -1,48 +0,0 @@
|
|||||||
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
@@ -1,43 +0,0 @@
|
|||||||
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
@@ -1,48 +0,0 @@
|
|||||||
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
@@ -1,46 +0,0 @@
|
|||||||
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,3 +1,2 @@
|
|||||||
dist/
|
dist/
|
||||||
test/
|
test/
|
||||||
.vscode/
|
|
||||||
|
|||||||
34
CHANGELOG
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# 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`
|
||||||
222
CHANGELOG.md
@@ -1,222 +0,0 @@
|
|||||||
# CHANGE LOG
|
|
||||||
|
|
||||||
## main branch
|
|
||||||
|
|
||||||
### DB Changes
|
|
||||||
|
|
||||||
新增 user 相关表,用于存储用户信息
|
|
||||||
|
|
||||||
- `db/2024-05-08-patch.sql`
|
|
||||||
|
|
||||||
### config changs
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# kv config for send email verification code
|
|
||||||
# [[kv_namespaces]]
|
|
||||||
# binding = "KV"
|
|
||||||
# id = "xxxx"
|
|
||||||
```
|
|
||||||
|
|
||||||
### function changs
|
|
||||||
|
|
||||||
- 增加用户注册功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证
|
|
||||||
- 增加默认以文本显示邮件,文本和HTML邮箱显示方式切换按钮
|
|
||||||
- 修复 `BUG` 随机生成的邮箱名字不合法 #211
|
|
||||||
- `admin` 邮件页面支持邮件内容搜索 #210
|
|
||||||
- 修复删除地址时邮件未删除的BUG #213
|
|
||||||
- UI 增加全局标签页位置配置, 侧边距配置
|
|
||||||
|
|
||||||
## v0.3.3
|
|
||||||
|
|
||||||
- 修复 Admin 删除邮件报错
|
|
||||||
- UI: 回复邮件按钮, 引用原始邮件文本 #186
|
|
||||||
- 添加发送邮件地址黑名单
|
|
||||||
- 添加 `CF Turnstile` 人机验证配置
|
|
||||||
- 添加 `/external/api/send_mail` 发送邮件 api, 使用 body 验证 #194
|
|
||||||
|
|
||||||
## v0.3.2
|
|
||||||
|
|
||||||
## What's Changed
|
|
||||||
|
|
||||||
- UI: 添加回复邮件按钮
|
|
||||||
- 添加定时清理功能,可在 admin 页面配置(需要在配置文件启用定时任务)
|
|
||||||
- 修复删除账户无反应的问题
|
|
||||||
|
|
||||||
* feat: UI: MailBox add reply button by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/187
|
|
||||||
* feat: add cron auto clean up by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/189
|
|
||||||
* fix: delete account by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/190
|
|
||||||
|
|
||||||
## v0.3.1
|
|
||||||
|
|
||||||
### 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` 账户配置页面,支持配置用户注册名称黑名单
|
|
||||||
|
|
||||||
* feat: support admin create address && add ENABLE_USER_CREATE_EMAIL co… by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/175
|
|
||||||
* feat: add SMTP proxy server by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/177
|
|
||||||
* fix: cf ui var is string by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/178
|
|
||||||
* fix: UI mailbox 100vh to 80vh by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/179
|
|
||||||
* fix: smtp_proxy_server hostname && add docker image for linux/arm64 by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/180
|
|
||||||
* fix: some browser do not support wasm by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/182
|
|
||||||
* feat: add COPYRIGHT by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/183
|
|
||||||
* feat: UI: add user page: useIframeShowMail && mailboxSplitSize by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/184
|
|
||||||
* feat: add address_block_list for new address by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/185
|
|
||||||
|
|
||||||
## 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`
|
|
||||||
209
README.md
@@ -1,23 +1,24 @@
|
|||||||
# 使用 cloudflare 免费服务,搭建临时邮箱
|
# 使用 cloudflare 免费服务,搭建临时邮箱
|
||||||
|
|
||||||
> 本项目仅供学习和个人用途,请勿将其用于任何违法行为,否则后果自负。
|
## [English](README_EN.md)
|
||||||
|
|
||||||
## [查看部署文档](https://temp-mail-docs.awsl.uk)
|
[CHANGELOG](CHANGELOG)
|
||||||
|
|
||||||
[](https://deploy.workers.cloudflare.com/?url=https://github.com/dreamhunter2333/cloudflare_temp_email)
|
[Backend](https://temp-email-api.dreamhunter2333.xyz/)
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
[Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/github-action.html)
|
[Frontend](https://temp-email.dreamhunter2333.xyz/)
|
||||||
|

|
||||||
[English Docs](https://temp-mail-docs.awsl.uk/en/)
|

|
||||||
|

|
||||||
## [CHANGELOG](CHANGELOG.md)
|

|
||||||
|

|
||||||
## [在线演示](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" />
|
||||||
@@ -26,30 +27,172 @@
|
|||||||
</picture>
|
</picture>
|
||||||
|
|
||||||
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
|
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
|
||||||
- [查看部署文档](#查看部署文档)
|
- [English](#english)
|
||||||
- [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] 使用 `password` 重新登录之前的邮箱
|
- [x] Cloudflare D1 作为数据库
|
||||||
- [x] 获取自定义名字的邮箱,`admin` 可配置黑名单
|
- [x] 使用 Cloudflare Pages 部署前端
|
||||||
|
- [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 发送邮件
|
|
||||||
- [x] 添加完整的用户注册登录功能,可绑定邮箱地址,绑定后可自动获取邮箱JWT凭证切换不同邮箱
|
|
||||||
|
|
||||||
## 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
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
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,21 +0,0 @@
|
|||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
user_email TEXT UNIQUE NOT NULL,
|
|
||||||
password TEXT NOT NULL,
|
|
||||||
user_info TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_user_email ON users(user_email);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users_address (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
user_id INTEGER,
|
|
||||||
address_id INTEGER UNIQUE,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id);
|
|
||||||
@@ -8,8 +8,6 @@ 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,
|
||||||
@@ -19,8 +17,6 @@ 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,
|
||||||
@@ -28,8 +24,6 @@ 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,
|
||||||
@@ -41,8 +35,6 @@ 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,
|
||||||
@@ -51,51 +43,3 @@ 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
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
user_email TEXT UNIQUE NOT NULL,
|
|
||||||
password TEXT NOT NULL,
|
|
||||||
user_info TEXT,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_user_email ON users(user_email);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users_address (
|
|
||||||
id INTEGER PRIMARY KEY,
|
|
||||||
user_id INTEGER,
|
|
||||||
address_id INTEGER UNIQUE,
|
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_address_user_id ON users_address(user_id);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_users_address_address_id ON users_address(address_id);
|
|
||||||
|
|||||||
1
frontend/.gitignore
vendored
@@ -29,4 +29,3 @@ coverage
|
|||||||
|
|
||||||
.env.*
|
.env.*
|
||||||
*-dist/
|
*-dist/
|
||||||
components.d.ts
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||||
<link rel="icon" href="/logo.png" sizes="any">
|
<link rel="icon" href="/logo.png" sizes="any">
|
||||||
<link rel="apple-touch-icon" href="/logo.png">
|
<link rel="apple-touch-icon" href="/logo.png">
|
||||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -6,36 +6,27 @@
|
|||||||
"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:preview": "npm run build && wrangler pages deploy ./dist --branch preview",
|
"deploy": "npm run build && wrangler pages deploy ../dist --branch production"
|
||||||
"deploy": "npm run build && wrangler pages deploy ./dist --branch production"
|
|
||||||
},
|
},
|
||||||
"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",
|
"naive-ui": "^2.38.1",
|
||||||
"naive-ui": "^2.38.2",
|
|
||||||
"postal-mime": "^2.2.5",
|
|
||||||
"vooks": "^0.2.12",
|
"vooks": "^0.2.12",
|
||||||
"vue": "^3.4.26",
|
"vue": "^3.4.21",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-i18n": "^9.13.1",
|
"vue-i18n": "^9.10.2",
|
||||||
"vue-router": "^4.3.2"
|
"vue-router": "^4.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vicons/fa": "^0.12.0",
|
"@vicons/fa": "^0.12.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^4.6.2",
|
||||||
"unplugin-auto-import": "^0.17.5",
|
"unplugin-auto-import": "^0.17.5",
|
||||||
"unplugin-vue-components": "^0.27.0",
|
"unplugin-vue-components": "^0.26.0",
|
||||||
"vite": "^5.2.11",
|
"vite": "^5.2.6",
|
||||||
"vite-plugin-pwa": "^0.19.8",
|
"vite-plugin-pwa": "^0.19.7",
|
||||||
"vite-plugin-top-level-await": "^1.4.1",
|
"workbox-window": "^7.0.0"
|
||||||
"vite-plugin-wasm": "^3.3.0",
|
|
||||||
"workbox-window": "^7.1.0",
|
|
||||||
"wrangler": "^3.53.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6567
frontend/pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 50 KiB |
@@ -1,18 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { darkTheme, NGlobalStyle, zhCN } from 'naive-ui'
|
import { darkTheme, NGlobalStyle } 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 { localeCache, isDark, loading, useSideMargin } = useGlobalState()
|
const theme = computed(() => themeSwitch.value ? darkTheme : null)
|
||||||
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()
|
||||||
const showSideMargin = computed(() => !isMobile.value && !useSideMargin.value);
|
|
||||||
|
|
||||||
const { locale } = useI18n({
|
const { locale } = useI18n({
|
||||||
useScope: 'global',
|
useScope: 'global',
|
||||||
@@ -40,19 +38,16 @@ onMounted(async () => {
|
|||||||
<n-spin description="loading..." :show="loading">
|
<n-spin description="loading..." :show="loading">
|
||||||
<n-message-provider>
|
<n-message-provider>
|
||||||
<n-grid x-gap="12" :cols="12">
|
<n-grid x-gap="12" :cols="12">
|
||||||
<n-gi v-if="!showSideMargin" span="1"></n-gi>
|
<n-gi v-if="!isMobile" span="1"></n-gi>
|
||||||
<n-gi :span="showSideMargin ? 12 : 10">
|
<n-gi :span="isMobile ? 12 : 10">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<n-space vertical>
|
<n-space vertical>
|
||||||
<n-layout style="min-height: 80vh;">
|
<Header />
|
||||||
<Header />
|
<router-view></router-view>
|
||||||
<router-view></router-view>
|
|
||||||
</n-layout>
|
|
||||||
<Footer />
|
|
||||||
</n-space>
|
</n-space>
|
||||||
</div>
|
</div>
|
||||||
</n-gi>
|
</n-gi>
|
||||||
<n-gi v-if="!showSideMargin" span="1"></n-gi>
|
<n-gi v-if="!isMobile" span="1"></n-gi>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
<n-back-top />
|
<n-back-top />
|
||||||
</n-message-provider>
|
</n-message-provider>
|
||||||
|
|||||||
@@ -2,15 +2,12 @@ import { useGlobalState } from '../store'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||||
const {
|
const { loading, auth, jwt, settings, openSettings } = useGlobalState();
|
||||||
loading, auth, jwt, settings, openSettings,
|
const { showAuth, adminAuth, showAdminAuth } = useGlobalState();
|
||||||
userOpenSettings, userSettings,
|
|
||||||
showAuth, adminAuth, showAdminAuth, userJwt
|
|
||||||
} = useGlobalState();
|
|
||||||
|
|
||||||
const instance = axios.create({
|
const instance = axios.create({
|
||||||
baseURL: API_BASE,
|
baseURL: API_BASE,
|
||||||
timeout: 30000
|
timeout: 10000
|
||||||
});
|
});
|
||||||
|
|
||||||
const apiFetch = async (path, options = {}) => {
|
const apiFetch = async (path, options = {}) => {
|
||||||
@@ -20,7 +17,6 @@ const apiFetch = async (path, options = {}) => {
|
|||||||
method: options.method || 'GET',
|
method: options.method || 'GET',
|
||||||
data: options.body || null,
|
data: options.body || null,
|
||||||
headers: {
|
headers: {
|
||||||
'x-user-token': userJwt.value,
|
|
||||||
'x-custom-auth': auth.value,
|
'x-custom-auth': auth.value,
|
||||||
'x-admin-auth': adminAuth.value,
|
'x-admin-auth': adminAuth.value,
|
||||||
'Authorization': `Bearer ${jwt.value}`,
|
'Authorization': `Bearer ${jwt.value}`,
|
||||||
@@ -29,7 +25,7 @@ const apiFetch = async (path, options = {}) => {
|
|||||||
});
|
});
|
||||||
if (response.status === 401 && openSettings.value.auth) {
|
if (response.status === 401 && openSettings.value.auth) {
|
||||||
showAuth.value = true;
|
showAuth.value = true;
|
||||||
throw new Error("Unauthorized, you access password is wrong")
|
throw new Error("Unauthorized, you password is wrong")
|
||||||
}
|
}
|
||||||
if (response.status === 401 && path.startsWith("/admin")) {
|
if (response.status === 401 && path.startsWith("/admin")) {
|
||||||
showAdminAuth.value = true;
|
showAdminAuth.value = true;
|
||||||
@@ -53,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");
|
||||||
Object.assign(openSettings.value, {
|
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) => {
|
||||||
@@ -61,14 +57,8 @@ 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,
|
|
||||||
cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "",
|
|
||||||
});
|
|
||||||
if (openSettings.value.needAuth) {
|
if (openSettings.value.needAuth) {
|
||||||
showAuth.value = true;
|
showAuth.value = true;
|
||||||
}
|
}
|
||||||
@@ -85,41 +75,17 @@ const getSettings = async () => {
|
|||||||
const res = await apiFetch("/api/settings");;
|
const res = await apiFetch("/api/settings");;
|
||||||
settings.value = {
|
settings.value = {
|
||||||
address: res["address"],
|
address: res["address"],
|
||||||
auto_reply: res["auto_reply"],
|
auto_reply: res["auto_reply"]
|
||||||
has_v1_mails: res["has_v1_mails"],
|
|
||||||
send_balance: res["send_balance"],
|
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
settings.value.fetched = true;
|
settings.value.fetched = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminShowPassword = async (id) => {
|
||||||
const getUserOpenSettings = async (message) => {
|
|
||||||
try {
|
try {
|
||||||
const res = await api.fetch(`/user_api/open_settings`);
|
const { password } = await apiFetch(`/admin/show_password/${id}`);
|
||||||
Object.assign(userOpenSettings.value, res);
|
return password;
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "fetch settings failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getUserSettings = async (message) => {
|
|
||||||
try {
|
|
||||||
if (!userJwt.value) return;
|
|
||||||
const res = await api.fetch("/user_api/settings")
|
|
||||||
Object.assign(userSettings.value, res)
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
} finally {
|
|
||||||
userSettings.value.fetched = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const adminShowAddressCredential = async (id) => {
|
|
||||||
try {
|
|
||||||
const { jwt: addressCredential } = await apiFetch(`/admin/show_password/${id}`);
|
|
||||||
return addressCredential;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -135,24 +101,10 @@ const adminDeleteAddress = async (id) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindUserAddress = async () => {
|
|
||||||
if (!userJwt.value) return;
|
|
||||||
try {
|
|
||||||
await apiFetch(`/user_api/bind_address`, {
|
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
fetch: apiFetch,
|
fetch: apiFetch,
|
||||||
getSettings,
|
getSettings: getSettings,
|
||||||
getOpenSettings,
|
getOpenSettings: getOpenSettings,
|
||||||
getUserOpenSettings,
|
adminShowPassword: adminShowPassword,
|
||||||
getUserSettings,
|
adminDeleteAddress: adminDeleteAddress,
|
||||||
adminShowAddressCredential,
|
|
||||||
adminDeleteAddress,
|
|
||||||
bindUserAddress,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,438 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { watch, onMounted, ref, onBeforeUnmount } from "vue";
|
|
||||||
import { useRouter } from "vue-router";
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useGlobalState } from '../store'
|
|
||||||
import { CloudDownloadRound, ReplyFilled } from '@vicons/material'
|
|
||||||
import { useIsMobile } from '../utils/composables'
|
|
||||||
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
|
||||||
|
|
||||||
const message = useMessage()
|
|
||||||
const isMobile = useIsMobile()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
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
|
|
||||||
},
|
|
||||||
showReply: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
requried: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const {
|
|
||||||
localeCache, isDark, mailboxSplitSize, indexTab,
|
|
||||||
useIframeShowMail, sendMailModel, preferShowTextMail
|
|
||||||
} = 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 showTextMail = ref(preferShowTextMail.value)
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || '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?',
|
|
||||||
reply: 'Reply',
|
|
||||||
showTextMail: 'Show Text Mail',
|
|
||||||
showHtmlMail: 'Show Html Mail'
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
success: '成功',
|
|
||||||
autoRefresh: '自动刷新',
|
|
||||||
refreshAfter: '{msg}秒后刷新',
|
|
||||||
refresh: '刷新',
|
|
||||||
downloadMail: '下载邮件',
|
|
||||||
attachments: '查看附件',
|
|
||||||
pleaseSelectMail: "请选择一封邮件查看。",
|
|
||||||
delete: '删除',
|
|
||||||
deleteMailTip: '确定要删除这封邮件吗?',
|
|
||||||
reply: '回复',
|
|
||||||
showTextMail: '显示纯文本邮件',
|
|
||||||
showHtmlMail: '显示HTML邮件'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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 ? (isDark.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 replyMail = async () => {
|
|
||||||
const emailRegex = /(.+?) <(.+?)>/;
|
|
||||||
let toMail = curMail.value.originalSource;
|
|
||||||
let toName = ""
|
|
||||||
const match = emailRegex.exec(curMail.value.source);
|
|
||||||
if (match) {
|
|
||||||
toName = match[1];
|
|
||||||
toMail = match[2];
|
|
||||||
}
|
|
||||||
Object.assign(sendMailModel.value, {
|
|
||||||
toName: toName,
|
|
||||||
toMail: toMail,
|
|
||||||
subject: `${t('reply')}: ${curMail.value.subject}`,
|
|
||||||
contentType: 'rich',
|
|
||||||
content: curMail.value.text ? `<p><br></p><blockquote>${curMail.value.text}</blockquote><p><br></p>` : '',
|
|
||||||
});
|
|
||||||
indexTab.value = 'sendmail';
|
|
||||||
};
|
|
||||||
|
|
||||||
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" tertiary>
|
|
||||||
{{ t('refresh') }}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
<div style="overflow: auto; height: 80vh;">
|
|
||||||
<n-list hoverable clickable>
|
|
||||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
|
||||||
:class="mailItemClass(row)">
|
|
||||||
<n-thing :title="row.subject">
|
|
||||||
<template #description>
|
|
||||||
<n-tag type="info">
|
|
||||||
ID: {{ row.id }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
{{ row.created_at }}
|
|
||||||
</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)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="CloudDownloadRound" />
|
|
||||||
</template>
|
|
||||||
{{ t('downloadMail') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button v-if="showReply" size="small" tertiary type="info" @click="replyMail">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="ReplyFilled" />
|
|
||||||
</template>
|
|
||||||
{{ t('reply') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
|
|
||||||
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
<pre v-if="showTextMail" style="margin-top: 10px;">{{ curMail.text }}</pre>
|
|
||||||
<iframe v-else-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" :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)">
|
|
||||||
<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-button v-if="showReply" size="small" tertiary type="info" @click="replyMail">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="ReplyFilled" />
|
|
||||||
</template>
|
|
||||||
{{ t('reply') }}
|
|
||||||
</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%;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, watch, defineModel, onMounted } from "vue";
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useGlobalState } from '../store'
|
|
||||||
const { localeCache, openSettings, isDark } = useGlobalState()
|
|
||||||
|
|
||||||
const cfToken = defineModel('value')
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
refresh: 'Refresh'
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
refresh: '刷新'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const cfTurnstileId = ref("")
|
|
||||||
const turnstileLoading = ref(false)
|
|
||||||
|
|
||||||
const checkCfTurnstile = async (remove) => {
|
|
||||||
if (!openSettings.value.cfTurnstileSiteKey) return;
|
|
||||||
turnstileLoading.value = true;
|
|
||||||
try {
|
|
||||||
let container = document.getElementById("cf-turnstile");
|
|
||||||
let count = 100;
|
|
||||||
while (!container && count-- > 0) {
|
|
||||||
container = document.getElementById("cf-turnstile");
|
|
||||||
await new Promise(r => setTimeout(r, 10));
|
|
||||||
}
|
|
||||||
count = 100;
|
|
||||||
while (!window.turnstile && count-- > 0) {
|
|
||||||
await new Promise(r => setTimeout(r, 10));
|
|
||||||
}
|
|
||||||
if (remove && cfTurnstileId.value) {
|
|
||||||
window.turnstile.remove(cfTurnstileId.value);
|
|
||||||
}
|
|
||||||
cfTurnstileId.value = window.turnstile.render(
|
|
||||||
"#cf-turnstile",
|
|
||||||
{
|
|
||||||
sitekey: openSettings.value.cfTurnstileSiteKey,
|
|
||||||
language: localeCache.value == 'zh' ? 'zh-CN' : 'en-US',
|
|
||||||
theme: isDark.value ? 'dark' : 'light',
|
|
||||||
callback: function (token) {
|
|
||||||
cfToken.value = token;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
turnstileLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(isDark, async (isDark) => {
|
|
||||||
checkCfTurnstile(true)
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
cfToken.value = "";
|
|
||||||
checkCfTurnstile(true);
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="openSettings.cfTurnstileSiteKey" class="center">
|
|
||||||
<n-spin description="loading..." :show="turnstileLoading">
|
|
||||||
<n-form-item-row>
|
|
||||||
<div id="cf-turnstile"></div>
|
|
||||||
<n-button text @click="checkCfTurnstile(true)">
|
|
||||||
{{ t('refresh') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form-item-row>
|
|
||||||
</n-spin>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-button {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -12,7 +12,7 @@ const i18n = createI18n({
|
|||||||
'en': {
|
'en': {
|
||||||
messages: {}
|
messages: {}
|
||||||
},
|
},
|
||||||
'zh': {
|
'zhCN': {
|
||||||
messages: {}
|
messages: {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
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 UserLogin from '../views/user/UserLogin.vue'
|
import Settings from '../views/Settings.vue'
|
||||||
import User from '../views/User.vue'
|
|
||||||
import SendMail from '../views/index/SendMail.vue'
|
|
||||||
import Admin from '../views/Admin.vue'
|
import Admin from '../views/Admin.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
@@ -13,8 +11,8 @@ const router = createRouter({
|
|||||||
component: Index
|
component: Index
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/user',
|
path: '/settings',
|
||||||
component: User
|
component: Settings
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
|
|||||||
@@ -1,26 +1,19 @@
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { createGlobalState, useStorage, useDark, useToggle } from '@vueuse/core'
|
import { createGlobalState, useStorage } 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,
|
||||||
adminContact: '',
|
domains: [{
|
||||||
enableUserCreateEmail: false,
|
label: 'test.com',
|
||||||
enableUserDeleteEmail: false,
|
value: 'test.com'
|
||||||
enableAutoReply: false,
|
}]
|
||||||
domains: [],
|
|
||||||
copyright: 'Dream Hunter',
|
|
||||||
cfTurnstileSiteKey: '',
|
|
||||||
})
|
})
|
||||||
const settings = ref({
|
const settings = ref({
|
||||||
fetched: false,
|
fetched: false,
|
||||||
has_v1_mails: false,
|
|
||||||
send_balance: 0,
|
|
||||||
address: '',
|
address: '',
|
||||||
auto_reply: {
|
auto_reply: {
|
||||||
subject: '',
|
subject: '',
|
||||||
@@ -29,72 +22,27 @@ export const useGlobalState = createGlobalState(
|
|||||||
source_prefix: '',
|
source_prefix: '',
|
||||||
name: '',
|
name: '',
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
const sendMailModel = useStorage('sendMailModel', {
|
|
||||||
fromName: "",
|
|
||||||
toName: "",
|
|
||||||
toMail: "",
|
|
||||||
subject: "",
|
|
||||||
contentType: 'text',
|
|
||||||
content: "",
|
|
||||||
});
|
|
||||||
const showAuth = ref(false);
|
const showAuth = ref(false);
|
||||||
const showAddressCredential = 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', 'zh');
|
const localeCache = useStorage('locale', 'zhCN');
|
||||||
const adminTab = ref("account");
|
const themeSwitch = useStorage('themeSwitch', false);
|
||||||
const adminMailTabAddress = ref("");
|
const showLogin = ref(false);
|
||||||
const adminSendBoxTabAddress = ref("");
|
|
||||||
const mailboxSplitSize = useStorage('mailboxSplitSize', 0.25);
|
|
||||||
const useIframeShowMail = useStorage('useIframeShowMail', false);
|
|
||||||
const preferShowTextMail = useStorage('preferShowTextMail', false);
|
|
||||||
const userJwt = useStorage('userJwt', '');
|
|
||||||
const userTab = useStorage('userTab', 'user_settings');
|
|
||||||
const indexTab = useStorage('indexTab', 'mailbox');
|
|
||||||
const globalTabplacement = useStorage('globalTabplacement', 'top');
|
|
||||||
const useSideMargin = useStorage('useSideMargin', true);
|
|
||||||
const userOpenSettings = ref({
|
|
||||||
enable: false,
|
|
||||||
enableMailVerify: false,
|
|
||||||
});
|
|
||||||
const userSettings = ref({
|
|
||||||
/** @type {boolean} */
|
|
||||||
fetched: false,
|
|
||||||
/** @type {string} */
|
|
||||||
user_email: '',
|
|
||||||
/** @type {number} */
|
|
||||||
user_id: 0,
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
isDark,
|
|
||||||
toggleDark,
|
|
||||||
loading,
|
loading,
|
||||||
settings,
|
settings,
|
||||||
sendMailModel,
|
|
||||||
openSettings,
|
openSettings,
|
||||||
showAuth,
|
showAuth,
|
||||||
showAddressCredential,
|
|
||||||
auth,
|
auth,
|
||||||
jwt,
|
jwt,
|
||||||
localeCache,
|
localeCache,
|
||||||
|
themeSwitch,
|
||||||
adminAuth,
|
adminAuth,
|
||||||
showAdminAuth,
|
showAdminAuth,
|
||||||
adminTab,
|
showLogin,
|
||||||
adminMailTabAddress,
|
|
||||||
adminSendBoxTabAddress,
|
|
||||||
mailboxSplitSize,
|
|
||||||
useIframeShowMail,
|
|
||||||
preferShowTextMail,
|
|
||||||
userJwt,
|
|
||||||
userTab,
|
|
||||||
indexTab,
|
|
||||||
userOpenSettings,
|
|
||||||
userSettings,
|
|
||||||
globalTabplacement,
|
|
||||||
useSideMargin,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,20 +1,13 @@
|
|||||||
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
|
||||||
item.originalSource = item.source;
|
|
||||||
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 || '';
|
||||||
item.message = parsedEmail.body_html || parsedEmail.text || '';
|
item.message = parsedEmail.body_html || parsedEmail.text || '';
|
||||||
item.text = parsedEmail.text || '';
|
|
||||||
item.attachments = parsedEmail.attachments?.map((a_item) => {
|
item.attachments = parsedEmail.attachments?.map((a_item) => {
|
||||||
const blob_url = URL.createObjectURL(
|
const blob_url = URL.createObjectURL(
|
||||||
new Blob(
|
new Blob(
|
||||||
@@ -27,7 +20,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: humanFileSize(a_item.content?.length || 0),
|
size: a_item.content?.length || 0,
|
||||||
url: blob_url
|
url: blob_url
|
||||||
}
|
}
|
||||||
}) || [];
|
}) || [];
|
||||||
@@ -47,7 +40,6 @@ export async function processItem(item) {
|
|||||||
}
|
}
|
||||||
item.subject = parsedEmail.subject || 'No Subject';
|
item.subject = parsedEmail.subject || 'No Subject';
|
||||||
item.message = parsedEmail.html || parsedEmail.text || item.raw;
|
item.message = parsedEmail.html || parsedEmail.text || item.raw;
|
||||||
item.text = parsedEmail.text || '';
|
|
||||||
item.attachments = parsedEmail.attachments?.map((a_item) => {
|
item.attachments = parsedEmail.attachments?.map((a_item) => {
|
||||||
const blob_url = URL.createObjectURL(
|
const blob_url = URL.createObjectURL(
|
||||||
new Blob(
|
new Blob(
|
||||||
@@ -60,7 +52,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: humanFileSize(a_item.content?.length || 0),
|
size: a_item.content?.length || 0,
|
||||||
url: blob_url
|
url: blob_url
|
||||||
}
|
}
|
||||||
}) || [];
|
}) || [];
|
||||||
@@ -70,7 +62,6 @@ 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,6 +0,0 @@
|
|||||||
export const hashPassword = async (password) => {
|
|
||||||
// user crypto to hash password
|
|
||||||
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(password));
|
|
||||||
const hashArray = Array.from(new Uint8Array(digest));
|
|
||||||
return hashArray.map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,20 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from 'vue';
|
import { ref, h, onMounted, watch } 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 SenderAccess from './admin/SenderAccess.vue'
|
const { localeCache, adminAuth, showAdminAuth } = useGlobalState()
|
||||||
import Statistics from "./admin/Statistics.vue"
|
const router = useRouter()
|
||||||
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 UserManagement from './admin/UserManagement.vue';
|
|
||||||
import UserSettings from './admin/UserSettings.vue';
|
|
||||||
import Mails from './admin/Mails.vue';
|
|
||||||
import MailsUnknow from './admin/MailsUnknow.vue';
|
|
||||||
import Maintenance from './admin/Maintenance.vue';
|
|
||||||
import Appearance from './common/Appearance.vue';
|
|
||||||
|
|
||||||
const {
|
|
||||||
localeCache, adminAuth, showAdminAuth, adminTab, loading, globalTabplacement
|
|
||||||
} = 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()
|
||||||
@@ -34,98 +27,371 @@ const { t } = useI18n({
|
|||||||
locale: localeCache.value || 'zh',
|
locale: localeCache.value || 'zh',
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
accessHeader: 'Admin Password',
|
title: 'Temp Email Admin',
|
||||||
accessTip: 'Please enter the admin password',
|
auth: 'Admin Auth',
|
||||||
|
home: 'Home',
|
||||||
|
authTip: 'Please enter the correct auth code',
|
||||||
|
name: 'Name',
|
||||||
|
created_at: 'Created At',
|
||||||
|
showPass: 'Show Passwrod',
|
||||||
|
password: 'Password',
|
||||||
|
passwordTip: 'Please copy the password and you can use it to login to your email account.',
|
||||||
|
delete: 'Delete',
|
||||||
|
deleteTip: 'Are you sure to delete this email?',
|
||||||
|
refresh: 'Refresh',
|
||||||
mails: 'Emails',
|
mails: 'Emails',
|
||||||
|
itemCount: 'itemCount',
|
||||||
|
query: 'Query',
|
||||||
|
userCount: 'User Count',
|
||||||
|
activeUser: '7 days Active User',
|
||||||
|
mailCount: 'Mail Count',
|
||||||
account: 'Account',
|
account: 'Account',
|
||||||
account_create: 'Create Account',
|
unknow: 'Unknow',
|
||||||
account_settings: 'Account Settings',
|
addressQueryTip: 'Leave blank to query all addresses',
|
||||||
user_management: 'User Management',
|
|
||||||
user_settings: 'User Settings',
|
|
||||||
unknow: 'Mails with unknow receiver',
|
|
||||||
senderAccess: 'Sender Access Control',
|
|
||||||
sendBox: 'Send Box',
|
|
||||||
statistics: 'Statistics',
|
|
||||||
maintenance: 'Maintenance',
|
|
||||||
appearance: 'Appearance',
|
|
||||||
ok: 'OK',
|
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
accessHeader: 'Admin 密码',
|
title: '临时邮件 Admin',
|
||||||
accessTip: '请输入 Admin 密码',
|
auth: 'Admin 授权',
|
||||||
|
home: '首页',
|
||||||
|
authTip: '请输入正确的授权码',
|
||||||
|
name: '名称',
|
||||||
|
created_at: '创建时间',
|
||||||
|
showPass: '显示密码',
|
||||||
|
password: '密码',
|
||||||
|
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
||||||
|
delete: '删除',
|
||||||
|
deleteTip: '确定要删除这个邮箱吗?',
|
||||||
|
refresh: '刷新',
|
||||||
mails: '邮件',
|
mails: '邮件',
|
||||||
|
itemCount: '总数',
|
||||||
|
query: '查询',
|
||||||
|
userCount: '用户总数',
|
||||||
|
activeUser: '周活跃用户',
|
||||||
|
mailCount: '邮件总数',
|
||||||
account: '账号',
|
account: '账号',
|
||||||
account_create: '创建账号',
|
unknow: '未知',
|
||||||
account_settings: '账号设置',
|
addressQueryTip: '留空查询所有地址',
|
||||||
user_management: '用户管理',
|
|
||||||
user_settings: '用户设置',
|
|
||||||
unknow: '无收件人邮件',
|
|
||||||
senderAccess: '发件权限控制',
|
|
||||||
sendBox: '发件箱',
|
|
||||||
statistics: '统计',
|
|
||||||
maintenance: '维护',
|
|
||||||
appearance: '外观',
|
|
||||||
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
|
||||||
return;
|
} else {
|
||||||
|
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/v1/mails`
|
||||||
|
+ `?address=${mailAddress.value}`
|
||||||
|
+ `&limit=${mailPageSize.value}`
|
||||||
|
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
||||||
|
);
|
||||||
|
mailData.value = results;
|
||||||
|
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/v1/mails_unknow`
|
||||||
|
+ `?limit=${mailPageSize.value}`
|
||||||
|
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
||||||
|
);
|
||||||
|
mailUnknowData.value = results;
|
||||||
|
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="t('accessHeader')">
|
title="Dialog">
|
||||||
<p>{{ t('accessTip') }}</p>
|
<template #header>
|
||||||
<n-input v-model:value="adminAuth" type="textarea" :autosize="{ minRows: 3 }" />
|
<div>{{ t('auth') }}</div>
|
||||||
|
</template>
|
||||||
|
<p>{{ t('authTip') }}</p>
|
||||||
|
<n-input v-model:value="adminAuth" type="textarea" :autosize="{
|
||||||
|
minRows: 3
|
||||||
|
}" />
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="authFunc" type="primary" :loading="loading">
|
<n-button @click="authFunc" size="small" tertiary round type="primary">
|
||||||
{{ t('ok') }}
|
{{ t('auth') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</n-modal>
|
||||||
<n-tabs type="card" v-model:value="adminTab" :placement="globalTabplacement">
|
<n-modal v-model:show="showEmailPassword" preset="dialog" title="Dialog">
|
||||||
|
<template #header>
|
||||||
|
<div>{{ t("password") }}</div>
|
||||||
|
</template>
|
||||||
|
<span>
|
||||||
|
<p>{{ t("passwordTip") }}</p>
|
||||||
|
</span>
|
||||||
|
<n-card>
|
||||||
|
<b>{{ curEmailPassword }}</b>
|
||||||
|
</n-card>
|
||||||
|
<template #action>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
|
<n-row>
|
||||||
|
<n-col :span="8">
|
||||||
|
<n-statistic :label="t('userCount')" :value="statistics.userCount">
|
||||||
|
<template #prefix>
|
||||||
|
<n-icon :component="User" />
|
||||||
|
</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-col>
|
||||||
|
<n-col :span="8">
|
||||||
|
<n-statistic :label="t('activeUser')" :value="statistics.activeUserCount7days">
|
||||||
|
<template #prefix>
|
||||||
|
<n-icon :component="UserCheck" />
|
||||||
|
</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-col>
|
||||||
|
<n-col :span="8">
|
||||||
|
<n-statistic :label="t('mailCount')" :value="statistics.mailCount">
|
||||||
|
<template #prefix>
|
||||||
|
<n-icon :component="MailBulk" />
|
||||||
|
</template>
|
||||||
|
</n-statistic>
|
||||||
|
</n-col>
|
||||||
|
</n-row>
|
||||||
|
<n-tabs type="segment" v-model:value="tab">
|
||||||
<n-tab-pane name="account" :tab="t('account')">
|
<n-tab-pane name="account" :tab="t('account')">
|
||||||
<Account />
|
<n-input-group>
|
||||||
</n-tab-pane>
|
<n-input v-model:value="addressQuery" clearable :placeholder="t('addressQueryTip')" />
|
||||||
<n-tab-pane name="account_create" :tab="t('account_create')">
|
<n-button @click="fetchData" type="primary" ghost>
|
||||||
<CreateAccount />
|
{{ t('query') }}
|
||||||
</n-tab-pane>
|
</n-button>
|
||||||
<n-tab-pane name="account_settings" :tab="t('account_settings')">
|
</n-input-group>
|
||||||
<AccountSettings />
|
<div style="display: inline-block;">
|
||||||
</n-tab-pane>
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" :page-sizes="[20, 50, 100]"
|
||||||
<n-tab-pane name="user_management" :tab="t('user_management')">
|
show-size-picker>
|
||||||
<UserManagement />
|
<template #prefix="{ itemCount }">
|
||||||
</n-tab-pane>
|
{{ t('itemCount') }}: {{ itemCount }}
|
||||||
<n-tab-pane name="user_settings" :tab="t('user_settings')">
|
</template>
|
||||||
<UserSettings />
|
</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')">
|
||||||
<Mails />
|
<n-input-group>
|
||||||
|
<n-input v-model:value="mailAddress" />
|
||||||
|
<n-button @click="fetchMailData" type="primary" ghost>
|
||||||
|
{{ t('query') }}
|
||||||
|
</n-button>
|
||||||
|
</n-input-group>
|
||||||
|
<n-list hoverable clickable>
|
||||||
|
<div style="display: inline-block; margin-bottom: 10px;">
|
||||||
|
<n-pagination v-model:page="mailPage" v-model:page-size="mailPageSize" :item-count="mailCount" simple>
|
||||||
|
<template #prefix="{ itemCount }">
|
||||||
|
{{ t('itemCount') }}: {{ itemCount }}
|
||||||
|
</template>
|
||||||
|
</n-pagination>
|
||||||
|
</div>
|
||||||
|
<n-list-item v-for="row in mailData" v-bind:key="row.id">
|
||||||
|
<n-thing class="center" :title="row.subject">
|
||||||
|
<template #description>
|
||||||
|
<n-space>
|
||||||
|
<n-tag type="info">
|
||||||
|
FROM: {{ row.source }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ row.id }}
|
||||||
|
</n-tag>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
<div v-html="row.message"></div>
|
||||||
|
</n-thing>
|
||||||
|
</n-list-item>
|
||||||
|
</n-list>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="unknow" :tab="t('unknow')">
|
<n-tab-pane name="unknow" :tab="t('unknow')">
|
||||||
<MailsUnknow />
|
<n-button @click="fetchMailUnknowData" type="primary" ghost>
|
||||||
</n-tab-pane>
|
{{ t('query') }}
|
||||||
<n-tab-pane name="senderAccess" :tab="t('senderAccess')">
|
</n-button>
|
||||||
<SenderAccess />
|
<n-list hoverable clickable>
|
||||||
</n-tab-pane>
|
<div style="display: inline-block; margin-bottom: 10px;">
|
||||||
<n-tab-pane name="sendBox" :tab="t('sendBox')">
|
<n-pagination v-model:page="mailUnknowPage" v-model:page-size="mailUnknowPageSize"
|
||||||
<SendBox />
|
:item-count="mailUnknowCount" simple>
|
||||||
</n-tab-pane>
|
<template #prefix="{ itemCount }">
|
||||||
<n-tab-pane name="statistics" :tab="t('statistics')">
|
{{ t('itemCount') }}: {{ itemCount }}
|
||||||
<Statistics />
|
</template>
|
||||||
</n-tab-pane>
|
</n-pagination>
|
||||||
<n-tab-pane name="maintenance" :tab="t('maintenance')">
|
</div>
|
||||||
<Maintenance />
|
<n-list-item v-for="row in mailUnknowData" v-bind:key="row.id">
|
||||||
</n-tab-pane>
|
<n-thing class="center" :title="row.subject">
|
||||||
<n-tab-pane name="appearance" :tab="t('appearance')">
|
<template #description>
|
||||||
<Appearance />
|
<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>
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,32 +1,46 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import useClipboard from 'vue-clipboard3'
|
||||||
import { ref, h, computed, onMounted } from 'vue'
|
import { ref, h, computed, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRoute, useRouter, RouterLink } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useIsMobile } from '../utils/composables'
|
import { useIsMobile } from '../utils/composables'
|
||||||
import {
|
import { DarkModeFilled, LightModeFilled, MenuFilled, AdminPanelSettingsFilled } from '@vicons/material'
|
||||||
DarkModeFilled, LightModeFilled, MenuFilled,
|
import { GithubAlt, Language, User, Home, Copy } from '@vicons/fa'
|
||||||
AdminPanelSettingsFilled
|
|
||||||
} from '@vicons/material'
|
|
||||||
import { GithubAlt, Language, User, Home } from '@vicons/fa'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
|
const { toClipboard } = useClipboard()
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
|
|
||||||
const {
|
const { jwt, localeCache, themeSwitch, showAuth, adminAuth, auth } = useGlobalState()
|
||||||
localeCache, toggleDark, isDark, openSettings,
|
const { showLogin, openSettings, settings } = useGlobalState()
|
||||||
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 showMobileMenu = ref(false)
|
const showNewEmail = ref(false)
|
||||||
const menuValue = computed(() => {
|
const showLogout = ref(false)
|
||||||
if (route.path.includes("user")) return "user";
|
const showDelteAccount = ref(false)
|
||||||
if (route.path.includes("admin")) return "admin";
|
const password = ref('')
|
||||||
return "home";
|
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 {
|
||||||
@@ -48,69 +62,89 @@ const { t } = useI18n({
|
|||||||
title: 'Cloudflare Temp Email',
|
title: 'Cloudflare Temp Email',
|
||||||
dark: 'Dark',
|
dark: 'Dark',
|
||||||
light: 'Light',
|
light: 'Light',
|
||||||
accessHeader: 'Access Password',
|
login: 'Login',
|
||||||
accessTip: 'Please enter the correct access password',
|
logout: 'Logout',
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
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',
|
||||||
|
copied: 'Copied',
|
||||||
|
showPassword: 'Show Password',
|
||||||
|
fetchAddressError: 'Fetch address error, maybe your jwt is invalid or network error.',
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
title: 'Cloudflare 临时邮件',
|
title: 'Cloudflare 临时邮件',
|
||||||
dark: '暗色',
|
dark: '暗色',
|
||||||
light: '亮色',
|
light: '亮色',
|
||||||
accessHeader: '访问密码',
|
login: '登录',
|
||||||
accessTip: '请输入站点访问密码',
|
logout: '登出',
|
||||||
|
logoutConfirm: '确定要登出吗?',
|
||||||
|
delteAccount: "删除账户",
|
||||||
|
delteAccountConfirm: "确定要删除你的账户和其中的所有邮件吗?",
|
||||||
|
auth: '授权',
|
||||||
|
authTip: '请输入正确的授权码',
|
||||||
|
settings: '设置',
|
||||||
home: '主页',
|
home: '主页',
|
||||||
menu: '菜单',
|
menu: '菜单',
|
||||||
user: '用户',
|
user: '用户',
|
||||||
|
pleaseGetNewEmail: '请"登录"或点击 "获取新邮箱" 按钮来获取一个新的邮箱地址',
|
||||||
|
getNewEmail: '获取新邮箱',
|
||||||
|
getNewEmailTip1: '请输入你想要使用的邮箱地址。',
|
||||||
|
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
||||||
|
yourAddress: '你的邮箱地址是',
|
||||||
|
password: '密码',
|
||||||
|
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
||||||
|
cancel: '取消',
|
||||||
ok: '确定',
|
ok: '确定',
|
||||||
|
copy: '复制',
|
||||||
|
copied: '已复制',
|
||||||
|
showPassword: '查看密码',
|
||||||
|
fetchAddressError: '获取地址失败, 请检查你的 jwt 是否有效 或 网络是否正常。',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const showUserMenu = computed(() => !!settings.value.address)
|
||||||
|
|
||||||
const menuOptions = computed(() => [
|
const menuOptions = computed(() => [
|
||||||
{
|
{
|
||||||
label: () => h(NButton,
|
label: () => h(
|
||||||
|
NButton,
|
||||||
{
|
{
|
||||||
text: true,
|
bordered: false,
|
||||||
|
ghost: true,
|
||||||
size: "small",
|
size: "small",
|
||||||
type: menuValue.value == "home" ? "primary" : "default",
|
onClick: () => router.push('/')
|
||||||
style: "width: 100%",
|
|
||||||
onClick: async () => { await router.push('/'); showMobileMenu.value = false; }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => t('home'),
|
default: () => t('home'),
|
||||||
icon: () => h(NIcon, { component: Home })
|
icon: () => h(NIcon, { component: Home })
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
key: "home"
|
key: "home"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
text: true,
|
bordered: false,
|
||||||
|
ghost: true,
|
||||||
size: "small",
|
size: "small",
|
||||||
type: menuValue.value == "user" ? "primary" : "default",
|
onClick: () => router.push('/admin')
|
||||||
style: "width: 100%",
|
|
||||||
onClick: async () => { await router.push("/user"); showMobileMenu.value = false; }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
default: () => t('user'),
|
|
||||||
icon: () => h(NIcon, { component: User }),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
key: "user",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: () => h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
text: true,
|
|
||||||
size: "small",
|
|
||||||
type: menuValue.value == "admin" ? "primary" : "default",
|
|
||||||
style: "width: 100%",
|
|
||||||
onClick: async () => { await router.push('/admin'); showMobileMenu.value = false; }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => "Admin",
|
default: () => "Admin",
|
||||||
@@ -124,15 +158,85 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
text: true,
|
bordered: false,
|
||||||
|
ghost: true,
|
||||||
size: "small",
|
size: "small",
|
||||||
style: "width: 100%",
|
|
||||||
onClick: () => { toggleDark(); showMobileMenu.value = false; }
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => isDark.value ? t('light') : t('dark'),
|
default: () => t('user'),
|
||||||
|
icon: () => h(NIcon, { component: User }),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
show: showUserMenu.value,
|
||||||
|
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(
|
||||||
|
NButton,
|
||||||
|
{
|
||||||
|
bordered: false,
|
||||||
|
ghost: true,
|
||||||
|
size: "small",
|
||||||
|
onClick: () => { themeSwitch.value = !themeSwitch.value }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
default: () => themeSwitch.value ? t('light') : t('dark'),
|
||||||
icon: () => h(
|
icon: () => h(
|
||||||
NIcon, { component: isDark.value ? LightModeFilled : DarkModeFilled }
|
NIcon, { component: themeSwitch.value ? LightModeFilled : DarkModeFilled }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -142,13 +246,10 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
text: true,
|
bordered: false,
|
||||||
|
ghost: true,
|
||||||
size: "small",
|
size: "small",
|
||||||
style: "width: 100%",
|
onClick: () => localeCache.value == 'zh' ? changeLocale('en') : changeLocale('zh')
|
||||||
onClick: () => {
|
|
||||||
localeCache.value == 'zh' ? changeLocale('en') : changeLocale('zh');
|
|
||||||
showMobileMenu.value = false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: () => localeCache.value == 'zh' ? "English" : "中文",
|
default: () => localeCache.value == 'zh' ? "English" : "中文",
|
||||||
@@ -163,9 +264,9 @@ const menuOptions = computed(() => [
|
|||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
{
|
{
|
||||||
text: true,
|
bordered: !isMobile.value,
|
||||||
|
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",
|
||||||
@@ -179,47 +280,189 @@ const menuOptions = computed(() => [
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const menuOptionsMobile = computed(() => [
|
||||||
|
{
|
||||||
|
label: t('menu'),
|
||||||
|
icon: () => h(
|
||||||
|
NIcon,
|
||||||
|
{
|
||||||
|
component: MenuFilled
|
||||||
|
}
|
||||||
|
),
|
||||||
|
key: "menu",
|
||||||
|
children: menuOptions.value
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
const copy = async () => {
|
||||||
|
try {
|
||||||
|
await toClipboard(settings.value.address)
|
||||||
|
message.success(t('copied'));
|
||||||
|
} catch (e) {
|
||||||
|
message.error(e.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 : "";
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-page-header>
|
<n-layout-header>
|
||||||
<template #title>
|
<h2 style="display: inline-block; margin-left: 10px;">{{ t('title') }}</h2>
|
||||||
<h3>{{ t('title') }}</h3>
|
<div>
|
||||||
</template>
|
<n-menu v-if="!isMobile" mode="horizontal" :options="menuOptions" />
|
||||||
<template #avatar>
|
<n-menu v-else mode="horizontal" :options="menuOptionsMobile" />
|
||||||
<n-avatar style="margin-left: 10px;" src="/logo.png" />
|
</div>
|
||||||
</template>
|
</n-layout-header>
|
||||||
<template #extra>
|
<div v-if="!isAdminRoute">
|
||||||
<n-space>
|
<n-card v-if="!settings.fetched">
|
||||||
<n-menu v-if="!isMobile" mode="horizontal" :options="menuOptions" responsive />
|
<n-skeleton style="height: 50vh" />
|
||||||
<n-button v-else :text="true" @click="showMobileMenu = !showMobileMenu" style="margin-right: 10px;">
|
</n-card>
|
||||||
<template #icon>
|
<n-alert v-else-if="settings.address" type="info" show-icon>
|
||||||
<n-icon :component="MenuFilled" />
|
<span>
|
||||||
</template>
|
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
||||||
{{ t('menu') }}
|
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary round type="primary">
|
||||||
|
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</n-space>
|
</span>
|
||||||
|
</n-alert>
|
||||||
|
<n-card v-else>
|
||||||
|
<n-result status="info" :description="t('pleaseGetNewEmail')">
|
||||||
|
<template #footer>
|
||||||
|
<n-alert v-if="jwt" type="warning" show-icon>
|
||||||
|
<span>{{ t('fetchAddressError') }}</span>
|
||||||
|
</n-alert>
|
||||||
|
<n-button @click="showLogin = true" tertiary round type="primary">
|
||||||
|
{{ t('login') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="showNewEmail = true" tertiary round type="primary">
|
||||||
|
{{ t('getNewEmail') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-result>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
<n-modal v-model:show="showNewEmail" preset="dialog" title="Dialog">
|
||||||
|
<template #header>
|
||||||
|
<div>{{ t('getNewEmail') }}</div>
|
||||||
</template>
|
</template>
|
||||||
</n-page-header>
|
<span>
|
||||||
<n-drawer v-model:show="showMobileMenu" placement="top" style="height: 100vh;">
|
<p>{{ t("getNewEmailTip1") }}</p>
|
||||||
<n-drawer-content :title="t('menu')" closable>
|
<p>{{ t("getNewEmailTip2") }}</p>
|
||||||
<n-menu :options="menuOptions" />
|
</span>
|
||||||
</n-drawer-content>
|
<n-input-group>
|
||||||
</n-drawer>
|
<n-input-group-label v-if="openSettings.prefix">
|
||||||
<n-modal v-model:show="showAuth" :closable="false" :closeOnEsc="false" :maskClosable="false" preset="dialog"
|
{{ openSettings.prefix }}
|
||||||
:title="t('accessHeader')">
|
</n-input-group-label>
|
||||||
<p>{{ t('accessTip') }}</p>
|
<n-input v-model:value="emailName" />
|
||||||
<n-input v-model:value="auth" type="textarea" :autosize="{ minRows: 3 }" />
|
<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>
|
<template #action>
|
||||||
<n-button :loading="loading" @click="authFunc" type="primary">
|
<n-button @click="showNewEmail = false">
|
||||||
|
{{ t('cancel') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button @click="newEmail" type="primary">
|
||||||
{{ t('ok') }}
|
{{ t('ok') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
</template>
|
</template>
|
||||||
</n-modal>
|
</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"
|
||||||
|
title="Dialog">
|
||||||
|
<template #header>
|
||||||
|
<div>{{ t('auth') }}</div>
|
||||||
|
</template>
|
||||||
|
<p>{{ t('authTip') }}</p>
|
||||||
|
<n-input v-model:value="auth" type="textarea" :autosize="{
|
||||||
|
minRows: 3
|
||||||
|
}" />
|
||||||
|
<template #action>
|
||||||
|
<n-button @click="authFunc" size="small" tertiary round type="primary">
|
||||||
|
{{ t('auth') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -239,16 +482,4 @@ 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,67 +1,304 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { watch, onMounted, ref } from "vue";
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
import { useI18n } from 'vue-i18n'
|
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 AddressBar from './index/AddressBar.vue';
|
const message = useMessage()
|
||||||
import MailBox from '../components/MailBox.vue';
|
const isMobile = useIsMobile()
|
||||||
import AutoReply from './index/AutoReply.vue';
|
|
||||||
import SendBox from './index/SendBox.vue';
|
|
||||||
import SendMail from './index/SendMail.vue';
|
|
||||||
import AccountSettings from './index/AccountSettings.vue';
|
|
||||||
|
|
||||||
const { localeCache, settings, openSettings, indexTab, globalTabplacement } = useGlobalState()
|
const { settings, themeSwitch } = useGlobalState()
|
||||||
|
const autoRefresh = ref(false)
|
||||||
|
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({
|
const { t } = useI18n({
|
||||||
locale: localeCache.value || 'zh',
|
locale: 'zh',
|
||||||
messages: {
|
messages: {
|
||||||
en: {
|
en: {
|
||||||
mailbox: 'Mail Box',
|
autoRefresh: 'Auto Refresh',
|
||||||
sendbox: 'Send Box',
|
refresh: 'Refresh',
|
||||||
sendmail: 'Send Mail',
|
attachments: 'Show Attachments',
|
||||||
auto_reply: 'Auto Reply',
|
pleaseSelectMail: "Please select a mail to view."
|
||||||
accountSettings: 'Account Settings',
|
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
mailbox: '收件箱',
|
autoRefresh: '自动刷新',
|
||||||
sendbox: '发件箱',
|
refresh: '刷新',
|
||||||
sendmail: '发送邮件',
|
attachments: '查看附件',
|
||||||
auto_reply: '自动回复',
|
pleaseSelectMail: "请选择一封邮件查看。"
|
||||||
accountSettings: '账户设置',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchMailData = async (limit, offset) => {
|
const setupAutoRefresh = async (autoRefresh) => {
|
||||||
return await api.fetch(`/api/mails?limit=${limit}&offset=${offset}`);
|
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/v1/mails`
|
||||||
|
+ `?limit=${pageSize.value}`
|
||||||
|
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||||
|
);
|
||||||
|
data.value = results;
|
||||||
|
if (totalCount > 0) {
|
||||||
|
count.value = totalCount;
|
||||||
|
}
|
||||||
|
if (!isMobile.value && !curMail.value && data.value.length > 0) {
|
||||||
|
curMail.value = results[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteMail = async (curMailId) => {
|
const clickRow = async (row) => {
|
||||||
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
|
curMail.value = row;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getAttachments = async (attachment_id) => {
|
||||||
|
try {
|
||||||
|
const res = await api.fetch(
|
||||||
|
`/api/v1/attachment/${attachment_id}`
|
||||||
|
);
|
||||||
|
curAttachments.value = res
|
||||||
|
.filter((item) => item?.content?.data)
|
||||||
|
.map((item) => {
|
||||||
|
return {
|
||||||
|
id: item.contentId || Math.random().toString(36).substring(2, 15),
|
||||||
|
filename: item.filename || "",
|
||||||
|
size: item.size,
|
||||||
|
url: URL.createObjectURL(
|
||||||
|
new Blob(
|
||||||
|
[new Uint8Array(item.content.data)],
|
||||||
|
{ type: item.contentType || 'application/octet-stream' }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
showAttachments.value = true;
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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>
|
||||||
<AddressBar />
|
<n-layout v-if="settings.address">
|
||||||
<n-tabs v-if="settings.address" type="card" v-model:value="indexTab" :placement="globalTabplacement">
|
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25" :default-size="0.25">
|
||||||
<n-tab-pane name="mailbox" :tab="t('mailbox')">
|
<template #1>
|
||||||
<MailBox :showEMailTo="false" :showReply="true" :enableUserDeleteEmail="openSettings.enableUserDeleteEmail"
|
<div>
|
||||||
:fetchMailData="fetchMailData" :deleteMail="deleteMail" />
|
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
||||||
</n-tab-pane>
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||||
<n-tab-pane name="sendbox" :tab="t('sendbox')">
|
</div>
|
||||||
<SendBox />
|
<n-switch v-model:value="autoRefresh" size="small">
|
||||||
</n-tab-pane>
|
<template #checked>
|
||||||
<n-tab-pane name="sendmail" :tab="t('sendmail')">
|
{{ t('autoRefresh') }}
|
||||||
<SendMail />
|
</template>
|
||||||
</n-tab-pane>
|
<template #unchecked>
|
||||||
<n-tab-pane name="accountSettings" :tab="t('accountSettings')">
|
{{ t('autoRefresh') }}
|
||||||
<AccountSettings />
|
</template></n-switch>
|
||||||
</n-tab-pane>
|
<n-button class="center" @click="refresh" size="small" type="primary">
|
||||||
<n-tab-pane v-if="openSettings.enableAutoReply" name="auto_reply" :tab="t('auto_reply')">
|
{{ t('refresh') }}
|
||||||
<AutoReply />
|
</n-button>
|
||||||
</n-tab-pane>
|
</div>
|
||||||
</n-tabs>
|
<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.attachment_id" size="small" tertiary type="info"
|
||||||
|
@click="getAttachments(curMail.attachment_id)">
|
||||||
|
{{ t('attachments') }}
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
<div v-html="curMail.message" style="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.attachment_id" size="small" tertiary type="info"
|
||||||
|
@click="getAttachments(curMail.attachment_id)">
|
||||||
|
{{ t('attachments') }}
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
import Header from './Header.vue'
|
||||||
import { api } from '../../api'
|
import { useGlobalState } from '../store'
|
||||||
|
import { api } from '../api'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const sourcePrefix = ref("")
|
const sourcePrefix = ref("")
|
||||||
@@ -41,22 +42,18 @@ const { t } = useI18n({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchData = async () => {
|
const getSettings = async () => {
|
||||||
try {
|
await api.getSettings()
|
||||||
const res = await api.fetch("/api/auto_reply")
|
sourcePrefix.value = settings.value.auto_reply.source_prefix || ""
|
||||||
sourcePrefix.value = res.source_prefix || ""
|
enableAutoReply.value = settings.value.auto_reply.enabled || false
|
||||||
enableAutoReply.value = res.enabled || false
|
name.value = settings.value.auto_reply.name || ""
|
||||||
name.value = res.name || ""
|
autoReplyMessage.value = settings.value.auto_reply.message || ""
|
||||||
autoReplyMessage.value = res.message || ""
|
subject.value = settings.value.auto_reply.subject || ""
|
||||||
subject.value = res.subject || ""
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveData = async () => {
|
const saveSettings = async () => {
|
||||||
try {
|
try {
|
||||||
await api.fetch("/api/auto_reply", {
|
await api.fetch("/api/settings", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
auto_reply: {
|
auto_reply: {
|
||||||
@@ -75,7 +72,7 @@ const saveData = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchData()
|
await getSettings()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -83,7 +80,7 @@ onMounted(async () => {
|
|||||||
<div class="center">
|
<div class="center">
|
||||||
<n-card v-if="settings.address" :title='t("settings")'>
|
<n-card v-if="settings.address" :title='t("settings")'>
|
||||||
<div class="right">
|
<div class="right">
|
||||||
<n-button type="primary" @click="saveData">{{ t('save') }}</n-button>
|
<n-button type="primary" @click="saveSettings">{{ t('save') }}</n-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<n-form-item :label="t('enableAutoReply')" label-placement="left">
|
<n-form-item :label="t('enableAutoReply')" label-placement="left">
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../store'
|
|
||||||
|
|
||||||
import AddressMangement from './user/AddressManagement.vue';
|
|
||||||
import UserSettingsPage from './user/UserSettings.vue';
|
|
||||||
import UserBar from './user/UserBar.vue';
|
|
||||||
import BindAddress from './user/BindAddress.vue';
|
|
||||||
|
|
||||||
const {
|
|
||||||
localeCache, userTab, globalTabplacement, userSettings
|
|
||||||
} = useGlobalState()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
address_management: 'Address Management',
|
|
||||||
user_settings: 'User Settings',
|
|
||||||
bind_address: 'Bind Mail Address',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
address_management: '地址管理',
|
|
||||||
user_settings: '用户设置',
|
|
||||||
bind_address: '绑定邮箱地址',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<UserBar />
|
|
||||||
<n-tabs v-if="userSettings.user_email" type="card" v-model:value="userTab" :placement="globalTabplacement">
|
|
||||||
<n-tab-pane name="address_management" :tab="t('address_management')">
|
|
||||||
<AddressMangement />
|
|
||||||
</n-tab-pane>
|
|
||||||
<n-tab-pane name="user_settings" :tab="t('user_settings')">
|
|
||||||
<UserSettingsPage />
|
|
||||||
</n-tab-pane>
|
|
||||||
<n-tab-pane name="bind_address" :tab="t('bind_address')">
|
|
||||||
<BindAddress />
|
|
||||||
</n-tab-pane>
|
|
||||||
</n-tabs>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
<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',
|
|
||||||
showCredential: 'Show Mail Address Credential',
|
|
||||||
addressCredential: 'Mail Address Credential',
|
|
||||||
addressCredentialTip: 'Please copy the Mail Address Credential 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: '发送数量',
|
|
||||||
showCredential: '查看邮箱地址凭证',
|
|
||||||
addressCredential: '邮箱地址凭证',
|
|
||||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
|
||||||
delete: '删除',
|
|
||||||
deleteTip: '确定要删除这个邮箱吗?',
|
|
||||||
delteAccount: '删除邮箱',
|
|
||||||
viewMails: '查看邮件',
|
|
||||||
viewSendBox: '查看发件箱',
|
|
||||||
itemCount: '总数',
|
|
||||||
query: '查询',
|
|
||||||
addressQueryTip: '留空查询所有地址',
|
|
||||||
actions: '操作',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const showEmailCredential = ref(false)
|
|
||||||
const curEmailCredential = ref("")
|
|
||||||
const curDeleteAddressId = ref(0);
|
|
||||||
|
|
||||||
const addressQuery = ref("")
|
|
||||||
|
|
||||||
const data = ref([])
|
|
||||||
const count = ref(0)
|
|
||||||
const page = ref(1)
|
|
||||||
const pageSize = ref(20)
|
|
||||||
const showDeleteAccount = ref(false)
|
|
||||||
|
|
||||||
const showCredential = async (id) => {
|
|
||||||
try {
|
|
||||||
curEmailCredential.value = await api.adminShowAddressCredential(id)
|
|
||||||
showEmailCredential.value = true
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
showEmailCredential.value = false
|
|
||||||
curEmailCredential.value = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteEmail = async () => {
|
|
||||||
try {
|
|
||||||
await api.adminDeleteAddress(curDeleteAddressId.value)
|
|
||||||
message.success("success");
|
|
||||||
await fetchData()
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
} finally {
|
|
||||||
showDeleteAccount.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: () => showCredential(row.id)
|
|
||||||
},
|
|
||||||
{ default: () => t('showCredential') }
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
showDeleteAccount.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="showEmailCredential" preset="dialog" title="Dialog">
|
|
||||||
<template #header>
|
|
||||||
<div>{{ t("addressCredential") }}</div>
|
|
||||||
</template>
|
|
||||||
<span>
|
|
||||||
<p>{{ t("addressCredentialTip") }}</p>
|
|
||||||
</span>
|
|
||||||
<n-card>
|
|
||||||
<b>{{ curEmailCredential }}</b>
|
|
||||||
</n-card>
|
|
||||||
<template #action>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showDeleteAccount" 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" tertiary>
|
|
||||||
{{ 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>
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted, ref } 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: {
|
|
||||||
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',
|
|
||||||
send_address_block_list: 'Address Block Keywords for send email',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
save: '保存',
|
|
||||||
successTip: '保存成功',
|
|
||||||
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
|
|
||||||
address_block_list_placeholder: '请输入您想要屏蔽的关键词',
|
|
||||||
send_address_block_list: '发送邮件地址屏蔽关键词',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const addressBlockList = ref([])
|
|
||||||
const sendAddressBlockList = ref([])
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const res = await api.fetch(`/admin/account_settings`)
|
|
||||||
addressBlockList.value = res.blockList || []
|
|
||||||
sendAddressBlockList.value = res.sendBlockList || []
|
|
||||||
} 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 || [],
|
|
||||||
sendBlockList: sendAddressBlockList.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-form-item-row :label="t('send_address_block_list')">
|
|
||||||
<n-select v-model:value="sendAddressBlockList" 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>
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
<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',
|
|
||||||
addressCredential: 'Mail Address Credential',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
address: '地址',
|
|
||||||
enablePrefix: '是否启用前缀',
|
|
||||||
creatNewEmail: '创建新邮箱',
|
|
||||||
fillInAllFields: '请填写完整信息',
|
|
||||||
successTip: '创建成功',
|
|
||||||
addressCredential: '邮箱地址凭证',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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('addressCredential')">
|
|
||||||
<p>{{ t('addressCredential') }}</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>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, onMounted, watch } 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',
|
|
||||||
keywordQueryTip: 'Leave blank to not query by keyword',
|
|
||||||
query: 'Query',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
addressQueryTip: '留空查询所有地址',
|
|
||||||
keywordQueryTip: '留空不按关键字查询',
|
|
||||||
query: '查询',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const mailBoxKey = ref("")
|
|
||||||
const mailKeyword = ref("")
|
|
||||||
|
|
||||||
watch([adminMailTabAddress, mailKeyword], () => {
|
|
||||||
adminMailTabAddress.value = adminMailTabAddress.value.trim();
|
|
||||||
mailKeyword.value = mailKeyword.value.trim();
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryMail = () => {
|
|
||||||
mailBoxKey.value = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchMailData = async (limit, offset) => {
|
|
||||||
return await api.fetch(
|
|
||||||
`/admin/mails`
|
|
||||||
+ `?limit=${limit}`
|
|
||||||
+ `&offset=${offset}`
|
|
||||||
+ (adminMailTabAddress.value ? `&address=${adminMailTabAddress.value}` : '')
|
|
||||||
+ (mailKeyword.value ? `&keyword=${mailKeyword.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-input v-model:value="mailKeyword" :placeholder="t('keywordQueryTip')" />
|
|
||||||
<n-button @click="queryMail" type="primary" tertiary>
|
|
||||||
{{ t('query') }}
|
|
||||||
</n-button>
|
|
||||||
</n-input-group>
|
|
||||||
<MailBox :key="mailBoxKey" :enableUserDeleteEmail="false" :fetchMailData="fetchMailData" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
<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 cleanupModel = ref({
|
|
||||||
enableMailsAutoCleanup: false,
|
|
||||||
cleanMailsDays: 30,
|
|
||||||
enableUnknowMailsAutoCleanup: false,
|
|
||||||
cleanUnknowMailsDays: 30,
|
|
||||||
enableAddressAutoCleanup: false,
|
|
||||||
cleanAddressDays: 30,
|
|
||||||
enableSendBoxAutoCleanup: false,
|
|
||||||
cleanSendBoxDays: 30,
|
|
||||||
})
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
tip: 'Please input the cleanup days',
|
|
||||||
mailBoxLabel: 'Clean up days for mailbox',
|
|
||||||
mailUnknowLabel: "Clean up days for unknow receiver",
|
|
||||||
addressUnActiveLabel: "Clean up days for unactive address",
|
|
||||||
sendBoxLabel: "Clean up days for sendbox",
|
|
||||||
cleanupNow: "Cleanup now",
|
|
||||||
autoCleanup: "Auto cleanup",
|
|
||||||
cleanupSuccess: "Cleanup success",
|
|
||||||
save: "Save",
|
|
||||||
cronTip: "Enable cron cleanup, need to configure [crons] in worker, please refer to the document",
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
tip: '请输入清理天数',
|
|
||||||
mailBoxLabel: '收件箱清理天数',
|
|
||||||
mailUnknowLabel: "无收件人邮件清理天数",
|
|
||||||
addressUnActiveLabel: "未活跃地址清理天数",
|
|
||||||
sendBoxLabel: "发件箱清理天数",
|
|
||||||
autoCleanup: "自动清理",
|
|
||||||
cleanupSuccess: "清理成功",
|
|
||||||
cleanupNow: "立即清理",
|
|
||||||
save: "保存",
|
|
||||||
cronTip: "启用定时清理, 需在 worker 配置 [crons] 参数, 请参考文档",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const res = await api.fetch('/admin/auto_cleanup');
|
|
||||||
if (res) Object.assign(cleanupModel.value, res);
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = async () => {
|
|
||||||
try {
|
|
||||||
await api.fetch('/admin/auto_cleanup', {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(cleanupModel.value)
|
|
||||||
});
|
|
||||||
message.success(t('cleanupSuccess'));
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!adminAuth.value) {
|
|
||||||
showAdminAuth.value = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await fetchData();
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="center">
|
|
||||||
<n-card>
|
|
||||||
<n-alert :show-icon="false">
|
|
||||||
<span>{{ t('cronTip') }}</span>
|
|
||||||
</n-alert>
|
|
||||||
<n-form :model="cleanupModel">
|
|
||||||
<n-form-item-row :label="t('mailBoxLabel')">
|
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableMailsAutoCleanup">
|
|
||||||
{{ t('autoCleanup') }}
|
|
||||||
</n-checkbox>
|
|
||||||
<n-input-number v-model:value="cleanupModel.cleanMailsDays" :placeholder="t('tip')" />
|
|
||||||
<n-button @click="cleanup('mails', cleanupModel.cleanMailsDays)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="CleaningServicesFilled" />
|
|
||||||
</template>
|
|
||||||
{{ t('cleanupNow') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('mailUnknowLabel')">
|
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableUnknowMailsAutoCleanup">
|
|
||||||
{{ t('autoCleanup') }}
|
|
||||||
</n-checkbox>
|
|
||||||
<n-input-number v-model:value="cleanupModel.cleanUnknowMailsDays" :placeholder="t('tip')" />
|
|
||||||
<n-button @click="cleanup('mails_unknow', cleanupModel.cleanUnknowMailsDays)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="CleaningServicesFilled" />
|
|
||||||
</template>
|
|
||||||
{{ t('cleanupNow') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('addressUnActiveLabel')">
|
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableAddressAutoCleanup">
|
|
||||||
{{ t('autoCleanup') }}
|
|
||||||
</n-checkbox>
|
|
||||||
<n-input-number v-model:value="cleanupModel.cleanAddressDays" :placeholder="t('tip')" />
|
|
||||||
<n-button @click="cleanup('address', cleanupModel.cleanAddressDays)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="CleaningServicesFilled" />
|
|
||||||
</template>
|
|
||||||
{{ t('cleanupNow') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('mailBoxLabel')">
|
|
||||||
<n-checkbox v-model:checked="cleanupModel.enableSendBoxAutoCleanup">
|
|
||||||
{{ t('autoCleanup') }}
|
|
||||||
</n-checkbox>
|
|
||||||
<n-input-number v-model:value="cleanupModel.cleanSendBoxDays" :placeholder="t('tip')" />
|
|
||||||
<n-button @click="cleanup('sendbox', cleanupModel.cleanSendBoxDays)">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="CleaningServicesFilled" />
|
|
||||||
</template>
|
|
||||||
{{ t('cleanupNow') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-button @click="save" type="primary" block :loading="loading">
|
|
||||||
{{ t('save') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.n-card {
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
text-align: center;
|
|
||||||
place-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-alert {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, h, onMounted, watch } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
|
||||||
|
|
||||||
const { localeCache, adminAuth, adminSendBoxTabAddress, showAdminAuth } = 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',
|
|
||||||
tertiary: 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>
|
|
||||||
<n-modal v-model:show="showModal" preset="dialog" style="width: 100%;">
|
|
||||||
<pre style="overflow: auto;">{{ curRow.raw }}</pre>
|
|
||||||
</n-modal>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input v-model:value="adminSendBoxTabAddress" />
|
|
||||||
<n-button @click="fetchData" type="primary" tertiary>
|
|
||||||
{{ 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>
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
<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',
|
|
||||||
is_enabled: 'Is Enabled',
|
|
||||||
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: '成功',
|
|
||||||
is_enabled: '是否启用',
|
|
||||||
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: t('is_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',
|
|
||||||
tertiary: 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" tertiary>
|
|
||||||
{{ 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>
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
<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: 'Account Count',
|
|
||||||
activeUser: '7 days Active Mail Account',
|
|
||||||
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>
|
|
||||||
@@ -1,289 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, h, onMounted, watch } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { NMenu, NButton, NBadge } from 'naive-ui';
|
|
||||||
import { MenuFilled } from '@vicons/material'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
|
||||||
import { hashPassword } from '../../utils';
|
|
||||||
|
|
||||||
const { localeCache, loading } = useGlobalState()
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
success: 'Success',
|
|
||||||
user_email: 'User Email',
|
|
||||||
address_count: 'Address Count',
|
|
||||||
created_at: 'Created At',
|
|
||||||
actions: 'Actions',
|
|
||||||
query: 'Query',
|
|
||||||
itemCount: 'itemCount',
|
|
||||||
deleteUser: 'Delete User',
|
|
||||||
delete: 'Delete',
|
|
||||||
deleteUserTip: 'Are you sure you want to delete this user?',
|
|
||||||
resetPassword: 'Reset Password',
|
|
||||||
pleaseInput: 'Please input complete information',
|
|
||||||
createUser: 'Create User',
|
|
||||||
email: 'Email',
|
|
||||||
password: 'Password',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
success: '成功',
|
|
||||||
user_email: '用户邮箱',
|
|
||||||
address_count: '地址数量',
|
|
||||||
created_at: '创建时间',
|
|
||||||
actions: '操作',
|
|
||||||
query: '查询',
|
|
||||||
itemCount: '总数',
|
|
||||||
deleteUser: '删除用户',
|
|
||||||
delete: '删除',
|
|
||||||
deleteUserTip: '确定要删除此用户吗?',
|
|
||||||
resetPassword: '重置密码',
|
|
||||||
pleaseInput: '请输入完整信息',
|
|
||||||
createUser: '创建用户',
|
|
||||||
email: '邮箱',
|
|
||||||
password: '密码',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const data = ref([])
|
|
||||||
const count = ref(0)
|
|
||||||
const page = ref(1)
|
|
||||||
const pageSize = ref(20)
|
|
||||||
|
|
||||||
const userQuery = ref('')
|
|
||||||
const showResetPassword = ref(false)
|
|
||||||
const newResetPassword = ref('')
|
|
||||||
const showDeleteUser = ref(false)
|
|
||||||
const curUserId = ref(0)
|
|
||||||
const showCreateUser = ref(false)
|
|
||||||
const user = ref({
|
|
||||||
email: "",
|
|
||||||
password: ""
|
|
||||||
})
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const { results, count: userCount } = await api.fetch(
|
|
||||||
`/admin/users`
|
|
||||||
+ `?limit=${pageSize.value}`
|
|
||||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
|
||||||
+ (userQuery.value ? `&query=${userQuery.value}` : '')
|
|
||||||
);
|
|
||||||
data.value = results;
|
|
||||||
if (userCount > 0) {
|
|
||||||
count.value = userCount;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetPassword = async () => {
|
|
||||||
if (!newResetPassword.value) {
|
|
||||||
message.error(t('pleaseInput'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await api.fetch(`/admin/users/${curUserId.value}/reset_password`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
password: await hashPassword(newResetPassword.value)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
message.success(t('success'));
|
|
||||||
showResetPassword.value = false;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createUser = async () => {
|
|
||||||
if (!user.value.email || !user.value.password) {
|
|
||||||
message.error(t('pleaseInput'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await api.fetch(`/admin/users`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: user.value.email,
|
|
||||||
password: await hashPassword(user.value.password)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
message.success(t('success'));
|
|
||||||
await fetchData();
|
|
||||||
showCreateUser.value = false;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteUser = async () => {
|
|
||||||
try {
|
|
||||||
await api.fetch(`/admin/users/${curUserId.value}`, {
|
|
||||||
method: "DELETE"
|
|
||||||
});
|
|
||||||
message.success(t('success'));
|
|
||||||
showDeleteUser.value = false;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: "ID",
|
|
||||||
key: "id"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('user_email'),
|
|
||||||
key: "user_email"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('address_count'),
|
|
||||||
key: "address_count",
|
|
||||||
render(row) {
|
|
||||||
return h(NBadge, {
|
|
||||||
value: row.address_count,
|
|
||||||
'show-zero': true,
|
|
||||||
max: 99,
|
|
||||||
type: "success"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('created_at'),
|
|
||||||
key: "created_at"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('actions'),
|
|
||||||
key: 'actions',
|
|
||||||
render(row) {
|
|
||||||
return h('div', [
|
|
||||||
h(NMenu, {
|
|
||||||
mode: "horizontal",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
label: t('actions'),
|
|
||||||
icon: () => h(MenuFilled),
|
|
||||||
key: "action",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
label: () => h(NButton,
|
|
||||||
{
|
|
||||||
text: true,
|
|
||||||
onClick: () => {
|
|
||||||
curUserId.value = row.id;
|
|
||||||
newResetPassword.value = '';
|
|
||||||
showResetPassword.value = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ default: () => t('resetPassword') }
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: () => h(NButton,
|
|
||||||
{
|
|
||||||
text: true,
|
|
||||||
onClick: () => {
|
|
||||||
curUserId.value = row.id;
|
|
||||||
user.value.email = '';
|
|
||||||
user.value.password = '';
|
|
||||||
showDeleteUser.value = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ default: () => t('delete') }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
watch([page, pageSize], async () => {
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-modal v-model:show="showCreateUser" preset="dialog" :title="t('createUser')">
|
|
||||||
<n-form>
|
|
||||||
<n-form-item-row :label="t('email')" required>
|
|
||||||
<n-input v-model:value="user.email" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('password')" required>
|
|
||||||
<n-input v-model:value="user.password" type="password" show-password-on="click" />
|
|
||||||
</n-form-item-row>
|
|
||||||
</n-form>
|
|
||||||
<template #action>
|
|
||||||
<n-button :loading="loading" @click="createUser" size="small" tertiary type="primary">
|
|
||||||
{{ t('createUser') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showResetPassword" preset="dialog" :title="t('resetPassword')">
|
|
||||||
<n-form-item-row :label="t('password')" required>
|
|
||||||
<n-input v-model:value="newResetPassword" type="password" show-password-on="click" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<template #action>
|
|
||||||
<n-button :loading="loading" @click="resetPassword" size="small" tertiary type="primary">
|
|
||||||
{{ t('resetPassword') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-modal v-model:show="showDeleteUser" preset="dialog" :title="t('deleteUser')">
|
|
||||||
<p>{{ t('deleteUserTip') }}</p>
|
|
||||||
<template #action>
|
|
||||||
<n-button :loading="loading" @click="deleteUser" size="small" tertiary type="error">
|
|
||||||
{{ t('deleteUser') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input v-model:value="userQuery" />
|
|
||||||
<n-button @click="fetchData" type="primary" tertiary>
|
|
||||||
{{ 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>
|
|
||||||
<template #suffix>
|
|
||||||
<n-button @click="showCreateUser = true" size="small" tertiary type="primary"
|
|
||||||
style="margin-left: 10px">
|
|
||||||
{{ t('createUser') }}
|
|
||||||
</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>
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted, ref } 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: {
|
|
||||||
save: 'Save',
|
|
||||||
successTip: 'Save Success',
|
|
||||||
enable: 'Enable',
|
|
||||||
enableUserRegister: 'Allow User Register',
|
|
||||||
enableMailVerify: 'Enable Mail Verify (Send address must be an address in the system with a balance and can send mail normally)',
|
|
||||||
verifyMailSender: 'Verify Mail Sender',
|
|
||||||
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
|
|
||||||
mailAllowList: 'Mail Address Allow List',
|
|
||||||
maxAddressCount: 'Maximum number of email addresses that can be binded',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
save: '保存',
|
|
||||||
successTip: '保存成功',
|
|
||||||
enable: '启用',
|
|
||||||
enableUserRegister: "允许用户注册",
|
|
||||||
enableMailVerify: '启用邮件验证(发送地址必须是系统中能有余额且能正常发送邮件的地址)',
|
|
||||||
verifyMailSender: '验证邮件发送地址',
|
|
||||||
enableMailAllowList: '启用邮件地址白名单(可手动输入)',
|
|
||||||
mailAllowList: '邮件地址白名单',
|
|
||||||
maxAddressCount: '可绑定最大邮箱地址数量',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const commonMail = [
|
|
||||||
"gmail.com", "163.com", "126.com", "qq.com", "outlook.com", "hotmail.com",
|
|
||||||
"icloud.com", "yahoo.com", "foxmail.com"
|
|
||||||
]
|
|
||||||
|
|
||||||
const mailAllowOptions = commonMail.map((item) => {
|
|
||||||
return { label: item, value: item }
|
|
||||||
})
|
|
||||||
|
|
||||||
const userSettings = ref({
|
|
||||||
enable: false,
|
|
||||||
enableMailVerify: false,
|
|
||||||
verifyMailSender: "",
|
|
||||||
enableMailAllowList: false,
|
|
||||||
mailAllowList: commonMail,
|
|
||||||
maxAddressCount: 5,
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const res = await api.fetch(`/admin/user_settings`)
|
|
||||||
Object.assign(userSettings.value, res)
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const save = async () => {
|
|
||||||
try {
|
|
||||||
await api.fetch(`/admin/user_settings`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify(userSettings.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 :model="userSettings">
|
|
||||||
<n-form-item-row :label="t('enableUserRegister')">
|
|
||||||
<n-checkbox v-model:checked="userSettings.enable" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('enableMailVerify')">
|
|
||||||
<n-input-group>
|
|
||||||
<n-checkbox v-model:checked="userSettings.enableMailVerify" style="width: 20%;">
|
|
||||||
{{ t('enable') }}
|
|
||||||
</n-checkbox>
|
|
||||||
<n-input v-model:value="userSettings.verifyMailSender" style="width: 80%;"
|
|
||||||
:placeholder="t('verifyMailSender')" />
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('enableMailAllowList')">
|
|
||||||
<n-input-group>
|
|
||||||
<n-checkbox v-model:checked="userSettings.enableMailAllowList" style="width: 20%;">
|
|
||||||
{{ t('enable') }}
|
|
||||||
</n-checkbox>
|
|
||||||
<n-select v-model:value="userSettings.mailAllowList" filterable multiple tag style="width: 80%;"
|
|
||||||
:options="mailAllowOptions" :placeholder="t('mailAllowList')" />
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('maxAddressCount')">
|
|
||||||
<n-input-group>
|
|
||||||
<n-input-number v-model:value="userSettings.maxAddressCount"
|
|
||||||
:placeholder="t('maxAddressCount')" />
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-button @click="save" type="primary" block :loading="loading">
|
|
||||||
{{ t('save') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
text-align: left;
|
|
||||||
place-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<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" :show-icon="false">
|
|
||||||
<span>{{ t('adminContact', { msg: openSettings.adminContact }) }}</span>
|
|
||||||
</n-alert>
|
|
||||||
</template>
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
|
|
||||||
const {
|
|
||||||
localeCache, mailboxSplitSize, useIframeShowMail, preferShowTextMail,
|
|
||||||
globalTabplacement, useSideMargin
|
|
||||||
} = useGlobalState()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
mailboxSplitSize: 'Mailbox Split Size',
|
|
||||||
useIframeShowMail: 'Use iframe Show HTML Mail',
|
|
||||||
preferShowTextMail: 'Display text Mail by default',
|
|
||||||
useSideMargin: 'Turn on the side margins on the left and right sides of the page',
|
|
||||||
globalTabplacement: 'Global Tab Placement',
|
|
||||||
left: 'left',
|
|
||||||
top: 'top',
|
|
||||||
right: 'right',
|
|
||||||
bottom: 'bottom',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
mailboxSplitSize: '邮箱界面分栏大小',
|
|
||||||
preferShowTextMail: '默认以文本显示邮件',
|
|
||||||
useIframeShowMail: '使用iframe显示HTML邮件',
|
|
||||||
globalTabplacement: '全局选项卡位置',
|
|
||||||
useSideMargin: '开启页面左右两侧侧边距',
|
|
||||||
left: '左侧',
|
|
||||||
top: '顶部',
|
|
||||||
right: '右侧',
|
|
||||||
bottom: '底部',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="center">
|
|
||||||
<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('preferShowTextMail')">
|
|
||||||
<n-switch v-model:value="preferShowTextMail" :round="false" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('useIframeShowMail')">
|
|
||||||
<n-switch v-model:value="useIframeShowMail" :round="false" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('useSideMargin')">
|
|
||||||
<n-switch v-model:value="useSideMargin" :round="false" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('globalTabplacement')">
|
|
||||||
<n-radio-group v-model:value="globalTabplacement">
|
|
||||||
<n-radio-button value="top" :label="t('top')" />
|
|
||||||
<n-radio-button value="left" :label="t('left')" />
|
|
||||||
<n-radio-button value="right" :label="t('right')" />
|
|
||||||
<n-radio-button value="bottom" :label="t('bottom')" />
|
|
||||||
</n-radio-group>
|
|
||||||
</n-form-item-row>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.n-card {
|
|
||||||
max-width: 800px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { NewLabelOutlined, EmailOutlined } from '@vicons/material'
|
|
||||||
|
|
||||||
import AdminContact from '../common/AdminContact.vue'
|
|
||||||
import Turnstile from '../../components/Turnstile.vue'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
|
||||||
const message = useMessage()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const {
|
|
||||||
jwt, localeCache, loading, openSettings,
|
|
||||||
showAddressCredential, userSettings
|
|
||||||
} = useGlobalState()
|
|
||||||
|
|
||||||
const tabValue = ref('signin')
|
|
||||||
const credential = ref('')
|
|
||||||
const emailName = ref("")
|
|
||||||
const emailDomain = ref("")
|
|
||||||
const cfToken = ref("")
|
|
||||||
|
|
||||||
const login = async () => {
|
|
||||||
if (!credential.value) {
|
|
||||||
message.error(t('credentialInput'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
jwt.value = credential.value;
|
|
||||||
await api.getSettings();
|
|
||||||
try {
|
|
||||||
await api.bindUserAddress();
|
|
||||||
} catch (error) {
|
|
||||||
message.error(`${t('bindUserAddressError')}: ${error.message}`);
|
|
||||||
}
|
|
||||||
await router.push("/");
|
|
||||||
} 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: 'Create 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.',
|
|
||||||
credential: 'Email Address Credential',
|
|
||||||
ok: 'OK',
|
|
||||||
generateName: 'Generate Fake Name',
|
|
||||||
help: 'Help',
|
|
||||||
credentialInput: 'Please input the Mail Address Credential',
|
|
||||||
bindUserInfo: 'Logged in user, login without binding email or create new email address will bind to current user',
|
|
||||||
bindUserAddressError: 'Error when bind email address to user',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
login: '登录',
|
|
||||||
pleaseGetNewEmail: '请"登录"或点击 "注册新邮箱" 按钮来获取一个新的邮箱地址',
|
|
||||||
getNewEmail: '创建新邮箱',
|
|
||||||
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 ., a-z, A-Z, 0-9',
|
|
||||||
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
|
||||||
getNewEmailTip3: '你可以从下拉列表中选择一个域名。',
|
|
||||||
credential: '邮箱地址凭据',
|
|
||||||
ok: '确定',
|
|
||||||
generateName: '生成随机名字',
|
|
||||||
help: '帮助',
|
|
||||||
credentialInput: '请输入邮箱地址凭据',
|
|
||||||
bindUserInfo: '已登录用户, 登录未绑定邮箱或创建新邮箱地址将绑定到当前用户',
|
|
||||||
bindUserAddressError: '绑定邮箱地址到用户时错误',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const generateNameLoading = ref(false);
|
|
||||||
const generateName = async () => {
|
|
||||||
try {
|
|
||||||
generateNameLoading.value = true;
|
|
||||||
const { faker } = await import('https://esm.sh/@faker-js/faker');
|
|
||||||
emailName.value = faker.internet.email()
|
|
||||||
.split('@')[0]
|
|
||||||
.replace(/\s+/g, '.')
|
|
||||||
.replace(/\.{2,}/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`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
name: emailName.value,
|
|
||||||
domain: emailDomain.value,
|
|
||||||
cf_token: cfToken.value,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
jwt.value = res["jwt"];
|
|
||||||
await api.getSettings();
|
|
||||||
await router.push("/");
|
|
||||||
showAddressCredential.value = true;
|
|
||||||
try {
|
|
||||||
await api.bindUserAddress();
|
|
||||||
} catch (error) {
|
|
||||||
message.error(`${t('bindUserAddressError')}: ${error.message}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
if (!openSettings.value.domains || openSettings.value.domains.length === 0) {
|
|
||||||
await api.getOpenSettings();
|
|
||||||
}
|
|
||||||
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0]?.value : "";
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-alert v-if="userSettings.user_email" :show-icon="false" closable>
|
|
||||||
<span>{{ t('bindUserInfo') }}</span>
|
|
||||||
</n-alert>
|
|
||||||
<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('credential')" required>
|
|
||||||
<n-input v-model:value="credential" type="textarea" :autosize="{ minRows: 3 }" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-button @click="login" :loading="loading" type="primary" block secondary strong>
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="EmailOutlined" />
|
|
||||||
</template>
|
|
||||||
{{ t('login') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button v-if="openSettings.enableUserCreateEmail" @click="tabValue = 'register'" block secondary
|
|
||||||
strong>
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="NewLabelOutlined" />
|
|
||||||
</template>
|
|
||||||
{{ 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>
|
|
||||||
<Turnstile v-model:value="cfToken" />
|
|
||||||
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="NewLabelOutlined" />
|
|
||||||
</template>
|
|
||||||
{{ t('getNewEmail') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form>
|
|
||||||
</n-spin>
|
|
||||||
</n-tab-pane>
|
|
||||||
<n-tab-pane name="help" :tab="t('help')">
|
|
||||||
<n-alert :show-icon="false">
|
|
||||||
<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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-form {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
|
||||||
import Appearance from '../common/Appearance.vue'
|
|
||||||
|
|
||||||
const {
|
|
||||||
jwt, localeCache, settings, showAddressCredential, loading
|
|
||||||
} = useGlobalState()
|
|
||||||
const router = useRouter()
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const showLogout = ref(false)
|
|
||||||
const showDelteAccount = ref(false)
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
logout: "Logout",
|
|
||||||
delteAccount: "Delete Account",
|
|
||||||
showAddressCredential: 'Show Address Credential',
|
|
||||||
logoutConfirm: 'Are you sure to logout?',
|
|
||||||
delteAccount: "Delete Account",
|
|
||||||
delteAccountConfirm: "Are you sure to delete your account and all emails for this account?",
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
logout: '退出登录',
|
|
||||||
delteAccount: "删除账户",
|
|
||||||
showAddressCredential: '查看邮箱地址凭证',
|
|
||||||
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>
|
|
||||||
<Appearance />
|
|
||||||
<n-button @click="showAddressCredential = true" type="primary" secondary block strong>
|
|
||||||
{{ t('showAddressCredential') }}
|
|
||||||
</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="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>
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import useClipboard from 'vue-clipboard3'
|
|
||||||
import { onMounted } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { Copy, User } from '@vicons/fa'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
|
||||||
import Login from '../common/Login.vue'
|
|
||||||
|
|
||||||
const { toClipboard } = useClipboard()
|
|
||||||
const message = useMessage()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const {
|
|
||||||
jwt, localeCache, settings, showAddressCredential, openSettings
|
|
||||||
} = useGlobalState()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
yourAddress: 'Your email address is',
|
|
||||||
ok: 'OK',
|
|
||||||
copy: 'Copy',
|
|
||||||
copied: 'Copied',
|
|
||||||
fetchAddressError: 'Mail address credential is invalid or account not exist, it may be network connection issue, please try again later.',
|
|
||||||
mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.',
|
|
||||||
addressCredential: 'Mail Address Credential',
|
|
||||||
addressCredentialTip: 'Please copy the Mail Address Credential and you can use it to login to your email account.',
|
|
||||||
userLogin: 'User Login',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
yourAddress: '你的邮箱地址是',
|
|
||||||
ok: '确定',
|
|
||||||
copy: '复制',
|
|
||||||
copied: '已复制',
|
|
||||||
fetchAddressError: '邮箱地址凭证无效或邮箱地址不存在,也可能是网络连接异常,请稍后再尝试。',
|
|
||||||
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
|
|
||||||
addressCredential: '邮箱地址凭证',
|
|
||||||
addressCredentialTip: '请复制邮箱地址凭证,你可以使用它登录你的邮箱。',
|
|
||||||
userLogin: '用户登录',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const copy = async () => {
|
|
||||||
try {
|
|
||||||
await toClipboard(settings.value.address)
|
|
||||||
message.success(t('copied'));
|
|
||||||
} catch (e) {
|
|
||||||
message.error(e.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await api.getSettings();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-card v-if="!settings.fetched">
|
|
||||||
<n-skeleton style="height: 50vh" />
|
|
||||||
</n-card>
|
|
||||||
<div v-else-if="settings.address">
|
|
||||||
<n-alert v-if="settings.has_v1_mails" type="warning" :show-icon="false" closable>
|
|
||||||
<span>
|
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" href="https://mail-v1.awsl.uk">
|
|
||||||
<b>{{ t('mailV1Alert') }} </b>
|
|
||||||
</n-button>
|
|
||||||
</span>
|
|
||||||
</n-alert>
|
|
||||||
<n-alert type="info" :show-icon="false">
|
|
||||||
<span>
|
|
||||||
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
|
||||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary type="primary">
|
|
||||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
|
||||||
</n-button>
|
|
||||||
</span>
|
|
||||||
</n-alert>
|
|
||||||
</div>
|
|
||||||
<div v-else class="center">
|
|
||||||
<n-card style="max-width: 600px;">
|
|
||||||
<n-alert v-if="jwt" type="warning" :show-icon="false" closable>
|
|
||||||
<span>{{ t('fetchAddressError') }}</span>
|
|
||||||
</n-alert>
|
|
||||||
<Login />
|
|
||||||
<n-divider />
|
|
||||||
<n-button @click="router.push('/user')" type="primary" block secondary strong>
|
|
||||||
<template #icon>
|
|
||||||
<n-icon :component="User" />
|
|
||||||
</template>
|
|
||||||
{{ t('userLogin') }}
|
|
||||||
</n-button>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
<n-modal v-model:show="showAddressCredential" preset="dialog" :title="t('addressCredential')">
|
|
||||||
<span>
|
|
||||||
<p>{{ t("addressCredentialTip") }}</p>
|
|
||||||
</span>
|
|
||||||
<n-card>
|
|
||||||
<b>{{ jwt }}</b>
|
|
||||||
</n-card>
|
|
||||||
</n-modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.n-alert {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-card {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
text-align: left;
|
|
||||||
place-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
<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" style="width: 100%;">
|
|
||||||
<pre style="overflow: auto;">{{ 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" tertiary>
|
|
||||||
{{ 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>
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import '@wangeditor/editor/dist/css/style.css'
|
|
||||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { onMounted, onBeforeUnmount, ref, shallowRef } from 'vue'
|
|
||||||
import AdminContact from '../common/AdminContact.vue'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
|
||||||
|
|
||||||
const message = useMessage()
|
|
||||||
const isPreview = ref(false)
|
|
||||||
const editorRef = shallowRef()
|
|
||||||
|
|
||||||
|
|
||||||
const { settings, sendMailModel, indexTab } = 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: sendMailModel.value.fromName,
|
|
||||||
to_name: sendMailModel.value.toName,
|
|
||||||
to_mail: sendMailModel.value.toMail,
|
|
||||||
subject: sendMailModel.value.subject,
|
|
||||||
is_html: sendMailModel.value.contentType != 'text',
|
|
||||||
content: sendMailModel.value.content,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
sendMailModel.value = {
|
|
||||||
fromName: "",
|
|
||||||
toName: "",
|
|
||||||
toMail: "",
|
|
||||||
subject: "",
|
|
||||||
contentType: 'text',
|
|
||||||
content: "",
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
} finally {
|
|
||||||
message.success(t("successSend"));
|
|
||||||
indexTab.value = '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="false">
|
|
||||||
{{ t('requestAccessTip') }}
|
|
||||||
<n-button type="primary" tertiary @click="requestAccess" size="small">{{ t('requestAccess')
|
|
||||||
}}</n-button>
|
|
||||||
</n-alert>
|
|
||||||
<br />
|
|
||||||
<AdminContact />
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<n-alert type="info" :show-icon="false">
|
|
||||||
{{ 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="sendMailModel">
|
|
||||||
<n-form-item :label="t('fromName')" label-placement="top">
|
|
||||||
<n-input-group>
|
|
||||||
<n-input v-model:value="sendMailModel.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="sendMailModel.toName" />
|
|
||||||
<n-input v-model:value="sendMailModel.toMail" />
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="t('subject')" label-placement="top">
|
|
||||||
<n-input v-model:value="sendMailModel.subject" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="t('options')" label-placement="top">
|
|
||||||
<n-radio-group v-model:value="sendMailModel.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="sendMailModel.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="sendMailModel.content" />
|
|
||||||
</n-card>
|
|
||||||
<div v-else-if="sendMailModel.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="sendMailModel.content"
|
|
||||||
:defaultConfig="editorConfig" mode="default" @onCreated="handleCreated" />
|
|
||||||
</div>
|
|
||||||
<n-input v-else type="textarea" v-model:value="sendMailModel.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>
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, h, onMounted } from 'vue';
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { NBadge, NPopconfirm, NButton } from 'naive-ui'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
|
||||||
|
|
||||||
const { localeCache, jwt } = useGlobalState()
|
|
||||||
const message = useMessage()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
success: 'success',
|
|
||||||
name: 'Name',
|
|
||||||
mail_count: 'Mail Count',
|
|
||||||
send_count: 'Send Count',
|
|
||||||
actions: 'Actions',
|
|
||||||
changeMailAddress: 'Change Mail Address',
|
|
||||||
unbindAddress: 'Unbind Address',
|
|
||||||
unbindAddressTip: 'Before unbinding, please switch to this email address and save the email address credential.',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
success: '成功',
|
|
||||||
name: '名称',
|
|
||||||
mail_count: '邮件数量',
|
|
||||||
send_count: '发送数量',
|
|
||||||
actions: '操作',
|
|
||||||
changeMailAddress: '切换邮箱地址',
|
|
||||||
unbindAddress: '解绑地址',
|
|
||||||
unbindAddressTip: '解绑前请切换到此邮箱地址并保存邮箱地址凭证。',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = ref([])
|
|
||||||
|
|
||||||
const changeMailAddress = async (address_id) => {
|
|
||||||
try {
|
|
||||||
const res = await api.fetch(`/user_api/bind_address_jwt/${address_id}`);
|
|
||||||
message.success(t('changeMailAddress') + " " + t('success'));
|
|
||||||
if (!res.jwt) {
|
|
||||||
message.error("jwt not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
jwt.value = res.jwt;
|
|
||||||
await router.push('/');
|
|
||||||
location.reload();
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unbindAddress = async (address_id) => {
|
|
||||||
try {
|
|
||||||
const res = await api.fetch(`/user_api/unbind_address`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({ address_id })
|
|
||||||
});
|
|
||||||
message.success(t('unbindAddress') + " " + t('success'));
|
|
||||||
await fetchData();
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
try {
|
|
||||||
const { results, count: addressCount } = await api.fetch(
|
|
||||||
`/user_api/bind_address`
|
|
||||||
);
|
|
||||||
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('mail_count'),
|
|
||||||
key: "mail_count",
|
|
||||||
render(row) {
|
|
||||||
return h(NBadge, {
|
|
||||||
value: row.mail_count,
|
|
||||||
'show-zero': true,
|
|
||||||
max: 99,
|
|
||||||
type: "success"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('send_count'),
|
|
||||||
key: "send_count",
|
|
||||||
render(row) {
|
|
||||||
return h(NBadge, {
|
|
||||||
value: row.send_count,
|
|
||||||
'show-zero': true,
|
|
||||||
max: 99,
|
|
||||||
type: "success"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('actions'),
|
|
||||||
key: 'actions',
|
|
||||||
render(row) {
|
|
||||||
return h('div', [
|
|
||||||
h(NPopconfirm,
|
|
||||||
{
|
|
||||||
onPositiveClick: () => changeMailAddress(row.id)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
trigger: () => h(NButton,
|
|
||||||
{
|
|
||||||
tertiary: true,
|
|
||||||
type: "primary",
|
|
||||||
},
|
|
||||||
{ default: () => t('changeMailAddress') }
|
|
||||||
),
|
|
||||||
default: () => `${t('changeMailAddress')}?`
|
|
||||||
}
|
|
||||||
),
|
|
||||||
h(NPopconfirm,
|
|
||||||
{
|
|
||||||
onPositiveClick: () => unbindAddress(row.id)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
trigger: () => h(NButton,
|
|
||||||
{
|
|
||||||
tertiary: true,
|
|
||||||
type: "error",
|
|
||||||
},
|
|
||||||
{ default: () => t('unbindAddress') }
|
|
||||||
),
|
|
||||||
default: () => t('unbindAddressTip')
|
|
||||||
}
|
|
||||||
),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import Login from '../common/Login.vue'
|
|
||||||
|
|
||||||
const { userJwt, localeCache, userSettings, } = useGlobalState()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
logout: 'Logout',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
logout: '退出登录',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="center" v-if="userSettings.user_email">
|
|
||||||
<n-card style="max-width: 600px;">
|
|
||||||
<Login />
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
text-align: center;
|
|
||||||
place-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
|
||||||
import UserLogin from './UserLogin.vue'
|
|
||||||
|
|
||||||
const message = useMessage()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const {
|
|
||||||
localeCache, userSettings, userJwt, userOpenSettings
|
|
||||||
} = useGlobalState()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
currentUser: 'Current Login User',
|
|
||||||
fetchUserSettingsError: 'Login password is invalid or account not exist, it may be network connection issue, please try again later.',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
currentUser: '当前登录用户',
|
|
||||||
fetchUserSettingsError: '登录信息已过期或账号不存在,也可能是网络连接异常,请稍后再尝试。',
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await api.getUserOpenSettings(message);
|
|
||||||
await api.getUserSettings(message);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-card v-if="!userSettings.fetched">
|
|
||||||
<n-skeleton style="height: 50vh" />
|
|
||||||
</n-card>
|
|
||||||
<div v-else-if="userSettings.user_email">
|
|
||||||
<n-alert type="success" :show-icon="false">
|
|
||||||
<span>
|
|
||||||
<b>{{ t('currentUser') }} <b>{{ userSettings.user_email }}</b></b>
|
|
||||||
</span>
|
|
||||||
</n-alert>
|
|
||||||
</div>
|
|
||||||
<div v-else class="center">
|
|
||||||
<n-card style="max-width: 600px;">
|
|
||||||
<n-alert v-if="userJwt" type="warning" :show-icon="false" closable>
|
|
||||||
<span>{{ t('fetchUserSettingsError') }}</span>
|
|
||||||
</n-alert>
|
|
||||||
<UserLogin />
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.n-alert {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
text-align: center;
|
|
||||||
place-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { computed, onMounted, ref } from "vue";
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
|
|
||||||
import { api } from '../../api';
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { hashPassword } from '../../utils';
|
|
||||||
|
|
||||||
import Turnstile from '../../components/Turnstile.vue';
|
|
||||||
|
|
||||||
const { userJwt, localeCache, userTab, userOpenSettings } = useGlobalState()
|
|
||||||
const message = useMessage();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
login: 'Login',
|
|
||||||
register: 'Register',
|
|
||||||
email: 'Email',
|
|
||||||
password: 'Password',
|
|
||||||
verifyCode: 'Verification Code',
|
|
||||||
verifyCodeSent: 'Verification Code Sent, expires in {timeout} seconds',
|
|
||||||
waitforVerifyCode: 'Wait for {timeout} seconds',
|
|
||||||
sendVerificationCode: 'Send Verification Code',
|
|
||||||
forgotPassword: 'Forgot Password',
|
|
||||||
cannotForgotPassword: 'Mail verification is disabled or register is disabled, cannot reset password, please contact administrator',
|
|
||||||
resetPassword: 'Reset Password',
|
|
||||||
pleaseInput: 'Please input email and password',
|
|
||||||
pleaseInputEmail: 'Please input email',
|
|
||||||
pleaseInputCode: 'Please input code',
|
|
||||||
pleaseCompleteTurnstile: 'Please complete turnstile',
|
|
||||||
pleaseLogin: 'Please login',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
login: '登录',
|
|
||||||
register: '注册',
|
|
||||||
email: '邮箱',
|
|
||||||
password: '密码',
|
|
||||||
verifyCode: '验证码',
|
|
||||||
sendVerificationCode: '发送验证码',
|
|
||||||
verifyCodeSent: '验证码已发送, {timeout} 秒后失效',
|
|
||||||
waitforVerifyCode: '等待{timeout}秒',
|
|
||||||
forgotPassword: '忘记密码',
|
|
||||||
cannotForgotPassword: '未开启邮箱验证或未开启注册功能,无法重置密码,请联系管理员',
|
|
||||||
resetPassword: '重置密码',
|
|
||||||
pleaseInput: '请输入邮箱和密码',
|
|
||||||
pleaseInputEmail: '请输入邮箱',
|
|
||||||
pleaseInputCode: '请输入验证码',
|
|
||||||
pleaseCompleteTurnstile: '请完成人机验证',
|
|
||||||
pleaseLogin: '请登录',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const tabValue = ref("signin");
|
|
||||||
const showModal = ref(false);
|
|
||||||
const user = ref({
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
code: ""
|
|
||||||
});
|
|
||||||
const cfToken = ref("")
|
|
||||||
|
|
||||||
const emailLogin = async () => {
|
|
||||||
if (!user.value.email || !user.value.password) {
|
|
||||||
message.error(t('pleaseInput'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await api.fetch(`/user_api/login`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: user.value.email,
|
|
||||||
// hash password
|
|
||||||
password: await hashPassword(user.value.password)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
userJwt.value = res.jwt;
|
|
||||||
location.reload();
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "login failed");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const verifyCodeExpire = ref(0);
|
|
||||||
const verifyCodeTimeout = ref(0);
|
|
||||||
|
|
||||||
const getVerifyCodeTimeout = () => {
|
|
||||||
if (!verifyCodeExpire.value || verifyCodeExpire.value < new Date().getTime()) return 0;
|
|
||||||
return Math.round((verifyCodeExpire.value - new Date().getTime()) / 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sendVerificationCode = async () => {
|
|
||||||
if (!user.value.email) {
|
|
||||||
message.error(t('pleaseInputEmail'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!cfToken.value && userOpenSettings.value.enableMailVerify) {
|
|
||||||
message.error(t('pleaseCompleteTurnstile'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await api.fetch(`/user_api/verify_code`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: user.value.email,
|
|
||||||
cf_token: cfToken.value
|
|
||||||
})
|
|
||||||
});
|
|
||||||
if (res && res.expirationTtl) {
|
|
||||||
message.success(t('verifyCodeSent', { timeout: res.expirationTtl }));
|
|
||||||
verifyCodeExpire.value = new Date().getTime() + res.expirationTtl * 1000;
|
|
||||||
const intervalId = setInterval(() => {
|
|
||||||
verifyCodeTimeout.value = getVerifyCodeTimeout();
|
|
||||||
if (verifyCodeTimeout.value <= 0) {
|
|
||||||
clearInterval(intervalId);
|
|
||||||
verifyCodeTimeout.value = 0;
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "send verification code failed");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const emailSignup = async () => {
|
|
||||||
if (!user.value.email || !user.value.password) {
|
|
||||||
message.error(t('pleaseInput'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!user.value.code && userOpenSettings.value.enableMailVerify) {
|
|
||||||
message.error(t('pleaseInputCode'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await api.fetch(`/user_api/register`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: user.value.email,
|
|
||||||
// hash password
|
|
||||||
password: await hashPassword(user.value.password),
|
|
||||||
code: user.value.code
|
|
||||||
}),
|
|
||||||
message: message
|
|
||||||
});
|
|
||||||
if (res) {
|
|
||||||
tabValue.value = "signin";
|
|
||||||
message.success(t('pleaseLogin'));
|
|
||||||
}
|
|
||||||
showModal.value = false;
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "register failed");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="center">
|
|
||||||
<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('email')" required>
|
|
||||||
<n-input v-model:value="user.email" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('password')" required>
|
|
||||||
<n-input v-model:value="user.password" type="password" show-password-on="click" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-button @click="emailLogin" type="primary" block secondary strong>
|
|
||||||
{{ t('login') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button @click="showModal = true" type="info" quaternary size="tiny">
|
|
||||||
{{ t('forgotPassword') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form>
|
|
||||||
</n-tab-pane>
|
|
||||||
<n-tab-pane v-if="userOpenSettings.enable" name="signup" :tab="t('register')">
|
|
||||||
<n-form>
|
|
||||||
<n-form-item-row :label="t('email')" required>
|
|
||||||
<n-input v-model:value="user.email" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('password')" required>
|
|
||||||
<n-input v-model:value="user.password" type="password" show-password-on="click" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<Turnstile v-if="userOpenSettings.enableMailVerify" v-model:value="cfToken" />
|
|
||||||
<n-form-item-row v-if="userOpenSettings.enableMailVerify" :label="t('verifyCode')" required>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input v-model:value="user.code" />
|
|
||||||
<n-button @click="sendVerificationCode" style="margin-bottom: 0" type="primary" ghost
|
|
||||||
:disabled="verifyCodeTimeout > 0">
|
|
||||||
{{ verifyCodeTimeout > 0 ? t('waitforVerifyCode', { timeout: verifyCodeTimeout })
|
|
||||||
: t('sendVerificationCode') }}
|
|
||||||
</n-button>
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item-row>
|
|
||||||
</n-form>
|
|
||||||
<n-button @click="emailSignup" type="primary" block secondary strong>
|
|
||||||
{{ t('register') }}
|
|
||||||
</n-button>
|
|
||||||
</n-tab-pane>
|
|
||||||
</n-tabs>
|
|
||||||
<n-modal v-model:show="showModal" style="max-width: 600px;" preset="card" :title="t('forgotPassword')">
|
|
||||||
<n-form v-if="userOpenSettings.enable && userOpenSettings.enableMailVerify">
|
|
||||||
<n-form-item-row :label="t('email')" required>
|
|
||||||
<n-input v-model:value="user.email" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-form-item-row :label="t('password')" required>
|
|
||||||
<n-input v-model:value="user.password" type="password" show-password-on="click" />
|
|
||||||
</n-form-item-row>
|
|
||||||
<Turnstile v-model:value="cfToken" />
|
|
||||||
<n-form-item-row :label="t('verifyCode')" required>
|
|
||||||
<n-input-group>
|
|
||||||
<n-input v-model:value="user.code" />
|
|
||||||
<n-button @click="sendVerificationCode" style="margin-bottom: 0" type="primary" ghost
|
|
||||||
:disabled="verifyCodeTimeout > 0">
|
|
||||||
{{ verifyCodeTimeout > 0 ? t('waitforVerifyCode', { timeout: verifyCodeTimeout })
|
|
||||||
: t('sendVerificationCode') }}
|
|
||||||
</n-button>
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item-row>
|
|
||||||
<n-button @click="emailSignup" type="primary" block secondary strong>
|
|
||||||
{{ t('resetPassword') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form>
|
|
||||||
<n-alert v-else :show-icon="false">
|
|
||||||
<span>
|
|
||||||
{{ t('cannotForgotPassword') }}
|
|
||||||
</span>
|
|
||||||
</n-alert>
|
|
||||||
</n-modal>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
text-align: center;
|
|
||||||
place-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
|
||||||
|
|
||||||
const { userJwt, localeCache, userSettings, } = useGlobalState()
|
|
||||||
const router = useRouter()
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const showLogout = ref(false)
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
logout: 'Logout',
|
|
||||||
logoutConfirm: 'Are you sure you want to logout?',
|
|
||||||
passordTip: 'The server will only receive the hash value of the password, and will not receive the plaintext password, so it cannot view or retrieve your password. If the administrator enables email verification, you can reset the password in incognito mode',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
logout: '退出登录',
|
|
||||||
logoutConfirm: '确定要退出登录吗?',
|
|
||||||
passordTip: '服务器只会接收到密码的哈希值,不会接收到明文密码,因此无法查看或者找回您的密码, 如果管理员启用了邮件验证您可以在无痕模式重置密码',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const logout = async () => {
|
|
||||||
userJwt.value = '';
|
|
||||||
location.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchData = async () => {
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="center" v-if="userSettings.user_email">
|
|
||||||
<n-card>
|
|
||||||
<n-alert :show-icon="false">
|
|
||||||
<span>
|
|
||||||
{{ t('passordTip') }}
|
|
||||||
</span>
|
|
||||||
</n-alert>
|
|
||||||
<n-button @click="showLogout = true" secondary block strong>
|
|
||||||
{{ t('logout') }}
|
|
||||||
</n-button>
|
|
||||||
</n-card>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.n-card {
|
|
||||||
max-width: 800px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-button {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -3,30 +3,19 @@ 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'
|
||||||
import wasm from "vite-plugin-wasm";
|
|
||||||
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(),
|
splitVendorChunkPlugin(),
|
||||||
topLevelAwait(),
|
|
||||||
AutoImport({
|
AutoImport({
|
||||||
imports: [
|
imports: [
|
||||||
'vue',
|
'vue',
|
||||||
|
|||||||
|
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 |
@@ -1,2 +0,0 @@
|
|||||||
proxy_url=https://temp-email-api.xxx.xxx
|
|
||||||
port=8025
|
|
||||||
161
smtp_proxy_server/.gitignore
vendored
@@ -1,161 +0,0 @@
|
|||||||
# 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*
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
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" ]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
aiosmtpd==1.4.5
|
|
||||||
pydantic-settings==2.2.1
|
|
||||||
requests==2.31.0
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
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 = {
|
|
||||||
"token": session.auth_data.password.decode(),
|
|
||||||
"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}/external/api/send_mail",
|
|
||||||
json=send_body, headers={
|
|
||||||
"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
@@ -1,307 +0,0 @@
|
|||||||
## 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
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
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'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
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'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
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: '查看邮件 API', link: 'feature/mail-api' },
|
|
||||||
{ text: '配置子域名邮箱', link: 'feature/subdomain' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: '功能简介',
|
|
||||||
collapsed: false,
|
|
||||||
items: [
|
|
||||||
{ text: 'Admin 控制台', link: 'feature/admin' },
|
|
||||||
{ text: 'Admin 用户管理', link: 'feature/admin-user-management' },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{ text: '参考', base: "/", link: 'reference' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
# 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
|
|
||||||
# create a namespace, and copy the output to wrangler.toml in the next step
|
|
||||||
wrangler kv:namespace create DEV
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 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`
|
|
||||||
|
|
||||||
```toml
|
|
||||||
name = "cloudflare_temp_email"
|
|
||||||
main = "src/worker.js"
|
|
||||||
compatibility_date = "2023-08-14"
|
|
||||||
node_compat = true
|
|
||||||
|
|
||||||
# enable cron if you want set auto clean up
|
|
||||||
# [triggers]
|
|
||||||
# crons = [ "0 0 * * *" ]
|
|
||||||
|
|
||||||
[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
|
|
||||||
# Turnstile verification configuration
|
|
||||||
# CF_TURNSTILE_SITE_KEY = ""
|
|
||||||
# CF_TURNSTILE_SECRET_KEY = ""
|
|
||||||
# dkim config
|
|
||||||
# DKIM_SELECTOR = "mailchannels" # Refer to the DKIM section mailchannels._domainkey for mailchannels
|
|
||||||
# DKIM_PRIVATE_KEY = "" # Refer to the contents of priv_key.txt in the DKIM section
|
|
||||||
|
|
||||||
[[d1_databases]]
|
|
||||||
binding = "DB"
|
|
||||||
database_name = "xxx" # D1 database name
|
|
||||||
database_id = "xxx" # D1 database ID
|
|
||||||
|
|
||||||
# kv config for send email verification code
|
|
||||||
# [[kv_namespaces]]
|
|
||||||
# binding = "KV"
|
|
||||||
# id = "xxxx"
|
|
||||||
|
|
||||||
# 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>`
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
---
|
|
||||||
# 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
|
|
||||||
---
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
# 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 发送邮件
|
|
||||||
---
|
|
||||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 26 KiB |
@@ -1,6 +0,0 @@
|
|||||||
# Reference
|
|
||||||
|
|
||||||
- https://developers.cloudflare.com/d1/
|
|
||||||
- https://developers.cloudflare.com/pages/
|
|
||||||
- https://developers.cloudflare.com/workers/
|
|
||||||
- https://developers.cloudflare.com/email-routing/
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# 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/) |       |
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# 初始化/更新 D1 数据库
|
|
||||||
|
|
||||||
第一次执行登录 wrangler 命令时,会提示登录, 按提示操作即可
|
|
||||||
|
|
||||||
## 初始化数据库
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd worker
|
|
||||||
cp wrangler.toml.template wrangler.toml
|
|
||||||
# 创建 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
|
|
||||||
cd worker
|
|
||||||
wrangler d1 execute dev --file=../db/2024-01-13-patch.sql
|
|
||||||
wrangler d1 execute dev --file=../db/2024-04-03-patch.sql
|
|
||||||
```
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# 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` 配置自定义域名
|
|
||||||
|
|
||||||

|
|
||||||