feat: add ENABLE_USER_DELETE_EMAIL && ENABLE_AUTO_REPLY && modify fetchAddressError i18n && UI: show autoRefreshInterval (#169)

This commit is contained in:
Dream Hunter
2024-04-27 23:16:18 +08:00
committed by GitHub
parent 1404079073
commit 08a3d4ce0e
10 changed files with 92 additions and 254 deletions
+2 -208
View File
@@ -2,7 +2,7 @@
## [查看部署文档](https://temp-mail-docs.awsl.uk) ## [查看部署文档](https://temp-mail-docs.awsl.uk)
## [English](https://temp-mail-docs.awsl.uk/en/) ## [English Docs](https://temp-mail-docs.awsl.uk/en/)
## [CHANGELOG](CHANGELOG.md) ## [CHANGELOG](CHANGELOG.md)
@@ -21,20 +21,10 @@
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱) - [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
- [查看部署文档](#查看部署文档) - [查看部署文档](#查看部署文档)
- [English](#english) - [English Docs](#english-docs)
- [CHANGELOG](#changelog) - [CHANGELOG](#changelog)
- [在线演示](#在线演示) - [在线演示](#在线演示)
- [功能/TODO](#功能todo) - [功能/TODO](#功能todo)
- [什么是临时邮箱](#什么是临时邮箱)
- [Cloudflare 服务](#cloudflare-服务)
- [wrangler 的安装](#wrangler-的安装)
- [D1 数据库](#d1-数据库)
- [Cloudflare workers 后端](#cloudflare-workers-后端)
- [Cloudflare Email Routing](#cloudflare-email-routing)
- [Cloudflare Pages 前端](#cloudflare-pages-前端)
- [配置发送邮件](#配置发送邮件)
- [配置 DKIM](#配置-dkim)
- [参考资料](#参考资料)
## 功能/TODO ## 功能/TODO
@@ -51,199 +41,3 @@
- [x] 使用 rust wasm 解析邮件 - [x] 使用 rust wasm 解析邮件
- [x] 支持发送邮件 - [x] 支持发送邮件
- [x] 支持 DKIM - [x] 支持 DKIM
---
## 什么是临时邮箱
临时邮箱,也被称为一次性邮箱或临时邮件地址,是一种用于临时接收邮件的虚拟邮箱。与常规邮箱不同,临时邮箱旨在提供一种匿名且临时的邮件接收解决方案。
临时邮箱往往由网站或在线服务提供商提供,用户可以在需要注册或接收验证邮件时使用临时邮箱地址,而无需暴露自己的真实邮箱地址。这样做的好处是可以保护个人隐私
---
## Cloudflare 服务
- `D1``Cloudflare` 的原生无服务器数据库。
- `Pages``Cloudflare` 的静态网站托管服务, 速度超快,始终保持最新状态。
- `Workers``Cloudflare``serverless` 应用服务,可以在全球 300 个数据中心运行代码, 而无需配置或维护基础架构。
- `Cloudflare Email Routing` 可以处理域名的所有电子邮件流量,而无需管理电子邮件服务器。
---
## wrangler 的安装
安装 wrangler
```bash
npm install wrangler -g
```
克隆项目
```bash
git clone https://github.com/dreamhunter2333/cloudflare_temp_email.git
# 切换到最新 tag 或者你想部署的分支,你也可以直接使用 main 分支
# git checkout $(git describe --tags $(git rev-list --tags --max-count=1))
```
---
## D1 数据库
第一次执行登录 wrangler 命令时,会提示登录, 按提示操作即可
```bash
# 创建 D1 并执行 schema.sql
wrangler d1 create dev
wrangler d1 execute dev --file=db/schema.sql
# schema 更新,如果你在此日期之前初始化过数据库,可以执行此命令更新
# wrangler d1 execute dev --file=db/2024-01-13-patch.sql
# wrangler d1 execute dev --file=db/2024-04-03-patch.sql
```
创建完成后,我们在 cloudflare 的控制台可以看到 D1 数据库
![D1](vitepress-docs/docs/public/readme_assets/d1.png)
---
## Cloudflare workers 后端
初始化项目
```bash
cd worker
pnpm install
cp wrangler.toml.template wrangler.toml
```
修改 `wrangler.toml` 文件
```bash
name = "cloudflare_temp_email"
main = "src/worker.js"
compatibility_date = "2023-08-14"
node_compat = true
[vars]
PREFIX = "tmp" # 要处理的邮箱名称前缀
# 如果你想要你的网站私有,取消下面的注释,并修改密码
# PASSWORDS = ["123", "456"]
# admin 控制台密码, 不配置则不允许访问控制台
# ADMIN_PASSWORDS = ["123", "456"]
# admin 联系方式,不配置则不显示,可配置任意字符串
# ADMIN_CONTACT = "xx@xx.xxx"
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
# 默认发送邮件余额,如果不设置,将为 0
# DEFAULT_SEND_BALANCE = 1
# dkim config
# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels
# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容
[[d1_databases]]
binding = "DB"
database_name = "xxx" # D1 数据库名称
database_id = "xxx" # D1 数据库 ID
# 新建地址限流配置
# [[unsafe.bindings]]
# name = "RATE_LIMITER"
# type = "ratelimit"
# namespace_id = "1001"
# # 10 requests per minute
# simple = { limit = 10, period = 60 }
```
部署
第一次部署会提示创建项目, `production` 分支请填写 `production`
```bash
pnpm run deploy
```
部署成功之后再路由中可以看到 `worker``url`,控制台也会输出 `worker``url`
![worker](vitepress-docs/docs/public/readme_assets/worker.png)
---
## Cloudflare Email Routing
在将电子邮件地址绑定到您的 Worker 之前,您需要启用电子邮件路由并拥有至少一个经过验证的电子邮件地址。
配置对应域名的 `电子邮件 DNS 记录`
配置 `Cloudflare Email Routing` catch-all 发送到 `worker`
![email](vitepress-docs/docs/public/readme_assets/email.png)
---
## Cloudflare Pages 前端
第一次部署会提示创建项目, `production` 分支请填写 `production`
```bash
cd frontend
pnpm install
cp .env.example .env.local
```
修改 `.env.local` 文件, 将 `VITE_API_BASE` 修改为 `worker``url`, 不要在末尾加 `/`
例如: `VITE_API_BASE=https://xxx.xxx.workers.dev`
```bash
pnpm build --emptyOutDir
# 根据提示创建 pages
pnpm run deploy
```
![pages](vitepress-docs/docs/public/readme_assets/pages.png)
## 配置发送邮件
找到域名 `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`
## 配置 DKIM
参考: [Adding-a-DKIM-Signature](https://support.mailchannels.com/hc/en-us/articles/7122849237389-Adding-a-DKIM-Signature)
Creating a DKIM private and public key:
Private key as PEM file and base64 encoded txt file:
```bash
openssl genrsa 2048 | tee priv_key.pem | openssl rsa -outform der | openssl base64 -A > priv_key.txt
```
Public key as DNS record:
```bash
echo -n "v=DKIM1;p=" > pub_key_record.txt && \
openssl rsa -in priv_key.pem -pubout -outform der | openssl base64 -A >> pub_key_record.txt
```
`Cloudflare``DNS` 记录中添加 `TXT` 记录
- `_dmarc`: `v=DMARC1; p=none; adkim=r; aspf=r;`
- `mailchannels._domainkey`: `v=DKIM1; p=<content of the file pub_key_record.txt>`
## 参考资料
- https://developers.cloudflare.com/d1/
- https://developers.cloudflare.com/pages/
- https://developers.cloudflare.com/workers/
- https://developers.cloudflare.com/email-routing/
+2
View File
@@ -59,6 +59,8 @@ const getOpenSettings = async (message) => {
} }
}), }),
adminContact: res["adminContact"] || "", adminContact: res["adminContact"] || "",
enableUserDeleteEmail: res["enableUserDeleteEmail"] || false,
enableAutoReply: res["enableAutoReply"] || false,
}; };
if (openSettings.value.needAuth) { if (openSettings.value.needAuth) {
showAuth.value = true; showAuth.value = true;
+2
View File
@@ -11,6 +11,8 @@ export const useGlobalState = createGlobalState(
prefix: '', prefix: '',
needAuth: false, needAuth: false,
adminContact: '', adminContact: '',
enableUserDeleteEmail: false,
enableAutoReply: false,
domains: [{ domains: [{
label: 'test.com', label: 'test.com',
value: 'test.com' value: 'test.com'
+4 -2
View File
@@ -93,7 +93,7 @@ const { t } = useI18n({
copy: 'Copy', copy: 'Copy',
copied: 'Copied', copied: 'Copied',
showPassword: 'Show Password', showPassword: 'Show Password',
fetchAddressError: 'Fetch address error, maybe your jwt is invalid or network error.', fetchAddressError: 'Login password is invalid or account not exist, it may be network connection issue, please try again later.',
mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.', mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.',
generateName: 'Generate Fake Name', generateName: 'Generate Fake Name',
}, },
@@ -127,7 +127,7 @@ const { t } = useI18n({
copy: '复制', copy: '复制',
copied: '已复制', copied: '已复制',
showPassword: '查看密码', showPassword: '查看密码',
fetchAddressError: '获取地址失败, 请检查你的 jwt 是否有效 或 网络是否正常。', fetchAddressError: '登录密码无效或账号不存在,也可能是网络连接异常,请稍后再尝试。',
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。', mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
generateName: '生成随机名字', generateName: '生成随机名字',
} }
@@ -223,6 +223,7 @@ const menuOptions = computed(() => [
}, },
{ default: () => t('settings') } { default: () => t('settings') }
), ),
show: openSettings.value.enableAutoReply,
key: "settings" key: "settings"
}, },
{ {
@@ -249,6 +250,7 @@ const menuOptions = computed(() => [
}, },
{ default: () => t('delteAccount') } { default: () => t('delteAccount') }
), ),
show: openSettings.value.enableUserDeleteEmail,
key: "delte_account" key: "delte_account"
} }
] ]
+17 -7
View File
@@ -11,8 +11,9 @@ import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
const message = useMessage() const message = useMessage()
const isMobile = useIsMobile() const isMobile = useIsMobile()
const { settings, themeSwitch } = useGlobalState() const { settings, openSettings, themeSwitch } = useGlobalState()
const autoRefresh = ref(false) const autoRefresh = ref(false)
const autoRefreshInterval = ref(30)
const data = ref([]) const data = ref([])
const timer = ref(null) const timer = ref(null)
@@ -29,6 +30,7 @@ const { t } = useI18n({
messages: { messages: {
en: { en: {
autoRefresh: 'Auto Refresh', autoRefresh: 'Auto Refresh',
refreshAfter: 'Refresh After {msg} Seconds',
refresh: 'Refresh', refresh: 'Refresh',
attachments: 'Show Attachments', attachments: 'Show Attachments',
downloadMail: 'Download Mail', downloadMail: 'Download Mail',
@@ -38,6 +40,7 @@ const { t } = useI18n({
}, },
zh: { zh: {
autoRefresh: '自动刷新', autoRefresh: '自动刷新',
refreshAfter: '{msg}秒后刷新',
refresh: '刷新', refresh: '刷新',
downloadMail: '下载邮件', downloadMail: '下载邮件',
attachments: '查看附件', attachments: '查看附件',
@@ -49,10 +52,16 @@ const { t } = useI18n({
}); });
const setupAutoRefresh = async (autoRefresh) => { const setupAutoRefresh = async (autoRefresh) => {
// auto refresh every 30 seconds
autoRefreshInterval.value = 30;
if (autoRefresh) { if (autoRefresh) {
timer.value = setInterval(async () => { timer.value = setInterval(async () => {
autoRefreshInterval.value--;
if (autoRefreshInterval.value <= 0) {
autoRefreshInterval.value = 30;
await refresh(); await refresh();
}, 30000) }
}, 1000)
} else { } else {
clearInterval(timer.value) clearInterval(timer.value)
timer.value = null timer.value = null
@@ -125,19 +134,20 @@ onMounted(async () => {
<template> <template>
<div> <div>
<n-layout 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"> <n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25" :default-size="0.3">
<template #1> <template #1>
<div class="center"> <div class="center">
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;"> <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" /> <n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
</div> </div>
<n-switch v-model:value="autoRefresh" size="small"> <n-switch v-model:value="autoRefresh" size="small" :round="false">
<template #checked> <template #checked>
{{ t('autoRefresh') }} {{ t('refreshAfter', { msg: autoRefreshInterval }) }}
</template> </template>
<template #unchecked> <template #unchecked>
{{ t('autoRefresh') }} {{ t('autoRefresh') }}
</template></n-switch> </template>
</n-switch>
<n-button @click="refresh" size="small" type="primary"> <n-button @click="refresh" size="small" type="primary">
{{ t('refresh') }} {{ t('refresh') }}
</n-button> </n-button>
@@ -175,7 +185,7 @@ onMounted(async () => {
<n-tag type="info"> <n-tag type="info">
FROM: {{ curMail.source }} FROM: {{ curMail.source }}
</n-tag> </n-tag>
<n-popconfirm @positive-click="deleteMail"> <n-popconfirm v-if="openSettings.enableUserDeleteEmail" @positive-click="deleteMail">
<template #trigger> <template #trigger>
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button> <n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
</template> </template>
+4
View File
@@ -73,6 +73,10 @@ PREFIX = "tmp" # The mailbox name prefix to be processed
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # your domain name DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # your domain name
JWT_SECRET = "xxx" # Key used to generate jwt JWT_SECRET = "xxx" # Key used to generate jwt
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
# Allow users to delete messages
ENABLE_USER_DELETE_EMAIL = true
# Allow automatic replies to emails
ENABLE_AUTO_REPLY = false
# default send balance, if not set, it will be 0 # default send balance, if not set, it will be 0
# DEFAULT_SEND_BALANCE = 1 # DEFAULT_SEND_BALANCE = 1
# dkim config # dkim config
@@ -31,6 +31,10 @@ PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名 DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权 JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔 BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
# 允许用户删除邮件, 不配置则不允许
ENABLE_USER_DELETE_EMAIL = true
# 允许自动回复邮件
ENABLE_AUTO_REPLY = false
# 默认发送邮件余额,如果不设置,将为 0 # 默认发送邮件余额,如果不设置,将为 0
# DEFAULT_SEND_BALANCE = 1 # DEFAULT_SEND_BALANCE = 1
# dkim config # dkim config
+2
View File
@@ -22,6 +22,7 @@ async function email(message, env, ctx) {
} }
// auto reply email // auto reply email
if (env.ENABLE_AUTO_REPLY) {
try { try {
const results = await env.DB.prepare( const results = await env.DB.prepare(
`SELECT * FROM auto_reply_mails where address = ? and enabled = 1` `SELECT * FROM auto_reply_mails where address = ? and enabled = 1`
@@ -50,6 +51,7 @@ async function email(message, env, ctx) {
} catch (error) { } catch (error) {
console.log("reply email error", error); console.log("reply email error", error);
} }
}
} else { } else {
message.setReject(`Unknown address ${message.to}`); message.setReject(`Unknown address ${message.to}`);
console.log(`Unknown address ${message.to}`); console.log(`Unknown address ${message.to}`);
+13
View File
@@ -34,6 +34,9 @@ api.get('/api/mails', async (c) => {
}) })
api.delete('/api/mails/:id', async (c) => { api.delete('/api/mails/:id', async (c) => {
if (c.env.ENABLE_USER_DELETE_EMAIL) {
return c.text("User delete email is disabled", 403)
}
const { address } = c.get("jwtPayload") const { address } = c.get("jwtPayload")
const { id } = c.req.param(); const { id } = c.req.param();
const { success } = await c.env.DB.prepare( const { success } = await c.env.DB.prepare(
@@ -82,6 +85,7 @@ api.get('/api/settings', async (c) => {
} }
} }
let auto_reply = {}; let auto_reply = {};
if (c.env.ENABLE_AUTO_REPLY) {
const results = await c.env.DB.prepare( const results = await c.env.DB.prepare(
`SELECT * FROM auto_reply_mails where address = ? ` `SELECT * FROM auto_reply_mails where address = ? `
).bind(address).first(); ).bind(address).first();
@@ -94,6 +98,7 @@ api.get('/api/settings', async (c) => {
name: results.name, name: results.name,
} }
} }
}
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();
@@ -111,6 +116,9 @@ api.get('/api/settings', async (c) => {
api.post('/api/settings', async (c) => { api.post('/api/settings', async (c) => {
const { address } = c.get("jwtPayload") const { address } = c.get("jwtPayload")
if (!c.env.ENABLE_AUTO_REPLY) {
return c.text("Auto reply is disabled", 403)
}
const { auto_reply } = await c.req.json(); const { auto_reply } = await c.req.json();
const { name, subject, source_prefix, message, enabled } = auto_reply; const { name, subject, source_prefix, message, enabled } = auto_reply;
if ((!subject || !message) && enabled) { if ((!subject || !message) && enabled) {
@@ -147,6 +155,8 @@ api.get('/open_api/settings', async (c) => {
"domains": getDomains(c), "domains": getDomains(c),
"needAuth": needAuth, "needAuth": needAuth,
"adminContact": c.env.ADMIN_CONTACT, "adminContact": c.env.ADMIN_CONTACT,
"enableUserDeleteEmail": c.env.ENABLE_USER_DELETE_EMAIL,
"enableAutoReply": c.env.ENABLE_AUTO_REPLY,
}); });
}) })
@@ -204,6 +214,9 @@ api.get('/api/new_address', async (c) => {
}) })
api.delete('/api/delete_address', async (c) => { api.delete('/api/delete_address', async (c) => {
if (c.env.ENABLE_USER_DELETE_EMAIL) {
return c.text("User delete email is disabled", 403)
}
const { address } = c.get("jwtPayload") const { address } = c.get("jwtPayload")
let name = address; let name = address;
if (address.startsWith(c.env.PREFIX)) { if (address.startsWith(c.env.PREFIX)) {
+6 -1
View File
@@ -18,7 +18,12 @@ PREFIX = "tmp"
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
JWT_SECRET = "xxx" JWT_SECRET = "xxx"
BLACK_LIST = "" BLACK_LIST = ""
# DEFAULT_SEND_BALANCE = 1 # default send balance, if not set, it will be 0 # Allow users to delete messages
ENABLE_USER_DELETE_EMAIL = true
# Allow automatic replies to emails
ENABLE_AUTO_REPLY = false
# default send balance, if not set, it will be 0
# DEFAULT_SEND_BALANCE = 1
# dkim config # dkim config
# DKIM_SELECTOR = "" # DKIM_SELECTOR = ""
# DKIM_PRIVATE_KEY = "" # DKIM_PRIVATE_KEY = ""