Full trust Wino Server implementation. (#295)
* Separation of messages. Introducing Wino.Messages library. * Wino.Server and Wino.Packaging projects. Enabling full trust for UWP and app service connection manager basics. * Remove debug code. * Enable generating assembly info to deal with unsupported os platform warnings. * Fix server-client connection. * UIMessage communication. Single instancing for server and re-connection mechanism on suspension. * Removed IWinoSynchronizerFactory from UWP project. * Removal of background task service from core. * Delegating changes to UI and triggering new background synchronization. * Fix build error. * Moved core lib messages to Messaging project. * Better client-server communication. Handling of requests in the server. New synchronizer factory in the server. * WAM broker and MSAL token caching for OutlookAuthenticator. Handling account creation for Outlook. * WinoServerResponse basics. * Delegating protocol activation for Gmail authenticator. * Adding margin to searchbox to match action bar width. * Move libraries into lib folder. * Storing base64 encoded mime on draft creation instead of MimeMessage object. Fixes serialization/deserialization issue with S.T.Json * Scrollbar adjustments * WınoExpander for thread expander layout ıssue. * Handling synchronizer state changes. * Double init on background activation. * FIxing packaging issues and new Wino Mail launcher protocol for activation from full thrust process. * Remove debug deserialization. * Remove debug code. * Making sure the server connection is established when the app is launched. * Thrust -> Trust string replacement... * Rename package to Wino Mail * Enable translated values in the server. * Fixed an issue where toast activation can't find the clicked mail after the folder is initialized. * Revert debug code. * Change server background sync to every 3 minute and Inbox only synchronization. * Revert google auth changes. * App preferences page. * Changing tray icon visibility on preference change. * Start the server with invisible tray icon if set to invisible. * Reconnect button on the title bar. * Handling of toast actions. * Enable x86 build for server during packaging. * Get rid of old background tasks and v180 migration. * Terminate client when Exit clicked in server. * Introducing SynchronizationSource to prevent notifying UI after server tick synchronization. * Remove confirmAppClose restricted capability and unused debug code in manifest. * Closing the reconnect info popup when reconnect is clicked. * Custom RetryHandler for OutlookSynchronizer and separating client/server logs. * Running server on Windows startup. * Fix startup exe. * Fix for expander list view item paddings. * Force full sync on app launch instead of Inbox. * Fix draft creation. * Fix an issue with custom folder sync logic. * Reporting back account sync progress from server. * Fix sending drafts and missing notifications for imap. * Changing imap folder sync requirements. * Retain file count is set to 3. * Disabled swipe gestures temporarily due to native crash with SwipeControl * Save all attachments implementation. * Localization for save all attachments button. * Fix logging dates for logs. * Fixing ARM64 build. * Add ARM64 build config to packaging project. * Comment out OutOfProcPDB for ARM64. * Hnadling GONE response for Outlook folder synchronization.
This commit is contained in:
@@ -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<IBackgroundSynchronizer>();
|
||||
var dbService = providere.GetService<IDatabaseService>();
|
||||
var logInitializer = providere.GetService<ILogInitializer>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,25 +18,6 @@
|
||||
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<AllowCrossPlatformRetargeting>false</AllowCrossPlatformRetargeting>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ namespace Wino.Core.Domain.Entities
|
||||
|
||||
public Guid AccountId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique object storage for authenticators if needed.
|
||||
/// </summary>
|
||||
public string UniqueId { get; set; }
|
||||
public string Address { get; set; }
|
||||
|
||||
public void RefreshTokens(TokenInformationBase tokenInformationBase)
|
||||
|
||||
12
Wino.Core.Domain/Enums/ServerBackgroundMode.cs
Normal file
12
Wino.Core.Domain/Enums/ServerBackgroundMode.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Wino.Core.Domain.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// What should happen to server app when the client is terminated.
|
||||
/// </summary>
|
||||
public enum ServerBackgroundMode
|
||||
{
|
||||
MinimizedTray, // Still runs, tray icon is visible.
|
||||
Invisible, // Still runs, tray icon is invisible.
|
||||
Terminate // Server is terminated as Wino terminates.
|
||||
}
|
||||
}
|
||||
11
Wino.Core.Domain/Enums/StartupBehaviorResult.cs
Normal file
11
Wino.Core.Domain/Enums/StartupBehaviorResult.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Wino.Core.Domain.Enums
|
||||
{
|
||||
public enum StartupBehaviorResult
|
||||
{
|
||||
Enabled,
|
||||
Disabled,
|
||||
DisabledByUser,
|
||||
DisabledByPolicy,
|
||||
Fatal
|
||||
}
|
||||
}
|
||||
12
Wino.Core.Domain/Enums/SynchronizationSource.cs
Normal file
12
Wino.Core.Domain/Enums/SynchronizationSource.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Wino.Core.Domain.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// Enumeration for the source of synchronization.
|
||||
/// Right now it can either be from the client or the server.
|
||||
/// </summary>
|
||||
public enum SynchronizationSource
|
||||
{
|
||||
Client,
|
||||
Server
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
MailListPage,
|
||||
ReadComposePanePage,
|
||||
LanguageTimePage,
|
||||
AppPreferencesPage,
|
||||
SettingOptionsPage,
|
||||
}
|
||||
}
|
||||
|
||||
11
Wino.Core.Domain/Enums/WinoServerConnectionStatus.cs
Normal file
11
Wino.Core.Domain/Enums/WinoServerConnectionStatus.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace Wino.Core.Domain.Enums
|
||||
{
|
||||
public enum WinoServerConnectionStatus
|
||||
{
|
||||
None,
|
||||
Connecting,
|
||||
Connected,
|
||||
Disconnected,
|
||||
Failed
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Wino.Core.Domain.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// An exception thrown when the background task execution policies are denied for some reason.
|
||||
/// </summary>
|
||||
public class BackgroundTaskExecutionRequestDeniedException : Exception { }
|
||||
}
|
||||
12
Wino.Core.Domain/Exceptions/WinoServerException.cs
Normal file
12
Wino.Core.Domain/Exceptions/WinoServerException.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Wino.Core.Domain.Exceptions
|
||||
{
|
||||
/// <summary>
|
||||
/// All server crash types. Wino Server ideally should not throw anything else than this Exception type.
|
||||
/// </summary>
|
||||
public class WinoServerException : Exception
|
||||
{
|
||||
public WinoServerException(string message) : base(message) { }
|
||||
}
|
||||
}
|
||||
20
Wino.Core.Domain/Extensions/MimeExtensions.cs
Normal file
20
Wino.Core.Domain/Extensions/MimeExtensions.cs
Normal file
@@ -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)));
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
public interface IAppInitializerService
|
||||
{
|
||||
string GetApplicationDataFolder();
|
||||
string GetPublisherSharedFolder();
|
||||
|
||||
Task MigrateAsync();
|
||||
}
|
||||
}
|
||||
21
Wino.Core.Domain/Interfaces/IApplicationConfiguration.cs
Normal file
21
Wino.Core.Domain/Interfaces/IApplicationConfiguration.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface IApplicationConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Application data folder.
|
||||
/// </summary>
|
||||
string ApplicationDataFolderPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Publisher shared folder path.
|
||||
/// </summary>
|
||||
string PublisherSharedFolderPath { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="expectedAccountAddress">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.</param>
|
||||
/// <returns>Freshly created TokenInformation..</returns>
|
||||
Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken);
|
||||
|
||||
/// <summary>
|
||||
/// Required for external authorization on launched browser to continue.
|
||||
/// Used for Gmail.
|
||||
/// </summary>
|
||||
/// <param name="authorizationResponseUri">Response's redirect uri.</param>
|
||||
void ContinueAuthorization(Uri authorizationResponseUri);
|
||||
|
||||
/// <summary>
|
||||
/// For external browser required authentications.
|
||||
/// Canceling Gmail authentication dialog etc.
|
||||
/// </summary>
|
||||
void CancelAuthorization();
|
||||
|
||||
/// <summary>
|
||||
/// ClientId in case of needed for authorization/authentication.
|
||||
/// </summary>
|
||||
|
||||
6
Wino.Core.Domain/Interfaces/IAuthenticatorTypes.cs
Normal file
6
Wino.Core.Domain/Interfaces/IAuthenticatorTypes.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
public interface IOutlookAuthenticator : IAuthenticator { }
|
||||
public interface IGmailAuthenticator : IAuthenticator { }
|
||||
public interface IImapAuthenticator : IAuthenticator { }
|
||||
}
|
||||
54
Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
Normal file
54
Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Account that is assigned for this synchronizer.
|
||||
/// </summary>
|
||||
MailAccount Account { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Synchronizer state.
|
||||
/// </summary>
|
||||
AccountSynchronizerState State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Queues a single request to be executed in the next synchronization.
|
||||
/// </summary>
|
||||
/// <param name="request">Request to queue.</param>
|
||||
void QueueRequest(IRequestBase request);
|
||||
|
||||
/// <summary>
|
||||
/// TODO
|
||||
/// </summary>
|
||||
/// <returns>Whether active synchronization is stopped or not.</returns>
|
||||
bool CancelActiveSynchronization();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="options">Options for synchronization.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result summary of synchronization.</returns>
|
||||
Task<SynchronizationResult> SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a single MIME message from the server and saves it to disk.
|
||||
/// </summary>
|
||||
/// <param name="mailItem">Mail item to download from server.</param>
|
||||
/// <param name="transferProgress">Optional progress reporting for download operation.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
8
Wino.Core.Domain/Interfaces/IClientMessage.cs
Normal file
8
Wino.Core.Domain/Interfaces/IClientMessage.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// All messages that Client sends to Server and awaits a response in return.
|
||||
/// For example; triggering a new synchronization request.
|
||||
/// </summary>
|
||||
public interface IClientMessage;
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
public interface ILogInitializer
|
||||
{
|
||||
void SetupLogger(string logFolderPath);
|
||||
void SetupLogger(string fullLogFilePath);
|
||||
|
||||
void RefreshLoggingLevel();
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
Task<MailCopy> GetSingleMailItemAsync(string mailCopyId, string remoteFolderId);
|
||||
Task<MailCopy> GetSingleMailItemAsync(Guid uniqueMailId);
|
||||
Task<MailCopy> CreateDraftAsync(MailAccount composerAccount, MimeMessage generatedReplyMime, MimeMessage replyingMimeMessage = null, IMailItem replyingMailItem = null);
|
||||
Task<MailCopy> CreateDraftAsync(MailAccount composerAccount, string generatedReplyMimeMessageBase64, MimeMessage replyingMimeMessage = null, IMailItem replyingMailItem = null);
|
||||
Task<List<IMailItem>> FetchMailsAsync(MailListInitializationOptions options);
|
||||
|
||||
/// <summary>
|
||||
@@ -51,7 +51,16 @@ namespace Wino.Core.Domain.Interfaces
|
||||
/// <param name="newThreadId"></param>
|
||||
Task MapLocalDraftAsync(string newMailCopyId, string newDraftId, string newThreadId);
|
||||
|
||||
Task<MimeMessage> CreateDraftMimeMessageAsync(Guid accountId, DraftCreationOptions options);
|
||||
/// <summary>
|
||||
/// Creates a draft message with the given options.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account to create draft for.</param>
|
||||
/// <param name="options">Draft creation options.</param>
|
||||
/// <returns>
|
||||
/// Base64 encoded string of MimeMessage object.
|
||||
/// This is mainly for serialization purposes.
|
||||
/// </returns>
|
||||
Task<string> CreateDraftMimeBase64Async(Guid accountId, DraftCreationOptions options);
|
||||
Task UpdateMailAsync(MailCopy mailCopy);
|
||||
|
||||
/// <summary>
|
||||
@@ -92,5 +101,14 @@ namespace Wino.Core.Domain.Interfaces
|
||||
/// </summary>
|
||||
/// <param name="folderId">Folder id to get unread mails for.</param>
|
||||
Task<List<MailCopy>> GetUnreadMailsByFolderIdAsync(Guid folderId);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="messageId">Message id</param>
|
||||
/// <param name="folderId">Folder's local id.</param>
|
||||
/// <returns>Whether mail exists in the folder or not.</returns>
|
||||
Task<bool> IsMailExistsAsync(string mailCopyId, Guid folderId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,20 @@ namespace Wino.Core.Domain.Interfaces
|
||||
Task<string> GetEditorBundlePathAsync();
|
||||
Task LaunchFileAsync(string filePath);
|
||||
Task LaunchUriAsync(Uri uri);
|
||||
|
||||
/// <summary>
|
||||
/// Launches the default browser with the specified uri and waits for protocol activation to finish.
|
||||
/// </summary>
|
||||
/// <param name="authenticator"></param>
|
||||
/// <returns>Response callback from the browser.</returns>
|
||||
Task<Uri> GetAuthorizationResponseUriAsync(IAuthenticator authenticator, string authorizationUri);
|
||||
|
||||
/// <summary>
|
||||
/// Finalizes GetAuthorizationResponseUriAsync for current IAuthenticator.
|
||||
/// </summary>
|
||||
/// <param name="authorizationResponseUri"></param>
|
||||
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.
|
||||
/// </summary>
|
||||
GoogleAuthorizationRequest GetGoogleAuthorizationRequest();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
Func<IntPtr> GetCoreWindowHwnd { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
bool AutoSelectNextItem { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Setting: Gets or sets what should happen to server app when the client is terminated.
|
||||
/// </summary>
|
||||
ServerBackgroundMode ServerTerminationBehavior { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Wino.Core.Domain.Interfaces
|
||||
TRequest NativeRequest { get; }
|
||||
}
|
||||
|
||||
public interface IRequestBase
|
||||
public interface IRequestBase : IClientMessage
|
||||
{
|
||||
/// <summary>
|
||||
/// Synchronizer option to perform.
|
||||
|
||||
20
Wino.Core.Domain/Interfaces/IStartupBehaviorService.cs
Normal file
20
Wino.Core.Domain/Interfaces/IStartupBehaviorService.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
public interface IStartupBehaviorService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets whether Wino Server is set to launch on startup or not.
|
||||
/// </summary>
|
||||
Task<StartupBehaviorResult> GetCurrentStartupBehaviorAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Enables/disables the current startup behavior for Wino Server.
|
||||
/// </summary>
|
||||
/// <param name="isEnabled">Whether to launch enabled or disabled.</param>
|
||||
/// <returns>True if operation success, false if not.</returns>
|
||||
Task<StartupBehaviorResult> ToggleStartupBehavior(bool isEnabled);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public interface ISynchronizationProgress
|
||||
{
|
||||
/// <summary>
|
||||
/// Reports account synchronization progress.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account id for the report.</param>
|
||||
/// <param name="progress">Value. This is always between 0 - 100</param>
|
||||
void AccountProgressUpdated(Guid accountId, int progress);
|
||||
}
|
||||
}
|
||||
11
Wino.Core.Domain/Interfaces/ISynchronizerFactory.cs
Normal file
11
Wino.Core.Domain/Interfaces/ISynchronizerFactory.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
public interface ISynchronizerFactory
|
||||
{
|
||||
Task<IBaseSynchronizer> GetAccountSynchronizerAsync(Guid accountId);
|
||||
Task InitializeAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Wino.Core.Domain.Models.Requests
|
||||
namespace Wino.Core.Domain.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
|
||||
public interface IUIMessage;
|
||||
}
|
||||
60
Wino.Core.Domain/Interfaces/IWinoServerConnectionManager.cs
Normal file
60
Wino.Core.Domain/Interfaces/IWinoServerConnectionManager.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// When the connection status changes, this event will be triggered.
|
||||
/// </summary>
|
||||
event EventHandler<WinoServerConnectionStatus> StatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the connection status.
|
||||
/// </summary>
|
||||
WinoServerConnectionStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <returns>Whether connection is established or not.</returns>
|
||||
Task<bool> ConnectAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects from existing connection and disposes the connection.
|
||||
/// </summary>
|
||||
/// <returns>Whether disconnection is succesfull or not.</returns>
|
||||
Task<bool> DisconnectAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Queues a new user request to be processed by Wino Server.
|
||||
/// Healthy connection must present before calling this method.
|
||||
/// </summary>
|
||||
/// <param name="request">Request to queue for synchronizer in the server.</param>
|
||||
/// <param name="accountId">Account id to queueu request for.</param>
|
||||
Task QueueRequestAsync(IRequestBase request, Guid accountId);
|
||||
|
||||
/// <summary>
|
||||
/// Returns response from server for the given request.
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">Response type.</typeparam>
|
||||
/// <typeparam name="TRequestType">Request type.</typeparam>
|
||||
/// <param name="clientMessage">Request type.</param>
|
||||
/// <returns>Response received from the server for the given TResponse type.</returns>
|
||||
Task<WinoServerResponse<TResponse>> GetResponseAsync<TResponse, TRequestType>(TRequestType clientMessage) where TRequestType : IClientMessage;
|
||||
}
|
||||
|
||||
public interface IWinoServerConnectionManager<TAppServiceConnection> : IWinoServerConnectionManager, IInitializeAsync
|
||||
{
|
||||
/// <summary>
|
||||
/// Existing connection handle to the server of TAppServiceConnection type.
|
||||
/// </summary>
|
||||
TAppServiceConnection Connection { get; set; }
|
||||
}
|
||||
}
|
||||
11
Wino.Core.Domain/Interfaces/IWinoSynchronizerFactory.cs
Normal file
11
Wino.Core.Domain/Interfaces/IWinoSynchronizerFactory.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a single rule for toggling user actions if needed.
|
||||
16
Wino.Core.Domain/Models/Reader/WebViewMessage.cs
Normal file
16
Wino.Core.Domain/Models/Reader/WebViewMessage.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Reader
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to pass messages from the webview to the app.
|
||||
/// </summary>
|
||||
public class WebViewMessage
|
||||
{
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
[JsonProperty("value")]
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
||||
18
Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs
Normal file
18
Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Requests
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Encapsulates request to queue and account for synchronizer.
|
||||
/// </summary>
|
||||
/// <param name="AccountId"><inheritdoc/></param>
|
||||
/// <param name="Request"></param>
|
||||
/// <param name="Request">Prepared request for the server.</param>
|
||||
/// <param name="AccountId">Whihc account to execute this request for.</param>
|
||||
public record ServerRequestPackage(Guid AccountId, IRequestBase Request) : IClientMessage
|
||||
{
|
||||
public override string ToString() => $"Server Package: {Request.GetType().Name}";
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
42
Wino.Core.Domain/Models/Server/WinoServerResponse.cs
Normal file
42
Wino.Core.Domain/Models/Server/WinoServerResponse.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Wino.Core.Domain.Exceptions;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Encapsulates responses from the Wino server.
|
||||
/// Exceptions are stored separately in the Message and StackTrace properties due to serialization issues.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the expected response.</typeparam>
|
||||
public class WinoServerResponse<T>
|
||||
{
|
||||
public bool IsSuccess { get; set; }
|
||||
public string Message { get; set; }
|
||||
public T Data { get; set; }
|
||||
|
||||
// protected WinoServerResponse() { }
|
||||
|
||||
public static WinoServerResponse<T> CreateSuccessResponse(T data)
|
||||
{
|
||||
return new WinoServerResponse<T>
|
||||
{
|
||||
IsSuccess = true,
|
||||
Data = data
|
||||
};
|
||||
}
|
||||
|
||||
public static WinoServerResponse<T> CreateErrorResponse(string message)
|
||||
{
|
||||
return new WinoServerResponse<T>
|
||||
{
|
||||
IsSuccess = false,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
|
||||
public void ThrowIfFailed()
|
||||
{
|
||||
if (!IsSuccess)
|
||||
throw new WinoServerException(Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// </summary>
|
||||
public List<Guid> SynchronizationFolderIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A listener to be notified about the progress of the synchronization.
|
||||
/// </summary>
|
||||
public ISynchronizationProgress ProgressListener { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
|
||||
@@ -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() { }
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public IEnumerable<IMailItem> DownloadedMessages { get; set; } = new List<IMailItem>();
|
||||
public SynchronizationCompletedState CompletedState { get; set; }
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
}
|
||||
|
||||
125
Wino.Core.Domain/Translator.Designer.cs
generated
125
Wino.Core.Domain/Translator.Designer.cs
generated
@@ -548,6 +548,11 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
public static string Emoji => Resources.GetTranslatedString(@"Emoji");
|
||||
|
||||
/// <summary>
|
||||
/// Wino server failed.
|
||||
/// </summary>
|
||||
public static string Exception_WinoServerException => Resources.GetTranslatedString(@"Exception_WinoServerException");
|
||||
|
||||
/// <summary>
|
||||
/// Couldn't find mailbox settings.
|
||||
/// </summary>
|
||||
@@ -1698,6 +1703,11 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
public static string Right => Resources.GetTranslatedString(@"Right");
|
||||
|
||||
/// <summary>
|
||||
/// Save all attachments
|
||||
/// </summary>
|
||||
public static string Reader_SaveAllAttachmentButtonText => Resources.GetTranslatedString(@"Reader_SaveAllAttachmentButtonText");
|
||||
|
||||
/// <summary>
|
||||
/// up to date
|
||||
/// </summary>
|
||||
@@ -2113,6 +2123,71 @@ namespace Wino.Core.Domain
|
||||
/// </summary>
|
||||
public static string SettingsManageAccountSettings_Title => Resources.GetTranslatedString(@"SettingsManageAccountSettings_Title");
|
||||
|
||||
/// <summary>
|
||||
/// App Preferences
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_Title => Resources.GetTranslatedString(@"SettingsAppPreferences_Title");
|
||||
|
||||
/// <summary>
|
||||
/// General settings / preferences for Wino Mail.
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_Description => Resources.GetTranslatedString(@"SettingsAppPreferences_Description");
|
||||
|
||||
/// <summary>
|
||||
/// Application close behavior
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_CloseBehavior_Title => Resources.GetTranslatedString(@"SettingsAppPreferences_CloseBehavior_Title");
|
||||
|
||||
/// <summary>
|
||||
/// What should happen when you close the app?
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_CloseBehavior_Description => Resources.GetTranslatedString(@"SettingsAppPreferences_CloseBehavior_Description");
|
||||
|
||||
/// <summary>
|
||||
/// Start minimized on Windows startup
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_StartupBehavior_Title => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_Title");
|
||||
|
||||
/// <summary>
|
||||
/// Allow Wino Mail to launch minimized when Windows starts. Always allow it to receive notifications.
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_StartupBehavior_Description => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_Description");
|
||||
|
||||
/// <summary>
|
||||
/// Wino Mail successfully set to be launched in the background on Windows startup.
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_StartupBehavior_Enabled => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_Enabled");
|
||||
|
||||
/// <summary>
|
||||
/// Wino Mail will not be launched on Windows startup. This will cause you to miss notifications when you restart your computer.
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_StartupBehavior_Disabled => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_Disabled");
|
||||
|
||||
/// <summary>
|
||||
/// Your administrator or group policies disabled running applications on startup. Thus, Wino Mail can't be set to launch on Windows startup.
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_StartupBehavior_DisabledByPolicy => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_DisabledByPolicy");
|
||||
|
||||
/// <summary>
|
||||
/// Please go to Task Manager -> Startup tab to allow Wino Mail to launch on Windows startup.
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_StartupBehavior_DisabledByUser => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_DisabledByUser");
|
||||
|
||||
/// <summary>
|
||||
/// Fatal error occurred while changing the startup mode for Wino Mail.
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_StartupBehavior_FatalError => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_FatalError");
|
||||
|
||||
/// <summary>
|
||||
/// Enable
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_StartupBehavior_Enable => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_Enable");
|
||||
|
||||
/// <summary>
|
||||
/// Disable
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_StartupBehavior_Disable => Resources.GetTranslatedString(@"SettingsAppPreferences_StartupBehavior_Disable");
|
||||
|
||||
/// <summary>
|
||||
/// Reorder Accounts
|
||||
/// </summary>
|
||||
@@ -2642,5 +2717,55 @@ namespace Wino.Core.Domain
|
||||
/// None
|
||||
/// </summary>
|
||||
public static string SettingsSignature_NoneSignatureName => Resources.GetTranslatedString(@"SettingsSignature_NoneSignatureName");
|
||||
|
||||
/// <summary>
|
||||
/// Minimize to system tray
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Title => Resources.GetTranslatedString(@"SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Title");
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description => Resources.GetTranslatedString(@"SettingsAppPreferences_ServerBackgroundingMode_MinimizeTray_Description");
|
||||
|
||||
/// <summary>
|
||||
/// Run in the background
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title => Resources.GetTranslatedString(@"SettingsAppPreferences_ServerBackgroundingMode_Invisible_Title");
|
||||
|
||||
/// <summary>
|
||||
/// Wino Mail will keep running in the background. You will be notified as new mails arrive.
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description => Resources.GetTranslatedString(@"SettingsAppPreferences_ServerBackgroundingMode_Invisible_Description");
|
||||
|
||||
/// <summary>
|
||||
/// Terminate
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title => Resources.GetTranslatedString(@"SettingsAppPreferences_ServerBackgroundingMode_Terminate_Title");
|
||||
|
||||
/// <summary>
|
||||
/// Wino Mail will not keep running anywhere. You will not be notified as new mails arrive. Launch Wino Mail again to continue mail synchronization.
|
||||
/// </summary>
|
||||
public static string SettingsAppPreferences_ServerBackgroundingMode_Terminate_Description => Resources.GetTranslatedString(@"SettingsAppPreferences_ServerBackgroundingMode_Terminate_Description");
|
||||
|
||||
/// <summary>
|
||||
/// no connection
|
||||
/// </summary>
|
||||
public static string TitleBarServerDisconnectedButton_Title => Resources.GetTranslatedString(@"TitleBarServerDisconnectedButton_Title");
|
||||
|
||||
/// <summary>
|
||||
/// Wino is disconnected from the network. Click reconnect to restore connection.
|
||||
/// </summary>
|
||||
public static string TitleBarServerDisconnectedButton_Description => Resources.GetTranslatedString(@"TitleBarServerDisconnectedButton_Description");
|
||||
|
||||
/// <summary>
|
||||
/// reconnect
|
||||
/// </summary>
|
||||
public static string TitleBarServerReconnectButton_Title => Resources.GetTranslatedString(@"TitleBarServerReconnectButton_Title");
|
||||
|
||||
/// <summary>
|
||||
/// connecting
|
||||
/// </summary>
|
||||
public static string TitleBarServerReconnectingButton_Title => Resources.GetTranslatedString(@"TitleBarServerReconnectingButton_Title");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,19 @@
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<Platforms>AnyCPU;x64;x86</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Models\Communication\**" />
|
||||
<EmbeddedResource Remove="Models\Communication\**" />
|
||||
<None Remove="Models\Communication\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Interfaces\IWinoSynchronizerFactory.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Translations\ca_ES\resources.json" />
|
||||
<None Remove="Translations\cs_CZ\resources.json" />
|
||||
@@ -49,9 +60,11 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MimeKit" Version="4.4.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.7.1" />
|
||||
<PackageReference Include="MailKit" Version="4.7.1.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.4" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
|
||||
|
||||
@@ -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<IWinoServerConnectionManager>(serverConnectionManager);
|
||||
services.AddSingleton<IWinoServerConnectionManager<AppServiceConnection>>(serverConnectionManager);
|
||||
|
||||
services.AddSingleton<IUnderlyingThemeService, UnderlyingThemeService>();
|
||||
services.AddSingleton<INativeAppService, NativeAppService>();
|
||||
services.AddSingleton<IStoreManagementService, StoreManagementService>();
|
||||
services.AddSingleton<IBackgroundTaskService, BackgroundTaskService>();
|
||||
|
||||
services.AddTransient<IAppInitializerService, AppInitializerService>();
|
||||
|
||||
services.AddTransient<IConfigurationService, ConfigurationService>();
|
||||
services.AddTransient<IFileService, FileService>();
|
||||
services.AddTransient<IStoreRatingService, StoreRatingService>();
|
||||
services.AddTransient<IKeyPressService, KeyPressService>();
|
||||
services.AddTransient<IBackgroundSynchronizer, BackgroundSynchronizer>();
|
||||
services.AddTransient<INotificationBuilder, NotificationBuilder>();
|
||||
services.AddTransient<IClipboardService, ClipboardService>();
|
||||
services.AddTransient<IStartupBehaviorService, StartupBehaviorService>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
Wino.Core.UWP/Extensions/StartupTaskStateExtensions.cs
Normal file
25
Wino.Core.UWP/Extensions/StartupTaskStateExtensions.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private async Task MoveExistingDatabaseToSharedCacheFolderAsync()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Service responsible for handling background synchronization on timer and session connected events.
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string> registeredBackgroundTaskNames = new List<string>();
|
||||
|
||||
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<bool>(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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Uri> authorizationCompletedTaskSource;
|
||||
|
||||
public string GetWebAuthenticationBrokerUri() => WebAuthenticationBroker.GetCurrentApplicationCallbackUri().AbsoluteUri;
|
||||
public Func<IntPtr> GetCoreWindowHwnd { get; set; }
|
||||
|
||||
public string GetWebAuthenticationBrokerUri()
|
||||
{
|
||||
#if WINDOWS_UWP
|
||||
return WebAuthenticationBroker.GetCurrentApplicationCallbackUri().AbsoluteUri;
|
||||
#endif
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public async Task<string> 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<Uri> GetAuthorizationResponseUriAsync(IAuthenticator authenticator, string authorizationUri)
|
||||
{
|
||||
if (authorizationCompletedTaskSource != null)
|
||||
{
|
||||
authorizationCompletedTaskSource.TrySetException(new AuthenticationException(Translator.Exception_AuthenticationCanceled));
|
||||
authorizationCompletedTaskSource = null;
|
||||
}
|
||||
|
||||
authorizationCompletedTaskSource = new TaskCompletionSource<Uri>();
|
||||
|
||||
await LaunchUriAsync(new Uri(authorizationUri));
|
||||
|
||||
return await authorizationCompletedTaskSource.Task;
|
||||
}
|
||||
|
||||
public void ContinueAuthorization(Uri authorizationResponseUri)
|
||||
{
|
||||
if (authorizationCompletedTaskSource != null)
|
||||
{
|
||||
authorizationCompletedTaskSource.TrySetResult(authorizationResponseUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
Wino.Core.UWP/Services/StartupBehaviorService.cs
Normal file
37
Wino.Core.UWP/Services/StartupBehaviorService.cs
Normal file
@@ -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<StartupBehaviorResult> ToggleStartupBehavior(bool isEnabled)
|
||||
{
|
||||
var task = await StartupTask.GetAsync(WinoServerTaskId);
|
||||
|
||||
if (isEnabled)
|
||||
{
|
||||
await task.RequestEnableAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
task.Disable();
|
||||
}
|
||||
|
||||
return await GetCurrentStartupBehaviorAsync();
|
||||
}
|
||||
|
||||
public async Task<StartupBehaviorResult> GetCurrentStartupBehaviorAsync()
|
||||
{
|
||||
var task = await StartupTask.GetAsync(WinoServerTaskId);
|
||||
|
||||
return task.State.AsStartupBehaviorResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
316
Wino.Core.UWP/Services/WinoServerConnectionManager.cs
Normal file
316
Wino.Core.UWP/Services/WinoServerConnectionManager.cs
Normal file
@@ -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<AppServiceConnection>,
|
||||
IRecipient<WinoServerConnectionEstrablished>
|
||||
{
|
||||
private const int ServerConnectionTimeoutMs = 5000;
|
||||
|
||||
public event EventHandler<WinoServerConnectionStatus> StatusChanged;
|
||||
private TaskCompletionSource<bool> _connectionTaskCompletionSource;
|
||||
|
||||
private ILogger Logger => Logger.ForContext<WinoServerConnectionManager>();
|
||||
|
||||
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<bool> ConnectAsync()
|
||||
{
|
||||
if (Status == WinoServerConnectionStatus.Connected) return true;
|
||||
|
||||
if (ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0))
|
||||
{
|
||||
try
|
||||
{
|
||||
_connectionTaskCompletionSource ??= new TaskCompletionSource<bool>();
|
||||
|
||||
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<bool> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unpacks IServerMessage objects and delegate it to Messenger for UI to process.
|
||||
/// </summary>
|
||||
/// <param name="messageJson">Message data in json format.</param>
|
||||
private void HandleUIMessage(string messageJson, string typeName)
|
||||
{
|
||||
switch (typeName)
|
||||
{
|
||||
case nameof(MailAddedMessage):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MailAddedMessage>(messageJson));
|
||||
break;
|
||||
case nameof(MailDownloadedMessage):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MailDownloadedMessage>(messageJson));
|
||||
break;
|
||||
case nameof(MailRemovedMessage):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MailRemovedMessage>(messageJson));
|
||||
break;
|
||||
case nameof(MailUpdatedMessage):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MailUpdatedMessage>(messageJson));
|
||||
break;
|
||||
case nameof(AccountCreatedMessage):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountCreatedMessage>(messageJson));
|
||||
break;
|
||||
case nameof(AccountRemovedMessage):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountRemovedMessage>(messageJson));
|
||||
break;
|
||||
case nameof(AccountUpdatedMessage):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountUpdatedMessage>(messageJson));
|
||||
break;
|
||||
case nameof(DraftCreated):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<DraftCreated>(messageJson));
|
||||
break;
|
||||
case nameof(DraftFailed):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<DraftFailed>(messageJson));
|
||||
break;
|
||||
case nameof(DraftMapped):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<DraftMapped>(messageJson));
|
||||
break;
|
||||
case nameof(FolderRenamed):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<FolderRenamed>(messageJson));
|
||||
break;
|
||||
case nameof(FolderSynchronizationEnabled):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<FolderSynchronizationEnabled>(messageJson));
|
||||
break;
|
||||
case nameof(MergedInboxRenamed):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<MergedInboxRenamed>(messageJson));
|
||||
break;
|
||||
case nameof(AccountSynchronizationCompleted):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountSynchronizationCompleted>(messageJson));
|
||||
break;
|
||||
case nameof(RefreshUnreadCountsMessage):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<RefreshUnreadCountsMessage>(messageJson));
|
||||
break;
|
||||
case nameof(AccountSynchronizerStateChanged):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountSynchronizerStateChanged>(messageJson));
|
||||
break;
|
||||
case nameof(AccountSynchronizationProgressUpdatedMessage):
|
||||
WeakReferenceMessenger.Default.Send(JsonSerializer.Deserialize<AccountSynchronizationProgressUpdatedMessage>(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<bool, ServerRequestPackage>(queuePackage, new Dictionary<string, object>()
|
||||
{
|
||||
{ MessageConstants.MessageDataRequestAccountIdKey, accountId }
|
||||
});
|
||||
|
||||
queueResponse.ThrowIfFailed();
|
||||
}
|
||||
|
||||
public Task<WinoServerResponse<TResponse>> GetResponseAsync<TResponse, TRequestType>(TRequestType message) where TRequestType : IClientMessage
|
||||
=> GetResponseInternalAsync<TResponse, TRequestType>(message);
|
||||
|
||||
private async Task<WinoServerResponse<TResponse>> GetResponseInternalAsync<TResponse, TRequestType>(TRequestType message, Dictionary<string, object> parameters = null)
|
||||
{
|
||||
if (Connection == null)
|
||||
return WinoServerResponse<TResponse>.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<TResponse>.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<TResponse>.CreateErrorResponse($"Failed to send message to server.\n{serverSendException.Message}");
|
||||
}
|
||||
|
||||
// It should be always Success.
|
||||
if (response.Status != AppServiceResponseStatus.Success)
|
||||
return WinoServerResponse<TResponse>.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<TResponse>.CreateErrorResponse("Server response did not contain message data.");
|
||||
|
||||
// Try deserialize the message data.
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<WinoServerResponse<TResponse>>(messageJson);
|
||||
}
|
||||
catch (Exception jsonDeserializationError)
|
||||
{
|
||||
Logger.Error(jsonDeserializationError, $"Failed to deserialize server response message data.");
|
||||
return WinoServerResponse<TResponse>.CreateErrorResponse($"Failed to deserialize Wino server response message data.\n{jsonDeserializationError.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Receive(WinoServerConnectionEstrablished message)
|
||||
{
|
||||
if (_connectionTaskCompletionSource != null)
|
||||
{
|
||||
_connectionTaskCompletionSource.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,25 +17,6 @@
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<ProjectTypeGuids>{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE;NETFX_CORE;WINDOWS_UWP</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|x86'">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
@@ -118,17 +99,21 @@
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<RestoreProjectStyle>PackageReference</RestoreProjectStyle>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="CoreUWPContainerSetup.cs" />
|
||||
<Compile Include="Dispatcher.cs" />
|
||||
<Compile Include="Extensions\ElementThemeExtensions.cs" />
|
||||
<Compile Include="Extensions\StartupTaskStateExtensions.cs" />
|
||||
<Compile Include="Models\Personalization\CustomAppTheme.cs" />
|
||||
<Compile Include="Models\Personalization\PreDefinedAppTheme.cs" />
|
||||
<Compile Include="Models\Personalization\SystemAppTheme.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Services\AppInitializerService.cs" />
|
||||
<Compile Include="Services\BackgroundSynchronizer.cs" />
|
||||
<Compile Include="Services\PreferencesService.cs" />
|
||||
<Compile Include="Services\StartupBehaviorService.cs" />
|
||||
<Compile Include="Services\StatePersistenceService.cs" />
|
||||
<Compile Include="Services\WinoServerConnectionManager.cs" />
|
||||
<Compile Include="Services\BackgroundTaskService.cs" />
|
||||
<Compile Include="Services\ClipboardService.cs" />
|
||||
<Compile Include="Services\ConfigurationService.cs" />
|
||||
@@ -143,9 +128,9 @@
|
||||
<EmbeddedResource Include="Properties\Wino.Core.UWP.rd.xml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!--<PackageReference Include="CommunityToolkit.Uwp.Helpers">
|
||||
<Version>8.0.230907</Version>
|
||||
</PackageReference>-->
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Notifications">
|
||||
<Version>7.1.2</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AppCenter.Analytics">
|
||||
<Version>5.0.4</Version>
|
||||
</PackageReference>
|
||||
@@ -155,9 +140,6 @@
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp">
|
||||
<Version>7.1.3</Version>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications">
|
||||
<Version>7.1.3</Version>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj">
|
||||
@@ -168,8 +150,16 @@
|
||||
<Project>{e6b1632a-8901-41e8-9ddf-6793c7698b0b}</Project>
|
||||
<Name>Wino.Core</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj">
|
||||
<Project>{0c307d7e-256f-448c-8265-5622a812fbcc}</Project>
|
||||
<Name>Wino.Messaging</Name>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<SDKReference Include="WindowsDesktop, Version=10.0.22621.0">
|
||||
<Name>Windows Desktop Extensions for the UWP</Name>
|
||||
</SDKReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
<PropertyGroup Condition=" '$(VisualStudioVersion)' == '' or '$(VisualStudioVersion)' < '14.0' ">
|
||||
<VisualStudioVersion>14.0</VisualStudioVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -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<string> InteractiveAuthenticationRequired;
|
||||
|
||||
public void CancelAuthorization() { }
|
||||
|
||||
public void ContinueAuthorization(Uri authorizationResponseUri) { }
|
||||
|
||||
public Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<TokenInformation> GetTokenAsync(MailAccount account)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Uri> _authorizationCompletionSource = null;
|
||||
private CancellationTokenSource _authorizationCancellationTokenSource = null;
|
||||
|
||||
private readonly INativeAppService _nativeAppService;
|
||||
|
||||
public event EventHandler<string> InteractiveAuthenticationRequired;
|
||||
@@ -99,8 +93,6 @@ namespace Wino.Core.Authenticators
|
||||
};
|
||||
}
|
||||
|
||||
public void ContinueAuthorization(Uri authorizationResponseUri) => _authorizationCompletionSource?.TrySetResult(authorizationResponseUri);
|
||||
|
||||
public async Task<TokenInformation> 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<Uri>();
|
||||
_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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// Authenticator for Outlook provider.
|
||||
/// Token cache is managed by MSAL, not by Wino.
|
||||
/// </summary>
|
||||
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<TokenInformation> 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<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken)
|
||||
|
||||
@@ -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<string> InteractiveAuthenticationRequired;
|
||||
|
||||
public void CancelAuthorization()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void ContinueAuthorization(Uri authorizationResponseUri)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<TokenInformation> GenerateTokenAsync(MailAccount account, bool saveToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task<TokenInformation> GetTokenAsync(MailAccount account)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ILogInitializer, LogInitializer>();
|
||||
|
||||
services.AddSingleton<IApplicationConfiguration, ApplicationConfiguration>();
|
||||
services.AddSingleton<ITranslationService, TranslationService>();
|
||||
services.AddSingleton<IDatabaseService, DatabaseService>();
|
||||
services.AddSingleton<IWinoSynchronizerFactory, WinoSynchronizerFactory>();
|
||||
services.AddSingleton<IThreadingStrategyProvider, ThreadingStrategyProvider>();
|
||||
services.AddSingleton<IMimeFileService, MimeFileService>();
|
||||
|
||||
@@ -42,9 +43,14 @@ namespace Wino.Core
|
||||
services.AddTransient<IFontService, FontService>();
|
||||
services.AddTransient<IUnsubscriptionService, UnsubscriptionService>();
|
||||
|
||||
services.AddTransient<IOutlookAuthenticator, OutlookAuthenticator>();
|
||||
services.AddTransient<IGmailAuthenticator, GmailAuthenticator>();
|
||||
|
||||
services.AddTransient<OutlookThreadingStrategy>();
|
||||
services.AddTransient<GmailThreadingStrategy>();
|
||||
services.AddTransient<ImapThreadStrategy>();
|
||||
|
||||
services.AddSingleton<ISynchronizerFactory, SynchronizerFactory>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
49
Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs
Normal file
49
Wino.Core/Integration/Json/ServerRequestTypeInfoResolver.cs
Normal file
@@ -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<JsonTypeInfo>(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)),
|
||||
}
|
||||
};
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,15 @@ namespace Wino.Core.Integration.Processors
|
||||
/// <returns>Whether the mime has b</returns>
|
||||
Task<bool> IsMailExistsAsync(string messageId);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="messageId">Message id</param>
|
||||
/// <param name="folderId">Folder's local id.</param>
|
||||
/// <returns>Whether mail exists in the folder or not.</returns>
|
||||
Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId);
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <param name="synchronizationIdentifier">New synchronization identifier.</param>
|
||||
/// <returns>New identifier if success.</returns>
|
||||
Task UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string deltaSynchronizationIdentifier);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="folderId">Local folder id to reset token for.</param>
|
||||
/// <returns>Empty string to assign folder delta sync for.</returns>
|
||||
Task<string> ResetFolderDeltaTokenAsync(Guid folderId);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="accountId">Account identifier to reset delta token for.</param>
|
||||
/// <returns>Empty string to assign account delta sync for.</returns>
|
||||
Task<string> 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<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string synchronizationDeltaIdentifier)
|
||||
=> _accountService.UpdateSynchronizationIdentifierAsync(accountId, synchronizationDeltaIdentifier);
|
||||
=> AccountService.UpdateSynchronizationIdentifierAsync(accountId, synchronizationDeltaIdentifier);
|
||||
|
||||
public Task ChangeFlagStatusAsync(string mailCopyId, bool isFlagged)
|
||||
=> MailService.ChangeFlagStatusAsync(mailCopyId, isFlagged);
|
||||
|
||||
@@ -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<bool> IsMailExistsAsync(string messageId)
|
||||
=> MailService.IsMailExistsAsync(messageId);
|
||||
|
||||
public Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId)
|
||||
=> MailService.IsMailExistsAsync(messageId, folderId);
|
||||
|
||||
public Task<string> ResetAccountDeltaTokenAsync(Guid accountId)
|
||||
=> AccountService.UpdateSynchronizationIdentifierAsync(accountId, null);
|
||||
|
||||
public async Task<string> 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);
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Wino.Core.Messages.Mails
|
||||
{
|
||||
public record RefreshUnreadCountsMessage(Guid AccountId);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Synchronizers;
|
||||
|
||||
namespace Wino.Core.Messages.Synchronization
|
||||
{
|
||||
/// <summary>
|
||||
/// Emitted when synchronizer state is updated.
|
||||
/// </summary>
|
||||
/// <param name="synchronizer">Account Synchronizer</param>
|
||||
/// <param name="newState">New state.</param>
|
||||
public record AccountSynchronizerStateChanged(IBaseSynchronizer Synchronizer, AccountSynchronizerState NewState);
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
23
Wino.Core/Requests/Bundles/ServerRequestPackage.cs
Normal file
23
Wino.Core/Requests/Bundles/ServerRequestPackage.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Requests
|
||||
{
|
||||
///// <summary>
|
||||
///// Encapsulates request to queue and account for synchronizer.
|
||||
///// </summary>
|
||||
///// <param name="AccountId"><inheritdoc/></param>
|
||||
///// <param name="Request"></param>
|
||||
///// <param name="Request">Prepared request for the server.</param>
|
||||
///// <param name="AccountId">Which account to execute this request for.</param>
|
||||
public class ServerRequestBundle(Guid accountId, IRequestBase request) : IClientMessage
|
||||
{
|
||||
public Guid AccountId { get; } = accountId;
|
||||
|
||||
public IRequestBase Request { get; } = request;
|
||||
}
|
||||
|
||||
|
||||
//public record ServerRequestPackage<TUserActionRequestType>(Guid AccountId, TUserActionRequestType Request)
|
||||
// : ServerRequestBundle(AccountId), IClientMessage where TUserActionRequestType : IRequestBase;
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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<BatchMarkReadRequest>(Request.MailItem, MailSynchronizerOperation.Send),
|
||||
: RequestBase<BatchSendDraftRequestRequest>(Request.MailItem, MailSynchronizerOperation.Send),
|
||||
ICustomFolderSynchronizationRequest
|
||||
{
|
||||
public List<Guid> SynchronizationFolderIds
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
13
Wino.Core/Services/ApplicationConfiguration.cs
Normal file
13
Wino.Core/Services/ApplicationConfiguration.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<MailCopy> 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<MimeMessage> CreateDraftMimeMessageAsync(Guid accountId, DraftCreationOptions draftCreationOptions)
|
||||
public async Task<string> 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<bool> IsMailExistsAsync(string mailCopyId)
|
||||
=> Connection.ExecuteScalarAsync<bool>("SELECT EXISTS(SELECT 1 FROM MailCopy WHERE Id = ?)", mailCopyId);
|
||||
|
||||
public Task<bool> IsMailExistsAsync(string mailCopyId, Guid folderId)
|
||||
=> Connection.ExecuteScalarAsync<bool>("SELECT EXISTS(SELECT 1 FROM MailCopy WHERE Id = ? AND FolderId = ?)", mailCopyId, folderId);
|
||||
}
|
||||
}
|
||||
|
||||
101
Wino.Core/Services/SynchronizerFactory.cs
Normal file
101
Wino.Core/Services/SynchronizerFactory.cs
Normal file
@@ -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<IBaseSynchronizer> 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<IBaseSynchronizer> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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<WinoRequestDelegator>();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<IRequest>();
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Account that is assigned for this synchronizer.
|
||||
/// </summary>
|
||||
MailAccount Account { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Synchronizer state.
|
||||
/// </summary>
|
||||
AccountSynchronizerState State { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Queues a single request to be executed in the next synchronization.
|
||||
/// </summary>
|
||||
/// <param name="request">Request to queue.</param>
|
||||
void QueueRequest(IRequestBase request);
|
||||
|
||||
/// <summary>
|
||||
/// TODO
|
||||
/// </summary>
|
||||
/// <returns>Whether active synchronization is stopped or not.</returns>
|
||||
bool CancelActiveSynchronization();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="options">Options for synchronization.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Result summary of synchronization.</returns>
|
||||
Task<SynchronizationResult> SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Downloads a single MIME message from the server and saves it to disk.
|
||||
/// </summary>
|
||||
/// <param name="mailItem">Mail item to download from server.</param>
|
||||
/// <param name="transferProgress">Optional progress reporting for download operation.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
Task DownloadMissingMimeMessageAsync(IMailItem mailItem, ITransferProgress transferProgress, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public abstract class BaseSynchronizer<TBaseRequest, TMessageType> : BaseMailIntegrator<TBaseRequest>, 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));
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// <returns>New synchronization options with minimal HTTP effort.</returns>
|
||||
private SynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(IEnumerable<IRequestBase> requests)
|
||||
{
|
||||
bool isAllCustomSynchronizationRequests = requests.All(a => a is ICustomFolderSynchronizationRequest);
|
||||
List<Guid> synchronizationFolderIds = new();
|
||||
|
||||
if (requests.All(a => a is IBatchChangeRequest))
|
||||
{
|
||||
var requestsInsideBatches = requests.Cast<IBatchChangeRequest>().SelectMany(b => b.Items);
|
||||
|
||||
// Gather FolderIds to synchronize.
|
||||
synchronizationFolderIds = requestsInsideBatches
|
||||
.Where(a => a is ICustomFolderSynchronizationRequest)
|
||||
.Cast<ICustomFolderSynchronizationRequest>()
|
||||
.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<ICustomFolderSynchronizationRequest>().SelectMany(a => a.SynchronizationFolderIds).ToList();
|
||||
options.SynchronizationFolderIds = synchronizationFolderIds;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
/// <param name="messageIds">Gmail message ids to download.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
private async Task BatchDownloadMessagesAsync(IEnumerable<string> messageIds, ISynchronizationProgress progressListener = null, CancellationToken cancellationToken = default)
|
||||
private async Task BatchDownloadMessagesAsync(IEnumerable<string> 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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -407,21 +407,19 @@ namespace Wino.Core.Synchronizers
|
||||
|
||||
public override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// options.Type = SynchronizationType.FoldersOnly;
|
||||
|
||||
var downloadedMessageIds = new List<string>();
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var downloadedMessageIds = new List<string>();
|
||||
@@ -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<string, object> additionalData)
|
||||
=> additionalData != null && additionalData.ContainsKey("@removed");
|
||||
|
||||
private bool IsResourceUpdated(IDictionary<string, object> additionalData)
|
||||
=> additionalData == null || !additionalData.Any();
|
||||
|
||||
private async Task<bool> 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<MailFolder, Microsoft.Graph.Me.MailFolders.Delta.DeltaGetResponse>.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));
|
||||
|
||||
@@ -5,8 +5,13 @@
|
||||
<RootNamespace>Wino.Core</RootNamespace>
|
||||
<Configurations>Debug;Release</Configurations>
|
||||
<LangVersion>12</LangVersion>
|
||||
<Platforms>AnyCPU;x64;x86</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="WinoSynchronizerFactory.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" />
|
||||
@@ -18,11 +23,13 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MailKit" Version="4.6.0" />
|
||||
<PackageReference Include="MailKit" Version="4.7.1.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Graph" Version="5.55.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.47.2" />
|
||||
<PackageReference Include="MimeKit" Version="4.6.0" />
|
||||
<PackageReference Include="Microsoft.Graph" Version="5.56.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client" Version="4.62.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.62.0" />
|
||||
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.62.0" />
|
||||
<PackageReference Include="MimeKit" Version="4.7.1" />
|
||||
<PackageReference Include="morelinq" Version="4.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />
|
||||
@@ -33,10 +40,10 @@
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.8" />
|
||||
<PackageReference Include="SqlKata" Version="2.4.0" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||
<PackageReference Include="Xamarin.Essentials" Version="1.8.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
|
||||
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user