mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
fix: remove reply-to from transactional emails (#2725)
This commit is contained in:
@@ -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 <noreply@notifications.openclaw.ai>
|
||||
CLAWHUB_NOREPLY_FROM=ClawHub <noreply@notifications.openclaw.ai>
|
||||
|
||||
@@ -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<unknown>;
|
||||
};
|
||||
|
||||
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 <noreply@notifications.openclaw.ai>",
|
||||
to: "target@example.com",
|
||||
subject: "Your ClawHub account has been suspended",
|
||||
});
|
||||
expect(payload).not.toHaveProperty("replyTo");
|
||||
expect(options).toEqual({ idempotencyKey: "ban:users:target:1700000000000" });
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
} from "./lib/emails";
|
||||
|
||||
const DEFAULT_FROM = "ClawHub Security <noreply@notifications.openclaw.ai>";
|
||||
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,
|
||||
|
||||
@@ -8121,6 +8121,7 @@ describe("httpApiV1 handlers", () => {
|
||||
const resendBody = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));
|
||||
expect(resendBody.from).toBe("ClawHub <noreply@notifications.openclaw.ai>");
|
||||
expect(resendBody.from).not.toBe("Legacy <legacy@example.com>");
|
||||
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 <noreply@notifications.openclaw.ai>");
|
||||
expect(resendBody).not.toHaveProperty("replyTo");
|
||||
});
|
||||
|
||||
it("set role requires auth", async () => {
|
||||
|
||||
@@ -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 <noreply@notifications.openclaw.ai>` on the verified Resend
|
||||
domain
|
||||
|
||||
Reference in New Issue
Block a user