diff --git a/Wino.BackgroundTasks/SessionConnectedTask.cs b/Wino.BackgroundTasks/SessionConnectedTask.cs index 90db6769..cf583956 100644 --- a/Wino.BackgroundTasks/SessionConnectedTask.cs +++ b/Wino.BackgroundTasks/SessionConnectedTask.cs @@ -1,13 +1,6 @@ using System; -using Microsoft.Extensions.DependencyInjection; -using Serilog; +using Windows.ApplicationModel; using Windows.ApplicationModel.Background; -using Windows.Storage; -using Wino.Core; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Services; -using Wino.Core.UWP; -using Wino.Services; namespace Wino.BackgroundTasks { @@ -17,32 +10,10 @@ namespace Wino.BackgroundTasks { var def = taskInstance.GetDeferral(); - try - { - var services = new ServiceCollection(); + // Run server on session connected by launching the Full Thrust process. + await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync(); - services.RegisterCoreServices(); - services.RegisterCoreUWPServices(); - - var providere = services.BuildServiceProvider(); - - var backgroundTaskService = providere.GetService(); - var dbService = providere.GetService(); - var logInitializer = providere.GetService(); - - logInitializer.SetupLogger(ApplicationData.Current.LocalFolder.Path); - - await dbService.InitializeAsync(); - await backgroundTaskService.RunBackgroundSynchronizationAsync(Core.Domain.Enums.BackgroundSynchronizationReason.SessionConnected); - } - catch (Exception ex) - { - Log.Error(ex, "Background synchronization failed from background task."); - } - finally - { - def.Complete(); - } + def.Complete(); } } } diff --git a/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj b/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj index 94ab7119..7f5790f8 100644 --- a/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj +++ b/Wino.BackgroundTasks/Wino.BackgroundTasks.csproj @@ -18,25 +18,6 @@ {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} false - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE;NETFX_CORE;WINDOWS_UWP - prompt - 4 - x86 true diff --git a/Wino.Core.Domain/Constants.cs b/Wino.Core.Domain/Constants.cs index 1123fbb8..36aaac94 100644 --- a/Wino.Core.Domain/Constants.cs +++ b/Wino.Core.Domain/Constants.cs @@ -8,8 +8,10 @@ public const string WinoLocalDraftHeader = "X-Wino-Draft-Id"; public const string LocalDraftStartPrefix = "localDraft_"; - public const string ToastMailItemIdKey = nameof(ToastMailItemIdKey); - public const string ToastMailItemRemoteFolderIdKey = nameof(ToastMailItemRemoteFolderIdKey); + public const string ToastMailUniqueIdKey = nameof(ToastMailUniqueIdKey); public const string ToastActionKey = nameof(ToastActionKey); + + public const string ClientLogFile = "Client_.log"; + public const string ServerLogFile = "Server_.log"; } } diff --git a/Wino.Core.Domain/Entities/TokenInformation.cs b/Wino.Core.Domain/Entities/TokenInformation.cs index 88bf292c..af86de02 100644 --- a/Wino.Core.Domain/Entities/TokenInformation.cs +++ b/Wino.Core.Domain/Entities/TokenInformation.cs @@ -11,6 +11,10 @@ namespace Wino.Core.Domain.Entities public Guid AccountId { get; set; } + /// + /// Unique object storage for authenticators if needed. + /// + public string UniqueId { get; set; } public string Address { get; set; } public void RefreshTokens(TokenInformationBase tokenInformationBase) diff --git a/Wino.Core.Domain/Enums/ServerBackgroundMode.cs b/Wino.Core.Domain/Enums/ServerBackgroundMode.cs new file mode 100644 index 00000000..835a4776 --- /dev/null +++ b/Wino.Core.Domain/Enums/ServerBackgroundMode.cs @@ -0,0 +1,12 @@ +namespace Wino.Core.Domain.Enums +{ + /// + /// What should happen to server app when the client is terminated. + /// + public enum ServerBackgroundMode + { + MinimizedTray, // Still runs, tray icon is visible. + Invisible, // Still runs, tray icon is invisible. + Terminate // Server is terminated as Wino terminates. + } +} diff --git a/Wino.Core.Domain/Enums/StartupBehaviorResult.cs b/Wino.Core.Domain/Enums/StartupBehaviorResult.cs new file mode 100644 index 00000000..912f0ecc --- /dev/null +++ b/Wino.Core.Domain/Enums/StartupBehaviorResult.cs @@ -0,0 +1,11 @@ +namespace Wino.Core.Domain.Enums +{ + public enum StartupBehaviorResult + { + Enabled, + Disabled, + DisabledByUser, + DisabledByPolicy, + Fatal + } +} diff --git a/Wino.Core.Domain/Enums/SynchronizationSource.cs b/Wino.Core.Domain/Enums/SynchronizationSource.cs new file mode 100644 index 00000000..f64b00e5 --- /dev/null +++ b/Wino.Core.Domain/Enums/SynchronizationSource.cs @@ -0,0 +1,12 @@ +namespace Wino.Core.Domain.Enums +{ + /// + /// Enumeration for the source of synchronization. + /// Right now it can either be from the client or the server. + /// + public enum SynchronizationSource + { + Client, + Server + } +} diff --git a/Wino.Core.Domain/Enums/WinoPage.cs b/Wino.Core.Domain/Enums/WinoPage.cs index e3326ac3..2a7264a2 100644 --- a/Wino.Core.Domain/Enums/WinoPage.cs +++ b/Wino.Core.Domain/Enums/WinoPage.cs @@ -21,6 +21,7 @@ MailListPage, ReadComposePanePage, LanguageTimePage, + AppPreferencesPage, SettingOptionsPage, } } diff --git a/Wino.Core.Domain/Enums/WinoServerConnectionStatus.cs b/Wino.Core.Domain/Enums/WinoServerConnectionStatus.cs new file mode 100644 index 00000000..e997ce4b --- /dev/null +++ b/Wino.Core.Domain/Enums/WinoServerConnectionStatus.cs @@ -0,0 +1,11 @@ +namespace Wino.Core.Domain.Enums +{ + public enum WinoServerConnectionStatus + { + None, + Connecting, + Connected, + Disconnected, + Failed + } +} diff --git a/Wino.Core.Domain/Exceptions/BackgroundTaskExecutionRequestDeniedException.cs b/Wino.Core.Domain/Exceptions/BackgroundTaskExecutionRequestDeniedException.cs deleted file mode 100644 index 532addbd..00000000 --- a/Wino.Core.Domain/Exceptions/BackgroundTaskExecutionRequestDeniedException.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace Wino.Core.Domain.Exceptions -{ - /// - /// An exception thrown when the background task execution policies are denied for some reason. - /// - public class BackgroundTaskExecutionRequestDeniedException : Exception { } -} diff --git a/Wino.Core.Domain/Exceptions/WinoServerException.cs b/Wino.Core.Domain/Exceptions/WinoServerException.cs new file mode 100644 index 00000000..8cbf87d4 --- /dev/null +++ b/Wino.Core.Domain/Exceptions/WinoServerException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Wino.Core.Domain.Exceptions +{ + /// + /// All server crash types. Wino Server ideally should not throw anything else than this Exception type. + /// + public class WinoServerException : Exception + { + public WinoServerException(string message) : base(message) { } + } +} diff --git a/Wino.Core.Domain/Extensions/MimeExtensions.cs b/Wino.Core.Domain/Extensions/MimeExtensions.cs new file mode 100644 index 00000000..31f918b4 --- /dev/null +++ b/Wino.Core.Domain/Extensions/MimeExtensions.cs @@ -0,0 +1,20 @@ +using System; + +namespace Wino.Core.Domain.Extensions +{ + public static class MimeExtensions + { + public static string GetBase64MimeMessage(this MimeKit.MimeMessage message) + { + using System.IO.MemoryStream memoryStream = new(); + message.WriteTo(MimeKit.FormatOptions.Default, memoryStream); + byte[] buffer = memoryStream.GetBuffer(); + int count = (int)memoryStream.Length; + + return Convert.ToBase64String(buffer); + } + + public static MimeKit.MimeMessage GetMimeMessageFromBase64(this string base64) + => MimeKit.MimeMessage.Load(new System.IO.MemoryStream(Convert.FromBase64String(base64))); + } +} diff --git a/Wino.Core.Domain/Interfaces/IAppInitializerService.cs b/Wino.Core.Domain/Interfaces/IAppInitializerService.cs deleted file mode 100644 index 0ff1fe76..00000000 --- a/Wino.Core.Domain/Interfaces/IAppInitializerService.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Threading.Tasks; - -namespace Wino.Core.Domain.Interfaces -{ - public interface IAppInitializerService - { - string GetApplicationDataFolder(); - string GetPublisherSharedFolder(); - - Task MigrateAsync(); - } -} diff --git a/Wino.Core.Domain/Interfaces/IApplicationConfiguration.cs b/Wino.Core.Domain/Interfaces/IApplicationConfiguration.cs new file mode 100644 index 00000000..4a9aca60 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IApplicationConfiguration.cs @@ -0,0 +1,21 @@ +namespace Wino.Core.Domain.Interfaces +{ + /// + /// Singleton object that holds the application data folder path and the publisher shared folder path. + /// Load the values before calling any service. + /// App data folder is used for storing files. + /// Pubhlisher cache folder is only used for database file so other apps can access it in the same package by same publisher. + /// + public interface IApplicationConfiguration + { + /// + /// Application data folder. + /// + string ApplicationDataFolderPath { get; set; } + + /// + /// Publisher shared folder path. + /// + string PublisherSharedFolderPath { get; set; } + } +} diff --git a/Wino.Core.Domain/Interfaces/IAuthenticator.cs b/Wino.Core.Domain/Interfaces/IAuthenticator.cs index ced495d6..eb85b812 100644 --- a/Wino.Core.Domain/Interfaces/IAuthenticator.cs +++ b/Wino.Core.Domain/Interfaces/IAuthenticator.cs @@ -1,5 +1,4 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; @@ -23,28 +22,12 @@ namespace Wino.Core.Domain.Interfaces /// /// Initial creation of token. Requires user interaction. - /// This will save token into database, but still returns for account creation + /// This will cache the token but still returns for account creation /// since account address is required. /// - /// Token cache might ask for regeneration of token for specific - /// account address. If one is provided and re-generation native token doesn't belong to this address - /// token saving to database won't happen. /// Freshly created TokenInformation.. Task GenerateTokenAsync(MailAccount account, bool saveToken); - /// - /// Required for external authorization on launched browser to continue. - /// Used for Gmail. - /// - /// Response's redirect uri. - void ContinueAuthorization(Uri authorizationResponseUri); - - /// - /// For external browser required authentications. - /// Canceling Gmail authentication dialog etc. - /// - void CancelAuthorization(); - /// /// ClientId in case of needed for authorization/authentication. /// diff --git a/Wino.Core.Domain/Interfaces/IAuthenticatorTypes.cs b/Wino.Core.Domain/Interfaces/IAuthenticatorTypes.cs new file mode 100644 index 00000000..51f1fd80 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IAuthenticatorTypes.cs @@ -0,0 +1,6 @@ +namespace Wino.Core.Domain.Interfaces +{ + public interface IOutlookAuthenticator : IAuthenticator { } + public interface IGmailAuthenticator : IAuthenticator { } + public interface IImapAuthenticator : IAuthenticator { } +} diff --git a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs new file mode 100644 index 00000000..21ca1f8a --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs @@ -0,0 +1,54 @@ +using System.Threading; +using System.Threading.Tasks; +using MailKit; +using Wino.Core.Domain.Entities; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Domain.Models.Synchronization; + +namespace Wino.Core.Domain.Interfaces +{ + public interface IBaseSynchronizer + { + /// + /// Account that is assigned for this synchronizer. + /// + MailAccount Account { get; } + + /// + /// Synchronizer state. + /// + AccountSynchronizerState State { get; } + + /// + /// Queues a single request to be executed in the next synchronization. + /// + /// Request to queue. + void QueueRequest(IRequestBase request); + + /// + /// TODO + /// + /// Whether active synchronization is stopped or not. + bool CancelActiveSynchronization(); + + /// + /// Performs a full synchronization with the server with given options. + /// This will also prepares batch requests for execution. + /// Requests are executed in the order they are queued and happens before the synchronization. + /// Result of the execution queue is processed during the synchronization. + /// + /// Options for synchronization. + /// Cancellation token. + /// Result summary of synchronization. + Task SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default); + + /// + /// Downloads a single MIME message from the server and saves it to disk. + /// + /// Mail item to download from server. + /// Optional progress reporting for download operation. + /// Cancellation token. + Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default); + } +} diff --git a/Wino.Core.Domain/Interfaces/IClientMessage.cs b/Wino.Core.Domain/Interfaces/IClientMessage.cs new file mode 100644 index 00000000..5244059e --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IClientMessage.cs @@ -0,0 +1,8 @@ +namespace Wino.Core.Domain.Interfaces +{ + /// + /// All messages that Client sends to Server and awaits a response in return. + /// For example; triggering a new synchronization request. + /// + public interface IClientMessage; +} diff --git a/Wino.Core.Domain/Interfaces/ILogInitializer.cs b/Wino.Core.Domain/Interfaces/ILogInitializer.cs index f5f7e97d..13accf16 100644 --- a/Wino.Core.Domain/Interfaces/ILogInitializer.cs +++ b/Wino.Core.Domain/Interfaces/ILogInitializer.cs @@ -2,7 +2,7 @@ { public interface ILogInitializer { - void SetupLogger(string logFolderPath); + void SetupLogger(string fullLogFilePath); void RefreshLoggingLevel(); } diff --git a/Wino.Core.Domain/Interfaces/IMailService.cs b/Wino.Core.Domain/Interfaces/IMailService.cs index 108c1b3e..e314d854 100644 --- a/Wino.Core.Domain/Interfaces/IMailService.cs +++ b/Wino.Core.Domain/Interfaces/IMailService.cs @@ -11,7 +11,7 @@ namespace Wino.Core.Domain.Interfaces { Task GetSingleMailItemAsync(string mailCopyId, string remoteFolderId); Task GetSingleMailItemAsync(Guid uniqueMailId); - Task CreateDraftAsync(MailAccount composerAccount, MimeMessage generatedReplyMime, MimeMessage replyingMimeMessage = null, IMailItem replyingMailItem = null); + Task CreateDraftAsync(MailAccount composerAccount, string generatedReplyMimeMessageBase64, MimeMessage replyingMimeMessage = null, IMailItem replyingMailItem = null); Task> FetchMailsAsync(MailListInitializationOptions options); /// @@ -51,7 +51,16 @@ namespace Wino.Core.Domain.Interfaces /// Task MapLocalDraftAsync(string newMailCopyId, string newDraftId, string newThreadId); - Task CreateDraftMimeMessageAsync(Guid accountId, DraftCreationOptions options); + /// + /// Creates a draft message with the given options. + /// + /// Account to create draft for. + /// Draft creation options. + /// + /// Base64 encoded string of MimeMessage object. + /// This is mainly for serialization purposes. + /// + Task CreateDraftMimeBase64Async(Guid accountId, DraftCreationOptions options); Task UpdateMailAsync(MailCopy mailCopy); /// @@ -92,5 +101,14 @@ namespace Wino.Core.Domain.Interfaces /// /// Folder id to get unread mails for. Task> GetUnreadMailsByFolderIdAsync(Guid folderId); + + /// + /// 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. + /// + /// Message id + /// Folder's local id. + /// Whether mail exists in the folder or not. + Task IsMailExistsAsync(string mailCopyId, Guid folderId); } } diff --git a/Wino.Core.Domain/Interfaces/INativeAppService.cs b/Wino.Core.Domain/Interfaces/INativeAppService.cs index b6d14249..7b1aeeaf 100644 --- a/Wino.Core.Domain/Interfaces/INativeAppService.cs +++ b/Wino.Core.Domain/Interfaces/INativeAppService.cs @@ -11,6 +11,20 @@ namespace Wino.Core.Domain.Interfaces Task GetEditorBundlePathAsync(); Task LaunchFileAsync(string filePath); Task LaunchUriAsync(Uri uri); + + /// + /// Launches the default browser with the specified uri and waits for protocol activation to finish. + /// + /// + /// Response callback from the browser. + Task GetAuthorizationResponseUriAsync(IAuthenticator authenticator, string authorizationUri); + + /// + /// Finalizes GetAuthorizationResponseUriAsync for current IAuthenticator. + /// + /// + void ContinueAuthorization(Uri authorizationResponseUri); + bool IsAppRunning(); string GetFullAppVersion(); @@ -21,5 +35,11 @@ namespace Wino.Core.Domain.Interfaces /// Some cryptographic shit is needed for requesting Google authentication in UWP. /// GoogleAuthorizationRequest GetGoogleAuthorizationRequest(); + + /// + /// Gets or sets the function that returns a pointer for main window hwnd for UWP. + /// This is used to display WAM broker dialog on running UWP app called by a windowless server code. + /// + Func GetCoreWindowHwnd { get; set; } } } diff --git a/Wino.Core.Domain/Interfaces/IPreferencesService.cs b/Wino.Core.Domain/Interfaces/IPreferencesService.cs index f3d43016..505b309c 100644 --- a/Wino.Core.Domain/Interfaces/IPreferencesService.cs +++ b/Wino.Core.Domain/Interfaces/IPreferencesService.cs @@ -150,5 +150,10 @@ namespace Wino.Core.Domain.Interfaces /// Setting: Whether the next item should be automatically selected once the current item is moved or removed. /// bool AutoSelectNextItem { get; set; } + + /// + /// Setting: Gets or sets what should happen to server app when the client is terminated. + /// + ServerBackgroundMode ServerTerminationBehavior { get; set; } } } diff --git a/Wino.Core.Domain/Interfaces/IRequestBundle.cs b/Wino.Core.Domain/Interfaces/IRequestBundle.cs index 7c81cbd9..3ad5b4db 100644 --- a/Wino.Core.Domain/Interfaces/IRequestBundle.cs +++ b/Wino.Core.Domain/Interfaces/IRequestBundle.cs @@ -22,7 +22,7 @@ namespace Wino.Core.Domain.Interfaces TRequest NativeRequest { get; } } - public interface IRequestBase + public interface IRequestBase : IClientMessage { /// /// Synchronizer option to perform. diff --git a/Wino.Core.Domain/Interfaces/IStartupBehaviorService.cs b/Wino.Core.Domain/Interfaces/IStartupBehaviorService.cs new file mode 100644 index 00000000..3c0b7a61 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IStartupBehaviorService.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.Domain.Interfaces +{ + public interface IStartupBehaviorService + { + /// + /// Gets whether Wino Server is set to launch on startup or not. + /// + Task GetCurrentStartupBehaviorAsync(); + + /// + /// Enables/disables the current startup behavior for Wino Server. + /// + /// Whether to launch enabled or disabled. + /// True if operation success, false if not. + Task ToggleStartupBehavior(bool isEnabled); + } +} diff --git a/Wino.Core.Domain/Interfaces/ISynchronizationProgress.cs b/Wino.Core.Domain/Interfaces/ISynchronizationProgress.cs deleted file mode 100644 index eb952cf7..00000000 --- a/Wino.Core.Domain/Interfaces/ISynchronizationProgress.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Wino.Core.Domain.Interfaces -{ - /// - /// An interface for reporting progress of the synchronization. - /// Gmail does not support reporting folder progress. - /// For others, account progress is calculated based on the number of folders. - /// - public interface ISynchronizationProgress - { - /// - /// Reports account synchronization progress. - /// - /// Account id for the report. - /// Value. This is always between 0 - 100 - void AccountProgressUpdated(Guid accountId, int progress); - } -} diff --git a/Wino.Core.Domain/Interfaces/ISynchronizerFactory.cs b/Wino.Core.Domain/Interfaces/ISynchronizerFactory.cs new file mode 100644 index 00000000..52077da0 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/ISynchronizerFactory.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; + +namespace Wino.Core.Domain.Interfaces +{ + public interface ISynchronizerFactory + { + Task GetAccountSynchronizerAsync(Guid accountId); + Task InitializeAsync(); + } +} diff --git a/Wino.Core.Domain/Models/Requests/IUIMessage.cs b/Wino.Core.Domain/Interfaces/IUIMessage.cs similarity index 88% rename from Wino.Core.Domain/Models/Requests/IUIMessage.cs rename to Wino.Core.Domain/Interfaces/IUIMessage.cs index 44b74525..bace1c26 100644 --- a/Wino.Core.Domain/Models/Requests/IUIMessage.cs +++ b/Wino.Core.Domain/Interfaces/IUIMessage.cs @@ -1,4 +1,4 @@ -namespace Wino.Core.Domain.Models.Requests +namespace Wino.Core.Domain.Interfaces { /// /// Interface for all messages to report UI changes from synchronizers to UI. @@ -6,5 +6,6 @@ /// They are sent either from processor or view models to signal some other /// parts of the application. /// + public interface IUIMessage; } diff --git a/Wino.Core.Domain/Interfaces/IWinoServerConnectionManager.cs b/Wino.Core.Domain/Interfaces/IWinoServerConnectionManager.cs new file mode 100644 index 00000000..4285f128 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IWinoServerConnectionManager.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Models.Server; + +namespace Wino.Core.Domain.Interfaces +{ + public interface IWinoServerConnectionManager + { + /// + /// When the connection status changes, this event will be triggered. + /// + event EventHandler StatusChanged; + + /// + /// Gets the connection status. + /// + WinoServerConnectionStatus Status { get; } + + /// + /// Launches Full Trust process (Wino Server) and awaits connection completion. + /// If connection is not established in 5 seconds, it will return false. + /// If the server process is already running, it'll connect to existing one. + /// If the server process is not running, it'll be launched and connection establishment is awaited. + /// + /// Whether connection is established or not. + Task ConnectAsync(); + + /// + /// Disconnects from existing connection and disposes the connection. + /// + /// Whether disconnection is succesfull or not. + Task DisconnectAsync(); + + /// + /// Queues a new user request to be processed by Wino Server. + /// Healthy connection must present before calling this method. + /// + /// Request to queue for synchronizer in the server. + /// Account id to queueu request for. + Task QueueRequestAsync(IRequestBase request, Guid accountId); + + /// + /// Returns response from server for the given request. + /// + /// Response type. + /// Request type. + /// Request type. + /// Response received from the server for the given TResponse type. + Task> GetResponseAsync(TRequestType clientMessage) where TRequestType : IClientMessage; + } + + public interface IWinoServerConnectionManager : IWinoServerConnectionManager, IInitializeAsync + { + /// + /// Existing connection handle to the server of TAppServiceConnection type. + /// + TAppServiceConnection Connection { get; set; } + } +} diff --git a/Wino.Core.Domain/Interfaces/IWinoSynchronizerFactory.cs b/Wino.Core.Domain/Interfaces/IWinoSynchronizerFactory.cs new file mode 100644 index 00000000..b14480c9 --- /dev/null +++ b/Wino.Core.Domain/Interfaces/IWinoSynchronizerFactory.cs @@ -0,0 +1,11 @@ +using System; +using Wino.Core.Domain.Entities; + +namespace Wino.Core.Domain.Interfaces +{ + public interface IWinoSynchronizerFactory : IInitializeAsync + { + IBaseSynchronizer GetAccountSynchronizer(Guid accountId); + IBaseSynchronizer CreateNewSynchronizer(MailAccount account); + } +} diff --git a/Wino.Core.Domain/Models/MailItem/DraftCreationOptions.cs b/Wino.Core.Domain/Models/MailItem/DraftCreationOptions.cs index aff9b515..cc8e8280 100644 --- a/Wino.Core.Domain/Models/MailItem/DraftCreationOptions.cs +++ b/Wino.Core.Domain/Models/MailItem/DraftCreationOptions.cs @@ -1,5 +1,6 @@ using System.Collections.Specialized; using System.Linq; +using System.Text.Json.Serialization; using MimeKit; using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; @@ -8,6 +9,7 @@ namespace Wino.Core.Domain.Models.MailItem { public class DraftCreationOptions { + [JsonIgnore] public MimeMessage ReferenceMimeMessage { get; set; } public MailCopy ReferenceMailCopy { get; set; } public DraftCreationReason Reason { get; set; } diff --git a/Wino.Core.Domain/Models/MailItem/DraftPreperationRequest.cs b/Wino.Core.Domain/Models/MailItem/DraftPreperationRequest.cs index 2be737f4..e2a88536 100644 --- a/Wino.Core.Domain/Models/MailItem/DraftPreperationRequest.cs +++ b/Wino.Core.Domain/Models/MailItem/DraftPreperationRequest.cs @@ -1,23 +1,49 @@ using System; -using System.Collections.Generic; -using System.Text; +using System.Text.Json.Serialization; using MimeKit; using Wino.Core.Domain.Entities; +using Wino.Core.Domain.Extensions; namespace Wino.Core.Domain.Models.MailItem { public class DraftPreperationRequest : DraftCreationOptions { - public DraftPreperationRequest(MailAccount account, MailCopy createdLocalDraftCopy, MimeMessage createdLocalDraftMimeMessage) + public DraftPreperationRequest(MailAccount account, MailCopy createdLocalDraftCopy, string base64EncodedMimeMessage) { Account = account ?? throw new ArgumentNullException(nameof(account)); CreatedLocalDraftCopy = createdLocalDraftCopy ?? throw new ArgumentNullException(nameof(createdLocalDraftCopy)); - CreatedLocalDraftMimeMessage = createdLocalDraftMimeMessage ?? throw new ArgumentNullException(nameof(createdLocalDraftMimeMessage)); + + // MimeMessage is not serializable with System.Text.Json. Convert to base64 string. + // This is additional work when deserialization needed, but not much to do atm. + + Base64LocalDraftMimeMessage = base64EncodedMimeMessage; } + [JsonConstructor] + private DraftPreperationRequest() { } + public MailCopy CreatedLocalDraftCopy { get; set; } - public MimeMessage CreatedLocalDraftMimeMessage { get; set; } + + public string Base64LocalDraftMimeMessage { get; set; } + + [JsonIgnore] + private MimeMessage createdLocalDraftMimeMessage; + + [JsonIgnore] + public MimeMessage CreatedLocalDraftMimeMessage + { + get + { + if (createdLocalDraftMimeMessage == null) + { + createdLocalDraftMimeMessage = Base64LocalDraftMimeMessage.GetMimeMessageFromBase64(); + } + + return createdLocalDraftMimeMessage; + } + } + public MailAccount Account { get; } } } diff --git a/Wino.Core.Domain/Models/MailItem/SendDraftPreparationRequest.cs b/Wino.Core.Domain/Models/MailItem/SendDraftPreparationRequest.cs index f1159931..41f7a17b 100644 --- a/Wino.Core.Domain/Models/MailItem/SendDraftPreparationRequest.cs +++ b/Wino.Core.Domain/Models/MailItem/SendDraftPreparationRequest.cs @@ -1,7 +1,49 @@ -using MimeKit; +using System.Text.Json.Serialization; +using MimeKit; using Wino.Core.Domain.Entities; +using Wino.Core.Domain.Extensions; namespace Wino.Core.Domain.Models.MailItem { - public record SendDraftPreparationRequest(MailCopy MailItem, MimeMessage Mime, MailItemFolder DraftFolder, MailItemFolder SentFolder, MailAccountPreferences AccountPreferences); + public class SendDraftPreparationRequest + { + public MailCopy MailItem { get; set; } + public string Base64MimeMessage { get; set; } + public MailItemFolder SentFolder { get; set; } + public MailItemFolder DraftFolder { get; set; } + public MailAccountPreferences AccountPreferences { get; set; } + + public SendDraftPreparationRequest(MailCopy mailItem, + MailItemFolder sentFolder, + MailItemFolder draftFolder, + MailAccountPreferences accountPreferences, + string base64MimeMessage) + { + MailItem = mailItem; + SentFolder = sentFolder; + DraftFolder = draftFolder; + AccountPreferences = accountPreferences; + Base64MimeMessage = base64MimeMessage; + } + + [JsonConstructor] + private SendDraftPreparationRequest() { } + + [JsonIgnore] + private MimeMessage mime; + + [JsonIgnore] + public MimeMessage Mime + { + get + { + if (mime == null) + { + mime = Base64MimeMessage.GetMimeMessageFromBase64(); + } + + return mime; + } + } + } } diff --git a/Wino.Core.Domain/Models/Requests/ToggleRequestRule.cs b/Wino.Core.Domain/Models/MailItem/ToggleRequestRule.cs similarity index 86% rename from Wino.Core.Domain/Models/Requests/ToggleRequestRule.cs rename to Wino.Core.Domain/Models/MailItem/ToggleRequestRule.cs index b69db903..9ed10f48 100644 --- a/Wino.Core.Domain/Models/Requests/ToggleRequestRule.cs +++ b/Wino.Core.Domain/Models/MailItem/ToggleRequestRule.cs @@ -1,8 +1,7 @@ using System; using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Models.MailItem; -namespace Wino.Core.Domain.Models.Requests +namespace Wino.Core.Domain.Models.MailItem { /// /// Defines a single rule for toggling user actions if needed. diff --git a/Wino.Core.Domain/Models/Reader/WebViewMessage.cs b/Wino.Core.Domain/Models/Reader/WebViewMessage.cs new file mode 100644 index 00000000..f3a462b1 --- /dev/null +++ b/Wino.Core.Domain/Models/Reader/WebViewMessage.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace Wino.Core.Domain.Models.Reader +{ + /// + /// Used to pass messages from the webview to the app. + /// + public class WebViewMessage + { + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } + } +} diff --git a/Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs b/Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs new file mode 100644 index 00000000..98ca5792 --- /dev/null +++ b/Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs @@ -0,0 +1,18 @@ +using System; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Core.Domain.Models.Requests +{ + + /// + /// Encapsulates request to queue and account for synchronizer. + /// + /// + /// + /// Prepared request for the server. + /// Whihc account to execute this request for. + public record ServerRequestPackage(Guid AccountId, IRequestBase Request) : IClientMessage + { + public override string ToString() => $"Server Package: {Request.GetType().Name}"; + } +} diff --git a/Wino.Core.Domain/Models/Requests/WebViewMessage.cs b/Wino.Core.Domain/Models/Requests/WebViewMessage.cs deleted file mode 100644 index 2c6aab9c..00000000 --- a/Wino.Core.Domain/Models/Requests/WebViewMessage.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Wino.Core.Domain.Models.Requests -{ - // Used to pass messages from the webview to the app. - public class WebViewMessage - { - public string type { get; set; } - public string value { get; set; } - } -} diff --git a/Wino.Core.Domain/Models/Server/WinoServerResponse.cs b/Wino.Core.Domain/Models/Server/WinoServerResponse.cs new file mode 100644 index 00000000..45b8bb06 --- /dev/null +++ b/Wino.Core.Domain/Models/Server/WinoServerResponse.cs @@ -0,0 +1,42 @@ +using Wino.Core.Domain.Exceptions; + +namespace Wino.Core.Domain.Models.Server +{ + /// + /// Encapsulates responses from the Wino server. + /// Exceptions are stored separately in the Message and StackTrace properties due to serialization issues. + /// + /// Type of the expected response. + public class WinoServerResponse + { + public bool IsSuccess { get; set; } + public string Message { get; set; } + public T Data { get; set; } + + // protected WinoServerResponse() { } + + public static WinoServerResponse CreateSuccessResponse(T data) + { + return new WinoServerResponse + { + IsSuccess = true, + Data = data + }; + } + + public static WinoServerResponse CreateErrorResponse(string message) + { + return new WinoServerResponse + { + IsSuccess = false, + Message = message + }; + } + + public void ThrowIfFailed() + { + if (!IsSuccess) + throw new WinoServerException(Message); + } + } +} diff --git a/Wino.Core.Domain/Models/Synchronization/SynchronizationOptions.cs b/Wino.Core.Domain/Models/Synchronization/SynchronizationOptions.cs index 31ced1de..baa292b4 100644 --- a/Wino.Core.Domain/Models/Synchronization/SynchronizationOptions.cs +++ b/Wino.Core.Domain/Models/Synchronization/SynchronizationOptions.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; namespace Wino.Core.Domain.Models.Synchronization { @@ -27,11 +26,6 @@ namespace Wino.Core.Domain.Models.Synchronization /// public List SynchronizationFolderIds { get; set; } - /// - /// A listener to be notified about the progress of the synchronization. - /// - public ISynchronizationProgress ProgressListener { get; set; } - /// /// When doing a linked inbox synchronization, we must ignore reporting completion to the caller for each folder. /// This Id will help tracking that. Id is unique, but this one can be the same for all sync requests diff --git a/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs b/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs index c10a7b96..19e45eb7 100644 --- a/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs +++ b/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Text.Json.Serialization; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.MailItem; @@ -6,8 +7,14 @@ namespace Wino.Core.Domain.Models.Synchronization { public class SynchronizationResult { - protected SynchronizationResult() { } + public SynchronizationResult() { } + /// + /// Gets the new downloaded messages from synchronization. + /// Server will create notifications for these messages. + /// It's ignored in serialization. Client should not react to this. + /// + [JsonIgnore] public IEnumerable DownloadedMessages { get; set; } = new List(); public SynchronizationCompletedState CompletedState { get; set; } diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json index 5b3f7486..c75f8baa 100644 --- a/Wino.Core.Domain/Translations/en_US/resources.json +++ b/Wino.Core.Domain/Translations/en_US/resources.json @@ -105,6 +105,7 @@ "ElementTheme_Default": "Use system setting", "ElementTheme_Light": "Light mode", "Emoji": "Emoji", + "Exception_WinoServerException": "Wino server failed.", "Exception_ImapAutoDiscoveryFailed": "Couldn't find mailbox settings.", "Exception_ImapClientPoolFailed": "IMAP Client Pool failed.", "Exception_AuthenticationCanceled": "Authentication canceled", @@ -335,6 +336,7 @@ "ProtocolLogAvailable_Message": "Protocol logs are available for diagnostics.", "Results": "Results", "Right": "Right", + "Reader_SaveAllAttachmentButtonText": "Save all attachments", "SynchronizationFolderReport_Success": "up to date", "SynchronizationFolderReport_Failed": "synchronization is failed", "SearchBarPlaceholder": "Search", @@ -418,6 +420,19 @@ "SettingsFolderMenuStyle_Description": "Change whether account folders should be nested inside an account menu item or not. Toggle this off if you like the old menu system in Windows Mail", "SettingsManageAccountSettings_Description": "Notifications, signatures, synchronization and other settings per account.", "SettingsManageAccountSettings_Title": "Manage Account Settings", + "SettingsAppPreferences_Title": "App Preferences", + "SettingsAppPreferences_Description": "General settings / preferences for Wino Mail.", + "SettingsAppPreferences_CloseBehavior_Title": "Application close behavior", + "SettingsAppPreferences_CloseBehavior_Description": "What should happen when you close the app?", + "SettingsAppPreferences_StartupBehavior_Title": "Start minimized on Windows startup", + "SettingsAppPreferences_StartupBehavior_Description": "Allow Wino Mail to launch minimized when Windows starts. Always allow it to receive notifications.", + "SettingsAppPreferences_StartupBehavior_Enabled": "Wino Mail successfully set to be launched in the background on Windows startup.", + "SettingsAppPreferences_StartupBehavior_Disabled": "Wino Mail will not be launched on Windows startup. This will cause you to miss notifications when you restart your computer.", + "SettingsAppPreferences_StartupBehavior_DisabledByPolicy": "Your administrator or group policies disabled running applications on startup. Thus, Wino Mail can't be set to launch on Windows startup.", + "SettingsAppPreferences_StartupBehavior_DisabledByUser": "Please go to Task Manager -> Startup tab to allow Wino Mail to launch on Windows startup.", + "SettingsAppPreferences_StartupBehavior_FatalError": "Fatal error occurred while changing the startup mode for Wino Mail.", + "SettingsAppPreferences_StartupBehavior_Enable": "Enable", + "SettingsAppPreferences_StartupBehavior_Disable": "Disable", "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.", @@ -523,6 +538,16 @@ "SettingsSignature_AddCustomSignature_Button": "Add signature", "SettingsSignature_EditSignature_Title": "Edit signature", "SettingsSignature_DeleteSignature_Title": "Delete signature", - "SettingsSignature_NoneSignatureName": "None" + "SettingsSignature_NoneSignatureName": "None", + "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Title": "Minimize to system tray", + "SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description": "Wino Mail will keep running on the system tray. Available to launch by clicking on an icon. You will be notified as new mails arrive.", + "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title": "Run in the background", + "SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description": "Wino Mail will keep running in the background. You will be notified as new mails arrive.", + "SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title": "Terminate", + "SettingsAppPreferences_ServerBackgroundingMode_Terminate_Description": "Wino Mail will not keep running anywhere. You will not be notified as new mails arrive. Launch Wino Mail again to continue mail synchronization.", + "TitleBarServerDisconnectedButton_Title": "no connection", + "TitleBarServerDisconnectedButton_Description": "Wino is disconnected from the network. Click reconnect to restore connection.", + "TitleBarServerReconnectButton_Title": "reconnect", + "TitleBarServerReconnectingButton_Title": "connecting" } diff --git a/Wino.Core.Domain/Translator.Designer.cs b/Wino.Core.Domain/Translator.Designer.cs index 4964614c..6fc65a03 100644 --- a/Wino.Core.Domain/Translator.Designer.cs +++ b/Wino.Core.Domain/Translator.Designer.cs @@ -548,6 +548,11 @@ namespace Wino.Core.Domain /// public static string Emoji => Resources.GetTranslatedString(@"Emoji"); + /// + /// Wino server failed. + /// + public static string Exception_WinoServerException => Resources.GetTranslatedString(@"Exception_WinoServerException"); + /// /// Couldn't find mailbox settings. /// @@ -1698,6 +1703,11 @@ namespace Wino.Core.Domain /// public static string Right => Resources.GetTranslatedString(@"Right"); + /// + /// Save all attachments + /// + public static string Reader_SaveAllAttachmentButtonText => Resources.GetTranslatedString(@"Reader_SaveAllAttachmentButtonText"); + /// /// up to date /// @@ -2113,6 +2123,71 @@ namespace Wino.Core.Domain /// public static string SettingsManageAccountSettings_Title => Resources.GetTranslatedString(@"SettingsManageAccountSettings_Title"); + /// + /// App Preferences + /// + public static string SettingsAppPreferences_Title => Resources.GetTranslatedString(@"SettingsAppPreferences_Title"); + + /// + /// General settings / preferences for Wino Mail. + /// + public static string SettingsAppPreferences_Description => Resources.GetTranslatedString(@"SettingsAppPreferences_Description"); + + /// + /// Application close behavior + /// + public static string SettingsAppPreferences_CloseBehavior_Title => Resources.GetTranslatedString(@"SettingsAppPreferences_CloseBehavior_Title"); + + /// + /// What should happen when you close the app? + /// + public static string SettingsAppPreferences_CloseBehavior_Description => Resources.GetTranslatedString(@"SettingsAppPreferences_CloseBehavior_Description"); + + /// + /// Start minimized on Windows startup + /// + public static string SettingsAppPreferences_StartupBehavior_Title => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_Title"); + + /// + /// Allow Wino Mail to launch minimized when Windows starts. Always allow it to receive notifications. + /// + public static string SettingsAppPreferences_StartupBehavior_Description => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_Description"); + + /// + /// Wino Mail successfully set to be launched in the background on Windows startup. + /// + public static string SettingsAppPreferences_StartupBehavior_Enabled => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_Enabled"); + + /// + /// Wino Mail will not be launched on Windows startup. This will cause you to miss notifications when you restart your computer. + /// + public static string SettingsAppPreferences_StartupBehavior_Disabled => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_Disabled"); + + /// + /// Your administrator or group policies disabled running applications on startup. Thus, Wino Mail can't be set to launch on Windows startup. + /// + public static string SettingsAppPreferences_StartupBehavior_DisabledByPolicy => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_DisabledByPolicy"); + + /// + /// Please go to Task Manager -> Startup tab to allow Wino Mail to launch on Windows startup. + /// + public static string SettingsAppPreferences_StartupBehavior_DisabledByUser => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_DisabledByUser"); + + /// + /// Fatal error occurred while changing the startup mode for Wino Mail. + /// + public static string SettingsAppPreferences_StartupBehavior_FatalError => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_FatalError"); + + /// + /// Enable + /// + public static string SettingsAppPreferences_StartupBehavior_Enable => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_Enable"); + + /// + /// Disable + /// + public static string SettingsAppPreferences_StartupBehavior_Disable => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_Disable"); + /// /// Reorder Accounts /// @@ -2642,5 +2717,55 @@ namespace Wino.Core.Domain /// None /// public static string SettingsSignature_NoneSignatureName => Resources.GetTranslatedString(@"SettingsSignature_NoneSignatureName"); + + /// + /// Minimize to system tray + /// + public static string SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Title => Resources.GetTranslatedString(@"SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Title"); + + /// + /// Wino Mail will keep running on the system tray. Available to launch by clicking on an icon. You will be notified as new mails arrive. + /// + public static string SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description => Resources.GetTranslatedString(@"SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description"); + + /// + /// Run in the background + /// + public static string SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title => Resources.GetTranslatedString(@"SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title"); + + /// + /// Wino Mail will keep running in the background. You will be notified as new mails arrive. + /// + public static string SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description => Resources.GetTranslatedString(@"SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description"); + + /// + /// Terminate + /// + public static string SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title => Resources.GetTranslatedString(@"SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title"); + + /// + /// Wino Mail will not keep running anywhere. You will not be notified as new mails arrive. Launch Wino Mail again to continue mail synchronization. + /// + public static string SettingsAppPreferences_ServerBackgroundingMode_Terminate_Description => Resources.GetTranslatedString(@"SettingsAppPreferences_ServerBackgroundingMode_Terminate_Description"); + + /// + /// no connection + /// + public static string TitleBarServerDisconnectedButton_Title => Resources.GetTranslatedString(@"TitleBarServerDisconnectedButton_Title"); + + /// + /// Wino is disconnected from the network. Click reconnect to restore connection. + /// + public static string TitleBarServerDisconnectedButton_Description => Resources.GetTranslatedString(@"TitleBarServerDisconnectedButton_Description"); + + /// + /// reconnect + /// + public static string TitleBarServerReconnectButton_Title => Resources.GetTranslatedString(@"TitleBarServerReconnectButton_Title"); + + /// + /// connecting + /// + public static string TitleBarServerReconnectingButton_Title => Resources.GetTranslatedString(@"TitleBarServerReconnectingButton_Title"); } } diff --git a/Wino.Core.Domain/Wino.Core.Domain.csproj b/Wino.Core.Domain/Wino.Core.Domain.csproj index 187841b8..2a01010c 100644 --- a/Wino.Core.Domain/Wino.Core.Domain.csproj +++ b/Wino.Core.Domain/Wino.Core.Domain.csproj @@ -4,8 +4,19 @@ netstandard2.0 true 12.0 + AnyCPU;x64;x86 + + + + + + + + + + @@ -49,9 +60,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + diff --git a/Wino.Core.UWP/CoreUWPContainerSetup.cs b/Wino.Core.UWP/CoreUWPContainerSetup.cs index 295c907c..07cd00e8 100644 --- a/Wino.Core.UWP/CoreUWPContainerSetup.cs +++ b/Wino.Core.UWP/CoreUWPContainerSetup.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Windows.ApplicationModel.AppService; using Wino.Core.Domain.Interfaces; using Wino.Core.UWP.Services; using Wino.Services; @@ -9,19 +10,24 @@ namespace Wino.Core.UWP { public static void RegisterCoreUWPServices(this IServiceCollection services) { + var serverConnectionManager = new WinoServerConnectionManager(); + + services.AddSingleton(serverConnectionManager); + services.AddSingleton>(serverConnectionManager); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); - services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } } } diff --git a/Wino.Core.UWP/Extensions/StartupTaskStateExtensions.cs b/Wino.Core.UWP/Extensions/StartupTaskStateExtensions.cs new file mode 100644 index 00000000..652fb2fa --- /dev/null +++ b/Wino.Core.UWP/Extensions/StartupTaskStateExtensions.cs @@ -0,0 +1,25 @@ +using Windows.ApplicationModel; +using Wino.Core.Domain.Enums; + +namespace Wino.Core.UWP.Extensions +{ + public static class StartupTaskStateExtensions + { + public static StartupBehaviorResult AsStartupBehaviorResult(this StartupTaskState state) + { + switch (state) + { + case StartupTaskState.Disabled: + case StartupTaskState.DisabledByPolicy: + return StartupBehaviorResult.Disabled; + case StartupTaskState.DisabledByUser: + return StartupBehaviorResult.DisabledByUser; + case StartupTaskState.Enabled: + case StartupTaskState.EnabledByPolicy: + return StartupBehaviorResult.Enabled; + default: + return StartupBehaviorResult.Fatal; + } + } + } +} diff --git a/Wino.Core.UWP/Services/AppInitializerService.cs b/Wino.Core.UWP/Services/AppInitializerService.cs deleted file mode 100644 index 87e78bc9..00000000 --- a/Wino.Core.UWP/Services/AppInitializerService.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Threading.Tasks; -using Windows.Storage; -using Wino.Core.Domain.Interfaces; - -namespace Wino.Core.UWP.Services -{ - public class AppInitializerService : IAppInitializerService - { - private readonly IBackgroundTaskService _backgroundTaskService; - - public AppInitializerService(IBackgroundTaskService backgroundTaskService) - { - _backgroundTaskService = backgroundTaskService; - } - - public string GetPublisherSharedFolder() => ApplicationData.Current.GetPublisherCacheFolder("WinoShared").Path; - public string GetApplicationDataFolder() => ApplicationData.Current.LocalFolder.Path; - - public Task MigrateAsync() - { - UnregisterAllBackgroundTasks(); - - return Task.CompletedTask; - } - - #region 1.6.8 -> 1.6.9 - - private void UnregisterAllBackgroundTasks() - { - _backgroundTaskService.UnregisterAllBackgroundTask(); - } - - #endregion - - #region 1.7.0 - - /// - /// We decided to use publisher cache folder as a database going forward. - /// This migration will move the file from application local folder and delete it. - /// Going forward database will be initialized from publisher cache folder. - /// - private async Task MoveExistingDatabaseToSharedCacheFolderAsync() - { - throw new NotImplementedException(); - } - - #endregion - } -} diff --git a/Wino.Core.UWP/Services/BackgroundSynchronizer.cs b/Wino.Core.UWP/Services/BackgroundSynchronizer.cs deleted file mode 100644 index 64fbb57b..00000000 --- a/Wino.Core.UWP/Services/BackgroundSynchronizer.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.Threading.Tasks; -using Serilog; -using Windows.Storage; -using Wino.Core; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Exceptions; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Synchronization; -using Wino.Core.Synchronizers; - -namespace Wino.Services -{ - public interface IBackgroundSynchronizer - { - Task RunBackgroundSynchronizationAsync(BackgroundSynchronizationReason reason); - void CreateLock(); - void ReleaseLock(); - bool IsBackgroundSynchronizationLocked(); - } - - /// - /// Service responsible for handling background synchronization on timer and session connected events. - /// - public class BackgroundSynchronizer : IBackgroundSynchronizer - { - private const string BackgroundSynchronizationLock = nameof(BackgroundSynchronizationLock); - - private readonly IAccountService _accountService; - private readonly IFolderService _folderService; - private readonly IWinoSynchronizerFactory _winoSynchronizerFactory; - - public BackgroundSynchronizer(IAccountService accountService, - IFolderService folderService, - IWinoSynchronizerFactory winoSynchronizerFactory) - { - _accountService = accountService; - _folderService = folderService; - _winoSynchronizerFactory = winoSynchronizerFactory; - } - - public void CreateLock() => ApplicationData.Current.LocalSettings.Values[BackgroundSynchronizationLock] = true; - public void ReleaseLock() => ApplicationData.Current.LocalSettings.Values[BackgroundSynchronizationLock] = false; - - public bool IsBackgroundSynchronizationLocked() - => ApplicationData.Current.LocalSettings.Values.ContainsKey(BackgroundSynchronizationLock) - && ApplicationData.Current.LocalSettings.Values[BackgroundSynchronizationLock] is bool boolValue && boolValue; - - public async Task RunBackgroundSynchronizationAsync(BackgroundSynchronizationReason reason) - { - Log.Information($"{reason} background synchronization is kicked in."); - - // This should never crash. - // We might be in-process or out-of-process. - - //if (IsBackgroundSynchronizationLocked()) - //{ - // Log.Warning("Background synchronization is locked. Hence another background synchronization is canceled."); - // return; - //} - - try - { - CreateLock(); - - var accounts = await _accountService.GetAccountsAsync(); - - foreach (var account in accounts) - { - // We can't sync broken account. - if (account.AttentionReason != AccountAttentionReason.None) - continue; - - // TODO - // We can't synchronize without system folder setup is done. - //var isSystemFolderSetupDone = await _folderService.CheckSystemFolderSetupDoneAsync(account.Id); - - //// No need to throw here. It's a background process. - //if (!isSystemFolderSetupDone) - // continue; - - var synchronizer = _winoSynchronizerFactory.GetAccountSynchronizer(account.Id); - - if (synchronizer.State != AccountSynchronizerState.Idle) - { - Log.Information("Skipping background synchronization for {Name} since current state is {State}", synchronizer.Account.Name, synchronizer.State); - - return; - } - - await HandleSynchronizationAsync(synchronizer, reason); - } - } - catch (Exception ex) - { - Log.Error($"[BackgroundSynchronization] Failed with message {ex.Message}"); - } - finally - { - ReleaseLock(); - } - } - - private async Task HandleSynchronizationAsync(IBaseSynchronizer synchronizer, BackgroundSynchronizationReason reason) - { - if (synchronizer.State != AccountSynchronizerState.Idle) return; - - var account = synchronizer.Account; - - try - { - // SessionConnected will do Full synchronization for logon, Timer task will do Inbox only. - - var syncType = reason == BackgroundSynchronizationReason.SessionConnected ? SynchronizationType.Full : SynchronizationType.Inbox; - - var options = new SynchronizationOptions() - { - AccountId = account.Id, - Type = syncType, - }; - - await synchronizer.SynchronizeAsync(options); - } - catch (AuthenticationAttentionException authenticationAttentionException) - { - Log.Error(authenticationAttentionException, $"[BackgroundSync] Invalid credentials for account {account.Address}"); - - account.AttentionReason = AccountAttentionReason.InvalidCredentials; - await _accountService.UpdateAccountAsync(account); - } - catch (SystemFolderConfigurationMissingException configMissingException) - { - Log.Error(configMissingException, $"[BackgroundSync] Missing system folder configuration for account {account.Address}"); - - account.AttentionReason = AccountAttentionReason.MissingSystemFolderConfiguration; - await _accountService.UpdateAccountAsync(account); - } - catch (Exception ex) - { - Log.Error(ex, "[BackgroundSync] Synchronization failed."); - } - } - } -} diff --git a/Wino.Core.UWP/Services/BackgroundTaskService.cs b/Wino.Core.UWP/Services/BackgroundTaskService.cs index a0ac1c07..7c9aaee3 100644 --- a/Wino.Core.UWP/Services/BackgroundTaskService.cs +++ b/Wino.Core.UWP/Services/BackgroundTaskService.cs @@ -1,92 +1,60 @@ using System; -using System.Collections.Generic; using System.Threading.Tasks; using Serilog; using Windows.ApplicationModel.Background; using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Exceptions; namespace Wino.Core.UWP.Services { public class BackgroundTaskService : IBackgroundTaskService { - private const string IsBackgroundExecutionDeniedMessageKey = nameof(IsBackgroundExecutionDeniedMessageKey); + private const string Is180BackgroundTasksRegisteredKey = nameof(Is180BackgroundTasksRegisteredKey); - public const string BackgroundSynchronizationTimerTaskNameEx = nameof(BackgroundSynchronizationTimerTaskNameEx); public const string ToastActivationTaskEx = nameof(ToastActivationTaskEx); private const string SessionConnectedTaskEntryPoint = "Wino.BackgroundTasks.SessionConnectedTask"; private const string SessionConnectedTaskName = "SessionConnectedTask"; private readonly IConfigurationService _configurationService; - private readonly List registeredBackgroundTaskNames = new List(); public BackgroundTaskService(IConfigurationService configurationService) { _configurationService = configurationService; - - LoadRegisteredTasks(); - } - - // Calling WinRT all the time for registered tasks might be slow. Cache them on ctor. - private void LoadRegisteredTasks() - { - foreach (var task in BackgroundTaskRegistration.AllTasks) - { - registeredBackgroundTaskNames.Add(task.Value.Name); - } - - Log.Information($"Found {registeredBackgroundTaskNames.Count} registered background tasks. [{string.Join(',', registeredBackgroundTaskNames)}]"); } public async Task HandleBackgroundTaskRegistrations() { + bool is180BackgroundTaskRegistered = _configurationService.Get(Is180BackgroundTasksRegisteredKey); + + // Don't re-register tasks. + if (is180BackgroundTaskRegistered) return; + var response = await BackgroundExecutionManager.RequestAccessAsync(); - if (response == BackgroundAccessStatus.DeniedBySystemPolicy || - response == BackgroundAccessStatus.DeniedByUser) + if (response != BackgroundAccessStatus.DeniedBySystemPolicy || + response != BackgroundAccessStatus.DeniedByUser) { - // Only notify users about disabled background execution once. + // Unregister all tasks and register new ones. - bool isNotifiedBefore = _configurationService.Get(IsBackgroundExecutionDeniedMessageKey, false); - - if (!isNotifiedBefore) - { - _configurationService.Set(IsBackgroundExecutionDeniedMessageKey, true); - - throw new BackgroundTaskExecutionRequestDeniedException(); - } - } - else - { + UnregisterAllBackgroundTask(); RegisterSessionConnectedTask(); - RegisterTimerSynchronizationTask(); - RegisterToastNotificationHandlerBackgroundTask(); + + _configurationService.Set(Is180BackgroundTasksRegisteredKey, true); } } - private bool IsBackgroundTaskRegistered(string taskName) - => registeredBackgroundTaskNames.Contains(taskName); - public void UnregisterAllBackgroundTask() { foreach (var task in BackgroundTaskRegistration.AllTasks) { task.Value.Unregister(true); } - } - private void LogBackgroundTaskRegistration(string taskName) - { - Log.Information($"Registered new background task -> {taskName}"); - - registeredBackgroundTaskNames.Add($"{taskName}"); + Log.Information("Unregistered all background tasks."); } private BackgroundTaskRegistration RegisterSessionConnectedTask() { - if (IsBackgroundTaskRegistered(SessionConnectedTaskName)) return null; - var builder = new BackgroundTaskBuilder { Name = SessionConnectedTaskName, @@ -95,41 +63,6 @@ namespace Wino.Core.UWP.Services builder.SetTrigger(new SystemTrigger(SystemTriggerType.SessionConnected, false)); - LogBackgroundTaskRegistration(SessionConnectedTaskName); - - return builder.Register(); - } - - private BackgroundTaskRegistration RegisterToastNotificationHandlerBackgroundTask() - { - if (IsBackgroundTaskRegistered(ToastActivationTaskEx)) return null; - - var builder = new BackgroundTaskBuilder - { - Name = ToastActivationTaskEx - }; - - builder.SetTrigger(new ToastNotificationActionTrigger()); - - LogBackgroundTaskRegistration(ToastActivationTaskEx); - - return builder.Register(); - } - - private BackgroundTaskRegistration RegisterTimerSynchronizationTask() - { - if (IsBackgroundTaskRegistered(BackgroundSynchronizationTimerTaskNameEx)) return null; - - var builder = new BackgroundTaskBuilder - { - Name = BackgroundSynchronizationTimerTaskNameEx - }; - - builder.SetTrigger(new TimeTrigger(15, false)); - builder.AddCondition(new SystemCondition(SystemConditionType.InternetAvailable)); - - LogBackgroundTaskRegistration(BackgroundSynchronizationTimerTaskNameEx); - return builder.Register(); } } diff --git a/Wino.Core.UWP/Services/NativeAppService.cs b/Wino.Core.UWP/Services/NativeAppService.cs index 27d4b3cc..a39d4bdc 100644 --- a/Wino.Core.UWP/Services/NativeAppService.cs +++ b/Wino.Core.UWP/Services/NativeAppService.cs @@ -9,10 +9,17 @@ using Windows.Storage; using Windows.Storage.Streams; using Windows.System; using Windows.UI.Shell; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Controls; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Authorization; +using Wino.Core.Domain.Exceptions; +using Wino.Core.Domain; + + + +#if WINDOWS_UWP +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +#endif namespace Wino.Services { @@ -20,8 +27,18 @@ namespace Wino.Services { private string _mimeMessagesFolder; private string _editorBundlePath; + private TaskCompletionSource authorizationCompletedTaskSource; - public string GetWebAuthenticationBrokerUri() => WebAuthenticationBroker.GetCurrentApplicationCallbackUri().AbsoluteUri; + public Func GetCoreWindowHwnd { get; set; } + + public string GetWebAuthenticationBrokerUri() + { +#if WINDOWS_UWP + return WebAuthenticationBroker.GetCurrentApplicationCallbackUri().AbsoluteUri; +#endif + + return string.Empty; + } public async Task GetMimeMessageStoragePath() { @@ -91,7 +108,16 @@ namespace Wino.Services return _editorBundlePath; } - public bool IsAppRunning() => (Window.Current?.Content as Frame)?.Content != null; + [Obsolete("This should be removed. There should be no functionality.")] + public bool IsAppRunning() + { +#if WINDOWS_UWP + return (Window.Current?.Content as Frame)?.Content != null; +#endif + + return true; + } + public async Task LaunchFileAsync(string filePath) { @@ -100,7 +126,7 @@ namespace Wino.Services await Launcher.LaunchFileAsync(file); } - public Task LaunchUriAsync(Uri uri) => Xamarin.Essentials.Launcher.OpenAsync(uri); + public Task LaunchUriAsync(Uri uri) => Launcher.LaunchUriAsync(uri).AsTask(); public string GetFullAppVersion() { @@ -127,5 +153,28 @@ namespace Wino.Services await taskbarManager.RequestPinCurrentAppAsync(); } + + public async Task GetAuthorizationResponseUriAsync(IAuthenticator authenticator, string authorizationUri) + { + if (authorizationCompletedTaskSource != null) + { + authorizationCompletedTaskSource.TrySetException(new AuthenticationException(Translator.Exception_AuthenticationCanceled)); + authorizationCompletedTaskSource = null; + } + + authorizationCompletedTaskSource = new TaskCompletionSource(); + + await LaunchUriAsync(new Uri(authorizationUri)); + + return await authorizationCompletedTaskSource.Task; + } + + public void ContinueAuthorization(Uri authorizationResponseUri) + { + if (authorizationCompletedTaskSource != null) + { + authorizationCompletedTaskSource.TrySetResult(authorizationResponseUri); + } + } } } diff --git a/Wino.Core.UWP/Services/NotificationBuilder.cs b/Wino.Core.UWP/Services/NotificationBuilder.cs index c967819b..c11aa04f 100644 --- a/Wino.Core.UWP/Services/NotificationBuilder.cs +++ b/Wino.Core.UWP/Services/NotificationBuilder.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Microsoft.Toolkit.Uwp.Notifications; +using CommunityToolkit.WinUI.Notifications; using Windows.Data.Xml.Dom; using Windows.UI.Notifications; using Wino.Core.Domain; @@ -70,8 +70,8 @@ namespace Wino.Core.UWP.Services foreach (var mailItem in validItems) { - if (mailItem.IsRead) - continue; + //if (mailItem.IsRead) + // continue; var builder = new ToastContentBuilder(); builder.SetToastScenario(ToastScenario.Default); @@ -104,11 +104,11 @@ namespace Wino.Core.UWP.Services builder.AddText(mailItem.Subject); builder.AddText(mailItem.PreviewText); - builder.AddArgument(Constants.ToastMailItemIdKey, mailItem.UniqueId.ToString()); + builder.AddArgument(Constants.ToastMailUniqueIdKey, mailItem.UniqueId.ToString()); builder.AddArgument(Constants.ToastActionKey, MailOperation.Navigate); - builder.AddButton(GetMarkedAsRead(mailItem.Id, mailItem.AssignedFolder.RemoteFolderId)); - builder.AddButton(GetDeleteButton(mailItem.Id, mailItem.AssignedFolder.RemoteFolderId)); + builder.AddButton(GetMarkedAsRead(mailItem.UniqueId)); + builder.AddButton(GetDeleteButton(mailItem.UniqueId)); builder.AddButton(GetDismissButton()); builder.Show(); @@ -123,21 +123,19 @@ namespace Wino.Core.UWP.Services .SetDismissActivation() .SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/dismiss.png")); - private ToastButton GetDeleteButton(string mailCopyId, string remoteFolderId) + private ToastButton GetDeleteButton(Guid mailUniqueId) => new ToastButton() .SetContent(Translator.MailOperation_Delete) .SetImageUri(new Uri("ms-appx:///Assets/NotificationIcons/delete.png")) - .AddArgument(Constants.ToastMailItemIdKey, mailCopyId) - .AddArgument(Constants.ToastMailItemRemoteFolderIdKey, remoteFolderId) + .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) .AddArgument(Constants.ToastActionKey, MailOperation.SoftDelete) .SetBackgroundActivation(); - private ToastButton GetMarkedAsRead(string mailCopyId, string remoteFolderId) + private ToastButton GetMarkedAsRead(Guid mailUniqueId) => new ToastButton() .SetContent(Translator.MailOperation_MarkAsRead) .SetImageUri(new System.Uri("ms-appx:///Assets/NotificationIcons/markread.png")) - .AddArgument(Constants.ToastMailItemIdKey, mailCopyId) - .AddArgument(Constants.ToastMailItemRemoteFolderIdKey, remoteFolderId) + .AddArgument(Constants.ToastMailUniqueIdKey, mailUniqueId.ToString()) .AddArgument(Constants.ToastActionKey, MailOperation.MarkAsRead) .SetBackgroundActivation(); diff --git a/Wino.Mail/Services/PreferencesService.cs b/Wino.Core.UWP/Services/PreferencesService.cs similarity index 95% rename from Wino.Mail/Services/PreferencesService.cs rename to Wino.Core.UWP/Services/PreferencesService.cs index 953ada88..7d77aef5 100644 --- a/Wino.Mail/Services/PreferencesService.cs +++ b/Wino.Core.UWP/Services/PreferencesService.cs @@ -7,7 +7,7 @@ using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Reader; using Wino.Core.Services; -namespace Wino.Services +namespace Wino.Core.UWP.Services { public class PreferencesService : ObservableObject, IPreferencesService { @@ -201,5 +201,11 @@ namespace Wino.Services get => _configurationService.Get(nameof(AutoSelectNextItem), true); set => SaveProperty(propertyName: nameof(AutoSelectNextItem), value); } + + public ServerBackgroundMode ServerTerminationBehavior + { + get => _configurationService.Get(nameof(ServerTerminationBehavior), ServerBackgroundMode.MinimizedTray); + set => SaveProperty(propertyName: nameof(ServerTerminationBehavior), value); + } } } diff --git a/Wino.Core.UWP/Services/StartupBehaviorService.cs b/Wino.Core.UWP/Services/StartupBehaviorService.cs new file mode 100644 index 00000000..e889b21f --- /dev/null +++ b/Wino.Core.UWP/Services/StartupBehaviorService.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading.Tasks; +using Windows.ApplicationModel; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.UWP.Extensions; + +namespace Wino.Core.UWP.Services +{ + public class StartupBehaviorService : IStartupBehaviorService + { + private const string WinoServerTaskId = "WinoServer"; + + public async Task ToggleStartupBehavior(bool isEnabled) + { + var task = await StartupTask.GetAsync(WinoServerTaskId); + + if (isEnabled) + { + await task.RequestEnableAsync(); + } + else + { + task.Disable(); + } + + return await GetCurrentStartupBehaviorAsync(); + } + + public async Task GetCurrentStartupBehaviorAsync() + { + var task = await StartupTask.GetAsync(WinoServerTaskId); + + return task.State.AsStartupBehaviorResult(); + } + } +} diff --git a/Wino.Mail/Services/StatePersistenceService.cs b/Wino.Core.UWP/Services/StatePersistenceService.cs similarity index 89% rename from Wino.Mail/Services/StatePersistenceService.cs rename to Wino.Core.UWP/Services/StatePersistenceService.cs index 4c995ef9..f9119c6a 100644 --- a/Wino.Mail/Services/StatePersistenceService.cs +++ b/Wino.Core.UWP/Services/StatePersistenceService.cs @@ -2,9 +2,8 @@ using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Messaging; -using Microsoft.AppCenter.Crashes; using Wino.Core.Domain.Interfaces; -using Wino.Core.Messages.Shell; +using Wino.Messaging.Client.Shell; namespace Wino.Services { @@ -116,17 +115,10 @@ namespace Wino.Services private void UpdateAppCoreWindowTitle() { - try - { - var appView = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView(); + var appView = Windows.UI.ViewManagement.ApplicationView.GetForCurrentView(); - if (appView != null) - appView.Title = CoreWindowTitle; - } - catch (System.Exception ex) - { - Crashes.TrackError(ex); - } + if (appView != null) + appView.Title = CoreWindowTitle; } } } diff --git a/Wino.Core.UWP/Services/ThemeService.cs b/Wino.Core.UWP/Services/ThemeService.cs index 9649a625..8db94f1c 100644 --- a/Wino.Core.UWP/Services/ThemeService.cs +++ b/Wino.Core.UWP/Services/ThemeService.cs @@ -20,10 +20,10 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Personalization; -using Wino.Core.Messages.Shell; using Wino.Core.UWP.Extensions; using Wino.Core.UWP.Models.Personalization; using Wino.Core.UWP.Services; +using Wino.Messaging.Client.Shell; namespace Wino.Services { @@ -167,6 +167,7 @@ namespace Wino.Services await ApplyCustomThemeAsync(true); // Registering to color changes, thus we notice when user changes theme system wide + uiSettings.ColorValuesChanged -= UISettingsColorChanged; uiSettings.ColorValuesChanged += UISettingsColorChanged; } diff --git a/Wino.Core.UWP/Services/UnderlyingThemeService.cs b/Wino.Core.UWP/Services/UnderlyingThemeService.cs index 5f88a393..4c06648f 100644 --- a/Wino.Core.UWP/Services/UnderlyingThemeService.cs +++ b/Wino.Core.UWP/Services/UnderlyingThemeService.cs @@ -1,5 +1,5 @@ using Windows.UI.ViewManagement; -using Windows.UI.Xaml; +using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; namespace Wino.Core.UWP.Services @@ -21,12 +21,12 @@ namespace Wino.Core.UWP.Services public bool IsUnderlyingThemeDark() { - var currentTheme = _configurationService.Get(SelectedAppThemeKey, ElementTheme.Default); + var currentTheme = _configurationService.Get(SelectedAppThemeKey, ApplicationElementTheme.Default); - if (currentTheme == ElementTheme.Default) + if (currentTheme == ApplicationElementTheme.Default) return uiSettings.GetColorValue(UIColorType.Background).ToString() == "#FF000000"; else - return currentTheme == ElementTheme.Dark; + return currentTheme == ApplicationElementTheme.Dark; } } } diff --git a/Wino.Core.UWP/Services/WinoServerConnectionManager.cs b/Wino.Core.UWP/Services/WinoServerConnectionManager.cs new file mode 100644 index 00000000..f0551888 --- /dev/null +++ b/Wino.Core.UWP/Services/WinoServerConnectionManager.cs @@ -0,0 +1,316 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.Messaging; +using Nito.AsyncEx; +using Serilog; +using Windows.ApplicationModel; +using Windows.ApplicationModel.AppService; +using Windows.Foundation.Collections; +using Windows.Foundation.Metadata; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Requests; +using Wino.Core.Domain.Models.Server; +using Wino.Core.Integration.Json; +using Wino.Messaging; +using Wino.Messaging.Client.Connection; +using Wino.Messaging.Enums; +using Wino.Messaging.UI; + +namespace Wino.Core.UWP.Services +{ + public class WinoServerConnectionManager : + IWinoServerConnectionManager, + IRecipient + { + private const int ServerConnectionTimeoutMs = 5000; + + public event EventHandler StatusChanged; + private TaskCompletionSource _connectionTaskCompletionSource; + + private ILogger Logger => Logger.ForContext(); + + private WinoServerConnectionStatus status; + + public WinoServerConnectionStatus Status + { + get { return status; } + private set + { + status = value; + StatusChanged?.Invoke(this, value); + } + } + + private AppServiceConnection _connection; + public AppServiceConnection Connection + { + get { return _connection; } + set + { + if (_connection != null) + { + _connection.RequestReceived -= ServerMessageReceived; + _connection.ServiceClosed -= ServerDisconnected; + } + + _connection = value; + + if (value == null) + { + Status = WinoServerConnectionStatus.Disconnected; + } + else + { + value.RequestReceived += ServerMessageReceived; + value.ServiceClosed += ServerDisconnected; + + Status = WinoServerConnectionStatus.Connected; + } + } + } + + private readonly JsonSerializerOptions _jsonSerializerOptions = new() + { + TypeInfoResolver = new ServerRequestTypeInfoResolver() + }; + + public WinoServerConnectionManager() + { + WeakReferenceMessenger.Default.Register(this); + } + + public async Task ConnectAsync() + { + if (Status == WinoServerConnectionStatus.Connected) return true; + + if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0)) + { + try + { + _connectionTaskCompletionSource ??= new TaskCompletionSource(); + + var connectionCancellationToken = new CancellationTokenSource(TimeSpan.FromMilliseconds(ServerConnectionTimeoutMs)); + + Status = WinoServerConnectionStatus.Connecting; + + await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync(); + + // Connection establishment handler is in App.xaml.cs OnBackgroundActivated. + // Once the connection is established, the handler will set the Connection property + // and WinoServerConnectionEstrablished will be fired by the messenger. + + await _connectionTaskCompletionSource.Task.WaitAsync(connectionCancellationToken.Token); + } + catch (Exception) + { + Status = WinoServerConnectionStatus.Failed; + return false; + } + + return true; + } + + return false; + } + + public async Task DisconnectAsync() + { + if (Connection == null || Status == WinoServerConnectionStatus.Disconnected) return true; + + // TODO: Send disconnect message to the fulltrust process. + + return true; + } + + public async Task InitializeAsync() + { + var isConnectionSuccessfull = await ConnectAsync(); + + // TODO: Log connection status + } + + private void ServerMessageReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args) + { + if (args.Request.Message.TryGetValue(MessageConstants.MessageTypeKey, out object messageTypeObject) && messageTypeObject is int messageTypeInt) + { + var messageType = (MessageType)messageTypeInt; + + if (args.Request.Message.TryGetValue(MessageConstants.MessageDataKey, out object messageDataObject) && messageDataObject is string messageJson) + { + switch (messageType) + { + case MessageType.UIMessage: + if (!args.Request.Message.TryGetValue(MessageConstants.MessageDataTypeKey, out object dataTypeObject) || dataTypeObject is not string dataTypeName) + throw new ArgumentException("Message data type is missing."); + + HandleUIMessage(messageJson, dataTypeName); + break; + default: + break; + } + } + } + } + + /// + /// Unpacks IServerMessage objects and delegate it to Messenger for UI to process. + /// + /// Message data in json format. + private void HandleUIMessage(string messageJson, string typeName) + { + switch (typeName) + { + case nameof(MailAddedMessage): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(MailDownloadedMessage): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(MailRemovedMessage): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(MailUpdatedMessage): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(AccountCreatedMessage): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(AccountRemovedMessage): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(AccountUpdatedMessage): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(DraftCreated): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(DraftFailed): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(DraftMapped): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(FolderRenamed): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(FolderSynchronizationEnabled): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(MergedInboxRenamed): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(AccountSynchronizationCompleted): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(RefreshUnreadCountsMessage): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(AccountSynchronizerStateChanged): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + case nameof(AccountSynchronizationProgressUpdatedMessage): + WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize(messageJson)); + break; + default: + throw new Exception("Invalid data type name passed to client."); + } + } + + private void ServerDisconnected(AppServiceConnection sender, AppServiceClosedEventArgs args) + { + // TODO: Handle server disconnection. + } + + public async Task QueueRequestAsync(IRequestBase request, Guid accountId) + { + var queuePackage = new ServerRequestPackage(accountId, request); + + var queueResponse = await GetResponseInternalAsync(queuePackage, new Dictionary() + { + { MessageConstants.MessageDataRequestAccountIdKey, accountId } + }); + + queueResponse.ThrowIfFailed(); + } + + public Task> GetResponseAsync(TRequestType message) where TRequestType : IClientMessage + => GetResponseInternalAsync(message); + + private async Task> GetResponseInternalAsync(TRequestType message, Dictionary parameters = null) + { + if (Connection == null) + return WinoServerResponse.CreateErrorResponse("Server connection is not established."); + + string serializedMessage = string.Empty; + + try + { + serializedMessage = JsonSerializer.Serialize(message, _jsonSerializerOptions); + } + catch (Exception serializationException) + { + Logger.Error(serializationException, $"Failed to serialize client message for sending."); + return WinoServerResponse.CreateErrorResponse($"Failed to serialize message.\n{serializationException.Message}"); + } + + AppServiceResponse response = null; + + try + { + var valueSet = new ValueSet + { + { MessageConstants.MessageTypeKey, (int)MessageType.ServerMessage }, + { MessageConstants.MessageDataKey, serializedMessage }, + { MessageConstants.MessageDataTypeKey, message.GetType().Name } + }; + + // Add additional parameters into ValueSet + if (parameters != null) + { + foreach (var item in parameters) + { + valueSet.Add(item.Key, item.Value); + } + } + + response = await Connection.SendMessageAsync(valueSet); + } + catch (Exception serverSendException) + { + Logger.Error(serverSendException, $"Failed to send message to server."); + return WinoServerResponse.CreateErrorResponse($"Failed to send message to server.\n{serverSendException.Message}"); + } + + // It should be always Success. + if (response.Status != AppServiceResponseStatus.Success) + return WinoServerResponse.CreateErrorResponse($"Wino Server responded with '{response.Status}' status to message delivery."); + + // All responses must contain a message data. + if (!(response.Message.TryGetValue(MessageConstants.MessageDataKey, out object messageDataObject) && messageDataObject is string messageJson)) + return WinoServerResponse.CreateErrorResponse("Server response did not contain message data."); + + // Try deserialize the message data. + try + { + return JsonSerializer.Deserialize>(messageJson); + } + catch (Exception jsonDeserializationError) + { + Logger.Error(jsonDeserializationError, $"Failed to deserialize server response message data."); + return WinoServerResponse.CreateErrorResponse($"Failed to deserialize Wino server response message data.\n{jsonDeserializationError.Message}"); + } + } + + public void Receive(WinoServerConnectionEstrablished message) + { + if (_connectionTaskCompletionSource != null) + { + _connectionTaskCompletionSource.TrySetResult(true); + } + } + } +} diff --git a/Wino.Core.UWP/Wino.Core.UWP.csproj b/Wino.Core.UWP/Wino.Core.UWP.csproj index 993d9168..043212a9 100644 --- a/Wino.Core.UWP/Wino.Core.UWP.csproj +++ b/Wino.Core.UWP/Wino.Core.UWP.csproj @@ -17,25 +17,6 @@ 512 {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - - AnyCPU - true - full - false - bin\Debug\ - DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP - prompt - 4 - - - AnyCPU - pdbonly - true - bin\Release\ - TRACE;NETFX_CORE;WINDOWS_UWP - prompt - 4 - x86 true @@ -118,17 +99,21 @@ PackageReference + 12.0 + - - + + + + @@ -143,9 +128,9 @@ - + + 7.1.2 + 5.0.4 @@ -155,9 +140,6 @@ 7.1.3 - - 7.1.3 - @@ -168,8 +150,16 @@ {e6b1632a-8901-41e8-9ddf-6793c7698b0b} Wino.Core + + {0c307d7e-256f-448c-8265-5622a812fbcc} + Wino.Messaging + + + + + Windows Desktop Extensions for the UWP + - 14.0 diff --git a/Wino.Core/Authenticators/CustomAuthenticator.cs b/Wino.Core/Authenticators/CustomAuthenticator.cs deleted file mode 100644 index a254d66c..00000000 --- a/Wino.Core/Authenticators/CustomAuthenticator.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Threading.Tasks; -using Wino.Core.Domain.Entities; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Services; - -namespace Wino.Core.Authenticators -{ - public class CustomAuthenticator : BaseAuthenticator, IAuthenticator - { - public CustomAuthenticator(ITokenService tokenService) : base(tokenService) { } - - public override MailProviderType ProviderType => MailProviderType.IMAP4; - - public string ClientId => throw new NotImplementedException(); // Not needed. - - public event EventHandler InteractiveAuthenticationRequired; - - public void CancelAuthorization() { } - - public void ContinueAuthorization(Uri authorizationResponseUri) { } - - public Task GenerateTokenAsync(MailAccount account, bool saveToken) - { - throw new NotImplementedException(); - } - - public Task GetTokenAsync(MailAccount account) - { - throw new NotImplementedException(); - } - } -} diff --git a/Wino.Core/Authenticators/GmailAuthenticator.cs b/Wino.Core/Authenticators/GmailAuthenticator.cs index 3c77136e..32f0235d 100644 --- a/Wino.Core/Authenticators/GmailAuthenticator.cs +++ b/Wino.Core/Authenticators/GmailAuthenticator.cs @@ -1,10 +1,8 @@ using System; using System.Net.Http; using System.Text; -using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using Nito.AsyncEx; using Wino.Core.Domain; using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; @@ -13,11 +11,10 @@ using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Authentication; using Wino.Core.Domain.Models.Authorization; using Wino.Core.Services; -using Xamarin.Essentials; namespace Wino.Core.Authenticators { - public class GmailAuthenticator : BaseAuthenticator, IAuthenticator + public class GmailAuthenticator : BaseAuthenticator, IGmailAuthenticator { public string ClientId { get; } = "973025879644-s7b4ur9p3rlgop6a22u7iuptdc0brnrn.apps.googleusercontent.com"; @@ -27,9 +24,6 @@ namespace Wino.Core.Authenticators public override MailProviderType ProviderType => MailProviderType.Gmail; - private TaskCompletionSource _authorizationCompletionSource = null; - private CancellationTokenSource _authorizationCancellationTokenSource = null; - private readonly INativeAppService _nativeAppService; public event EventHandler InteractiveAuthenticationRequired; @@ -99,8 +93,6 @@ namespace Wino.Core.Authenticators }; } - public void ContinueAuthorization(Uri authorizationResponseUri) => _authorizationCompletionSource?.TrySetResult(authorizationResponseUri); - public async Task GetTokenAsync(MailAccount account) { var cachedToken = await TokenService.GetTokenInformationAsync(account.Id) @@ -127,29 +119,19 @@ namespace Wino.Core.Authenticators { var authRequest = _nativeAppService.GetGoogleAuthorizationRequest(); - _authorizationCompletionSource = new TaskCompletionSource(); - _authorizationCancellationTokenSource = new CancellationTokenSource(); - var authorizationUri = authRequest.BuildRequest(ClientId); - await Browser.OpenAsync(authorizationUri, BrowserLaunchMode.SystemPreferred); - Uri responseRedirectUri = null; try { - responseRedirectUri = await _authorizationCompletionSource.Task.WaitAsync(_authorizationCancellationTokenSource.Token); + //await _authorizationCompletionSource.Task.WaitAsync(_authorizationCancellationTokenSource.Token); + responseRedirectUri = await _nativeAppService.GetAuthorizationResponseUriAsync(this, authorizationUri); } - catch (OperationCanceledException) + catch (Exception) { throw new AuthenticationException(Translator.Exception_AuthenticationCanceled); } - finally - { - _authorizationCancellationTokenSource.Dispose(); - _authorizationCancellationTokenSource = null; - _authorizationCompletionSource = null; - } authRequest.ValidateAuthorizationCode(responseRedirectUri); @@ -213,7 +195,5 @@ namespace Wino.Core.Authenticators RefreshToken = activeRefreshToken }; } - - public void CancelAuthorization() => _authorizationCancellationTokenSource?.Cancel(); } } diff --git a/Wino.Core/Authenticators/Office365Authenticator.cs b/Wino.Core/Authenticators/Office365Authenticator.cs index 916be96c..4d3a759d 100644 --- a/Wino.Core/Authenticators/Office365Authenticator.cs +++ b/Wino.Core/Authenticators/Office365Authenticator.cs @@ -6,7 +6,7 @@ namespace Wino.Core.Authenticators { public class Office365Authenticator : OutlookAuthenticator { - public Office365Authenticator(ITokenService tokenService, INativeAppService nativeAppService) : base(tokenService, nativeAppService) { } + public Office365Authenticator(ITokenService tokenService, INativeAppService nativeAppService, IApplicationConfiguration applicationConfiguration) : base(tokenService, nativeAppService, applicationConfiguration) { } public override MailProviderType ProviderType => MailProviderType.Office365; } diff --git a/Wino.Core/Authenticators/OutlookAuthenticator.cs b/Wino.Core/Authenticators/OutlookAuthenticator.cs index 878d1c91..52bd8654 100644 --- a/Wino.Core/Authenticators/OutlookAuthenticator.cs +++ b/Wino.Core/Authenticators/OutlookAuthenticator.cs @@ -2,6 +2,8 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Broker; +using Microsoft.Identity.Client.Extensions.Msal; using Wino.Core.Domain; using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; @@ -12,76 +14,82 @@ using Wino.Core.Services; namespace Wino.Core.Authenticators { - public class OutlookAuthenticator : BaseAuthenticator, IAuthenticator + /// + /// Authenticator for Outlook provider. + /// Token cache is managed by MSAL, not by Wino. + /// + public class OutlookAuthenticator : BaseAuthenticator, IOutlookAuthenticator { + private const string TokenCacheFileName = "OutlookCache.bin"; + private bool isTokenCacheAttached = false; + // Outlook private const string Authority = "https://login.microsoftonline.com/common"; public string ClientId { get; } = "b19c2035-d740-49ff-b297-de6ec561b208"; - private readonly string[] MailScope = new string[] { "email", "mail.readwrite", "offline_access", "mail.send" }; + private readonly string[] MailScope = ["email", "mail.readwrite", "offline_access", "mail.send"]; public override MailProviderType ProviderType => MailProviderType.Outlook; private readonly IPublicClientApplication _publicClientApplication; + private readonly IApplicationConfiguration _applicationConfiguration; - public OutlookAuthenticator(ITokenService tokenService, INativeAppService nativeAppService) : base(tokenService) + public OutlookAuthenticator(ITokenService tokenService, + INativeAppService nativeAppService, + IApplicationConfiguration applicationConfiguration) : base(tokenService) { + _applicationConfiguration = applicationConfiguration; + var authenticationRedirectUri = nativeAppService.GetWebAuthenticationBrokerUri(); - _publicClientApplication = PublicClientApplicationBuilder.Create(ClientId) - .WithAuthority(Authority) - .WithRedirectUri(authenticationRedirectUri) - .Build(); + var options = new BrokerOptions(BrokerOptions.OperatingSystems.Windows) + { + Title = "Wino Mail", + ListOperatingSystemAccounts = true, + }; + + var outlookAppBuilder = PublicClientApplicationBuilder.Create(ClientId) + .WithParentActivityOrWindow(nativeAppService.GetCoreWindowHwnd) + .WithBroker(options) + .WithDefaultRedirectUri() + .WithAuthority(Authority); + + _publicClientApplication = outlookAppBuilder.Build(); } -#pragma warning disable S1133 // Deprecated code should be removed - [Obsolete("Not used for OutlookAuthenticator.")] -#pragma warning restore S1133 // Deprecated code should be removed - public void ContinueAuthorization(Uri authorizationResponseUri) { } - -#pragma warning disable S1133 // Deprecated code should be removed - [Obsolete("Not used for OutlookAuthenticator.")] -#pragma warning restore S1133 // Deprecated code should be removed - public void CancelAuthorization() { } - public async Task GetTokenAsync(MailAccount account) { - var cachedToken = await TokenService.GetTokenInformationAsync(account.Id) - ?? throw new AuthenticationAttentionException(account); - - // We have token but it's expired. - // Silently refresh the token and save new token. - - if (cachedToken.IsExpired) + if (!isTokenCacheAttached) { - var cachedOutlookAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(a => a.Username == account.Address); + var storageProperties = new StorageCreationPropertiesBuilder(TokenCacheFileName, _applicationConfiguration.PublisherSharedFolderPath).Build(); + var msalcachehelper = await MsalCacheHelper.CreateAsync(storageProperties); + msalcachehelper.RegisterCache(_publicClientApplication.UserTokenCache); - // Again, not expected at all... - // Force interactive login at this point. - - if (cachedOutlookAccount == null) - { - // What if interactive login info is for different account? - - return await GenerateTokenAsync(account, true); - } - else - { - // Silently refresh token from cache. - - AuthenticationResult authResult = await _publicClientApplication.AcquireTokenSilent(MailScope, cachedOutlookAccount).ExecuteAsync(); - - // Save refreshed token and return - var refreshedTokenInformation = authResult.CreateTokenInformation(); - - await TokenService.SaveTokenInformationAsync(account.Id, refreshedTokenInformation); - - return refreshedTokenInformation; - } + isTokenCacheAttached = true; + } + + var storedAccount = (await _publicClientApplication.GetAccountsAsync()).FirstOrDefault(a => a.Username == account.Address); + + // TODO: Handle it from the server. + if (storedAccount == null) throw new AuthenticationAttentionException(account); + + try + { + var authResult = await _publicClientApplication.AcquireTokenSilent(MailScope, storedAccount).ExecuteAsync(); + + return authResult.CreateTokenInformation() ?? throw new Exception("Failed to get Outlook token."); + } + catch (MsalUiRequiredException) + { + // Somehow MSAL is not able to refresh the token silently. + // Force interactive login. + return await GenerateTokenAsync(account, true); + } + catch (Exception) + { + throw; } - else - return cachedToken; } public async Task GenerateTokenAsync(MailAccount account, bool saveToken) diff --git a/Wino.Core/Authenticators/YahooAuthenticator.cs b/Wino.Core/Authenticators/YahooAuthenticator.cs deleted file mode 100644 index f42842ca..00000000 --- a/Wino.Core/Authenticators/YahooAuthenticator.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Threading.Tasks; -using Wino.Core.Domain.Entities; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Services; - -namespace Wino.Core.Authenticators -{ - public class YahooAuthenticator : BaseAuthenticator, IAuthenticator - { - public YahooAuthenticator(ITokenService tokenService) : base(tokenService) { } - - public override MailProviderType ProviderType => MailProviderType.Yahoo; - - public string ClientId => throw new NotImplementedException(); - - public event EventHandler InteractiveAuthenticationRequired; - - public void CancelAuthorization() - { - throw new NotImplementedException(); - } - - public void ContinueAuthorization(Uri authorizationResponseUri) - { - throw new NotImplementedException(); - } - - public Task GenerateTokenAsync(MailAccount account, bool saveToken) - { - throw new NotImplementedException(); - } - - public Task GetTokenAsync(MailAccount account) - { - throw new NotImplementedException(); - } - } -} diff --git a/Wino.Core/CoreContainerSetup.cs b/Wino.Core/CoreContainerSetup.cs index f57b66ee..b0cbd702 100644 --- a/Wino.Core/CoreContainerSetup.cs +++ b/Wino.Core/CoreContainerSetup.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Serilog.Core; +using Wino.Core.Authenticators; using Wino.Core.Domain.Interfaces; using Wino.Core.Integration.Processors; using Wino.Core.Integration.Threading; @@ -16,9 +17,9 @@ namespace Wino.Core services.AddSingleton(loggerLevelSwitcher); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -42,9 +43,14 @@ namespace Wino.Core services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); + + services.AddSingleton(); } } } diff --git a/Wino.Core/Extensions/TokenizationExtensions.cs b/Wino.Core/Extensions/TokenizationExtensions.cs index 3ffefb57..e03aebd7 100644 --- a/Wino.Core/Extensions/TokenizationExtensions.cs +++ b/Wino.Core/Extensions/TokenizationExtensions.cs @@ -8,21 +8,15 @@ namespace Wino.Core.Extensions { public static TokenInformation CreateTokenInformation(this AuthenticationResult clientBuilderResult) { - var expirationDate = clientBuilderResult.ExpiresOn.UtcDateTime; - var accesToken = clientBuilderResult.AccessToken; - var userName = clientBuilderResult.Account.Username; - - // MSAL does not expose refresh token for security reasons. - // This token info will be created without refresh token. - // but OutlookIntegrator will ask for publicApplication to refresh it - // in case of expiration. + // Plain access token info is not stored for Outlook in Wino's database. + // Here we store UniqueId and Access Token in memory only to compare the UniqueId returned from MSAL auth result. var tokenInfo = new TokenInformation() { - ExpiresAt = expirationDate, - AccessToken = accesToken, - Address = userName, + Address = clientBuilderResult.Account.Username, Id = Guid.NewGuid(), + UniqueId = clientBuilderResult.UniqueId, + AccessToken = clientBuilderResult.AccessToken }; return tokenInfo; diff --git a/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs b/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs new file mode 100644 index 00000000..530f3050 --- /dev/null +++ b/Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs @@ -0,0 +1,49 @@ +using System.Text.Json.Serialization.Metadata; +using Wino.Core.Domain.Entities; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.MailItem; +using Wino.Core.Requests; + +namespace Wino.Core.Integration.Json +{ + public class ServerRequestTypeInfoResolver : DefaultJsonTypeInfoResolver + { + public ServerRequestTypeInfoResolver() + { + Modifiers.Add(new System.Action(t => + { + if (t.Type == typeof(IRequestBase)) + { + t.PolymorphismOptions = new() + { + DerivedTypes = + { + new JsonDerivedType(typeof(AlwaysMoveToRequest), nameof(AlwaysMoveToRequest)), + new JsonDerivedType(typeof(ArchiveRequest), nameof(ArchiveRequest)), + new JsonDerivedType(typeof(ChangeFlagRequest), nameof(ChangeFlagRequest)), + new JsonDerivedType(typeof(CreateDraftRequest), nameof(CreateDraftRequest)), + new JsonDerivedType(typeof(DeleteRequest), nameof(DeleteRequest)), + new JsonDerivedType(typeof(EmptyFolderRequest), nameof(EmptyFolderRequest)), + new JsonDerivedType(typeof(MarkFolderAsReadRequest), nameof(MarkFolderAsReadRequest)), + new JsonDerivedType(typeof(MarkReadRequest), nameof(MarkReadRequest)), + new JsonDerivedType(typeof(MoveRequest), nameof(MoveRequest)), + new JsonDerivedType(typeof(MoveToFocusedRequest), nameof(MoveToFocusedRequest)), + new JsonDerivedType(typeof(RenameFolderRequest), nameof(RenameFolderRequest)), + new JsonDerivedType(typeof(SendDraftRequest), nameof(SendDraftRequest)), + } + }; + } + else if (t.Type == typeof(IMailItem)) + { + t.PolymorphismOptions = new JsonPolymorphismOptions() + { + DerivedTypes = + { + new JsonDerivedType(typeof(MailCopy), nameof(MailCopy)), + } + }; + } + })); + } + } +} diff --git a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs index 990856eb..14ebc1de 100644 --- a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs @@ -62,6 +62,15 @@ namespace Wino.Core.Integration.Processors /// 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. + /// + /// Message id + /// Folder's local id. + /// Whether mail exists in the folder or not. + Task IsMailExistsInFolderAsync(string messageId, Guid folderId); + /// /// Updates Folder's delta synchronization identifier. /// Only used in Outlook since it does per-folder sync. @@ -70,6 +79,24 @@ namespace Wino.Core.Integration.Processors /// New synchronization identifier. /// New identifier if success. Task UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string deltaSynchronizationIdentifier); + + /// + /// Outlook may expire folder's delta token after a while. + /// Recommended action for this scenario is to reset token and do full sync. + /// This method resets the token for the given folder. + /// + /// Local folder id to reset token for. + /// Empty string to assign folder delta sync for. + Task ResetFolderDeltaTokenAsync(Guid folderId); + + /// + /// Outlook may expire account's delta token after a while. + /// This will result returning 410 GONE response from the API for synchronizing folders. + /// This method resets the token for the given account for re-syncing folders. + /// + /// Account identifier to reset delta token for. + /// Empty string to assign account delta sync for. + Task ResetAccountDeltaTokenAsync(Guid accountId); } public interface IImapChangeProcessor : IDefaultChangeProcessor @@ -90,11 +117,11 @@ namespace Wino.Core.Integration.Processors protected IMailService MailService = mailService; protected IFolderService FolderService = folderService; - private readonly IAccountService _accountService = accountService; + protected IAccountService AccountService = accountService; private readonly IMimeFileService _mimeFileService = mimeFileService; public Task UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string synchronizationDeltaIdentifier) - => _accountService.UpdateSynchronizationIdentifierAsync(accountId, synchronizationDeltaIdentifier); + => AccountService.UpdateSynchronizationIdentifierAsync(accountId, synchronizationDeltaIdentifier); public Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged) => MailService.ChangeFlagStatusAsync(mailCopyId, isFlagged); diff --git a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs index 7c28f494..1c0262f6 100644 --- a/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs +++ b/Wino.Core/Integration/Processors/OutlookChangeProcessor.cs @@ -9,11 +9,29 @@ namespace Wino.Core.Integration.Processors IFolderService folderService, IMailService mailService, IAccountService accountService, - IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, accountService, mimeFileService), IOutlookChangeProcessor + IMimeFileService mimeFileService) : DefaultChangeProcessor(databaseService, folderService, mailService, accountService, mimeFileService) + , IOutlookChangeProcessor { public Task IsMailExistsAsync(string messageId) => MailService.IsMailExistsAsync(messageId); + public Task IsMailExistsInFolderAsync(string messageId, Guid folderId) + => MailService.IsMailExistsAsync(messageId, folderId); + + public Task ResetAccountDeltaTokenAsync(Guid accountId) + => AccountService.UpdateSynchronizationIdentifierAsync(accountId, null); + + public async Task ResetFolderDeltaTokenAsync(Guid folderId) + { + var folder = await FolderService.GetFolderAsync(folderId); + + folder.DeltaToken = null; + + await FolderService.UpdateFolderAsync(folder); + + return string.Empty; + } + public Task UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string synchronizationIdentifier) => Connection.ExecuteAsync("UPDATE MailItemFolder SET DeltaToken = ? WHERE Id = ?", synchronizationIdentifier, folderId); } diff --git a/Wino.Core/Messages/Mails/RefreshUnreadCountsMessage.cs b/Wino.Core/Messages/Mails/RefreshUnreadCountsMessage.cs deleted file mode 100644 index 5e06029a..00000000 --- a/Wino.Core/Messages/Mails/RefreshUnreadCountsMessage.cs +++ /dev/null @@ -1,6 +0,0 @@ -using System; - -namespace Wino.Core.Messages.Mails -{ - public record RefreshUnreadCountsMessage(Guid AccountId); -} diff --git a/Wino.Core/Messages/Synchronization/AccountSynchronizerStateChanged.cs b/Wino.Core/Messages/Synchronization/AccountSynchronizerStateChanged.cs deleted file mode 100644 index ea7db120..00000000 --- a/Wino.Core/Messages/Synchronization/AccountSynchronizerStateChanged.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Wino.Core.Domain.Enums; -using Wino.Core.Synchronizers; - -namespace Wino.Core.Messages.Synchronization -{ - /// - /// Emitted when synchronizer state is updated. - /// - /// Account Synchronizer - /// New state. - public record AccountSynchronizerStateChanged(IBaseSynchronizer Synchronizer, AccountSynchronizerState NewState); -} diff --git a/Wino.Core/Requests/ArchiveRequest.cs b/Wino.Core/Requests/ArchiveRequest.cs index f519f4d8..2cbbc2a5 100644 --- a/Wino.Core/Requests/ArchiveRequest.cs +++ b/Wino.Core/Requests/ArchiveRequest.cs @@ -7,6 +7,7 @@ using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Requests { diff --git a/Wino.Core/Requests/Bundles/ServerRequestPackage.cs b/Wino.Core/Requests/Bundles/ServerRequestPackage.cs new file mode 100644 index 00000000..e9a6cb85 --- /dev/null +++ b/Wino.Core/Requests/Bundles/ServerRequestPackage.cs @@ -0,0 +1,23 @@ +using System; +using Wino.Core.Domain.Interfaces; + +namespace Wino.Core.Domain.Models.Requests +{ + ///// + ///// Encapsulates request to queue and account for synchronizer. + ///// + ///// + ///// + ///// Prepared request for the server. + ///// Which account to execute this request for. + public class ServerRequestBundle(Guid accountId, IRequestBase request) : IClientMessage + { + public Guid AccountId { get; } = accountId; + + public IRequestBase Request { get; } = request; + } + + + //public record ServerRequestPackage(Guid AccountId, TUserActionRequestType Request) + // : ServerRequestBundle(AccountId), IClientMessage where TUserActionRequestType : IRequestBase; +} diff --git a/Wino.Core/Requests/ChangeFlagRequest.cs b/Wino.Core/Requests/ChangeFlagRequest.cs index e7259c27..840339b7 100644 --- a/Wino.Core/Requests/ChangeFlagRequest.cs +++ b/Wino.Core/Requests/ChangeFlagRequest.cs @@ -7,6 +7,7 @@ using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Requests { diff --git a/Wino.Core/Requests/CreateDraftRequest.cs b/Wino.Core/Requests/CreateDraftRequest.cs index 9887ed4a..5e3ca403 100644 --- a/Wino.Core/Requests/CreateDraftRequest.cs +++ b/Wino.Core/Requests/CreateDraftRequest.cs @@ -7,6 +7,7 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Requests { diff --git a/Wino.Core/Requests/DeleteRequest.cs b/Wino.Core/Requests/DeleteRequest.cs index f8cf966d..2d2e15f3 100644 --- a/Wino.Core/Requests/DeleteRequest.cs +++ b/Wino.Core/Requests/DeleteRequest.cs @@ -7,6 +7,7 @@ using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Requests { diff --git a/Wino.Core/Requests/EmptyFolderRequest.cs b/Wino.Core/Requests/EmptyFolderRequest.cs index 7adfdda6..6fb14faf 100644 --- a/Wino.Core/Requests/EmptyFolderRequest.cs +++ b/Wino.Core/Requests/EmptyFolderRequest.cs @@ -5,6 +5,7 @@ using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Requests { diff --git a/Wino.Core/Requests/MarkFolderAsReadRequest.cs b/Wino.Core/Requests/MarkFolderAsReadRequest.cs index 31994ac2..213a38b8 100644 --- a/Wino.Core/Requests/MarkFolderAsReadRequest.cs +++ b/Wino.Core/Requests/MarkFolderAsReadRequest.cs @@ -5,6 +5,7 @@ using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Requests { diff --git a/Wino.Core/Requests/MarkReadRequest.cs b/Wino.Core/Requests/MarkReadRequest.cs index c4f138a2..3262b45e 100644 --- a/Wino.Core/Requests/MarkReadRequest.cs +++ b/Wino.Core/Requests/MarkReadRequest.cs @@ -7,6 +7,7 @@ using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Requests { diff --git a/Wino.Core/Requests/MoveRequest.cs b/Wino.Core/Requests/MoveRequest.cs index bbae7f74..981d10ae 100644 --- a/Wino.Core/Requests/MoveRequest.cs +++ b/Wino.Core/Requests/MoveRequest.cs @@ -7,6 +7,7 @@ using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Requests { diff --git a/Wino.Core/Requests/RenameFolderRequest.cs b/Wino.Core/Requests/RenameFolderRequest.cs index 13db1553..80536e53 100644 --- a/Wino.Core/Requests/RenameFolderRequest.cs +++ b/Wino.Core/Requests/RenameFolderRequest.cs @@ -2,6 +2,7 @@ using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Requests { diff --git a/Wino.Core/Requests/SendDraftRequest.cs b/Wino.Core/Requests/SendDraftRequest.cs index 5cace856..1e62fe78 100644 --- a/Wino.Core/Requests/SendDraftRequest.cs +++ b/Wino.Core/Requests/SendDraftRequest.cs @@ -7,11 +7,12 @@ using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Requests { public record SendDraftRequest(SendDraftPreparationRequest Request) - : RequestBase(Request.MailItem, MailSynchronizerOperation.Send), + : RequestBase(Request.MailItem, MailSynchronizerOperation.Send), ICustomFolderSynchronizationRequest { public List SynchronizationFolderIds diff --git a/Wino.Core/Requests/UIMessages.cs b/Wino.Core/Requests/UIMessages.cs deleted file mode 100644 index 6373a350..00000000 --- a/Wino.Core/Requests/UIMessages.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using Wino.Core.Domain.Entities; -using Wino.Core.Domain.Models.Folders; -using Wino.Core.Domain.Models.Requests; - -namespace Wino.Core.Requests -{ - public record MailAddedMessage(MailCopy AddedMail) : IUIMessage; - public record MailRemovedMessage(MailCopy RemovedMail) : IUIMessage; - public record MailUpdatedMessage(MailCopy UpdatedMail) : IUIMessage; - public record MailDownloadedMessage(MailCopy DownloadedMail) : IUIMessage; - - public record AccountCreatedMessage(MailAccount Account) : IUIMessage; - public record AccountRemovedMessage(MailAccount Account) : IUIMessage; - public record AccountUpdatedMessage(MailAccount Account) : IUIMessage; - - public record DraftCreated(MailCopy DraftMail, MailAccount Account) : IUIMessage; - public record DraftFailed(MailCopy DraftMail, MailAccount Account) : IUIMessage; - public record DraftMapped(string LocalDraftCopyId, string RemoteDraftCopyId) : IUIMessage; - - public record MergedInboxRenamed(Guid MergedInboxId, string NewName) : IUIMessage; - - public record FolderRenamed(IMailItemFolder MailItemFolder) : IUIMessage; - public record FolderSynchronizationEnabled(IMailItemFolder MailItemFolder) : IUIMessage; -} diff --git a/Wino.Core/Services/AccountService.cs b/Wino.Core/Services/AccountService.cs index 851c22ea..63e1055b 100644 --- a/Wino.Core/Services/AccountService.cs +++ b/Wino.Core/Services/AccountService.cs @@ -11,8 +11,8 @@ using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Extensions; -using Wino.Core.Messages.Accounts; -using Wino.Core.Requests; +using Wino.Messaging.Client.Accounts; +using Wino.Messaging.UI; namespace Wino.Core.Services { @@ -382,7 +382,10 @@ namespace Wino.Core.Services if (customServerInformation != null) await Connection.InsertAsync(customServerInformation); - if (tokenInformation != null) + // Outlook token cache is managed by MSAL. + // Don't save it to database. + + if (tokenInformation != null && account.ProviderType != MailProviderType.Outlook) await Connection.InsertAsync(tokenInformation); } diff --git a/Wino.Core/Services/ApplicationConfiguration.cs b/Wino.Core/Services/ApplicationConfiguration.cs new file mode 100644 index 00000000..e920a12e --- /dev/null +++ b/Wino.Core/Services/ApplicationConfiguration.cs @@ -0,0 +1,13 @@ +using Wino.Core.Domain.Interfaces; + +namespace Wino.Core.Services +{ + public class ApplicationConfiguration : IApplicationConfiguration + { + public const string SharedFolderName = "WinoShared"; + + public string ApplicationDataFolderPath { get; set; } + + public string PublisherSharedFolderPath { get; set; } + } +} diff --git a/Wino.Core/Services/AuthenticationProvider.cs b/Wino.Core/Services/AuthenticationProvider.cs index 20c78bcc..e68be8d5 100644 --- a/Wino.Core/Services/AuthenticationProvider.cs +++ b/Wino.Core/Services/AuthenticationProvider.cs @@ -11,22 +11,23 @@ namespace Wino.Core.Services { private readonly INativeAppService _nativeAppService; private readonly ITokenService _tokenService; + private readonly IApplicationConfiguration _applicationConfiguration; - public AuthenticationProvider(INativeAppService nativeAppService, ITokenService tokenService) + public AuthenticationProvider(INativeAppService nativeAppService, ITokenService tokenService, IApplicationConfiguration applicationConfiguration) { _nativeAppService = nativeAppService; _tokenService = tokenService; + _applicationConfiguration = applicationConfiguration; } public IAuthenticator GetAuthenticator(MailProviderType providerType) { + // TODO: Move DI return providerType switch { - MailProviderType.Outlook => new OutlookAuthenticator(_tokenService, _nativeAppService), - MailProviderType.Office365 => new Office365Authenticator(_tokenService, _nativeAppService), + MailProviderType.Outlook => new OutlookAuthenticator(_tokenService, _nativeAppService, _applicationConfiguration), + MailProviderType.Office365 => new Office365Authenticator(_tokenService, _nativeAppService, _applicationConfiguration), MailProviderType.Gmail => new GmailAuthenticator(_tokenService, _nativeAppService), - MailProviderType.Yahoo => new YahooAuthenticator(_tokenService), - MailProviderType.IMAP4 => new CustomAuthenticator(_tokenService), _ => throw new ArgumentException(Translator.Exception_UnsupportedProvider), }; } diff --git a/Wino.Core/Services/BaseDatabaseService.cs b/Wino.Core/Services/BaseDatabaseService.cs index cd0136e2..18e6d9ce 100644 --- a/Wino.Core/Services/BaseDatabaseService.cs +++ b/Wino.Core/Services/BaseDatabaseService.cs @@ -1,6 +1,6 @@ using CommunityToolkit.Mvvm.Messaging; using SQLite; -using Wino.Core.Domain.Models.Requests; +using Wino.Core.Domain.Interfaces; namespace Wino.Core.Services { diff --git a/Wino.Core/Services/DatabaseService.cs b/Wino.Core/Services/DatabaseService.cs index 8abac819..246f9a63 100644 --- a/Wino.Core/Services/DatabaseService.cs +++ b/Wino.Core/Services/DatabaseService.cs @@ -14,16 +14,16 @@ namespace Wino.Core.Services public class DatabaseService : IDatabaseService { - private string DatabaseName => "Wino172.db"; + private const string DatabaseName = "Wino172.db"; private bool _isInitialized = false; - private readonly IAppInitializerService _appInitializerService; + private readonly IApplicationConfiguration _folderConfiguration; public SQLiteAsyncConnection Connection { get; private set; } - public DatabaseService(IAppInitializerService appInitializerService) + public DatabaseService(IApplicationConfiguration folderConfiguration) { - _appInitializerService = appInitializerService; + _folderConfiguration = folderConfiguration; } public async Task InitializeAsync() @@ -31,8 +31,8 @@ namespace Wino.Core.Services if (_isInitialized) return; - var applicationData = _appInitializerService.GetPublisherSharedFolder(); - var databaseFileName = Path.Combine(applicationData, DatabaseName); + var publisherCacheFolder = _folderConfiguration.PublisherSharedFolderPath; + var databaseFileName = Path.Combine(publisherCacheFolder, DatabaseName); Connection = new SQLiteAsyncConnection(databaseFileName) { @@ -45,7 +45,6 @@ namespace Wino.Core.Services }) }; - await CreateTablesAsync(); _isInitialized = true; diff --git a/Wino.Core/Services/FolderService.cs b/Wino.Core/Services/FolderService.cs index 04ffafae..87b2a6cf 100644 --- a/Wino.Core/Services/FolderService.cs +++ b/Wino.Core/Services/FolderService.cs @@ -16,7 +16,7 @@ using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Extensions; using Wino.Core.MenuItems; -using Wino.Core.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Services { diff --git a/Wino.Core/Services/ImapTestService.cs b/Wino.Core/Services/ImapTestService.cs index b9cc54bf..ae7f92c3 100644 --- a/Wino.Core/Services/ImapTestService.cs +++ b/Wino.Core/Services/ImapTestService.cs @@ -11,11 +11,11 @@ namespace Wino.Core.Services public const string ProtocolLogFileName = "ImapProtocolLog.log"; private readonly IPreferencesService _preferencesService; - private readonly IAppInitializerService _appInitializerService; + private readonly IApplicationConfiguration _appInitializerService; private Stream _protocolLogStream; - public ImapTestService(IPreferencesService preferencesService, IAppInitializerService appInitializerService) + public ImapTestService(IPreferencesService preferencesService, IApplicationConfiguration appInitializerService) { _preferencesService = preferencesService; _appInitializerService = appInitializerService; @@ -24,7 +24,7 @@ namespace Wino.Core.Services private void EnsureProtocolLogFileExists() { // Create new file for protocol logger. - var localAppFolderPath = _appInitializerService.GetApplicationDataFolder(); + var localAppFolderPath = _appInitializerService.ApplicationDataFolderPath; var logFile = Path.Combine(localAppFolderPath, ProtocolLogFileName); diff --git a/Wino.Core/Services/LogInitializer.cs b/Wino.Core/Services/LogInitializer.cs index 624723c6..10fc7e1e 100644 --- a/Wino.Core/Services/LogInitializer.cs +++ b/Wino.Core/Services/LogInitializer.cs @@ -1,5 +1,4 @@ -using System.IO; -using Serilog; +using Serilog; using Serilog.Core; using Serilog.Exceptions; using Wino.Core.Domain.Interfaces; @@ -8,8 +7,6 @@ namespace Wino.Core.Services { public class LogInitializer : ILogInitializer { - public const string WinoLogFileName = "WinoDiagnostics.log"; - private readonly LoggingLevelSwitch _levelSwitch = new LoggingLevelSwitch(); private readonly IPreferencesService _preferencesService; @@ -25,13 +22,11 @@ namespace Wino.Core.Services _levelSwitch.MinimumLevel = _preferencesService.IsLoggingEnabled ? Serilog.Events.LogEventLevel.Debug : Serilog.Events.LogEventLevel.Fatal; } - public void SetupLogger(string logFolderPath) + public void SetupLogger(string fullLogFilePath) { - string logFilePath = Path.Combine(logFolderPath, WinoLogFileName); - Log.Logger = new LoggerConfiguration() .MinimumLevel.ControlledBy(_levelSwitch) - .WriteTo.File(logFilePath) + .WriteTo.File(fullLogFilePath, retainedFileCountLimit: 3, rollOnFileSizeLimit: true, rollingInterval: RollingInterval.Day) .WriteTo.Debug() .Enrich.FromLogContext() .Enrich.WithExceptionDetails() diff --git a/Wino.Core/Services/MailService.cs b/Wino.Core/Services/MailService.cs index f6c64974..81e6f981 100644 --- a/Wino.Core/Services/MailService.cs +++ b/Wino.Core/Services/MailService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.Kiota.Abstractions.Extensions; @@ -10,11 +11,12 @@ using SqlKata; using Wino.Core.Domain; using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Extensions; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Comparers; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Extensions; -using Wino.Core.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Services { @@ -52,10 +54,12 @@ namespace Wino.Core.Services } public async Task CreateDraftAsync(MailAccount composerAccount, - MimeMessage createdDraftMimeMessage, + string generatedReplyMimeMessageBase64, MimeMessage replyingMimeMessage = null, IMailItem replyingMailItem = null) { + var createdDraftMimeMessage = generatedReplyMimeMessageBase64.GetMimeMessageFromBase64(); + bool isImapAccount = composerAccount.ServerInformation != null; string fromName; @@ -625,7 +629,7 @@ namespace Wino.Core.Services } } - public async Task CreateDraftMimeMessageAsync(Guid accountId, DraftCreationOptions draftCreationOptions) + public async Task CreateDraftMimeBase64Async(Guid accountId, DraftCreationOptions draftCreationOptions) { // This unique id is stored in mime headers for Wino to identify remote message with local copy. // Same unique id will be used for the local copy as well. @@ -796,7 +800,14 @@ namespace Wino.Core.Services // Update TextBody from existing HtmlBody if exists. } - return message; + using MemoryStream memoryStream = new(); + message.WriteTo(FormatOptions.Default, memoryStream); + byte[] buffer = memoryStream.GetBuffer(); + int count = (int)memoryStream.Length; + + return Convert.ToBase64String(buffer); + + // return message; // Generates html representation of To/Cc/From/Time and so on from referenced message. string CreateHtmlForReferencingMessage(MimeMessage referenceMessage) @@ -919,5 +930,8 @@ namespace Wino.Core.Services public Task IsMailExistsAsync(string mailCopyId) => Connection.ExecuteScalarAsync("SELECT EXISTS(SELECT 1 FROM MailCopy WHERE Id = ?)", mailCopyId); + + public Task IsMailExistsAsync(string mailCopyId, Guid folderId) + => Connection.ExecuteScalarAsync("SELECT EXISTS(SELECT 1 FROM MailCopy WHERE Id = ? AND FolderId = ?)", mailCopyId, folderId); } } diff --git a/Wino.Core/Services/SynchronizerFactory.cs b/Wino.Core/Services/SynchronizerFactory.cs new file mode 100644 index 00000000..4aa509a6 --- /dev/null +++ b/Wino.Core/Services/SynchronizerFactory.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Wino.Core.Domain.Entities; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Integration.Processors; +using Wino.Core.Synchronizers; + +namespace Wino.Core.Services +{ + public class SynchronizerFactory : ISynchronizerFactory + { + private bool isInitialized = false; + + private readonly IAccountService _accountService; + private readonly IOutlookChangeProcessor _outlookChangeProcessor; + private readonly IGmailChangeProcessor _gmailChangeProcessor; + private readonly IImapChangeProcessor _imapChangeProcessor; + private readonly IOutlookAuthenticator _outlookAuthenticator; + private readonly IGmailAuthenticator _gmailAuthenticator; + + private readonly List synchronizerCache = new(); + + public SynchronizerFactory(IOutlookChangeProcessor outlookChangeProcessor, + IGmailChangeProcessor gmailChangeProcessor, + IImapChangeProcessor imapChangeProcessor, + IOutlookAuthenticator outlookAuthenticator, + IGmailAuthenticator gmailAuthenticator, + IAccountService accountService) + { + _outlookChangeProcessor = outlookChangeProcessor; + _gmailChangeProcessor = gmailChangeProcessor; + _imapChangeProcessor = imapChangeProcessor; + _outlookAuthenticator = outlookAuthenticator; + _gmailAuthenticator = gmailAuthenticator; + _accountService = accountService; + } + + public async Task GetAccountSynchronizerAsync(Guid accountId) + { + var synchronizer = synchronizerCache.Find(a => a.Account.Id == accountId); + + if (synchronizer == null) + { + var account = await _accountService.GetAccountAsync(accountId); + + if (account != null) + { + synchronizer = CreateNewSynchronizer(account); + + return await GetAccountSynchronizerAsync(accountId); + } + } + + return synchronizer; + } + + private IBaseSynchronizer CreateIntegratorWithDefaultProcessor(MailAccount mailAccount) + { + var providerType = mailAccount.ProviderType; + + switch (providerType) + { + case Domain.Enums.MailProviderType.Outlook: + case Domain.Enums.MailProviderType.Office365: + return new OutlookSynchronizer(mailAccount, _outlookAuthenticator, _outlookChangeProcessor); + case Domain.Enums.MailProviderType.Gmail: + return new GmailSynchronizer(mailAccount, _gmailAuthenticator, _gmailChangeProcessor); + case Domain.Enums.MailProviderType.IMAP4: + return new ImapSynchronizer(mailAccount, _imapChangeProcessor); + default: + break; + } + + return null; + } + + public IBaseSynchronizer CreateNewSynchronizer(MailAccount account) + { + var synchronizer = CreateIntegratorWithDefaultProcessor(account); + + synchronizerCache.Add(synchronizer); + + return synchronizer; + } + + public async Task InitializeAsync() + { + if (isInitialized) return; + + var accounts = await _accountService.GetAccountsAsync(); + + foreach (var account in accounts) + { + CreateNewSynchronizer(account); + } + + isInitialized = true; + } + } +} diff --git a/Wino.Core/Services/TranslationService.cs b/Wino.Core/Services/TranslationService.cs index aea449fc..a6007407 100644 --- a/Wino.Core/Services/TranslationService.cs +++ b/Wino.Core/Services/TranslationService.cs @@ -8,7 +8,7 @@ using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Translations; -using Wino.Core.Messages.Shell; +using Wino.Messaging.Client.Shell; namespace Wino.Core.Services { diff --git a/Wino.Core/Services/WinoRequestDelegator.cs b/Wino.Core/Services/WinoRequestDelegator.cs index af1bd00e..6a270239 100644 --- a/Wino.Core/Services/WinoRequestDelegator.cs +++ b/Wino.Core/Services/WinoRequestDelegator.cs @@ -11,26 +11,26 @@ using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; -using Wino.Core.Messages.Synchronization; using Wino.Core.Requests; +using Wino.Messaging.Server; namespace Wino.Core.Services { public class WinoRequestDelegator : IWinoRequestDelegator { private readonly IWinoRequestProcessor _winoRequestProcessor; - private readonly IWinoSynchronizerFactory _winoSynchronizerFactory; + private readonly IWinoServerConnectionManager _winoServerConnectionManager; private readonly IFolderService _folderService; private readonly IDialogService _dialogService; private readonly ILogger _logger = Log.ForContext(); public WinoRequestDelegator(IWinoRequestProcessor winoRequestProcessor, - IWinoSynchronizerFactory winoSynchronizerFactory, + IWinoServerConnectionManager winoServerConnectionManager, IFolderService folderService, IDialogService dialogService) { _winoRequestProcessor = winoRequestProcessor; - _winoSynchronizerFactory = winoSynchronizerFactory; + _winoServerConnectionManager = winoServerConnectionManager; _folderService = folderService; _dialogService = dialogService; } @@ -77,7 +77,7 @@ namespace Wino.Core.Services { foreach (var accountRequest in accountId) { - QueueRequest(accountRequest, accountId.Key); + await QueueRequestAsync(accountRequest, accountId.Key); } QueueSynchronization(accountId.Key); @@ -107,43 +107,36 @@ namespace Wino.Core.Services if (request == null) return; - QueueRequest(request, accountId); + await QueueRequestAsync(request, accountId); QueueSynchronization(accountId); } - public Task ExecuteAsync(DraftPreperationRequest draftPreperationRequest) + public async Task ExecuteAsync(DraftPreperationRequest draftPreperationRequest) { var request = new CreateDraftRequest(draftPreperationRequest); - QueueRequest(request, draftPreperationRequest.Account.Id); + await QueueRequestAsync(request, draftPreperationRequest.Account.Id); QueueSynchronization(draftPreperationRequest.Account.Id); - - return Task.CompletedTask; } - public Task ExecuteAsync(SendDraftPreparationRequest sendDraftPreperationRequest) + public async Task ExecuteAsync(SendDraftPreparationRequest sendDraftPreperationRequest) { var request = new SendDraftRequest(sendDraftPreperationRequest); - QueueRequest(request, sendDraftPreperationRequest.MailItem.AssignedAccount.Id); + await QueueRequestAsync(request, sendDraftPreperationRequest.MailItem.AssignedAccount.Id); QueueSynchronization(sendDraftPreperationRequest.MailItem.AssignedAccount.Id); - - return Task.CompletedTask; } - private void QueueRequest(IRequestBase request, Guid accountId) + private async Task QueueRequestAsync(IRequestBase request, Guid accountId) { - var synchronizer = _winoSynchronizerFactory.GetAccountSynchronizer(accountId); - - if (synchronizer == null) + try { - _logger.Warning("Synchronizer not found for account {AccountId}.", accountId); - _logger.Warning("Skipping queueing request {Operation}.", request.Operation); - - return; + await _winoServerConnectionManager.QueueRequestAsync(request, accountId); + } + catch (WinoServerException serverException) + { + _dialogService.InfoBarMessage("", serverException.Message, InfoBarMessageType.Error); } - - synchronizer.QueueRequest(request); } private void QueueSynchronization(Guid accountId) @@ -154,7 +147,7 @@ namespace Wino.Core.Services Type = SynchronizationType.ExecuteRequests }; - WeakReferenceMessenger.Default.Send(new NewSynchronizationRequested(options)); + WeakReferenceMessenger.Default.Send(new NewSynchronizationRequested(options, SynchronizationSource.Client)); } } } diff --git a/Wino.Core/Services/WinoRequestProcessor.cs b/Wino.Core/Services/WinoRequestProcessor.cs index ffcf3f01..4e54f788 100644 --- a/Wino.Core/Services/WinoRequestProcessor.cs +++ b/Wino.Core/Services/WinoRequestProcessor.cs @@ -9,7 +9,6 @@ using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; -using Wino.Core.Domain.Models.Requests; using Wino.Core.Requests; namespace Wino.Core.Services @@ -92,6 +91,7 @@ namespace Wino.Core.Services var requests = new List(); + // TODO: Fix: Collection was modified; enumeration operation may not execute foreach (var item in preperationRequest.MailItems) { var singleRequest = await GetSingleRequestAsync(item, action, moveTargetStructure, preperationRequest.ToggleExecution); diff --git a/Wino.Core/Synchronizers/BaseSynchronizer.cs b/Wino.Core/Synchronizers/BaseSynchronizer.cs index 246c432f..acd7f213 100644 --- a/Wino.Core/Synchronizers/BaseSynchronizer.cs +++ b/Wino.Core/Synchronizers/BaseSynchronizer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,57 +15,12 @@ using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Synchronization; using Wino.Core.Integration; -using Wino.Core.Messages.Mails; -using Wino.Core.Messages.Synchronization; using Wino.Core.Misc; using Wino.Core.Requests; +using Wino.Messaging.UI; namespace Wino.Core.Synchronizers { - public interface IBaseSynchronizer - { - /// - /// Account that is assigned for this synchronizer. - /// - MailAccount Account { get; } - - /// - /// Synchronizer state. - /// - AccountSynchronizerState State { get; } - - /// - /// Queues a single request to be executed in the next synchronization. - /// - /// Request to queue. - void QueueRequest(IRequestBase request); - - /// - /// TODO - /// - /// Whether active synchronization is stopped or not. - bool CancelActiveSynchronization(); - - /// - /// Performs a full synchronization with the server with given options. - /// This will also prepares batch requests for execution. - /// Requests are executed in the order they are queued and happens before the synchronization. - /// Result of the execution queue is processed during the synchronization. - /// - /// Options for synchronization. - /// Cancellation token. - /// Result summary of synchronization. - Task SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default); - - /// - /// Downloads a single MIME message from the server and saves it to disk. - /// - /// Mail item to download from server. - /// Optional progress reporting for download operation. - /// Cancellation token. - Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default); - } - public abstract class BaseSynchronizer : BaseMailIntegrator, IBaseSynchronizer { private SemaphoreSlim synchronizationSemaphore = new(1); @@ -88,7 +44,7 @@ namespace Wino.Core.Synchronizers { state = value; - WeakReferenceMessenger.Default.Send(new AccountSynchronizerStateChanged(this, value)); + WeakReferenceMessenger.Default.Send(new AccountSynchronizerStateChanged(Account.Id, value)); } } @@ -166,18 +122,21 @@ namespace Wino.Core.Synchronizers } catch (OperationCanceledException) { - Logger.Warning("Synchronization cancelled."); + Logger.Warning("Synchronization canceled."); + return SynchronizationResult.Canceled; } - catch (Exception) + catch (Exception ex) { - // Disable maybe? + Logger.Error(ex, "Synchronization failed for {Name}", Account.Name); + Debugger.Break(); + throw; } finally { // Reset account progress to hide the progress. - options.ProgressListener?.AccountProgressUpdated(Account.Id, 0); + PublishSynchronizationProgress(0); State = AccountSynchronizerState.Idle; synchronizationSemaphore.Release(); @@ -191,6 +150,9 @@ namespace Wino.Core.Synchronizers private void PublishUnreadItemChanges() => WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(Account.Id)); + public void PublishSynchronizationProgress(double progress) + => WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress)); + /// /// 1. Group all requests by operation type. /// 2. Group all individual operation type requests with equality check. @@ -302,20 +264,31 @@ namespace Wino.Core.Synchronizers /// New synchronization options with minimal HTTP effort. private SynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(IEnumerable requests) { - bool isAllCustomSynchronizationRequests = requests.All(a => a is ICustomFolderSynchronizationRequest); + List synchronizationFolderIds = new(); + + if (requests.All(a => a is IBatchChangeRequest)) + { + var requestsInsideBatches = requests.Cast().SelectMany(b => b.Items); + + // Gather FolderIds to synchronize. + synchronizationFolderIds = requestsInsideBatches + .Where(a => a is ICustomFolderSynchronizationRequest) + .Cast() + .SelectMany(a => a.SynchronizationFolderIds) + .ToList(); + } var options = new SynchronizationOptions() { AccountId = Account.Id, - Type = SynchronizationType.FoldersOnly }; - if (isAllCustomSynchronizationRequests) + if (synchronizationFolderIds.Count > 0) { // Gather FolderIds to synchronize. options.Type = SynchronizationType.Custom; - options.SynchronizationFolderIds = requests.Cast().SelectMany(a => a.SynchronizationFolderIds).ToList(); + options.SynchronizationFolderIds = synchronizationFolderIds; } else { diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs index c100366d..3ee7e044 100644 --- a/Wino.Core/Synchronizers/GmailSynchronizer.cs +++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs @@ -212,7 +212,7 @@ namespace Wino.Core.Synchronizers } // Start downloading missing messages. - await BatchDownloadMessagesAsync(missingMessageIds, options.ProgressListener, cancellationToken).ConfigureAwait(false); + await BatchDownloadMessagesAsync(missingMessageIds, cancellationToken).ConfigureAwait(false); // Map remote drafts to local drafts. await MapDraftIdsAsync(cancellationToken).ConfigureAwait(false); @@ -353,7 +353,7 @@ namespace Wino.Core.Synchronizers /// /// Gmail message ids to download. /// Cancellation token. - private async Task BatchDownloadMessagesAsync(IEnumerable messageIds, ISynchronizationProgress progressListener = null, CancellationToken cancellationToken = default) + private async Task BatchDownloadMessagesAsync(IEnumerable messageIds, CancellationToken cancellationToken = default) { var totalDownloadCount = messageIds.Count(); @@ -396,7 +396,7 @@ namespace Wino.Core.Synchronizers var progressValue = downloadedItemCount * 100 / Math.Max(1, totalDownloadCount); - progressListener?.AccountProgressUpdated(Account.Id, progressValue); + PublishSynchronizationProgress(progressValue); }); }); diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs index 99cf586d..71ba0b90 100644 --- a/Wino.Core/Synchronizers/ImapSynchronizer.cs +++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs @@ -407,21 +407,19 @@ namespace Wino.Core.Synchronizers public override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) { - // options.Type = SynchronizationType.FoldersOnly; - var downloadedMessageIds = new List(); _logger.Information("Internal synchronization started for {Name}", Account.Name); _logger.Information("Options: {Options}", options); - options.ProgressListener?.AccountProgressUpdated(Account.Id, 1); + PublishSynchronizationProgress(1); - // Only do folder sync for these types. - // Opening folder and checking their UidValidity is slow. - // Therefore this should be avoided as many times as possible. + bool shouldDoFolderSync = options.Type == SynchronizationType.Full || options.Type == SynchronizationType.FoldersOnly; - // This may create some inconsistencies, but nothing we can do... - await SynchronizeFoldersAsync(cancellationToken).ConfigureAwait(false); + if (shouldDoFolderSync) + { + await SynchronizeFoldersAsync(cancellationToken).ConfigureAwait(false); + } if (options.Type != SynchronizationType.FoldersOnly) { @@ -432,14 +430,14 @@ namespace Wino.Core.Synchronizers var folder = synchronizationFolders[i]; var progress = (int)Math.Round((double)(i + 1) / synchronizationFolders.Count * 100); - options.ProgressListener?.AccountProgressUpdated(Account.Id, progress); + PublishSynchronizationProgress(progress); var folderDownloadedMessageIds = await SynchronizeFolderInternalAsync(folder, cancellationToken).ConfigureAwait(false); downloadedMessageIds.AddRange(folderDownloadedMessageIds); } } - options.ProgressListener?.AccountProgressUpdated(Account.Id, 100); + PublishSynchronizationProgress(100); // Get all unread new downloaded items and return in the result. // This is primarily used in notifications. @@ -943,7 +941,12 @@ namespace Wino.Core.Synchronizers foreach (var mailPackage in createdMailPackages) { - await _imapChangeProcessor.CreateMailAsync(Account.Id, mailPackage).ConfigureAwait(false); + bool isCreated = await _imapChangeProcessor.CreateMailAsync(Account.Id, mailPackage).ConfigureAwait(false); + + if (isCreated) + { + downloadedMessageIds.Add(mailPackage.Copy.Id); + } } } } diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs index adec003d..554e9dcf 100644 --- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs +++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs @@ -13,6 +13,8 @@ using Microsoft.Graph; using Microsoft.Graph.Models; using Microsoft.Kiota.Abstractions; using Microsoft.Kiota.Abstractions.Authentication; +using Microsoft.Kiota.Http.HttpClientLibrary.Middleware; +using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options; using MimeKit; using MoreLinq.Extensions; using Serilog; @@ -73,19 +75,59 @@ namespace Wino.Core.Synchronizers { var tokenProvider = new MicrosoftTokenProvider(Account, authenticator); - // Add immutable id preffered client. + // Update request handlers for Graph client. var handlers = GraphClientFactory.CreateDefaultHandlers(); - handlers.Add(new MicrosoftImmutableIdHandler()); + + handlers.Add(GetMicrosoftImmutableIdHandler()); + + // Remove existing RetryHandler and add a new one with custom options. + var existingRetryHandler = handlers.FirstOrDefault(a => a is RetryHandler); + if (existingRetryHandler != null) + handlers.Remove(existingRetryHandler); + + // Add custom one. + handlers.Add(GetRetryHandler()); var httpClient = GraphClientFactory.Create(handlers); - _graphClient = new GraphServiceClient(httpClient, new BaseBearerTokenAuthenticationProvider(tokenProvider)); + _outlookChangeProcessor = outlookChangeProcessor; // Specify to use TLS 1.2 as default connection - System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; } + #region MS Graph Handlers + + private MicrosoftImmutableIdHandler GetMicrosoftImmutableIdHandler() => new(); + + private RetryHandler GetRetryHandler() + { + var options = new RetryHandlerOption() + { + ShouldRetry = (delay, attempt, httpResponse) => + { + var statusCode = httpResponse.StatusCode; + + return statusCode switch + { + HttpStatusCode.ServiceUnavailable => true, + HttpStatusCode.GatewayTimeout => true, + (HttpStatusCode)429 => true, + HttpStatusCode.Unauthorized => true, + _ => false + }; + }, + Delay = 3, + MaxRetry = 3 + }; + + return new RetryHandler(options); + } + + #endregion + + public override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default) { var downloadedMessageIds = new List(); @@ -95,7 +137,7 @@ namespace Wino.Core.Synchronizers try { - options.ProgressListener?.AccountProgressUpdated(Account.Id, 1); + PublishSynchronizationProgress(1); await SynchronizeFoldersAsync(cancellationToken).ConfigureAwait(false); @@ -111,7 +153,7 @@ namespace Wino.Core.Synchronizers var folder = synchronizationFolders[i]; var progress = (int)Math.Round((double)(i + 1) / synchronizationFolders.Count * 100); - options.ProgressListener?.AccountProgressUpdated(Account.Id, progress); + PublishSynchronizationProgress(progress); var folderDownloadedMessageIds = await SynchronizeFolderAsync(folder, cancellationToken).ConfigureAwait(false); downloadedMessageIds.AddRange(folderDownloadedMessageIds); @@ -120,13 +162,14 @@ namespace Wino.Core.Synchronizers } catch (Exception ex) { - _logger.Error(ex, "Synchronization failed for {Name}", Account.Name); + _logger.Error(ex, "Synchronizing folders for {Name}", Account.Name); + Debugger.Break(); throw; } finally { - options.ProgressListener?.AccountProgressUpdated(Account.Id, 100); + PublishSynchronizationProgress(100); } // Get all unred new downloaded items and return in the result. @@ -238,20 +281,12 @@ namespace Wino.Core.Synchronizers private bool IsResourceDeleted(IDictionary additionalData) => additionalData != null && additionalData.ContainsKey("@removed"); - private bool IsResourceUpdated(IDictionary additionalData) - => additionalData == null || !additionalData.Any(); - private async Task HandleFolderRetrievedAsync(MailFolder folder, OutlookSpecialFolderIdInformation outlookSpecialFolderIdInformation, CancellationToken cancellationToken = default) { if (IsResourceDeleted(folder.AdditionalData)) { await _outlookChangeProcessor.DeleteFolderAsync(Account.Id, folder.Id).ConfigureAwait(false); } - else if (IsResourceUpdated(folder.AdditionalData)) - { - // TODO - Debugger.Break(); - } else { // New folder created. @@ -297,38 +332,45 @@ namespace Wino.Core.Synchronizers await _outlookChangeProcessor.DeleteAssignmentAsync(Account.Id, item.Id, folder.RemoteFolderId).ConfigureAwait(false); } - else if (IsResourceUpdated(item.AdditionalData)) - { - // Some of the properties of the item are updated. - - if (item.IsRead != null) - { - await _outlookChangeProcessor.ChangeMailReadStatusAsync(item.Id, item.IsRead.GetValueOrDefault()).ConfigureAwait(false); - } - - if (item.Flag?.FlagStatus != null) - { - await _outlookChangeProcessor.ChangeFlagStatusAsync(item.Id, item.Flag.FlagStatus.GetValueOrDefault() == FollowupFlagStatus.Flagged) - .ConfigureAwait(false); - } - } else { - // Package may return null on some cases mapping the remote draft to existing local draft. + // If the item exists in the local database, it means that it's already downloaded. Process as an Update. - var newMailPackages = await CreateNewMailPackagesAsync(item, folder, cancellationToken); + var isMailExists = await _outlookChangeProcessor.IsMailExistsInFolderAsync(item.Id, folder.Id); - if (newMailPackages != null) + if (isMailExists) { - foreach (var package in newMailPackages) - { - // Only add to downloaded message ids if it's inserted successfuly. - // Updates should not be added to the list because they are not new. - bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false); + // Some of the properties of the item are updated. - if (isInserted) + if (item.IsRead != null) + { + await _outlookChangeProcessor.ChangeMailReadStatusAsync(item.Id, item.IsRead.GetValueOrDefault()).ConfigureAwait(false); + } + + if (item.Flag?.FlagStatus != null) + { + await _outlookChangeProcessor.ChangeFlagStatusAsync(item.Id, item.Flag.FlagStatus.GetValueOrDefault() == FollowupFlagStatus.Flagged) + .ConfigureAwait(false); + } + } + else + { + // Package may return null on some cases mapping the remote draft to existing local draft. + + var newMailPackages = await CreateNewMailPackagesAsync(item, folder, cancellationToken); + + if (newMailPackages != null) + { + foreach (var package in newMailPackages) { - downloadedMessageIds.Add(package.Copy.Id); + // Only add to downloaded message ids if it's inserted successfuly. + // Updates should not be added to the list because they are not new. + bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false); + + if (isInserted) + { + downloadedMessageIds.Add(package.Copy.Id); + } } } } @@ -339,11 +381,12 @@ namespace Wino.Core.Synchronizers private async Task SynchronizeFoldersAsync(CancellationToken cancellationToken = default) { - // Gather special folders by default. - // Others will be other type. + // Gather special folders by default. + // Others will be other type. - // Get well known folder ids by batch. + // Get well known folder ids by batch. + retry: var wellKnownFolderIdBatch = new BatchRequestContentCollection(_graphClient); var inboxRequest = _graphClient.Me.MailFolders[INBOX_NAME].ToGetRequestInformation((t) => { t.QueryParameters.Select = ["id"]; }); @@ -394,9 +437,19 @@ namespace Wino.Core.Synchronizers deltaRequest.UrlTemplate = deltaRequest.UrlTemplate.Insert(deltaRequest.UrlTemplate.Length - 1, ",%24deltaToken"); deltaRequest.QueryParameters.Add("%24deltaToken", currentDeltaLink); - graphFolders = await _graphClient.RequestAdapter.SendAsync(deltaRequest, + + try + { + graphFolders = await _graphClient.RequestAdapter.SendAsync(deltaRequest, Microsoft.Graph.Me.MailFolders.Delta.DeltaGetResponse.CreateFromDiscriminatorValue, cancellationToken: cancellationToken).ConfigureAwait(false); + } + catch (ApiException apiException) when (apiException.ResponseStatusCode == 410) + { + Account.SynchronizationDeltaIdentifier = await _outlookChangeProcessor.ResetAccountDeltaTokenAsync(Account.Id); + + goto retry; + } } var iterator = PageIterator.CreatePageIterator(_graphClient, graphFolders, (folder) => @@ -686,6 +739,8 @@ namespace Wino.Core.Synchronizers HttpResponseMessage httpResponseMessage, CancellationToken cancellationToken = default) { + if (httpResponseMessage == null) return; + if (!httpResponseMessage.IsSuccessStatusCode) { throw new SynchronizerException(string.Format(Translator.Exception_SynchronizerFailureHTTP, httpResponseMessage.StatusCode)); diff --git a/Wino.Core/Wino.Core.csproj b/Wino.Core/Wino.Core.csproj index 5a83fd36..a975630d 100644 --- a/Wino.Core/Wino.Core.csproj +++ b/Wino.Core/Wino.Core.csproj @@ -5,8 +5,13 @@ Wino.Core Debug;Release 12 + AnyCPU;x64;x86 + + + + @@ -18,11 +23,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + + + + @@ -33,10 +40,10 @@ - + diff --git a/Wino.Core/WinoSynchronizerFactory.cs b/Wino.Core/WinoSynchronizerFactory.cs index d01b7c86..89c34409 100644 --- a/Wino.Core/WinoSynchronizerFactory.cs +++ b/Wino.Core/WinoSynchronizerFactory.cs @@ -10,13 +10,6 @@ using Wino.Core.Synchronizers; namespace Wino.Core { - public interface IWinoSynchronizerFactory : IInitializeAsync - { - IBaseSynchronizer GetAccountSynchronizer(Guid accountId); - IBaseSynchronizer CreateNewSynchronizer(MailAccount account); - void DeleteSynchronizer(MailAccount account); - } - /// /// Factory that keeps track of all integrator with associated mail accounts. /// Synchronizer per-account makes sense because re-generating synchronizers are not ideal. @@ -82,7 +75,6 @@ namespace Wino.Core return new OutlookSynchronizer(mailAccount, outlookAuthenticator, _outlookChangeProcessor); case Domain.Enums.MailProviderType.Gmail: var gmailAuthenticator = new GmailAuthenticator(_tokenService, _nativeAppService); - return new GmailSynchronizer(mailAccount, gmailAuthenticator, _gmailChangeProcessor); case Domain.Enums.MailProviderType.Office365: break; diff --git a/Wino.Mail.ViewModels/AboutPageViewModel.cs b/Wino.Mail.ViewModels/AboutPageViewModel.cs index 3b363854..36987a56 100644 --- a/Wino.Mail.ViewModels/AboutPageViewModel.cs +++ b/Wino.Mail.ViewModels/AboutPageViewModel.cs @@ -12,7 +12,7 @@ namespace Wino.Mail.ViewModels { private readonly IStoreRatingService _storeRatingService; private readonly INativeAppService _nativeAppService; - private readonly IAppInitializerService _appInitializerService; + private readonly IApplicationConfiguration _appInitializerService; private readonly IFileService _fileService; private readonly ILogInitializer _logInitializer; @@ -31,7 +31,7 @@ namespace Wino.Mail.ViewModels IDialogService dialogService, INativeAppService nativeAppService, IPreferencesService preferencesService, - IAppInitializerService appInitializerService, + IApplicationConfiguration appInitializerService, IFileService fileService, ILogInitializer logInitializer) : base(dialogService) { @@ -72,12 +72,13 @@ namespace Wino.Mail.ViewModels private Task ShareProtocolLogAsync() => SaveLogInternalAsync(ImapTestService.ProtocolLogFileName); + private Task ShareWinoLogAsync() - => SaveLogInternalAsync(LogInitializer.WinoLogFileName); + => SaveLogInternalAsync(Constants.ClientLogFile); private async Task SaveLogInternalAsync(string sourceFileName) { - var appDataFolder = _appInitializerService.GetApplicationDataFolder(); + var appDataFolder = _appInitializerService.ApplicationDataFolderPath; var logFile = Path.Combine(appDataFolder, sourceFileName); diff --git a/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs b/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs index 8f373065..eb8e0fc0 100644 --- a/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs +++ b/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs @@ -5,21 +5,19 @@ using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; -using Wino.Core; using Wino.Core.Domain; using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Navigation; -using Wino.Core.Messages.Navigation; -using Wino.Core.Requests; +using Wino.Messaging.Client.Navigation; +using Wino.Messaging.UI; namespace Wino.Mail.ViewModels { public partial class AccountDetailsPageViewModel : BaseViewModel { - private readonly IWinoSynchronizerFactory _synchronizerFactory; private readonly IAccountService _accountService; private readonly IFolderService _folderService; @@ -45,11 +43,9 @@ namespace Wino.Mail.ViewModels public AccountDetailsPageViewModel(IDialogService dialogService, - IWinoSynchronizerFactory synchronizerFactory, IAccountService accountService, IFolderService folderService) : base(dialogService) { - _synchronizerFactory = synchronizerFactory; _accountService = accountService; _folderService = folderService; } @@ -99,10 +95,7 @@ namespace Wino.Mail.ViewModels await _accountService.DeleteAccountAsync(Account); - _synchronizerFactory.DeleteSynchronizer(Account); - - // TODO: Clear existing requests. - // _synchronizationWorker.ClearRequests(Account.Id); + // TODO: Server: Cancel ongoing calls from server for this account. DialogService.InfoBarMessage(Translator.Info_AccountDeletedTitle, string.Format(Translator.Info_AccountDeletedMessage, Account.Name), InfoBarMessageType.Success); diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs index dfdd5a09..e503b99b 100644 --- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs +++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs @@ -17,10 +17,11 @@ using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Store; using Wino.Core.Domain.Models.Synchronization; -using Wino.Core.Messages.Authorization; -using Wino.Core.Messages.Navigation; -using Wino.Core.Requests; using Wino.Mail.ViewModels.Data; +using Wino.Messaging.Client.Authorization; +using Wino.Messaging.Client.Navigation; +using Wino.Messaging.Server; +using Wino.Messaging.UI; namespace Wino.Mail.ViewModels { @@ -35,7 +36,7 @@ namespace Wino.Mail.ViewModels private readonly IStoreManagementService _storeManagementService; private readonly IPreferencesService _preferencesService; private readonly IAuthenticationProvider _authenticationProvider; - private readonly IWinoSynchronizerFactory _synchronizerFactory; + private readonly IWinoServerConnectionManager _winoServerConnectionManager; public ObservableCollection Accounts { get; set; } = []; @@ -60,22 +61,22 @@ namespace Wino.Mail.ViewModels public AccountManagementViewModel(IDialogService dialogService, IWinoNavigationService navigationService, - IWinoSynchronizerFactory synchronizerFactory, IAccountService accountService, IProviderService providerService, IFolderService folderService, IStoreManagementService storeManagementService, IPreferencesService preferencesService, - IAuthenticationProvider authenticationProvider) : base(dialogService) + IAuthenticationProvider authenticationProvider, + IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService) { _accountService = accountService; - _synchronizerFactory = synchronizerFactory; _dialogService = dialogService; _providerService = providerService; _folderService = folderService; _storeManagementService = storeManagementService; _preferencesService = preferencesService; _authenticationProvider = authenticationProvider; + _winoServerConnectionManager = winoServerConnectionManager; } [RelayCommand] @@ -153,7 +154,7 @@ namespace Wino.Mail.ViewModels { creationDialog = _dialogService.GetAccountCreationDialog(accountCreationDialogResult.ProviderType); - _accountService.ExternalAuthenticationAuthenticator = _authenticationProvider.GetAuthenticator(accountCreationDialogResult.ProviderType); + // _accountService.ExternalAuthenticationAuthenticator = _authenticationProvider.GetAuthenticator(accountCreationDialogResult.ProviderType); CustomServerInformation customServerInformation = null; @@ -193,9 +194,13 @@ namespace Wino.Mail.ViewModels { // For OAuth authentications, we just generate token and assign it to the MailAccount. - tokenInformation = await _accountService.ExternalAuthenticationAuthenticator.GenerateTokenAsync(createdAccount, false) - ?? throw new AuthenticationException(Translator.Exception_TokenInfoRetrivalFailed); + var tokenInformationResponse = await _winoServerConnectionManager.GetResponseAsync(new AuthorizationRequested(accountCreationDialogResult.ProviderType, createdAccount)); + tokenInformationResponse.ThrowIfFailed(); + + // ?? throw new AuthenticationException(Translator.Exception_TokenInfoRetrivalFailed); + + tokenInformation = tokenInformationResponse.Data; createdAccount.Address = tokenInformation.Address; tokenInformation.AccountId = createdAccount.Id; } @@ -205,8 +210,6 @@ namespace Wino.Mail.ViewModels // Local account has been created. // Create new synchronizer and start synchronization. - var synchronizer = _synchronizerFactory.CreateNewSynchronizer(createdAccount); - if (creationDialog is ICustomServerAccountCreationDialog customServerAccountCreationDialog) customServerAccountCreationDialog.ShowPreparingFolders(); else @@ -218,8 +221,9 @@ namespace Wino.Mail.ViewModels Type = SynchronizationType.FoldersOnly }; - var synchronizationResult = await synchronizer.SynchronizeAsync(options); + var synchronizationResultResponse = await _winoServerConnectionManager.GetResponseAsync(new NewSynchronizationRequested(options, SynchronizationSource.Client)); + var synchronizationResult = synchronizationResultResponse.Data; if (synchronizationResult.CompletedState != SynchronizationCompletedState.Success) throw new Exception(Translator.Exception_FailedToSynchronizeFolders); @@ -377,11 +381,10 @@ namespace Wino.Mail.ViewModels return new AccountProviderDetailViewModel(provider, account); } - public void Receive(ProtocolAuthorizationCallbackReceived message) + public async void Receive(ProtocolAuthorizationCallbackReceived message) { - // Authorization must be completed in account service. - - _accountService.ExternalAuthenticationAuthenticator?.ContinueAuthorization(message.AuthorizationResponseUri); + // Authorization must be completed in the server. + await _winoServerConnectionManager.GetResponseAsync(message); } } } diff --git a/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs b/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs new file mode 100644 index 00000000..7c9d1586 --- /dev/null +++ b/Wino.Mail.ViewModels/AppPreferencesPageViewModel.cs @@ -0,0 +1,140 @@ +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Wino.Core.Domain; +using Wino.Core.Domain.Enums; +using Wino.Core.Domain.Interfaces; +using Wino.Core.Domain.Models.Navigation; +using Wino.Messaging.Server; + +namespace Wino.Mail.ViewModels +{ + public partial class AppPreferencesPageViewModel : BaseViewModel + { + public IPreferencesService PreferencesService { get; } + + [ObservableProperty] + private List _appTerminationBehavior; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsStartupBehaviorDisabled))] + [NotifyPropertyChangedFor(nameof(IsStartupBehaviorEnabled))] + private StartupBehaviorResult startupBehaviorResult; + + public bool IsStartupBehaviorDisabled => !IsStartupBehaviorEnabled; + public bool IsStartupBehaviorEnabled => StartupBehaviorResult == StartupBehaviorResult.Enabled; + + private string _selectedAppTerminationBehavior; + public string SelectedAppTerminationBehavior + { + get => _selectedAppTerminationBehavior; + set + { + SetProperty(ref _selectedAppTerminationBehavior, value); + + PreferencesService.ServerTerminationBehavior = (ServerBackgroundMode)AppTerminationBehavior.IndexOf(value); + } + } + + private readonly IWinoServerConnectionManager _winoServerConnectionManager; + private readonly IStartupBehaviorService _startupBehaviorService; + + public AppPreferencesPageViewModel(IDialogService dialogService, + IPreferencesService preferencesService, + IWinoServerConnectionManager winoServerConnectionManager, + IStartupBehaviorService startupBehaviorService) : base(dialogService) + { + PreferencesService = preferencesService; + _winoServerConnectionManager = winoServerConnectionManager; + _startupBehaviorService = startupBehaviorService; + + // Load the app termination behavior options + + _appTerminationBehavior = + [ + Translator.SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Title, // "Minimize to tray" + Translator.SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title, // "Invisible" + Translator.SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title // "Terminate" + ]; + + SelectedAppTerminationBehavior = _appTerminationBehavior[(int)PreferencesService.ServerTerminationBehavior]; + } + + [RelayCommand] + private async Task ToggleStartupBehaviorAsync() + { + if (IsStartupBehaviorEnabled) + { + await DisableStartupAsync(); + } + else + { + await EnableStartupAsync(); + } + + OnPropertyChanged(nameof(IsStartupBehaviorEnabled)); + } + + private async Task EnableStartupAsync() + { + StartupBehaviorResult = await _startupBehaviorService.ToggleStartupBehavior(true); + + NotifyCurrentStartupState(); + } + + private async Task DisableStartupAsync() + { + StartupBehaviorResult = await _startupBehaviorService.ToggleStartupBehavior(false); + + NotifyCurrentStartupState(); + } + + private void NotifyCurrentStartupState() + { + if (StartupBehaviorResult == StartupBehaviorResult.Enabled) + { + DialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.SettingsAppPreferences_StartupBehavior_Enabled, InfoBarMessageType.Success); + } + else if (StartupBehaviorResult == StartupBehaviorResult.Disabled) + { + DialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.SettingsAppPreferences_StartupBehavior_Disabled, InfoBarMessageType.Warning); + } + else if (StartupBehaviorResult == StartupBehaviorResult.DisabledByPolicy) + { + DialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.SettingsAppPreferences_StartupBehavior_DisabledByPolicy, InfoBarMessageType.Warning); + } + else if (StartupBehaviorResult == StartupBehaviorResult.DisabledByUser) + { + DialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.SettingsAppPreferences_StartupBehavior_DisabledByUser, InfoBarMessageType.Warning); + } + else + { + DialogService.InfoBarMessage(Translator.GeneralTitle_Error, Translator.SettingsAppPreferences_StartupBehavior_FatalError, InfoBarMessageType.Error); + } + } + + protected override async void OnPropertyChanged(PropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.PropertyName == nameof(SelectedAppTerminationBehavior)) + { + var terminationModeChangedResult = await _winoServerConnectionManager.GetResponseAsync(new ServerTerminationModeChanged(PreferencesService.ServerTerminationBehavior)); + + if (!terminationModeChangedResult.IsSuccess) + { + DialogService.InfoBarMessage(Translator.GeneralTitle_Error, terminationModeChangedResult.Message, InfoBarMessageType.Error); + } + } + } + + public override async void OnNavigatedTo(NavigationMode mode, object parameters) + { + base.OnNavigatedTo(mode, parameters); + + StartupBehaviorResult = await _startupBehaviorService.GetCurrentStartupBehaviorAsync(); + } + } +} diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/AppShellViewModel.cs index 5717d901..f40fbd16 100644 --- a/Wino.Mail.ViewModels/AppShellViewModel.cs +++ b/Wino.Mail.ViewModels/AppShellViewModel.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; using Microsoft.AppCenter.Crashes; using MoreLinq; @@ -13,33 +14,30 @@ using Wino.Core; using Wino.Core.Domain; using Wino.Core.Domain.Entities; using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Exceptions; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Synchronization; using Wino.Core.MenuItems; -using Wino.Core.Messages.Accounts; -using Wino.Core.Messages.Mails; -using Wino.Core.Messages.Navigation; -using Wino.Core.Messages.Shell; -using Wino.Core.Messages.Synchronization; -using Wino.Core.Requests; using Wino.Core.Services; +using Wino.Messaging.Client.Accounts; +using Wino.Messaging.Client.Navigation; +using Wino.Messaging.Client.Shell; +using Wino.Messaging.Server; +using Wino.Messaging.UI; namespace Wino.Mail.ViewModels { public partial class AppShellViewModel : BaseViewModel, - ISynchronizationProgress, - IRecipient, IRecipient, IRecipient, IRecipient, IRecipient, IRecipient, IRecipient, - IRecipient + IRecipient, + IRecipient { #region Menu Items @@ -62,6 +60,7 @@ namespace Wino.Mail.ViewModels #endregion public IStatePersistanceService StatePersistenceService { get; } + public IWinoServerConnectionManager ServerConnectionManager { get; } public IPreferencesService PreferencesService { get; } public IWinoNavigationService NavigationService { get; } @@ -73,7 +72,6 @@ namespace Wino.Mail.ViewModels private readonly INotificationBuilder _notificationBuilder; private readonly IWinoRequestDelegator _winoRequestDelegator; - private readonly IWinoSynchronizerFactory _synchronizerFactory; private readonly IBackgroundTaskService _backgroundTaskService; private readonly IMimeFileService _mimeFileService; @@ -82,9 +80,11 @@ namespace Wino.Mail.ViewModels private readonly SemaphoreSlim accountInitFolderUpdateSlim = new SemaphoreSlim(1); + [ObservableProperty] + private WinoServerConnectionStatus activeConnectionStatus; + public AppShellViewModel(IDialogService dialogService, IWinoNavigationService navigationService, - IWinoSynchronizerFactory synchronizerFactory, IBackgroundTaskService backgroundTaskService, IMimeFileService mimeFileService, INativeAppService nativeAppService, @@ -97,13 +97,24 @@ namespace Wino.Mail.ViewModels INotificationBuilder notificationBuilder, IWinoRequestDelegator winoRequestDelegator, IFolderService folderService, - IStatePersistanceService statePersistanceService) : base(dialogService) + IStatePersistanceService statePersistanceService, + IWinoServerConnectionManager serverConnectionManager) : base(dialogService) { StatePersistenceService = statePersistanceService; + ServerConnectionManager = serverConnectionManager; + + ActiveConnectionStatus = serverConnectionManager.Status; + ServerConnectionManager.StatusChanged += async (sender, status) => + { + await ExecuteUIThread(() => + { + ActiveConnectionStatus = status; + }); + }; + PreferencesService = preferencesService; NavigationService = navigationService; - _synchronizerFactory = synchronizerFactory; _backgroundTaskService = backgroundTaskService; _mimeFileService = mimeFileService; _nativeAppService = nativeAppService; @@ -117,6 +128,9 @@ namespace Wino.Mail.ViewModels _winoRequestDelegator = winoRequestDelegator; } + [RelayCommand] + private Task ReconnectServerAsync() => ServerConnectionManager.ConnectAsync(); + protected override void OnDispatcherAssigned() { base.OnDispatcherAssigned(); @@ -221,9 +235,7 @@ namespace Wino.Mail.ViewModels await RecreateMenuItemsAsync(); await ProcessLaunchOptionsAsync(); -#if !DEBUG await ForceAllAccountSynchronizationsAsync(); -#endif await ConfigureBackgroundTasksAsync(); } @@ -233,10 +245,6 @@ namespace Wino.Mail.ViewModels { await _backgroundTaskService.HandleBackgroundTaskRegistrations(); } - catch (BackgroundTaskExecutionRequestDeniedException) - { - await DialogService.ShowMessageAsync(Translator.Info_BackgroundExecutionDeniedMessage, Translator.Info_BackgroundExecutionDeniedTitle); - } catch (Exception ex) { Crashes.TrackError(ex); @@ -255,10 +263,10 @@ namespace Wino.Mail.ViewModels var options = new SynchronizationOptions() { AccountId = account.Id, - Type = SynchronizationType.Inbox + Type = SynchronizationType.Full }; - Messenger.Send(new NewSynchronizationRequested(options)); + Messenger.Send(new NewSynchronizationRequested(options, SynchronizationSource.Client)); } } @@ -344,23 +352,27 @@ namespace Wino.Mail.ViewModels } } - public async Task NavigateFolderAsync(IBaseFolderMenuItem baseFolderMenuItem) + public async Task NavigateFolderAsync(IBaseFolderMenuItem baseFolderMenuItem, TaskCompletionSource folderInitAwaitTask = null) { // It's already there. Don't navigate again. if (SelectedMenuItem == baseFolderMenuItem) return; - SelectedMenuItem = baseFolderMenuItem; - baseFolderMenuItem.IsSelected = true; + await ExecuteUIThread(() => + { + SelectedMenuItem = baseFolderMenuItem; + baseFolderMenuItem.IsSelected = true; - var mailInitCompletionSource = new TaskCompletionSource(); - var args = new NavigateMailFolderEventArgs(baseFolderMenuItem, mailInitCompletionSource); + if (folderInitAwaitTask == null) folderInitAwaitTask = new TaskCompletionSource(); - NavigationService.NavigateFolder(args); + var args = new NavigateMailFolderEventArgs(baseFolderMenuItem, folderInitAwaitTask); - UpdateWindowTitleForFolder(baseFolderMenuItem); + NavigationService.NavigateFolder(args); + + UpdateWindowTitleForFolder(baseFolderMenuItem); + }); // Wait until mail list page picks up the event and finish initialization of the mails. - await mailInitCompletionSource.Task; + await folderInitAwaitTask.Task; } private void UpdateWindowTitleForFolder(IBaseFolderMenuItem folder) @@ -594,11 +606,7 @@ namespace Wino.Mail.ViewModels if (navigateInbox) { await Task.Yield(); - - await ExecuteUIThread(() => - { - NavigateInbox(clickedBaseAccountMenuItem); - }); + await NavigateInboxAsync(clickedBaseAccountMenuItem); } } @@ -666,20 +674,22 @@ namespace Wino.Mail.ViewModels await _notificationBuilder.UpdateTaskbarIconBadgeAsync(); } - private async void NavigateInbox(IAccountMenuItem clickedBaseAccountMenuItem) + private async Task NavigateInboxAsync(IAccountMenuItem clickedBaseAccountMenuItem) { + var folderInitAwaitTask = new TaskCompletionSource(); + if (clickedBaseAccountMenuItem is AccountMenuItem accountMenuItem) { if (MenuItems.TryGetWindowsStyleRootSpecialFolderMenuItem(accountMenuItem.AccountId, SpecialFolderType.Inbox, out FolderMenuItem inboxFolder)) { - await NavigateFolderAsync(inboxFolder); + await NavigateFolderAsync(inboxFolder, folderInitAwaitTask); } } else if (clickedBaseAccountMenuItem is MergedAccountMenuItem mergedAccountMenuItem) { if (MenuItems.TryGetMergedAccountSpecialFolderMenuItem(mergedAccountMenuItem.EntityId.GetValueOrDefault(), SpecialFolderType.Inbox, out IBaseFolderMenuItem inboxFolder)) { - await NavigateFolderAsync(inboxFolder); + await NavigateFolderAsync(inboxFolder, folderInitAwaitTask); } } } @@ -769,75 +779,13 @@ namespace Wino.Mail.ViewModels MailtoParameters = _launchProtocolService.MailtoParameters }; - var createdMimeMessage = await _mailService.CreateDraftMimeMessageAsync(account.Id, draftOptions).ConfigureAwait(false); - var createdDraftMailMessage = await _mailService.CreateDraftAsync(account, createdMimeMessage).ConfigureAwait(false); + var createdBase64EncodedMimeMessage = await _mailService.CreateDraftMimeBase64Async(account.Id, draftOptions).ConfigureAwait(false); + var createdDraftMailMessage = await _mailService.CreateDraftAsync(account, createdBase64EncodedMimeMessage).ConfigureAwait(false); - var draftPreperationRequest = new DraftPreperationRequest(account, createdDraftMailMessage, createdMimeMessage); + var draftPreperationRequest = new DraftPreperationRequest(account, createdDraftMailMessage, createdBase64EncodedMimeMessage); await _winoRequestDelegator.ExecuteAsync(draftPreperationRequest); } - - - public async void Receive(NewSynchronizationRequested message) - { - // Don't send message for sync completion when we execute requests. - // People are usually interested in seeing the notification after they trigger the synchronization. - - bool shouldReportSynchronizationResult = message.Options.Type != SynchronizationType.ExecuteRequests; - - var synchronizer = _synchronizerFactory.GetAccountSynchronizer(message.Options.AccountId); - - if (synchronizer == null) return; - - var accountId = message.Options.AccountId; - - message.Options.ProgressListener = this; - - bool isSynchronizationSucceeded = false; - - try - { - // TODO: Cancellation Token - var synchronizationResult = await synchronizer.SynchronizeAsync(message.Options); - - isSynchronizationSucceeded = synchronizationResult.CompletedState == SynchronizationCompletedState.Success; - - // Create notification for synchronization result. - if (synchronizationResult.DownloadedMessages.Any()) - { - var accountInboxFolder = await _folderService.GetSpecialFolderByAccountIdAsync(message.Options.AccountId, SpecialFolderType.Inbox); - - if (accountInboxFolder == null) return; - - await _notificationBuilder.CreateNotificationsAsync(accountInboxFolder.Id, synchronizationResult.DownloadedMessages); - } - } - catch (AuthenticationAttentionException) - { - await SetAccountAttentionAsync(accountId, AccountAttentionReason.InvalidCredentials); - } - catch (SystemFolderConfigurationMissingException) - { - await SetAccountAttentionAsync(accountId, AccountAttentionReason.MissingSystemFolderConfiguration); - } - catch (OperationCanceledException) - { - DialogService.InfoBarMessage(Translator.Info_SyncCanceledMessage, Translator.Info_SyncCanceledMessage, InfoBarMessageType.Warning); - } - catch (Exception ex) - { - DialogService.InfoBarMessage(Translator.Info_SyncFailedTitle, ex.Message, InfoBarMessageType.Error); - } - finally - { - if (shouldReportSynchronizationResult) - Messenger.Send(new AccountSynchronizationCompleted(accountId, - isSynchronizationSucceeded ? SynchronizationCompletedState.Success : SynchronizationCompletedState.Failed, - message.Options.GroupedSynchronizationTrackingId)); - } - } - - protected override async void OnAccountUpdated(MailAccount updatedAccount) { await ExecuteUIThread(() => @@ -867,11 +815,12 @@ namespace Wino.Mail.ViewModels Type = SynchronizationType.Full, }; - Messenger.Send(new NewSynchronizationRequested(options)); + Messenger.Send(new NewSynchronizationRequested(options, SynchronizationSource.Client)); await _nativeAppService.PinAppToTaskbarAsync(); } + // TODO: Handle by messaging. private async Task SetAccountAttentionAsync(Guid accountId, AccountAttentionReason reason) { if (!MenuItems.TryGetAccountMenuItem(accountId, out IAccountMenuItem accountMenuItem)) return; @@ -913,15 +862,6 @@ namespace Wino.Mail.ViewModels await CreateNewMailForAsync(targetAccount); } - public async void AccountProgressUpdated(Guid accountId, int progress) - { - var accountMenuItem = MenuItems.GetSpecificAccountMenuItem(accountId); - - if (accountMenuItem == null) return; - - await ExecuteUIThread(() => { accountMenuItem.SynchronizationProgress = progress; }); - } - private async Task RecreateMenuItemsAsync() { await ExecuteUIThread(() => @@ -1009,5 +949,14 @@ namespace Wino.Mail.ViewModels UpdateFolderCollection(mailItemFolder); } + + public async void Receive(AccountSynchronizationProgressUpdatedMessage message) + { + var accountMenuItem = MenuItems.GetSpecificAccountMenuItem(message.AccountId); + + if (accountMenuItem == null) return; + + await ExecuteUIThread(() => { accountMenuItem.SynchronizationProgress = message.Progress; }); + } } } diff --git a/Wino.Mail.ViewModels/BaseViewModel.cs b/Wino.Mail.ViewModels/BaseViewModel.cs index 544ddda8..ee78fd29 100644 --- a/Wino.Mail.ViewModels/BaseViewModel.cs +++ b/Wino.Mail.ViewModels/BaseViewModel.cs @@ -6,8 +6,7 @@ using Wino.Core.Domain.Entities; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Folders; using Wino.Core.Domain.Models.Navigation; -using Wino.Core.Domain.Models.Requests; -using Wino.Core.Requests; +using Wino.Messaging.UI; namespace Wino.Mail.ViewModels { @@ -72,9 +71,7 @@ namespace Wino.Mail.ViewModels protected virtual void OnFolderRenamed(IMailItemFolder mailItemFolder) { } protected virtual void OnFolderSynchronizationEnabled(IMailItemFolder mailItemFolder) { } - public void ReportUIChange(TMessage message) where TMessage : class, IUIMessage - => Messenger.Send(message); - + public void ReportUIChange(TMessage message) where TMessage : class, IUIMessage => Messenger.Send(message); void IRecipient.Receive(AccountCreatedMessage message) => OnAccountCreated(message.Account); void IRecipient.Receive(AccountRemovedMessage message) => OnAccountRemoved(message.Account); void IRecipient.Receive(AccountUpdatedMessage message) => OnAccountUpdated(message.Account); diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs index bd10ad5e..e99adb70 100644 --- a/Wino.Mail.ViewModels/ComposePageViewModel.cs +++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs @@ -18,9 +18,9 @@ using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Reader; using Wino.Core.Extensions; -using Wino.Core.Messages.Mails; using Wino.Core.Services; using Wino.Mail.ViewModels.Data; +using Wino.Messaging.Client.Mails; namespace Wino.Mail.ViewModels { @@ -171,7 +171,14 @@ namespace Wino.Mail.ViewModels var assignedAccount = CurrentMailDraftItem.AssignedAccount; var sentFolder = await _folderService.GetSpecialFolderByAccountIdAsync(assignedAccount.Id, SpecialFolderType.Sent); - var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy, CurrentMimeMessage, CurrentMailDraftItem.AssignedFolder, sentFolder, CurrentMailDraftItem.AssignedAccount.Preferences); + + using MemoryStream memoryStream = new(); + CurrentMimeMessage.WriteTo(FormatOptions.Default, memoryStream); + byte[] buffer = memoryStream.GetBuffer(); + int count = (int)memoryStream.Length; + + var base64EncodedMessage = Convert.ToBase64String(buffer); + var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy, sentFolder, CurrentMailDraftItem.AssignedFolder, CurrentMailDraftItem.AssignedAccount.Preferences, base64EncodedMessage); await _worker.ExecuteAsync(draftSendPreparationRequest); } diff --git a/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs b/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs index 616e2dea..c90b81aa 100644 --- a/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs +++ b/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs @@ -1,5 +1,5 @@ using CommunityToolkit.Mvvm.ComponentModel; -using Wino.Core.Messages.Navigation; +using Wino.Messaging.Client.Navigation; namespace Wino.Mail.ViewModels.Data { diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs index 423bdd76..e3f24b1a 100644 --- a/Wino.Mail.ViewModels/MailListPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs @@ -24,12 +24,13 @@ using Wino.Core.Domain.Models.MailItem; using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Reader; using Wino.Core.Domain.Models.Synchronization; -using Wino.Core.Messages.Mails; -using Wino.Core.Messages.Shell; -using Wino.Core.Messages.Synchronization; using Wino.Mail.ViewModels.Collections; using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Messages; +using Wino.Messaging.Client.Mails; +using Wino.Messaging.Client.Shell; +using Wino.Messaging.Server; +using Wino.Messaging.UI; namespace Wino.Mail.ViewModels { @@ -72,12 +73,11 @@ namespace Wino.Mail.ViewModels private readonly IMailService _mailService; private readonly IFolderService _folderService; - private readonly IWinoSynchronizerFactory _winoSynchronizerFactory; private readonly IThreadingStrategyProvider _threadingStrategyProvider; private readonly IContextMenuItemService _contextMenuItemService; private readonly IWinoRequestDelegator _winoRequestDelegator; private readonly IKeyPressService _keyPressService; - + private readonly IWinoServerConnectionManager _winoServerConnectionManager; private MailItemViewModel _activeMailItem; public List SortingOptions { get; } = @@ -143,20 +143,20 @@ namespace Wino.Mail.ViewModels IMailService mailService, IStatePersistanceService statePersistanceService, IFolderService folderService, - IWinoSynchronizerFactory winoSynchronizerFactory, IThreadingStrategyProvider threadingStrategyProvider, IContextMenuItemService contextMenuItemService, IWinoRequestDelegator winoRequestDelegator, IKeyPressService keyPressService, - IPreferencesService preferencesService) : base(dialogService) + IPreferencesService preferencesService, + IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService) { PreferencesService = preferencesService; + _winoServerConnectionManager = winoServerConnectionManager; StatePersistanceService = statePersistanceService; NavigationService = navigationService; _mailService = mailService; _folderService = folderService; - _winoSynchronizerFactory = winoSynchronizerFactory; _threadingStrategyProvider = threadingStrategyProvider; _contextMenuItemService = contextMenuItemService; _winoRequestDelegator = winoRequestDelegator; @@ -450,7 +450,7 @@ namespace Wino.Mail.ViewModels GroupedSynchronizationTrackingId = trackingSynchronizationId }; - Messenger.Send(new NewSynchronizationRequested(options)); + Messenger.Send(new NewSynchronizationRequested(options, SynchronizationSource.Client)); } } @@ -602,6 +602,8 @@ namespace Wino.Mail.ViewModels { base.OnMailAdded(addedMail); + if (addedMail.AssignedAccount == null || addedMail.AssignedFolder == null) return; + try { await listManipulationSemepahore.WaitAsync(); @@ -866,6 +868,8 @@ namespace Wino.Mail.ViewModels // Let awaiters know about the completion of mail init. message.FolderInitLoadAwaitTask?.TrySetResult(true); + await Task.Yield(); + isChangingFolder = false; void ResetFilters() @@ -914,6 +918,7 @@ namespace Wino.Mail.ViewModels void IRecipient.Receive(MailItemNavigationRequested message) { + Debug.WriteLine($"Mail item navigation requested"); // Find mail item and add to selected items. MailItemViewModel navigatingMailItem = null; @@ -975,13 +980,9 @@ namespace Wino.Mail.ViewModels foreach (var accountId in accountIds) { - var synchronizer = _winoSynchronizerFactory.GetAccountSynchronizer(accountId); + var serverResponse = await _winoServerConnectionManager.GetResponseAsync(new SynchronizationExistenceCheckRequest(accountId)); - if (synchronizer == null) continue; - - bool isAccountSynchronizing = synchronizer.State != AccountSynchronizerState.Idle; - - if (isAccountSynchronizing) + if (serverResponse.IsSuccess && serverResponse.Data == true) { isAnyAccountSynchronizing = true; break; diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs index 0c44eda4..8aae0408 100644 --- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs +++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs @@ -21,10 +21,11 @@ using Wino.Core.Domain.Models.Menus; using Wino.Core.Domain.Models.Navigation; using Wino.Core.Domain.Models.Reader; using Wino.Core.Extensions; -using Wino.Core.Messages.Mails; using Wino.Core.Services; using Wino.Mail.ViewModels.Data; using Wino.Mail.ViewModels.Messages; +using Wino.Messaging.Client.Mails; +using Wino.Messaging.Server; namespace Wino.Mail.ViewModels { @@ -37,11 +38,10 @@ namespace Wino.Mail.ViewModels private readonly IMimeFileService _mimeFileService; private readonly Core.Domain.Interfaces.IMailService _mailService; private readonly IFileService _fileService; - private readonly IWinoSynchronizerFactory _winoSynchronizerFactory; private readonly IWinoRequestDelegator _requestDelegator; private readonly IClipboardService _clipboardService; private readonly IUnsubscriptionService _unsubscriptionService; - + private readonly IWinoServerConnectionManager _winoServerConnectionManager; private bool forceImageLoading = false; private MailItemViewModel initializedMailItemViewModel = null; @@ -81,6 +81,8 @@ namespace Wino.Mail.ViewModels } } + public bool HasMultipleAttachments => Attachments.Count > 1; + [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShouldDisplayDownloadProgress))] private bool isIndetermineProgress; @@ -124,24 +126,23 @@ namespace Wino.Mail.ViewModels IMimeFileService mimeFileService, Core.Domain.Interfaces.IMailService mailService, IFileService fileService, - IWinoSynchronizerFactory winoSynchronizerFactory, IWinoRequestDelegator requestDelegator, IStatePersistanceService statePersistanceService, IClipboardService clipboardService, IUnsubscriptionService unsubscriptionService, - IPreferencesService preferencesService) : base(dialogService) + IPreferencesService preferencesService, + IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService) { NativeAppService = nativeAppService; StatePersistanceService = statePersistanceService; PreferencesService = preferencesService; - + _winoServerConnectionManager = winoServerConnectionManager; _clipboardService = clipboardService; _unsubscriptionService = unsubscriptionService; _underlyingThemeService = underlyingThemeService; _mimeFileService = mimeFileService; _mailService = mailService; _fileService = fileService; - _winoSynchronizerFactory = winoSynchronizerFactory; _requestDelegator = requestDelegator; } @@ -168,10 +169,7 @@ namespace Wino.Mail.ViewModels if (initializedMailItemViewModel == null && initializedMimeMessageInformation == null) return; - if (initializedMailItemViewModel != null) - await RenderAsync(initializedMimeMessageInformation); - else - await RenderAsync(initializedMimeMessageInformation); + await RenderAsync(initializedMimeMessageInformation); } [RelayCommand] @@ -272,14 +270,16 @@ namespace Wino.Mail.ViewModels draftOptions.ReferenceMailCopy = initializedMailItemViewModel.MailCopy; draftOptions.ReferenceMimeMessage = initializedMimeMessageInformation.MimeMessage; - var createdMimeMessage = await _mailService.CreateDraftMimeMessageAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false); + var createdMimeMessage = await _mailService.CreateDraftMimeBase64Async(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false); var createdDraftMailMessage = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount, createdMimeMessage, initializedMimeMessageInformation.MimeMessage, initializedMailItemViewModel).ConfigureAwait(false); - var draftPreperationRequest = new DraftPreperationRequest(initializedMailItemViewModel.AssignedAccount, createdDraftMailMessage, createdMimeMessage) + var draftPreperationRequest = new DraftPreperationRequest(initializedMailItemViewModel.AssignedAccount, + createdDraftMailMessage, + createdMimeMessage) { ReferenceMimeMessage = initializedMimeMessageInformation.MimeMessage, ReferenceMailCopy = initializedMailItemViewModel.MailCopy @@ -302,6 +302,9 @@ namespace Wino.Mail.ViewModels { base.OnNavigatedTo(mode, parameters); + Attachments.CollectionChanged -= AttachmentsUpdated; + Attachments.CollectionChanged += AttachmentsUpdated; + renderCancellationTokenSource.Cancel(); initializedMailItemViewModel = null; @@ -344,17 +347,20 @@ namespace Wino.Mail.ViewModels } } + private async void AttachmentsUpdated(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) + { + await ExecuteUIThread(() => { OnPropertyChanged(nameof(HasMultipleAttachments)); }); + } private async Task HandleSingleItemDownloadAsync(MailItemViewModel mailItemViewModel) { - var synchronizer = _winoSynchronizerFactory.GetAccountSynchronizer(mailItemViewModel.AssignedAccount.Id); - try { // To show the progress on the UI. CurrentDownloadPercentage = 1; - await synchronizer.DownloadMissingMimeMessageAsync(mailItemViewModel.MailCopy, this, renderCancellationTokenSource.Token); + var package = new DownloadMissingMessageRequested(mailItemViewModel.AssignedAccount.Id, mailItemViewModel.MailCopy); + await _winoServerConnectionManager.GetResponseAsync(package); } catch (OperationCanceledException) { @@ -455,6 +461,8 @@ namespace Wino.Mail.ViewModels { base.OnNavigatedFrom(mode, parameters); + Attachments.CollectionChanged -= AttachmentsUpdated; + renderCancellationTokenSource.Cancel(); CurrentDownloadPercentage = 0d; @@ -634,6 +642,32 @@ namespace Wino.Mail.ViewModels } } + [RelayCommand] + private async Task SaveAllAttachmentsAsync() + { + var pickedPath = await DialogService.PickWindowsFolderAsync(); + + if (string.IsNullOrEmpty(pickedPath)) return; + + try + { + + foreach (var attachmentViewModel in Attachments) + { + await SaveAttachmentInternalAsync(attachmentViewModel, pickedPath); + } + + DialogService.InfoBarMessage(Translator.Info_AttachmentSaveSuccessTitle, Translator.Info_AttachmentSaveSuccessMessage, InfoBarMessageType.Success); + } + catch (Exception ex) + { + Log.Error(ex, WinoErrors.SaveAttachment); + Crashes.TrackError(ex); + + DialogService.InfoBarMessage(Translator.Info_AttachmentSaveFailedTitle, Translator.Info_AttachmentSaveFailedMessage, InfoBarMessageType.Error); + } + } + // Returns created file path. private async Task SaveAttachmentInternalAsync(MailAttachmentViewModel attachmentViewModel, string saveFolderPath) { @@ -666,6 +700,23 @@ namespace Wino.Mail.ViewModels // For upload. void ITransferProgress.Report(long bytesTransferred) { } - public async void Receive(NewMailItemRenderingRequestedEvent message) => await RenderAsync(message.MailItemViewModel, renderCancellationTokenSource.Token); + public async void Receive(NewMailItemRenderingRequestedEvent message) + { + try + { + await RenderAsync(message.MailItemViewModel, renderCancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + Log.Information("Canceled mail rendering."); + } + catch (Exception ex) + { + DialogService.InfoBarMessage(Translator.Info_MailRenderingFailedTitle, string.Format(Translator.Info_MailRenderingFailedMessage, ex.Message), InfoBarMessageType.Error); + + Crashes.TrackError(ex); + Log.Error(ex, "Render Failed"); + } + } } } diff --git a/Wino.Mail.ViewModels/MergedAccountDetailsPageViewModel.cs b/Wino.Mail.ViewModels/MergedAccountDetailsPageViewModel.cs index 9121954b..cca7da65 100644 --- a/Wino.Mail.ViewModels/MergedAccountDetailsPageViewModel.cs +++ b/Wino.Mail.ViewModels/MergedAccountDetailsPageViewModel.cs @@ -8,9 +8,9 @@ using CommunityToolkit.Mvvm.Messaging; using Wino.Core.Domain; using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Models.Navigation; -using Wino.Core.Messages.Navigation; -using Wino.Core.Requests; using Wino.Mail.ViewModels.Data; +using Wino.Messaging.Client.Navigation; +using Wino.Messaging.UI; namespace Wino.Mail.ViewModels { diff --git a/Wino.Mail.ViewModels/SettingOptionsPageViewModel.cs b/Wino.Mail.ViewModels/SettingOptionsPageViewModel.cs index 4231d19a..4d90f928 100644 --- a/Wino.Mail.ViewModels/SettingOptionsPageViewModel.cs +++ b/Wino.Mail.ViewModels/SettingOptionsPageViewModel.cs @@ -4,8 +4,7 @@ using CommunityToolkit.Mvvm.Messaging; using Wino.Core.Domain; using Wino.Core.Domain.Enums; using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.Navigation; -using Wino.Core.Messages.Navigation; +using Wino.Messaging.Client.Navigation; namespace Wino.Mail.ViewModels { @@ -26,6 +25,7 @@ namespace Wino.Mail.ViewModels WinoPage.MessageListPage => Translator.SettingsMessageList_Title, WinoPage.ReadComposePanePage => Translator.SettingsReadComposePane_Title, WinoPage.LanguageTimePage => Translator.SettingsLanguageTime_Title, + WinoPage.AppPreferencesPage => Translator.SettingsAppPreferences_Title, _ => throw new NotImplementedException() }; diff --git a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj index ccac6be6..946a3bf8 100644 --- a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj +++ b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj @@ -3,6 +3,7 @@ netstandard2.0 12 + AnyCPU;x64;x86 @@ -17,6 +18,7 @@ + diff --git a/Wino.Mail/Activation/BackgroundActivationHandler.cs b/Wino.Mail/Activation/BackgroundActivationHandler.cs deleted file mode 100644 index 62c6c45c..00000000 --- a/Wino.Mail/Activation/BackgroundActivationHandler.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System.Diagnostics; -using System.Threading.Tasks; -using Microsoft.Toolkit.Uwp.Notifications; -using Serilog; -using Windows.ApplicationModel.Activation; -using Windows.ApplicationModel.Background; -using Windows.UI.Notifications; -using Wino.Core; -using Wino.Core.Domain; -using Wino.Core.Domain.Enums; -using Wino.Core.Domain.Interfaces; -using Wino.Core.Domain.Models.MailItem; -using Wino.Core.Domain.Models.Synchronization; -using Wino.Core.UWP.Services; -using Wino.Services; - -namespace Wino.Activation -{ - internal class BackgroundActivationHandler : ActivationHandler - { - private const string BackgroundExecutionLogTag = "[BackgroundExecution] "; - - private readonly IWinoRequestDelegator _winoRequestDelegator; - private readonly IBackgroundSynchronizer _backgroundSynchronizer; - private readonly INativeAppService _nativeAppService; - private readonly IWinoRequestProcessor _winoRequestProcessor; - private readonly IWinoSynchronizerFactory _winoSynchronizerFactory; - private readonly IMailService _mailService; - private ToastArguments _toastArguments; - - BackgroundTaskDeferral _deferral; - public BackgroundActivationHandler(IWinoRequestDelegator winoRequestDelegator, - IBackgroundSynchronizer backgroundSynchronizer, - INativeAppService nativeAppService, - IWinoRequestProcessor winoRequestProcessor, - IWinoSynchronizerFactory winoSynchronizerFactory, - IMailService mailService) - { - _winoRequestDelegator = winoRequestDelegator; - _backgroundSynchronizer = backgroundSynchronizer; - _nativeAppService = nativeAppService; - _winoRequestProcessor = winoRequestProcessor; - _winoSynchronizerFactory = winoSynchronizerFactory; - _mailService = mailService; - } - - protected override async Task HandleInternalAsync(BackgroundActivatedEventArgs args) - { - var instance = args.TaskInstance; - var taskName = instance.Task.Name; - - instance.Canceled -= OnBackgroundExecutionCanceled; - instance.Canceled += OnBackgroundExecutionCanceled; - - _deferral = instance.GetDeferral(); - - if (taskName == BackgroundTaskService.ToastActivationTaskEx) - { - if (instance.TriggerDetails is ToastNotificationActionTriggerDetail toastNotificationActionTriggerDetail) - _toastArguments = ToastArguments.Parse(toastNotificationActionTriggerDetail.Argument); - - // All toast activation mail actions are handled here like mark as read or delete. - // This should not launch the application on the foreground. - - // Get the action and mail item id. - // Prepare package and send to delegator. - - if (_toastArguments.TryGetValue(Constants.ToastMailItemIdKey, out string mailItemId) && - _toastArguments.TryGetValue(Constants.ToastActionKey, out MailOperation action) && - _toastArguments.TryGetValue(Constants.ToastMailItemRemoteFolderIdKey, out string remoteFolderId)) - { - var mailItem = await _mailService.GetSingleMailItemAsync(mailItemId, remoteFolderId); - - if (mailItem == null) return; - - if (_nativeAppService.IsAppRunning()) - { - // Just send the package. We should reflect the UI changes as well. - var package = new MailOperationPreperationRequest(action, mailItem); - - await _winoRequestDelegator.ExecuteAsync(package); - } - else - { - // We need to synchronize changes without reflection the UI changes. - - var synchronizer = _winoSynchronizerFactory.GetAccountSynchronizer(mailItem.AssignedAccount.Id); - var prepRequest = new MailOperationPreperationRequest(action, mailItem); - - var requests = await _winoRequestProcessor.PrepareRequestsAsync(prepRequest); - - foreach (var request in requests) - { - synchronizer.QueueRequest(request); - } - - var options = new SynchronizationOptions() - { - Type = SynchronizationType.ExecuteRequests, - AccountId = mailItem.AssignedAccount.Id - }; - - await synchronizer.SynchronizeAsync(options); - } - } - } - else if (taskName == BackgroundTaskService.BackgroundSynchronizationTimerTaskNameEx) - { - var watch = new Stopwatch(); - watch.Start(); - - // Run timer based background synchronization. - - await _backgroundSynchronizer.RunBackgroundSynchronizationAsync(BackgroundSynchronizationReason.Timer); - - watch.Stop(); - Log.Information($"{BackgroundExecutionLogTag}Background synchronization is completed in {watch.Elapsed.TotalSeconds} seconds."); - } - - instance.Canceled -= OnBackgroundExecutionCanceled; - - _deferral.Complete(); - } - - private void OnBackgroundExecutionCanceled(Windows.ApplicationModel.Background.IBackgroundTaskInstance sender, Windows.ApplicationModel.Background.BackgroundTaskCancellationReason reason) - { - Log.Error($"{BackgroundExecutionLogTag} ({sender.Task.Name}) Background task is canceled. Reason -> {reason}"); - - _deferral?.Complete(); - } - - protected override bool CanHandleInternal(BackgroundActivatedEventArgs args) - { - var instance = args.TaskInstance; - var taskName = instance.Task.Name; - - if (taskName == BackgroundTaskService.ToastActivationTaskEx) - { - // User clicked Mark as Read or Delete in toast notification. - // MailId and Action must present in the arguments. - - return true; - - //if (instance.TriggerDetails is ToastNotificationActionTriggerDetail toastNotificationActionTriggerDetail) - //{ - // _toastArguments = ToastArguments.Parse(toastNotificationActionTriggerDetail.Argument); - - // return - // _toastArguments.Contains(Constants.ToastMailItemIdKey) && - // _toastArguments.Contains(Constants.ToastActionKey); - //} - - } - else if (taskName == BackgroundTaskService.BackgroundSynchronizationTimerTaskNameEx) - { - // This is timer based background synchronization. - - - return true; - } - - return false; - } - } -} diff --git a/Wino.Mail/Activation/ProtocolActivationHandler.cs b/Wino.Mail/Activation/ProtocolActivationHandler.cs index f6ad3aa9..37e116dd 100644 --- a/Wino.Mail/Activation/ProtocolActivationHandler.cs +++ b/Wino.Mail/Activation/ProtocolActivationHandler.cs @@ -3,8 +3,8 @@ using System.Web; using CommunityToolkit.Mvvm.Messaging; using Windows.ApplicationModel.Activation; using Wino.Core.Domain.Interfaces; -using Wino.Core.Messages.Authorization; -using Wino.Core.Messages.Shell; +using Wino.Messaging.Client.Authorization; +using Wino.Messaging.Client.Shell; namespace Wino.Activation { @@ -25,7 +25,6 @@ namespace Wino.Activation protected override Task HandleInternalAsync(ProtocolActivatedEventArgs args) { // Check URI prefix. - var protocolString = args.Uri.AbsoluteUri; // Google OAuth Response diff --git a/Wino.Mail/Activation/ToastNotificationActivationHandler.cs b/Wino.Mail/Activation/ToastNotificationActivationHandler.cs index 212d37da..f8e3f3be 100644 --- a/Wino.Mail/Activation/ToastNotificationActivationHandler.cs +++ b/Wino.Mail/Activation/ToastNotificationActivationHandler.cs @@ -7,7 +7,7 @@ using Serilog; using Windows.ApplicationModel.Activation; using Wino.Core.Domain; using Wino.Core.Domain.Interfaces; -using Wino.Core.Messages.Accounts; +using Wino.Messaging.Client.Accounts; namespace Wino.Activation { @@ -39,7 +39,7 @@ namespace Wino.Activation // Otherwise we'll save it and handle it when the shell loads all accounts. // Parse the mail unique id and perform above actions. - if (Guid.TryParse(_toastArguments[Constants.ToastMailItemIdKey], out Guid mailItemUniqueId)) + if (Guid.TryParse(_toastArguments[Constants.ToastMailUniqueIdKey], out Guid mailItemUniqueId)) { var account = await _mailService.GetMailAccountByUniqueIdAsync(mailItemUniqueId).ConfigureAwait(false); if (account == null) return; @@ -65,7 +65,7 @@ namespace Wino.Activation _toastArguments = ToastArguments.Parse(args.Argument); return - _toastArguments.Contains(Constants.ToastMailItemIdKey) && + _toastArguments.Contains(Constants.ToastMailUniqueIdKey) && _toastArguments.Contains(Constants.ToastActionKey); } catch (Exception ex) diff --git a/Wino.Mail/App.xaml b/Wino.Mail/App.xaml index 9934fc63..64b6c59e 100644 --- a/Wino.Mail/App.xaml +++ b/Wino.Mail/App.xaml @@ -1,10 +1,9 @@ - + @@ -61,10 +60,9 @@ - - - - - - - - - + + --> + + - - - + - - - + - - + @@ -393,6 +278,7 @@ x:Name="SearchBar" VerticalAlignment="Center" BorderBrush="Transparent" + Margin="2,0,-2,0" GotFocus="SearchBoxFocused" LostFocus="SearchBarUnfocused" PlaceholderText="{x:Bind domain:Translator.SearchBarPlaceholder}" @@ -435,7 +321,7 @@ @@ -729,10 +615,11 @@ + - + - + - + @@ -67,29 +63,26 @@ - + - + - + @@ -102,46 +95,41 @@ - + - + - + - + - + @@ -149,20 +137,18 @@ - + - + @@ -177,40 +163,35 @@ - + - + - + - + - + @@ -223,24 +204,21 @@ - + - + - + @@ -252,29 +230,25 @@ - + - + - - + + @@ -284,15 +258,13 @@ - - + + @@ -302,15 +274,13 @@ - - + + @@ -322,71 +292,78 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + -