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
|
# Transactional email
|
||||||
RESEND_API_KEY=
|
RESEND_API_KEY=
|
||||||
CLAWHUB_SECURITY_EMAIL=security@notifications.openclaw.ai
|
|
||||||
CLAWHUB_SECURITY_EMAIL_FROM=ClawHub Security <noreply@notifications.openclaw.ai>
|
CLAWHUB_SECURITY_EMAIL_FROM=ClawHub Security <noreply@notifications.openclaw.ai>
|
||||||
CLAWHUB_NOREPLY_FROM=ClawHub <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";
|
} from "./lib/emails";
|
||||||
|
|
||||||
const DEFAULT_FROM = "ClawHub Security <noreply@notifications.openclaw.ai>";
|
const DEFAULT_FROM = "ClawHub Security <noreply@notifications.openclaw.ai>";
|
||||||
const DEFAULT_REPLY_TO = "security@notifications.openclaw.ai";
|
|
||||||
|
|
||||||
const notificationArtifactValidator = v.object({
|
const notificationArtifactValidator = v.object({
|
||||||
kind: v.union(v.literal("skill"), v.literal("plugin")),
|
kind: v.union(v.literal("skill"), v.literal("plugin")),
|
||||||
@@ -32,7 +31,6 @@ function getEmailConfig() {
|
|||||||
return {
|
return {
|
||||||
apiKey: process.env.RESEND_API_KEY,
|
apiKey: process.env.RESEND_API_KEY,
|
||||||
from: process.env.CLAWHUB_SECURITY_EMAIL_FROM || DEFAULT_FROM,
|
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,
|
from: config.from,
|
||||||
to: args.to,
|
to: args.to,
|
||||||
replyTo: config.replyTo,
|
|
||||||
subject: args.subject,
|
subject: args.subject,
|
||||||
text: args.text,
|
text: args.text,
|
||||||
html: args.html,
|
html: args.html,
|
||||||
|
|||||||
@@ -8121,6 +8121,7 @@ describe("httpApiV1 handlers", () => {
|
|||||||
const resendBody = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));
|
const resendBody = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));
|
||||||
expect(resendBody.from).toBe("ClawHub <noreply@notifications.openclaw.ai>");
|
expect(resendBody.from).toBe("ClawHub <noreply@notifications.openclaw.ai>");
|
||||||
expect(resendBody.from).not.toBe("Legacy <legacy@example.com>");
|
expect(resendBody.from).not.toBe("Legacy <legacy@example.com>");
|
||||||
|
expect(resendBody).not.toHaveProperty("replyTo");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("staff email ignores legacy noreply sender env names", async () => {
|
it("staff email ignores legacy noreply sender env names", async () => {
|
||||||
@@ -8169,6 +8170,7 @@ describe("httpApiV1 handlers", () => {
|
|||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
const resendBody = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));
|
const resendBody = JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body));
|
||||||
expect(resendBody.from).toBe("ClawHub <noreply@notifications.openclaw.ai>");
|
expect(resendBody.from).toBe("ClawHub <noreply@notifications.openclaw.ai>");
|
||||||
|
expect(resendBody).not.toHaveProperty("replyTo");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("set role requires auth", async () => {
|
it("set role requires auth", async () => {
|
||||||
|
|||||||
@@ -112,8 +112,6 @@ Ensure Convex env is set (auth + embeddings):
|
|||||||
- `JWKS`
|
- `JWKS`
|
||||||
- `OPENAI_API_KEY`
|
- `OPENAI_API_KEY`
|
||||||
- `RESEND_API_KEY` for account-ban notification email
|
- `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_EMAIL_FROM` for the outbound From header, defaulting to
|
||||||
`ClawHub Security <noreply@notifications.openclaw.ai>` on the verified Resend
|
`ClawHub Security <noreply@notifications.openclaw.ai>` on the verified Resend
|
||||||
domain
|
domain
|
||||||
|
|||||||
Reference in New Issue
Block a user