Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 7eea45731d fix(auth): catch folder sync errors during Microsoft OAuth callback to prevent dead-end redirect
https://sonarly.com/issue/5916?type=bug

Users connecting a Microsoft account without an Exchange Online mailbox (on-premise, shared, or soft-deleted) get redirected to an error page during the OAuth callback, even though the connected account was already created successfully.

Fix: Wrap syncMessageFolders calls in try-catch in all three OAuth callback services
    (microsoft-apis.service.ts, google-apis.service.ts, imap-smtp-caldav-apis.service.ts).
    Folder sync is non-critical during account creation — failures are logged and execution
    continues so message queue jobs get properly enqueued. The background job
    (MessagingMessageListFetchJob) handles folder sync independently with proper error recovery.
2026-04-30 09:46:18 +00:00
5 changed files with 50 additions and 17 deletions
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
@@ -49,6 +49,8 @@ import { isDefined } from 'twenty-shared/utils';
@Injectable()
export class GoogleAPIsService {
private readonly logger = new Logger(GoogleAPIsService.name);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectMessageQueue(MessageQueue.messagingQueue)
@@ -254,10 +256,16 @@ export class GoogleAPIsService {
);
if (isDefined(newMessageChannel)) {
await this.syncMessageFoldersService.syncMessageFolders({
messageChannel: newMessageChannel,
workspaceId,
});
try {
await this.syncMessageFoldersService.syncMessageFolders({
messageChannel: newMessageChannel,
workspaceId,
});
} catch (error) {
this.logger.error(
`Failed to sync message folders for connected account ${newOrExistingConnectedAccountId}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import {
@@ -47,6 +47,8 @@ import { isDefined } from 'twenty-shared/utils';
@Injectable()
export class MicrosoftAPIsService {
private readonly logger = new Logger(MicrosoftAPIsService.name);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectMessageQueue(MessageQueue.messagingQueue)
@@ -233,10 +235,16 @@ export class MicrosoftAPIsService {
);
if (isDefined(newMessageChannel)) {
await this.syncMessageFoldersService.syncMessageFolders({
messageChannel: newMessageChannel,
workspaceId,
});
try {
await this.syncMessageFoldersService.syncMessageFolders({
messageChannel: newMessageChannel,
workspaceId,
});
} catch (error) {
this.logger.error(
`Failed to sync message folders for connected account ${newOrExistingConnectedAccountId}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
@@ -8,6 +8,10 @@ import {
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
@@ -74,6 +78,13 @@ export class GuardRedirectService {
)
return;
// Expected driver errors (e.g. unlicensed Microsoft accounts) are not actionable
if (
err instanceof MessageImportDriverException &&
err.code === MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS
)
return;
this.exceptionHandlerService.captureExceptions([err], {
workspace: {
id: workspaceId,
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ConnectedAccountProvider } from 'twenty-shared/types';
@@ -23,6 +23,8 @@ import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-
@Injectable()
export class ImapSmtpCalDavAPIService {
private readonly logger = new Logger(ImapSmtpCalDavAPIService.name);
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
@InjectRepository(CalendarChannelEntity)
@@ -188,10 +190,16 @@ export class ImapSmtpCalDavAPIService {
);
if (isDefined(newMessageChannel)) {
await this.syncMessageFoldersService.syncMessageFolders({
messageChannel: newMessageChannel,
workspaceId,
});
try {
await this.syncMessageFoldersService.syncMessageFolders({
messageChannel: newMessageChannel,
workspaceId,
});
} catch (error) {
this.logger.error(
`Failed to sync message folders for connected account ${newOrExistingAccountId}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
}
@@ -58,8 +58,6 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
`Connected account ${connectedAccount.id}: Error fetching folders: ${error.message}`,
);
this.microsoftMessageListFetchErrorHandler.handleError(error);
return { value: [] };
});
const folders = (response.value as MicrosoftGraphFolder[]) || [];