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,
CalendarEventImportDriverExceptionCode,
} 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 = (
error: GaxiosError,
@ -11,11 +12,11 @@ export const parseGaxiosError = (
const { code } = error;
switch (code) {
case 'ECONNRESET':
case 'ENOTFOUND':
case 'ECONNABORTED':
case 'ETIMEDOUT':
case 'ERR_NETWORK':
case MessageNetworkExceptionCode.ECONNRESET:
case MessageNetworkExceptionCode.ENOTFOUND:
case MessageNetworkExceptionCode.ECONNABORTED:
case MessageNetworkExceptionCode.ETIMEDOUT:
case MessageNetworkExceptionCode.ERR_NETWORK:
return new CalendarEventImportDriverException(
error.message,
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,
MessageImportDriverExceptionCode,
} 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 = (
error: GaxiosError,
@ -11,11 +12,11 @@ export const parseGaxiosError = (
const { code } = error;
switch (code) {
case 'ECONNRESET':
case 'ENOTFOUND':
case 'ECONNABORTED':
case 'ETIMEDOUT':
case 'ERR_NETWORK':
case MessageNetworkExceptionCode.ECONNRESET:
case MessageNetworkExceptionCode.ENOTFOUND:
case MessageNetworkExceptionCode.ECONNABORTED:
case MessageNetworkExceptionCode.ETIMEDOUT:
case MessageNetworkExceptionCode.ERR_NETWORK:
return new MessageImportDriverException(
error.message,
MessageImportDriverExceptionCode.TEMPORARY_ERROR,

View File

@ -1,16 +1,16 @@
import { Injectable, Logger } from '@nestjs/common';
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 { 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()
export class MicrosoftFetchByBatchService {
private readonly logger = new Logger(MicrosoftFetchByBatchService.name);
constructor(
private readonly microsoftClientProvider: MicrosoftClientProvider,
private readonly microsoftHandleErrorService: MicrosoftHandleErrorService,
) {}
async fetchAllByBatches(
@ -52,25 +52,9 @@ export class MicrosoftFetchByBatchService {
batchResponses.push(batchResponse);
} catch (error) {
if (
error.body &&
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;
}
this.microsoftHandleErrorService.handleMicrosoftMessageFetchByBatchError(
error,
);
}
}

View File

@ -77,7 +77,12 @@ export class MicrosoftGetMessageListService {
.headers({
Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}, IdType="ImmutableId"`,
})
.get();
.get()
.catch((error) => {
this.microsoftHandleErrorService.handleMicrosoftGetMessageListError(
error,
);
});
const callback: PageIteratorCallback = (data) => {
messageExternalIds.push(data.id);
@ -92,7 +97,9 @@ export class MicrosoftGetMessageListService {
});
await pageIterator.iterate().catch((error) => {
this.microsoftHandleErrorService.handleMicrosoftMessageFetchError(error);
this.microsoftHandleErrorService.handleMicrosoftGetMessageListError(
error,
);
});
return {
@ -195,7 +202,12 @@ export class MicrosoftGetMessageListService {
.headers({
Prefer: `odata.maxpagesize=${MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT}, IdType="ImmutableId"`,
})
.get();
.get()
.catch((error) => {
this.microsoftHandleErrorService.handleMicrosoftGetMessageListError(
error,
);
});
const callback: PageIteratorCallback = (data) => {
if (data['@removed']) {
@ -214,7 +226,9 @@ export class MicrosoftGetMessageListService {
});
await pageIterator.iterate().catch((error) => {
this.microsoftHandleErrorService.handleMicrosoftMessageFetchError(error);
this.microsoftHandleErrorService.handleMicrosoftGetMessageListError(
error,
);
});
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 { 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 { 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';
@ -43,7 +44,7 @@ export class MicrosoftGetMessagesService {
return messages;
} catch (error) {
this.microsoftHandleErrorService.handleMicrosoftMessageFetchError(error);
this.microsoftHandleErrorService.handleMicrosoftGetMessagesError(error);
return [];
}
@ -69,8 +70,10 @@ export class MicrosoftGetMessagesService {
const messages = parsedResponses.map((response) => {
if ('error' in response) {
this.microsoftHandleErrorService.throwMicrosoftBatchError(
response.error,
throw new MicrosoftImportDriverException(
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 {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} 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()
export class MicrosoftHandleErrorService {
private readonly logger = new Logger(MicrosoftHandleErrorService.name);
// 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) {
throw new MessageImportDriverException(
`Microsoft Graph API unknown error: ${error}`,
@ -17,25 +35,10 @@ export class MicrosoftHandleErrorService {
);
}
if (error.statusCode === 401) {
throw new MessageImportDriverException(
'Unauthorized access to Microsoft Graph API',
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
);
}
const exception = parseMicrosoftMessagesImportError(error);
if (error.statusCode === 403) {
throw new MessageImportDriverException(
'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,
);
if (exception) {
throw exception;
}
throw new MessageImportDriverException(
@ -45,11 +48,44 @@ export class MicrosoftHandleErrorService {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public throwMicrosoftBatchError(error: any): void {
throw new MicrosoftImportDriverException(
error.message,
error.code,
error.statusCode,
public handleMicrosoftGetMessageListError(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,
);
}
// 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,
MessageImportDriverExceptionCode,
} 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 {
MessageImportException,
MessageImportExceptionCode,
@ -51,6 +52,11 @@ export class MessageImportExceptionHandlerService {
);
break;
case MessageImportDriverExceptionCode.TEMPORARY_ERROR:
case MessageNetworkExceptionCode.ECONNABORTED:
case MessageNetworkExceptionCode.ENOTFOUND:
case MessageNetworkExceptionCode.ECONNRESET:
case MessageNetworkExceptionCode.ETIMEDOUT:
case MessageNetworkExceptionCode.ERR_NETWORK:
await this.handleTemporaryException(
syncStep,
messageChannel,