# How Email Synchronization Works in Twenty CRM: A Technical Architecture Guide

> Discover how Twenty CRM email synchronization works. Our technical guide explains the message-import pipeline, sync cursors, and Message Import Manager for seamless Gmail and Microsoft 365 integration.

- Repository: [Twenty/twenty](https://github.com/twentyhq/twenty)
- Tags: architecture
- Published: 2026-03-27

---

**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-import` queue
- **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`](https://github.com/twentyhq/twenty/blob/main/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:

```typescript
// 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`](https://github.com/twentyhq/twenty/blob/main/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`](https://github.com/twentyhq/twenty/blob/main/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:

```typescript
// 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`](https://github.com/twentyhq/twenty/blob/main/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`](https://github.com/twentyhq/twenty/blob/main/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:

```typescript
// 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:

1. **Authentication Refresh**: Validates and refreshes OAuth tokens via `messagingAccountAuthenticationService.validateAndRefreshConnectedAccountAuthentication`
2. **Folder Synchronization**: Calls `syncMessageFoldersService` to update folder metadata and sync status
3. **Message List Retrieval**: Invokes `messagingGetMessageListService.getMessageLists` for enabled folders only
4. **Cursor Persistence**: Updates the channel cursor via `messagingCursorService`
5. **Import Queuing**: Triggers `messagingMessagesImportService.processMessageBatchImport` to enqueue content fetching jobs

```typescript
// 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 `isSynced` flag is true in the `MessageFolderWorkspaceEntity` record

The [`sync-message-folders.service.ts`](https://github.com/twentyhq/twenty/blob/main/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`](https://github.com/twentyhq/twenty/blob/main/gmail-get-message-list.service.ts), [`microsoft-get-message-list.service.ts`](https://github.com/twentyhq/twenty/blob/main/microsoft-get-message-list.service.ts), and [`imap-get-message-list.service.ts`](https://github.com/twentyhq/twenty/blob/main/imap-get-message-list.service.ts) normalizes disparate APIs into a unified import pipeline
- **Full sync detection** occurs when `syncCursor` is empty, triggering historical backfill and stale record cleanup
- **Folder-level policies** allow users to exclude sensitive folders via the `isSynced` flag in `MessageFolderWorkspaceEntity`
- **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`](https://github.com/twentyhq/twenty/blob/main/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.