diff --git a/packages/twenty-front/src/modules/accounts/types/MessageFolder.ts b/packages/twenty-front/src/modules/accounts/types/MessageFolder.ts
index 471ba6a006..f8fa66382d 100644
--- a/packages/twenty-front/src/modules/accounts/types/MessageFolder.ts
+++ b/packages/twenty-front/src/modules/accounts/types/MessageFolder.ts
@@ -5,5 +5,7 @@ export type MessageFolder = {
isSentFolder: boolean;
isSynced: boolean;
messageChannelId: string;
+ parentFolderId: string | null;
+ externalId: string | null;
__typename: 'MessageFolder';
};
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsAccountsMessageFoldersCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsAccountsMessageFoldersCard.tsx
index ae5fe0506d..fe1d5a39f6 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsAccountsMessageFoldersCard.tsx
+++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsAccountsMessageFoldersCard.tsx
@@ -5,7 +5,8 @@ import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graph
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { SettingsMessageFoldersEmptyStateCard } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersEmptyStateCard';
-import { SettingsMessageFoldersTableRow } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersTableRow';
+import { SettingsMessageFoldersTreeItem } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem';
+import { computeMessageFolderTree } from '@/settings/accounts/components/message-folders/utils/computeMessageFolderTree';
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { Table } from '@/ui/layout/table/components/Table';
@@ -18,8 +19,14 @@ import { Label } from 'twenty-ui/display';
import { Checkbox, CheckboxSize } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
-const StyledTableRows = styled.div`
- max-height: 300px;
+const StyledTreeList = styled.ul`
+ list-style: none;
+ margin: 0;
+ padding: 0;
+`;
+
+const StyledFoldersContainer = styled.div`
+ max-height: 400px;
overflow-y: auto;
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(2)};
@@ -87,6 +94,10 @@ export const SettingsAccountsMessageFoldersCard = () => {
);
}, [messageFolders, search]);
+ const folderTreeNodes = useMemo(() => {
+ return computeMessageFolderTree(filteredMessageFolders);
+ }, [filteredMessageFolders]);
+
const allFoldersToggled = useMemo(() => {
return filteredMessageFolders.every((folder) => folder.isSynced);
}, [filteredMessageFolders]);
@@ -142,15 +153,17 @@ export const SettingsAccountsMessageFoldersCard = () => {
-
- {filteredMessageFolders?.map((folder) => (
- handleToggleFolder(folder)}
- />
- ))}
-
+
+
+ {folderTreeNodes.map((rootFolder) => (
+
+ ))}
+
+
);
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsMessageFoldersTableRow.tsx b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsMessageFoldersTableRow.tsx
deleted file mode 100644
index f8fa74a8c7..0000000000
--- a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsMessageFoldersTableRow.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { type MessageFolder } from '@/accounts/types/MessageFolder';
-import { SettingsAccountsMessageFolderIcon } from '@/settings/accounts/components/message-folders/SettingsAccountsMessageFolderIcon';
-import { formatFolderName } from '@/settings/accounts/components/message-folders/utils/formatFolderName.util';
-
-import { TableCell } from '@/ui/layout/table/components/TableCell';
-import { TableRow } from '@/ui/layout/table/components/TableRow';
-import styled from '@emotion/styled';
-import { Checkbox, CheckboxSize } from 'twenty-ui/input';
-
-const StyledFolderNameCell = styled.div`
- align-items: center;
- display: flex;
- gap: ${({ theme }) => theme.spacing(1)};
- color: ${({ theme }) => theme.font.color.primary};
-`;
-
-const StyledTableRow = styled(TableRow)``;
-
-const StyledCheckboxCell = styled(TableCell)`
- display: flex;
- justify-content: flex-end;
-`;
-
-type SettingsMessageFoldersTableRowProps = {
- folder: MessageFolder;
- onSyncToggle: () => void;
-};
-
-export const SettingsMessageFoldersTableRow = ({
- folder,
- onSyncToggle,
-}: SettingsMessageFoldersTableRowProps) => {
- return (
-
-
-
-
- {formatFolderName(folder.name)}
-
-
-
-
-
-
- );
-};
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
new file mode 100644
index 0000000000..52e76dea07
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem.tsx
@@ -0,0 +1,166 @@
+import { type MessageFolder } from '@/accounts/types/MessageFolder';
+import { SettingsAccountsMessageFolderIcon } from '@/settings/accounts/components/message-folders/SettingsAccountsMessageFolderIcon';
+
+import { type MessageFolderTreeNode } from '@/settings/accounts/components/message-folders/utils/computeMessageFolderTree';
+import { formatFolderName } from '@/settings/accounts/components/message-folders/utils/formatFolderName';
+import styled from '@emotion/styled';
+import { useState } from 'react';
+import { IconChevronRight } from 'twenty-ui/display';
+import { Checkbox, CheckboxSize } from 'twenty-ui/input';
+
+type SettingsMessageFoldersTreeItemProps = {
+ folderTreeNode: MessageFolderTreeNode;
+ onToggleFolder: (folder: MessageFolder) => void;
+ depth?: number;
+};
+
+const StyledTreeItem = styled.li<{ hasChildren: boolean; depth: number }>`
+ position: relative;
+ margin-left: ${({ hasChildren, depth, theme }) =>
+ !hasChildren && depth > 0 ? theme.spacing(3) : 0};
+
+ &:not(:last-child) {
+ margin-bottom: ${({ theme }) => theme.spacing(1)};
+ }
+`;
+
+const StyledNestedList = styled.ul`
+ border-left: 1px solid ${({ theme }) => theme.border.color.medium};
+ list-style: none;
+ margin: ${({ theme }) => theme.spacing(1)} 0 0
+ ${({ theme }) => theme.spacing(3)};
+ padding: 0;
+ padding-left: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledTreeItemContent = styled.div`
+ display: flex;
+ align-items: center;
+ gap: ${({ theme }) => theme.spacing(2)};
+ padding: ${({ theme }) => theme.spacing(1)};
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ cursor: pointer;
+ transition: background-color
+ ${({ theme }) => theme.animation.duration.instant}s;
+
+ &:hover {
+ background-color: ${({ theme }) => theme.background.transparent.lighter};
+ }
+`;
+
+const StyledExpandButton = styled.button<{ isExpanded: boolean }>`
+ background: none;
+ border: none;
+ padding: 0;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: ${({ theme }) => theme.spacing(4)};
+ height: ${({ theme }) => theme.spacing(4)};
+ color: ${({ theme }) => theme.font.color.tertiary};
+ transition: transform ${({ theme }) => theme.animation.duration.instant}s;
+ transform: ${({ isExpanded }) =>
+ isExpanded ? 'rotate(90deg)' : 'rotate(0deg)'};
+
+ &:hover {
+ color: ${({ theme }) => theme.font.color.primary};
+ }
+`;
+
+const StyledFolderContent = styled.div`
+ align-items: center;
+ display: flex;
+ flex: 1;
+ gap: ${({ theme }) => theme.spacing(2)};
+ justify-content: space-between;
+`;
+
+const StyledFolderInfo = styled.div`
+ display: flex;
+ align-items: center;
+ gap: ${({ theme }) => theme.spacing(2)};
+ flex: 1;
+ min-width: 0;
+`;
+
+const StyledFolderName = styled.span`
+ color: ${({ theme }) => theme.font.color.primary};
+ font-size: ${({ theme }) => theme.font.size.md};
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+`;
+
+const StyledCheckboxWrapper = styled.div`
+ align-items: center;
+ display: flex;
+`;
+
+export const SettingsMessageFoldersTreeItem = ({
+ folderTreeNode,
+ onToggleFolder,
+ depth = 0,
+}: SettingsMessageFoldersTreeItemProps) => {
+ const [isExpanded, setIsExpanded] = useState(true);
+ const { folder, children, hasChildren } = folderTreeNode;
+
+ const handleExpandToggle = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ setIsExpanded(!isExpanded);
+ };
+
+ const handleRowClick = () => {
+ if (hasChildren) {
+ setIsExpanded(!isExpanded);
+ }
+ };
+
+ const handleCheckboxClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ };
+
+ return (
+
+
+ {hasChildren && (
+
+
+
+ )}
+
+
+
+
+ {formatFolderName(folder.name)}
+
+
+
+ onToggleFolder(folder)}
+ size={CheckboxSize.Small}
+ />
+
+
+
+
+ {hasChildren && isExpanded && (
+
+ {children.map((child) => (
+
+ ))}
+
+ )}
+
+ );
+};
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
new file mode 100644
index 0000000000..58af3f81be
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/__tests__/computeMessageFolderTree.test.ts
@@ -0,0 +1,149 @@
+import { type MessageFolder } from '@/accounts/types/MessageFolder';
+import { computeMessageFolderTree } from '@/settings/accounts/components/message-folders/utils/computeMessageFolderTree';
+
+describe('computeMessageFolderTree', () => {
+ const createFolder = (
+ id: string,
+ name: string,
+ parentFolderId: string | null = null,
+ externalId: string | null = null,
+ ): MessageFolder => ({
+ id,
+ name,
+ parentFolderId,
+ externalId: externalId || id,
+ isSentFolder: false,
+ isSynced: false,
+ messageChannelId: '20202020-1c25-4d02-bf25-6aeccf7ea419',
+ __typename: 'MessageFolder',
+ syncCursor: '',
+ });
+
+ it('should return empty array for empty input', () => {
+ expect(computeMessageFolderTree([])).toEqual([]);
+ });
+
+ it('should return single root folder without children', () => {
+ const folder = createFolder(
+ '20202020-7cf8-40bc-a681-b80b771449b1',
+ 'Inbox',
+ null,
+ );
+ const result = computeMessageFolderTree([folder]);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].folder).toEqual(folder);
+ expect(result[0].hasChildren).toBe(false);
+ expect(result[0].children).toEqual([]);
+ });
+
+ it('should organize parent-child relationship', () => {
+ const parent = createFolder(
+ '20202020-7cf8-40bc-a681-b80b771449b2',
+ 'Work',
+ null,
+ 'ext-parent-id',
+ );
+ const child = createFolder(
+ '20202020-7cf8-40bc-a681-b80b771449b3',
+ 'Projects',
+ 'ext-parent-id',
+ 'ext-child-id',
+ );
+ const result = computeMessageFolderTree([parent, child]);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].folder.name).toBe('Work');
+ expect(result[0].hasChildren).toBe(true);
+ expect(result[0].children).toHaveLength(1);
+ expect(result[0].children[0].folder.name).toBe('Projects');
+ });
+
+ it('should handle multiple levels of nesting', () => {
+ const grandparent = createFolder(
+ '20202020-7cf8-40bc-a681-b80b771449b4',
+ 'Work',
+ null,
+ 'ext-grandparent-id',
+ );
+ const parent = createFolder(
+ '20202020-7cf8-40bc-a681-b80b771449b5',
+ 'Projects',
+ 'ext-grandparent-id',
+ 'ext-parent-id',
+ );
+ const child = createFolder(
+ '20202020-7cf8-40bc-a681-b80b771449b6',
+ '2024',
+ 'ext-parent-id',
+ 'ext-child-id',
+ );
+ const result = computeMessageFolderTree([grandparent, parent, child]);
+
+ expect(result[0].folder.name).toBe('Work');
+ expect(result[0].children[0].folder.name).toBe('Projects');
+ expect(result[0].children[0].children[0].folder.name).toBe('2024');
+ });
+
+ it('should sort root folders alphabetically', () => {
+ const folders = [
+ createFolder('20202020-7cf8-40bc-a681-b80b771449b7', 'Sent', null),
+ createFolder('20202020-7cf8-40bc-a681-b80b771449b8', 'Inbox', null),
+ createFolder('20202020-7cf8-40bc-a681-b80b771449b9', 'Drafts', null),
+ ];
+ const result = computeMessageFolderTree(folders);
+
+ expect(result[0].folder.name).toBe('Drafts');
+ expect(result[1].folder.name).toBe('Inbox');
+ expect(result[2].folder.name).toBe('Sent');
+ });
+
+ it('should sort children alphabetically', () => {
+ const parent = createFolder(
+ '20202020-7cf8-40bc-a681-b80b771449c0',
+ 'Work',
+ null,
+ 'ext-parent-id',
+ );
+ const child1 = createFolder(
+ '20202020-7cf8-40bc-a681-b80b771449c1',
+ 'Zebra',
+ 'ext-parent-id',
+ 'ext-child1-id',
+ );
+ const child2 = createFolder(
+ '20202020-7cf8-40bc-a681-b80b771449c2',
+ 'Apple',
+ 'ext-parent-id',
+ 'ext-child2-id',
+ );
+ const result = computeMessageFolderTree([parent, child1, child2]);
+
+ expect(result[0].children[0].folder.name).toBe('Apple');
+ expect(result[0].children[1].folder.name).toBe('Zebra');
+ });
+
+ it('should treat orphaned folders as root folders', () => {
+ const orphan = createFolder('o', 'Orphan', 'non-existent', 'ext-o');
+ const normal = createFolder('n', 'Normal', null, 'ext-n');
+ const result = computeMessageFolderTree([orphan, normal]);
+
+ expect(result).toHaveLength(2);
+ expect(result.map((r) => r.folder.name).sort()).toEqual([
+ 'Normal',
+ 'Orphan',
+ ]);
+ });
+
+ it('should handle Gmail-style nested labels', () => {
+ const work = createFolder('1', 'Work', null, 'ext-work');
+ const projects = createFolder('2', 'Projects', 'ext-work', 'ext-proj');
+ const clients = createFolder('3', 'Clients', 'ext-work', 'ext-cli');
+ const result = computeMessageFolderTree([work, projects, clients]);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].children).toHaveLength(2);
+ expect(result[0].children[0].folder.name).toBe('Clients');
+ expect(result[0].children[1].folder.name).toBe('Projects');
+ });
+});
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeMessageFolderTree.ts b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeMessageFolderTree.ts
new file mode 100644
index 0000000000..9d4d291d29
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/computeMessageFolderTree.ts
@@ -0,0 +1,55 @@
+import { type MessageFolder } from '@/accounts/types/MessageFolder';
+import { isDefined } from 'twenty-shared/utils';
+
+export type MessageFolderTreeNode = {
+ folder: MessageFolder;
+ children: MessageFolderTreeNode[];
+ hasChildren: boolean;
+};
+
+export const computeMessageFolderTree = (
+ folders: MessageFolder[],
+): MessageFolderTreeNode[] => {
+ const folderByExternalIdMap = new Map();
+ const childrenMap = new Map();
+
+ folders.forEach((folder) => {
+ if (isDefined(folder.externalId)) {
+ folderByExternalIdMap.set(folder.externalId, folder);
+ }
+ });
+
+ folders.forEach((folder) => {
+ if (isDefined(folder.parentFolderId)) {
+ const parent = folderByExternalIdMap.get(folder.parentFolderId);
+ if (isDefined(parent)) {
+ const siblings = childrenMap.get(parent.id) || [];
+ siblings.push(folder);
+ childrenMap.set(parent.id, siblings);
+ }
+ }
+ });
+
+ const buildTreeNode = (folder: MessageFolder): MessageFolderTreeNode => {
+ const children = childrenMap.get(folder.id) || [];
+ const sortedChildren = [...children].sort((a, b) =>
+ a.name.localeCompare(b.name),
+ );
+
+ return {
+ folder,
+ children: sortedChildren.map((child) => buildTreeNode(child)),
+ hasChildren: children.length > 0,
+ };
+ };
+
+ const rootFolders = folders.filter((folder) => {
+ if (!folder.parentFolderId) return true;
+
+ return !folderByExternalIdMap.has(folder.parentFolderId);
+ });
+
+ rootFolders.sort((a, b) => a.name.localeCompare(b.name));
+
+ return rootFolders.map((folder) => buildTreeNode(folder));
+};
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/formatFolderName.util.ts b/packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/formatFolderName.ts
similarity index 100%
rename from packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/formatFolderName.util.ts
rename to packages/twenty-front/src/modules/settings/accounts/components/message-folders/utils/formatFolderName.ts
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts
index 9ba624f8c6..9d9b27078d 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts
@@ -260,6 +260,7 @@ export const MESSAGE_CHANNEL_STANDARD_FIELD_IDS = {
export const MESSAGE_FOLDER_STANDARD_FIELD_IDS = {
name: '20202020-7cf8-40bc-a681-b80b771449b7',
+ parentFolderId: '20202020-e45d-49de-a4aa-587bbf9601f3',
messageChannel: '20202020-b658-408f-bd46-3bd2d15d7e52',
syncCursor: '20202020-98cd-49ed-8dfc-cb5796400e64',
isSentFolder: '20202020-2af5-4a25-b2de-3c9386da941b',
diff --git a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-folder.workspace-entity.ts b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-folder.workspace-entity.ts
index e8ab95b10d..6fa4c6c88f 100644
--- a/packages/twenty-server/src/modules/messaging/common/standard-objects/message-folder.workspace-entity.ts
+++ b/packages/twenty-server/src/modules/messaging/common/standard-objects/message-folder.workspace-entity.ts
@@ -91,6 +91,17 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
})
isSynced: boolean;
+ @WorkspaceField({
+ standardId: MESSAGE_FOLDER_STANDARD_FIELD_IDS.parentFolderId,
+ type: FieldMetadataType.TEXT,
+ label: msg`Parent Folder ID`,
+ description: msg`Parent Folder ID`,
+ icon: 'IconFolder',
+ defaultValue: null,
+ })
+ @WorkspaceIsNullable()
+ parentFolderId: string | null;
+
@WorkspaceField({
standardId: MESSAGE_FOLDER_STANDARD_FIELD_IDS.externalId,
type: FieldMetadataType.TEXT,
diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service.ts
index 9ab942b72d..bd2bdb35a8 100644
--- a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service.ts
+++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/gmail-get-all-folders.service.ts
@@ -7,6 +7,8 @@ import {
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
+import { extractGmailFolderName } from 'src/modules/messaging/message-folder-manager/drivers/gmail/utils/extract-gmail-folder-name.util';
+import { getGmailFolderParentId } from 'src/modules/messaging/message-folder-manager/drivers/gmail/utils/get-gmail-folder-parent-id.util';
import { MESSAGING_GMAIL_DEFAULT_NOT_SYNCED_LABELS } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-default-not-synced-labels';
import { GmailMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-message-list-fetch-error-handler.service';
@@ -53,18 +55,34 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
const folders: MessageFolder[] = [];
+ const labelNameToIdMap = new Map();
+
+ for (const label of labels) {
+ if (!label.name || !label.id) {
+ continue;
+ }
+
+ labelNameToIdMap.set(label.name, label.id);
+ }
+
for (const label of labels) {
if (!label.name || !label.id) {
continue;
}
const isSentFolder = label.id === 'SENT';
+ const folderName = extractGmailFolderName(label.name);
+ const parentFolderId = getGmailFolderParentId(
+ label.name,
+ labelNameToIdMap,
+ );
folders.push({
externalId: label.id,
- name: label.name,
+ name: folderName,
isSynced: this.isSyncedByDefault(label.id),
isSentFolder,
+ parentFolderId,
});
}
diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/utils/__tests__/extract-gmail-folder-name.util.spec.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/utils/__tests__/extract-gmail-folder-name.util.spec.ts
new file mode 100644
index 0000000000..0eb75fb3be
--- /dev/null
+++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/utils/__tests__/extract-gmail-folder-name.util.spec.ts
@@ -0,0 +1,30 @@
+import { extractGmailFolderName } from 'src/modules/messaging/message-folder-manager/drivers/gmail/utils/extract-gmail-folder-name.util';
+
+describe('extractGmailFolderName', () => {
+ it('should return full name for top-level folders', () => {
+ expect(extractGmailFolderName('Inbox')).toBe('Inbox');
+ expect(extractGmailFolderName('Sent')).toBe('Sent');
+ });
+
+ it('should extract folder name from nested folder', () => {
+ expect(extractGmailFolderName('Work/Projects')).toBe('Projects');
+ });
+
+ it('should extract folder name from deeply nested folder', () => {
+ expect(extractGmailFolderName('Work/Projects/2024')).toBe('2024');
+ });
+
+ it('should handle Gmail-style nested labels', () => {
+ expect(extractGmailFolderName('[Gmail]/Sent Mail')).toBe('Sent Mail');
+ });
+
+ it('should handle single character names', () => {
+ expect(extractGmailFolderName('A/B/C')).toBe('C');
+ });
+
+ it('should handle special characters', () => {
+ expect(extractGmailFolderName('Work/Client - ABC Corp')).toBe(
+ 'Client - ABC Corp',
+ );
+ });
+});
diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/utils/__tests__/get-gmail-folder-parent-id.util.spec.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/utils/__tests__/get-gmail-folder-parent-id.util.spec.ts
new file mode 100644
index 0000000000..40902a2c40
--- /dev/null
+++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/utils/__tests__/get-gmail-folder-parent-id.util.spec.ts
@@ -0,0 +1,56 @@
+import { getGmailFolderParentId } from 'src/modules/messaging/message-folder-manager/drivers/gmail/utils/get-gmail-folder-parent-id.util';
+
+describe('getGmailFolderParentId', () => {
+ it('should return null for top-level folders without slash', () => {
+ const labelNameToIdMap = new Map([
+ ['Inbox', 'INBOX'],
+ ['Sent', 'SENT'],
+ ]);
+
+ expect(getGmailFolderParentId('Inbox', labelNameToIdMap)).toBeNull();
+ });
+
+ it('should return parent ID for nested folder', () => {
+ const labelNameToIdMap = new Map([
+ ['Work', 'work-id'],
+ ['Work/Projects', 'projects-id'],
+ ]);
+
+ expect(getGmailFolderParentId('Work/Projects', labelNameToIdMap)).toBe(
+ 'work-id',
+ );
+ });
+
+ it('should return parent ID for deeply nested folder', () => {
+ const labelNameToIdMap = new Map([
+ ['Work', 'work-id'],
+ ['Work/Projects', 'projects-id'],
+ ['Work/Projects/2024', '2024-id'],
+ ]);
+
+ expect(getGmailFolderParentId('Work/Projects/2024', labelNameToIdMap)).toBe(
+ 'projects-id',
+ );
+ });
+
+ it('should return null if parent folder does not exist in map', () => {
+ const labelNameToIdMap = new Map([
+ ['Work/Projects', 'projects-id'],
+ ]);
+
+ expect(
+ getGmailFolderParentId('Work/Projects', labelNameToIdMap),
+ ).toBeNull();
+ });
+
+ it('should handle Gmail-style nested labels', () => {
+ const labelNameToIdMap = new Map([
+ ['[Gmail]', 'gmail-id'],
+ ['[Gmail]/Sent Mail', 'sent-id'],
+ ]);
+
+ expect(getGmailFolderParentId('[Gmail]/Sent Mail', labelNameToIdMap)).toBe(
+ 'gmail-id',
+ );
+ });
+});
diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/utils/extract-gmail-folder-name.util.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/utils/extract-gmail-folder-name.util.ts
new file mode 100644
index 0000000000..2f24f6a6e3
--- /dev/null
+++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/utils/extract-gmail-folder-name.util.ts
@@ -0,0 +1,7 @@
+export const extractGmailFolderName = (labelName: string): string => {
+ if (!labelName.includes('/')) {
+ return labelName;
+ }
+
+ return labelName.substring(labelName.lastIndexOf('/') + 1);
+};
diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/utils/get-gmail-folder-parent-id.util.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/utils/get-gmail-folder-parent-id.util.ts
new file mode 100644
index 0000000000..7c7002724b
--- /dev/null
+++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/gmail/utils/get-gmail-folder-parent-id.util.ts
@@ -0,0 +1,11 @@
+export const getGmailFolderParentId = (
+ labelName: string,
+ labelNameToIdMap: Map,
+): string | null => {
+ if (!labelName.includes('/')) {
+ return null;
+ }
+ const parentName = labelName.substring(0, labelName.lastIndexOf('/'));
+
+ return labelNameToIdMap.get(parentName) || null;
+};
diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/imap/imap-get-all-folders.service.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/imap/imap-get-all-folders.service.ts
index d4081e6fc7..3fe5df7679 100644
--- a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/imap/imap-get-all-folders.service.ts
+++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/imap/imap-get-all-folders.service.ts
@@ -55,47 +55,64 @@ export class ImapGetAllFoldersService implements MessageFolderDriver {
mailboxList: ListResponse[],
): Promise {
const folders: MessageFolder[] = [];
- const sentFolderPath =
+ const pathToExternalIdMap = new Map();
+ const sentFolder =
await this.imapFindSentFolderService.findSentFolder(client);
- if (isDefined(sentFolderPath)) {
- const sentMailbox = mailboxList.find((m) => m.path === sentFolderPath);
+ if (isDefined(sentFolder)) {
+ const sentMailbox = mailboxList.find((m) => m.path === sentFolder.path);
const uidValidity = sentMailbox
? await this.getUidValidity(client, sentMailbox)
: null;
+ const externalId = uidValidity
+ ? `${sentFolder.path}:${uidValidity.toString()}`
+ : sentFolder.path;
+
+ pathToExternalIdMap.set(sentFolder.path, externalId);
+
folders.push({
- externalId: uidValidity
- ? `${sentFolderPath}:${uidValidity.toString()}`
- : sentFolderPath,
- name: sentFolderPath,
+ externalId,
+ name: sentFolder.name,
isSynced: true,
isSentFolder: true,
+ parentFolderId: sentMailbox?.parentPath || null,
});
}
- const validMailboxes = mailboxList.filter((mailbox) =>
- this.isValidMailbox(mailbox, folders),
- );
-
- for (const mailbox of validMailboxes) {
- const isInbox = await this.isInboxFolder(mailbox);
+ for (const mailbox of mailboxList) {
const uidValidity = await this.getUidValidity(client, mailbox);
- const standardFolder = getStandardFolderByRegex(mailbox.path);
- const isSynced = this.shouldSyncByDefault(
- mailbox,
- standardFolder,
- isInbox,
- );
+ const externalId = uidValidity
+ ? `${mailbox.path}:${uidValidity}`
+ : mailbox.path;
- folders.push({
- externalId: uidValidity
- ? `${mailbox.path}:${uidValidity}`
- : mailbox.path,
- name: mailbox.path,
- isSynced,
- isSentFolder: false,
- });
+ pathToExternalIdMap.set(mailbox.path, externalId);
+
+ if (this.isValidMailbox(mailbox, folders)) {
+ const isInbox = await this.isInboxFolder(mailbox);
+ const standardFolder = getStandardFolderByRegex(mailbox.path);
+ const isSynced = this.shouldSyncByDefault(
+ mailbox,
+ standardFolder,
+ isInbox,
+ );
+
+ folders.push({
+ externalId,
+ name: mailbox.name,
+ isSynced,
+ isSentFolder: false,
+ parentFolderId: mailbox.parentPath || null,
+ });
+ }
+ }
+
+ for (const folder of folders) {
+ if (folder.parentFolderId) {
+ const parentExternalId = pathToExternalIdMap.get(folder.parentFolderId);
+
+ folder.parentFolderId = parentExternalId || null;
+ }
}
return folders;
@@ -109,9 +126,15 @@ export class ImapGetAllFoldersService implements MessageFolderDriver {
return false;
}
- const isDuplicate = existingFolders.some(
- (folder) => folder.name === mailbox.path,
- );
+ const isDuplicate = existingFolders.some((folder) => {
+ const folderPath = folder?.externalId?.split(':')[0];
+
+ if (!isDefined(folderPath)) {
+ return false;
+ }
+
+ return folderPath === mailbox.path;
+ });
return !isDuplicate;
}
diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/microsoft/microsoft-get-all-folders.service.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/microsoft/microsoft-get-all-folders.service.ts
index dd23c8fff7..04066a332d 100644
--- a/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/microsoft/microsoft-get-all-folders.service.ts
+++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/drivers/microsoft/microsoft-get-all-folders.service.ts
@@ -1,5 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
+import { isDefined } from 'twenty-shared/utils';
+
import {
MessageFolder,
MessageFolderDriver,
@@ -9,13 +11,13 @@ import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MicrosoftMessageListFetchErrorHandler } from 'src/modules/messaging/message-import-manager/drivers/microsoft/services/microsoft-message-list-fetch-error-handler.service';
import { StandardFolder } from 'src/modules/messaging/message-import-manager/drivers/types/standard-folder';
-import { getStandardFolderByRegex } from 'src/modules/messaging/message-import-manager/drivers/utils/get-standard-folder-by-regex';
type MicrosoftGraphFolder = {
id: string;
displayName: string;
childFolderCount?: number;
parentFolderId?: string;
+ wellKnownName?: string;
};
const MESSAGING_MICROSOFT_MAIL_FOLDERS_LIST_MAX_RESULT = 999;
@@ -56,6 +58,7 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
});
const folders = (response.value as MicrosoftGraphFolder[]) || [];
+ const rootFolderId = this.getRootFolderId(folders);
const folderInfos: MessageFolder[] = [];
for (const folder of folders) {
@@ -63,7 +66,9 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
continue;
}
- const standardFolder = getStandardFolderByRegex(folder.displayName);
+ const standardFolder = this.getStandardFolderFromWellKnownName(
+ folder.wellKnownName,
+ );
const isSentFolder = this.isSentFolder(standardFolder);
const isSynced = this.shouldSyncByDefault(standardFolder);
@@ -72,6 +77,10 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
name: folder.displayName,
isSynced,
isSentFolder,
+ parentFolderId: this.getParentFolderId(
+ folder.parentFolderId,
+ rootFolderId,
+ ),
});
}
@@ -105,4 +114,57 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
return true;
}
+
+ private getStandardFolderFromWellKnownName(
+ wellKnownName?: string,
+ ): StandardFolder | null {
+ if (!isDefined(wellKnownName)) {
+ return null;
+ }
+
+ switch (wellKnownName.toLowerCase()) {
+ case 'inbox':
+ return StandardFolder.INBOX;
+ case 'drafts':
+ return StandardFolder.DRAFTS;
+ case 'sentitems':
+ return StandardFolder.SENT;
+ case 'deleteditems':
+ return StandardFolder.TRASH;
+ case 'junkemail':
+ return StandardFolder.JUNK;
+ default:
+ return null;
+ }
+ }
+
+ /*
+ * All Microsoft folders have a parentFolderId including the standard folders
+ * which point to root node which doesn't exits in the API response.
+ * We remove this to simplify the folder hierarchy on frontend.
+ */
+ private getRootFolderId(folders: MicrosoftGraphFolder[]): string | null {
+ for (const folder of folders) {
+ if (isDefined(folder.wellKnownName) && isDefined(folder.parentFolderId)) {
+ return folder.parentFolderId;
+ }
+ }
+
+ return null;
+ }
+
+ private getParentFolderId(
+ parentFolderId: string | undefined,
+ rootFolderId: string | null,
+ ): string | null {
+ if (!isDefined(parentFolderId)) {
+ return null;
+ }
+
+ if (parentFolderId === rootFolderId) {
+ return null;
+ }
+
+ return parentFolderId;
+ }
}
diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface.ts
index 78d7aed8eb..7ac6f31953 100644
--- a/packages/twenty-server/src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface.ts
+++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface.ts
@@ -3,7 +3,7 @@ import { type MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/
export type MessageFolder = Pick<
MessageFolderWorkspaceEntity,
- 'name' | 'isSynced' | 'isSentFolder' | 'externalId'
+ 'name' | 'isSynced' | 'isSentFolder' | 'externalId' | 'parentFolderId'
>;
export interface MessageFolderDriver {
diff --git a/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts b/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts
index 56451e4612..b79acce18d 100644
--- a/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts
+++ b/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts
@@ -32,10 +32,14 @@ type MessageFolderToInsert = Pick<
| 'isSynced'
| 'isSentFolder'
| 'externalId'
+ | 'parentFolderId'
>;
type MessageFolderToUpdate = Partial<
- Pick
+ Pick<
+ MessageFolderWorkspaceEntity,
+ 'name' | 'externalId' | 'isSentFolder' | 'parentFolderId'
+ >
>;
@Injectable()
@@ -114,6 +118,7 @@ export class SyncMessageFoldersService {
name: folder.name,
externalId: folder.externalId,
isSentFolder: folder.isSentFolder,
+ parentFolderId: folder.parentFolderId,
},
]);
continue;
@@ -127,6 +132,7 @@ export class SyncMessageFoldersService {
isSynced: folder.isSynced,
isSentFolder: folder.isSentFolder,
externalId: folder.externalId,
+ parentFolderId: folder.parentFolderId,
});
}
diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-find-sent-folder.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-find-sent-folder.service.ts
index 2d91cdfc34..91711af64a 100644
--- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-find-sent-folder.service.ts
+++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-find-sent-folder.service.ts
@@ -5,6 +5,11 @@ import { ListResponse, type ImapFlow } from 'imapflow';
import { getImapSentFolderCandidatesByRegex } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/get-sent-folder-candidates-by-regex.util';
+type SentFolderResult = {
+ name: string;
+ path: string;
+} | null;
+
/**
* Service to find sent folder using IMAP special-use flags
*
@@ -19,7 +24,7 @@ import { getImapSentFolderCandidatesByRegex } from 'src/modules/messaging/messag
export class ImapFindSentFolderService {
private readonly logger = new Logger(ImapFindSentFolderService.name);
- public async findSentFolder(client: ImapFlow): Promise {
+ public async findSentFolder(client: ImapFlow): Promise {
try {
const list = await client.list();
@@ -60,7 +65,7 @@ export class ImapFindSentFolderService {
private async findSentFolderBySpecialUse(
client: ImapFlow,
list: ListResponse[],
- ): Promise {
+ ): Promise {
for (const folder of list) {
if (folder.specialUse && folder.specialUse.includes('\\Sent')) {
this.logger.log(
@@ -73,7 +78,10 @@ export class ImapFindSentFolderService {
);
if (messageCount > 0) {
- return folder.path;
+ return {
+ name: folder.name,
+ path: folder.path,
+ };
}
this.logger.warn(
@@ -90,25 +98,38 @@ export class ImapFindSentFolderService {
private async findSentFolderByRegexCandidates(
client: ImapFlow,
list: ListResponse[],
- ): Promise {
+ ): Promise {
const regexCandidateFolders = getImapSentFolderCandidatesByRegex(list);
for (const folder of regexCandidateFolders) {
- const messageCount = await this.getFolderMessageCount(client, folder);
+ const messageCount = await this.getFolderMessageCount(
+ client,
+ folder.path,
+ );
if (messageCount > 0) {
- this.logger.log(`Selected sent folder via pattern match: ${folder}`);
+ this.logger.log(
+ `Selected sent folder via pattern match: ${folder.path}`,
+ );
- return folder;
+ return {
+ name: folder.name,
+ path: folder.path,
+ };
}
}
if (regexCandidateFolders.length > 0) {
this.logger.log(
- `Using first regex candidate sent folder: ${regexCandidateFolders[0]} (no messages found in any regex candidate)`,
+ `Using first regex candidate sent folder: ${regexCandidateFolders[0].path} (no messages found in any regex candidate)`,
);
- return regexCandidateFolders[0];
+ const folder = regexCandidateFolders[0];
+
+ return {
+ name: folder.name,
+ path: folder.path,
+ };
}
return null;
diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-get-message-list.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-get-message-list.service.ts
index ae1b84990e..1d00e769dd 100644
--- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-get-message-list.service.ts
+++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/services/imap-get-message-list.service.ts
@@ -41,10 +41,17 @@ export class ImapGetMessageListService {
for (const folder of messageFolders) {
this.logger.log(`Processing folder: ${folder.name}`);
+ const folderPath = folder.externalId?.split(':')[0];
+
+ if (!folderPath) {
+ this.logger.warn(`Folder ${folder.name} has no path. Skipping.`);
+ continue;
+ }
+
try {
const response = await this.getMessageList(
client,
- folder.name,
+ folderPath,
folder,
);
diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/utils/__tests__/get-sent-folder-candidates-by-regex.util.spec.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/utils/__tests__/get-sent-folder-candidates-by-regex.util.spec.ts
index 862d901483..5334507c66 100644
--- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/utils/__tests__/get-sent-folder-candidates-by-regex.util.spec.ts
+++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/utils/__tests__/get-sent-folder-candidates-by-regex.util.spec.ts
@@ -3,7 +3,7 @@ import { type ListResponse } from 'imapflow';
import { getImapSentFolderCandidatesByRegex } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/get-sent-folder-candidates-by-regex.util';
function makeList(paths: string[]): ListResponse[] {
- return paths.map((p) => ({ path: p }) as ListResponse);
+ return paths.map((p) => ({ path: p, name: p }) as ListResponse);
}
describe('getSentFolderCandidatesByRegex', () => {
@@ -18,7 +18,9 @@ describe('getSentFolderCandidatesByRegex', () => {
const input = makeList(englishVariants);
const result = getImapSentFolderCandidatesByRegex(input);
- expect(result).toEqual(expect.arrayContaining(englishVariants));
+ expect(result.map((r) => r.path)).toEqual(
+ expect.arrayContaining(englishVariants),
+ );
});
it('matches French variants', () => {
@@ -26,7 +28,9 @@ describe('getSentFolderCandidatesByRegex', () => {
const input = makeList(frenchVariants);
const result = getImapSentFolderCandidatesByRegex(input);
- expect(result).toEqual(expect.arrayContaining(frenchVariants));
+ expect(result.map((r) => r.path)).toEqual(
+ expect.arrayContaining(frenchVariants),
+ );
});
it('matches German variants', () => {
@@ -34,7 +38,9 @@ describe('getSentFolderCandidatesByRegex', () => {
const input = makeList(germanVariants);
const result = getImapSentFolderCandidatesByRegex(input);
- expect(result).toEqual(expect.arrayContaining(germanVariants));
+ expect(result.map((r) => r.path)).toEqual(
+ expect.arrayContaining(germanVariants),
+ );
});
it('matches Spanish variants', () => {
@@ -42,7 +48,9 @@ describe('getSentFolderCandidatesByRegex', () => {
const input = makeList(spanishVariants);
const result = getImapSentFolderCandidatesByRegex(input);
- expect(result).toEqual(expect.arrayContaining(spanishVariants));
+ expect(result.map((r) => r.path)).toEqual(
+ expect.arrayContaining(spanishVariants),
+ );
});
it('matches Portuguese variants', () => {
@@ -50,7 +58,9 @@ describe('getSentFolderCandidatesByRegex', () => {
const input = makeList(portugueseVariants);
const result = getImapSentFolderCandidatesByRegex(input);
- expect(result).toEqual(expect.arrayContaining(portugueseVariants));
+ expect(result.map((r) => r.path)).toEqual(
+ expect.arrayContaining(portugueseVariants),
+ );
});
it('matches Italian variants', () => {
@@ -58,7 +68,9 @@ describe('getSentFolderCandidatesByRegex', () => {
const input = makeList(italianVariants);
const result = getImapSentFolderCandidatesByRegex(input);
- expect(result).toEqual(expect.arrayContaining(italianVariants));
+ expect(result.map((r) => r.path)).toEqual(
+ expect.arrayContaining(italianVariants),
+ );
});
it('matches Korean variant', () => {
@@ -66,7 +78,7 @@ describe('getSentFolderCandidatesByRegex', () => {
const input = makeList(koreanVariants);
const result = getImapSentFolderCandidatesByRegex(input);
- expect(result).toEqual(koreanVariants);
+ expect(result.map((r) => r.path)).toEqual(koreanVariants);
});
it('matches Japanese variants', () => {
@@ -74,7 +86,9 @@ describe('getSentFolderCandidatesByRegex', () => {
const input = makeList(japaneseVariants);
const result = getImapSentFolderCandidatesByRegex(input);
- expect(result).toEqual(expect.arrayContaining(japaneseVariants));
+ expect(result.map((r) => r.path)).toEqual(
+ expect.arrayContaining(japaneseVariants),
+ );
});
it('matches Polish variants', () => {
@@ -82,7 +96,9 @@ describe('getSentFolderCandidatesByRegex', () => {
const input = makeList(polishVariants);
const result = getImapSentFolderCandidatesByRegex(input);
- expect(result).toEqual(expect.arrayContaining(polishVariants));
+ expect(result.map((r) => r.path)).toEqual(
+ expect.arrayContaining(polishVariants),
+ );
});
it('matches Russian variants', () => {
@@ -95,7 +111,9 @@ describe('getSentFolderCandidatesByRegex', () => {
const input = makeList(russianVariants);
const result = getImapSentFolderCandidatesByRegex(input);
- expect(result).toEqual(expect.arrayContaining(russianVariants));
+ expect(result.map((r) => r.path)).toEqual(
+ expect.arrayContaining(russianVariants),
+ );
});
it('matches Gmail special folder', () => {
@@ -103,7 +121,9 @@ describe('getSentFolderCandidatesByRegex', () => {
const input = makeList(gmailVariants);
const result = getImapSentFolderCandidatesByRegex(input);
- expect(result).toEqual(expect.arrayContaining(gmailVariants));
+ expect(result.map((r) => r.path)).toEqual(
+ expect.arrayContaining(gmailVariants),
+ );
});
it('does not match unrelated folders', () => {
diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/utils/get-sent-folder-candidates-by-regex.util.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/utils/get-sent-folder-candidates-by-regex.util.ts
index 3bbd05f63b..142330cffc 100644
--- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/utils/get-sent-folder-candidates-by-regex.util.ts
+++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/utils/get-sent-folder-candidates-by-regex.util.ts
@@ -5,7 +5,7 @@ import { getStandardFolderByRegex } from 'src/modules/messaging/message-import-m
export function getImapSentFolderCandidatesByRegex(
list: ListResponse[],
-): string[] {
+): { name: string; path: string }[] {
const regexCandidateFolders: string[] = [];
for (const folder of list) {
@@ -16,5 +16,12 @@ export function getImapSentFolderCandidatesByRegex(
}
}
- return regexCandidateFolders;
+ return regexCandidateFolders.map((folderPath) => {
+ const folder = list.find((folder) => folder.path === folderPath);
+
+ return {
+ name: folder?.name ?? folderPath,
+ path: folderPath,
+ };
+ });
}