Catching "no licence" microsoft account (#12143)

Catching "no licence - removed" microsoft message channels. 

Current behabiour 
> ` MessageImportException [Error]: The mailbox is either inactive,
soft-deleted, or is hosted on-premise.`

Goal:
better track errors VS user mistakes

Context: 
A similar logic was already implemented for the calendar channels. I
just replicated it to message channels
This commit is contained in:
Guillim
2025-05-21 11:04:11 +02:00
committed by GitHub
parent fe25557337
commit 91e487dd63
9 changed files with 167 additions and 64 deletions

View File

@ -4,6 +4,7 @@ import {
CalendarEventImportDriverException, CalendarEventImportDriverException,
CalendarEventImportDriverExceptionCode, CalendarEventImportDriverExceptionCode,
} from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception'; } from 'src/modules/calendar/calendar-event-import-manager/drivers/exceptions/calendar-event-import-driver.exception';
import { MessageNetworkExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-network.exception';
export const parseGaxiosError = ( export const parseGaxiosError = (
error: GaxiosError, error: GaxiosError,
@ -11,11 +12,11 @@ export const parseGaxiosError = (
const { code } = error; const { code } = error;
switch (code) { switch (code) {
case 'ECONNRESET': case MessageNetworkExceptionCode.ECONNRESET:
case 'ENOTFOUND': case MessageNetworkExceptionCode.ENOTFOUND:
case 'ECONNABORTED': case MessageNetworkExceptionCode.ECONNABORTED:
case 'ETIMEDOUT': case MessageNetworkExceptionCode.ETIMEDOUT:
case 'ERR_NETWORK': case MessageNetworkExceptionCode.ERR_NETWORK:
return new CalendarEventImportDriverException( return new CalendarEventImportDriverException(
error.message, error.message,
CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR, CalendarEventImportDriverExceptionCode.TEMPORARY_ERROR,

View File

@ -0,0 +1,7 @@
export enum MessageNetworkExceptionCode {
ECONNRESET = 'ECONNRESET',
ENOTFOUND = 'ENOTFOUND',
ECONNABORTED = 'ECONNABORTED',
ETIMEDOUT = 'ETIMEDOUT',
ERR_NETWORK = 'ERR_NETWORK',
}

View File

@ -4,6 +4,7 @@ import {
MessageImportDriverException, MessageImportDriverException,
MessageImportDriverExceptionCode, MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { MessageNetworkExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-network.exception';
export const parseGaxiosError = ( export const parseGaxiosError = (
error: GaxiosError, error: GaxiosError,
@ -11,11 +12,11 @@ export const parseGaxiosError = (
const { code } = error; const { code } = error;
switch (code) { switch (code) {
case 'ECONNRESET': case MessageNetworkExceptionCode.ECONNRESET:
case 'ENOTFOUND': case MessageNetworkExceptionCode.ENOTFOUND:
case 'ECONNABORTED': case MessageNetworkExceptionCode.ECONNABORTED:
case 'ETIMEDOUT': case MessageNetworkExceptionCode.ETIMEDOUT:
case 'ERR_NETWORK': case MessageNetworkExceptionCode.ERR_NETWORK:
return new MessageImportDriverException( return new MessageImportDriverException(
error.message, error.message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR, MessageImportDriverExceptionCode.TEMPORARY_ERROR,

View File

@ -1,16 +1,16 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MicrosoftImportDriverException } from 'src/modules/messaging/message-import-manager/drivers/microsoft/exceptions/microsoft-import-driver.exception';
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider'; import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface'; import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
import { isMicrosoftClientTemporaryError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-temporary-error.utils'; import { MicrosoftHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-handle-error.service';
@Injectable() @Injectable()
export class MicrosoftFetchByBatchService { export class MicrosoftFetchByBatchService {
private readonly logger = new Logger(MicrosoftFetchByBatchService.name); private readonly logger = new Logger(MicrosoftFetchByBatchService.name);
constructor( constructor(
private readonly microsoftClientProvider: MicrosoftClientProvider, private readonly microsoftClientProvider: MicrosoftClientProvider,
private readonly microsoftHandleErrorService: MicrosoftHandleErrorService,
) {} ) {}
async fetchAllByBatches( async fetchAllByBatches(
@ -52,25 +52,9 @@ export class MicrosoftFetchByBatchService {
batchResponses.push(batchResponse); batchResponses.push(batchResponse);
} catch (error) { } catch (error) {
if ( this.microsoftHandleErrorService.handleMicrosoftMessageFetchByBatchError(
error.body && error,
typeof error.body === 'string' && );
isMicrosoftClientTemporaryError(error.body)
) {
// TODO: remove this log once we catch better the error codes
this.logger.error(
`Error temporary (${error.code}) fetching messages for account ${connectedAccount.id.slice(0, 8)}`,
);
this.logger.log(error);
throw new MicrosoftImportDriverException(error.body, error.code, 429);
} else {
// TODO: remove this log once we catch better the error codes
this.logger.error(
`Error unknown (${error.code}) fetching messages for account ${connectedAccount.id.slice(0, 8)}`,
);
this.logger.log(error);
throw error;
}
} }
} }

View File

@ -77,7 +77,12 @@ export class MicrosoftGetMessageListService {
.headers({ .headers({
Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}, IdType="ImmutableId"`, Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}, IdType="ImmutableId"`,
}) })
.get(); .get()
.catch((error) => {
this.microsoftHandleErrorService.handleMicrosoftGetMessageListError(
error,
);
});
const callback: PageIteratorCallback = (data) => { const callback: PageIteratorCallback = (data) => {
messageExternalIds.push(data.id); messageExternalIds.push(data.id);
@ -92,7 +97,9 @@ export class MicrosoftGetMessageListService {
}); });
await pageIterator.iterate().catch((error) => { await pageIterator.iterate().catch((error) => {
this.microsoftHandleErrorService.handleMicrosoftMessageFetchError(error); this.microsoftHandleErrorService.handleMicrosoftGetMessageListError(
error,
);
}); });
return { return {
@ -195,7 +202,12 @@ export class MicrosoftGetMessageListService {
.headers({ .headers({
Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}, IdType="ImmutableId"`, Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}, IdType="ImmutableId"`,
}) })
.get(); .get()
.catch((error) => {
this.microsoftHandleErrorService.handleMicrosoftGetMessageListError(
error,
);
});
const callback: PageIteratorCallback = (data) => { const callback: PageIteratorCallback = (data) => {
if (data['@removed']) { if (data['@removed']) {
@ -214,7 +226,9 @@ export class MicrosoftGetMessageListService {
}); });
await pageIterator.iterate().catch((error) => { await pageIterator.iterate().catch((error) => {
this.microsoftHandleErrorService.handleMicrosoftMessageFetchError(error); this.microsoftHandleErrorService.handleMicrosoftGetMessageListError(
error,
);
}); });
return { return {

View File

@ -4,6 +4,7 @@ import { isDefined } from 'twenty-shared/utils';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { computeMessageDirection } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util'; import { computeMessageDirection } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-message-direction.util';
import { MicrosoftImportDriverException } from 'src/modules/messaging/message-import-manager/drivers/microsoft/exceptions/microsoft-import-driver.exception';
import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface'; import { MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
import { MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message'; import { MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message';
import { formatAddressObjectAsParticipants } from 'src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util'; import { formatAddressObjectAsParticipants } from 'src/modules/messaging/message-import-manager/utils/format-address-object-as-participants.util';
@ -43,7 +44,7 @@ export class MicrosoftGetMessagesService {
return messages; return messages;
} catch (error) { } catch (error) {
this.microsoftHandleErrorService.handleMicrosoftMessageFetchError(error); this.microsoftHandleErrorService.handleMicrosoftGetMessagesError(error);
return []; return [];
} }
@ -69,8 +70,10 @@ export class MicrosoftGetMessagesService {
const messages = parsedResponses.map((response) => { const messages = parsedResponses.map((response) => {
if ('error' in response) { if ('error' in response) {
this.microsoftHandleErrorService.throwMicrosoftBatchError( throw new MicrosoftImportDriverException(
response.error, response.error.message,
response.error.code,
response.error.statusCode,
); );
} }

View File

@ -1,15 +1,33 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { import {
MessageImportDriverException, MessageImportDriverException,
MessageImportDriverExceptionCode, MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { MicrosoftImportDriverException } from 'src/modules/messaging/message-import-manager/drivers/microsoft/exceptions/microsoft-import-driver.exception'; import { isMicrosoftClientTemporaryError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-temporary-error.utils';
import { parseMicrosoftMessagesImportError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/parse-microsoft-messages-import.util';
@Injectable() @Injectable()
export class MicrosoftHandleErrorService { export class MicrosoftHandleErrorService {
private readonly logger = new Logger(MicrosoftHandleErrorService.name);
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
public handleMicrosoftMessageFetchError(error: any): void { public handleMicrosoftMessageFetchByBatchError(error: any): void {
// TODO: remove this log once we catch better the error codes
this.logger.error(`Error temporary (${error.code}) fetching messages`);
this.logger.log(error);
const isBodyString = error.body && typeof error.body === 'string';
const isTemporaryError =
isBodyString && isMicrosoftClientTemporaryError(error.body);
if (isTemporaryError) {
throw new MessageImportDriverException(
`code: ${error.code} - body: ${error.body}`,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
}
if (!error.statusCode) { if (!error.statusCode) {
throw new MessageImportDriverException( throw new MessageImportDriverException(
`Microsoft Graph API unknown error: ${error}`, `Microsoft Graph API unknown error: ${error}`,
@ -17,25 +35,10 @@ export class MicrosoftHandleErrorService {
); );
} }
if (error.statusCode === 401) { const exception = parseMicrosoftMessagesImportError(error);
throw new MessageImportDriverException(
'Unauthorized access to Microsoft Graph API',
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
}
if (error.statusCode === 403) { if (exception) {
throw new MessageImportDriverException( throw exception;
'Forbidden access to Microsoft Graph API',
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
}
if (error.statusCode === 429) {
throw new MessageImportDriverException(
`Microsoft Graph API ${error.code} ${error.statusCode} error: ${error.message}`,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
} }
throw new MessageImportDriverException( throw new MessageImportDriverException(
@ -45,11 +48,44 @@ export class MicrosoftHandleErrorService {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
public throwMicrosoftBatchError(error: any): void { public handleMicrosoftGetMessageListError(error: any): void {
throw new MicrosoftImportDriverException( if (!error.statusCode) {
error.message, throw new MessageImportDriverException(
error.code, `Microsoft Graph API unknown error: ${error}`,
error.statusCode, MessageImportDriverExceptionCode.UNKNOWN,
);
}
const exception = parseMicrosoftMessagesImportError(error);
if (exception) {
throw exception;
}
throw new MessageImportDriverException(
`Microsoft driver error: ${error.message}`,
MessageImportDriverExceptionCode.UNKNOWN,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public handleMicrosoftGetMessagesError(error: any): void {
if (!error.statusCode) {
throw new MessageImportDriverException(
`Microsoft Graph API unknown error: ${error}`,
MessageImportDriverExceptionCode.UNKNOWN,
);
}
const exception = parseMicrosoftMessagesImportError(error);
if (exception) {
throw exception;
}
throw new MessageImportDriverException(
`Microsoft driver error: ${error.message}`,
MessageImportDriverExceptionCode.UNKNOWN,
); );
} }
} }

View File

@ -0,0 +1,51 @@
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
export const parseMicrosoftMessagesImportError = (error: {
statusCode: number;
message?: string;
code?: string;
}): MessageImportDriverException | undefined => {
if (error.statusCode === 401) {
return new MessageImportDriverException(
'Unauthorized access to Microsoft Graph API',
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
}
if (error.statusCode === 403) {
return new MessageImportDriverException(
'Forbidden access to Microsoft Graph API',
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
}
if (error.statusCode === 404) {
if (
error.message?.includes(
'The mailbox is either inactive, soft-deleted, or is hosted on-premise.',
)
) {
return new MessageImportDriverException(
`Disabled, deleted, inactive or no licence Microsoft account - code:${error.code}`,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
} else {
return new MessageImportDriverException(
`Not found - code:${error.code}`,
MessageImportDriverExceptionCode.NOT_FOUND,
);
}
}
if (error.statusCode === 429) {
return new MessageImportDriverException(
`Microsoft Graph API ${error.code} ${error.statusCode} error: ${error.message}`,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,
);
}
return undefined;
};

View File

@ -12,6 +12,7 @@ import {
MessageImportDriverException, MessageImportDriverException,
MessageImportDriverExceptionCode, MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { MessageNetworkExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-network.exception';
import { import {
MessageImportException, MessageImportException,
MessageImportExceptionCode, MessageImportExceptionCode,
@ -51,6 +52,11 @@ export class MessageImportExceptionHandlerService {
); );
break; break;
case MessageImportDriverExceptionCode.TEMPORARY_ERROR: case MessageImportDriverExceptionCode.TEMPORARY_ERROR:
case MessageNetworkExceptionCode.ECONNABORTED:
case MessageNetworkExceptionCode.ENOTFOUND:
case MessageNetworkExceptionCode.ECONNRESET:
case MessageNetworkExceptionCode.ETIMEDOUT:
case MessageNetworkExceptionCode.ERR_NETWORK:
await this.handleTemporaryException( await this.handleTemporaryException(
syncStep, syncStep,
messageChannel, messageChannel,