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:
Burak Kaan Köse
2024-08-05 00:36:26 +02:00
committed by GitHub
parent 4dc225184d
commit ff77b2b3dc
275 changed files with 4986 additions and 2381 deletions

10
Wino.Server/App.xaml Normal file
View File

@@ -0,0 +1,10 @@
<Application
x:Class="Wino.Server.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
ShutdownMode="OnExplicitShutdown"
xmlns:local="clr-namespace:Wino.Server">
<Application.Resources>
<ResourceDictionary Source="TrayIconResources.xaml" />
</Application.Resources>
</Application>

215
Wino.Server/App.xaml.cs Normal file
View File

@@ -0,0 +1,215 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using H.NotifyIcon;
using Microsoft.Extensions.DependencyInjection;
using Windows.Storage;
using Wino.Core;
using Wino.Core.Domain;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Services;
using Wino.Core.UWP.Services;
using Wino.Server.Core;
using Wino.Server.MessageHandlers;
using Wino.Services;
namespace Wino.Server
{
/// <summary>
/// Single instance Wino Server.
/// Instancing is done using Mutex.
/// App will not start if another instance is already running.
/// App will let running server know that server execution is triggered, which will
/// led server to start new connection to requesting UWP app.
/// </summary>
public partial class App : Application
{
[DllImport("user32.dll", SetLastError = true)]
static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
[DllImport("user32.dll", SetLastError = true)]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
private const string FRAME_WINDOW = "ApplicationFrameWindow";
public const string WinoMailLaunchProtocol = "wino.mail.launch";
private const string NotifyIconResourceKey = "NotifyIcon";
private const string WinoServerAppName = "Wino.Server";
private const string WinoServerActivatedName = "Wino.Server.Activated";
public new static App Current => (App)Application.Current;
private TaskbarIcon? notifyIcon;
private static Mutex _mutex = null;
private EventWaitHandle _eventWaitHandle;
public IServiceProvider Services { get; private set; }
private IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddTransient<ServerContext>();
services.AddTransient<ServerViewModel>();
services.RegisterCoreServices();
// Below services belongs to UWP.Core package and some APIs are not available for WPF.
// We register them here to avoid compilation errors.
services.AddSingleton<IConfigurationService, ConfigurationService>();
services.AddSingleton<INativeAppService, NativeAppService>();
services.AddSingleton<IPreferencesService, PreferencesService>();
services.AddTransient<INotificationBuilder, NotificationBuilder>();
services.AddTransient<IUnderlyingThemeService, UnderlyingThemeService>();
// Register server message handler factory.
var serverMessageHandlerFactory = new ServerMessageHandlerFactory();
serverMessageHandlerFactory.Setup(services);
services.AddSingleton<IServerMessageHandlerFactory>(serverMessageHandlerFactory);
return services.BuildServiceProvider();
}
private async Task<ServerViewModel> InitializeNewServerAsync()
{
// Make sure app config is setup before anything else.
var applicationFolderConfiguration = Services.GetService<IApplicationConfiguration>();
applicationFolderConfiguration.ApplicationDataFolderPath = ApplicationData.Current.LocalFolder.Path;
applicationFolderConfiguration.PublisherSharedFolderPath = ApplicationData.Current.GetPublisherCacheFolder(ApplicationConfiguration.SharedFolderName).Path;
// Setup logger
var logInitializer = Services.GetService<ILogInitializer>();
var logFilePath = Path.Combine(ApplicationData.Current.LocalFolder.Path, Constants.ServerLogFile);
logInitializer.SetupLogger(logFilePath);
// Make sure the database is ready.
var databaseService = Services.GetService<IDatabaseService>();
await databaseService.InitializeAsync();
// Setup core window handler for native app service.
// WPF app uses UWP app's window handle to display authentication dialog.
var nativeAppService = Services.GetService<INativeAppService>();
nativeAppService.GetCoreWindowHwnd = FindUWPClientWindowHandle;
// Initialize translations.
var translationService = Services.GetService<ITranslationService>();
await translationService.InitializeAsync();
// Make sure all accounts have synchronizers.
var synchronizerFactory = Services.GetService<ISynchronizerFactory>();
await synchronizerFactory.InitializeAsync();
// Load up the server view model.
var serverViewModel = Services.GetRequiredService<ServerViewModel>();
await serverViewModel.InitializeAsync();
return serverViewModel;
}
/// <summary>
/// OutlookAuthenticator for WAM requires window handle to display the dialog.
/// Since server app is windowless, we need to find the UWP app window handle.
/// </summary>
/// <param name="proc"></param>
/// <returns>Pointer to running UWP app's hwnd.</returns>
private IntPtr FindUWPClientWindowHandle()
{
// TODO: Resilient.
var proc = Process.GetProcessesByName("Wino")[0];
for (IntPtr appWindow = FindWindowEx(IntPtr.Zero, IntPtr.Zero, FRAME_WINDOW, null); appWindow != IntPtr.Zero;
appWindow = FindWindowEx(IntPtr.Zero, appWindow, FRAME_WINDOW, null))
{
IntPtr coreWindow = FindWindowEx(appWindow, IntPtr.Zero, "Windows.UI.Core.CoreWindow", null);
if (coreWindow != IntPtr.Zero)
{
GetWindowThreadProcessId(coreWindow, out var corePid);
if (corePid == proc.Id)
{
return appWindow;
}
}
}
return IntPtr.Zero;
}
protected override async void OnStartup(StartupEventArgs e)
{
_mutex = new Mutex(true, WinoServerAppName, out bool isCreatedNew);
_eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, WinoServerActivatedName);
if (isCreatedNew)
{
// Spawn a thread which will be waiting for our event
var thread = new Thread(() =>
{
while (_eventWaitHandle.WaitOne())
{
if (notifyIcon == null) return;
Current.Dispatcher.BeginInvoke(async () =>
{
if (notifyIcon.DataContext is ServerViewModel trayIconViewModel)
{
await trayIconViewModel.ReconnectAsync();
}
});
}
});
// It is important mark it as background otherwise it will prevent app from exiting.
thread.IsBackground = true;
thread.Start();
Services = ConfigureServices();
base.OnStartup(e);
var serverViewModel = await InitializeNewServerAsync();
// Create taskbar icon for the new server.
notifyIcon = (TaskbarIcon)FindResource(NotifyIconResourceKey);
notifyIcon.DataContext = serverViewModel;
notifyIcon.ForceCreate(enablesEfficiencyMode: true);
// Hide the icon if user has set it to invisible.
var preferencesService = Services.GetService<IPreferencesService>();
ChangeNotifyIconVisiblity(preferencesService.ServerTerminationBehavior != ServerBackgroundMode.Invisible);
}
else
{
// Notify other instance so it could reconnect to UWP app if needed.
_eventWaitHandle.Set();
// Terminate this instance.
Shutdown();
}
}
protected override void OnExit(ExitEventArgs e)
{
notifyIcon?.Dispose();
base.OnExit(e);
}
public void ChangeNotifyIconVisiblity(bool isVisible)
{
if (notifyIcon == null) return;
Current.Dispatcher.BeginInvoke(() =>
{
notifyIcon.Visibility = isVisible ? Visibility.Visible : Visibility.Collapsed;
});
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]

View File

@@ -0,0 +1,75 @@
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Windows.ApplicationModel.AppService;
using Windows.Foundation.Collections;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Server;
using Wino.Messaging;
namespace Wino.Server.Core
{
public abstract class ServerMessageHandlerBase
{
public string HandlingRequestType { get; }
public abstract Task ExecuteAsync(IClientMessage message, AppServiceRequest request = null, CancellationToken cancellationToken = default);
}
public abstract class ServerMessageHandler<TClientMessage, TResponse> : ServerMessageHandlerBase where TClientMessage : IClientMessage
{
/// <summary>
/// Response to return when server encounters and exception while executing code.
/// </summary>
/// <param name="ex">Exception that target threw.</param>
/// <returns>Default response on failure object.</returns>
public abstract WinoServerResponse<TResponse> FailureDefaultResponse(Exception ex);
/// <summary>
/// Safely executes the handler code and returns the response.
/// This call will never crash the server. Exceptions encountered will be handled and returned as response.
/// </summary>
/// <param name="message">IClientMessage that client asked the response for from the server.</param>
/// <param name="request">optional AppServiceRequest to return response for.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Response object that server executes for the given method.</returns>
public override async Task ExecuteAsync(IClientMessage message, AppServiceRequest request = null, CancellationToken cancellationToken = default)
{
WinoServerResponse<TResponse> response = default;
try
{
response = await HandleAsync((TClientMessage)message, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
response = FailureDefaultResponse(ex);
}
finally
{
// No need to send response if request is null.
// Handler might've been called directly from the server itself.
if (request != null)
{
var valueSet = new ValueSet()
{
{ MessageConstants.MessageDataKey, JsonSerializer.Serialize(response) }
};
await request.SendResponseAsync(valueSet);
}
}
}
/// <summary>
/// Code that will be executed directly on the server.
/// All handlers must implement this method.
/// Response is wrapped with WinoServerResponse.
/// </summary>
/// <param name="message">IClientMessage that client asked the response for from the server.</param>
/// <param name="cancellationToken">Cancellation token.</param>
protected abstract Task<WinoServerResponse<TResponse>> HandleAsync(TClientMessage message, CancellationToken cancellationToken = default);
}
}

View File

@@ -0,0 +1,41 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Messaging.Client.Authorization;
using Wino.Messaging.Server;
using Wino.Server.MessageHandlers;
namespace Wino.Server.Core
{
public class ServerMessageHandlerFactory : IServerMessageHandlerFactory
{
public ServerMessageHandlerBase GetHandler(string typeName)
{
return typeName switch
{
nameof(NewSynchronizationRequested) => App.Current.Services.GetService<SynchronizationRequestHandler>(),
nameof(ServerRequestPackage) => App.Current.Services.GetService<UserActionRequestHandler>(),
nameof(DownloadMissingMessageRequested) => App.Current.Services.GetService<SingleMimeDownloadHandler>(),
nameof(AuthorizationRequested) => App.Current.Services.GetService<AuthenticationHandler>(),
nameof(ProtocolAuthorizationCallbackReceived) => App.Current.Services.GetService<ProtocolAuthActivationHandler>(),
nameof(SynchronizationExistenceCheckRequest) => App.Current.Services.GetService<SyncExistenceHandler>(),
nameof(ServerTerminationModeChanged) => App.Current.Services.GetService<ServerTerminationModeHandler>(),
_ => throw new Exception($"Server handler for {typeName} is not registered."),
};
}
public void Setup(IServiceCollection serviceCollection)
{
// Register all known handlers.
serviceCollection.AddTransient<SynchronizationRequestHandler>();
serviceCollection.AddTransient<UserActionRequestHandler>();
serviceCollection.AddTransient<SingleMimeDownloadHandler>();
serviceCollection.AddTransient<AuthenticationHandler>();
serviceCollection.AddTransient<ProtocolAuthActivationHandler>();
serviceCollection.AddTransient<SyncExistenceHandler>();
serviceCollection.AddTransient<ServerTerminationModeHandler>();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -0,0 +1,12 @@
using Microsoft.Extensions.DependencyInjection;
using Wino.Server.Core;
namespace Wino.Server.MessageHandlers
{
public interface IServerMessageHandlerFactory
{
void Setup(IServiceCollection serviceCollection);
ServerMessageHandlerBase GetHandler(string typeName);
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Server;
using Wino.Messaging.Server;
using Wino.Server.Core;
namespace Wino.Server.MessageHandlers
{
public class AuthenticationHandler : ServerMessageHandler<AuthorizationRequested, TokenInformation>
{
private readonly IAuthenticationProvider _authenticationProvider;
public override WinoServerResponse<TokenInformation> FailureDefaultResponse(Exception ex)
=> WinoServerResponse<TokenInformation>.CreateErrorResponse(ex.Message);
public AuthenticationHandler(IAuthenticationProvider authenticationProvider)
{
_authenticationProvider = authenticationProvider;
}
protected override async Task<WinoServerResponse<TokenInformation>> HandleAsync(AuthorizationRequested message, CancellationToken cancellationToken = default)
{
var authenticator = _authenticationProvider.GetAuthenticator(message.MailProviderType);
// Do not save the token here. Call is coming from account creation and things are atomic there.
var generatedToken = await authenticator.GenerateTokenAsync(message.CreatedAccount, saveToken: false);
return WinoServerResponse<TokenInformation>.CreateSuccessResponse(generatedToken);
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Server;
using Wino.Messaging.Client.Authorization;
using Wino.Server.Core;
namespace Wino.Server.MessageHandlers
{
public class ProtocolAuthActivationHandler : ServerMessageHandler<ProtocolAuthorizationCallbackReceived, bool>
{
public override WinoServerResponse<bool> FailureDefaultResponse(Exception ex) => WinoServerResponse<bool>.CreateErrorResponse(ex.Message);
private readonly INativeAppService _nativeAppService;
public ProtocolAuthActivationHandler(INativeAppService nativeAppService)
{
_nativeAppService = nativeAppService;
}
protected override Task<WinoServerResponse<bool>> HandleAsync(ProtocolAuthorizationCallbackReceived message, CancellationToken cancellationToken = default)
{
_nativeAppService.ContinueAuthorization(message.AuthorizationResponseUri);
return Task.FromResult(WinoServerResponse<bool>.CreateSuccessResponse(true));
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Models.Server;
using Wino.Messaging.Server;
using Wino.Server.Core;
namespace Wino.Server.MessageHandlers
{
public class ServerTerminationModeHandler : ServerMessageHandler<ServerTerminationModeChanged, bool>
{
public override WinoServerResponse<bool> FailureDefaultResponse(Exception ex) => WinoServerResponse<bool>.CreateErrorResponse(ex.Message);
protected override Task<WinoServerResponse<bool>> HandleAsync(ServerTerminationModeChanged message, CancellationToken cancellationToken = default)
{
WeakReferenceMessenger.Default.Send(message);
return Task.FromResult(WinoServerResponse<bool>.CreateSuccessResponse(true));
}
}
}

View File

@@ -0,0 +1,31 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Server;
using Wino.Messaging.Server;
using Wino.Server.Core;
namespace Wino.Server.MessageHandlers
{
public class SingleMimeDownloadHandler : ServerMessageHandler<DownloadMissingMessageRequested, bool>
{
public override WinoServerResponse<bool> FailureDefaultResponse(Exception ex) => WinoServerResponse<bool>.CreateErrorResponse(ex.Message);
private readonly ISynchronizerFactory _synchronizerFactory;
public SingleMimeDownloadHandler(ISynchronizerFactory synchronizerFactory)
{
_synchronizerFactory = synchronizerFactory;
}
protected override async Task<WinoServerResponse<bool>> HandleAsync(DownloadMissingMessageRequested message, CancellationToken cancellationToken = default)
{
var synchronizer = await _synchronizerFactory.GetAccountSynchronizerAsync(message.AccountId);
// TODO: ITransferProgress support is lost.
await synchronizer.DownloadMissingMimeMessageAsync(message.MailItem, null, cancellationToken);
return WinoServerResponse<bool>.CreateSuccessResponse(true);
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Server;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Server.Core;
namespace Wino.Server.MessageHandlers
{
public class SyncExistenceHandler : ServerMessageHandler<SynchronizationExistenceCheckRequest, bool>
{
public override WinoServerResponse<bool> FailureDefaultResponse(Exception ex)
=> WinoServerResponse<bool>.CreateErrorResponse(ex.Message);
private readonly ISynchronizerFactory _synchronizerFactory;
public SyncExistenceHandler(ISynchronizerFactory synchronizerFactory)
{
_synchronizerFactory = synchronizerFactory;
}
protected override async Task<WinoServerResponse<bool>> HandleAsync(SynchronizationExistenceCheckRequest message, CancellationToken cancellationToken = default)
{
var synchronizer = await _synchronizerFactory.GetAccountSynchronizerAsync(message.AccountId);
return WinoServerResponse<bool>.CreateSuccessResponse(synchronizer.State != Wino.Core.Domain.Enums.AccountSynchronizerState.Idle);
}
}
}

View File

@@ -0,0 +1,96 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Server;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Messaging.Server;
using Wino.Messaging.UI;
using Wino.Server.Core;
namespace Wino.Server.MessageHandlers
{
/// <summary>
/// Handler for NewSynchronizationRequested from the client.
/// </summary>
public class SynchronizationRequestHandler : ServerMessageHandler<NewSynchronizationRequested, SynchronizationResult>
{
public override WinoServerResponse<SynchronizationResult> FailureDefaultResponse(Exception ex)
=> WinoServerResponse<SynchronizationResult>.CreateErrorResponse(ex.Message);
private readonly ISynchronizerFactory _synchronizerFactory;
private readonly INotificationBuilder _notificationBuilder;
private readonly IFolderService _folderService;
public SynchronizationRequestHandler(ISynchronizerFactory synchronizerFactory,
INotificationBuilder notificationBuilder,
IFolderService folderService)
{
_synchronizerFactory = synchronizerFactory;
_notificationBuilder = notificationBuilder;
_folderService = folderService;
}
protected override async Task<WinoServerResponse<SynchronizationResult>> HandleAsync(NewSynchronizationRequested message, CancellationToken cancellationToken = default)
{
var synchronizer = await _synchronizerFactory.GetAccountSynchronizerAsync(message.Options.AccountId);
// 1. Don't send message for sync completion when we execute requests.
// People are usually interested in seeing the notification after they trigger the synchronization.
// 2. Don't send message for sync completion when we are synchronizing from the server.
// It happens very common and there is no need to send a message for each synchronization.
bool shouldReportSynchronizationResult =
message.Options.Type != SynchronizationType.ExecuteRequests &&
message.Source == SynchronizationSource.Client;
try
{
var synchronizationResult = await synchronizer.SynchronizeAsync(message.Options, cancellationToken).ConfigureAwait(false);
if (synchronizationResult.DownloadedMessages.Any())
{
var accountInboxFolder = await _folderService.GetSpecialFolderByAccountIdAsync(message.Options.AccountId, SpecialFolderType.Inbox);
if (accountInboxFolder != null)
{
await _notificationBuilder.CreateNotificationsAsync(accountInboxFolder.Id, synchronizationResult.DownloadedMessages);
}
}
var isSynchronizationSucceeded = synchronizationResult.CompletedState == SynchronizationCompletedState.Success;
if (shouldReportSynchronizationResult)
{
var completedMessage = new AccountSynchronizationCompleted(message.Options.AccountId,
isSynchronizationSucceeded ? SynchronizationCompletedState.Success : SynchronizationCompletedState.Failed,
message.Options.GroupedSynchronizationTrackingId);
WeakReferenceMessenger.Default.Send(completedMessage);
}
return WinoServerResponse<SynchronizationResult>.CreateSuccessResponse(synchronizationResult);
}
// TODO: Following cases might always be thrown from server. Handle them properly.
//catch (AuthenticationAttentionException)
//{
// // TODO
// // await SetAccountAttentionAsync(accountId, AccountAttentionReason.InvalidCredentials);
//}
//catch (SystemFolderConfigurationMissingException)
//{
// // TODO
// // await SetAccountAttentionAsync(accountId, AccountAttentionReason.MissingSystemFolderConfiguration);
//}
catch (Exception)
{
throw;
}
}
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Server;
using Wino.Server.Core;
namespace Wino.Server.MessageHandlers
{
public class UserActionRequestHandler : ServerMessageHandler<ServerRequestPackage, bool>
{
private readonly ISynchronizerFactory _synchronizerFactory;
public override WinoServerResponse<bool> FailureDefaultResponse(Exception ex) => WinoServerResponse<bool>.CreateErrorResponse(ex.Message);
public UserActionRequestHandler(ISynchronizerFactory synchronizerFactory)
{
_synchronizerFactory = synchronizerFactory;
}
protected override async Task<WinoServerResponse<bool>> HandleAsync(ServerRequestPackage package, CancellationToken cancellationToken = default)
{
var synchronizer = await _synchronizerFactory.GetAccountSynchronizerAsync(package.AccountId);
synchronizer.QueueRequest(package.Request);
//if (package.QueueSynchronization)
//{
// var options = new SynchronizationOptions
// {
// AccountId = package.AccountId,
// Type = Wino.Core.Domain.Enums.SynchronizationType.ExecuteRequests
// };
// WeakReferenceMessenger.Default.Send(new NewSynchronizationRequested(options));
//}
return WinoServerResponse<bool>.CreateSuccessResponse(true);
}
}
}

View File

@@ -0,0 +1,343 @@
using System;
using System.Diagnostics;
using System.Text.Json;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
using Serilog;
using Windows.ApplicationModel;
using Windows.ApplicationModel.AppService;
using Windows.Foundation.Collections;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Integration.Json;
using Wino.Core.Services;
using Wino.Messaging;
using Wino.Messaging.Client.Authorization;
using Wino.Messaging.Enums;
using Wino.Messaging.Server;
using Wino.Messaging.UI;
using Wino.Server.MessageHandlers;
namespace Wino.Server
{
public class ServerContext :
IRecipient<AccountCreatedMessage>,
IRecipient<AccountUpdatedMessage>,
IRecipient<AccountRemovedMessage>,
IRecipient<DraftCreated>,
IRecipient<DraftFailed>,
IRecipient<DraftMapped>,
IRecipient<FolderRenamed>,
IRecipient<FolderSynchronizationEnabled>,
IRecipient<MailAddedMessage>,
IRecipient<MailDownloadedMessage>,
IRecipient<MailRemovedMessage>,
IRecipient<MailUpdatedMessage>,
IRecipient<MergedInboxRenamed>,
IRecipient<AccountSynchronizationCompleted>,
IRecipient<AccountSynchronizerStateChanged>,
IRecipient<RefreshUnreadCountsMessage>,
IRecipient<ServerTerminationModeChanged>,
IRecipient<AccountSynchronizationProgressUpdatedMessage>
{
private readonly System.Timers.Timer _timer;
private static object connectionLock = new object();
private AppServiceConnection connection = null;
private readonly IDatabaseService _databaseService;
private readonly IApplicationConfiguration _applicationFolderConfiguration;
private readonly ISynchronizerFactory _synchronizerFactory;
private readonly IServerMessageHandlerFactory _serverMessageHandlerFactory;
private readonly IAccountService _accountService;
private readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
{
TypeInfoResolver = new ServerRequestTypeInfoResolver()
};
public ServerContext(IDatabaseService databaseService,
IApplicationConfiguration applicationFolderConfiguration,
ISynchronizerFactory synchronizerFactory,
IServerMessageHandlerFactory serverMessageHandlerFactory,
IAccountService accountService)
{
// Setup timer for synchronization.
_timer = new System.Timers.Timer(1000 * 60 * 3); // 1 minute
_timer.Elapsed += SynchronizationTimerTriggered;
_databaseService = databaseService;
_applicationFolderConfiguration = applicationFolderConfiguration;
_synchronizerFactory = synchronizerFactory;
_serverMessageHandlerFactory = serverMessageHandlerFactory;
_accountService = accountService;
WeakReferenceMessenger.Default.RegisterAll(this);
_timer.Start();
}
private async void SynchronizationTimerTriggered(object sender, System.Timers.ElapsedEventArgs e)
{
// Send sync request for all accounts.
var accounts = await _accountService.GetAccountsAsync();
foreach (var account in accounts)
{
var options = new SynchronizationOptions
{
AccountId = account.Id,
Type = SynchronizationType.Inbox,
};
var request = new NewSynchronizationRequested(options, SynchronizationSource.Server);
await ExecuteServerMessageSafeAsync(null, request);
}
}
#region Message Handlers
public async void Receive(MailAddedMessage message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(AccountCreatedMessage message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(AccountUpdatedMessage message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(AccountRemovedMessage message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(DraftCreated message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(DraftFailed message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(DraftMapped message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(FolderRenamed message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(FolderSynchronizationEnabled message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(MailDownloadedMessage message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(MailRemovedMessage message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(MailUpdatedMessage message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(MergedInboxRenamed message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(AccountSynchronizationCompleted message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(RefreshUnreadCountsMessage message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(AccountSynchronizerStateChanged message) => await SendMessageAsync(MessageType.UIMessage, message);
public async void Receive(AccountSynchronizationProgressUpdatedMessage message) => await SendMessageAsync(MessageType.UIMessage, message);
#endregion
private string GetAppPackagFamilyName()
{
// If running as a standalone app, Package will throw exception.
// Return hardcoded value for debugging purposes.
// Connection will not be available in this case.
try
{
return Package.Current.Id.FamilyName;
}
catch (Exception)
{
return "Debug.Wino.Server.FamilyName";
}
}
/// <summary>
/// Open connection to UWP app service
/// </summary>
public async Task InitializeAppServiceConnectionAsync()
{
if (connection != null) DisposeConnection();
connection = new AppServiceConnection
{
AppServiceName = "WinoInteropService",
PackageFamilyName = GetAppPackagFamilyName()
};
connection.RequestReceived += OnWinRTMessageReceived;
connection.ServiceClosed += OnConnectionClosed;
AppServiceConnectionStatus status = await connection.OpenAsync();
if (status != AppServiceConnectionStatus.Success)
{
// TODO: Handle connection error
DisposeConnection();
}
}
/// <summary>
/// Disposes current connection to UWP app service.
/// </summary>
private void DisposeConnection()
{
lock (connectionLock)
{
if (connection == null) return;
connection.RequestReceived -= OnWinRTMessageReceived;
connection.ServiceClosed -= OnConnectionClosed;
connection.Dispose();
connection = null;
}
}
/// <summary>
/// Sends a serialized object to UWP application if connection exists with given type.
/// </summary>
/// <param name="messageType">Type of the message.</param>
/// <param name="message">IServerMessage object that will be serialized.</param>
/// <returns></returns>
/// <exception cref="ArgumentException">When the message is not IServerMessage.</exception>
private async Task SendMessageAsync(MessageType messageType, object message)
{
if (connection == null) return;
if (message is not IUIMessage serverMessage)
throw new ArgumentException("Server message must be a type of IUIMessage");
string json = JsonSerializer.Serialize(message);
var set = new ValueSet
{
{ MessageConstants.MessageTypeKey, (int)messageType },
{ MessageConstants.MessageDataKey, json },
{ MessageConstants.MessageDataTypeKey, message.GetType().Name }
};
await connection.SendMessageAsync(set);
}
private void OnConnectionClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
{
// TODO: Handle connection closed.
// UWP app might've been terminated or suspended.
// At this point, we must keep active synchronizations going, but connection is lost.
// As long as this process is alive, database will be kept updated, but no messages will be sent.
DisposeConnection();
}
private async void OnWinRTMessageReceived(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)
{
if (!args.Request.Message.TryGetValue(MessageConstants.MessageDataTypeKey, out object dataTypeObject) || dataTypeObject is not string dataTypeName)
throw new ArgumentException("Message data type is missing.");
if (messageType == MessageType.ServerMessage)
{
// Client is awaiting a response from server.
// ServerMessage calls are awaited on the server and response is returned back in the args.
await HandleServerMessageAsync(messageJson, dataTypeName, args).ConfigureAwait(false);
}
else if (messageType == MessageType.UIMessage)
throw new Exception("Received UIMessage from UWP. This is not expected.");
}
}
}
private async Task HandleServerMessageAsync(string messageJson, string typeName, AppServiceRequestReceivedEventArgs args)
{
switch (typeName)
{
case nameof(NewSynchronizationRequested):
Debug.WriteLine($"New synchronization requested.");
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize<NewSynchronizationRequested>(messageJson, _jsonSerializerOptions));
break;
case nameof(DownloadMissingMessageRequested):
Debug.WriteLine($"Download missing message requested.");
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize<DownloadMissingMessageRequested>(messageJson, _jsonSerializerOptions));
break;
case nameof(ServerRequestPackage):
var serverPackage = JsonSerializer.Deserialize<ServerRequestPackage>(messageJson, _jsonSerializerOptions);
Debug.WriteLine(serverPackage);
await ExecuteServerMessageSafeAsync(args, serverPackage);
break;
case nameof(AuthorizationRequested):
Debug.WriteLine($"Authorization requested.");
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize<AuthorizationRequested>(messageJson, _jsonSerializerOptions));
break;
case nameof(ProtocolAuthorizationCallbackReceived):
Debug.WriteLine($"Continuing authorization from protocol activation.");
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize<ProtocolAuthorizationCallbackReceived>(messageJson, _jsonSerializerOptions));
break;
case nameof(SynchronizationExistenceCheckRequest):
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize<SynchronizationExistenceCheckRequest>(messageJson, _jsonSerializerOptions));
break;
case nameof(ServerTerminationModeChanged):
await ExecuteServerMessageSafeAsync(args, JsonSerializer.Deserialize<ServerTerminationModeChanged>(messageJson, _jsonSerializerOptions));
break;
default:
Debug.WriteLine($"Missing handler for {typeName} in the server. Check ServerContext.cs - HandleServerMessageAsync.");
break;
}
}
/// <summary>
/// Executes ServerMessage coming from the UWP.
/// These requests are awaited and expected to return a response.
/// </summary>
/// <param name="args">App service request args.</param>
/// <param name="message">Message that client sent to server.</param>
private async Task ExecuteServerMessageSafeAsync(AppServiceRequestReceivedEventArgs args, IClientMessage message)
{
AppServiceDeferral deferral = args?.GetDeferral() ?? null;
try
{
var messageName = message.GetType().Name;
var handler = _serverMessageHandlerFactory.GetHandler(messageName);
await handler.ExecuteAsync(message, args?.Request ?? null).ConfigureAwait(false);
}
catch (Exception ex)
{
Log.Error(ex, "ExecuteServerMessageSafeAsync crashed.");
Debugger.Break();
}
finally
{
deferral?.Complete();
}
}
public void Receive(ServerTerminationModeChanged message)
{
var backgroundMode = message.ServerBackgroundMode;
bool isServerTrayIconVisible = backgroundMode == ServerBackgroundMode.MinimizedTray || backgroundMode == ServerBackgroundMode.Terminate;
App.Current.ChangeNotifyIconVisiblity(isServerTrayIconVisible);
}
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Windows.ApplicationModel;
using Windows.System;
using Wino.Core.Domain.Interfaces;
namespace Wino.Server
{
public partial class ServerViewModel : ObservableObject, IInitializeAsync
{
private readonly INotificationBuilder _notificationBuilder;
public ServerContext Context { get; }
public ServerViewModel(ServerContext serverContext, INotificationBuilder notificationBuilder)
{
Context = serverContext;
_notificationBuilder = notificationBuilder;
}
[RelayCommand]
public Task LaunchWinoAsync()
{
//var opt = new SynchronizationOptions()
//{
// Type = Wino.Core.Domain.Enums.SynchronizationType.Full,
// AccountId = Guid.Parse("b3620ce7-8a69-4d81-83d5-a94bbe177431")
//};
//var req = new NewSynchronizationRequested(opt, Wino.Core.Domain.Enums.SynchronizationSource.Server);
//WeakReferenceMessenger.Default.Send(req);
// return Task.CompletedTask;
return Launcher.LaunchUriAsync(new Uri($"{App.WinoMailLaunchProtocol}:")).AsTask();
//await _notificationBuilder.CreateNotificationsAsync(Guid.Empty, new List<IMailItem>()
//{
// new MailCopy(){ UniqueId = Guid.Parse("8f25d2a0-4448-4fee-96a9-c9b25a19e866")}
//});
}
/// <summary>
/// Shuts down the application.
/// </summary>
[RelayCommand]
public async Task ExitApplication()
{
// Find the running UWP app by AppDiagnosticInfo API and terminate it if possible.
var appDiagnosticInfos = await AppDiagnosticInfo.RequestInfoForPackageAsync(Package.Current.Id.FamilyName);
var clientDiagnosticInfo = appDiagnosticInfos.FirstOrDefault();
if (clientDiagnosticInfo == null)
{
Debug.WriteLine($"Wino Mail client is not running. Termination is skipped.");
}
else
{
var appResourceGroupInfo = clientDiagnosticInfo.GetResourceGroups().FirstOrDefault();
if (appResourceGroupInfo != null)
{
await appResourceGroupInfo.StartTerminateAsync();
Debug.WriteLine($"Wino Mail client is terminated succesfully.");
}
}
Application.Current.Shutdown();
}
public async Task ReconnectAsync() => await Context.InitializeAppServiceConnectionAsync();
public Task InitializeAsync() => Context.InitializeAppServiceConnectionAsync();
}
}

View File

@@ -0,0 +1,21 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:tb="http://www.hardcodet.net/taskbar"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:server="clr-namespace:Wino.Server">
<ContextMenu x:Shared="false" x:Key="SysTrayMenu">
<MenuItem Header="Launch Wino Mail" Command="{Binding LaunchWinoCommand}" />
<Separator />
<MenuItem Header="Exit" Command="{Binding ExitApplicationCommand}" />
</ContextMenu>
<tb:TaskbarIcon
x:Key="NotifyIcon"
IconSource="Images/Wino_Icon.ico"
ToolTipText="Wino Mail"
LeftClickCommand="{Binding LaunchWinoCommand}"
NoLeftClickDelay="True"
ContextMenu="{StaticResource SysTrayMenu}" />
</ResourceDictionary>

View File

@@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.22621.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<OutputType>WinExe</OutputType>
<GenerateAssemblyInfo>true</GenerateAssemblyInfo>
<UseWPF>true</UseWPF>
<ImportWindowsDesktopTargets>true</ImportWindowsDesktopTargets>
<CsWinRTComponent>true</CsWinRTComponent>
<CsWinRTWindowsMetadata>10.0.22621.0</CsWinRTWindowsMetadata>
<Platforms>x64;x86;ARM32;ARM64</Platforms>
</PropertyGroup>
<PropertyGroup>
<StartupObject>Wino.Server.App</StartupObject>
</PropertyGroup>
<ItemGroup>
<None Remove="Images\Wino_Icon.ico" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Wino.Core.UWP\Services\ConfigurationService.cs" Link="Services\ConfigurationService.cs" />
<Compile Include="..\Wino.Core.UWP\Services\NativeAppService.cs" Link="Services\NativeAppService.cs" />
<Compile Include="..\Wino.Core.UWP\Services\PreferencesService.cs" Link="Services\PreferencesService.cs" />
<Compile Include="..\Wino.Core.UWP\Services\NotificationBuilder.cs" Link="Services\NotificationBuilder.cs" />
<Compile Include="..\Wino.Core.UWP\Services\UnderlyingThemeService.cs" Link="Services\UnderlyingThemeService.cs" />
</ItemGroup>
<ItemGroup>
<Resource Include="Images\Wino_Icon.ico">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="H.NotifyIcon.Wpf" Version="2.1.0" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
<ProjectReference Include="..\Wino.Core\Wino.Core.csproj" />
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
</ItemGroup>
</Project>