Files
twenty/packages/twenty-server/src/modules/messaging/message-folder-manager/services/sync-message-folders.service.ts
T
neo773 82611de9b6 connected accounts follow up (#18998)
- Fixed group emails actions
- Fixed delta updates for folder manager
- Updated crons
2026-03-26 13:14:49 +00:00

197 lines
6.9 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In } from 'typeorm';
import {
DiscoveredMessageFolder,
MessageFolder,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { GmailGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/gmail/services/gmail-get-all-folders.service';
import { ImapGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/imap/services/imap-get-all-folders.service';
import { MicrosoftGetAllFoldersService } from 'src/modules/messaging/message-folder-manager/drivers/microsoft/services/microsoft-get-all-folders.service';
import { computeFolderIdsToDelete } from 'src/modules/messaging/message-folder-manager/utils/compute-folder-ids-to-delete.util';
import { computeFoldersToCreate } from 'src/modules/messaging/message-folder-manager/utils/compute-folders-to-create.util';
import { computeFoldersToUpdate } from 'src/modules/messaging/message-folder-manager/utils/compute-folders-to-update.util';
import { computeUpdatedFolders } from 'src/modules/messaging/message-folder-manager/utils/compute-updated-folders.util';
@Injectable()
export class SyncMessageFoldersService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
private readonly gmailGetAllFoldersService: GmailGetAllFoldersService,
private readonly microsoftGetAllFoldersService: MicrosoftGetAllFoldersService,
private readonly imapGetAllFoldersService: ImapGetAllFoldersService,
) {}
async syncMessageFolders({
messageChannel,
workspaceId,
}: {
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'id' | 'messageFolderImportPolicy'
> & {
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
| 'provider'
| 'accessToken'
| 'refreshToken'
| 'id'
| 'handle'
| 'connectionParameters'
>;
messageFolders: MessageFolder[];
};
workspaceId: string;
}): Promise<MessageFolder[]> {
const discoveredFolders = await this.discoverAllFolders(
messageChannel.connectedAccount,
messageChannel,
);
const { messageFolders: existingFolders, id: messageChannelId } =
messageChannel;
return this.syncFolderChanges(
discoveredFolders,
existingFolders,
messageChannelId,
workspaceId,
);
}
async discoverAllFolders(
connectedAccount: Pick<
ConnectedAccountWorkspaceEntity,
| 'accessToken'
| 'refreshToken'
| 'id'
| 'handle'
| 'provider'
| 'connectionParameters'
>,
messageChannel: Pick<
MessageChannelWorkspaceEntity,
'messageFolderImportPolicy'
>,
): Promise<DiscoveredMessageFolder[]> {
switch (connectedAccount.provider) {
case ConnectedAccountProvider.GOOGLE:
return this.gmailGetAllFoldersService.getAllMessageFolders(
connectedAccount,
messageChannel,
);
case ConnectedAccountProvider.MICROSOFT:
return this.microsoftGetAllFoldersService.getAllMessageFolders(
connectedAccount,
messageChannel,
);
case ConnectedAccountProvider.IMAP_SMTP_CALDAV:
return this.imapGetAllFoldersService.getAllMessageFolders(
connectedAccount,
messageChannel,
);
default:
throw new Error(
`Provider ${connectedAccount.provider} is not supported`,
);
}
}
private async syncFolderChanges(
discoveredFolders: DiscoveredMessageFolder[],
existingFolders: MessageFolder[],
messageChannelId: string,
workspaceId: string,
): Promise<MessageFolder[]> {
const foldersToCreate = computeFoldersToCreate({
discoveredFolders,
existingFolders,
messageChannelId,
});
const foldersToUpdate = computeFoldersToUpdate({
discoveredFolders,
existingFolders,
});
const folderIdsToDelete = computeFolderIdsToDelete({
discoveredFolders,
existingFolders,
});
const authContext = buildSystemAuthContext(workspaceId);
// TODO: Restore transaction wrapper once migration is complete — folder
// sync operations (create/update/delete) are no longer atomic since
// the data access layer routes writes across workspace and core schemas.
// Acceptable during transition as sync is idempotent and self-corrects.
return this.globalWorkspaceOrmManager.executeInWorkspaceContext(
async () => {
if (folderIdsToDelete.length > 0) {
await this.messageFolderDataAccessService.update(
workspaceId,
{ id: In(folderIdsToDelete) },
{
pendingSyncAction: MessageFolderPendingSyncAction.FOLDER_DELETION,
},
);
}
if (foldersToUpdate.size > 0) {
for (const [id, data] of foldersToUpdate.entries()) {
await this.messageFolderDataAccessService.update(
workspaceId,
{ id, messageChannelId },
data,
);
}
}
if (foldersToCreate.length > 0) {
for (const folderToCreate of foldersToCreate) {
await this.messageFolderDataAccessService.save(
workspaceId,
folderToCreate,
);
}
}
const createdFolders =
foldersToCreate.length > 0
? await this.messageFolderDataAccessService.find(workspaceId, {
messageChannelId,
externalId: In(
foldersToCreate
.map((folder) => folder.externalId)
.filter(isDefined),
),
})
: [];
const updatedExistingFolders = computeUpdatedFolders({
existingFolders,
foldersToUpdate,
folderIdsToDelete,
});
return [
...updatedExistingFolders,
...(createdFolders as MessageFolder[]),
];
},
authContext,
);
}
}