From 495bd193a3ab29a11579c0ece37bdcce0cc05de3 Mon Sep 17 00:00:00 2001
From: BOHEUS <56270748+BOHEUS@users.noreply.github.com>
Date: Mon, 3 Aug 2026 14:34:31 +0200
Subject: [PATCH] Messaging archived account fix (#23553)
Fix case where workspace admin wants to reconnect inherited connected
account
Case:
- workspace admin inherits team accounts from other workspace member who
left the workspace
- admin wants to reconnect inherited channels but it's not possible as
there's no path to make archived connected account active
Expected outcome: admin, who has credentials to archived connected
accounts, can reconnect said accounts
---------
Co-authored-by: neo773
Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com>
---
.../auth/services/google-apis.service.spec.ts | 67 ++++++++++++++++-
.../auth/services/google-apis.service.ts | 35 +++++++++
.../services/microsoft-apis.service.spec.ts | 67 ++++++++++++++++-
.../auth/services/microsoft-apis.service.ts | 25 +++++++
...-connected-account-on-reconnect.service.ts | 1 +
.../imap-smtp-caldav-apis.service.spec.ts | 72 +++++++++++++++++++
.../services/imap-smtp-caldav-apis.service.ts | 28 ++++++++
7 files changed, 293 insertions(+), 2 deletions(-)
diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts
index 1a12b2a901..afa772f6ec 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.spec.ts
@@ -47,8 +47,13 @@ describe('GoogleAPIsService', () => {
findOne: jest.fn(),
};
+ const mockTransactionEntityRepository = {
+ save: jest.fn(),
+ update: jest.fn(),
+ };
+
const mockTransactionManager = {
- getRepository: jest.fn().mockReturnValue({ save: jest.fn() }),
+ getRepository: jest.fn().mockReturnValue(mockTransactionEntityRepository),
};
const mockMessageChannelRepository = {
@@ -298,5 +303,65 @@ describe('GoogleAPIsService', () => {
createMessageChannelService.createMessageChannel,
).toHaveBeenCalled();
});
+
+ it('should re-enable sync on existing channels when reconnecting an archived account', async () => {
+ mockTwentyConfigService.get.mockImplementation((key) => {
+ if (key === 'CALENDAR_PROVIDER_GOOGLE_ENABLED') return true;
+ if (key === 'MESSAGING_PROVIDER_GMAIL_ENABLED') return true;
+
+ return false;
+ });
+
+ const archivedConnectedAccount = {
+ id: 'archived-account-id',
+ handle: 'test@example.com',
+ userWorkspaceId: 'user-workspace-id',
+ provider: ConnectedAccountProvider.GOOGLE,
+ archivedAt: new Date('2026-01-01'),
+ } as ConnectedAccountEntity;
+
+ mockConnectedAccountRepository.findOne.mockResolvedValue(
+ archivedConnectedAccount,
+ );
+
+ mockWorkspaceMemberRepository.findOne.mockResolvedValue({
+ id: 'workspace-member-id',
+ userId: 'user-id',
+ });
+
+ mockMessageChannelRepository.find.mockResolvedValue([
+ {
+ id: 'message-channel-id',
+ connectedAccountId: 'archived-account-id',
+ },
+ ]);
+
+ mockCalendarChannelRepository.find.mockResolvedValue([
+ {
+ id: 'calendar-channel-id',
+ connectedAccountId: 'archived-account-id',
+ },
+ ]);
+
+ await service.refreshGoogleRefreshToken({
+ handle: 'test@example.com',
+ userId: 'user-id',
+ workspaceMemberId: 'workspace-member-id',
+ workspaceId: 'workspace-id',
+ accessToken: 'new-access-token' as PlaintextString,
+ refreshToken: 'new-refresh-token' as PlaintextString,
+ calendarVisibility: CalendarChannelVisibility.SHARE_EVERYTHING,
+ messageVisibility: MessageChannelVisibility.SHARE_EVERYTHING,
+ });
+
+ expect(mockTransactionEntityRepository.update).toHaveBeenCalledWith(
+ {
+ connectedAccountId: 'archived-account-id',
+ workspaceId: 'workspace-id',
+ },
+ { isSyncEnabled: true },
+ );
+ expect(mockTransactionEntityRepository.update).toHaveBeenCalledTimes(2);
+ });
});
});
diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts
index 1f5e5e04e1..ed6c61326a 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts
@@ -160,6 +160,7 @@ export class GoogleAPIsService {
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
+ const wasArchived = isDefined(connectedAccount?.archivedAt);
const existingMessageChannels =
await this.messageChannelRepository.find({
@@ -239,6 +240,40 @@ export class GoogleAPIsService {
transactionManager,
});
}
+
+ if (
+ wasArchived &&
+ isMessagingEnabled &&
+ isMessagingAvailable &&
+ existingMessageChannels.length > 0
+ ) {
+ await transactionManager
+ .getRepository(MessageChannelEntity)
+ .update(
+ {
+ connectedAccountId: newOrExistingConnectedAccountId,
+ workspaceId,
+ },
+ { isSyncEnabled: true },
+ );
+ }
+
+ if (
+ wasArchived &&
+ isCalendarEnabled &&
+ isCalendarAvailable &&
+ existingCalendarChannels.length > 0
+ ) {
+ await transactionManager
+ .getRepository(CalendarChannelEntity)
+ .update(
+ {
+ connectedAccountId: newOrExistingConnectedAccountId,
+ workspaceId,
+ },
+ { isSyncEnabled: true },
+ );
+ }
},
);
diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts
index 4b5771a846..7015692d02 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.spec.ts
@@ -44,8 +44,13 @@ describe('MicrosoftAPIsService', () => {
findOne: jest.fn(),
};
+ const mockTransactionEntityRepository = {
+ save: jest.fn(),
+ update: jest.fn(),
+ };
+
const mockTransactionManager = {
- getRepository: jest.fn().mockReturnValue({ save: jest.fn() }),
+ getRepository: jest.fn().mockReturnValue(mockTransactionEntityRepository),
};
const mockMessageChannelRepository = {
@@ -277,5 +282,65 @@ describe('MicrosoftAPIsService', () => {
createMessageChannelService.createMessageChannel,
).not.toHaveBeenCalled();
});
+
+ it('should re-enable sync on existing channels when reconnecting an archived account', async () => {
+ mockTwentyConfigService.get.mockImplementation((key) => {
+ if (key === 'CALENDAR_PROVIDER_MICROSOFT_ENABLED') return true;
+ if (key === 'MESSAGING_PROVIDER_MICROSOFT_ENABLED') return true;
+
+ return false;
+ });
+
+ const archivedConnectedAccount = {
+ id: 'archived-account-id',
+ handle: 'test@example.com',
+ userWorkspaceId: 'user-workspace-id',
+ provider: ConnectedAccountProvider.MICROSOFT,
+ archivedAt: new Date('2026-01-01'),
+ } as ConnectedAccountEntity;
+
+ mockConnectedAccountRepository.findOne.mockResolvedValue(
+ archivedConnectedAccount,
+ );
+
+ mockWorkspaceMemberRepository.findOne.mockResolvedValue({
+ id: 'workspace-member-id',
+ userId: 'user-id',
+ });
+
+ mockMessageChannelRepository.find.mockResolvedValue([
+ {
+ id: 'message-channel-id',
+ connectedAccountId: 'archived-account-id',
+ },
+ ]);
+
+ mockCalendarChannelRepository.find.mockResolvedValue([
+ {
+ id: 'calendar-channel-id',
+ connectedAccountId: 'archived-account-id',
+ },
+ ]);
+
+ await service.refreshMicrosoftRefreshToken({
+ handle: 'test@example.com',
+ userId: 'user-id',
+ workspaceMemberId: 'workspace-member-id',
+ workspaceId: 'workspace-id',
+ accessToken: 'new-access-token' as PlaintextString,
+ refreshToken: 'new-refresh-token' as PlaintextString,
+ calendarVisibility: CalendarChannelVisibility.SHARE_EVERYTHING,
+ messageVisibility: MessageChannelVisibility.SHARE_EVERYTHING,
+ });
+
+ expect(mockTransactionEntityRepository.update).toHaveBeenCalledWith(
+ {
+ connectedAccountId: 'archived-account-id',
+ workspaceId: 'workspace-id',
+ },
+ { isSyncEnabled: true },
+ );
+ expect(mockTransactionEntityRepository.update).toHaveBeenCalledTimes(2);
+ });
});
});
diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts
index 5559b823e7..75c7f8bef7 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/services/microsoft-apis.service.ts
@@ -126,6 +126,7 @@ export class MicrosoftAPIsService {
const existingAccountId = connectedAccount?.id;
const newOrExistingConnectedAccountId = existingAccountId ?? v4();
+ const wasArchived = isDefined(connectedAccount?.archivedAt);
const existingMessageChannels =
await this.messageChannelRepository.find({
@@ -217,6 +218,30 @@ export class MicrosoftAPIsService {
transactionManager,
});
}
+
+ if (wasArchived && existingMessageChannels.length > 0) {
+ await transactionManager
+ .getRepository(MessageChannelEntity)
+ .update(
+ {
+ connectedAccountId: newOrExistingConnectedAccountId,
+ workspaceId,
+ },
+ { isSyncEnabled: true },
+ );
+ }
+
+ if (wasArchived && existingCalendarChannels.length > 0) {
+ await transactionManager
+ .getRepository(CalendarChannelEntity)
+ .update(
+ {
+ connectedAccountId: newOrExistingConnectedAccountId,
+ workspaceId,
+ },
+ { isSyncEnabled: true },
+ );
+ }
},
);
diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts
index 461c17c524..fd995750fe 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/services/update-connected-account-on-reconnect.service.ts
@@ -60,6 +60,7 @@ export class UpdateConnectedAccountOnReconnectService {
refreshToken: encryptedRefreshToken,
scopes,
authFailedAt: null,
+ archivedAt: null,
},
);
}, authContext);
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 f91d2e46ed..cb3cd11438 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
@@ -60,9 +60,11 @@ describe('ImapSmtpCalDavAPIService', () => {
let service: ImapSmtpCalDavAPIService;
const mockTransactionManagerSave = jest.fn();
+ const mockTransactionManagerUpdate = jest.fn();
const mockTransactionManager = {
getRepository: jest.fn().mockReturnValue({
save: mockTransactionManagerSave,
+ update: mockTransactionManagerUpdate,
}),
};
@@ -284,6 +286,7 @@ describe('ImapSmtpCalDavAPIService', () => {
userWorkspaceId: 'user-workspace-id',
workspaceId: 'workspace-id',
authFailedAt: null,
+ archivedAt: null,
});
expect(
@@ -363,6 +366,7 @@ describe('ImapSmtpCalDavAPIService', () => {
userWorkspaceId: 'user-workspace-id',
workspaceId: 'workspace-id',
authFailedAt: null,
+ archivedAt: null,
});
expect(
@@ -399,6 +403,73 @@ describe('ImapSmtpCalDavAPIService', () => {
);
});
+ it('should re-enable sync on existing channels when reconnecting an archived account', async () => {
+ const archivedAccount = {
+ id: 'archived-account-id',
+ handle: 'test@example.com',
+ userWorkspaceId: 'user-workspace-id',
+ provider: ConnectedAccountProvider.IMAP_SMTP_CALDAV,
+ archivedAt: new Date('2026-01-01'),
+ } as ConnectedAccountEntity;
+
+ const existingMessageChannel = {
+ id: 'existing-message-channel-id',
+ connectedAccountId: 'archived-account-id',
+ syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
+ } as MessageChannelEntity;
+
+ const existingCalendarChannel = {
+ id: 'existing-calendar-channel-id',
+ connectedAccountId: 'archived-account-id',
+ syncStage: CalendarChannelSyncStage.CALENDAR_EVENT_LIST_FETCH_PENDING,
+ } as CalendarChannelEntity;
+
+ mockConnectedAccountRepository.findOne.mockResolvedValue(archivedAccount);
+ mockMessageChannelRepository.findOne.mockResolvedValue(
+ existingMessageChannel,
+ );
+ mockCalendarChannelRepository.findOne.mockResolvedValue(
+ existingCalendarChannel,
+ );
+ mockWorkspaceMemberRepository.findOne.mockResolvedValue({
+ id: 'workspace-member-id',
+ userId: 'user-id',
+ });
+ mockUserWorkspaceRepository.findOne.mockResolvedValue({
+ id: 'user-workspace-id',
+ userId: 'user-id',
+ });
+
+ const inputWithCalDav = {
+ ...baseInput,
+ connectionParameters: {
+ ...baseInput.connectionParameters,
+ CALDAV: {
+ host: 'caldav.example.com',
+ port: 443,
+ connectionSecurity: 'SSL_TLS',
+ username: 'test@example.com',
+ password: 'password' as PlaintextString,
+ },
+ } as PlaintextImapSmtpCaldavParams,
+ };
+
+ await service.upsertConnectedAccount(inputWithCalDav);
+
+ expect(mockTransactionManagerSave).toHaveBeenCalledWith(
+ expect.objectContaining({ archivedAt: null }),
+ );
+
+ expect(mockTransactionManagerUpdate).toHaveBeenCalledWith(
+ { id: 'existing-message-channel-id', workspaceId: 'workspace-id' },
+ { isSyncEnabled: true },
+ );
+ expect(mockTransactionManagerUpdate).toHaveBeenCalledWith(
+ { id: 'existing-calendar-channel-id', workspaceId: 'workspace-id' },
+ { isSyncEnabled: true },
+ );
+ });
+
it('should leave channels in PENDING_CONFIGURATION untouched', async () => {
const existingAccount = {
id: 'existing-account-id',
@@ -672,6 +743,7 @@ describe('ImapSmtpCalDavAPIService', () => {
userWorkspaceId: 'user-workspace-id',
workspaceId: 'workspace-id',
authFailedAt: null,
+ archivedAt: null,
});
});
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 74ced60179..efdcdb7811 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
@@ -95,6 +95,7 @@ export class ImapSmtpCalDavAPIService {
}));
const newOrExistingAccountId = existingAccount?.id ?? v4();
+ const wasArchived = isDefined(existingAccount?.archivedAt);
const existingMessageChannel = existingAccount
? await this.messageChannelRepository.findOne({
@@ -134,6 +135,7 @@ export class ImapSmtpCalDavAPIService {
userWorkspaceId,
workspaceId,
authFailedAt: null,
+ archivedAt: null,
});
if (shouldCreateMessageChannel) {
@@ -153,6 +155,32 @@ export class ImapSmtpCalDavAPIService {
transactionManager,
});
}
+
+ if (
+ wasArchived &&
+ isDefined(existingMessageChannel) &&
+ isDefined(input.connectionParameters.IMAP)
+ ) {
+ await transactionManager
+ .getRepository(MessageChannelEntity)
+ .update(
+ { id: existingMessageChannel.id, workspaceId },
+ { isSyncEnabled: true },
+ );
+ }
+
+ if (
+ wasArchived &&
+ isDefined(existingCalendarChannel) &&
+ isDefined(input.connectionParameters.CALDAV)
+ ) {
+ await transactionManager
+ .getRepository(CalendarChannelEntity)
+ .update(
+ { id: existingCalendarChannel.id, workspaceId },
+ { isSyncEnabled: true },
+ );
+ }
},
);