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 a4a2c8a15d..2d91cdfc34 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 @@ -3,6 +3,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { isNumber } from 'class-validator'; 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'; + /** * Service to find sent folder using IMAP special-use flags * @@ -89,7 +91,7 @@ export class ImapFindSentFolderService { client: ImapFlow, list: ListResponse[], ): Promise { - const regexCandidateFolders = this.getSentFolderCandidatesByRegex(list); + const regexCandidateFolders = getImapSentFolderCandidatesByRegex(list); for (const folder of regexCandidateFolders) { const messageCount = await this.getFolderMessageCount(client, folder); @@ -112,28 +114,6 @@ export class ImapFindSentFolderService { return null; } - private getSentFolderCandidatesByRegex(list: ListResponse[]): string[] { - // Comprehensive regex pattern for legacy IMAP servers - // Source: https://imapsync.lamiral.info/FAQ.d/FAQ.Folders_Mapping.txt - // Based on imapsync's regextrans2 examples (originally "Sent|Sent Messages|Gesendet") - // Extended with additional common localizations for broader provider/language support - const sentFolderPattern = - /^(.*\/)?(sent|sent[\s_-]?(items|mail|messages|elements)?|envoy[éê]s?|[ée]l[ée]ments[\s_-]?envoy[éê]s|gesendet|gesendete[\s_-]?elemente|enviados?|elementos[\s_-]?enviados|itens[\s_-]?enviados|posta[\s_-]?inviata|inviati|보낸편지함|\[gmail\]\/sent[\s_-]?mail)$/i; - - const regexCandidateFolders = []; - - for (const folder of list) { - if (sentFolderPattern.test(folder.path)) { - this.logger.debug( - `Found potential sent folder via pattern match: ${folder.path}`, - ); - regexCandidateFolders.push(folder.path); - } - } - - return regexCandidateFolders; - } - private async getFolderMessageCount( client: ImapFlow, folderPath: string, 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 new file mode 100644 index 0000000000..862d901483 --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/utils/__tests__/get-sent-folder-candidates-by-regex.util.spec.ts @@ -0,0 +1,124 @@ +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); +} + +describe('getSentFolderCandidatesByRegex', () => { + it('matches English variants', () => { + const englishVariants = [ + 'Sent', + 'Sent Items', + 'Sent Mail', + 'Sent Messages', + 'sent-elements', + ]; + const input = makeList(englishVariants); + const result = getImapSentFolderCandidatesByRegex(input); + + expect(result).toEqual(expect.arrayContaining(englishVariants)); + }); + + it('matches French variants', () => { + const frenchVariants = ['Envoyés', 'Éléments envoyés', 'Objets envoyés']; + const input = makeList(frenchVariants); + const result = getImapSentFolderCandidatesByRegex(input); + + expect(result).toEqual(expect.arrayContaining(frenchVariants)); + }); + + it('matches German variants', () => { + const germanVariants = ['Gesendet', 'Gesendete Elemente']; + const input = makeList(germanVariants); + const result = getImapSentFolderCandidatesByRegex(input); + + expect(result).toEqual(expect.arrayContaining(germanVariants)); + }); + + it('matches Spanish variants', () => { + const spanishVariants = ['Enviados', 'Elementos enviados']; + const input = makeList(spanishVariants); + const result = getImapSentFolderCandidatesByRegex(input); + + expect(result).toEqual(expect.arrayContaining(spanishVariants)); + }); + + it('matches Portuguese variants', () => { + const portugueseVariants = ['Itens enviados']; + const input = makeList(portugueseVariants); + const result = getImapSentFolderCandidatesByRegex(input); + + expect(result).toEqual(expect.arrayContaining(portugueseVariants)); + }); + + it('matches Italian variants', () => { + const italianVariants = ['Posta inviata', 'Inviati']; + const input = makeList(italianVariants); + const result = getImapSentFolderCandidatesByRegex(input); + + expect(result).toEqual(expect.arrayContaining(italianVariants)); + }); + + it('matches Korean variant', () => { + const koreanVariants = ['보낸편지함']; + const input = makeList(koreanVariants); + const result = getImapSentFolderCandidatesByRegex(input); + + expect(result).toEqual(koreanVariants); + }); + + it('matches Japanese variants', () => { + const japaneseVariants = ['送信済みメール', '送信済み']; + const input = makeList(japaneseVariants); + const result = getImapSentFolderCandidatesByRegex(input); + + expect(result).toEqual(expect.arrayContaining(japaneseVariants)); + }); + + it('matches Polish variants', () => { + const polishVariants = ['Wysłane', 'Elementy wysłane']; + const input = makeList(polishVariants); + const result = getImapSentFolderCandidatesByRegex(input); + + expect(result).toEqual(expect.arrayContaining(polishVariants)); + }); + + it('matches Russian variants', () => { + const russianVariants = [ + 'Отправленные', + 'Отправленные письма', + 'Отправленные сообщения', + 'Исходящие', + ]; + const input = makeList(russianVariants); + const result = getImapSentFolderCandidatesByRegex(input); + + expect(result).toEqual(expect.arrayContaining(russianVariants)); + }); + + it('matches Gmail special folder', () => { + const gmailVariants = ['[Gmail]/Sent Mail', '[Gmail]\\Sent Mail']; + const input = makeList(gmailVariants); + const result = getImapSentFolderCandidatesByRegex(input); + + expect(result).toEqual(expect.arrayContaining(gmailVariants)); + }); + + it('does not match unrelated folders', () => { + const unrelatedFolders = [ + 'Inbox', + 'Drafts', + 'Trash', + 'Archive', + 'Junk', + 'Important', + 'RandomFolder', + ]; + const input = makeList(unrelatedFolders); + const result = getImapSentFolderCandidatesByRegex(input); + + expect(result).toEqual([]); + }); +}); 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 new file mode 100644 index 0000000000..7f0d1466fd --- /dev/null +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/imap/utils/get-sent-folder-candidates-by-regex.util.ts @@ -0,0 +1,53 @@ +import { type ListResponse } from 'imapflow'; + +export function getImapSentFolderCandidatesByRegex( + list: ListResponse[], +): string[] { + const sentFolderPattern = new RegExp( + [ + // EN + 'sent([\\s_-]?(items|mail|messages|elements))?', + // FR + 'envoy[éê]s?', + '[ée]l[ée]ments[\\s_-]?envoy[éê]s', + // DE + 'gesendet', + 'gesendete[\\s_-]?elemente', + // ES + 'enviados?', + 'elementos[\\s_-]?enviados', + // PT + 'itens[\\s_-]?enviados', + // IT + 'posta[\\s_-]?inviata', + 'inviati', + // KO + '보낸편지함', + // JA + '送信済みメール', + '送信済み', + // PL + 'elementy[\\s_-]?wysłane', + 'wysłane', + // RU + 'отправленные', + 'отправленные[\\s_-]?(сообщения|письма)?', + 'исходящие', + // GMAIL + '\\[gmail\\][\\/]+sent[\\s_-]?mail', + ] + .map((s) => `(${s})`) + .join('|'), + 'i', + ); + + const regexCandidateFolders = []; + + for (const folder of list) { + if (sentFolderPattern.test(folder.path)) { + regexCandidateFolders.push(folder.path); + } + } + + return regexCandidateFolders; +}