mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-07-04 13:51:35 +08:00
Compare commits
1 Commits
v0.2.1
...
feature/ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09e0d0b7d7 |
46
.github/workflows/tag_build.yml
vendored
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
|
|
||||||
34
CHANGELOG
Normal file
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`
|
||||||
76
CHANGELOG.md
76
CHANGELOG.md
@@ -1,76 +0,0 @@
|
|||||||
# CHANGE LOG
|
|
||||||
|
|
||||||
## 2024-04-12 v0.2.1
|
|
||||||
|
|
||||||
- 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`
|
|
||||||
36
README.md
36
README.md
@@ -2,16 +2,9 @@
|
|||||||
|
|
||||||
## [English](README_EN.md)
|
## [English](README_EN.md)
|
||||||
|
|
||||||
## [新版部署文档](https://temp-mail-docs.awsl.uk)
|
[CHANGELOG](CHANGELOG)
|
||||||
|
|
||||||
## [CHANGELOG](CHANGELOG.md)
|
[Backend](https://temp-email-api.dreamhunter2333.xyz/)
|
||||||
|
|
||||||
## [在线演示](https://mail.awsl.uk/)
|
|
||||||
|
|
||||||
[https://mail.awsl.uk](https://mail.awsl.uk/)
|
|
||||||
或者 [https://temp-email.dreamhunter2333.xyz](https://temp-email.dreamhunter2333.xyz/)
|
|
||||||
|
|
||||||
[Backend](https://temp-email-api.awsl.uk/)
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -19,7 +12,7 @@
|
|||||||

|

|
||||||

|

|
||||||
|
|
||||||
[Frontend](https://mail.awsl.uk/)
|
[Frontend](https://temp-email.dreamhunter2333.xyz/)
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
@@ -35,7 +28,6 @@
|
|||||||
|
|
||||||
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
|
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
|
||||||
- [English](#english)
|
- [English](#english)
|
||||||
- [CHANGELOG](#changelog)
|
|
||||||
- [在线演示](#在线演示)
|
- [在线演示](#在线演示)
|
||||||
- [功能/TODO](#功能todo)
|
- [功能/TODO](#功能todo)
|
||||||
- [什么是临时邮箱](#什么是临时邮箱)
|
- [什么是临时邮箱](#什么是临时邮箱)
|
||||||
@@ -46,9 +38,9 @@
|
|||||||
- [Cloudflare Workers 后端](#cloudflare-workers-后端-1)
|
- [Cloudflare Workers 后端](#cloudflare-workers-后端-1)
|
||||||
- [Cloudflare Email Routing](#cloudflare-email-routing)
|
- [Cloudflare Email Routing](#cloudflare-email-routing)
|
||||||
- [Cloudflare Pages 前端](#cloudflare-pages-前端)
|
- [Cloudflare Pages 前端](#cloudflare-pages-前端)
|
||||||
- [配置发送邮件](#配置发送邮件)
|
|
||||||
- [参考资料](#参考资料)
|
- [参考资料](#参考资料)
|
||||||
|
|
||||||
|
## [在线演示](https://temp-email.dreamhunter2333.xyz/)
|
||||||
|
|
||||||
## 功能/TODO
|
## 功能/TODO
|
||||||
|
|
||||||
@@ -63,7 +55,6 @@
|
|||||||
- [x] 增加自动回复功能
|
- [x] 增加自动回复功能
|
||||||
- [x] 增加查看附件功能
|
- [x] 增加查看附件功能
|
||||||
- [x] 使用 rust wasm 解析邮件
|
- [x] 使用 rust wasm 解析邮件
|
||||||
- [x] 支持发送邮件
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -159,8 +150,6 @@ database_id = "xxx" # D1 数据库 ID
|
|||||||
|
|
||||||
部署
|
部署
|
||||||
|
|
||||||
第一次部署会提示创建项目, `production` 分支请填写 `production`
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run deploy
|
pnpm run deploy
|
||||||
```
|
```
|
||||||
@@ -173,8 +162,6 @@ pnpm run deploy
|
|||||||
|
|
||||||
## Cloudflare Email Routing
|
## Cloudflare Email Routing
|
||||||
|
|
||||||
在将电子邮件地址绑定到您的 Worker 之前,您需要启用电子邮件路由并拥有至少一个经过验证的电子邮件地址。
|
|
||||||
|
|
||||||
配置对应域名的 `电子邮件 DNS 记录`
|
配置对应域名的 `电子邮件 DNS 记录`
|
||||||
|
|
||||||
配置 `Cloudflare Email Routing` catch-all 发送到 `worker`
|
配置 `Cloudflare Email Routing` catch-all 发送到 `worker`
|
||||||
@@ -185,8 +172,6 @@ pnpm run deploy
|
|||||||
|
|
||||||
## Cloudflare Pages 前端
|
## Cloudflare Pages 前端
|
||||||
|
|
||||||
第一次部署会提示创建项目, `production` 分支请填写 `production`
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
pnpm install
|
pnpm install
|
||||||
@@ -205,19 +190,6 @@ pnpm run deploy
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 配置发送邮件
|
|
||||||
|
|
||||||
找到域名 `DNS` 记录的 `TXT` 的 `SPF` 记录, 增加 `include:relay.mailchannels.net`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
v=spf1 include:_spf.mx.cloudflare.net include:relay.mailchannels.net ~all
|
|
||||||
```
|
|
||||||
|
|
||||||
新建 `_mailchannels` 记录, 类型为 `TXT`, 内容为 `v=mc1 cfid=你的worker域名`
|
|
||||||
|
|
||||||
- 此处 worker 域名为后端 api 的域名,比如我部署在 `https://temp-email-api.awsl.uk/`,则填写 `v=mc1 cfid=awsl.uk`
|
|
||||||
- 如果你的域名是 `https://temp-email-api.xxx.workers.dev`,则填写 `v=mc1 cfid=xxx.workers.dev`
|
|
||||||
|
|
||||||
## 参考资料
|
## 参考资料
|
||||||
|
|
||||||
- https://developers.cloudflare.com/d1/
|
- https://developers.cloudflare.com/d1/
|
||||||
|
|||||||
28
README_EN.md
28
README_EN.md
@@ -2,11 +2,9 @@
|
|||||||
|
|
||||||
## [中文](README.md)
|
## [中文](README.md)
|
||||||
|
|
||||||
[CHANGELOG](CHANGELOG.md)
|
[CHANGELOG](CHANGELOG)
|
||||||
|
|
||||||
## [Live Demo](https://mail.awsl.uk/)
|
## [Live Demo](https://temp-email.dreamhunter2333.xyz/)
|
||||||
|
|
||||||
[https://mail.awsl.uk](https://mail.awsl.uk/)
|
|
||||||
|
|
||||||
This is a temporary email service that uses Cloudflare Workers to create a temporary email address and view the received email in web browser.
|
This is a temporary email service that uses Cloudflare Workers to create a temporary email address and view the received email in web browser.
|
||||||
|
|
||||||
@@ -23,7 +21,6 @@ This is a temporary email service that uses Cloudflare Workers to create a tempo
|
|||||||
- [x] Add auto reply feature
|
- [x] Add auto reply feature
|
||||||
- [x] Add attachment viewing function
|
- [x] Add attachment viewing function
|
||||||
- [x] use rust wasm to parse email
|
- [x] use rust wasm to parse email
|
||||||
- [x] support send email
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -46,8 +43,6 @@ wrangler d1 execute dev --file=db/schema.sql
|
|||||||
|
|
||||||
### Backend - Cloudflare workers
|
### Backend - Cloudflare workers
|
||||||
|
|
||||||
The first deployment will prompt you to create a project. Please fill in `production` for the `production` branch.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd worker
|
cd worker
|
||||||
pnpm install
|
pnpm install
|
||||||
@@ -70,18 +65,12 @@ 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
|
enable email route and config email forward catch-all to the worker
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Frontend - Cloudflare pages
|
### Frontend - Cloudflare pages
|
||||||
|
|
||||||
The first deployment will prompt you to create a project. Please fill in `production` for the `production` branch.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
pnpm install
|
pnpm install
|
||||||
@@ -93,16 +82,3 @@ pnpm run deploy
|
|||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Configure sending emails
|
|
||||||
|
|
||||||
Find the `SPF` record of `TXT` in the domain name `DNS` record, and add `include:relay.mailchannels.net`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
v=spf1 include:_spf.mx.cloudflare.net include:relay.mailchannels.net ~all
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a new `_mailchannels` record, the type is `TXT`, the content is `v=mc1 cfid=your worker domain name`
|
|
||||||
|
|
||||||
- The worker domain name here is the domain name of the back-end api. For example, if I deploy it at `https://temp-email-api.awsl.uk/`, fill in `v=mc1 cfid=awsl.uk`
|
|
||||||
- If your domain name is `https://temp-email-api.xxx.workers.dev`, fill in `v=mc1 cfid=xxx.workers.dev`
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
|
||||||
@@ -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,22 +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);
|
|
||||||
|
|||||||
@@ -6,17 +6,14 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build -m prod --emptyOutDir",
|
"build": "vite build -m prod --emptyOutDir",
|
||||||
"build:release": "vite build -m example --emptyOutDir",
|
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"deploy": "npm run build && wrangler pages deploy ./dist --branch production"
|
"deploy": "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",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
"mail-parser-wasm": "^0.1.6",
|
|
||||||
"naive-ui": "^2.38.1",
|
"naive-ui": "^2.38.1",
|
||||||
"postal-mime": "^2.2.1",
|
|
||||||
"vooks": "^0.2.12",
|
"vooks": "^0.2.12",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
@@ -30,8 +27,6 @@
|
|||||||
"unplugin-vue-components": "^0.26.0",
|
"unplugin-vue-components": "^0.26.0",
|
||||||
"vite": "^5.2.6",
|
"vite": "^5.2.6",
|
||||||
"vite-plugin-pwa": "^0.19.7",
|
"vite-plugin-pwa": "^0.19.7",
|
||||||
"vite-plugin-top-level-await": "^1.4.1",
|
|
||||||
"vite-plugin-wasm": "^3.3.0",
|
|
||||||
"workbox-window": "^7.0.0"
|
"workbox-window": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
184
frontend/pnpm-lock.yaml
generated
184
frontend/pnpm-lock.yaml
generated
@@ -14,15 +14,9 @@ dependencies:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^1.6.8
|
specifier: ^1.6.8
|
||||||
version: 1.6.8
|
version: 1.6.8
|
||||||
mail-parser-wasm:
|
|
||||||
specifier: ^0.1.6
|
|
||||||
version: 0.1.6
|
|
||||||
naive-ui:
|
naive-ui:
|
||||||
specifier: ^2.38.1
|
specifier: ^2.38.1
|
||||||
version: 2.38.1(vue@3.4.21)
|
version: 2.38.1(vue@3.4.21)
|
||||||
postal-mime:
|
|
||||||
specifier: ^2.2.1
|
|
||||||
version: 2.2.1
|
|
||||||
vooks:
|
vooks:
|
||||||
specifier: ^0.2.12
|
specifier: ^0.2.12
|
||||||
version: 0.2.12(vue@3.4.21)
|
version: 0.2.12(vue@3.4.21)
|
||||||
@@ -58,12 +52,6 @@ devDependencies:
|
|||||||
vite-plugin-pwa:
|
vite-plugin-pwa:
|
||||||
specifier: ^0.19.7
|
specifier: ^0.19.7
|
||||||
version: 0.19.7(vite@5.2.6)(workbox-build@7.0.0)(workbox-window@7.0.0)
|
version: 0.19.7(vite@5.2.6)(workbox-build@7.0.0)(workbox-window@7.0.0)
|
||||||
vite-plugin-top-level-await:
|
|
||||||
specifier: ^1.4.1
|
|
||||||
version: 1.4.1(rollup@2.79.1)(vite@5.2.6)
|
|
||||||
vite-plugin-wasm:
|
|
||||||
specifier: ^3.3.0
|
|
||||||
version: 3.3.0(vite@5.2.6)
|
|
||||||
workbox-window:
|
workbox-window:
|
||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.0.0
|
version: 7.0.0
|
||||||
@@ -1605,18 +1593,6 @@ packages:
|
|||||||
rollup: 2.79.1
|
rollup: 2.79.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@rollup/plugin-virtual@3.0.2(rollup@2.79.1):
|
|
||||||
resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
rollup:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
rollup: 2.79.1
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@rollup/pluginutils@3.1.0(rollup@2.79.1):
|
/@rollup/pluginutils@3.1.0(rollup@2.79.1):
|
||||||
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
|
resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==}
|
||||||
engines: {node: '>= 8.0.0'}
|
engines: {node: '>= 8.0.0'}
|
||||||
@@ -1765,131 +1741,6 @@ packages:
|
|||||||
string.prototype.matchall: 4.0.11
|
string.prototype.matchall: 4.0.11
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@swc/core-darwin-arm64@1.4.12:
|
|
||||||
resolution: {integrity: sha512-BZUUq91LGJsLI2BQrhYL3yARkcdN4TS3YGNS6aRYUtyeWrGCTKHL90erF2BMU2rEwZLLkOC/U899R4o4oiSHfA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [darwin]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@swc/core-darwin-x64@1.4.12:
|
|
||||||
resolution: {integrity: sha512-Wkk8rq1RwCOgg5ybTlfVtOYXLZATZ+QjgiBNM7pIn03A5/zZicokNTYd8L26/mifly2e74Dz34tlIZBT4aTGDA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [darwin]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@swc/core-linux-arm-gnueabihf@1.4.12:
|
|
||||||
resolution: {integrity: sha512-8jb/SN67oTQ5KSThWlKLchhU6xnlAlnmnLCCOKK1xGtFS6vD+By9uL+qeEY2krV98UCRTf68WSmC0SLZhVoz5A==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [arm]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@swc/core-linux-arm64-gnu@1.4.12:
|
|
||||||
resolution: {integrity: sha512-DhW47DQEZKCdSq92v5F03rqdpjRXdDMqxfu4uAlZ9Uo1wJEGvY23e1SNmhji2sVHsZbBjSvoXoBLk0v00nSG8w==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@swc/core-linux-arm64-musl@1.4.12:
|
|
||||||
resolution: {integrity: sha512-PR57pT3TssnCRvdsaKNsxZy9N8rFg9AKA1U7W+LxbZ/7Z7PHc5PjxF0GgZpE/aLmU6xOn5VyQTlzjoamVkt05g==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@swc/core-linux-x64-gnu@1.4.12:
|
|
||||||
resolution: {integrity: sha512-HLZIWNHWuFIlH+LEmXr1lBiwGQeCshKOGcqbJyz7xpqTh7m2IPAxPWEhr/qmMTMsjluGxeIsLrcsgreTyXtgNA==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@swc/core-linux-x64-musl@1.4.12:
|
|
||||||
resolution: {integrity: sha512-M5fBAtoOcpz2YQAFtNemrPod5BqmzAJc8pYtT3dVTn1MJllhmLHlphU8BQytvoGr1PHgJL8ZJBlBGdt70LQ7Mw==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@swc/core-win32-arm64-msvc@1.4.12:
|
|
||||||
resolution: {integrity: sha512-K8LjjgZ7VQFtM+eXqjfAJ0z+TKVDng3r59QYn7CL6cyxZI2brLU3lNknZcUFSouZD+gsghZI/Zb8tQjVk7aKDQ==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [win32]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@swc/core-win32-ia32-msvc@1.4.12:
|
|
||||||
resolution: {integrity: sha512-hflO5LCxozngoOmiQbDPyvt6ODc5Cu9AwTJP9uH/BSMPdEQ6PCnefuUOJLAKew2q9o+NmDORuJk+vgqQz9Uzpg==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [ia32]
|
|
||||||
os: [win32]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@swc/core-win32-x64-msvc@1.4.12:
|
|
||||||
resolution: {integrity: sha512-3A4qMtddBDbtprV5edTB/SgJn9L+X5TL7RGgS3eWtEgn/NG8gA80X/scjf1v2MMeOsrcxiYhnemI2gXCKuQN2g==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [win32]
|
|
||||||
requiresBuild: true
|
|
||||||
dev: true
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
/@swc/core@1.4.12:
|
|
||||||
resolution: {integrity: sha512-QljRxTaUajSLB9ui93cZ38/lmThwIw/BPxjn+TphrYN6LPU3vu9/ykjgHtlpmaXDDcngL4K5i396E7iwwEUxYg==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
requiresBuild: true
|
|
||||||
peerDependencies:
|
|
||||||
'@swc/helpers': ^0.5.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@swc/helpers':
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
'@swc/counter': 0.1.3
|
|
||||||
'@swc/types': 0.1.6
|
|
||||||
optionalDependencies:
|
|
||||||
'@swc/core-darwin-arm64': 1.4.12
|
|
||||||
'@swc/core-darwin-x64': 1.4.12
|
|
||||||
'@swc/core-linux-arm-gnueabihf': 1.4.12
|
|
||||||
'@swc/core-linux-arm64-gnu': 1.4.12
|
|
||||||
'@swc/core-linux-arm64-musl': 1.4.12
|
|
||||||
'@swc/core-linux-x64-gnu': 1.4.12
|
|
||||||
'@swc/core-linux-x64-musl': 1.4.12
|
|
||||||
'@swc/core-win32-arm64-msvc': 1.4.12
|
|
||||||
'@swc/core-win32-ia32-msvc': 1.4.12
|
|
||||||
'@swc/core-win32-x64-msvc': 1.4.12
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@swc/counter@0.1.3:
|
|
||||||
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@swc/types@0.1.6:
|
|
||||||
resolution: {integrity: sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==}
|
|
||||||
dependencies:
|
|
||||||
'@swc/counter': 0.1.3
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/@types/estree@0.0.39:
|
/@types/estree@0.0.39:
|
||||||
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
|
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -3118,10 +2969,6 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.4.15
|
'@jridgewell/sourcemap-codec': 1.4.15
|
||||||
|
|
||||||
/mail-parser-wasm@0.1.6:
|
|
||||||
resolution: {integrity: sha512-RoPPXqpGcCe4BcnXmxH4Cl5u0AH8y0JUNutksg2xzK0qFGEVE3xipx90JHzUUZ3MuMxo7doQTRktcABTIb3aeg==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/merge-stream@2.0.0:
|
/merge-stream@2.0.0:
|
||||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -3284,10 +3131,6 @@ packages:
|
|||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/postal-mime@2.2.1:
|
|
||||||
resolution: {integrity: sha512-YqGeFmiKXUxv32hOy2t47VX67mYydC47CTCc7+HKd3xlNKPDhivnO/ZovN3iWXxvyyL2TRTxusuuq3etWeCKsw==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/postcss@8.4.38:
|
/postcss@8.4.38:
|
||||||
resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
|
resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
@@ -3899,11 +3742,6 @@ packages:
|
|||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/uuid@9.0.1:
|
|
||||||
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
|
|
||||||
hasBin: true
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/vdirs@0.1.8(vue@3.4.21):
|
/vdirs@0.1.8(vue@3.4.21):
|
||||||
resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==}
|
resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3935,28 +3773,6 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vite-plugin-top-level-await@1.4.1(rollup@2.79.1)(vite@5.2.6):
|
|
||||||
resolution: {integrity: sha512-hogbZ6yT7+AqBaV6lK9JRNvJDn4/IJvHLu6ET06arNfo0t2IsyCaon7el9Xa8OumH+ESuq//SDf8xscZFE0rWw==}
|
|
||||||
peerDependencies:
|
|
||||||
vite: '>=2.8'
|
|
||||||
dependencies:
|
|
||||||
'@rollup/plugin-virtual': 3.0.2(rollup@2.79.1)
|
|
||||||
'@swc/core': 1.4.12
|
|
||||||
uuid: 9.0.1
|
|
||||||
vite: 5.2.6
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@swc/helpers'
|
|
||||||
- rollup
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/vite-plugin-wasm@3.3.0(vite@5.2.6):
|
|
||||||
resolution: {integrity: sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==}
|
|
||||||
peerDependencies:
|
|
||||||
vite: ^2 || ^3 || ^4 || ^5
|
|
||||||
dependencies:
|
|
||||||
vite: 5.2.6
|
|
||||||
dev: true
|
|
||||||
|
|
||||||
/vite@5.2.6:
|
/vite@5.2.6:
|
||||||
resolution: {integrity: sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==}
|
resolution: {integrity: sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
|
|||||||
@@ -75,9 +75,7 @@ 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;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const i18n = createI18n({
|
|||||||
'en': {
|
'en': {
|
||||||
messages: {}
|
messages: {}
|
||||||
},
|
},
|
||||||
'zh': {
|
'zhCN': {
|
||||||
messages: {}
|
messages: {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import Index from '../views/Index.vue'
|
import Index from '../views/Index.vue'
|
||||||
import Settings from '../views/Settings.vue'
|
import Settings from '../views/Settings.vue'
|
||||||
import SendMail from '../views/send/SendMail.vue'
|
|
||||||
import Admin from '../views/Admin.vue'
|
import Admin from '../views/Admin.vue'
|
||||||
import SendBox from '../views/send/SendBox.vue'
|
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(),
|
history: createWebHistory(),
|
||||||
@@ -16,14 +14,6 @@ const router = createRouter({
|
|||||||
path: '/settings',
|
path: '/settings',
|
||||||
component: Settings
|
component: Settings
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/send',
|
|
||||||
component: SendMail
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/sendbox',
|
|
||||||
component: SendBox
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/admin',
|
path: '/admin',
|
||||||
component: Admin
|
component: Admin
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ export const useGlobalState = createGlobalState(
|
|||||||
})
|
})
|
||||||
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: '',
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import PostalMime from 'postal-mime';
|
import PostalMime from 'postal-mime';
|
||||||
import { parse_message } from 'mail-parser-wasm'
|
import { parse_message } from 'mail-parser-wasm'
|
||||||
|
|
||||||
function humanFileSize(size) {
|
|
||||||
const i = size === 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
|
|
||||||
return parseFloat((size / Math.pow(1024, i)).toFixed(2)) + ' ' + ['B', 'KB', 'MB', 'GB', 'TB'][i];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processItem(item) {
|
export async function processItem(item) {
|
||||||
// Try to parse the email using mail-parser-wasm
|
// Try to parse the email using mail-parser-wasm
|
||||||
try {
|
try {
|
||||||
@@ -25,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
|
||||||
}
|
}
|
||||||
}) || [];
|
}) || [];
|
||||||
@@ -57,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
|
||||||
}
|
}
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { User, UserCheck, MailBulk } from '@vicons/fa'
|
|||||||
|
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
import { processItem } from '../utils/email-parser'
|
|
||||||
import SenderAccess from './admin/SenderAccess.vue'
|
|
||||||
|
|
||||||
const { localeCache, adminAuth, showAdminAuth } = useGlobalState()
|
const { localeCache, adminAuth, showAdminAuth } = useGlobalState()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -50,7 +48,6 @@ const { t } = useI18n({
|
|||||||
account: 'Account',
|
account: 'Account',
|
||||||
unknow: 'Unknow',
|
unknow: 'Unknow',
|
||||||
addressQueryTip: 'Leave blank to query all addresses',
|
addressQueryTip: 'Leave blank to query all addresses',
|
||||||
senderAccess: 'Sender Access Control',
|
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
title: '临时邮件 Admin',
|
title: '临时邮件 Admin',
|
||||||
@@ -74,7 +71,6 @@ const { t } = useI18n({
|
|||||||
account: '账号',
|
account: '账号',
|
||||||
unknow: '未知',
|
unknow: '未知',
|
||||||
addressQueryTip: '留空查询所有地址',
|
addressQueryTip: '留空查询所有地址',
|
||||||
senderAccess: '发件权限控制',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -221,14 +217,12 @@ const fetchMailData = async () => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const { results, count } = await api.fetch(
|
const { results, count } = await api.fetch(
|
||||||
`/admin/mails`
|
`/admin/v1/mails`
|
||||||
+ `?address=${mailAddress.value}`
|
+ `?address=${mailAddress.value}`
|
||||||
+ `&limit=${mailPageSize.value}`
|
+ `&limit=${mailPageSize.value}`
|
||||||
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
||||||
);
|
);
|
||||||
mailData.value = await Promise.all(results.map(async (item) => {
|
mailData.value = results;
|
||||||
return await processItem(item);
|
|
||||||
}));
|
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
mailCount.value = count;
|
mailCount.value = count;
|
||||||
}
|
}
|
||||||
@@ -251,13 +245,11 @@ watch([mailUnknowPage, mailUnknowPageSize], async () => {
|
|||||||
const fetchMailUnknowData = async () => {
|
const fetchMailUnknowData = async () => {
|
||||||
try {
|
try {
|
||||||
const { results, count } = await api.fetch(
|
const { results, count } = await api.fetch(
|
||||||
`/admin/mails_unknow`
|
`/admin/v1/mails_unknow`
|
||||||
+ `?limit=${mailPageSize.value}`
|
+ `?limit=${mailPageSize.value}`
|
||||||
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
+ `&offset=${(mailPage.value - 1) * mailPageSize.value}`
|
||||||
);
|
);
|
||||||
mailUnknowData.value = await Promise.all(results.map(async (item) => {
|
mailUnknowData.value = results;
|
||||||
return await processItem(item);
|
|
||||||
}));
|
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
mailUnknowCount.value = count;
|
mailUnknowCount.value = count;
|
||||||
}
|
}
|
||||||
@@ -276,7 +268,9 @@ const fetchMailUnknowData = async () => {
|
|||||||
<div>{{ t('auth') }}</div>
|
<div>{{ t('auth') }}</div>
|
||||||
</template>
|
</template>
|
||||||
<p>{{ t('authTip') }}</p>
|
<p>{{ t('authTip') }}</p>
|
||||||
<n-input v-model:value="adminAuth" type="textarea" :autosize="{ minRows: 3 }" />
|
<n-input v-model:value="adminAuth" type="textarea" :autosize="{
|
||||||
|
minRows: 3
|
||||||
|
}" />
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="authFunc" size="small" tertiary round type="primary">
|
<n-button @click="authFunc" size="small" tertiary round type="primary">
|
||||||
{{ t('auth') }}
|
{{ t('auth') }}
|
||||||
@@ -399,9 +393,6 @@ const fetchMailUnknowData = async () => {
|
|||||||
</n-list-item>
|
</n-list-item>
|
||||||
</n-list>
|
</n-list>
|
||||||
</n-tab-pane>
|
</n-tab-pane>
|
||||||
<n-tab-pane name="senderAccess" :tab="t('senderAccess')">
|
|
||||||
<SenderAccess />
|
|
||||||
</n-tab-pane>
|
|
||||||
</n-tabs>
|
</n-tabs>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { ref, h, computed, onMounted } from 'vue'
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useIsMobile } from '../utils/composables'
|
import { useIsMobile } from '../utils/composables'
|
||||||
import { DarkModeFilled, LightModeFilled, MenuFilled, AdminPanelSettingsFilled, SendFilled } from '@vicons/material'
|
import { DarkModeFilled, LightModeFilled, MenuFilled, AdminPanelSettingsFilled } from '@vicons/material'
|
||||||
import { GithubAlt, Language, User, Home, Copy } from '@vicons/fa'
|
import { GithubAlt, Language, User, Home, Copy } from '@vicons/fa'
|
||||||
|
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
@@ -73,13 +73,10 @@ const { t } = useI18n({
|
|||||||
home: 'Home',
|
home: 'Home',
|
||||||
menu: 'Menu',
|
menu: 'Menu',
|
||||||
user: 'User',
|
user: 'User',
|
||||||
sendbox: 'Send Box',
|
|
||||||
sendMail: 'Send Mail',
|
|
||||||
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
|
pleaseGetNewEmail: 'Please login or click "Get New Email" button to get a new email address',
|
||||||
getNewEmail: 'Get New Email',
|
getNewEmail: 'Get New Email',
|
||||||
getNewEmailTip1: 'Please input the email you want to use. only allow ., a-z, A-Z and 0-9',
|
getNewEmailTip1: 'Please input the email you want to use.',
|
||||||
getNewEmailTip2: 'Levaing it blank will generate a random email address.',
|
getNewEmailTip2: 'Levaing it blank will generate a random email address.',
|
||||||
getNewEmailTip3: 'You can choose a domain from the dropdown list.',
|
|
||||||
yourAddress: 'Your email address is',
|
yourAddress: 'Your email address is',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
passwordTip: 'Please copy the password and you can use it to login to your email account.', cancel: 'Cancel',
|
passwordTip: 'Please copy the password and you can use it to login to your email account.', cancel: 'Cancel',
|
||||||
@@ -88,8 +85,6 @@ const { t } = useI18n({
|
|||||||
copied: 'Copied',
|
copied: 'Copied',
|
||||||
showPassword: 'Show Password',
|
showPassword: 'Show Password',
|
||||||
fetchAddressError: 'Fetch address error, maybe your jwt is invalid or network error.',
|
fetchAddressError: 'Fetch address error, maybe your jwt is invalid or network error.',
|
||||||
mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.',
|
|
||||||
generateName: 'Generate Fake Name',
|
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
title: 'Cloudflare 临时邮件',
|
title: 'Cloudflare 临时邮件',
|
||||||
@@ -106,13 +101,10 @@ const { t } = useI18n({
|
|||||||
home: '主页',
|
home: '主页',
|
||||||
menu: '菜单',
|
menu: '菜单',
|
||||||
user: '用户',
|
user: '用户',
|
||||||
sendbox: '发件箱',
|
|
||||||
sendMail: '发送邮件',
|
|
||||||
pleaseGetNewEmail: '请"登录"或点击 "获取新邮箱" 按钮来获取一个新的邮箱地址',
|
pleaseGetNewEmail: '请"登录"或点击 "获取新邮箱" 按钮来获取一个新的邮箱地址',
|
||||||
getNewEmail: '获取新邮箱',
|
getNewEmail: '获取新邮箱',
|
||||||
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 ., a-z, A-Z, 0-9',
|
getNewEmailTip1: '请输入你想要使用的邮箱地址。',
|
||||||
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
||||||
getNewEmailTip3: '你可以从下拉列表中选择一个域名。',
|
|
||||||
yourAddress: '你的邮箱地址是',
|
yourAddress: '你的邮箱地址是',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
||||||
@@ -122,8 +114,6 @@ const { t } = useI18n({
|
|||||||
copied: '已复制',
|
copied: '已复制',
|
||||||
showPassword: '查看密码',
|
showPassword: '查看密码',
|
||||||
fetchAddressError: '获取地址失败, 请检查你的 jwt 是否有效 或 网络是否正常。',
|
fetchAddressError: '获取地址失败, 请检查你的 jwt 是否有效 或 网络是否正常。',
|
||||||
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
|
|
||||||
generateName: '生成随机名字',
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -180,19 +170,6 @@ const menuOptions = computed(() => [
|
|||||||
show: showUserMenu.value,
|
show: showUserMenu.value,
|
||||||
key: "user",
|
key: "user",
|
||||||
children: [
|
children: [
|
||||||
{
|
|
||||||
label: () => h(
|
|
||||||
NButton,
|
|
||||||
{
|
|
||||||
tertiary: true,
|
|
||||||
ghost: true,
|
|
||||||
size: "small",
|
|
||||||
onClick: () => router.push('/sendbox')
|
|
||||||
},
|
|
||||||
{ default: () => t('sendbox') }
|
|
||||||
),
|
|
||||||
key: "sendbox"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: () => h(
|
label: () => h(
|
||||||
NButton,
|
NButton,
|
||||||
@@ -327,23 +304,6 @@ const copy = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generateNameLoading = ref(false);
|
|
||||||
const generateName = async () => {
|
|
||||||
try {
|
|
||||||
generateNameLoading.value = true;
|
|
||||||
const { faker } = await import('https://esm.sh/@faker-js/faker');
|
|
||||||
emailName.value = faker.person
|
|
||||||
.fullName()
|
|
||||||
.replace(/\s+/g, '.')
|
|
||||||
.replace(/[^a-zA-Z0-9.]/g, '')
|
|
||||||
.toLowerCase();
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
} finally {
|
|
||||||
generateNameLoading.value = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const newEmail = async () => {
|
const newEmail = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.fetch(
|
const res = await api.fetch(
|
||||||
@@ -375,7 +335,6 @@ const deleteAccount = async () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await api.getOpenSettings(message);
|
await api.getOpenSettings(message);
|
||||||
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0].value : "";
|
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0].value : "";
|
||||||
await api.getSettings();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -392,28 +351,14 @@ onMounted(async () => {
|
|||||||
<n-card v-if="!settings.fetched">
|
<n-card v-if="!settings.fetched">
|
||||||
<n-skeleton style="height: 50vh" />
|
<n-skeleton style="height: 50vh" />
|
||||||
</n-card>
|
</n-card>
|
||||||
<div v-else-if="settings.address">
|
<n-alert v-else-if="settings.address" type="info" show-icon>
|
||||||
<n-alert v-if="settings.has_v1_mails" type="warning" show-icon closable>
|
<span>
|
||||||
<span>
|
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small"
|
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary round type="primary">
|
||||||
href="https://mail-v1.awsl.uk">
|
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||||
<b>{{ t('mailV1Alert') }} </b>
|
</n-button>
|
||||||
</n-button>
|
</span>
|
||||||
</span>
|
</n-alert>
|
||||||
</n-alert>
|
|
||||||
<n-alert type="info" show-icon>
|
|
||||||
<span>
|
|
||||||
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
|
||||||
<n-button style="margin-left: 10px" @click="router.push('/send')" size="small" tertiary round
|
|
||||||
type="primary">
|
|
||||||
<n-icon :component="SendFilled" /> {{ t('sendMail') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary round type="primary">
|
|
||||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
|
||||||
</n-button>
|
|
||||||
</span>
|
|
||||||
</n-alert>
|
|
||||||
</div>
|
|
||||||
<n-card v-else>
|
<n-card v-else>
|
||||||
<n-result status="info" :description="t('pleaseGetNewEmail')">
|
<n-result status="info" :description="t('pleaseGetNewEmail')">
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -434,25 +379,18 @@ onMounted(async () => {
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div>{{ t('getNewEmail') }}</div>
|
<div>{{ t('getNewEmail') }}</div>
|
||||||
</template>
|
</template>
|
||||||
<n-spin :show="generateNameLoading">
|
<span>
|
||||||
<span>
|
<p>{{ t("getNewEmailTip1") }}</p>
|
||||||
<p>{{ t("getNewEmailTip1") }}</p>
|
<p>{{ t("getNewEmailTip2") }}</p>
|
||||||
<p>{{ t("getNewEmailTip2") }}</p>
|
</span>
|
||||||
<p>{{ t("getNewEmailTip3") }}</p>
|
<n-input-group>
|
||||||
</span>
|
<n-input-group-label v-if="openSettings.prefix">
|
||||||
<n-button @click="generateName" style="margin-bottom: 10px;">
|
{{ openSettings.prefix }}
|
||||||
{{ t('generateName') }}
|
</n-input-group-label>
|
||||||
</n-button>
|
<n-input v-model:value="emailName" />
|
||||||
<n-input-group>
|
<n-input-group-label>@</n-input-group-label>
|
||||||
<n-input-group-label v-if="openSettings.prefix">
|
<n-select v-model:value="emailDomain" :consistent-menu-width="false" :options="openSettings.domains" />
|
||||||
{{ openSettings.prefix }}
|
</n-input-group>
|
||||||
</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-spin>
|
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="showNewEmail = false">
|
<n-button @click="showNewEmail = false">
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
|
|||||||
@@ -1,11 +1,304 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import MailBox from './MailBox.vue';
|
import { watch, onMounted, ref } from "vue";
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
const { settings } = useGlobalState()
|
import { api } from '../api'
|
||||||
|
import { CloudDownloadRound } from '@vicons/material'
|
||||||
|
import { useIsMobile } from '../utils/composables'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
|
||||||
|
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({
|
||||||
|
locale: 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
autoRefresh: 'Auto Refresh',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
attachments: 'Show Attachments',
|
||||||
|
pleaseSelectMail: "Please select a mail to view."
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
autoRefresh: '自动刷新',
|
||||||
|
refresh: '刷新',
|
||||||
|
attachments: '查看附件',
|
||||||
|
pleaseSelectMail: "请选择一封邮件查看。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupAutoRefresh = async (autoRefresh) => {
|
||||||
|
if (autoRefresh) {
|
||||||
|
timer.value = setInterval(async () => {
|
||||||
|
await refresh();
|
||||||
|
}, 30000)
|
||||||
|
} else {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(autoRefresh, async (autoRefresh, old) => {
|
||||||
|
setupAutoRefresh(autoRefresh)
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
|
||||||
|
if (page !== oldPage || pageSize !== oldPageSize) {
|
||||||
|
await refresh();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
if (typeof settings.value.address != 'string' || settings.value.address.trim() === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { results, count: totalCount } = await api.fetch(
|
||||||
|
`/api/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 clickRow = async (row) => {
|
||||||
|
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>
|
||||||
<MailBox v-if="settings.address" />
|
<n-layout v-if="settings.address">
|
||||||
|
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25" :default-size="0.25">
|
||||||
|
<template #1>
|
||||||
|
<div>
|
||||||
|
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
||||||
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
||||||
|
</div>
|
||||||
|
<n-switch v-model:value="autoRefresh" size="small">
|
||||||
|
<template #checked>
|
||||||
|
{{ t('autoRefresh') }}
|
||||||
|
</template>
|
||||||
|
<template #unchecked>
|
||||||
|
{{ t('autoRefresh') }}
|
||||||
|
</template></n-switch>
|
||||||
|
<n-button class="center" @click="refresh" size="small" type="primary">
|
||||||
|
{{ t('refresh') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
<div style="overflow: scroll; height: 80vh;">
|
||||||
|
<n-list hoverable clickable>
|
||||||
|
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||||
|
:class="mailItemClass(row)">
|
||||||
|
<n-thing class="center" :title="row.subject" style="overflow: scroll">
|
||||||
|
<template #description>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ row.id }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ row.created_at }}
|
||||||
|
</n-tag>
|
||||||
|
<div style="word-break: break-all; font-size: small;">
|
||||||
|
FROM: {{ row.source }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n-thing>
|
||||||
|
</n-list-item>
|
||||||
|
</n-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #2>
|
||||||
|
<n-card v-if="curMail" class="mail-item" :title="curMail.subject" style="overflow: scroll;">
|
||||||
|
<n-space>
|
||||||
|
<n-tag type="info">
|
||||||
|
ID: {{ curMail.id }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
{{ curMail.created_at }}
|
||||||
|
</n-tag>
|
||||||
|
<n-tag type="info">
|
||||||
|
FROM: {{ curMail.source }}
|
||||||
|
</n-tag>
|
||||||
|
<n-button v-if="curMail.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>
|
||||||
|
|||||||
@@ -1,294 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { watch, onMounted, ref } from "vue";
|
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useGlobalState } from '../store'
|
|
||||||
import { api } from '../api'
|
|
||||||
import { CloudDownloadRound } from '@vicons/material'
|
|
||||||
import { useIsMobile } from '../utils/composables'
|
|
||||||
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
|
||||||
|
|
||||||
const message = useMessage()
|
|
||||||
const isMobile = useIsMobile()
|
|
||||||
|
|
||||||
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({
|
|
||||||
locale: 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
autoRefresh: 'Auto Refresh',
|
|
||||||
refresh: 'Refresh',
|
|
||||||
attachments: 'Show Attachments',
|
|
||||||
downloadMail: 'Download Mail',
|
|
||||||
pleaseSelectMail: "Please select a mail to view."
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
autoRefresh: '自动刷新',
|
|
||||||
refresh: '刷新',
|
|
||||||
downloadMail: '下载邮件',
|
|
||||||
attachments: '查看附件',
|
|
||||||
pleaseSelectMail: "请选择一封邮件查看。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const setupAutoRefresh = async (autoRefresh) => {
|
|
||||||
if (autoRefresh) {
|
|
||||||
timer.value = setInterval(async () => {
|
|
||||||
await refresh();
|
|
||||||
}, 30000)
|
|
||||||
} else {
|
|
||||||
clearInterval(timer.value)
|
|
||||||
timer.value = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(autoRefresh, async (autoRefresh, old) => {
|
|
||||||
setupAutoRefresh(autoRefresh)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
|
|
||||||
if (page !== oldPage || pageSize !== oldPageSize) {
|
|
||||||
await refresh();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
try {
|
|
||||||
const { results, count: totalCount } = await api.fetch(
|
|
||||||
`/api/mails`
|
|
||||||
+ `?limit=${pageSize.value}`
|
|
||||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
|
||||||
);
|
|
||||||
data.value = await Promise.all(results.map(async (item) => {
|
|
||||||
return await processItem(item);
|
|
||||||
}));
|
|
||||||
if (totalCount > 0) {
|
|
||||||
count.value = totalCount;
|
|
||||||
}
|
|
||||||
if (!isMobile.value && !curMail.value && data.value.length > 0) {
|
|
||||||
curMail.value = data.value[0];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clickRow = async (row) => {
|
|
||||||
curMail.value = row;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAttachments = (attachments) => {
|
|
||||||
curAttachments.value = attachments;
|
|
||||||
showAttachments.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mailItemClass = (row) => {
|
|
||||||
return curMail.value && row.id == curMail.value.id ? (themeSwitch.value ? 'overlay overlay-dark-backgroud' : 'overlay overlay-light-backgroud') : '';
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await refresh();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-layout v-if="settings.address">
|
|
||||||
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25" :default-size="0.25">
|
|
||||||
<template #1>
|
|
||||||
<div>
|
|
||||||
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
|
||||||
</div>
|
|
||||||
<n-switch v-model:value="autoRefresh" size="small">
|
|
||||||
<template #checked>
|
|
||||||
{{ t('autoRefresh') }}
|
|
||||||
</template>
|
|
||||||
<template #unchecked>
|
|
||||||
{{ t('autoRefresh') }}
|
|
||||||
</template></n-switch>
|
|
||||||
<n-button class="center" @click="refresh" size="small" type="primary">
|
|
||||||
{{ t('refresh') }}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
<div style="overflow: 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 class="center" :title="row.subject">
|
|
||||||
<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: 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-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
|
||||||
@click="getAttachments(curMail.attachments)">
|
|
||||||
{{ t('attachments') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
|
||||||
:href="getDownloadEmlUrl(curMail.raw)">
|
|
||||||
<n-icon :component="CloudDownloadRound" />
|
|
||||||
{{ t('downloadMail') }}
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
<div v-html="curMail.message" style="margin-top: 10px;"></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: 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 class="center" :title="row.subject">
|
|
||||||
<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: 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-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
|
||||||
@click="getAttachments(curMail.attachments)">
|
|
||||||
{{ t('attachments') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
|
||||||
:href="getDownloadEmlUrl(curMail)">
|
|
||||||
<n-icon :component="CloudDownloadRound" />
|
|
||||||
{{ t('downloadMail') }}
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
<div v-html="curMail.message" style="margin-top: 10px;"></div>
|
|
||||||
</n-card>
|
|
||||||
</n-drawer-content>
|
|
||||||
</n-drawer>
|
|
||||||
</div>
|
|
||||||
</n-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>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.left {
|
|
||||||
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,6 +2,7 @@
|
|||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
import Header from './Header.vue'
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
import { api } from '../api'
|
import { api } from '../api'
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ const { t } = useI18n({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getSettings = async () => {
|
const getSettings = async () => {
|
||||||
|
await api.getSettings()
|
||||||
sourcePrefix.value = settings.value.auto_reply.source_prefix || ""
|
sourcePrefix.value = settings.value.auto_reply.source_prefix || ""
|
||||||
enableAutoReply.value = settings.value.auto_reply.enabled || false
|
enableAutoReply.value = settings.value.auto_reply.enabled || false
|
||||||
name.value = settings.value.auto_reply.name || ""
|
name.value = settings.value.auto_reply.name || ""
|
||||||
|
|||||||
@@ -1,190 +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 } = useGlobalState()
|
|
||||||
const message = useMessage()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: localeCache.value || 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
address: 'Address',
|
|
||||||
success: 'Success',
|
|
||||||
enable: 'Enable',
|
|
||||||
disable: 'Disable',
|
|
||||||
modify: 'Modify',
|
|
||||||
created_at: 'Created At',
|
|
||||||
action: 'Action',
|
|
||||||
itemCount: 'itemCount',
|
|
||||||
modalTip: 'Please input the sender balance',
|
|
||||||
balance: 'Balance',
|
|
||||||
refresh: 'Refresh',
|
|
||||||
ok: 'OK'
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
address: '地址',
|
|
||||||
success: '成功',
|
|
||||||
enable: '启用',
|
|
||||||
disable: '禁用',
|
|
||||||
modify: '修改',
|
|
||||||
created_at: '创建时间',
|
|
||||||
action: '操作',
|
|
||||||
itemCount: '总数',
|
|
||||||
modalTip: '请输入发件额度',
|
|
||||||
balance: '余额',
|
|
||||||
refresh: '刷新',
|
|
||||||
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 updateData = async () => {
|
|
||||||
try {
|
|
||||||
await api.fetch(`/admin/address_sender`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
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}`
|
|
||||||
);
|
|
||||||
data.value = results;
|
|
||||||
if (addressCount > 0) {
|
|
||||||
count.value = addressCount;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: "ID",
|
|
||||||
key: "id"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('address'),
|
|
||||||
key: "address"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('created_at'),
|
|
||||||
key: "created_at"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('balance'),
|
|
||||||
key: "balance"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Enabled",
|
|
||||||
key: "enabled",
|
|
||||||
render(row) {
|
|
||||||
return h('div', [
|
|
||||||
h('span', row.enabled ? t('enable') : t('disable'))
|
|
||||||
])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: t('action'),
|
|
||||||
key: 'actions',
|
|
||||||
render(row) {
|
|
||||||
return h('div', [
|
|
||||||
h(NButton,
|
|
||||||
{
|
|
||||||
type: 'success',
|
|
||||||
ghost: true,
|
|
||||||
onClick: () => {
|
|
||||||
showModal.value = true;
|
|
||||||
curRow.value = row;
|
|
||||||
senderEnabled.value = row.enabled ? true : false;
|
|
||||||
senderBalance.value = row.balance;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ default: () => t('modify') }
|
|
||||||
)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
watch([page, pageSize], async () => {
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await fetchData()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<n-modal v-model:show="showModal" preset="dialog">
|
|
||||||
<p>{{ curRow.address }}</p>
|
|
||||||
<p>{{ t('modalTip') }}</p>
|
|
||||||
<n-form-item :show-label="false">
|
|
||||||
<n-checkbox v-model:checked="senderEnabled">
|
|
||||||
{{ t('enable') }}
|
|
||||||
</n-checkbox>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :show-label="false">
|
|
||||||
<n-input-number v-model:value="senderBalance" :min="0" :max="1000" />
|
|
||||||
</n-form-item>
|
|
||||||
<template #action>
|
|
||||||
<n-button @click="updateData()" size="small" tertiary round type="primary">
|
|
||||||
{{ t('ok') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
<div style="display: inline-block;">
|
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" :page-sizes="[20, 50, 100]"
|
|
||||||
show-size-picker>
|
|
||||||
<template #prefix="{ itemCount }">
|
|
||||||
{{ t('itemCount') }}: {{ itemCount }}
|
|
||||||
</template>
|
|
||||||
<template #suffix>
|
|
||||||
<n-button @click="fetchData" type="primary" size="small" ghost>
|
|
||||||
{{ t('refresh') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-pagination>
|
|
||||||
</div>
|
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.n-pagination {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -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">
|
|
||||||
<pre>{{ curRow.raw }}</pre>
|
|
||||||
</n-modal>
|
|
||||||
<div style="display: inline-block;">
|
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
|
||||||
:page-sizes="[20, 50, 100]" show-size-picker>
|
|
||||||
<template #prefix="{ itemCount }">
|
|
||||||
{{ t('itemCount') }}: {{ itemCount }}
|
|
||||||
</template>
|
|
||||||
<template #suffix>
|
|
||||||
<n-button @click="fetchData" type="primary" size="small" ghost>
|
|
||||||
{{ t('refresh') }}
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-pagination>
|
|
||||||
</div>
|
|
||||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.n-pagination {
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useStorage } from '@vueuse/core'
|
|
||||||
|
|
||||||
import { useGlobalState } from '../../store'
|
|
||||||
import { api } from '../../api'
|
|
||||||
import router from '../../router'
|
|
||||||
|
|
||||||
const message = useMessage()
|
|
||||||
const isPreview = ref(false)
|
|
||||||
|
|
||||||
const mailModel = useStorage('mailModelCache', {
|
|
||||||
fromName: "",
|
|
||||||
toName: "",
|
|
||||||
toMail: "",
|
|
||||||
subject: "",
|
|
||||||
isHtml: false,
|
|
||||||
content: "",
|
|
||||||
})
|
|
||||||
|
|
||||||
const { settings } = useGlobalState()
|
|
||||||
|
|
||||||
const { t } = useI18n({
|
|
||||||
locale: 'zh',
|
|
||||||
messages: {
|
|
||||||
en: {
|
|
||||||
successSend: 'Please check your sendbox. If failed, please check your balance or try again later.',
|
|
||||||
fromName: 'Your Name and Address, leave Name blank to use email address',
|
|
||||||
toName: 'Recipient Name and Address, leave Name blank to use email address',
|
|
||||||
subject: 'Subject',
|
|
||||||
options: 'Options',
|
|
||||||
isHtml: 'Enable HTML',
|
|
||||||
edit: 'Edit',
|
|
||||||
preview: 'Preview',
|
|
||||||
content: 'Content',
|
|
||||||
send: 'Send',
|
|
||||||
requestAccess: 'Request Access',
|
|
||||||
requestAccessTip: 'You need to request access to send mail',
|
|
||||||
send_balance: 'Send Mail Balance Left',
|
|
||||||
},
|
|
||||||
zh: {
|
|
||||||
successSend: '请查看您的发件箱, 如果失败, 请检查您的余额或稍后重试。',
|
|
||||||
fromName: '你的名称和地址,名称不填写则使用邮箱地址',
|
|
||||||
toName: '收件人名称和地址,名称不填写则使用邮箱地址',
|
|
||||||
subject: '主题',
|
|
||||||
options: '选项',
|
|
||||||
isHtml: '启用HTML',
|
|
||||||
edit: '编辑',
|
|
||||||
preview: '预览',
|
|
||||||
content: '内容',
|
|
||||||
send: '发送',
|
|
||||||
requestAccess: '申请权限',
|
|
||||||
requestAccessTip: '您需要申请权限才能发送邮件',
|
|
||||||
send_balance: '剩余发送邮件额度',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const send = async () => {
|
|
||||||
try {
|
|
||||||
await api.fetch(`/api/send_mail`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body:
|
|
||||||
JSON.stringify({
|
|
||||||
from_name: mailModel.value.fromName,
|
|
||||||
to_name: mailModel.value.toName,
|
|
||||||
to_mail: mailModel.value.toMail,
|
|
||||||
subject: mailModel.value.subject,
|
|
||||||
is_html: mailModel.value.isHtml,
|
|
||||||
content: mailModel.value.content,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
mailModel.value = {
|
|
||||||
fromName: "",
|
|
||||||
toName: "",
|
|
||||||
toMail: "",
|
|
||||||
subject: "",
|
|
||||||
isHtml: false,
|
|
||||||
content: "",
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
} finally {
|
|
||||||
message.success(t("successSend"));
|
|
||||||
router.push('/');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestAccess = async () => {
|
|
||||||
try {
|
|
||||||
await api.fetch(`/api/requset_send_mail_access`,
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({})
|
|
||||||
}
|
|
||||||
)
|
|
||||||
message.success(t("success"))
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="center" v-if="settings.address">
|
|
||||||
<n-card>
|
|
||||||
<div v-if="!settings.send_balance || settings.send_balance <= 0">
|
|
||||||
<n-alert type="warning" show-icon>
|
|
||||||
{{ t('requestAccessTip') }}
|
|
||||||
<n-button type="primary" ghost @click="requestAccess">{{ t('requestAccess') }}</n-button>
|
|
||||||
</n-alert>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<n-alert type="info" show-icon>
|
|
||||||
{{ t('send_balance') }}: {{ settings.send_balance }}
|
|
||||||
</n-alert>
|
|
||||||
<div class="right">
|
|
||||||
<n-button type="primary" @click="send">{{ t('send') }}</n-button>
|
|
||||||
</div>
|
|
||||||
<div class="left">
|
|
||||||
<n-form :model="mailModel">
|
|
||||||
<n-form-item :label="t('fromName')" label-placement="top">
|
|
||||||
<n-input-group>
|
|
||||||
<n-input v-model:value="mailModel.fromName" />
|
|
||||||
<n-input :value="settings.address" disabled />
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="t('toName')" label-placement="top">
|
|
||||||
<n-input-group>
|
|
||||||
<n-input v-model:value="mailModel.toName" />
|
|
||||||
<n-input v-model:value="mailModel.toMail" />
|
|
||||||
</n-input-group>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="t('subject')" label-placement="top">
|
|
||||||
<n-input v-model:value="mailModel.subject" />
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="t('options')" label-placement="top">
|
|
||||||
<n-checkbox v-model:checked="mailModel.isHtml">
|
|
||||||
{{ t('isHtml') }}
|
|
||||||
</n-checkbox>
|
|
||||||
<n-button v-if="mailModel.isHtml" @click="isPreview = !isPreview">
|
|
||||||
{{ isPreview ? t('edit') : t('preview') }}
|
|
||||||
</n-button>
|
|
||||||
</n-form-item>
|
|
||||||
<n-form-item :label="t('content')" label-placement="top">
|
|
||||||
<div v-if="isPreview" v-html="mailModel.content" />
|
|
||||||
<n-input v-else type="textarea" v-model:value="mailModel.content" :autosize="{
|
|
||||||
minRows: 3
|
|
||||||
}" />
|
|
||||||
</n-form-item>
|
|
||||||
</n-form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</n-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.n-card {
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.n-button {
|
|
||||||
text-align: left;
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
display: flex;
|
|
||||||
text-align: center;
|
|
||||||
place-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.left {
|
|
||||||
text-align: left;
|
|
||||||
place-items: left;
|
|
||||||
justify-content: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
|
||||||
text-align: right;
|
|
||||||
place-items: right;
|
|
||||||
justify-content: right;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -7,18 +7,14 @@ 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',
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
wasm(),
|
|
||||||
topLevelAwait(),
|
|
||||||
splitVendorChunkPlugin(),
|
splitVendorChunkPlugin(),
|
||||||
AutoImport({
|
AutoImport({
|
||||||
imports: [
|
imports: [
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "wrangler dev",
|
"dev": "wrangler dev",
|
||||||
"deploy": "wrangler deploy",
|
"deploy": "wrangler deploy",
|
||||||
"start": "wrangler dev",
|
"start": "wrangler dev"
|
||||||
"build": "wrangler deploy src/worker.js --dry-run --outdir dist --minify"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"wrangler": "^3.48.0"
|
"wrangler": "^3.48.0"
|
||||||
|
|||||||
@@ -138,53 +138,9 @@ api.get('/admin/mails_unknow', async (c) => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
api.get('/admin/address_sender', async (c) => {
|
|
||||||
const { limit, offset } = c.req.query();
|
|
||||||
if (!limit || limit < 0 || limit > 100) {
|
|
||||||
return c.text("Invalid limit", 400)
|
|
||||||
}
|
|
||||||
if (!offset || offset < 0) {
|
|
||||||
return c.text("Invalid offset", 400)
|
|
||||||
}
|
|
||||||
const { results } = await c.env.DB.prepare(
|
|
||||||
`SELECT * FROM address_sender order by id desc limit ? offset ? `
|
|
||||||
).bind(limit, offset).all();
|
|
||||||
let count = 0;
|
|
||||||
if (offset == 0) {
|
|
||||||
const { count: addressCount } = await c.env.DB.prepare(
|
|
||||||
`SELECT count(*) as count FROM address_sender`
|
|
||||||
).first();
|
|
||||||
count = addressCount;
|
|
||||||
}
|
|
||||||
return c.json({
|
|
||||||
results: results,
|
|
||||||
count: count
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
api.post('/admin/address_sender', async (c) => {
|
|
||||||
let { address_id, balance, enabled } = await c.req.json();
|
|
||||||
if (!address_id) {
|
|
||||||
return c.text("Invalid address_id", 400)
|
|
||||||
}
|
|
||||||
enabled = enabled ? 1 : 0;
|
|
||||||
const { success } = await c.env.DB.prepare(
|
|
||||||
`UPDATE address_sender SET enabled = ?, balance = ? WHERE id = ? `
|
|
||||||
).bind(enabled, balance, address_id).run();
|
|
||||||
if (!success) {
|
|
||||||
return c.text("Failed to update address sender", 500)
|
|
||||||
}
|
|
||||||
return c.json({
|
|
||||||
success: success
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
api.get('/admin/statistics', async (c) => {
|
api.get('/admin/statistics', async (c) => {
|
||||||
const { count: mailCountV1 } = await c.env.DB.prepare(`
|
|
||||||
SELECT count(*) as count FROM mails`
|
|
||||||
).first();
|
|
||||||
const { count: mailCount } = await c.env.DB.prepare(`
|
const { count: mailCount } = await c.env.DB.prepare(`
|
||||||
SELECT count(*) as count FROM raw_mails`
|
SELECT count(*) as count FROM mails`
|
||||||
).first();
|
).first();
|
||||||
const { count: addressCount } = await c.env.DB.prepare(`
|
const { count: addressCount } = await c.env.DB.prepare(`
|
||||||
SELECT count(*) as count FROM address`
|
SELECT count(*) as count FROM address`
|
||||||
@@ -193,7 +149,7 @@ api.get('/admin/statistics', async (c) => {
|
|||||||
SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')`
|
SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')`
|
||||||
).first();
|
).first();
|
||||||
return c.json({
|
return c.json({
|
||||||
mailCount: (mailCountV1 || 0) + (mailCount || 0),
|
mailCount: mailCount,
|
||||||
userCount: addressCount,
|
userCount: addressCount,
|
||||||
activeUserCount7days: activeUserCount7days
|
activeUserCount7days: activeUserCount7days
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,8 +8,18 @@ async function email(message, env, ctx) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!env.PREFIX || (message.to && message.to.startsWith(env.PREFIX))) {
|
if (!env.PREFIX || (message.to && message.to.startsWith(env.PREFIX))) {
|
||||||
const rawEmail = await new Response(message.raw).text();
|
const reader = message.raw.getReader();
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
let rawEmail = "";
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
rawEmail += decoder.decode(value);
|
||||||
|
}
|
||||||
const message_id = message.headers.get("Message-ID");
|
const message_id = message.headers.get("Message-ID");
|
||||||
|
|
||||||
// save email
|
// save email
|
||||||
const { success } = await env.DB.prepare(
|
const { success } = await env.DB.prepare(
|
||||||
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
|
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
|
||||||
|
|||||||
@@ -84,15 +84,10 @@ api.get('/api/settings', async (c) => {
|
|||||||
const { count: mailCountV1 } = await c.env.DB.prepare(
|
const { count: mailCountV1 } = await c.env.DB.prepare(
|
||||||
`SELECT count(*) as count FROM mails where address = ?`
|
`SELECT count(*) as count FROM mails where address = ?`
|
||||||
).bind(address).first();
|
).bind(address).first();
|
||||||
const balance = await c.env.DB.prepare(
|
|
||||||
`SELECT balance FROM address_sender
|
|
||||||
where address = ? and enabled = 1`
|
|
||||||
).bind(address).first("balance");
|
|
||||||
return c.json({
|
return c.json({
|
||||||
auto_reply: auto_reply,
|
auto_reply: auto_reply,
|
||||||
address: address,
|
address: address,
|
||||||
has_v1_mails: mailCountV1 && mailCountV1 > 0,
|
has_v1_mails: mailCountV1 > 0
|
||||||
send_balance: balance || 0,
|
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -136,20 +131,11 @@ api.get('/open_api/settings', async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
api.get('/api/new_address', async (c) => {
|
api.get('/api/new_address', async (c) => {
|
||||||
let { name, domain } = c.req.query();
|
let { name, domain } = await c.req.query();
|
||||||
// if no name, generate random name
|
// if no name, generate random name
|
||||||
if (!name) {
|
if (!name) {
|
||||||
name = Math.random().toString(36).substring(2, 15);
|
name = Math.random().toString(36).substring(2, 15);
|
||||||
}
|
}
|
||||||
// remove special characters
|
|
||||||
name = name.replace(/[^a-zA-Z0-9.]/g, '')
|
|
||||||
// check name length
|
|
||||||
if (name.length < 0) {
|
|
||||||
return c.text("Name too short", 400)
|
|
||||||
}
|
|
||||||
if (name.length > 100) {
|
|
||||||
return c.text("Name too long (max 100)", 400)
|
|
||||||
}
|
|
||||||
// check domain, generate random domain
|
// check domain, generate random domain
|
||||||
if (!domain || !c.env.DOMAINS.includes(domain)) {
|
if (!domain || !c.env.DOMAINS.includes(domain)) {
|
||||||
domain = c.env.DOMAINS[Math.floor(Math.random() * c.env.DOMAINS.length)];
|
domain = c.env.DOMAINS[Math.floor(Math.random() * c.env.DOMAINS.length)];
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
import { Hono } from 'hono'
|
|
||||||
|
|
||||||
const api = new Hono()
|
|
||||||
|
|
||||||
api.post('/api/requset_send_mail_access', async (c) => {
|
|
||||||
const { address } = c.get("jwtPayload")
|
|
||||||
if (!address) {
|
|
||||||
return c.text("No address", 400)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { success } = await c.env.DB.prepare(
|
|
||||||
`INSERT INTO address_sender (address, enabled) VALUES (?, 0)`
|
|
||||||
).bind(address).run();
|
|
||||||
if (!success) {
|
|
||||||
return c.text("Failed to request send mail access", 500)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message && e.message.includes("UNIQUE")) {
|
|
||||||
return c.text("Already requested", 400)
|
|
||||||
}
|
|
||||||
return c.text("Failed to request send mail access", 500)
|
|
||||||
}
|
|
||||||
return c.json({ status: "ok" })
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
api.post('/api/send_mail', async (c) => {
|
|
||||||
const { address } = c.get("jwtPayload")
|
|
||||||
// check permission
|
|
||||||
const balance = await c.env.DB.prepare(
|
|
||||||
`SELECT balance FROM address_sender
|
|
||||||
where address = ? and enabled = 1`
|
|
||||||
).bind(address).first("balance");
|
|
||||||
if (!balance || balance <= 0) {
|
|
||||||
return c.text("No balance", 400);
|
|
||||||
}
|
|
||||||
let {
|
|
||||||
from_name, to_mail, to_name,
|
|
||||||
subject, content, is_html
|
|
||||||
} = await c.req.json();
|
|
||||||
if (!address) {
|
|
||||||
return c.text("No address", 400)
|
|
||||||
}
|
|
||||||
if (!to_mail) {
|
|
||||||
return c.text("Invalid to mail", 400)
|
|
||||||
}
|
|
||||||
from_name = from_name || address;
|
|
||||||
to_name = to_name || to_mail;
|
|
||||||
if (!subject) {
|
|
||||||
return c.text("Invalid subject", 400)
|
|
||||||
}
|
|
||||||
if (!content) {
|
|
||||||
return c.text("Invalid content", 400)
|
|
||||||
}
|
|
||||||
const body = JSON.stringify({
|
|
||||||
"personalizations": [
|
|
||||||
{
|
|
||||||
"to": [{
|
|
||||||
"email": to_mail,
|
|
||||||
"name": to_name,
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"from": {
|
|
||||||
"email": address,
|
|
||||||
"name": from_name,
|
|
||||||
},
|
|
||||||
"subject": subject,
|
|
||||||
"content": [{
|
|
||||||
"type": is_html ? "text/html" : "text/plain",
|
|
||||||
"value": content,
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
let send_request = new Request("https://api.mailchannels.net/tx/v1/send", {
|
|
||||||
"method": "POST",
|
|
||||||
"headers": {
|
|
||||||
"content-type": "application/json",
|
|
||||||
},
|
|
||||||
"body": body,
|
|
||||||
});
|
|
||||||
const resp = await fetch(send_request);
|
|
||||||
const respText = await resp.text();
|
|
||||||
console.log(resp.status + " " + resp.statusText + ": " + respText);
|
|
||||||
if (resp.status >= 300) {
|
|
||||||
return c.text("Failed to send mail", 500)
|
|
||||||
}
|
|
||||||
// update balance
|
|
||||||
try {
|
|
||||||
const { success } = await c.env.DB.prepare(
|
|
||||||
`UPDATE address_sender SET balance = balance - 1
|
|
||||||
where address = ?`
|
|
||||||
).bind(address).run();
|
|
||||||
if (!success) {
|
|
||||||
console.warn(`Failed to update balance for ${address}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Failed to update balance for ${address}`);
|
|
||||||
}
|
|
||||||
// save to sendbox
|
|
||||||
try {
|
|
||||||
const { success: success2 } = await c.env.DB.prepare(
|
|
||||||
`INSERT INTO sendbox (address, raw) VALUES (?, ?)`
|
|
||||||
).bind(address, body).run();
|
|
||||||
if (!success2) {
|
|
||||||
console.warn(`Failed to save to sendbox for ${address}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Failed to save to sendbox for ${address}`);
|
|
||||||
}
|
|
||||||
return c.json({ status: "ok" });
|
|
||||||
})
|
|
||||||
|
|
||||||
api.get('/api/sendbox', async (c) => {
|
|
||||||
const { address } = c.get("jwtPayload")
|
|
||||||
if (!address) {
|
|
||||||
return c.json({ "error": "No address" }, 400)
|
|
||||||
}
|
|
||||||
const { limit, offset } = c.req.query();
|
|
||||||
if (!limit || limit < 0 || limit > 100) {
|
|
||||||
return c.text("Invalid limit", 400)
|
|
||||||
}
|
|
||||||
if (!offset || offset < 0) {
|
|
||||||
return c.text("Invalid offset", 400)
|
|
||||||
}
|
|
||||||
const { results } = await c.env.DB.prepare(
|
|
||||||
`SELECT * FROM sendbox where address = ?
|
|
||||||
order by id desc limit ? offset ?`
|
|
||||||
).bind(address, limit, offset).all();
|
|
||||||
let count = 0;
|
|
||||||
if (offset == 0) {
|
|
||||||
const { count: mailCount } = await c.env.DB.prepare(
|
|
||||||
`SELECT count(*) as count FROM sendbox where address = ?`
|
|
||||||
).bind(address).first();
|
|
||||||
count = mailCount;
|
|
||||||
}
|
|
||||||
return c.json({
|
|
||||||
results: results,
|
|
||||||
count: count
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
export { api }
|
|
||||||
@@ -5,7 +5,6 @@ import { jwt } from 'hono/jwt'
|
|||||||
import { api } from './router';
|
import { api } from './router';
|
||||||
import { api as adminApi } from './admin_api';
|
import { api as adminApi } from './admin_api';
|
||||||
import { api as apiV1 } from './api_v1';
|
import { api as apiV1 } from './api_v1';
|
||||||
import { api as apiSendMail } from './send_mail_api'
|
|
||||||
import { email } from './email';
|
import { email } from './email';
|
||||||
|
|
||||||
const app = new Hono()
|
const app = new Hono()
|
||||||
@@ -41,7 +40,6 @@ app.use('/admin/*', async (c, next) => {
|
|||||||
app.route('/', api)
|
app.route('/', api)
|
||||||
app.route('/', adminApi)
|
app.route('/', adminApi)
|
||||||
app.route('/', apiV1)
|
app.route('/', apiV1)
|
||||||
app.route('/', apiSendMail)
|
|
||||||
|
|
||||||
app.all('/*', async c => c.text("Not Found", 404))
|
app.all('/*', async c => c.text("Not Found", 404))
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ name = "cloudflare_temp_email"
|
|||||||
main = "src/worker.js"
|
main = "src/worker.js"
|
||||||
compatibility_date = "2023-12-01"
|
compatibility_date = "2023-12-01"
|
||||||
node_compat = true
|
node_compat = true
|
||||||
# if you want use custom_domain, you need to add routes
|
|
||||||
# routes = [
|
|
||||||
# { pattern = "temp-email-api.xxxxx.xyz", custom_domain = true },
|
|
||||||
# ]
|
|
||||||
|
|
||||||
[vars]
|
[vars]
|
||||||
PREFIX = "tmp"
|
PREFIX = "tmp"
|
||||||
|
|||||||
Reference in New Issue
Block a user