message channel change 2 (#15269)

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
neo773
2025-10-24 20:09:22 +05:30
committed by GitHub
parent a937090b5f
commit e37b8b61ba
13 changed files with 309 additions and 50 deletions
@@ -21,6 +21,9 @@ export const isMatchingSelectFilter = ({
case selectFilter.eq !== undefined: {
return value === selectFilter.eq;
}
case selectFilter.neq !== undefined: {
return value !== selectFilter.neq;
}
default: {
throw new Error(
`Unexpected value for select filter : ${JSON.stringify(selectFilter)}`,
@@ -101,16 +101,33 @@ export const SettingsAccountsMessageChannelDetails = ({
return (
<StyledDetailsContainer>
{isFolderControlEnabled && messageChannel.messageFolders && (
<Section>
<H2Title
title={t`Import`}
description={t`Emails from the blocklist will be ignored. Manage blocklist on the “Accounts” setting page.`}
/>
<SettingsAccountsMessageFolderCard
onChange={handleMessageFolderImportPolicyChange}
value={messageChannel.messageFolderImportPolicy}
/>
</Section>
<>
<Section>
<H2Title
title={t`Import`}
description={t`Emails from the blocklist will be ignored. Manage blocklist on the “Accounts” setting page.`}
/>
<SettingsAccountsMessageFolderCard
onChange={handleMessageFolderImportPolicyChange}
value={messageChannel.messageFolderImportPolicy}
/>
</Section>
<Section>
<Card rounded>
<SettingsOptionCardContentToggle
Icon={IconUsers}
title={t`Exclude group emails`}
description={t`Don't sync emails from team@ support@ noreply@...`}
checked={messageChannel.excludeGroupEmails}
onChange={() =>
handleIsGroupEmailExcludedToggle(
!messageChannel.excludeGroupEmails,
)
}
/>
</Card>
</Section>
</>
)}
<Section>
<H2Title
@@ -137,8 +154,7 @@ export const SettingsAccountsMessageChannelDetails = ({
<SettingsOptionCardContentToggle
Icon={IconBriefcase}
title={t`Exclude non-professional emails`}
description={t`Dont create contacts from/to Gmail, Outlook emails`}
divider
description={t`Don't create contacts from/to Gmail, Outlook emails`}
checked={messageChannel.excludeNonProfessionalEmails}
onChange={() => {
handleIsNonProfessionalEmailExcludedToggle(
@@ -146,17 +162,6 @@ export const SettingsAccountsMessageChannelDetails = ({
);
}}
/>
<SettingsOptionCardContentToggle
Icon={IconUsers}
title={t`Exclude group emails`}
description={t`Dont sync emails from team@ support@ noreply@...`}
checked={messageChannel.excludeGroupEmails}
onChange={() =>
handleIsGroupEmailExcludedToggle(
!messageChannel.excludeGroupEmails,
)
}
/>
</Card>
</Section>
</StyledDetailsContainer>
@@ -10,6 +10,7 @@ import { CalendarQueryHookModule } from 'src/modules/calendar/common/query-hooks
import { ConnectedAccountQueryHookModule } from 'src/modules/connected-account/query-hooks/connected-account-query-hook.module';
import { DashboardQueryHookModule } from 'src/modules/dashboard/query-hooks/dashboard-query-hook.module';
import { MessagingQueryHookModule } from 'src/modules/messaging/common/query-hooks/messaging-query-hook.module';
import { MessageChannelQueryHookModule } from 'src/modules/messaging/message-channel-manager/query-hooks/message-channel-query-hook.module';
import { WorkspaceMemberQueryHookModule } from 'src/modules/workspace-member/query-hooks/workspace-member-query-hook.module';
@Module({
@@ -19,6 +20,7 @@ import { WorkspaceMemberQueryHookModule } from 'src/modules/workspace-member/que
ConnectedAccountQueryHookModule,
DashboardQueryHookModule,
BlocklistQueryHookModule,
MessageChannelQueryHookModule,
WorkspaceMemberQueryHookModule,
DiscoveryModule,
],
@@ -1,9 +1,11 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import {
MessageChannelSyncStage,
MessageChannelSyncStatus,
@@ -11,6 +13,7 @@ import {
MessageChannelVisibility,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { SyncMessageFoldersService } from 'src/modules/messaging/message-folder-manager/services/sync-message-folders.service';
export type CreateMessageChannelInput = {
workspaceId: string;
@@ -24,6 +27,7 @@ export type CreateMessageChannelInput = {
export class CreateMessageChannelService {
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly syncMessageFoldersService: SyncMessageFoldersService,
) {}
async createMessageChannel(
@@ -37,6 +41,16 @@ export class CreateMessageChannelService {
manager,
} = input;
const connectedAccountRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>(
workspaceId,
'connectedAccount',
);
const connectedAccount = await connectedAccountRepository.findOne({
where: { id: connectedAccountId },
});
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspaceId,
@@ -58,6 +72,15 @@ export class CreateMessageChannelService {
manager,
);
if (isDefined(connectedAccount)) {
await this.syncMessageFoldersService.syncMessageFolders({
workspaceId,
messageChannelId: newMessageChannel.id,
connectedAccount,
manager,
});
}
return newMessageChannel.id;
}
}
@@ -247,6 +247,8 @@ export const MESSAGE_CHANNEL_STANDARD_FIELD_IDS = {
messageChannelMessageAssociations: '20202020-49b8-4766-88fd-75f1e21b3d5f',
messageFolders: '20202020-cc39-4432-9fe8-ec8ab8bbed94',
messageFolderImportPolicy: '20202020-cc39-4432-9fe8-ec8ab8bbed95',
syncAllFolders: '20202020-e36b-409c-a9bd-79071596cdc0',
pendingGroupEmailsAction: '20202020-17c5-4e9f-bc50-af46a89fdd42',
isSyncEnabled: '20202020-d9a6-48e9-990b-b97fdf22e8dd',
syncCursor: '20202020-79d1-41cf-b738-bcf5ed61e256',
syncedAt: '20202020-263d-4c6b-ad51-137ada56f7d4',
@@ -263,6 +265,7 @@ export const MESSAGE_FOLDER_STANDARD_FIELD_IDS = {
isSentFolder: '20202020-2af5-4a25-b2de-3c9386da941b',
isSynced: '20202020-764f-4e09-8f95-cd46b6bfe3c4',
externalId: '20202020-f3a8-4d2b-9c7e-1b5f9a8e4c6d',
pendingSyncAction: '20202020-4f97-4c79-9517-16387fe237f7',
} as const;
export const MESSAGE_PARTICIPANT_STANDARD_FIELD_IDS = {
@@ -65,6 +65,12 @@ export enum MessageFolderImportPolicy {
SELECTED_FOLDERS = 'SELECTED_FOLDERS',
}
export enum MessageChannelPendingGroupEmailsAction {
GROUP_EMAILS_DELETION = 'GROUP_EMAILS_DELETION',
GROUP_EMAILS_IMPORT = 'GROUP_EMAILS_IMPORT',
NONE = 'NONE',
}
registerEnumType(MessageChannelVisibility, {
name: 'MessageChannelVisibility',
});
@@ -89,6 +95,10 @@ registerEnumType(MessageFolderImportPolicy, {
name: 'MessageFolderImportPolicy',
});
registerEnumType(MessageChannelPendingGroupEmailsAction, {
name: 'MessageChannelPendingGroupEmailsAction',
});
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.messageChannel,
namePlural: 'messageChannels',
@@ -249,6 +259,46 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
})
excludeGroupEmails: boolean;
@WorkspaceField({
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.syncAllFolders,
type: FieldMetadataType.BOOLEAN,
label: msg`Sync all folders`,
description: msg`Sync all folders including new ones`,
icon: 'IconFolders',
defaultValue: true,
})
syncAllFolders: boolean;
@WorkspaceField({
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.pendingGroupEmailsAction,
type: FieldMetadataType.SELECT,
label: msg`Pending group emails action`,
description: msg`Pending action for group emails`,
icon: 'IconUsersGroup',
options: [
{
value: MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION,
label: 'Group emails deletion',
position: 0,
color: 'red',
},
{
value: MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT,
label: 'Group emails import',
position: 1,
color: 'green',
},
{
value: MessageChannelPendingGroupEmailsAction.NONE,
label: 'None',
position: 2,
color: 'blue',
},
],
defaultValue: `'${MessageChannelPendingGroupEmailsAction.NONE}'`,
})
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
@WorkspaceField({
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.isSyncEnabled,
type: FieldMetadataType.BOOLEAN,
@@ -1,3 +1,5 @@
import { registerEnumType } from '@nestjs/graphql';
import { msg } from '@lingui/core/macro';
import { FieldMetadataType } from 'twenty-shared/types';
import { Relation } from 'typeorm';
@@ -18,6 +20,16 @@ import { STANDARD_OBJECT_ICONS } from 'src/engine/workspace-manager/workspace-sy
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
export enum MessageFolderPendingSyncAction {
FOLDER_DELETION = 'FOLDER_DELETION',
FOLDER_IMPORT = 'FOLDER_IMPORT',
NONE = 'NONE',
}
registerEnumType(MessageFolderPendingSyncAction, {
name: 'MessageFolderPendingSyncAction',
});
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.messageFolder,
namePlural: 'messageFolders',
@@ -90,6 +102,36 @@ export class MessageFolderWorkspaceEntity extends BaseWorkspaceEntity {
@WorkspaceIsNullable()
externalId: string | null;
@WorkspaceField({
standardId: MESSAGE_FOLDER_STANDARD_FIELD_IDS.pendingSyncAction,
type: FieldMetadataType.SELECT,
label: msg`Pending Sync Action`,
description: msg`Pending action for folder sync`,
icon: 'IconReload',
options: [
{
value: MessageFolderPendingSyncAction.FOLDER_DELETION,
label: 'Folder deletion',
position: 0,
color: 'red',
},
{
value: MessageFolderPendingSyncAction.FOLDER_IMPORT,
label: 'Folder import',
position: 1,
color: 'green',
},
{
value: MessageFolderPendingSyncAction.NONE,
label: 'None',
position: 2,
color: 'blue',
},
],
defaultValue: `'${MessageFolderPendingSyncAction.NONE}'`,
})
pendingSyncAction: MessageFolderPendingSyncAction;
@WorkspaceJoinColumn('messageChannel')
messageChannelId: string;
}
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { MessageChannelUpdateOnePreQueryHook } from 'src/modules/messaging/message-channel-manager/query-hooks/message-channel-update-one.pre-query.hook';
@Module({
providers: [MessageChannelUpdateOnePreQueryHook],
exports: [MessageChannelUpdateOnePreQueryHook],
})
export class MessageChannelQueryHookModule {}
@@ -0,0 +1,106 @@
import { msg } from '@lingui/core/macro';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { Not } from 'typeorm';
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
import { type UpdateOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
import {
WorkspaceQueryRunnerException,
WorkspaceQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception';
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import {
MessageChannelPendingGroupEmailsAction,
MessageChannelSyncStage,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageFolderPendingSyncAction,
type MessageFolderWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
const ONGOING_SYNC_STAGES = [
MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING,
MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING,
];
@WorkspaceQueryHook(`messageChannel.updateOne`)
export class MessageChannelUpdateOnePreQueryHook
implements WorkspacePreQueryHookInstance
{
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async execute(
authContext: AuthContext,
_objectName: string,
payload: UpdateOneResolverArgs<MessageChannelWorkspaceEntity>,
): Promise<UpdateOneResolverArgs<MessageChannelWorkspaceEntity>> {
const workspace = authContext.workspace;
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
const messageChannelRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelWorkspaceEntity>(
workspace.id,
'messageChannel',
);
const messageChannel = await messageChannelRepository.findOne({
where: { id: payload.id },
});
if (!messageChannel) {
throw new WorkspaceQueryRunnerException(
'Message channel not found',
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
{
userFriendlyMessage: msg`Message channel not found`,
},
);
}
const isSyncOngoing = ONGOING_SYNC_STAGES.includes(
messageChannel.syncStage,
);
const messageFolderRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageFolderWorkspaceEntity>(
workspace.id,
'messageFolder',
);
const folderWithPendingAction = await messageFolderRepository.findOne({
where: {
messageChannelId: messageChannel.id,
pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE),
},
});
const hasPendingFolderActions = !!folderWithPendingAction;
const hasPendingGroupEmailsAction =
messageChannel.pendingGroupEmailsAction !==
MessageChannelPendingGroupEmailsAction.NONE;
if (
isSyncOngoing &&
(hasPendingFolderActions || hasPendingGroupEmailsAction)
) {
throw new WorkspaceQueryRunnerException(
'Cannot update message channel while sync is ongoing with pending actions',
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
{
userFriendlyMessage: msg`Cannot update message channel while sync is ongoing. Please wait for the sync to complete.`,
},
);
}
return payload;
}
}
@@ -81,13 +81,19 @@ export class ImapGetAllFoldersService implements MessageFolderDriver {
for (const mailbox of validMailboxes) {
const isInbox = await this.isInboxFolder(mailbox);
const uidValidity = await this.getUidValidity(client, mailbox);
const standardFolder = getStandardFolderByRegex(mailbox.path);
const isSynced = this.shouldSyncByDefault(
mailbox,
standardFolder,
isInbox,
);
folders.push({
externalId: uidValidity
? `${mailbox.path}:${uidValidity}`
: mailbox.path,
name: mailbox.path,
isSynced: isInbox,
isSynced,
isSentFolder: false,
});
}
@@ -126,23 +132,35 @@ export class ImapGetAllFoldersService implements MessageFolderDriver {
return true;
}
return false;
}
private shouldSyncByDefault(
mailbox: ListResponse,
standardFolder: StandardFolder | null,
isInbox: boolean,
): boolean {
if (
mailbox.specialUse === '\\Drafts' ||
mailbox.specialUse === '\\Trash' ||
mailbox.specialUse === '\\Junk'
) {
return true;
}
const standardFolder = getStandardFolderByRegex(mailbox.path);
if (!standardFolder) {
return false;
}
return (
standardFolder !== StandardFolder.SENT &&
standardFolder !== StandardFolder.INBOX
);
if (
standardFolder === StandardFolder.DRAFTS ||
standardFolder === StandardFolder.TRASH ||
standardFolder === StandardFolder.JUNK
) {
return false;
}
if (isInbox) {
return true;
}
return false;
}
private async getUidValidity(
@@ -67,18 +67,13 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
}
const standardFolder = getStandardFolderByRegex(folder.displayName);
if (this.shouldExcludeFolder(standardFolder)) {
continue;
}
const isInbox = this.isInboxFolder(standardFolder);
const isSentFolder = this.isSentFolder(standardFolder);
const isSynced = this.shouldSyncByDefault(standardFolder);
folderInfos.push({
externalId: folder.id,
name: folder.displayName,
isSynced: isInbox || isSentFolder,
isSynced,
isSentFolder,
});
}
@@ -98,19 +93,19 @@ export class MicrosoftGetAllFoldersService implements MessageFolderDriver {
}
}
private isInboxFolder(standardFolder: StandardFolder | null): boolean {
return standardFolder === StandardFolder.INBOX;
}
private isSentFolder(standardFolder: StandardFolder | null): boolean {
return standardFolder === StandardFolder.SENT;
}
private shouldExcludeFolder(standardFolder: StandardFolder | null): boolean {
return (
standardFolder !== null &&
standardFolder !== StandardFolder.SENT &&
standardFolder !== StandardFolder.INBOX
);
private shouldSyncByDefault(standardFolder: StandardFolder | null): boolean {
if (
standardFolder === StandardFolder.JUNK ||
standardFolder === StandardFolder.DRAFTS ||
standardFolder === StandardFolder.TRASH
) {
return false;
}
return true;
}
}
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { MessagingBlocklistManagerModule } from 'src/modules/messaging/blocklist-manager/messaging-blocklist-manager.module';
import { MessageChannelQueryHookModule } from 'src/modules/messaging/message-channel-manager/query-hooks/message-channel-query-hook.module';
import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cleaner/messaging-message-cleaner.module';
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
import { MessageParticipantManagerModule } from 'src/modules/messaging/message-participant-manager/message-participant-manager.module';
@@ -13,6 +14,7 @@ import { MessagingMonitoringModule } from 'src/modules/messaging/monitoring/mess
MessageParticipantManagerModule,
MessagingBlocklistManagerModule,
MessagingMonitoringModule,
MessageChannelQueryHookModule,
],
providers: [],
exports: [MessagingImportManagerModule],
@@ -114,6 +114,7 @@ export type SelectFilter = {
is?: IsFilter;
in?: string[];
eq?: string;
neq?: string;
};
export type MultiSelectFilter = {