From 23d0eeab16535b83810e6aa7cf29551717233991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Burak=20Kaan=20K=C3=B6se?= Date: Sat, 25 May 2024 17:00:52 +0200 Subject: [PATCH] Prevent downloading existing messages for Outlook. --- Wino.Core.Domain/Interfaces/IMailService.cs | 8 +++++ Wino.Core/CoreContainerSetup.cs | 1 + .../Processors/DefaultChangeProcessor.cs | 29 ++++++++++++------- .../Processors/OutlookChangeProcessor.cs | 16 ++++++++++ Wino.Core/Services/MailService.cs | 3 +- .../Synchronizers/OutlookSynchronizer.cs | 18 +++++++++--- Wino.Core/WinoSynchronizerFactory.cs | 7 +++-- 7 files changed, 64 insertions(+), 18 deletions(-) create mode 100644 Wino.Core/Integration/Processors/OutlookChangeProcessor.cs diff --git a/Wino.Core.Domain/Interfaces/IMailService.cs b/Wino.Core.Domain/Interfaces/IMailService.cs index 7b09dc51..30f7083f 100644 --- a/Wino.Core.Domain/Interfaces/IMailService.cs +++ b/Wino.Core.Domain/Interfaces/IMailService.cs @@ -77,5 +77,13 @@ namespace Wino.Core.Domain.Interfaces /// Unique id of the mail item. /// Account that mail belongs to. Task GetMailAccountByUniqueIdAsync(Guid uniqueMailId); + + /// + /// Checks whether the given mail copy id exists in the database. + /// Safely used for Outlook to prevent downloading the same mail twice. + /// For Gmail, it should be avoided since one mail may belong to multiple folders. + /// + /// Native mail id of the message. + Task IsMailExistsAsync(string mailCopyId); } } diff --git a/Wino.Core/CoreContainerSetup.cs b/Wino.Core/CoreContainerSetup.cs index 67f4c2ce..c2cd5839 100644 --- a/Wino.Core/CoreContainerSetup.cs +++ b/Wino.Core/CoreContainerSetup.cs @@ -23,6 +23,7 @@ namespace Wino.Core services.AddSingleton(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs index ecf10231..0bbcb6d4 100644 --- a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs @@ -55,7 +55,13 @@ namespace Wino.Core.Integration.Processors public interface IOutlookChangeProcessor : IDefaultChangeProcessor { - + /// + /// Interrupted initial synchronization may cause downloaded mails to be saved in the database twice. + /// Since downloading mime is costly in Outlook, we need to check if the actual copy of the message has been saved before. + /// + /// MailCopyId of the message. + /// Whether the mime has b + Task IsMailExistsAsync(string messageId); } public class DefaultChangeProcessor(IDatabaseService databaseService, @@ -64,8 +70,9 @@ namespace Wino.Core.Integration.Processors IAccountService accountService, IMimeFileService mimeFileService) : BaseDatabaseService(databaseService), IDefaultChangeProcessor { + protected IMailService MailService = mailService; + private readonly IFolderService _folderService = folderService; - private readonly IMailService _mailService = mailService; private readonly IAccountService _accountService = accountService; private readonly IMimeFileService _mimeFileService = mimeFileService; @@ -73,32 +80,32 @@ namespace Wino.Core.Integration.Processors => _accountService.UpdateSynchronizationIdentifierAsync(accountId, synchronizationDeltaIdentifier); public Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged) - => _mailService.ChangeFlagStatusAsync(mailCopyId, isFlagged); + => MailService.ChangeFlagStatusAsync(mailCopyId, isFlagged); public Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead) - => _mailService.ChangeReadStatusAsync(mailCopyId, isRead); + => MailService.ChangeReadStatusAsync(mailCopyId, isRead); public Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId) - => _mailService.DeleteAssignmentAsync(accountId, mailCopyId, remoteFolderId); + => MailService.DeleteAssignmentAsync(accountId, mailCopyId, remoteFolderId); public Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId) - => _mailService.CreateAssignmentAsync(accountId, mailCopyId, remoteFolderId); + => MailService.CreateAssignmentAsync(accountId, mailCopyId, remoteFolderId); public Task DeleteMailAsync(Guid accountId, string mailId) - => _mailService.DeleteMailAsync(accountId, mailId); + => MailService.DeleteMailAsync(accountId, mailId); public Task CreateMailAsync(Guid accountId, NewMailItemPackage package) - => _mailService.CreateMailAsync(accountId, package); + => MailService.CreateMailAsync(accountId, package); // Folder methods public Task UpdateFolderStructureAsync(Guid accountId, List allFolders) => _folderService.BulkUpdateFolderStructureAsync(accountId, allFolders); public Task 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); public Task MapLocalDraftAsync(string mailCopyId, string newDraftId, string newThreadId) - => _mailService.MapLocalDraftAsync(mailCopyId, newDraftId, newThreadId); + => MailService.MapLocalDraftAsync(mailCopyId, newDraftId, newThreadId); public Task> GetSynchronizationFoldersAsync(SynchronizationOptions options) => _folderService.GetSynchronizationFoldersAsync(options); @@ -113,7 +120,7 @@ namespace Wino.Core.Integration.Processors => _folderService.InsertFolderAsync(folder); public Task> GetDownloadedUnreadMailsAsync(Guid accountId, IEnumerable downloadedMailCopyIds) - => _mailService.GetDownloadedUnreadMailsAsync(accountId, downloadedMailCopyIds); + => MailService.GetDownloadedUnreadMailsAsync(accountId, downloadedMailCopyIds); public Task> GetKnownUidsForFolderAsync(Guid folderId) => _folderService.GetKnownUidsForFolderAsync(folderId); diff --git a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs new file mode 100644 index 00000000..c36d3991 --- /dev/null +++ b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Services; + +namespace Wino.Core.Integration.Processors +{ + public class OutlookChangeProcessor(IDatabaseService databaseService, + IFolderService folderService, + IMailService mailService, + IAccountService accountService, + IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, accountService, mimeFileService), IOutlookChangeProcessor + { + public Task IsMailExistsAsync(string messageId) + => MailService.IsMailExistsAsync(messageId); + } +} diff --git a/Wino.Core/Services/MailService.cs b/Wino.Core/Services/MailService.cs index cff165f2..fdaf4e9f 100644 --- a/Wino.Core/Services/MailService.cs +++ b/Wino.Core/Services/MailService.cs @@ -856,6 +856,7 @@ namespace Wino.Core.Services return Connection.FindWithQueryAsync(query); } - + public Task IsMailExistsAsync(string mailCopyId) + => Connection.ExecuteScalarAsync("SELECT EXISTS(SELECT 1 FROM MailCopy WHERE Id = ?)", mailCopyId); } } diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index 738b8672..8947dbb7 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -63,11 +63,11 @@ namespace Wino.Core.Synchronizers ]; private readonly ILogger _logger = Log.ForContext(); - private readonly IDefaultChangeProcessor _outlookChangeProcessor; + private readonly IOutlookChangeProcessor _outlookChangeProcessor; private readonly GraphServiceClient _graphClient; public OutlookSynchronizer(MailAccount account, IAuthenticator authenticator, - IDefaultChangeProcessor outlookChangeProcessor) : base(account) + IOutlookChangeProcessor outlookChangeProcessor) : base(account) { var tokenProvider = new MicrosoftTokenProvider(Account, authenticator); @@ -191,7 +191,7 @@ namespace Wino.Core.Synchronizers { // Parse Delta Token from Delta Link since v5 of Graph SDK works based on the token, not the link. - var deltaToken = Regex.Split(messageIteratorAsync.Deltalink, "deltatoken=")[1]; + var deltaToken = GetDeltaTokenFromDeltaLink(latestDeltaLink); await _outlookChangeProcessor.UpdateFolderDeltaSynchronizationIdentifierAsync(folder.Id, deltaToken).ConfigureAwait(false); } @@ -199,6 +199,9 @@ namespace Wino.Core.Synchronizers return downloadedMessageIds; } + private string GetDeltaTokenFromDeltaLink(string deltaLink) + => Regex.Split(deltaLink, "deltatoken=")[1]; + private bool IsResourceDeleted(IDictionary additionalData) => additionalData != null && additionalData.ContainsKey("@removed"); @@ -606,7 +609,14 @@ namespace Wino.Core.Synchronizers public override async Task> CreateNewMailPackagesAsync(Message message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default) { - var mimeMessage = await DownloadMimeMessageAsync(message.Id).ConfigureAwait(false); + bool isMailExists = await _outlookChangeProcessor.IsMailExistsAsync(message.Id); + + if (isMailExists) + { + return null; + } + + var mimeMessage = await DownloadMimeMessageAsync(message.Id, cancellationToken).ConfigureAwait(false); var mailCopy = message.AsMailCopy(); if (mimeMessage.Headers.Contains(Domain.Constants.WinoLocalDraftHeader) diff --git a/Wino.Core/WinoSynchronizerFactory.cs b/Wino.Core/WinoSynchronizerFactory.cs index 58a31ee6..2ca63a47 100644 --- a/Wino.Core/WinoSynchronizerFactory.cs +++ b/Wino.Core/WinoSynchronizerFactory.cs @@ -38,6 +38,7 @@ namespace Wino.Core private readonly IDatabaseService _databaseService; private readonly IMimeFileService _mimeFileService; private readonly IDefaultChangeProcessor _defaultChangeProcessor; + private readonly IOutlookChangeProcessor _outlookChangeProcessor; public WinoSynchronizerFactory(INativeAppService nativeAppService, ITokenService tokenService, @@ -48,7 +49,8 @@ namespace Wino.Core ISignatureService signatureService, IDatabaseService databaseService, IMimeFileService mimeFileService, - IDefaultChangeProcessor defaultChangeProcessor) + IDefaultChangeProcessor defaultChangeProcessor, + IOutlookChangeProcessor outlookChangeProcessor) { _contactService = contactService; _notificationBuilder = notificationBuilder; @@ -60,6 +62,7 @@ namespace Wino.Core _databaseService = databaseService; _mimeFileService = mimeFileService; _defaultChangeProcessor = defaultChangeProcessor; + _outlookChangeProcessor = outlookChangeProcessor; } public IBaseSynchronizer GetAccountSynchronizer(Guid accountId) @@ -73,7 +76,7 @@ namespace Wino.Core { case Domain.Enums.MailProviderType.Outlook: var outlookAuthenticator = new OutlookAuthenticator(_tokenService, _nativeAppService); - return new OutlookSynchronizer(mailAccount, outlookAuthenticator, _defaultChangeProcessor); + return new OutlookSynchronizer(mailAccount, outlookAuthenticator, _outlookChangeProcessor); case Domain.Enums.MailProviderType.Gmail: var gmailAuthenticator = new GmailAuthenticator(_tokenService, _nativeAppService);