[CONNECTED_ACCOUNT_BREAKING_CHANGE] Encrypt ConnectedAccount connectionParameters (#20673)
# Introduction Prevent any cross user `connectedAccount` `connectionParamaters` leak Also encrypt in db all `connectionParameters` password Never return any password through `DTO` anymore The settings now allow update mutation without providing the password in edition mode Verified all `connectionParameters.password` interaction ## Integration tests - Added more coverage for both failing and successful paths - Introduced a new env var that allow bypass the provider connection test ## Legacy connected Account decryption support Stop allowing non encrypted decryption on `accessToken` and `refreshToken`, only allow legacy decryption on refactored `connectionParameters` ## Upsert ownership Completely got rid of the connected workspace schema context which is legacy Also now a user can only upsert a connected account for him only.. ## New UI <img width="1770" height="1852" alt="image" src="https://github.com/user-attachments/assets/55c1dc89-42ff-4084-95e2-cc5f9e23753b" /> If in edition the password is by default disabled It needs to be selected as being edited to be enabled ## Next - Refactor tool permissions flag not to include connected accounts - Remove the legacy connected standard object - Refactor and improve connected account resolver auth
This commit is contained in:
+6
-1
@@ -2,12 +2,17 @@ import { Module } from '@nestjs/common';
|
||||
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||
import { ConnectedAccountTokenEncryptionModule } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.module';
|
||||
import { CalDavClientService } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/services/caldav-client.service';
|
||||
import { CalDavFetchEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/services/caldav-fetch-events.service';
|
||||
import { CalDavGetEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/services/caldav-get-events.service';
|
||||
|
||||
@Module({
|
||||
imports: [SecureHttpClientModule, TwentyConfigModule],
|
||||
imports: [
|
||||
SecureHttpClientModule,
|
||||
TwentyConfigModule,
|
||||
ConnectedAccountTokenEncryptionModule,
|
||||
],
|
||||
providers: [
|
||||
CalDavClientService,
|
||||
CalDavFetchEventsService,
|
||||
|
||||
+11
-10
@@ -1,9 +1,9 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { ConnectedAccountTokenEncryptionService } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service';
|
||||
import { CalDavClientService } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/services/caldav-client.service';
|
||||
import { CalDavFetchEventsService } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/services/caldav-fetch-events.service';
|
||||
import { type CalDavSyncCursor } from 'src/modules/calendar/calendar-event-import-manager/drivers/caldav/types/caldav-sync-cursor';
|
||||
@@ -20,28 +20,29 @@ export class CalDavGetEventsService {
|
||||
constructor(
|
||||
private readonly clientService: CalDavClientService,
|
||||
private readonly fetchEventsService: CalDavFetchEventsService,
|
||||
private readonly connectedAccountTokenEncryptionService: ConnectedAccountTokenEncryptionService,
|
||||
) {}
|
||||
|
||||
async getCalendarEvents(
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'provider' | 'id' | 'connectionParameters' | 'handle'
|
||||
'provider' | 'id' | 'connectionParameters' | 'handle' | 'workspaceId'
|
||||
>,
|
||||
syncCursor?: string,
|
||||
): Promise<GetCalendarEventsResponse> {
|
||||
this.logger.debug(`Getting calendar events for ${connectedAccount.handle}`);
|
||||
|
||||
try {
|
||||
const params = connectedAccount.connectionParameters?.CALDAV;
|
||||
|
||||
if (
|
||||
!isNonEmptyString(params?.host) ||
|
||||
!isNonEmptyString(params?.password) ||
|
||||
!isDefined(connectedAccount.handle)
|
||||
) {
|
||||
throw new Error('Missing required CalDAV connection parameters');
|
||||
if (!isDefined(connectedAccount.connectionParameters?.CALDAV)) {
|
||||
throw new Error('CalDAV settings not configured for this account');
|
||||
}
|
||||
|
||||
const params =
|
||||
this.connectedAccountTokenEncryptionService.decryptProtocolPassword({
|
||||
protocolParams: connectedAccount.connectionParameters.CALDAV,
|
||||
workspaceId: connectedAccount.workspaceId,
|
||||
});
|
||||
|
||||
const client = await this.clientService.getClient({
|
||||
serverUrl: params.host,
|
||||
username: params.username ?? connectedAccount.handle,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { ConnectedAccountTokenEncryptionModule } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.module';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
@@ -35,6 +36,7 @@ import { MessagingFolderSyncManagerModule } from 'src/modules/messaging/message-
|
||||
AuthModule,
|
||||
CalendarCommonModule,
|
||||
ConnectedAccountModule,
|
||||
ConnectedAccountTokenEncryptionModule,
|
||||
MessagingCommonModule,
|
||||
MessagingFolderSyncManagerModule,
|
||||
],
|
||||
|
||||
+80
-26
@@ -9,7 +9,7 @@ import {
|
||||
|
||||
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 ImapSmtpCaldavParams } from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
|
||||
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 { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
@@ -19,6 +19,7 @@ import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channe
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
|
||||
import { CalendarEventListFetchJob } from 'src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job';
|
||||
import { ConnectedAccountTokenEncryptionService } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services/imap-smtp-caldav-apis.service';
|
||||
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
|
||||
@@ -76,7 +77,9 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
};
|
||||
|
||||
const mockUserWorkspaceRepository = {
|
||||
findOne: jest.fn().mockResolvedValue({ id: 'user-workspace-id' }),
|
||||
findOne: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ id: 'user-workspace-id', userId: 'user-id' }),
|
||||
};
|
||||
|
||||
const mockWorkspaceMemberRepository = {
|
||||
@@ -114,6 +117,36 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
resetAndMarkAsCalendarEventListFetchPending: jest.fn(),
|
||||
};
|
||||
|
||||
const encryptPassword = (password: string) => `enc:v2:${password}`;
|
||||
|
||||
const withEncryptedPasswords = (
|
||||
params: ImapSmtpCaldavParams,
|
||||
): ImapSmtpCaldavParams => {
|
||||
const result: ImapSmtpCaldavParams = {};
|
||||
|
||||
for (const protocol of ['IMAP', 'SMTP', 'CALDAV'] as const) {
|
||||
if (params[protocol]) {
|
||||
result[protocol] = {
|
||||
...params[protocol],
|
||||
password: encryptPassword(params[protocol]!.password),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const mockConnectedAccountTokenEncryptionService = {
|
||||
encryptConnectionParameters: jest.fn(
|
||||
({
|
||||
connectionParameters,
|
||||
}: {
|
||||
connectionParameters: ImapSmtpCaldavParams;
|
||||
workspaceId: string;
|
||||
}) => withEncryptedPasswords(connectionParameters),
|
||||
),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
@@ -185,6 +218,10 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
provide: CalendarChannelSyncStatusService,
|
||||
useValue: mockCalendarChannelSyncStatusService,
|
||||
},
|
||||
{
|
||||
provide: ConnectedAccountTokenEncryptionService,
|
||||
useValue: mockConnectedAccountTokenEncryptionService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -193,10 +230,10 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('processAccount', () => {
|
||||
describe('upsertConnectedAccount', () => {
|
||||
const baseInput = {
|
||||
handle: 'test@example.com',
|
||||
workspaceMemberId: 'workspace-member-id',
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
workspaceId: 'workspace-id',
|
||||
connectionParameters: {
|
||||
IMAP: {
|
||||
@@ -212,7 +249,7 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
username: 'test@example.com',
|
||||
password: 'password',
|
||||
},
|
||||
} as EmailAccountConnectionParameters,
|
||||
} as ImapSmtpCaldavParams,
|
||||
};
|
||||
|
||||
it('should create new account with message channel when account does not exist and IMAP is configured', async () => {
|
||||
@@ -225,15 +262,18 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'user-workspace-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
await service.processAccount(baseInput);
|
||||
await service.upsertConnectedAccount(baseInput);
|
||||
|
||||
expect(mockTransactionManagerSave).toHaveBeenCalledWith({
|
||||
id: 'mocked-uuid',
|
||||
handle: 'test@example.com',
|
||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
connectionParameters: baseInput.connectionParameters,
|
||||
connectionParameters: withEncryptedPasswords(
|
||||
baseInput.connectionParameters,
|
||||
),
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
workspaceId: 'workspace-id',
|
||||
authFailedAt: null,
|
||||
@@ -286,6 +326,7 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'user-workspace-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
const inputWithConnectedAccountId = {
|
||||
@@ -299,17 +340,19 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
username: 'test@example.com',
|
||||
password: 'password',
|
||||
},
|
||||
} as EmailAccountConnectionParameters,
|
||||
} as ImapSmtpCaldavParams,
|
||||
connectedAccountId: 'existing-account-id',
|
||||
};
|
||||
|
||||
await service.processAccount(inputWithConnectedAccountId);
|
||||
await service.upsertConnectedAccount(inputWithConnectedAccountId);
|
||||
|
||||
expect(mockTransactionManagerSave).toHaveBeenCalledWith({
|
||||
id: 'existing-account-id',
|
||||
handle: 'test@example.com',
|
||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
connectionParameters: inputWithConnectedAccountId.connectionParameters,
|
||||
connectionParameters: withEncryptedPasswords(
|
||||
inputWithConnectedAccountId.connectionParameters,
|
||||
),
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
workspaceId: 'workspace-id',
|
||||
authFailedAt: null,
|
||||
@@ -374,11 +417,12 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'user-workspace-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
await service.processAccount({
|
||||
await service.upsertConnectedAccount({
|
||||
...baseInput,
|
||||
connectedAccountId: 'existing-account-id',
|
||||
existingAccount,
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -397,9 +441,10 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'user-workspace-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
await service.processAccount(baseInput);
|
||||
await service.upsertConnectedAccount(baseInput);
|
||||
|
||||
expect(
|
||||
mockAccountsToReconnectService.removeAccountToReconnect,
|
||||
@@ -424,7 +469,7 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
secure: true,
|
||||
password: 'password',
|
||||
},
|
||||
} as EmailAccountConnectionParameters,
|
||||
} as ImapSmtpCaldavParams,
|
||||
};
|
||||
|
||||
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
|
||||
@@ -436,9 +481,10 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'user-workspace-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
await service.processAccount(imapOnlyInput);
|
||||
await service.upsertConnectedAccount(imapOnlyInput);
|
||||
|
||||
expect(
|
||||
mockCreateMessageChannelService.createMessageChannel,
|
||||
@@ -459,7 +505,7 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
username: 'test@example.com',
|
||||
password: 'password',
|
||||
},
|
||||
} as EmailAccountConnectionParameters,
|
||||
} as ImapSmtpCaldavParams,
|
||||
};
|
||||
|
||||
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
|
||||
@@ -471,9 +517,10 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'user-workspace-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
await service.processAccount(caldavOnlyInput);
|
||||
await service.upsertConnectedAccount(caldavOnlyInput);
|
||||
|
||||
expect(
|
||||
mockCreateMessageChannelService.createMessageChannel,
|
||||
@@ -500,7 +547,7 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
username: 'test@example.com',
|
||||
password: 'password',
|
||||
},
|
||||
} as EmailAccountConnectionParameters,
|
||||
} as ImapSmtpCaldavParams,
|
||||
};
|
||||
|
||||
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
|
||||
@@ -512,9 +559,10 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'user-workspace-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
await service.processAccount(imapSmtpInput);
|
||||
await service.upsertConnectedAccount(imapSmtpInput);
|
||||
|
||||
expect(
|
||||
mockCreateMessageChannelService.createMessageChannel,
|
||||
@@ -548,7 +596,7 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
username: 'test@example.com',
|
||||
password: 'password',
|
||||
},
|
||||
} as EmailAccountConnectionParameters,
|
||||
} as ImapSmtpCaldavParams,
|
||||
};
|
||||
|
||||
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
|
||||
@@ -560,9 +608,10 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'user-workspace-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
await service.processAccount(fullConfigInput);
|
||||
await service.upsertConnectedAccount(fullConfigInput);
|
||||
|
||||
expect(
|
||||
mockCreateMessageChannelService.createMessageChannel,
|
||||
@@ -592,9 +641,10 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'user-workspace-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
await service.processAccount(baseInput);
|
||||
await service.upsertConnectedAccount(baseInput);
|
||||
|
||||
expect(mockConnectedAccountRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
@@ -608,7 +658,9 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
id: 'existing-account-id',
|
||||
handle: 'test@example.com',
|
||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
connectionParameters: baseInput.connectionParameters,
|
||||
connectionParameters: withEncryptedPasswords(
|
||||
baseInput.connectionParameters,
|
||||
),
|
||||
userWorkspaceId: 'user-workspace-id',
|
||||
workspaceId: 'workspace-id',
|
||||
authFailedAt: null,
|
||||
@@ -626,7 +678,7 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
username: 'test@example.com',
|
||||
password: 'password',
|
||||
},
|
||||
} as EmailAccountConnectionParameters,
|
||||
} as ImapSmtpCaldavParams,
|
||||
};
|
||||
|
||||
mockConnectedAccountRepository.findOne.mockResolvedValue(null);
|
||||
@@ -638,9 +690,10 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'user-workspace-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
await service.processAccount(smtpOnlyInput);
|
||||
await service.upsertConnectedAccount(smtpOnlyInput);
|
||||
|
||||
expect(
|
||||
mockCreateMessageChannelService.createMessageChannel,
|
||||
@@ -660,9 +713,10 @@ describe('ImapSmtpCalDavAPIService', () => {
|
||||
});
|
||||
mockUserWorkspaceRepository.findOne.mockResolvedValue({
|
||||
id: 'user-workspace-id',
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
await service.processAccount(baseInput);
|
||||
await service.upsertConnectedAccount(baseInput);
|
||||
|
||||
expect(
|
||||
mockConnectedAccountRepository.manager.transaction,
|
||||
|
||||
+138
-190
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import {
|
||||
@@ -7,24 +7,21 @@ import {
|
||||
MessageChannelSyncStage,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { EntityManager, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
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 { NotFoundError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import { type ImapSmtpCaldavParams } from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { ConnectedAccountTokenEncryptionService } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { getWorkspaceContext } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
import { resolveRolePermissionConfig } from 'src/engine/twenty-orm/utils/resolve-role-permission-config.util';
|
||||
import {
|
||||
CalendarEventListFetchJob,
|
||||
type CalendarEventListFetchJobData,
|
||||
@@ -32,17 +29,17 @@ import {
|
||||
import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
import { MessageChannelSyncStatusService } from 'src/modules/messaging/common/services/message-channel-sync-status.service';
|
||||
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
|
||||
import {
|
||||
MessagingMessageListFetchJob,
|
||||
type MessagingMessageListFetchJobData,
|
||||
} from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
|
||||
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
|
||||
import { type WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ImapSmtpCalDavAPIService {
|
||||
private readonly logger = new Logger(ImapSmtpCalDavAPIService.name);
|
||||
|
||||
constructor(
|
||||
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
|
||||
@InjectRepository(CalendarChannelEntity)
|
||||
private readonly calendarChannelRepository: Repository<CalendarChannelEntity>,
|
||||
@InjectRepository(ConnectedAccountEntity)
|
||||
@@ -61,210 +58,161 @@ export class ImapSmtpCalDavAPIService {
|
||||
private readonly accountsToReconnectService: AccountsToReconnectService,
|
||||
private readonly messagingChannelSyncStatusService: MessageChannelSyncStatusService,
|
||||
private readonly calendarChannelSyncStatusService: CalendarChannelSyncStatusService,
|
||||
private readonly connectedAccountTokenEncryptionService: ConnectedAccountTokenEncryptionService,
|
||||
) {}
|
||||
|
||||
async getImapSmtpCaldavConnectedAccount(
|
||||
workspaceId: string,
|
||||
id: string,
|
||||
): Promise<ConnectedAccountEntity | null> {
|
||||
const authContext = buildSystemAuthContext(workspaceId);
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const connectedAccount = await this.connectedAccountRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return connectedAccount;
|
||||
},
|
||||
authContext,
|
||||
);
|
||||
}
|
||||
|
||||
async processAccount(input: {
|
||||
async upsertConnectedAccount(input: {
|
||||
handle: string;
|
||||
workspaceMemberId: string;
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
connectionParameters: EmailAccountConnectionParameters;
|
||||
connectedAccountId?: string;
|
||||
connectionParameters: ImapSmtpCaldavParams;
|
||||
existingAccount?: ConnectedAccountEntity | null;
|
||||
}): Promise<string> {
|
||||
const { handle, workspaceId, workspaceMemberId, connectedAccountId } =
|
||||
input;
|
||||
const { handle, workspaceId, userWorkspaceId } = input;
|
||||
|
||||
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
|
||||
async () => {
|
||||
const workspaceContext = getWorkspaceContext();
|
||||
const rolePermissionConfig = resolveRolePermissionConfig({
|
||||
authContext: workspaceContext.authContext,
|
||||
userWorkspaceRoleMap: workspaceContext.userWorkspaceRoleMap,
|
||||
apiKeyRoleMap: workspaceContext.apiKeyRoleMap,
|
||||
});
|
||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: { id: userWorkspaceId, workspaceId },
|
||||
});
|
||||
|
||||
const workspaceMemberRepo =
|
||||
await this.globalWorkspaceOrmManager.getRepository<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
rolePermissionConfig ?? undefined,
|
||||
);
|
||||
if (!isDefined(userWorkspace)) {
|
||||
throw new NotFoundError(
|
||||
`UserWorkspace with id ${userWorkspaceId} not found in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const member = await workspaceMemberRepo.findOne({
|
||||
where: { id: workspaceMemberId },
|
||||
});
|
||||
const existingAccount =
|
||||
input.existingAccount ??
|
||||
(await this.connectedAccountRepository.findOne({
|
||||
where: { handle, userWorkspaceId, workspaceId },
|
||||
}));
|
||||
|
||||
if (!member) {
|
||||
throw new NotFoundError(
|
||||
`Workspace member with id ${workspaceMemberId} not found`,
|
||||
);
|
||||
}
|
||||
const newOrExistingAccountId = existingAccount?.id ?? v4();
|
||||
|
||||
const userWorkspace = await this.userWorkspaceRepository.findOne({
|
||||
where: { userId: member.userId, workspaceId },
|
||||
});
|
||||
const existingMessageChannel = existingAccount
|
||||
? await this.messageChannelRepository.findOne({
|
||||
where: { connectedAccountId: existingAccount.id, workspaceId },
|
||||
})
|
||||
: null;
|
||||
|
||||
if (!userWorkspace) {
|
||||
throw new NotFoundError(
|
||||
`UserWorkspace not found for userId ${member.userId} in workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
const existingCalendarChannel = existingAccount
|
||||
? await this.calendarChannelRepository.findOne({
|
||||
where: { connectedAccountId: existingAccount.id, workspaceId },
|
||||
})
|
||||
: null;
|
||||
|
||||
const userWorkspaceId = userWorkspace.id;
|
||||
const shouldCreateMessageChannel =
|
||||
!isDefined(existingMessageChannel) &&
|
||||
Boolean(input.connectionParameters.IMAP);
|
||||
|
||||
const existingAccount = connectedAccountId
|
||||
? await this.connectedAccountRepository.findOne({
|
||||
where: { id: connectedAccountId, workspaceId },
|
||||
})
|
||||
: await this.connectedAccountRepository.findOne({
|
||||
where: {
|
||||
handle,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
const shouldCreateCalendarChannel =
|
||||
!isDefined(existingCalendarChannel) &&
|
||||
Boolean(input.connectionParameters.CALDAV);
|
||||
|
||||
const newOrExistingAccountId =
|
||||
existingAccount?.id ?? connectedAccountId ?? v4();
|
||||
|
||||
const existingMessageChannel = existingAccount
|
||||
? await this.messageChannelRepository.findOne({
|
||||
where: { connectedAccountId: existingAccount.id, workspaceId },
|
||||
})
|
||||
: null;
|
||||
|
||||
const existingCalendarChannel = existingAccount
|
||||
? await this.calendarChannelRepository.findOne({
|
||||
where: { connectedAccountId: existingAccount.id, workspaceId },
|
||||
})
|
||||
: null;
|
||||
|
||||
const shouldCreateMessageChannel =
|
||||
!isDefined(existingMessageChannel) &&
|
||||
Boolean(input.connectionParameters.IMAP);
|
||||
|
||||
const shouldCreateCalendarChannel =
|
||||
!isDefined(existingCalendarChannel) &&
|
||||
Boolean(input.connectionParameters.CALDAV);
|
||||
|
||||
await this.connectedAccountRepository.manager.transaction(
|
||||
async (transactionManager: EntityManager) => {
|
||||
await transactionManager
|
||||
.getRepository(ConnectedAccountEntity)
|
||||
.save({
|
||||
id: newOrExistingAccountId,
|
||||
handle,
|
||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
connectionParameters: input.connectionParameters,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
authFailedAt: null,
|
||||
});
|
||||
|
||||
if (shouldCreateMessageChannel) {
|
||||
await this.createMessageChannelService.createMessageChannel({
|
||||
workspaceId,
|
||||
connectedAccountId: newOrExistingAccountId,
|
||||
handle,
|
||||
transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
if (shouldCreateCalendarChannel) {
|
||||
await this.createCalendarChannelService.createCalendarChannel({
|
||||
workspaceId,
|
||||
connectedAccountId: newOrExistingAccountId,
|
||||
handle,
|
||||
transactionManager,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(existingAccount)) {
|
||||
await this.accountsToReconnectService.removeAccountToReconnect(
|
||||
member.userId,
|
||||
workspaceId,
|
||||
newOrExistingAccountId,
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldCreateMessageChannel) {
|
||||
const newMessageChannel = await this.messageChannelRepository.findOne(
|
||||
await this.connectedAccountRepository.manager.transaction(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const encryptedConnectionParameters =
|
||||
this.connectedAccountTokenEncryptionService.encryptConnectionParameters(
|
||||
{
|
||||
where: {
|
||||
connectedAccountId: newOrExistingAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['connectedAccount', 'messageFolders'],
|
||||
connectionParameters: input.connectionParameters,
|
||||
workspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(newMessageChannel)) {
|
||||
await this.syncMessageFoldersService.syncMessageFolders({
|
||||
messageChannel: newMessageChannel,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
await transactionManager.getRepository(ConnectedAccountEntity).save({
|
||||
id: newOrExistingAccountId,
|
||||
handle,
|
||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
connectionParameters: encryptedConnectionParameters,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
authFailedAt: null,
|
||||
});
|
||||
|
||||
if (
|
||||
isDefined(existingMessageChannel) &&
|
||||
isDefined(input.connectionParameters.IMAP) &&
|
||||
existingMessageChannel.syncStage !==
|
||||
MessageChannelSyncStage.PENDING_CONFIGURATION
|
||||
) {
|
||||
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
|
||||
[existingMessageChannel.id],
|
||||
if (shouldCreateMessageChannel) {
|
||||
await this.createMessageChannelService.createMessageChannel({
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
|
||||
MessagingMessageListFetchJob.name,
|
||||
{ workspaceId, messageChannelId: existingMessageChannel.id },
|
||||
);
|
||||
connectedAccountId: newOrExistingAccountId,
|
||||
handle,
|
||||
transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(existingCalendarChannel) &&
|
||||
isDefined(input.connectionParameters.CALDAV) &&
|
||||
existingCalendarChannel.syncStage !==
|
||||
CalendarChannelSyncStage.PENDING_CONFIGURATION
|
||||
) {
|
||||
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
|
||||
[existingCalendarChannel.id],
|
||||
if (shouldCreateCalendarChannel) {
|
||||
await this.createCalendarChannelService.createCalendarChannel({
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
|
||||
CalendarEventListFetchJob.name,
|
||||
{ workspaceId, calendarChannelId: existingCalendarChannel.id },
|
||||
);
|
||||
connectedAccountId: newOrExistingAccountId,
|
||||
handle,
|
||||
transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
return newOrExistingAccountId;
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(existingAccount)) {
|
||||
await this.accountsToReconnectService.removeAccountToReconnect(
|
||||
userWorkspace.userId,
|
||||
workspaceId,
|
||||
newOrExistingAccountId,
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldCreateMessageChannel) {
|
||||
const newMessageChannel = await this.messageChannelRepository.findOne({
|
||||
where: {
|
||||
connectedAccountId: newOrExistingAccountId,
|
||||
workspaceId,
|
||||
},
|
||||
relations: ['connectedAccount', 'messageFolders'],
|
||||
});
|
||||
|
||||
if (isDefined(newMessageChannel)) {
|
||||
try {
|
||||
await this.syncMessageFoldersService.syncMessageFolders({
|
||||
messageChannel: newMessageChannel,
|
||||
workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Initial folder sync failed for account ${newOrExistingAccountId}, will retry on next scheduled sync: ${error?.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(existingMessageChannel) &&
|
||||
isDefined(input.connectionParameters.IMAP) &&
|
||||
existingMessageChannel.syncStage !==
|
||||
MessageChannelSyncStage.PENDING_CONFIGURATION
|
||||
) {
|
||||
await this.messagingChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
|
||||
[existingMessageChannel.id],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.messageQueueService.add<MessagingMessageListFetchJobData>(
|
||||
MessagingMessageListFetchJob.name,
|
||||
{ workspaceId, messageChannelId: existingMessageChannel.id },
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(existingCalendarChannel) &&
|
||||
isDefined(input.connectionParameters.CALDAV) &&
|
||||
existingCalendarChannel.syncStage !==
|
||||
CalendarChannelSyncStage.PENDING_CONFIGURATION
|
||||
) {
|
||||
await this.calendarChannelSyncStatusService.resetAndMarkAsCalendarEventListFetchPending(
|
||||
[existingCalendarChannel.id],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.calendarQueueService.add<CalendarEventListFetchJobData>(
|
||||
CalendarEventListFetchJob.name,
|
||||
{ workspaceId, calendarChannelId: existingCalendarChannel.id },
|
||||
);
|
||||
}
|
||||
|
||||
return newOrExistingAccountId;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -28,12 +28,13 @@ const createMockMailbox = (
|
||||
|
||||
const CONNECTED_ACCOUNT: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'id' | 'provider' | 'connectionParameters' | 'handle'
|
||||
'id' | 'provider' | 'connectionParameters' | 'handle' | 'workspaceId'
|
||||
> = {
|
||||
id: 'account-1',
|
||||
provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
|
||||
connectionParameters: {},
|
||||
handle: 'test@example.com',
|
||||
workspaceId: 'workspace-1',
|
||||
};
|
||||
|
||||
const MESSAGE_CHANNEL: Pick<MessageChannelEntity, 'messageFolderImportPolicy'> =
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ export class ImapGetAllFoldersService implements MessageFolderDriver {
|
||||
public async getAllMessageFolders(
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'id' | 'provider' | 'connectionParameters' | 'handle'
|
||||
'id' | 'provider' | 'connectionParameters' | 'handle' | 'workspaceId'
|
||||
>,
|
||||
messageChannel: Pick<MessageChannelEntity, 'messageFolderImportPolicy'>,
|
||||
): Promise<DiscoveredMessageFolder[]> {
|
||||
|
||||
+2
@@ -5,6 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { ConnectedAccountTokenEncryptionModule } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.module';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity';
|
||||
@@ -31,6 +32,7 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p
|
||||
EmailAliasManagerModule,
|
||||
FeatureFlagModule,
|
||||
SecureHttpClientModule,
|
||||
ConnectedAccountTokenEncryptionModule,
|
||||
WorkspaceDataSourceModule,
|
||||
MessageParticipantManagerModule,
|
||||
],
|
||||
|
||||
+15
-22
@@ -2,17 +2,16 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { ImapFlow } from 'imapflow';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { CustomError, isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type ImapSmtpCaldavParams } from 'src/engine/core-modules/imap-smtp-caldav-connection/types/imap-smtp-caldav-connection.type';
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { MessageImportDriverExceptionCode } from 'src/modules/messaging/message-import-manager/drivers/exceptions/message-import-driver.exception';
|
||||
import { ConnectedAccountTokenEncryptionService } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service';
|
||||
import { parseImapAuthenticationError } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/parse-imap-authentication-error.util';
|
||||
|
||||
type ConnectedAccountIdentifier = Pick<
|
||||
ConnectedAccountEntity,
|
||||
'id' | 'provider' | 'connectionParameters' | 'handle'
|
||||
'id' | 'provider' | 'connectionParameters' | 'handle' | 'workspaceId'
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
@@ -24,6 +23,7 @@ export class ImapClientProvider {
|
||||
|
||||
constructor(
|
||||
private readonly secureHttpClientService: SecureHttpClientService,
|
||||
private readonly connectedAccountTokenEncryptionService: ConnectedAccountTokenEncryptionService,
|
||||
) {}
|
||||
|
||||
async getClient(
|
||||
@@ -60,31 +60,24 @@ export class ImapClientProvider {
|
||||
throw new Error('Connected account is not an IMAP provider');
|
||||
}
|
||||
|
||||
const connectionParameters: ImapSmtpCaldavParams =
|
||||
(connectedAccount.connectionParameters as unknown as ImapSmtpCaldavParams) ||
|
||||
{};
|
||||
|
||||
if (!isDefined(connectedAccount.handle)) {
|
||||
throw new CustomError(
|
||||
'Handle is required',
|
||||
MessageImportDriverExceptionCode.CHANNEL_MISCONFIGURED,
|
||||
);
|
||||
}
|
||||
const imapParams =
|
||||
this.connectedAccountTokenEncryptionService.decryptProtocolPassword({
|
||||
protocolParams: connectedAccount.connectionParameters.IMAP,
|
||||
workspaceId: connectedAccount.workspaceId,
|
||||
});
|
||||
|
||||
const validatedImapHost =
|
||||
await this.secureHttpClientService.getValidatedHost(
|
||||
connectionParameters.IMAP?.host || '',
|
||||
);
|
||||
await this.secureHttpClientService.getValidatedHost(imapParams.host);
|
||||
|
||||
const client = new ImapFlow({
|
||||
host: validatedImapHost,
|
||||
port: connectionParameters.IMAP?.port || 993,
|
||||
secure: connectionParameters.IMAP?.secure,
|
||||
port: imapParams.port || 993,
|
||||
secure: imapParams.secure,
|
||||
auth: {
|
||||
user: isDefined(connectionParameters.IMAP?.username)
|
||||
? connectionParameters.IMAP?.username
|
||||
user: isDefined(imapParams.username)
|
||||
? imapParams.username
|
||||
: connectedAccount.handle,
|
||||
pass: connectionParameters.IMAP?.password || '',
|
||||
pass: imapParams.password,
|
||||
},
|
||||
logger: false,
|
||||
tls: {
|
||||
|
||||
+6
-1
@@ -18,7 +18,12 @@ import { sanitizeString } from 'src/modules/messaging/message-import-manager/uti
|
||||
|
||||
type ConnectedAccount = Pick<
|
||||
ConnectedAccountEntity,
|
||||
'id' | 'provider' | 'handle' | 'handleAliases' | 'connectionParameters'
|
||||
| 'id'
|
||||
| 'provider'
|
||||
| 'handle'
|
||||
| 'handleAliases'
|
||||
| 'connectionParameters'
|
||||
| 'workspaceId'
|
||||
>;
|
||||
|
||||
@Injectable()
|
||||
|
||||
+2
-1
@@ -1,11 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { SecureHttpClientModule } from 'src/engine/core-modules/secure-http-client/secure-http-client.module';
|
||||
import { ConnectedAccountTokenEncryptionModule } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.module';
|
||||
|
||||
import { SmtpClientProvider } from './providers/smtp-client.provider';
|
||||
|
||||
@Module({
|
||||
imports: [SecureHttpClientModule],
|
||||
imports: [SecureHttpClientModule, ConnectedAccountTokenEncryptionModule],
|
||||
providers: [SmtpClientProvider],
|
||||
exports: [SmtpClientProvider],
|
||||
})
|
||||
|
||||
+17
-6
@@ -1,31 +1,42 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { createTransport, type Transporter } from 'nodemailer';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import type SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-client/secure-http-client.service';
|
||||
import { type ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
import { ConnectedAccountTokenEncryptionService } from 'src/engine/metadata-modules/connected-account/services/connected-account-token-encryption.service';
|
||||
|
||||
@Injectable()
|
||||
export class SmtpClientProvider {
|
||||
constructor(
|
||||
private readonly secureHttpClientService: SecureHttpClientService,
|
||||
private readonly connectedAccountTokenEncryptionService: ConnectedAccountTokenEncryptionService,
|
||||
) {}
|
||||
|
||||
public async getSmtpClient(
|
||||
connectedAccount: Pick<
|
||||
ConnectedAccountEntity,
|
||||
'connectionParameters' | 'handle'
|
||||
'provider' | 'connectionParameters' | 'handle' | 'workspaceId'
|
||||
>,
|
||||
): Promise<Transporter> {
|
||||
const smtpParams = connectedAccount.connectionParameters?.SMTP;
|
||||
|
||||
if (!isDefined(smtpParams)) {
|
||||
throw new Error('SMTP settings not configured for this account');
|
||||
if (
|
||||
connectedAccount.provider !== ConnectedAccountProvider.IMAP_SMTP_CALDAV ||
|
||||
!isDefined(connectedAccount.connectionParameters?.SMTP)
|
||||
) {
|
||||
throw new Error('Connected account is not an SMTP provider');
|
||||
}
|
||||
|
||||
const smtpParams =
|
||||
this.connectedAccountTokenEncryptionService.decryptProtocolPassword({
|
||||
protocolParams: connectedAccount.connectionParameters.SMTP,
|
||||
workspaceId: connectedAccount.workspaceId,
|
||||
});
|
||||
|
||||
const validatedSmtpHost =
|
||||
await this.secureHttpClientService.getValidatedHost(smtpParams.host);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user