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

210
README.md
View File

@@ -2,7 +2,7 @@
## [查看部署文档](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)
@@ -21,20 +21,10 @@
- [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱)
- [查看部署文档](#查看部署文档)
- [English](#english)
- [English Docs](#english-docs)
- [CHANGELOG](#changelog)
- [在线演示](#在线演示)
- [功能/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
@@ -51,199 +41,3 @@
- [x] 使用 rust wasm 解析邮件
- [x] 支持发送邮件
- [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/

View File

@@ -59,6 +59,8 @@ const getOpenSettings = async (message) => {
}
}),
adminContact: res["adminContact"] || "",
enableUserDeleteEmail: res["enableUserDeleteEmail"] || false,
enableAutoReply: res["enableAutoReply"] || false,
};
if (openSettings.value.needAuth) {
showAuth.value = true;

View File

@@ -11,6 +11,8 @@ export const useGlobalState = createGlobalState(
prefix: '',
needAuth: false,
adminContact: '',
enableUserDeleteEmail: false,
enableAutoReply: false,
domains: [{
label: 'test.com',
value: 'test.com'

View File

@@ -93,7 +93,7 @@ const { t } = useI18n({
copy: 'Copy',
copied: 'Copied',
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.',
generateName: 'Generate Fake Name',
},
@@ -127,7 +127,7 @@ const { t } = useI18n({
copy: '复制',
copied: '已复制',
showPassword: '查看密码',
fetchAddressError: '获取地址失败, 请检查你的 jwt 是否有效 或 网络是否正常。',
fetchAddressError: '登录密码无效或账号不存在,也可能是网络连接异常,请稍后再尝试。',
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
generateName: '生成随机名字',
}
@@ -223,6 +223,7 @@ const menuOptions = computed(() => [
},
{ default: () => t('settings') }
),
show: openSettings.value.enableAutoReply,
key: "settings"
},
{
@@ -249,6 +250,7 @@ const menuOptions = computed(() => [
},
{ default: () => t('delteAccount') }
),
show: openSettings.value.enableUserDeleteEmail,
key: "delte_account"
}
]

View File

@@ -11,8 +11,9 @@ import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
const message = useMessage()
const isMobile = useIsMobile()
const { settings, themeSwitch } = useGlobalState()
const { settings, openSettings, themeSwitch } = useGlobalState()
const autoRefresh = ref(false)
const autoRefreshInterval = ref(30)
const data = ref([])
const timer = ref(null)
@@ -29,6 +30,7 @@ const { t } = useI18n({
messages: {
en: {
autoRefresh: 'Auto Refresh',
refreshAfter: 'Refresh After {msg} Seconds',
refresh: 'Refresh',
attachments: 'Show Attachments',
downloadMail: 'Download Mail',
@@ -38,6 +40,7 @@ const { t } = useI18n({
},
zh: {
autoRefresh: '自动刷新',
refreshAfter: '{msg}秒后刷新',
refresh: '刷新',
downloadMail: '下载邮件',
attachments: '查看附件',
@@ -49,10 +52,16 @@ const { t } = useI18n({
});
const setupAutoRefresh = async (autoRefresh) => {
// auto refresh every 30 seconds
autoRefreshInterval.value = 30;
if (autoRefresh) {
timer.value = setInterval(async () => {
await refresh();
}, 30000)
autoRefreshInterval.value--;
if (autoRefreshInterval.value <= 0) {
autoRefreshInterval.value = 30;
await refresh();
}
}, 1000)
} else {
clearInterval(timer.value)
timer.value = null
@@ -125,19 +134,20 @@ onMounted(async () => {
<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">
<n-split class="left" v-if="!isMobile" direction="horizontal" :max="0.75" :min="0.25" :default-size="0.3">
<template #1>
<div class="center">
<div style="display: inline-block; margin-top: 10px; margin-bottom: 10px;">
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count" simple size="small" />
</div>
<n-switch v-model:value="autoRefresh" size="small">
<n-switch v-model:value="autoRefresh" size="small" :round="false">
<template #checked>
{{ t('autoRefresh') }}
{{ t('refreshAfter', { msg: autoRefreshInterval }) }}
</template>
<template #unchecked>
{{ t('autoRefresh') }}
</template></n-switch>
</template>
</n-switch>
<n-button @click="refresh" size="small" type="primary">
{{ t('refresh') }}
</n-button>
@@ -175,7 +185,7 @@ onMounted(async () => {
<n-tag type="info">
FROM: {{ curMail.source }}
</n-tag>
<n-popconfirm @positive-click="deleteMail">
<n-popconfirm v-if="openSettings.enableUserDeleteEmail" @positive-click="deleteMail">
<template #trigger>
<n-button tertiary type="error" size="small">{{ t('delete') }}</n-button>
</template>

View File

@@ -73,6 +73,10 @@ PREFIX = "tmp" # The mailbox name prefix to be processed
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # your domain name
JWT_SECRET = "xxx" # Key used to generate jwt
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
# Allow users to 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

View File

@@ -31,6 +31,10 @@ PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
# 允许用户删除邮件, 不配置则不允许
ENABLE_USER_DELETE_EMAIL = true
# 允许自动回复邮件
ENABLE_AUTO_REPLY = false
# 默认发送邮件余额,如果不设置,将为 0
# DEFAULT_SEND_BALANCE = 1
# dkim config

View File

@@ -22,33 +22,35 @@ async function email(message, env, ctx) {
}
// auto reply email
try {
const results = await env.DB.prepare(
`SELECT * FROM auto_reply_mails where address = ? and enabled = 1`
).bind(message.to).first();
if (results && results.source_prefix && message.from.startsWith(results.source_prefix)) {
const msg = createMimeMessage();
msg.setHeader("In-Reply-To", message.headers.get("Message-ID"));
msg.setSender({
name: results.name || results.address,
addr: results.address
});
msg.setRecipient(message.from);
msg.setSubject(results.subject || "Auto-reply");
msg.addMessage({
contentType: 'text/plain',
data: results.message || "This is an auto-reply message, please reconact later."
});
if (env.ENABLE_AUTO_REPLY) {
try {
const results = await env.DB.prepare(
`SELECT * FROM auto_reply_mails where address = ? and enabled = 1`
).bind(message.to).first();
if (results && results.source_prefix && message.from.startsWith(results.source_prefix)) {
const msg = createMimeMessage();
msg.setHeader("In-Reply-To", message.headers.get("Message-ID"));
msg.setSender({
name: results.name || results.address,
addr: results.address
});
msg.setRecipient(message.from);
msg.setSubject(results.subject || "Auto-reply");
msg.addMessage({
contentType: 'text/plain',
data: results.message || "This is an auto-reply message, please reconact later."
});
const replyMessage = new EmailMessage(
message.to,
message.from,
msg.asRaw()
);
await message.reply(replyMessage);
const replyMessage = new EmailMessage(
message.to,
message.from,
msg.asRaw()
);
await message.reply(replyMessage);
}
} catch (error) {
console.log("reply email error", error);
}
} catch (error) {
console.log("reply email error", error);
}
} else {
message.setReject(`Unknown address ${message.to}`);

View File

@@ -34,6 +34,9 @@ api.get('/api/mails', 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 { id } = c.req.param();
const { success } = await c.env.DB.prepare(
@@ -82,16 +85,18 @@ api.get('/api/settings', async (c) => {
}
}
let auto_reply = {};
const results = await c.env.DB.prepare(
`SELECT * FROM auto_reply_mails where address = ? `
).bind(address).first();
if (results) {
auto_reply = {
subject: results.subject,
message: results.message,
enabled: results.enabled == 1,
source_prefix: results.source_prefix,
name: results.name,
if (c.env.ENABLE_AUTO_REPLY) {
const results = await c.env.DB.prepare(
`SELECT * FROM auto_reply_mails where address = ? `
).bind(address).first();
if (results) {
auto_reply = {
subject: results.subject,
message: results.message,
enabled: results.enabled == 1,
source_prefix: results.source_prefix,
name: results.name,
}
}
}
const { count: mailCountV1 } = await c.env.DB.prepare(
@@ -111,6 +116,9 @@ api.get('/api/settings', async (c) => {
api.post('/api/settings', async (c) => {
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 { name, subject, source_prefix, message, enabled } = auto_reply;
if ((!subject || !message) && enabled) {
@@ -147,6 +155,8 @@ api.get('/open_api/settings', async (c) => {
"domains": getDomains(c),
"needAuth": needAuth,
"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) => {
if (c.env.ENABLE_USER_DELETE_EMAIL) {
return c.text("User delete email is disabled", 403)
}
const { address } = c.get("jwtPayload")
let name = address;
if (address.startsWith(c.env.PREFIX)) {

View File

@@ -18,7 +18,12 @@ PREFIX = "tmp"
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
JWT_SECRET = "xxx"
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_SELECTOR = ""
# DKIM_PRIVATE_KEY = ""