Bulk updates for gmail.

This commit is contained in:
Burak Kaan Köse
2026-04-25 16:00:49 +02:00
parent 8fe40e9fe3
commit 3ea8b8ac46
6 changed files with 576 additions and 132 deletions
@@ -34,6 +34,7 @@ public interface IMailService
/// <param name="accountId">Account to remove from</param> /// <param name="accountId">Account to remove from</param>
/// <param name="mailCopyId">Mail copy id to remove.</param> /// <param name="mailCopyId">Mail copy id to remove.</param>
Task DeleteMailAsync(Guid accountId, string mailCopyId); Task DeleteMailAsync(Guid accountId, string mailCopyId);
Task DeleteMailsAsync(Guid accountId, IEnumerable<string> mailCopyIds);
Task ChangeReadStatusAsync(string mailCopyId, bool isRead); Task ChangeReadStatusAsync(string mailCopyId, bool isRead);
Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged); Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged);
@@ -42,8 +43,11 @@ public interface IMailService
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId); Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId); Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
Task CreateAssignmentsAsync(Guid accountId, IEnumerable<MailFolderAssignmentUpdate> assignments);
Task DeleteAssignmentsAsync(Guid accountId, IEnumerable<MailFolderAssignmentUpdate> assignments);
Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage package); Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage package);
Task CreateMailsAsync(Guid accountId, IReadOnlyList<NewMailItemPackage> packages);
/// <summary> /// <summary>
/// Maps new mail item with the existing local draft copy. /// Maps new mail item with the existing local draft copy.
@@ -0,0 +1,3 @@
namespace Wino.Core.Domain.Models.MailItem;
public sealed record MailFolderAssignmentUpdate(string MailCopyId, string RemoteFolderId);
@@ -30,7 +30,9 @@ public interface IDefaultChangeProcessor
Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead); Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead);
Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged); Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged);
Task<bool> CreateMailAsync(Guid AccountId, NewMailItemPackage package); Task<bool> CreateMailAsync(Guid AccountId, NewMailItemPackage package);
Task CreateMailsAsync(Guid accountId, IReadOnlyList<NewMailItemPackage> packages);
Task DeleteMailAsync(Guid accountId, string mailId); Task DeleteMailAsync(Guid accountId, string mailId);
Task DeleteMailsAsync(Guid accountId, IEnumerable<string> mailIds);
Task<List<MailCopy>> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable<string> downloadedMailCopyIds); Task<List<MailCopy>> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable<string> downloadedMailCopyIds);
Task SaveMimeFileAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId); Task SaveMimeFileAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId);
Task DeleteFolderAsync(Guid accountId, string remoteFolderId); Task DeleteFolderAsync(Guid accountId, string remoteFolderId);
@@ -56,6 +58,9 @@ public interface IDefaultChangeProcessor
Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken); Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken);
Task<List<MailCopy>> GetMailCopiesAsync(IEnumerable<string> mailCopyIds); Task<List<MailCopy>> GetMailCopiesAsync(IEnumerable<string> mailCopyIds);
Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package); Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package);
Task ApplyMailStateUpdatesAsync(IEnumerable<MailCopyStateUpdate> updates);
Task CreateAssignmentsAsync(Guid accountId, IEnumerable<MailFolderAssignmentUpdate> assignments);
Task DeleteAssignmentsAsync(Guid accountId, IEnumerable<MailFolderAssignmentUpdate> assignments);
Task DeleteUserMailCacheAsync(Guid accountId); Task DeleteUserMailCacheAsync(Guid accountId);
Task UpsertMailInvitationCalendarMappingAsync(MailInvitationCalendarMapping mapping); Task UpsertMailInvitationCalendarMappingAsync(MailInvitationCalendarMapping mapping);
Task<MailInvitationCalendarMapping> GetMailInvitationCalendarMappingAsync(Guid accountId, string mailCopyId); Task<MailInvitationCalendarMapping> GetMailInvitationCalendarMappingAsync(Guid accountId, string mailCopyId);
@@ -156,18 +161,33 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
public Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead) public Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead)
=> MailService.ChangeReadStatusAsync(mailCopyId, isRead); => MailService.ChangeReadStatusAsync(mailCopyId, isRead);
public Task ApplyMailStateUpdatesAsync(IEnumerable<MailCopyStateUpdate> updates)
=> MailService.ApplyMailStateUpdatesAsync(updates);
public Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId) public Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
=> MailService.DeleteAssignmentAsync(accountId, mailCopyId, remoteFolderId); => MailService.DeleteAssignmentAsync(accountId, mailCopyId, remoteFolderId);
public Task DeleteAssignmentsAsync(Guid accountId, IEnumerable<MailFolderAssignmentUpdate> assignments)
=> MailService.DeleteAssignmentsAsync(accountId, assignments);
public Task DeleteMailAsync(Guid accountId, string mailId) public Task DeleteMailAsync(Guid accountId, string mailId)
=> MailService.DeleteMailAsync(accountId, mailId); => MailService.DeleteMailAsync(accountId, mailId);
public Task DeleteMailsAsync(Guid accountId, IEnumerable<string> mailIds)
=> MailService.DeleteMailsAsync(accountId, mailIds);
public Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage package) public Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage package)
=> MailService.CreateMailAsync(accountId, package); => MailService.CreateMailAsync(accountId, package);
public Task CreateMailsAsync(Guid accountId, IReadOnlyList<NewMailItemPackage> packages)
=> MailService.CreateMailsAsync(accountId, packages);
public Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package) public Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package)
=> MailService.CreateMailRawAsync(account, mailItemFolder, package); => MailService.CreateMailRawAsync(account, mailItemFolder, package);
public Task CreateAssignmentsAsync(Guid accountId, IEnumerable<MailFolderAssignmentUpdate> assignments)
=> MailService.CreateAssignmentsAsync(accountId, assignments);
public Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId) public Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId)
=> MailService.MapLocalDraftAsync(accountId, localDraftCopyUniqueId, newMailCopyId, newDraftId, newThreadId); => MailService.MapLocalDraftAsync(accountId, localDraftCopyUniqueId, newMailCopyId, newDraftId, newThreadId);
+151 -50
View File
@@ -1050,6 +1050,11 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
{ {
_logger.Debug("Processing delta change {HistoryId} for {Name}", listHistoryResponse.HistoryId.GetValueOrDefault(), Account.Name); _logger.Debug("Processing delta change {HistoryId} for {Name}", listHistoryResponse.HistoryId.GetValueOrDefault(), Account.Name);
var pendingStateUpdates = new List<MailCopyStateUpdate>();
var pendingAssignmentCreates = new Dictionary<string, MailFolderAssignmentUpdate>(StringComparer.Ordinal);
var pendingAssignmentDeletes = new Dictionary<string, MailFolderAssignmentUpdate>(StringComparer.Ordinal);
var deletedMessageIds = new HashSet<string>(StringComparer.Ordinal);
foreach (var history in listHistoryResponse.History) foreach (var history in listHistoryResponse.History)
{ {
// Handle label additions. // Handle label additions.
@@ -1057,7 +1062,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
{ {
foreach (var addedLabel in history.LabelsAdded) foreach (var addedLabel in history.LabelsAdded)
{ {
await HandleLabelAssignmentAsync(addedLabel); await HandleLabelAssignmentAsync(addedLabel, pendingStateUpdates, pendingAssignmentCreates, pendingAssignmentDeletes).ConfigureAwait(false);
} }
} }
@@ -1066,7 +1071,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
{ {
foreach (var removedLabel in history.LabelsRemoved) foreach (var removedLabel in history.LabelsRemoved)
{ {
await HandleLabelRemovalAsync(removedLabel); await HandleLabelRemovalAsync(removedLabel, pendingStateUpdates, pendingAssignmentCreates, pendingAssignmentDeletes).ConfigureAwait(false);
} }
} }
@@ -1079,36 +1084,108 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
_logger.Debug("Processing message deletion for {MessageId}", messageId); _logger.Debug("Processing message deletion for {MessageId}", messageId);
await _gmailChangeProcessor.DeleteMailAsync(Account.Id, messageId).ConfigureAwait(false); deletedMessageIds.Add(messageId);
} }
} }
} }
if (pendingStateUpdates.Count > 0)
{
await _gmailChangeProcessor.ApplyMailStateUpdatesAsync(pendingStateUpdates).ConfigureAwait(false);
}
if (pendingAssignmentCreates.Count > 0)
{
await _gmailChangeProcessor.CreateAssignmentsAsync(Account.Id, pendingAssignmentCreates.Values.ToList()).ConfigureAwait(false);
}
if (pendingAssignmentDeletes.Count > 0)
{
await _gmailChangeProcessor.DeleteAssignmentsAsync(Account.Id, pendingAssignmentDeletes.Values.ToList()).ConfigureAwait(false);
}
if (deletedMessageIds.Count > 0)
{
await _gmailChangeProcessor.DeleteMailsAsync(Account.Id, deletedMessageIds).ConfigureAwait(false);
}
} }
private async Task HandleArchiveAssignmentAsync(string archivedMessageId) private static string GetAssignmentChangeKey(string messageId, string labelId)
=> $"{messageId}\u001f{labelId}";
private static void QueueAssignmentChange(
Dictionary<string, MailFolderAssignmentUpdate> creates,
Dictionary<string, MailFolderAssignmentUpdate> deletes,
MailFolderAssignmentUpdate assignment,
bool shouldCreate)
{ {
if (assignment == null ||
string.IsNullOrWhiteSpace(assignment.MailCopyId) ||
string.IsNullOrWhiteSpace(assignment.RemoteFolderId))
{
return;
}
var key = GetAssignmentChangeKey(assignment.MailCopyId, assignment.RemoteFolderId);
if (shouldCreate)
{
deletes.Remove(key);
creates[key] = assignment;
}
else
{
creates.Remove(key);
deletes[key] = assignment;
}
}
private async Task HandleArchiveAssignmentAsync(
string archivedMessageId,
Dictionary<string, MailFolderAssignmentUpdate> pendingAssignmentCreates,
Dictionary<string, MailFolderAssignmentUpdate> pendingAssignmentDeletes)
{
if (!archiveFolderId.HasValue)
return;
// Ignore if the message is already in the archive. // Ignore if the message is already in the archive.
bool archived = await _gmailChangeProcessor.IsMailExistsInFolderAsync(archivedMessageId, archiveFolderId.Value); bool archived = await _gmailChangeProcessor.IsMailExistsInFolderAsync(archivedMessageId, archiveFolderId.Value).ConfigureAwait(false);
if (archived) return; if (archived) return;
_logger.Debug("Processing archive assignment for message {Id}", archivedMessageId); _logger.Debug("Processing archive assignment for message {Id}", archivedMessageId);
QueueAssignmentChange(
await _gmailChangeProcessor.CreateAssignmentAsync(Account.Id, archivedMessageId, ServiceConstants.ARCHIVE_LABEL_ID).ConfigureAwait(false); pendingAssignmentCreates,
pendingAssignmentDeletes,
new MailFolderAssignmentUpdate(archivedMessageId, ServiceConstants.ARCHIVE_LABEL_ID),
shouldCreate: true);
} }
private async Task HandleUnarchiveAssignmentAsync(string unarchivedMessageId) private async Task HandleUnarchiveAssignmentAsync(
string unarchivedMessageId,
Dictionary<string, MailFolderAssignmentUpdate> pendingAssignmentCreates,
Dictionary<string, MailFolderAssignmentUpdate> pendingAssignmentDeletes)
{ {
if (!archiveFolderId.HasValue)
return;
// Ignore if the message is not in the archive. // Ignore if the message is not in the archive.
bool archived = await _gmailChangeProcessor.IsMailExistsInFolderAsync(unarchivedMessageId, archiveFolderId.Value); bool archived = await _gmailChangeProcessor.IsMailExistsInFolderAsync(unarchivedMessageId, archiveFolderId.Value).ConfigureAwait(false);
if (!archived) return; if (!archived) return;
_logger.Debug("Processing un-archive assignment for message {Id}", unarchivedMessageId); _logger.Debug("Processing un-archive assignment for message {Id}", unarchivedMessageId);
QueueAssignmentChange(
await _gmailChangeProcessor.DeleteAssignmentAsync(Account.Id, unarchivedMessageId, ServiceConstants.ARCHIVE_LABEL_ID).ConfigureAwait(false); pendingAssignmentCreates,
pendingAssignmentDeletes,
new MailFolderAssignmentUpdate(unarchivedMessageId, ServiceConstants.ARCHIVE_LABEL_ID),
shouldCreate: false);
} }
private async Task HandleLabelAssignmentAsync(HistoryLabelAdded addedLabel) private async Task HandleLabelAssignmentAsync(
HistoryLabelAdded addedLabel,
List<MailCopyStateUpdate> pendingStateUpdates,
Dictionary<string, MailFolderAssignmentUpdate> pendingAssignmentCreates,
Dictionary<string, MailFolderAssignmentUpdate> pendingAssignmentDeletes)
{ {
var messageId = addedLabel.Message.Id; var messageId = addedLabel.Message.Id;
@@ -1119,23 +1196,31 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
// ARCHIVE is a virtual folder - handle it separately // ARCHIVE is a virtual folder - handle it separately
if (labelId == ServiceConstants.ARCHIVE_LABEL_ID) if (labelId == ServiceConstants.ARCHIVE_LABEL_ID)
{ {
await HandleArchiveAssignmentAsync(messageId).ConfigureAwait(false); await HandleArchiveAssignmentAsync(messageId, pendingAssignmentCreates, pendingAssignmentDeletes).ConfigureAwait(false);
continue; continue;
} }
// When UNREAD label is added mark the message as un-read. // When UNREAD label is added mark the message as un-read.
if (labelId == ServiceConstants.UNREAD_LABEL_ID) if (labelId == ServiceConstants.UNREAD_LABEL_ID)
await _gmailChangeProcessor.ChangeMailReadStatusAsync(messageId, false).ConfigureAwait(false); pendingStateUpdates.Add(new MailCopyStateUpdate(messageId, IsRead: false));
// When STARRED label is added mark the message as flagged. // When STARRED label is added mark the message as flagged.
if (labelId == ServiceConstants.STARRED_LABEL_ID) if (labelId == ServiceConstants.STARRED_LABEL_ID)
await _gmailChangeProcessor.ChangeFlagStatusAsync(messageId, true).ConfigureAwait(false); pendingStateUpdates.Add(new MailCopyStateUpdate(messageId, IsFlagged: true));
await _gmailChangeProcessor.CreateAssignmentAsync(Account.Id, messageId, labelId).ConfigureAwait(false); QueueAssignmentChange(
pendingAssignmentCreates,
pendingAssignmentDeletes,
new MailFolderAssignmentUpdate(messageId, labelId),
shouldCreate: true);
} }
} }
private async Task HandleLabelRemovalAsync(HistoryLabelRemoved removedLabel) private async Task HandleLabelRemovalAsync(
HistoryLabelRemoved removedLabel,
List<MailCopyStateUpdate> pendingStateUpdates,
Dictionary<string, MailFolderAssignmentUpdate> pendingAssignmentCreates,
Dictionary<string, MailFolderAssignmentUpdate> pendingAssignmentDeletes)
{ {
var messageId = removedLabel.Message.Id; var messageId = removedLabel.Message.Id;
@@ -1146,20 +1231,23 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
// ARCHIVE is a virtual folder - handle it separately // ARCHIVE is a virtual folder - handle it separately
if (labelId == ServiceConstants.ARCHIVE_LABEL_ID) if (labelId == ServiceConstants.ARCHIVE_LABEL_ID)
{ {
await HandleUnarchiveAssignmentAsync(messageId).ConfigureAwait(false); await HandleUnarchiveAssignmentAsync(messageId, pendingAssignmentCreates, pendingAssignmentDeletes).ConfigureAwait(false);
continue; continue;
} }
// When UNREAD label is removed mark the message as read. // When UNREAD label is removed mark the message as read.
if (labelId == ServiceConstants.UNREAD_LABEL_ID) if (labelId == ServiceConstants.UNREAD_LABEL_ID)
await _gmailChangeProcessor.ChangeMailReadStatusAsync(messageId, true).ConfigureAwait(false); pendingStateUpdates.Add(new MailCopyStateUpdate(messageId, IsRead: true));
// When STARRED label is removed mark the message as un-flagged. // When STARRED label is removed mark the message as un-flagged.
if (labelId == ServiceConstants.STARRED_LABEL_ID) if (labelId == ServiceConstants.STARRED_LABEL_ID)
await _gmailChangeProcessor.ChangeFlagStatusAsync(messageId, false).ConfigureAwait(false); pendingStateUpdates.Add(new MailCopyStateUpdate(messageId, IsFlagged: false));
// For other labels remove the mail assignment. QueueAssignmentChange(
await _gmailChangeProcessor.DeleteAssignmentAsync(Account.Id, messageId, labelId).ConfigureAwait(false); pendingAssignmentCreates,
pendingAssignmentDeletes,
new MailFolderAssignmentUpdate(messageId, labelId),
shouldCreate: false);
} }
} }
@@ -1542,6 +1630,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
await Task.WhenAll(batchTasks).ConfigureAwait(false); await Task.WhenAll(batchTasks).ConfigureAwait(false);
// Process all downloaded messages // Process all downloaded messages
var pendingPackages = new List<NewMailItemPackage>();
foreach (var gmailMessage in downloadedMessages) foreach (var gmailMessage in downloadedMessages)
{ {
try try
@@ -1552,12 +1642,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
var packages = await CreateNewMailPackagesAsync(gmailMessage, null, cancellationToken).ConfigureAwait(false); var packages = await CreateNewMailPackagesAsync(gmailMessage, null, cancellationToken).ConfigureAwait(false);
if (packages != null) if (packages != null)
{ pendingPackages.AddRange(packages);
foreach (var package in packages)
{
await _gmailChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
}
}
// Update sync identifier if available // Update sync identifier if available
if (gmailMessage.HistoryId.HasValue) if (gmailMessage.HistoryId.HasValue)
@@ -1570,6 +1655,11 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
_logger.Error(ex, "Failed to process downloaded message {MessageId}", gmailMessage.Id); _logger.Error(ex, "Failed to process downloaded message {MessageId}", gmailMessage.Id);
} }
} }
if (pendingPackages.Count > 0)
{
await _gmailChangeProcessor.CreateMailsAsync(Account.Id, pendingPackages).ConfigureAwait(false);
}
} }
} }
@@ -1592,12 +1682,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
// Create mail packages from metadata // Create mail packages from metadata
var packages = await CreateNewMailPackagesAsync(gmailMessage, null, cancellationToken).ConfigureAwait(false); var packages = await CreateNewMailPackagesAsync(gmailMessage, null, cancellationToken).ConfigureAwait(false);
if (packages != null) if (packages != null && packages.Count > 0)
{ {
foreach (var package in packages) await _gmailChangeProcessor.CreateMailsAsync(Account.Id, packages).ConfigureAwait(false);
{
await _gmailChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
}
} }
// Update sync identifier if available // Update sync identifier if available
@@ -1942,12 +2029,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
// Create mail packages from the downloaded message // Create mail packages from the downloaded message
var packages = await CreateNewMailPackagesAsync(gmailMessage, null, cancellationToken).ConfigureAwait(false); var packages = await CreateNewMailPackagesAsync(gmailMessage, null, cancellationToken).ConfigureAwait(false);
if (packages != null) if (packages != null && packages.Count > 0)
{ {
foreach (var package in packages) await _gmailChangeProcessor.CreateMailsAsync(Account.Id, packages).ConfigureAwait(false);
{
await _gmailChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
}
} }
await UpdateAccountSyncIdentifierAsync(gmailMessage.HistoryId).ConfigureAwait(false); await UpdateAccountSyncIdentifierAsync(gmailMessage.HistoryId).ConfigureAwait(false);
@@ -1999,25 +2083,27 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
switch (bundle.UIChangeRequest) switch (bundle.UIChangeRequest)
{ {
case BatchMarkReadRequest batchMarkReadRequest: case BatchMarkReadRequest batchMarkReadRequest:
foreach (var request in batchMarkReadRequest) await _gmailChangeProcessor.ApplyMailStateUpdatesAsync(
{ batchMarkReadRequest.Select(request => new MailCopyStateUpdate(request.Item.Id, IsRead: request.IsRead)))
await _gmailChangeProcessor.ChangeMailReadStatusAsync(request.Item.Id, request.IsRead).ConfigureAwait(false); .ConfigureAwait(false);
}
break; break;
case MarkReadRequest markReadRequest: case MarkReadRequest markReadRequest:
await _gmailChangeProcessor.ChangeMailReadStatusAsync(markReadRequest.Item.Id, markReadRequest.IsRead).ConfigureAwait(false); await _gmailChangeProcessor.ApplyMailStateUpdatesAsync(
[new MailCopyStateUpdate(markReadRequest.Item.Id, IsRead: markReadRequest.IsRead)])
.ConfigureAwait(false);
break; break;
case BatchChangeFlagRequest batchChangeFlagRequest: case BatchChangeFlagRequest batchChangeFlagRequest:
foreach (var request in batchChangeFlagRequest) await _gmailChangeProcessor.ApplyMailStateUpdatesAsync(
{ batchChangeFlagRequest.Select(request => new MailCopyStateUpdate(request.Item.Id, IsFlagged: request.IsFlagged)))
await _gmailChangeProcessor.ChangeFlagStatusAsync(request.Item.Id, request.IsFlagged).ConfigureAwait(false); .ConfigureAwait(false);
}
break; break;
case ChangeFlagRequest changeFlagRequest: case ChangeFlagRequest changeFlagRequest:
await _gmailChangeProcessor.ChangeFlagStatusAsync(changeFlagRequest.Item.Id, changeFlagRequest.IsFlagged).ConfigureAwait(false); await _gmailChangeProcessor.ApplyMailStateUpdatesAsync(
[new MailCopyStateUpdate(changeFlagRequest.Item.Id, IsFlagged: changeFlagRequest.IsFlagged)])
.ConfigureAwait(false);
break; break;
} }
} }
@@ -2075,16 +2161,31 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
} }
var existingAfterDownload = await _gmailChangeProcessor.AreMailsExistsAsync(addedArchiveIds).ConfigureAwait(false); var existingAfterDownload = await _gmailChangeProcessor.AreMailsExistsAsync(addedArchiveIds).ConfigureAwait(false);
var pendingArchiveCreates = new Dictionary<string, MailFolderAssignmentUpdate>(StringComparer.Ordinal);
var pendingArchiveDeletes = new Dictionary<string, MailFolderAssignmentUpdate>(StringComparer.Ordinal);
foreach (var archiveAddedItem in existingAfterDownload) foreach (var archiveAddedItem in existingAfterDownload)
{ {
await HandleArchiveAssignmentAsync(archiveAddedItem).ConfigureAwait(false); await HandleArchiveAssignmentAsync(archiveAddedItem, pendingArchiveCreates, pendingArchiveDeletes).ConfigureAwait(false);
}
if (pendingArchiveCreates.Count > 0)
{
await _gmailChangeProcessor.CreateAssignmentsAsync(Account.Id, pendingArchiveCreates.Values.ToList()).ConfigureAwait(false);
} }
} }
var pendingArchiveRemovals = new Dictionary<string, MailFolderAssignmentUpdate>(StringComparer.Ordinal);
var pendingArchiveCreateOverrides = new Dictionary<string, MailFolderAssignmentUpdate>(StringComparer.Ordinal);
foreach (var unAarchivedRemovedItem in removedArchiveIds) foreach (var unAarchivedRemovedItem in removedArchiveIds)
{ {
await HandleUnarchiveAssignmentAsync(unAarchivedRemovedItem).ConfigureAwait(false); await HandleUnarchiveAssignmentAsync(unAarchivedRemovedItem, pendingArchiveCreateOverrides, pendingArchiveRemovals).ConfigureAwait(false);
}
if (pendingArchiveRemovals.Count > 0)
{
await _gmailChangeProcessor.DeleteAssignmentsAsync(Account.Id, pendingArchiveRemovals.Values.ToList()).ConfigureAwait(false);
} }
} }
@@ -21,6 +21,8 @@ namespace Wino.Mail.ViewModels.Collections;
public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsChangedMessage> public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsChangedMessage>
{ {
private const int UiMutationBatchSize = 40;
// We cache each mail copy id for faster access on updates. // We cache each mail copy id for faster access on updates.
// If the item provider here for update or removal doesn't exist here // If the item provider here for update or removal doesn't exist here
// we can ignore the operation. // we can ignore the operation.
@@ -763,18 +765,21 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
// Execute all updates in a single UI thread call // Execute all updates in a single UI thread call
if (itemsToUpdate.Count > 0) if (itemsToUpdate.Count > 0)
{ {
await ExecuteUIThread(() => foreach (var updateBatch in itemsToUpdate.Chunk(UiMutationBatchSize))
{ {
foreach (var (existing, updated) in itemsToUpdate) await ExecuteUIThread(() =>
{ {
UpdateUniqueIdHashes(existing, false); foreach (var (existing, updated) in updateBatch)
existing.UpdateFrom(updated); {
UpdateUniqueIdHashes(existing, true); UpdateUniqueIdHashes(existing, false);
} existing.UpdateFrom(updated);
}); UpdateUniqueIdHashes(existing, true);
}
});
}
} }
// Group items by their grouping key and add them in a single UI thread call // Group items by their grouping key and add them in bounded UI thread calls.
if (itemsToAdd.Count > 0) if (itemsToAdd.Count > 0)
{ {
var groupedItems = await Task.Run(() => itemsToAdd var groupedItems = await Task.Run(() => itemsToAdd
@@ -787,32 +792,35 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
}) })
.ToList()).ConfigureAwait(false); .ToList()).ConfigureAwait(false);
await ExecuteUIThread(() => foreach (var groupedItem in groupedItems)
{ {
foreach (var groupedItem in groupedItems) var groupKey = groupedItem.Key;
var groupItems = groupedItem.Items;
foreach (var groupBatch in groupItems.Chunk(UiMutationBatchSize))
{ {
var groupKey = groupedItem.Key; await ExecuteUIThread(() =>
var groupItems = groupedItem.Items;
// Update caches first
foreach (var item in groupItems)
{ {
UpdateUniqueIdHashes(item, true); // Update caches first so lookup helpers remain consistent during inserts.
UpdateThreadIdCache(item, true); foreach (var item in groupBatch)
}
foreach (var item in groupItems)
{
_mailItemSource.InsertItem(groupKey, listComparer, item, listComparer);
var targetGroup = _mailItemSource.FirstGroupByKeyOrDefault(groupKey);
if (targetGroup != null)
{ {
_itemToGroupMap[item] = targetGroup; UpdateUniqueIdHashes(item, true);
UpdateThreadIdCache(item, true);
} }
}
foreach (var item in groupBatch)
{
_mailItemSource.InsertItem(groupKey, listComparer, item, listComparer);
var targetGroup = _mailItemSource.FirstGroupByKeyOrDefault(groupKey);
if (targetGroup != null)
{
_itemToGroupMap[item] = targetGroup;
}
}
});
} }
}); }
} }
} }
@@ -972,20 +980,23 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
if (updates.Count == 0) if (updates.Count == 0)
return; return;
await ExecuteUIThread(() => foreach (var updateBatch in updates.Chunk(UiMutationBatchSize))
{ {
foreach (var update in updates) await ExecuteUIThread(() =>
{ {
var existingItem = update.ItemContainer.ItemViewModel; foreach (var update in updateBatch)
var appliedChanges = existingItem.ApplyStateChanges(update.UpdatedState.IsRead, update.UpdatedState.IsFlagged);
existingItem.IsBusy = mailUpdateSource == EntityUpdateSource.ClientUpdated;
if (update.ItemContainer.ThreadViewModel != null && appliedChanges != MailCopyChangeFlags.None)
{ {
update.ItemContainer.ThreadViewModel.NotifyMailItemUpdated(existingItem, appliedChanges); var existingItem = update.ItemContainer.ItemViewModel;
var appliedChanges = existingItem.ApplyStateChanges(update.UpdatedState.IsRead, update.UpdatedState.IsFlagged);
existingItem.IsBusy = mailUpdateSource == EntityUpdateSource.ClientUpdated;
if (update.ItemContainer.ThreadViewModel != null && appliedChanges != MailCopyChangeFlags.None)
{
update.ItemContainer.ThreadViewModel.NotifyMailItemUpdated(existingItem, appliedChanges);
}
} }
} });
}); }
} }
private async Task UpdateMailCopiesInternalAsync(IEnumerable<MailCopy> updatedMailCopies, EntityUpdateSource mailUpdateSource, MailCopyChangeFlags changedProperties) private async Task UpdateMailCopiesInternalAsync(IEnumerable<MailCopy> updatedMailCopies, EntityUpdateSource mailUpdateSource, MailCopyChangeFlags changedProperties)
@@ -1018,22 +1029,25 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
return; return;
} }
await ExecuteUIThread(() => foreach (var updateBatch in updates.Chunk(UiMutationBatchSize))
{ {
foreach (var update in updates) await ExecuteUIThread(() =>
{ {
var updatedMail = update.UpdatedMail; foreach (var update in updateBatch)
var itemContainer = update.ItemContainer;
var existingItem = itemContainer.ItemViewModel;
var appliedChanges = existingItem.UpdateFrom(updatedMail, changedProperties);
existingItem.IsBusy = mailUpdateSource == EntityUpdateSource.ClientUpdated;
if (itemContainer.ThreadViewModel != null && appliedChanges != MailCopyChangeFlags.None)
{ {
itemContainer.ThreadViewModel.NotifyMailItemUpdated(existingItem, appliedChanges); var updatedMail = update.UpdatedMail;
var itemContainer = update.ItemContainer;
var existingItem = itemContainer.ItemViewModel;
var appliedChanges = existingItem.UpdateFrom(updatedMail, changedProperties);
existingItem.IsBusy = mailUpdateSource == EntityUpdateSource.ClientUpdated;
if (itemContainer.ThreadViewModel != null && appliedChanges != MailCopyChangeFlags.None)
{
itemContainer.ThreadViewModel.NotifyMailItemUpdated(existingItem, appliedChanges);
}
} }
} });
}); }
} }
public MailItemViewModel GetFirst() => AllItems.ElementAtOrDefault(0); public MailItemViewModel GetFirst() => AllItems.ElementAtOrDefault(0);
+334 -32
View File
@@ -727,32 +727,70 @@ public class MailService : BaseDatabaseService, IMailService
public async Task DeleteMailAsync(Guid accountId, string mailCopyId) public async Task DeleteMailAsync(Guid accountId, string mailCopyId)
{ {
var allMails = await GetMailCopiesByIdAsync([mailCopyId]).ConfigureAwait(false); await DeleteMailsAsync(accountId, [mailCopyId]).ConfigureAwait(false);
}
foreach (var mailItem in allMails) public async Task DeleteMailsAsync(Guid accountId, IEnumerable<string> mailCopyIds)
{ {
// Delete mime file as well. var targetMailIds = mailCopyIds?
// Even though Gmail might have multiple copies for the same mail, we only have one MIME file for all. .Where(id => !string.IsNullOrWhiteSpace(id))
// Their FileId is inserted same. .Distinct(StringComparer.Ordinal)
.ToList() ?? [];
await DeleteMailInternalAsync(mailItem, preserveMimeFile: false).ConfigureAwait(false); if (targetMailIds.Count == 0)
} return;
var allMails = await GetMailCopiesByIdAsync(targetMailIds).ConfigureAwait(false);
await DeleteMailCopiesAsync(allMails, preserveMimeFile: false, reportUiChange: true).ConfigureAwait(false);
}
private void ReportAddedMails(IReadOnlyList<MailCopy> addedMails)
{
if (addedMails == null || addedMails.Count == 0)
return;
if (addedMails.Count == 1)
ReportUIChange(new MailAddedMessage(addedMails[0], EntityUpdateSource.Server));
else
ReportUIChange(new BulkMailAddedMessage(addedMails, EntityUpdateSource.Server));
}
private void ReportUpdatedMails(IReadOnlyList<MailCopy> updatedMails, MailCopyChangeFlags changedProperties = MailCopyChangeFlags.None)
{
if (updatedMails == null || updatedMails.Count == 0)
return;
if (updatedMails.Count == 1)
ReportUIChange(new MailUpdatedMessage(updatedMails[0], EntityUpdateSource.Server, changedProperties));
else
ReportUIChange(new BulkMailUpdatedMessage(updatedMails, EntityUpdateSource.Server, changedProperties));
}
private void ReportRemovedMails(IReadOnlyList<MailCopy> removedMails)
{
if (removedMails == null || removedMails.Count == 0)
return;
if (removedMails.Count == 1)
ReportUIChange(new MailRemovedMessage(removedMails[0], EntityUpdateSource.Server));
else
ReportUIChange(new BulkMailRemovedMessage(removedMails, EntityUpdateSource.Server));
} }
#region Repository Calls #region Repository Calls
private async Task InsertMailAsync(MailCopy mailCopy) private async Task<MailCopy> InsertMailAsync(MailCopy mailCopy, bool reportUiChange)
{ {
if (mailCopy == null) if (mailCopy == null)
{ {
_logger.Warning("Null mail passed to InsertMailAsync call."); _logger.Warning("Null mail passed to InsertMailAsync call.");
return; return null;
} }
if (mailCopy.FolderId == Guid.Empty) if (mailCopy.FolderId == Guid.Empty)
{ {
_logger.Warning("Invalid FolderId for MailCopyId {Id} for InsertMailAsync", mailCopy.Id); _logger.Warning("Invalid FolderId for MailCopyId {Id} for InsertMailAsync", mailCopy.Id);
return; return null;
} }
_logger.Debug("Inserting mail {MailCopyId} to {FolderName}", mailCopy.Id, mailCopy.AssignedFolder.FolderName); _logger.Debug("Inserting mail {MailCopyId} to {FolderName}", mailCopy.Id, mailCopy.AssignedFolder.FolderName);
@@ -760,21 +798,27 @@ public class MailService : BaseDatabaseService, IMailService
await Connection.InsertAsync(mailCopy, typeof(MailCopy)).ConfigureAwait(false); await Connection.InsertAsync(mailCopy, typeof(MailCopy)).ConfigureAwait(false);
var hydratedMailCopy = await HydrateMailCopyAsync(mailCopy).ConfigureAwait(false); var hydratedMailCopy = await HydrateMailCopyAsync(mailCopy).ConfigureAwait(false);
ReportUIChange(new MailAddedMessage(hydratedMailCopy, EntityUpdateSource.Server)); if (reportUiChange)
ReportAddedMails([hydratedMailCopy]);
return hydratedMailCopy;
} }
public async Task UpdateMailAsync(MailCopy mailCopy) public async Task UpdateMailAsync(MailCopy mailCopy)
=> await UpdateMailAsync(mailCopy, reportUiChange: true).ConfigureAwait(false);
private async Task<MailCopy> UpdateMailAsync(MailCopy mailCopy, bool reportUiChange, MailCopy existingMailCopy = null)
{ {
if (mailCopy == null) if (mailCopy == null)
{ {
_logger.Warning("Null mail passed to UpdateMailAsync call."); _logger.Warning("Null mail passed to UpdateMailAsync call.");
return; return null;
} }
_logger.Debug("Updating mail {MailCopyId} with Folder {FolderId}", mailCopy.Id, mailCopy.FolderId); _logger.Debug("Updating mail {MailCopyId} with Folder {FolderId}", mailCopy.Id, mailCopy.FolderId);
var existingMailCopy = mailCopy.UniqueId != Guid.Empty existingMailCopy ??= mailCopy.UniqueId != Guid.Empty
? await Connection.FindAsync<MailCopy>(mailCopy.UniqueId).ConfigureAwait(false) ? await Connection.FindAsync<MailCopy>(mailCopy.UniqueId).ConfigureAwait(false)
: null; : null;
@@ -787,16 +831,19 @@ public class MailService : BaseDatabaseService, IMailService
await Connection.UpdateAsync(mailCopy, typeof(MailCopy)).ConfigureAwait(false); await Connection.UpdateAsync(mailCopy, typeof(MailCopy)).ConfigureAwait(false);
var hydratedMailCopy = await HydrateMailCopyAsync(mailCopy).ConfigureAwait(false); var hydratedMailCopy = await HydrateMailCopyAsync(mailCopy).ConfigureAwait(false);
ReportUIChange(new MailUpdatedMessage(hydratedMailCopy, EntityUpdateSource.Server)); if (reportUiChange)
ReportUpdatedMails([hydratedMailCopy]);
return hydratedMailCopy;
} }
private async Task DeleteMailInternalAsync(MailCopy mailCopy, bool preserveMimeFile) private async Task<MailCopy> DeleteMailInternalAsync(MailCopy mailCopy, bool preserveMimeFile, bool reportUiChange)
{ {
if (mailCopy == null) if (mailCopy == null)
{ {
_logger.Warning("Null mail passed to DeleteMailAsync call."); _logger.Warning("Null mail passed to DeleteMailAsync call.");
return; return null;
} }
_logger.Debug("Deleting mail {Id} from folder {FolderName}", mailCopy.Id, mailCopy.AssignedFolder.FolderName); _logger.Debug("Deleting mail {Id} from folder {FolderName}", mailCopy.Id, mailCopy.AssignedFolder.FolderName);
@@ -812,7 +859,31 @@ public class MailService : BaseDatabaseService, IMailService
await _mimeFileService.DeleteMimeMessageAsync(mailCopy.AssignedAccount.Id, mailCopy.FileId).ConfigureAwait(false); await _mimeFileService.DeleteMimeMessageAsync(mailCopy.AssignedAccount.Id, mailCopy.FileId).ConfigureAwait(false);
} }
ReportUIChange(new MailRemovedMessage(mailCopy, EntityUpdateSource.Server)); if (reportUiChange)
ReportRemovedMails([mailCopy]);
return mailCopy;
}
private async Task<List<MailCopy>> DeleteMailCopiesAsync(IReadOnlyList<MailCopy> mailCopies, bool preserveMimeFile, bool reportUiChange)
{
if (mailCopies == null || mailCopies.Count == 0)
return [];
var removedMails = new List<MailCopy>(mailCopies.Count);
foreach (var mailCopy in mailCopies)
{
var removedMail = await DeleteMailInternalAsync(mailCopy, preserveMimeFile, reportUiChange: false).ConfigureAwait(false);
if (removedMail != null)
removedMails.Add(removedMail);
}
if (reportUiChange)
ReportRemovedMails(removedMails);
return removedMails;
} }
#endregion #endregion
@@ -1021,6 +1092,40 @@ public class MailService : BaseDatabaseService, IMailService
} }
public async Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId) public async Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
=> await CreateAssignmentsAsync(accountId, [new MailFolderAssignmentUpdate(mailCopyId, remoteFolderId)]).ConfigureAwait(false);
public async Task CreateAssignmentsAsync(Guid accountId, IEnumerable<MailFolderAssignmentUpdate> assignments)
{
var targetAssignments = assignments?
.Where(x => x != null &&
!string.IsNullOrWhiteSpace(x.MailCopyId) &&
!string.IsNullOrWhiteSpace(x.RemoteFolderId))
.GroupBy(x => $"{x.MailCopyId}\u001f{x.RemoteFolderId}", StringComparer.Ordinal)
.Select(group => group.First())
.ToList() ?? [];
if (targetAssignments.Count == 0)
return;
var addedMails = new List<MailCopy>(targetAssignments.Count);
var removedMails = new List<MailCopy>();
foreach (var assignment in targetAssignments)
{
var (addedMail, removedMail) = await CreateAssignmentInternalAsync(accountId, assignment.MailCopyId, assignment.RemoteFolderId).ConfigureAwait(false);
if (removedMail != null)
removedMails.Add(removedMail);
if (addedMail != null)
addedMails.Add(addedMail);
}
ReportRemovedMails(removedMails);
ReportAddedMails(addedMails);
}
private async Task<(MailCopy AddedMail, MailCopy RemovedMail)> CreateAssignmentInternalAsync(Guid accountId, string mailCopyId, string remoteFolderId)
{ {
// Note: Folder might not be available at the moment due to user not syncing folders before the delta processing. // Note: Folder might not be available at the moment due to user not syncing folders before the delta processing.
// This is a problem, because assignments won't be created. // This is a problem, because assignments won't be created.
@@ -1033,14 +1138,14 @@ public class MailService : BaseDatabaseService, IMailService
_logger.Warning("Local folder not found for remote folder {RemoteFolderId}", remoteFolderId); _logger.Warning("Local folder not found for remote folder {RemoteFolderId}", remoteFolderId);
_logger.Warning("Skipping assignment creation for the the message {MailCopyId}", mailCopyId); _logger.Warning("Skipping assignment creation for the the message {MailCopyId}", mailCopyId);
return; return (null, null);
} }
if (await IsMailExistsAsync(mailCopyId, localFolder.Id).ConfigureAwait(false)) if (await IsMailExistsAsync(mailCopyId, localFolder.Id).ConfigureAwait(false))
{ {
_logger.Debug("Skipping assignment creation for {MailCopyId} because folder {FolderId} already has a local copy.", _logger.Debug("Skipping assignment creation for {MailCopyId} because folder {FolderId} already has a local copy.",
mailCopyId, localFolder.Id); mailCopyId, localFolder.Id);
return; return (null, null);
} }
var mailCopy = await GetSingleMailItemWithoutFolderAssignmentAsync(mailCopyId); var mailCopy = await GetSingleMailItemWithoutFolderAssignmentAsync(mailCopyId);
@@ -1049,9 +1154,12 @@ public class MailService : BaseDatabaseService, IMailService
{ {
_logger.Warning("Can't create assignment for mail {MailCopyId} because it does not exist.", mailCopyId); _logger.Warning("Can't create assignment for mail {MailCopyId} because it does not exist.", mailCopyId);
return; return (null, null);
} }
MailCopy removedMail = null;
var mailCopyToInsert = mailCopy;
if (mailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent && if (mailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent &&
localFolder.SpecialFolderType == SpecialFolderType.Deleted) localFolder.SpecialFolderType == SpecialFolderType.Deleted)
{ {
@@ -1062,21 +1170,52 @@ public class MailService : BaseDatabaseService, IMailService
// This way item will only be visible in Trash folder as in Gmail Web UI. // This way item will only be visible in Trash folder as in Gmail Web UI.
// Don't delete MIME file since if exists. // Don't delete MIME file since if exists.
await DeleteMailInternalAsync(mailCopy, preserveMimeFile: true).ConfigureAwait(false); mailCopyToInsert = CloneMailCopy(mailCopy);
removedMail = await DeleteMailInternalAsync(mailCopy, preserveMimeFile: true, reportUiChange: false).ConfigureAwait(false);
} }
// Copy one of the mail copy and assign it to the new folder. // Copy one of the mail copy and assign it to the new folder.
// We don't need to create a new MIME pack. // We don't need to create a new MIME pack.
// Therefore FileId is not changed for the new MailCopy. // Therefore FileId is not changed for the new MailCopy.
mailCopy.UniqueId = Guid.NewGuid(); mailCopyToInsert.UniqueId = Guid.NewGuid();
mailCopy.FolderId = localFolder.Id; mailCopyToInsert.FolderId = localFolder.Id;
mailCopy.AssignedFolder = localFolder; mailCopyToInsert.AssignedFolder = localFolder;
await InsertMailAsync(mailCopy).ConfigureAwait(false); var addedMail = await InsertMailAsync(mailCopyToInsert, reportUiChange: false).ConfigureAwait(false);
return (addedMail, removedMail);
} }
public async Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId) public async Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
=> await DeleteAssignmentsAsync(accountId, [new MailFolderAssignmentUpdate(mailCopyId, remoteFolderId)]).ConfigureAwait(false);
public async Task DeleteAssignmentsAsync(Guid accountId, IEnumerable<MailFolderAssignmentUpdate> assignments)
{
var targetAssignments = assignments?
.Where(x => x != null &&
!string.IsNullOrWhiteSpace(x.MailCopyId) &&
!string.IsNullOrWhiteSpace(x.RemoteFolderId))
.GroupBy(x => $"{x.MailCopyId}\u001f{x.RemoteFolderId}", StringComparer.Ordinal)
.Select(group => group.First())
.ToList() ?? [];
if (targetAssignments.Count == 0)
return;
var removedMails = new List<MailCopy>(targetAssignments.Count);
foreach (var assignment in targetAssignments)
{
var removedMail = await DeleteAssignmentInternalAsync(accountId, assignment.MailCopyId, assignment.RemoteFolderId).ConfigureAwait(false);
if (removedMail != null)
removedMails.Add(removedMail);
}
ReportRemovedMails(removedMails);
}
private async Task<MailCopy> DeleteAssignmentInternalAsync(Guid accountId, string mailCopyId, string remoteFolderId)
{ {
var mailItem = await GetSingleMailItemAsync(mailCopyId, remoteFolderId).ConfigureAwait(false); var mailItem = await GetSingleMailItemAsync(mailCopyId, remoteFolderId).ConfigureAwait(false);
@@ -1084,7 +1223,7 @@ public class MailService : BaseDatabaseService, IMailService
{ {
_logger.Warning("Mail not found with id {MailCopyId} with remote folder {RemoteFolderId}", mailCopyId, remoteFolderId); _logger.Warning("Mail not found with id {MailCopyId} with remote folder {RemoteFolderId}", mailCopyId, remoteFolderId);
return; return null;
} }
var localFolder = await _folderService.GetFolderAsync(accountId, remoteFolderId); var localFolder = await _folderService.GetFolderAsync(accountId, remoteFolderId);
@@ -1093,10 +1232,50 @@ public class MailService : BaseDatabaseService, IMailService
{ {
_logger.Warning("Local folder not found for remote folder {RemoteFolderId}", remoteFolderId); _logger.Warning("Local folder not found for remote folder {RemoteFolderId}", remoteFolderId);
return; return null;
} }
await DeleteMailInternalAsync(mailItem, preserveMimeFile: false).ConfigureAwait(false); return await DeleteMailInternalAsync(mailItem, preserveMimeFile: false, reportUiChange: false).ConfigureAwait(false);
}
private static MailCopy CloneMailCopy(MailCopy source)
{
if (source == null)
return null;
return new MailCopy
{
UniqueId = source.UniqueId,
Id = source.Id,
FolderId = source.FolderId,
ThreadId = source.ThreadId,
MessageId = source.MessageId,
References = source.References,
InReplyTo = source.InReplyTo,
FromName = source.FromName,
FromAddress = source.FromAddress,
Subject = source.Subject,
PreviewText = source.PreviewText,
CreationDate = source.CreationDate,
Importance = source.Importance,
IsRead = source.IsRead,
IsFlagged = source.IsFlagged,
IsPinned = source.IsPinned,
IsFocused = source.IsFocused,
HasAttachments = source.HasAttachments,
ItemType = source.ItemType,
DraftId = source.DraftId,
IsDraft = source.IsDraft,
FileId = source.FileId,
AssignedFolder = source.AssignedFolder,
AssignedAccount = source.AssignedAccount,
SenderContact = source.SenderContact,
IsReadReceiptRequested = source.IsReadReceiptRequested,
ReadReceiptStatus = source.ReadReceiptStatus,
ReadReceiptAcknowledgedAtUtc = source.ReadReceiptAcknowledgedAtUtc,
ReadReceiptMessageUniqueId = source.ReadReceiptMessageUniqueId,
Categories = source.Categories == null ? [] : [.. source.Categories]
};
} }
public async Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package) public async Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package)
@@ -1113,7 +1292,7 @@ public class MailService : BaseDatabaseService, IMailService
await SaveContactsForPackageAsync(package).ConfigureAwait(false); await SaveContactsForPackageAsync(package).ConfigureAwait(false);
var mimeSaveTask = _mimeFileService.SaveMimeMessageAsync(mailCopy.FileId, mimeMessage, account.Id); var mimeSaveTask = _mimeFileService.SaveMimeMessageAsync(mailCopy.FileId, mimeMessage, account.Id);
var insertMailTask = InsertMailAsync(mailCopy); var insertMailTask = InsertMailAsync(mailCopy, reportUiChange: true);
await Task.WhenAll(mimeSaveTask, insertMailTask).ConfigureAwait(false); await Task.WhenAll(mimeSaveTask, insertMailTask).ConfigureAwait(false);
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false); await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
@@ -1125,6 +1304,129 @@ public class MailService : BaseDatabaseService, IMailService
} }
public async Task CreateMailsAsync(Guid accountId, IReadOnlyList<NewMailItemPackage> packages)
{
var targetPackages = packages?
.Where(package => package != null)
.ToList() ?? [];
if (targetPackages.Count == 0)
return;
var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
if (account == null)
return;
if (account.ProviderType != MailProviderType.Gmail)
{
foreach (var package in targetPackages)
await CreateMailAsync(accountId, package).ConfigureAwait(false);
return;
}
var pendingInserts = new List<(MailCopy MailCopy, NewMailItemPackage Package, MimeMessage MimeMessage)>();
var pendingUpdates = new List<(MailCopy MailCopy, MailCopy ExistingMailCopy, NewMailItemPackage Package, MimeMessage MimeMessage)>();
foreach (var package in targetPackages)
{
if (string.IsNullOrEmpty(package.AssignedRemoteFolderId))
{
_logger.Warning("Remote folder id is not set for {MailCopyId}.", package.Copy?.Id);
_logger.Warning("Ignoring creation of mail.");
continue;
}
var assignedFolder = await _folderService.GetFolderAsync(accountId, package.AssignedRemoteFolderId).ConfigureAwait(false);
if (assignedFolder == null)
{
_logger.Warning("Assigned folder not found for {MailCopyId}.", package.Copy?.Id);
_logger.Warning("Ignoring creation of mail.");
continue;
}
var mailCopy = package.Copy;
var mimeMessage = package.Mime;
mailCopy.UniqueId = Guid.NewGuid();
mailCopy.AssignedAccount = account;
mailCopy.AssignedFolder = assignedFolder;
mailCopy.SenderContact = await GetSenderContactForAccountAsync(account, mailCopy.FromAddress).ConfigureAwait(false);
mailCopy.FolderId = assignedFolder.Id;
if (mimeMessage != null)
{
var isMimeExists = await _mimeFileService.IsMimeExistAsync(accountId, mailCopy.FileId).ConfigureAwait(false);
if (!isMimeExists)
{
bool isMimeSaved = await _mimeFileService.SaveMimeMessageAsync(mailCopy.FileId, mimeMessage, accountId).ConfigureAwait(false);
if (!isMimeSaved)
{
_logger.Warning("Failed to save mime file for {MailCopyId}.", mailCopy.Id);
}
}
}
await SaveContactsForPackageAsync(package).ConfigureAwait(false);
var existingCopyItem = await Connection.Table<MailCopy>()
.FirstOrDefaultAsync(a => a.Id == mailCopy.Id && a.FolderId == assignedFolder.Id)
.ConfigureAwait(false);
if (existingCopyItem != null)
{
mailCopy.UniqueId = existingCopyItem.UniqueId;
pendingUpdates.Add((mailCopy, existingCopyItem, package, mimeMessage));
}
else
{
pendingInserts.Add((mailCopy, package, mimeMessage));
}
}
var insertedMails = new List<MailCopy>(pendingInserts.Count);
foreach (var pendingInsert in pendingInserts)
{
var insertedMail = await InsertMailAsync(pendingInsert.MailCopy, reportUiChange: false).ConfigureAwait(false);
if (insertedMail != null)
insertedMails.Add(insertedMail);
}
var updatedMails = new List<MailCopy>(pendingUpdates.Count);
foreach (var pendingUpdate in pendingUpdates)
{
var updatedMail = await UpdateMailAsync(
pendingUpdate.MailCopy,
reportUiChange: false,
existingMailCopy: pendingUpdate.ExistingMailCopy).ConfigureAwait(false);
if (updatedMail != null)
updatedMails.Add(updatedMail);
}
ReportAddedMails(insertedMails);
ReportUpdatedMails(updatedMails);
foreach (var pendingInsert in pendingInserts)
{
await ReplaceMailCategoriesForPackageAsync(accountId, pendingInsert.MailCopy, pendingInsert.Package).ConfigureAwait(false);
await _sentMailReceiptService.TrackSentMailAsync(pendingInsert.MailCopy, pendingInsert.MimeMessage).ConfigureAwait(false);
await _sentMailReceiptService.ProcessIncomingReceiptAsync(pendingInsert.MailCopy, pendingInsert.MimeMessage).ConfigureAwait(false);
}
foreach (var pendingUpdate in pendingUpdates)
{
await ReplaceMailCategoriesForPackageAsync(accountId, pendingUpdate.MailCopy, pendingUpdate.Package).ConfigureAwait(false);
await _sentMailReceiptService.TrackSentMailAsync(pendingUpdate.MailCopy, pendingUpdate.MimeMessage).ConfigureAwait(false);
await _sentMailReceiptService.ProcessIncomingReceiptAsync(pendingUpdate.MailCopy, pendingUpdate.MimeMessage).ConfigureAwait(false);
}
}
public async Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage package) public async Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage package)
{ {
var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false); var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
@@ -1193,7 +1495,7 @@ public class MailService : BaseDatabaseService, IMailService
{ {
mailCopy.UniqueId = existingCopyItem.UniqueId; mailCopy.UniqueId = existingCopyItem.UniqueId;
await UpdateMailAsync(mailCopy).ConfigureAwait(false); await UpdateMailAsync(mailCopy, reportUiChange: true, existingMailCopy: existingCopyItem).ConfigureAwait(false);
await ReplaceMailCategoriesForPackageAsync(accountId, mailCopy, package).ConfigureAwait(false); await ReplaceMailCategoriesForPackageAsync(accountId, mailCopy, package).ConfigureAwait(false);
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false); await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false); await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
@@ -1210,7 +1512,7 @@ public class MailService : BaseDatabaseService, IMailService
await DeleteMailAsync(accountId, mailCopy.Id).ConfigureAwait(false); await DeleteMailAsync(accountId, mailCopy.Id).ConfigureAwait(false);
} }
await InsertMailAsync(mailCopy).ConfigureAwait(false); await InsertMailAsync(mailCopy, reportUiChange: true).ConfigureAwait(false);
await ReplaceMailCategoriesForPackageAsync(accountId, mailCopy, package).ConfigureAwait(false); await ReplaceMailCategoriesForPackageAsync(accountId, mailCopy, package).ConfigureAwait(false);
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false); await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false); await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);