8.3 KiB
Outlook Queue-Based Synchronization Implementation
Overview
This document describes the implementation of the queue-based, metadata-only synchronization system for Outlook, mirroring the approach used in Gmail but adapted for Outlook's per-folder synchronization model.
Key Differences from Gmail
- Per-Folder Queue: Unlike Gmail which uses per-account
InitialSynchronizationStatus, Outlook uses per-folder queue tracking viaRemoteFolderIdinMailItemQueue. - Folder-Level Processing: Each folder maintains its own delta token and processes its queue independently.
- No Account-Level Status: Outlook doesn't use
Account.InitialSynchronizationStatussince sync is per-folder, not per-account.
Architecture Changes
1. MailItemQueue Entity Enhancement
File: Wino.Core.Domain\Entities\Mail\MailItemQueue.cs
Added RemoteFolderId property to support per-folder queue tracking:
public string RemoteFolderId { get; set; } // For Outlook per-folder sync
2. Service Layer Updates
Files:
Wino.Core.Domain\Interfaces\IMailService.csWino.Services\MailService.csWino.Core\Integration\Processors\DefaultChangeProcessor.cs
Added new methods for folder-specific queue operations:
Task<List<MailItemQueue>> GetMailItemQueueByFolderAsync(Guid accountId, string remoteFolderId, int take);
Task<int> GetMailItemQueueCountByFolderAsync(Guid accountId, string remoteFolderId);
3. OutlookSynchronizer Redesign
File: Wino.Core\Synchronizers\OutlookSynchronizer.cs
New Methods
-
QueueMailIdsForFolderAsync
- Queues all mail IDs for a specific folder using Delta API
- Only retrieves message IDs (minimal data transfer)
- Stores delta token for future incremental syncs
- Creates queue entries with
RemoteFolderIdfor folder tracking
-
ProcessMailQueueForFolderAsync
- Processes queued mail IDs in batches
- Downloads metadata only (no MIME content)
- Handles failures with retry logic
- Updates queue item status (IsProcessed, FailedCount)
-
DownloadMessageMetadataBatchAsync
- Downloads metadata for a batch of messages concurrently
- Uses semaphore to limit concurrent downloads (10 max)
- Calls
CreateMailCopyFromMessagefor metadata extraction - Creates
NewMailItemPackagewith null MimeMessage
-
CreateMailCopyFromMessage (Centralized)
- REPLACES scattered
AsMailCopy()andCreateMinimalMailCopyAsync()calls - Single source of truth for converting Graph Message to MailCopy
- Extracts all required fields from metadata
- Sets FolderId, UniqueId, and FileId
- REPLACES scattered
Modified Methods
-
DownloadMailsForInitialSyncAsync
- Now orchestrates queue-based sync
- Step 1: Queue all mail IDs via Delta API
- Step 2: Process queue in batches
-
ProcessDeltaChangesAndDownloadMailsAsync
- Downloads delta changes with metadata only
- Uses
DownloadMessageMetadataBatchAsyncinstead of full MIME download
-
CreateNewMailPackagesAsync
- Still downloads MIME for specific scenarios (search results, drafts)
- Uses
CreateMailCopyFromMessagefor consistency - Not called during normal sync operations
Removed Methods
DownloadMailsConcurrentlyAsync- Replaced by queue systemDownloadSingleMailAsync- Replaced by queue batch processing- Scattered
CreateMinimalMailCopyAsyncimplementations
Synchronization Flow
Initial Sync (Per Folder)
1. SynchronizeFolderAsync
├─ Check: !folder.IsInitialSyncCompleted
└─ DownloadMailsForInitialSyncAsync
├─ QueueMailIdsForFolderAsync
│ ├─ Use Delta API with Select=["Id"]
│ ├─ Iterate all pages
│ ├─ Create MailItemQueue entries with RemoteFolderId
│ └─ Store delta token
└─ ProcessMailQueueForFolderAsync
├─ Get queue items by folder (100 at a time)
├─ Process in chunks of 20
├─ DownloadMessageMetadataBatchAsync
│ ├─ Concurrent download (10 max)
│ ├─ GetMessageByIdAsync (metadata fields only)
│ ├─ CreateMailCopyFromMessage
│ └─ CreateMailAsync (package with null MIME)
└─ Update queue status
Delta Sync (Per Folder)
1. SynchronizeFolderAsync
├─ Check: folder.IsInitialSyncCompleted
└─ ProcessDeltaChangesAndDownloadMailsAsync
├─ Use Delta API with existing token
├─ Collect new mail IDs
├─ DownloadMessageMetadataBatchAsync
│ ├─ Download metadata only
│ └─ Create MailCopy entries
└─ Update delta token
On-Demand MIME Download
User Reads Mail
└─ DownloadMissingMimeMessageAsync
├─ Download full MIME via /messages/{id}/$value
└─ SaveMimeFileAsync
Benefits
- Reduced Bandwidth: Only metadata downloaded during sync (no 50+ MB MIME files)
- Faster Sync: Parallel processing with controlled concurrency
- Resilient: Queue system handles failures gracefully with retry logic
- Consistent: Centralized
CreateMailCopyFromMessagemethod - Scalable: Per-folder processing allows independent folder syncs
Code Consolidation
Before (Scattered Approach)
// Multiple places creating MailCopy
var mailCopy = message.AsMailCopy();
mailCopy.FolderId = folder.Id;
// ... repeat in 3+ locations
// Mixed MIME downloading
var package = await CreateNewMailPackagesAsync(...); // Downloads MIME
var minimal = await CreateMinimalMailCopyAsync(...); // No MIME
After (Centralized Approach)
// Single method for all scenarios
var mailCopy = CreateMailCopyFromMessage(message, folder);
// Clear separation
// Sync: metadata only
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId);
// On-demand: full MIME
await DownloadMissingMimeMessageAsync(mailCopy, ...);
Graph API Fields Used
Only essential fields are requested during sync:
private readonly string[] outlookMessageSelectParameters =
[
"InferenceClassification",
"Flag",
"Importance",
"IsRead",
"IsDraft",
"ReceivedDateTime",
"HasAttachments",
"BodyPreview",
"Id",
"ConversationId",
"From",
"Subject",
"ParentFolderId",
"InternetMessageId",
];
NOT downloaded during sync:
- Body content (HTML/Text)
- Raw MIME message
- Attachment content
- Extended properties
Migration Path for IMAP (Future)
The base WinoSynchronizer class has been updated with:
protected virtual Task QueueMailIdsForInitialSyncAsync(MailItemFolder folder, CancellationToken cancellationToken = default);
protected virtual Task<List<string>> DownloadMailsFromQueueAsync(MailItemFolder folder, int batchSize, CancellationToken cancellationToken = default);
protected virtual Task<MailCopy> CreateMinimalMailCopyAsync(TMessageType message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default);
IMAP can override these methods to implement similar queue-based sync:
- TODO: Add folder-based queue support to IMAP
- TODO: Implement metadata-only header parsing
- TODO: Centralize IMAP MailCopy creation
Testing Checklist
- Initial sync queues all mail IDs per folder
- Queue processing downloads metadata only
- Delta sync uses metadata-only approach
- Failed queue items retry correctly
- Concurrent download respects semaphore limits
- Delta token stored and used correctly per folder
- Search results still download MIME when needed
- Draft handling works with MIME headers
- On-demand MIME download functions correctly
- Large folders (1000+ messages) sync efficiently
- Network interruption recovery
Performance Expectations
Before (With MIME):
- 100 messages ≈ 100-500 MB download
- Sync time: 5-10 minutes
- API calls: 100+ individual message downloads
After (Metadata Only):
- 100 messages ≈ 1-5 MB download (metadata)
- Sync time: 30-60 seconds
- API calls: Batched requests (10-20 concurrent)
- MIME downloaded only when user reads (lazy loading)
Notes
CreateNewMailPackagesAsyncis now marked as only for special casesDefaultChangeProcessorno longer needed for basic operations- All synchronizers can benefit from this pattern (Gmail, IMAP, Outlook)
- The
InitialSyncMimeDownloadCountproperty is now obsolete