Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
2. Push commits to the current feature branch
3. **NEVER push directly to `main` or `test` branches** - always use feature branches and PRs
4. Before pushing, verify the current branch is not `main` or `test`
5. **Open PRs against the `test` branch**, not `main`

### Starting a New Task

Expand Down
6 changes: 3 additions & 3 deletions lib/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const INBOUND_EMAIL_DOMAIN = "@mail.recoupable.com";
/** Domain for sending outbound emails (e.g., [email protected]) */
export const OUTBOUND_EMAIL_DOMAIN = "@recoupable.com";

export const SUPABASE_STORAGE_BUCKET = "user-files";

/** Default from address for outbound emails sent by the agent */
/** Default from address for outbound emails */
export const RECOUP_FROM_EMAIL = `Agent by Recoup <agent${OUTBOUND_EMAIL_DOMAIN}>`;

export const SUPABASE_STORAGE_BUCKET = "user-files";
41 changes: 41 additions & 0 deletions lib/emails/__tests__/getEmailFooter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { getEmailFooter } from "../getEmailFooter";

describe("getEmailFooter", () => {
it("includes reply note in all cases", () => {
const footer = getEmailFooter();
expect(footer).toContain("you can reply directly to this email");
});

it("includes horizontal rule", () => {
const footer = getEmailFooter();
expect(footer).toContain("<hr");
});

it("excludes chat link when roomId is not provided", () => {
const footer = getEmailFooter();
expect(footer).not.toContain("chat.recoupable.com");
expect(footer).not.toContain("Or continue the conversation");
});

it("includes chat link when roomId is provided", () => {
const roomId = "test-room-123";
const footer = getEmailFooter(roomId);
expect(footer).toContain(`https://chat.recoupable.com/chat/${roomId}`);
expect(footer).toContain("Or continue the conversation on Recoup");
});

it("generates proper HTML with roomId", () => {
const roomId = "my-room-id";
const footer = getEmailFooter(roomId);
expect(footer).toContain(`href="https://chat.recoupable.com/chat/${roomId}"`);
expect(footer).toContain('target="_blank"');
expect(footer).toContain('rel="noopener noreferrer"');
});

it("applies proper styling", () => {
const footer = getEmailFooter("room-id");
expect(footer).toContain("font-size:12px");
expect(footer).toContain("color:#6b7280");
});
});
27 changes: 27 additions & 0 deletions lib/emails/getEmailFooter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Generates a standardized email footer HTML.
*
* @param roomId - Optional room ID for the chat link. If not provided, only the reply note is shown.
* @returns HTML string for the email footer.
*/
export function getEmailFooter(roomId?: string): string {
const replyNote = `
<p style="font-size:12px;color:#6b7280;margin:0 0 4px;">
Note: you can reply directly to this email to continue the conversation.
</p>`.trim();

const chatLink = roomId
? `
<p style="font-size:12px;color:#6b7280;margin:0;">
Or continue the conversation on Recoup:
<a href="https://chat.recoupable.com/chat/${roomId}" target="_blank" rel="noopener noreferrer">
https://chat.recoupable.com/chat/${roomId}
</a>
</p>`.trim()
: "";

return `
<hr style="margin-top:24px;margin-bottom:16px;border:none;border-top:1px solid #e5e7eb;" />
${replyNote}
${chatLink}`.trim();
}
16 changes: 2 additions & 14 deletions lib/emails/inbound/generateEmailResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { marked } from "marked";
import { ChatRequestBody } from "@/lib/chat/validateChatRequest";
import getGeneralAgent from "@/lib/agents/generalAgent/getGeneralAgent";
import { getEmailRoomMessages } from "@/lib/emails/inbound/getEmailRoomMessages";
import { getEmailFooter } from "@/lib/emails/getEmailFooter";

/**
* Generates the assistant response HTML for an email, including:
Expand Down Expand Up @@ -29,20 +30,7 @@ export async function generateEmailResponse(
const text = chatResponse.text;

const bodyHtml = marked(text);

const footerHtml = `
<hr style="margin-top:24px;margin-bottom:16px;border:none;border-top:1px solid #e5e7eb;" />
<p style="font-size:12px;color:#6b7280;margin:0 0 4px;">
Note: you can reply directly to this email to continue the conversation.
</p>
<p style="font-size:12px;color:#6b7280;margin:0;">
Or continue the conversation on Recoup:
<a href="https://chat.recoupable.com/chat/${roomId}" target="_blank" rel="noopener noreferrer">
https://chat.recoupable.com/chat/${roomId}
</a>
</p>
`.trim();

const footerHtml = getEmailFooter(roomId);
const html = `${bodyHtml}\n\n${footerHtml}`;

return { text, html };
Expand Down
4 changes: 4 additions & 0 deletions lib/emails/sendEmailSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export const sendEmailSchema = z.object({
.describe("Optional custom headers for the email")
.default({})
.optional(),
room_id: z
.string()
.describe("Optional room ID to include in the email footer link")
.optional(),
});

export type SendEmailInput = z.infer<typeof sendEmailSchema>;
3 changes: 1 addition & 2 deletions lib/mcp/tools/__tests__/registerSendEmailTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ describe("registerSendEmailTool", () => {
to: ["[email protected]"],
cc: undefined,
subject: "Test Subject",
text: "Test body",
html: undefined,
html: expect.stringMatching(/Test body.*you can reply directly to this email/s),
headers: {},
});

Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export const registerAllTools = (server: McpServer): void => {
registerContactTeamTool(server);
registerGetLocalTimeTool(server);
registerSearchWebTool(server);
registerSendEmailTool(server);
registerUpdateAccountInfoTool(server);
registerCreateSegmentsTool(server);
registerAllYouTubeTools(server);
registerSendEmailTool(server);
};
10 changes: 7 additions & 3 deletions lib/mcp/tools/registerSendEmailTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { sendEmailWithResend } from "@/lib/emails/sendEmail";
import { getToolResultSuccess } from "@/lib/mcp/getToolResultSuccess";
import { getToolResultError } from "@/lib/mcp/getToolResultError";
import { RECOUP_FROM_EMAIL } from "@/lib/const";
import { getEmailFooter } from "@/lib/emails/getEmailFooter";
import { NextResponse } from "next/server";

/**
Expand All @@ -20,15 +21,18 @@ export function registerSendEmailTool(server: McpServer): void {
inputSchema: sendEmailSchema,
},
async (args: SendEmailInput) => {
const { to, cc = [], subject, text, html = "", headers = {} } = args;
const { to, cc = [], subject, text, html = "", headers = {}, room_id } = args;

const footer = getEmailFooter(room_id);
const bodyHtml = html || (text ? `<p>${text}</p>` : "");
const htmlWithFooter = `${bodyHtml}\n\n${footer}`;

const result = await sendEmailWithResend({
from: RECOUP_FROM_EMAIL,
to,
cc: cc.length > 0 ? cc : undefined,
subject,
text,
html: html || undefined,
html: htmlWithFooter,
headers,
});

Expand Down