mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-10 17:43:31 +08:00
Compare commits
21 Commits
feature/ba
...
v0.2.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb74504282 | ||
|
|
cb758ec012 | ||
|
|
02835c18e9 | ||
|
|
f83492e683 | ||
|
|
074a3b6f2a | ||
|
|
d738210cb5 | ||
|
|
cfeafb2d30 | ||
|
|
49d29ac7cc | ||
|
|
372b71b08b | ||
|
|
0c5365da1f | ||
|
|
58ad025e61 | ||
|
|
6c41288a7b | ||
|
|
b8f0fa49cf | ||
|
|
ee2fdab279 | ||
|
|
fbd2e0e844 | ||
|
|
165efa69cc | ||
|
|
2790f65a5f | ||
|
|
37a9b0557a | ||
|
|
42a828e98b | ||
|
|
a124e00766 | ||
|
|
796d72badb |
46
.github/workflows/tag_build.yml
vendored
Normal file
46
.github/workflows/tag_build.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: Tag Build CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v3
|
||||||
|
name: Install pnpm
|
||||||
|
id: pnpm-install
|
||||||
|
with:
|
||||||
|
version: 8
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Build Frontend
|
||||||
|
run: cd frontend && pnpm install --no-frozen-lockfile && pnpm build:release
|
||||||
|
|
||||||
|
- name: Zip Frontend dist
|
||||||
|
run: cd frontend/dist/ && zip -r frontend.zip *
|
||||||
|
|
||||||
|
- name: cp wrangler.toml
|
||||||
|
run: cd worker && cp wrangler.toml.template wrangler.toml
|
||||||
|
|
||||||
|
- name: Build Backend
|
||||||
|
run: cd worker && pnpm install --no-frozen-lockfile && pnpm build
|
||||||
|
|
||||||
|
- name: Upload to Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
files: |
|
||||||
|
frontend/dist/frontend.zip
|
||||||
|
worker/dist/worker.js
|
||||||
34
CHANGELOG
34
CHANGELOG
@@ -1,34 +0,0 @@
|
|||||||
# CHANGE LOG
|
|
||||||
|
|
||||||
## 2024-04-10 v0.0.1
|
|
||||||
|
|
||||||
Breaking changes:
|
|
||||||
|
|
||||||
- remove `ENABLE_ATTACHMENT` config
|
|
||||||
- use rust wasm to parse email in frontend
|
|
||||||
- deprecated api moved to `/api/v1`
|
|
||||||
|
|
||||||
DB changes
|
|
||||||
|
|
||||||
- `db/2024-04-09-patch.sql`
|
|
||||||
|
|
||||||
## 2024-04-09 v0.0.0
|
|
||||||
|
|
||||||
release v0.0.0
|
|
||||||
|
|
||||||
## 2024-04-03
|
|
||||||
|
|
||||||
DB changes
|
|
||||||
|
|
||||||
- `db/2024-04-03-patch.sql`
|
|
||||||
|
|
||||||
Changes:
|
|
||||||
|
|
||||||
- add delete account
|
|
||||||
- add admin panel search
|
|
||||||
|
|
||||||
## 2024-01-13
|
|
||||||
|
|
||||||
DB changes
|
|
||||||
|
|
||||||
- `db/2024-01-13-patch.sql`
|
|
||||||
76
CHANGELOG.md
Normal file
76
CHANGELOG.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# 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,9 +2,16 @@
|
|||||||
|
|
||||||
## [English](README_EN.md)
|
## [English](README_EN.md)
|
||||||
|
|
||||||
[CHANGELOG](CHANGELOG)
|
## [新版部署文档](https://temp-mail-docs.awsl.uk)
|
||||||
|
|
||||||
[Backend](https://temp-email-api.dreamhunter2333.xyz/)
|
## [CHANGELOG](CHANGELOG.md)
|
||||||
|
|
||||||
|
## [在线演示](https://mail.awsl.uk/)
|
||||||
|
|
||||||
|
[https://mail.awsl.uk](https://mail.awsl.uk/)
|
||||||
|
或者 [https://temp-email.dreamhunter2333.xyz](https://temp-email.dreamhunter2333.xyz/)
|
||||||
|
|
||||||
|
[Backend](https://temp-email-api.awsl.uk/)
|
||||||

|

|
||||||

|

|
||||||

|

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

|

|
||||||

|

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

|

|
||||||

|

|
||||||

|

|
||||||
@@ -28,6 +35,7 @@
|
|||||||
|
|
||||||
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
|
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
|
||||||
- [English](#english)
|
- [English](#english)
|
||||||
|
- [CHANGELOG](#changelog)
|
||||||
- [在线演示](#在线演示)
|
- [在线演示](#在线演示)
|
||||||
- [功能/TODO](#功能todo)
|
- [功能/TODO](#功能todo)
|
||||||
- [什么是临时邮箱](#什么是临时邮箱)
|
- [什么是临时邮箱](#什么是临时邮箱)
|
||||||
@@ -38,9 +46,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
|
||||||
|
|
||||||
@@ -55,6 +63,7 @@
|
|||||||
- [x] 增加自动回复功能
|
- [x] 增加自动回复功能
|
||||||
- [x] 增加查看附件功能
|
- [x] 增加查看附件功能
|
||||||
- [x] 使用 rust wasm 解析邮件
|
- [x] 使用 rust wasm 解析邮件
|
||||||
|
- [x] 支持发送邮件
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -150,6 +159,8 @@ database_id = "xxx" # D1 数据库 ID
|
|||||||
|
|
||||||
部署
|
部署
|
||||||
|
|
||||||
|
第一次部署会提示创建项目, `production` 分支请填写 `production`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run deploy
|
pnpm run deploy
|
||||||
```
|
```
|
||||||
@@ -162,6 +173,8 @@ pnpm run deploy
|
|||||||
|
|
||||||
## Cloudflare Email Routing
|
## Cloudflare Email Routing
|
||||||
|
|
||||||
|
在将电子邮件地址绑定到您的 Worker 之前,您需要启用电子邮件路由并拥有至少一个经过验证的电子邮件地址。
|
||||||
|
|
||||||
配置对应域名的 `电子邮件 DNS 记录`
|
配置对应域名的 `电子邮件 DNS 记录`
|
||||||
|
|
||||||
配置 `Cloudflare Email Routing` catch-all 发送到 `worker`
|
配置 `Cloudflare Email Routing` catch-all 发送到 `worker`
|
||||||
@@ -172,6 +185,8 @@ pnpm run deploy
|
|||||||
|
|
||||||
## Cloudflare Pages 前端
|
## Cloudflare Pages 前端
|
||||||
|
|
||||||
|
第一次部署会提示创建项目, `production` 分支请填写 `production`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
pnpm install
|
pnpm install
|
||||||
@@ -190,6 +205,19 @@ 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,9 +2,11 @@
|
|||||||
|
|
||||||
## [中文](README.md)
|
## [中文](README.md)
|
||||||
|
|
||||||
[CHANGELOG](CHANGELOG)
|
[CHANGELOG](CHANGELOG.md)
|
||||||
|
|
||||||
## [Live Demo](https://temp-email.dreamhunter2333.xyz/)
|
## [Live Demo](https://mail.awsl.uk/)
|
||||||
|
|
||||||
|
[https://mail.awsl.uk](https://mail.awsl.uk/)
|
||||||
|
|
||||||
This is a temporary email service that uses Cloudflare Workers to create a temporary email address and view the received email in web browser.
|
This is a temporary email service that uses Cloudflare Workers to create a temporary email address and view the received email in web browser.
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ 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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -43,6 +46,8 @@ 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
|
||||||
@@ -65,12 +70,18 @@ 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
|
||||||
@@ -82,3 +93,16 @@ 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`
|
||||||
|
|||||||
14
db/2024-04-12-patch.sql
Normal file
14
db/2024-04-12-patch.sql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS address_sender (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
address TEXT UNIQUE,
|
||||||
|
balance INTEGER DEFAULT 0,
|
||||||
|
enabled INTEGER DEFAULT 1,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS sendbox (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
address TEXT,
|
||||||
|
raw TEXT,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
@@ -8,6 +8,8 @@ CREATE TABLE IF NOT EXISTS mails (
|
|||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_mails_address ON mails(address);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS raw_mails (
|
CREATE TABLE IF NOT EXISTS raw_mails (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
message_id TEXT,
|
message_id TEXT,
|
||||||
@@ -17,6 +19,8 @@ CREATE TABLE IF NOT EXISTS raw_mails (
|
|||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_raw_mails_address ON raw_mails(address);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS address (
|
CREATE TABLE IF NOT EXISTS address (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
name TEXT UNIQUE,
|
name TEXT UNIQUE,
|
||||||
@@ -24,6 +28,8 @@ CREATE TABLE IF NOT EXISTS address (
|
|||||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_address_name ON address(name);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
source_prefix TEXT,
|
source_prefix TEXT,
|
||||||
@@ -35,6 +41,8 @@ CREATE TABLE IF NOT EXISTS auto_reply_mails (
|
|||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_auto_reply_mails_address ON auto_reply_mails(address);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS attachments (
|
CREATE TABLE IF NOT EXISTS attachments (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
source TEXT,
|
source TEXT,
|
||||||
@@ -43,3 +51,22 @@ 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,8 +6,9 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ const getSettings = async () => {
|
|||||||
address: res["address"],
|
address: res["address"],
|
||||||
auto_reply: res["auto_reply"],
|
auto_reply: res["auto_reply"],
|
||||||
has_v1_mails: res["has_v1_mails"],
|
has_v1_mails: res["has_v1_mails"],
|
||||||
|
send_balance: res["send_balance"],
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
settings.value.fetched = true;
|
settings.value.fetched = true;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const i18n = createI18n({
|
|||||||
'en': {
|
'en': {
|
||||||
messages: {}
|
messages: {}
|
||||||
},
|
},
|
||||||
'zhCN': {
|
'zh': {
|
||||||
messages: {}
|
messages: {}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
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(),
|
||||||
@@ -14,6 +16,14 @@ 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
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const useGlobalState = createGlobalState(
|
|||||||
const settings = ref({
|
const settings = ref({
|
||||||
fetched: false,
|
fetched: false,
|
||||||
has_v1_mails: false,
|
has_v1_mails: false,
|
||||||
|
send_balance: 0,
|
||||||
address: '',
|
address: '',
|
||||||
auto_reply: {
|
auto_reply: {
|
||||||
subject: '',
|
subject: '',
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
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 {
|
||||||
@@ -20,7 +25,7 @@ export async function processItem(item) {
|
|||||||
return {
|
return {
|
||||||
id: a_item.content_id || Math.random().toString(36).substring(2, 15),
|
id: a_item.content_id || Math.random().toString(36).substring(2, 15),
|
||||||
filename: a_item.filename || a_item.content_id || "",
|
filename: a_item.filename || a_item.content_id || "",
|
||||||
size: a_item.content?.length || 0,
|
size: humanFileSize(a_item.content?.length || 0),
|
||||||
url: blob_url
|
url: blob_url
|
||||||
}
|
}
|
||||||
}) || [];
|
}) || [];
|
||||||
@@ -52,7 +57,7 @@ export async function processItem(item) {
|
|||||||
return {
|
return {
|
||||||
id: a_item.contentId || Math.random().toString(36).substring(2, 15),
|
id: a_item.contentId || Math.random().toString(36).substring(2, 15),
|
||||||
filename: a_item.filename || a_item.contentId || "",
|
filename: a_item.filename || a_item.contentId || "",
|
||||||
size: a_item.content?.length || 0,
|
size: humanFileSize(a_item.content?.length || 0),
|
||||||
url: blob_url
|
url: blob_url
|
||||||
}
|
}
|
||||||
}) || [];
|
}) || [];
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ 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, getDownloadEmlUrl } from '../utils/email-parser'
|
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()
|
||||||
@@ -49,6 +50,7 @@ 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',
|
||||||
@@ -72,6 +74,7 @@ const { t } = useI18n({
|
|||||||
account: '账号',
|
account: '账号',
|
||||||
unknow: '未知',
|
unknow: '未知',
|
||||||
addressQueryTip: '留空查询所有地址',
|
addressQueryTip: '留空查询所有地址',
|
||||||
|
senderAccess: '发件权限控制',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -396,6 +399,9 @@ 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 } from '@vicons/material'
|
import { DarkModeFilled, LightModeFilled, MenuFilled, AdminPanelSettingsFilled, SendFilled } from '@vicons/material'
|
||||||
import { GithubAlt, Language, User, Home, Copy } from '@vicons/fa'
|
import { GithubAlt, Language, User, Home, Copy } from '@vicons/fa'
|
||||||
|
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
@@ -73,10 +73,13 @@ 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.',
|
getNewEmailTip1: 'Please input the email you want to use. only allow ., a-z, A-Z and 0-9',
|
||||||
getNewEmailTip2: 'Levaing it blank will generate a random email address.',
|
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',
|
||||||
@@ -86,6 +89,7 @@ const { t } = useI18n({
|
|||||||
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.',
|
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 临时邮件',
|
||||||
@@ -102,10 +106,13 @@ const { t } = useI18n({
|
|||||||
home: '主页',
|
home: '主页',
|
||||||
menu: '菜单',
|
menu: '菜单',
|
||||||
user: '用户',
|
user: '用户',
|
||||||
|
sendbox: '发件箱',
|
||||||
|
sendMail: '发送邮件',
|
||||||
pleaseGetNewEmail: '请"登录"或点击 "获取新邮箱" 按钮来获取一个新的邮箱地址',
|
pleaseGetNewEmail: '请"登录"或点击 "获取新邮箱" 按钮来获取一个新的邮箱地址',
|
||||||
getNewEmail: '获取新邮箱',
|
getNewEmail: '获取新邮箱',
|
||||||
getNewEmailTip1: '请输入你想要使用的邮箱地址。',
|
getNewEmailTip1: '请输入你想要使用的邮箱地址, 只允许 ., a-z, A-Z, 0-9',
|
||||||
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
getNewEmailTip2: '留空将会生成一个随机的邮箱地址。',
|
||||||
|
getNewEmailTip3: '你可以从下拉列表中选择一个域名。',
|
||||||
yourAddress: '你的邮箱地址是',
|
yourAddress: '你的邮箱地址是',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
passwordTip: '请复制密码,你可以使用它登录你的邮箱。',
|
||||||
@@ -116,6 +123,7 @@ const { t } = useI18n({
|
|||||||
showPassword: '查看密码',
|
showPassword: '查看密码',
|
||||||
fetchAddressError: '获取地址失败, 请检查你的 jwt 是否有效 或 网络是否正常。',
|
fetchAddressError: '获取地址失败, 请检查你的 jwt 是否有效 或 网络是否正常。',
|
||||||
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
|
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
|
||||||
|
generateName: '生成随机名字',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -172,6 +180,19 @@ 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,
|
||||||
@@ -306,6 +327,23 @@ 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(
|
||||||
@@ -337,6 +375,7 @@ 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>
|
||||||
|
|
||||||
@@ -357,7 +396,7 @@ onMounted(async () => {
|
|||||||
<n-alert v-if="settings.has_v1_mails" type="warning" show-icon closable>
|
<n-alert v-if="settings.has_v1_mails" type="warning" show-icon closable>
|
||||||
<span>
|
<span>
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small"
|
<n-button tag="a" target="_blank" tertiary type="info" size="small"
|
||||||
href="https://temp-email-v1.dreamhunter2333.xyz/">
|
href="https://mail-v1.awsl.uk">
|
||||||
<b>{{ t('mailV1Alert') }} </b>
|
<b>{{ t('mailV1Alert') }} </b>
|
||||||
</n-button>
|
</n-button>
|
||||||
</span>
|
</span>
|
||||||
@@ -365,6 +404,10 @@ onMounted(async () => {
|
|||||||
<n-alert type="info" show-icon>
|
<n-alert type="info" show-icon>
|
||||||
<span>
|
<span>
|
||||||
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
||||||
|
<n-button style="margin-left: 10px" @click="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-button style="margin-left: 10px" @click="copy" size="small" tertiary round type="primary">
|
||||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||||
</n-button>
|
</n-button>
|
||||||
@@ -391,18 +434,25 @@ onMounted(async () => {
|
|||||||
<template #header>
|
<template #header>
|
||||||
<div>{{ t('getNewEmail') }}</div>
|
<div>{{ t('getNewEmail') }}</div>
|
||||||
</template>
|
</template>
|
||||||
<span>
|
<n-spin :show="generateNameLoading">
|
||||||
<p>{{ t("getNewEmailTip1") }}</p>
|
<span>
|
||||||
<p>{{ t("getNewEmailTip2") }}</p>
|
<p>{{ t("getNewEmailTip1") }}</p>
|
||||||
</span>
|
<p>{{ t("getNewEmailTip2") }}</p>
|
||||||
<n-input-group>
|
<p>{{ t("getNewEmailTip3") }}</p>
|
||||||
<n-input-group-label v-if="openSettings.prefix">
|
</span>
|
||||||
{{ openSettings.prefix }}
|
<n-button @click="generateName" style="margin-bottom: 10px;">
|
||||||
</n-input-group-label>
|
{{ t('generateName') }}
|
||||||
<n-input v-model:value="emailName" />
|
</n-button>
|
||||||
<n-input-group-label>@</n-input-group-label>
|
<n-input-group>
|
||||||
<n-select v-model:value="emailDomain" :consistent-menu-width="false" :options="openSettings.domains" />
|
<n-input-group-label v-if="openSettings.prefix">
|
||||||
</n-input-group>
|
{{ openSettings.prefix }}
|
||||||
|
</n-input-group-label>
|
||||||
|
<n-input v-model:value="emailName" />
|
||||||
|
<n-input-group-label>@</n-input-group-label>
|
||||||
|
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||||
|
:options="openSettings.domains" />
|
||||||
|
</n-input-group>
|
||||||
|
</n-spin>
|
||||||
<template #action>
|
<template #action>
|
||||||
<n-button @click="showNewEmail = false">
|
<n-button @click="showNewEmail = false">
|
||||||
{{ t('cancel') }}
|
{{ t('cancel') }}
|
||||||
|
|||||||
@@ -1,299 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { watch, onMounted, ref } from "vue";
|
import MailBox from './MailBox.vue';
|
||||||
import { useMessage } from 'naive-ui'
|
|
||||||
import { useI18n } from 'vue-i18n'
|
|
||||||
import { useGlobalState } from '../store'
|
import { useGlobalState } from '../store'
|
||||||
import { api } from '../api'
|
const { settings } = useGlobalState()
|
||||||
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 () => {
|
|
||||||
if (typeof settings.value.address != 'string' || settings.value.address.trim() === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const { results, count: totalCount } = await api.fetch(
|
|
||||||
`/api/mails`
|
|
||||||
+ `?limit=${pageSize.value}`
|
|
||||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
|
||||||
);
|
|
||||||
data.value = await Promise.all(results.map(async (item) => {
|
|
||||||
return await processItem(item);
|
|
||||||
}));
|
|
||||||
if (totalCount > 0) {
|
|
||||||
count.value = totalCount;
|
|
||||||
}
|
|
||||||
if (!isMobile.value && !curMail.value && data.value.length > 0) {
|
|
||||||
curMail.value = data.value[0];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
message.error(error.message || "error");
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clickRow = async (row) => {
|
|
||||||
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 api.getSettings();
|
|
||||||
await refresh();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<n-layout v-if="settings.address">
|
<MailBox 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.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
|
||||||
@click="getAttachments(curMail.attachments)">
|
|
||||||
{{ t('attachments') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
|
||||||
:href="getDownloadEmlUrl(curMail.raw)">
|
|
||||||
<n-icon :component="CloudDownloadRound" />
|
|
||||||
{{ t('downloadMail') }}
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
<div v-html="curMail.message" style="margin-top: 10px;max-height: 100vh;"></div>
|
|
||||||
</n-card>
|
|
||||||
<n-card class="mail-item" v-else>
|
|
||||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
|
||||||
</n-result>
|
|
||||||
</n-card>
|
|
||||||
</template>
|
|
||||||
</n-split>
|
|
||||||
<div class="left" v-else>
|
|
||||||
<div>
|
|
||||||
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
|
|
||||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
|
|
||||||
</div>
|
|
||||||
<n-switch v-model:value="autoRefresh" size="small">
|
|
||||||
<template #checked>
|
|
||||||
{{ t('autoRefresh') }}
|
|
||||||
</template>
|
|
||||||
<template #unchecked>
|
|
||||||
{{ t('autoRefresh') }}
|
|
||||||
</template></n-switch>
|
|
||||||
<n-button class="center" @click="refresh" size="small" type="primary">
|
|
||||||
{{ t('refresh') }}
|
|
||||||
</n-button>
|
|
||||||
</div>
|
|
||||||
<div id="drawer-target" style="overflow: scroll; max-height: 80vh;">
|
|
||||||
<n-list hoverable clickable>
|
|
||||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)">
|
|
||||||
<n-thing class="center" :title="row.subject" style="overflow: scroll">
|
|
||||||
<template #description>
|
|
||||||
<n-tag type="info">
|
|
||||||
ID: {{ row.id }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
{{ row.created_at }}
|
|
||||||
</n-tag>
|
|
||||||
<div style="word-break: break-all; font-size: small;">
|
|
||||||
FROM: {{ row.source }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</n-thing>
|
|
||||||
</n-list-item>
|
|
||||||
</n-list>
|
|
||||||
</div>
|
|
||||||
<n-drawer v-model:show="curMail" width="100%" :trap-focus="false" :block-scroll="false" to="#drawer-target">
|
|
||||||
<n-drawer-content :title="curMail.subject" closable>
|
|
||||||
<n-card style="overflow: scroll;">
|
|
||||||
<n-space>
|
|
||||||
<n-tag type="info">
|
|
||||||
ID: {{ curMail.id }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
{{ curMail.created_at }}
|
|
||||||
</n-tag>
|
|
||||||
<n-tag type="info">
|
|
||||||
FROM: {{ curMail.source }}
|
|
||||||
</n-tag>
|
|
||||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
|
||||||
@click="getAttachments(curMail.attachments)">
|
|
||||||
{{ t('attachments') }}
|
|
||||||
</n-button>
|
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small'" :download="curMail.id + '.eml'"
|
|
||||||
:href="getDownloadEmlUrl(curMail)">
|
|
||||||
{{ t('downloadMail') }}
|
|
||||||
<n-icon :component="CloudDownloadRound" />
|
|
||||||
</n-button>
|
|
||||||
</n-space>
|
|
||||||
<div v-html="curMail.message" style="max-height: 100vh;"></div>
|
|
||||||
</n-card>
|
|
||||||
</n-drawer-content>
|
|
||||||
</n-drawer>
|
|
||||||
</div>
|
|
||||||
</n-layout>
|
|
||||||
<n-modal v-model:show="showAttachments" preset="dialog" title="Dialog">
|
|
||||||
<template #header>
|
|
||||||
<div>{{ t("attachments") }}</div>
|
|
||||||
</template>
|
|
||||||
<n-list hoverable clickable>
|
|
||||||
<n-list-item v-for="row in curAttachments" v-bind:key="row.id">
|
|
||||||
<n-thing class="center" :title="row.filename">
|
|
||||||
<template #description>
|
|
||||||
<n-space>
|
|
||||||
<n-tag type="info">
|
|
||||||
Size: {{ row.size }}
|
|
||||||
</n-tag>
|
|
||||||
</n-space>
|
|
||||||
</template>
|
|
||||||
</n-thing>
|
|
||||||
<template #suffix>
|
|
||||||
<n-button tag="a" target="_blank" tertiary type="info" size="small'" :download="row.filename"
|
|
||||||
:href="row.url">
|
|
||||||
<n-icon :component="CloudDownloadRound" />
|
|
||||||
</n-button>
|
|
||||||
</template>
|
|
||||||
</n-list-item>
|
|
||||||
</n-list>
|
|
||||||
<template #action>
|
|
||||||
</template>
|
|
||||||
</n-modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.left {
|
|
||||||
overflow: scroll;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-dark-backgroud {
|
|
||||||
background-color: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay-light-backgroud {
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.mail-item {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
294
frontend/src/views/MailBox.vue
Normal file
294
frontend/src/views/MailBox.vue
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
<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,7 +2,6 @@
|
|||||||
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'
|
||||||
|
|
||||||
@@ -43,7 +42,6 @@ const { t } = useI18n({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getSettings = async () => {
|
const getSettings = async () => {
|
||||||
await api.getSettings()
|
|
||||||
sourcePrefix.value = settings.value.auto_reply.source_prefix || ""
|
sourcePrefix.value = settings.value.auto_reply.source_prefix || ""
|
||||||
enableAutoReply.value = settings.value.auto_reply.enabled || false
|
enableAutoReply.value = settings.value.auto_reply.enabled || false
|
||||||
name.value = settings.value.auto_reply.name || ""
|
name.value = settings.value.auto_reply.name || ""
|
||||||
|
|||||||
190
frontend/src/views/admin/SenderAccess.vue
Normal file
190
frontend/src/views/admin/SenderAccess.vue
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<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>
|
||||||
156
frontend/src/views/send/SendBox.vue
Normal file
156
frontend/src/views/send/SendBox.vue
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, h, onMounted, watch } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useGlobalState } from '../../store'
|
||||||
|
import { api } from '../../api'
|
||||||
|
|
||||||
|
const { localeCache, settings } = useGlobalState()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const { t } = useI18n({
|
||||||
|
locale: localeCache.value || 'zh',
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
address: 'Address',
|
||||||
|
success: 'Success',
|
||||||
|
to_mail: 'To Mail',
|
||||||
|
subject: 'Subject',
|
||||||
|
created_at: 'Created At',
|
||||||
|
action: 'Action',
|
||||||
|
refresh: 'Refresh',
|
||||||
|
itemCount: 'itemCount',
|
||||||
|
view: 'View',
|
||||||
|
ok: 'OK'
|
||||||
|
},
|
||||||
|
zh: {
|
||||||
|
address: '地址',
|
||||||
|
success: '成功',
|
||||||
|
to_mail: '收件人邮箱',
|
||||||
|
subject: '主题',
|
||||||
|
created_at: '创建时间',
|
||||||
|
action: '操作',
|
||||||
|
refresh: '刷新',
|
||||||
|
itemCount: '总数',
|
||||||
|
view: '查看',
|
||||||
|
ok: '确定'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const data = ref([])
|
||||||
|
const count = ref(0)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
|
||||||
|
const curRow = ref({})
|
||||||
|
const showModal = ref(false)
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const { results, count: addressCount } = await api.fetch(
|
||||||
|
`/api/sendbox`
|
||||||
|
+ `?limit=${pageSize.value}`
|
||||||
|
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||||
|
);
|
||||||
|
data.value = results.map((item) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(item.raw);
|
||||||
|
item.to_mail = data?.personalizations?.map(
|
||||||
|
(p) => p.to?.map((t) => t.email).join(',')
|
||||||
|
).join(';');
|
||||||
|
item.subject = data.subject;
|
||||||
|
item.raw = JSON.stringify(data, null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
if (addressCount > 0) {
|
||||||
|
count.value = addressCount;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
message.error(error.message || "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "ID",
|
||||||
|
key: "id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('address'),
|
||||||
|
key: "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('to_mail'),
|
||||||
|
key: "to_mail"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('subject'),
|
||||||
|
key: "subject"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('created_at'),
|
||||||
|
key: "created_at"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('action'),
|
||||||
|
key: 'actions',
|
||||||
|
render(row) {
|
||||||
|
return h('div', [
|
||||||
|
h(NButton,
|
||||||
|
{
|
||||||
|
type: 'success',
|
||||||
|
ghost: true,
|
||||||
|
onClick: () => {
|
||||||
|
showModal.value = true;
|
||||||
|
curRow.value = row;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ default: () => t('view') }
|
||||||
|
)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
watch([page, pageSize], async () => {
|
||||||
|
await fetchData()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="settings.address">
|
||||||
|
<n-modal v-model:show="showModal" preset="dialog">
|
||||||
|
<pre>{{ curRow.raw }}</pre>
|
||||||
|
</n-modal>
|
||||||
|
<div style="display: inline-block;">
|
||||||
|
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||||
|
:page-sizes="[20, 50, 100]" show-size-picker>
|
||||||
|
<template #prefix="{ itemCount }">
|
||||||
|
{{ t('itemCount') }}: {{ itemCount }}
|
||||||
|
</template>
|
||||||
|
<template #suffix>
|
||||||
|
<n-button @click="fetchData" type="primary" size="small" ghost>
|
||||||
|
{{ t('refresh') }}
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
</n-pagination>
|
||||||
|
</div>
|
||||||
|
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.n-pagination {
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
189
frontend/src/views/send/SendMail.vue
Normal file
189
frontend/src/views/send/SendMail.vue
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
<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>
|
||||||
@@ -13,7 +13,7 @@ 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(),
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
"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,10 +138,54 @@ 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: mailCount } = await c.env.DB.prepare(`
|
const { count: mailCountV1 } = await c.env.DB.prepare(`
|
||||||
SELECT count(*) as count FROM mails`
|
SELECT count(*) as count FROM mails`
|
||||||
).first();
|
).first();
|
||||||
|
const { count: mailCount } = await c.env.DB.prepare(`
|
||||||
|
SELECT count(*) as count FROM raw_mails`
|
||||||
|
).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`
|
||||||
).first();
|
).first();
|
||||||
@@ -149,7 +193,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: mailCount,
|
mailCount: (mailCountV1 || 0) + (mailCount || 0),
|
||||||
userCount: addressCount,
|
userCount: addressCount,
|
||||||
activeUserCount7days: activeUserCount7days
|
activeUserCount7days: activeUserCount7days
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,18 +8,8 @@ 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 reader = message.raw.getReader();
|
const rawEmail = await new Response(message.raw).text();
|
||||||
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,10 +84,15 @@ 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 > 0
|
has_v1_mails: mailCountV1 && mailCountV1 > 0,
|
||||||
|
send_balance: balance || 0,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -131,11 +136,20 @@ api.get('/open_api/settings', async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
api.get('/api/new_address', async (c) => {
|
api.get('/api/new_address', async (c) => {
|
||||||
let { name, domain } = await c.req.query();
|
let { name, domain } = 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)];
|
||||||
|
|||||||
142
worker/src/send_mail_api.js
Normal file
142
worker/src/send_mail_api.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
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,6 +5,7 @@ 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()
|
||||||
@@ -40,6 +41,7 @@ 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,6 +2,10 @@ 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