diff --git a/.env.local.example b/.env.local.example index ddb17226..83408086 100644 --- a/.env.local.example +++ b/.env.local.example @@ -24,6 +24,5 @@ OPENAI_API_KEY= # Transactional email RESEND_API_KEY= -CLAWHUB_SECURITY_EMAIL=security@notifications.openclaw.ai CLAWHUB_SECURITY_EMAIL_FROM=ClawHub Security CLAWHUB_NOREPLY_FROM=ClawHub diff --git a/convex/emailsNode.test.ts b/convex/emailsNode.test.ts new file mode 100644 index 00000000..4f525638 --- /dev/null +++ b/convex/emailsNode.test.ts @@ -0,0 +1,70 @@ +/* @vitest-environment node */ + +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { resendConstructorMock, resendSendMock } = vi.hoisted(() => ({ + resendConstructorMock: vi.fn(function ResendMock() { + return { emails: { send: resendSendMock } }; + }), + resendSendMock: vi.fn(), +})); + +vi.mock("resend", () => ({ + Resend: resendConstructorMock, +})); + +const { sendBanNotificationInternal } = await import("./emailsNode"); + +type SendBanNotificationHandler = { + _handler: ( + ctx: unknown, + args: { + userId: string; + bannedAt: number; + to: string; + handle?: string; + source: "manual" | "autoban"; + reason?: string; + }, + ) => Promise; +}; + +describe("transactional account emails", () => { + beforeEach(() => { + vi.stubEnv("RESEND_API_KEY", "resend_test"); + resendConstructorMock.mockClear(); + resendSendMock.mockReset(); + resendSendMock.mockResolvedValue({ data: { id: "email_123" }, error: null }); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("sends ban notifications without a Reply-To header", async () => { + const result = await ( + sendBanNotificationInternal as unknown as SendBanNotificationHandler + )._handler( + {}, + { + userId: "users:target", + bannedAt: 1_700_000_000_000, + to: "target@example.com", + handle: "target", + source: "manual", + reason: "security review", + }, + ); + + expect(result).toEqual({ ok: true, id: "email_123" }); + expect(resendSendMock).toHaveBeenCalledTimes(1); + const [payload, options] = resendSendMock.mock.calls[0] ?? []; + expect(payload).toMatchObject({ + from: "ClawHub Security ", + to: "target@example.com", + subject: "Your ClawHub account has been suspended", + }); + expect(payload).not.toHaveProperty("replyTo"); + expect(options).toEqual({ idempotencyKey: "ban:users:target:1700000000000" }); + }); +}); diff --git a/convex/emailsNode.ts b/convex/emailsNode.ts index 2438003c..246490d5 100644 --- a/convex/emailsNode.ts +++ b/convex/emailsNode.ts @@ -13,7 +13,6 @@ import { } from "./lib/emails"; const DEFAULT_FROM = "ClawHub Security "; -const DEFAULT_REPLY_TO = "security@notifications.openclaw.ai"; const notificationArtifactValidator = v.object({ kind: v.union(v.literal("skill"), v.literal("plugin")), @@ -32,7 +31,6 @@ function getEmailConfig() { return { apiKey: process.env.RESEND_API_KEY, from: process.env.CLAWHUB_SECURITY_EMAIL_FROM || DEFAULT_FROM, - replyTo: process.env.CLAWHUB_SECURITY_EMAIL || DEFAULT_REPLY_TO, }; } @@ -60,7 +58,6 @@ async function sendTransactionalEmail(args: SendEmailArgs) { { from: config.from, to: args.to, - replyTo: config.replyTo, subject: args.subject, text: args.text, html: args.html, diff --git a/convex/httpApiV1.handlers.test.ts b/convex/httpApiV1.handlers.test.ts index abb5d7d7..fe8a5183 100644 --- a/convex/httpApiV1.handlers.test.ts +++ b/convex/httpApiV1.handlers.test.ts @@ -8121,6 +8121,7 @@ describe("httpApiV1 handlers", () => { const resendBody = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body)); expect(resendBody.from).toBe("ClawHub "); expect(resendBody.from).not.toBe("Legacy "); + expect(resendBody).not.toHaveProperty("replyTo"); }); it("staff email ignores legacy noreply sender env names", async () => { @@ -8169,6 +8170,7 @@ describe("httpApiV1 handlers", () => { expect(response.status).toBe(200); const resendBody = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body)); expect(resendBody.from).toBe("ClawHub "); + expect(resendBody).not.toHaveProperty("replyTo"); }); it("set role requires auth", async () => { diff --git a/specs/deploy.md b/specs/deploy.md index 546fa32e..65d9b5dc 100644 --- a/specs/deploy.md +++ b/specs/deploy.md @@ -112,8 +112,6 @@ Ensure Convex env is set (auth + embeddings): - `JWKS` - `OPENAI_API_KEY` - `RESEND_API_KEY` for account-ban notification email -- `CLAWHUB_SECURITY_EMAIL` for account-action replies, defaulting to - `security@notifications.openclaw.ai` - `CLAWHUB_SECURITY_EMAIL_FROM` for the outbound From header, defaulting to `ClawHub Security ` on the verified Resend domain