From 8084753e3d3fc9463b282c28dd68c712922abe25 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:09:04 -0500 Subject: [PATCH 1/4] process emails immediately in onboarding --- .../[emailAccountId]/onboarding/page.tsx | 2 + apps/web/utils/actions/rule.ts | 16 ++- apps/web/utils/ai/choose-rule/run-rules.ts | 12 +- .../onboarding/process-onboarding-emails.ts | 112 ++++++++++++++++++ version.txt | 2 +- 5 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 apps/web/utils/onboarding/process-onboarding-emails.ts diff --git a/apps/web/app/(app)/[emailAccountId]/onboarding/page.tsx b/apps/web/app/(app)/[emailAccountId]/onboarding/page.tsx index ba11259fa8..cab181928c 100644 --- a/apps/web/app/(app)/[emailAccountId]/onboarding/page.tsx +++ b/apps/web/app/(app)/[emailAccountId]/onboarding/page.tsx @@ -6,6 +6,8 @@ import { OnboardingContent } from "@/app/(app)/[emailAccountId]/onboarding/Onboa import { fetchUserAndStoreUtms } from "@/app/(landing)/welcome/utms"; import { auth } from "@/utils/auth"; +export const maxDuration = 300; + export const metadata: Metadata = { title: "Onboarding | Inbox Zero", description: "Learn how Inbox Zero works and get set up.", diff --git a/apps/web/utils/actions/rule.ts b/apps/web/utils/actions/rule.ts index dc91de8f2b..403c8b106d 100644 --- a/apps/web/utils/actions/rule.ts +++ b/apps/web/utils/actions/rule.ts @@ -1,6 +1,7 @@ "use server"; import { revalidatePath } from "next/cache"; +import { after } from "next/server"; import { createRuleBody, updateRuleBody, @@ -39,6 +40,8 @@ import { resolveLabelNameAndId } from "@/utils/label/resolve-label"; import type { Logger } from "@/utils/logger"; import { validateGmailLabelName } from "@/utils/gmail/label-validation"; import { isGoogleProvider } from "@/utils/email/provider-types"; +import { processOnboardingEmails } from "@/utils/onboarding/process-onboarding-emails"; +import { getEmailAccountWithAi } from "@/utils/user/get"; export const createRuleAction = actionClient .metadata({ name: "createRule" }) @@ -272,10 +275,7 @@ export const createRulesOnboardingAction = actionClient } } - const emailAccount = await prisma.emailAccount.findUnique({ - where: { id: emailAccountId }, - select: { rulesPrompt: true }, - }); + const emailAccount = await getEmailAccountWithAi({ emailAccountId }); if (!emailAccount) throw new SafeError("User not found"); const promises: Promise[] = []; @@ -410,6 +410,14 @@ export const createRulesOnboardingAction = actionClient } await Promise.allSettled(promises); + + after(() => + processOnboardingEmails({ + emailAccount, + provider, + logger, + }), + ); }, ); diff --git a/apps/web/utils/ai/choose-rule/run-rules.ts b/apps/web/utils/ai/choose-rule/run-rules.ts index 883bc62a85..03422d3710 100644 --- a/apps/web/utils/ai/choose-rule/run-rules.ts +++ b/apps/web/utils/ai/choose-rule/run-rules.ts @@ -72,6 +72,7 @@ export async function runRules({ isTest, modelType, logger, + skipArchive, }: { provider: EmailProvider; message: ParsedMessage; @@ -80,6 +81,7 @@ export async function runRules({ isTest: boolean; modelType: ModelType; logger: Logger; + skipArchive?: boolean; }): Promise { const batchTimestamp = new Date(); // Single timestamp for this batch execution const { regularRules, conversationRules } = prepareRulesWithMetaRule(rules); @@ -187,6 +189,7 @@ export async function runRules({ modelType, batchTimestamp, logger, + skipArchive, ); executedRules.push({ @@ -253,8 +256,9 @@ async function executeMatchedRule( modelType: ModelType, batchTimestamp: Date, logger: Logger, + skipArchive?: boolean, ) { - const actionItems = await getActionItemsWithAiArgs({ + let actionItems = await getActionItemsWithAiArgs({ message, emailAccount, selectedRule: rule, @@ -264,6 +268,12 @@ async function executeMatchedRule( isTest, }); + if (skipArchive) { + actionItems = actionItems.filter( + (item) => item.type !== ActionType.ARCHIVE, + ); + } + const { immediateActions, delayedActions } = groupBy(actionItems, (item) => item.delayInMinutes != null && item.delayInMinutes > 0 ? "delayedActions" diff --git a/apps/web/utils/onboarding/process-onboarding-emails.ts b/apps/web/utils/onboarding/process-onboarding-emails.ts new file mode 100644 index 0000000000..cdd738406b --- /dev/null +++ b/apps/web/utils/onboarding/process-onboarding-emails.ts @@ -0,0 +1,112 @@ +import prisma from "@/utils/prisma"; +import { createEmailProvider } from "@/utils/email/provider"; +import { runRules } from "@/utils/ai/choose-rule/run-rules"; +import type { Logger } from "@/utils/logger"; +import type { EmailAccountWithAI } from "@/utils/llms/types"; +import type { ParsedMessage } from "@/utils/types"; + +const ONBOARDING_EMAIL_COUNT = 20; + +export async function processOnboardingEmails({ + emailAccount, + provider, + logger: log, +}: { + emailAccount: EmailAccountWithAI; + provider: string; + logger: Logger; +}) { + const logger = log.with({ module: "onboarding/process-emails" }); + + logger.info("Starting onboarding email processing"); + + try { + const emailProvider = await createEmailProvider({ + emailAccountId: emailAccount.id, + provider, + logger, + }); + + const [{ messages }, rules] = await Promise.all([ + emailProvider.getMessagesByFields({ + type: "inbox", + maxResults: ONBOARDING_EMAIL_COUNT, + }), + prisma.rule.findMany({ + where: { + emailAccountId: emailAccount.id, + enabled: true, + }, + include: { actions: true }, + }), + ]); + + if (messages.length === 0) { + logger.info("No inbox emails to process for onboarding"); + return; + } + + if (rules.length === 0) { + logger.info("No rules found for onboarding processing"); + return; + } + + const uniqueMessages = getLatestMessagePerThread(messages); + + logger.info("Processing emails with rules", { + ruleCount: rules.length, + emailCount: uniqueMessages.length, + totalFetched: messages.length, + }); + + let processedCount = 0; + let errorCount = 0; + + for (const message of uniqueMessages) { + try { + await runRules({ + provider: emailProvider, + message, + rules, + emailAccount, + isTest: false, + modelType: "default", + logger, + skipArchive: true, + }); + processedCount++; + } catch (error) { + errorCount++; + logger.error("Error processing email during onboarding", { + messageId: message.id, + error, + }); + // Continue processing other emails even if one fails + } + } + + logger.info("Completed onboarding email processing", { + processedCount, + errorCount, + totalEmails: uniqueMessages.length, + }); + } catch (error) { + logger.error("Failed to process onboarding emails", { error }); + } +} + +function getLatestMessagePerThread(messages: ParsedMessage[]): ParsedMessage[] { + const latestByThread = new Map(); + + for (const message of messages) { + const existing = latestByThread.get(message.threadId); + if ( + !existing || + new Date(message.date || 0) > new Date(existing.date || 0) + ) { + latestByThread.set(message.threadId, message); + } + } + + return Array.from(latestByThread.values()); +} diff --git a/version.txt b/version.txt index 602aa91d6e..81945235a6 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.21.2 +v2.21.3 From 5c5e8fe87dcfebe844cf362a390c0f0f2e7d4981 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:13:12 -0500 Subject: [PATCH 2/4] refactor --- apps/web/utils/actions/rule.ts | 2 +- .../choose-rule/bulk-process-emails.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename apps/web/utils/{onboarding/process-onboarding-emails.ts => ai/choose-rule/bulk-process-emails.ts} (100%) diff --git a/apps/web/utils/actions/rule.ts b/apps/web/utils/actions/rule.ts index 403c8b106d..de8d820725 100644 --- a/apps/web/utils/actions/rule.ts +++ b/apps/web/utils/actions/rule.ts @@ -40,7 +40,7 @@ import { resolveLabelNameAndId } from "@/utils/label/resolve-label"; import type { Logger } from "@/utils/logger"; import { validateGmailLabelName } from "@/utils/gmail/label-validation"; import { isGoogleProvider } from "@/utils/email/provider-types"; -import { processOnboardingEmails } from "@/utils/onboarding/process-onboarding-emails"; +import { processOnboardingEmails } from "@/utils/ai/choose-rule/bulk-process-emails"; import { getEmailAccountWithAi } from "@/utils/user/get"; export const createRuleAction = actionClient diff --git a/apps/web/utils/onboarding/process-onboarding-emails.ts b/apps/web/utils/ai/choose-rule/bulk-process-emails.ts similarity index 100% rename from apps/web/utils/onboarding/process-onboarding-emails.ts rename to apps/web/utils/ai/choose-rule/bulk-process-emails.ts From b833af3db658336e70639c219d32ae2598a4b8d7 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:19:09 -0500 Subject: [PATCH 3/4] use economy to run on existing emails --- apps/web/utils/ai/choose-rule/bulk-process-emails.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/utils/ai/choose-rule/bulk-process-emails.ts b/apps/web/utils/ai/choose-rule/bulk-process-emails.ts index cdd738406b..c84a41760a 100644 --- a/apps/web/utils/ai/choose-rule/bulk-process-emails.ts +++ b/apps/web/utils/ai/choose-rule/bulk-process-emails.ts @@ -70,7 +70,7 @@ export async function processOnboardingEmails({ rules, emailAccount, isTest: false, - modelType: "default", + modelType: "economy", logger, skipArchive: true, }); From e3e770528674ad643f65ee905a8a898c096469c2 Mon Sep 17 00:00:00 2001 From: Eliezer Steinbock <3090527+elie222@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:21:06 -0500 Subject: [PATCH 4/4] refactor --- apps/web/utils/actions/rule.ts | 6 +++-- .../ai/choose-rule/bulk-process-emails.ts | 26 ++++++++++--------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/apps/web/utils/actions/rule.ts b/apps/web/utils/actions/rule.ts index de8d820725..af15cf575d 100644 --- a/apps/web/utils/actions/rule.ts +++ b/apps/web/utils/actions/rule.ts @@ -40,7 +40,7 @@ import { resolveLabelNameAndId } from "@/utils/label/resolve-label"; import type { Logger } from "@/utils/logger"; import { validateGmailLabelName } from "@/utils/gmail/label-validation"; import { isGoogleProvider } from "@/utils/email/provider-types"; -import { processOnboardingEmails } from "@/utils/ai/choose-rule/bulk-process-emails"; +import { bulkProcessInboxEmails } from "@/utils/ai/choose-rule/bulk-process-emails"; import { getEmailAccountWithAi } from "@/utils/user/get"; export const createRuleAction = actionClient @@ -412,9 +412,11 @@ export const createRulesOnboardingAction = actionClient await Promise.allSettled(promises); after(() => - processOnboardingEmails({ + bulkProcessInboxEmails({ emailAccount, provider, + maxEmails: 20, + skipArchive: true, logger, }), ); diff --git a/apps/web/utils/ai/choose-rule/bulk-process-emails.ts b/apps/web/utils/ai/choose-rule/bulk-process-emails.ts index c84a41760a..264e6b83d2 100644 --- a/apps/web/utils/ai/choose-rule/bulk-process-emails.ts +++ b/apps/web/utils/ai/choose-rule/bulk-process-emails.ts @@ -5,20 +5,22 @@ import type { Logger } from "@/utils/logger"; import type { EmailAccountWithAI } from "@/utils/llms/types"; import type { ParsedMessage } from "@/utils/types"; -const ONBOARDING_EMAIL_COUNT = 20; - -export async function processOnboardingEmails({ +export async function bulkProcessInboxEmails({ emailAccount, provider, + maxEmails, + skipArchive, logger: log, }: { emailAccount: EmailAccountWithAI; provider: string; + maxEmails: number; + skipArchive: boolean; logger: Logger; }) { - const logger = log.with({ module: "onboarding/process-emails" }); + const logger = log.with({ module: "bulk-process-emails" }); - logger.info("Starting onboarding email processing"); + logger.info("Starting bulk inbox email processing"); try { const emailProvider = await createEmailProvider({ @@ -30,7 +32,7 @@ export async function processOnboardingEmails({ const [{ messages }, rules] = await Promise.all([ emailProvider.getMessagesByFields({ type: "inbox", - maxResults: ONBOARDING_EMAIL_COUNT, + maxResults: maxEmails, }), prisma.rule.findMany({ where: { @@ -42,12 +44,12 @@ export async function processOnboardingEmails({ ]); if (messages.length === 0) { - logger.info("No inbox emails to process for onboarding"); + logger.info("No inbox emails to process"); return; } if (rules.length === 0) { - logger.info("No rules found for onboarding processing"); + logger.info("No rules found"); return; } @@ -72,12 +74,12 @@ export async function processOnboardingEmails({ isTest: false, modelType: "economy", logger, - skipArchive: true, + skipArchive, }); processedCount++; } catch (error) { errorCount++; - logger.error("Error processing email during onboarding", { + logger.error("Error processing email", { messageId: message.id, error, }); @@ -85,13 +87,13 @@ export async function processOnboardingEmails({ } } - logger.info("Completed onboarding email processing", { + logger.info("Completed bulk email processing", { processedCount, errorCount, totalEmails: uniqueMessages.length, }); } catch (error) { - logger.error("Failed to process onboarding emails", { error }); + logger.error("Failed to process emails", { error }); } }