diff --git a/CHANGELOG.md b/CHANGELOG.md index b50027d0..7f46f342 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ # CHANGE LOG -## main(v0.8.4) +## main(v0.8.5) + +- feat: 增加调用其他 worker,功能解耦 +- feat: 增加 worker 配置 `ENABLE_ANOTHER_WORKER` 及 `ANOTHER_WORKER_LIST` ,用于调用其他 worker 的 rpc 接口 + +## v0.8.4 - fix: |UI| 修复 admin portal 无收件人邮箱删除调用api 错误 - feat: |Telegram Bot| 增加 telegram bot 清理无效地址凭证命令 diff --git a/vitepress-docs/docs/.vitepress/zh.ts b/vitepress-docs/docs/.vitepress/zh.ts index bf518a43..61014ff9 100644 --- a/vitepress-docs/docs/.vitepress/zh.ts +++ b/vitepress-docs/docs/.vitepress/zh.ts @@ -138,6 +138,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] { { text: '配置 webhook', link: 'feature/webhook' }, { text: '新建邮箱地址 API', link: 'feature/new-address-api' }, { text: 'Oauth2 第三方登录', link: 'feature/user-oauth2' }, + { text: '配置其他worker增强', link: 'feature/another-worker-enhanced' }, ] }, { diff --git a/vitepress-docs/docs/public/feature/another-worker-enhanced-01.png b/vitepress-docs/docs/public/feature/another-worker-enhanced-01.png new file mode 100644 index 00000000..eb39010b Binary files /dev/null and b/vitepress-docs/docs/public/feature/another-worker-enhanced-01.png differ diff --git a/vitepress-docs/docs/public/feature/another-worker-enhanced-02.png b/vitepress-docs/docs/public/feature/another-worker-enhanced-02.png new file mode 100644 index 00000000..89ac83d8 Binary files /dev/null and b/vitepress-docs/docs/public/feature/another-worker-enhanced-02.png differ diff --git a/vitepress-docs/docs/public/feature/another-worker-enhanced-03.png b/vitepress-docs/docs/public/feature/another-worker-enhanced-03.png new file mode 100644 index 00000000..913799c1 Binary files /dev/null and b/vitepress-docs/docs/public/feature/another-worker-enhanced-03.png differ diff --git a/vitepress-docs/docs/public/feature/another-worker-enhanced-04.png b/vitepress-docs/docs/public/feature/another-worker-enhanced-04.png new file mode 100644 index 00000000..67f59aed Binary files /dev/null and b/vitepress-docs/docs/public/feature/another-worker-enhanced-04.png differ diff --git a/vitepress-docs/docs/zh/guide/cli/worker.md b/vitepress-docs/docs/zh/guide/cli/worker.md index 2210b519..e99e44ce 100644 --- a/vitepress-docs/docs/zh/guide/cli/worker.md +++ b/vitepress-docs/docs/zh/guide/cli/worker.md @@ -108,6 +108,23 @@ ENABLE_AUTO_REPLY = false # ENABLE_CHECK_JUNK_MAIL = false # 垃圾邮件检查配置, 任何一项不存在或者不通过则被判定为垃圾邮件 # JUNK_MAIL_FORCE_PASS_LIST = ["spf", "dkim", "dmarc"] +# 是否开启其他 worker 处理邮件 +# ENABLE_ANOTHER_WORKER = false +# 其他 worker 处理邮件的配置,可以配置多个其他 worker。 +# 通过关键词筛选,调用对应绑定的 worker 的方法(默认方法名为 rpcEmail) +# keywords必填,否则 worker 将不会被触发 +#ANOTHER_WORKER_LIST =""" +#[ +# { +# "binding":"AUTH_INBOX", +# "method":"rpcEmail", +# "keywords":[ +# "验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认", +# "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm" +# ] +# } +#] +#""" # D1 数据库的名称和 ID 可以在 cloudflare 控制台查看 [[d1_databases]] @@ -127,6 +144,11 @@ database_id = "xxx" # D1 数据库 ID # namespace_id = "1001" # # 10 requests per minute # simple = { limit = 10, period = 60 } + +# 绑定其他 worker 处理邮件,例如通过 auth-inbox ai 能力解析验证码或激活链接 +# [[services]] +# binding = "AUTH_INBOX" +# service = "auth-inbox" ``` ## Telegram Bot 配置 diff --git a/vitepress-docs/docs/zh/guide/feature/another-worker-enhanced.md b/vitepress-docs/docs/zh/guide/feature/another-worker-enhanced.md new file mode 100644 index 00000000..9b34bfa2 --- /dev/null +++ b/vitepress-docs/docs/zh/guide/feature/another-worker-enhanced.md @@ -0,0 +1,144 @@ +# 通过其他 worker 增强 + +> 临时邮箱的核心能力在邮件的管理,通过其他 worker 可以增强临时邮箱的功能,例如通过 auth-inbox ai 能力解析验证码或激活链接 +> 该功能仅触发其他 worker ,在 webhook 后执行 +> [!NOTE] +> 如果要使用 worker 增强,请提前创建可以 rpc 调用的 worker,具体下文详述 +> 参考: +> - https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/ +> - https://developers.cloudflare.com/workers/runtime-apis/rpc/ +> - auth-inbox 项目:https://github.com/TooonyChen/AuthInbox + +## 创建其他 worker(以 auth-inbox 项目ai解析验证码为例子) + +### worker 改造为继承 WorkerEntrypoint + +一个简单,作为被调用方,提供 rpc 方法调用的worker代码如下(rpcEmail 方法为样例) +(使用已经修改好的项目 https://github.com/oneisall8955/AuthInbox-fork) + +src/index.ts 文件 +```js +import { WorkerEntrypoint } from "cloudflare:workers"; + +interface Env { + DB: D1Database; + // ... +} + +export default class extends WorkerEntrypoint { + async fetch(request: Request): Promise { + console.log("原本fetch接口入参是request,env,ctx"); + console.log("修改为WorkerEntrypoint风格后,只有一个入参request,获取环境变量和上下文有小改动"); + // 环境变量及上下文改动详见: + // https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#bindings-env + // https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#lifecycle-methods-ctx + const env: Env = this.env; + const ctx: ExecutionContext = this.ctx; + console.log("后续逻辑不变"); + return new Response('ok', { status: 200 }); + } + + // 主要功能 + async email(message: ForwardableEmailMessage): Promise { + console.log("原本fetch接口入参是message,env,ctx"); + console.log("修改为WorkerEntrypoint风格后,只有一个入参message,获取环境变量和上下文和fetch方法一样"); + const env: Env = this.env; + const ctx: ExecutionContext = this.ctx; + console.log("接受email routing请求后,后续逻辑不变"); + } + + // 暴露rpc接口,处理来自其他worker的邮件请求 + async rpcEmail(requestBody: string): Promise { + console.log(`接受其他worker(临时邮件服务cloudflare_temp_email)的请求,request body: ${requestBody}`); + // requestBody json 格式,由临时邮件服务发送,格式如下 + // type RPCEmailMessage = { + // from: string | undefined | null, + // to: string | undefined | null, + // rawEmail: string | undefined | null, + // headers: Map, + // } + // ... todo ... + } +} +``` + +### 部署其他 worker + +修改好或者使用 以auth-inbox 为例,部署到 cloudflare worker 上,详见 https://github.com/TooonyChen/AuthInbox ,或者使用已经修改好的项目 https://github.com/oneisall8955/AuthInbox-fork + +## 配置临时邮件服务,使用指定其他 worker 增强 + +## 绑定服务 + +### 通过 wrangler.toml 配置 + +```toml +[[services]] +binding = "AUTH_INBOX" +service = "auth-inbox" +``` + +这里的 `binding = "AUTH_INBOX"` 可以自定义,可以是任何字符串,`service = "auth-inbox"` 是部署好的提供rpc接口调用的worker名称。 + +### 用户界面配置 + +在设置-绑定,添加绑定,选择绑定服务。 +变量名称填写自定义的名称,可以任意字符串 ,例如 `AUTH_INBOX`。 +服务绑定选择上一步创建好的服务,例如 `auth-inbox`。 + +![another-worker-enhanced-01.png](/feature/another-worker-enhanced-01.png) + +![another-worker-enhanced-02.png](/feature/another-worker-enhanced-02.png) + +## 环境变量配置 + +### 通过 wrangler.toml 配置 + +```toml +ENABLE_ANOTHER_WORKER = true +ANOTHER_WORKER_LIST =""" +[ + { + "binding":"AUTH_INBOX", + "method":"rpcEmail", + "keywords":[ + "验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认", + "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm" + ] + } +] +""" +``` + +环境变量解释: +- ENABLE_ANOTHER_WORKER = true:默认为false,true则开启其他 worker 处理邮件 +- ANOTHER_WORKER_LIST 是一个JOSN数组,每个对象包3个字段 + - binding: *必填,必须与services部分指定的 binding = "XXX" 保持一致*,例子中为 AUTH_INBOX + - method: 可选,默认 rpcEmail,指的是调用这个 worker 的哪一个 rpc 方法处理 + - keywords: 关键词数组,忽略大小写。用于过滤,如果*解析后邮件文本*匹配到这些关键词,触发这个 worker,并且调用这个 worker 的 `method` 方法 + +### 用户界面配置 + +在设置-环境变量,添加环境变量 +- ENABLE_ANOTHER_WORKER = true +- ANOTHER_WORKER_LIST 为上面提及的JSON数组字符串,不再复述,详细介绍看上文 +```json +[ + { + "binding":"AUTH_INBOX", + "method":"rpcEmail", + "keywords":[ + "验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认", + "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm" + ] + } +] +``` + +![another-worker-enhanced-03.png](/feature/another-worker-enhanced-03.png) + +## 测试 + +发送一个邮件到临时邮箱,观察worker日志到,或者到 auth-inbox 提供的面板上查看验证码 + +![another-worker-enhanced-04.png](/feature/another-worker-enhanced-04.png) diff --git a/worker/src/admin_api/worker_config.ts b/worker/src/admin_api/worker_config.ts index ec41cbda..ecec5462 100644 --- a/worker/src/admin_api/worker_config.ts +++ b/worker/src/admin_api/worker_config.ts @@ -1,7 +1,7 @@ import { Context } from 'hono'; import { HonoCustomType } from '../types'; -import { getAdminPasswords, getBooleanValue, getDefaultDomains, getDomains, getIntValue, getPasswords, getStringArray, getStringValue, getUserRoles } from '../utils'; +import { getAdminPasswords, getBooleanValue, getDefaultDomains, getDomains, getIntValue, getPasswords, getStringArray, getStringValue, getUserRoles, getAnotherWorkerList } from '../utils'; import { CONSTANTS } from '../constants'; import { isS3Enabled } from '../mails_api/s3_attachment'; @@ -44,6 +44,9 @@ export default { "DISABLE_ADMIN_PASSWORD_CHECK": getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK), "ENABLE_CHECK_JUNK_MAIL": getBooleanValue(c.env.ENABLE_CHECK_JUNK_MAIL), "JUNK_MAIL_FORCE_PASS_LIST": getStringArray(c.env.JUNK_MAIL_FORCE_PASS_LIST), + + "ENABLE_ANOTHER_WORKER": getBooleanValue(c.env.ENABLE_ANOTHER_WORKER), + "ANOTHER_WORKER_LIST": getAnotherWorkerList(c), }) } } diff --git a/worker/src/common.ts b/worker/src/common.ts index 8ed9b056..ace83862 100644 --- a/worker/src/common.ts +++ b/worker/src/common.ts @@ -1,8 +1,8 @@ import { Context } from 'hono'; import { Jwt } from 'hono/utils/jwt' -import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting } from './utils'; -import { HonoCustomType, UserRole } from './types'; +import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList } from './utils'; +import { HonoCustomType, UserRole, AnotherWorker, RPCEmailMessage } from './types'; import { unbindTelegramByAddress } from './telegram_api/common'; import { CONSTANTS } from './constants'; import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models'; @@ -360,7 +360,7 @@ export async function triggerWebhook( address: string, raw_mail: string, message_id: string | null -): Promise { +): Promise { if (!c.env.KV || !getBooleanValue(c.env.ENABLE_WEBHOOK)) { return } @@ -408,4 +408,49 @@ export async function triggerWebhook( console.error(res.message); } } + return webhookMail.parsedText +} + +export async function triggerAnotherWorker( + c: Context, + rpcEmailMessage: RPCEmailMessage, + parsedText: string | undefined | null +): Promise { + if (!parsedText) { + return; + } + + const anotherWorkerList: AnotherWorker[] = getAnotherWorkerList(c); + if (!getBooleanValue(c.env.ENABLE_ANOTHER_WORKER) || anotherWorkerList.length === 0) { + console.log(`another worker disabled or anotherWorkerList is empty`); + return; + } + + const parsedTextLowercase: string = parsedText.toLowerCase(); + for (const worker of anotherWorkerList) { + + const keywords = worker?.keywords ?? []; + const bindingName = worker?.binding ?? ""; + const methodName = worker.method ?? "rpcEmail"; + + const serviceBinding = (c.env as any)[bindingName] ?? {}; + const method = serviceBinding[methodName]; + + if (!method || typeof method !== "function") { + console.log(`method = ${methodName} not found or not function`); + continue; + } + + if (!keywords.some(keyword => keyword && parsedTextLowercase.includes(keyword.toLowerCase()))) { + console.log(`worker.binding = ${bindingName} not match keywords, parsedText = ${parsedText}`); + continue; + } + + try { + const requestBody = JSON.stringify(rpcEmailMessage); + await method(requestBody); + } catch (e1) { + console.error(`execute method = ${methodName} error`, e1); + } + } } diff --git a/worker/src/email/index.ts b/worker/src/email/index.ts index 3ff88396..ff2a344c 100644 --- a/worker/src/email/index.ts +++ b/worker/src/email/index.ts @@ -2,10 +2,10 @@ import { Context } from "hono"; import { getEnvStringList } from "../utils"; import { sendMailToTelegram } from "../telegram_api"; -import { Bindings, HonoCustomType } from "../types"; +import { Bindings, HonoCustomType, RPCEmailMessage } from "../types"; import { auto_reply } from "./auto_reply"; import { isBlocked } from "./black_list"; -import { triggerWebhook } from "../common"; +import { triggerWebhook, triggerAnotherWorker, commonParseMail} from "../common"; import { check_if_junk_mail } from "./check_junk"; @@ -61,8 +61,9 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu } // send webhook + let parsedText; try { - await triggerWebhook( + parsedText = await triggerWebhook( { env: env } as Context, message.to, rawEmail, message_id ); @@ -70,6 +71,26 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu console.log("send webhook error", error); } + // trigger another worker + try { + const headersMap = new Map(); + if(message.headers) { + message.headers.forEach((value, key) => {headersMap.set(key, value);}); + } + if (!parsedText){ + parsedText = (await commonParseMail(rawEmail))?.text ?? "" + } + const rpcEmail: RPCEmailMessage = { + from: message.from, + to: message.to, + rawEmail: rawEmail, + headers: headersMap + } + await triggerAnotherWorker({ env: env } as Context, rpcEmail, parsedText); + } catch (error) { + console.error("trigger another worker error", error); + } + // auto reply email await auto_reply(message, env); } diff --git a/worker/src/types.d.ts b/worker/src/types.d.ts index 41476ef4..7fc7cb88 100644 --- a/worker/src/types.d.ts +++ b/worker/src/types.d.ts @@ -46,6 +46,10 @@ export type Bindings = { ENABLE_CHECK_JUNK_MAIL: string | boolean | undefined JUNK_MAIL_FORCE_PASS_LIST: string | string[] | undefined + ENABLE_ANOTHER_WORKER: string | boolean | undefined + ANOTHER_WORKER_LIST: string | AnotherWorker[] | undefined + + // s3 config S3_ENDPOINT: string | undefined S3_ACCESS_KEY_ID: string | undefined @@ -92,3 +96,16 @@ type HonoCustomType = { "Bindings": Bindings; "Variables": Variables; } + +type AnotherWorker = { + binding: string | undefined | null, + method: string | undefined | null, + keywords: string[] | undefined | null +} + +type RPCEmailMessage = { + from: string | undefined | null, + to: string | undefined | null, + rawEmail: string | undefined | null, + headers: Map, +} \ No newline at end of file diff --git a/worker/src/utils.ts b/worker/src/utils.ts index 68814bb9..b9273bd3 100644 --- a/worker/src/utils.ts +++ b/worker/src/utils.ts @@ -1,6 +1,6 @@ import { Context } from "hono"; import { createMimeMessage } from "mimetext"; -import { HonoCustomType, UserRole } from "./types"; +import { HonoCustomType, UserRole,AnotherWorker } from "./types"; export const getJsonObjectValue = ( value: string | any @@ -156,6 +156,22 @@ export const getUserRoles = (c: Context): UserRole[] => { return c.env.USER_ROLES; } +export const getAnotherWorkerList = (c: Context): AnotherWorker[] => { + if (!c.env.ANOTHER_WORKER_LIST) { + return []; + } + // check if ANOTHER_WORKER_LIST is an array, if not use json.parse + if (!Array.isArray(c.env.ANOTHER_WORKER_LIST)) { + try { + return JSON.parse(c.env.ANOTHER_WORKER_LIST); + } catch (e) { + console.error("Failed to parse ANOTHER_WORKER_LIST", e); + return []; + } + } + return c.env.ANOTHER_WORKER_LIST; +} + export const getPasswords = (c: Context): string[] => { if (!c.env.PASSWORDS) { return []; diff --git a/worker/wrangler.toml.template b/worker/wrangler.toml.template index 118722c1..312cf3fb 100644 --- a/worker/wrangler.toml.template +++ b/worker/wrangler.toml.template @@ -78,6 +78,20 @@ ENABLE_AUTO_REPLY = false # ENABLE_CHECK_JUNK_MAIL = false # junk mail force check pass list, if no status or status is not pass, will be marked as junk mail # JUNK_MAIL_FORCE_PASS_LIST = ["spf", "dkim", "dmarc"] +# Calling other woker to process email +#ENABLE_ANOTHER_WORKER = false +#ANOTHER_WORKER_LIST =""" +#[ +# { +# "binding":"AUTH_INBOX", +# "method":"rpcEmail", +# "keywords":[ +# "验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认", +# "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm" +# ] +# } +#] +#""" [[d1_databases]] binding = "DB" @@ -96,3 +110,8 @@ database_id = "xxx" # namespace_id = "1001" # # 10 requests per minute # simple = { limit = 10, period = 60 } + +# binding another worker service (parse the code or link), e.g. auth-inbox +# [[services]] +# binding = "AUTH_INBOX" +# service = "auth-inbox" \ No newline at end of file