feat: |Worker| support send mail by SMTP (#580)

This commit is contained in:
Dream Hunter
2025-02-15 18:17:14 +08:00
committed by GitHub
parent 7f6a02ca38
commit 61434ab6f7
7 changed files with 119 additions and 13 deletions
+1
View File
@@ -4,6 +4,7 @@
## main(v0.8.7) ## main(v0.8.7)
- fix: |UI| 修复移动设备日期显示问题 - 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 ## v0.8.6
@@ -6,8 +6,8 @@
## 通用 ## 通用
| 问题 | 解决方案 | | 问题 | 解决方案 |
| ---------------------------------------------- | ------------------------------------------------------------------------------- | | -------------------------------------------------- | ------------------------------------------------------------------------------- |
| 使用 Cloudflare Workers 给已认证的邮箱发送邮件 | 使用 cf 的 API 进行发送,只支持绑定到 CF 上的收件地址,即 CF EMAIL 转发目的地址 | | 使用 Cloudflare Workers 给已认证的转发邮箱发送邮件 | 使用 cf 的 API 进行发送,只支持绑定到 CF 上的收件地址,即 CF EMAIL 转发目的地址 |
| 绑定多个域名 | 每个域名都需要设置 email 转发到 worker | | 绑定多个域名 | 每个域名都需要设置 email 转发到 worker |
## worker 相关 ## worker 相关
@@ -1,9 +1,11 @@
# 配置发送邮件 # 配置发送邮件
## 使用 Cloudflare Workers 给已认证的邮箱发送邮件 ::: warning 注意
三种方式可以同时配置,发送邮件时会优先使用 `resend`,如果没有配置 `resend`,则会使用 `smtp`.
admin 后台 账号配置 `已验证地址列表(可通过 cf 内部 api 发送邮件)` 如果配置了 Cloudflare 已认证的转发邮箱地址,会优先使用 cf 内部 API 发送邮件
:::
## 使用 resend 发送邮件 ## 使用 resend 发送邮件
@@ -30,3 +32,53 @@ wrangler secret put RESEND_TOKEN
wrangler secret put RESEND_TOKEN_XXX_COM wrangler secret put RESEND_TOKEN_XXX_COM
wrangler secret put RESEND_TOKEN_DREAMHUNTER2333_XYZ 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 发送邮件)`
+2 -1
View File
@@ -27,7 +27,8 @@
"mimetext": "^3.0.27", "mimetext": "^3.0.27",
"postal-mime": "^2.4.3", "postal-mime": "^2.4.3",
"resend": "^4.1.1", "resend": "^4.1.1",
"telegraf": "4.16.3" "telegraf": "4.16.3",
"worker-mailer": "^1.0.1"
}, },
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
+8
View File
@@ -37,6 +37,9 @@ importers:
telegraf: telegraf:
specifier: 4.16.3 specifier: 4.16.3
version: 4.16.3(patch_hash=7d0a1784bb35f50fee25f26a14017734b9461612c635e71734b59527280c9563) version: 4.16.3(patch_hash=7d0a1784bb35f50fee25f26a14017734b9461612c635e71734b59527280c9563)
worker-mailer:
specifier: ^1.0.1
version: 1.0.1
devDependencies: devDependencies:
'@cloudflare/workers-types': '@cloudflare/workers-types':
specifier: ^4.20250129.0 specifier: ^4.20250129.0
@@ -1578,6 +1581,9 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
worker-mailer@1.0.1:
resolution: {integrity: sha512-y6U9B2cWGGasj7B+6ZtRBsdTPRAZ0P73ykKq5hsIHXReUB8WAq7feS4JoN2xmAZl7yQpVz/GTLqmqLyDmsOUnw==}
workerd@1.20250124.0: workerd@1.20250124.0:
resolution: {integrity: sha512-EnT9gN3M9/UHRFPZptKgK36DLOW8WfJV7cjNs3zstVbmF5cpFaHCAzX7tXWBO6zyvW/+EjklJPFtOvfatiZsuQ==} resolution: {integrity: sha512-EnT9gN3M9/UHRFPZptKgK36DLOW8WfJV7cjNs3zstVbmF5cpFaHCAzX7tXWBO6zyvW/+EjklJPFtOvfatiZsuQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
@@ -3564,6 +3570,8 @@ snapshots:
word-wrap@1.2.5: {} word-wrap@1.2.5: {}
worker-mailer@1.0.1: {}
workerd@1.20250124.0: workerd@1.20250124.0:
optionalDependencies: optionalDependencies:
'@cloudflare/workerd-darwin-64': 1.20250124.0 '@cloudflare/workerd-darwin-64': 1.20250124.0
+46 -5
View File
@@ -2,9 +2,10 @@ import { Context, Hono } from 'hono'
import { Jwt } from 'hono/utils/jwt' import { Jwt } from 'hono/utils/jwt'
import { createMimeMessage } from 'mimetext'; import { createMimeMessage } from 'mimetext';
import { Resend } from 'resend'; import { Resend } from 'resend';
import { WorkerMailer, WorkerMailerOptions } from 'worker-mailer';
import { CONSTANTS } from '../constants' 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 { GeoData } from '../models'
import { handleListQuery } from '../common' import { handleListQuery } from '../common'
import { HonoCustomType } from '../types'; import { HonoCustomType } from '../types';
@@ -89,6 +90,32 @@ const sendMailByResend = async (
console.log(`Resend success: ${JSON.stringify(data)}`); console.log(`Resend success: ${JSON.stringify(data)}`);
} }
const sendMailBySmtp = async (
c: Context<HonoCustomType>, address: string,
reqJson: {
from_name: string, to_mail: string, to_name: string,
subject: string, content: string, is_html: boolean
},
smtpOptions: WorkerMailerOptions
): Promise<void> => {
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 ( export const sendMail = async (
c: Context<HonoCustomType>, address: string, c: Context<HonoCustomType>, address: string,
reqJson: { reqJson: {
@@ -138,15 +165,20 @@ export const sendMail = async (
throw new Error("to_mail address is blocked") throw new Error("to_mail address is blocked")
} }
if (!subject) { if (!subject) {
throw new Error("Invalid subject") throw new Error("Subject is empty")
} }
if (!content) { if (!content) {
throw new Error("Invalid content") throw new Error("Content is empty")
} }
// send to verified address list, do not update balance // send to verified address list, do not update balance
const resendEnabled = c.env.RESEND_TOKEN || c.env[ const resendEnabled = c.env.RESEND_TOKEN || c.env[
`RESEND_TOKEN_${mailDomain.replace(/\./g, "_").toUpperCase()}` `RESEND_TOKEN_${mailDomain.replace(/\./g, "_").toUpperCase()}`
]; ];
// send by smtp
const smtpConfigMap = getJsonObjectValue<Record<string, WorkerMailerOptions>>(c.env.SMTP_CONFIG);
const smtpConfig = smtpConfigMap ? smtpConfigMap[mailDomain] : null;
// send by verified address list
let sendByVerifiedAddressList = false; let sendByVerifiedAddressList = false;
if (c.env.SEND_MAIL) { if (c.env.SEND_MAIL) {
const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY) || []; const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY) || [];
@@ -155,6 +187,8 @@ export const sendMail = async (
sendByVerifiedAddressList = true; sendByVerifiedAddressList = true;
} }
} }
// send mail workflow
if (sendByVerifiedAddressList) { if (sendByVerifiedAddressList) {
// do not update balance // do not update balance
} }
@@ -162,9 +196,16 @@ export const sendMail = async (
else if (resendEnabled) { else if (resendEnabled) {
await sendMailByResend(c, address, reqJson); await sendMailByResend(c, address, reqJson);
} }
else { else if (smtpConfig) {
throw new Error("Please enable resend or verified address list") 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 // update balance
if (!sendByVerifiedAddressList && needCheckBalance) { if (!sendByVerifiedAddressList && needCheckBalance) {
try { try {
+4 -1
View File
@@ -66,7 +66,10 @@ export type Bindings = {
// resend // resend
RESEND_TOKEN: string | undefined 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 config
TELEGRAM_BOT_TOKEN: string TELEGRAM_BOT_TOKEN: string