Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ef12e0ff3a | |||
| e7e201758e | |||
| 55ae6e1f3a | |||
| 3ea8b8ac46 | |||
| 8fe40e9fe3 |
@@ -34,6 +34,7 @@ public interface IMailService
|
||||
/// <param name="accountId">Account to remove from</param>
|
||||
/// <param name="mailCopyId">Mail copy id to remove.</param>
|
||||
Task DeleteMailAsync(Guid accountId, string mailCopyId);
|
||||
Task DeleteMailsAsync(Guid accountId, IEnumerable<string> mailCopyIds);
|
||||
|
||||
Task ChangeReadStatusAsync(string mailCopyId, bool isRead);
|
||||
Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged);
|
||||
@@ -42,8 +43,11 @@ public interface IMailService
|
||||
|
||||
Task CreateAssignmentAsync(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 CreateMailsAsync(Guid accountId, IReadOnlyList<NewMailItemPackage> packages);
|
||||
|
||||
/// <summary>
|
||||
/// Maps new mail item with the existing local draft copy.
|
||||
|
||||
@@ -51,6 +51,11 @@ public interface INotificationBuilder
|
||||
/// </summary>
|
||||
void CreateStoreUpdateNotification();
|
||||
|
||||
/// <summary>
|
||||
/// Shows the one-time release migration notification.
|
||||
/// </summary>
|
||||
void CreateReleaseMigrationNotification();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a calendar reminder toast for the specified calendar item.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Wino.Core.Domain.Models.MailItem;
|
||||
|
||||
public sealed record MailFolderAssignmentUpdate(string MailCopyId, string RemoteFolderId);
|
||||
@@ -704,6 +704,8 @@
|
||||
"Notifications_WinoUpdatedTitle": "Wino Mail has been updated.",
|
||||
"Notifications_StoreUpdateAvailableTitle": "Update available",
|
||||
"Notifications_StoreUpdateAvailableMessage": "A newer version of Wino Mail is ready to install from Microsoft Store.",
|
||||
"Notifications_ReleaseMigrationTitle": "New Wino Mail & Calendar",
|
||||
"Notifications_ReleaseMigrationMessage": "Wino Mail got updated to the next version. Please re-create your accounts and start using the next version including calendar, mail templates, shortcuts, and a bunch of other improvements.",
|
||||
"OnlineSearchFailed_Message": "Failed to perform search\n{0}\n\nListing offline mails.",
|
||||
"OnlineSearchTry_Line1": "Can't find what you are looking for?",
|
||||
"OnlineSearchTry_Line2": "Try online search.",
|
||||
|
||||
@@ -19,6 +19,32 @@ namespace Wino.Core.Tests.Synchronizers;
|
||||
|
||||
public sealed class GmailSynchronizerRequestSuccessTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task UpdateAccountSyncIdentifierAsync_EmptyStoredIdentifier_PersistsFirstHistoryCursor()
|
||||
{
|
||||
var changeProcessor = new Mock<IGmailChangeProcessor>(MockBehavior.Strict);
|
||||
changeProcessor
|
||||
.Setup(x => x.UpdateAccountDeltaSynchronizationIdentifierAsync(It.IsAny<Guid>(), "123"))
|
||||
.ReturnsAsync("123");
|
||||
|
||||
var synchronizer = CreateSynchronizer(changeProcessor.Object, synchronizationDeltaIdentifier: string.Empty);
|
||||
|
||||
await InvokeUpdateAccountSyncIdentifierAsync(synchronizer, 123);
|
||||
|
||||
changeProcessor.Verify(x => x.UpdateAccountDeltaSynchronizationIdentifierAsync(It.IsAny<Guid>(), "123"), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UpdateAccountSyncIdentifierAsync_OlderHistoryCursor_DoesNotRegressStoredCursor()
|
||||
{
|
||||
var changeProcessor = new Mock<IGmailChangeProcessor>(MockBehavior.Strict);
|
||||
var synchronizer = CreateSynchronizer(changeProcessor.Object, synchronizationDeltaIdentifier: "456");
|
||||
|
||||
await InvokeUpdateAccountSyncIdentifierAsync(synchronizer, 123);
|
||||
|
||||
changeProcessor.Verify(x => x.UpdateAccountDeltaSynchronizationIdentifierAsync(It.IsAny<Guid>(), It.IsAny<string>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ProcessSingleNativeRequestResponseAsync_BatchMarkReadRequest_PersistsLocalReadStateForEachMail()
|
||||
{
|
||||
@@ -209,13 +235,15 @@ public sealed class GmailSynchronizerRequestSuccessTests
|
||||
|
||||
private static GmailSynchronizer CreateSynchronizer(
|
||||
IGmailChangeProcessor changeProcessor,
|
||||
IGmailSynchronizerErrorHandlerFactory? errorFactory = null)
|
||||
IGmailSynchronizerErrorHandlerFactory? errorFactory = null,
|
||||
string? synchronizationDeltaIdentifier = null)
|
||||
{
|
||||
var account = new MailAccount
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Gmail",
|
||||
Address = "user@example.com"
|
||||
Address = "user@example.com",
|
||||
SynchronizationDeltaIdentifier = synchronizationDeltaIdentifier
|
||||
};
|
||||
|
||||
var authenticator = new Mock<IGmailAuthenticator>(MockBehavior.Loose);
|
||||
@@ -249,4 +277,17 @@ public sealed class GmailSynchronizerRequestSuccessTests
|
||||
task.Should().NotBeNull();
|
||||
await task!;
|
||||
}
|
||||
|
||||
private static async Task InvokeUpdateAccountSyncIdentifierAsync(GmailSynchronizer synchronizer, ulong historyId)
|
||||
{
|
||||
var method = typeof(GmailSynchronizer).GetMethod(
|
||||
"UpdateAccountSyncIdentifierAsync",
|
||||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
|
||||
method.Should().NotBeNull();
|
||||
|
||||
var task = method!.Invoke(synchronizer, [historyId]) as Task;
|
||||
task.Should().NotBeNull();
|
||||
await task!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -571,6 +571,19 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
||||
}
|
||||
|
||||
var aiStatus = aiStatusResponse.Result;
|
||||
if (IsExpiredAiEntitlement(aiStatus.EntitlementStatus))
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
_aiPackAddOn.IsPurchased = false;
|
||||
_aiPackAddOn.HasUsageData = false;
|
||||
_aiPackAddOn.ErrorText = string.Empty;
|
||||
_aiPackAddOn.RenewalText = string.Empty;
|
||||
_aiPackAddOn.UsageResetText = string.Empty;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (aiStatus.MonthlyLimit is not int usageLimit || usageLimit <= 0 || aiStatus.Used is not int usageCount)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
@@ -613,4 +626,7 @@ public partial class WinoAccountManagementPageViewModel : CoreBaseViewModel,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsExpiredAiEntitlement(string? entitlementStatus)
|
||||
=> string.Equals(entitlementStatus, "Expired", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ public interface IDefaultChangeProcessor
|
||||
Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead);
|
||||
Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged);
|
||||
Task<bool> CreateMailAsync(Guid AccountId, NewMailItemPackage package);
|
||||
Task CreateMailsAsync(Guid accountId, IReadOnlyList<NewMailItemPackage> packages);
|
||||
Task DeleteMailAsync(Guid accountId, string mailId);
|
||||
Task DeleteMailsAsync(Guid accountId, IEnumerable<string> mailIds);
|
||||
Task<List<MailCopy>> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable<string> downloadedMailCopyIds);
|
||||
Task SaveMimeFileAsync(Guid fileId, MimeMessage mimeMessage, Guid accountId);
|
||||
Task DeleteFolderAsync(Guid accountId, string remoteFolderId);
|
||||
@@ -56,6 +58,9 @@ public interface IDefaultChangeProcessor
|
||||
Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken);
|
||||
Task<List<MailCopy>> GetMailCopiesAsync(IEnumerable<string> mailCopyIds);
|
||||
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 UpsertMailInvitationCalendarMappingAsync(MailInvitationCalendarMapping mapping);
|
||||
Task<MailInvitationCalendarMapping> GetMailInvitationCalendarMappingAsync(Guid accountId, string mailCopyId);
|
||||
@@ -156,18 +161,33 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
|
||||
public Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead)
|
||||
=> MailService.ChangeReadStatusAsync(mailCopyId, isRead);
|
||||
|
||||
public Task ApplyMailStateUpdatesAsync(IEnumerable<MailCopyStateUpdate> updates)
|
||||
=> MailService.ApplyMailStateUpdatesAsync(updates);
|
||||
|
||||
public Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string 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)
|
||||
=> MailService.DeleteMailAsync(accountId, mailId);
|
||||
|
||||
public Task DeleteMailsAsync(Guid accountId, IEnumerable<string> mailIds)
|
||||
=> MailService.DeleteMailsAsync(accountId, mailIds);
|
||||
|
||||
public Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage 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)
|
||||
=> 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)
|
||||
=> MailService.MapLocalDraftAsync(accountId, localDraftCopyUniqueId, newMailCopyId, newDraftId, newThreadId);
|
||||
|
||||
|
||||
@@ -1050,6 +1050,11 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
{
|
||||
_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)
|
||||
{
|
||||
// Handle label additions.
|
||||
@@ -1057,7 +1062,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
{
|
||||
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)
|
||||
{
|
||||
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);
|
||||
|
||||
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.
|
||||
bool archived = await _gmailChangeProcessor.IsMailExistsInFolderAsync(archivedMessageId, archiveFolderId.Value);
|
||||
bool archived = await _gmailChangeProcessor.IsMailExistsInFolderAsync(archivedMessageId, archiveFolderId.Value).ConfigureAwait(false);
|
||||
|
||||
if (archived) return;
|
||||
|
||||
_logger.Debug("Processing archive assignment for message {Id}", archivedMessageId);
|
||||
|
||||
await _gmailChangeProcessor.CreateAssignmentAsync(Account.Id, archivedMessageId, ServiceConstants.ARCHIVE_LABEL_ID).ConfigureAwait(false);
|
||||
QueueAssignmentChange(
|
||||
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.
|
||||
bool archived = await _gmailChangeProcessor.IsMailExistsInFolderAsync(unarchivedMessageId, archiveFolderId.Value);
|
||||
bool archived = await _gmailChangeProcessor.IsMailExistsInFolderAsync(unarchivedMessageId, archiveFolderId.Value).ConfigureAwait(false);
|
||||
if (!archived) return;
|
||||
|
||||
_logger.Debug("Processing un-archive assignment for message {Id}", unarchivedMessageId);
|
||||
|
||||
await _gmailChangeProcessor.DeleteAssignmentAsync(Account.Id, unarchivedMessageId, ServiceConstants.ARCHIVE_LABEL_ID).ConfigureAwait(false);
|
||||
QueueAssignmentChange(
|
||||
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;
|
||||
|
||||
@@ -1119,23 +1196,31 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
// ARCHIVE is a virtual folder - handle it separately
|
||||
if (labelId == ServiceConstants.ARCHIVE_LABEL_ID)
|
||||
{
|
||||
await HandleArchiveAssignmentAsync(messageId).ConfigureAwait(false);
|
||||
await HandleArchiveAssignmentAsync(messageId, pendingAssignmentCreates, pendingAssignmentDeletes).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// When UNREAD label is added mark the message as un-read.
|
||||
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.
|
||||
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;
|
||||
|
||||
@@ -1146,20 +1231,23 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
// ARCHIVE is a virtual folder - handle it separately
|
||||
if (labelId == ServiceConstants.ARCHIVE_LABEL_ID)
|
||||
{
|
||||
await HandleUnarchiveAssignmentAsync(messageId).ConfigureAwait(false);
|
||||
await HandleUnarchiveAssignmentAsync(messageId, pendingAssignmentCreates, pendingAssignmentDeletes).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// When UNREAD label is removed mark the message as read.
|
||||
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.
|
||||
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.
|
||||
await _gmailChangeProcessor.DeleteAssignmentAsync(Account.Id, messageId, labelId).ConfigureAwait(false);
|
||||
QueueAssignmentChange(
|
||||
pendingAssignmentCreates,
|
||||
pendingAssignmentDeletes,
|
||||
new MailFolderAssignmentUpdate(messageId, labelId),
|
||||
shouldCreate: false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1542,6 +1630,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
await Task.WhenAll(batchTasks).ConfigureAwait(false);
|
||||
|
||||
// Process all downloaded messages
|
||||
var pendingPackages = new List<NewMailItemPackage>();
|
||||
|
||||
foreach (var gmailMessage in downloadedMessages)
|
||||
{
|
||||
try
|
||||
@@ -1552,12 +1642,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
var packages = await CreateNewMailPackagesAsync(gmailMessage, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (packages != null)
|
||||
{
|
||||
foreach (var package in packages)
|
||||
{
|
||||
await _gmailChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
pendingPackages.AddRange(packages);
|
||||
|
||||
// Update sync identifier if available
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
|
||||
}
|
||||
await _gmailChangeProcessor.CreateMailsAsync(Account.Id, packages).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Update sync identifier if available
|
||||
@@ -1897,9 +1984,19 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
if (historyId == null) return false;
|
||||
|
||||
var newHistoryId = historyId.Value;
|
||||
var currentSynchronizationIdentifier = Account.SynchronizationDeltaIdentifier;
|
||||
|
||||
return Account.SynchronizationDeltaIdentifier == null ||
|
||||
(ulong.TryParse(Account.SynchronizationDeltaIdentifier, out ulong currentIdentifier) && newHistoryId > currentIdentifier);
|
||||
if (string.IsNullOrWhiteSpace(currentSynchronizationIdentifier))
|
||||
return true;
|
||||
|
||||
if (!ulong.TryParse(currentSynchronizationIdentifier, out ulong currentIdentifier))
|
||||
{
|
||||
_logger.Warning("Current Gmail history ID '{HistoryId}' is invalid for {Name}. Replacing it with {NewHistoryId}.",
|
||||
currentSynchronizationIdentifier, Account.Name, newHistoryId);
|
||||
return true;
|
||||
}
|
||||
|
||||
return newHistoryId > currentIdentifier;
|
||||
}
|
||||
|
||||
private async Task UpdateAccountSyncIdentifierAsync(ulong? historyId)
|
||||
@@ -1932,12 +2029,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
// Create mail packages from the downloaded message
|
||||
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.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
|
||||
}
|
||||
await _gmailChangeProcessor.CreateMailsAsync(Account.Id, packages).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await UpdateAccountSyncIdentifierAsync(gmailMessage.HistoryId).ConfigureAwait(false);
|
||||
@@ -1989,25 +2083,27 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
switch (bundle.UIChangeRequest)
|
||||
{
|
||||
case BatchMarkReadRequest batchMarkReadRequest:
|
||||
foreach (var request in batchMarkReadRequest)
|
||||
{
|
||||
await _gmailChangeProcessor.ChangeMailReadStatusAsync(request.Item.Id, request.IsRead).ConfigureAwait(false);
|
||||
}
|
||||
await _gmailChangeProcessor.ApplyMailStateUpdatesAsync(
|
||||
batchMarkReadRequest.Select(request => new MailCopyStateUpdate(request.Item.Id, IsRead: request.IsRead)))
|
||||
.ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
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;
|
||||
|
||||
case BatchChangeFlagRequest batchChangeFlagRequest:
|
||||
foreach (var request in batchChangeFlagRequest)
|
||||
{
|
||||
await _gmailChangeProcessor.ChangeFlagStatusAsync(request.Item.Id, request.IsFlagged).ConfigureAwait(false);
|
||||
}
|
||||
await _gmailChangeProcessor.ApplyMailStateUpdatesAsync(
|
||||
batchChangeFlagRequest.Select(request => new MailCopyStateUpdate(request.Item.Id, IsFlagged: request.IsFlagged)))
|
||||
.ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -2065,16 +2161,31 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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>
|
||||
{
|
||||
private const int UiMutationBatchSize = 40;
|
||||
|
||||
// We cache each mail copy id for faster access on updates.
|
||||
// If the item provider here for update or removal doesn't exist here
|
||||
// we can ignore the operation.
|
||||
@@ -763,18 +765,21 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
||||
// Execute all updates in a single UI thread call
|
||||
if (itemsToUpdate.Count > 0)
|
||||
{
|
||||
await ExecuteUIThread(() =>
|
||||
foreach (var updateBatch in itemsToUpdate.Chunk(UiMutationBatchSize))
|
||||
{
|
||||
foreach (var (existing, updated) in itemsToUpdate)
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
UpdateUniqueIdHashes(existing, false);
|
||||
existing.UpdateFrom(updated);
|
||||
UpdateUniqueIdHashes(existing, true);
|
||||
}
|
||||
});
|
||||
foreach (var (existing, updated) in updateBatch)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var groupedItems = await Task.Run(() => itemsToAdd
|
||||
@@ -787,32 +792,35 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
||||
})
|
||||
.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;
|
||||
var groupItems = groupedItem.Items;
|
||||
|
||||
// Update caches first
|
||||
foreach (var item in groupItems)
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
UpdateUniqueIdHashes(item, true);
|
||||
UpdateThreadIdCache(item, true);
|
||||
}
|
||||
|
||||
foreach (var item in groupItems)
|
||||
{
|
||||
_mailItemSource.InsertItem(groupKey, listComparer, item, listComparer);
|
||||
|
||||
var targetGroup = _mailItemSource.FirstGroupByKeyOrDefault(groupKey);
|
||||
if (targetGroup != null)
|
||||
// Update caches first so lookup helpers remain consistent during inserts.
|
||||
foreach (var item in groupBatch)
|
||||
{
|
||||
_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)
|
||||
return;
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
foreach (var updateBatch in updates.Chunk(UiMutationBatchSize))
|
||||
{
|
||||
foreach (var update in updates)
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
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)
|
||||
foreach (var update in updateBatch)
|
||||
{
|
||||
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)
|
||||
@@ -1018,22 +1029,25 @@ public class WinoMailCollection : ObservableRecipient, IRecipient<SelectedItemsC
|
||||
return;
|
||||
}
|
||||
|
||||
await ExecuteUIThread(() =>
|
||||
foreach (var updateBatch in updates.Chunk(UiMutationBatchSize))
|
||||
{
|
||||
foreach (var update in updates)
|
||||
await ExecuteUIThread(() =>
|
||||
{
|
||||
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)
|
||||
foreach (var update in updateBatch)
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -479,6 +479,8 @@ public partial class App : WinoApplication,
|
||||
|
||||
EnsureAppNotificationRegistration();
|
||||
|
||||
await TranslationService.InitializeAsync();
|
||||
|
||||
await Services.GetRequiredService<ReleaseLocalAccountDataCleanupService>()
|
||||
.RunIfNeededAsync();
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<Identity
|
||||
Name="58272BurakKSE.WinoMailPreview"
|
||||
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
|
||||
Version="2.0.6.0" />
|
||||
Version="2.0.7.0" />
|
||||
|
||||
<mp:PhoneIdentity PhoneProductId="7b7e90e9-cc55-4409-9769-99b4b5ed6e9b" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||
|
||||
|
||||
@@ -233,6 +233,17 @@ public class NotificationBuilder : INotificationBuilder
|
||||
ShowNotification(builder, "store-update-available");
|
||||
}
|
||||
|
||||
public void CreateReleaseMigrationNotification()
|
||||
{
|
||||
var builder = CreateBuilder();
|
||||
builder.AddText(Translator.Notifications_ReleaseMigrationTitle);
|
||||
builder.AddText(Translator.Notifications_ReleaseMigrationMessage);
|
||||
builder.AddArgument(Constants.ToastModeKey, Constants.ToastModeMail);
|
||||
builder.AddButton(CreateDismissButton());
|
||||
|
||||
ShowNotification(builder, "release-migration-v2");
|
||||
}
|
||||
|
||||
public Task CreateCalendarReminderNotificationAsync(CalendarItem calendarItem, long reminderDurationInSeconds)
|
||||
{
|
||||
if (calendarItem == null)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
@@ -14,13 +15,16 @@ public sealed class ReleaseLocalAccountDataCleanupService
|
||||
|
||||
private readonly IConfigurationService _configurationService;
|
||||
private readonly IApplicationConfiguration _applicationConfiguration;
|
||||
private readonly INotificationBuilder _notificationBuilder;
|
||||
private readonly ILogger _logger = Log.ForContext<ReleaseLocalAccountDataCleanupService>();
|
||||
|
||||
public ReleaseLocalAccountDataCleanupService(IConfigurationService configurationService,
|
||||
IApplicationConfiguration applicationConfiguration)
|
||||
IApplicationConfiguration applicationConfiguration,
|
||||
INotificationBuilder notificationBuilder)
|
||||
{
|
||||
_configurationService = configurationService;
|
||||
_applicationConfiguration = applicationConfiguration;
|
||||
_notificationBuilder = notificationBuilder;
|
||||
}
|
||||
|
||||
public async Task RunIfNeededAsync()
|
||||
@@ -45,44 +49,70 @@ public sealed class ReleaseLocalAccountDataCleanupService
|
||||
Path.Combine(publisherPath, LegacyDatabaseFileName)
|
||||
};
|
||||
|
||||
var hadLegacyData = false;
|
||||
|
||||
foreach (var targetPath in cleanupTargets)
|
||||
{
|
||||
await DeletePathIfExistsAsync(localFolderPath, targetPath).ConfigureAwait(false);
|
||||
hadLegacyData |= await DeletePathIfExistsAsync(targetPath, localFolderPath, publisherPath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_configurationService.Set(CleanupCompletedSettingKey, true);
|
||||
|
||||
if (hadLegacyData)
|
||||
{
|
||||
_notificationBuilder.CreateReleaseMigrationNotification();
|
||||
}
|
||||
|
||||
_logger.Information("Completed one-time local account data cleanup for release migration.");
|
||||
}
|
||||
|
||||
private async Task DeletePathIfExistsAsync(string localFolderPath, string targetPath)
|
||||
private async Task<bool> DeletePathIfExistsAsync(string targetPath, params string[] allowedRootPaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fullTargetPath = Path.GetFullPath(targetPath);
|
||||
var fullLocalFolderPath = Path.GetFullPath(localFolderPath);
|
||||
|
||||
if (!fullTargetPath.StartsWith(fullLocalFolderPath, StringComparison.OrdinalIgnoreCase))
|
||||
if (!allowedRootPaths.Any(rootPath => IsPathUnderAllowedRoot(fullTargetPath, rootPath)))
|
||||
{
|
||||
_logger.Warning("Skipped startup cleanup for path outside local folder: {TargetPath}", fullTargetPath);
|
||||
return;
|
||||
_logger.Warning("Skipped startup cleanup for path outside allowed roots: {TargetPath}", fullTargetPath);
|
||||
return false;
|
||||
}
|
||||
|
||||
var targetExists = Directory.Exists(fullTargetPath) || File.Exists(fullTargetPath);
|
||||
|
||||
if (Directory.Exists(fullTargetPath))
|
||||
{
|
||||
await Task.Run(() => Directory.Delete(fullTargetPath, recursive: true)).ConfigureAwait(false);
|
||||
_logger.Information("Deleted legacy startup cleanup directory {TargetPath}", fullTargetPath);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (File.Exists(fullTargetPath))
|
||||
{
|
||||
File.Delete(fullTargetPath);
|
||||
_logger.Information("Deleted legacy startup cleanup file {TargetPath}", fullTargetPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return targetExists;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Failed to delete legacy startup cleanup path {TargetPath}", targetPath);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsPathUnderAllowedRoot(string fullTargetPath, string rootPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rootPath))
|
||||
return false;
|
||||
|
||||
var fullRootPath = Path.GetFullPath(rootPath);
|
||||
var relativePath = Path.GetRelativePath(fullRootPath, fullTargetPath);
|
||||
|
||||
return relativePath != "." &&
|
||||
!relativePath.StartsWith("..", StringComparison.Ordinal) &&
|
||||
!Path.IsPathRooted(relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
+334
-32
@@ -727,32 +727,70 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
|
||||
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)
|
||||
{
|
||||
// Delete mime file as well.
|
||||
// Even though Gmail might have multiple copies for the same mail, we only have one MIME file for all.
|
||||
// Their FileId is inserted same.
|
||||
public async Task DeleteMailsAsync(Guid accountId, IEnumerable<string> mailCopyIds)
|
||||
{
|
||||
var targetMailIds = mailCopyIds?
|
||||
.Where(id => !string.IsNullOrWhiteSpace(id))
|
||||
.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
|
||||
|
||||
private async Task InsertMailAsync(MailCopy mailCopy)
|
||||
private async Task<MailCopy> InsertMailAsync(MailCopy mailCopy, bool reportUiChange)
|
||||
{
|
||||
if (mailCopy == null)
|
||||
{
|
||||
_logger.Warning("Null mail passed to InsertMailAsync call.");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (mailCopy.FolderId == Guid.Empty)
|
||||
{
|
||||
_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);
|
||||
@@ -760,21 +798,27 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
await Connection.InsertAsync(mailCopy, typeof(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)
|
||||
=> await UpdateMailAsync(mailCopy, reportUiChange: true).ConfigureAwait(false);
|
||||
|
||||
private async Task<MailCopy> UpdateMailAsync(MailCopy mailCopy, bool reportUiChange, MailCopy existingMailCopy = null)
|
||||
{
|
||||
if (mailCopy == null)
|
||||
{
|
||||
_logger.Warning("Null mail passed to UpdateMailAsync call.");
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
_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)
|
||||
: null;
|
||||
|
||||
@@ -787,16 +831,19 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
await Connection.UpdateAsync(mailCopy, typeof(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)
|
||||
{
|
||||
_logger.Warning("Null mail passed to DeleteMailAsync call.");
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1021,6 +1092,40 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
}
|
||||
|
||||
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.
|
||||
// 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("Skipping assignment creation for the the message {MailCopyId}", mailCopyId);
|
||||
|
||||
return;
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
if (await IsMailExistsAsync(mailCopyId, localFolder.Id).ConfigureAwait(false))
|
||||
{
|
||||
_logger.Debug("Skipping assignment creation for {MailCopyId} because folder {FolderId} already has a local copy.",
|
||||
mailCopyId, localFolder.Id);
|
||||
return;
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
return;
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
MailCopy removedMail = null;
|
||||
var mailCopyToInsert = mailCopy;
|
||||
|
||||
if (mailCopy.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent &&
|
||||
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.
|
||||
// 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.
|
||||
// We don't need to create a new MIME pack.
|
||||
// Therefore FileId is not changed for the new MailCopy.
|
||||
|
||||
mailCopy.UniqueId = Guid.NewGuid();
|
||||
mailCopy.FolderId = localFolder.Id;
|
||||
mailCopy.AssignedFolder = localFolder;
|
||||
mailCopyToInsert.UniqueId = Guid.NewGuid();
|
||||
mailCopyToInsert.FolderId = localFolder.Id;
|
||||
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)
|
||||
=> 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);
|
||||
|
||||
@@ -1084,7 +1223,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
{
|
||||
_logger.Warning("Mail not found with id {MailCopyId} with remote folder {RemoteFolderId}", mailCopyId, remoteFolderId);
|
||||
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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)
|
||||
@@ -1113,7 +1292,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
await SaveContactsForPackageAsync(package).ConfigureAwait(false);
|
||||
|
||||
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 _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)
|
||||
{
|
||||
var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
|
||||
@@ -1193,7 +1495,7 @@ public class MailService : BaseDatabaseService, IMailService
|
||||
{
|
||||
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 _sentMailReceiptService.TrackSentMailAsync(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 InsertMailAsync(mailCopy).ConfigureAwait(false);
|
||||
await InsertMailAsync(mailCopy, reportUiChange: true).ConfigureAwait(false);
|
||||
await ReplaceMailCategoriesForPackageAsync(accountId, mailCopy, package).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.TrackSentMailAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||
await _sentMailReceiptService.ProcessIncomingReceiptAsync(mailCopy, mimeMessage).ConfigureAwait(false);
|
||||
|
||||
Reference in New Issue
Block a user