402 lines
17 KiB
C#
402 lines
17 KiB
C#
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.Core.Synchronizers;
|
|
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>,
|
|
IRecipient<NewSynchronizationRequested>
|
|
{
|
|
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()
|
|
};
|
|
|
|
private Task imapIdleTask = null;
|
|
|
|
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)
|
|
{
|
|
imapIdleTask = RegisterImapSynchronizerChangesAsync();
|
|
}
|
|
else
|
|
{
|
|
Log.Error("Opening server connection failed. Status: {status}", status);
|
|
|
|
DisposeConnection();
|
|
}
|
|
}
|
|
|
|
private async Task PerformForAllImapSynchronizers(Action<ImapSynchronizer> action)
|
|
{
|
|
var allAccounts = await _accountService.GetAccountsAsync().ConfigureAwait(false);
|
|
|
|
var imapAccounts = allAccounts.FindAll(a => a.ProviderType == MailProviderType.IMAP4);
|
|
|
|
foreach (var account in imapAccounts)
|
|
{
|
|
var synchronizer = await _synchronizerFactory.GetAccountSynchronizerAsync(account.Id).ConfigureAwait(false);
|
|
|
|
if (synchronizer == null) continue;
|
|
if (synchronizer is not ImapSynchronizer accountImapSynchronizer)
|
|
{
|
|
Log.Warning("Account '{Name}' has IMAP4 type but synchronizer is not ImapSynchronizer.", account.Name);
|
|
continue;
|
|
}
|
|
|
|
action(accountImapSynchronizer);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Hooks all ImapSynchronizer instances to listen for changes like new mail, folder rename etc.
|
|
/// </summary>
|
|
private Task RegisterImapSynchronizerChangesAsync()
|
|
=> PerformForAllImapSynchronizers(async accountImapSynchronizer =>
|
|
{
|
|
// First make sure that listening is stopped.
|
|
|
|
await accountImapSynchronizer.StopInboxListeningAsync();
|
|
|
|
var startListeningTask = accountImapSynchronizer.StartInboxListeningAsync();
|
|
|
|
await Task.Delay(10000); // Wait for 10 seconds.
|
|
|
|
if (startListeningTask.Exception == null)
|
|
{
|
|
Log.Information("IMAP change listening started for account '{Name}'.", accountImapSynchronizer.Account.Name);
|
|
}
|
|
});
|
|
|
|
public Task DisposeActiveImapConnectionsAsync()
|
|
=> PerformForAllImapSynchronizers(async accountImapSynchronizer =>
|
|
{
|
|
await accountImapSynchronizer.KillAsync();
|
|
});
|
|
|
|
/// <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)
|
|
{
|
|
// 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);
|
|
}
|
|
|
|
public async void Receive(NewSynchronizationRequested message)
|
|
{
|
|
await ExecuteServerMessageSafeAsync(null, message);
|
|
}
|
|
}
|
|
}
|