diff --git a/README.md b/README.md index a27dba3..9852185 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,12 @@ - 😄 支持多域名后缀 - 🚀 快速部署,无需服务器 -收件原理: +原理: - Email worker 接收电子邮件 - 前端显示电子邮件(remix) - 邮件存储(sqlite) - -发件原理: - -- 手打中... +- 发信使用 MailChannel API ## 👋 自部署教程 diff --git a/docs/receive-tutorial-en.md b/docs/receive-tutorial-en.md index 4fee141..6883030 100644 --- a/docs/receive-tutorial-en.md +++ b/docs/receive-tutorial-en.md @@ -1,4 +1,4 @@ -## Receive email steps +## Email receiving tutorial **1.Register a [turso](https://turso.tech) account, create a database, and create an emails table** diff --git a/docs/send-tutorial-en.md b/docs/send-tutorial-en.md index fc1e255..70e5b14 100644 --- a/docs/send-tutorial-en.md +++ b/docs/send-tutorial-en.md @@ -1,3 +1,215 @@ -## Send email steps +## Email Sending Tutorial -> working... \ No newline at end of file +### Preparation Work + +- Continue to use the domain hosted on cf from the email receiving tutorial. +- DKIM (optional) + +### 1. Create a Sending Worker + +Here we will manually create it: + +- Go to the Cloudflare dashboard at `https://dash.cloudflare.com/`, click on `Workers and Pages` in the sidebar. +- Click `Create an app` -> `Create a worker` -> Name it as you wish, for example, `sender` -> Click `Deploy`. +- Enter the worker and click `Edit code`. Paste the following [code](/docs/send-worker.js): + +
+View sender worker code +
/**
+ * Welcome to Cloudflare Workers! This is your first worker.
+ *
+ * - Run "npm run dev" in your terminal to start a development server
+ * - Open a browser tab at http://localhost:8787/ to see your worker in action
+ * - Run "npm run deploy" to publish your worker
+ *
+ * Learn more at https://developers.cloudflare.com/workers/ 
+ */
+async function readRequestBody(request, env) {
+  const { headers } = request;
+  const contentType = headers.get("content-type") || "";
+  if (contentType.includes("application/json")) {
+    return JSON.stringify(await request.json());
+  } else if (contentType.includes("form")) {
+    const formData = await request.formData();
+    const body = {};
+    for (const entry of formData.entries()) {
+      body[entry[0]] = entry[1];
+    }
+    let data = JSON.parse(JSON.stringify(body));
+    let combine = `{"personalizations":[{"to":[{"email":"${data.to}","name":"${data.ton}"}],"dkim_domain":"${env.DKIM_DOMAIN}","dkim_selector":"${env.DKIM_SELECTOR}","dkim_private_key":"${env.DKIM_PRIVATE_KEY}"}],"from":{"email":"${data.from}","name":"${data.fromn}"},"reply_to":{"email":"${data.rep}","name":"${data.repn}"},"subject":"${data.sbj}","content":[{"type":"${data.type}","value":"${data.body}"}]}`;
+    return combine;
+  } else {
+    return '{"success":false}';
+  }
+}
+async function handleRequest(request, env) {
+  let start = Date.now();
+  let reqBody = await readRequestBody(request, env);
+  let send_request = new Request("https://api.mailchannels.net/tx/v1/send",  {
+    method: "POST",
+    headers: {
+      "content-type": "application/json",
+    },
+    body: reqBody,
+  });
+  let resp = await fetch(send_request);
+  let respText = await resp.text();
+  let end = Date.now();
+  let total = end - start;
+  return new Response(respText, {
+    headers: {
+      "X-MC-Status": resp.status,
+      "X-Response-Time": total,
+    },
+  });
+}
+const htmlForm = `
+
+
+
+Submit your email
+
+
+(*) is required
+
+
+
+
+
+
+
+
+
+
+ +
+ +`; +export default { + async fetch(request, env, ctx) { + const { url } = request; + if (url.includes("submit")) { + return new Response(htmlForm, { + headers: { "Content-Type": "text/html" }, + }); + } + if (request.method === "POST") { + return handleRequest(request, env); + } else if (request.method === "GET") { + return new Response(`The request was a GET`); + } + }, +}; +
+
+ +Continue to save and deploy. + +- In the worker's `Settings` -> `Variables`, add the following variables: + +| Variable Name | Description | Example | +| ---------------- | ------------------------------------ | --------------------- | +| DKIM_DOMAIN | Required, domain suffix | `vmail.dev` | +| DKIM_PRIVATE_KEY | Required, DKIM private key | `MIIEpQIBAAKCAQEA...` | +| DKIM_SELECTOR | Required, fixed value `mailchannels` | `mailchannels` | + +The `DKIM_PRIVATE_KEY` will be explained later on how to obtain it. + +Remember the address assigned to the worker, i.e., `https://..workers.dev`, which will be used later. + +### 2. Prepare DKIM Private Key (optional) + +> DKIM is an email authentication technology that can verify the identity of the email sender. + +Execute the following command in the terminal to generate a DKIM private key, refer to [Adding a DKIM Signature](https://support.mailchannels.com/hc/en-us/articles/7122849237389-Adding-a-DKIM-Signature): + +```bash +openssl genrsa 2048 | tee private_key.pem | openssl rsa -outform der | openssl base64 -A > private_key.txt + +echo -n "v=DKIM1;p=" > dkim_record.txt && openssl rsa -in private_key.pem -pubout -outform der | openssl base64 -A >> dkim_record.txt +``` + +These two commands will generate 3 files: + +- `private_key.pem`: Private key file. +- `private_key.txt`: Private key file in base64 encoding. +- `dkim_record.txt`: DKIM record. + +The content of the `private_key.txt` file is the value for the `DKIM_PRIVATE_KEY` variable in the previous step. + +### 3. Configure DNS + +Enter the Cloudflare domain control console, select your domain -> Click `DNS`, next, you need to **add 3 TXT records** and **modify 1 TXT record**. + +The new ones are as follows: + +| Type | Name | Value | Remarks | +| ---- | ------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------ | +| TXT | `_dmarc` | `v=DMARC1; p=reject; adkim=s; aspf=s; rua=mailto:YYY; ruf=mailto:YYY pct=100; fo=1;` | Fixed value | +| TXT | `mailchannels._domainkey` | `v=DKIM1;p=MIIEpQIBAAKCAQEA...` | The value is the content of the `dkim_record.txt` file | +| TXT | `_mailchannels` | `v=mc1 cfid=yesmore.workers.dev` | Replace yesmore with your account name | + +The one that needs modification (the TXT record value is `example.com`, i.e., your domain name): + +| Type | Name | Value | Remarks | +| ---- | ------------- | --------------------------------------------------------------------------- | ----------------------------------------------------- | +| TXT | `example.com` | `v=spf1 include:_spf.mx.cloudflare.net include:relay.mailchannels.net ~all` | Replace the value with this, leave the rest unchanged | + +In the end, there will be a total of 4 TXT records: + +![](https://img.inke.app/file/b7422917c667de620ae95.png) + +Verify if the DKIM record is configured successfully at [dkim-record-checker](https://dmarcly.com/tools/dkim-record-checker). + +### 4. Verify Sending + +Method one: Open the webpage `https://..workers.dev/submit` to verify if sending is normal. + +Method two: Send via API + +- Request URL `https://..workers.dev` +- Request Method `POST` +- Request Body: + +```json +{ + "from": { + "email": "sender@vmail.dev", + "name": "Sender" + }, + "personalizations": [ + { + "to": [ + { + "email": "example@gmail.com", + "name": "Recipient" + } + ] + } + ], + "subject": "Email Subject", + "content": [ + { + "type": "text/plain", + "value": "Email Content" + } + ] +} +``` + +Example: + +![](https://img.inke.app/file/1f6f3ab53aff9a1855475.png) + +### 5. Add Environment Variables + +This project uses the method two from step four to send emails via API. + +In the Vercel project console, go to `Settings` -> `Environment Variables`, and add the following variable: + +- SEND_WORKER_URL: The value should be `https://..workers.dev` + +After adding, trigger a redeployment. \ No newline at end of file diff --git a/docs/send-tutorial.md b/docs/send-tutorial.md index 76d60c8..00ed917 100644 --- a/docs/send-tutorial.md +++ b/docs/send-tutorial.md @@ -1,3 +1,215 @@ ## 发送邮件部署教程 -手打中... \ No newline at end of file +### 准备工作 + +- 继续使用收件教程中托管到 cf 的域名 +- DKIM(可选) + +### 1. 创建发件 worker + +这里使用手动创建的方式: + +- 进入 Cloudflare 控制台 `https://dash.cloudflare.com/`,点击侧边栏 `Workers 和 Pages` +- 点击`创建应用程序` -> `创建 worker` -> 名称随意,比如 `sender` -> 点击 `部署` +- 进入到该 worker,点击 `编辑代码`,将以下[代码](/docs/send-worker.js)粘贴进去: + +
+查看sender worker代码 +
/**
+ * Welcome to Cloudflare Workers! This is your first worker.
+ *
+ * - Run "npm run dev" in your terminal to start a development server
+ * - Open a browser tab at http://localhost:8787/ to see your worker in action
+ * - Run "npm run deploy" to publish your worker
+ *
+ * Learn more at https://developers.cloudflare.com/workers/
+ */
+async function readRequestBody(request, env) {
+  const { headers } = request;
+  const contentType = headers.get("content-type") || "";
+  if (contentType.includes("application/json")) {
+    return JSON.stringify(await request.json());
+  } else if (contentType.includes("form")) {
+    const formData = await request.formData();
+    const body = {};
+    for (const entry of formData.entries()) {
+      body[entry[0]] = entry[1];
+    }
+    let data = JSON.parse(JSON.stringify(body));
+    let combine = `{"personalizations":[{"to":[{"email":"${data.to}","name":"${data.ton}"}],"dkim_domain":"${env.DKIM_DOMAIN}","dkim_selector":"${env.DKIM_SELECTOR}","dkim_private_key":"${env.DKIM_PRIVATE_KEY}"}],"from":{"email":"${data.from}","name":"${data.fromn}"},"reply_to":{"email":"${data.rep}","name":"${data.repn}"},"subject":"${data.sbj}","content":[{"type":"${data.type}","value":"${data.body}"}]}`;
+    return combine;
+  } else {
+    return '{"success":false}';
+  }
+}
+async function handleRequest(request, env) {
+  let start = Date.now();
+  let reqBody = await readRequestBody(request, env);
+  let send_request = new Request("https://api.mailchannels.net/tx/v1/send", {
+    method: "POST",
+    headers: {
+      "content-type": "application/json",
+    },
+    body: reqBody,
+  });
+  let resp = await fetch(send_request);
+  let respText = await resp.text();
+  let end = Date.now();
+  let total = end - start;
+  return new Response(respText, {
+    headers: {
+      "X-MC-Status": resp.status,
+      "X-Response-Time": total,
+    },
+  });
+}
+const htmlForm = `
+
+
+
+Submit your email
+
+(*) is required
+
+
+
+
+
+
+
+
+
+
+ +
+ +`; +export default { + async fetch(request, env, ctx) { + const { url } = request; + if (url.includes("submit")) { + return new Response(htmlForm, { + headers: { "Content-Type": "text/html" }, + }); + } + if (request.method === "POST") { + return handleRequest(request, env); + } else if (request.method === "GET") { + return new Response(`The request was a GET`); + } + }, +}; +
+
+ +继续保存并部署 + +- 在 worker 的 `设置` -> `变量` 中添加以下变量: + +| 变量名 | 说明 | 示例 | +| ---------------- | --------------------------- | --------------------- | +| DKIM_DOMAIN | 必填,域名后缀 | `vmail.dev` | +| DKIM_PRIVATE_KEY | 必填,DKIM 私钥 | `MIIEpQIBAAKCAQEA...` | +| DKIM_SELECTOR | 必填,固定值 `mailchannels` | `mailchannels` | + +`DKIM_PRIVATE_KEY` 后续会提到如何获取。 + +记住 worker 分配的地址,即 `https://..workers.dev`,后面会用到。 + +### 2. 准备 DKIM 私钥(可选) + +> DKIM 是一种电子邮件认证技术,可以验证邮件的发送者身份。 + +在终端执行下面的命令,生成 DKIM 私钥,参考 [Adding-a-DKIM-Signature](https://support.mailchannels.com/hc/en-us/articles/7122849237389-Adding-a-DKIM-Signature): + +```bash +openssl genrsa 2048 | tee private_key.pem | openssl rsa -outform der | openssl base64 -A > private_key.txt + +echo -n "v=DKIM1;p=" > dkim_record.txt && openssl rsa -in private_key.pem -pubout -outform der | openssl base64 -A >> dkim_record.txt +``` + +这两条命令会生成3个文件: + +- `private_key.pem`:私钥文件 +- `private_key.txt`:私钥文件,base64 编码 +- `dkim_record.txt`:DKIM 记录 + +其中 `private_key.txt` 文件中的内容就是上一步 `DKIM_PRIVATE_KEY` 变量的值。 + +### 3. 配置 DNS + +进入 Cloudflare 托管域名控制台,选择你的域名 -> 点击 `DNS`,接下来,需要**添加3条TXT记录**并**修改1条TXT记录**。 + +新增的如下: + +| 类型 | 名称 | 值 | 备注 | +| ---- | ------------------------- | ------------------------------------------------------------------------------------ | ----------------------------------- | +| TXT | `_dmarc` | `v=DMARC1; p=reject; adkim=s; aspf=s; rua=mailto:YYY; ruf=mailto:YYY pct=100; fo=1;` | 固定值 | +| TXT | `mailchannels._domainkey` | `v=DKIM1;p=MIIEpQIBAAKCAQEA...` | 值为 `dkim_record.txt` 文件的内容 | +| TXT | `_mailchannels` | `v=mc1 cfid=yesmore.workers.dev` | 将其中的 yesmore 修改为你的账号名称 | + +需要修改的如下(TXT记录值为 `example.com` 即你的域名的那条): + +| 类型 | 名称 | 值 | 备注 | +| ---- | ------------- | --------------------------------------------------------------------------- | ------------------------ | +| TXT | `example.com` | `v=spf1 include:_spf.mx.cloudflare.net include:relay.mailchannels.net ~all` | 把值替换为这个,其他不动 | + +最后一共4条TXT记录: + +![](https://img.inke.app/file/b7422917c667de620ae95.png) + +在 [dkim-record-checker](https://dmarcly.com/tools/dkim-record-checker) 验证 DKIM 记录是否配置成功。 + +### 4. 验证发送 + +方法一:打开网页 `https://..workers.dev/submit` 验证是否可以正常发送 + +方法二:使用 API 发送 + +- 请求地址 `https://..workers.dev` +- 请求方法 `POST` +- 请求体: + +```json +{ + "from": { + "email": "sender@vmail.dev", + "name": "发送者" + }, + "personalizations": [ + { + "to": [ + { + "email": "example@gmail.com", + "name": "接收者" + } + ] + } + ], + "subject": "邮件标题", + "content": [ + { + "type": "text/plain", + "value": "邮件内容" + } + ] +} +``` + +示例: + +![](https://img.inke.app/file/1f6f3ab53aff9a1855475.png) + + +### 5. 添加环境变量 + +此项目使用第4步中的方法二通过 API 发送邮件。 + +在 Vercel 项目控制台中,进入 `Settings` -> `Environment Variables`,添加以下变量: + +- SEND_WORKER_URL:值为 `https://..workers.dev` + +添加后重新触发部署一次即可。 \ No newline at end of file diff --git a/docs/send-worker.js b/docs/send-worker.js new file mode 100644 index 0000000..cc3169d --- /dev/null +++ b/docs/send-worker.js @@ -0,0 +1,93 @@ +/** + * Welcome to Cloudflare Workers! This is your first worker. + * + * - Run "npm run dev" in your terminal to start a development server + * - Open a browser tab at http://localhost:8787/ to see your worker in action + * - Run "npm run deploy" to publish your worker + * + * Learn more at https://developers.cloudflare.com/workers/ + */ + +async function readRequestBody(request, env) { + const { headers } = request; + const contentType = headers.get("content-type") || ""; + if (contentType.includes("application/json")) { + return JSON.stringify(await request.json()); + } else if (contentType.includes("form")) { + const formData = await request.formData(); + const body = {}; + for (const entry of formData.entries()) { + body[entry[0]] = entry[1]; + } + let data = JSON.parse(JSON.stringify(body)); + let combine = `{"personalizations":[{"to":[{"email":"${data.to}","name":"${data.ton}"}],"dkim_domain":"${env.DKIM_DOMAIN}","dkim_selector":"${env.DKIM_SELECTOR}","dkim_private_key":"${env.DKIM_PRIVATE_KEY}"}],"from":{"email":"${data.from}","name":"${data.fromn}"},"reply_to":{"email":"${data.rep}","name":"${data.repn}"},"subject":"${data.sbj}","content":[{"type":"${data.type}","value":"${data.body}"}]}`; + return combine; + } else { + return '{"success":false}'; + } +} + +async function handleRequest(request, env) { + let start = Date.now(); + let reqBody = await readRequestBody(request, env); + let send_request = new Request("https://api.mailchannels.net/tx/v1/send", { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: reqBody, + }); + let resp = await fetch(send_request); + let respText = await resp.text(); + let end = Date.now(); + let total = end - start; + return new Response(respText, { + headers: { + "X-MC-Status": resp.status, + "X-Response-Time": total, + }, + }); +} + +const htmlForm = ` + + + +Submit your email + +(*) is required +
+
+
+
+
+
+
+ +
+ +
+
+ +
+ +`; + +export default { + async fetch(request, env, ctx) { + const { url } = request; + if (url.includes("submit")) { + return new Response(htmlForm, { + headers: { "Content-Type": "text/html" }, + }); + } + if (request.method === "POST") { + return handleRequest(request, env); + } else if (request.method === "GET") { + return new Response(`The request was a GET`); + } + }, +};