diff --git a/apps/web/utils/email/microsoft.ts b/apps/web/utils/email/microsoft.ts index 8b60b297b8..48d6a78cd2 100644 --- a/apps/web/utils/email/microsoft.ts +++ b/apps/web/utils/email/microsoft.ts @@ -63,7 +63,6 @@ import { getOrCreateOutlookFolderIdByName, getOutlookFolderTree, } from "@/utils/outlook/folders"; -import { hasUnquotedParentFolderId } from "@/utils/outlook/message"; import { extractSignatureFromHtml } from "@/utils/email/signature-extraction"; import { moveMessagesForSenders } from "@/utils/outlook/batch"; @@ -754,37 +753,20 @@ export class OutlookProvider implements EmailProvider { hasDateFilters: dateFilters.length > 0, }); - // Check if the query already contains parentFolderId as an unquoted identifier - // If it does, skip applying the default folder filter to avoid conflicts - const queryHasParentFolderId = - originalQuery && hasUnquotedParentFolderId(originalQuery); - - // Get folder IDs to get the inbox folder ID - const folderIds = await getFolderIds(this.client); - const inboxFolderId = folderIds.inbox; - - if (!queryHasParentFolderId && !inboxFolderId) { - throw new Error("Could not find inbox folder ID"); - } - - // Only apply folder filtering if the query doesn't already specify parentFolderId - const folderId = queryHasParentFolderId ? undefined : inboxFolderId; - this.logger.info("Calling queryBatchMessages with separated parameters", { searchQuery: originalQuery.trim() || undefined, dateFilters, maxResults: options.maxResults || 20, pageToken: options.pageToken, - folderId, - queryHasParentFolderId, }); + // Don't pass folderId - let the API return all folders except Junk/Deleted (auto-excluded) + // Drafts are filtered out in convertMessages const response = await queryBatchMessages(this.client, { searchQuery: originalQuery.trim() || undefined, dateFilters, maxResults: options.maxResults || 20, pageToken: options.pageToken, - folderId, }); return { diff --git a/apps/web/utils/outlook/message.test.ts b/apps/web/utils/outlook/message.test.ts deleted file mode 100644 index cfde25ddde..0000000000 --- a/apps/web/utils/outlook/message.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { hasUnquotedParentFolderId } from "./message"; - -describe("hasUnquotedParentFolderId", () => { - describe("should return true for unquoted parentFolderId", () => { - it("detects direct parentFolderId filter", () => { - const query = "parentFolderId eq 'inbox'"; - expect(hasUnquotedParentFolderId(query)).toBe(true); - }); - - it("detects parentFolderId in compound query", () => { - const query = - "from eq 'test@example.com' and parentFolderId eq 'archive'"; - expect(hasUnquotedParentFolderId(query)).toBe(true); - }); - - it("detects parentFolderId with 'ne' operator", () => { - const query = "parentFolderId ne 'drafts'"; - expect(hasUnquotedParentFolderId(query)).toBe(true); - }); - - it("detects parentFolderId in or conditions", () => { - const query = - "(parentFolderId eq 'inbox' or parentFolderId eq 'archive')"; - expect(hasUnquotedParentFolderId(query)).toBe(true); - }); - }); - - describe("should return false for quoted parentFolderId", () => { - it("ignores parentFolderId inside single quotes", () => { - const query = "subject eq 'parentFolderId eq 123'"; - expect(hasUnquotedParentFolderId(query)).toBe(false); - }); - - it("ignores parentFolderId inside double quotes", () => { - const query = 'subject eq "parentFolderId eq 123"'; - expect(hasUnquotedParentFolderId(query)).toBe(false); - }); - - it("ignores parentFolderId in quoted subject", () => { - const query = - "from eq 'user@domain.com' and subject eq 'Re: parentFolderId issues'"; - expect(hasUnquotedParentFolderId(query)).toBe(false); - }); - - it("ignores parentFolderId in quoted body search", () => { - const query = "body contains 'This email mentions parentFolderId'"; - expect(hasUnquotedParentFolderId(query)).toBe(false); - }); - - it("handles escaped quotes correctly", () => { - const query = "subject eq 'with \\'escaped\\' quotes and parentFolderId'"; - expect(hasUnquotedParentFolderId(query)).toBe(false); - }); - - it("handles mixed single and double quotes", () => { - const query = `subject eq "outer 'inner parentFolderId' outer"`; - expect(hasUnquotedParentFolderId(query)).toBe(false); - }); - }); - - describe("mixed cases", () => { - it("detects unquoted parentFolderId even when quoted ones exist", () => { - const query = - "subject eq 'test parentFolderId' and parentFolderId eq 'inbox'"; - expect(hasUnquotedParentFolderId(query)).toBe(true); - }); - - it("handles complex nested quotes", () => { - const query = `subject eq "complex 'nested parentFolderId' quotes" and from eq 'test@example.com'`; - expect(hasUnquotedParentFolderId(query)).toBe(false); - }); - }); - - describe("edge cases", () => { - it("returns false for empty query", () => { - const query = ""; - expect(hasUnquotedParentFolderId(query)).toBe(false); - }); - - it("returns false for query without parentFolderId", () => { - const query = "from eq 'test@example.com'"; - expect(hasUnquotedParentFolderId(query)).toBe(false); - }); - - it("handles partial matches correctly", () => { - const query = "myparentFolderId eq 'test'"; - expect(hasUnquotedParentFolderId(query)).toBe(false); - }); - - it("handles word boundaries correctly", () => { - const query = "parentFolderIdSomething eq 'test'"; - expect(hasUnquotedParentFolderId(query)).toBe(false); - }); - - it("detects parentFolderId at start of query", () => { - const query = "parentFolderId eq 'inbox' and from eq 'test@example.com'"; - expect(hasUnquotedParentFolderId(query)).toBe(true); - }); - - it("detects parentFolderId at end of query", () => { - const query = "from eq 'test@example.com' and parentFolderId eq 'inbox'"; - expect(hasUnquotedParentFolderId(query)).toBe(true); - }); - - it("handles unclosed quotes gracefully", () => { - const query = "subject eq 'unclosed quote and parentFolderId"; - expect(hasUnquotedParentFolderId(query)).toBe(false); - }); - }); -}); diff --git a/apps/web/utils/outlook/message.ts b/apps/web/utils/outlook/message.ts index 4133268279..b3ff3d5036 100644 --- a/apps/web/utils/outlook/message.ts +++ b/apps/web/utils/outlook/message.ts @@ -13,64 +13,6 @@ const logger = createScopedLogger("outlook/message"); export const MESSAGE_SELECT_FIELDS = "id,conversationId,conversationIndex,subject,bodyPreview,from,sender,toRecipients,ccRecipients,receivedDateTime,isDraft,isRead,body,categories,parentFolderId"; -/** - * Removes quoted string literals from a query string to avoid false positives - * when checking for identifiers that might appear inside quotes. - * Handles both single and double quotes, including escaped quotes. - */ -function stripQuotedLiterals(query: string): string { - let result = ""; - let i = 0; - - while (i < query.length) { - const char = query[i]; - - if (char === "'" || char === '"') { - const quote = char; - i++; // Skip opening quote - - // Skip until we find the matching closing quote (or end of string) - while (i < query.length) { - const current = query[i]; - if (current === quote) { - // Found closing quote, check if it's escaped - let backslashCount = 0; - let j = i - 1; - while (j >= 0 && query[j] === "\\") { - backslashCount++; - j--; - } - - // If even number of backslashes (including 0), quote is not escaped - if (backslashCount % 2 === 0) { - i++; // Skip closing quote - break; - } - } - i++; - } - - // Replace the entire quoted section with spaces to maintain positions - // This prevents issues with adjacent identifiers - result += " "; - } else { - result += char; - i++; - } - } - - return result; -} - -/** - * Checks if parentFolderId appears as an unquoted identifier in the query. - * This avoids false positives when parentFolderId appears inside string literals. - */ -export function hasUnquotedParentFolderId(query: string): boolean { - const cleanedQuery = stripQuotedLiterals(query); - return /\bparentFolderId\b/.test(cleanedQuery); -} - // Well-known folder names in Outlook that are consistent across all languages export const WELL_KNOWN_FOLDERS = { inbox: "inbox", @@ -241,21 +183,11 @@ export async function queryBatchMessages( const hasSearchQuery = !!effectiveSearchQuery; const hasDateFilters = !!(dateFilters && dateFilters.length > 0); - // Always filter to only include inbox and archive folders - const inboxFolderId = folderIds.inbox; - const archiveFolderId = folderIds.archive; - - if (!inboxFolderId || !archiveFolderId) { - logger.warn("Missing required folder IDs", { - inboxFolderId, - archiveFolderId, - }); - } - - // Build folder filter for all cases + // Only apply folder filtering if a specific folderId is requested + // API already excludes Junk/Deleted by default, and drafts are filtered in convertMessages const folderFilter = folderId ? `parentFolderId eq '${escapeODataString(folderId)}'` - : `(parentFolderId eq '${escapeODataString(inboxFolderId)}' or parentFolderId eq '${escapeODataString(archiveFolderId)}')`; + : undefined; if (hasSearchQuery) { // Search path - use $search parameter @@ -276,16 +208,10 @@ export async function queryBatchMessages( const response: { value: Message[]; "@odata.nextLink"?: string } = await withOutlookRetry(() => request.get()); - // Filter messages to only include inbox and archive folders - const filteredMessages = response.value.filter((message) => { - if (folderId) { - return message.parentFolderId === folderId; - } - return ( - message.parentFolderId === inboxFolderId || - message.parentFolderId === archiveFolderId - ); - }); + // Filter to specific folder if requested, otherwise get all + const filteredMessages = folderId + ? response.value.filter((message) => message.parentFolderId === folderId) + : response.value; const messages = await convertMessages(filteredMessages, folderIds); nextPageToken = response["@odata.nextLink"] @@ -295,7 +221,7 @@ export async function queryBatchMessages( logger.info("Search results", { totalFound: response.value.length, - afterFolderFiltering: filteredMessages.length, + filteredByFolder: folderId ? filteredMessages.length : undefined, messageCount: messages.length, hasNextPageToken: !!nextPageToken, }); @@ -303,14 +229,20 @@ export async function queryBatchMessages( return { messages, nextPageToken }; } else { // Filter path - use $filter parameter for date filters or folder-only queries - const filters = [folderFilter]; + const filters: string[] = []; + + // Add folder filter if a specific folder is requested + if (folderFilter) { + filters.push(folderFilter); + } // Add date filters if provided if (hasDateFilters) { filters.push(...dateFilters!); } - const combinedFilter = filters.join(" and "); + const combinedFilter = + filters.length > 0 ? filters.join(" and ") : undefined; logger.info("Using filter path", { folderFilter, @@ -318,7 +250,10 @@ export async function queryBatchMessages( combinedFilter, }); - request = request.filter(combinedFilter); + // Only apply filter if we have something to filter + if (combinedFilter) { + request = request.filter(combinedFilter); + } if (pageToken) { request = request.skipToken(pageToken); diff --git a/version.txt b/version.txt index 7a7b10b0b1..1fccf689be 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.19.7 +v2.19.8