From 630f15c85ed777ae03e57e8c17b2a0936658641c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 29 Jun 2026 19:28:29 -0500 Subject: [PATCH 1/3] HOLD MERGE: move brand email to @recoupable.dev (+ inbound @mail.recoupable.dev) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OUTBOUND_EMAIL_DOMAIN @recoupable.com → @recoupable.dev (RECOUP_FROM_EMAIL derives from it: agent@recoupable.dev) - INBOUND_EMAIL_DOMAIN @mail.recoupable.com → @mail.recoupable.dev - shared@ (getSharedAccountConnections), sidney@ (isTestEmail), noreply (resolveGitUser), and from-address JSDoc → .dev - updated the coupled test assertions (getFromWithName, processAndSendEmail, resolveGitUser) to the new domains DO NOT MERGE until the recoupable.dev sending domain is verified in Resend AND MX/inbound on mail.recoupable.dev is live — else outbound bounces and inbound replies stop arriving. Per recoupable/chat#1819 decision 2026-06-29. Refs recoupable/chat#1819. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/api/emails/route.ts | 2 +- .../toolRouter/getSharedAccountConnections.ts | 2 +- lib/const.ts | 8 ++-- .../__tests__/processAndSendEmail.test.ts | 2 +- .../inbound/__tests__/getFromWithName.test.ts | 44 +++++++++---------- lib/emails/inbound/getFromWithName.ts | 2 +- lib/emails/isTestEmail.ts | 2 +- lib/emails/sendEmailHandler.ts | 2 +- lib/sandbox/__tests__/resolveGitUser.test.ts | 6 +-- lib/sandbox/resolveGitUser.ts | 2 +- 10 files changed, 36 insertions(+), 36 deletions(-) diff --git a/app/api/emails/route.ts b/app/api/emails/route.ts index df3555ae8..412379eab 100644 --- a/app/api/emails/route.ts +++ b/app/api/emails/route.ts @@ -18,7 +18,7 @@ export async function OPTIONS() { * POST /api/emails * * Sends an email to one or more explicit recipients via Resend. Emails are sent - * from `Agent by Recoup `. Account-scoped — requires + * from `Agent by Recoup `. Account-scoped — requires * authentication via x-api-key header or Authorization Bearer token. * * Body parameters: diff --git a/lib/composio/toolRouter/getSharedAccountConnections.ts b/lib/composio/toolRouter/getSharedAccountConnections.ts index 56e63c342..3facaa756 100644 --- a/lib/composio/toolRouter/getSharedAccountConnections.ts +++ b/lib/composio/toolRouter/getSharedAccountConnections.ts @@ -15,7 +15,7 @@ const SHARED_ACCOUNT_ID = "recoup-shared-767f498e-e1e9-43c6-a152-a96ae3bd8d07"; * Get Google Drive/Sheets/Docs connections from the shared Recoupable account. * * When a customer doesn't want to grant full Google Drive access, - * they can share specific files with shared@recoupable.com instead. + * they can share specific files with shared@recoupable.dev instead. * This function fetches the shared account's connections so they * can be used as a fallback in tool router sessions. * diff --git a/lib/const.ts b/lib/const.ts index 47dc64d85..395357809 100644 --- a/lib/const.ts +++ b/lib/const.ts @@ -15,11 +15,11 @@ export const PRIVY_PROJECT_SECRET = process.env.PRIVY_PROJECT_SECRET; /** Base URL for the public API documentation site */ export const DOCS_BASE_URL = "https://docs.recoupable.dev"; -/** Domain for receiving inbound emails (e.g., support@mail.recoupable.com) */ -export const INBOUND_EMAIL_DOMAIN = "@mail.recoupable.com"; +/** Domain for receiving inbound emails (e.g., support@mail.recoupable.dev) */ +export const INBOUND_EMAIL_DOMAIN = "@mail.recoupable.dev"; -/** Domain for sending outbound emails (e.g., support@recoupable.com) */ -export const OUTBOUND_EMAIL_DOMAIN = "@recoupable.com"; +/** Domain for sending outbound emails (e.g., support@recoupable.dev) */ +export const OUTBOUND_EMAIL_DOMAIN = "@recoupable.dev"; /** Default from address for outbound emails */ export const RECOUP_FROM_EMAIL = `Agent by Recoup `; diff --git a/lib/emails/__tests__/processAndSendEmail.test.ts b/lib/emails/__tests__/processAndSendEmail.test.ts index 4a6b14d23..f4940b133 100644 --- a/lib/emails/__tests__/processAndSendEmail.test.ts +++ b/lib/emails/__tests__/processAndSendEmail.test.ts @@ -34,7 +34,7 @@ describe("processAndSendEmail", () => { } expect(mockSendEmailWithResend).toHaveBeenCalledWith( expect.objectContaining({ - from: "Agent by Recoup ", + from: "Agent by Recoup ", to: ["user@example.com"], subject: "Test", html: expect.stringContaining("Hello world"), diff --git a/lib/emails/inbound/__tests__/getFromWithName.test.ts b/lib/emails/inbound/__tests__/getFromWithName.test.ts index 3b0c867d8..23e51c886 100644 --- a/lib/emails/inbound/__tests__/getFromWithName.test.ts +++ b/lib/emails/inbound/__tests__/getFromWithName.test.ts @@ -3,81 +3,81 @@ import { getFromWithName } from "../getFromWithName"; describe("getFromWithName", () => { describe("outbound domain conversion", () => { - it("converts inbound @mail.recoupable.com to outbound @recoupable.com", () => { - const result = getFromWithName(["support@mail.recoupable.com"]); + it("converts inbound @mail.recoupable.dev to outbound @recoupable.dev", () => { + const result = getFromWithName(["support@mail.recoupable.dev"]); - expect(result).toBe("Support by Recoup "); + expect(result).toBe("Support by Recoup "); }); it("preserves the email name when converting domains", () => { - const result = getFromWithName(["agent@mail.recoupable.com"]); + const result = getFromWithName(["agent@mail.recoupable.dev"]); - expect(result).toBe("Agent by Recoup "); + expect(result).toBe("Agent by Recoup "); }); }); describe("finding inbound email", () => { it("finds recoup email in to array", () => { - const result = getFromWithName(["hello@mail.recoupable.com"]); + const result = getFromWithName(["hello@mail.recoupable.dev"]); - expect(result).toBe("Hello by Recoup "); + expect(result).toBe("Hello by Recoup "); }); it("finds recoup email among multiple to addresses", () => { const result = getFromWithName([ "other@example.com", - "support@mail.recoupable.com", + "support@mail.recoupable.dev", "another@example.com", ]); - expect(result).toBe("Support by Recoup "); + expect(result).toBe("Support by Recoup "); }); it("falls back to cc array when not in to array", () => { - const result = getFromWithName(["other@example.com"], ["support@mail.recoupable.com"]); + const result = getFromWithName(["other@example.com"], ["support@mail.recoupable.dev"]); - expect(result).toBe("Support by Recoup "); + expect(result).toBe("Support by Recoup "); }); it("prefers to array over cc array", () => { const result = getFromWithName( - ["to-agent@mail.recoupable.com"], - ["cc-agent@mail.recoupable.com"], + ["to-agent@mail.recoupable.dev"], + ["cc-agent@mail.recoupable.dev"], ); - expect(result).toBe("To-agent by Recoup "); + expect(result).toBe("To-agent by Recoup "); }); it("handles case-insensitive domain matching", () => { - const result = getFromWithName(["Support@MAIL.RECOUPABLE.COM"]); + const result = getFromWithName(["Support@MAIL.RECOUPABLE.DEV"]); - expect(result).toBe("Support by Recoup "); + expect(result).toBe("Support by Recoup "); }); }); describe("error handling", () => { it("throws error when no recoup email found in to or cc", () => { expect(() => getFromWithName(["other@example.com"])).toThrow( - "No email found ending with @mail.recoupable.com", + "No email found ending with @mail.recoupable.dev", ); }); it("throws error when arrays are empty", () => { - expect(() => getFromWithName([])).toThrow("No email found ending with @mail.recoupable.com"); + expect(() => getFromWithName([])).toThrow("No email found ending with @mail.recoupable.dev"); }); }); describe("name formatting", () => { it("capitalizes first letter of name", () => { - const result = getFromWithName(["lowercase@mail.recoupable.com"]); + const result = getFromWithName(["lowercase@mail.recoupable.dev"]); - expect(result).toBe("Lowercase by Recoup "); + expect(result).toBe("Lowercase by Recoup "); }); it("preserves rest of name casing", () => { - const result = getFromWithName(["myAgent@mail.recoupable.com"]); + const result = getFromWithName(["myAgent@mail.recoupable.dev"]); - expect(result).toBe("MyAgent by Recoup "); + expect(result).toBe("MyAgent by Recoup "); }); }); }); diff --git a/lib/emails/inbound/getFromWithName.ts b/lib/emails/inbound/getFromWithName.ts index cac863610..53a8e8f63 100644 --- a/lib/emails/inbound/getFromWithName.ts +++ b/lib/emails/inbound/getFromWithName.ts @@ -6,7 +6,7 @@ import { OUTBOUND_EMAIL_DOMAIN, INBOUND_EMAIL_DOMAIN } from "@/lib/const"; * * @param toEmails - Array of email addresses from the 'to' field * @param ccEmails - Optional array of email addresses from the 'cc' field (fallback) - * @returns Formatted email address with display name (e.g., "Support by Recoup ") + * @returns Formatted email address with display name (e.g., "Support by Recoup ") * @throws Error if no email ending with the inbound domain is found in either array */ export function getFromWithName(toEmails: string[], ccEmails: string[] = []): string { diff --git a/lib/emails/isTestEmail.ts b/lib/emails/isTestEmail.ts index 8a05a3525..1bf2cecdb 100644 --- a/lib/emails/isTestEmail.ts +++ b/lib/emails/isTestEmail.ts @@ -1,4 +1,4 @@ // Returns true if the email is a test email export const isTestEmail = (email: string): boolean => { - return email === "sweetmantech@gmail.com" || email === "sidney@recoupable.com"; + return email === "sweetmantech@gmail.com" || email === "sidney@recoupable.dev"; }; diff --git a/lib/emails/sendEmailHandler.ts b/lib/emails/sendEmailHandler.ts index 006bd0028..8eac17e53 100644 --- a/lib/emails/sendEmailHandler.ts +++ b/lib/emails/sendEmailHandler.ts @@ -7,7 +7,7 @@ import { processAndSendEmail } from "@/lib/emails/processAndSendEmail"; * Handler for POST /api/emails. * * Sends an email to the explicit recipients in the request body via Resend - * (from `Agent by Recoup `), reusing the same + * (from `Agent by Recoup `), reusing the same * `processAndSendEmail` domain function as the `send_email` MCP tool. * Account-scoped: requires authentication via x-api-key or Authorization Bearer. * Body validation, auth, and the recipient restriction all live in diff --git a/lib/sandbox/__tests__/resolveGitUser.test.ts b/lib/sandbox/__tests__/resolveGitUser.test.ts index 04aef07f5..5b52c05f7 100644 --- a/lib/sandbox/__tests__/resolveGitUser.test.ts +++ b/lib/sandbox/__tests__/resolveGitUser.test.ts @@ -53,7 +53,7 @@ describe("resolveGitUser", () => { const gitUser = await resolveGitUser(ACCOUNT_ID); expect(gitUser.name).toBe("Ada Lovelace"); - expect(gitUser.email).toBe(`${ACCOUNT_ID}@users.noreply.recoupable.com`); + expect(gitUser.email).toBe(`${ACCOUNT_ID}@users.noreply.recoupable.dev`); }); it("falls back on both fields when nothing is on file", async () => { @@ -64,7 +64,7 @@ describe("resolveGitUser", () => { expect(gitUser).toEqual({ name: `recoupable-${ACCOUNT_ID.slice(0, 8)}`, - email: `${ACCOUNT_ID}@users.noreply.recoupable.com`, + email: `${ACCOUNT_ID}@users.noreply.recoupable.dev`, }); }); @@ -78,6 +78,6 @@ describe("resolveGitUser", () => { const gitUser = await resolveGitUser(ACCOUNT_ID); - expect(gitUser.email).toBe(`${ACCOUNT_ID}@users.noreply.recoupable.com`); + expect(gitUser.email).toBe(`${ACCOUNT_ID}@users.noreply.recoupable.dev`); }); }); diff --git a/lib/sandbox/resolveGitUser.ts b/lib/sandbox/resolveGitUser.ts index 12d3d027f..7e7efc47e 100644 --- a/lib/sandbox/resolveGitUser.ts +++ b/lib/sandbox/resolveGitUser.ts @@ -33,7 +33,7 @@ export async function resolveGitUser(accountId: string): Promise { const emailRow = emails.find(row => typeof row.email === "string" && row.email.length > 0); const name = account?.name?.trim() || `recoupable-${accountId.slice(0, 8)}`; - const email = emailRow?.email ?? `${accountId}@users.noreply.recoupable.com`; + const email = emailRow?.email ?? `${accountId}@users.noreply.recoupable.dev`; return { name, email }; } From b20e2492e1c5d23d12bfbba3c976d9e6f9763b9c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 29 Jun 2026 19:39:29 -0500 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20inbound=20email=20domain=20=E2=86=92?= =?UTF-8?q?=20apex=20@recoupable.dev=20(never=20mail.recoupable.dev)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per decision: use the apex @recoupable.dev for BOTH outbound and inbound — no mail. subdomain. The inbound MX already sits on the apex recoupable.dev, so code + DNS now agree. INBOUND_EMAIL_DOMAIN @mail.recoupable.dev → @recoupable.dev; getFromWithName's local-part-preserving swap is unchanged (now domain-identity). Tests updated. recoupable.dev is verified in Resend, so outbound sends. Refs recoupable/chat#1819. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/const.ts | 4 +-- .../inbound/__tests__/getFromWithName.test.ts | 26 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/const.ts b/lib/const.ts index 395357809..5b178dd05 100644 --- a/lib/const.ts +++ b/lib/const.ts @@ -15,8 +15,8 @@ export const PRIVY_PROJECT_SECRET = process.env.PRIVY_PROJECT_SECRET; /** Base URL for the public API documentation site */ export const DOCS_BASE_URL = "https://docs.recoupable.dev"; -/** Domain for receiving inbound emails (e.g., support@mail.recoupable.dev) */ -export const INBOUND_EMAIL_DOMAIN = "@mail.recoupable.dev"; +/** Domain for receiving inbound emails (e.g., support@recoupable.dev) */ +export const INBOUND_EMAIL_DOMAIN = "@recoupable.dev"; /** Domain for sending outbound emails (e.g., support@recoupable.dev) */ export const OUTBOUND_EMAIL_DOMAIN = "@recoupable.dev"; diff --git a/lib/emails/inbound/__tests__/getFromWithName.test.ts b/lib/emails/inbound/__tests__/getFromWithName.test.ts index 23e51c886..ebdd8370a 100644 --- a/lib/emails/inbound/__tests__/getFromWithName.test.ts +++ b/lib/emails/inbound/__tests__/getFromWithName.test.ts @@ -3,14 +3,14 @@ import { getFromWithName } from "../getFromWithName"; describe("getFromWithName", () => { describe("outbound domain conversion", () => { - it("converts inbound @mail.recoupable.dev to outbound @recoupable.dev", () => { - const result = getFromWithName(["support@mail.recoupable.dev"]); + it("converts inbound @recoupable.dev to outbound @recoupable.dev", () => { + const result = getFromWithName(["support@recoupable.dev"]); expect(result).toBe("Support by Recoup "); }); it("preserves the email name when converting domains", () => { - const result = getFromWithName(["agent@mail.recoupable.dev"]); + const result = getFromWithName(["agent@recoupable.dev"]); expect(result).toBe("Agent by Recoup "); }); @@ -18,7 +18,7 @@ describe("getFromWithName", () => { describe("finding inbound email", () => { it("finds recoup email in to array", () => { - const result = getFromWithName(["hello@mail.recoupable.dev"]); + const result = getFromWithName(["hello@recoupable.dev"]); expect(result).toBe("Hello by Recoup "); }); @@ -26,7 +26,7 @@ describe("getFromWithName", () => { it("finds recoup email among multiple to addresses", () => { const result = getFromWithName([ "other@example.com", - "support@mail.recoupable.dev", + "support@recoupable.dev", "another@example.com", ]); @@ -34,22 +34,22 @@ describe("getFromWithName", () => { }); it("falls back to cc array when not in to array", () => { - const result = getFromWithName(["other@example.com"], ["support@mail.recoupable.dev"]); + const result = getFromWithName(["other@example.com"], ["support@recoupable.dev"]); expect(result).toBe("Support by Recoup "); }); it("prefers to array over cc array", () => { const result = getFromWithName( - ["to-agent@mail.recoupable.dev"], - ["cc-agent@mail.recoupable.dev"], + ["to-agent@recoupable.dev"], + ["cc-agent@recoupable.dev"], ); expect(result).toBe("To-agent by Recoup "); }); it("handles case-insensitive domain matching", () => { - const result = getFromWithName(["Support@MAIL.RECOUPABLE.DEV"]); + const result = getFromWithName(["Support@RECOUPABLE.DEV"]); expect(result).toBe("Support by Recoup "); }); @@ -58,24 +58,24 @@ describe("getFromWithName", () => { describe("error handling", () => { it("throws error when no recoup email found in to or cc", () => { expect(() => getFromWithName(["other@example.com"])).toThrow( - "No email found ending with @mail.recoupable.dev", + "No email found ending with @recoupable.dev", ); }); it("throws error when arrays are empty", () => { - expect(() => getFromWithName([])).toThrow("No email found ending with @mail.recoupable.dev"); + expect(() => getFromWithName([])).toThrow("No email found ending with @recoupable.dev"); }); }); describe("name formatting", () => { it("capitalizes first letter of name", () => { - const result = getFromWithName(["lowercase@mail.recoupable.dev"]); + const result = getFromWithName(["lowercase@recoupable.dev"]); expect(result).toBe("Lowercase by Recoup "); }); it("preserves rest of name casing", () => { - const result = getFromWithName(["myAgent@mail.recoupable.dev"]); + const result = getFromWithName(["myAgent@recoupable.dev"]); expect(result).toBe("MyAgent by Recoup "); }); From fc067651bd0e1677ba0b9e78364853a5f5e79f0c Mon Sep 17 00:00:00 2001 From: Sweets Sweetman Date: Mon, 29 Jun 2026 20:58:25 -0500 Subject: [PATCH 3/3] style: prettier fix for getFromWithName test (shorter .dev addresses) Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/emails/inbound/__tests__/getFromWithName.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/emails/inbound/__tests__/getFromWithName.test.ts b/lib/emails/inbound/__tests__/getFromWithName.test.ts index ebdd8370a..7dfaa6e40 100644 --- a/lib/emails/inbound/__tests__/getFromWithName.test.ts +++ b/lib/emails/inbound/__tests__/getFromWithName.test.ts @@ -40,10 +40,7 @@ describe("getFromWithName", () => { }); it("prefers to array over cc array", () => { - const result = getFromWithName( - ["to-agent@recoupable.dev"], - ["cc-agent@recoupable.dev"], - ); + const result = getFromWithName(["to-agent@recoupable.dev"], ["cc-agent@recoupable.dev"]); expect(result).toBe("To-agent by Recoup "); });