diff --git a/CHANGELOG.md b/CHANGELOG.md index e19e985d..3fffa658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## main(v0.8.7) - fix: |UI| 修复移动设备日期显示问题 +- feat: |Worker| 支持通过 `SMTP` 发送邮件, 使用 [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md) ## v0.8.6 diff --git a/vitepress-docs/docs/zh/guide/common-issues.md b/vitepress-docs/docs/zh/guide/common-issues.md index 3f725a05..91f2ea50 100644 --- a/vitepress-docs/docs/zh/guide/common-issues.md +++ b/vitepress-docs/docs/zh/guide/common-issues.md @@ -5,10 +5,10 @@ ## 通用 -| 问题 | 解决方案 | -| ---------------------------------------------- | ------------------------------------------------------------------------------- | -| 使用 Cloudflare Workers 给已认证的邮箱发送邮件 | 使用 cf 的 API 进行发送,只支持绑定到 CF 上的收件地址,即 CF EMAIL 转发目的地址 | -| 绑定多个域名 | 每个域名都需要设置 email 转发到 worker | +| 问题 | 解决方案 | +| -------------------------------------------------- | ------------------------------------------------------------------------------- | +| 使用 Cloudflare Workers 给已认证的转发邮箱发送邮件 | 使用 cf 的 API 进行发送,只支持绑定到 CF 上的收件地址,即 CF EMAIL 转发目的地址 | +| 绑定多个域名 | 每个域名都需要设置 email 转发到 worker | ## worker 相关 diff --git a/vitepress-docs/docs/zh/guide/config-send-mail.md b/vitepress-docs/docs/zh/guide/config-send-mail.md index aa9bc0f7..01b7da50 100644 --- a/vitepress-docs/docs/zh/guide/config-send-mail.md +++ b/vitepress-docs/docs/zh/guide/config-send-mail.md @@ -1,9 +1,11 @@ # 配置发送邮件 -## 使用 Cloudflare Workers 给已认证的邮箱发送邮件 +::: warning 注意 +三种方式可以同时配置,发送邮件时会优先使用 `resend`,如果没有配置 `resend`,则会使用 `smtp`. -admin 后台 账号配置 `已验证地址列表(可通过 cf 内部 api 发送邮件)` +如果配置了 Cloudflare 已认证的转发邮箱地址,会优先使用 cf 内部 API 发送邮件 +::: ## 使用 resend 发送邮件 @@ -30,3 +32,53 @@ wrangler secret put RESEND_TOKEN wrangler secret put RESEND_TOKEN_XXX_COM wrangler secret put RESEND_TOKEN_DREAMHUNTER2333_XYZ ``` + +## 使用 SMTP 发送邮件 + +`SMTP_CONFIG` 的格式如下,key 为域名,value 为 SMTP 配置,SMTP 配置格式详情可以参考 [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md) + +```json +{ + "awsl.uk": { + "host": "smtp.xxx.com", + "port": 465, + "secure": true, + "authType": [ + "plain", + "login" + ], + "credentials": { + "username": "username", + "password": "password" + } + } +} +``` + +然后执行下面的命令,将 `SMTP_CONFIG` 添加到 secrets 中 + +> [!NOTE] +> 如果你觉得麻烦,也可以直接明文放在 `wrangler.toml` 中 `[vars]` 下面,但是不推荐这样做 + +如果你是通过 UI 部署的,可以在 Cloudflare 的 UI 界面中添加到 `Variables and Secrets` 下面 + +```bash +# 切换到 worker 目录 +cd worker +wrangler secret put SMTP_CONFIG +``` + +## 给 Cloudflare 上已认证的转发邮箱发送邮件 + +仅支持 CLI 部署时使用,在 `wrangler.toml` 中添加 `send_email` 配置 + +发送的目的邮箱地址必须是 Cloudflare 上已认证的邮箱地址,局限性较大,如果需要发送邮件给其他邮箱,可以使用 `resend` 或者 `smtp` 发送邮件 + +```toml +# 通过 Cloudflare 发送邮件 +send_email = [ + { name = "SEND_MAIL" }, +] +``` + +admin 后台 账号配置 `已验证地址列表(可通过 cf 内部 api 发送邮件)` diff --git a/worker/package.json b/worker/package.json index 172b5228..1b9a8d62 100644 --- a/worker/package.json +++ b/worker/package.json @@ -27,7 +27,8 @@ "mimetext": "^3.0.27", "postal-mime": "^2.4.3", "resend": "^4.1.1", - "telegraf": "4.16.3" + "telegraf": "4.16.3", + "worker-mailer": "^1.0.1" }, "pnpm": { "patchedDependencies": { diff --git a/worker/pnpm-lock.yaml b/worker/pnpm-lock.yaml index 96f27291..f07b2e41 100644 --- a/worker/pnpm-lock.yaml +++ b/worker/pnpm-lock.yaml @@ -37,6 +37,9 @@ importers: telegraf: specifier: 4.16.3 version: 4.16.3(patch_hash=7d0a1784bb35f50fee25f26a14017734b9461612c635e71734b59527280c9563) + worker-mailer: + specifier: ^1.0.1 + version: 1.0.1 devDependencies: '@cloudflare/workers-types': specifier: ^4.20250129.0 @@ -1578,6 +1581,9 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + worker-mailer@1.0.1: + resolution: {integrity: sha512-y6U9B2cWGGasj7B+6ZtRBsdTPRAZ0P73ykKq5hsIHXReUB8WAq7feS4JoN2xmAZl7yQpVz/GTLqmqLyDmsOUnw==} + workerd@1.20250124.0: resolution: {integrity: sha512-EnT9gN3M9/UHRFPZptKgK36DLOW8WfJV7cjNs3zstVbmF5cpFaHCAzX7tXWBO6zyvW/+EjklJPFtOvfatiZsuQ==} engines: {node: '>=16'} @@ -3564,6 +3570,8 @@ snapshots: word-wrap@1.2.5: {} + worker-mailer@1.0.1: {} + workerd@1.20250124.0: optionalDependencies: '@cloudflare/workerd-darwin-64': 1.20250124.0 diff --git a/worker/src/mails_api/send_mail_api.ts b/worker/src/mails_api/send_mail_api.ts index 47744583..5a3fcd57 100644 --- a/worker/src/mails_api/send_mail_api.ts +++ b/worker/src/mails_api/send_mail_api.ts @@ -2,9 +2,10 @@ import { Context, Hono } from 'hono' import { Jwt } from 'hono/utils/jwt' import { createMimeMessage } from 'mimetext'; import { Resend } from 'resend'; +import { WorkerMailer, WorkerMailerOptions } from 'worker-mailer'; import { CONSTANTS } from '../constants' -import { getJsonSetting, getDomains, getIntValue, getBooleanValue, getStringValue } from '../utils'; +import { getJsonSetting, getDomains, getIntValue, getBooleanValue, getStringValue, getJsonObjectValue } from '../utils'; import { GeoData } from '../models' import { handleListQuery } from '../common' import { HonoCustomType } from '../types'; @@ -89,6 +90,32 @@ const sendMailByResend = async ( console.log(`Resend success: ${JSON.stringify(data)}`); } +const sendMailBySmtp = async ( + c: Context, address: string, + reqJson: { + from_name: string, to_mail: string, to_name: string, + subject: string, content: string, is_html: boolean + }, + smtpOptions: WorkerMailerOptions +): Promise => { + await WorkerMailer.send( + smtpOptions, + { + from: { + name: reqJson.from_name, + email: address + }, + to: { + name: reqJson.to_name, + email: reqJson.to_mail + }, + subject: reqJson.subject, + text: reqJson.is_html ? undefined : reqJson.content, + html: reqJson.is_html ? reqJson.content : undefined + } + ) +} + export const sendMail = async ( c: Context, address: string, reqJson: { @@ -138,15 +165,20 @@ export const sendMail = async ( throw new Error("to_mail address is blocked") } if (!subject) { - throw new Error("Invalid subject") + throw new Error("Subject is empty") } if (!content) { - throw new Error("Invalid content") + throw new Error("Content is empty") } + // send to verified address list, do not update balance const resendEnabled = c.env.RESEND_TOKEN || c.env[ `RESEND_TOKEN_${mailDomain.replace(/\./g, "_").toUpperCase()}` ]; + // send by smtp + const smtpConfigMap = getJsonObjectValue>(c.env.SMTP_CONFIG); + const smtpConfig = smtpConfigMap ? smtpConfigMap[mailDomain] : null; + // send by verified address list let sendByVerifiedAddressList = false; if (c.env.SEND_MAIL) { const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY) || []; @@ -155,6 +187,8 @@ export const sendMail = async ( sendByVerifiedAddressList = true; } } + + // send mail workflow if (sendByVerifiedAddressList) { // do not update balance } @@ -162,9 +196,16 @@ export const sendMail = async ( else if (resendEnabled) { await sendMailByResend(c, address, reqJson); } - else { - throw new Error("Please enable resend or verified address list") + else if (smtpConfig) { + await sendMailBySmtp(c, address, reqJson, smtpConfig); } + else { + if (c.env.SEND_MAIL) { + throw new Error(`Please enable resend or smtp for domain ${mailDomain}. Or add ${to_mail} to verified address list`); + } + throw new Error(`Please enable resend or smtp for domain ${mailDomain}`); + } + // update balance if (!sendByVerifiedAddressList && needCheckBalance) { try { diff --git a/worker/src/types.d.ts b/worker/src/types.d.ts index 4eba49e7..4100a77f 100644 --- a/worker/src/types.d.ts +++ b/worker/src/types.d.ts @@ -66,7 +66,10 @@ export type Bindings = { // resend RESEND_TOKEN: string | undefined - [key: `RESEND_TOKEN_${string}`]: string | undefined; + [key: `RESEND_TOKEN_${string}`]: string | undefined + + // SMTP config + SMTP_CONFIG: string | object | undefined // telegram config TELEGRAM_BOT_TOKEN: string