From eafcf00e5e3c162afe64e3d23ee13c13ac71dd62 Mon Sep 17 00:00:00 2001 From: Dream Hunter Date: Mon, 15 Apr 2024 13:20:22 +0800 Subject: [PATCH] feat: support DKIM (#129) --- README.md | 37 +++++-- README_EN.md | 115 --------------------- vitepress-docs/docs/.vitepress/zh.ts | 2 + vitepress-docs/docs/en/cli.md | 27 +++++ vitepress-docs/docs/zh/guide/cli/worker.md | 5 +- vitepress-docs/docs/zh/guide/dkim.md | 31 ++++++ worker/src/admin_api.js | 4 +- worker/src/send_mail_api.js | 22 +++- worker/wrangler.toml.template | 3 + 9 files changed, 117 insertions(+), 129 deletions(-) delete mode 100644 README_EN.md create mode 100644 vitepress-docs/docs/zh/guide/dkim.md diff --git a/README.md b/README.md index fa2b7b08..67a4ee79 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,13 @@ # 使用 cloudflare 免费服务,搭建临时邮箱 -## [English](README_EN.md) - ## [查看部署文档](https://temp-mail-docs.awsl.uk) +## [English](https://temp-mail-docs.awsl.uk/en/) + ## [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/) ![](https://uptime.aks.awsl.icu/api/badge/10/status) ![](https://uptime.aks.awsl.icu/api/badge/10/uptime) @@ -34,8 +31,8 @@ - [使用 cloudflare 免费服务,搭建临时邮箱](#使用-cloudflare-免费服务搭建临时邮箱) - - [English](#english) - [查看部署文档](#查看部署文档) + - [English](#english) - [CHANGELOG](#changelog) - [在线演示](#在线演示) - [功能/TODO](#功能todo) @@ -47,6 +44,7 @@ - [Cloudflare Email Routing](#cloudflare-email-routing) - [Cloudflare Pages 前端](#cloudflare-pages-前端) - [配置发送邮件](#配置发送邮件) + - [配置 DKIM](#配置-dkim) - [参考资料](#参考资料) ## 功能/TODO @@ -63,6 +61,7 @@ - [x] 增加查看附件功能 - [x] 使用 rust wasm 解析邮件 - [x] 支持发送邮件 +- [x] 支持 DKIM --- @@ -147,6 +146,9 @@ PREFIX = "tmp" # 要处理的邮箱名称前缀 DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名 JWT_SECRET = "xxx" # 用于生成 jwt 的密钥 BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔 +# dkim config +# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels +# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容 [[d1_databases]] binding = "DB" @@ -215,6 +217,29 @@ v=spf1 include:_spf.mx.cloudflare.net include:relay.mailchannels.net ~all - 此处 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=` + ## 参考资料 - https://developers.cloudflare.com/d1/ diff --git a/README_EN.md b/README_EN.md deleted file mode 100644 index a8986208..00000000 --- a/README_EN.md +++ /dev/null @@ -1,115 +0,0 @@ -# cloudflare temp email - -## [中文](README.md) - -[CHANGELOG](CHANGELOG.md) - -## [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. - -## Features - -- [x] Cloudflare D1 as a database -- [x] Deploy the front end with Cloudflare Pages -- [x] Deploy the backend with Cloudflare Workers -- [x] Email forwarding using Cloudflare Email Routing -- [x] Use password to login to the previous mailbox again. -- [x] Get Custom Name Email -- [x] Support multiple languages -- [x] Add access authorization, which can be used as a private site -- [x] Add auto reply feature -- [x] Add attachment viewing function -- [x] use rust wasm to parse email -- [x] support send email - -![demo](vitepress-docs/docs/public/readme_assets/demo.png) - -## Deploy - -[Install/Update Wrangler](https://developers.cloudflare.com/workers/wrangler/install-and-update/) - -```bash -npm install wrangler -g -git clone https://github.com/dreamhunter2333/cloudflare_temp_email.git -# Switch to the latest tag or the branch you want to deploy. You can also use the main branch directly. -# git checkout $(git describe --tags $(git rev-list --tags --max-count=1)) -``` - -## DB - Cloudflare D1 - -```bash -# create a database, and copy the output to wrangler.toml in the next step -wrangler d1 create dev -wrangler d1 execute dev --file=db/schema.sql -# schema update, if you have initialized the database before this date, you can execute this command to update -# wrangler d1 execute dev --file=db/2024-01-13-patch.sql -# wrangler d1 execute dev --file=db/2024-04-03-patch.sql -``` - -![d1](vitepress-docs/docs/public/readme_assets/d1.png) - -### Backend - Cloudflare workers - -The first deployment will prompt you to create a project. Please fill in `production` for the `production` branch. - -```bash -cd worker -pnpm install -# copy wrangler.toml.template to wrangler.toml -# and add your d1 config and these config -# PREFIX = "tmp" - the email create will be like tmp@DOMAIN -# IF YOU WANT TO MAKE YOUR SITE PRIVATE, UNCOMMENT THE FOLLOWING LINES -# PASSWORDS = ["123", "456"] -# For admin panel, if not set will no allow to access the admin panel -# ADMIN_PASSWORDS = ["123", "456"] -# DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] you domain name -# JWT_SECRET = "xxx" -# BLACK_LIST = "" -cp wrangler.toml.template wrangler.toml -# deploy -pnpm run deploy -``` - -you can find and test the worker's url in the workers dashboard - -![worker](vitepress-docs/docs/public/readme_assets/worker.png) - -## 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 - -![email](vitepress-docs/docs/public/readme_assets/email.png) - -### Frontend - Cloudflare pages - -The first deployment will prompt you to create a project. Please fill in `production` for the `production` branch. - -```bash -cd frontend -pnpm install -# add .env.local and modify VITE_API_BASE to your worker's url -# VITE_API_BASE=https://xxx.xxx.workers.dev - don't put / in the end -cp .env.example .env.local -pnpm build --emptyOutDir -pnpm run deploy -``` - -![pages](vitepress-docs/docs/public/readme_assets/pages.png) - -## 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` diff --git a/vitepress-docs/docs/.vitepress/zh.ts b/vitepress-docs/docs/.vitepress/zh.ts index cf55daa6..4ca67b41 100644 --- a/vitepress-docs/docs/.vitepress/zh.ts +++ b/vitepress-docs/docs/.vitepress/zh.ts @@ -100,6 +100,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] { items: [ { text: '命令行部署准备', link: 'cli/pre-requisite' }, { text: 'D1 数据库', link: 'cli/d1' }, + { text: '配置 DKIM', link: 'dkim' }, { text: 'Cloudflare workers 后端', link: 'cli/worker' }, { text: '配置邮件转发', link: 'email-routing.md' }, { text: 'Cloudflare Pages 前端', link: 'cli/pages' }, @@ -111,6 +112,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] { collapsed: false, items: [ { text: 'D1 数据库', link: 'ui/d1' }, + { text: '配置 DKIM', link: 'dkim' }, { text: 'Cloudflare workers 后端', link: 'ui/worker' }, { text: '配置邮件转发', link: 'email-routing.md' }, { text: 'Cloudflare Pages 前端', link: 'ui/pages' }, diff --git a/vitepress-docs/docs/en/cli.md b/vitepress-docs/docs/en/cli.md index a215f527..cbdd3e6d 100644 --- a/vitepress-docs/docs/en/cli.md +++ b/vitepress-docs/docs/en/cli.md @@ -1,5 +1,7 @@ # cloudflare temp email +This is a temporary email service that uses Cloudflare Workers to create a temporary email address and view the received email in web browser. + ## Features - [x] Cloudflare D1 as a database @@ -14,6 +16,7 @@ - [x] Add attachment viewing function - [x] use rust wasm to parse email - [x] support send email +- [x] support DKIM ## Deploy @@ -101,3 +104,27 @@ Create a new `_mailchannels` record, the type is `TXT`, the content is `v=mc1 cf - The worker domain name here is the domain name of the back-end api. For example, if I deploy it at `https://temp-email-api.awsl.uk/`, fill in `v=mc1 cfid=awsl.uk` - If your domain name is `https://temp-email-api.xxx.workers.dev`, fill in `v=mc1 cfid=xxx.workers.dev` + + +## Configure DKIM + +Ref: [Adding-a-DKIM-Signature](https://support.mailchannels.com/hc/en-us/articles/7122849237389-Adding-a-DKIM-Signature) + +Creating a DKIM private and public key: +Private key as PEM file and base64 encoded txt file: + +```bash +openssl genrsa 2048 | tee priv_key.pem | openssl rsa -outform der | openssl base64 -A > priv_key.txt +``` + +Public key as DNS record: + +```bash +echo -n "v=DKIM1;p=" > pub_key_record.txt && \ +openssl rsa -in priv_key.pem -pubout -outform der | openssl base64 -A >> pub_key_record.txt +``` + +Add `TXT` record in `Cloudflare` all your mail domain `DNS` + +- `_dmarc`: `v=DMARC1; p=none; adkim=r; aspf=r;` +- `mailchannels._domainkey`: `v=DKIM1; p=` diff --git a/vitepress-docs/docs/zh/guide/cli/worker.md b/vitepress-docs/docs/zh/guide/cli/worker.md index 07acbad9..0cc6e4a1 100644 --- a/vitepress-docs/docs/zh/guide/cli/worker.md +++ b/vitepress-docs/docs/zh/guide/cli/worker.md @@ -10,7 +10,7 @@ cp wrangler.toml.template wrangler.toml ## 修改 `wrangler.toml` 配置文件 -```bash +```toml name = "cloudflare_temp_email" main = "src/worker.js" compatibility_date = "2023-12-01" @@ -29,6 +29,9 @@ PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空 DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名 JWT_SECRET = "xxx" # 用于生成 jwt 的密钥 BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔 +# dkim config +# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels +# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容 # D1 数据库的名称和 ID 可以在 cloudflare 控制台查看 [[d1_databases]] diff --git a/vitepress-docs/docs/zh/guide/dkim.md b/vitepress-docs/docs/zh/guide/dkim.md new file mode 100644 index 00000000..603bad84 --- /dev/null +++ b/vitepress-docs/docs/zh/guide/dkim.md @@ -0,0 +1,31 @@ +# 配置 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=` + +那我在 `wrangler.toml` 中的配置应该是这样的: + +```toml +DKIM_SELECTOR = "mailchannels" +DKIM_PRIVATE_KEY = "" +``` diff --git a/worker/src/admin_api.js b/worker/src/admin_api.js index b1526436..42ddd543 100644 --- a/worker/src/admin_api.js +++ b/worker/src/admin_api.js @@ -96,7 +96,7 @@ api.get('/admin/mails', async (c) => { return c.text("Invalid offset", 400) } const { results } = await c.env.DB.prepare( - `SELECT id, source, raw, created_at FROM raw_mails where address = ? order by id desc limit ? offset ?` + `SELECT * FROM raw_mails where address = ? order by id desc limit ? offset ?` ).bind(address, limit, offset).all(); let count = 0; if (offset == 0) { @@ -120,7 +120,7 @@ api.get('/admin/mails_unknow', async (c) => { return c.text("Invalid offset", 400) } const { results } = await c.env.DB.prepare(` - SELECT id, source, raw, created_at FROM raw_mails + SELECT * FROM raw_mails where address NOT IN(select concat('${c.env.PREFIX}', name) from address) order by id desc limit ? offset ? ` ).bind(limit, offset).all(); diff --git a/worker/src/send_mail_api.js b/worker/src/send_mail_api.js index 26f6436b..884562be 100644 --- a/worker/src/send_mail_api.js +++ b/worker/src/send_mail_api.js @@ -52,13 +52,22 @@ api.post('/api/send_mail', async (c) => { if (!content) { return c.text("Invalid content", 400) } - const body = JSON.stringify({ + let dmikBody = {} + if (c.env.DKIM_SELECTOR && c.env.DKIM_PRIVATE_KEY && address.includes("@")) { + dmikBody = { + "dkim_domain": address.split("@")[1], + "dkim_selector": c.env.DKIM_SELECTOR, + "dkim_private_key": c.env.DKIM_PRIVATE_KEY, + } + } + const body = { "personalizations": [ { "to": [{ "email": to_mail, "name": to_name, - }] + }], + ...dmikBody, } ], "from": { @@ -70,13 +79,13 @@ api.post('/api/send_mail', async (c) => { "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, + "body": JSON.stringify(body), }); const resp = await fetch(send_request); const respText = await resp.text(); @@ -98,9 +107,12 @@ api.post('/api/send_mail', async (c) => { } // save to sendbox try { + if (body?.personalizations?.[0]?.dkim_private_key) { + delete body.personalizations[0].dkim_private_key; + } const { success: success2 } = await c.env.DB.prepare( `INSERT INTO sendbox (address, raw) VALUES (?, ?)` - ).bind(address, body).run(); + ).bind(address, JSON.stringify(body)).run(); if (!success2) { console.warn(`Failed to save to sendbox for ${address}`); } diff --git a/worker/wrangler.toml.template b/worker/wrangler.toml.template index a1137659..bebbb272 100644 --- a/worker/wrangler.toml.template +++ b/worker/wrangler.toml.template @@ -16,6 +16,9 @@ PREFIX = "tmp" DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] JWT_SECRET = "xxx" BLACK_LIST = "" +# dkim config +# DKIM_SELECTOR = "" +# DKIM_PRIVATE_KEY = "" [[d1_databases]] binding = "DB"