Reuse OAuth access tokens (#15089)

This commit is contained in:
neo773
2025-10-21 15:08:49 +05:30
committed by GitHub
parent 793dc3d6fc
commit c66261a38b
49 changed files with 632 additions and 421 deletions
@@ -5,9 +5,9 @@ import {
MessageFolderDriver,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
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 { MESSAGING_GMAIL_DEFAULT_NOT_SYNCED_LABELS } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-default-not-synced-labels';
import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
import { GmailHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-handle-error.service';
@Injectable()
@@ -15,7 +15,7 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
private readonly logger = new Logger(GmailGetAllFoldersService.name);
constructor(
private readonly gmailClientProvider: GmailClientProvider,
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
private readonly gmailHandleErrorService: GmailHandleErrorService,
) {}
@@ -26,12 +26,16 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
async getAllMessageFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken' | 'id' | 'handle'
'provider' | 'refreshToken' | 'accessToken' | 'id' | 'handle'
>,
): Promise<MessageFolder[]> {
try {
const gmailClient =
await this.gmailClientProvider.getGmailClient(connectedAccount);
const oAuth2Client =
await this.oAuth2ClientManagerService.getGoogleOAuth2Client(
connectedAccount,
);
const gmailClient = oAuth2Client.gmail({ version: 'v1' });
const response = await gmailClient.users.labels
.list({ userId: 'me' })
@@ -5,8 +5,8 @@ import {
MessageFolderDriver,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
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 { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
import { MicrosoftHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-handle-error.service';
import { StandardFolder } from 'src/modules/messaging/message-import-manager/drivers/types/standard-folder';
import { getStandardFolderByRegex } from 'src/modules/messaging/message-import-manager/drivers/utils/get-standard-folder-by-regex';
@@ -25,19 +25,22 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
private readonly logger = new Logger(MicrosoftGetAllFoldersService.name);
constructor(
private readonly microsoftClientProvider: MicrosoftClientProvider,
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
private readonly microsoftHandleErrorService: MicrosoftHandleErrorService,
) {}
async getAllMessageFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'refreshToken' | 'id' | 'handle'
'accessToken' | 'refreshToken' | 'id' | 'handle' | 'provider'
>,
): Promise<MessageFolder[]> {
try {
const microsoftClient =
await this.microsoftClientProvider.getMicrosoftClient(connectedAccount);
await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client(
connectedAccount,
);
const response = await microsoftClient
.api('/me/mailFolders')
@@ -10,7 +10,12 @@ export interface MessageFolderDriver {
getAllMessageFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken' | 'id' | 'handle' | 'connectionParameters'
| 'provider'
| 'accessToken'
| 'refreshToken'
| 'id'
| 'handle'
| 'connectionParameters'
>,
): Promise<MessageFolder[]>;
}
@@ -5,6 +5,7 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module';
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/imap-get-all-folders.service';
@@ -19,6 +20,7 @@ import { MessagingMicrosoftDriverModule } from 'src/modules/messaging/message-im
WorkspaceDataSourceModule,
DataSourceModule,
TypeOrmModule.forFeature([Workspace]),
OAuth2ClientManagerModule,
MessagingGmailDriverModule,
MessagingMicrosoftDriverModule,
MessagingIMAPDriverModule,
@@ -11,8 +11,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 { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
import { OAuth2ClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/oauth2-client.provider';
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';
@@ -36,8 +34,6 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p
MessageParticipantManagerModule,
],
providers: [
GmailClientProvider,
OAuth2ClientProvider,
GmailGetHistoryService,
GmailFetchByBatchService,
GmailGetMessagesService,
@@ -47,8 +43,7 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p
exports: [
GmailGetMessagesService,
GmailGetMessageListService,
GmailClientProvider,
OAuth2ClientProvider,
GmailHandleErrorService,
],
})
@@ -1,30 +0,0 @@
import { Injectable } from '@nestjs/common';
import { type gmail_v1, google } from 'googleapis';
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';
@Injectable()
export class GmailClientProvider {
constructor(
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
) {}
public async getGmailClient(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken'
>,
): Promise<gmail_v1.Gmail> {
const oAuth2Client =
await this.oAuth2ClientManagerService.getOAuth2Client(connectedAccount);
const gmailClient = google.gmail({
version: 'v1',
auth: oAuth2Client,
});
return gmailClient;
}
}
@@ -1,28 +0,0 @@
import { Injectable } from '@nestjs/common';
import { type oauth2_v2, google } from 'googleapis';
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';
@Injectable()
export class OAuth2ClientProvider {
constructor(
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
) {}
public async getOAuth2Client(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken'
>,
): Promise<oauth2_v2.Oauth2> {
const oAuth2Client =
await this.oAuth2ClientManagerService.getOAuth2Client(connectedAccount);
return google.oauth2({
version: 'v2',
auth: oAuth2Client,
});
}
}
@@ -2,23 +2,29 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { ConnectedAccountProvider } from 'twenty-shared/types';
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 { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
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 { GmailHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-handle-error.service';
describe('GmailGetMessageListService', () => {
let service: GmailGetMessageListService;
let gmailClientProvider: GmailClientProvider;
let oAuth2ClientManagerService: OAuth2ClientManagerService;
const mockConnectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken' | 'id' | 'handle' | 'connectionParameters'
| 'provider'
| 'accessToken'
| 'refreshToken'
| 'id'
| 'handle'
| 'connectionParameters'
> = {
id: 'connected-account-id',
provider: ConnectedAccountProvider.GOOGLE,
refreshToken: 'refresh-token',
accessToken: 'access-token',
refreshToken: 'refresh-token', // dummy value for testing
handle: 'test@gmail.com',
connectionParameters: {},
};
@@ -28,9 +34,9 @@ describe('GmailGetMessageListService', () => {
providers: [
GmailGetMessageListService,
{
provide: GmailClientProvider,
provide: OAuth2ClientManagerService,
useValue: {
getGmailClient: jest.fn(),
getGoogleOAuth2Client: jest.fn(),
},
},
{
@@ -53,7 +59,9 @@ describe('GmailGetMessageListService', () => {
service = module.get<GmailGetMessageListService>(
GmailGetMessageListService,
);
gmailClientProvider = module.get<GmailClientProvider>(GmailClientProvider);
oAuth2ClientManagerService = module.get<OAuth2ClientManagerService>(
OAuth2ClientManagerService,
);
});
describe('getMessageList', () => {
@@ -71,9 +79,13 @@ describe('GmailGetMessageListService', () => {
},
};
(gmailClientProvider.getGmailClient as jest.Mock).mockResolvedValue(
mockGmailClient,
);
const mockOAuth2Client = {
gmail: jest.fn().mockReturnValue(mockGmailClient),
};
(
oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock
).mockResolvedValue(mockOAuth2Client);
const result = await service.getMessageLists({
messageChannel: { syncCursor: '', id: 'my-id' },
@@ -122,9 +134,13 @@ describe('GmailGetMessageListService', () => {
},
};
(gmailClientProvider.getGmailClient as jest.Mock).mockResolvedValue(
mockGmailClient,
);
const mockOAuth2Client = {
gmail: jest.fn().mockReturnValue(mockGmailClient),
};
(
oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock
).mockResolvedValue(mockOAuth2Client);
const result = await service.getMessageLists({
messageChannel: { syncCursor: '', id: 'my-id' },
@@ -173,9 +189,13 @@ describe('GmailGetMessageListService', () => {
},
};
(gmailClientProvider.getGmailClient as jest.Mock).mockResolvedValue(
mockGmailClient,
);
const mockOAuth2Client = {
gmail: jest.fn().mockReturnValue(mockGmailClient),
};
(
oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock
).mockResolvedValue(mockOAuth2Client);
const result = await service.getMessageLists({
messageChannel: { syncCursor: '', id: 'my-id' },
@@ -205,9 +225,13 @@ describe('GmailGetMessageListService', () => {
},
};
(gmailClientProvider.getGmailClient as jest.Mock).mockResolvedValue(
mockGmailClient,
);
const mockOAuth2Client = {
gmail: jest.fn().mockReturnValue(mockGmailClient),
};
(
oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock
).mockResolvedValue(mockOAuth2Client);
const result = await service.getMessageLists({
messageChannel: { syncCursor: '', id: 'my-id' },
@@ -4,6 +4,7 @@ import { isNonEmptyString } from '@sniptt/guards';
import { type gmail_v1 as gmailV1 } 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 { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import {
@@ -11,7 +12,6 @@ import {
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { MESSAGING_GMAIL_USERS_MESSAGES_LIST_MAX_RESULT } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-users-messages-list-max-result.constant';
import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
import { GmailGetHistoryService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-history.service';
import { GmailHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-handle-error.service';
import { computeGmailExcludeSearchFilter } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-gmail-exclude-search-filter.util';
@@ -23,23 +23,27 @@ import { assertNotNull } from 'src/utils/assert';
export class GmailGetMessageListService {
private readonly logger = new Logger(GmailGetMessageListService.name);
constructor(
private readonly gmailClientProvider: GmailClientProvider,
private readonly gmailGetHistoryService: GmailGetHistoryService,
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
private readonly gmailHandleErrorService: GmailHandleErrorService,
) {}
private async getMessageListWithoutCursor(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken' | 'id' | 'handle'
'provider' | 'accessToken' | 'refreshToken' | 'id' | 'handle'
>,
messageFolders: Pick<
MessageFolderWorkspaceEntity,
'name' | 'externalId' | 'isSynced'
>[],
): Promise<GetMessageListsResponse> {
const gmailClient =
await this.gmailClientProvider.getGmailClient(connectedAccount);
const oAuth2Client =
await this.oAuth2ClientManagerService.getGoogleOAuth2Client(
connectedAccount,
);
const gmailClient = oAuth2Client.gmail({ version: 'v1' });
let pageToken: string | undefined;
let hasMoreMessages = true;
@@ -139,8 +143,11 @@ export class GmailGetMessageListService {
connectedAccount,
messageFolders,
}: GetMessageListsArgs): Promise<GetMessageListsResponse> {
const gmailClient =
await this.gmailClientProvider.getGmailClient(connectedAccount);
const oAuth2Client =
await this.oAuth2ClientManagerService.getGoogleOAuth2Client(
connectedAccount,
);
const gmailClient = oAuth2Client.gmail({ version: 'v1' });
if (!isNonEmptyString(messageChannel.syncCursor)) {
return this.getMessageListWithoutCursor(connectedAccount, messageFolders);
@@ -21,7 +21,7 @@ export class GmailGetMessagesService {
messageIds: string[],
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'accessToken' | 'refreshToken' | 'id' | 'handle' | 'handleAliases'
'accessToken' | 'id' | 'handle' | 'handleAliases'
>,
): Promise<MessageWithParticipants[]> {
const { messageIdsByBatch, batchResponses } =
@@ -4,10 +4,8 @@ import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
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 { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
import { MicrosoftFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service';
import { MicrosoftGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service';
import { MicrosoftHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-handle-error.service';
@@ -24,17 +22,13 @@ import { MicrosoftGetMessageListService } from './services/microsoft-get-message
ObjectMetadataRepositoryModule,
],
providers: [
MicrosoftClientProvider,
MicrosoftGetMessageListService,
MicrosoftGetMessagesService,
MicrosoftFetchByBatchService,
MicrosoftHandleErrorService,
MicrosoftOAuth2ClientManagerService,
],
exports: [
MicrosoftGetMessageListService,
MicrosoftClientProvider,
MicrosoftGetMessagesService,
MicrosoftHandleErrorService,
],
@@ -1,24 +0,0 @@
import { Injectable } from '@nestjs/common';
import { type Client } from '@microsoft/microsoft-graph-client';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
@Injectable()
export class MicrosoftClientProvider {
constructor(
private readonly microsoftOAuth2ClientManagerService: MicrosoftOAuth2ClientManagerService,
) {}
public async getMicrosoftClient(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'refreshToken' | 'id'
>,
): Promise<Client> {
return await this.microsoftOAuth2ClientManagerService.getOAuth2Client(
connectedAccount.refreshToken,
);
}
}
@@ -1,20 +1,20 @@
import { Injectable } from '@nestjs/common';
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 { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
import { type MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
@Injectable()
export class MicrosoftFetchByBatchService {
constructor(
private readonly microsoftClientProvider: MicrosoftClientProvider,
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
) {}
async fetchAllByBatches(
messageIds: string[],
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'refreshToken' | 'id'
'accessToken' | 'refreshToken' | 'id' | 'provider'
>,
): Promise<{
messageIdsByBatch: string[][];
@@ -25,7 +25,9 @@ export class MicrosoftFetchByBatchService {
const messageIdsByBatch: string[][] = [];
const client =
await this.microsoftClientProvider.getMicrosoftClient(connectedAccount);
await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client(
connectedAccount,
);
for (let i = 0; i < messageIds.length; i += batchLimit) {
const batchMessageIds = messageIds.slice(i, i + batchLimit);
@@ -5,25 +5,32 @@ import { ConnectedAccountProvider } from 'twenty-shared/types';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
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 { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { microsoftGraphWithMessagesDeltaLink } from 'src/modules/messaging/message-import-manager/drivers/microsoft/mocks/microsoft-api-examples';
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
import { MessageFolderName } from 'src/modules/messaging/message-import-manager/drivers/microsoft/types/folders';
import { MicrosoftGetMessageListService } from './microsoft-get-message-list.service';
import { MicrosoftHandleErrorService } from './microsoft-handle-error.service';
// in case you have "Please provide a valid token" it may be because you need to pass the env varible to the .env.test file
const accessToken = 'replace-with-your-access-token';
const refreshToken = 'replace-with-your-refresh-token';
const syncCursor = `replace-with-your-sync-cursor`;
const mockConnectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken' | 'id' | 'handle' | 'connectionParameters'
| 'provider'
| 'accessToken'
| 'refreshToken'
| 'id'
| 'handle'
| 'connectionParameters'
> = {
id: 'connected-account-id',
provider: ConnectedAccountProvider.MICROSOFT,
accessToken: accessToken,
refreshToken: refreshToken,
handle: 'test@gmail.com',
connectionParameters: {},
@@ -45,7 +52,7 @@ xdescribe('Microsoft dev tests : get message list service', () => {
imports: [TwentyConfigModule.forRoot()],
providers: [
MicrosoftGetMessageListService,
MicrosoftClientProvider,
OAuth2ClientManagerService,
MicrosoftHandleErrorService,
MicrosoftOAuth2ClientManagerService,
ConfigService,
@@ -80,6 +87,7 @@ xdescribe('Microsoft dev tests : get message list service', () => {
const mockConnectedAccountUnvalid = {
id: 'connected-account-id',
provider: ConnectedAccountProvider.MICROSOFT,
accessToken: 'invalid-token',
refreshToken: 'invalid-token',
handle: 'test@microsoft.com',
connectionParameters: {},
@@ -195,7 +203,7 @@ xdescribe('Microsoft dev tests : get message list service for folders', () => {
imports: [TwentyConfigModule.forRoot()],
providers: [
MicrosoftGetMessageListService,
MicrosoftClientProvider,
OAuth2ClientManagerService,
MicrosoftHandleErrorService,
MicrosoftOAuth2ClientManagerService,
ConfigService,
@@ -214,7 +222,7 @@ xdescribe('Microsoft dev tests : get message list service for folders', () => {
};
jest
.spyOn(MicrosoftClientProvider.prototype, 'getMicrosoftClient')
.spyOn(OAuth2ClientManagerService.prototype, 'getMicrosoftOAuth2Client')
.mockResolvedValue(mockMicrosoftClient as any);
});
@@ -7,13 +7,13 @@ import {
} from '@microsoft/microsoft-graph-client';
import { isNonEmptyString } from '@sniptt/guards';
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 { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import {
MessageImportDriverException,
MessageImportDriverExceptionCode,
} from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
import { MicrosoftHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-handle-error.service';
import { isAccessTokenRefreshingError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-access-token-refreshing-error.utils';
import { type GetMessageListsArgs } from 'src/modules/messaging/message-import-manager/types/get-message-lists-args.type';
@@ -29,7 +29,7 @@ const MESSAGING_MICROSOFT_USERS_MESSAGES_LIST_MAX_RESULT = 999;
export class MicrosoftGetMessageListService {
private readonly logger = new Logger(MicrosoftGetMessageListService.name);
constructor(
private readonly microsoftClientProvider: MicrosoftClientProvider,
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
private readonly microsoftHandleErrorService: MicrosoftHandleErrorService,
) {}
@@ -62,7 +62,7 @@ export class MicrosoftGetMessageListService {
public async getMessageList(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken' | 'id'
'provider' | 'accessToken' | 'id'
>,
messageFolder: Pick<
MessageFolderWorkspaceEntity,
@@ -73,7 +73,9 @@ export class MicrosoftGetMessageListService {
const messageExternalIdsToDelete: string[] = [];
const microsoftClient =
await this.microsoftClientProvider.getMicrosoftClient(connectedAccount);
await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client(
connectedAccount,
);
const folderId = messageFolder.externalId || messageFolder.name;
const apiUrl = isNonEmptyString(messageFolder.syncCursor)
@@ -5,7 +5,7 @@ import { ConnectedAccountProvider } from 'twenty-shared/types';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { MicrosoftFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service';
import { MicrosoftGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service';
@@ -16,6 +16,7 @@ const mockMessageIds = [
'AAkALgAAAAAAHYQDEapmEc2byACqAC-EWg0AGnUPtcQC-Eiwmc39SmMpPgAAAiVYkAAA',
];
const accessToken = 'replace-with-your-access-token';
const refreshToken = 'replace-with-your-refresh-token';
xdescribe('Microsoft dev tests : get messages service', () => {
@@ -27,7 +28,7 @@ xdescribe('Microsoft dev tests : get messages service', () => {
providers: [
MicrosoftGetMessagesService,
MicrosoftHandleErrorService,
MicrosoftClientProvider,
OAuth2ClientManagerService,
MicrosoftOAuth2ClientManagerService,
MicrosoftFetchByBatchService,
ConfigService,
@@ -44,6 +45,7 @@ xdescribe('Microsoft dev tests : get messages service', () => {
provider: ConnectedAccountProvider.MICROSOFT,
handle: 'John.Walker@outlook.fr',
handleAliases: '',
accessToken: accessToken,
refreshToken: refreshToken,
};
@@ -1,15 +1,17 @@
import { ConfigService } from '@nestjs/config';
import { Logger } from '@nestjs/common';
import { Test, type TestingModule } from '@nestjs/testing';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { GoogleOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service';
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import {
microsoftGraphBatchWithHtmlMessagesResponse,
microsoftGraphBatchWithTwoMessagesResponse,
} from 'src/modules/messaging/message-import-manager/drivers/microsoft/mocks/microsoft-api-examples';
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
import { MicrosoftFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-fetch-by-batch.service';
import { type MicrosoftGraphBatchResponse } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.interface';
import { MicrosoftGetMessagesService } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-get-messages.service';
@@ -24,7 +26,8 @@ describe('Microsoft get messages service', () => {
providers: [
MicrosoftGetMessagesService,
MicrosoftHandleErrorService,
MicrosoftClientProvider,
OAuth2ClientManagerService,
GoogleOAuth2ClientManagerService,
MicrosoftOAuth2ClientManagerService,
MicrosoftFetchByBatchService,
ConfigService,
@@ -32,6 +35,10 @@ describe('Microsoft get messages service', () => {
provide: TwentyConfigService,
useValue: {},
},
{
provide: Logger,
useValue: { log: jest.fn(), error: jest.fn() },
},
],
}).compile();
@@ -54,6 +61,7 @@ describe('Microsoft get messages service', () => {
const connectedAccount = {
id: 'connected-account-id',
provider: ConnectedAccountProvider.MICROSOFT,
accessToken: 'access-token',
refreshToken: 'refresh-token',
handle: 'John.l@outlook.fr',
handleAliases: '',
@@ -148,6 +156,7 @@ describe('Microsoft get messages service', () => {
const connectedAccount = {
id: 'connected-account-id',
provider: ConnectedAccountProvider.MICROSOFT,
accessToken: 'access-token',
refreshToken: 'refresh-token',
handle: 'John.l@outlook.fr',
handleAliases: '',
@@ -17,7 +17,12 @@ import { MicrosoftHandleErrorService } from './microsoft-handle-error.service';
type ConnectedAccountType = Pick<
ConnectedAccountWorkspaceEntity,
'refreshToken' | 'id' | 'provider' | 'handle' | 'handleAliases'
| 'accessToken'
| 'refreshToken'
| 'id'
| 'provider'
| 'handle'
| 'handleAliases'
>;
@Injectable()
@@ -9,6 +9,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works
import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module';
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
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 { RefreshTokensManagerModule } from 'src/modules/connected-account/refresh-tokens-manager/connected-account-refresh-tokens-manager.module';
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cleaner/messaging-message-cleaner.module';
@@ -47,6 +48,7 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
imports: [
RefreshTokensManagerModule,
WorkspaceDataSourceModule,
OAuth2ClientManagerModule,
MessagingGmailDriverModule,
MessagingMicrosoftDriverModule,
MessagingIMAPDriverModule,
@@ -2,17 +2,14 @@ import { Test, type TestingModule } from '@nestjs/testing';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
import { OAuth2ClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/oauth2-client.provider';
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { ImapClientProvider } from 'src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider';
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
import { SmtpClientProvider } from 'src/modules/messaging/message-import-manager/drivers/smtp/providers/smtp-client.provider';
import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service';
describe('MessagingSendMessageService - Gmail HTML Support', () => {
let service: MessagingSendMessageService;
let gmailClientProvider: jest.Mocked<GmailClientProvider>;
let oAuth2ClientProvider: jest.Mocked<OAuth2ClientProvider>;
let oAuth2ClientManagerService: OAuth2ClientManagerService;
beforeEach(async () => {
const mockGmailClient = {
@@ -24,6 +21,7 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => {
};
const mockOAuth2Client = {
gmail: jest.fn().mockReturnValue(mockGmailClient),
userinfo: {
get: jest.fn().mockResolvedValue({
data: { email: 'test@example.com', name: 'Test User' },
@@ -35,21 +33,13 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => {
providers: [
MessagingSendMessageService,
{
provide: GmailClientProvider,
provide: OAuth2ClientManagerService,
useValue: {
getGmailClient: jest.fn().mockResolvedValue(mockGmailClient),
getGoogleOAuth2Client: jest
.fn()
.mockResolvedValue(mockOAuth2Client),
},
},
{
provide: OAuth2ClientProvider,
useValue: {
getOAuth2Client: jest.fn().mockResolvedValue(mockOAuth2Client),
},
},
{
provide: MicrosoftClientProvider,
useValue: {},
},
{
provide: SmtpClientProvider,
useValue: {},
@@ -64,8 +54,9 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => {
service = module.get<MessagingSendMessageService>(
MessagingSendMessageService,
);
gmailClientProvider = module.get(GmailClientProvider);
oAuth2ClientProvider = module.get(OAuth2ClientProvider);
oAuth2ClientManagerService = module.get<OAuth2ClientManagerService>(
OAuth2ClientManagerService,
);
});
it('should send multipart/alternative email with both text and HTML parts via Gmail', async () => {
@@ -84,8 +75,9 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => {
await service.sendMessage(sendMessageInput, connectedAccount);
const gmailClient =
await gmailClientProvider.getGmailClient(connectedAccount);
const mockOAuth2Client =
await oAuth2ClientManagerService.getGoogleOAuth2Client(connectedAccount);
const gmailClient = mockOAuth2Client.gmail({ version: 'v1' });
const sendCall = gmailClient.users.messages.send as jest.Mock;
expect(sendCall).toHaveBeenCalledTimes(1);
@@ -111,7 +103,16 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => {
});
it('should handle missing fromName gracefully', async () => {
const mockGmailClient = {
users: {
messages: {
send: jest.fn().mockResolvedValue({ data: { id: 'message-id' } }),
},
},
};
const mockOAuth2ClientNoName = {
gmail: jest.fn().mockReturnValue(mockGmailClient),
userinfo: {
get: jest.fn().mockResolvedValue({
data: { email: 'test@example.com' }, // No name field
@@ -119,9 +120,9 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => {
},
};
(oAuth2ClientProvider.getOAuth2Client as jest.Mock).mockResolvedValueOnce(
mockOAuth2ClientNoName,
);
(
oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock
).mockResolvedValueOnce(mockOAuth2ClientNoName);
const sendMessageInput = {
to: 'recipient@example.com',
@@ -138,9 +139,10 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => {
await service.sendMessage(sendMessageInput, connectedAccount);
const gmailClient =
await gmailClientProvider.getGmailClient(connectedAccount);
const sendCall = gmailClient.users.messages.send as jest.Mock;
const sendCall = mockGmailClient.users.messages.send as jest.Mock;
expect(sendCall).toHaveBeenCalledTimes(1);
const rawMessage = Buffer.from(
sendCall.mock.calls[0][0].requestBody.raw,
'base64',
@@ -49,7 +49,7 @@ export class MessagingAccountAuthenticationService {
};
}
return await this.refreshAccessTokenForNonImapProvider({
return await this.refreshAccessTokenForOAuthProvider({
connectedAccount,
workspaceId,
messageChannelId,
@@ -79,7 +79,7 @@ export class MessagingAccountAuthenticationService {
}
}
private async refreshAccessTokenForNonImapProvider({
private async refreshAccessTokenForOAuthProvider({
connectedAccount,
workspaceId,
messageChannelId,
@@ -5,15 +5,13 @@ import { ConnectedAccountProvider } from 'twenty-shared/types';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { z } from 'zod';
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 { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
import { OAuth2ClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/oauth2-client.provider';
import { ImapClientProvider } from 'src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider';
import { MicrosoftClientProvider } from 'src/modules/messaging/message-import-manager/drivers/microsoft/providers/microsoft-client.provider';
import { isAccessTokenRefreshingError } from 'src/modules/messaging/message-import-manager/drivers/microsoft/utils/is-access-token-refreshing-error.utils';
import { SmtpClientProvider } from 'src/modules/messaging/message-import-manager/drivers/smtp/providers/smtp-client.provider';
import { mimeEncode } from 'src/modules/messaging/message-import-manager/utils/mime-encode.util';
@@ -28,9 +26,7 @@ interface SendMessageInput {
@Injectable()
export class MessagingSendMessageService {
constructor(
private readonly gmailClientProvider: GmailClientProvider,
private readonly oAuth2ClientProvider: OAuth2ClientProvider,
private readonly microsoftClientProvider: MicrosoftClientProvider,
private readonly oAuth2ClientManagerService: OAuth2ClientManagerService,
private readonly smtpClientProvider: SmtpClientProvider,
private readonly imapClientProvider: ImapClientProvider,
) {}
@@ -41,11 +37,14 @@ export class MessagingSendMessageService {
): Promise<void> {
switch (connectedAccount.provider) {
case ConnectedAccountProvider.GOOGLE: {
const gmailClient =
await this.gmailClientProvider.getGmailClient(connectedAccount);
const oAuth2Client =
await this.oAuth2ClientProvider.getOAuth2Client(connectedAccount);
await this.oAuth2ClientManagerService.getGoogleOAuth2Client(
connectedAccount,
);
const gmailClient = oAuth2Client.gmail({
version: 'v1',
});
const { data } = await oAuth2Client.userinfo.get();
@@ -93,7 +92,7 @@ export class MessagingSendMessageService {
}
case ConnectedAccountProvider.MICROSOFT: {
const microsoftClient =
await this.microsoftClientProvider.getMicrosoftClient(
await this.oAuth2ClientManagerService.getMicrosoftOAuth2Client(
connectedAccount,
);
@@ -6,7 +6,12 @@ export type GetMessageListsArgs = {
messageChannel: Pick<MessageChannelWorkspaceEntity, 'syncCursor' | 'id'>;
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
'provider' | 'refreshToken' | 'id' | 'handle' | 'connectionParameters'
| 'provider'
| 'accessToken'
| 'refreshToken'
| 'id'
| 'handle'
| 'connectionParameters'
>;
messageFolders: Pick<
MessageFolderWorkspaceEntity,