Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 45e3da0527 Microsoft email folder filters ignored during sync
https://sonarly.com/issue/26908?type=bug

When users select "Some folders" and pick specific subfolders for Microsoft/Outlook email sync, ALL emails are imported because the backend only discovers top-level mail folders from the Microsoft Graph API and never finds the user-selected child folders.

Fix: Added recursive child folder discovery to MicrosoftGetAllFoldersService. The existing code only called /me/mailFolders which returns top-level Microsoft mail folders. When a user selected "Some folders" and picked a subfolder (e.g., "Inbox/Twenty"), that subfolder was never discovered by the backend, so it could never have isSynced=true, and the isSynced filter in the message list service would find zero matching folders.

The fix adds a private fetchFoldersRecursively method that checks each folder's childFolderCount (already present in the Microsoft Graph API response and already typed in MicrosoftGraphFolder). For folders with children, it calls /me/mailFolders/{id}/childFolders to fetch nested folders, and recurses for any deeper nesting. The error handling follows the same .catch() pattern used for the top-level folder fetch — errors are logged and return empty results rather than breaking the entire sync.

The downstream isSynced filtering in MicrosoftGetMessageListService (line 41-44) already correctly filters folders when SELECTED_FOLDERS policy is active, and the early-exit guard (line 47) already returns empty when no folders match. The only missing piece was that child folders were never discovered in the first place.
2026-04-16 07:22:32 +00:00
@@ -62,11 +62,19 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
return { value: [] };
});
const folders = (response.value as MicrosoftGraphFolder[]) || [];
const rootFolderId = this.getRootFolderId(folders);
const topLevelFolders =
(response.value as MicrosoftGraphFolder[]) || [];
const allFolders = await this.fetchFoldersRecursively(
microsoftClient,
connectedAccount,
topLevelFolders,
);
const rootFolderId = this.getRootFolderId(topLevelFolders);
const folderInfos: DiscoveredMessageFolder[] = [];
for (const folder of folders) {
for (const folder of allFolders) {
if (!folder.displayName) {
continue;
}
@@ -111,6 +119,44 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
}
}
private async fetchFoldersRecursively(
microsoftClient: any,
connectedAccount: Pick<ConnectedAccountEntity, 'id'>,
folders: MicrosoftGraphFolder[],
): Promise<MicrosoftGraphFolder[]> {
const allFolders: MicrosoftGraphFolder[] = [...folders];
for (const folder of folders) {
if (folder.childFolderCount && folder.childFolderCount > 0) {
const childResponse = await microsoftClient
.api(`/me/mailFolders/${folder.id}/childFolders`)
.version('beta')
.top(MESSAGING_MICROSOFT_MAIL_FOLDERS_LIST_MAX_RESULT)
.get()
.catch((error: Error) => {
this.logger.error(
`Connected account ${connectedAccount.id}: Error fetching child folders for ${folder.id}: ${error.message}`,
);
return { value: [] };
});
const childFolders =
(childResponse.value as MicrosoftGraphFolder[]) || [];
const nestedFolders = await this.fetchFoldersRecursively(
microsoftClient,
connectedAccount,
childFolders,
);
allFolders.push(...nestedFolders);
}
}
return allFolders;
}
private isSentFolder(standardFolder: StandardFolder | null): boolean {
return standardFolder === StandardFolder.SENT;
}