Skip to content

Comments

Outlook threads API#511

Closed
WhiteSoxx wants to merge 2 commits intoelie222:mainfrom
Francisca105:feat/outlook-mail
Closed

Outlook threads API#511
WhiteSoxx wants to merge 2 commits intoelie222:mainfrom
Francisca105:feat/outlook-mail

Conversation

@WhiteSoxx
Copy link

@WhiteSoxx WhiteSoxx commented Jun 18, 2025

This PR adapts the API to allow users to grab threads from an Outlook account by using the /api/outlook/threads?emailAccount= endpoint.

Notes:

Related Issue:

Summary by CodeRabbit

  • New Features

    • Added support for Microsoft Entra ID (Outlook) sign-in and authentication alongside Google.
    • Enabled viewing and fetching Outlook email threads in the Mail component.
    • Introduced utilities for Gmail and Outlook integration, including email sending, label management, drafts, filters, permissions, attachments, and thread/message operations.
    • Added batch operations and utility functions for Microsoft Graph and Gmail APIs.
    • Provided new API endpoints for Outlook email threads.
  • Bug Fixes

    • Disabled an onboarding redirect to prevent excessive redirects for users without email rules.
  • Documentation

    • Updated documentation to include setup instructions for Microsoft OAuth credentials and improved formatting for Google OAuth setup.
    • Updated environment variable examples for Microsoft authentication.
  • Tests

    • Added comprehensive unit tests for new email utilities, including reply formatting, signature extraction, and snippet processing.
  • Chores

    • Updated development scripts and added necessary dependencies for Microsoft Graph API integration.

WhiteSoxx added 2 commits June 6, 2025 16:16
* add Microsoft Graph

* abstract API and add Outlook REST support

* lay threads endpoint groundwork

* add provider-based permission checking

* add Outlook utils and fix README

* some mail format tests based on gmail
@vercel
Copy link

vercel bot commented Jun 18, 2025

@WhiteSoxx is attempting to deploy a commit to the Inbox Zero Team on Vercel.

A member of the Team first needs to authorize it.

@jit-ci
Copy link

jit-ci bot commented Jun 18, 2025

Hi, I’m Jit, a friendly security platform designed to help developers build secure applications from day zero with an MVS (Minimal viable security) mindset.

In case there are security findings, they will be communicated to you as a comment inside the PR.

Hope you’ll enjoy using Jit.

Questions? Comments? Want to learn more? Get in touch with us.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jun 18, 2025

Walkthrough

This update introduces comprehensive support for Microsoft Outlook and Microsoft Entra ID (Azure OAuth) throughout the application. It adds new authentication flows, environment variable configurations, API endpoints, and utility modules for handling Outlook and Gmail operations. The README and environment files are updated to guide users through Azure OAuth setup. Numerous utility modules are added for Gmail and Outlook integration, including message, thread, label, filter, and permission management, as well as email formatting and batch operations.

Changes

File(s) / Path(s) Change Summary
README.md, apps/web/.env.example, apps/web/env.ts, apps/web/package.json Added Azure OAuth setup instructions, new Microsoft auth environment variables, and Microsoft Graph client dependency.
apps/web/app/(app)/[emailAccountId]/mail/page.tsx, apps/web/app/(app)/[emailAccountId]/assistant/page.tsx Extended mail view to support Outlook provider; disabled onboarding redirect due to excessive redirects.
apps/web/app/(landing)/login/LoginForm.tsx Added Microsoft Entra ID sign-in option with dialog and provider-specific logic.
apps/web/app/api/outlook/threads/controller.ts, apps/web/app/api/outlook/threads/route.ts, apps/web/app/api/outlook/threads/validation.ts Introduced Outlook threads API endpoint, controller, and query validation schema.
apps/web/utils/account.ts, apps/web/utils/auth.ts, apps/web/utils/middleware.ts Extended authentication, token management, and middleware to support Microsoft providers and Outlook clients.
apps/web/utils/outlook/attachment.ts, apps/web/utils/outlook/batch.ts, apps/web/utils/outlook/client.ts, apps/web/utils/outlook/contact.ts, apps/web/utils/outlook/constants.ts, apps/web/utils/outlook/decode.ts, apps/web/utils/outlook/draft.ts, apps/web/utils/outlook/filter.ts, apps/web/utils/outlook/forward.ts, apps/web/utils/outlook/history.ts, apps/web/utils/outlook/label.ts, apps/web/utils/outlook/mail.ts, apps/web/utils/outlook/message.ts, apps/web/utils/outlook/permissions.ts, apps/web/utils/outlook/reply.ts, apps/web/utils/outlook/retry.ts, apps/web/utils/outlook/scopes.ts, apps/web/utils/outlook/settings.ts, apps/web/utils/outlook/signature.ts, apps/web/utils/outlook/snippet.ts, apps/web/utils/outlook/spam.ts, apps/web/utils/outlook/thread.ts, apps/web/utils/outlook/trash.ts, apps/web/utils/outlook/watch.ts Added new utility modules for Gmail and Outlook integration: handling attachments, batch requests, clients, contacts, constants, decoding, drafts, filters, forwarding, history, labels, mail sending, message fetching, permissions, reply formatting, retries, scopes, settings, signature extraction, snippet processing, spam marking, thread management, trash, and watch/unwatch notifications.
apps/web/utils/outlook/forward.test.ts, apps/web/utils/outlook/reply.test.ts, apps/web/utils/outlook/signature.test.ts, apps/web/utils/outlook/snippet.test.ts Added test suites for forwarding, reply formatting, signature extraction, and snippet processing utilities.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant WebApp
    participant AuthProvider (Google/Microsoft)
    participant API
    participant Outlook/Gmail API

    User->>WebApp: Clicks "Sign in with Google" or "Sign in with Microsoft"
    WebApp->>AuthProvider: Initiates OAuth flow (provider-specific)
    AuthProvider-->>WebApp: Returns OAuth token(s)
    WebApp->>API: Stores tokens, links account (provider-aware)
    User->>WebApp: Navigates to mail page
    WebApp->>API: Requests threads (provider-aware)
    API->>Outlook/Gmail API: Fetches threads via appropriate client
    Outlook/Gmail API-->>API: Returns threads
    API-->>WebApp: Returns threads (provider-specific format)
    WebApp-->>User: Displays threads
Loading

Possibly related PRs

  • elie222/inbox-zero#414: Reorganizes and elaborates Google OAuth setup instructions in the README, related to the current PR's README changes but does not add Azure-specific content.

Poem

A hop, a skip, a leap so wide,
Now Outlook joins the Gmail side!
Azure clouds and tokens bright,
New threads, replies, and filters light.
With tests and docs, the code’s in tune—
Inbox Zero’s growing soon!
🐇✨📧

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/web/app/(landing)/login/LoginForm.tsx

Oops! Something went wrong! :(

ESLint: 9.24.0

ESLint couldn't find an eslint.config.(js|mjs|cjs) file.

From ESLint v9.0.0, the default configuration file is now eslint.config.js.
If you are using a .eslintrc.* file, please follow the migration guide
to update your configuration file to the new format:

https://eslint.org/docs/latest/use/configure/migration-guide

If you still have problems after following the migration guide, please stop by
https://eslint.org/chat/help to chat with the team.

apps/web/app/(app)/[emailAccountId]/assistant/page.tsx

Oops! Something went wrong! :(

ESLint: 9.24.0

ESLint couldn't find an eslint.config.(js|mjs|cjs) file.

From ESLint v9.0.0, the default configuration file is now eslint.config.js.
If you are using a .eslintrc.* file, please follow the migration guide
to update your configuration file to the new format:

https://eslint.org/docs/latest/use/configure/migration-guide

If you still have problems after following the migration guide, please stop by
https://eslint.org/chat/help to chat with the team.

apps/web/app/(app)/[emailAccountId]/mail/page.tsx

Oops! Something went wrong! :(

ESLint: 9.24.0

ESLint couldn't find an eslint.config.(js|mjs|cjs) file.

From ESLint v9.0.0, the default configuration file is now eslint.config.js.
If you are using a .eslintrc.* file, please follow the migration guide
to update your configuration file to the new format:

https://eslint.org/docs/latest/use/configure/migration-guide

If you still have problems after following the migration guide, please stop by
https://eslint.org/chat/help to chat with the team.

  • 35 others
✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 26

🔭 Outside diff range comments (1)
apps/web/app/(app)/[emailAccountId]/assistant/page.tsx (1)

13-19: Incorrect typing & unnecessary await on params

params is delivered synchronously by Next.js; treating it as a Promise breaks type-safety and adds needless micro-tasks.

-export default async function AssistantPage({
-  params,
-}: {
-  params: Promise<{ emailAccountId: string }>;
-}) {
-  const { emailAccountId } = await params;
+export default async function AssistantPage({
+  params,
+}: {
+  params: { emailAccountId: string };
+}) {
+  const { emailAccountId } = params;

Similarly, cookies() is synchronous:

-  const cookieStore = await cookies();
+  const cookieStore = cookies();
🧹 Nitpick comments (45)
apps/web/utils/outlook/scopes.ts (1)

3-15: Avoid redundant scopes & mark the list as immutable

"Mail.ReadWrite" already implies "Mail.Read", so requesting both is redundant noise in the consent screen.
While harmless, it unnecessarily broadens what users must approve.
Also, adding as const or an explicit readonly annotation prevents accidental mutation elsewhere.

-export const SCOPES = [
+export const SCOPES = [
   "openid",
@@
-  "Mail.ReadWrite",
-  "Mail.Send",
-  "Mail.ReadBasic",
-  "Mail.Read",
+  "Mail.ReadWrite", // already includes Mail.Read
+  "Mail.Send",
+  "Mail.ReadBasic",
   "MailboxSettings.ReadWrite",
   ...(env.NEXT_PUBLIC_CONTACTS_ENABLED ? ["Contacts.ReadWrite"] : []),
-];
+] as const;
apps/web/package.json (1)

6-6: --turbo + 16 GB heap may back-fire on everyday dev machines

The combo of --turbo (still experimental) and NODE_OPTIONS=--max_old_space_size=16384 forces a 16 GB heap even for tiny projects, slowing startup and blowing up laptops.

Consider:

-"dev": "cross-env NODE_OPTIONS=--max_old_space_size=16384 next dev --turbo",
+"dev": "cross-env NODE_OPTIONS=--max_old_space_size=4096 next dev --turbo",

or drop the flag entirely until the team is confident it’s stable.

apps/web/utils/outlook/contact.ts (1)

1-20: Minor filter logic & naming mismatch

  1. The utility lives under outlook/ but uses Google People API – consider relocating to google/ for clarity.
  2. filter((c) => c.person?.emailAddresses?.[0]) reads the first element instead of checking .length, which is clearer.
-  const contacts =
-    res.data.results?.filter((c) => c.person?.emailAddresses?.[0]) || [];
+  const contacts =
+    res.data.results?.filter(
+      (c) => (c.person?.emailAddresses?.length ?? 0) > 0,
+    ) || [];
apps/web/env.ts (1)

15-17: Ensure non-empty MICROSOFT_ISSUER as well

MICROSOFT_ISSUER is declared with z.string().default("common"), but unlike the client-id/secret it is not constrained with .min(1).
If the variable is present in the environment but empty (MICROSOFT_ISSUER="") validation will silently accept the empty string and override the default, which is probably unintended.

-    MICROSOFT_ISSUER: z.string().default("common"),
+    MICROSOFT_ISSUER: z.string().min(1).default("common"),
apps/web/utils/middleware.ts (1)

155-161: Minor: use the cloned request (authReq) for header look-ups

After authMiddleware we already have authReq (a clone). Reading headers from the original req instead of authReq has no functional impact today, but it is easy to drift if another middleware mutates headers in the future. Prefer staying on the same object for clarity.

apps/web/utils/outlook/spam.ts (1)

1-17: Namespace mismatch similar to history helper

markSpam is Gmail-only but sits in utils/outlook/. Aligning file structure with provider avoids future mix-ups.

apps/web/utils/outlook/attachment.ts (1)

1-7: Directory / naming mismatch – Gmail helper lives under outlook path

The helper is Gmail-specific yet sits in utils/outlook. This will be confusing once the Outlook attachment helper lands.
Move it to utils/gmail/attachment.ts (or a provider-agnostic folder) and keep the Outlook equivalent beside it.

README.md (3)

156-160: Minor wording – “Copy and paste”

Copy pasteCopy and paste for clarity.

-   3. Copy paste the below into the `Manually add scopes` box:
+   3. Copy and paste the text below into the `Manually add scopes` box:

166-170: Duplicate numbered step

Two consecutive steps are labelled 5. causing Markdown renderers to restart the list.
Renumber the second occurrence to 6. (or rely on 1. automatic numbering).


175-175: Heading trailing colon violates MD026

Remove the trailing : to appease markdown-lint and keep headings consistent.

-### Updating .env file with Azure credentials:
+### Updating .env file with Azure credentials
apps/web/app/api/outlook/threads/validation.ts (1)

3-10: Tighten validation – enforce positive limit and restrict type

  1. limit should also have a lower bound to avoid negative / zero requests.
  2. type appears to represent a finite set (e.g. “inbox”, “sent”). Use z.enum to catch typos.
-  limit: z.coerce.number().max(100).nullish(),
-  type: z.string().nullish(),
+  limit: z
+    .coerce.number()
+    .int()
+    .min(1)
+    .max(100)
+    .nullish(),
+  type: z.enum(["inbox", "sent", "drafts"]).nullish(), // adjust list

This prevents invalid queries reaching the controller layer.

apps/web/app/(landing)/login/LoginForm.tsx (1)

110-126: Independent loading state per provider

loading is shared between Google & Microsoft flows.
Clicking one button sets loading=true, disabling the other dialog’s button even if the first flow errors or the dialog is closed.

Quick fix:

-const [loading, setLoading] = useState(false);
+const [googleLoading, setGoogleLoading] = useState(false);
+const [microsoftLoading, setMicrosoftLoading] = useState(false);

…and use the respective setter/button prop.
Keeps UX responsive and avoids a forced page refresh after a cancelled auth popup.

apps/web/utils/outlook/snippet.test.ts (1)

11-15: Redundant test – identical to previous

"should return the entire string if 'On DAY' is not present" repeats the scenario covered by the first test.
Combine or remove to keep the suite concise.

apps/web/app/api/outlook/threads/controller.ts (1)

14-18: Prefer template literal, avoid brittle string concatenation

Building the request URL via five concatenations is harder to read and error-prone. Switch to a single template literal:

-  const url =
-    "https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages" +
-    "?" +
-    params.toString() +
-    "&$select=id,conversationId,subject,bodyPreview,from,receivedDateTime";
+  const url = `https://graph.microsoft.com/v1.0/me/mailFolders/inbox/messages?${params.toString()}&$select=id,conversationId,subject,bodyPreview,from,receivedDateTime`;
apps/web/utils/outlook/snippet.ts (1)

4-7: Hoist and reuse regex, add case-insensitive flag

The regex is recreated on every call. Extract it once and mark it case-insensitive to handle “On Mon,” / “on Mon,” variants.

-  try {
-    const regex = /On (Mon|Tue|Wed|Thu|Fri|Sat|Sun),/;
-    const match = snippet.split(regex)[0];
+  const RE_REPLY_PREFIX = /on (Mon|Tue|Wed|Thu|Fri|Sat|Sun),/i;
+  try {
+    const match = snippet.split(RE_REPLY_PREFIX)[0];
apps/web/utils/outlook/watch.ts (1)

5-16: Surface API errors instead of silently returning res.data

gmail.users.watch can fail (invalid topic, insufficient scopes, etc.). Returning res.data hides that. Throw when res.status !== 200 to bubble the error:

-  const res = await gmail.users.watch({ ... });
-
-  return res.data;
+  const res = await gmail.users.watch({ ... });
+  if (res.status !== 200) {
+    throw new Error(
+      `Gmail watch failed: ${res.status} ${JSON.stringify(res.data)}`
+    );
+  }
+  return res.data;
apps/web/utils/outlook/retry.ts (1)

4-4: Logger scope is misleading

The utility lives in utils/outlook but the scoped logger string is "gmail-retry". Rename the scope or move the file to avoid future confusion when grepping logs.

apps/web/utils/outlook/signature.ts (2)

1-2: Heavyweight dependency loaded eagerly on every import

jsdom pulls in ~1 MB of code and a few native addons. Importing it at module-load time will bundle it into every edge/server function that merely imports this file, even if extractGmailSignature is never called.
To keep lambdas slim, lazy-load it inside the function:

-import { JSDOM } from "jsdom";
+let JSDOM: typeof import("jsdom").JSDOM;

and inside the try block:

if (!JSDOM) ({ JSDOM } = await import("jsdom"));

30-32: Use project logger instead of console.error

Every other util pipes errors through createScopedLogger; using console breaks log aggregation and tracing. Replace with the scoped logger for consistency.

apps/web/utils/outlook/batch.ts (1)

17-21: Throw early for zero IDs is fine, but returning [] hides misuse

Silently returning an empty array on !ids.length can mask a logic bug upstream. Prefer throwing or logging so the caller knows it made a pointless network trip.

apps/web/utils/outlook/constants.ts (1)

16-16: Constant belongs to Google auth yet sits in Outlook util

GOOGLE_LINKING_STATE_COOKIE_NAME is unrelated to Outlook; consider moving it to a more generic auth constants file to avoid circular deps later.

apps/web/app/api/outlook/threads/route.ts (1)

26-27: emailAccountId retrieved but unused

The variable is never referenced; remove it or pass it to the controller to avoid dead code and satisfy the linter.

apps/web/utils/outlook/draft.ts (2)

9-18: Add explicit return type & null-guard the draft message

Explicitly typing the function helps downstream callers and avoids accidental any.
response.data.message can be undefined; access without a guard will throw.

-export async function getDraft(draftId: string, gmail: gmail_v1.Gmail) {
+export async function getDraft(
+  draftId: string,
+  gmail: gmail_v1.Gmail,
+): Promise<ParsedMessage | null> {-  const message = parseMessage(response.data.message as MessageWithPayload);
+  const raw = response.data.message as MessageWithPayload | undefined;
+  if (!raw) return null;
+  const message = parseMessage(raw);
   return message;
 }

1-8: Gmail-specific utility lives in an outlook/ folder

Placing Gmail helpers under utils/outlook is misleading and will confuse future maintainers.
Consider moving this file (and similar Gmail helpers) to utils/gmail/ or a provider-agnostic location.

apps/web/utils/outlook/settings.ts (1)

1-14: File location/naming is misleading

The module only touches Gmail settings yet resides in utils/outlook.
Please relocate (e.g. utils/gmail/settings.ts) or rename the folder to something provider-agnostic.

apps/web/utils/outlook/permissions.ts (2)

1-5: Gmail permissions checker placed inside outlook folder

The directory hierarchy no longer reflects the content, making navigation harder.
Move to utils/gmail/permissions.ts or a generic utils/permissions/ namespace.


90-97: Token clean-up targets Account only

If the Gmail refresh/access tokens are duplicated on the related emailAccount record, those should be cleared too to avoid orphaned credentials.

apps/web/utils/outlook/trash.ts (1)

1-5: Gmail trash helper located under outlook/

As with the other files, please move to a Gmail-specific directory to keep provider code isolated and discoverable.

apps/web/utils/outlook/forward.ts (2)

4-6: Duplicate “Fwd:” prefixes

forwardEmailSubject always prepends "Fwd:" even if the subject already starts with it, leading to Fwd: Fwd: chains.

-export const forwardEmailSubject = (subject: string) => {
-  return `Fwd: ${subject}`;
+export const forwardEmailSubject = (subject: string) =>
+  subject.trim().toLowerCase().startsWith("fwd:")
+    ? subject
+    : `Fwd: ${subject}`;

1-3: File location inconsistent with provider

The helper relies on Gmail-specific formatting utilities but lives in an outlook directory.
Re-evaluate folder structure to avoid cross-provider coupling.

apps/web/utils/outlook/forward.test.ts (1)

54-85: Test may be too brittle – asserts entire HTML blob

Asserting the full HTML string (incl. whitespace + exact attribute order) makes the test fragile: a harmless re-indent or attribute re-ordering will break it.
Prefer checking only the semantics you care about (e.g. headers, presence of gmail_quote, embedded original HTML).

expect(html).toContain("---------- Forwarded message ----------");
expect(html).toContain("from@demo.com");
expect(html).toContain(message.textHtml);

This keeps the test valuable while reducing maintenance overhead.

apps/web/utils/outlook/reply.ts (2)

34-35: contentHtml may be empty, resulting in <div dir="ltr"></div>

When only htmlContent is omitted and textContent is undefined, contentHtml resolves to "", leading to an empty wrapper element inside the final HTML.
Consider short-circuiting the wrapper if no new content was provided.


60-69: Hour is not zero-padded in formatEmailDate

Minutes are padded, hours are not. For 2 a.m. this yields 2:05, deviating from Gmail’s 02:05.

-const hour = date.getHours();
+const hour = date.getHours().toString().padStart(2, "0");
apps/web/utils/outlook/filter.ts (2)

1-3: Module sits under outlook/ but contains Gmail-specific helpers

Everything here is Gmail API–specific (@googleapis/gmail, GmailLabel).
Relocate to utils/gmail/ (or rename) to avoid provider confusion and accidental imports.


64-67: isFilterExistsError brittle – relies on nested errors[0].message

Google may return error.message without the nested array. Safer:

const msg = (error as any)?.message ?? (error as any)?.errors?.[0]?.message;
return msg === "Filter already exists";
apps/web/utils/outlook/mail.ts (1)

63-75: Recipient splitting only handles , delimiter

Users often paste ;-separated address lists (Outlook default).
Consider splitting on /[,;]\s*/ to be more forgiving.

apps/web/app/(app)/[emailAccountId]/mail/page.tsx (2)

50-60: getKey builds URLSearchParams from object containing undefined values

Although new URLSearchParams({...}) silently drops undefined, it still keeps empty strings created later (e.g. "cc=").
Filter falsy keys first for predictable URLs:

const queryParams = new URLSearchParams(
  Object.entries(query)
    .filter(([, v]) => v !== undefined && v !== "")
    .map(([k, v]) => [k, String(v)]),
);

112-113: Permissions check gated by provider but comment says Gmail-only

The run-time guard is correct, but the preceding comment is outdated (“provider !== outlook”).
Update to avoid confusion.

apps/web/utils/outlook/client.ts (1)

48-64: Logger scope & comments reference Gmail

createScopedLogger("gmail/client") and inline comments still mention Gmail, reinforcing the mis-placement.

Update scope & comments once the correct Outlook logic is in place to avoid future confusion.

apps/web/utils/outlook/message.ts (1)

72-73: Mismatch between limit check and error text

if (messageIds.length > 100) contradicts error text “Max 1000”. Adjust one of them to avoid developer confusion.

apps/web/utils/auth.ts (2)

46-66: Hard-coded scope list – ignore imported constant

MICROSOFT_SCOPES is imported but a literal array is used. Prefer the constant to keep scopes in one place:

-          scope: [
-            "openid", … "MailboxSettings.ReadWrite",
-          ].join(" "),
+          scope: MICROSOFT_SCOPES.join(" "),

404-405: Implicit any type

let response; infers any, defeating TypeScript safety. Declare an explicit type, e.g.:

let response: Response;

or narrow further (Promise<Response> etc.).

apps/web/utils/outlook/label.ts (1)

18-34: Path / namespace mismatch

All exports here manipulate Gmail labels, yet the module sits under outlook/. Consider relocating to gmail/label.ts to avoid cognitive overhead when onboarding new contributors.

apps/web/utils/outlook/thread.ts (2)

175-180: Unused emailAccountId parameter.

emailAccountId is declared in the interface but never referenced in getOutlookThread, creating dead code and misleading intent.

-interface GetOutlookThreadParams {
-  id: string;
-  includeDrafts?: boolean;
-  graphClient: any;
-  emailAccountId: string;
-}
+interface GetOutlookThreadParams {
+  id: string;
+  includeDrafts?: boolean;
+  graphClient: any;
+}

and update the call signature accordingly.


199-206: Minor performance nit – use flatMap instead of manual push.

-  let messages: any[] = [];
-  for (const thread of threads) {
-    if (Array.isArray(thread.posts)) {
-      messages = messages.concat(thread.posts);
-    }
-  }
+  const messages = threads.flatMap((t: any) =>
+    Array.isArray(t.posts) ? t.posts : [],
+  );
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7da3124 and 587da02.

⛔ Files ignored due to path filters (2)
  • apps/web/public/images/microsoft.svg is excluded by !**/*.svg
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (41)
  • README.md (2 hunks)
  • apps/web/.env.example (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/assistant/page.tsx (1 hunks)
  • apps/web/app/(app)/[emailAccountId]/mail/page.tsx (3 hunks)
  • apps/web/app/(landing)/login/LoginForm.tsx (1 hunks)
  • apps/web/app/api/outlook/threads/controller.ts (1 hunks)
  • apps/web/app/api/outlook/threads/route.ts (1 hunks)
  • apps/web/app/api/outlook/threads/validation.ts (1 hunks)
  • apps/web/env.ts (1 hunks)
  • apps/web/package.json (2 hunks)
  • apps/web/utils/account.ts (2 hunks)
  • apps/web/utils/auth.ts (11 hunks)
  • apps/web/utils/middleware.ts (1 hunks)
  • apps/web/utils/outlook/attachment.ts (1 hunks)
  • apps/web/utils/outlook/batch.ts (1 hunks)
  • apps/web/utils/outlook/client.ts (1 hunks)
  • apps/web/utils/outlook/constants.ts (1 hunks)
  • apps/web/utils/outlook/contact.ts (1 hunks)
  • apps/web/utils/outlook/decode.ts (1 hunks)
  • apps/web/utils/outlook/draft.ts (1 hunks)
  • apps/web/utils/outlook/filter.ts (1 hunks)
  • apps/web/utils/outlook/forward.test.ts (1 hunks)
  • apps/web/utils/outlook/forward.ts (1 hunks)
  • apps/web/utils/outlook/history.ts (1 hunks)
  • apps/web/utils/outlook/label.ts (1 hunks)
  • apps/web/utils/outlook/mail.ts (1 hunks)
  • apps/web/utils/outlook/message.ts (1 hunks)
  • apps/web/utils/outlook/permissions.ts (1 hunks)
  • apps/web/utils/outlook/reply.test.ts (1 hunks)
  • apps/web/utils/outlook/reply.ts (1 hunks)
  • apps/web/utils/outlook/retry.ts (1 hunks)
  • apps/web/utils/outlook/scopes.ts (1 hunks)
  • apps/web/utils/outlook/settings.ts (1 hunks)
  • apps/web/utils/outlook/signature.test.ts (1 hunks)
  • apps/web/utils/outlook/signature.ts (1 hunks)
  • apps/web/utils/outlook/snippet.test.ts (1 hunks)
  • apps/web/utils/outlook/snippet.ts (1 hunks)
  • apps/web/utils/outlook/spam.ts (1 hunks)
  • apps/web/utils/outlook/thread.ts (1 hunks)
  • apps/web/utils/outlook/trash.ts (1 hunks)
  • apps/web/utils/outlook/watch.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (18)
apps/web/utils/middleware.ts (1)
apps/web/utils/config.ts (1)
  • EMAIL_ACCOUNT_HEADER (3-3)
apps/web/utils/outlook/watch.ts (2)
apps/web/utils/outlook/label.ts (1)
  • GmailLabel (20-34)
apps/web/env.ts (1)
  • env (5-206)
apps/web/app/(landing)/login/LoginForm.tsx (2)
apps/web/components/ui/dialog.tsx (5)
  • Dialog (112-112)
  • DialogTrigger (116-116)
  • DialogContent (117-117)
  • DialogHeader (118-118)
  • DialogTitle (120-120)
apps/web/components/Typography.tsx (1)
  • SectionDescription (114-114)
apps/web/utils/outlook/spam.ts (1)
apps/web/utils/outlook/label.ts (1)
  • GmailLabel (20-34)
apps/web/utils/outlook/batch.ts (1)
apps/web/utils/types.ts (1)
  • isDefined (8-10)
apps/web/app/api/outlook/threads/controller.ts (1)
apps/web/app/api/outlook/threads/validation.ts (1)
  • ThreadsQuery (11-11)
apps/web/utils/outlook/scopes.ts (1)
apps/web/env.ts (1)
  • env (5-206)
apps/web/app/api/outlook/threads/route.ts (3)
apps/web/utils/middleware.ts (1)
  • withEmailAccount (194-198)
apps/web/app/api/outlook/threads/validation.ts (1)
  • threadsQuery (3-10)
apps/web/app/api/outlook/threads/controller.ts (1)
  • getOutlookThreads (3-26)
apps/web/utils/outlook/signature.test.ts (1)
apps/web/utils/outlook/signature.ts (1)
  • extractGmailSignature (11-35)
apps/web/utils/outlook/reply.ts (1)
apps/web/utils/types.ts (1)
  • ParsedMessage (47-58)
apps/web/utils/outlook/snippet.test.ts (1)
apps/web/utils/outlook/snippet.ts (1)
  • snippetRemoveReply (2-11)
apps/web/utils/account.ts (2)
apps/web/utils/outlook/client.ts (2)
  • getAccessTokenFromClient (112-117)
  • getGmailClientWithRefresh (49-98)
apps/web/utils/gmail/client.ts (2)
  • getAccessTokenFromClient (112-117)
  • getGmailClientWithRefresh (49-98)
apps/web/utils/outlook/draft.ts (4)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-65)
apps/web/utils/mail.ts (1)
  • parseMessage (14-16)
apps/web/utils/types.ts (1)
  • MessageWithPayload (39-41)
apps/web/utils/error.ts (1)
  • isGmailError (37-46)
apps/web/utils/outlook/filter.ts (1)
apps/web/utils/outlook/label.ts (1)
  • GmailLabel (20-34)
apps/web/app/(app)/[emailAccountId]/mail/page.tsx (3)
apps/web/app/api/outlook/threads/validation.ts (1)
  • ThreadsQuery (11-11)
apps/web/app/api/google/threads/controller.ts (1)
  • ThreadsResponse (16-16)
apps/web/app/(app)/[emailAccountId]/PermissionsCheck.tsx (1)
  • PermissionsCheck (11-28)
apps/web/utils/outlook/mail.ts (3)
apps/web/app/api/google/webhook/logger.ts (1)
  • logger (3-3)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-65)
apps/web/utils/mail.ts (1)
  • convertEmailHtmlToText (143-157)
apps/web/utils/outlook/message.ts (6)
apps/web/utils/logger.ts (1)
  • createScopedLogger (17-65)
apps/web/utils/types.ts (5)
  • MessageWithPayload (39-41)
  • ParsedMessage (47-58)
  • BatchError (24-31)
  • isBatchError (33-37)
  • isDefined (8-10)
apps/web/utils/gmail/batch.ts (1)
  • getBatch (10-44)
apps/web/utils/mail.ts (1)
  • parseMessage (14-16)
apps/web/utils/email.ts (1)
  • extractDomainFromEmail (53-71)
apps/web/utils/outlook/client.ts (1)
  • getAccessTokenFromClient (112-117)
apps/web/utils/outlook/thread.ts (4)
apps/web/utils/types.ts (3)
  • ThreadWithPayloadMessages (43-45)
  • isDefined (8-10)
  • MessageWithPayload (39-41)
apps/web/utils/gmail/batch.ts (1)
  • getBatch (10-44)
apps/web/utils/mail.ts (2)
  • parseMessages (24-51)
  • parseMessage (14-16)
apps/web/utils/outlook/label.ts (1)
  • GmailLabel (20-34)
🪛 LanguageTool
README.md

[uncategorized] ~134-~134: Possible missing preposition found.
Context: ...4. Enter your contact information 5. Agree to the User Data policy and then click ...

(AI_HYDRA_LEO_MISSING_TO)


[duplication] ~140-~140: Possible typo: you repeated a word.
Context: ...Client ID. 2. In Application Type, Choose Web application 3. Choose a name for your web client 4. In Aut...

(ENGLISH_WORD_REPEAT_RULE)


[grammar] ~156-~156: Did you mean “Copy and paste”?
Context: ... 2. Click Add or remove scopes 3. Copy paste the below into the `Manually add scopes...

(COPY_PASTE)


[duplication] ~166-~166: Possible typo: you repeated a word.
Context: ...gleapis.com/auth/contacts ``` 4. Click Update 5. Click `Save` in the Data Access page. 5. Add...

(ENGLISH_WORD_REPEAT_RULE)


[style] ~209-~209: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...f the client secret you just created. - Set MICROSOFT_ISSUER to your **Directory ...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🪛 markdownlint-cli2 (0.17.2)
README.md

175-175: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

🪛 Biome (1.9.4)
apps/web/app/api/outlook/threads/controller.ts

[error] 15-18: Template literals are preferred over string concatenation.

Unsafe fix: Use a template literal.

(lint/style/useTemplate)

apps/web/utils/auth.ts

[error] 144-145: Do not use template literals if interpolation and special-character handling are not needed.

Unsafe fix: Replace with string literal

(lint/style/noUnusedTemplateLiteral)


[error] 404-405: This variable implicitly has the any type.

Variable declarations without type annotation and initialization implicitly have the any type. Declare a type or initialize the variable with some value.

(lint/suspicious/noImplicitAnyLet)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: Static Code Analysis Js
  • GitHub Check: Jit Security
🔇 Additional comments (11)
apps/web/package.json (1)

37-37: New dependency added – verify tree-shaking & bundle size

@microsoft/microsoft-graph-client (~300 kB min+gzip) lands in the client bundle if imported in React components. Double-check imports stay server-side / API-side only, or leverage dynamic import() to keep the main bundle slim.

apps/web/.env.example (1)

13-15: ** 👍 Helpful placeholders added**

The Microsoft OAuth variables are clearly named and positioned next to the Gmail block—nice touch.

apps/web/utils/outlook/decode.ts (1)

3-5: Looks good – concise and covers invisible chars

The helper cleanly decodes HTML entities and strips zero-width characters. 👍

apps/web/app/api/outlook/threads/controller.ts (1)

11-13: Verify correct pagination parameter – Graph may expect $skiptoken

Graph’s message listing typically supports $skiptoken for paging large result sets, whereas $skip is an OData offset that is not always enabled. Ensure the API really honours $skip; otherwise switch to $skiptoken (the value returned in @odata.nextLink).

apps/web/utils/outlook/reply.test.ts (1)

1-3: Path / folder mismatch may confuse future maintainers

The test sits in utils/outlook yet imports the implementation from "@/utils/gmail/reply".
If the reply helper is generic, consider moving it to a provider-agnostic folder; otherwise rename either the test file or the import to keep Outlook-specific code together.

apps/web/utils/outlook/signature.test.ts (1)

4-68: Comprehensive and readable test coverage – nice work

The suite exercises multiple edge cases (nested signatures, entities, absence of signature, empty input). Assertions are clear and deterministic.

apps/web/utils/outlook/signature.ts (1)

23-28: Whitespace normalisation may corrupt inline HTML

The regex \s+< removes all spaces before a <, which also strips significant spaces inside attribute values (<span title="2 < 3">). Consider limiting this clean-up to text nodes or using an HTML serializer.

apps/web/app/api/outlook/threads/route.ts (1)

18-19: Access-token lookup fails when session.user is undefined

session.user can be undefined (e.g., when using jwt sessions). Guard or narrow the type before optional chaining:

const accessToken =
  (session as any).accessToken ??
  (session.user && (session.user as any).accessToken);
apps/web/utils/account.ts (1)

119-128: ```bash
#!/bin/bash

Inspect the implementation of getOutlookClientWithRefresh

rg -n "getOutlookClientWithRefresh" -C10 apps/web/utils/outlook/client.ts


</details>
<details>
<summary>apps/web/utils/outlook/thread.ts (2)</summary>

`8-10`: **Possible wrong import path for `GmailLabel`.**

The enum lives under `utils/outlook/label.ts` (see provided snippet) but is imported from `utils/gmail/label`.  
If the latter file does not exist or does not re-export `GmailLabel`, this will break compilation at runtime.

```diff
-import { GmailLabel } from "@/utils/gmail/label";
+import { GmailLabel } from "@/utils/outlook/label";

Please verify the correct location or add a barrel export.


182-221: ```shell
#!/bin/bash

Search for pagination usage or nextLink in thread.ts

rg --color never -n "@odata.nextLink" -n "nextLink" -n "while" -n "for" apps/web/utils/outlook/thread.ts

Search for expand posts usage

rg --color never -n "expand("posts"" apps/web/utils/outlook/thread.ts

Search for draft detection patterns

rg --color never -n "isDraft" -n "messageType" -n "Sent" apps/web/utils/outlook/thread.ts

Search for error handling around graphClient uses

rg --color never -n "try" -n "catch" -n "finally" apps/web/utils/outlook/thread.ts


</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment on lines +31 to +34
// FIXME: redirected too many times
// if (!hasRule) {
// redirect(prefixPath(emailAccountId, "/assistant?onboarding=true"));
// }
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

Commented-out redirect leaves UX edge case unresolved

The “too many redirects” loop is masked by commenting code out, but new users without a rule now silently skip onboarding. Either:

  1. Fix the redirect condition, or
  2. Add a TODO with a tracking issue reference so this doesn’t ship unnoticed.
🤖 Prompt for AI Agents
In apps/web/app/(app)/[emailAccountId]/assistant/page.tsx around lines 31 to 34,
the redirect code is commented out to avoid a "too many redirects" loop, but
this causes new users without a rule to skip onboarding silently. To fix this,
either correct the redirect condition to prevent the loop or add a TODO comment
referencing a tracking issue to ensure this UX edge case is addressed before
shipping.

Comment on lines +155 to 161
let emailAccountId = req.headers.get(EMAIL_ACCOUNT_HEADER);

if (!emailAccountId) {
const url = new URL(req.url);
emailAccountId = url.searchParams.get("emailAccountId") ?? undefined;
}
if (!emailAccountId) {
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

Query-param name may not match new endpoint – risk of false 403

emailAccountId = url.searchParams.get("emailAccountId") only recognises the exact key emailAccountId.
According to the PR description the new endpoint uses ?emailAccount=…. A mismatch will make every call fall through to the 403 branch even when the parameter is supplied.

Recommend accepting both variants (or, preferably, standardise the public contract):

-    emailAccountId = url.searchParams.get("emailAccountId") ?? undefined;
+    emailAccountId =
+      url.searchParams.get("emailAccountId") ??
+      url.searchParams.get("emailAccount") ??
+      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
let emailAccountId = req.headers.get(EMAIL_ACCOUNT_HEADER);
if (!emailAccountId) {
const url = new URL(req.url);
emailAccountId = url.searchParams.get("emailAccountId") ?? undefined;
}
if (!emailAccountId) {
let emailAccountId = req.headers.get(EMAIL_ACCOUNT_HEADER);
if (!emailAccountId) {
const url = new URL(req.url);
emailAccountId =
url.searchParams.get("emailAccountId") ??
url.searchParams.get("emailAccount") ??
undefined;
}
if (!emailAccountId) {
🤖 Prompt for AI Agents
In apps/web/utils/middleware.ts around lines 155 to 161, the code only checks
for the query parameter "emailAccountId" but the new endpoint uses
"emailAccount". Update the code to check for both "emailAccountId" and
"emailAccount" query parameters from the URL search params, assigning whichever
is present to emailAccountId. This will prevent false 403 errors when the
parameter is supplied under the new name.

Comment on lines +1 to +19
import type { gmail_v1 } from "@googleapis/gmail";

export async function getHistory(
gmail: gmail_v1.Gmail,
options: {
startHistoryId: string;
historyTypes?: string[];
maxResults?: number;
},
) {
const history = await gmail.users.history.list({
userId: "me",
startHistoryId: options.startHistoryId,
historyTypes: options.historyTypes,
maxResults: options.maxResults,
});

return history.data;
}
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

Gmail helper lives under an outlook path – confusing placement

The function is Gmail-specific (gmail.users.history.list) yet the file resides in utils/outlook/. This makes code navigation harder and contradicts the new Outlook-centric modules added in the PR.

Consider relocating to utils/gmail/history.ts (or a provider-agnostic namespace) to keep responsibilities clear.

🤖 Prompt for AI Agents
In apps/web/utils/outlook/history.ts lines 1 to 19, the Gmail-specific helper
function is misplaced under an Outlook directory, causing confusion. Move this
file to a more appropriate location such as apps/web/utils/gmail/history.ts or a
shared provider-agnostic folder to clearly separate Gmail-related code from
Outlook modules and improve code organization.

Comment on lines +8 to +14
const attachment = await gmail.users.messages.attachments.get({
userId: "me",
id: attachmentId,
messageId,
});
const attachmentData = attachment.data;
return attachmentData;
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

Add defensive error handling & return the decoded payload only

attachments.get can throw (token expiry, 404, etc.) and attachment.data still contains wrapper metadata. Consider:

-  const attachment = await gmail.users.messages.attachments.get({
-    userId: "me",
-    id: attachmentId,
-    messageId,
-  });
-  const attachmentData = attachment.data;
-  return attachmentData;
+  try {
+    const { data } = await gmail.users.messages.attachments.get({
+      userId: "me",
+      id: attachmentId,
+      messageId,
+    });
+    // `data.data` is the base64 payload – surface that directly
+    return data.data ?? "";
+  } catch (err) {
+    // propagate a clean, provider-agnostic error
+    throw new Error(`Failed to fetch Gmail attachment ${attachmentId}: ${String(err)}`);
+  }

This keeps callers provider-agnostic and prevents them from parsing Google’s response shape.

📝 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 attachment = await gmail.users.messages.attachments.get({
userId: "me",
id: attachmentId,
messageId,
});
const attachmentData = attachment.data;
return attachmentData;
try {
const { data } = await gmail.users.messages.attachments.get({
userId: "me",
id: attachmentId,
messageId,
});
// `data.data` is the base64 payload – surface that directly
return data.data ?? "";
} catch (err) {
// propagate a clean, provider-agnostic error
throw new Error(`Failed to fetch Gmail attachment ${attachmentId}: ${String(err)}`);
}
🤖 Prompt for AI Agents
In apps/web/utils/outlook/attachment.ts around lines 8 to 14, add a try-catch
block around the call to gmail.users.messages.attachments.get to handle
potential errors like token expiry or 404. In the catch block, handle or rethrow
the error appropriately. Also, decode the attachment data payload (e.g., base64
decode) before returning it, so callers receive only the raw decoded content
without Google-specific metadata, keeping the interface provider-agnostic.

Comment on lines +5 to +9
it("should return the string before 'On DAY'", () => {
const snippet = "This is a test email. On Monday, we will meet.";
const result = snippetRemoveReply(snippet);
expect(result).toBe(snippet);
});
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

Test description doesn’t match assertion

The test says it “should return the string before ‘On DAY’” but asserts the entire snippet (because “Monday” is not matched).
Either update the description or use an abbreviated day to make the assertion meaningful.

-it("should return the string before 'On DAY'", () => {
-  const snippet = "This is a test email. On Monday, we will meet.";
+it("should return the string before 'On DAY (abbr)'", () => {
+  const snippet = "This is a test email. On Mon, we will meet.";
   const result = snippetRemoveReply(snippet);
-  expect(result).toBe(snippet);
+  expect(result).toBe("This is a test email.");
});
📝 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
it("should return the string before 'On DAY'", () => {
const snippet = "This is a test email. On Monday, we will meet.";
const result = snippetRemoveReply(snippet);
expect(result).toBe(snippet);
});
it("should return the string before 'On DAY (abbr)'", () => {
const snippet = "This is a test email. On Mon, we will meet.";
const result = snippetRemoveReply(snippet);
expect(result).toBe("This is a test email.");
});
🤖 Prompt for AI Agents
In apps/web/utils/outlook/snippet.test.ts around lines 5 to 9, the test
description states it should return the string before 'On DAY' but the assertion
checks the entire snippet because "Monday" does not match the pattern. To fix
this, either update the test description to reflect that the entire string is
expected or modify the snippet to use an abbreviated day (e.g., "Mon") so the
function correctly matches and returns the substring before 'On DAY', making the
assertion meaningful.

Comment on lines +99 to +112
export async function getOutlookClientForEmail({
emailAccountId,
}: {
emailAccountId: string;
}) {
const tokens = await getTokens({ emailAccountId });
const gmail = getOutlookClientWithRefresh({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken || "",
expiresAt: tokens.expiresAt ?? null,
emailAccountId,
});
return gmail;
}
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

Undefined helper & misleading variable name

getOutlookClientWithRefresh is neither imported nor declared, causing a compile-time reference error.
Additionally, the returned instance is stored in a variable named gmail, which is confusing for Outlook logic.

-import { /* … */ } from "@/utils/gmail/client";
+import { getOutlookClientWithRefresh } from "@/utils/outlook/client"; // or correct path-  const gmail = getOutlookClientWithRefresh({ … });
-  return gmail;
+  const outlook = await getOutlookClientWithRefresh({ … });
+  return outlook;
📝 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
export async function getOutlookClientForEmail({
emailAccountId,
}: {
emailAccountId: string;
}) {
const tokens = await getTokens({ emailAccountId });
const gmail = getOutlookClientWithRefresh({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken || "",
expiresAt: tokens.expiresAt ?? null,
emailAccountId,
});
return gmail;
}
-import { /* … */ } from "@/utils/gmail/client";
+import { getOutlookClientWithRefresh } from "@/utils/outlook/client";
export async function getOutlookClientForEmail({
emailAccountId,
}: {
emailAccountId: string;
}) {
const tokens = await getTokens({ emailAccountId });
- const gmail = getOutlookClientWithRefresh({
+ const outlook = await getOutlookClientWithRefresh({
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken || "",
expiresAt: tokens.expiresAt ?? null,
emailAccountId,
});
- return gmail;
+ return outlook;
}
🤖 Prompt for AI Agents
In apps/web/utils/account.ts around lines 99 to 112, the function uses an
undefined helper getOutlookClientWithRefresh which must be imported or declared
to avoid reference errors. Also, rename the variable currently named gmail to a
more appropriate name like outlookClient to accurately reflect the Outlook
context and avoid confusion. Ensure the import statement for
getOutlookClientWithRefresh is added at the top if missing.

Comment on lines +1 to +10
import { auth, gmail, type gmail_v1 } from "@googleapis/gmail";
import { people } from "@googleapis/people";
import { saveTokens } from "@/utils/auth";
import { env } from "@/env";
import { createScopedLogger } from "@/utils/logger";
import { SCOPES } from "@/utils/gmail/scopes";
import { SafeError } from "@/utils/error";

const logger = createScopedLogger("gmail/client");

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

Module mis-named – contains Gmail implementation

This “outlook” client actually imports @googleapis/gmail, people, and uses Google scopes.
Rename the file (gmail/client.ts) or rewrite it to build and refresh Microsoft Graph tokens; otherwise Outlook-related imports in other modules cannot be resolved.

🤖 Prompt for AI Agents
In apps/web/utils/outlook/client.ts lines 1 to 10, the file is misnamed as
"outlook" but contains Gmail-specific imports and logic. Rename the file from
"outlook/client.ts" to "gmail/client.ts" to accurately reflect its Gmail
implementation and avoid confusion or import resolution issues in other modules.

Comment on lines +1 to +15
import type { gmail_v1 } from "@googleapis/gmail";
import { parseMessage } from "@/utils/mail";
import {
type BatchError,
type MessageWithPayload,
type ParsedMessage,
isBatchError,
isDefined,
} from "@/utils/types";
import { getBatch } from "@/utils/gmail/batch";
import { extractDomainFromEmail } from "@/utils/email";
import { createScopedLogger } from "@/utils/logger";
import { sleep } from "@/utils/sleep";
import { getAccessTokenFromClient } from "@/utils/gmail/client";

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

Gmail-specific code inside Outlook module

The file lives under outlook/ yet pulls Gmail types & helpers. Either move it to a Gmail folder or re-implement using Microsoft Graph; current location is misleading.

🤖 Prompt for AI Agents
In apps/web/utils/outlook/message.ts lines 1 to 15, the code imports
Gmail-specific types and utilities in a file located in the Outlook module,
which is misleading. To fix this, either move this file and its code to a
Gmail-specific folder to match its dependencies or refactor the implementation
to use Microsoft Graph APIs and types appropriate for Outlook, ensuring the
module's content aligns with its folder and purpose.

Comment on lines +168 to +182
const PUBLIC_DOMAINS = new Set([
"gmail.com",
"yahoo.com",
"hotmail.com",
"outlook.com",
"aol.com",
"icloud.com",
"@me.com",
"protonmail.com",
"zoho.com",
"yandex.com",
"fastmail.com",
"gmx.com",
"@hey.com",
]);
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

Inconsistent domain set

PUBLIC_DOMAINS mixes plain domains and entries starting with “@” which will never match the output of extractDomainFromEmail (returns domain without “@”).
Strip the @ symbols or make all entries consistent.

-  "@me.com",
+  "me.com",
-  "@hey.com",
+  "hey.com",
📝 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 PUBLIC_DOMAINS = new Set([
"gmail.com",
"yahoo.com",
"hotmail.com",
"outlook.com",
"aol.com",
"icloud.com",
"@me.com",
"protonmail.com",
"zoho.com",
"yandex.com",
"fastmail.com",
"gmx.com",
"@hey.com",
]);
const PUBLIC_DOMAINS = new Set([
"gmail.com",
"yahoo.com",
"hotmail.com",
"outlook.com",
"aol.com",
"icloud.com",
"me.com",
"protonmail.com",
"zoho.com",
"yandex.com",
"fastmail.com",
"gmx.com",
"hey.com",
]);
🤖 Prompt for AI Agents
In apps/web/utils/outlook/message.ts around lines 168 to 182, the PUBLIC_DOMAINS
set inconsistently includes some domains with a leading "@" symbol, which will
not match the domain strings returned by extractDomainFromEmail since it returns
domains without "@". To fix this, remove the "@" symbols from all entries in the
PUBLIC_DOMAINS set so that all domains are plain domain strings for consistent
matching.

Comment on lines +75 to +86
export async function getThreadsBatch(
threadIds: string[],
accessToken: string,
): Promise<ThreadWithPayloadMessages[]> {
const batch = await getBatch(
threadIds,
"/gmail/v1/users/me/threads",
accessToken,
);

return batch;
}
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

getThreadsBatch can overflow the Gmail batch limit.

getBatch throws when ids.length > BATCH_LIMIT (100).
getThreadsBatch forwards threadIds unchanged, so any caller that passes >100 IDs will explode.

Consider chunking:

 export async function getThreadsBatch(
   threadIds: string[],
   accessToken: string,
 ): Promise<ThreadWithPayloadMessages[]> {
-  const batch = await getBatch(
-    threadIds,
-    "/gmail/v1/users/me/threads",
-    accessToken,
-  );
-
-  return batch;
+  const CHUNK = 100; // Gmail batch hard-limit
+  const results: ThreadWithPayloadMessages[] = [];
+
+  for (let i = 0; i < threadIds.length; i += CHUNK) {
+    const slice = threadIds.slice(i, i + CHUNK);
+    const batch = await getBatch(
+      slice,
+      "/gmail/v1/users/me/threads",
+      accessToken,
+    );
+    results.push(...batch);
+  }
+
+  return results;
 }
📝 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
export async function getThreadsBatch(
threadIds: string[],
accessToken: string,
): Promise<ThreadWithPayloadMessages[]> {
const batch = await getBatch(
threadIds,
"/gmail/v1/users/me/threads",
accessToken,
);
return batch;
}
export async function getThreadsBatch(
threadIds: string[],
accessToken: string,
): Promise<ThreadWithPayloadMessages[]> {
const CHUNK = 100; // Gmail batch hard-limit
const results: ThreadWithPayloadMessages[] = [];
for (let i = 0; i < threadIds.length; i += CHUNK) {
const slice = threadIds.slice(i, i + CHUNK);
const batch = await getBatch(
slice,
"/gmail/v1/users/me/threads",
accessToken,
);
results.push(...batch);
}
return results;
}
🤖 Prompt for AI Agents
In apps/web/utils/outlook/thread.ts around lines 75 to 86, getThreadsBatch
forwards the entire threadIds array to getBatch, which throws an error if the
array length exceeds 100. To fix this, modify getThreadsBatch to split threadIds
into chunks of 100 or fewer, call getBatch for each chunk, and then combine all
results before returning, ensuring no batch exceeds the Gmail API limit.

@elie222
Copy link
Owner

elie222 commented Jun 19, 2025

Hey, we have a few people that all took this issue at the same time :(

Being worked on here:
#493

If you have comments or think anything should be changed happy to hear.

@elie222
Copy link
Owner

elie222 commented Jul 3, 2025

Outlook will go live soon via the other PR

@elie222 elie222 closed this Jul 3, 2025
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.

2 participants