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
10 changes: 9 additions & 1 deletion apps/web/utils/ai/reply/draft-with-knowledge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const getUserPrompt = ({
calendarAvailability,
writingStyle,
mcpContext,
meetingContext,
}: {
messages: (EmailForLLM & { to: string })[];
emailAccount: EmailAccountWithAI;
Expand All @@ -47,6 +48,7 @@ const getUserPrompt = ({
calendarAvailability: CalendarAvailabilityContext | null;
writingStyle: string | null;
mcpContext: string | null;
meetingContext: string | null;
}) => {
const userAbout = emailAccount.about
? `Context about the user:
Expand Down Expand Up @@ -137,13 +139,15 @@ You can suggest this booking link if it helps with scheduling (e.g., "Feel free

const mcpToolsContext = mcpContext
? `Additional context fetched from external tools (such as CRM systems, task managers, or other integrations) that may help draft a response:

<external_tools_context>
${mcpContext}
</external_tools_context>
`
: "";

const upcomingMeetingsContext = meetingContext || "";

return `${userAbout}
${relevantKnowledge}
${historicalContext}
Expand All @@ -152,6 +156,7 @@ ${writingStylePrompt}
${calendarContext}
${bookingLinkContext}
${mcpToolsContext}
${upcomingMeetingsContext}

Here is the context of the email thread (from oldest to newest):
${getEmailListPrompt({ messages, messageMaxLength: 3000 })}
Expand All @@ -178,6 +183,7 @@ export async function aiDraftWithKnowledge({
calendarAvailability,
writingStyle,
mcpContext,
meetingContext,
}: {
messages: (EmailForLLM & { to: string })[];
emailAccount: EmailAccountWithAI;
Expand All @@ -187,6 +193,7 @@ export async function aiDraftWithKnowledge({
calendarAvailability: CalendarAvailabilityContext | null;
writingStyle: string | null;
mcpContext: string | null;
meetingContext: string | null;
}) {
try {
logger.info("Drafting email with knowledge base", {
Expand All @@ -211,6 +218,7 @@ export async function aiDraftWithKnowledge({
calendarAvailability,
writingStyle,
mcpContext,
meetingContext,
});

const modelOptions = getModel(emailAccount.user);
Expand Down
119 changes: 119 additions & 0 deletions apps/web/utils/email.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
import {
extractNameFromEmail,
extractEmailAddress,
extractEmailAddresses,
extractDomainFromEmail,
participant,
normalizeEmailAddress,
Expand Down Expand Up @@ -33,6 +34,124 @@ describe("email utils", () => {
});
});

describe("extractEmailAddresses", () => {
it("returns empty array for empty string", () => {
expect(extractEmailAddresses("")).toEqual([]);
});

it("extracts single email address", () => {
expect(extractEmailAddresses("john@example.com")).toEqual([
"john@example.com",
]);
});

it("extracts multiple email addresses separated by commas", () => {
expect(
extractEmailAddresses("john@example.com, jane@example.com"),
).toEqual(["john@example.com", "jane@example.com"]);
});

it("extracts emails from format 'Name <email>'", () => {
expect(extractEmailAddresses("John Doe <john@example.com>")).toEqual([
"john@example.com",
]);
});

it("extracts multiple emails with names", () => {
expect(
extractEmailAddresses(
"John Doe <john@example.com>, Jane Smith <jane@example.com>",
),
).toEqual(["john@example.com", "jane@example.com"]);
});

it("handles mixed formats (with and without names)", () => {
expect(
extractEmailAddresses("John Doe <john@example.com>, jane@example.com"),
).toEqual(["john@example.com", "jane@example.com"]);
});

it("handles commas inside quoted names", () => {
expect(
extractEmailAddresses(
'"Doe, John" <john@example.com>, jane@example.com',
),
).toEqual(["john@example.com", "jane@example.com"]);
});

it("trims whitespace around email addresses", () => {
expect(
extractEmailAddresses(" john@example.com , jane@example.com "),
).toEqual(["john@example.com", "jane@example.com"]);
});

it("filters out invalid email addresses", () => {
expect(extractEmailAddresses("invalid-email, valid@example.com")).toEqual(
["valid@example.com"],
);
});

it("handles multiple commas and extra spaces", () => {
expect(
extractEmailAddresses(
"john@example.com , jane@example.com , bob@example.com",
),
).toEqual(["john@example.com", "jane@example.com", "bob@example.com"]);
});

it("handles empty parts between commas", () => {
expect(
extractEmailAddresses("john@example.com,,jane@example.com"),
).toEqual(["john@example.com", "jane@example.com"]);
});

it("handles trailing comma", () => {
expect(extractEmailAddresses("john@example.com,")).toEqual([
"john@example.com",
]);
});

it("handles leading comma", () => {
expect(extractEmailAddresses(",john@example.com")).toEqual([
"john@example.com",
]);
});

it("handles complex real-world header format", () => {
expect(
extractEmailAddresses(
'"Smith, John" <john.smith@example.com>, "Doe, Jane" <jane.doe@example.com>, admin@example.com',
),
).toEqual([
"john.smith@example.com",
"jane.doe@example.com",
"admin@example.com",
]);
});

it("handles emails with plus addressing", () => {
expect(
extractEmailAddresses("user+tag@example.com, user+other@example.com"),
).toEqual(["user+tag@example.com", "user+other@example.com"]);
});

it("handles emails with hyphens", () => {
expect(
extractEmailAddresses("no-reply@example.com, support-team@example.com"),
).toEqual(["no-reply@example.com", "support-team@example.com"]);
});

it("handles single email with angle brackets", () => {
expect(extractEmailAddresses("<john@example.com>")).toEqual([
"john@example.com",
]);
});

it("handles all invalid emails", () => {
expect(extractEmailAddresses("invalid, also-invalid")).toEqual([]);
});
});

describe("extractEmailAddress", () => {
it("extracts email from format 'Name <email>'", () => {
expect(extractEmailAddress("John Doe <john.doe@gmail.com>")).toBe(
Expand Down
13 changes: 13 additions & 0 deletions apps/web/utils/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ export function extractNameFromEmail(email: string) {
return email;
}

// Extracts all email addresses from a comma-separated header string
// e.g., "John <john@example.com>, Jane <jane@example.com>" -> ["john@example.com", "jane@example.com"]
export function extractEmailAddresses(header: string): string[] {
if (!header) return [];

// split by comma, but be careful about commas inside quoted names
const parts = header.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/);

return parts
.map((part) => extractEmailAddress(part.trim()))
.filter((email) => email.length > 0);
}

// Converts "John Doe <john.doe@gmail>" to "john.doe@gmail"
export function extractEmailAddress(email: string): string {
if (!email) return "";
Expand Down
Loading