feat: Add Outlook support to Deep Clean feature#875
feat: Add Outlook support to Deep Clean feature#875salja03-t21 wants to merge 4 commits intoelie222:mainfrom
Conversation
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
|
@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. |
WalkthroughMulti-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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Areas requiring extra attention:
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 addscoverage(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
isGoogleProviderimport 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
providervariable is included in the dependency array but is no longer used in thenavItemscomputation. 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
useEffectdepends onrunPreview, 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 auseRefflag 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 adopteduseStepwith anonPreviouscallback. 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
ARCHIVEentry 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
📒 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.tsapps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/api/clean/outlook/route.tsapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/utils/outlook/folders.tsapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/web/utils/actions/clean.tsapps/web/components/ui/tooltip.tsxapps/web/utils/email/constants.tsapps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsxapps/web/components/SideNav.tsxapps/web/app/api/clean/route.tsapps/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.tsapps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/api/clean/outlook/route.tsapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/utils/outlook/folders.tsapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/web/utils/actions/clean.tsapps/web/components/ui/tooltip.tsxapps/web/utils/email/constants.ts.gitignoreapps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsxapps/web/components/SideNav.tsxapps/web/app/api/clean/route.tsapps/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.tsapps/web/app/api/clean/outlook/route.tsapps/web/utils/outlook/folders.tsapps/web/utils/actions/clean.tsapps/web/utils/email/constants.tsapps/web/app/api/clean/route.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/logging.mdc)
**/*.{ts,tsx}: UsecreateScopedLoggerfor logging in backend TypeScript files
Typically add the logger initialization at the top of the file when usingcreateScopedLogger
Only use.with()on a logger instance within a specific function, not for a global loggerImport 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.tsapps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/api/clean/outlook/route.tsapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/utils/outlook/folders.tsapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/web/utils/actions/clean.tsapps/web/components/ui/tooltip.tsxapps/web/utils/email/constants.tsapps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsxapps/web/components/SideNav.tsxapps/web/app/api/clean/route.tsapps/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.tsapps/web/utils/outlook/folders.tsapps/web/utils/actions/clean.tsapps/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.tsapps/web/utils/outlook/folders.tsapps/web/utils/actions/clean.tsapps/web/utils/email/constants.ts
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,jsx,ts,tsx}: Don't useelements 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.tsapps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/api/clean/outlook/route.tsapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/utils/outlook/folders.tsapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/web/utils/actions/clean.tsapps/web/components/ui/tooltip.tsxapps/web/utils/email/constants.tsapps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsxapps/web/components/SideNav.tsxapps/web/app/api/clean/route.tsapps/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.tsapps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/api/clean/outlook/route.tsapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/utils/outlook/folders.tsapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/web/utils/actions/clean.tsapps/web/components/ui/tooltip.tsxapps/web/utils/email/constants.ts.gitignoreapps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsxapps/web/components/SideNav.tsxapps/web/app/api/clean/route.tsapps/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.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/api/clean/outlook/route.tsapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsxapps/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
Useresult?.serverErrorwithtoastErrorandtoastSuccess
UseLoadingContentcomponent to handle loading and error states consistently
Passloading,error, and children props toLoadingContent
Files:
apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/web/components/ui/tooltip.tsxapps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsxapps/web/components/SideNav.tsxapps/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.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/web/components/ui/tooltip.tsxapps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsxapps/web/components/SideNav.tsxapps/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.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/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.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/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.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsx
apps/web/app/**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
Components with
onClickmust be client components withuse clientdirective
Files:
apps/web/app/(app)/[emailAccountId]/clean/CleanInstructionsStep.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/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.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/web/components/ui/tooltip.tsxapps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsxapps/web/components/SideNav.tsxapps/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.tsxapps/web/app/(app)/[emailAccountId]/clean/useStep.tsxapps/web/app/(app)/[emailAccountId]/clean/PreviewStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ActionSelectionStep.tsxapps/web/app/(app)/[emailAccountId]/clean/ConfirmationStep.tsxapps/web/components/ui/tooltip.tsxapps/web/app/(app)/[emailAccountId]/clean/TimeRangeStep.tsxapps/web/components/SideNav.tsxapps/web/components/ui/dropdown-menu.tsxapps/web/app/api/**/route.ts
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
apps/web/app/api/**/route.ts: UsewithAuthfor user-level operations
UsewithEmailAccountfor 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 withwithAuthorwithEmailAccountmiddleware 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.tsapps/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).
UsewithEmailAccountmiddleware for API routes that operate on a specific email account (i.e., use or requireemailAccountId).
UsewithAuthmiddleware for API routes that operate at the user level (i.e., use or require onlyuserId).
UsewithErrormiddleware (with proper validation) for public endpoints, custom authentication, or cron endpoints.
Cron endpoints MUST usewithErrormiddleware and validate the cron secret usinghasCronSecret(request)orhasPostCronSecret(request).
Cron endpoints MUST capture unauthorized attempts withcaptureExceptionand 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.
AllfindUniqueandfindFirstPrisma calls in API routes MUST include ownership filters (e.g., userId or emailAccountId).
AllfindManyPrisma 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.tsapps/web/app/api/clean/route.tsapps/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.tsapps/web/app/api/clean/route.tsapps/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-actionprovides centralized error handling
Use Zod schemas for validation on both client and server
UserevalidatePathin server actions for cache invalidation
apps/web/utils/actions/**/*.ts: Use server actions (withnext-safe-action) for all mutations (create/update/delete operations); do NOT use POST API routes for mutations.
UserevalidatePathin server actions to invalidate cache after mutations.Files:
apps/web/utils/actions/clean.tsapps/web/utils/actions/*.ts
📄 CodeRabbit inference engine (.cursor/rules/server-actions.mdc)
apps/web/utils/actions/*.ts: Implement all server actions using thenext-safe-actionlibrary for type safety, input validation, context management, and error handling. Refer toapps/web/utils/actions/safe-action.tsfor client definitions (actionClient,actionClientUser,adminActionClient).
UseactionClientUserwhen only authenticated user context (userId) is needed.
UseactionClientwhen both authenticated user context and a specificemailAccountIdare needed. TheemailAccountIdmust be bound when calling the action from the client.
UseadminActionClientfor actions restricted to admin users.
Access necessary context (likeuserId,emailAccountId, etc.) provided by the safe action client via thectxobject 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.
UseSafeErrorfor expected/handled errors within actions if needed.next-safe-actionprovides centralized error handling.
Use the.metadata({ name: "actionName" })method to provide a meaningful name for monitoring. Sentry instrumentation is automatically applied viawithServerActionInstrumentationwithin the safe action clients.
If an action modifies data displayed elsewhere, userevalidatePathorrevalidateTagfromnext/cachewithin the action handler as needed.Server action files must start with
use serverFiles:
apps/web/utils/actions/clean.tsapps/web/components/**/*.tsx
📄 CodeRabbit inference engine (apps/web/CLAUDE.md)
Use React Hook Form with Zod validation for form handling
Use the
LoadingContentcomponent 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.tsxapps/web/components/SideNav.tsxapps/web/components/ui/dropdown-menu.tsxapps/web/components/ui/**
📄 CodeRabbit inference engine (.cursor/rules/project-structure.mdc)
Shadcn components are in
components/uiFiles:
apps/web/components/ui/tooltip.tsxapps/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.tsapps/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 ofsuppressHydrationWarning.The forwardRef implementation looks correct and properly enables ref forwarding. However, the
suppressHydrationWarningattribute 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 ofsuppressHydrationWarning.The forwardRef implementation correctly enables ref forwarding with proper TypeScript typing. However, the
suppressHydrationWarningattribute 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 OutlookBack 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
verifySignatureAppRouterandwithErrormiddleware 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
CleanGmailBodyandCleanOutlookBodyhave compatible field requirements since they're used in a union type. The optionalmarkedDoneLabelIdandprocessedLabelIdinCleanOutlookBodyshould be compatible withCleanGmailBody.
157-183: Verify Outlook SENT label mapping and remove duplicate function.Two issues:
Duplicate logic:
isSent(line 163) andhasSentMail(line 181) have identical implementations. Consider consolidating them.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
labelIdswithGmailLabel.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
createEmailProviderand 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
labelIdsandexcludeLabelNamesparameters used incleanInboxAction(lines 117-121) are explicitly marked as "For Google" in the ThreadsQuery schema and are flagged withbiome-ignoreTODO comments in OutlookProvider, indicating they're not yet implemented for Outlook. Meanwhile, the undo/change actions support Outlook because they use provider-agnostic methods likegetLabelByName()andarchiveThreadWithLabel().To fully support Outlook for Deep Clean, implement the
labelIdsandexcludeLabelNameslogic in OutlookProvider, or clarify in the PR description that only undo/change operations support Outlook.
| <div className="mt-6"> | ||
| <Button variant="outline" onClick={onPrevious}> | ||
| Back | ||
| </Button> | ||
| </div> |
There was a problem hiding this comment.
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.
| <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.
| <div className="mt-6 flex justify-center gap-2"> | ||
| <Button size="lg" variant="outline" onClick={onPrevious}> | ||
| Back | ||
| </Button> |
There was a problem hiding this comment.
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.
| const runPreview = useCallback(async () => { | ||
| setIsLoading(true); | ||
| setError(null); |
There was a problem hiding this comment.
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.
| 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.
| 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); | ||
| } | ||
| }; |
There was a problem hiding this comment.
🛠️ 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.
| <div className="mt-6"> | ||
| <Button variant="outline" onClick={onPrevious}> | ||
| Back | ||
| </Button> | ||
| </div> |
There was a problem hiding this comment.
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.
| <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.
| const onPrevious = useCallback(() => { | ||
| setStep(step - 1); | ||
| }, [step, setStep]); |
There was a problem hiding this comment.
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.
| if (shouldMarkAsRead) { | ||
| await markMessageAsRead({ | ||
| client: outlook, | ||
| messageId, | ||
| read: true, | ||
| }); | ||
| logger.info("Marked message as read", { messageId }); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| // 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"); |
There was a problem hiding this comment.
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:
- 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");- 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");
}- 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.
| isFlagged?: boolean; // Outlook: indicates message is flagged/starred | ||
| } |
There was a problem hiding this comment.
🧩 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 -A2Length 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 -A3Length 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 -100Length of output: 6353
🏁 Script executed:
#!/bin/bash
# View parseMessage function implementation
sed -n '471,550p' apps/web/utils/outlook/message.tsLength of output: 789
🏁 Script executed:
#!/bin/bash
# View full parseMessage function including signature
sed -n '469,530p' apps/web/utils/outlook/message.tsLength 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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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("User not premium");
- if (!isActivePremium(premium)) throw new SafeError("Premium not active");
+ // Premium check disabled for development/testing
+ // TODO: Re-enable for production
+ // const premium = await getUserPremium({ userId: emailAccount.userId });
</file context>
| const { emailAccountId } = useAccount(); | ||
| const { emailAccountId, emailAccount } = useAccount(); | ||
| const { onPrevious } = useStep(); | ||
| const isGmail = isGoogleProvider(emailAccount?.provider); |
There was a problem hiding this 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.
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 () => {
</file context>
| const isGmail = isGoogleProvider(emailAccount?.provider); | |
| const isGmail = isGoogleProvider(emailAccount?.account?.provider); |
|
Thanks for submitting! Did you test this out yourself? |
|
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 |
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
Technical Details
Testing
Tested with both Gmail and Outlook accounts to ensure:
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.tsxapps/web/components/SideNav.tsxAll changes maintain backward compatibility with existing Gmail functionality while extending support to Outlook users.
Summary by CodeRabbit
Release Notes