How Email Synchronization Works in Twenty CRM: A Technical Architecture Guide
Twenty CRM implements email synchronization through a message-import pipeline that uses sync cursors to pull incremental updates from Gmail, Microsoft 365, or IMAP mailboxes, orchestrated by the Message Import Manager in the twenty-server package.
The twentyhq/twenty repository contains a sophisticated email integration system that continuously mirrors external mailbox activity into workspace timelines. This architecture supports multiple providers while respecting user-defined privacy boundaries and minimizing API quota consumption through delta synchronization.
The Message Import Pipeline Architecture
At the core of Twenty's email synchronization is the Message Import Manager, located in the twenty-server package. This system treats each connected mailbox as a MessageChannelWorkspaceEntity, a database entity that persists synchronization state between runs.
The pipeline follows a continuous fetch-and-import loop:
- Poll the external provider for message metadata using stored cursors
- Filter results based on user-configured folder policies
- Cache external message IDs in a
messages-to-importqueue - Import full message content via background workers
This design separates metadata enumeration from content ingestion, allowing Twenty to maintain responsiveness even when importing large historical mailboxes.
Sync Cursors and Incremental Updates
The Sync Cursor Mechanism
Each MessageChannel record stores a syncCursor field that marks the last processed point in the provider's change history. According to the source code in messaging-message-list-fetch.service.ts, the cursor format varies by provider—Gmail uses history IDs, Microsoft uses delta tokens, and IMAP uses UID values.
When the service initializes, it checks the cursor state to determine synchronization scope:
// messaging-message-list-fetch.service.ts (excerpt)
if (!isNonEmptyString(messageChannel.syncCursor)) {
// No cursor → full sync
return this.getMessageListWithoutCursor(connectedAccount, messageFolders, messageChannel);
}
Full vs. Incremental Sync Detection
Twenty distinguishes between full synchronization and incremental synchronization by evaluating the previousSyncCursor returned by provider drivers. A full sync occurs when every returned cursor is empty and the channel's stored cursor is null. During full sync operations, the system also performs garbage collection to remove stale messages not present in the provider.
The cursor update logic resides in messaging-cursor.service.ts, which atomically persists the nextSyncCursor after successful batch processing to ensure exactly-once semantics during restarts.
Provider-Specific Driver Implementations
Twenty abstracts provider APIs through a driver pattern, with each implementation handling authentication, pagination, and cursor translation.
Gmail Driver
The Gmail implementation in gmail-get-message-list.service.ts leverages Google's History API for efficient delta detection. When a syncCursor exists, the driver calls gmailGetHistoryService to retrieve added and deleted message IDs since the previous history ID:
// gmail-get-message-list.service.ts (excerpt)
const { history, historyId: nextSyncCursor } = await this.gmailGetHistoryService.getHistory(
gmailClient,
messageChannel.syncCursor,
);
const { messagesAdded, messagesDeleted } = await this.gmailGetHistoryService.getMessageIdsFromHistory(history);
This approach retrieves only modified message headers rather than full mailbox listings, reducing bandwidth and latency.
Microsoft Driver
For Microsoft 365 and Outlook.com accounts, microsoft-get-message-list.service.ts implements the Microsoft Graph delta query pattern. Similar to Gmail, it maintains synchronization state via delta tokens stored in the syncCursor field, paginating through changes using the Graph API's @odata.deltaLink mechanism.
IMAP Driver
Generic IMAP support in imap-get-message-list.service.ts uses UID-based cursors stored per folder (folder.syncCursor). The driver issues standard IMAP SEARCH and FETCH commands, comparing the remote UIDVALIDITY and UIDNEXT values against locally stored cursors to identify new messages:
// imap-get-message-list.service.ts (excerpt)
// Fetches messages via IMAP SEARCH/FETCH sequence
const messageLists = await this.getMessageListFromFolder(
imapClient,
folder.syncCursor,
);
The Sync Loop Orchestration
The MessagingMessageListFetchService drives the entire workflow through its processMessageListFetch method. This orchestrator coordinates multiple subsystems:
- Authentication Refresh: Validates and refreshes OAuth tokens via
messagingAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication - Folder Synchronization: Calls
syncMessageFoldersServiceto update folder metadata and sync status - Message List Retrieval: Invokes
messagingGetMessageListService.getMessageListsfor enabled folders only - Cursor Persistence: Updates the channel cursor via
messagingCursorService - Import Queuing: Triggers
messagingMessagesImportService.processMessageBatchImportto enqueue content fetching jobs
// messaging-message-list-fetch.service.ts (excerpt)
const freshMessageChannel = await this.messageChannelDataAccessService.findOne(workspaceId, {
where: { id: messageChannel.id },
relations: ['connectedAccount', 'messageFolders'],
});
// ... token refresh and folder sync logic
const messageLists = await this.messagingGetMessageListService.getMessageLists(
freshMessageChannel,
messageFolders.filter(f => f.pendingSyncAction === MessageFolderPendingSyncAction.NONE),
);
await this.messagingCursorService.updateCursor(freshMessageChannel, nextSyncCursor, workspaceId, folderId);
Folder Selection and User Controls
Users control synchronization scope through folder import policies configured in Settings → Accounts. The system supports two modes:
- All Folders: Automatically syncs every folder returned by the provider
- Selected Folders: Only processes folders where the
isSyncedflag is true in theMessageFolderWorkspaceEntityrecord
The sync-message-folders.service.ts module maintains the mapping between external folder hierarchies and Twenty's internal representation, respecting the pendingSyncAction state to handle renames and deletions gracefully.
Summary
- Cursor-based synchronization enables efficient incremental updates using provider-specific tokens (Gmail history IDs, Microsoft delta tokens, IMAP UIDs)
- Driver abstraction in
gmail-get-message-list.service.ts,microsoft-get-message-list.service.ts, andimap-get-message-list.service.tsnormalizes disparate APIs into a unified import pipeline - Full sync detection occurs when
syncCursoris empty, triggering historical backfill and stale record cleanup - Folder-level policies allow users to exclude sensitive folders via the
isSyncedflag inMessageFolderWorkspaceEntity - Token refresh automation ensures continuous synchronization without manual re-authentication
Frequently Asked Questions
What triggers a full sync versus an incremental sync in Twenty CRM?
A full sync occurs when the MessageChannel.syncCursor field is empty or null, indicating either a new connection or a reset state. In this mode, the system retrieves the complete message list from the provider and purges any local records not found in the remote mailbox. Once the initial sync completes, Twenty stores a cursor value and switches to incremental mode, fetching only changes since the last synchronization checkpoint.
How does Twenty CRM handle OAuth token expiration during email synchronization?
The messaging-message-list-fetch.service.ts explicitly refreshes authentication before each sync cycle by calling messagingAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication. This method exchanges refresh tokens for new access tokens using the provider's OAuth endpoints, ensuring the Gmail, Microsoft, or IMAP connection remains valid without user intervention.
Can users exclude specific folders from email synchronization?
Yes. Users can configure folder import policies in the workspace settings to limit synchronization to selected folders only. The system respects the isSynced boolean flag on MessageFolderWorkspaceEntity records, processing only folders where this value is true. This allows exclusion of private or high-volume folders like Spam or Archive from the CRM activity timeline.
What happens to emails deleted in the external provider?
During incremental synchronization, provider drivers return both added and deleted message IDs. For Gmail, the gmailGetHistoryService identifies deletions through the history API; Microsoft Graph returns deleted items in delta queries; and IMAP tracks UID validity changes. These deleted IDs populate the messageExternalIdsToDelete array, which triggers removal of the corresponding records from Twenty's database to maintain consistency with the source mailbox.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →