Flag changes for uid based imap sync

This commit is contained in:
Burak Kaan Köse
2026-02-14 13:22:16 +01:00
parent 744145be06
commit 64b9bfc392
@@ -108,27 +108,22 @@ public class UnifiedImapSynchronizer
_ => await SynchronizeWithUidDeltaAsync(client, folder, synchronizer, cancellationToken).ConfigureAwait(false)
};
if (strategy == ImapSyncStrategy.QResync)
{
if (folder.HighestModeSeq != originalHighestModeSeq)
{
await _folderService.UpdateFolderHighestModeSeqAsync(folder.Id, folder.HighestModeSeq).ConfigureAwait(false);
}
bool highestModeSeqChanged = folder.HighestModeSeq != originalHighestModeSeq;
bool requiresFullFolderUpdate =
folder.UidValidity != originalUidValidity
|| folder.HighestKnownUid != originalHighestKnownUid
|| folder.LastUidReconcileUtc != originalLastUidReconcileUtc;
bool requiresFullFolderUpdate =
folder.UidValidity != originalUidValidity
|| folder.HighestKnownUid != originalHighestKnownUid
|| folder.LastUidReconcileUtc != originalLastUidReconcileUtc;
if (requiresFullFolderUpdate)
{
await _folderService.UpdateFolderAsync(folder).ConfigureAwait(false);
}
}
else
if (requiresFullFolderUpdate)
{
// Persist all sync-state fields in one write when any non-mod-seq token changed.
await _folderService.UpdateFolderAsync(folder).ConfigureAwait(false);
}
else if (highestModeSeqChanged)
{
// Avoid full-folder write when only mod-seq changed.
await _folderService.UpdateFolderHighestModeSeqAsync(folder.Id, folder.HighestModeSeq).ConfigureAwait(false);
}
return FolderSyncResult.Successful(folder.Id, folder.FolderName, downloadedIds.Count);
}
@@ -393,6 +388,8 @@ public class UnifiedImapSynchronizer
UpdateHighestKnownUid(folder, remoteFolder, deltaUids.Select(a => a.Id));
}
await ReconcileUidBasedFlagChangesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
if (ShouldRunUidReconcile(folder))
{
await ReconcileDeletedMessagesAsync(folder, remoteFolder, cancellationToken).ConfigureAwait(false);
@@ -540,6 +537,116 @@ public class UnifiedImapSynchronizer
}
}
private async Task ReconcileUidBasedFlagChangesAsync(MailItemFolder localFolder, IMailFolder remoteFolder, CancellationToken cancellationToken)
{
var localMails = await _mailService.GetMailsByFolderIdAsync(localFolder.Id).ConfigureAwait(false);
if (localMails == null || localMails.Count == 0)
return;
var localByUid = new Dictionary<uint, MailCopy>();
var localUnreadUids = new HashSet<uint>();
var localFlaggedUids = new HashSet<uint>();
foreach (var localMail in localMails)
{
if (localMail == null || string.IsNullOrEmpty(localMail.Id))
continue;
uint uid;
try
{
uid = MailkitClientExtensions.ResolveUid(localMail.Id);
}
catch (ArgumentOutOfRangeException)
{
continue;
}
localByUid[uid] = localMail;
if (!localMail.IsRead)
localUnreadUids.Add(uid);
if (localMail.IsFlagged)
localFlaggedUids.Add(uid);
}
if (localByUid.Count == 0)
return;
var remoteUnreadUids = (await remoteFolder.SearchAsync(SearchQuery.NotSeen, cancellationToken).ConfigureAwait(false))
.Select(a => a.Id)
.ToHashSet();
var remoteFlaggedUids = (await remoteFolder.SearchAsync(SearchQuery.Flagged, cancellationToken).ConfigureAwait(false))
.Select(a => a.Id)
.ToHashSet();
var markReadCandidates = localUnreadUids.Except(remoteUnreadUids).ToList();
var unflagCandidates = localFlaggedUids.Except(remoteFlaggedUids).ToList();
var existingMarkReadCandidates = await FilterExistingRemoteUidsAsync(remoteFolder, markReadCandidates, cancellationToken).ConfigureAwait(false);
var existingUnflagCandidates = await FilterExistingRemoteUidsAsync(remoteFolder, unflagCandidates, cancellationToken).ConfigureAwait(false);
foreach (var uid in existingMarkReadCandidates)
{
if (!localByUid.TryGetValue(uid, out var localMail) || localMail.IsRead)
continue;
await _mailService.ChangeReadStatusAsync(localMail.Id, true).ConfigureAwait(false);
}
foreach (var uid in remoteUnreadUids)
{
if (!localByUid.TryGetValue(uid, out var localMail) || !localMail.IsRead)
continue;
await _mailService.ChangeReadStatusAsync(localMail.Id, false).ConfigureAwait(false);
}
foreach (var uid in existingUnflagCandidates)
{
if (!localByUid.TryGetValue(uid, out var localMail) || !localMail.IsFlagged)
continue;
await _mailService.ChangeFlagStatusAsync(localMail.Id, false).ConfigureAwait(false);
}
foreach (var uid in remoteFlaggedUids)
{
if (!localByUid.TryGetValue(uid, out var localMail) || localMail.IsFlagged)
continue;
await _mailService.ChangeFlagStatusAsync(localMail.Id, true).ConfigureAwait(false);
}
}
private static async Task<HashSet<uint>> FilterExistingRemoteUidsAsync(IMailFolder remoteFolder, IEnumerable<uint> candidateUids, CancellationToken cancellationToken)
{
var existing = new HashSet<uint>();
var uidList = candidateUids?.Distinct().ToList();
if (uidList == null || uidList.Count == 0)
return existing;
foreach (var batch in uidList.Batch(200))
{
cancellationToken.ThrowIfCancellationRequested();
var batchUids = batch.Select(a => new UniqueId(a)).ToList();
var existingBatch = await remoteFolder
.SearchAsync(SearchQuery.Uids(new UniqueIdSet(batchUids, SortOrder.Ascending)), cancellationToken)
.ConfigureAwait(false);
foreach (var existingUid in existingBatch)
{
existing.Add(existingUid.Id);
}
}
return existing;
}
private bool ShouldRunUidReconcile(MailItemFolder folder)
{
return ShouldRunUidReconcile(folder.LastUidReconcileUtc, DateTime.UtcNow, UidReconcileInterval);