Skip to content

Comments

feat: Add Outlook support to Deep Clean feature#875

Closed
salja03-t21 wants to merge 4 commits intoelie222:mainfrom
salja03-t21:outlook-deep-clean-pr
Closed

feat: Add Outlook support to Deep Clean feature#875
salja03-t21 wants to merge 4 commits intoelie222:mainfrom
salja03-t21:outlook-deep-clean-pr

Conversation

@salja03-t21
Copy link

@salja03-t21 salja03-t21 commented Oct 29, 2025

Overview

This PR adds Outlook support to the existing Deep Clean feature, enabling Outlook users to perform inbox cleanup operations similar to Gmail users.

Changes

  • Outlook API Integration: Added to handle Outlook-specific cleanup operations
  • Provider-Agnostic UI: Updated navigation and wizard components to work with both Gmail and Outlook
  • Enhanced UX: Added backward navigation support in the cleanup wizard
  • Provider Detection: Improved provider property path handling across components

Technical Details

  • Added new API endpoint for Outlook cleanup operations
  • Updated email constants to support both providers
  • Modified preview step to handle provider-specific differences
  • Enhanced wizard navigation with back button functionality

Testing

Tested with both Gmail and Outlook accounts to ensure:

  • Deep Clean feature accessible for both providers
  • API endpoints properly route based on provider
  • Preview and confirmation steps work correctly
  • Navigation flows smoothly in both directions

Files Changed

  • apps/web/app/api/clean/outlook/route.ts (new)
  • apps/web/utils/email/constants.ts (new)
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/components/SideNav.tsx
  • Various wizard step components

All changes maintain backward compatibility with existing Gmail functionality while extending support to Outlook users.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added back button navigation throughout the cleaning workflow to easily revise previous steps
    • Introduced preview mode to review and test cleaning actions before executing on full inbox
    • Extended support to Outlook email accounts alongside existing Gmail support
    • Enhanced action descriptions with provider-specific language for Gmail and Outlook

Implement complete Outlook integration for the deep-clean feature:

Backend Changes:
- Created /api/clean/outlook/route.ts with Graph API integration
- Implemented archive operation (moves to Archive folder)
- Implemented mark as read operation
- Added provider detection and routing in /api/clean/route.ts
- Provider-agnostic static rules (starred/flagged, sent, etc.)
- QStash integration with signature verification

UI Improvements:
- Updated 'Starred emails' to 'Starred/Flagged emails'
- Provider-aware confirmation text (Gmail vs Outlook)
- Dynamic messaging for archive and mark-as-read operations
- Made Deep Clean available for all email providers (not just Gmail)

Files Created:
- apps/web/app/api/clean/outlook/route.ts
- apps/web/utils/email/constants.ts

Files Modified:
- apps/web/app/api/clean/route.ts
- apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
- apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
- apps/web/components/SideNav.tsx
- apps/web/utils/outlook/folders.ts
- apps/web/utils/types.ts
- .gitignore
- Added onPrevious() function to useStep hook
- Added Back buttons to all wizard steps:
  * ActionSelectionStep
  * CleanInstructionsStep
  * TimeRangeStep
  * ConfirmationStep
- Users can now navigate back through the wizard steps
- Improves UX by allowing users to correct mistakes without restarting
- Both lines 87 and 153 now correctly access provider from account object
- Pattern matches usage throughout codebase (safe-action.ts, etc)
- Type definition includes provider in account select
Added preview functionality for clean operations with Outlook support:
- Created PreviewStep component for email preview before cleanup
- Added provider-aware email rendering (Gmail vs Outlook)
- Improved UX with email preview and confirmation flow
- Added useEmailStream hook for real-time preview updates
@vercel
Copy link

vercel bot commented Oct 29, 2025

@salja03-t21 is attempting to deploy a commit to the Inbox Zero OSS Program Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant
Copy link

CLAassistant commented Oct 29, 2025

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 29, 2025

Walkthrough

Multi-provider email support implementation introducing Outlook alongside Gmail, adding back-navigation UI to the clean workflow, provider-agnostic email state mappings, new Outlook API routes and folder utilities, refactored server actions to use provider-based operations, and UI component ref-forwarding fixes.

Changes

Cohort / File(s) Summary
Configuration & Type Extensions
\.gitignore`, apps/web/utils/types.ts`
Updated gitignore to exclude memory-bank directory; added optional isFlagged property to ParsedMessage interface for Outlook message metadata.
Email Provider Abstractions
apps/web/utils/email/constants.ts
Introduced provider-agnostic email state and category enums (EmailState, EmailCategory, InboxZeroFolder) with cross-provider mappings (GmailStateMap, OutlookStateMap, etc.) and helper utility getInboxZeroFolderName() for composing folder names.
Outlook Utilities
apps/web/utils/outlook/folders.ts
Added Outlook folder operations: getOrCreateInboxZeroFolder(), moveMessageToFolder(), markMessageAsRead(), flagMessage(), and getWellKnownFolderId() for managing message states and folder manipulations.
Clean Workflow UI Steps
apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx, CleanInstructionsStep.tsx, TimeRangeStep.tsx, ConfirmationStep.tsx
Added back-navigation support via onPrevious hook callback and Back button UI; ConfirmationStep now includes provider-conditional text for archive and read actions (Gmail vs. Outlook phrasing).
Preview & Step Management
apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx, useStep.tsx
New PreviewStep component orchestrating email preview cleanup with streaming; enhanced useStep hook to include onPrevious callback for backward navigation.
API Routes
apps/web/app/api/clean/route.ts, apps/web/app/api/clean/outlook/route.ts
Updated main clean route to route requests to Gmail or Outlook endpoints based on provider; new Outlook-specific route implementing archive, read, and cleanup actions with Qstash authentication and database result persistence.
Server Actions
apps/web/utils/actions/clean.ts
Refactored cleanInboxAction, undoCleanInboxAction, and changeKeepToDoneAction to use provider-based email operations instead of direct Gmail APIs; updated function signatures to include provider in context.
Navigation & UI Components
apps/web/components/SideNav.tsx, apps/web/components/ui/dropdown-menu.tsx, apps/web/components/ui/tooltip.tsx
Deep Clean navigation item now available for all providers; wrapped DropdownMenuTrigger and TooltipTrigger with React.forwardRef for proper ref forwarding and added suppressHydrationWarning.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CleanFlow as Clean Workflow UI
    participant useStep as useStep Hook
    participant API as /api/clean Route
    participant Provider as Email Provider (Gmail/Outlook)
    participant DB as Database

    User->>CleanFlow: Start Clean
    CleanFlow->>useStep: step = 0 (ActionSelectionStep)
    
    User->>CleanFlow: Select Action
    CleanFlow->>useStep: onNext() → step = 1
    
    CleanFlow->>CleanFlow: CleanInstructionsStep
    User->>CleanFlow: Click Back
    CleanFlow->>useStep: onPrevious() → step = 0
    
    User->>CleanFlow: Select Action Again
    CleanFlow->>useStep: onNext() → step = 1
    User->>CleanFlow: Continue
    CleanFlow->>useStep: onNext() → step = 2 (TimeRangeStep)
    User->>CleanFlow: Continue
    CleanFlow->>useStep: onNext() → step = 3 (PreviewStep)
    
    CleanFlow->>API: POST /api/clean (with provider)
    API->>API: Determine provider (Gmail vs Outlook)
    alt Gmail
        API->>Provider: Use Gmail client
    else Outlook
        API->>Provider: Use Outlook client
    end
    
    Provider->>Provider: Fetch messages
    Provider->>DB: Query email account
    API->>DB: Store cleanup results
    API-->>CleanFlow: Return preview data
    
    CleanFlow->>User: Show preview with Back & Run buttons
    User->>CleanFlow: Click Back
    CleanFlow->>useStep: Navigate to previous step
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • Provider routing logic in /api/clean/route.ts: Verify conditional Gmail vs. Outlook flows are exhaustive and handle both providers correctly, including message fetching differences (Gmail getThreadMessages vs. Outlook getOutlookMessage)
  • Function signature updates: Confirm all callers of cleanInboxAction, undoCleanInboxAction, and changeKeepToDoneAction have been updated to pass provider in context
  • Outlook folder helpers: Validate that getOrCreateInboxZeroFolder(), moveMessageToFolder(), and markMessageAsRead() properly handle API responses and error cases
  • Cross-provider email state mappings: Ensure GmailStateMap and OutlookStateMap cover all necessary state transitions and correctly represent archive/read semantics for each provider
  • PreviewStep data flow: Verify that runPreview() and full inbox execution properly marshal and propagate provider context through the action chain

Possibly related PRs

  • Outlook support #537: Implements the foundational multi-provider email abstraction and Outlook support—refactoring Gmail-specific APIs to a unified EmailProvider with GmailProvider/OutlookProvider implementations and adding core Outlook routes and clients.
  • Use emailprovider in more places #821: "Use emailprovider in more places"—directly overlaps with refactoring actions and utilities to use the provider abstraction, updating function signatures and removing direct Gmail client dependencies.
  • Refactor Outlook folder functionality #678: Modifies apps/web/utils/outlook/folders.ts with identical Outlook folder operation helpers and folder-based message routing logic for archive and read actions.

Suggested reviewers

  • elie222
  • mosesjames7271-svg

Poem

🐰 Hoppy hoppers, email hops,
Now Gmail and Outlook both have stops!
Back buttons bounce through the workflow maze,
Multi-provider magic in so many ways!
Outlook's folders join the Inbox Zero play,
Two providers, one clean—hooray, hooray! 🎉

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.43% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "feat: Add Outlook support to Deep Clean feature" directly and clearly summarizes the primary objective of the PR. The changeset demonstrates comprehensive Outlook integration across the Deep Clean feature, including a new Outlook API endpoint, provider-aware routing logic, UI updates to enable Deep Clean for all providers (not just Gmail), and new provider-agnostic email constants. The title accurately captures this scope without being vague, using specific, actionable language ("Add Outlook support") that clearly identifies both the provider being added and the feature being extended. A teammate scanning the history would immediately understand that this PR introduces multi-provider support for an existing feature.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (7)
.gitignore (1)

80-80: Redundant coverage ignore pattern.

Line 10 already ignores /coverage (anchored to the repo root). Line 80 adds coverage (which matches at any level). Both patterns effectively ignore the same directory at the project root, making the new entry redundant.

Consider removing line 80 or consolidating these entries.

apps/web/components/SideNav.tsx (2)

59-59: Remove unused import.

The isGoogleProvider import is no longer used after making the Deep Clean feature provider-agnostic.

Apply this diff to remove the unused import:

-import { isGoogleProvider } from "@/utils/email/provider-types";

107-107: Remove unused dependency from useMemo.

The provider variable is included in the dependency array but is no longer used in the navItems computation. This can cause unnecessary re-renders.

Apply this diff to remove the unused dependency:

-    [currentEmailAccountId, provider],
+    [currentEmailAccountId],
apps/web/components/ui/tooltip.tsx (1)

3-3: Consider using named imports instead of namespace imports.

The coding guidelines specify avoiding namespace imports. Consider using named imports instead:

-import * as React from "react";
+import { forwardRef, type ElementRef, type ComponentPropsWithoutRef } from "react";

Then update the component definition accordingly. However, given that namespace imports are already used for Radix UI primitives throughout this file, this is a low-priority style consistency point.

apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx (2)

134-148: Consider effect dependency stability.

The useEffect depends on runPreview, which will be recreated whenever its dependencies change. This could cause the preview to re-run unexpectedly if search params change during the component's lifecycle. If this becomes an issue, consider extracting the preview logic or using a useRef flag to ensure it only runs once on mount.


154-163: Consider using relative navigation for the Back button.

The Back button hardcodes step=4, which is fragile if the step ordering changes. According to the PR context, other wizard components have adopted useStep with an onPrevious callback. Consider aligning this component with that pattern for consistency.

Example using relative navigation:

import { useStep } from "@/app/(app)/[emailAccountId]/clean/useStep";

// In component:
const { onPrevious } = useStep();

// In button:
<Button variant="outline" onClick={onPrevious}>
  ← Back
</Button>
apps/web/utils/email/constants.ts (1)

29-39: Clarify ARCHIVE label usage.

The ARCHIVE entry maps to the string "ARCHIVE", but Gmail doesn't have an actual "ARCHIVE" label—archiving means removing the INBOX label. While the comment on line 38 notes this, consumers of this mapping might try to use it as a real label ID, causing runtime errors. Consider documenting this special case more prominently or handling it differently in the API.

Example approach:

// Option 1: Use a symbol to indicate special handling
export const SPECIAL_STATE_ARCHIVE = Symbol("ARCHIVE");

// Option 2: Add documentation
/**
 * Gmail Label mappings
 * Note: ARCHIVE is a special case - it means "not in INBOX", not an actual label
 */
export const GmailStateMap: Record<EmailState, string> = {
  // ... existing mappings
  [EmailState.ARCHIVE]: "ARCHIVE", // Special: indicates absence of INBOX label, not a real label
};
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f085fea and 0628a06.

📒 Files selected for processing (16)
  • .gitignore (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx (2 hunks)
  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx (3 hunks)
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx (4 hunks)
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx (2 hunks)
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx (1 hunks)
  • apps/web/app/api/clean/outlook/route.ts (1 hunks)
  • apps/web/app/api/clean/route.ts (8 hunks)
  • apps/web/components/SideNav.tsx (1 hunks)
  • apps/web/components/ui/dropdown-menu.tsx (1 hunks)
  • apps/web/components/ui/tooltip.tsx (2 hunks)
  • apps/web/utils/actions/clean.ts (3 hunks)
  • apps/web/utils/email/constants.ts (1 hunks)
  • apps/web/utils/outlook/folders.ts (1 hunks)
  • apps/web/utils/types.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (24)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TypeScript with strict null checks
Path aliases: Use @/ for imports from project root
Use proper error handling with try/catch blocks
Format code with Prettier
Leverage TypeScript inference for better DX

Files:

  • apps/web/utils/types.ts
  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/api/clean/outlook/route.ts
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/utils/outlook/folders.ts
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/utils/actions/clean.ts
  • apps/web/components/ui/tooltip.tsx
  • apps/web/utils/email/constants.ts
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/app/api/clean/route.ts
  • apps/web/components/ui/dropdown-menu.tsx
!{.cursor/rules/*.mdc}

📄 CodeRabbit inference engine (.cursor/rules/cursor-rules.mdc)

Never place rule files in the project root, in subdirectories outside .cursor/rules, or in any other location

Files:

  • apps/web/utils/types.ts
  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/api/clean/outlook/route.ts
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/utils/outlook/folders.ts
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/utils/actions/clean.ts
  • apps/web/components/ui/tooltip.tsx
  • apps/web/utils/email/constants.ts
  • .gitignore
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/app/api/clean/route.ts
  • apps/web/components/ui/dropdown-menu.tsx
**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/form-handling.mdc)

**/*.ts: The same validation should be done in the server action too
Define validation schemas using Zod

Files:

  • apps/web/utils/types.ts
  • apps/web/app/api/clean/outlook/route.ts
  • apps/web/utils/outlook/folders.ts
  • apps/web/utils/actions/clean.ts
  • apps/web/utils/email/constants.ts
  • apps/web/app/api/clean/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/logging.mdc)

**/*.{ts,tsx}: Use createScopedLogger for logging in backend TypeScript files
Typically add the logger initialization at the top of the file when using createScopedLogger
Only use .with() on a logger instance within a specific function, not for a global logger

Import Prisma in the project using import prisma from "@/utils/prisma";

**/*.{ts,tsx}: Don't use TypeScript enums.
Don't use TypeScript const enum.
Don't use the TypeScript directive @ts-ignore.
Don't use primitive type aliases or misleading types.
Don't use empty type parameters in type aliases and interfaces.
Don't use any or unknown as type constraints.
Don't use implicit any type on variable declarations.
Don't let variables evolve into any type through reassignments.
Don't use non-null assertions with the ! postfix operator.
Don't misuse the non-null assertion operator (!) in TypeScript files.
Don't use user-defined types.
Use as const instead of literal types and type annotations.
Use export type for types.
Use import type for types.
Don't declare empty interfaces.
Don't merge interfaces and classes unsafely.
Don't use overload signatures that aren't next to each other.
Use the namespace keyword instead of the module keyword to declare TypeScript namespaces.
Don't use TypeScript namespaces.
Don't export imported variables.
Don't add type annotations to variables, parameters, and class properties that are initialized with literal expressions.
Don't use parameter properties in class constructors.
Use either T[] or Array consistently.
Initialize each enum member value explicitly.
Make sure all enum members are literal values.

Files:

  • apps/web/utils/types.ts
  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/api/clean/outlook/route.ts
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/utils/outlook/folders.ts
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/utils/actions/clean.ts
  • apps/web/components/ui/tooltip.tsx
  • apps/web/utils/email/constants.ts
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/app/api/clean/route.ts
  • apps/web/components/ui/dropdown-menu.tsx
apps/web/utils/**

📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)

Create utility functions in utils/ folder for reusable logic

Files:

  • apps/web/utils/types.ts
  • apps/web/utils/outlook/folders.ts
  • apps/web/utils/actions/clean.ts
  • apps/web/utils/email/constants.ts
apps/web/utils/**/*.ts

📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)

apps/web/utils/**/*.ts: Use lodash utilities for common operations (arrays, objects, strings)
Import specific lodash functions to minimize bundle size

Files:

  • apps/web/utils/types.ts
  • apps/web/utils/outlook/folders.ts
  • apps/web/utils/actions/clean.ts
  • apps/web/utils/email/constants.ts
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{js,jsx,ts,tsx}: Don't use elements in Next.js projects.
Don't use elements in Next.js projects.
Don't use namespace imports.
Don't access namespace imports dynamically.
Don't use global eval().
Don't use console.
Don't use debugger.
Don't use var.
Don't use with statements in non-strict contexts.
Don't use the arguments object.
Don't use consecutive spaces in regular expression literals.
Don't use the comma operator.
Don't use unnecessary boolean casts.
Don't use unnecessary callbacks with flatMap.
Use for...of statements instead of Array.forEach.
Don't create classes that only have static members (like a static namespace).
Don't use this and super in static contexts.
Don't use unnecessary catch clauses.
Don't use unnecessary constructors.
Don't use unnecessary continue statements.
Don't export empty modules that don't change anything.
Don't use unnecessary escape sequences in regular expression literals.
Don't use unnecessary labels.
Don't use unnecessary nested block statements.
Don't rename imports, exports, and destructured assignments to the same name.
Don't use unnecessary string or template literal concatenation.
Don't use String.raw in template literals when there are no escape sequences.
Don't use useless case statements in switch statements.
Don't use ternary operators when simpler alternatives exist.
Don't use useless this aliasing.
Don't initialize variables to undefined.
Don't use the void operators (they're not familiar).
Use arrow functions instead of function expressions.
Use Date.now() to get milliseconds since the Unix Epoch.
Use .flatMap() instead of map().flat() when possible.
Use literal property access instead of computed property access.
Don't use parseInt() or Number.parseInt() when binary, octal, or hexadecimal literals work.
Use concise optional chaining instead of chained logical expressions.
Use regular expression literals instead of the RegExp constructor when possible.
Don't use number literal object member names th...

Files:

  • apps/web/utils/types.ts
  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/api/clean/outlook/route.ts
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/utils/outlook/folders.ts
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/utils/actions/clean.ts
  • apps/web/components/ui/tooltip.tsx
  • apps/web/utils/email/constants.ts
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/app/api/clean/route.ts
  • apps/web/components/ui/dropdown-menu.tsx
!pages/_document.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

!pages/_document.{js,jsx,ts,tsx}: Don't import next/document outside of pages/_document.jsx in Next.js projects.
Don't import next/document outside of pages/_document.jsx in Next.js projects.

Files:

  • apps/web/utils/types.ts
  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/api/clean/outlook/route.ts
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/utils/outlook/folders.ts
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/utils/actions/clean.ts
  • apps/web/components/ui/tooltip.tsx
  • apps/web/utils/email/constants.ts
  • .gitignore
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/app/api/clean/route.ts
  • apps/web/components/ui/dropdown-menu.tsx
apps/web/app/**

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

NextJS app router structure with (app) directory

Files:

  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/api/clean/outlook/route.ts
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
  • apps/web/app/api/clean/route.ts
apps/web/**/*.tsx

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/**/*.tsx: Follow tailwindcss patterns with prettier-plugin-tailwindcss
Prefer functional components with hooks
Use shadcn/ui components when available
Ensure responsive design with mobile-first approach
Follow consistent naming conventions (PascalCase for components)
Use LoadingContent component for async data
Use result?.serverError with toastError and toastSuccess
Use LoadingContent component to handle loading and error states consistently
Pass loading, error, and children props to LoadingContent

Files:

  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/components/ui/tooltip.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/components/ui/dropdown-menu.tsx
**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/form-handling.mdc)

**/*.tsx: Use React Hook Form with Zod for validation
Validate form inputs before submission
Show validation errors inline next to form fields

Files:

  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/components/ui/tooltip.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/components/ui/dropdown-menu.tsx
apps/web/app/(app)/*/**

📄 CodeRabbit inference engine (.cursor/rules/page-structure.mdc)

Components for the page are either put in page.tsx, or in the apps/web/app/(app)/PAGE_NAME folder

Files:

  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
apps/web/app/(app)/*/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/page-structure.mdc)

If you need to use onClick in a component, that component is a client component and file must start with 'use client'

Files:

  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
apps/web/app/(app)/*/**/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/page-structure.mdc)

If we're in a deeply nested component we will use swr to fetch via API

Files:

  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
apps/web/app/**/*.tsx

📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)

Components with onClick must be client components with use client directive

Files:

  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{jsx,tsx}: Don't destructure props inside JSX components in Solid projects.
Don't use both children and dangerouslySetInnerHTML props on the same element.
Don't use Array index in keys.
Don't assign to React component props.
Don't define React components inside other components.
Don't use event handlers on non-interactive elements.
Don't assign JSX properties multiple times.
Don't add extra closing tags for components without children.
Use <>...</> instead of ....
Don't insert comments as text nodes.
Don't use the return value of React.render.
Make sure all dependencies are correctly specified in React hooks.
Make sure all React hooks are called from the top level of component functions.
Don't use unnecessary fragments.
Don't pass children as props.
Use semantic elements instead of role attributes in JSX.

Files:

  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/components/ui/tooltip.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/components/ui/dropdown-menu.tsx
**/*.{html,jsx,tsx}

📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)

**/*.{html,jsx,tsx}: Don't use or elements.
Don't use accessKey attribute on any HTML element.
Don't set aria-hidden="true" on focusable elements.
Don't add ARIA roles, states, and properties to elements that don't support them.
Only use the scope prop on elements.
Don't assign non-interactive ARIA roles to interactive HTML elements.
Make sure label elements have text content and are associated with an input.
Don't assign interactive ARIA roles to non-interactive HTML elements.
Don't assign tabIndex to non-interactive HTML elements.
Don't use positive integers for tabIndex property.
Don't include "image", "picture", or "photo" in img alt prop.
Don't use explicit role property that's the same as the implicit/default role.
Make static elements with click handlers use a valid role attribute.
Always include a title element for SVG elements.
Give all elements requiring alt text meaningful information for screen readers.
Make sure anchors have content that's accessible to screen readers.
Assign tabIndex to non-interactive HTML elements with aria-activedescendant.
Include all required ARIA attributes for elements with ARIA roles.
Make sure ARIA properties are valid for the element's supported roles.
Always include a type attribute for button elements.
Make elements with interactive roles and handlers focusable.
Give heading elements content that's accessible to screen readers (not hidden with aria-hidden).
Always include a lang attribute on the html element.
Always include a title attribute for iframe elements.
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress.
Accompany onMouseOver/onMouseOut with onFocus/onBlur.
Include caption tracks for audio and video elements.
Make sure all anchors are valid and navigable.
Ensure all ARIA properties (aria-*) are valid.
Use valid, non-abstract ARIA roles for elements with ARIA roles.
Use valid ARIA state and property values.
Use valid values for the autocomplete attribute on input eleme...

Files:

  • apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
  • apps/web/components/ui/tooltip.tsx
  • apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/components/ui/dropdown-menu.tsx
apps/web/app/api/**/route.ts

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/app/api/**/route.ts: Use withAuth for user-level operations
Use withEmailAccount for email-account-level operations
Do NOT use POST API routes for mutations - use server actions instead
No need for try/catch in GET routes when using middleware
Export response types from GET routes

apps/web/app/api/**/route.ts: Wrap all GET API route handlers with withAuth or withEmailAccount middleware for authentication and authorization.
Export response types from GET API routes for type-safe client usage.
Do not use try/catch in GET API routes when using authentication middleware; rely on centralized error handling.

Files:

  • apps/web/app/api/clean/outlook/route.ts
  • apps/web/app/api/clean/route.ts
**/api/**/route.ts

📄 CodeRabbit inference engine (.cursor/rules/security.mdc)

**/api/**/route.ts: ALL API routes that handle user data MUST use appropriate authentication and authorization middleware (withAuth or withEmailAccount).
ALL database queries in API routes MUST be scoped to the authenticated user/account (e.g., include userId or emailAccountId in query filters).
Always validate that resources belong to the authenticated user before performing operations (resource ownership validation).
Use withEmailAccount middleware for API routes that operate on a specific email account (i.e., use or require emailAccountId).
Use withAuth middleware for API routes that operate at the user level (i.e., use or require only userId).
Use withError middleware (with proper validation) for public endpoints, custom authentication, or cron endpoints.
Cron endpoints MUST use withError middleware and validate the cron secret using hasCronSecret(request) or hasPostCronSecret(request).
Cron endpoints MUST capture unauthorized attempts with captureException and return a 401 status for unauthorized requests.
All parameters in API routes MUST be validated for type, format, and length before use.
Request bodies in API routes MUST be validated using Zod schemas before use.
All Prisma queries in API routes MUST only return necessary fields and never expose sensitive data.
Error messages in API routes MUST not leak internal information or sensitive data; use generic error messages and SafeError where appropriate.
API routes MUST use a consistent error response format, returning JSON with an error message and status code.
All findUnique and findFirst Prisma calls in API routes MUST include ownership filters (e.g., userId or emailAccountId).
All findMany Prisma calls in API routes MUST be scoped to the authenticated user's data.
Never use direct object references in API routes without ownership checks (prevent IDOR vulnerabilities).
Prevent mass assignment vulnerabilities by only allowing explicitly whitelisted fields in update operations in AP...

Files:

  • apps/web/app/api/clean/outlook/route.ts
  • apps/web/app/api/clean/route.ts
apps/web/app/api/**/*.{ts,js}

📄 CodeRabbit inference engine (.cursor/rules/security-audit.mdc)

apps/web/app/api/**/*.{ts,js}: All API route handlers in 'apps/web/app/api/' must use authentication middleware: withAuth, withEmailAccount, or withError (with custom authentication logic).
All Prisma queries in API routes must include user/account filtering (e.g., emailAccountId or userId in WHERE clauses) to prevent unauthorized data access.
All parameters used in API routes must be validated before use; do not use parameters from 'params' or request bodies directly in queries without validation.
Request bodies in API routes should use Zod schemas for validation.
API routes should only return necessary fields using Prisma's 'select' and must not include sensitive data in error messages.
Error messages in API routes must not reveal internal details; use generic errors and SafeError for user-facing errors.
All QStash endpoints (API routes called via publishToQstash or publishToQstashQueue) must use verifySignatureAppRouter to verify request authenticity.
All cron endpoints in API routes must use hasCronSecret or hasPostCronSecret for authentication.
Do not hardcode weak or plaintext secrets in API route files; secrets must not be directly assigned as string literals.
Review all new withError usage in API routes to ensure custom authentication is implemented where required.

Files:

  • apps/web/app/api/clean/outlook/route.ts
  • apps/web/app/api/clean/route.ts
apps/web/utils/actions/**/*.ts

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

apps/web/utils/actions/**/*.ts: Use server actions for all mutations (create/update/delete operations)
next-safe-action provides centralized error handling
Use Zod schemas for validation on both client and server
Use revalidatePath in server actions for cache invalidation

apps/web/utils/actions/**/*.ts: Use server actions (with next-safe-action) for all mutations (create/update/delete operations); do NOT use POST API routes for mutations.
Use revalidatePath in server actions to invalidate cache after mutations.

Files:

  • apps/web/utils/actions/clean.ts
apps/web/utils/actions/*.ts

📄 CodeRabbit inference engine (.cursor/rules/server-actions.mdc)

apps/web/utils/actions/*.ts: Implement all server actions using the next-safe-action library for type safety, input validation, context management, and error handling. Refer to apps/web/utils/actions/safe-action.ts for client definitions (actionClient, actionClientUser, adminActionClient).
Use actionClientUser when only authenticated user context (userId) is needed.
Use actionClient when both authenticated user context and a specific emailAccountId are needed. The emailAccountId must be bound when calling the action from the client.
Use adminActionClient for actions restricted to admin users.
Access necessary context (like userId, emailAccountId, etc.) provided by the safe action client via the ctx object in the .action() handler.
Server Actions are strictly for mutations (operations that change data, e.g., creating, updating, deleting). Do NOT use Server Actions for data fetching (GET operations). For data fetching, use dedicated GET API Routes combined with SWR Hooks.
Use SafeError for expected/handled errors within actions if needed. next-safe-action provides centralized error handling.
Use the .metadata({ name: "actionName" }) method to provide a meaningful name for monitoring. Sentry instrumentation is automatically applied via withServerActionInstrumentation within the safe action clients.
If an action modifies data displayed elsewhere, use revalidatePath or revalidateTag from next/cache within the action handler as needed.

Server action files must start with use server

Files:

  • apps/web/utils/actions/clean.ts
apps/web/components/**/*.tsx

📄 CodeRabbit inference engine (apps/web/CLAUDE.md)

Use React Hook Form with Zod validation for form handling

Use the LoadingContent component to handle loading and error states consistently in data-fetching components.

Use PascalCase for components (e.g. components/Button.tsx)

Files:

  • apps/web/components/ui/tooltip.tsx
  • apps/web/components/SideNav.tsx
  • apps/web/components/ui/dropdown-menu.tsx
apps/web/components/ui/**

📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)

Shadcn components are in components/ui

Files:

  • apps/web/components/ui/tooltip.tsx
  • apps/web/components/ui/dropdown-menu.tsx
🧠 Learnings (2)
📚 Learning: 2025-07-08T13:14:07.449Z
Learnt from: elie222
PR: elie222/inbox-zero#537
File: apps/web/app/(app)/[emailAccountId]/clean/onboarding/page.tsx:30-34
Timestamp: 2025-07-08T13:14:07.449Z
Learning: The clean onboarding page in apps/web/app/(app)/[emailAccountId]/clean/onboarding/page.tsx is intentionally Gmail-specific and should show an error for non-Google email accounts rather than attempting to support multiple providers.

Applied to files:

  • apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx
📚 Learning: 2025-07-18T15:05:34.899Z
Learnt from: CR
PR: elie222/inbox-zero#0
File: .cursor/rules/gmail-api.mdc:0-0
Timestamp: 2025-07-18T15:05:34.899Z
Learning: Applies to apps/web/utils/gmail/**/*.ts : Keep provider-specific implementation details isolated in the appropriate utils subfolder (e.g., 'apps/web/utils/gmail/')

Applied to files:

  • apps/web/utils/actions/clean.ts
  • apps/web/utils/email/constants.ts
🧬 Code graph analysis (9)
apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx (1)
apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx (1)
  • useStep (5-27)
apps/web/app/api/clean/outlook/route.ts (6)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-80)
apps/web/utils/error.ts (1)
  • SafeError (86-96)
apps/web/utils/outlook/client.ts (1)
  • getOutlookClientWithRefresh (84-156)
apps/web/utils/outlook/folders.ts (2)
  • moveMessageToFolder (175-183)
  • markMessageAsRead (188-196)
apps/web/utils/outlook/message.ts (1)
  • WELL_KNOWN_FOLDERS (72-79)
apps/web/utils/middleware.ts (1)
  • withError (283-288)
apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx (5)
apps/web/providers/EmailAccountProvider.tsx (1)
  • useAccount (79-89)
apps/web/utils/actions/clean.ts (1)
  • cleanInboxAction (27-181)
apps/web/app/(app)/[emailAccountId]/clean/useEmailStream.ts (1)
  • useEmailStream (8-154)
apps/web/components/LoadingContent.tsx (1)
  • LoadingContent (13-27)
apps/web/app/(app)/[emailAccountId]/clean/EmailFirehose.tsx (1)
  • EmailFirehose (13-178)
apps/web/utils/outlook/folders.ts (3)
apps/web/utils/outlook/client.ts (1)
  • OutlookClient (19-71)
apps/web/utils/email/google.ts (1)
  • getOrCreateOutlookFolderIdByName (926-929)
apps/web/utils/email/microsoft.ts (1)
  • getOrCreateOutlookFolderIdByName (1284-1286)
apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx (1)
apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx (1)
  • useStep (5-27)
apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx (3)
apps/web/providers/EmailAccountProvider.tsx (1)
  • useAccount (79-89)
apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx (1)
  • useStep (5-27)
apps/web/utils/email/provider-types.ts (1)
  • isGoogleProvider (1-3)
apps/web/utils/actions/clean.ts (3)
apps/web/utils/email/provider.ts (1)
  • createEmailProvider (13-29)
apps/web/utils/error.ts (1)
  • SafeError (86-96)
apps/web/utils/label.ts (1)
  • inboxZeroLabels (30-61)
apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx (1)
apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx (1)
  • useStep (5-27)
apps/web/app/api/clean/route.ts (9)
apps/web/utils/types.ts (1)
  • ParsedMessage (47-63)
apps/web/utils/email/provider-types.ts (1)
  • isGoogleProvider (1-3)
apps/web/utils/gmail/client.ts (1)
  • getGmailClientWithRefresh (49-103)
apps/web/utils/gmail/thread.ts (1)
  • getThreadMessages (140-149)
apps/web/utils/outlook/client.ts (1)
  • getOutlookClientWithRefresh (84-156)
apps/web/utils/gmail/label.ts (1)
  • GmailLabel (20-34)
apps/web/app/api/clean/gmail/route.ts (1)
  • CleanGmailBody (26-26)
apps/web/app/api/clean/outlook/route.ts (1)
  • CleanOutlookBody (29-29)
apps/web/utils/upstash/index.ts (1)
  • publishToQstash (14-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Jit Security
🔇 Additional comments (20)
apps/web/components/SideNav.tsx (1)

91-95: LGTM! Provider-agnostic Deep Clean navigation.

The Deep Clean navigation item is now available for all email providers, correctly supporting the PR's goal of extending functionality to Outlook. The feature flag filtering (lines 113-114) ensures controlled rollout.

apps/web/components/ui/tooltip.tsx (1)

31-42: Verify the necessity of suppressHydrationWarning.

The forwardRef implementation looks correct and properly enables ref forwarding. However, the suppressHydrationWarning attribute should be verified—it can mask legitimate hydration mismatches between server and client renders.

Please confirm this attribute is intentionally added to handle a known hydration difference (e.g., client-side event handlers or dynamic attributes) and not preemptively suppressing potential issues.

apps/web/components/ui/dropdown-menu.tsx (1)

11-21: Verify the necessity of suppressHydrationWarning.

The forwardRef implementation correctly enables ref forwarding with proper TypeScript typing. However, the suppressHydrationWarning attribute should be verified to ensure it's addressing a known hydration difference rather than masking potential issues.

Please confirm this is intentionally handling a known client/server render difference specific to dropdown triggers (such as event handlers or dynamic ARIA attributes).

apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsx (1)

20-21: Back wiring looks good; label update aligns with Outlook

Back button uses type="button" correctly. “Starred/Flagged emails” label matches multi‑provider context. No changes needed.

Also applies to: 54-55, 106-111

apps/web/app/api/clean/outlook/route.ts (4)

1-18: LGTM!

Imports and logger initialization follow coding guidelines correctly.


20-29: LGTM!

Schema validation with Zod follows best practices, and the optional label fields align with Outlook's category limitations noted in the code.


126-172: LGTM!

The save operations properly use Prisma relationships and are scoped to the verified emailAccountId.


174-183: LGTM!

POST handler correctly uses QStash authentication with verifySignatureAppRouter and withError middleware per coding guidelines. The use of a POST route here is justified as this is a queue-triggered endpoint, not a client-facing mutation.

apps/web/utils/outlook/folders.ts (4)

154-169: LGTM!

The helper function correctly leverages existing utilities and provides a clean interface for InboxZero folder management.


175-196: LGTM!

Both functions correctly use Microsoft Graph API endpoints. The error handling pattern is consistent with other utilities in this file.


202-215: LGTM!

Flag operation correctly maps to Outlook's flag/star concept and follows Graph API conventions.


221-238: LGTM!

Well-known folder retrieval correctly uses Microsoft Graph's standard folder paths and provides type-safe access.

apps/web/utils/email/constants.ts (2)

52-71: Ensure special state values are handled correctly.

The OutlookStateMap includes special marker values like "UNREAD_FLAG", "FLAGGED", and "IMPORTANT_FLAG" that aren't actual folder names. While the comments clearly indicate these are flags, ensure that consuming code checks for these special values and handles them differently from regular folder operations. Without this, attempts to use them as folder IDs will fail.

Consider adding a helper function to detect flag-based states:

export function isOutlookFlagState(state: EmailState): boolean {
  return [
    EmailState.UNREAD,
    EmailState.STARRED,
    EmailState.IMPORTANT,
  ].includes(state);
}

77-90: LGTM!

InboxZero folder constants and helper function provide a clean, consistent naming scheme for tracking labels/folders.

apps/web/app/api/clean/route.ts (3)

98-123: Single message fetch is a known limitation for Outlook.

The Outlook implementation currently fetches only a single message per thread, while Gmail fetches all messages in a thread. This could affect the accuracy of clean decisions, especially for conversations. The TODO on line 114 acknowledges this, but consider prioritizing the implementation of full conversation fetching to maintain feature parity with Gmail.

Would you like me to generate code to implement full conversation fetching for Outlook, or should this be tracked separately?


277-352: LGTM with type compatibility note.

The provider-based routing logic correctly directs requests to Gmail or Outlook endpoints with appropriate queue keys. The endpoint selection and logging provide good observability.

Minor note: Ensure CleanGmailBody and CleanOutlookBody have compatible field requirements since they're used in a union type. The optional markedDoneLabelId and processedLabelId in CleanOutlookBody should be compatible with CleanGmailBody.


157-183: Verify Outlook SENT label mapping and remove duplicate function.

Two issues:

  1. Duplicate logic: isSent (line 163) and hasSentMail (line 181) have identical implementations. Consider consolidating them.

  2. Verify Outlook SENT mapping: The comment on line 165 states that SENT labels are mapped during Outlook message parsing, but this mapping isn't visible in the provided code. Ensure that Outlook sent messages actually populate labelIds with GmailLabel.SENT, otherwise the skip-conversation logic will fail for Outlook.

apps/web/utils/actions/clean.ts (3)

188-266: LGTM!

The undo action is properly refactored to be provider-agnostic, using createEmailProvider and provider-specific methods. The error handling appropriately continues even if Redis updates fail, ensuring the email operation succeeds.


268-344: LGTM!

The change-to-done action correctly uses provider-agnostic operations and maintains consistency with the undo action's structure. Error handling ensures email operations succeed even if Redis updates fail.


35-39: cleanInboxAction cannot support Outlook due to unimplemented query parameters.

The Google-only restriction is correct. The labelIds and excludeLabelNames parameters used in cleanInboxAction (lines 117-121) are explicitly marked as "For Google" in the ThreadsQuery schema and are flagged with biome-ignore TODO comments in OutlookProvider, indicating they're not yet implemented for Outlook. Meanwhile, the undo/change actions support Outlook because they use provider-agnostic methods like getLabelByName() and archiveThreadWithLabel().

To fully support Outlook for Deep Clean, implement the labelIds and excludeLabelNames logic in OutlookProvider, or clarify in the PR description that only undo/change operations support Outlook.

Comment on lines +44 to +48
<div className="mt-6">
<Button variant="outline" onClick={onPrevious}>
Back
</Button>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add explicit button type

Match other steps and prevent accidental submits inside forms by setting type="button".

Apply:

-      <div className="mt-6">
-        <Button variant="outline" onClick={onPrevious}>
+      <div className="mt-6">
+        <Button type="button" variant="outline" onClick={onPrevious}>
           Back
         </Button>
       </div>

As per coding guidelines.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="mt-6">
<Button variant="outline" onClick={onPrevious}>
Back
</Button>
</div>
<div className="mt-6">
<Button type="button" variant="outline" onClick={onPrevious}>
Back
</Button>
</div>
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsx around
lines 44 to 48, the Back button lacks an explicit type which can cause
accidental form submission; set the Button's type attribute to "button" (e.g.,
add type="button") so it matches other steps and prevents implicit submit
behavior inside forms.

Comment on lines +124 to +127
<div className="mt-6 flex justify-center gap-2">
<Button size="lg" variant="outline" onClick={onPrevious}>
Back
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent duplicate submissions; gate Gmail‑only settings link; add button type

  • Starting cleaning can be clicked repeatedly; add a loading/disabled state to avoid double job creation.
  • The “Edit settings” link points to a Gmail‑only page; hide or alter for non‑Google providers to avoid an error page.
  • Add type="button" on Back.

Apply:

@@
-export function ConfirmationStep({
+export function ConfirmationStep({
@@
-  const router = useRouter();
+  const router = useRouter();
+  const [isLoading, setIsLoading] = useState(false);
@@
-  const handleStartCleaning = async () => {
-    const result = await cleanInboxAction(emailAccountId, {
+  const handleStartCleaning = async () => {
+    setIsLoading(true);
+    const result = await cleanInboxAction(emailAccountId, {
       daysOld: timeRange ?? 7,
       instructions: instructions || "",
       action: action || CleanAction.ARCHIVE,
       maxEmails: PREVIEW_RUN_COUNT,
       skips,
     });
 
     if (result?.serverError) {
       toastError({ description: result.serverError });
-      return;
+      setIsLoading(false);
+      return;
     }
 
     router.push(
       prefixPath(
         emailAccountId,
         `/clean/run?jobId=${result?.data?.jobId}&isPreviewBatch=true`,
       ),
     );
+    setIsLoading(false);
   };
@@
-      <div className="mt-6 flex justify-center gap-2">
-        <Button size="lg" variant="outline" onClick={onPrevious}>
+      <div className="mt-6 flex justify-center gap-2">
+        <Button type="button" size="lg" variant="outline" onClick={onPrevious}>
           Back
         </Button>
-        <Button size="lg" onClick={handleStartCleaning}>
+        <Button
+          size="lg"
+          onClick={handleStartCleaning}
+          disabled={isLoading}
+          aria-busy={isLoading}
+        >
           Start Cleaning
         </Button>
       </div>
@@
-          <FooterLink
-            icon={SettingsIcon}
-            text="Edit settings"
-            href={prefixPath(emailAccountId, "/clean/onboarding")}
-          />
+          {isGmail && (
+            <FooterLink
+              icon={SettingsIcon}
+              text="Edit settings"
+              href={prefixPath(emailAccountId, "/clean/onboarding")}
+            />
+          )}

Add missing import:

-import Link from "next/link";
+import Link from "next/link";
+import { useState } from "react";

Optionally, consider provider‑specific copy: Gmail uses “All Mail” rather than an “Archive” folder; you may want to keep the badge label constant (“Archive”) for both or switch Gmail text to “All Mail”.

Based on learnings.

Also applies to: 133-147

🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx around lines
124-127 (and similarly 133-147), prevent duplicate submissions by adding a local
loading state (import useState from React) and set loading=true when starting
the cleaning job, disable the primary Start/Confirm button while loading and
show a spinner or loading label, and ensure the Back button has type="button";
additionally gate the “Edit settings” link so it only renders or navigates for
Google/Gmail providers (for others either hide it or render a non-navigating
tooltip/disabled link), and add the missing import for useState (or whatever
hook you use) at the top of the file.

Comment on lines +44 to +46
const runPreview = useCallback(async () => {
setIsLoading(true);
setError(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix type inconsistency.

Line 46 calls setError(null) but the error state is typed as string | undefined (line 28). Use undefined for consistency.

Apply this diff:

   const runPreview = useCallback(async () => {
     setIsLoading(true);
-    setError(null);
+    setError(undefined);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const runPreview = useCallback(async () => {
setIsLoading(true);
setError(null);
const runPreview = useCallback(async () => {
setIsLoading(true);
setError(undefined);
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx around lines 44 to
46, the component calls setError(null) but the error state is typed as string |
undefined (declared around line 28); replace the null call with
setError(undefined) (or update the state type to include null) so the value
matches the declared type and removes the type inconsistency.

Comment on lines +44 to +132
const runPreview = useCallback(async () => {
setIsLoading(true);
setError(null);

const result = await cleanInboxAction(emailAccountId, {
daysOld: timeRange,
instructions: instructions || "",
action,
maxEmails: PREVIEW_RUN_COUNT,
skips: {
reply: skipReply,
starred: skipStarred,
calendar: skipCalendar,
receipt: skipReceipt,
attachment: skipAttachment,
conversation: false,
},
});

if (result?.serverError) {
setError(result.serverError);
toastError({ description: result.serverError });
} else if (result?.data?.jobId) {
setJobId(result.data.jobId);
}

setIsLoading(false);
}, [
emailAccountId,
action,
timeRange,
instructions,
skipReply,
skipStarred,
skipCalendar,
skipReceipt,
skipAttachment,
]);

const handleProcessPreviewOnly = async () => {
setIsLoadingPreview(true);
const result = await cleanInboxAction(emailAccountId, {
daysOld: timeRange,
instructions: instructions || "",
action,
maxEmails: PREVIEW_RUN_COUNT,
skips: {
reply: skipReply,
starred: skipStarred,
calendar: skipCalendar,
receipt: skipReceipt,
attachment: skipAttachment,
conversation: false,
},
});

setIsLoadingPreview(false);

if (result?.serverError) {
toastError({ description: result.serverError });
} else if (result?.data?.jobId) {
setJobId(result.data.jobId);
}
};

const handleRunOnFullInbox = async () => {
setIsLoadingFull(true);
const result = await cleanInboxAction(emailAccountId, {
daysOld: timeRange,
instructions: instructions || "",
action,
skips: {
reply: skipReply,
starred: skipStarred,
calendar: skipCalendar,
receipt: skipReceipt,
attachment: skipAttachment,
conversation: false,
},
});

setIsLoadingFull(false);

if (result?.serverError) {
toastError({ description: result.serverError });
} else if (result?.data?.jobId) {
setJobId(result.data.jobId);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Extract shared payload construction logic.

The payload construction is duplicated across runPreview, handleProcessPreviewOnly, and handleRunOnFullInbox. This violates the DRY principle and makes maintenance more difficult.

Apply this diff to extract a helper function:

+  const buildCleanPayload = useCallback((maxEmails?: number) => ({
+    daysOld: timeRange,
+    instructions: instructions || "",
+    action,
+    ...(maxEmails && { maxEmails }),
+    skips: {
+      reply: skipReply,
+      starred: skipStarred,
+      calendar: skipCalendar,
+      receipt: skipReceipt,
+      attachment: skipAttachment,
+      conversation: false,
+    },
+  }), [
+    timeRange,
+    instructions,
+    action,
+    skipReply,
+    skipStarred,
+    skipCalendar,
+    skipReceipt,
+    skipAttachment,
+  ]);
+
   const runPreview = useCallback(async () => {
     setIsLoading(true);
     setError(undefined);
 
-    const result = await cleanInboxAction(emailAccountId, {
-      daysOld: timeRange,
-      instructions: instructions || "",
-      action,
-      maxEmails: PREVIEW_RUN_COUNT,
-      skips: {
-        reply: skipReply,
-        starred: skipStarred,
-        calendar: skipCalendar,
-        receipt: skipReceipt,
-        attachment: skipAttachment,
-        conversation: false,
-      },
-    });
+    const result = await cleanInboxAction(
+      emailAccountId,
+      buildCleanPayload(PREVIEW_RUN_COUNT)
+    );
 
     if (result?.serverError) {
       setError(result.serverError);
@@ -68,49 +53,16 @@
     }
 
     setIsLoading(false);
-  }, [
-    emailAccountId,
-    action,
-    timeRange,
-    instructions,
-    skipReply,
-    skipStarred,
-    skipCalendar,
-    skipReceipt,
-    skipAttachment,
-  ]);
+  }, [emailAccountId, buildCleanPayload]);
 
   const handleProcessPreviewOnly = async () => {
     setIsLoadingPreview(true);
-    const result = await cleanInboxAction(emailAccountId, {
-      daysOld: timeRange,
-      instructions: instructions || "",
-      action,
-      maxEmails: PREVIEW_RUN_COUNT,
-      skips: {
-        reply: skipReply,
-        starred: skipStarred,
-        calendar: skipCalendar,
-        receipt: skipReceipt,
-        attachment: skipAttachment,
-        conversation: false,
-      },
-    });
+    const result = await cleanInboxAction(
+      emailAccountId,
+      buildCleanPayload(PREVIEW_RUN_COUNT)
+    );
 
     setIsLoadingPreview(false);
 
@@ -123,22 +75,11 @@
 
   const handleRunOnFullInbox = async () => {
     setIsLoadingFull(true);
-    const result = await cleanInboxAction(emailAccountId, {
-      daysOld: timeRange,
-      instructions: instructions || "",
-      action,
-      skips: {
-        reply: skipReply,
-        starred: skipStarred,
-        calendar: skipCalendar,
-        receipt: skipReceipt,
-        attachment: skipAttachment,
-        conversation: false,
-      },
-    });
+    const result = await cleanInboxAction(
+      emailAccountId,
+      buildCleanPayload()
+    );
 
     setIsLoadingFull(false);
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsx around lines 44 to
132, the payload build is duplicated in runPreview, handleProcessPreviewOnly,
and handleRunOnFullInbox; extract a small helper (e.g., buildCleanPayload) that
takes an options flag for preview to return the common object with daysOld,
instructions (fallback ""), action, skips (reply, starred, calendar, receipt,
attachment, conversation: false) and conditionally includes maxEmails:
PREVIEW_RUN_COUNT for preview flows; replace the three inline payload objects
with calls to this helper, update runPreview and handleProcessPreviewOnly to
pass preview=true (so maxEmails is included) and handleRunOnFullInbox to pass
preview=false, and adjust any useCallback dependency lists if the helper uses
local vars.

Comment on lines +35 to +39
<div className="mt-6">
<Button variant="outline" onClick={onPrevious}>
Back
</Button>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add explicit button type for accessibility and predictability

Include type="button" to avoid implicit submit behavior and align with guidelines.

Apply:

-      <div className="mt-6">
-        <Button variant="outline" onClick={onPrevious}>
+      <div className="mt-6">
+        <Button type="button" variant="outline" onClick={onPrevious}>
           Back
         </Button>
       </div>

As per coding guidelines.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div className="mt-6">
<Button variant="outline" onClick={onPrevious}>
Back
</Button>
</div>
<div className="mt-6">
<Button type="button" variant="outline" onClick={onPrevious}>
Back
</Button>
</div>
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx around lines 35
to 39, the Back Button is missing an explicit type which can cause it to act as
a form submit button; add type="button" to the Button element (e.g., <Button
type="button" ...>) so its behavior is explicit and accessible per guidelines.

Comment on lines +17 to +19
const onPrevious = useCallback(() => {
setStep(step - 1);
}, [step, setStep]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clamp backward navigation and prevent underflow

Going back from the first step can produce invalid step values. Clamp to the intro step.

Apply:

-  const onPrevious = useCallback(() => {
-    setStep(step - 1);
-  }, [step, setStep]);
+  const onPrevious = useCallback(() => {
+    setStep(Math.max(step - 1, CleanStep.INTRO));
+  }, [step, setStep]);

Optional: if useQueryState supports functional updaters, prefer:

-  const onNext = useCallback(() => {
-    setStep(step + 1);
-  }, [step, setStep]);
+  const onNext = useCallback(() => {
+    setStep((s) => s + 1);
+  }, [setStep]);

-  const onPrevious = useCallback(() => {
-    setStep(Math.max(step - 1, CleanStep.INTRO));
-  }, [step, setStep]);
+  const onPrevious = useCallback(() => {
+    setStep((s) => Math.max(s - 1, CleanStep.INTRO));
+  }, [setStep]);
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/clean/useStep.tsx around lines 17 to 19,
the onPrevious handler allows the step to underflow below the intro step; change
it to clamp the new step to the intro minimum (e.g. Math.max(step - 1,
INTRO_STEP_INDEX)) so it never goes below the first valid step. If the state
setter returned by useQueryState supports functional updaters, use a functional
update form to compute prev => Math.max(prev - 1, INTRO_STEP_INDEX) to avoid
stale closures and remove step from the dependency array.

Comment on lines +87 to +94
if (shouldMarkAsRead) {
await markMessageAsRead({
client: outlook,
messageId,
read: true,
});
logger.info("Marked message as read", { messageId });
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix parameter name mismatch.

Line 91 passes read: true, but the markMessageAsRead function signature (line 188-196) expects isRead as the parameter name.

Apply this diff:

     if (shouldMarkAsRead) {
       await markMessageAsRead({
         client: outlook,
         messageId,
-        read: true,
+        isRead: true,
       });
       logger.info("Marked message as read", { messageId });
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (shouldMarkAsRead) {
await markMessageAsRead({
client: outlook,
messageId,
read: true,
});
logger.info("Marked message as read", { messageId });
}
if (shouldMarkAsRead) {
await markMessageAsRead({
client: outlook,
messageId,
isRead: true,
});
logger.info("Marked message as read", { messageId });
}
🤖 Prompt for AI Agents
In apps/web/app/api/clean/outlook/route.ts around lines 87 to 94, the call to
markMessageAsRead uses the wrong parameter name (read: true) whereas the
function signature expects isRead; update the call to pass isRead: true (and
remove or rename any redundant read property) so the argument matches the
function signature, and run a quick type-check to ensure no other call sites are
affected.

Comment on lines +78 to +82
// Premium check disabled for development/testing
// TODO: Re-enable for production
// const premium = await getUserPremium({ userId: emailAccount.userId });
// if (!premium) throw new SafeError("User not premium");
// if (!isActivePremium(premium)) throw new SafeError("Premium not active");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Re-enable premium check or add deployment safeguard.

The premium check is disabled, which could allow non-premium users to access this paid feature if deployed to production. The TODO comment isn't sufficient to prevent accidental deployment.

Options to address this:

  1. Remove the comment and re-enable the check immediately:
-  // Premium check disabled for development/testing
-  // TODO: Re-enable for production
-  // const premium = await getUserPremium({ userId: emailAccount.userId });
-  // if (!premium) throw new SafeError("User not premium");
-  // if (!isActivePremium(premium)) throw new SafeError("Premium not active");
+  const premium = await getUserPremium({ userId: emailAccount.userId });
+  if (!premium) throw new SafeError("User not premium");
+  if (!isActivePremium(premium)) throw new SafeError("Premium not active");
  1. Add an environment-based check:
if (env.NODE_ENV === "production") {
  const premium = await getUserPremium({ userId: emailAccount.userId });
  if (!premium) throw new SafeError("User not premium");
  if (!isActivePremium(premium)) throw new SafeError("Premium not active");
}
  1. Create a feature flag:
if (env.ENFORCE_PREMIUM_CHECK !== "false") {
  const premium = await getUserPremium({ userId: emailAccount.userId });
  if (!premium) throw new SafeError("User not premium");
  if (!isActivePremium(premium)) throw new SafeError("Premium not active");
}
🤖 Prompt for AI Agents
In apps/web/app/api/clean/route.ts around lines 78-82 the premium check is
commented out which risks exposing a paid feature in production; re-enable the
premium validation but gate it by deployment configuration: either (a) re-enable
unconditionally for production by wrapping the existing getUserPremium +
isActivePremium checks inside a NODE_ENV === "production" conditional, or (b)
implement a feature flag (e.g., env.ENFORCE_PREMIUM_CHECK !== "false") around
those same checks; ensure you import/read the env variable, call getUserPremium
with the emailAccount.userId, and throw SafeError when missing or inactive so
non-premium users are blocked, with the default behavior enforcing the check in
production.

Comment on lines +62 to 63
isFlagged?: boolean; // Outlook: indicates message is flagged/starred
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Standardize isFlagged semantics and usage

Optional boolean is fine, but tri-state (true/false/undefined) can leak into UI. Recommend documenting cross‑provider meaning and normalizing to a concrete boolean in mappers (default false) to simplify consumers. Also audit reads to use message.isFlagged ?? false.

Run to find usages expecting truthy/undefined:


🏁 Script executed:

#!/bin/bash
rg -n --type=ts --type=tsx -C2 '\bisFlagged\b'

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Search for isFlagged usages with corrected approach
rg -n -C2 '\bisFlagged\b' apps/web/ --glob='*.ts' --glob='*.tsx'

Length of output: 1375


🏁 Script executed:

#!/bin/bash
# Search for where isFlagged is assigned or set (mappers, parsers)
rg -n 'isFlagged\s*[:=]' apps/web/ --glob='*.ts' --glob='*.tsx' -B2 -A2

Length of output: 291


🏁 Script executed:

#!/bin/bash
# Search for Outlook message parsing/mapping logic
rg -n 'flag|isFlagged' apps/web/utils/outlook/ --glob='*.ts' -B3 -A3

Length of output: 1614


🏁 Script executed:

#!/bin/bash
# Search for message parsing/fetching in outlook utils and app directory
rg -n 'flagStatus|ParsedMessage|mapMessage' apps/web/utils/outlook/ apps/web/app/ --glob='*.ts' --glob='*.tsx' -B2 -A2 | head -100

Length of output: 6353


🏁 Script executed:

#!/bin/bash
# View parseMessage function implementation
sed -n '471,550p' apps/web/utils/outlook/message.ts

Length of output: 789


🏁 Script executed:

#!/bin/bash
# View full parseMessage function including signature
sed -n '469,530p' apps/web/utils/outlook/message.ts

Length of output: 851


Update line 160 in apps/web/app/api/clean/route.ts to explicitly normalize isFlagged

The review concern is valid. The Outlook message parser never sets isFlagged, leaving it undefined. Line 160 currently reads message.labelIds?.includes(GmailLabel.STARRED) || message.isFlagged, which implicitly treats undefined as falsy. Update to message.labelIds?.includes(GmailLabel.STARRED) || (message.isFlagged ?? false) for clarity and to prevent tri-state semantics from leaking into business logic.

🤖 Prompt for AI Agents
In apps/web/utils/types.ts around lines 62 to 63, the isFlagged field can be
undefined and downstream code (specifically line 160 in
apps/web/app/api/clean/route.ts) currently mixes tri-state semantics into
business logic; update the usage to explicitly normalize the value by replacing
the expression message.labelIds?.includes(GmailLabel.STARRED) ||
message.isFlagged with message.labelIds?.includes(GmailLabel.STARRED) ||
(message.isFlagged ?? false) so isFlagged is coerced to a boolean and undefined
no longer propagates.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 16 files

Prompt for AI agents (all 2 issues)

Understand the root cause of the following 2 issues and fix them.


<file name="apps/web/app/api/clean/route.ts">

<violation number="1" location="apps/web/app/api/clean/route.ts:78">
Re‑enable premium validation (or guard by environment/feature flag) to prevent non‑premium users accessing this paid feature in production.</violation>
</file>

<file name="apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx">

<violation number="1" location="apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx:43">
`emailAccount?.provider` is always undefined because the provider lives under `emailAccount.account.provider`, so Gmail accounts will fall through to the Outlook messaging. Use the nested provider value (or the provider field returned by `useAccount`) when calling `isGoogleProvider`.</violation>
</file>

React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai to give feedback, ask questions, or re-run the review.

const premium = await getUserPremium({ userId: emailAccount.userId });
if (!premium) throw new SafeError("User not premium");
if (!isActivePremium(premium)) throw new SafeError("Premium not active");
// Premium check disabled for development/testing
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re‑enable premium validation (or guard by environment/feature flag) to prevent non‑premium users accessing this paid feature in production.

Prompt for AI agents
Address the following comment on apps/web/app/api/clean/route.ts at line 78:

<comment>Re‑enable premium validation (or guard by environment/feature flag) to prevent non‑premium users accessing this paid feature in production.</comment>

<file context>
@@ -67,22 +71,56 @@ async function cleanThread({
-  const premium = await getUserPremium({ userId: emailAccount.userId });
-  if (!premium) throw new SafeError(&quot;User not premium&quot;);
-  if (!isActivePremium(premium)) throw new SafeError(&quot;Premium not active&quot;);
+  // Premium check disabled for development/testing
+  // TODO: Re-enable for production
+  // const premium = await getUserPremium({ userId: emailAccount.userId });
</file context>
Fix with Cubic

const { emailAccountId } = useAccount();
const { emailAccountId, emailAccount } = useAccount();
const { onPrevious } = useStep();
const isGmail = isGoogleProvider(emailAccount?.provider);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emailAccount?.provider is always undefined because the provider lives under emailAccount.account.provider, so Gmail accounts will fall through to the Outlook messaging. Use the nested provider value (or the provider field returned by useAccount) when calling isGoogleProvider.

Prompt for AI agents
Address the following comment on apps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsx at line 43:

<comment>`emailAccount?.provider` is always undefined because the provider lives under `emailAccount.account.provider`, so Gmail accounts will fall through to the Outlook messaging. Use the nested provider value (or the provider field returned by `useAccount`) when calling `isGoogleProvider`.</comment>

<file context>
@@ -36,7 +38,9 @@ export function ConfirmationStep({
-  const { emailAccountId } = useAccount();
+  const { emailAccountId, emailAccount } = useAccount();
+  const { onPrevious } = useStep();
+  const isGmail = isGoogleProvider(emailAccount?.provider);
 
   const handleStartCleaning = async () =&gt; {
</file context>
Suggested change
const isGmail = isGoogleProvider(emailAccount?.provider);
const isGmail = isGoogleProvider(emailAccount?.account?.provider);
Fix with Cubic

@elie222
Copy link
Owner

elie222 commented Oct 29, 2025

Thanks for submitting! Did you test this out yourself?
Would love to know if it's working well for you

@salja03-t21
Copy link
Author

Yes, I'm currently using it in development- so far so good. I'll submit a second update that fixes the code rabbit identified issues

@salja03-t21 salja03-t21 closed this Nov 3, 2025
@salja03-t21 salja03-t21 deleted the outlook-deep-clean-pr branch November 3, 2025 01:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants