From 616d58bc7e4e756255ccfcd441ad494b266024c4 Mon Sep 17 00:00:00 2001
From: neo773 <62795688+neo773@users.noreply.github.com>
Date: Fri, 19 Jun 2026 05:29:35 +0530
Subject: [PATCH] messaging: gmail folder backfill (#21753)
demo
https://github.com/user-attachments/assets/a157cee1-a8fa-4050-af1b-c31a83fb75da
/closes #17095
---
.../src/metadata/generated/schema.graphql | 3 +-
.../src/metadata/generated/schema.ts | 5 +-
.../src/generated-metadata/graphql.ts | 11 ++--
.../modules/accounts/types/MessageFolder.ts | 3 +
.../SettingsMessageFoldersTreeItem.tsx | 7 +++
.../computeFolderIdsForSyncToggle.test.ts | 2 +
.../computeMessageFolderTree.test.ts | 2 +
.../graphql/mutations/updateMessageFolders.ts | 1 +
.../graphql/queries/getMyMessageFolders.ts | 1 +
.../useUpdateMessageFoldersSyncStatus.ts | 2 +
...t-to-message-folder-pending-sync-action.ts | 25 ++++++++
.../instance-commands.constant.ts | 2 +
.../dtos/update-message-folder.input.ts | 13 +----
.../message-folder-metadata.service.ts | 58 +++++++++++++++++--
.../resolvers/message-folder.resolver.ts | 2 +-
.../gmail-get-message-list.service.ts | 2 +-
.../messaging-import-manager.module.ts | 6 +-
...essaging-import-folder-messages.service.ts | 49 ++++++++++++++++
.../messaging-message-list-fetch.service.ts | 27 +++++----
...essaging-process-folder-actions.service.ts | 57 +++++++++++++-----
.../types/MessageFolderPendingSyncAction.ts | 1 +
21 files changed, 223 insertions(+), 56 deletions(-)
create mode 100644 packages/twenty-server/src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action.ts
create mode 100644 packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service.ts
diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
index 9166dc2536..b74535d330 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
@@ -2943,6 +2943,7 @@ type MessageFolder {
enum MessageFolderPendingSyncAction {
FOLDER_DELETION
+ FOLDER_IMPORT
NONE
}
@@ -4374,7 +4375,7 @@ input UpdateMessageFolderInput {
}
input UpdateMessageFolderInputUpdates {
- isSynced: Boolean
+ isSynced: Boolean!
}
input UpdateMessageFoldersInput {
diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
index 287a94ba77..4e505130ea 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
@@ -2596,7 +2596,7 @@ export interface MessageFolder {
__typename: 'MessageFolder'
}
-export type MessageFolderPendingSyncAction = 'FOLDER_DELETION' | 'NONE'
+export type MessageFolderPendingSyncAction = 'FOLDER_DELETION' | 'FOLDER_IMPORT' | 'NONE'
export interface CollectionHash {
collectionName: AllMetadataName
@@ -6467,7 +6467,7 @@ id: Scalars['UUID']}
export interface UpdateMessageFolderInput {id: Scalars['UUID'],update: UpdateMessageFolderInputUpdates}
-export interface UpdateMessageFolderInputUpdates {isSynced?: (Scalars['Boolean'] | null)}
+export interface UpdateMessageFolderInputUpdates {isSynced: Scalars['Boolean']}
export interface UpdateMessageFoldersInput {ids: Scalars['UUID'][],update: UpdateMessageFolderInputUpdates}
@@ -9197,6 +9197,7 @@ export const enumCalendarChannelContactAutoCreationPolicy = {
export const enumMessageFolderPendingSyncAction = {
FOLDER_DELETION: 'FOLDER_DELETION' as const,
+ FOLDER_IMPORT: 'FOLDER_IMPORT' as const,
NONE: 'NONE' as const
}
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index 7b78f95170..7e93128138 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -2380,6 +2380,7 @@ export enum MessageFolderImportPolicy {
export enum MessageFolderPendingSyncAction {
FOLDER_DELETION = 'FOLDER_DELETION',
+ FOLDER_IMPORT = 'FOLDER_IMPORT',
NONE = 'NONE'
}
@@ -5345,7 +5346,7 @@ export type UpdateMessageFolderInput = {
};
export type UpdateMessageFolderInputUpdates = {
- isSynced?: InputMaybe;
+ isSynced: Scalars['Boolean']['input'];
};
export type UpdateMessageFoldersInput = {
@@ -7545,7 +7546,7 @@ export type UpdateMessageFoldersMutationVariables = Exact<{
}>;
-export type UpdateMessageFoldersMutation = { __typename?: 'Mutation', updateMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, isSynced: boolean }> };
+export type UpdateMessageFoldersMutation = { __typename?: 'Mutation', updateMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, isSynced: boolean, pendingSyncAction: MessageFolderPendingSyncAction }> };
export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{
id: Scalars['UUID']['input'];
@@ -7578,7 +7579,7 @@ export type MyMessageFoldersQueryVariables = Exact<{
}>;
-export type MyMessageFoldersQuery = { __typename?: 'Query', myMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, name?: string | null, isSynced: boolean, isSentFolder: boolean, parentFolderId?: string | null, externalId?: string | null, messageChannelId: string, createdAt: string, updatedAt: string }> };
+export type MyMessageFoldersQuery = { __typename?: 'Query', myMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, name?: string | null, isSynced: boolean, isSentFolder: boolean, parentFolderId?: string | null, externalId?: string | null, pendingSyncAction: MessageFolderPendingSyncAction, messageChannelId: string, createdAt: string, updatedAt: string }> };
export type ApplicationRegistrationFragmentFragment = { __typename?: 'ApplicationRegistration', id: string, universalIdentifier: string, name: string, logoUrl?: string | null, oAuthClientId: string, oAuthRedirectUris: Array, oAuthScopes: Array, sourceType: ApplicationRegistrationSourceType, sourcePackage?: string | null, latestAvailableVersion?: string | null, isListed: boolean, isFeatured: boolean, isPreInstalled: boolean, isConfigured: boolean, ownerWorkspaceId?: string | null, createdAt: string, updatedAt: string };
@@ -8779,12 +8780,12 @@ export const StartChannelSyncDocument = {"kind":"Document","definitions":[{"kind
export const UpdateCalendarChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCalendarChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateCalendarChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCalendarChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}}]}}]}}]} as unknown as DocumentNode;
export const UpdateMessageChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}}]}}]}}]} as unknown as DocumentNode;
export const UpdateMessageFolderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFolderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}}]}}]}}]} as unknown as DocumentNode;
-export const UpdateMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFoldersInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}}]}}]}}]} as unknown as DocumentNode;
+export const UpdateMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFoldersInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}},{"kind":"Field","name":{"kind":"Name","value":"pendingSyncAction"}}]}}]}}]} as unknown as DocumentNode;
export const GetConnectedImapSmtpCaldavAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectedImapSmtpCaldavAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getConnectedImapSmtpCaldavAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"connectionParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"IMAP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"SMTP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}}]}},{"kind":"Field","name":{"kind":"Name","value":"CALDAV"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}}]}}]}}]} as unknown as DocumentNode;
export const MyCalendarChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyCalendarChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myCalendarChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode;
export const MyConnectedAccountsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"authFailedAt"}},{"kind":"Field","name":{"kind":"Name","value":"archivedAt"}},{"kind":"Field","name":{"kind":"Name","value":"scopes"}},{"kind":"Field","name":{"kind":"Name","value":"handleAliases"}},{"kind":"Field","name":{"kind":"Name","value":"lastSignedInAt"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"connectionProviderId"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"lastCredentialsRefreshedAt"}},{"kind":"Field","name":{"kind":"Name","value":"connectionParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"IMAP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"SMTP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"connectionSecurity"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"CALDAV"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode;
export const MyMessageChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode;
-export const MyMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"messageChannelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}},{"kind":"Field","name":{"kind":"Name","value":"isSentFolder"}},{"kind":"Field","name":{"kind":"Name","value":"parentFolderId"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"messageChannelId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode;
+export const MyMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"messageChannelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}},{"kind":"Field","name":{"kind":"Name","value":"isSentFolder"}},{"kind":"Field","name":{"kind":"Name","value":"parentFolderId"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"pendingSyncAction"}},{"kind":"Field","name":{"kind":"Name","value":"messageChannelId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode;
export const DeleteApplicationRegistrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteApplicationRegistration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteApplicationRegistration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}]}]}}]} as unknown as DocumentNode;
export const RotateApplicationRegistrationClientSecretDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RotateApplicationRegistrationClientSecret"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rotateApplicationRegistrationClientSecret"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"clientSecret"}}]}}]}}]} as unknown as DocumentNode;
export const TransferApplicationRegistrationOwnershipDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TransferApplicationRegistrationOwnership"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"targetWorkspaceSubdomain"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"transferApplicationRegistrationOwnership"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRegistrationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRegistrationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"targetWorkspaceSubdomain"},"value":{"kind":"Variable","name":{"kind":"Name","value":"targetWorkspaceSubdomain"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode;
diff --git a/packages/twenty-front/src/modules/accounts/types/MessageFolder.ts b/packages/twenty-front/src/modules/accounts/types/MessageFolder.ts
index 2fcc332829..dbc9441b4a 100644
--- a/packages/twenty-front/src/modules/accounts/types/MessageFolder.ts
+++ b/packages/twenty-front/src/modules/accounts/types/MessageFolder.ts
@@ -1,3 +1,5 @@
+import { type MessageFolderPendingSyncAction } from 'twenty-shared/types';
+
export type MessageFolder = {
id: string;
name: string;
@@ -5,6 +7,7 @@ export type MessageFolder = {
isSentFolder: boolean;
parentFolderId: string | null;
externalId: string | null;
+ pendingSyncAction: MessageFolderPendingSyncAction;
messageChannelId: string;
createdAt: string;
updatedAt: string;
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem.tsx b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem.tsx
index 5216bcf7ad..f39ea373da 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem.tsx
+++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem.tsx
@@ -8,6 +8,8 @@ import { isFolderTreePartiallySelected } from '@/settings/accounts/components/me
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
+import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
+import { Status } from 'twenty-ui/data-display';
import { IconChevronDown, IconChevronUp } from 'twenty-ui/icon';
import { Checkbox, CheckboxSize } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -176,6 +178,11 @@ export const SettingsMessageFoldersTreeItem = ({
+ {folder.pendingSyncAction ===
+ MessageFolderPendingSyncAction.FOLDER_IMPORT && (
+
+ )}
+
{hasChildren && (
<>
{childCount}
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeFolderIdsForSyncToggle.test.ts b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeFolderIdsForSyncToggle.test.ts
index 2b78704826..ead8f67432 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeFolderIdsForSyncToggle.test.ts
+++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeFolderIdsForSyncToggle.test.ts
@@ -1,5 +1,6 @@
import { type MessageFolder } from '@/accounts/types/MessageFolder';
import { computeFolderIdsForSyncToggle } from '@/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle';
+import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
describe('computeFolderIdsForSyncToggle', () => {
const createFolder = ({
@@ -22,6 +23,7 @@ describe('computeFolderIdsForSyncToggle', () => {
externalId: externalId || id,
isSentFolder: false,
isSynced,
+ pendingSyncAction: MessageFolderPendingSyncAction.NONE,
messageChannelId: 'channel-1',
createdAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-01T00:00:00Z',
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts
index 03f564d997..d29a858719 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts
+++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts
@@ -1,5 +1,6 @@
import { type MessageFolder } from '@/accounts/types/MessageFolder';
import { computeMessageFolderTree } from '@/settings/accounts/components/message-folders/utils/computeMessageFolderTree';
+import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
describe('computeMessageFolderTree', () => {
const createFolder = (
@@ -15,6 +16,7 @@ describe('computeMessageFolderTree', () => {
externalId: externalId || id,
isSentFolder: false,
isSynced: false,
+ pendingSyncAction: MessageFolderPendingSyncAction.NONE,
messageChannelId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
createdAt: '2026-01-01T00:00:00Z',
updatedAt: '2026-01-01T00:00:00Z',
diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolders.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolders.ts
index f1327626bf..33b54dd02c 100644
--- a/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolders.ts
+++ b/packages/twenty-front/src/modules/settings/accounts/graphql/mutations/updateMessageFolders.ts
@@ -5,6 +5,7 @@ export const UPDATE_MESSAGE_FOLDERS = gql`
updateMessageFolders(input: $input) {
id
isSynced
+ pendingSyncAction
}
}
`;
diff --git a/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageFolders.ts b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageFolders.ts
index 029d0c1f2d..c6647b24ea 100644
--- a/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageFolders.ts
+++ b/packages/twenty-front/src/modules/settings/accounts/graphql/queries/getMyMessageFolders.ts
@@ -9,6 +9,7 @@ export const GET_MY_MESSAGE_FOLDERS = gql`
isSentFolder
parentFolderId
externalId
+ pendingSyncAction
messageChannelId
createdAt
updatedAt
diff --git a/packages/twenty-front/src/modules/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus.ts b/packages/twenty-front/src/modules/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus.ts
index ab4652c658..1b3c7ba842 100644
--- a/packages/twenty-front/src/modules/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus.ts
+++ b/packages/twenty-front/src/modules/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus.ts
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
import { UPDATE_MESSAGE_FOLDERS } from '@/settings/accounts/graphql/mutations/updateMessageFolders';
import { useApolloClient } from '@apollo/client/react';
+import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
type UpdateMessageFoldersSyncStatusArgs = {
messageFolderIds: string[];
@@ -33,6 +34,7 @@ export const useUpdateMessageFoldersSyncStatus = () => {
__typename: 'MessageFolder',
id,
isSynced,
+ pendingSyncAction: MessageFolderPendingSyncAction.NONE,
})),
},
});
diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action.ts
new file mode 100644
index 0000000000..9fe85335df
--- /dev/null
+++ b/packages/twenty-server/src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action.ts
@@ -0,0 +1,25 @@
+import { QueryRunner } from 'typeorm';
+
+import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
+import { FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
+
+@RegisteredInstanceCommand('2.15.0', 1781714499016)
+export class AddFolderImportToMessageFolderPendingSyncActionFastInstanceCommand implements FastInstanceCommand {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query('ALTER TYPE "core"."messageFolder_pendingsyncaction_enum" RENAME TO "messageFolder_pendingsyncaction_enum_old"');
+ await queryRunner.query('CREATE TYPE "core"."messageFolder_pendingsyncaction_enum" AS ENUM(\'FOLDER_DELETION\', \'FOLDER_IMPORT\', \'NONE\')');
+ await queryRunner.query('ALTER TABLE "core"."messageFolder" ALTER COLUMN "pendingSyncAction" DROP DEFAULT');
+ await queryRunner.query('ALTER TABLE "core"."messageFolder" ALTER COLUMN "pendingSyncAction" TYPE "core"."messageFolder_pendingsyncaction_enum" USING "pendingSyncAction"::"text"::"core"."messageFolder_pendingsyncaction_enum"');
+ await queryRunner.query('ALTER TABLE "core"."messageFolder" ALTER COLUMN "pendingSyncAction" SET DEFAULT \'NONE\'');
+ await queryRunner.query('DROP TYPE "core"."messageFolder_pendingsyncaction_enum_old"');
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query('CREATE TYPE "core"."messageFolder_pendingsyncaction_enum_old" AS ENUM(\'FOLDER_DELETION\', \'NONE\')');
+ await queryRunner.query('ALTER TABLE "core"."messageFolder" ALTER COLUMN "pendingSyncAction" DROP DEFAULT');
+ await queryRunner.query('ALTER TABLE "core"."messageFolder" ALTER COLUMN "pendingSyncAction" TYPE "core"."messageFolder_pendingsyncaction_enum_old" USING (CASE WHEN "pendingSyncAction"::"text" = \'FOLDER_IMPORT\' THEN \'NONE\' ELSE "pendingSyncAction"::"text" END)::"core"."messageFolder_pendingsyncaction_enum_old"');
+ await queryRunner.query('ALTER TABLE "core"."messageFolder" ALTER COLUMN "pendingSyncAction" SET DEFAULT \'NONE\'');
+ await queryRunner.query('DROP TYPE "core"."messageFolder_pendingsyncaction_enum"');
+ await queryRunner.query('ALTER TYPE "core"."messageFolder_pendingsyncaction_enum_old" RENAME TO "messageFolder_pendingsyncaction_enum"');
+ }
+}
diff --git a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts
index dcf29fcdce..7aed346e28 100644
--- a/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts
+++ b/packages/twenty-server/src/database/commands/upgrade-version-command/instance-commands.constant.ts
@@ -73,6 +73,7 @@ import { AddLogicFunctionExecutionModeFastInstanceCommand } from 'src/database/c
import { EncryptNonSecretApplicationVariableSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1798400000000-encrypt-non-secret-application-variable';
import { MigrateAiModelPreferencesSlowInstanceCommand } from 'src/database/commands/upgrade-version-command/2-9/2-9-instance-command-slow-1799000010000-migrate-ai-model-preferences';
import { AddHasPaymentMethodToBillingCustomerFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-15/2-15-instance-command-fast-1781280240009-add-has-payment-method-to-billing-customer';
+import { AddFolderImportToMessageFolderPendingSyncActionFastInstanceCommand } from './2-15/2-15-instance-command-fast-1781714499016-add-folder-import-to-message-folder-pending-sync-action';
export const INSTANCE_COMMANDS = [
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
@@ -148,4 +149,5 @@ export const INSTANCE_COMMANDS = [
SetTableWidgetViewsVisibilityToWorkspaceSlowInstanceCommand,
AddIsSystemSideEffectFastInstanceCommand,
BackfillConnectionSecuritySlowInstanceCommand,
+ AddFolderImportToMessageFolderPendingSyncActionFastInstanceCommand,
];
diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/update-message-folder.input.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/update-message-folder.input.ts
index 596806dfc5..4dfdb255a2 100644
--- a/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/update-message-folder.input.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/dtos/update-message-folder.input.ts
@@ -1,22 +1,15 @@
import { Field, InputType } from '@nestjs/graphql';
import { Type } from 'class-transformer';
-import {
- IsBoolean,
- IsNotEmpty,
- IsOptional,
- IsUUID,
- ValidateNested,
-} from 'class-validator';
+import { IsBoolean, IsNotEmpty, IsUUID, ValidateNested } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class UpdateMessageFolderInputUpdates {
- @IsOptional()
@IsBoolean()
- @Field({ nullable: true })
- isSynced?: boolean;
+ @Field()
+ isSynced: boolean;
}
@InputType()
diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.service.ts
index 1933b95925..8d6e98174e 100644
--- a/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/message-folder-metadata.service.ts
@@ -3,7 +3,10 @@ import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
-import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
+import {
+ ConnectedAccountProvider,
+ MessageFolderPendingSyncAction,
+} from 'twenty-shared/types';
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
import { MessageFolderDTO } from 'src/engine/metadata-modules/message-folder/dtos/message-folder.dto';
@@ -183,7 +186,7 @@ export class MessageFolderMetadataService {
return this.repository.findOneOrFail({ where: { id, workspaceId } });
}
- async updateMany({
+ async setSyncStatus({
ids,
workspaceId,
data,
@@ -192,10 +195,53 @@ export class MessageFolderMetadataService {
workspaceId: string;
data: Partial;
}): Promise {
- await this.repository.update(
- { id: In(ids), workspaceId },
- data as Record,
- );
+ await this.repository.manager.transaction(async (manager) => {
+ if (!data.isSynced) {
+ await manager.update(
+ MessageFolderEntity,
+ { id: In(ids), workspaceId },
+ { isSynced: false },
+ );
+ await manager.update(
+ MessageFolderEntity,
+ {
+ id: In(ids),
+ workspaceId,
+ pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_IMPORT,
+ },
+ { pendingSyncAction: MessageFolderPendingSyncAction.NONE },
+ );
+
+ return;
+ }
+
+ const folderIdsToBackfill = (
+ await manager.find(MessageFolderEntity, {
+ where: {
+ id: In(ids),
+ workspaceId,
+ isSynced: false,
+ messageChannel: {
+ connectedAccount: { provider: ConnectedAccountProvider.GOOGLE },
+ },
+ },
+ })
+ ).map((folder) => folder.id);
+
+ await manager.update(
+ MessageFolderEntity,
+ { id: In(ids), workspaceId },
+ { isSynced: true },
+ );
+
+ if (folderIdsToBackfill.length > 0) {
+ await manager.update(
+ MessageFolderEntity,
+ { id: In(folderIdsToBackfill), workspaceId },
+ { pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_IMPORT },
+ );
+ }
+ });
return this.repository.find({ where: { id: In(ids), workspaceId } });
}
diff --git a/packages/twenty-server/src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver.ts
index e0660cc6f6..6958c7e512 100644
--- a/packages/twenty-server/src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/message-folder/resolvers/message-folder.resolver.ts
@@ -86,7 +86,7 @@ export class MessageFolderResolver {
),
);
- return this.messageFolderMetadataService.updateMany({
+ return this.messageFolderMetadataService.setSyncStatus({
ids: input.ids,
workspaceId: workspace.id,
data: input.update,
diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts
index 17de4c99ed..ea6a83aa50 100644
--- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts
+++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service.ts
@@ -28,7 +28,7 @@ export class GmailGetMessageListService {
private readonly gmailMessageListFetchErrorHandler: GmailMessageListFetchErrorHandler,
) {}
- private async getMessageListWithoutCursor(
+ async getMessageListWithoutCursor(
connectedAccount: Pick<
ConnectedAccountEntity,
'provider' | 'id' | 'handle'
diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts
index a39f91e7c0..693806ffb8 100644
--- a/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts
+++ b/packages/twenty-server/src/modules/messaging/message-import-manager/messaging-import-manager.module.ts
@@ -18,7 +18,6 @@ import { RefreshTokensManagerModule } from 'src/modules/connected-account/refres
import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module';
import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cleaner/messaging-message-cleaner.module';
import { MessagingFolderSyncManagerModule } from 'src/modules/messaging/message-folder-manager/messaging-folder-sync-manager.module';
-import { MessagingSingleMessageImportCommand } from 'src/modules/messaging/message-import-manager/commands/messaging-single-message-import.command';
import { MessagingTriggerMessageListFetchCommand } from 'src/modules/messaging/message-import-manager/commands/messaging-trigger-message-list-fetch.command';
import { MessagingMessageListFetchCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-message-list-fetch.cron.command';
import { MessagingMessagesImportCronCommand } from 'src/modules/messaging/message-import-manager/crons/commands/messaging-messages-import.cron.command';
@@ -34,7 +33,6 @@ import { MessagingInboundEmailDriverModule } from 'src/modules/messaging/message
import { InboundEmailImportService } from 'src/modules/messaging/message-import-manager/drivers/inbound-email/services/inbound-email-import.service';
import { MessagingMicrosoftDriverModule } from 'src/modules/messaging/message-import-manager/drivers/microsoft/messaging-microsoft-driver.module';
import { MessagingSmtpDriverModule } from 'src/modules/messaging/message-import-manager/drivers/smtp/messaging-smtp-driver.module';
-import { MessagingAddSingleMessageToCacheForImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-add-single-message-to-cache-for-import.job';
import { MessagingCleanCacheJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-clean-cache';
import { MessagingInboundEmailImportJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-inbound-email-import.job';
import { MessagingMessageListFetchJob } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
@@ -44,6 +42,7 @@ import { MessagingRelaunchFailedMessageChannelJob } from 'src/modules/messaging/
import { MessagingCursorService } from 'src/modules/messaging/message-import-manager/services/messaging-cursor.service';
import { MessagingDeleteFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-delete-folder-messages.service';
import { MessagingDeleteGroupEmailMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-delete-group-email-messages.service';
+import { MessagingImportFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service';
import { MessagingGetMessageListService } from 'src/modules/messaging/message-import-manager/services/messaging-get-message-list.service';
import { MessagingGetMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-get-messages.service';
import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
@@ -92,7 +91,6 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
MessagingMessagesImportCronCommand,
MessagingOngoingStaleCronCommand,
MessagingRelaunchFailedMessageChannelsCronCommand,
- MessagingSingleMessageImportCommand,
MessagingTriggerMessageListFetchCommand,
MessagingMessageListFetchJob,
MessagingMessagesImportJob,
@@ -102,7 +100,6 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
MessagingMessagesImportCronJob,
MessagingOngoingStaleCronJob,
MessagingRelaunchFailedMessageChannelsCronJob,
- MessagingAddSingleMessageToCacheForImportJob,
MessagingCleanCacheJob,
MessagingInboundEmailImportJob,
MessagingMessageService,
@@ -117,6 +114,7 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
MessagingProcessFolderActionsService,
MessagingProcessGroupEmailActionsService,
MessagingDeleteFolderMessagesService,
+ MessagingImportFolderMessagesService,
MessagingDeleteGroupEmailMessagesService,
InboundEmailImportService,
],
diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service.ts
new file mode 100644
index 0000000000..ffccee41ce
--- /dev/null
+++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service.ts
@@ -0,0 +1,49 @@
+import { Injectable } from '@nestjs/common';
+
+import {
+ ConnectedAccountProvider,
+ MessageFolderImportPolicy,
+} from 'twenty-shared/types';
+
+import { type MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
+import { type MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
+import { GmailGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service';
+
+@Injectable()
+export class MessagingImportFolderMessagesService {
+ constructor(
+ private readonly gmailGetMessageListService: GmailGetMessageListService,
+ ) {}
+
+ async getFolderMessageIdsToImport(
+ messageChannel: MessageChannelEntity,
+ messageFolder: MessageFolderEntity,
+ ): Promise {
+ switch (messageChannel.connectedAccount.provider) {
+ case ConnectedAccountProvider.GOOGLE: {
+ const foldersScopedToImportedFolder = messageChannel.messageFolders.map(
+ (folder) => ({
+ name: folder.name,
+ externalId: folder.externalId,
+ parentFolderId: folder.parentFolderId,
+ isSynced: folder.id === messageFolder.id,
+ }),
+ );
+
+ const [messageList] =
+ await this.gmailGetMessageListService.getMessageListWithoutCursor(
+ messageChannel.connectedAccount,
+ foldersScopedToImportedFolder,
+ {
+ messageFolderImportPolicy:
+ MessageFolderImportPolicy.SELECTED_FOLDERS,
+ },
+ );
+
+ return messageList?.messageExternalIds ?? [];
+ }
+ default:
+ return [];
+ }
+ }
+}
diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts
index 46a03a7105..48d1ab64bb 100644
--- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts
+++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-message-list-fetch.service.ts
@@ -27,7 +27,10 @@ import {
MessageImportSyncStep,
} from 'src/modules/messaging/message-import-manager/services/messaging-import-exception-handler.service';
import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service';
-import { MessagingProcessFolderActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service';
+import {
+ MessagingProcessFolderActionsService,
+ type ProcessFolderActionsResult,
+} from 'src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service';
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
@@ -68,7 +71,7 @@ export class MessagingMessageListFetchService {
workspaceId,
);
- const pendingFolderActionsProcessed =
+ const processedfolderActionsResult =
await this.processPendingFolderActions(messageChannel, workspaceId);
await this.messageChannelSyncStatusService.markAsMessagesListFetchOngoing(
@@ -81,7 +84,8 @@ export class MessagingMessageListFetchService {
);
const freshMessageChannel =
- pendingGroupEmailActionsProcessed || pendingFolderActionsProcessed
+ pendingGroupEmailActionsProcessed ||
+ isDefined(processedfolderActionsResult)
? await this.messageChannelRepository.findOne({
where: {
id: messageChannel.id,
@@ -120,9 +124,12 @@ export class MessagingMessageListFetchService {
`messages-to-import:${workspaceId}:${freshMessageChannel.id}`,
);
- const messageExternalIds = messageLists.flatMap(
- (messageList) => messageList.messageExternalIds,
- );
+ const messageExternalIds = [
+ ...messageLists.flatMap(
+ (messageList) => messageList.messageExternalIds,
+ ),
+ ...(processedfolderActionsResult?.messageExternalIdsToImport ?? []),
+ ];
const messageExternalIdsToDelete = messageLists.flatMap(
(messageList) => messageList.messageExternalIdsToDelete,
@@ -312,7 +319,7 @@ export class MessagingMessageListFetchService {
private async processPendingFolderActions(
messageChannel: MessageChannelEntity,
workspaceId: string,
- ): Promise {
+ ): Promise {
const foldersWithPendingActions = messageChannel.messageFolders.filter(
(folder) =>
isDefined(folder.pendingSyncAction) &&
@@ -320,20 +327,18 @@ export class MessagingMessageListFetchService {
);
if (foldersWithPendingActions.length === 0) {
- return false;
+ return null;
}
this.logger.log(
`messageChannelId: ${messageChannel.id} Processing pending folder actions before message list fetch`,
);
- await this.messagingProcessFolderActionsService.processFolderActions(
+ return this.messagingProcessFolderActionsService.processFolderActions(
messageChannel,
foldersWithPendingActions,
workspaceId,
);
-
- return true;
}
private async computeFullSyncMessageChannelMessageAssociationsToDelete(
diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts
index 3ca45eb6d6..bfbf3f5765 100644
--- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts
+++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-process-folder-actions.service.ts
@@ -10,6 +10,11 @@ import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspac
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
import { MessagingDeleteFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-delete-folder-messages.service';
+import { MessagingImportFolderMessagesService } from 'src/modules/messaging/message-import-manager/services/messaging-import-folder-messages.service';
+
+export type ProcessFolderActionsResult = {
+ messageExternalIdsToImport: string[];
+};
@Injectable()
export class MessagingProcessFolderActionsService {
@@ -22,13 +27,14 @@ export class MessagingProcessFolderActionsService {
@InjectRepository(MessageFolderEntity)
private readonly messageFolderRepository: Repository,
private readonly messagingDeleteFolderMessagesService: MessagingDeleteFolderMessagesService,
+ private readonly messagingImportFolderMessagesService: MessagingImportFolderMessagesService,
) {}
async processFolderActions(
messageChannel: MessageChannelEntity,
messageFolders: MessageFolderEntity[],
workspaceId: string,
- ): Promise {
+ ): Promise {
const foldersWithPendingActions = messageFolders.filter(
(folder) =>
isDefined(folder.pendingSyncAction) &&
@@ -36,13 +42,14 @@ export class MessagingProcessFolderActionsService {
);
if (foldersWithPendingActions.length === 0) {
- return;
+ return { messageExternalIdsToImport: [] };
}
this.logger.log(
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id} - Processing ${foldersWithPendingActions.length} folders with pending actions`,
);
+ const messageExternalIdsToImport: string[] = [];
const folderIdsToDelete: string[] = [];
const processedFolderIds: string[] = [];
const failedFolderIds: Array<{ folderId: string; error: Error }> = [];
@@ -53,21 +60,37 @@ export class MessagingProcessFolderActionsService {
`WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${folder.id} - Processing folder action: ${folder.pendingSyncAction}`,
);
- if (
- folder.pendingSyncAction ===
- MessageFolderPendingSyncAction.FOLDER_DELETION
- ) {
- await this.messagingDeleteFolderMessagesService.deleteFolderMessages(
- workspaceId,
- messageChannel,
- folder,
- );
+ switch (folder.pendingSyncAction) {
+ case MessageFolderPendingSyncAction.FOLDER_DELETION: {
+ await this.messagingDeleteFolderMessagesService.deleteFolderMessages(
+ workspaceId,
+ messageChannel,
+ folder,
+ );
- folderIdsToDelete.push(folder.id);
+ folderIdsToDelete.push(folder.id);
- this.logger.debug(
- `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${folder.id} - Completed FOLDER_DELETION action`,
- );
+ this.logger.debug(
+ `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${folder.id} - Completed FOLDER_DELETION action`,
+ );
+ break;
+ }
+ case MessageFolderPendingSyncAction.FOLDER_IMPORT: {
+ const folderMessageExternalIdsToImport =
+ await this.messagingImportFolderMessagesService.getFolderMessageIdsToImport(
+ messageChannel,
+ folder,
+ );
+
+ messageExternalIdsToImport.push(
+ ...folderMessageExternalIdsToImport,
+ );
+
+ this.logger.debug(
+ `WorkspaceId: ${workspaceId}, MessageChannelId: ${messageChannel.id}, FolderId: ${folder.id} - Completed FOLDER_IMPORT action`,
+ );
+ break;
+ }
}
processedFolderIds.push(folder.id);
@@ -117,5 +140,9 @@ export class MessagingProcessFolderActionsService {
{ lite: true },
);
}
+
+ return {
+ messageExternalIdsToImport: [...new Set(messageExternalIdsToImport)],
+ };
}
}
diff --git a/packages/twenty-shared/src/types/MessageFolderPendingSyncAction.ts b/packages/twenty-shared/src/types/MessageFolderPendingSyncAction.ts
index 9e24eeba8e..f3c9e7396d 100644
--- a/packages/twenty-shared/src/types/MessageFolderPendingSyncAction.ts
+++ b/packages/twenty-shared/src/types/MessageFolderPendingSyncAction.ts
@@ -1,4 +1,5 @@
export enum MessageFolderPendingSyncAction {
FOLDER_DELETION = 'FOLDER_DELETION',
+ FOLDER_IMPORT = 'FOLDER_IMPORT',
NONE = 'NONE',
}