diff --git a/apps/web/app/api/google/webhook/process-history-item.test.ts b/apps/web/app/api/google/webhook/process-history-item.test.ts index 92cd077972..5bb9753f2f 100644 --- a/apps/web/app/api/google/webhook/process-history-item.test.ts +++ b/apps/web/app/api/google/webhook/process-history-item.test.ts @@ -6,7 +6,7 @@ import { import { ColdEmailSetting } from "@prisma/client"; import type { gmail_v1 } from "@googleapis/gmail"; import { isAssistantEmail } from "@/utils/assistant/is-assistant-email"; -import { runColdEmailBlocker } from "@/utils/cold-email/is-cold-email"; +import { runColdEmailBlockerWithProvider } from "@/utils/cold-email/is-cold-email"; import { blockUnsubscribedEmails } from "@/app/api/google/webhook/block-unsubscribed-emails"; import { markMessageAsProcessing } from "@/utils/redis/message-processing"; @@ -50,6 +50,9 @@ vi.mock("@/utils/cold-email/is-cold-email", () => ({ runColdEmailBlocker: vi .fn() .mockResolvedValue({ isColdEmail: false, reason: "hasPreviousEmail" }), + runColdEmailBlockerWithProvider: vi + .fn() + .mockResolvedValue({ isColdEmail: false, reason: "hasPreviousEmail" }), })); vi.mock("@/app/api/google/webhook/block-unsubscribed-emails", () => ({ blockUnsubscribedEmails: vi.fn().mockResolvedValue(false), @@ -102,6 +105,7 @@ describe("processHistoryItem", () => { const defaultOptions = { gmail: {} as any, + provider: {} as any, email: "user@test.com", accessToken: "fake-token", hasAutomationRules: false, @@ -141,7 +145,7 @@ describe("processHistoryItem", () => { await processHistoryItem(createHistoryItem(), options); expect(blockUnsubscribedEmails).not.toHaveBeenCalled(); - expect(runColdEmailBlocker).not.toHaveBeenCalled(); + expect(runColdEmailBlockerWithProvider).not.toHaveBeenCalled(); expect(processAssistantEmail).toHaveBeenCalledWith({ message: expect.objectContaining({ headers: expect.objectContaining({ @@ -184,7 +188,7 @@ describe("processHistoryItem", () => { await processHistoryItem(createHistoryItem(), options); expect(blockUnsubscribedEmails).not.toHaveBeenCalled(); - expect(runColdEmailBlocker).not.toHaveBeenCalled(); + expect(runColdEmailBlockerWithProvider).not.toHaveBeenCalled(); }); it("should skip if email is unsubscribed", async () => { @@ -196,7 +200,7 @@ describe("processHistoryItem", () => { }; await processHistoryItem(createHistoryItem(), options); - expect(runColdEmailBlocker).not.toHaveBeenCalled(); + expect(runColdEmailBlockerWithProvider).not.toHaveBeenCalled(); }); it("should run cold email blocker when enabled", async () => { @@ -211,7 +215,7 @@ describe("processHistoryItem", () => { await processHistoryItem(createHistoryItem(), options); - expect(runColdEmailBlocker).toHaveBeenCalledWith({ + expect(runColdEmailBlockerWithProvider).toHaveBeenCalledWith({ email: expect.objectContaining({ from: "sender@example.com", subject: "Test Email", @@ -220,13 +224,13 @@ describe("processHistoryItem", () => { threadId: "thread-123", date: expect.any(Date), }), - gmail: options.gmail, + provider: expect.any(Object), emailAccount: options.emailAccount, }); }); it("should skip further processing if cold email is detected", async () => { - vi.mocked(runColdEmailBlocker).mockResolvedValueOnce({ + vi.mocked(runColdEmailBlockerWithProvider).mockResolvedValueOnce({ isColdEmail: true, reason: "ai", aiReason: "This appears to be a cold email", @@ -254,7 +258,7 @@ describe("processHistoryItem", () => { }); it("should add cold email to digest when coldEmailDigest is true and cold email is detected", async () => { - vi.mocked(runColdEmailBlocker).mockResolvedValueOnce({ + vi.mocked(runColdEmailBlockerWithProvider).mockResolvedValueOnce({ isColdEmail: true, reason: "ai", aiReason: "This appears to be a cold email", @@ -275,7 +279,7 @@ describe("processHistoryItem", () => { await processHistoryItem(createHistoryItem(), options); - expect(runColdEmailBlocker).toHaveBeenCalledWith({ + expect(runColdEmailBlockerWithProvider).toHaveBeenCalledWith({ email: expect.objectContaining({ from: "sender@example.com", subject: "Test Email", @@ -284,7 +288,7 @@ describe("processHistoryItem", () => { threadId: "thread-123", date: expect.any(Date), }), - gmail: options.gmail, + provider: expect.any(Object), emailAccount: options.emailAccount, }); @@ -322,13 +326,13 @@ describe("processHistoryItem", () => { await processHistoryItem(createHistoryItem(), options); - expect(runColdEmailBlocker).not.toHaveBeenCalled(); + expect(runColdEmailBlockerWithProvider).not.toHaveBeenCalled(); expect(categorizeSender).toHaveBeenCalled(); expect(runRules).toHaveBeenCalled(); }); it("should process normally when cold email is not detected with coldEmailDigest enabled", async () => { - vi.mocked(runColdEmailBlocker).mockResolvedValueOnce({ + vi.mocked(runColdEmailBlockerWithProvider).mockResolvedValueOnce({ isColdEmail: false, reason: "hasPreviousEmail", }); @@ -347,14 +351,14 @@ describe("processHistoryItem", () => { await processHistoryItem(createHistoryItem(), options); - expect(runColdEmailBlocker).toHaveBeenCalled(); + expect(runColdEmailBlockerWithProvider).toHaveBeenCalled(); expect(categorizeSender).toHaveBeenCalled(); expect(runRules).toHaveBeenCalled(); }); it("should add second email from known cold emailer to digest when coldEmailDigest is enabled", async () => { // Mock the response for a known cold emailer (already in database) - vi.mocked(runColdEmailBlocker).mockResolvedValueOnce({ + vi.mocked(runColdEmailBlockerWithProvider).mockResolvedValueOnce({ isColdEmail: true, reason: "ai-already-labeled", coldEmailId: "existing-cold-email-456", // Existing cold email entry ID @@ -374,7 +378,7 @@ describe("processHistoryItem", () => { await processHistoryItem(createHistoryItem("456", "thread-456"), options); - expect(runColdEmailBlocker).toHaveBeenCalledWith({ + expect(runColdEmailBlockerWithProvider).toHaveBeenCalledWith({ email: expect.objectContaining({ from: "sender@example.com", subject: "Test Email", @@ -383,7 +387,7 @@ describe("processHistoryItem", () => { threadId: "thread-456", date: expect.any(Date), }), - gmail: options.gmail, + provider: expect.any(Object), emailAccount: options.emailAccount, }); diff --git a/apps/web/app/api/google/webhook/process-history-item.ts b/apps/web/app/api/google/webhook/process-history-item.ts index 94647d9983..a9597a06f8 100644 --- a/apps/web/app/api/google/webhook/process-history-item.ts +++ b/apps/web/app/api/google/webhook/process-history-item.ts @@ -2,7 +2,7 @@ import type { gmail_v1 } from "@googleapis/gmail"; import prisma from "@/utils/prisma"; import { emailToContent } from "@/utils/mail"; import { GmailLabel } from "@/utils/gmail/label"; -import { runColdEmailBlocker } from "@/utils/cold-email/is-cold-email"; +import { runColdEmailBlockerWithProvider } from "@/utils/cold-email/is-cold-email"; import { runRules } from "@/utils/ai/choose-rule/run-rules"; import { blockUnsubscribedEmails } from "@/app/api/google/webhook/block-unsubscribed-emails"; import { categorizeSender } from "@/utils/categorize/senders/categorize"; @@ -152,7 +152,7 @@ export async function processHistoryItem( const content = emailToContent(parsedMessage); - const response = await runColdEmailBlocker({ + const response = await runColdEmailBlockerWithProvider({ email: { from: parsedMessage.headers.from, to: "", @@ -162,7 +162,7 @@ export async function processHistoryItem( threadId, date: internalDateToDate(parsedMessage.internalDate), }, - gmail, + provider, emailAccount, }); diff --git a/apps/web/utils/actions/cold-email.ts b/apps/web/utils/actions/cold-email.ts index dde2c72cbc..1c0174271d 100644 --- a/apps/web/utils/actions/cold-email.ts +++ b/apps/web/utils/actions/cold-email.ts @@ -18,6 +18,7 @@ import { import { actionClient } from "@/utils/actions/safe-action"; import { getGmailClientForEmail } from "@/utils/account"; import { SafeError } from "@/utils/error"; +import { createEmailProvider } from "@/utils/email/provider"; export const updateColdEmailSettingsAction = actionClient .metadata({ name: "updateColdEmailSettings" }) @@ -119,12 +120,20 @@ export const testColdEmailAction = actionClient where: { id: emailAccountId }, include: { user: { select: { aiProvider: true, aiModel: true, aiApiKey: true } }, + account: { + select: { + provider: true, + }, + }, }, }); if (!emailAccount) throw new SafeError("Email account not found"); - const gmail = await getGmailClientForEmail({ emailAccountId }); + const emailProvider = await createEmailProvider({ + emailAccountId, + provider: emailAccount.account?.provider, + }); const content = emailToContent({ textHtml: textHtml || undefined, @@ -143,7 +152,7 @@ export const testColdEmailAction = actionClient id: messageId || "", }, emailAccount, - gmail, + provider: emailProvider, }); return response; diff --git a/apps/web/utils/cold-email/is-cold-email.test.ts b/apps/web/utils/cold-email/is-cold-email.test.ts index 773b720396..b7b3dc67f4 100644 --- a/apps/web/utils/cold-email/is-cold-email.test.ts +++ b/apps/web/utils/cold-email/is-cold-email.test.ts @@ -1,11 +1,9 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { gmail_v1 } from "@googleapis/gmail"; import prisma from "@/utils/prisma"; import { ColdEmailSetting, ColdEmailStatus } from "@prisma/client"; -import { GmailLabel } from "@/utils/gmail/label"; -import * as labelUtils from "@/utils/gmail/label"; -import { blockColdEmail } from "./is-cold-email"; +import { blockColdEmailWithProvider } from "./is-cold-email"; import { getEmailAccount } from "@/__tests__/helpers"; +import type { EmailProvider } from "@/utils/email/provider"; // Mock dependencies vi.mock("server-only", () => ({})); @@ -18,17 +16,14 @@ vi.mock("@/utils/prisma", () => ({ }, })); -vi.mock("@/utils/gmail/label", async () => { - const actual = await vi.importActual("@/utils/gmail/label"); - return { - ...actual, +describe("blockColdEmail", () => { + const mockProvider = { getOrCreateInboxZeroLabel: vi.fn(), labelMessage: vi.fn(), - }; -}); + archiveThread: vi.fn(), + markReadThread: vi.fn(), + } as unknown as EmailProvider; -describe("blockColdEmail", () => { - const mockGmail = {} as gmail_v1.Gmail; const mockEmail = { from: "sender@example.com", id: "123", @@ -45,8 +40,14 @@ describe("blockColdEmail", () => { }); it("should upsert cold email record in database", async () => { - await blockColdEmail({ - gmail: mockGmail, + vi.mocked(mockProvider.getOrCreateInboxZeroLabel).mockResolvedValue({ + id: "label123", + name: "Cold Email", + type: "user", + }); + + await blockColdEmailWithProvider({ + provider: mockProvider, email: mockEmail, emailAccount: mockEmailAccount, aiReason: mockAiReason, @@ -72,27 +73,26 @@ describe("blockColdEmail", () => { }); it("should add cold email label when coldEmailBlocker is LABEL", async () => { - vi.mocked(labelUtils.getOrCreateInboxZeroLabel).mockResolvedValue({ + vi.mocked(mockProvider.getOrCreateInboxZeroLabel).mockResolvedValue({ id: "label123", + name: "Cold Email", + type: "user", }); - await blockColdEmail({ - gmail: mockGmail, + await blockColdEmailWithProvider({ + provider: mockProvider, email: mockEmail, emailAccount: mockEmailAccount, aiReason: mockAiReason, }); - expect(labelUtils.getOrCreateInboxZeroLabel).toHaveBeenCalledWith({ - gmail: mockGmail, - key: "cold_email", - }); - expect(labelUtils.labelMessage).toHaveBeenCalledWith({ - gmail: mockGmail, - messageId: mockEmail.id, - addLabelIds: ["label123"], - removeLabelIds: undefined, - }); + expect(mockProvider.getOrCreateInboxZeroLabel).toHaveBeenCalledWith( + "cold_email", + ); + expect(mockProvider.labelMessage).toHaveBeenCalledWith( + mockEmail.id, + "Cold Email", + ); }); it("should archive email when coldEmailBlocker is ARCHIVE_AND_LABEL", async () => { @@ -100,23 +100,27 @@ describe("blockColdEmail", () => { ...mockEmailAccount, coldEmailBlocker: ColdEmailSetting.ARCHIVE_AND_LABEL, }; - vi.mocked(labelUtils.getOrCreateInboxZeroLabel).mockResolvedValue({ + vi.mocked(mockProvider.getOrCreateInboxZeroLabel).mockResolvedValue({ id: "label123", + name: "Cold Email", + type: "user", }); - await blockColdEmail({ - gmail: mockGmail, + await blockColdEmailWithProvider({ + provider: mockProvider, email: mockEmail, emailAccount: userWithArchive, aiReason: mockAiReason, }); - expect(labelUtils.labelMessage).toHaveBeenCalledWith({ - gmail: mockGmail, - messageId: mockEmail.id, - addLabelIds: ["label123"], - removeLabelIds: [GmailLabel.INBOX], - }); + expect(mockProvider.labelMessage).toHaveBeenCalledWith( + mockEmail.id, + "Cold Email", + ); + expect(mockProvider.archiveThread).toHaveBeenCalledWith( + mockEmail.threadId, + userWithArchive.email, + ); }); it("should archive and mark as read when coldEmailBlocker is ARCHIVE_AND_READ_AND_LABEL", async () => { @@ -124,31 +128,39 @@ describe("blockColdEmail", () => { ...mockEmailAccount, coldEmailBlocker: ColdEmailSetting.ARCHIVE_AND_READ_AND_LABEL, }; - vi.mocked(labelUtils.getOrCreateInboxZeroLabel).mockResolvedValue({ + vi.mocked(mockProvider.getOrCreateInboxZeroLabel).mockResolvedValue({ id: "label123", + name: "Cold Email", + type: "user", }); - await blockColdEmail({ - gmail: mockGmail, + await blockColdEmailWithProvider({ + provider: mockProvider, email: mockEmail, emailAccount: userWithArchiveAndRead, aiReason: mockAiReason, }); - expect(labelUtils.labelMessage).toHaveBeenCalledWith({ - gmail: mockGmail, - messageId: mockEmail.id, - addLabelIds: ["label123"], - removeLabelIds: [GmailLabel.INBOX, GmailLabel.UNREAD], - }); + expect(mockProvider.labelMessage).toHaveBeenCalledWith( + mockEmail.id, + "Cold Email", + ); + expect(mockProvider.archiveThread).toHaveBeenCalledWith( + mockEmail.threadId, + userWithArchiveAndRead.email, + ); + expect(mockProvider.markReadThread).toHaveBeenCalledWith( + mockEmail.threadId, + true, + ); }); it("should throw error when user email is missing", async () => { const userWithoutEmail = { ...mockEmailAccount, email: null as any }; await expect( - blockColdEmail({ - gmail: mockGmail, + blockColdEmailWithProvider({ + provider: mockProvider, email: mockEmail, emailAccount: userWithoutEmail, aiReason: mockAiReason, @@ -157,23 +169,23 @@ describe("blockColdEmail", () => { }); it("should handle missing label id", async () => { - vi.mocked(labelUtils.getOrCreateInboxZeroLabel).mockResolvedValue({ - id: null, + vi.mocked(mockProvider.getOrCreateInboxZeroLabel).mockResolvedValue({ + id: "", + name: "Cold Email", + type: "user", }); - await blockColdEmail({ - gmail: mockGmail, + await blockColdEmailWithProvider({ + provider: mockProvider, email: mockEmail, emailAccount: mockEmailAccount, aiReason: mockAiReason, }); - expect(labelUtils.labelMessage).toHaveBeenCalledWith({ - gmail: mockGmail, - messageId: mockEmail.id, - addLabelIds: undefined, - removeLabelIds: undefined, - }); + expect(mockProvider.labelMessage).toHaveBeenCalledWith( + mockEmail.id, + "Cold Email", + ); }); it("should not modify labels when coldEmailBlocker is DISABLED", async () => { @@ -182,14 +194,14 @@ describe("blockColdEmail", () => { coldEmailBlocker: ColdEmailSetting.DISABLED, }; - await blockColdEmail({ - gmail: mockGmail, + await blockColdEmailWithProvider({ + provider: mockProvider, email: mockEmail, emailAccount: userWithBlockerOff, aiReason: mockAiReason, }); - expect(labelUtils.getOrCreateInboxZeroLabel).not.toHaveBeenCalled(); - expect(labelUtils.labelMessage).not.toHaveBeenCalled(); + expect(mockProvider.getOrCreateInboxZeroLabel).not.toHaveBeenCalled(); + expect(mockProvider.labelMessage).not.toHaveBeenCalled(); }); }); diff --git a/apps/web/utils/cold-email/is-cold-email.ts b/apps/web/utils/cold-email/is-cold-email.ts index 927290b48d..407f9ead7b 100644 --- a/apps/web/utils/cold-email/is-cold-email.ts +++ b/apps/web/utils/cold-email/is-cold-email.ts @@ -1,9 +1,6 @@ import { z } from "zod"; -import type { gmail_v1 } from "@googleapis/gmail"; import { chatCompletionObject } from "@/utils/llms"; import type { EmailAccountWithAI } from "@/utils/llms/types"; -import { getOrCreateInboxZeroLabel, GmailLabel } from "@/utils/gmail/label"; -import { labelMessage } from "@/utils/gmail/label"; import type { ColdEmail } from "@prisma/client"; import { ColdEmailSetting, @@ -14,7 +11,6 @@ import prisma from "@/utils/prisma"; import { DEFAULT_COLD_EMAIL_PROMPT } from "@/utils/cold-email/prompt"; import { stringifyEmail } from "@/utils/stringify-email"; import { createScopedLogger } from "@/utils/logger"; -import { hasPreviousCommunicationsWithSenderOrDomain } from "@/utils/gmail/message"; import type { EmailForLLM } from "@/utils/types"; import type { EmailProvider } from "@/utils/email/provider"; @@ -25,11 +21,11 @@ type ColdEmailBlockerReason = "hasPreviousEmail" | "ai" | "ai-already-labeled"; export async function isColdEmail({ email, emailAccount, - gmail, + provider, }: { email: EmailForLLM & { threadId?: string }; emailAccount: Pick & EmailAccountWithAI; - gmail: gmail_v1.Gmail; + provider: EmailProvider; }): Promise<{ isColdEmail: boolean; reason: ColdEmailBlockerReason; @@ -59,7 +55,7 @@ export async function isColdEmail({ const hasPreviousEmail = email.date && email.id - ? await hasPreviousCommunicationsWithSenderOrDomain(gmail, { + ? await provider.hasPreviousCommunicationsWithSenderOrDomain({ from: email.from, date: email.date, messageId: email.id, @@ -219,29 +215,6 @@ ${stringifyEmail(email, 500)} return response.object; } -export async function runColdEmailBlocker(options: { - email: EmailForLLM & { threadId: string }; - gmail: gmail_v1.Gmail; - emailAccount: Pick & - EmailAccountWithAI; -}): Promise<{ - isColdEmail: boolean; - reason: ColdEmailBlockerReason; - aiReason?: string | null; - coldEmailId?: string | null; -}> { - const response = await isColdEmail(options); - - if (!response.isColdEmail) return { ...response, coldEmailId: null }; - - const coldEmail = await blockColdEmail({ - ...options, - aiReason: response.aiReason ?? null, - }); - return { ...response, coldEmailId: coldEmail.id }; -} - -// New function that works with EmailProvider export async function runColdEmailBlockerWithProvider(options: { email: EmailForLLM & { threadId: string }; provider: EmailProvider; @@ -268,73 +241,6 @@ export async function runColdEmailBlockerWithProvider(options: { return { ...response, coldEmailId: coldEmail.id }; } -export async function blockColdEmail(options: { - gmail: gmail_v1.Gmail; - email: { from: string; id: string; threadId: string }; - emailAccount: Pick & EmailAccountWithAI; - aiReason: string | null; -}): Promise { - const { gmail, email, emailAccount, aiReason } = options; - - const coldEmail = await prisma.coldEmail.upsert({ - where: { - emailAccountId_fromEmail: { - emailAccountId: emailAccount.id, - fromEmail: email.from, - }, - }, - update: { status: ColdEmailStatus.AI_LABELED_COLD }, - create: { - status: ColdEmailStatus.AI_LABELED_COLD, - fromEmail: email.from, - emailAccountId: emailAccount.id, - reason: aiReason, - messageId: email.id, - threadId: email.threadId, - }, - }); - - if ( - emailAccount.coldEmailBlocker === ColdEmailSetting.LABEL || - emailAccount.coldEmailBlocker === ColdEmailSetting.ARCHIVE_AND_LABEL || - emailAccount.coldEmailBlocker === - ColdEmailSetting.ARCHIVE_AND_READ_AND_LABEL - ) { - if (!emailAccount.email) throw new Error("User email is required"); - const coldEmailLabel = await getOrCreateInboxZeroLabel({ - gmail, - key: "cold_email", - }); - if (!coldEmailLabel?.id) - logger.error("No gmail label id", { emailAccountId: emailAccount.id }); - - const shouldArchive = - emailAccount.coldEmailBlocker === ColdEmailSetting.ARCHIVE_AND_LABEL || - emailAccount.coldEmailBlocker === - ColdEmailSetting.ARCHIVE_AND_READ_AND_LABEL; - - const shouldMarkRead = - emailAccount.coldEmailBlocker === - ColdEmailSetting.ARCHIVE_AND_READ_AND_LABEL; - - const addLabelIds: string[] = []; - if (coldEmailLabel?.id) addLabelIds.push(coldEmailLabel.id); - - const removeLabelIds: string[] = []; - if (shouldArchive) removeLabelIds.push(GmailLabel.INBOX); - if (shouldMarkRead) removeLabelIds.push(GmailLabel.UNREAD); - - await labelMessage({ - gmail, - messageId: email.id, - addLabelIds: addLabelIds.length ? addLabelIds : undefined, - removeLabelIds: removeLabelIds.length ? removeLabelIds : undefined, - }); - } - - return coldEmail; -} - // New function that works with EmailProvider export async function blockColdEmailWithProvider(options: { provider: EmailProvider; @@ -385,7 +291,9 @@ export async function blockColdEmailWithProvider(options: { // For Outlook, we'll use the provider's labelMessage method // The provider will handle the differences between Gmail labels and Outlook categories - await provider.labelMessage(email.id, coldEmailLabel.name); + if (coldEmailLabel?.name) { + await provider.labelMessage(email.id, coldEmailLabel.name); + } // For archiving and marking as read, we'll need to implement these in the provider if (shouldArchive) { diff --git a/apps/web/utils/email/provider.ts b/apps/web/utils/email/provider.ts index 5c0a7ab680..d5a2295148 100644 --- a/apps/web/utils/email/provider.ts +++ b/apps/web/utils/email/provider.ts @@ -799,7 +799,7 @@ export class GmailProvider implements EmailProvider { date: Date; messageId: string; }): Promise { - return hasPreviousCommunicationsWithSenderOrDomain(this.client, options); + return hasPreviousCommunicationsWithSenderOrDomain(this, options); } async getThreadsFromSenderWithSubject( diff --git a/apps/web/utils/gmail/message.ts b/apps/web/utils/gmail/message.ts index 8bc1e30d28..13515d6fdf 100644 --- a/apps/web/utils/gmail/message.ts +++ b/apps/web/utils/gmail/message.ts @@ -15,6 +15,7 @@ import { getAccessTokenFromClient } from "@/utils/gmail/client"; import { GmailLabel } from "@/utils/gmail/label"; import { isIgnoredSender } from "@/utils/filter-ignored-senders"; import parse from "gmail-api-parse-message"; +import type { EmailProvider } from "@/utils/email/provider"; const logger = createScopedLogger("gmail/message"); @@ -165,42 +166,39 @@ export async function getMessagesBatch({ } async function findPreviousEmailsWithSender( - gmail: gmail_v1.Gmail, + client: EmailProvider, options: { sender: string; dateInSeconds: number; }, ) { - // Check for both incoming emails from sender and outgoing emails to sender + const beforeDate = new Date(options.dateInSeconds * 1000); const [incomingEmails, outgoingEmails] = await Promise.all([ - // Incoming - gmail.users.messages.list({ - userId: "me", - q: `from:${options.sender} before:${options.dateInSeconds}`, + client.getMessagesWithPagination({ + query: `from:${options.sender}`, maxResults: 2, + before: beforeDate, }), - // Outgoing - gmail.users.messages.list({ - userId: "me", - q: `to:${options.sender} before:${options.dateInSeconds}`, - maxResults: 1, + client.getMessagesWithPagination({ + query: `to:${options.sender}`, + maxResults: 2, + before: beforeDate, }), ]); - // Combine both incoming and outgoing messages const allMessages = [ - ...(incomingEmails.data.messages || []), - ...(outgoingEmails.data.messages || []), + ...(incomingEmails.messages || []), + ...(outgoingEmails.messages || []), ]; return allMessages; } export async function hasPreviousCommunicationWithSender( - gmail: gmail_v1.Gmail, + client: EmailProvider, options: { from: string; date: Date; messageId: string }, ) { - const previousEmails = await findPreviousEmailsWithSender(gmail, { + const previousEmails = await findPreviousEmailsWithSender(client, { sender: options.from, dateInSeconds: +new Date(options.date) / 1000, }); @@ -229,11 +227,11 @@ const PUBLIC_DOMAINS = new Set([ ]); export async function hasPreviousCommunicationsWithSenderOrDomain( - gmail: gmail_v1.Gmail, + client: EmailProvider, options: { from: string; date: Date; messageId: string }, ) { const domain = extractDomainFromEmail(options.from); - if (!domain) return hasPreviousCommunicationWithSender(gmail, options); + if (!domain) return hasPreviousCommunicationWithSender(client, options); // For public email providers (gmail, yahoo, etc), search by full email address // For company domains, search by domain to catch emails from different people at same company @@ -241,7 +239,7 @@ export async function hasPreviousCommunicationsWithSenderOrDomain( ? options.from : domain; - return hasPreviousCommunicationWithSender(gmail, { + return hasPreviousCommunicationWithSender(client, { ...options, from: searchTerm, });