fix: IMAP Import (#13500)

/closes #13397
This commit is contained in:
neo773
2025-07-30 18:38:00 +05:30
committed by GitHub
parent 11a0fce1c8
commit 0e4e3acaba
5 changed files with 111 additions and 56 deletions
@@ -145,6 +145,11 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
ApiKeyService,
AuthSsoService,
],
exports: [AccessTokenService, LoginTokenService, RefreshTokenService],
exports: [
AccessTokenService,
LoginTokenService,
RefreshTokenService,
CreateMessageFolderService,
],
})
export class AuthModule {}
@@ -1,6 +1,7 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
@@ -17,6 +18,7 @@ import { ImapSmtpCalDavAPIService } from 'src/modules/connected-account/services
TwentyConfigModule,
TwentyORMModule,
FeatureFlagModule,
AuthModule,
],
providers: [ImapSmtpCalDavAPIService],
exports: [ImapSmtpCalDavAPIService],
@@ -3,10 +3,12 @@ import { Injectable } from '@nestjs/common';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { v4 } from 'uuid';
import { CreateMessageFolderService } from 'src/engine/core-modules/auth/services/create-message-folder.service';
import { EmailAccountConnectionParameters } from 'src/engine/core-modules/imap-smtp-caldav-connection/dtos/imap-smtp-caldav-connection.dto';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import {
@@ -38,6 +40,7 @@ export class ImapSmtpCalDavAPIService {
private readonly messageQueueService: MessageQueueService,
@InjectMessageQueue(MessageQueue.calendarQueue)
private readonly calendarQueueService: MessageQueueService,
private readonly createMessageFolderService: CreateMessageFolderService,
) {}
async setupCompleteAccount(input: {
@@ -86,25 +89,28 @@ export class ImapSmtpCalDavAPIService {
let createdMessageChannel: MessageChannelWorkspaceEntity | null = null;
let createdCalendarChannel: CalendarChannelWorkspaceEntity | null = null;
await workspaceDataSource.transaction(async () => {
await this.upsertConnectedAccount(
input,
accountId,
connectedAccountRepository,
);
await workspaceDataSource.transaction(
async (manager: WorkspaceEntityManager) => {
await this.upsertConnectedAccount(
input,
accountId,
connectedAccountRepository,
);
createdMessageChannel = await this.setupMessageChannels(
input,
accountId,
messageChannelRepository,
);
createdMessageChannel = await this.setupMessageChannels(
input,
accountId,
messageChannelRepository,
manager,
);
createdCalendarChannel = await this.setupCalendarChannels(
input,
accountId,
calendarChannelRepository,
);
});
createdCalendarChannel = await this.setupCalendarChannels(
input,
accountId,
calendarChannelRepository,
);
},
);
await this.enqueueSyncJobs(
input,
@@ -143,6 +149,7 @@ export class ImapSmtpCalDavAPIService {
},
accountId: string,
messageChannelRepository: WorkspaceRepository<MessageChannelWorkspaceEntity>,
manager: WorkspaceEntityManager,
): Promise<MessageChannelWorkspaceEntity | null> {
const existingChannels = await messageChannelRepository.find({
where: { connectedAccountId: accountId },
@@ -175,6 +182,12 @@ export class ImapSmtpCalDavAPIService {
{},
);
await this.createMessageFolderService.createMessageFolders({
workspaceId: input.workspaceId,
messageChannelId: newMessageChannel.id,
manager,
});
return shouldEnableSync ? newMessageChannel : null;
}
@@ -4,9 +4,13 @@ import { ImapFlow } from 'imapflow';
import { ImapClientProvider } from 'src/modules/messaging/message-import-manager/drivers/imap/providers/imap-client.provider';
import { ImapHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-handle-error.service';
import { MessageFolderName } from 'src/modules/messaging/message-import-manager/drivers/imap/types/folders';
import { findSentMailbox } from 'src/modules/messaging/message-import-manager/drivers/imap/utils/find-sent-mailbox.util';
import { GetMessageListsArgs } from 'src/modules/messaging/message-import-manager/types/get-message-lists-args.type';
import { GetMessageListsResponse } from 'src/modules/messaging/message-import-manager/types/get-message-lists-response.type';
import {
GetMessageListsResponse,
GetOneMessageListResponse,
} from 'src/modules/messaging/message-import-manager/types/get-message-lists-response.type';
@Injectable()
export class ImapGetMessageListService {
@@ -20,59 +24,38 @@ export class ImapGetMessageListService {
async getMessageLists({
messageChannel,
connectedAccount,
messageFolders,
}: GetMessageListsArgs): Promise<GetMessageListsResponse> {
try {
const client = await this.imapClientProvider.getClient(connectedAccount);
const result: GetMessageListsResponse = [];
const mailboxes = ['INBOX'];
for (const folder of messageFolders) {
const mailboxName = await this.getMailboxName(client, folder.name);
const sentFolder = await findSentMailbox(client, this.logger);
if (!mailboxName) {
continue;
}
if (sentFolder) {
mailboxes.push(sentFolder);
}
let allMessages: { id: string; date: string }[] = [];
for (const mailbox of mailboxes) {
try {
const messages = await this.getMessagesFromMailbox(
const response = await this.getMessageListForMailbox(
client,
mailbox,
messageChannel.syncCursor,
mailboxName,
folder.syncCursor,
);
allMessages = [...allMessages, ...messages];
this.logger.log(
`Fetched ${messages.length} messages from ${mailbox}`,
);
result.push({
...response,
folderId: folder.id,
});
} catch (error) {
this.logger.warn(
`Error fetching from mailbox ${mailbox}: ${error.message}. Continuing with other mailboxes.`,
`Error fetching from folder ${folder.name} (${mailboxName}): ${error.message}. Continuing with other folders.`,
);
}
}
allMessages.sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
);
const messageExternalIds = allMessages.map((message) => message.id);
const nextSyncCursor =
allMessages.length > 0
? allMessages[allMessages.length - 1].date
: messageChannel.syncCursor || '';
return [
{
messageExternalIds,
nextSyncCursor,
previousSyncCursor: messageChannel.syncCursor,
messageExternalIdsToDelete: [],
folderId: undefined,
},
];
return result;
} catch (error) {
this.logger.error(
`Error getting message list: ${error.message}`,
@@ -95,6 +78,54 @@ export class ImapGetMessageListService {
}
}
private async getMailboxName(
client: ImapFlow,
folderName: string,
): Promise<string | null> {
if (folderName === MessageFolderName.INBOX) {
return 'INBOX';
}
if (folderName === MessageFolderName.SENT_ITEMS) {
const sentMailbox = await findSentMailbox(client, this.logger);
if (!sentMailbox) {
this.logger.warn('SENT folder not found, skipping');
return null;
}
return sentMailbox;
}
return folderName;
}
private async getMessageListForMailbox(
client: ImapFlow,
mailbox: string,
cursor?: string,
): Promise<GetOneMessageListResponse> {
const messages = await this.getMessagesFromMailbox(client, mailbox, cursor);
messages.sort(
(a, b) => new Date(b.date).getTime() - new Date(a.date).getTime(),
);
const messageExternalIds = messages.map((message) => message.id);
const nextSyncCursor =
messages.length > 0 ? messages[messages.length - 1].date : cursor || '';
return {
messageExternalIds,
nextSyncCursor,
previousSyncCursor: cursor || '',
messageExternalIdsToDelete: [],
folderId: undefined,
};
}
private async getMessagesFromMailbox(
client: ImapFlow,
mailbox: string,
@@ -0,0 +1,4 @@
export enum MessageFolderName {
INBOX = 'inbox',
SENT_ITEMS = 'sentItems',
}