diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts index 3a7346b741..cf3d604768 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts @@ -1,4 +1,3 @@ -import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; @@ -11,7 +10,6 @@ import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects import { EmailAliasManagerModule } from 'src/modules/connected-account/email-alias-manager/email-alias-manager.module'; import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; -import { GmailFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-fetch-by-batch.service'; import { GmailGetHistoryService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service'; import { GmailGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service'; import { GmailGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service'; @@ -21,9 +19,6 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p @Module({ imports: [ - HttpModule.register({ - baseURL: 'https://www.googleapis.com/batch/gmail/v1', - }), TwentyConfigModule, ObjectMetadataRepositoryModule.forFeature([BlocklistWorkspaceEntity]), MessagingCommonModule, @@ -36,7 +31,6 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p ], providers: [ GmailGetHistoryService, - GmailFetchByBatchService, GmailGetMessagesService, GmailGetMessageListService, GmailMessageListFetchErrorHandler, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-batch-api-error-mocks.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-batch-api-error-mocks.ts deleted file mode 100644 index 336987e321..0000000000 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-batch-api-error-mocks.ts +++ /dev/null @@ -1,217 +0,0 @@ -import { type GmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type'; - -const gmailBatchApiErrorMocks = { - // 400 Bad Request - Invalid query parameters - badRequest: { - code: 400, - errors: [ - { - domain: 'global', - location: 'orderBy', - locationType: 'parameter', - message: - 'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.', - reason: 'badRequest', - }, - ], - message: - 'Sorting is not supported for queries with fullText terms. Results are always in descending relevance order.', - }, - - // 400 Invalid Grant - invalidGrant: { - code: 400, - errors: [ - { - domain: 'global', - reason: 'invalid_grant', - message: 'Invalid Credentials', - }, - ], - message: 'Invalid Credentials', - }, - - // 400 Failed Precondition - failedPrecondition: { - code: 400, - errors: [ - { - domain: 'global', - reason: 'failedPrecondition', - message: 'Failed Precondition', - }, - ], - message: 'Failed Precondition', - }, - - // 401 Invalid Credentials - invalidCredentials: { - errors: [ - { - domain: 'global', - reason: 'authError', - message: 'Invalid Credentials', - locationType: 'header', - location: 'Authorization', - }, - ], - code: 401, - message: 'Invalid Credentials', - }, - - // 404 Not Found - notFound: { - errors: [ - { - domain: 'global', - reason: 'notFound', - message: 'Resource not found: userId', - location: 'userId', - locationType: 'parameter', - }, - ], - code: 404, - message: 'Resource not found: userId', - }, - - // 410 Gone - gone: { - errors: [ - { - domain: 'global', - reason: 'resourceGone', - message: 'Resource has been deleted', - location: 'messageId', - locationType: 'parameter', - }, - ], - code: 410, - message: 'Resource has been deleted', - }, - - // 403 Daily Limit Exceeded - dailyLimitExceeded: { - errors: [ - { - domain: 'usageLimits', - reason: 'dailyLimitExceeded', - message: 'Daily Limit Exceeded', - }, - ], - code: 403, - message: 'Daily Limit Exceeded', - }, - - // 403 User Rate Limit Exceeded - userRateLimitExceeded: { - errors: [ - { - domain: 'usageLimits', - reason: 'userRateLimitExceeded', - message: 'User Rate Limit Exceeded', - }, - ], - code: 403, - message: 'User Rate Limit Exceeded', - }, - - // 403 Rate Limit Exceeded - rateLimitExceeded: { - errors: [ - { - domain: 'usageLimits', - reason: 'rateLimitExceeded', - message: 'Rate Limit Exceeded', - }, - ], - code: 403, - message: 'Rate Limit Exceeded', - }, - - // 403 Domain Policy Error - domainPolicyError: { - errors: [ - { - domain: 'global', - reason: 'domainPolicy', - message: 'The domain administrators have disabled Gmail apps.', - }, - ], - code: 403, - message: 'The domain administrators have disabled Gmail apps.', - }, - - // 429 Too Many Requests (Concurrent Requests) - tooManyConcurrentRequests: { - errors: [ - { - domain: 'global', - reason: 'rateLimitExceeded', - message: 'Too many concurrent requests for user', - }, - ], - code: 429, - message: 'Too many concurrent requests for user', - }, - - // 500 Backend Error - backendError: { - errors: [ - { - domain: 'global', - reason: 'backendError', - message: 'Backend Error', - }, - ], - code: 500, - message: 'Backend Error', - }, - - getError: function (code: number, type?: string): GmailApiBatchError { - switch (code) { - case 400: - switch (type) { - case 'invalid_grant': - return this.invalidGrant; - case 'failedPrecondition': - return this.failedPrecondition; - default: - return this.badRequest; - } - case 401: - return this.invalidCredentials; - case 403: - switch (type) { - case 'dailyLimit': - return this.dailyLimitExceeded; - case 'userRateLimit': - return this.userRateLimitExceeded; - case 'rateLimit': - return this.rateLimitExceeded; - case 'domainPolicy': - return this.domainPolicyError; - default: - return this.rateLimitExceeded; - } - case 404: - return this.notFound; - case 410: - return this.gone; - case 429: - switch (type) { - case 'concurrent': - return this.tooManyConcurrentRequests; - case 'mailSending': - return this.mailSendingLimitExceeded; - default: - return this.tooManyConcurrentRequests; - } - case 500: - return this.backendError; - default: - throw new Error(`Unknown error code: ${code}`); - } - }, -}; - -export default gmailBatchApiErrorMocks; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-fetch-by-batch.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-fetch-by-batch.service.ts deleted file mode 100644 index b146c8f4e3..0000000000 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-fetch-by-batch.service.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { HttpService } from '@nestjs/axios'; -import { Injectable } from '@nestjs/common'; - -import { type AxiosResponse } from 'axios'; - -import { type GmailMessageParsedResponse } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message-parsed-response.type'; -import { createQueriesFromMessageIds } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/create-queries-from-message-ids.util'; -import { type BatchQueries } from 'src/modules/messaging/message-import-manager/types/batch-queries'; - -@Injectable() -export class GmailFetchByBatchService { - constructor(private readonly httpService: HttpService) {} - - async fetchAllByBatches( - messageIds: string[], - accessToken: string, - boundary: string, - ): Promise<{ - messageIdsByBatch: string[][]; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - batchResponses: AxiosResponse[]; - }> { - const batchLimit = 20; - - let batchOffset = 0; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let batchResponses: AxiosResponse[] = []; - - const messageIdsByBatch: string[][] = []; - - while (batchOffset < messageIds.length) { - const batchResponse = await this.fetchBatch( - messageIds, - accessToken, - batchOffset, - batchLimit, - boundary, - ); - - batchResponses = batchResponses.concat(batchResponse); - - messageIdsByBatch.push( - messageIds.slice(batchOffset, batchOffset + batchLimit), - ); - - batchOffset += batchLimit; - } - - return { messageIdsByBatch, batchResponses }; - } - - async fetchBatch( - messageIds: string[], - accessToken: string, - batchOffset: number, - batchLimit: number, - boundary: string, - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ): Promise> { - const queries = createQueriesFromMessageIds(messageIds); - - const limitedQueries = queries.slice(batchOffset, batchOffset + batchLimit); - - const response = await this.httpService.axiosRef.post( - '/', - this.createBatchBody(limitedQueries, boundary), - { - headers: { - 'Content-Type': 'multipart/mixed; boundary=' + boundary, - Authorization: 'Bearer ' + accessToken, - }, - }, - ); - - return response; - } - - createBatchBody(queries: BatchQueries, boundary: string): string { - let batchBody: string[] = []; - - queries.forEach(function (call) { - const method = 'GET'; - const uri = call.uri; - - batchBody = batchBody.concat([ - '--', - boundary, - '\r\n', - 'Content-Type: application/http', - '\r\n\r\n', - - method, - ' ', - uri, - '\r\n\r\n', - ]); - }); - - return batchBody.concat(['--', boundary, '--']).join(''); - } - - parseBatch( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - responseCollection: AxiosResponse, - ): GmailMessageParsedResponse[] { - const responseItems: GmailMessageParsedResponse[] = []; - - const boundary = this.getBatchSeparator(responseCollection); - - const responseLines: string[] = responseCollection.data.split( - '--' + boundary, - ); - - responseLines.forEach(function (response) { - const startJson = response.indexOf('{'); - const endJson = response.lastIndexOf('}'); - - if (startJson < 0 || endJson < 0) return; - - const responseJson = response.substring(startJson, endJson + 1); - - const item = JSON.parse(responseJson); - - responseItems.push(item); - }); - - return responseItems; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - getBatchSeparator(responseCollection: AxiosResponse): string { - const headers = responseCollection.headers; - - const contentType: string = headers['content-type']; - - if (!contentType) return ''; - - const components = contentType.split('; '); - - const boundary = components.find((item) => item.startsWith('boundary=')); - - return boundary?.replace('boundary=', '').trim() || ''; - } -} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts index 9ea95fee42..38a8249cbc 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-messages.service.ts @@ -1,23 +1,21 @@ import { Injectable } from '@nestjs/common'; -import { type AxiosResponse } from 'axios'; -import { type gmail_v1 as gmailV1 } from 'googleapis'; +import { batchFetchImplementation } from '@jrmdayn/googleapis-batcher'; +import { type gmail_v1 as gmailV1, google } from 'googleapis'; import { isDefined } from 'twenty-shared/utils'; +import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { - MessageImportDriverException, - MessageImportDriverExceptionCode, -} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; -import { GmailFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-fetch-by-batch.service'; import { GmailMessagesImportErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service'; import { parseAndFormatGmailMessage } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-and-format-gmail-message.util'; import { type MessageWithParticipants } from 'src/modules/messaging/message-import-manager/types/message'; +const GMAIL_BATCH_REQUEST_MAX_SIZE = 50; + @Injectable() export class GmailGetMessagesService { constructor( - private readonly fetchByBatchesService: GmailFetchByBatchService, + private readonly oAuth2ClientManagerService: OAuth2ClientManagerService, private readonly gmailMessagesImportErrorHandler: GmailMessagesImportErrorHandler, ) {} @@ -25,62 +23,56 @@ export class GmailGetMessagesService { messageIds: string[], connectedAccount: Pick< ConnectedAccountWorkspaceEntity, - 'accessToken' | 'id' | 'handle' | 'handleAliases' + | 'provider' + | 'accessToken' + | 'refreshToken' + | 'id' + | 'handle' + | 'handleAliases' >, ): Promise { - if (!isDefined(connectedAccount.accessToken)) { - throw new MessageImportDriverException( - 'Access token is required', - MessageImportDriverExceptionCode.ACCESS_TOKEN_MISSING, - ); - } - const { messageIdsByBatch, batchResponses } = - await this.fetchByBatchesService.fetchAllByBatches( - messageIds, - connectedAccount.accessToken, - 'batch_gmail_messages', - ); - - const messages = batchResponses.flatMap((response, index) => { - return this.formatBatchResponseAsMessage( - messageIdsByBatch[index], - response, + const oAuth2Client = + await this.oAuth2ClientManagerService.getGoogleOAuth2Client( connectedAccount, ); + + const batchedFetchImplementation = batchFetchImplementation({ + maxBatchSize: GMAIL_BATCH_REQUEST_MAX_SIZE, }); + const batchedGmailClient = google.gmail({ + version: 'v1', + auth: oAuth2Client, + fetchImplementation: batchedFetchImplementation, + }); + + const messagePromises = messageIds.map((messageId) => + batchedGmailClient.users.messages + .get({ + userId: 'me', + id: messageId, + }) + .then((response) => ({ messageId, data: response.data, error: null })) + .catch((error) => ({ messageId, data: null, error })), + ); + + const results = await Promise.all(messagePromises); + + const messages = results + .map(({ messageId, data, error }) => { + if (error) { + this.gmailMessagesImportErrorHandler.handleError(error, messageId); + + return undefined; + } + + return parseAndFormatGmailMessage( + data as gmailV1.Schema$Message, + connectedAccount, + ); + }) + .filter(isDefined); + return messages; } - - private formatBatchResponseAsMessage( - messageIds: string[], - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - responseCollection: AxiosResponse, - connectedAccount: Pick< - ConnectedAccountWorkspaceEntity, - 'handle' | 'handleAliases' - >, - ): MessageWithParticipants[] { - const parsedResponses = - this.fetchByBatchesService.parseBatch(responseCollection); - - const messages = parsedResponses.map((response, index) => { - if ('error' in response) { - this.gmailMessagesImportErrorHandler.handleError( - response.error, - messageIds[index], - ); - - return undefined; - } - - return parseAndFormatGmailMessage( - response as gmailV1.Schema$Message, - connectedAccount, - ); - }); - - return messages.filter(isDefined); - } } diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service.ts index dff97e887a..8ea963b77d 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service.ts @@ -1,14 +1,12 @@ import { Injectable, Logger } from '@nestjs/common'; -import { isDefined } from 'twenty-shared/utils'; - import { MessageImportDriverException, MessageImportDriverExceptionCode, } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; -import { isGmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-batch-error.util'; +import { isGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-error.util'; import { isGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-network-error.util'; -import { parseGmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-batch-error.util'; +import { parseGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-error.util'; import { parseGmailNetworkError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-network-error.util'; @Injectable() @@ -19,21 +17,22 @@ export class GmailMessagesImportErrorHandler { public handleError(error: unknown, messageExternalId: string): void { this.logger.error( - `Gmail: Error importing messages: ${JSON.stringify(error)}`, + `Gmail: Error importing message ${messageExternalId}: ${JSON.stringify(error)}`, ); if (isGmailNetworkError(error)) { throw parseGmailNetworkError(error); } - if (isGmailApiBatchError(error)) { - const exception = parseGmailApiBatchError(error, messageExternalId); + if (isGmailApiError(error)) { + const status = error.response?.status; - if (!isDefined(exception)) { + // 404/410 means message was deleted - skip silently + if (status === 404 || status === 410) { return; } - throw exception; + throw parseGmailApiError(error); } throw new MessageImportDriverException( diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type.ts deleted file mode 100644 index 95490c4f17..0000000000 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type GmailApiBatchError = { - code: number; - errors: { - reason: string; - message: string; - }[]; -}; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message-parsed-response.type.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message-parsed-response.type.ts deleted file mode 100644 index 134bc9e70c..0000000000 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-message-parsed-response.type.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { type gmail_v1 } from 'googleapis'; - -type GmailMessageError = { - error: { - code: number; - message: string; - status: string; - }; -}; - -export type GmailMessageParsedResponse = - | gmail_v1.Schema$Message - | GmailMessageError; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-messages-import-error.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-messages-import-error.spec.ts index bd1c44455e..acdfd0b68c 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-messages-import-error.spec.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/__tests__/parse-gmail-messages-import-error.spec.ts @@ -1,149 +1,125 @@ -import { - MessageImportDriverException, - MessageImportDriverExceptionCode, -} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; -import { default as gmailBatchApiErrorMocks } from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-batch-api-error-mocks'; -import { parseGmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-batch-error.util'; +import { MessageImportDriverExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; +import { getGmailApiError } from 'src/modules/messaging/message-import-manager/drivers/gmail/mocks/gmail-api-error-mocks'; +import { GmailMessagesImportErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-messages-import-error-handler.service'; const messageExternalId = '123'; -describe('parseGmailApiBatchError', () => { - it('should handle 400 Bad Request', () => { - const error = gmailBatchApiErrorMocks.getError(400); - const exception = parseGmailApiBatchError(error, messageExternalId); +describe('GmailMessagesImportErrorHandler', () => { + let handler: GmailMessagesImportErrorHandler; - expect(exception).toBeInstanceOf(MessageImportDriverException); - expect(exception?.code).toBe(MessageImportDriverExceptionCode.UNKNOWN); - expect(exception?.message).toBe( - `${error.errors[0].message} for message with externalId: ${messageExternalId}`, + beforeEach(() => { + handler = new GmailMessagesImportErrorHandler(); + }); + + it('should handle 400 Bad Request', () => { + const error = getGmailApiError({ code: 400 }); + + expect(() => handler.handleError(error, messageExternalId)).toThrow( + expect.objectContaining({ + code: MessageImportDriverExceptionCode.UNKNOWN, + }), ); }); it('should handle 400 Invalid Grant', () => { - const error = gmailBatchApiErrorMocks.getError(400, 'invalid_grant'); - const exception = parseGmailApiBatchError(error, messageExternalId); + const error = getGmailApiError({ code: 400, reason: 'invalid_grant' }); - expect(exception).toBeInstanceOf(MessageImportDriverException); - expect(exception?.code).toBe( - MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, - ); - expect(exception?.message).toBe( - `${error.errors[0].message} for message with externalId: ${messageExternalId}`, + expect(() => handler.handleError(error, messageExternalId)).toThrow( + expect.objectContaining({ + code: MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + }), ); }); it('should handle 400 Failed Precondition', () => { - const error = gmailBatchApiErrorMocks.getError(400, 'failedPrecondition'); - const exception = parseGmailApiBatchError(error, messageExternalId); + const error = getGmailApiError({ code: 400, reason: 'failedPrecondition' }); - expect(exception).toBeInstanceOf(MessageImportDriverException); - expect(exception?.code).toBe( - MessageImportDriverExceptionCode.TEMPORARY_ERROR, + expect(() => handler.handleError(error, messageExternalId)).toThrow( + expect.objectContaining({ + code: MessageImportDriverExceptionCode.TEMPORARY_ERROR, + }), ); }); it('should handle 401 Invalid Credentials', () => { - const error = gmailBatchApiErrorMocks.getError(401); - const exception = parseGmailApiBatchError(error, messageExternalId); + const error = getGmailApiError({ code: 401 }); - expect(exception).toBeInstanceOf(MessageImportDriverException); - expect(exception?.code).toBe( - MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, - ); - expect(exception?.message).toBe( - `${error.errors[0].message} for message with externalId: ${messageExternalId}`, + expect(() => handler.handleError(error, messageExternalId)).toThrow( + expect.objectContaining({ + code: MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + }), ); }); it('should handle 403 Daily Limit Exceeded', () => { - const error = gmailBatchApiErrorMocks.getError(403, 'dailyLimit'); - const exception = parseGmailApiBatchError(error, messageExternalId); + const error = getGmailApiError({ code: 403, reason: 'dailyLimit' }); - expect(exception).toBeInstanceOf(MessageImportDriverException); - expect(exception?.code).toBe( - MessageImportDriverExceptionCode.TEMPORARY_ERROR, - ); - expect(exception?.message).toBe( - `${error.errors[0].message} for message with externalId: ${messageExternalId}`, + expect(() => handler.handleError(error, messageExternalId)).toThrow( + expect.objectContaining({ + code: MessageImportDriverExceptionCode.TEMPORARY_ERROR, + }), ); }); it('should handle 403 User Rate Limit Exceeded', () => { - const error = gmailBatchApiErrorMocks.getError(403, 'userRateLimit'); - const exception = parseGmailApiBatchError(error, messageExternalId); + const error = getGmailApiError({ code: 403, reason: 'userRateLimit' }); - expect(exception).toBeInstanceOf(MessageImportDriverException); - expect(exception?.code).toBe( - MessageImportDriverExceptionCode.TEMPORARY_ERROR, - ); - expect(exception?.message).toBe( - `${error.errors[0].message} for message with externalId: ${messageExternalId}`, + expect(() => handler.handleError(error, messageExternalId)).toThrow( + expect.objectContaining({ + code: MessageImportDriverExceptionCode.TEMPORARY_ERROR, + }), ); }); it('should handle 403 Rate Limit Exceeded', () => { - const error = gmailBatchApiErrorMocks.getError(403, 'rateLimit'); - const exception = parseGmailApiBatchError(error, messageExternalId); + const error = getGmailApiError({ code: 403, reason: 'rateLimit' }); - expect(exception).toBeInstanceOf(MessageImportDriverException); - expect(exception?.code).toBe( - MessageImportDriverExceptionCode.TEMPORARY_ERROR, - ); - expect(exception?.message).toBe( - `${error.errors[0].message} for message with externalId: ${messageExternalId}`, + expect(() => handler.handleError(error, messageExternalId)).toThrow( + expect.objectContaining({ + code: MessageImportDriverExceptionCode.TEMPORARY_ERROR, + }), ); }); it('should handle 403 Domain Policy Error', () => { - const error = gmailBatchApiErrorMocks.getError(403, 'domainPolicy'); - const exception = parseGmailApiBatchError(error, messageExternalId); + const error = getGmailApiError({ code: 403, reason: 'domainPolicy' }); - expect(exception).toBeInstanceOf(MessageImportDriverException); - expect(exception?.code).toBe( - MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, - ); - expect(exception?.message).toBe( - `${error.errors[0].message} for message with externalId: ${messageExternalId}`, + expect(() => handler.handleError(error, messageExternalId)).toThrow( + expect.objectContaining({ + code: MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, + }), ); }); - it('should handle 404 Not Found', () => { - const error = gmailBatchApiErrorMocks.getError(404); - const exception = parseGmailApiBatchError(error, messageExternalId); + it('should handle 404 Not Found by returning silently', () => { + const error = getGmailApiError({ code: 404 }); - expect(exception).toBeUndefined(); + expect(() => handler.handleError(error, messageExternalId)).not.toThrow(); }); - it('should handle 410 Gone', () => { - const error = gmailBatchApiErrorMocks.getError(410); - const exception = parseGmailApiBatchError(error, messageExternalId); + it('should handle 410 Gone by returning silently', () => { + const error = getGmailApiError({ code: 410 }); - expect(exception).toBeUndefined(); + expect(() => handler.handleError(error, messageExternalId)).not.toThrow(); }); it('should handle 429 Too Many Requests', () => { - const error = gmailBatchApiErrorMocks.getError(429, 'concurrent'); - const exception = parseGmailApiBatchError(error, messageExternalId); + const error = getGmailApiError({ code: 429 }); - expect(exception).toBeInstanceOf(MessageImportDriverException); - expect(exception?.code).toBe( - MessageImportDriverExceptionCode.TEMPORARY_ERROR, - ); - expect(exception?.message).toBe( - `${error.errors[0].message} for message with externalId: ${messageExternalId}`, + expect(() => handler.handleError(error, messageExternalId)).toThrow( + expect.objectContaining({ + code: MessageImportDriverExceptionCode.TEMPORARY_ERROR, + }), ); }); it('should handle 500 Backend Error', () => { - const error = gmailBatchApiErrorMocks.getError(500); - const exception = parseGmailApiBatchError(error, messageExternalId); + const error = getGmailApiError({ code: 500 }); - expect(exception).toBeInstanceOf(MessageImportDriverException); - expect(exception?.code).toBe( - MessageImportDriverExceptionCode.TEMPORARY_ERROR, - ); - expect(exception?.message).toBe( - `${error.errors[0].message} for message with externalId: ${messageExternalId}`, + expect(() => handler.handleError(error, messageExternalId)).toThrow( + expect.objectContaining({ + code: MessageImportDriverExceptionCode.TEMPORARY_ERROR, + }), ); }); }); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/create-queries-from-message-ids.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/create-queries-from-message-ids.util.ts deleted file mode 100644 index e3e57078b7..0000000000 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/create-queries-from-message-ids.util.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type MessageQuery } from 'src/modules/messaging/message-import-manager/types/message-or-thread-query'; - -export const createQueriesFromMessageIds = ( - messageExternalIds: string[], -): MessageQuery[] => { - return messageExternalIds.map((messageId) => ({ - uri: '/gmail/v1/users/me/messages/' + messageId + '?format=FULL', - })); -}; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-batch-error.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-batch-error.util.ts deleted file mode 100644 index 324652a21a..0000000000 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/is-gmail-api-batch-error.util.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { type GmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type'; - -export const isGmailApiBatchError = ( - error: unknown, -): error is GmailApiBatchError => { - if (error === null || typeof error !== 'object') { - return false; - } - - if ( - !('code' in error) || - !('errors' in error) || - !Array.isArray(error.errors) || - error.errors.length === 0 || - error.errors.some((error) => !('reason' in error) || !('message' in error)) - ) { - return false; - } - - return true; -}; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-batch-error.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-batch-error.util.ts deleted file mode 100644 index f2d9755bfd..0000000000 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/utils/parse-gmail-api-batch-error.util.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { - MessageImportDriverException, - MessageImportDriverExceptionCode, -} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception'; -import { type GmailApiBatchError } from 'src/modules/messaging/message-import-manager/drivers/gmail/types/gmail-api-batch-error.type'; - -export const parseGmailApiBatchError = ( - error: GmailApiBatchError, - messageExternalId?: string, -): MessageImportDriverException | undefined => { - const { code, errors } = error; - - const reason = errors?.[0]?.reason; - const originalMessage = errors?.[0]?.message; - const message = `${errors?.[0]?.message} for message with externalId: ${messageExternalId}`; - - switch (code) { - case 400: - if (reason === 'invalid_grant') { - return new MessageImportDriverException( - message, - MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, - ); - } - if (reason === 'failedPrecondition') { - if (originalMessage.includes('Mail service not enabled')) { - return new MessageImportDriverException( - message, - MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, - ); - } - - return new MessageImportDriverException( - message, - MessageImportDriverExceptionCode.TEMPORARY_ERROR, - ); - } - - return new MessageImportDriverException( - message, - MessageImportDriverExceptionCode.UNKNOWN, - ); - - case 404: - case 410: - return undefined; - - case 429: - return new MessageImportDriverException( - message, - MessageImportDriverExceptionCode.TEMPORARY_ERROR, - ); - - case 403: - if ( - reason === 'rateLimitExceeded' || - reason === 'userRateLimitExceeded' || - reason === 'dailyLimitExceeded' - ) { - return new MessageImportDriverException( - message, - MessageImportDriverExceptionCode.TEMPORARY_ERROR, - ); - } - if (reason === 'domainPolicy') { - return new MessageImportDriverException( - message, - MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, - ); - } - - break; - - case 401: - return new MessageImportDriverException( - message, - MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS, - ); - - case 503: - return new MessageImportDriverException( - message, - MessageImportDriverExceptionCode.TEMPORARY_ERROR, - ); - - case 500: - case 502: - case 504: - if (reason === 'backendError') { - return new MessageImportDriverException( - message, - MessageImportDriverExceptionCode.TEMPORARY_ERROR, - ); - } - - if (errors?.[0]?.message.includes(`Authentication backend unavailable`)) { - return new MessageImportDriverException( - `${code} - ${reason} - ${message}`, - MessageImportDriverExceptionCode.TEMPORARY_ERROR, - ); - } - break; - - default: - break; - } - - return new MessageImportDriverException( - message, - MessageImportDriverExceptionCode.UNKNOWN, - ); -}; diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/types/batch-queries.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/types/batch-queries.ts deleted file mode 100644 index c072a6e718..0000000000 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/types/batch-queries.ts +++ /dev/null @@ -1,5 +0,0 @@ -type Query = { - uri: string; -}; - -export type BatchQueries = Query[];