diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 4dd8f3c2e9..57f013e152 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -5574,7 +5574,7 @@ export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{ }>; -export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, accountOwnerId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, password: string } | null, SMTP?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, port: number, secure?: boolean | null, password: string } | null, CALDAV?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, password: string } | null } | null } }; +export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, accountOwnerId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username?: string | null, password: string } | null, SMTP?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, port: number, secure?: boolean | null, password: string } | null, CALDAV?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, password: string } | null } | null } }; export type CreateDatabaseConfigVariableMutationVariables = Exact<{ key: Scalars['String']; @@ -10202,6 +10202,7 @@ export const GetConnectedImapSmtpCaldavAccountDocument = gql` host port secure + username password } SMTP { diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getConnectedImapSmtpCaldavAccount.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getConnectedImapSmtpCaldavAccount.ts index 0713b60d2d..57434185ae 100644 --- a/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getConnectedImapSmtpCaldavAccount.ts +++ b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getConnectedImapSmtpCaldavAccount.ts @@ -12,6 +12,7 @@ export const GET_CONNECTED_IMAP_SMTP_CALDAV_ACCOUNT = gql` host port secure + username password } SMTP { diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index 7bfe55a976..df475557ed 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -150,6 +150,12 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; TransientTokenService, AuthSsoService, ], - exports: [AccessTokenService, LoginTokenService, RefreshTokenService], + exports: [ + AccessTokenService, + LoginTokenService, + RefreshTokenService, + CreateMessageChannelService, + CreateCalendarChannelService, + ], }) export class AuthModule {} diff --git a/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.spec.ts b/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.spec.ts index f4cdc51241..f1f8c08ca9 100644 --- a/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.spec.ts +++ b/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.spec.ts @@ -2,20 +2,14 @@ import { Test, type TestingModule } from '@nestjs/testing'; import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service'; +import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service'; import { type EmailAccountConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto'; -import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services/imap-smtp-caldav-apis.service'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { - MessageChannelPendingGroupEmailsAction, - MessageChannelSyncStage, - MessageChannelSyncStatus, - MessageChannelType, - type MessageChannelWorkspaceEntity, -} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; jest.mock('uuid', () => ({ v4: jest.fn(() => 'mocked-uuid'), @@ -43,12 +37,12 @@ describe('ImapSmtpCalDavAPIService', () => { transaction: jest.fn((callback) => callback({})), }; - const mockMessageQueueService = { - add: jest.fn(), + const mockCreateMessageChannelService = { + createMessageChannel: jest.fn().mockResolvedValue('mocked-uuid'), }; - const mockCalendarQueueService = { - add: jest.fn(), + const mockCreateCalendarChannelService = { + createCalendarChannel: jest.fn().mockResolvedValue('mocked-uuid'), }; beforeEach(async () => { @@ -76,12 +70,12 @@ describe('ImapSmtpCalDavAPIService', () => { }, }, { - provide: getQueueToken(MessageQueue.messagingQueue), - useValue: mockMessageQueueService, + provide: CreateMessageChannelService, + useValue: mockCreateMessageChannelService, }, { - provide: getQueueToken(MessageQueue.calendarQueue), - useValue: mockCalendarQueueService, + provide: CreateCalendarChannelService, + useValue: mockCreateCalendarChannelService, }, ], }).compile(); @@ -113,28 +107,11 @@ describe('ImapSmtpCalDavAPIService', () => { } as EmailAccountConnectionParameters, }; - it('should create new account with message and calendar channels when account does not exist', async () => { + it('should create new account with message channel when account does not exist and IMAP is configured', async () => { mockConnectedAccountRepository.findOne.mockResolvedValue(null); mockMessageChannelRepository.findOne.mockResolvedValue(null); mockCalendarChannelRepository.findOne.mockResolvedValue(null); - const expectedMessageChannel = { - id: 'mocked-uuid', - connectedAccountId: 'mocked-uuid', - type: MessageChannelType.EMAIL, - handle: 'test@example.com', - isSyncEnabled: true, - syncStatus: MessageChannelSyncStatus.NOT_SYNCED, - syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION, - pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE, - syncCursor: '', - syncStageStartedAt: null, - }; - - mockMessageChannelRepository.save.mockResolvedValue( - expectedMessageChannel, - ); - await service.processAccount(baseInput); expect(mockConnectedAccountRepository.save).toHaveBeenCalledWith( @@ -146,25 +123,21 @@ describe('ImapSmtpCalDavAPIService', () => { accountOwnerId: 'workspace-member-id', }, {}, - ); - - expect(mockMessageChannelRepository.save).toHaveBeenCalledWith( - { - id: 'mocked-uuid', - connectedAccountId: 'mocked-uuid', - type: MessageChannelType.EMAIL, - handle: 'test@example.com', - isSyncEnabled: true, - syncStatus: MessageChannelSyncStatus.NOT_SYNCED, - syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION, - pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE, - syncCursor: '', - syncStageStartedAt: null, - }, {}, ); - expect(mockMessageQueueService.add).not.toHaveBeenCalled(); + expect( + mockCreateMessageChannelService.createMessageChannel, + ).toHaveBeenCalledWith({ + workspaceId: 'workspace-id', + connectedAccountId: 'mocked-uuid', + handle: 'test@example.com', + manager: {}, + }); + + expect( + mockCreateCalendarChannelService.createCalendarChannel, + ).not.toHaveBeenCalled(); }); it('should preserve existing channels when updating account credentials', async () => { @@ -178,10 +151,6 @@ describe('ImapSmtpCalDavAPIService', () => { const existingMessageChannel = { id: 'existing-message-channel-id', connectedAccountId: 'existing-account-id', - type: MessageChannelType.EMAIL, - handle: 'test@example.com', - isSyncEnabled: true, - syncStatus: MessageChannelSyncStatus.ONGOING, } as MessageChannelWorkspaceEntity; const existingCalendarChannel = { @@ -213,12 +182,15 @@ describe('ImapSmtpCalDavAPIService', () => { accountOwnerId: 'workspace-member-id', }, {}, + {}, ); - expect(mockMessageChannelRepository.save).not.toHaveBeenCalled(); - expect(mockCalendarChannelRepository.save).not.toHaveBeenCalled(); - - expect(mockMessageQueueService.add).not.toHaveBeenCalled(); + expect( + mockCreateMessageChannelService.createMessageChannel, + ).not.toHaveBeenCalled(); + expect( + mockCreateCalendarChannelService.createCalendarChannel, + ).not.toHaveBeenCalled(); }); it('should only create message channel when only IMAP is configured', async () => { @@ -238,27 +210,14 @@ describe('ImapSmtpCalDavAPIService', () => { mockMessageChannelRepository.findOne.mockResolvedValue(null); mockCalendarChannelRepository.findOne.mockResolvedValue(null); - const expectedMessageChannel = { - id: 'mocked-uuid', - connectedAccountId: 'mocked-uuid', - type: MessageChannelType.EMAIL, - handle: 'test@example.com', - isSyncEnabled: true, - syncStatus: MessageChannelSyncStatus.NOT_SYNCED, - syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION, - }; - - mockMessageChannelRepository.save.mockResolvedValue( - expectedMessageChannel, - ); - await service.processAccount(imapOnlyInput); - expect(mockMessageChannelRepository.save).toHaveBeenCalled(); - expect(mockCalendarChannelRepository.save).not.toHaveBeenCalled(); - expect(mockMessageQueueService.add).not.toHaveBeenCalled(); - - expect(mockCalendarQueueService.add).not.toHaveBeenCalled(); + expect( + mockCreateMessageChannelService.createMessageChannel, + ).toHaveBeenCalled(); + expect( + mockCreateCalendarChannelService.createCalendarChannel, + ).not.toHaveBeenCalled(); }); it('should only create calendar channel when only CALDAV is configured', async () => { @@ -279,23 +238,14 @@ describe('ImapSmtpCalDavAPIService', () => { mockMessageChannelRepository.findOne.mockResolvedValue(null); mockCalendarChannelRepository.findOne.mockResolvedValue(null); - const expectedCalendarChannel = { - id: 'mocked-uuid', - connectedAccountId: 'mocked-uuid', - handle: 'test@example.com', - }; - - mockCalendarChannelRepository.save.mockResolvedValue( - expectedCalendarChannel, - ); - await service.processAccount(caldavOnlyInput); - expect(mockMessageChannelRepository.save).not.toHaveBeenCalled(); - expect(mockCalendarChannelRepository.save).toHaveBeenCalled(); - - expect(mockMessageQueueService.add).not.toHaveBeenCalled(); - expect(mockCalendarQueueService.add).not.toHaveBeenCalled(); + expect( + mockCreateMessageChannelService.createMessageChannel, + ).not.toHaveBeenCalled(); + expect( + mockCreateCalendarChannelService.createCalendarChannel, + ).toHaveBeenCalled(); }); it('should handle IMAP + SMTP configuration without CALDAV', async () => { @@ -322,26 +272,14 @@ describe('ImapSmtpCalDavAPIService', () => { mockMessageChannelRepository.findOne.mockResolvedValue(null); mockCalendarChannelRepository.findOne.mockResolvedValue(null); - const expectedMessageChannel = { - id: 'mocked-uuid', - connectedAccountId: 'mocked-uuid', - type: MessageChannelType.EMAIL, - handle: 'test@example.com', - isSyncEnabled: true, - syncStatus: MessageChannelSyncStatus.NOT_SYNCED, - syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION, - }; - - mockMessageChannelRepository.save.mockResolvedValue( - expectedMessageChannel, - ); - await service.processAccount(imapSmtpInput); - expect(mockMessageChannelRepository.save).toHaveBeenCalled(); - expect(mockCalendarChannelRepository.save).not.toHaveBeenCalled(); - expect(mockMessageQueueService.add).not.toHaveBeenCalled(); - expect(mockCalendarQueueService.add).not.toHaveBeenCalled(); + expect( + mockCreateMessageChannelService.createMessageChannel, + ).toHaveBeenCalled(); + expect( + mockCreateCalendarChannelService.createCalendarChannel, + ).not.toHaveBeenCalled(); }); it('should handle full IMAP + SMTP + CALDAV configuration', async () => { @@ -375,35 +313,14 @@ describe('ImapSmtpCalDavAPIService', () => { mockMessageChannelRepository.findOne.mockResolvedValue(null); mockCalendarChannelRepository.findOne.mockResolvedValue(null); - const expectedMessageChannel = { - id: 'mocked-uuid', - connectedAccountId: 'mocked-uuid', - type: MessageChannelType.EMAIL, - handle: 'test@example.com', - isSyncEnabled: true, - syncStatus: MessageChannelSyncStatus.NOT_SYNCED, - syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION, - }; - - const expectedCalendarChannel = { - id: 'mocked-uuid', - connectedAccountId: 'mocked-uuid', - handle: 'test@example.com', - }; - - mockMessageChannelRepository.save.mockResolvedValue( - expectedMessageChannel, - ); - mockCalendarChannelRepository.save.mockResolvedValue( - expectedCalendarChannel, - ); - await service.processAccount(fullConfigInput); - expect(mockMessageChannelRepository.save).toHaveBeenCalled(); - expect(mockCalendarChannelRepository.save).toHaveBeenCalled(); - expect(mockMessageQueueService.add).not.toHaveBeenCalled(); - expect(mockCalendarQueueService.add).not.toHaveBeenCalled(); + expect( + mockCreateMessageChannelService.createMessageChannel, + ).toHaveBeenCalled(); + expect( + mockCreateCalendarChannelService.createCalendarChannel, + ).toHaveBeenCalled(); }); it('should handle account found by handle when connectedAccountId is not provided', async () => { @@ -439,11 +356,12 @@ describe('ImapSmtpCalDavAPIService', () => { accountOwnerId: 'workspace-member-id', }, {}, + {}, ); }); - it('should not enqueue sync jobs when channels are disabled', async () => { - const disabledInput = { + it('should not create channels when neither IMAP nor CALDAV is configured', async () => { + const smtpOnlyInput = { ...baseInput, connectionParameters: { SMTP: { @@ -460,20 +378,20 @@ describe('ImapSmtpCalDavAPIService', () => { mockMessageChannelRepository.findOne.mockResolvedValue(null); mockCalendarChannelRepository.findOne.mockResolvedValue(null); - await service.processAccount(disabledInput); + await service.processAccount(smtpOnlyInput); - expect(mockMessageQueueService.add).not.toHaveBeenCalled(); - expect(mockCalendarQueueService.add).not.toHaveBeenCalled(); + expect( + mockCreateMessageChannelService.createMessageChannel, + ).not.toHaveBeenCalled(); + expect( + mockCreateCalendarChannelService.createCalendarChannel, + ).not.toHaveBeenCalled(); }); it('should handle transaction correctly', async () => { mockConnectedAccountRepository.findOne.mockResolvedValue(null); mockMessageChannelRepository.findOne.mockResolvedValue(null); mockCalendarChannelRepository.findOne.mockResolvedValue(null); - mockMessageChannelRepository.save.mockResolvedValue({ - id: 'mocked-uuid', - connectedAccountId: 'mocked-uuid', - }); await service.processAccount(baseInput); diff --git a/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts b/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts index b726bf96b6..d549a20d2d 100644 --- a/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts +++ b/packages/twenty-server/src/modules/connected-account/services/imap-smtp-caldav-apis.service.ts @@ -1,29 +1,24 @@ import { Injectable } from '@nestjs/common'; import { ConnectedAccountProvider } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; import { v4 } from 'uuid'; +import { CreateCalendarChannelService } from 'src/engine/core-modules/auth/services/create-calendar-channel.service'; +import { CreateMessageChannelService } from 'src/engine/core-modules/auth/services/create-message-channel.service'; import { type EmailAccountConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto'; -import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; +import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { - CalendarChannelSyncStage, - CalendarChannelSyncStatus, - type CalendarChannelWorkspaceEntity, -} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; -import { - MessageChannelPendingGroupEmailsAction, - MessageChannelSyncStage, - MessageChannelSyncStatus, - MessageChannelType, - type MessageChannelWorkspaceEntity, -} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; +import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; @Injectable() export class ImapSmtpCalDavAPIService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly createMessageChannelService: CreateMessageChannelService, + private readonly createCalendarChannelService: CreateCalendarChannelService, ) {} async processAccount(input: { @@ -62,138 +57,68 @@ export class ImapSmtpCalDavAPIService { where: { handle, accountOwnerId: workspaceMemberId }, }); - const accountId = existingAccount?.id ?? connectedAccountId ?? v4(); + const newOrExistingAccountId = + existingAccount?.id ?? connectedAccountId ?? v4(); const workspaceDataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace({ workspaceId, }); - let messageChannel: MessageChannelWorkspaceEntity | null = existingAccount + const existingMessageChannel = existingAccount ? await messageChannelRepository.findOne({ where: { connectedAccountId: existingAccount.id }, }) : null; - let calendarChannel: CalendarChannelWorkspaceEntity | null = existingAccount + const existingCalendarChannel = existingAccount ? await calendarChannelRepository.findOne({ where: { connectedAccountId: existingAccount.id }, }) : null; - await workspaceDataSource.transaction(async () => { - await this.upsertConnectedAccount( - input, - accountId, - connectedAccountRepository, - ); + const shouldCreateMessageChannel = + !isDefined(existingMessageChannel) && + Boolean(input.connectionParameters.IMAP); - if (!messageChannel) { - messageChannel = await this.setupMessageChannels( - input, - accountId, - messageChannelRepository, + const shouldCreateCalendarChannel = + !isDefined(existingCalendarChannel) && + Boolean(input.connectionParameters.CALDAV); + + await workspaceDataSource.transaction( + async (manager: WorkspaceEntityManager) => { + await connectedAccountRepository.save( + { + id: newOrExistingAccountId, + handle, + provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV, + connectionParameters: input.connectionParameters, + accountOwnerId: workspaceMemberId, + }, + {}, + manager, ); - } - if (!calendarChannel) { - calendarChannel = await this.setupCalendarChannels( - input, - accountId, - calendarChannelRepository, - ); - } - }); + if (shouldCreateMessageChannel) { + await this.createMessageChannelService.createMessageChannel({ + workspaceId, + connectedAccountId: newOrExistingAccountId, + handle, + manager, + }); + } - return accountId; - } - - private async upsertConnectedAccount( - input: { - handle: string; - workspaceMemberId: string; - workspaceId: string; - connectionParameters: EmailAccountConnectionParameters; - }, - accountId: string, - connectedAccountRepository: WorkspaceRepository, - ) { - const accountData = { - id: accountId, - handle: input.handle, - provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV, - connectionParameters: input.connectionParameters, - accountOwnerId: input.workspaceMemberId, - }; - - await connectedAccountRepository.save(accountData, {}); - } - - private async setupMessageChannels( - input: { - handle: string; - workspaceId: string; - connectionParameters: EmailAccountConnectionParameters; - }, - accountId: string, - messageChannelRepository: WorkspaceRepository, - ): Promise { - const shouldCreateMessageChannel = Boolean(input.connectionParameters.IMAP); - - if (shouldCreateMessageChannel) { - const newMessageChannel = await messageChannelRepository.save( - { - id: v4(), - connectedAccountId: accountId, - type: MessageChannelType.EMAIL, - handle: input.handle, - isSyncEnabled: true, - syncStatus: MessageChannelSyncStatus.NOT_SYNCED, - syncStage: MessageChannelSyncStage.PENDING_CONFIGURATION, - pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction.NONE, - syncCursor: '', - syncStageStartedAt: null, - }, - {}, - ); - - return newMessageChannel; - } - - return null; - } - - private async setupCalendarChannels( - input: { - handle: string; - workspaceId: string; - connectionParameters: EmailAccountConnectionParameters; - }, - accountId: string, - calendarChannelRepository: WorkspaceRepository, - ): Promise { - const shouldCreateCalendarChannel = Boolean( - input.connectionParameters.CALDAV, + if (shouldCreateCalendarChannel) { + await this.createCalendarChannelService.createCalendarChannel({ + workspaceId, + connectedAccountId: newOrExistingAccountId, + handle, + manager, + }); + } + }, ); - if (shouldCreateCalendarChannel) { - const newCalendarChannel = await calendarChannelRepository.save( - { - id: v4(), - connectedAccountId: accountId, - handle: input.handle, - isSyncEnabled: shouldCreateCalendarChannel, - syncStatus: CalendarChannelSyncStatus.NOT_SYNCED, - syncStage: CalendarChannelSyncStage.PENDING_CONFIGURATION, - syncCursor: '', - syncStageStartedAt: null, - }, - {}, - ); - - return newCalendarChannel; - } - - return null; + return newOrExistingAccountId; } }