diff --git a/Wino.Core.Domain/Enums/MailListDisplayMode.cs b/Wino.Core.Domain/Enums/MailListDisplayMode.cs
index c1d04584..b25b45eb 100644
--- a/Wino.Core.Domain/Enums/MailListDisplayMode.cs
+++ b/Wino.Core.Domain/Enums/MailListDisplayMode.cs
@@ -1,4 +1,5 @@
-namespace Wino.Core.Domain.Enums;
+
+namespace Wino.Core.Domain.Enums;
public enum MailListDisplayMode
{
diff --git a/Wino.Core.Domain/Enums/SearchMode.cs b/Wino.Core.Domain/Enums/SearchMode.cs
new file mode 100644
index 00000000..f34f3f8a
--- /dev/null
+++ b/Wino.Core.Domain/Enums/SearchMode.cs
@@ -0,0 +1,6 @@
+namespace Wino.Core.Domain.Enums;
+public enum SearchMode
+{
+ Local,
+ Online
+}
diff --git a/Wino.Core.Domain/Interfaces/IImapSynchronizerStrategy.cs b/Wino.Core.Domain/Interfaces/IImapSynchronizerStrategy.cs
index be6e1218..67ca3ef0 100644
--- a/Wino.Core.Domain/Interfaces/IImapSynchronizerStrategy.cs
+++ b/Wino.Core.Domain/Interfaces/IImapSynchronizerStrategy.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
+using MailKit;
using MailKit.Net.Imap;
using Wino.Core.Domain.Entities.Mail;
@@ -17,5 +18,15 @@ public interface IImapSynchronizerStrategy
/// Cancellation token.
/// List of new downloaded message ids that don't exist locally.
Task> HandleSynchronizationAsync(IImapClient client, MailItemFolder folder, IImapSynchronizer synchronizer, CancellationToken cancellationToken = default);
+
+ ///
+ /// Downloads given set of messages from the folder.
+ /// Folder is expected to be opened and synchronizer is connected.
+ ///
+ /// Synchronizer that performs the action.
+ /// Remote folder to download messages from.
+ /// Set of message uniqueids.
+ /// Cancellation token.
+ Task DownloadMessagesAsync(IImapSynchronizer synchronizer, IMailFolder folder, UniqueIdSet uniqueIdSet, CancellationToken cancellationToken = default);
}
diff --git a/Wino.Core.Domain/Interfaces/IMailService.cs b/Wino.Core.Domain/Interfaces/IMailService.cs
index 854015e5..911640ef 100644
--- a/Wino.Core.Domain/Interfaces/IMailService.cs
+++ b/Wino.Core.Domain/Interfaces/IMailService.cs
@@ -13,6 +13,14 @@ public interface IMailService
{
Task GetSingleMailItemAsync(string mailCopyId, string remoteFolderId);
Task GetSingleMailItemAsync(Guid uniqueMailId);
+
+ ///
+ /// Returns the single mail item with the given mail copy id.
+ /// Caution: This method is not safe. Use other overrides.
+ ///
+ ///
+ ///
+ Task GetSingleMailItemAsync(string mailCopyId);
Task> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default);
///
@@ -117,4 +125,14 @@ public interface IMailService
///
///
Task> GetExistingMailsAsync(Guid folderId, IEnumerable uniqueIds);
+
+ ///
+ /// Creates a new mail from a package without doing any existence check.
+ /// Use it with caution.
+ ///
+ /// Account that mail belongs to.
+ /// Assigned folder.
+ /// Mail creation package.
+ ///
+ Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package);
}
diff --git a/Wino.Core.Domain/Interfaces/IPreferencesService.cs b/Wino.Core.Domain/Interfaces/IPreferencesService.cs
index 739518c0..14eef740 100644
--- a/Wino.Core.Domain/Interfaces/IPreferencesService.cs
+++ b/Wino.Core.Domain/Interfaces/IPreferencesService.cs
@@ -39,6 +39,17 @@ public interface IPreferencesService
///
bool Prefer24HourTimeFormat { get; set; }
+ ///
+ /// Diagnostic ID for the application.
+ /// Changes per-install.
+ ///
+ string DiagnosticId { get; set; }
+
+ ///
+ /// Setting: Defines the user's preference of default search mode in mail list.
+ /// Local search will still offer online search at the end of local search results.
+ ///
+ SearchMode DefaultSearchMode { get; set; }
#endregion
#region Mail
@@ -187,7 +198,7 @@ public interface IPreferencesService
DayOfWeek WorkingDayStart { get; set; }
DayOfWeek WorkingDayEnd { get; set; }
double HourHeight { get; set; }
- string DiagnosticId { get; set; }
+
CalendarSettings GetCurrentCalendarSettings();
diff --git a/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs b/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs
index e5a3d806..ba2384b5 100644
--- a/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs
+++ b/Wino.Core.Domain/Interfaces/IWinoSynchronizerBase.cs
@@ -1,6 +1,9 @@
-using System.Threading;
+using System.Collections.Generic;
+using System.Threading;
using System.Threading.Tasks;
using MailKit;
+using Wino.Core.Domain.Entities.Mail;
+using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
@@ -35,4 +38,13 @@ public interface IWinoSynchronizerBase : IBaseSynchronizer
/// 3. Dispose all resources.
///
Task KillSynchronizerAsync();
+
+ ///
+ /// Perform online search on the server.
+ ///
+ /// Search query.
+ /// Folders to include in search. All folders if null.
+ /// Cancellation token.
+ /// Search results after downloading missing mail copies from server.
+ Task> OnlineSearchAsync(string queryText, List folders, CancellationToken cancellationToken = default);
}
diff --git a/Wino.Core.Domain/Models/MailItem/MailListInitializationOptions.cs b/Wino.Core.Domain/Models/MailItem/MailListInitializationOptions.cs
index 5d3df2f4..033ec012 100644
--- a/Wino.Core.Domain/Models/MailItem/MailListInitializationOptions.cs
+++ b/Wino.Core.Domain/Models/MailItem/MailListInitializationOptions.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Folders;
@@ -11,4 +12,5 @@ public record MailListInitializationOptions(IEnumerable Folders
bool CreateThreads,
bool? IsFocusedOnly,
string SearchQuery,
- IEnumerable ExistingUniqueIds);
+ IEnumerable ExistingUniqueIds,
+ List PreFetchMailCopies = null);
diff --git a/Wino.Core.Domain/Models/Synchronization/OnlineSearchResult.cs b/Wino.Core.Domain/Models/Synchronization/OnlineSearchResult.cs
new file mode 100644
index 00000000..4dad6df8
--- /dev/null
+++ b/Wino.Core.Domain/Models/Synchronization/OnlineSearchResult.cs
@@ -0,0 +1,6 @@
+using System.Collections.Generic;
+using Wino.Core.Domain.Entities.Mail;
+
+namespace Wino.Core.Domain.Models.Synchronization;
+
+public record OnlineSearchResult(List SearchResult);
diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json
index d7902599..5ea22845 100644
--- a/Wino.Core.Domain/Translations/en_US/resources.json
+++ b/Wino.Core.Domain/Translations/en_US/resources.json
@@ -393,6 +393,9 @@
"Notifications_WinoUpdatedMessage": "Checkout new version {0}",
"Notifications_WinoUpdatedTitle": "Wino Mail has been updated.",
"Other": "Other",
+ "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.",
"PaneLengthOption_Default": "Default",
"PaneLengthOption_ExtraLarge": "Extra Large",
"PaneLengthOption_Large": "Large",
@@ -520,6 +523,10 @@
"SettingsAppPreferences_StartupBehavior_FatalError": "Fatal error occurred while changing the startup mode for Wino Mail.",
"SettingsAppPreferences_StartupBehavior_Enable": "Enable",
"SettingsAppPreferences_StartupBehavior_Disable": "Disable",
+ "SettingsAppPreferences_SearchMode_Title": "Default search mode",
+ "SettingsAppPreferences_SearchMode_Description": "Set whether Wino should check fetched mails first while doing a search or ask your mail server online. Local search is always faster and you can always do an online search if your mail is not in the results.",
+ "SettingsAppPreferences_SearchMode_Local": "Local",
+ "SettingsAppPreferences_SearchMode_Online": "Online",
"SettingsReorderAccounts_Title": "Reorder Accounts",
"SettingsReorderAccounts_Description": "Change the order of accounts in the account list.",
"SettingsManageLink_Description": "Move items to add new link or remove existing link.",
@@ -650,3 +657,4 @@
"QuickEventDialog_IsAllDay": "All day"
}
+
diff --git a/Wino.Core.UWP/Dialogs/NewAccountDialog.xaml b/Wino.Core.UWP/Dialogs/NewAccountDialog.xaml
index b53453f4..b6330156 100644
--- a/Wino.Core.UWP/Dialogs/NewAccountDialog.xaml
+++ b/Wino.Core.UWP/Dialogs/NewAccountDialog.xaml
@@ -72,6 +72,7 @@
SaveProperty(propertyName: nameof(DiagnosticId), value);
}
+ public SearchMode DefaultSearchMode
+ {
+ get => _configurationService.Get(nameof(DefaultSearchMode), SearchMode.Local);
+ set => SaveProperty(propertyName: nameof(DefaultSearchMode), value);
+ }
+
public DayOfWeek FirstDayOfWeek
{
get => _configurationService.Get(nameof(FirstDayOfWeek), DayOfWeek.Monday);
diff --git a/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs b/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs
index ea33ea64..918b1e60 100644
--- a/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs
+++ b/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs
@@ -1,7 +1,7 @@
using System.Text.Json.Serialization.Metadata;
-using MailKit;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Requests.Folder;
using Wino.Core.Requests.Mail;
@@ -45,7 +45,7 @@ public class ServerRequestTypeInfoResolver : DefaultJsonTypeInfoResolver
}
};
}
- else if (t.Type == typeof(IMailFolder))
+ else if (t.Type == typeof(IMailItemFolder))
{
t.PolymorphismOptions = new JsonPolymorphismOptions()
{
diff --git a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
index 672759ac..a11ae05d 100644
--- a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
+++ b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
@@ -38,9 +38,17 @@ public interface IDefaultChangeProcessor
Task> GetSynchronizationFoldersAsync(MailSynchronizationOptions options);
Task MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
Task UpdateFolderLastSyncDateAsync(Guid folderId);
- Task> GetExistingFoldersAsync(Guid accountId);
Task UpdateRemoteAliasInformationAsync(MailAccount account, List remoteAccountAliases);
+ ///
+ /// 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.
+ /// This is also used in online search to prevent duplicate mails.
+ ///
+ /// MailCopyId of the message.
+ /// Whether mail exists or not.
+ Task IsMailExistsAsync(string messageId);
+
// Calendar
Task> GetAccountCalendarsAsync(Guid accountId);
@@ -51,6 +59,8 @@ public interface IDefaultChangeProcessor
Task UpdateAccountCalendarAsync(AccountCalendar accountCalendar);
Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken);
+ Task GetMailCopyAsync(string mailCopyId);
+ Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package);
}
public interface IGmailChangeProcessor : IDefaultChangeProcessor
@@ -62,14 +72,6 @@ public interface IGmailChangeProcessor : IDefaultChangeProcessor
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);
-
///
/// Checks whether the mail exists in the folder.
/// When deciding Create or Update existing mail, we need to check if the mail exists in the folder.
@@ -141,22 +143,26 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
public Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged)
=> MailService.ChangeFlagStatusAsync(mailCopyId, isFlagged);
+ public Task IsMailExistsAsync(string messageId)
+ => MailService.IsMailExistsAsync(messageId);
+
+ public Task GetMailCopyAsync(string mailCopyId)
+ => MailService.GetSingleMailItemAsync(mailCopyId);
+
public Task ChangeMailReadStatusAsync(string mailCopyId, bool isRead)
=> MailService.ChangeReadStatusAsync(mailCopyId, isRead);
public Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId)
=> MailService.DeleteAssignmentAsync(accountId, mailCopyId, remoteFolderId);
-
-
public Task DeleteMailAsync(Guid accountId, string mailId)
=> MailService.DeleteMailAsync(accountId, mailId);
public Task CreateMailAsync(Guid accountId, NewMailItemPackage package)
=> MailService.CreateMailAsync(accountId, package);
- public Task> GetExistingFoldersAsync(Guid accountId)
- => FolderService.GetFoldersAsync(accountId);
+ public Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package)
+ => MailService.CreateMailRawAsync(account, mailItemFolder, package);
public Task MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId)
=> MailService.MapLocalDraftAsync(accountId, localDraftCopyUniqueId, newMailCopyId, newDraftId, newThreadId);
diff --git a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs
index a780aa2a..aaecf767 100644
--- a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs
+++ b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs
@@ -20,8 +20,6 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
, IOutlookChangeProcessor
{
- public Task IsMailExistsAsync(string messageId)
- => MailService.IsMailExistsAsync(messageId);
public Task IsMailExistsInFolderAsync(string messageId, Guid folderId)
=> MailService.IsMailExistsAsync(messageId, folderId);
diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs
index b87b6bc2..9ede3831 100644
--- a/Wino.Core/Synchronizers/GmailSynchronizer.cs
+++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs
@@ -24,6 +24,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
+using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Extensions;
@@ -903,6 +904,77 @@ public class GmailSynchronizer : WinoSynchronizer(networkCall, singleDraftRequest, singleDraftRequest)];
}
+ public override async Task> OnlineSearchAsync(string queryText, List folders, CancellationToken cancellationToken = default)
+ {
+ var request = _gmailService.Users.Messages.List("me");
+ request.Q = queryText;
+ request.MaxResults = 500; // Max 500 is returned.
+
+ string pageToken = null;
+
+ var messagesToDownload = new List();
+
+ do
+ {
+ if (folders?.Any() ?? false)
+ {
+ request.LabelIds = folders.Select(a => a.RemoteFolderId).ToList();
+ }
+
+ if (!string.IsNullOrEmpty(pageToken))
+ {
+ request.PageToken = pageToken;
+ }
+
+ var response = await request.ExecuteAsync(cancellationToken);
+ if (response.Messages == null) break;
+
+ // Handle skipping manually
+ foreach (var message in response.Messages)
+ {
+ messagesToDownload.Add(message);
+ }
+
+ pageToken = response.NextPageToken;
+ } while (!string.IsNullOrEmpty(pageToken));
+
+ // Do not download messages that exists, but return them for listing.
+
+ var messageIds = messagesToDownload.Select(a => a.Id).ToList();
+
+ List downloadRequireMessageIds = new();
+
+ foreach (var messageId in messageIds)
+ {
+ var exists = await _gmailChangeProcessor.IsMailExistsAsync(messageId).ConfigureAwait(false);
+
+ if (!exists)
+ {
+ downloadRequireMessageIds.Add(messageId);
+ }
+ }
+
+ // Download missing messages.
+ await BatchDownloadMessagesAsync(downloadRequireMessageIds, cancellationToken);
+
+ // Get results from database and return.
+
+ var searchResults = new List();
+
+ foreach (var messageId in messageIds)
+ {
+ var copy = await _gmailChangeProcessor.GetMailCopyAsync(messageId).ConfigureAwait(false);
+
+ if (copy == null) continue;
+
+ searchResults.Add(copy);
+ }
+
+ return searchResults;
+
+ // TODO: Return the search result ids.
+ }
+
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
ITransferProgress transferProgress = null,
CancellationToken cancellationToken = default)
diff --git a/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyBase.cs b/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyBase.cs
index 4e00d8d2..59ef6163 100644
--- a/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyBase.cs
+++ b/Wino.Core/Synchronizers/ImapSync/ImapSynchronizationStrategyBase.cs
@@ -83,38 +83,19 @@ public abstract class ImapSynchronizationStrategyBase : IImapSynchronizerStrateg
// Fetch the new mails in batch.
- var batchedMessageIds = newMessageIds.Batch(50);
+ var batchedMessageIds = newMessageIds.Batch(50).ToList();
+ var downloadTasks = new List();
+ // Create tasks for each batch.
foreach (var group in batchedMessageIds)
{
- var uniqueIdSet = new UniqueIdSet(group, SortOrder.Ascending);
-
- var summaries = await remoteFolder.FetchAsync(uniqueIdSet, MailSynchronizationFlags, cancellationToken).ConfigureAwait(false);
-
- foreach (var summary in summaries)
- {
- var mimeMessage = await remoteFolder.GetMessageAsync(summary.UniqueId, cancellationToken).ConfigureAwait(false);
-
- var creationPackage = new ImapMessageCreationPackage(summary, mimeMessage);
-
- var mailPackages = await synchronizer.CreateNewMailPackagesAsync(creationPackage, Folder, cancellationToken).ConfigureAwait(false);
-
- if (mailPackages != null)
- {
- foreach (var package in mailPackages)
- {
- // Local draft is mapped. We don't need to create a new mail copy.
- if (package == null) continue;
-
- bool isCreatedNew = await MailService.CreateMailAsync(Folder.MailAccountId, package).ConfigureAwait(false);
-
- // This is upsert. We are not interested in updated mails.
- if (isCreatedNew) downloadedMessageIds.Add(package.Copy.Id);
- }
- }
- }
+ downloadedMessageIds.AddRange(group.Select(a => MailkitClientExtensions.CreateUid(Folder.Id, a.Id)));
+ var task = DownloadMessagesAsync(synchronizer, remoteFolder, new UniqueIdSet(group), cancellationToken);
+ downloadTasks.Add(task);
}
+ // Wait for all batches to complete.
+ await Task.WhenAll(downloadTasks).ConfigureAwait(false);
return downloadedMessageIds;
}
@@ -183,4 +164,32 @@ public abstract class ImapSynchronizationStrategyBase : IImapSynchronizerStrateg
await HandleMessageDeletedAsync(deletedUids).ConfigureAwait(false);
}
}
+
+ public async Task DownloadMessagesAsync(IImapSynchronizer synchronizer,
+ IMailFolder folder,
+ UniqueIdSet uniqueIdSet,
+ CancellationToken cancellationToken = default)
+ {
+ var summaries = await folder.FetchAsync(uniqueIdSet, MailSynchronizationFlags, cancellationToken).ConfigureAwait(false);
+
+ foreach (var summary in summaries)
+ {
+ var mimeMessage = await folder.GetMessageAsync(summary.UniqueId, cancellationToken).ConfigureAwait(false);
+
+ var creationPackage = new ImapMessageCreationPackage(summary, mimeMessage);
+
+ var mailPackages = await synchronizer.CreateNewMailPackagesAsync(creationPackage, Folder, cancellationToken).ConfigureAwait(false);
+
+ if (mailPackages != null)
+ {
+ foreach (var package in mailPackages)
+ {
+ // Local draft is mapped. We don't need to create a new mail copy.
+ if (package == null) continue;
+
+ await MailService.CreateMailAsync(Folder.MailAccountId, package).ConfigureAwait(false);
+ }
+ }
+ }
+ }
}
diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs
index d7b0d784..788ff77a 100644
--- a/Wino.Core/Synchronizers/ImapSynchronizer.cs
+++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using MailKit;
using MailKit.Net.Imap;
+using MailKit.Search;
using MoreLinq;
using Serilog;
using Wino.Core.Domain.Entities.Mail;
@@ -16,6 +17,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Connectivity;
+using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Extensions;
@@ -628,6 +630,80 @@ public class ImapSynchronizer : WinoSynchronizer> OnlineSearchAsync(string queryText, List folders, CancellationToken cancellationToken = default)
+ {
+ IImapClient client = null;
+ IMailFolder activeFolder = null;
+
+ try
+ {
+ client = await _clientPool.GetClientAsync().ConfigureAwait(false);
+
+ var searchResults = new List();
+ List searchResultFolderMailUids = new();
+
+ foreach (var folder in folders)
+ {
+ var remoteFolder = await client.GetFolderAsync(folder.RemoteFolderId).ConfigureAwait(false);
+ await remoteFolder.OpenAsync(FolderAccess.ReadOnly, cancellationToken).ConfigureAwait(false);
+
+ // Look for subject and body.
+ var query = SearchQuery.BodyContains(queryText).Or(SearchQuery.SubjectContains(queryText));
+
+ var searchResultsInFolder = await remoteFolder.SearchAsync(query, cancellationToken).ConfigureAwait(false);
+ var nonExisttingUniqueIds = new List();
+
+ foreach (var searchResultId in searchResultsInFolder)
+ {
+ var folderMailUid = MailkitClientExtensions.CreateUid(folder.Id, searchResultId.Id);
+ searchResultFolderMailUids.Add(folderMailUid);
+
+ bool exists = await _imapChangeProcessor.IsMailExistsAsync(folderMailUid);
+
+ if (!exists)
+ {
+ nonExisttingUniqueIds.Add(searchResultId);
+ }
+ }
+
+ if (nonExisttingUniqueIds.Any())
+ {
+ var syncStrategy = _imapSynchronizationStrategyProvider.GetSynchronizationStrategy(client);
+ await syncStrategy.DownloadMessagesAsync(this, remoteFolder, new UniqueIdSet(nonExisttingUniqueIds, SortOrder.Ascending), cancellationToken).ConfigureAwait(false);
+ }
+
+ await remoteFolder.CloseAsync().ConfigureAwait(false);
+ }
+
+ foreach (var messageId in searchResultFolderMailUids)
+ {
+ var copy = await _imapChangeProcessor.GetMailCopyAsync(messageId).ConfigureAwait(false);
+
+ if (copy == null) continue;
+
+ searchResults.Add(copy);
+ }
+
+ return searchResults;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Failed to perform online imap search.");
+ throw;
+ }
+ finally
+ {
+ if (activeFolder?.IsOpen ?? false)
+ {
+ await activeFolder.CloseAsync().ConfigureAwait(false);
+ }
+
+ _clientPool.Release(client);
+ }
+
+ return new List();
+ }
+
private async Task> SynchronizeFolderInternalAsync(MailItemFolder folder, CancellationToken cancellationToken = default)
{
if (!folder.IsSynchronizationEnabled) return default;
diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
index 380a07e1..dfca959d 100644
--- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs
+++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
@@ -28,6 +28,7 @@ using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
+using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Extensions;
@@ -42,7 +43,7 @@ namespace Wino.Core.Synchronizers.Mail;
[JsonSerializable(typeof(Microsoft.Graph.Me.Messages.Item.Move.MovePostRequestBody))]
[JsonSerializable(typeof(OutlookFileAttachment))]
-public partial class OutlookSynchronizerJsonContext: JsonSerializerContext;
+public partial class OutlookSynchronizerJsonContext : JsonSerializerContext;
public class OutlookSynchronizer : WinoSynchronizer
{
@@ -187,6 +188,33 @@ public class OutlookSynchronizer : WinoSynchronizer
+ {
+ config.QueryParameters.Select = outlookMessageSelectParameters;
+ }, cancellationToken).ConfigureAwait(false);
+
+ var mailPackages = await CreateNewMailPackagesAsync(message, assignedFolder, cancellationToken).ConfigureAwait(false);
+
+ if (mailPackages == null) return;
+
+ foreach (var package in mailPackages)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ await _outlookChangeProcessor.CreateMailRawAsync(Account, assignedFolder, package).ConfigureAwait(false);
+ }
+ }
+
private async Task> SynchronizeFolderAsync(MailItemFolder folder, CancellationToken cancellationToken = default)
{
var downloadedMessageIds = new List();
@@ -927,6 +955,109 @@ public class OutlookSynchronizer : WinoSynchronizer> OnlineSearchAsync(string queryText, List folders, CancellationToken cancellationToken = default)
+ {
+ bool isFoldersIncluded = folders?.Any() ?? false;
+
+ var messagesToDownload = new List();
+
+ // Perform search for each folder separately.
+ if (isFoldersIncluded)
+ {
+ var folderIds = folders.Select(a => a.RemoteFolderId);
+
+ var tasks = folderIds.Select(async folderId =>
+ {
+ var mailQuery = _graphClient.Me.MailFolders[folderId].Messages
+ .GetAsync(requestConfig =>
+ {
+ requestConfig.QueryParameters.Search = $"\"{queryText}\"";
+ requestConfig.QueryParameters.Select = ["Id, ParentFolderId"];
+ requestConfig.QueryParameters.Top = 1000;
+ });
+
+ var result = await mailQuery;
+
+ if (result?.Value != null)
+ {
+ lock (messagesToDownload)
+ {
+ messagesToDownload.AddRange(result.Value);
+ }
+ }
+ });
+
+ await Task.WhenAll(tasks);
+ }
+ else
+ {
+ // Perform search for all messages without folder data.
+ var mailQuery = _graphClient.Me.Messages
+ .GetAsync(requestConfig =>
+ {
+ requestConfig.QueryParameters.Search = $"\"{queryText}\"";
+ requestConfig.QueryParameters.Select = ["Id, ParentFolderId"];
+ requestConfig.QueryParameters.Top = 1000;
+ });
+
+ var result = await mailQuery;
+
+ if (result?.Value != null)
+ {
+ lock (messagesToDownload)
+ {
+ messagesToDownload.AddRange(result.Value);
+ }
+ }
+ }
+
+ // Do not download messages that exists, but return them for listing.
+
+ var localFolders = await _outlookChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
+
+ var existingMessageIds = new List();
+
+ //Download missing messages.
+ foreach (var message in messagesToDownload)
+ {
+ var messageId = message.Id;
+ var parentFolderId = message.ParentFolderId;
+
+ if (!localFolders.Any(a => a.RemoteFolderId == parentFolderId))
+ {
+ Log.Warning($"Search result returned a message from a folder that is not synchronized.");
+ continue;
+ }
+
+ existingMessageIds.Add(messageId);
+
+ var exists = await _outlookChangeProcessor.IsMailExistsAsync(messageId).ConfigureAwait(false);
+
+ if (!exists)
+ {
+ // Check if folder exists. We can't download a mail without existing folder.
+
+ var localFolder = localFolders.Find(a => a.RemoteFolderId == parentFolderId);
+
+ await DownloadSearchResultMessageAsync(messageId, localFolder, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ // Get results from database and return.
+ var searchResults = new List();
+
+ foreach (var messageId in existingMessageIds)
+ {
+ var copy = await _outlookChangeProcessor.GetMailCopyAsync(messageId).ConfigureAwait(false);
+
+ if (copy == null) continue;
+
+ searchResults.Add(copy);
+ }
+
+ return searchResults;
+ }
+
private async Task DownloadMimeMessageAsync(string messageId, CancellationToken cancellationToken = default)
{
var mimeContentStream = await _graphClient.Me.Messages[messageId].Content.GetAsync(null, cancellationToken).ConfigureAwait(false);
@@ -935,7 +1066,6 @@ public class OutlookSynchronizer : WinoSynchronizer> CreateNewMailPackagesAsync(Message message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default)
{
-
var mimeMessage = await DownloadMimeMessageAsync(message.Id, cancellationToken).ConfigureAwait(false);
var mailCopy = message.AsMailCopy();
diff --git a/Wino.Core/Synchronizers/WinoSynchronizer.cs b/Wino.Core/Synchronizers/WinoSynchronizer.cs
index 73cb72ff..8bf7567f 100644
--- a/Wino.Core/Synchronizers/WinoSynchronizer.cs
+++ b/Wino.Core/Synchronizers/WinoSynchronizer.cs
@@ -14,6 +14,7 @@ using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Accounts;
+using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Requests.Bundles;
@@ -416,6 +417,16 @@ public abstract class WinoSynchronizerCancellation token.
public virtual Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress = null, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
+ ///
+ /// Performs an online search for the given query text in the given folders.
+ /// Downloads the missing messages from the server.
+ ///
+ /// Query to search for.
+ /// Which folders to include in.
+ /// Cancellation token.
+ ///
+ public virtual Task> OnlineSearchAsync(string queryText, List folders, CancellationToken cancellationToken = default) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
+
public List> CreateSingleTaskBundle(Func action, IRequestBase request, IUIChangeRequest uIChangeRequest)
{
return [new ImapRequestBundle(new ImapRequest(action, request), request, uIChangeRequest)];
diff --git a/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs b/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs
index 18132e0f..f80f0ee3 100644
--- a/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs
+++ b/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs
@@ -18,6 +18,10 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
[ObservableProperty]
private List _appTerminationBehavior;
+
+ [ObservableProperty]
+ public partial List SearchModes { get; set; }
+
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsStartupBehaviorDisabled))]
[NotifyPropertyChangedFor(nameof(IsStartupBehaviorEnabled))]
@@ -38,6 +42,18 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
}
}
+ private string _selectedDefaultSearchMode;
+ public string SelectedDefaultSearchMode
+ {
+ get => _selectedDefaultSearchMode;
+ set
+ {
+ SetProperty(ref _selectedDefaultSearchMode, value);
+
+ PreferencesService.DefaultSearchMode = (SearchMode)SearchModes.IndexOf(value);
+ }
+ }
+
private readonly IMailDialogService _dialogService;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
private readonly IStartupBehaviorService _startupBehaviorService;
@@ -61,7 +77,14 @@ public partial class AppPreferencesPageViewModel : MailBaseViewModel
Translator.SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title // "Terminate"
];
+ SearchModes =
+ [
+ Translator.SettingsAppPreferences_SearchMode_Local,
+ Translator.SettingsAppPreferences_SearchMode_Online
+ ];
+
SelectedAppTerminationBehavior = _appTerminationBehavior[(int)PreferencesService.ServerTerminationBehavior];
+ SelectedDefaultSearchMode = SearchModes[(int)PreferencesService.DefaultSearchMode];
}
[RelayCommand]
diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs
index 7d0907e6..08a6e602 100644
--- a/Wino.Mail.ViewModels/MailListPageViewModel.cs
+++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs
@@ -22,6 +22,7 @@ using Wino.Core.Domain.Models.Folders;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Menus;
using Wino.Core.Domain.Models.Reader;
+using Wino.Core.Domain.Models.Server;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Mail.ViewModels.Collections;
using Wino.Mail.ViewModels.Data;
@@ -71,12 +72,14 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public IThemeService ThemeService { get; }
private readonly IAccountService _accountService;
+ private readonly IMailDialogService _mailDialogService;
private readonly IMailService _mailService;
private readonly IFolderService _folderService;
private readonly IThreadingStrategyProvider _threadingStrategyProvider;
private readonly IContextMenuItemService _contextMenuItemService;
private readonly IWinoRequestDelegator _winoRequestDelegator;
private readonly IKeyPressService _keyPressService;
+ private readonly IWinoLogger _winoLogger;
private readonly IWinoServerConnectionManager _winoServerConnectionManager;
private MailItemViewModel _activeMailItem;
@@ -100,7 +103,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private bool isMultiSelectionModeEnabled;
[ObservableProperty]
- private string searchQuery;
+ public partial string SearchQuery { get; set; }
[ObservableProperty]
private FilterOption _selectedFilterOption;
@@ -109,7 +112,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Indicates state when folder is initializing. It can happen after folder navigation, search or filter change applied or loading more items.
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsEmpty))]
- [NotifyPropertyChangedFor(nameof(IsCriteriaFailed))]
[NotifyPropertyChangedFor(nameof(IsFolderEmpty))]
[NotifyPropertyChangedFor(nameof(IsProgressRing))]
private bool isInitializingFolder;
@@ -147,6 +149,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public MailListPageViewModel(IMailDialogService dialogService,
INavigationService navigationService,
IAccountService accountService,
+ IMailDialogService mailDialogService,
IMailService mailService,
IStatePersistanceService statePersistenceService,
IFolderService folderService,
@@ -156,14 +159,17 @@ public partial class MailListPageViewModel : MailBaseViewModel,
IKeyPressService keyPressService,
IPreferencesService preferencesService,
IThemeService themeService,
+ IWinoLogger winoLogger,
IWinoServerConnectionManager winoServerConnectionManager)
{
PreferencesService = preferencesService;
ThemeService = themeService;
+ _winoLogger = winoLogger;
_winoServerConnectionManager = winoServerConnectionManager;
StatePersistenceService = statePersistenceService;
NavigationService = navigationService;
_accountService = accountService;
+ _mailDialogService = mailDialogService;
_mailService = mailService;
_folderService = folderService;
_threadingStrategyProvider = threadingStrategyProvider;
@@ -254,6 +260,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
public bool IsArchiveSpecialFolder => ActiveFolder?.SpecialFolderType == SpecialFolderType.Archive;
public string SelectedMessageText => HasSelectedItems ? string.Format(Translator.MailsSelected, SelectedItemCount) : Translator.NoMailSelected;
+
///
/// Indicates current state of the mail list. Doesn't matter it's loading or no.
///
@@ -263,11 +270,21 @@ public partial class MailListPageViewModel : MailBaseViewModel,
/// Progress ring only should be visible when the folder is initializing and there are no items. We don't need to show it when there are items.
///
public bool IsProgressRing => IsInitializingFolder && IsEmpty;
- private bool isFilters => IsInSearchMode || SelectedFilterOption.Type != FilterOptionType.All;
- public bool IsCriteriaFailed => !IsInitializingFolder && IsEmpty && isFilters;
- public bool IsFolderEmpty => !IsInitializingFolder && IsEmpty && !isFilters;
+ public bool IsFolderEmpty => !IsInitializingFolder && IsEmpty;
- public bool IsInSearchMode { get; set; }
+ public bool HasNoOnlineSearchResult { get; private set; }
+
+ [ObservableProperty]
+ public partial bool IsInSearchMode { get; set; }
+
+ [ObservableProperty]
+ public partial bool IsOnlineSearchButtonVisible { get; set; }
+
+ [ObservableProperty]
+ public partial bool IsOnlineSearchEnabled { get; set; }
+
+ [ObservableProperty]
+ public partial bool AreSearchResultsOnline { get; set; }
#endregion
@@ -333,7 +350,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private void NotifyItemFoundState()
{
OnPropertyChanged(nameof(IsEmpty));
- OnPropertyChanged(nameof(IsCriteriaFailed));
OnPropertyChanged(nameof(IsFolderEmpty));
}
@@ -378,41 +394,52 @@ public partial class MailListPageViewModel : MailBaseViewModel,
private async Task UpdateFolderPivotsAsync()
{
+ if (ActiveFolder == null) return;
+
PivotFolders.Clear();
SelectedFolderPivot = null;
- if (ActiveFolder == null) return;
-
- // Merged folders don't support focused feature.
-
- if (ActiveFolder is IMergedAccountFolderMenuItem)
+ if (IsInSearchMode)
{
- PivotFolders.Add(new FolderPivotViewModel(ActiveFolder.FolderName, null));
+ var isFocused = SelectedFolderPivot?.IsFocused;
+
+ PivotFolders.Add(new FolderPivotViewModel(Translator.SearchPivotName, isFocused));
}
- else if (ActiveFolder is IFolderMenuItem singleFolderMenuItem)
+ else
{
- var parentAccount = singleFolderMenuItem.ParentAccount;
+ // Merged folders don't support focused feature.
- bool isFocusedInboxEnabled = await _accountService.IsAccountFocusedEnabledAsync(parentAccount.Id);
- bool isInboxFolder = ActiveFolder.SpecialFolderType == SpecialFolderType.Inbox;
-
- // Folder supports Focused - Other
- if (isInboxFolder && isFocusedInboxEnabled)
+ if (ActiveFolder is IMergedAccountFolderMenuItem)
{
- // Can be passed as empty string. Focused - Other will be used regardless.
- var focusedItem = new FolderPivotViewModel(string.Empty, true);
- var otherItem = new FolderPivotViewModel(string.Empty, false);
-
- PivotFolders.Add(focusedItem);
- PivotFolders.Add(otherItem);
+ PivotFolders.Add(new FolderPivotViewModel(ActiveFolder.FolderName, null));
}
- else
+ else if (ActiveFolder is IFolderMenuItem singleFolderMenuItem)
{
- // If the account and folder doesn't support focused feature, just add itself.
- PivotFolders.Add(new FolderPivotViewModel(singleFolderMenuItem.FolderName, null));
+ var parentAccount = singleFolderMenuItem.ParentAccount;
+
+ bool isFocusedInboxEnabled = await _accountService.IsAccountFocusedEnabledAsync(parentAccount.Id);
+ bool isInboxFolder = ActiveFolder.SpecialFolderType == SpecialFolderType.Inbox;
+
+ // Folder supports Focused - Other
+ if (isInboxFolder && isFocusedInboxEnabled)
+ {
+ // Can be passed as empty string. Focused - Other will be used regardless.
+ var focusedItem = new FolderPivotViewModel(string.Empty, true);
+ var otherItem = new FolderPivotViewModel(string.Empty, false);
+
+ PivotFolders.Add(focusedItem);
+ PivotFolders.Add(otherItem);
+ }
+ else
+ {
+ // If the account and folder doesn't support focused feature, just add itself.
+ PivotFolders.Add(new FolderPivotViewModel(singleFolderMenuItem.FolderName, null));
+ }
}
}
+
+
// This will trigger refresh.
SelectedFolderPivot = PivotFolders.FirstOrDefault();
}
@@ -512,33 +539,16 @@ public partial class MailListPageViewModel : MailBaseViewModel,
[RelayCommand]
public async Task PerformSearchAsync()
{
- if (string.IsNullOrEmpty(SearchQuery) && IsInSearchMode)
+ IsOnlineSearchEnabled = false;
+ AreSearchResultsOnline = false;
+ IsInSearchMode = !string.IsNullOrEmpty(SearchQuery);
+
+ if (IsInSearchMode)
{
- await UpdateFolderPivotsAsync();
- IsInSearchMode = false;
- await InitializeFolderAsync();
+ IsOnlineSearchButtonVisible = false;
}
- if (!string.IsNullOrEmpty(SearchQuery))
- {
-
- IsInSearchMode = true;
- CreateSearchPivot();
- }
-
- void CreateSearchPivot()
- {
- PivotFolders.Clear();
- var isFocused = SelectedFolderPivot?.IsFocused;
- SelectedFolderPivot = null;
-
- if (ActiveFolder == null) return;
-
- PivotFolders.Add(new FolderPivotViewModel(Translator.SearchPivotName, isFocused));
-
- // This will trigger refresh.
- SelectedFolderPivot = PivotFolders.FirstOrDefault();
- }
+ await UpdateFolderPivotsAsync();
}
[RelayCommand]
@@ -555,7 +565,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
[RelayCommand]
private async Task LoadMoreItemsAsync()
{
- if (IsInitializingFolder) return;
+ if (IsInitializingFolder || IsOnlineSearchEnabled) return;
await ExecuteUIThread(() => { IsInitializingFolder = true; });
@@ -763,6 +773,15 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}
}
+ [RelayCommand]
+ private async Task PerformOnlineSearchAsync()
+ {
+ IsOnlineSearchButtonVisible = false;
+ IsOnlineSearchEnabled = true;
+
+ await InitializeFolderAsync();
+ }
+
private async Task InitializeFolderAsync()
{
if (SelectedFilterOption == null || SelectedFolderPivot == null || SelectedSortingOption == null)
@@ -783,14 +802,6 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Folder is changed during initialization.
// Just cancel the existing one and wait for new initialization.
- //if (listManipulationSemepahore.CurrentCount == 0)
- //{
- // Debug.WriteLine("Canceling initialization of mails.");
-
- // listManipulationCancellationTokenSource.Cancel();
- // listManipulationCancellationTokenSource.Token.ThrowIfCancellationRequested();
- //}
-
if (!listManipulationCancellationTokenSource.IsCancellationRequested)
{
listManipulationCancellationTokenSource.Cancel();
@@ -812,15 +823,77 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Here items are sorted and filtered.
+ List items = null;
+ List onlineSearchItems = null;
+
+ bool isDoingSearch = !string.IsNullOrEmpty(SearchQuery);
+ bool isDoingOnlineSearch = false;
+
+ if (isDoingSearch)
+ {
+ isDoingOnlineSearch = PreferencesService.DefaultSearchMode == SearchMode.Online || IsOnlineSearchEnabled;
+
+ // Perform online search.
+ if (isDoingOnlineSearch)
+ {
+ WinoServerResponse onlineSearchResult = null;
+ string onlineSearchFailedMessage = null;
+
+ try
+ {
+ var accountIds = ActiveFolder.HandlingFolders.Select(a => a.MailAccountId).ToList();
+ var folders = ActiveFolder.HandlingFolders.ToList();
+ var searchRequest = new OnlineSearchRequested(accountIds, SearchQuery, folders);
+
+ onlineSearchResult = await _winoServerConnectionManager.GetResponseAsync(searchRequest, cancellationToken);
+
+ if (onlineSearchResult.IsSuccess)
+ {
+ await ExecuteUIThread(() => { AreSearchResultsOnline = true; });
+
+ onlineSearchItems = onlineSearchResult.Data.SearchResult;
+ }
+ else
+ {
+ onlineSearchFailedMessage = onlineSearchResult.Message;
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ Log.Warning(ex, "Failed to perform online search.");
+ onlineSearchFailedMessage = ex.Message;
+ }
+
+ if (onlineSearchResult != null && !onlineSearchResult.IsSuccess)
+ {
+ // Query or server error.
+ var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, onlineSearchResult.Message);
+ _mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning);
+
+ }
+ else if (!string.IsNullOrEmpty(onlineSearchFailedMessage))
+ {
+ // Fatal error.
+ var serverErrorMessage = string.Format(Translator.OnlineSearchFailed_Message, onlineSearchFailedMessage);
+ _mailDialogService.InfoBarMessage(Translator.GeneralTitle_Error, serverErrorMessage, InfoBarMessageType.Warning);
+ }
+ }
+ }
+
var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
SelectedFilterOption.Type,
SelectedSortingOption.Type,
PreferencesService.IsThreadingEnabled,
SelectedFolderPivot.IsFocused,
SearchQuery,
- MailCollection.MailCopyIdHashSet);
+ MailCollection.MailCopyIdHashSet,
+ onlineSearchItems);
- var items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
+ items = await _mailService.FetchMailsAsync(initializationOptions, cancellationToken).ConfigureAwait(false);
if (!listManipulationCancellationTokenSource.IsCancellationRequested)
{
@@ -830,7 +903,15 @@ public partial class MailListPageViewModel : MailBaseViewModel,
var viewModels = PrepareMailViewModels(items);
- await ExecuteUIThread(() => { MailCollection.AddRange(viewModels, true); });
+ await ExecuteUIThread(() =>
+ {
+ MailCollection.AddRange(viewModels, true);
+
+ if (isDoingSearch && !isDoingOnlineSearch)
+ {
+ IsOnlineSearchButtonVisible = true;
+ }
+ });
}
}
catch (OperationCanceledException)
@@ -887,6 +968,10 @@ public partial class MailListPageViewModel : MailBaseViewModel,
// Notify change for archive-unarchive app bar button.
OnPropertyChanged(nameof(IsArchiveSpecialFolder));
+ IsInSearchMode = false;
+ IsOnlineSearchButtonVisible = false;
+ AreSearchResultsOnline = false;
+
// Prepare Focused - Other or folder name tabs.
await UpdateFolderPivotsAsync();
@@ -918,6 +1003,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
SelectedSortingOption = SortingOptions[0];
SearchQuery = string.Empty;
IsInSearchMode = false;
+ IsOnlineSearchEnabled = false;
}
}
diff --git a/Wino.Mail/Views/MailListPage.xaml b/Wino.Mail/Views/MailListPage.xaml
index 1cb58a74..2e48b829 100644
--- a/Wino.Mail/Views/MailListPage.xaml
+++ b/Wino.Mail/Views/MailListPage.xaml
@@ -3,6 +3,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:abstract="using:Wino.Views.Abstract"
+ xmlns:animatedvisuals="using:Microsoft.UI.Xaml.Controls.AnimatedVisuals"
xmlns:collections="using:CommunityToolkit.Mvvm.Collections"
xmlns:controls="using:Wino.Controls"
xmlns:converters="using:Wino.Converters"
@@ -292,27 +293,38 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+ Visibility="{x:Bind ViewModel.IsCriteriaFailed, Mode=OneWay}" />-->
+ Canvas.ZIndex="2000" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Wino.Messages/Server/OnlineSearchRequested.cs b/Wino.Messages/Server/OnlineSearchRequested.cs
new file mode 100644
index 00000000..2b731485
--- /dev/null
+++ b/Wino.Messages/Server/OnlineSearchRequested.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Folders;
+
+namespace Wino.Messaging.Server;
+
+///
+/// Raised when user performs search on the search bar.
+///
+/// Accounts that performs the query. Multiple accounts for linked accounts.
+/// Search query.
+/// Folders to include in search. All folders if null.
+public record OnlineSearchRequested(List AccountIds, string QueryText, List Folders) : IClientMessage;
diff --git a/Wino.Server/Core/ServerMessageHandlerFactory.cs b/Wino.Server/Core/ServerMessageHandlerFactory.cs
index e92d3711..abbb1e79 100644
--- a/Wino.Server/Core/ServerMessageHandlerFactory.cs
+++ b/Wino.Server/Core/ServerMessageHandlerFactory.cs
@@ -23,6 +23,7 @@ public class ServerMessageHandlerFactory : IServerMessageHandlerFactory
nameof(TerminateServerRequested) => App.Current.Services.GetService(),
nameof(ImapConnectivityTestRequested) => App.Current.Services.GetService(),
nameof(KillAccountSynchronizerRequested) => App.Current.Services.GetService(),
+ nameof(OnlineSearchRequested) => App.Current.Services.GetService(),
_ => throw new Exception($"Server handler for {typeName} is not registered."),
};
}
@@ -41,5 +42,6 @@ public class ServerMessageHandlerFactory : IServerMessageHandlerFactory
serviceCollection.AddTransient();
serviceCollection.AddTransient();
serviceCollection.AddTransient();
+ serviceCollection.AddTransient();
}
}
diff --git a/Wino.Server/MessageHandlers/MailSynchronizationRequestHandler.cs b/Wino.Server/MessageHandlers/MailSynchronizationRequestHandler.cs
index 47134a09..add369d5 100644
--- a/Wino.Server/MessageHandlers/MailSynchronizationRequestHandler.cs
+++ b/Wino.Server/MessageHandlers/MailSynchronizationRequestHandler.cs
@@ -51,6 +51,14 @@ public class MailSynchronizationRequestHandler : ServerMessageHandler.CreateSuccessResponse(MailSynchronizationResult.Canceled);
+ //}
+
var synchronizationResult = await synchronizer.SynchronizeMailsAsync(message.Options, cancellationToken).ConfigureAwait(false);
if (synchronizationResult.DownloadedMessages?.Any() ?? false || !synchronizer.Account.Preferences.IsNotificationsEnabled)
diff --git a/Wino.Server/MessageHandlers/OnlineSearchRequestHandler.cs b/Wino.Server/MessageHandlers/OnlineSearchRequestHandler.cs
new file mode 100644
index 00000000..6ba4ca7b
--- /dev/null
+++ b/Wino.Server/MessageHandlers/OnlineSearchRequestHandler.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Server;
+using Wino.Core.Domain.Models.Synchronization;
+using Wino.Messaging.Server;
+using Wino.Server.Core;
+
+namespace Wino.Server.MessageHandlers;
+
+public class OnlineSearchRequestHandler : ServerMessageHandler
+{
+ private readonly ISynchronizerFactory _synchronizerFactory;
+
+ public OnlineSearchRequestHandler(ISynchronizerFactory synchronizerFactory)
+ {
+ _synchronizerFactory = synchronizerFactory;
+ }
+
+ public override WinoServerResponse FailureDefaultResponse(Exception ex)
+ => WinoServerResponse.CreateErrorResponse(ex.Message);
+
+ protected override async Task> HandleAsync(OnlineSearchRequested message, CancellationToken cancellationToken = default)
+ {
+ List synchronizers = new();
+
+ foreach (var accountId in message.AccountIds)
+ {
+ var synchronizer = await _synchronizerFactory.GetAccountSynchronizerAsync(accountId);
+ synchronizers.Add(synchronizer);
+ }
+
+ var tasks = synchronizers.Select(s => s.OnlineSearchAsync(message.QueryText, message.Folders, cancellationToken)).ToList();
+ var results = await Task.WhenAll(tasks);
+
+ // Flatten the results from all synchronizers into a single list
+ var allResults = results.SelectMany(x => x).ToList();
+
+ return WinoServerResponse.CreateSuccessResponse(new OnlineSearchResult(allResults));
+ }
+}
diff --git a/Wino.Server/ServerContext.cs b/Wino.Server/ServerContext.cs
index 98eb31bc..46dd634d 100644
--- a/Wino.Server/ServerContext.cs
+++ b/Wino.Server/ServerContext.cs
@@ -43,7 +43,8 @@ public class ServerContext :
IRecipient,
IRecipient,
IRecipient,
- IRecipient
+ IRecipient,
+ IRecipient
{
private readonly System.Timers.Timer _timer;
private static object connectionLock = new object();
@@ -141,8 +142,11 @@ public class ServerContext :
public async void Receive(AccountFolderConfigurationUpdated message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(CopyAuthURLRequested message) => await SendMessageAsync(MessageType.UIMessage, message);
+
public async void Receive(NewMailSynchronizationRequested message) => await SendMessageAsync(MessageType.UIMessage, message);
+ public async void Receive(OnlineSearchRequested message) => await SendMessageAsync(MessageType.UIMessage, message);
+
#endregion
private string GetAppPackagFamilyName()
@@ -326,6 +330,9 @@ public class ServerContext :
case nameof(KillAccountSynchronizerRequested):
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize(messageJson, _jsonSerializerOptions));
break;
+ case nameof(OnlineSearchRequested):
+ await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize(messageJson, _jsonSerializerOptions));
+ break;
default:
Debug.WriteLine($"Missing handler for {typeName} in the server. Check ServerContext.cs - HandleServerMessageAsync.");
break;
diff --git a/Wino.Services/MailService.cs b/Wino.Services/MailService.cs
index c497f633..983fc8a4 100644
--- a/Wino.Services/MailService.cs
+++ b/Wino.Services/MailService.cs
@@ -195,9 +195,20 @@ public class MailService : BaseDatabaseService, IMailService
public async Task> FetchMailsAsync(MailListInitializationOptions options, CancellationToken cancellationToken = default)
{
- var query = BuildMailFetchQuery(options);
+ List mails = null;
- var mails = await Connection.QueryAsync(query);
+ // If user performs an online search, mail copies are passed to options.
+ if (options.PreFetchMailCopies != null)
+ {
+ mails = options.PreFetchMailCopies;
+ }
+ else
+ {
+ // If not just do the query.
+ var query = BuildMailFetchQuery(options);
+
+ mails = await Connection.QueryAsync(query);
+ }
Dictionary folderCache = [];
Dictionary accountCache = [];
@@ -406,6 +417,27 @@ public class MailService : BaseDatabaseService, IMailService
return mailCopy;
}
+ ///
+ /// Using this override is dangerous.
+ /// Gmail stores multiple copies of same mail in different folders.
+ /// This one will always return the first one. Use with caution.
+ ///
+ /// Mail copy id.
+ public async Task GetSingleMailItemAsync(string mailCopyId)
+ {
+ var query = new Query("MailCopy")
+ .Where("MailCopy.Id", mailCopyId)
+ .SelectRaw("MailCopy.*")
+ .GetRawQuery();
+
+ var mailCopy = await Connection.FindWithQueryAsync(query);
+ if (mailCopy == null) return null;
+
+ await LoadAssignedPropertiesAsync(mailCopy).ConfigureAwait(false);
+
+ return mailCopy;
+ }
+
public async Task GetSingleMailItemAsync(string mailCopyId, string remoteFolderId)
{
var query = new Query("MailCopy")
@@ -634,6 +666,24 @@ public class MailService : BaseDatabaseService, IMailService
await DeleteMailInternalAsync(mailItem, preserveMimeFile: false).ConfigureAwait(false);
}
+ public async Task CreateMailRawAsync(MailAccount account, MailItemFolder mailItemFolder, NewMailItemPackage package)
+ {
+ var mailCopy = package.Copy;
+ var mimeMessage = package.Mime;
+
+ mailCopy.UniqueId = Guid.NewGuid();
+ mailCopy.AssignedAccount = account;
+ mailCopy.AssignedFolder = mailItemFolder;
+ mailCopy.SenderContact = await GetSenderContactForAccountAsync(account, mailCopy.FromAddress).ConfigureAwait(false);
+ mailCopy.FolderId = mailItemFolder.Id;
+
+ var mimeSaveTask = _mimeFileService.SaveMimeMessageAsync(mailCopy.FileId, mimeMessage, account.Id);
+ var contactSaveTask = _contactService.SaveAddressInformationAsync(mimeMessage);
+ var insertMailTask = InsertMailAsync(mailCopy);
+
+ await Task.WhenAll(mimeSaveTask, contactSaveTask, insertMailTask).ConfigureAwait(false);
+ }
+
public async Task CreateMailAsync(Guid accountId, NewMailItemPackage package)
{
var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);