feat: WmailHub v1.0.0 —— Cloudflare Email Workers 邮件接收与转发平台(本地可用版本)
- Console:React 19 + antd 6.6.5 前端(登录 admin/admin)+ Hono 管理 API (Nodes/Domains/Mailboxes/Emails/Webhooks/Keys/OAuth/Admin) - Node:Bun 转发节点(HMAC 签名、有限重试、心跳、结构化日志、优雅退出) - Email Worker:Cloudflare email handler + 本地联调入口 - shared:HMAC 协议(docs/04)、wmh_ Key(32 位)、AES-GCM 加密、SSRF 防护 - 邮件管线:postal-mime 解析、附件、catch-all、限额、malformed 降级、TTL 1 天 - Webhook:签名投递、指数退避重试、SSRF 校验、投递日志 - API v1 + MCP(JSON-RPC,六工具,凭据 kind 隔离) - Cloudflare OAuth:真实 + 本地 sim 模式,自动安装管线(幂等,无需手工命令) - 测试:bun test 41 例(协议安全/管线/TTL/Webhook/SSRF/MCP/OAuth/鉴权)+ Playwright UI 冒烟 - LOGO/favicon 由 codex CLI 生成
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
/** Email Worker 环境变量(wrangler vars 注入) */
|
||||
export interface ForwarderEnv {
|
||||
/** Node ingress 完整 URL */
|
||||
NODE_INGRESS_URL: string;
|
||||
/** Worker -> Node 共享密钥 */
|
||||
NODE_INGRESS_SECRET: string;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* WmailHub Email Worker(部署入口):
|
||||
* Cloudflare Email Routing -> email() handler -> Node ingress。
|
||||
* 只做转发,不存业务数据(docs/01);持有 Node ingress secret(docs/08)。
|
||||
*
|
||||
* 生产部署:Console provisioning 自动上传脚本;或 `bunx wrangler deploy`(worker/)。
|
||||
* 本地联调:`bun worker/src/local.ts` 起 HTTP 服务,POST /__email_test 模拟邮件事件。
|
||||
*/
|
||||
|
||||
import type { ForwarderEnv } from "./env.ts";
|
||||
|
||||
/** # 成员变量: Cloudflare Email Message 最小接口 **/
|
||||
export interface EmailMessage {
|
||||
from: string;
|
||||
to: string;
|
||||
raw: ReadableStream;
|
||||
}
|
||||
|
||||
/** # 函数
|
||||
* 转发 raw MIME 到 Node ingress
|
||||
*/
|
||||
export async function forward(env: ForwarderEnv, raw: ArrayBuffer, from: string, to: string): Promise<Response> {
|
||||
const resp = await fetch(env.NODE_INGRESS_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "message/rfc822",
|
||||
"X-WmailHub-Ingress-Secret": env.NODE_INGRESS_SECRET,
|
||||
"X-WmailHub-Mail-From": from,
|
||||
"X-WmailHub-Rcpt-To": to,
|
||||
},
|
||||
body: raw,
|
||||
});
|
||||
return resp;
|
||||
}
|
||||
|
||||
/** # 函数
|
||||
* 处理 Email Worker 邮件事件(与测试入口共用)
|
||||
*/
|
||||
export async function handleEmailEvent(env: ForwarderEnv, message: EmailMessage): Promise<void> {
|
||||
const raw = await new Response(message.raw).arrayBuffer();
|
||||
const resp = await forward(env, raw, message.from, message.to);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`node ingress responded ${resp.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** # 成员变量: Workers 导出(email + fetch) **/
|
||||
export default {
|
||||
async email(message: EmailMessage, env: ForwarderEnv): Promise<void> {
|
||||
await handleEmailEvent(env, message);
|
||||
},
|
||||
|
||||
async fetch(request: Request, env: ForwarderEnv): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
if (url.pathname === "/__health") {
|
||||
return Response.json({ ok: true, service: "wmailhub-email-worker" });
|
||||
}
|
||||
if (request.method === "POST" && url.pathname === "/__email_test") {
|
||||
if (request.headers.get("X-WmailHub-Ingress-Secret") !== env.NODE_INGRESS_SECRET) {
|
||||
return Response.json({ error: "forbidden" }, { status: 403 });
|
||||
}
|
||||
const from = request.headers.get("X-WmailHub-Test-From") || "sender@example.org";
|
||||
const to = request.headers.get("X-WmailHub-Test-To") || "hello@example.com";
|
||||
const raw = await request.arrayBuffer();
|
||||
try {
|
||||
const resp = await forward(env, raw, from, to);
|
||||
return Response.json({ ok: resp.ok, status: resp.status }, { status: resp.ok ? 200 : 502 });
|
||||
} catch (err) {
|
||||
return Response.json({ error: String(err) }, { status: 502 });
|
||||
}
|
||||
}
|
||||
return Response.json({ error: "not found" }, { status: 404 });
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Email Worker 本地联调服务:把部署入口的 fetch handler 跑在 Bun 上。
|
||||
* bun worker/src/local.ts
|
||||
* 测试:POST http://127.0.0.1:18789/__email_test
|
||||
* headers: X-WmailHub-Ingress-Secret / X-WmailHub-Test-From / X-WmailHub-Test-To
|
||||
* body: raw MIME
|
||||
*/
|
||||
|
||||
import worker from "./index.ts";
|
||||
import type { ForwarderEnv } from "./env.ts";
|
||||
|
||||
const env: ForwarderEnv = {
|
||||
NODE_INGRESS_URL: process.env.NODE_INGRESS_URL ?? "http://127.0.0.1:18788/ingress/email",
|
||||
NODE_INGRESS_SECRET: process.env.NODE_INGRESS_SECRET ?? "",
|
||||
};
|
||||
|
||||
if (!env.NODE_INGRESS_SECRET) {
|
||||
console.error("missing NODE_INGRESS_SECRET env");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const PORT = Number(process.env.WMAILHUB_WORKER_PORT ?? 18789);
|
||||
const server = Bun.serve({
|
||||
port: PORT,
|
||||
hostname: "0.0.0.0",
|
||||
fetch: (req) => worker.fetch(req, env),
|
||||
});
|
||||
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
t: new Date().toISOString(),
|
||||
lvl: "info",
|
||||
msg: "email worker started",
|
||||
port: PORT,
|
||||
node_ingress: env.NODE_INGRESS_URL,
|
||||
}),
|
||||
);
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
server.stop(true);
|
||||
process.exit(0);
|
||||
});
|
||||
process.on("SIGTERM", () => {
|
||||
server.stop(true);
|
||||
process.exit(0);
|
||||
});
|
||||
Reference in New Issue
Block a user